rune-lab 0.4.2 → 0.4.4

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <h1 align="center">
2
- <img src="https://raw.githubusercontent.com/Yrrrrrf/rune-lab/main/src/sdk/ui/static/img/rune.png" alt="Rune Lab Icon" width="128" height="128" descripti``on="Icon representing the Svelte Runes system">
2
+ <img src="https://raw.githubusercontent.com/Yrrrrrf/rune-lab/main/static/img/rune.png" alt="Rune Lab Icon" width="128" height="128" descripti``on="Icon representing the Svelte Runes system">
3
3
  <div align="center">Rune Lab</div>
4
4
  </h1>
5
5
 
@@ -37,9 +37,11 @@
37
37
 
38
38
  // 0. Create and provide the built-in AppStore
39
39
  const appStore = createAppStore();
40
- if (config.app) {
41
- appStore.init(config.app);
42
- }
40
+ untrack(() => {
41
+ if (config.app) {
42
+ appStore.init(config.app);
43
+ }
44
+ });
43
45
  setContext(RUNE_LAB_CONTEXT.app, appStore);
44
46
 
45
47
  // 1. Register all plugins
@@ -5,8 +5,15 @@ export type ConfigStore<T = unknown> = {
5
5
  set: (id: unknown) => void;
6
6
  get: (id: unknown) => T | undefined;
7
7
  getProp: <K extends keyof T>(prop: K, id?: unknown) => T[K] | undefined;
8
- /** Append additional items (e.g. custom themes/currencies from the consuming app) */
9
8
  addItems: (newItems: T[]) => void;
9
+ /**
10
+ * Inject (or replace) the persistence driver at runtime.
11
+ * Call this inside your plugin factory so the real driver
12
+ * (e.g. localStorageDriver from RuneProvider) is used instead
13
+ * of the default in-memory fallback that was set at module-load time.
14
+ * Also re-reads any value already persisted under storageKey.
15
+ */
16
+ setDriver: (driver: PersistenceDriver) => void;
10
17
  };
11
18
  export interface ConfigStoreOptions<T = unknown> {
12
19
  /** Array of available items */
@@ -30,6 +30,22 @@ class ConfigStoreImpl {
30
30
  );
31
31
  }
32
32
  }
33
+ /**
34
+ * Replace the persistence driver and immediately re-read any saved value.
35
+ *
36
+ * Singletons are constructed at module-load time (before RuneProvider
37
+ * mounts), so they default to createInMemoryDriver(). Call this inside
38
+ * your plugin factory to swap in the real driver (e.g. localStorageDriver)
39
+ * so that all subsequent .set() calls are persisted correctly.
40
+ */
41
+ setDriver(driver) {
42
+ this.#driver = driver;
43
+ // Re-read persisted value now that we have a real driver
44
+ const saved = driver.get(this.#options.storageKey);
45
+ if (saved && this.get(saved)) {
46
+ this.current = saved;
47
+ }
48
+ }
33
49
  /**
34
50
  * Set current item with validation
35
51
  */
package/dist/mod.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./kernel/src/mod.js";
3
3
  export * from "./runes/layout/src/mod.js";
4
4
  export * from "./runes/palettes/src/mod.js";
5
5
  export * from "./runes/plugins/money/src/mod.js";
6
+ export declare const version: () => string;
package/dist/mod.js CHANGED
@@ -4,5 +4,4 @@ export * from "./kernel/src/mod.js";
4
4
  export * from "./runes/layout/src/mod.js";
5
5
  export * from "./runes/palettes/src/mod.js";
6
6
  export * from "./runes/plugins/money/src/mod.js";
7
- // import pkgConfig from "../../package.json" with { type: "json" };
8
- // export const version = (): string => pkgConfig.version;
7
+ export const version = () => "0.4.4";
@@ -24,6 +24,9 @@
24
24
 
25
25
  let isOpen = $state(false);
26
26
  let triggerEl = $state<HTMLElement>();
27
+ // FIX: track the portaled panel so clicks inside it are not treated as
28
+ // "outside" clicks. The panel lives outside triggerEl in the DOM (portal).
29
+ let panelEl = $state<HTMLElement | null>(null);
27
30
  let panelStyle = $state("position:fixed;visibility:hidden");
28
31
 
29
32
  function computePosition() {
@@ -53,24 +56,44 @@
53
56
  isOpen = false;
54
57
  }
55
58
 
59
+ // FIX: removed `capture: true`.
60
+ //
61
+ // With capture:true, handleOutsideClick fired BEFORE the item button's
62
+ // onclick. Because Svelte 5 flushes {#if} DOM removals synchronously in
63
+ // the same reactive batch, the panel was torn down before the bubble phase
64
+ // could deliver the click to the button. Result: LanguageSelector and
65
+ // CurrencySelector needed two clicks (ThemeSelector was immune only
66
+ // because its radio `change` event is a separate, later event).
67
+ //
68
+ // Without capture:true the order is:
69
+ // button onclick → wrapper div onclick (close) → document bubble
70
+ // The item action runs first; then we close; then outside-click check.
71
+ //
72
+ // panelEl is checked too: even after the panel is removed from the live
73
+ // DOM, the detached node still answers .contains() correctly, so we never
74
+ // double-close on a panel click.
56
75
  function handleOutsideClick(e: MouseEvent) {
57
- if (!triggerEl?.contains(e.target as Node)) close();
76
+ const target = e.target as Node;
77
+ if (!triggerEl?.contains(target) && !panelEl?.contains(target)) {
78
+ close();
79
+ }
80
+ }
81
+
82
+ // FIX: named function so we can actually remove it in cleanup.
83
+ // Before, an inline arrow was passed to addEventListener and could never
84
+ // be removed — a new Escape handler stacked up on every open.
85
+ function handleKeyDown(e: KeyboardEvent) {
86
+ if (e.key === "Escape") close();
58
87
  }
59
88
 
60
89
  $effect(() => {
61
90
  if (isOpen) {
62
- document.addEventListener("click", handleOutsideClick, {
63
- capture: true,
64
- });
65
- document.addEventListener(
66
- "keydown",
67
- (e) => e.key === "Escape" && close(),
68
- );
91
+ document.addEventListener("click", handleOutsideClick);
92
+ document.addEventListener("keydown", handleKeyDown);
69
93
  }
70
94
  return () => {
71
- document.removeEventListener("click", handleOutsideClick, {
72
- capture: true,
73
- });
95
+ document.removeEventListener("click", handleOutsideClick);
96
+ document.removeEventListener("keydown", handleKeyDown); // was missing
74
97
  };
75
98
  });
76
99
  </script>
@@ -148,7 +171,9 @@
148
171
  </div>
149
172
 
150
173
  {#if isOpen}
174
+ <!-- FIX: bind:this={panelEl} so handleOutsideClick can exclude it -->
151
175
  <div
176
+ bind:this={panelEl}
152
177
  use:portal
153
178
  style={panelStyle}
154
179
  class={responsive ? "hidden md:block" : "block"}
@@ -159,8 +184,6 @@
159
184
  >
160
185
  {#each options as option}
161
186
  <li role="option" aria-selected={value === option}>
162
- <!-- Event delegation: we close on click unless they clicked the scrollbar -->
163
- <!-- In a real world component you'd wrap the button down here or add a capture handler -->
164
187
  <div
165
188
  role="presentation"
166
189
  onclick={close}
@@ -34,15 +34,17 @@ export const LayoutPlugin = defineRune({
34
34
  {
35
35
  id: "theme",
36
36
  contextKey: RUNE_LAB_CONTEXT.theme,
37
- factory: (config, _driver) => {
37
+ factory: (config, driver) => {
38
+ // FIX: inject the real driver (e.g. localStorageDriver from RuneProvider).
39
+ // The singleton was built at module-load time with createInMemoryDriver(),
40
+ // so without this call every theme change is lost on page reload.
41
+ themeStore.setDriver(driver);
38
42
  const c = config;
39
43
  if (c.customThemes) {
40
44
  themeStore.addItems(c.customThemes);
41
45
  }
42
- if (
43
- c.defaultTheme &&
44
- !themeStore.current
45
- ) {
46
+ // Only apply defaultTheme when there's no persisted value already loaded
47
+ if (c.defaultTheme && !themeStore.current) {
46
48
  if (themeStore.get(c.defaultTheme)) {
47
49
  themeStore.set(c.defaultTheme);
48
50
  }
@@ -53,7 +55,10 @@ export const LayoutPlugin = defineRune({
53
55
  {
54
56
  id: "language",
55
57
  contextKey: RUNE_LAB_CONTEXT.language,
56
- factory: (config, _driver) => {
58
+ factory: (config, driver) => {
59
+ // FIX: same as theme — swap in the real driver so language choices
60
+ // are persisted to localStorage and survive page reloads.
61
+ languageStore.setDriver(driver);
57
62
  const c = config;
58
63
  if (c.locales) {
59
64
  const localesToKeep = c.locales;
@@ -31,7 +31,11 @@ export const MoneyPlugin = defineRune({
31
31
  {
32
32
  id: "currency",
33
33
  contextKey: RUNE_LAB_CONTEXT.currency,
34
- factory: (config, _driver, stores) => {
34
+ factory: (config, driver, stores) => {
35
+ // FIX: inject the real driver so currency selection is persisted.
36
+ // Previously `_driver` was ignored, leaving the singleton on its
37
+ // default createInMemoryDriver() — wiped on every page reload.
38
+ currencyStore.setDriver(driver);
35
39
  const c = config;
36
40
  setExchangeRateStore(stores.get("exchangeRate"));
37
41
  if (c?.currencies) {
@@ -39,10 +43,7 @@ export const MoneyPlugin = defineRune({
39
43
  currencyStore.addCurrency(cur);
40
44
  }
41
45
  }
42
- if (
43
- c?.defaultCurrency &&
44
- !currencyStore.current
45
- ) {
46
+ if (c?.defaultCurrency && !currencyStore.current) {
46
47
  if (currencyStore.get(c.defaultCurrency)) {
47
48
  currencyStore.set(c.defaultCurrency);
48
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune-lab",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Modern toolkit for Svelte 5 Runes applications.",
5
5
  "type": "module",
6
6
  "readme": "./README.md",
@@ -11,7 +11,8 @@
11
11
  "url": "git+https://github.com/Yrrrrrf/rune-lab.git"
12
12
  },
13
13
  "scripts": {
14
- "dev": "vite dev"
14
+ "dev": "vite dev",
15
+ "gen:version": "node scripts/gen-version.js"
15
16
  },
16
17
  "exports": {
17
18
  ".": {
@@ -58,4 +59,4 @@
58
59
  "dist/**",
59
60
  "**/*.test.ts"
60
61
  ]
61
- }
62
+ }