rune-lab 0.4.4 β 0.4.6
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 +200 -140
- package/dist/RuneProvider.svelte +30 -1
- package/dist/kernel/src/persistence/createConfigStore.svelte.d.ts +5 -0
- package/dist/kernel/src/persistence/createConfigStore.svelte.js +36 -7
- package/dist/mod.js +1 -1
- package/dist/runes/layout/src/AppSettingSelector.svelte +30 -9
- package/dist/runes/layout/src/AppSettingSelector.svelte.d.ts +28 -14
- package/dist/runes/layout/src/LanguageSelector.svelte +6 -4
- package/dist/runes/layout/src/ResourceSelector.svelte +11 -9
- package/dist/runes/layout/src/ThemeSelector.svelte +7 -4
- package/dist/runes/layout/src/language.svelte.d.ts +4 -0
- package/dist/runes/layout/src/language.svelte.js +8 -0
- package/dist/runes/layout/src/theme.svelte.d.ts +4 -0
- package/dist/runes/layout/src/theme.svelte.js +8 -0
- package/dist/runes/plugins/money/src/CurrencySelector.svelte +6 -2
- package/dist/runes/plugins/money/src/currency.svelte.d.ts +4 -0
- package/dist/runes/plugins/money/src/currency.svelte.js +8 -0
- package/dist/runes/plugins/money/src/mod.d.ts +3 -0
- package/dist/runes/plugins/money/src/mod.js +3 -0
- package/package.json +2 -6
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/Yrrrrrf/rune-lab/main/static/img/rune.png" alt="Rune Lab Icon" width="128" height="128"
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Yrrrrrf/rune-lab/main/static/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
|
|
|
@@ -14,72 +14,125 @@
|
|
|
14
14
|
|
|
15
15
|
## Overview
|
|
16
16
|
|
|
17
|
-
**Rune Lab** is
|
|
18
|
-
applications
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
**Rune Lab** is a modern, extensible **plugin-based UI shell** for **Svelte 5**
|
|
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.
|
|
22
|
+
|
|
23
|
+
Everything is wired through a centralized **Provider + Registry + Context**
|
|
24
|
+
architecture, ensuring clean dependency injection and state isolation across
|
|
25
|
+
your app.
|
|
21
26
|
|
|
22
27
|
## Key Features
|
|
23
28
|
|
|
24
|
-
- **β¨ Svelte 5 Runes
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
|
|
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+/`).
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
interact with database schemas exposed by `prism-py` APIs directly within your
|
|
35
|
-
Svelte application. Test CRUD operations, execute functions, and understand
|
|
36
|
-
your data structure like never before.
|
|
37
|
-
- **π Smart API Integration:** Includes `apiStore` (using `prism-ts`) for easy
|
|
38
|
-
and type-safe connection to backend APIs. -->
|
|
45
|
+
## Installation
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
### Using NPM / Bun
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
```bash
|
|
50
|
+
npm install rune-lab
|
|
51
|
+
# or
|
|
52
|
+
bun install rune-lab
|
|
53
|
+
```
|
|
44
54
|
|
|
45
|
-
##
|
|
55
|
+
## Project Configuration (Required)
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
After installing, two configuration steps are required to ensure the framework's
|
|
58
|
+
components are compiled and styled correctly in your consuming project.
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
# Add to your Deno project
|
|
51
|
-
deno add @yrrrrrf/rune-lab
|
|
52
|
-
``` -->
|
|
60
|
+
### Step 1 β Vite: Process `rune-lab` through the Svelte compiler
|
|
53
61
|
|
|
54
|
-
|
|
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:
|
|
55
65
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
80
|
+
|
|
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 */
|
|
59
89
|
```
|
|
60
90
|
|
|
61
91
|
## Quick Start
|
|
62
92
|
|
|
63
|
-
Get your application shell running in less than
|
|
93
|
+
Get your application shell running in less than 40 lines. Inside your
|
|
64
94
|
`+layout.svelte`:
|
|
65
95
|
|
|
66
96
|
```svelte
|
|
67
97
|
<script lang="ts">
|
|
68
|
-
import {
|
|
69
|
-
|
|
98
|
+
import {
|
|
99
|
+
RuneProvider,
|
|
100
|
+
WorkspaceLayout,
|
|
101
|
+
ConnectedNavigationPanel,
|
|
102
|
+
LayoutPlugin,
|
|
103
|
+
PalettesPlugin,
|
|
104
|
+
cookieDriver
|
|
105
|
+
} from "rune-lab";
|
|
106
|
+
import type { NavigationSection } from "rune-lab";
|
|
107
|
+
import { setLocale } from "$lib/i18n/paraglide/runtime.js";
|
|
70
108
|
|
|
71
109
|
let { children } = $props();
|
|
72
110
|
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
];
|
|
75
121
|
</script>
|
|
76
122
|
|
|
123
|
+
<!-- Initialize the system with your required plugins and callbacks -->
|
|
77
124
|
<RuneProvider
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
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}`)}
|
|
80
132
|
>
|
|
81
133
|
<WorkspaceLayout>
|
|
82
134
|
{#snippet navigationPanel()}
|
|
135
|
+
<!-- Auto-wires to LayoutStore state -->
|
|
83
136
|
<ConnectedNavigationPanel {sections} />
|
|
84
137
|
{/snippet}
|
|
85
138
|
|
|
@@ -92,115 +145,130 @@ Get your application shell running in less than 20 lines. Inside your
|
|
|
92
145
|
</RuneProvider>
|
|
93
146
|
```
|
|
94
147
|
|
|
95
|
-
You now have a fully
|
|
96
|
-
notification system, and theme
|
|
97
|
-
|
|
98
|
-
## Project Configuration
|
|
148
|
+
You now have a fully reactive layout, a keyboard command palette, a toast
|
|
149
|
+
notification system, and theme/language switchers.
|
|
99
150
|
|
|
100
|
-
|
|
101
|
-
compiled and styled correctly in your consuming project.
|
|
151
|
+
## Money & Currency Plugin
|
|
102
152
|
|
|
103
|
-
|
|
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`:
|
|
104
156
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
```svelte
|
|
158
|
+
<script lang="ts">
|
|
159
|
+
import { MoneyPlugin } from "rune-lab";
|
|
160
|
+
</script>
|
|
109
161
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
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
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
plugins={[LayoutPlugin, PalettesPlugin, MoneyPlugin]}
|
|
173
|
+
>
|
|
174
|
+
<!-- App Content -->
|
|
175
|
+
</RuneProvider>
|
|
118
176
|
```
|
|
119
177
|
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
### Displaying & Inputting Money
|
|
179
|
+
|
|
180
|
+
```svelte
|
|
181
|
+
<script lang="ts">
|
|
182
|
+
import {
|
|
183
|
+
MoneyDisplay,
|
|
184
|
+
MoneyInput,
|
|
185
|
+
CurrencySelector,
|
|
186
|
+
useMoney
|
|
187
|
+
} from "rune-lab";
|
|
188
|
+
|
|
189
|
+
let price = $state(15000); // Minor units (e.g., cents) -> $150.00
|
|
190
|
+
const { convert, format } = useMoney();
|
|
191
|
+
</script>
|
|
122
192
|
|
|
123
|
-
|
|
193
|
+
<!-- Select from available currencies -->
|
|
194
|
+
<CurrencySelector />
|
|
124
195
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
scanned by default and the components will appear unstyled.
|
|
196
|
+
<!-- Formats safely and localizes based on the active LanguageStore -->
|
|
197
|
+
<MoneyDisplay amount={price} currency="USD" />
|
|
128
198
|
|
|
129
|
-
|
|
130
|
-
|
|
199
|
+
<!-- Compact notation ($1.5M) -->
|
|
200
|
+
<MoneyDisplay amount={150000000} currency="USD" compact />
|
|
131
201
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@import "tailwindcss";
|
|
135
|
-
@source "../node_modules/rune-lab/dist"; /* π add this */
|
|
202
|
+
<!-- Integer-backed masked input (prevents floating point errors) -->
|
|
203
|
+
<MoneyInput bind:amount={price} currency="USD" />
|
|
136
204
|
```
|
|
137
205
|
|
|
138
|
-
|
|
139
|
-
> a different depth in your project tree. With both steps in place, all DaisyUI
|
|
140
|
-
> component classes used by `rune-lab` will be included in your build and theme
|
|
141
|
-
> switching will work across library components and your own code alike.
|
|
142
|
-
|
|
143
|
-
## Money & Currency
|
|
206
|
+
## Persistence Drivers
|
|
144
207
|
|
|
145
|
-
Rune Lab provides
|
|
146
|
-
|
|
208
|
+
Rune Lab provides generic drivers to remember user preferences across reloads.
|
|
209
|
+
Pass one to `config.persistence` on `<RuneProvider>`:
|
|
147
210
|
|
|
148
|
-
|
|
211
|
+
- `cookieDriver`: Best for SSR applications (like SvelteKit) to prevent "theme
|
|
212
|
+
flash" on initial load.
|
|
213
|
+
- `localStorageDriver`: Best for client-only applications (SPAs).
|
|
214
|
+
- `sessionStorageDriver`: For preferences that should clear when the browser tab
|
|
215
|
+
closes.
|
|
149
216
|
|
|
150
217
|
```svelte
|
|
151
|
-
<script>
|
|
152
|
-
import {
|
|
218
|
+
<script lang="ts">
|
|
219
|
+
import { cookieDriver } from "rune-lab";
|
|
153
220
|
</script>
|
|
154
221
|
|
|
155
|
-
|
|
156
|
-
|
|
222
|
+
<RuneProvider config={{ persistence: cookieDriver }} plugins={[...]}>
|
|
223
|
+
```
|
|
157
224
|
|
|
158
|
-
|
|
159
|
-
<MoneyDisplay amount={150} unit="major" currency="USD" />
|
|
225
|
+
## Advanced Patterns
|
|
160
226
|
|
|
161
|
-
|
|
162
|
-
<MoneyDisplay amount={1200000} unit="major" compact />
|
|
227
|
+
### Store Observers (`onChange`)
|
|
163
228
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
229
|
+
Beyond the `RuneProvider` props, you can imperatively subscribe to any
|
|
230
|
+
`ConfigStore` (theme, language, currency) from your own services or components.
|
|
231
|
+
Callbacks include `try/catch` protection so they never crash the store.
|
|
167
232
|
|
|
168
|
-
|
|
233
|
+
```ts
|
|
234
|
+
import { getThemeStore } from "rune-lab";
|
|
169
235
|
|
|
170
|
-
|
|
171
|
-
exclusively with integers.
|
|
236
|
+
const themeStore = getThemeStore();
|
|
172
237
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<MoneyInput bind:amount={price} unit="major" currency="USD" />
|
|
238
|
+
// Returns an unsubscribe function
|
|
239
|
+
const unsub = themeStore.onChange((newId, oldId) => {
|
|
240
|
+
console.log(`Theme swapped from ${oldId} to ${newId}`);
|
|
241
|
+
});
|
|
180
242
|
```
|
|
181
243
|
|
|
182
|
-
|
|
244
|
+
### Keyboard Shortcuts (Auto-Cleanup)
|
|
183
245
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
- `cookieDriver`: Best for SSR applications (like SvelteKit) because the server
|
|
189
|
-
can read the cookie and prevent a "theme flash" on initial load.
|
|
190
|
-
- `localStorageDriver`: Best for client-only applications (SPA) looking for
|
|
191
|
-
long-term persistence.
|
|
192
|
-
- `sessionStorageDriver`: For preferences that should clear when the browser tab
|
|
193
|
-
closes.
|
|
246
|
+
Any component deep in your tree can register its own keyboard shortcuts using
|
|
247
|
+
the `useShortcuts` composable. It handles Svelte's `$effect` lifecycle
|
|
248
|
+
internally, ensuring shortcuts unregister when the component unmounts:
|
|
194
249
|
|
|
195
250
|
```svelte
|
|
196
251
|
<script lang="ts">
|
|
197
|
-
import {
|
|
198
|
-
|
|
252
|
+
import { useShortcuts, getToastStore } from "rune-lab";
|
|
253
|
+
|
|
254
|
+
const toasts = getToastStore();
|
|
255
|
+
|
|
256
|
+
useShortcuts([
|
|
257
|
+
{
|
|
258
|
+
id: "feature.save",
|
|
259
|
+
keys: "ctrl+s, cmd+s", // Comma-separated alternative keys
|
|
260
|
+
label: "Save Document",
|
|
261
|
+
category: "Editor",
|
|
262
|
+
scope: "global",
|
|
263
|
+
handler: (e) => {
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
toasts.success("Document Saved!");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
]);
|
|
199
269
|
</script>
|
|
200
270
|
```
|
|
201
271
|
|
|
202
|
-
## Advanced Patterns
|
|
203
|
-
|
|
204
272
|
### SvelteKit Route Syncing
|
|
205
273
|
|
|
206
274
|
To keep your layout's active navigation state synchronized with the SvelteKit
|
|
@@ -214,39 +282,31 @@ router, use an `$effect` inside your `+layout.svelte` right after the provider:
|
|
|
214
282
|
const layoutStore = getLayoutStore();
|
|
215
283
|
|
|
216
284
|
$effect(() => {
|
|
217
|
-
//
|
|
285
|
+
// Automatically open the correct nav tree branch
|
|
218
286
|
const segment = page.url.pathname.split("/")[1] || "home";
|
|
219
287
|
layoutStore.navigate(segment);
|
|
220
288
|
});
|
|
221
289
|
</script>
|
|
222
290
|
```
|
|
223
291
|
|
|
224
|
-
|
|
292
|
+
### Calling Toasts from Outside Svelte Components
|
|
225
293
|
|
|
226
|
-
|
|
294
|
+
If you need to trigger a toast from a pure `.ts` file (like a fetching utility
|
|
295
|
+
or global error handler), you can use the Toast Bridge:
|
|
227
296
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
id: "feature.save",
|
|
242
|
-
keys: "ctrl s",
|
|
243
|
-
label: "Save Document",
|
|
244
|
-
handler: () => toasts.success("Document Saved!")
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
return () => shortcuts.unregister("feature.save"); // Important!
|
|
248
|
-
});
|
|
249
|
-
</script>
|
|
297
|
+
```ts
|
|
298
|
+
import { createToastBridge } from "rune-lab";
|
|
299
|
+
|
|
300
|
+
const { notify } = createToastBridge();
|
|
301
|
+
|
|
302
|
+
export async function fetchUser() {
|
|
303
|
+
try {
|
|
304
|
+
// ...
|
|
305
|
+
} catch (err) {
|
|
306
|
+
// Safely queues the toast if the UI hasn't mounted yet
|
|
307
|
+
notify("Failed to fetch user data", "error");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
250
310
|
```
|
|
251
311
|
|
|
252
312
|
## License
|
package/dist/RuneProvider.svelte
CHANGED
|
@@ -26,10 +26,20 @@
|
|
|
26
26
|
[pluginId: string]: unknown;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
let {
|
|
29
|
+
let {
|
|
30
|
+
children,
|
|
31
|
+
config = {},
|
|
32
|
+
plugins = [],
|
|
33
|
+
onThemeChange,
|
|
34
|
+
onLanguageChange,
|
|
35
|
+
onCurrencyChange,
|
|
36
|
+
} = $props<{
|
|
30
37
|
children: Snippet;
|
|
31
38
|
config?: RuneLabConfig;
|
|
32
39
|
plugins?: RunePlugin[];
|
|
40
|
+
onThemeChange?: (newTheme: any, oldTheme: any) => void;
|
|
41
|
+
onLanguageChange?: (newLang: any, oldLang: any) => void;
|
|
42
|
+
onCurrencyChange?: (newCurrency: any, oldCurrency: any) => void;
|
|
33
43
|
}>();
|
|
34
44
|
|
|
35
45
|
const initialPlugins = untrack(() => plugins);
|
|
@@ -78,9 +88,28 @@
|
|
|
78
88
|
|
|
79
89
|
// ββ Initialization for layout ββββββββββββββββββββββββββ
|
|
80
90
|
const layoutStore = stores.get("layout") as any;
|
|
91
|
+
const themeStore = stores.get("theme") as any;
|
|
92
|
+
const languageStore = stores.get("language") as any;
|
|
93
|
+
const currencyStore = stores.get("currency") as any;
|
|
81
94
|
|
|
82
95
|
onMount(() => {
|
|
83
96
|
if (layoutStore) layoutStore.init();
|
|
97
|
+
|
|
98
|
+
const unsubs: (() => void)[] = [];
|
|
99
|
+
|
|
100
|
+
if (onThemeChange && themeStore) {
|
|
101
|
+
unsubs.push(themeStore.onChange(onThemeChange));
|
|
102
|
+
}
|
|
103
|
+
if (onLanguageChange && languageStore) {
|
|
104
|
+
unsubs.push(languageStore.onChange(onLanguageChange));
|
|
105
|
+
}
|
|
106
|
+
if (onCurrencyChange && currencyStore) {
|
|
107
|
+
unsubs.push(currencyStore.onChange(onCurrencyChange));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
unsubs.forEach((unsub) => unsub());
|
|
112
|
+
};
|
|
84
113
|
});
|
|
85
114
|
|
|
86
115
|
// Meta tags derived from app store state
|
|
@@ -6,6 +6,11 @@ export type ConfigStore<T = unknown> = {
|
|
|
6
6
|
get: (id: unknown) => T | undefined;
|
|
7
7
|
getProp: <K extends keyof T>(prop: K, id?: unknown) => T[K] | undefined;
|
|
8
8
|
addItems: (newItems: T[]) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Register a callback to fire when the current item changes.
|
|
11
|
+
* Returns an unsubscribe function.
|
|
12
|
+
*/
|
|
13
|
+
onChange: (cb: (newId: unknown, oldId: unknown) => void) => () => void;
|
|
9
14
|
/**
|
|
10
15
|
* Inject (or replace) the persistence driver at runtime.
|
|
11
16
|
* Call this inside your plugin factory so the real driver
|
|
@@ -9,6 +9,7 @@ class ConfigStoreImpl {
|
|
|
9
9
|
available = $state([]);
|
|
10
10
|
#options;
|
|
11
11
|
#driver;
|
|
12
|
+
#callbacks = new Set();
|
|
12
13
|
constructor(options) {
|
|
13
14
|
this.#options = options;
|
|
14
15
|
const { items, idKey, storageKey, driver = createInMemoryDriver() } =
|
|
@@ -21,15 +22,19 @@ class ConfigStoreImpl {
|
|
|
21
22
|
if (saved && this.get(saved)) {
|
|
22
23
|
this.current = saved;
|
|
23
24
|
}
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
current: this.current,
|
|
29
|
-
},
|
|
30
|
-
);
|
|
25
|
+
// Only log here if we were initialized with a real driver (not the default in-memory)
|
|
26
|
+
if (DEV && options.driver) {
|
|
27
|
+
this.#logConfig();
|
|
31
28
|
}
|
|
32
29
|
}
|
|
30
|
+
#logConfig() {
|
|
31
|
+
console.log(
|
|
32
|
+
`${this.#options.icon ?? "βοΈ"} ${this.#options.displayName} configured:`,
|
|
33
|
+
{
|
|
34
|
+
current: this.current,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Replace the persistence driver and immediately re-read any saved value.
|
|
35
40
|
*
|
|
@@ -45,6 +50,9 @@ class ConfigStoreImpl {
|
|
|
45
50
|
if (saved && this.get(saved)) {
|
|
46
51
|
this.current = saved;
|
|
47
52
|
}
|
|
53
|
+
if (DEV) {
|
|
54
|
+
this.#logConfig();
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
58
|
* Set current item with validation
|
|
@@ -55,8 +63,29 @@ class ConfigStoreImpl {
|
|
|
55
63
|
console.warn(`${this.#options.displayName} "${id}" not found`);
|
|
56
64
|
return;
|
|
57
65
|
}
|
|
66
|
+
const old = this.current;
|
|
58
67
|
this.current = id;
|
|
59
68
|
this.#driver.set(this.#options.storageKey, String(id));
|
|
69
|
+
// Call callbacks after state update and persistence
|
|
70
|
+
this.#callbacks.forEach((cb) => {
|
|
71
|
+
try {
|
|
72
|
+
cb(id, old);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (DEV) {
|
|
75
|
+
console.error(
|
|
76
|
+
`[rune-lab] Error in ${this.#options.displayName} onChange callback:`,
|
|
77
|
+
err,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Register a change callback
|
|
85
|
+
*/
|
|
86
|
+
onChange(cb) {
|
|
87
|
+
this.#callbacks.add(cb);
|
|
88
|
+
return () => this.#callbacks.delete(cb);
|
|
60
89
|
}
|
|
61
90
|
/**
|
|
62
91
|
* Get item by id
|
package/dist/mod.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { portal } from "../../../kernel/src/mod.js";
|
|
1
|
+
<script lang="ts" module>
|
|
3
2
|
import { type Snippet } from "svelte";
|
|
4
3
|
|
|
4
|
+
export interface AppSettingSelectorProps<T> {
|
|
5
|
+
options: T[];
|
|
6
|
+
value: T;
|
|
7
|
+
item: Snippet<[T]>;
|
|
8
|
+
triggerLabel: Snippet<[T]>;
|
|
9
|
+
tooltip?: string;
|
|
10
|
+
direction?: "top" | "bottom" | "left" | "right" | "end" | "auto";
|
|
11
|
+
responsive?: boolean;
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" generics="T">
|
|
16
|
+
import { portal } from "../../../kernel/src/mod.js";
|
|
17
|
+
|
|
5
18
|
let {
|
|
6
19
|
options,
|
|
7
20
|
value,
|
|
@@ -11,10 +24,10 @@
|
|
|
11
24
|
direction = "bottom",
|
|
12
25
|
responsive = true,
|
|
13
26
|
} = $props<{
|
|
14
|
-
options:
|
|
15
|
-
value:
|
|
16
|
-
item: Snippet<[
|
|
17
|
-
triggerLabel: Snippet<[
|
|
27
|
+
options: T[];
|
|
28
|
+
value: T;
|
|
29
|
+
item: Snippet<[T]>;
|
|
30
|
+
triggerLabel: Snippet<[T]>;
|
|
18
31
|
tooltip?: string;
|
|
19
32
|
direction?: "top" | "bottom" | "left" | "right" | "end" | "auto";
|
|
20
33
|
responsive?: boolean;
|
|
@@ -131,12 +144,20 @@
|
|
|
131
144
|
class="border-b border-base-100 last:border-0"
|
|
132
145
|
role="menuitem"
|
|
133
146
|
>
|
|
134
|
-
<
|
|
135
|
-
|
|
147
|
+
<div
|
|
148
|
+
role="button"
|
|
149
|
+
tabindex="0"
|
|
150
|
+
class="w-full text-left py-3 cursor-pointer hover:bg-base-200 px-4 transition-colors"
|
|
136
151
|
onclick={() => modal?.close()}
|
|
152
|
+
onkeydown={(e) => {
|
|
153
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
modal?.close();
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
137
158
|
>
|
|
138
159
|
{@render item(option)}
|
|
139
|
-
</
|
|
160
|
+
</div>
|
|
140
161
|
</li>
|
|
141
162
|
{/each}
|
|
142
163
|
</ul>
|
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { type Snippet } from "svelte";
|
|
3
|
+
export interface AppSettingSelectorProps<T> {
|
|
4
|
+
options: T[];
|
|
5
|
+
value: T;
|
|
6
|
+
item: Snippet<[T]>;
|
|
7
|
+
triggerLabel: Snippet<[T]>;
|
|
8
|
+
tooltip?: string;
|
|
9
|
+
direction?: "top" | "bottom" | "left" | "right" | "end" | "auto";
|
|
10
|
+
responsive?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare class __sveltets_Render<T> {
|
|
13
|
+
props(): Record<string, never>;
|
|
14
|
+
events(): {} & {
|
|
5
15
|
[evt: string]: CustomEvent<any>;
|
|
6
16
|
};
|
|
7
|
-
slots: {};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type AppSettingSelectorEvents =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
slots(): {};
|
|
18
|
+
}
|
|
19
|
+
export type AppSettingSelectorProps_<T> = ReturnType<
|
|
20
|
+
__sveltets_Render<T>["props"]
|
|
21
|
+
>;
|
|
22
|
+
export type AppSettingSelectorEvents<T> = ReturnType<
|
|
23
|
+
__sveltets_Render<T>["events"]
|
|
24
|
+
>;
|
|
25
|
+
export type AppSettingSelectorSlots<T> = ReturnType<
|
|
26
|
+
__sveltets_Render<T>["slots"]
|
|
27
|
+
>;
|
|
28
|
+
export default class AppSettingSelector<T> extends SvelteComponentTyped<
|
|
29
|
+
AppSettingSelectorProps_<T>,
|
|
30
|
+
AppSettingSelectorEvents<T>,
|
|
31
|
+
AppSettingSelectorSlots<T>
|
|
18
32
|
> {
|
|
19
33
|
}
|
|
20
34
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ResourceSelector from "./ResourceSelector.svelte";
|
|
3
3
|
import { getLanguageStore } from "../../../kernel/src/mod.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getLanguageName } from "./language.svelte.js";
|
|
5
5
|
|
|
6
6
|
const languageStore = getLanguageStore();
|
|
7
7
|
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
{onchange}
|
|
26
26
|
>
|
|
27
27
|
{#snippet triggerLabel(active)}
|
|
28
|
-
<
|
|
28
|
+
<div class="flex items-center gap-2">
|
|
29
|
+
<span class="text-lg">{active.flag}</span>
|
|
30
|
+
</div>
|
|
29
31
|
{/snippet}
|
|
30
32
|
|
|
31
33
|
{#snippet item(l)}
|
|
@@ -33,13 +35,13 @@
|
|
|
33
35
|
class="flex items-center gap-3 w-full"
|
|
34
36
|
onclick={() => {
|
|
35
37
|
languageStore.set(l.code);
|
|
36
|
-
setLocale(l.code as any);
|
|
37
38
|
current = l.code;
|
|
38
39
|
onchange?.(l.code);
|
|
39
40
|
}}
|
|
40
41
|
>
|
|
41
42
|
<span class="text-lg">{l.flag}</span>
|
|
42
|
-
<span>{l.code
|
|
43
|
+
<span class="text-xs opacity-50 uppercase">{l.code}</span>
|
|
44
|
+
<span class="flex-grow text-left">{getLanguageName(l)}</span>
|
|
43
45
|
</button>
|
|
44
46
|
{/snippet}
|
|
45
47
|
</ResourceSelector>
|
|
@@ -61,18 +61,20 @@
|
|
|
61
61
|
);
|
|
62
62
|
</script>
|
|
63
63
|
|
|
64
|
+
{#snippet _triggerLabel(v: any)}
|
|
65
|
+
{@render triggerLabel(v)}
|
|
66
|
+
{/snippet}
|
|
67
|
+
|
|
68
|
+
{#snippet _item(option: any)}
|
|
69
|
+
{@render item(option)}
|
|
70
|
+
{/snippet}
|
|
71
|
+
|
|
64
72
|
<AppSettingSelector
|
|
65
73
|
value={active}
|
|
66
74
|
options={available}
|
|
67
75
|
tooltip={resolveLabel(active)}
|
|
68
76
|
{direction}
|
|
69
77
|
{responsive}
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
{/snippet}
|
|
74
|
-
|
|
75
|
-
{#snippet item(option)}
|
|
76
|
-
{@render item(option)}
|
|
77
|
-
{/snippet}
|
|
78
|
-
</AppSettingSelector>
|
|
78
|
+
triggerLabel={_triggerLabel}
|
|
79
|
+
item={_item}
|
|
80
|
+
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ResourceSelector from "./ResourceSelector.svelte";
|
|
3
|
-
import { getThemeStore
|
|
3
|
+
import { getThemeStore } from "../../../kernel/src/mod.js";
|
|
4
|
+
import { getThemeName } from "./theme.svelte.js";
|
|
4
5
|
|
|
5
6
|
const themeStore = getThemeStore();
|
|
6
7
|
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
{onchange}
|
|
23
24
|
>
|
|
24
25
|
{#snippet triggerLabel(active)}
|
|
25
|
-
<
|
|
26
|
+
<div class="flex items-center gap-2">
|
|
27
|
+
<span class="text-lg">{active.icon}</span>
|
|
28
|
+
</div>
|
|
26
29
|
{/snippet}
|
|
27
30
|
|
|
28
31
|
{#snippet item(t)}
|
|
@@ -42,8 +45,8 @@
|
|
|
42
45
|
bind:group={themeStore.current}
|
|
43
46
|
/>
|
|
44
47
|
<span class="text-lg">{t.icon}</span>
|
|
45
|
-
<span class="capitalize">
|
|
46
|
-
{t
|
|
48
|
+
<span class="flex-grow text-left capitalize">
|
|
49
|
+
{getThemeName(t)}
|
|
47
50
|
</span>
|
|
48
51
|
</button>
|
|
49
52
|
{/snippet}
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
} from "../../../kernel/src/mod.js";
|
|
6
6
|
import type { Language } from "../../../kernel/src/mod.js";
|
|
7
7
|
export type { Language };
|
|
8
|
+
/**
|
|
9
|
+
* Resolver to get the display name of a language in the current locale
|
|
10
|
+
*/
|
|
11
|
+
export declare const getLanguageName: (option: Language) => string;
|
|
8
12
|
export declare const LANGUAGES: readonly [{
|
|
9
13
|
readonly code: "es";
|
|
10
14
|
readonly flag: "π²π½";
|
|
@@ -3,6 +3,14 @@ import {
|
|
|
3
3
|
createConfigStore,
|
|
4
4
|
getLanguageStore,
|
|
5
5
|
} from "../../../kernel/src/mod.js";
|
|
6
|
+
import { createMessageResolver } from "../../../i18n/message-resolver.js";
|
|
7
|
+
import * as m from "../../../i18n/paraglide/messages.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolver to get the display name of a language in the current locale
|
|
10
|
+
*/
|
|
11
|
+
export const getLanguageName = createMessageResolver(m, {
|
|
12
|
+
keyExtractor: (l) => l.code,
|
|
13
|
+
});
|
|
6
14
|
export const LANGUAGES = [
|
|
7
15
|
// --- INDOEUROPEAS (Rama Romance / LatΓn) ---
|
|
8
16
|
{ code: "es", flag: "π²π½" },
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createConfigStore, getThemeStore } from "../../../kernel/src/mod.js";
|
|
2
2
|
import type { Theme } from "../../../kernel/src/mod.js";
|
|
3
3
|
export type { Theme };
|
|
4
|
+
/**
|
|
5
|
+
* Resolver to get the display name of a theme in the current locale
|
|
6
|
+
*/
|
|
7
|
+
export declare const getThemeName: (option: Theme) => string;
|
|
4
8
|
export declare const themeStore: ConfigStore<Theme>;
|
|
5
9
|
export type ThemeStore = ReturnType<typeof createConfigStore<Theme>>;
|
|
6
10
|
export { getThemeStore };
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
// client/packages/ui/src/state/theme-config.svelte.ts
|
|
3
3
|
import { createConfigStore, getThemeStore } from "../../../kernel/src/mod.js";
|
|
4
4
|
import { BROWSER } from "esm-env";
|
|
5
|
+
import { createMessageResolver } from "../../../i18n/message-resolver.js";
|
|
6
|
+
import * as m from "../../../i18n/paraglide/messages.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolver to get the display name of a theme in the current locale
|
|
9
|
+
*/
|
|
10
|
+
export const getThemeName = createMessageResolver(m, {
|
|
11
|
+
keyExtractor: (t) => t.name,
|
|
12
|
+
});
|
|
5
13
|
// Icon map for known themes - unknown ones fall back to π¨
|
|
6
14
|
const THEME_ICONS = {
|
|
7
15
|
light: "π",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ResourceSelector } from "../../../layout/src/mod.js";
|
|
3
3
|
import { getCurrencyStore } from "../../../../kernel/src/mod.js";
|
|
4
|
+
import { getCurrencyName } from "./currency.svelte.js";
|
|
4
5
|
|
|
5
6
|
const currencyStore = getCurrencyStore();
|
|
6
7
|
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
{onchange}
|
|
23
24
|
>
|
|
24
25
|
{#snippet triggerLabel(active)}
|
|
25
|
-
<
|
|
26
|
+
<div class="flex items-center gap-2">
|
|
27
|
+
<span class="font-bold">{active.symbol}</span>
|
|
28
|
+
</div>
|
|
26
29
|
{/snippet}
|
|
27
30
|
|
|
28
31
|
{#snippet item(c)}
|
|
@@ -35,7 +38,8 @@
|
|
|
35
38
|
}}
|
|
36
39
|
>
|
|
37
40
|
<span class="badge badge-sm badge-ghost w-8">{c.symbol}</span>
|
|
38
|
-
<span>{c.code}</span>
|
|
41
|
+
<span class="text-xs opacity-50 uppercase">{c.code}</span>
|
|
42
|
+
<span class="flex-grow text-left">{getCurrencyName(c)}</span>
|
|
39
43
|
</button>
|
|
40
44
|
{/snippet}
|
|
41
45
|
</ResourceSelector>
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
import type { Currency } from "../../../../kernel/src/mod.js";
|
|
6
6
|
import type { ExchangeRateStore } from "./exchange-rate.svelte.js";
|
|
7
7
|
export type { Currency };
|
|
8
|
+
/**
|
|
9
|
+
* Resolver to get the display name of a currency in the current locale
|
|
10
|
+
*/
|
|
11
|
+
export declare const getCurrencyName: (option: Currency) => string;
|
|
8
12
|
export declare function setExchangeRateStore(store: ExchangeRateStore): void;
|
|
9
13
|
/**
|
|
10
14
|
* Extension: Atomic currency registration.
|
|
@@ -3,6 +3,14 @@ import {
|
|
|
3
3
|
getCurrencyStore,
|
|
4
4
|
} from "../../../../kernel/src/mod.js";
|
|
5
5
|
import { registerCurrency } from "./mod.js";
|
|
6
|
+
import { createMessageResolver } from "../../../../i18n/message-resolver.js";
|
|
7
|
+
import * as m from "../../../../i18n/paraglide/messages.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolver to get the display name of a currency in the current locale
|
|
10
|
+
*/
|
|
11
|
+
export const getCurrencyName = createMessageResolver(m, {
|
|
12
|
+
keyExtractor: (c) => c.code,
|
|
13
|
+
});
|
|
6
14
|
/**
|
|
7
15
|
* Helper to build a minimal Dinero definition from Currency metadata.
|
|
8
16
|
* Assumes base 10 (standard decimal) for auto-registration.
|
|
@@ -7,6 +7,9 @@ export * from "./strategies.js";
|
|
|
7
7
|
export * from "./money-primitive.js";
|
|
8
8
|
export * from "./useMoney.js";
|
|
9
9
|
export * from "./useMoneyFilter.svelte.js";
|
|
10
|
+
export { default as CurrencySelector } from "./CurrencySelector.svelte";
|
|
11
|
+
export { default as MoneyDisplay } from "./MoneyDisplay.svelte";
|
|
12
|
+
export { default as MoneyInput } from "./MoneyInput.svelte";
|
|
10
13
|
/**
|
|
11
14
|
* Money Plugin β provides currency management and exchange rate conversion.
|
|
12
15
|
*/
|
|
@@ -9,6 +9,9 @@ export * from "./strategies.js";
|
|
|
9
9
|
export * from "./money-primitive.js";
|
|
10
10
|
export * from "./useMoney.js";
|
|
11
11
|
export * from "./useMoneyFilter.svelte.js";
|
|
12
|
+
export { default as CurrencySelector } from "./CurrencySelector.svelte";
|
|
13
|
+
export { default as MoneyDisplay } from "./MoneyDisplay.svelte";
|
|
14
|
+
export { default as MoneyInput } from "./MoneyInput.svelte";
|
|
12
15
|
/**
|
|
13
16
|
* Money Plugin β provides currency management and exchange rate conversion.
|
|
14
17
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune-lab",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Modern toolkit for Svelte 5 Runes applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"readme": "./README.md",
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/Yrrrrrf/rune-lab.git"
|
|
12
12
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"dev": "vite dev",
|
|
15
|
-
"gen:version": "node scripts/gen-version.js"
|
|
16
|
-
},
|
|
17
13
|
"exports": {
|
|
18
14
|
".": {
|
|
19
15
|
"svelte": "./dist/mod.js",
|
|
@@ -59,4 +55,4 @@
|
|
|
59
55
|
"dist/**",
|
|
60
56
|
"**/*.test.ts"
|
|
61
57
|
]
|
|
62
|
-
}
|
|
58
|
+
}
|