@nurix/etna 0.18.19 → 0.18.20
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/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/skills/design/SKILL.md +65 -0
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
- package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
- package/data/harnesses/code/skills/design/references/create.md +126 -0
- package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
- package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
- package/data/harnesses/code/skills/design/references/improve.md +85 -0
- package/data/harnesses/code/skills/design/references/interaction.md +77 -0
- package/data/harnesses/code/skills/design/references/introspect.md +100 -0
- package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
- package/data/harnesses/code/skills/design/references/personality.md +504 -0
- package/data/harnesses/code/skills/design/references/structure.md +138 -0
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
- package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
- package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
- package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema: design-md/v1
|
|
3
|
+
product: Nurix
|
|
4
|
+
provenance: default
|
|
5
|
+
sourced: 2026-07-04
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Nurix — the default operator design
|
|
9
|
+
|
|
10
|
+
The document a zero-input project wears. No project `design.md`, no confirmed library match, and
|
|
11
|
+
this is the brand personality synthesis derives from. It is a *deliberate* operator reading of the
|
|
12
|
+
Nurix brand — engineered, calm, dense-capable, never decorative — not a marketing transcription:
|
|
13
|
+
the brand's marketing surfaces breathe; an operator surface earns its density and keeps one accent.
|
|
14
|
+
Both machine blocks are fully valued (no `hint:`); every judgment away from the marketing brand is
|
|
15
|
+
annotated `# rationale:` where it lands.
|
|
16
|
+
|
|
17
|
+
## 1 Visual Theme
|
|
18
|
+
|
|
19
|
+
Light by default; dark is opt-in. Flat: hierarchy is carried by hairline `border`, type weight, and
|
|
20
|
+
color contrast — never by heavy shadow. A single accent, Nurix `primary` (vibrant blue), reserved for
|
|
21
|
+
the one action that matters on a surface. The neutral ramp is a cool slate that leans faintly toward
|
|
22
|
+
the brand's Deep Navy, so the whole UI reads engineered rather than flatly gray. The anchor surface is
|
|
23
|
+
a white content canvas beside a faintly cool-tinted `sidebar`. Density is `standard` and steps denser
|
|
24
|
+
per surface archetype — dense-capable, not cramped.
|
|
25
|
+
|
|
26
|
+
## 2 Color Roles
|
|
27
|
+
|
|
28
|
+
Light values first (the manifest carries the exact `oklch`; the brand hex is cited where it names the
|
|
29
|
+
identity). One accent — `primary` is the only chromatic hue that carries meaning; status hues speak
|
|
30
|
+
only status.
|
|
31
|
+
|
|
32
|
+
| Role | Value | Allowed where |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `background` / `card` / `popover` | white `oklch(1 0 0)` | page, cards, and overlays — the dominant surface |
|
|
35
|
+
| `foreground` | deep cool slate `oklch(0.2077 …)` | all primary ink |
|
|
36
|
+
| `muted` / `secondary` | faint cool tint `oklch(0.9683 …)` | hover rows, subtle fills, secondary buttons |
|
|
37
|
+
| `muted-foreground` | mid slate `oklch(0.5544 …)` | labels, captions, secondary text |
|
|
38
|
+
| `border` / `input` | hairline `oklch(0.9288 …)` (brand `#E2E8F0`) | every separator, grid line, and field edge |
|
|
39
|
+
| `primary` | vibrant blue `oklch(0.5211 …)` (brand `#245AE2`) | the one primary action; active nav; key interactive detail |
|
|
40
|
+
| `accent` | blue-tint `oklch(0.9603 …)` | selected/hovered rows and nav, on-accent surfaces only |
|
|
41
|
+
| `ring` | `primary` | the focus outline — unmistakable, because keyboard-first |
|
|
42
|
+
| `destructive` | red `oklch(0.5771 …)` | destructive actions and error state only |
|
|
43
|
+
| `success` / `warning` | green / amber | status signalling only, never chrome |
|
|
44
|
+
|
|
45
|
+
Electric Lime and Deep Navy are brand graces, not operator tokens: they carry no role here (see §7).
|
|
46
|
+
|
|
47
|
+
## 3 Typography
|
|
48
|
+
|
|
49
|
+
One sans, one mono. `font-sans` is Inter — the UI, body, and headings all ride it; heading weight and
|
|
50
|
+
tracking, not a second family, carry the display voice. `font-mono` is Geist Mono for IDs, codes,
|
|
51
|
+
timestamps, and log lines. Numeric columns render `tabular-nums` (Inter carries `tnum`) so figures
|
|
52
|
+
align down a column (floor P4).
|
|
53
|
+
|
|
54
|
+
| Step | Role | Size · line | Weight · tracking |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| h1 | page title | 24px / 1.25 | `font-weight-strong` · `heading-tracking` |
|
|
57
|
+
| h2 | section head | 18px / 1.3 | `font-weight-strong` · `heading-tracking` |
|
|
58
|
+
| h3 | card / group head | 15px / 1.35 | `font-weight-strong` · `heading-tracking` |
|
|
59
|
+
| body | UI + prose | 14px / 1.45 | `font-weight-normal` |
|
|
60
|
+
| small | labels, meta, table dense | 13px / 1.4 | `font-weight-normal` |
|
|
61
|
+
| caption | uppercase eyebrow | 12px / 1.2 | `font-weight-strong`, `+0.02em`, uppercase |
|
|
62
|
+
|
|
63
|
+
Bump weight one step for emphasis; never change size to emphasize.
|
|
64
|
+
|
|
65
|
+
## 4 Component Stylings
|
|
66
|
+
|
|
67
|
+
Every recurring operator component composes from the primitives — no literal values.
|
|
68
|
+
|
|
69
|
+
- **Button** — `primary`: filled `primary` on `primary-foreground`, `radius`, no pill. Secondary:
|
|
70
|
+
`secondary` fill, `border`, `foreground`. Ghost: transparent, `accent` on hover.
|
|
71
|
+
- **Input** — `background` fill, 1px `border` (`border-weight`), `radius`; focus draws the `ring` at
|
|
72
|
+
full 2px, no glow.
|
|
73
|
+
- **Card** — `card` surface, 1px `border`, `radius`, shadow only when lifted (`shadow-intensity`);
|
|
74
|
+
inset by `density-inset`.
|
|
75
|
+
- **Table row** — height `table-row-height`; `border` grid lines; `muted` on hover; `tabular-nums` on
|
|
76
|
+
numeric columns; row selection tints `accent`.
|
|
77
|
+
- **Side panel** — detail and edit open in the side panel (`side-panel-width`, `side-panel-width-rich`
|
|
78
|
+
for content-heavy), beside the work, not over it (floor P3).
|
|
79
|
+
- **Badge** — hairline `border`, `muted`/status fill, uppercase caption; status badges take
|
|
80
|
+
`success` / `warning` / `destructive`, never `primary`.
|
|
81
|
+
|
|
82
|
+
## 5 Layout
|
|
83
|
+
|
|
84
|
+
Base spacing unit 4px (Tailwind `--spacing` 0.25rem); the on-scale ramp is `2 · 4 · 8 · 12 · 16 · 24 ·
|
|
85
|
+
32 · 48 · 64`. Shell dimensions map one-to-one to the layout primitives: sidebar
|
|
86
|
+
`sidebar-width-expanded` / `sidebar-width-collapsed`, topbar `topbar-height`, side panel
|
|
87
|
+
`side-panel-width` / `side-panel-width-rich`, modal cap `modal-width-max`, forms `form-max-width`,
|
|
88
|
+
tables full-width. Density is governed globally by `density-tier` (rewriting the spacing scale through
|
|
89
|
+
`density-gap`) and stepped per archetype by the surface catalog — never by absolute overrides.
|
|
90
|
+
|
|
91
|
+
## 6 Depth
|
|
92
|
+
|
|
93
|
+
Flat. Hierarchy comes from the surface ladder and hairlines, not shadow:
|
|
94
|
+
|
|
95
|
+
- **0 — canvas** `background`, no border.
|
|
96
|
+
- **1 — panel / card** `card` on a 1px `border`; shadow only when it must lift off the canvas.
|
|
97
|
+
- **2 — overlay** `popover` / side panel / modal — the one place `shadow-intensity` earns a soft `md`
|
|
98
|
+
shadow. No level stacks a heavier shadow to signal rank; rank is border and position.
|
|
99
|
+
|
|
100
|
+
Focus is a 2px `ring` outline (never a glow), visible on every interactive element (floor P6).
|
|
101
|
+
Three z-levels: content, sticky chrome, overlay.
|
|
102
|
+
|
|
103
|
+
## 7 Do's & Don'ts
|
|
104
|
+
|
|
105
|
+
- **Do** reserve `primary` for the single most important action on a surface; everything else is
|
|
106
|
+
neutral or status.
|
|
107
|
+
- **Do** carry hierarchy with hairlines, weight, and whitespace — the brand is predominantly flat.
|
|
108
|
+
- **Do** apply `tabular-nums` to every numeric column and `font-mono` to IDs, codes, and timestamps.
|
|
109
|
+
- **Don't** promote a second chromatic accent. Electric Lime and Deep Navy stay brand-decorative — a
|
|
110
|
+
lime "success" reads yellow and fails contrast; status uses `success` / `warning` / `destructive`.
|
|
111
|
+
- **Don't** pill the primary CTA or reach for a radius outside `radius-scale`.
|
|
112
|
+
- **Don't** open detail or edit in a modal when the side panel fits — modals are for confirmations
|
|
113
|
+
only.
|
|
114
|
+
- **Don't** signal elevation with heavy drop shadows.
|
|
115
|
+
|
|
116
|
+
## 8 Responsive
|
|
117
|
+
|
|
118
|
+
The operator sweep is 375 / 768 / 1024 / 1440.
|
|
119
|
+
|
|
120
|
+
| Breakpoint | What changes |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| 375 | sidebar → off-canvas drawer; two-pane list+detail stacks; side panel → full sheet; density holds |
|
|
123
|
+
| 768 | sidebar collapses to the icon rail (`sidebar-width-collapsed`); tables scroll their overflow |
|
|
124
|
+
| 1024 | sidebar expands; two-pane queue+detail holds side by side |
|
|
125
|
+
| 1440 | content caps at `form-max-width` for forms; tables and dashboards use the full width |
|
|
126
|
+
|
|
127
|
+
## 9 Agent Prompt Guide
|
|
128
|
+
|
|
129
|
+
Directives the emitting agent follows verbatim:
|
|
130
|
+
|
|
131
|
+
- Lead every list surface with its table; lead every entity with its primary read, not its chrome.
|
|
132
|
+
- Accent only the primary action. If two things want the accent, one of them is not primary.
|
|
133
|
+
- Detail and edit go in the side panel; a modal is a confirmation, not a workspace (P3).
|
|
134
|
+
- Every numeric column is `tabular-nums`; every ID / code / timestamp is `font-mono` (P4).
|
|
135
|
+
- Every workflow completes from the keyboard; the `ring` focus outline is always visible (P6).
|
|
136
|
+
- Stay flat — hairline and weight before shadow. Reach past a hairline only for a real overlay.
|
|
137
|
+
- One accent. Lime and navy are not operator colors here.
|
|
138
|
+
|
|
139
|
+
```yaml design-primitives
|
|
140
|
+
radius: 6px # rationale: brand default 12px is marketing-surface soft; operator default tightens for a denser, engineered read (scale still offers 12px for large cards)
|
|
141
|
+
shadow-intensity: 0.4 # rationale: brand is predominantly flat; shadow only lifts true overlays
|
|
142
|
+
border-weight: 1px
|
|
143
|
+
density-gap: 1
|
|
144
|
+
density-inset: 1
|
|
145
|
+
font-sans: Inter, system-ui, sans-serif # rationale: brand's body font; the single sans slot serves dense body/table chrome, so Inter over the marketing display font Manrope
|
|
146
|
+
font-mono: Geist Mono, ui-monospace, SFMono-Regular, monospace # rationale: Geist (brand numeric family) monospace sibling — IDs, codes, log lines
|
|
147
|
+
font-weight-normal: 400 # rationale: brand base 500 is display-tuned; dense operator body reads cleaner at 400
|
|
148
|
+
font-weight-strong: 600
|
|
149
|
+
heading-tracking: -0.01em # rationale: brand display tracks 0; at operator heading sizes Inter reads tighter and more engineered
|
|
150
|
+
duration-unit: 50ms
|
|
151
|
+
duration-fast: 100ms
|
|
152
|
+
duration-normal: 150ms
|
|
153
|
+
duration-slow: 250ms
|
|
154
|
+
ease-default: cubic-bezier(0.4,0,0.2,1)
|
|
155
|
+
contrast-boost: 1
|
|
156
|
+
icon-stroke: 1.75 # rationale: one step under lucide's 2 for a refined operator stroke
|
|
157
|
+
icon-size-base: 16px
|
|
158
|
+
sidebar-width-expanded: 248px
|
|
159
|
+
sidebar-width-collapsed: 56px
|
|
160
|
+
topbar-height: 52px
|
|
161
|
+
side-panel-width: 420px
|
|
162
|
+
side-panel-width-rich: 640px
|
|
163
|
+
modal-width-max: 560px
|
|
164
|
+
form-max-width: 640px
|
|
165
|
+
table-row-height: 40px # rationale: comfortable at standard density; dense archetypes step down via the surface catalog
|
|
166
|
+
verbosity: default
|
|
167
|
+
feedback: subtle
|
|
168
|
+
density-tier: standard # rationale: a general operator default balances legibility and density; archetypes step denser, never the whole app
|
|
169
|
+
product-traits: [operator-console, crm, queue, dashboard, admin]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```yaml lever-manifest
|
|
173
|
+
version: 1
|
|
174
|
+
fonts: { sans: "Inter, system-ui, sans-serif", mono: "Geist Mono, ui-monospace, SFMono-Regular, monospace" }
|
|
175
|
+
color-tokens:
|
|
176
|
+
light:
|
|
177
|
+
background: "oklch(1 0 0)"
|
|
178
|
+
foreground: "oklch(0.2077 0.0398 265.75)"
|
|
179
|
+
card: "oklch(1 0 0)"
|
|
180
|
+
card-foreground: "oklch(0.2077 0.0398 265.75)"
|
|
181
|
+
muted: "oklch(0.9683 0.0069 247.9)"
|
|
182
|
+
muted-foreground: "oklch(0.5544 0.0407 257.42)"
|
|
183
|
+
border: "oklch(0.9288 0.0126 255.51)"
|
|
184
|
+
input: "oklch(0.9288 0.0126 255.51)"
|
|
185
|
+
ring: "oklch(0.5211 0.2145 263.89)"
|
|
186
|
+
primary: "oklch(0.5211 0.2145 263.89)"
|
|
187
|
+
primary-foreground: "oklch(1 0 0)"
|
|
188
|
+
accent: "oklch(0.9603 0.0144 264.5)"
|
|
189
|
+
accent-foreground: "oklch(0.5211 0.2145 263.89)"
|
|
190
|
+
destructive: "oklch(0.5771 0.2152 27.33)"
|
|
191
|
+
destructive-foreground: "oklch(1 0 0)"
|
|
192
|
+
success: "oklch(0.6271 0.1699 149.21)"
|
|
193
|
+
warning: "oklch(0.6658 0.1574 58.32)"
|
|
194
|
+
popover: "oklch(1 0 0)"
|
|
195
|
+
popover-foreground: "oklch(0.2077 0.0398 265.75)"
|
|
196
|
+
secondary: "oklch(0.9683 0.0069 247.9)"
|
|
197
|
+
secondary-foreground: "oklch(0.2077 0.0398 265.75)"
|
|
198
|
+
chart-1: "oklch(0.5211 0.2145 263.89)"
|
|
199
|
+
chart-2: "oklch(0.6089 0.1109 221.72)"
|
|
200
|
+
chart-3: "oklch(0.6271 0.1699 149.21)"
|
|
201
|
+
chart-4: "oklch(0.6658 0.1574 58.32)"
|
|
202
|
+
chart-5: "oklch(0.5413 0.2466 293.01)"
|
|
203
|
+
sidebar: "oklch(0.9842 0.0034 247.86)"
|
|
204
|
+
sidebar-foreground: "oklch(0.2077 0.0398 265.75)"
|
|
205
|
+
sidebar-primary: "oklch(0.5211 0.2145 263.89)"
|
|
206
|
+
sidebar-primary-foreground: "oklch(1 0 0)"
|
|
207
|
+
sidebar-accent: "oklch(0.9603 0.0144 264.5)"
|
|
208
|
+
sidebar-accent-foreground: "oklch(0.5211 0.2145 263.89)"
|
|
209
|
+
sidebar-border: "oklch(0.9288 0.0126 255.51)"
|
|
210
|
+
sidebar-ring: "oklch(0.5211 0.2145 263.89)"
|
|
211
|
+
spacing-scale: [2, 4, 8, 12, 16, 24, 32, 48, 64]
|
|
212
|
+
radius-scale: [0, 4, 6, 12, 9999]
|
|
213
|
+
shadow-levels: [none, sm, md]
|
|
214
|
+
motion: { durations: [100, 150, 250], easings: ["cubic-bezier(0.4,0,0.2,1)"] }
|
|
215
|
+
icons: { family: lucide, stroke: 1.75 }
|
|
216
|
+
accent-count: 1
|
|
217
|
+
```
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/command-center.tsx — personality-invariant: composition anchor, not a look source.
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Activity, AlertTriangle, ArrowDown, ArrowUp, Hand, Radio, RefreshCw, ShieldAlert, Signal, Undo2, WifiOff, type LucideIcon } from "lucide-react";
|
|
4
|
+
import { Button } from "@nurix/components/button";
|
|
5
|
+
import { Badge } from "@nurix/components/badge";
|
|
6
|
+
import { Card } from "@nurix/components/card";
|
|
7
|
+
import { Skeleton } from "@nurix/components/skeleton";
|
|
8
|
+
import { EmptyState } from "@nurix/components/empty-state";
|
|
9
|
+
import { Alert, AlertTitle, AlertDescription } from "@nurix/components/alert";
|
|
10
|
+
import { ScrollArea } from "@nurix/components/scroll-area";
|
|
11
|
+
import { Sparkline } from "@nurix/components/sparkline";
|
|
12
|
+
import { Kbd } from "@nurix/components/kbd";
|
|
13
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose } from "@nurix/components/dialog";
|
|
14
|
+
import { Statistics, StatisticsHeader, StatisticsHeading, StatisticsTitle, StatisticsMetric, StatisticsValue, StatisticsUnit, StatisticsTrend } from "@nurix/components/statistics";
|
|
15
|
+
type SurfaceState = "loading" | "ready" | "error";
|
|
16
|
+
type PanelStatus = "ok" | "degraded" | "down" | "loading";
|
|
17
|
+
type Sentiment = "negative" | "neutral" | "positive";
|
|
18
|
+
interface HealthPanel { key: string; label: string; status: PanelStatus; p95: number; trend: number[]; since: string; }
|
|
19
|
+
interface StreamEvent { id: string; at: string; session: string; kind: "started" | "escalated" | "resolved" | "dropped"; detail: string; }
|
|
20
|
+
interface Escalation { id: string; session: string; reason: string; sentiment: Sentiment; waitingSec: number; }
|
|
21
|
+
export interface CommandCenterProps {
|
|
22
|
+
state?: SurfaceState;
|
|
23
|
+
panels?: HealthPanel[];
|
|
24
|
+
events?: StreamEvent[];
|
|
25
|
+
escalations?: Escalation[];
|
|
26
|
+
updatedAt?: string;
|
|
27
|
+
onRetry?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STATUS_TONE: Record<PanelStatus, { text: string; word: string; Icon: LucideIcon }> = {
|
|
31
|
+
ok: { text: "text-success", word: "healthy", Icon: Signal },
|
|
32
|
+
degraded: { text: "text-warning", word: "degraded", Icon: AlertTriangle },
|
|
33
|
+
down: { text: "text-destructive", word: "down", Icon: WifiOff },
|
|
34
|
+
loading: { text: "text-muted-foreground", word: "probing", Icon: RefreshCw },
|
|
35
|
+
};
|
|
36
|
+
const SENTIMENT_TONE: Record<Sentiment, string> = { negative: "text-destructive", neutral: "text-muted-foreground", positive: "text-success" };
|
|
37
|
+
const KIND_LABEL: Record<StreamEvent["kind"], string> = { started: "Session started", escalated: "Escalated", resolved: "Resolved", dropped: "Call dropped" };
|
|
38
|
+
|
|
39
|
+
// Format a waiting duration in seconds as a compact mm:ss string.
|
|
40
|
+
function fmtWait(sec: number): string {
|
|
41
|
+
return `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`;
|
|
42
|
+
}
|
|
43
|
+
// One KPI tile in the health header; the numeric value carries tabular figures.
|
|
44
|
+
function KpiTile({ label, value, unit, trend }: { label: string; value: string; unit?: string; trend?: number }) {
|
|
45
|
+
return (
|
|
46
|
+
<Statistics variant="card" size="md">
|
|
47
|
+
<StatisticsHeader>
|
|
48
|
+
<StatisticsHeading><StatisticsTitle size="sm">{label}</StatisticsTitle></StatisticsHeading>
|
|
49
|
+
</StatisticsHeader>
|
|
50
|
+
<StatisticsMetric>
|
|
51
|
+
<StatisticsValue className="tabular-nums">{value}</StatisticsValue>
|
|
52
|
+
{unit ? <StatisticsUnit>{unit}</StatisticsUnit> : null}
|
|
53
|
+
{trend !== undefined ? <StatisticsTrend value={trend} pill /> : null}
|
|
54
|
+
</StatisticsMetric>
|
|
55
|
+
</Statistics>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
// One subsystem health panel; loads, degrades, and fails independently and names which.
|
|
59
|
+
function HealthTile({ panel }: { panel: HealthPanel }) {
|
|
60
|
+
if (panel.status === "loading") {
|
|
61
|
+
return (
|
|
62
|
+
<Card className="flex flex-col gap-3 p-4" role="status" aria-busy="true">
|
|
63
|
+
<Skeleton className="h-4 w-24" /><Skeleton className="h-7 w-16" /><Skeleton className="h-7 w-full" />
|
|
64
|
+
</Card>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const tone = STATUS_TONE[panel.status];
|
|
68
|
+
return (
|
|
69
|
+
<Card className="flex flex-col gap-2 p-4">
|
|
70
|
+
<div className="flex items-center justify-between">
|
|
71
|
+
<span className="text-sm font-medium text-foreground">{panel.label}</span>
|
|
72
|
+
<span className={`flex items-center gap-1 text-xs ${tone.text}`}><tone.Icon className="size-4" aria-hidden />{tone.word}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-baseline gap-1">
|
|
75
|
+
<span className="text-xl font-semibold tabular-nums text-foreground">{panel.p95}</span>
|
|
76
|
+
<span className="text-xs text-muted-foreground">ms p95</span>
|
|
77
|
+
</div>
|
|
78
|
+
<Sparkline points={panel.trend} type="line" height={28} className="w-full text-primary" />
|
|
79
|
+
{panel.status === "degraded" ? <p className="text-xs text-warning">Stale since {panel.since}; showing last known values.</p> : null}
|
|
80
|
+
{panel.status === "down" ? <p className="text-xs text-destructive">Probe timed out {panel.since} ago; retrying automatically.</p> : null}
|
|
81
|
+
</Card>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
// The live session-event stream; additions announce politely without stealing focus.
|
|
85
|
+
function StreamPane({ events }: { events: StreamEvent[] }) {
|
|
86
|
+
return (
|
|
87
|
+
<Card className="flex min-h-0 flex-col">
|
|
88
|
+
<div className="flex items-center gap-2 border-b border-border px-4 py-3">
|
|
89
|
+
<Activity className="size-4 text-primary" aria-hidden />
|
|
90
|
+
<h2 className="text-sm font-semibold text-foreground">Live session stream</h2>
|
|
91
|
+
<Badge variant="outline" className="ml-auto tabular-nums">{events.length}</Badge>
|
|
92
|
+
</div>
|
|
93
|
+
<ScrollArea className="min-h-0 flex-1">
|
|
94
|
+
<ul aria-live="polite" aria-relevant="additions" className="divide-y divide-border">
|
|
95
|
+
{events.map((e) => (
|
|
96
|
+
<li key={e.id} className="flex items-baseline gap-3 px-4 py-2 text-sm">
|
|
97
|
+
<time className="shrink-0 font-mono tabular-nums text-muted-foreground">{e.at}</time>
|
|
98
|
+
<span className="shrink-0 font-mono text-muted-foreground">{e.session}</span>
|
|
99
|
+
<span className="shrink-0 text-foreground">{KIND_LABEL[e.kind]}</span>
|
|
100
|
+
<span className="truncate text-muted-foreground">{e.detail}</span>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
</ScrollArea>
|
|
105
|
+
</Card>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
// The escalation queue: a sortable, keyboard-actionable table of sessions flagged for human takeover.
|
|
109
|
+
function EscalationTable({ rows, sort, onToggleSort, onTakeOver, onDismiss }: { rows: Escalation[]; sort: "asc" | "desc"; onToggleSort: () => void; onTakeOver: (e: Escalation) => void; onDismiss: (e: Escalation) => void }) {
|
|
110
|
+
return (
|
|
111
|
+
<table className="w-full border-collapse text-sm">
|
|
112
|
+
<thead className="sticky top-0 bg-card">
|
|
113
|
+
<tr className="border-b border-border text-left text-muted-foreground">
|
|
114
|
+
<th scope="col" className="px-3 py-2 font-medium">Session</th>
|
|
115
|
+
<th scope="col" className="px-3 py-2 font-medium">Reason</th>
|
|
116
|
+
<th scope="col" className="px-3 py-2 font-medium">Sentiment</th>
|
|
117
|
+
<th scope="col" aria-sort={sort === "asc" ? "ascending" : "descending"} className="px-3 py-2 text-right font-medium">
|
|
118
|
+
<button type="button" onClick={onToggleSort} className="ml-auto inline-flex items-center gap-1 rounded focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">Waiting{sort === "asc" ? <ArrowUp className="size-4" aria-hidden /> : <ArrowDown className="size-4" aria-hidden />}</button>
|
|
119
|
+
</th>
|
|
120
|
+
<th scope="col" className="px-3 py-2 text-right font-medium">Actions</th>
|
|
121
|
+
</tr>
|
|
122
|
+
</thead>
|
|
123
|
+
<tbody>
|
|
124
|
+
{rows.map((e) => (
|
|
125
|
+
<tr key={e.id} className="border-b border-border">
|
|
126
|
+
<td className="px-3 py-2"><span className="font-mono tabular-nums text-foreground">{e.session}</span></td>
|
|
127
|
+
<td className="px-3 py-2 text-muted-foreground"><span className="line-clamp-1">{e.reason}</span></td>
|
|
128
|
+
<td className="px-3 py-2"><span className={SENTIMENT_TONE[e.sentiment]}>{e.sentiment}</span></td>
|
|
129
|
+
<td className="px-3 py-2 text-right"><span className="font-mono tabular-nums text-foreground">{fmtWait(e.waitingSec)}</span></td>
|
|
130
|
+
<td className="px-3 py-2">
|
|
131
|
+
<div className="flex items-center justify-end gap-2">
|
|
132
|
+
<Button size="sm" onClick={() => onTakeOver(e)}><Hand className="size-4" aria-hidden />Take over</Button>
|
|
133
|
+
<Button size="sm" variant="ghost" onClick={() => onDismiss(e)}>Dismiss</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</td>
|
|
136
|
+
</tr>
|
|
137
|
+
))}
|
|
138
|
+
</tbody>
|
|
139
|
+
</table>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// The whole-surface loading state: skeleton tiles mirror the KPI, health, and pane grid.
|
|
143
|
+
function CommandSkeleton() {
|
|
144
|
+
return (
|
|
145
|
+
<div className="flex flex-1 flex-col gap-4" role="status" aria-busy="true">
|
|
146
|
+
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-20 w-full" />)}</div>
|
|
147
|
+
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-28 w-full" />)}</div>
|
|
148
|
+
<div className="grid min-h-64 flex-1 grid-cols-1 gap-4 lg:grid-cols-2"><Skeleton className="h-full w-full" /><Skeleton className="h-full w-full" /></div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
// Voice-AI live-ops bridge: monitor concurrent sessions, detect escalations, intervene under time pressure.
|
|
153
|
+
export function CommandCenter({ state = "ready", panels = SEED_PANELS, events = SEED_EVENTS, escalations = SEED_ESCALATIONS, updatedAt = "14:32:07", onRetry }: CommandCenterProps) {
|
|
154
|
+
const [sort, setSort] = useState<"asc" | "desc">("desc");
|
|
155
|
+
const [active, setActive] = useState<Escalation | null>(null);
|
|
156
|
+
const [handled, setHandled] = useState<{ id: string; session: string; verb: string } | null>(null);
|
|
157
|
+
const rows = useMemo(() => {
|
|
158
|
+
const visible = handled ? escalations.filter((e) => e.id !== handled.id) : escalations;
|
|
159
|
+
return [...visible].sort((a, b) => (sort === "asc" ? a.waitingSec - b.waitingSec : b.waitingSec - a.waitingSec));
|
|
160
|
+
}, [escalations, handled, sort]);
|
|
161
|
+
return (
|
|
162
|
+
<main data-density-step="tighter" className="flex min-h-[100dvh] flex-col gap-4 bg-background p-4 text-foreground">
|
|
163
|
+
<header className="flex flex-wrap items-center gap-3">
|
|
164
|
+
<Radio className="size-5 text-primary" aria-hidden />
|
|
165
|
+
<h1 className="text-lg font-semibold">Voice operations bridge</h1>
|
|
166
|
+
<span className="flex items-center gap-1 text-sm text-success"><Signal className="size-4" aria-hidden />Live</span>
|
|
167
|
+
<span className="ml-auto flex items-center gap-2 text-muted-foreground">Updated <time className="font-mono tabular-nums">{updatedAt}</time><Kbd>/</Kbd>to search</span>
|
|
168
|
+
</header>
|
|
169
|
+
{state === "loading" ? <CommandSkeleton /> : state === "error" ? (
|
|
170
|
+
<div className="flex flex-1 items-center justify-center">
|
|
171
|
+
<Alert variant="destructive" className="max-w-md">
|
|
172
|
+
<AlertTitle>Telemetry feed unavailable</AlertTitle>
|
|
173
|
+
<AlertDescription>The bridge lost its link to the session gateway, so live signal is not arriving. Reconnect to resume monitoring.</AlertDescription>
|
|
174
|
+
<Button size="sm" variant="outline" onClick={onRetry} className="mt-3"><RefreshCw className="size-4" aria-hidden />Reconnect</Button>
|
|
175
|
+
</Alert>
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<>
|
|
179
|
+
<section aria-label="Key metrics" className="grid grid-cols-2 gap-4 lg:grid-cols-4">{/* mock: illustrative KPI figures */}
|
|
180
|
+
<KpiTile label="Active sessions" value="128" trend={6} />
|
|
181
|
+
<KpiTile label="Escalations waiting" value={String(rows.length)} trend={-3} />
|
|
182
|
+
<KpiTile label="Median handle time" value="2:41" unit="min" />
|
|
183
|
+
<KpiTile label="Containment rate" value="72" unit="%" trend={2} />
|
|
184
|
+
</section>
|
|
185
|
+
<section aria-label="Subsystem health" className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
186
|
+
{panels.map((p) => <HealthTile key={p.key} panel={p} />)}
|
|
187
|
+
</section>
|
|
188
|
+
<section aria-label="Live operations" className="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-2">
|
|
189
|
+
<StreamPane events={events} />
|
|
190
|
+
<Card className="flex min-h-0 flex-col">
|
|
191
|
+
<div className="flex items-center gap-2 border-b border-border px-4 py-3">
|
|
192
|
+
<ShieldAlert className="size-4 text-primary" aria-hidden />
|
|
193
|
+
<h2 className="text-sm font-semibold text-foreground">Escalation queue</h2>
|
|
194
|
+
<Badge variant="outline" className="ml-auto tabular-nums">{rows.length}</Badge>
|
|
195
|
+
</div>
|
|
196
|
+
<ScrollArea className="min-h-0 flex-1">
|
|
197
|
+
{rows.length === 0 ? (
|
|
198
|
+
<EmptyState icon={<ShieldAlert className="size-8 text-muted-foreground" aria-hidden />} title="No escalations waiting" description="Every live session is being handled by the AI agent. Flagged sessions surface here for human takeover." />
|
|
199
|
+
) : (
|
|
200
|
+
<EscalationTable rows={rows} sort={sort} onToggleSort={() => setSort((s) => (s === "asc" ? "desc" : "asc"))} onTakeOver={setActive} onDismiss={(e) => setHandled({ id: e.id, session: e.session, verb: "Dismissed" })} />
|
|
201
|
+
)}
|
|
202
|
+
</ScrollArea>
|
|
203
|
+
</Card>
|
|
204
|
+
</section>
|
|
205
|
+
{handled ? (
|
|
206
|
+
<div role="status" className="flex items-center gap-3 rounded-md border border-border bg-muted px-4 py-2 text-sm text-foreground">
|
|
207
|
+
<span>{handled.verb} session <span className="font-mono tabular-nums">{handled.session}</span>.</span>
|
|
208
|
+
<Button size="sm" variant="ghost" className="ml-auto" onClick={() => setHandled(null)}><Undo2 className="size-4" aria-hidden />Undo</Button>
|
|
209
|
+
</div>
|
|
210
|
+
) : null}
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
<Dialog open={!!active} onOpenChange={(open) => { if (!open) setActive(null); }}>
|
|
214
|
+
<DialogContent variant="danger" size="sm">
|
|
215
|
+
<DialogHeader>
|
|
216
|
+
<DialogTitle>Take over session {active?.session}?</DialogTitle>
|
|
217
|
+
<DialogDescription>The AI agent will be muted and the caller connected to a human. The current turn is interrupted immediately.</DialogDescription>
|
|
218
|
+
</DialogHeader>
|
|
219
|
+
<DialogBody className="text-sm text-muted-foreground">Flagged for: {active?.reason} Waiting {active ? fmtWait(active.waitingSec) : ""}.</DialogBody>
|
|
220
|
+
<DialogFooter>
|
|
221
|
+
<DialogClose asChild><Button variant="ghost" size="sm">Cancel</Button></DialogClose>
|
|
222
|
+
<Button size="sm" onClick={() => { if (active) setHandled({ id: active.id, session: active.session, verb: "Took over" }); setActive(null); }}><Hand className="size-4" aria-hidden />Take over session</Button>
|
|
223
|
+
</DialogFooter>
|
|
224
|
+
</DialogContent>
|
|
225
|
+
</Dialog>
|
|
226
|
+
</main>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
// exemplar fixture data — seeded, domain-shaped
|
|
230
|
+
const SEED_PANELS: HealthPanel[] = [
|
|
231
|
+
{ key: "asr", label: "Speech recognition", status: "ok", p95: 214, trend: [190, 205, 198, 212, 208, 214], since: "" },
|
|
232
|
+
{ key: "llm", label: "Language model", status: "degraded", p95: 1180, trend: [420, 610, 880, 1020, 1140, 1180], since: "14:29" },
|
|
233
|
+
{ key: "tts", label: "Speech synthesis", status: "ok", p95: 96, trend: [88, 92, 90, 94, 91, 96], since: "" },
|
|
234
|
+
{ key: "tel", label: "Telephony gateway", status: "down", p95: 0, trend: [140, 150, 0, 0, 0, 0], since: "2m" },
|
|
235
|
+
];
|
|
236
|
+
const SEED_EVENTS: StreamEvent[] = [
|
|
237
|
+
{ id: "v1", at: "14:32:07", session: "cs-4821", kind: "escalated", detail: "Three low-confidence recognition turns on a billing dispute; caller growing frustrated." },
|
|
238
|
+
{ id: "v2", at: "14:32:01", session: "cs-4790", kind: "resolved", detail: "Refund confirmed, transcript archived." },
|
|
239
|
+
{ id: "v3", at: "14:31:44", session: "cs-4818", kind: "started", detail: "Inbound, plan upgrade intent." },
|
|
240
|
+
{ id: "v4", at: "14:31:22", session: "cs-4802", kind: "dropped", detail: "Carrier RTP timeout on the telephony gateway before the greeting completed." },
|
|
241
|
+
{ id: "v5", at: "14:31:05", session: "cs-4795", kind: "escalated", detail: "Explicit request for a human agent." },
|
|
242
|
+
];
|
|
243
|
+
const SEED_ESCALATIONS: Escalation[] = [
|
|
244
|
+
{ id: "e-01", session: "cs-4821", reason: "Three low-confidence recognition turns on a billing dispute; caller growing frustrated.", sentiment: "negative", waitingSec: 187 },
|
|
245
|
+
{ id: "e-02", session: "cs-4795", reason: "Explicit request for a human agent.", sentiment: "neutral", waitingSec: 94 },
|
|
246
|
+
{ id: "e-03", session: "cs-4766", reason: "Sentiment dropped after a failed address verification.", sentiment: "negative", waitingSec: 63 },
|
|
247
|
+
{ id: "e-04", session: "cs-4740", reason: "Refund amount exceeds the agent's autonomous limit.", sentiment: "neutral", waitingSec: 41 },
|
|
248
|
+
{ id: "e-05", session: "cs-4712", reason: "Caller confirmed resolution; awaiting supervisor sign-off.", sentiment: "positive", waitingSec: 12 },
|
|
249
|
+
];
|