@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.
- package/README.md +4 -3
- package/dist/alpha/generateAlphaScale.d.ts +5 -0
- package/dist/alpha/generateAlphaScale.js +34 -0
- package/dist/contrast/apca.d.ts +2 -0
- package/dist/contrast/apca.js +5 -0
- package/dist/contrast/onSolid.d.ts +6 -0
- package/dist/contrast/onSolid.js +28 -0
- package/dist/contrast/solveText.d.ts +2 -0
- package/dist/contrast/solveText.js +31 -0
- package/dist/createTheme.d.ts +34 -0
- package/dist/createTheme.js +88 -0
- package/dist/data/radixSeeds.d.ts +3 -0
- package/dist/data/radixSeeds.js +34 -0
- package/dist/diagnostics/analyzeScale.d.ts +2 -0
- package/dist/diagnostics/analyzeScale.js +7 -0
- package/dist/diagnostics/analyzeTheme.d.ts +2 -0
- package/dist/diagnostics/analyzeTheme.js +35 -0
- package/dist/diagnostics/warnings.d.ts +2 -0
- package/dist/diagnostics/warnings.js +20 -0
- package/dist/engine/curves.d.ts +9 -0
- package/dist/engine/curves.js +48 -0
- package/dist/engine/oklch.d.ts +8 -0
- package/dist/engine/oklch.js +40 -0
- package/dist/engine/templates.d.ts +14 -0
- package/dist/engine/templates.js +45 -0
- package/dist/exporters/selectColorMode.d.ts +2 -0
- package/dist/exporters/selectColorMode.js +19 -0
- package/dist/exporters/toCssVars.d.ts +12 -0
- package/dist/exporters/toCssVars.js +84 -0
- package/dist/exporters/toJson.d.ts +3 -0
- package/dist/exporters/toJson.js +25 -0
- package/dist/exporters/toReactNative.d.ts +30 -0
- package/dist/exporters/toReactNative.js +26 -0
- package/dist/exporters/toTailwind.d.ts +16 -0
- package/dist/exporters/toTailwind.js +90 -0
- package/dist/exporters/toTs.d.ts +3 -0
- package/dist/exporters/toTs.js +28 -0
- package/dist/generateScale.d.ts +48 -0
- package/dist/generateScale.js +274 -0
- package/dist/index.d.ts +17 -0
- package/{src/index.ts → dist/index.js} +0 -15
- package/dist/tokens/presetRadixLikeUi.d.ts +5 -0
- package/dist/tokens/presetRadixLikeUi.js +55 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/package.json +19 -3
- package/.markdownlint.json +0 -4
- package/biome.json +0 -43
- package/src/alpha/generateAlphaScale.ts +0 -43
- package/src/contrast/apca.ts +0 -7
- package/src/contrast/onSolid.ts +0 -38
- package/src/contrast/solveText.ts +0 -49
- package/src/createTheme.ts +0 -130
- package/src/data/radixSeeds.ts +0 -37
- package/src/diagnostics/analyzeScale.ts +0 -6
- package/src/diagnostics/analyzeTheme.ts +0 -54
- package/src/diagnostics/warnings.ts +0 -25
- package/src/engine/curves.ts +0 -64
- package/src/engine/oklch.ts +0 -53
- package/src/engine/templates.ts +0 -58
- package/src/exporters/selectColorMode.ts +0 -25
- package/src/exporters/toCssVars.ts +0 -116
- package/src/exporters/toJson.ts +0 -31
- package/src/exporters/toReactNative.ts +0 -39
- package/src/exporters/toTailwind.ts +0 -110
- package/src/exporters/toTs.ts +0 -34
- package/src/generateScale.ts +0 -163
- package/src/tokens/presetRadixLikeUi.ts +0 -75
- package/src/types.ts +0 -63
- 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
|
|
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
|
|
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
|
-
|
|
44
|
+
p3: true,
|
|
44
45
|
});
|
|
45
46
|
```
|
|
46
47
|
|
|
@@ -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,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,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,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,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,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,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 {};
|