@clhaas/palette-kit 0.1.0 → 0.1.2

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 (70) hide show
  1. package/README.md +4 -3
  2. package/dist/alpha/generateAlphaScale.d.ts +5 -0
  3. package/dist/alpha/generateAlphaScale.js +34 -0
  4. package/dist/contrast/apca.d.ts +2 -0
  5. package/dist/contrast/apca.js +5 -0
  6. package/dist/contrast/onSolid.d.ts +6 -0
  7. package/dist/contrast/onSolid.js +28 -0
  8. package/dist/contrast/solveText.d.ts +2 -0
  9. package/dist/contrast/solveText.js +31 -0
  10. package/dist/createTheme.d.ts +34 -0
  11. package/dist/createTheme.js +88 -0
  12. package/dist/data/radixSeeds.d.ts +3 -0
  13. package/dist/data/radixSeeds.js +34 -0
  14. package/dist/diagnostics/analyzeScale.d.ts +2 -0
  15. package/dist/diagnostics/analyzeScale.js +7 -0
  16. package/dist/diagnostics/analyzeTheme.d.ts +2 -0
  17. package/dist/diagnostics/analyzeTheme.js +35 -0
  18. package/dist/diagnostics/warnings.d.ts +2 -0
  19. package/dist/diagnostics/warnings.js +20 -0
  20. package/dist/engine/curves.d.ts +9 -0
  21. package/dist/engine/curves.js +48 -0
  22. package/dist/engine/oklch.d.ts +8 -0
  23. package/dist/engine/oklch.js +40 -0
  24. package/dist/engine/templates.d.ts +14 -0
  25. package/dist/engine/templates.js +45 -0
  26. package/dist/exporters/selectColorMode.d.ts +2 -0
  27. package/dist/exporters/selectColorMode.js +19 -0
  28. package/dist/exporters/toCssVars.d.ts +12 -0
  29. package/dist/exporters/toCssVars.js +84 -0
  30. package/dist/exporters/toJson.d.ts +3 -0
  31. package/dist/exporters/toJson.js +25 -0
  32. package/dist/exporters/toReactNative.d.ts +30 -0
  33. package/dist/exporters/toReactNative.js +26 -0
  34. package/dist/exporters/toTailwind.d.ts +16 -0
  35. package/dist/exporters/toTailwind.js +90 -0
  36. package/dist/exporters/toTs.d.ts +3 -0
  37. package/dist/exporters/toTs.js +28 -0
  38. package/dist/generateScale.d.ts +48 -0
  39. package/dist/generateScale.js +274 -0
  40. package/dist/index.d.ts +17 -0
  41. package/{src/index.ts → dist/index.js} +0 -15
  42. package/dist/tokens/presetRadixLikeUi.d.ts +5 -0
  43. package/dist/tokens/presetRadixLikeUi.js +55 -0
  44. package/dist/types.d.ts +59 -0
  45. package/dist/types.js +1 -0
  46. package/package.json +19 -3
  47. package/.markdownlint.json +0 -4
  48. package/biome.json +0 -43
  49. package/src/alpha/generateAlphaScale.ts +0 -43
  50. package/src/contrast/apca.ts +0 -7
  51. package/src/contrast/onSolid.ts +0 -38
  52. package/src/contrast/solveText.ts +0 -49
  53. package/src/createTheme.ts +0 -130
  54. package/src/data/radixSeeds.ts +0 -37
  55. package/src/diagnostics/analyzeScale.ts +0 -6
  56. package/src/diagnostics/analyzeTheme.ts +0 -54
  57. package/src/diagnostics/warnings.ts +0 -25
  58. package/src/engine/curves.ts +0 -64
  59. package/src/engine/oklch.ts +0 -53
  60. package/src/engine/templates.ts +0 -58
  61. package/src/exporters/selectColorMode.ts +0 -25
  62. package/src/exporters/toCssVars.ts +0 -116
  63. package/src/exporters/toJson.ts +0 -31
  64. package/src/exporters/toReactNative.ts +0 -39
  65. package/src/exporters/toTailwind.ts +0 -110
  66. package/src/exporters/toTs.ts +0 -34
  67. package/src/generateScale.ts +0 -163
  68. package/src/tokens/presetRadixLikeUi.ts +0 -75
  69. package/src/types.ts +0 -63
  70. package/tsconfig.json +0 -14
package/README.md CHANGED
@@ -23,10 +23,11 @@ pnpm add @clhaas/palette-kit
23
23
  - 12-step scale (light/dark) from a seed.
24
24
  - Semantic tokens for UI (`radix-like-ui` preset).
25
25
  - Alpha scale for overlays.
26
- - Exporters for TS, JSON, and CSS vars.
26
+ - Exporters for TS, JSON, CSS vars, Tailwind, and React Native.
27
+ - Auto anchor selection per mode (light/dark), overridable via `anchorStep`.
27
28
  - Basic contrast and gamut diagnostics.
28
29
 
29
- ## Usage example (planned API)
30
+ ## Usage example
30
31
 
31
32
  ```ts
32
33
  import { createTheme } from "@clhaas/palette-kit";
@@ -40,7 +41,7 @@ const theme = createTheme({
40
41
  danger: { source: "seed", value: "#ef4444" },
41
42
  },
42
43
  tokens: { preset: "radix-like-ui" },
43
- output: { format: "css", cssVarPrefix: "pk" },
44
+ p3: true,
44
45
  });
45
46
  ```
46
47
 
@@ -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,34 @@
1
+ import type { GenerateScaleOptions } from "./generateScale.js";
2
+ import type { ColorHex, ColorSource, Theme } from "./types.js";
3
+ export type TokenOverrides = {
4
+ light?: Record<string, ColorHex>;
5
+ dark?: Record<string, ColorHex>;
6
+ };
7
+ export type CreateThemeOptions = {
8
+ neutral: ColorSource;
9
+ accent: ColorSource;
10
+ semantic?: {
11
+ success?: ColorSource;
12
+ warning?: ColorSource;
13
+ danger?: ColorSource;
14
+ };
15
+ extras?: Record<string, ColorSource>;
16
+ tokens?: {
17
+ preset?: "radix-like-ui";
18
+ overrides?: TokenOverrides;
19
+ };
20
+ alpha?: {
21
+ enabled?: boolean;
22
+ background?: {
23
+ light?: ColorHex;
24
+ dark?: ColorHex;
25
+ };
26
+ };
27
+ contrast?: {
28
+ textPrimary?: number;
29
+ textSecondary?: number;
30
+ };
31
+ scale?: Omit<GenerateScaleOptions, "source" | "mode" | "p3">;
32
+ p3?: boolean;
33
+ };
34
+ export declare function createTheme(options: CreateThemeOptions): Theme;
@@ -0,0 +1,88 @@
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 scaleOptions = options.scale ?? {};
10
+ const scales = {
11
+ neutral: generateScale({ source: options.neutral, ...scaleOptions, p3: includeP3 }),
12
+ accent: generateScale({ source: options.accent, ...scaleOptions, p3: includeP3 }),
13
+ };
14
+ if (options.semantic?.success) {
15
+ scales.success = generateScale({
16
+ source: options.semantic.success,
17
+ ...scaleOptions,
18
+ p3: includeP3,
19
+ });
20
+ }
21
+ if (options.semantic?.warning) {
22
+ scales.warning = generateScale({
23
+ source: options.semantic.warning,
24
+ ...scaleOptions,
25
+ p3: includeP3,
26
+ });
27
+ }
28
+ if (options.semantic?.danger) {
29
+ scales.danger = generateScale({
30
+ source: options.semantic.danger,
31
+ ...scaleOptions,
32
+ p3: includeP3,
33
+ });
34
+ }
35
+ if (options.extras) {
36
+ for (const [key, source] of Object.entries(options.extras)) {
37
+ scales[key] = generateScale({ source, ...scaleOptions, p3: includeP3 });
38
+ }
39
+ }
40
+ const preset = options.tokens?.preset ?? "radix-like-ui";
41
+ const tokens = preset === "radix-like-ui" ? buildPresetTokens(scales) : { light: {}, dark: {} };
42
+ const lightBg = tokens.light["bg.app"];
43
+ const darkBg = tokens.dark["bg.app"];
44
+ const textPrimaryTarget = options.contrast?.textPrimary ?? 75;
45
+ const textSecondaryTarget = options.contrast?.textSecondary ?? 60;
46
+ if (lightBg && tokens.light["text.primary"]) {
47
+ tokens.light["text.primary"] = adjustTextColor(tokens.light["text.primary"], lightBg, textPrimaryTarget);
48
+ }
49
+ if (lightBg && tokens.light["text.secondary"]) {
50
+ tokens.light["text.secondary"] = adjustTextColor(tokens.light["text.secondary"], lightBg, textSecondaryTarget);
51
+ }
52
+ if (darkBg && tokens.dark["text.primary"]) {
53
+ tokens.dark["text.primary"] = adjustTextColor(tokens.dark["text.primary"], darkBg, textPrimaryTarget);
54
+ }
55
+ if (darkBg && tokens.dark["text.secondary"]) {
56
+ tokens.dark["text.secondary"] = adjustTextColor(tokens.dark["text.secondary"], darkBg, textSecondaryTarget);
57
+ }
58
+ const accentScale = scales.accent;
59
+ const lightOnSolid = onSolidTextTokens(accentScale.light[9]);
60
+ const darkOnSolid = onSolidTextTokens(accentScale.dark[9]);
61
+ tokens.light["onSolid.primary"] = lightOnSolid.primary;
62
+ tokens.light["onSolid.secondary"] = lightOnSolid.secondary;
63
+ tokens.light["onSolid.disabled"] = lightOnSolid.disabled;
64
+ tokens.dark["onSolid.primary"] = darkOnSolid.primary;
65
+ tokens.dark["onSolid.secondary"] = darkOnSolid.secondary;
66
+ tokens.dark["onSolid.disabled"] = darkOnSolid.disabled;
67
+ if (options.tokens?.overrides?.light) {
68
+ Object.assign(tokens.light, options.tokens.overrides.light);
69
+ }
70
+ if (options.tokens?.overrides?.dark) {
71
+ Object.assign(tokens.dark, options.tokens.overrides.dark);
72
+ }
73
+ let alpha;
74
+ if (options.alpha?.enabled !== false) {
75
+ const background = {
76
+ light: options.alpha?.background?.light ?? "#ffffff",
77
+ dark: options.alpha?.background?.dark ?? "#111111",
78
+ };
79
+ alpha = generateAlphaScale(accentScale.light[9], background);
80
+ }
81
+ const diagnostics = analyzeTheme({ scales, tokens, alpha });
82
+ return {
83
+ scales,
84
+ tokens,
85
+ alpha,
86
+ diagnostics,
87
+ };
88
+ }
@@ -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,7 @@
1
+ export function analyzeScale(scale) {
2
+ return {
3
+ outOfGamutCount: scale.meta?.outOfGamutCount ?? 0,
4
+ outOfP3GamutCount: scale.meta?.outOfP3GamutCount ?? 0,
5
+ anchorSteps: scale.meta?.anchorSteps,
6
+ };
7
+ }
@@ -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 {};