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,310 @@
1
+ /**
2
+ * components/primitives/Input.tsx -- saas-dashboard preset v1.0.0
3
+ *
4
+ * WHY FIELD WRAPS INPUT
5
+ * ---------------------
6
+ * A bare <input> has no label, no hint, no error affordance, and no
7
+ * icon slots. Developers who use bare inputs inevitably re-invent
8
+ * these around them, producing inconsistent patterns: one uses a
9
+ * <div> with a floating label, another uses a placeholder, a third
10
+ * uses a <p> beneath with a class that varies per form.
11
+ *
12
+ * The Field primitive is the contract: an accessible input group
13
+ * that ALWAYS has a real <label> element, MAY have a hint, and
14
+ * COMMUNICATES errors accessibly (role=alert, aria-invalid).
15
+ *
16
+ * PLACEHOLDER RULE (non-negotiable)
17
+ * ----------------------------------
18
+ * Placeholder text is NOT a label. It disappears when the user types,
19
+ * is not read by all screen readers in all modes, and fails WCAG
20
+ * 1.3.1 (Info and Relationships). The Field primitive structurally
21
+ * enforces this by requiring the `label` prop and rendering it as a
22
+ * real <label> element.
23
+ *
24
+ * If the visual design calls for a label-free input (search box in
25
+ * a toolbar), pass `labelHidden` to visually hide the label while
26
+ * keeping it in the DOM for screen readers.
27
+ *
28
+ * STATE MODEL
29
+ * -----------
30
+ * resting: border-default background
31
+ * focus: border-emphasis + shadow-focus ring (2px, 2px offset)
32
+ * error: border-danger + error message with role=alert
33
+ * disabled: opacity-50 + cursor-not-allowed on the input
34
+ * (focus-visible ring is also applied via globals.css :focus-visible
35
+ * on the input element itself for keyboard navigation)
36
+ *
37
+ * ICON SLOTS
38
+ * ----------
39
+ * Icons are absolutely positioned inside a relative wrapper.
40
+ * The input's padding-left/padding-right is extended via CSS
41
+ * custom property --input-pl when iconLeft is present (36px).
42
+ * This avoids the icon overlapping text on very short inputs.
43
+ *
44
+ * TRANSITION RULES
45
+ * ----------------
46
+ * Border colour change on focus: --duration-normal, --ease-in-out.
47
+ * Only `border-color` and `box-shadow` are transitioned.
48
+ * Never transition `padding`, `width`, or `height`.
49
+ *
50
+ * TEXTAREA AND SELECT VARIANTS
51
+ * ----------------------------
52
+ * The `as` prop renders a <textarea> or <select> instead of <input>.
53
+ * Both variants share the same visual treatment as the text input.
54
+ * Textarea has a min-height and resizes vertically only (resize:vertical).
55
+ */
56
+
57
+ import {
58
+ type InputHTMLAttributes,
59
+ type TextareaHTMLAttributes,
60
+ type SelectHTMLAttributes,
61
+ type ReactNode,
62
+ forwardRef,
63
+ useId,
64
+ } from 'react';
65
+
66
+ /* ----------------------------------------------------------------
67
+ FIELD
68
+ ---------------------------------------------------------------- */
69
+
70
+ export interface FieldProps {
71
+ /** REQUIRED: always rendered as <label>, never omitted */
72
+ label: string;
73
+ /** Hint text below input, linked via aria-describedby */
74
+ hint?: string;
75
+ /** Error message: overrides hint, sets aria-invalid on input */
76
+ error?: string;
77
+ /** Visually hides label but keeps it for screen readers */
78
+ labelHidden?: boolean;
79
+ /** Lucide icon inside input, left side */
80
+ iconLeft?: ReactNode;
81
+ /** Lucide icon or button inside input, right side */
82
+ iconRight?: ReactNode;
83
+ /** Adds asterisk to label and aria-required to input */
84
+ required?: boolean;
85
+ /** Input element type; textarea renders a <textarea> */
86
+ as?: 'input' | 'textarea' | 'select';
87
+ /** Passed to the underlying input element */
88
+ inputProps?: InputHTMLAttributes<HTMLInputElement> &
89
+ TextareaHTMLAttributes<HTMLTextAreaElement> &
90
+ SelectHTMLAttributes<HTMLSelectElement>;
91
+ /** Child elements for <select> variant (options) */
92
+ children?: ReactNode;
93
+ /** Additional class on the root Field wrapper */
94
+ className?: string;
95
+ }
96
+
97
+ export function Field({
98
+ label,
99
+ hint,
100
+ error,
101
+ labelHidden = false,
102
+ iconLeft,
103
+ iconRight,
104
+ required = false,
105
+ as: InputAs = 'input',
106
+ inputProps,
107
+ children,
108
+ className,
109
+ }: FieldProps) {
110
+ const id = useId();
111
+ const hintId = `${id}-hint`;
112
+ const errorId = `${id}-error`;
113
+ const hasError = Boolean(error);
114
+ const descBy = [hint ? hintId : '', hasError ? errorId : '']
115
+ .filter(Boolean)
116
+ .join(' ') || undefined;
117
+
118
+ /* ---- shared input classes ---- */
119
+ const inputClasses = [
120
+ 'w-full rounded-[var(--input-radius)]',
121
+ 'bg-[var(--color-bg-sunken)]',
122
+ 'text-[var(--type-body)] text-[var(--color-text-primary)]',
123
+ 'placeholder:text-[var(--color-text-muted)]',
124
+ 'border border-[var(--color-border-default)]',
125
+ 'py-[var(--spacing-input-pad-v)]',
126
+ /* Horizontal padding adjusts based on icon presence */
127
+ iconLeft
128
+ ? 'pl-10 pr-[var(--spacing-input-pad-h)]'
129
+ : iconRight
130
+ ? 'pl-[var(--spacing-input-pad-h)] pr-10'
131
+ : 'px-[var(--spacing-input-pad-h)]',
132
+ /* Transitions: only border-color and box-shadow */
133
+ 'transition-[border-color,box-shadow]',
134
+ '[transition-duration:var(--duration-normal)]',
135
+ '[transition-timing-function:var(--ease-in-out)]',
136
+ /* Focus state */
137
+ 'focus:outline-none',
138
+ 'focus:border-[var(--color-border-emphasis)]',
139
+ 'focus:shadow-[var(--shadow-focus)]',
140
+ /* Error state */
141
+ hasError
142
+ ? 'border-[var(--color-danger)] focus:border-[var(--color-danger)]'
143
+ : '',
144
+ /* Disabled state */
145
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
146
+ /* Textarea specific */
147
+ InputAs === 'textarea'
148
+ ? 'resize-vertical min-h-[var(--space-32)] leading-[var(--type-body-lh)]'
149
+ : 'h-[var(--input-h)]',
150
+ /* Select specific */
151
+ InputAs === 'select' ? 'cursor-pointer appearance-none' : '',
152
+ ]
153
+ .filter(Boolean)
154
+ .join(' ');
155
+
156
+ return (
157
+ <div className={['flex flex-col gap-[var(--space-2)]', className].filter(Boolean).join(' ')}>
158
+ {/* Label -- always present in DOM, never replaced by placeholder */}
159
+ <label
160
+ htmlFor={id}
161
+ className={[
162
+ 'text-[var(--type-small)] font-[var(--font-weight-medium)]',
163
+ 'text-[var(--color-text-primary)]',
164
+ 'leading-[var(--type-small-lh)]',
165
+ labelHidden ? 'sr-only' : '',
166
+ ]
167
+ .filter(Boolean)
168
+ .join(' ')}
169
+ >
170
+ {label}
171
+ {required && (
172
+ <span
173
+ aria-hidden="true"
174
+ className="ml-[var(--space-1)] text-[var(--color-danger)]"
175
+ >
176
+ *
177
+ </span>
178
+ )}
179
+ </label>
180
+
181
+ {/* Input wrapper: relative for icon absolute positioning */}
182
+ <div className="relative">
183
+ {iconLeft && (
184
+ <span
185
+ aria-hidden="true"
186
+ className={[
187
+ 'absolute left-[var(--space-3)] top-1/2 -translate-y-1/2',
188
+ 'text-[var(--color-text-muted)] pointer-events-none',
189
+ 'flex items-center justify-center w-4 h-4',
190
+ ].join(' ')}
191
+ >
192
+ {iconLeft}
193
+ </span>
194
+ )}
195
+
196
+ {InputAs === 'textarea' ? (
197
+ <textarea
198
+ id={id}
199
+ aria-invalid={hasError || undefined}
200
+ aria-describedby={descBy}
201
+ aria-required={required || undefined}
202
+ className={inputClasses}
203
+ {...(inputProps as TextareaHTMLAttributes<HTMLTextAreaElement>)}
204
+ />
205
+ ) : InputAs === 'select' ? (
206
+ <select
207
+ id={id}
208
+ aria-invalid={hasError || undefined}
209
+ aria-describedby={descBy}
210
+ aria-required={required || undefined}
211
+ className={inputClasses}
212
+ {...(inputProps as SelectHTMLAttributes<HTMLSelectElement>)}
213
+ >
214
+ {children}
215
+ </select>
216
+ ) : (
217
+ <input
218
+ id={id}
219
+ aria-invalid={hasError || undefined}
220
+ aria-describedby={descBy}
221
+ aria-required={required || undefined}
222
+ className={inputClasses}
223
+ {...(inputProps as InputHTMLAttributes<HTMLInputElement>)}
224
+ />
225
+ )}
226
+
227
+ {iconRight && (
228
+ <span
229
+ aria-hidden="true"
230
+ className={[
231
+ 'absolute right-[var(--space-3)] top-1/2 -translate-y-1/2',
232
+ 'text-[var(--color-text-muted)]',
233
+ 'flex items-center justify-center w-4 h-4',
234
+ ].join(' ')}
235
+ >
236
+ {iconRight}
237
+ </span>
238
+ )}
239
+ </div>
240
+
241
+ {/* Error message: role=alert ensures SR announces immediately */}
242
+ {hasError && (
243
+ <p
244
+ id={errorId}
245
+ role="alert"
246
+ className={[
247
+ 'text-[var(--type-caption)] leading-[var(--type-caption-lh)]',
248
+ 'text-[var(--color-danger)]',
249
+ ].join(' ')}
250
+ >
251
+ {error}
252
+ </p>
253
+ )}
254
+
255
+ {/* Hint text: only shown when no error (error takes priority) */}
256
+ {hint && !hasError && (
257
+ <p
258
+ id={hintId}
259
+ className={[
260
+ 'text-[var(--type-caption)] leading-[var(--type-caption-lh)]',
261
+ 'text-[var(--color-text-muted)]',
262
+ ].join(' ')}
263
+ >
264
+ {hint}
265
+ </p>
266
+ )}
267
+ </div>
268
+ );
269
+ }
270
+
271
+ /* ----------------------------------------------------------------
272
+ INPUT (bare, for use outside Field context when Field is overkill)
273
+ Forwards a ref for form library integration (react-hook-form).
274
+ ---------------------------------------------------------------- */
275
+
276
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
277
+ /** Applies error styling without full Field wrapper */
278
+ hasError?: boolean;
279
+ }
280
+
281
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
282
+ ({ hasError, className, ...rest }, ref) => (
283
+ <input
284
+ ref={ref}
285
+ aria-invalid={hasError || undefined}
286
+ className={[
287
+ 'w-full h-[var(--input-h)] rounded-[var(--input-radius)]',
288
+ 'bg-[var(--color-bg-sunken)]',
289
+ 'text-[var(--type-body)] text-[var(--color-text-primary)]',
290
+ 'placeholder:text-[var(--color-text-muted)]',
291
+ 'border border-[var(--color-border-default)]',
292
+ 'px-[var(--spacing-input-pad-h)]',
293
+ 'transition-[border-color,box-shadow]',
294
+ '[transition-duration:var(--duration-normal)]',
295
+ '[transition-timing-function:var(--ease-in-out)]',
296
+ 'focus:outline-none',
297
+ 'focus:border-[var(--color-border-emphasis)]',
298
+ 'focus:shadow-[var(--shadow-focus)]',
299
+ hasError ? 'border-[var(--color-danger)]' : '',
300
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
301
+ className,
302
+ ]
303
+ .filter(Boolean)
304
+ .join(' ')}
305
+ {...rest}
306
+ />
307
+ ),
308
+ );
309
+
310
+ Input.displayName = 'Input';
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ /**
3
+ * components/primitives/Spinner.tsx -- saas-dashboard preset v1.0.0
4
+ *
5
+ * WHY SVG AND NOT A BORDER-BASED SPINNER
6
+ * ---------------------------------------
7
+ * CSS border-based spinners (border-t-transparent trick) are common but
8
+ * have two problems in a design-system context:
9
+ * 1. They rely on a border on the element itself, which conflicts with
10
+ * Button's own border (secondary/destructive variants).
11
+ * 2. They cannot easily inherit the parent's text color at different
12
+ * opacities for the track vs. indicator.
13
+ *
14
+ * An SVG circle with stroke-dasharray/stroke-dashoffset animation:
15
+ * - Is fully isolated from the surrounding layout
16
+ * - Inherits currentColor for both track and indicator (different opacities)
17
+ * - Renders at exactly the declared size (no border-box inconsistencies)
18
+ * - Is composable: Spinner renders inline, needs no wrapper
19
+ *
20
+ * REDUCED MOTION
21
+ * --------------
22
+ * When prefers-reduced-motion is active, the rotating animation is
23
+ * replaced with a gentle opacity pulse. This preserves the loading
24
+ * feedback without vestibular-triggering rotation.
25
+ * The globals.css @media block collapses animation-duration to 0.01ms
26
+ * which effectively freezes the spinner. We supplement with a CSS
27
+ * class that triggers the static pulse fallback.
28
+ *
29
+ * SIZE SYSTEM
30
+ * -----------
31
+ * sm: 16px -- inside sm/md Button, inline with body text
32
+ * md: 20px -- inside lg Button, standalone inline indicators
33
+ * lg: 32px -- full-page Suspense boundaries, large empty states
34
+ *
35
+ * COLOR
36
+ * -----
37
+ * The spinner inherits currentColor from its parent. Inside a
38
+ * primary Button (white text) it renders white. Inside a secondary
39
+ * Button (text-primary) it renders text-primary. No explicit color
40
+ * prop needed; the variant system handles this via text color.
41
+ */
42
+
43
+ const SIZES: Record<string, number> = {
44
+ sm: 16,
45
+ md: 20,
46
+ lg: 32,
47
+ };
48
+
49
+ const STROKE_WIDTH: Record<string, number> = {
50
+ sm: 2,
51
+ md: 2,
52
+ lg: 2.5,
53
+ };
54
+
55
+ export interface SpinnerProps {
56
+ /** Visual size */
57
+ size?: 'sm' | 'md' | 'lg';
58
+ /** Additional class names */
59
+ className?: string;
60
+ /** Aria label for standalone loading indicators */
61
+ 'aria-label'?: string;
62
+ /** Explicitly suppress aria for decorative usage inside Button */
63
+ 'aria-hidden'?: boolean | 'true' | 'false';
64
+ }
65
+
66
+ export function Spinner({
67
+ size = 'md',
68
+ className,
69
+ 'aria-label': ariaLabel,
70
+ 'aria-hidden': ariaHidden,
71
+ }: SpinnerProps) {
72
+ const px = SIZES[size] ?? SIZES.md;
73
+ const stroke = STROKE_WIDTH[size] ?? 2;
74
+ const r = (px - stroke * 2) / 2;
75
+ const circumference = 2 * Math.PI * r;
76
+ /* Show ~25% of circumference as the visible arc */
77
+ const dasharray = `${circumference * 0.25} ${circumference * 0.75}`;
78
+
79
+ return (
80
+ <svg
81
+ width={px}
82
+ height={px}
83
+ viewBox={`0 0 ${px} ${px}`}
84
+ fill="none"
85
+ aria-hidden={ariaHidden}
86
+ aria-label={ariaLabel}
87
+ role={ariaLabel ? 'status' : undefined}
88
+ className={['shrink-0', 'spinner-svg', className].filter(Boolean).join(' ')}
89
+ style={
90
+ {
91
+ /* Rotation animation: composited on GPU (transform only) */
92
+ animation:
93
+ 'spinner-rotate 0.75s linear infinite',
94
+ } as React.CSSProperties
95
+ }
96
+ >
97
+ <style>{`
98
+ @keyframes spinner-rotate {
99
+ from { transform: rotate(0deg); transform-origin: center; }
100
+ to { transform: rotate(360deg); transform-origin: center; }
101
+ }
102
+ @keyframes spinner-pulse {
103
+ 0%, 100% { opacity: 1; }
104
+ 50% { opacity: 0.3; }
105
+ }
106
+ @media (prefers-reduced-motion: reduce) {
107
+ .spinner-svg { animation: spinner-pulse 1.4s ease-in-out infinite !important; }
108
+ }
109
+ `}</style>
110
+
111
+ {/* Track: full circle at low opacity for visual context */}
112
+ <circle
113
+ cx={px / 2}
114
+ cy={px / 2}
115
+ r={r}
116
+ stroke="currentColor"
117
+ strokeWidth={stroke}
118
+ opacity={0.15}
119
+ />
120
+
121
+ {/* Indicator: 25% arc, full opacity */}
122
+ <circle
123
+ cx={px / 2}
124
+ cy={px / 2}
125
+ r={r}
126
+ stroke="currentColor"
127
+ strokeWidth={stroke}
128
+ strokeDasharray={dasharray}
129
+ strokeLinecap="round"
130
+ opacity={0.9}
131
+ />
132
+ </svg>
133
+ );
134
+ }
135
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * components/primitives/index.ts -- saas-dashboard preset v1.0.0
3
+ *
4
+ * WHY A BARREL EXPORT
5
+ * --------------------
6
+ * Without a barrel, consumers must know the exact file path for each
7
+ * primitive, leading to divergence:
8
+ * import { Button } from '@/components/primitives/Button'
9
+ * import { Card } from '@/components/primitives/card' // different casing
10
+ * import { Badge } from '../../primitives/Badge' // relative path
11
+ *
12
+ * The barrel enforces a single import path:
13
+ * import { Button, Card, CardHeader, Field, Badge } from '@/components/primitives'
14
+ *
15
+ * This means:
16
+ * 1. Renaming a primitive file only requires updating this file.
17
+ * 2. IDE autocomplete surfaces all available primitives from one location.
18
+ * 3. Tree-shaking works correctly -- unused primitives are eliminated.
19
+ *
20
+ * WHAT BELONGS HERE
21
+ * -----------------
22
+ * Only true primitives: components with no dependencies on other local
23
+ * components (only design tokens and external packages).
24
+ * Composite components (DataTable, StatCard, CommandPalette) have their
25
+ * own barrel exports in their respective directories.
26
+ */
27
+
28
+ /* Button + types */
29
+ export { Button } from './Button';
30
+ export type { ButtonProps } from './Button';
31
+
32
+ /* Card compound component + all sub-component types */
33
+ export {
34
+ Card,
35
+ CardHeader,
36
+ CardTitle,
37
+ CardDescription,
38
+ CardContent,
39
+ CardFooter,
40
+ } from './Card';
41
+ export type {
42
+ CardProps,
43
+ CardHeaderProps,
44
+ CardTitleProps,
45
+ CardDescriptionProps,
46
+ CardContentProps,
47
+ CardFooterProps,
48
+ } from './Card';
49
+
50
+ /* Input + Field + types */
51
+ export { Input, Field } from './Input';
52
+ export type { InputProps, FieldProps } from './Input';
53
+
54
+ /* Badge + types */
55
+ export { Badge } from './Badge';
56
+ export type { BadgeProps } from './Badge';
57
+
58
+ /* Spinner + types */
59
+ export { Spinner } from './Spinner';
60
+ export type { SpinnerProps } from './Spinner';
@@ -0,0 +1,174 @@
1
+ /*
2
+ * AppShell.tsx -- Application Shell Composition
3
+ * ============================================================
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * The app shell is the outermost spatial container for every
7
+ * authenticated page. It establishes the two-dimensional layout
8
+ * that all page content lives within:
9
+ *
10
+ * [sidebar column | main column]
11
+ * Sidebar Topbar (sticky)
12
+ * <main> (scrolls)
13
+ * Container
14
+ * {children}
15
+ *
16
+ * LAYOUT APPROACH: CSS GRID + CSS VARIABLE COLUMN WIDTH
17
+ * ------------------------------------------------------
18
+ * We use CSS Grid with a single-row, two-column layout. The
19
+ * sidebar column width is driven by a CSS custom property
20
+ * (--shell-sidebar-w) that is updated by SidebarContext when
21
+ * the user collapses or expands. This means:
22
+ *
23
+ * - The grid reacts to state changes without any JavaScript
24
+ * calculating pixel positions.
25
+ * - The transition is applied to grid-template-columns via a
26
+ * CSS transition. Modern browsers animate this correctly
27
+ * when the value is a length.
28
+ * - No absolute positioning or transform-based sliding is
29
+ * needed on the sidebar element itself for desktop layout.
30
+ *
31
+ * RESPONSIVE BEHAVIOR
32
+ * -------------------
33
+ * Three breakpoint tiers:
34
+ *
35
+ * Mobile (<768px):
36
+ * - Grid collapses to a single column (sidebar column = 0).
37
+ * - Sidebar is removed from flow (position: fixed, off-screen
38
+ * via translateX(-100%)).
39
+ * - A backdrop overlay appears when isMobileOpen is true.
40
+ * - The Topbar's hamburger button calls openMobile().
41
+ *
42
+ * Tablet (768px-1023px):
43
+ * - Grid sidebar column exists but defaults to collapsed
44
+ * width (64px). User can expand.
45
+ *
46
+ * Desktop (>=1024px):
47
+ * - Full sidebar visible; collapse is optional.
48
+ *
49
+ * FOCUS MANAGEMENT ON ROUTE CHANGE
50
+ * ----------------------------------
51
+ * When Next.js navigates to a new route, focus is moved to the
52
+ * <main> element via a ref. This ensures screen reader users
53
+ * hear the new page content without manually navigating there.
54
+ * The main element receives tabIndex={-1} so it can be focused
55
+ * programmatically without appearing in the natural tab order.
56
+ *
57
+ * Z-INDEX SCALE (defined in globals.css)
58
+ * ---------------------------------------
59
+ * --z-sidebar: 30 (desktop: in normal flow; mobile: fixed overlay)
60
+ * --z-topbar: 40
61
+ * --z-dropdown: 50
62
+ * --z-tooltip: 60
63
+ * --z-modal: 70
64
+ * --z-toast: 80
65
+ * --z-command: 90
66
+ *
67
+ * PERFORMANCE
68
+ * -----------
69
+ * We animate only the CSS custom property change that drives
70
+ * grid-template-columns width. No width, height, or position
71
+ * properties are transitioned on individual elements.
72
+ */
73
+
74
+ 'use client'
75
+
76
+ import React, { useRef, useEffect, useCallback } from 'react'
77
+ import { usePathname } from 'next/navigation'
78
+ import { useSidebar } from './SidebarContext'
79
+ import { Sidebar } from './Sidebar'
80
+ import { Topbar } from './Topbar'
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Types
84
+ // ---------------------------------------------------------------------------
85
+
86
+ interface AppShellProps {
87
+ children: React.ReactNode
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Component
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export function AppShell({ children }: AppShellProps) {
95
+ const { isCollapsed, isMobileOpen, closeMobile } = useSidebar()
96
+ const mainRef = useRef<HTMLElement>(null)
97
+ const pathname = usePathname()
98
+
99
+ // Focus management: when the route changes, move focus to <main>.
100
+ // This is a11y-critical for keyboard and screen reader navigation.
101
+ useEffect(() => {
102
+ mainRef.current?.focus({ preventScroll: true })
103
+ }, [pathname])
104
+
105
+ // Close mobile drawer when Escape is pressed.
106
+ const handleKeyDown = useCallback(
107
+ (e: React.KeyboardEvent) => {
108
+ if (e.key === 'Escape' && isMobileOpen) {
109
+ closeMobile()
110
+ }
111
+ },
112
+ [isMobileOpen, closeMobile]
113
+ )
114
+
115
+ return (
116
+ /*
117
+ * The shell-root div uses CSS custom properties to drive the
118
+ * grid column width. The inline style sets the --shell-sidebar-w
119
+ * variable so the CSS transition in globals.css picks it up.
120
+ * Using an inline style for a single token change is intentional:
121
+ * it avoids className string manipulation while keeping the
122
+ * transition on the CSS side (not JS-driven).
123
+ */
124
+ <div
125
+ className="shell-root"
126
+ style={{
127
+ // This CSS variable feeds grid-template-columns in globals.css.
128
+ // The transition is declared on the .shell-root rule there.
129
+ '--shell-sidebar-w': isCollapsed
130
+ ? 'var(--spacing-sidebar-w-col)'
131
+ : 'var(--spacing-sidebar-w)',
132
+ } as React.CSSProperties}
133
+ onKeyDown={handleKeyDown}
134
+ >
135
+ {/*
136
+ * Mobile backdrop overlay. Visible only when isMobileOpen.
137
+ * Clicking it closes the drawer. It sits below the sidebar
138
+ * (z-index 29) so it does not intercept sidebar interactions.
139
+ * The backdrop uses opacity transition (not visibility) so
140
+ * reduced-motion users still see it fade correctly (opacity
141
+ * transitions at 80ms are preserved under reduced-motion).
142
+ */}
143
+ {isMobileOpen && (
144
+ <div
145
+ className="shell-backdrop"
146
+ aria-hidden="true"
147
+ onClick={closeMobile}
148
+ />
149
+ )}
150
+
151
+ {/* Sidebar: fixed on mobile, in-flow grid column on desktop */}
152
+ <Sidebar />
153
+
154
+ {/*
155
+ * Main column: contains the sticky Topbar and the scrollable
156
+ * page content. overflow-y: auto here (not on <body>) so the
157
+ * sidebar never scrolls with the page content.
158
+ */}
159
+ <div className="shell-main">
160
+ <Topbar />
161
+ <main
162
+ ref={mainRef}
163
+ id="main-content"
164
+ // tabIndex={-1} allows programmatic focus (route change)
165
+ // without placing <main> in the natural tab order.
166
+ tabIndex={-1}
167
+ className="shell-content"
168
+ >
169
+ {children}
170
+ </main>
171
+ </div>
172
+ </div>
173
+ )
174
+ }