strata-react 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +108 -2
- package/dist/components/app-shell.d.ts +29 -0
- package/dist/components/async-button.d.ts +17 -0
- package/dist/components/button.d.ts +16 -0
- package/dist/components/data-table.d.ts +38 -0
- package/dist/components/data-view.d.ts +30 -0
- package/dist/components/done.d.ts +13 -0
- package/dist/components/field.d.ts +29 -0
- package/dist/components/line-chart.d.ts +35 -0
- package/dist/components/metric.d.ts +15 -0
- package/dist/components/page-header.d.ts +13 -0
- package/dist/components/status.d.ts +8 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/undo.d.ts +18 -0
- package/dist/components/virtual-list.d.ts +11 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1085 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/format.d.ts +28 -0
- package/dist/lib/icon.d.ts +6 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/query/index.d.ts +24 -0
- package/dist/query-BXH-Gfj7.js +69 -0
- package/dist/query-BXH-Gfj7.js.map +1 -0
- package/dist/strata.css +181 -0
- package/dist/testing/index.d.ts +64 -0
- package/dist/testing.js +32 -0
- package/dist/testing.js.map +1 -0
- package/package.json +124 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { a as fetchJson, i as describeHttpError, n as StrataQueryProvider, o as toLoad, r as createQueryClient, s as useLoad, t as HttpError } from "./query-BXH-Gfj7.js";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
6
|
+
import { cva } from "class-variance-authority";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, Check, CircleAlert, LoaderCircle, RotateCcw } from "lucide-react";
|
|
9
|
+
import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
|
|
10
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
11
|
+
import { Link, NavLink, useSearchParams } from "react-router";
|
|
12
|
+
// src/lib/utils.ts
|
|
13
|
+
var typeSteps = [
|
|
14
|
+
"caption",
|
|
15
|
+
"label",
|
|
16
|
+
"body-sm",
|
|
17
|
+
"body",
|
|
18
|
+
"heading",
|
|
19
|
+
"title",
|
|
20
|
+
"figure",
|
|
21
|
+
"display"
|
|
22
|
+
];
|
|
23
|
+
var twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ type: [...typeSteps] }] } } });
|
|
24
|
+
function cn(...inputs) {
|
|
25
|
+
return twMerge(clsx(inputs));
|
|
26
|
+
}
|
|
27
|
+
// src/lib/format.ts
|
|
28
|
+
var NBSP = "\xA0";
|
|
29
|
+
function createFormat(locale) {
|
|
30
|
+
const integer = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
|
|
31
|
+
const decimal = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
|
|
32
|
+
const percent = new Intl.NumberFormat(locale, {
|
|
33
|
+
style: "percent",
|
|
34
|
+
maximumFractionDigits: 0
|
|
35
|
+
});
|
|
36
|
+
const clock = new Intl.DateTimeFormat(locale, {
|
|
37
|
+
hour: "2-digit",
|
|
38
|
+
minute: "2-digit"
|
|
39
|
+
});
|
|
40
|
+
const day = new Intl.DateTimeFormat(locale, {
|
|
41
|
+
weekday: "long",
|
|
42
|
+
day: "numeric",
|
|
43
|
+
month: "long"
|
|
44
|
+
});
|
|
45
|
+
const shortDay = new Intl.DateTimeFormat(locale, {
|
|
46
|
+
weekday: "short",
|
|
47
|
+
hour: "2-digit",
|
|
48
|
+
minute: "2-digit"
|
|
49
|
+
});
|
|
50
|
+
const withUnit = (value, unit, fmt = integer) => `${fmt.format(value)} ${unit}`;
|
|
51
|
+
return {
|
|
52
|
+
locale,
|
|
53
|
+
integer,
|
|
54
|
+
decimal,
|
|
55
|
+
percent,
|
|
56
|
+
clock,
|
|
57
|
+
day,
|
|
58
|
+
shortDay,
|
|
59
|
+
withUnit,
|
|
60
|
+
celsius: (value) => withUnit(value, "°C"),
|
|
61
|
+
/** "1 piece", "6 pieces": the count and its noun, so copy never reads "1 pieces". */
|
|
62
|
+
count: (value, one, many = `${one}s`) => `${integer.format(value)} ${new Intl.PluralRules(locale).select(value) === "one" ? one : many}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var format = createFormat("en-GB");
|
|
66
|
+
// src/components/button.tsx
|
|
67
|
+
var buttonVariants = cva("inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-control type-label transition-state press disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
|
68
|
+
variants: {
|
|
69
|
+
variant: {
|
|
70
|
+
default: "bg-primary text-primary-foreground hover:bg-primary-10",
|
|
71
|
+
outline: "border border-input bg-background text-foreground hover:bg-muted",
|
|
72
|
+
secondary: "bg-muted text-foreground hover:bg-neutral-4",
|
|
73
|
+
ghost: "text-foreground hover:bg-muted",
|
|
74
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-danger",
|
|
75
|
+
link: "h-auto px-0 text-primary-11 underline underline-offset-4 hover:text-primary-12"
|
|
76
|
+
},
|
|
77
|
+
size: {
|
|
78
|
+
default: "h-(--control-h) px-4",
|
|
79
|
+
sm: "h-(--control-h-sm) px-3",
|
|
80
|
+
icon: "size-(--control-h)"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
defaultVariants: {
|
|
84
|
+
variant: "outline",
|
|
85
|
+
size: "default"
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
function Button({ className, variant, size, asChild = false, type = "button", ...props }) {
|
|
89
|
+
return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
|
|
90
|
+
type: asChild ? void 0 : type,
|
|
91
|
+
className: cn(buttonVariants({
|
|
92
|
+
variant,
|
|
93
|
+
size
|
|
94
|
+
}), className),
|
|
95
|
+
...props
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function IconButton({ icon: Icon, label, variant = "ghost", ...props }) {
|
|
99
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
100
|
+
size: "icon",
|
|
101
|
+
variant,
|
|
102
|
+
"aria-label": label,
|
|
103
|
+
title: label,
|
|
104
|
+
...props,
|
|
105
|
+
children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": true })
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// src/components/async-button.tsx
|
|
109
|
+
function AsyncButton({ phase, labels, disabled, ...props }) {
|
|
110
|
+
const errorLabel = labels.error ?? "Try again";
|
|
111
|
+
const current = {
|
|
112
|
+
idle: labels.idle,
|
|
113
|
+
pending: labels.pending,
|
|
114
|
+
done: labels.done,
|
|
115
|
+
error: errorLabel
|
|
116
|
+
}[phase];
|
|
117
|
+
const Icon = {
|
|
118
|
+
idle: null,
|
|
119
|
+
pending: LoaderCircle,
|
|
120
|
+
done: Check,
|
|
121
|
+
error: CircleAlert
|
|
122
|
+
}[phase];
|
|
123
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
124
|
+
...props,
|
|
125
|
+
disabled: disabled || phase === "pending",
|
|
126
|
+
"aria-disabled": phase === "pending" || void 0,
|
|
127
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, {
|
|
128
|
+
"aria-hidden": true,
|
|
129
|
+
className: phase === "pending" ? "motion-safe:animate-spin" : void 0
|
|
130
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
131
|
+
className: "grid",
|
|
132
|
+
children: [
|
|
133
|
+
labels.idle,
|
|
134
|
+
labels.pending,
|
|
135
|
+
labels.done,
|
|
136
|
+
errorLabel
|
|
137
|
+
].map((l) => /* @__PURE__ */ jsx("span", {
|
|
138
|
+
"aria-hidden": l !== current,
|
|
139
|
+
className: l === current ? "[grid-area:1/1]" : "invisible [grid-area:1/1]",
|
|
140
|
+
children: l
|
|
141
|
+
}, l))
|
|
142
|
+
})]
|
|
143
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
144
|
+
role: "status",
|
|
145
|
+
className: "sr-only",
|
|
146
|
+
children: phase === "done" ? labels.done : phase === "error" ? errorLabel : ""
|
|
147
|
+
})] });
|
|
148
|
+
}
|
|
149
|
+
/** Runs an async action and reports its phase. `done` returns to `idle` after `settleMs`. */
|
|
150
|
+
function useAsyncAction(action, settleMs = 2e3) {
|
|
151
|
+
const [phase, setPhase] = React.useState("idle");
|
|
152
|
+
const timer = React.useRef(void 0);
|
|
153
|
+
React.useEffect(() => () => window.clearTimeout(timer.current), []);
|
|
154
|
+
return [phase, React.useCallback(async (...args) => {
|
|
155
|
+
window.clearTimeout(timer.current);
|
|
156
|
+
setPhase("pending");
|
|
157
|
+
try {
|
|
158
|
+
await action(...args);
|
|
159
|
+
setPhase("done");
|
|
160
|
+
timer.current = window.setTimeout(() => setPhase("idle"), settleMs);
|
|
161
|
+
} catch {
|
|
162
|
+
setPhase("error");
|
|
163
|
+
}
|
|
164
|
+
}, [action, settleMs])];
|
|
165
|
+
}
|
|
166
|
+
// src/components/field.tsx
|
|
167
|
+
var FieldContext = React.createContext(null);
|
|
168
|
+
function useField(id) {
|
|
169
|
+
const field = React.useContext(FieldContext);
|
|
170
|
+
return {
|
|
171
|
+
id: field?.id ?? id,
|
|
172
|
+
"aria-describedby": field?.describedBy,
|
|
173
|
+
"aria-invalid": field?.invalid || void 0
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function Field({ label, id: idProp, hint, error, children, className }) {
|
|
177
|
+
const generated = React.useId();
|
|
178
|
+
const id = idProp ?? generated;
|
|
179
|
+
const hintId = hint ? `${id}-hint` : void 0;
|
|
180
|
+
const errorId = error ? `${id}-error` : void 0;
|
|
181
|
+
const describedBy = [errorId, hintId].filter(Boolean).join(" ") || void 0;
|
|
182
|
+
return /* @__PURE__ */ jsx(FieldContext.Provider, {
|
|
183
|
+
value: {
|
|
184
|
+
id,
|
|
185
|
+
describedBy,
|
|
186
|
+
invalid: Boolean(error)
|
|
187
|
+
},
|
|
188
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
189
|
+
className: cn("grid min-w-0 content-start gap-1.5", className),
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ jsx("label", {
|
|
192
|
+
htmlFor: id,
|
|
193
|
+
className: "type-label text-foreground",
|
|
194
|
+
children: label
|
|
195
|
+
}),
|
|
196
|
+
children,
|
|
197
|
+
error && /* @__PURE__ */ jsxs("p", {
|
|
198
|
+
id: errorId,
|
|
199
|
+
className: "flex items-start gap-1.5 type-body-sm text-danger",
|
|
200
|
+
children: [/* @__PURE__ */ jsx(CircleAlert, {
|
|
201
|
+
"aria-hidden": true,
|
|
202
|
+
className: "mt-0.5 size-4 shrink-0"
|
|
203
|
+
}), error]
|
|
204
|
+
}),
|
|
205
|
+
hint && /* @__PURE__ */ jsx("p", {
|
|
206
|
+
id: hintId,
|
|
207
|
+
className: "type-body-sm text-muted-foreground",
|
|
208
|
+
children: hint
|
|
209
|
+
})
|
|
210
|
+
]
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
var control = "w-full min-w-0 rounded-control border border-input bg-background px-3 type-body text-foreground transition-state placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-70 aria-invalid:border-danger";
|
|
215
|
+
function Input({ className, id, ...props }) {
|
|
216
|
+
return /* @__PURE__ */ jsx("input", {
|
|
217
|
+
...useField(id),
|
|
218
|
+
className: cn(control, "h-(--control-h)", className),
|
|
219
|
+
...props
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function Textarea({ className, id, ...props }) {
|
|
223
|
+
return /* @__PURE__ */ jsx("textarea", {
|
|
224
|
+
...useField(id),
|
|
225
|
+
className: cn(control, "min-h-24 py-2", className),
|
|
226
|
+
...props
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function NativeSelect({ className, id, ...props }) {
|
|
230
|
+
return /* @__PURE__ */ jsx("select", {
|
|
231
|
+
...useField(id),
|
|
232
|
+
className: cn(control, "h-(--control-h) pr-8", className),
|
|
233
|
+
...props
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function ChoiceGroup({ legend, name, choices, value, onChange, error }) {
|
|
237
|
+
const errorId = React.useId();
|
|
238
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
239
|
+
className: "grid gap-2",
|
|
240
|
+
"aria-describedby": error ? errorId : void 0,
|
|
241
|
+
children: [
|
|
242
|
+
/* @__PURE__ */ jsx("legend", {
|
|
243
|
+
className: "mb-1.5 type-label text-foreground",
|
|
244
|
+
children: legend
|
|
245
|
+
}),
|
|
246
|
+
choices.map((c) => /* @__PURE__ */ jsxs("label", {
|
|
247
|
+
className: "flex min-h-(--control-h-sm) cursor-pointer items-start gap-3 py-1",
|
|
248
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
249
|
+
type: "radio",
|
|
250
|
+
name,
|
|
251
|
+
value: c.value,
|
|
252
|
+
checked: value === c.value,
|
|
253
|
+
onChange: () => onChange(c.value),
|
|
254
|
+
className: "mt-1 size-4 shrink-0 accent-primary"
|
|
255
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
256
|
+
className: "grid",
|
|
257
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
258
|
+
className: "type-body",
|
|
259
|
+
children: c.label
|
|
260
|
+
}), c.detail && /* @__PURE__ */ jsx("span", {
|
|
261
|
+
className: "type-body-sm text-muted-foreground",
|
|
262
|
+
children: c.detail
|
|
263
|
+
})]
|
|
264
|
+
})]
|
|
265
|
+
}, c.value)),
|
|
266
|
+
error && /* @__PURE__ */ jsxs("p", {
|
|
267
|
+
id: errorId,
|
|
268
|
+
className: "flex items-start gap-1.5 type-body-sm text-danger",
|
|
269
|
+
children: [/* @__PURE__ */ jsx(CircleAlert, {
|
|
270
|
+
"aria-hidden": true,
|
|
271
|
+
className: "mt-0.5 size-4 shrink-0"
|
|
272
|
+
}), error]
|
|
273
|
+
})
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// src/components/status.tsx
|
|
278
|
+
var tones = {
|
|
279
|
+
neutral: "bg-muted text-muted-foreground",
|
|
280
|
+
info: "bg-info-subtle text-info",
|
|
281
|
+
success: "bg-success-subtle text-success",
|
|
282
|
+
warning: "bg-warning-subtle text-warning",
|
|
283
|
+
danger: "bg-danger-subtle text-danger"
|
|
284
|
+
};
|
|
285
|
+
function Status({ tone, icon: Icon, children, className }) {
|
|
286
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
287
|
+
className: cn("inline-flex items-center gap-1.5 whitespace-nowrap rounded-pill py-0.5 pr-2.5 pl-2 type-caption font-medium", tones[tone], className),
|
|
288
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, {
|
|
289
|
+
"aria-hidden": true,
|
|
290
|
+
className: "size-3.5 shrink-0"
|
|
291
|
+
}), children]
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// src/components/metric.tsx
|
|
295
|
+
function MetricStrip({ label, children, className }) {
|
|
296
|
+
return /* @__PURE__ */ jsx("dl", {
|
|
297
|
+
"aria-label": label,
|
|
298
|
+
className: cn("grid grid-cols-1 border-y border-border sm:auto-cols-fr sm:grid-flow-col sm:divide-x sm:divide-border", className),
|
|
299
|
+
children
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function Metric({ label, value, unit, note }) {
|
|
303
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
304
|
+
className: "grid content-start gap-0.5 border-border py-3 not-first:border-t sm:px-5 sm:not-first:border-t-0 sm:first:pl-0",
|
|
305
|
+
children: [
|
|
306
|
+
/* @__PURE__ */ jsx("dt", {
|
|
307
|
+
className: "type-label text-muted-foreground",
|
|
308
|
+
children: label
|
|
309
|
+
}),
|
|
310
|
+
/* @__PURE__ */ jsxs("dd", {
|
|
311
|
+
className: "type-figure text-foreground",
|
|
312
|
+
children: [value, unit && /* @__PURE__ */ jsx("span", {
|
|
313
|
+
className: "type-body text-muted-foreground",
|
|
314
|
+
children: "\xA0" + unit
|
|
315
|
+
})]
|
|
316
|
+
}),
|
|
317
|
+
note && /* @__PURE__ */ jsx("dd", {
|
|
318
|
+
className: "type-body-sm text-muted-foreground",
|
|
319
|
+
children: note
|
|
320
|
+
})
|
|
321
|
+
]
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// src/components/data-view.tsx
|
|
325
|
+
function DataView({ state, isEmpty, skeleton, empty, children, className }) {
|
|
326
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
327
|
+
className: cn("min-w-0", className),
|
|
328
|
+
"aria-busy": state.status === "loading" || void 0,
|
|
329
|
+
children: [
|
|
330
|
+
state.status === "loading" && skeleton,
|
|
331
|
+
state.status === "error" && /* @__PURE__ */ jsxs("div", {
|
|
332
|
+
role: "alert",
|
|
333
|
+
className: "flex flex-wrap items-start gap-3 rounded-surface bg-danger-subtle p-4 text-danger",
|
|
334
|
+
children: [
|
|
335
|
+
/* @__PURE__ */ jsx(CircleAlert, {
|
|
336
|
+
"aria-hidden": true,
|
|
337
|
+
className: "mt-0.5 size-5 shrink-0"
|
|
338
|
+
}),
|
|
339
|
+
/* @__PURE__ */ jsx("p", {
|
|
340
|
+
className: "min-w-0 flex-1 type-body",
|
|
341
|
+
children: state.message
|
|
342
|
+
}),
|
|
343
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
344
|
+
size: "sm",
|
|
345
|
+
onClick: state.retry,
|
|
346
|
+
children: [/* @__PURE__ */ jsx(RotateCcw, { "aria-hidden": true }), "Try again"]
|
|
347
|
+
})
|
|
348
|
+
]
|
|
349
|
+
}),
|
|
350
|
+
state.status === "ready" && (isEmpty(state.data) ? empty : children(state.data))
|
|
351
|
+
]
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function EmptyState({ title, children, action }) {
|
|
355
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
356
|
+
className: "grid justify-items-start gap-2 border-y border-border py-6",
|
|
357
|
+
children: [
|
|
358
|
+
/* @__PURE__ */ jsx("p", {
|
|
359
|
+
className: "type-heading",
|
|
360
|
+
children: title
|
|
361
|
+
}),
|
|
362
|
+
children && /* @__PURE__ */ jsx("p", {
|
|
363
|
+
className: "max-w-prose type-body text-muted-foreground",
|
|
364
|
+
children
|
|
365
|
+
}),
|
|
366
|
+
action && /* @__PURE__ */ jsx("div", {
|
|
367
|
+
className: "mt-2",
|
|
368
|
+
children: action
|
|
369
|
+
})
|
|
370
|
+
]
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function Skeleton({ className }) {
|
|
374
|
+
return /* @__PURE__ */ jsx("div", {
|
|
375
|
+
"aria-hidden": true,
|
|
376
|
+
className: cn("rounded-control bg-muted motion-safe:animate-pulse", className)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// src/components/data-table.tsx
|
|
380
|
+
function useLocalSort(initial) {
|
|
381
|
+
const [sort, setSort] = React.useState(initial);
|
|
382
|
+
return [sort, setSort];
|
|
383
|
+
}
|
|
384
|
+
function parseSort(value) {
|
|
385
|
+
if (!value) return void 0;
|
|
386
|
+
const [key, dir] = value.split(".");
|
|
387
|
+
if (!key) return void 0;
|
|
388
|
+
return {
|
|
389
|
+
key,
|
|
390
|
+
dir: dir === "desc" ? "descending" : "ascending"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function serialiseSort(sort) {
|
|
394
|
+
return `${sort.key}.${sort.dir === "descending" ? "desc" : "asc"}`;
|
|
395
|
+
}
|
|
396
|
+
function useUrlSort(param, initial) {
|
|
397
|
+
const [params, setParams] = useSearchParams();
|
|
398
|
+
return [parseSort(params.get(param)) ?? initial, React.useCallback((next) => setParams((p) => {
|
|
399
|
+
const out = new URLSearchParams(p);
|
|
400
|
+
out.set(param, serialiseSort(next));
|
|
401
|
+
return out;
|
|
402
|
+
}, { replace: true }), [param, setParams])];
|
|
403
|
+
}
|
|
404
|
+
function DataTable(props) {
|
|
405
|
+
return props.sortParam ? /* @__PURE__ */ jsx(UrlSortedTable, {
|
|
406
|
+
...props,
|
|
407
|
+
sortParam: props.sortParam
|
|
408
|
+
}) : /* @__PURE__ */ jsx(LocalSortedTable, { ...props });
|
|
409
|
+
}
|
|
410
|
+
function LocalSortedTable(props) {
|
|
411
|
+
return /* @__PURE__ */ jsx(Table, {
|
|
412
|
+
...props,
|
|
413
|
+
sortState: useLocalSort(props.initialSort)
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function UrlSortedTable(props) {
|
|
417
|
+
return /* @__PURE__ */ jsx(Table, {
|
|
418
|
+
...props,
|
|
419
|
+
sortState: useUrlSort(props.sortParam, props.initialSort)
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function Table({ caption, columns, rows, rowKey, sortState: [sort, setSort], virtualizeFrom = 100, maxHeight = 480, className }) {
|
|
423
|
+
const defs = React.useMemo(() => columns.map((c) => ({
|
|
424
|
+
id: c.key,
|
|
425
|
+
accessorFn: (row) => row,
|
|
426
|
+
header: c.header,
|
|
427
|
+
cell: (info) => c.cell(info.row.original),
|
|
428
|
+
enableSorting: Boolean(c.sort),
|
|
429
|
+
sortingFn: c.sort ? (a, b) => c.sort(a.original, b.original) : void 0,
|
|
430
|
+
meta: {
|
|
431
|
+
numeric: c.numeric,
|
|
432
|
+
truncate: c.truncate,
|
|
433
|
+
className: c.className
|
|
434
|
+
}
|
|
435
|
+
})), [columns]);
|
|
436
|
+
const sorting = sort && columns.some((c) => c.key === sort.key && c.sort) ? [{
|
|
437
|
+
id: sort.key,
|
|
438
|
+
desc: sort.dir === "descending"
|
|
439
|
+
}] : [];
|
|
440
|
+
const table = useReactTable({
|
|
441
|
+
data: rows,
|
|
442
|
+
columns: defs,
|
|
443
|
+
state: { sorting },
|
|
444
|
+
getRowId: (row) => rowKey(row),
|
|
445
|
+
getCoreRowModel: getCoreRowModel(),
|
|
446
|
+
getSortedRowModel: getSortedRowModel()
|
|
447
|
+
});
|
|
448
|
+
const toggle = (key) => setSort({
|
|
449
|
+
key,
|
|
450
|
+
dir: sort?.key === key && sort.dir === "ascending" ? "descending" : "ascending"
|
|
451
|
+
});
|
|
452
|
+
const modelRows = table.getRowModel().rows;
|
|
453
|
+
const virtual = modelRows.length >= virtualizeFrom;
|
|
454
|
+
const scroller = React.useRef(null);
|
|
455
|
+
const virtualizer = useVirtualizer({
|
|
456
|
+
count: virtual ? modelRows.length : 0,
|
|
457
|
+
getScrollElement: () => scroller.current,
|
|
458
|
+
estimateSize: () => 41,
|
|
459
|
+
overscan: 8,
|
|
460
|
+
initialRect: {
|
|
461
|
+
width: 0,
|
|
462
|
+
height: maxHeight
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
const items = virtual ? virtualizer.getVirtualItems() : [];
|
|
466
|
+
const padTop = items.length ? items[0].start : 0;
|
|
467
|
+
const padBottom = items.length ? virtualizer.getTotalSize() - items[items.length - 1].end : 0;
|
|
468
|
+
const visible = virtual ? items.map((i) => modelRows[i.index]) : modelRows;
|
|
469
|
+
return /* @__PURE__ */ jsx("div", {
|
|
470
|
+
ref: scroller,
|
|
471
|
+
role: "region",
|
|
472
|
+
"aria-label": caption,
|
|
473
|
+
tabIndex: 0,
|
|
474
|
+
className: cn("min-w-0 overflow-x-auto", virtual && "overflow-y-auto", className),
|
|
475
|
+
style: virtual ? { maxHeight } : void 0,
|
|
476
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
477
|
+
className: "w-full border-collapse type-body-sm",
|
|
478
|
+
"aria-rowcount": virtual ? modelRows.length + 1 : void 0,
|
|
479
|
+
children: [
|
|
480
|
+
/* @__PURE__ */ jsx("caption", {
|
|
481
|
+
className: "sr-only",
|
|
482
|
+
children: caption
|
|
483
|
+
}),
|
|
484
|
+
/* @__PURE__ */ jsx("thead", {
|
|
485
|
+
className: cn(virtual && "sticky top-0 z-10 bg-background"),
|
|
486
|
+
children: table.getHeaderGroups().map((group) => /* @__PURE__ */ jsx("tr", { children: group.headers.map((header) => {
|
|
487
|
+
const meta = header.column.columnDef.meta;
|
|
488
|
+
const active = sort?.key === header.id && header.column.getCanSort() ? sort.dir : void 0;
|
|
489
|
+
const Icon = active === "ascending" ? ArrowUp : active === "descending" ? ArrowDown : ArrowUpDown;
|
|
490
|
+
const label = flexRender(header.column.columnDef.header, header.getContext());
|
|
491
|
+
return /* @__PURE__ */ jsx("th", {
|
|
492
|
+
scope: "col",
|
|
493
|
+
"aria-sort": active,
|
|
494
|
+
className: cn("py-2 pr-4 text-left align-bottom type-caption text-muted-foreground last:pr-0", meta?.numeric && "text-right"),
|
|
495
|
+
children: header.column.getCanSort() ? /* @__PURE__ */ jsxs("button", {
|
|
496
|
+
type: "button",
|
|
497
|
+
onClick: () => toggle(header.id),
|
|
498
|
+
className: cn("-mx-1 inline-flex cursor-pointer items-center gap-1 rounded-control px-1 transition-state hover:text-foreground", meta?.numeric && "flex-row-reverse", active && "text-foreground"),
|
|
499
|
+
children: [label, /* @__PURE__ */ jsx(Icon, {
|
|
500
|
+
"aria-hidden": true,
|
|
501
|
+
className: cn("size-3.5", !active && "opacity-50")
|
|
502
|
+
})]
|
|
503
|
+
}) : label
|
|
504
|
+
}, header.id);
|
|
505
|
+
}) }, group.id))
|
|
506
|
+
}),
|
|
507
|
+
/* @__PURE__ */ jsxs("tbody", { children: [
|
|
508
|
+
padTop > 0 && /* @__PURE__ */ jsx("tr", {
|
|
509
|
+
"aria-hidden": true,
|
|
510
|
+
style: { height: padTop }
|
|
511
|
+
}),
|
|
512
|
+
visible.map((row, i) => /* @__PURE__ */ jsx("tr", {
|
|
513
|
+
className: "border-t border-border",
|
|
514
|
+
"aria-rowindex": virtual ? (items[i]?.index ?? 0) + 2 : void 0,
|
|
515
|
+
children: row.getVisibleCells().map((cell) => {
|
|
516
|
+
const meta = cell.column.columnDef.meta;
|
|
517
|
+
return /* @__PURE__ */ jsx("td", {
|
|
518
|
+
className: cn("py-2.5 pr-4 align-middle last:pr-0", meta?.numeric && "text-right tabular-nums", meta?.truncate && "max-w-48 truncate", meta?.className),
|
|
519
|
+
children: flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
520
|
+
}, cell.id);
|
|
521
|
+
})
|
|
522
|
+
}, row.id)),
|
|
523
|
+
padBottom > 0 && /* @__PURE__ */ jsx("tr", {
|
|
524
|
+
"aria-hidden": true,
|
|
525
|
+
style: { height: padBottom }
|
|
526
|
+
})
|
|
527
|
+
] })
|
|
528
|
+
]
|
|
529
|
+
})
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function TableSkeleton({ rows = 4, columns = 5 }) {
|
|
533
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
534
|
+
"aria-hidden": true,
|
|
535
|
+
className: "grid gap-0",
|
|
536
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
537
|
+
className: "flex gap-4 py-2",
|
|
538
|
+
children: Array.from({ length: columns }, (_, i) => /* @__PURE__ */ jsx("div", { className: "h-3 flex-1 rounded-control bg-muted" }, i))
|
|
539
|
+
}), Array.from({ length: rows }, (_, r) => /* @__PURE__ */ jsx("div", {
|
|
540
|
+
className: "flex gap-4 border-t border-border py-3.5",
|
|
541
|
+
children: Array.from({ length: columns }, (_, i) => /* @__PURE__ */ jsx("div", { className: "h-4 flex-1 rounded-control bg-muted motion-safe:animate-pulse" }, i))
|
|
542
|
+
}, r))]
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// src/components/virtual-list.tsx
|
|
546
|
+
function VirtualList({ label, items, itemKey, children, estimateSize = 48, height = 480, virtualizeFrom = 100, className }) {
|
|
547
|
+
const scroller = React.useRef(null);
|
|
548
|
+
const virtual = items.length >= virtualizeFrom;
|
|
549
|
+
const virtualizer = useVirtualizer({
|
|
550
|
+
count: virtual ? items.length : 0,
|
|
551
|
+
getScrollElement: () => scroller.current,
|
|
552
|
+
estimateSize: () => estimateSize,
|
|
553
|
+
overscan: 6,
|
|
554
|
+
initialRect: {
|
|
555
|
+
width: 0,
|
|
556
|
+
height
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
if (!virtual) return /* @__PURE__ */ jsx("ul", {
|
|
560
|
+
"aria-label": label,
|
|
561
|
+
className: cn("grid min-w-0", className),
|
|
562
|
+
children: items.map((item, i) => /* @__PURE__ */ jsx("li", {
|
|
563
|
+
className: "border-t border-border first:border-t-0",
|
|
564
|
+
children: children(item, i)
|
|
565
|
+
}, itemKey(item)))
|
|
566
|
+
});
|
|
567
|
+
return /* @__PURE__ */ jsx("div", {
|
|
568
|
+
ref: scroller,
|
|
569
|
+
role: "region",
|
|
570
|
+
"aria-label": label,
|
|
571
|
+
tabIndex: 0,
|
|
572
|
+
className: cn("min-w-0 overflow-y-auto", className),
|
|
573
|
+
style: { height },
|
|
574
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
575
|
+
"aria-label": label,
|
|
576
|
+
className: "relative w-full",
|
|
577
|
+
style: { height: virtualizer.getTotalSize() },
|
|
578
|
+
children: virtualizer.getVirtualItems().map((v) => {
|
|
579
|
+
const item = items[v.index];
|
|
580
|
+
return /* @__PURE__ */ jsx("li", {
|
|
581
|
+
ref: virtualizer.measureElement,
|
|
582
|
+
"data-index": v.index,
|
|
583
|
+
"aria-setsize": items.length,
|
|
584
|
+
"aria-posinset": v.index + 1,
|
|
585
|
+
className: cn("absolute inset-x-0 top-0", v.index > 0 && "border-t border-border"),
|
|
586
|
+
style: { translate: `0 ${v.start}px` },
|
|
587
|
+
children: children(item, v.index)
|
|
588
|
+
}, itemKey(item));
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
// src/components/line-chart.tsx
|
|
594
|
+
function useWidth() {
|
|
595
|
+
const ref = React.useRef(null);
|
|
596
|
+
const [width, setWidth] = React.useState(0);
|
|
597
|
+
React.useLayoutEffect(() => {
|
|
598
|
+
const el = ref.current;
|
|
599
|
+
if (!el) return;
|
|
600
|
+
setWidth(el.getBoundingClientRect().width);
|
|
601
|
+
const ro = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
|
|
602
|
+
ro.observe(el);
|
|
603
|
+
return () => ro.disconnect();
|
|
604
|
+
}, []);
|
|
605
|
+
return [ref, width];
|
|
606
|
+
}
|
|
607
|
+
/** Four gridline steps of a size people write on a scale (1, 2, 2.5, 4 or 5 × 10ⁿ), ending above the max. */
|
|
608
|
+
function niceMax(max) {
|
|
609
|
+
if (!(max > 0)) return 4;
|
|
610
|
+
const raw = max / 4;
|
|
611
|
+
const mag = 10 ** Math.floor(Math.log10(raw));
|
|
612
|
+
return ([
|
|
613
|
+
1,
|
|
614
|
+
2,
|
|
615
|
+
2.5,
|
|
616
|
+
4,
|
|
617
|
+
5,
|
|
618
|
+
10
|
|
619
|
+
].map((m) => m * mag).find((s) => s >= raw) ?? 10 * mag) * 4;
|
|
620
|
+
}
|
|
621
|
+
function LineChart({ label, series, seriesLabel, planned, plannedLabel, band, formatX, formatY, xTicks, colour = "var(--chart-1)", colourScale, colourScaleLabel, height = 280, className }) {
|
|
622
|
+
const [ref, width] = useWidth();
|
|
623
|
+
const gradientId = `heat-${React.useId().replace(/[^\w-]/g, "")}`;
|
|
624
|
+
const all = [...series, ...planned ?? []];
|
|
625
|
+
const xMin = Math.min(...all.map((p) => p.x));
|
|
626
|
+
const xMax = Math.max(...all.map((p) => p.x));
|
|
627
|
+
const yMax = niceMax(Math.max(...all.map((p) => p.y)));
|
|
628
|
+
const yTicks = [
|
|
629
|
+
0,
|
|
630
|
+
.25,
|
|
631
|
+
.5,
|
|
632
|
+
.75,
|
|
633
|
+
1
|
|
634
|
+
].map((f) => f * yMax);
|
|
635
|
+
const pad = {
|
|
636
|
+
top: 16,
|
|
637
|
+
right: 16,
|
|
638
|
+
bottom: 28,
|
|
639
|
+
left: 56
|
|
640
|
+
};
|
|
641
|
+
const w = Math.max(0, width - pad.left - pad.right);
|
|
642
|
+
const h = height - pad.top - pad.bottom;
|
|
643
|
+
const sx = (x) => pad.left + (x - xMin) / (xMax - xMin || 1) * w;
|
|
644
|
+
const sy = (y) => pad.top + h - y / yMax * h;
|
|
645
|
+
const path = (pts) => pts.map((p, i) => `${i ? "L" : "M"}${sx(p.x).toFixed(1)},${sy(p.y).toFixed(1)}`).join("");
|
|
646
|
+
const last = series[series.length - 1];
|
|
647
|
+
const peak = series.reduce((a, b) => b.y > a.y ? b : a, series[0]);
|
|
648
|
+
const stops = colourScale ? [...colourScale].sort((a, b) => a.at - b.at) : [];
|
|
649
|
+
/** The colour a value lands on: the highest stop at or below it. Used for the labelled points. */
|
|
650
|
+
const colourAt = (y) => stops.length ? [...stops].reverse().find((s) => s.at <= y)?.colour ?? stops[0].colour : colour;
|
|
651
|
+
const stroke = stops.length ? `url(#${gradientId})` : colour;
|
|
652
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
653
|
+
className: cn("grid min-w-0 gap-2", className),
|
|
654
|
+
children: [
|
|
655
|
+
/* @__PURE__ */ jsx("div", {
|
|
656
|
+
ref,
|
|
657
|
+
className: "min-w-0",
|
|
658
|
+
children: width > 0 && /* @__PURE__ */ jsxs("svg", {
|
|
659
|
+
width,
|
|
660
|
+
height,
|
|
661
|
+
role: "img",
|
|
662
|
+
"aria-label": label,
|
|
663
|
+
className: "block overflow-visible",
|
|
664
|
+
children: [
|
|
665
|
+
stops.length > 0 && /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("linearGradient", {
|
|
666
|
+
id: gradientId,
|
|
667
|
+
gradientUnits: "userSpaceOnUse",
|
|
668
|
+
x1: 0,
|
|
669
|
+
x2: 0,
|
|
670
|
+
y1: sy(0),
|
|
671
|
+
y2: sy(yMax),
|
|
672
|
+
children: stops.map((s) => /* @__PURE__ */ jsx("stop", {
|
|
673
|
+
offset: Math.min(1, Math.max(0, s.at / yMax)),
|
|
674
|
+
stopColor: s.colour
|
|
675
|
+
}, s.at))
|
|
676
|
+
}) }),
|
|
677
|
+
band && /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("rect", {
|
|
678
|
+
x: pad.left,
|
|
679
|
+
y: sy(band.below),
|
|
680
|
+
width: w,
|
|
681
|
+
height: sy(0) - sy(band.below),
|
|
682
|
+
fill: "var(--success-subtle)"
|
|
683
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
684
|
+
x: pad.left + 8,
|
|
685
|
+
y: sy(band.below) + 16,
|
|
686
|
+
className: "fill-success type-caption",
|
|
687
|
+
children: band.label
|
|
688
|
+
})] }),
|
|
689
|
+
yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
|
|
690
|
+
x1: pad.left,
|
|
691
|
+
x2: pad.left + w,
|
|
692
|
+
y1: sy(t),
|
|
693
|
+
y2: sy(t),
|
|
694
|
+
stroke: "var(--border)",
|
|
695
|
+
strokeWidth: 1
|
|
696
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
697
|
+
x: pad.left - 8,
|
|
698
|
+
y: sy(t),
|
|
699
|
+
dy: "0.32em",
|
|
700
|
+
textAnchor: "end",
|
|
701
|
+
className: "fill-muted-foreground type-caption tabular-nums",
|
|
702
|
+
children: formatY(t)
|
|
703
|
+
})] }, t)),
|
|
704
|
+
xTicks.map((t) => /* @__PURE__ */ jsx("text", {
|
|
705
|
+
x: sx(t),
|
|
706
|
+
y: height - 8,
|
|
707
|
+
textAnchor: "middle",
|
|
708
|
+
className: "fill-muted-foreground type-caption tabular-nums",
|
|
709
|
+
children: formatX(t)
|
|
710
|
+
}, t)),
|
|
711
|
+
planned && /* @__PURE__ */ jsx("path", {
|
|
712
|
+
d: path(planned),
|
|
713
|
+
fill: "none",
|
|
714
|
+
stroke: "var(--muted-foreground)",
|
|
715
|
+
strokeWidth: 1.5,
|
|
716
|
+
strokeDasharray: "4 4"
|
|
717
|
+
}),
|
|
718
|
+
/* @__PURE__ */ jsx("path", {
|
|
719
|
+
d: path(series),
|
|
720
|
+
fill: "none",
|
|
721
|
+
stroke,
|
|
722
|
+
strokeWidth: stops.length ? 2.5 : 2,
|
|
723
|
+
strokeLinejoin: "round",
|
|
724
|
+
strokeLinecap: "round"
|
|
725
|
+
}),
|
|
726
|
+
/* @__PURE__ */ jsx("circle", {
|
|
727
|
+
cx: sx(peak.x),
|
|
728
|
+
cy: sy(peak.y),
|
|
729
|
+
r: 3,
|
|
730
|
+
fill: colourAt(peak.y)
|
|
731
|
+
}),
|
|
732
|
+
/* @__PURE__ */ jsxs("text", {
|
|
733
|
+
x: sx(peak.x) + 8,
|
|
734
|
+
y: sy(peak.y) - 8,
|
|
735
|
+
className: "fill-foreground type-caption tabular-nums",
|
|
736
|
+
children: [formatY(peak.y), " peak"]
|
|
737
|
+
}),
|
|
738
|
+
/* @__PURE__ */ jsx("line", {
|
|
739
|
+
x1: sx(last.x),
|
|
740
|
+
x2: sx(last.x),
|
|
741
|
+
y1: pad.top,
|
|
742
|
+
y2: pad.top + h,
|
|
743
|
+
stroke: "var(--foreground)",
|
|
744
|
+
strokeWidth: 1
|
|
745
|
+
}),
|
|
746
|
+
/* @__PURE__ */ jsx("circle", {
|
|
747
|
+
cx: sx(last.x),
|
|
748
|
+
cy: sy(last.y),
|
|
749
|
+
r: 3.5,
|
|
750
|
+
fill: colourAt(last.y)
|
|
751
|
+
}),
|
|
752
|
+
/* @__PURE__ */ jsxs("text", {
|
|
753
|
+
x: sx(last.x) - 8,
|
|
754
|
+
y: sy(last.y) - 10,
|
|
755
|
+
textAnchor: "end",
|
|
756
|
+
className: "fill-foreground type-label tabular-nums",
|
|
757
|
+
children: [formatY(last.y), " now"]
|
|
758
|
+
})
|
|
759
|
+
]
|
|
760
|
+
})
|
|
761
|
+
}),
|
|
762
|
+
/* @__PURE__ */ jsxs("figcaption", {
|
|
763
|
+
className: "flex flex-wrap gap-x-5 gap-y-1 type-caption text-muted-foreground",
|
|
764
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
765
|
+
className: "inline-flex items-center gap-2",
|
|
766
|
+
children: [
|
|
767
|
+
stops.length ? /* @__PURE__ */ jsx("svg", {
|
|
768
|
+
width: stops.length * 8,
|
|
769
|
+
height: "3",
|
|
770
|
+
"aria-hidden": true,
|
|
771
|
+
children: stops.map((s, i) => /* @__PURE__ */ jsx("line", {
|
|
772
|
+
x1: i * 8,
|
|
773
|
+
x2: i * 8 + 8,
|
|
774
|
+
y1: "1.5",
|
|
775
|
+
y2: "1.5",
|
|
776
|
+
stroke: s.colour,
|
|
777
|
+
strokeWidth: 3
|
|
778
|
+
}, s.at))
|
|
779
|
+
}) : /* @__PURE__ */ jsx("svg", {
|
|
780
|
+
width: "16",
|
|
781
|
+
height: "2",
|
|
782
|
+
"aria-hidden": true,
|
|
783
|
+
children: /* @__PURE__ */ jsx("line", {
|
|
784
|
+
x1: "0",
|
|
785
|
+
x2: "16",
|
|
786
|
+
y1: "1",
|
|
787
|
+
y2: "1",
|
|
788
|
+
stroke: colour,
|
|
789
|
+
strokeWidth: 2
|
|
790
|
+
})
|
|
791
|
+
}),
|
|
792
|
+
seriesLabel,
|
|
793
|
+
stops.length > 0 && colourScaleLabel && /* @__PURE__ */ jsxs("span", { children: [
|
|
794
|
+
"(",
|
|
795
|
+
colourScaleLabel,
|
|
796
|
+
")"
|
|
797
|
+
] })
|
|
798
|
+
]
|
|
799
|
+
}), planned && plannedLabel && /* @__PURE__ */ jsxs("span", {
|
|
800
|
+
className: "inline-flex items-center gap-2",
|
|
801
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
802
|
+
width: "16",
|
|
803
|
+
height: "2",
|
|
804
|
+
"aria-hidden": true,
|
|
805
|
+
children: /* @__PURE__ */ jsx("line", {
|
|
806
|
+
x1: "0",
|
|
807
|
+
x2: "16",
|
|
808
|
+
y1: "1",
|
|
809
|
+
y2: "1",
|
|
810
|
+
stroke: "var(--muted-foreground)",
|
|
811
|
+
strokeWidth: 1.5,
|
|
812
|
+
strokeDasharray: "4 4"
|
|
813
|
+
})
|
|
814
|
+
}), plannedLabel]
|
|
815
|
+
})]
|
|
816
|
+
}),
|
|
817
|
+
/* @__PURE__ */ jsx("div", {
|
|
818
|
+
className: "sr-only",
|
|
819
|
+
children: /* @__PURE__ */ jsxs("table", { children: [
|
|
820
|
+
/* @__PURE__ */ jsx("caption", { children: label }),
|
|
821
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
|
|
822
|
+
scope: "col",
|
|
823
|
+
children: "Time"
|
|
824
|
+
}), /* @__PURE__ */ jsx("th", {
|
|
825
|
+
scope: "col",
|
|
826
|
+
children: seriesLabel
|
|
827
|
+
})] }) }),
|
|
828
|
+
/* @__PURE__ */ jsx("tbody", { children: series.map((p) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", { children: formatX(p.x) }), /* @__PURE__ */ jsx("td", { children: formatY(p.y) })] }, p.x)) })
|
|
829
|
+
] })
|
|
830
|
+
})
|
|
831
|
+
]
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
// src/components/page-header.tsx
|
|
835
|
+
function PageHeader({ title, scope, actions, finding, as: Heading = "h1" }) {
|
|
836
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
837
|
+
className: "grid gap-4",
|
|
838
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
839
|
+
className: "flex flex-wrap items-end justify-between gap-x-6 gap-y-3",
|
|
840
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
841
|
+
className: "grid min-w-0 gap-0.5",
|
|
842
|
+
children: [/* @__PURE__ */ jsx(Heading, {
|
|
843
|
+
className: "type-title text-balance",
|
|
844
|
+
children: title
|
|
845
|
+
}), scope && /* @__PURE__ */ jsx("p", {
|
|
846
|
+
className: "type-body-sm text-muted-foreground",
|
|
847
|
+
children: scope
|
|
848
|
+
})]
|
|
849
|
+
}), actions && /* @__PURE__ */ jsx("div", {
|
|
850
|
+
className: "flex flex-wrap gap-2",
|
|
851
|
+
children: actions
|
|
852
|
+
})]
|
|
853
|
+
}), finding && /* @__PURE__ */ jsxs("div", {
|
|
854
|
+
className: "flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-y border-border py-3",
|
|
855
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
856
|
+
className: "max-w-prose min-w-0 type-body text-pretty",
|
|
857
|
+
children: finding.text
|
|
858
|
+
}), finding.action]
|
|
859
|
+
})]
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
// src/components/undo.tsx
|
|
863
|
+
function useUndoable(windowMs = 8e3) {
|
|
864
|
+
const [pending, setPending] = React.useState(null);
|
|
865
|
+
const current = React.useRef(null);
|
|
866
|
+
const timer = React.useRef(void 0);
|
|
867
|
+
const set = React.useCallback((p) => {
|
|
868
|
+
current.current = p;
|
|
869
|
+
setPending(p);
|
|
870
|
+
}, []);
|
|
871
|
+
const schedule = React.useCallback((next) => {
|
|
872
|
+
window.clearTimeout(timer.current);
|
|
873
|
+
current.current?.commit();
|
|
874
|
+
set(next);
|
|
875
|
+
timer.current = window.setTimeout(() => {
|
|
876
|
+
current.current?.commit();
|
|
877
|
+
set(null);
|
|
878
|
+
}, windowMs);
|
|
879
|
+
}, [set, windowMs]);
|
|
880
|
+
const undo = React.useCallback(() => {
|
|
881
|
+
window.clearTimeout(timer.current);
|
|
882
|
+
set(null);
|
|
883
|
+
}, [set]);
|
|
884
|
+
React.useEffect(() => () => {
|
|
885
|
+
window.clearTimeout(timer.current);
|
|
886
|
+
current.current?.commit();
|
|
887
|
+
current.current = null;
|
|
888
|
+
}, []);
|
|
889
|
+
return {
|
|
890
|
+
pending,
|
|
891
|
+
schedule,
|
|
892
|
+
undo
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function UndoBar({ pending, onUndo, inline = false }) {
|
|
896
|
+
return /* @__PURE__ */ jsx("div", {
|
|
897
|
+
"aria-live": "polite",
|
|
898
|
+
className: inline ? "flex" : "pointer-events-none fixed inset-x-0 bottom-4 z-50 flex justify-center px-4",
|
|
899
|
+
children: pending && /* @__PURE__ */ jsxs("div", {
|
|
900
|
+
className: "pointer-events-auto flex max-w-lg items-center gap-4 rounded-overlay bg-foreground py-2 pr-2 pl-4 text-background shadow-overlay",
|
|
901
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
902
|
+
className: "min-w-0 type-body-sm",
|
|
903
|
+
children: pending.message
|
|
904
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
905
|
+
size: "sm",
|
|
906
|
+
variant: "ghost",
|
|
907
|
+
className: "text-background hover:bg-neutral-11",
|
|
908
|
+
onClick: onUndo,
|
|
909
|
+
children: "Undo"
|
|
910
|
+
})]
|
|
911
|
+
})
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
// src/components/done.tsx
|
|
915
|
+
function Done({ title, next, figure, actions, focusOnMount = true, className }) {
|
|
916
|
+
const heading = React.useRef(null);
|
|
917
|
+
const titleId = React.useId();
|
|
918
|
+
React.useEffect(() => {
|
|
919
|
+
if (focusOnMount) heading.current?.focus();
|
|
920
|
+
}, [focusOnMount]);
|
|
921
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
922
|
+
"aria-labelledby": titleId,
|
|
923
|
+
className: cn("grid gap-5", className),
|
|
924
|
+
children: [
|
|
925
|
+
/* @__PURE__ */ jsxs("div", {
|
|
926
|
+
className: "grid gap-2",
|
|
927
|
+
children: [/* @__PURE__ */ jsxs("h1", {
|
|
928
|
+
id: titleId,
|
|
929
|
+
ref: heading,
|
|
930
|
+
tabIndex: -1,
|
|
931
|
+
className: "flex items-start gap-2 type-title",
|
|
932
|
+
children: [/* @__PURE__ */ jsx(Check, {
|
|
933
|
+
"aria-hidden": true,
|
|
934
|
+
className: "mt-1 size-6 shrink-0 text-success"
|
|
935
|
+
}), title]
|
|
936
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
937
|
+
className: "max-w-prose type-body text-muted-foreground",
|
|
938
|
+
children: next
|
|
939
|
+
})]
|
|
940
|
+
}),
|
|
941
|
+
figure,
|
|
942
|
+
actions && /* @__PURE__ */ jsx("div", {
|
|
943
|
+
className: "flex flex-wrap items-center gap-3",
|
|
944
|
+
children: actions
|
|
945
|
+
})
|
|
946
|
+
]
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
// src/components/app-shell.tsx
|
|
950
|
+
function AppShell({ product, nav, navLabel = "Main", topBar, children, className }) {
|
|
951
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
952
|
+
className: cn("min-h-dvh md:grid md:grid-cols-[14rem_minmax(0,1fr)]", className),
|
|
953
|
+
children: [/* @__PURE__ */ jsx("aside", {
|
|
954
|
+
className: "border-b border-border md:border-r md:border-b-0",
|
|
955
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
956
|
+
className: "flex items-center gap-4 px-4 py-3 md:block md:px-3 md:py-4",
|
|
957
|
+
children: [/* @__PURE__ */ jsx(Link, {
|
|
958
|
+
to: "/",
|
|
959
|
+
className: "shrink-0 rounded-control px-2 type-heading text-foreground no-underline",
|
|
960
|
+
children: product
|
|
961
|
+
}), /* @__PURE__ */ jsx(SideNav, {
|
|
962
|
+
items: nav,
|
|
963
|
+
label: navLabel,
|
|
964
|
+
className: "min-w-0 flex-1 md:mt-4"
|
|
965
|
+
})]
|
|
966
|
+
})
|
|
967
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
968
|
+
className: "min-w-0",
|
|
969
|
+
children: [topBar && /* @__PURE__ */ jsx("div", {
|
|
970
|
+
className: "flex items-center gap-3 border-b border-border px-4 py-2 md:px-8",
|
|
971
|
+
children: topBar
|
|
972
|
+
}), /* @__PURE__ */ jsx("main", {
|
|
973
|
+
className: "px-4 py-6 md:px-8 md:py-8",
|
|
974
|
+
children
|
|
975
|
+
})]
|
|
976
|
+
})]
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
function SideNav({ items, label, className }) {
|
|
980
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
981
|
+
"aria-label": label,
|
|
982
|
+
className: cn("min-w-0", className),
|
|
983
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
984
|
+
className: "flex gap-1 overflow-x-auto md:grid md:overflow-visible",
|
|
985
|
+
children: items.map(({ to, label, icon: Icon, end }) => /* @__PURE__ */ jsx("li", {
|
|
986
|
+
className: "shrink-0",
|
|
987
|
+
children: /* @__PURE__ */ jsxs(NavLink, {
|
|
988
|
+
to,
|
|
989
|
+
end,
|
|
990
|
+
className: ({ isActive }) => cn("flex h-(--control-h-sm) items-center gap-2 rounded-control px-2 type-label whitespace-nowrap no-underline transition-state", isActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"),
|
|
991
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, {
|
|
992
|
+
"aria-hidden": true,
|
|
993
|
+
className: "size-4 shrink-0"
|
|
994
|
+
}), label]
|
|
995
|
+
})
|
|
996
|
+
}, to))
|
|
997
|
+
})
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function Breadcrumb({ items }) {
|
|
1001
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
1002
|
+
"aria-label": "Breadcrumb",
|
|
1003
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
1004
|
+
className: "flex flex-wrap items-center gap-x-2 type-body-sm text-muted-foreground",
|
|
1005
|
+
children: items.map((item, i) => {
|
|
1006
|
+
const last = i === items.length - 1;
|
|
1007
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
1008
|
+
className: "flex items-center gap-2",
|
|
1009
|
+
children: [item.to && !last ? /* @__PURE__ */ jsx(Link, {
|
|
1010
|
+
to: item.to,
|
|
1011
|
+
className: "text-muted-foreground underline hover:text-foreground",
|
|
1012
|
+
children: item.label
|
|
1013
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1014
|
+
"aria-current": last ? "page" : void 0,
|
|
1015
|
+
className: cn(last && "text-foreground"),
|
|
1016
|
+
children: item.label
|
|
1017
|
+
}), !last && /* @__PURE__ */ jsx("span", {
|
|
1018
|
+
"aria-hidden": true,
|
|
1019
|
+
children: "/"
|
|
1020
|
+
})]
|
|
1021
|
+
}, `${item.label}-${i}`);
|
|
1022
|
+
})
|
|
1023
|
+
})
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
// src/components/tabs.tsx
|
|
1027
|
+
function Tabs({ param, items, label, children, className }) {
|
|
1028
|
+
const [params, setParams] = useSearchParams();
|
|
1029
|
+
const fallback = items[0]?.value ?? "";
|
|
1030
|
+
const raw = params.get(param);
|
|
1031
|
+
const value = items.some((i) => i.value === raw) ? raw : fallback;
|
|
1032
|
+
const baseId = React.useId();
|
|
1033
|
+
const refs = React.useRef([]);
|
|
1034
|
+
const select = (next, focus = false) => {
|
|
1035
|
+
setParams((p) => {
|
|
1036
|
+
const out = new URLSearchParams(p);
|
|
1037
|
+
if (next === fallback) out.delete(param);
|
|
1038
|
+
else out.set(param, next);
|
|
1039
|
+
return out;
|
|
1040
|
+
}, { replace: true });
|
|
1041
|
+
if (focus) refs.current[items.findIndex((i) => i.value === next)]?.focus();
|
|
1042
|
+
};
|
|
1043
|
+
const onKeyDown = (e, index) => {
|
|
1044
|
+
const last = items.length - 1;
|
|
1045
|
+
const to = e.key === "ArrowRight" ? index === last ? 0 : index + 1 : e.key === "ArrowLeft" ? index === 0 ? last : index - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : void 0;
|
|
1046
|
+
if (to === void 0) return;
|
|
1047
|
+
e.preventDefault();
|
|
1048
|
+
select(items[to].value, true);
|
|
1049
|
+
};
|
|
1050
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1051
|
+
className: cn("grid gap-4", className),
|
|
1052
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1053
|
+
role: "tablist",
|
|
1054
|
+
"aria-label": label,
|
|
1055
|
+
className: "flex gap-4 overflow-x-auto border-b border-border",
|
|
1056
|
+
children: items.map((item, i) => {
|
|
1057
|
+
const selected = item.value === value;
|
|
1058
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1059
|
+
ref: (el) => {
|
|
1060
|
+
refs.current[i] = el;
|
|
1061
|
+
},
|
|
1062
|
+
type: "button",
|
|
1063
|
+
role: "tab",
|
|
1064
|
+
id: `${baseId}-tab-${item.value}`,
|
|
1065
|
+
"aria-selected": selected,
|
|
1066
|
+
"aria-controls": `${baseId}-panel`,
|
|
1067
|
+
tabIndex: selected ? 0 : -1,
|
|
1068
|
+
onClick: () => select(item.value),
|
|
1069
|
+
onKeyDown: (e) => onKeyDown(e, i),
|
|
1070
|
+
className: cn("-mb-px cursor-pointer border-b-2 py-2 type-label whitespace-nowrap transition-state", selected ? "border-foreground font-semibold text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"),
|
|
1071
|
+
children: item.label
|
|
1072
|
+
}, item.value);
|
|
1073
|
+
})
|
|
1074
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1075
|
+
role: "tabpanel",
|
|
1076
|
+
id: `${baseId}-panel`,
|
|
1077
|
+
"aria-labelledby": `${baseId}-tab-${value}`,
|
|
1078
|
+
tabIndex: 0,
|
|
1079
|
+
children: children(value)
|
|
1080
|
+
})]
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
export { AppShell, AsyncButton, Breadcrumb, Button, ChoiceGroup, DataTable, DataView, Done, EmptyState, Field, HttpError, IconButton, Input, LineChart, Metric, MetricStrip, NBSP, NativeSelect, PageHeader, SideNav, Skeleton, Status, StrataQueryProvider, TableSkeleton, Tabs, Textarea, UndoBar, VirtualList, buttonVariants, cn, createFormat, createQueryClient, describeHttpError, fetchJson, format, parseSort, serialiseSort, toLoad, typeSteps, useAsyncAction, useLoad, useUndoable };
|
|
1084
|
+
|
|
1085
|
+
//# sourceMappingURL=index.js.map
|