@cosmicdrift/kumiko-renderer-web 0.337.1 → 0.339.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/default-color-scheme.d.ts +2 -0
- package/dist/default-color-scheme.js +3 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/layout/language-switcher.js +6 -2
- package/dist/layout/theme-label.d.ts +9 -3
- package/dist/layout/theme-label.js +32 -8
- package/dist/layout/theme-menu-item.d.ts +3 -1
- package/dist/layout/theme-menu-item.js +9 -4
- package/dist/layout/theme-toggle.d.ts +9 -7
- package/dist/layout/theme-toggle.js +10 -6
- package/dist/primitives/dropdown-menu.d.ts +4 -1
- package/dist/primitives/dropdown-menu.js +2 -2
- package/dist/primitives/index.js +33 -6
- package/dist/primitives/lightbox.d.ts +1 -1
- package/dist/primitives/lightbox.js +29 -3
- package/dist/primitives/modal-shell.d.ts +3 -2
- package/dist/primitives/modal-shell.js +2 -2
- package/dist/theme-plugin.d.ts +3 -0
- package/dist/theme-plugin.js +4 -0
- package/dist/tokens.d.ts +4 -4
- package/dist/tokens.js +105 -31
- package/dist/version/update-checker.d.ts +2 -7
- package/dist/version/update-checker.js +10 -1
- package/dist/widgets/charts.d.ts +11 -1
- package/dist/widgets/charts.js +11 -4
- package/dist/widgets/index.d.ts +1 -1
- package/package.json +7 -7
- package/src/changes.json +35 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { AppPrimitives, AppSchema, AppTokens, BannerProps, ButtonProps, CardProps, ColorTokens, CorePrimitives, CoreTokens, DataTableProps, DispatcherProviderProps, FeatureSchema, FieldProps, FontTokens, FormProps, GridCellProps, GridProps, HeadingProps, InputProps, KumikoScreenProps, LiveEvent, LiveEventSubscriber, LiveEventsProviderProps, LocaleProviderProps, NavApi, NavProviderProps, NavRoute, NavTarget, PrimitivesProviderProps, PrimitivesRegistry, RadiusTokens, RenderEditChangeState, RenderEditControls, RenderEditProps, RenderFieldProps, RenderListProps, SectionProps, ShadowTokens, SpacingTokens, TextProps, ThemeMode, Tokens, TokensApi, TokensProviderProps, TranslationBundle, TranslationsByLocale, UseFormOptions, UseFormResult, UseQueryOptions, UseQueryResult, UserRolesProviderProps, WorkspaceSchema, } from "@cosmicdrift/kumiko-renderer";
|
|
1
|
+
export type { AppPrimitives, AppSchema, AppTokens, BannerProps, ButtonProps, CardProps, ColorTokens, CorePrimitives, CoreTokens, DataTableProps, DispatcherProviderProps, FeatureSchema, FieldProps, FontTokens, FormProps, GridCellProps, GridProps, HeadingProps, InputProps, KumikoScreenProps, LiveEvent, LiveEventSubscriber, LiveEventsProviderProps, LocaleProviderProps, NavApi, NavProviderProps, NavRoute, NavTarget, PrimitivesProviderProps, PrimitivesRegistry, RadiusTokens, RenderEditChangeState, RenderEditControls, RenderEditProps, RenderFieldProps, RenderListProps, SectionProps, ShadowTokens, SpacingTokens, TextProps, ThemeMode, ThemePreference, Tokens, TokensApi, TokensProviderProps, TranslationBundle, TranslationsByLocale, UseFormOptions, UseFormResult, UseQueryOptions, UseQueryResult, UserRolesProviderProps, WorkspaceSchema, } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
export { createStaticLocaleResolver, cssVarTokens, DispatcherProvider, formatPath, KumikoScreen, LiveEventsProvider, LocaleProvider, NavProvider, PrimitivesProvider, parsePath, qualifyScreenId, RenderEdit, RenderField, RenderList, synthesizeActionFormEntity, synthesizeActionFormScreen, TokensProvider, UserRolesProvider, useDispatcher, useDispatcherStatus, useForm, useLiveEvents, useLocale, useNav, usePrimitives, useQuery, useStore, useStoreSelector, useTokenController, useTokens, useTranslation, useUserRoles, } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
export { createBrowserLocaleResolver } from "./app/browser-locale.js";
|
|
4
4
|
export type { ClientFeatureDefinition } from "./app/client-plugin.js";
|
|
@@ -67,5 +67,7 @@ export { createEventSourceLiveEvents } from "./sse/live-events.js";
|
|
|
67
67
|
export { applyTokensToCssVars, defaultTokens, lightTokens, useBrowserTokensApi, } from "./tokens.js";
|
|
68
68
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable.js";
|
|
69
69
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.js";
|
|
70
|
-
export type {
|
|
70
|
+
export type { KumikoBuild } from "./version/update-checker.js";
|
|
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";
|
|
71
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";
|
package/dist/index.js
CHANGED
|
@@ -46,4 +46,5 @@ export { createEventSourceLiveEvents } from "./sse/live-events.js";
|
|
|
46
46
|
export { applyTokensToCssVars, defaultTokens, lightTokens, useBrowserTokensApi, } from "./tokens.js";
|
|
47
47
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable.js";
|
|
48
48
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.js";
|
|
49
|
+
export { readLoadedBuild } from "./version/update-checker.js";
|
|
49
50
|
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";
|
|
@@ -12,13 +12,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
// itself; an app that doesn't want an icon import gets the globe
|
|
13
13
|
// unicode glyph (🌐) as default.
|
|
14
14
|
import { useLocale, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
15
|
-
import { useMemo } from "react";
|
|
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
18
|
export function LanguageSwitcher({ locales, icon = "🌐", label, testId, }) {
|
|
19
19
|
const resolver = useLocale();
|
|
20
20
|
const t = useTranslation();
|
|
21
21
|
const resolvedLabel = label ?? t("kumiko.nav.language");
|
|
22
|
+
// Portal into the surrounding landmark: content under document.body would sit outside every
|
|
23
|
+
// landmark and fail axe's "region" rule.
|
|
24
|
+
const [trigger, setTrigger] = useState(null);
|
|
25
|
+
const landmark = trigger?.closest("header, nav, main, aside, [role='banner']");
|
|
22
26
|
const activeLocale = resolver.locale();
|
|
23
27
|
// Matches either exact ("de-DE") or the language root ("de") against
|
|
24
28
|
// the available options. So the switcher shows "German" active when
|
|
@@ -35,5 +39,5 @@ export function LanguageSwitcher({ locales, icon = "🌐", label, testId, }) {
|
|
|
35
39
|
return null;
|
|
36
40
|
}
|
|
37
41
|
const setLocale = resolver.setLocale;
|
|
38
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { 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, 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 }), _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))) })] }));
|
|
39
43
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { type ThemePreference } from "@cosmicdrift/kumiko-renderer";
|
|
1
2
|
export type ThemeToggleLabelOptions = {
|
|
3
|
+
/** Title while the next step is the light theme. */
|
|
2
4
|
readonly titleInDark?: string | undefined;
|
|
5
|
+
/** Title while the next step is the dark theme. */
|
|
3
6
|
readonly titleInLight?: string | undefined;
|
|
7
|
+
/** Title while the next step is the automatic theme. */
|
|
8
|
+
readonly titleForAuto?: string | undefined;
|
|
4
9
|
};
|
|
5
|
-
export declare function
|
|
6
|
-
|
|
10
|
+
export declare function nextThemePreference(current: ThemePreference): ThemePreference;
|
|
11
|
+
export declare function useThemeToggleLabel({ titleInDark, titleInLight, titleForAuto, }: ThemeToggleLabelOptions): {
|
|
12
|
+
readonly nextPreference: ThemePreference;
|
|
7
13
|
readonly title: string;
|
|
8
|
-
readonly
|
|
14
|
+
readonly cyclePreference: () => void;
|
|
9
15
|
};
|
|
@@ -1,11 +1,35 @@
|
|
|
1
|
-
import { useOptionalTranslation, useTokenController } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { useOptionalTranslation, useTokenController, } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
// light -> dark -> auto -> light: a click describes and applies the NEXT step.
|
|
3
|
+
export function nextThemePreference(current) {
|
|
4
|
+
if (current === "light")
|
|
5
|
+
return "dark";
|
|
6
|
+
if (current === "dark")
|
|
7
|
+
return "auto";
|
|
8
|
+
return "light";
|
|
9
|
+
}
|
|
10
|
+
export function useThemeToggleLabel({ titleInDark, titleInLight, titleForAuto, }) {
|
|
11
|
+
const { mode, preference = mode, setPreference, toggleMode } = useTokenController();
|
|
4
12
|
// Optional: the toggle also renders outside a LocaleProvider (samples, tests).
|
|
5
13
|
const translate = useOptionalTranslation();
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
? (
|
|
9
|
-
:
|
|
10
|
-
|
|
14
|
+
// A TokensApi without setPreference is a plain light/dark implementation: no auto step.
|
|
15
|
+
const nextPreference = setPreference
|
|
16
|
+
? nextThemePreference(preference)
|
|
17
|
+
: mode === "dark"
|
|
18
|
+
? "light"
|
|
19
|
+
: "dark";
|
|
20
|
+
const titles = {
|
|
21
|
+
light: titleInDark ?? translate?.("kumiko.theme.light") ?? "Light theme",
|
|
22
|
+
dark: titleInLight ?? translate?.("kumiko.theme.dark") ?? "Dark theme",
|
|
23
|
+
auto: titleForAuto ?? translate?.("kumiko.theme.auto") ?? "Automatic theme",
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
nextPreference,
|
|
27
|
+
title: titles[nextPreference],
|
|
28
|
+
cyclePreference: () => {
|
|
29
|
+
if (setPreference)
|
|
30
|
+
setPreference(nextPreference);
|
|
31
|
+
else
|
|
32
|
+
toggleMode();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
11
35
|
}
|
|
@@ -2,9 +2,11 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
export type ThemeMenuItemProps = {
|
|
3
3
|
readonly lightIcon?: ReactNode;
|
|
4
4
|
readonly darkIcon?: ReactNode;
|
|
5
|
+
readonly autoIcon?: ReactNode;
|
|
5
6
|
readonly titleInDark?: string;
|
|
6
7
|
readonly titleInLight?: string;
|
|
8
|
+
readonly titleForAuto?: string;
|
|
7
9
|
readonly testId?: string;
|
|
8
10
|
};
|
|
9
11
|
/** Theme toggle as a dropdown entry, for a user menu that carries the shell controls. */
|
|
10
|
-
export declare function ThemeMenuItem({ lightIcon, darkIcon, titleInDark, titleInLight, testId, }: ThemeMenuItemProps): ReactNode;
|
|
12
|
+
export declare function ThemeMenuItem({ lightIcon, darkIcon, autoIcon, titleInDark, titleInLight, titleForAuto, testId, }: ThemeMenuItemProps): ReactNode;
|
|
@@ -2,10 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { DropdownMenuItem } from "../primitives/dropdown-menu.js";
|
|
3
3
|
import { useThemeToggleLabel } from "./theme-label.js";
|
|
4
4
|
/** Theme toggle as a dropdown entry, for a user menu that carries the shell controls. */
|
|
5
|
-
export function ThemeMenuItem({ lightIcon = "☀", darkIcon = "☾", titleInDark, titleInLight, testId, }) {
|
|
6
|
-
const {
|
|
5
|
+
export function ThemeMenuItem({ lightIcon = "☀", darkIcon = "☾", autoIcon = "◐", titleInDark, titleInLight, titleForAuto, testId, }) {
|
|
6
|
+
const { nextPreference, title, cyclePreference } = useThemeToggleLabel({
|
|
7
|
+
titleInDark,
|
|
8
|
+
titleInLight,
|
|
9
|
+
titleForAuto,
|
|
10
|
+
});
|
|
11
|
+
const icons = { light: lightIcon, dark: darkIcon, auto: autoIcon };
|
|
7
12
|
return (_jsxs(DropdownMenuItem, { "data-testid": testId, onSelect: (event) => {
|
|
8
13
|
event.preventDefault();
|
|
9
|
-
|
|
10
|
-
}, children: [_jsx("span", { "aria-hidden": "true", children:
|
|
14
|
+
cyclePreference();
|
|
15
|
+
}, children: [_jsx("span", { "aria-hidden": "true", children: icons[nextPreference] }), _jsx("span", { children: title })] }));
|
|
11
16
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
export type ThemeToggleProps = {
|
|
3
|
-
/** Icon
|
|
4
|
-
* Klick wechselt zu light). Default: ☀ */
|
|
3
|
+
/** Icon for the step to light mode. Default: ☀ */
|
|
5
4
|
readonly lightIcon?: ReactNode;
|
|
6
|
-
/** Icon
|
|
7
|
-
* Klick wechselt zu dark). Default: ☾ */
|
|
5
|
+
/** Icon for the step to dark mode. Default: ☾ */
|
|
8
6
|
readonly darkIcon?: ReactNode;
|
|
9
|
-
/**
|
|
7
|
+
/** Icon for the step to auto mode. Default: ◐ */
|
|
8
|
+
readonly autoIcon?: ReactNode;
|
|
9
|
+
/** Title/aria-label for the step to light mode. Default: i18n `kumiko.theme.light` */
|
|
10
10
|
readonly titleInDark?: string;
|
|
11
|
-
/** Title/aria-label
|
|
11
|
+
/** Title/aria-label for the step to dark mode. Default: i18n `kumiko.theme.dark` */
|
|
12
12
|
readonly titleInLight?: string;
|
|
13
|
+
/** Title/aria-label for the step to auto mode. Default: i18n `kumiko.theme.auto` */
|
|
14
|
+
readonly titleForAuto?: string;
|
|
13
15
|
readonly testId?: string;
|
|
14
16
|
};
|
|
15
|
-
export declare function ThemeToggle({ lightIcon, darkIcon, titleInDark, titleInLight, testId, }: ThemeToggleProps): ReactNode;
|
|
17
|
+
export declare function ThemeToggle({ lightIcon, darkIcon, autoIcon, titleInDark, titleInLight, titleForAuto, testId, }: ThemeToggleProps): ReactNode;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// ThemeToggle — Button der
|
|
2
|
+
// ThemeToggle — Button der die Theme-Wahl weiterschaltet (hell → dunkel → auto).
|
|
3
3
|
// Icon-Slots als Props, damit renderer-web keine Icon-Lib als Hard-
|
|
4
4
|
// Dependency zieht. Default: Unicode-Glyphs (☀ / ☾) — funktionieren in
|
|
5
5
|
// jedem Browser, jede App kann lucide/heroicons/eigene SVG via Props
|
|
@@ -7,12 +7,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
import { useContext } from "react";
|
|
8
8
|
import { HeaderOverflowMenuContext, headerOverflowMenuItemClass } from "./header-overflow-menu.js";
|
|
9
9
|
import { useThemeToggleLabel } from "./theme-label.js";
|
|
10
|
-
export function ThemeToggle({ lightIcon = "☀", darkIcon = "☾", titleInDark, titleInLight, testId, }) {
|
|
10
|
+
export function ThemeToggle({ lightIcon = "☀", darkIcon = "☾", autoIcon = "◐", titleInDark, titleInLight, titleForAuto, testId, }) {
|
|
11
11
|
const inOverflowMenu = useContext(HeaderOverflowMenuContext);
|
|
12
|
-
const {
|
|
13
|
-
|
|
12
|
+
const { nextPreference, title, cyclePreference } = useThemeToggleLabel({
|
|
13
|
+
titleInDark,
|
|
14
|
+
titleInLight,
|
|
15
|
+
titleForAuto,
|
|
16
|
+
});
|
|
17
|
+
const icon = { light: lightIcon, dark: darkIcon, auto: autoIcon }[nextPreference];
|
|
14
18
|
if (inOverflowMenu) {
|
|
15
|
-
return (_jsxs("button", { type: "button", role: "menuitem", onClick:
|
|
19
|
+
return (_jsxs("button", { type: "button", role: "menuitem", onClick: cyclePreference, className: headerOverflowMenuItemClass, "data-testid": testId, children: [_jsx("span", { "aria-hidden": "true", children: icon }), _jsx("span", { children: title })] }));
|
|
16
20
|
}
|
|
17
|
-
return (_jsx("button", { type: "button", onClick:
|
|
21
|
+
return (_jsx("button", { type: "button", onClick: cyclePreference, className: "inline-flex h-8 w-8 items-center justify-center rounded-md border bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", title: title, "aria-label": title, "data-testid": testId, children: icon }));
|
|
18
22
|
}
|
|
@@ -3,7 +3,10 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
|
3
3
|
export declare const DropdownMenu: import("react").FC<Primitive.DropdownMenuProps>;
|
|
4
4
|
export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<Primitive.DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
5
5
|
export declare const DropdownMenuPortal: import("react").FC<Primitive.DropdownMenuPortalProps>;
|
|
6
|
-
export declare function DropdownMenuContent({ className, sideOffset, ...props }: ComponentPropsWithoutRef<typeof Primitive.Content>
|
|
6
|
+
export declare function DropdownMenuContent({ className, sideOffset, portalContainer, ...props }: ComponentPropsWithoutRef<typeof Primitive.Content> & {
|
|
7
|
+
/** Mount point of the portal. Default: document.body, which sits outside every landmark. */
|
|
8
|
+
readonly portalContainer?: HTMLElement | null;
|
|
9
|
+
}): ReactNode;
|
|
7
10
|
export declare function DropdownMenuItem({ className, ...props }: ComponentPropsWithoutRef<typeof Primitive.Item>): ReactNode;
|
|
8
11
|
export declare function DropdownMenuCheckboxItem({ className, children, checked, ...props }: ComponentPropsWithoutRef<typeof Primitive.CheckboxItem>): ReactNode;
|
|
9
12
|
export declare const DropdownMenuRadioGroup: import("react").ForwardRefExoticComponent<Primitive.DropdownMenuRadioGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -23,8 +23,8 @@ const contentClass = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-pop
|
|
|
23
23
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 " +
|
|
24
24
|
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 " +
|
|
25
25
|
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2";
|
|
26
|
-
export function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|
27
|
-
return (_jsx(Primitive.Portal, { children: _jsx(Primitive.Content, { sideOffset: sideOffset, className: cn(contentClass, className), ...props }) }));
|
|
26
|
+
export function DropdownMenuContent({ className, sideOffset = 4, portalContainer, ...props }) {
|
|
27
|
+
return (_jsx(Primitive.Portal, { container: portalContainer, children: _jsx(Primitive.Content, { sideOffset: sideOffset, className: cn(contentClass, className), ...props }) }));
|
|
28
28
|
}
|
|
29
29
|
const itemClass = "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm " +
|
|
30
30
|
"outline-none transition-colors " +
|
package/dist/primitives/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { applyFormatSpec, isSafeHref } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import { InsideDrawerProvider, needsActionConfirm, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useColumnRenderer, useInsideDrawer, useOptionalLocale, useOptionalTranslation, useTranslation, WriteFailedError, } from "@cosmicdrift/kumiko-renderer";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
|
-
import { ArrowDown, ArrowUp, ArrowUpDown, Building, CalendarDays, ChevronDown, ChevronLeft, ChevronRight, Globe, Hash, KeyRound, Link, Loader2, Lock, Mail, MapPin, MoreHorizontal, Phone, Search, SlidersHorizontal, Tag, Undo2, User, X, } from "lucide-react";
|
|
5
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, Building, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, Globe, Hash, KeyRound, Link, Loader2, Lock, Mail, MapPin, MoreHorizontal, Phone, Search, SlidersHorizontal, Tag, Undo2, User, X, } from "lucide-react";
|
|
6
6
|
// The main entry pulls Node-only deps (yargs/pngjs) under bundlers that ignore
|
|
7
7
|
// package.json#browser (Metro), hence the explicit browser subpath.
|
|
8
8
|
import QRCode from "qrcode/lib/browser.js";
|
|
@@ -144,7 +144,7 @@ const DrawerBodyContext = createContext(false);
|
|
|
144
144
|
function DefaultField({ id, label, required, issues, labelAppendix, fieldAppendix, appendixPlacement, description, status, children, layout, accent, hideLabel, testId, changed, }) {
|
|
145
145
|
const t = useTranslation();
|
|
146
146
|
const hasError = issues !== undefined && issues.length > 0;
|
|
147
|
-
const labelEl = (_jsxs(UiLabel, { id: fieldLabelId(id), htmlFor: id, className: cn("min-w-0 gap-0 text-[13px]", hasError ? "text-destructive" : "text-foreground", hideLabel === true && "sr-only"), children: [_jsx("span", { className: "truncate", title: typeof label === "string" ? label : undefined, children: label }), required === true && (_jsx("span", { "data-required": true, className: "ml-0.5 shrink-0 text-destructive", children: "*" }))] }));
|
|
147
|
+
const labelEl = (_jsxs(UiLabel, { id: fieldLabelId(id), htmlFor: id, className: cn("min-w-0 gap-0 text-[13px]", hasError ? "text-destructive" : "text-foreground", hideLabel === true && "sr-only"), children: [_jsx("span", { className: layout === "inline" ? undefined : "truncate", title: typeof label === "string" ? label : undefined, children: label }), required === true && (_jsx("span", { "data-required": true, className: "ml-0.5 shrink-0 text-destructive", children: "*" }))] }));
|
|
148
148
|
const errorsEl = hasError ? (_jsx("div", { role: "alert", "data-testid": testId !== undefined ? `${testId}-errors` : undefined, className: "text-xs text-destructive", children: issues.map((issue) => (_jsx("div", { children: t(issue.i18nKey, issue.params) }, `${issue.path}:${issue.code}`))) })) : null;
|
|
149
149
|
// Inline (boolean/checkbox): Control links, Label rechts — shadcn-Muster.
|
|
150
150
|
if (layout === "inline") {
|
|
@@ -787,8 +787,18 @@ function isEmptyCellValue(value) {
|
|
|
787
787
|
return (value === null || value === undefined || (typeof value === "string" && value.trim() === ""));
|
|
788
788
|
}
|
|
789
789
|
// A boolean false renders as an empty string unless the column's format spec names a falseLabel.
|
|
790
|
+
// Projection lists synthesize every column as type "text", so a boolean format spec
|
|
791
|
+
// marks a boolean column on its own.
|
|
792
|
+
function isBooleanColumn(type, renderer) {
|
|
793
|
+
if (type === "boolean")
|
|
794
|
+
return true;
|
|
795
|
+
return (typeof renderer === "object" &&
|
|
796
|
+
renderer !== null &&
|
|
797
|
+
"format" in renderer &&
|
|
798
|
+
renderer.format === "boolean");
|
|
799
|
+
}
|
|
790
800
|
function isUnlabeledFalse(type, value, renderer) {
|
|
791
|
-
if (
|
|
801
|
+
if (value !== false || !isBooleanColumn(type, renderer))
|
|
792
802
|
return false;
|
|
793
803
|
if (renderer === undefined)
|
|
794
804
|
return true;
|
|
@@ -798,10 +808,24 @@ function isUnlabeledFalse(type, value, renderer) {
|
|
|
798
808
|
renderer.format === "boolean" &&
|
|
799
809
|
!("falseLabel" in renderer));
|
|
800
810
|
}
|
|
811
|
+
function isUnlabeledBooleanFormat(renderer) {
|
|
812
|
+
return (typeof renderer === "object" &&
|
|
813
|
+
renderer !== null &&
|
|
814
|
+
"format" in renderer &&
|
|
815
|
+
renderer.format === "boolean" &&
|
|
816
|
+
!("trueLabel" in renderer) &&
|
|
817
|
+
!("falseLabel" in renderer));
|
|
818
|
+
}
|
|
819
|
+
function BooleanCell({ value, translate, }) {
|
|
820
|
+
const label = value
|
|
821
|
+
? (translate?.("kumiko.boolean.yes") ?? "Yes")
|
|
822
|
+
: (translate?.("kumiko.boolean.no") ?? "No");
|
|
823
|
+
return (_jsxs("span", { "data-boolean-cell": value ? "true" : "false", children: [value ? (_jsx(Check, { "aria-hidden": "true", className: "h-4 w-4" })) : (_jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: EMPTY_CELL_PLACEHOLDER })), _jsx("span", { className: "sr-only", children: label })] }));
|
|
824
|
+
}
|
|
801
825
|
// True when the cell would render the default "✓": no renderer, or a boolean
|
|
802
826
|
// format spec that names no trueLabel of its own.
|
|
803
827
|
function showsBareCheckMark(type, value, renderer) {
|
|
804
|
-
if (
|
|
828
|
+
if (value !== true || !isBooleanColumn(type, renderer))
|
|
805
829
|
return false;
|
|
806
830
|
if (renderer === undefined)
|
|
807
831
|
return true;
|
|
@@ -1162,13 +1186,13 @@ function humanizeSlug(slug) {
|
|
|
1162
1186
|
}
|
|
1163
1187
|
// Tooltip-Text für truncated Cells — bei Hover zeigt der Browser den
|
|
1164
1188
|
// vollen Text. Skipping für Object/Array (das ist nicht user-readable);
|
|
1165
|
-
// Number
|
|
1189
|
+
// Number stringifyt der Browser ohnehin korrekt; booleans get no raw true/false tooltip.
|
|
1166
1190
|
function cellTitle(value) {
|
|
1167
1191
|
if (value === null || value === undefined)
|
|
1168
1192
|
return undefined;
|
|
1169
1193
|
if (typeof value === "string")
|
|
1170
1194
|
return value.length > 0 ? value : undefined;
|
|
1171
|
-
if (typeof value === "number"
|
|
1195
|
+
if (typeof value === "number")
|
|
1172
1196
|
return String(value);
|
|
1173
1197
|
return undefined;
|
|
1174
1198
|
}
|
|
@@ -1228,6 +1252,9 @@ function DataTableCell({ value, row, field, type, renderer, optionLabels, option
|
|
|
1228
1252
|
if (isEmptyCellValue(value) && typeof renderer !== "function" && componentRef === undefined) {
|
|
1229
1253
|
return (_jsx("span", { "data-empty-cell": "true", className: "text-muted-foreground", children: EMPTY_CELL_PLACEHOLDER }));
|
|
1230
1254
|
}
|
|
1255
|
+
if (typeof value === "boolean" && isUnlabeledBooleanFormat(renderer)) {
|
|
1256
|
+
return _jsx(BooleanCell, { value: value, translate: translate });
|
|
1257
|
+
}
|
|
1231
1258
|
if (isUnlabeledFalse(type, value, renderer)) {
|
|
1232
1259
|
return (_jsx("span", { "data-empty-cell": "true", className: "text-muted-foreground", children: EMPTY_CELL_PLACEHOLDER }));
|
|
1233
1260
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { LightboxProps } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
export declare function DefaultLightbox(
|
|
3
|
+
export declare function DefaultLightbox(props: LightboxProps): ReactNode;
|
|
@@ -1,9 +1,35 @@
|
|
|
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 { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
5
|
import { cn } from "../lib/cn.js";
|
|
5
6
|
import { ModalShell } from "./modal-shell.js";
|
|
6
|
-
|
|
7
|
+
const NAV_BUTTON_CLASS = "absolute top-1/2 -translate-y-1/2 flex size-9 items-center justify-center rounded-full border border-border bg-card text-foreground opacity-80 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring";
|
|
8
|
+
export function DefaultLightbox(props) {
|
|
9
|
+
const { open, onOpenChange, testId } = props;
|
|
7
10
|
const t = useTranslation();
|
|
8
|
-
|
|
11
|
+
const images = "images" in props ? props.images : [{ src: props.src, alt: props.alt }];
|
|
12
|
+
const onIndexChange = "images" in props ? props.onIndexChange : undefined;
|
|
13
|
+
const total = images.length;
|
|
14
|
+
// Clamped here so a stale index after the image set shrinks never renders blank.
|
|
15
|
+
const requestedIndex = "images" in props ? props.index : 0;
|
|
16
|
+
const index = Math.min(Math.max(requestedIndex, 0), total - 1);
|
|
17
|
+
const current = images[index];
|
|
18
|
+
if (current === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
const canNavigate = onIndexChange !== undefined && total > 1;
|
|
21
|
+
const goTo = (target) => {
|
|
22
|
+
onIndexChange?.((target + total) % total);
|
|
23
|
+
};
|
|
24
|
+
const handleKeyDown = (event) => {
|
|
25
|
+
if (event.key === "ArrowRight") {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
goTo(index + 1);
|
|
28
|
+
}
|
|
29
|
+
else if (event.key === "ArrowLeft") {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
goTo(index - 1);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs(ModalShell, { open: open, onOpenChange: onOpenChange, testId: testId, closeLabel: t("kumiko.dialog.close"), noAriaDescription: true, ...(canNavigate && { onContentKeyDown: handleKeyDown }), contentClassName: cn("border-0 bg-transparent p-0 shadow-none", "max-w-[95vw] max-h-[90vh] w-auto"), children: [_jsx(DialogPrimitive.Title, { className: "sr-only", children: current.alt }), _jsx("img", { src: current.src, alt: current.alt, className: "block max-h-[85vh] max-w-[90vw] w-auto rounded-lg border border-border bg-card shadow-lg" }), canNavigate && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "aria-label": t("kumiko.lightbox.previous"), onClick: () => goTo(index - 1), className: cn(NAV_BUTTON_CLASS, "left-2"), children: _jsx(ChevronLeft, { className: "size-5" }) }), _jsx("button", { type: "button", "aria-label": t("kumiko.lightbox.next"), onClick: () => goTo(index + 1), className: cn(NAV_BUTTON_CLASS, "right-2"), children: _jsx(ChevronRight, { className: "size-5" }) }), _jsx("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-card px-3 py-1 text-xs text-foreground shadow-sm", children: t("kumiko.lightbox.position", { current: index + 1, total }) })] }))] }));
|
|
9
35
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import type { KeyboardEvent, ReactNode } from "react";
|
|
2
2
|
export type ModalShellProps = {
|
|
3
3
|
readonly open: boolean;
|
|
4
4
|
readonly onOpenChange: (open: boolean) => void;
|
|
@@ -12,5 +12,6 @@ export type ModalShellProps = {
|
|
|
12
12
|
/** Lets the caller take over Radix's initial-focus target — call
|
|
13
13
|
* `event.preventDefault()` and focus the desired element yourself. */
|
|
14
14
|
readonly onOpenAutoFocus?: (event: Event) => void;
|
|
15
|
+
readonly onContentKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
15
16
|
};
|
|
16
|
-
export declare function ModalShell({ open, onOpenChange, children, testId, contentClassName, noAriaDescription, closeLabel, showCloseButton, onOpenAutoFocus, }: ModalShellProps): ReactNode;
|
|
17
|
+
export declare function ModalShell({ open, onOpenChange, children, testId, contentClassName, noAriaDescription, closeLabel, showCloseButton, onOpenAutoFocus, onContentKeyDown, }: ModalShellProps): ReactNode;
|
|
@@ -4,6 +4,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/cn.js";
|
|
7
|
-
export function ModalShell({ open, onOpenChange, children, testId, contentClassName, noAriaDescription, closeLabel, showCloseButton = true, onOpenAutoFocus, }) {
|
|
8
|
-
return (_jsx(DialogPrimitive.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Overlay, { className: "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" }), _jsxs(DialogPrimitive.Content, { "data-testid": testId, ...(noAriaDescription && { "aria-describedby": undefined }), ...(onOpenAutoFocus !== undefined && { onOpenAutoFocus }), className: cn("fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", contentClassName), children: [children, showCloseButton && closeLabel !== undefined && (_jsx(DialogPrimitive.Close, { asChild: true, children: _jsx("button", { type: "button", "aria-label": closeLabel, className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none", children: _jsx(X, { className: "size-4" }) }) }))] })] }) }));
|
|
7
|
+
export function ModalShell({ open, onOpenChange, children, testId, contentClassName, noAriaDescription, closeLabel, showCloseButton = true, onOpenAutoFocus, onContentKeyDown, }) {
|
|
8
|
+
return (_jsx(DialogPrimitive.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Overlay, { className: "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" }), _jsxs(DialogPrimitive.Content, { "data-testid": testId, ...(noAriaDescription && { "aria-describedby": undefined }), ...(onOpenAutoFocus !== undefined && { onOpenAutoFocus }), ...(onContentKeyDown !== undefined && { onKeyDown: onContentKeyDown }), className: cn("fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", contentClassName), children: [children, showCloseButton && closeLabel !== undefined && (_jsx(DialogPrimitive.Close, { asChild: true, children: _jsx("button", { type: "button", "aria-label": closeLabel, className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none", children: _jsx(X, { className: "size-4" }) }) }))] })] }) }));
|
|
9
9
|
}
|
package/dist/theme-plugin.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import plugin from "tailwindcss/plugin";
|
|
2
|
+
import { type DefaultColorScheme } from "./default-color-scheme.js";
|
|
2
3
|
export declare const FRAMEWORK_COLOR_NAMES: readonly ["background", "foreground", "foreground-secondary", "foreground-disabled", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "secondary", "secondary-foreground", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "border", "border-strong", "border-row", "input", "ring", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-muted", "sidebar-input", "sidebar-border", "sidebar-ring", "status-ok", "status-ok-surface", "status-warn", "status-warn-surface", "status-bad", "status-bad-surface", "status-critical", "status-critical-surface", "status-neutral", "status-neutral-surface", "status-active", "status-active-surface", "syntax-key", "syntax-string", "syntax-number", "syntax-literal", "promo", "promo-foreground", "promo-accent", "promo-accent-foreground"];
|
|
3
4
|
export type FrameworkColorName = (typeof FRAMEWORK_COLOR_NAMES)[number];
|
|
4
5
|
/** A plain string applies to both modes; `dark` falls back to `light`. */
|
|
@@ -7,6 +8,8 @@ export type ThemeColorValue = string | {
|
|
|
7
8
|
readonly dark?: string;
|
|
8
9
|
};
|
|
9
10
|
export type AppTheme = {
|
|
11
|
+
/** Scheme for visitors without a stored choice. "auto" follows the OS setting. */
|
|
12
|
+
readonly defaultColorScheme?: DefaultColorScheme;
|
|
10
13
|
readonly colors?: Readonly<Partial<Record<FrameworkColorName | (string & {}), ThemeColorValue>>>;
|
|
11
14
|
/** Base radius; the sm/md/lg/xl scale derives from it. */
|
|
12
15
|
readonly radius?: string;
|
package/dist/theme-plugin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import plugin from "tailwindcss/plugin";
|
|
2
|
+
import { DEFAULT_COLOR_SCHEME_VARIABLE } from "./default-color-scheme.js";
|
|
2
3
|
export const FRAMEWORK_COLOR_NAMES = [
|
|
3
4
|
"background",
|
|
4
5
|
"foreground",
|
|
@@ -87,6 +88,9 @@ function buildColorDeclarations(colors) {
|
|
|
87
88
|
}
|
|
88
89
|
function buildModeInvariantDeclarations(theme) {
|
|
89
90
|
const modeInvariant = {};
|
|
91
|
+
if (theme.defaultColorScheme !== undefined) {
|
|
92
|
+
modeInvariant[DEFAULT_COLOR_SCHEME_VARIABLE] = theme.defaultColorScheme;
|
|
93
|
+
}
|
|
90
94
|
if (theme.radius !== undefined)
|
|
91
95
|
modeInvariant["--radius"] = theme.radius;
|
|
92
96
|
if (theme.fonts?.sans !== undefined)
|
package/dist/tokens.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Tokens, type TokensApi } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
export declare const THEME_STORAGE_KEY = "kumiko:theme";
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
/** Reads the persisted theme choice (or the app default) and sets the `.dark`
|
|
4
|
+
* class. Called on the first useBrowserTokensApi mount; the inline script in
|
|
5
|
+
* the host HTML (see header comment) does the same synchronously before the
|
|
6
|
+
* first paint. */
|
|
7
7
|
export declare function applyStoredThemeMode(): void;
|
|
8
8
|
/** Nur für Tests: der once-per-page-load-Guard ist ein Module-Singleton —
|
|
9
9
|
* ohne Reset wäre der Mount-Restore-Pfad nach der ersten Render im
|
package/dist/tokens.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createStore } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import { cssVarTokens, } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
-
import { useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { DEFAULT_COLOR_SCHEME_VARIABLE } from "./default-color-scheme.js";
|
|
4
5
|
// Web-spezifische TokensApi-Impl. Theme-Toggle via `.dark`-Class auf
|
|
5
6
|
// <html>. Die echten Farben leben in styles.css; hier ist nur die
|
|
6
7
|
// JS-Seite die den class-switch triggert und React-Consumer mit
|
|
@@ -11,56 +12,105 @@ import { useState, useSyncExternalStore } from "react";
|
|
|
11
12
|
// jedem Class-Wechsel hochzählen. So bleibt die DOM-Klasse die einzige
|
|
12
13
|
// Wahrheit — readCurrentMode liest sie frisch bei jedem getSnapshot.
|
|
13
14
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// Persistence: the choice is stored in localStorage (THEME_STORAGE_KEY) and
|
|
16
|
+
// restored on the first hook mount; without it the toggle was lost on every
|
|
17
|
+
// reload (prod bug 2026-06-07).
|
|
18
|
+
// Preference: "auto" follows prefers-color-scheme live (matchMedia listener),
|
|
19
|
+
// "light"/"dark" are explicit choices and win. Without a stored choice,
|
|
20
|
+
// AppTheme.defaultColorScheme (CSS variable) applies, otherwise the HTML state.
|
|
19
21
|
//
|
|
20
|
-
//
|
|
22
|
+
// Against FOUC the host HTML also needs a synchronous inline script BEFORE
|
|
23
|
+
// the stylesheet link (nonce/hash under a strict CSP):
|
|
24
|
+
//
|
|
25
|
+
// <script>try{var s=localStorage.getItem("kumiko:theme");
|
|
26
|
+
// if(s==="dark"||(s==="auto"&&matchMedia("(prefers-color-scheme: dark)").matches))
|
|
21
27
|
// document.documentElement.classList.add("dark")}catch(e){}</script>
|
|
22
28
|
const themeTick = createStore(0);
|
|
23
29
|
export const THEME_STORAGE_KEY = "kumiko:theme";
|
|
30
|
+
const DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
|
|
31
|
+
function isThemePreference(value) {
|
|
32
|
+
return value === "auto" || value === "light" || value === "dark";
|
|
33
|
+
}
|
|
24
34
|
function readCurrentMode() {
|
|
25
35
|
if (typeof document === "undefined")
|
|
26
36
|
return "dark";
|
|
27
37
|
return document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
28
38
|
}
|
|
39
|
+
function systemPrefersDark() {
|
|
40
|
+
return typeof window.matchMedia === "function" && window.matchMedia(DARK_SCHEME_QUERY).matches;
|
|
41
|
+
}
|
|
42
|
+
function resolveMode(preference) {
|
|
43
|
+
if (preference === "auto")
|
|
44
|
+
return systemPrefersDark() ? "dark" : "light";
|
|
45
|
+
return preference;
|
|
46
|
+
}
|
|
47
|
+
function readDefaultPreference() {
|
|
48
|
+
const declared = getComputedStyle(document.documentElement)
|
|
49
|
+
.getPropertyValue(DEFAULT_COLOR_SCHEME_VARIABLE)
|
|
50
|
+
.trim();
|
|
51
|
+
return isThemePreference(declared) ? declared : readCurrentMode();
|
|
52
|
+
}
|
|
53
|
+
function readStoredPreference() {
|
|
54
|
+
try {
|
|
55
|
+
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
56
|
+
return isThemePreference(stored) ? stored : undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// skip: localStorage can throw (private mode); without a stored choice
|
|
60
|
+
// the default stays.
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Module singleton like storedModeApplied: null until the first read, so the
|
|
65
|
+
// import stays safe without a DOM (SSR/tests).
|
|
66
|
+
let currentPreference = null;
|
|
67
|
+
function readCurrentPreference() {
|
|
68
|
+
if (currentPreference === null) {
|
|
69
|
+
if (typeof document === "undefined")
|
|
70
|
+
return "dark";
|
|
71
|
+
currentPreference = readStoredPreference() ?? readDefaultPreference();
|
|
72
|
+
}
|
|
73
|
+
return currentPreference;
|
|
74
|
+
}
|
|
29
75
|
// @wrapper-known semantic-alias
|
|
30
76
|
function notifyThemeChange() {
|
|
31
77
|
themeTick.setState((t) => t + 1);
|
|
32
78
|
}
|
|
33
79
|
// @wrapper-known semantic-alias
|
|
34
|
-
function
|
|
80
|
+
function persistPreference(preference) {
|
|
35
81
|
try {
|
|
36
|
-
window.localStorage.setItem(THEME_STORAGE_KEY,
|
|
82
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, preference);
|
|
37
83
|
}
|
|
38
84
|
catch {
|
|
39
85
|
// skip: localStorage kann werfen (Private-Mode/Quota) — Theme bleibt
|
|
40
86
|
// dann sessionbasiert, der Class-Toggle hat trotzdem funktioniert.
|
|
41
87
|
}
|
|
42
88
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
function applyPreference(preference) {
|
|
90
|
+
currentPreference = preference;
|
|
91
|
+
document.documentElement.classList.toggle("dark", resolveMode(preference) === "dark");
|
|
92
|
+
notifyThemeChange();
|
|
93
|
+
}
|
|
94
|
+
/** Reads the persisted theme choice (or the app default) and sets the `.dark`
|
|
95
|
+
* class. Called on the first useBrowserTokensApi mount; the inline script in
|
|
96
|
+
* the host HTML (see header comment) does the same synchronously before the
|
|
97
|
+
* first paint. */
|
|
47
98
|
export function applyStoredThemeMode() {
|
|
48
99
|
// skip: no document (SSR/non-DOM context), nothing to apply
|
|
49
100
|
if (typeof document === "undefined")
|
|
50
101
|
return;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
stored
|
|
54
|
-
|
|
55
|
-
catch {
|
|
56
|
-
// skip: localStorage kann werfen (Private-Mode) — ohne gespeicherte
|
|
57
|
-
// Wahl bleibt der Server-/HTML-Default stehen.
|
|
58
|
-
}
|
|
59
|
-
// skip: no valid persisted mode stored, keep server/HTML default
|
|
60
|
-
if (stored !== "dark" && stored !== "light")
|
|
102
|
+
const stored = readStoredPreference();
|
|
103
|
+
if (stored !== undefined) {
|
|
104
|
+
applyPreference(stored);
|
|
105
|
+
// skip: a stored choice wins over the app default
|
|
61
106
|
return;
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
}
|
|
108
|
+
// Without a stored choice only an app-declared default changes the HTML state.
|
|
109
|
+
const declared = getComputedStyle(document.documentElement)
|
|
110
|
+
.getPropertyValue(DEFAULT_COLOR_SCHEME_VARIABLE)
|
|
111
|
+
.trim();
|
|
112
|
+
if (isThemePreference(declared))
|
|
113
|
+
applyPreference(declared);
|
|
64
114
|
}
|
|
65
115
|
let storedModeApplied = false;
|
|
66
116
|
/** Nur für Tests: der once-per-page-load-Guard ist ein Module-Singleton —
|
|
@@ -68,6 +118,7 @@ let storedModeApplied = false;
|
|
|
68
118
|
* Testfile strukturell unerreichbar. */
|
|
69
119
|
export function __resetStoredModeAppliedForTests() {
|
|
70
120
|
storedModeApplied = false;
|
|
121
|
+
currentPreference = null;
|
|
71
122
|
}
|
|
72
123
|
/** Hook der eine TokensApi für den Browser baut. Wird von
|
|
73
124
|
* createKumikoApp genutzt; App-Code der einen eigenen Token-State
|
|
@@ -87,24 +138,47 @@ export function useBrowserTokensApi() {
|
|
|
87
138
|
return null;
|
|
88
139
|
});
|
|
89
140
|
const mode = useSyncExternalStore(themeTick.subscribe, readCurrentMode, () => "dark");
|
|
141
|
+
const preference = useSyncExternalStore(themeTick.subscribe, readCurrentPreference, () => "dark");
|
|
142
|
+
// kumiko-lint-ignore no-raw-hooks listener lifecycle, no data fetching
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
// skip: without matchMedia, auto keeps the mode resolved at apply time
|
|
145
|
+
if (typeof window.matchMedia !== "function")
|
|
146
|
+
return;
|
|
147
|
+
const query = window.matchMedia(DARK_SCHEME_QUERY);
|
|
148
|
+
const followSystem = () => {
|
|
149
|
+
if (readCurrentPreference() === "auto")
|
|
150
|
+
applyPreference("auto");
|
|
151
|
+
};
|
|
152
|
+
// The OS scheme may have changed between the first apply and this mount.
|
|
153
|
+
followSystem();
|
|
154
|
+
query.addEventListener("change", followSystem);
|
|
155
|
+
return () => query.removeEventListener("change", followSystem);
|
|
156
|
+
}, []);
|
|
90
157
|
return {
|
|
91
158
|
tokens: cssVarTokens,
|
|
92
159
|
mode,
|
|
160
|
+
preference,
|
|
161
|
+
setPreference: (next) => {
|
|
162
|
+
// skip: no document (SSR/non-DOM context), nothing to apply
|
|
163
|
+
if (typeof document === "undefined")
|
|
164
|
+
return;
|
|
165
|
+
persistPreference(next);
|
|
166
|
+
applyPreference(next);
|
|
167
|
+
},
|
|
93
168
|
setMode: (next) => {
|
|
94
169
|
// skip: no document (SSR/non-DOM context), nothing to toggle
|
|
95
170
|
if (typeof document === "undefined")
|
|
96
171
|
return;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
notifyThemeChange();
|
|
172
|
+
persistPreference(next);
|
|
173
|
+
applyPreference(next);
|
|
100
174
|
},
|
|
101
175
|
toggleMode: () => {
|
|
102
176
|
// skip: no document (SSR/non-DOM context), nothing to toggle
|
|
103
177
|
if (typeof document === "undefined")
|
|
104
178
|
return;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
179
|
+
const next = readCurrentMode() === "dark" ? "light" : "dark";
|
|
180
|
+
persistPreference(next);
|
|
181
|
+
applyPreference(next);
|
|
108
182
|
},
|
|
109
183
|
};
|
|
110
184
|
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
type KumikoBuild = {
|
|
2
|
+
export type KumikoBuild = {
|
|
3
3
|
readonly id: string;
|
|
4
4
|
readonly builtAt: string;
|
|
5
5
|
};
|
|
6
|
-
declare
|
|
7
|
-
interface Window {
|
|
8
|
-
__KUMIKO_BUILD__?: KumikoBuild;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
6
|
+
export declare function readLoadedBuild(): KumikoBuild | undefined;
|
|
11
7
|
export declare function shouldShowUpdate(loadedId: string | undefined, server: KumikoBuild | null): boolean;
|
|
12
8
|
export declare function isKumikoBuild(v: unknown): v is Pick<KumikoBuild, "id">;
|
|
13
9
|
export declare function UpdateChecker(): ReactNode;
|
|
14
|
-
export {};
|
|
@@ -11,6 +11,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
12
12
|
import { useEffect, useState } from "react";
|
|
13
13
|
import { cn } from "../lib/cn.js";
|
|
14
|
+
export function readLoadedBuild() {
|
|
15
|
+
if (typeof document === "undefined")
|
|
16
|
+
return undefined;
|
|
17
|
+
const meta = document.querySelector('meta[name="kumiko-build"]');
|
|
18
|
+
const id = meta?.getAttribute("content");
|
|
19
|
+
if (!id)
|
|
20
|
+
return undefined;
|
|
21
|
+
return { id, builtAt: meta?.getAttribute("data-built-at") ?? "" };
|
|
22
|
+
}
|
|
14
23
|
// Korrektheitsgrenze der Update-Erkennung: ein Banner NUR bei echtem
|
|
15
24
|
// ID-Drift. Kein geladener Stand (Dev/altes Bundle) oder kein Server-Stand
|
|
16
25
|
// (Fetch-Fehler/kaputtes JSON → null) → nie ein Fake-Banner.
|
|
@@ -43,7 +52,7 @@ async function fetchServerBuild() {
|
|
|
43
52
|
}
|
|
44
53
|
export function UpdateChecker() {
|
|
45
54
|
const t = useTranslation();
|
|
46
|
-
const loaded =
|
|
55
|
+
const loaded = readLoadedBuild();
|
|
47
56
|
const [hasUpdate, setHasUpdate] = useState(false);
|
|
48
57
|
useEffect(() => {
|
|
49
58
|
const loadedId = loaded?.id;
|
package/dist/widgets/charts.d.ts
CHANGED
|
@@ -35,10 +35,17 @@ 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 TimeseriesReferenceLine = {
|
|
39
|
+
/** Same unit as the point values (e.g. a p95 or SLO threshold in ms). */
|
|
40
|
+
readonly value: number;
|
|
41
|
+
/** Translated by the caller. */
|
|
42
|
+
readonly label: string;
|
|
43
|
+
readonly tone?: StatusTone;
|
|
44
|
+
};
|
|
38
45
|
/** Zeitreihen-Linien-Chart (geglättete Linie + Flächen-Verlauf). x-Achse =
|
|
39
46
|
* ZEIT im Fenster windowStartMs..windowEndMs, nicht Index — 5 Min Daten in
|
|
40
47
|
* einem 30-Tage-Fenster ergeben ehrlich einen schmalen Streifen rechts. */
|
|
41
|
-
export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, tone, ariaLabel, axisLabels, markers, formatMarkerTime, emptyContent, testId, }: {
|
|
48
|
+
export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, tone, ariaLabel, axisLabels, markers, formatMarkerTime, referenceLines, emptyContent, testId, }: {
|
|
42
49
|
readonly points: readonly TimeseriesPoint[];
|
|
43
50
|
readonly windowStartMs: number;
|
|
44
51
|
readonly windowEndMs: number;
|
|
@@ -53,6 +60,9 @@ export declare function TimeseriesChart({ points, windowStartMs, windowEndMs, to
|
|
|
53
60
|
/** Numbered pins on the x-axis plus a numbered list below the chart. */
|
|
54
61
|
readonly markers?: readonly ChartMarker[];
|
|
55
62
|
readonly formatMarkerTime?: (atMs: number) => string;
|
|
63
|
+
/** Dashed horizontal threshold lines (e.g. p95); they extend the y-scale
|
|
64
|
+
* when above the data maximum. Non-finite and negative values are dropped. */
|
|
65
|
+
readonly referenceLines?: readonly TimeseriesReferenceLine[];
|
|
56
66
|
/** Rendert statt des Charts wenn <2 Messwerte vorliegen. */
|
|
57
67
|
readonly emptyContent?: ReactNode;
|
|
58
68
|
readonly testId?: string;
|
package/dist/widgets/charts.js
CHANGED
|
@@ -64,20 +64,23 @@ export function smoothPath(pts) {
|
|
|
64
64
|
/** Zeitreihen-Linien-Chart (geglättete Linie + Flächen-Verlauf). x-Achse =
|
|
65
65
|
* ZEIT im Fenster windowStartMs..windowEndMs, nicht Index — 5 Min Daten in
|
|
66
66
|
* einem 30-Tage-Fenster ergeben ehrlich einen schmalen Streifen rechts. */
|
|
67
|
-
export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok", ariaLabel, axisLabels, markers, formatMarkerTime, emptyContent, testId, }) {
|
|
67
|
+
export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok", ariaLabel, axisLabels, markers, formatMarkerTime, referenceLines, emptyContent, testId, }) {
|
|
68
68
|
const gradientId = useId();
|
|
69
|
+
const descId = useId();
|
|
69
70
|
const chartWidth = 300;
|
|
70
71
|
const chartHeight = 64;
|
|
71
72
|
const values = points.map((p) => p.value).filter((v) => v !== null);
|
|
72
73
|
if (values.length < 2) {
|
|
73
74
|
return (_jsx("div", { className: "flex h-16 items-center justify-center text-[13px] text-muted-foreground", children: emptyContent }));
|
|
74
75
|
}
|
|
75
|
-
const
|
|
76
|
+
const drawableLines = (referenceLines ?? []).filter((line) => Number.isFinite(line.value) && line.value >= 0);
|
|
77
|
+
const maxValue = Math.max(...values, ...drawableLines.map((line) => line.value), 1);
|
|
78
|
+
const yOf = (value) => chartHeight - (value / maxValue) * chartHeight;
|
|
76
79
|
const span = Math.max(1, windowEndMs - windowStartMs);
|
|
77
80
|
const xOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span)) * chartWidth;
|
|
78
81
|
const chartPoints = points.map((p) => ({
|
|
79
82
|
x: xOf(p.atMs),
|
|
80
|
-
y: p.value === null ? chartHeight :
|
|
83
|
+
y: p.value === null ? chartHeight : yOf(p.value),
|
|
81
84
|
}));
|
|
82
85
|
const linePath = smoothPath(chartPoints);
|
|
83
86
|
const firstPoint = chartPoints[0];
|
|
@@ -86,7 +89,11 @@ export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok
|
|
|
86
89
|
? `${linePath} L ${lastPoint.x.toFixed(1)} ${chartHeight} L ${firstPoint.x.toFixed(1)} ${chartHeight} Z`
|
|
87
90
|
: "";
|
|
88
91
|
const color = TONE_VAR[tone];
|
|
89
|
-
|
|
92
|
+
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}`)))] }));
|
|
94
|
+
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
|
+
// A line near the top would push its label above the chart box.
|
|
96
|
+
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 }) })] }))] }));
|
|
90
97
|
}
|
|
91
98
|
const CHART_TONE_VAR = {
|
|
92
99
|
positive: "var(--color-status-ok)",
|
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, } 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, } 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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.339.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.339.0",
|
|
24
|
+
"@cosmicdrift/kumiko-framework": "0.339.0",
|
|
25
|
+
"@cosmicdrift/kumiko-headless": "0.339.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer": "0.339.0",
|
|
27
|
+
"@cosmicdrift/kumiko-types": "0.339.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.339.0"
|
|
76
76
|
},
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.339.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Columns with format boolean show a check mark with the accessible name Yes, or a dash with No, instead of raw true/false",
|
|
6
|
+
"detail": "format boolean renders a localized Yes/No instead of raw true/false",
|
|
7
|
+
"migration": "No code change needed."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.339.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "Inline field labels wrap instead of truncating",
|
|
13
|
+
"detail": "`Field layout=\"inline\"` no longer puts its label in a single truncated line. Long checkbox labels such as the checkout consent texts wrap, so the dialog stays inside the viewport and its buttons stay reachable.",
|
|
14
|
+
"migration": "keine"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.339.0",
|
|
18
|
+
"type": "fix",
|
|
19
|
+
"title": "The open LanguageSwitcher no longer hides the app root with aria-hidden and renders its menu inside the surrounding header landmark",
|
|
20
|
+
"detail": "LanguageSwitcher is axe-clean while open",
|
|
21
|
+
"migration": "No code change needed."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"version": "0.339.0",
|
|
25
|
+
"type": "improvement",
|
|
26
|
+
"title": "Theme preference auto follows the OS color scheme live; ThemeToggle and ThemeMenuItem step through light, dark and auto, a stored choice wins, and defineAppTheme accepts defaultColorScheme",
|
|
27
|
+
"detail": "Theme mode auto follows prefers-color-scheme",
|
|
28
|
+
"migration": "Apps with a theme-restore inline script in their host HTML must update it. The toggle can now store \"auto\", which an old script that only checks for \"dark\" treats as light, so dark-mode users see a light flash on load. Use the script from the comment in renderer-web tokens.ts, which also handles \"auto\" via matchMedia. Under a strict CSP, update the script hash or keep the nonce."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"version": "0.339.0",
|
|
32
|
+
"type": "improvement",
|
|
33
|
+
"title": "TimeseriesChart draws optional reference lines",
|
|
34
|
+
"detail": "`TimeseriesChart` accepts an optional `referenceLines` prop that draws dashed horizontal threshold lines (for example a p95 or an SLO target) with an accessible label. A line above the data maximum extends the y-scale.",
|
|
35
|
+
"migration": "No code change needed. Pass `referenceLines={[{ value, label, tone }]}` to draw a dashed threshold such as p95 or an SLO target."
|
|
36
|
+
},
|
|
2
37
|
{
|
|
3
38
|
"version": "0.337.0",
|
|
4
39
|
"type": "fix",
|