chati-dev 4.3.0 → 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 (156) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +32 -4
  3. package/framework/agents/build/dev.md +9 -5
  4. package/framework/agents/discover/brief.md +2 -0
  5. package/framework/agents/discover/brownfield-wu.md +2 -0
  6. package/framework/agents/discover/greenfield-wu.md +18 -1
  7. package/framework/agents/plan/detail.md +2 -0
  8. package/framework/agents/plan/tasks.md +31 -0
  9. package/framework/agents/plan/ux-brand-architect.md +21 -2
  10. package/framework/agents/plan/ux-component-engineer.md +10 -0
  11. package/framework/agents/quality/qa-implementation.md +1 -1
  12. package/framework/agents/quality/qa-planning.md +1 -1
  13. package/framework/agents/quality/qa-visual.md +33 -21
  14. package/framework/config.yaml +5 -4
  15. package/framework/constitution.md +58 -11
  16. package/framework/context/governance.md +12 -10
  17. package/framework/context/root.md +2 -2
  18. package/framework/data/entity-registry.yaml +12 -4
  19. package/framework/data/qa-rubrics.yaml +335 -0
  20. package/framework/domains/constitution.yaml +3 -3
  21. package/framework/executors/json-validate.js +80 -0
  22. package/framework/executors/npm-script.js +72 -0
  23. package/framework/executors/path-check.js +67 -0
  24. package/framework/executors/registry.yaml +36 -0
  25. package/framework/hooks/advance-trigger.js +47 -3
  26. package/framework/hooks/constitution-guard.js +9 -4
  27. package/framework/hooks/git-push-authority.js +113 -0
  28. package/framework/hooks/license-guard.js +59 -9
  29. package/framework/hooks/model-governance.js +40 -17
  30. package/framework/hooks/prism-engine.js +78 -11
  31. package/framework/hooks/session-digest.js +12 -3
  32. package/framework/hooks/settings.json +7 -3
  33. package/framework/hooks/style-guard.js +52 -3
  34. package/framework/hooks/team-quality-gate.js +43 -5
  35. package/framework/i18n/en.yaml +3 -3
  36. package/framework/i18n/es.yaml +3 -3
  37. package/framework/i18n/fr.yaml +3 -3
  38. package/framework/i18n/pt.yaml +3 -3
  39. package/framework/intelligence/confidence.yaml +85 -6
  40. package/framework/intelligence/context-engine.md +9 -5
  41. package/framework/intelligence/decision-engine.md +1 -1
  42. package/framework/orchestrator/chati-router.js +59 -5
  43. package/framework/orchestrator/chati-update.md +19 -3
  44. package/framework/orchestrator/chati.md +20 -1
  45. package/framework/quality-gates/planning-gate.md +2 -0
  46. package/framework/rules/human-writing-style.md +47 -0
  47. package/framework/scaffold/motion-premium/README.md +12 -2
  48. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
  49. package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
  50. package/framework/scaffold/motion-premium-3d/README.md +10 -0
  51. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  52. package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
  53. package/framework/scaffold/saas-dashboard/README.md +58 -0
  54. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  55. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  56. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  57. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  58. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  59. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  60. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  61. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  62. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  63. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  64. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  65. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  66. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  67. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  68. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  69. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  70. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  71. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  72. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  73. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  74. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  75. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  76. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  77. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  78. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  79. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  80. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  81. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  82. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  83. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  84. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  85. package/framework/schemas/session.schema.json +5 -0
  86. package/framework/schemas/task.schema.json +9 -0
  87. package/framework/scripts/visual-qa.js +101 -3
  88. package/framework/tasks/orchestrator-health.md +4 -4
  89. package/framework/tasks/qa-impl-verdict.md +11 -5
  90. package/framework/templates/brandbook-html-tmpl.md +1 -1
  91. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  92. package/package.json +4 -3
  93. package/src/config/context-file-generator.js +0 -6
  94. package/src/dashboard/renderer.js +0 -36
  95. package/src/executors/runner.js +204 -0
  96. package/src/installer/core.js +13 -3
  97. package/src/installer/templates.js +5 -6
  98. package/src/intelligence/registry-manager.js +1 -1
  99. package/src/license/client.js +9 -3
  100. package/src/license/commands.js +12 -2
  101. package/src/license/machine-id.js +42 -1
  102. package/src/memory/gotchas.js +58 -40
  103. package/src/memory/magic-docs.js +1 -1
  104. package/src/memory/session-digest.js +9 -4
  105. package/src/orchestrator/cli.js +186 -15
  106. package/src/orchestrator/doctor.js +98 -16
  107. package/src/orchestrator/pipeline-manager.js +44 -22
  108. package/src/orchestrator/session-manager.js +64 -8
  109. package/src/telemetry/sender.js +7 -19
  110. package/src/terminal/run-team.js +3 -3
  111. package/src/upgrade/tracked-files-detector.js +34 -16
  112. package/src/utils/feature-flags.js +1 -1
  113. package/src/utils/flatten-entities.js +4 -40
  114. package/src/utils/schema-validator.js +0 -14
  115. package/src/wizard/i18n.js +3 -3
  116. package/src/api/index.js +0 -120
  117. package/src/autonomy/autonomous-gate.js +0 -294
  118. package/src/autonomy/build-loop.js +0 -281
  119. package/src/autonomy/build-state.js +0 -286
  120. package/src/autonomy/cause-analyzer.js +0 -177
  121. package/src/autonomy/escalation.js +0 -214
  122. package/src/autonomy/index.js +0 -51
  123. package/src/autonomy/mode-manager.js +0 -225
  124. package/src/autonomy/mode-suggester.js +0 -283
  125. package/src/autonomy/progress-reporter.js +0 -275
  126. package/src/autonomy/safety-net.js +0 -370
  127. package/src/config/agent-customizer.js +0 -231
  128. package/src/decision/analyzer.js +0 -291
  129. package/src/decision/engine.js +0 -250
  130. package/src/decision/index.js +0 -38
  131. package/src/decision/registry-healer.js +0 -468
  132. package/src/decision/registry-updater.js +0 -339
  133. package/src/extensions/loader.js +0 -145
  134. package/src/extensions/registry.js +0 -134
  135. package/src/gates/circuit-breaker.js +0 -151
  136. package/src/gates/g1-planning-complete.js +0 -154
  137. package/src/gates/g2-qa-planning.js +0 -156
  138. package/src/gates/g3-implementation.js +0 -215
  139. package/src/gates/g4-qa-implementation.js +0 -240
  140. package/src/gates/g5-deploy-ready.js +0 -181
  141. package/src/gates/gate-base.js +0 -185
  142. package/src/gates/index.js +0 -46
  143. package/src/health/auto-fix.js +0 -216
  144. package/src/health/engine.js +0 -246
  145. package/src/merger/semantic-merger.js +0 -292
  146. package/src/preview/detector.js +0 -238
  147. package/src/preview/index.js +0 -20
  148. package/src/preview/launcher.js +0 -235
  149. package/src/preview/log-buffer.js +0 -103
  150. package/src/quality/metrics-collector.js +0 -281
  151. package/src/quality/test-runner.js +0 -366
  152. package/src/tasks/executor.js +0 -195
  153. package/src/tasks/index.js +0 -4
  154. package/src/tasks/loader.js +0 -210
  155. package/src/tasks/router.js +0 -182
  156. package/src/utils/event-bus.js +0 -126
@@ -0,0 +1,261 @@
1
+ /**
2
+ * components/primitives/Button.tsx -- saas-dashboard preset v1.0.0
3
+ *
4
+ * WHY THIS IS A PRIMITIVE
5
+ * -----------------------
6
+ * Every interactive affordance in the UI eventually traces back to a
7
+ * button: form submission, navigation, dialog open, action trigger.
8
+ * If each feature builds its own button-shaped element, subtle
9
+ * inconsistencies accumulate: slightly different padding here, missing
10
+ * active state there, focus ring forgotten on a ghost variant.
11
+ *
12
+ * The Button primitive is the shared language. All interactive affordances
13
+ * that are not links inside prose use this component. Features that need
14
+ * a custom shape (icon-only circular button) extend this via the 'icon'
15
+ * size variant.
16
+ *
17
+ * STATE MODEL
18
+ * -----------
19
+ * Every variant has exactly 5 states:
20
+ * resting: default appearance
21
+ * hover: one perceptible property change (background, border, or shadow)
22
+ * active: scale(0.97) + slightly darker background -- physical press feel
23
+ * disabled: opacity-50 + pointer-events:none + aria-disabled
24
+ * loading: spinner replaces iconLeft, text persists, min-width locked
25
+ * (focus is handled globally via :focus-visible in globals.css)
26
+ *
27
+ * TRANSITION RULES (enforced here, no exceptions)
28
+ * ------------------------------------------------
29
+ * - Never use `transition-all` or `transition: all`.
30
+ * - Each transition names exactly the property being transitioned.
31
+ * - Duration tokens from --duration-*; easing from --ease-*.
32
+ * - Active scale: --duration-instant (80ms) --ease-spring for tactile feel.
33
+ * - Background/color changes: --duration-normal (200ms) --ease-in-out.
34
+ *
35
+ * VARIANT PHILOSOPHY
36
+ * ------------------
37
+ * primary: High-emphasis CTA. Accent background. One per view, maximum.
38
+ * secondary: Medium-emphasis. Raised surface with border. Multiple OK.
39
+ * ghost: Low-emphasis. Transparent. Used for icon buttons, toolbars.
40
+ * destructive: For irreversible actions. Danger tint; text danger color.
41
+ * link: Prose-inline or navigation. No background, no border.
42
+ *
43
+ * SIZE PHILOSOPHY
44
+ * ---------------
45
+ * sm: 32px height. Tight contexts: table action cells, compact toolbars.
46
+ * md: 40px height. Default. Most UI buttons.
47
+ * lg: 48px height. Hero CTAs, prominent forms.
48
+ * icon: Square. Width = height of the selected size. Icon-only buttons.
49
+ * MUST include a <span className="sr-only">label</span> child for
50
+ * screen reader accessibility.
51
+ *
52
+ * HREF PROP
53
+ * ---------
54
+ * When href is provided, the component renders as a Next.js <Link>
55
+ * instead of a <button>. All variants are supported. The semantics
56
+ * are correct: navigation actions should be anchors, not buttons.
57
+ *
58
+ * LOADING STATE
59
+ * -------------
60
+ * The loading prop:
61
+ * 1. Replaces iconLeft with a Spinner component.
62
+ * 2. Adds aria-busy="true" and aria-disabled="true".
63
+ * 3. Adds a min-width equal to the button's natural width to prevent
64
+ * the button from collapsing when text width changes. This is
65
+ * implemented via inline style measuring the rendered width on mount.
66
+ * Simpler approach: the parent should give the button a fixed width
67
+ * when the loading state is predictable (form submit buttons).
68
+ */
69
+
70
+ import { type ButtonHTMLAttributes, type ReactNode, forwardRef } from 'react';
71
+ import Link from 'next/link';
72
+ import { Spinner } from './Spinner';
73
+
74
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
75
+ /** Visual style variant */
76
+ variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'link';
77
+ /** Height, padding, and font-size tier */
78
+ size?: 'sm' | 'md' | 'lg' | 'icon';
79
+ /** Replaces iconLeft with a spinner; keeps width stable via min-width */
80
+ loading?: boolean;
81
+ /** Lucide icon rendered before children */
82
+ iconLeft?: ReactNode;
83
+ /** Lucide icon rendered after children */
84
+ iconRight?: ReactNode;
85
+ /** Stretches to container width */
86
+ fullWidth?: boolean;
87
+ /** Renders as Next.js Link instead of button */
88
+ href?: string;
89
+ /** External link target (only relevant with href) */
90
+ target?: string;
91
+ }
92
+
93
+ /* ----------------------------------------------------------------
94
+ CLASS MAP
95
+ All Tailwind classes are defined in this map, not inline in JSX.
96
+ This makes the variant contract explicit and auditable.
97
+ ---------------------------------------------------------------- */
98
+
99
+ const BASE = [
100
+ 'relative inline-flex items-center justify-center gap-2',
101
+ 'rounded-[var(--btn-radius)] font-medium',
102
+ 'select-none whitespace-nowrap',
103
+ 'transition-[background-color,box-shadow,transform,border-color,opacity]',
104
+ '[transition-duration:var(--duration-normal)]',
105
+ '[transition-timing-function:var(--ease-in-out)]',
106
+ 'focus-visible:outline-none focus-visible:ring-2',
107
+ 'focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2',
108
+ 'focus-visible:ring-offset-[var(--color-bg-base)]',
109
+ 'disabled:pointer-events-none disabled:opacity-50',
110
+ 'aria-disabled:pointer-events-none aria-disabled:opacity-50',
111
+ 'active:[transition-duration:var(--duration-instant)]',
112
+ 'active:[transition-timing-function:var(--ease-spring)]',
113
+ 'active:scale-[0.97]',
114
+ ].join(' ');
115
+
116
+ const VARIANT_CLASSES: Record<NonNullable<ButtonProps['variant']>, string> = {
117
+ primary: [
118
+ 'bg-[var(--color-accent)] text-white',
119
+ 'shadow-[var(--shadow-1)]',
120
+ 'hover:bg-[var(--color-accent-hover)]',
121
+ 'hover:shadow-[var(--shadow-2)]',
122
+ 'active:bg-[var(--color-accent-active)]',
123
+ ].join(' '),
124
+
125
+ secondary: [
126
+ 'bg-[var(--color-bg-raised)] text-[var(--color-text-primary)]',
127
+ 'border border-[var(--color-border-default)]',
128
+ 'shadow-[var(--shadow-1)]',
129
+ 'hover:border-[var(--color-border-emphasis)]',
130
+ 'hover:bg-[var(--color-bg-raised-hover)]',
131
+ 'active:bg-[var(--color-bg-raised-active)]',
132
+ ].join(' '),
133
+
134
+ ghost: [
135
+ 'bg-transparent text-[var(--color-text-secondary)]',
136
+ 'hover:bg-[var(--color-bg-highlight)] hover:text-[var(--color-text-primary)]',
137
+ 'active:bg-[var(--color-accent-subtle)]',
138
+ ].join(' '),
139
+
140
+ destructive: [
141
+ 'bg-[var(--color-danger-subtle)] text-[var(--color-danger)]',
142
+ 'border border-[var(--color-danger-subtle)]',
143
+ 'hover:bg-[var(--color-danger-hover-bg)] hover:border-[var(--color-danger)]',
144
+ 'active:bg-[var(--color-danger-active-bg)]',
145
+ ].join(' '),
146
+
147
+ link: [
148
+ 'bg-transparent text-[var(--color-text-accent)]',
149
+ 'underline-offset-4 hover:underline',
150
+ 'active:opacity-70',
151
+ 'h-auto! px-0! py-0!',
152
+ ].join(' '),
153
+ };
154
+
155
+ const SIZE_CLASSES: Record<NonNullable<ButtonProps['size']>, string> = {
156
+ sm: 'h-[var(--btn-h-sm)] px-[var(--btn-px-sm)] text-[var(--type-small)]',
157
+ md: 'h-[var(--btn-h-md)] px-[var(--btn-px-md)] text-[var(--type-small)]',
158
+ lg: 'h-[var(--btn-h-lg)] px-[var(--btn-px-lg)] text-[var(--type-body)]',
159
+ icon: 'h-[var(--btn-h-md)] w-[var(--btn-h-md)] px-0',
160
+ };
161
+
162
+ const SIZE_ICON_SIZE: Record<NonNullable<ButtonProps['size']>, number> = {
163
+ sm: 14,
164
+ md: 16,
165
+ lg: 18,
166
+ icon: 16,
167
+ };
168
+
169
+ /* ----------------------------------------------------------------
170
+ COMPONENT
171
+ ---------------------------------------------------------------- */
172
+
173
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
174
+ (
175
+ {
176
+ variant = 'primary',
177
+ size = 'md',
178
+ loading = false,
179
+ iconLeft,
180
+ iconRight,
181
+ fullWidth = false,
182
+ href,
183
+ target,
184
+ disabled,
185
+ children,
186
+ className,
187
+ ...rest
188
+ },
189
+ ref,
190
+ ) => {
191
+ const isDisabled = disabled || loading;
192
+ const iconSize = SIZE_ICON_SIZE[size];
193
+
194
+ const classes = [
195
+ BASE,
196
+ VARIANT_CLASSES[variant],
197
+ SIZE_CLASSES[size],
198
+ fullWidth ? 'w-full' : '',
199
+ className,
200
+ ]
201
+ .filter(Boolean)
202
+ .join(' ');
203
+
204
+ const content = (
205
+ <>
206
+ {/* Loading spinner replaces iconLeft, preserving layout */}
207
+ {loading ? (
208
+ <Spinner size={size === 'lg' ? 'md' : 'sm'} aria-hidden="true" />
209
+ ) : iconLeft ? (
210
+ <span aria-hidden="true" className="shrink-0">
211
+ {iconLeft}
212
+ </span>
213
+ ) : null}
214
+
215
+ {/* Children: hidden from SR during loading for clean state */}
216
+ {children && (
217
+ <span className={loading ? 'opacity-0 select-none' : undefined}>
218
+ {children}
219
+ </span>
220
+ )}
221
+
222
+ {iconRight && !loading && (
223
+ <span aria-hidden="true" className="shrink-0">
224
+ {iconRight}
225
+ </span>
226
+ )}
227
+ </>
228
+ );
229
+
230
+ /* Render as Next.js Link for navigation actions */
231
+ if (href) {
232
+ return (
233
+ <Link
234
+ href={href}
235
+ target={target}
236
+ className={classes}
237
+ aria-disabled={isDisabled || undefined}
238
+ tabIndex={isDisabled ? -1 : undefined}
239
+ >
240
+ {content}
241
+ </Link>
242
+ );
243
+ }
244
+
245
+ return (
246
+ <button
247
+ ref={ref}
248
+ type="button"
249
+ disabled={isDisabled}
250
+ aria-disabled={isDisabled || undefined}
251
+ aria-busy={loading || undefined}
252
+ className={classes}
253
+ {...rest}
254
+ >
255
+ {content}
256
+ </button>
257
+ );
258
+ },
259
+ );
260
+
261
+ Button.displayName = 'Button';
@@ -0,0 +1,287 @@
1
+ /**
2
+ * components/primitives/Card.tsx -- saas-dashboard preset v1.0.0
3
+ *
4
+ * WHY A COMPOUND COMPONENT
5
+ * ------------------------
6
+ * A single <Card> component with a content slot is flexible but
7
+ * loses structural meaning: every usage would re-invent its own
8
+ * header/footer layout, producing inconsistent internal spacing.
9
+ *
10
+ * The compound component pattern (<Card>, <CardHeader>, <CardTitle>,
11
+ * etc.) gives consumers a shared grammar:
12
+ * - CardHeader has a defined height, flex-row layout, and icon slot
13
+ * - CardTitle and CardDescription have defined type scale tokens
14
+ * - CardContent handles the padding relationship with CardHeader
15
+ * - CardFooter has a border-top and consistent action layout
16
+ *
17
+ * This means QA can verify the card pattern in one place, and any
18
+ * change to spacing or typography propagates to every usage.
19
+ *
20
+ * ELEVATION SYSTEM
21
+ * ----------------
22
+ * Elevation 1 (default): --shadow-1
23
+ * Use for: standard cards, stat cards, panels.
24
+ * The inset top border simulates a light source from above.
25
+ *
26
+ * Elevation 2: --shadow-2
27
+ * Use for: dropdowns, popovers, hover-elevated cards.
28
+ * More ambient shadow; adds a 1px outer ring.
29
+ *
30
+ * VARIANT MODIFIERS
31
+ * -----------------
32
+ * featured: 2px accent-color top border.
33
+ * Use for: primary plan in a pricing grid, pinned items.
34
+ * ONE featured card per visible group maximum.
35
+ *
36
+ * interactive: hover translateY(-2px) + shadow-2.
37
+ * Use for: clickable resource cards, navigation tiles.
38
+ * Add onRowClick or an onClick handler alongside this prop.
39
+ *
40
+ * noPadding: Removes CardContent internal padding.
41
+ * Use for: DataTable, full-bleed images, embedded charts.
42
+ * Sub-components are still padded unless also given noPadding.
43
+ *
44
+ * stretch: height: 100% for use inside flex/grid containers.
45
+ *
46
+ * TRANSITION RULES
47
+ * ----------------
48
+ * Interactive card hover:
49
+ * transform: translateY(-2px) -- duration-normal ease-out (entering)
50
+ * box-shadow: from shadow-1 to shadow-2 -- duration-normal ease-out
51
+ * Hover exit:
52
+ * transform: translateY(0) -- duration-normal ease-in
53
+ * box-shadow: back to shadow-1 -- duration-normal ease-in
54
+ * Both transitions name exactly the properties being transitioned.
55
+ */
56
+
57
+ import React, { type HTMLAttributes, forwardRef } from 'react';
58
+
59
+ /* ----------------------------------------------------------------
60
+ CARD ROOT
61
+ ---------------------------------------------------------------- */
62
+
63
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
64
+ /** Shadow elevation level */
65
+ elevation?: 1 | 2;
66
+ /** Adds 2px accent top border for featured/primary cards */
67
+ featured?: boolean;
68
+ /** Removes internal padding for full-bleed content */
69
+ noPadding?: boolean;
70
+ /** Adds hover lift (translateY -2px, shadow-2) for clickable cards */
71
+ interactive?: boolean;
72
+ /** Fills height of flex/grid container */
73
+ stretch?: boolean;
74
+ }
75
+
76
+ export const Card = forwardRef<HTMLDivElement, CardProps>(
77
+ (
78
+ {
79
+ elevation = 1,
80
+ featured = false,
81
+ noPadding = false,
82
+ interactive = false,
83
+ stretch = false,
84
+ children,
85
+ className,
86
+ style,
87
+ ...rest
88
+ },
89
+ ref,
90
+ ) => {
91
+ const shadowVar =
92
+ elevation === 2
93
+ ? 'var(--shadow-2)'
94
+ : 'var(--shadow-1)';
95
+
96
+ const classes = [
97
+ /* Surface */
98
+ 'relative rounded-[var(--radius-lg)] overflow-hidden',
99
+ 'bg-[var(--color-bg-raised)]',
100
+ 'border border-[var(--color-border-default)]',
101
+ /* Featured: accent border replaces top edge */
102
+ featured
103
+ ? 'border-t-2 border-t-[var(--color-accent)]'
104
+ : '',
105
+ /* Padding */
106
+ noPadding ? '' : 'p-[var(--spacing-card-pad)]',
107
+ /* Interactive hover */
108
+ interactive
109
+ ? [
110
+ 'cursor-pointer',
111
+ 'transition-[transform,box-shadow]',
112
+ '[transition-duration:var(--duration-normal)]',
113
+ '[transition-timing-function:var(--ease-out)]',
114
+ 'hover:-translate-y-0.5',
115
+ 'hover:shadow-[var(--shadow-2)]',
116
+ 'active:translate-y-0 active:shadow-[var(--shadow-1)]',
117
+ 'active:[transition-timing-function:var(--ease-in)]',
118
+ 'active:[transition-duration:var(--duration-instant)]',
119
+ ].join(' ')
120
+ : '',
121
+ /* Stretch */
122
+ stretch ? 'h-full' : '',
123
+ className,
124
+ ]
125
+ .filter(Boolean)
126
+ .join(' ');
127
+
128
+ return (
129
+ <div
130
+ ref={ref}
131
+ style={{ boxShadow: shadowVar, ...style }}
132
+ className={classes}
133
+ {...rest}
134
+ >
135
+ {children}
136
+ </div>
137
+ );
138
+ },
139
+ );
140
+
141
+ Card.displayName = 'Card';
142
+
143
+ /* ----------------------------------------------------------------
144
+ CARD HEADER
145
+ flex row, space-between, consistent vertical padding.
146
+ ---------------------------------------------------------------- */
147
+
148
+ export interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {}
149
+
150
+ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
151
+ ({ children, className, ...rest }, ref) => (
152
+ <div
153
+ ref={ref}
154
+ className={[
155
+ 'flex flex-row items-center justify-between',
156
+ 'px-[var(--spacing-card-pad)] pt-[var(--spacing-card-pad)]',
157
+ 'pb-[var(--space-4)]',
158
+ className,
159
+ ]
160
+ .filter(Boolean)
161
+ .join(' ')}
162
+ {...rest}
163
+ >
164
+ {children}
165
+ </div>
166
+ ),
167
+ );
168
+
169
+ CardHeader.displayName = 'CardHeader';
170
+
171
+ /* ----------------------------------------------------------------
172
+ CARD TITLE
173
+ h4-level type scale; semibold; primary text color.
174
+ ---------------------------------------------------------------- */
175
+
176
+ export interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
177
+ /** Heading level -- defaults to h3 for document outline. Override
178
+ * only when the card is inside an existing h2 section. */
179
+ as?: 'h2' | 'h3' | 'h4';
180
+ }
181
+
182
+ export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
183
+ ({ as: Tag = 'h3', children, className, ...rest }, ref) => (
184
+ <Tag
185
+ ref={ref as React.Ref<HTMLHeadingElement>}
186
+ className={[
187
+ 'text-[var(--type-h4)] leading-[var(--type-h4-lh)]',
188
+ 'tracking-[var(--type-h4-ls)]',
189
+ 'font-[var(--font-weight-semibold)]',
190
+ 'text-[var(--color-text-primary)]',
191
+ className,
192
+ ]
193
+ .filter(Boolean)
194
+ .join(' ')}
195
+ {...rest}
196
+ >
197
+ {children}
198
+ </Tag>
199
+ ),
200
+ );
201
+
202
+ CardTitle.displayName = 'CardTitle';
203
+
204
+ /* ----------------------------------------------------------------
205
+ CARD DESCRIPTION
206
+ small type scale; secondary text color.
207
+ ---------------------------------------------------------------- */
208
+
209
+ export interface CardDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {}
210
+
211
+ export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
212
+ ({ children, className, ...rest }, ref) => (
213
+ <p
214
+ ref={ref}
215
+ className={[
216
+ 'text-[var(--type-small)] leading-[var(--type-small-lh)]',
217
+ 'text-[var(--color-text-secondary)]',
218
+ 'mt-[var(--space-1)]',
219
+ className,
220
+ ]
221
+ .filter(Boolean)
222
+ .join(' ')}
223
+ {...rest}
224
+ >
225
+ {children}
226
+ </p>
227
+ ),
228
+ );
229
+
230
+ CardDescription.displayName = 'CardDescription';
231
+
232
+ /* ----------------------------------------------------------------
233
+ CARD CONTENT
234
+ Removes top padding when it follows a CardHeader.
235
+ ---------------------------------------------------------------- */
236
+
237
+ export interface CardContentProps extends HTMLAttributes<HTMLDivElement> {}
238
+
239
+ export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
240
+ ({ children, className, ...rest }, ref) => (
241
+ <div
242
+ ref={ref}
243
+ className={[
244
+ 'px-[var(--spacing-card-pad)] pb-[var(--spacing-card-pad)]',
245
+ /* Remove top padding: CardHeader already provides bottom padding */
246
+ '[.card-has-header_+_&]:pt-0 pt-[var(--spacing-card-pad)]',
247
+ className,
248
+ ]
249
+ .filter(Boolean)
250
+ .join(' ')}
251
+ {...rest}
252
+ >
253
+ {children}
254
+ </div>
255
+ ),
256
+ );
257
+
258
+ CardContent.displayName = 'CardContent';
259
+
260
+ /* ----------------------------------------------------------------
261
+ CARD FOOTER
262
+ Border-top divider; flex row for action buttons.
263
+ ---------------------------------------------------------------- */
264
+
265
+ export interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {}
266
+
267
+ export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
268
+ ({ children, className, ...rest }, ref) => (
269
+ <div
270
+ ref={ref}
271
+ className={[
272
+ 'flex flex-row items-center gap-[var(--space-2)]',
273
+ 'px-[var(--spacing-card-pad)] py-[var(--space-4)]',
274
+ 'border-t border-[var(--color-border-default)]',
275
+ className,
276
+ ]
277
+ .filter(Boolean)
278
+ .join(' ')}
279
+ {...rest}
280
+ >
281
+ {children}
282
+ </div>
283
+ ),
284
+ );
285
+
286
+ CardFooter.displayName = 'CardFooter';
287
+