@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.
Files changed (94) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +2 -2
  3. package/data/harnesses/code/skills/design/SKILL.md +65 -0
  4. package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
  5. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
  6. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
  7. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
  8. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
  9. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
  10. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
  11. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
  12. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
  13. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
  14. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
  15. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
  16. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
  17. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
  18. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
  19. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
  20. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
  21. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
  22. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
  23. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
  24. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
  25. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
  26. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
  27. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
  28. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
  29. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
  30. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
  31. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
  32. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
  33. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
  34. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
  35. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
  36. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
  37. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
  38. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
  39. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
  40. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
  41. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
  42. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
  43. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
  44. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
  45. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
  46. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
  47. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
  48. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
  49. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
  50. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
  51. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
  52. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
  53. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
  54. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
  55. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
  56. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
  57. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
  58. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
  59. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
  60. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
  61. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
  62. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
  63. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
  64. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
  65. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
  66. package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
  67. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
  68. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
  69. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
  70. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
  71. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
  72. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
  73. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
  74. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
  75. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
  76. package/data/harnesses/code/skills/design/references/create.md +126 -0
  77. package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
  78. package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
  79. package/data/harnesses/code/skills/design/references/improve.md +85 -0
  80. package/data/harnesses/code/skills/design/references/interaction.md +77 -0
  81. package/data/harnesses/code/skills/design/references/introspect.md +100 -0
  82. package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
  83. package/data/harnesses/code/skills/design/references/personality.md +504 -0
  84. package/data/harnesses/code/skills/design/references/structure.md +138 -0
  85. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
  86. package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
  87. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
  88. package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
  89. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
  90. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
  91. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
  92. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
  93. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
  94. package/package.json +1 -1
@@ -0,0 +1,245 @@
1
+ // skills/design/data/operator-pack/exemplars/entity-detail.tsx
2
+ // personality-invariant: composition anchor, not a look source.
3
+ import { useEffect, useState } from "react";
4
+ import { Button } from "@nurix/components/button";
5
+ import { Badge } from "@nurix/components/badge";
6
+ import { Card, CardContent } 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 { Label } from "@nurix/components/label";
11
+ import { Kbd } from "@nurix/components/kbd";
12
+ import { Input } from "@nurix/components/input";
13
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@nurix/components/tabs";
14
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose } from "@nurix/components/sheet";
15
+ import { Table, defineColumn, type ColumnDefinition } from "@nurix/components/table";
16
+ import { Boxes, CircleCheck, CircleX, Pencil, RefreshCw, Rocket, ScrollText, TriangleAlert } from "lucide-react";
17
+ // A deployment-console service: a long-lived entity with many attributes, related instances, and deployment history (entity-detail).
18
+ type ServiceHealth = "healthy" | "degraded" | "down";
19
+ type SaveState = "idle" | "saving" | "saved" | "conflict";
20
+ type LoadStatus = "ready" | "loading" | "error";
21
+ export interface Service {
22
+ id: string; name: string; slug: string; environment: string; health: ServiceHealth;
23
+ owner: string; runtime: string; region: string; replicas: number; lastDeployedAt: string;
24
+ }
25
+ export interface Instance { id: string; region: string; version: string; status: string; cpuPercent: number; startedAt: string }
26
+ export interface Deployment { id: string; version: string; actor: string; status: "succeeded" | "failed" | "rolled_back"; at: string }
27
+ export interface ConfigPatch { replicas: number }
28
+ export interface EntityDetailProps {
29
+ service: Service | null; instances: Instance[]; deployments: Deployment[];
30
+ status?: LoadStatus; metricsStale?: boolean; errorDetail?: string; saveState?: SaveState;
31
+ onRetry?: () => void; onDeploy?: () => void; onSaveConfig?: (patch: ConfigPatch) => void;
32
+ }
33
+ const HEALTH_META: Record<ServiceHealth, { label: string; cls: string; Icon: typeof CircleCheck }> = {
34
+ healthy: { label: "Healthy", cls: "text-success", Icon: CircleCheck },
35
+ degraded: { label: "Degraded", cls: "text-warning", Icon: TriangleAlert },
36
+ down: { label: "Down", cls: "text-destructive", Icon: CircleX },
37
+ };
38
+ const DEPLOY_LABEL: Record<Deployment["status"], string> = { succeeded: "Deployed", failed: "Deploy failed", rolled_back: "Rolled back" };
39
+ const INSTANCE_STATUS = [{ value: "running", label: "Running" }, { value: "starting", label: "Starting" }, { value: "unknown", label: "Unknown" }];
40
+ const instanceColumns: ColumnDefinition<Instance>[] = [
41
+ defineColumn<Instance>({ slug: "id", label: "Instance", type: "text", cell: (r) => <span className="font-mono tabular-nums">{r.id}</span> }),
42
+ defineColumn<Instance>({ slug: "region", label: "Region", type: "text" }),
43
+ defineColumn<Instance>({ slug: "version", label: "Version", type: "text", cell: (r) => <span className="font-mono">{r.version}</span> }),
44
+ defineColumn<Instance>({ slug: "status", label: "Status", type: "enum", component: "status", metadata: { options: INSTANCE_STATUS } }),
45
+ defineColumn<Instance>({ slug: "cpuPercent", label: "CPU", type: "number", metadata: { suffix: "%" } }),
46
+ defineColumn<Instance>({ slug: "startedAt", label: "Started", type: "date", metadata: { relativeFormat: true } }),
47
+ ];
48
+ // Loading shell mirrors the header identity and tab bar, never a centered spinner.
49
+ function DetailSkeleton() {
50
+ return (
51
+ <div className="min-h-[100dvh] bg-background p-6">
52
+ <div className="flex items-center gap-3">
53
+ <Skeleton className="h-10 w-10 rounded-md" />
54
+ <div className="flex flex-col gap-2"><Skeleton className="h-5 w-48" /><Skeleton className="h-4 w-32" /></div>
55
+ </div>
56
+ <div className="mt-6 flex gap-2"><Skeleton className="h-8 w-24" /><Skeleton className="h-8 w-24" /><Skeleton className="h-8 w-24" /></div>
57
+ <div className="mt-6"><Skeleton className="h-40 w-full" /></div>
58
+ </div>
59
+ );
60
+ }
61
+ // Entity load failure states what happened, why, and the recovery path. Never a dead end.
62
+ function DetailError({ detail, onRetry }: { detail?: string; onRetry?: () => void }) {
63
+ return (
64
+ <div className="flex min-h-[100dvh] items-center justify-center bg-background p-6">
65
+ <Alert variant="destructive" className="max-w-md">
66
+ <CircleX className="h-4 w-4" />
67
+ <AlertTitle>Could not load this service</AlertTitle>
68
+ <AlertDescription>{detail ?? "The deployment API did not respond."} Retry the request, or open the platform status page if it keeps failing.</AlertDescription>
69
+ <div className="mt-3"><Button size="sm" onClick={onRetry}><RefreshCw className="h-4 w-4" /> Retry</Button></div>
70
+ </Alert>
71
+ </div>
72
+ );
73
+ }
74
+ // Config edit opens beside the work; the save path narrates saving, saved, and the concurrent-edit conflict.
75
+ function EditConfigSheet({ service, open, onOpenChange, saveState, onSave }: {
76
+ service: Service; open: boolean; onOpenChange: (v: boolean) => void; saveState: SaveState; onSave?: (patch: ConfigPatch) => void;
77
+ }) {
78
+ const [replicas, setReplicas] = useState(String(service.replicas));
79
+ const [replicaError, setReplicaError] = useState<string | null>(null);
80
+ const saving = saveState === "saving";
81
+ return (
82
+ <Sheet open={open} onOpenChange={onOpenChange}>
83
+ <SheetContent side="right">
84
+ <SheetHeader>
85
+ <SheetTitle>Edit configuration</SheetTitle>
86
+ <SheetDescription>Changes apply on the next deploy of {service.slug}.</SheetDescription>
87
+ </SheetHeader>
88
+ <SheetBody className="flex flex-col gap-4">
89
+ {saveState === "conflict" && (
90
+ <Alert variant="destructive">
91
+ <TriangleAlert className="h-4 w-4" />
92
+ <AlertTitle>Configuration changed elsewhere</AlertTitle>
93
+ <AlertDescription>Another operator saved this service while you were editing. Reload to see their values before saving yours.</AlertDescription>
94
+ </Alert>
95
+ )}
96
+ <div className="flex flex-col gap-2">
97
+ <Label htmlFor="replicas">Desired replicas</Label>
98
+ <Input id="replicas" inputMode="numeric" value={replicas} aria-invalid={!!replicaError} onChange={(e) => setReplicas(e.target.value)}
99
+ onBlur={() => { const n = Number(replicas); setReplicaError(!Number.isInteger(n) || n < 1 ? "Enter a whole number of one or more." : null); }} />
100
+ {replicaError && <p className="text-sm text-destructive">{replicaError}</p>}
101
+ </div>
102
+ <p role="status" aria-live="polite" className="min-h-4 text-sm text-muted-foreground">
103
+ {saveState === "saving" && "Saving configuration…"}
104
+ {saveState === "saved" && "Configuration saved."}
105
+ </p>
106
+ </SheetBody>
107
+ <SheetFooter>
108
+ <SheetClose asChild><Button variant="secondary" size="sm">Cancel</Button></SheetClose>
109
+ <Button size="sm" disabled={saving || !!replicaError} onClick={() => onSave?.({ replicas: Number(replicas) })}>
110
+ {saving && <RefreshCw className="h-4 w-4" />} Save changes
111
+ </Button>
112
+ </SheetFooter>
113
+ </SheetContent>
114
+ </Sheet>
115
+ );
116
+ }
117
+ // Service entity-detail: identity header, tabbed overview / instances / activity, edit in a side panel.
118
+ export function EntityDetail(props: EntityDetailProps) {
119
+ const { service, instances, deployments, status = "ready", metricsStale = false, errorDetail, saveState = "idle", onRetry, onDeploy, onSaveConfig } = props;
120
+ const [tab, setTab] = useState("overview");
121
+ const [editOpen, setEditOpen] = useState(false);
122
+ // Keyboard parity for the Edit affordance: press E to open the config panel.
123
+ useEffect(() => {
124
+ function onKey(e: KeyboardEvent) {
125
+ const el = e.target as HTMLElement | null;
126
+ if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA")) return;
127
+ if (e.key === "e" && !editOpen) { e.preventDefault(); setEditOpen(true); }
128
+ }
129
+ window.addEventListener("keydown", onKey);
130
+ return () => window.removeEventListener("keydown", onKey);
131
+ }, [editOpen]);
132
+ if (status === "loading") return <DetailSkeleton />;
133
+ if (status === "error" || !service) return <DetailError detail={errorDetail} onRetry={onRetry} />;
134
+ const health = HEALTH_META[service.health];
135
+ const attributes: { label: string; value: string; mono?: boolean; num?: boolean }[] = [
136
+ { label: "Environment", value: service.environment }, { label: "Runtime", value: service.runtime, mono: true },
137
+ { label: "Region", value: service.region, mono: true }, { label: "Owner", value: service.owner },
138
+ { label: "Desired replicas", value: String(service.replicas), num: true }, { label: "Last deployed", value: new Date(service.lastDeployedAt).toLocaleString(), num: true },
139
+ ];
140
+ return (
141
+ <div className="min-h-[100dvh] bg-background text-foreground">
142
+ <header className="flex flex-wrap items-start justify-between gap-4 border-b border-border p-6">
143
+ <div className="flex items-start gap-3">
144
+ <span className="grid h-10 w-10 place-items-center rounded-md bg-muted text-muted-foreground"><Boxes className="h-5 w-5" /></span>
145
+ <div>
146
+ <div className="flex items-center gap-2">
147
+ <h1 className="text-xl font-medium">{service.name}</h1>
148
+ <Badge variant="outline" className={`gap-1 ${health.cls}`}><health.Icon className="h-3.5 w-3.5" /> {health.label}</Badge>
149
+ </div>
150
+ <p className="mt-1 text-sm text-muted-foreground">{service.slug} · {service.environment}</p>
151
+ </div>
152
+ </div>
153
+ <div className="flex items-center gap-2">
154
+ <Button variant="secondary" size="sm" onClick={() => setEditOpen(true)}><Pencil className="h-4 w-4" /> Edit config <Kbd className="ml-1">E</Kbd></Button>
155
+ <Button size="sm" onClick={onDeploy}><Rocket className="h-4 w-4" /> Deploy</Button>
156
+ </div>
157
+ </header>
158
+ <div className="p-6">
159
+ {metricsStale && (
160
+ <Alert className="mb-4">
161
+ <TriangleAlert className="h-4 w-4 text-warning" />
162
+ <AlertTitle>Live health signals unavailable</AlertTitle>
163
+ <AlertDescription>The health collector is unreachable, so instance status and the figures below reflect the last cached snapshot and may be stale.</AlertDescription>
164
+ </Alert>
165
+ )}
166
+ <Tabs value={tab} onValueChange={setTab}>
167
+ <TabsList>
168
+ <TabsTrigger value="overview">Overview</TabsTrigger>
169
+ <TabsTrigger value="instances">Instances</TabsTrigger>
170
+ <TabsTrigger value="activity">Activity</TabsTrigger>
171
+ </TabsList>
172
+ <TabsContent value="overview">
173
+ <section className="mt-4">
174
+ <h2 className="mb-3 text-sm font-medium">Configuration <span className="text-xs font-normal text-muted-foreground">sample data</span></h2>
175
+ <Card><CardContent className="p-0">
176
+ {/* mock: fixture-supplied values, not authoritative production readings */}
177
+ <dl className="divide-y divide-border">
178
+ {attributes.map((a) => (
179
+ <div key={a.label} className="flex items-center justify-between gap-4 px-4 py-3 text-sm">
180
+ <dt className="text-muted-foreground">{a.label}</dt>
181
+ <dd className={`${a.mono ? "font-mono" : ""} ${a.num ? "tabular-nums" : ""}`}>{a.value}</dd>
182
+ </div>
183
+ ))}
184
+ </dl>
185
+ </CardContent></Card>
186
+ </section>
187
+ </TabsContent>
188
+ <TabsContent value="instances">
189
+ <div className="mt-4">
190
+ {instances.length === 0 ? (
191
+ <EmptyState icon={<Boxes className="h-10 w-10 text-muted-foreground" />} title="No running instances"
192
+ description="This service has no instances in this environment yet. Deploy a build to bring the first one online."
193
+ action={<Button size="sm" onClick={onDeploy}><Rocket className="h-4 w-4" /> Deploy</Button>} />
194
+ ) : (
195
+ <Table data={instances} columns={instanceColumns} tableConfig={{ header: true, density: "compact" }} />
196
+ )}
197
+ </div>
198
+ </TabsContent>
199
+ <TabsContent value="activity">
200
+ <div className="mt-4">
201
+ {deployments.length === 0 ? (
202
+ <EmptyState icon={<ScrollText className="h-10 w-10 text-muted-foreground" />} title="No deployments recorded"
203
+ description="Deployment history for this service appears here once the first build ships." />
204
+ ) : (
205
+ <ol className="flex flex-col">
206
+ {deployments.map((d) => (
207
+ <li key={d.id} className="flex items-center gap-3 border-b border-border py-3 text-sm last:border-0">
208
+ {d.status === "succeeded" ? <CircleCheck className="h-4 w-4 text-success" /> : <CircleX className="h-4 w-4 text-destructive" />}
209
+ <span className="font-mono">{d.version}</span>
210
+ <span className="text-muted-foreground">{DEPLOY_LABEL[d.status]} by {d.actor}</span>
211
+ <time className="ml-auto font-mono tabular-nums text-muted-foreground">{new Date(d.at).toLocaleString()}</time>
212
+ </li>
213
+ ))}
214
+ </ol>
215
+ )}
216
+ </div>
217
+ </TabsContent>
218
+ </Tabs>
219
+ </div>
220
+ <EditConfigSheet service={service} open={editOpen} onOpenChange={setEditOpen} saveState={saveState} onSave={onSaveConfig} />
221
+ </div>
222
+ );
223
+ }
224
+ // exemplar fixture data — seeded, domain-shaped
225
+ export const FIXTURE_SEED = 7314;
226
+ export const service: Service = {
227
+ id: "svc_0e9c1a7b", name: "checkout-api", slug: "checkout-api", environment: "production", health: "degraded",
228
+ owner: "payments-platform", runtime: "node20", region: "iad", replicas: 6, lastDeployedAt: "2026-07-03T14:22:00Z",
229
+ };
230
+ export const instances: Instance[] = [
231
+ { id: "checkout-api-7f4b21", region: "iad", version: "2026.7.3-a19f", status: "running", cpuPercent: 34, startedAt: "2026-07-03T14:23:00Z" },
232
+ { id: "checkout-api-9c02de", region: "iad", version: "2026.7.3-a19f", status: "running", cpuPercent: 51, startedAt: "2026-07-03T14:23:00Z" },
233
+ { id: "checkout-api-b73a10", region: "sfo", version: "2026.7.3-a19f", status: "running", cpuPercent: 47, startedAt: "2026-07-03T14:24:00Z" },
234
+ { id: "checkout-api-1de8ff", region: "sfo", version: "2026.6.28-c4e2-hotfix-billing-migration", status: "starting", cpuPercent: 0, startedAt: "2026-07-04T02:11:00Z" },
235
+ { id: "checkout-api-0000aa", region: "fra", version: "2026.6.28-c4e2", status: "unknown", cpuPercent: 0, startedAt: "2026-07-02T09:05:00Z" },
236
+ { id: "checkout-api-55b9c1", region: "fra", version: "2026.7.3-a19f", status: "running", cpuPercent: 62, startedAt: "2026-07-03T14:25:00Z" },
237
+ ];
238
+ export const deployments: Deployment[] = [
239
+ { id: "dep_5a1", version: "2026.7.3-a19f", actor: "m.okonkwo", status: "succeeded", at: "2026-07-03T14:22:00Z" },
240
+ { id: "dep_4f7", version: "2026.7.1-77b0", actor: "priya.nair", status: "rolled_back", at: "2026-07-01T18:40:00Z" },
241
+ { id: "dep_4e2", version: "2026.6.28-c4e2", actor: "s.delacroix", status: "failed", at: "2026-06-28T22:05:00Z" },
242
+ { id: "dep_4b0", version: "2026.6.20-11ad", actor: "m.okonkwo", status: "succeeded", at: "2026-06-20T10:33:00Z" },
243
+ ];
244
+ export const emptyInstances: Instance[] = [];
245
+ export const emptyDeployments: Deployment[] = [];
@@ -0,0 +1,223 @@
1
+ // skills/design/data/operator-pack/exemplars/list-detail.tsx
2
+ // personality-invariant: composition anchor, not a look source.
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { Button } from "@nurix/components/button";
5
+ import { Badge } from "@nurix/components/badge";
6
+ import { Card, CardContent } 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 { Input } from "@nurix/components/input";
11
+ import { Kbd } from "@nurix/components/kbd";
12
+ import { Checkbox } from "@nurix/components/checkbox";
13
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose } from "@nurix/components/sheet";
14
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@nurix/components/native-select";
15
+ import { ArrowDown, ArrowUp, CircleCheck, CircleDot, CircleX, Inbox, Plus, RefreshCw, TriangleAlert } from "lucide-react";
16
+ // A support-ticket queue: many rows of one entity, each with a per-row action and lifecycle (list-detail, the reference archetype).
17
+ type Priority = "urgent" | "high" | "normal";
18
+ type Status = "open" | "pending" | "resolved";
19
+ type LoadStatus = "ready" | "loading" | "error";
20
+ export interface Ticket {
21
+ id: string; subject: string; requester: string; priority: Priority; status: Status; assignee: string | null; slaMinutes: number; updatedAt: string;
22
+ }
23
+ export interface ListDetailProps {
24
+ tickets: Ticket[]; status?: LoadStatus; stale?: boolean; errorDetail?: string;
25
+ onRetry?: () => void; onNew?: () => void; onResolve?: (id: string) => void; onAssignSelected?: (ids: string[]) => void;
26
+ }
27
+ const PAGE_SIZE = 8;
28
+ const STATUS_META: Record<Status, { label: string; cls: string; Icon: typeof CircleDot }> = {
29
+ open: { label: "Open", cls: "text-warning", Icon: CircleDot },
30
+ pending: { label: "Pending", cls: "text-muted-foreground", Icon: CircleDot },
31
+ resolved: { label: "Resolved", cls: "text-success", Icon: CircleCheck },
32
+ };
33
+ const PRIORITY_META: Record<Priority, { label: string; cls: string }> = {
34
+ urgent: { label: "Urgent", cls: "text-destructive" }, high: { label: "High", cls: "text-warning" }, normal: { label: "Normal", cls: "text-muted-foreground" },
35
+ };
36
+ // Loading shell mirrors the toolbar and table rows, never a centered spinner.
37
+ function QueueSkeleton() {
38
+ return (
39
+ <div className="min-h-[100dvh] bg-background p-6">
40
+ <div className="flex items-center gap-3"><Skeleton className="h-8 w-64" /><Skeleton className="h-8 w-32" /></div>
41
+ <div className="mt-6 flex flex-col gap-2">{Array.from({ length: 8 }).map((_, i) => <Skeleton key={i} className="h-11 w-full" />)}</div>
42
+ </div>
43
+ );
44
+ }
45
+ // Queue load failure states what happened, why, and the recovery path. Never a dead end.
46
+ function QueueError({ detail, onRetry }: { detail?: string; onRetry?: () => void }) {
47
+ return (
48
+ <div className="flex min-h-[100dvh] items-center justify-center bg-background p-6">
49
+ <Alert variant="destructive" className="max-w-md">
50
+ <CircleX className="h-4 w-4" />
51
+ <AlertTitle>Could not load the ticket queue</AlertTitle>
52
+ <AlertDescription>{detail ?? "The queue service did not respond."} Retry the request, or open the status page if it keeps failing.</AlertDescription>
53
+ <div className="mt-3"><Button size="sm" onClick={onRetry}><RefreshCw className="h-4 w-4" /> Retry</Button></div>
54
+ </Alert>
55
+ </div>
56
+ );
57
+ }
58
+ // The selected row projects into a side panel beside the table, never a modal (floor P3).
59
+ function TicketDetail({ ticket, open, onOpenChange, onResolve }: { ticket: Ticket | null; open: boolean; onOpenChange: (v: boolean) => void; onResolve?: (id: string) => void }) {
60
+ if (!ticket) return null;
61
+ const st = STATUS_META[ticket.status];
62
+ const rows: { label: string; value: string; mono?: boolean; num?: boolean }[] = [
63
+ { label: "Ticket", value: ticket.id, mono: true }, { label: "Requester", value: ticket.requester },
64
+ { label: "Priority", value: PRIORITY_META[ticket.priority].label }, { label: "Assignee", value: ticket.assignee ?? "Unassigned" },
65
+ { label: "SLA remaining", value: `${ticket.slaMinutes} min`, num: true }, { label: "Updated", value: new Date(ticket.updatedAt).toLocaleString(), num: true },
66
+ ];
67
+ return (
68
+ <Sheet open={open} onOpenChange={onOpenChange}>
69
+ <SheetContent side="right">
70
+ <SheetHeader>
71
+ <SheetTitle>{ticket.subject}</SheetTitle>
72
+ <SheetDescription className={st.cls}>{st.label} · {ticket.id}</SheetDescription>
73
+ </SheetHeader>
74
+ <SheetBody>
75
+ <dl className="divide-y divide-border">
76
+ {rows.map((r) => (
77
+ <div key={r.label} className="flex items-center justify-between gap-4 py-3 text-sm">
78
+ <dt className="text-muted-foreground">{r.label}</dt>
79
+ <dd className={`${r.mono ? "font-mono" : ""} ${r.num ? "tabular-nums" : ""}`}>{r.value}</dd>
80
+ </div>
81
+ ))}
82
+ </dl>
83
+ </SheetBody>
84
+ <SheetFooter>
85
+ <SheetClose asChild><Button variant="secondary" size="sm">Close</Button></SheetClose>
86
+ <Button size="sm" disabled={ticket.status === "resolved"} onClick={() => onResolve?.(ticket.id)}><CircleCheck className="h-4 w-4" /> Resolve</Button>
87
+ </SheetFooter>
88
+ </SheetContent>
89
+ </Sheet>
90
+ );
91
+ }
92
+ // Ticket queue: toolbar (search, filter, bulk action) + a keyboard-navigable table + a side-panel detail.
93
+ export function ListDetail(props: ListDetailProps) {
94
+ const { tickets, status = "ready", stale = false, errorDetail, onRetry, onNew, onResolve, onAssignSelected } = props;
95
+ const [query, setQuery] = useState("");
96
+ const [statusFilter, setStatusFilter] = useState<"all" | Status>("all");
97
+ const [sortDesc, setSortDesc] = useState(true);
98
+ const [page, setPage] = useState(0);
99
+ const [selected, setSelected] = useState<Set<string>>(new Set());
100
+ const [activeIndex, setActiveIndex] = useState(0);
101
+ const [openId, setOpenId] = useState<string | null>(null);
102
+ const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
103
+ const filtered = tickets
104
+ .filter((t) => (statusFilter === "all" || t.status === statusFilter) && (query === "" || t.subject.toLowerCase().includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())))
105
+ .sort((a, b) => (sortDesc ? 1 : -1) * (new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()));
106
+ const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
107
+ const pageRows = filtered.slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE);
108
+ // Roving-tabindex keyboard travel: j/k or arrows move the active row, Enter opens its detail panel.
109
+ useEffect(() => { rowRefs.current[activeIndex]?.focus(); }, [activeIndex, page]);
110
+ function onRowsKeyDown(e: React.KeyboardEvent) {
111
+ if (e.key === "j" || e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, pageRows.length - 1)); }
112
+ else if (e.key === "k" || e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, 0)); }
113
+ else if (e.key === "Enter" && pageRows[activeIndex]) { e.preventDefault(); setOpenId(pageRows[activeIndex].id); }
114
+ }
115
+ function toggle(id: string) { setSelected((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }); }
116
+ const allOnPage = pageRows.length > 0 && pageRows.every((t) => selected.has(t.id));
117
+ if (status === "loading") return <QueueSkeleton />;
118
+ if (status === "error") return <QueueError detail={errorDetail} onRetry={onRetry} />;
119
+ const start = filtered.length === 0 ? 0 : page * PAGE_SIZE + 1;
120
+ const end = Math.min(filtered.length, (page + 1) * PAGE_SIZE);
121
+ return (
122
+ <div className="min-h-[100dvh] bg-background text-foreground">
123
+ <header className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-6">
124
+ <div><h1 className="text-xl font-medium">Ticket queue</h1><p className="mt-1 text-sm text-muted-foreground">Scan, triage, and resolve support tickets.</p></div>
125
+ <Button size="sm" onClick={onNew}><Plus className="h-4 w-4" /> New ticket <Kbd className="ml-1">N</Kbd></Button>
126
+ </header>
127
+ <div className="flex flex-wrap items-center gap-2 border-b border-border px-6 py-3">
128
+ <Input type="search" aria-label="Search tickets by subject or id" placeholder="Search tickets" value={query} onChange={(e) => { setQuery(e.target.value); setPage(0); }} className="max-w-xs" />
129
+ <Select value={statusFilter} onValueChange={(v) => { setStatusFilter(v as "all" | Status); setPage(0); }}>
130
+ <SelectTrigger className="w-40" aria-label="Filter by status"><SelectValue /></SelectTrigger>
131
+ <SelectContent><SelectItem value="all">All statuses</SelectItem><SelectItem value="open">Open</SelectItem><SelectItem value="pending">Pending</SelectItem><SelectItem value="resolved">Resolved</SelectItem></SelectContent>
132
+ </Select>
133
+ {selected.size > 0 && <Button variant="secondary" size="sm" onClick={() => { onAssignSelected?.([...selected]); setSelected(new Set()); }}>Assign {selected.size} to me</Button>}
134
+ </div>
135
+ {stale && (
136
+ <Alert className="mx-6 mt-4">
137
+ <TriangleAlert className="h-4 w-4 text-warning" />
138
+ <AlertTitle>Showing a cached snapshot</AlertTitle>
139
+ <AlertDescription>The live queue feed is unreachable, so SLA timers and statuses below may be stale until it reconnects.</AlertDescription>
140
+ </Alert>
141
+ )}
142
+ <div className="p-6">
143
+ {filtered.length === 0 ? (
144
+ tickets.length === 0 ? (
145
+ <EmptyState icon={<Inbox className="h-10 w-10 text-muted-foreground" />} title="No tickets yet"
146
+ description="When requesters open tickets they land here, newest first. Create one to see how the queue reads."
147
+ action={<Button size="sm" onClick={onNew}><Plus className="h-4 w-4" /> New ticket</Button>} />
148
+ ) : (
149
+ <EmptyState icon={<Inbox className="h-10 w-10 text-muted-foreground" />} title="No tickets match these filters"
150
+ description="No open ticket matches your search and status filter. Clear them to see the full queue."
151
+ action={<Button size="sm" variant="secondary" onClick={() => { setQuery(""); setStatusFilter("all"); }}>Clear filters</Button>} />
152
+ )
153
+ ) : (
154
+ <Card><CardContent className="p-0">
155
+ <div className="overflow-auto" onKeyDown={onRowsKeyDown}>
156
+ <table className="w-full border-collapse text-sm">
157
+ <thead className="sticky top-0 bg-card">
158
+ <tr className="border-b border-border text-left text-muted-foreground">
159
+ <th scope="col" className="w-10 px-4 py-2"><Checkbox checked={allOnPage} aria-label="Select all on this page" onCheckedChange={() => setSelected((s) => { const n = new Set(s); allOnPage ? pageRows.forEach((t) => n.delete(t.id)) : pageRows.forEach((t) => n.add(t.id)); return n; })} /></th>
160
+ <th scope="col" className="px-4 py-2 font-medium">Ticket</th>
161
+ <th scope="col" className="px-4 py-2 font-medium">Subject</th>
162
+ <th scope="col" className="px-4 py-2 font-medium">Priority</th>
163
+ <th scope="col" className="px-4 py-2 font-medium">Status</th>
164
+ <th scope="col" className="px-4 py-2 text-right font-medium tabular-nums">SLA</th>
165
+ <th scope="col" aria-sort={sortDesc ? "descending" : "ascending"} className="px-4 py-2 text-right font-medium">
166
+ <button type="button" onClick={() => setSortDesc((v) => !v)} className="ml-auto inline-flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Updated {sortDesc ? <ArrowDown className="h-3.5 w-3.5" /> : <ArrowUp className="h-3.5 w-3.5" />}</button>
167
+ </th>
168
+ <th scope="col" className="px-4 py-2 text-right font-medium">Action</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody>
172
+ {pageRows.map((t, i) => {
173
+ const st = STATUS_META[t.status];
174
+ return (
175
+ <tr key={t.id} ref={(el) => { rowRefs.current[i] = el; }} tabIndex={i === activeIndex ? 0 : -1} aria-selected={selected.has(t.id)}
176
+ onClick={() => { setActiveIndex(i); setOpenId(t.id); }}
177
+ className="cursor-pointer border-b border-border last:border-0 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
178
+ <td className="px-4 py-2" onClick={(e) => e.stopPropagation()}><Checkbox checked={selected.has(t.id)} aria-label={`Select ${t.id}`} onCheckedChange={() => toggle(t.id)} /></td>
179
+ <td className="px-4 py-2 font-mono tabular-nums">{t.id}</td>
180
+ <td className="max-w-xs truncate px-4 py-2">{t.subject}</td>
181
+ <td className={`px-4 py-2 ${PRIORITY_META[t.priority].cls}`}>{PRIORITY_META[t.priority].label}</td>
182
+ <td className="px-4 py-2"><Badge variant="outline" className={`gap-1 ${st.cls}`}><st.Icon className="h-3.5 w-3.5" /> {st.label}</Badge></td>
183
+ <td className="px-4 py-2 text-right font-mono tabular-nums">{t.slaMinutes}</td>
184
+ <td className="px-4 py-2 text-right font-mono tabular-nums text-muted-foreground">{new Date(t.updatedAt).toLocaleDateString()}</td>
185
+ <td className="px-4 py-2 text-right" onClick={(e) => e.stopPropagation()}>
186
+ <Button size="sm" variant="ghost" disabled={t.status === "resolved"} onClick={() => onResolve?.(t.id)}>Resolve</Button>
187
+ </td>
188
+ </tr>
189
+ );
190
+ })}
191
+ </tbody>
192
+ </table>
193
+ </div>
194
+ <div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3 text-sm text-muted-foreground">
195
+ <span className="tabular-nums">{start}-{end} of {filtered.length}</span>
196
+ <div className="flex items-center gap-2">
197
+ <Button size="sm" variant="secondary" disabled={page === 0} onClick={() => setPage((p) => Math.max(0, p - 1))}>Previous</Button>
198
+ <span className="tabular-nums">Page {page + 1} of {pageCount}</span>
199
+ <Button size="sm" variant="secondary" disabled={page + 1 >= pageCount} onClick={() => setPage((p) => Math.min(pageCount - 1, p + 1))}>Next</Button>
200
+ </div>
201
+ </div>
202
+ </CardContent></Card>
203
+ )}
204
+ </div>
205
+ <TicketDetail ticket={filtered.find((t) => t.id === openId) ?? null} open={openId !== null} onOpenChange={(v) => setOpenId(v ? openId : null)} onResolve={onResolve} />
206
+ </div>
207
+ );
208
+ }
209
+ // exemplar fixture data — seeded, domain-shaped
210
+ export const FIXTURE_SEED = 5120;
211
+ export const tickets: Ticket[] = [
212
+ { id: "TCK-4821", subject: "Checkout returns 500 on saved cards", requester: "m.okonkwo", priority: "urgent", status: "open", assignee: null, slaMinutes: 12, updatedAt: "2026-07-04T09:41:00Z" },
213
+ { id: "TCK-4820", subject: "SSO redirect loops for the eu tenant on first login of the day", requester: "priya.nair", priority: "high", status: "pending", assignee: "s.delacroix", slaMinutes: 48, updatedAt: "2026-07-04T09:12:00Z" },
214
+ { id: "TCK-4816", subject: "Export CSV truncates at 10k rows", requester: "j.watanabe", priority: "normal", status: "open", assignee: "a.novak", slaMinutes: 220, updatedAt: "2026-07-04T08:55:00Z" },
215
+ { id: "TCK-4814", subject: "Webhook retries stop after one 429", requester: "l.fernandez", priority: "high", status: "open", assignee: null, slaMinutes: 33, updatedAt: "2026-07-04T08:20:00Z" },
216
+ { id: "TCK-4809", subject: "Timezone off by one hour in the audit log", requester: "d.abiodun", priority: "normal", status: "pending", assignee: "s.delacroix", slaMinutes: 640, updatedAt: "2026-07-03T22:05:00Z" },
217
+ { id: "TCK-4802", subject: "Rate-limit banner never clears", requester: "h.ito", priority: "normal", status: "resolved", assignee: "a.novak", slaMinutes: 0, updatedAt: "2026-07-03T19:48:00Z" },
218
+ { id: "TCK-4799", subject: "Bulk assign silently drops the last selection", requester: "m.okonkwo", priority: "high", status: "open", assignee: null, slaMinutes: 90, updatedAt: "2026-07-03T18:31:00Z" },
219
+ { id: "TCK-4790", subject: "Search ignores the status filter on page two", requester: "priya.nair", priority: "normal", status: "pending", assignee: "a.novak", slaMinutes: 410, updatedAt: "2026-07-03T16:02:00Z" },
220
+ { id: "TCK-4781", subject: "Session expires mid-form and loses entered values", requester: "j.watanabe", priority: "urgent", status: "open", assignee: "s.delacroix", slaMinutes: 5, updatedAt: "2026-07-03T14:44:00Z" },
221
+ { id: "TCK-4778", subject: "Column picker resets on reload", requester: "l.fernandez", priority: "normal", status: "resolved", assignee: "a.novak", slaMinutes: 0, updatedAt: "2026-07-03T11:19:00Z" },
222
+ ];
223
+ export const emptyTickets: Ticket[] = [];