@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
|
@@ -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,25 @@
|
|
|
1
|
+
export function toJson(theme) {
|
|
2
|
+
return JSON.stringify(theme, null, 2);
|
|
3
|
+
}
|
|
4
|
+
export function toJsonWithMode(theme, mode) {
|
|
5
|
+
if (mode === "p3") {
|
|
6
|
+
return JSON.stringify(toP3Theme(theme), null, 2);
|
|
7
|
+
}
|
|
8
|
+
return JSON.stringify(theme, null, 2);
|
|
9
|
+
}
|
|
10
|
+
function toP3Theme(theme) {
|
|
11
|
+
const scales = Object.fromEntries(Object.entries(theme.scales).map(([slot, scale]) => {
|
|
12
|
+
if (!scale.p3) {
|
|
13
|
+
return [slot, scale];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
slot,
|
|
17
|
+
{
|
|
18
|
+
...scale,
|
|
19
|
+
light: scale.p3.light,
|
|
20
|
+
dark: scale.p3.dark,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}));
|
|
24
|
+
return { ...theme, scales };
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Theme } from "../types.js";
|
|
2
|
+
type ReactNativeOptions = {
|
|
3
|
+
includeTokens?: boolean;
|
|
4
|
+
includeScales?: boolean;
|
|
5
|
+
includeAlpha?: boolean;
|
|
6
|
+
includeP3?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function toReactNative(theme: Theme, options?: ReactNativeOptions): {
|
|
9
|
+
light: {
|
|
10
|
+
tokens: Record<string, `#${string}`>;
|
|
11
|
+
scales: {
|
|
12
|
+
[k: string]: Record<import("../types.js").Step, `#${string}`>;
|
|
13
|
+
};
|
|
14
|
+
alpha: Record<import("../types.js").Step, `#${string}`> | undefined;
|
|
15
|
+
p3: {
|
|
16
|
+
[k: string]: Record<import("../types.js").Step, string> | undefined;
|
|
17
|
+
} | undefined;
|
|
18
|
+
};
|
|
19
|
+
dark: {
|
|
20
|
+
tokens: Record<string, `#${string}`>;
|
|
21
|
+
scales: {
|
|
22
|
+
[k: string]: Record<import("../types.js").Step, `#${string}`>;
|
|
23
|
+
};
|
|
24
|
+
alpha: Record<import("../types.js").Step, `#${string}`> | undefined;
|
|
25
|
+
p3: {
|
|
26
|
+
[k: string]: Record<import("../types.js").Step, string> | undefined;
|
|
27
|
+
} | undefined;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function toReactNative(theme, options) {
|
|
2
|
+
const includeTokens = options?.includeTokens ?? true;
|
|
3
|
+
const includeScales = options?.includeScales ?? true;
|
|
4
|
+
const includeAlpha = options?.includeAlpha ?? true;
|
|
5
|
+
const includeP3 = options?.includeP3 ?? false;
|
|
6
|
+
function buildMode(mode) {
|
|
7
|
+
const scales = includeScales
|
|
8
|
+
? Object.fromEntries(Object.entries(theme.scales).map(([slot, scale]) => [slot, scale[mode]]))
|
|
9
|
+
: {};
|
|
10
|
+
const p3 = includeP3
|
|
11
|
+
? Object.fromEntries(Object.entries(theme.scales)
|
|
12
|
+
.filter(([, scale]) => scale.p3?.[mode])
|
|
13
|
+
.map(([slot, scale]) => [slot, scale.p3?.[mode]]))
|
|
14
|
+
: undefined;
|
|
15
|
+
return {
|
|
16
|
+
tokens: includeTokens ? theme.tokens[mode] : {},
|
|
17
|
+
scales,
|
|
18
|
+
alpha: includeAlpha ? theme.alpha?.[mode] : undefined,
|
|
19
|
+
p3,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
light: buildMode("light"),
|
|
24
|
+
dark: buildMode("dark"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Theme } from "../types.js";
|
|
2
|
+
type TailwindOptions = {
|
|
3
|
+
mode?: "light" | "dark" | "both";
|
|
4
|
+
includeTokens?: boolean;
|
|
5
|
+
includeScales?: boolean;
|
|
6
|
+
includeAlpha?: boolean;
|
|
7
|
+
includeP3?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function toTailwind(theme: Theme, options?: TailwindOptions): {
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
colors: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function setNested(target, path, value) {
|
|
2
|
+
let cursor = target;
|
|
3
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
4
|
+
const key = path[i];
|
|
5
|
+
if (!cursor[key] || typeof cursor[key] !== "object") {
|
|
6
|
+
cursor[key] = {};
|
|
7
|
+
}
|
|
8
|
+
cursor = cursor[key];
|
|
9
|
+
}
|
|
10
|
+
cursor[path[path.length - 1]] = value;
|
|
11
|
+
}
|
|
12
|
+
function tokensToNested(tokens) {
|
|
13
|
+
const output = {};
|
|
14
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
15
|
+
setNested(output, token.split("."), value);
|
|
16
|
+
}
|
|
17
|
+
return output;
|
|
18
|
+
}
|
|
19
|
+
function scalesToNested(scales, mode, useP3) {
|
|
20
|
+
const output = {};
|
|
21
|
+
for (const [slot, scale] of Object.entries(scales)) {
|
|
22
|
+
const source = useP3 ? scale.p3?.[mode] : scale[mode];
|
|
23
|
+
if (!source) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const stepMap = {};
|
|
27
|
+
for (const [step, value] of Object.entries(source)) {
|
|
28
|
+
stepMap[String(step)] = value;
|
|
29
|
+
}
|
|
30
|
+
output[slot] = stepMap;
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
function alphaToNested(alpha, mode) {
|
|
35
|
+
if (!alpha) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const output = {};
|
|
39
|
+
for (const [step, value] of Object.entries(alpha[mode])) {
|
|
40
|
+
output[String(step)] = value;
|
|
41
|
+
}
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
export function toTailwind(theme, options) {
|
|
45
|
+
const mode = options?.mode ?? "both";
|
|
46
|
+
const includeTokens = options?.includeTokens ?? true;
|
|
47
|
+
const includeScales = options?.includeScales ?? false;
|
|
48
|
+
const includeAlpha = options?.includeAlpha ?? false;
|
|
49
|
+
const includeP3 = options?.includeP3 ?? false;
|
|
50
|
+
function buildModeTokens(modeKey, useP3) {
|
|
51
|
+
const colors = {};
|
|
52
|
+
if (includeTokens) {
|
|
53
|
+
colors.tokens = tokensToNested(theme.tokens[modeKey]);
|
|
54
|
+
}
|
|
55
|
+
if (includeScales) {
|
|
56
|
+
colors.scale = scalesToNested(theme.scales, modeKey, useP3);
|
|
57
|
+
}
|
|
58
|
+
if (includeAlpha && theme.alpha) {
|
|
59
|
+
colors.alpha = alphaToNested(theme.alpha, modeKey);
|
|
60
|
+
}
|
|
61
|
+
return colors;
|
|
62
|
+
}
|
|
63
|
+
const colors = {};
|
|
64
|
+
if (mode === "light" || mode === "both") {
|
|
65
|
+
colors.light = buildModeTokens("light");
|
|
66
|
+
}
|
|
67
|
+
if (mode === "dark" || mode === "both") {
|
|
68
|
+
colors.dark = buildModeTokens("dark");
|
|
69
|
+
}
|
|
70
|
+
if (includeP3) {
|
|
71
|
+
const hasP3 = Object.values(theme.scales).some((scale) => scale.p3);
|
|
72
|
+
if (hasP3) {
|
|
73
|
+
const p3 = {};
|
|
74
|
+
if (mode === "light" || mode === "both") {
|
|
75
|
+
p3.light = buildModeTokens("light", true);
|
|
76
|
+
}
|
|
77
|
+
if (mode === "dark" || mode === "both") {
|
|
78
|
+
p3.dark = buildModeTokens("dark", true);
|
|
79
|
+
}
|
|
80
|
+
colors.p3 = p3;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
theme: {
|
|
85
|
+
extend: {
|
|
86
|
+
colors,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function toTs(theme) {
|
|
2
|
+
const serialized = JSON.stringify(theme, null, 2);
|
|
3
|
+
return `export const theme = ${serialized} as const;\n`;
|
|
4
|
+
}
|
|
5
|
+
export function toTsWithMode(theme, mode) {
|
|
6
|
+
if (mode === "p3") {
|
|
7
|
+
const serialized = JSON.stringify(toP3Theme(theme), null, 2);
|
|
8
|
+
return `export const theme = ${serialized} as const;\n`;
|
|
9
|
+
}
|
|
10
|
+
const serialized = JSON.stringify(theme, null, 2);
|
|
11
|
+
return `export const theme = ${serialized} as const;\n`;
|
|
12
|
+
}
|
|
13
|
+
function toP3Theme(theme) {
|
|
14
|
+
const scales = Object.fromEntries(Object.entries(theme.scales).map(([slot, scale]) => {
|
|
15
|
+
if (!scale.p3) {
|
|
16
|
+
return [slot, scale];
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
slot,
|
|
20
|
+
{
|
|
21
|
+
...scale,
|
|
22
|
+
light: scale.p3.light,
|
|
23
|
+
dark: scale.p3.dark,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
}));
|
|
27
|
+
return { ...theme, scales };
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CurveConfig } from "./engine/curves.js";
|
|
2
|
+
import type { ColorSource, Scale, Step, TemplateId } from "./types.js";
|
|
3
|
+
export type AnchorStepOption = Step | "auto" | {
|
|
4
|
+
light?: Step | "auto";
|
|
5
|
+
dark?: Step | "auto";
|
|
6
|
+
};
|
|
7
|
+
export type AutoAnchorModeOptions = {
|
|
8
|
+
candidateSteps?: Step[];
|
|
9
|
+
backgroundStep?: Step;
|
|
10
|
+
backgroundSteps?: Step[];
|
|
11
|
+
solidStep?: Step;
|
|
12
|
+
textStep?: Step;
|
|
13
|
+
targetContrast?: number;
|
|
14
|
+
minBackgroundL?: number;
|
|
15
|
+
maxBackgroundL?: number;
|
|
16
|
+
minTextL?: number;
|
|
17
|
+
maxTextL?: number;
|
|
18
|
+
};
|
|
19
|
+
export type AutoAnchorOptions = {
|
|
20
|
+
candidateSteps?: Step[];
|
|
21
|
+
light?: AutoAnchorModeOptions;
|
|
22
|
+
dark?: AutoAnchorModeOptions;
|
|
23
|
+
};
|
|
24
|
+
export type SeedNormalizeRange = {
|
|
25
|
+
minL?: number;
|
|
26
|
+
maxL?: number;
|
|
27
|
+
minC?: number;
|
|
28
|
+
maxC?: number;
|
|
29
|
+
};
|
|
30
|
+
export type SeedNormalizeOptions = {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
light?: SeedNormalizeRange;
|
|
33
|
+
dark?: SeedNormalizeRange;
|
|
34
|
+
};
|
|
35
|
+
export type GenerateScaleOptions = {
|
|
36
|
+
source: ColorSource;
|
|
37
|
+
mode?: "light" | "dark" | "both";
|
|
38
|
+
anchorStep?: AnchorStepOption;
|
|
39
|
+
autoAnchor?: AutoAnchorOptions;
|
|
40
|
+
seedNormalize?: SeedNormalizeOptions;
|
|
41
|
+
template?: "auto" | TemplateId;
|
|
42
|
+
curves?: CurveConfig;
|
|
43
|
+
gamut?: {
|
|
44
|
+
strategy: "compress" | "clip";
|
|
45
|
+
};
|
|
46
|
+
p3?: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare function generateScale(options: GenerateScaleOptions): Scale;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { apcaContrast } from "./contrast/apca.js";
|
|
2
|
+
import { onSolidTextTokens } from "./contrast/onSolid.js";
|
|
3
|
+
import { radixSeeds } from "./data/radixSeeds.js";
|
|
4
|
+
import { resolveCurves } from "./engine/curves.js";
|
|
5
|
+
import { compressToP3, compressToSrgb, hexToOklch, inP3Gamut, inSrgbGamut, oklchToHex, oklchToP3, } from "./engine/oklch.js";
|
|
6
|
+
import { selectTemplateId, templates } from "./engine/templates.js";
|
|
7
|
+
const steps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
8
|
+
const defaultAutoAnchor = {
|
|
9
|
+
candidateSteps: [8, 9, 10],
|
|
10
|
+
light: {
|
|
11
|
+
backgroundStep: 1,
|
|
12
|
+
backgroundSteps: [1, 2, 3],
|
|
13
|
+
solidStep: 9,
|
|
14
|
+
textStep: 12,
|
|
15
|
+
targetContrast: 90,
|
|
16
|
+
minBackgroundL: 0.86,
|
|
17
|
+
maxBackgroundL: 0.98,
|
|
18
|
+
minTextL: 0.22,
|
|
19
|
+
maxTextL: 0.55,
|
|
20
|
+
},
|
|
21
|
+
dark: {
|
|
22
|
+
backgroundStep: 1,
|
|
23
|
+
backgroundSteps: [1, 2, 3],
|
|
24
|
+
solidStep: 9,
|
|
25
|
+
textStep: 12,
|
|
26
|
+
targetContrast: 75,
|
|
27
|
+
minBackgroundL: 0.1,
|
|
28
|
+
maxBackgroundL: 0.32,
|
|
29
|
+
minTextL: 0.75,
|
|
30
|
+
maxTextL: 0.98,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const defaultSeedNormalize = {
|
|
34
|
+
light: {
|
|
35
|
+
minL: 0.35,
|
|
36
|
+
maxL: 0.9,
|
|
37
|
+
},
|
|
38
|
+
dark: {
|
|
39
|
+
minL: 0.32,
|
|
40
|
+
maxL: 0.82,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
function getSeedHex(source) {
|
|
44
|
+
if (source.source === "seed") {
|
|
45
|
+
return source.value;
|
|
46
|
+
}
|
|
47
|
+
const seed = radixSeeds[source.name];
|
|
48
|
+
if (!seed) {
|
|
49
|
+
throw new Error(`Unknown Radix seed: ${source.name}`);
|
|
50
|
+
}
|
|
51
|
+
return seed;
|
|
52
|
+
}
|
|
53
|
+
function normalizeHue(hue) {
|
|
54
|
+
const normalized = ((hue % 360) + 360) % 360;
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
function clampValue(value, min, max) {
|
|
58
|
+
let current = value;
|
|
59
|
+
if (min !== undefined) {
|
|
60
|
+
current = Math.max(min, current);
|
|
61
|
+
}
|
|
62
|
+
if (max !== undefined) {
|
|
63
|
+
current = Math.min(max, current);
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
function normalizeSeed(seed, range) {
|
|
68
|
+
const l = clampValue(seed.l, range.minL, range.maxL);
|
|
69
|
+
const c = clampValue(seed.c, range.minC, range.maxC);
|
|
70
|
+
return {
|
|
71
|
+
l,
|
|
72
|
+
c: Math.max(0, c),
|
|
73
|
+
h: seed.h,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function resolveAnchorOption(option, mode) {
|
|
77
|
+
if (!option) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (typeof option === "object") {
|
|
81
|
+
return option[mode];
|
|
82
|
+
}
|
|
83
|
+
return option;
|
|
84
|
+
}
|
|
85
|
+
function resolveSeedNormalizeOptions(options, autoEnabled) {
|
|
86
|
+
const enabled = options?.enabled ?? autoEnabled;
|
|
87
|
+
return {
|
|
88
|
+
enabled,
|
|
89
|
+
light: { ...defaultSeedNormalize.light, ...options?.light },
|
|
90
|
+
dark: { ...defaultSeedNormalize.dark, ...options?.dark },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function resolveCandidateSteps(candidateSteps) {
|
|
94
|
+
const unique = Array.from(new Set(candidateSteps ?? defaultAutoAnchor.candidateSteps));
|
|
95
|
+
return unique.length > 0 ? unique : [9];
|
|
96
|
+
}
|
|
97
|
+
function resolveAutoAnchorModeOptions(base, overrides, candidateSteps) {
|
|
98
|
+
const resolvedCandidates = resolveCandidateSteps(overrides?.candidateSteps ?? candidateSteps);
|
|
99
|
+
const resolvedBackgroundStep = overrides?.backgroundStep ?? base.backgroundStep;
|
|
100
|
+
const resolvedBackgroundSteps = overrides?.backgroundSteps ?? base.backgroundSteps;
|
|
101
|
+
return {
|
|
102
|
+
candidateSteps: resolvedCandidates,
|
|
103
|
+
backgroundStep: resolvedBackgroundStep,
|
|
104
|
+
backgroundSteps: resolvedBackgroundSteps.length > 0 ? resolvedBackgroundSteps : [resolvedBackgroundStep],
|
|
105
|
+
solidStep: overrides?.solidStep ?? base.solidStep,
|
|
106
|
+
textStep: overrides?.textStep ?? base.textStep,
|
|
107
|
+
targetContrast: overrides?.targetContrast ?? base.targetContrast,
|
|
108
|
+
minBackgroundL: overrides?.minBackgroundL ?? base.minBackgroundL,
|
|
109
|
+
maxBackgroundL: overrides?.maxBackgroundL ?? base.maxBackgroundL,
|
|
110
|
+
minTextL: overrides?.minTextL ?? base.minTextL,
|
|
111
|
+
maxTextL: overrides?.maxTextL ?? base.maxTextL,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function resolveAutoAnchorOptions(options) {
|
|
115
|
+
const candidateSteps = resolveCandidateSteps(options?.candidateSteps);
|
|
116
|
+
return {
|
|
117
|
+
light: resolveAutoAnchorModeOptions(defaultAutoAnchor.light, options?.light, candidateSteps),
|
|
118
|
+
dark: resolveAutoAnchorModeOptions(defaultAutoAnchor.dark, options?.dark, candidateSteps),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function rangePenalty(value, min, max, weight = 100) {
|
|
122
|
+
if (value < min) {
|
|
123
|
+
return (min - value) * weight;
|
|
124
|
+
}
|
|
125
|
+
if (value > max) {
|
|
126
|
+
return (value - max) * weight;
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
function scoreScale(scale, options) {
|
|
131
|
+
const solid = scale[options.solidStep];
|
|
132
|
+
const onSolid = onSolidTextTokens(solid);
|
|
133
|
+
const contrast = Math.abs(apcaContrast(onSolid.primary, solid));
|
|
134
|
+
const text = scale[options.textStep];
|
|
135
|
+
const textL = hexToOklch(text).l;
|
|
136
|
+
const backgroundLs = options.backgroundSteps.map((step) => hexToOklch(scale[step]).l);
|
|
137
|
+
const minBackgroundL = Math.min(...backgroundLs);
|
|
138
|
+
const maxBackgroundL = Math.max(...backgroundLs);
|
|
139
|
+
let score = 0;
|
|
140
|
+
const contrastDelta = options.targetContrast - contrast;
|
|
141
|
+
if (contrastDelta > 0) {
|
|
142
|
+
score += contrastDelta * 2.2;
|
|
143
|
+
}
|
|
144
|
+
score += rangePenalty(minBackgroundL, options.minBackgroundL, options.maxBackgroundL, 180);
|
|
145
|
+
score += rangePenalty(maxBackgroundL, options.minBackgroundL, options.maxBackgroundL, 180);
|
|
146
|
+
score += rangePenalty(textL, options.minTextL, options.maxTextL, 120);
|
|
147
|
+
return score;
|
|
148
|
+
}
|
|
149
|
+
function pickAutoAnchorStep(seed, templateId, curves, gamutStrategy, mode, options) {
|
|
150
|
+
let bestStep = 9;
|
|
151
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
152
|
+
for (const candidate of options.candidateSteps) {
|
|
153
|
+
const result = buildScaleForMode(seed, templateId, candidate, curves, gamutStrategy, mode, false);
|
|
154
|
+
const score = scoreScale(result.scale, options) + Math.abs(candidate - 9) * 2;
|
|
155
|
+
if (score < bestScore) {
|
|
156
|
+
bestScore = score;
|
|
157
|
+
bestStep = candidate;
|
|
158
|
+
}
|
|
159
|
+
else if (score === bestScore) {
|
|
160
|
+
if (Math.abs(candidate - 9) < Math.abs(bestStep - 9)) {
|
|
161
|
+
bestStep = candidate;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return bestStep;
|
|
166
|
+
}
|
|
167
|
+
function buildScaleForMode(seed, templateId, anchorStep, curves, gamutStrategy = "compress", mode = "light", includeP3 = false) {
|
|
168
|
+
const template = templates[mode][templateId];
|
|
169
|
+
const anchor = template[anchorStep];
|
|
170
|
+
const dL = seed.l - anchor.l;
|
|
171
|
+
const dC = seed.c - anchor.c;
|
|
172
|
+
const dH = seed.h - anchor.h;
|
|
173
|
+
const curveSet = resolveCurves(curves);
|
|
174
|
+
const output = {};
|
|
175
|
+
const p3Output = includeP3 ? {} : undefined;
|
|
176
|
+
let outOfGamutCount = 0;
|
|
177
|
+
let outOfP3GamutCount = 0;
|
|
178
|
+
for (const step of steps) {
|
|
179
|
+
const base = template[step];
|
|
180
|
+
const l = base.l + dL * curveSet.lightness[step];
|
|
181
|
+
const c = Math.max(0, base.c + dC * curveSet.chroma[step]);
|
|
182
|
+
const h = normalizeHue(base.h + dH);
|
|
183
|
+
const candidate = { l, c, h };
|
|
184
|
+
let current = candidate;
|
|
185
|
+
if (!inSrgbGamut(current)) {
|
|
186
|
+
outOfGamutCount += 1;
|
|
187
|
+
if (gamutStrategy === "compress") {
|
|
188
|
+
current = compressToSrgb(current);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
output[step] = oklchToHex(current);
|
|
192
|
+
if (p3Output) {
|
|
193
|
+
let p3Current = candidate;
|
|
194
|
+
if (!inP3Gamut(p3Current)) {
|
|
195
|
+
outOfP3GamutCount += 1;
|
|
196
|
+
p3Current = compressToP3(p3Current);
|
|
197
|
+
}
|
|
198
|
+
p3Output[step] = oklchToP3(p3Current);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
scale: output,
|
|
203
|
+
p3: p3Output,
|
|
204
|
+
outOfGamutCount,
|
|
205
|
+
outOfP3GamutCount,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export function generateScale(options) {
|
|
209
|
+
const seedHex = getSeedHex(options.source);
|
|
210
|
+
const seedOklch = hexToOklch(seedHex);
|
|
211
|
+
const mode = options.mode ?? "both";
|
|
212
|
+
const templateId = options.template === "auto" || !options.template
|
|
213
|
+
? selectTemplateId(seedOklch)
|
|
214
|
+
: options.template;
|
|
215
|
+
const gamutStrategy = options.gamut?.strategy ?? "compress";
|
|
216
|
+
const anchorOption = options.anchorStep ?? "auto";
|
|
217
|
+
const anchorLightOption = resolveAnchorOption(anchorOption, "light");
|
|
218
|
+
const anchorDarkOption = resolveAnchorOption(anchorOption, "dark");
|
|
219
|
+
const autoAnchorOptions = resolveAutoAnchorOptions(options.autoAnchor);
|
|
220
|
+
const autoEnabled = anchorLightOption === "auto" || anchorDarkOption === "auto";
|
|
221
|
+
const seedNormalize = resolveSeedNormalizeOptions(options.seedNormalize, autoEnabled);
|
|
222
|
+
const lightSeed = seedNormalize.enabled
|
|
223
|
+
? normalizeSeed(seedOklch, seedNormalize.light)
|
|
224
|
+
: seedOklch;
|
|
225
|
+
const darkSeed = seedNormalize.enabled ? normalizeSeed(seedOklch, seedNormalize.dark) : seedOklch;
|
|
226
|
+
const lightAnchorStep = anchorLightOption === "auto"
|
|
227
|
+
? pickAutoAnchorStep(lightSeed, templateId, options.curves, gamutStrategy, "light", autoAnchorOptions.light)
|
|
228
|
+
: (anchorLightOption ?? 9);
|
|
229
|
+
const darkAnchorStep = anchorDarkOption === "auto"
|
|
230
|
+
? pickAutoAnchorStep(darkSeed, templateId, options.curves, gamutStrategy, "dark", autoAnchorOptions.dark)
|
|
231
|
+
: (anchorDarkOption ?? 9);
|
|
232
|
+
const lightResult = buildScaleForMode(lightSeed, templateId, lightAnchorStep, options.curves, gamutStrategy, "light", options.p3 ?? false);
|
|
233
|
+
const darkResult = buildScaleForMode(darkSeed, templateId, darkAnchorStep, options.curves, gamutStrategy, "dark", options.p3 ?? false);
|
|
234
|
+
const scale = {
|
|
235
|
+
light: lightResult.scale,
|
|
236
|
+
dark: darkResult.scale,
|
|
237
|
+
p3: lightResult.p3 && darkResult.p3 ? { light: lightResult.p3, dark: darkResult.p3 } : undefined,
|
|
238
|
+
meta: {
|
|
239
|
+
outOfGamutCount: lightResult.outOfGamutCount + darkResult.outOfGamutCount,
|
|
240
|
+
outOfP3GamutCount: lightResult.outOfP3GamutCount + darkResult.outOfP3GamutCount,
|
|
241
|
+
anchorSteps: {
|
|
242
|
+
light: lightAnchorStep,
|
|
243
|
+
dark: darkAnchorStep,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
if (mode === "light") {
|
|
248
|
+
return {
|
|
249
|
+
...scale,
|
|
250
|
+
dark: scale.light,
|
|
251
|
+
p3: scale.p3 ? { light: scale.p3.light, dark: scale.p3.light } : undefined,
|
|
252
|
+
meta: scale.meta
|
|
253
|
+
? {
|
|
254
|
+
...scale.meta,
|
|
255
|
+
anchorSteps: { light: lightAnchorStep, dark: lightAnchorStep },
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (mode === "dark") {
|
|
261
|
+
return {
|
|
262
|
+
...scale,
|
|
263
|
+
light: scale.dark,
|
|
264
|
+
p3: scale.p3 ? { light: scale.p3.dark, dark: scale.p3.dark } : undefined,
|
|
265
|
+
meta: scale.meta
|
|
266
|
+
? {
|
|
267
|
+
...scale.meta,
|
|
268
|
+
anchorSteps: { light: darkAnchorStep, dark: darkAnchorStep },
|
|
269
|
+
}
|
|
270
|
+
: undefined,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return scale;
|
|
274
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { generateAlphaScale } from "./alpha/generateAlphaScale.js";
|
|
2
|
+
export { apcaContrast } from "./contrast/apca.js";
|
|
3
|
+
export { onSolidTextTokens } from "./contrast/onSolid.js";
|
|
4
|
+
export { adjustTextColor } from "./contrast/solveText.js";
|
|
5
|
+
export { createTheme } from "./createTheme.js";
|
|
6
|
+
export { radixSeedNames, radixSeeds } from "./data/radixSeeds.js";
|
|
7
|
+
export { analyzeScale } from "./diagnostics/analyzeScale.js";
|
|
8
|
+
export { analyzeTheme } from "./diagnostics/analyzeTheme.js";
|
|
9
|
+
export { selectThemeColorMode } from "./exporters/selectColorMode.js";
|
|
10
|
+
export { toCssVars } from "./exporters/toCssVars.js";
|
|
11
|
+
export { toJson, toJsonWithMode } from "./exporters/toJson.js";
|
|
12
|
+
export { toReactNative } from "./exporters/toReactNative.js";
|
|
13
|
+
export { toTailwind } from "./exporters/toTailwind.js";
|
|
14
|
+
export { toTs, toTsWithMode } from "./exporters/toTs.js";
|
|
15
|
+
export type { AnchorStepOption, AutoAnchorModeOptions, AutoAnchorOptions, GenerateScaleOptions, SeedNormalizeOptions, SeedNormalizeRange, } from "./generateScale.js";
|
|
16
|
+
export { generateScale } from "./generateScale.js";
|
|
17
|
+
export type { AlphaScale, ColorHex, ColorSource, OklchColor, RadixSeedName, Scale, ScaleColorMode, ScaleDiagnostics, Step, TemplateId, Theme, ThemeColorMode, ThemeDiagnostics, } from "./types.js";
|
|
@@ -13,18 +13,3 @@ export { toReactNative } from "./exporters/toReactNative.js";
|
|
|
13
13
|
export { toTailwind } from "./exporters/toTailwind.js";
|
|
14
14
|
export { toTs, toTsWithMode } from "./exporters/toTs.js";
|
|
15
15
|
export { generateScale } from "./generateScale.js";
|
|
16
|
-
export type {
|
|
17
|
-
AlphaScale,
|
|
18
|
-
ColorHex,
|
|
19
|
-
ColorSource,
|
|
20
|
-
OklchColor,
|
|
21
|
-
RadixSeedName,
|
|
22
|
-
Scale,
|
|
23
|
-
ScaleColorMode,
|
|
24
|
-
ScaleDiagnostics,
|
|
25
|
-
Step,
|
|
26
|
-
TemplateId,
|
|
27
|
-
Theme,
|
|
28
|
-
ThemeColorMode,
|
|
29
|
-
ThemeDiagnostics,
|
|
30
|
-
} from "./types.js";
|