@validation-os/dashboard 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  "use client";
2
2
 
3
+ // src/dashboard-app.tsx
4
+ import { useCallback as useCallback4, useEffect as useEffect5, useState as useState8 } from "react";
5
+
3
6
  // src/labels.ts
4
7
  var REGISTER_LABEL = {
5
8
  assumptions: "Assumptions",
@@ -10,6 +13,15 @@ var REGISTER_LABEL = {
10
13
  glossary: "Glossary",
11
14
  people: "People"
12
15
  };
16
+ var REGISTER_SINGULAR = {
17
+ assumptions: "Assumption",
18
+ experiments: "Experiment",
19
+ readings: "Reading",
20
+ goals: "Goal",
21
+ decisions: "Decision",
22
+ glossary: "Glossary term",
23
+ people: "Person"
24
+ };
13
25
  var REGISTER_ORDER = [
14
26
  "assumptions",
15
27
  "experiments",
@@ -19,44 +31,78 @@ var REGISTER_ORDER = [
19
31
  "glossary",
20
32
  "people"
21
33
  ];
34
+ var REGISTER_SUBTITLE = {
35
+ assumptions: "Falsifiable beliefs the plan rests on. Risk = Impact \xD7 (1 \u2212 max(0, Confidence)/100).",
36
+ experiments: "The tests that move Confidence in the beliefs.",
37
+ readings: "The evidence logged against experiments and beliefs.",
38
+ goals: "The outcomes the plan is steering toward.",
39
+ decisions: "The choices made, and what they rest on.",
40
+ glossary: "The shared vocabulary for this venture.",
41
+ people: "The people the plan touches."
42
+ };
43
+ var REGISTER_ICON = {
44
+ assumptions: "\u25CE",
45
+ experiments: "\u2697",
46
+ readings: "\u25A4",
47
+ goals: "\u25C7",
48
+ decisions: "\xA7",
49
+ glossary: "A",
50
+ people: "\u263A"
51
+ };
52
+ var REGISTER_GROUPS = [
53
+ {
54
+ label: "Registers",
55
+ registers: ["assumptions", "experiments", "readings", "goals", "decisions", "glossary"]
56
+ },
57
+ { label: "Reference", registers: ["people"] }
58
+ ];
22
59
 
23
- // src/register-counts.tsx
60
+ // src/register-browser.tsx
61
+ import { useState as useState6 } from "react";
62
+
63
+ // src/drawer-shell.tsx
64
+ import { useEffect, useRef } from "react";
24
65
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
25
- function RegisterCounts({ counts, caption, hrefFor }) {
26
- const registers = REGISTER_ORDER.filter(
27
- (r) => counts[r] !== void 0
28
- );
29
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Register counts", children: [
30
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4", children: registers.map((register) => /* @__PURE__ */ jsx(
31
- StatTile,
32
- {
33
- label: REGISTER_LABEL[register],
34
- value: counts[register] ?? 0,
35
- href: hrefFor?.(register)
36
- },
37
- register
38
- )) }),
39
- caption ? /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-neutral-500 dark:text-neutral-400", children: caption }) : null
40
- ] });
41
- }
42
- function StatTile({
43
- label,
44
- value,
45
- href
66
+ function DrawerShell({
67
+ open,
68
+ onClose,
69
+ ariaLabel,
70
+ children
46
71
  }) {
47
- const body = /* @__PURE__ */ jsxs(Fragment, { children: [
48
- /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-neutral-500 dark:text-neutral-400", children: label }),
49
- /* @__PURE__ */ jsx("div", { className: "mt-1 text-3xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50", children: value.toLocaleString() })
72
+ const panelRef = useRef(null);
73
+ useEffect(() => {
74
+ if (!open) return;
75
+ const onKey = (e) => {
76
+ if (e.key === "Escape") onClose();
77
+ };
78
+ document.addEventListener("keydown", onKey);
79
+ panelRef.current?.focus();
80
+ return () => document.removeEventListener("keydown", onKey);
81
+ }, [open, onClose]);
82
+ if (!open) return null;
83
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
84
+ /* @__PURE__ */ jsx(
85
+ "button",
86
+ {
87
+ type: "button",
88
+ "aria-label": "Close",
89
+ onClick: onClose,
90
+ className: "vos-scrim"
91
+ }
92
+ ),
93
+ /* @__PURE__ */ jsx(
94
+ "aside",
95
+ {
96
+ ref: panelRef,
97
+ role: "dialog",
98
+ "aria-modal": "true",
99
+ "aria-label": ariaLabel,
100
+ tabIndex: -1,
101
+ className: "vos-drawer",
102
+ children
103
+ }
104
+ )
50
105
  ] });
51
- const className = "block rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900";
52
- return href ? /* @__PURE__ */ jsx(
53
- "a",
54
- {
55
- href,
56
- className: `${className} transition-colors hover:border-neutral-300 hover:bg-neutral-50 dark:hover:border-neutral-700 dark:hover:bg-neutral-800`,
57
- children: body
58
- }
59
- ) : /* @__PURE__ */ jsx("div", { className, children: body });
60
106
  }
61
107
 
62
108
  // src/columns.ts
@@ -65,13 +111,19 @@ function derivedField(field) {
65
111
  }
66
112
  var COLUMNS = {
67
113
  assumptions: [
68
- { key: "Title", header: "Assumption" },
114
+ { key: "Title", header: "Belief" },
115
+ {
116
+ key: "Status",
117
+ header: "Status",
118
+ kind: "status"
119
+ },
69
120
  { key: "Impact", header: "Impact", align: "right" },
70
121
  {
71
122
  key: "confidence",
72
123
  header: "Confidence",
73
124
  align: "right",
74
125
  derived: true,
126
+ kind: "confidence",
75
127
  accessor: derivedField("confidence")
76
128
  },
77
129
  {
@@ -79,13 +131,13 @@ var COLUMNS = {
79
131
  header: "Risk",
80
132
  align: "right",
81
133
  derived: true,
134
+ kind: "risk",
82
135
  accessor: derivedField("risk")
83
- },
84
- { key: "Status", header: "Status" }
136
+ }
85
137
  ],
86
138
  experiments: [
87
139
  { key: "Title", header: "Experiment" },
88
- { key: "Status", header: "Status" },
140
+ { key: "Status", header: "Status", kind: "status" },
89
141
  { key: "Feasibility", header: "Feasibility" }
90
142
  ],
91
143
  readings: [
@@ -102,16 +154,16 @@ var COLUMNS = {
102
154
  ],
103
155
  goals: [
104
156
  { key: "Title", header: "Goal" },
105
- { key: "Status", header: "Status" },
157
+ { key: "Status", header: "Status", kind: "status" },
106
158
  { key: "Outcome", header: "Outcome" }
107
159
  ],
108
160
  decisions: [
109
161
  { key: "Title", header: "Decision" },
110
- { key: "Status", header: "Status" }
162
+ { key: "Status", header: "Status", kind: "status" }
111
163
  ],
112
164
  glossary: [
113
165
  { key: "Title", header: "Term" },
114
- { key: "Status", header: "Status" }
166
+ { key: "Status", header: "Status", kind: "status" }
115
167
  ],
116
168
  people: [
117
169
  { key: "Name", header: "Name" },
@@ -177,8 +229,244 @@ function derivedLabel(key) {
177
229
  return DERIVED_LABEL[key] ?? fieldLabel(key);
178
230
  }
179
231
 
232
+ // src/primitives.ts
233
+ var GOOD_STATUS = /* @__PURE__ */ new Set([
234
+ "live",
235
+ "concluded",
236
+ "closed",
237
+ "done",
238
+ "resolved",
239
+ "accepted",
240
+ "adopted",
241
+ "shipped"
242
+ ]);
243
+ var WARN_STATUS = /* @__PURE__ */ new Set([
244
+ "testing",
245
+ "running",
246
+ "in progress",
247
+ "in-progress",
248
+ "draft"
249
+ ]);
250
+ var CRIT_STATUS = /* @__PURE__ */ new Set([
251
+ "invalidated",
252
+ "rejected",
253
+ "blocked",
254
+ "failed",
255
+ "at risk"
256
+ ]);
257
+ function statusTone(status) {
258
+ if (!status) return "neutral";
259
+ const s = status.trim().toLowerCase();
260
+ if (GOOD_STATUS.has(s)) return "good";
261
+ if (WARN_STATUS.has(s)) return "warn";
262
+ if (CRIT_STATUS.has(s)) return "crit";
263
+ return "neutral";
264
+ }
265
+ var RISK_CRIT = 60;
266
+ var RISK_WARN = 30;
267
+ function riskLevel(risk) {
268
+ if (risk >= RISK_CRIT) return "crit";
269
+ if (risk >= RISK_WARN) return "warn";
270
+ return "good";
271
+ }
272
+ function riskFraction(risk) {
273
+ if (!Number.isFinite(risk)) return 0;
274
+ return Math.max(0, Math.min(100, risk)) / 100;
275
+ }
276
+ function confidenceTone(confidence) {
277
+ return confidence < 0 ? "crit" : "good";
278
+ }
279
+ function derivedTone(field, value) {
280
+ if (field === "confidence") return confidenceTone(value);
281
+ if (field === "risk") return riskLevel(value);
282
+ return "neutral";
283
+ }
284
+ function heroToneClass(tone) {
285
+ if (tone === "crit") return "vos-text-crit";
286
+ if (tone === "warn") return "vos-text-warn";
287
+ return "";
288
+ }
289
+ function formatSigned(n) {
290
+ const r = Math.round(n);
291
+ return r > 0 ? `+${r}` : String(r);
292
+ }
293
+ function formatCount(n) {
294
+ return n.toLocaleString();
295
+ }
296
+ function sparklinePath(values, width, height, min, max) {
297
+ if (values.length < 2) return "";
298
+ const lo = min ?? Math.min(...values, 0);
299
+ const hi = max ?? Math.max(...values, 0);
300
+ const span = hi - lo || 1;
301
+ const inset = 2;
302
+ return values.map((v, i) => {
303
+ const x = i / (values.length - 1) * (width - inset * 2) + inset;
304
+ const y = height - inset - (v - lo) / span * (height - inset * 2);
305
+ return `${i ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`;
306
+ }).join(" ");
307
+ }
308
+ function sparklineY(value, height, lo, hi) {
309
+ const span = hi - lo || 1;
310
+ const inset = 2;
311
+ return height - inset - (value - lo) / span * (height - inset * 2);
312
+ }
313
+
314
+ // src/primitives-view.tsx
315
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
316
+ var PILL_CLASS = {
317
+ good: "vos-pill vos-pill-good",
318
+ warn: "vos-pill vos-pill-warn",
319
+ crit: "vos-pill vos-pill-crit",
320
+ accent: "vos-pill vos-pill-accent",
321
+ neutral: "vos-pill vos-pill-neutral"
322
+ };
323
+ function StatusPill({ status }) {
324
+ if (!status) return /* @__PURE__ */ jsx2("span", { className: "vos-muted", children: "\u2014" });
325
+ return /* @__PURE__ */ jsx2("span", { className: PILL_CLASS[statusTone(status)], children: status });
326
+ }
327
+ var FILL_CLASS = {
328
+ good: "vos-fill-good",
329
+ warn: "vos-fill-warn",
330
+ crit: "vos-fill-crit",
331
+ accent: "vos-fill-good",
332
+ neutral: "vos-fill-good"
333
+ };
334
+ var TEXT_CLASS = {
335
+ good: "vos-text-good",
336
+ warn: "vos-text-warn",
337
+ crit: "vos-text-crit",
338
+ accent: "vos-text-good",
339
+ neutral: ""
340
+ };
341
+ function RiskBar({ risk }) {
342
+ const level = riskLevel(risk);
343
+ const pct = Math.round(riskFraction(risk) * 100);
344
+ return /* @__PURE__ */ jsxs2("span", { className: "vos-metric-cell", children: [
345
+ /* @__PURE__ */ jsx2(
346
+ "span",
347
+ {
348
+ className: "vos-risk-bar",
349
+ role: "img",
350
+ "aria-label": `Risk ${Math.round(risk)} of 100`,
351
+ children: /* @__PURE__ */ jsx2("i", { className: FILL_CLASS[level], style: { width: `${pct}%` } })
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsx2("b", { className: `vos-metric-num ${TEXT_CLASS[level]}`, children: Math.round(risk) })
355
+ ] });
356
+ }
357
+ function ConfidenceCell({
358
+ confidence,
359
+ history
360
+ }) {
361
+ const tone = confidenceTone(confidence);
362
+ return /* @__PURE__ */ jsxs2("span", { className: "vos-metric-cell", children: [
363
+ history && history.length >= 2 ? /* @__PURE__ */ jsx2(
364
+ Sparkline,
365
+ {
366
+ values: history,
367
+ width: 46,
368
+ height: 16,
369
+ min: -100,
370
+ max: 100,
371
+ tone
372
+ }
373
+ ) : null,
374
+ /* @__PURE__ */ jsx2("b", { className: `vos-metric-num ${TEXT_CLASS[tone]}`, children: formatSigned(confidence) })
375
+ ] });
376
+ }
377
+ var STROKE_VAR = {
378
+ good: "var(--vos-good)",
379
+ warn: "var(--vos-warn)",
380
+ crit: "var(--vos-crit)",
381
+ accent: "var(--vos-accent)",
382
+ neutral: "var(--vos-muted)"
383
+ };
384
+ function Sparkline({
385
+ values,
386
+ width = 240,
387
+ height = 44,
388
+ min,
389
+ max,
390
+ tone = "good",
391
+ fill = false,
392
+ ariaLabel
393
+ }) {
394
+ const d = sparklinePath(values, width, height, min, max);
395
+ if (!d) return null;
396
+ const lo = min ?? Math.min(...values, 0);
397
+ const hi = max ?? Math.max(...values, 0);
398
+ const stroke = STROKE_VAR[tone];
399
+ const last = values[values.length - 1];
400
+ const lastX = width - 2;
401
+ const lastY = sparklineY(last, height, lo, hi);
402
+ const zeroY = sparklineY(0, height, lo, hi);
403
+ const showBaseline = lo < 0 && hi > 0;
404
+ return /* @__PURE__ */ jsxs2(
405
+ "svg",
406
+ {
407
+ className: "vos-spark",
408
+ width,
409
+ height,
410
+ viewBox: `0 0 ${width} ${height}`,
411
+ preserveAspectRatio: "none",
412
+ role: "img",
413
+ "aria-label": ariaLabel ?? `Trend, now ${formatSigned(last)}`,
414
+ children: [
415
+ fill ? /* @__PURE__ */ jsx2(
416
+ "path",
417
+ {
418
+ d: `${d} L ${width - 2} ${height} L 2 ${height} Z`,
419
+ fill: stroke,
420
+ opacity: "0.13"
421
+ }
422
+ ) : null,
423
+ showBaseline ? /* @__PURE__ */ jsx2(
424
+ "line",
425
+ {
426
+ x1: 2,
427
+ x2: width - 2,
428
+ y1: zeroY,
429
+ y2: zeroY,
430
+ stroke: "var(--vos-border-strong)",
431
+ strokeWidth: 1,
432
+ strokeDasharray: "3 3"
433
+ }
434
+ ) : null,
435
+ /* @__PURE__ */ jsx2("path", { d, fill: "none", stroke, strokeWidth: 2 }),
436
+ /* @__PURE__ */ jsx2("circle", { cx: lastX, cy: lastY, r: 3, fill: stroke })
437
+ ]
438
+ }
439
+ );
440
+ }
441
+ function StatTile({
442
+ label,
443
+ value,
444
+ sub,
445
+ onClick,
446
+ active
447
+ }) {
448
+ const body = /* @__PURE__ */ jsxs2(Fragment2, { children: [
449
+ /* @__PURE__ */ jsx2("div", { className: "vos-tile-label", children: label }),
450
+ /* @__PURE__ */ jsx2("div", { className: "vos-tile-value", children: formatCount(value) }),
451
+ sub ? /* @__PURE__ */ jsx2("div", { className: "vos-tile-sub", children: sub }) : null
452
+ ] });
453
+ if (onClick) {
454
+ return /* @__PURE__ */ jsx2(
455
+ "button",
456
+ {
457
+ type: "button",
458
+ className: "vos-tile",
459
+ onClick,
460
+ "aria-pressed": active,
461
+ children: body
462
+ }
463
+ );
464
+ }
465
+ return /* @__PURE__ */ jsx2("div", { className: "vos-tile", children: body });
466
+ }
467
+
180
468
  // src/register-table.tsx
181
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
469
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
182
470
  function RegisterTable({
183
471
  register,
184
472
  records,
@@ -187,21 +475,21 @@ function RegisterTable({
187
475
  }) {
188
476
  const columns = columnsFor(register);
189
477
  if (records.length === 0) {
190
- return /* @__PURE__ */ jsx2("p", { className: "rounded-xl border border-dashed border-neutral-300 p-8 text-center text-sm text-neutral-500 dark:border-neutral-700 dark:text-neutral-400", children: "No records yet." });
478
+ return /* @__PURE__ */ jsx3("p", { className: "vos-empty", children: "No records yet." });
191
479
  }
192
- return /* @__PURE__ */ jsx2("div", { className: "overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800", children: /* @__PURE__ */ jsxs2("table", { className: "w-full border-collapse text-left text-sm", children: [
193
- /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { className: "border-b border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900", children: columns.map((c) => /* @__PURE__ */ jsx2(
480
+ return /* @__PURE__ */ jsx3("div", { className: "vos-card vos-table-scroll", children: /* @__PURE__ */ jsxs3("table", { className: "vos-table", children: [
481
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { children: columns.map((c) => /* @__PURE__ */ jsx3(
194
482
  "th",
195
483
  {
196
484
  scope: "col",
197
- className: `px-4 py-2.5 font-medium text-neutral-500 dark:text-neutral-400 ${c.align === "right" ? "text-right tabular-nums" : "text-left"}`,
485
+ className: c.align === "right" ? "vos-r" : void 0,
198
486
  children: c.header
199
487
  },
200
488
  c.key
201
489
  )) }) }),
202
- /* @__PURE__ */ jsx2("tbody", { children: records.map((record) => {
490
+ /* @__PURE__ */ jsx3("tbody", { children: records.map((record) => {
203
491
  const isSelected = record.id === selectedId;
204
- return /* @__PURE__ */ jsx2(
492
+ return /* @__PURE__ */ jsx3(
205
493
  "tr",
206
494
  {
207
495
  onClick: () => onRowClick?.(record.id),
@@ -213,121 +501,156 @@ function RegisterTable({
213
501
  } : void 0,
214
502
  tabIndex: onRowClick ? 0 : void 0,
215
503
  "aria-selected": isSelected,
216
- className: `border-b border-neutral-100 last:border-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 dark:border-neutral-900 ${onRowClick ? "cursor-pointer" : ""} ${isSelected ? "bg-blue-50 dark:bg-blue-950/40" : "hover:bg-neutral-50 dark:hover:bg-neutral-900/60"}`,
217
- children: columns.map((c, i) => {
218
- const raw = cellValue(c, record);
219
- const text = i === 0 && (raw === null || raw === void 0 || raw === "") ? primaryLabel(record) : formatValue(raw);
220
- return /* @__PURE__ */ jsx2(
221
- "td",
222
- {
223
- className: `px-4 py-2.5 ${c.align === "right" ? "text-right tabular-nums text-neutral-700 dark:text-neutral-300" : "text-left"} ${i === 0 ? "font-medium text-neutral-900 dark:text-neutral-100" : "text-neutral-600 dark:text-neutral-400"}`,
224
- children: text
225
- },
226
- c.key
227
- );
228
- })
504
+ className: isSelected ? "is-selected" : void 0,
505
+ children: columns.map((c, i) => /* @__PURE__ */ jsx3(
506
+ "td",
507
+ {
508
+ className: c.align === "right" ? "vos-r" : void 0,
509
+ children: /* @__PURE__ */ jsx3(Cell, { column: c, record, headline: i === 0 })
510
+ },
511
+ c.key
512
+ ))
229
513
  },
230
514
  record.id
231
515
  );
232
516
  }) })
233
517
  ] }) });
234
518
  }
235
-
236
- // src/record-drawer.tsx
237
- import { useEffect, useRef } from "react";
238
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
239
- var META_FIELDS = /* @__PURE__ */ new Set(["id", "version", "createdAt", "updatedAt", "derived"]);
240
- function RecordDrawer({
241
- register,
519
+ function Cell({
520
+ column,
242
521
  record,
243
- loading,
244
- error,
245
- open,
246
- onClose
522
+ headline
247
523
  }) {
248
- const closeButtonRef = useRef(null);
249
- useEffect(() => {
250
- if (!open) return;
251
- const onKey = (e) => {
252
- if (e.key === "Escape") onClose();
253
- };
254
- document.addEventListener("keydown", onKey);
255
- closeButtonRef.current?.focus();
256
- return () => document.removeEventListener("keydown", onKey);
257
- }, [open, onClose]);
258
- if (!open) return null;
259
- const derived = record && record.derived && typeof record.derived === "object" ? record.derived : null;
260
- const fields = record ? Object.keys(record).filter((k) => !META_FIELDS.has(k)) : [];
261
- return /* @__PURE__ */ jsxs3("div", { className: "fixed inset-0 z-50 flex justify-end", children: [
262
- /* @__PURE__ */ jsx3(
263
- "button",
264
- {
265
- type: "button",
266
- "aria-label": "Close",
267
- onClick: onClose,
268
- className: "absolute inset-0 bg-neutral-950/30 backdrop-blur-sm"
269
- }
270
- ),
271
- /* @__PURE__ */ jsxs3(
272
- "aside",
273
- {
274
- role: "dialog",
275
- "aria-modal": "true",
276
- "aria-label": `${REGISTER_LABEL[register]} record`,
277
- className: "relative flex h-full w-full max-w-md flex-col overflow-y-auto border-l border-neutral-200 bg-white shadow-xl dark:border-neutral-800 dark:bg-neutral-950",
278
- children: [
279
- /* @__PURE__ */ jsxs3("header", { className: "flex items-start justify-between gap-4 border-b border-neutral-200 p-5 dark:border-neutral-800", children: [
280
- /* @__PURE__ */ jsxs3("div", { children: [
281
- /* @__PURE__ */ jsx3("p", { className: "text-xs font-medium uppercase tracking-wide text-neutral-400", children: REGISTER_LABEL[register] }),
282
- /* @__PURE__ */ jsx3("h2", { className: "mt-1 text-lg font-semibold text-neutral-900 dark:text-neutral-50", children: record ? primaryLabel(record) : loading ? "Loading\u2026" : "\u2014" })
283
- ] }),
284
- /* @__PURE__ */ jsx3(
285
- "button",
286
- {
287
- ref: closeButtonRef,
288
- type: "button",
289
- onClick: onClose,
290
- className: "rounded-md px-2 py-1 text-sm text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-900",
291
- children: "Close"
292
- }
293
- )
294
- ] }),
295
- /* @__PURE__ */ jsx3("div", { className: "flex-1 p-5", children: loading ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-neutral-500", children: "Loading record\u2026" }) : error ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : !record ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-neutral-500", children: "No record." }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
296
- derived ? /* @__PURE__ */ jsxs3("section", { className: "mb-6", children: [
297
- /* @__PURE__ */ jsx3("h3", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400", children: "Computed \xB7 not editable" }),
298
- /* @__PURE__ */ jsx3("dl", { className: "grid grid-cols-3 gap-3", children: Object.entries(derived).map(([key, value]) => /* @__PURE__ */ jsxs3(
299
- "div",
300
- {
301
- className: "rounded-lg border border-neutral-200 bg-neutral-50 p-3 dark:border-neutral-800 dark:bg-neutral-900",
302
- children: [
303
- /* @__PURE__ */ jsx3("dt", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: derivedLabel(key) }),
304
- /* @__PURE__ */ jsx3("dd", { className: "mt-1 text-xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50", children: formatValue(value) })
305
- ]
306
- },
307
- key
308
- )) })
309
- ] }) : null,
310
- /* @__PURE__ */ jsx3("dl", { className: "divide-y divide-neutral-100 dark:divide-neutral-900", children: fields.map((key) => /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-3 py-2.5", children: [
311
- /* @__PURE__ */ jsx3("dt", { className: "text-sm text-neutral-500 dark:text-neutral-400", children: fieldLabel(key) }),
312
- /* @__PURE__ */ jsx3("dd", { className: "col-span-2 text-sm text-neutral-800 dark:text-neutral-200", children: formatValue(record[key]) })
313
- ] }, key)) })
314
- ] }) }),
315
- record ? /* @__PURE__ */ jsxs3("footer", { className: "border-t border-neutral-200 p-5 text-xs text-neutral-400 dark:border-neutral-800", children: [
316
- record.id,
317
- " \xB7 version ",
318
- String(record.version),
319
- " \xB7 updated",
320
- " ",
321
- formatValue(record.updatedAt)
322
- ] }) : null
323
- ]
324
- }
325
- )
326
- ] });
524
+ const raw = cellValue(column, record);
525
+ if (column.kind === "status") {
526
+ return /* @__PURE__ */ jsx3(StatusPill, { status: raw == null ? null : String(raw) });
527
+ }
528
+ if (column.kind === "risk") {
529
+ return typeof raw === "number" ? /* @__PURE__ */ jsx3(RiskBar, { risk: raw }) : /* @__PURE__ */ jsx3("span", { className: "vos-muted", children: "\u2014" });
530
+ }
531
+ if (column.kind === "confidence") {
532
+ return typeof raw === "number" ? /* @__PURE__ */ jsx3(ConfidenceCell, { confidence: raw }) : /* @__PURE__ */ jsx3("span", { className: "vos-muted", children: "\u2014" });
533
+ }
534
+ const text = headline && (raw === null || raw === void 0 || raw === "") ? primaryLabel(record) : formatValue(raw);
535
+ return /* @__PURE__ */ jsx3("span", { className: headline ? "vos-ttl" : void 0, children: text });
327
536
  }
328
537
 
329
- // src/register-browser.tsx
330
- import { useState as useState2 } from "react";
538
+ // src/record-drawer.tsx
539
+ import { useEffect as useEffect3, useState as useState2 } from "react";
540
+
541
+ // src/edit.ts
542
+ var CONFLICT_MESSAGE = "Someone edited this while you had it open \u2014 your changes are safe, take a look before saving again.";
543
+ var t = (key, label) => ({ key, label, kind: "text" });
544
+ var area = (key, label) => ({
545
+ key,
546
+ label,
547
+ kind: "textarea"
548
+ });
549
+ var num = (key, label) => ({
550
+ key,
551
+ label,
552
+ kind: "number",
553
+ nullable: true
554
+ });
555
+ var sel = (key, label, options, nullable = false) => ({ key, label, kind: "select", options, nullable });
556
+ var READING_RUNGS = [
557
+ "Opinion",
558
+ "Pitch-deck reaction",
559
+ "Anecdotal",
560
+ "Desk research",
561
+ "Survey at scale",
562
+ "Prototype usage",
563
+ "Signed intent",
564
+ "Paying users"
565
+ ];
566
+ var SOURCE_QUALITY = ["1", "0.7", "0.5"];
567
+ var EDITORS = {
568
+ assumptions: [
569
+ t("Title", "Assumption"),
570
+ area("Description", "Description"),
571
+ num("Impact", "Impact"),
572
+ sel("Status", "Status", ["Draft", "Live", "Invalidated"]),
573
+ t("Lens", "Lens"),
574
+ area("5 Whys", "5 Whys"),
575
+ area("Metric for truth", "Metric for truth"),
576
+ area("Scoring justification", "Scoring justification")
577
+ // `moot` is deliberately not editable here — mooting an assumption is a
578
+ // gated business action (see core `relations.ts`), not a free-form toggle.
579
+ ],
580
+ experiments: [
581
+ t("Title", "Experiment"),
582
+ t("Instrument", "Instrument"),
583
+ sel("Feasibility", "Feasibility", ["High", "Medium", "Low"], true),
584
+ sel("Status", "Status", ["Running", "Closed"]),
585
+ sel(
586
+ "closureReason",
587
+ "Closure reason",
588
+ ["Completed", "Early-stop", "Kill"],
589
+ true
590
+ ),
591
+ t("Date", "Date")
592
+ ],
593
+ readings: [
594
+ t("Title", "Reading"),
595
+ t("Source", "Source"),
596
+ sel("Rung", "Rung", READING_RUNGS),
597
+ sel("Result", "Result", ["Validated", "Invalidated", "Inconclusive"]),
598
+ sel("Representativeness", "Representativeness", SOURCE_QUALITY),
599
+ sel("Credibility", "Credibility", SOURCE_QUALITY),
600
+ sel("magnitudeBand", "Magnitude band", ["Low", "Typical", "High"], true),
601
+ t("Date", "Date")
602
+ ],
603
+ goals: [
604
+ t("Title", "Goal"),
605
+ sel("Status", "Status", ["Draft", "Active", "Closed"]),
606
+ sel("Outcome", "Outcome", ["Achieved", "Missed", "Dropped"], true),
607
+ t("Date", "Date")
608
+ ],
609
+ decisions: [
610
+ t("Title", "Decision"),
611
+ sel("Status", "Status", ["Active", "Provisional", "Superseded", "Reversed"])
612
+ ],
613
+ glossary: [
614
+ t("Title", "Term"),
615
+ sel("Status", "Status", ["Active", "Provisional", "Superseded"])
616
+ ],
617
+ people: [t("Name", "Name"), t("Role", "Role"), t("Segment", "Segment")]
618
+ };
619
+ function editableFields(register) {
620
+ return EDITORS[register];
621
+ }
622
+ function draftFrom(register, record) {
623
+ const draft = {};
624
+ for (const f of editableFields(register)) {
625
+ const value = record[f.key];
626
+ draft[f.key] = value === null || value === void 0 ? "" : String(value);
627
+ }
628
+ return draft;
629
+ }
630
+ function coerce(field, raw) {
631
+ const str2 = String(raw);
632
+ if (field.kind === "number") {
633
+ if (str2.trim() === "") return null;
634
+ const n = Number(str2);
635
+ return Number.isNaN(n) ? null : n;
636
+ }
637
+ if (str2 === "" && (field.kind === "select" || field.nullable)) return null;
638
+ return str2;
639
+ }
640
+ function norm(value) {
641
+ return value === void 0 || value === "" ? null : value;
642
+ }
643
+ function buildPatch(register, original, draft) {
644
+ const patch = { version: original.version };
645
+ for (const field of editableFields(register)) {
646
+ const next = coerce(field, draft[field.key] ?? "");
647
+ if (norm(next) !== norm(original[field.key])) patch[field.key] = next;
648
+ }
649
+ return patch;
650
+ }
651
+ function hasEdits(register, original, draft) {
652
+ return Object.keys(buildPatch(register, original, draft)).length > 1;
653
+ }
331
654
 
332
655
  // src/use-records.ts
333
656
  import { useCallback, useEffect as useEffect2, useState } from "react";
@@ -362,7 +685,7 @@ function useJsonResource(url) {
362
685
  live = false;
363
686
  };
364
687
  }, [url, tick]);
365
- const refresh = useCallback(() => setTick((t) => t + 1), []);
688
+ const refresh = useCallback(() => setTick((t2) => t2 + 1), []);
366
689
  return { ...state, refresh };
367
690
  }
368
691
  function useList(register, basePath = "/api") {
@@ -373,26 +696,975 @@ function useList(register, basePath = "/api") {
373
696
  }
374
697
  function useRecord(register, id, basePath = "/api") {
375
698
  const url = id ? `${basePath}/${register}/${encodeURIComponent(id)}` : null;
376
- const { data, loading, error } = useJsonResource(url);
377
- return { record: data, loading, error };
699
+ const { data, loading, error, refresh } = useJsonResource(url);
700
+ return { record: data, loading, error, refresh };
701
+ }
702
+ var SAVE_FAILED = "Couldn't save your changes \u2014 please try again.";
703
+ function interpretSave(status, body) {
704
+ if (status >= 200 && status < 300) {
705
+ return { ok: true, record: pickData(body) };
706
+ }
707
+ const message = body?.message;
708
+ const text = typeof message === "string" ? message : null;
709
+ if (status === 409) {
710
+ return { ok: false, conflict: true, message: text ?? CONFLICT_MESSAGE };
711
+ }
712
+ return { ok: false, conflict: false, message: text ?? SAVE_FAILED };
713
+ }
714
+ function useUpdate(register, basePath = "/api") {
715
+ const [saving, setSaving] = useState(false);
716
+ const [conflict, setConflict] = useState(null);
717
+ const [error, setError] = useState(null);
718
+ const save = useCallback(
719
+ async (id, patch) => {
720
+ setSaving(true);
721
+ setConflict(null);
722
+ setError(null);
723
+ try {
724
+ const res = await fetch(
725
+ `${basePath}/${register}/${encodeURIComponent(id)}`,
726
+ {
727
+ method: "PATCH",
728
+ headers: { "content-type": "application/json" },
729
+ body: JSON.stringify(patch)
730
+ }
731
+ );
732
+ const body = await res.json().catch(() => null);
733
+ const result = interpretSave(res.status, body);
734
+ if (!result.ok && result.conflict) setConflict(result.message);
735
+ else if (!result.ok) setError(result.message);
736
+ return result;
737
+ } catch (e) {
738
+ const message = e instanceof Error ? e.message : "Couldn't save your changes.";
739
+ setError(message);
740
+ return { ok: false, conflict: false, message };
741
+ } finally {
742
+ setSaving(false);
743
+ }
744
+ },
745
+ [register, basePath]
746
+ );
747
+ const reset = useCallback(() => {
748
+ setConflict(null);
749
+ setError(null);
750
+ }, []);
751
+ return { save, saving, conflict, error, reset };
752
+ }
753
+
754
+ // src/understanding.ts
755
+ import {
756
+ toReadingInput
757
+ } from "@validation-os/core";
758
+ import {
759
+ confidenceAttribution,
760
+ confidenceTrajectory,
761
+ experimentProgress,
762
+ isConcluded
763
+ } from "@validation-os/core/derivation";
764
+ function str(v) {
765
+ return typeof v === "string" && v !== "" ? v : null;
766
+ }
767
+ function testsAssumption(exp, assumptionId) {
768
+ const bars = exp.barLines;
769
+ if (bars?.some((b) => b.assumptionId === assumptionId)) return true;
770
+ const ids = exp.barLineAssumptionIds;
771
+ return ids?.includes(assumptionId) ?? false;
772
+ }
773
+ function buildUnderstanding(assumption, readings, experiments) {
774
+ const mine = readings.filter((r) => r.assumptionId === assumption.id);
775
+ const inputs = mine.map(toReadingInput);
776
+ const { confidence, movers } = confidenceAttribution(inputs);
777
+ const experimentsById = new Map(experiments.map((e) => [e.id, e]));
778
+ const moverByExperiment = new Map(
779
+ movers.filter((m) => m.kind === "experiment").map((m) => [m.experimentId, m])
780
+ );
781
+ const experimentIds = /* @__PURE__ */ new Set([
782
+ ...experiments.filter((e) => testsAssumption(e, assumption.id)).map((e) => e.id),
783
+ ...moverByExperiment.keys()
784
+ ]);
785
+ const experimentViews = [...experimentIds].map((id) => {
786
+ const exp = experimentsById.get(id);
787
+ const mover = moverByExperiment.get(id);
788
+ const bars = exp?.barLines ?? [];
789
+ const progress = bars.length ? experimentProgress(bars) : null;
790
+ const status = exp ? str(exp.Status) : null;
791
+ return {
792
+ experimentId: id,
793
+ title: exp ? str(exp.Title) : null,
794
+ status,
795
+ contribution: mover?.contribution ?? 0,
796
+ magnitude: mover?.magnitude ?? 0,
797
+ readingCount: mover?.readingCount ?? 0,
798
+ progress,
799
+ done: status === "Closed" || progress?.concluded === true
800
+ };
801
+ });
802
+ experimentViews.sort(
803
+ (a, b) => b.magnitude - a.magnitude || Number(a.done) - Number(b.done) || a.experimentId.localeCompare(b.experimentId)
804
+ );
805
+ const otherMovers = movers.filter((m) => m.kind !== "experiment").map((m) => ({
806
+ key: m.key,
807
+ kind: m.kind,
808
+ goalId: m.goalId,
809
+ contribution: m.contribution,
810
+ magnitude: m.magnitude,
811
+ readingCount: m.readingCount
812
+ }));
813
+ return {
814
+ confidence,
815
+ experiments: experimentViews,
816
+ otherMovers,
817
+ trajectory: confidenceTrajectory(inputs),
818
+ readingCount: inputs.filter((r) => isConcluded(r.result)).length
819
+ };
820
+ }
821
+
822
+ // src/understanding-panel.tsx
823
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
824
+ function UnderstandingPanel({
825
+ assumption,
826
+ basePath
827
+ }) {
828
+ const readings = useList("readings", basePath);
829
+ const experiments = useList("experiments", basePath);
830
+ if (readings.loading || experiments.loading) {
831
+ return /* @__PURE__ */ jsx4(Muted, { children: "Working out what moved the number\u2026" });
832
+ }
833
+ if (readings.error || experiments.error) {
834
+ return /* @__PURE__ */ jsx4(Muted, { children: "Couldn't load the evidence behind this number." });
835
+ }
836
+ const u = buildUnderstanding(
837
+ assumption,
838
+ readings.records ?? [],
839
+ experiments.records ?? []
840
+ );
841
+ if (u.experiments.length === 0 && u.otherMovers.length === 0) {
842
+ return /* @__PURE__ */ jsx4(Muted, { children: "No experiments or concluded readings yet \u2014 Confidence rests on the neutral prior alone." });
843
+ }
844
+ const maxMagnitude = Math.max(
845
+ ...u.experiments.map((e) => e.magnitude),
846
+ ...u.otherMovers.map((m) => m.magnitude),
847
+ 0.01
848
+ );
849
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
850
+ u.experiments.length ? /* @__PURE__ */ jsxs4("section", { children: [
851
+ /* @__PURE__ */ jsx4("div", { className: "vos-why-section-title", children: "What's moving Confidence" }),
852
+ u.experiments.map((e) => /* @__PURE__ */ jsx4(
853
+ PushRow,
854
+ {
855
+ label: e.title ?? `Experiment ${e.experimentId}`,
856
+ note: experimentDetail(e),
857
+ contribution: e.contribution,
858
+ magnitude: e.magnitude,
859
+ max: maxMagnitude,
860
+ done: e.done
861
+ },
862
+ e.experimentId
863
+ ))
864
+ ] }) : null,
865
+ u.otherMovers.length ? /* @__PURE__ */ jsxs4("section", { children: [
866
+ /* @__PURE__ */ jsx4("div", { className: "vos-why-section-title", children: "Other evidence" }),
867
+ u.otherMovers.map((m) => /* @__PURE__ */ jsx4(
868
+ PushRow,
869
+ {
870
+ label: otherMoverLabel(m),
871
+ contribution: m.contribution,
872
+ magnitude: m.magnitude,
873
+ max: maxMagnitude
874
+ },
875
+ m.key
876
+ ))
877
+ ] }) : null,
878
+ /* @__PURE__ */ jsx4(Trajectory, { points: u.trajectory })
879
+ ] });
880
+ }
881
+ function PushRow({
882
+ label,
883
+ note,
884
+ contribution,
885
+ magnitude,
886
+ max,
887
+ done
888
+ }) {
889
+ const up = contribution >= 0;
890
+ const moving = magnitude > 0;
891
+ const width = Math.round(magnitude / max * 50);
892
+ const fill = up ? { left: "50%", width: `${width}%`, background: "var(--vos-good)" } : { right: "50%", width: `${width}%`, background: "var(--vos-crit)" };
893
+ return /* @__PURE__ */ jsxs4("div", { className: "vos-mover", children: [
894
+ /* @__PURE__ */ jsx4("span", { className: "vos-mover-name", children: label }),
895
+ note ? /* @__PURE__ */ jsx4("span", { className: `vos-mover-note ${done ? "vos-text-good" : "vos-text-warn"}`, children: note }) : null,
896
+ /* @__PURE__ */ jsx4("span", { className: "vos-track vos-signed", children: moving ? /* @__PURE__ */ jsx4("i", { style: fill }) : null }),
897
+ /* @__PURE__ */ jsx4(
898
+ "span",
899
+ {
900
+ className: "vos-mover-val",
901
+ style: { color: up ? "var(--vos-good)" : "var(--vos-crit)" },
902
+ children: moving ? formatSigned(contribution) : "\u2014"
903
+ }
904
+ )
905
+ ] });
906
+ }
907
+ function Trajectory({ points }) {
908
+ if (points.length < 2) {
909
+ return /* @__PURE__ */ jsx4("p", { className: "vos-hint", children: points.length === 1 ? `One dated reading so far \u2014 Confidence ${formatSigned(points[0].confidence)} on ${points[0].date}.` : "No dated readings yet to chart a trajectory." });
910
+ }
911
+ const first = points[0];
912
+ const last = points[points.length - 1];
913
+ const values = points.map((p) => p.confidence);
914
+ return /* @__PURE__ */ jsxs4("div", { className: "vos-traj", children: [
915
+ /* @__PURE__ */ jsx4("div", { className: "vos-traj-head", children: /* @__PURE__ */ jsx4("span", { className: "vos-lbl", children: "Confidence over time" }) }),
916
+ /* @__PURE__ */ jsx4(
917
+ Sparkline,
918
+ {
919
+ values,
920
+ width: 260,
921
+ height: 44,
922
+ min: -100,
923
+ max: 100,
924
+ tone: confidenceTone(last.confidence),
925
+ fill: true,
926
+ ariaLabel: `Confidence moved from ${formatSigned(first.confidence)} to ${formatSigned(last.confidence)}`
927
+ }
928
+ ),
929
+ /* @__PURE__ */ jsxs4("div", { className: "vos-traj-foot", children: [
930
+ /* @__PURE__ */ jsx4("span", { children: first.date }),
931
+ /* @__PURE__ */ jsxs4("span", { className: "vos-num", children: [
932
+ "now ",
933
+ formatSigned(last.confidence)
934
+ ] })
935
+ ] })
936
+ ] });
937
+ }
938
+ function experimentDetail(e) {
939
+ const evidence = e.readingCount === 0 ? "No readings yet" : `${e.readingCount} reading${e.readingCount === 1 ? "" : "s"}`;
940
+ const p = e.progress;
941
+ if (e.done) {
942
+ return p ? `Concluded \xB7 ${p.total} of ${p.total} bars settled` : `Concluded \xB7 ${evidence}`;
943
+ }
944
+ if (!p || p.total === 0) return `${evidence} \xB7 no pre-registered bars`;
945
+ return `${evidence} \xB7 ${p.settled} of ${p.total} bars settled \xB7 ${p.toGo} to go`;
946
+ }
947
+ function otherMoverLabel(m) {
948
+ if (m.kind === "goal") return `Goal ${m.goalId ?? ""}`.trim();
949
+ return m.readingCount === 1 ? "A direct reading" : "Direct readings";
950
+ }
951
+ function Muted({ children }) {
952
+ return /* @__PURE__ */ jsx4("p", { className: "vos-hint", children });
953
+ }
954
+
955
+ // src/record-drawer.tsx
956
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
957
+ var META_FIELDS = /* @__PURE__ */ new Set(["id", "version", "createdAt", "updatedAt", "derived"]);
958
+ var DERIVED_SUB = {
959
+ confidence: "Signed average of concluded readings",
960
+ risk: "Impact \xD7 (1 \u2212 Confidence\u207A/100)",
961
+ derivedImpact: "Impact re-weighted by links",
962
+ impact: "Impact re-weighted by links",
963
+ strength: "of the evidence base",
964
+ sourceQuality: "of the source"
965
+ };
966
+ function RecordDrawer({
967
+ register,
968
+ record,
969
+ loading,
970
+ error,
971
+ open,
972
+ onClose,
973
+ basePath,
974
+ onChanged,
975
+ children
976
+ }) {
977
+ const [editing, setEditing] = useState2(false);
978
+ const [draft, setDraft] = useState2({});
979
+ const [baseline, setBaseline] = useState2(null);
980
+ const [why, setWhy] = useState2(false);
981
+ const { save, saving, conflict, error: saveError, reset } = useUpdate(
982
+ register,
983
+ basePath
984
+ );
985
+ const recordId = record?.id ?? null;
986
+ useEffect3(() => {
987
+ setEditing(false);
988
+ setBaseline(null);
989
+ setWhy(false);
990
+ reset();
991
+ }, [recordId, reset]);
992
+ const derived = record && record.derived && typeof record.derived === "object" ? record.derived : null;
993
+ const fields = record ? Object.keys(record).filter((k) => !META_FIELDS.has(k)) : [];
994
+ function startEditing() {
995
+ if (!record) return;
996
+ setBaseline(record);
997
+ setDraft(draftFrom(register, record));
998
+ reset();
999
+ setEditing(true);
1000
+ }
1001
+ function cancelEditing() {
1002
+ setEditing(false);
1003
+ reset();
1004
+ }
1005
+ async function onSave() {
1006
+ if (!record || !baseline) return;
1007
+ const patch = buildPatch(register, baseline, draft);
1008
+ patch.version = record.version;
1009
+ if (Object.keys(patch).length <= 1) {
1010
+ setEditing(false);
1011
+ return;
1012
+ }
1013
+ const result = await save(record.id, patch);
1014
+ if (result.ok) {
1015
+ setEditing(false);
1016
+ onChanged?.();
1017
+ }
1018
+ }
1019
+ function reloadLatest() {
1020
+ reset();
1021
+ onChanged?.();
1022
+ }
1023
+ const setField = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
1024
+ return /* @__PURE__ */ jsxs5(
1025
+ DrawerShell,
1026
+ {
1027
+ open,
1028
+ onClose,
1029
+ ariaLabel: `${REGISTER_LABEL[register]} record`,
1030
+ children: [
1031
+ /* @__PURE__ */ jsxs5("header", { className: "vos-drawer-header", children: [
1032
+ /* @__PURE__ */ jsxs5("div", { style: { flex: 1, minWidth: 0 }, children: [
1033
+ /* @__PURE__ */ jsx5("p", { className: "vos-drawer-eyebrow", children: REGISTER_LABEL[register] }),
1034
+ /* @__PURE__ */ jsx5("h2", { className: "vos-drawer-title", children: record ? primaryLabel(record) : loading ? "Loading\u2026" : "\u2014" })
1035
+ ] }),
1036
+ record ? /* @__PURE__ */ jsxs5("span", { className: "vos-verbadge", children: [
1037
+ "v",
1038
+ formatValue(record.version)
1039
+ ] }) : null,
1040
+ record && !editing ? /* @__PURE__ */ jsx5(
1041
+ "button",
1042
+ {
1043
+ type: "button",
1044
+ onClick: startEditing,
1045
+ className: "vos-btn vos-btn-ghost vos-btn-sm",
1046
+ children: "Edit"
1047
+ }
1048
+ ) : null,
1049
+ /* @__PURE__ */ jsx5(
1050
+ "button",
1051
+ {
1052
+ type: "button",
1053
+ onClick: onClose,
1054
+ className: "vos-iconbtn",
1055
+ "aria-label": "Close",
1056
+ children: "\u2715"
1057
+ }
1058
+ )
1059
+ ] }),
1060
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-body", children: loading ? /* @__PURE__ */ jsx5("p", { className: "vos-muted", children: "Loading record\u2026" }) : error ? /* @__PURE__ */ jsx5("p", { className: "vos-error", children: error }) : !record ? /* @__PURE__ */ jsx5("p", { className: "vos-muted", children: "No record." }) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
1061
+ derived ? /* @__PURE__ */ jsxs5("div", { children: [
1062
+ /* @__PURE__ */ jsxs5("div", { className: "vos-derived", children: [
1063
+ /* @__PURE__ */ jsxs5("div", { className: "vos-derived-head", children: [
1064
+ "Derived",
1065
+ /* @__PURE__ */ jsx5("span", { className: "vos-lock", children: "\u{1F512} computed on write \u2014 not editable" })
1066
+ ] }),
1067
+ /* @__PURE__ */ jsx5("div", { className: "vos-dgrid", children: Object.entries(derived).map(([key, value]) => /* @__PURE__ */ jsx5(
1068
+ DerivedCell,
1069
+ {
1070
+ field: key,
1071
+ value,
1072
+ showWhy: key === "confidence",
1073
+ whyOpen: why,
1074
+ onWhy: () => setWhy((w) => !w)
1075
+ },
1076
+ key
1077
+ )) })
1078
+ ] }),
1079
+ "confidence" in derived && why ? /* @__PURE__ */ jsx5("div", { className: "vos-why-panel", children: /* @__PURE__ */ jsx5(UnderstandingPanel, { assumption: record, basePath }) }) : null
1080
+ ] }) : null,
1081
+ conflict ? /* @__PURE__ */ jsx5(ConflictBanner, { message: conflict, onReload: reloadLatest }) : null,
1082
+ saveError ? /* @__PURE__ */ jsx5("p", { role: "alert", className: "vos-banner vos-banner-crit", children: saveError }) : null,
1083
+ editing ? /* @__PURE__ */ jsx5(
1084
+ EditFields,
1085
+ {
1086
+ register,
1087
+ draft,
1088
+ onField: setField
1089
+ }
1090
+ ) : /* @__PURE__ */ jsx5("div", { className: "vos-detail-list", children: fields.map((key) => /* @__PURE__ */ jsxs5("div", { className: "vos-detail-row", children: [
1091
+ /* @__PURE__ */ jsx5("span", { className: "vos-detail-k", children: fieldLabel(key) }),
1092
+ /* @__PURE__ */ jsx5("span", { className: "vos-detail-v", children: formatValue(record[key]) })
1093
+ ] }, key)) })
1094
+ ] }) }),
1095
+ record && !loading && !error && !editing ? children : null,
1096
+ record && editing ? /* @__PURE__ */ jsxs5("footer", { className: "vos-drawer-footer", children: [
1097
+ /* @__PURE__ */ jsx5(
1098
+ "button",
1099
+ {
1100
+ type: "button",
1101
+ onClick: cancelEditing,
1102
+ disabled: saving,
1103
+ className: "vos-btn vos-btn-ghost vos-btn-sm",
1104
+ children: "Cancel"
1105
+ }
1106
+ ),
1107
+ /* @__PURE__ */ jsx5(
1108
+ "button",
1109
+ {
1110
+ type: "button",
1111
+ onClick: onSave,
1112
+ disabled: saving,
1113
+ className: "vos-btn vos-btn-sm",
1114
+ children: saving ? "Saving\u2026" : "Save"
1115
+ }
1116
+ )
1117
+ ] }) : record ? /* @__PURE__ */ jsxs5("footer", { className: "vos-drawer-footer", children: [
1118
+ record.id,
1119
+ " \xB7 updated ",
1120
+ formatValue(record.updatedAt)
1121
+ ] }) : null
1122
+ ]
1123
+ }
1124
+ );
1125
+ }
1126
+ function DerivedCell({
1127
+ field,
1128
+ value,
1129
+ showWhy,
1130
+ whyOpen,
1131
+ onWhy
1132
+ }) {
1133
+ const num2 = typeof value === "number" ? value : null;
1134
+ let toneClass = "";
1135
+ let display = formatValue(value);
1136
+ if (num2 !== null) {
1137
+ toneClass = heroToneClass(derivedTone(field, num2));
1138
+ display = field === "confidence" ? formatSigned(num2) : String(Math.round(num2));
1139
+ }
1140
+ return /* @__PURE__ */ jsxs5("div", { className: "vos-dcell", children: [
1141
+ /* @__PURE__ */ jsxs5("div", { className: "vos-dcell-k", children: [
1142
+ derivedLabel(field),
1143
+ showWhy ? /* @__PURE__ */ jsxs5(
1144
+ "button",
1145
+ {
1146
+ type: "button",
1147
+ className: "vos-why",
1148
+ onClick: onWhy,
1149
+ "aria-expanded": whyOpen,
1150
+ children: [
1151
+ "Why? ",
1152
+ whyOpen ? "\u25B4" : "\u25BE"
1153
+ ]
1154
+ }
1155
+ ) : null
1156
+ ] }),
1157
+ /* @__PURE__ */ jsx5("div", { className: `vos-dcell-v ${toneClass}`, children: display }),
1158
+ DERIVED_SUB[field] ? /* @__PURE__ */ jsx5("div", { className: "vos-dcell-sub", children: DERIVED_SUB[field] }) : null
1159
+ ] });
1160
+ }
1161
+ function ConflictBanner({
1162
+ message,
1163
+ onReload
1164
+ }) {
1165
+ return /* @__PURE__ */ jsxs5("div", { role: "alert", className: "vos-banner vos-banner-warn", children: [
1166
+ /* @__PURE__ */ jsx5("div", { className: "vos-banner-body", children: /* @__PURE__ */ jsx5("span", { children: message }) }),
1167
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: onReload, children: "Review the latest" })
1168
+ ] });
1169
+ }
1170
+ function EditFields({
1171
+ register,
1172
+ draft,
1173
+ onField
1174
+ }) {
1175
+ return /* @__PURE__ */ jsx5("div", { className: "vos-field-stack", children: editableFields(register).map((field) => /* @__PURE__ */ jsx5(
1176
+ FieldInput,
1177
+ {
1178
+ field,
1179
+ value: draft[field.key],
1180
+ onChange: (v) => onField(field.key, v)
1181
+ },
1182
+ field.key
1183
+ )) });
1184
+ }
1185
+ function FieldInput({
1186
+ field,
1187
+ value,
1188
+ onChange
1189
+ }) {
1190
+ const id = `field-${field.key.replace(/\s+/g, "-")}`;
1191
+ return /* @__PURE__ */ jsxs5("div", { className: "vos-field", children: [
1192
+ /* @__PURE__ */ jsx5("label", { htmlFor: id, children: field.label }),
1193
+ field.kind === "textarea" ? /* @__PURE__ */ jsx5(
1194
+ "textarea",
1195
+ {
1196
+ id,
1197
+ value: String(value ?? ""),
1198
+ onChange: (e) => onChange(e.target.value),
1199
+ rows: 3,
1200
+ className: "vos-input"
1201
+ }
1202
+ ) : field.kind === "select" ? /* @__PURE__ */ jsxs5(
1203
+ "select",
1204
+ {
1205
+ id,
1206
+ value: String(value ?? ""),
1207
+ onChange: (e) => onChange(e.target.value),
1208
+ className: "vos-input",
1209
+ children: [
1210
+ field.nullable ? /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }) : null,
1211
+ field.options?.map((opt) => /* @__PURE__ */ jsx5("option", { value: opt, children: opt }, opt))
1212
+ ]
1213
+ }
1214
+ ) : /* @__PURE__ */ jsx5(
1215
+ "input",
1216
+ {
1217
+ id,
1218
+ type: field.kind === "number" ? "number" : "text",
1219
+ value: String(value ?? ""),
1220
+ onChange: (e) => onChange(e.target.value),
1221
+ className: "vos-input"
1222
+ }
1223
+ )
1224
+ ] });
1225
+ }
1226
+
1227
+ // src/record-form.tsx
1228
+ import { useMemo, useState as useState4 } from "react";
1229
+
1230
+ // src/form-fields.ts
1231
+ import {
1232
+ GOAL_RUNG_VALUES,
1233
+ TESTING_RUNGS
1234
+ } from "@validation-os/core";
1235
+ var ASSUMPTION_STATUS = ["Draft", "Live", "Invalidated"];
1236
+ var EXPERIMENT_STATUS = ["Running", "Closed"];
1237
+ var CLOSURE_REASON = ["Completed", "Early-stop", "Kill"];
1238
+ var GOAL_STATUS = ["Draft", "Active", "Closed"];
1239
+ var GOAL_OUTCOME = ["Achieved", "Missed", "Dropped"];
1240
+ var DECISION_STATUS = ["Active", "Provisional", "Superseded", "Reversed"];
1241
+ var GLOSSARY_STATUS = ["Active", "Provisional", "Superseded"];
1242
+ var RESULT = ["Validated", "Invalidated", "Inconclusive"];
1243
+ var MAGNITUDE = ["Low", "Typical", "High"];
1244
+ var FEASIBILITY = ["High", "Medium", "Low"];
1245
+ var RUNGS = [...TESTING_RUNGS, ...GOAL_RUNG_VALUES];
1246
+ var QUALITY = ["1", "0.7", "0.5"];
1247
+ var FIELDS = {
1248
+ assumptions: [
1249
+ { key: "Title", label: "Assumption", kind: "text", required: true },
1250
+ { key: "Description", label: "Description", kind: "textarea" },
1251
+ { key: "Lens", label: "Lens", kind: "text" },
1252
+ { key: "Impact", label: "Impact (0\u2013100)", kind: "number" },
1253
+ { key: "Status", label: "Status", kind: "select", options: ASSUMPTION_STATUS },
1254
+ { key: "5 Whys", label: "5 Whys", kind: "textarea" },
1255
+ { key: "Metric for truth", label: "Metric for truth", kind: "textarea" },
1256
+ {
1257
+ key: "Scoring justification",
1258
+ label: "Scoring justification",
1259
+ kind: "textarea"
1260
+ }
1261
+ ],
1262
+ experiments: [
1263
+ { key: "Title", label: "Experiment", kind: "text", required: true },
1264
+ { key: "Instrument", label: "Instrument", kind: "text" },
1265
+ { key: "Feasibility", label: "Feasibility", kind: "select", options: FEASIBILITY },
1266
+ { key: "Status", label: "Status", kind: "select", options: EXPERIMENT_STATUS },
1267
+ {
1268
+ key: "closureReason",
1269
+ label: "Closure reason",
1270
+ kind: "select",
1271
+ options: CLOSURE_REASON
1272
+ },
1273
+ { key: "Date", label: "Date", kind: "text", placeholder: "YYYY-MM-DD" }
1274
+ ],
1275
+ readings: [
1276
+ { key: "Title", label: "Reading", kind: "text", required: true },
1277
+ { key: "Source", label: "Source", kind: "text" },
1278
+ { key: "Rung", label: "Rung", kind: "select", options: RUNGS, required: true },
1279
+ { key: "Result", label: "Result", kind: "select", options: RESULT, required: true },
1280
+ {
1281
+ key: "Representativeness",
1282
+ label: "Representativeness",
1283
+ kind: "select",
1284
+ options: QUALITY,
1285
+ coerce: "number"
1286
+ },
1287
+ {
1288
+ key: "Credibility",
1289
+ label: "Credibility",
1290
+ kind: "select",
1291
+ options: QUALITY,
1292
+ coerce: "number"
1293
+ },
1294
+ {
1295
+ key: "magnitudeBand",
1296
+ label: "Magnitude band",
1297
+ kind: "select",
1298
+ options: MAGNITUDE
1299
+ },
1300
+ { key: "Date", label: "Date", kind: "text", placeholder: "YYYY-MM-DD" }
1301
+ ],
1302
+ goals: [
1303
+ { key: "Title", label: "Goal", kind: "text", required: true },
1304
+ { key: "Status", label: "Status", kind: "select", options: GOAL_STATUS },
1305
+ { key: "Outcome", label: "Outcome", kind: "select", options: GOAL_OUTCOME },
1306
+ { key: "Date", label: "Date", kind: "text", placeholder: "YYYY-MM-DD" }
1307
+ ],
1308
+ decisions: [
1309
+ { key: "Title", label: "Decision", kind: "text", required: true },
1310
+ { key: "Status", label: "Status", kind: "select", options: DECISION_STATUS }
1311
+ ],
1312
+ glossary: [
1313
+ { key: "Title", label: "Term", kind: "text", required: true },
1314
+ { key: "Status", label: "Status", kind: "select", options: GLOSSARY_STATUS }
1315
+ ],
1316
+ people: [
1317
+ { key: "Name", label: "Name", kind: "text", required: true },
1318
+ { key: "Role", label: "Role", kind: "text" },
1319
+ { key: "Segment", label: "Segment", kind: "text" }
1320
+ ]
1321
+ };
1322
+ function formFieldsFor(register) {
1323
+ return FIELDS[register];
1324
+ }
1325
+ function emptyDraft(register) {
1326
+ const draft = {};
1327
+ for (const f of formFieldsFor(register)) draft[f.key] = "";
1328
+ return draft;
1329
+ }
1330
+ function missingRequired(register, draft) {
1331
+ return formFieldsFor(register).filter((f) => f.required && !String(draft[f.key] ?? "").trim()).map((f) => f.label);
1332
+ }
1333
+ function toCreatePayload(register, draft) {
1334
+ const out = {};
1335
+ for (const f of formFieldsFor(register)) {
1336
+ const raw = String(draft[f.key] ?? "").trim();
1337
+ if (raw === "") continue;
1338
+ if (f.kind === "number" || f.coerce === "number") {
1339
+ const n = Number(raw);
1340
+ if (!Number.isNaN(n)) out[f.key] = n;
1341
+ } else {
1342
+ out[f.key] = raw;
1343
+ }
1344
+ }
1345
+ return out;
1346
+ }
1347
+
1348
+ // src/field-styles.ts
1349
+ var FIELD_LABEL_CLASS = "vos-field-label";
1350
+ var FIELD_CONTROL_CLASS = "vos-input";
1351
+
1352
+ // src/use-mutations.ts
1353
+ import { useCallback as useCallback2, useState as useState3 } from "react";
1354
+ async function postJson(url, body) {
1355
+ const res = await fetch(url, {
1356
+ method: "POST",
1357
+ headers: { "content-type": "application/json" },
1358
+ body: JSON.stringify(body)
1359
+ });
1360
+ const payload = await res.json().catch(() => null);
1361
+ if (!res.ok) {
1362
+ const message = payload?.message ?? payload?.error ?? `Request failed (${res.status})`;
1363
+ throw new Error(message);
1364
+ }
1365
+ return payload.data;
1366
+ }
1367
+ function useCreate(register, basePath = "/api") {
1368
+ const [saving, setSaving] = useState3(false);
1369
+ const [error, setError] = useState3(null);
1370
+ const create = useCallback2(
1371
+ async (data) => {
1372
+ setSaving(true);
1373
+ setError(null);
1374
+ try {
1375
+ return await postJson(`${basePath}/${register}`, data);
1376
+ } catch (e) {
1377
+ const message = e instanceof Error ? e.message : "Failed to create";
1378
+ setError(message);
1379
+ throw e;
1380
+ } finally {
1381
+ setSaving(false);
1382
+ }
1383
+ },
1384
+ [register, basePath]
1385
+ );
1386
+ return { create, saving, error };
1387
+ }
1388
+ function useLink(basePath = "/api") {
1389
+ const [linking, setLinking] = useState3(false);
1390
+ const [error, setError] = useState3(null);
1391
+ const link = useCallback2(
1392
+ async (args) => {
1393
+ setLinking(true);
1394
+ setError(null);
1395
+ try {
1396
+ await postJson(`${basePath}/link`, args);
1397
+ } catch (e) {
1398
+ const message = e instanceof Error ? e.message : "Failed to link";
1399
+ setError(message);
1400
+ throw e;
1401
+ } finally {
1402
+ setLinking(false);
1403
+ }
1404
+ },
1405
+ [basePath]
1406
+ );
1407
+ return { link, linking, error };
1408
+ }
1409
+
1410
+ // src/record-form.tsx
1411
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1412
+ function RecordForm({
1413
+ register,
1414
+ basePath,
1415
+ onCreated,
1416
+ onCancel
1417
+ }) {
1418
+ const fields = useMemo(() => formFieldsFor(register), [register]);
1419
+ const [draft, setDraft] = useState4(
1420
+ () => emptyDraft(register)
1421
+ );
1422
+ const { create, saving, error } = useCreate(register, basePath);
1423
+ const missing = missingRequired(register, draft);
1424
+ const set = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
1425
+ const onSubmit = async (e) => {
1426
+ e.preventDefault();
1427
+ if (missing.length > 0 || saving) return;
1428
+ try {
1429
+ const created = await create(toCreatePayload(register, draft));
1430
+ onCreated(created.id);
1431
+ } catch {
1432
+ }
1433
+ };
1434
+ return /* @__PURE__ */ jsxs6("form", { onSubmit, className: "vos-form", children: [
1435
+ /* @__PURE__ */ jsxs6("div", { className: "vos-form-body", children: [
1436
+ fields.map((field) => /* @__PURE__ */ jsx6(
1437
+ Field,
1438
+ {
1439
+ field,
1440
+ value: draft[field.key] ?? "",
1441
+ onChange: (v) => set(field.key, v)
1442
+ },
1443
+ field.key
1444
+ )),
1445
+ error ? /* @__PURE__ */ jsx6("p", { className: "vos-error", children: error }) : null
1446
+ ] }),
1447
+ /* @__PURE__ */ jsxs6("footer", { className: "vos-drawer-footer", children: [
1448
+ /* @__PURE__ */ jsx6("button", { type: "button", onClick: onCancel, className: "vos-btn vos-btn-ghost vos-btn-sm", children: "Cancel" }),
1449
+ /* @__PURE__ */ jsx6(
1450
+ "button",
1451
+ {
1452
+ type: "submit",
1453
+ disabled: missing.length > 0 || saving,
1454
+ title: missing.length > 0 ? `Fill in: ${missing.join(", ")}` : void 0,
1455
+ className: "vos-btn vos-btn-sm",
1456
+ children: saving ? "Creating\u2026" : `Create ${REGISTER_LABEL[register]}`
1457
+ }
1458
+ )
1459
+ ] })
1460
+ ] });
1461
+ }
1462
+ function Field({
1463
+ field,
1464
+ value,
1465
+ onChange
1466
+ }) {
1467
+ const id = `field-${field.key}`;
1468
+ return /* @__PURE__ */ jsxs6("div", { className: "vos-field", children: [
1469
+ /* @__PURE__ */ jsxs6("label", { htmlFor: id, className: FIELD_LABEL_CLASS, children: [
1470
+ field.label,
1471
+ field.required ? /* @__PURE__ */ jsx6("span", { className: "vos-req", children: " *" }) : null
1472
+ ] }),
1473
+ field.kind === "textarea" ? /* @__PURE__ */ jsx6(
1474
+ "textarea",
1475
+ {
1476
+ id,
1477
+ value,
1478
+ rows: 3,
1479
+ placeholder: field.placeholder,
1480
+ onChange: (e) => onChange(e.target.value),
1481
+ className: FIELD_CONTROL_CLASS
1482
+ }
1483
+ ) : field.kind === "select" ? /* @__PURE__ */ jsxs6(
1484
+ "select",
1485
+ {
1486
+ id,
1487
+ value,
1488
+ onChange: (e) => onChange(e.target.value),
1489
+ className: FIELD_CONTROL_CLASS,
1490
+ children: [
1491
+ /* @__PURE__ */ jsx6("option", { value: "", children: "\u2014" }),
1492
+ field.options?.map((opt) => /* @__PURE__ */ jsx6("option", { value: opt, children: opt }, opt))
1493
+ ]
1494
+ }
1495
+ ) : /* @__PURE__ */ jsx6(
1496
+ "input",
1497
+ {
1498
+ id,
1499
+ type: field.kind === "number" ? "number" : "text",
1500
+ value,
1501
+ placeholder: field.placeholder,
1502
+ onChange: (e) => onChange(e.target.value),
1503
+ className: FIELD_CONTROL_CLASS
1504
+ }
1505
+ )
1506
+ ] });
1507
+ }
1508
+
1509
+ // src/relation-editor.tsx
1510
+ import { useMemo as useMemo2, useState as useState5 } from "react";
1511
+
1512
+ // src/link-choices.ts
1513
+ import { RELATIONS } from "@validation-os/core";
1514
+ var RELATION_LABEL = {
1515
+ "assumption-reading": "Add reading",
1516
+ "assumption-depends-on": "Depends on",
1517
+ "assumption-contradicts": "Contradicts",
1518
+ "reading-experiment": "From experiment",
1519
+ "reading-goal": "From goal",
1520
+ "decision-based-on": "Based on assumption",
1521
+ "decision-resolves": "Resolves assumption",
1522
+ "goal-based-on": "Based on assumption"
1523
+ };
1524
+ function linkChoicesFrom(register) {
1525
+ return Object.keys(RELATIONS).filter((r) => RELATIONS[r].from.register === register).map((relation) => ({
1526
+ relation,
1527
+ label: RELATION_LABEL[relation],
1528
+ targetRegister: RELATIONS[relation].targetRegister
1529
+ }));
1530
+ }
1531
+
1532
+ // src/relation-editor.tsx
1533
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1534
+ function RelationEditor({
1535
+ register,
1536
+ recordId,
1537
+ basePath,
1538
+ onLinked
1539
+ }) {
1540
+ const choices = useMemo2(() => linkChoicesFrom(register), [register]);
1541
+ const [relation, setRelation] = useState5("");
1542
+ const [targetId, setTargetId] = useState5("");
1543
+ const { link, linking, error } = useLink(basePath);
1544
+ const active = choices.find((c) => c.relation === relation) ?? null;
1545
+ const { records } = useList(
1546
+ active?.targetRegister ?? register,
1547
+ basePath
1548
+ );
1549
+ const targets = (records ?? []).filter(
1550
+ (r) => !(active?.targetRegister === register && r.id === recordId)
1551
+ );
1552
+ if (choices.length === 0) return null;
1553
+ const onLink = async () => {
1554
+ if (!active || !targetId || linking) return;
1555
+ try {
1556
+ await link({
1557
+ relation: active.relation,
1558
+ from: { register, id: recordId },
1559
+ to: { register: active.targetRegister, id: targetId }
1560
+ });
1561
+ setTargetId("");
1562
+ setRelation("");
1563
+ onLinked();
1564
+ } catch {
1565
+ }
1566
+ };
1567
+ return /* @__PURE__ */ jsxs7("section", { className: "vos-relation", children: [
1568
+ /* @__PURE__ */ jsx7("h3", { className: "vos-sectitle", children: "Link a record" }),
1569
+ /* @__PURE__ */ jsxs7("div", { className: "vos-field-stack", children: [
1570
+ /* @__PURE__ */ jsxs7(
1571
+ "select",
1572
+ {
1573
+ "aria-label": "Relation",
1574
+ value: relation,
1575
+ onChange: (e) => {
1576
+ setRelation(e.target.value);
1577
+ setTargetId("");
1578
+ },
1579
+ className: FIELD_CONTROL_CLASS,
1580
+ children: [
1581
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Choose a relation\u2026" }),
1582
+ choices.map((c) => /* @__PURE__ */ jsx7("option", { value: c.relation, children: c.label }, c.relation))
1583
+ ]
1584
+ }
1585
+ ),
1586
+ active ? /* @__PURE__ */ jsxs7(
1587
+ "select",
1588
+ {
1589
+ "aria-label": "Target record",
1590
+ value: targetId,
1591
+ onChange: (e) => setTargetId(e.target.value),
1592
+ className: FIELD_CONTROL_CLASS,
1593
+ children: [
1594
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Choose a record\u2026" }),
1595
+ targets.map((r) => /* @__PURE__ */ jsx7("option", { value: r.id, children: primaryLabel(r) }, r.id))
1596
+ ]
1597
+ }
1598
+ ) : null,
1599
+ error ? /* @__PURE__ */ jsx7("p", { className: "vos-error", children: error }) : null,
1600
+ /* @__PURE__ */ jsx7(
1601
+ "button",
1602
+ {
1603
+ type: "button",
1604
+ onClick: onLink,
1605
+ disabled: !active || !targetId || linking,
1606
+ className: "vos-btn vos-btn-sm",
1607
+ style: { alignSelf: "flex-start" },
1608
+ children: linking ? "Linking\u2026" : "Link"
1609
+ }
1610
+ )
1611
+ ] })
1612
+ ] });
378
1613
  }
379
1614
 
380
1615
  // src/register-browser.tsx
381
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
382
- function RegisterBrowser({ register, basePath }) {
383
- const { records, loading, error } = useList(register, basePath);
384
- const [openId, setOpenId] = useState2(null);
1616
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1617
+ function RegisterBrowser({
1618
+ register,
1619
+ basePath,
1620
+ subtitle
1621
+ }) {
1622
+ const { records, loading, error, refresh: refreshList } = useList(
1623
+ register,
1624
+ basePath
1625
+ );
1626
+ const [openId, setOpenId] = useState6(null);
1627
+ const [creating, setCreating] = useState6(false);
385
1628
  const {
386
1629
  record,
387
1630
  loading: recordLoading,
388
- error: recordError
1631
+ error: recordError,
1632
+ refresh: refreshRecord
389
1633
  } = useRecord(register, openId, basePath);
390
- return /* @__PURE__ */ jsxs4("div", { children: [
391
- loading && !records ? /* @__PURE__ */ jsxs4("p", { className: "text-sm text-neutral-500", children: [
1634
+ return /* @__PURE__ */ jsxs8("div", { children: [
1635
+ /* @__PURE__ */ jsxs8("div", { className: "vos-head", children: [
1636
+ /* @__PURE__ */ jsxs8("div", { children: [
1637
+ /* @__PURE__ */ jsx8("h1", { children: REGISTER_LABEL[register] }),
1638
+ subtitle ? /* @__PURE__ */ jsx8("p", { children: subtitle }) : null
1639
+ ] }),
1640
+ /* @__PURE__ */ jsx8("div", { className: "vos-spacer" }),
1641
+ /* @__PURE__ */ jsx8(
1642
+ "button",
1643
+ {
1644
+ type: "button",
1645
+ onClick: () => refreshList(),
1646
+ className: "vos-btn vos-btn-ghost",
1647
+ children: "\u21BB Refresh"
1648
+ }
1649
+ ),
1650
+ /* @__PURE__ */ jsxs8(
1651
+ "button",
1652
+ {
1653
+ type: "button",
1654
+ onClick: () => setCreating(true),
1655
+ className: "vos-btn",
1656
+ children: [
1657
+ "+ New ",
1658
+ REGISTER_SINGULAR[register]
1659
+ ]
1660
+ }
1661
+ )
1662
+ ] }),
1663
+ loading && !records ? /* @__PURE__ */ jsxs8("p", { className: "vos-muted", children: [
392
1664
  "Loading ",
393
1665
  REGISTER_LABEL[register].toLowerCase(),
394
1666
  "\u2026"
395
- ] }) : error ? /* @__PURE__ */ jsx4("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : /* @__PURE__ */ jsx4(
1667
+ ] }) : error ? /* @__PURE__ */ jsx8("p", { className: "vos-error", children: error }) : /* @__PURE__ */ jsx8(
396
1668
  RegisterTable,
397
1669
  {
398
1670
  register,
@@ -401,7 +1673,7 @@ function RegisterBrowser({ register, basePath }) {
401
1673
  selectedId: openId
402
1674
  }
403
1675
  ),
404
- /* @__PURE__ */ jsx4(
1676
+ /* @__PURE__ */ jsx8(
405
1677
  RecordDrawer,
406
1678
  {
407
1679
  register,
@@ -409,20 +1681,96 @@ function RegisterBrowser({ register, basePath }) {
409
1681
  loading: recordLoading,
410
1682
  error: recordError,
411
1683
  open: openId !== null,
412
- onClose: () => setOpenId(null)
1684
+ onClose: () => setOpenId(null),
1685
+ basePath,
1686
+ onChanged: () => {
1687
+ refreshRecord();
1688
+ refreshList();
1689
+ },
1690
+ children: openId ? /* @__PURE__ */ jsx8(
1691
+ RelationEditor,
1692
+ {
1693
+ register,
1694
+ recordId: openId,
1695
+ basePath,
1696
+ onLinked: () => {
1697
+ refreshRecord();
1698
+ refreshList();
1699
+ }
1700
+ }
1701
+ ) : null
1702
+ }
1703
+ ),
1704
+ /* @__PURE__ */ jsxs8(
1705
+ DrawerShell,
1706
+ {
1707
+ open: creating,
1708
+ onClose: () => setCreating(false),
1709
+ ariaLabel: `New ${REGISTER_SINGULAR[register]} record`,
1710
+ children: [
1711
+ /* @__PURE__ */ jsx8("header", { className: "vos-drawer-header", children: /* @__PURE__ */ jsxs8("div", { children: [
1712
+ /* @__PURE__ */ jsx8("p", { className: "vos-drawer-eyebrow", children: "New" }),
1713
+ /* @__PURE__ */ jsx8("h2", { className: "vos-drawer-title", children: REGISTER_SINGULAR[register] })
1714
+ ] }) }),
1715
+ /* @__PURE__ */ jsx8(
1716
+ RecordForm,
1717
+ {
1718
+ register,
1719
+ basePath,
1720
+ onCreated: (id) => {
1721
+ setCreating(false);
1722
+ refreshList();
1723
+ setOpenId(id);
1724
+ },
1725
+ onCancel: () => setCreating(false)
1726
+ }
1727
+ )
1728
+ ]
413
1729
  }
414
1730
  )
415
1731
  ] });
416
1732
  }
417
1733
 
1734
+ // src/register-nav.tsx
1735
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1736
+ function RegisterNav({
1737
+ active,
1738
+ onSelect,
1739
+ counts,
1740
+ registers
1741
+ }) {
1742
+ const groups = REGISTER_GROUPS.map((g) => ({
1743
+ ...g,
1744
+ registers: registers ? g.registers.filter((r) => registers.includes(r)) : g.registers
1745
+ })).filter((g) => g.registers.length > 0);
1746
+ return /* @__PURE__ */ jsx9("nav", { className: "vos-nav", "aria-label": "Registers", children: groups.map((group) => /* @__PURE__ */ jsxs9("div", { children: [
1747
+ /* @__PURE__ */ jsx9("div", { className: "vos-nav-group", children: group.label }),
1748
+ group.registers.map((register) => /* @__PURE__ */ jsxs9(
1749
+ "button",
1750
+ {
1751
+ type: "button",
1752
+ className: `vos-nav-item ${register === active ? "is-active" : ""}`,
1753
+ "aria-current": register === active ? "page" : void 0,
1754
+ onClick: () => onSelect(register),
1755
+ children: [
1756
+ /* @__PURE__ */ jsx9("span", { className: "vos-nav-ic", "aria-hidden": "true", children: REGISTER_ICON[register] }),
1757
+ REGISTER_LABEL[register],
1758
+ /* @__PURE__ */ jsx9("span", { className: "vos-nav-count vos-num", children: counts?.[register] !== void 0 ? formatCount(counts[register] ?? 0) : "\xB7" })
1759
+ ]
1760
+ },
1761
+ register
1762
+ ))
1763
+ ] }, group.label)) });
1764
+ }
1765
+
418
1766
  // src/use-counts.ts
419
- import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
1767
+ import { useCallback as useCallback3, useEffect as useEffect4, useState as useState7 } from "react";
420
1768
  function useCounts(basePath = "/api") {
421
- const [counts, setCounts] = useState3(null);
422
- const [loading, setLoading] = useState3(true);
423
- const [error, setError] = useState3(null);
424
- const [tick, setTick] = useState3(0);
425
- useEffect3(() => {
1769
+ const [counts, setCounts] = useState7(null);
1770
+ const [loading, setLoading] = useState7(true);
1771
+ const [error, setError] = useState7(null);
1772
+ const [tick, setTick] = useState7(0);
1773
+ useEffect4(() => {
426
1774
  let live = true;
427
1775
  setLoading(true);
428
1776
  fetch(`${basePath}/counts`).then(async (res) => {
@@ -442,22 +1790,181 @@ function useCounts(basePath = "/api") {
442
1790
  live = false;
443
1791
  };
444
1792
  }, [basePath, tick]);
445
- const refresh = useCallback2(() => setTick((t) => t + 1), []);
1793
+ const refresh = useCallback3(() => setTick((t2) => t2 + 1), []);
446
1794
  return { counts, loading, error, refresh };
447
1795
  }
1796
+
1797
+ // src/dashboard-app.tsx
1798
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1799
+ function initialsOf(name) {
1800
+ const parts = name.trim().split(/\s+/);
1801
+ const first = parts[0]?.[0] ?? "";
1802
+ const last = parts.length > 1 ? parts[parts.length - 1]?.[0] ?? "" : "";
1803
+ return (first + last).toUpperCase() || "?";
1804
+ }
1805
+ function registerFromHash(available) {
1806
+ if (typeof window === "undefined") return null;
1807
+ const hash = window.location.hash.replace(/^#\/?/, "");
1808
+ return available.includes(hash) ? hash : null;
1809
+ }
1810
+ function ValidationOSDashboard({ config = {} }) {
1811
+ const {
1812
+ basePath = "/api",
1813
+ backendLabel,
1814
+ branding,
1815
+ agentLabel,
1816
+ user,
1817
+ registers = REGISTER_ORDER
1818
+ } = config;
1819
+ const [active, setActive] = useState8(
1820
+ () => registerFromHash(registers) ?? registers[0] ?? "assumptions"
1821
+ );
1822
+ const { counts } = useCounts(basePath);
1823
+ useEffect5(() => {
1824
+ if (typeof window === "undefined") return;
1825
+ const onHash = () => {
1826
+ const next = registerFromHash(registers);
1827
+ if (next) setActive(next);
1828
+ };
1829
+ window.addEventListener("hashchange", onHash);
1830
+ return () => window.removeEventListener("hashchange", onHash);
1831
+ }, [registers]);
1832
+ const select = useCallback4((register) => {
1833
+ setActive(register);
1834
+ if (typeof window !== "undefined") {
1835
+ window.location.hash = register;
1836
+ }
1837
+ }, []);
1838
+ const toggleTheme = useCallback4(() => {
1839
+ const root = document.documentElement;
1840
+ const current = root.getAttribute("data-theme") ?? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
1841
+ root.setAttribute("data-theme", current === "dark" ? "light" : "dark");
1842
+ }, []);
1843
+ const brandName = branding?.name ?? "Validation-OS";
1844
+ const brandMark = branding?.initials ?? "V";
1845
+ return /* @__PURE__ */ jsxs10("div", { className: "vos-app", children: [
1846
+ /* @__PURE__ */ jsxs10("div", { className: "vos-brand", children: [
1847
+ /* @__PURE__ */ jsx10("span", { className: "vos-brand-dot", children: branding?.logoUrl ? /* @__PURE__ */ jsx10("img", { src: branding.logoUrl, alt: "" }) : brandMark }),
1848
+ " ",
1849
+ brandName
1850
+ ] }),
1851
+ /* @__PURE__ */ jsxs10("div", { className: "vos-topbar", children: [
1852
+ backendLabel ? /* @__PURE__ */ jsxs10("div", { className: "vos-backend", children: [
1853
+ /* @__PURE__ */ jsx10("span", { className: "vos-live-dot" }),
1854
+ " Backend: ",
1855
+ /* @__PURE__ */ jsx10("b", { children: backendLabel })
1856
+ ] }) : null,
1857
+ agentLabel ? /* @__PURE__ */ jsxs10("span", { className: "vos-hint", children: [
1858
+ "Agent: ",
1859
+ /* @__PURE__ */ jsx10("b", { style: { color: "var(--vos-text)" }, children: agentLabel })
1860
+ ] }) : null,
1861
+ /* @__PURE__ */ jsx10("div", { className: "vos-spacer" }),
1862
+ /* @__PURE__ */ jsx10("button", { type: "button", className: "vos-iconbtn", onClick: toggleTheme, children: "\u25D0 Theme" }),
1863
+ user?.name ? /* @__PURE__ */ jsxs10("div", { className: "vos-user", children: [
1864
+ /* @__PURE__ */ jsx10("span", { className: "vos-avatar", children: initialsOf(user.name) }),
1865
+ /* @__PURE__ */ jsxs10("div", { children: [
1866
+ /* @__PURE__ */ jsx10("span", { className: "vos-user-name", children: user.name }),
1867
+ user.caption ? /* @__PURE__ */ jsx10("small", { children: user.caption }) : null
1868
+ ] })
1869
+ ] }) : null
1870
+ ] }),
1871
+ /* @__PURE__ */ jsx10(
1872
+ RegisterNav,
1873
+ {
1874
+ active,
1875
+ onSelect: select,
1876
+ counts,
1877
+ registers
1878
+ }
1879
+ ),
1880
+ /* @__PURE__ */ jsx10("main", { className: "vos-main", children: /* @__PURE__ */ jsx10(
1881
+ RegisterBrowser,
1882
+ {
1883
+ register: active,
1884
+ basePath,
1885
+ subtitle: REGISTER_SUBTITLE[active]
1886
+ },
1887
+ active
1888
+ ) })
1889
+ ] });
1890
+ }
1891
+
1892
+ // src/register-counts.tsx
1893
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1894
+ function RegisterCounts({
1895
+ counts,
1896
+ caption,
1897
+ onSelect,
1898
+ active
1899
+ }) {
1900
+ const registers = REGISTER_ORDER.filter(
1901
+ (r) => counts[r] !== void 0
1902
+ );
1903
+ return /* @__PURE__ */ jsxs11("section", { "aria-label": "Register counts", children: [
1904
+ /* @__PURE__ */ jsx11("div", { className: "vos-tile-grid", children: registers.map((register) => /* @__PURE__ */ jsx11(
1905
+ StatTile,
1906
+ {
1907
+ label: REGISTER_LABEL[register],
1908
+ value: counts[register] ?? 0,
1909
+ onClick: onSelect ? () => onSelect(register) : void 0,
1910
+ active: active === register
1911
+ },
1912
+ register
1913
+ )) }),
1914
+ caption ? /* @__PURE__ */ jsx11("p", { className: "vos-hint", style: { marginTop: 16 }, children: caption }) : null
1915
+ ] });
1916
+ }
448
1917
  export {
1918
+ CONFLICT_MESSAGE,
1919
+ ConfidenceCell,
1920
+ REGISTER_GROUPS,
1921
+ REGISTER_ICON,
449
1922
  REGISTER_LABEL,
450
1923
  REGISTER_ORDER,
1924
+ REGISTER_SINGULAR,
1925
+ REGISTER_SUBTITLE,
1926
+ RISK_CRIT,
1927
+ RISK_WARN,
451
1928
  RecordDrawer,
1929
+ RecordForm,
452
1930
  RegisterBrowser,
453
1931
  RegisterCounts,
1932
+ RegisterNav,
454
1933
  RegisterTable,
1934
+ RelationEditor,
1935
+ RiskBar,
1936
+ Sparkline,
1937
+ StatTile,
1938
+ StatusPill,
1939
+ UnderstandingPanel,
1940
+ ValidationOSDashboard,
1941
+ buildPatch,
1942
+ buildUnderstanding,
455
1943
  cellValue,
456
1944
  columnsFor,
1945
+ confidenceTone,
1946
+ draftFrom,
1947
+ editableFields,
1948
+ emptyDraft,
1949
+ formFieldsFor,
1950
+ formatCount,
1951
+ formatSigned,
457
1952
  formatValue,
1953
+ hasEdits,
1954
+ interpretSave,
1955
+ linkChoicesFrom,
1956
+ missingRequired,
458
1957
  primaryLabel,
1958
+ riskFraction,
1959
+ riskLevel,
1960
+ sparklinePath,
1961
+ statusTone,
1962
+ toCreatePayload,
459
1963
  useCounts,
1964
+ useCreate,
1965
+ useLink,
460
1966
  useList,
461
- useRecord
1967
+ useRecord,
1968
+ useUpdate
462
1969
  };
463
1970
  //# sourceMappingURL=index.js.map