diff --git a/.changeset/stale-bugs-reflect.md b/.changeset/stale-bugs-reflect.md new file mode 100644 index 00000000..86bdb978 --- /dev/null +++ b/.changeset/stale-bugs-reflect.md @@ -0,0 +1,5 @@ +--- +'@cobalt-ui/plugin-css': minor +--- + +Add Utility CSS generation diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 9adf52ad..75e300a5 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -273,6 +273,8 @@ i { --vp-c-brand-3: oklch(60% 0.216564 269); --vp-c-brand-soft: oklch(60% 0.216564 269/0.2); + --vp-code-color: var(--vp-c-text-1); + --vp-c-tip-1: var(--vp-c-brand-1); --vp-c-tip-2: var(--vp-c-brand-2); --vp-c-tip-3: var(--vp-c-brand-3); diff --git a/docs/integrations/css.md b/docs/integrations/css.md index d0df48b5..708083d2 100644 --- a/docs/integrations/css.md +++ b/docs/integrations/css.md @@ -105,103 +105,122 @@ export default { If your tokens are saved locally (by default in `src/tokens/tokens.css`), VS Code will automatically pick up on this file and allow autocompletions. However, if you’re publishing your package to npm, it will ignore `node_modules`. The [CSS Variable Autocomplete](https://marketplace.visualstudio.com/items?itemName=vunguyentuan.vscode-css-variables) extension lets you add additional files for autocompletion. -## Color tokens +## Utility CSS -::: code-group - -```js [tokens.config.mjs] {5} -/** @type import('@cobalt-ui/core').Config */ -export default { - plugins: [ - pluginCSS({ - colorFormat: 'oklch', - }), - ], -}; -``` - -::: +By default, this plugin will **only generate CSS variables**. To generate some lightweight utility CSS classes from your tokens _a la_ Tailwind or Bootstrap Utility CSS, specify a `utility` object to enable the types of utility classes you’d like to generate. -By specifying a `colorFormat`, you can transform all your colors to [any browser-supported colorspace](https://www.w3.org/TR/css-color-4/). Any of the following colorspaces are accepted: +By default, **all groups are off**. to generate a group, pass its name as the key, along with an array of **token selectors** (wildcards) to match tokens. For example, the following config: -- [hex](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) (default) -- [rgb](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb) -- [hsl](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl) -- [hwb](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb) -- [lab](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab) -- [lch](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch) -- [oklab](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab) -- [oklch](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) -- [p3](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color) -- [srgb-linear](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) -- [xyz-d50](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) -- [xyz-d65](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) - -If you are unfamiliar with these colorspaces, the default `hex` value is best for most users (though [you should use OKLCH to define your colors](https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl)). +```js +pluginCSS({ + utility: { + bg: ['color.semantic.*'], + text: ['color.semantic.*'], + margin: ['space.*'], + }, +}); +``` -## Link tokens +…will generate the following CSS: -Say you have [Link tokens](/tokens/link) in your `tokens.json`: - -::: code-group - -```json [JSON] -{ - "icon": { - "alert": { - "$type": "link", - "$value": "./icon/alert.svg" - } - } +```css +.bg-primary { + background-color: var(--color-semantic-primary); } +.bg-secondary { + background-color: var(--color-semantic-secondary); +} +.text-primary { + color: var(--color-semantic-primary); +} +.text-secondary { + color: var(--color-semantic-secondary); +} +.mt-1 { + margin-top: 0.25rem; +} +.mr-1 { + margin-right: 0.25rem; +} +.mb-1 { + margin-bottom: 0.25rem; +} +/* … */ ``` -```yaml [YAML] -icon: - alert: - $type: link - value: ./icon/alert.svg -``` +Here are all the groups available, along with the associated CSS: + +| Group | Class Name | CSS | +| :---------- | :----------------------- | :---------------------------------------------------------------------- | +| **bg** | `.bg-[token]` | `background-color: [value]` \* | +| **border** | `.border-[token]` | `border: [value]` | +| | `.border-top-[token]` | `border-top: [value]` | +| | `.border-right-[token]` | `border-right: [value]` | +| | `.border-bottom-[token]` | `border-bottom: [value]` | +| | `.border-left-[token]` | `border-left: [value]` | +| **font** | `.font-[token]` | (all typographic properties of [Typography Tokens](/tokens/typography)) | +| **gap** | `.gap-[token]` | `gap: [value]` | +| | `.gap-col-[token]` | `column-gap: [value]` | +| | `.gap-row-[token]` | `row-gap: [value]` | +| **margin** | `.mt-[token]` | `margin-top: [value]` | +| | `.mr-[token]` | `margin-right: [value]` | +| | `.mb-[token]` | `margin-bottom: [value]` | +| | `.ml-[token]` | `margin-left: [value]` | +| | `.ms-[token]` | `margin-inline-start: [value]` | +| | `.me-[token]` | `margin-inline-end: [value]` | +| | `.mx-[token]` | `margin-left: [value]; margin-right: [value]` | +| | `.my-[token]` | `margin-top: [value]; margin-bottom: [value]` | +| | `.ma-[token]` | `margin: [value]` | +| **padding** | `.pt-[token]` | `padding-top: [value]` | +| | `.pr-[token]` | `padding-right: [value]` | +| | `.pb-[token]` | `padding-bottom: [value]` | +| | `.pl-[token]` | `padding-left: [value]` | +| | `.px-[token]` | `padding-left: [value]; padding-right: [value]` | +| | `.py-[token]` | `padding-top: [value]; padding-bottom: [value]` | +| | `.pa-[token]` | `padding: [value]` | +| **shadow** | `.shadow-[token]` | `box-shadow: [value]` | +| **text** | `.text-[token]` | `color: [value]` \* | + +::: info + +The **bg** and **text** groups also accept [Gradient Tokens](/tokens/gradient), and will generate the appropriate CSS for those. ::: -By default, consuming those will print values as-is: +### Naming -```css -:root { - --icon-alert: url('./icon/alert.svg'); -} +The `utility` mapping will use the remainder of the token ID, minus the selector (but will always keep the last segment, no matter what). For example, if you had a `color.semantic.primary` token, here’s how you’d control the generated CSS name: -.icon-alert { - background-image: var(--icon-alert); -} -``` +| Selector | CSS Class | +| :--------------------------- | :--------------------------- | +| `['color.semantic.primary']` | `.bg-primary` | +| `['color.semantic.*']` | `.bg-primary` | +| `['color.*']` | `.bg-semantic-primary` | +| `['*']` | `.bg-color-semantic-primary` | -In some scenarios this is preferable, but in others, this may result in too many requests and may result in degraded performance. You can set `embedFiles: true` to generate the following instead: +You can use as much or as little of the token ID as you like, according to what makes sense to you. -```css -:root { - --icon-alert: url('image/svg+xml;utf8,'); -} +This comes up a lot with spacing ([Dimension](/tokens/dimension)) tokens: if, for example, you had a `space.layout.xs` token, you could specify `['space.*']` if you wanted the CSS class `.mt-layout-xs`, or `['space.layout.*']` if you wanted `.mt-xs`. Only you know your DS and what makes the most sense, and when a name is either too long or too short. -.icon-alert { - background-image: var(--icon-alert); -} -``` +Note that **this utility does not let you rename token IDs** for ease of use. If you want to remap and/or mix and combine tokens into different class names, you’ll have to write your own CSS manually (using the generated CSS variables, of course). -::: tip +### Comparison to Tailwind -The CSS plugin uses [SVGO](https://github.com/svg/svgo) to optimize SVGs at lossless quality. However, raster images won’t be optimized so quality isn’t degraded. +This plugin’s utility CSS can be used **in place of Tailwind,** and probably works best if the project isn’t based on Tailwind. It’s simply a lighter-weight way of using your design tokens directly in CSS. For comparison: -::: +- ✅ **It respects dynamic vars.** The generated utility CSS references core vars, which means all your modes and [Mode Selectors](#mode-selectors) are preserved, and all the dynamism of your variables are kept. +- ✅ **Direct 1:1 mapping with tokens**. There’s no additional translation layer, or renaming into Tailwind. This just references your tokens as you’ve named them (with the only learning curve being familiarizing yourself with a few prefixes). +- ✅ **No additional build step or dependencies.** Utility CSS gets generated along with the rest of your DS code, without any additional setup. +- ✅ **No code scanning.** You only generate what you need, so no need to scan your code. +- ❌ **No automatic treeshaking.** Conversely, you control everything in the config, so you’ll have to configure for yourself how much CSS to generate from your DS (for most DSs it’s a negligible amount of CSS, however, huge DSs may need to be more selective). -[Read more about the advantages to inlining files](https://css-tricks.com/data-uris/) +If you are already using Tailwind in your project, you may find the [Tailwind Plugin](https://cobalt-ui.pages.dev/docs/integrations/tailwind/) more useful. -## Generate name +## Renaming CSS variables Use the `generateName()` option to customize the naming of CSS tokens, such as adding prefixes/suffixes, or just changing how the default variable naming works in general. -### Default naming +#### Default naming By default, Cobalt takes your dot-separated token IDs and… @@ -209,7 +228,7 @@ By default, Cobalt takes your dot-separated token IDs and… - camelCases any group or token name that has a space in the middle of it - Joins the normalized segments together with a single dashes -### Custom naming +#### Custom naming To override specific or all CSS variable names yourself, use the `generateName()` option: @@ -366,7 +385,7 @@ That will generate the following: [Learn more about modes](/guides/modes) -## Transform +## Transforming values Inside plugin options, you can specify an optional `transform()` function. @@ -423,9 +442,105 @@ export default { ::: -## Sass interop +## Special token behavior + +Helpful information for @cobalt-ui/plugin-css’ handling of specific token types. + +### Color tokens + +::: code-group + +```js [tokens.config.mjs] {5} +/** @type import('@cobalt-ui/core').Config */ +export default { + plugins: [ + pluginCSS({ + colorFormat: 'oklch', + }), + ], +}; +``` + +::: + +By specifying a `colorFormat`, you can transform all your colors to [any browser-supported colorspace](https://www.w3.org/TR/css-color-4/). Any of the following colorspaces are accepted: + +- [hex](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) (default) +- [rgb](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb) +- [hsl](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl) +- [hwb](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb) +- [lab](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab) +- [lch](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch) +- [oklab](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab) +- [oklch](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) +- [p3](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color) +- [srgb-linear](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) +- [xyz-d50](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) +- [xyz-d65](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) + +If you are unfamiliar with these colorspaces, the default `hex` value is best for most users (though [you should use OKLCH to define your colors](https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl)). + +### Link tokens + +Say you have [Link tokens](/tokens/link) in your `tokens.json`: + +::: code-group + +```json [JSON] +{ + "icon": { + "alert": { + "$type": "link", + "$value": "./icon/alert.svg" + } + } +} +``` + +```yaml [YAML] +icon: + alert: + $type: link + value: ./icon/alert.svg +``` + +::: + +By default, consuming those will print values as-is: + +```css +:root { + --icon-alert: url('./icon/alert.svg'); +} + +.icon-alert { + background-image: var(--icon-alert); +} +``` + +In some scenarios this is preferable, but in others, this may result in too many requests and may result in degraded performance. You can set `embedFiles: true` to generate the following instead: + +```css +:root { + --icon-alert: url('image/svg+xml;utf8,'); +} + +.icon-alert { + background-image: var(--icon-alert); +} +``` + +::: tip + +The CSS plugin uses [SVGO](https://github.com/svg/svgo) to optimize SVGs at lossless quality. However, raster images won’t be optimized so quality isn’t degraded. + +::: + +[Read more about the advantages to inlining files](https://css-tricks.com/data-uris/) + +## Sass typechecking -If you’re using Sass in your project, you can load this plugin through [@cobalt-ui/plugin-sass](/integrations/sass), which gives you all the benefits of this plugin plus Sass’ typechecking (the Sass plugin’s normal Sass vars will be swapped for CSS vars, but it will still error on any mistyped tokens). +If you’re using Sass in your project, you can load this plugin through [@cobalt-ui/plugin-sass](/integrations/sass), which lets you keep the dynamism of CSS variables but lets Sass check for typos (by default, the Sass plugin uses static values). To use this, replace this plugin with @cobalt-ui/plugin-sass in `tokens.config.mjs` and move your options into the `pluginCSS: {}` option: diff --git a/docs/package.json b/docs/package.json index 43bf6b7b..65fec578 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,6 +13,6 @@ }, "devDependencies": { "vite": "^5.0.2", - "vitepress": "1.0.0-rc.29" + "vitepress": "1.0.0-rc.30" } } diff --git a/packages/plugin-css/.npmignore b/packages/plugin-css/.npmignore index 1a74c3d9..f27ec4d2 100644 --- a/packages/plugin-css/.npmignore +++ b/packages/plugin-css/.npmignore @@ -1,2 +1,3 @@ +*.test.* test tsconfig.json diff --git a/packages/plugin-css/src/index.ts b/packages/plugin-css/src/index.ts index 709345a9..d2612193 100644 --- a/packages/plugin-css/src/index.ts +++ b/packages/plugin-css/src/index.ts @@ -1,26 +1,15 @@ -import type { - BuildResult, - ParsedColorToken, - ParsedCubicBezierToken, - ParsedDimensionToken, - ParsedDurationToken, - ParsedFontFamilyToken, - ParsedFontWeightToken, - ParsedNumberToken, - ParsedLinkToken, - ParsedStrokeStyleToken, - ParsedToken, - Plugin, - ResolvedConfig, -} from '@cobalt-ui/core'; -import {indent, isAlias, kebabinate, FG_YELLOW, RESET} from '@cobalt-ui/utils'; -import {clampChroma, converter, formatCss, formatHex, formatHex8, formatHsl, formatRgb, parse as parseColor} from 'culori'; -import {CustomNameGenerator, DASH_PREFIX_RE, defaultNameGenerator, makeNameGenerator} from './utils/generate-token-name.js'; +import type {BuildResult, ParsedToken, Plugin, ResolvedConfig} from '@cobalt-ui/core'; +import {indent, FG_YELLOW, RESET} from '@cobalt-ui/utils'; +import {formatCss} from 'culori'; +import defaultTransformer, {type ColorFormat, toRGB} from './transform/index.js'; +import {CustomNameGenerator, isTokenMatch, makeNameGenerator} from './utils/token.js'; import {encode} from './utils/encode.js'; -import {formatFontNames} from './utils/format-font-names.js'; -import {isTokenMatch} from './utils/is-token-match.js'; +import generateUtilityCSS, {type UtilityCSSGroup} from './utils/utility-css.js'; -export {makeNameGenerator as _INTERNAL_makeNameGenerator, defaultNameGenerator} from './utils/generate-token-name.js'; +export {makeNameGenerator as _INTERNAL_makeNameGenerator, defaultTransformer}; +export * from './utils/utility-css.js'; +export * from './utils/token.js'; +export * from './transform/index.js'; const SELECTOR_BRACKET_RE = /\s*{/; const HEX_RE = /#[0-9a-f]{3,8}/g; @@ -51,24 +40,13 @@ export interface Options { /** enable P3 color enhancement? (default: true) */ p3?: boolean; /** normalize all color outputs to format (default: "hex") or specify "none" to keep as-is */ - colorFormat?: 'none' | 'hex' | 'rgb' | 'hsl' | 'hwb' | 'srgb-linear' | 'p3' | 'lab' | 'lch' | 'oklab' | 'oklch' | 'xyz-d50' | 'xyz-d65'; + colorFormat?: ColorFormat; /** generate variable name */ generateName?: CustomNameGenerator; + /** generate utility CSS? */ + utility?: Record; } -/** ⚠️ Important! We do NOT want to parse as P3. We want to parse as sRGB, then expand 1:1 to P3. @see https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/ */ -const toHSL = converter('hsl'); -const toHWB = converter('hwb'); -const toLab = converter('lab'); -const toLch = converter('lch'); -const toOklab = converter('oklab'); -const toOklch = converter('oklch'); -const toP3 = converter('p3'); -const toRGB = converter('rgb'); -const toRGBLinear = converter('lrgb'); -const toXYZ50 = converter('xyz50'); -const toXYZ65 = converter('xyz65'); - export default function pluginCSS(options?: Options): Plugin { let config: ResolvedConfig; let filename = options?.filename || './tokens.css'; @@ -118,6 +96,7 @@ export default function pluginCSS(options?: Options): Plugin { config = c; }, async build({tokens, metadata}): Promise { + const tokenIdToRefs: Record = {}; const tokenVals: {[variableName: string]: any} = {}; const modeVals: {[selector: string]: {[variableName: string]: any}} = {}; const selectors: string[] = []; @@ -127,10 +106,14 @@ export default function pluginCSS(options?: Options): Plugin { if (value === undefined || value === null) { value = defaultTransformer(token, {prefix, colorFormat, generateName, tokens}); } + + const ref = generateName(token.id, token); + tokenIdToRefs[token.id] = ref; + switch (token.$type) { case 'link': { if (options?.embedFiles) value = encode(value as string, config.outDir); - tokenVals[generateName(token.id, token)] = value; + tokenVals[ref] = value; break; } case 'typography': { @@ -140,7 +123,7 @@ export default function pluginCSS(options?: Options): Plugin { break; } default: { - tokenVals[generateName(token.id, token)] = value; + tokenVals[ref] = value; break; } } @@ -259,6 +242,13 @@ export default function pluginCSS(options?: Options): Plugin { code.push(''); + // Utility CSS + if (options?.utility && Object.keys(options.utility).length) { + code.push(generateUtilityCSS(options.utility, {refs: tokenIdToRefs, tokens})); + } + + code.push(''); + return [ { filename, @@ -269,300 +259,6 @@ export default function pluginCSS(options?: Options): Plugin { }; } -/** transform color */ -export function transformColor(value: ParsedColorToken['$value'], colorFormat: NonNullable): string { - if (colorFormat === 'none') { - return String(value); - } - const parsed = parseColor(value); - if (!parsed) throw new Error(`invalid color "${value}"`); - switch (colorFormat) { - case 'rgb': { - return formatRgb(clampChroma(toRGB(value)!, 'rgb')); - } - case 'hex': { - const rgb = clampChroma(toRGB(value)!, 'rgb'); - return typeof parsed.alpha === 'number' && parsed.alpha < 1 ? formatHex8(rgb) : formatHex(rgb); - } - case 'hsl': { - return formatHsl(clampChroma(toHSL(value)!, 'rgb')); - } - case 'hwb': { - return formatCss(clampChroma(toHWB(value)!, 'rgb')); - } - case 'lab': { - return formatCss(toLab(value)!); - } - case 'lch': { - return formatCss(toLch(value)!); - } - case 'oklab': { - return formatCss(toOklab(value)!); - } - case 'oklch': { - return formatCss(toOklch(value)!); - } - case 'p3': { - return formatCss(toP3(value)!); - } - case 'srgb-linear': { - return formatCss(toRGBLinear(value)!); - } - case 'xyz-d50': { - return formatCss(toXYZ50(value)!); - } - case 'xyz-d65': { - return formatCss(toXYZ65(value)!); - } - } -} -/** transform dimension */ -export function transformDimension(value: ParsedDimensionToken['$value']): string { - return String(value); -} -/** transform duration */ -export function transformDuration(value: ParsedDurationToken['$value']): string { - return String(value); -} -/** transform font family */ -export function transformFontFamily(value: ParsedFontFamilyToken['$value']): string { - return formatFontNames(value); -} -/** transform font weight */ -export function transformFontWeight(value: ParsedFontWeightToken['$value']): number { - return Number(value); -} -/** transform cubic beziér */ -export function transformCubicBezier(value: ParsedCubicBezierToken['$value']): string { - return `cubic-bezier(${value.join(', ')})`; -} -/** transform number */ -export function transformNumber(value: ParsedNumberToken['$value']): number { - return Number(value); -} -/** transform file */ -export function transformLink(value: ParsedLinkToken['$value']): string { - return `url('${value}')`; -} -/** transform stroke style */ -export function transformStrokeStyle(value: ParsedStrokeStyleToken['$value']): string { - if (typeof value === 'string') { - return value; - } - return 'dashed'; // CSS doesn’t support custom dashes or line caps, so just convert to `dashed` -} - -export function defaultTransformer( - token: ParsedToken, - { - colorFormat = 'hex', - generateName, - mode, - prefix, - tokens, - }: { - colorFormat: Options['colorFormat']; - generateName?: ReturnType; - mode?: string; - prefix?: string; - tokens?: ParsedToken[]; - }, -): string | number | Record { - switch (token.$type) { - // base tokens - case 'color': { - const {originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal, {prefix, tokens, generateName}); - } - return transformColor(originalVal, colorFormat); // note: use original value because it may have been normalized to hex (which matters if it wasn’t in sRGB gamut to begin with) - } - case 'dimension': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformDimension(value); - } - case 'duration': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformDuration(value); - } - case 'font' as 'fontFamily': // @deprecated (but keep support for now) - case 'fontFamily': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformFontFamily(value); - } - case 'fontWeight': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformFontWeight(value); - } - case 'cubicBezier': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformCubicBezier(value); - } - case 'number': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformNumber(value); - } - case 'link': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformLink(value); - } - case 'strokeStyle': { - const {value, originalVal} = getMode(token, mode); - if (isAlias(originalVal)) { - return varRef(originalVal as string, {prefix, tokens, generateName}); - } - return transformStrokeStyle(value); - } - // composite tokens - case 'border': { - const {value, originalVal} = getMode(token, mode); - if (typeof originalVal === 'string') { - return varRef(originalVal, {prefix, tokens, generateName}); - } - const width = isAlias(originalVal.width) ? varRef(originalVal.width, {prefix, tokens, generateName}) : transformDimension(value.width); - const color = isAlias(originalVal.color) ? varRef(originalVal.color, {prefix, tokens, generateName}) : transformColor(originalVal.color, colorFormat); - const style = isAlias(originalVal.style) ? varRef(originalVal.style as string, {prefix, tokens, generateName}) : transformStrokeStyle(value.style); - return `${width} ${style} ${color}`; - } - case 'shadow': { - let {value, originalVal} = getMode(token, mode); - if (typeof originalVal === 'string') { - return varRef(originalVal, {prefix, tokens, generateName}); - } - - // handle backwards compat for previous versions that didn’t always return array - if (!Array.isArray(value)) value = [value]; - if (!Array.isArray(originalVal)) originalVal = [originalVal]; - - return value - .map((shadow, i) => { - const origShadow = originalVal[i]!; - if (typeof origShadow === 'string') { - return varRef(origShadow, {prefix, tokens, generateName}); - } - const offsetX = isAlias(origShadow.offsetX) ? varRef(origShadow.offsetX, {prefix, tokens, generateName}) : transformDimension(shadow.offsetX); - const offsetY = isAlias(origShadow.offsetY) ? varRef(origShadow.offsetY, {prefix, tokens, generateName}) : transformDimension(shadow.offsetY); - const blur = isAlias(origShadow.blur) ? varRef(origShadow.blur, {prefix, tokens, generateName}) : transformDimension(shadow.blur); - const spread = isAlias(origShadow.spread) ? varRef(origShadow.spread, {prefix, tokens, generateName}) : transformDimension(shadow.spread); - const color = isAlias(origShadow.color) ? varRef(origShadow.color, {prefix, tokens, generateName}) : transformColor(origShadow.color, colorFormat); - return `${shadow.inset ? 'inset ' : ''}${offsetX} ${offsetY} ${blur} ${spread} ${color}`; - }) - .join(', '); - } - case 'gradient': { - const {value, originalVal} = getMode(token, mode); - if (typeof originalVal === 'string') { - return varRef(originalVal, {prefix, tokens, generateName}); - } - return value - .map((gradient, i) => { - const origGradient = originalVal[i]!; - if (typeof origGradient === 'string') { - return varRef(origGradient, {prefix, tokens, generateName}); - } - const color = isAlias(origGradient.color) ? varRef(origGradient.color, {prefix, tokens, generateName}) : transformColor(origGradient.color, colorFormat); - const stop = isAlias(origGradient.position) ? varRef(origGradient.position as any, {prefix, tokens, generateName}) : `${100 * gradient.position}%`; - return `${color} ${stop}`; - }) - .join(', '); - } - case 'transition': { - const {value, originalVal} = getMode(token, mode); - if (typeof originalVal === 'string') { - return varRef(originalVal, {prefix, tokens, generateName}); - } - const duration = isAlias(originalVal.duration) ? varRef(originalVal.duration, {prefix, tokens, generateName}) : transformDuration(value.duration); - let delay: string | undefined = undefined; - if (value.delay) { - delay = isAlias(originalVal.delay) ? varRef(originalVal.delay, {prefix, tokens, generateName}) : transformDuration(value.delay); - } - const timingFunction = isAlias(originalVal.timingFunction) ? varRef(originalVal.timingFunction as any, {prefix, tokens, generateName}) : transformCubicBezier(value.timingFunction); - return `${duration} ${delay ?? ''} ${timingFunction}`; - } - case 'typography': { - const {value, originalVal} = getMode(token, mode); - if (typeof originalVal === 'string') { - return varRef(originalVal, {prefix, tokens, generateName}); - } - const output: Record = {}; - for (const [k, v] of Object.entries(value)) { - const formatter = k === 'fontFamily' ? transformFontFamily : (val: any): string => String(val); - output[kebabinate(k)] = isAlias((originalVal as any)[k] as any) ? varRef((originalVal as any)[k], {prefix, tokens, generateName}) : formatter(v as any); - } - return output; - } - default: { - throw new Error(`No transformer defined for $type: ${(token as any).$type} tokens`); - } - } -} - -function getMode(token: T, mode?: string): {value: T['$value']; originalVal: T['$value'] | string} { - if (mode) { - if (!token.$extensions?.mode || !token.$extensions.mode[mode]) throw new Error(`Token ${token.id} missing "$extensions.mode.${mode}"`); - return { - value: token.$extensions.mode[mode]!, - originalVal: token._original.$extensions.mode[mode]!, - }; - } - return {value: token.$value, originalVal: token._original.$value}; -} - -/** - * Reference an existing CSS var - * - */ -export function varRef( - id: string, - options?: { - prefix?: string; - suffix?: string; - // note: the following properties are optional to preserve backwards-compat without a breaking change - tokens?: ParsedToken[]; - generateName?: ReturnType; - }, -): string { - let refID = id; - if (isAlias(id)) { - // unclear if mode is ever appended to id when passed here, but leaving for safety in case - const [rootID, _mode] = id.substring(1, id.length - 1).split('#'); - refID = rootID!; - } - - const token = options?.tokens?.find((t) => t.id === refID); - - if (!token) { - console.warn(`Tried to reference variable with id: ${refID}, no token found`); // eslint-disable-line no-console - } - - // suffix is only used internally (one place in plugin-sass), so handle it here rather than clutter the public API in defaultNameGenerator - const normalizedSuffix = options?.suffix ? `-${options?.suffix.replace(DASH_PREFIX_RE, '')}` : ''; - const variableId = refID + normalizedSuffix; - - return `var(${options?.generateName?.(variableId, token) ?? defaultNameGenerator(variableId, options?.prefix)})`; -} - /** @deprecated parse legacy modeSelector */ function parseLegacyModeSelector(modeID: string): [string, string] { if (!modeID.includes('#')) throw new Error(`modeSelector key must have "#" character`); diff --git a/packages/plugin-css/src/transform/border.ts b/packages/plugin-css/src/transform/border.ts new file mode 100644 index 00000000..34f1e134 --- /dev/null +++ b/packages/plugin-css/src/transform/border.ts @@ -0,0 +1,20 @@ +import {ParsedBorderToken, ParsedToken} from '@cobalt-ui/core'; +import {isAlias} from '@cobalt-ui/utils'; +import {getMode, makeNameGenerator, varRef} from '../utils/token.js'; +import transformColor, {type ColorFormat} from './color.js'; +import transformDimension from './dimension.js'; +import transformStrokeStyle from './stroke-style.js'; + +export default function transformBorder( + token: ParsedBorderToken, + {colorFormat, mode, prefix, tokens, generateName}: {colorFormat: ColorFormat; mode?: string; prefix?: string; tokens?: ParsedToken[]; generateName?: ReturnType}, +): string { + const {value, originalVal} = getMode(token, mode); + if (typeof originalVal === 'string') { + return varRef(originalVal, {prefix, tokens, generateName}); + } + const width = isAlias(originalVal.width) ? varRef(originalVal.width, {prefix, tokens, generateName}) : transformDimension(value.width); + const color = isAlias(originalVal.color) ? varRef(originalVal.color, {prefix, tokens, generateName}) : transformColor(originalVal.color, colorFormat); + const style = isAlias(originalVal.style) ? varRef(originalVal.style as string, {prefix, tokens, generateName}) : transformStrokeStyle(value.style); + return `${width} ${style} ${color}`; +} diff --git a/packages/plugin-css/src/transform/color.ts b/packages/plugin-css/src/transform/color.ts new file mode 100644 index 00000000..e17557bb --- /dev/null +++ b/packages/plugin-css/src/transform/color.ts @@ -0,0 +1,65 @@ +import type {ParsedColorToken} from '@cobalt-ui/core'; +import {formatCss, formatRgb, clampChroma, formatHex8, formatHex, formatHsl, converter, parse as parseColor} from 'culori'; + +/** normalize all color outputs to format (default: "hex") or specify "none" to keep as-is */ +export type ColorFormat = 'none' | 'hex' | 'rgb' | 'hsl' | 'hwb' | 'srgb-linear' | 'p3' | 'lab' | 'lch' | 'oklab' | 'oklch' | 'xyz-d50' | 'xyz-d65'; + +/** ⚠️ Important! We do NOT want to parse as P3. We want to parse as sRGB, then expand 1:1 to P3. @see https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/ */ +export const toHSL = converter('hsl'); +export const toHWB = converter('hwb'); +export const toLab = converter('lab'); +export const toLch = converter('lch'); +export const toOklab = converter('oklab'); +export const toOklch = converter('oklch'); +export const toP3 = converter('p3'); +export const toRGB = converter('rgb'); +export const toRGBLinear = converter('lrgb'); +export const toXYZ50 = converter('xyz50'); +export const toXYZ65 = converter('xyz65'); + +export default function transformColor(value: ParsedColorToken['$value'], colorFormat: ColorFormat): string { + if (colorFormat === 'none') { + return String(value); + } + const parsed = parseColor(value); + if (!parsed) throw new Error(`invalid color "${value}"`); + switch (colorFormat) { + case 'rgb': { + return formatRgb(clampChroma(toRGB(value)!, 'rgb')); + } + case 'hex': { + const rgb = clampChroma(toRGB(value)!, 'rgb'); + return typeof parsed.alpha === 'number' && parsed.alpha < 1 ? formatHex8(rgb) : formatHex(rgb); + } + case 'hsl': { + return formatHsl(clampChroma(toHSL(value)!, 'rgb')); + } + case 'hwb': { + return formatCss(clampChroma(toHWB(value)!, 'rgb')); + } + case 'lab': { + return formatCss(toLab(value)!); + } + case 'lch': { + return formatCss(toLch(value)!); + } + case 'oklab': { + return formatCss(toOklab(value)!); + } + case 'oklch': { + return formatCss(toOklch(value)!); + } + case 'p3': { + return formatCss(toP3(value)!); + } + case 'srgb-linear': { + return formatCss(toRGBLinear(value)!); + } + case 'xyz-d50': { + return formatCss(toXYZ50(value)!); + } + case 'xyz-d65': { + return formatCss(toXYZ65(value)!); + } + } +} diff --git a/packages/plugin-css/src/transform/cubic-bezier.ts b/packages/plugin-css/src/transform/cubic-bezier.ts new file mode 100644 index 00000000..5a800aa5 --- /dev/null +++ b/packages/plugin-css/src/transform/cubic-bezier.ts @@ -0,0 +1,6 @@ +import type {ParsedCubicBezierToken} from '@cobalt-ui/core'; + +/** transform cubic beziér */ +export default function transformCubicBezier(value: ParsedCubicBezierToken['$value']): string { + return `cubic-bezier(${value.join(', ')})`; +} diff --git a/packages/plugin-css/src/transform/dimension.ts b/packages/plugin-css/src/transform/dimension.ts new file mode 100644 index 00000000..f40aa409 --- /dev/null +++ b/packages/plugin-css/src/transform/dimension.ts @@ -0,0 +1,6 @@ +import type {ParsedDimensionToken} from '@cobalt-ui/core'; + +/** transform dimension */ +export default function transformDimension(value: ParsedDimensionToken['$value']): string { + return String(value); +} diff --git a/packages/plugin-css/src/transform/duration.ts b/packages/plugin-css/src/transform/duration.ts new file mode 100644 index 00000000..95e0fc57 --- /dev/null +++ b/packages/plugin-css/src/transform/duration.ts @@ -0,0 +1,6 @@ +import type {ParsedDurationToken} from '@cobalt-ui/core'; + +/** transform duration */ +export default function transformDuration(value: ParsedDurationToken['$value']): string { + return String(value); +} diff --git a/packages/plugin-css/src/transform/font-family.ts b/packages/plugin-css/src/transform/font-family.ts new file mode 100644 index 00000000..285db845 --- /dev/null +++ b/packages/plugin-css/src/transform/font-family.ts @@ -0,0 +1,10 @@ +import type {ParsedFontFamilyToken} from '@cobalt-ui/core'; + +/** transform font family */ +export default function transformFontFamily(value: ParsedFontFamilyToken['$value']): string { + return formatFontNames(value); +} + +function formatFontNames(fontNames: string[]): string { + return fontNames.map((n) => (n.includes(' ') ? `"${n}"` : n)).join(', '); +} diff --git a/packages/plugin-css/src/transform/font-weight.ts b/packages/plugin-css/src/transform/font-weight.ts new file mode 100644 index 00000000..19a2eace --- /dev/null +++ b/packages/plugin-css/src/transform/font-weight.ts @@ -0,0 +1,6 @@ +import type {ParsedFontWeightToken} from '@cobalt-ui/core'; + +/** transform font weight */ +export default function transformFontWeight(value: ParsedFontWeightToken['$value']): number { + return Number(value); +} diff --git a/packages/plugin-css/src/transform/index.ts b/packages/plugin-css/src/transform/index.ts new file mode 100644 index 00000000..f302318c --- /dev/null +++ b/packages/plugin-css/src/transform/index.ts @@ -0,0 +1,175 @@ +import {ParsedToken} from '@cobalt-ui/core'; +import {isAlias, kebabinate} from '@cobalt-ui/utils'; +import transformColor, {type ColorFormat} from './color.js'; +import transformDimension from './dimension.js'; +import transformDuration from './duration.js'; +import transformFontFamily from './font-family.js'; +import transformFontWeight from './font-weight.js'; +import transformCubicBezier from './cubic-bezier.js'; +import transformNumber from './number.js'; +import transformLink from './link.js'; +import transformStrokeStyle from './stroke-style.js'; +import {getMode, type makeNameGenerator, varRef} from '../utils/token.js'; +import transformBorder from './border.js'; + +export * from './color.js'; +export {transformColor, transformDimension, transformDuration, transformFontFamily, transformFontWeight, transformCubicBezier, transformNumber, transformLink, transformStrokeStyle}; + +export default function transform( + token: ParsedToken, + { + colorFormat = 'hex', + generateName, + mode, + prefix, + tokens, + }: { + colorFormat: ColorFormat; + generateName?: ReturnType; + mode?: string; + prefix?: string; + tokens?: ParsedToken[]; + }, +): string | number | Record { + switch (token.$type) { + // base tokens + case 'color': { + const {originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal, {prefix, tokens, generateName}); + } + return transformColor(originalVal, colorFormat); // note: use original value because it may have been normalized to hex (which matters if it wasn’t in sRGB gamut to begin with) + } + case 'dimension': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformDimension(value); + } + case 'duration': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformDuration(value); + } + case 'font' as 'fontFamily': // @deprecated (but keep support for now) + case 'fontFamily': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformFontFamily(value); + } + case 'fontWeight': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformFontWeight(value); + } + case 'cubicBezier': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformCubicBezier(value); + } + case 'number': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformNumber(value); + } + case 'link': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformLink(value); + } + case 'strokeStyle': { + const {value, originalVal} = getMode(token, mode); + if (isAlias(originalVal)) { + return varRef(originalVal as string, {prefix, tokens, generateName}); + } + return transformStrokeStyle(value); + } + // composite tokens + case 'border': { + return transformBorder(token, {mode, prefix, tokens, generateName, colorFormat}); + } + case 'shadow': { + let {value, originalVal} = getMode(token, mode); + if (typeof originalVal === 'string') { + return varRef(originalVal, {prefix, tokens, generateName}); + } + + // handle backwards compat for previous versions that didn’t always return array + if (!Array.isArray(value)) value = [value]; + if (!Array.isArray(originalVal)) originalVal = [originalVal]; + + return value + .map((shadow, i) => { + const origShadow = originalVal[i]!; + if (typeof origShadow === 'string') { + return varRef(origShadow, {prefix, tokens, generateName}); + } + const offsetX = isAlias(origShadow.offsetX) ? varRef(origShadow.offsetX, {prefix, tokens, generateName}) : transformDimension(shadow.offsetX); + const offsetY = isAlias(origShadow.offsetY) ? varRef(origShadow.offsetY, {prefix, tokens, generateName}) : transformDimension(shadow.offsetY); + const blur = isAlias(origShadow.blur) ? varRef(origShadow.blur, {prefix, tokens, generateName}) : transformDimension(shadow.blur); + const spread = isAlias(origShadow.spread) ? varRef(origShadow.spread, {prefix, tokens, generateName}) : transformDimension(shadow.spread); + const color = isAlias(origShadow.color) ? varRef(origShadow.color, {prefix, tokens, generateName}) : transformColor(origShadow.color, colorFormat); + return `${shadow.inset ? 'inset ' : ''}${offsetX} ${offsetY} ${blur} ${spread} ${color}`; + }) + .join(', '); + } + case 'gradient': { + const {value, originalVal} = getMode(token, mode); + if (typeof originalVal === 'string') { + return varRef(originalVal, {prefix, tokens, generateName}); + } + return value + .map((gradient, i) => { + const origGradient = originalVal[i]!; + if (typeof origGradient === 'string') { + return varRef(origGradient, {prefix, tokens, generateName}); + } + const color = isAlias(origGradient.color) ? varRef(origGradient.color, {prefix, tokens, generateName}) : transformColor(origGradient.color, colorFormat); + const stop = isAlias(origGradient.position) ? varRef(origGradient.position as any, {prefix, tokens, generateName}) : `${100 * gradient.position}%`; + return `${color} ${stop}`; + }) + .join(', '); + } + case 'transition': { + const {value, originalVal} = getMode(token, mode); + if (typeof originalVal === 'string') { + return varRef(originalVal, {prefix, tokens, generateName}); + } + const duration = isAlias(originalVal.duration) ? varRef(originalVal.duration, {prefix, tokens, generateName}) : transformDuration(value.duration); + let delay: string | undefined = undefined; + if (value.delay) { + delay = isAlias(originalVal.delay) ? varRef(originalVal.delay, {prefix, tokens, generateName}) : transformDuration(value.delay); + } + const timingFunction = isAlias(originalVal.timingFunction) ? varRef(originalVal.timingFunction as any, {prefix, tokens, generateName}) : transformCubicBezier(value.timingFunction); + return `${duration} ${delay ?? ''} ${timingFunction}`; + } + case 'typography': { + const {value, originalVal} = getMode(token, mode); + if (typeof originalVal === 'string') { + return varRef(originalVal, {prefix, tokens, generateName}); + } + const output: Record = {}; + for (const [k, v] of Object.entries(value)) { + const formatter = k === 'fontFamily' ? transformFontFamily : (val: any): string => String(val); + output[kebabinate(k)] = isAlias((originalVal as any)[k] as any) ? varRef((originalVal as any)[k], {prefix, tokens, generateName}) : formatter(v as any); + } + return output; + } + default: { + throw new Error(`No transformer defined for $type: ${(token as any).$type} tokens`); + } + } +} diff --git a/packages/plugin-css/src/transform/link.ts b/packages/plugin-css/src/transform/link.ts new file mode 100644 index 00000000..9ad0fdc9 --- /dev/null +++ b/packages/plugin-css/src/transform/link.ts @@ -0,0 +1,6 @@ +import type {ParsedLinkToken} from '@cobalt-ui/core'; + +/** transform file */ +export default function transformLink(value: ParsedLinkToken['$value']): string { + return `url('${value}')`; +} diff --git a/packages/plugin-css/src/transform/number.ts b/packages/plugin-css/src/transform/number.ts new file mode 100644 index 00000000..5fec1669 --- /dev/null +++ b/packages/plugin-css/src/transform/number.ts @@ -0,0 +1,6 @@ +import type {ParsedNumberToken} from '@cobalt-ui/core'; + +/** transform number */ +export default function transformNumber(value: ParsedNumberToken['$value']): number { + return Number(value); +} diff --git a/packages/plugin-css/src/transform/stroke-style.ts b/packages/plugin-css/src/transform/stroke-style.ts new file mode 100644 index 00000000..b0577c41 --- /dev/null +++ b/packages/plugin-css/src/transform/stroke-style.ts @@ -0,0 +1,9 @@ +import type {ParsedStrokeStyleToken} from '@cobalt-ui/core'; + +/** transform stroke style */ +export default function transformStrokeStyle(value: ParsedStrokeStyleToken['$value']): string { + if (typeof value === 'string') { + return value; + } + return 'dashed'; // CSS doesn’t support custom dashes or line caps, so just convert to `dashed` +} diff --git a/packages/plugin-css/src/utils/encode.ts b/packages/plugin-css/src/utils/encode.ts index 3716a768..8afe856d 100644 --- a/packages/plugin-css/src/utils/encode.ts +++ b/packages/plugin-css/src/utils/encode.ts @@ -15,7 +15,9 @@ export function encode(cssURL: string, cwd: URL): string { // https://css-tricks.com/probably-dont-base64-svg/ if (type === 'image/svg+xml') { const svg = svgo.optimize(fs.readFileSync(filename, 'utf8')); - if (svg.data) return `url('${type};utf8,${svg.data}')`; + if (svg.data) { + return `url('${type};utf8,${svg.data}')`; + } } return `url('${type};base64,${fs.readFileSync(filename).toString('base64')}')`; diff --git a/packages/plugin-css/src/utils/format-font-names.ts b/packages/plugin-css/src/utils/format-font-names.ts deleted file mode 100644 index b7627de9..00000000 --- a/packages/plugin-css/src/utils/format-font-names.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** format font stack */ -export function formatFontNames(fontNames: string[]): string { - return fontNames.map((n) => (n.includes(' ') ? `"${n}"` : n)).join(', '); -} diff --git a/packages/plugin-css/src/utils/generate-token-name.ts b/packages/plugin-css/src/utils/generate-token-name.ts deleted file mode 100644 index 9f923f26..00000000 --- a/packages/plugin-css/src/utils/generate-token-name.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type {ParsedToken} from '@cobalt-ui/core'; - -function normalizeIdSegment(inputString: string): string { - const words = inputString.trim().split(' '); - - /* Only camelCase id segments if they have a middle space. - This prevents a breaking change to names that have capitals but no spaces. */ - if (words.length > 1) { - return words - .map((word, i) => { - if (i === 0) { - return word.toLocaleLowerCase(); - } else { - return word[0]?.toLocaleUpperCase() + word.slice(1).toLocaleLowerCase(); - } - }) - .join(''); - } - - return words[0]!; -} - -export const DASH_PREFIX_RE = /^-+/; -const DASH_SUFFIX_RE = /-+$/; - -export function defaultNameGenerator( - /** dot separated path to token with possible type specific modifications (such as appended composite token property names) */ - variableId: string, - /** @deprecated implement prefixes directly in `generateName` instead */ - prefix?: string, -): string { - const normalizedPrefix = prefix ? `${prefix.replace(DASH_PREFIX_RE, '').replace(DASH_SUFFIX_RE, '')}-` : ''; - return `${normalizedPrefix}${variableId.split('.').map(normalizeIdSegment).join('-')}`; -} - -export type CustomNameGenerator = (variableId: string, token?: ParsedToken) => string | undefined | null; - -// TODO: remove prefix arg in next major version -export function makeNameGenerator(customNameGenerator?: CustomNameGenerator, prefix?: string) { - return ( - /** dot separated path to token with possible type specific modifications (such as appended composite token property names) */ - variableId: string, - token?: ParsedToken, - ): string => { - const name = customNameGenerator?.(variableId, token) || defaultNameGenerator(variableId, prefix); - - return `--${name.replace(DASH_PREFIX_RE, '')}`; - }; -} diff --git a/packages/plugin-css/src/utils/is-token-match.test.ts b/packages/plugin-css/src/utils/is-token-match.test.ts deleted file mode 100644 index 90c0a617..00000000 --- a/packages/plugin-css/src/utils/is-token-match.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {describe, expect, test} from 'vitest'; -import {isTokenMatch} from './is-token-match.js'; - -describe('isTokenMatch', () => { - test('finds matching tokens', () => { - expect(isTokenMatch('color.blue.50', ['color.*'])).toBe(true); - expect(isTokenMatch('size.m.padding', ['*.m.*'])).toBe(true); - }); - - test('skips non-matching tokens', () => { - expect(isTokenMatch('typography.base', ['typography'])).toBe(false); - expect(isTokenMatch('button.color.base', ['color.*'])).toBe(false); - }); -}); diff --git a/packages/plugin-css/src/utils/is-token-match.ts b/packages/plugin-css/src/utils/is-token-match.ts deleted file mode 100644 index 6e0f1b07..00000000 --- a/packages/plugin-css/src/utils/is-token-match.ts +++ /dev/null @@ -1,13 +0,0 @@ -import wcmatch from '../wildcard-match.js'; - -/** match token against globs */ -export function isTokenMatch(tokenID: string, globPatterns: string[]): boolean { - let isMatch = false; - for (const tokenMatch of globPatterns) { - if (wcmatch(tokenMatch)(tokenID)) { - isMatch = true; - break; - } - } - return isMatch; -} diff --git a/packages/plugin-css/src/utils/generate-token-name.test.ts b/packages/plugin-css/src/utils/token.test.ts similarity index 86% rename from packages/plugin-css/src/utils/generate-token-name.test.ts rename to packages/plugin-css/src/utils/token.test.ts index ea47d546..92a02ea3 100644 --- a/packages/plugin-css/src/utils/generate-token-name.test.ts +++ b/packages/plugin-css/src/utils/token.test.ts @@ -1,6 +1,18 @@ import type {ParsedColorToken} from '@cobalt-ui/core'; import {describe, expect, test} from 'vitest'; -import {defaultNameGenerator, makeNameGenerator} from './generate-token-name.js'; +import {defaultNameGenerator, isTokenMatch, makeNameGenerator} from './token.js'; + +describe('isTokenMatch', () => { + test('finds matching tokens', () => { + expect(isTokenMatch('color.blue.50', ['color.*'])).toBe('color.*'); + expect(isTokenMatch('size.m.padding', ['*.m.*'])).toBe('*.m.*'); + }); + + test('skips non-matching tokens', () => { + expect(isTokenMatch('typography.base', ['typography'])).toBe(undefined); + expect(isTokenMatch('button.color.base', ['color.*'])).toBe(undefined); + }); +}); describe('defaultNameGenerator', () => { test('separate groups names with single dashes', () => { diff --git a/packages/plugin-css/src/utils/token.ts b/packages/plugin-css/src/utils/token.ts new file mode 100644 index 00000000..fe9a96e2 --- /dev/null +++ b/packages/plugin-css/src/utils/token.ts @@ -0,0 +1,106 @@ +import {ParsedToken} from '@cobalt-ui/core'; +import {isAlias} from '@cobalt-ui/utils'; +import wcmatch from '../wildcard-match.js'; + +const DASH_PREFIX_RE = /^-+/; +const DASH_SUFFIX_RE = /-+$/; + +export function getMode(token: T, mode?: string): {value: T['$value']; originalVal: T['$value'] | string} { + if (mode) { + if (!token.$extensions?.mode || !token.$extensions.mode[mode]) { + throw new Error(`Token ${token.id} missing "$extensions.mode.${mode}"`); + } + return { + value: token.$extensions.mode[mode]!, + originalVal: token._original.$extensions.mode[mode]!, + }; + } + return {value: token.$value, originalVal: token._original.$value}; +} + +/** match token against globs */ +export function isTokenMatch(tokenID: string, globPatterns: string[]): string | undefined { + for (const glob of globPatterns) { + if (wcmatch(glob)(tokenID)) { + return glob; + } + } +} + +/** + * Reference an existing CSS var + */ +export function varRef( + id: string, + options?: { + prefix?: string; + suffix?: string; + // note: the following properties are optional to preserve backwards-compat without a breaking change + tokens?: ParsedToken[]; + generateName?: ReturnType; + }, +): string { + let refID = id; + if (isAlias(id)) { + // unclear if mode is ever appended to id when passed here, but leaving for safety in case + const [rootID, _mode] = id.substring(1, id.length - 1).split('#'); + refID = rootID!; + } + + const token = options?.tokens?.find((t) => t.id === refID); + + if (!token) { + console.warn(`Tried to reference variable with id: ${refID}, no token found`); // eslint-disable-line no-console + } + + // suffix is only used internally (one place in plugin-sass), so handle it here rather than clutter the public API in defaultNameGenerator + const normalizedSuffix = options?.suffix ? `-${options?.suffix.replace(DASH_PREFIX_RE, '')}` : ''; + const variableId = refID + normalizedSuffix; + + return `var(${options?.generateName?.(variableId, token) ?? defaultNameGenerator(variableId, options?.prefix)})`; +} + +function normalizeIdSegment(inputString: string): string { + const words = inputString.trim().split(' '); + + /* Only camelCase id segments if they have a middle space. + This prevents a breaking change to names that have capitals but no spaces. */ + if (words.length > 1) { + return words + .map((word, i) => { + if (i === 0) { + return word.toLocaleLowerCase(); + } else { + return word[0]?.toLocaleUpperCase() + word.slice(1).toLocaleLowerCase(); + } + }) + .join(''); + } + + return words[0]!; +} + +export function defaultNameGenerator( + /** dot separated path to token with possible type specific modifications (such as appended composite token property names) */ + variableId: string, + /** @deprecated implement prefixes directly in `generateName` instead */ + prefix?: string, +): string { + const normalizedPrefix = prefix ? `${prefix.replace(DASH_PREFIX_RE, '').replace(DASH_SUFFIX_RE, '')}-` : ''; + return `${normalizedPrefix}${variableId.split('.').map(normalizeIdSegment).join('-')}`; +} + +export type CustomNameGenerator = (variableId: string, token?: ParsedToken) => string | undefined | null; + +// TODO: remove prefix arg in next major version +export function makeNameGenerator(customNameGenerator?: CustomNameGenerator, prefix?: string) { + return ( + /** dot separated path to token with possible type specific modifications (such as appended composite token property names) */ + variableId: string, + token?: ParsedToken, + ): string => { + const name = customNameGenerator?.(variableId, token) || defaultNameGenerator(variableId, prefix); + + return `--${name.replace(DASH_PREFIX_RE, '')}`; + }; +} diff --git a/packages/plugin-css/src/utils/utility-css.ts b/packages/plugin-css/src/utils/utility-css.ts new file mode 100644 index 00000000..0b15aa3e --- /dev/null +++ b/packages/plugin-css/src/utils/utility-css.ts @@ -0,0 +1,194 @@ +import type {ParsedToken} from '@cobalt-ui/core'; +import {getLocalID, kebabinate} from '@cobalt-ui/utils'; +import {defaultNameGenerator, isTokenMatch} from './token.js'; + +export type UtilityCSSGroup = 'bg' | 'border' | 'font' | 'gap' | 'margin' | 'padding' | 'shadow' | 'text'; + +const ENDING_GLOB = /\.\*$/; +const STARTING_DOT = /^\./; + +export default function generateUtilityCSS(groups: Record, {refs, tokens}: {refs: Record; tokens: ParsedToken[]}): string { + const output: string[] = []; + + const groupEntries = Object.entries(groups); + groupEntries.sort((a, b) => a[0].localeCompare(b[0])); + + for (const [group, selectors] of groupEntries) { + const selectedTokens: {id: string; partialID: string; type: ParsedToken['$type']; value: string | Record}[] = []; + for (const token of tokens) { + const globMatch = isTokenMatch(token.id, selectors); + if (globMatch) { + let partialID = defaultNameGenerator(token.id.replace(globMatch.replace(ENDING_GLOB, ''), '').replace(STARTING_DOT, '')); + if (!partialID) { + partialID = getLocalID(token.id); + } + if (token.$type === 'typography') { + const value: Record = {}; + if (typeof token.$value === 'string') continue; // skip aliased token + for (const k of Object.keys(token.$value)) { + const property = kebabinate(k); + value[property] = `var(${refs[token.id]}-${property});`; + } + selectedTokens.push({id: token.id, partialID, type: token.$type, value}); + } else { + selectedTokens.push({id: token.id, partialID, type: token.$type, value: `var(${refs[token.id]})`}); + } + } + } + if (!selectedTokens.length) { + console.warn(`utility group "${group}" selected 0 tokens: ${JSON.stringify(selectors)}`); // eslint-disable-line no-console + break; + } + + switch (group) { + case 'bg': { + for (const token of selectedTokens) { + if (token.type === 'color') { + output.push(`.bg-${token.partialID} { + background-color: ${token.value}; +}`); + } else if (token.type === 'gradient') { + output.push(`.bg-${token.partialID} { + background-image: linear-gradient(${token.value}); +}`); + } + } + break; + } + case 'border': { + // ALL generic properties must come before specific properties + for (const token of selectedTokens) { + let property = ''; + if (token.type === 'border') property = 'border'; + else if (token.type === 'color') property = 'border-color'; + else if (token.type === 'dimension') property = 'border-width'; + else if (token.type === 'strokeStyle') property = 'border-style'; + else continue; // skip invalid token types + + output.push(`.border-${token.partialID} { + ${property}: ${token.value}; +}`); + } + for (const token of selectedTokens) { + for (const dir of ['top', 'right', 'bottom', 'left']) { + let property = ''; + if (token.type === 'border') property = `border-${dir}`; + else if (token.type === 'color') property = `border-${dir}-color`; + else if (token.type === 'dimension') property = `border-${dir}-width`; + else if (token.type === 'strokeStyle') property = `border-${dir}-style`; + else continue; // skip invalid token types + + output.push(`.border-${dir}-${token.partialID} { + ${property}: ${token.value}; +}`); + } + } + break; + } + case 'font': { + for (const token of selectedTokens) { + if (token.type === 'typography' && token.value && typeof token.value === 'object') { + output.push(`.font-${token.partialID} {`); + for (const [k, v] of Object.entries(token.value)) { + output.push(` ${k}: ${v}`); + } + output.push('}'); + } + } + break; + } + case 'gap': { + const filteredTokens = selectedTokens.filter((t) => t.type === 'dimension'); // only dimension tokens here + + // ALL generic properties (gap) must come before specific properties (column-gap) + for (const token of filteredTokens) { + output.push(`.gap-${token.partialID} { + gap: ${token.value}; +}`); + } + for (const token of filteredTokens) { + output.push(`.gap-col-${token.partialID} { + column-gap: ${token.value}; +}`); + } + for (const token of filteredTokens) { + output.push(`.gap-row-${token.partialID} { + row-gap: ${token.value}; +}`); + } + break; + } + case 'margin': + case 'padding': { + const filteredTokens = selectedTokens.filter((t) => t.type === 'dimension'); // only dimension tokens here + const name = group === 'margin' ? 'm' : 'p'; + const property = group === 'margin' ? 'margin' : 'padding'; + + // note: ALL generic properties (margin: [value]) MUST come before specific properties (margin-top: [value]) + // this is why we loop through all tokens so many times + for (const token of filteredTokens) { + output.push(`.${name}a-${token.partialID} { + ${property}: ${token.value}; +}`); + } + for (const token of filteredTokens) { + output.push(`.${name}x-${token.partialID} { + ${property}-left: ${token.value}; + ${property}-right: ${token.value}; +}`); + output.push(`.${name}y-${token.partialID} { + ${property}-bottom: ${token.value}; + ${property}-top: ${token.value}; +}`); + } + for (const dir of ['top', 'right', 'bottom', 'left']) { + for (const token of filteredTokens) { + output.push(`.${name}${dir[0]}-${token.partialID} { + ${property}-${dir}: ${token.value}; +}`); + } + } + for (const token of filteredTokens) { + output.push(`.${name}s-${token.partialID} { + ${property}-inline-start: ${token.value}; +}`); + output.push(`.${name}e-${token.partialID} { + ${property}-inline-end: ${token.value}; +}`); + } + break; + } + case 'shadow': { + for (const token of selectedTokens) { + if (token.type !== 'shadow') continue; + output.push(`.shadow-${token.partialID} { + box-shadow: ${token.value}; +}`); + } + break; + } + case 'text': { + for (const token of selectedTokens) { + if (token.type === 'color') { + output.push(`.text-${token.partialID} { + color: ${token.value}; +}`); + } else if (token.type === 'gradient') { + output.push(`.text-${token.partialID} { + background: -webkit-linear-gradient(${token.value}); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +}`); + } + } + break; + } + default: { + console.warn(`utility: unknown group "${group}", ignoring`); // eslint-disable-line no-console + break; + } + } + } + + return output.join('\n'); +} diff --git a/packages/plugin-css/test/build.test.ts b/packages/plugin-css/test/build.test.ts index b2056f14..ff0831ca 100644 --- a/packages/plugin-css/test/build.test.ts +++ b/packages/plugin-css/test/build.test.ts @@ -134,12 +134,14 @@ describe('@cobalt-ui/plugin-css', () => { expect(fs.readFileSync(new URL('./actual.css', cwd), 'utf8')).toMatchFileSnapshot(fileURLToPath(new URL('./want.css', cwd))); }); - test('uses a custom name generator when provided', async () => { + test('generateName', async () => { function myGenerator(variableId, token) { - if (variableId === 'color.gray') return 'super-special-variable'; - - if (token.$type === 'border') return `rad-${defaultNameGenerator(variableId)}`; - + if (variableId === 'color.gray') { + return 'super-special-variable'; + } + if (token.$type === 'border') { + return `rad-${defaultNameGenerator(variableId)}`; + } return defaultNameGenerator(variableId); } @@ -153,5 +155,31 @@ describe('@cobalt-ui/plugin-css', () => { }); expect(fs.readFileSync(new URL('./actual.css', cwd), 'utf8')).toMatchFileSnapshot(fileURLToPath(new URL('./want.css', cwd))); }); + + test('utility', async () => { + const cwd = new URL('./utility/', import.meta.url); + const tokens = JSON.parse(fs.readFileSync(new URL('./tokens.json', cwd), 'utf8')); + await build(tokens, { + outDir: cwd, + plugins: [ + pluginCSS({ + filename: 'actual.css', + utility: { + bg: ['color.semantic.*', 'color.gradient.*'], + border: ['border.*'], + font: ['typography.*'], + gap: ['space.*'], + margin: ['space.*'], + padding: ['space.*'], + shadow: ['shadow.*'], + text: ['color.semantic.*', 'color.gradient.*'], + }, + }), + ], + color: {}, + tokens: [], + }); + expect(fs.readFileSync(new URL('./actual.css', cwd), 'utf8')).toMatchFileSnapshot(fileURLToPath(new URL('./want.css', cwd))); + }); }); }); diff --git a/packages/plugin-css/test/utility/tokens.json b/packages/plugin-css/test/utility/tokens.json new file mode 100644 index 00000000..85997c76 --- /dev/null +++ b/packages/plugin-css/test/utility/tokens.json @@ -0,0 +1,77 @@ +{ + "border": { + "base": { + "$type": "border", + "$value": { + "color": "{border.color.default}", + "width": "{border.size.default}", + "style": "{border.style.default}" + } + }, + "color": { + "$type": "color", + "default": {"$value": "rgb(248, 248, 248)"} + }, + "size": { + "$type": "dimension", + "default": {"$value": "1px"} + }, + "style": { + "$type": "strokeStyle", + "default": {"$value": "solid"} + } + }, + "color": { + "$type": "color", + "base": { + "blue": {"$value": "rgb(20, 122, 243)"}, + "green": {"$value": "rgb(0, 122, 77)"} + }, + "semantic": { + "primary": {"$value": "{color.base.blue}"} + }, + "gradient": { + "$type": "gradient", + "blue-to-green": { + "$value": [ + {"color": "{color.base.blue}", "position": 0}, + {"color": "{color.base.green}", "position": 1} + ] + } + } + }, + "shadow": { + "$type": "shadow", + "md": { + "$value": { + "offsetX": 0, + "offsetY": "{space.1}", + "blur": "{space.1}", + "color": "rgba(0, 0, 0, 0.1)" + } + } + }, + "space": { + "$type": "dimension", + "1": {"$value": "0.25rem"}, + "2": {"$value": "0.5rem"}, + "3": {"$value": "1rem"}, + "4": {"$value": "2rem"} + }, + "typography": { + "$type": "typography", + "base": { + "$value": { + "fontFamily": "Helvetica", + "fontSize": "0.875rem", + "fontStyle": "normal", + "fontWeight": 400, + "fontVariantNumeric": "tabular-nums", + "letterSpacing": 0, + "lineHeight": 1.4, + "textDecoration": "none", + "textTransform": "none" + } + } + } +} diff --git a/packages/plugin-css/test/utility/want.css b/packages/plugin-css/test/utility/want.css new file mode 100644 index 00000000..01e11ffd --- /dev/null +++ b/packages/plugin-css/test/utility/want.css @@ -0,0 +1,396 @@ +/** + * Design Tokens + * Autogenerated from tokens.json. + * DO NOT EDIT! + */ + +:root { + --border-base: var(--border-size-default) var(--border-style-default) var(--border-color-default); + --border-color-default: #f8f8f8; + --border-size-default: 1px; + --border-style-default: solid; + --color-base-blue: #147af3; + --color-base-green: #007a4d; + --color-gradient-blue-to-green: var(--color-base-blue) 0%, var(--color-base-green) 100%; + --color-semantic-primary: var(--color-base-blue); + --shadow-md: 0 var(--space-1) var(--space-1) 0 #0000001a; + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 1rem; + --space-4: 2rem; + --typography-base-font-family: Helvetica; + --typography-base-font-size: 0.875rem; + --typography-base-font-style: normal; + --typography-base-font-variant-numeric: tabular-nums; + --typography-base-font-weight: 400; + --typography-base-letter-spacing: 0; + --typography-base-line-height: 1.4; + --typography-base-text-decoration: none; + --typography-base-text-transform: none; +} + +@supports (color: color(display-p3 1 1 1)) { + :root { + --border-color-default: color(display-p3 0.9725490196078431 0.9725490196078431 0.9725490196078431); + --color-base-blue: color(display-p3 0.0784313725490196 0.47843137254901963 0.9529411764705882); + --color-base-green: color(display-p3 0 0.47843137254901963 0.30196078431372547); + --shadow-md: 0 var(--space-1) var(--space-1) 0 color(display-p3 0 0 0 / 0.10196078431372549); + } +} + +.bg-blue-to-green { + background-image: linear-gradient(var(--color-gradient-blue-to-green)); +} +.bg-primary { + background-color: var(--color-semantic-primary); +} +.border-base { + border: var(--border-base); +} +.border-color-default { + border-color: var(--border-color-default); +} +.border-size-default { + border-width: var(--border-size-default); +} +.border-style-default { + border-style: var(--border-style-default); +} +.border-top-base { + border-top: var(--border-base); +} +.border-right-base { + border-right: var(--border-base); +} +.border-bottom-base { + border-bottom: var(--border-base); +} +.border-left-base { + border-left: var(--border-base); +} +.border-top-color-default { + border-top-color: var(--border-color-default); +} +.border-right-color-default { + border-right-color: var(--border-color-default); +} +.border-bottom-color-default { + border-bottom-color: var(--border-color-default); +} +.border-left-color-default { + border-left-color: var(--border-color-default); +} +.border-top-size-default { + border-top-width: var(--border-size-default); +} +.border-right-size-default { + border-right-width: var(--border-size-default); +} +.border-bottom-size-default { + border-bottom-width: var(--border-size-default); +} +.border-left-size-default { + border-left-width: var(--border-size-default); +} +.border-top-style-default { + border-top-style: var(--border-style-default); +} +.border-right-style-default { + border-right-style: var(--border-style-default); +} +.border-bottom-style-default { + border-bottom-style: var(--border-style-default); +} +.border-left-style-default { + border-left-style: var(--border-style-default); +} +.font-base { + font-family: var(--typography-base-font-family); + font-size: var(--typography-base-font-size); + font-style: var(--typography-base-font-style); + font-weight: var(--typography-base-font-weight); + font-variant-numeric: var(--typography-base-font-variant-numeric); + letter-spacing: var(--typography-base-letter-spacing); + line-height: var(--typography-base-line-height); + text-decoration: var(--typography-base-text-decoration); + text-transform: var(--typography-base-text-transform); +} +.gap-1 { + gap: var(--space-1); +} +.gap-2 { + gap: var(--space-2); +} +.gap-3 { + gap: var(--space-3); +} +.gap-4 { + gap: var(--space-4); +} +.gap-col-1 { + column-gap: var(--space-1); +} +.gap-col-2 { + column-gap: var(--space-2); +} +.gap-col-3 { + column-gap: var(--space-3); +} +.gap-col-4 { + column-gap: var(--space-4); +} +.gap-row-1 { + row-gap: var(--space-1); +} +.gap-row-2 { + row-gap: var(--space-2); +} +.gap-row-3 { + row-gap: var(--space-3); +} +.gap-row-4 { + row-gap: var(--space-4); +} +.ma-1 { + margin: var(--space-1); +} +.ma-2 { + margin: var(--space-2); +} +.ma-3 { + margin: var(--space-3); +} +.ma-4 { + margin: var(--space-4); +} +.mx-1 { + margin-left: var(--space-1); + margin-right: var(--space-1); +} +.my-1 { + margin-bottom: var(--space-1); + margin-top: var(--space-1); +} +.mx-2 { + margin-left: var(--space-2); + margin-right: var(--space-2); +} +.my-2 { + margin-bottom: var(--space-2); + margin-top: var(--space-2); +} +.mx-3 { + margin-left: var(--space-3); + margin-right: var(--space-3); +} +.my-3 { + margin-bottom: var(--space-3); + margin-top: var(--space-3); +} +.mx-4 { + margin-left: var(--space-4); + margin-right: var(--space-4); +} +.my-4 { + margin-bottom: var(--space-4); + margin-top: var(--space-4); +} +.mt-1 { + margin-top: var(--space-1); +} +.mt-2 { + margin-top: var(--space-2); +} +.mt-3 { + margin-top: var(--space-3); +} +.mt-4 { + margin-top: var(--space-4); +} +.mr-1 { + margin-right: var(--space-1); +} +.mr-2 { + margin-right: var(--space-2); +} +.mr-3 { + margin-right: var(--space-3); +} +.mr-4 { + margin-right: var(--space-4); +} +.mb-1 { + margin-bottom: var(--space-1); +} +.mb-2 { + margin-bottom: var(--space-2); +} +.mb-3 { + margin-bottom: var(--space-3); +} +.mb-4 { + margin-bottom: var(--space-4); +} +.ml-1 { + margin-left: var(--space-1); +} +.ml-2 { + margin-left: var(--space-2); +} +.ml-3 { + margin-left: var(--space-3); +} +.ml-4 { + margin-left: var(--space-4); +} +.ms-1 { + margin-inline-start: var(--space-1); +} +.me-1 { + margin-inline-end: var(--space-1); +} +.ms-2 { + margin-inline-start: var(--space-2); +} +.me-2 { + margin-inline-end: var(--space-2); +} +.ms-3 { + margin-inline-start: var(--space-3); +} +.me-3 { + margin-inline-end: var(--space-3); +} +.ms-4 { + margin-inline-start: var(--space-4); +} +.me-4 { + margin-inline-end: var(--space-4); +} +.pa-1 { + padding: var(--space-1); +} +.pa-2 { + padding: var(--space-2); +} +.pa-3 { + padding: var(--space-3); +} +.pa-4 { + padding: var(--space-4); +} +.px-1 { + padding-left: var(--space-1); + padding-right: var(--space-1); +} +.py-1 { + padding-bottom: var(--space-1); + padding-top: var(--space-1); +} +.px-2 { + padding-left: var(--space-2); + padding-right: var(--space-2); +} +.py-2 { + padding-bottom: var(--space-2); + padding-top: var(--space-2); +} +.px-3 { + padding-left: var(--space-3); + padding-right: var(--space-3); +} +.py-3 { + padding-bottom: var(--space-3); + padding-top: var(--space-3); +} +.px-4 { + padding-left: var(--space-4); + padding-right: var(--space-4); +} +.py-4 { + padding-bottom: var(--space-4); + padding-top: var(--space-4); +} +.pt-1 { + padding-top: var(--space-1); +} +.pt-2 { + padding-top: var(--space-2); +} +.pt-3 { + padding-top: var(--space-3); +} +.pt-4 { + padding-top: var(--space-4); +} +.pr-1 { + padding-right: var(--space-1); +} +.pr-2 { + padding-right: var(--space-2); +} +.pr-3 { + padding-right: var(--space-3); +} +.pr-4 { + padding-right: var(--space-4); +} +.pb-1 { + padding-bottom: var(--space-1); +} +.pb-2 { + padding-bottom: var(--space-2); +} +.pb-3 { + padding-bottom: var(--space-3); +} +.pb-4 { + padding-bottom: var(--space-4); +} +.pl-1 { + padding-left: var(--space-1); +} +.pl-2 { + padding-left: var(--space-2); +} +.pl-3 { + padding-left: var(--space-3); +} +.pl-4 { + padding-left: var(--space-4); +} +.ps-1 { + padding-inline-start: var(--space-1); +} +.pe-1 { + padding-inline-end: var(--space-1); +} +.ps-2 { + padding-inline-start: var(--space-2); +} +.pe-2 { + padding-inline-end: var(--space-2); +} +.ps-3 { + padding-inline-start: var(--space-3); +} +.pe-3 { + padding-inline-end: var(--space-3); +} +.ps-4 { + padding-inline-start: var(--space-4); +} +.pe-4 { + padding-inline-end: var(--space-4); +} +.shadow-md { + box-shadow: var(--shadow-md); +} +.text-blue-to-green { + background: -webkit-linear-gradient(var(--color-gradient-blue-to-green)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} +.text-primary { + color: var(--color-semantic-primary); +} diff --git a/packages/plugin-sass/test/plugin-css/tokens.css b/packages/plugin-sass/test/plugin-css/tokens.css index ce151872..6ba3ea4a 100644 --- a/packages/plugin-sass/test/plugin-css/tokens.css +++ b/packages/plugin-sass/test/plugin-css/tokens.css @@ -42,3 +42,4 @@ --ds-shadow: 0 4px 8px 0 color(display-p3 0 0 0 / 0.10196078431372549); } } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f9d1e13..4d3de16b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: specifier: ^5.0.2 version: 5.0.2 vitepress: - specifier: 1.0.0-rc.29 - version: 1.0.0-rc.29(@algolia/client-search@4.20.0)(search-insights@2.11.0)(typescript@5.3.2) + specifier: 1.0.0-rc.30 + version: 1.0.0-rc.30(@algolia/client-search@4.20.0)(search-insights@2.11.0)(typescript@5.3.2) examples/adobe: devDependencies: @@ -1445,96 +1445,96 @@ packages: resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} dev: false - /@rollup/rollup-android-arm-eabi@4.5.1: - resolution: {integrity: sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==} + /@rollup/rollup-android-arm-eabi@4.5.2: + resolution: {integrity: sha512-ee7BudTwwrglFYSc3UnqInDDjCLWHKrFmGNi4aK7jlEyg4CyPa1DCMrZfsN1O13YT76UFEqXz2CoN7BCGpUlJw==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.5.1: - resolution: {integrity: sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw==} + /@rollup/rollup-android-arm64@4.5.2: + resolution: {integrity: sha512-xOuhj9HHtn8128ir8veoQsBbAUBasDbHIBniYTEx02pAmu9EXL+ZjJqngnNEy6ZgZ4h1JwL33GMNu3yJL5Mzow==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.5.1: - resolution: {integrity: sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw==} + /@rollup/rollup-darwin-arm64@4.5.2: + resolution: {integrity: sha512-NTGJWoL8bKyqyWFn9/RzSv4hQ4wTbaAv0lHHRwf4OnpiiP4P8W0jiXbm8Nc5BCXKmWAwuvJY82mcIU2TayC20g==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.5.1: - resolution: {integrity: sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw==} + /@rollup/rollup-darwin-x64@4.5.2: + resolution: {integrity: sha512-hlKqj7bpPvU15sZo4za14u185lpMzdwWLMc9raMqPK4wywt0wR23y1CaVQ4oAFXat3b5/gmRntyfpwWTKl+vvA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.5.1: - resolution: {integrity: sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A==} + /@rollup/rollup-linux-arm-gnueabihf@4.5.2: + resolution: {integrity: sha512-7ZIZx8c3u+pfI0ohQsft/GywrXez0uR6dUP0JhBuCK3sFO5TfdLn/YApnVkvPxuTv3+YKPIZend9Mt7Cz6sS3Q==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.5.1: - resolution: {integrity: sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww==} + /@rollup/rollup-linux-arm64-gnu@4.5.2: + resolution: {integrity: sha512-7Pk/5mO11JW/cH+a8lL/i0ZxmRGrbpYqN0VwO2DHhU+SJWWOH2zE1RAcPaj8KqiwC8DCDIJOSxjV9+9lLb6aeA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.5.1: - resolution: {integrity: sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw==} + /@rollup/rollup-linux-arm64-musl@4.5.2: + resolution: {integrity: sha512-KrRnuG5phJx756e62wxvWH2e+TK84MP2IVuPwfge+GBvWqIUfVzFRn09TKruuQBXzZp52Vyma7FjMDkwlA9xpg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.5.1: - resolution: {integrity: sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw==} + /@rollup/rollup-linux-x64-gnu@4.5.2: + resolution: {integrity: sha512-My+53GasPa2D2tU5dXiyHYwrELAUouSfkNlZ3bUKpI7btaztO5vpALEs3mvFjM7aKTvEbc7GQckuXeXIDKQ0fg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.5.1: - resolution: {integrity: sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ==} + /@rollup/rollup-linux-x64-musl@4.5.2: + resolution: {integrity: sha512-/f0Q6Sc+Vw54Ws6N8fxaEe4R7at3b8pFyv+O/F2VaQ4hODUJcRUcCBJh6zuqtgQQt7w845VTkGLFgWZkP3tUoQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.5.1: - resolution: {integrity: sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ==} + /@rollup/rollup-win32-arm64-msvc@4.5.2: + resolution: {integrity: sha512-NCKuuZWLht6zj7s6EIFef4BxCRX1GMr83S2W4HPCA0RnJ4iHE4FS1695q6Ewoa6A9nFjJe1//yUu0kgBU07Edw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.5.1: - resolution: {integrity: sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg==} + /@rollup/rollup-win32-ia32-msvc@4.5.2: + resolution: {integrity: sha512-J5zL3riR4AOyU/J3M/i4k/zZ8eP1yT+nTmAKztCXJtnI36jYH0eepvob22mAQ/kLwfsK2TB6dbyVY1F8c/0H5A==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.5.1: - resolution: {integrity: sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q==} + /@rollup/rollup-win32-x64-msvc@4.5.2: + resolution: {integrity: sha512-pL0RXRHuuGLhvs7ayX/SAHph1hrDPXOM5anyYUQXWJEENxw3nfHkzv8FfVlEVcLyKPAEgDRkd6RKZq2SMqS/yg==} cpu: [x64] os: [win32] requiresBuild: true @@ -1571,6 +1571,12 @@ packages: /@types/culori@2.0.4: resolution: {integrity: sha512-GeLW8+KBRkwqIgeGrU8EnNbBE2D7waYbQHkx2xnI5exlzSGTMpjWtDaHzLWK1PTYmyJN9u6dPvMYumFevDe+VA==} + /@types/hast@3.0.3: + resolution: {integrity: sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==} + dependencies: + '@types/unist': 3.0.2 + dev: true + /@types/is-ci@3.0.4: resolution: {integrity: sha512-AkCYCmwlXeuH89DagDCzvCAyltI2v9lh3U3DqSg/GrBYoReAaWwxfXCqMx9UV5MajLZ4ZFwZzV4cABGIxk2XRw==} dependencies: @@ -1592,6 +1598,12 @@ packages: '@types/mdurl': 1.0.5 dev: true + /@types/mdast@4.0.3: + resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} + dependencies: + '@types/unist': 3.0.2 + dev: true + /@types/mdurl@1.0.5: resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} dev: true @@ -1630,6 +1642,10 @@ packages: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} dev: true + /@types/unist@3.0.2: + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + dev: true + /@types/web-bluetooth@0.0.20: resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} dev: true @@ -2041,10 +2057,6 @@ packages: engines: {node: '>=8'} dev: true - /ansi-sequence-parser@1.1.1: - resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} - dev: true - /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -2277,6 +2289,10 @@ packages: engines: {node: '>=10'} dev: true + /ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + dev: true + /chai@4.3.10: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} @@ -2307,6 +2323,14 @@ packages: supports-color: 7.2.0 dev: true + /character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + dev: true + + /character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + dev: true + /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -2387,6 +2411,10 @@ packages: requiresBuild: true dev: true + /comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + dev: true + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2631,11 +2659,22 @@ packages: slash: 4.0.0 dev: true + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} dev: true + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + dev: true + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -2715,7 +2754,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -3461,6 +3499,88 @@ packages: function-bind: 1.1.2 dev: true + /hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + dependencies: + '@types/hast': 3.0.3 + '@types/unist': 3.0.2 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.4.0 + vfile: 6.0.1 + vfile-location: 5.0.2 + web-namespaces: 2.0.1 + dev: true + + /hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + dependencies: + '@types/hast': 3.0.3 + dev: true + + /hast-util-raw@9.0.1: + resolution: {integrity: sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==} + dependencies: + '@types/hast': 3.0.3 + '@types/unist': 3.0.2 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.0.2 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: true + + /hast-util-to-html@9.0.0: + resolution: {integrity: sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==} + dependencies: + '@types/hast': 3.0.3 + '@types/unist': 3.0.2 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-raw: 9.0.1 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.0.2 + property-information: 6.4.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.3 + zwitch: 2.0.4 + dev: true + + /hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + dependencies: + '@types/hast': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.4.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: true + + /hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + dependencies: + '@types/hast': 3.0.3 + dev: true + + /hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + dependencies: + '@types/hast': 3.0.3 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.4.0 + space-separated-tokens: 2.0.2 + dev: true + /hdr-histogram-js@2.0.3: resolution: {integrity: sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==} dependencies: @@ -3484,6 +3604,10 @@ packages: lru-cache: 6.0.0 dev: true + /html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + dev: true + /human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} dev: true @@ -3946,6 +4070,19 @@ packages: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} dev: true + /mdast-util-to-hast@13.0.2: + resolution: {integrity: sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og==} + dependencies: + '@types/hast': 3.0.3 + '@types/mdast': 4.0.3 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + dev: true + /mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} dev: false @@ -4003,6 +4140,33 @@ packages: engines: {node: '>= 8'} dev: true + /micromark-util-character@2.0.1: + resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==} + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: true + + /micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + dev: true + + /micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + dev: true + + /micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + dev: true + + /micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + dev: true + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -4047,8 +4211,8 @@ packages: kind-of: 6.0.3 dev: true - /minisearch@6.2.0: - resolution: {integrity: sha512-BECkorDF1TY2rGKt9XHdSeP9TP29yUbrAaCh/C03wpyf1vx3uYcP/+8XlMcpTkgoU0rBVnHMAOaP83Rc9Tm+TQ==} + /minisearch@6.3.0: + resolution: {integrity: sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==} dev: true /mixme@0.5.10: @@ -4370,6 +4534,12 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4593,6 +4763,10 @@ packages: react-is: 18.2.0 dev: true + /property-information@6.4.0: + resolution: {integrity: sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==} + dev: true + /pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true @@ -4775,23 +4949,23 @@ packages: fsevents: 2.3.3 dev: true - /rollup@4.5.1: - resolution: {integrity: sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA==} + /rollup@4.5.2: + resolution: {integrity: sha512-CRK1uoROBfkcqrZKyaFcqCcZWNsvJ6yVYZkqTlRocZhO2s5yER6Z3f/QaYtO8RGyloPnmhwgzuPQpNGeK210xQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.5.1 - '@rollup/rollup-android-arm64': 4.5.1 - '@rollup/rollup-darwin-arm64': 4.5.1 - '@rollup/rollup-darwin-x64': 4.5.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.5.1 - '@rollup/rollup-linux-arm64-gnu': 4.5.1 - '@rollup/rollup-linux-arm64-musl': 4.5.1 - '@rollup/rollup-linux-x64-gnu': 4.5.1 - '@rollup/rollup-linux-x64-musl': 4.5.1 - '@rollup/rollup-win32-arm64-msvc': 4.5.1 - '@rollup/rollup-win32-ia32-msvc': 4.5.1 - '@rollup/rollup-win32-x64-msvc': 4.5.1 + '@rollup/rollup-android-arm-eabi': 4.5.2 + '@rollup/rollup-android-arm64': 4.5.2 + '@rollup/rollup-darwin-arm64': 4.5.2 + '@rollup/rollup-darwin-x64': 4.5.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.5.2 + '@rollup/rollup-linux-arm64-gnu': 4.5.2 + '@rollup/rollup-linux-arm64-musl': 4.5.2 + '@rollup/rollup-linux-x64-gnu': 4.5.2 + '@rollup/rollup-linux-x64-musl': 4.5.2 + '@rollup/rollup-win32-arm64-msvc': 4.5.2 + '@rollup/rollup-win32-ia32-msvc': 4.5.2 + '@rollup/rollup-win32-x64-msvc': 4.5.2 fsevents: 2.3.3 dev: true @@ -4908,13 +5082,16 @@ packages: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} dev: true - /shiki@0.14.5: - resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==} + /shikiji-transformers@0.7.4: + resolution: {integrity: sha512-oykilNekcW2FnRGbvZm+RNWHYroSeCVMOaMMwAbxozZgpTdcJtHoA+1+MDFw6/o2hCkX88kKbxG6FwAZoUZ6WQ==} dependencies: - ansi-sequence-parser: 1.1.1 - jsonc-parser: 3.2.0 - vscode-oniguruma: 1.7.0 - vscode-textmate: 8.0.0 + shikiji: 0.7.4 + dev: true + + /shikiji@0.7.4: + resolution: {integrity: sha512-N5dmPvyhH/zfcsuWysUEAMwRJDMz26LUns2VEUs5y4Ozbf5jkAODU0Yswjcf/tZAwpFnk5x3y34dupFMnF2+NA==} + dependencies: + hast-util-to-html: 9.0.0 dev: true /side-channel@1.0.4: @@ -4960,6 +5137,10 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + dev: true + /spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -5075,6 +5256,13 @@ packages: es-abstract: 1.22.3 dev: true + /stringify-entities@4.0.3: + resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5274,6 +5462,10 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + dev: true + /trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -5416,6 +5608,39 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true + /unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + dependencies: + '@types/unist': 3.0.2 + dev: true + + /unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + dependencies: + '@types/unist': 3.0.2 + dev: true + + /unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + dependencies: + '@types/unist': 3.0.2 + dev: true + + /unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + dev: true + + /unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: true + /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -5443,6 +5668,28 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vfile-location@5.0.2: + resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} + dependencies: + '@types/unist': 3.0.2 + vfile: 6.0.1 + dev: true + + /vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + dev: true + + /vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + dev: true + /vite-node@0.34.6(@types/node@20.9.3): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} @@ -5531,13 +5778,13 @@ packages: dependencies: esbuild: 0.19.7 postcss: 8.4.31 - rollup: 4.5.1 + rollup: 4.5.2 optionalDependencies: fsevents: 2.3.3 dev: true - /vitepress@1.0.0-rc.29(@algolia/client-search@4.20.0)(search-insights@2.11.0)(typescript@5.3.2): - resolution: {integrity: sha512-6sKmyEvH16SgMqkHzRwwadt9Uju13AOIqouzOVEg3Rk6X9mds6jLsq2GxnAJvg0s6bl/0Qs/cw+f8SNki82ltw==} + /vitepress@1.0.0-rc.30(@algolia/client-search@4.20.0)(search-insights@2.11.0)(typescript@5.3.2): + resolution: {integrity: sha512-OolAbFU2hjs0KuIpPq0wRd4vJlTMvrFgHSh/hB+XQid7U31KtB6F1NxWihMwKkwncpxu9mt2Somet5AGiyTgPA==} hasBin: true peerDependencies: markdown-it-mathjax3: ^4.3.2 @@ -5557,9 +5804,10 @@ packages: '@vueuse/integrations': 10.6.1(focus-trap@7.5.4)(vue@3.3.8) focus-trap: 7.5.4 mark.js: 8.11.1 - minisearch: 6.2.0 + minisearch: 6.3.0 mrmime: 1.0.1 - shiki: 0.14.5 + shikiji: 0.7.4 + shikiji-transformers: 0.7.4 vite: 5.0.2 vue: 3.3.8(typescript@5.3.2) transitivePeerDependencies: @@ -5655,14 +5903,6 @@ packages: - terser dev: true - /vscode-oniguruma@1.7.0: - resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} - dev: true - - /vscode-textmate@8.0.0: - resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} - dev: true - /vue-demi@0.14.6(vue@3.3.8): resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} engines: {node: '>=12'} @@ -5699,6 +5939,10 @@ packages: defaults: 1.0.4 dev: true + /web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + dev: true + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true @@ -5878,3 +6122,7 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + dev: true