@chemx/starter-kit 26.9.20-228 → 26.9.20-390

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/blueprints/_package.json +17 -0
  2. package/blueprints/_tsconfig.json +15 -0
  3. package/blueprints/_vitest.config.ts +9 -0
  4. package/blueprints/workflows/chemx-audit.yml +4 -5
  5. package/cli/generator-templates/archetypes/collection-archetypes.js +275 -0
  6. package/cli/generator-templates/archetypes/domain-archetypes.js +219 -0
  7. package/cli/generator-templates/archetypes/index.js +35 -0
  8. package/cli/generator-templates/archetypes/input-archetypes.js +193 -0
  9. package/cli/generator-templates/archetypes/layout-archetypes.js +219 -0
  10. package/cli/generator-templates/archetypes/status-archetypes.js +241 -0
  11. package/cli/generator-templates/controller.js +10 -1
  12. package/cli/generator-templates/react.js +37 -4
  13. package/cli/generator-templates/types.js +16 -2
  14. package/cli/generator-templates.d.ts +14 -0
  15. package/cli/generator-templates.js +1 -0
  16. package/cli/generator-templates.spec.js +64 -1
  17. package/cli/generator.d.ts +3 -2
  18. package/cli/generator.js +59 -0
  19. package/cli/generator.spec.js +17 -0
  20. package/cli/help.js +18 -0
  21. package/cli/installer-templates.js +4 -5
  22. package/cli/mcp/installer.spec.js +4 -6
  23. package/cli/patcher.js +42 -0
  24. package/cli/patcher.spec.js +19 -0
  25. package/cli/reader.js +25 -0
  26. package/cli/scaffold.js +27 -7
  27. package/cli/team/team-multi-model-telemetry.spec.js +80 -0
  28. package/cli/team/team-schema.js +6 -0
  29. package/cli/team/team-telemetry.js +61 -38
  30. package/cli/theme.js +1 -0
  31. package/cli/ui-actions-codebase.js +75 -0
  32. package/cli/ui-actions-studio.js +91 -0
  33. package/cli/ui-actions-tasks.js +87 -0
  34. package/cli/ui-actions.js +11 -27
  35. package/cli/ui-canonical-routes.spec.js +130 -0
  36. package/cli/ui-client-api.js +58 -20
  37. package/cli/ui-client-filetree.js +93 -0
  38. package/cli/ui-client-forum.js +85 -0
  39. package/cli/ui-client-script.js +52 -37
  40. package/cli/ui-client-studio.js +95 -0
  41. package/cli/ui-e2e-verification.spec.js +186 -0
  42. package/cli/ui-filetree.spec.js +175 -0
  43. package/cli/ui-forum-data.js +97 -0
  44. package/cli/ui-forum-topics.js +65 -0
  45. package/cli/ui-handlers.js +61 -65
  46. package/cli/ui-html.js +6 -0
  47. package/cli/ui-kanban.spec.js +206 -0
  48. package/cli/ui-server-routes.js +56 -28
  49. package/cli/ui-server.js +12 -6
  50. package/cli/ui-styles.js +80 -27
  51. package/cli/ui-template-agents.js +64 -0
  52. package/cli/ui-template-dbstudio.js +82 -0
  53. package/cli/ui-template-filetree.js +92 -0
  54. package/cli/ui-template-forum.js +86 -0
  55. package/cli/ui-template-kanban.js +83 -0
  56. package/cli/ui-template-thread.js +51 -0
  57. package/cli/ui-template-timeline.js +35 -0
  58. package/cli/ui-template-workbench.js +55 -0
  59. package/cli/ui-template.js +14 -34
  60. package/cli/ui-vbulletin.spec.js +189 -0
  61. package/cli/ui-workbench-dbstudio.spec.js +172 -0
  62. package/cli/verify.js +107 -16
  63. package/package.json +1 -1
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Input & Capture Archetypes: Form Wizard, Dropdown Select, Toggle Switch, File Upload
3
+ */
4
+
5
+ export const INPUT_ARCHETYPES = [
6
+ {
7
+ id: 'form-wizard',
8
+ name: 'Form & Wizard',
9
+ keywords: ['form', 'editor', 'wizard', 'stepper', 'signup', 'checkout', 'compose'],
10
+ destructure: 'values, errors, isSubmitting, step, setFieldValue, setStep, submit',
11
+ buildState: (name, pascal) => `export interface ${pascal}Values {
12
+ readonly [key: string]: unknown;
13
+ }
14
+
15
+ export interface ${pascal}State {
16
+ readonly values: ${pascal}Values;
17
+ readonly errors: Record<string, string>;
18
+ readonly isSubmitting: boolean;
19
+ readonly step: number;
20
+ }
21
+ `,
22
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
23
+ readonly initialValues?: Record<string, unknown>;
24
+ readonly onSubmit?: (values: Record<string, unknown>) => void;
25
+ }
26
+ `,
27
+ buildController: (name, pascal) => `import { useState } from 'react';
28
+
29
+ export const use${pascal}Controller = (options: { initialValues?: Record<string, unknown>; onSubmit?: (values: Record<string, unknown>) => void } = {}) => {
30
+ const [values, setValues] = useState<Record<string, unknown>>(options.initialValues || {});
31
+ const [errors, setErrors] = useState<Record<string, string>>({});
32
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
33
+ const [step, setStep] = useState<number>(1);
34
+
35
+ const setFieldValue = (field: string, val: unknown) => {
36
+ setValues((prev) => ({ ...prev, [field]: val }));
37
+ if (errors[field]) {
38
+ setErrors((prev) => {
39
+ const next = { ...prev };
40
+ delete next[field];
41
+ return next;
42
+ });
43
+ }
44
+ };
45
+
46
+ const submit = async () => {
47
+ setIsSubmitting(true);
48
+ try {
49
+ options.onSubmit?.(values);
50
+ } finally {
51
+ setIsSubmitting(false);
52
+ }
53
+ };
54
+
55
+ return { values, errors, isSubmitting, step, setFieldValue, setStep, submit };
56
+ };
57
+ `,
58
+ buildReactBody: (name, pascal) => ` <form className="${name}__form" onSubmit={(e) => { e.preventDefault(); submit(); }}>
59
+ <div className="${name}__field">
60
+ <label>Input</label>
61
+ <input type="text" onChange={(e) => setFieldValue('input', e.target.value)} />
62
+ </div>
63
+ <button type="submit" disabled={isSubmitting}>{isSubmitting ? 'Submitting...' : 'Submit'}</button>
64
+ </form>`
65
+ },
66
+ {
67
+ id: 'dropdown-select',
68
+ name: 'Dropdown & Select',
69
+ keywords: ['dropdown', 'select', 'combobox', 'picker', 'popover'],
70
+ destructure: 'options = [], isOpen, selectedId, toggle, select',
71
+ buildState: (name, pascal) => `export interface ${pascal}Option {
72
+ readonly id: string;
73
+ readonly label: string;
74
+ }
75
+
76
+ export interface ${pascal}State {
77
+ readonly isOpen: boolean;
78
+ readonly selectedId: string | null;
79
+ }
80
+ `,
81
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
82
+ readonly options?: readonly { id: string; label: string }[];
83
+ readonly value?: string;
84
+ readonly onSelect?: (id: string) => void;
85
+ }
86
+ `,
87
+ buildController: (name, pascal) => `import { useState } from 'react';
88
+
89
+ export const use${pascal}Controller = (options: { onSelect?: (id: string) => void } = {}) => {
90
+ const [isOpen, setIsOpen] = useState<boolean>(false);
91
+ const [selectedId, setSelectedId] = useState<string | null>(null);
92
+
93
+ const toggle = () => setIsOpen((prev) => !prev);
94
+ const select = (id: string) => {
95
+ setSelectedId(id);
96
+ setIsOpen(false);
97
+ options.onSelect?.(id);
98
+ };
99
+
100
+ return { isOpen, selectedId, toggle, select };
101
+ };
102
+ `,
103
+ buildReactBody: (name, pascal) => ` <div className="${name}__select" onClick={toggle}>
104
+ <span>{selectedId || 'Select option...'}</span>
105
+ </div>
106
+ {isOpen && (
107
+ <ul className="${name}__menu">
108
+ {(options || []).map((opt) => (
109
+ <li key={opt.id} onClick={() => select(opt.id)}>{opt.label}</li>
110
+ ))}
111
+ </ul>
112
+ )}`
113
+ },
114
+ {
115
+ id: 'toggle-switch',
116
+ name: 'Toggle & Controls',
117
+ keywords: ['toggle', 'switch', 'segmented', 'checkbox', 'radio'],
118
+ destructure: 'checked, toggle',
119
+ buildState: (name, pascal) => `export interface ${pascal}State {
120
+ readonly checked: boolean;
121
+ }
122
+ `,
123
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
124
+ readonly defaultChecked?: boolean;
125
+ readonly onChange?: (checked: boolean) => void;
126
+ }
127
+ `,
128
+ buildController: (name, pascal) => `import { useState } from 'react';
129
+
130
+ export const use${pascal}Controller = (options: { defaultChecked?: boolean; onChange?: (val: boolean) => void } = {}) => {
131
+ const [checked, setChecked] = useState<boolean>(Boolean(options.defaultChecked));
132
+
133
+ const toggle = () => {
134
+ setChecked((prev) => {
135
+ const next = !prev;
136
+ options.onChange?.(next);
137
+ return next;
138
+ });
139
+ };
140
+
141
+ return { checked, toggle };
142
+ };
143
+ `,
144
+ buildReactBody: (name, pascal) => ` <button type="button" role="switch" aria-checked={checked} className={checked ? '${name}__switch ${name}__switch--on' : '${name}__switch'} onClick={toggle}>
145
+ <span className="${name}__thumb" />
146
+ </button>`
147
+ },
148
+ {
149
+ id: 'file-upload',
150
+ name: 'Upload & Dropzone',
151
+ keywords: ['upload', 'dropzone', 'file', 'attachment', 'asset', 'uploader'],
152
+ destructure: 'files, isUploading, progress, removeFile',
153
+ buildState: (name, pascal) => `export interface ${pascal}File {
154
+ readonly name: string;
155
+ readonly size: number;
156
+ }
157
+
158
+ export interface ${pascal}State {
159
+ readonly files: readonly ${pascal}File[];
160
+ readonly isUploading: boolean;
161
+ readonly progress: number;
162
+ }
163
+ `,
164
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
165
+ readonly maxFiles?: number;
166
+ readonly onUpload?: (files: File[]) => void;
167
+ }
168
+ `,
169
+ buildController: (name, pascal) => `import { useState } from 'react';
170
+ import type { ${pascal}File } from './types';
171
+
172
+ export const use${pascal}Controller = () => {
173
+ const [files, setFiles] = useState<readonly ${pascal}File[]>([]);
174
+ const [isUploading, setIsUploading] = useState<boolean>(false);
175
+ const [progress, setProgress] = useState<number>(0);
176
+
177
+ const removeFile = (name: string) => {
178
+ setFiles((prev) => prev.filter((f) => f.name !== name));
179
+ };
180
+
181
+ return { files, isUploading, progress, removeFile, setFiles, setIsUploading };
182
+ };
183
+ `,
184
+ buildReactBody: (name, pascal) => ` <div className="${name}__dropzone">
185
+ <span>Drag & drop files here</span>
186
+ </div>
187
+ <ul className="${name}__file-list">
188
+ {files.map((f) => (
189
+ <li key={f.name}>{f.name} ({f.size}B) <button type="button" onClick={() => removeFile(f.name)}>×</button></li>
190
+ ))}
191
+ </ul>`
192
+ }
193
+ ];
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Layout & Structural Archetypes: Modal, Tabs, Inspector, State Boundary
3
+ */
4
+
5
+ export const LAYOUT_ARCHETYPES = [
6
+ {
7
+ id: 'modal-dialog',
8
+ name: 'Modal & Dialog',
9
+ keywords: ['modal', 'dialog', 'sheet', 'drawer', 'popup', 'lightbox'],
10
+ destructure: 'isOpen, payload, open, close, confirm',
11
+ buildState: (name, pascal) => `export interface ${pascal}State {
12
+ readonly isOpen: boolean;
13
+ readonly payload?: unknown;
14
+ }
15
+ `,
16
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
17
+ readonly defaultOpen?: boolean;
18
+ readonly onClose?: () => void;
19
+ readonly onConfirm?: () => void;
20
+ }
21
+ `,
22
+ buildController: (name, pascal) => `import { useState } from 'react';
23
+
24
+ export const use${pascal}Controller = (options: { defaultOpen?: boolean; onClose?: () => void; onConfirm?: () => void } = {}) => {
25
+ const [isOpen, setIsOpen] = useState<boolean>(Boolean(options.defaultOpen));
26
+ const [payload, setPayload] = useState<unknown>(null);
27
+
28
+ const open = (data?: unknown) => {
29
+ setPayload(data || null);
30
+ setIsOpen(true);
31
+ };
32
+
33
+ const close = () => {
34
+ setIsOpen(false);
35
+ setPayload(null);
36
+ options.onClose?.();
37
+ };
38
+
39
+ const confirm = () => {
40
+ options.onConfirm?.();
41
+ close();
42
+ };
43
+
44
+ return { isOpen, payload, open, close, confirm };
45
+ };
46
+ `,
47
+ buildReactBody: (name, pascal) => ` {isOpen && (
48
+ <div className="${name}__overlay" onClick={close}>
49
+ <div className="${name}__content" onClick={(e) => e.stopPropagation()}>
50
+ <div className="${name}__header">
51
+ <h3>Modal</h3>
52
+ <button type="button" onClick={close}>×</button>
53
+ </div>
54
+ <div className="${name}__actions">
55
+ <button type="button" onClick={close}>Cancel</button>
56
+ <button type="button" onClick={confirm}>Confirm</button>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ )}`
61
+ },
62
+ {
63
+ id: 'navigation-tabs',
64
+ name: 'Navigation & Tabs',
65
+ keywords: ['nav', 'tabs', 'menu', 'breadcrumbs', 'pagination'],
66
+ destructure: 'tabs = [], activeTabId, selectTab',
67
+ buildState: (name, pascal) => `export interface ${pascal}Tab {
68
+ readonly id: string;
69
+ readonly label: string;
70
+ }
71
+
72
+ export interface ${pascal}State {
73
+ readonly activeTabId: string;
74
+ }
75
+ `,
76
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
77
+ readonly tabs?: readonly { id: string; label: string }[];
78
+ readonly activeTabId?: string;
79
+ readonly onTabChange?: (tabId: string) => void;
80
+ }
81
+ `,
82
+ buildController: (name, pascal) => `import { useState } from 'react';
83
+
84
+ export const use${pascal}Controller = (options: { defaultTabId?: string; onTabChange?: (id: string) => void } = {}) => {
85
+ const [activeTabId, setActiveTabId] = useState<string>(options.defaultTabId || 'overview');
86
+
87
+ const selectTab = (id: string) => {
88
+ setActiveTabId(id);
89
+ options.onTabChange?.(id);
90
+ };
91
+
92
+ return { activeTabId, selectTab };
93
+ };
94
+ `,
95
+ buildReactBody: (name, pascal) => ` <div className="${name}__tabs" role="tablist">
96
+ {(tabs || [{ id: 'overview', label: 'Overview' }]).map((t) => (
97
+ <button
98
+ key={t.id}
99
+ type="button"
100
+ role="tab"
101
+ aria-selected={activeTabId === t.id}
102
+ className={activeTabId === t.id ? '${name}__tab ${name}__tab--active' : '${name}__tab'}
103
+ onClick={() => selectTab(t.id)}
104
+ >
105
+ {t.label}
106
+ </button>
107
+ ))}
108
+ </div>`
109
+ },
110
+ {
111
+ id: 'detail-inspector',
112
+ name: 'Detail & Inspector',
113
+ keywords: ['detail', 'inspector', 'sidebar', 'properties', 'pane'],
114
+ destructure: 'selectedEntityId, activeSection, isDirty, save, close',
115
+ buildState: (name, pascal) => `export interface ${pascal}State {
116
+ readonly selectedEntityId: string | null;
117
+ readonly activeSection: string;
118
+ readonly isDirty: boolean;
119
+ }
120
+ `,
121
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
122
+ readonly entityId?: string | null;
123
+ readonly onClose?: () => void;
124
+ readonly onSave?: () => void;
125
+ }
126
+ `,
127
+ buildController: (name, pascal) => `import { useState } from 'react';
128
+
129
+ export const use${pascal}Controller = (options: { entityId?: string | null; onClose?: () => void; onSave?: () => void } = {}) => {
130
+ const [selectedEntityId, setSelectedEntityId] = useState<string | null>(options.entityId || null);
131
+ const [activeSection, setActiveSection] = useState<string>('general');
132
+ const [isDirty, setIsDirty] = useState<boolean>(false);
133
+
134
+ const save = () => {
135
+ options.onSave?.();
136
+ setIsDirty(false);
137
+ };
138
+
139
+ const close = () => {
140
+ options.onClose?.();
141
+ };
142
+
143
+ return { selectedEntityId, activeSection, isDirty, setActiveSection, setIsDirty, save, close };
144
+ };
145
+ `,
146
+ buildReactBody: (name, pascal) => ` <div className="${name}__inspector">
147
+ <div className="${name}__header">
148
+ <span>Entity: {selectedEntityId || 'None'}</span>
149
+ <button type="button" onClick={close}>×</button>
150
+ </div>
151
+ <div className="${name}__body">
152
+ <button type="button" disabled={!isDirty} onClick={save}>Save Changes</button>
153
+ </div>
154
+ </div>`
155
+ },
156
+ {
157
+ id: 'state-boundary',
158
+ name: 'State Boundary & Fallback',
159
+ keywords: ['empty', 'fallback', 'error-boundary', 'placeholder', 'offline'],
160
+ destructure: 'state, canProceed, descriptor, handleAction',
161
+ buildState: (name, pascal) => `export type ${pascal}State =
162
+ | { readonly status: 'idle' }
163
+ | { readonly status: 'loading'; readonly progress: number }
164
+ | { readonly status: 'active'; readonly activeId: string }
165
+ | { readonly status: 'fault'; readonly faultMessage: string };
166
+
167
+ export interface ${pascal}Descriptor {
168
+ readonly text: string;
169
+ readonly className: string;
170
+ }
171
+ `,
172
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
173
+ readonly title?: string;
174
+ readonly subtitle?: string;
175
+ readonly onAction?: () => void;
176
+ }
177
+ `,
178
+ buildController: (name, pascal) => `import { useState, useMemo } from 'react';
179
+ import type { ${pascal}State, ${pascal}Descriptor } from './types';
180
+
181
+ interface ControllerOptions {
182
+ readonly initialState?: ${pascal}State;
183
+ readonly onAction?: () => void;
184
+ }
185
+
186
+ export const use${pascal}Controller = (options: ControllerOptions = {}) => {
187
+ const [state, setState] = useState<${pascal}State>(
188
+ options.initialState || { status: 'idle' }
189
+ );
190
+
191
+ const isIdle = state.status === 'idle';
192
+ const isPending = state.status === 'loading';
193
+ const canProceed = isIdle && !isPending;
194
+
195
+ const descriptor: ${pascal}Descriptor = useMemo(() => {
196
+ if (state.status === 'loading') {
197
+ return { text: 'Loading...', className: '${name}__badge ${name}__badge--pending' };
198
+ }
199
+ if (state.status === 'active') {
200
+ return { text: 'Active', className: '${name}__badge ${name}__badge--active' };
201
+ }
202
+ return { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
203
+ }, [state.status]);
204
+
205
+ const handleAction = () => {
206
+ if (!canProceed) return;
207
+ setState({ status: 'active', activeId: 'item-1' });
208
+ options.onAction?.();
209
+ };
210
+
211
+ return { state, canProceed, descriptor, handleAction };
212
+ };
213
+ `,
214
+ buildReactBody: (name, pascal) => ` <div className="${name}__status">
215
+ <span className={descriptor.className}>{descriptor.text}</span>
216
+ <button type="button" disabled={!canProceed} onClick={handleAction}>Action</button>
217
+ </div>`
218
+ }
219
+ ];
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Status, Feedback & Search Archetypes: Search Filter, KPI Metric, Toast, Media Player, Timer
3
+ */
4
+
5
+ export const STATUS_ARCHETYPES = [
6
+ {
7
+ id: 'search-filter',
8
+ name: 'Input & Search Filter',
9
+ keywords: ['search', 'filter', 'query', 'find', 'lookup', 'keyword'],
10
+ destructure: 'query, isSearching, activeFilters, handleQueryChange, toggleFilter, clear, placeholder',
11
+ buildState: (name, pascal) => `export interface ${pascal}State {
12
+ readonly query: string;
13
+ readonly isSearching: boolean;
14
+ readonly activeFilters: readonly string[];
15
+ }
16
+ `,
17
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
18
+ readonly placeholder?: string;
19
+ readonly onSearch?: (query: string) => void;
20
+ readonly onFilterChange?: (filters: readonly string[]) => void;
21
+ }
22
+ `,
23
+ buildController: (name, pascal) => `import { useState } from 'react';
24
+
25
+ export const use${pascal}Controller = (options: { onSearch?: (q: string) => void; onFilterChange?: (f: readonly string[]) => void } = {}) => {
26
+ const [query, setQuery] = useState<string>('');
27
+ const [isSearching, setIsSearching] = useState<boolean>(false);
28
+ const [activeFilters, setActiveFilters] = useState<readonly string[]>([]);
29
+
30
+ const handleQueryChange = (val: string) => {
31
+ setQuery(val);
32
+ options.onSearch?.(val);
33
+ };
34
+
35
+ const toggleFilter = (tag: string) => {
36
+ const next = activeFilters.includes(tag)
37
+ ? activeFilters.filter((t) => t !== tag)
38
+ : [...activeFilters, tag];
39
+ setActiveFilters(next);
40
+ options.onFilterChange?.(next);
41
+ };
42
+
43
+ const clear = () => {
44
+ setQuery('');
45
+ setActiveFilters([]);
46
+ options.onSearch?.('');
47
+ };
48
+
49
+ return { query, isSearching, activeFilters, handleQueryChange, toggleFilter, clear, setIsSearching };
50
+ };
51
+ `,
52
+ buildReactBody: (name, pascal) => ` <div className="${name}__search-bar">
53
+ <input type="text" value={query} placeholder={placeholder || 'Search...'} onChange={(e) => handleQueryChange(e.target.value)} />
54
+ {query && <button type="button" onClick={clear}>Clear</button>}
55
+ </div>`
56
+ },
57
+ {
58
+ id: 'kpi-metric',
59
+ name: 'Card & KPI Metric',
60
+ keywords: ['kpi', 'metric', 'stat', 'counter', 'tile'],
61
+ destructure: 'label, value, delta, trend, refresh',
62
+ buildState: (name, pascal) => `export interface ${pascal}State {
63
+ readonly value: number | string;
64
+ readonly delta: number;
65
+ readonly trend: 'up' | 'down' | 'neutral';
66
+ }
67
+ `,
68
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
69
+ readonly label?: string;
70
+ readonly value?: number | string;
71
+ readonly delta?: number;
72
+ readonly onRefresh?: () => void;
73
+ }
74
+ `,
75
+ buildController: (name, pascal) => `import { useState } from 'react';
76
+
77
+ export const use${pascal}Controller = (options: { value?: number | string; delta?: number; onRefresh?: () => void } = {}) => {
78
+ const [value, setValue] = useState<number | string>(options.value ?? 1284);
79
+ const [delta, setDelta] = useState<number>(options.delta ?? 12.5);
80
+
81
+ const trend: 'up' | 'down' | 'neutral' = delta > 0 ? 'up' : delta < 0 ? 'down' : 'neutral';
82
+
83
+ const refresh = () => {
84
+ options.onRefresh?.();
85
+ };
86
+
87
+ return { value, delta, trend, setValue, setDelta, refresh };
88
+ };
89
+ `,
90
+ buildReactBody: (name, pascal) => ` <div className="${name}__card">
91
+ <span className="${name}__label">{label || 'Total Metric'}</span>
92
+ <h2 className="${name}__value">{value}</h2>
93
+ <span className={\`${name}__trend ${name}__trend--\${trend}\`}>{delta > 0 ? '+' : ''}{delta}%</span>
94
+ </div>`
95
+ },
96
+ {
97
+ id: 'notification-toast',
98
+ name: 'Notification & Toast',
99
+ keywords: ['toast', 'alert', 'banner', 'notice', 'snackbar'],
100
+ destructure: 'notifications, notify, dismiss',
101
+ buildState: (name, pascal) => `export interface ${pascal}Notification {
102
+ readonly id: string;
103
+ readonly type: 'info' | 'success' | 'warning' | 'error';
104
+ readonly message: string;
105
+ }
106
+
107
+ export interface ${pascal}State {
108
+ readonly notifications: readonly ${pascal}Notification[];
109
+ }
110
+ `,
111
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
112
+ readonly autoDismissMs?: number;
113
+ }
114
+ `,
115
+ buildController: (name, pascal) => `import { useState } from 'react';
116
+ import type { ${pascal}Notification } from './types';
117
+
118
+ export const use${pascal}Controller = (options: { autoDismissMs?: number } = {}) => {
119
+ const [notifications, setNotifications] = useState<readonly ${pascal}Notification[]>([]);
120
+
121
+ const notify = (message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') => {
122
+ const id = String(Date.now());
123
+ setNotifications((prev) => [...prev, { id, type, message }]);
124
+ if (options.autoDismissMs) {
125
+ setTimeout(() => dismiss(id), options.autoDismissMs);
126
+ }
127
+ };
128
+
129
+ const dismiss = (id: string) => {
130
+ setNotifications((prev) => prev.filter((n) => n.id !== id));
131
+ };
132
+
133
+ return { notifications, notify, dismiss };
134
+ };
135
+ `,
136
+ buildReactBody: (name, pascal) => ` <div className="${name}__tray">
137
+ {notifications.map((n) => (
138
+ <div key={n.id} className={\`${name}__toast ${name}__toast--\${n.type}\`}>
139
+ <span>{n.message}</span>
140
+ <button type="button" onClick={() => dismiss(n.id)}>×</button>
141
+ </div>
142
+ ))}
143
+ </div>`
144
+ },
145
+ {
146
+ id: 'media-player',
147
+ name: 'Media Player & Carousel',
148
+ keywords: ['player', 'carousel', 'gallery', 'slider', 'audio', 'video'],
149
+ destructure: 'currentIndex, isPlaying, total, next, prev, togglePlay',
150
+ buildState: (name, pascal) => `export interface ${pascal}State {
151
+ readonly currentIndex: number;
152
+ readonly isPlaying: boolean;
153
+ readonly totalItems: number;
154
+ }
155
+ `,
156
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
157
+ readonly itemCount?: number;
158
+ readonly onIndexChange?: (idx: number) => void;
159
+ }
160
+ `,
161
+ buildController: (name, pascal) => `import { useState } from 'react';
162
+
163
+ export const use${pascal}Controller = (options: { itemCount?: number; onIndexChange?: (idx: number) => void } = {}) => {
164
+ const total = options.itemCount || 3;
165
+ const [currentIndex, setCurrentIndex] = useState<number>(0);
166
+ const [isPlaying, setIsPlaying] = useState<boolean>(false);
167
+
168
+ const next = () => {
169
+ setCurrentIndex((prev) => {
170
+ const nextIdx = (prev + 1) % total;
171
+ options.onIndexChange?.(nextIdx);
172
+ return nextIdx;
173
+ });
174
+ };
175
+
176
+ const prev = () => {
177
+ setCurrentIndex((prev) => {
178
+ const prevIdx = (prev - 1 + total) % total;
179
+ options.onIndexChange?.(prevIdx);
180
+ return prevIdx;
181
+ });
182
+ };
183
+
184
+ const togglePlay = () => setIsPlaying((p) => !p);
185
+
186
+ return { currentIndex, isPlaying, total, next, prev, togglePlay };
187
+ };
188
+ `,
189
+ buildReactBody: (name, pascal) => ` <div className="${name}__viewer">
190
+ <span>Slide {currentIndex + 1} of {total}</span>
191
+ <div className="${name}__controls">
192
+ <button type="button" onClick={prev}>Prev</button>
193
+ <button type="button" onClick={togglePlay}>{isPlaying ? 'Pause' : 'Play'}</button>
194
+ <button type="button" onClick={next}>Next</button>
195
+ </div>
196
+ </div>`
197
+ },
198
+ {
199
+ id: 'timer-countdown',
200
+ name: 'Timer & Countdown',
201
+ keywords: ['timer', 'stopwatch', 'countdown', 'clock', 'schedule'],
202
+ destructure: 'remainingSeconds, isRunning, isComplete, start, pause, reset',
203
+ buildState: (name, pascal) => `export interface ${pascal}State {
204
+ readonly remainingSeconds: number;
205
+ readonly isRunning: boolean;
206
+ readonly isComplete: boolean;
207
+ }
208
+ `,
209
+ buildProps: (name, pascal) => `export interface ${pascal}Props {
210
+ readonly durationSeconds?: number;
211
+ readonly onComplete?: () => void;
212
+ }
213
+ `,
214
+ buildController: (name, pascal) => `import { useState } from 'react';
215
+
216
+ export const use${pascal}Controller = (options: { durationSeconds?: number; onComplete?: () => void } = {}) => {
217
+ const initial = options.durationSeconds ?? 60;
218
+ const [remainingSeconds, setRemainingSeconds] = useState<number>(initial);
219
+ const [isRunning, setIsRunning] = useState<boolean>(false);
220
+
221
+ const start = () => setIsRunning(true);
222
+ const pause = () => setIsRunning(false);
223
+ const reset = () => {
224
+ setIsRunning(false);
225
+ setRemainingSeconds(initial);
226
+ };
227
+
228
+ const isComplete = remainingSeconds <= 0;
229
+
230
+ return { remainingSeconds, isRunning, isComplete, start, pause, reset };
231
+ };
232
+ `,
233
+ buildReactBody: (name, pascal) => ` <div className="${name}__timer">
234
+ <h2>{remainingSeconds}s</h2>
235
+ <div className="${name}__actions">
236
+ <button type="button" onClick={isRunning ? pause : start}>{isRunning ? 'Pause' : 'Start'}</button>
237
+ <button type="button" onClick={reset}>Reset</button>
238
+ </div>
239
+ </div>`
240
+ }
241
+ ];
@@ -1,4 +1,12 @@
1
- export const buildController = (name, pascalName) => `import { useState, useMemo } from 'react';
1
+ import { resolveArchetype } from './archetypes/index.js';
2
+
3
+ export const buildController = (name, pascalName) => {
4
+ const archetype = resolveArchetype(name);
5
+ if (archetype && typeof archetype.buildController === 'function') {
6
+ return archetype.buildController(name, pascalName);
7
+ }
8
+
9
+ return `import { useState, useMemo } from 'react';
2
10
  import type { ${pascalName}State, ${pascalName}Descriptor } from './types';
3
11
 
4
12
  interface ControllerOptions {
@@ -34,3 +42,4 @@ export const use${pascalName}Controller = (options: ControllerOptions = {}) => {
34
42
  return { state, canProceed, descriptor, handleAction };
35
43
  };
36
44
  `;
45
+ };