chati-dev 4.3.1 → 4.4.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/README.md +18 -13
- package/bin/chati.js +12 -4
- package/framework/agents/build/dev.md +5 -1
- package/framework/agents/discover/greenfield-wu.md +16 -1
- package/framework/agents/plan/tasks.md +31 -0
- package/framework/agents/plan/ux-brand-architect.md +21 -2
- package/framework/agents/plan/ux-component-engineer.md +10 -0
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +24 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +58 -11
- package/framework/context/governance.md +12 -10
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +12 -4
- package/framework/data/qa-rubrics.yaml +335 -0
- package/framework/domains/constitution.yaml +3 -3
- package/framework/executors/json-validate.js +80 -0
- package/framework/executors/npm-script.js +72 -0
- package/framework/executors/path-check.js +67 -0
- package/framework/executors/registry.yaml +36 -0
- package/framework/hooks/advance-trigger.js +47 -3
- package/framework/hooks/constitution-guard.js +9 -4
- package/framework/hooks/git-push-authority.js +113 -0
- package/framework/hooks/license-guard.js +59 -9
- package/framework/hooks/model-governance.js +40 -17
- package/framework/hooks/prism-engine.js +78 -11
- package/framework/hooks/session-digest.js +12 -3
- package/framework/hooks/settings.json +7 -3
- package/framework/hooks/style-guard.js +52 -3
- package/framework/hooks/team-quality-gate.js +43 -5
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/confidence.yaml +85 -6
- package/framework/intelligence/context-engine.md +9 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/orchestrator/chati-router.js +1 -1
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +19 -1
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/saas-dashboard/README.md +58 -0
- package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
- package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
- package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
- package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
- package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
- package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
- package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
- package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
- package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
- package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
- package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
- package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
- package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
- package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
- package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
- package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
- package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
- package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
- package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
- package/framework/schemas/session.schema.json +5 -0
- package/framework/schemas/task.schema.json +9 -0
- package/framework/scripts/visual-qa.js +87 -1
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +3 -3
- package/src/config/context-file-generator.js +0 -6
- package/src/dashboard/renderer.js +0 -36
- package/src/executors/runner.js +204 -0
- package/src/installer/core.js +2 -1
- package/src/installer/templates.js +5 -6
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +9 -3
- package/src/license/commands.js +12 -2
- package/src/license/machine-id.js +42 -1
- package/src/memory/gotchas.js +58 -40
- package/src/memory/magic-docs.js +1 -1
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +174 -14
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +56 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/feature-flags.js +1 -1
- package/src/utils/flatten-entities.js +4 -40
- package/src/utils/schema-validator.js +0 -14
- package/src/wizard/i18n.js +3 -3
- package/src/api/index.js +0 -120
- package/src/autonomy/autonomous-gate.js +0 -294
- package/src/autonomy/build-loop.js +0 -281
- package/src/autonomy/build-state.js +0 -286
- package/src/autonomy/cause-analyzer.js +0 -177
- package/src/autonomy/escalation.js +0 -214
- package/src/autonomy/index.js +0 -51
- package/src/autonomy/mode-manager.js +0 -225
- package/src/autonomy/mode-suggester.js +0 -283
- package/src/autonomy/progress-reporter.js +0 -275
- package/src/autonomy/safety-net.js +0 -370
- package/src/config/agent-customizer.js +0 -231
- package/src/decision/analyzer.js +0 -291
- package/src/decision/engine.js +0 -250
- package/src/decision/index.js +0 -38
- package/src/decision/registry-healer.js +0 -468
- package/src/decision/registry-updater.js +0 -339
- package/src/extensions/loader.js +0 -145
- package/src/extensions/registry.js +0 -134
- package/src/gates/circuit-breaker.js +0 -151
- package/src/gates/g1-planning-complete.js +0 -154
- package/src/gates/g2-qa-planning.js +0 -156
- package/src/gates/g3-implementation.js +0 -215
- package/src/gates/g4-qa-implementation.js +0 -240
- package/src/gates/g5-deploy-ready.js +0 -181
- package/src/gates/gate-base.js +0 -185
- package/src/gates/index.js +0 -46
- package/src/health/auto-fix.js +0 -216
- package/src/health/engine.js +0 -246
- package/src/merger/semantic-merger.js +0 -292
- package/src/preview/detector.js +0 -238
- package/src/preview/index.js +0 -20
- package/src/preview/launcher.js +0 -235
- package/src/preview/log-buffer.js +0 -103
- package/src/quality/metrics-collector.js +0 -281
- package/src/quality/test-runner.js +0 -366
- package/src/tasks/executor.js +0 -195
- package/src/tasks/index.js +0 -4
- package/src/tasks/loader.js +0 -210
- package/src/tasks/router.js +0 -182
- package/src/utils/event-bus.js +0 -126
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* components/interaction/Form.tsx
|
|
3
|
+
* SaaS Dashboard Preset -- D_interaction phase
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* Form composition is one of the highest-friction areas in dashboard UI.
|
|
8
|
+
* Three recurring failures:
|
|
9
|
+
*
|
|
10
|
+
* 1. Error summary placement: errors appear in field-level tooltips only,
|
|
11
|
+
* so keyboard users navigating linearly miss them. A centralized error
|
|
12
|
+
* summary at the top of the form, announced via role=alert and focus-
|
|
13
|
+
* managed on submit failure, is the accessible pattern.
|
|
14
|
+
*
|
|
15
|
+
* 2. Responsive layout inconsistency: developers reach for ad-hoc grid
|
|
16
|
+
* utilities, producing forms that look great on desktop but stack
|
|
17
|
+
* badly on mobile or create awkward widths on tablet. FormRow
|
|
18
|
+
* encapsulates the 1-column mobile / 2-column desktop pattern once.
|
|
19
|
+
*
|
|
20
|
+
* 3. Action bar stickiness on mobile: a long form with a floating submit
|
|
21
|
+
* button that disappears into the scroll area is a conversion killer
|
|
22
|
+
* on mobile. FormActions sticks to the bottom of the viewport on
|
|
23
|
+
* narrow screens but flows inline at desktop widths.
|
|
24
|
+
*
|
|
25
|
+
* COMPONENT MAP
|
|
26
|
+
* -------------
|
|
27
|
+
* Form -- <form> wrapper, handles onSubmit + error summary
|
|
28
|
+
* FormSection -- Labeled group (<fieldset> + <legend> or <div> + heading)
|
|
29
|
+
* FormRow -- CSS grid: 1-col mobile, 2-col desktop
|
|
30
|
+
* FormActions -- Action bar with primary + cancel
|
|
31
|
+
*
|
|
32
|
+
* USAGE EXAMPLE
|
|
33
|
+
* -------------
|
|
34
|
+
* <Form onSubmit={handleSubmit} errors={formErrors}>
|
|
35
|
+
* <FormSection label="Personal details">
|
|
36
|
+
* <FormRow>
|
|
37
|
+
* <Field label="First name" required inputProps={{ name: 'firstName' }} />
|
|
38
|
+
* <Field label="Last name" required inputProps={{ name: 'lastName' }} />
|
|
39
|
+
* </FormRow>
|
|
40
|
+
* <Field label="Email address" required inputProps={{ type: 'email', name: 'email' }} />
|
|
41
|
+
* </FormSection>
|
|
42
|
+
* <FormActions primaryLabel="Save changes" onCancel={handleCancel} />
|
|
43
|
+
* </Form>
|
|
44
|
+
*
|
|
45
|
+
* TOKEN USAGE
|
|
46
|
+
* -----------
|
|
47
|
+
* All spacing, colors, and radii reference CSS custom properties.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
'use client';
|
|
51
|
+
|
|
52
|
+
import React, {
|
|
53
|
+
useRef,
|
|
54
|
+
useEffect,
|
|
55
|
+
type FormHTMLAttributes,
|
|
56
|
+
type ReactNode,
|
|
57
|
+
} from 'react';
|
|
58
|
+
import { AlertCircle } from 'lucide-react';
|
|
59
|
+
import { Button } from '@/components/primitives/Button';
|
|
60
|
+
import { Field } from '@/components/interaction/Field';
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Form root
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export interface FormError {
|
|
67
|
+
field?: string;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'> {
|
|
72
|
+
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
73
|
+
/** Top-level errors rendered in the error summary block. */
|
|
74
|
+
errors?: FormError[];
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function Form({ onSubmit, errors, children, ...rest }: FormProps) {
|
|
79
|
+
const errorSummaryRef = useRef<HTMLDivElement>(null);
|
|
80
|
+
|
|
81
|
+
// Move focus to error summary on new errors (accessibility: SR users hear it)
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (errors && errors.length > 0) {
|
|
84
|
+
errorSummaryRef.current?.focus();
|
|
85
|
+
}
|
|
86
|
+
}, [errors]);
|
|
87
|
+
|
|
88
|
+
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
await onSubmit(e);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<form
|
|
95
|
+
onSubmit={handleSubmit}
|
|
96
|
+
noValidate
|
|
97
|
+
style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-section)' }}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
{/* Error summary */}
|
|
101
|
+
{errors && errors.length > 0 && (
|
|
102
|
+
<div
|
|
103
|
+
ref={errorSummaryRef}
|
|
104
|
+
role="alert"
|
|
105
|
+
aria-live="assertive"
|
|
106
|
+
tabIndex={-1}
|
|
107
|
+
style={{
|
|
108
|
+
display: 'flex',
|
|
109
|
+
gap: 'var(--space-3)',
|
|
110
|
+
padding: 'var(--space-4)',
|
|
111
|
+
backgroundColor: 'var(--color-danger-subtle)',
|
|
112
|
+
border: '1px solid var(--color-danger)',
|
|
113
|
+
borderRadius: 'var(--radius-default)',
|
|
114
|
+
outline: 'none',
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<AlertCircle
|
|
118
|
+
size={18}
|
|
119
|
+
aria-hidden="true"
|
|
120
|
+
style={{ color: 'var(--color-danger)', flexShrink: 0, marginTop: '1px' }}
|
|
121
|
+
/>
|
|
122
|
+
<div style={{ flex: 1 }}>
|
|
123
|
+
<p
|
|
124
|
+
style={{
|
|
125
|
+
fontSize: 'var(--type-small)',
|
|
126
|
+
fontWeight: 600,
|
|
127
|
+
color: 'var(--color-danger)',
|
|
128
|
+
margin: '0 0 var(--space-1)',
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
Please fix the following errors:
|
|
132
|
+
</p>
|
|
133
|
+
<ul
|
|
134
|
+
style={{
|
|
135
|
+
margin: 0,
|
|
136
|
+
paddingLeft: 'var(--space-4)',
|
|
137
|
+
fontSize: 'var(--type-small)',
|
|
138
|
+
color: 'var(--color-danger)',
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{errors.map((err, i) => (
|
|
142
|
+
<li key={i}>{err.message}</li>
|
|
143
|
+
))}
|
|
144
|
+
</ul>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{children}
|
|
150
|
+
</form>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// FormSection
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
export interface FormSectionProps {
|
|
159
|
+
/** Rendered as a <legend> inside <fieldset> when useFieldset=true,
|
|
160
|
+
* otherwise as a heading above the group. */
|
|
161
|
+
label: string;
|
|
162
|
+
/** Optional description below the label. */
|
|
163
|
+
description?: string;
|
|
164
|
+
/** Use semantic <fieldset> + <legend>. Default true for radio/checkbox groups. */
|
|
165
|
+
useFieldset?: boolean;
|
|
166
|
+
children: ReactNode;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function FormSection({
|
|
170
|
+
label,
|
|
171
|
+
description,
|
|
172
|
+
useFieldset = false,
|
|
173
|
+
children,
|
|
174
|
+
}: FormSectionProps) {
|
|
175
|
+
const content = (
|
|
176
|
+
<>
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
paddingBottom: 'var(--space-5)',
|
|
180
|
+
borderBottom: '1px solid var(--color-border-default)',
|
|
181
|
+
marginBottom: 'var(--space-6)',
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{useFieldset ? (
|
|
185
|
+
<legend
|
|
186
|
+
style={{
|
|
187
|
+
fontSize: 'var(--type-h4)',
|
|
188
|
+
lineHeight: 'var(--type-h4-lh)',
|
|
189
|
+
fontWeight: 600,
|
|
190
|
+
color: 'var(--color-text-primary)',
|
|
191
|
+
letterSpacing: 'var(--type-h4-ls)',
|
|
192
|
+
padding: 0,
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
{label}
|
|
196
|
+
</legend>
|
|
197
|
+
) : (
|
|
198
|
+
<h3
|
|
199
|
+
style={{
|
|
200
|
+
fontSize: 'var(--type-h4)',
|
|
201
|
+
lineHeight: 'var(--type-h4-lh)',
|
|
202
|
+
fontWeight: 600,
|
|
203
|
+
color: 'var(--color-text-primary)',
|
|
204
|
+
letterSpacing: 'var(--type-h4-ls)',
|
|
205
|
+
margin: 0,
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
{label}
|
|
209
|
+
</h3>
|
|
210
|
+
)}
|
|
211
|
+
{description && (
|
|
212
|
+
<p
|
|
213
|
+
style={{
|
|
214
|
+
fontSize: 'var(--type-small)',
|
|
215
|
+
lineHeight: 'var(--type-small-lh)',
|
|
216
|
+
color: 'var(--color-text-secondary)',
|
|
217
|
+
margin: 'var(--space-1) 0 0',
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{description}
|
|
221
|
+
</p>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
<div
|
|
225
|
+
style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-card-gap)' }}
|
|
226
|
+
>
|
|
227
|
+
{children}
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (useFieldset) {
|
|
233
|
+
return (
|
|
234
|
+
<fieldset
|
|
235
|
+
style={{
|
|
236
|
+
border: 'none',
|
|
237
|
+
padding: 0,
|
|
238
|
+
margin: 0,
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{content}
|
|
242
|
+
</fieldset>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return <div>{content}</div>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// FormRow (responsive 2-col grid)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
export function FormRow({ children }: { children: ReactNode }) {
|
|
254
|
+
return (
|
|
255
|
+
<div
|
|
256
|
+
style={{
|
|
257
|
+
display: 'grid',
|
|
258
|
+
gap: 'var(--spacing-card-gap)',
|
|
259
|
+
// CSS custom property trick: Tailwind-free responsive grid via
|
|
260
|
+
// container query when available, viewport query as fallback.
|
|
261
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))',
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{children}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// FormActions
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
export interface FormActionsProps {
|
|
274
|
+
/** Primary action label (submit button). */
|
|
275
|
+
primaryLabel?: string;
|
|
276
|
+
/** Called when cancel is clicked. Renders cancel button only when provided. */
|
|
277
|
+
onCancel?: () => void;
|
|
278
|
+
/** Set true when form submission is in progress. */
|
|
279
|
+
loading?: boolean;
|
|
280
|
+
/** Disables primary button. */
|
|
281
|
+
disabled?: boolean;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function FormActions({
|
|
285
|
+
primaryLabel = 'Save changes',
|
|
286
|
+
onCancel,
|
|
287
|
+
loading = false,
|
|
288
|
+
disabled = false,
|
|
289
|
+
}: FormActionsProps) {
|
|
290
|
+
return (
|
|
291
|
+
<div
|
|
292
|
+
style={{
|
|
293
|
+
display: 'flex',
|
|
294
|
+
alignItems: 'center',
|
|
295
|
+
justifyContent: 'flex-end',
|
|
296
|
+
gap: 'var(--space-3)',
|
|
297
|
+
paddingTop: 'var(--space-6)',
|
|
298
|
+
borderTop: '1px solid var(--color-border-default)',
|
|
299
|
+
// Sticky on mobile: stays visible at bottom of viewport
|
|
300
|
+
position: 'sticky',
|
|
301
|
+
bottom: 0,
|
|
302
|
+
backgroundColor: 'var(--color-bg-base)',
|
|
303
|
+
zIndex: 10,
|
|
304
|
+
marginInline: 'calc(-1 * var(--spacing-card-pad))',
|
|
305
|
+
paddingInline: 'var(--spacing-card-pad)',
|
|
306
|
+
paddingBottom: 'var(--space-4)',
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
{onCancel && (
|
|
310
|
+
<Button
|
|
311
|
+
variant="secondary"
|
|
312
|
+
size="md"
|
|
313
|
+
type="button"
|
|
314
|
+
onClick={onCancel}
|
|
315
|
+
disabled={loading}
|
|
316
|
+
>
|
|
317
|
+
Cancel
|
|
318
|
+
</Button>
|
|
319
|
+
)}
|
|
320
|
+
<Button
|
|
321
|
+
variant="primary"
|
|
322
|
+
size="md"
|
|
323
|
+
type="submit"
|
|
324
|
+
loading={loading}
|
|
325
|
+
disabled={disabled}
|
|
326
|
+
>
|
|
327
|
+
{primaryLabel}
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// FormExample (demonstration component, not part of the public API)
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* FormExample: a complete, self-contained form demonstrating:
|
|
339
|
+
* - Real labels (never placeholder-only)
|
|
340
|
+
* - Validation error states
|
|
341
|
+
* - Required field marking
|
|
342
|
+
* - 2-column responsive row
|
|
343
|
+
* - FormActions with loading state
|
|
344
|
+
*
|
|
345
|
+
* Import path: @/components/interaction/Form (named export)
|
|
346
|
+
*/
|
|
347
|
+
export function FormExample() {
|
|
348
|
+
return (
|
|
349
|
+
<Form
|
|
350
|
+
onSubmit={async (e) => {
|
|
351
|
+
// Replace with your submit handler
|
|
352
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
353
|
+
}}
|
|
354
|
+
errors={[
|
|
355
|
+
{ field: 'email', message: 'Email address is not valid' },
|
|
356
|
+
]}
|
|
357
|
+
>
|
|
358
|
+
<FormSection
|
|
359
|
+
label="Account details"
|
|
360
|
+
description="Update your personal information and contact details."
|
|
361
|
+
>
|
|
362
|
+
<FormRow>
|
|
363
|
+
<Field
|
|
364
|
+
label="First name"
|
|
365
|
+
required
|
|
366
|
+
inputProps={{
|
|
367
|
+
name: 'firstName',
|
|
368
|
+
autoComplete: 'given-name',
|
|
369
|
+
placeholder: 'Ada',
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
|
+
<Field
|
|
373
|
+
label="Last name"
|
|
374
|
+
required
|
|
375
|
+
inputProps={{
|
|
376
|
+
name: 'lastName',
|
|
377
|
+
autoComplete: 'family-name',
|
|
378
|
+
placeholder: 'Lovelace',
|
|
379
|
+
}}
|
|
380
|
+
/>
|
|
381
|
+
</FormRow>
|
|
382
|
+
|
|
383
|
+
<Field
|
|
384
|
+
label="Email address"
|
|
385
|
+
required
|
|
386
|
+
error="Email address is not valid"
|
|
387
|
+
hint="Used for login and notifications"
|
|
388
|
+
inputProps={{
|
|
389
|
+
type: 'email',
|
|
390
|
+
name: 'email',
|
|
391
|
+
autoComplete: 'email',
|
|
392
|
+
placeholder: 'ada@example.com',
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
<Field
|
|
397
|
+
label="Role"
|
|
398
|
+
as="select"
|
|
399
|
+
hint="Determines permissions and dashboard access."
|
|
400
|
+
selectProps={{ name: 'role', defaultValue: '' }}
|
|
401
|
+
>
|
|
402
|
+
<option value="" disabled>
|
|
403
|
+
Select a role...
|
|
404
|
+
</option>
|
|
405
|
+
<option value="admin">Admin</option>
|
|
406
|
+
<option value="editor">Editor</option>
|
|
407
|
+
<option value="viewer">Viewer</option>
|
|
408
|
+
</Field>
|
|
409
|
+
|
|
410
|
+
<Field
|
|
411
|
+
label="Notes"
|
|
412
|
+
as="textarea"
|
|
413
|
+
hint="Optional context for the team."
|
|
414
|
+
textareaProps={{
|
|
415
|
+
name: 'notes',
|
|
416
|
+
placeholder: 'Anything the team should know...',
|
|
417
|
+
}}
|
|
418
|
+
/>
|
|
419
|
+
</FormSection>
|
|
420
|
+
|
|
421
|
+
<FormActions primaryLabel="Save changes" onCancel={() => {}} />
|
|
422
|
+
</Form>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* components/primitives/Badge.tsx -- saas-dashboard preset v1.0.0
|
|
3
|
+
*
|
|
4
|
+
* WHY BADGES NEED A PRIMITIVE
|
|
5
|
+
* ----------------------------
|
|
6
|
+
* Status indicators, count bubbles, category tags, and removable
|
|
7
|
+
* filter chips all share the same fundamental anatomy:
|
|
8
|
+
* - Pill or rounded-rectangle shape
|
|
9
|
+
* - Small type scale (caption or small)
|
|
10
|
+
* - Semantic color that communicates intent (success=green, etc.)
|
|
11
|
+
* - Optional leading dot for additional visual emphasis
|
|
12
|
+
* - Optional trailing X for removable tags
|
|
13
|
+
*
|
|
14
|
+
* Without a shared primitive, these proliferate as ad-hoc div+span
|
|
15
|
+
* combinations with inconsistent padding, varying font weights, and
|
|
16
|
+
* colour values that reference raw hex instead of semantic tokens.
|
|
17
|
+
* The Badge primitive centralises this into one contract.
|
|
18
|
+
*
|
|
19
|
+
* VARIANT SEMANTICS
|
|
20
|
+
* -----------------
|
|
21
|
+
* default: Neutral tint. Use for categories, labels.
|
|
22
|
+
* success: Green tint. Use for active, complete, healthy.
|
|
23
|
+
* warning: Amber tint. Use for pending, at-risk, expiring.
|
|
24
|
+
* danger: Red tint. Use for error, critical, overdue.
|
|
25
|
+
* info: Sky tint. Use for informational, new, beta.
|
|
26
|
+
* accent: Violet tint. Use for featured, promoted, selected.
|
|
27
|
+
*
|
|
28
|
+
* Each variant uses the semantic *-subtle token for background and
|
|
29
|
+
* the full-saturation token for text/dot. This ensures the badge
|
|
30
|
+
* reads clearly on all surface levels (bg-base, bg-raised, bg-overlay).
|
|
31
|
+
*
|
|
32
|
+
* SIZE SYSTEM
|
|
33
|
+
* -----------
|
|
34
|
+
* sm: 12px type (caption), 2px/8px padding -- for table cells, tight grids
|
|
35
|
+
* md: 14px type (small), 4px/10px padding -- default, standalone usage
|
|
36
|
+
*
|
|
37
|
+
* DOT VARIANT
|
|
38
|
+
* -----------
|
|
39
|
+
* Renders a 6px filled circle before the label text, color-matched
|
|
40
|
+
* to the variant's full-saturation token. Communicates "status"
|
|
41
|
+
* more emphatically than text alone.
|
|
42
|
+
*
|
|
43
|
+
* REMOVABLE VARIANT
|
|
44
|
+
* -----------------
|
|
45
|
+
* Appends an X icon button. The button has its own focus ring and
|
|
46
|
+
* is keyboard-accessible. The badge animates out via framer-motion
|
|
47
|
+
* AnimatePresence when removed (opacity fade, scale, --duration-fast).
|
|
48
|
+
*
|
|
49
|
+
* The parent is responsible for removing the badge from the list on
|
|
50
|
+
* onRemove; the badge itself only handles the visual transition.
|
|
51
|
+
*
|
|
52
|
+
* TRANSITIONS
|
|
53
|
+
* -----------
|
|
54
|
+
* Mount/unmount animation via framer-motion AnimatePresence:
|
|
55
|
+
* enter: opacity 0 -> 1, scale 0.85 -> 1, ease-out, duration-fast
|
|
56
|
+
* exit: opacity 1 -> 0, scale 1 -> 0.85, ease-in, duration-fast
|
|
57
|
+
* Hover on removable X: opacity 0.7 -> 1, duration-instant.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { type HTMLAttributes, type MouseEventHandler } from 'react';
|
|
61
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
62
|
+
import { X } from 'lucide-react';
|
|
63
|
+
import { DURATION, EASING } from '@/lib/brand';
|
|
64
|
+
|
|
65
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
66
|
+
/** Color intent */
|
|
67
|
+
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info' | 'accent';
|
|
68
|
+
/** Size */
|
|
69
|
+
size?: 'sm' | 'md';
|
|
70
|
+
/** Renders a 6px status dot before label */
|
|
71
|
+
dot?: boolean;
|
|
72
|
+
/** Makes badge a removable tag with X icon button */
|
|
73
|
+
removable?: boolean;
|
|
74
|
+
/** Called when X is clicked (requires removable=true) */
|
|
75
|
+
onRemove?: MouseEventHandler<HTMLButtonElement>;
|
|
76
|
+
/** Controls AnimatePresence for exit animation */
|
|
77
|
+
visible?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ----------------------------------------------------------------
|
|
81
|
+
CLASS MAPS
|
|
82
|
+
---------------------------------------------------------------- */
|
|
83
|
+
|
|
84
|
+
const SIZE_CLASSES: Record<NonNullable<BadgeProps['size']>, string> = {
|
|
85
|
+
sm: 'px-[var(--space-2)] py-[var(--space-px)] text-[var(--type-caption)] leading-[var(--type-caption-lh)] tracking-[var(--type-caption-ls)]',
|
|
86
|
+
md: 'px-[var(--space-3)] py-[var(--space-1)] text-[var(--type-small)] leading-[var(--type-small-lh)]',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const DOT_COLOR: Record<NonNullable<BadgeProps['variant']>, string> = {
|
|
90
|
+
default: 'bg-[var(--color-neutral-400)]',
|
|
91
|
+
success: 'bg-[var(--color-success)]',
|
|
92
|
+
warning: 'bg-[var(--color-warning)]',
|
|
93
|
+
danger: 'bg-[var(--color-danger)]',
|
|
94
|
+
info: 'bg-[var(--color-info)]',
|
|
95
|
+
accent: 'bg-[var(--color-accent)]',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const VARIANT_CLASSES: Record<NonNullable<BadgeProps['variant']>, string> = {
|
|
99
|
+
default: 'bg-[var(--color-bg-raised)] text-[var(--color-text-secondary)] border border-[var(--color-border-default)]',
|
|
100
|
+
success: 'bg-[var(--color-success-subtle)] text-[var(--color-success)]',
|
|
101
|
+
warning: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning)]',
|
|
102
|
+
danger: 'bg-[var(--color-danger-subtle)] text-[var(--color-danger)]',
|
|
103
|
+
info: 'bg-[var(--color-info-subtle)] text-[var(--color-info)]',
|
|
104
|
+
accent: 'bg-[var(--color-accent-subtle)] text-[var(--color-accent-hover)]',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/* ----------------------------------------------------------------
|
|
108
|
+
COMPONENT
|
|
109
|
+
---------------------------------------------------------------- */
|
|
110
|
+
|
|
111
|
+
export function Badge({
|
|
112
|
+
variant = 'default',
|
|
113
|
+
size = 'md',
|
|
114
|
+
dot = false,
|
|
115
|
+
removable = false,
|
|
116
|
+
onRemove,
|
|
117
|
+
visible = true,
|
|
118
|
+
children,
|
|
119
|
+
className,
|
|
120
|
+
...rest
|
|
121
|
+
}: BadgeProps) {
|
|
122
|
+
const classes = [
|
|
123
|
+
'inline-flex items-center gap-[var(--space-1)]',
|
|
124
|
+
'rounded-[var(--radius-pill)]',
|
|
125
|
+
'font-[var(--font-weight-medium)]',
|
|
126
|
+
'whitespace-nowrap',
|
|
127
|
+
VARIANT_CLASSES[variant],
|
|
128
|
+
SIZE_CLASSES[size],
|
|
129
|
+
className,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(' ');
|
|
133
|
+
|
|
134
|
+
const badgeContent = (
|
|
135
|
+
<span className={classes} {...rest}>
|
|
136
|
+
{dot && (
|
|
137
|
+
<span
|
|
138
|
+
aria-hidden="true"
|
|
139
|
+
className={[
|
|
140
|
+
'inline-block rounded-full w-1.5 h-1.5 shrink-0',
|
|
141
|
+
DOT_COLOR[variant],
|
|
142
|
+
].join(' ')}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
<span>{children}</span>
|
|
147
|
+
|
|
148
|
+
{removable && (
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={onRemove}
|
|
152
|
+
aria-label={`Remove ${typeof children === 'string' ? children : 'item'}`}
|
|
153
|
+
className={[
|
|
154
|
+
'inline-flex items-center justify-center',
|
|
155
|
+
'w-3.5 h-3.5 rounded-full',
|
|
156
|
+
'opacity-60 hover:opacity-100',
|
|
157
|
+
'transition-opacity [transition-duration:var(--duration-instant)]',
|
|
158
|
+
'[transition-timing-function:var(--ease-in-out)]',
|
|
159
|
+
'focus-visible:outline-none focus-visible:ring-1',
|
|
160
|
+
'focus-visible:ring-[var(--color-focus-ring)]',
|
|
161
|
+
'cursor-pointer',
|
|
162
|
+
].join(' ')}
|
|
163
|
+
>
|
|
164
|
+
<X size={10} aria-hidden="true" />
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
</span>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
/* For removable badges, wrap in AnimatePresence for exit animation */
|
|
171
|
+
if (removable) {
|
|
172
|
+
return (
|
|
173
|
+
<AnimatePresence>
|
|
174
|
+
{visible && (
|
|
175
|
+
<motion.span
|
|
176
|
+
initial={{ opacity: 0, scale: 0.85 }}
|
|
177
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
178
|
+
exit={{ opacity: 0, scale: 0.85 }}
|
|
179
|
+
transition={{
|
|
180
|
+
duration: DURATION.fast / 1000,
|
|
181
|
+
ease: EASING.out,
|
|
182
|
+
}}
|
|
183
|
+
style={{ display: 'inline-flex' }}
|
|
184
|
+
>
|
|
185
|
+
{badgeContent}
|
|
186
|
+
</motion.span>
|
|
187
|
+
)}
|
|
188
|
+
</AnimatePresence>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return badgeContent;
|
|
193
|
+
}
|