@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.
- package/dist/layout/language-switcher.d.ts +4 -1
- package/dist/layout/language-switcher.js +2 -2
- package/dist/primitives/index.js +8 -3
- package/dist/ui/sidebar.d.ts +1 -1
- package/dist/ui/sidebar.js +3 -1
- package/dist/widgets/charts.d.ts +2 -1
- package/dist/widgets/charts.js +19 -7
- package/package.json +7 -7
- package/src/changes.json +12 -0
|
@@ -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
|
}
|
package/dist/primitives/index.js
CHANGED
|
@@ -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",
|
|
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
|
-
:
|
|
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.
|
package/dist/ui/sidebar.d.ts
CHANGED
|
@@ -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<"
|
|
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;
|
package/dist/ui/sidebar.js
CHANGED
|
@@ -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("
|
|
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 }));
|
package/dist/widgets/charts.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/widgets/charts.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.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.
|
|
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",
|