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,104 @@
1
+ /**
2
+ * components/data/DataTable.types.ts
3
+ * ============================================================
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * DataTable is a generic component (DataTable<T>) that needs its
7
+ * column definition type to be importable independently. Without a
8
+ * dedicated types file, any component that imports ColumnDef<T>
9
+ * would pull in the entire DataTable render tree. Separating types
10
+ * allows tree-shaking and prevents circular imports when
11
+ * DataTablePagination imports the type for its own props.
12
+ *
13
+ * DESIGN DECISIONS
14
+ * ----------------
15
+ * ColumnDef<T> mirrors the TanStack Table v8 surface but does NOT
16
+ * depend on it. This keeps the preset dependency-light while
17
+ * matching the industry-standard mental model, making migration
18
+ * to full TanStack Table trivial (swap ColumnDef import only).
19
+ *
20
+ * The `render` function receives the full row datum so columns can
21
+ * compose multiple fields (e.g., an avatar + name cell) without
22
+ * needing a separate row accessor. The return type is ReactNode
23
+ * rather than string so cells can render badges, links, and icons.
24
+ *
25
+ * `align` defaults to 'left' for content columns and should be
26
+ * 'right' for numeric columns so decimal points stack vertically
27
+ * (a core data-table accessibility and readability requirement).
28
+ *
29
+ * SortDirection is a discriminated union rather than boolean to
30
+ * support the three-state cycle: none -> asc -> desc -> none.
31
+ * This matches how most data grids behave and avoids ambiguity
32
+ * about what `false` means for sort direction.
33
+ * ============================================================
34
+ */
35
+
36
+ import type { ReactNode } from 'react';
37
+ import type React from 'react';
38
+
39
+ /** Three-state sort cycle: unsorted, ascending, descending. */
40
+ export type SortDirection = 'asc' | 'desc' | 'none';
41
+
42
+ /**
43
+ * Column definition for DataTable<T>.
44
+ * T is the row datum type (e.g., User, Order, Invoice).
45
+ */
46
+ export interface ColumnDef<T> {
47
+ /**
48
+ * Unique key used as the React key and as the sort field identifier.
49
+ * Must be stable across renders (not an array index).
50
+ */
51
+ key: string;
52
+
53
+ /** Column header label. Rendered in <th> scope="col". */
54
+ header: string;
55
+
56
+ /**
57
+ * Cell renderer. Receives the full row datum.
58
+ * Return a ReactNode so cells can include badges, links, avatars.
59
+ * When omitted, falls back to String(row[key as keyof T]).
60
+ */
61
+ render?: (row: T) => ReactNode;
62
+
63
+ /**
64
+ * Text alignment within the cell.
65
+ * Use 'right' for numeric/currency columns to align decimal points.
66
+ * Defaults to 'left'.
67
+ */
68
+ align?: 'left' | 'center' | 'right';
69
+
70
+ /**
71
+ * Whether this column is sortable.
72
+ * When true, the header renders a ChevronUp/ChevronDown toggle
73
+ * and calls the onSort callback.
74
+ * Defaults to false.
75
+ */
76
+ sortable?: boolean;
77
+
78
+ /**
79
+ * Fixed column width in pixels or a CSS width string (e.g., '20%').
80
+ * Set on the <col> element inside <colgroup>.
81
+ * When omitted, the browser distributes width naturally.
82
+ */
83
+ width?: number | string;
84
+
85
+ /**
86
+ * sr-only description appended to the header for screen readers.
87
+ * Use for icon-only headers (e.g., a "Status" column with only a
88
+ * colored dot). Supplements, does not replace, the `header` string.
89
+ */
90
+ srDescription?: string;
91
+ }
92
+
93
+ /** Sort state passed to the onSort callback. */
94
+ export interface SortState {
95
+ key: string;
96
+ direction: SortDirection;
97
+ }
98
+
99
+ /** Pagination state for DataTablePagination. */
100
+ export interface PaginationState {
101
+ page: number;
102
+ pageSize: number;
103
+ total: number;
104
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * components/data/DataTablePagination.tsx
3
+ * ============================================================
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * Pagination is extracted from DataTable into its own file for two
7
+ * reasons:
8
+ *
9
+ * 1. Composability: callers who render DataTable inside a drawer or
10
+ * modal may want pagination above the table, or not at all. A
11
+ * standalone component supports every layout without conditional
12
+ * rendering inside DataTable.
13
+ *
14
+ * 2. Testability: pagination logic ("Showing X to Y of Z",
15
+ * page-size options, boundary conditions on prev/next) is much
16
+ * easier to unit-test when isolated.
17
+ *
18
+ * DESIGN DECISIONS
19
+ * ----------------
20
+ * Page-size select: uses a native <select> (not a custom dropdown)
21
+ * because native <select> has built-in keyboard navigation,
22
+ * mobile-native UX (bottom sheet picker on iOS), and zero
23
+ * JavaScript overhead. A real <label> is provided (sr-only)
24
+ * so the field is fully accessible.
25
+ *
26
+ * "Showing X to Y of Z" copy: computed client-side from props.
27
+ * When total is 0, it shows "No results" instead of "Showing 0
28
+ * to 0 of 0" which is confusing.
29
+ *
30
+ * Prev/Next buttons: disabled at boundaries. `aria-disabled` is set
31
+ * in addition to the HTML `disabled` attribute so CSS can style
32
+ * the disabled state without relying on the attribute selector
33
+ * specificity.
34
+ *
35
+ * Page count: shown as "Page X of Y" in between prev/next for
36
+ * context. Hidden on very small viewports (< 400px) to avoid
37
+ * wrapping.
38
+ *
39
+ * MOTION: no transition on the pagination row itself. The table body
40
+ * re-renders with new rows, and that transition (if any) is
41
+ * handled by DataTable's row entry animation.
42
+ * ============================================================
43
+ */
44
+
45
+ 'use client';
46
+
47
+ import React, { useId } from 'react';
48
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
49
+
50
+ export interface DataTablePaginationProps {
51
+ /** Current page number (1-indexed). */
52
+ page: number;
53
+ /** Number of rows per page. */
54
+ pageSize: number;
55
+ /** Total number of rows across all pages. */
56
+ total: number;
57
+ /** Called when user navigates to a different page. */
58
+ onPageChange: (page: number) => void;
59
+ /** Called when user changes rows-per-page. */
60
+ onPageSizeChange: (size: number) => void;
61
+ /** Available page-size options. Defaults to [10, 25, 50, 100]. */
62
+ pageSizeOptions?: number[];
63
+ }
64
+
65
+ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
66
+
67
+ export function DataTablePagination({
68
+ page,
69
+ pageSize,
70
+ total,
71
+ onPageChange,
72
+ onPageSizeChange,
73
+ pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
74
+ }: DataTablePaginationProps) {
75
+ const selectId = useId();
76
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
77
+ const firstRow = total === 0 ? 0 : (page - 1) * pageSize + 1;
78
+ const lastRow = Math.min(page * pageSize, total);
79
+ const canPrev = page > 1;
80
+ const canNext = page < totalPages;
81
+
82
+ const rowSummary =
83
+ total === 0
84
+ ? 'No results'
85
+ : `Showing ${firstRow} to ${lastRow} of ${total}`;
86
+
87
+ return (
88
+ <div
89
+ role="navigation"
90
+ aria-label="Table pagination"
91
+ style={paginationWrapStyle}
92
+ >
93
+ {/* Left: rows summary */}
94
+ <span
95
+ aria-live="polite"
96
+ aria-atomic="true"
97
+ style={{
98
+ fontSize: 'var(--type-small)',
99
+ color: 'var(--color-text-secondary)',
100
+ flexShrink: 0,
101
+ }}
102
+ >
103
+ {rowSummary}
104
+ </span>
105
+
106
+ {/* Right: page-size selector + navigation */}
107
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
108
+ {/* Rows per page */}
109
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)' }}>
110
+ {/* Real label, sr-only */}
111
+ <label
112
+ htmlFor={selectId}
113
+ style={srOnlyStyle}
114
+ >
115
+ Rows per page
116
+ </label>
117
+ <select
118
+ id={selectId}
119
+ value={pageSize}
120
+ onChange={(e) => {
121
+ onPageSizeChange(Number(e.target.value));
122
+ onPageChange(1);
123
+ }}
124
+ style={selectStyle}
125
+ aria-label="Rows per page"
126
+ >
127
+ {pageSizeOptions.map((opt) => (
128
+ <option key={opt} value={opt}>
129
+ {opt} rows
130
+ </option>
131
+ ))}
132
+ </select>
133
+ </div>
134
+
135
+ {/* Page indicator */}
136
+ <span
137
+ style={{
138
+ fontSize: 'var(--type-small)',
139
+ color: 'var(--color-text-secondary)',
140
+ // Hide on very narrow containers to prevent wrap
141
+ display: 'inline',
142
+ whiteSpace: 'nowrap',
143
+ }}
144
+ aria-label={`Page ${page} of ${totalPages}`}
145
+ >
146
+ {page} / {totalPages}
147
+ </span>
148
+
149
+ {/* Previous */}
150
+ <button
151
+ type="button"
152
+ onClick={() => canPrev && onPageChange(page - 1)}
153
+ disabled={!canPrev}
154
+ aria-disabled={!canPrev}
155
+ aria-label="Previous page"
156
+ style={navButtonStyle(!canPrev)}
157
+ >
158
+ <ChevronLeft size={15} aria-hidden="true" />
159
+ </button>
160
+
161
+ {/* Next */}
162
+ <button
163
+ type="button"
164
+ onClick={() => canNext && onPageChange(page + 1)}
165
+ disabled={!canNext}
166
+ aria-disabled={!canNext}
167
+ aria-label="Next page"
168
+ style={navButtonStyle(!canNext)}
169
+ >
170
+ <ChevronRight size={15} aria-hidden="true" />
171
+ </button>
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Styles
179
+ // ---------------------------------------------------------------------------
180
+
181
+ const paginationWrapStyle: React.CSSProperties = {
182
+ display: 'flex',
183
+ alignItems: 'center',
184
+ justifyContent: 'space-between',
185
+ flexWrap: 'wrap',
186
+ gap: 'var(--space-3)',
187
+ padding: 'var(--space-3) var(--space-4)',
188
+ borderTop: '1px solid var(--color-border-default)',
189
+ backgroundColor: 'var(--color-bg-raised)',
190
+ };
191
+
192
+ const selectStyle: React.CSSProperties = {
193
+ backgroundColor: 'var(--color-bg-sunken)',
194
+ color: 'var(--color-text-primary)',
195
+ border: '1px solid var(--color-border-default)',
196
+ borderRadius: 'var(--radius-sm)',
197
+ fontSize: 'var(--type-small)',
198
+ padding: 'var(--space-1) var(--space-2)',
199
+ cursor: 'pointer',
200
+ outline: 'none',
201
+ // Focus ring handled by global :focus-visible rule in globals.css
202
+ };
203
+
204
+ function navButtonStyle(disabled: boolean): React.CSSProperties {
205
+ return {
206
+ display: 'inline-flex',
207
+ alignItems: 'center',
208
+ justifyContent: 'center',
209
+ width: 'var(--space-8)',
210
+ height: 'var(--space-8)',
211
+ borderRadius: 'var(--radius-default)',
212
+ border: '1px solid var(--color-border-default)',
213
+ backgroundColor: 'transparent',
214
+ color: disabled ? 'var(--color-text-muted)' : 'var(--color-text-primary)',
215
+ cursor: disabled ? 'not-allowed' : 'pointer',
216
+ opacity: disabled ? 0.4 : 1,
217
+ transition: `background-color var(--duration-fast) var(--ease-out),
218
+ opacity var(--duration-fast) var(--ease-out)`,
219
+ outline: 'none',
220
+ pointerEvents: disabled ? 'none' : undefined,
221
+ };
222
+ }
223
+
224
+ const srOnlyStyle: React.CSSProperties = {
225
+ position: 'absolute',
226
+ width: '1px',
227
+ height: '1px',
228
+ padding: 0,
229
+ margin: '-1px',
230
+ overflow: 'hidden',
231
+ clip: 'rect(0,0,0,0)',
232
+ whiteSpace: 'nowrap',
233
+ border: 0,
234
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * components/data/SkeletonBlock.tsx
3
+ * ============================================================
4
+ * WHY THIS COMPONENT EXISTS
5
+ * -------------------------
6
+ * Loading states are a first-class quality concern in dashboard UIs.
7
+ * A generic skeleton block eliminates the temptation to use spinners
8
+ * (which do not communicate layout) or plain grey boxes (which shift
9
+ * layout when content loads). SkeletonBlock reserves exact space
10
+ * with the same dimensions as the content it stands in for.
11
+ *
12
+ * ANIMATION APPROACH
13
+ * ------------------
14
+ * The shimmer effect is implemented as a CSS gradient sweep on a
15
+ * ::after pseudo-element. Critically, only `transform: translateX`
16
+ * is animated -- never `background-position` or `left`. This keeps
17
+ * the animation on the compositor thread and prevents layout
18
+ * thrashing even when hundreds of skeleton blocks are visible
19
+ * simultaneously (e.g., a 50-row table in loading state).
20
+ *
21
+ * The gradient is 200% wide so the sweep covers the full element
22
+ * width with a centered highlight, creating the perception of light
23
+ * reflecting across a surface.
24
+ *
25
+ * REDUCED-MOTION
26
+ * --------------
27
+ * When prefers-reduced-motion: reduce is active, the shimmer
28
+ * animation is disabled via the global globals.css rule that sets
29
+ * animation-duration to 0.01ms. The element still renders as a
30
+ * static muted block (using --color-bg-highlight), which preserves
31
+ * the layout placeholder without causing motion discomfort.
32
+ *
33
+ * USAGE
34
+ * -----
35
+ * <SkeletonBlock width="60%" height={14} borderRadius="var(--radius-sharp)" />
36
+ * <SkeletonBlock width={32} height={32} borderRadius="var(--radius-circle)" />
37
+ * ============================================================
38
+ */
39
+
40
+ import React from 'react';
41
+
42
+ export interface SkeletonBlockProps {
43
+ /** Width as a CSS string ('60%', '120px') or raw pixel number. */
44
+ width?: number | string;
45
+ /** Height as a CSS string or raw pixel number. */
46
+ height?: number | string;
47
+ /** Border radius mapped to a token (e.g., 'var(--radius-default)'). */
48
+ borderRadius?: string;
49
+ /** Additional className for layout overrides. */
50
+ className?: string;
51
+ }
52
+
53
+ export function SkeletonBlock({
54
+ width = '100%',
55
+ height = 16,
56
+ borderRadius = 'var(--radius-sharp)',
57
+ className,
58
+ }: SkeletonBlockProps) {
59
+ const widthVal = typeof width === 'number' ? `${width}px` : width;
60
+ const heightVal = typeof height === 'number' ? `${height}px` : height;
61
+
62
+ return (
63
+ <span
64
+ className={className}
65
+ aria-hidden="true"
66
+ style={{
67
+ display: 'block',
68
+ width: widthVal,
69
+ height: heightVal,
70
+ borderRadius,
71
+ // Base color: a step above the card surface so it's perceptible
72
+ // but not jarring. Uses the overlay bg to match card interiors.
73
+ backgroundColor: 'var(--color-bg-highlight)',
74
+ position: 'relative',
75
+ overflow: 'hidden',
76
+ // Shimmer is the ::after pseudo-element, but since inline styles
77
+ // cannot target pseudo-elements, the animation class is injected
78
+ // via a <style> tag rendered once at the module level.
79
+ }}
80
+ >
81
+ {/* Shimmer layer injected via inline <style> -- scoped keyframes */}
82
+ <style>{SHIMMER_KEYFRAMES}</style>
83
+ <span
84
+ aria-hidden="true"
85
+ style={{
86
+ position: 'absolute',
87
+ inset: 0,
88
+ background:
89
+ 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.07) 50%, transparent 100%)',
90
+ backgroundSize: '200% 100%',
91
+ // translateX sweeps from -100% to +100% of the element width
92
+ animation: `skeleton-shimmer 1.4s var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1)) infinite`,
93
+ // Will-change hints to compositor for GPU promotion
94
+ willChange: 'transform',
95
+ }}
96
+ />
97
+ </span>
98
+ );
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Keyframes injected once per page render (React deduplicates identical
103
+ // <style> elements in the DOM, so this does not multiply).
104
+ // Only transform is animated -- never left, width, or background-position.
105
+ // ---------------------------------------------------------------------------
106
+ const SHIMMER_KEYFRAMES = `
107
+ @keyframes skeleton-shimmer {
108
+ from { transform: translateX(-100%); }
109
+ to { transform: translateX(100%); }
110
+ }
111
+ `;