@cosmicdrift/kumiko-renderer-web 0.347.0 → 0.348.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/layout/language-switcher.d.ts +4 -1
- package/dist/layout/language-switcher.js +4 -2
- package/dist/widgets/charts.d.ts +19 -1
- package/dist/widgets/charts.js +33 -6
- package/dist/widgets/index.d.ts +2 -2
- package/dist/widgets/mode-switch.d.ts +6 -1
- package/dist/widgets/mode-switch.js +18 -5
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -69,5 +69,5 @@ export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resiz
|
|
|
69
69
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.js";
|
|
70
70
|
export type { KumikoBuild } from "./version/update-checker.js";
|
|
71
71
|
export { readLoadedBuild } from "./version/update-checker.js";
|
|
72
|
-
export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, NumberFieldProps, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesPoint, TimeseriesReferenceLine, UploadZoneProps, } from "./widgets/index.js";
|
|
72
|
+
export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, ModeSwitchVariant, NumberFieldProps, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesXAxis, TimeseriesYAxis, UploadZoneProps, } from "./widgets/index.js";
|
|
73
73
|
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
|
|
@@ -16,6 +16,9 @@ export type LanguageSwitcherProps = {
|
|
|
16
16
|
/** What the trigger shows next to the icon: the uppercase code (default), the active
|
|
17
17
|
* locale's label, or the icon only. aria-label/title are unaffected. */
|
|
18
18
|
readonly triggerContent?: "code" | "label" | "icon-only";
|
|
19
|
+
/** `default`: icon plus code/label button. `chip`: compact monospace chip with
|
|
20
|
+
* the uppercase code in a border; `icon` and `triggerContent` are ignored. */
|
|
21
|
+
readonly variant?: "default" | "chip";
|
|
19
22
|
readonly testId?: string;
|
|
20
23
|
};
|
|
21
|
-
export declare function LanguageSwitcher({ locales, icon, label, triggerContent, testId, }: LanguageSwitcherProps): ReactNode;
|
|
24
|
+
export declare function LanguageSwitcher({ locales, icon, label, triggerContent, variant, testId, }: LanguageSwitcherProps): ReactNode;
|
|
@@ -15,7 +15,7 @@ import { useLocale, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
|
15
15
|
import { useMemo, useState } from "react";
|
|
16
16
|
import { cn } from "../lib/cn.js";
|
|
17
17
|
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "../primitives/dropdown-menu.js";
|
|
18
|
-
export function LanguageSwitcher({ locales, icon = "🌐", label, triggerContent = "code", testId, }) {
|
|
18
|
+
export function LanguageSwitcher({ locales, icon = "🌐", label, triggerContent = "code", variant = "default", testId, }) {
|
|
19
19
|
const resolver = useLocale();
|
|
20
20
|
const t = useTranslation();
|
|
21
21
|
const resolvedLabel = label ?? t("kumiko.nav.language");
|
|
@@ -39,5 +39,7 @@ export function LanguageSwitcher({ locales, icon = "🌐", label, triggerContent
|
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
41
|
const setLocale = resolver.setLocale;
|
|
42
|
-
return (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { ref: setTrigger, type: "button", "aria-label": resolvedLabel, title: resolvedLabel, "data-testid": testId, className: cn("inline-flex
|
|
42
|
+
return (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { ref: setTrigger, type: "button", "aria-label": resolvedLabel, title: resolvedLabel, "data-testid": testId, className: cn("inline-flex items-center border bg-background", "hover:bg-accent hover:text-accent-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", variant === "chip"
|
|
43
|
+
? "h-7 rounded-md px-2 font-mono text-xs text-foreground"
|
|
44
|
+
: "h-8 gap-1.5 rounded-md px-2 text-sm"), children: [variant === "chip" && (activeOption?.code ?? activeLocale.slice(0, 2)).toUpperCase(), variant === "default" && _jsx("span", { "aria-hidden": "true", children: icon }), variant === "default" && triggerContent === "label" && (_jsx("span", { className: "text-xs text-muted-foreground", children: activeOption?.label ?? activeLocale.slice(0, 2) })), variant === "default" && triggerContent === "code" && (_jsx("span", { className: "uppercase text-xs text-muted-foreground", children: activeOption?.code ?? activeLocale.slice(0, 2) }))] }) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[10rem]", "aria-label": resolvedLabel, portalContainer: landmark instanceof HTMLElement ? landmark : undefined, children: locales.map((opt) => (_jsx(DropdownMenuCheckboxItem, { checked: opt === activeOption, onSelect: () => setLocale(opt.code), children: _jsx("span", { className: "truncate", children: opt.label }) }, opt.code))) })] }));
|
|
43
45
|
}
|
package/dist/widgets/charts.d.ts
CHANGED
|
@@ -35,6 +35,18 @@ export type TimeseriesPoint = {
|
|
|
35
35
|
/** null = Ausfall/kein Wert → fällt auf die Grundlinie (sichtbarer Einbruch). */
|
|
36
36
|
readonly value: number | null;
|
|
37
37
|
};
|
|
38
|
+
export type TimeseriesYAxis = {
|
|
39
|
+
/** Number of gridlines including the zero baseline (min 2). Tick values are
|
|
40
|
+
* rounded to 1/2/5 x 10^n steps and the y-scale reaches the top tick. */
|
|
41
|
+
readonly ticks: number;
|
|
42
|
+
/** Label text per tick value (unit, locale). Default: the plain number. */
|
|
43
|
+
readonly format?: (value: number) => string;
|
|
44
|
+
};
|
|
45
|
+
export type TimeseriesXAxis = {
|
|
46
|
+
/** Number of evenly spaced time labels (min 2). */
|
|
47
|
+
readonly ticks: number;
|
|
48
|
+
readonly format: (atMs: number) => string;
|
|
49
|
+
};
|
|
38
50
|
export type TimeseriesReferenceLine = {
|
|
39
51
|
/** Same unit as the point values (e.g. a p95 or SLO threshold in ms). */
|
|
40
52
|
readonly value: number;
|
|
@@ -45,7 +57,7 @@ export type TimeseriesReferenceLine = {
|
|
|
45
57
|
/** Zeitreihen-Linien-Chart (geglättete Linie + Flächen-Verlauf). x-Achse =
|
|
46
58
|
* ZEIT im Fenster windowStartMs..windowEndMs, nicht Index — 5 Min Daten in
|
|
47
59
|
* einem 30-Tage-Fenster ergeben ehrlich einen schmalen Streifen rechts. */
|
|
48
|
-
export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, tone, ariaLabel, axisLabels, markers, formatMarkerTime, referenceLines, emptyContent, testId, }: {
|
|
60
|
+
export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, tone, ariaLabel, axisLabels, xAxis, yAxis, height, markers, formatMarkerTime, referenceLines, emptyContent, testId, }: {
|
|
49
61
|
readonly points: readonly TimeseriesPoint[];
|
|
50
62
|
readonly windowStartMs: number;
|
|
51
63
|
readonly windowEndMs: number;
|
|
@@ -57,6 +69,12 @@ export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, to
|
|
|
57
69
|
readonly mid?: string;
|
|
58
70
|
readonly end: string;
|
|
59
71
|
};
|
|
72
|
+
/** Date axis with n evenly spaced labels; replaces `axisLabels` when set. */
|
|
73
|
+
readonly xAxis?: TimeseriesXAxis;
|
|
74
|
+
/** Gridlines plus value labels in a left gutter. */
|
|
75
|
+
readonly yAxis?: TimeseriesYAxis;
|
|
76
|
+
/** Chart height in px. Default: 64 (`h-16`). */
|
|
77
|
+
readonly height?: number;
|
|
60
78
|
/** Numbered pins on the x-axis plus a numbered list below the chart. */
|
|
61
79
|
readonly markers?: readonly ChartMarker[];
|
|
62
80
|
readonly formatMarkerTime?: (atMs: number) => string;
|
package/dist/widgets/charts.js
CHANGED
|
@@ -65,23 +65,44 @@ export function smoothPath(pts) {
|
|
|
65
65
|
return d;
|
|
66
66
|
return `${d} L ${last.x.toFixed(1)} ${last.y.toFixed(1)}`;
|
|
67
67
|
}
|
|
68
|
+
const DEFAULT_TIMESERIES_HEIGHT_CLASS = "h-16";
|
|
69
|
+
const MIN_AXIS_TICKS = 2;
|
|
70
|
+
const MAX_AXIS_TICKS = 20;
|
|
71
|
+
function clampTickCount(requested) {
|
|
72
|
+
return Math.min(MAX_AXIS_TICKS, Math.max(MIN_AXIS_TICKS, Math.floor(requested) || MIN_AXIS_TICKS));
|
|
73
|
+
}
|
|
74
|
+
function evenlySpaced(count) {
|
|
75
|
+
const n = clampTickCount(count);
|
|
76
|
+
return Array.from({ length: n }, (_, i) => i / (n - 1));
|
|
77
|
+
}
|
|
78
|
+
function yTickPosition(index, count) {
|
|
79
|
+
if (index === 0)
|
|
80
|
+
return "-translate-y-full";
|
|
81
|
+
if (index === count - 1)
|
|
82
|
+
return "";
|
|
83
|
+
return "-translate-y-1/2";
|
|
84
|
+
}
|
|
68
85
|
/** Zeitreihen-Linien-Chart (geglättete Linie + Flächen-Verlauf). x-Achse =
|
|
69
86
|
* ZEIT im Fenster windowStartMs..windowEndMs, nicht Index — 5 Min Daten in
|
|
70
87
|
* einem 30-Tage-Fenster ergeben ehrlich einen schmalen Streifen rechts. */
|
|
71
|
-
export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok", ariaLabel, axisLabels, markers, formatMarkerTime, referenceLines, emptyContent, testId, }) {
|
|
88
|
+
export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok", ariaLabel, axisLabels, xAxis, yAxis, height, markers, formatMarkerTime, referenceLines, emptyContent, testId, }) {
|
|
72
89
|
const gradientId = useId();
|
|
73
90
|
const descId = useId();
|
|
74
91
|
const chartWidth = 300;
|
|
75
92
|
const chartHeight = 64;
|
|
93
|
+
const heightClass = height === undefined ? DEFAULT_TIMESERIES_HEIGHT_CLASS : undefined;
|
|
94
|
+
const heightStyle = height === undefined ? undefined : { height };
|
|
76
95
|
const values = points.map((p) => p.value).filter((v) => v !== null);
|
|
77
96
|
if (values.length === 0) {
|
|
78
|
-
return (_jsx("div", { className: "flex
|
|
97
|
+
return (_jsx("div", { className: cn("flex items-center justify-center text-[13px] text-muted-foreground", heightClass), style: heightStyle, children: emptyContent }));
|
|
79
98
|
}
|
|
80
99
|
const drawableLines = (referenceLines ?? []).filter((line) => Number.isFinite(line.value) && line.value >= 0);
|
|
81
100
|
const singleValue = values.length === 1 ? values[0] : undefined;
|
|
82
101
|
// A lone value at the top edge would sit on the clip boundary; doubling the
|
|
83
102
|
// scale centres it.
|
|
84
|
-
const
|
|
103
|
+
const dataMax = Math.max(...values.map((value) => (singleValue === undefined ? value : value * 2)), ...drawableLines.map((line) => line.value), 1);
|
|
104
|
+
const yTicks = yAxis === undefined ? [] : niceYTicks(dataMax, yAxis.ticks);
|
|
105
|
+
const maxValue = yTicks.length > 0 ? (yTicks[yTicks.length - 1] ?? dataMax) : dataMax;
|
|
85
106
|
const yOf = (value) => chartHeight - (value / maxValue) * chartHeight;
|
|
86
107
|
const span = Math.max(1, windowEndMs - windowStartMs);
|
|
87
108
|
const xOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span)) * chartWidth;
|
|
@@ -97,10 +118,11 @@ export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok
|
|
|
97
118
|
: "";
|
|
98
119
|
const color = TONE_VAR[tone];
|
|
99
120
|
const hasReferenceLines = drawableLines.length > 0;
|
|
100
|
-
const svg = (_jsxs("svg", { viewBox: `0 0 ${chartWidth} ${chartHeight}`, preserveAspectRatio: "none", className: "block
|
|
101
|
-
|
|
121
|
+
const svg = (_jsxs("svg", { viewBox: `0 0 ${chartWidth} ${chartHeight}`, preserveAspectRatio: "none", className: cn("block w-full", heightClass), style: heightStyle, role: "img", "aria-label": ariaLabel, "aria-describedby": hasReferenceLines ? descId : undefined, children: [_jsx("title", { children: ariaLabel }), hasReferenceLines && _jsx("desc", { id: descId, children: drawableLines.map((l) => l.label).join(", ") }), _jsx("defs", { children: _jsxs("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: color, stopOpacity: 0.3 }), _jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0.02 })] }) }), yTicks.map((tick) => (_jsx("line", { "data-grid-line": "", x1: 0, x2: chartWidth, y1: yOf(tick), y2: yOf(tick), stroke: "var(--color-border)", strokeWidth: 1, vectorEffect: "non-scaling-stroke" }, `grid-${tick}`))), singleValue === undefined ? (_jsxs(_Fragment, { children: [_jsx("path", { d: areaPath, fill: `url(#${gradientId})`, stroke: "none" }), _jsx("path", { d: linePath, fill: "none", stroke: color, strokeWidth: 1.5, strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" })] })) : (_jsx("circle", { "data-testid": "timeseries-single-point", cx: chartWidth / 2, cy: clampDotY(yOf(singleValue), chartHeight), r: SINGLE_POINT_RADIUS, fill: color })), drawableLines.map((line) => (_jsx("line", { "data-reference-line": "", x1: 0, x2: chartWidth, y1: yOf(line.value), y2: yOf(line.value), stroke: TONE_VAR[line.tone ?? "muted"], strokeWidth: 1, strokeDasharray: "4 3", vectorEffect: "non-scaling-stroke" }, `${line.value}:${line.label}`)))] }));
|
|
122
|
+
const plot = (_jsxs(_Fragment, { children: [hasReferenceLines ? (_jsxs("div", { className: "relative", children: [svg, drawableLines.map((line) => (_jsx("span", { "aria-hidden": "true", className: cn("absolute right-0 rounded bg-background/80 px-1 text-[11px]",
|
|
102
123
|
// A line near the top would push its label above the chart box.
|
|
103
|
-
yOf(line.value) >= chartHeight / 4 && "-translate-y-full", STATUS_TONE_TEXT[line.tone ?? "muted"]), style: { top: `${(yOf(line.value) / chartHeight) * 100}%` }, children: line.label }, `${line.value}:${line.label}`)))] })) : (svg),
|
|
124
|
+
yOf(line.value) >= chartHeight / 4 && "-translate-y-full", STATUS_TONE_TEXT[line.tone ?? "muted"]), style: { top: `${(yOf(line.value) / chartHeight) * 100}%` }, children: line.label }, `${line.value}:${line.label}`)))] })) : (svg), xAxis !== undefined ? (_jsx("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: evenlySpaced(xAxis.ticks).map((fraction) => (_jsx("span", { "data-x-label": "", children: xAxis.format(windowStartMs + fraction * (windowEndMs - windowStartMs)) }, fraction))) })) : (axisLabels !== undefined && (_jsxs("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: axisLabels.start }), axisLabels.mid !== undefined && _jsx("span", { children: axisLabels.mid }), _jsx("span", { children: axisLabels.end })] })))] }));
|
|
125
|
+
return (_jsxs("div", { "data-testid": testId, className: STATUS_TONE_TEXT[tone], children: [yAxis === undefined ? (plot) : (_jsxs("div", { className: "flex gap-1.5", children: [_jsx("div", { "aria-hidden": "true", className: cn("relative w-12 shrink-0 text-right text-[11px] text-muted-foreground", heightClass), style: heightStyle, children: yTicks.map((tick, index) => (_jsx("span", { "data-y-label": "", className: cn("absolute right-0 whitespace-nowrap leading-none", yTickPosition(index, yTicks.length)), style: { top: `${(yOf(tick) / chartHeight) * 100}%` }, children: (yAxis.format ?? String)(tick) }, tick))) }), _jsx("div", { className: "min-w-0 flex-1", children: plot })] })), markers !== undefined && markers.length > 0 && (_jsxs("div", { className: "text-foreground", children: [_jsx(MarkerPins, { markers: markers, windowStartMs: windowStartMs, windowEndMs: windowEndMs }), _jsx("div", { className: "mt-2", children: _jsx(MarkerLegend, { markers: markers, formatMarkerTime: formatMarkerTime }) })] }))] }));
|
|
104
126
|
}
|
|
105
127
|
const CHART_TONE_VAR = {
|
|
106
128
|
positive: "var(--color-status-ok)",
|
|
@@ -122,6 +144,11 @@ function niceCeil(max) {
|
|
|
122
144
|
const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
|
|
123
145
|
return step * magnitude;
|
|
124
146
|
}
|
|
147
|
+
function niceYTicks(max, tickCount) {
|
|
148
|
+
const count = clampTickCount(tickCount);
|
|
149
|
+
const step = niceCeil(max / (count - 1));
|
|
150
|
+
return Array.from({ length: count }, (_, i) => i * step);
|
|
151
|
+
}
|
|
125
152
|
function bucketTimes(series) {
|
|
126
153
|
const times = new Set();
|
|
127
154
|
for (const s of series)
|
package/dist/widgets/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AiTextArea, type AiTextAreaProps, AiTextField, type AiTextFieldProps, } from "./ai-text-field.js";
|
|
2
|
-
export { type ChartMarker, type ChartSeries, type ChartTone, SegmentBarChart, type SegmentBarRow, StackedAreaChart, StackedBarChart, StatusBarChart, type StatusBarEntry, smoothPath, TimeseriesChart, type TimeseriesPoint, type TimeseriesReferenceLine, } from "./charts.js";
|
|
2
|
+
export { type ChartMarker, type ChartSeries, type ChartTone, SegmentBarChart, type SegmentBarRow, StackedAreaChart, StackedBarChart, StatusBarChart, type StatusBarEntry, smoothPath, TimeseriesChart, type TimeseriesPoint, type TimeseriesReferenceLine, type TimeseriesXAxis, type TimeseriesYAxis, } from "./charts.js";
|
|
3
3
|
export { CollapsibleSection } from "./collapsible-section.js";
|
|
4
4
|
export { type DashboardListColumn, DashboardListTable } from "./dashboard-list.js";
|
|
5
5
|
export { DetailList } from "./detail-list.js";
|
|
@@ -8,7 +8,7 @@ export { FeedList, type FeedRow } from "./feed-list.js";
|
|
|
8
8
|
export { FloatingPanel, type FloatingPanelGeometry, type FloatingPanelProps, } from "./floating-panel.js";
|
|
9
9
|
export { BooleanField, type BooleanFieldProps, DateField, type DateFieldProps, FileField, type FileFieldProps, MoneyField, NumberField, type NumberFieldProps, PercentField, RangeField, type RangeFieldProps, SelectField, type SelectFieldProps, TextareaField, type TextareaFieldProps, TextField, type TextFieldProps, } from "./form-fields.js";
|
|
10
10
|
export { InfinityList, type InfinityListProps, type InfinityListSelection, } from "./infinity-list.js";
|
|
11
|
-
export { ModeSwitch } from "./mode-switch.js";
|
|
11
|
+
export { ModeSwitch, type ModeSwitchVariant } from "./mode-switch.js";
|
|
12
12
|
export { type PhotoSlotSpec, PhotoSlots, type PhotoSlotsProps } from "./photo-slots.js";
|
|
13
13
|
export { PlanCard, type PlanCardActionSlot, type PlanCardPrice, type PlanCardProps, PlanGrid, type PlanGridProps, } from "./plan-card.js";
|
|
14
14
|
export { ProgressBar } from "./progress-bar.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
export type ModeSwitchVariant = "outline" | "pill";
|
|
2
3
|
/** Segmented-Control für sich ausschließende Modi — die prominente
|
|
3
4
|
* Alternative zum vergrabenen <select>. */
|
|
4
|
-
export declare function ModeSwitch<T extends string>({ value, options, onChange, ariaLabel, testId, }: {
|
|
5
|
+
export declare function ModeSwitch<T extends string>({ value, options, onChange, ariaLabel, variant, className, testId, }: {
|
|
5
6
|
readonly value: T;
|
|
6
7
|
readonly options: readonly {
|
|
7
8
|
readonly value: T;
|
|
@@ -11,5 +12,9 @@ export declare function ModeSwitch<T extends string>({ value, options, onChange,
|
|
|
11
12
|
}[];
|
|
12
13
|
readonly onChange: (value: T) => void;
|
|
13
14
|
readonly ariaLabel?: string;
|
|
15
|
+
/** `outline` (default): bordered segments, active tinted. `pill`: grey
|
|
16
|
+
* track with a raised active segment. */
|
|
17
|
+
readonly variant?: ModeSwitchVariant;
|
|
18
|
+
readonly className?: string;
|
|
14
19
|
readonly testId?: string;
|
|
15
20
|
}): ReactNode;
|
|
@@ -2,11 +2,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
/** Segmented-Control für sich ausschließende Modi — die prominente
|
|
4
4
|
* Alternative zum vergrabenen <select>. */
|
|
5
|
-
export function ModeSwitch({ value, options, onChange, ariaLabel, testId, }) {
|
|
6
|
-
|
|
5
|
+
export function ModeSwitch({ value, options, onChange, ariaLabel, variant = "outline", className, testId, }) {
|
|
6
|
+
const pill = variant === "pill";
|
|
7
|
+
return (_jsx("div", { "data-testid": testId, role: "group", "aria-label": ariaLabel, className: cn("flex min-h-8", pill
|
|
8
|
+
? "gap-0.5 rounded-lg bg-muted p-0.5"
|
|
9
|
+
: "overflow-hidden rounded-md border border-input bg-background", className), children: options.map((o) => {
|
|
7
10
|
const active = o.value === value;
|
|
8
|
-
return (_jsxs("button", { type: "button", "aria-pressed": active, onClick: () => onChange(o.value), className: cn("grow
|
|
9
|
-
?
|
|
10
|
-
|
|
11
|
+
return (_jsxs("button", { type: "button", "aria-pressed": active, onClick: () => onChange(o.value), className: cn("grow px-3 text-sm transition-colors", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", pill
|
|
12
|
+
? [
|
|
13
|
+
"rounded-md",
|
|
14
|
+
active
|
|
15
|
+
? "bg-card font-semibold text-foreground shadow-sm dark:bg-foreground/10"
|
|
16
|
+
: "text-muted-foreground hover:text-foreground",
|
|
17
|
+
]
|
|
18
|
+
: [
|
|
19
|
+
"border-l border-input first:border-l-0",
|
|
20
|
+
active
|
|
21
|
+
? "bg-primary/10 font-semibold text-primary"
|
|
22
|
+
: "text-foreground hover:bg-muted",
|
|
23
|
+
]), children: [o.label, o.count !== undefined && (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-count-${o.value}` : undefined, className: cn("ml-1.5 font-normal tabular-nums", !active && "text-muted-foreground"), children: o.count }))] }, o.value));
|
|
11
24
|
}) }));
|
|
12
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.348.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.348.0",
|
|
24
|
+
"@cosmicdrift/kumiko-framework": "0.348.0",
|
|
25
|
+
"@cosmicdrift/kumiko-headless": "0.348.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer": "0.348.0",
|
|
27
|
+
"@cosmicdrift/kumiko-types": "0.348.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.348.0"
|
|
76
76
|
},
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|