@clhaas/palette-kit 0.1.0 → 0.1.1

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.
Files changed (69) hide show
  1. package/dist/alpha/generateAlphaScale.d.ts +5 -0
  2. package/dist/alpha/generateAlphaScale.js +34 -0
  3. package/dist/contrast/apca.d.ts +2 -0
  4. package/dist/contrast/apca.js +5 -0
  5. package/dist/contrast/onSolid.d.ts +6 -0
  6. package/dist/contrast/onSolid.js +28 -0
  7. package/dist/contrast/solveText.d.ts +2 -0
  8. package/dist/contrast/solveText.js +31 -0
  9. package/dist/createTheme.d.ts +32 -0
  10. package/dist/createTheme.js +75 -0
  11. package/dist/data/radixSeeds.d.ts +3 -0
  12. package/dist/data/radixSeeds.js +34 -0
  13. package/dist/diagnostics/analyzeScale.d.ts +2 -0
  14. package/dist/diagnostics/analyzeScale.js +4 -0
  15. package/dist/diagnostics/analyzeTheme.d.ts +2 -0
  16. package/dist/diagnostics/analyzeTheme.js +35 -0
  17. package/dist/diagnostics/warnings.d.ts +2 -0
  18. package/dist/diagnostics/warnings.js +20 -0
  19. package/dist/engine/curves.d.ts +9 -0
  20. package/dist/engine/curves.js +48 -0
  21. package/dist/engine/oklch.d.ts +8 -0
  22. package/dist/engine/oklch.js +40 -0
  23. package/dist/engine/templates.d.ts +14 -0
  24. package/dist/engine/templates.js +45 -0
  25. package/dist/exporters/selectColorMode.d.ts +2 -0
  26. package/dist/exporters/selectColorMode.js +19 -0
  27. package/dist/exporters/toCssVars.d.ts +12 -0
  28. package/dist/exporters/toCssVars.js +84 -0
  29. package/dist/exporters/toJson.d.ts +3 -0
  30. package/dist/exporters/toJson.js +25 -0
  31. package/dist/exporters/toReactNative.d.ts +30 -0
  32. package/dist/exporters/toReactNative.js +26 -0
  33. package/dist/exporters/toTailwind.d.ts +16 -0
  34. package/dist/exporters/toTailwind.js +90 -0
  35. package/dist/exporters/toTs.d.ts +3 -0
  36. package/dist/exporters/toTs.js +28 -0
  37. package/dist/generateScale.d.ts +15 -0
  38. package/dist/generateScale.js +96 -0
  39. package/{src/index.ts → dist/index.d.ts} +1 -15
  40. package/dist/index.js +15 -0
  41. package/dist/tokens/presetRadixLikeUi.d.ts +5 -0
  42. package/dist/tokens/presetRadixLikeUi.js +55 -0
  43. package/dist/types.d.ts +55 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +18 -2
  46. package/.markdownlint.json +0 -4
  47. package/biome.json +0 -43
  48. package/src/alpha/generateAlphaScale.ts +0 -43
  49. package/src/contrast/apca.ts +0 -7
  50. package/src/contrast/onSolid.ts +0 -38
  51. package/src/contrast/solveText.ts +0 -49
  52. package/src/createTheme.ts +0 -130
  53. package/src/data/radixSeeds.ts +0 -37
  54. package/src/diagnostics/analyzeScale.ts +0 -6
  55. package/src/diagnostics/analyzeTheme.ts +0 -54
  56. package/src/diagnostics/warnings.ts +0 -25
  57. package/src/engine/curves.ts +0 -64
  58. package/src/engine/oklch.ts +0 -53
  59. package/src/engine/templates.ts +0 -58
  60. package/src/exporters/selectColorMode.ts +0 -25
  61. package/src/exporters/toCssVars.ts +0 -116
  62. package/src/exporters/toJson.ts +0 -31
  63. package/src/exporters/toReactNative.ts +0 -39
  64. package/src/exporters/toTailwind.ts +0 -110
  65. package/src/exporters/toTs.ts +0 -34
  66. package/src/generateScale.ts +0 -163
  67. package/src/tokens/presetRadixLikeUi.ts +0 -75
  68. package/src/types.ts +0 -63
  69. package/tsconfig.json +0 -14
@@ -0,0 +1,5 @@
1
+ import type { AlphaScale, ColorHex } from "../types.js";
2
+ export declare function generateAlphaScale(base: ColorHex, _background: {
3
+ light: ColorHex;
4
+ dark: ColorHex;
5
+ }): AlphaScale;
@@ -0,0 +1,34 @@
1
+ import Color from "colorjs.io";
2
+ const steps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
3
+ const alphaCurve = {
4
+ 1: 0.05,
5
+ 2: 0.1,
6
+ 3: 0.15,
7
+ 4: 0.2,
8
+ 5: 0.3,
9
+ 6: 0.4,
10
+ 7: 0.5,
11
+ 8: 0.6,
12
+ 9: 0.7,
13
+ 10: 0.8,
14
+ 11: 0.9,
15
+ 12: 0.95,
16
+ };
17
+ function mixWithAlpha(foreground, alpha) {
18
+ const color = new Color(foreground).to("srgb");
19
+ const [r, g, b] = color.coords;
20
+ const hex = new Color({ space: "srgb", coords: [r, g, b], alpha }).toString({
21
+ format: "hex",
22
+ });
23
+ return hex;
24
+ }
25
+ export function generateAlphaScale(base, _background) {
26
+ const light = {};
27
+ const dark = {};
28
+ for (const step of steps) {
29
+ const alpha = alphaCurve[step];
30
+ light[step] = mixWithAlpha(base, alpha);
31
+ dark[step] = mixWithAlpha(base, alpha);
32
+ }
33
+ return { light, dark };
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { ColorHex } from "../types.js";
2
+ export declare function apcaContrast(foreground: ColorHex, background: ColorHex): number;
@@ -0,0 +1,5 @@
1
+ import { calcAPCA } from "apca-w3";
2
+ export function apcaContrast(foreground, background) {
3
+ const contrast = Number(calcAPCA(foreground, background));
4
+ return Number(contrast.toFixed(2));
5
+ }
@@ -0,0 +1,6 @@
1
+ import type { ColorHex } from "../types.js";
2
+ export declare function onSolidTextTokens(background: ColorHex): {
3
+ primary: ColorHex;
4
+ secondary: ColorHex;
5
+ disabled: ColorHex;
6
+ };
@@ -0,0 +1,28 @@
1
+ import { apcaContrast } from "./apca.js";
2
+ const white = "#ffffff";
3
+ const black = "#000000";
4
+ const alphaLevels = {
5
+ primary: 0.92,
6
+ secondary: 0.72,
7
+ disabled: 0.48,
8
+ };
9
+ function withAlpha(hex, alpha) {
10
+ const normalized = hex.replace("#", "");
11
+ const alphaHex = Math.round(alpha * 255)
12
+ .toString(16)
13
+ .padStart(2, "0");
14
+ return `#${normalized}${alphaHex}`;
15
+ }
16
+ function chooseTextColor(background) {
17
+ const whiteScore = Math.abs(apcaContrast(white, background));
18
+ const blackScore = Math.abs(apcaContrast(black, background));
19
+ return whiteScore >= blackScore ? white : black;
20
+ }
21
+ export function onSolidTextTokens(background) {
22
+ const base = chooseTextColor(background);
23
+ return {
24
+ primary: withAlpha(base, alphaLevels.primary),
25
+ secondary: withAlpha(base, alphaLevels.secondary),
26
+ disabled: withAlpha(base, alphaLevels.disabled),
27
+ };
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { ColorHex } from "../types.js";
2
+ export declare function adjustTextColor(foreground: ColorHex, background: ColorHex, target: number): ColorHex;
@@ -0,0 +1,31 @@
1
+ import Color from "colorjs.io";
2
+ import { compressToSrgb, oklchToHex } from "../engine/oklch.js";
3
+ import { apcaContrast } from "./apca.js";
4
+ function clamp(value, min, max) {
5
+ return Math.min(max, Math.max(min, value));
6
+ }
7
+ function adjustLightness(oklch, background, target, maxIterations = 24) {
8
+ const bg = new Color(background).to("oklch");
9
+ const bgL = bg.coords[0] ?? 0;
10
+ const direction = bgL > 0.5 ? -1 : 1;
11
+ let current = { ...oklch };
12
+ for (let i = 0; i < maxIterations; i += 1) {
13
+ const contrast = Math.abs(apcaContrast(oklchToHex(current), background));
14
+ if (contrast >= target) {
15
+ return current;
16
+ }
17
+ current = {
18
+ ...current,
19
+ l: clamp(current.l + direction * 0.02, 0, 1),
20
+ };
21
+ }
22
+ return current;
23
+ }
24
+ export function adjustTextColor(foreground, background, target) {
25
+ const fg = new Color(foreground).to("oklch");
26
+ const [l, c, h] = fg.coords;
27
+ let candidate = { l: l ?? 0, c: c ?? 0, h: h ?? 0 };
28
+ candidate = adjustLightness(candidate, background, target);
29
+ candidate = compressToSrgb(candidate);
30
+ return oklchToHex(candidate);
31
+ }
@@ -0,0 +1,32 @@
1
+ import type { ColorHex, ColorSource, Theme } from "./types.js";
2
+ export type TokenOverrides = {
3
+ light?: Record<string, ColorHex>;
4
+ dark?: Record<string, ColorHex>;
5
+ };
6
+ export type CreateThemeOptions = {
7
+ neutral: ColorSource;
8
+ accent: ColorSource;
9
+ semantic?: {
10
+ success?: ColorSource;
11
+ warning?: ColorSource;
12
+ danger?: ColorSource;
13
+ };
14
+ extras?: Record<string, ColorSource>;
15
+ tokens?: {
16
+ preset?: "radix-like-ui";
17
+ overrides?: TokenOverrides;
18
+ };
19
+ alpha?: {
20
+ enabled?: boolean;
21
+ background?: {
22
+ light?: ColorHex;
23
+ dark?: ColorHex;
24
+ };
25
+ };
26
+ contrast?: {
27
+ textPrimary?: number;
28
+ textSecondary?: number;
29
+ };
30
+ p3?: boolean;
31
+ };
32
+ export declare function createTheme(options: CreateThemeOptions): Theme;
@@ -0,0 +1,75 @@
1
+ import { generateAlphaScale } from "./alpha/generateAlphaScale.js";
2
+ import { onSolidTextTokens } from "./contrast/onSolid.js";
3
+ import { adjustTextColor } from "./contrast/solveText.js";
4
+ import { analyzeTheme } from "./diagnostics/analyzeTheme.js";
5
+ import { generateScale } from "./generateScale.js";
6
+ import { buildPresetTokens } from "./tokens/presetRadixLikeUi.js";
7
+ export function createTheme(options) {
8
+ const includeP3 = options.p3 ?? false;
9
+ const scales = {
10
+ neutral: generateScale({ source: options.neutral, p3: includeP3 }),
11
+ accent: generateScale({ source: options.accent, p3: includeP3 }),
12
+ };
13
+ if (options.semantic?.success) {
14
+ scales.success = generateScale({ source: options.semantic.success, p3: includeP3 });
15
+ }
16
+ if (options.semantic?.warning) {
17
+ scales.warning = generateScale({ source: options.semantic.warning, p3: includeP3 });
18
+ }
19
+ if (options.semantic?.danger) {
20
+ scales.danger = generateScale({ source: options.semantic.danger, p3: includeP3 });
21
+ }
22
+ if (options.extras) {
23
+ for (const [key, source] of Object.entries(options.extras)) {
24
+ scales[key] = generateScale({ source, p3: includeP3 });
25
+ }
26
+ }
27
+ const preset = options.tokens?.preset ?? "radix-like-ui";
28
+ const tokens = preset === "radix-like-ui" ? buildPresetTokens(scales) : { light: {}, dark: {} };
29
+ const lightBg = tokens.light["bg.app"];
30
+ const darkBg = tokens.dark["bg.app"];
31
+ const textPrimaryTarget = options.contrast?.textPrimary ?? 75;
32
+ const textSecondaryTarget = options.contrast?.textSecondary ?? 60;
33
+ if (lightBg && tokens.light["text.primary"]) {
34
+ tokens.light["text.primary"] = adjustTextColor(tokens.light["text.primary"], lightBg, textPrimaryTarget);
35
+ }
36
+ if (lightBg && tokens.light["text.secondary"]) {
37
+ tokens.light["text.secondary"] = adjustTextColor(tokens.light["text.secondary"], lightBg, textSecondaryTarget);
38
+ }
39
+ if (darkBg && tokens.dark["text.primary"]) {
40
+ tokens.dark["text.primary"] = adjustTextColor(tokens.dark["text.primary"], darkBg, textPrimaryTarget);
41
+ }
42
+ if (darkBg && tokens.dark["text.secondary"]) {
43
+ tokens.dark["text.secondary"] = adjustTextColor(tokens.dark["text.secondary"], darkBg, textSecondaryTarget);
44
+ }
45
+ const accentScale = scales.accent;
46
+ const lightOnSolid = onSolidTextTokens(accentScale.light[9]);
47
+ const darkOnSolid = onSolidTextTokens(accentScale.dark[9]);
48
+ tokens.light["onSolid.primary"] = lightOnSolid.primary;
49
+ tokens.light["onSolid.secondary"] = lightOnSolid.secondary;
50
+ tokens.light["onSolid.disabled"] = lightOnSolid.disabled;
51
+ tokens.dark["onSolid.primary"] = darkOnSolid.primary;
52
+ tokens.dark["onSolid.secondary"] = darkOnSolid.secondary;
53
+ tokens.dark["onSolid.disabled"] = darkOnSolid.disabled;
54
+ if (options.tokens?.overrides?.light) {
55
+ Object.assign(tokens.light, options.tokens.overrides.light);
56
+ }
57
+ if (options.tokens?.overrides?.dark) {
58
+ Object.assign(tokens.dark, options.tokens.overrides.dark);
59
+ }
60
+ let alpha;
61
+ if (options.alpha?.enabled !== false) {
62
+ const background = {
63
+ light: options.alpha?.background?.light ?? "#ffffff",
64
+ dark: options.alpha?.background?.dark ?? "#111111",
65
+ };
66
+ alpha = generateAlphaScale(accentScale.light[9], background);
67
+ }
68
+ const diagnostics = analyzeTheme({ scales, tokens, alpha });
69
+ return {
70
+ scales,
71
+ tokens,
72
+ alpha,
73
+ diagnostics,
74
+ };
75
+ }
@@ -0,0 +1,3 @@
1
+ import type { ColorHex, RadixSeedName } from "../types.js";
2
+ export declare const radixSeeds: Record<RadixSeedName, ColorHex>;
3
+ export declare const radixSeedNames: string[];
@@ -0,0 +1,34 @@
1
+ export const radixSeeds = {
2
+ amber: "#ffc53d",
3
+ blue: "#0090ff",
4
+ bronze: "#a18072",
5
+ brown: "#ad7f58",
6
+ crimson: "#e93d82",
7
+ cyan: "#00a2c7",
8
+ gold: "#978365",
9
+ grass: "#46a758",
10
+ gray: "#8d8d8d",
11
+ green: "#30a46c",
12
+ indigo: "#3e63dd",
13
+ iris: "#5b5bd6",
14
+ jade: "#29a383",
15
+ lime: "#bdee63",
16
+ mauve: "#8e8c99",
17
+ mint: "#86ead4",
18
+ olive: "#898e87",
19
+ orange: "#f76b15",
20
+ pink: "#d6409f",
21
+ plum: "#ab4aba",
22
+ purple: "#8e4ec6",
23
+ red: "#e5484d",
24
+ ruby: "#e54666",
25
+ sage: "#868e8b",
26
+ sand: "#8d8d86",
27
+ sky: "#7ce2fe",
28
+ slate: "#8b8d98",
29
+ teal: "#12a594",
30
+ tomato: "#e54d2e",
31
+ violet: "#6e56cf",
32
+ yellow: "#ffe629",
33
+ };
34
+ export const radixSeedNames = Object.keys(radixSeeds).sort();
@@ -0,0 +1,2 @@
1
+ import type { Scale, ScaleDiagnostics } from "../types.js";
2
+ export declare function analyzeScale(scale: Scale): ScaleDiagnostics;
@@ -0,0 +1,4 @@
1
+ export function analyzeScale(scale) {
2
+ const outOfGamutCount = scale.meta?.outOfGamutCount ?? 0;
3
+ return { outOfGamutCount };
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { Theme, ThemeDiagnostics } from "../types.js";
2
+ export declare function analyzeTheme(theme: Theme): ThemeDiagnostics;
@@ -0,0 +1,35 @@
1
+ import { apcaContrast } from "../contrast/apca.js";
2
+ import { analyzeWarnings } from "./warnings.js";
3
+ export function analyzeTheme(theme) {
4
+ const contrast = {};
5
+ const lightBg = theme.tokens.light["bg.app"];
6
+ const darkBg = theme.tokens.dark["bg.app"];
7
+ if (lightBg) {
8
+ if (theme.tokens.light["text.primary"]) {
9
+ contrast["light.text.primary"] = apcaContrast(theme.tokens.light["text.primary"], lightBg);
10
+ }
11
+ if (theme.tokens.light["text.secondary"]) {
12
+ contrast["light.text.secondary"] = apcaContrast(theme.tokens.light["text.secondary"], lightBg);
13
+ }
14
+ }
15
+ if (darkBg) {
16
+ if (theme.tokens.dark["text.primary"]) {
17
+ contrast["dark.text.primary"] = apcaContrast(theme.tokens.dark["text.primary"], darkBg);
18
+ }
19
+ if (theme.tokens.dark["text.secondary"]) {
20
+ contrast["dark.text.secondary"] = apcaContrast(theme.tokens.dark["text.secondary"], darkBg);
21
+ }
22
+ }
23
+ if (theme.tokens.light["onSolid.primary"] && theme.tokens.light["accent.solid"]) {
24
+ contrast["light.onSolid.primary"] = apcaContrast(theme.tokens.light["onSolid.primary"], theme.tokens.light["accent.solid"]);
25
+ }
26
+ if (theme.tokens.dark["onSolid.primary"] && theme.tokens.dark["accent.solid"]) {
27
+ contrast["dark.onSolid.primary"] = apcaContrast(theme.tokens.dark["onSolid.primary"], theme.tokens.dark["accent.solid"]);
28
+ }
29
+ let outOfGamutCount = 0;
30
+ for (const scale of Object.values(theme.scales)) {
31
+ outOfGamutCount += scale.meta?.outOfGamutCount ?? 0;
32
+ }
33
+ const warnings = analyzeWarnings(theme);
34
+ return { contrast, outOfGamutCount, warnings };
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { Theme } from "../types.js";
2
+ export declare function analyzeWarnings(theme: Theme): string[];
@@ -0,0 +1,20 @@
1
+ import Color from "colorjs.io";
2
+ export function analyzeWarnings(theme) {
3
+ const warnings = [];
4
+ const accent = theme.scales.accent?.light?.[9];
5
+ if (accent) {
6
+ const { coords } = new Color(accent).to("oklch");
7
+ const l = coords[0] ?? 0;
8
+ const c = coords[1] ?? 0;
9
+ if (c < 0.03) {
10
+ warnings.push("Accent seed has very low chroma; the palette may look gray.");
11
+ }
12
+ if (l < 0.2) {
13
+ warnings.push("Accent seed is very dark; light mode solids may lack contrast.");
14
+ }
15
+ if (l > 0.9) {
16
+ warnings.push("Accent seed is very light; dark mode solids may lack contrast.");
17
+ }
18
+ }
19
+ return warnings;
20
+ }
@@ -0,0 +1,9 @@
1
+ import type { Step } from "../types.js";
2
+ export type CurveConfig = {
3
+ lightness?: Partial<Record<Step, number>>;
4
+ chroma?: Partial<Record<Step, number>>;
5
+ };
6
+ export declare function resolveCurves(curves?: CurveConfig): {
7
+ lightness: Record<Step, number>;
8
+ chroma: Record<Step, number>;
9
+ };
@@ -0,0 +1,48 @@
1
+ const steps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
2
+ const defaultLightness = {
3
+ 1: 0.3,
4
+ 2: 0.3,
5
+ 3: 0.6,
6
+ 4: 0.6,
7
+ 5: 0.6,
8
+ 6: 0.85,
9
+ 7: 0.85,
10
+ 8: 0.85,
11
+ 9: 1,
12
+ 10: 1,
13
+ 11: 1,
14
+ 12: 1,
15
+ };
16
+ const defaultChroma = {
17
+ 1: 0.2,
18
+ 2: 0.2,
19
+ 3: 0.6,
20
+ 4: 0.6,
21
+ 5: 0.6,
22
+ 6: 0.8,
23
+ 7: 0.8,
24
+ 8: 0.8,
25
+ 9: 1,
26
+ 10: 1,
27
+ 11: 0.7,
28
+ 12: 0.7,
29
+ };
30
+ export function resolveCurves(curves) {
31
+ const lightness = { ...defaultLightness };
32
+ const chroma = { ...defaultChroma };
33
+ if (curves?.lightness) {
34
+ for (const step of steps) {
35
+ if (curves.lightness[step] !== undefined) {
36
+ lightness[step] = curves.lightness[step];
37
+ }
38
+ }
39
+ }
40
+ if (curves?.chroma) {
41
+ for (const step of steps) {
42
+ if (curves.chroma[step] !== undefined) {
43
+ chroma[step] = curves.chroma[step];
44
+ }
45
+ }
46
+ }
47
+ return { lightness, chroma };
48
+ }
@@ -0,0 +1,8 @@
1
+ import type { ColorHex, ColorP3, OklchColor } from "../types.js";
2
+ export declare function hexToOklch(hex: ColorHex): OklchColor;
3
+ export declare function oklchToHex(oklch: OklchColor): ColorHex;
4
+ export declare function inSrgbGamut(oklch: OklchColor): boolean;
5
+ export declare function compressToSrgb(oklch: OklchColor): OklchColor;
6
+ export declare function inP3Gamut(oklch: OklchColor): boolean;
7
+ export declare function compressToP3(oklch: OklchColor): OklchColor;
8
+ export declare function oklchToP3(oklch: OklchColor): ColorP3;
@@ -0,0 +1,40 @@
1
+ import Color from "colorjs.io";
2
+ export function hexToOklch(hex) {
3
+ const color = new Color(hex).to("oklch");
4
+ const [l, c, h] = color.coords;
5
+ return { l: l ?? 0, c: c ?? 0, h: h ?? 0 };
6
+ }
7
+ export function oklchToHex(oklch) {
8
+ const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
9
+ return color.to("srgb").toString({ format: "hex" });
10
+ }
11
+ export function inSrgbGamut(oklch) {
12
+ const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
13
+ return color.inGamut("srgb");
14
+ }
15
+ export function compressToSrgb(oklch) {
16
+ let current = { ...oklch };
17
+ let iterations = 0;
18
+ while (!inSrgbGamut(current) && current.c > 0 && iterations < 40) {
19
+ current = { ...current, c: current.c * 0.95 };
20
+ iterations += 1;
21
+ }
22
+ return current;
23
+ }
24
+ export function inP3Gamut(oklch) {
25
+ const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
26
+ return color.inGamut("p3");
27
+ }
28
+ export function compressToP3(oklch) {
29
+ let current = { ...oklch };
30
+ let iterations = 0;
31
+ while (!inP3Gamut(current) && current.c > 0 && iterations < 40) {
32
+ current = { ...current, c: current.c * 0.95 };
33
+ iterations += 1;
34
+ }
35
+ return current;
36
+ }
37
+ export function oklchToP3(oklch) {
38
+ const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
39
+ return color.to("p3").toString({ format: "color" });
40
+ }
@@ -0,0 +1,14 @@
1
+ import type { OklchColor, Step, TemplateId } from "../types.js";
2
+ export declare const templates: {
3
+ light: {
4
+ neutral: Record<Step, OklchColor>;
5
+ warm: Record<Step, OklchColor>;
6
+ cool: Record<Step, OklchColor>;
7
+ };
8
+ dark: {
9
+ neutral: Record<Step, OklchColor>;
10
+ warm: Record<Step, OklchColor>;
11
+ cool: Record<Step, OklchColor>;
12
+ };
13
+ };
14
+ export declare function selectTemplateId(oklch: OklchColor): TemplateId;
@@ -0,0 +1,45 @@
1
+ const steps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
2
+ const lightnessLight = [0.99, 0.975, 0.95, 0.92, 0.89, 0.84, 0.77, 0.68, 0.58, 0.5, 0.4, 0.3];
3
+ const lightnessDark = [0.12, 0.14, 0.18, 0.22, 0.26, 0.32, 0.4, 0.48, 0.58, 0.66, 0.76, 0.86];
4
+ const chromaNeutral = [
5
+ 0.01, 0.012, 0.016, 0.02, 0.024, 0.03, 0.04, 0.05, 0.055, 0.045, 0.035, 0.028,
6
+ ];
7
+ const chromaWarm = [0.02, 0.03, 0.05, 0.08, 0.12, 0.16, 0.18, 0.2, 0.22, 0.19, 0.12, 0.08];
8
+ const chromaCool = [0.02, 0.03, 0.05, 0.075, 0.1, 0.14, 0.17, 0.19, 0.21, 0.18, 0.11, 0.08];
9
+ const baseHue = {
10
+ neutral: 250,
11
+ warm: 40,
12
+ cool: 220,
13
+ };
14
+ function buildTemplate(lightness, chroma, hue) {
15
+ const output = {};
16
+ for (let i = 0; i < steps.length; i += 1) {
17
+ const step = steps[i];
18
+ output[step] = {
19
+ l: lightness[i],
20
+ c: chroma[i],
21
+ h: hue,
22
+ };
23
+ }
24
+ return output;
25
+ }
26
+ export const templates = {
27
+ light: {
28
+ neutral: buildTemplate(lightnessLight, chromaNeutral, baseHue.neutral),
29
+ warm: buildTemplate(lightnessLight, chromaWarm, baseHue.warm),
30
+ cool: buildTemplate(lightnessLight, chromaCool, baseHue.cool),
31
+ },
32
+ dark: {
33
+ neutral: buildTemplate(lightnessDark, chromaNeutral, baseHue.neutral),
34
+ warm: buildTemplate(lightnessDark, chromaWarm, baseHue.warm),
35
+ cool: buildTemplate(lightnessDark, chromaCool, baseHue.cool),
36
+ },
37
+ };
38
+ export function selectTemplateId(oklch) {
39
+ if (oklch.c < 0.05) {
40
+ return "neutral";
41
+ }
42
+ const hue = ((oklch.h % 360) + 360) % 360;
43
+ const isWarm = hue <= 60 || hue >= 330;
44
+ return isWarm ? "warm" : "cool";
45
+ }
@@ -0,0 +1,2 @@
1
+ import type { Theme, ThemeColorMode } from "../types.js";
2
+ export declare function selectThemeColorMode(theme: Theme, mode: "srgb" | "p3"): ThemeColorMode;
@@ -0,0 +1,19 @@
1
+ export function selectThemeColorMode(theme, mode) {
2
+ if (mode === "srgb") {
3
+ return theme;
4
+ }
5
+ const scales = Object.fromEntries(Object.entries(theme.scales).map(([slot, scale]) => {
6
+ if (!scale.p3) {
7
+ return [slot, scale];
8
+ }
9
+ return [
10
+ slot,
11
+ {
12
+ ...scale,
13
+ light: scale.p3.light,
14
+ dark: scale.p3.dark,
15
+ },
16
+ ];
17
+ }));
18
+ return { ...theme, scales };
19
+ }
@@ -0,0 +1,12 @@
1
+ import type { Theme } from "../types.js";
2
+ type CssVarsOptions = {
3
+ prefix?: string;
4
+ includeTokens?: boolean;
5
+ includeScales?: boolean;
6
+ includeAlpha?: boolean;
7
+ includeP3?: boolean;
8
+ lightSelector?: string;
9
+ darkSelector?: string;
10
+ };
11
+ export declare function toCssVars(theme: Theme, options?: CssVarsOptions): string;
12
+ export {};
@@ -0,0 +1,84 @@
1
+ import Color from "colorjs.io";
2
+ function tokenToCssVar(token, prefix) {
3
+ const normalized = token.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
4
+ const dashed = normalized.replace(/\./g, "-");
5
+ return `--${prefix}-${dashed}`;
6
+ }
7
+ function scaleToCssVar(slot, step, prefix) {
8
+ return `--${prefix}-scale-${slot}-${step}`;
9
+ }
10
+ function alphaToCssVar(step, prefix) {
11
+ return `--${prefix}-alpha-${step}`;
12
+ }
13
+ function hexToP3(value) {
14
+ return new Color(value).to("p3").toString({ format: "color" });
15
+ }
16
+ export function toCssVars(theme, options) {
17
+ const prefix = options?.prefix ?? "pk";
18
+ const includeTokens = options?.includeTokens ?? true;
19
+ const includeScales = options?.includeScales ?? true;
20
+ const includeAlpha = options?.includeAlpha ?? true;
21
+ const includeP3 = options?.includeP3 ?? false;
22
+ const lightSelector = options?.lightSelector ?? ":root";
23
+ const darkSelector = options?.darkSelector ?? ".dark";
24
+ const lines = [];
25
+ const hasP3 = Object.values(theme.scales).some((scale) => scale.p3);
26
+ function emitSelector(selector, mode) {
27
+ lines.push(`${selector} {`);
28
+ if (includeTokens) {
29
+ for (const [token, value] of Object.entries(theme.tokens[mode])) {
30
+ lines.push(` ${tokenToCssVar(token, prefix)}: ${value};`);
31
+ }
32
+ }
33
+ if (includeScales) {
34
+ for (const [slot, scale] of Object.entries(theme.scales)) {
35
+ for (const [step, value] of Object.entries(scale[mode])) {
36
+ lines.push(` ${scaleToCssVar(slot, Number(step), prefix)}: ${value};`);
37
+ }
38
+ }
39
+ }
40
+ if (includeAlpha && theme.alpha) {
41
+ for (const [step, value] of Object.entries(theme.alpha[mode])) {
42
+ lines.push(` ${alphaToCssVar(Number(step), prefix)}: ${value};`);
43
+ }
44
+ }
45
+ lines.push("}");
46
+ }
47
+ emitSelector(lightSelector, "light");
48
+ lines.push("");
49
+ emitSelector(darkSelector, "dark");
50
+ if (includeP3 && hasP3) {
51
+ lines.push("");
52
+ lines.push("@supports (color: color(display-p3 1 1 1)) {");
53
+ function emitP3Selector(selector, mode) {
54
+ lines.push(` ${selector} {`);
55
+ if (includeTokens) {
56
+ for (const [token, value] of Object.entries(theme.tokens[mode])) {
57
+ lines.push(` ${tokenToCssVar(token, prefix)}: ${hexToP3(value)};`);
58
+ }
59
+ }
60
+ if (includeScales) {
61
+ for (const [slot, scale] of Object.entries(theme.scales)) {
62
+ const p3Scale = scale.p3?.[mode];
63
+ if (!p3Scale) {
64
+ continue;
65
+ }
66
+ for (const [step, value] of Object.entries(p3Scale)) {
67
+ lines.push(` ${scaleToCssVar(slot, Number(step), prefix)}: ${value};`);
68
+ }
69
+ }
70
+ }
71
+ if (includeAlpha && theme.alpha) {
72
+ for (const [step, value] of Object.entries(theme.alpha[mode])) {
73
+ lines.push(` ${alphaToCssVar(Number(step), prefix)}: ${hexToP3(value)};`);
74
+ }
75
+ }
76
+ lines.push(" }");
77
+ }
78
+ emitP3Selector(lightSelector, "light");
79
+ lines.push("");
80
+ emitP3Selector(darkSelector, "dark");
81
+ lines.push("}");
82
+ }
83
+ return lines.join("\n");
84
+ }
@@ -0,0 +1,3 @@
1
+ import type { Theme } from "../types.js";
2
+ export declare function toJson(theme: Theme): string;
3
+ export declare function toJsonWithMode(theme: Theme, mode: "srgb" | "p3"): string;