@hewnui/themes 0.0.0-stage → 0.1.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/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 +98 -4
- 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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Himanshu Kumar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hewnui/themes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Semantic OKLCH design tokens, motion tokens, and six curated theme packs (forest, tide, graphite, dune, clay, ember), with DESIGN.md, DTCG, and Tailwind v4 exports.
|
|
4
|
+
|
|
5
|
+
Part of [Hewn](https://hewnui.dev).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @hewnui/themes
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## CSS entrypoints
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
@import "@hewnui/themes/tokens.css";
|
|
17
|
+
@import "@hewnui/themes/motion.css";
|
|
18
|
+
@import "@hewnui/themes/radius.css";
|
|
19
|
+
@import "@hewnui/themes/glass.css";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Packs live under `@hewnui/themes/packs/*`. Public theme exports (DESIGN.md, DTCG, Tailwind `@theme`, SKILL.md) are also published on the docs site at `/themes`.
|
|
23
|
+
|
|
24
|
+
Programmatic theme authoring:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { derivePack, BASE_PACKS } from "@hewnui/themes/authoring";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Docs: https://hewnui.dev/docs/themes
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResolvedMode } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Map a resolved mode to CSS custom properties for scoped live preview.
|
|
4
|
+
* Includes Tailwind `--color-*` rebinds so utilities do not keep :root-resolved
|
|
5
|
+
* docs-shell values, plus local glass vars so html[data-material=glass] rules
|
|
6
|
+
* sample the draft pack instead of the documentation theme.
|
|
7
|
+
*/
|
|
8
|
+
export declare function modeToCssVars(mode: ResolvedMode): Record<string, string>;
|
|
9
|
+
/** Stylesheet block for a live-preview selector (no :root). */
|
|
10
|
+
export declare function modeToPreviewCss(selector: string, mode: ResolvedMode): string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { formatOklch } from "./oklch.js";
|
|
2
|
+
import { colorEntries, structuralEntries } from "./token-entries.js";
|
|
3
|
+
const COLOR_BRIDGE = [
|
|
4
|
+
"background",
|
|
5
|
+
"foreground",
|
|
6
|
+
"surface",
|
|
7
|
+
"surface-raised",
|
|
8
|
+
"muted",
|
|
9
|
+
"muted-foreground",
|
|
10
|
+
"accent",
|
|
11
|
+
"accent-foreground",
|
|
12
|
+
"border",
|
|
13
|
+
"ring",
|
|
14
|
+
"danger",
|
|
15
|
+
"danger-foreground",
|
|
16
|
+
"danger-subtle",
|
|
17
|
+
"danger-border",
|
|
18
|
+
"success",
|
|
19
|
+
"success-foreground",
|
|
20
|
+
"success-subtle",
|
|
21
|
+
"success-border",
|
|
22
|
+
"warning",
|
|
23
|
+
"warning-foreground",
|
|
24
|
+
"warning-subtle",
|
|
25
|
+
"warning-border",
|
|
26
|
+
"info",
|
|
27
|
+
"info-foreground",
|
|
28
|
+
"info-subtle",
|
|
29
|
+
"info-border",
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Map a resolved mode to CSS custom properties for scoped live preview.
|
|
33
|
+
* Includes Tailwind `--color-*` rebinds so utilities do not keep :root-resolved
|
|
34
|
+
* docs-shell values, plus local glass vars so html[data-material=glass] rules
|
|
35
|
+
* sample the draft pack instead of the documentation theme.
|
|
36
|
+
*/
|
|
37
|
+
export function modeToCssVars(mode) {
|
|
38
|
+
const vars = {};
|
|
39
|
+
for (const entry of colorEntries(mode)) {
|
|
40
|
+
vars[`--kp-${entry.name}`] = entry.value;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of structuralEntries(mode)) {
|
|
43
|
+
vars[`--kp-${entry.name}`] = entry.value;
|
|
44
|
+
}
|
|
45
|
+
vars["--kp-font-sans"] =
|
|
46
|
+
'var(--font-plex-sans, "IBM Plex Sans"), ui-sans-serif, system-ui, sans-serif';
|
|
47
|
+
vars["--kp-font-mono"] =
|
|
48
|
+
'var(--font-plex-mono, "IBM Plex Mono"), ui-monospace, monospace';
|
|
49
|
+
vars["--kp-font-display"] =
|
|
50
|
+
'var(--font-space-grotesk, "Space Grotesk"), var(--kp-font-sans)';
|
|
51
|
+
vars["--kp-shadow-surface"] =
|
|
52
|
+
`0 1px 2px color-mix(in oklch, ${formatOklch(mode.foreground)} 8%, transparent), ` +
|
|
53
|
+
`0 16px 40px -28px color-mix(in oklch, ${formatOklch(mode.foreground)} 28%, transparent)`;
|
|
54
|
+
// Recompute glass locally so inherited html glass fills cannot leak docs tint.
|
|
55
|
+
vars["--kp-glass-fill"] =
|
|
56
|
+
"color-mix(in oklch, var(--kp-surface) 46%, transparent)";
|
|
57
|
+
vars["--kp-glass-border"] =
|
|
58
|
+
"color-mix(in oklch, var(--kp-foreground) 16%, transparent)";
|
|
59
|
+
vars["--kp-glass-sheen"] =
|
|
60
|
+
"linear-gradient(148deg, color-mix(in oklch, var(--kp-glass-tint) 70%, white 8%), " +
|
|
61
|
+
"color-mix(in oklch, var(--kp-surface) 22%, transparent) 42%, " +
|
|
62
|
+
"color-mix(in oklch, var(--kp-surface) 38%, transparent))";
|
|
63
|
+
for (const name of COLOR_BRIDGE) {
|
|
64
|
+
vars[`--color-${name}`] = `var(--kp-${name})`;
|
|
65
|
+
}
|
|
66
|
+
// Fumadocs tokens occasionally paint nested chrome; keep them on the draft.
|
|
67
|
+
vars["--color-fd-background"] = "var(--kp-background)";
|
|
68
|
+
vars["--color-fd-foreground"] = "var(--kp-foreground)";
|
|
69
|
+
vars["--color-fd-card"] = "var(--kp-surface)";
|
|
70
|
+
vars["--color-fd-card-foreground"] = "var(--kp-foreground)";
|
|
71
|
+
vars["--color-fd-muted"] = "var(--kp-muted)";
|
|
72
|
+
vars["--color-fd-muted-foreground"] = "var(--kp-muted-foreground)";
|
|
73
|
+
vars["--color-fd-primary"] = "var(--kp-accent)";
|
|
74
|
+
vars["--color-fd-primary-foreground"] = "var(--kp-accent-foreground)";
|
|
75
|
+
vars["--color-fd-border"] = "var(--kp-border)";
|
|
76
|
+
vars["--color-fd-ring"] = "var(--kp-ring)";
|
|
77
|
+
return vars;
|
|
78
|
+
}
|
|
79
|
+
/** Stylesheet block for a live-preview selector (no :root). */
|
|
80
|
+
export function modeToPreviewCss(selector, mode) {
|
|
81
|
+
const lines = Object.entries(modeToCssVars(mode)).map(([name, value]) => ` ${name}: ${value};`);
|
|
82
|
+
return `${selector} {\n${lines.join("\n")}\n}\n`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { applyGlass, applyMotion, applyNeutrals, applySemantic, applyShape } from "./pro-apply.js";
|
|
2
|
+
import { BASE_PACKS, supportedModes } from "./packs.js";
|
|
3
|
+
import { accentSetFromSeed } from "./ramps.js";
|
|
4
|
+
const RADIUS_PRESETS = {
|
|
5
|
+
sharp: {
|
|
6
|
+
radiusSm: "0.125rem",
|
|
7
|
+
radiusMd: "0.2rem",
|
|
8
|
+
radiusLg: "0.3rem",
|
|
9
|
+
radiusPanel: "0.25rem",
|
|
10
|
+
},
|
|
11
|
+
rounded: {
|
|
12
|
+
radiusSm: "0.85rem",
|
|
13
|
+
radiusMd: "1.15rem",
|
|
14
|
+
radiusLg: "1.5rem",
|
|
15
|
+
radiusPanel: "1.25rem",
|
|
16
|
+
},
|
|
17
|
+
pill: {
|
|
18
|
+
radiusSm: "9999px",
|
|
19
|
+
radiusMd: "9999px",
|
|
20
|
+
radiusLg: "1.5rem",
|
|
21
|
+
radiusPanel: "1.15rem",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export function derivePack(overrides) {
|
|
25
|
+
const base = BASE_PACKS[overrides.basePackId];
|
|
26
|
+
const modes = supportedModes(overrides.basePackId);
|
|
27
|
+
const adjustments = [];
|
|
28
|
+
const resolved = {};
|
|
29
|
+
let structural = { ...base.structural };
|
|
30
|
+
if (overrides.radiusPreset && overrides.radiusPreset !== "soft") {
|
|
31
|
+
structural = { ...structural, ...RADIUS_PRESETS[overrides.radiusPreset] };
|
|
32
|
+
}
|
|
33
|
+
structural = applyShape(structural, overrides.shape);
|
|
34
|
+
structural = applyMotion(structural, overrides.motion);
|
|
35
|
+
for (const mode of modes) {
|
|
36
|
+
const colors = base.modes[mode];
|
|
37
|
+
if (!colors)
|
|
38
|
+
continue;
|
|
39
|
+
let next = { ...colors };
|
|
40
|
+
if (overrides.accentSeed) {
|
|
41
|
+
const accent = accentSetFromSeed(overrides.accentSeed, mode);
|
|
42
|
+
if ("error" in accent) {
|
|
43
|
+
return { ok: false, errors: [accent.error] };
|
|
44
|
+
}
|
|
45
|
+
next = {
|
|
46
|
+
...next,
|
|
47
|
+
accent: accent.set.accent,
|
|
48
|
+
accentForeground: accent.set.accentForeground,
|
|
49
|
+
ring: accent.set.ring,
|
|
50
|
+
glassTint: accent.set.glassTint,
|
|
51
|
+
};
|
|
52
|
+
adjustments.push(...accent.adjustments);
|
|
53
|
+
}
|
|
54
|
+
next = applyNeutrals(next, overrides.neutrals, mode, adjustments);
|
|
55
|
+
const semantic = applySemantic(next, overrides.semantic, mode, adjustments);
|
|
56
|
+
if ("error" in semantic) {
|
|
57
|
+
return { ok: false, errors: [semantic.error] };
|
|
58
|
+
}
|
|
59
|
+
next = applyGlass(semantic, overrides.glass);
|
|
60
|
+
resolved[mode] = {
|
|
61
|
+
...next,
|
|
62
|
+
...structural,
|
|
63
|
+
mode,
|
|
64
|
+
colorScheme: mode,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const packId = overrides.packId ??
|
|
68
|
+
`custom-${overrides.basePackId}${overrides.accentSeed ? "-accent" : ""}`;
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
pack: {
|
|
72
|
+
id: packId,
|
|
73
|
+
basePackId: overrides.basePackId,
|
|
74
|
+
supportedModes: modes,
|
|
75
|
+
modes: resolved,
|
|
76
|
+
adjustments: [...new Set(adjustments)],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedPack } from "./types";
|
|
2
|
+
/** Free-tier CSS pack export — drop-in `[data-theme]` file. */
|
|
3
|
+
export declare function exportPackCss(pack: ResolvedPack): string;
|
|
4
|
+
/**
|
|
5
|
+
* Free — Tailwind v4 `@theme` block for the primary mode, plus mode overrides
|
|
6
|
+
* that remap the same `--color-*` custom properties.
|
|
7
|
+
*/
|
|
8
|
+
export declare function exportTailwindTheme(pack: ResolvedPack): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { colorEntries, modeOrder, primaryMode, structuralEntries, } from "./token-entries.js";
|
|
2
|
+
function colorLines(mode) {
|
|
3
|
+
return [
|
|
4
|
+
` color-scheme: ${mode.colorScheme};`,
|
|
5
|
+
...colorEntries(mode).map((entry) => ` --kp-${entry.name}: ${entry.value};`),
|
|
6
|
+
].join("\n");
|
|
7
|
+
}
|
|
8
|
+
function structuralLines(mode) {
|
|
9
|
+
return [
|
|
10
|
+
...structuralEntries(mode).map((entry) => ` --kp-${entry.name}: ${entry.value};`),
|
|
11
|
+
` --kp-font-sans: var(--font-plex-sans, "IBM Plex Sans"), ui-sans-serif, system-ui,`,
|
|
12
|
+
` sans-serif;`,
|
|
13
|
+
` --kp-font-mono: var(--font-plex-mono, "IBM Plex Mono"), ui-monospace, monospace;`,
|
|
14
|
+
` --kp-font-display: var(--font-space-grotesk, "Space Grotesk"), var(--kp-font-sans);`,
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
/** Free-tier CSS pack export — drop-in `[data-theme]` file. */
|
|
18
|
+
export function exportPackCss(pack) {
|
|
19
|
+
const blocks = [];
|
|
20
|
+
let first = true;
|
|
21
|
+
for (const modeName of modeOrder(pack)) {
|
|
22
|
+
const mode = pack.modes[modeName];
|
|
23
|
+
if (!mode)
|
|
24
|
+
continue;
|
|
25
|
+
const selector = first
|
|
26
|
+
? `:root,\n[data-theme="${pack.id}"][data-mode="${mode.mode}"]`
|
|
27
|
+
: `[data-theme="${pack.id}"][data-mode="${mode.mode}"]`;
|
|
28
|
+
const body = first
|
|
29
|
+
? `${colorLines(mode)}\n${structuralLines(mode)}`
|
|
30
|
+
: colorLines(mode);
|
|
31
|
+
blocks.push(`${selector} {\n${body}\n}`);
|
|
32
|
+
first = false;
|
|
33
|
+
}
|
|
34
|
+
return `${blocks.join("\n\n")}\n`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Free — Tailwind v4 `@theme` block for the primary mode, plus mode overrides
|
|
38
|
+
* that remap the same `--color-*` custom properties.
|
|
39
|
+
*/
|
|
40
|
+
export function exportTailwindTheme(pack) {
|
|
41
|
+
const primary = primaryMode(pack);
|
|
42
|
+
const themeLines = [
|
|
43
|
+
...colorEntries(primary).map((entry) => ` --color-${entry.name}: ${entry.value};`),
|
|
44
|
+
...structuralEntries(primary)
|
|
45
|
+
.filter((entry) => entry.name.startsWith("radius-"))
|
|
46
|
+
.map((entry) => ` --${entry.name}: ${entry.value};`),
|
|
47
|
+
` --font-sans: var(--font-plex-sans, "IBM Plex Sans"), ui-sans-serif, system-ui, sans-serif;`,
|
|
48
|
+
` --font-mono: var(--font-plex-mono, "IBM Plex Mono"), ui-monospace, monospace;`,
|
|
49
|
+
` --font-display: var(--font-space-grotesk, "Space Grotesk"), var(--font-sans);`,
|
|
50
|
+
];
|
|
51
|
+
const blocks = [`@theme {\n${themeLines.join("\n")}\n}`];
|
|
52
|
+
for (const modeName of modeOrder(pack).slice(1)) {
|
|
53
|
+
const mode = pack.modes[modeName];
|
|
54
|
+
if (!mode)
|
|
55
|
+
continue;
|
|
56
|
+
const lines = colorEntries(mode).map((entry) => ` --color-${entry.name}: ${entry.value};`);
|
|
57
|
+
blocks.push(`[data-theme="${pack.id}"][data-mode="${mode.mode}"] {\n${lines.join("\n")}\n}`);
|
|
58
|
+
}
|
|
59
|
+
return `/* ${pack.id} — Tailwind v4 @theme */\n${blocks.join("\n\n")}\n`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ResolvedPack } from "./types";
|
|
2
|
+
/** Free — human- and AI-readable design brief for one pack. */
|
|
3
|
+
export declare function exportDesignMd(pack: ResolvedPack): string;
|
|
4
|
+
/** Convenience — expose formatted OKLCH for tests/docs samples. */
|
|
5
|
+
export declare function sampleAccent(pack: ResolvedPack): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { COLOR_ROLES, DESIGN_DO, DESIGN_DONT, PACK_IDENTITY, } from "./pack-meta.js";
|
|
2
|
+
import { formatOklch } from "./oklch.js";
|
|
3
|
+
import { colorEntries, modeOrder, primaryMode, structuralEntries, } from "./token-entries.js";
|
|
4
|
+
function identity(pack) {
|
|
5
|
+
const meta = PACK_IDENTITY[pack.basePackId];
|
|
6
|
+
return meta ?? { label: pack.id, summary: `Custom pack based on ${pack.basePackId}.` };
|
|
7
|
+
}
|
|
8
|
+
function paletteTable(pack) {
|
|
9
|
+
const modes = modeOrder(pack);
|
|
10
|
+
const header = modes.length > 1
|
|
11
|
+
? `| Token | Role | ${modes.map((m) => m).join(" | ")} |`
|
|
12
|
+
: `| Token | Role | ${modes[0]} |`;
|
|
13
|
+
const sep = modes.length > 1
|
|
14
|
+
? `|---|---|${modes.map(() => "---").join("|")}|`
|
|
15
|
+
: `|---|---|---|`;
|
|
16
|
+
const primary = primaryMode(pack);
|
|
17
|
+
const rows = colorEntries(primary).map((entry) => {
|
|
18
|
+
const role = COLOR_ROLES[entry.name] ?? entry.name;
|
|
19
|
+
const values = modes.map((modeName) => {
|
|
20
|
+
const mode = pack.modes[modeName];
|
|
21
|
+
if (!mode)
|
|
22
|
+
return "—";
|
|
23
|
+
const match = colorEntries(mode).find((e) => e.name === entry.name);
|
|
24
|
+
return match ? `\`${match.value}\`` : "—";
|
|
25
|
+
});
|
|
26
|
+
return `| \`${entry.name}\` | ${role} | ${values.join(" | ")} |`;
|
|
27
|
+
});
|
|
28
|
+
return [header, sep, ...rows].join("\n");
|
|
29
|
+
}
|
|
30
|
+
function structuralBlock(pack) {
|
|
31
|
+
const mode = primaryMode(pack);
|
|
32
|
+
return structuralEntries(mode)
|
|
33
|
+
.map((entry) => `- \`${entry.name}\`: \`${entry.value}\``)
|
|
34
|
+
.join("\n");
|
|
35
|
+
}
|
|
36
|
+
function list(items) {
|
|
37
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
38
|
+
}
|
|
39
|
+
/** Free — human- and AI-readable design brief for one pack. */
|
|
40
|
+
export function exportDesignMd(pack) {
|
|
41
|
+
const { label, summary } = identity(pack);
|
|
42
|
+
const modes = modeOrder(pack).join(", ");
|
|
43
|
+
const mode = primaryMode(pack);
|
|
44
|
+
return `# ${label} — Design brief
|
|
45
|
+
|
|
46
|
+
> Generated from Hewn curated theme packs. Semantic OKLCH tokens; do not edit by hand.
|
|
47
|
+
|
|
48
|
+
## Identity
|
|
49
|
+
|
|
50
|
+
${summary}
|
|
51
|
+
|
|
52
|
+
- **Pack id:** \`${pack.id}\`
|
|
53
|
+
- **Base pack:** \`${pack.basePackId}\`
|
|
54
|
+
- **Supported modes:** ${modes}
|
|
55
|
+
- **Color scheme (primary):** ${mode.colorScheme}
|
|
56
|
+
|
|
57
|
+
## Palette
|
|
58
|
+
|
|
59
|
+
${paletteTable(pack)}
|
|
60
|
+
|
|
61
|
+
## Typography
|
|
62
|
+
|
|
63
|
+
| Role | Stack | Usage |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Sans | IBM Plex Sans, ui-sans-serif, system-ui | UI chrome, body, headings |
|
|
66
|
+
| Mono | IBM Plex Mono, ui-monospace | Labels, code, tabular metrics |
|
|
67
|
+
| Display | Space Grotesk → sans | Marketing hero H1 and brand wordmark ≥18px only |
|
|
68
|
+
|
|
69
|
+
Body text uses pretty wrapping; headings use balanced wrapping; dynamic numbers use \`tabular-nums\`.
|
|
70
|
+
|
|
71
|
+
## Radius & density
|
|
72
|
+
|
|
73
|
+
${structuralBlock(pack)}
|
|
74
|
+
|
|
75
|
+
Density scales control spacing rhythm. Panel radius (\`radius-panel\`) caps menus and dialogs so they never go full-pill.
|
|
76
|
+
|
|
77
|
+
## Motion
|
|
78
|
+
|
|
79
|
+
- Duration: \`${mode.motionDuration}\`
|
|
80
|
+
- Spring stiffness: \`${mode.motionStiffness}\`
|
|
81
|
+
- Spring damping: \`${mode.motionDamping}\`
|
|
82
|
+
- Reduced motion always wins over global motion toggles.
|
|
83
|
+
- Prefer transform / opacity / filter; never animate layout properties continuously.
|
|
84
|
+
|
|
85
|
+
## Material
|
|
86
|
+
|
|
87
|
+
- \`solid\` — structured product surfaces and separators
|
|
88
|
+
- \`glass\` — accent-tinted frosted surfaces (blur, saturation, refraction, opaque fallback)
|
|
89
|
+
|
|
90
|
+
## Do
|
|
91
|
+
|
|
92
|
+
${list(DESIGN_DO)}
|
|
93
|
+
|
|
94
|
+
## Don't
|
|
95
|
+
|
|
96
|
+
${list(DESIGN_DONT)}
|
|
97
|
+
|
|
98
|
+
## Token reference (primary mode)
|
|
99
|
+
|
|
100
|
+
\`\`\`
|
|
101
|
+
${colorEntries(mode)
|
|
102
|
+
.map((e) => `--kp-${e.name}: ${e.value};`)
|
|
103
|
+
.join("\n")}
|
|
104
|
+
${structuralEntries(mode)
|
|
105
|
+
.map((e) => `--kp-${e.name}: ${e.value};`)
|
|
106
|
+
.join("\n")}
|
|
107
|
+
\`\`\`
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
/** Convenience — expose formatted OKLCH for tests/docs samples. */
|
|
111
|
+
export function sampleAccent(pack) {
|
|
112
|
+
return formatOklch(primaryMode(pack).accent);
|
|
113
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { colorEntries, modeOrder, primaryMode, structuralEntries, } from "./token-entries.js";
|
|
2
|
+
function leaf(type, value, description) {
|
|
3
|
+
return description
|
|
4
|
+
? { $type: type, $value: value, $description: description }
|
|
5
|
+
: { $type: type, $value: value };
|
|
6
|
+
}
|
|
7
|
+
function modeColors(mode) {
|
|
8
|
+
const color = {};
|
|
9
|
+
for (const entry of colorEntries(mode)) {
|
|
10
|
+
color[entry.name] = leaf("color", entry.value);
|
|
11
|
+
}
|
|
12
|
+
return color;
|
|
13
|
+
}
|
|
14
|
+
function structuralGroup(mode) {
|
|
15
|
+
const radius = {};
|
|
16
|
+
const motion = {};
|
|
17
|
+
let density;
|
|
18
|
+
for (const entry of structuralEntries(mode)) {
|
|
19
|
+
if (entry.name.startsWith("radius-")) {
|
|
20
|
+
radius[entry.name.replace(/^radius-/, "")] = leaf("dimension", entry.value);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (entry.name.startsWith("motion-")) {
|
|
24
|
+
const key = entry.name.replace(/^motion-/, "");
|
|
25
|
+
motion[key] = leaf(entry.kind === "number" ? "number" : "duration", entry.kind === "number" ? Number(entry.value) : entry.value);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (entry.name === "density") {
|
|
29
|
+
density = leaf("number", Number(entry.value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
radius,
|
|
34
|
+
motion,
|
|
35
|
+
...(density ? { density } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Free — W3C Design Tokens Community Group JSON (`$value` / `$type`).
|
|
40
|
+
* Modes live under `modes.light` / `modes.dark` when supported.
|
|
41
|
+
*/
|
|
42
|
+
export function exportDtcg(pack) {
|
|
43
|
+
const modes = {};
|
|
44
|
+
for (const modeName of modeOrder(pack)) {
|
|
45
|
+
const mode = pack.modes[modeName];
|
|
46
|
+
if (!mode)
|
|
47
|
+
continue;
|
|
48
|
+
modes[modeName] = {
|
|
49
|
+
color: modeColors(mode),
|
|
50
|
+
...structuralGroup(mode),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const primary = primaryMode(pack);
|
|
54
|
+
const doc = {
|
|
55
|
+
$description: `Hewn ${pack.id} theme — DTCG design tokens (${modeOrder(pack).join(" / ")}).`,
|
|
56
|
+
meta: {
|
|
57
|
+
packId: leaf("string", pack.id),
|
|
58
|
+
basePackId: leaf("string", pack.basePackId),
|
|
59
|
+
supportedModes: leaf("string", modeOrder(pack).join(",")),
|
|
60
|
+
colorScheme: leaf("string", primary.colorScheme),
|
|
61
|
+
},
|
|
62
|
+
typography: {
|
|
63
|
+
fontFamily: {
|
|
64
|
+
sans: leaf("fontFamily", [
|
|
65
|
+
"IBM Plex Sans",
|
|
66
|
+
"ui-sans-serif",
|
|
67
|
+
"system-ui",
|
|
68
|
+
"sans-serif",
|
|
69
|
+
]),
|
|
70
|
+
mono: leaf("fontFamily", [
|
|
71
|
+
"IBM Plex Mono",
|
|
72
|
+
"ui-monospace",
|
|
73
|
+
"monospace",
|
|
74
|
+
]),
|
|
75
|
+
display: leaf("fontFamily", [
|
|
76
|
+
"Space Grotesk",
|
|
77
|
+
"IBM Plex Sans",
|
|
78
|
+
"sans-serif",
|
|
79
|
+
]),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
modes,
|
|
83
|
+
};
|
|
84
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ResolvedPack } from "./types";
|
|
2
|
+
type TsToken = {
|
|
3
|
+
value: string | number;
|
|
4
|
+
type: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
};
|
|
7
|
+
type TsSet = {
|
|
8
|
+
[key: string]: TsToken | TsSet;
|
|
9
|
+
};
|
|
10
|
+
type TsTheme = {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
selectedTokenSets: Record<string, "enabled" | "source">;
|
|
14
|
+
};
|
|
15
|
+
/** Build Tokens Studio token sets + themes for one or more packs. */
|
|
16
|
+
export declare function buildFigmaDocument(packs: readonly ResolvedPack[]): {
|
|
17
|
+
tokenSets: Record<string, TsSet>;
|
|
18
|
+
themes: TsTheme[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Pro — Tokens Studio–compatible multi-set export for one authored pack.
|
|
22
|
+
* Import via Tokens Studio → Figma variables.
|
|
23
|
+
*/
|
|
24
|
+
export declare function exportFigmaVariables(pack: ResolvedPack): string;
|
|
25
|
+
/** Curated kit — all packs in one Tokens Studio document. */
|
|
26
|
+
export declare function exportFigmaKit(packs: readonly ResolvedPack[]): string;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { colorEntries, modeOrder, primaryMode, structuralEntries, } from "./token-entries.js";
|
|
2
|
+
function nest(set, path, token) {
|
|
3
|
+
let cursor = set;
|
|
4
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
5
|
+
const key = path[i];
|
|
6
|
+
const existing = cursor[key];
|
|
7
|
+
if (!existing || typeof existing !== "object" || "value" in existing) {
|
|
8
|
+
cursor[key] = {};
|
|
9
|
+
}
|
|
10
|
+
cursor = cursor[key];
|
|
11
|
+
}
|
|
12
|
+
cursor[path[path.length - 1]] = token;
|
|
13
|
+
}
|
|
14
|
+
/** Build Tokens Studio token sets + themes for one or more packs. */
|
|
15
|
+
export function buildFigmaDocument(packs) {
|
|
16
|
+
const tokenSets = {};
|
|
17
|
+
const themes = [];
|
|
18
|
+
for (const pack of packs) {
|
|
19
|
+
const structural = {};
|
|
20
|
+
const primary = primaryMode(pack);
|
|
21
|
+
for (const entry of structuralEntries(primary)) {
|
|
22
|
+
nest(structural, [entry.name], {
|
|
23
|
+
value: entry.kind === "number" ? Number(entry.value) : entry.value,
|
|
24
|
+
type: entry.kind === "number" ? "number" : "sizing",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
tokenSets[`${pack.id}/structural`] = structural;
|
|
28
|
+
for (const modeName of modeOrder(pack)) {
|
|
29
|
+
const mode = pack.modes[modeName];
|
|
30
|
+
if (!mode)
|
|
31
|
+
continue;
|
|
32
|
+
const setName = `${pack.id}/${modeName}`;
|
|
33
|
+
const set = {};
|
|
34
|
+
for (const entry of colorEntries(mode)) {
|
|
35
|
+
nest(set, ["color", entry.name], {
|
|
36
|
+
value: entry.value,
|
|
37
|
+
type: "color",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
tokenSets[setName] = set;
|
|
41
|
+
themes.push({
|
|
42
|
+
id: `${pack.id}-${modeName}`,
|
|
43
|
+
name: `${pack.id} / ${modeName}`,
|
|
44
|
+
selectedTokenSets: {
|
|
45
|
+
[`${pack.id}/structural`]: "source",
|
|
46
|
+
[setName]: "enabled",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { tokenSets, themes };
|
|
52
|
+
}
|
|
53
|
+
function stringifyFigmaDoc(tokenSets, themes) {
|
|
54
|
+
const doc = {
|
|
55
|
+
$metadata: {
|
|
56
|
+
tokenSetOrder: Object.keys(tokenSets),
|
|
57
|
+
activeThemeEditor: themes[0]?.id ?? null,
|
|
58
|
+
},
|
|
59
|
+
$themes: themes,
|
|
60
|
+
...tokenSets,
|
|
61
|
+
};
|
|
62
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Pro — Tokens Studio–compatible multi-set export for one authored pack.
|
|
66
|
+
* Import via Tokens Studio → Figma variables.
|
|
67
|
+
*/
|
|
68
|
+
export function exportFigmaVariables(pack) {
|
|
69
|
+
const { tokenSets, themes } = buildFigmaDocument([pack]);
|
|
70
|
+
return stringifyFigmaDoc(tokenSets, themes);
|
|
71
|
+
}
|
|
72
|
+
/** Curated kit — all packs in one Tokens Studio document. */
|
|
73
|
+
export function exportFigmaKit(packs) {
|
|
74
|
+
const { tokenSets, themes } = buildFigmaDocument(packs);
|
|
75
|
+
return stringifyFigmaDoc(tokenSets, themes);
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ResolvedPack } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Export tier marker — today every curated Theme Lab format is `free`.
|
|
4
|
+
* Advanced Studio formats (Style Dictionary, Figma) can flip to `pro` later
|
|
5
|
+
* without changing call sites; consumers must not gate on this yet.
|
|
6
|
+
*/
|
|
7
|
+
export type ExportTier = "free" | "pro";
|
|
8
|
+
export type ThemeExportFormatId = "design-md" | "dtcg" | "tailwind-theme" | "skill-md";
|
|
9
|
+
export type ThemeExportFormat = {
|
|
10
|
+
id: ThemeExportFormatId;
|
|
11
|
+
/** Short UI label */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Accessible description for buttons / docs */
|
|
14
|
+
description: string;
|
|
15
|
+
/** Suggested download filename (pack id substituted) */
|
|
16
|
+
filename: (packId: string) => string;
|
|
17
|
+
mime: string;
|
|
18
|
+
tier: ExportTier;
|
|
19
|
+
render: (pack: ResolvedPack) => string;
|
|
20
|
+
};
|
|
21
|
+
/** Free curated Theme Lab / docs site formats. */
|
|
22
|
+
export declare const THEME_EXPORT_FORMATS: readonly ThemeExportFormat[];
|
|
23
|
+
export declare function getThemeExportFormat(id: ThemeExportFormatId): ThemeExportFormat;
|
|
24
|
+
export declare function renderThemeExport(id: ThemeExportFormatId, pack: ResolvedPack): string;
|
|
25
|
+
export declare const THEME_EXPORT_BASENAMES: Record<ThemeExportFormatId, string>;
|
|
26
|
+
/** Static public path for a generated curated export. */
|
|
27
|
+
export declare function themeExportPublicPath(packId: string, id: ThemeExportFormatId): string;
|