@cosmicdrift/kumiko-renderer-web 0.48.1 → 0.51.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.48.1",
3
+ "version": "0.51.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>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.45.0",
20
- "@cosmicdrift/kumiko-headless": "0.45.0",
21
- "@cosmicdrift/kumiko-renderer": "0.45.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.50.0",
20
+ "@cosmicdrift/kumiko-headless": "0.50.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.50.0",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -0,0 +1,43 @@
1
+ // AppLayout: pinnt den fill-Vertrag — Default ist der klassische
2
+ // min-h-screen-Seitenflow, fill schaltet auf h-screen + innen-scrollendes
3
+ // main (min-h-0). Plus die className/mainClassName-Erweiterungspunkte.
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { AppLayout } from "../layout/app-layout";
7
+ import { render } from "./test-utils";
8
+
9
+ function root(container: HTMLElement): HTMLElement {
10
+ const el = container.querySelector('[data-kumiko-layout="app"]');
11
+ if (el === null) throw new Error("AppLayout root not found");
12
+ return el as HTMLElement;
13
+ }
14
+
15
+ describe("AppLayout fill", () => {
16
+ test("Default → min-h-screen (Seiten-Scroll), kein fill-Marker", () => {
17
+ const { container } = render(<AppLayout>x</AppLayout>);
18
+ const el = root(container);
19
+ expect(el.className).toContain("min-h-screen");
20
+ expect(el.getAttribute("data-kumiko-fill")).toBeNull();
21
+ expect((container.querySelector("main") as HTMLElement).className).not.toContain("min-h-0");
22
+ });
23
+
24
+ test("fill → h-screen + main min-h-0 (Innen-Scroll), fill-Marker gesetzt", () => {
25
+ const { container } = render(<AppLayout fill>x</AppLayout>);
26
+ const el = root(container);
27
+ expect(el.className).not.toContain("min-h-screen");
28
+ expect(el.className).toContain("h-screen");
29
+ expect(el.getAttribute("data-kumiko-fill")).toBe("true");
30
+ expect((container.querySelector("main") as HTMLElement).className).toContain("min-h-0");
31
+ });
32
+
33
+ test("className/mainClassName werden an die Defaults angehängt", () => {
34
+ const { container } = render(
35
+ <AppLayout className="bg-gradient-test" mainClassName="main-test">
36
+ x
37
+ </AppLayout>,
38
+ );
39
+ expect(root(container).className).toContain("bg-gradient-test");
40
+ expect(root(container).className).toContain("flex");
41
+ expect((container.querySelector("main") as HTMLElement).className).toContain("main-test");
42
+ });
43
+ });
@@ -179,4 +179,34 @@ describe("NavTree", () => {
179
179
  // Items selbst bleibt sichtbar
180
180
  expect(screen.getByText("Items")).toBeTruthy();
181
181
  });
182
+
183
+ test("Nav-Eintrag mit bekanntem icon rendert ein Lucide-Icon, ohne icon den Dot", () => {
184
+ const schema = {
185
+ featureName: "showcase",
186
+ entities: {},
187
+ screens: [
188
+ { id: "dash", type: "entityList", entity: "x", columns: [] },
189
+ { id: "plain", type: "entityList", entity: "x", columns: [] },
190
+ ],
191
+ navs: [
192
+ { id: "dash", label: "Dash", screen: "dash", order: 10, icon: "dashboard" },
193
+ { id: "plain", label: "Plain", screen: "plain", order: 20 },
194
+ ],
195
+ } as FeatureSchema;
196
+ const { container } = render(<NavTree schema={schema} />);
197
+ // Flache Navigation ohne Sections → keine Chevrons. Genau EIN svg:
198
+ // das dashboard-Icon. Das icon-lose Item rendert den Dot (span, kein svg).
199
+ expect(container.querySelectorAll("svg").length).toBe(1);
200
+ });
201
+
202
+ test("unbekannter icon-Key fällt sauber auf den Dot zurück (kein svg)", () => {
203
+ const schema = {
204
+ featureName: "showcase",
205
+ entities: {},
206
+ screens: [{ id: "x", type: "entityList", entity: "x", columns: [] }],
207
+ navs: [{ id: "x", label: "X", screen: "x", order: 10, icon: "does-not-exist" }],
208
+ } as FeatureSchema;
209
+ const { container } = render(<NavTree schema={schema} />);
210
+ expect(container.querySelectorAll("svg").length).toBe(0);
211
+ });
182
212
  });
@@ -63,7 +63,7 @@ export function createBrowserLocaleResolver(
63
63
 
64
64
  return {
65
65
  translate: (key) => key,
66
- locale: () => store.getSnapshot(),
66
+ locale: () => store.getSnapshot(), // @wrapper-known semantic-alias
67
67
  timeZone: () => timeZone,
68
68
  subscribe: store.subscribe,
69
69
  setLocale: (next) => {
@@ -0,0 +1,170 @@
1
+ // End-to-end Settings-Hub visibility on the REAL boot path:
2
+ // createRegistry → buildAppSchema → buildNavRegistrySliceForApp → resolveNavigation.
3
+ // These are the actual functions the shell runs at boot; no stubs. Proves a
4
+ // config key declared with `mask` surfaces as a role-filtered nav tree that
5
+ // only shows inside its own synthetic "settings" workspace.
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import {
9
+ access,
10
+ buildAppSchema,
11
+ createRegistry,
12
+ createSystemConfig,
13
+ createTenantConfig,
14
+ createUserConfig,
15
+ defineFeature,
16
+ } from "@cosmicdrift/kumiko-framework/engine";
17
+ import { resolveNavigation } from "@cosmicdrift/kumiko-headless";
18
+ import { qualifyScreenId } from "@cosmicdrift/kumiko-renderer";
19
+ import { buildNavRegistrySliceForApp } from "../nav-tree";
20
+
21
+ const billing = defineFeature("billing", (r) => {
22
+ r.config({
23
+ keys: {
24
+ stripeKey: createTenantConfig("text", { mask: { title: "billing.stripe-key" } }),
25
+ // system-scope default write = ["system"] (internal actor) → human-hidden
26
+ platformFee: createSystemConfig("number", { mask: { title: "billing.platform-fee" } }),
27
+ },
28
+ });
29
+ });
30
+ const notify = defineFeature("notify", (r) => {
31
+ r.config({
32
+ keys: { digest: createUserConfig("boolean", { mask: { title: "notify.digest" } }) },
33
+ });
34
+ });
35
+ // A system-scope key that explicitly opts a HUMAN role into write → its
36
+ // settings entry becomes visible to that admin (the opt-in path).
37
+ const ops = defineFeature("ops", (r) => {
38
+ r.config({
39
+ keys: {
40
+ maintenanceMode: createSystemConfig("boolean", {
41
+ write: access.systemAdmin,
42
+ mask: { title: "ops.maintenance" },
43
+ }),
44
+ },
45
+ });
46
+ });
47
+ // A real work-workspace, so the app is in workspace (filter) mode.
48
+ const shell = defineFeature("shell", (r) => {
49
+ r.entity("thing", { fields: { label: { type: "text" } } });
50
+ r.screen({ id: "home", type: "entityList", entity: "thing", columns: ["label"] });
51
+ r.nav({ id: "home", label: "Home", screen: "home" });
52
+ r.workspace({ id: "main", label: "Main", nav: ["shell:nav:home"] });
53
+ });
54
+
55
+ const app = buildAppSchema(createRegistry([shell, billing, notify, ops]));
56
+
57
+ function navMembersOf(workspaceId: string): ReadonlySet<string> {
58
+ const ws = app.workspaces?.find((w) => w.definition.id === workspaceId);
59
+ if (ws === undefined) throw new Error(`no workspace "${workspaceId}"`);
60
+ return new Set(ws.navMembers);
61
+ }
62
+
63
+ function qualifiedNames(tree: ReturnType<typeof resolveNavigation>): string[] {
64
+ const out: string[] = [];
65
+ const walk = (nodes: ReturnType<typeof resolveNavigation>): void => {
66
+ for (const n of nodes) {
67
+ out.push(n.qualifiedName);
68
+ walk(n.children);
69
+ }
70
+ };
71
+ walk(tree);
72
+ return out;
73
+ }
74
+
75
+ // Mirrors the exact screen lookup KumikoScreen runs (kumiko-screen.tsx:102) —
76
+ // the REAL qualifyScreenId against the FeatureSchema's short screen ids. Proves
77
+ // a nav's screen-QN resolves to a renderable definition, not just that the ref
78
+ // string matches a convention.
79
+ function resolveScreenDef(qn: string) {
80
+ for (const f of app.features) {
81
+ const hit = f.screens.find((s) => qualifyScreenId(f.featureName, s.id) === qn);
82
+ if (hit !== undefined) return hit;
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ function screenRefsIn(tree: ReturnType<typeof resolveNavigation>): string[] {
88
+ const out: string[] = [];
89
+ const walk = (nodes: ReturnType<typeof resolveNavigation>): void => {
90
+ for (const n of nodes) {
91
+ if (n.screen !== undefined) out.push(n.screen);
92
+ walk(n.children);
93
+ }
94
+ };
95
+ walk(tree);
96
+ return out;
97
+ }
98
+
99
+ describe("Settings-Hub visibility — full boot pipeline", () => {
100
+ test("privileged user sees the human-relevant hub; system-internal keys stay hidden", () => {
101
+ const slice = buildNavRegistrySliceForApp(app, navMembersOf("settings"));
102
+ const tree = resolveNavigation({
103
+ source: slice,
104
+ user: { id: "u-1", roles: ["TenantAdmin", "SystemAdmin"] },
105
+ });
106
+ const names = qualifiedNames(tree);
107
+
108
+ expect(names).toContain("config:nav:audience-tenant");
109
+ expect(names).toContain("config:nav:audience-user");
110
+ // system audience surfaces ONLY because `ops` opted a human (SystemAdmin)
111
+ // into write; its child ops-system shows, billing's system-internal key
112
+ // (write ["system"]) stays hidden from the same admin.
113
+ expect(names).toContain("config:nav:audience-system");
114
+ expect(names).toContain("config:nav:ops-system");
115
+ expect(names).not.toContain("config:nav:billing-system");
116
+
117
+ // schema-level completeness: the hidden key IS generated (a system actor
118
+ // could resolve it) — it's gated at resolve-time, not omitted at build-time.
119
+ expect(navMembersOf("settings").has("config:nav:billing-system")).toBe(true);
120
+
121
+ // hierarchy: the billing-tenant screen hangs under the tenant audience
122
+ const tenantAudience = tree.find((n) => n.qualifiedName === "config:nav:audience-tenant");
123
+ expect(tenantAudience?.children.map((c) => c.qualifiedName)).toContain(
124
+ "config:nav:billing-tenant",
125
+ );
126
+ expect(tenantAudience?.children[0]?.screen).toBe("config:screen:billing-tenant");
127
+ });
128
+
129
+ test("anonymous user sees only the openToAll (user-scope) audience", () => {
130
+ const slice = buildNavRegistrySliceForApp(app, navMembersOf("settings"));
131
+ const tree = resolveNavigation({ source: slice }); // no user
132
+ const names = qualifiedNames(tree);
133
+
134
+ // digest is createUserConfig → write `all` → openToAll → visible to anyone
135
+ expect(names).toContain("config:nav:audience-user");
136
+ // admin/system audiences are role-gated → hidden from anonymous
137
+ expect(names).not.toContain("config:nav:audience-tenant");
138
+ expect(names).not.toContain("config:nav:audience-system");
139
+ });
140
+
141
+ test("every visible leaf nav resolves to a real configEdit screen (nav → screen → definition)", () => {
142
+ const slice = buildNavRegistrySliceForApp(app, navMembersOf("settings"));
143
+ const tree = resolveNavigation({
144
+ source: slice,
145
+ user: { id: "u-1", roles: ["TenantAdmin", "SystemAdmin"] },
146
+ });
147
+ const refs = screenRefsIn(tree);
148
+ expect(refs.length).toBeGreaterThan(0); // audiences have no screen; children do
149
+
150
+ // the short screen ids in the config FeatureSchema must qualify back to the
151
+ // exact QN the nav carries — qualifyScreenId is NOT idempotent, so a mismatch
152
+ // would render "Screen not found" on every settings click.
153
+ for (const qn of refs) {
154
+ expect(resolveScreenDef(qn)?.type).toBe("configEdit");
155
+ }
156
+ expect(resolveScreenDef("config:screen:billing-tenant")?.type).toBe("configEdit");
157
+ });
158
+
159
+ test("the work workspace does NOT leak the settings hub", () => {
160
+ const slice = buildNavRegistrySliceForApp(app, navMembersOf("main"));
161
+ const tree = resolveNavigation({
162
+ source: slice,
163
+ user: { id: "u-1", roles: ["TenantAdmin", "SystemAdmin"] },
164
+ });
165
+ const names = qualifiedNames(tree);
166
+
167
+ expect(names).toContain("shell:nav:home");
168
+ expect(names.some((n) => n.startsWith("config:nav:"))).toBe(false);
169
+ });
170
+ });
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
2
3
 
3
4
  // Linear-Pattern: Sidebar ist Geschwister von <Topbar+Main>-Spalte,
4
5
  // nimmt die volle Höhe. Wenn ein Topbar gesetzt ist, lebt er NUR über
@@ -12,23 +13,50 @@ export type AppLayoutProps = {
12
13
  readonly sidebar?: ReactNode;
13
14
  readonly children: ReactNode;
14
15
  readonly testId?: string;
16
+ /** Viewport-fit Shell. true → Wurzel = `h-screen` (fixe Viewport-Höhe),
17
+ * Sidebar/Topbar bleiben stehen, der Main-Bereich scrollt INNEN
18
+ * (`min-h-0` + `overflow-auto`). false (Default) → klassischer
19
+ * `min-h-screen`-Flow, die ganze Seite scrollt. Dashboard-artige Apps
20
+ * wollen `true`; eine öffentliche, lange Content-Seite eher `false`.
21
+ * Clippt nie — der Content scrollt in `main` statt im Body. */
22
+ readonly fill?: boolean;
23
+ /** Optionaler Klassen-Append an die Wurzel (eigener Hintergrund etc.).
24
+ * Erweitert die Defaults, ersetzt sie nicht (cn-merge). */
25
+ readonly className?: string;
26
+ /** Optionaler Klassen-Append an `<main>`. */
27
+ readonly mainClassName?: string;
15
28
  };
16
29
 
17
- export function AppLayout({ topbar, sidebar, children, testId }: AppLayoutProps): ReactNode {
30
+ export function AppLayout({
31
+ topbar,
32
+ sidebar,
33
+ children,
34
+ testId,
35
+ fill,
36
+ className,
37
+ mainClassName,
38
+ }: AppLayoutProps): ReactNode {
18
39
  return (
19
40
  <div
20
41
  data-testid={testId}
21
42
  data-kumiko-layout="app"
22
- className="flex min-h-screen flex-row bg-background text-foreground"
43
+ {...(fill === true && { "data-kumiko-fill": "true" })}
44
+ className={cn(
45
+ "flex flex-row bg-background text-foreground",
46
+ fill === true ? "h-screen" : "min-h-screen",
47
+ className,
48
+ )}
23
49
  >
24
50
  {sidebar}
25
- <div className="flex flex-1 min-w-0 flex-col">
51
+ <div className={cn("flex flex-1 min-w-0 flex-col", fill === true && "min-h-0")}>
26
52
  {topbar}
27
53
  {/* main hat KEIN Padding — Screens (Form, Liste, Demo-Pages)
28
54
  managen ihr Padding selber, damit Action-Bars edge-to-edge
29
55
  spannen können ohne Negative-Margin-Tricks die mit
30
56
  `position: sticky` kollidieren. */}
31
- <main className="flex-1 overflow-auto">{children}</main>
57
+ <main className={cn("flex-1 overflow-auto", fill === true && "min-h-0", mainClassName)}>
58
+ {children}
59
+ </main>
32
60
  </div>
33
61
  </div>
34
62
  );
@@ -44,6 +44,10 @@ export type DefaultAppShellProps = {
44
44
  /** Footer-Slot unten in der Sidebar — Profile-Row, Help-Link,
45
45
  * Plan-Banner. Klebt am unteren Rand via `mt-auto`. */
46
46
  readonly sidebarFooter?: ReactNode;
47
+ /** Viewport-fit Shell — durchgereicht an AppLayout. true → fixe
48
+ * Viewport-Höhe, Content scrollt innen statt der ganzen Seite.
49
+ * Default false (klassischer Seiten-Scroll). Siehe AppLayout.fill. */
50
+ readonly fill?: boolean;
47
51
  /** Screen-Content der in `main` gerendert wird. */
48
52
  readonly children: ReactNode;
49
53
  };
@@ -54,10 +58,12 @@ export function DefaultAppShell({
54
58
  user,
55
59
  sidebarActions,
56
60
  sidebarFooter,
61
+ fill,
57
62
  children,
58
63
  }: DefaultAppShellProps): ReactNode {
59
64
  return (
60
65
  <AppLayout
66
+ {...(fill !== undefined && { fill })}
61
67
  sidebar={
62
68
  <Sidebar
63
69
  header={brand}
@@ -16,11 +16,72 @@ import type { NavNode, NavRegistrySlice } from "@cosmicdrift/kumiko-headless";
16
16
  import { resolveNavigation } from "@cosmicdrift/kumiko-headless";
17
17
  import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
18
18
  import { lastSegment, toAppSchema, useNav, useTranslation } from "@cosmicdrift/kumiko-renderer";
19
- import { ChevronDown, ChevronRight } from "lucide-react";
19
+ import {
20
+ BarChart3,
21
+ Bell,
22
+ Calculator,
23
+ CalendarDays,
24
+ ChevronDown,
25
+ ChevronRight,
26
+ Coins,
27
+ CreditCard,
28
+ FileText,
29
+ Folder,
30
+ Gauge,
31
+ Home,
32
+ LayoutDashboard,
33
+ LineChart,
34
+ List,
35
+ PiggyBank,
36
+ Receipt,
37
+ Search,
38
+ Settings,
39
+ Shield,
40
+ Sparkles,
41
+ Table,
42
+ TrendingUp,
43
+ User,
44
+ Users,
45
+ Wallet,
46
+ Wand2,
47
+ } from "lucide-react";
20
48
  import { type ReactNode, useCallback, useMemo, useState } from "react";
21
49
  import { KumikoLink } from "../app/nav";
22
50
  import { cn } from "../lib/cn";
23
51
 
52
+ // Nav-Icon-Registry: ein Nav-Eintrag setzt `icon: "<key>"` (im r.nav-Decl),
53
+ // der Renderer mappt den symbolischen Key auf ein lucide-Component. Unknown
54
+ // Keys → kein Icon (sauberer Fallback auf den Dot, kein Boot-Fail). Spiegelt
55
+ // das NODE_ICONS-Muster vom Visual-Tree. App-Authors referenzieren nur diese
56
+ // Keys; Erweiterung = neuer Eintrag hier (eine Quelle, alle Apps).
57
+ const NAV_ICONS: Readonly<Record<string, typeof Folder>> = {
58
+ dashboard: LayoutDashboard,
59
+ gauge: Gauge,
60
+ list: List,
61
+ table: Table,
62
+ calculator: Calculator,
63
+ wallet: Wallet,
64
+ coins: Coins,
65
+ "credit-card": CreditCard,
66
+ "piggy-bank": PiggyBank,
67
+ receipt: Receipt,
68
+ chart: LineChart,
69
+ "bar-chart": BarChart3,
70
+ trending: TrendingUp,
71
+ sparkles: Sparkles,
72
+ wand: Wand2,
73
+ calendar: CalendarDays,
74
+ file: FileText,
75
+ folder: Folder,
76
+ home: Home,
77
+ bell: Bell,
78
+ shield: Shield,
79
+ settings: Settings,
80
+ users: Users,
81
+ user: User,
82
+ search: Search,
83
+ };
84
+
24
85
  export type NavTreeProps = {
25
86
  // Akzeptiert beide Shapes — AppSchema (multi-feature) oder
26
87
  // FeatureSchema (legacy single-feature). toAppSchema normalisiert
@@ -82,6 +143,7 @@ function NavNodeItem({ node, depth, collapsed, onToggle }: NavNodeItemProps): Re
82
143
  const t = useTranslation();
83
144
  const active = node.screen !== undefined && nav.route?.screenId === lastSegment(node.screen);
84
145
 
146
+ const NavIcon = node.icon !== undefined ? NAV_ICONS[node.icon] : undefined;
85
147
  const hasChildren = node.children.length > 0;
86
148
  const isCollapsed = collapsed.has(node.qualifiedName);
87
149
  const indent = { paddingLeft: `${0.5 + depth * 1}rem` };
@@ -156,13 +218,23 @@ function NavNodeItem({ node, depth, collapsed, onToggle }: NavNodeItemProps): Re
156
218
  )}
157
219
  {...(active && { "aria-current": "page" })}
158
220
  >
159
- <span
160
- aria-hidden="true"
161
- className={cn(
162
- "inline-block size-1.5 rounded-full",
163
- active ? "bg-accent-foreground" : "bg-muted-foreground/40",
164
- )}
165
- />
221
+ {NavIcon !== undefined ? (
222
+ <NavIcon
223
+ aria-hidden="true"
224
+ className={cn(
225
+ "size-4 shrink-0",
226
+ active ? "text-foreground" : "text-muted-foreground",
227
+ )}
228
+ />
229
+ ) : (
230
+ <span
231
+ aria-hidden="true"
232
+ className={cn(
233
+ "inline-block size-1.5 rounded-full",
234
+ active ? "bg-accent-foreground" : "bg-muted-foreground/40",
235
+ )}
236
+ />
237
+ )}
166
238
  <span className="truncate">{displayLabel}</span>
167
239
  </KumikoLink>
168
240
  {chevron !== null && <div className="pr-2">{chevron}</div>}
package/src/styles.css CHANGED
@@ -19,12 +19,17 @@
19
19
  Defaults bleiben rdp's eigenes Stylesheet. */
20
20
  @import "react-day-picker/style.css";
21
21
 
22
- /* Tailwind v4 scannt per default vom Import-Ort aus; da das CSS
23
- aus einem temp-dir kompiliert wird, findet es sonst keine
24
- Source-Dateien. Explizite @source-Direktiven decken alle
25
- Workspace-Sources ab der CLI scannt dann unsere Primitives,
26
- Layout-Components UND die Sample-App. */
27
- @source "../../renderer-web/src/**/*.{ts,tsx}";
22
+ /* Tailwind v4 scannt per default vom Import-Ort aus; da das CSS aus
23
+ einem temp-dir kompiliert wird, findet es sonst keine Source-Dateien.
24
+ Die erste Zeile ist self-relativ (`./` = dieses src/) und MUSS es
25
+ bleiben: das Paket shippt `src`, also löst sie in jedem Install-Layout
26
+ auf — Workspace UND Standalone-Consumer (node_modules). Ein monorepo-
27
+ relativer Pfad fand im Standalone-Build die eigenen Shell-Klassen
28
+ nicht → unstyled prod, 15KB statt 48KB (#359). Die restlichen Zeilen
29
+ sind Monorepo-Build-Quellen (renderer hat keine eigenen Klassen,
30
+ samples nur für den In-Monorepo-Sample-Build) — im Consumer matchen
31
+ sie nichts und sind dort ein harmloser No-op. */
32
+ @source "./**/*.{ts,tsx}";
28
33
  @source "../../renderer/src/**/*.{ts,tsx}";
29
34
  @source "../../../samples/**/src/**/*.{ts,tsx}";
30
35
 
package/src/tokens.ts CHANGED
@@ -35,10 +35,12 @@ function readCurrentMode(): ThemeMode {
35
35
  return document.documentElement.classList.contains("dark") ? "dark" : "light";
36
36
  }
37
37
 
38
+ // @wrapper-known semantic-alias
38
39
  function notifyThemeChange(): void {
39
40
  themeTick.setState((t) => t + 1);
40
41
  }
41
42
 
43
+ // @wrapper-known semantic-alias
42
44
  function persistMode(mode: ThemeMode): void {
43
45
  try {
44
46
  window.localStorage.setItem(THEME_STORAGE_KEY, mode);