@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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/dist/authoring/css-vars.d.ts +10 -0
  4. package/dist/authoring/css-vars.js +83 -0
  5. package/dist/authoring/derive.d.ts +9 -0
  6. package/dist/authoring/derive.js +79 -0
  7. package/dist/authoring/export-css.d.ts +8 -0
  8. package/dist/authoring/export-css.js +60 -0
  9. package/dist/authoring/export-design-md.d.ts +5 -0
  10. package/dist/authoring/export-design-md.js +113 -0
  11. package/dist/authoring/export-dtcg.d.ts +6 -0
  12. package/dist/authoring/export-dtcg.js +85 -0
  13. package/dist/authoring/export-figma.d.ts +27 -0
  14. package/dist/authoring/export-figma.js +76 -0
  15. package/dist/authoring/export-formats.d.ts +27 -0
  16. package/dist/authoring/export-formats.js +62 -0
  17. package/dist/authoring/export-skill.d.ts +5 -0
  18. package/dist/authoring/export-skill.js +75 -0
  19. package/dist/authoring/export-sticker.d.ts +6 -0
  20. package/dist/authoring/export-sticker.js +54 -0
  21. package/dist/authoring/export-style-dictionary.d.ts +3 -0
  22. package/dist/authoring/export-style-dictionary.js +52 -0
  23. package/dist/authoring/figma-kit.d.ts +16 -0
  24. package/dist/authoring/figma-kit.js +41 -0
  25. package/dist/authoring/index.d.ts +21 -0
  26. package/dist/authoring/index.js +18 -0
  27. package/dist/authoring/oklch.d.ts +11 -0
  28. package/dist/authoring/oklch.js +116 -0
  29. package/dist/authoring/override-map.d.ts +9 -0
  30. package/dist/authoring/override-map.js +150 -0
  31. package/dist/authoring/pack-def.d.ts +7 -0
  32. package/dist/authoring/pack-def.js +3 -0
  33. package/dist/authoring/pack-meta.d.ts +10 -0
  34. package/dist/authoring/pack-meta.js +58 -0
  35. package/dist/authoring/packs-dual.d.ts +129 -0
  36. package/dist/authoring/packs-dual.js +130 -0
  37. package/dist/authoring/packs-single.d.ts +87 -0
  38. package/dist/authoring/packs-single.js +88 -0
  39. package/dist/authoring/packs.d.ts +8 -0
  40. package/dist/authoring/packs.js +26 -0
  41. package/dist/authoring/pro-apply.d.ts +8 -0
  42. package/dist/authoring/pro-apply.js +107 -0
  43. package/dist/authoring/propose.d.ts +21 -0
  44. package/dist/authoring/propose.js +162 -0
  45. package/dist/authoring/ramps.d.ts +13 -0
  46. package/dist/authoring/ramps.js +40 -0
  47. package/dist/authoring/status-ramps.d.ts +13 -0
  48. package/dist/authoring/status-ramps.js +76 -0
  49. package/dist/authoring/token-entries.d.ts +10 -0
  50. package/dist/authoring/token-entries.js +52 -0
  51. package/dist/authoring/types.d.ts +110 -0
  52. package/dist/authoring/types.js +11 -0
  53. package/exports/clay/DESIGN.md +138 -0
  54. package/exports/clay/SKILL.md +94 -0
  55. package/exports/clay/theme.css +37 -0
  56. package/exports/clay/tokens.dtcg.json +200 -0
  57. package/exports/dune/DESIGN.md +138 -0
  58. package/exports/dune/SKILL.md +94 -0
  59. package/exports/dune/theme.css +37 -0
  60. package/exports/dune/tokens.dtcg.json +200 -0
  61. package/exports/ember/DESIGN.md +138 -0
  62. package/exports/ember/SKILL.md +94 -0
  63. package/exports/ember/theme.css +37 -0
  64. package/exports/ember/tokens.dtcg.json +200 -0
  65. package/exports/forest/DESIGN.md +138 -0
  66. package/exports/forest/SKILL.md +94 -0
  67. package/exports/forest/theme.css +67 -0
  68. package/exports/forest/tokens.dtcg.json +348 -0
  69. package/exports/graphite/DESIGN.md +138 -0
  70. package/exports/graphite/SKILL.md +94 -0
  71. package/exports/graphite/theme.css +67 -0
  72. package/exports/graphite/tokens.dtcg.json +348 -0
  73. package/exports/index.json +45 -0
  74. package/exports/tide/DESIGN.md +138 -0
  75. package/exports/tide/SKILL.md +94 -0
  76. package/exports/tide/theme.css +67 -0
  77. package/exports/tide/tokens.dtcg.json +348 -0
  78. package/figma/README.md +16 -0
  79. package/figma/hewn.tokens.json +1308 -0
  80. package/figma/sticker-sheet.svg +93 -0
  81. package/package.json +77 -3
  82. package/src/authoring/css-vars.ts +92 -0
  83. package/src/authoring/derive.ts +106 -0
  84. package/src/authoring/export-css.ts +86 -0
  85. package/src/authoring/export-design-md.ts +134 -0
  86. package/src/authoring/export-dtcg.ts +116 -0
  87. package/src/authoring/export-figma.ts +109 -0
  88. package/src/authoring/export-formats.ts +101 -0
  89. package/src/authoring/export-skill.ts +93 -0
  90. package/src/authoring/export-sticker.ts +60 -0
  91. package/src/authoring/export-style-dictionary.ts +67 -0
  92. package/src/authoring/figma-kit.ts +60 -0
  93. package/src/authoring/index.ts +61 -0
  94. package/src/authoring/oklch.ts +137 -0
  95. package/src/authoring/override-map.ts +191 -0
  96. package/src/authoring/pack-def.ts +11 -0
  97. package/src/authoring/pack-meta.ts +66 -0
  98. package/src/authoring/packs-dual.ts +131 -0
  99. package/src/authoring/packs-single.ts +89 -0
  100. package/src/authoring/packs.ts +43 -0
  101. package/src/authoring/pro-apply.ts +156 -0
  102. package/src/authoring/propose.ts +181 -0
  103. package/src/authoring/ramps.ts +62 -0
  104. package/src/authoring/status-ramps.ts +98 -0
  105. package/src/authoring/token-entries.ts +59 -0
  106. package/src/authoring/types.ts +133 -0
  107. package/src/glass-buttons.css +46 -0
  108. package/src/glass.css +266 -0
  109. package/src/motion.css +60 -0
  110. package/src/packs/clay.css +45 -0
  111. package/src/packs/dune.css +45 -0
  112. package/src/packs/ember.css +45 -0
  113. package/src/packs/forest.css +80 -0
  114. package/src/packs/graphite.css +79 -0
  115. package/src/packs/tide.css +79 -0
  116. package/src/radius.css +25 -0
  117. 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
- # Temporary Holding Version
1
+ # @hewnui/themes
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.js";
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,9 @@
1
+ import type { OverrideMap, ResolvedPack } from "./types.js";
2
+ export type DeriveResult = {
3
+ ok: true;
4
+ pack: ResolvedPack;
5
+ } | {
6
+ ok: false;
7
+ errors: string[];
8
+ };
9
+ export declare function derivePack(overrides: OverrideMap): DeriveResult;
@@ -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.js";
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.js";
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,6 @@
1
+ import type { ResolvedPack } from "./types.js";
2
+ /**
3
+ * Free — W3C Design Tokens Community Group JSON (`$value` / `$type`).
4
+ * Modes live under `modes.light` / `modes.dark` when supported.
5
+ */
6
+ export declare function exportDtcg(pack: ResolvedPack): string;
@@ -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.js";
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.js";
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;