@nghitrum/dsforge 0.1.5-alpha.5 → 0.1.5-alpha.7

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.
@@ -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
+ };
@@ -27,7 +27,7 @@ function generatePackageJson(config, componentNames) {
27
27
  componentNames.map((c) => [`./metadata/${c}`, `./metadata/${c}.json`])
28
28
  )
29
29
  },
30
- files: ["dist", "tokens", "metadata", "docs", "CHANGELOG.md"],
30
+ files: ["dist", "tokens", "metadata", "CHANGELOG.md"],
31
31
  scripts: {
32
32
  build: "tsc",
33
33
  prepublishOnly: "npm run build"
package/dist/cli/index.js CHANGED
@@ -3,7 +3,16 @@ import {
3
3
  generateChangelog,
4
4
  generatePackageJson,
5
5
  generateTsConfig
6
- } from "../chunk-RI3XDGKU.js";
6
+ } from "../chunk-QHE35QQQ.js";
7
+ import {
8
+ PRESETS,
9
+ PRESET_BASE_UNITS,
10
+ RADIUS_PRESETS,
11
+ SPACING_PRESETS,
12
+ applyPreset,
13
+ buildSemanticSpacing,
14
+ isProUnlocked
15
+ } from "../chunk-JUMR3N5J.js";
7
16
 
8
17
  // src/cli/index.ts
9
18
  import { program } from "commander";
@@ -399,43 +408,6 @@ async function confirm(question) {
399
408
  }
400
409
 
401
410
  // src/cli/commands/init.ts
402
- var SPACING_PRESETS = {
403
- compact: {
404
- "1": 2,
405
- "2": 4,
406
- "3": 8,
407
- "4": 12,
408
- "5": 16,
409
- "6": 24,
410
- "7": 32,
411
- "8": 48
412
- },
413
- comfortable: {
414
- "1": 4,
415
- "2": 8,
416
- "3": 12,
417
- "4": 16,
418
- "5": 24,
419
- "6": 32,
420
- "7": 48,
421
- "8": 64
422
- },
423
- spacious: {
424
- "1": 6,
425
- "2": 12,
426
- "3": 18,
427
- "4": 24,
428
- "5": 36,
429
- "6": 48,
430
- "7": 72,
431
- "8": 96
432
- }
433
- };
434
- var RADIUS_PRESETS = {
435
- compact: { none: 0, sm: 2, md: 3, lg: 6, xl: 10, full: 9999 },
436
- comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
437
- spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
438
- };
439
411
  function buildInitialConfig(name, preset = "comfortable") {
440
412
  const spacing = SPACING_PRESETS[preset];
441
413
  const radius = RADIUS_PRESETS[preset];
@@ -626,19 +598,9 @@ function buildInitialConfig(name, preset = "comfortable") {
626
598
  }
627
599
  },
628
600
  spacing: {
629
- baseUnit: preset === "compact" ? 2 : preset === "spacious" ? 6 : 4,
601
+ baseUnit: PRESET_BASE_UNITS[preset],
630
602
  scale: spacing,
631
- semantic: {
632
- "component-padding-xs": `${spacing[1]}`,
633
- "component-padding-sm": `${spacing[2]}`,
634
- "component-padding-md": `${spacing[4]}`,
635
- "component-padding-lg": `${spacing[5]}`,
636
- "layout-gap-xs": `${spacing[2]}`,
637
- "layout-gap-sm": `${spacing[3]}`,
638
- "layout-gap-md": `${spacing[5]}`,
639
- "layout-gap-lg": `${spacing[6]}`,
640
- "layout-section": `${spacing[7]}`
641
- }
603
+ semantic: buildSemanticSpacing(spacing)
642
604
  },
643
605
  radius,
644
606
  elevation: {
@@ -775,7 +737,15 @@ async function runInit(cwd, options) {
775
737
  }
776
738
  const name = rawName.replace(/\s+/g, "-").toLowerCase();
777
739
  let preset;
778
- if (options.preset && VALID_PRESETS.includes(options.preset)) {
740
+ if (!isProUnlocked()) {
741
+ if (options.preset && options.preset !== "comfortable") {
742
+ logger.hint(
743
+ `Preset "${options.preset}" requires dsforge Pro`,
744
+ `Set DSFORGE_KEY to unlock compact and spacious. Using comfortable.`
745
+ );
746
+ }
747
+ preset = "comfortable";
748
+ } else if (options.preset && VALID_PRESETS.includes(options.preset)) {
779
749
  preset = options.preset;
780
750
  } else {
781
751
  const answer = await ask(
@@ -1861,6 +1831,34 @@ function emitThemeCss(themeName, themeOverrides, config) {
1861
1831
  lines.push(emitBlock(`:root[data-theme="${themeName}"]`, entries));
1862
1832
  return lines.join("\n") + "\n";
1863
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
+ }
1864
1862
  function generateCssFiles(config, resolution) {
1865
1863
  const files = [];
1866
1864
  files.push({
@@ -4088,6 +4086,62 @@ function generateThemeProvider(config) {
4088
4086
  const themeNames = Object.keys(config.themes ?? { light: {}, dark: {} });
4089
4087
  const defaultTheme = themeNames.includes("light") ? "light" : themeNames[0] ?? "light";
4090
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>` : "";
4091
4145
  return `/**
4092
4146
  * ThemeProvider \u2014 ${config.meta.name}
4093
4147
  *
@@ -4099,27 +4153,27 @@ function generateThemeProvider(config) {
4099
4153
  * import "@${config.meta.name}/tokens/light.css"; // or dark.css
4100
4154
  * import { ThemeProvider } from "@${config.meta.name}";
4101
4155
  *
4102
- * <ThemeProvider theme="light">
4156
+ * <ThemeProvider theme="light"${isPro ? ` density="${defaultDensity}"` : ""}>
4103
4157
  * <App />
4104
4158
  * </ThemeProvider>
4105
4159
  */
4106
4160
 
4107
- import React from "react";
4161
+ import React from "react";${densityImport}
4108
4162
 
4109
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
4110
4164
 
4111
4165
  export type ThemeName = ${themeType};
4112
-
4166
+ ${densityTypes}
4113
4167
  export interface ThemeContextValue {
4114
4168
  theme: ThemeName;
4115
4169
  setTheme: (theme: ThemeName) => void;
4116
4170
  }
4117
-
4171
+ ${densityContextTypes}
4118
4172
  export interface ThemeProviderProps {
4119
4173
  /** Initial theme. Defaults to "${defaultTheme}". */
4120
4174
  theme?: ThemeName;
4121
4175
  /** Called when setTheme is invoked \u2014 use to persist theme preference. */
4122
- onThemeChange?: (theme: ThemeName) => void;
4176
+ onThemeChange?: (theme: ThemeName) => void;${densityProp}${densityOnChangeProp}
4123
4177
  children: React.ReactNode;
4124
4178
  }
4125
4179
 
@@ -4137,12 +4191,12 @@ export const ThemeContext = React.createContext<ThemeContextValue>({
4137
4191
  export function useTheme(): ThemeContextValue {
4138
4192
  return React.useContext(ThemeContext);
4139
4193
  }
4140
-
4194
+ ${densityContext}
4141
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
4142
4196
 
4143
4197
  export function ThemeProvider({
4144
4198
  theme: initialTheme = "${defaultTheme}",
4145
- onThemeChange,
4199
+ onThemeChange,${densityDestructure}
4146
4200
  children,
4147
4201
  }: ThemeProviderProps) {
4148
4202
  const [theme, setThemeState] = React.useState<ThemeName>(initialTheme);
@@ -4158,13 +4212,13 @@ export function ThemeProvider({
4158
4212
  },
4159
4213
  [onThemeChange],
4160
4214
  );
4161
-
4162
- return (
4163
- <ThemeContext.Provider value={{ theme, setTheme }}>
4164
- <div data-theme={theme} style={{ display: "contents" }}>
4165
- {children}
4166
- </div>
4167
- </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}
4168
4222
  );
4169
4223
  }
4170
4224
  `;
@@ -4761,6 +4815,10 @@ async function runGenerate(cwd, options) {
4761
4815
  options.debug ?? false
4762
4816
  );
4763
4817
  }
4818
+ const presetValue = config.meta.preset;
4819
+ if (presetValue === "compact" || presetValue === "comfortable" || presetValue === "spacious") {
4820
+ applyPreset(config, presetValue);
4821
+ }
4764
4822
  const fullRules = Object.fromEntries(
4765
4823
  REACT_COMPONENTS.map((name) => [name, rules[name] ?? {}])
4766
4824
  );
@@ -4817,6 +4875,13 @@ async function runGenerate(cwd, options) {
4817
4875
  await writeFile(path3.join(tokensDir, filename), content);
4818
4876
  logger.dim(` \u2192 tokens/${filename}`);
4819
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
+ }
4820
4885
  const tokenFiles = reactAdapter.generateTokenFiles(config, resolution);
4821
4886
  for (const { filename, content } of tokenFiles) {
4822
4887
  await writeFile(path3.join(tokensDir, filename), content);
@@ -4855,7 +4920,7 @@ async function runGenerate(cwd, options) {
4855
4920
  logger.dim(` \u2192 src/${idxFile}`);
4856
4921
  logger.success(`${generatedNames.length} components generated`);
4857
4922
  }
4858
- if (!only || only === "metadata") {
4923
+ if (isProUnlocked() && (!only || only === "metadata")) {
4859
4924
  logger.step("Writing AI metadata...");
4860
4925
  const metaDir = path3.join(outRoot, "metadata");
4861
4926
  await ensureDir(metaDir);
@@ -4866,27 +4931,6 @@ async function runGenerate(cwd, options) {
4866
4931
  }
4867
4932
  logger.success(`Metadata written (${metaFiles.length} files)`);
4868
4933
  }
4869
- if (!only || only === "docs") {
4870
- logger.step("Generating docs...");
4871
- const docsDir = path3.join(outRoot, "docs");
4872
- await ensureDir(docsDir);
4873
- const metadataFiles = generateMetadata(config, fullRules, tokenCount);
4874
- const metadataMap = {};
4875
- for (const { filename, content } of metadataFiles) {
4876
- const name = filename.replace(".json", "");
4877
- if (name !== "index") {
4878
- metadataMap[name] = JSON.parse(
4879
- content
4880
- );
4881
- }
4882
- }
4883
- const docFiles = reactAdapter.generateDocs(config, fullRules, metadataMap);
4884
- for (const { filename, content } of docFiles) {
4885
- await writeFile(path3.join(docsDir, filename), content);
4886
- logger.dim(` \u2192 docs/${filename}`);
4887
- }
4888
- logger.success(`Docs written (${docFiles.length} files)`);
4889
- }
4890
4934
  if (!only) {
4891
4935
  logger.step("Writing package files...");
4892
4936
  const componentNames = Object.keys(fullRules);
@@ -4896,7 +4940,7 @@ async function runGenerate(cwd, options) {
4896
4940
  const tsConfig = generateTsConfig();
4897
4941
  await writeFile(path3.join(outRoot, "tsconfig.json"), tsConfig);
4898
4942
  logger.dim(` \u2192 tsconfig.json`);
4899
- const { generateReadme } = await import("../emitter-ZNRPJ4D6.js");
4943
+ const { generateReadme } = await import("../emitter-KNYIQTS5.js");
4900
4944
  await writeFile(
4901
4945
  path3.join(outRoot, "README.md"),
4902
4946
  generateReadme(config, componentNames)
@@ -4912,7 +4956,7 @@ async function runGenerate(cwd, options) {
4912
4956
  logger.success(`Package files written`);
4913
4957
  }
4914
4958
  logger.step("Generating showcase...");
4915
- const { generateShowcase } = await import("../html-6SIG34W5.js");
4959
+ const { generateShowcase } = await import("../html-LQHDCSG4.js");
4916
4960
  const showcaseHtml = generateShowcase(config, resolution);
4917
4961
  await writeFile(path3.join(outRoot, "showcase.html"), showcaseHtml);
4918
4962
  logger.dim(` \u2192 showcase.html`);
@@ -5299,7 +5343,7 @@ async function runMenu() {
5299
5343
  // package.json
5300
5344
  var package_default = {
5301
5345
  name: "@nghitrum/dsforge",
5302
- version: "0.1.5-alpha.5",
5346
+ version: "0.1.5-alpha.7",
5303
5347
  description: "AI-native design system generator \u2014 tokens \u2192 components \u2192 docs \u2192 npm",
5304
5348
  keywords: [
5305
5349
  "design-system",
@@ -3,7 +3,7 @@ import {
3
3
  generatePackageJson,
4
4
  generateReadme,
5
5
  generateTsConfig
6
- } from "./chunk-RI3XDGKU.js";
6
+ } from "./chunk-QHE35QQQ.js";
7
7
  export {
8
8
  generateChangelog,
9
9
  generatePackageJson,
@@ -1,3 +1,11 @@
1
+ import {
2
+ PRESETS,
3
+ RADIUS_PRESETS,
4
+ SPACING_PRESETS,
5
+ buildSemanticSpacing,
6
+ isProUnlocked
7
+ } from "./chunk-JUMR3N5J.js";
8
+
1
9
  // src/generators/showcase/types.ts
2
10
  function esc(s) {
3
11
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -32,31 +40,6 @@ function componentTokens(config, tokens) {
32
40
  };
33
41
  }
34
42
 
35
- // src/lib/license.ts
36
- import { readFileSync } from "fs";
37
- import { join } from "path";
38
- function readKeyFromDotEnv() {
39
- try {
40
- const content = readFileSync(join(process.cwd(), ".env"), "utf8");
41
- for (const raw of content.split("\n")) {
42
- const line = raw.trim();
43
- if (!line || line.startsWith("#")) continue;
44
- const eq = line.indexOf("=");
45
- if (eq === -1) continue;
46
- const key = line.slice(0, eq).trim();
47
- if (key !== "DSFORGE_KEY") continue;
48
- const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
49
- return val || void 0;
50
- }
51
- } catch {
52
- }
53
- return void 0;
54
- }
55
- function isProUnlocked() {
56
- const key = process.env["DSFORGE_KEY"] ?? readKeyFromDotEnv();
57
- return typeof key === "string" && key.length > 0;
58
- }
59
-
60
43
  // src/generators/showcase/foundations.ts
61
44
  function buildColorSection(config, tokens) {
62
45
  const groups = [];
@@ -117,26 +100,33 @@ function buildTypographySection(config) {
117
100
  }
118
101
  function buildSpacingSection(config) {
119
102
  const scale = config.spacing?.scale ?? {};
120
- const baseUnit = config.spacing?.baseUnit ?? 4;
121
- 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) => `
122
112
  <div class="spacing-row">
123
113
  <span class="spacing-key">${esc(key)}</span>
124
114
  <div class="spacing-bar-wrap">
125
- <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>
126
116
  </div>
127
- <span class="spacing-val">${val}px</span>
117
+ <span class="spacing-val" data-spacing-var="--${esc(key)}"></span>
128
118
  </div>`;
129
119
  return `
130
120
  <div class="section-block">
131
- <h3 class="group-title">Base Unit: ${baseUnit}px</h3>
121
+ <h3 class="group-title">Scale</h3>
132
122
  <div class="spacing-list">
133
- ${Object.entries(scale).map(([k, v]) => row(k, v)).join("")}
123
+ ${Object.keys(scale).map((k) => scaleRow(k)).join("")}
134
124
  </div>
135
125
  </div>
136
126
  <div class="section-block">
137
127
  <h3 class="group-title">Semantic Spacing</h3>
138
128
  <div class="spacing-list">
139
- ${Object.entries(config.spacing?.semantic ?? {}).map(([k, v]) => row(k, v)).join("")}
129
+ ${Object.keys(config.spacing?.semantic ?? {}).map((k) => semanticRow(k)).join("")}
140
130
  </div>
141
131
  </div>
142
132
  `;
@@ -147,12 +137,12 @@ function buildRadiusSection(config) {
147
137
  <div class="section-block">
148
138
  <h3 class="group-title">Border Radius</h3>
149
139
  <div class="radius-grid">
150
- ${Object.entries(radius).map(
151
- ([key, val]) => `
140
+ ${Object.keys(radius).map(
141
+ (key) => `
152
142
  <div class="radius-item">
153
- <div class="radius-box" style="border-radius:${val}px"></div>
143
+ <div class="radius-box" style="border-radius:var(--radius-${esc(key)})"></div>
154
144
  <span class="radius-key">${esc(key)}</span>
155
- <span class="radius-val">${val}px</span>
145
+ <span class="radius-val" data-spacing-var="--radius-${esc(key)}"></span>
156
146
  </div>
157
147
  `
158
148
  ).join("")}
@@ -231,7 +221,9 @@ var lockedPanel = (label) => `
231
221
  function buildComponentPage(def, isPro) {
232
222
  const tabId = (tab) => `${def.id}-tab-${tab}`;
233
223
  const panelId = (tab) => `${def.id}-panel-${tab}`;
234
- const overviewHtml = `<div class="comp-overview">${def.overviewHtml}</div>`;
224
+ const overviewHtml = `
225
+ <div class="comp-overview">${def.overviewHtml}</div>
226
+ <p class="component-description">${esc(def.description)}</p>`;
235
227
  const propsTable = `
236
228
  <table class="props-table">
237
229
  <thead>
@@ -366,6 +358,9 @@ function buttonDef(config, tokens) {
366
358
  id: "button",
367
359
  label: "Button",
368
360
  description: "Triggers an action or event. Use for form submissions, dialogs, and in-page actions.",
361
+ usageExample: `<Button variant="primary" size="md" onClick={() => {}}>
362
+ Save changes
363
+ </Button>`,
369
364
  overviewHtml: `
370
365
  <div class="comp-overview-section">
371
366
  <div class="comp-overview-label">Variants</div>
@@ -552,6 +547,13 @@ function inputDef(config, tokens) {
552
547
  id: "input",
553
548
  label: "Input",
554
549
  description: "Single-line text field. Covers all standard input types with label, helper text, and validation states.",
550
+ usageExample: `<Input
551
+ label="Email"
552
+ placeholder="you@example.com"
553
+ value={email}
554
+ onChange={(e) => setEmail(e.target.value)}
555
+ error={emailError}
556
+ />`,
555
557
  overviewHtml: `
556
558
  <div class="comp-overview-section">
557
559
  <div class="comp-overview-label">States</div>
@@ -757,6 +759,10 @@ function cardDef(config, tokens) {
757
759
  id: "card",
758
760
  label: "Card",
759
761
  description: "A surface that groups related content. Supports header, body, and optional footer slots.",
762
+ usageExample: `<Card padding="lg">
763
+ <h2>Card title</h2>
764
+ <p>Card content goes here.</p>
765
+ </Card>`,
760
766
  overviewHtml: `
761
767
  <div class="comp-overview-section">
762
768
  <div class="comp-overview-label">Variants</div>
@@ -913,6 +919,8 @@ function badgeDef(config, tokens) {
913
919
  id: "badge",
914
920
  label: "Badge",
915
921
  description: "Compact label for status, categories, or counts. Display-only \u2014 not interactive.",
922
+ usageExample: `<Badge variant="success">Active</Badge>
923
+ <Badge variant="warning">Pending</Badge>`,
916
924
  overviewHtml: `
917
925
  <div class="comp-overview-section">
918
926
  <div class="comp-overview-label">Variants</div>
@@ -1057,6 +1065,11 @@ function checkboxDef(config, tokens) {
1057
1065
  id: "checkbox",
1058
1066
  label: "Checkbox",
1059
1067
  description: "Binary toggle for boolean values. Supports indeterminate state for partial selections.",
1068
+ usageExample: `<Checkbox
1069
+ label="Accept terms"
1070
+ checked={accepted}
1071
+ onChange={(e) => setAccepted(e.target.checked)}
1072
+ />`,
1060
1073
  overviewHtml: `
1061
1074
  <div class="comp-overview-section">
1062
1075
  <div class="comp-overview-label">States</div>
@@ -1239,6 +1252,8 @@ function radioDef(config, tokens) {
1239
1252
  id: "radio",
1240
1253
  label: "Radio",
1241
1254
  description: "Single selection within a mutually exclusive group. Always pair Radio with RadioGroup.",
1255
+ usageExample: `<Radio label="Option A" name="choice" value="a" checked={choice === 'a'} onChange={() => setChoice('a')} />
1256
+ <Radio label="Option B" name="choice" value="b" checked={choice === 'b'} onChange={() => setChoice('b')} />`,
1242
1257
  overviewHtml: `
1243
1258
  <div class="comp-overview-section">
1244
1259
  <div class="comp-overview-label">RadioGroup (vertical)</div>
@@ -1425,6 +1440,12 @@ function selectDef(config, tokens) {
1425
1440
  id: "select",
1426
1441
  label: "Select",
1427
1442
  description: "Dropdown picker for selecting from a list of options. Wraps native <select> for full accessibility.",
1443
+ usageExample: `<Select
1444
+ label="Country"
1445
+ options={[{ label: 'Norway', value: 'no' }, { label: 'Sweden', value: 'se' }]}
1446
+ value={country}
1447
+ onChange={(e) => setCountry(e.target.value)}
1448
+ />`,
1428
1449
  overviewHtml: `
1429
1450
  <div class="comp-overview-section">
1430
1451
  <div class="comp-overview-label">States</div>
@@ -1610,6 +1631,12 @@ function toastDef(config, tokens) {
1610
1631
  id: "toast",
1611
1632
  label: "Toast / Alert",
1612
1633
  description: "Feedback for user actions. Alert is inline and static; Toast is an overlay with auto-dismiss and a useToast() hook.",
1634
+ usageExample: `<Toast
1635
+ message="Changes saved successfully"
1636
+ variant="success"
1637
+ duration={3000}
1638
+ onDismiss={() => setToast(null)}
1639
+ />`,
1613
1640
  overviewHtml: `
1614
1641
  <div class="comp-overview-section">
1615
1642
  <div class="comp-overview-label">Alert \u2014 inline variants</div>
@@ -1791,6 +1818,7 @@ function spinnerDef(config, tokens) {
1791
1818
  id: "spinner",
1792
1819
  label: "Spinner",
1793
1820
  description: "Loading indicator for async operations. Includes a visually hidden status label for screen readers.",
1821
+ usageExample: `<Spinner size="lg" label="Saving your changes" />`,
1794
1822
  overviewHtml: `
1795
1823
  <div class="comp-overview-section">
1796
1824
  <div class="comp-overview-label">Sizes</div>
@@ -1966,11 +1994,29 @@ var SHOWCASE_COMPONENTS = [
1966
1994
  ];
1967
1995
 
1968
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
+ }
1969
2014
  function generateShowcase(config, resolution) {
1970
2015
  const tokens = resolution.tokens;
1971
2016
  const name = config.meta?.name ?? "Design System";
1972
2017
  const version = config.meta?.version ?? "0.1.0";
1973
2018
  const themes = Object.keys(config.themes ?? {});
2019
+ const defaultDensity = config.meta?.preset ?? "comfortable";
1974
2020
  const foundationItems = [
1975
2021
  { id: "colors", label: "Colors" },
1976
2022
  { id: "typography", label: "Typography" },
@@ -2006,8 +2052,9 @@ function generateShowcase(config, resolution) {
2006
2052
  const darkTheme = config.themes?.["dark"] ?? {};
2007
2053
  const themeCssLight = Object.entries({ ...flatTokens, ...lightTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2008
2054
  const themeCssDark = Object.entries({ ...flatTokens, ...darkTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2055
+ const densityCss = buildDensityCss();
2009
2056
  return `<!DOCTYPE html>
2010
- <html lang="en" data-theme="light">
2057
+ <html lang="en" data-theme="light" data-density="${defaultDensity}">
2011
2058
  <head>
2012
2059
  <meta charset="UTF-8" />
2013
2060
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -2022,6 +2069,9 @@ ${themeCssLight}
2022
2069
  ${themeCssDark}
2023
2070
  }
2024
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
+
2025
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 */
2026
2076
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2027
2077
  html { font-size: 16px; }
@@ -2087,6 +2137,33 @@ ${themeCssDark}
2087
2137
  color: var(--color-text-primary, #0f172a); font-weight: 500;
2088
2138
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2089
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
+ }
2090
2167
  .content { padding: 36px 40px 80px; max-width: 860px; }
2091
2168
  .page { display: none; }
2092
2169
  .page.active { display: block; }
@@ -2290,16 +2367,37 @@ ${themeCssDark}
2290
2367
  .locked-hint code { background: var(--color-bg-overlay, #f1f5f9); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--color-border-default, #e2e8f0); }
2291
2368
 
2292
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 */
2293
- .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; }
2294
2371
  .ds-btn:hover:not(:disabled) { filter: brightness(0.92); }
2295
- .ds-field { display: flex; flex-direction: column; gap: 4px; }
2372
+ .ds-field { display: flex; flex-direction: column; gap: var(--component-padding-xs, 4px); }
2296
2373
  .ds-label { font-size: 13px; font-weight: 500; }
2297
- .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%; }
2298
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; }
2299
- .ds-card { border: 1px solid; overflow: hidden; width: 220px; }
2300
- .ds-card-header { padding: 12px 14px; font-size: 14px; font-weight: 600; }
2301
- .ds-card-body { padding: 12px 14px; }
2302
- .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; }
2380
+
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 */
2382
+ .component-docs { margin-top: 36px; }
2383
+ .component-description {
2384
+ font-size: 14px; color: var(--color-text-secondary, #64748b);
2385
+ line-height: 1.65; margin-bottom: 24px; max-width: 640px;
2386
+ }
2387
+ .component-docs h4 {
2388
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
2389
+ letter-spacing: 0.07em; color: var(--color-text-secondary, #64748b);
2390
+ margin-bottom: 12px; padding-bottom: 8px;
2391
+ border-bottom: 1px solid var(--color-border-default, #e2e8f0);
2392
+ }
2393
+ .usage-example {
2394
+ margin: 0; padding: 16px 18px;
2395
+ background: var(--color-bg-default, #fff);
2396
+ color: var(--color-text-primary, #0f172a);
2397
+ border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 8px;
2398
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
2399
+ font-size: 12.5px; line-height: 1.65; overflow-x: auto; white-space: pre;
2400
+ }
2303
2401
 
2304
2402
  /* \u2500\u2500 Spinner animation \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 */
2305
2403
  @keyframes dsforge-spin { to { transform: rotate(360deg); } }
@@ -2337,6 +2435,16 @@ ${themeCssDark}
2337
2435
  ${esc(name)} / <span id="topbar-current">Colors</span>
2338
2436
  </div>
2339
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>`}
2340
2448
  ${themes.length >= 2 ? `
2341
2449
  <div class="theme-toggle">
2342
2450
  ${themes.map(
@@ -2377,6 +2485,22 @@ ${themeCssDark}
2377
2485
  btn.classList.add('active');
2378
2486
  }
2379
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
+
2380
2504
  function switchTab(compId, tabId, btn) {
2381
2505
  const tabs = document.querySelectorAll('#' + compId + '-tabs .comp-tab');
2382
2506
  const panels = document.querySelectorAll('#' + compId + '-tabs .comp-tab-panel');
@@ -2395,6 +2519,9 @@ ${themeCssDark}
2395
2519
  setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
2396
2520
  });
2397
2521
  }
2522
+
2523
+ // Populate spacing/radius value labels on load
2524
+ document.addEventListener('DOMContentLoaded', updateSpacingValues);
2398
2525
  </script>
2399
2526
  </body>
2400
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
@@ -1143,7 +1143,32 @@ var rl = readline.createInterface({
1143
1143
  output: process.stdout
1144
1144
  });
1145
1145
 
1146
- // src/cli/commands/init.ts
1146
+ // src/lib/license.ts
1147
+ import { readFileSync } from "fs";
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
+ }
1170
+
1171
+ // src/presets/index.ts
1147
1172
  var SPACING_PRESETS = {
1148
1173
  compact: {
1149
1174
  "1": 2,
@@ -1181,6 +1206,26 @@ var RADIUS_PRESETS = {
1181
1206
  comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
1182
1207
  spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
1183
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
1184
1229
  function buildInitialConfig(name, preset = "comfortable") {
1185
1230
  const spacing = SPACING_PRESETS[preset];
1186
1231
  const radius = RADIUS_PRESETS[preset];
@@ -1371,19 +1416,9 @@ function buildInitialConfig(name, preset = "comfortable") {
1371
1416
  }
1372
1417
  },
1373
1418
  spacing: {
1374
- baseUnit: preset === "compact" ? 2 : preset === "spacious" ? 6 : 4,
1419
+ baseUnit: PRESET_BASE_UNITS[preset],
1375
1420
  scale: spacing,
1376
- semantic: {
1377
- "component-padding-xs": `${spacing[1]}`,
1378
- "component-padding-sm": `${spacing[2]}`,
1379
- "component-padding-md": `${spacing[4]}`,
1380
- "component-padding-lg": `${spacing[5]}`,
1381
- "layout-gap-xs": `${spacing[2]}`,
1382
- "layout-gap-sm": `${spacing[3]}`,
1383
- "layout-gap-md": `${spacing[5]}`,
1384
- "layout-gap-lg": `${spacing[6]}`,
1385
- "layout-section": `${spacing[7]}`
1386
- }
1421
+ semantic: buildSemanticSpacing(spacing)
1387
1422
  },
1388
1423
  radius,
1389
1424
  elevation: {
@@ -2559,6 +2594,62 @@ function generateThemeProvider(config) {
2559
2594
  const themeNames = Object.keys(config.themes ?? { light: {}, dark: {} });
2560
2595
  const defaultTheme = themeNames.includes("light") ? "light" : themeNames[0] ?? "light";
2561
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>` : "";
2562
2653
  return `/**
2563
2654
  * ThemeProvider \u2014 ${config.meta.name}
2564
2655
  *
@@ -2570,27 +2661,27 @@ function generateThemeProvider(config) {
2570
2661
  * import "@${config.meta.name}/tokens/light.css"; // or dark.css
2571
2662
  * import { ThemeProvider } from "@${config.meta.name}";
2572
2663
  *
2573
- * <ThemeProvider theme="light">
2664
+ * <ThemeProvider theme="light"${isPro ? ` density="${defaultDensity}"` : ""}>
2574
2665
  * <App />
2575
2666
  * </ThemeProvider>
2576
2667
  */
2577
2668
 
2578
- import React from "react";
2669
+ import React from "react";${densityImport}
2579
2670
 
2580
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
2581
2672
 
2582
2673
  export type ThemeName = ${themeType};
2583
-
2674
+ ${densityTypes}
2584
2675
  export interface ThemeContextValue {
2585
2676
  theme: ThemeName;
2586
2677
  setTheme: (theme: ThemeName) => void;
2587
2678
  }
2588
-
2679
+ ${densityContextTypes}
2589
2680
  export interface ThemeProviderProps {
2590
2681
  /** Initial theme. Defaults to "${defaultTheme}". */
2591
2682
  theme?: ThemeName;
2592
2683
  /** Called when setTheme is invoked \u2014 use to persist theme preference. */
2593
- onThemeChange?: (theme: ThemeName) => void;
2684
+ onThemeChange?: (theme: ThemeName) => void;${densityProp}${densityOnChangeProp}
2594
2685
  children: React.ReactNode;
2595
2686
  }
2596
2687
 
@@ -2608,12 +2699,12 @@ export const ThemeContext = React.createContext<ThemeContextValue>({
2608
2699
  export function useTheme(): ThemeContextValue {
2609
2700
  return React.useContext(ThemeContext);
2610
2701
  }
2611
-
2702
+ ${densityContext}
2612
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
2613
2704
 
2614
2705
  export function ThemeProvider({
2615
2706
  theme: initialTheme = "${defaultTheme}",
2616
- onThemeChange,
2707
+ onThemeChange,${densityDestructure}
2617
2708
  children,
2618
2709
  }: ThemeProviderProps) {
2619
2710
  const [theme, setThemeState] = React.useState<ThemeName>(initialTheme);
@@ -2629,13 +2720,13 @@ export function ThemeProvider({
2629
2720
  },
2630
2721
  [onThemeChange],
2631
2722
  );
2632
-
2633
- return (
2634
- <ThemeContext.Provider value={{ theme, setTheme }}>
2635
- <div data-theme={theme} style={{ display: "contents" }}>
2636
- {children}
2637
- </div>
2638
- </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}
2639
2730
  );
2640
2731
  }
2641
2732
  `;
@@ -3062,7 +3153,7 @@ function generatePackageJson(config, componentNames) {
3062
3153
  componentNames.map((c) => [`./metadata/${c}`, `./metadata/${c}.json`])
3063
3154
  )
3064
3155
  },
3065
- files: ["dist", "tokens", "metadata", "docs", "CHANGELOG.md"],
3156
+ files: ["dist", "tokens", "metadata", "CHANGELOG.md"],
3066
3157
  scripts: {
3067
3158
  build: "tsc",
3068
3159
  prepublishOnly: "npm run build"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nghitrum/dsforge",
3
- "version": "0.1.5-alpha.5",
3
+ "version": "0.1.5-alpha.7",
4
4
  "description": "AI-native design system generator — tokens → components → docs → npm",
5
5
  "keywords": [
6
6
  "design-system",