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.
Files changed (147) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +12 -4
  3. package/framework/agents/build/dev.md +5 -1
  4. package/framework/agents/discover/greenfield-wu.md +16 -1
  5. package/framework/agents/plan/tasks.md +31 -0
  6. package/framework/agents/plan/ux-brand-architect.md +21 -2
  7. package/framework/agents/plan/ux-component-engineer.md +10 -0
  8. package/framework/agents/quality/qa-implementation.md +1 -1
  9. package/framework/agents/quality/qa-planning.md +1 -1
  10. package/framework/agents/quality/qa-visual.md +24 -21
  11. package/framework/config.yaml +5 -4
  12. package/framework/constitution.md +58 -11
  13. package/framework/context/governance.md +12 -10
  14. package/framework/context/root.md +2 -2
  15. package/framework/data/entity-registry.yaml +12 -4
  16. package/framework/data/qa-rubrics.yaml +335 -0
  17. package/framework/domains/constitution.yaml +3 -3
  18. package/framework/executors/json-validate.js +80 -0
  19. package/framework/executors/npm-script.js +72 -0
  20. package/framework/executors/path-check.js +67 -0
  21. package/framework/executors/registry.yaml +36 -0
  22. package/framework/hooks/advance-trigger.js +47 -3
  23. package/framework/hooks/constitution-guard.js +9 -4
  24. package/framework/hooks/git-push-authority.js +113 -0
  25. package/framework/hooks/license-guard.js +59 -9
  26. package/framework/hooks/model-governance.js +40 -17
  27. package/framework/hooks/prism-engine.js +78 -11
  28. package/framework/hooks/session-digest.js +12 -3
  29. package/framework/hooks/settings.json +7 -3
  30. package/framework/hooks/style-guard.js +52 -3
  31. package/framework/hooks/team-quality-gate.js +43 -5
  32. package/framework/i18n/en.yaml +3 -3
  33. package/framework/i18n/es.yaml +3 -3
  34. package/framework/i18n/fr.yaml +3 -3
  35. package/framework/i18n/pt.yaml +3 -3
  36. package/framework/intelligence/confidence.yaml +85 -6
  37. package/framework/intelligence/context-engine.md +9 -5
  38. package/framework/intelligence/decision-engine.md +1 -1
  39. package/framework/orchestrator/chati-router.js +1 -1
  40. package/framework/orchestrator/chati-update.md +19 -3
  41. package/framework/orchestrator/chati.md +19 -1
  42. package/framework/quality-gates/planning-gate.md +2 -0
  43. package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
  44. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  45. package/framework/scaffold/saas-dashboard/README.md +58 -0
  46. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  47. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  48. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  49. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  50. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  51. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  52. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  53. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  54. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  55. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  56. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  57. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  58. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  59. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  60. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  61. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  62. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  63. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  64. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  65. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  66. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  67. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  68. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  69. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  70. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  71. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  72. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  73. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  74. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  75. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  76. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  77. package/framework/schemas/session.schema.json +5 -0
  78. package/framework/schemas/task.schema.json +9 -0
  79. package/framework/scripts/visual-qa.js +87 -1
  80. package/framework/tasks/orchestrator-health.md +4 -4
  81. package/framework/tasks/qa-impl-verdict.md +11 -5
  82. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  83. package/package.json +3 -3
  84. package/src/config/context-file-generator.js +0 -6
  85. package/src/dashboard/renderer.js +0 -36
  86. package/src/executors/runner.js +204 -0
  87. package/src/installer/core.js +2 -1
  88. package/src/installer/templates.js +5 -6
  89. package/src/intelligence/registry-manager.js +1 -1
  90. package/src/license/client.js +9 -3
  91. package/src/license/commands.js +12 -2
  92. package/src/license/machine-id.js +42 -1
  93. package/src/memory/gotchas.js +58 -40
  94. package/src/memory/magic-docs.js +1 -1
  95. package/src/memory/session-digest.js +9 -4
  96. package/src/orchestrator/cli.js +174 -14
  97. package/src/orchestrator/doctor.js +98 -16
  98. package/src/orchestrator/pipeline-manager.js +44 -22
  99. package/src/orchestrator/session-manager.js +56 -8
  100. package/src/telemetry/sender.js +7 -19
  101. package/src/terminal/run-team.js +3 -3
  102. package/src/upgrade/tracked-files-detector.js +34 -16
  103. package/src/utils/feature-flags.js +1 -1
  104. package/src/utils/flatten-entities.js +4 -40
  105. package/src/utils/schema-validator.js +0 -14
  106. package/src/wizard/i18n.js +3 -3
  107. package/src/api/index.js +0 -120
  108. package/src/autonomy/autonomous-gate.js +0 -294
  109. package/src/autonomy/build-loop.js +0 -281
  110. package/src/autonomy/build-state.js +0 -286
  111. package/src/autonomy/cause-analyzer.js +0 -177
  112. package/src/autonomy/escalation.js +0 -214
  113. package/src/autonomy/index.js +0 -51
  114. package/src/autonomy/mode-manager.js +0 -225
  115. package/src/autonomy/mode-suggester.js +0 -283
  116. package/src/autonomy/progress-reporter.js +0 -275
  117. package/src/autonomy/safety-net.js +0 -370
  118. package/src/config/agent-customizer.js +0 -231
  119. package/src/decision/analyzer.js +0 -291
  120. package/src/decision/engine.js +0 -250
  121. package/src/decision/index.js +0 -38
  122. package/src/decision/registry-healer.js +0 -468
  123. package/src/decision/registry-updater.js +0 -339
  124. package/src/extensions/loader.js +0 -145
  125. package/src/extensions/registry.js +0 -134
  126. package/src/gates/circuit-breaker.js +0 -151
  127. package/src/gates/g1-planning-complete.js +0 -154
  128. package/src/gates/g2-qa-planning.js +0 -156
  129. package/src/gates/g3-implementation.js +0 -215
  130. package/src/gates/g4-qa-implementation.js +0 -240
  131. package/src/gates/g5-deploy-ready.js +0 -181
  132. package/src/gates/gate-base.js +0 -185
  133. package/src/gates/index.js +0 -46
  134. package/src/health/auto-fix.js +0 -216
  135. package/src/health/engine.js +0 -246
  136. package/src/merger/semantic-merger.js +0 -292
  137. package/src/preview/detector.js +0 -238
  138. package/src/preview/index.js +0 -20
  139. package/src/preview/launcher.js +0 -235
  140. package/src/preview/log-buffer.js +0 -103
  141. package/src/quality/metrics-collector.js +0 -281
  142. package/src/quality/test-runner.js +0 -366
  143. package/src/tasks/executor.js +0 -195
  144. package/src/tasks/index.js +0 -4
  145. package/src/tasks/loader.js +0 -210
  146. package/src/tasks/router.js +0 -182
  147. 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
+ }