@nghitrum/dsforge 0.1.5-alpha.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +0 -1
- package/dist/{chunk-JUMR3N5J.js → chunk-5YT3VNE6.js} +6 -26
- package/dist/chunk-A7VW6SII.js +436 -0
- package/dist/{chunk-QHE35QQQ.js → chunk-ZZRPNO6Z.js} +12 -17
- package/dist/cli/index.js +218 -310
- package/dist/componentDefinitions-5LFCNFQY.js +8 -0
- package/dist/{emitter-KNYIQTS5.js → emitter-IC77G4QF.js} +1 -1
- package/dist/generateAiFolder-3OOFWBH7.js +70 -0
- package/dist/generateComponentJson-XBEUWCW6.js +16 -0
- package/dist/generateComponentMetadata-2L5VNERD.js +13 -0
- package/dist/generateRegistry-3MEZDJAJ.js +19 -0
- package/dist/{html-LQHDCSG4.js → html-4DD6GOHE.js} +223 -130
- package/dist/index.js +86 -110
- package/package.json +1 -1
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nghi Nguyen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -194,7 +194,6 @@ Run `dsforge` with no arguments for an interactive menu.
|
|
|
194
194
|
## Coming next
|
|
195
195
|
|
|
196
196
|
- Additional components: Modal, Table, Tooltip, DatePicker
|
|
197
|
-
- **Pro**: AI-assisted token generation from brand guidelines or a Figma file
|
|
198
197
|
- Figma Variables API sync
|
|
199
198
|
- CI integration — fail builds on governance violations
|
|
200
199
|
|
|
@@ -1,28 +1,3 @@
|
|
|
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
1
|
// src/presets/index.ts
|
|
27
2
|
var PRESETS = [
|
|
28
3
|
"compact",
|
|
@@ -66,6 +41,11 @@ var RADIUS_PRESETS = {
|
|
|
66
41
|
comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
|
|
67
42
|
spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
|
|
68
43
|
};
|
|
44
|
+
var CONTROL_SIZE_PRESETS = {
|
|
45
|
+
compact: { sm: 12, md: 14, lg: 18 },
|
|
46
|
+
comfortable: { sm: 14, md: 16, lg: 20 },
|
|
47
|
+
spacious: { sm: 16, md: 18, lg: 24 }
|
|
48
|
+
};
|
|
69
49
|
var PRESET_BASE_UNITS = {
|
|
70
50
|
compact: 2,
|
|
71
51
|
comfortable: 4,
|
|
@@ -102,10 +82,10 @@ function applyPreset(config, preset) {
|
|
|
102
82
|
}
|
|
103
83
|
|
|
104
84
|
export {
|
|
105
|
-
isProUnlocked,
|
|
106
85
|
PRESETS,
|
|
107
86
|
SPACING_PRESETS,
|
|
108
87
|
RADIUS_PRESETS,
|
|
88
|
+
CONTROL_SIZE_PRESETS,
|
|
109
89
|
PRESET_BASE_UNITS,
|
|
110
90
|
buildSemanticSpacing,
|
|
111
91
|
applyPreset
|
|
@@ -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
|
+
};
|
|
@@ -22,12 +22,14 @@ function generatePackageJson(config, componentNames) {
|
|
|
22
22
|
),
|
|
23
23
|
"./tokens": "./tokens/tokens.js",
|
|
24
24
|
"./tailwind": "./tokens/tailwind.js",
|
|
25
|
-
"./metadata": "./metadata/index.json",
|
|
26
25
|
...Object.fromEntries(
|
|
27
|
-
componentNames.map((c) =>
|
|
26
|
+
componentNames.map((c) => {
|
|
27
|
+
const pascal = c.charAt(0).toUpperCase() + c.slice(1);
|
|
28
|
+
return [`./components/${pascal}`, `./components/${pascal}/${pascal}.json`];
|
|
29
|
+
})
|
|
28
30
|
)
|
|
29
31
|
},
|
|
30
|
-
files: ["dist", "tokens", "
|
|
32
|
+
files: ["dist", "tokens", "components", "CHANGELOG.md"],
|
|
31
33
|
scripts: {
|
|
32
34
|
build: "tsc",
|
|
33
35
|
prepublishOnly: "npm run build"
|
|
@@ -67,7 +69,7 @@ function generateTsConfig() {
|
|
|
67
69
|
moduleResolution: "NodeNext",
|
|
68
70
|
lib: ["ES2020", "DOM"],
|
|
69
71
|
outDir: "./dist",
|
|
70
|
-
rootDir: "
|
|
72
|
+
rootDir: ".",
|
|
71
73
|
declaration: true,
|
|
72
74
|
declarationMap: true,
|
|
73
75
|
sourceMap: true,
|
|
@@ -76,7 +78,7 @@ function generateTsConfig() {
|
|
|
76
78
|
skipLibCheck: true,
|
|
77
79
|
jsx: "react-jsx"
|
|
78
80
|
},
|
|
79
|
-
include: ["
|
|
81
|
+
include: ["index.ts", "components/**/*"],
|
|
80
82
|
exclude: ["node_modules", "dist"]
|
|
81
83
|
},
|
|
82
84
|
null,
|
|
@@ -125,7 +127,10 @@ function App() {
|
|
|
125
127
|
## Components
|
|
126
128
|
|
|
127
129
|
${componentNames.map(
|
|
128
|
-
(c) =>
|
|
130
|
+
(c) => {
|
|
131
|
+
const pascal = c.charAt(0).toUpperCase() + c.slice(1);
|
|
132
|
+
return `- **${pascal}** \u2014 see \`components/${pascal}/${pascal}.json\` for props and usage`;
|
|
133
|
+
}
|
|
129
134
|
).join("\n")}
|
|
130
135
|
|
|
131
136
|
## Themes
|
|
@@ -200,20 +205,10 @@ every semantic and component token that references it.
|
|
|
200
205
|
|
|
201
206
|
## AI tool integration
|
|
202
207
|
|
|
203
|
-
|
|
208
|
+
Each component ships with a machine-readable JSON contract (e.g. \`components/Button/Button.json\`).
|
|
204
209
|
AI coding assistants (Copilot, Cursor, Claude Code) can read these to
|
|
205
210
|
generate UI that respects your governance rules automatically.
|
|
206
211
|
|
|
207
|
-
\`\`\`json
|
|
208
|
-
// ${pkgName}/metadata/button.json
|
|
209
|
-
{
|
|
210
|
-
"component": "Button",
|
|
211
|
-
"allowedVariants": ["primary", "secondary", "danger", "ghost"],
|
|
212
|
-
"requiredProps": ["aria-label"],
|
|
213
|
-
"accessibilityContract": { "keyboard": true, "focusRing": true }
|
|
214
|
-
}
|
|
215
|
-
\`\`\`
|
|
216
|
-
|
|
217
212
|
---
|
|
218
213
|
|
|
219
214
|
Generated by [dsforge](https://github.com/nghitrum/dsforge) v${config.meta.version}.
|