@clhaas/palette-kit 0.1.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex/skills/color-pipeline-implementer/SKILL.md +23 -0
- package/.codex/skills/commit-message-crafter/SKILL.md +63 -0
- package/.codex/skills/commit-message-crafter/references/benchmarks.md +20 -0
- package/.codex/skills/contrast-solver-helper/SKILL.md +20 -0
- package/.codex/skills/exporters-builder/SKILL.md +20 -0
- package/.codex/skills/markdownlint-writer/SKILL.md +32 -0
- package/.codex/skills/phase-implementation-runbook/SKILL.md +92 -0
- package/.codex/skills/type-contract-auditor/SKILL.md +21 -0
- package/.github/skills/review-guide/SKILL.md +23 -0
- package/.github/skills/review-guide/references/review-guide-v0.3.md +629 -0
- package/.markdownlint.json +4 -0
- package/AGENTS.md +16 -0
- package/CHANGELOG.md +34 -0
- package/README.md +79 -169
- package/biome.json +43 -0
- package/dist/cli/args.d.ts +12 -0
- package/dist/cli/args.js +56 -0
- package/dist/cli/args.test.js +22 -0
- package/dist/cli/codegen/__snapshots__/tokens.test.js.snap +87 -0
- package/dist/cli/codegen/tokens.d.ts +12 -0
- package/dist/cli/codegen/tokens.js +139 -0
- package/dist/cli/codegen/tokens.test.d.ts +1 -0
- package/dist/cli/codegen/tokens.test.js +51 -0
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +34 -0
- package/dist/cli/validate.d.ts +2 -0
- package/dist/cli/validate.js +33 -0
- package/dist/cli/validate.test.d.ts +1 -0
- package/dist/cli/validate.test.js +40 -0
- package/dist/cli.js +138 -140
- package/dist/contrast/apca.d.ts +2 -2
- package/dist/contrast/apca.js +14 -4
- package/dist/contrast/apca.test.d.ts +1 -0
- package/dist/contrast/apca.test.js +16 -0
- package/dist/contrast/index.d.ts +4 -0
- package/dist/contrast/index.js +4 -0
- package/dist/contrast/scoring.d.ts +4 -0
- package/dist/contrast/scoring.js +31 -0
- package/dist/contrast/scoring.test.d.ts +1 -0
- package/dist/contrast/scoring.test.js +148 -0
- package/dist/contrast/solver.d.ts +13 -0
- package/dist/contrast/solver.js +170 -0
- package/dist/contrast/solver.test.d.ts +1 -0
- package/dist/contrast/solver.test.js +75 -0
- package/dist/contrast/types.d.ts +17 -0
- package/dist/contrast/types.js +1 -0
- package/dist/contrast/utils.d.ts +4 -0
- package/dist/contrast/utils.js +18 -0
- package/dist/contrast/wcag2.d.ts +3 -0
- package/dist/contrast/wcag2.js +19 -0
- package/dist/contrast/wcag2.test.d.ts +1 -0
- package/dist/contrast/wcag2.test.js +17 -0
- package/dist/core/createTheme.d.ts +35 -0
- package/dist/core/createTheme.js +24 -0
- package/dist/core/dx-helpers.test.d.ts +1 -0
- package/dist/core/dx-helpers.test.js +61 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/onSolid.test.d.ts +1 -0
- package/dist/core/onSolid.test.js +118 -0
- package/dist/core/qa.v1.test.d.ts +1 -0
- package/dist/core/qa.v1.test.js +112 -0
- package/dist/core/resolve.d.ts +3 -0
- package/dist/core/resolve.js +8 -0
- package/dist/core/resolve.test.d.ts +1 -0
- package/dist/core/resolve.test.js +89 -0
- package/dist/core/resolveMany.d.ts +8 -0
- package/dist/core/resolveMany.js +17 -0
- package/dist/core/tokenRegistry.d.ts +23 -0
- package/dist/core/tokenRegistry.js +83 -0
- package/dist/core/tokenRegistry.test.d.ts +1 -0
- package/dist/core/tokenRegistry.test.js +133 -0
- package/dist/engine/applyOperators.d.ts +3 -0
- package/dist/engine/applyOperators.js +23 -0
- package/dist/engine/context.d.ts +4 -0
- package/dist/engine/context.js +1 -0
- package/dist/engine/gamut.d.ts +13 -0
- package/dist/engine/gamut.js +101 -0
- package/dist/engine/gamut.test.d.ts +1 -0
- package/dist/engine/gamut.test.js +23 -0
- package/dist/engine/generateScale.d.ts +15 -0
- package/dist/engine/generateScale.js +29 -0
- package/dist/engine/generateScale.test.d.ts +1 -0
- package/dist/engine/generateScale.test.js +32 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +4 -0
- package/dist/engine/normalize.d.ts +43 -0
- package/dist/engine/normalize.js +403 -0
- package/dist/engine/normalize.test.d.ts +1 -0
- package/dist/engine/normalize.test.js +136 -0
- package/dist/engine/onSolid.d.ts +3 -0
- package/dist/engine/onSolid.js +110 -0
- package/dist/engine/resolveBaseColor.d.ts +25 -0
- package/dist/engine/resolveBaseColor.js +127 -0
- package/dist/engine/resolveBaseColor.test.d.ts +1 -0
- package/dist/engine/resolveBaseColor.test.js +97 -0
- package/dist/export/__snapshots__/exportTheme.test.js.snap +74 -0
- package/dist/export/exportTheme.d.ts +47 -0
- package/dist/export/exportTheme.js +170 -0
- package/dist/export/exportTheme.test.d.ts +1 -0
- package/dist/export/exportTheme.test.js +118 -0
- package/dist/export/index.d.ts +1 -0
- package/dist/export/index.js +1 -0
- package/dist/export/serializeColor.d.ts +1 -0
- package/dist/export/serializeColor.js +1 -0
- package/dist/export/serializeColor.test.d.ts +1 -0
- package/dist/export/serializeColor.test.js +54 -0
- package/dist/export.d.ts +1 -0
- package/dist/export.js +1 -0
- package/dist/index.d.ts +3 -22
- package/dist/index.js +2 -18
- package/dist/operators/emphasis.d.ts +3 -0
- package/dist/operators/emphasis.js +113 -0
- package/dist/operators/emphasis.test.d.ts +1 -0
- package/dist/operators/emphasis.test.js +69 -0
- package/dist/operators/index.d.ts +3 -0
- package/dist/operators/index.js +2 -0
- package/dist/operators/state.d.ts +3 -0
- package/dist/operators/state.js +102 -0
- package/dist/operators/state.test.d.ts +1 -0
- package/dist/operators/state.test.js +48 -0
- package/dist/operators/types.d.ts +13 -0
- package/dist/operators/types.js +1 -0
- package/dist/operators/utils.d.ts +16 -0
- package/dist/operators/utils.js +23 -0
- package/dist/presets/curves.d.ts +28 -0
- package/dist/presets/curves.js +145 -0
- package/dist/presets/index.d.ts +2 -0
- package/dist/presets/index.js +1 -0
- package/dist/presets/tokens/index.d.ts +3 -0
- package/dist/presets/tokens/index.js +3 -0
- package/dist/presets/tokens/minimal-ui.d.ts +6 -0
- package/dist/presets/tokens/minimal-ui.js +53 -0
- package/dist/presets/tokens/modern-ui.d.ts +5 -0
- package/dist/presets/tokens/modern-ui.js +83 -0
- package/dist/presets/tokens/presets.test.d.ts +1 -0
- package/dist/presets/tokens/presets.test.js +31 -0
- package/dist/presets/tokens/radixLike-ui.d.ts +6 -0
- package/dist/presets/tokens/radixLike-ui.js +77 -0
- package/dist/serialize/index.d.ts +1 -0
- package/dist/serialize/index.js +1 -0
- package/dist/serialize/normalizeOutput.d.ts +6 -0
- package/dist/serialize/normalizeOutput.js +45 -0
- package/dist/serialize/serializeColor.d.ts +21 -0
- package/dist/serialize/serializeColor.js +178 -0
- package/dist/serialize/serializeResolved.test.d.ts +1 -0
- package/dist/serialize/serializeResolved.test.js +45 -0
- package/dist/serialize.d.ts +1 -0
- package/dist/serialize.js +1 -0
- package/dist/types/index.d.ts +187 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/clamp.d.ts +1 -0
- package/dist/utils/clamp.js +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/lerp.d.ts +1 -0
- package/dist/utils/lerp.js +1 -0
- package/dist/utils/parseColor.d.ts +6 -0
- package/dist/utils/parseColor.js +67 -0
- package/dist/utils/parseColor.test.d.ts +1 -0
- package/dist/utils/parseColor.test.js +51 -0
- package/dist/utils/smoothstep.d.ts +1 -0
- package/dist/utils/smoothstep.js +5 -0
- package/package.json +19 -12
- package/planning/phase-10-review.md +550 -0
- package/planning/phase-7-review.md +411 -0
- package/planning/phase-8-review.md +669 -0
- package/planning/phase-9-review.md +564 -0
- package/planning/roadmap-v0.3.md +284 -0
- package/planning/spec-serializer-v0.3.md +324 -0
- package/planning/spec-v0.3.md +305 -0
- package/src/cli/args.test.ts +28 -0
- package/src/cli/args.ts +66 -0
- package/src/cli/codegen/__snapshots__/tokens.test.ts.snap +87 -0
- package/src/cli/codegen/tokens.test.ts +61 -0
- package/src/cli/codegen/tokens.ts +191 -0
- package/src/cli/config.ts +71 -0
- package/src/cli/validate.test.ts +49 -0
- package/src/cli/validate.ts +38 -0
- package/src/cli.ts +183 -0
- package/src/contrast/apca.test.ts +20 -0
- package/src/contrast/apca.ts +26 -0
- package/src/contrast/index.ts +4 -0
- package/src/contrast/scoring.test.ts +188 -0
- package/src/contrast/scoring.ts +48 -0
- package/src/contrast/solver.test.ts +147 -0
- package/src/contrast/solver.ts +235 -0
- package/src/contrast/types.ts +20 -0
- package/src/contrast/utils.ts +28 -0
- package/src/contrast/wcag2.test.ts +21 -0
- package/src/contrast/wcag2.ts +24 -0
- package/src/core/createTheme.ts +78 -0
- package/src/core/dx-helpers.test.ts +82 -0
- package/src/core/index.ts +7 -0
- package/src/core/onSolid.test.ts +146 -0
- package/src/core/qa.v1.test.ts +149 -0
- package/src/core/resolve.test.ts +99 -0
- package/src/core/resolve.ts +11 -0
- package/src/core/resolveMany.ts +22 -0
- package/src/core/tokenRegistry.test.ts +153 -0
- package/src/core/tokenRegistry.ts +114 -0
- package/src/engine/applyOperators.ts +32 -0
- package/src/engine/context.ts +8 -0
- package/src/engine/gamut.test.ts +30 -0
- package/src/engine/gamut.ts +144 -0
- package/src/engine/generateScale.test.ts +46 -0
- package/src/engine/generateScale.ts +48 -0
- package/src/engine/index.ts +8 -0
- package/src/engine/normalize.test.ts +222 -0
- package/src/engine/normalize.ts +550 -0
- package/src/engine/onSolid.ts +178 -0
- package/src/engine/resolveBaseColor.test.ts +117 -0
- package/src/engine/resolveBaseColor.ts +203 -0
- package/src/export/__snapshots__/exportTheme.test.ts.snap +74 -0
- package/src/export/exportTheme.test.ts +144 -0
- package/src/export/exportTheme.ts +251 -0
- package/src/export/index.ts +1 -0
- package/src/export/serializeColor.test.ts +73 -0
- package/src/export/serializeColor.ts +1 -0
- package/src/export.ts +1 -0
- package/src/index.ts +3 -0
- package/src/operators/emphasis.test.ts +85 -0
- package/src/operators/emphasis.ts +132 -0
- package/src/operators/index.ts +3 -0
- package/src/operators/state.test.ts +66 -0
- package/src/operators/state.ts +122 -0
- package/src/operators/types.ts +14 -0
- package/src/operators/utils.ts +44 -0
- package/src/presets/curves.ts +168 -0
- package/src/presets/index.ts +2 -0
- package/src/presets/tokens/index.ts +3 -0
- package/src/presets/tokens/minimal-ui.ts +55 -0
- package/src/presets/tokens/modern-ui.ts +85 -0
- package/src/presets/tokens/presets.test.ts +46 -0
- package/src/presets/tokens/radixLike-ui.ts +79 -0
- package/src/serialize/index.ts +1 -0
- package/src/serialize/normalizeOutput.ts +63 -0
- package/src/serialize/serializeColor.ts +260 -0
- package/src/serialize/serializeResolved.test.ts +57 -0
- package/src/serialize.ts +1 -0
- package/src/types/index.ts +207 -0
- package/src/utils/clamp.ts +2 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/lerp.ts +1 -0
- package/src/utils/parseColor.test.ts +66 -0
- package/src/utils/parseColor.ts +87 -0
- package/src/utils/smoothstep.ts +6 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +15 -0
- package/dist/alpha/generateAlphaScale.d.ts +0 -5
- package/dist/alpha/generateAlphaScale.js +0 -34
- package/dist/contrast/onSolid.d.ts +0 -6
- package/dist/contrast/onSolid.js +0 -28
- package/dist/contrast/solveText.d.ts +0 -2
- package/dist/contrast/solveText.js +0 -31
- package/dist/createTheme.d.ts +0 -38
- package/dist/createTheme.js +0 -148
- package/dist/data/radixSeeds.d.ts +0 -3
- package/dist/data/radixSeeds.js +0 -34
- package/dist/diagnostics/analyzeScale.d.ts +0 -2
- package/dist/diagnostics/analyzeScale.js +0 -7
- package/dist/diagnostics/analyzeTheme.d.ts +0 -2
- package/dist/diagnostics/analyzeTheme.js +0 -35
- package/dist/diagnostics/warnings.d.ts +0 -2
- package/dist/diagnostics/warnings.js +0 -20
- package/dist/engine/curves.d.ts +0 -9
- package/dist/engine/curves.js +0 -48
- package/dist/engine/oklch.d.ts +0 -8
- package/dist/engine/oklch.js +0 -40
- package/dist/engine/templates.d.ts +0 -14
- package/dist/engine/templates.js +0 -45
- package/dist/exporters/selectColorMode.d.ts +0 -2
- package/dist/exporters/selectColorMode.js +0 -19
- package/dist/exporters/toCssVars.d.ts +0 -13
- package/dist/exporters/toCssVars.js +0 -108
- package/dist/exporters/toJson.d.ts +0 -3
- package/dist/exporters/toJson.js +0 -25
- package/dist/exporters/toReactNative.d.ts +0 -54
- package/dist/exporters/toReactNative.js +0 -33
- package/dist/exporters/toTailwind.d.ts +0 -17
- package/dist/exporters/toTailwind.js +0 -111
- package/dist/exporters/toTs.d.ts +0 -3
- package/dist/exporters/toTs.js +0 -43
- package/dist/generateScale.d.ts +0 -48
- package/dist/generateScale.js +0 -274
- package/dist/overlays/generateOverlayScale.d.ts +0 -2
- package/dist/overlays/generateOverlayScale.js +0 -34
- package/dist/text/generateTextScale.d.ts +0 -8
- package/dist/text/generateTextScale.js +0 -18
- package/dist/text/resolveOnBgText.d.ts +0 -9
- package/dist/text/resolveOnBgText.js +0 -28
- package/dist/tokens/presetRadixLikeUi.d.ts +0 -5
- package/dist/tokens/presetRadixLikeUi.js +0 -55
- package/dist/types.d.ts +0 -69
- /package/dist/{types.js → cli/args.test.d.ts} +0 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AlphaStrategy,
|
|
3
|
+
BackgroundHint,
|
|
4
|
+
ColorContext,
|
|
5
|
+
ColorEmphasis,
|
|
6
|
+
ColorQuery,
|
|
7
|
+
ColorSpace,
|
|
8
|
+
ColorState,
|
|
9
|
+
ColorUsage,
|
|
10
|
+
ContrastRequirement,
|
|
11
|
+
OnSolidQuery,
|
|
12
|
+
OutputOptions,
|
|
13
|
+
SemanticVariant,
|
|
14
|
+
SurfaceIntent,
|
|
15
|
+
} from "../types/index.js";
|
|
16
|
+
|
|
17
|
+
export type NormalizedQuery = Required<
|
|
18
|
+
Pick<ColorQuery, "role" | "usage" | "context" | "surface" | "state" | "emphasis">
|
|
19
|
+
> & {
|
|
20
|
+
variant?: ColorQuery["variant"];
|
|
21
|
+
on?: ColorQuery["on"];
|
|
22
|
+
contrast?: ColorQuery["contrast"];
|
|
23
|
+
alpha?: ColorQuery["alpha"];
|
|
24
|
+
output: Required<Omit<OutputOptions, "format">>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NormalizedOnSolidQuery = Required<
|
|
28
|
+
Pick<OnSolidQuery, "bgRole" | "usage" | "context" | "state" | "emphasis">
|
|
29
|
+
> & {
|
|
30
|
+
alpha?: OnSolidQuery["alpha"];
|
|
31
|
+
contrast?: OnSolidQuery["contrast"];
|
|
32
|
+
output: Required<Omit<OutputOptions, "format">>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const usages: ColorUsage[] = ["bg", "border", "text", "icon", "ring", "shadow", "stroke", "fill"];
|
|
36
|
+
const onSolidUsages: OnSolidQuery["usage"][] = ["text", "icon"];
|
|
37
|
+
|
|
38
|
+
const contexts: ColorContext[] = ["light", "dark", "highContrast", "dimmed"];
|
|
39
|
+
|
|
40
|
+
const surfaces: SurfaceIntent[] = [
|
|
41
|
+
"app",
|
|
42
|
+
"surface",
|
|
43
|
+
"subtle",
|
|
44
|
+
"solid",
|
|
45
|
+
"overlay",
|
|
46
|
+
"data",
|
|
47
|
+
"transparent",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const COLOR_STATES: readonly ColorState[] = [
|
|
51
|
+
"default",
|
|
52
|
+
"hover",
|
|
53
|
+
"active",
|
|
54
|
+
"selected",
|
|
55
|
+
"focus",
|
|
56
|
+
"disabled",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const emphases: ColorEmphasis[] = ["muted", "subtle", "default", "strong", "inverted"];
|
|
60
|
+
|
|
61
|
+
const semanticVariants: SemanticVariant[] = [
|
|
62
|
+
"neutral",
|
|
63
|
+
"accent",
|
|
64
|
+
"success",
|
|
65
|
+
"warning",
|
|
66
|
+
"danger",
|
|
67
|
+
"info",
|
|
68
|
+
"highlight",
|
|
69
|
+
"premium",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const colorSpaces: ColorSpace[] = ["srgb", "p3", "oklch"];
|
|
73
|
+
|
|
74
|
+
const gamutMappings: NonNullable<OutputOptions["gamutMapping"]>[] = [
|
|
75
|
+
"clip",
|
|
76
|
+
"compressChroma",
|
|
77
|
+
"preferP3ThenCompress",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const srgbFormats: NonNullable<OutputOptions["srgbFormat"]>[] = ["hex", "rgb", "rgba"];
|
|
81
|
+
|
|
82
|
+
const formatString = (value: string | undefined) => (value ? value.trim() : undefined);
|
|
83
|
+
|
|
84
|
+
const assertOneOf = <T extends string>(value: string, options: readonly T[], label: string): T => {
|
|
85
|
+
if (!options.includes(value as T)) {
|
|
86
|
+
throw new Error(`Invalid ${label}: "${value}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return value as T;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const normalizeRole = (role: string | undefined) => {
|
|
93
|
+
if (role === undefined) {
|
|
94
|
+
throw new Error("Color role is required");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const trimmed = role.trim();
|
|
98
|
+
if (!trimmed) {
|
|
99
|
+
throw new Error("Color role is required");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return trimmed;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const normalizeBgRole = (role: string | undefined) => {
|
|
106
|
+
if (role === undefined) {
|
|
107
|
+
throw new Error("Background role (bgRole) is required");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const trimmed = role.trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
throw new Error("Background role (bgRole) is required");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return trimmed;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const hexColorPattern = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
119
|
+
|
|
120
|
+
const inferUsageFromRole = (role: string): ColorUsage | undefined => {
|
|
121
|
+
const normalizedRole = role.trim().toLowerCase();
|
|
122
|
+
|
|
123
|
+
if (normalizedRole.startsWith("text.")) {
|
|
124
|
+
return "text";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (normalizedRole.startsWith("icon.")) {
|
|
128
|
+
return "icon";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (normalizedRole.startsWith("border.")) {
|
|
132
|
+
return "border";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
normalizedRole.startsWith("bg.") ||
|
|
137
|
+
normalizedRole.startsWith("surface.") ||
|
|
138
|
+
normalizedRole.startsWith("overlay.")
|
|
139
|
+
) {
|
|
140
|
+
return "bg";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (normalizedRole.startsWith("focus.") || normalizedRole.startsWith("ring.")) {
|
|
144
|
+
return "ring";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (normalizedRole.startsWith("chart.")) {
|
|
148
|
+
const tokens = normalizedRole.split(".");
|
|
149
|
+
|
|
150
|
+
if (tokens.includes("stroke")) {
|
|
151
|
+
return "stroke";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (tokens.includes("fill")) {
|
|
155
|
+
return "fill";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (tokens.includes("grid")) {
|
|
159
|
+
return "border";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (tokens.includes("label") || tokens.includes("text")) {
|
|
163
|
+
return "text";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return undefined;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Infer surface intent from role naming patterns.
|
|
172
|
+
*
|
|
173
|
+
* Recognizes:
|
|
174
|
+
* - Direct surface token prefixes: `"app.*"`, `"surface.*"`, `"solid.*"`, etc.
|
|
175
|
+
* - Background patterns: `"bg.app"`, `"bg.surface"`, `"bg.solid"`, etc.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* inferSurfaceFromRole("bg.app") // "app"
|
|
179
|
+
* @example
|
|
180
|
+
* inferSurfaceFromRole("app.bg") // "app"
|
|
181
|
+
*/
|
|
182
|
+
const inferSurfaceFromRole = (role: string): SurfaceIntent | undefined => {
|
|
183
|
+
const normalizedRole = role.trim().toLowerCase();
|
|
184
|
+
const tokens = normalizedRole.split(".");
|
|
185
|
+
const [first, second] = tokens;
|
|
186
|
+
|
|
187
|
+
if (first && surfaces.includes(first as SurfaceIntent)) {
|
|
188
|
+
return first as SurfaceIntent;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (first === "bg" && second && surfaces.includes(second as SurfaceIntent)) {
|
|
192
|
+
return second as SurfaceIntent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return undefined;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Infer semantic variant from role token.
|
|
200
|
+
*
|
|
201
|
+
* Recognizes:
|
|
202
|
+
* - Semantic variants: `"neutral"`, `"accent"`, `"success"`, `"warning"`, etc.
|
|
203
|
+
* - Custom categories: `"category:*"`
|
|
204
|
+
* - Chart variants: `"chart:*"`
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* inferVariantFromRole("success.bg") // "success"
|
|
208
|
+
* @example
|
|
209
|
+
* inferVariantFromRole("category:sales.fill") // "category:sales"
|
|
210
|
+
*/
|
|
211
|
+
const inferVariantFromRole = (role: string): SemanticVariant | undefined => {
|
|
212
|
+
const token = role.trim().split(".")[0];
|
|
213
|
+
const normalized = token?.toLowerCase();
|
|
214
|
+
|
|
215
|
+
if (!normalized) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (semanticVariants.includes(normalized as SemanticVariant)) {
|
|
220
|
+
return normalized as SemanticVariant;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (/^(category|chart):.+/.test(normalized)) {
|
|
224
|
+
return normalized as SemanticVariant;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return undefined;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const normalizeVariant = (variant: string | undefined): SemanticVariant | undefined => {
|
|
231
|
+
if (!variant) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const trimmed = variant.trim();
|
|
236
|
+
|
|
237
|
+
if (semanticVariants.includes(trimmed as SemanticVariant)) {
|
|
238
|
+
return trimmed as SemanticVariant;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (/^(category|chart):.+/.test(trimmed)) {
|
|
242
|
+
return trimmed as SemanticVariant;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw new Error(`Invalid variant: "${variant}"`);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const warn = (message: string) => {
|
|
249
|
+
console.warn(message);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Cache used to deduplicate inference warnings.
|
|
254
|
+
*
|
|
255
|
+
* Notes:
|
|
256
|
+
* - This is intentionally module-scoped so repeated calls don't spam logs.
|
|
257
|
+
* - To avoid unbounded growth in long-running processes, this cache is capped
|
|
258
|
+
* and clears itself when the cap is reached.
|
|
259
|
+
*/
|
|
260
|
+
const inferenceWarnings = new Set<string>();
|
|
261
|
+
|
|
262
|
+
const MAX_INFERENCE_WARNINGS = 1000;
|
|
263
|
+
|
|
264
|
+
const warnInferenceOnce = (key: string, message: string) => {
|
|
265
|
+
if (inferenceWarnings.has(key)) return;
|
|
266
|
+
if (inferenceWarnings.size >= MAX_INFERENCE_WARNINGS) {
|
|
267
|
+
inferenceWarnings.clear();
|
|
268
|
+
}
|
|
269
|
+
inferenceWarnings.add(key);
|
|
270
|
+
console.warn(message);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const missingUsageMessage = (role: string) =>
|
|
274
|
+
`Usage is required for role: "${role}". Provide usage explicitly (e.g. { usage: "text" }) or use a role prefix like "text.", "icon.", "bg.", "border.", "ring."`;
|
|
275
|
+
|
|
276
|
+
const missingSurfaceMessage = (role: string) =>
|
|
277
|
+
`Surface is required for role: "${role}". Provide surface explicitly (e.g. { surface: "surface" }) or use a role pattern like "bg.app", "bg.surface", "app.*"`;
|
|
278
|
+
|
|
279
|
+
const normalizeBackgroundHint = (
|
|
280
|
+
hint: BackgroundHint | undefined,
|
|
281
|
+
strict: boolean,
|
|
282
|
+
): BackgroundHint | undefined => {
|
|
283
|
+
if (!hint) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (hint.kind === "auto") {
|
|
288
|
+
return hint;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (hint.kind === "role") {
|
|
292
|
+
const trimmedRole = normalizeRole(hint.role);
|
|
293
|
+
return { kind: "role", role: trimmedRole };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (hint.kind === "color") {
|
|
297
|
+
const value = hint.value.trim();
|
|
298
|
+
if (!value) {
|
|
299
|
+
throw new Error("Background hint color value is required");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!hexColorPattern.test(value)) {
|
|
303
|
+
if (strict) {
|
|
304
|
+
throw new Error(`Invalid background hint color value: "${hint.value}"`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
warn(`Unvalidated background hint color value: "${hint.value}"`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { kind: "color", value };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
throw new Error(`Invalid background hint kind: "${(hint as BackgroundHint).kind}"`);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const normalizeContrast = (
|
|
317
|
+
contrast: ContrastRequirement | undefined,
|
|
318
|
+
): ContrastRequirement | undefined => {
|
|
319
|
+
if (!contrast) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (contrast.model === "apca") {
|
|
324
|
+
if (!Number.isFinite(contrast.targetLc)) {
|
|
325
|
+
throw new Error("APCA targetLc must be a number");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (contrast.minLc !== undefined && !Number.isFinite(contrast.minLc)) {
|
|
329
|
+
throw new Error("APCA minLc must be a number");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (contrast.maxLc !== undefined && !Number.isFinite(contrast.maxLc)) {
|
|
333
|
+
throw new Error("APCA maxLc must be a number");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Validate logical relationships between APCA contrast bounds
|
|
337
|
+
if (contrast.minLc !== undefined && contrast.maxLc !== undefined) {
|
|
338
|
+
if (contrast.minLc > contrast.maxLc) {
|
|
339
|
+
throw new Error("APCA minLc cannot be greater than maxLc");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (contrast.minLc !== undefined && contrast.minLc > contrast.targetLc) {
|
|
344
|
+
throw new Error("APCA minLc cannot be greater than targetLc");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (contrast.maxLc !== undefined && contrast.targetLc > contrast.maxLc) {
|
|
348
|
+
throw new Error("APCA targetLc cannot be greater than maxLc");
|
|
349
|
+
}
|
|
350
|
+
return contrast;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (contrast.model === "wcag2") {
|
|
354
|
+
const minRatio =
|
|
355
|
+
"minRatio" in contrast ? contrast.minRatio : (contrast as { ratio?: number }).ratio;
|
|
356
|
+
|
|
357
|
+
if (minRatio === undefined) {
|
|
358
|
+
throw new Error("WCAG2 contrast requires either 'minRatio' or legacy 'ratio' property");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!Number.isFinite(minRatio)) {
|
|
362
|
+
throw new Error("WCAG2 minRatio must be a number");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { model: "wcag2", minRatio };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (contrast.model === "none") {
|
|
369
|
+
return contrast;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
throw new Error(`Invalid contrast model: "${(contrast as ContrastRequirement).model}"`);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const validateIncludeSpaces = (includeSpaces: ColorSpace[] | undefined): ColorSpace[] => {
|
|
376
|
+
const value = includeSpaces ?? [];
|
|
377
|
+
|
|
378
|
+
value.forEach((space) => {
|
|
379
|
+
assertOneOf(space, colorSpaces, "output includeSpaces");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return value;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const normalizeAlpha = (alpha: AlphaStrategy | undefined): AlphaStrategy | undefined => {
|
|
386
|
+
if (!alpha) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (alpha.mode === "none") {
|
|
391
|
+
return alpha;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (alpha.mode === "fixed") {
|
|
395
|
+
if (!Number.isFinite(alpha.alpha)) {
|
|
396
|
+
throw new Error("Fixed alpha must be a number");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return alpha;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (alpha.mode === "solveOnBackground") {
|
|
403
|
+
return alpha;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
throw new Error(`Invalid alpha strategy mode: "${(alpha as AlphaStrategy).mode}"`);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const normalizeOutput = (
|
|
410
|
+
output: OutputOptions | undefined,
|
|
411
|
+
): Required<Omit<OutputOptions, "format">> => {
|
|
412
|
+
const preferSpaceValue = formatString(output?.preferSpace);
|
|
413
|
+
const gamutMappingValue = formatString(output?.gamutMapping);
|
|
414
|
+
const srgbFormatValue = formatString(output?.srgbFormat);
|
|
415
|
+
const includeSpaces = validateIncludeSpaces(output?.includeSpaces);
|
|
416
|
+
|
|
417
|
+
const preferSpace = preferSpaceValue
|
|
418
|
+
? assertOneOf(preferSpaceValue, colorSpaces, "output preferSpace")
|
|
419
|
+
: "oklch";
|
|
420
|
+
|
|
421
|
+
const gamutMapping = gamutMappingValue
|
|
422
|
+
? assertOneOf(gamutMappingValue, gamutMappings, "output gamutMapping")
|
|
423
|
+
: "preferP3ThenCompress";
|
|
424
|
+
|
|
425
|
+
const srgbFormat = srgbFormatValue
|
|
426
|
+
? assertOneOf(srgbFormatValue, srgbFormats, "output srgbFormat")
|
|
427
|
+
: "hex";
|
|
428
|
+
|
|
429
|
+
if (output?.strict !== undefined && typeof output.strict !== "boolean") {
|
|
430
|
+
throw new Error("Output strict must be a boolean");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (output?.includeMeta !== undefined && typeof output.includeMeta !== "boolean") {
|
|
434
|
+
throw new Error("Output includeMeta must be a boolean");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
preferSpace,
|
|
439
|
+
includeSpaces,
|
|
440
|
+
gamutMapping,
|
|
441
|
+
strict: output?.strict ?? false,
|
|
442
|
+
precision: {
|
|
443
|
+
l: 1,
|
|
444
|
+
c: 3,
|
|
445
|
+
h: 1,
|
|
446
|
+
...output?.precision,
|
|
447
|
+
},
|
|
448
|
+
includeMeta: output?.includeMeta ?? false,
|
|
449
|
+
srgbFormat,
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Normalize a user-facing ColorQuery into a fully populated, validated structure.
|
|
455
|
+
*
|
|
456
|
+
* - Applies defaults for missing fields (context, surface, state, emphasis, output).
|
|
457
|
+
* - Infers usage and surface from role naming patterns when not provided.
|
|
458
|
+
* - In strict mode, missing required fields throw actionable errors.
|
|
459
|
+
* - In non-strict mode, safe defaults are applied with explicit warnings.
|
|
460
|
+
* - Validates nested objects (background hints, contrast requirements, alpha strategies).
|
|
461
|
+
* - Trims string inputs and enforces allowed enum values.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* normalizeQuery({ role: "text.primary" });
|
|
465
|
+
* @example
|
|
466
|
+
* normalizeQuery({
|
|
467
|
+
* role: "bg.canvas",
|
|
468
|
+
* on: { kind: "color", value: "#fff" },
|
|
469
|
+
* contrast: { model: "apca", targetLc: 60 },
|
|
470
|
+
* output: { strict: true },
|
|
471
|
+
* });
|
|
472
|
+
*/
|
|
473
|
+
export function normalizeQuery(q: ColorQuery): NormalizedQuery {
|
|
474
|
+
const role = normalizeRole(formatString(q.role));
|
|
475
|
+
const contextValue = formatString(q.context) ?? "light";
|
|
476
|
+
const inferredSurface = inferSurfaceFromRole(role);
|
|
477
|
+
const surfaceValue = formatString(q.surface) ?? inferredSurface ?? "surface";
|
|
478
|
+
const stateValue = formatString(q.state) ?? "default";
|
|
479
|
+
const emphasisValue = formatString(q.emphasis) ?? "default";
|
|
480
|
+
const output = normalizeOutput(q.output);
|
|
481
|
+
const usageValue = formatString(q.usage) ?? inferUsageFromRole(role);
|
|
482
|
+
const variantValue = formatString(q.variant) ?? inferVariantFromRole(role);
|
|
483
|
+
|
|
484
|
+
if (!usageValue) {
|
|
485
|
+
if (output.strict) {
|
|
486
|
+
throw new Error(missingUsageMessage(role));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
warnInferenceOnce(
|
|
490
|
+
`usage:${role}`,
|
|
491
|
+
`Defaulting usage to "bg" for role: "${role}". ${missingUsageMessage(role)}`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!formatString(q.surface) && !inferredSurface) {
|
|
496
|
+
if (output.strict) {
|
|
497
|
+
throw new Error(missingSurfaceMessage(role));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
warnInferenceOnce(
|
|
501
|
+
`surface:${role}`,
|
|
502
|
+
`Defaulting surface to "surface" for role: "${role}". ${missingSurfaceMessage(role)}`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
role,
|
|
508
|
+
usage: assertOneOf(usageValue ?? "bg", usages, "usage"),
|
|
509
|
+
context: assertOneOf(contextValue, contexts, "context"),
|
|
510
|
+
surface: assertOneOf(surfaceValue, surfaces, "surface"),
|
|
511
|
+
state: assertOneOf(stateValue, COLOR_STATES, "state"),
|
|
512
|
+
emphasis: assertOneOf(emphasisValue, emphases, "emphasis"),
|
|
513
|
+
variant: normalizeVariant(variantValue),
|
|
514
|
+
on: normalizeBackgroundHint(q.on, output.strict),
|
|
515
|
+
contrast: normalizeContrast(q.contrast),
|
|
516
|
+
alpha: normalizeAlpha(q.alpha),
|
|
517
|
+
output,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Normalize a user-facing OnSolidQuery into a fully populated, validated structure.
|
|
523
|
+
*
|
|
524
|
+
* - Applies defaults for missing fields (context, state, emphasis, output).
|
|
525
|
+
* - Validates usage ("text" | "icon") and required background role.
|
|
526
|
+
* - Validates nested objects (contrast requirements, alpha strategies).
|
|
527
|
+
*/
|
|
528
|
+
export function normalizeOnSolidQuery(q: OnSolidQuery): NormalizedOnSolidQuery {
|
|
529
|
+
const bgRole = normalizeBgRole(formatString(q.bgRole));
|
|
530
|
+
const usageValue = formatString(q.usage);
|
|
531
|
+
const contextValue = formatString(q.context) ?? "light";
|
|
532
|
+
const stateValue = formatString(q.state) ?? "default";
|
|
533
|
+
const emphasisValue = formatString(q.emphasis) ?? "default";
|
|
534
|
+
const output = normalizeOutput(q.output);
|
|
535
|
+
|
|
536
|
+
if (!usageValue) {
|
|
537
|
+
throw new Error("On-solid usage is required (text or icon)");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
bgRole,
|
|
542
|
+
usage: assertOneOf(usageValue, onSolidUsages, "onSolid usage"),
|
|
543
|
+
context: assertOneOf(contextValue, contexts, "context"),
|
|
544
|
+
state: assertOneOf(stateValue, COLOR_STATES, "state"),
|
|
545
|
+
emphasis: assertOneOf(emphasisValue, emphases, "emphasis"),
|
|
546
|
+
contrast: normalizeContrast(q.contrast),
|
|
547
|
+
alpha: normalizeAlpha(q.alpha),
|
|
548
|
+
output,
|
|
549
|
+
};
|
|
550
|
+
}
|