@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.d.ts +487 -25
- package/dist/index.js +1681 -174
- package/dist/index.js.map +1 -1
- package/dist/styles.css +971 -0
- package/package.json +4 -3
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-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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: "
|
|
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
|
|
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__ */
|
|
478
|
+
return /* @__PURE__ */ jsx3("p", { className: "vos-empty", children: "No records yet." });
|
|
191
479
|
}
|
|
192
|
-
return /* @__PURE__ */
|
|
193
|
-
/* @__PURE__ */
|
|
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:
|
|
485
|
+
className: c.align === "right" ? "vos-r" : void 0,
|
|
198
486
|
children: c.header
|
|
199
487
|
},
|
|
200
488
|
c.key
|
|
201
489
|
)) }) }),
|
|
202
|
-
/* @__PURE__ */
|
|
490
|
+
/* @__PURE__ */ jsx3("tbody", { children: records.map((record) => {
|
|
203
491
|
const isSelected = record.id === selectedId;
|
|
204
|
-
return /* @__PURE__ */
|
|
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:
|
|
217
|
-
children: columns.map((c, i) =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
error,
|
|
245
|
-
open,
|
|
246
|
-
onClose
|
|
522
|
+
headline
|
|
247
523
|
}) {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return ()
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
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/
|
|
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((
|
|
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
|
|
382
|
-
function RegisterBrowser({
|
|
383
|
-
|
|
384
|
-
|
|
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__ */
|
|
391
|
-
|
|
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__ */
|
|
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__ */
|
|
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
|
|
1767
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState7 } from "react";
|
|
420
1768
|
function useCounts(basePath = "/api") {
|
|
421
|
-
const [counts, setCounts] =
|
|
422
|
-
const [loading, setLoading] =
|
|
423
|
-
const [error, setError] =
|
|
424
|
-
const [tick, setTick] =
|
|
425
|
-
|
|
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 =
|
|
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
|