@danxbot/ui 1.0.0 → 2.1.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/theme.css ADDED
@@ -0,0 +1,276 @@
1
+ /* @danxbot/ui — Tailwind theme bridge for consuming applications.
2
+ *
3
+ * GENERATED by scripts/postbuild-css.mjs from the @theme blocks in
4
+ * src/styles/. Do not edit; edit the source stylesheets.
5
+ *
6
+ * Import it alongside the compiled stylesheet, after Tailwind:
7
+ *
8
+ * @import "tailwindcss";
9
+ * @import "@danxbot/ui/theme.css"; <- teaches YOUR build bg-surface etc.
10
+ * @import "@danxbot/ui/styles.css"; <- the token values and component CSS
11
+ * @import "@danxbot/ui/fonts.css";
12
+ *
13
+ * Without this file a consumer's own semantic utilities compile to nothing,
14
+ * with no error anywhere.
15
+ */
16
+
17
+ /* from src/styles/index.css */
18
+ @theme inline {
19
+ /* --- Surfaces + structure --- */
20
+ --color-canvas: var(--canvas);
21
+ --color-canvas-subtle: var(--canvas-subtle);
22
+ --color-surface: var(--surface);
23
+ --color-surface-sunken: var(--surface-sunken);
24
+ --color-surface-raised: var(--surface-raised);
25
+ --color-surface-overlay: var(--surface-overlay);
26
+ --color-surface-inset: var(--surface-inset);
27
+ --color-surface-hover: var(--surface-hover);
28
+ --color-surface-active: var(--surface-active);
29
+ --color-surface-selected: var(--surface-selected);
30
+
31
+ --color-border-subtle: var(--border-subtle);
32
+ --color-border: var(--border);
33
+ --color-border-strong: var(--border-strong);
34
+ --color-border-interactive: var(--border-interactive);
35
+
36
+ /* --- Content --- */
37
+ --color-primary: var(--text-primary);
38
+ --color-secondary: var(--text-secondary);
39
+ --color-tertiary: var(--text-tertiary);
40
+ --color-disabled: var(--text-disabled);
41
+ --color-inverse: var(--text-inverse);
42
+
43
+ /* --- Accent (celadon) --- */
44
+ --color-accent: var(--accent);
45
+ --color-accent-hover: var(--accent-hover);
46
+ --color-accent-active: var(--accent-active);
47
+ --color-accent-subtle: var(--accent-subtle);
48
+ --color-accent-subtle-hover: var(--accent-subtle-hover);
49
+ --color-accent-border: var(--accent-border);
50
+ --color-accent-text: var(--accent-text);
51
+ --color-accent-contrast: var(--accent-contrast);
52
+
53
+ /* --- Brand (ember) --- */
54
+ --color-brand: var(--brand);
55
+ --color-brand-hover: var(--brand-hover);
56
+ --color-brand-subtle: var(--brand-subtle);
57
+ --color-brand-text: var(--brand-text);
58
+ --color-brand-contrast: var(--brand-contrast);
59
+
60
+ /* --- Status --- */
61
+ --color-success: var(--success);
62
+ --color-success-subtle: var(--success-subtle);
63
+ --color-success-text: var(--success-text);
64
+ --color-success-border: var(--success-border);
65
+
66
+ --color-warning: var(--warning);
67
+ --color-warning-subtle: var(--warning-subtle);
68
+ --color-warning-text: var(--warning-text);
69
+ --color-warning-border: var(--warning-border);
70
+
71
+ --color-danger: var(--danger);
72
+ --color-danger-hover: var(--danger-hover);
73
+ --color-danger-subtle: var(--danger-subtle);
74
+ --color-danger-text: var(--danger-text);
75
+ --color-danger-border: var(--danger-border);
76
+ --color-danger-contrast: var(--danger-contrast);
77
+
78
+ --color-info: var(--info);
79
+ --color-info-subtle: var(--info-subtle);
80
+ --color-info-text: var(--info-text);
81
+ --color-info-border: var(--info-border);
82
+
83
+ /* --- Fields --- */
84
+ --color-field: var(--field-bg);
85
+ --color-field-hover: var(--field-bg-hover);
86
+ --color-field-border: var(--field-border);
87
+ --color-field-border-hover: var(--field-border-hover);
88
+ --color-field-placeholder: var(--field-placeholder);
89
+
90
+ --color-scrim: var(--scrim);
91
+ --color-veil: var(--veil);
92
+ --color-focus-halo: var(--focus-halo);
93
+ --color-control-knob: var(--control-knob);
94
+ --color-control-knob-on: var(--control-knob-on);
95
+ --color-switch-track-off: var(--switch-track-off);
96
+ --color-focus-ring: var(--focus-ring);
97
+
98
+ /* --- Type --- */
99
+ --font-display: var(--font-display);
100
+ --font-sans: var(--font-sans);
101
+ --font-mono: var(--font-mono);
102
+
103
+ --text-2xs: var(--text-2xs);
104
+ --text-xs: var(--text-xs);
105
+ --text-sm: var(--text-sm);
106
+ --text-base: var(--text-base);
107
+ --text-md: var(--text-md);
108
+ --text-lg: var(--text-lg);
109
+ --text-xl: var(--text-xl);
110
+ --text-2xl: var(--text-2xl);
111
+ --text-3xl: var(--text-3xl);
112
+ --text-4xl: var(--text-4xl);
113
+ --text-5xl: var(--text-5xl);
114
+
115
+ /* Remap Tailwind's weight utilities onto the non-integer ramp, so
116
+ `font-medium` / `font-bold` pick up 510 / 680 rather than 500 / 700. */
117
+ --font-weight-normal: var(--weight-normal);
118
+ --font-weight-medium: var(--weight-medium);
119
+ --font-weight-semibold: var(--weight-semibold);
120
+ --font-weight-bold: var(--weight-bold);
121
+
122
+ --leading-tight: var(--leading-tight);
123
+ --leading-snug: var(--leading-snug);
124
+ --leading-normal: var(--leading-normal);
125
+ --leading-relaxed: var(--leading-relaxed);
126
+
127
+ --tracking-tighter: var(--tracking-tighter);
128
+ --tracking-tight: var(--tracking-tight);
129
+ --tracking-normal: var(--tracking-normal);
130
+ --tracking-wide: var(--tracking-wide);
131
+ --tracking-widest: var(--tracking-widest);
132
+
133
+ /* --- Shape --- */
134
+ --radius-xs: var(--radius-xs);
135
+ --radius-sm: var(--radius-sm);
136
+ --radius-md: var(--radius-md);
137
+ --radius-lg: var(--radius-lg);
138
+ --radius-xl: var(--radius-xl);
139
+ --radius-2xl: var(--radius-2xl);
140
+ --radius-3xl: var(--radius-3xl);
141
+
142
+ /* --- Control heights, exposed as sizing utilities (h-control-md etc.) --- */
143
+ /* Tailwind's spacing BASE, retargeted through the size multiplier.
144
+ This one line is what makes every `p-4`, `gap-3`, `size-5` in every
145
+ component respond to the size control — without it the utilities keep
146
+ Tailwind's fixed 0.25rem and only the hand-written control heights move,
147
+ which is why the first version of the control read as "changes padding
148
+ slightly" and nothing else. */
149
+ --spacing: calc(0.25rem * var(--ui-scale));
150
+
151
+ --spacing-control-xs: var(--control-xs);
152
+ --spacing-control-sm: var(--control-sm);
153
+ --spacing-control-md: var(--control-md);
154
+ --spacing-control-lg: var(--control-lg);
155
+ --spacing-control-xl: var(--control-xl);
156
+
157
+ /* --- Elevation --- */
158
+ --shadow-xs: var(--shadow-xs);
159
+ --shadow-sm: var(--shadow-sm);
160
+ --shadow-md: var(--shadow-md);
161
+ --shadow-lg: var(--shadow-lg);
162
+ --shadow-xl: var(--shadow-xl);
163
+ --shadow-tooltip: var(--shadow-tooltip);
164
+ --shadow-glow-accent: var(--shadow-glow-accent);
165
+
166
+ /* --- Motion ---
167
+ The curves, durations and `animate-*` utilities are owned by motion.css,
168
+ which declares them directly in Tailwind's own namespaces. All that is
169
+ left here is retargeting the framework's bare `transition` default off
170
+ its Material-1-era `cubic-bezier(0.4, 0, 0.2, 1)` @ 150ms. */
171
+ --default-transition-duration: var(--dur-base);
172
+ --default-transition-timing-function: var(--ease-standard);
173
+ }
174
+
175
+ /* from src/styles/tokens.css */
176
+ @theme inline {
177
+ --color-surface-selected-hover: var(--surface-selected-hover);
178
+ --color-danger-subtle-hover: var(--danger-subtle-hover);
179
+ }
180
+
181
+ /* from src/styles/motion.css */
182
+ @theme static {
183
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1); /* on-screen to on-screen */
184
+ --ease-entrance: cubic-bezier(0, 0, 0.2, 1); /* decelerate IN */
185
+ --ease-exit: cubic-bezier(0.4, 0, 1, 1); /* accelerate OUT */
186
+ --ease-emphasis: cubic-bezier(0.05, 0.7, 0.1, 1);
187
+ /* y1 = 1: hits full speed instantly, then decelerates. This is why hover
188
+ feedback with it reads as instantaneous. */
189
+ --ease-hover: cubic-bezier(0.4, 1, 0.6, 1);
190
+ --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
191
+ --ease-in-strong: cubic-bezier(0.32, 0, 0.67, 0);
192
+
193
+ /* --- Springs, baked as linear() ---------------------------------------
194
+ Generated by integrating the damped-spring ODE and sampling 50 points.
195
+ Fewer than ~25 points reads robotic; 50 is indistinguishable from a real
196
+ spring.
197
+
198
+ KEY PROPERTY: a linear() curve encodes SHAPE ONLY — it is a function of
199
+ the damping ratio alone. Replaying it over any duration yields a real,
200
+ physically-valid spring. So there is one curve per damping ratio, and
201
+ duration stays a free parameter chosen from the bands below.
202
+
203
+ Zero JS. A JS spring runtime costs 35-40 KB to do what these do free. */
204
+
205
+ /* zeta 0.751 — 2.8% overshoot. The UI default. */
206
+ --ease-snappy: linear(0, 0.0118, 0.0436, 0.0903, 0.1478, 0.2123, 0.281,
207
+ 0.3513, 0.4214, 0.4896, 0.5549, 0.6164, 0.6734, 0.7257, 0.773, 0.8154,
208
+ 0.853, 0.8858, 0.9143, 0.9386, 0.9591, 0.9762, 0.9901, 1.0013, 1.0101,
209
+ 1.0168, 1.0216, 1.025, 1.027, 1.028, 1.0282, 1.0276, 1.0266, 1.0252,
210
+ 1.0235, 1.0216, 1.0197, 1.0177, 1.0157, 1.0138, 1.012, 1.0103, 1.0087,
211
+ 1.0073, 1.006, 1.0049, 1.0038, 1.003, 1.0022, 1.0015, 1);
212
+
213
+ /* zeta 1.000 — critically damped, ZERO overshoot. Use where overshoot would
214
+ lie: progress, numeric values, destructive confirmations. */
215
+ --ease-smooth: linear(0, 0.0151, 0.0535, 0.107, 0.1694, 0.2361, 0.304,
216
+ 0.3707, 0.4346, 0.4949, 0.551, 0.6025, 0.6494, 0.6918, 0.7299, 0.7638,
217
+ 0.7941, 0.8208, 0.8444, 0.8651, 0.8832, 0.8991, 0.913, 0.925, 0.9354,
218
+ 0.9445, 0.9524, 0.9591, 0.965, 0.97, 0.9743, 0.9781, 0.9813, 0.984,
219
+ 0.9864, 0.9884, 0.9901, 0.9916, 0.9928, 0.9939, 0.9948, 0.9956, 0.9963,
220
+ 0.9968, 0.9973, 0.9977, 0.9981, 0.9984, 0.9986, 0.9988, 1);
221
+
222
+ /* zeta 0.400 — 25.3% overshoot. Rare, deliberate delight only. */
223
+ --ease-bouncy: linear(0, 0.0552, 0.1966, 0.3883, 0.5977, 0.7985, 0.9716,
224
+ 1.1054, 1.1955, 1.243, 1.2533, 1.2343, 1.1953, 1.1451, 1.0919, 1.042,
225
+ 0.9998, 0.968, 0.9472, 0.9371, 0.9361, 0.9421, 0.9527, 0.9658, 0.9792,
226
+ 0.9916, 1.0018, 1.0094, 1.0141, 1.0162, 1.016, 1.0142, 1.0114, 1.008,
227
+ 1.0046, 1.0016, 0.9991, 0.9973, 0.9963, 0.9959, 0.996, 0.9965, 0.9973,
228
+ 0.9981, 0.999, 0.9997, 1.0003, 1.0007, 1.001, 1.0011, 1);
229
+
230
+ /* zeta 0.899 — 0.2% overshoot. A kiss of spring, for large surfaces. */
231
+ --ease-gentle: linear(0, 0.0128, 0.0462, 0.0941, 0.1516, 0.2146, 0.2804,
232
+ 0.3465, 0.4112, 0.4734, 0.5321, 0.5869, 0.6375, 0.6837, 0.7255, 0.7631,
233
+ 0.7966, 0.8264, 0.8526, 0.8756, 0.8955, 0.9129, 0.9278, 0.9406, 0.9515,
234
+ 0.9607, 0.9685, 0.975, 0.9804, 0.9849, 0.9886, 0.9916, 0.994, 0.996,
235
+ 0.9975, 0.9987, 0.9996, 1.0003, 1.0008, 1.0011, 1.0014, 1.0015, 1.0016,
236
+ 1.0016, 1.0015, 1.0015, 1.0014, 1.0013, 1.0012, 1.0011, 1);
237
+
238
+ /* Pen curve for the checkbox tick: slows at the stroke's corner (29.5% of
239
+ duration, dipping to 52% speed) and decelerates into the tip. Tuned to
240
+ the specific path `M5 12.5 L10 17.5 L19 7`, whose corner sits at 33.83%
241
+ of arc length — regenerate for a different path. */
242
+ --ease-pen: linear(0, 0.0426, 0.085, 0.1271, 0.1683, 0.2077, 0.2443, 0.2766,
243
+ 0.3043, 0.3282, 0.3507, 0.3743, 0.4012, 0.4319, 0.4657, 0.5012, 0.5374,
244
+ 0.5735, 0.609, 0.6438, 0.6776, 0.7106, 0.7427, 0.7737, 0.8037, 0.8325,
245
+ 0.8602, 0.8868, 0.9121, 0.9361, 0.9588, 0.9801, 1);
246
+ }
247
+
248
+ /* from src/styles/motion.css */
249
+ @theme static {
250
+ --animate-fade-in: dx-fade-in var(--dur-base) var(--ease-entrance) both;
251
+ --animate-rise-in: dx-rise-in var(--dur-base) var(--ease-entrance) both;
252
+ --animate-scale-in: dx-scale-in var(--dur-fast) var(--ease-snappy) both;
253
+ --animate-fade-out: dx-fade-in var(--dur-exit) var(--ease-exit) reverse both;
254
+
255
+ /* The 500ms delay is deliberate — a skeleton sits still before it starts
256
+ breathing, which reads as composed rather than instantly agitated. */
257
+ --animate-pulse: dx-pulse 1.8s var(--ease-standard) 0.5s infinite;
258
+ --animate-sweep: dx-sweep 1.6s var(--ease-standard) infinite;
259
+ /* A spinner is the one animation someone WATCHES rather than glances at, and
260
+ a fast one reads as frantic — it makes a two-second wait feel like the app
261
+ is struggling. Slow enough to look composed, fast enough to look alive.
262
+
263
+ The two rates are deliberately not multiples of each other: 1.4s against
264
+ 2.2s means the arc's growth lands in a different place on every rotation,
265
+ so the motion never settles into a pattern the eye can predict and start
266
+ ignoring. */
267
+ --animate-spin: dx-spin 1.4s linear infinite;
268
+ --animate-spinner-dash: dx-spinner-dash 2.2s var(--ease-standard) infinite;
269
+
270
+ /* Live TEXT — "Thinking…", "Ran 3 commands". Distinct from the skeleton
271
+ sweep above, which stands in for content that has not arrived; this one
272
+ marks content that IS here and still changing. Slow, because it sits
273
+ beside prose someone is reading and a fast highlight travelling through a
274
+ sentence makes it harder to read, not more obviously alive. */
275
+ --animate-shimmer: dx-shimmer 2.4s var(--ease-standard) infinite;
276
+ }
@@ -0,0 +1,15 @@
1
+ export interface ConfidenceMeterProps {
2
+ /** 0..1, or null when nothing scored it. */
3
+ confidence: number | null;
4
+ /**
5
+ * What to say when there is no score.
6
+ *
7
+ * Rendered as text rather than an empty bar, because a bar at zero and a bar
8
+ * with nothing to show look identical and mean opposite things.
9
+ */
10
+ unscoredLabel?: string;
11
+ /** Hides the band name, for dense rows where the bar is enough. */
12
+ compact?: boolean;
13
+ className?: string;
14
+ }
15
+ export declare function ConfidenceMeter({ confidence, unscoredLabel, compact, className, }: ConfidenceMeterProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import type { ProvenanceState } from "./types";
2
+ export interface ProvenanceBadgeProps {
3
+ state: ProvenanceState;
4
+ /** Render the badge even for states that are quiet by default. */
5
+ alwaysShow?: boolean;
6
+ size?: "sm" | "md";
7
+ className?: string;
8
+ }
9
+ export declare function ProvenanceBadge({ state, alwaysShow, size, className, }: ProvenanceBadgeProps): import("react").JSX.Element | null;
10
+ /** The words this library uses for each state. Exported so a consumer's own
11
+ filters and legends read the same as its badges. */
12
+ export declare function provenanceStateLabel(state: ProvenanceState): string;
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ProvenanceRecord } from "./types";
3
+ export interface ProvenanceDisclosureProps {
4
+ record: ProvenanceRecord;
5
+ /**
6
+ * Rendered at the foot of the panel — sign-off, correction, "open the source".
7
+ *
8
+ * A slot rather than props, because the actions a consumer offers over an
9
+ * extracted value are theirs: this library has no idea who is allowed to sign
10
+ * one, or what correcting it should do.
11
+ */
12
+ actions?: ReactNode;
13
+ /** Shown in place of the value when there is none and it did not fail. */
14
+ emptyLabel?: string;
15
+ /** Shown in place of the value when the viewer may not see it. */
16
+ redactedLabel?: string;
17
+ className?: string;
18
+ }
19
+ export declare function ProvenanceDisclosure({ record, actions, emptyLabel, redactedLabel, className, }: ProvenanceDisclosureProps): import("react").JSX.Element;
@@ -0,0 +1,4 @@
1
+ export * from "./types";
2
+ export * from "./ConfidenceMeter";
3
+ export * from "./ProvenanceBadge";
4
+ export * from "./ProvenanceDisclosure";
@@ -0,0 +1,78 @@
1
+ import type { ReactNode } from "react";
2
+ export type ProvenanceState =
3
+ /** Machine-extracted, and the check passed. */
4
+ "verified"
5
+ /** A person signed it — on top of the machine's verdict, never instead. */
6
+ | "human"
7
+ /** Extracted, but nothing confirmed it. The value stands, flagged. */
8
+ | "unverified"
9
+ /** The check actively failed. There is no value. */
10
+ | "failed";
11
+ /** Where a value came from, precisely enough to go and look. */
12
+ export interface ProvenanceSource {
13
+ /** The thing it came from — a document title, a URL, a table name. */
14
+ label: string;
15
+ /** Where inside it: "page 4", "line 82", "§3.1". Null when there is no locus. */
16
+ locus: string | null;
17
+ /** The exact text at that locus. */
18
+ quote: string | null;
19
+ }
20
+ /**
21
+ * The verdict of whatever check the consumer ran.
22
+ *
23
+ * `passed` is a tri-state on purpose: `true`, `false`, and `null` for "no check
24
+ * was run" — which is a different claim from "a check ran and failed", and
25
+ * collapsing the two would let an unchecked value read as a passing one.
26
+ */
27
+ export interface ProvenanceCheck {
28
+ passed: boolean | null;
29
+ /** How it was checked, or why it could not be. */
30
+ detail: string;
31
+ }
32
+ /** The model call that produced the value. */
33
+ export interface ProvenanceCall {
34
+ model: string;
35
+ tokensIn: number;
36
+ tokensOut: number;
37
+ latencyMs: number;
38
+ }
39
+ /** A person's signature over the value. */
40
+ export interface ProvenanceSignoff {
41
+ by: string;
42
+ at: string;
43
+ note: string | null;
44
+ /**
45
+ * What the value was before they changed it.
46
+ *
47
+ * Null when they signed without correcting. Present means the value was
48
+ * replaced — and the original is kept, because a correction whose previous
49
+ * value is gone is an assertion rather than a record.
50
+ */
51
+ replaced: string | null;
52
+ }
53
+ export interface ProvenanceRecord {
54
+ /** What this value IS. Always shown, even when the value is hidden. */
55
+ label: string;
56
+ /** Null when it failed, or when the viewer may not see it. */
57
+ value: ReactNode | null;
58
+ state: ProvenanceState;
59
+ /** 0..1. Null when nothing scored it. */
60
+ confidence: number | null;
61
+ source: ProvenanceSource | null;
62
+ reasoning: string | null;
63
+ check: ProvenanceCheck | null;
64
+ call: ProvenanceCall | null;
65
+ signoff: ProvenanceSignoff | null;
66
+ /**
67
+ * Withheld from this viewer.
68
+ *
69
+ * Distinct from a null value: the label and the verification state still
70
+ * render, so a reader can see that a value EXISTS and was checked without
71
+ * seeing it. Hiding the row entirely would make the record look thinner than
72
+ * it is to the person least able to tell.
73
+ */
74
+ redacted?: boolean;
75
+ }
76
+ /** 0..1 to a band. A bare percentage invites false precision. */
77
+ export type ConfidenceBand = "high" | "moderate" | "low";
78
+ export declare function confidenceBand(confidence: number): ConfidenceBand;
@@ -1,5 +1,5 @@
1
1
  import { type BadgeProps } from "../Badge";
2
- import type { Domain, Phase, RoadmapItem, RoadmapItemStatus, UseCase, UseCaseStatus } from "./types";
2
+ import type { Domain, Phase, RoadmapItem, RoadmapItemStatus, Track, UseCase, UseCaseStatus } from "./types";
3
3
  export declare const ROADMAP_ITEM_STATUS_BADGE: Record<RoadmapItemStatus, {
4
4
  tone: BadgeProps["tone"];
5
5
  label: string;
@@ -22,11 +22,18 @@ export interface RoadmapItemCardProps {
22
22
  more, so a caller handing it the wrong slice silently mis-derives status
23
23
  rather than the card silently re-filtering a different assumption. */
24
24
  useCases: UseCase[];
25
+ /** The consumer's build tracks, in display order. Threaded straight through
26
+ to every `UseCaseCard`, and used for this item's own status rollup —
27
+ "shipped" means every live use case is verified AND built on all of them,
28
+ so an item cannot report shipped off a built interface alone. */
29
+ tracks: readonly Track[];
25
30
  /** Forwarded to each `UseCaseCard`'s `onStatusChange` as
26
31
  `onUseCaseStatusChange(useCase.ref, status)`. Omitted (the default,
27
32
  `RoadmapSwimlaneBoard`'s customer-facing use) keeps every `UseCaseCard`
28
33
  read-only — see `UseCaseCard`'s own `onStatusChange` doc. */
29
34
  onUseCaseStatusChange?: (ref: string, status: UseCaseStatus) => void;
35
+ /** Same forwarding as `onUseCaseStatusChange`, for the build chips. */
36
+ onUseCaseBuiltChange?: (ref: string, trackId: string, built: boolean) => void;
30
37
  defaultExpanded?: boolean;
31
38
  className?: string;
32
39
  }
@@ -42,4 +49,4 @@ export interface RoadmapItemCardProps {
42
49
  * may reasonably want to add its OWN click behaviour (e.g. deep-linking) later
43
50
  * without fighting this one for the same element.
44
51
  */
45
- export declare function RoadmapItemCard({ item, domain, domainIndex, phase, phaseIndex, useCases, onUseCaseStatusChange, defaultExpanded, className, }: RoadmapItemCardProps): import("react").JSX.Element;
52
+ export declare function RoadmapItemCard({ item, domain, domainIndex, phase, phaseIndex, useCases, tracks, onUseCaseStatusChange, onUseCaseBuiltChange, defaultExpanded, className, }: RoadmapItemCardProps): import("react").JSX.Element;
@@ -1,9 +1,12 @@
1
- import type { Domain, Phase, RoadmapItem, UseCase } from "./types";
1
+ import type { Domain, Phase, RoadmapItem, Track, UseCase } from "./types";
2
2
  export interface RoadmapPageProps {
3
3
  domains: Domain[];
4
4
  phases: Phase[];
5
5
  items: RoadmapItem[];
6
6
  useCases: UseCase[];
7
+ /** Passed straight through — see `RoadmapSwimlaneBoard` for why the build
8
+ tracks stay visible on the customer-facing board. */
9
+ tracks: Track[];
7
10
  className?: string;
8
11
  }
9
- export declare function RoadmapPage({ domains, phases, items, useCases, className }: RoadmapPageProps): import("react").JSX.Element;
12
+ export declare function RoadmapPage({ domains, phases, items, useCases, tracks, className, }: RoadmapPageProps): import("react").JSX.Element;
@@ -1,9 +1,15 @@
1
- import type { Domain, Phase, RoadmapItem, UseCase } from "./types";
1
+ import type { Domain, Phase, RoadmapItem, Track, UseCase } from "./types";
2
2
  export interface RoadmapSwimlaneBoardProps {
3
3
  domains: Domain[];
4
4
  phases: Phase[];
5
5
  items: RoadmapItem[];
6
6
  useCases: UseCase[];
7
+ /** The consumer's build tracks — shown on every use case here too, NOT
8
+ hidden from the customer-facing view. It is tempting to drop the second
9
+ track from a prospect's board, and it is the wrong instinct: "the
10
+ interface is built and the service behind it lands on this date" is a
11
+ stronger and more honest thing to show than a single ambiguous tick. */
12
+ tracks: readonly Track[];
7
13
  className?: string;
8
14
  }
9
- export declare function RoadmapSwimlaneBoard({ domains, phases, items, useCases, className, }: RoadmapSwimlaneBoardProps): import("react").JSX.Element;
15
+ export declare function RoadmapSwimlaneBoard({ domains, phases, items, useCases, tracks, className, }: RoadmapSwimlaneBoardProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import type { Track, UseCase } from "./types";
2
+ export interface TrackManagerProps {
3
+ tracks: Track[];
4
+ /** Read-only — used only to compute progress and the delete guard below. */
5
+ useCases: UseCase[];
6
+ /** `order` is pre-computed by this component (next after the current max)
7
+ so the caller only has to assign an id and persist. */
8
+ onCreate: (track: {
9
+ name: string;
10
+ order: number;
11
+ }) => void;
12
+ onRename: (id: string, name: string) => void;
13
+ /** The full track list, reordered and renumbered (`order` === new index) —
14
+ the caller persists each row's new `order`, e.g. one `update` per id. */
15
+ onReorder: (tracks: Track[]) => void;
16
+ onDelete: (id: string) => void;
17
+ className?: string;
18
+ }
19
+ export declare function TrackManager({ tracks, useCases, onCreate, onRename, onReorder, onDelete, className, }: TrackManagerProps): import("react").JSX.Element;
@@ -1,6 +1,16 @@
1
- import type { UseCase, UseCaseStatus } from "./types";
1
+ import type { Track, UseCase, UseCaseStatus } from "./types";
2
2
  export interface UseCaseCardProps {
3
3
  useCase: UseCase;
4
+ /**
5
+ * The build tracks to report against, in display order — the consumer's own
6
+ * list, whatever it named them. A track absent from `useCase.built` reads as
7
+ * not built.
8
+ *
9
+ * Required rather than optional: a card that silently renders no build state
10
+ * because a caller forgot the prop is exactly the "looks finished, says
11
+ * nothing" failure the ledger exists to prevent.
12
+ */
13
+ tracks: readonly Track[];
4
14
  /**
5
15
  * Makes `status` user-settable: supplying this swaps the read-only status
6
16
  * Badge for a `Select`. Omitted (the default) keeps the Badge — so
@@ -9,6 +19,12 @@ export interface UseCaseCardProps {
9
19
  * because this component knows how to be one.
10
20
  */
11
21
  onStatusChange?: (status: UseCaseStatus) => void;
22
+ /**
23
+ * Same opt-in shape as `onStatusChange`, for the build chips: supply it and
24
+ * each track chip becomes a toggle button; omit it and they stay plain
25
+ * badges. The customer-facing roadmap omits it.
26
+ */
27
+ onBuiltChange?: (trackId: string, built: boolean) => void;
12
28
  className?: string;
13
29
  }
14
- export declare function UseCaseCard({ useCase, onStatusChange, className }: UseCaseCardProps): import("react").JSX.Element;
30
+ export declare function UseCaseCard({ useCase, tracks, onStatusChange, onBuiltChange, className, }: UseCaseCardProps): import("react").JSX.Element;
@@ -1,14 +1,22 @@
1
- import type { Domain, Phase, RoadmapItem, UseCase, UseCaseStatus } from "./types";
1
+ import type { Domain, Phase, RoadmapItem, Track, UseCase, UseCaseStatus } from "./types";
2
2
  export interface UseCaseLedgerPageProps {
3
3
  domains: Domain[];
4
4
  phases: Phase[];
5
5
  items: RoadmapItem[];
6
6
  useCases: UseCase[];
7
+ /** The consumer's build tracks, in display order — every use case reports
8
+ against all of them, and an item is only "shipped" once each live case is
9
+ verified and built on every one. Mount `TrackManager` beside this page
10
+ to manage the list. */
11
+ tracks: Track[];
7
12
  /** Forwarded to every `RoadmapItemCard` -> `UseCaseCard` on this page,
8
13
  making every use case's status genuinely user-settable here. Omitted
9
14
  keeps every status read-only, the same default `UseCaseCard` itself
10
15
  carries. */
11
16
  onUseCaseStatusChange?: (ref: string, status: UseCaseStatus) => void;
17
+ /** Same forwarding, for the build chips: supply it and every track chip on
18
+ this page becomes a toggle. Omitted keeps them read-only badges. */
19
+ onUseCaseBuiltChange?: (ref: string, trackId: string, built: boolean) => void;
12
20
  className?: string;
13
21
  }
14
22
  /**
@@ -18,4 +26,4 @@ export interface UseCaseLedgerPageProps {
18
26
  * one still "unresolved" because a use case needs a human decision, is
19
27
  * supposed to be visible.
20
28
  */
21
- export declare function UseCaseLedgerPage({ domains, phases, items, useCases, onUseCaseStatusChange, className, }: UseCaseLedgerPageProps): import("react").JSX.Element;
29
+ export declare function UseCaseLedgerPage({ domains, phases, items, useCases, tracks, onUseCaseStatusChange, onUseCaseBuiltChange, className, }: UseCaseLedgerPageProps): import("react").JSX.Element;
@@ -1,4 +1,8 @@
1
- import type { RoadmapItemStatus, UseCase } from "./types";
1
+ import type { RoadmapItemStatus, Track, UseCase } from "./types";
2
+ /** Built on every track in `trackIds`. An absent key is not built. */
3
+ export declare function isFullyBuilt(useCase: UseCase, trackIds: readonly string[]): boolean;
4
+ /** Built on none of them. */
5
+ export declare function isUnbuilt(useCase: UseCase, trackIds: readonly string[]): boolean;
2
6
  /**
3
7
  * A RoadmapItem's status is derived from its own UseCases, never hand-set —
4
8
  * the same reasoning as any parent whose state depends on its children:
@@ -8,5 +12,28 @@ import type { RoadmapItemStatus, UseCase } from "./types";
8
12
  * A goal left with no live cases (everything rejected) is "unresolved" rather
9
13
  * than defaulting to "planned", which would misreport a goal that needs a
10
14
  * human decision as one that simply hasn't started.
15
+ *
16
+ * SHIPPED REQUIRES EVERY TRACK, and that is the point of tracks existing. A
17
+ * team building its interface ahead of its backend would otherwise roll up to
18
+ * "shipped" on a screen with nothing behind it — the ledger's single most
19
+ * damaging possible lie, because "shipped" is exactly the word people stop
20
+ * checking.
21
+ *
22
+ * `trackIds` is required rather than defaulted. There is no sensible default:
23
+ * an empty list would make `every` vacuously true and quietly restore the old
24
+ * one-bit behaviour under a new name, which is worse than an argument.
25
+ */
26
+ export declare function deriveRoadmapItemStatus(useCases: UseCase[], trackIds: readonly string[]): RoadmapItemStatus;
27
+ /** `Track.id`s in display order — what every rollup above wants. */
28
+ export declare function trackIds(tracks: readonly Track[]): string[];
29
+ /**
30
+ * How far a set of use cases has got along ONE track: `{ built, live }`.
31
+ *
32
+ * Counted over live cases only, for the same reason the rollup ignores them:
33
+ * a rejected case can never be built, so including it would cap every
34
+ * progress reading below 100% forever.
11
35
  */
12
- export declare function deriveRoadmapItemStatus(useCases: UseCase[]): RoadmapItemStatus;
36
+ export declare function trackProgress(useCases: UseCase[], trackId: string): {
37
+ built: number;
38
+ live: number;
39
+ };
@@ -7,3 +7,4 @@ export * from "./RoadmapSwimlaneBoard";
7
7
  export * from "./UseCaseLedgerPage";
8
8
  export * from "./RoadmapPage";
9
9
  export * from "./PhaseManager";
10
+ export * from "./TrackManager";
@@ -31,6 +31,30 @@ export interface RoadmapItem {
31
31
  /** Gates whether this item (and its rollup status) appears on the public Roadmap. */
32
32
  customerVisible: boolean;
33
33
  }
34
+ /**
35
+ * A build track — an independent axis along which a `UseCase` can be built.
36
+ *
37
+ * WHY THIS EXISTS. "Built" is not one bit for most real products. A team that
38
+ * builds its interface first and the service behind it afterwards needs both
39
+ * states on the same card, because a screen that works against simulated data
40
+ * and a screen backed by a real endpoint are different claims, and collapsing
41
+ * them into one boolean makes the ledger say "shipped" about something with
42
+ * nothing behind it.
43
+ *
44
+ * Consumer-managed, exactly like `Domain` and `Phase`: this library ships no
45
+ * vocabulary. It does not know what "UI" or "Backend" means, only that some
46
+ * tracks were named and use cases were marked against them. CRUD lives in
47
+ * `TrackManager`.
48
+ *
49
+ * A consumer with genuinely one axis passes one track, and the ledger reads
50
+ * the way it always did.
51
+ */
52
+ export interface Track {
53
+ id: string;
54
+ name: string;
55
+ /** Display/sort order — lower sorts first. */
56
+ order: number;
57
+ }
34
58
  export interface UseCaseDecision {
35
59
  timestamp: string;
36
60
  note: string;
@@ -46,9 +70,17 @@ export interface UseCase {
46
70
  background?: string;
47
71
  /** User-managed. Verified and Rejected are the only end states. */
48
72
  status: UseCaseStatus;
49
- /** Claude-managed. */
50
- implemented: boolean;
51
- implementedNote?: string;
73
+ /**
74
+ * Agent-managed. Keyed by `Track.id`.
75
+ *
76
+ * AN ABSENT KEY MEANS NOT BUILT — there is no third state. A tri-state here
77
+ * ("built" / "not built" / "unknown") would be ambiguity this data does not
78
+ * actually have: nobody has ever needed to record that they do not know
79
+ * whether they built something.
80
+ */
81
+ built: Record<string, boolean>;
82
+ /** Optional per-track note — same keying as `built`. */
83
+ builtNotes?: Record<string, string>;
52
84
  /** Append-only decision log — caveats, reversals, "why it landed here". */
53
85
  decisions?: UseCaseDecision[];
54
86
  }