@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,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
colorEntries,
|
|
3
|
+
modeOrder,
|
|
4
|
+
primaryMode,
|
|
5
|
+
structuralEntries,
|
|
6
|
+
} from "./token-entries";
|
|
7
|
+
import type { ResolvedMode, ResolvedPack } from "./types";
|
|
8
|
+
|
|
9
|
+
type DtcgLeaf = {
|
|
10
|
+
$type: string;
|
|
11
|
+
$value: string | number | string[];
|
|
12
|
+
$description?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type DtcgNode = { [key: string]: DtcgNode | DtcgLeaf };
|
|
16
|
+
|
|
17
|
+
function leaf(
|
|
18
|
+
type: string,
|
|
19
|
+
value: string | number | string[],
|
|
20
|
+
description?: string,
|
|
21
|
+
): DtcgLeaf {
|
|
22
|
+
return description
|
|
23
|
+
? { $type: type, $value: value, $description: description }
|
|
24
|
+
: { $type: type, $value: value };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function modeColors(mode: ResolvedMode): DtcgNode {
|
|
28
|
+
const color: DtcgNode = {};
|
|
29
|
+
for (const entry of colorEntries(mode)) {
|
|
30
|
+
color[entry.name] = leaf("color", entry.value);
|
|
31
|
+
}
|
|
32
|
+
return color;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function structuralGroup(mode: ResolvedMode): DtcgNode {
|
|
36
|
+
const radius: DtcgNode = {};
|
|
37
|
+
const motion: DtcgNode = {};
|
|
38
|
+
let density: DtcgLeaf | undefined;
|
|
39
|
+
|
|
40
|
+
for (const entry of structuralEntries(mode)) {
|
|
41
|
+
if (entry.name.startsWith("radius-")) {
|
|
42
|
+
radius[entry.name.replace(/^radius-/, "")] = leaf(
|
|
43
|
+
"dimension",
|
|
44
|
+
entry.value,
|
|
45
|
+
);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (entry.name.startsWith("motion-")) {
|
|
49
|
+
const key = entry.name.replace(/^motion-/, "");
|
|
50
|
+
motion[key] = leaf(
|
|
51
|
+
entry.kind === "number" ? "number" : "duration",
|
|
52
|
+
entry.kind === "number" ? Number(entry.value) : entry.value,
|
|
53
|
+
);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (entry.name === "density") {
|
|
57
|
+
density = leaf("number", Number(entry.value));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
radius,
|
|
63
|
+
motion,
|
|
64
|
+
...(density ? { density } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Free — W3C Design Tokens Community Group JSON (`$value` / `$type`).
|
|
70
|
+
* Modes live under `modes.light` / `modes.dark` when supported.
|
|
71
|
+
*/
|
|
72
|
+
export function exportDtcg(pack: ResolvedPack): string {
|
|
73
|
+
const modes: DtcgNode = {};
|
|
74
|
+
for (const modeName of modeOrder(pack)) {
|
|
75
|
+
const mode = pack.modes[modeName];
|
|
76
|
+
if (!mode) continue;
|
|
77
|
+
modes[modeName] = {
|
|
78
|
+
color: modeColors(mode),
|
|
79
|
+
...structuralGroup(mode),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const primary = primaryMode(pack);
|
|
84
|
+
const doc = {
|
|
85
|
+
$description: `Hewn ${pack.id} theme — DTCG design tokens (${modeOrder(pack).join(" / ")}).`,
|
|
86
|
+
meta: {
|
|
87
|
+
packId: leaf("string", pack.id),
|
|
88
|
+
basePackId: leaf("string", pack.basePackId),
|
|
89
|
+
supportedModes: leaf("string", modeOrder(pack).join(",")),
|
|
90
|
+
colorScheme: leaf("string", primary.colorScheme),
|
|
91
|
+
},
|
|
92
|
+
typography: {
|
|
93
|
+
fontFamily: {
|
|
94
|
+
sans: leaf("fontFamily", [
|
|
95
|
+
"IBM Plex Sans",
|
|
96
|
+
"ui-sans-serif",
|
|
97
|
+
"system-ui",
|
|
98
|
+
"sans-serif",
|
|
99
|
+
]),
|
|
100
|
+
mono: leaf("fontFamily", [
|
|
101
|
+
"IBM Plex Mono",
|
|
102
|
+
"ui-monospace",
|
|
103
|
+
"monospace",
|
|
104
|
+
]),
|
|
105
|
+
display: leaf("fontFamily", [
|
|
106
|
+
"Space Grotesk",
|
|
107
|
+
"IBM Plex Sans",
|
|
108
|
+
"sans-serif",
|
|
109
|
+
]),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
modes,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
colorEntries,
|
|
3
|
+
modeOrder,
|
|
4
|
+
primaryMode,
|
|
5
|
+
structuralEntries,
|
|
6
|
+
} from "./token-entries";
|
|
7
|
+
import type { ResolvedPack } from "./types";
|
|
8
|
+
|
|
9
|
+
type TsToken = {
|
|
10
|
+
value: string | number;
|
|
11
|
+
type: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type TsSet = { [key: string]: TsToken | TsSet };
|
|
16
|
+
|
|
17
|
+
type TsTheme = {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
selectedTokenSets: Record<string, "enabled" | "source">;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function nest(set: TsSet, path: string[], token: TsToken) {
|
|
24
|
+
let cursor: TsSet = set;
|
|
25
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
26
|
+
const key = path[i]!;
|
|
27
|
+
const existing = cursor[key];
|
|
28
|
+
if (!existing || typeof existing !== "object" || "value" in existing) {
|
|
29
|
+
cursor[key] = {};
|
|
30
|
+
}
|
|
31
|
+
cursor = cursor[key] as TsSet;
|
|
32
|
+
}
|
|
33
|
+
cursor[path[path.length - 1]!] = token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Build Tokens Studio token sets + themes for one or more packs. */
|
|
37
|
+
export function buildFigmaDocument(packs: readonly ResolvedPack[]): {
|
|
38
|
+
tokenSets: Record<string, TsSet>;
|
|
39
|
+
themes: TsTheme[];
|
|
40
|
+
} {
|
|
41
|
+
const tokenSets: Record<string, TsSet> = {};
|
|
42
|
+
const themes: TsTheme[] = [];
|
|
43
|
+
|
|
44
|
+
for (const pack of packs) {
|
|
45
|
+
const structural: TsSet = {};
|
|
46
|
+
const primary = primaryMode(pack);
|
|
47
|
+
for (const entry of structuralEntries(primary)) {
|
|
48
|
+
nest(structural, [entry.name], {
|
|
49
|
+
value: entry.kind === "number" ? Number(entry.value) : entry.value,
|
|
50
|
+
type: entry.kind === "number" ? "number" : "sizing",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
tokenSets[`${pack.id}/structural`] = structural;
|
|
54
|
+
|
|
55
|
+
for (const modeName of modeOrder(pack)) {
|
|
56
|
+
const mode = pack.modes[modeName];
|
|
57
|
+
if (!mode) continue;
|
|
58
|
+
const setName = `${pack.id}/${modeName}`;
|
|
59
|
+
const set: TsSet = {};
|
|
60
|
+
for (const entry of colorEntries(mode)) {
|
|
61
|
+
nest(set, ["color", entry.name], {
|
|
62
|
+
value: entry.value,
|
|
63
|
+
type: "color",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
tokenSets[setName] = set;
|
|
67
|
+
themes.push({
|
|
68
|
+
id: `${pack.id}-${modeName}`,
|
|
69
|
+
name: `${pack.id} / ${modeName}`,
|
|
70
|
+
selectedTokenSets: {
|
|
71
|
+
[`${pack.id}/structural`]: "source",
|
|
72
|
+
[setName]: "enabled",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { tokenSets, themes };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stringifyFigmaDoc(
|
|
82
|
+
tokenSets: Record<string, TsSet>,
|
|
83
|
+
themes: TsTheme[],
|
|
84
|
+
): string {
|
|
85
|
+
const doc = {
|
|
86
|
+
$metadata: {
|
|
87
|
+
tokenSetOrder: Object.keys(tokenSets),
|
|
88
|
+
activeThemeEditor: themes[0]?.id ?? null,
|
|
89
|
+
},
|
|
90
|
+
$themes: themes,
|
|
91
|
+
...tokenSets,
|
|
92
|
+
};
|
|
93
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pro — Tokens Studio–compatible multi-set export for one authored pack.
|
|
98
|
+
* Import via Tokens Studio → Figma variables.
|
|
99
|
+
*/
|
|
100
|
+
export function exportFigmaVariables(pack: ResolvedPack): string {
|
|
101
|
+
const { tokenSets, themes } = buildFigmaDocument([pack]);
|
|
102
|
+
return stringifyFigmaDoc(tokenSets, themes);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Curated kit — all packs in one Tokens Studio document. */
|
|
106
|
+
export function exportFigmaKit(packs: readonly ResolvedPack[]): string {
|
|
107
|
+
const { tokenSets, themes } = buildFigmaDocument(packs);
|
|
108
|
+
return stringifyFigmaDoc(tokenSets, themes);
|
|
109
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { exportDesignMd } from "./export-design-md";
|
|
2
|
+
import { exportDtcg } from "./export-dtcg";
|
|
3
|
+
import { exportTailwindTheme } from "./export-css";
|
|
4
|
+
import { exportSkillMd } from "./export-skill";
|
|
5
|
+
import type { ResolvedPack } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Export tier marker — today every curated Theme Lab format is `free`.
|
|
9
|
+
* Advanced Studio formats (Style Dictionary, Figma) can flip to `pro` later
|
|
10
|
+
* without changing call sites; consumers must not gate on this yet.
|
|
11
|
+
*/
|
|
12
|
+
export type ExportTier = "free" | "pro";
|
|
13
|
+
|
|
14
|
+
export type ThemeExportFormatId =
|
|
15
|
+
| "design-md"
|
|
16
|
+
| "dtcg"
|
|
17
|
+
| "tailwind-theme"
|
|
18
|
+
| "skill-md";
|
|
19
|
+
|
|
20
|
+
export type ThemeExportFormat = {
|
|
21
|
+
id: ThemeExportFormatId;
|
|
22
|
+
/** Short UI label */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Accessible description for buttons / docs */
|
|
25
|
+
description: string;
|
|
26
|
+
/** Suggested download filename (pack id substituted) */
|
|
27
|
+
filename: (packId: string) => string;
|
|
28
|
+
mime: string;
|
|
29
|
+
tier: ExportTier;
|
|
30
|
+
render: (pack: ResolvedPack) => string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Free curated Theme Lab / docs site formats. */
|
|
34
|
+
export const THEME_EXPORT_FORMATS: readonly ThemeExportFormat[] = [
|
|
35
|
+
{
|
|
36
|
+
id: "design-md",
|
|
37
|
+
label: "DESIGN.md",
|
|
38
|
+
description: "Human- and AI-readable design brief with OKLCH roles and rules",
|
|
39
|
+
filename: (packId) => `${packId}.DESIGN.md`,
|
|
40
|
+
mime: "text/markdown;charset=utf-8",
|
|
41
|
+
tier: "free",
|
|
42
|
+
render: exportDesignMd,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "dtcg",
|
|
46
|
+
label: "DTCG tokens",
|
|
47
|
+
description: "W3C Design Tokens JSON ($value / $type) with light and dark modes",
|
|
48
|
+
filename: (packId) => `${packId}.tokens.dtcg.json`,
|
|
49
|
+
mime: "application/json",
|
|
50
|
+
tier: "free",
|
|
51
|
+
render: exportDtcg,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "tailwind-theme",
|
|
55
|
+
label: "Tailwind @theme",
|
|
56
|
+
description: "Tailwind CSS v4 @theme file with mode remaps",
|
|
57
|
+
filename: (packId) => `${packId}.theme.css`,
|
|
58
|
+
mime: "text/css;charset=utf-8",
|
|
59
|
+
tier: "free",
|
|
60
|
+
render: exportTailwindTheme,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "skill-md",
|
|
64
|
+
label: "Agent skill",
|
|
65
|
+
description: "SKILL.md instructions for coding agents using this theme",
|
|
66
|
+
filename: (packId) => `${packId}.SKILL.md`,
|
|
67
|
+
mime: "text/markdown;charset=utf-8",
|
|
68
|
+
tier: "free",
|
|
69
|
+
render: exportSkillMd,
|
|
70
|
+
},
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
export function getThemeExportFormat(
|
|
74
|
+
id: ThemeExportFormatId,
|
|
75
|
+
): ThemeExportFormat {
|
|
76
|
+
const format = THEME_EXPORT_FORMATS.find((entry) => entry.id === id);
|
|
77
|
+
if (!format) throw new Error(`Unknown theme export format: ${id}`);
|
|
78
|
+
return format;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function renderThemeExport(
|
|
82
|
+
id: ThemeExportFormatId,
|
|
83
|
+
pack: ResolvedPack,
|
|
84
|
+
): string {
|
|
85
|
+
return getThemeExportFormat(id).render(pack);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const THEME_EXPORT_BASENAMES: Record<ThemeExportFormatId, string> = {
|
|
89
|
+
"design-md": "DESIGN.md",
|
|
90
|
+
dtcg: "tokens.dtcg.json",
|
|
91
|
+
"tailwind-theme": "theme.css",
|
|
92
|
+
"skill-md": "SKILL.md",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Static public path for a generated curated export. */
|
|
96
|
+
export function themeExportPublicPath(
|
|
97
|
+
packId: string,
|
|
98
|
+
id: ThemeExportFormatId,
|
|
99
|
+
): string {
|
|
100
|
+
return `/themes/${packId}/${THEME_EXPORT_BASENAMES[id]}`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COLOR_ROLES,
|
|
3
|
+
DESIGN_DO,
|
|
4
|
+
DESIGN_DONT,
|
|
5
|
+
PACK_IDENTITY,
|
|
6
|
+
} from "./pack-meta";
|
|
7
|
+
import {
|
|
8
|
+
colorEntries,
|
|
9
|
+
modeOrder,
|
|
10
|
+
primaryMode,
|
|
11
|
+
structuralEntries,
|
|
12
|
+
} from "./token-entries";
|
|
13
|
+
import type { BasePackId, ResolvedPack } from "./types";
|
|
14
|
+
|
|
15
|
+
function identity(pack: ResolvedPack) {
|
|
16
|
+
return (
|
|
17
|
+
PACK_IDENTITY[pack.basePackId as BasePackId] ?? {
|
|
18
|
+
label: pack.id,
|
|
19
|
+
summary: `Custom pack based on ${pack.basePackId}.`,
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Free — Cursor-style agent skill (SKILL.md) for applying this theme correctly.
|
|
26
|
+
*/
|
|
27
|
+
export function exportSkillMd(pack: ResolvedPack): string {
|
|
28
|
+
const { label, summary } = identity(pack);
|
|
29
|
+
const modes = modeOrder(pack);
|
|
30
|
+
const mode = primaryMode(pack);
|
|
31
|
+
const skillName = `hewn-${pack.id}-theme`;
|
|
32
|
+
|
|
33
|
+
const colorLines = colorEntries(mode)
|
|
34
|
+
.map((e) => `| \`--kp-${e.name}\` / \`--color-${e.name}\` | ${COLOR_ROLES[e.name] ?? e.name} | \`${e.value}\` |`)
|
|
35
|
+
.join("\n");
|
|
36
|
+
|
|
37
|
+
const structuralLines = structuralEntries(mode)
|
|
38
|
+
.map((e) => `- \`--kp-${e.name}\`: \`${e.value}\``)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
return `---
|
|
42
|
+
name: ${skillName}
|
|
43
|
+
description: Apply the Hewn ${label} theme tokens, typography, motion, and design rules when building or restyling UI.
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# ${label} theme (Hewn)
|
|
47
|
+
|
|
48
|
+
${summary}
|
|
49
|
+
|
|
50
|
+
Use this skill whenever generating UI that should match the **${label}** pack
|
|
51
|
+
(\`${pack.id}\`). Supported modes: **${modes.join(", ")}**.
|
|
52
|
+
|
|
53
|
+
## Quick rules
|
|
54
|
+
|
|
55
|
+
${DESIGN_DO.map((item) => `- ${item}`).join("\n")}
|
|
56
|
+
|
|
57
|
+
## Never
|
|
58
|
+
|
|
59
|
+
${DESIGN_DONT.map((item) => `- ${item}`).join("\n")}
|
|
60
|
+
|
|
61
|
+
## How to apply
|
|
62
|
+
|
|
63
|
+
1. Set \`data-theme="${pack.id}"\` and \`data-mode="{${modes.join("|")}}"\` on a root element (or import the generated \`@theme\` CSS).
|
|
64
|
+
2. Prefer semantic utilities / CSS variables (\`bg-background\`, \`text-foreground\`, \`bg-accent\`, \`border-border\`, \`--kp-*\`).
|
|
65
|
+
3. Respect reduced motion — disable springs/shimmers when \`prefers-reduced-motion: reduce\`.
|
|
66
|
+
4. Keep glass as a global material toggle; do not add per-component glass props.
|
|
67
|
+
|
|
68
|
+
## Color tokens (primary mode: ${mode.mode})
|
|
69
|
+
|
|
70
|
+
| Token | Role | Value |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
${colorLines}
|
|
73
|
+
|
|
74
|
+
## Structure
|
|
75
|
+
|
|
76
|
+
${structuralLines}
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
- **Sans:** IBM Plex Sans — UI and body
|
|
81
|
+
- **Mono:** IBM Plex Mono — labels, code, metrics (\`tabular-nums\`)
|
|
82
|
+
- **Display:** Space Grotesk — hero H1 / brand wordmark ≥18px only
|
|
83
|
+
|
|
84
|
+
## Export companions
|
|
85
|
+
|
|
86
|
+
- \`DESIGN.md\` — full design brief
|
|
87
|
+
- \`tokens.dtcg.json\` — DTCG (\`$value\` / \`$type\`) with light/dark modes
|
|
88
|
+
- \`theme.css\` — Tailwind v4 \`@theme\` block
|
|
89
|
+
|
|
90
|
+
When unsure, re-read \`DESIGN.md\` for this pack and keep accents within the
|
|
91
|
+
curated ramp — do not invent a new hue family.
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { formatOklch } from "./oklch";
|
|
2
|
+
import { primaryMode } from "./token-entries";
|
|
3
|
+
import type { ResolvedPack } from "./types";
|
|
4
|
+
|
|
5
|
+
const ROW_H = 96;
|
|
6
|
+
const PAD = 24;
|
|
7
|
+
const COL_W = 120;
|
|
8
|
+
const LABEL_W = 88;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Core sticker sheet SVG — surfaces, button, and input per pack.
|
|
12
|
+
* Use as a Figma community kit reference (paste or import SVG).
|
|
13
|
+
*/
|
|
14
|
+
export function exportStickerSheetSvg(packs: readonly ResolvedPack[]): string {
|
|
15
|
+
const width = PAD * 2 + LABEL_W + COL_W * 4;
|
|
16
|
+
const height = PAD * 2 + 40 + packs.length * ROW_H;
|
|
17
|
+
const rows = packs
|
|
18
|
+
.map((pack, index) => renderRow(pack, PAD + 40 + index * ROW_H))
|
|
19
|
+
.join("\n");
|
|
20
|
+
|
|
21
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="Hewn sticker sheet">
|
|
23
|
+
<rect width="100%" height="100%" fill="#f4f4f2"/>
|
|
24
|
+
<text x="${PAD}" y="${PAD + 16}" font-family="IBM Plex Sans, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="600" fill="#1a1a18">Hewn · sticker sheet</text>
|
|
25
|
+
<text x="${PAD + LABEL_W}" y="${PAD + 34}" font-family="IBM Plex Mono, ui-monospace, monospace" font-size="10" fill="#6b6b66">surface</text>
|
|
26
|
+
<text x="${PAD + LABEL_W + COL_W}" y="${PAD + 34}" font-family="IBM Plex Mono, ui-monospace, monospace" font-size="10" fill="#6b6b66">raised</text>
|
|
27
|
+
<text x="${PAD + LABEL_W + COL_W * 2}" y="${PAD + 34}" font-family="IBM Plex Mono, ui-monospace, monospace" font-size="10" fill="#6b6b66">button</text>
|
|
28
|
+
<text x="${PAD + LABEL_W + COL_W * 3}" y="${PAD + 34}" font-family="IBM Plex Mono, ui-monospace, monospace" font-size="10" fill="#6b6b66">input</text>
|
|
29
|
+
${rows}
|
|
30
|
+
</svg>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderRow(pack: ResolvedPack, y: number): string {
|
|
35
|
+
const mode = primaryMode(pack);
|
|
36
|
+
const bg = formatOklch(mode.background);
|
|
37
|
+
const surface = formatOklch(mode.surface);
|
|
38
|
+
const raised = formatOklch(mode.surfaceRaised);
|
|
39
|
+
const accent = formatOklch(mode.accent);
|
|
40
|
+
const accentFg = formatOklch(mode.accentForeground);
|
|
41
|
+
const fg = formatOklch(mode.foreground);
|
|
42
|
+
const border = formatOklch(mode.border);
|
|
43
|
+
const muted = formatOklch(mode.mutedForeground);
|
|
44
|
+
const x0 = PAD + LABEL_W;
|
|
45
|
+
|
|
46
|
+
return ` <g>
|
|
47
|
+
<text x="${PAD}" y="${y + 42}" font-family="IBM Plex Sans, ui-sans-serif, system-ui, sans-serif" font-size="13" fill="#1a1a18">${pack.id}</text>
|
|
48
|
+
<rect x="${x0}" y="${y}" width="100" height="72" rx="10" fill="${bg}" stroke="${border}" stroke-width="1"/>
|
|
49
|
+
<rect x="${x0 + 12}" y="${y + 14}" width="76" height="44" rx="8" fill="${surface}" stroke="${border}" stroke-width="1"/>
|
|
50
|
+
<rect x="${x0 + COL_W}" y="${y}" width="100" height="72" rx="10" fill="${bg}" stroke="${border}" stroke-width="1"/>
|
|
51
|
+
<rect x="${x0 + COL_W + 12}" y="${y + 14}" width="76" height="44" rx="8" fill="${raised}" stroke="${border}" stroke-width="1"/>
|
|
52
|
+
<rect x="${x0 + COL_W * 2}" y="${y}" width="100" height="72" rx="10" fill="${bg}" stroke="${border}" stroke-width="1"/>
|
|
53
|
+
<rect x="${x0 + COL_W * 2 + 14}" y="${y + 22}" width="72" height="28" rx="8" fill="${accent}"/>
|
|
54
|
+
<text x="${x0 + COL_W * 2 + 50}" y="${y + 40}" text-anchor="middle" font-family="IBM Plex Sans, ui-sans-serif, system-ui, sans-serif" font-size="11" fill="${accentFg}">Button</text>
|
|
55
|
+
<rect x="${x0 + COL_W * 3}" y="${y}" width="100" height="72" rx="10" fill="${bg}" stroke="${border}" stroke-width="1"/>
|
|
56
|
+
<rect x="${x0 + COL_W * 3 + 10}" y="${y + 22}" width="80" height="28" rx="8" fill="${surface}" stroke="${border}" stroke-width="1"/>
|
|
57
|
+
<text x="${x0 + COL_W * 3 + 18}" y="${y + 40}" font-family="IBM Plex Sans, ui-sans-serif, system-ui, sans-serif" font-size="11" fill="${muted}">Input</text>
|
|
58
|
+
<text x="${x0 + COL_W * 3 + 70}" y="${y + 40}" font-family="IBM Plex Sans, ui-sans-serif, system-ui, sans-serif" font-size="9" fill="${fg}" opacity="0.35">|</text>
|
|
59
|
+
</g>`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
colorEntries,
|
|
3
|
+
modeOrder,
|
|
4
|
+
primaryMode,
|
|
5
|
+
structuralEntries,
|
|
6
|
+
} from "./token-entries";
|
|
7
|
+
import type { ResolvedPack } from "./types";
|
|
8
|
+
|
|
9
|
+
type SdLeaf = { value: string | number; type: string };
|
|
10
|
+
type SdNode = { [key: string]: SdNode | SdLeaf };
|
|
11
|
+
|
|
12
|
+
function setPath(root: SdNode, path: string[], leaf: SdLeaf) {
|
|
13
|
+
let cursor = root;
|
|
14
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
15
|
+
const key = path[i]!;
|
|
16
|
+
const existing = cursor[key];
|
|
17
|
+
if (!existing || typeof existing !== "object" || "value" in existing) {
|
|
18
|
+
cursor[key] = {};
|
|
19
|
+
}
|
|
20
|
+
cursor = cursor[key] as SdNode;
|
|
21
|
+
}
|
|
22
|
+
cursor[path[path.length - 1]!] = leaf;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseNumeric(value: string): string | number {
|
|
26
|
+
const asNum = Number(value);
|
|
27
|
+
return Number.isFinite(asNum) && value.trim() !== "" ? asNum : value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pro — Style Dictionary–friendly nested token JSON (all modes). */
|
|
31
|
+
export function exportStyleDictionary(pack: ResolvedPack): string {
|
|
32
|
+
const tree: SdNode = {};
|
|
33
|
+
const modes = modeOrder(pack);
|
|
34
|
+
|
|
35
|
+
for (const modeName of modes) {
|
|
36
|
+
const mode = pack.modes[modeName];
|
|
37
|
+
if (!mode) continue;
|
|
38
|
+
for (const entry of colorEntries(mode)) {
|
|
39
|
+
setPath(tree, ["color", entry.name, modeName], {
|
|
40
|
+
value: entry.value,
|
|
41
|
+
type: "color",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const primary = primaryMode(pack);
|
|
47
|
+
for (const entry of structuralEntries(primary)) {
|
|
48
|
+
const [group, ...rest] = entry.name.split("-");
|
|
49
|
+
const path = rest.length > 0 ? [group!, rest.join("-")] : [entry.name];
|
|
50
|
+
setPath(tree, path, {
|
|
51
|
+
value: parseNumeric(entry.value),
|
|
52
|
+
type: entry.kind === "number" ? "number" : "dimension",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setPath(tree, ["meta", "packId"], { value: pack.id, type: "other" });
|
|
57
|
+
setPath(tree, ["meta", "basePackId"], {
|
|
58
|
+
value: pack.basePackId,
|
|
59
|
+
type: "other",
|
|
60
|
+
});
|
|
61
|
+
setPath(tree, ["meta", "supportedModes"], {
|
|
62
|
+
value: modes.join(","),
|
|
63
|
+
type: "other",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return `${JSON.stringify(tree, null, 2)}\n`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { derivePack } from "./derive";
|
|
2
|
+
import { exportFigmaKit } from "./export-figma";
|
|
3
|
+
import {
|
|
4
|
+
THEME_EXPORT_BASENAMES,
|
|
5
|
+
THEME_EXPORT_FORMATS,
|
|
6
|
+
type ThemeExportFormatId,
|
|
7
|
+
} from "./export-formats";
|
|
8
|
+
import { exportStickerSheetSvg } from "./export-sticker";
|
|
9
|
+
import { BASE_PACK_IDS, type BasePackId, type ResolvedPack } from "./types";
|
|
10
|
+
|
|
11
|
+
/** Derive curated packs with no overrides (canonical kit source). */
|
|
12
|
+
export function curatedResolvedPacks(): ResolvedPack[] {
|
|
13
|
+
return BASE_PACK_IDS.map((basePackId) => curatedResolvedPack(basePackId));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function curatedResolvedPack(basePackId: BasePackId): ResolvedPack {
|
|
17
|
+
const result = derivePack({
|
|
18
|
+
schemaVersion: 2,
|
|
19
|
+
basePackId,
|
|
20
|
+
packId: basePackId,
|
|
21
|
+
});
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`failed to derive curated pack ${basePackId}: ${result.errors.join(", ")}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return result.pack;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function exportCuratedFigmaKit(): string {
|
|
31
|
+
return exportFigmaKit(curatedResolvedPacks());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function exportCuratedStickerSheet(): string {
|
|
35
|
+
return exportStickerSheetSvg(curatedResolvedPacks());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type CuratedThemeExportFile = {
|
|
39
|
+
packId: string;
|
|
40
|
+
formatId: ThemeExportFormatId;
|
|
41
|
+
/** Stable basename under `/themes/{packId}/` */
|
|
42
|
+
basename: string;
|
|
43
|
+
contents: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** All free Theme Lab formats × six curated packs. */
|
|
47
|
+
export function exportCuratedThemeFiles(): CuratedThemeExportFile[] {
|
|
48
|
+
const files: CuratedThemeExportFile[] = [];
|
|
49
|
+
for (const pack of curatedResolvedPacks()) {
|
|
50
|
+
for (const format of THEME_EXPORT_FORMATS) {
|
|
51
|
+
files.push({
|
|
52
|
+
packId: pack.id,
|
|
53
|
+
formatId: format.id,
|
|
54
|
+
basename: THEME_EXPORT_BASENAMES[format.id],
|
|
55
|
+
contents: format.render(pack),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export { derivePack } from "./derive";
|
|
2
|
+
export type { DeriveResult } from "./derive";
|
|
3
|
+
export { exportPackCss, exportTailwindTheme } from "./export-css";
|
|
4
|
+
export { exportDesignMd, sampleAccent } from "./export-design-md";
|
|
5
|
+
export { exportDtcg } from "./export-dtcg";
|
|
6
|
+
export {
|
|
7
|
+
getThemeExportFormat,
|
|
8
|
+
renderThemeExport,
|
|
9
|
+
themeExportPublicPath,
|
|
10
|
+
THEME_EXPORT_BASENAMES,
|
|
11
|
+
THEME_EXPORT_FORMATS,
|
|
12
|
+
type ExportTier,
|
|
13
|
+
type ThemeExportFormat,
|
|
14
|
+
type ThemeExportFormatId,
|
|
15
|
+
} from "./export-formats";
|
|
16
|
+
export { exportSkillMd } from "./export-skill";
|
|
17
|
+
export { modeToCssVars, modeToPreviewCss } from "./css-vars";
|
|
18
|
+
export { exportFigmaKit, exportFigmaVariables } from "./export-figma";
|
|
19
|
+
export { exportStyleDictionary } from "./export-style-dictionary";
|
|
20
|
+
export { exportStickerSheetSvg } from "./export-sticker";
|
|
21
|
+
export {
|
|
22
|
+
curatedResolvedPack,
|
|
23
|
+
curatedResolvedPacks,
|
|
24
|
+
exportCuratedFigmaKit,
|
|
25
|
+
exportCuratedStickerSheet,
|
|
26
|
+
exportCuratedThemeFiles,
|
|
27
|
+
type CuratedThemeExportFile,
|
|
28
|
+
} from "./figma-kit";
|
|
29
|
+
export { contrastForeground, contrastRatio, formatHex, formatOklch, parseColor } from "./oklch";
|
|
30
|
+
export { normalizeOverrideMap } from "./override-map";
|
|
31
|
+
export type { NormalizeResult } from "./override-map";
|
|
32
|
+
export { PACK_IDENTITY, DESIGN_DO, DESIGN_DONT } from "./pack-meta";
|
|
33
|
+
export { BASE_PACKS, supportedModes } from "./packs";
|
|
34
|
+
export {
|
|
35
|
+
extractColorsFromCss,
|
|
36
|
+
parseAiProposal,
|
|
37
|
+
pickAccentFromColors,
|
|
38
|
+
proposeFromPalette,
|
|
39
|
+
proposeFromPrompt,
|
|
40
|
+
proposalToOverrideMap,
|
|
41
|
+
suggestBasePack,
|
|
42
|
+
suggestRadius,
|
|
43
|
+
} from "./propose";
|
|
44
|
+
export type { ThemeProposal } from "./propose";
|
|
45
|
+
export { accentSetFromSeed } from "./ramps";
|
|
46
|
+
export {
|
|
47
|
+
BASE_PACK_IDS,
|
|
48
|
+
PRO_AUTHORING_UNLOCKED,
|
|
49
|
+
SCHEMA_VERSION,
|
|
50
|
+
type BasePackId,
|
|
51
|
+
type DensityPreset,
|
|
52
|
+
type GlassOverrides,
|
|
53
|
+
type MotionCharacter,
|
|
54
|
+
type NeutralOverrides,
|
|
55
|
+
type OverrideMap,
|
|
56
|
+
type RadiusPreset,
|
|
57
|
+
type ResolvedPack,
|
|
58
|
+
type SemanticOverrides,
|
|
59
|
+
type ShapeOverrides,
|
|
60
|
+
type ThemeMode,
|
|
61
|
+
} from "./types";
|