@hewnui/themes 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +33 -2
- package/dist/authoring/css-vars.d.ts +10 -0
- package/dist/authoring/css-vars.js +83 -0
- package/dist/authoring/derive.d.ts +9 -0
- package/dist/authoring/derive.js +79 -0
- package/dist/authoring/export-css.d.ts +8 -0
- package/dist/authoring/export-css.js +60 -0
- package/dist/authoring/export-design-md.d.ts +5 -0
- package/dist/authoring/export-design-md.js +113 -0
- package/dist/authoring/export-dtcg.d.ts +6 -0
- package/dist/authoring/export-dtcg.js +85 -0
- package/dist/authoring/export-figma.d.ts +27 -0
- package/dist/authoring/export-figma.js +76 -0
- package/dist/authoring/export-formats.d.ts +27 -0
- package/dist/authoring/export-formats.js +62 -0
- package/dist/authoring/export-skill.d.ts +5 -0
- package/dist/authoring/export-skill.js +75 -0
- package/dist/authoring/export-sticker.d.ts +6 -0
- package/dist/authoring/export-sticker.js +54 -0
- package/dist/authoring/export-style-dictionary.d.ts +3 -0
- package/dist/authoring/export-style-dictionary.js +52 -0
- package/dist/authoring/figma-kit.d.ts +16 -0
- package/dist/authoring/figma-kit.js +41 -0
- package/dist/authoring/index.d.ts +21 -0
- package/dist/authoring/index.js +18 -0
- package/dist/authoring/oklch.d.ts +11 -0
- package/dist/authoring/oklch.js +116 -0
- package/dist/authoring/override-map.d.ts +9 -0
- package/dist/authoring/override-map.js +150 -0
- package/dist/authoring/pack-def.d.ts +7 -0
- package/dist/authoring/pack-def.js +3 -0
- package/dist/authoring/pack-meta.d.ts +10 -0
- package/dist/authoring/pack-meta.js +58 -0
- package/dist/authoring/packs-dual.d.ts +129 -0
- package/dist/authoring/packs-dual.js +130 -0
- package/dist/authoring/packs-single.d.ts +87 -0
- package/dist/authoring/packs-single.js +88 -0
- package/dist/authoring/packs.d.ts +8 -0
- package/dist/authoring/packs.js +26 -0
- package/dist/authoring/pro-apply.d.ts +8 -0
- package/dist/authoring/pro-apply.js +107 -0
- package/dist/authoring/propose.d.ts +21 -0
- package/dist/authoring/propose.js +162 -0
- package/dist/authoring/ramps.d.ts +13 -0
- package/dist/authoring/ramps.js +40 -0
- package/dist/authoring/status-ramps.d.ts +13 -0
- package/dist/authoring/status-ramps.js +76 -0
- package/dist/authoring/token-entries.d.ts +10 -0
- package/dist/authoring/token-entries.js +52 -0
- package/dist/authoring/types.d.ts +110 -0
- package/dist/authoring/types.js +11 -0
- package/exports/clay/DESIGN.md +138 -0
- package/exports/clay/SKILL.md +94 -0
- package/exports/clay/theme.css +37 -0
- package/exports/clay/tokens.dtcg.json +200 -0
- package/exports/dune/DESIGN.md +138 -0
- package/exports/dune/SKILL.md +94 -0
- package/exports/dune/theme.css +37 -0
- package/exports/dune/tokens.dtcg.json +200 -0
- package/exports/ember/DESIGN.md +138 -0
- package/exports/ember/SKILL.md +94 -0
- package/exports/ember/theme.css +37 -0
- package/exports/ember/tokens.dtcg.json +200 -0
- package/exports/forest/DESIGN.md +138 -0
- package/exports/forest/SKILL.md +94 -0
- package/exports/forest/theme.css +67 -0
- package/exports/forest/tokens.dtcg.json +348 -0
- package/exports/graphite/DESIGN.md +138 -0
- package/exports/graphite/SKILL.md +94 -0
- package/exports/graphite/theme.css +67 -0
- package/exports/graphite/tokens.dtcg.json +348 -0
- package/exports/index.json +45 -0
- package/exports/tide/DESIGN.md +138 -0
- package/exports/tide/SKILL.md +94 -0
- package/exports/tide/theme.css +67 -0
- package/exports/tide/tokens.dtcg.json +348 -0
- package/figma/README.md +16 -0
- package/figma/hewn.tokens.json +1308 -0
- package/figma/sticker-sheet.svg +93 -0
- package/package.json +77 -3
- package/src/authoring/css-vars.ts +92 -0
- package/src/authoring/derive.ts +106 -0
- package/src/authoring/export-css.ts +86 -0
- package/src/authoring/export-design-md.ts +134 -0
- package/src/authoring/export-dtcg.ts +116 -0
- package/src/authoring/export-figma.ts +109 -0
- package/src/authoring/export-formats.ts +101 -0
- package/src/authoring/export-skill.ts +93 -0
- package/src/authoring/export-sticker.ts +60 -0
- package/src/authoring/export-style-dictionary.ts +67 -0
- package/src/authoring/figma-kit.ts +60 -0
- package/src/authoring/index.ts +61 -0
- package/src/authoring/oklch.ts +137 -0
- package/src/authoring/override-map.ts +191 -0
- package/src/authoring/pack-def.ts +11 -0
- package/src/authoring/pack-meta.ts +66 -0
- package/src/authoring/packs-dual.ts +131 -0
- package/src/authoring/packs-single.ts +89 -0
- package/src/authoring/packs.ts +43 -0
- package/src/authoring/pro-apply.ts +156 -0
- package/src/authoring/propose.ts +181 -0
- package/src/authoring/ramps.ts +62 -0
- package/src/authoring/status-ramps.ts +98 -0
- package/src/authoring/token-entries.ts +59 -0
- package/src/authoring/types.ts +133 -0
- package/src/glass-buttons.css +46 -0
- package/src/glass.css +266 -0
- package/src/motion.css +60 -0
- package/src/packs/clay.css +45 -0
- package/src/packs/dune.css +45 -0
- package/src/packs/ember.css +45 -0
- package/src/packs/forest.css +80 -0
- package/src/packs/graphite.css +79 -0
- package/src/packs/tide.css +79 -0
- package/src/radius.css +25 -0
- package/src/tokens.css +104 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Oklch } from "./types";
|
|
2
|
+
|
|
3
|
+
const OKLCH_RE =
|
|
4
|
+
/^oklch\(\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)(?:\s*\/\s*([0-9.]+)%?)?\s*\)$/i;
|
|
5
|
+
const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
6
|
+
|
|
7
|
+
export function formatOklch(color: Oklch): string {
|
|
8
|
+
const { l, c, h, alpha } = color;
|
|
9
|
+
const base = `oklch(${round(l)} ${round(c)} ${round(h)}`;
|
|
10
|
+
if (alpha === undefined || alpha >= 1) return `${base})`;
|
|
11
|
+
return `${base} / ${Math.round(alpha * 100)}%)`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatHex(color: Oklch): string {
|
|
15
|
+
const [r, g, b] = oklchToSrgb(color);
|
|
16
|
+
const toByte = (channel: number) =>
|
|
17
|
+
Math.round(Math.min(1, Math.max(0, channel)) * 255);
|
|
18
|
+
return `#${[toByte(r), toByte(g), toByte(b)]
|
|
19
|
+
.map((n) => n.toString(16).padStart(2, "0"))
|
|
20
|
+
.join("")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseColor(input: string): Oklch | null {
|
|
24
|
+
const trimmed = input.trim();
|
|
25
|
+
const oklch = trimmed.match(OKLCH_RE);
|
|
26
|
+
if (oklch) {
|
|
27
|
+
return {
|
|
28
|
+
l: Number(oklch[1]),
|
|
29
|
+
c: Number(oklch[2]),
|
|
30
|
+
h: Number(oklch[3]),
|
|
31
|
+
...(oklch[4] !== undefined
|
|
32
|
+
? {
|
|
33
|
+
alpha:
|
|
34
|
+
Number(oklch[4]) > 1
|
|
35
|
+
? Number(oklch[4]) / 100
|
|
36
|
+
: Number(oklch[4]),
|
|
37
|
+
}
|
|
38
|
+
: {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const hex = trimmed.match(HEX_RE);
|
|
42
|
+
if (!hex?.[1]) return null;
|
|
43
|
+
return srgbToOklch(hexToSrgb(hex[1]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function relativeLuminance(color: Oklch): number {
|
|
47
|
+
const [r, g, b] = oklchToSrgb(color);
|
|
48
|
+
const lin = (channel: number) => {
|
|
49
|
+
const c = Math.min(1, Math.max(0, channel));
|
|
50
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
};
|
|
52
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function contrastRatio(a: Oklch, b: Oklch): number {
|
|
56
|
+
const l1 = relativeLuminance(a);
|
|
57
|
+
const l2 = relativeLuminance(b);
|
|
58
|
+
const lighter = Math.max(l1, l2);
|
|
59
|
+
const darker = Math.min(l1, l2);
|
|
60
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Pick black or white foreground that meets AA (4.5:1) against the background. */
|
|
64
|
+
export function contrastForeground(
|
|
65
|
+
background: Oklch,
|
|
66
|
+
prefer: Oklch,
|
|
67
|
+
minRatio = 4.5,
|
|
68
|
+
): { color: Oklch; adjusted: boolean } {
|
|
69
|
+
if (contrastRatio(prefer, background) >= minRatio) {
|
|
70
|
+
return { color: prefer, adjusted: false };
|
|
71
|
+
}
|
|
72
|
+
const white: Oklch = { l: 0.99, c: 0, h: background.h };
|
|
73
|
+
const black: Oklch = { l: 0.18, c: 0, h: background.h };
|
|
74
|
+
const whiteRatio = contrastRatio(white, background);
|
|
75
|
+
const blackRatio = contrastRatio(black, background);
|
|
76
|
+
return {
|
|
77
|
+
color: whiteRatio >= blackRatio ? white : black,
|
|
78
|
+
adjusted: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function round(value: number): number {
|
|
83
|
+
return Math.round(value * 1000) / 1000;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hexToSrgb(hex: string): [number, number, number] {
|
|
87
|
+
const full =
|
|
88
|
+
hex.length === 3
|
|
89
|
+
? hex
|
|
90
|
+
.split("")
|
|
91
|
+
.map((ch) => ch + ch)
|
|
92
|
+
.join("")
|
|
93
|
+
: hex;
|
|
94
|
+
const n = Number.parseInt(full, 16);
|
|
95
|
+
return [(n >> 16) / 255, ((n >> 8) & 0xff) / 255, (n & 0xff) / 255];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Compact OKLCH ↔ sRGB (Björn Ottosson). Good enough for contrast floors. */
|
|
99
|
+
function oklchToSrgb(color: Oklch): [number, number, number] {
|
|
100
|
+
const a = color.c * Math.cos((color.h * Math.PI) / 180);
|
|
101
|
+
const b = color.c * Math.sin((color.h * Math.PI) / 180);
|
|
102
|
+
const l_ = color.l + 0.3963377774 * a + 0.2158037573 * b;
|
|
103
|
+
const m_ = color.l - 0.1055613458 * a - 0.0638541728 * b;
|
|
104
|
+
const s_ = color.l - 0.0894841775 * a - 1.291485548 * b;
|
|
105
|
+
const l = l_ ** 3;
|
|
106
|
+
const m = m_ ** 3;
|
|
107
|
+
const s = s_ ** 3;
|
|
108
|
+
return [
|
|
109
|
+
+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
110
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
111
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function srgbToOklch([r, g, b]: [number, number, number]): Oklch {
|
|
116
|
+
const toLinear = (c: number) =>
|
|
117
|
+
c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
118
|
+
const rl = toLinear(r);
|
|
119
|
+
const gl = toLinear(g);
|
|
120
|
+
const bl = toLinear(b);
|
|
121
|
+
const l =
|
|
122
|
+
0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl;
|
|
123
|
+
const m =
|
|
124
|
+
0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl;
|
|
125
|
+
const s =
|
|
126
|
+
0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl;
|
|
127
|
+
const l_ = Math.cbrt(l);
|
|
128
|
+
const m_ = Math.cbrt(m);
|
|
129
|
+
const s_ = Math.cbrt(s);
|
|
130
|
+
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
131
|
+
const A = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
132
|
+
const B = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
133
|
+
const C = Math.sqrt(A * A + B * B);
|
|
134
|
+
let H = (Math.atan2(B, A) * 180) / Math.PI;
|
|
135
|
+
if (H < 0) H += 360;
|
|
136
|
+
return { l: L, c: C, h: H };
|
|
137
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BASE_PACK_IDS,
|
|
3
|
+
SCHEMA_VERSION,
|
|
4
|
+
type DensityPreset,
|
|
5
|
+
type GlassOverrides,
|
|
6
|
+
type MotionCharacter,
|
|
7
|
+
type NeutralOverrides,
|
|
8
|
+
type OverrideMap,
|
|
9
|
+
type SemanticOverrides,
|
|
10
|
+
type ShapeOverrides,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
14
|
+
const RADIUS = new Set(["sharp", "soft", "rounded", "pill"]);
|
|
15
|
+
const DENSITY = new Set(["compact", "default", "comfortable"]);
|
|
16
|
+
const MOTION = new Set(["snappy", "balanced", "soft"]);
|
|
17
|
+
|
|
18
|
+
export type NormalizeResult =
|
|
19
|
+
| { ok: true; map: OverrideMap }
|
|
20
|
+
| { ok: false; errors: string[] };
|
|
21
|
+
|
|
22
|
+
function clamp(value: number, min: number, max: number): number {
|
|
23
|
+
return Math.min(max, Math.max(min, value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readNeutrals(
|
|
27
|
+
value: unknown,
|
|
28
|
+
errors: string[],
|
|
29
|
+
): NeutralOverrides | undefined {
|
|
30
|
+
if (value === undefined) return undefined;
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
32
|
+
errors.push("neutrals must be an object");
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const record = value as Record<string, unknown>;
|
|
36
|
+
const next: NeutralOverrides = {};
|
|
37
|
+
if (record.surfaceShift !== undefined) {
|
|
38
|
+
if (typeof record.surfaceShift !== "number" || Number.isNaN(record.surfaceShift)) {
|
|
39
|
+
errors.push("neutrals.surfaceShift must be a number");
|
|
40
|
+
} else {
|
|
41
|
+
next.surfaceShift = clamp(record.surfaceShift, -0.08, 0.08);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (record.chromaScale !== undefined) {
|
|
45
|
+
if (typeof record.chromaScale !== "number" || Number.isNaN(record.chromaScale)) {
|
|
46
|
+
errors.push("neutrals.chromaScale must be a number");
|
|
47
|
+
} else {
|
|
48
|
+
next.chromaScale = clamp(record.chromaScale, 0.5, 1.5);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readSemantic(
|
|
55
|
+
value: unknown,
|
|
56
|
+
errors: string[],
|
|
57
|
+
): SemanticOverrides | undefined {
|
|
58
|
+
if (value === undefined) return undefined;
|
|
59
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
60
|
+
errors.push("semantic must be an object");
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const record = value as Record<string, unknown>;
|
|
64
|
+
if (record.dangerSeed === undefined) return undefined;
|
|
65
|
+
if (typeof record.dangerSeed !== "string") {
|
|
66
|
+
errors.push("semantic.dangerSeed must be a string");
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return { dangerSeed: record.dangerSeed };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readShape(
|
|
73
|
+
value: unknown,
|
|
74
|
+
errors: string[],
|
|
75
|
+
): ShapeOverrides | undefined {
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
78
|
+
errors.push("shape must be an object");
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const record = value as Record<string, unknown>;
|
|
82
|
+
if (record.density === undefined) return undefined;
|
|
83
|
+
if (typeof record.density !== "string" || !DENSITY.has(record.density)) {
|
|
84
|
+
errors.push("shape.density must be compact | default | comfortable");
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return { density: record.density as DensityPreset };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readGlass(
|
|
91
|
+
value: unknown,
|
|
92
|
+
errors: string[],
|
|
93
|
+
): GlassOverrides | undefined {
|
|
94
|
+
if (value === undefined) return undefined;
|
|
95
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
96
|
+
errors.push("glass must be an object");
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const record = value as Record<string, unknown>;
|
|
100
|
+
if (record.tintStrength === undefined) return undefined;
|
|
101
|
+
if (typeof record.tintStrength !== "number" || Number.isNaN(record.tintStrength)) {
|
|
102
|
+
errors.push("glass.tintStrength must be a number");
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return { tintStrength: clamp(record.tintStrength, 0.5, 1.5) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeOverrideMap(value: unknown): NormalizeResult {
|
|
109
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
110
|
+
return { ok: false, errors: ["override map must be an object"] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const record = value as Record<string, unknown>;
|
|
114
|
+
const errors: string[] = [];
|
|
115
|
+
let schemaVersion =
|
|
116
|
+
typeof record.schemaVersion === "number" ? record.schemaVersion : 0;
|
|
117
|
+
|
|
118
|
+
if (schemaVersion < 1) schemaVersion = SCHEMA_VERSION;
|
|
119
|
+
if (schemaVersion > SCHEMA_VERSION) {
|
|
120
|
+
errors.push(
|
|
121
|
+
`unsupported schemaVersion ${schemaVersion} (max ${SCHEMA_VERSION})`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
typeof record.basePackId !== "string" ||
|
|
127
|
+
!BASE_PACK_IDS.includes(record.basePackId as (typeof BASE_PACK_IDS)[number])
|
|
128
|
+
) {
|
|
129
|
+
errors.push(`basePackId must be one of: ${BASE_PACK_IDS.join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (record.packId !== undefined) {
|
|
133
|
+
if (typeof record.packId !== "string" || !KEBAB.test(record.packId)) {
|
|
134
|
+
errors.push("packId must be kebab-case");
|
|
135
|
+
} else if (
|
|
136
|
+
BASE_PACK_IDS.includes(record.packId as (typeof BASE_PACK_IDS)[number])
|
|
137
|
+
) {
|
|
138
|
+
errors.push(`packId collides with curated pack "${record.packId}"`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
record.accentSeed !== undefined &&
|
|
144
|
+
typeof record.accentSeed !== "string"
|
|
145
|
+
) {
|
|
146
|
+
errors.push("accentSeed must be a string");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
record.radiusPreset !== undefined &&
|
|
151
|
+
(typeof record.radiusPreset !== "string" ||
|
|
152
|
+
!RADIUS.has(record.radiusPreset))
|
|
153
|
+
) {
|
|
154
|
+
errors.push("radiusPreset must be sharp | soft | rounded | pill");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
record.motion !== undefined &&
|
|
159
|
+
(typeof record.motion !== "string" || !MOTION.has(record.motion))
|
|
160
|
+
) {
|
|
161
|
+
errors.push("motion must be snappy | balanced | soft");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const neutrals = readNeutrals(record.neutrals, errors);
|
|
165
|
+
const semantic = readSemantic(record.semantic, errors);
|
|
166
|
+
const shape = readShape(record.shape, errors);
|
|
167
|
+
const glass = readGlass(record.glass, errors);
|
|
168
|
+
|
|
169
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
170
|
+
|
|
171
|
+
const map: OverrideMap = {
|
|
172
|
+
schemaVersion: SCHEMA_VERSION,
|
|
173
|
+
basePackId: record.basePackId as OverrideMap["basePackId"],
|
|
174
|
+
};
|
|
175
|
+
if (typeof record.packId === "string") map.packId = record.packId;
|
|
176
|
+
if (typeof record.accentSeed === "string") map.accentSeed = record.accentSeed;
|
|
177
|
+
if (typeof record.radiusPreset === "string" && RADIUS.has(record.radiusPreset)) {
|
|
178
|
+
map.radiusPreset = record.radiusPreset as NonNullable<
|
|
179
|
+
OverrideMap["radiusPreset"]
|
|
180
|
+
>;
|
|
181
|
+
}
|
|
182
|
+
if (neutrals) map.neutrals = neutrals;
|
|
183
|
+
if (semantic) map.semantic = semantic;
|
|
184
|
+
if (shape) map.shape = shape;
|
|
185
|
+
if (glass) map.glass = glass;
|
|
186
|
+
if (typeof record.motion === "string" && MOTION.has(record.motion)) {
|
|
187
|
+
map.motion = record.motion as MotionCharacter;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { ok: true, map };
|
|
191
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CoreModeColors, Oklch, StructuralTokens, ThemeMode } from "./types";
|
|
2
|
+
|
|
3
|
+
export function o(l: number, c: number, h: number, alpha?: number): Oklch {
|
|
4
|
+
return alpha === undefined ? { l, c, h } : { l, c, h, alpha };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Curated pack definition before status families are merged. */
|
|
8
|
+
export type PackDef = {
|
|
9
|
+
modes: Partial<Record<ThemeMode, CoreModeColors>>;
|
|
10
|
+
structural: StructuralTokens;
|
|
11
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { BasePackId } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Curated pack identity copy — mirrors docs/design-hewn-v2.md. */
|
|
4
|
+
export const PACK_IDENTITY: Record<
|
|
5
|
+
BasePackId,
|
|
6
|
+
{ label: string; summary: string }
|
|
7
|
+
> = {
|
|
8
|
+
forest: {
|
|
9
|
+
label: "Forest",
|
|
10
|
+
summary: "Moss and emerald; grounded organic product UI.",
|
|
11
|
+
},
|
|
12
|
+
tide: {
|
|
13
|
+
label: "Tide",
|
|
14
|
+
summary: "Mineral neutrals and desaturated teal.",
|
|
15
|
+
},
|
|
16
|
+
graphite: {
|
|
17
|
+
label: "Graphite",
|
|
18
|
+
summary: "Technical neutral and restrained steel-blue.",
|
|
19
|
+
},
|
|
20
|
+
dune: {
|
|
21
|
+
label: "Dune",
|
|
22
|
+
summary: "Sand, ochre, soft paper surfaces.",
|
|
23
|
+
},
|
|
24
|
+
clay: {
|
|
25
|
+
label: "Clay",
|
|
26
|
+
summary: "Warm stone and terracotta.",
|
|
27
|
+
},
|
|
28
|
+
ember: {
|
|
29
|
+
label: "Ember",
|
|
30
|
+
summary: "Charcoal and burnt orange.",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const COLOR_ROLES: Record<string, string> = {
|
|
35
|
+
background: "Page and app canvas",
|
|
36
|
+
foreground: "Primary text and icons",
|
|
37
|
+
surface: "Cards, panels, and elevated chrome",
|
|
38
|
+
"surface-raised": "Overlays and nested elevation",
|
|
39
|
+
muted: "Subtle fills and disabled wells",
|
|
40
|
+
"muted-foreground": "Secondary text and captions",
|
|
41
|
+
accent: "Primary actions and focus accents",
|
|
42
|
+
"accent-foreground": "Text/icons on accent",
|
|
43
|
+
border: "Separators and control outlines",
|
|
44
|
+
ring: "Focus ring",
|
|
45
|
+
danger: "Destructive and error states",
|
|
46
|
+
"glass-tint": "Tinted frosted glass material",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Shared design rules for DESIGN.md and agent skills. */
|
|
50
|
+
export const DESIGN_DO = [
|
|
51
|
+
"Use semantic OKLCH tokens (`--kp-*` / `--color-*`); never hard-code theme hues.",
|
|
52
|
+
"Typography: IBM Plex Sans (UI), IBM Plex Mono (code/labels), Space Grotesk only for marketing hero H1 and brand wordmark ≥18px.",
|
|
53
|
+
"Icons: Phosphor. Interactive hit targets ≥40×40px; press feedback `scale(0.96)`.",
|
|
54
|
+
"Motion: animate transform, opacity, and filter; reduced motion always wins.",
|
|
55
|
+
"Radius: concentric scale; panels use `--kp-radius-panel` (never full-pill menus/dialogs).",
|
|
56
|
+
"Glass is a global material — tint, blur, saturation, refraction edge, opaque fallback.",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
export const DESIGN_DONT = [
|
|
60
|
+
"No purple/lila defaults, neon glow, pure black, Inter, emoji, or generic centered heroes.",
|
|
61
|
+
"Never `transition: all` — list exact properties.",
|
|
62
|
+
"Never `h-screen` for full-height sections — use `min-h-[100dvh]`.",
|
|
63
|
+
"No per-component `glass` prop; material is global.",
|
|
64
|
+
"Do not invent unsupported light/dark combinations for a pack.",
|
|
65
|
+
"Do not put Space Grotesk in UI chrome, forms, or dense product copy.",
|
|
66
|
+
] as const;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { o, type PackDef } from "./pack-def";
|
|
2
|
+
|
|
3
|
+
/** Dual-mode curated packs. Keep in sync with src/packs/*.css. */
|
|
4
|
+
export const DUAL_PACKS = {
|
|
5
|
+
forest: {
|
|
6
|
+
structural: {
|
|
7
|
+
radiusSm: "0.5rem",
|
|
8
|
+
radiusMd: "0.75rem",
|
|
9
|
+
radiusLg: "1rem",
|
|
10
|
+
radiusPanel: "1rem",
|
|
11
|
+
density: 1,
|
|
12
|
+
motionDuration: "260ms",
|
|
13
|
+
motionStiffness: 360,
|
|
14
|
+
motionDamping: 28,
|
|
15
|
+
},
|
|
16
|
+
modes: {
|
|
17
|
+
light: {
|
|
18
|
+
background: o(0.972, 0.012, 145),
|
|
19
|
+
foreground: o(0.225, 0.028, 145),
|
|
20
|
+
surface: o(0.995, 0.004, 145),
|
|
21
|
+
surfaceRaised: o(1, 0, 0),
|
|
22
|
+
muted: o(0.925, 0.022, 145),
|
|
23
|
+
mutedForeground: o(0.49, 0.035, 145),
|
|
24
|
+
accent: o(0.49, 0.105, 148),
|
|
25
|
+
accentForeground: o(0.985, 0.008, 145),
|
|
26
|
+
border: o(0.36, 0.035, 145, 0.14),
|
|
27
|
+
ring: o(0.55, 0.12, 148),
|
|
28
|
+
danger: o(0.56, 0.17, 27),
|
|
29
|
+
glassTint: o(0.72, 0.12, 148, 0.28),
|
|
30
|
+
},
|
|
31
|
+
dark: {
|
|
32
|
+
background: o(0.185, 0.018, 145),
|
|
33
|
+
foreground: o(0.93, 0.018, 145),
|
|
34
|
+
surface: o(0.225, 0.024, 145),
|
|
35
|
+
surfaceRaised: o(0.265, 0.028, 145),
|
|
36
|
+
muted: o(0.285, 0.026, 145),
|
|
37
|
+
mutedForeground: o(0.715, 0.03, 145),
|
|
38
|
+
accent: o(0.71, 0.12, 148),
|
|
39
|
+
accentForeground: o(0.985, 0.01, 145),
|
|
40
|
+
border: o(0.86, 0.025, 145, 0.13),
|
|
41
|
+
ring: o(0.71, 0.12, 148),
|
|
42
|
+
danger: o(0.68, 0.16, 27),
|
|
43
|
+
glassTint: o(0.63, 0.13, 148, 0.24),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
tide: {
|
|
48
|
+
structural: {
|
|
49
|
+
radiusSm: "0.45rem",
|
|
50
|
+
radiusMd: "0.7rem",
|
|
51
|
+
radiusLg: "0.95rem",
|
|
52
|
+
radiusPanel: "0.95rem",
|
|
53
|
+
density: 1,
|
|
54
|
+
motionDuration: "240ms",
|
|
55
|
+
motionStiffness: 380,
|
|
56
|
+
motionDamping: 30,
|
|
57
|
+
},
|
|
58
|
+
modes: {
|
|
59
|
+
light: {
|
|
60
|
+
background: o(0.97, 0.01, 220),
|
|
61
|
+
foreground: o(0.24, 0.03, 230),
|
|
62
|
+
surface: o(0.995, 0.004, 220),
|
|
63
|
+
surfaceRaised: o(1, 0, 0),
|
|
64
|
+
muted: o(0.93, 0.015, 220),
|
|
65
|
+
mutedForeground: o(0.48, 0.03, 230),
|
|
66
|
+
accent: o(0.52, 0.07, 210),
|
|
67
|
+
accentForeground: o(0.98, 0.01, 210),
|
|
68
|
+
border: o(0.35, 0.03, 230, 0.14),
|
|
69
|
+
ring: o(0.58, 0.08, 210),
|
|
70
|
+
danger: o(0.56, 0.17, 27),
|
|
71
|
+
glassTint: o(0.72, 0.08, 210, 0.28),
|
|
72
|
+
},
|
|
73
|
+
dark: {
|
|
74
|
+
background: o(0.18, 0.02, 230),
|
|
75
|
+
foreground: o(0.92, 0.015, 220),
|
|
76
|
+
surface: o(0.22, 0.025, 230),
|
|
77
|
+
surfaceRaised: o(0.26, 0.028, 230),
|
|
78
|
+
muted: o(0.28, 0.025, 230),
|
|
79
|
+
mutedForeground: o(0.72, 0.025, 220),
|
|
80
|
+
accent: o(0.72, 0.08, 205),
|
|
81
|
+
accentForeground: o(0.985, 0.01, 220),
|
|
82
|
+
border: o(0.86, 0.02, 220, 0.13),
|
|
83
|
+
ring: o(0.72, 0.08, 205),
|
|
84
|
+
danger: o(0.68, 0.16, 27),
|
|
85
|
+
glassTint: o(0.6, 0.09, 205, 0.24),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
graphite: {
|
|
90
|
+
structural: {
|
|
91
|
+
radiusSm: "0.4rem",
|
|
92
|
+
radiusMd: "0.6rem",
|
|
93
|
+
radiusLg: "0.85rem",
|
|
94
|
+
radiusPanel: "0.85rem",
|
|
95
|
+
density: 1.05,
|
|
96
|
+
motionDuration: "220ms",
|
|
97
|
+
motionStiffness: 420,
|
|
98
|
+
motionDamping: 32,
|
|
99
|
+
},
|
|
100
|
+
modes: {
|
|
101
|
+
light: {
|
|
102
|
+
background: o(0.965, 0.005, 255),
|
|
103
|
+
foreground: o(0.23, 0.02, 255),
|
|
104
|
+
surface: o(0.99, 0.003, 255),
|
|
105
|
+
surfaceRaised: o(1, 0, 0),
|
|
106
|
+
muted: o(0.925, 0.008, 255),
|
|
107
|
+
mutedForeground: o(0.47, 0.02, 255),
|
|
108
|
+
accent: o(0.48, 0.06, 250),
|
|
109
|
+
accentForeground: o(0.98, 0.01, 250),
|
|
110
|
+
border: o(0.34, 0.02, 255, 0.14),
|
|
111
|
+
ring: o(0.55, 0.07, 250),
|
|
112
|
+
danger: o(0.55, 0.17, 27),
|
|
113
|
+
glassTint: o(0.7, 0.06, 250, 0.26),
|
|
114
|
+
},
|
|
115
|
+
dark: {
|
|
116
|
+
background: o(0.17, 0.012, 255),
|
|
117
|
+
foreground: o(0.93, 0.01, 255),
|
|
118
|
+
surface: o(0.21, 0.015, 255),
|
|
119
|
+
surfaceRaised: o(0.25, 0.018, 255),
|
|
120
|
+
muted: o(0.27, 0.016, 255),
|
|
121
|
+
mutedForeground: o(0.715, 0.015, 255),
|
|
122
|
+
accent: o(0.7, 0.07, 245),
|
|
123
|
+
accentForeground: o(0.985, 0.008, 255),
|
|
124
|
+
border: o(0.85, 0.012, 255, 0.12),
|
|
125
|
+
ring: o(0.7, 0.07, 245),
|
|
126
|
+
danger: o(0.68, 0.16, 27),
|
|
127
|
+
glassTint: o(0.58, 0.07, 245, 0.22),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
} as const satisfies Record<string, PackDef>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { o, type PackDef } from "./pack-def";
|
|
2
|
+
|
|
3
|
+
/** Single-mode curated packs. Keep in sync with src/packs/*.css. */
|
|
4
|
+
export const SINGLE_PACKS = {
|
|
5
|
+
dune: {
|
|
6
|
+
structural: {
|
|
7
|
+
radiusSm: "0.55rem",
|
|
8
|
+
radiusMd: "0.85rem",
|
|
9
|
+
radiusLg: "1.15rem",
|
|
10
|
+
radiusPanel: "1.15rem",
|
|
11
|
+
density: 0.95,
|
|
12
|
+
motionDuration: "280ms",
|
|
13
|
+
motionStiffness: 300,
|
|
14
|
+
motionDamping: 26,
|
|
15
|
+
},
|
|
16
|
+
modes: {
|
|
17
|
+
light: {
|
|
18
|
+
background: o(0.96, 0.02, 85),
|
|
19
|
+
foreground: o(0.28, 0.04, 65),
|
|
20
|
+
surface: o(0.985, 0.012, 90),
|
|
21
|
+
surfaceRaised: o(0.995, 0.006, 95),
|
|
22
|
+
muted: o(0.92, 0.025, 85),
|
|
23
|
+
mutedForeground: o(0.5, 0.04, 70),
|
|
24
|
+
accent: o(0.58, 0.11, 70),
|
|
25
|
+
accentForeground: o(0.98, 0.01, 90),
|
|
26
|
+
border: o(0.4, 0.04, 70, 0.16),
|
|
27
|
+
ring: o(0.62, 0.12, 70),
|
|
28
|
+
danger: o(0.55, 0.16, 30),
|
|
29
|
+
glassTint: o(0.78, 0.09, 80, 0.3),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
clay: {
|
|
34
|
+
structural: {
|
|
35
|
+
radiusSm: "0.5rem",
|
|
36
|
+
radiusMd: "0.8rem",
|
|
37
|
+
radiusLg: "1.05rem",
|
|
38
|
+
radiusPanel: "1.05rem",
|
|
39
|
+
density: 1,
|
|
40
|
+
motionDuration: "270ms",
|
|
41
|
+
motionStiffness: 320,
|
|
42
|
+
motionDamping: 27,
|
|
43
|
+
},
|
|
44
|
+
modes: {
|
|
45
|
+
light: {
|
|
46
|
+
background: o(0.955, 0.02, 55),
|
|
47
|
+
foreground: o(0.27, 0.045, 40),
|
|
48
|
+
surface: o(0.98, 0.012, 50),
|
|
49
|
+
surfaceRaised: o(0.995, 0.006, 45),
|
|
50
|
+
muted: o(0.915, 0.025, 50),
|
|
51
|
+
mutedForeground: o(0.5, 0.045, 40),
|
|
52
|
+
accent: o(0.55, 0.12, 40),
|
|
53
|
+
accentForeground: o(0.98, 0.01, 50),
|
|
54
|
+
border: o(0.4, 0.05, 40, 0.16),
|
|
55
|
+
ring: o(0.6, 0.13, 40),
|
|
56
|
+
danger: o(0.54, 0.17, 28),
|
|
57
|
+
glassTint: o(0.74, 0.1, 45, 0.28),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
ember: {
|
|
62
|
+
structural: {
|
|
63
|
+
radiusSm: "0.45rem",
|
|
64
|
+
radiusMd: "0.7rem",
|
|
65
|
+
radiusLg: "0.95rem",
|
|
66
|
+
radiusPanel: "0.95rem",
|
|
67
|
+
density: 1.05,
|
|
68
|
+
motionDuration: "230ms",
|
|
69
|
+
motionStiffness: 400,
|
|
70
|
+
motionDamping: 28,
|
|
71
|
+
},
|
|
72
|
+
modes: {
|
|
73
|
+
dark: {
|
|
74
|
+
background: o(0.17, 0.02, 40),
|
|
75
|
+
foreground: o(0.93, 0.02, 70),
|
|
76
|
+
surface: o(0.21, 0.025, 40),
|
|
77
|
+
surfaceRaised: o(0.255, 0.03, 42),
|
|
78
|
+
muted: o(0.275, 0.028, 40),
|
|
79
|
+
mutedForeground: o(0.72, 0.03, 60),
|
|
80
|
+
accent: o(0.7, 0.15, 50),
|
|
81
|
+
accentForeground: o(0.985, 0.015, 50),
|
|
82
|
+
border: o(0.84, 0.03, 60, 0.14),
|
|
83
|
+
ring: o(0.7, 0.15, 50),
|
|
84
|
+
danger: o(0.66, 0.17, 28),
|
|
85
|
+
glassTint: o(0.58, 0.15, 50, 0.28),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
} as const satisfies Record<string, PackDef>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PackDef } from "./pack-def";
|
|
2
|
+
import { DUAL_PACKS } from "./packs-dual";
|
|
3
|
+
import { SINGLE_PACKS } from "./packs-single";
|
|
4
|
+
import { PACK_STATUS_HUES, statusColors } from "./status-ramps";
|
|
5
|
+
import type {
|
|
6
|
+
BasePackId,
|
|
7
|
+
ModeColorTokens,
|
|
8
|
+
StructuralTokens,
|
|
9
|
+
ThemeMode,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export type ResolvedPackDef = {
|
|
13
|
+
modes: Partial<Record<ThemeMode, ModeColorTokens>>;
|
|
14
|
+
structural: StructuralTokens;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function withStatus(id: BasePackId, def: PackDef): ResolvedPackDef {
|
|
18
|
+
const hues = PACK_STATUS_HUES[id];
|
|
19
|
+
const modes: Partial<Record<ThemeMode, ModeColorTokens>> = {};
|
|
20
|
+
for (const [mode, colors] of Object.entries(def.modes) as Array<
|
|
21
|
+
[ThemeMode, NonNullable<PackDef["modes"][ThemeMode]>]
|
|
22
|
+
>) {
|
|
23
|
+
modes[mode] = {
|
|
24
|
+
...colors,
|
|
25
|
+
...statusColors(mode, colors.danger, hues),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { structural: def.structural, modes };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Curated pack color bases (mirrors CSS packs), including status families. */
|
|
32
|
+
export const BASE_PACKS: Record<BasePackId, ResolvedPackDef> = {
|
|
33
|
+
forest: withStatus("forest", DUAL_PACKS.forest),
|
|
34
|
+
tide: withStatus("tide", DUAL_PACKS.tide),
|
|
35
|
+
graphite: withStatus("graphite", DUAL_PACKS.graphite),
|
|
36
|
+
dune: withStatus("dune", SINGLE_PACKS.dune),
|
|
37
|
+
clay: withStatus("clay", SINGLE_PACKS.clay),
|
|
38
|
+
ember: withStatus("ember", SINGLE_PACKS.ember),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function supportedModes(packId: BasePackId): ThemeMode[] {
|
|
42
|
+
return Object.keys(BASE_PACKS[packId].modes) as ThemeMode[];
|
|
43
|
+
}
|