@nghitrum/dsforge 0.1.5-alpha.6 → 0.1.5-alpha.8

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/README.md CHANGED
@@ -24,7 +24,6 @@ design-system.config.json → dsforge generate
24
24
  dist-ds/
25
25
  ├── src/ 9 typed React components
26
26
  ├── tokens/ CSS custom properties, JS map, Tailwind extension
27
- ├── docs/ MDX documentation per component
28
27
  ├── metadata/ AI-readable JSON contracts per component
29
28
  └── showcase.html visual docs — open directly in the browser, no server
30
29
  ```
@@ -97,10 +96,6 @@ Each component is typed, themed with your actual tokens, and ships with a prop t
97
96
  `tokens.js` — JS token map for runtime use
98
97
  `tailwind.js` — Tailwind theme extension, ready to drop into `tailwind.config.js`
99
98
 
100
- ### MDX docs
101
-
102
- One `.mdx` file per component, generated from your config. Import them into any docs site.
103
-
104
99
  ### AI metadata contracts
105
100
 
106
101
  Each component emits `dist-ds/metadata/<component>.json`:
@@ -0,0 +1,112 @@
1
+ // src/lib/license.ts
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ function readKeyFromDotEnv() {
5
+ try {
6
+ const content = readFileSync(join(process.cwd(), ".env"), "utf8");
7
+ for (const raw of content.split("\n")) {
8
+ const line = raw.trim();
9
+ if (!line || line.startsWith("#")) continue;
10
+ const eq = line.indexOf("=");
11
+ if (eq === -1) continue;
12
+ const key = line.slice(0, eq).trim();
13
+ if (key !== "DSFORGE_KEY") continue;
14
+ const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
15
+ return val || void 0;
16
+ }
17
+ } catch {
18
+ }
19
+ return void 0;
20
+ }
21
+ function isProUnlocked() {
22
+ const key = process.env["DSFORGE_KEY"] ?? readKeyFromDotEnv();
23
+ return typeof key === "string" && key.length > 0;
24
+ }
25
+
26
+ // src/presets/index.ts
27
+ var PRESETS = [
28
+ "compact",
29
+ "comfortable",
30
+ "spacious"
31
+ ];
32
+ var SPACING_PRESETS = {
33
+ compact: {
34
+ "1": 2,
35
+ "2": 4,
36
+ "3": 8,
37
+ "4": 12,
38
+ "5": 16,
39
+ "6": 24,
40
+ "7": 32,
41
+ "8": 48
42
+ },
43
+ comfortable: {
44
+ "1": 4,
45
+ "2": 8,
46
+ "3": 12,
47
+ "4": 16,
48
+ "5": 24,
49
+ "6": 32,
50
+ "7": 48,
51
+ "8": 64
52
+ },
53
+ spacious: {
54
+ "1": 6,
55
+ "2": 12,
56
+ "3": 18,
57
+ "4": 24,
58
+ "5": 36,
59
+ "6": 48,
60
+ "7": 72,
61
+ "8": 96
62
+ }
63
+ };
64
+ var RADIUS_PRESETS = {
65
+ compact: { none: 0, sm: 2, md: 3, lg: 6, xl: 10, full: 9999 },
66
+ comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
67
+ spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
68
+ };
69
+ var PRESET_BASE_UNITS = {
70
+ compact: 2,
71
+ comfortable: 4,
72
+ spacious: 6
73
+ };
74
+ function buildSemanticSpacing(scale) {
75
+ return {
76
+ "component-padding-xs": `${scale["1"]}`,
77
+ "component-padding-sm": `${scale["2"]}`,
78
+ "component-padding-md": `${scale["4"]}`,
79
+ "component-padding-lg": `${scale["5"]}`,
80
+ "layout-gap-xs": `${scale["2"]}`,
81
+ "layout-gap-sm": `${scale["3"]}`,
82
+ "layout-gap-md": `${scale["5"]}`,
83
+ "layout-gap-lg": `${scale["6"]}`,
84
+ "layout-section": `${scale["7"]}`
85
+ };
86
+ }
87
+ function applyPreset(config, preset) {
88
+ const scale = SPACING_PRESETS[preset];
89
+ const radius = RADIUS_PRESETS[preset];
90
+ const baseUnit = PRESET_BASE_UNITS[preset];
91
+ config.spacing = {
92
+ ...config.spacing,
93
+ baseUnit,
94
+ scale,
95
+ semantic: buildSemanticSpacing(scale)
96
+ };
97
+ config.radius = { ...config.radius, ...radius };
98
+ config.philosophy = {
99
+ ...config.philosophy,
100
+ density: preset
101
+ };
102
+ }
103
+
104
+ export {
105
+ isProUnlocked,
106
+ PRESETS,
107
+ SPACING_PRESETS,
108
+ RADIUS_PRESETS,
109
+ PRESET_BASE_UNITS,
110
+ buildSemanticSpacing,
111
+ applyPreset
112
+ };
package/dist/cli/index.js CHANGED
@@ -5,8 +5,14 @@ import {
5
5
  generateTsConfig
6
6
  } from "../chunk-QHE35QQQ.js";
7
7
  import {
8
+ PRESETS,
9
+ PRESET_BASE_UNITS,
10
+ RADIUS_PRESETS,
11
+ SPACING_PRESETS,
12
+ applyPreset,
13
+ buildSemanticSpacing,
8
14
  isProUnlocked
9
- } from "../chunk-OAMEFG6Q.js";
15
+ } from "../chunk-JUMR3N5J.js";
10
16
 
11
17
  // src/cli/index.ts
12
18
  import { program } from "commander";
@@ -402,69 +408,6 @@ async function confirm(question) {
402
408
  }
403
409
 
404
410
  // src/cli/commands/init.ts
405
- var SPACING_PRESETS = {
406
- compact: {
407
- "1": 2,
408
- "2": 4,
409
- "3": 8,
410
- "4": 12,
411
- "5": 16,
412
- "6": 24,
413
- "7": 32,
414
- "8": 48
415
- },
416
- comfortable: {
417
- "1": 4,
418
- "2": 8,
419
- "3": 12,
420
- "4": 16,
421
- "5": 24,
422
- "6": 32,
423
- "7": 48,
424
- "8": 64
425
- },
426
- spacious: {
427
- "1": 6,
428
- "2": 12,
429
- "3": 18,
430
- "4": 24,
431
- "5": 36,
432
- "6": 48,
433
- "7": 72,
434
- "8": 96
435
- }
436
- };
437
- var RADIUS_PRESETS = {
438
- compact: { none: 0, sm: 2, md: 3, lg: 6, xl: 10, full: 9999 },
439
- comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
440
- spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
441
- };
442
- function applyPreset(config, preset) {
443
- const spacing = SPACING_PRESETS[preset];
444
- const radius = RADIUS_PRESETS[preset];
445
- const baseUnit = preset === "compact" ? 2 : preset === "spacious" ? 6 : 4;
446
- config.spacing = {
447
- ...config.spacing,
448
- baseUnit,
449
- scale: spacing,
450
- semantic: {
451
- "component-padding-xs": `${spacing["1"]}`,
452
- "component-padding-sm": `${spacing["2"]}`,
453
- "component-padding-md": `${spacing["4"]}`,
454
- "component-padding-lg": `${spacing["5"]}`,
455
- "layout-gap-xs": `${spacing["2"]}`,
456
- "layout-gap-sm": `${spacing["3"]}`,
457
- "layout-gap-md": `${spacing["5"]}`,
458
- "layout-gap-lg": `${spacing["6"]}`,
459
- "layout-section": `${spacing["7"]}`
460
- }
461
- };
462
- config.radius = { ...config.radius, ...radius };
463
- config.philosophy = {
464
- ...config.philosophy,
465
- density: preset
466
- };
467
- }
468
411
  function buildInitialConfig(name, preset = "comfortable") {
469
412
  const spacing = SPACING_PRESETS[preset];
470
413
  const radius = RADIUS_PRESETS[preset];
@@ -655,19 +598,9 @@ function buildInitialConfig(name, preset = "comfortable") {
655
598
  }
656
599
  },
657
600
  spacing: {
658
- baseUnit: preset === "compact" ? 2 : preset === "spacious" ? 6 : 4,
601
+ baseUnit: PRESET_BASE_UNITS[preset],
659
602
  scale: spacing,
660
- semantic: {
661
- "component-padding-xs": `${spacing[1]}`,
662
- "component-padding-sm": `${spacing[2]}`,
663
- "component-padding-md": `${spacing[4]}`,
664
- "component-padding-lg": `${spacing[5]}`,
665
- "layout-gap-xs": `${spacing[2]}`,
666
- "layout-gap-sm": `${spacing[3]}`,
667
- "layout-gap-md": `${spacing[5]}`,
668
- "layout-gap-lg": `${spacing[6]}`,
669
- "layout-section": `${spacing[7]}`
670
- }
603
+ semantic: buildSemanticSpacing(spacing)
671
604
  },
672
605
  radius,
673
606
  elevation: {
@@ -1898,6 +1831,34 @@ function emitThemeCss(themeName, themeOverrides, config) {
1898
1831
  lines.push(emitBlock(`:root[data-theme="${themeName}"]`, entries));
1899
1832
  return lines.join("\n") + "\n";
1900
1833
  }
1834
+ function emitDensityCss(config) {
1835
+ const lines = [
1836
+ `/* \u2500\u2500\u2500 ${config.meta.name} \u2014 density presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */`,
1837
+ `/* Generated by dsforge. Do not edit manually. */`,
1838
+ `/* Pro feature: import this file to enable runtime density switching. */`,
1839
+ `/* Usage: <html data-density="compact | comfortable | spacious"> */`,
1840
+ `/* or wrap with <DensityProvider density="compact"> */`,
1841
+ ""
1842
+ ];
1843
+ for (const preset of PRESETS) {
1844
+ const scale = SPACING_PRESETS[preset];
1845
+ const radius = RADIUS_PRESETS[preset];
1846
+ const semantic = buildSemanticSpacing(scale);
1847
+ const entries = [];
1848
+ for (const [key, value] of Object.entries(scale)) {
1849
+ entries.push([`spacing-${key}`, `${value}px`]);
1850
+ }
1851
+ for (const [key, value] of Object.entries(semantic)) {
1852
+ entries.push([key, `${value}px`]);
1853
+ }
1854
+ for (const [key, value] of Object.entries(radius)) {
1855
+ entries.push([`radius-${key}`, value === 9999 ? "9999px" : `${value}px`]);
1856
+ }
1857
+ lines.push(emitBlock(`[data-density="${preset}"]`, entries, `Preset: ${preset}`));
1858
+ lines.push("");
1859
+ }
1860
+ return lines.join("\n");
1861
+ }
1901
1862
  function generateCssFiles(config, resolution) {
1902
1863
  const files = [];
1903
1864
  files.push({
@@ -4125,6 +4086,62 @@ function generateThemeProvider(config) {
4125
4086
  const themeNames = Object.keys(config.themes ?? { light: {}, dark: {} });
4126
4087
  const defaultTheme = themeNames.includes("light") ? "light" : themeNames[0] ?? "light";
4127
4088
  const themeType = themeNames.map((t) => `"${t}"`).join(" | ");
4089
+ const isPro = isProUnlocked();
4090
+ const defaultDensity = config.meta.preset ?? "comfortable";
4091
+ const densityImport = isPro ? `
4092
+ import "../tokens/density.css";` : "";
4093
+ const densityTypes = isPro ? `
4094
+ export type DensityName = "compact" | "comfortable" | "spacious";
4095
+ ` : "";
4096
+ const densityContextTypes = isPro ? `
4097
+ export interface DensityContextValue {
4098
+ density: DensityName;
4099
+ setDensity: (density: DensityName) => void;
4100
+ }
4101
+ ` : "";
4102
+ const densityContext = isPro ? `
4103
+ export const DensityContext = React.createContext<DensityContextValue>({
4104
+ density: "${defaultDensity}",
4105
+ setDensity: () => undefined,
4106
+ });
4107
+
4108
+ /**
4109
+ * Hook to read and change the current density.
4110
+ * Must be used inside a <ThemeProvider>.
4111
+ */
4112
+ export function useDensity(): DensityContextValue {
4113
+ return React.useContext(DensityContext);
4114
+ }
4115
+ ` : "";
4116
+ const densityProp = isPro ? `
4117
+ /** Component density. Requires density.css to be imported. Defaults to "${defaultDensity}". */
4118
+ density?: DensityName;` : "";
4119
+ const densityOnChangeProp = isPro ? `
4120
+ /** Called when setDensity is invoked. */
4121
+ onDensityChange?: (density: DensityName) => void;` : "";
4122
+ const densityState = isPro ? `
4123
+ const [density, setDensityState] = React.useState<DensityName>(initialDensity);
4124
+
4125
+ React.useEffect(() => {
4126
+ setDensityState(initialDensity);
4127
+ }, [initialDensity]);
4128
+
4129
+ const setDensity = React.useCallback(
4130
+ (next: DensityName) => {
4131
+ setDensityState(next);
4132
+ onDensityChange?.(next);
4133
+ },
4134
+ [onDensityChange],
4135
+ );
4136
+ ` : "";
4137
+ const densityDestructure = isPro ? `,
4138
+ density: initialDensity = "${defaultDensity}",
4139
+ onDensityChange,` : "";
4140
+ const densityProviderOpen = isPro ? `
4141
+ <DensityContext.Provider value={{ density, setDensity }}>` : "";
4142
+ const densityDataAttr = isPro ? ` data-density={density}` : "";
4143
+ const densityProviderClose = isPro ? `
4144
+ </DensityContext.Provider>` : "";
4128
4145
  return `/**
4129
4146
  * ThemeProvider \u2014 ${config.meta.name}
4130
4147
  *
@@ -4136,27 +4153,27 @@ function generateThemeProvider(config) {
4136
4153
  * import "@${config.meta.name}/tokens/light.css"; // or dark.css
4137
4154
  * import { ThemeProvider } from "@${config.meta.name}";
4138
4155
  *
4139
- * <ThemeProvider theme="light">
4156
+ * <ThemeProvider theme="light"${isPro ? ` density="${defaultDensity}"` : ""}>
4140
4157
  * <App />
4141
4158
  * </ThemeProvider>
4142
4159
  */
4143
4160
 
4144
- import React from "react";
4161
+ import React from "react";${densityImport}
4145
4162
 
4146
4163
  // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4147
4164
 
4148
4165
  export type ThemeName = ${themeType};
4149
-
4166
+ ${densityTypes}
4150
4167
  export interface ThemeContextValue {
4151
4168
  theme: ThemeName;
4152
4169
  setTheme: (theme: ThemeName) => void;
4153
4170
  }
4154
-
4171
+ ${densityContextTypes}
4155
4172
  export interface ThemeProviderProps {
4156
4173
  /** Initial theme. Defaults to "${defaultTheme}". */
4157
4174
  theme?: ThemeName;
4158
4175
  /** Called when setTheme is invoked \u2014 use to persist theme preference. */
4159
- onThemeChange?: (theme: ThemeName) => void;
4176
+ onThemeChange?: (theme: ThemeName) => void;${densityProp}${densityOnChangeProp}
4160
4177
  children: React.ReactNode;
4161
4178
  }
4162
4179
 
@@ -4174,12 +4191,12 @@ export const ThemeContext = React.createContext<ThemeContextValue>({
4174
4191
  export function useTheme(): ThemeContextValue {
4175
4192
  return React.useContext(ThemeContext);
4176
4193
  }
4177
-
4194
+ ${densityContext}
4178
4195
  // \u2500\u2500\u2500 Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4179
4196
 
4180
4197
  export function ThemeProvider({
4181
4198
  theme: initialTheme = "${defaultTheme}",
4182
- onThemeChange,
4199
+ onThemeChange,${densityDestructure}
4183
4200
  children,
4184
4201
  }: ThemeProviderProps) {
4185
4202
  const [theme, setThemeState] = React.useState<ThemeName>(initialTheme);
@@ -4195,13 +4212,13 @@ export function ThemeProvider({
4195
4212
  },
4196
4213
  [onThemeChange],
4197
4214
  );
4198
-
4199
- return (
4200
- <ThemeContext.Provider value={{ theme, setTheme }}>
4201
- <div data-theme={theme} style={{ display: "contents" }}>
4202
- {children}
4203
- </div>
4204
- </ThemeContext.Provider>
4215
+ ${densityState}
4216
+ return (${densityProviderOpen}
4217
+ <ThemeContext.Provider value={{ theme, setTheme }}>
4218
+ <div data-theme={theme}${densityDataAttr} style={{ display: "contents" }}>
4219
+ {children}
4220
+ </div>
4221
+ </ThemeContext.Provider>${densityProviderClose}
4205
4222
  );
4206
4223
  }
4207
4224
  `;
@@ -4858,6 +4875,13 @@ async function runGenerate(cwd, options) {
4858
4875
  await writeFile(path3.join(tokensDir, filename), content);
4859
4876
  logger.dim(` \u2192 tokens/${filename}`);
4860
4877
  }
4878
+ if (isProUnlocked()) {
4879
+ await writeFile(
4880
+ path3.join(tokensDir, "density.css"),
4881
+ emitDensityCss(config)
4882
+ );
4883
+ logger.dim(` \u2192 tokens/density.css`);
4884
+ }
4861
4885
  const tokenFiles = reactAdapter.generateTokenFiles(config, resolution);
4862
4886
  for (const { filename, content } of tokenFiles) {
4863
4887
  await writeFile(path3.join(tokensDir, filename), content);
@@ -4932,7 +4956,7 @@ async function runGenerate(cwd, options) {
4932
4956
  logger.success(`Package files written`);
4933
4957
  }
4934
4958
  logger.step("Generating showcase...");
4935
- const { generateShowcase } = await import("../html-BJBKRTSX.js");
4959
+ const { generateShowcase } = await import("../html-LQHDCSG4.js");
4936
4960
  const showcaseHtml = generateShowcase(config, resolution);
4937
4961
  await writeFile(path3.join(outRoot, "showcase.html"), showcaseHtml);
4938
4962
  logger.dim(` \u2192 showcase.html`);
@@ -5319,7 +5343,7 @@ async function runMenu() {
5319
5343
  // package.json
5320
5344
  var package_default = {
5321
5345
  name: "@nghitrum/dsforge",
5322
- version: "0.1.5-alpha.6",
5346
+ version: "0.1.5-alpha.8",
5323
5347
  description: "AI-native design system generator \u2014 tokens \u2192 components \u2192 docs \u2192 npm",
5324
5348
  keywords: [
5325
5349
  "design-system",
@@ -1,6 +1,10 @@
1
1
  import {
2
+ PRESETS,
3
+ RADIUS_PRESETS,
4
+ SPACING_PRESETS,
5
+ buildSemanticSpacing,
2
6
  isProUnlocked
3
- } from "./chunk-OAMEFG6Q.js";
7
+ } from "./chunk-JUMR3N5J.js";
4
8
 
5
9
  // src/generators/showcase/types.ts
6
10
  function esc(s) {
@@ -96,26 +100,33 @@ function buildTypographySection(config) {
96
100
  }
97
101
  function buildSpacingSection(config) {
98
102
  const scale = config.spacing?.scale ?? {};
99
- const baseUnit = config.spacing?.baseUnit ?? 4;
100
- const row = (key, val) => `
103
+ const scaleRow = (key) => `
104
+ <div class="spacing-row">
105
+ <span class="spacing-key">spacing-${esc(key)}</span>
106
+ <div class="spacing-bar-wrap">
107
+ <div class="spacing-bar" style="width:min(calc(var(--spacing-${esc(key)}) * 2), 320px)"></div>
108
+ </div>
109
+ <span class="spacing-val" data-spacing-var="--spacing-${esc(key)}"></span>
110
+ </div>`;
111
+ const semanticRow = (key) => `
101
112
  <div class="spacing-row">
102
113
  <span class="spacing-key">${esc(key)}</span>
103
114
  <div class="spacing-bar-wrap">
104
- <div class="spacing-bar" style="width:${Math.min(Number(val) * 2, 320)}px"></div>
115
+ <div class="spacing-bar" style="width:min(calc(var(--${esc(key)}) * 2), 320px)"></div>
105
116
  </div>
106
- <span class="spacing-val">${val}px</span>
117
+ <span class="spacing-val" data-spacing-var="--${esc(key)}"></span>
107
118
  </div>`;
108
119
  return `
109
120
  <div class="section-block">
110
- <h3 class="group-title">Base Unit: ${baseUnit}px</h3>
121
+ <h3 class="group-title">Scale</h3>
111
122
  <div class="spacing-list">
112
- ${Object.entries(scale).map(([k, v]) => row(k, v)).join("")}
123
+ ${Object.keys(scale).map((k) => scaleRow(k)).join("")}
113
124
  </div>
114
125
  </div>
115
126
  <div class="section-block">
116
127
  <h3 class="group-title">Semantic Spacing</h3>
117
128
  <div class="spacing-list">
118
- ${Object.entries(config.spacing?.semantic ?? {}).map(([k, v]) => row(k, v)).join("")}
129
+ ${Object.keys(config.spacing?.semantic ?? {}).map((k) => semanticRow(k)).join("")}
119
130
  </div>
120
131
  </div>
121
132
  `;
@@ -126,12 +137,12 @@ function buildRadiusSection(config) {
126
137
  <div class="section-block">
127
138
  <h3 class="group-title">Border Radius</h3>
128
139
  <div class="radius-grid">
129
- ${Object.entries(radius).map(
130
- ([key, val]) => `
140
+ ${Object.keys(radius).map(
141
+ (key) => `
131
142
  <div class="radius-item">
132
- <div class="radius-box" style="border-radius:${val}px"></div>
143
+ <div class="radius-box" style="border-radius:var(--radius-${esc(key)})"></div>
133
144
  <span class="radius-key">${esc(key)}</span>
134
- <span class="radius-val">${val}px</span>
145
+ <span class="radius-val" data-spacing-var="--radius-${esc(key)}"></span>
135
146
  </div>
136
147
  `
137
148
  ).join("")}
@@ -1983,11 +1994,29 @@ var SHOWCASE_COMPONENTS = [
1983
1994
  ];
1984
1995
 
1985
1996
  // src/generators/showcase/html.ts
1997
+ function buildDensityCss() {
1998
+ const blocks = [];
1999
+ for (const preset of PRESETS) {
2000
+ const scale = SPACING_PRESETS[preset];
2001
+ const radius = RADIUS_PRESETS[preset];
2002
+ const semantic = buildSemanticSpacing(scale);
2003
+ const vars = [];
2004
+ for (const [k, v] of Object.entries(scale)) vars.push(` --spacing-${k}: ${v}px;`);
2005
+ for (const [k, v] of Object.entries(semantic)) vars.push(` --${k}: ${v}px;`);
2006
+ for (const [k, v] of Object.entries(radius))
2007
+ vars.push(` --radius-${k}: ${v === 9999 ? "9999px" : `${v}px`};`);
2008
+ blocks.push(` [data-density="${preset}"] {
2009
+ ${vars.join("\n")}
2010
+ }`);
2011
+ }
2012
+ return blocks.join("\n");
2013
+ }
1986
2014
  function generateShowcase(config, resolution) {
1987
2015
  const tokens = resolution.tokens;
1988
2016
  const name = config.meta?.name ?? "Design System";
1989
2017
  const version = config.meta?.version ?? "0.1.0";
1990
2018
  const themes = Object.keys(config.themes ?? {});
2019
+ const defaultDensity = config.meta?.preset ?? "comfortable";
1991
2020
  const foundationItems = [
1992
2021
  { id: "colors", label: "Colors" },
1993
2022
  { id: "typography", label: "Typography" },
@@ -2023,8 +2052,9 @@ function generateShowcase(config, resolution) {
2023
2052
  const darkTheme = config.themes?.["dark"] ?? {};
2024
2053
  const themeCssLight = Object.entries({ ...flatTokens, ...lightTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2025
2054
  const themeCssDark = Object.entries({ ...flatTokens, ...darkTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2055
+ const densityCss = buildDensityCss();
2026
2056
  return `<!DOCTYPE html>
2027
- <html lang="en" data-theme="light">
2057
+ <html lang="en" data-theme="light" data-density="${defaultDensity}">
2028
2058
  <head>
2029
2059
  <meta charset="UTF-8" />
2030
2060
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -2039,6 +2069,9 @@ ${themeCssLight}
2039
2069
  ${themeCssDark}
2040
2070
  }
2041
2071
 
2072
+ /* \u2500\u2500 Density presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2073
+ ${densityCss}
2074
+
2042
2075
  /* \u2500\u2500 Reset + base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2043
2076
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2044
2077
  html { font-size: 16px; }
@@ -2104,6 +2137,33 @@ ${themeCssDark}
2104
2137
  color: var(--color-text-primary, #0f172a); font-weight: 500;
2105
2138
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2106
2139
  }
2140
+ .density-toggle {
2141
+ display: flex; gap: 3px; align-items: center;
2142
+ background: var(--color-bg-subtle, #f8fafc);
2143
+ border: 1px solid var(--color-border-default, #e2e8f0);
2144
+ border-radius: 7px; padding: 3px;
2145
+ }
2146
+ .density-toggle.locked { opacity: 0.5; cursor: not-allowed; }
2147
+ .density-btn {
2148
+ padding: 3px 10px; border-radius: 4px; border: none;
2149
+ background: transparent; font-size: 12px; cursor: pointer;
2150
+ color: var(--color-text-secondary, #64748b);
2151
+ transition: background 120ms, color 120ms;
2152
+ }
2153
+ .density-btn:disabled { cursor: not-allowed; }
2154
+ .density-btn.active {
2155
+ background: var(--color-bg-default, #fff);
2156
+ color: var(--color-text-primary, #0f172a); font-weight: 500;
2157
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2158
+ }
2159
+ .density-lock {
2160
+ font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
2161
+ color: var(--color-text-secondary, #64748b);
2162
+ padding: 2px 6px; border-radius: 4px;
2163
+ background: var(--color-bg-overlay, #f1f5f9);
2164
+ border: 1px solid var(--color-border-default, #e2e8f0);
2165
+ white-space: nowrap;
2166
+ }
2107
2167
  .content { padding: 36px 40px 80px; max-width: 860px; }
2108
2168
  .page { display: none; }
2109
2169
  .page.active { display: block; }
@@ -2307,16 +2367,16 @@ ${themeCssDark}
2307
2367
  .locked-hint code { background: var(--color-bg-overlay, #f1f5f9); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--color-border-default, #e2e8f0); }
2308
2368
 
2309
2369
  /* \u2500\u2500 Component primitives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2310
- .ds-btn { border: none; cursor: pointer; font-size: 14px; font-weight: 500; padding: 8px 16px; transition: filter 120ms; }
2370
+ .ds-btn { border: none; cursor: pointer; font-size: 14px; font-weight: 500; padding: var(--component-padding-sm, 8px) var(--component-padding-md, 16px); border-radius: var(--radius-md, 4px); transition: filter 120ms; }
2311
2371
  .ds-btn:hover:not(:disabled) { filter: brightness(0.92); }
2312
- .ds-field { display: flex; flex-direction: column; gap: 4px; }
2372
+ .ds-field { display: flex; flex-direction: column; gap: var(--component-padding-xs, 4px); }
2313
2373
  .ds-label { font-size: 13px; font-weight: 500; }
2314
- .ds-input { border: 1.5px solid; padding: 8px 12px; font-size: 14px; outline: none; transition: border-color 150ms, box-shadow 150ms; width: 100%; }
2374
+ .ds-input { border: 1.5px solid; padding: var(--component-padding-sm, 8px) var(--component-padding-sm, 12px); font-size: 14px; outline: none; border-radius: var(--radius-sm, 2px); transition: border-color 150ms, box-shadow 150ms; width: 100%; }
2315
2375
  .ds-input:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-action, #2563eb) 20%, transparent); border-color: var(--color-action, #2563eb) !important; }
2316
- .ds-card { border: 1px solid; overflow: hidden; width: 220px; }
2317
- .ds-card-header { padding: 12px 14px; font-size: 14px; font-weight: 600; }
2318
- .ds-card-body { padding: 12px 14px; }
2319
- .ds-card-footer { padding: 10px 14px; display: flex; justify-content: flex-end; }
2376
+ .ds-card { border: 1px solid; overflow: hidden; width: 220px; border-radius: var(--radius-lg, 8px); }
2377
+ .ds-card-header { padding: var(--component-padding-sm, 12px) var(--component-padding-sm, 14px); font-size: 14px; font-weight: 600; }
2378
+ .ds-card-body { padding: var(--component-padding-sm, 12px) var(--component-padding-sm, 14px); }
2379
+ .ds-card-footer { padding: var(--component-padding-xs, 10px) var(--component-padding-sm, 14px); display: flex; justify-content: flex-end; }
2320
2380
 
2321
2381
  /* \u2500\u2500 Component docs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2322
2382
  .component-docs { margin-top: 36px; }
@@ -2375,6 +2435,16 @@ ${themeCssDark}
2375
2435
  ${esc(name)} / <span id="topbar-current">Colors</span>
2376
2436
  </div>
2377
2437
  <div class="topbar-actions">
2438
+ ${isPro ? `<div class="density-toggle" id="density-toggle">
2439
+ ${PRESETS.map((p) => `
2440
+ <button class="density-btn${p === defaultDensity ? " active" : ""}" onclick="setDensity('${p}', this)">${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2441
+ `).join("")}
2442
+ </div>` : `<div class="density-toggle locked" title="Density switching requires dsforge Pro. Set DSFORGE_KEY to unlock.">
2443
+ ${PRESETS.map((p) => `
2444
+ <button class="density-btn${p === defaultDensity ? " active" : ""}" disabled>${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2445
+ `).join("")}
2446
+ <span class="density-lock">\u2298 Pro</span>
2447
+ </div>`}
2378
2448
  ${themes.length >= 2 ? `
2379
2449
  <div class="theme-toggle">
2380
2450
  ${themes.map(
@@ -2415,6 +2485,22 @@ ${themeCssDark}
2415
2485
  btn.classList.add('active');
2416
2486
  }
2417
2487
 
2488
+ function setDensity(name, btn) {
2489
+ document.documentElement.setAttribute('data-density', name);
2490
+ document.querySelectorAll('.density-btn').forEach(b => b.classList.remove('active'));
2491
+ btn.classList.add('active');
2492
+ updateSpacingValues();
2493
+ }
2494
+
2495
+ function updateSpacingValues() {
2496
+ const style = getComputedStyle(document.documentElement);
2497
+ document.querySelectorAll('[data-spacing-var]').forEach(el => {
2498
+ const prop = el.getAttribute('data-spacing-var');
2499
+ const val = style.getPropertyValue(prop).trim();
2500
+ if (val) el.textContent = val;
2501
+ });
2502
+ }
2503
+
2418
2504
  function switchTab(compId, tabId, btn) {
2419
2505
  const tabs = document.querySelectorAll('#' + compId + '-tabs .comp-tab');
2420
2506
  const panels = document.querySelectorAll('#' + compId + '-tabs .comp-tab-panel');
@@ -2433,6 +2519,9 @@ ${themeCssDark}
2433
2519
  setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
2434
2520
  });
2435
2521
  }
2522
+
2523
+ // Populate spacing/radius value labels on load
2524
+ document.addEventListener('DOMContentLoaded', updateSpacingValues);
2436
2525
  </script>
2437
2526
  </body>
2438
2527
  </html>`;
package/dist/index.d.ts CHANGED
@@ -381,7 +381,19 @@ declare function hasRefs(value: string): boolean;
381
381
 
382
382
  declare function validateConfig(config: DesignSystemConfig, rules: RulesConfig): ValidationResult;
383
383
 
384
+ /**
385
+ * Density preset definitions.
386
+ *
387
+ * This is the single source of truth for compact / comfortable / spacious
388
+ * spacing and radius values. Imported by:
389
+ * - cli/commands/init.ts (initial config scaffolding)
390
+ * - cli/commands/generate.ts (re-applying preset on each generate run)
391
+ * - generators/tokens/css-vars.ts (emitting density.css for Pro)
392
+ * - generators/showcase/html.ts (embedding density CSS in showcase)
393
+ */
394
+
384
395
  type Preset = "compact" | "comfortable" | "spacious";
396
+
385
397
  declare function buildInitialConfig(name: string, preset?: Preset): DesignSystemConfig;
386
398
  declare function buildInitialRules(): RulesConfig;
387
399
 
package/dist/index.js CHANGED
@@ -1146,8 +1146,29 @@ var rl = readline.createInterface({
1146
1146
  // src/lib/license.ts
1147
1147
  import { readFileSync } from "fs";
1148
1148
  import { join } from "path";
1149
+ function readKeyFromDotEnv() {
1150
+ try {
1151
+ const content = readFileSync(join(process.cwd(), ".env"), "utf8");
1152
+ for (const raw of content.split("\n")) {
1153
+ const line = raw.trim();
1154
+ if (!line || line.startsWith("#")) continue;
1155
+ const eq = line.indexOf("=");
1156
+ if (eq === -1) continue;
1157
+ const key = line.slice(0, eq).trim();
1158
+ if (key !== "DSFORGE_KEY") continue;
1159
+ const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
1160
+ return val || void 0;
1161
+ }
1162
+ } catch {
1163
+ }
1164
+ return void 0;
1165
+ }
1166
+ function isProUnlocked() {
1167
+ const key = process.env["DSFORGE_KEY"] ?? readKeyFromDotEnv();
1168
+ return typeof key === "string" && key.length > 0;
1169
+ }
1149
1170
 
1150
- // src/cli/commands/init.ts
1171
+ // src/presets/index.ts
1151
1172
  var SPACING_PRESETS = {
1152
1173
  compact: {
1153
1174
  "1": 2,
@@ -1185,6 +1206,26 @@ var RADIUS_PRESETS = {
1185
1206
  comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
1186
1207
  spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
1187
1208
  };
1209
+ var PRESET_BASE_UNITS = {
1210
+ compact: 2,
1211
+ comfortable: 4,
1212
+ spacious: 6
1213
+ };
1214
+ function buildSemanticSpacing(scale) {
1215
+ return {
1216
+ "component-padding-xs": `${scale["1"]}`,
1217
+ "component-padding-sm": `${scale["2"]}`,
1218
+ "component-padding-md": `${scale["4"]}`,
1219
+ "component-padding-lg": `${scale["5"]}`,
1220
+ "layout-gap-xs": `${scale["2"]}`,
1221
+ "layout-gap-sm": `${scale["3"]}`,
1222
+ "layout-gap-md": `${scale["5"]}`,
1223
+ "layout-gap-lg": `${scale["6"]}`,
1224
+ "layout-section": `${scale["7"]}`
1225
+ };
1226
+ }
1227
+
1228
+ // src/cli/commands/init.ts
1188
1229
  function buildInitialConfig(name, preset = "comfortable") {
1189
1230
  const spacing = SPACING_PRESETS[preset];
1190
1231
  const radius = RADIUS_PRESETS[preset];
@@ -1375,19 +1416,9 @@ function buildInitialConfig(name, preset = "comfortable") {
1375
1416
  }
1376
1417
  },
1377
1418
  spacing: {
1378
- baseUnit: preset === "compact" ? 2 : preset === "spacious" ? 6 : 4,
1419
+ baseUnit: PRESET_BASE_UNITS[preset],
1379
1420
  scale: spacing,
1380
- semantic: {
1381
- "component-padding-xs": `${spacing[1]}`,
1382
- "component-padding-sm": `${spacing[2]}`,
1383
- "component-padding-md": `${spacing[4]}`,
1384
- "component-padding-lg": `${spacing[5]}`,
1385
- "layout-gap-xs": `${spacing[2]}`,
1386
- "layout-gap-sm": `${spacing[3]}`,
1387
- "layout-gap-md": `${spacing[5]}`,
1388
- "layout-gap-lg": `${spacing[6]}`,
1389
- "layout-section": `${spacing[7]}`
1390
- }
1421
+ semantic: buildSemanticSpacing(spacing)
1391
1422
  },
1392
1423
  radius,
1393
1424
  elevation: {
@@ -2563,6 +2594,62 @@ function generateThemeProvider(config) {
2563
2594
  const themeNames = Object.keys(config.themes ?? { light: {}, dark: {} });
2564
2595
  const defaultTheme = themeNames.includes("light") ? "light" : themeNames[0] ?? "light";
2565
2596
  const themeType = themeNames.map((t) => `"${t}"`).join(" | ");
2597
+ const isPro = isProUnlocked();
2598
+ const defaultDensity = config.meta.preset ?? "comfortable";
2599
+ const densityImport = isPro ? `
2600
+ import "../tokens/density.css";` : "";
2601
+ const densityTypes = isPro ? `
2602
+ export type DensityName = "compact" | "comfortable" | "spacious";
2603
+ ` : "";
2604
+ const densityContextTypes = isPro ? `
2605
+ export interface DensityContextValue {
2606
+ density: DensityName;
2607
+ setDensity: (density: DensityName) => void;
2608
+ }
2609
+ ` : "";
2610
+ const densityContext = isPro ? `
2611
+ export const DensityContext = React.createContext<DensityContextValue>({
2612
+ density: "${defaultDensity}",
2613
+ setDensity: () => undefined,
2614
+ });
2615
+
2616
+ /**
2617
+ * Hook to read and change the current density.
2618
+ * Must be used inside a <ThemeProvider>.
2619
+ */
2620
+ export function useDensity(): DensityContextValue {
2621
+ return React.useContext(DensityContext);
2622
+ }
2623
+ ` : "";
2624
+ const densityProp = isPro ? `
2625
+ /** Component density. Requires density.css to be imported. Defaults to "${defaultDensity}". */
2626
+ density?: DensityName;` : "";
2627
+ const densityOnChangeProp = isPro ? `
2628
+ /** Called when setDensity is invoked. */
2629
+ onDensityChange?: (density: DensityName) => void;` : "";
2630
+ const densityState = isPro ? `
2631
+ const [density, setDensityState] = React.useState<DensityName>(initialDensity);
2632
+
2633
+ React.useEffect(() => {
2634
+ setDensityState(initialDensity);
2635
+ }, [initialDensity]);
2636
+
2637
+ const setDensity = React.useCallback(
2638
+ (next: DensityName) => {
2639
+ setDensityState(next);
2640
+ onDensityChange?.(next);
2641
+ },
2642
+ [onDensityChange],
2643
+ );
2644
+ ` : "";
2645
+ const densityDestructure = isPro ? `,
2646
+ density: initialDensity = "${defaultDensity}",
2647
+ onDensityChange,` : "";
2648
+ const densityProviderOpen = isPro ? `
2649
+ <DensityContext.Provider value={{ density, setDensity }}>` : "";
2650
+ const densityDataAttr = isPro ? ` data-density={density}` : "";
2651
+ const densityProviderClose = isPro ? `
2652
+ </DensityContext.Provider>` : "";
2566
2653
  return `/**
2567
2654
  * ThemeProvider \u2014 ${config.meta.name}
2568
2655
  *
@@ -2574,27 +2661,27 @@ function generateThemeProvider(config) {
2574
2661
  * import "@${config.meta.name}/tokens/light.css"; // or dark.css
2575
2662
  * import { ThemeProvider } from "@${config.meta.name}";
2576
2663
  *
2577
- * <ThemeProvider theme="light">
2664
+ * <ThemeProvider theme="light"${isPro ? ` density="${defaultDensity}"` : ""}>
2578
2665
  * <App />
2579
2666
  * </ThemeProvider>
2580
2667
  */
2581
2668
 
2582
- import React from "react";
2669
+ import React from "react";${densityImport}
2583
2670
 
2584
2671
  // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2585
2672
 
2586
2673
  export type ThemeName = ${themeType};
2587
-
2674
+ ${densityTypes}
2588
2675
  export interface ThemeContextValue {
2589
2676
  theme: ThemeName;
2590
2677
  setTheme: (theme: ThemeName) => void;
2591
2678
  }
2592
-
2679
+ ${densityContextTypes}
2593
2680
  export interface ThemeProviderProps {
2594
2681
  /** Initial theme. Defaults to "${defaultTheme}". */
2595
2682
  theme?: ThemeName;
2596
2683
  /** Called when setTheme is invoked \u2014 use to persist theme preference. */
2597
- onThemeChange?: (theme: ThemeName) => void;
2684
+ onThemeChange?: (theme: ThemeName) => void;${densityProp}${densityOnChangeProp}
2598
2685
  children: React.ReactNode;
2599
2686
  }
2600
2687
 
@@ -2612,12 +2699,12 @@ export const ThemeContext = React.createContext<ThemeContextValue>({
2612
2699
  export function useTheme(): ThemeContextValue {
2613
2700
  return React.useContext(ThemeContext);
2614
2701
  }
2615
-
2702
+ ${densityContext}
2616
2703
  // \u2500\u2500\u2500 Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2617
2704
 
2618
2705
  export function ThemeProvider({
2619
2706
  theme: initialTheme = "${defaultTheme}",
2620
- onThemeChange,
2707
+ onThemeChange,${densityDestructure}
2621
2708
  children,
2622
2709
  }: ThemeProviderProps) {
2623
2710
  const [theme, setThemeState] = React.useState<ThemeName>(initialTheme);
@@ -2633,13 +2720,13 @@ export function ThemeProvider({
2633
2720
  },
2634
2721
  [onThemeChange],
2635
2722
  );
2636
-
2637
- return (
2638
- <ThemeContext.Provider value={{ theme, setTheme }}>
2639
- <div data-theme={theme} style={{ display: "contents" }}>
2640
- {children}
2641
- </div>
2642
- </ThemeContext.Provider>
2723
+ ${densityState}
2724
+ return (${densityProviderOpen}
2725
+ <ThemeContext.Provider value={{ theme, setTheme }}>
2726
+ <div data-theme={theme}${densityDataAttr} style={{ display: "contents" }}>
2727
+ {children}
2728
+ </div>
2729
+ </ThemeContext.Provider>${densityProviderClose}
2643
2730
  );
2644
2731
  }
2645
2732
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nghitrum/dsforge",
3
- "version": "0.1.5-alpha.6",
3
+ "version": "0.1.5-alpha.8",
4
4
  "description": "AI-native design system generator — tokens → components → docs → npm",
5
5
  "keywords": [
6
6
  "design-system",
@@ -1,28 +0,0 @@
1
- // src/lib/license.ts
2
- import { readFileSync } from "fs";
3
- import { join } from "path";
4
- function readKeyFromDotEnv() {
5
- try {
6
- const content = readFileSync(join(process.cwd(), ".env"), "utf8");
7
- for (const raw of content.split("\n")) {
8
- const line = raw.trim();
9
- if (!line || line.startsWith("#")) continue;
10
- const eq = line.indexOf("=");
11
- if (eq === -1) continue;
12
- const key = line.slice(0, eq).trim();
13
- if (key !== "DSFORGE_KEY") continue;
14
- const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
15
- return val || void 0;
16
- }
17
- } catch {
18
- }
19
- return void 0;
20
- }
21
- function isProUnlocked() {
22
- const key = process.env["DSFORGE_KEY"] ?? readKeyFromDotEnv();
23
- return typeof key === "string" && key.length > 0;
24
- }
25
-
26
- export {
27
- isProUnlocked
28
- };