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,301 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Topbar.tsx -- Application Topbar
|
|
3
|
+
* ============================================================
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
* --------------------
|
|
6
|
+
* The topbar is the sticky horizontal bar that sits at the top
|
|
7
|
+
* of the main content column (not the full viewport width).
|
|
8
|
+
* It serves four roles:
|
|
9
|
+
*
|
|
10
|
+
* 1. Navigation context: the breadcrumb trail tells the user
|
|
11
|
+
* where they are in the application hierarchy.
|
|
12
|
+
*
|
|
13
|
+
* 2. Sidebar control: the hamburger/collapse button is the
|
|
14
|
+
* only interactive element that controls sidebar state.
|
|
15
|
+
* On mobile it opens the overlay drawer; on desktop it
|
|
16
|
+
* toggles icon-only mode.
|
|
17
|
+
*
|
|
18
|
+
* 3. Global search: the search button opens the CommandPalette
|
|
19
|
+
* (Cmd+K shortcut). The button is keyboard-accessible and
|
|
20
|
+
* shows the keyboard shortcut hint.
|
|
21
|
+
*
|
|
22
|
+
* 4. User affordances: notification bell (with unread count
|
|
23
|
+
* badge) and a user menu trigger (avatar + chevron).
|
|
24
|
+
*
|
|
25
|
+
* POSITIONING
|
|
26
|
+
* -----------
|
|
27
|
+
* The Topbar uses position:sticky with top:0. It sits in the
|
|
28
|
+
* second grid column (shell-main) alongside the scrollable
|
|
29
|
+
* <main>. This means it scrolls with the sidebar but stays
|
|
30
|
+
* fixed relative to the main content viewport. z-index is
|
|
31
|
+
* --z-topbar (40) so it appears above content but below modals.
|
|
32
|
+
*
|
|
33
|
+
* BACKDROP BLUR
|
|
34
|
+
* -------------
|
|
35
|
+
* backdrop-filter: blur(8px) creates a frosted-glass effect
|
|
36
|
+
* when content scrolls behind the topbar. We also set a
|
|
37
|
+
* semi-transparent bg-overlay background (NOT full opacity)
|
|
38
|
+
* to let the blur be visible. This is intentional: full-opacity
|
|
39
|
+
* backgrounds would make the blur invisible.
|
|
40
|
+
*
|
|
41
|
+
* BREADCRUMB
|
|
42
|
+
* ----------
|
|
43
|
+
* The Breadcrumb component is imported from its own file where
|
|
44
|
+
* it derives crumbs from the Next.js pathname. Topbar owns the
|
|
45
|
+
* placement but not the logic.
|
|
46
|
+
*
|
|
47
|
+
* SEARCH KEYBOARD SHORTCUT
|
|
48
|
+
* -------------------------
|
|
49
|
+
* A useEffect registers the Cmd+K (Mac) / Ctrl+K (Windows/Linux)
|
|
50
|
+
* shortcut globally. The shortcut state is lifted to this
|
|
51
|
+
* component because Topbar is always mounted, making it the
|
|
52
|
+
* right place for global keyboard shortcuts that do not belong
|
|
53
|
+
* to a specific page.
|
|
54
|
+
*
|
|
55
|
+
* ARIA
|
|
56
|
+
* ----
|
|
57
|
+
* The topbar is a <header> element (banner landmark). The
|
|
58
|
+
* hamburger button has aria-expanded and aria-controls tied
|
|
59
|
+
* to the sidebar nav. The notification button uses aria-label
|
|
60
|
+
* that includes the count ("12 unread notifications") so
|
|
61
|
+
* screen readers announce the count without the user needing
|
|
62
|
+
* to read the visual badge.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
'use client'
|
|
66
|
+
|
|
67
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
|
68
|
+
import { useCommandPalette } from '../interaction/CommandPalette';
|
|
69
|
+
import Link from 'next/link'
|
|
70
|
+
import {
|
|
71
|
+
Menu,
|
|
72
|
+
Search,
|
|
73
|
+
Bell,
|
|
74
|
+
ChevronDown,
|
|
75
|
+
Settings,
|
|
76
|
+
LogOut,
|
|
77
|
+
User,
|
|
78
|
+
Command,
|
|
79
|
+
type LucideIcon,
|
|
80
|
+
} from 'lucide-react'
|
|
81
|
+
import { useSidebar } from './SidebarContext'
|
|
82
|
+
import { Breadcrumb } from './Breadcrumb'
|
|
83
|
+
import { ThemeToggle } from './ThemeToggle'
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Types
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
// Notification count: in a real app this would come from a server action
|
|
90
|
+
// or a real-time subscription. Static for the preset.
|
|
91
|
+
const NOTIFICATION_COUNT = 4
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// User menu items
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
type UserMenuItem = { label: string; href: string; icon: LucideIcon; destructive?: boolean };
|
|
98
|
+
|
|
99
|
+
const USER_MENU_ITEMS: UserMenuItem[] = [
|
|
100
|
+
{ label: 'Your Profile', href: '/settings/profile', icon: User },
|
|
101
|
+
{ label: 'Settings', href: '/settings', icon: Settings },
|
|
102
|
+
{ label: 'Sign out', href: '/auth/sign-out', icon: LogOut, destructive: true },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Component
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export function Topbar() {
|
|
110
|
+
const { toggle, openMobile, isCollapsed } = useSidebar()
|
|
111
|
+
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
|
112
|
+
const userMenuRef = useRef<HTMLDivElement>(null)
|
|
113
|
+
|
|
114
|
+
// The CommandPaletteProvider (mounted in layout) owns the palette open state
|
|
115
|
+
// and the global Cmd+K / Ctrl+K shortcut. The top bar just opens it.
|
|
116
|
+
const { open: openCommandPalette } = useCommandPalette()
|
|
117
|
+
|
|
118
|
+
// ---- Close user menu on click-outside -----------------------------------
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
function handleClickOutside(e: MouseEvent) {
|
|
121
|
+
if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) {
|
|
122
|
+
setUserMenuOpen(false)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (userMenuOpen) {
|
|
126
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
127
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
128
|
+
}
|
|
129
|
+
}, [userMenuOpen])
|
|
130
|
+
|
|
131
|
+
// ---- Close user menu on Escape ------------------------------------------
|
|
132
|
+
const handleUserMenuKeyDown = useCallback(
|
|
133
|
+
(e: React.KeyboardEvent) => {
|
|
134
|
+
if (e.key === 'Escape') {
|
|
135
|
+
setUserMenuOpen(false)
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
[]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// ---- Determine if we are on mobile (client-only) ------------------------
|
|
142
|
+
// We detect mobile by CSS media query rather than viewport width JS so
|
|
143
|
+
// the hamburger behavior (openMobile vs toggle) is correct on resize.
|
|
144
|
+
const handleSidebarTrigger = useCallback(() => {
|
|
145
|
+
if (window.matchMedia('(max-width: 767px)').matches) {
|
|
146
|
+
openMobile()
|
|
147
|
+
} else {
|
|
148
|
+
toggle()
|
|
149
|
+
}
|
|
150
|
+
}, [toggle, openMobile])
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<header className="topbar" role="banner">
|
|
154
|
+
<div className="topbar-inner">
|
|
155
|
+
{/* ---- Left: sidebar trigger + breadcrumb ---- */}
|
|
156
|
+
<div className="topbar-left">
|
|
157
|
+
{/*
|
|
158
|
+
* Sidebar toggle button. On mobile: opens the overlay drawer.
|
|
159
|
+
* On desktop: toggles icon-only collapse.
|
|
160
|
+
* aria-expanded reflects sidebar state (desktop only).
|
|
161
|
+
* aria-controls links to the sidebar nav element.
|
|
162
|
+
*/}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={handleSidebarTrigger}
|
|
166
|
+
aria-label="Toggle navigation"
|
|
167
|
+
aria-expanded={!isCollapsed}
|
|
168
|
+
aria-controls="sidebar-nav"
|
|
169
|
+
className="topbar-icon-btn"
|
|
170
|
+
>
|
|
171
|
+
<Menu size={20} aria-hidden="true" />
|
|
172
|
+
</button>
|
|
173
|
+
|
|
174
|
+
{/* Breadcrumb trail: derives crumbs from Next.js pathname */}
|
|
175
|
+
<Breadcrumb />
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* ---- Right: search + notifications + user menu ---- */}
|
|
179
|
+
<div className="topbar-right">
|
|
180
|
+
{/*
|
|
181
|
+
* Search / command palette trigger.
|
|
182
|
+
* Shows Cmd+K hint on desktop (hidden on mobile via CSS).
|
|
183
|
+
*/}
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={openCommandPalette}
|
|
187
|
+
aria-label="Open command palette"
|
|
188
|
+
aria-keyshortcuts="Control+k Meta+k"
|
|
189
|
+
className="topbar-search-btn"
|
|
190
|
+
>
|
|
191
|
+
<Search size={16} aria-hidden="true" className="topbar-search-icon" />
|
|
192
|
+
<span className="topbar-search-label">Search...</span>
|
|
193
|
+
<kbd className="topbar-search-kbd" aria-hidden="true">
|
|
194
|
+
<Command size={10} />
|
|
195
|
+
K
|
|
196
|
+
</kbd>
|
|
197
|
+
</button>
|
|
198
|
+
|
|
199
|
+
{/*
|
|
200
|
+
* Notification bell.
|
|
201
|
+
* aria-label includes the count so SR users hear it.
|
|
202
|
+
* The visual badge is aria-hidden (count is in the label).
|
|
203
|
+
*/}
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
aria-label={
|
|
207
|
+
NOTIFICATION_COUNT > 0
|
|
208
|
+
? `${NOTIFICATION_COUNT} unread notifications`
|
|
209
|
+
: 'Notifications, none unread'
|
|
210
|
+
}
|
|
211
|
+
className="topbar-icon-btn topbar-icon-btn--relative"
|
|
212
|
+
>
|
|
213
|
+
<Bell size={20} aria-hidden="true" />
|
|
214
|
+
{NOTIFICATION_COUNT > 0 && (
|
|
215
|
+
<span className="topbar-notif-badge" aria-hidden="true">
|
|
216
|
+
{NOTIFICATION_COUNT > 9 ? '9+' : NOTIFICATION_COUNT}
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
</button>
|
|
220
|
+
|
|
221
|
+
{/*
|
|
222
|
+
* User menu: avatar + name + dropdown.
|
|
223
|
+
* role="button" + aria-haspopup + aria-expanded for the trigger.
|
|
224
|
+
* The dropdown is a role="menu" with role="menuitem" children.
|
|
225
|
+
*/}
|
|
226
|
+
<div
|
|
227
|
+
ref={userMenuRef}
|
|
228
|
+
className="topbar-user-menu"
|
|
229
|
+
onKeyDown={handleUserMenuKeyDown}
|
|
230
|
+
>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
id="user-menu-trigger"
|
|
234
|
+
aria-haspopup="menu"
|
|
235
|
+
aria-expanded={userMenuOpen}
|
|
236
|
+
aria-controls="user-menu-dropdown"
|
|
237
|
+
onClick={() => setUserMenuOpen((v) => !v)}
|
|
238
|
+
className="topbar-user-trigger"
|
|
239
|
+
>
|
|
240
|
+
{/* Avatar */}
|
|
241
|
+
<span className="topbar-avatar" aria-hidden="true">
|
|
242
|
+
GA
|
|
243
|
+
</span>
|
|
244
|
+
{/* Name: hidden on mobile */}
|
|
245
|
+
<span className="topbar-user-name">Gabriel A.</span>
|
|
246
|
+
<ChevronDown
|
|
247
|
+
size={14}
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
className={[
|
|
250
|
+
'topbar-chevron',
|
|
251
|
+
userMenuOpen ? 'topbar-chevron--open' : '',
|
|
252
|
+
]
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join(' ')}
|
|
255
|
+
/>
|
|
256
|
+
</button>
|
|
257
|
+
|
|
258
|
+
{/* Dropdown menu */}
|
|
259
|
+
{userMenuOpen && (
|
|
260
|
+
<div
|
|
261
|
+
id="user-menu-dropdown"
|
|
262
|
+
role="menu"
|
|
263
|
+
aria-labelledby="user-menu-trigger"
|
|
264
|
+
className="topbar-dropdown"
|
|
265
|
+
>
|
|
266
|
+
{USER_MENU_ITEMS.map((item) => {
|
|
267
|
+
const Icon = item.icon
|
|
268
|
+
return (
|
|
269
|
+
<Link
|
|
270
|
+
key={item.href}
|
|
271
|
+
href={item.href}
|
|
272
|
+
role="menuitem"
|
|
273
|
+
onClick={() => setUserMenuOpen(false)}
|
|
274
|
+
className={[
|
|
275
|
+
'topbar-dropdown-item',
|
|
276
|
+
item.destructive ? 'topbar-dropdown-item--destructive' : '',
|
|
277
|
+
]
|
|
278
|
+
.filter(Boolean)
|
|
279
|
+
.join(' ')}
|
|
280
|
+
>
|
|
281
|
+
<Icon size={15} aria-hidden="true" />
|
|
282
|
+
<span>{item.label}</span>
|
|
283
|
+
</Link>
|
|
284
|
+
)
|
|
285
|
+
})}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{/*
|
|
293
|
+
* CommandPalette portal slot: rendered here so it is inside the
|
|
294
|
+
* <header> DOM subtree for correct ARIA ownership. In production
|
|
295
|
+
* this would use Next.js dynamic import for code splitting.
|
|
296
|
+
* The actual CommandPalette component (D_interaction builder) is
|
|
297
|
+
* imported here; for this preset it is a placeholder trigger slot.
|
|
298
|
+
*/}
|
|
299
|
+
</header>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/brand.ts -- saas-dashboard preset v1.0.0
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
* --------------------
|
|
6
|
+
* CSS custom properties are excellent for theming but opaque to TypeScript.
|
|
7
|
+
* When a Recharts component needs the violet series color, it cannot read
|
|
8
|
+
* a CSS variable at the component author's type-check time. This module
|
|
9
|
+
* provides the same values as typed constants so charting libraries,
|
|
10
|
+
* framer-motion animation objects, and utility functions can consume them
|
|
11
|
+
* with full IDE autocomplete and compile-time safety.
|
|
12
|
+
*
|
|
13
|
+
* CONTRACT: mirror, never diverge
|
|
14
|
+
* --------------------------------
|
|
15
|
+
* Every constant here mirrors a CSS custom property in globals.css.
|
|
16
|
+
* They must always match. If you add a token to globals.css, add it here too.
|
|
17
|
+
* The authoritative source is globals.css (rendered in the browser);
|
|
18
|
+
* this file is its TypeScript shadow.
|
|
19
|
+
*
|
|
20
|
+
* IMMUTABILITY
|
|
21
|
+
* ------------
|
|
22
|
+
* All exports use "as const" to produce literal types. This allows
|
|
23
|
+
* TypeScript to infer exact string/number values instead of widening
|
|
24
|
+
* to `string` or `number`, enabling exhaustiveness checks in components.
|
|
25
|
+
*
|
|
26
|
+
* USAGE EXAMPLES
|
|
27
|
+
* --------------
|
|
28
|
+
* // Recharts area chart series:
|
|
29
|
+
* import { COLORS } from '@/lib/brand'
|
|
30
|
+
* <Area stroke={COLORS.viz[0]} fill={COLORS.viz[0]} />
|
|
31
|
+
*
|
|
32
|
+
* // Framer-motion duration:
|
|
33
|
+
* import { DURATION, EASING } from '@/lib/brand'
|
|
34
|
+
* animate={{ opacity: 1 }}
|
|
35
|
+
* transition={{ duration: DURATION.normal / 1000, ease: EASING.out }}
|
|
36
|
+
*
|
|
37
|
+
* // Stagger children:
|
|
38
|
+
* import { staggerChildren } from '@/lib/brand'
|
|
39
|
+
* const childVariants = staggerChildren(6) // 6 items, 40ms apart
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/* ================================================================
|
|
43
|
+
COLOR TOKENS
|
|
44
|
+
================================================================ */
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Raw neutral scale values.
|
|
48
|
+
* Components should prefer semantic tokens (SEMANTIC_COLORS) over
|
|
49
|
+
* these. The raw scale is exported for design tooling and token
|
|
50
|
+
* documentation generation only.
|
|
51
|
+
*/
|
|
52
|
+
export const NEUTRAL = {
|
|
53
|
+
0: '#ffffff',
|
|
54
|
+
50: '#f7f8fa',
|
|
55
|
+
100: '#eef0f4',
|
|
56
|
+
200: '#dde1e9',
|
|
57
|
+
300: '#c4cad6',
|
|
58
|
+
400: '#9aa3b4',
|
|
59
|
+
500: '#6b7689',
|
|
60
|
+
600: '#4e5769',
|
|
61
|
+
700: '#343d4f',
|
|
62
|
+
800: '#1f2635',
|
|
63
|
+
850: '#171d2b',
|
|
64
|
+
900: '#0f1420',
|
|
65
|
+
950: '#080c14',
|
|
66
|
+
1000: '#000000',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
export type NeutralStep = keyof typeof NEUTRAL;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Accent (violet 258deg) values.
|
|
73
|
+
* Role 1: interactive affordance (buttons, links, active states).
|
|
74
|
+
* Role 2: brand emphasis (tinted backgrounds, selected rows).
|
|
75
|
+
*/
|
|
76
|
+
export const ACCENT = {
|
|
77
|
+
base: '#7c5af8',
|
|
78
|
+
hover: '#9070ff',
|
|
79
|
+
active: '#6644e8',
|
|
80
|
+
subtle: '#1e1640',
|
|
81
|
+
muted: '#4a3490',
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Status palette: success, warning, danger, info.
|
|
86
|
+
* Each status has a full-saturation value (text/icons) and
|
|
87
|
+
* a subtle value (background tints).
|
|
88
|
+
*/
|
|
89
|
+
export const STATUS = {
|
|
90
|
+
success: '#22c55e',
|
|
91
|
+
successSubtle: '#052e16',
|
|
92
|
+
warning: '#f59e0b',
|
|
93
|
+
warningSubtle: '#27200a',
|
|
94
|
+
danger: '#ef4444',
|
|
95
|
+
dangerSubtle: '#2a0a0a',
|
|
96
|
+
info: '#38bdf8',
|
|
97
|
+
infoSubtle: '#082030',
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Data visualisation series: 6 perceptually-spaced hues.
|
|
102
|
+
* The array order matters: series[0] is always the primary (violet/accent),
|
|
103
|
+
* which creates visual brand coherence in charts. Subsequent hues are
|
|
104
|
+
* ordered to maximise distinguishability at 4px line weight.
|
|
105
|
+
*/
|
|
106
|
+
export const VIZ_SERIES = [
|
|
107
|
+
'#7c5af8', // 0: violet (accent, brand-tied)
|
|
108
|
+
'#38bdf8', // 1: sky
|
|
109
|
+
'#22c55e', // 2: emerald
|
|
110
|
+
'#f59e0b', // 3: amber
|
|
111
|
+
'#f472b6', // 4: pink
|
|
112
|
+
'#fb923c', // 5: orange
|
|
113
|
+
] as const;
|
|
114
|
+
|
|
115
|
+
export type VizSeriesIndex = 0 | 1 | 2 | 3 | 4 | 5;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Semantic color map for use in JS contexts.
|
|
119
|
+
* Keys mirror the CSS custom property names (without the --color- prefix).
|
|
120
|
+
*/
|
|
121
|
+
export const COLORS = {
|
|
122
|
+
/* Surface layers (dark mode defaults) */
|
|
123
|
+
bgBase: NEUTRAL[900],
|
|
124
|
+
bgRaised: NEUTRAL[800],
|
|
125
|
+
bgOverlay: NEUTRAL[850],
|
|
126
|
+
bgSunken: NEUTRAL[950],
|
|
127
|
+
bgHighlight: ACCENT.subtle,
|
|
128
|
+
|
|
129
|
+
/* Text */
|
|
130
|
+
textPrimary: NEUTRAL[50],
|
|
131
|
+
textSecondary: NEUTRAL[400],
|
|
132
|
+
textMuted: NEUTRAL[500],
|
|
133
|
+
textInverse: NEUTRAL[950],
|
|
134
|
+
textAccent: ACCENT.hover,
|
|
135
|
+
|
|
136
|
+
/* Borders */
|
|
137
|
+
borderDefault: NEUTRAL[700],
|
|
138
|
+
borderEmphasis: NEUTRAL[600],
|
|
139
|
+
borderAccent: ACCENT.muted,
|
|
140
|
+
|
|
141
|
+
/* Status */
|
|
142
|
+
...STATUS,
|
|
143
|
+
|
|
144
|
+
/* Data viz */
|
|
145
|
+
viz: VIZ_SERIES,
|
|
146
|
+
|
|
147
|
+
/* Focus */
|
|
148
|
+
focusRing: ACCENT.base,
|
|
149
|
+
} as const;
|
|
150
|
+
|
|
151
|
+
export type ColorKey = keyof typeof COLORS;
|
|
152
|
+
|
|
153
|
+
/* ================================================================
|
|
154
|
+
MOTION TOKENS
|
|
155
|
+
================================================================ */
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Duration values in MILLISECONDS.
|
|
159
|
+
* Divide by 1000 when passing to framer-motion (which uses seconds).
|
|
160
|
+
*
|
|
161
|
+
* Usage with framer-motion:
|
|
162
|
+
* transition={{ duration: DURATION.normal / 1000, ease: EASING.out }}
|
|
163
|
+
*/
|
|
164
|
+
export const DURATION = {
|
|
165
|
+
instant: 80,
|
|
166
|
+
fast: 120,
|
|
167
|
+
normal: 200,
|
|
168
|
+
medium: 300,
|
|
169
|
+
slow: 400,
|
|
170
|
+
layout: 500,
|
|
171
|
+
} as const;
|
|
172
|
+
|
|
173
|
+
export type DurationKey = keyof typeof DURATION;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Easing curves as cubic-bezier arrays for framer-motion.
|
|
177
|
+
* Framer-motion accepts [x1, y1, x2, y2] tuples directly.
|
|
178
|
+
* The string form is also exported for CSS transition declarations.
|
|
179
|
+
*/
|
|
180
|
+
export const EASING = {
|
|
181
|
+
/** Entering: spring-like deceleration. Use for elements appearing on screen. */
|
|
182
|
+
out: [0.16, 1, 0.3, 1] as [number, number, number, number],
|
|
183
|
+
/** Leaving: builds momentum. Use for elements leaving the screen. */
|
|
184
|
+
in: [0.4, 0, 1, 1] as [number, number, number, number],
|
|
185
|
+
/** Toggling: symmetric. Use for accordions, sliders, state flips. */
|
|
186
|
+
inOut: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
|
187
|
+
/** Delight: slight overshoot. Use for badge appear, success pop. */
|
|
188
|
+
spring: [0.34, 1.56, 0.64, 1] as [number, number, number, number],
|
|
189
|
+
/** Subtle: minimal energy. Use for low-profile hover fades. */
|
|
190
|
+
subtle: [0.25, 0, 0, 1] as [number, number, number, number],
|
|
191
|
+
} as const;
|
|
192
|
+
|
|
193
|
+
/** CSS string form of easing curves for direct use in style attributes */
|
|
194
|
+
export const EASING_CSS = {
|
|
195
|
+
out: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
196
|
+
in: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
197
|
+
inOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
198
|
+
spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
199
|
+
subtle: 'cubic-bezier(0.25, 0, 0, 1)',
|
|
200
|
+
} as const;
|
|
201
|
+
|
|
202
|
+
export type EasingKey = keyof typeof EASING;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Stagger delay: 40ms between list items.
|
|
206
|
+
* Below 30ms items appear simultaneous; above 60ms feels mechanical.
|
|
207
|
+
*/
|
|
208
|
+
export const STAGGER_BASE = 40; // milliseconds
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns a framer-motion staggerChildren configuration for N items.
|
|
212
|
+
* Delay starts at 0 and increments by STAGGER_BASE for each child.
|
|
213
|
+
*
|
|
214
|
+
* Usage:
|
|
215
|
+
* const container = {
|
|
216
|
+
* animate: staggerChildren(items.length),
|
|
217
|
+
* }
|
|
218
|
+
* <motion.ul variants={container} animate="animate">
|
|
219
|
+
* {items.map((item, i) => (
|
|
220
|
+
* <motion.li key={i} variants={itemVariant}>{item}</motion.li>
|
|
221
|
+
* ))}
|
|
222
|
+
* </motion.ul>
|
|
223
|
+
*/
|
|
224
|
+
export function staggerChildren(count: number) {
|
|
225
|
+
return {
|
|
226
|
+
staggerChildren: STAGGER_BASE / 1000,
|
|
227
|
+
delayChildren: 0,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ================================================================
|
|
232
|
+
SPACING TOKENS
|
|
233
|
+
================================================================ */
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Spacing values in pixels, mirroring the --space-* CSS tokens.
|
|
237
|
+
* Use these in JS contexts: canvas drawing, Recharts margins,
|
|
238
|
+
* programmatic layout calculations.
|
|
239
|
+
*/
|
|
240
|
+
export const SPACING = {
|
|
241
|
+
px: 1,
|
|
242
|
+
0: 0,
|
|
243
|
+
1: 4,
|
|
244
|
+
2: 8,
|
|
245
|
+
3: 12,
|
|
246
|
+
4: 16,
|
|
247
|
+
5: 20,
|
|
248
|
+
6: 24,
|
|
249
|
+
7: 28,
|
|
250
|
+
8: 32,
|
|
251
|
+
9: 36,
|
|
252
|
+
10: 40,
|
|
253
|
+
11: 44,
|
|
254
|
+
12: 48,
|
|
255
|
+
14: 56,
|
|
256
|
+
16: 64,
|
|
257
|
+
20: 80,
|
|
258
|
+
24: 96,
|
|
259
|
+
32: 128,
|
|
260
|
+
40: 160,
|
|
261
|
+
48: 192,
|
|
262
|
+
56: 224,
|
|
263
|
+
64: 256,
|
|
264
|
+
} as const;
|
|
265
|
+
|
|
266
|
+
export type SpacingStep = keyof typeof SPACING;
|
|
267
|
+
|
|
268
|
+
/** Semantic spacing aliases */
|
|
269
|
+
export const SPACING_SEMANTIC = {
|
|
270
|
+
cardPad: SPACING[6], // 24px
|
|
271
|
+
cardGap: SPACING[5], // 20px
|
|
272
|
+
sectionGap: SPACING[8], // 32px
|
|
273
|
+
topbarH: SPACING[12], // 48px
|
|
274
|
+
sidebarW: 240, // expanded
|
|
275
|
+
sidebarWCol: SPACING[16], // 64px collapsed
|
|
276
|
+
} as const;
|
|
277
|
+
|
|
278
|
+
/* ================================================================
|
|
279
|
+
BREAKPOINTS
|
|
280
|
+
================================================================ */
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Breakpoint values in pixels, mirroring Tailwind v4 defaults
|
|
284
|
+
* except xl which is overridden to 1200px (75rem).
|
|
285
|
+
*
|
|
286
|
+
* Use these in JS contexts: ResizeObserver thresholds,
|
|
287
|
+
* matchMedia queries, chart responsive adjustments.
|
|
288
|
+
*
|
|
289
|
+
* Note: xl is 1200px not 1280px -- see globals.css @theme block.
|
|
290
|
+
*/
|
|
291
|
+
export const BREAKPOINTS = {
|
|
292
|
+
sm: 640,
|
|
293
|
+
md: 768,
|
|
294
|
+
lg: 1024,
|
|
295
|
+
xl: 1200, /* overridden from Tailwind default 1280 */
|
|
296
|
+
'2xl': 1536,
|
|
297
|
+
} as const;
|
|
298
|
+
|
|
299
|
+
export type BreakpointKey = keyof typeof BREAKPOINTS;
|
|
300
|
+
|
|
301
|
+
/* ================================================================
|
|
302
|
+
CHART HELPERS
|
|
303
|
+
================================================================ */
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Default Recharts chart margins using spacing tokens.
|
|
307
|
+
* Ensures consistent chart padding across all AreaChart/BarChart instances.
|
|
308
|
+
*/
|
|
309
|
+
export const CHART_MARGINS = {
|
|
310
|
+
top: SPACING[2], // 8px
|
|
311
|
+
right: SPACING[3], // 12px
|
|
312
|
+
bottom: SPACING[2], // 8px
|
|
313
|
+
left: SPACING[2], // 8px -- adjusted by formatY label width at runtime
|
|
314
|
+
} as const;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Recharts axis tick style using type and color tokens.
|
|
318
|
+
* Pass to <XAxis tickStyle={AXIS_TICK_STYLE}> etc.
|
|
319
|
+
*/
|
|
320
|
+
export const AXIS_TICK_STYLE = {
|
|
321
|
+
fill: COLORS.textMuted,
|
|
322
|
+
fontSize: 12, // matches --type-caption: 0.75rem
|
|
323
|
+
fontFamily: 'Inter Variable, system-ui, sans-serif',
|
|
324
|
+
} as const;
|