color-map 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +12 -0
- package/.codex-plugin/plugin.json +12 -0
- package/.cursor-plugin/plugin.json +12 -0
- package/CHANGELOG.md +94 -0
- package/README.md +71 -8
- package/bin/cli-CNBftDBE.js +6414 -0
- package/bin/color-map.js +9 -0
- package/bin/index.js +1 -0
- package/cjs/color/index.d.ts +5 -0
- package/cjs/color/index.js +52 -0
- package/cjs/color-wNrkrhC7.js +374 -0
- package/cjs/color-wNrkrhC7.js.map +1 -0
- package/cjs/contrast-XjF5Rwui.js +293 -0
- package/cjs/contrast-XjF5Rwui.js.map +1 -0
- package/cjs/gamut-CG1J2aat.d.ts +158 -0
- package/cjs/gamut-CG1J2aat.d.ts.map +1 -0
- package/cjs/index-BcEqCPHz.d.ts +137 -0
- package/cjs/index-BcEqCPHz.d.ts.map +1 -0
- package/cjs/index-CGKCqRqD.d.ts +111 -0
- package/cjs/index-CGKCqRqD.d.ts.map +1 -0
- package/cjs/index-Cv3iUUYE.d.ts +171 -0
- package/cjs/index-Cv3iUUYE.d.ts.map +1 -0
- package/cjs/index-Dw43Hj4v.d.ts +41 -0
- package/cjs/index-Dw43Hj4v.d.ts.map +1 -0
- package/cjs/index-Dy8UARqO.d.ts +72 -0
- package/cjs/index-Dy8UARqO.d.ts.map +1 -0
- package/cjs/index.d.ts +26 -36
- package/cjs/index.d.ts.map +1 -1
- package/cjs/index.js +83 -62
- package/cjs/index.js.map +1 -1
- package/cjs/legacy/index.d.ts +2 -0
- package/cjs/legacy/index.js +75 -0
- package/cjs/legacy/index.js.map +1 -0
- package/cjs/mix-Cs4owSbW.js +158 -0
- package/cjs/mix-Cs4owSbW.js.map +1 -0
- package/cjs/mix-DGlKoUw4.d.ts +31 -0
- package/cjs/mix-DGlKoUw4.d.ts.map +1 -0
- package/cjs/palette/index.d.ts +3 -0
- package/cjs/palette/index.js +8 -0
- package/cjs/palette-DyFV_KoJ.js +332 -0
- package/cjs/palette-DyFV_KoJ.js.map +1 -0
- package/cjs/parse-8mt2OMKf.js +1508 -0
- package/cjs/parse-8mt2OMKf.js.map +1 -0
- package/cjs/scale/index.d.ts +2 -0
- package/cjs/scale/index.js +11 -0
- package/cjs/scale-Cq9pXrc1.js +318 -0
- package/cjs/scale-Cq9pXrc1.js.map +1 -0
- package/cjs/semantic-CrEIo8tS.d.ts +52 -0
- package/cjs/semantic-CrEIo8tS.d.ts.map +1 -0
- package/cjs/serialize-C69DAmoE.d.ts +25 -0
- package/cjs/serialize-C69DAmoE.d.ts.map +1 -0
- package/cjs/serialize-Ci2X0cPN.js +103 -0
- package/cjs/serialize-Ci2X0cPN.js.map +1 -0
- package/cjs/tokens/index.d.ts +2 -0
- package/cjs/tokens/index.js +6 -0
- package/cjs/tokens-CzXxaMgM.js +168 -0
- package/cjs/tokens-CzXxaMgM.js.map +1 -0
- package/cjs/tone-BAhu-QzQ.js +118 -0
- package/cjs/tone-BAhu-QzQ.js.map +1 -0
- package/dist/color-map.es5.js.map +1 -1
- package/esm/color/a98-rgb.d.ts +7 -0
- package/esm/color/a98-rgb.d.ts.map +1 -0
- package/esm/color/a98-rgb.js +13 -0
- package/esm/color/a98-rgb.js.map +1 -0
- package/esm/color/argb.d.ts +6 -0
- package/esm/color/argb.d.ts.map +1 -0
- package/esm/color/argb.js +25 -0
- package/esm/color/argb.js.map +1 -0
- package/esm/color/calc.js +82 -0
- package/esm/color/calc.js.map +1 -0
- package/esm/color/compare.d.ts +20 -0
- package/esm/color/compare.d.ts.map +1 -0
- package/esm/color/compare.js +46 -0
- package/esm/color/compare.js.map +1 -0
- package/esm/color/contrast.d.ts +107 -0
- package/esm/color/contrast.d.ts.map +1 -0
- package/esm/color/contrast.js +242 -0
- package/esm/color/contrast.js.map +1 -0
- package/esm/color/convert.d.ts +9 -0
- package/esm/color/convert.d.ts.map +1 -0
- package/esm/color/convert.js +45 -0
- package/esm/color/convert.js.map +1 -0
- package/esm/color/display-p3-linear.d.ts +7 -0
- package/esm/color/display-p3-linear.d.ts.map +1 -0
- package/esm/color/display-p3-linear.js +12 -0
- package/esm/color/display-p3-linear.js.map +1 -0
- package/esm/color/display-p3.d.ts +7 -0
- package/esm/color/display-p3.d.ts.map +1 -0
- package/esm/color/display-p3.js +12 -0
- package/esm/color/display-p3.js.map +1 -0
- package/esm/color/expr.d.ts +43 -0
- package/esm/color/expr.d.ts.map +1 -0
- package/esm/color/expr.js +160 -0
- package/esm/color/expr.js.map +1 -0
- package/esm/color/gamut.d.ts +31 -0
- package/esm/color/gamut.d.ts.map +1 -0
- package/esm/color/gamut.js +210 -0
- package/esm/color/gamut.js.map +1 -0
- package/esm/color/hsl.d.ts +7 -0
- package/esm/color/hsl.d.ts.map +1 -0
- package/esm/color/hsl.js +67 -0
- package/esm/color/hsl.js.map +1 -0
- package/esm/color/hwb.d.ts +7 -0
- package/esm/color/hwb.d.ts.map +1 -0
- package/esm/color/hwb.js +40 -0
- package/esm/color/hwb.js.map +1 -0
- package/esm/color/index.d.ts +28 -0
- package/esm/color/index.js +27 -0
- package/esm/color/lab.d.ts +7 -0
- package/esm/color/lab.d.ts.map +1 -0
- package/esm/color/lab.js +33 -0
- package/esm/color/lab.js.map +1 -0
- package/esm/color/lch.d.ts +7 -0
- package/esm/color/lch.d.ts.map +1 -0
- package/esm/color/lch.js +11 -0
- package/esm/color/lch.js.map +1 -0
- package/esm/color/math.js +32 -0
- package/esm/color/math.js.map +1 -0
- package/esm/color/matrices.js +180 -0
- package/esm/color/matrices.js.map +1 -0
- package/esm/color/mix.d.ts +30 -0
- package/esm/color/mix.d.ts.map +1 -0
- package/esm/color/mix.js +152 -0
- package/esm/color/mix.js.map +1 -0
- package/esm/color/named.js +11 -0
- package/esm/color/named.js.map +1 -0
- package/esm/color/oklab.d.ts +7 -0
- package/esm/color/oklab.d.ts.map +1 -0
- package/esm/color/oklab.js +21 -0
- package/esm/color/oklab.js.map +1 -0
- package/esm/color/oklch.d.ts +7 -0
- package/esm/color/oklch.d.ts.map +1 -0
- package/esm/color/oklch.js +11 -0
- package/esm/color/oklch.js.map +1 -0
- package/esm/color/parse.d.ts +9 -0
- package/esm/color/parse.d.ts.map +1 -0
- package/esm/color/parse.js +452 -0
- package/esm/color/parse.js.map +1 -0
- package/esm/color/polar.js +27 -0
- package/esm/color/polar.js.map +1 -0
- package/esm/color/prophoto-rgb.d.ts +7 -0
- package/esm/color/prophoto-rgb.d.ts.map +1 -0
- package/esm/color/prophoto-rgb.js +18 -0
- package/esm/color/prophoto-rgb.js.map +1 -0
- package/esm/color/rec2020.d.ts +7 -0
- package/esm/color/rec2020.d.ts.map +1 -0
- package/esm/color/rec2020.js +13 -0
- package/esm/color/rec2020.js.map +1 -0
- package/esm/color/resolve.d.ts +18 -0
- package/esm/color/resolve.d.ts.map +1 -0
- package/esm/color/resolve.js +98 -0
- package/esm/color/resolve.js.map +1 -0
- package/esm/color/rgb.js +20 -0
- package/esm/color/rgb.js.map +1 -0
- package/esm/color/serialize.d.ts +24 -0
- package/esm/color/serialize.d.ts.map +1 -0
- package/esm/color/serialize.js +88 -0
- package/esm/color/serialize.js.map +1 -0
- package/esm/color/spaces.d.ts +6 -0
- package/esm/color/spaces.d.ts.map +1 -0
- package/esm/color/spaces.js +42 -0
- package/esm/color/spaces.js.map +1 -0
- package/esm/color/srgb-linear.d.ts +7 -0
- package/esm/color/srgb-linear.d.ts.map +1 -0
- package/esm/color/srgb-linear.js +12 -0
- package/esm/color/srgb-linear.js.map +1 -0
- package/esm/color/srgb.d.ts +7 -0
- package/esm/color/srgb.d.ts.map +1 -0
- package/esm/color/srgb.js +12 -0
- package/esm/color/srgb.js.map +1 -0
- package/esm/color/transfer.js +11 -0
- package/esm/color/transfer.js.map +1 -0
- package/esm/color/types.d.ts +23 -0
- package/esm/color/types.d.ts.map +1 -0
- package/esm/color/unknown-name.js +16 -0
- package/esm/color/unknown-name.js.map +1 -0
- package/esm/color/xyz-d50.d.ts +7 -0
- package/esm/color/xyz-d50.d.ts.map +1 -0
- package/esm/color/xyz-d50.js +15 -0
- package/esm/color/xyz-d50.js.map +1 -0
- package/esm/color/xyz-d65.d.ts +4 -0
- package/esm/color/xyz-d65.d.ts.map +1 -0
- package/esm/color/xyz-d65.js +10 -0
- package/esm/color/xyz-d65.js.map +1 -0
- package/esm/index.d.ts +63 -6
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +54 -5
- package/esm/index.js.map +1 -0
- package/esm/legacy/createColors.d.ts +9 -0
- package/esm/legacy/createColors.d.ts.map +1 -0
- package/esm/{createColors.js → legacy/createColors.js} +7 -2
- package/esm/legacy/createColors.js.map +1 -0
- package/esm/legacy/createColorsFromMap.d.ts +9 -0
- package/esm/legacy/createColorsFromMap.d.ts.map +1 -0
- package/esm/{createColorsFromMap.js → legacy/createColorsFromMap.js} +4 -2
- package/esm/legacy/createColorsFromMap.js.map +1 -0
- package/esm/legacy/index.d.ts +6 -0
- package/esm/legacy/index.js +5 -0
- package/esm/legacy/interfaces.d.ts +19 -0
- package/esm/legacy/interfaces.d.ts.map +1 -0
- package/esm/{rgbHex.d.ts → legacy/rgbHex.d.ts} +1 -4
- package/esm/legacy/rgbHex.d.ts.map +1 -0
- package/esm/{rgbHex.js → legacy/rgbHex.js} +1 -2
- package/esm/legacy/rgbHex.js.map +1 -0
- package/esm/legacy/rgbaString.d.ts +4 -0
- package/esm/{rgbaString.d.ts.map → legacy/rgbaString.d.ts.map} +1 -1
- package/esm/legacy/rgbaString.js +8 -0
- package/esm/legacy/rgbaString.js.map +1 -0
- package/esm/palette/dark.d.ts +23 -0
- package/esm/palette/dark.d.ts.map +1 -0
- package/esm/palette/dark.js +54 -0
- package/esm/palette/dark.js.map +1 -0
- package/esm/palette/harmony.d.ts +56 -0
- package/esm/palette/harmony.d.ts.map +1 -0
- package/esm/palette/harmony.js +115 -0
- package/esm/palette/harmony.js.map +1 -0
- package/esm/palette/index.d.ts +5 -0
- package/esm/palette/index.js +5 -0
- package/esm/palette/semantic.d.ts +53 -0
- package/esm/palette/semantic.d.ts.map +1 -0
- package/esm/palette/semantic.js +96 -0
- package/esm/palette/semantic.js.map +1 -0
- package/esm/palette/validate.d.ts +39 -0
- package/esm/palette/validate.d.ts.map +1 -0
- package/esm/palette/validate.js +56 -0
- package/esm/palette/validate.js.map +1 -0
- package/esm/scale/brand.d.ts +21 -0
- package/esm/scale/brand.d.ts.map +1 -0
- package/esm/scale/brand.js +63 -0
- package/esm/scale/brand.js.map +1 -0
- package/esm/scale/contrast-scale.d.ts +33 -0
- package/esm/scale/contrast-scale.d.ts.map +1 -0
- package/esm/scale/contrast-scale.js +26 -0
- package/esm/scale/contrast-scale.js.map +1 -0
- package/esm/scale/gradient.d.ts +38 -0
- package/esm/scale/gradient.d.ts.map +1 -0
- package/esm/scale/gradient.js +73 -0
- package/esm/scale/gradient.js.map +1 -0
- package/esm/scale/index.d.ts +6 -0
- package/esm/scale/index.js +6 -0
- package/esm/scale/scale.d.ts +31 -0
- package/esm/scale/scale.d.ts.map +1 -0
- package/esm/scale/scale.js +152 -0
- package/esm/scale/scale.js.map +1 -0
- package/esm/scale/tone.d.ts +25 -0
- package/esm/scale/tone.d.ts.map +1 -0
- package/esm/scale/tone.js +96 -0
- package/esm/scale/tone.js.map +1 -0
- package/esm/tokens/css.d.ts +18 -0
- package/esm/tokens/css.d.ts.map +1 -0
- package/esm/tokens/css.js +23 -0
- package/esm/tokens/css.js.map +1 -0
- package/esm/tokens/dtcg.d.ts +33 -0
- package/esm/tokens/dtcg.d.ts.map +1 -0
- package/esm/tokens/dtcg.js +42 -0
- package/esm/tokens/dtcg.js.map +1 -0
- package/esm/tokens/index.d.ts +5 -0
- package/esm/tokens/index.js +4 -0
- package/esm/tokens/tailwind.d.ts +19 -0
- package/esm/tokens/tailwind.d.ts.map +1 -0
- package/esm/tokens/tailwind.js +19 -0
- package/esm/tokens/tailwind.js.map +1 -0
- package/esm/tokens/tokens.d.ts +11 -0
- package/esm/tokens/tokens.d.ts.map +1 -0
- package/esm/tokens/tokens.js +77 -0
- package/esm/tokens/tokens.js.map +1 -0
- package/llms.txt +29 -0
- package/package.json +276 -64
- package/plugin.json +18 -0
- package/schemas/contrast.schema.json +24 -0
- package/schemas/convert.schema.json +60 -0
- package/schemas/export.schema.json +106 -0
- package/schemas/find-contrast.schema.json +51 -0
- package/schemas/harmony.schema.json +58 -0
- package/schemas/mix.schema.json +72 -0
- package/schemas/palette.schema.json +56 -0
- package/schemas/scale.schema.json +86 -0
- package/skills/color-map/SKILL.md +38 -0
- package/src/cli/bin.ts +11 -0
- package/src/cli/cli.ts +40 -0
- package/src/cli/commands/contrast.ts +36 -0
- package/src/cli/commands/convert.ts +37 -0
- package/src/cli/commands/export.ts +102 -0
- package/src/cli/commands/find-contrast.ts +37 -0
- package/src/cli/commands/harmony.ts +30 -0
- package/src/cli/commands/mix.ts +29 -0
- package/src/cli/commands/palette.ts +66 -0
- package/src/cli/commands/scale.ts +75 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/schema-command.ts +31 -0
- package/src/cli/schemas.ts +86 -0
- package/src/cli/shared.ts +72 -0
- package/src/cli/testing.ts +27 -0
- package/src/color/a98-rgb.ts +11 -0
- package/src/color/argb.ts +21 -0
- package/src/color/calc.ts +84 -0
- package/src/color/compare.ts +53 -0
- package/src/color/contrast.ts +277 -0
- package/src/color/convert.ts +43 -0
- package/src/color/display-p3-linear.ts +9 -0
- package/src/color/display-p3.ts +9 -0
- package/src/color/expr.ts +158 -0
- package/src/color/gamut.ts +215 -0
- package/src/color/hsl.ts +57 -0
- package/src/color/hwb.ts +26 -0
- package/src/color/index.ts +43 -0
- package/src/color/lab.ts +29 -0
- package/src/color/lch.ts +8 -0
- package/src/color/math.ts +30 -0
- package/src/color/matrices.ts +91 -0
- package/src/color/mix.ts +158 -0
- package/src/color/named.ts +13 -0
- package/src/color/oklab.ts +19 -0
- package/src/color/oklch.ts +8 -0
- package/src/color/parse.ts +355 -0
- package/src/color/polar.ts +17 -0
- package/src/color/prophoto-rgb.ts +18 -0
- package/src/color/rec2020.ts +11 -0
- package/src/color/resolve.ts +87 -0
- package/src/color/rgb.ts +20 -0
- package/src/color/serialize.ts +96 -0
- package/src/color/spaces.ts +42 -0
- package/src/color/srgb-linear.ts +9 -0
- package/src/color/srgb.ts +9 -0
- package/src/color/test-utils.ts +10 -0
- package/src/color/transfer.ts +14 -0
- package/src/color/types.ts +41 -0
- package/src/color/unknown-name.ts +15 -0
- package/src/color/xyz-d50.ts +13 -0
- package/src/color/xyz-d65.ts +8 -0
- package/src/index.ts +37 -0
- package/{ts → src/legacy}/createColors.ts +6 -0
- package/{ts → src/legacy}/createColorsFromMap.ts +3 -0
- package/{ts → src/legacy}/interfaces.ts +4 -0
- package/{ts → src/legacy}/rgbaString.ts +1 -0
- package/src/palette/dark.ts +55 -0
- package/src/palette/harmony.ts +107 -0
- package/src/palette/index.ts +18 -0
- package/src/palette/semantic.ts +120 -0
- package/src/palette/validate.ts +82 -0
- package/src/scale/brand.ts +50 -0
- package/src/scale/contrast-scale.ts +48 -0
- package/src/scale/gradient.ts +91 -0
- package/src/scale/index.ts +5 -0
- package/src/scale/scale.ts +127 -0
- package/src/scale/tone.ts +69 -0
- package/src/tokens/css.ts +32 -0
- package/src/tokens/dtcg.ts +62 -0
- package/src/tokens/index.ts +11 -0
- package/src/tokens/tailwind.ts +24 -0
- package/src/tokens/tokens.ts +82 -0
- package/esm/createColors.d.ts +0 -6
- package/esm/createColors.d.ts.map +0 -1
- package/esm/createColors.js.map +0 -1
- package/esm/createColorsFromMap.d.ts +0 -9
- package/esm/createColorsFromMap.d.ts.map +0 -1
- package/esm/createColorsFromMap.js.map +0 -1
- package/esm/interfaces.d.ts +0 -18
- package/esm/interfaces.d.ts.map +0 -1
- package/esm/rgbHex.d.ts.map +0 -1
- package/esm/rgbHex.js.map +0 -1
- package/esm/rgbaString.d.ts +0 -6
- package/esm/rgbaString.js +0 -8
- package/esm/rgbaString.js.map +0 -1
- /package/{ts → src/legacy}/index.ts +0 -0
- /package/{ts → src/legacy}/rgbHex.ts +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ContrastOption, findContrast } from '../color/contrast.js'
|
|
2
|
+
import { toColor } from '../color/parse.js'
|
|
3
|
+
import { componentsIn } from '../color/spaces.js'
|
|
4
|
+
import type { Color } from '../color/types.js'
|
|
5
|
+
|
|
6
|
+
/** Options for `createContrastScale`. */
|
|
7
|
+
export interface ContrastScaleOptions {
|
|
8
|
+
/** The color the scale is built around: its hue and chroma carry to every step. */
|
|
9
|
+
seed: Color | string
|
|
10
|
+
/** What every step is measured against. */
|
|
11
|
+
background: Color | string
|
|
12
|
+
/** Target contrast per step, in order, or a record of named targets (`{ text: 7, border: 3 }`). */
|
|
13
|
+
targets: number[] | Record<string, number>
|
|
14
|
+
/** Default `wcag2`. */
|
|
15
|
+
contrast?: ContrastOption
|
|
16
|
+
/** Which way to look first from the background. Default: darker on a light background, lighter on a dark one. */
|
|
17
|
+
direction?: 'lighter' | 'darker'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One step of a contrast scale: the target it was given and the color and contrast it got. */
|
|
21
|
+
export interface ContrastStep {
|
|
22
|
+
/** The key in a record of targets; otherwise the step's index as a string. */
|
|
23
|
+
name: string
|
|
24
|
+
target: number
|
|
25
|
+
color: Color
|
|
26
|
+
/** The contrast the color achieves, measured on the color itself. */
|
|
27
|
+
contrast: number
|
|
28
|
+
/** False when no lightness reaches `target`; `color` is then the best one found. */
|
|
29
|
+
met: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A Leonardo-style scale: each step is the color nearest the background's
|
|
34
|
+
* lightness, at the seed's hue and chroma, that reaches its target contrast.
|
|
35
|
+
* Contrast is the same across hues, which a fixed-lightness scale cannot give.
|
|
36
|
+
*/
|
|
37
|
+
export function createContrastScale(options: ContrastScaleOptions): ContrastStep[] {
|
|
38
|
+
const { seed, background, targets, contrast, direction } = options
|
|
39
|
+
const [, chroma, hue] = componentsIn(toColor(seed), 'oklch') as [number, number, number | null]
|
|
40
|
+
const entries: [string, number][] = Array.isArray(targets)
|
|
41
|
+
? targets.map((t, i) => [String(i), t])
|
|
42
|
+
: Object.entries(targets)
|
|
43
|
+
return entries.map(([name, target]) => ({
|
|
44
|
+
name,
|
|
45
|
+
target,
|
|
46
|
+
...findContrast(background, target, { hue: hue ?? 0, chroma, contrast, direction })
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type MixOptions, mix } from '../color/mix.js'
|
|
2
|
+
import { toColor } from '../color/parse.js'
|
|
3
|
+
import type { Color } from '../color/types.js'
|
|
4
|
+
|
|
5
|
+
/** A color at a position on the gradient line. */
|
|
6
|
+
export interface GradientStop {
|
|
7
|
+
/** A CSS color string or a color object. Its alpha is the stop's alpha. */
|
|
8
|
+
color: Color | string
|
|
9
|
+
/**
|
|
10
|
+
* Where the stop sits, with 0 the start and 1 the end; it may fall outside
|
|
11
|
+
* 0 to 1. Leave it out to spread the stop evenly between its neighbors, as
|
|
12
|
+
* CSS gradients do.
|
|
13
|
+
*/
|
|
14
|
+
at?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `in` and `hue` work as they do in `mix()`. The default space is `oklab`. */
|
|
18
|
+
export type GradientOptions = MixOptions
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Places each stop the way CSS gradients do (CSS Images 3 section 3.5.3): a
|
|
22
|
+
* first stop without a position goes at 0 and a last one at 1, a position
|
|
23
|
+
* before an earlier stop's moves up to it, and a run of stops without a
|
|
24
|
+
* position spreads evenly between the stops around it.
|
|
25
|
+
*/
|
|
26
|
+
function positions(stops: readonly GradientStop[]): number[] {
|
|
27
|
+
const n = stops.length
|
|
28
|
+
const at: (number | undefined)[] = stops.map((s, i) => {
|
|
29
|
+
if (s.at !== undefined && !Number.isFinite(s.at)) throw new Error(`stop ${i} has a position of ${s.at}`)
|
|
30
|
+
return s.at
|
|
31
|
+
})
|
|
32
|
+
if (at[0] === undefined) at[0] = 0
|
|
33
|
+
if (at[n - 1] === undefined) at[n - 1] = 1
|
|
34
|
+
let max = Number.NEGATIVE_INFINITY
|
|
35
|
+
for (let i = 0; i < n; i++) {
|
|
36
|
+
const p = at[i]
|
|
37
|
+
if (p !== undefined) at[i] = max = Math.max(max, p)
|
|
38
|
+
}
|
|
39
|
+
for (let i = 1; i < n; i++) {
|
|
40
|
+
if (at[i] !== undefined) continue
|
|
41
|
+
let j = i
|
|
42
|
+
while (at[j] === undefined) j++
|
|
43
|
+
const from = at[i - 1] as number
|
|
44
|
+
const step = ((at[j] as number) - from) / (j - i + 1)
|
|
45
|
+
for (let k = i; k < j; k++) at[k] = from + step * (k - i + 1)
|
|
46
|
+
}
|
|
47
|
+
return at as number[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds `count` colors, evenly spaced from 0 to 1 along a gradient through
|
|
52
|
+
* `stops`, the way a CSS gradient blends them: each pair of neighboring stops
|
|
53
|
+
* is mixed with `mix()`, so `in` and `hue` work as they do there. A stop is
|
|
54
|
+
* `{ color, at? }`, or a bare color when it needs no position. Before the
|
|
55
|
+
* first stop and after the last, the color holds; where two stops share a
|
|
56
|
+
* position, the color changes there and the sample at that position takes the
|
|
57
|
+
* later stop. Colors come back in the `in` space, default `oklab`.
|
|
58
|
+
*
|
|
59
|
+
* Alpha comes from each stop's color, 1 when the color has none, so
|
|
60
|
+
* `rgb(0 0 255 / 0.2)` is a stop at alpha 0.2. Alpha is interpolated linearly
|
|
61
|
+
* between stops, and the other components are premultiplied by it, so a
|
|
62
|
+
* transparent stop does not pull its neighbor's color toward its own. A color
|
|
63
|
+
* whose alpha is 1 has no `alpha` key; a `none` alpha takes the neighboring
|
|
64
|
+
* stop's.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* createGradient([{ color: 'blue', at: 0 }, { color: 'lime', at: 0.6 }, { color: 'yellow', at: 1 }], 72)
|
|
68
|
+
* createGradient(['blue', 'lime'], 5, { in: 'oklch' })
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function createGradient(
|
|
72
|
+
stops: readonly (GradientStop | Color | string)[],
|
|
73
|
+
count: number,
|
|
74
|
+
options: GradientOptions = {}
|
|
75
|
+
): Color[] {
|
|
76
|
+
if (stops.length < 2) throw new Error(`a gradient needs at least 2 stops, got ${stops.length}`)
|
|
77
|
+
if (!Number.isInteger(count) || count < 2) throw new Error(`count must be an integer of at least 2, got ${count}`)
|
|
78
|
+
const list = stops.map((s): GradientStop => (typeof s === 'string' || 'colorSpace' in s ? { color: s } : s))
|
|
79
|
+
const colors = list.map((s) => toColor(s.color))
|
|
80
|
+
const at = positions(list)
|
|
81
|
+
const last = list.length - 2
|
|
82
|
+
return Array.from({ length: count }, (_, i) => {
|
|
83
|
+
const t = i / (count - 1)
|
|
84
|
+
let s = 0
|
|
85
|
+
while (s < last && (at[s + 1] as number) <= t) s++
|
|
86
|
+
const from = at[s] as number
|
|
87
|
+
const width = (at[s + 1] as number) - from
|
|
88
|
+
const local = width > 0 ? Math.min(1, Math.max(0, (t - from) / width)) : t < from ? 0 : 1
|
|
89
|
+
return mix(colors[s] as Color, colors[s + 1] as Color, local, options)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type BrandScaleOptions, scaleFromBrand } from './brand.js'
|
|
2
|
+
export { type ContrastScaleOptions, type ContrastStep, createContrastScale } from './contrast-scale.js'
|
|
3
|
+
export { createGradient, type GradientOptions, type GradientStop } from './gradient.js'
|
|
4
|
+
export { createScale, type ScaleOptions, type ScalePreset } from './scale.js'
|
|
5
|
+
export { lstar, MATERIAL_TONES, type TonalPaletteOptions, tonalPalette, toneColor } from './tone.js'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { GamutId } from '../color/gamut.js'
|
|
2
|
+
import { toGamut } from '../color/gamut.js'
|
|
3
|
+
import { toColor } from '../color/parse.js'
|
|
4
|
+
import { componentsIn } from '../color/spaces.js'
|
|
5
|
+
import type { Color } from '../color/types.js'
|
|
6
|
+
import { unknownName } from '../color/unknown-name.js'
|
|
7
|
+
import { MATERIAL_TONES, toneColor } from './tone.js'
|
|
8
|
+
|
|
9
|
+
/** The built-in step curves. */
|
|
10
|
+
export type ScalePreset = 'tailwind-11' | 'radix-12' | 'tone-13' | 'even'
|
|
11
|
+
|
|
12
|
+
/** A step curve. `chroma` and `hueShift` are relative to the step at `anchor`, which takes the seed's own. */
|
|
13
|
+
interface Curve {
|
|
14
|
+
/** OKLCH lightness 0 to 1, or CIE L* tones 0 to 100 when `tone` is set. */
|
|
15
|
+
lightness: number[]
|
|
16
|
+
chroma: number[]
|
|
17
|
+
hueShift: number[]
|
|
18
|
+
anchor: number
|
|
19
|
+
tone?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tailwind v4 blue, 50 to 950. The preset is this one hand-tuned scale, so a
|
|
23
|
+
// seed of blue-500 gives Tailwind's blue back, and other hues get its shape.
|
|
24
|
+
// biome-ignore lint/suspicious/noApproximativeNumericConstant: 0.707 is Tailwind blue-400's lightness, not 1/sqrt(2)
|
|
25
|
+
const TW_L = [0.97, 0.932, 0.882, 0.809, 0.707, 0.623, 0.546, 0.488, 0.424, 0.379, 0.282]
|
|
26
|
+
const TW_C = [0.014, 0.032, 0.059, 0.105, 0.165, 0.214, 0.245, 0.243, 0.199, 0.146, 0.091]
|
|
27
|
+
const TW_H = [254.604, 255.585, 254.128, 251.813, 254.624, 259.815, 262.881, 264.376, 265.638, 265.522, 267.935]
|
|
28
|
+
|
|
29
|
+
// Radix-like light scale: step 9 is the pure solid, 11 and 12 are text.
|
|
30
|
+
const RADIX_L = [0.992, 0.978, 0.953, 0.925, 0.894, 0.857, 0.81, 0.745, 0.62, 0.585, 0.52, 0.32]
|
|
31
|
+
const RADIX_C = [0.04, 0.08, 0.2, 0.32, 0.43, 0.52, 0.6, 0.75, 1, 0.95, 0.9, 0.35]
|
|
32
|
+
|
|
33
|
+
function even(n: number): Curve {
|
|
34
|
+
const t = (i: number) => i / (n - 1)
|
|
35
|
+
const bell = (i: number) => Math.sin(Math.PI * (0.05 + 0.9 * t(i)))
|
|
36
|
+
const anchor = Math.round((n - 1) / 2)
|
|
37
|
+
return {
|
|
38
|
+
lightness: Array.from({ length: n }, (_, i) => 0.97 - 0.72 * t(i)),
|
|
39
|
+
chroma: Array.from({ length: n }, (_, i) => bell(i) / bell(anchor)),
|
|
40
|
+
hueShift: Array.from({ length: n }, () => 0),
|
|
41
|
+
anchor
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PRESETS: Record<string, Curve> = {
|
|
46
|
+
__proto__: null as never,
|
|
47
|
+
'tailwind-11': {
|
|
48
|
+
lightness: TW_L,
|
|
49
|
+
chroma: TW_C.map((c) => c / TW_C[5]),
|
|
50
|
+
hueShift: TW_H.map((h) => h - TW_H[5]),
|
|
51
|
+
anchor: 5
|
|
52
|
+
},
|
|
53
|
+
'radix-12': {
|
|
54
|
+
lightness: RADIX_L,
|
|
55
|
+
chroma: RADIX_C,
|
|
56
|
+
hueShift: RADIX_L.map(() => 0),
|
|
57
|
+
anchor: 8
|
|
58
|
+
},
|
|
59
|
+
'tone-13': {
|
|
60
|
+
lightness: [...MATERIAL_TONES].reverse(),
|
|
61
|
+
chroma: MATERIAL_TONES.map(() => 1),
|
|
62
|
+
hueShift: MATERIAL_TONES.map(() => 0),
|
|
63
|
+
anchor: 6,
|
|
64
|
+
tone: true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Options for `createScale`. Only `seed` is required. */
|
|
69
|
+
export interface ScaleOptions {
|
|
70
|
+
/** The color the scale is built around. Its hue and chroma set the anchor step. */
|
|
71
|
+
seed: Color | string
|
|
72
|
+
/**
|
|
73
|
+
* A preset, or a number of steps for an even scale. Default `tailwind-11`.
|
|
74
|
+
* `tone-13` steps are CIE L* tones, so any two steps whose tones differ by
|
|
75
|
+
* 40 reach contrast 3 and by 50 about 4.5.
|
|
76
|
+
*/
|
|
77
|
+
steps?: ScalePreset | number
|
|
78
|
+
/** Overrides the curve's lightness, one per step: OKLCH L 0 to 1 (L* tones 0 to 100 for `tone-13`). */
|
|
79
|
+
lightness?: number[]
|
|
80
|
+
/** Overrides chroma, as a multiplier of the seed's chroma, one per step. */
|
|
81
|
+
chroma?: number[]
|
|
82
|
+
/** Overrides hue drift, as degrees added to the seed's hue, one per step; a number is applied to every step. */
|
|
83
|
+
hueShift?: number[] | number
|
|
84
|
+
/** The gamut every step must fit. Default `srgb`. */
|
|
85
|
+
gamut?: GamutId
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function curveOf(steps: ScalePreset | number): Curve {
|
|
89
|
+
if (typeof steps === 'number') {
|
|
90
|
+
if (!Number.isInteger(steps) || steps < 2) throw new Error(`steps must be an integer of at least 2, got ${steps}`)
|
|
91
|
+
return even(steps)
|
|
92
|
+
}
|
|
93
|
+
if (steps === 'even') return even(11)
|
|
94
|
+
const curve = PRESETS[steps]
|
|
95
|
+
if (!curve) throw unknownName('scale preset', steps, [...Object.keys(PRESETS), 'even'])
|
|
96
|
+
return curve
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds a scale from light to dark around `seed`: one color per step, in
|
|
101
|
+
* OKLCH, mapped into `gamut`. The preset gives each step's lightness, chroma
|
|
102
|
+
* (relative to the seed's) and hue drift; pass `lightness`, `chroma` or
|
|
103
|
+
* `hueShift` to replace any of them.
|
|
104
|
+
*/
|
|
105
|
+
export function createScale(options: ScaleOptions): Color[] {
|
|
106
|
+
const { seed, steps = 'tailwind-11', gamut = 'srgb' } = options
|
|
107
|
+
const curve = curveOf(steps)
|
|
108
|
+
const n = curve.lightness.length
|
|
109
|
+
const pick = <T>(name: string, over: T[] | undefined, base: T[]): T[] => {
|
|
110
|
+
if (over === undefined) return base
|
|
111
|
+
if (over.length !== n) throw new Error(`${name} needs ${n} values, got ${over.length}`)
|
|
112
|
+
return over
|
|
113
|
+
}
|
|
114
|
+
const lightness = pick('lightness', options.lightness, curve.lightness)
|
|
115
|
+
const chroma = pick('chroma', options.chroma, curve.chroma)
|
|
116
|
+
const drift =
|
|
117
|
+
typeof options.hueShift === 'number' ? curve.lightness.map(() => options.hueShift as number) : options.hueShift
|
|
118
|
+
const hueShift = pick('hueShift', drift, curve.hueShift)
|
|
119
|
+
|
|
120
|
+
const [, seedC, seedH] = componentsIn(toColor(seed), 'oklch') as [number, number, number | null]
|
|
121
|
+
const hue = seedH ?? 0
|
|
122
|
+
return lightness.map((l, i) => {
|
|
123
|
+
const c = seedC * (chroma[i] as number)
|
|
124
|
+
const h = hue + (hueShift[i] as number)
|
|
125
|
+
return curve.tone ? toneColor(l, c, h, gamut) : toGamut({ colorSpace: 'oklch', components: [l, c, h] }, { gamut })
|
|
126
|
+
})
|
|
127
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type GamutId, inGamut } from '../color/gamut.js'
|
|
2
|
+
import { componentsIn } from '../color/spaces.js'
|
|
3
|
+
import type { Color } from '../color/types.js'
|
|
4
|
+
|
|
5
|
+
/** CIE L* (0 to 100) of a color: its relative luminance on the CIELAB lightness curve. */
|
|
6
|
+
export function lstar(color: Color): number {
|
|
7
|
+
const y = Math.max(0, componentsIn(color, 'xyz-d65')[1] as number)
|
|
8
|
+
return y > 216 / 24389 ? 116 * Math.cbrt(y) - 16 : (24389 / 27) * y
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Material's tones: 0 is black, 100 is white. */
|
|
12
|
+
export const MATERIAL_TONES: readonly number[] = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100]
|
|
13
|
+
|
|
14
|
+
/** Options for `tonalPalette`. */
|
|
15
|
+
export interface TonalPaletteOptions {
|
|
16
|
+
/** The gamut the colors must fit. Default `srgb`. */
|
|
17
|
+
gamut?: GamutId
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The color at CIE L* `tone` with the given OKLCH hue and chroma, mapped into
|
|
22
|
+
* the gamut. OKLCH lightness is searched until the *mapped* color lands on
|
|
23
|
+
* `tone`, so chroma tapers where the gamut narrows and the tone stays true.
|
|
24
|
+
*/
|
|
25
|
+
export function toneColor(tone: number, chroma: number, hue: number, gamut: GamutId = 'srgb'): Color {
|
|
26
|
+
if (tone <= 0) return { colorSpace: 'oklch', components: [0, 0, hue] }
|
|
27
|
+
if (tone >= 100) return { colorSpace: 'oklch', components: [1, 0, hue] }
|
|
28
|
+
// The hue is fixed, so its cosine and sine are taken once: OKLCH converts by
|
|
29
|
+
// way of OKLab with exactly these products, so the results are the same.
|
|
30
|
+
const r = (hue * Math.PI) / 180
|
|
31
|
+
const cos = Math.cos(r)
|
|
32
|
+
const sin = Math.sin(r)
|
|
33
|
+
const lab = (l: number, c: number): Color => ({ colorSpace: 'oklab', components: [l, c * cos, c * sin] })
|
|
34
|
+
// The most chroma, up to `chroma`, that fits at this lightness; no clipping, so L* is monotone in L.
|
|
35
|
+
const chromaAt = (l: number): number => {
|
|
36
|
+
if (inGamut(lab(l, chroma), gamut)) return chroma
|
|
37
|
+
let [lo, hi] = [0, chroma]
|
|
38
|
+
for (let i = 0; i < 24; i++) {
|
|
39
|
+
const mid = (lo + hi) / 2
|
|
40
|
+
if (inGamut(lab(l, mid), gamut)) lo = mid
|
|
41
|
+
else hi = mid
|
|
42
|
+
}
|
|
43
|
+
return lo
|
|
44
|
+
}
|
|
45
|
+
let lo = 0
|
|
46
|
+
let hi = 1
|
|
47
|
+
for (let i = 0; i < 40; i++) {
|
|
48
|
+
const mid = (lo + hi) / 2
|
|
49
|
+
if (lstar(lab(mid, chromaAt(mid))) < tone) lo = mid
|
|
50
|
+
else hi = mid
|
|
51
|
+
}
|
|
52
|
+
const l = (lo + hi) / 2
|
|
53
|
+
return { colorSpace: 'oklch', components: [l, chromaAt(l), hue] }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A tonal palette, as in Material: one OKLCH hue and chroma held across L*
|
|
58
|
+
* tones. Two colors whose tones differ by 40 or more reach WCAG 2 contrast 3,
|
|
59
|
+
* and by 50 or more about 4.5 (4.48 at the worst pair, 100 against 50), so a
|
|
60
|
+
* pair can be picked by tone alone. Default tones are Material's 13.
|
|
61
|
+
*/
|
|
62
|
+
export function tonalPalette(
|
|
63
|
+
hue: number,
|
|
64
|
+
chroma: number,
|
|
65
|
+
tones: readonly number[] = MATERIAL_TONES,
|
|
66
|
+
options: TonalPaletteOptions = {}
|
|
67
|
+
): Color[] {
|
|
68
|
+
return tones.map((t) => toneColor(t, chroma, hue, options.gamut))
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ToCssOptions, toCss } from '../color/serialize.js'
|
|
2
|
+
import { convert } from '../color/spaces.js'
|
|
3
|
+
import type { ColorSpaceId } from '../color/types.js'
|
|
4
|
+
import { cssNames, flatten, type TokenTree } from './tokens.js'
|
|
5
|
+
|
|
6
|
+
/** Options for `toCssVariables`. It also takes the options of `toCss`. */
|
|
7
|
+
export interface CssVariablesOptions extends ToCssOptions {
|
|
8
|
+
/** The first part of every name. Default `color`, so `primary.500` becomes `--color-primary-500`. */
|
|
9
|
+
prefix?: string
|
|
10
|
+
/** The rule's selector. Default `:root`. */
|
|
11
|
+
selector?: string
|
|
12
|
+
/** Convert every color to this space first. Default: print each in its own space. */
|
|
13
|
+
space?: ColorSpaceId
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Prints one `--name: value;` line per color. */
|
|
17
|
+
export function declarations(tree: TokenTree, options: CssVariablesOptions & { prefix: string }): string[] {
|
|
18
|
+
const { prefix, space, ...css } = options
|
|
19
|
+
return [...cssNames(flatten(tree), prefix)].map(
|
|
20
|
+
([name, color]) => `${name}: ${toCss(space ? convert(color, space) : color, css)};`
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Exports colors as CSS custom properties in one rule:
|
|
26
|
+
* `:root {\n\t--color-primary-500: oklch(0.62 0.19 260);\n}`.
|
|
27
|
+
*/
|
|
28
|
+
export function toCssVariables(tree: TokenTree, options: CssVariablesOptions = {}): string {
|
|
29
|
+
const { selector = ':root', prefix = 'color', ...rest } = options
|
|
30
|
+
const lines = declarations(tree, { ...rest, prefix })
|
|
31
|
+
return `${selector} {\n${lines.map((l) => `\t${l}\n`).join('')}}\n`
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toGamut } from '../color/gamut.js'
|
|
2
|
+
import { toHex } from '../color/serialize.js'
|
|
3
|
+
import { convert } from '../color/spaces.js'
|
|
4
|
+
import type { Color, ColorSpaceId, Component } from '../color/types.js'
|
|
5
|
+
import { flatten, type TokenTree } from './tokens.js'
|
|
6
|
+
|
|
7
|
+
/** Options for `toDtcg` and `toDtcgValue`. */
|
|
8
|
+
export interface DtcgOptions {
|
|
9
|
+
/** Add the `hex` fallback (an sRGB `#rrggbb`, gamut-mapped when the color is outside sRGB). Default `true`. */
|
|
10
|
+
hex?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A color value as DTCG 2025.10 defines it. */
|
|
14
|
+
export interface DtcgColorValue {
|
|
15
|
+
colorSpace: Exclude<ColorSpaceId, 'display-p3-linear'>
|
|
16
|
+
components: (number | 'none')[]
|
|
17
|
+
alpha?: number | 'none'
|
|
18
|
+
hex?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A DTCG color token: a `$type` of `color` and its {@link DtcgColorValue}. */
|
|
22
|
+
export interface DtcgToken {
|
|
23
|
+
$type: 'color'
|
|
24
|
+
$value: DtcgColorValue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A DTCG group: named tokens and nested groups, as `toDtcg` returns for a token tree. */
|
|
28
|
+
export interface DtcgGroup {
|
|
29
|
+
[name: string]: DtcgToken | DtcgGroup
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const none = (c: Component): number | 'none' => (c === null ? 'none' : c)
|
|
33
|
+
|
|
34
|
+
/** One color as a DTCG color value. `display-p3-linear` is not a DTCG space, so it becomes `display-p3`. */
|
|
35
|
+
export function toDtcgValue(color: Color, options: DtcgOptions = {}): DtcgColorValue {
|
|
36
|
+
const c = color.colorSpace === 'display-p3-linear' ? convert(color, 'display-p3') : color
|
|
37
|
+
const value: DtcgColorValue = {
|
|
38
|
+
colorSpace: c.colorSpace as DtcgColorValue['colorSpace'],
|
|
39
|
+
components: c.components.map(none)
|
|
40
|
+
}
|
|
41
|
+
if (c.alpha !== undefined && c.alpha !== 1) value.alpha = none(c.alpha)
|
|
42
|
+
if (options.hex !== false) value.hex = toHex(toGamut({ ...c, alpha: 1 }, { gamut: 'srgb' }))
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exports colors as a DTCG 2025.10 token file: nested groups whose leaves are
|
|
48
|
+
* `{ "$type": "color", "$value": { colorSpace, components, alpha?, hex? } }`.
|
|
49
|
+
* `none` components print as `"none"`, as the format asks. The result is plain
|
|
50
|
+
* JSON, so `JSON.stringify` writes the file.
|
|
51
|
+
*/
|
|
52
|
+
export function toDtcg(tree: TokenTree, options: DtcgOptions = {}): DtcgGroup {
|
|
53
|
+
// Groups have no prototype, so a token named `__proto__` or `constructor` is an ordinary key.
|
|
54
|
+
const root: DtcgGroup = Object.create(null)
|
|
55
|
+
for (const { path, color } of flatten(tree)) {
|
|
56
|
+
if (!path.length) throw new TypeError('A DTCG file needs a name for each token: pass an object, not a single color')
|
|
57
|
+
let group = root
|
|
58
|
+
for (const name of path.slice(0, -1)) group = (group[name] ??= Object.create(null)) as DtcgGroup
|
|
59
|
+
group[path[path.length - 1] as string] = { $type: 'color', $value: toDtcgValue(color, options) }
|
|
60
|
+
}
|
|
61
|
+
return root
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { type CssVariablesOptions, toCssVariables } from './css.js'
|
|
2
|
+
export {
|
|
3
|
+
type DtcgColorValue,
|
|
4
|
+
type DtcgGroup,
|
|
5
|
+
type DtcgOptions,
|
|
6
|
+
type DtcgToken,
|
|
7
|
+
toDtcg,
|
|
8
|
+
toDtcgValue
|
|
9
|
+
} from './dtcg.js'
|
|
10
|
+
export { type TailwindThemeOptions, toTailwindTheme } from './tailwind.js'
|
|
11
|
+
export type { TokenTree } from './tokens.js'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ColorSpaceId } from '../color/types.js'
|
|
2
|
+
import { type CssVariablesOptions, declarations } from './css.js'
|
|
3
|
+
import type { TokenTree } from './tokens.js'
|
|
4
|
+
|
|
5
|
+
/** Options for `toTailwindTheme`. It also takes the options of `toCss`. */
|
|
6
|
+
export interface TailwindThemeOptions extends Omit<CssVariablesOptions, 'selector' | 'prefix'> {
|
|
7
|
+
/** The theme variable namespace. Default `color`, which makes `bg-primary-500` and `text-primary-500` work. */
|
|
8
|
+
namespace?: string
|
|
9
|
+
/** Write `@theme inline`, so utilities hold the value instead of a `var()` reference. */
|
|
10
|
+
inline?: boolean
|
|
11
|
+
/** Default `oklch`, the space Tailwind v4's own palette uses. */
|
|
12
|
+
space?: ColorSpaceId
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Exports colors as a Tailwind v4 `@theme` block. Paste it into the CSS file
|
|
17
|
+
* that has `@import "tailwindcss"`. An array of 11 colors gets the keys
|
|
18
|
+
* 50 to 950, so a `tailwind-11` scale reads like Tailwind's own.
|
|
19
|
+
*/
|
|
20
|
+
export function toTailwindTheme(tree: TokenTree, options: TailwindThemeOptions = {}): string {
|
|
21
|
+
const { namespace = 'color', inline = false, space = 'oklch', ...rest } = options
|
|
22
|
+
const lines = declarations(tree, { ...rest, space, prefix: namespace })
|
|
23
|
+
return `@theme${inline ? ' inline' : ''} {\n${lines.map((l) => `\t${l}\n`).join('')}}\n`
|
|
24
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { parse } from '../color/parse.js'
|
|
2
|
+
import type { Color } from '../color/types.js'
|
|
3
|
+
import type { SemanticPalette } from '../palette/semantic.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What the exporters accept: a color, a CSS color string, an array of them (a
|
|
7
|
+
* scale), or an object of any of these nested to any depth. A `SemanticPalette`
|
|
8
|
+
* works as is: its `scales` and its role pairs export, its `meta` and ratios do not.
|
|
9
|
+
*/
|
|
10
|
+
export type TokenTree = SemanticPalette | Color | string | readonly TokenTree[] | { readonly [name: string]: TokenTree }
|
|
11
|
+
|
|
12
|
+
/** A color and the path of names that leads to it. */
|
|
13
|
+
export interface Token {
|
|
14
|
+
path: string[]
|
|
15
|
+
color: Color
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TAILWIND_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
19
|
+
|
|
20
|
+
const isColor = (v: unknown): v is Color =>
|
|
21
|
+
typeof v === 'object' &&
|
|
22
|
+
v !== null &&
|
|
23
|
+
typeof (v as Color).colorSpace === 'string' &&
|
|
24
|
+
Array.isArray((v as Color).components)
|
|
25
|
+
|
|
26
|
+
// A semantic palette carries numbers (`ratio`, `target`, `meta`) that are not colors.
|
|
27
|
+
function dropMeasures(tree: Record<string, unknown>): Record<string, unknown> {
|
|
28
|
+
const { meta: _meta, scales, roles, ...rest } = tree
|
|
29
|
+
const pairs = Object.fromEntries(
|
|
30
|
+
Object.entries(roles as Record<string, { bg: Color; fg: Color }>).map(([name, p]) => [name, { bg: p.bg, fg: p.fg }])
|
|
31
|
+
)
|
|
32
|
+
return { ...rest, ...(scales as object), roles: pairs }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isSemantic = (v: object): boolean => 'roles' in v && 'scales' in v && 'meta' in v
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walks a token tree into a flat list. An array gets the Tailwind keys
|
|
39
|
+
* (50 to 950) when it has 11 colors and 1-based numbers otherwise; pass an
|
|
40
|
+
* object to choose the keys. A value that is not a color, a string, an array
|
|
41
|
+
* or an object throws with its path.
|
|
42
|
+
*/
|
|
43
|
+
export function flatten(tree: TokenTree): Token[] {
|
|
44
|
+
const out: Token[] = []
|
|
45
|
+
const walk = (node: unknown, path: string[]): void => {
|
|
46
|
+
if (typeof node === 'string') return void out.push({ path, color: parse(node) })
|
|
47
|
+
if (isColor(node)) return void out.push({ path, color: node })
|
|
48
|
+
if (Array.isArray(node)) {
|
|
49
|
+
node.forEach((v, i) => {
|
|
50
|
+
walk(v, [...path, String(node.length === TAILWIND_STEPS.length ? TAILWIND_STEPS[i] : i + 1)])
|
|
51
|
+
})
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (typeof node === 'object' && node !== null) {
|
|
55
|
+
const obj = isSemantic(node) ? dropMeasures(node as Record<string, unknown>) : (node as Record<string, unknown>)
|
|
56
|
+
for (const [k, v] of Object.entries(obj)) walk(v, [...path, k])
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
throw new TypeError(`${path.join('.') || '(root)'} is not a color, a string, an array or an object`)
|
|
60
|
+
}
|
|
61
|
+
walk(tree, [])
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `primaryContainer` and `Primary Container` both become `primary-container`. */
|
|
66
|
+
export const kebab = (name: string): string =>
|
|
67
|
+
name
|
|
68
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
69
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
70
|
+
.replace(/^-+|-+$/g, '')
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
|
|
73
|
+
/** Names tokens for CSS: kebab-case segments joined by `-` after `prefix`. Two tokens with one name throw. */
|
|
74
|
+
export function cssNames(tokens: Token[], prefix: string): Map<string, Color> {
|
|
75
|
+
const out = new Map<string, Color>()
|
|
76
|
+
for (const { path, color } of tokens) {
|
|
77
|
+
const name = `--${[prefix, ...path].map(kebab).filter(Boolean).join('-')}`
|
|
78
|
+
if (out.has(name)) throw new Error(`${name} is produced by more than one token`)
|
|
79
|
+
out.set(name, color)
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
package/esm/createColors.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { Alpha, RGB, RGBA } from "./interfaces.js";
|
|
2
|
-
//#region ts/createColors.d.ts
|
|
3
|
-
declare function createColors(from: RGB | RGBA, to: RGB | RGBA, shades: number, alpha?: Alpha): RGBA[];
|
|
4
|
-
//#endregion
|
|
5
|
-
export { createColors };
|
|
6
|
-
//# sourceMappingURL=createColors.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createColors.d.ts","names":[],"sources":["../ts/createColors.ts"],"mappings":";;iBAEgB,aACf,MAAM,MAAM,MACZ,IAAI,MAAM,MACV,gBACA,QAAO,QAIL"}
|
package/esm/createColors.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createColors.js","names":[],"sources":["../ts/createColors.ts"],"sourcesContent":["import type { Alpha, RGB, RGBA } from './interfaces.js'\n\nexport function createColors(\n\tfrom: RGB | RGBA,\n\tto: RGB | RGBA,\n\tshades: number,\n\talpha: Alpha = [\n\t\tfrom.length === 4 ? from[3] : to.length === 4 ? to[3] : 1,\n\t\tto.length === 4 ? to[3] : from.length === 4 ? from[3] : 1\n\t]\n): RGBA[] {\n\tconst start = [...from]\n\tconst diff = [to[0] - from[0], to[1] - from[1], to[2] - from[2], alpha[1] - alpha[0]]\n\n\tif (start.length === 3) start.push(alpha[0])\n\n\tconst inc = 1 / Math.max(shades - 1, 1)\n\n\tconst rgba: any[] = []\n\tfor (let i = 0; i < shades; i++) {\n\t\tconst color = [\n\t\t\tMath.round(start[0] + i * diff[0] * inc),\n\t\t\tMath.round(start[1] + i * diff[1] * inc),\n\t\t\tMath.round(start[2] + i * diff[2] * inc),\n\t\t\tstart[3] + i * diff[3] * inc\n\t\t]\n\t\trgba.push(color)\n\t}\n\treturn rgba\n}\n"],"mappings":";AAEA,SAAgB,aACf,MACA,IACA,QACA,QAAe,CACd,KAAK,WAAW,IAAI,KAAK,KAAK,GAAG,WAAW,IAAI,GAAG,KAAK,GACxD,GAAG,WAAW,IAAI,GAAG,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,CACzD,GACS;CACT,MAAM,QAAQ,CAAC,GAAG,IAAI;CACtB,MAAM,OAAO;EAAC,GAAG,KAAK,KAAK;EAAI,GAAG,KAAK,KAAK;EAAI,GAAG,KAAK,KAAK;EAAI,MAAM,KAAK,MAAM;CAAE;CAEpF,IAAI,MAAM,WAAW,GAAG,MAAM,KAAK,MAAM,EAAE;CAE3C,MAAM,MAAM,IAAI,KAAK,IAAI,SAAS,GAAG,CAAC;CAEtC,MAAM,OAAc,CAAC;CACrB,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAChC,MAAM,QAAQ;GACb,KAAK,MAAM,MAAM,KAAK,IAAI,KAAK,KAAK,GAAG;GACvC,KAAK,MAAM,MAAM,KAAK,IAAI,KAAK,KAAK,GAAG;GACvC,KAAK,MAAM,MAAM,KAAK,IAAI,KAAK,KAAK,GAAG;GACvC,MAAM,KAAK,IAAI,KAAK,KAAK;EAC1B;EACA,KAAK,KAAK,KAAK;CAChB;CACA,OAAO;AACR"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Alpha, ColorMap, RGBA } from "./interfaces.js";
|
|
2
|
-
//#region ts/createColorsFromMap.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Create colors with specified color map.
|
|
5
|
-
*/
|
|
6
|
-
declare function createColorsFromMap(colormap: ColorMap, shades: number, alpha?: Alpha): RGBA[];
|
|
7
|
-
//#endregion
|
|
8
|
-
export { createColorsFromMap };
|
|
9
|
-
//# sourceMappingURL=createColorsFromMap.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createColorsFromMap.d.ts","names":[],"sources":["../ts/createColorsFromMap.ts"],"mappings":";;;;;iBAMgB,oBAAoB,UAAU,UAAU,gBAAgB,QAAQ,QAAQ"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createColorsFromMap.js","names":[],"sources":["../ts/createColorsFromMap.ts"],"sourcesContent":["import { createColors } from './createColors.js'\nimport type { Alpha, ColorMap, RGBA } from './interfaces.js'\n\n/**\n * Create colors with specified color map.\n */\nexport function createColorsFromMap(colormap: ColorMap, shades: number, alpha?: Alpha): RGBA[] {\n\tif (shades < colormap.length) {\n\t\tthrow new Error(`Requires at least ${colormap.length} shades.`)\n\t}\n\n\tconst result: RGBA[] = []\n\tconst steps: number[] = []\n\tfor (let i = 0; i < colormap.length; i++) {\n\t\tsteps.push(Math.round(colormap[i].index * shades))\n\t}\n\n\tfor (let i = 0; i < colormap.length - 1; i++) {\n\t\tconst n = steps[i + 1] - steps[i]\n\t\tconst from = colormap[i].rgb\n\t\tconst to = colormap[i + 1].rgb\n\t\tresult.push(...createColors(from, to, n, alpha))\n\t}\n\n\treturn result\n}\n"],"mappings":";;;;;AAMA,SAAgB,oBAAoB,UAAoB,QAAgB,OAAuB;CAC9F,IAAI,SAAS,SAAS,QACrB,MAAM,IAAI,MAAM,qBAAqB,SAAS,OAAO,SAAS;CAG/D,MAAM,SAAiB,CAAC;CACxB,MAAM,QAAkB,CAAC;CACzB,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KACpC,MAAM,KAAK,KAAK,MAAM,SAAS,EAAE,CAAC,QAAQ,MAAM,CAAC;CAGlD,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;EAC7C,MAAM,IAAI,MAAM,IAAI,KAAK,MAAM;EAC/B,MAAM,OAAO,SAAS,EAAE,CAAC;EACzB,MAAM,KAAK,SAAS,IAAI,EAAE,CAAC;EAC3B,OAAO,KAAK,GAAG,aAAa,MAAM,IAAI,GAAG,KAAK,CAAC;CAChD;CAEA,OAAO;AACR"}
|
package/esm/interfaces.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//#region ts/interfaces.d.ts
|
|
2
|
-
type RGB = [number, number, number];
|
|
3
|
-
type RGBA = [number, number, number, number];
|
|
4
|
-
type Alpha = [number, number];
|
|
5
|
-
type ColorMap = {
|
|
6
|
-
/**
|
|
7
|
-
* Defines how fast or slow the `rgb` values will change from one segment to the next.
|
|
8
|
-
* i.e. the steepness of the gradient between two segments.
|
|
9
|
-
*/
|
|
10
|
-
index: number;
|
|
11
|
-
/**
|
|
12
|
-
* RGB or RGBA. if RGBA, this alpha will be used instead of the alpha specified in the call.
|
|
13
|
-
*/
|
|
14
|
-
rgb: RGB | RGBA;
|
|
15
|
-
}[];
|
|
16
|
-
//#endregion
|
|
17
|
-
export { Alpha, ColorMap, RGB, RGBA };
|
|
18
|
-
//# sourceMappingURL=interfaces.d.ts.map
|
package/esm/interfaces.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"interfaces.d.ts","names":[],"sources":["../ts/interfaces.ts"],"mappings":";KAAY;KACA;KACA;KAEA;;;;;EAKX;;;;EAIA,KAAK,MAAM"}
|
package/esm/rgbHex.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rgbHex.d.ts","names":[],"sources":["../ts/rgbHex.ts"],"mappings":";;;;;;iBAMgB,OAAO,KAAK,MAAM"}
|
package/esm/rgbHex.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rgbHex.js","names":[],"sources":["../ts/rgbHex.ts"],"sourcesContent":["import type { RGB, RGBA } from './interfaces.js'\n\n/**\n * Convert `RGB` to `#rgb`\n * JavaScript note: no check for array length, use it properly.\n */\nexport function rgbHex(rgb: RGB | RGBA) {\n\tlet hex = '#'\n\tfor (let i = 0; i < 3; i++) {\n\t\thex += d2h(rgb[i])\n\t}\n\treturn hex\n}\n\nfunction d2h(d: number) {\n\tconst s = (+d).toString(16)\n\treturn s.length < 2 ? '0' + s : s\n}\n"],"mappings":";;;;;AAMA,SAAgB,OAAO,KAAiB;CACvC,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACtB,OAAO,IAAI,IAAI,EAAE;CAElB,OAAO;AACR;AAEA,SAAS,IAAI,GAAW;CACvB,MAAM,KAAK,CAAC,EAAA,CAAG,SAAS,EAAE;CAC1B,OAAO,EAAE,SAAS,IAAI,MAAM,IAAI;AACjC"}
|
package/esm/rgbaString.d.ts
DELETED
package/esm/rgbaString.js
DELETED
package/esm/rgbaString.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rgbaString.js","names":[],"sources":["../ts/rgbaString.ts"],"sourcesContent":["import type { RGBA } from './interfaces.js'\n\nexport function rgbaString(rgba: RGBA) {\n\treturn 'rgba(' + rgba.join(',') + ')'\n}\n"],"mappings":";AAEA,SAAgB,WAAW,MAAY;CACtC,OAAO,UAAU,KAAK,KAAK,GAAG,IAAI;AACnC"}
|
|
File without changes
|
|
File without changes
|