rune-lab 0.4.2 → 0.4.3
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/kernel/src/persistence/createConfigStore.svelte.d.ts +8 -1
- package/dist/kernel/src/persistence/createConfigStore.svelte.js +16 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +2 -2
- package/dist/runes/layout/src/AppSettingSelector.svelte +36 -13
- package/dist/runes/layout/src/mod.js +11 -6
- package/dist/runes/plugins/money/src/mod.js +6 -5
- package/package.json +3 -2
|
@@ -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
package/dist/mod.js
CHANGED
|
@@ -4,5 +4,5 @@ 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
|
-
|
|
8
|
-
|
|
7
|
+
import pkgConfig from "../package.json" with { type: "json" };
|
|
8
|
+
export const version = () => pkgConfig.version;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
43
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.4.3",
|
|
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
|
".": {
|