@cosmicdrift/kumiko-renderer-web 0.347.0 → 0.348.1

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 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 h-8 items-center gap-1.5 rounded-md border bg-background px-2 text-sm", "hover:bg-accent hover:text-accent-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"), children: [_jsx("span", { "aria-hidden": "true", children: icon }), triggerContent === "label" && (_jsx("span", { className: "text-xs text-muted-foreground", children: activeOption?.label ?? activeLocale.slice(0, 2) })), 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))) })] }));
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
  }
@@ -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;
@@ -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 h-16 items-center justify-center text-[13px] text-muted-foreground", children: emptyContent }));
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 maxValue = Math.max(...values.map((value) => (singleValue === undefined ? value : value * 2)), ...drawableLines.map((line) => line.value), 1);
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 h-16 w-full", 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 })] }) }), 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}`)))] }));
101
- return (_jsxs("div", { "data-testid": testId, className: STATUS_TONE_TEXT[tone], 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]",
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), 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 })] })), 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 }) })] }))] }));
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)
@@ -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
- return (_jsx("div", { "data-testid": testId, role: "group", "aria-label": ariaLabel, className: "flex min-h-8 overflow-hidden rounded-md border border-input bg-background", children: options.map((o) => {
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 border-l border-input px-3 text-sm transition-colors first:border-l-0", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", active
9
- ? "bg-primary/10 font-semibold text-primary"
10
- : "text-foreground hover:bg-muted"), 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
+ 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.347.0",
3
+ "version": "0.348.1",
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.347.0",
24
- "@cosmicdrift/kumiko-framework": "0.347.0",
25
- "@cosmicdrift/kumiko-headless": "0.347.0",
26
- "@cosmicdrift/kumiko-renderer": "0.347.0",
27
- "@cosmicdrift/kumiko-types": "0.347.0",
23
+ "@cosmicdrift/kumiko-dispatcher-live": "0.348.1",
24
+ "@cosmicdrift/kumiko-framework": "0.348.1",
25
+ "@cosmicdrift/kumiko-headless": "0.348.1",
26
+ "@cosmicdrift/kumiko-renderer": "0.348.1",
27
+ "@cosmicdrift/kumiko-types": "0.348.1",
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.347.0"
75
+ "@cosmicdrift/kumiko-locale-de": "0.348.1"
76
76
  },
77
77
  "repository": {
78
78
  "type": "git",