@cosmicdrift/kumiko-renderer-web 0.340.0 → 0.342.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.
@@ -13,6 +13,9 @@ export type LanguageSwitcherProps = {
13
13
  readonly icon?: ReactNode;
14
14
  /** aria-label + title of the trigger. Default: translated "kumiko.nav.language". */
15
15
  readonly label?: string;
16
+ /** What the trigger shows next to the icon: the uppercase code (default), the active
17
+ * locale's label, or the icon only. aria-label/title are unaffected. */
18
+ readonly triggerContent?: "code" | "label" | "icon-only";
16
19
  readonly testId?: string;
17
20
  };
18
- export declare function LanguageSwitcher({ locales, icon, label, testId, }: LanguageSwitcherProps): ReactNode;
21
+ export declare function LanguageSwitcher({ locales, icon, label, triggerContent, 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, testId, }) {
18
+ export function LanguageSwitcher({ locales, icon = "🌐", label, triggerContent = "code", testId, }) {
19
19
  const resolver = useLocale();
20
20
  const t = useTranslation();
21
21
  const resolvedLabel = label ?? t("kumiko.nav.language");
@@ -39,5 +39,5 @@ export function LanguageSwitcher({ locales, icon = "🌐", label, testId, }) {
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 }), _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 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))) })] }));
43
43
  }
@@ -106,19 +106,24 @@ function DefaultButton({ type = "button", onClick, disabled, loading, variant =
106
106
  return (_jsxs(UiButton, { ref: ref, type: type, onClick: onClick, disabled: disabled === true || loading === true, ...dataAttributes, "data-testid": testId, "data-loading": loading === true ? "true" : undefined, variant: BUTTON_VARIANT[variant], size: BUTTON_SIZE[size], "aria-label": ariaLabel, "aria-pressed": pressed, "aria-expanded": expanded, title: title ?? (size === "icon" ? ariaLabel : undefined), className: resolvedClassName, children: [leading, children, IconEnd !== undefined && _jsx(IconEnd, { className: "size-4", "aria-hidden": "true" })] }));
107
107
  }
108
108
  // ---- Banner (shadcn: Alert) ----
109
- function DefaultBanner({ variant = "info", children, actions, padded, testId, id, }) {
109
+ function DefaultBanner({ variant = "info", title, titleTestId, children, actions, padded, testId, id, }) {
110
110
  const isError = variant === "error";
111
111
  const isWarning = variant === "warning";
112
+ const isPrimary = variant === "primary";
112
113
  // `id` is set when a <Field> wraps this Banner as its control (see
113
114
  // BannerProps.id). A <div> isn't labelable, so a <label htmlFor> pointing
114
115
  // at it is inert for screen readers — role="group" + aria-labelledby is
115
116
  // the actual mechanism, pointed at the label id Field emits (fieldLabelId).
116
117
  const bannerRole = id !== undefined ? "group" : isError ? "alert" : undefined;
117
- const banner = (_jsxs("div", { role: bannerRole, ...(id !== undefined && { "aria-labelledby": fieldLabelId(id) }), "data-testid": testId, "data-variant": variant, className: cn("relative w-full rounded-lg border px-4 py-3 text-sm flex items-center gap-3", isError
118
+ const banner = (_jsxs("div", { role: bannerRole, ...(id !== undefined && { "aria-labelledby": fieldLabelId(id) }), "data-testid": testId, "data-variant": variant, className: cn("relative w-full rounded-lg border px-4 py-3 text-sm flex items-center gap-3",
119
+ // Viewport breakpoint like the form footer bar; actions drop below the text on phones.
120
+ isPrimary && "flex-col items-start sm:flex-row sm:items-center", isError
118
121
  ? "border-destructive/50 text-destructive bg-destructive/10 dark:border-destructive"
119
122
  : isWarning
120
123
  ? "border-status-warn/30 bg-status-warn/10 text-status-warn"
121
- : "bg-card text-card-foreground"), children: [_jsx("div", { className: "flex-1", children: children }), actions !== undefined && _jsx("div", { "data-slot": "actions", children: actions })] }));
124
+ : isPrimary
125
+ ? "border-l-4 border-l-primary bg-card text-card-foreground"
126
+ : "bg-card text-card-foreground"), children: [_jsxs("div", { className: "flex-1", children: [title !== undefined && (_jsx("div", { "data-testid": titleTestId, className: "font-semibold", children: title })), children] }), actions !== undefined && _jsx("div", { "data-slot": "actions", children: actions })] }));
122
127
  // Page-State: Banner sitzt alleine im Main (das kein Padding mehr
123
128
  // hat). Wrapper gibt 24px Außenabstand damit der Banner nicht edge-
124
129
  // to-edge an Sidebar/Browser klebt.
@@ -26,7 +26,7 @@ declare function Sidebar({ side, variant, collapsible, className, children, ...p
26
26
  }): React.JSX.Element;
27
27
  declare function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>): React.JSX.Element;
28
28
  declare function SidebarRail({ className, ...props }: React.ComponentProps<"button">): React.JSX.Element;
29
- declare function SidebarInset({ className, ...props }: React.ComponentProps<"main">): React.JSX.Element;
29
+ declare function SidebarInset({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
30
30
  declare function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>): React.JSX.Element;
31
31
  declare function SidebarHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
32
32
  declare function SidebarFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
@@ -112,8 +112,10 @@ function SidebarRail({ className, ...props }) {
112
112
  const { toggleSidebar } = useSidebar();
113
113
  return (_jsx("button", { "data-sidebar": "rail", "data-slot": "sidebar-rail", "aria-label": "Toggle Sidebar", tabIndex: -1, onClick: toggleSidebar, title: "Toggle Sidebar", className: cn("absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className), ...props }));
114
114
  }
115
+ // Deviates from the shadcn template (which renders <main>): the shells own the
116
+ // single main landmark around the screen content, so the inset must not be one.
115
117
  function SidebarInset({ className, ...props }) {
116
- return (_jsx("main", { "data-slot": "sidebar-inset", className: cn("relative flex w-full flex-1 flex-col bg-card", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className), ...props }));
118
+ return (_jsx("div", { "data-slot": "sidebar-inset", className: cn("relative flex w-full flex-1 flex-col bg-card", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className), ...props }));
117
119
  }
118
120
  function SidebarInput({ className, ...props }) {
119
121
  return (_jsx(Input, { "data-slot": "sidebar-input", "data-sidebar": "input", className: cn("h-8 w-full border-sidebar-border bg-sidebar-input text-sidebar-foreground shadow-none placeholder:text-sidebar-muted", className), ...props }));
@@ -63,7 +63,8 @@ export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, to
63
63
  /** Dashed horizontal threshold lines (e.g. p95); they extend the y-scale
64
64
  * when above the data maximum. Non-finite and negative values are dropped. */
65
65
  readonly referenceLines?: readonly TimeseriesReferenceLine[];
66
- /** Rendert statt des Charts wenn <2 Messwerte vorliegen. */
66
+ /** Rendered instead of the chart when there is no value; a single value
67
+ * is drawn as a point. */
67
68
  readonly emptyContent?: ReactNode;
68
69
  readonly testId?: string;
69
70
  }): ReactNode;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useId } from "react";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { STATUS_TONE_TEXT } from "./status-badge.js";
@@ -40,6 +40,10 @@ export function StatusBarChart({ entries, ariaLabel, startLabel, endLabel, highl
40
40
  return (_jsxs("g", { children: [!dense && (_jsx("defs", { children: _jsxs("linearGradient", { id: gradId, x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: color, stopOpacity: isLast ? 0.85 : 0.5 }), _jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0.05 })] }) })), isLast && (_jsx("rect", { x: x - barGap / 2, y: 0, width: barWidth + barGap, height: chartHeight, fill: "var(--color-foreground)", fillOpacity: 0.06 })), _jsx("rect", { x: x, y: barY, width: barWidth, height: barHeight, fill: dense ? color : `url(#${gradId})`, fillOpacity: dense ? (isLast ? 1 : 0.75) : undefined, children: entry.label !== undefined && _jsx("title", { children: entry.label }) }), !dense && (_jsx("rect", { x: x, y: barY - tickHeight, width: barWidth, height: tickHeight, fill: "var(--color-foreground)", fillOpacity: isLast ? 1.0 : 0.7 }))] }, entry.key));
41
41
  })] }), (startLabel !== undefined || endLabel !== undefined) && (_jsxs("div", { className: "mt-0.5 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: startLabel }), _jsx("span", { children: endLabel })] }))] }));
42
42
  }
43
+ const SINGLE_POINT_RADIUS = 3;
44
+ function clampDotY(y, height) {
45
+ return Math.max(SINGLE_POINT_RADIUS, Math.min(height - SINGLE_POINT_RADIUS, y));
46
+ }
43
47
  /** Quadratic-durch-Mittelpunkte-Trick: glättet die Zick-Zack-Linie ohne
44
48
  * Overshoot (~5 Zeilen statt Catmull-Rom/Bezier-Fit). */
45
49
  export function smoothPath(pts) {
@@ -70,11 +74,14 @@ export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok
70
74
  const chartWidth = 300;
71
75
  const chartHeight = 64;
72
76
  const values = points.map((p) => p.value).filter((v) => v !== null);
73
- if (values.length < 2) {
77
+ if (values.length === 0) {
74
78
  return (_jsx("div", { className: "flex h-16 items-center justify-center text-[13px] text-muted-foreground", children: emptyContent }));
75
79
  }
76
80
  const drawableLines = (referenceLines ?? []).filter((line) => Number.isFinite(line.value) && line.value >= 0);
77
- const maxValue = Math.max(...values, ...drawableLines.map((line) => line.value), 1);
81
+ const singleValue = values.length === 1 ? values[0] : undefined;
82
+ // A lone value at the top edge would sit on the clip boundary; doubling the
83
+ // scale centres it.
84
+ const maxValue = Math.max(...values.map((value) => (singleValue === undefined ? value : value * 2)), ...drawableLines.map((line) => line.value), 1);
78
85
  const yOf = (value) => chartHeight - (value / maxValue) * chartHeight;
79
86
  const span = Math.max(1, windowEndMs - windowStartMs);
80
87
  const xOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span)) * chartWidth;
@@ -90,7 +97,7 @@ export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok
90
97
  : "";
91
98
  const color = TONE_VAR[tone];
92
99
  const hasReferenceLines = drawableLines.length > 0;
93
- 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 })] }) }), _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" }), 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}`)))] }));
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}`)))] }));
94
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]",
95
102
  // A line near the top would push its label above the chart box.
96
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 }) })] }))] }));
@@ -212,14 +219,16 @@ function MarkerLegend({ markers, formatMarkerTime, }) {
212
219
  export function StackedAreaChart({ series, windowStartMs, windowEndMs, todayMs, tones, markers, ariaLabel, todayLabel, formatBucketLabel, formatValue = String, formatMarkerTime, emptyContent, testId, }) {
213
220
  const clipId = useId();
214
221
  const times = bucketTimes(series);
215
- if (times.length < 2)
222
+ if (times.length === 0)
216
223
  return _jsx("div", { "data-testid": testId, children: emptyContent });
217
224
  const width = 300;
218
225
  const height = 120;
219
226
  const span = Math.max(1, windowEndMs - windowStartMs);
220
227
  const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span));
221
228
  const totals = times.map((atMs) => series.reduce((sum, s) => sum + valueAt(s, atMs), 0));
222
- const max = niceCeil(Math.max(...totals));
229
+ const isSingleBucket = times.length === 1;
230
+ // Doubling keeps a lone bucket away from the top edge of the y-scale.
231
+ const max = niceCeil(Math.max(...totals) * (isSingleBucket ? 2 : 1));
223
232
  const yOf = (value) => height - (value / max) * height;
224
233
  const lower = times.map(() => 0);
225
234
  const bands = series.map((s, si) => {
@@ -228,17 +237,20 @@ export function StackedAreaChart({ series, windowStartMs, windowEndMs, todayMs,
228
237
  const bottom = times
229
238
  .map((atMs, ti) => `${(fractionOf(atMs) * width).toFixed(1)} ${yOf(lower[ti] ?? 0).toFixed(1)}`)
230
239
  .reverse();
240
+ const bucketTop = upper[0] ?? 0;
241
+ const bucketHeight = bucketTop - (lower[0] ?? 0);
231
242
  for (let ti = 0; ti < times.length; ti++)
232
243
  lower[ti] = upper[ti] ?? 0;
233
244
  return {
234
245
  key: s.key,
235
246
  color: chartColor(s.key, si, tones),
236
247
  d: `M ${top.join(" L ")} L ${bottom.join(" L ")} Z`,
248
+ dot: isSingleBucket && bucketHeight > 0 ? clampDotY(yOf(bucketTop), height) : undefined,
237
249
  };
238
250
  });
239
251
  const todayFraction = todayMs !== undefined ? fractionOf(todayMs) : undefined;
240
252
  const todayX = (todayFraction ?? 1) * width;
241
- const renderBands = (fillOpacity) => bands.map((band) => (_jsx("path", { d: band.d, fill: band.color, fillOpacity: fillOpacity, stroke: "none" }, band.key)));
253
+ const renderBands = (fillOpacity) => bands.map((band) => band.dot === undefined ? (_jsx("path", { d: band.d, fill: band.color, fillOpacity: fillOpacity, stroke: "none" }, band.key)) : (_jsx("circle", { "data-testid": "stacked-area-single-point", cx: width / 2, cy: band.dot, r: SINGLE_POINT_RADIUS, fill: band.color, fillOpacity: fillOpacity }, band.key)));
242
254
  return (_jsxs("div", { "data-testid": testId, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx(YTicks, { max: max, formatValue: formatValue, className: "h-32 pb-0" }), _jsxs("div", { className: "relative grow", children: [todayFraction !== undefined && (_jsx("span", { "data-testid": "chart-today-label", className: "absolute -top-4 -translate-x-1/2 text-[11px] text-foreground", style: { left: percent(todayFraction) }, children: todayLabel })), _jsxs("svg", { viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", className: "block h-32 w-full", role: "img", "aria-label": ariaLabel, children: [_jsx("title", { children: ariaLabel }), _jsxs("defs", { children: [_jsx("clipPath", { id: `${clipId}-past`, children: _jsx("rect", { x: 0, y: 0, width: todayX, height: height }) }), _jsx("clipPath", { id: `${clipId}-future`, children: _jsx("rect", { x: todayX, y: 0, width: width - todayX, height: height }) })] }), _jsx("g", { clipPath: `url(#${clipId}-past)`, children: renderBands(0.85) }), todayFraction !== undefined && (_jsx("g", { "data-testid": "chart-forecast-region", clipPath: `url(#${clipId}-future)`, children: renderBands(0.3) })), todayFraction !== undefined && (_jsx("line", { "data-testid": "chart-today-line", x1: todayX, x2: todayX, y1: 0, y2: height, stroke: "var(--color-foreground)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }))] }), _jsxs("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: formatBucketLabel(windowStartMs) }), _jsx("span", { children: formatBucketLabel(windowEndMs) })] }), markers !== undefined && markers.length > 0 && (_jsx(MarkerPins, { markers: markers, windowStartMs: windowStartMs, windowEndMs: windowEndMs }))] })] }), _jsx(ChartLegend, { formatValue: formatValue, items: series.map((s, i) => ({
243
255
  key: s.key,
244
256
  label: s.label,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.340.0",
3
+ "version": "0.342.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.340.0",
24
- "@cosmicdrift/kumiko-framework": "0.340.0",
25
- "@cosmicdrift/kumiko-headless": "0.340.0",
26
- "@cosmicdrift/kumiko-renderer": "0.340.0",
27
- "@cosmicdrift/kumiko-types": "0.340.0",
23
+ "@cosmicdrift/kumiko-dispatcher-live": "0.342.0",
24
+ "@cosmicdrift/kumiko-framework": "0.342.0",
25
+ "@cosmicdrift/kumiko-headless": "0.342.0",
26
+ "@cosmicdrift/kumiko-renderer": "0.342.0",
27
+ "@cosmicdrift/kumiko-types": "0.342.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.340.0"
75
+ "@cosmicdrift/kumiko-locale-de": "0.342.0"
76
76
  },
77
77
  "repository": {
78
78
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.342.0",
4
+ "type": "improvement",
5
+ "title": "Single-value timeseries point and LanguageSwitcher triggerContent",
6
+ "detail": "TimeseriesChart and StackedAreaChart draw a single value; LanguageSwitcher takes triggerContent\n`TimeseriesChart` and `StackedAreaChart` show `emptyContent` only when there is no value at all. With exactly one value they draw a point at the horizontal centre with the y-scale widened around it, so it no longer sits on the edge. `LanguageSwitcher` takes `triggerContent: \"code\" | \"label\" | \"icon-only\"` (default `\"code\"`); `\"label\"` shows the label of the active locale in the trigger. `aria-label` and `title` are unchanged."
7
+ },
8
+ {
9
+ "version": "0.341.0",
10
+ "type": "fix",
11
+ "title": "App shells render a single main landmark",
12
+ "detail": "`DefaultAppShell` and `WorkspaceShell` render exactly one `main` landmark. `SidebarInset` is now a `div`, so the shell's own `main` around the screen content is the only one, and the `ShellHeader` sits outside it. Screen readers no longer announce two main regions, and `getByRole(\"main\")` in E2E tests matches a single element."
13
+ },
2
14
  {
3
15
  "version": "0.339.0",
4
16
  "type": "fix",