@validation-os/dashboard 0.1.0 → 0.2.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 +104 -3
- package/dist/index.js +392 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { Collection } from '@validation-os/core';
|
|
2
|
+
import { Collection, AnyRecord } from '@validation-os/core';
|
|
3
3
|
|
|
4
4
|
type Counts = Partial<Record<Collection, number>>;
|
|
5
5
|
interface UseCountsResult {
|
|
@@ -19,6 +19,12 @@ interface RegisterCountsProps {
|
|
|
19
19
|
counts: Counts;
|
|
20
20
|
/** Optional caption under the tiles (e.g. the backend the numbers came from). */
|
|
21
21
|
caption?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional link target per register. When supplied, each tile becomes a link
|
|
24
|
+
* (a plain anchor — works with any router), so counts double as navigation
|
|
25
|
+
* into the browse tables.
|
|
26
|
+
*/
|
|
27
|
+
hrefFor?: (register: Collection) => string;
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* A glanceable grid of stat tiles — one per register, the register's row count
|
|
@@ -26,7 +32,102 @@ interface RegisterCountsProps {
|
|
|
26
32
|
* through the adapter) and hands them in. Styled with Tailwind utility classes;
|
|
27
33
|
* the host app provides Tailwind.
|
|
28
34
|
*/
|
|
29
|
-
declare function RegisterCounts({ counts, caption }: RegisterCountsProps): react.JSX.Element;
|
|
35
|
+
declare function RegisterCounts({ counts, caption, hrefFor }: RegisterCountsProps): react.JSX.Element;
|
|
36
|
+
|
|
37
|
+
interface RegisterTableProps {
|
|
38
|
+
register: Collection;
|
|
39
|
+
records: AnyRecord[];
|
|
40
|
+
/** Called with the clicked record's id — opens the read-only drawer. */
|
|
41
|
+
onRowClick?: (id: string) => void;
|
|
42
|
+
/** The id of the currently-open record, highlighted in the list. */
|
|
43
|
+
selectedId?: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A list table for one register — a row per record, the register's key fields
|
|
47
|
+
* as columns (assumptions show Impact, Confidence and Risk). Presentational:
|
|
48
|
+
* the caller supplies the rows. Clicking a row opens the read-only drawer.
|
|
49
|
+
* Styled with Tailwind utilities the host app provides.
|
|
50
|
+
*/
|
|
51
|
+
declare function RegisterTable({ register, records, onRowClick, selectedId, }: RegisterTableProps): react.JSX.Element;
|
|
52
|
+
|
|
53
|
+
interface RecordDrawerProps {
|
|
54
|
+
register: Collection;
|
|
55
|
+
/** The open record, or null while loading / when nothing is selected. */
|
|
56
|
+
record: AnyRecord | null;
|
|
57
|
+
/** True while the record is being fetched. */
|
|
58
|
+
loading?: boolean;
|
|
59
|
+
error?: string | null;
|
|
60
|
+
/** Whether the drawer is open (a row is selected). */
|
|
61
|
+
open: boolean;
|
|
62
|
+
onClose: () => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A read-only drawer for one record. Any derived numbers lead as the hero,
|
|
66
|
+
* explicitly marked computed — not editable (spec user story 4), so a reader
|
|
67
|
+
* trusts they follow the formulas; the record's own fields follow. This slice
|
|
68
|
+
* is read-only; editing lands in a later slice.
|
|
69
|
+
*/
|
|
70
|
+
declare function RecordDrawer({ register, record, loading, error, open, onClose, }: RecordDrawerProps): react.JSX.Element | null;
|
|
71
|
+
|
|
72
|
+
interface RegisterBrowserProps {
|
|
73
|
+
register: Collection;
|
|
74
|
+
/** API base path (default `/api`). */
|
|
75
|
+
basePath?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The browse-and-open surface for one register: a list table that opens a
|
|
79
|
+
* read-only drawer on row click. Reads over HTTP through the Clerk-gated API
|
|
80
|
+
* read routes (list + get), so the browser never touches Firestore directly.
|
|
81
|
+
* The thin host app renders this with a `register` — that's the whole page.
|
|
82
|
+
*/
|
|
83
|
+
declare function RegisterBrowser({ register, basePath }: RegisterBrowserProps): react.JSX.Element;
|
|
84
|
+
|
|
85
|
+
interface UseListResult {
|
|
86
|
+
records: AnyRecord[] | null;
|
|
87
|
+
loading: boolean;
|
|
88
|
+
error: string | null;
|
|
89
|
+
refresh: () => void;
|
|
90
|
+
}
|
|
91
|
+
/** Fetch every row of a register. */
|
|
92
|
+
declare function useList(register: Collection, basePath?: string): UseListResult;
|
|
93
|
+
interface UseRecordResult {
|
|
94
|
+
record: AnyRecord | null;
|
|
95
|
+
loading: boolean;
|
|
96
|
+
error: string | null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fetch one record by id. `id` may be null (nothing open) — the hook stays
|
|
100
|
+
* idle until an id is supplied, so it drives a drawer that opens on row click.
|
|
101
|
+
*/
|
|
102
|
+
declare function useRecord(register: Collection, id: string | null, basePath?: string): UseRecordResult;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Per-register table columns and value formatting — the presentational shape
|
|
106
|
+
* of the browse tables, kept as pure data/functions so it is unit-testable
|
|
107
|
+
* without a DOM. The register is a surface a non-technical teammate meets, so
|
|
108
|
+
* headers are plain language and derived numbers are shown, never hidden.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
interface ColumnDef {
|
|
112
|
+
/** Stable key; also the default field read from the record. */
|
|
113
|
+
key: string;
|
|
114
|
+
/** Plain-language column header. */
|
|
115
|
+
header: string;
|
|
116
|
+
/** Pull the display value from a record (defaults to `record[key]`). */
|
|
117
|
+
accessor?: (record: AnyRecord) => unknown;
|
|
118
|
+
/** Numeric columns right-align for a clean column of figures. */
|
|
119
|
+
align?: "left" | "right";
|
|
120
|
+
/** Marks a column whose value is computed, never hand-typed (spec story 4). */
|
|
121
|
+
derived?: boolean;
|
|
122
|
+
}
|
|
123
|
+
/** The columns to render for a register. */
|
|
124
|
+
declare function columnsFor(register: Collection): ColumnDef[];
|
|
125
|
+
/** Read a column's raw value from a record (accessor, else `record[key]`). */
|
|
126
|
+
declare function cellValue(column: ColumnDef, record: AnyRecord): unknown;
|
|
127
|
+
/** Format any stored value for display; empty/missing reads as an em dash. */
|
|
128
|
+
declare function formatValue(value: unknown): string;
|
|
129
|
+
/** The record's headline for a row/drawer: Title, else Name, else its id. */
|
|
130
|
+
declare function primaryLabel(record: AnyRecord): string;
|
|
30
131
|
|
|
31
132
|
/** Plain-language labels — the register is a surface a non-technical
|
|
32
133
|
* teammate meets, so no jargon and no code-y plurals. */
|
|
@@ -34,4 +135,4 @@ declare const REGISTER_LABEL: Record<Collection, string>;
|
|
|
34
135
|
/** The order tiles read left-to-right, top-to-bottom. */
|
|
35
136
|
declare const REGISTER_ORDER: Collection[];
|
|
36
137
|
|
|
37
|
-
export { type Counts, REGISTER_LABEL, REGISTER_ORDER, RegisterCounts, type RegisterCountsProps, type UseCountsResult, useCounts };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,8 @@ var REGISTER_ORDER = [
|
|
|
21
21
|
];
|
|
22
22
|
|
|
23
23
|
// src/register-counts.tsx
|
|
24
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
25
|
-
function RegisterCounts({ counts, caption }) {
|
|
24
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
25
|
+
function RegisterCounts({ counts, caption, hrefFor }) {
|
|
26
26
|
const registers = REGISTER_ORDER.filter(
|
|
27
27
|
(r) => counts[r] !== void 0
|
|
28
28
|
);
|
|
@@ -31,28 +31,398 @@ function RegisterCounts({ counts, caption }) {
|
|
|
31
31
|
StatTile,
|
|
32
32
|
{
|
|
33
33
|
label: REGISTER_LABEL[register],
|
|
34
|
-
value: counts[register] ?? 0
|
|
34
|
+
value: counts[register] ?? 0,
|
|
35
|
+
href: hrefFor?.(register)
|
|
35
36
|
},
|
|
36
37
|
register
|
|
37
38
|
)) }),
|
|
38
39
|
caption ? /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-neutral-500 dark:text-neutral-400", children: caption }) : null
|
|
39
40
|
] });
|
|
40
41
|
}
|
|
41
|
-
function StatTile({
|
|
42
|
-
|
|
42
|
+
function StatTile({
|
|
43
|
+
label,
|
|
44
|
+
value,
|
|
45
|
+
href
|
|
46
|
+
}) {
|
|
47
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
48
|
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-neutral-500 dark:text-neutral-400", children: label }),
|
|
44
49
|
/* @__PURE__ */ jsx("div", { className: "mt-1 text-3xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50", children: value.toLocaleString() })
|
|
45
50
|
] });
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/columns.ts
|
|
63
|
+
function derivedField(field) {
|
|
64
|
+
return (r) => r.derived?.[field];
|
|
65
|
+
}
|
|
66
|
+
var COLUMNS = {
|
|
67
|
+
assumptions: [
|
|
68
|
+
{ key: "Title", header: "Assumption" },
|
|
69
|
+
{ key: "Impact", header: "Impact", align: "right" },
|
|
70
|
+
{
|
|
71
|
+
key: "confidence",
|
|
72
|
+
header: "Confidence",
|
|
73
|
+
align: "right",
|
|
74
|
+
derived: true,
|
|
75
|
+
accessor: derivedField("confidence")
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "risk",
|
|
79
|
+
header: "Risk",
|
|
80
|
+
align: "right",
|
|
81
|
+
derived: true,
|
|
82
|
+
accessor: derivedField("risk")
|
|
83
|
+
},
|
|
84
|
+
{ key: "Status", header: "Status" }
|
|
85
|
+
],
|
|
86
|
+
experiments: [
|
|
87
|
+
{ key: "Title", header: "Experiment" },
|
|
88
|
+
{ key: "Status", header: "Status" },
|
|
89
|
+
{ key: "Feasibility", header: "Feasibility" }
|
|
90
|
+
],
|
|
91
|
+
readings: [
|
|
92
|
+
{ key: "Title", header: "Reading" },
|
|
93
|
+
{ key: "Result", header: "Result" },
|
|
94
|
+
{ key: "Rung", header: "Rung" },
|
|
95
|
+
{
|
|
96
|
+
key: "strength",
|
|
97
|
+
header: "Strength",
|
|
98
|
+
align: "right",
|
|
99
|
+
derived: true,
|
|
100
|
+
accessor: derivedField("strength")
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
goals: [
|
|
104
|
+
{ key: "Title", header: "Goal" },
|
|
105
|
+
{ key: "Status", header: "Status" },
|
|
106
|
+
{ key: "Outcome", header: "Outcome" }
|
|
107
|
+
],
|
|
108
|
+
decisions: [
|
|
109
|
+
{ key: "Title", header: "Decision" },
|
|
110
|
+
{ key: "Status", header: "Status" }
|
|
111
|
+
],
|
|
112
|
+
glossary: [
|
|
113
|
+
{ key: "Title", header: "Term" },
|
|
114
|
+
{ key: "Status", header: "Status" }
|
|
115
|
+
],
|
|
116
|
+
people: [
|
|
117
|
+
{ key: "Name", header: "Name" },
|
|
118
|
+
{ key: "Role", header: "Role" },
|
|
119
|
+
{ key: "Segment", header: "Segment" }
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
function columnsFor(register) {
|
|
123
|
+
return COLUMNS[register];
|
|
124
|
+
}
|
|
125
|
+
function cellValue(column, record) {
|
|
126
|
+
return column.accessor ? column.accessor(record) : record[column.key];
|
|
127
|
+
}
|
|
128
|
+
function formatValue(value) {
|
|
129
|
+
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
130
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
return value.length ? value.map(formatScalar).join(", ") : "\u2014";
|
|
133
|
+
}
|
|
134
|
+
return formatScalar(value);
|
|
135
|
+
}
|
|
136
|
+
function formatScalar(value) {
|
|
137
|
+
if (value === null || value === void 0) return "\u2014";
|
|
138
|
+
if (typeof value === "number") return String(value);
|
|
139
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
140
|
+
return String(value);
|
|
141
|
+
}
|
|
142
|
+
function primaryLabel(record) {
|
|
143
|
+
const title = record.Title ?? record.Name;
|
|
144
|
+
return typeof title === "string" && title.trim() ? title : record.id;
|
|
145
|
+
}
|
|
146
|
+
var DERIVED_LABEL = {
|
|
147
|
+
confidence: "Confidence",
|
|
148
|
+
risk: "Risk",
|
|
149
|
+
derivedImpact: "Derived Impact",
|
|
150
|
+
impact: "Derived Impact",
|
|
151
|
+
sourceQuality: "Source quality",
|
|
152
|
+
strength: "Strength"
|
|
153
|
+
};
|
|
154
|
+
var FIELD_LABEL = {
|
|
155
|
+
dependsOnIds: "Depends on",
|
|
156
|
+
enablesIds: "Enables",
|
|
157
|
+
contradictsIds: "Contradicts",
|
|
158
|
+
readingIds: "Readings",
|
|
159
|
+
assumptionId: "Assumption",
|
|
160
|
+
experimentId: "Experiment",
|
|
161
|
+
goalId: "Goal",
|
|
162
|
+
basedOnIds: "Based on",
|
|
163
|
+
resolvesIds: "Resolves",
|
|
164
|
+
barLineAssumptionIds: "Assumptions tested",
|
|
165
|
+
closureReason: "Closure reason",
|
|
166
|
+
magnitudeBand: "Magnitude band",
|
|
167
|
+
moot: "Moot"
|
|
168
|
+
};
|
|
169
|
+
function fieldLabel(key) {
|
|
170
|
+
const override = FIELD_LABEL[key];
|
|
171
|
+
if (override) return override;
|
|
172
|
+
if (/^[A-Z]/.test(key)) return key;
|
|
173
|
+
const spaced = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
174
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
175
|
+
}
|
|
176
|
+
function derivedLabel(key) {
|
|
177
|
+
return DERIVED_LABEL[key] ?? fieldLabel(key);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/register-table.tsx
|
|
181
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
182
|
+
function RegisterTable({
|
|
183
|
+
register,
|
|
184
|
+
records,
|
|
185
|
+
onRowClick,
|
|
186
|
+
selectedId
|
|
187
|
+
}) {
|
|
188
|
+
const columns = columnsFor(register);
|
|
189
|
+
if (records.length === 0) {
|
|
190
|
+
return /* @__PURE__ */ jsx2("p", { className: "rounded-xl border border-dashed border-neutral-300 p-8 text-center text-sm text-neutral-500 dark:border-neutral-700 dark:text-neutral-400", children: "No records yet." });
|
|
191
|
+
}
|
|
192
|
+
return /* @__PURE__ */ jsx2("div", { className: "overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800", children: /* @__PURE__ */ jsxs2("table", { className: "w-full border-collapse text-left text-sm", children: [
|
|
193
|
+
/* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { className: "border-b border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900", children: columns.map((c) => /* @__PURE__ */ jsx2(
|
|
194
|
+
"th",
|
|
195
|
+
{
|
|
196
|
+
scope: "col",
|
|
197
|
+
className: `px-4 py-2.5 font-medium text-neutral-500 dark:text-neutral-400 ${c.align === "right" ? "text-right tabular-nums" : "text-left"}`,
|
|
198
|
+
children: c.header
|
|
199
|
+
},
|
|
200
|
+
c.key
|
|
201
|
+
)) }) }),
|
|
202
|
+
/* @__PURE__ */ jsx2("tbody", { children: records.map((record) => {
|
|
203
|
+
const isSelected = record.id === selectedId;
|
|
204
|
+
return /* @__PURE__ */ jsx2(
|
|
205
|
+
"tr",
|
|
206
|
+
{
|
|
207
|
+
onClick: () => onRowClick?.(record.id),
|
|
208
|
+
onKeyDown: onRowClick ? (e) => {
|
|
209
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
onRowClick(record.id);
|
|
212
|
+
}
|
|
213
|
+
} : void 0,
|
|
214
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
215
|
+
"aria-selected": isSelected,
|
|
216
|
+
className: `border-b border-neutral-100 last:border-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 dark:border-neutral-900 ${onRowClick ? "cursor-pointer" : ""} ${isSelected ? "bg-blue-50 dark:bg-blue-950/40" : "hover:bg-neutral-50 dark:hover:bg-neutral-900/60"}`,
|
|
217
|
+
children: columns.map((c, i) => {
|
|
218
|
+
const raw = cellValue(c, record);
|
|
219
|
+
const text = i === 0 && (raw === null || raw === void 0 || raw === "") ? primaryLabel(record) : formatValue(raw);
|
|
220
|
+
return /* @__PURE__ */ jsx2(
|
|
221
|
+
"td",
|
|
222
|
+
{
|
|
223
|
+
className: `px-4 py-2.5 ${c.align === "right" ? "text-right tabular-nums text-neutral-700 dark:text-neutral-300" : "text-left"} ${i === 0 ? "font-medium text-neutral-900 dark:text-neutral-100" : "text-neutral-600 dark:text-neutral-400"}`,
|
|
224
|
+
children: text
|
|
225
|
+
},
|
|
226
|
+
c.key
|
|
227
|
+
);
|
|
228
|
+
})
|
|
229
|
+
},
|
|
230
|
+
record.id
|
|
231
|
+
);
|
|
232
|
+
}) })
|
|
233
|
+
] }) });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/record-drawer.tsx
|
|
237
|
+
import { useEffect, useRef } from "react";
|
|
238
|
+
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
239
|
+
var META_FIELDS = /* @__PURE__ */ new Set(["id", "version", "createdAt", "updatedAt", "derived"]);
|
|
240
|
+
function RecordDrawer({
|
|
241
|
+
register,
|
|
242
|
+
record,
|
|
243
|
+
loading,
|
|
244
|
+
error,
|
|
245
|
+
open,
|
|
246
|
+
onClose
|
|
247
|
+
}) {
|
|
248
|
+
const closeButtonRef = useRef(null);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!open) return;
|
|
251
|
+
const onKey = (e) => {
|
|
252
|
+
if (e.key === "Escape") onClose();
|
|
253
|
+
};
|
|
254
|
+
document.addEventListener("keydown", onKey);
|
|
255
|
+
closeButtonRef.current?.focus();
|
|
256
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
257
|
+
}, [open, onClose]);
|
|
258
|
+
if (!open) return null;
|
|
259
|
+
const derived = record && record.derived && typeof record.derived === "object" ? record.derived : null;
|
|
260
|
+
const fields = record ? Object.keys(record).filter((k) => !META_FIELDS.has(k)) : [];
|
|
261
|
+
return /* @__PURE__ */ jsxs3("div", { className: "fixed inset-0 z-50 flex justify-end", children: [
|
|
262
|
+
/* @__PURE__ */ jsx3(
|
|
263
|
+
"button",
|
|
264
|
+
{
|
|
265
|
+
type: "button",
|
|
266
|
+
"aria-label": "Close",
|
|
267
|
+
onClick: onClose,
|
|
268
|
+
className: "absolute inset-0 bg-neutral-950/30 backdrop-blur-sm"
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
/* @__PURE__ */ jsxs3(
|
|
272
|
+
"aside",
|
|
273
|
+
{
|
|
274
|
+
role: "dialog",
|
|
275
|
+
"aria-modal": "true",
|
|
276
|
+
"aria-label": `${REGISTER_LABEL[register]} record`,
|
|
277
|
+
className: "relative flex h-full w-full max-w-md flex-col overflow-y-auto border-l border-neutral-200 bg-white shadow-xl dark:border-neutral-800 dark:bg-neutral-950",
|
|
278
|
+
children: [
|
|
279
|
+
/* @__PURE__ */ jsxs3("header", { className: "flex items-start justify-between gap-4 border-b border-neutral-200 p-5 dark:border-neutral-800", children: [
|
|
280
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
281
|
+
/* @__PURE__ */ jsx3("p", { className: "text-xs font-medium uppercase tracking-wide text-neutral-400", children: REGISTER_LABEL[register] }),
|
|
282
|
+
/* @__PURE__ */ jsx3("h2", { className: "mt-1 text-lg font-semibold text-neutral-900 dark:text-neutral-50", children: record ? primaryLabel(record) : loading ? "Loading\u2026" : "\u2014" })
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsx3(
|
|
285
|
+
"button",
|
|
286
|
+
{
|
|
287
|
+
ref: closeButtonRef,
|
|
288
|
+
type: "button",
|
|
289
|
+
onClick: onClose,
|
|
290
|
+
className: "rounded-md px-2 py-1 text-sm text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
|
291
|
+
children: "Close"
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
] }),
|
|
295
|
+
/* @__PURE__ */ jsx3("div", { className: "flex-1 p-5", children: loading ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-neutral-500", children: "Loading record\u2026" }) : error ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : !record ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-neutral-500", children: "No record." }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
296
|
+
derived ? /* @__PURE__ */ jsxs3("section", { className: "mb-6", children: [
|
|
297
|
+
/* @__PURE__ */ jsx3("h3", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400", children: "Computed \xB7 not editable" }),
|
|
298
|
+
/* @__PURE__ */ jsx3("dl", { className: "grid grid-cols-3 gap-3", children: Object.entries(derived).map(([key, value]) => /* @__PURE__ */ jsxs3(
|
|
299
|
+
"div",
|
|
300
|
+
{
|
|
301
|
+
className: "rounded-lg border border-neutral-200 bg-neutral-50 p-3 dark:border-neutral-800 dark:bg-neutral-900",
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ jsx3("dt", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: derivedLabel(key) }),
|
|
304
|
+
/* @__PURE__ */ jsx3("dd", { className: "mt-1 text-xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50", children: formatValue(value) })
|
|
305
|
+
]
|
|
306
|
+
},
|
|
307
|
+
key
|
|
308
|
+
)) })
|
|
309
|
+
] }) : null,
|
|
310
|
+
/* @__PURE__ */ jsx3("dl", { className: "divide-y divide-neutral-100 dark:divide-neutral-900", children: fields.map((key) => /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-3 py-2.5", children: [
|
|
311
|
+
/* @__PURE__ */ jsx3("dt", { className: "text-sm text-neutral-500 dark:text-neutral-400", children: fieldLabel(key) }),
|
|
312
|
+
/* @__PURE__ */ jsx3("dd", { className: "col-span-2 text-sm text-neutral-800 dark:text-neutral-200", children: formatValue(record[key]) })
|
|
313
|
+
] }, key)) })
|
|
314
|
+
] }) }),
|
|
315
|
+
record ? /* @__PURE__ */ jsxs3("footer", { className: "border-t border-neutral-200 p-5 text-xs text-neutral-400 dark:border-neutral-800", children: [
|
|
316
|
+
record.id,
|
|
317
|
+
" \xB7 version ",
|
|
318
|
+
String(record.version),
|
|
319
|
+
" \xB7 updated",
|
|
320
|
+
" ",
|
|
321
|
+
formatValue(record.updatedAt)
|
|
322
|
+
] }) : null
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
] });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/register-browser.tsx
|
|
330
|
+
import { useState as useState2 } from "react";
|
|
331
|
+
|
|
332
|
+
// src/use-records.ts
|
|
333
|
+
import { useCallback, useEffect as useEffect2, useState } from "react";
|
|
334
|
+
function pickData(body) {
|
|
335
|
+
return body.data;
|
|
336
|
+
}
|
|
337
|
+
function useJsonResource(url) {
|
|
338
|
+
const [state, setState] = useState({
|
|
339
|
+
data: null,
|
|
340
|
+
loading: url !== null,
|
|
341
|
+
error: null
|
|
342
|
+
});
|
|
343
|
+
const [tick, setTick] = useState(0);
|
|
344
|
+
useEffect2(() => {
|
|
345
|
+
if (url === null) {
|
|
346
|
+
setState({ data: null, loading: false, error: null });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let live = true;
|
|
350
|
+
setState((s) => ({ ...s, loading: true }));
|
|
351
|
+
fetch(url).then(async (res) => {
|
|
352
|
+
if (!res.ok) throw new Error(`Request to ${url} failed (${res.status})`);
|
|
353
|
+
return pickData(await res.json());
|
|
354
|
+
}).then((data) => live && setState({ data, loading: false, error: null })).catch(
|
|
355
|
+
(e) => live && setState({
|
|
356
|
+
data: null,
|
|
357
|
+
loading: false,
|
|
358
|
+
error: e instanceof Error ? e.message : "Failed to load"
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
return () => {
|
|
362
|
+
live = false;
|
|
363
|
+
};
|
|
364
|
+
}, [url, tick]);
|
|
365
|
+
const refresh = useCallback(() => setTick((t) => t + 1), []);
|
|
366
|
+
return { ...state, refresh };
|
|
367
|
+
}
|
|
368
|
+
function useList(register, basePath = "/api") {
|
|
369
|
+
const { data, loading, error, refresh } = useJsonResource(
|
|
370
|
+
`${basePath}/${register}`
|
|
371
|
+
);
|
|
372
|
+
return { records: data, loading, error, refresh };
|
|
373
|
+
}
|
|
374
|
+
function useRecord(register, id, basePath = "/api") {
|
|
375
|
+
const url = id ? `${basePath}/${register}/${encodeURIComponent(id)}` : null;
|
|
376
|
+
const { data, loading, error } = useJsonResource(url);
|
|
377
|
+
return { record: data, loading, error };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/register-browser.tsx
|
|
381
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
382
|
+
function RegisterBrowser({ register, basePath }) {
|
|
383
|
+
const { records, loading, error } = useList(register, basePath);
|
|
384
|
+
const [openId, setOpenId] = useState2(null);
|
|
385
|
+
const {
|
|
386
|
+
record,
|
|
387
|
+
loading: recordLoading,
|
|
388
|
+
error: recordError
|
|
389
|
+
} = useRecord(register, openId, basePath);
|
|
390
|
+
return /* @__PURE__ */ jsxs4("div", { children: [
|
|
391
|
+
loading && !records ? /* @__PURE__ */ jsxs4("p", { className: "text-sm text-neutral-500", children: [
|
|
392
|
+
"Loading ",
|
|
393
|
+
REGISTER_LABEL[register].toLowerCase(),
|
|
394
|
+
"\u2026"
|
|
395
|
+
] }) : error ? /* @__PURE__ */ jsx4("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : /* @__PURE__ */ jsx4(
|
|
396
|
+
RegisterTable,
|
|
397
|
+
{
|
|
398
|
+
register,
|
|
399
|
+
records: records ?? [],
|
|
400
|
+
onRowClick: setOpenId,
|
|
401
|
+
selectedId: openId
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
/* @__PURE__ */ jsx4(
|
|
405
|
+
RecordDrawer,
|
|
406
|
+
{
|
|
407
|
+
register,
|
|
408
|
+
record,
|
|
409
|
+
loading: recordLoading,
|
|
410
|
+
error: recordError,
|
|
411
|
+
open: openId !== null,
|
|
412
|
+
onClose: () => setOpenId(null)
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
] });
|
|
46
416
|
}
|
|
47
417
|
|
|
48
418
|
// src/use-counts.ts
|
|
49
|
-
import { useCallback, useEffect, useState } from "react";
|
|
419
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
|
|
50
420
|
function useCounts(basePath = "/api") {
|
|
51
|
-
const [counts, setCounts] =
|
|
52
|
-
const [loading, setLoading] =
|
|
53
|
-
const [error, setError] =
|
|
54
|
-
const [tick, setTick] =
|
|
55
|
-
|
|
421
|
+
const [counts, setCounts] = useState3(null);
|
|
422
|
+
const [loading, setLoading] = useState3(true);
|
|
423
|
+
const [error, setError] = useState3(null);
|
|
424
|
+
const [tick, setTick] = useState3(0);
|
|
425
|
+
useEffect3(() => {
|
|
56
426
|
let live = true;
|
|
57
427
|
setLoading(true);
|
|
58
428
|
fetch(`${basePath}/counts`).then(async (res) => {
|
|
@@ -72,13 +442,22 @@ function useCounts(basePath = "/api") {
|
|
|
72
442
|
live = false;
|
|
73
443
|
};
|
|
74
444
|
}, [basePath, tick]);
|
|
75
|
-
const refresh =
|
|
445
|
+
const refresh = useCallback2(() => setTick((t) => t + 1), []);
|
|
76
446
|
return { counts, loading, error, refresh };
|
|
77
447
|
}
|
|
78
448
|
export {
|
|
79
449
|
REGISTER_LABEL,
|
|
80
450
|
REGISTER_ORDER,
|
|
451
|
+
RecordDrawer,
|
|
452
|
+
RegisterBrowser,
|
|
81
453
|
RegisterCounts,
|
|
82
|
-
|
|
454
|
+
RegisterTable,
|
|
455
|
+
cellValue,
|
|
456
|
+
columnsFor,
|
|
457
|
+
formatValue,
|
|
458
|
+
primaryLabel,
|
|
459
|
+
useCounts,
|
|
460
|
+
useList,
|
|
461
|
+
useRecord
|
|
83
462
|
};
|
|
84
463
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/labels.ts","../src/register-counts.tsx","../src/use-counts.ts"],"sourcesContent":["import type { Collection } from \"@validation-os/core\";\n\n/** Plain-language labels — the register is a surface a non-technical\n * teammate meets, so no jargon and no code-y plurals. */\nexport const REGISTER_LABEL: Record<Collection, string> = {\n assumptions: \"Assumptions\",\n experiments: \"Experiments\",\n readings: \"Readings\",\n goals: \"Goals\",\n decisions: \"Decisions\",\n glossary: \"Glossary\",\n people: \"People\",\n};\n\n/** The order tiles read left-to-right, top-to-bottom. */\nexport const REGISTER_ORDER: Collection[] = [\n \"assumptions\",\n \"experiments\",\n \"readings\",\n \"goals\",\n \"decisions\",\n \"glossary\",\n \"people\",\n];\n","import type { Collection } from \"@validation-os/core\";\nimport { REGISTER_LABEL, REGISTER_ORDER } from \"./labels.js\";\nimport type { Counts } from \"./use-counts.js\";\n\nexport interface RegisterCountsProps {\n counts: Counts;\n /** Optional caption under the tiles (e.g. the backend the numbers came from). */\n caption?: string;\n}\n\n/**\n * A glanceable grid of stat tiles — one per register, the register's row count\n * as the hero number. Presentational: the caller reads the counts (server-side,\n * through the adapter) and hands them in. Styled with Tailwind utility classes;\n * the host app provides Tailwind.\n */\nexport function RegisterCounts({ counts, caption }: RegisterCountsProps) {\n const registers = REGISTER_ORDER.filter(\n (r): r is Collection => counts[r] !== undefined,\n );\n return (\n <section aria-label=\"Register counts\">\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {registers.map((register) => (\n <StatTile\n key={register}\n label={REGISTER_LABEL[register]}\n value={counts[register] ?? 0}\n />\n ))}\n </div>\n {caption ? (\n <p className=\"mt-4 text-sm text-neutral-500 dark:text-neutral-400\">\n {caption}\n </p>\n ) : null}\n </section>\n );\n}\n\nfunction StatTile({ label, value }: { label: string; value: number }) {\n return (\n <div className=\"rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900\">\n <div className=\"text-sm font-medium text-neutral-500 dark:text-neutral-400\">\n {label}\n </div>\n <div className=\"mt-1 text-3xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50\">\n {value.toLocaleString()}\n </div>\n </div>\n );\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { Collection } from \"@validation-os/core\";\n\nexport type Counts = Partial<Record<Collection, number>>;\n\nexport interface UseCountsResult {\n counts: Counts | null;\n loading: boolean;\n error: string | null;\n refresh: () => void;\n}\n\n/**\n * Client hook: fetch per-register counts from the API (`GET {basePath}/counts`).\n * The API reads Firestore server-side through the adapter; this only speaks\n * HTTP, so no backend credentials ever reach the browser.\n */\nexport function useCounts(basePath = \"/api\"): UseCountsResult {\n const [counts, setCounts] = useState<Counts | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [tick, setTick] = useState(0);\n\n useEffect(() => {\n let live = true;\n setLoading(true);\n fetch(`${basePath}/counts`)\n .then(async (res) => {\n if (!res.ok) throw new Error(`Counts request failed (${res.status})`);\n return (await res.json()) as { counts: Counts };\n })\n .then((body) => {\n if (!live) return;\n setCounts(body.counts);\n setError(null);\n })\n .catch((e: unknown) => {\n if (!live) return;\n setError(e instanceof Error ? e.message : \"Failed to load counts\");\n })\n .finally(() => {\n if (live) setLoading(false);\n });\n return () => {\n live = false;\n };\n }, [basePath, tick]);\n\n const refresh = useCallback(() => setTick((t) => t + 1), []);\n return { counts, loading, error, refresh };\n}\n"],"mappings":";;;AAIO,IAAM,iBAA6C;AAAA,EACxD,aAAa;AAAA,EACb,aAAa;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AACV;AAGO,IAAM,iBAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;ACFI,SAGM,KAHN;AALG,SAAS,eAAe,EAAE,QAAQ,QAAQ,GAAwB;AACvE,QAAM,YAAY,eAAe;AAAA,IAC/B,CAAC,MAAuB,OAAO,CAAC,MAAM;AAAA,EACxC;AACA,SACE,qBAAC,aAAQ,cAAW,mBAClB;AAAA,wBAAC,SAAI,WAAU,wDACZ,oBAAU,IAAI,CAAC,aACd;AAAA,MAAC;AAAA;AAAA,QAEC,OAAO,eAAe,QAAQ;AAAA,QAC9B,OAAO,OAAO,QAAQ,KAAK;AAAA;AAAA,MAFtB;AAAA,IAGP,CACD,GACH;AAAA,IACC,UACC,oBAAC,OAAE,WAAU,uDACV,mBACH,IACE;AAAA,KACN;AAEJ;AAEA,SAAS,SAAS,EAAE,OAAO,MAAM,GAAqC;AACpE,SACE,qBAAC,SAAI,WAAU,iGACb;AAAA,wBAAC,SAAI,WAAU,8DACZ,iBACH;AAAA,IACA,oBAAC,SAAI,WAAU,kFACZ,gBAAM,eAAe,GACxB;AAAA,KACF;AAEJ;;;ACnDA,SAAS,aAAa,WAAW,gBAAgB;AAiB1C,SAAS,UAAU,WAAW,QAAyB;AAC5D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,IAAI;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAElC,YAAU,MAAM;AACd,QAAI,OAAO;AACX,eAAW,IAAI;AACf,UAAM,GAAG,QAAQ,SAAS,EACvB,KAAK,OAAO,QAAQ;AACnB,UAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,GAAG;AACpE,aAAQ,MAAM,IAAI,KAAK;AAAA,IACzB,CAAC,EACA,KAAK,CAAC,SAAS;AACd,UAAI,CAAC,KAAM;AACX,gBAAU,KAAK,MAAM;AACrB,eAAS,IAAI;AAAA,IACf,CAAC,EACA,MAAM,CAAC,MAAe;AACrB,UAAI,CAAC,KAAM;AACX,eAAS,aAAa,QAAQ,EAAE,UAAU,uBAAuB;AAAA,IACnE,CAAC,EACA,QAAQ,MAAM;AACb,UAAI,KAAM,YAAW,KAAK;AAAA,IAC5B,CAAC;AACH,WAAO,MAAM;AACX,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,UAAU,IAAI,CAAC;AAEnB,QAAM,UAAU,YAAY,MAAM,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3D,SAAO,EAAE,QAAQ,SAAS,OAAO,QAAQ;AAC3C;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/labels.ts","../src/register-counts.tsx","../src/columns.ts","../src/register-table.tsx","../src/record-drawer.tsx","../src/register-browser.tsx","../src/use-records.ts","../src/use-counts.ts"],"sourcesContent":["import type { Collection } from \"@validation-os/core\";\n\n/** Plain-language labels — the register is a surface a non-technical\n * teammate meets, so no jargon and no code-y plurals. */\nexport const REGISTER_LABEL: Record<Collection, string> = {\n assumptions: \"Assumptions\",\n experiments: \"Experiments\",\n readings: \"Readings\",\n goals: \"Goals\",\n decisions: \"Decisions\",\n glossary: \"Glossary\",\n people: \"People\",\n};\n\n/** The order tiles read left-to-right, top-to-bottom. */\nexport const REGISTER_ORDER: Collection[] = [\n \"assumptions\",\n \"experiments\",\n \"readings\",\n \"goals\",\n \"decisions\",\n \"glossary\",\n \"people\",\n];\n","import type { Collection } from \"@validation-os/core\";\nimport { REGISTER_LABEL, REGISTER_ORDER } from \"./labels.js\";\nimport type { Counts } from \"./use-counts.js\";\n\nexport interface RegisterCountsProps {\n counts: Counts;\n /** Optional caption under the tiles (e.g. the backend the numbers came from). */\n caption?: string;\n /**\n * Optional link target per register. When supplied, each tile becomes a link\n * (a plain anchor — works with any router), so counts double as navigation\n * into the browse tables.\n */\n hrefFor?: (register: Collection) => string;\n}\n\n/**\n * A glanceable grid of stat tiles — one per register, the register's row count\n * as the hero number. Presentational: the caller reads the counts (server-side,\n * through the adapter) and hands them in. Styled with Tailwind utility classes;\n * the host app provides Tailwind.\n */\nexport function RegisterCounts({ counts, caption, hrefFor }: RegisterCountsProps) {\n const registers = REGISTER_ORDER.filter(\n (r): r is Collection => counts[r] !== undefined,\n );\n return (\n <section aria-label=\"Register counts\">\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {registers.map((register) => (\n <StatTile\n key={register}\n label={REGISTER_LABEL[register]}\n value={counts[register] ?? 0}\n href={hrefFor?.(register)}\n />\n ))}\n </div>\n {caption ? (\n <p className=\"mt-4 text-sm text-neutral-500 dark:text-neutral-400\">\n {caption}\n </p>\n ) : null}\n </section>\n );\n}\n\nfunction StatTile({\n label,\n value,\n href,\n}: {\n label: string;\n value: number;\n href?: string;\n}) {\n const body = (\n <>\n <div className=\"text-sm font-medium text-neutral-500 dark:text-neutral-400\">\n {label}\n </div>\n <div className=\"mt-1 text-3xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50\">\n {value.toLocaleString()}\n </div>\n </>\n );\n const className =\n \"block rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900\";\n return href ? (\n <a\n href={href}\n className={`${className} transition-colors hover:border-neutral-300 hover:bg-neutral-50 dark:hover:border-neutral-700 dark:hover:bg-neutral-800`}\n >\n {body}\n </a>\n ) : (\n <div className={className}>{body}</div>\n );\n}\n","/**\n * Per-register table columns and value formatting — the presentational shape\n * of the browse tables, kept as pure data/functions so it is unit-testable\n * without a DOM. The register is a surface a non-technical teammate meets, so\n * headers are plain language and derived numbers are shown, never hidden.\n */\nimport type { AnyRecord, Collection } from \"@validation-os/core\";\n\nexport interface ColumnDef {\n /** Stable key; also the default field read from the record. */\n key: string;\n /** Plain-language column header. */\n header: string;\n /** Pull the display value from a record (defaults to `record[key]`). */\n accessor?: (record: AnyRecord) => unknown;\n /** Numeric columns right-align for a clean column of figures. */\n align?: \"left\" | \"right\";\n /** Marks a column whose value is computed, never hand-typed (spec story 4). */\n derived?: boolean;\n}\n\n/** Read a nested `derived` number off a record, tolerating a missing tuple. */\nfunction derivedField(field: string): (r: AnyRecord) => unknown {\n return (r) => (r.derived as Record<string, unknown> | undefined)?.[field];\n}\n\n/**\n * The columns each register shows, left-to-right. The first column is always\n * the record's headline (Title, or Name for people); assumptions additionally\n * surface Impact, Confidence and Risk at a glance (spec user story 2).\n */\nconst COLUMNS: Record<Collection, ColumnDef[]> = {\n assumptions: [\n { key: \"Title\", header: \"Assumption\" },\n { key: \"Impact\", header: \"Impact\", align: \"right\" },\n {\n key: \"confidence\",\n header: \"Confidence\",\n align: \"right\",\n derived: true,\n accessor: derivedField(\"confidence\"),\n },\n {\n key: \"risk\",\n header: \"Risk\",\n align: \"right\",\n derived: true,\n accessor: derivedField(\"risk\"),\n },\n { key: \"Status\", header: \"Status\" },\n ],\n experiments: [\n { key: \"Title\", header: \"Experiment\" },\n { key: \"Status\", header: \"Status\" },\n { key: \"Feasibility\", header: \"Feasibility\" },\n ],\n readings: [\n { key: \"Title\", header: \"Reading\" },\n { key: \"Result\", header: \"Result\" },\n { key: \"Rung\", header: \"Rung\" },\n {\n key: \"strength\",\n header: \"Strength\",\n align: \"right\",\n derived: true,\n accessor: derivedField(\"strength\"),\n },\n ],\n goals: [\n { key: \"Title\", header: \"Goal\" },\n { key: \"Status\", header: \"Status\" },\n { key: \"Outcome\", header: \"Outcome\" },\n ],\n decisions: [\n { key: \"Title\", header: \"Decision\" },\n { key: \"Status\", header: \"Status\" },\n ],\n glossary: [\n { key: \"Title\", header: \"Term\" },\n { key: \"Status\", header: \"Status\" },\n ],\n people: [\n { key: \"Name\", header: \"Name\" },\n { key: \"Role\", header: \"Role\" },\n { key: \"Segment\", header: \"Segment\" },\n ],\n};\n\n/** The columns to render for a register. */\nexport function columnsFor(register: Collection): ColumnDef[] {\n return COLUMNS[register];\n}\n\n/** Read a column's raw value from a record (accessor, else `record[key]`). */\nexport function cellValue(column: ColumnDef, record: AnyRecord): unknown {\n return column.accessor ? column.accessor(record) : record[column.key];\n}\n\n/** Format any stored value for display; empty/missing reads as an em dash. */\nexport function formatValue(value: unknown): string {\n if (value === null || value === undefined || value === \"\") return \"—\";\n if (typeof value === \"boolean\") return value ? \"Yes\" : \"No\";\n if (Array.isArray(value)) {\n return value.length ? value.map(formatScalar).join(\", \") : \"—\";\n }\n return formatScalar(value);\n}\n\nfunction formatScalar(value: unknown): string {\n if (value === null || value === undefined) return \"—\";\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\n/** The record's headline for a row/drawer: Title, else Name, else its id. */\nexport function primaryLabel(record: AnyRecord): string {\n const title = record.Title ?? record.Name;\n return typeof title === \"string\" && title.trim() ? title : record.id;\n}\n\n// ── Field labels ────────────────────────────────────────────────────────────\n// The register is a surface a non-technical teammate meets, so field names are\n// shown in plain language, never as raw camelCase keys. This is the single\n// source for both the drawer's field list and its computed-numbers block.\n\n/** Labels for the computed `derived` tuple. Both `derivedImpact` (the\n * derivation module's name) and `impact` (the migrated data's name, pending the\n * OPS-1273 schema ripple) map to one label so the drawer reads cleanly either\n * way. */\nconst DERIVED_LABEL: Record<string, string> = {\n confidence: \"Confidence\",\n risk: \"Risk\",\n derivedImpact: \"Derived Impact\",\n impact: \"Derived Impact\",\n sourceQuality: \"Source quality\",\n strength: \"Strength\",\n};\n\n/** Plain-language names for the camelCase id/relation fields; Title-cased\n * fields (Title, Status, Impact, Owner…) already read cleanly and pass through. */\nconst FIELD_LABEL: Record<string, string> = {\n dependsOnIds: \"Depends on\",\n enablesIds: \"Enables\",\n contradictsIds: \"Contradicts\",\n readingIds: \"Readings\",\n assumptionId: \"Assumption\",\n experimentId: \"Experiment\",\n goalId: \"Goal\",\n basedOnIds: \"Based on\",\n resolvesIds: \"Resolves\",\n barLineAssumptionIds: \"Assumptions tested\",\n closureReason: \"Closure reason\",\n magnitudeBand: \"Magnitude band\",\n moot: \"Moot\",\n};\n\n/** A record field's display label — an override, else a humanised key. */\nexport function fieldLabel(key: string): string {\n const override = FIELD_LABEL[key];\n if (override) return override;\n if (/^[A-Z]/.test(key)) return key; // already Title-cased, reads fine\n const spaced = key.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").toLowerCase();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/** A computed (`derived`) field's display label. */\nexport function derivedLabel(key: string): string {\n return DERIVED_LABEL[key] ?? fieldLabel(key);\n}\n","import type { AnyRecord, Collection } from \"@validation-os/core\";\nimport { cellValue, columnsFor, formatValue, primaryLabel } from \"./columns.js\";\n\nexport interface RegisterTableProps {\n register: Collection;\n records: AnyRecord[];\n /** Called with the clicked record's id — opens the read-only drawer. */\n onRowClick?: (id: string) => void;\n /** The id of the currently-open record, highlighted in the list. */\n selectedId?: string | null;\n}\n\n/**\n * A list table for one register — a row per record, the register's key fields\n * as columns (assumptions show Impact, Confidence and Risk). Presentational:\n * the caller supplies the rows. Clicking a row opens the read-only drawer.\n * Styled with Tailwind utilities the host app provides.\n */\nexport function RegisterTable({\n register,\n records,\n onRowClick,\n selectedId,\n}: RegisterTableProps) {\n const columns = columnsFor(register);\n\n if (records.length === 0) {\n return (\n <p className=\"rounded-xl border border-dashed border-neutral-300 p-8 text-center text-sm text-neutral-500 dark:border-neutral-700 dark:text-neutral-400\">\n No records yet.\n </p>\n );\n }\n\n return (\n <div className=\"overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800\">\n <table className=\"w-full border-collapse text-left text-sm\">\n <thead>\n <tr className=\"border-b border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900\">\n {columns.map((c) => (\n <th\n key={c.key}\n scope=\"col\"\n className={`px-4 py-2.5 font-medium text-neutral-500 dark:text-neutral-400 ${\n c.align === \"right\" ? \"text-right tabular-nums\" : \"text-left\"\n }`}\n >\n {c.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {records.map((record) => {\n const isSelected = record.id === selectedId;\n return (\n <tr\n key={record.id}\n onClick={() => onRowClick?.(record.id)}\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(record.id);\n }\n }\n : undefined\n }\n tabIndex={onRowClick ? 0 : undefined}\n aria-selected={isSelected}\n className={`border-b border-neutral-100 last:border-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 dark:border-neutral-900 ${\n onRowClick ? \"cursor-pointer\" : \"\"\n } ${\n isSelected\n ? \"bg-blue-50 dark:bg-blue-950/40\"\n : \"hover:bg-neutral-50 dark:hover:bg-neutral-900/60\"\n }`}\n >\n {columns.map((c, i) => {\n const raw = cellValue(c, record);\n // The headline cell falls back to the record's id so a row is\n // never blank; other cells show an em dash when empty.\n const text =\n i === 0 && (raw === null || raw === undefined || raw === \"\")\n ? primaryLabel(record)\n : formatValue(raw);\n return (\n <td\n key={c.key}\n className={`px-4 py-2.5 ${\n c.align === \"right\"\n ? \"text-right tabular-nums text-neutral-700 dark:text-neutral-300\"\n : \"text-left\"\n } ${\n i === 0\n ? \"font-medium text-neutral-900 dark:text-neutral-100\"\n : \"text-neutral-600 dark:text-neutral-400\"\n }`}\n >\n {text}\n </td>\n );\n })}\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport type { AnyRecord, Collection } from \"@validation-os/core\";\nimport { REGISTER_LABEL } from \"./labels.js\";\nimport { derivedLabel, fieldLabel, formatValue, primaryLabel } from \"./columns.js\";\n\nexport interface RecordDrawerProps {\n register: Collection;\n /** The open record, or null while loading / when nothing is selected. */\n record: AnyRecord | null;\n /** True while the record is being fetched. */\n loading?: boolean;\n error?: string | null;\n /** Whether the drawer is open (a row is selected). */\n open: boolean;\n onClose: () => void;\n}\n\n/** Provider-owned/meta fields are shown in the footer, not as content rows. */\nconst META_FIELDS = new Set([\"id\", \"version\", \"createdAt\", \"updatedAt\", \"derived\"]);\n\n/**\n * A read-only drawer for one record. Any derived numbers lead as the hero,\n * explicitly marked computed — not editable (spec user story 4), so a reader\n * trusts they follow the formulas; the record's own fields follow. This slice\n * is read-only; editing lands in a later slice.\n */\nexport function RecordDrawer({\n register,\n record,\n loading,\n error,\n open,\n onClose,\n}: RecordDrawerProps) {\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n\n // Escape closes the drawer; focus lands inside it when it opens so keyboard\n // users aren't left behind an aria-modal dialog.\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n closeButtonRef.current?.focus();\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n if (!open) return null;\n\n const derived =\n record && record.derived && typeof record.derived === \"object\"\n ? (record.derived as Record<string, unknown>)\n : null;\n\n const fields = record\n ? Object.keys(record).filter((k) => !META_FIELDS.has(k))\n : [];\n\n return (\n <div className=\"fixed inset-0 z-50 flex justify-end\">\n {/* Overlay — click to dismiss. */}\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"absolute inset-0 bg-neutral-950/30 backdrop-blur-sm\"\n />\n <aside\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${REGISTER_LABEL[register]} record`}\n 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\"\n >\n <header className=\"flex items-start justify-between gap-4 border-b border-neutral-200 p-5 dark:border-neutral-800\">\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-neutral-400\">\n {REGISTER_LABEL[register]}\n </p>\n <h2 className=\"mt-1 text-lg font-semibold text-neutral-900 dark:text-neutral-50\">\n {record ? primaryLabel(record) : loading ? \"Loading…\" : \"—\"}\n </h2>\n </div>\n <button\n ref={closeButtonRef}\n type=\"button\"\n onClick={onClose}\n className=\"rounded-md px-2 py-1 text-sm text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-900\"\n >\n Close\n </button>\n </header>\n\n <div className=\"flex-1 p-5\">\n {loading ? (\n <p className=\"text-sm text-neutral-500\">Loading record…</p>\n ) : error ? (\n <p className=\"text-sm text-red-600 dark:text-red-400\">{error}</p>\n ) : !record ? (\n <p className=\"text-sm text-neutral-500\">No record.</p>\n ) : (\n <>\n {derived ? (\n <section className=\"mb-6\">\n <h3 className=\"mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400\">\n Computed · not editable\n </h3>\n <dl className=\"grid grid-cols-3 gap-3\">\n {Object.entries(derived).map(([key, value]) => (\n <div\n key={key}\n className=\"rounded-lg border border-neutral-200 bg-neutral-50 p-3 dark:border-neutral-800 dark:bg-neutral-900\"\n >\n <dt className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {derivedLabel(key)}\n </dt>\n <dd className=\"mt-1 text-xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50\">\n {formatValue(value)}\n </dd>\n </div>\n ))}\n </dl>\n </section>\n ) : null}\n\n <dl className=\"divide-y divide-neutral-100 dark:divide-neutral-900\">\n {fields.map((key) => (\n <div key={key} className=\"grid grid-cols-3 gap-3 py-2.5\">\n <dt className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {fieldLabel(key)}\n </dt>\n <dd className=\"col-span-2 text-sm text-neutral-800 dark:text-neutral-200\">\n {formatValue(record[key])}\n </dd>\n </div>\n ))}\n </dl>\n </>\n )}\n </div>\n\n {record ? (\n <footer className=\"border-t border-neutral-200 p-5 text-xs text-neutral-400 dark:border-neutral-800\">\n {record.id} · version {String(record.version)} · updated{\" \"}\n {formatValue(record.updatedAt)}\n </footer>\n ) : null}\n </aside>\n </div>\n );\n}\n","import { useState } from \"react\";\nimport type { Collection } from \"@validation-os/core\";\nimport { REGISTER_LABEL } from \"./labels.js\";\nimport { RegisterTable } from \"./register-table.js\";\nimport { RecordDrawer } from \"./record-drawer.js\";\nimport { useList, useRecord } from \"./use-records.js\";\n\nexport interface RegisterBrowserProps {\n register: Collection;\n /** API base path (default `/api`). */\n basePath?: string;\n}\n\n/**\n * The browse-and-open surface for one register: a list table that opens a\n * read-only drawer on row click. Reads over HTTP through the Clerk-gated API\n * read routes (list + get), so the browser never touches Firestore directly.\n * The thin host app renders this with a `register` — that's the whole page.\n */\nexport function RegisterBrowser({ register, basePath }: RegisterBrowserProps) {\n const { records, loading, error } = useList(register, basePath);\n const [openId, setOpenId] = useState<string | null>(null);\n const {\n record,\n loading: recordLoading,\n error: recordError,\n } = useRecord(register, openId, basePath);\n\n return (\n <div>\n {loading && !records ? (\n <p className=\"text-sm text-neutral-500\">\n Loading {REGISTER_LABEL[register].toLowerCase()}…\n </p>\n ) : error ? (\n <p className=\"text-sm text-red-600 dark:text-red-400\">{error}</p>\n ) : (\n <RegisterTable\n register={register}\n records={records ?? []}\n onRowClick={setOpenId}\n selectedId={openId}\n />\n )}\n\n <RecordDrawer\n register={register}\n record={record}\n loading={recordLoading}\n error={recordError}\n open={openId !== null}\n onClose={() => setOpenId(null)}\n />\n </div>\n );\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { AnyRecord, Collection } from \"@validation-os/core\";\n\n/**\n * Client hooks that read the register over HTTP through the API read routes\n * (`GET {basePath}/{register}` and `GET {basePath}/{register}/{id}`). The API\n * reaches Firestore server-side through the adapter, so no backend credentials\n * ever reach the browser — the browser only ever speaks to the Clerk-gated API.\n */\n\ninterface AsyncState<T> {\n data: T | null;\n loading: boolean;\n error: string | null;\n}\n\n/** Both read routes wrap their payload in a `{ data }` envelope. */\nfunction pickData<T>(body: unknown): T {\n return (body as { data: T }).data;\n}\n\n/**\n * Fetch one JSON resource, tracking loading/error. A null `url` means \"idle\"\n * (nothing to load yet) — the drawer's get hook uses this until a row is\n * clicked. Returns a `refresh` that re-runs the fetch. This is the shared\n * engine behind both `useList` and `useRecord`.\n */\nfunction useJsonResource<T>(url: string | null): AsyncState<T> & {\n refresh: () => void;\n} {\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n loading: url !== null,\n error: null,\n });\n const [tick, setTick] = useState(0);\n\n useEffect(() => {\n if (url === null) {\n setState({ data: null, loading: false, error: null });\n return;\n }\n let live = true;\n setState((s) => ({ ...s, loading: true }));\n fetch(url)\n .then(async (res) => {\n if (!res.ok) throw new Error(`Request to ${url} failed (${res.status})`);\n return pickData<T>(await res.json());\n })\n .then((data) => live && setState({ data, loading: false, error: null }))\n .catch(\n (e: unknown) =>\n live &&\n setState({\n data: null,\n loading: false,\n error: e instanceof Error ? e.message : \"Failed to load\",\n }),\n );\n return () => {\n live = false;\n };\n }, [url, tick]);\n\n const refresh = useCallback(() => setTick((t) => t + 1), []);\n return { ...state, refresh };\n}\n\nexport interface UseListResult {\n records: AnyRecord[] | null;\n loading: boolean;\n error: string | null;\n refresh: () => void;\n}\n\n/** Fetch every row of a register. */\nexport function useList(register: Collection, basePath = \"/api\"): UseListResult {\n const { data, loading, error, refresh } = useJsonResource<AnyRecord[]>(\n `${basePath}/${register}`,\n );\n return { records: data, loading, error, refresh };\n}\n\nexport interface UseRecordResult {\n record: AnyRecord | null;\n loading: boolean;\n error: string | null;\n}\n\n/**\n * Fetch one record by id. `id` may be null (nothing open) — the hook stays\n * idle until an id is supplied, so it drives a drawer that opens on row click.\n */\nexport function useRecord(\n register: Collection,\n id: string | null,\n basePath = \"/api\",\n): UseRecordResult {\n const url = id\n ? `${basePath}/${register}/${encodeURIComponent(id)}`\n : null;\n const { data, loading, error } = useJsonResource<AnyRecord>(url);\n return { record: data, loading, error };\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { Collection } from \"@validation-os/core\";\n\nexport type Counts = Partial<Record<Collection, number>>;\n\nexport interface UseCountsResult {\n counts: Counts | null;\n loading: boolean;\n error: string | null;\n refresh: () => void;\n}\n\n/**\n * Client hook: fetch per-register counts from the API (`GET {basePath}/counts`).\n * The API reads Firestore server-side through the adapter; this only speaks\n * HTTP, so no backend credentials ever reach the browser.\n */\nexport function useCounts(basePath = \"/api\"): UseCountsResult {\n const [counts, setCounts] = useState<Counts | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [tick, setTick] = useState(0);\n\n useEffect(() => {\n let live = true;\n setLoading(true);\n fetch(`${basePath}/counts`)\n .then(async (res) => {\n if (!res.ok) throw new Error(`Counts request failed (${res.status})`);\n return (await res.json()) as { counts: Counts };\n })\n .then((body) => {\n if (!live) return;\n setCounts(body.counts);\n setError(null);\n })\n .catch((e: unknown) => {\n if (!live) return;\n setError(e instanceof Error ? e.message : \"Failed to load counts\");\n })\n .finally(() => {\n if (live) setLoading(false);\n });\n return () => {\n live = false;\n };\n }, [basePath, tick]);\n\n const refresh = useCallback(() => setTick((t) => t + 1), []);\n return { counts, loading, error, refresh };\n}\n"],"mappings":"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","names":["jsx","jsxs","Fragment","jsx","jsxs","useState","useEffect","jsx","jsxs","useState","useCallback","useEffect","useState"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@validation-os/dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React dashboard components + hooks for validation-os, consuming the DataProvider seam.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@validation-os/core": "0.
|
|
24
|
+
"@validation-os/core": "0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": ">=18",
|