@momoi-labs/kiso-react 0.6.1 → 0.8.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/README.md CHANGED
@@ -66,6 +66,8 @@ Data and structure
66
66
  primary action in the top bar and no sidebar rail.
67
67
  - Split, Pane, and Splitter, a list-detail layout with a resizable divider.
68
68
  - LogView, LogViewLine, LogViewTime, and LogViewLevel.
69
+ - StepList, a run's steps with a state each, and StepBar, the same run as
70
+ one segment per step for a summary, a table cell, or a toast.
69
71
  - Stat, StatHeader, StatLabel, StatValue, StatFoot, and StatDelta.
70
72
  - KV, KVKey, and KVValue, a description list of fixed facts.
71
73
  - Separator, horizontal or vertical, and Dot and `dotVariants`.
@@ -82,6 +84,10 @@ Navigation
82
84
  - ThemeSelector, a controlled selector. The application owns theme persistence
83
85
  and applies `data-theme="light"` or `data-theme="dark"` to the document root.
84
86
  Remove the attribute for system mode.
87
+ - AccentSelector, a controlled selector, plus the `accents` list and `Accent`
88
+ type. The application owns accent persistence and applies
89
+ `data-accent="<name>"` to the document root, or to any container that should
90
+ carry its own accent. No attribute means violet.
85
91
 
86
92
  Feedback
87
93
 
@@ -193,3 +199,49 @@ isolated consumer, including TypeScript, CSS bundling, and rendered semantics.
193
199
 
194
200
  See [Publishing Kiso](../../docs/publishing.md) for releases. Adapted shadcn/ui
195
201
  code retains its upstream MIT notice in `SHADCN-LICENSE`.
202
+
203
+ ## Metrics dashboards
204
+
205
+ Chart supports `line` and `stacked-area`, one to five named series, null gaps,
206
+ UTC timestamps, synchronized inspection through `syncId`, a summary legend,
207
+ and a disclosure containing every exact sample. Pass the same timestamp grid
208
+ to synchronized panels. Put the common unit in `label`.
209
+
210
+ ```tsx
211
+ <Chart
212
+ label="CPU (%)"
213
+ series={[{ key: "user", label: "User", slot: 1 }]}
214
+ data={[
215
+ { timestamp: 1789203600000, values: { user: 24 } },
216
+ { timestamp: 1789203610000, values: { user: null } },
217
+ { timestamp: 1789203620000, values: { user: 31 } },
218
+ ]}
219
+ />
220
+ ```
221
+
222
+ ChartLegend is also exported for standalone summaries. Meter, Progress,
223
+ BarGauge, Disclosure, TimeRangeControl, DashboardGrid, and DashboardPanel
224
+ compose the surrounding dashboard. See their contracts in `@momoi-labs/kiso`
225
+ and the gallery previews for complete examples. Sparkline now accepts null
226
+ gaps without shifting later sample positions.
227
+
228
+ Chart presentation options compose independently:
229
+
230
+ ```tsx
231
+ <Chart
232
+ label="CPU (%)"
233
+ data={samples}
234
+ series={series}
235
+ variant="stacked-area"
236
+ layout="compact"
237
+ legend="inline"
238
+ highlightSeries={highlight}
239
+ onHighlightSeriesChange={setHighlight}
240
+ />
241
+ ```
242
+
243
+ `layout` accepts `standard`, `compact`, or `split`; `legend` accepts `table`,
244
+ `inline`, or `sidebar`. Split requires `variant="line"` and keeps a shared
245
+ scale across lanes. The sidebar follows the inspected timestamp. Omit
246
+ `highlightSeries` for local legend selection, or pass a key/null with the
247
+ callback for controlled selection. All combinations retain exact values.
@@ -0,0 +1,9 @@
1
+ export declare const accents: readonly ["violet", "terracotta", "teal", "cobalt", "nocturne"];
2
+ export type Accent = (typeof accents)[number];
3
+ export declare function AccentSelector({ accent, onChange, options, preview, }: {
4
+ accent: string;
5
+ onChange: (accent: Accent) => void;
6
+ options?: readonly Accent[];
7
+ /** Render the live preview of the selected accent below the pills. */
8
+ preview?: boolean;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ export const accents = [
4
+ "violet",
5
+ "terracotta",
6
+ "teal",
7
+ "cobalt",
8
+ "nocturne",
9
+ ];
10
+ const label = (accent) => `${accent[0].toUpperCase()}${accent.slice(1)}`;
11
+ export function AccentSelector({ accent, onChange, options = accents, preview = true, }) {
12
+ return (_jsxs("div", { className: "accent-row", children: [_jsxs("div", { className: "theme-row", children: [_jsx("span", { className: "t-label", children: "Accent" }), _jsx("div", { className: "accent-pills", role: "group", "aria-label": "Accent", children: options.map((option) => (_jsxs("button", { type: "button", className: "accent-pill", "aria-pressed": accent === option, "data-accent": option, onClick: () => onChange(option), children: [_jsx("span", { className: "accent-dot", "aria-hidden": "true" }), label(option)] }, option))) })] }), preview && (_jsxs("div", { className: "accent-preview", "data-accent": accent, "aria-hidden": "true", children: [_jsxs("span", { className: "accent-preview-sidebar", children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {}), _jsx("i", {})] }), _jsxs("span", { className: "accent-preview-main", children: [_jsx("i", { className: "accent-preview-title" }), _jsx("i", { className: "accent-preview-text" }), _jsx("i", { className: "accent-preview-link" }), _jsxs("span", { className: "accent-preview-actions", children: [_jsx("i", { className: "accent-preview-primary" }), _jsx("i", { className: "accent-preview-secondary" })] }), _jsx("i", { className: "accent-preview-selected" })] })] }))] }));
13
+ }
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ export type BarGaugeProps = Omit<React.ComponentProps<"div">, "children"> & {
3
+ label: string;
4
+ rows: readonly {
5
+ key: string;
6
+ label: string;
7
+ value: number | null;
8
+ }[];
9
+ max: number;
10
+ formatValue?: (value: number) => string;
11
+ };
12
+ export declare function BarGauge({ label, rows, max, formatValue, className, ...props }: BarGaugeProps): React.JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { clsx as cn } from "clsx";
3
+ import { Meter } from "./meter.js";
4
+ export function BarGauge({ label, rows, max, formatValue = String, className, ...props }) {
5
+ if (!Number.isFinite(max) || max <= 0)
6
+ throw new RangeError("Bar gauge max must be positive.");
7
+ return _jsx("div", { ...props, role: "group", "aria-label": label, "data-slot": "bar-gauge", className: cn("bar-gauge", className), children: rows.length ? rows.map(row => _jsx(Meter, { label: row.label, value: row.value, max: max, valueText: row.value !== null && Number.isFinite(row.value) ? formatValue(row.value) : "Not collected" }, row.key))
8
+ : _jsx("p", { children: "No measurements." }) });
9
+ }
@@ -0,0 +1,24 @@
1
+ export type ChartSample = {
2
+ /** Epoch milliseconds. Include every expected timestamp, including gaps. */
3
+ timestamp: number;
4
+ values: Readonly<Record<string, number | null>>;
5
+ };
6
+ export type ChartSeries = {
7
+ key: string;
8
+ label: string;
9
+ /** Stable identity across panels. Defaults to its position, starting at 1. */
10
+ slot?: 1 | 2 | 3 | 4 | 5;
11
+ };
12
+ export declare const chartStrokes: readonly [undefined, "8 4", "2 3", "8 3 2 3", "12 3 2 3 2 3"];
13
+ export declare const chartSlot: (series: ChartSeries, index: number) => number;
14
+ export declare const chartColor: (slot: number) => string;
15
+ export declare const measured: (value: number | null | undefined) => value is number;
16
+ export declare function validateChart(data: readonly ChartSample[], series: readonly ChartSeries[]): void;
17
+ export declare function seriesSummary(data: readonly ChartSample[], key: string): {
18
+ min: number | null;
19
+ max: number | null;
20
+ avg: number | null;
21
+ current: number | null;
22
+ };
23
+ export declare const formatChartTime: (timestamp: number) => string;
24
+ export declare const formatChartValue: (value: number) => string;
@@ -0,0 +1,34 @@
1
+ export const chartStrokes = [undefined, "8 4", "2 3", "8 3 2 3", "12 3 2 3 2 3"];
2
+ export const chartSlot = (series, index) => series.slot ?? index + 1;
3
+ export const chartColor = (slot) => `var(--color-chart-${slot})`;
4
+ export const measured = (value) => typeof value === "number" && Number.isFinite(value);
5
+ export function validateChart(data, series) {
6
+ if (series.length < 1 || series.length > 5)
7
+ throw new RangeError("Charts require one to five series.");
8
+ const keys = new Set();
9
+ const slots = new Set();
10
+ series.forEach((item, index) => {
11
+ const slot = chartSlot(item, index);
12
+ if (!item.key || !item.label || keys.has(item.key) || slots.has(slot) || slot < 1 || slot > 5 || !Number.isInteger(slot)) {
13
+ throw new RangeError("Chart series require unique keys, slots from 1 to 5, and labels.");
14
+ }
15
+ keys.add(item.key);
16
+ slots.add(slot);
17
+ });
18
+ data.forEach((sample, index) => {
19
+ if (!Number.isFinite(sample.timestamp) || Math.abs(sample.timestamp) > 8.64e15 || (index > 0 && sample.timestamp <= data[index - 1].timestamp)) {
20
+ throw new RangeError("Chart timestamps must be finite and strictly increasing.");
21
+ }
22
+ });
23
+ }
24
+ export function seriesSummary(data, key) {
25
+ const values = data.map(sample => sample.values[key]).filter(measured);
26
+ return {
27
+ min: values.length ? Math.min(...values) : null,
28
+ max: values.length ? Math.max(...values) : null,
29
+ avg: values.length ? values.reduce((sum, value) => sum + value / values.length, 0) : null,
30
+ current: measured(data.at(-1)?.values[key]) ? data.at(-1).values[key] : null,
31
+ };
32
+ }
33
+ export const formatChartTime = (timestamp) => new Date(timestamp).toISOString();
34
+ export const formatChartValue = (value) => String(value);
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ import { type ChartSample, type ChartSeries } from "./chart-data.js";
3
+ export type ChartLegendProps = React.ComponentProps<"div"> & {
4
+ data: readonly ChartSample[];
5
+ series: readonly ChartSeries[];
6
+ label: string;
7
+ formatValue?: (value: number) => string;
8
+ variant?: "table" | "inline" | "sidebar";
9
+ activeTimestamp?: number | null;
10
+ formatTime?: (timestamp: number) => string;
11
+ highlightSeries?: string | null;
12
+ onHighlightSeriesChange?: (key: string | null) => void;
13
+ };
14
+ export declare function ChartSeriesLabel({ series, index }: {
15
+ series: ChartSeries;
16
+ index: number;
17
+ }): React.JSX.Element;
18
+ export declare function ChartLegend({ data, series, label, formatValue, variant, activeTimestamp, formatTime, highlightSeries, onHighlightSeriesChange, className, ...props }: ChartLegendProps): React.JSX.Element;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { clsx as cn } from "clsx";
4
+ import { chartColor, chartSlot, formatChartTime, formatChartValue, measured, seriesSummary, validateChart } from "./chart-data.js";
5
+ export function ChartSeriesLabel({ series, index }) {
6
+ const slot = chartSlot(series, index);
7
+ return _jsxs("span", { className: "chart-series-label", children: [_jsx("svg", { "aria-hidden": "true", width: "28", height: "12", viewBox: "0 0 28 12", children: _jsx("path", { d: "M0 6H28", stroke: chartColor(slot), strokeWidth: "2" }) }), _jsxs("span", { children: [slot, ". ", series.label] })] });
8
+ }
9
+ export function ChartLegend({ data, series, label, formatValue = formatChartValue, variant = "table", activeTimestamp, formatTime = formatChartTime, highlightSeries, onHighlightSeriesChange, className, ...props }) {
10
+ validateChart(data, series);
11
+ const seriesLabel = (item, index) => onHighlightSeriesChange
12
+ ? _jsx("button", { type: "button", className: "chart-series-button", "aria-pressed": highlightSeries === item.key, onClick: () => onHighlightSeriesChange(highlightSeries === item.key ? null : item.key), children: _jsx(ChartSeriesLabel, { series: item, index: index }) })
13
+ : _jsx(ChartSeriesLabel, { series: item, index: index });
14
+ if (variant !== "table") {
15
+ const sample = activeTimestamp == null ? data.at(-1) : data.find(item => item.timestamp === activeTimestamp);
16
+ return _jsxs("div", { ...props, "data-slot": "chart-legend", "data-variant": variant, className: cn("chart-legend", `chart-legend-${variant}`, className), "aria-label": label, "aria-live": variant === "sidebar" ? "polite" : undefined, children: [_jsx("span", { className: "chart-legend-caption", children: variant === "sidebar" && activeTimestamp != null
17
+ ? formatTime(activeTimestamp) : "Current" }), _jsx("dl", { children: series.map((item, index) => _jsxs("div", { children: [_jsx("dt", { children: seriesLabel(item, index) }), _jsx("dd", { children: measured(sample?.values[item.key]) ? formatValue(sample.values[item.key]) : "Not collected" })] }, item.key)) })] });
18
+ }
19
+ return _jsx("div", { ...props, "data-slot": "chart-legend", "data-variant": "table", className: cn("chart-table-scroll", className), children: _jsxs("table", { className: "chart-table", children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Series" }), ["Min", "Max", "Avg", "Current"].map(name => _jsx("th", { scope: "col", children: name }, name))] }) }), _jsx("tbody", { children: series.map((item, index) => {
20
+ const summary = seriesSummary(data, item.key);
21
+ return _jsxs("tr", { children: [_jsx("th", { scope: "row", children: seriesLabel(item, index) }), ["min", "max", "avg", "current"].map(stat => _jsx("td", { children: summary[stat] === null ? "Not collected" : formatValue(summary[stat]) }, stat))] }, item.key);
22
+ }) })] }) });
23
+ }
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ import { type ChartSample, type ChartSeries } from "./chart-data.js";
3
+ export type { ChartSample, ChartSeries } from "./chart-data.js";
4
+ export type ChartProps = Omit<React.ComponentProps<"figure">, "children"> & {
5
+ data: readonly ChartSample[];
6
+ series: readonly ChartSeries[];
7
+ label: string;
8
+ variant?: "line" | "stacked-area";
9
+ layout?: "standard" | "compact" | "split";
10
+ legend?: "table" | "inline" | "sidebar";
11
+ /** A controlled highlight. Pass null to clear it, or omit for local selection. */
12
+ highlightSeries?: string | null;
13
+ onHighlightSeriesChange?: (key: string | null) => void;
14
+ height?: number;
15
+ /** Panels in a group inspect the same timestamp, not the same array index. */
16
+ syncId?: string;
17
+ formatTime?: (timestamp: number) => string;
18
+ formatValue?: (value: number) => string;
19
+ min?: number;
20
+ max?: number;
21
+ };
22
+ export declare function Chart({ data, series, label, variant, layout, legend, highlightSeries, onHighlightSeriesChange, height, syncId, formatTime, formatValue, min, max, className, ...props }: ChartProps): React.JSX.Element;
package/dist/chart.js ADDED
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { clsx as cn } from "clsx";
5
+ import { Area, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
6
+ import { ChartLegend, ChartSeriesLabel } from "./chart-legend.js";
7
+ import { chartColor, chartSlot, formatChartTime, formatChartValue, measured, validateChart } from "./chart-data.js";
8
+ export function Chart({ data, series, label, variant = "line", layout = "standard", legend = "table", highlightSeries, onHighlightSeriesChange, height = layout === "compact" ? 165 : layout === "split" ? 90 : 200, syncId, formatTime = formatChartTime, formatValue = formatChartValue, min, max, className, ...props }) {
9
+ const id = React.useId();
10
+ const [localHighlight, setLocalHighlight] = React.useState(null);
11
+ const [activeTimestamp, setActiveTimestamp] = React.useState(null);
12
+ const requestedHighlight = highlightSeries === undefined ? localHighlight : highlightSeries;
13
+ const highlighted = series.some(item => item.key === requestedHighlight) ? requestedHighlight : null;
14
+ const highlight = (key) => {
15
+ if (highlightSeries === undefined)
16
+ setLocalHighlight(key);
17
+ onHighlightSeriesChange?.(key);
18
+ };
19
+ validateChart(data, series);
20
+ if (layout === "split" && variant === "stacked-area") {
21
+ throw new RangeError("Split layout requires line charts. Stacked areas share one plot.");
22
+ }
23
+ if (variant === "stacked-area" && data.some(sample => series.some(item => measured(sample.values[item.key]) && sample.values[item.key] < 0))) {
24
+ throw new RangeError("Stacked areas require nonnegative values. Use lines for signed data.");
25
+ }
26
+ if (!Number.isFinite(height) || height <= 0 || (min !== undefined && !Number.isFinite(min))
27
+ || (max !== undefined && !Number.isFinite(max)) || (min !== undefined && max !== undefined && min >= max)) {
28
+ throw new RangeError("Chart height must be positive and numeric bounds must be finite and ordered.");
29
+ }
30
+ // A partial stack has an unknown total. Omit the entire column rather than
31
+ // drawing the missing contribution as zero. Tables retain the known values.
32
+ const plot = data.map(sample => {
33
+ const gap = variant === "stacked-area" && series.some(item => !measured(sample.values[item.key]));
34
+ return { timestamp: sample.timestamp, values: series.map(item => !gap && measured(sample.values[item.key]) ? sample.values[item.key] : null) };
35
+ });
36
+ const hasData = plot.some(sample => sample.values.some(measured));
37
+ const displayValue = (value) => measured(value) ? formatValue(value) : "Not collected";
38
+ const known = plot.flatMap(sample => sample.values).filter(measured);
39
+ let splitMin = min ?? Math.min(0, ...known);
40
+ let splitMax = max ?? Math.max(0, ...known);
41
+ if (splitMin === splitMax) {
42
+ if (max === undefined)
43
+ splitMax += Math.max(1, Math.abs(splitMax) * 0.1);
44
+ else
45
+ splitMin -= Math.max(1, Math.abs(splitMin) * 0.1);
46
+ }
47
+ const renderPlot = (indices, lane) => _jsx("div", { className: "chart-plot", style: { height }, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: plot, syncId: syncId ?? (layout === "split" ? id : undefined), syncMethod: "value", accessibilityLayer: true, "aria-label": lane ? `${label}: ${lane.label}` : label, "aria-describedby": `${id}-help`, margin: { top: 8, right: 12, bottom: 0, left: 0 }, children: [_jsx(CartesianGrid, { vertical: false, stroke: "var(--color-border)", strokeWidth: 0.5 }), _jsx(XAxis, { dataKey: "timestamp", type: "number", domain: ["dataMin", "dataMax"], scale: "time", axisLine: false, tickLine: false, tickFormatter: formatTime, minTickGap: 48, tick: { fill: "var(--color-muted-foreground)" } }), _jsx(YAxis, { domain: layout === "split" ? [splitMin, splitMax]
48
+ : [min ?? (variant === "stacked-area" ? 0 : "auto"), max ?? "auto"], tickCount: 3, axisLine: false, tickLine: false, tickFormatter: formatValue, tick: { fill: "var(--color-muted-foreground)" }, width: 56 }), _jsx(Tooltip, { filterNull: false, isAnimationActive: false, cursor: { stroke: "var(--color-border-strong)", strokeDasharray: "3 3" }, content: ({ active, label: timestamp }) => _jsx(ChartReadout, { active: active, timestamp: Number(timestamp), data: data, series: series, formatTime: formatTime, displayValue: displayValue, sidebar: legend === "sidebar", onInspect: legend === "sidebar" && (!lane || indices[0] === 0) ? setActiveTimestamp : undefined }) }), indices.map(index => {
49
+ const item = series[index];
50
+ const slot = chartSlot(item, index);
51
+ const dimmed = highlighted !== null && item.key !== highlighted;
52
+ const common = { dataKey: `values.${index}`, name: item.label, stroke: chartColor(slot),
53
+ strokeWidth: item.key === highlighted ? 2.2 : 1.5, strokeOpacity: dimmed ? 0.3 : 1,
54
+ connectNulls: false, isAnimationActive: false,
55
+ dot: (dotProps) => {
56
+ const at = dotProps.index ?? -1;
57
+ const isolated = measured(plot[at]?.values[index])
58
+ && !measured(plot[at - 1]?.values[index]) && !measured(plot[at + 1]?.values[index]);
59
+ return _jsx("circle", { cx: dotProps.cx, cy: dotProps.cy, r: isolated ? 3 : 0, fill: chartColor(slot), opacity: dimmed ? 0.3 : 1 }, at);
60
+ }, activeDot: { r: 4 } };
61
+ return variant === "stacked-area"
62
+ ? _jsx(Area, { ...common, type: "linear", stackId: "total", fill: chartColor(slot), fillOpacity: dimmed ? 0.06 : 0.2 }, item.key)
63
+ : _jsx(Line, { ...common, type: "linear" }, item.key);
64
+ })] }) }) });
65
+ const legendContent = _jsx(ChartLegend, { data: data, series: series, label: `${label}: summary`, variant: legend, formatValue: formatValue, formatTime: formatTime, activeTimestamp: legend === "sidebar" && hasData ? activeTimestamp : null, highlightSeries: highlighted, onHighlightSeriesChange: highlight });
66
+ return _jsxs("figure", { ...props, "data-slot": "chart", "data-layout": layout, "data-legend": legend, className: cn("framed-chart", className), "aria-labelledby": `${id}-title`, children: [_jsx("figcaption", { id: `${id}-title`, className: "t-h3", children: label }), _jsx("p", { id: `${id}-help`, className: "chart-help", children: "Focus the chart and use Left and Right to inspect samples. Select a series in the legend to highlight it. Exact values are available below." }), _jsxs("div", { className: "chart-body", children: [_jsx("div", { className: "chart-plots", children: hasData ? layout === "split" ? series.map((item, index) => _jsxs("div", { className: "chart-lane", children: [_jsxs("div", { className: "chart-lane-heading", children: [_jsx(ChartSeriesLabel, { series: item, index: index }), _jsx("span", { children: displayValue(data.at(-1)?.values[item.key]) })] }), renderPlot([index], item)] }, item.key)) : renderPlot(series.map((_, index) => index))
67
+ : _jsx("div", { className: "chart-empty hatch", style: { minHeight: height }, children: "No collected samples in this window." }) }), legend === "sidebar" && legendContent] }), legend !== "sidebar" && legendContent, _jsxs("details", { className: "disclosure chart-values", children: [_jsx("summary", { children: "View exact values" }), _jsx("div", { className: "chart-table-scroll", children: _jsxs("table", { className: "chart-table", children: [_jsxs("caption", { children: [label, ": collected samples, timestamps in UTC"] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Timestamp" }), series.map(item => _jsx("th", { scope: "col", children: item.label }, item.key))] }) }), _jsx("tbody", { children: data.map(sample => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: formatChartTime(sample.timestamp) }), series.map(item => _jsx("td", { children: measured(sample.values[item.key]) ? String(sample.values[item.key]) : "Not collected" }, item.key))] }, sample.timestamp)) })] }) })] })] });
68
+ }
69
+ // Observe Recharts' synchronized tooltip state after render. This keeps the
70
+ // sidebar on the same timestamp for mouse, keyboard, touch, and sibling panels.
71
+ function ChartReadout({ active, timestamp, data, series, formatTime, displayValue, sidebar, onInspect }) {
72
+ const sample = active ? data.find(item => item.timestamp === timestamp) : undefined;
73
+ const inspectedTimestamp = sample?.timestamp ?? null;
74
+ React.useEffect(() => { onInspect?.(inspectedTimestamp); }, [onInspect, inspectedTimestamp]);
75
+ if (!sample || sidebar)
76
+ return null;
77
+ return _jsxs("div", { className: "chart-inspection", role: "status", children: [_jsx("strong", { children: formatTime(sample.timestamp) }), _jsx("dl", { children: series.map((item, index) => _jsxs(React.Fragment, { children: [_jsx("dt", { children: _jsx(ChartSeriesLabel, { series: item, index: index }) }), _jsx("dd", { children: displayValue(sample.values[item.key]) })] }, item.key)) })] });
78
+ }
@@ -0,0 +1,6 @@
1
+ import * as React from "react";
2
+ export declare function DashboardGrid({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
3
+ export type DashboardPanelProps = React.ComponentProps<"div"> & {
4
+ span?: 3 | 4 | 6 | 8 | 12;
5
+ };
6
+ export declare function DashboardPanel({ span, className, style, ...props }: DashboardPanelProps): React.JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { clsx as cn } from "clsx";
3
+ export function DashboardGrid({ className, ...props }) {
4
+ return _jsx("div", { ...props, "data-slot": "dashboard-grid", className: cn("dashboard-grid", className) });
5
+ }
6
+ export function DashboardPanel({ span = 6, className, style, ...props }) {
7
+ return _jsx("div", { ...props, "data-slot": "dashboard-panel", "data-span": span, className: cn("dashboard-panel", className), style: { "--dashboard-span": span, ...style } });
8
+ }
@@ -0,0 +1,5 @@
1
+ import * as React from "react";
2
+ export type DisclosureProps = Omit<React.ComponentProps<"details">, "title"> & {
3
+ summary: React.ReactNode;
4
+ };
5
+ export declare function Disclosure({ summary, className, children, ...props }: DisclosureProps): React.JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { clsx as cn } from "clsx";
3
+ export function Disclosure({ summary, className, children, ...props }) {
4
+ return _jsxs("details", { ...props, "data-slot": "disclosure", className: cn("disclosure", className), children: [_jsx("summary", { children: summary }), _jsx("div", { className: "disclosure-content", children: children })] });
5
+ }
package/dist/form.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+ type FormProps = ComponentProps<"form">;
3
+ declare function Form({ className, ...props }: FormProps): import("react").JSX.Element;
4
+ type FormActionsProps = ComponentProps<"div"> & {
5
+ sticky?: boolean;
6
+ tone?: "neutral" | "warning" | "danger";
7
+ message?: ReactNode;
8
+ };
9
+ declare function FormActions({ className, sticky, tone, message, children, ...props }: FormActionsProps): import("react").JSX.Element;
10
+ export { Form, FormActions };
11
+ export type { FormProps, FormActionsProps };
package/dist/form.js ADDED
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { clsx } from "clsx";
3
+ function Form({ className, ...props }) {
4
+ return _jsx("form", { "data-slot": "form", className: clsx("form", className), ...props });
5
+ }
6
+ function FormActions({ className, sticky = false, tone = "neutral", message, children, ...props }) {
7
+ return (_jsxs("div", { "data-slot": "form-actions", "data-sticky": sticky || undefined, "data-tone": tone, className: clsx("form-actions", className), ...props, children: [_jsx("div", { className: "form-actions-message", role: "status", "aria-atomic": "true", children: message }), _jsx("div", { className: "form-actions-buttons", children: children })] }));
8
+ }
9
+ export { Form, FormActions };
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./drawer.js";
15
15
  export * from "./dropdown-menu.js";
16
16
  export * from "./empty-state.js";
17
17
  export * from "./form-field.js";
18
+ export * from "./form.js";
18
19
  export * from "./header.js";
19
20
  export * from "./input.js";
20
21
  export * from "./kv.js";
@@ -40,6 +41,15 @@ export * from "./tabs.js";
40
41
  export * from "./terminal-icon.js";
41
42
  export * from "./textarea.js";
42
43
  export * from "./theme-selector.js";
44
+ export * from "./accent-selector.js";
43
45
  export * from "./toast.js";
44
46
  export * from "./tooltip.js";
45
47
  export * from "./validation-message.js";
48
+ export * from "./chart.js";
49
+ export * from "./chart-legend.js";
50
+ export * from "./meter.js";
51
+ export * from "./bar-gauge.js";
52
+ export * from "./disclosure.js";
53
+ export * from "./time-range-control.js";
54
+ export * from "./dashboard-grid.js";
55
+ export * from "./step-list.js";
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export * from "./drawer.js";
16
16
  export * from "./dropdown-menu.js";
17
17
  export * from "./empty-state.js";
18
18
  export * from "./form-field.js";
19
+ export * from "./form.js";
19
20
  export * from "./header.js";
20
21
  export * from "./input.js";
21
22
  export * from "./kv.js";
@@ -41,6 +42,15 @@ export * from "./tabs.js";
41
42
  export * from "./terminal-icon.js";
42
43
  export * from "./textarea.js";
43
44
  export * from "./theme-selector.js";
45
+ export * from "./accent-selector.js";
44
46
  export * from "./toast.js";
45
47
  export * from "./tooltip.js";
46
48
  export * from "./validation-message.js";
49
+ export * from "./chart.js";
50
+ export * from "./chart-legend.js";
51
+ export * from "./meter.js";
52
+ export * from "./bar-gauge.js";
53
+ export * from "./disclosure.js";
54
+ export * from "./time-range-control.js";
55
+ export * from "./dashboard-grid.js";
56
+ export * from "./step-list.js";
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ export type MeterProps = Omit<React.ComponentProps<"div">, "children"> & {
3
+ label: string;
4
+ value: number | null;
5
+ min?: number;
6
+ max?: number;
7
+ valueText?: string;
8
+ };
9
+ export declare function Meter({ label, value, min, max, valueText, className, ...props }: MeterProps): React.JSX.Element;
10
+ export type ProgressProps = Omit<MeterProps, "min" | "value"> & {
11
+ value?: number | null;
12
+ };
13
+ export declare function Progress({ label, value, max, valueText, className, ...props }: ProgressProps): React.JSX.Element;
package/dist/meter.js ADDED
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { clsx as cn } from "clsx";
3
+ export function Meter({ label, value, min = 0, max = 100, valueText, className, ...props }) {
4
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min)
5
+ throw new RangeError("Meter max must exceed min.");
6
+ const known = value !== null && Number.isFinite(value);
7
+ const bounded = known ? Math.min(max, Math.max(min, value)) : undefined;
8
+ const text = valueText ?? (known ? `${value} / ${max}` : "Not collected");
9
+ return _jsxs("div", { ...props, "data-slot": "meter", className: cn("meter", className), children: [_jsxs("div", { className: "meter-label", children: [_jsx("span", { children: label }), _jsx("span", { children: text })] }), _jsx("div", { role: known ? "meter" : undefined, "aria-label": label, "aria-valuemin": known ? min : undefined, "aria-valuemax": known ? max : undefined, "aria-valuenow": bounded, "aria-valuetext": known ? text : undefined, className: cn("meter-track", !known && "hatch"), children: bounded !== undefined && _jsx("span", { style: { width: `${(bounded - min) / (max - min) * 100}%` } }) })] });
10
+ }
11
+ export function Progress({ label, value, max = 100, valueText, className, ...props }) {
12
+ if (!Number.isFinite(max) || max <= 0)
13
+ throw new RangeError("Progress max must be positive.");
14
+ const known = value != null && Number.isFinite(value);
15
+ const text = valueText ?? (known ? `${value} / ${max}` : "In progress");
16
+ return _jsxs("div", { ...props, "data-slot": "progress", className: cn("meter", className), children: [_jsxs("div", { className: "meter-label", children: [_jsx("span", { children: label }), _jsx("span", { children: text })] }), _jsx("div", { role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuenow": known ? Math.min(max, Math.max(0, value)) : undefined, "aria-valuetext": text, className: cn("meter-track", !known && "hatch"), children: known && _jsx("span", { style: { width: `${Math.min(1, Math.max(0, value / max)) * 100}%` } }) })] });
17
+ }
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  export type SparklineProps = Omit<React.ComponentProps<"div">, "ref"> & {
3
- values: number[];
3
+ values: readonly (number | null)[];
4
4
  height?: number;
5
5
  tone?: "neutral" | "primary";
6
6
  fill?: boolean;
package/dist/sparkline.js CHANGED
@@ -5,15 +5,16 @@ import { clsx as cn } from "clsx";
5
5
  function Sparkline({ values, height = 24, tone = "neutral", fill = false, min, max, label, className, style, ref, ...props }) {
6
6
  // Below two samples there is no shape to show, and a flat rule across a
7
7
  // cell reads as a border rather than as a measurement.
8
- if (values.length < 2)
8
+ const known = values.filter((value) => value !== null && Number.isFinite(value));
9
+ if (known.length < 2)
9
10
  return null;
10
11
  // Default domain is the data's own extent; pass min and max to make
11
12
  // sibling sparklines share one scale, so rows compare honestly.
12
- let lo = min ?? Math.min(...values);
13
- let hi = max ?? Math.max(...values);
13
+ let lo = min ?? Math.min(...known);
14
+ let hi = max ?? Math.max(...known);
14
15
  const pad = lo === hi ? Math.max(1, Math.abs(lo) * 0.1) : 0;
15
16
  lo -= pad;
16
17
  hi += pad;
17
- return (_jsx("div", { ref: ref, "data-slot": "sparkline", "data-tone": tone, role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, className: cn("sparkline", tone === "primary" && "primary", className), style: { height, ...style }, ...props, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(AreaChart, { data: values.map((value) => ({ value })), margin: { top: 2, right: 0, bottom: 2, left: 0 }, children: [_jsx(YAxis, { hide: true, domain: [lo, hi] }), _jsx(Area, { type: "monotone", dataKey: "value", stroke: "currentColor", strokeWidth: 1.5, fill: fill ? "currentColor" : "none", fillOpacity: fill ? 0.12 : 0, isAnimationActive: false, dot: false, activeDot: false })] }) }) }));
18
+ return (_jsx("div", { ref: ref, "data-slot": "sparkline", "data-tone": tone, role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, className: cn("sparkline", tone === "primary" && "primary", className), style: { height, ...style }, ...props, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(AreaChart, { data: values.map((value) => ({ value: value !== null && Number.isFinite(value) ? value : null })), margin: { top: 2, right: 0, bottom: 2, left: 0 }, children: [_jsx(YAxis, { hide: true, domain: [lo, hi] }), _jsx(Area, { type: "monotone", connectNulls: false, dataKey: "value", stroke: "currentColor", strokeWidth: 1.5, fill: fill ? "currentColor" : "none", fillOpacity: fill ? 0.12 : 0, isAnimationActive: false, dot: false, activeDot: false })] }) }) }));
18
19
  }
19
20
  export { Sparkline };
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ export type StepState = "pending" | "running" | "done" | "skipped" | "failed";
3
+ export type StepItem = {
4
+ key: string;
5
+ label: string;
6
+ state: StepState;
7
+ meta?: string;
8
+ detail?: React.ReactNode;
9
+ };
10
+ export type StepListProps = Omit<React.ComponentProps<"ol">, "children" | "onSelect"> & {
11
+ label: string;
12
+ steps: readonly StepItem[];
13
+ selected?: string | null;
14
+ onSelect?: (key: string) => void;
15
+ };
16
+ export declare function StepList({ label, steps, selected, onSelect, className, ...props }: StepListProps): React.JSX.Element;
17
+ export type StepBarProps = Omit<React.ComponentProps<"div">, "children"> & {
18
+ label: string;
19
+ steps: readonly {
20
+ key: string;
21
+ label: string;
22
+ state: StepState;
23
+ progress?: number | null;
24
+ }[];
25
+ };
26
+ export declare function StepBar({ label, steps, className, ...props }: StepBarProps): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { clsx as cn } from "clsx";
4
+ const STATE_TEXT = {
5
+ pending: "Pending", running: "Running", done: "Done", skipped: "Skipped", failed: "Failed",
6
+ };
7
+ const DEFAULT_META = { running: "Running", skipped: "Not needed" };
8
+ /* The rail is the progress: each connector takes the state of the step below
9
+ it, so the line fills as the run advances. Nothing else measures it. */
10
+ export function StepList({ label, steps, selected, onSelect, className, ...props }) {
11
+ return _jsx("ol", { ...props, "aria-label": label, "data-slot": "step-list", className: cn("step-list", className), children: steps.map(step => {
12
+ const row = _jsxs(_Fragment, { children: [_jsx("span", { className: "step-label", children: step.label }), _jsx("span", { className: "step-meta", children: step.meta ?? DEFAULT_META[step.state] ?? "" })] });
13
+ return _jsxs("li", { "data-slot": "step", "data-state": step.state, className: "step", "aria-current": step.state === "running" ? "step" : undefined, children: [_jsx("span", { className: "step-rail", "aria-hidden": "true", children: _jsx("span", { className: "step-mark" }) }), _jsxs("div", { className: "step-body", children: [onSelect
14
+ ? _jsx("button", { type: "button", className: "step-row", "aria-pressed": selected === step.key, "aria-label": `${step.label}, ${STATE_TEXT[step.state].toLowerCase()}`, onClick: () => onSelect(step.key), children: row })
15
+ : _jsx("div", { className: "step-row", role: "img", "aria-label": `${step.label}, ${STATE_TEXT[step.state].toLowerCase()}`, children: row }), step.detail != null && _jsx("div", { className: "step-detail", children: step.detail })] })] }, step.key);
16
+ }) });
17
+ }
18
+ /* One segment per step, coloured by state. The product writes the count and
19
+ the current step beside it; the bar only names them for assistive tech. */
20
+ export function StepBar({ label, steps, className, ...props }) {
21
+ const failed = steps.findIndex(step => step.state === "failed");
22
+ const running = steps.findIndex(step => step.state === "running");
23
+ const at = failed >= 0 ? failed : running;
24
+ const finished = steps.length > 0 && steps.every(step => step.state === "done" || step.state === "skipped");
25
+ const name = at >= 0
26
+ ? `${label}: ${failed >= 0 ? "failed at" : "running"} step ${at + 1} of ${steps.length}, ${steps[at].label}`
27
+ : finished ? `${label}: all ${steps.length} steps done` : `${label}: not started`;
28
+ return _jsx("div", { ...props, role: "img", "aria-label": name, "data-slot": "step-bar", className: cn("step-bar", className), children: steps.map(step => {
29
+ const progress = step.state === "running" && step.progress != null && Number.isFinite(step.progress)
30
+ ? `${Math.min(1, Math.max(0, step.progress)) * 100}%` : undefined;
31
+ return _jsx("span", { "data-state": step.state, title: step.label, style: progress !== undefined ? { "--step-progress": progress } : undefined }, step.key);
32
+ }) });
33
+ }
package/dist/styles.css CHANGED
@@ -21,6 +21,69 @@
21
21
  color: var(--color-foreground);
22
22
  box-shadow: var(--shadow-xs);
23
23
  }
24
+ /* AccentSelector: a pill row that chooses, and one preview that answers.
25
+ Each pill and the preview carry their own data-accent, so their colours
26
+ resolve under that accent without any inline values. */
27
+ .accent-row { display: grid; gap: var(--spacing-sm); }
28
+ /* Only the variant with a preview is a block-level settings row and can be a
29
+ size container; inline-size containment would collapse the preview-less
30
+ variant to zero width inside a toolbar's flex row. */
31
+ .accent-row:has(.accent-preview) { container-type: inline-size; }
32
+ .accent-pills { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--spacing-xs); }
33
+ /* Narrow hosts (a sidebar footer) stack the label over a two-column grid of
34
+ pills instead of wrapping them ragged beside it. */
35
+ @container (width < 360px) {
36
+ .accent-row .theme-row { flex-direction: column; align-items: stretch; }
37
+ .accent-pills { display: grid; grid-template-columns: 1fr 1fr; }
38
+ .accent-pill { justify-content: center; }
39
+ }
40
+ .accent-pill {
41
+ display: inline-flex; align-items: center; gap: var(--spacing-xs);
42
+ height: var(--size-control-xs);
43
+ padding-inline: var(--spacing-xs) var(--spacing-sm);
44
+ border: 1px solid var(--color-border);
45
+ border-radius: var(--radius-full);
46
+ background: var(--color-card);
47
+ color: var(--color-muted-foreground);
48
+ font: inherit; font-size: var(--type-size-label);
49
+ cursor: pointer;
50
+ transition: background var(--motion-duration-fast) var(--motion-easing-standard),
51
+ border-color var(--motion-duration-fast) var(--motion-easing-standard);
52
+ }
53
+ .accent-pill:hover { background: var(--color-accent-surface); color: var(--color-foreground); }
54
+ .accent-pill[aria-pressed="true"] {
55
+ border-color: var(--color-primary);
56
+ background: var(--color-accent-surface);
57
+ color: var(--color-foreground);
58
+ font-weight: var(--type-weight-medium);
59
+ }
60
+ .accent-dot { width: var(--spacing-md); height: var(--spacing-md); border-radius: var(--radius-full); background: var(--color-primary); }
61
+
62
+ .accent-preview {
63
+ display: grid; grid-template-columns: 1fr 3fr;
64
+ height: 120px;
65
+ border: 1px solid var(--color-border);
66
+ border-radius: var(--radius-lg);
67
+ overflow: hidden;
68
+ background: var(--color-background);
69
+ }
70
+ .accent-preview i { display: block; border-radius: var(--radius-sm); }
71
+ .accent-preview-sidebar {
72
+ display: grid; align-content: start; gap: var(--spacing-sm);
73
+ padding: var(--spacing-md) var(--spacing-sm);
74
+ background: var(--color-sidebar);
75
+ border-inline-end: 1px solid var(--color-border);
76
+ }
77
+ .accent-preview-sidebar i { height: var(--spacing-xs); width: 70%; background: var(--color-border-strong); }
78
+ .accent-preview-sidebar i:nth-child(2) { width: 85%; background: var(--color-link); }
79
+ .accent-preview-main { display: grid; align-content: start; gap: var(--spacing-sm); padding: var(--spacing-md); }
80
+ .accent-preview-title { height: var(--spacing-sm); width: 55%; background: var(--color-foreground); opacity: .8; }
81
+ .accent-preview-text { height: var(--spacing-xs); width: 80%; background: var(--color-muted-foreground); }
82
+ .accent-preview-link { height: var(--spacing-xs); width: 40%; background: var(--color-link); }
83
+ .accent-preview-actions { display: flex; gap: var(--spacing-sm); }
84
+ .accent-preview-primary { height: var(--spacing-lg); width: 56px; background: var(--color-primary); }
85
+ .accent-preview-secondary { height: var(--spacing-lg); width: 44px; border: 1px solid var(--color-border-strong); background: var(--color-card); }
86
+ .accent-preview-selected { height: var(--spacing-md); background: var(--color-selected); }
24
87
 
25
88
  .kiso-checkbox {
26
89
  position: relative;
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ export type TimeRange = {
3
+ from: number;
4
+ to: number;
5
+ };
6
+ export type TimeRangeControlProps = {
7
+ value: TimeRange;
8
+ bounds: TimeRange;
9
+ onValueChange: (range: TimeRange) => void;
10
+ label?: string;
11
+ disabled?: boolean;
12
+ className?: string;
13
+ presets?: readonly {
14
+ label: string;
15
+ duration: number;
16
+ }[];
17
+ };
18
+ export declare function TimeRangeControl({ value, bounds, onValueChange, label, disabled, className, presets }: TimeRangeControlProps): React.JSX.Element;
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { clsx as cn } from "clsx";
5
+ import { Button } from "./button.js";
6
+ import { Form, FormActions } from "./form.js";
7
+ import { Input } from "./input.js";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
9
+ const defaultPresets = [
10
+ { label: "Last 5 minutes", duration: 300_000 },
11
+ { label: "Last 15 minutes", duration: 900_000 },
12
+ { label: "Last 30 minutes", duration: 1_800_000 },
13
+ ];
14
+ const dateInput = (value) => new Date(value).toISOString().slice(0, 23);
15
+ const shortRange = (range) => {
16
+ const from = dateInput(range.from);
17
+ const to = dateInput(range.to);
18
+ const sameDay = from.slice(0, 10) === to.slice(0, 10);
19
+ const compact = (date) => (sameDay ? date.slice(11) : date.replace("T", " ")).replace(/:00\.000$/, "").replace(/\.000$/, "");
20
+ return `${compact(from)} to ${compact(to)} UTC`;
21
+ };
22
+ const validRange = (range) => Number.isFinite(range.from) && Number.isFinite(range.to)
23
+ && Math.abs(range.from) <= 8.64e15 && Math.abs(range.to) <= 8.64e15 && range.from < range.to;
24
+ export function TimeRangeControl({ value, bounds, onValueChange, label = "Time range", disabled, className, presets = defaultPresets }) {
25
+ const [open, setOpen] = React.useState(false);
26
+ const [custom, setCustom] = React.useState(false);
27
+ if (!validRange(bounds) || !validRange(value) || value.from < bounds.from || value.to > bounds.to) {
28
+ throw new RangeError("Time range must be ordered and contained within the collection bounds.");
29
+ }
30
+ if (presets.some(preset => !Number.isFinite(preset.duration) || preset.duration <= 0)) {
31
+ throw new RangeError("Time range preset durations must be positive milliseconds.");
32
+ }
33
+ const apply = (range) => { onValueChange(range); setOpen(false); };
34
+ return _jsxs(Popover, { open: open, onOpenChange: next => { setOpen(next); setCustom(false); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { disabled: disabled, className: cn("time-range-trigger", className), "aria-label": `${label}: ${dateInput(value.from).replace("T", " ")} to ${dateInput(value.to).replace("T", " ")} UTC`, children: [shortRange(value), " ", _jsx("span", { "aria-hidden": "true", children: "\u2304" })] }) }), _jsx(PopoverContent, { className: "time-range-content", "aria-label": label, children: custom ? _jsxs("div", { className: "stack-sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setCustom(false), children: "Back to presets" }), _jsx(TimeRangeForm, { value: value, bounds: bounds, onApply: apply }, `${value.from}-${value.to}-${open}`)] }) : _jsxs("div", { className: "time-range-presets", children: [_jsxs(Button, { variant: "ghost", onClick: () => apply(bounds), children: [_jsx("span", { children: "Entire run" }), _jsx("small", { children: shortRange(bounds) })] }), presets.map(preset => {
35
+ const range = { from: Math.max(bounds.from, bounds.to - preset.duration), to: bounds.to };
36
+ return _jsxs(Button, { variant: "ghost", onClick: () => apply(range), children: [_jsx("span", { children: preset.label }), _jsx("small", { children: shortRange(range) })] }, preset.label);
37
+ }), _jsx(Button, { variant: "ghost", onClick: () => setCustom(true), children: "Custom range" })] }) })] });
38
+ }
39
+ function TimeRangeForm({ value, bounds, onApply }) {
40
+ const id = React.useId();
41
+ const [from, setFrom] = React.useState(dateInput(value.from));
42
+ const [to, setTo] = React.useState(dateInput(value.to));
43
+ const [error, setError] = React.useState("");
44
+ return _jsxs(Form, { className: "stack-sm", onSubmit: event => {
45
+ event.preventDefault();
46
+ const range = { from: Date.parse(`${from}Z`), to: Date.parse(`${to}Z`) };
47
+ if (!validRange(range) || range.from < bounds.from || range.to > bounds.to) {
48
+ setError("Choose a start before the end, within the collection window.");
49
+ return;
50
+ }
51
+ onApply(range);
52
+ }, children: [_jsx("label", { htmlFor: `${id}-from`, children: "From (UTC)" }), _jsx(Input, { id: `${id}-from`, type: "datetime-local", step: "0.001", required: true, value: from, min: dateInput(bounds.from), max: dateInput(bounds.to), onChange: event => { setFrom(event.target.value); setError(""); }, "aria-invalid": !!error, "aria-describedby": error ? `${id}-error` : undefined }), _jsx("label", { htmlFor: `${id}-to`, children: "To (UTC)" }), _jsx(Input, { id: `${id}-to`, type: "datetime-local", step: "0.001", required: true, value: to, min: dateInput(bounds.from), max: dateInput(bounds.to), onChange: event => { setTo(event.target.value); setError(""); }, "aria-invalid": !!error, "aria-describedby": error ? `${id}-error` : undefined }), error && _jsx("p", { id: `${id}-error`, role: "alert", className: "field-error", children: error }), _jsx(FormActions, { children: _jsx(Button, { type: "submit", variant: "primary", children: "Apply range" }) })] });
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momoi-labs/kiso-react",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Kiso React components built with shadcn/ui and Radix",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "prepack": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@momoi-labs/kiso": "^0.9.0",
35
+ "@momoi-labs/kiso": "^0.11.0",
36
36
  "class-variance-authority": "0.7.1",
37
37
  "clsx": "2.1.1",
38
38
  "radix-ui": "1.6.7",