rune-lab 0.5.0-rc.1 → 0.5.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.
Files changed (44) hide show
  1. package/README.md +34 -257
  2. package/dist/src/core/forge/define-plugin.d.ts +11 -0
  3. package/dist/src/core/forge/define-plugin.js +11 -1
  4. package/dist/src/core/kernel/wiring.d.ts +1 -0
  5. package/dist/src/core/kernel/wiring.js +16 -17
  6. package/dist/src/plugins/layout/stores/layout.svelte.d.ts +3 -1
  7. package/dist/src/plugins/layout/stores/layout.svelte.js +9 -3
  8. package/dist/src/plugins/layout/stores/theme.svelte.js +15 -7
  9. package/dist/src/plugins/palettes/SettingsModal.svelte +69 -45
  10. package/dist/src/plugins/palettes/host/PaletteHost.svelte +10 -2
  11. package/dist/src/plugins/palettes/palettes/shortcuts/ShortcutPalette.svelte +3 -1
  12. package/dist/src/plugins/palettes/plugin.d.ts +8 -3
  13. package/dist/src/plugins/palettes/plugin.js +4 -3
  14. package/dist/src/plugins/palettes/registry/registry.svelte.d.ts +15 -2
  15. package/dist/src/plugins/palettes/registry/registry.svelte.js +14 -3
  16. package/dist/src/plugins/palettes/settings-modal/AppMetadata.svelte +59 -0
  17. package/dist/src/plugins/palettes/settings-modal/AppMetadata.svelte.d.ts +7 -0
  18. package/dist/src/plugins/palettes/settings-modal/GeneralSection.svelte +27 -0
  19. package/dist/src/plugins/palettes/settings-modal/GeneralSection.svelte.d.ts +12 -0
  20. package/dist/src/plugins/palettes/settings-modal/SettingsSearchResults.svelte +60 -30
  21. package/dist/src/plugins/palettes/settings-modal/SettingsSearchResults.svelte.d.ts +1 -0
  22. package/dist/src/plugins/palettes/settings-modal/SettingsSectionContent.svelte +20 -7
  23. package/dist/src/plugins/palettes/settings-modal/SettingsSectionContent.svelte.d.ts +3 -2
  24. package/dist/src/plugins/palettes/settings-modal/SettingsSidebar.svelte +49 -35
  25. package/dist/src/plugins/palettes/settings-modal/SettingsSidebar.svelte.d.ts +2 -3
  26. package/dist/src/plugins/palettes/settings-modal/hash-routing.d.ts +2 -8
  27. package/dist/src/plugins/palettes/settings-modal/hash-routing.js +9 -37
  28. package/dist/src/plugins/palettes/settings-modal/route.svelte.d.ts +4 -0
  29. package/dist/src/plugins/palettes/settings-modal/route.svelte.js +54 -0
  30. package/dist/src/plugins/palettes/settings-modal/search.d.ts +4 -3
  31. package/dist/src/plugins/palettes/settings-modal/search.js +34 -6
  32. package/dist/src/plugins/palettes/settings-modal/sections.d.ts +17 -0
  33. package/dist/src/plugins/palettes/settings-modal/sections.js +28 -0
  34. package/dist/src/ui/RuneProvider.svelte +10 -9
  35. package/dist/src/ui/RuneProvider.svelte.d.ts +0 -3
  36. package/dist/src/ui/mod.js +1 -1
  37. package/dist/src/ui/reactivity/app.svelte.d.ts +3 -11
  38. package/dist/src/ui/reactivity/app.svelte.js +13 -46
  39. package/dist/src/ui/reactivity/data-store.svelte.d.ts +10 -0
  40. package/dist/src/ui/reactivity/data-store.svelte.js +35 -0
  41. package/dist/src/ui/settings/SettingsFields.svelte +7 -5
  42. package/package.json +1 -1
  43. package/dist/src/ui/static/attributions.md +0 -11
  44. package/dist/src/ui/static/img/rune.png +0 -0
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <h1 align="center">
2
- <img src="https://raw.githubusercontent.com/Yrrrrrf/rune-lab/refs/heads/main/src/package/src/static/img/rune.png" alt="Rune Lab Icon" width="128" height="128" description="Icon representing the Svelte Runes system">
2
+ <img src="https://raw.githubusercontent.com/Yrrrrrf/rune-lab/refs/heads/main/apps/lab/src/lib/assets/img/rune.png" alt="Rune Lab Icon" width="128" height="128" description="Icon representing the Svelte Runes system">
3
3
  <div align="center">Rune Lab</div>
4
4
  </h1>
5
5
 
@@ -16,293 +16,70 @@
16
16
 
17
17
  **Rune Lab** is a modern, extensible **plugin-based UI shell** for **Svelte 5**
18
18
  applications. Harnessing the power of Svelte's new **Runes** system, it provides
19
- a complete application skeleton with built-in layout management, dynamic
20
- theming, i18n, keyboard shortcuts, a command palette, toast notifications, and a
21
- highly precise money/currency subsystem.
19
+ a complete application skeleton with layout management, dynamic theming, i18n,
20
+ keyboard shortcuts, a command palette, toast notifications, and currency
21
+ controls.
22
22
 
23
23
  Everything is wired through a centralized **Provider + Registry + Context**
24
24
  architecture, ensuring clean dependency injection and state isolation across
25
25
  your app.
26
26
 
27
- ## Key Features
28
-
29
- - **✨ Svelte 5 Runes-First:** Built from the ground up using `$state`,
30
- `$derived`, and `$effect`. No legacy Svelte 4 stores.
31
- - **🧩 Extensible Plugin Architecture:** Features are isolated into plugins
32
- (`LayoutPlugin`, `PalettesPlugin`, `MoneyPlugin`). Only load what you need.
33
- - **🎨 Dynamic Theming & i18n:** 32 DaisyUI themes and 13 pre-configured locales
34
- powered by Paraglide JS, with zero-flash SSR persistence.
35
- - **🔗 Declarative Callbacks:** Bridge store changes (theme, language, currency)
36
- to your own system hooks via `RuneProvider` props.
37
- - **💾 Abstract Persistence Layer:** Swap between cookies, localStorage,
38
- sessionStorage, or in-memory state seamlessly via generic drivers.
39
- - **💸 Robust Money Subsystem:** Backed by Dinero.js for floating-point-safe
40
- precision arithmetic, complete with exchange rate strategies and masked
41
- currency inputs.
42
- - **⌨️ Developer & Power-User Friendly:** Out-of-the-box Command Palette
43
- (`Ctrl+K`) and interactive Shortcuts Palette (`Ctrl+/`).
44
-
45
27
  ## Installation
46
28
 
47
- ### Using NPM / Bun
48
-
49
29
  ```bash
50
30
  npm install rune-lab
51
31
  # or
52
32
  bun install rune-lab
53
33
  ```
54
34
 
55
- ## Project Configuration (Required)
56
-
57
- After installing, two configuration steps are required to ensure the framework's
58
- components are compiled and styled correctly in your consuming project.
59
-
60
- ### Step 1 — Vite: Process `rune-lab` through the Svelte compiler
61
-
62
- By default, Vite externalizes `node_modules` during SSR, bypassing the Svelte
63
- compiler. Add the following to your `vite.config.ts` to process `rune-lab`
64
- properly:
65
-
66
- ```ts
67
- // vite.config.ts
68
- import { defineConfig } from "vite";
69
- import { sveltekit } from "@sveltejs/kit/vite";
70
-
71
- export default defineConfig({
72
- plugins: [sveltekit()],
73
- ssr: {
74
- noExternal: ["rune-lab"], // 👈 CRITICAL for Svelte 5 components in node_modules
75
- },
76
- });
77
- ```
78
-
79
- ### Step 2 — Tailwind CSS v4: Scan `rune-lab` for utility classes
35
+ ## Setup & Quick Start
80
36
 
81
- Tailwind only generates CSS for classes it finds by scanning your source files.
82
- Add a `@source` directive to your project's main CSS file so Tailwind compiles
83
- the DaisyUI classes used internally by `rune-lab`:
84
-
85
- ```css
86
- /* app.css / layout.css / global.css */
87
- @import "tailwindcss";
88
- @source "../node_modules/rune-lab/dist"; /* 👈 add this */
89
- ```
90
-
91
- ## Quick Start
92
-
93
- Get your application shell running in less than 40 lines. Inside your
94
- `+layout.svelte`:
37
+ Configure your application layout with the plugins. Here is what you need to set
38
+ up in your root `+layout.svelte`:
95
39
 
96
40
  ```svelte
97
41
  <script lang="ts">
98
- import {
99
- ConnectedNavigationPanel,
100
- cookieDriver,
101
- LayoutPlugin,
102
- PalettesPlugin,
103
- RuneProvider,
104
- WorkspaceLayout,
105
- } from "rune-lab";
106
- import type { NavigationSection } from "rune-lab";
107
- import { setLocale } from "$lib/i18n/paraglide/runtime.js";
42
+ import "./layout.css";
43
+ import { RuneProvider, version } from "rune-lab";
44
+ import { I18nPlugin } from "rune-lab/i18n";
45
+ import { LayoutPlugin } from "rune-lab/layout";
46
+ import { PalettesPlugin } from "rune-lab/palettes";
47
+ import type { Snippet } from "svelte";
48
+ import AppLayout from "./AppLayout.svelte";
108
49
 
109
- let { children } = $props();
50
+ import faviconUrl from "$lib/static/img/rune.png";
110
51
 
111
- const sections: NavigationSection[] = [
112
- {
113
- id: "main",
114
- title: "Main Menu",
115
- items: [
116
- { id: "home", label: "Dashboard", icon: "🏠" },
117
- { id: "settings", label: "Settings", icon: "⚙️" },
118
- ],
119
- },
120
- ];
52
+ let { children }: { children: Snippet } = $props();
121
53
  </script>
122
54
 
123
- <!-- Initialize the system with your required plugins and callbacks -->
124
55
  <RuneProvider
125
56
  config={{
126
- app: { name: "My Startup", version: "1.0.0" },
127
- persistence: cookieDriver,
128
- }}
129
- plugins={[LayoutPlugin, PalettesPlugin]}
130
- onLanguageChange={(l) => setLocale(l.code)}
131
- onThemeChange={(t) => console.log(`Theme changed to ${t.name}`)}
132
- >
133
- <WorkspaceLayout>
134
- {#snippet navigationPanel()}
135
- <!-- Auto-wires to LayoutStore state -->
136
- <ConnectedNavigationPanel {sections} />
137
- {/snippet}
138
-
139
- {#snippet content()}
140
- <div class="p-8">
141
- {@render children()}
142
- </div>
143
- {/snippet}
144
- </WorkspaceLayout>
145
- </RuneProvider>
146
- ```
147
-
148
- You now have a fully reactive layout, a keyboard command palette, a toast
149
- notification system, and theme/language switchers.
150
-
151
- ## Money & Currency Plugin
152
-
153
- Rune Lab provides a robust money layer that handles precision arithmetic,
154
- formatting, and live exchange-rate triangulation. To use it, simply register the
155
- `MoneyPlugin`:
156
-
157
- ```svelte
158
- <script lang="ts">
159
- import { MoneyPlugin } from "rune-lab";
160
- </script>
161
-
162
- <RuneProvider
163
- config={{
164
- "rune-lab.money": {
165
- defaultCurrency: "USD",
166
- exchangeRates: {
167
- base: "USD",
168
- rates: { MXN: 17.23, EUR: 0.91 },
169
- },
57
+ favicon: faviconUrl,
58
+ icons: "material",
59
+ app: {
60
+ name: "Rune Lab",
61
+ version: version(),
62
+ description: "Plugin test bench",
63
+ author: "Yrrrrrf",
170
64
  },
65
+ // Namespaced per-plugin config
66
+ "rune-lab.layout": "dark",
171
67
  }}
172
- plugins={[LayoutPlugin, PalettesPlugin, MoneyPlugin]}
68
+ plugins={[LayoutPlugin, PalettesPlugin, I18nPlugin]}
173
69
  >
174
- <!-- App Content -->
70
+ <AppLayout>
71
+ {@render children()}
72
+ </AppLayout>
175
73
  </RuneProvider>
176
74
  ```
177
75
 
178
- ### Displaying & Inputting Money
179
-
180
- ```svelte
181
- <script lang="ts">
182
- import { CurrencySelector, MoneyDisplay, MoneyInput, useMoney } from "rune-lab";
183
-
184
- let price = $state(15000); // Minor units (e.g., cents) -> $150.00
185
- const { convert, format } = useMoney();
186
- </script>
187
-
188
- <!-- Select from available currencies -->
189
- <CurrencySelector />
190
-
191
- <!-- Formats safely and localizes based on the active LanguageStore -->
192
- <MoneyDisplay amount={price} currency="USD" />
193
-
194
- <!-- Compact notation ($1.5M) -->
195
- <MoneyDisplay amount={150000000} currency="USD" compact />
196
-
197
- <!-- Integer-backed masked input (prevents floating point errors) -->
198
- <MoneyInput bind:amount={price} currency="USD" />
199
- ```
200
-
201
- ## Persistence Drivers
202
-
203
- Rune Lab provides generic drivers to remember user preferences across reloads.
204
- Pass one to `config.persistence` on `<RuneProvider>`:
205
-
206
- - `cookieDriver`: Best for SSR applications (like SvelteKit) to prevent "theme
207
- flash" on initial load.
208
- - `localStorageDriver`: Best for client-only applications (SPAs).
209
- - `sessionStorageDriver`: For preferences that should clear when the browser tab
210
- closes.
211
-
212
- ```svelte
213
- <script lang="ts">
214
- import { cookieDriver } from "rune-lab";
215
- </script>
216
-
217
- <RuneProvider config={{ persistence: cookieDriver }} plugins={[...]}>
218
- ```
219
-
220
- ## Advanced Patterns
76
+ ## Plugins
221
77
 
222
- ### Store Observers (`onChange`)
223
-
224
- Beyond the `RuneProvider` props, you can imperatively subscribe to any
225
- `ConfigStore` (theme, language, currency) from your own services or components.
226
- Callbacks include `try/catch` protection so they never crash the store.
227
-
228
- ```ts
229
- import { getThemeStore } from "rune-lab";
230
-
231
- const themeStore = getThemeStore();
232
-
233
- // Returns an unsubscribe function
234
- const unsub = themeStore.onChange((newId, oldId) => {
235
- console.log(`Theme swapped from ${oldId} to ${newId}`);
236
- });
237
- ```
238
-
239
- ### Keyboard Shortcuts (Auto-Cleanup)
240
-
241
- Any component deep in your tree can register its own keyboard shortcuts using
242
- the `useShortcuts` composable. It handles Svelte's `$effect` lifecycle
243
- internally, ensuring shortcuts unregister when the component unmounts:
244
-
245
- ```svelte
246
- <script lang="ts">
247
- import { getToastStore, useShortcuts } from "rune-lab";
78
+ Rune Lab is modular. Plugins are documented in their respective directories:
248
79
 
249
- const toasts = getToastStore();
250
-
251
- useShortcuts([
252
- {
253
- id: "feature.save",
254
- keys: "ctrl+s, cmd+s", // Comma-separated alternative keys
255
- label: "Save Document",
256
- category: "Editor",
257
- scope: "global",
258
- handler: (e) => {
259
- e.preventDefault();
260
- toasts.success("Document Saved!");
261
- },
262
- },
263
- ]);
264
- </script>
265
- ```
266
-
267
- ### SvelteKit Route Syncing
268
-
269
- To keep your layout's active navigation state synchronized with the SvelteKit
270
- router, use an `$effect` inside your `+layout.svelte` right after the provider:
271
-
272
- ```svelte
273
- <script lang="ts">
274
- import { page } from "$app/state";
275
- import { getLayoutStore } from "rune-lab";
276
-
277
- const layoutStore = getLayoutStore();
278
-
279
- $effect(() => {
280
- // Automatically open the correct nav tree branch
281
- const segment = page.url.pathname.split("/")[1] || "home";
282
- layoutStore.navigate(segment);
283
- });
284
- </script>
285
- ```
286
-
287
- ### Calling Toasts from Outside Svelte Components
288
-
289
- If you need to trigger a toast from a pure `.ts` file (like a fetching utility
290
- or global error handler), you can use the Toast Bridge:
291
-
292
- ```ts
293
- import { createToastBridge } from "rune-lab";
294
-
295
- const { notify } = createToastBridge();
296
-
297
- export async function fetchUser() {
298
- try {
299
- // ...
300
- } catch (err) {
301
- // Safely queues the toast if the UI hasn't mounted yet
302
- notify("Failed to fetch user data", "error");
303
- }
304
- }
305
- ```
80
+ - **Layout Plugin (`rune-lab/layout`)**:
81
+ - **Palettes Plugin (`rune-lab/palettes`)**:
82
+ - **i18n Plugin (`rune-lab/i18n`)**:
306
83
 
307
84
  ## License
308
85
 
@@ -1,7 +1,16 @@
1
+ import type { Schema } from "effect";
1
2
  import type { ContributionEntry } from "./define-contribution.js";
2
3
  import type { SettingsSchema } from "./define-settings.js";
3
4
  import type { BaseSlotSpec } from "./define-slot.js";
4
5
  import { type SlotDescriptor } from "./descriptors.js";
6
+ export type SlotConfigInput<TSpec extends BaseSlotSpec> = TSpec extends {
7
+ config: Schema.Schema<infer TConfig, unknown, never>;
8
+ }
9
+ ? TConfig
10
+ : unknown;
11
+ export type PluginConfigInput<TSlots extends Record<string, BaseSlotSpec>> = {
12
+ [K in keyof TSlots]?: SlotConfigInput<TSlots[K]>;
13
+ };
5
14
  export interface ForgedPlugin<
6
15
  TId extends string = string,
7
16
  TSlots extends Record<string, BaseSlotSpec> = Record<string, BaseSlotSpec>,
@@ -13,6 +22,8 @@ export interface ForgedPlugin<
13
22
  overlays?: unknown[];
14
23
  contributions?: ContributionEntry<unknown>[];
15
24
  descriptors: Record<keyof TSlots, SlotDescriptor>;
25
+ config?: Record<string, unknown>;
26
+ with(config: PluginConfigInput<TSlots>): ForgedPlugin<TId, TSlots>;
16
27
  }
17
28
  export type PluginInput =
18
29
  | ForgedPlugin<string, Record<string, BaseSlotSpec>>
@@ -9,7 +9,7 @@ export function definePlugin(spec) {
9
9
  accessorName: getAccessorName(slotName),
10
10
  };
11
11
  }
12
- return {
12
+ const plugin = {
13
13
  id: spec.id,
14
14
  requires: spec.requires,
15
15
  slots: slots,
@@ -17,5 +17,15 @@ export function definePlugin(spec) {
17
17
  overlays: spec.overlays,
18
18
  contributions: spec.contributions,
19
19
  descriptors: descriptors,
20
+ with(config) {
21
+ return {
22
+ ...this,
23
+ config: {
24
+ ...(this.config || {}),
25
+ ...config,
26
+ },
27
+ };
28
+ },
20
29
  };
30
+ return plugin;
21
31
  }
@@ -13,6 +13,7 @@ export interface NormalizedSlot {
13
13
  expose: boolean;
14
14
  persist?: boolean | string[];
15
15
  configSchema?: unknown;
16
+ pluginConfig?: Record<string, unknown>;
16
17
  create: (context: SlotContext<unknown>) => unknown;
17
18
  }
18
19
  export declare function resolveSlotRef(
@@ -33,12 +33,6 @@ function normalizePlugins(inputs) {
33
33
  return true;
34
34
  });
35
35
  }
36
- function getPluginConfig(pluginId, config) {
37
- if (config[pluginId] !== undefined) {
38
- return config[pluginId];
39
- }
40
- return config;
41
- }
42
36
  function sortPlugins(resolved) {
43
37
  const pluginMap = new Map(resolved.map((p) => [p.id, p]));
44
38
  const graphNodes = resolved.map((p) => ({
@@ -97,6 +91,7 @@ export function normalizeSlots(sorted) {
97
91
  expose: slotSpec.expose !== false,
98
92
  persist: slotSpec.persist,
99
93
  configSchema: slotSpec.config,
94
+ pluginConfig: plugin.config,
100
95
  create: slotSpec.create,
101
96
  };
102
97
  allSlots.push(slot);
@@ -151,18 +146,22 @@ function buildLayers(slots, options) {
151
146
  const storeLayer = Layer.effect(
152
147
  storeTag,
153
148
  Effect.gen(function* () {
154
- const pluginConfig = getPluginConfig(slot.pluginId, options.config);
155
- let configSlice = pluginConfig;
149
+ const rawConfig = slot.pluginConfig?.[slot.slotName];
150
+ let configSlice = rawConfig;
156
151
  if (slot.configSchema) {
157
- try {
158
- configSlice = Schema.decodeUnknownSync(slot.configSchema)(
159
- pluginConfig,
160
- );
161
- } catch (err) {
162
- const errMsg = err instanceof Error ? err.message : String(err);
163
- throw new Error(
164
- `[Kernel] Config validation failed for plugin "${slot.pluginId}", slot "${slot.slotName}": ${errMsg}`,
165
- );
152
+ if (rawConfig !== undefined) {
153
+ try {
154
+ configSlice = Schema.decodeUnknownSync(slot.configSchema)(
155
+ rawConfig,
156
+ );
157
+ } catch (err) {
158
+ const errMsg = err instanceof Error ? err.message : String(err);
159
+ throw new Error(
160
+ `[Kernel] Config validation failed for plugin "${slot.pluginId}", slot "${slot.slotName}": ${errMsg}`,
161
+ );
162
+ }
163
+ } else {
164
+ configSlice = undefined;
166
165
  }
167
166
  }
168
167
  let persistenceHandle;
@@ -1,11 +1,11 @@
1
1
  import type { SlotContext } from "rune-lab/core";
2
2
  import type { PresetState, WorkspaceItem } from "../types.js";
3
3
  export declare class LayoutStore {
4
+ #private;
4
5
  workspaces: WorkspaceItem[];
5
6
  activeWorkspaceId: string | null;
6
7
  activeNavItemId: string | null;
7
8
  collapsedSections: Set<string>;
8
- preset: string;
9
9
  zones: Record<
10
10
  string,
11
11
  {
@@ -14,6 +14,8 @@ export declare class LayoutStore {
14
14
  }
15
15
  >;
16
16
  constructor(_ctx?: SlotContext<unknown>);
17
+ get preset(): string;
18
+ set preset(v: string);
17
19
  applyPreset(presetId: string | PresetState): void;
18
20
  toggleZone(zoneId: string): void;
19
21
  setWorkspaces(items: WorkspaceItem[]): void;
@@ -5,7 +5,7 @@ export class LayoutStore {
5
5
  activeNavItemId = $state(null);
6
6
  collapsedSections = $state(new Set());
7
7
  // Zone & Preset state
8
- preset = $state("workspace");
8
+ #preset = $state("workspace");
9
9
  zones = $state({
10
10
  nav: { visible: true, size: 280 },
11
11
  strip: { visible: true, size: 64 },
@@ -15,17 +15,23 @@ export class LayoutStore {
15
15
  "overlay-anchor": { visible: true },
16
16
  });
17
17
  constructor(_ctx) {}
18
+ get preset() {
19
+ return this.#preset;
20
+ }
21
+ set preset(v) {
22
+ this.applyPreset(v);
23
+ }
18
24
  applyPreset(presetId) {
19
25
  if (typeof presetId === "string") {
20
26
  const state = PRESETS[presetId];
21
27
  if (state) {
22
- this.preset = presetId;
28
+ this.#preset = presetId;
23
29
  for (const [zone, val] of Object.entries(state)) {
24
30
  this.zones[zone] = { ...val };
25
31
  }
26
32
  }
27
33
  } else {
28
- this.preset = "custom";
34
+ this.#preset = "custom";
29
35
  for (const [zone, val] of Object.entries(presetId)) {
30
36
  this.zones[zone] = { ...val };
31
37
  }
@@ -51,13 +51,21 @@ export function createThemeStore(ctx) {
51
51
  icon: "🎨",
52
52
  driver: ctx.persistence,
53
53
  });
54
- if (!store.current && BROWSER) {
55
- const prefersDark = globalThis.matchMedia(
56
- "(prefers-color-scheme: dark)",
57
- ).matches;
58
- const systemDefault = prefersDark ? "dark" : "light";
59
- if (store.get(systemDefault)) {
60
- store.set(systemDefault);
54
+ const saved = ctx.persistence.get("theme");
55
+ const hasPersisted =
56
+ typeof saved === "string" ? Boolean(saved && store.get(saved)) : false;
57
+ if (!hasPersisted) {
58
+ const configTheme = typeof ctx.config === "string" ? ctx.config : undefined;
59
+ if (configTheme && store.get(configTheme)) {
60
+ store.set(configTheme);
61
+ } else if (BROWSER) {
62
+ const prefersDark = globalThis.matchMedia(
63
+ "(prefers-color-scheme: dark)",
64
+ ).matches;
65
+ const systemDefault = prefersDark ? "dark" : "light";
66
+ if (store.get(systemDefault)) {
67
+ store.set(systemDefault);
68
+ }
61
69
  }
62
70
  }
63
71
  return store;