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.
- package/README.md +18 -13
- package/bin/chati.js +32 -4
- package/framework/agents/build/dev.md +9 -5
- package/framework/agents/discover/brief.md +2 -0
- package/framework/agents/discover/brownfield-wu.md +2 -0
- package/framework/agents/discover/greenfield-wu.md +18 -1
- package/framework/agents/plan/detail.md +2 -0
- package/framework/agents/plan/tasks.md +31 -0
- package/framework/agents/plan/ux-brand-architect.md +21 -2
- package/framework/agents/plan/ux-component-engineer.md +10 -0
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +33 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +58 -11
- package/framework/context/governance.md +12 -10
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +12 -4
- package/framework/data/qa-rubrics.yaml +335 -0
- package/framework/domains/constitution.yaml +3 -3
- package/framework/executors/json-validate.js +80 -0
- package/framework/executors/npm-script.js +72 -0
- package/framework/executors/path-check.js +67 -0
- package/framework/executors/registry.yaml +36 -0
- package/framework/hooks/advance-trigger.js +47 -3
- package/framework/hooks/constitution-guard.js +9 -4
- package/framework/hooks/git-push-authority.js +113 -0
- package/framework/hooks/license-guard.js +59 -9
- package/framework/hooks/model-governance.js +40 -17
- package/framework/hooks/prism-engine.js +78 -11
- package/framework/hooks/session-digest.js +12 -3
- package/framework/hooks/settings.json +7 -3
- package/framework/hooks/style-guard.js +52 -3
- package/framework/hooks/team-quality-gate.js +43 -5
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/confidence.yaml +85 -6
- package/framework/intelligence/context-engine.md +9 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/orchestrator/chati-router.js +59 -5
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +20 -1
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +12 -2
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
- package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
- package/framework/scaffold/motion-premium-3d/README.md +10 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
- package/framework/scaffold/saas-dashboard/README.md +58 -0
- package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
- package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
- package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
- package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
- package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
- package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
- package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
- package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
- package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
- package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
- package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
- package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
- package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
- package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
- package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
- package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
- package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
- package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
- package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
- package/framework/schemas/session.schema.json +5 -0
- package/framework/schemas/task.schema.json +9 -0
- package/framework/scripts/visual-qa.js +101 -3
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -3
- package/src/config/context-file-generator.js +0 -6
- package/src/dashboard/renderer.js +0 -36
- package/src/executors/runner.js +204 -0
- package/src/installer/core.js +13 -3
- package/src/installer/templates.js +5 -6
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +9 -3
- package/src/license/commands.js +12 -2
- package/src/license/machine-id.js +42 -1
- package/src/memory/gotchas.js +58 -40
- package/src/memory/magic-docs.js +1 -1
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +186 -15
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +64 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/feature-flags.js +1 -1
- package/src/utils/flatten-entities.js +4 -40
- package/src/utils/schema-validator.js +0 -14
- package/src/wizard/i18n.js +3 -3
- package/src/api/index.js +0 -120
- package/src/autonomy/autonomous-gate.js +0 -294
- package/src/autonomy/build-loop.js +0 -281
- package/src/autonomy/build-state.js +0 -286
- package/src/autonomy/cause-analyzer.js +0 -177
- package/src/autonomy/escalation.js +0 -214
- package/src/autonomy/index.js +0 -51
- package/src/autonomy/mode-manager.js +0 -225
- package/src/autonomy/mode-suggester.js +0 -283
- package/src/autonomy/progress-reporter.js +0 -275
- package/src/autonomy/safety-net.js +0 -370
- package/src/config/agent-customizer.js +0 -231
- package/src/decision/analyzer.js +0 -291
- package/src/decision/engine.js +0 -250
- package/src/decision/index.js +0 -38
- package/src/decision/registry-healer.js +0 -468
- package/src/decision/registry-updater.js +0 -339
- package/src/extensions/loader.js +0 -145
- package/src/extensions/registry.js +0 -134
- package/src/gates/circuit-breaker.js +0 -151
- package/src/gates/g1-planning-complete.js +0 -154
- package/src/gates/g2-qa-planning.js +0 -156
- package/src/gates/g3-implementation.js +0 -215
- package/src/gates/g4-qa-implementation.js +0 -240
- package/src/gates/g5-deploy-ready.js +0 -181
- package/src/gates/gate-base.js +0 -185
- package/src/gates/index.js +0 -46
- package/src/health/auto-fix.js +0 -216
- package/src/health/engine.js +0 -246
- package/src/merger/semantic-merger.js +0 -292
- package/src/preview/detector.js +0 -238
- package/src/preview/index.js +0 -20
- package/src/preview/launcher.js +0 -235
- package/src/preview/log-buffer.js +0 -103
- package/src/quality/metrics-collector.js +0 -281
- package/src/quality/test-runner.js +0 -366
- package/src/tasks/executor.js +0 -195
- package/src/tasks/index.js +0 -4
- package/src/tasks/loader.js +0 -210
- package/src/tasks/router.js +0 -182
- package/src/utils/event-bus.js +0 -126
|
@@ -0,0 +1,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
|
+
`;
|