@cosmicdrift/kumiko-renderer-web 0.334.0 → 0.336.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app-schema-boot.d.ts +2 -0
- package/dist/app/app-schema-boot.js +13 -4
- package/dist/app/create-app.js +28 -24
- package/dist/app/dashboard-body.js +51 -16
- package/dist/layout/nav-tree.js +1 -1
- package/dist/layout/shell-header.js +9 -3
- package/dist/layout/theme-label.d.ts +9 -0
- package/dist/layout/theme-label.js +11 -0
- package/dist/layout/theme-menu-item.js +2 -7
- package/dist/layout/theme-toggle.js +2 -8
- package/dist/primitives/combobox.js +1 -1
- package/dist/primitives/embedded-list-input.js +1 -1
- package/dist/primitives/index.js +104 -44
- package/dist/primitives/layout.d.ts +1 -0
- package/dist/primitives/layout.js +7 -0
- package/dist/primitives/metric.d.ts +1 -0
- package/dist/primitives/metric.js +6 -1
- package/dist/primitives/narrow-pinned-footer.d.ts +10 -0
- package/dist/primitives/narrow-pinned-footer.js +90 -0
- package/dist/primitives/number-input.d.ts +2 -1
- package/dist/primitives/number-input.js +2 -2
- package/dist/primitives/use-narrow-viewport.d.ts +1 -0
- package/dist/primitives/use-narrow-viewport.js +20 -8
- package/dist/widgets/charts.js +2 -1
- package/dist/widgets/dashboard-list.d.ts +3 -2
- package/dist/widgets/dashboard-list.js +9 -3
- package/dist/widgets/drawer.js +6 -19
- package/dist/widgets/step-bar.d.ts +10 -4
- package/dist/widgets/step-bar.js +40 -15
- package/package.json +7 -7
- package/src/changes.json +79 -0
package/dist/widgets/charts.js
CHANGED
|
@@ -31,7 +31,8 @@ export function StatusBarChart({ entries, ariaLabel, startLabel, endLabel, highl
|
|
|
31
31
|
return (_jsxs("div", { "data-testid": testId, children: [_jsxs("svg", { viewBox: `0 0 ${totalWidth} ${chartHeight}`, preserveAspectRatio: dense ? undefined : "none", width: dense ? totalWidth : undefined, height: dense ? chartHeight : undefined, className: dense ? "block" : "block h-9 w-full", role: "img", "aria-label": ariaLabel, children: [_jsx("title", { children: ariaLabel }), entries.map((entry, idx) => {
|
|
32
32
|
const x = idx * (barWidth + barGap);
|
|
33
33
|
const level = Math.max(0, Math.min(1, entry.level));
|
|
34
|
-
|
|
34
|
+
// Dense has no tick rect, so level 0 would otherwise render nothing and lose its tooltip.
|
|
35
|
+
const barHeight = Math.max((chartHeight - tickHeight) * level, dense ? 1 : 0);
|
|
35
36
|
const barY = chartHeight - barHeight;
|
|
36
37
|
const isLast = highlightLast && idx === lastIdx;
|
|
37
38
|
const color = TONE_VAR[entry.tone];
|
|
@@ -3,17 +3,18 @@ export type DashboardListColumn = {
|
|
|
3
3
|
readonly field: string;
|
|
4
4
|
/** Translated by the caller. */
|
|
5
5
|
readonly label: string;
|
|
6
|
-
readonly display?: "bar" | "badge";
|
|
6
|
+
readonly display?: "bar" | "badge" | "datetime";
|
|
7
7
|
readonly badgeToneField?: string;
|
|
8
8
|
};
|
|
9
9
|
type Row = Readonly<Record<string, unknown>>;
|
|
10
10
|
/** Compact 40px-row table for dashboard lists with optional bar/badge
|
|
11
11
|
* columns. */
|
|
12
|
-
export declare function DashboardListTable({ columns, rows, formatPercent, formatNumber, translate, testId, }: {
|
|
12
|
+
export declare function DashboardListTable({ columns, rows, formatPercent, formatNumber, formatDateTime, translate, testId, }: {
|
|
13
13
|
readonly columns: readonly DashboardListColumn[];
|
|
14
14
|
readonly rows: readonly Row[];
|
|
15
15
|
readonly formatPercent: (fraction: number) => string;
|
|
16
16
|
readonly formatNumber: (value: number) => string;
|
|
17
|
+
readonly formatDateTime: (atMs: number) => string;
|
|
17
18
|
readonly translate: (key: string) => string;
|
|
18
19
|
readonly testId?: string;
|
|
19
20
|
}): ReactNode;
|
|
@@ -11,7 +11,7 @@ function fractionOf(raw) {
|
|
|
11
11
|
}
|
|
12
12
|
// Rows may carry i18n keys (quota labels, failure reasons); translate falls back to the key itself.
|
|
13
13
|
const I18N_KEY_SHAPE = /^[a-z][\w-]*(?:[:.][\w-]+)+$/i;
|
|
14
|
-
function Cell({ column, row, formatPercent, formatNumber, translate, }) {
|
|
14
|
+
function Cell({ column, row, formatPercent, formatNumber, formatDateTime, translate, }) {
|
|
15
15
|
const raw = row[column.field];
|
|
16
16
|
if (column.display === "bar") {
|
|
17
17
|
const fraction = fractionOf(raw);
|
|
@@ -24,6 +24,12 @@ function Cell({ column, row, formatPercent, formatNumber, translate, }) {
|
|
|
24
24
|
const tone = (typeof toneKey === "string" ? BADGE_TONE[toneKey] : undefined) ?? "muted";
|
|
25
25
|
return _jsx(StatusBadge, { tone: tone, children: String(raw) });
|
|
26
26
|
}
|
|
27
|
+
if (column.display === "datetime") {
|
|
28
|
+
const atMs = typeof raw === "number" ? raw : typeof raw === "string" ? Date.parse(raw) : Number.NaN;
|
|
29
|
+
if (!Number.isFinite(atMs))
|
|
30
|
+
return "—";
|
|
31
|
+
return _jsx("span", { className: "tabular-nums", children: formatDateTime(atMs) });
|
|
32
|
+
}
|
|
27
33
|
if (typeof raw === "number")
|
|
28
34
|
return _jsx("span", { className: "tabular-nums", children: formatNumber(raw) });
|
|
29
35
|
if (raw === undefined || raw === null || raw === "")
|
|
@@ -33,6 +39,6 @@ function Cell({ column, row, formatPercent, formatNumber, translate, }) {
|
|
|
33
39
|
}
|
|
34
40
|
/** Compact 40px-row table for dashboard lists with optional bar/badge
|
|
35
41
|
* columns. */
|
|
36
|
-
export function DashboardListTable({ columns, rows, formatPercent, formatNumber, translate, testId, }) {
|
|
37
|
-
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { "data-testid": testId, className: "w-full text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "h-9 border-b border-border-row text-left text-xs text-muted-foreground", children: columns.map((column) => (_jsx("th", { scope: "col", className: "px-4 font-medium", children: column.label }, column.field))) }) }), _jsx("tbody", { children: rows.map((row, index) => (_jsx("tr", { className: "h-10 border-b border-border-row last:border-b-0", children: columns.map((column) => (_jsx("td", { className: "px-4", children: _jsx(Cell, { column: column, row: row, formatPercent: formatPercent, formatNumber: formatNumber, translate: translate }) }, column.field))) }, typeof row["id"] === "string" ? row["id"] : index))) })] }) }));
|
|
42
|
+
export function DashboardListTable({ columns, rows, formatPercent, formatNumber, formatDateTime, translate, testId, }) {
|
|
43
|
+
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { "data-testid": testId, className: "w-full text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "h-9 border-b border-border-row text-left text-xs text-muted-foreground", children: columns.map((column) => (_jsx("th", { scope: "col", className: "px-4 font-medium", children: column.label }, column.field))) }) }), _jsx("tbody", { children: rows.map((row, index) => (_jsx("tr", { className: "h-10 border-b border-border-row last:border-b-0", children: columns.map((column) => (_jsx("td", { className: "px-4", children: _jsx(Cell, { column: column, row: row, formatPercent: formatPercent, formatNumber: formatNumber, formatDateTime: formatDateTime, translate: translate }) }, column.field))) }, typeof row["id"] === "string" ? row["id"] : index))) })] }) }));
|
|
38
44
|
}
|
package/dist/widgets/drawer.js
CHANGED
|
@@ -41,6 +41,8 @@ function normalizeWidth(width) {
|
|
|
41
41
|
// Must match the static `max(600px,37.5vw)` in sidePanelClass — Tailwind's
|
|
42
42
|
// JIT can't read DEFAULT_WIDTH_MIN_PX/DEFAULT_WIDTH_VIEWPORT_RATIO at
|
|
43
43
|
// runtime, so the two are kept in sync by hand.
|
|
44
|
+
// Same cap as WIDTH_CLASS: an explicit `width` must not outgrow the viewport.
|
|
45
|
+
const MAX_VIEWPORT_WIDTH = "85vw";
|
|
44
46
|
const WIDTH_CLASS = "w-[max(600px,37.5vw)] max-w-[85vw] sm:max-w-[max(600px,37.5vw)]";
|
|
45
47
|
// "floating": 32px margin + 32px radius so the panel reads as detached from
|
|
46
48
|
// the viewport edge, unlike the sheet primitive's flush-edge default.
|
|
@@ -54,18 +56,7 @@ function sidePanelClass(side, narrow, variant, belowHeader) {
|
|
|
54
56
|
return "inset-0 h-full w-full max-w-none rounded-none border-0 overflow-hidden";
|
|
55
57
|
if (variant === "flush") {
|
|
56
58
|
switch (side) {
|
|
57
|
-
//
|
|
58
|
-
// set — DrawerSheetContent's own base className already carries
|
|
59
|
-
// `inset-y-0` for these sides, and tailwind-merge (v3.6, checked
|
|
60
|
-
// directly) does NOT dedupe `top-*`/`bottom-*` against it (unlike
|
|
61
|
-
// same-group `top-0` vs `top-(--x)`, which it does). Overriding top
|
|
62
|
-
// via inline `style` instead (Drawer's `verticalOffsetStyle`) always
|
|
63
|
-
// wins over both, without depending on that gap.
|
|
64
|
-
// `h-full` fights that same style override on height: it pins the
|
|
65
|
-
// panel to 100% of the viewport regardless of `top`, pushing the
|
|
66
|
-
// bottom edge (and the footer slot) past the viewport when belowHeader
|
|
67
|
-
// also shifts `top` down. `h-auto` lets `inset-y-0`'s `bottom-0` (kept
|
|
68
|
-
// in sync by the inline `bottom: 0`) determine the height instead.
|
|
59
|
+
// h-full would pin height to 100vh despite the shifted top; h-auto lets inset-y-0 size it.
|
|
69
60
|
case "left":
|
|
70
61
|
return `inset-y-0 left-0 ${belowHeader ? "h-auto" : "h-full"} ${WIDTH_CLASS} border-r shadow-2xl overflow-hidden`;
|
|
71
62
|
case "top":
|
|
@@ -95,7 +86,7 @@ export function Drawer({ open, onOpenChange, side = "right", title, description,
|
|
|
95
86
|
const narrow = useIsNarrowViewport();
|
|
96
87
|
const canResize = resize !== undefined && (side === "left" || side === "right");
|
|
97
88
|
const customWidthStyle = !canResize && !narrow && (side === "left" || side === "right") && width !== undefined
|
|
98
|
-
? { width: normalizeWidth(width), maxWidth:
|
|
89
|
+
? { width: normalizeWidth(width), maxWidth: MAX_VIEWPORT_WIDTH }
|
|
99
90
|
: undefined;
|
|
100
91
|
const minWidthPx = resize?.minWidthPx ?? MIN_WIDTH_PX;
|
|
101
92
|
const maxWidthPx = resize?.maxWidthPx ?? MAX_WIDTH_PX;
|
|
@@ -111,12 +102,8 @@ export function Drawer({ open, onOpenChange, side = "right", title, description,
|
|
|
111
102
|
...(blurPx > 0 ? { backdropFilter: `blur(${blurPx}px)` } : {}),
|
|
112
103
|
};
|
|
113
104
|
const effectiveWidthPx = maximized ? effectiveMaxWidthPx() : resizedWidthPx;
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// (verified directly against the pinned tailwind-merge), so a class-only
|
|
117
|
-
// override would leave both `inset-y-0` and the offset in the className,
|
|
118
|
-
// with the winner then depending on Tailwind's generated CSS order. style
|
|
119
|
-
// always wins over a class for the same property, no such dependency.
|
|
105
|
+
// Inline style because tailwind-merge doesn't dedupe `top-*`/`bottom-*` against the base `inset-y-0`;
|
|
106
|
+
// style wins over class regardless of generated CSS order.
|
|
120
107
|
const verticalOffsetStyle = belowHeader && variant === "flush" && !narrow && (side === "left" || side === "right")
|
|
121
108
|
? { top: "var(--shell-header-height)", bottom: 0 }
|
|
122
109
|
: undefined;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
/** Wizard step overview — numbered chips with a connector line between
|
|
3
|
-
* them. With `onStepSelect`, done chips render as buttons for jumping back
|
|
4
|
-
* current
|
|
3
|
+
* them. With `onStepSelect`, done chips render as buttons for jumping back
|
|
4
|
+
* (every non-current chip with `selectableSteps="all"`); the current chip
|
|
5
|
+
* never does. `doneSteps` overrides the position-based done state. With
|
|
6
|
+
* `onStepSelect` and `selectableSteps="all"` the narrow-viewport label becomes
|
|
7
|
+
* a dropdown listing every step (same rows as the rail), so phones can jump
|
|
8
|
+
* too. Three visual states, none conveyed by color alone:
|
|
5
9
|
* done (checkmark replaces the number, `aria-current` absent, a sr-only
|
|
6
10
|
* label says so since the number itself is gone), current (`aria-current
|
|
7
11
|
* ="step"`, own background), upcoming (dimmed, number visible). Below
|
|
@@ -9,11 +13,13 @@ import type { ReactNode } from "react";
|
|
|
9
13
|
* don't fit on a phone) — both live in the DOM, Tailwind's
|
|
10
14
|
* `hidden`/`sm:hidden` pair picks the visible one per viewport (same
|
|
11
15
|
* pattern as embedded-list-input.tsx's desktop/mobile split). */
|
|
12
|
-
export declare function StepBar({ steps, currentIndex, compactLabel, onStepSelect, narrowLayout, orientation, heading, description, subtitles, upNext, testId, compactTestId, }: {
|
|
16
|
+
export declare function StepBar({ steps, currentIndex, compactLabel, onStepSelect, doneSteps, selectableSteps, narrowLayout, orientation, heading, description, subtitles, upNext, testId, compactTestId, }: {
|
|
13
17
|
readonly steps: readonly string[];
|
|
14
18
|
readonly currentIndex: number;
|
|
15
19
|
readonly compactLabel: string;
|
|
16
20
|
readonly onStepSelect?: (index: number) => void;
|
|
21
|
+
readonly doneSteps?: readonly boolean[];
|
|
22
|
+
readonly selectableSteps?: "done" | "all";
|
|
17
23
|
readonly narrowLayout?: "label" | "steps";
|
|
18
24
|
readonly orientation?: "horizontal" | "vertical";
|
|
19
25
|
readonly heading?: string;
|
package/dist/widgets/step-bar.js
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
-
import { Check } from "lucide-react";
|
|
3
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
4
|
+
import { useId, useRef, useState } from "react";
|
|
4
5
|
import { cn } from "../lib/cn.js";
|
|
5
6
|
const CHIP_CLASS = "flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-colors";
|
|
6
7
|
function chipTestId(testId, index) {
|
|
7
8
|
return testId !== undefined ? `${testId}-step-${index}` : undefined;
|
|
8
9
|
}
|
|
10
|
+
function StepRailRow({ index, label, subtitle, subtitleTestId, rowTestId, state: { isCurrent, isDone, isSelectable }, onSelect, doneLabel, minHeightClass = "min-h-9", }) {
|
|
11
|
+
const rowClass = cn("flex w-full items-center gap-2.5 rounded-md px-2 py-1 text-left text-sm", minHeightClass, isCurrent && "bg-primary/10 font-semibold text-primary", isDone && "text-foreground", !isCurrent && !isDone && "text-foreground-secondary");
|
|
12
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: cn("flex size-[22px] shrink-0 items-center justify-center rounded-full border text-xs font-semibold", isCurrent && "border-primary bg-primary text-primary-foreground", isDone && "border-status-ok-surface bg-status-ok-surface text-status-ok", !isCurrent && !isDone && "border-input bg-card text-foreground-secondary"), children: isDone ? _jsx(Check, { className: "size-3" }) : index + 1 }), isDone && _jsx("span", { className: "sr-only", children: doneLabel }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: label }), subtitle !== undefined && (_jsx("span", { "data-testid": subtitleTestId, className: "truncate text-xs font-normal text-foreground-secondary", children: subtitle }))] })] }));
|
|
13
|
+
return isSelectable ? (_jsx("button", { type: "button", onClick: () => onSelect?.(index), "data-testid": rowTestId, className: cn(rowClass, "hover:bg-muted-foreground/10"), children: content })) : (_jsx("span", { "aria-current": isCurrent ? "step" : undefined, "data-testid": rowTestId, className: rowClass, children: content }));
|
|
14
|
+
}
|
|
15
|
+
function CompactStepPicker({ steps, compactLabel, subtitles, stepState, onStepSelect, compactTestId, doneLabel, }) {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
const listId = useId();
|
|
18
|
+
const toggleRef = useRef(null);
|
|
19
|
+
return (_jsxs("div", { onKeyDown: (event) => {
|
|
20
|
+
if (event.key === "Escape" && open) {
|
|
21
|
+
setOpen(false);
|
|
22
|
+
toggleRef.current?.focus();
|
|
23
|
+
}
|
|
24
|
+
}, children: [_jsxs("button", { ref: toggleRef, type: "button", "aria-expanded": open, "aria-controls": listId, "data-testid": compactTestId, onClick: () => setOpen((wasOpen) => !wasOpen), className: "flex min-h-11 w-full items-center justify-between gap-2 rounded-md border border-border bg-card px-3 text-left text-sm font-medium text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [compactLabel, _jsx(ChevronDown, { "aria-hidden": "true", className: cn("size-4 shrink-0 transition-transform", open && "rotate-180") })] }), open && (_jsx("ol", { id: listId, className: "m-0 mt-1 flex list-none flex-col gap-0.5 rounded-md border border-border bg-card p-1", children: steps.map((label, i) => (_jsx("li", { children: _jsx(StepRailRow, { index: i, label: label, subtitle: subtitles?.[i], subtitleTestId: undefined, rowTestId: chipTestId(compactTestId, i), state: stepState(i), onSelect: (index) => {
|
|
25
|
+
setOpen(false);
|
|
26
|
+
onStepSelect(index);
|
|
27
|
+
}, doneLabel: doneLabel, minHeightClass: "min-h-11" }) }, `${i}-${label}`))) }))] }));
|
|
28
|
+
}
|
|
9
29
|
/** Wizard step overview — numbered chips with a connector line between
|
|
10
|
-
* them. With `onStepSelect`, done chips render as buttons for jumping back
|
|
11
|
-
* current
|
|
30
|
+
* them. With `onStepSelect`, done chips render as buttons for jumping back
|
|
31
|
+
* (every non-current chip with `selectableSteps="all"`); the current chip
|
|
32
|
+
* never does. `doneSteps` overrides the position-based done state. With
|
|
33
|
+
* `onStepSelect` and `selectableSteps="all"` the narrow-viewport label becomes
|
|
34
|
+
* a dropdown listing every step (same rows as the rail), so phones can jump
|
|
35
|
+
* too. Three visual states, none conveyed by color alone:
|
|
12
36
|
* done (checkmark replaces the number, `aria-current` absent, a sr-only
|
|
13
37
|
* label says so since the number itself is gone), current (`aria-current
|
|
14
38
|
* ="step"`, own background), upcoming (dimmed, number visible). Below
|
|
@@ -16,20 +40,21 @@ function chipTestId(testId, index) {
|
|
|
16
40
|
* don't fit on a phone) — both live in the DOM, Tailwind's
|
|
17
41
|
* `hidden`/`sm:hidden` pair picks the visible one per viewport (same
|
|
18
42
|
* pattern as embedded-list-input.tsx's desktop/mobile split). */
|
|
19
|
-
export function StepBar({ steps, currentIndex, compactLabel, onStepSelect, narrowLayout = "label", orientation = "horizontal", heading, description, subtitles, upNext, testId, compactTestId, }) {
|
|
43
|
+
export function StepBar({ steps, currentIndex, compactLabel, onStepSelect, doneSteps, selectableSteps = "done", narrowLayout = "label", orientation = "horizontal", heading, description, subtitles, upNext, testId, compactTestId, }) {
|
|
20
44
|
const t = useTranslation();
|
|
45
|
+
const stepState = (i) => {
|
|
46
|
+
const isCurrent = i === currentIndex;
|
|
47
|
+
const isDone = !isCurrent && (doneSteps !== undefined ? doneSteps[i] === true : i < currentIndex);
|
|
48
|
+
const isSelectable = onStepSelect !== undefined && !isCurrent && (selectableSteps === "all" || isDone);
|
|
49
|
+
return { isCurrent, isDone, isSelectable };
|
|
50
|
+
};
|
|
51
|
+
const compactPicker = onStepSelect !== undefined && selectableSteps === "all";
|
|
52
|
+
const pickerElement = compactPicker ? (_jsx(CompactStepPicker, { steps: steps, compactLabel: compactLabel, subtitles: subtitles, stepState: stepState, onStepSelect: onStepSelect, compactTestId: compactTestId, doneLabel: t("kumiko.widget.step-bar.done") })) : null;
|
|
21
53
|
if (orientation === "vertical") {
|
|
22
|
-
return (_jsxs(_Fragment, { children: [_jsxs("nav", { className: "hidden w-[280px] shrink-0 flex-col gap-4 overflow-y-auto border-r border-border bg-muted px-6 py-7 lg:flex", children: [heading !== undefined && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-sm font-semibold text-foreground", children: heading }), description !== undefined && (_jsx("span", { className: "text-[13px] text-foreground-secondary", children: description }))] })), _jsx("ol", { "data-testid": testId, className: "m-0 flex list-none flex-col gap-0.5 p-0", children: steps.map((label, i) => {
|
|
23
|
-
const isDone = i < currentIndex;
|
|
24
|
-
const isCurrent = i === currentIndex;
|
|
25
|
-
const rowClass = cn("flex min-h-9 w-full items-center gap-2.5 rounded-md px-2 py-1 text-left text-sm", isCurrent && "bg-primary/10 font-semibold text-primary", isDone && "text-foreground", !isCurrent && !isDone && "text-foreground-secondary");
|
|
26
|
-
const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: cn("flex size-[22px] shrink-0 items-center justify-center rounded-full border text-xs font-semibold", isCurrent && "border-primary bg-primary text-primary-foreground", isDone && "border-status-ok-surface bg-status-ok-surface text-status-ok", !isCurrent && !isDone && "border-input bg-card text-foreground-secondary"), children: isDone ? _jsx(Check, { className: "size-3" }) : i + 1 }), isDone && _jsx("span", { className: "sr-only", children: t("kumiko.widget.step-bar.done") }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: label }), subtitles?.[i] !== undefined && (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-subtitle-${i}` : undefined, className: "truncate text-xs font-normal text-foreground-secondary", children: subtitles[i] }))] })] }));
|
|
27
|
-
return (_jsx("li", { children: isDone && onStepSelect !== undefined ? (_jsx("button", { type: "button", onClick: () => onStepSelect(i), "data-testid": chipTestId(testId, i), className: cn(rowClass, "hover:bg-muted-foreground/10"), children: content })) : (_jsx("span", { "aria-current": isCurrent ? "step" : undefined, "data-testid": chipTestId(testId, i), className: rowClass, children: content })) }, `${i}-${label}`));
|
|
28
|
-
}) }), upNext !== undefined && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-up-next` : undefined, className: "mt-2 flex flex-col gap-1 rounded-md border border-border bg-card px-3 py-2.5", children: [_jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: upNext.heading }), _jsx("span", { className: "text-sm font-medium text-foreground", children: upNext.title }), upNext.subtitle !== undefined && (_jsx("span", { className: "text-xs text-foreground-secondary", children: upNext.subtitle }))] }))] }), _jsx("p", { "data-testid": compactTestId, className: "px-4 pt-3 text-sm text-muted-foreground lg:hidden", children: compactLabel })] }));
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsxs("nav", { className: "hidden w-[280px] shrink-0 flex-col gap-4 overflow-y-auto border-r border-border bg-muted px-6 py-7 lg:flex", children: [heading !== undefined && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-sm font-semibold text-foreground", children: heading }), description !== undefined && (_jsx("span", { className: "text-[13px] text-foreground-secondary", children: description }))] })), _jsx("ol", { "data-testid": testId, className: "m-0 flex list-none flex-col gap-0.5 p-0", children: steps.map((label, i) => (_jsx("li", { children: _jsx(StepRailRow, { index: i, label: label, subtitle: subtitles?.[i], subtitleTestId: testId !== undefined ? `${testId}-subtitle-${i}` : undefined, rowTestId: chipTestId(testId, i), state: stepState(i), onSelect: onStepSelect, doneLabel: t("kumiko.widget.step-bar.done") }) }, `${i}-${label}`))) }), upNext !== undefined && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-up-next` : undefined, className: "mt-2 flex flex-col gap-1 rounded-md border border-border bg-card px-3 py-2.5", children: [_jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: upNext.heading }), _jsx("span", { className: "text-sm font-medium text-foreground", children: upNext.title }), upNext.subtitle !== undefined && (_jsx("span", { className: "text-xs text-foreground-secondary", children: upNext.subtitle }))] }))] }), compactPicker ? (_jsx("div", { className: "px-4 pt-3 lg:hidden", children: pickerElement })) : (_jsx("p", { "data-testid": compactTestId, className: "px-4 pt-3 text-sm text-muted-foreground lg:hidden", children: compactLabel }))] }));
|
|
29
55
|
}
|
|
30
56
|
return (_jsxs(_Fragment, { children: [_jsx("ol", { "data-testid": testId, className: cn("items-center gap-2", narrowLayout === "steps" ? "flex flex-wrap" : "hidden sm:flex sm:flex-wrap"), children: steps.map((label, i) => {
|
|
31
|
-
const isDone = i
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}) }), _jsx("p", { "data-testid": compactTestId, className: cn("text-sm text-muted-foreground", narrowLayout === "steps" ? "sr-only" : "sm:hidden"), children: compactLabel })] }));
|
|
57
|
+
const { isCurrent, isDone, isSelectable } = stepState(i);
|
|
58
|
+
return (_jsxs("li", { className: "flex items-center gap-2", children: [i > 0 && _jsx("span", { "aria-hidden": "true", className: "h-px w-4 bg-border" }), isSelectable ? (_jsxs("button", { type: "button", onClick: () => onStepSelect?.(i), "data-testid": chipTestId(testId, i), className: cn(CHIP_CLASS, isDone ? "text-primary" : "text-muted-foreground", "max-sm:min-h-11 hover:bg-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), children: [isDone ? (_jsxs(_Fragment, { children: [_jsx(Check, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: t("kumiko.widget.step-bar.done") })] })) : (_jsx("span", { className: "text-xs font-semibold", children: i + 1 })), label] })) : (_jsxs("span", { "aria-current": isCurrent ? "step" : undefined, "data-testid": chipTestId(testId, i), className: cn(CHIP_CLASS, isCurrent && "bg-primary font-semibold text-primary-foreground", isDone && "text-primary", !isCurrent && !isDone && "text-muted-foreground"), children: [isDone ? (_jsxs(_Fragment, { children: [_jsx(Check, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: t("kumiko.widget.step-bar.done") })] })) : (_jsx("span", { className: "text-xs font-semibold", children: i + 1 })), label] }))] }, `${i}-${label}`));
|
|
59
|
+
}) }), compactPicker ? (_jsx("div", { className: narrowLayout === "steps" ? "hidden" : "sm:hidden", children: pickerElement })) : (_jsx("p", { "data-testid": compactTestId, className: cn("text-sm text-muted-foreground", narrowLayout === "steps" ? "hidden" : "sm:hidden"), children: compactLabel }))] }));
|
|
35
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.336.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
24
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
25
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
26
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
27
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
23
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.336.0",
|
|
24
|
+
"@cosmicdrift/kumiko-framework": "0.336.0",
|
|
25
|
+
"@cosmicdrift/kumiko-headless": "0.336.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer": "0.336.0",
|
|
27
|
+
"@cosmicdrift/kumiko-types": "0.336.0",
|
|
28
28
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
29
29
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
30
30
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@types/react-dom": "^19.2.3",
|
|
73
73
|
"jsdom": "^29.1.1",
|
|
74
74
|
"tailwindcss": "^4.3.0",
|
|
75
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
75
|
+
"@cosmicdrift/kumiko-locale-de": "0.336.0"
|
|
76
76
|
},
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.336.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "EventSource live events parse the signal-only frame",
|
|
6
|
+
"detail": "`createEventSourceLiveEvents` forwards the new frame shape without payload and with `eventType`.",
|
|
7
|
+
"migration": "No code change needed."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.336.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Update-mode wizards get a step picker on narrow viewports; the pinned footer is a single fixed row on phones",
|
|
13
|
+
"detail": "`StepBar` with `onStepSelect` and `selectableSteps=\"all\"` renders the compact label as a dropdown listing every step. Below `sm` the pinned footer is one row: Back as icon, other actions in a \"…\" popover that only shows while one of them is enabled, primary action filling the rest. New renderer markers `FOOTER_ACTION_ROLE_PROP` and `NARROW_LABEL_PROP` classify custom footer buttons.",
|
|
14
|
+
"migration": "No code change needed."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.335.0",
|
|
18
|
+
"type": "improvement",
|
|
19
|
+
"title": "Combobox options carry a value-based data-testid"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.335.0",
|
|
23
|
+
"type": "fix",
|
|
24
|
+
"title": "Header-card detail screens default to a 4xl column; metric band aligns with the form column; explicit screen form widths left-align; password input keeps focus",
|
|
25
|
+
"detail": "A projectionDetail screen with a header card (headerRegion with metrics or header actions) on a full-height screen form without tabs now defaults to a `4xl` column instead of 640px, and the metric band lines up with the form column instead of carrying its own padding. An explicit `layout.width` still wins. Explicit screen form widths (`3xl`, `4xl`, `full`) now left-align with the form column instead of centering, which moves screens such as the showcase item edit. Typing into a password input that shows a clear or undo action no longer remounts the input and drops focus after the first character.\nThe secretMint reveal phase with a confirm step renders as one screen form. The reveal block (title, warning, secret) leads the confirm form through the new `RenderEdit` prop `leadContent`, and the confirm form no longer sits in a second card. The `kumiko-screen-secret-mint-card` test id now marks only that reveal block. The confirm step shows the parent screen's translated title (new `RenderEdit` prop `i18nScreenId`) instead of the raw key `<screen>:confirm`.\nText config keys stored encrypted at rest (`encrypted: true` or `backing: \"secrets\"`, such as the Stripe API key and webhook secret) are write-only on generated settings screens. The field shows whether this scope stores a value, keeps it unless a new one is typed, and resets it when cleared. Hand-written configEdit text fields over such keys become write-only as well, and form drafts never store write-only fields. `config:write:set` answers with the mask for these keys, and pattern, select option and extension validation errors no longer return the value. A hand-written configEdit screen may declare `writeOnly` only on a field whose key is encrypted at rest; other keys fail at boot."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"version": "0.335.0",
|
|
29
|
+
"type": "fix",
|
|
30
|
+
"title": "Page title stays visible on narrow screens",
|
|
31
|
+
"detail": "Wide header actions no longer push the page title out of the header on phones; the actions area shrinks instead."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.335.0",
|
|
35
|
+
"type": "fix",
|
|
36
|
+
"title": "Dashboard list columns support display \"datetime\"; overview lists no longer show raw ISO timestamps",
|
|
37
|
+
"detail": "Dashboard list columns accept `display: \"datetime\"`, which formats an ISO string or epoch-ms value in the user's locale and time zone. The admin-shell overview lists use it for `startedAt` and `failedAt` instead of showing raw ISO strings."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"version": "0.335.0",
|
|
41
|
+
"type": "fix",
|
|
42
|
+
"title": "Dashboard time-range queries carry the user's time zone so day buckets match the axis labels",
|
|
43
|
+
"detail": "Dashboard time-range queries now send the user's time zone, so metric day buckets line up with the axis labels instead of being cut in UTC. `kumiko-bundled-features` declares `ioredis` and `hono`, which its emitted type declarations import."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"version": "0.335.0",
|
|
47
|
+
"type": "fix",
|
|
48
|
+
"title": "Labelled multiSelect checkbox group, mobile touch target for secondary form actions, bounded number unit suffix"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"version": "0.335.0",
|
|
52
|
+
"type": "fix",
|
|
53
|
+
"title": "Textarea respects preventDefault, Card data-slot is not overridable, degraded FormRoot scopes Enter and submit to its own inputs",
|
|
54
|
+
"detail": "A nested `Form` that degrades to a `<div>` now submits on Enter inside its own inputs and ignores submit buttons that belong to a portal or a deeper nested form. The textarea `onSubmitShortcut` no longer fires when the caller's `onKeyDown` called `preventDefault()`. `Card` no longer lets `dataAttributes` override `data-slot`."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"version": "0.335.0",
|
|
58
|
+
"type": "breaking",
|
|
59
|
+
"title": "InfinityList live defaults to false",
|
|
60
|
+
"detail": "Review round 2 fixes for renderer-web.\n- `InfinityList` now defaults `live` to `false` (it was `true`). Lists that relied on the implicit realtime refresh no longer update on SSE events until they pass `live={true}`.\n- A number input with a `unit` exposes the unit to screen readers through `aria-describedby` instead of hiding it with `aria-hidden`.\n- `Drawer` with an explicit `width` is capped at `85vw`, the same limit as the default width.",
|
|
61
|
+
"migration": "Pass `live={true}` to every `<InfinityList>` that should refresh from SSE events. Without it the list only loads on mount and on pagination."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"version": "0.335.0",
|
|
65
|
+
"type": "fix",
|
|
66
|
+
"title": "Number input unit is announced by screen readers and Drawer width is capped to the viewport"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"version": "0.335.0",
|
|
70
|
+
"type": "fix",
|
|
71
|
+
"title": "Row-actions kebab keeps focus in the confirm dialog, embedded-list headers truncate in fixed columns",
|
|
72
|
+
"detail": "The row-actions kebab no longer returns focus to its trigger while a confirm dialog is open, so keyboard and screen-reader users reach the dialog buttons. Column headers of the embedded-list desktop table truncate (with a `title`) inside their fixed-width column instead of overlapping the neighbouring header."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"version": "0.335.0",
|
|
76
|
+
"type": "improvement",
|
|
77
|
+
"title": "StepBar supports explicit done state and jumping to any non-current step",
|
|
78
|
+
"detail": "The default `StepBar` forwards the new `doneSteps` and `selectableSteps` props: an upcoming chip can be a button that shows its number, and done state no longer has to follow position.",
|
|
79
|
+
"migration": "No code change needed."
|
|
80
|
+
},
|
|
2
81
|
{
|
|
3
82
|
"version": "0.334.0",
|
|
4
83
|
"type": "improvement",
|