@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.d.ts
CHANGED
|
@@ -1,5 +1,143 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Collection, AnyRecord, Relation } from '@validation-os/core';
|
|
4
|
+
import { Progress, MoverKind, TrajectoryPoint } from '@validation-os/core/derivation';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Everything the instance passes — config only, never secrets. The API base
|
|
8
|
+
* path points at where the Clerk-gated `@validation-os/api` routes are mounted;
|
|
9
|
+
* the backend label (from the connector config) shows in the topbar; branding
|
|
10
|
+
* and the signed-in user are optional cosmetics. Auth is the instance's Clerk
|
|
11
|
+
* provider that this renders within — no credential reaches the package.
|
|
12
|
+
*/
|
|
13
|
+
interface DashboardConfig {
|
|
14
|
+
/** Where the API is mounted (default `/api`). */
|
|
15
|
+
basePath?: string;
|
|
16
|
+
/** Topbar backend indicator, e.g. "Firestore · doshi-crm". */
|
|
17
|
+
backendLabel?: string;
|
|
18
|
+
/** Optional product branding for the sidebar. */
|
|
19
|
+
branding?: {
|
|
20
|
+
/** Product name shown next to the mark (default "Validation-OS"). */
|
|
21
|
+
name?: string;
|
|
22
|
+
/** One or two letters for the square mark (default "V"). */
|
|
23
|
+
initials?: string;
|
|
24
|
+
/** A logo image for the square mark; overrides `initials` when set. */
|
|
25
|
+
logoUrl?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Optional agent label shown in the topbar, e.g. "Claude Code". */
|
|
28
|
+
agentLabel?: string;
|
|
29
|
+
/** The signed-in user, for the topbar avatar + name. */
|
|
30
|
+
user?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
caption?: string;
|
|
33
|
+
};
|
|
34
|
+
/** Restrict/reorder the registers shown; defaults to all, in order. */
|
|
35
|
+
registers?: Collection[];
|
|
36
|
+
}
|
|
37
|
+
interface ValidationOSDashboardProps {
|
|
38
|
+
config?: DashboardConfig;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The entire styled dashboard as one mountable app (spec OPS-1280): the frame —
|
|
42
|
+
* sidebar composing register nav + live counts, topbar with the backend
|
|
43
|
+
* indicator and user — and the register views (browse → drawer → understanding)
|
|
44
|
+
* it composes from the package's own bricks. Navigation is owned here, not the
|
|
45
|
+
* host router: the active register lives in client state (synced to the URL
|
|
46
|
+
* hash), so the instance mounts this at one route and wires no routing. Styled
|
|
47
|
+
* by the package's own token sheet — the instance imports `styles.css` once and
|
|
48
|
+
* builds no UI.
|
|
49
|
+
*/
|
|
50
|
+
declare function ValidationOSDashboard({ config }: ValidationOSDashboardProps): react.JSX.Element;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure presentation logic for the dashboard's visual primitives — status→tone,
|
|
54
|
+
* risk→fraction/level, the sparkline path, and count/number formatting. Kept
|
|
55
|
+
* free of React and the DOM so the mapping that drives every pill, bar and
|
|
56
|
+
* sparkline is unit-tested at this seam (spec story 13), and the components stay
|
|
57
|
+
* thin wrappers over it.
|
|
58
|
+
*/
|
|
59
|
+
/** A pill/fill tone — maps onto the `--vos-good/warn/crit/accent` tokens. */
|
|
60
|
+
type Tone = "good" | "warn" | "crit" | "accent" | "neutral";
|
|
61
|
+
/**
|
|
62
|
+
* A record's Status → a pill tone. Live/concluded read good, testing/running
|
|
63
|
+
* read warn, invalidated/rejected read crit; everything else (Proposed, and any
|
|
64
|
+
* status we don't recognise) stays neutral rather than guessing a colour. Case-
|
|
65
|
+
* and whitespace-insensitive.
|
|
66
|
+
*/
|
|
67
|
+
declare function statusTone(status: string | null | undefined): Tone;
|
|
68
|
+
/** Risk thresholds — a belief is critical at ≥60, watch at ≥30 (prototype). */
|
|
69
|
+
declare const RISK_CRIT = 60;
|
|
70
|
+
declare const RISK_WARN = 30;
|
|
71
|
+
/** Risk (0–100) → a tone for the bar fill and the number. */
|
|
72
|
+
declare function riskLevel(risk: number): Tone;
|
|
73
|
+
/** Risk (0–100) → the bar's fill fraction (0–1), clamped. */
|
|
74
|
+
declare function riskFraction(risk: number): number;
|
|
75
|
+
/** Confidence (signed) → the tone for its number: negative reads crit. */
|
|
76
|
+
declare function confidenceTone(confidence: number): Tone;
|
|
77
|
+
/** A signed number rendered with an explicit sign: `+6`, `-3`, `0`. */
|
|
78
|
+
declare function formatSigned(n: number): string;
|
|
79
|
+
/** A count for the nav/tiles, thousands-separated. */
|
|
80
|
+
declare function formatCount(n: number): string;
|
|
81
|
+
/**
|
|
82
|
+
* An SVG polyline `d` for a sparkline over `values`, fit to a `width`×`height`
|
|
83
|
+
* box with a 2px inset. The vertical domain defaults to the data's own range
|
|
84
|
+
* (with 0 always included, so a signed series keeps its baseline meaningful);
|
|
85
|
+
* pass `min`/`max` to pin it — e.g. −100…100 for Confidence. Returns "" for
|
|
86
|
+
* fewer than two points (nothing to draw).
|
|
87
|
+
*/
|
|
88
|
+
declare function sparklinePath(values: number[], width: number, height: number, min?: number, max?: number): string;
|
|
89
|
+
|
|
90
|
+
/** A colored status pill — tone from the status, the label shown verbatim. */
|
|
91
|
+
declare function StatusPill({ status }: {
|
|
92
|
+
status: string | null | undefined;
|
|
93
|
+
}): react.JSX.Element;
|
|
94
|
+
/**
|
|
95
|
+
* A risk bar (0–100) plus its number, both toned by threshold: a longer, redder
|
|
96
|
+
* bar means a riskier belief the eye should land on first (spec story 5).
|
|
97
|
+
*/
|
|
98
|
+
declare function RiskBar({ risk }: {
|
|
99
|
+
risk: number;
|
|
100
|
+
}): react.JSX.Element;
|
|
101
|
+
/**
|
|
102
|
+
* A signed confidence reading: a tiny sparkline of its trajectory when a
|
|
103
|
+
* history is available, always the +/- number (negative reads crit) — so a row
|
|
104
|
+
* shows both where a belief stands and, when known, which way it is trending
|
|
105
|
+
* (spec story 6). With no history the number stands alone; the list endpoint
|
|
106
|
+
* carries no per-row series, so the in-row sparkline appears wherever a caller
|
|
107
|
+
* does have one (e.g. the drawer trajectory uses `Sparkline` directly).
|
|
108
|
+
*/
|
|
109
|
+
declare function ConfidenceCell({ confidence, history, }: {
|
|
110
|
+
confidence: number;
|
|
111
|
+
history?: number[];
|
|
112
|
+
}): react.JSX.Element;
|
|
113
|
+
/**
|
|
114
|
+
* A signed sparkline over `values`. A zero baseline is drawn whenever the domain
|
|
115
|
+
* spans it, so a line dipping below zero reads as a belief losing ground. The
|
|
116
|
+
* last point gets a dot. `fill` shades the area under the line. Pure geometry
|
|
117
|
+
* comes from `sparklinePath`/`sparklineY`.
|
|
118
|
+
*/
|
|
119
|
+
declare function Sparkline({ values, width, height, min, max, tone, fill, ariaLabel, }: {
|
|
120
|
+
values: number[];
|
|
121
|
+
width?: number;
|
|
122
|
+
height?: number;
|
|
123
|
+
min?: number;
|
|
124
|
+
max?: number;
|
|
125
|
+
tone?: Tone;
|
|
126
|
+
fill?: boolean;
|
|
127
|
+
ariaLabel?: string;
|
|
128
|
+
}): react.JSX.Element | null;
|
|
129
|
+
/**
|
|
130
|
+
* A glanceable stat tile — a label, a big number, and an optional sub-caption.
|
|
131
|
+
* Becomes a button when `onClick` is given (counts that double as nav). Pure
|
|
132
|
+
* presentation; the caller supplies the value.
|
|
133
|
+
*/
|
|
134
|
+
declare function StatTile({ label, value, sub, onClick, active, }: {
|
|
135
|
+
label: string;
|
|
136
|
+
value: number;
|
|
137
|
+
sub?: ReactNode;
|
|
138
|
+
onClick?: () => void;
|
|
139
|
+
active?: boolean;
|
|
140
|
+
}): react.JSX.Element;
|
|
3
141
|
|
|
4
142
|
type Counts = Partial<Record<Collection, number>>;
|
|
5
143
|
interface UseCountsResult {
|
|
@@ -15,24 +153,43 @@ interface UseCountsResult {
|
|
|
15
153
|
*/
|
|
16
154
|
declare function useCounts(basePath?: string): UseCountsResult;
|
|
17
155
|
|
|
156
|
+
interface RegisterNavProps {
|
|
157
|
+
/** The active register, highlighted with a clear active state (spec story 7). */
|
|
158
|
+
active: Collection;
|
|
159
|
+
/** Called when a register is chosen. */
|
|
160
|
+
onSelect: (register: Collection) => void;
|
|
161
|
+
/** Live per-register counts; a middle dot shows until they load. */
|
|
162
|
+
counts?: Counts | null;
|
|
163
|
+
/** Restrict/reorder the registers shown; defaults to all, grouped. */
|
|
164
|
+
registers?: Collection[];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The sidebar register nav — every register with its live count and an active
|
|
168
|
+
* state, grouped into the register set and reference data (spec story 7). A
|
|
169
|
+
* presentational brick: the caller owns which register is active and supplies
|
|
170
|
+
* the counts. The assembled `<ValidationOSDashboard />` composes this; it's also
|
|
171
|
+
* exported so anyone building their own surface can reuse it (the second level
|
|
172
|
+
* of entry). Styled with the package's own token sheet — no host Tailwind.
|
|
173
|
+
*/
|
|
174
|
+
declare function RegisterNav({ active, onSelect, counts, registers, }: RegisterNavProps): react.JSX.Element;
|
|
175
|
+
|
|
18
176
|
interface RegisterCountsProps {
|
|
19
177
|
counts: Counts;
|
|
20
178
|
/** Optional caption under the tiles (e.g. the backend the numbers came from). */
|
|
21
179
|
caption?: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
hrefFor?: (register: Collection) => string;
|
|
180
|
+
/** Optional click handler per register — makes each tile a nav button. */
|
|
181
|
+
onSelect?: (register: Collection) => void;
|
|
182
|
+
/** The active register, highlighted when tiles are nav. */
|
|
183
|
+
active?: Collection | null;
|
|
28
184
|
}
|
|
29
185
|
/**
|
|
30
186
|
* A glanceable grid of stat tiles — one per register, the register's row count
|
|
31
|
-
* as the hero number.
|
|
32
|
-
*
|
|
33
|
-
* the
|
|
187
|
+
* as the hero number. A brick kept for a counts landing; the assembled app
|
|
188
|
+
* surfaces the same counts in the sidebar nav. Presentational: the caller reads
|
|
189
|
+
* the counts (server-side, through the adapter) and hands them in. Rendered in
|
|
190
|
+
* the package's own token sheet — no host Tailwind.
|
|
34
191
|
*/
|
|
35
|
-
declare function RegisterCounts({ counts, caption,
|
|
192
|
+
declare function RegisterCounts({ counts, caption, onSelect, active, }: RegisterCountsProps): react.JSX.Element;
|
|
36
193
|
|
|
37
194
|
interface RegisterTableProps {
|
|
38
195
|
register: Collection;
|
|
@@ -44,9 +201,11 @@ interface RegisterTableProps {
|
|
|
44
201
|
}
|
|
45
202
|
/**
|
|
46
203
|
* A list table for one register — a row per record, the register's key fields
|
|
47
|
-
* as columns
|
|
48
|
-
*
|
|
49
|
-
*
|
|
204
|
+
* as columns. Assumptions read their state at a glance: a colored Status pill, a
|
|
205
|
+
* signed Confidence, and a threshold-toned Risk bar (spec stories 4–6). Which
|
|
206
|
+
* cells render as pills/bars/sparklines is declared on the column (`kind`), so
|
|
207
|
+
* the treatment stays testable at the columns seam and this component stays a
|
|
208
|
+
* dumb renderer. Presentational: the caller supplies the rows.
|
|
50
209
|
*/
|
|
51
210
|
declare function RegisterTable({ register, records, onRowClick, selectedId, }: RegisterTableProps): react.JSX.Element;
|
|
52
211
|
|
|
@@ -60,27 +219,144 @@ interface RecordDrawerProps {
|
|
|
60
219
|
/** Whether the drawer is open (a row is selected). */
|
|
61
220
|
open: boolean;
|
|
62
221
|
onClose: () => void;
|
|
222
|
+
/** API base path (default `/api`). */
|
|
223
|
+
basePath?: string;
|
|
224
|
+
/** Re-fetch the record + its list — called after a save, and the re-fetch
|
|
225
|
+
* path offered when a concurrent edit is detected. */
|
|
226
|
+
onChanged?: () => void;
|
|
227
|
+
/** Extra content below the fields in read mode — e.g. the relation editor. */
|
|
228
|
+
children?: ReactNode;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A record drawer that reads and edits, and wires relations. The derived
|
|
232
|
+
* numbers lead as a visually distinct "computed — not editable" hero (spec
|
|
233
|
+
* stories 4/10): a bordered box, each number big and mono, Confidence toned by
|
|
234
|
+
* sign and Risk by threshold, with a "Why?" reveal opening the understanding
|
|
235
|
+
* layer in the same accent/pill language (story 11). Editing recomputes those
|
|
236
|
+
* numbers server-side on save; a concurrent edit surfaces as a gentle, jargon-
|
|
237
|
+
* free prompt with a re-fetch path (story 12). In read mode the drawer hosts
|
|
238
|
+
* the relation editor (`children`). Chrome is shared via `DrawerShell`; styled
|
|
239
|
+
* with the package's own token sheet, no host Tailwind.
|
|
240
|
+
*/
|
|
241
|
+
declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, children, }: RecordDrawerProps): react.JSX.Element;
|
|
242
|
+
|
|
243
|
+
interface RecordFormProps {
|
|
244
|
+
register: Collection;
|
|
245
|
+
basePath?: string;
|
|
246
|
+
/** Called with the new record's id after a successful create. */
|
|
247
|
+
onCreated: (id: string) => void;
|
|
248
|
+
onCancel: () => void;
|
|
63
249
|
}
|
|
64
250
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
251
|
+
* The "new record" form for one register (spec user story 13). Editable own-
|
|
252
|
+
* fields only — derived numbers are computed server-side and marked
|
|
253
|
+
* computed-not-editable elsewhere; relations are wired by linking after the
|
|
254
|
+
* record exists. Presence-gap fields (5 Whys, etc.) appear as first-class
|
|
255
|
+
* textareas. On submit it POSTs through the API, which recomputes on write.
|
|
256
|
+
*/
|
|
257
|
+
declare function RecordForm({ register, basePath, onCreated, onCancel, }: RecordFormProps): react.JSX.Element;
|
|
258
|
+
|
|
259
|
+
interface RelationEditorProps {
|
|
260
|
+
/** The register of the open record initiating the link. */
|
|
261
|
+
register: Collection;
|
|
262
|
+
/** The open record's id (the `from` end). */
|
|
263
|
+
recordId: string;
|
|
264
|
+
basePath?: string;
|
|
265
|
+
/** Called after a relation is wired, so the drawer can re-fetch. */
|
|
266
|
+
onLinked: () => void;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Wire a relation from the open record (spec user story 14). Pick an edge, then
|
|
270
|
+
* a target record from the register that edge points at; linking sets both ends
|
|
271
|
+
* server-side and recomputes derived fields. Registers with no outbound edges
|
|
272
|
+
* (glossary, people) render nothing.
|
|
273
|
+
*/
|
|
274
|
+
declare function RelationEditor({ register, recordId, basePath, onLinked, }: RelationEditorProps): react.JSX.Element | null;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The understanding layer behind the Confidence "Why?" (OPS-1276): which
|
|
278
|
+
* experiments move the number (ranked by push) and how close each running
|
|
279
|
+
* experiment is to concluding, the goal/direct evidence that also moves it, and
|
|
280
|
+
* Confidence over time. It lazy-loads the readings + experiments registers (it
|
|
281
|
+
* only mounts when the Reveal is open), then derives everything through the
|
|
282
|
+
* shared derivation module. Restyled into the drawer's accent/pill language
|
|
283
|
+
* (spec story 11): signed push tracks and a signed trajectory sparkline, in the
|
|
284
|
+
* package's own token sheet — no host Tailwind.
|
|
69
285
|
*/
|
|
70
|
-
declare function
|
|
286
|
+
declare function UnderstandingPanel({ assumption, basePath, }: {
|
|
287
|
+
assumption: AnyRecord;
|
|
288
|
+
basePath?: string;
|
|
289
|
+
}): react.JSX.Element;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The understanding layer's data join (OPS-1276). Pure: given an assumption
|
|
293
|
+
* and the readings + experiments registers, it produces everything the Reveal
|
|
294
|
+
* shows — the experiments testing the belief (each with how hard it moves
|
|
295
|
+
* Confidence and how close it is to concluding), the goal/direct evidence that
|
|
296
|
+
* also moves the number, and the Confidence-over-time trajectory.
|
|
297
|
+
*
|
|
298
|
+
* The record → derivation-input mapping is `@validation-os/core`'s shared
|
|
299
|
+
* `toReadingInput`, so a reading is read here exactly as it is server-side.
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
/** An experiment testing this assumption: how hard it moves Confidence, and
|
|
303
|
+
* how close it is to concluding. `contribution` is 0 for a running experiment
|
|
304
|
+
* that has not produced a concluded reading yet — it still shows, so its
|
|
305
|
+
* progress-to-conclusion is visible. */
|
|
306
|
+
interface ExperimentView {
|
|
307
|
+
experimentId: string;
|
|
308
|
+
title: string | null;
|
|
309
|
+
status: string | null;
|
|
310
|
+
/** Signed push on Confidence; 0 until a concluded reading lands. */
|
|
311
|
+
contribution: number;
|
|
312
|
+
magnitude: number;
|
|
313
|
+
/** Concluded readings this experiment has produced for the belief. */
|
|
314
|
+
readingCount: number;
|
|
315
|
+
progress: Progress | null;
|
|
316
|
+
/** Concluded/closed — reads as done rather than in-flight. */
|
|
317
|
+
done: boolean;
|
|
318
|
+
}
|
|
319
|
+
/** Goal-rung or direct evidence that moves Confidence but is not an experiment. */
|
|
320
|
+
interface OtherMover {
|
|
321
|
+
key: string;
|
|
322
|
+
kind: Exclude<MoverKind, "experiment">;
|
|
323
|
+
goalId: string | null;
|
|
324
|
+
contribution: number;
|
|
325
|
+
magnitude: number;
|
|
326
|
+
readingCount: number;
|
|
327
|
+
}
|
|
328
|
+
interface Understanding {
|
|
329
|
+
/** The same Confidence the derived box shows. */
|
|
330
|
+
confidence: number;
|
|
331
|
+
/** Experiments testing this belief, ranked by how hard they push. */
|
|
332
|
+
experiments: ExperimentView[];
|
|
333
|
+
/** Goal/direct evidence that also moves the number. */
|
|
334
|
+
otherMovers: OtherMover[];
|
|
335
|
+
/** Confidence over time; empty when no concluded reading is dated. */
|
|
336
|
+
trajectory: TrajectoryPoint[];
|
|
337
|
+
/** Concluded readings feeding the number, across all sources. */
|
|
338
|
+
readingCount: number;
|
|
339
|
+
}
|
|
340
|
+
declare function buildUnderstanding(assumption: AnyRecord, readings: AnyRecord[], experiments: AnyRecord[]): Understanding;
|
|
71
341
|
|
|
72
342
|
interface RegisterBrowserProps {
|
|
73
343
|
register: Collection;
|
|
74
344
|
/** API base path (default `/api`). */
|
|
75
345
|
basePath?: string;
|
|
346
|
+
/** A one-line description under the register title (spec story 7/9). */
|
|
347
|
+
subtitle?: string;
|
|
76
348
|
}
|
|
77
349
|
/**
|
|
78
|
-
* The browse-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
350
|
+
* The browse-create-edit surface for one register: a list table that opens a
|
|
351
|
+
* record drawer on row click, a "New" button that opens the create form, and a
|
|
352
|
+
* relation editor in the drawer for wiring links. All reads and writes go over
|
|
353
|
+
* HTTP through the Clerk-gated API (which recomputes derived fields on write),
|
|
354
|
+
* so the browser never touches Firestore directly. After an edit saves, both
|
|
355
|
+
* the drawer's record and the list re-fetch so recomputed derived numbers show
|
|
356
|
+
* everywhere. The thin host app renders this with a `register` — that's the
|
|
357
|
+
* whole page.
|
|
82
358
|
*/
|
|
83
|
-
declare function RegisterBrowser({ register, basePath }: RegisterBrowserProps): react.JSX.Element;
|
|
359
|
+
declare function RegisterBrowser({ register, basePath, subtitle, }: RegisterBrowserProps): react.JSX.Element;
|
|
84
360
|
|
|
85
361
|
interface UseListResult {
|
|
86
362
|
records: AnyRecord[] | null;
|
|
@@ -94,12 +370,122 @@ interface UseRecordResult {
|
|
|
94
370
|
record: AnyRecord | null;
|
|
95
371
|
loading: boolean;
|
|
96
372
|
error: string | null;
|
|
373
|
+
/** Re-fetch the record — the re-fetch path after a save or edit conflict. */
|
|
374
|
+
refresh: () => void;
|
|
97
375
|
}
|
|
98
376
|
/**
|
|
99
377
|
* Fetch one record by id. `id` may be null (nothing open) — the hook stays
|
|
100
378
|
* idle until an id is supplied, so it drives a drawer that opens on row click.
|
|
379
|
+
* `refresh` re-fetches: the edit flow calls it after a save, and it's the
|
|
380
|
+
* re-fetch path offered when a write hits a concurrent-edit conflict.
|
|
101
381
|
*/
|
|
102
382
|
declare function useRecord(register: Collection, id: string | null, basePath?: string): UseRecordResult;
|
|
383
|
+
/** The outcome of a save: the recomputed record, or a rejection with a
|
|
384
|
+
* plain-language message (a concurrent-edit conflict, or another failure). */
|
|
385
|
+
type SaveResult = {
|
|
386
|
+
ok: true;
|
|
387
|
+
record: AnyRecord;
|
|
388
|
+
} | {
|
|
389
|
+
ok: false;
|
|
390
|
+
conflict: boolean;
|
|
391
|
+
message: string;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Map an update response (status + parsed body) to a `SaveResult`. Pure, so
|
|
395
|
+
* the 409/error/ok branching is unit-testable without a DOM. A 409 is a
|
|
396
|
+
* concurrent-edit conflict, surfaced in the API's plain-language copy (or the
|
|
397
|
+
* `CONFLICT_MESSAGE` fallback) — never version jargon (spec user story 12).
|
|
398
|
+
*/
|
|
399
|
+
declare function interpretSave(status: number, body: unknown): SaveResult;
|
|
400
|
+
interface UseUpdateResult {
|
|
401
|
+
/** PATCH `{ version, ...patch }`; resolves to the recomputed record or a
|
|
402
|
+
* rejection. Never throws — callers branch on `result.ok`. */
|
|
403
|
+
save: (id: string, patch: Record<string, unknown>) => Promise<SaveResult>;
|
|
404
|
+
saving: boolean;
|
|
405
|
+
/** Plain-language conflict prompt when a concurrent edit was detected. */
|
|
406
|
+
conflict: string | null;
|
|
407
|
+
/** Plain-language message for any other save failure. */
|
|
408
|
+
error: string | null;
|
|
409
|
+
/** Clear conflict/error state (e.g. when re-entering edit mode). */
|
|
410
|
+
reset: () => void;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Version-guarded update over the API write route. A stale version comes back
|
|
414
|
+
* as a 409, which we surface as a gentle, jargon-free `conflict` message (spec
|
|
415
|
+
* user story 12) — the API sends the copy, and `CONFLICT_MESSAGE` is the
|
|
416
|
+
* fallback. Derived values are recomputed server-side, so the returned record
|
|
417
|
+
* carries the authoritative numbers, never anything the client computed.
|
|
418
|
+
*/
|
|
419
|
+
declare function useUpdate(register: Collection, basePath?: string): UseUpdateResult;
|
|
420
|
+
|
|
421
|
+
interface UseCreateResult {
|
|
422
|
+
create: (data: Record<string, unknown>) => Promise<AnyRecord>;
|
|
423
|
+
saving: boolean;
|
|
424
|
+
error: string | null;
|
|
425
|
+
}
|
|
426
|
+
/** Create a record in `register`; the server stamps derived fields on write. */
|
|
427
|
+
declare function useCreate(register: Collection, basePath?: string): UseCreateResult;
|
|
428
|
+
interface LinkArgs {
|
|
429
|
+
relation: Relation;
|
|
430
|
+
from: {
|
|
431
|
+
register: Collection;
|
|
432
|
+
id: string;
|
|
433
|
+
};
|
|
434
|
+
to: {
|
|
435
|
+
register: Collection;
|
|
436
|
+
id: string;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
interface UseLinkResult {
|
|
440
|
+
link: (args: LinkArgs) => Promise<void>;
|
|
441
|
+
linking: boolean;
|
|
442
|
+
error: string | null;
|
|
443
|
+
}
|
|
444
|
+
/** Wire a relation; the API sets both ends and recomputes derived fields. */
|
|
445
|
+
declare function useLink(basePath?: string): UseLinkResult;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The pure edit-logic seam — which fields a register lets you edit, how a
|
|
449
|
+
* form draft maps back to a version-guarded patch, and the plain-language
|
|
450
|
+
* conflict copy. Kept as pure data/functions (no DOM) so the edit behaviour
|
|
451
|
+
* is unit-testable exactly like `columns.ts`; the drawer component consumes it.
|
|
452
|
+
*
|
|
453
|
+
* Derived numbers (Confidence, Risk, Derived Impact, Strength) are never
|
|
454
|
+
* editable — they are computed server-side on write (spec user story 4/11),
|
|
455
|
+
* so they never appear here. Relation links are set through `link`, a separate
|
|
456
|
+
* story, so relation-id fields aren't editable inputs either.
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
/** The plain-language conflict prompt — never version jargon (user story 12).
|
|
460
|
+
* The API returns its own copy on a 409; this is the client-side fallback. */
|
|
461
|
+
declare const CONFLICT_MESSAGE: string;
|
|
462
|
+
type FieldKind = "text" | "textarea" | "number" | "select";
|
|
463
|
+
interface FieldEditor {
|
|
464
|
+
/** The record key this editor writes. */
|
|
465
|
+
key: string;
|
|
466
|
+
/** Plain-language label shown beside the input. */
|
|
467
|
+
label: string;
|
|
468
|
+
kind: FieldKind;
|
|
469
|
+
/** For `select`: the allowed options (a leading blank = "unset"). */
|
|
470
|
+
options?: readonly string[];
|
|
471
|
+
/** An empty input clears the field to `null` rather than "". */
|
|
472
|
+
nullable?: boolean;
|
|
473
|
+
}
|
|
474
|
+
/** The fields a register lets you edit, in render order. */
|
|
475
|
+
declare function editableFields(register: Collection): FieldEditor[];
|
|
476
|
+
/** A form draft is a plain string map keyed by field — what the inputs hold. */
|
|
477
|
+
type Draft = Record<string, string>;
|
|
478
|
+
/** Build the initial form draft from a record: every editable field becomes a
|
|
479
|
+
* string input value, and missing values become empty inputs. */
|
|
480
|
+
declare function draftFrom(register: Collection, record: AnyRecord): Draft;
|
|
481
|
+
/**
|
|
482
|
+
* A version-guarded patch from a draft: the loaded `version` plus only the
|
|
483
|
+
* fields whose value actually changed (so an untouched save doesn't churn
|
|
484
|
+
* unrelated fields). The API rejects a stale version with a 409.
|
|
485
|
+
*/
|
|
486
|
+
declare function buildPatch(register: Collection, original: AnyRecord, draft: Draft): Record<string, unknown>;
|
|
487
|
+
/** Whether a draft differs from the record it was loaded from. */
|
|
488
|
+
declare function hasEdits(register: Collection, original: AnyRecord, draft: Draft): boolean;
|
|
103
489
|
|
|
104
490
|
/**
|
|
105
491
|
* Per-register table columns and value formatting — the presentational shape
|
|
@@ -108,6 +494,14 @@ declare function useRecord(register: Collection, id: string | null, basePath?: s
|
|
|
108
494
|
* headers are plain language and derived numbers are shown, never hidden.
|
|
109
495
|
*/
|
|
110
496
|
|
|
497
|
+
/**
|
|
498
|
+
* How a cell renders. `text` is plain formatted text; `status` is a colored
|
|
499
|
+
* pill; `risk` is a threshold-toned bar + number; `confidence` is a signed
|
|
500
|
+
* number (with a sparkline when a trajectory is available). Keeping this on the
|
|
501
|
+
* column — not branching by key in the table — is what makes the visual
|
|
502
|
+
* treatment declarative and testable at this seam (spec story 13).
|
|
503
|
+
*/
|
|
504
|
+
type CellKind = "text" | "status" | "risk" | "confidence";
|
|
111
505
|
interface ColumnDef {
|
|
112
506
|
/** Stable key; also the default field read from the record. */
|
|
113
507
|
key: string;
|
|
@@ -119,6 +513,8 @@ interface ColumnDef {
|
|
|
119
513
|
align?: "left" | "right";
|
|
120
514
|
/** Marks a column whose value is computed, never hand-typed (spec story 4). */
|
|
121
515
|
derived?: boolean;
|
|
516
|
+
/** How the cell renders; defaults to `text`. */
|
|
517
|
+
kind?: CellKind;
|
|
122
518
|
}
|
|
123
519
|
/** The columns to render for a register. */
|
|
124
520
|
declare function columnsFor(register: Collection): ColumnDef[];
|
|
@@ -129,10 +525,76 @@ declare function formatValue(value: unknown): string;
|
|
|
129
525
|
/** The record's headline for a row/drawer: Title, else Name, else its id. */
|
|
130
526
|
declare function primaryLabel(record: AnyRecord): string;
|
|
131
527
|
|
|
528
|
+
/**
|
|
529
|
+
* The editable-field spec for the "new record" form, per register — kept as
|
|
530
|
+
* pure data + functions (like `columns.ts`) so the create form is unit-testable
|
|
531
|
+
* without a DOM. Only a record's own scalar fields appear here: derived numbers
|
|
532
|
+
* are computed server-side (never typed), and relations are wired separately by
|
|
533
|
+
* linking (see `relations.ts` / the relation editor), so id/relation fields are
|
|
534
|
+
* deliberately absent. Vocabularies mirror `core`'s type unions.
|
|
535
|
+
*/
|
|
536
|
+
|
|
537
|
+
interface FormField {
|
|
538
|
+
/** Record field key (what the create payload uses). */
|
|
539
|
+
key: string;
|
|
540
|
+
/** Plain-language label a non-technical teammate reads. */
|
|
541
|
+
label: string;
|
|
542
|
+
kind: FieldKind;
|
|
543
|
+
/** Options for a `select`. */
|
|
544
|
+
options?: readonly string[];
|
|
545
|
+
/** A `select`/`text` value that should be stored as a number. */
|
|
546
|
+
coerce?: "number";
|
|
547
|
+
required?: boolean;
|
|
548
|
+
placeholder?: string;
|
|
549
|
+
}
|
|
550
|
+
/** The editable fields for a register's create form, in display order. */
|
|
551
|
+
declare function formFieldsFor(register: Collection): FormField[];
|
|
552
|
+
/** A blank draft: every field keyed to an empty string (form-friendly). */
|
|
553
|
+
declare function emptyDraft(register: Collection): Record<string, string>;
|
|
554
|
+
/** Labels of required fields the draft has left blank (submit-gating). */
|
|
555
|
+
declare function missingRequired(register: Collection, draft: Record<string, string>): string[];
|
|
556
|
+
/**
|
|
557
|
+
* Turn a string-keyed form draft into a create payload: numbers coerced, blank
|
|
558
|
+
* optional values dropped so the record stores only what was filled. Derived
|
|
559
|
+
* and relation fields are never here — the server computes the former and
|
|
560
|
+
* linking wires the latter.
|
|
561
|
+
*/
|
|
562
|
+
declare function toCreatePayload(register: Collection, draft: Record<string, string>): Record<string, unknown>;
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Which relations a record of a given register can initiate — the menu the
|
|
566
|
+
* relation editor offers. Derived from `core`'s single `RELATIONS` table (so
|
|
567
|
+
* the two never drift) and turned into plain-language choices, each naming the
|
|
568
|
+
* register whose records are valid link targets.
|
|
569
|
+
*/
|
|
570
|
+
|
|
571
|
+
interface LinkChoice {
|
|
572
|
+
relation: Relation;
|
|
573
|
+
/** Plain-language name of the edge, from the initiating record's side. */
|
|
574
|
+
label: string;
|
|
575
|
+
/** The register to pick a target record from. */
|
|
576
|
+
targetRegister: Collection;
|
|
577
|
+
}
|
|
578
|
+
/** The relations a record of `register` can initiate, in table order. */
|
|
579
|
+
declare function linkChoicesFrom(register: Collection): LinkChoice[];
|
|
580
|
+
|
|
132
581
|
/** Plain-language labels — the register is a surface a non-technical
|
|
133
582
|
* teammate meets, so no jargon and no code-y plurals. */
|
|
134
583
|
declare const REGISTER_LABEL: Record<Collection, string>;
|
|
584
|
+
/** Singular labels — for "New {thing}" affordances (never a naive de-pluralise
|
|
585
|
+
* that would read "New Peopl" / "New Glossary"). */
|
|
586
|
+
declare const REGISTER_SINGULAR: Record<Collection, string>;
|
|
135
587
|
/** The order tiles read left-to-right, top-to-bottom. */
|
|
136
588
|
declare const REGISTER_ORDER: Collection[];
|
|
589
|
+
/** A one-line description shown under each register's title (spec story 9). */
|
|
590
|
+
declare const REGISTER_SUBTITLE: Record<Collection, string>;
|
|
591
|
+
/** A single-glyph icon per register for the sidebar nav (matches the prototype
|
|
592
|
+
* feel; a glyph, not an icon font, so nothing external is pulled in). */
|
|
593
|
+
declare const REGISTER_ICON: Record<Collection, string>;
|
|
594
|
+
/** The sidebar groups the registers into the register set and reference data. */
|
|
595
|
+
declare const REGISTER_GROUPS: {
|
|
596
|
+
label: string;
|
|
597
|
+
registers: Collection[];
|
|
598
|
+
}[];
|
|
137
599
|
|
|
138
|
-
export { type ColumnDef, type Counts, REGISTER_LABEL, REGISTER_ORDER, RecordDrawer, type RecordDrawerProps, RegisterBrowser, type RegisterBrowserProps, RegisterCounts, type RegisterCountsProps, RegisterTable, type RegisterTableProps, type UseCountsResult, type UseListResult, type UseRecordResult, cellValue, columnsFor, formatValue, primaryLabel, useCounts, useList, useRecord };
|
|
600
|
+
export { CONFLICT_MESSAGE, type CellKind, type ColumnDef, ConfidenceCell, type Counts, type DashboardConfig, type Draft, type ExperimentView, type FieldEditor, type FieldKind, type FormField, type LinkArgs, type LinkChoice, type OtherMover, REGISTER_GROUPS, REGISTER_ICON, REGISTER_LABEL, REGISTER_ORDER, REGISTER_SINGULAR, REGISTER_SUBTITLE, RISK_CRIT, RISK_WARN, RecordDrawer, type RecordDrawerProps, RecordForm, type RecordFormProps, RegisterBrowser, type RegisterBrowserProps, RegisterCounts, type RegisterCountsProps, RegisterNav, type RegisterNavProps, RegisterTable, type RegisterTableProps, RelationEditor, type RelationEditorProps, RiskBar, type SaveResult, Sparkline, StatTile, StatusPill, type Tone, type Understanding, UnderstandingPanel, type UseCountsResult, type UseCreateResult, type UseLinkResult, type UseListResult, type UseRecordResult, type UseUpdateResult, ValidationOSDashboard, type ValidationOSDashboardProps, buildPatch, buildUnderstanding, cellValue, columnsFor, confidenceTone, draftFrom, editableFields, emptyDraft, formFieldsFor, formatCount, formatSigned, formatValue, hasEdits, interpretSave, linkChoicesFrom, missingRequired, primaryLabel, riskFraction, riskLevel, sparklinePath, statusTone, toCreatePayload, useCounts, useCreate, useLink, useList, useRecord, useUpdate };
|