@nghitrum/dsforge 0.1.5-alpha.1 → 0.1.5-alpha.10

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
@@ -4,9 +4,7 @@
4
4
 
5
5
  Your design tokens live in Figma. Your components drift from the spec. Your docs are six sprints out of date. dsforge fixes all three with one config file.
6
6
 
7
- ![dsforge showcase](docs/showcase.png)
8
-
9
- ---
7
+ <video src="https://github.com/user-attachments/assets/1f4b563e-9217-4636-a265-702ccd93ba25" autoplay loop muted playsinline></video>
10
8
 
11
9
  ## Before / After
12
10
 
@@ -26,7 +24,6 @@ design-system.config.json → dsforge generate
26
24
  dist-ds/
27
25
  ├── src/ 9 typed React components
28
26
  ├── tokens/ CSS custom properties, JS map, Tailwind extension
29
- ├── docs/ MDX documentation per component
30
27
  ├── metadata/ AI-readable JSON contracts per component
31
28
  └── showcase.html visual docs — open directly in the browser, no server
32
29
  ```
@@ -58,18 +55,18 @@ Output lands in `dist-ds/`. Regenerate it any time by running `generate` again.
58
55
  "global": {
59
56
  "brand-600": "#2563eb",
60
57
  "neutral-900": "#0f172a",
61
- "neutral-0": "#ffffff"
58
+ "neutral-0": "#ffffff"
62
59
  },
63
60
  "semantic": {
64
- "color-action": "{global.brand-600}",
61
+ "color-action": "{global.brand-600}",
65
62
  "color-text-primary": "{global.neutral-900}",
66
- "color-bg-default": "{global.neutral-0}"
63
+ "color-bg-default": "{global.neutral-0}"
67
64
  }
68
65
  },
69
66
  "typography": {
70
67
  "fontFamily": "Inter, system-ui, sans-serif",
71
68
  "roles": {
72
- "body": { "size": 16, "weight": 400, "lineHeight": 1.6 },
69
+ "body": { "size": 16, "weight": 400, "lineHeight": 1.6 },
73
70
  "heading": { "size": 24, "weight": 700, "lineHeight": 1.2 }
74
71
  }
75
72
  },
@@ -99,10 +96,6 @@ Each component is typed, themed with your actual tokens, and ships with a prop t
99
96
  `tokens.js` — JS token map for runtime use
100
97
  `tailwind.js` — Tailwind theme extension, ready to drop into `tailwind.config.js`
101
98
 
102
- ### MDX docs
103
-
104
- One `.mdx` file per component, generated from your config. Import them into any docs site.
105
-
106
99
  ### AI metadata contracts
107
100
 
108
101
  Each component emits `dist-ds/metadata/<component>.json`:
@@ -144,17 +137,17 @@ Includes live component previews with all variants and states, themed with your
144
137
 
145
138
  `dsforge validate` runs nine health checks and produces a scored report:
146
139
 
147
- | Check | Max score |
148
- | --- | --- |
149
- | Token architecture | 15 |
150
- | Typography | 10 |
151
- | Spacing | 10 |
152
- | Radius | 5 |
153
- | Elevation | 5 |
154
- | Motion | 5 |
155
- | Themes | 10 |
156
- | Token resolution | 14 |
157
- | Governance rules | 15 |
140
+ | Check | Max score |
141
+ | ------------------ | --------- |
142
+ | Token architecture | 15 |
143
+ | Typography | 10 |
144
+ | Spacing | 10 |
145
+ | Radius | 5 |
146
+ | Elevation | 5 |
147
+ | Motion | 5 |
148
+ | Themes | 10 |
149
+ | Token resolution | 14 |
150
+ | Governance rules | 15 |
158
151
 
159
152
  Scores below 70 are flagged as warnings. WCAG contrast is checked automatically for all color token pairs.
160
153
 
@@ -186,13 +179,13 @@ Run this before deploying a config update to understand downstream impact.
186
179
 
187
180
  ## Commands
188
181
 
189
- | Command | What it does |
190
- | --- | --- |
191
- | `dsforge init` | Scaffold `design-system.config.json` and `design-system.rules.json` |
182
+ | Command | What it does |
183
+ | ------------------ | -------------------------------------------------------------------- |
184
+ | `dsforge init` | Scaffold `design-system.config.json` and `design-system.rules.json` |
192
185
  | `dsforge generate` | Run the full pipeline — tokens, components, metadata, docs, showcase |
193
- | `dsforge validate` | Run 9 health checks and score against governance rules |
194
- | `dsforge diff` | Compare two configs — BREAKING / CHANGED / ADDED |
195
- | `dsforge showcase` | Open `dist-ds/showcase.html` in your default browser |
186
+ | `dsforge validate` | Run 9 health checks and score against governance rules |
187
+ | `dsforge diff` | Compare two configs — BREAKING / CHANGED / ADDED |
188
+ | `dsforge showcase` | Open `dist-ds/showcase.html` in your default browser |
196
189
 
197
190
  Run `dsforge` with no arguments for an interactive menu.
198
191
 
@@ -0,0 +1,436 @@
1
+ // src/adapters/react/componentDefinitions.ts
2
+ var COMPONENT_JSON_DEFINITIONS = {
3
+ Button: {
4
+ name: "Button",
5
+ description: "A clickable element that triggers an action. Supports multiple visual variants and sizes, inheriting all design tokens automatically.",
6
+ props: [
7
+ { name: "variant", type: "'primary' | 'secondary' | 'destructive' | 'ghost'", default: "'primary'", required: false, description: "Visual style variant" },
8
+ { name: "size", type: "'sm' | 'md' | 'lg'", default: "'md'", required: false, description: "Size of the button" },
9
+ { name: "disabled", type: "boolean", default: "false", required: false, description: "Disables interaction and applies muted styling" },
10
+ { name: "onClick", type: "() => void", default: "\u2014", required: false, description: "Click handler" },
11
+ { name: "children", type: "ReactNode", default: "\u2014", required: true, description: "Button label or content" }
12
+ ],
13
+ examples: [
14
+ { label: "Primary", code: '<Button variant="primary">Save changes</Button>' },
15
+ { label: "Secondary", code: '<Button variant="secondary">Cancel</Button>' },
16
+ { label: "Destructive", code: '<Button variant="destructive">Delete account</Button>' },
17
+ { label: "Ghost", code: '<Button variant="ghost">Learn more</Button>' },
18
+ { label: "Disabled", code: '<Button variant="primary" disabled>Processing...</Button>' },
19
+ { label: "Small", code: '<Button variant="primary" size="sm">Confirm</Button>' },
20
+ { label: "Large", code: '<Button variant="primary" size="lg">Get started</Button>' }
21
+ ]
22
+ },
23
+ Input: {
24
+ name: "Input",
25
+ description: "A text input field with support for label, placeholder, error state, and disabled state. Styled consistently with design tokens.",
26
+ props: [
27
+ { name: "label", type: "string", default: "\u2014", required: false, description: "Label displayed above the input" },
28
+ { name: "placeholder", type: "string", default: "\u2014", required: false, description: "Placeholder text" },
29
+ { name: "value", type: "string", default: "\u2014", required: true, description: "Controlled input value" },
30
+ { name: "onChange", type: "(e: React.ChangeEvent<HTMLInputElement>) => void", default: "\u2014", required: true, description: "Change handler" },
31
+ { name: "error", type: "string", default: "\u2014", required: false, description: "Error message displayed below the input" },
32
+ { name: "disabled", type: "boolean", default: "false", required: false, description: "Disables the input" }
33
+ ],
34
+ examples: [
35
+ { label: "Default", code: '<Input label="Email" placeholder="you@example.com" value={email} onChange={e => setEmail(e.target.value)} />' },
36
+ { label: "With error", code: '<Input label="Email" value={email} onChange={e => setEmail(e.target.value)} error="Please enter a valid email address" />' },
37
+ { label: "Disabled", code: '<Input label="Email" value="user@example.com" onChange={() => {}} disabled />' },
38
+ { label: "No label", code: '<Input placeholder="Search..." value={query} onChange={e => setQuery(e.target.value)} />' }
39
+ ]
40
+ },
41
+ Card: {
42
+ name: "Card",
43
+ description: "A surface container for grouping related content. Applies background, border, and shadow tokens consistently.",
44
+ props: [
45
+ { name: "children", type: "ReactNode", default: "\u2014", required: true, description: "Card content" },
46
+ { name: "padding", type: "'sm' | 'md' | 'lg'", default: "'md'", required: false, description: "Inner padding size" },
47
+ { name: "shadow", type: "boolean", default: "true", required: false, description: "Applies elevation shadow" }
48
+ ],
49
+ examples: [
50
+ { label: "Default", code: "<Card>\n <h2>Card title</h2>\n <p>Card content goes here.</p>\n</Card>" },
51
+ { label: "No shadow", code: "<Card shadow={false}>\n <p>Flat card, no elevation.</p>\n</Card>" },
52
+ { label: "Large padding", code: '<Card padding="lg">\n <p>Spacious card layout.</p>\n</Card>' }
53
+ ]
54
+ },
55
+ ThemeProvider: {
56
+ name: "ThemeProvider",
57
+ description: "Wraps the application and injects CSS custom properties from your design tokens. Required at the root of any app using this design system.",
58
+ props: [
59
+ { name: "theme", type: "'light' | 'dark'", default: "'light'", required: false, description: "Active theme" },
60
+ { name: "children", type: "ReactNode", default: "\u2014", required: true, description: "Application content" }
61
+ ],
62
+ examples: [
63
+ { label: "Light theme", code: '<ThemeProvider theme="light">\n <App />\n</ThemeProvider>' },
64
+ { label: "Dark theme", code: '<ThemeProvider theme="dark">\n <App />\n</ThemeProvider>' }
65
+ ]
66
+ },
67
+ Badge: {
68
+ name: "Badge",
69
+ description: "A small label used to highlight status, category, or count. Supports semantic colour variants mapped to your token palette.",
70
+ props: [
71
+ { name: "variant", type: "'default' | 'success' | 'warning' | 'error' | 'info'", default: "'default'", required: false, description: "Semantic colour variant" },
72
+ { name: "children", type: "ReactNode", default: "\u2014", required: true, description: "Badge label" }
73
+ ],
74
+ examples: [
75
+ { label: "Default", code: "<Badge>New</Badge>" },
76
+ { label: "Success", code: '<Badge variant="success">Active</Badge>' },
77
+ { label: "Warning", code: '<Badge variant="warning">Pending</Badge>' },
78
+ { label: "Error", code: '<Badge variant="error">Failed</Badge>' },
79
+ { label: "Info", code: '<Badge variant="info">In review</Badge>' }
80
+ ]
81
+ },
82
+ Checkbox: {
83
+ name: "Checkbox",
84
+ description: "A boolean input for toggling an option on or off. Supports label, checked state, indeterminate state, and disabled.",
85
+ props: [
86
+ { name: "label", type: "string", default: "\u2014", required: false, description: "Label displayed beside the checkbox" },
87
+ { name: "checked", type: "boolean", default: "false", required: true, description: "Controlled checked state" },
88
+ { name: "onChange", type: "(e: React.ChangeEvent<HTMLInputElement>) => void", default: "\u2014", required: true, description: "Change handler" },
89
+ { name: "indeterminate", type: "boolean", default: "false", required: false, description: "Indeterminate visual state (used in select-all patterns)" },
90
+ { name: "disabled", type: "boolean", default: "false", required: false, description: "Disables interaction" }
91
+ ],
92
+ examples: [
93
+ { label: "Default", code: '<Checkbox label="Accept terms" checked={accepted} onChange={e => setAccepted(e.target.checked)} />' },
94
+ { label: "Checked", code: '<Checkbox label="Notifications enabled" checked={true} onChange={() => {}} />' },
95
+ { label: "Indeterminate", code: '<Checkbox label="Select all" checked={false} indeterminate onChange={() => {}} />' },
96
+ { label: "Disabled", code: '<Checkbox label="Unavailable option" checked={false} disabled onChange={() => {}} />' }
97
+ ]
98
+ },
99
+ Radio: {
100
+ name: "Radio",
101
+ description: "A single-select input within a group of options. Use multiple Radio components sharing a name to form a group.",
102
+ props: [
103
+ { name: "label", type: "string", default: "\u2014", required: false, description: "Label displayed beside the radio button" },
104
+ { name: "value", type: "string", default: "\u2014", required: true, description: "Value for this option" },
105
+ { name: "checked", type: "boolean", default: "false", required: true, description: "Whether this option is selected" },
106
+ { name: "onChange", type: "(e: React.ChangeEvent<HTMLInputElement>) => void", default: "\u2014", required: true, description: "Change handler" },
107
+ { name: "name", type: "string", default: "\u2014", required: true, description: "Group name \u2014 shared across all options in a group" },
108
+ { name: "disabled", type: "boolean", default: "false", required: false, description: "Disables this option" }
109
+ ],
110
+ examples: [
111
+ {
112
+ label: "Radio group",
113
+ code: `<Radio label="Option A" name="choice" value="a" checked={choice === 'a'} onChange={() => setChoice('a')} />
114
+ <Radio label="Option B" name="choice" value="b" checked={choice === 'b'} onChange={() => setChoice('b')} />
115
+ <Radio label="Option C" name="choice" value="c" checked={choice === 'c'} onChange={() => setChoice('c')} />`
116
+ },
117
+ { label: "Disabled option", code: '<Radio label="Unavailable" name="choice" value="x" checked={false} disabled onChange={() => {}} />' }
118
+ ]
119
+ },
120
+ Select: {
121
+ name: "Select",
122
+ description: "A dropdown input for choosing one option from a list. Accepts an options array and handles label, placeholder, and error state.",
123
+ props: [
124
+ { name: "label", type: "string", default: "\u2014", required: false, description: "Label displayed above the select" },
125
+ { name: "options", type: "Array<{ label: string; value: string }>", default: "\u2014", required: true, description: "List of options" },
126
+ { name: "value", type: "string", default: "\u2014", required: true, description: "Controlled selected value" },
127
+ { name: "onChange", type: "(e: React.ChangeEvent<HTMLSelectElement>) => void", default: "\u2014", required: true, description: "Change handler" },
128
+ { name: "placeholder", type: "string", default: "\u2014", required: false, description: "Placeholder option shown when no value is selected" },
129
+ { name: "error", type: "string", default: "\u2014", required: false, description: "Error message displayed below the select" },
130
+ { name: "disabled", type: "boolean", default: "false", required: false, description: "Disables the select" }
131
+ ],
132
+ examples: [
133
+ {
134
+ label: "Default",
135
+ code: `<Select
136
+ label="Country"
137
+ options={[{ label: 'Norway', value: 'no' }, { label: 'Sweden', value: 'se' }]}
138
+ value={country}
139
+ onChange={e => setCountry(e.target.value)}
140
+ />`
141
+ },
142
+ {
143
+ label: "With placeholder",
144
+ code: `<Select
145
+ label="Country"
146
+ placeholder="Select a country"
147
+ options={[{ label: 'Norway', value: 'no' }]}
148
+ value={country}
149
+ onChange={e => setCountry(e.target.value)}
150
+ />`
151
+ },
152
+ {
153
+ label: "With error",
154
+ code: `<Select
155
+ label="Country"
156
+ options={[{ label: 'Norway', value: 'no' }]}
157
+ value={country}
158
+ onChange={e => setCountry(e.target.value)}
159
+ error="Please select a country"
160
+ />`
161
+ }
162
+ ]
163
+ },
164
+ Toast: {
165
+ name: "Toast",
166
+ description: "A brief, auto-dismissing notification. Use for confirmations, errors, and non-blocking alerts.",
167
+ props: [
168
+ { name: "message", type: "string", default: "\u2014", required: true, description: "Notification message" },
169
+ { name: "variant", type: "'info' | 'success' | 'warning' | 'error'", default: "'info'", required: false, description: "Semantic variant" },
170
+ { name: "duration", type: "number", default: "3000", required: false, description: "Auto-dismiss duration in milliseconds" },
171
+ { name: "onDismiss", type: "() => void", default: "\u2014", required: false, description: "Called when the toast is dismissed" }
172
+ ],
173
+ examples: [
174
+ { label: "Success", code: '<Toast message="Changes saved successfully" variant="success" onDismiss={() => setToast(null)} />' },
175
+ { label: "Error", code: '<Toast message="Something went wrong. Please try again." variant="error" onDismiss={() => setToast(null)} />' },
176
+ { label: "Warning", code: '<Toast message="Your session will expire in 5 minutes." variant="warning" onDismiss={() => setToast(null)} />' },
177
+ { label: "Custom duration", code: '<Toast message="Copied to clipboard" variant="success" duration={1500} onDismiss={() => setToast(null)} />' }
178
+ ]
179
+ },
180
+ Spinner: {
181
+ name: "Spinner",
182
+ description: "An animated loading indicator. Use when an async operation is in progress and the duration is unknown.",
183
+ props: [
184
+ { name: "size", type: "'sm' | 'md' | 'lg'", default: "'md'", required: false, description: "Size of the spinner" },
185
+ { name: "label", type: "string", default: "'Loading\u2026'", required: false, description: "Accessible label used as aria-label" }
186
+ ],
187
+ examples: [
188
+ { label: "Default", code: "<Spinner />" },
189
+ { label: "Small", code: '<Spinner size="sm" />' },
190
+ { label: "Large", code: '<Spinner size="lg" />' },
191
+ { label: "Custom label", code: '<Spinner size="md" label="Saving your changes" />' }
192
+ ]
193
+ }
194
+ };
195
+ var COMPONENT_METADATA_DEFINITIONS = {
196
+ Button: {
197
+ name: "Button",
198
+ role: "action-trigger",
199
+ hierarchyLevel: "primary",
200
+ destructiveVariants: ["destructive"],
201
+ variants: ["primary", "secondary", "destructive", "ghost"],
202
+ accessibilityContract: {
203
+ keyboard: true,
204
+ focusRing: "required",
205
+ ariaLabel: "required-for-icon-only",
206
+ roles: ["button"],
207
+ notes: [
208
+ "Must have visible focus ring \u2014 never remove outline without replacement",
209
+ "Icon-only buttons must have an aria-label",
210
+ "Disabled buttons should still be focusable for screen reader awareness"
211
+ ]
212
+ },
213
+ aiGuidance: [
214
+ "Use primary for the single most important action on a page or in a section",
215
+ "Never place two primary buttons side by side \u2014 only one primary action per context",
216
+ "Use destructive only for irreversible actions \u2014 always pair with a confirmation dialog",
217
+ "Use ghost for tertiary actions that should not compete visually with primary and secondary",
218
+ "Never use a button for navigation \u2014 use a link instead"
219
+ ]
220
+ },
221
+ Input: {
222
+ name: "Input",
223
+ role: "text-input",
224
+ hierarchyLevel: "utility",
225
+ destructiveVariants: [],
226
+ variants: ["default", "error", "disabled"],
227
+ accessibilityContract: {
228
+ keyboard: true,
229
+ focusRing: "required",
230
+ ariaLabel: "optional",
231
+ roles: ["textbox"],
232
+ notes: [
233
+ "Always associate label with input \u2014 never use placeholder as a replacement for label",
234
+ "Error messages must be connected via aria-describedby",
235
+ "Disabled inputs should use the disabled attribute, not just visual styling"
236
+ ]
237
+ },
238
+ aiGuidance: [
239
+ "Always provide a label \u2014 placeholder text alone is not accessible",
240
+ "Error prop should describe what went wrong, not just that something went wrong",
241
+ "Use controlled inputs \u2014 always provide value and onChange together",
242
+ "Do not use Input for multiline text \u2014 use a Textarea component instead"
243
+ ]
244
+ },
245
+ Card: {
246
+ name: "Card",
247
+ role: "surface-container",
248
+ hierarchyLevel: "utility",
249
+ destructiveVariants: [],
250
+ variants: ["default"],
251
+ accessibilityContract: {
252
+ keyboard: false,
253
+ focusRing: "none",
254
+ ariaLabel: "optional",
255
+ roles: ["region"],
256
+ notes: [
257
+ "If a card is interactive (clickable), wrap in a button or anchor \u2014 never use onClick on a div",
258
+ "Add aria-label or aria-labelledby if the card represents a distinct region of the page"
259
+ ]
260
+ },
261
+ aiGuidance: [
262
+ "Card is a layout container \u2014 do not put interaction on the Card itself",
263
+ 'Use padding="lg" for content-heavy cards, padding="sm" for compact UI like sidebars',
264
+ "Disable shadow when cards are on a surface that already has elevation"
265
+ ]
266
+ },
267
+ ThemeProvider: {
268
+ name: "ThemeProvider",
269
+ role: "theme-context",
270
+ hierarchyLevel: "utility",
271
+ destructiveVariants: [],
272
+ variants: ["light", "dark"],
273
+ accessibilityContract: {
274
+ keyboard: false,
275
+ focusRing: "none",
276
+ ariaLabel: "none",
277
+ roles: [],
278
+ notes: [
279
+ "Ensure colour contrast meets WCAG AA in both light and dark themes",
280
+ "Do not rely on colour alone to convey meaning"
281
+ ]
282
+ },
283
+ aiGuidance: [
284
+ "ThemeProvider must wrap the entire application at the root \u2014 not individual components",
285
+ "Never nest ThemeProviders \u2014 use one at the root",
286
+ "Theme value should come from user preference (prefers-color-scheme) or an explicit user toggle"
287
+ ]
288
+ },
289
+ Badge: {
290
+ name: "Badge",
291
+ role: "status-indicator",
292
+ hierarchyLevel: "tertiary",
293
+ destructiveVariants: ["error"],
294
+ variants: ["default", "success", "warning", "error", "info"],
295
+ accessibilityContract: {
296
+ keyboard: false,
297
+ focusRing: "none",
298
+ ariaLabel: "optional",
299
+ roles: ["status"],
300
+ notes: [
301
+ "Do not rely on colour alone \u2014 badge text must convey the meaning",
302
+ "For dynamic status changes, wrap in an aria-live region"
303
+ ]
304
+ },
305
+ aiGuidance: [
306
+ "Use Badge for status, categories, or counts \u2014 not for actions",
307
+ "Badge text should be short \u2014 one or two words maximum",
308
+ "Use error variant sparingly \u2014 only for genuine failure states",
309
+ "Do not use Badge as a button or interactive element"
310
+ ]
311
+ },
312
+ Checkbox: {
313
+ name: "Checkbox",
314
+ role: "boolean-input",
315
+ hierarchyLevel: "utility",
316
+ destructiveVariants: [],
317
+ variants: ["default", "checked", "indeterminate", "disabled"],
318
+ accessibilityContract: {
319
+ keyboard: true,
320
+ focusRing: "required",
321
+ ariaLabel: "optional",
322
+ roles: ["checkbox"],
323
+ notes: [
324
+ "Always associate a label \u2014 either via label prop or aria-label",
325
+ "Indeterminate state must be set via the indeterminate DOM property, not just visually",
326
+ "Group related checkboxes in a fieldset with a legend"
327
+ ]
328
+ },
329
+ aiGuidance: [
330
+ "Use Checkbox for independent boolean options \u2014 not for mutually exclusive choices (use Radio for that)",
331
+ "Indeterminate state is for parent checkboxes in a select-all pattern only",
332
+ "Always use controlled state \u2014 provide checked and onChange together"
333
+ ]
334
+ },
335
+ Radio: {
336
+ name: "Radio",
337
+ role: "single-select-input",
338
+ hierarchyLevel: "utility",
339
+ destructiveVariants: [],
340
+ variants: ["default", "checked", "disabled"],
341
+ accessibilityContract: {
342
+ keyboard: true,
343
+ focusRing: "required",
344
+ ariaLabel: "optional",
345
+ roles: ["radio"],
346
+ notes: [
347
+ "All Radio components in a group must share the same name prop",
348
+ "Group in a fieldset with a legend describing the group question",
349
+ "Arrow keys should navigate between options within the group"
350
+ ]
351
+ },
352
+ aiGuidance: [
353
+ "Use Radio for mutually exclusive choices \u2014 not for independent toggles (use Checkbox for that)",
354
+ "Always render the full group \u2014 never a single Radio in isolation",
355
+ "All options in a group must share the same name prop",
356
+ "Pre-select the most common or safest option \u2014 never leave a radio group with no selection"
357
+ ]
358
+ },
359
+ Select: {
360
+ name: "Select",
361
+ role: "dropdown-input",
362
+ hierarchyLevel: "utility",
363
+ destructiveVariants: [],
364
+ variants: ["default", "error", "disabled"],
365
+ accessibilityContract: {
366
+ keyboard: true,
367
+ focusRing: "required",
368
+ ariaLabel: "optional",
369
+ roles: ["combobox", "listbox"],
370
+ notes: [
371
+ "Always provide a label \u2014 never rely on placeholder alone",
372
+ "Error messages must be connected via aria-describedby",
373
+ "Use a placeholder option with an empty value to represent the unselected state"
374
+ ]
375
+ },
376
+ aiGuidance: [
377
+ "Use Select when there are 5 or more options \u2014 use Radio for fewer options",
378
+ "Always provide a label separate from placeholder",
379
+ 'Placeholder should say what the field is for, not just "Select..."',
380
+ "Always use controlled state \u2014 provide value and onChange together"
381
+ ]
382
+ },
383
+ Toast: {
384
+ name: "Toast",
385
+ role: "feedback",
386
+ hierarchyLevel: "utility",
387
+ destructiveVariants: [],
388
+ variants: ["info", "success", "warning", "error"],
389
+ accessibilityContract: {
390
+ keyboard: false,
391
+ focusRing: "none",
392
+ ariaLabel: "none",
393
+ roles: ["alert", "status"],
394
+ notes: [
395
+ 'Toast container must be an aria-live region \u2014 role="status" for non-urgent, role="alert" for errors',
396
+ "Do not auto-dismiss error toasts \u2014 errors require user acknowledgement",
397
+ "Ensure toast is readable before it dismisses \u2014 minimum 3000ms for average message length"
398
+ ]
399
+ },
400
+ aiGuidance: [
401
+ "Use Toast for non-blocking feedback only \u2014 never for critical errors that block the user",
402
+ "Error toasts should not auto-dismiss \u2014 set duration={0} or a very long duration",
403
+ "Never stack more than 3 toasts \u2014 dismiss older ones when new ones appear",
404
+ "Toast message should describe what happened, not just that it happened"
405
+ ]
406
+ },
407
+ Spinner: {
408
+ name: "Spinner",
409
+ role: "loading-indicator",
410
+ hierarchyLevel: "utility",
411
+ destructiveVariants: [],
412
+ variants: ["sm", "md", "lg"],
413
+ accessibilityContract: {
414
+ keyboard: false,
415
+ focusRing: "none",
416
+ ariaLabel: "required",
417
+ roles: ["status"],
418
+ notes: [
419
+ "Must always have an aria-label describing what is loading",
420
+ "Wrap in an aria-live region so screen readers announce the loading state",
421
+ "Remove from DOM when loading is complete \u2014 do not just hide visually"
422
+ ]
423
+ },
424
+ aiGuidance: [
425
+ 'Always provide a descriptive label \u2014 never use the default "Loading\u2026" in production',
426
+ 'Use size="sm" inline with content, size="lg" for full-page loading states',
427
+ "Remove the spinner from the DOM when loading completes \u2014 do not hide with CSS",
428
+ "Pair with a disabled state on the triggering button so users cannot re-submit"
429
+ ]
430
+ }
431
+ };
432
+
433
+ export {
434
+ COMPONENT_JSON_DEFINITIONS,
435
+ COMPONENT_METADATA_DEFINITIONS
436
+ };
@@ -0,0 +1,118 @@
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 CONTROL_SIZE_PRESETS = {
70
+ compact: { sm: 12, md: 14, lg: 18 },
71
+ comfortable: { sm: 14, md: 16, lg: 20 },
72
+ spacious: { sm: 16, md: 18, lg: 24 }
73
+ };
74
+ var PRESET_BASE_UNITS = {
75
+ compact: 2,
76
+ comfortable: 4,
77
+ spacious: 6
78
+ };
79
+ function buildSemanticSpacing(scale) {
80
+ return {
81
+ "component-padding-xs": `${scale["1"]}`,
82
+ "component-padding-sm": `${scale["2"]}`,
83
+ "component-padding-md": `${scale["4"]}`,
84
+ "component-padding-lg": `${scale["5"]}`,
85
+ "layout-gap-xs": `${scale["2"]}`,
86
+ "layout-gap-sm": `${scale["3"]}`,
87
+ "layout-gap-md": `${scale["5"]}`,
88
+ "layout-gap-lg": `${scale["6"]}`,
89
+ "layout-section": `${scale["7"]}`
90
+ };
91
+ }
92
+ function applyPreset(config, preset) {
93
+ const scale = SPACING_PRESETS[preset];
94
+ const radius = RADIUS_PRESETS[preset];
95
+ const baseUnit = PRESET_BASE_UNITS[preset];
96
+ config.spacing = {
97
+ ...config.spacing,
98
+ baseUnit,
99
+ scale,
100
+ semantic: buildSemanticSpacing(scale)
101
+ };
102
+ config.radius = { ...config.radius, ...radius };
103
+ config.philosophy = {
104
+ ...config.philosophy,
105
+ density: preset
106
+ };
107
+ }
108
+
109
+ export {
110
+ isProUnlocked,
111
+ PRESETS,
112
+ SPACING_PRESETS,
113
+ RADIUS_PRESETS,
114
+ CONTROL_SIZE_PRESETS,
115
+ PRESET_BASE_UNITS,
116
+ buildSemanticSpacing,
117
+ applyPreset
118
+ };