更新前端静态网页获取方式,放弃使用后端获取api

This commit is contained in:
2025-09-09 10:47:51 +08:00
parent 6889ca37e5
commit 44a4f1bae1
25558 changed files with 2463152 additions and 153 deletions

2713
frontend/node_modules/tailwindcss/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

21
frontend/node_modules/tailwindcss/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Tailwind Labs, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

40
frontend/node_modules/tailwindcss/README.md generated vendored Normal file
View File

@@ -0,0 +1,40 @@
<p align="center">
<a href="https://tailwindcss.com" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg">
<img alt="Tailwind CSS" src="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg" width="350" height="70" style="max-width: 100%;">
</picture>
</a>
</p>
<p align="center">
A utility-first CSS framework for rapidly building custom user interfaces.
</p>
<p align="center">
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=main" alt="Build Status"></a>
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
<a href="https://github.com/tailwindcss/tailwindcss/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
</p>
---
## Documentation
For full documentation, visit [tailwindcss.com](https://tailwindcss.com/).
## Community
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
For casual chit-chat with others using the framework:
[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)
## Contributing
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.

1
frontend/node_modules/tailwindcss/base.css generated vendored Normal file
View File

@@ -0,0 +1 @@
@tailwind base;

3
frontend/node_modules/tailwindcss/colors.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { DefaultColors } from './types/generated/colors'
declare const colors: DefaultColors
export = colors

2
frontend/node_modules/tailwindcss/colors.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let colors = require('./lib/public/colors')
module.exports = (colors.__esModule ? colors : { default: colors }).default

1
frontend/node_modules/tailwindcss/components.css generated vendored Normal file
View File

@@ -0,0 +1 @@
@tailwind components;

3
frontend/node_modules/tailwindcss/defaultConfig.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { Config } from './types/config'
declare const config: Config
export = config

2
frontend/node_modules/tailwindcss/defaultConfig.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let defaultConfig = require('./lib/public/default-config')
module.exports = (defaultConfig.__esModule ? defaultConfig : { default: defaultConfig }).default

4
frontend/node_modules/tailwindcss/defaultTheme.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
import type { Config } from './types/config'
import { DefaultTheme } from './types/generated/default-theme'
declare const theme: Config['theme'] & DefaultTheme
export = theme

2
frontend/node_modules/tailwindcss/defaultTheme.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let defaultTheme = require('./lib/public/default-theme')
module.exports = (defaultTheme.__esModule ? defaultTheme : { default: defaultTheme }).default

4
frontend/node_modules/tailwindcss/loadConfig.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
import type { Config } from './types/config'
declare function loadConfig(path: string): Config
export = loadConfig

2
frontend/node_modules/tailwindcss/loadConfig.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let loadConfig = require('./lib/public/load-config')
module.exports = (loadConfig.__esModule ? loadConfig : { default: loadConfig }).default

4
frontend/node_modules/tailwindcss/nesting/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
import type { AcceptedPlugin, PluginCreator } from 'postcss'
declare const plugin: PluginCreator<AcceptedPlugin | string | void>
export = plugin

2
frontend/node_modules/tailwindcss/nesting/index.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let nesting = require('../lib/postcss-plugins/nesting')
module.exports = (nesting.__esModule ? nesting : { default: nesting }).default

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Anton Kastritskiy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,42 @@
{
"name": "lilconfig",
"version": "3.1.3",
"description": "A zero-dependency alternative to cosmiconfig",
"main": "src/index.js",
"types": "src/index.d.ts",
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules ./node_modules/.bin/jest --coverage",
"lint": "biome ci ./src",
"types": "tsc"
},
"keywords": [
"cosmiconfig",
"config",
"configuration",
"search"
],
"files": [
"src/index.*"
],
"repository": {
"type": "git",
"url": "https://github.com/antonk52/lilconfig"
},
"bugs": "https://github.com/antonk52/lilconfig/issues",
"author": "antonk52",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "^1.6.0",
"@types/jest": "^29.5.12",
"@types/node": "^14.18.63",
"@types/webpack-env": "^1.18.5",
"cosmiconfig": "^8.3.6",
"jest": "^29.7.0",
"typescript": "^5.3.3",
"uvu": "^0.5.6"
},
"funding": "https://github.com/sponsors/antonk52",
"engines": {
"node": ">=14"
}
}

View File

@@ -0,0 +1,98 @@
# Lilconfig ⚙️
[![npm version](https://badge.fury.io/js/lilconfig.svg)](https://badge.fury.io/js/lilconfig)
[![install size](https://packagephobia.now.sh/badge?p=lilconfig)](https://packagephobia.now.sh/result?p=lilconfig)
[![Coverage Status](https://coveralls.io/repos/github/antonk52/lilconfig/badge.svg)](https://coveralls.io/github/antonk52/lilconfig)
A zero-dependency alternative to [cosmiconfig](https://www.npmjs.com/package/cosmiconfig) with the same API.
## Installation
```sh
npm install lilconfig
```
## Usage
```js
import {lilconfig, lilconfigSync} from 'lilconfig';
// all keys are optional
const options = {
stopDir: '/Users/you/some/dir',
searchPlaces: ['package.json', 'myapp.conf.js'],
ignoreEmptySearchPlaces: false
}
lilconfig(
'myapp',
options // optional
).search() // Promise<LilconfigResult>
lilconfigSync(
'myapp',
options // optional
).load(pathToConfig) // LilconfigResult
/**
* LilconfigResult
* {
* config: any; // your config
* filepath: string;
* }
*/
```
## ESM
ESM configs can be loaded with **async API only**. Specifically `js` files in projects with `"type": "module"` in `package.json` or `mjs` files.
## Difference to `cosmiconfig`
Lilconfig does not intend to be 100% compatible with `cosmiconfig` but tries to mimic it where possible. The key difference is **no** support for yaml files out of the box(`lilconfig` attempts to parse files with no extension as JSON instead of YAML). You can still add the support for YAML files by providing a loader, see an [example](#yaml-loader) below.
### Options difference between the two.
|cosmiconfig option | lilconfig |
|------------------------|-----------|
|cache | ✅ |
|loaders | ✅ |
|ignoreEmptySearchPlaces | ✅ |
|packageProp | ✅ |
|searchPlaces | ✅ |
|stopDir | ✅ |
|transform | ✅ |
## Loaders examples
### Yaml loader
If you need the YAML support you can provide your own loader
```js
import {lilconfig} from 'lilconfig';
import yaml from 'yaml';
function loadYaml(filepath, content) {
return yaml.parse(content);
}
const options = {
loaders: {
'.yaml': loadYaml,
'.yml': loadYaml,
// loader for files with no extension
noExt: loadYaml
}
};
lilconfig('myapp', options)
.search()
.then(result => {
result // {config, filepath}
});
```
## Version correlation
- lilconig v1 → cosmiconfig v6
- lilconig v2 → cosmiconfig v7
- lilconig v3 → cosmiconfig v8

View File

@@ -0,0 +1,54 @@
export type LilconfigResult = null | {
filepath: string;
config: any;
isEmpty?: boolean;
};
interface OptionsBase {
cache?: boolean;
stopDir?: string;
searchPlaces?: string[];
ignoreEmptySearchPlaces?: boolean;
packageProp?: string | string[];
}
export type Transform =
| TransformSync
| ((result: LilconfigResult) => Promise<LilconfigResult>);
export type TransformSync = (result: LilconfigResult) => LilconfigResult;
type LoaderResult = any;
export type LoaderSync = (filepath: string, content: string) => LoaderResult;
export type Loader =
| LoaderSync
| ((filepath: string, content: string) => Promise<LoaderResult>);
export type Loaders = Record<string, Loader>;
export type LoadersSync = Record<string, LoaderSync>;
export interface Options extends OptionsBase {
loaders?: Loaders;
transform?: Transform;
}
export interface OptionsSync extends OptionsBase {
loaders?: LoadersSync;
transform?: TransformSync;
}
export declare const defaultLoadersSync: LoadersSync;
export declare const defaultLoaders: Loaders;
type ClearCaches = {
clearLoadCache: () => void;
clearSearchCache: () => void;
clearCaches: () => void;
};
type AsyncSearcher = {
search(searchFrom?: string): Promise<LilconfigResult>;
load(filepath: string): Promise<LilconfigResult>;
} & ClearCaches;
export declare function lilconfig(
name: string,
options?: Partial<Options>,
): AsyncSearcher;
type SyncSearcher = {
search(searchFrom?: string): LilconfigResult;
load(filepath: string): LilconfigResult;
} & ClearCaches;
export declare function lilconfigSync(
name: string,
options?: OptionsSync,
): SyncSearcher;

View File

@@ -0,0 +1,460 @@
// @ts-check
const path = require('path');
const fs = require('fs');
const os = require('os');
const url = require('url');
const fsReadFileAsync = fs.promises.readFile;
/** @type {(name: string, sync: boolean) => string[]} */
function getDefaultSearchPlaces(name, sync) {
return [
'package.json',
`.${name}rc.json`,
`.${name}rc.js`,
`.${name}rc.cjs`,
...(sync ? [] : [`.${name}rc.mjs`]),
`.config/${name}rc`,
`.config/${name}rc.json`,
`.config/${name}rc.js`,
`.config/${name}rc.cjs`,
...(sync ? [] : [`.config/${name}rc.mjs`]),
`${name}.config.js`,
`${name}.config.cjs`,
...(sync ? [] : [`${name}.config.mjs`]),
];
}
/**
* @type {(p: string) => string}
*
* see #17
* On *nix, if cwd is not under homedir,
* the last path will be '', ('/build' -> '')
* but it should be '/' actually.
* And on Windows, this will never happen. ('C:\build' -> 'C:')
*/
function parentDir(p) {
return path.dirname(p) || path.sep;
}
/** @type {import('./index').LoaderSync} */
const jsonLoader = (_, content) => JSON.parse(content);
// Use plain require in webpack context for dynamic import
const requireFunc =
typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
/** @type {import('./index').LoadersSync} */
const defaultLoadersSync = Object.freeze({
'.js': requireFunc,
'.json': requireFunc,
'.cjs': requireFunc,
noExt: jsonLoader,
});
module.exports.defaultLoadersSync = defaultLoadersSync;
/** @type {import('./index').Loader} */
const dynamicImport = async id => {
try {
const fileUrl = url.pathToFileURL(id).href;
const mod = await import(/* webpackIgnore: true */ fileUrl);
return mod.default;
} catch (e) {
try {
return requireFunc(id);
} catch (/** @type {any} */ requireE) {
if (
requireE.code === 'ERR_REQUIRE_ESM' ||
(requireE instanceof SyntaxError &&
requireE
.toString()
.includes('Cannot use import statement outside a module'))
) {
throw e;
}
throw requireE;
}
}
};
/** @type {import('./index').Loaders} */
const defaultLoaders = Object.freeze({
'.js': dynamicImport,
'.mjs': dynamicImport,
'.cjs': dynamicImport,
'.json': jsonLoader,
noExt: jsonLoader,
});
module.exports.defaultLoaders = defaultLoaders;
/**
* @param {string} name
* @param {import('./index').Options | import('./index').OptionsSync} options
* @param {boolean} sync
* @returns {Required<import('./index').Options | import('./index').OptionsSync>}
*/
function getOptions(name, options, sync) {
/** @type {Required<import('./index').Options>} */
const conf = {
stopDir: os.homedir(),
searchPlaces: getDefaultSearchPlaces(name, sync),
ignoreEmptySearchPlaces: true,
cache: true,
transform: x => x,
packageProp: [name],
...options,
loaders: {
...(sync ? defaultLoadersSync : defaultLoaders),
...options.loaders,
},
};
conf.searchPlaces.forEach(place => {
const key = path.extname(place) || 'noExt';
const loader = conf.loaders[key];
if (!loader) {
throw new Error(`Missing loader for extension "${place}"`);
}
if (typeof loader !== 'function') {
throw new Error(
`Loader for extension "${place}" is not a function: Received ${typeof loader}.`,
);
}
});
return conf;
}
/** @type {(props: string | string[], obj: Record<string, any>) => unknown} */
function getPackageProp(props, obj) {
if (typeof props === 'string' && props in obj) return obj[props];
return (
(Array.isArray(props) ? props : props.split('.')).reduce(
(acc, prop) => (acc === undefined ? acc : acc[prop]),
obj,
) || null
);
}
/** @param {string} filepath */
function validateFilePath(filepath) {
if (!filepath) throw new Error('load must pass a non-empty string');
}
/** @type {(loader: import('./index').Loader, ext: string) => void} */
function validateLoader(loader, ext) {
if (!loader) throw new Error(`No loader specified for extension "${ext}"`);
if (typeof loader !== 'function') throw new Error('loader is not a function');
}
/** @type {(enableCache: boolean) => <T>(c: Map<string, T>, filepath: string, res: T) => T} */
const makeEmplace = enableCache => (c, filepath, res) => {
if (enableCache) c.set(filepath, res);
return res;
};
/** @type {import('./index').lilconfig} */
module.exports.lilconfig = function lilconfig(name, options) {
const {
ignoreEmptySearchPlaces,
loaders,
packageProp,
searchPlaces,
stopDir,
transform,
cache,
} = getOptions(name, options ?? {}, false);
const searchCache = new Map();
const loadCache = new Map();
const emplace = makeEmplace(cache);
return {
async search(searchFrom = process.cwd()) {
/** @type {import('./index').LilconfigResult} */
const result = {
config: null,
filepath: '',
};
/** @type {Set<string>} */
const visited = new Set();
let dir = searchFrom;
dirLoop: while (true) {
if (cache) {
const r = searchCache.get(dir);
if (r !== undefined) {
for (const p of visited) searchCache.set(p, r);
return r;
}
visited.add(dir);
}
for (const searchPlace of searchPlaces) {
const filepath = path.join(dir, searchPlace);
try {
await fs.promises.access(filepath);
} catch {
continue;
}
const content = String(await fsReadFileAsync(filepath));
const loaderKey = path.extname(searchPlace) || 'noExt';
const loader = loaders[loaderKey];
// handle package.json
if (searchPlace === 'package.json') {
const pkg = await loader(filepath, content);
const maybeConfig = getPackageProp(packageProp, pkg);
if (maybeConfig != null) {
result.config = maybeConfig;
result.filepath = filepath;
break dirLoop;
}
continue;
}
// handle other type of configs
const isEmpty = content.trim() === '';
if (isEmpty && ignoreEmptySearchPlaces) continue;
if (isEmpty) {
result.isEmpty = true;
result.config = undefined;
} else {
validateLoader(loader, loaderKey);
result.config = await loader(filepath, content);
}
result.filepath = filepath;
break dirLoop;
}
if (dir === stopDir || dir === parentDir(dir)) break dirLoop;
dir = parentDir(dir);
}
const transformed =
// not found
result.filepath === '' && result.config === null
? transform(null)
: transform(result);
if (cache) {
for (const p of visited) searchCache.set(p, transformed);
}
return transformed;
},
async load(filepath) {
validateFilePath(filepath);
const absPath = path.resolve(process.cwd(), filepath);
if (cache && loadCache.has(absPath)) {
return loadCache.get(absPath);
}
const {base, ext} = path.parse(absPath);
const loaderKey = ext || 'noExt';
const loader = loaders[loaderKey];
validateLoader(loader, loaderKey);
const content = String(await fsReadFileAsync(absPath));
if (base === 'package.json') {
const pkg = await loader(absPath, content);
return emplace(
loadCache,
absPath,
transform({
config: getPackageProp(packageProp, pkg),
filepath: absPath,
}),
);
}
/** @type {import('./index').LilconfigResult} */
const result = {
config: null,
filepath: absPath,
};
// handle other type of configs
const isEmpty = content.trim() === '';
if (isEmpty && ignoreEmptySearchPlaces)
return emplace(
loadCache,
absPath,
transform({
config: undefined,
filepath: absPath,
isEmpty: true,
}),
);
// cosmiconfig returns undefined for empty files
result.config = isEmpty ? undefined : await loader(absPath, content);
return emplace(
loadCache,
absPath,
transform(isEmpty ? {...result, isEmpty, config: undefined} : result),
);
},
clearLoadCache() {
if (cache) loadCache.clear();
},
clearSearchCache() {
if (cache) searchCache.clear();
},
clearCaches() {
if (cache) {
loadCache.clear();
searchCache.clear();
}
},
};
};
/** @type {import('./index').lilconfigSync} */
module.exports.lilconfigSync = function lilconfigSync(name, options) {
const {
ignoreEmptySearchPlaces,
loaders,
packageProp,
searchPlaces,
stopDir,
transform,
cache,
} = getOptions(name, options ?? {}, true);
const searchCache = new Map();
const loadCache = new Map();
const emplace = makeEmplace(cache);
return {
search(searchFrom = process.cwd()) {
/** @type {import('./index').LilconfigResult} */
const result = {
config: null,
filepath: '',
};
/** @type {Set<string>} */
const visited = new Set();
let dir = searchFrom;
dirLoop: while (true) {
if (cache) {
const r = searchCache.get(dir);
if (r !== undefined) {
for (const p of visited) searchCache.set(p, r);
return r;
}
visited.add(dir);
}
for (const searchPlace of searchPlaces) {
const filepath = path.join(dir, searchPlace);
try {
fs.accessSync(filepath);
} catch {
continue;
}
const loaderKey = path.extname(searchPlace) || 'noExt';
const loader = loaders[loaderKey];
const content = String(fs.readFileSync(filepath));
// handle package.json
if (searchPlace === 'package.json') {
const pkg = loader(filepath, content);
const maybeConfig = getPackageProp(packageProp, pkg);
if (maybeConfig != null) {
result.config = maybeConfig;
result.filepath = filepath;
break dirLoop;
}
continue;
}
// handle other type of configs
const isEmpty = content.trim() === '';
if (isEmpty && ignoreEmptySearchPlaces) continue;
if (isEmpty) {
result.isEmpty = true;
result.config = undefined;
} else {
validateLoader(loader, loaderKey);
result.config = loader(filepath, content);
}
result.filepath = filepath;
break dirLoop;
}
if (dir === stopDir || dir === parentDir(dir)) break dirLoop;
dir = parentDir(dir);
}
const transformed =
// not found
result.filepath === '' && result.config === null
? transform(null)
: transform(result);
if (cache) {
for (const p of visited) searchCache.set(p, transformed);
}
return transformed;
},
load(filepath) {
validateFilePath(filepath);
const absPath = path.resolve(process.cwd(), filepath);
if (cache && loadCache.has(absPath)) {
return loadCache.get(absPath);
}
const {base, ext} = path.parse(absPath);
const loaderKey = ext || 'noExt';
const loader = loaders[loaderKey];
validateLoader(loader, loaderKey);
const content = String(fs.readFileSync(absPath));
if (base === 'package.json') {
const pkg = loader(absPath, content);
return transform({
config: getPackageProp(packageProp, pkg),
filepath: absPath,
});
}
const result = {
config: null,
filepath: absPath,
};
// handle other type of configs
const isEmpty = content.trim() === '';
if (isEmpty && ignoreEmptySearchPlaces)
return emplace(
loadCache,
absPath,
transform({
filepath: absPath,
config: undefined,
isEmpty: true,
}),
);
// cosmiconfig returns undefined for empty files
result.config = isEmpty ? undefined : loader(absPath, content);
return emplace(
loadCache,
absPath,
transform(isEmpty ? {...result, isEmpty, config: undefined} : result),
);
},
clearLoadCache() {
if (cache) loadCache.clear();
},
clearSearchCache() {
if (cache) searchCache.clear();
},
clearCaches() {
if (cache) {
loadCache.clear();
searchCache.clear();
}
},
};
};

118
frontend/node_modules/tailwindcss/package.json generated vendored Normal file
View File

@@ -0,0 +1,118 @@
{
"name": "tailwindcss",
"version": "3.4.17",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"main": "lib/index.js",
"types": "types/index.d.ts",
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
"homepage": "https://tailwindcss.com",
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"scripts": {
"prebuild": "npm run generate && rimraf lib",
"build": "swc src --out-dir lib --copy-files",
"postbuild": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
"rebuild-fixtures": "npm run build && node -r @swc/register scripts/rebuildFixtures.js",
"style": "eslint .",
"pretest": "npm run generate",
"test": "jest",
"test:integrations": "npm run test --prefix ./integrations",
"install:integrations": "node scripts/install-integrations.js",
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
"generate:types": "node -r @swc/register scripts/generate-types.js",
"generate": "npm run generate:plugin-list && npm run generate:types",
"release-channel": "node ./scripts/release-channel.js",
"release-notes": "node ./scripts/release-notes.js",
"prepublishOnly": "npm install --force && npm run build"
},
"files": [
"src/*",
"cli/*",
"lib/*",
"peers/*",
"scripts/*.js",
"stubs/*",
"nesting/*",
"types/**/*",
"*.d.ts",
"*.css",
"*.js"
],
"devDependencies": {
"@swc/cli": "0.1.62",
"@swc/core": "1.3.55",
"@swc/jest": "0.2.26",
"@swc/register": "0.1.10",
"autoprefixer": "^10.4.20",
"browserslist": "^4.24.0",
"concurrently": "^8.2.2",
"cssnano": "^6.1.2",
"esbuild": "^0.24.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.7.0",
"jest-diff": "^29.7.0",
"lightningcss": "1.27.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.10",
"source-map-js": "^1.2.1",
"turbo": "^1.13.4"
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
"sucrase": "^3.35.0"
},
"browserslist": [
"> 1%",
"not edge <= 18",
"not ie 11",
"not op_mini all"
],
"jest": {
"testTimeout": 30000,
"setupFilesAfterEnv": [
"<rootDir>/jest/customMatchers.js"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/integrations/",
"/standalone-cli/",
"\\.test\\.skip\\.js$"
],
"transformIgnorePatterns": [
"node_modules/(?!lightningcss)"
],
"transform": {
"\\.js$": "@swc/jest",
"\\.ts$": "@swc/jest"
}
},
"engines": {
"node": ">=14.0.0"
}
}

97992
frontend/node_modules/tailwindcss/peers/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

11
frontend/node_modules/tailwindcss/plugin.d.ts generated vendored Normal file
View File

@@ -0,0 +1,11 @@
import type { Config, PluginCreator } from './types/config'
type Plugin = {
withOptions<T>(
plugin: (options: T) => PluginCreator,
config?: (options: T) => Partial<Config>
): { (options: T): { handler: PluginCreator; config?: Partial<Config> }; __isOptionsFunction: true }
(plugin: PluginCreator, config?: Partial<Config>): { handler: PluginCreator; config?: Partial<Config> }
}
declare const plugin: Plugin
export = plugin

2
frontend/node_modules/tailwindcss/plugin.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let createPlugin = require('./lib/public/create-plugin')
module.exports = (createPlugin.__esModule ? createPlugin : { default: createPlugin }).default

19
frontend/node_modules/tailwindcss/prettier.config.js generated vendored Normal file
View File

@@ -0,0 +1,19 @@
module.exports = {
// These settings are duplicated in .editorconfig:
tabWidth: 2, // indent_size = 2
useTabs: false, // indent_style = space
endOfLine: 'lf', // end_of_line = lf
semi: false, // default: true
singleQuote: true, // default: false
printWidth: 100, // default: 80
trailingComma: 'es5',
bracketSpacing: true,
overrides: [
{
files: '*.js',
options: {
parser: 'flow',
},
},
],
}

31
frontend/node_modules/tailwindcss/resolveConfig.d.ts generated vendored Normal file
View File

@@ -0,0 +1,31 @@
import { Config, ResolvableTo, ThemeConfig } from './types/config'
import { DefaultTheme } from './types/generated/default-theme'
import { DefaultColors } from './types/generated/colors'
type ResolvedConfig<T extends Config> = Omit<T, 'theme'> & {
theme: MergeThemes<
UnwrapResolvables<Omit<T['theme'], 'extend'>>,
T['theme'] extends { extend: infer TExtend } ? UnwrapResolvables<TExtend> : {}
>
}
type UnwrapResolvables<T> = {
[K in keyof T]: T[K] extends ResolvableTo<infer R> ? R : T[K]
}
type ThemeConfigResolved = UnwrapResolvables<ThemeConfig>
type DefaultThemeFull = DefaultTheme & { colors: DefaultColors }
type MergeThemes<Overrides extends object, Extensions extends object> = {
[K in keyof ThemeConfigResolved | keyof Overrides]: (K extends keyof Overrides
? Overrides[K]
: K extends keyof DefaultThemeFull
? DefaultThemeFull[K]
: K extends keyof ThemeConfigResolved
? ThemeConfigResolved[K]
: never) &
(K extends keyof Extensions ? Extensions[K] : {})
}
declare function resolveConfig<T extends Config>(config: T): ResolvedConfig<T>
export = resolveConfig

2
frontend/node_modules/tailwindcss/resolveConfig.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
let resolveConfig = require('./lib/public/resolve-config')
module.exports = (resolveConfig.__esModule ? resolveConfig : { default: resolveConfig }).default

1
frontend/node_modules/tailwindcss/screens.css generated vendored Normal file
View File

@@ -0,0 +1 @@
@tailwind screens;

View File

@@ -0,0 +1,10 @@
import { corePlugins } from '../src/corePlugins'
import fs from 'fs'
import path from 'path'
let corePluginList = Object.keys(corePlugins)
fs.writeFileSync(
path.join(process.cwd(), 'src', 'corePluginList.js'),
`export default ${JSON.stringify(corePluginList)}`
)

View File

@@ -0,0 +1,104 @@
import prettier from 'prettier'
import { corePlugins } from '../src/corePlugins'
import colors from '../src/public/colors'
import defaultTheme from '../src/public/default-theme'
import fs from 'fs'
import path from 'path'
import * as types from './type-utils'
fs.writeFileSync(
path.join(process.cwd(), 'types', 'generated', 'corePluginList.d.ts'),
`export type CorePluginList = ${Object.keys(corePlugins)
.map((p) => `'${p}'`)
.join(' | ')}`
)
let colorsWithoutDeprecatedColors = Object.fromEntries(
Object.entries(Object.getOwnPropertyDescriptors(colors))
.filter(([_, { value }]) => {
return typeof value !== 'undefined'
})
.map(([name, definition]) => [name, definition.value])
)
let deprecatedColors = Object.entries(Object.getOwnPropertyDescriptors(colors))
.filter(([_, { value }]) => {
return typeof value === 'undefined'
})
.map(([name, definition]) => {
let warn = console.warn
let messages = []
console.warn = (...args) => messages.push(args.pop())
definition.get()
console.warn = warn
let message = messages.join(' ').trim()
let newColor = message.match(/renamed to `(.*)`/)[1]
return `/** @deprecated ${message} */${name}: DefaultColors['${newColor}'],`
})
.join('\n')
fs.writeFileSync(
path.join(process.cwd(), 'types', 'generated', 'colors.d.ts'),
prettier.format(
`export interface DefaultColors { ${JSON.stringify(colorsWithoutDeprecatedColors).slice(
1,
-1
)}\n${deprecatedColors}\n}`,
{
semi: false,
singleQuote: true,
printWidth: 100,
parser: 'typescript',
}
)
)
const defaultThemeTypes = Object.entries(defaultTheme)
.map(([name, value]) => {
// Special cases for slightly more accurate types
if (name === 'keyframes') {
return [name, `Record<${types.forKeys(value)}, Record<string, CSSDeclarationList>>`]
}
if (name === 'fontSize') {
return [name, `Record<${types.forKeys(value)}, [string, { lineHeight: string }]>`]
}
// General cases
if (typeof value === 'string') {
return [name, `string`]
}
if (typeof value === 'function') {
return [name, null]
}
if (typeof value === 'object') {
if (Object.keys(value).length === 0) {
return [name, null]
}
return [name, types.forValue(value)]
}
return [name, `unknown`]
})
.filter(([, type]) => type !== null)
.map(([name, type]) => `${name}: ${type}`)
.join('\n')
fs.writeFileSync(
path.join(process.cwd(), 'types', 'generated', 'default-theme.d.ts'),
prettier.format(
`
type CSSDeclarationList = Record<string, string>
export type DefaultTheme = { ${defaultThemeTypes} }
`,
{
semi: false,
singleQuote: true,
printWidth: 100,
parser: 'typescript',
}
)
)

View File

@@ -0,0 +1,18 @@
// Given a version, figure out what the release channel is so that we can publish to the correct
// channel on npm.
//
// E.g.:
//
// 1.2.3 -> latest (default)
// 0.0.0-insiders.ffaa88 -> insiders
// 4.1.0-alpha.4 -> alpha
let version =
process.argv[2] || process.env.npm_package_version || require('../package.json').version
let match = /\d+\.\d+\.\d+-(.*)\.\d+/g.exec(version)
if (match) {
console.log(match[1])
} else {
console.log('latest')
}

View File

@@ -0,0 +1,21 @@
// Given a version, figure out what the release notes are so that we can use this to pre-fill the
// relase notes on a GitHub release for the current version.
let path = require('path')
let fs = require('fs')
let version =
process.argv[2] || process.env.npm_package_version || require('../package.json').version
let changelog = fs.readFileSync(path.resolve(__dirname, '..', 'CHANGELOG.md'), 'utf8')
let match = new RegExp(
`## \\[${version}\\] - (.*)\\n\\n([\\s\\S]*?)\\n(?:(?:##\\s)|(?:\\[))`,
'g'
).exec(changelog)
if (match) {
let [, , notes] = match
console.log(notes.trim())
} else {
console.log(`Placeholder release notes for version: v${version}`)
}

View File

@@ -0,0 +1,27 @@
export function union(types) {
return [...new Set(types)].join(' | ')
}
export function unionValues(values) {
return union(values.map(forValue))
}
export function forKeys(value) {
return union(Object.keys(value).map((key) => `'${key}'`))
}
export function forValue(value) {
if (Array.isArray(value)) {
return `(${unionValues(value)})[]`
}
if (typeof value === 'object') {
return `Record<${forKeys(value)}, ${unionValues(Object.values(value))}>`
}
if (typeof value === 'string') {
return `string`
}
return `any`
}

View File

@@ -0,0 +1,15 @@
export function lazyPostcss() {
return require('postcss')
}
export function lazyPostcssImport() {
return require('postcss-import')
}
export function lazyAutoprefixer() {
return require('autoprefixer')
}
export function lazyCssnano() {
return require('cssnano')
}

3
frontend/node_modules/tailwindcss/src/cli.js generated vendored Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
module.exports = require('./cli/index')

View File

@@ -0,0 +1,70 @@
// @ts-check
import packageJson from '../../../package.json'
export function help({ message, usage, commands, options }) {
let indent = 2
// Render header
console.log()
console.log(`${packageJson.name} v${packageJson.version}`)
// Render message
if (message) {
console.log()
for (let msg of message.split('\n')) {
console.log(msg)
}
}
// Render usage
if (usage && usage.length > 0) {
console.log()
console.log('Usage:')
for (let example of usage) {
console.log(' '.repeat(indent), example)
}
}
// Render commands
if (commands && commands.length > 0) {
console.log()
console.log('Commands:')
for (let command of commands) {
console.log(' '.repeat(indent), command)
}
}
// Render options
if (options) {
let groupedOptions = {}
for (let [key, value] of Object.entries(options)) {
if (typeof value === 'object') {
groupedOptions[key] = { ...value, flags: [key] }
} else {
groupedOptions[value].flags.push(key)
}
}
console.log()
console.log('Options:')
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
if (deprecated) continue
if (flags.length === 1) {
console.log(
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
flags.slice().reverse().join(', ').padEnd(20, ' '),
description
)
} else {
console.log(
' '.repeat(indent),
flags.slice().reverse().join(', ').padEnd(24, ' '),
description
)
}
}
}
console.log()
}

216
frontend/node_modules/tailwindcss/src/cli/index.js generated vendored Normal file
View File

@@ -0,0 +1,216 @@
#!/usr/bin/env node
import path from 'path'
import arg from 'arg'
import fs from 'fs'
import { build } from './build'
import { help } from './help'
import { init } from './init'
function oneOf(...options) {
return Object.assign(
(value = true) => {
for (let option of options) {
let parsed = option(value)
if (parsed === value) {
return parsed
}
}
throw new Error('...')
},
{ manualParsing: true }
)
}
let commands = {
init: {
run: init,
args: {
'--esm': { type: Boolean, description: `Initialize configuration file as ESM` },
'--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` },
'--postcss': { type: Boolean, description: `Initialize a \`postcss.config.js\` file` },
'--full': {
type: Boolean,
description: `Include the default values for all options in the generated configuration file`,
},
'-f': '--full',
'-p': '--postcss',
},
},
build: {
run: build,
args: {
'--input': { type: String, description: 'Input file' },
'--output': { type: String, description: 'Output file' },
'--watch': {
type: oneOf(String, Boolean),
description: 'Watch for changes and rebuild as needed',
},
'--poll': {
type: Boolean,
description: 'Use polling instead of filesystem events when watching',
},
'--content': {
type: String,
description: 'Content paths to use for removing unused classes',
},
'--purge': {
type: String,
deprecated: true,
},
'--postcss': {
type: oneOf(String, Boolean),
description: 'Load custom PostCSS configuration',
},
'--minify': { type: Boolean, description: 'Minify the output' },
'--config': {
type: String,
description: 'Path to a custom config file',
},
'--no-autoprefixer': {
type: Boolean,
description: 'Disable autoprefixer',
},
'-c': '--config',
'-i': '--input',
'-o': '--output',
'-m': '--minify',
'-w': '--watch',
'-p': '--poll',
},
},
}
let sharedFlags = {
'--help': { type: Boolean, description: 'Display usage information' },
'-h': '--help',
}
if (
process.stdout.isTTY /* Detect redirecting output to a file */ &&
(process.argv[2] === undefined ||
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
) {
help({
usage: [
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
'tailwindcss init [--full] [--postcss] [options...]',
],
commands: Object.keys(commands)
.filter((command) => command !== 'build')
.map((command) => `${command} [options]`),
options: { ...commands.build.args, ...sharedFlags },
})
process.exit(0)
}
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
if (commands[command] === undefined) {
if (fs.existsSync(path.resolve(command))) {
// TODO: Deprecate this in future versions
// Check if non-existing command, might be a file.
command = 'build'
} else {
help({
message: `Invalid command: ${command}`,
usage: ['tailwindcss <command> [options]'],
commands: Object.keys(commands)
.filter((command) => command !== 'build')
.map((command) => `${command} [options]`),
options: sharedFlags,
})
process.exit(1)
}
}
// Execute command
let { args: flags, run } = commands[command]
let args = (() => {
try {
let result = arg(
Object.fromEntries(
Object.entries({ ...flags, ...sharedFlags })
.filter(([_key, value]) => !value?.type?.manualParsing)
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
),
{ permissive: true }
)
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
for (let i = result['_'].length - 1; i >= 0; --i) {
let flag = result['_'][i]
if (!flag.startsWith('-')) continue
let [flagName, flagValue] = flag.split('=')
let handler = flags[flagName]
// Resolve flagName & handler
while (typeof handler === 'string') {
flagName = handler
handler = flags[handler]
}
if (!handler) continue
let args = []
let offset = i + 1
// --flag value syntax was used so we need to pull `value` from `args`
if (flagValue === undefined) {
// Parse args for current flag
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
args.push(result['_'][offset++])
}
// Cleanup manually parsed flags + args
result['_'].splice(i, 1 + args.length)
// No args were provided, use default value defined in handler
// One arg was provided, use that directly
// Multiple args were provided so pass them all in an array
flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args
} else {
// Remove the whole flag from the args array
result['_'].splice(i, 1)
}
// Set the resolved value in the `result` object
result[flagName] = handler.type(flagValue, flagName)
}
// Ensure that the `command` is always the first argument in the `args`.
// This is important so that we don't have to check if a default command
// (build) was used or not from within each plugin.
//
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
if (result['_'][0] !== command) {
result['_'].unshift(command)
}
return result
} catch (err) {
if (err.code === 'ARG_UNKNOWN_OPTION') {
help({
message: err.message,
usage: ['tailwindcss <command> [options]'],
options: sharedFlags,
})
process.exit(1)
}
throw err
}
})()
if (args['--help']) {
help({
options: { ...flags, ...sharedFlags },
usage: [`tailwindcss ${command} [options]`],
})
process.exit(0)
}
run(args)

View File

@@ -0,0 +1,79 @@
// @ts-check
import fs from 'fs'
import path from 'path'
function isESM() {
const pkgPath = path.resolve('./package.json')
try {
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
return pkg.type && pkg.type === 'module'
} catch (err) {
return false
}
}
export function init(args) {
let messages = []
let isProjectESM = args['--ts'] || args['--esm'] || isESM()
let syntax = args['--ts'] ? 'ts' : isProjectESM ? 'js' : 'cjs'
let extension = args['--ts'] ? 'ts' : 'js'
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./tailwind.config.${extension}`)
if (fs.existsSync(tailwindConfigLocation)) {
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
} else {
let stubContentsFile = fs.readFileSync(
args['--full']
? path.resolve(__dirname, '../../../stubs/config.full.js')
: path.resolve(__dirname, '../../../stubs/config.simple.js'),
'utf8'
)
let stubFile = fs.readFileSync(
path.resolve(__dirname, `../../../stubs/tailwind.config.${syntax}`),
'utf8'
)
// Change colors import
stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors')
// Replace contents of {ts,js,cjs} file with the stub {simple,full}.
stubFile =
stubFile
.replace('__CONFIG__', stubContentsFile.replace('module.exports =', '').trim())
.trim() + '\n\n'
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
}
if (args['--postcss']) {
let postcssConfigLocation = path.resolve('./postcss.config.js')
if (fs.existsSync(postcssConfigLocation)) {
messages.push(`${path.basename(postcssConfigLocation)} already exists.`)
} else {
let stubFile = fs.readFileSync(
isProjectESM
? path.resolve(__dirname, '../../../stubs/postcss.config.js')
: path.resolve(__dirname, '../../../stubs/postcss.config.cjs'),
'utf8'
)
fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8')
messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`)
}
}
if (messages.length > 0) {
console.log()
for (let message of messages) {
console.log(message)
}
}
}

View File

@@ -0,0 +1 @@
export default ["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","lineClamp","display","aspectRatio","size","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","captionSide","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","listStyleImage","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","hyphens","whitespace","textWrap","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","contain","content","forcedColorAdjust"]

3007
frontend/node_modules/tailwindcss/src/corePlugins.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

25
frontend/node_modules/tailwindcss/src/css/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,25 @@
MIT License
Copyright (c) Nicolas Gallagher
Copyright (c) Jonathan Neal
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Copyright (c) Adam Wathan
Copyright (c) Jonathan Reinink
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

386
frontend/node_modules/tailwindcss/src/css/preflight.css generated vendored Normal file
View File

@@ -0,0 +1,386 @@
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-moz-tab-size: 4; /* 3 */
tab-size: 4; /* 3 */
font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */
font-feature-settings: theme('fontFamily.sans[1].fontFeatureSettings', normal); /* 5 */
font-variation-settings: theme('fontFamily.sans[1].fontVariationSettings', normal); /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0; /* 1 */
line-height: inherit; /* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */
font-feature-settings: theme('fontFamily.mono[1].fontFeatureSettings', normal); /* 2 */
font-variation-settings: theme('fontFamily.mono[1].fontVariationSettings', normal); /* 3 */
font-size: 1em; /* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-feature-settings: inherit; /* 1 */
font-variation-settings: inherit; /* 1 */
font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */
letter-spacing: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button; /* 1 */
background-color: transparent; /* 2 */
background-image: none; /* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::placeholder,
textarea::placeholder {
opacity: 1; /* 1 */
color: theme('colors.gray.400', #9ca3af); /* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}

62
frontend/node_modules/tailwindcss/src/featureFlags.js generated vendored Normal file
View File

@@ -0,0 +1,62 @@
import colors from 'picocolors'
import log from './util/log'
let defaults = {
optimizeUniversalDefaults: false,
generalizedModifiers: true,
disableColorOpacityUtilitiesByDefault: false,
relativeContentPathsByDefault: false,
}
let featureFlags = {
future: [
'hoverOnlyWhenSupported',
'respectDefaultRingColorOpacity',
'disableColorOpacityUtilitiesByDefault',
'relativeContentPathsByDefault',
],
experimental: ['optimizeUniversalDefaults', 'generalizedModifiers'],
}
export function flagEnabled(config, flag) {
if (featureFlags.future.includes(flag)) {
return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false)
}
if (featureFlags.experimental.includes(flag)) {
return (
config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false)
)
}
return false
}
function experimentalFlagsEnabled(config) {
if (config.experimental === 'all') {
return featureFlags.experimental
}
return Object.keys(config?.experimental ?? {}).filter(
(flag) => featureFlags.experimental.includes(flag) && config.experimental[flag]
)
}
export function issueFlagNotices(config) {
if (process.env.JEST_WORKER_ID !== undefined) {
return
}
if (experimentalFlagsEnabled(config).length > 0) {
let changes = experimentalFlagsEnabled(config)
.map((s) => colors.yellow(s))
.join(', ')
log.warn('experimental-flags-enabled', [
`You have enabled experimental features: ${changes}`,
'Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time.',
])
}
}
export default featureFlags

1
frontend/node_modules/tailwindcss/src/index.js generated vendored Normal file
View File

@@ -0,0 +1 @@
module.exports = require('./plugin')

47
frontend/node_modules/tailwindcss/src/plugin.js generated vendored Normal file
View File

@@ -0,0 +1,47 @@
import setupTrackingContext from './lib/setupTrackingContext'
import processTailwindFeatures from './processTailwindFeatures'
import { env } from './lib/sharedState'
import { findAtConfigPath } from './lib/findAtConfigPath'
module.exports = function tailwindcss(configOrPath) {
return {
postcssPlugin: 'tailwindcss',
plugins: [
env.DEBUG &&
function (root) {
console.log('\n')
console.time('JIT TOTAL')
return root
},
async function (root, result) {
// Use the path for the `@config` directive if it exists, otherwise use the
// path for the file being processed
configOrPath = findAtConfigPath(root, result) ?? configOrPath
let context = setupTrackingContext(configOrPath)
if (root.type === 'document') {
let roots = root.nodes.filter((node) => node.type === 'root')
for (const root of roots) {
if (root.type === 'root') {
await processTailwindFeatures(context)(root, result)
}
}
return
}
await processTailwindFeatures(context)(root, result)
},
env.DEBUG &&
function (root) {
console.timeEnd('JIT TOTAL')
console.log('\n')
return root
},
].filter(Boolean),
}
}
module.exports.postcss = true

View File

@@ -0,0 +1,42 @@
# tailwindcss/nesting
This is a PostCSS plugin that wraps [postcss-nested](https://github.com/postcss/postcss-nested) or [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) and acts as a compatibility layer to make sure your nesting plugin of choice properly understands Tailwind's custom syntax like `@apply` and `@screen`.
Add it to your PostCSS configuration, somewhere before Tailwind itself:
```js
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting'),
require('tailwindcss'),
require('autoprefixer'),
]
}
```
By default, it uses the [postcss-nested](https://github.com/postcss/postcss-nested) plugin under the hood, which uses a Sass-like syntax and is the plugin that powers nesting support in the [Tailwind CSS plugin API](https://tailwindcss.com/docs/plugins#css-in-js-syntax).
If you'd rather use [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) (which is based on the work-in-progress [CSS Nesting](https://drafts.csswg.org/css-nesting-1/) specification), first install the plugin alongside:
```shell
npm install postcss-nesting
```
Then pass the plugin itself as an argument to `tailwindcss/nesting` in your PostCSS configuration:
```js
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting')(require('postcss-nesting')),
require('tailwindcss'),
require('autoprefixer'),
]
}
```
This can also be helpful if for whatever reason you need to use a very specific version of `postcss-nested` and want to override the version we bundle with `tailwindcss/nesting` itself.

View File

@@ -0,0 +1,13 @@
import { nesting } from './plugin'
export default Object.assign(
function (opts) {
return {
postcssPlugin: 'tailwindcss/nesting',
Once(root, { result }) {
return nesting(opts)(root, result)
},
}
},
{ postcss: true }
)

View File

@@ -0,0 +1,80 @@
import postcss from 'postcss'
import postcssNested from 'postcss-nested'
export function nesting(opts = postcssNested) {
return (root, result) => {
root.walkAtRules('screen', (rule) => {
rule.name = 'media'
rule.params = `screen(${rule.params})`
})
root.walkAtRules('apply', (rule) => {
rule.before(postcss.decl({ prop: '__apply', value: rule.params, source: rule.source }))
rule.remove()
})
let plugin = (() => {
if (
typeof opts === 'function' ||
(typeof opts === 'object' && opts?.hasOwnProperty?.('postcssPlugin'))
) {
return opts
}
if (typeof opts === 'string') {
return require(opts)
}
if (Object.keys(opts).length <= 0) {
return postcssNested
}
throw new Error('tailwindcss/nesting should be loaded with a nesting plugin.')
})()
postcss([plugin]).process(root, result.opts).sync()
root.walkDecls('__apply', (decl) => {
decl.before(postcss.atRule({ name: 'apply', params: decl.value, source: decl.source }))
decl.remove()
})
/**
* Use a private PostCSS API to remove the "clean" flag from the entire AST.
* This is done because running process() on the AST will set the "clean"
* flag on all nodes, which we don't want.
*
* This causes downstream plugins using the visitor API to be skipped.
*
* This is guarded because the PostCSS API is not public
* and may change in future versions of PostCSS.
*
* See https://github.com/postcss/postcss/issues/1712 for more details
*
* @param {import('postcss').Node} node
*/
function markDirty(node) {
if (!('markDirty' in node)) {
return
}
// Traverse the tree down to the leaf nodes
if (node.nodes) {
node.nodes.forEach((n) => markDirty(n))
}
// If it's a leaf node mark it as dirty
// We do this here because marking a node as dirty
// will walk up the tree and mark all parents as dirty
// resulting in a lot of unnecessary work if we did this
// for every single node
if (!node.nodes) {
node.markDirty()
}
}
markDirty(root)
return root
}
}

View File

@@ -0,0 +1,56 @@
import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives'
import expandTailwindAtRules from './lib/expandTailwindAtRules'
import expandApplyAtRules from './lib/expandApplyAtRules'
import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
import substituteScreenAtRules from './lib/substituteScreenAtRules'
import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
import collapseAdjacentRules from './lib/collapseAdjacentRules'
import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations'
import partitionApplyAtRules from './lib/partitionApplyAtRules'
import { createContext } from './lib/setupContextUtils'
import { issueFlagNotices } from './featureFlags'
export default function processTailwindFeatures(setupContext) {
return async function (root, result) {
let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)
// Partition apply rules that are found in the css
// itself.
partitionApplyAtRules()(root, result)
let context = setupContext({
tailwindDirectives,
applyDirectives,
registerDependency(dependency) {
result.messages.push({
plugin: 'tailwindcss',
parent: result.opts.from,
...dependency,
})
},
createContext(tailwindConfig, changedContent) {
return createContext(tailwindConfig, changedContent, root)
},
})(root, result)
if (context.tailwindConfig.separator === '-') {
throw new Error(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
}
issueFlagNotices(context.tailwindConfig)
await expandTailwindAtRules(context)(root, result)
// Partition apply rules that are generated by
// addComponents, addUtilities and so on.
partitionApplyAtRules()(root, result)
expandApplyAtRules(context)(root, result)
evaluateTailwindFunctions(context)(root, result)
substituteScreenAtRules(context)(root, result)
resolveDefaultsAtRules(context)(root, result)
collapseAdjacentRules(context)(root, result)
collapseDuplicateDeclarations(context)(root, result)
}
}

322
frontend/node_modules/tailwindcss/src/public/colors.js generated vendored Normal file
View File

@@ -0,0 +1,322 @@
import log from '../util/log'
function warn({ version, from, to }) {
log.warn(`${from}-color-renamed`, [
`As of Tailwind CSS ${version}, \`${from}\` has been renamed to \`${to}\`.`,
'Update your configuration file to silence this warning.',
])
}
export default {
inherit: 'inherit',
current: 'currentColor',
transparent: 'transparent',
black: '#000',
white: '#fff',
slate: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
zinc: {
50: '#fafafa',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
950: '#09090b',
},
neutral: {
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
300: '#d4d4d4',
400: '#a3a3a3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0a0a0a',
},
stone: {
50: '#fafaf9',
100: '#f5f5f4',
200: '#e7e5e4',
300: '#d6d3d1',
400: '#a8a29e',
500: '#78716c',
600: '#57534e',
700: '#44403c',
800: '#292524',
900: '#1c1917',
950: '#0c0a09',
},
red: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
orange: {
50: '#fff7ed',
100: '#ffedd5',
200: '#fed7aa',
300: '#fdba74',
400: '#fb923c',
500: '#f97316',
600: '#ea580c',
700: '#c2410c',
800: '#9a3412',
900: '#7c2d12',
950: '#431407',
},
amber: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
950: '#451a03',
},
yellow: {
50: '#fefce8',
100: '#fef9c3',
200: '#fef08a',
300: '#fde047',
400: '#facc15',
500: '#eab308',
600: '#ca8a04',
700: '#a16207',
800: '#854d0e',
900: '#713f12',
950: '#422006',
},
lime: {
50: '#f7fee7',
100: '#ecfccb',
200: '#d9f99d',
300: '#bef264',
400: '#a3e635',
500: '#84cc16',
600: '#65a30d',
700: '#4d7c0f',
800: '#3f6212',
900: '#365314',
950: '#1a2e05',
},
green: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
emerald: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
950: '#022c22',
},
teal: {
50: '#f0fdfa',
100: '#ccfbf1',
200: '#99f6e4',
300: '#5eead4',
400: '#2dd4bf',
500: '#14b8a6',
600: '#0d9488',
700: '#0f766e',
800: '#115e59',
900: '#134e4a',
950: '#042f2e',
},
cyan: {
50: '#ecfeff',
100: '#cffafe',
200: '#a5f3fc',
300: '#67e8f9',
400: '#22d3ee',
500: '#06b6d4',
600: '#0891b2',
700: '#0e7490',
800: '#155e75',
900: '#164e63',
950: '#083344',
},
sky: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
blue: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
indigo: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1',
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
950: '#1e1b4b',
},
violet: {
50: '#f5f3ff',
100: '#ede9fe',
200: '#ddd6fe',
300: '#c4b5fd',
400: '#a78bfa',
500: '#8b5cf6',
600: '#7c3aed',
700: '#6d28d9',
800: '#5b21b6',
900: '#4c1d95',
950: '#2e1065',
},
purple: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7e22ce',
800: '#6b21a8',
900: '#581c87',
950: '#3b0764',
},
fuchsia: {
50: '#fdf4ff',
100: '#fae8ff',
200: '#f5d0fe',
300: '#f0abfc',
400: '#e879f9',
500: '#d946ef',
600: '#c026d3',
700: '#a21caf',
800: '#86198f',
900: '#701a75',
950: '#4a044e',
},
pink: {
50: '#fdf2f8',
100: '#fce7f3',
200: '#fbcfe8',
300: '#f9a8d4',
400: '#f472b6',
500: '#ec4899',
600: '#db2777',
700: '#be185d',
800: '#9d174d',
900: '#831843',
950: '#500724',
},
rose: {
50: '#fff1f2',
100: '#ffe4e6',
200: '#fecdd3',
300: '#fda4af',
400: '#fb7185',
500: '#f43f5e',
600: '#e11d48',
700: '#be123c',
800: '#9f1239',
900: '#881337',
950: '#4c0519',
},
get lightBlue() {
warn({ version: 'v2.2', from: 'lightBlue', to: 'sky' })
return this.sky
},
get warmGray() {
warn({ version: 'v3.0', from: 'warmGray', to: 'stone' })
return this.stone
},
get trueGray() {
warn({ version: 'v3.0', from: 'trueGray', to: 'neutral' })
return this.neutral
},
get coolGray() {
warn({ version: 'v3.0', from: 'coolGray', to: 'gray' })
return this.gray
},
get blueGray() {
warn({ version: 'v3.0', from: 'blueGray', to: 'slate' })
return this.slate
},
}

View File

@@ -0,0 +1,2 @@
import createPlugin from '../util/createPlugin'
export default createPlugin

View File

@@ -0,0 +1,4 @@
import { cloneDeep } from '../util/cloneDeep'
import defaultConfig from '../../stubs/config.full'
export default cloneDeep(defaultConfig)

View File

@@ -0,0 +1,4 @@
import { cloneDeep } from '../util/cloneDeep'
import defaultFullConfig from '../../stubs/config.full'
export default cloneDeep(defaultFullConfig.theme)

View File

@@ -0,0 +1,2 @@
import { loadConfig } from '../lib/load-config'
export default loadConfig

View File

@@ -0,0 +1,7 @@
import resolveConfigObjects from '../util/resolveConfig'
import getAllConfigs from '../util/getAllConfigs'
export default function resolveConfig(...configs) {
let [, ...defaultConfigs] = getAllConfigs(configs[0])
return resolveConfigObjects([...configs, ...defaultConfigs])
}

View File

@@ -0,0 +1,26 @@
import parser from 'postcss-selector-parser'
import { movePseudos } from './pseudoElements'
export function applyImportantSelector(selector, important) {
let sel = parser().astSync(selector)
sel.each((sel) => {
// For nesting, we only need to wrap a selector with :is() if it has a top-level combinator,
// e.g. `.dark .text-white`, to be independent of DOM order. Any other selector, including
// combinators inside of pseudos like `:where()`, are ok to nest.
let shouldWrap = sel.nodes.some((node) => node.type === 'combinator')
if (shouldWrap) {
sel.nodes = [
parser.pseudo({
value: ':is',
nodes: [sel.clone()],
}),
]
}
movePseudos(sel)
})
return `${important} ${sel.toString()}`
}

View File

@@ -0,0 +1,3 @@
export default function bigSign(bigIntValue) {
return (bigIntValue > 0n) - (bigIntValue < 0n)
}

View File

@@ -0,0 +1,22 @@
export default function buildMediaQuery(screens) {
screens = Array.isArray(screens) ? screens : [screens]
return screens
.map((screen) => {
let values = screen.values.map((screen) => {
if (screen.raw !== undefined) {
return screen.raw
}
return [
screen.min && `(min-width: ${screen.min})`,
screen.max && `(max-width: ${screen.max})`,
]
.filter(Boolean)
.join(' and ')
})
return screen.not ? `not all and ${values}` : values
})
.join(', ')
}

View File

@@ -0,0 +1,11 @@
export function cloneDeep(value) {
if (Array.isArray(value)) {
return value.map((child) => cloneDeep(child))
}
if (typeof value === 'object' && value !== null) {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)]))
}
return value
}

View File

@@ -0,0 +1,49 @@
/**
* @param {import('postcss').Container[]} nodes
* @param {any} source
* @param {any} raws
* @returns {import('postcss').Container[]}
*/
export default function cloneNodes(nodes, source = undefined, raws = undefined) {
return nodes.map((node) => {
let cloned = node.clone()
if (raws !== undefined) {
cloned.raws.tailwind = {
...cloned.raws.tailwind,
...raws,
}
}
if (source !== undefined) {
traverse(cloned, (node) => {
// Do not traverse nodes that have opted
// to preserve their original source
let shouldPreserveSource = node.raws.tailwind?.preserveSource === true && node.source
if (shouldPreserveSource) {
return false
}
// Otherwise we can safely replace the source
// And continue traversing
node.source = source
})
}
return cloned
})
}
/**
* Traverse a tree of nodes and don't traverse children if the callback
* returns false. Ideally we'd use Container#walk instead of this
* function but it stops traversing siblings too.
*
* @param {import('postcss').Container} node
* @param {(node: import('postcss').Container) => boolean} onNode
*/
function traverse(node, onNode) {
if (onNode(node) !== false) {
node.each?.((child) => traverse(child, onNode))
}
}

88
frontend/node_modules/tailwindcss/src/util/color.js generated vendored Normal file
View File

@@ -0,0 +1,88 @@
import namedColors from './colorNames'
let HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i
let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i
let VALUE = /(?:\d+|\d*\.\d+)%?/
let SEP = /(?:\s*,\s*|\s+)/
let ALPHA_SEP = /\s*[,/]\s*/
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/
let RGB = new RegExp(
`^(rgba?)\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
)
let HSL = new RegExp(
`^(hsla?)\\(\\s*((?:${VALUE.source})(?:deg|rad|grad|turn)?|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
)
// In "loose" mode the color may contain fewer than 3 parts, as long as at least
// one of the parts is variable.
export function parseColor(value, { loose = false } = {}) {
if (typeof value !== 'string') {
return null
}
value = value.trim()
if (value === 'transparent') {
return { mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }
}
if (value in namedColors) {
return { mode: 'rgb', color: namedColors[value].map((v) => v.toString()) }
}
let hex = value
.replace(SHORT_HEX, (_, r, g, b, a) => ['#', r, r, g, g, b, b, a ? a + a : ''].join(''))
.match(HEX)
if (hex !== null) {
return {
mode: 'rgb',
color: [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)].map((v) =>
v.toString()
),
alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined,
}
}
let match = value.match(RGB) ?? value.match(HSL)
if (match === null) {
return null
}
let color = [match[2], match[3], match[4]].filter(Boolean).map((v) => v.toString())
// rgba(var(--my-color), 0.1)
// hsla(var(--my-color), 0.1)
if (color.length === 2 && color[0].startsWith('var(')) {
return {
mode: match[1],
color: [color[0]],
alpha: color[1],
}
}
if (!loose && color.length !== 3) {
return null
}
if (color.length < 3 && !color.some((part) => /^var\(.*?\)$/.test(part))) {
return null
}
return {
mode: match[1],
color,
alpha: match[5]?.toString?.(),
}
}
export function formatColor({ mode, color, alpha }) {
let hasAlpha = alpha !== undefined
if (mode === 'rgba' || mode === 'hsla') {
return `${mode}(${color.join(', ')}${hasAlpha ? `, ${alpha}` : ''})`
}
return `${mode}(${color.join(' ')}${hasAlpha ? ` / ${alpha}` : ''})`
}

View File

@@ -0,0 +1,150 @@
export default {
aliceblue: [240, 248, 255],
antiquewhite: [250, 235, 215],
aqua: [0, 255, 255],
aquamarine: [127, 255, 212],
azure: [240, 255, 255],
beige: [245, 245, 220],
bisque: [255, 228, 196],
black: [0, 0, 0],
blanchedalmond: [255, 235, 205],
blue: [0, 0, 255],
blueviolet: [138, 43, 226],
brown: [165, 42, 42],
burlywood: [222, 184, 135],
cadetblue: [95, 158, 160],
chartreuse: [127, 255, 0],
chocolate: [210, 105, 30],
coral: [255, 127, 80],
cornflowerblue: [100, 149, 237],
cornsilk: [255, 248, 220],
crimson: [220, 20, 60],
cyan: [0, 255, 255],
darkblue: [0, 0, 139],
darkcyan: [0, 139, 139],
darkgoldenrod: [184, 134, 11],
darkgray: [169, 169, 169],
darkgreen: [0, 100, 0],
darkgrey: [169, 169, 169],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkseagreen: [143, 188, 143],
darkslateblue: [72, 61, 139],
darkslategray: [47, 79, 79],
darkslategrey: [47, 79, 79],
darkturquoise: [0, 206, 209],
darkviolet: [148, 0, 211],
deeppink: [255, 20, 147],
deepskyblue: [0, 191, 255],
dimgray: [105, 105, 105],
dimgrey: [105, 105, 105],
dodgerblue: [30, 144, 255],
firebrick: [178, 34, 34],
floralwhite: [255, 250, 240],
forestgreen: [34, 139, 34],
fuchsia: [255, 0, 255],
gainsboro: [220, 220, 220],
ghostwhite: [248, 248, 255],
gold: [255, 215, 0],
goldenrod: [218, 165, 32],
gray: [128, 128, 128],
green: [0, 128, 0],
greenyellow: [173, 255, 47],
grey: [128, 128, 128],
honeydew: [240, 255, 240],
hotpink: [255, 105, 180],
indianred: [205, 92, 92],
indigo: [75, 0, 130],
ivory: [255, 255, 240],
khaki: [240, 230, 140],
lavender: [230, 230, 250],
lavenderblush: [255, 240, 245],
lawngreen: [124, 252, 0],
lemonchiffon: [255, 250, 205],
lightblue: [173, 216, 230],
lightcoral: [240, 128, 128],
lightcyan: [224, 255, 255],
lightgoldenrodyellow: [250, 250, 210],
lightgray: [211, 211, 211],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightsalmon: [255, 160, 122],
lightseagreen: [32, 178, 170],
lightskyblue: [135, 206, 250],
lightslategray: [119, 136, 153],
lightslategrey: [119, 136, 153],
lightsteelblue: [176, 196, 222],
lightyellow: [255, 255, 224],
lime: [0, 255, 0],
limegreen: [50, 205, 50],
linen: [250, 240, 230],
magenta: [255, 0, 255],
maroon: [128, 0, 0],
mediumaquamarine: [102, 205, 170],
mediumblue: [0, 0, 205],
mediumorchid: [186, 85, 211],
mediumpurple: [147, 112, 219],
mediumseagreen: [60, 179, 113],
mediumslateblue: [123, 104, 238],
mediumspringgreen: [0, 250, 154],
mediumturquoise: [72, 209, 204],
mediumvioletred: [199, 21, 133],
midnightblue: [25, 25, 112],
mintcream: [245, 255, 250],
mistyrose: [255, 228, 225],
moccasin: [255, 228, 181],
navajowhite: [255, 222, 173],
navy: [0, 0, 128],
oldlace: [253, 245, 230],
olive: [128, 128, 0],
olivedrab: [107, 142, 35],
orange: [255, 165, 0],
orangered: [255, 69, 0],
orchid: [218, 112, 214],
palegoldenrod: [238, 232, 170],
palegreen: [152, 251, 152],
paleturquoise: [175, 238, 238],
palevioletred: [219, 112, 147],
papayawhip: [255, 239, 213],
peachpuff: [255, 218, 185],
peru: [205, 133, 63],
pink: [255, 192, 203],
plum: [221, 160, 221],
powderblue: [176, 224, 230],
purple: [128, 0, 128],
rebeccapurple: [102, 51, 153],
red: [255, 0, 0],
rosybrown: [188, 143, 143],
royalblue: [65, 105, 225],
saddlebrown: [139, 69, 19],
salmon: [250, 128, 114],
sandybrown: [244, 164, 96],
seagreen: [46, 139, 87],
seashell: [255, 245, 238],
sienna: [160, 82, 45],
silver: [192, 192, 192],
skyblue: [135, 206, 235],
slateblue: [106, 90, 205],
slategray: [112, 128, 144],
slategrey: [112, 128, 144],
snow: [255, 250, 250],
springgreen: [0, 255, 127],
steelblue: [70, 130, 180],
tan: [210, 180, 140],
teal: [0, 128, 128],
thistle: [216, 191, 216],
tomato: [255, 99, 71],
turquoise: [64, 224, 208],
violet: [238, 130, 238],
wheat: [245, 222, 179],
white: [255, 255, 255],
whitesmoke: [245, 245, 245],
yellow: [255, 255, 0],
yellowgreen: [154, 205, 50],
}

View File

@@ -0,0 +1,23 @@
export default function (pluginConfig, plugins) {
if (pluginConfig === undefined) {
return plugins
}
const pluginNames = Array.isArray(pluginConfig)
? pluginConfig
: [
...new Set(
plugins
.filter((pluginName) => {
return pluginConfig !== false && pluginConfig[pluginName] !== false
})
.concat(
Object.keys(pluginConfig).filter((pluginName) => {
return pluginConfig[pluginName] !== false
})
)
),
]
return pluginNames
}

View File

@@ -0,0 +1,27 @@
function createPlugin(plugin, config) {
return {
handler: plugin,
config,
}
}
createPlugin.withOptions = function (pluginFunction, configFunction = () => ({})) {
const optionsFunction = function (options) {
return {
__options: options,
handler: pluginFunction(options),
config: configFunction(options),
}
}
optionsFunction.__isOptionsFunction = true
// Expose plugin dependencies so that `object-hash` returns a different
// value if anything here changes, to ensure a rebuild is triggered.
optionsFunction.__pluginFunction = pluginFunction
optionsFunction.__configFunction = configFunction
return optionsFunction
}
export default createPlugin

View File

@@ -0,0 +1,37 @@
import transformThemeValue from './transformThemeValue'
export default function createUtilityPlugin(
themeKey,
utilityVariations = [[themeKey, [themeKey]]],
{ filterDefault = false, ...options } = {}
) {
let transformValue = transformThemeValue(themeKey)
return function ({ matchUtilities, theme }) {
for (let utilityVariation of utilityVariations) {
let group = Array.isArray(utilityVariation[0]) ? utilityVariation : [utilityVariation]
matchUtilities(
group.reduce((obj, [classPrefix, properties]) => {
return Object.assign(obj, {
[classPrefix]: (value) => {
return properties.reduce((obj, name) => {
if (Array.isArray(name)) {
return Object.assign(obj, { [name[0]]: name[1] })
}
return Object.assign(obj, { [name]: transformValue(value) })
}, {})
},
})
}, {}),
{
...options,
values: filterDefault
? Object.fromEntries(
Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
)
: theme(themeKey),
}
)
}
}
}

442
frontend/node_modules/tailwindcss/src/util/dataTypes.js generated vendored Normal file
View File

@@ -0,0 +1,442 @@
import { parseColor } from './color'
import { parseBoxShadowValue } from './parseBoxShadowValue'
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
let cssFunctions = ['min', 'max', 'clamp', 'calc']
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
function isCSSFunction(value) {
return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value))
}
// These properties accept a `<dashed-ident>` as one of the values. This means that you can use them
// as: `timeline-scope: --tl;`
//
// Without the `var(--tl)`, in these cases we don't want to normalize the value, and you should add
// the `var()` yourself.
//
// More info:
// - https://drafts.csswg.org/scroll-animations/#propdef-timeline-scope
// - https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope#dashed-ident
// - https://www.w3.org/TR/css-anchor-position-1
//
const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([
// Concrete properties
'scroll-timeline-name',
'timeline-scope',
'view-timeline-name',
'font-palette',
'anchor-name',
'anchor-scope',
'position-anchor',
'position-try-options',
// Shorthand properties
'scroll-timeline',
'animation-timeline',
'view-timeline',
'position-try',
])
// This is not a data type, but rather a function that can normalize the
// correct values.
export function normalize(value, context = null, isRoot = true) {
let isVarException = context && AUTO_VAR_INJECTION_EXCEPTIONS.has(context.property)
if (value.startsWith('--') && !isVarException) {
return `var(${value})`
}
// Keep raw strings if it starts with `url(`
if (value.includes('url(')) {
return value
.split(/(url\(.*?\))/g)
.filter(Boolean)
.map((part) => {
if (/^url\(.*?\)$/.test(part)) {
return part
}
return normalize(part, context, false)
})
.join('')
}
// Convert `_` to ` `, except for escaped underscores `\_`
value = value
.replace(
/([^\\])_+/g,
(fullMatch, characterBefore) => characterBefore + ' '.repeat(fullMatch.length - 1)
)
.replace(/^_/g, ' ')
.replace(/\\_/g, '_')
// Remove leftover whitespace
if (isRoot) {
value = value.trim()
}
value = normalizeMathOperatorSpacing(value)
return value
}
export function normalizeAttributeSelectors(value) {
// Wrap values in attribute selectors with quotes
if (value.includes('=')) {
value = value.replace(/(=.*)/g, (_fullMatch, match) => {
if (match[1] === "'" || match[1] === '"') {
return match
}
// Handle regex flags on unescaped values
if (match.length > 2) {
let trailingCharacter = match[match.length - 1]
if (
match[match.length - 2] === ' ' &&
(trailingCharacter === 'i' ||
trailingCharacter === 'I' ||
trailingCharacter === 's' ||
trailingCharacter === 'S')
) {
return `="${match.slice(1, -2)}" ${match[match.length - 1]}`
}
}
return `="${match.slice(1)}"`
})
}
return value
}
/**
* Add spaces around operators inside math functions
* like calc() that do not follow an operator, '(', or `,`.
*
* @param {string} value
* @returns {string}
*/
function normalizeMathOperatorSpacing(value) {
let preventFormattingInFunctions = ['theme']
let preventFormattingKeywords = [
'min-content',
'max-content',
'fit-content',
// Env
'safe-area-inset-top',
'safe-area-inset-right',
'safe-area-inset-bottom',
'safe-area-inset-left',
'titlebar-area-x',
'titlebar-area-y',
'titlebar-area-width',
'titlebar-area-height',
'keyboard-inset-top',
'keyboard-inset-right',
'keyboard-inset-bottom',
'keyboard-inset-left',
'keyboard-inset-width',
'keyboard-inset-height',
'radial-gradient',
'linear-gradient',
'conic-gradient',
'repeating-radial-gradient',
'repeating-linear-gradient',
'repeating-conic-gradient',
'anchor-size',
]
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
let result = ''
function lastChar() {
let char = result.trimEnd()
return char[char.length - 1]
}
for (let i = 0; i < match.length; i++) {
function peek(word) {
return word.split('').every((char, j) => match[i + j] === char)
}
function consumeUntil(chars) {
let minIndex = Infinity
for (let char of chars) {
let index = match.indexOf(char, i)
if (index !== -1 && index < minIndex) {
minIndex = index
}
}
let result = match.slice(i, minIndex)
i += result.length - 1
return result
}
let char = match[i]
// Handle `var(--variable)`
if (peek('var')) {
// When we consume until `)`, then we are dealing with this scenario:
// `var(--example)`
//
// When we consume until `,`, then we are dealing with this scenario:
// `var(--example, 1rem)`
//
// In this case we do want to "format", the default value as well
result += consumeUntil([')', ','])
}
// Skip formatting of known keywords
else if (preventFormattingKeywords.some((keyword) => peek(keyword))) {
let keyword = preventFormattingKeywords.find((keyword) => peek(keyword))
result += keyword
i += keyword.length - 1
}
// Skip formatting inside known functions
else if (preventFormattingInFunctions.some((fn) => peek(fn))) {
result += consumeUntil([')'])
}
// Don't break CSS grid track names
else if (peek('[')) {
result += consumeUntil([']'])
}
// Handle operators
else if (
['+', '-', '*', '/'].includes(char) &&
!['(', '+', '-', '*', '/', ','].includes(lastChar())
) {
result += ` ${char} `
} else {
result += char
}
}
// Simplify multiple spaces
return result.replace(/\s+/g, ' ')
})
}
export function url(value) {
return value.startsWith('url(')
}
export function number(value) {
return !isNaN(Number(value)) || isCSSFunction(value)
}
export function percentage(value) {
return (value.endsWith('%') && number(value.slice(0, -1))) || isCSSFunction(value)
}
// Please refer to MDN when updating this list:
// https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries#container_query_length_units
let lengthUnits = [
'cm',
'mm',
'Q',
'in',
'pc',
'pt',
'px',
'em',
'ex',
'ch',
'rem',
'lh',
'rlh',
'vw',
'vh',
'vmin',
'vmax',
'vb',
'vi',
'svw',
'svh',
'lvw',
'lvh',
'dvw',
'dvh',
'cqw',
'cqh',
'cqi',
'cqb',
'cqmin',
'cqmax',
]
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
export function length(value) {
return (
value === '0' ||
new RegExp(`^[+-]?[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?${lengthUnitsPattern}$`).test(value) ||
isCSSFunction(value)
)
}
let lineWidths = new Set(['thin', 'medium', 'thick'])
export function lineWidth(value) {
return lineWidths.has(value)
}
export function shadow(value) {
let parsedShadows = parseBoxShadowValue(normalize(value))
for (let parsedShadow of parsedShadows) {
if (!parsedShadow.valid) {
return false
}
}
return true
}
export function color(value) {
let colors = 0
let result = splitAtTopLevelOnly(value, '_').every((part) => {
part = normalize(part)
if (part.startsWith('var(')) return true
if (parseColor(part, { loose: true }) !== null) return colors++, true
return false
})
if (!result) return false
return colors > 0
}
export function image(value) {
let images = 0
let result = splitAtTopLevelOnly(value, ',').every((part) => {
part = normalize(part)
if (part.startsWith('var(')) return true
if (
url(part) ||
gradient(part) ||
['element(', 'image(', 'cross-fade(', 'image-set('].some((fn) => part.startsWith(fn))
) {
images++
return true
}
return false
})
if (!result) return false
return images > 0
}
let gradientTypes = new Set([
'conic-gradient',
'linear-gradient',
'radial-gradient',
'repeating-conic-gradient',
'repeating-linear-gradient',
'repeating-radial-gradient',
])
export function gradient(value) {
value = normalize(value)
for (let type of gradientTypes) {
if (value.startsWith(`${type}(`)) {
return true
}
}
return false
}
let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
export function position(value) {
let positions = 0
let result = splitAtTopLevelOnly(value, '_').every((part) => {
part = normalize(part)
if (part.startsWith('var(')) return true
if (validPositions.has(part) || length(part) || percentage(part)) {
positions++
return true
}
return false
})
if (!result) return false
return positions > 0
}
export function familyName(value) {
let fonts = 0
let result = splitAtTopLevelOnly(value, ',').every((part) => {
part = normalize(part)
if (part.startsWith('var(')) return true
// If it contains spaces, then it should be quoted
if (part.includes(' ')) {
if (!/(['"])([^"']+)\1/g.test(part)) {
return false
}
}
// If it starts with a number, it's invalid
if (/^\d/g.test(part)) {
return false
}
fonts++
return true
})
if (!result) return false
return fonts > 0
}
let genericNames = new Set([
'serif',
'sans-serif',
'monospace',
'cursive',
'fantasy',
'system-ui',
'ui-serif',
'ui-sans-serif',
'ui-monospace',
'ui-rounded',
'math',
'emoji',
'fangsong',
])
export function genericName(value) {
return genericNames.has(value)
}
let absoluteSizes = new Set([
'xx-small',
'x-small',
'small',
'medium',
'large',
'x-large',
'xx-large',
'xxx-large',
])
export function absoluteSize(value) {
return absoluteSizes.has(value)
}
let relativeSizes = new Set(['larger', 'smaller'])
export function relativeSize(value) {
return relativeSizes.has(value)
}

17
frontend/node_modules/tailwindcss/src/util/defaults.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
export function defaults(target, ...sources) {
for (let source of sources) {
for (let k in source) {
if (!target?.hasOwnProperty?.(k)) {
target[k] = source[k]
}
}
for (let k of Object.getOwnPropertySymbols(source)) {
if (!target?.hasOwnProperty?.(k)) {
target[k] = source[k]
}
}
}
return target
}

View File

@@ -0,0 +1,8 @@
import parser from 'postcss-selector-parser'
import escapeCommas from './escapeCommas'
export default function escapeClassName(className) {
let node = parser.className()
node.value = className
return escapeCommas(node?.raws?.value ?? node.value)
}

View File

@@ -0,0 +1,3 @@
export default function escapeCommas(className) {
return className.replace(/\\,/g, '\\2c ')
}

View File

@@ -0,0 +1,13 @@
const flattenColorPalette = (colors) =>
Object.assign(
{},
...Object.entries(colors ?? {}).flatMap(([color, values]) =>
typeof values == 'object'
? Object.entries(flattenColorPalette(values)).map(([number, hex]) => ({
[color + (number === 'DEFAULT' ? '' : `-${number}`)]: hex,
}))
: [{ [`${color}`]: values }]
)
)
export default flattenColorPalette

View File

@@ -0,0 +1,324 @@
import selectorParser from 'postcss-selector-parser'
import unescape from 'postcss-selector-parser/dist/util/unesc'
import escapeClassName from '../util/escapeClassName'
import prefixSelector from '../util/prefixSelector'
import { movePseudos } from './pseudoElements'
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
/** @typedef {import('postcss-selector-parser').Root} Root */
/** @typedef {import('postcss-selector-parser').Selector} Selector */
/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
/** @typedef {import('postcss-selector-parser').Node} Node */
/** @typedef {{format: string, respectPrefix: boolean}[]} RawFormats */
/** @typedef {import('postcss-selector-parser').Root} ParsedFormats */
/** @typedef {RawFormats | ParsedFormats} AcceptedFormats */
let MERGE = ':merge'
/**
* @param {RawFormats} formats
* @param {{context: any, candidate: string, base: string | null}} options
* @returns {ParsedFormats | null}
*/
export function formatVariantSelector(formats, { context, candidate }) {
let prefix = context?.tailwindConfig.prefix ?? ''
// Parse the format selector into an AST
let parsedFormats = formats.map((format) => {
let ast = selectorParser().astSync(format.format)
return {
...format,
ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast,
}
})
// We start with the candidate selector
let formatAst = selectorParser.root({
nodes: [
selectorParser.selector({
nodes: [selectorParser.className({ value: escapeClassName(candidate) })],
}),
],
})
// And iteratively merge each format selector into the candidate selector
for (let { ast } of parsedFormats) {
// 1. Handle :merge() special pseudo-class
;[formatAst, ast] = handleMergePseudo(formatAst, ast)
// 2. Merge the format selector into the current selector AST
ast.walkNesting((nesting) => nesting.replaceWith(...formatAst.nodes[0].nodes))
// 3. Keep going!
formatAst = ast
}
return formatAst
}
/**
* Given any node in a selector this gets the "simple" selector it's a part of
* A simple selector is just a list of nodes without any combinators
* Technically :is(), :not(), :has(), etc… can have combinators but those are nested
* inside the relevant node and won't be picked up so they're fine to ignore
*
* @param {Node} node
* @returns {Node[]}
**/
function simpleSelectorForNode(node) {
/** @type {Node[]} */
let nodes = []
// Walk backwards until we hit a combinator node (or the start)
while (node.prev() && node.prev().type !== 'combinator') {
node = node.prev()
}
// Now record all non-combinator nodes until we hit one (or the end)
while (node && node.type !== 'combinator') {
nodes.push(node)
node = node.next()
}
return nodes
}
/**
* Resorts the nodes in a selector to ensure they're in the correct order
* Tags go before classes, and pseudo classes go after classes
*
* @param {Selector} sel
* @returns {Selector}
**/
function resortSelector(sel) {
sel.sort((a, b) => {
if (a.type === 'tag' && b.type === 'class') {
return -1
} else if (a.type === 'class' && b.type === 'tag') {
return 1
} else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) {
return -1
} else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') {
return 1
}
return sel.index(a) - sel.index(b)
})
return sel
}
/**
* Remove extraneous selectors that do not include the base class/candidate
*
* Example:
* Given the utility `.a, .b { color: red}`
* Given the candidate `sm:b`
*
* The final selector should be `.sm\:b` and not `.a, .sm\:b`
*
* @param {Selector} ast
* @param {string} base
*/
export function eliminateIrrelevantSelectors(sel, base) {
let hasClassesMatchingCandidate = false
sel.walk((child) => {
if (child.type === 'class' && child.value === base) {
hasClassesMatchingCandidate = true
return false // Stop walking
}
})
if (!hasClassesMatchingCandidate) {
sel.remove()
}
// We do NOT recursively eliminate sub selectors that don't have the base class
// as this is NOT a safe operation. For example, if we have:
// `.space-x-2 > :not([hidden]) ~ :not([hidden])`
// We cannot remove the [hidden] from the :not() because it would change the
// meaning of the selector.
// TODO: Can we do this for :matches, :is, and :where?
}
/**
* @param {string} current
* @param {AcceptedFormats} formats
* @param {{context: any, candidate: string, base: string | null}} options
* @returns {string}
*/
export function finalizeSelector(current, formats, { context, candidate, base }) {
let separator = context?.tailwindConfig?.separator ?? ':'
// Split by the separator, but ignore the separator inside square brackets:
//
// E.g.: dark:lg:hover:[paint-order:markers]
// ┬ ┬ ┬ ┬
// │ │ │ ╰── We will not split here
// ╰──┴─────┴─────────────── We will split here
//
base = base ?? splitAtTopLevelOnly(candidate, separator).pop()
// Parse the selector into an AST
let selector = selectorParser().astSync(current)
// Normalize escaped classes, e.g.:
//
// The idea would be to replace the escaped `base` in the selector with the
// `format`. However, in css you can escape the same selector in a few
// different ways. This would result in different strings and therefore we
// can't replace it properly.
//
// base: bg-[rgb(255,0,0)]
// base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
// escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
//
selector.walkClasses((node) => {
if (node.raws && node.value.includes(base)) {
node.raws.value = escapeClassName(unescape(node.raws.value))
}
})
// Remove extraneous selectors that do not include the base candidate
selector.each((sel) => eliminateIrrelevantSelectors(sel, base))
// If ffter eliminating irrelevant selectors, we end up with nothing
// Then the whole "rule" this is associated with does not need to exist
// We use `null` as a marker value for that case
if (selector.length === 0) {
return null
}
// If there are no formats that means there were no variants added to the candidate
// so we can just return the selector as-is
let formatAst = Array.isArray(formats)
? formatVariantSelector(formats, { context, candidate })
: formats
if (formatAst === null) {
return selector.toString()
}
let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
// We can safely replace the escaped base now, since the `base` section is
// now in a normalized escaped value.
selector.walkClasses((node) => {
if (node.value !== base) {
return
}
let parent = node.parent
let formatNodes = formatAst.nodes[0].nodes
// Perf optimization: if the parent is a single class we can just replace it and be done
if (parent.nodes.length === 1) {
node.replaceWith(...formatNodes)
return
}
let simpleSelector = simpleSelectorForNode(node)
parent.insertBefore(simpleSelector[0], simpleStart)
parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
for (let child of formatNodes) {
parent.insertBefore(simpleSelector[0], child.clone())
}
node.remove()
// Re-sort the simple selector to ensure it's in the correct order
simpleSelector = simpleSelectorForNode(simpleStart)
let firstNode = parent.index(simpleStart)
parent.nodes.splice(
firstNode,
simpleSelector.length,
...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
)
simpleStart.remove()
simpleEnd.remove()
})
// Remove unnecessary pseudo selectors that we used as placeholders
selector.walkPseudos((p) => {
if (p.value === MERGE) {
p.replaceWith(p.nodes)
}
})
// Move pseudo elements to the end of the selector (if necessary)
selector.each((sel) => movePseudos(sel))
return selector.toString()
}
/**
*
* @param {Selector} selector
* @param {Selector} format
*/
export function handleMergePseudo(selector, format) {
/** @type {{pseudo: Pseudo, value: string}[]} */
let merges = []
// Find all :merge() pseudo-classes in `selector`
selector.walkPseudos((pseudo) => {
if (pseudo.value === MERGE) {
merges.push({
pseudo,
value: pseudo.nodes[0].toString(),
})
}
})
// Find all :merge() "attachments" in `format` and attach them to the matching selector in `selector`
format.walkPseudos((pseudo) => {
if (pseudo.value !== MERGE) {
return
}
let value = pseudo.nodes[0].toString()
// Does `selector` contain a :merge() pseudo-class with the same value?
let existing = merges.find((merge) => merge.value === value)
// Nope so there's nothing to do
if (!existing) {
return
}
// Everything after `:merge()` up to the next combinator is what is attached to the merged selector
let attachments = []
let next = pseudo.next()
while (next && next.type !== 'combinator') {
attachments.push(next)
next = next.next()
}
let combinator = next
existing.pseudo.parent.insertAfter(
existing.pseudo,
selectorParser.selector({ nodes: attachments.map((node) => node.clone()) })
)
pseudo.remove()
attachments.forEach((node) => node.remove())
// What about this case:
// :merge(.group):focus > &
// :merge(.group):hover &
if (combinator && combinator.type === 'combinator') {
combinator.remove()
}
})
return [selector, format]
}

View File

@@ -0,0 +1,38 @@
import defaultFullConfig from '../../stubs/config.full.js'
import { flagEnabled } from '../featureFlags'
export default function getAllConfigs(config) {
const configs = (config?.presets ?? [defaultFullConfig])
.slice()
.reverse()
.flatMap((preset) => getAllConfigs(preset instanceof Function ? preset() : preset))
const features = {
// Add experimental configs here...
respectDefaultRingColorOpacity: {
theme: {
ringColor: ({ theme }) => ({
DEFAULT: '#3b82f67f',
...theme('colors'),
}),
},
},
disableColorOpacityUtilitiesByDefault: {
corePlugins: {
backgroundOpacity: false,
borderOpacity: false,
divideOpacity: false,
placeholderOpacity: false,
ringOpacity: false,
textOpacity: false,
},
},
}
const experimentals = Object.keys(features)
.filter((feature) => flagEnabled(config, feature))
.map((feature) => features[feature])
return [config, ...experimentals, ...configs]
}

View File

@@ -0,0 +1,5 @@
import hash from 'object-hash'
export default function hashConfig(config) {
return hash(config, { ignoreUnknown: true })
}

View File

@@ -0,0 +1,3 @@
export default function isKeyframeRule(rule) {
return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name)
}

View File

@@ -0,0 +1,8 @@
export default function isPlainObject(value) {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false
}
const prototype = Object.getPrototypeOf(value)
return prototype === null || Object.getPrototypeOf(prototype) === null
}

View File

@@ -0,0 +1,61 @@
let matchingBrackets = new Map([
['{', '}'],
['[', ']'],
['(', ')'],
])
let inverseMatchingBrackets = new Map(
Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
)
let quotes = new Set(['"', "'", '`'])
// Arbitrary values must contain balanced brackets (), [] and {}. Escaped
// values don't count, and brackets inside quotes also don't count.
//
// E.g.: w-[this-is]w-[weird-and-invalid]
// E.g.: w-[this-is\\]w-\\[weird-but-valid]
// E.g.: content-['this-is-also-valid]-weirdly-enough']
export default function isSyntacticallyValidPropertyValue(value) {
let stack = []
let inQuotes = false
for (let i = 0; i < value.length; i++) {
let char = value[i]
if (char === ':' && !inQuotes && stack.length === 0) {
return false
}
// Non-escaped quotes allow us to "allow" anything in between
if (quotes.has(char) && value[i - 1] !== '\\') {
inQuotes = !inQuotes
}
if (inQuotes) continue
if (value[i - 1] === '\\') continue // Escaped
if (matchingBrackets.has(char)) {
stack.push(char)
} else if (inverseMatchingBrackets.has(char)) {
let inverse = inverseMatchingBrackets.get(char)
// Nothing to pop from, therefore it is unbalanced
if (stack.length <= 0) {
return false
}
// Popped value must match the inverse value, otherwise it is unbalanced
if (stack.pop() !== inverse) {
return false
}
}
}
// If there is still something on the stack, it is also unbalanced
if (stack.length > 0) {
return false
}
// All good, totally balanced!
return true
}

29
frontend/node_modules/tailwindcss/src/util/log.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
import colors from 'picocolors'
let alreadyShown = new Set()
function log(type, messages, key) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return
if (key && alreadyShown.has(key)) return
if (key) alreadyShown.add(key)
console.warn('')
messages.forEach((message) => console.warn(type, '-', message))
}
export function dim(input) {
return colors.dim(input)
}
export default {
info(key, messages) {
log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key]))
},
warn(key, messages) {
log(colors.bold(colors.yellow('warn')), ...(Array.isArray(key) ? [key] : [messages, key]))
},
risk(key, messages) {
log(colors.bold(colors.magenta('risk')), ...(Array.isArray(key) ? [key] : [messages, key]))
},
}

View File

@@ -0,0 +1,30 @@
import escapeClassName from './escapeClassName'
import escapeCommas from './escapeCommas'
export function asClass(name) {
return escapeCommas(`.${escapeClassName(name)}`)
}
export default function nameClass(classPrefix, key) {
return asClass(formatClass(classPrefix, key))
}
export function formatClass(classPrefix, key) {
if (key === 'DEFAULT') {
return classPrefix
}
if (key === '-' || key === '-DEFAULT') {
return `-${classPrefix}`
}
if (key.startsWith('-')) {
return `-${classPrefix}${key}`
}
if (key.startsWith('/')) {
return `${classPrefix}${key}`
}
return `${classPrefix}-${key}`
}

View File

@@ -0,0 +1,24 @@
export default function negateValue(value) {
value = `${value}`
if (value === '0') {
return '0'
}
// Flip sign of numbers
if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) {
return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
}
// What functions we support negating numeric values for
// var() isn't inherently a numeric function but we support it anyway
// The trigonometric functions are omitted because you'll need to use calc(…) with them _anyway_
// to produce generally useful results and that will be covered already
let numericFunctions = ['var', 'calc', 'min', 'max', 'clamp']
for (const fn of numericFunctions) {
if (value.includes(`${fn}(`)) {
return `calc(${value} * -1)`
}
}
}

View File

@@ -0,0 +1,299 @@
import { flagEnabled } from '../featureFlags'
import log, { dim } from './log'
export function normalizeConfig(config) {
// Quick structure validation
/**
* type FilePath = string
* type RawFile = { raw: string, extension?: string }
* type ExtractorFn = (content: string) => Array<string>
* type TransformerFn = (content: string) => string
*
* type Content =
* | Array<FilePath | RawFile>
* | {
* files: Array<FilePath | RawFile>,
* extract?: ExtractorFn | { [extension: string]: ExtractorFn }
* transform?: TransformerFn | { [extension: string]: TransformerFn }
* }
*/
let valid = (() => {
// `config.purge` should not exist anymore
if (config.purge) {
return false
}
// `config.content` should exist
if (!config.content) {
return false
}
// `config.content` should be an object or an array
if (
!Array.isArray(config.content) &&
!(typeof config.content === 'object' && config.content !== null)
) {
return false
}
// When `config.content` is an array, it should consist of FilePaths or RawFiles
if (Array.isArray(config.content)) {
return config.content.every((path) => {
// `path` can be a string
if (typeof path === 'string') return true
// `path` can be an object { raw: string, extension?: string }
// `raw` must be a string
if (typeof path?.raw !== 'string') return false
// `extension` (if provided) should also be a string
if (path?.extension && typeof path?.extension !== 'string') {
return false
}
return true
})
}
// When `config.content` is an object
if (typeof config.content === 'object' && config.content !== null) {
// Only `files`, `relative`, `extract`, and `transform` can exist in `config.content`
if (
Object.keys(config.content).some(
(key) => !['files', 'relative', 'extract', 'transform'].includes(key)
)
) {
return false
}
// `config.content.files` should exist of FilePaths or RawFiles
if (Array.isArray(config.content.files)) {
if (
!config.content.files.every((path) => {
// `path` can be a string
if (typeof path === 'string') return true
// `path` can be an object { raw: string, extension?: string }
// `raw` must be a string
if (typeof path?.raw !== 'string') return false
// `extension` (if provided) should also be a string
if (path?.extension && typeof path?.extension !== 'string') {
return false
}
return true
})
) {
return false
}
// `config.content.extract` is optional, and can be a Function or a Record<String, Function>
if (typeof config.content.extract === 'object') {
for (let value of Object.values(config.content.extract)) {
if (typeof value !== 'function') {
return false
}
}
} else if (
!(config.content.extract === undefined || typeof config.content.extract === 'function')
) {
return false
}
// `config.content.transform` is optional, and can be a Function or a Record<String, Function>
if (typeof config.content.transform === 'object') {
for (let value of Object.values(config.content.transform)) {
if (typeof value !== 'function') {
return false
}
}
} else if (
!(
config.content.transform === undefined || typeof config.content.transform === 'function'
)
) {
return false
}
// `config.content.relative` is optional and can be a boolean
if (
typeof config.content.relative !== 'boolean' &&
typeof config.content.relative !== 'undefined'
) {
return false
}
}
return true
}
return false
})()
if (!valid) {
log.warn('purge-deprecation', [
'The `purge`/`content` options have changed in Tailwind CSS v3.0.',
'Update your configuration file to eliminate this warning.',
'https://tailwindcss.com/docs/upgrade-guide#configure-content-sources',
])
}
// Normalize the `safelist`
config.safelist = (() => {
let { content, purge, safelist } = config
if (Array.isArray(safelist)) return safelist
if (Array.isArray(content?.safelist)) return content.safelist
if (Array.isArray(purge?.safelist)) return purge.safelist
if (Array.isArray(purge?.options?.safelist)) return purge.options.safelist
return []
})()
// Normalize the `blocklist`
config.blocklist = (() => {
let { blocklist } = config
if (Array.isArray(blocklist)) {
if (blocklist.every((item) => typeof item === 'string')) {
return blocklist
}
log.warn('blocklist-invalid', [
'The `blocklist` option must be an array of strings.',
'https://tailwindcss.com/docs/content-configuration#discarding-classes',
])
}
return []
})()
// Normalize prefix option
if (typeof config.prefix === 'function') {
log.warn('prefix-function', [
'As of Tailwind CSS v3.0, `prefix` cannot be a function.',
'Update `prefix` in your configuration to be a string to eliminate this warning.',
'https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function',
])
config.prefix = ''
} else {
config.prefix = config.prefix ?? ''
}
// Normalize the `content`
config.content = {
relative: (() => {
let { content } = config
if (content?.relative) {
return content.relative
}
return flagEnabled(config, 'relativeContentPathsByDefault')
})(),
files: (() => {
let { content, purge } = config
if (Array.isArray(purge)) return purge
if (Array.isArray(purge?.content)) return purge.content
if (Array.isArray(content)) return content
if (Array.isArray(content?.content)) return content.content
if (Array.isArray(content?.files)) return content.files
return []
})(),
extract: (() => {
let extract = (() => {
if (config.purge?.extract) return config.purge.extract
if (config.content?.extract) return config.content.extract
if (config.purge?.extract?.DEFAULT) return config.purge.extract.DEFAULT
if (config.content?.extract?.DEFAULT) return config.content.extract.DEFAULT
if (config.purge?.options?.extractors) return config.purge.options.extractors
if (config.content?.options?.extractors) return config.content.options.extractors
return {}
})()
let extractors = {}
let defaultExtractor = (() => {
if (config.purge?.options?.defaultExtractor) {
return config.purge.options.defaultExtractor
}
if (config.content?.options?.defaultExtractor) {
return config.content.options.defaultExtractor
}
return undefined
})()
if (defaultExtractor !== undefined) {
extractors.DEFAULT = defaultExtractor
}
// Functions
if (typeof extract === 'function') {
extractors.DEFAULT = extract
}
// Arrays
else if (Array.isArray(extract)) {
for (let { extensions, extractor } of extract ?? []) {
for (let extension of extensions) {
extractors[extension] = extractor
}
}
}
// Objects
else if (typeof extract === 'object' && extract !== null) {
Object.assign(extractors, extract)
}
return extractors
})(),
transform: (() => {
let transform = (() => {
if (config.purge?.transform) return config.purge.transform
if (config.content?.transform) return config.content.transform
if (config.purge?.transform?.DEFAULT) return config.purge.transform.DEFAULT
if (config.content?.transform?.DEFAULT) return config.content.transform.DEFAULT
return {}
})()
let transformers = {}
if (typeof transform === 'function') {
transformers.DEFAULT = transform
} else if (typeof transform === 'object' && transform !== null) {
Object.assign(transformers, transform)
}
return transformers
})(),
}
// Validate globs to prevent bogus globs.
// E.g.: `./src/*.{html}` is invalid, the `{html}` should just be `html`
for (let file of config.content.files) {
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
log.warn('invalid-glob-braces', [
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
// TODO: Add https://tw.wtf/invalid-glob-braces
])
break
}
}
return config
}

View File

@@ -0,0 +1,140 @@
/**
* @typedef {object} ScreenValue
* @property {number|undefined} min
* @property {number|undefined} max
* @property {string|undefined} raw
*/
/**
* @typedef {object} Screen
* @property {string} name
* @property {boolean} not
* @property {ScreenValue[]} values
*/
/**
* A function that normalizes the various forms that the screens object can be
* provided in.
*
* Input(s):
* - ['100px', '200px'] // Raw strings
* - { sm: '100px', md: '200px' } // Object with string values
* - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
* - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
*
* Output(s):
* - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
*
* @returns {Screen[]}
*/
export function normalizeScreens(screens, root = true) {
if (Array.isArray(screens)) {
return screens.map((screen) => {
if (root && Array.isArray(screen)) {
throw new Error('The tuple syntax is not supported for `screens`.')
}
if (typeof screen === 'string') {
return { name: screen.toString(), not: false, values: [{ min: screen, max: undefined }] }
}
let [name, options] = screen
name = name.toString()
if (typeof options === 'string') {
return { name, not: false, values: [{ min: options, max: undefined }] }
}
if (Array.isArray(options)) {
return { name, not: false, values: options.map((option) => resolveValue(option)) }
}
return { name, not: false, values: [resolveValue(options)] }
})
}
return normalizeScreens(Object.entries(screens ?? {}), false)
}
/**
* @param {Screen} screen
* @returns {{result: false, reason: string} | {result: true, reason: null}}
*/
export function isScreenSortable(screen) {
if (screen.values.length !== 1) {
return { result: false, reason: 'multiple-values' }
} else if (screen.values[0].raw !== undefined) {
return { result: false, reason: 'raw-values' }
} else if (screen.values[0].min !== undefined && screen.values[0].max !== undefined) {
return { result: false, reason: 'min-and-max' }
}
return { result: true, reason: null }
}
/**
* @param {'min' | 'max'} type
* @param {Screen | 'string'} a
* @param {Screen | 'string'} z
* @returns {number}
*/
export function compareScreens(type, a, z) {
let aScreen = toScreen(a, type)
let zScreen = toScreen(z, type)
let aSorting = isScreenSortable(aScreen)
let bSorting = isScreenSortable(zScreen)
// These cases should never happen and indicate a bug in Tailwind CSS itself
if (aSorting.reason === 'multiple-values' || bSorting.reason === 'multiple-values') {
throw new Error(
'Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.'
)
} else if (aSorting.reason === 'raw-values' || bSorting.reason === 'raw-values') {
throw new Error(
'Attempted to sort a screen with raw values. This should never happen. Please open a bug report.'
)
} else if (aSorting.reason === 'min-and-max' || bSorting.reason === 'min-and-max') {
throw new Error(
'Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.'
)
}
// Let the sorting begin
let { min: aMin, max: aMax } = aScreen.values[0]
let { min: zMin, max: zMax } = zScreen.values[0]
// Negating screens flip their behavior. Basically `not min-width` is `max-width`
if (a.not) [aMin, aMax] = [aMax, aMin]
if (z.not) [zMin, zMax] = [zMax, zMin]
aMin = aMin === undefined ? aMin : parseFloat(aMin)
aMax = aMax === undefined ? aMax : parseFloat(aMax)
zMin = zMin === undefined ? zMin : parseFloat(zMin)
zMax = zMax === undefined ? zMax : parseFloat(zMax)
let [aValue, zValue] = type === 'min' ? [aMin, zMin] : [zMax, aMax]
return aValue - zValue
}
/**
*
* @param {PartialScreen> | string} value
* @param {'min' | 'max'} type
* @returns {Screen}
*/
export function toScreen(value, type) {
if (typeof value === 'object') {
return value
}
return {
name: 'arbitrary-screen',
values: [{ [type]: value }],
}
}
function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
return { min, max, raw }
}

View File

@@ -0,0 +1,68 @@
const DIRECTIONS = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse'])
const PLAY_STATES = new Set(['running', 'paused'])
const FILL_MODES = new Set(['none', 'forwards', 'backwards', 'both'])
const ITERATION_COUNTS = new Set(['infinite'])
const TIMINGS = new Set([
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'step-start',
'step-end',
])
const TIMING_FNS = ['cubic-bezier', 'steps']
const COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubic-bezier(a, b, c)` these don't count.
const SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
const TIME = /^(-?[\d.]+m?s)$/
const DIGIT = /^(\d+)$/
export default function parseAnimationValue(input) {
let animations = input.split(COMMA)
return animations.map((animation) => {
let value = animation.trim()
let result = { value }
let parts = value.split(SPACE)
let seen = new Set()
for (let part of parts) {
if (!seen.has('DIRECTIONS') && DIRECTIONS.has(part)) {
result.direction = part
seen.add('DIRECTIONS')
} else if (!seen.has('PLAY_STATES') && PLAY_STATES.has(part)) {
result.playState = part
seen.add('PLAY_STATES')
} else if (!seen.has('FILL_MODES') && FILL_MODES.has(part)) {
result.fillMode = part
seen.add('FILL_MODES')
} else if (
!seen.has('ITERATION_COUNTS') &&
(ITERATION_COUNTS.has(part) || DIGIT.test(part))
) {
result.iterationCount = part
seen.add('ITERATION_COUNTS')
} else if (!seen.has('TIMING_FUNCTION') && TIMINGS.has(part)) {
result.timingFunction = part
seen.add('TIMING_FUNCTION')
} else if (!seen.has('TIMING_FUNCTION') && TIMING_FNS.some((f) => part.startsWith(`${f}(`))) {
result.timingFunction = part
seen.add('TIMING_FUNCTION')
} else if (!seen.has('DURATION') && TIME.test(part)) {
result.duration = part
seen.add('DURATION')
} else if (!seen.has('DELAY') && TIME.test(part)) {
result.delay = part
seen.add('DELAY')
} else if (!seen.has('NAME')) {
result.name = part
seen.add('NAME')
} else {
if (!result.unknown) result.unknown = []
result.unknown.push(part)
}
}
return result
})
}

View File

@@ -0,0 +1,72 @@
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset'])
let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
export function parseBoxShadowValue(input) {
let shadows = splitAtTopLevelOnly(input, ',')
return shadows.map((shadow) => {
let value = shadow.trim()
let result = { raw: value }
let parts = value.split(SPACE)
let seen = new Set()
for (let part of parts) {
// Reset index, since the regex is stateful.
LENGTH.lastIndex = 0
// Keyword
if (!seen.has('KEYWORD') && KEYWORDS.has(part)) {
result.keyword = part
seen.add('KEYWORD')
}
// Length value
else if (LENGTH.test(part)) {
if (!seen.has('X')) {
result.x = part
seen.add('X')
} else if (!seen.has('Y')) {
result.y = part
seen.add('Y')
} else if (!seen.has('BLUR')) {
result.blur = part
seen.add('BLUR')
} else if (!seen.has('SPREAD')) {
result.spread = part
seen.add('SPREAD')
}
}
// Color or unknown
else {
if (!result.color) {
result.color = part
} else {
if (!result.unknown) result.unknown = []
result.unknown.push(part)
}
}
}
// Check if valid
result.valid = result.x !== undefined && result.y !== undefined
return result
})
}
export function formatBoxShadowValue(shadows) {
return shadows
.map((shadow) => {
if (!shadow.valid) {
return shadow.raw
}
return [shadow.keyword, shadow.x, shadow.y, shadow.blur, shadow.spread, shadow.color]
.filter(Boolean)
.join(' ')
})
.join(', ')
}

View File

@@ -0,0 +1,44 @@
// @ts-check
/**
* @typedef {{type: 'dependency', file: string} | {type: 'dir-dependency', dir: string, glob: string}} Dependency
*/
/**
*
* @param {import('../lib/content.js').ContentPath} contentPath
* @returns {Dependency[]}
*/
export default function parseDependency(contentPath) {
if (contentPath.ignore) {
return []
}
if (!contentPath.glob) {
return [
{
type: 'dependency',
file: contentPath.base,
},
]
}
if (process.env.ROLLUP_WATCH === 'true') {
// rollup-plugin-postcss does not support dir-dependency messages
// but directories can be watched in the same way as files
return [
{
type: 'dependency',
file: contentPath.base,
},
]
}
return [
{
type: 'dir-dependency',
dir: contentPath.base,
glob: contentPath.glob,
},
]
}

View File

@@ -0,0 +1,23 @@
import globParent from 'glob-parent'
// Based on `glob-base`
// https://github.com/micromatch/glob-base/blob/master/index.js
export function parseGlob(pattern) {
let glob = pattern
let base = globParent(pattern)
if (base !== '.') {
glob = pattern.substr(base.length)
if (glob.charAt(0) === '/') {
glob = glob.substr(1)
}
}
if (glob.substr(0, 2) === './') {
glob = glob.substr(2)
} else if (glob.charAt(0) === '/') {
glob = glob.substr(1)
}
return { base, glob }
}

View File

@@ -0,0 +1,19 @@
import postcss from 'postcss'
import postcssNested from 'postcss-nested'
import postcssJs from 'postcss-js'
export default function parseObjectStyles(styles) {
if (!Array.isArray(styles)) {
return parseObjectStyles([styles])
}
return styles.flatMap((style) => {
return postcss([
postcssNested({
bubble: ['screen'],
}),
]).process(style, {
parser: postcssJs,
}).root.nodes
})
}

View File

@@ -0,0 +1,307 @@
import escapeCommas from './escapeCommas'
import { withAlphaValue } from './withAlphaVariable'
import {
normalize,
length,
number,
percentage,
url,
color as validateColor,
genericName,
familyName,
image,
absoluteSize,
relativeSize,
position,
lineWidth,
shadow,
} from './dataTypes'
import negateValue from './negateValue'
import { backgroundSize } from './validateFormalSyntax'
import { flagEnabled } from '../featureFlags.js'
/**
* @param {import('postcss-selector-parser').Container} selectors
* @param {(className: string) => string} updateClass
* @returns {string}
*/
export function updateAllClasses(selectors, updateClass) {
selectors.walkClasses((sel) => {
sel.value = updateClass(sel.value)
if (sel.raws && sel.raws.value) {
sel.raws.value = escapeCommas(sel.raws.value)
}
})
}
function resolveArbitraryValue(modifier, validate) {
if (!isArbitraryValue(modifier)) {
return undefined
}
let value = modifier.slice(1, -1)
if (!validate(value)) {
return undefined
}
return normalize(value)
}
function asNegativeValue(modifier, lookup = {}, validate) {
let positiveValue = lookup[modifier]
if (positiveValue !== undefined) {
return negateValue(positiveValue)
}
if (isArbitraryValue(modifier)) {
let resolved = resolveArbitraryValue(modifier, validate)
if (resolved === undefined) {
return undefined
}
return negateValue(resolved)
}
}
export function asValue(modifier, options = {}, { validate = () => true } = {}) {
let value = options.values?.[modifier]
if (value !== undefined) {
return value
}
if (options.supportsNegativeValues && modifier.startsWith('-')) {
return asNegativeValue(modifier.slice(1), options.values, validate)
}
return resolveArbitraryValue(modifier, validate)
}
function isArbitraryValue(input) {
return input.startsWith('[') && input.endsWith(']')
}
function splitUtilityModifier(modifier) {
let slashIdx = modifier.lastIndexOf('/')
// If the `/` is inside an arbitrary, we want to find the previous one if any
// This logic probably isn't perfect but it should work for most cases
let arbitraryStartIdx = modifier.lastIndexOf('[', slashIdx)
let arbitraryEndIdx = modifier.indexOf(']', slashIdx)
let isNextToArbitrary = modifier[slashIdx - 1] === ']' || modifier[slashIdx + 1] === '['
// Backtrack to the previous `/` if the one we found was inside an arbitrary
if (!isNextToArbitrary) {
if (arbitraryStartIdx !== -1 && arbitraryEndIdx !== -1) {
if (arbitraryStartIdx < slashIdx && slashIdx < arbitraryEndIdx) {
slashIdx = modifier.lastIndexOf('/', arbitraryStartIdx)
}
}
}
if (slashIdx === -1 || slashIdx === modifier.length - 1) {
return [modifier, undefined]
}
let arbitrary = isArbitraryValue(modifier)
// The modifier could be of the form `[foo]/[bar]`
// We want to handle this case properly
// without affecting `[foo/bar]`
if (arbitrary && !modifier.includes(']/[')) {
return [modifier, undefined]
}
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
}
export function parseColorFormat(value) {
if (typeof value === 'string' && value.includes('<alpha-value>')) {
let oldValue = value
return ({ opacityValue = 1 }) => oldValue.replace(/<alpha-value>/g, opacityValue)
}
return value
}
function unwrapArbitraryModifier(modifier) {
return normalize(modifier.slice(1, -1))
}
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
if (options.values?.[modifier] !== undefined) {
return parseColorFormat(options.values?.[modifier])
}
// TODO: Hoist this up to getMatchingTypes or something
// We do this here because we need the alpha value (if any)
let [color, alpha] = splitUtilityModifier(modifier)
if (alpha !== undefined) {
let normalizedColor =
options.values?.[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined)
if (normalizedColor === undefined) {
return undefined
}
normalizedColor = parseColorFormat(normalizedColor)
if (isArbitraryValue(alpha)) {
return withAlphaValue(normalizedColor, unwrapArbitraryModifier(alpha))
}
if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
return undefined
}
return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
}
return asValue(modifier, options, { validate: validateColor })
}
export function asLookupValue(modifier, options = {}) {
return options.values?.[modifier]
}
function guess(validate) {
return (modifier, options) => {
return asValue(modifier, options, { validate })
}
}
export let typeMap = {
any: asValue,
color: asColor,
url: guess(url),
image: guess(image),
length: guess(length),
percentage: guess(percentage),
position: guess(position),
lookup: asLookupValue,
'generic-name': guess(genericName),
'family-name': guess(familyName),
number: guess(number),
'line-width': guess(lineWidth),
'absolute-size': guess(absoluteSize),
'relative-size': guess(relativeSize),
shadow: guess(shadow),
size: guess(backgroundSize),
}
let supportedTypes = Object.keys(typeMap)
function splitAtFirst(input, delim) {
let idx = input.indexOf(delim)
if (idx === -1) return [undefined, input]
return [input.slice(0, idx), input.slice(idx + 1)]
}
export function coerceValue(types, modifier, options, tailwindConfig) {
if (options.values && modifier in options.values) {
for (let { type } of types ?? []) {
let result = typeMap[type](modifier, options, {
tailwindConfig,
})
if (result === undefined) {
continue
}
return [result, type, null]
}
}
if (isArbitraryValue(modifier)) {
let arbitraryValue = modifier.slice(1, -1)
let [explicitType, value] = splitAtFirst(arbitraryValue, ':')
// It could be that this resolves to `url(https` which is not a valid
// identifier. We currently only support "simple" words with dashes or
// underscores. E.g.: family-name
if (!/^[\w-_]+$/g.test(explicitType)) {
value = arbitraryValue
}
//
else if (explicitType !== undefined && !supportedTypes.includes(explicitType)) {
return []
}
if (value.length > 0 && supportedTypes.includes(explicitType)) {
return [asValue(`[${value}]`, options), explicitType, null]
}
}
let matches = getMatchingTypes(types, modifier, options, tailwindConfig)
// Find first matching type
for (let match of matches) {
return match
}
return []
}
/**
*
* @param {{type: string}[]} types
* @param {string} rawModifier
* @param {any} options
* @param {any} tailwindConfig
* @returns {Iterator<[value: string, type: string, modifier: string | null]>}
*/
export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let [modifier, utilityModifier] = splitUtilityModifier(rawModifier)
let canUseUtilityModifier =
modifiersEnabled &&
options.modifiers != null &&
(options.modifiers === 'any' ||
(typeof options.modifiers === 'object' &&
((utilityModifier && isArbitraryValue(utilityModifier)) ||
utilityModifier in options.modifiers)))
if (!canUseUtilityModifier) {
modifier = rawModifier
utilityModifier = undefined
}
if (utilityModifier !== undefined && modifier === '') {
modifier = 'DEFAULT'
}
// Check the full value first
// TODO: Move to asValue… somehow
if (utilityModifier !== undefined) {
if (typeof options.modifiers === 'object') {
let configValue = options.modifiers?.[utilityModifier] ?? null
if (configValue !== null) {
utilityModifier = configValue
} else if (isArbitraryValue(utilityModifier)) {
utilityModifier = unwrapArbitraryModifier(utilityModifier)
}
}
}
for (let { type } of types ?? []) {
let result = typeMap[type](modifier, options, {
tailwindConfig,
})
if (result === undefined) {
continue
}
yield [result, type, utilityModifier ?? null]
}
}

View File

@@ -0,0 +1,33 @@
import parser from 'postcss-selector-parser'
/**
* @template {string | import('postcss-selector-parser').Root} T
*
* Prefix all classes in the selector with the given prefix
*
* It can take either a string or a selector AST and will return the same type
*
* @param {string} prefix
* @param {T} selector
* @param {boolean} prependNegative
* @returns {T}
*/
export default function (prefix, selector, prependNegative = false) {
if (prefix === '') {
return selector
}
/** @type {import('postcss-selector-parser').Root} */
let ast = typeof selector === 'string' ? parser().astSync(selector) : selector
ast.walkClasses((classSelector) => {
let baseClass = classSelector.value
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')
classSelector.value = shouldPlaceNegativeBeforePrefix
? `-${prefix}${baseClass.slice(1)}`
: `${prefix}${baseClass}`
})
return typeof selector === 'string' ? ast.toString() : ast
}

View File

@@ -0,0 +1,171 @@
/** @typedef {import('postcss-selector-parser').Root} Root */
/** @typedef {import('postcss-selector-parser').Selector} Selector */
/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
/** @typedef {import('postcss-selector-parser').Node} Node */
// There are some pseudo-elements that may or may not be:
// **Actionable**
// Zero or more user-action pseudo-classes may be attached to the pseudo-element itself
// structural-pseudo-classes are NOT allowed but we don't make
// The spec is not clear on whether this is allowed or not — but in practice it is.
// **Terminal**
// It MUST be placed at the end of a selector
//
// This is the required in the spec. However, some pseudo elements are not "terminal" because
// they represent a "boundary piercing" that is compiled out by a build step.
// **Jumpable**
// Any terminal element may "jump" over combinators when moving to the end of the selector
//
// This is a backwards-compat quirk of pseudo element variants from earlier versions of Tailwind CSS.
/** @typedef {'terminal' | 'actionable' | 'jumpable'} PseudoProperty */
/** @type {Record<string, PseudoProperty[]>} */
let elementProperties = {
// Pseudo elements from the spec
'::after': ['terminal', 'jumpable'],
'::backdrop': ['terminal', 'jumpable'],
'::before': ['terminal', 'jumpable'],
'::cue': ['terminal'],
'::cue-region': ['terminal'],
'::first-letter': ['terminal', 'jumpable'],
'::first-line': ['terminal', 'jumpable'],
'::grammar-error': ['terminal'],
'::marker': ['terminal', 'jumpable'],
'::part': ['terminal', 'actionable'],
'::placeholder': ['terminal', 'jumpable'],
'::selection': ['terminal', 'jumpable'],
'::slotted': ['terminal'],
'::spelling-error': ['terminal'],
'::target-text': ['terminal'],
// Pseudo elements from the spec with special rules
'::file-selector-button': ['terminal', 'actionable'],
// Library-specific pseudo elements used by component libraries
// These are Shadow DOM-like
'::deep': ['actionable'],
'::v-deep': ['actionable'],
'::ng-deep': ['actionable'],
// Note: As a rule, double colons (::) should be used instead of a single colon
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
// this distinction was not present in older versions of the W3C spec, most
// browsers support both syntaxes for the original pseudo-elements.
':after': ['terminal', 'jumpable'],
':before': ['terminal', 'jumpable'],
':first-letter': ['terminal', 'jumpable'],
':first-line': ['terminal', 'jumpable'],
':where': [],
':is': [],
':has': [],
// The default value is used when the pseudo-element is not recognized
// Because it's not recognized, we don't know if it's terminal or not
// So we assume it can be moved AND can have user-action pseudo classes attached to it
__default__: ['terminal', 'actionable'],
}
/**
* @param {Selector} sel
* @returns {Selector}
*/
export function movePseudos(sel) {
let [pseudos] = movablePseudos(sel)
// Remove all pseudo elements from their respective selectors
pseudos.forEach(([sel, pseudo]) => sel.removeChild(pseudo))
// Re-add them to the end of the selector in the correct order.
// This moves terminal pseudo elements to the end of the
// selector otherwise the selector will not be valid.
//
// Examples:
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
//
// The selector `::before:hover` does not work but we
// can make it work for you by flipping the order.
sel.nodes.push(...pseudos.map(([, pseudo]) => pseudo))
return sel
}
/** @typedef {[sel: Selector, pseudo: Pseudo, attachedTo: Pseudo | null]} MovablePseudo */
/** @typedef {[pseudos: MovablePseudo[], lastSeenElement: Pseudo | null]} MovablePseudosResult */
/**
* @param {Selector} sel
* @returns {MovablePseudosResult}
*/
function movablePseudos(sel) {
/** @type {MovablePseudo[]} */
let buffer = []
/** @type {Pseudo | null} */
let lastSeenElement = null
for (let node of sel.nodes) {
if (node.type === 'combinator') {
buffer = buffer.filter(([, node]) => propertiesForPseudo(node).includes('jumpable'))
lastSeenElement = null
} else if (node.type === 'pseudo') {
if (isMovablePseudoElement(node)) {
lastSeenElement = node
buffer.push([sel, node, null])
} else if (lastSeenElement && isAttachablePseudoClass(node, lastSeenElement)) {
buffer.push([sel, node, lastSeenElement])
} else {
lastSeenElement = null
}
for (let sub of node.nodes ?? []) {
let [movable, lastSeenElementInSub] = movablePseudos(sub)
lastSeenElement = lastSeenElementInSub || lastSeenElement
buffer.push(...movable)
}
}
}
return [buffer, lastSeenElement]
}
/**
* @param {Node} node
* @returns {boolean}
*/
function isPseudoElement(node) {
return node.value.startsWith('::') || elementProperties[node.value] !== undefined
}
/**
* @param {Node} node
* @returns {boolean}
*/
function isMovablePseudoElement(node) {
return isPseudoElement(node) && propertiesForPseudo(node).includes('terminal')
}
/**
* @param {Node} node
* @param {Pseudo} pseudo
* @returns {boolean}
*/
function isAttachablePseudoClass(node, pseudo) {
if (node.type !== 'pseudo') return false
if (isPseudoElement(node)) return false
return propertiesForPseudo(pseudo).includes('actionable')
}
/**
* @param {Pseudo} pseudo
* @returns {PseudoProperty[]}
*/
function propertiesForPseudo(pseudo) {
return elementProperties[pseudo.value] ?? elementProperties.__default__
}

View File

@@ -0,0 +1,26 @@
/**
* This function removes any uses of CSS variables used as an alpha channel
*
* This is required for selectors like `:visited` which do not allow
* changes in opacity or external control using CSS variables.
*
* @param {import('postcss').Container} container
* @param {string[]} toRemove
*/
export function removeAlphaVariables(container, toRemove) {
container.walkDecls((decl) => {
if (toRemove.includes(decl.prop)) {
decl.remove()
return
}
for (let varName of toRemove) {
if (decl.value.includes(`/ var(${varName})`)) {
decl.value = decl.value.replace(`/ var(${varName})`, '')
} else if (decl.value.includes(`/ var(${varName}, 1)`)) {
decl.value = decl.value.replace(`/ var(${varName}, 1)`, '')
}
}
})
}

View File

@@ -0,0 +1,277 @@
import negateValue from './negateValue'
import corePluginList from '../corePluginList'
import configurePlugins from './configurePlugins'
import colors from '../public/colors'
import { defaults } from './defaults'
import { toPath } from './toPath'
import { normalizeConfig } from './normalizeConfig'
import isPlainObject from './isPlainObject'
import { cloneDeep } from './cloneDeep'
import { parseColorFormat } from './pluginUtils'
import { withAlphaValue } from './withAlphaVariable'
import toColorValue from './toColorValue'
function isFunction(input) {
return typeof input === 'function'
}
function mergeWith(target, ...sources) {
let customizer = sources.pop()
for (let source of sources) {
for (let k in source) {
let merged = customizer(target[k], source[k])
if (merged === undefined) {
if (isPlainObject(target[k]) && isPlainObject(source[k])) {
target[k] = mergeWith({}, target[k], source[k], customizer)
} else {
target[k] = source[k]
}
} else {
target[k] = merged
}
}
}
return target
}
const configUtils = {
colors,
negative(scale) {
// TODO: Log that this function isn't really needed anymore?
return Object.keys(scale)
.filter((key) => scale[key] !== '0')
.reduce((negativeScale, key) => {
let negativeValue = negateValue(scale[key])
if (negativeValue !== undefined) {
negativeScale[`-${key}`] = negativeValue
}
return negativeScale
}, {})
},
breakpoints(screens) {
return Object.keys(screens)
.filter((key) => typeof screens[key] === 'string')
.reduce(
(breakpoints, key) => ({
...breakpoints,
[`screen-${key}`]: screens[key],
}),
{}
)
},
}
function value(valueToResolve, ...args) {
return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve
}
function collectExtends(items) {
return items.reduce((merged, { extend }) => {
return mergeWith(merged, extend, (mergedValue, extendValue) => {
if (mergedValue === undefined) {
return [extendValue]
}
if (Array.isArray(mergedValue)) {
return [extendValue, ...mergedValue]
}
return [extendValue, mergedValue]
})
}, {})
}
function mergeThemes(themes) {
return {
...themes.reduce((merged, theme) => defaults(merged, theme), {}),
// In order to resolve n config objects, we combine all of their `extend` properties
// into arrays instead of objects so they aren't overridden.
extend: collectExtends(themes),
}
}
function mergeExtensionCustomizer(merged, value) {
// When we have an array of objects, we do want to merge it
if (Array.isArray(merged) && isPlainObject(merged[0])) {
return merged.concat(value)
}
// When the incoming value is an array, and the existing config is an object, prepend the existing object
if (Array.isArray(value) && isPlainObject(value[0]) && isPlainObject(merged)) {
return [merged, ...value]
}
// Override arrays (for example for font-families, box-shadows, ...)
if (Array.isArray(value)) {
return value
}
// Execute default behaviour
return undefined
}
function mergeExtensions({ extend, ...theme }) {
return mergeWith(theme, extend, (themeValue, extensions) => {
// The `extend` property is an array, so we need to check if it contains any functions
if (!isFunction(themeValue) && !extensions.some(isFunction)) {
return mergeWith({}, themeValue, ...extensions, mergeExtensionCustomizer)
}
return (resolveThemePath, utils) =>
mergeWith(
{},
...[themeValue, ...extensions].map((e) => value(e, resolveThemePath, utils)),
mergeExtensionCustomizer
)
})
}
/**
*
* @param {string} key
* @return {Iterable<string[] & {alpha: string | undefined}>}
*/
function* toPaths(key) {
let path = toPath(key)
if (path.length === 0) {
return
}
yield path
if (Array.isArray(key)) {
return
}
let pattern = /^(.*?)\s*\/\s*([^/]+)$/
let matches = key.match(pattern)
if (matches !== null) {
let [, prefix, alpha] = matches
let newPath = toPath(prefix)
newPath.alpha = alpha
yield newPath
}
}
function resolveFunctionKeys(object) {
// theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5]
const resolvePath = (key, defaultValue) => {
for (const path of toPaths(key)) {
let index = 0
let val = object
while (val !== undefined && val !== null && index < path.length) {
val = val[path[index++]]
let shouldResolveAsFn =
isFunction(val) && (path.alpha === undefined || index <= path.length - 1)
val = shouldResolveAsFn ? val(resolvePath, configUtils) : val
}
if (val !== undefined) {
if (path.alpha !== undefined) {
let normalized = parseColorFormat(val)
return withAlphaValue(normalized, path.alpha, toColorValue(normalized))
}
if (isPlainObject(val)) {
return cloneDeep(val)
}
return val
}
}
return defaultValue
}
Object.assign(resolvePath, {
theme: resolvePath,
...configUtils,
})
return Object.keys(object).reduce((resolved, key) => {
resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key]
return resolved
}, {})
}
function extractPluginConfigs(configs) {
let allConfigs = []
configs.forEach((config) => {
allConfigs = [...allConfigs, config]
const plugins = config?.plugins ?? []
if (plugins.length === 0) {
return
}
plugins.forEach((plugin) => {
if (plugin.__isOptionsFunction) {
plugin = plugin()
}
allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])]
})
})
return allConfigs
}
function resolveCorePlugins(corePluginConfigs) {
const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => {
if (isFunction(corePluginConfig)) {
return corePluginConfig({ corePlugins: resolved })
}
return configurePlugins(corePluginConfig, resolved)
}, corePluginList)
return result
}
function resolvePluginLists(pluginLists) {
const result = [...pluginLists].reduceRight((resolved, pluginList) => {
return [...resolved, ...pluginList]
}, [])
return result
}
export default function resolveConfig(configs) {
let allConfigs = [
...extractPluginConfigs(configs),
{
prefix: '',
important: false,
separator: ':',
},
]
return normalizeConfig(
defaults(
{
theme: resolveFunctionKeys(
mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
),
corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
},
...allConfigs
)
)
}

View File

@@ -0,0 +1,68 @@
import fs from 'fs'
import path from 'path'
const defaultConfigFiles = [
'./tailwind.config.js',
'./tailwind.config.cjs',
'./tailwind.config.mjs',
'./tailwind.config.ts',
'./tailwind.config.cts',
'./tailwind.config.mts',
]
function isObject(value) {
return typeof value === 'object' && value !== null
}
function isEmpty(obj) {
return Object.keys(obj).length === 0
}
function isString(value) {
return typeof value === 'string' || value instanceof String
}
export default function resolveConfigPath(pathOrConfig) {
// require('tailwindcss')({ theme: ..., variants: ... })
if (isObject(pathOrConfig) && pathOrConfig.config === undefined && !isEmpty(pathOrConfig)) {
return null
}
// require('tailwindcss')({ config: 'custom-config.js' })
if (
isObject(pathOrConfig) &&
pathOrConfig.config !== undefined &&
isString(pathOrConfig.config)
) {
return path.resolve(pathOrConfig.config)
}
// require('tailwindcss')({ config: { theme: ..., variants: ... } })
if (
isObject(pathOrConfig) &&
pathOrConfig.config !== undefined &&
isObject(pathOrConfig.config)
) {
return null
}
// require('tailwindcss')('custom-config.js')
if (isString(pathOrConfig)) {
return path.resolve(pathOrConfig)
}
// require('tailwindcss')
return resolveDefaultConfigPath()
}
export function resolveDefaultConfigPath() {
for (const configFile of defaultConfigFiles) {
try {
const configPath = path.resolve(configFile)
fs.accessSync(configPath)
return configPath
} catch (err) {}
}
return null
}

View File

@@ -0,0 +1,10 @@
import postcss from 'postcss'
import cloneNodes from './cloneNodes'
export default function responsive(rules) {
return postcss
.atRule({
name: 'responsive',
})
.append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
}

View File

@@ -0,0 +1,48 @@
/**
* This splits a string on a top-level character.
*
* Regex doesn't support recursion (at least not the JS-flavored version).
* So we have to use a tiny state machine to keep track of paren placement.
*
* Expected behavior using commas:
* var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)
* ─┬─ ┬ ┬ ┬
* x x x ╰──────── Split because top-level
* ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens
*
* @param {string} input
* @param {string} separator
*/
export function splitAtTopLevelOnly(input, separator) {
let stack = []
let parts = []
let lastPos = 0
let isEscaped = false
for (let idx = 0; idx < input.length; idx++) {
let char = input[idx]
if (stack.length === 0 && char === separator[0] && !isEscaped) {
if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) {
parts.push(input.slice(lastPos, idx))
lastPos = idx + separator.length
}
}
isEscaped = isEscaped ? false : char === '\\'
if (char === '(' || char === '[' || char === '{') {
stack.push(char)
} else if (
(char === ')' && stack[stack.length - 1] === '(') ||
(char === ']' && stack[stack.length - 1] === '[') ||
(char === '}' && stack[stack.length - 1] === '{')
) {
stack.pop()
}
}
parts.push(input.slice(lastPos))
return parts
}

4
frontend/node_modules/tailwindcss/src/util/tap.js generated vendored Normal file
View File

@@ -0,0 +1,4 @@
export function tap(value, mutator) {
mutator(value)
return value
}

View File

@@ -0,0 +1,3 @@
export default function toColorValue(maybeFunction) {
return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction
}

26
frontend/node_modules/tailwindcss/src/util/toPath.js generated vendored Normal file
View File

@@ -0,0 +1,26 @@
/**
* Parse a path string into an array of path segments.
*
* Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators.
*
* Example:
* a -> ['a']
* a.b.c -> ['a', 'b', 'c']
* a[b].c -> ['a', 'b', 'c']
* a[b.c].e.f -> ['a', 'b.c', 'e', 'f']
* a[b][c][d] -> ['a', 'b', 'c', 'd']
*
* @param {string|string[]} path
**/
export function toPath(path) {
if (Array.isArray(path)) return path
let openBrackets = path.split('[').length - 1
let closedBrackets = path.split(']').length - 1
if (openBrackets !== closedBrackets) {
throw new Error(`Path is invalid. Has unbalanced brackets: ${path}`)
}
return path.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)
}

View File

@@ -0,0 +1,62 @@
import postcss from 'postcss'
import isPlainObject from './isPlainObject'
export default function transformThemeValue(themeSection) {
if (['fontSize', 'outline'].includes(themeSection)) {
return (value) => {
if (typeof value === 'function') value = value({})
if (Array.isArray(value)) value = value[0]
return value
}
}
if (themeSection === 'fontFamily') {
return (value) => {
if (typeof value === 'function') value = value({})
let families = Array.isArray(value) && isPlainObject(value[1]) ? value[0] : value
return Array.isArray(families) ? families.join(', ') : families
}
}
if (
[
'boxShadow',
'transitionProperty',
'transitionDuration',
'transitionDelay',
'transitionTimingFunction',
'backgroundImage',
'backgroundSize',
'backgroundColor',
'cursor',
'animation',
].includes(themeSection)
) {
return (value) => {
if (typeof value === 'function') value = value({})
if (Array.isArray(value)) value = value.join(', ')
return value
}
}
// For backwards compatibility reasons, before we switched to underscores
// instead of commas for arbitrary values.
if (['gridTemplateColumns', 'gridTemplateRows', 'objectPosition'].includes(themeSection)) {
return (value) => {
if (typeof value === 'function') value = value({})
if (typeof value === 'string') value = postcss.list.comma(value).join(' ')
return value
}
}
return (value, opts = {}) => {
if (typeof value === 'function') {
value = value(opts)
}
return value
}
}

View File

@@ -0,0 +1,26 @@
import log from './log'
export function validateConfig(config) {
if (config.content.files.length === 0) {
log.warn('content-problems', [
'The `content` option in your Tailwind CSS configuration is missing or empty.',
'Configure your content sources or your generated CSS will be missing styles.',
'https://tailwindcss.com/docs/content-configuration',
])
}
// Warn if the line-clamp plugin is installed
try {
let plugin = require('@tailwindcss/line-clamp')
if (config.plugins.includes(plugin)) {
log.warn('line-clamp-in-core', [
'As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default.',
'Remove it from the `plugins` array in your configuration to eliminate this warning.',
])
config.plugins = config.plugins.filter((p) => p !== plugin)
}
} catch {}
return config
}

View File

@@ -0,0 +1,34 @@
import { length, percentage } from './dataTypes'
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
/**
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/background-size#formal_syntax
*
* background-size =
* <bg-size>#
*
* <bg-size> =
* [ <length-percentage [0,∞]> | auto ]{1,2} |
* cover |
* contain
*
* <length-percentage> =
* <length> |
* <percentage>
*
* @param {string} value
*/
export function backgroundSize(value) {
let keywordValues = ['cover', 'contain']
// the <length-percentage> type will probably be a css function
// so we have to use `splitAtTopLevelOnly`
return splitAtTopLevelOnly(value, ',').every((part) => {
let sizes = splitAtTopLevelOnly(part, '_').filter(Boolean)
if (sizes.length === 1 && keywordValues.includes(sizes[0])) return true
if (sizes.length !== 1 && sizes.length !== 2) return false
return sizes.every((size) => length(size) || percentage(size) || size === 'auto')
})
}

Some files were not shown because too many files have changed in this diff Show More