rune-lab 0.3.0 → 0.4.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/README.md +1 -1
- package/dist/core/design-tokens/props.d.ts +52 -0
- package/dist/core/design-tokens/props.d.ts.map +1 -0
- package/dist/core/design-tokens/props.js +34 -0
- package/dist/core/exchange-rate/strategies.d.ts +52 -0
- package/dist/core/exchange-rate/strategies.d.ts.map +1 -0
- package/dist/core/exchange-rate/strategies.js +72 -0
- package/dist/core/index.d.ts +8 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +8 -3
- package/dist/core/internal/message-resolver.d.ts +1 -1
- package/dist/core/internal/message-resolver.d.ts.map +1 -1
- package/dist/core/layout/types.d.ts +60 -0
- package/dist/core/layout/types.d.ts.map +1 -0
- package/dist/core/layout/types.js +4 -0
- package/dist/core/money/index.d.ts +1 -1
- package/dist/core/money/index.d.ts.map +1 -1
- package/dist/core/money/index.js +1 -1
- package/dist/core/money/money-primitive.d.ts +101 -0
- package/dist/core/money/money-primitive.d.ts.map +1 -0
- package/dist/core/money/money-primitive.js +161 -0
- package/dist/core/money/money.d.ts +74 -2
- package/dist/core/money/money.d.ts.map +1 -1
- package/dist/core/money/money.js +120 -2
- package/dist/core/shortcuts/types.d.ts +60 -0
- package/dist/core/shortcuts/types.d.ts.map +1 -0
- package/dist/core/shortcuts/types.js +4 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +6 -4
- package/dist/state/api.svelte.js +2 -2
- package/dist/state/app.svelte.js +1 -1
- package/dist/state/auth/index.d.ts +2 -2
- package/dist/state/auth/index.js +1 -1
- package/dist/state/auth/session.svelte.d.ts +1 -1
- package/dist/state/auth/session.svelte.js +7 -5
- package/dist/state/auth/types.d.ts +1 -1
- package/dist/state/cart.svelte.d.ts +1 -1
- package/dist/state/cart.svelte.js +1 -1
- package/dist/state/commands.svelte.d.ts +7 -7
- package/dist/state/commands.svelte.js +1 -1
- package/dist/state/composables/useMoney.d.ts +19 -3
- package/dist/state/composables/useMoney.js +70 -6
- package/dist/state/composables/useMoneyFilter.d.ts +20 -0
- package/dist/state/composables/useMoneyFilter.js +81 -0
- package/dist/state/composables/usePersistence.d.ts +1 -1
- package/dist/state/composables/usePersistence.js +1 -1
- package/dist/state/composables/useRuneLab.d.ts +1 -1
- package/dist/state/composables/useRuneLab.js +2 -2
- package/dist/state/composables/useShortcuts.d.ts +33 -0
- package/dist/state/composables/useShortcuts.js +75 -0
- package/dist/state/context.d.ts +1 -0
- package/dist/state/context.js +1 -0
- package/dist/state/createConfigStore.svelte.d.ts +4 -31
- package/dist/state/createConfigStore.svelte.js +62 -51
- package/dist/state/currency.svelte.d.ts +13 -9
- package/dist/state/currency.svelte.js +26 -10
- package/dist/state/currency.test.d.ts +1 -0
- package/dist/state/currency.test.js +35 -0
- package/dist/state/exchange-rate.svelte.d.ts +43 -0
- package/dist/state/exchange-rate.svelte.js +145 -0
- package/dist/state/exchange-rate.test.d.ts +1 -0
- package/dist/state/exchange-rate.test.js +75 -0
- package/dist/state/index.d.ts +26 -19
- package/dist/state/index.js +25 -18
- package/dist/state/language.svelte.d.ts +3 -10
- package/dist/state/language.svelte.js +4 -5
- package/dist/state/layout.svelte.d.ts +1 -1
- package/dist/state/layout.svelte.js +4 -4
- package/dist/state/persistence/drivers.d.ts +1 -1
- package/dist/state/persistence/drivers.js +9 -7
- package/dist/state/persistence/drivers.test.d.ts +1 -0
- package/dist/state/persistence/drivers.test.js +79 -0
- package/dist/state/persistence/provider.d.ts +23 -0
- package/dist/state/persistence/provider.js +43 -0
- package/dist/state/persistence/provider.test.d.ts +1 -0
- package/dist/state/persistence/provider.test.js +51 -0
- package/dist/state/registry/index.d.ts +44 -0
- package/dist/state/registry/index.js +58 -0
- package/dist/state/registry/registry.test.d.ts +1 -0
- package/dist/state/registry/registry.test.js +112 -0
- package/dist/state/registry/types.d.ts +20 -0
- package/dist/state/registry/types.js +3 -0
- package/dist/state/shortcuts.svelte.js +4 -4
- package/dist/state/theme.svelte.d.ts +3 -10
- package/dist/state/theme.svelte.js +8 -8
- package/dist/state/toast-bridge.d.ts +1 -1
- package/dist/state/toast.svelte.js +1 -1
- package/dist/ui/components/ApiMonitor.svelte +2 -2
- package/dist/ui/components/Icon.svelte +1 -1
- package/dist/ui/components/RuneProvider.svelte +28 -8
- package/dist/ui/components/RuneProvider.svelte.d.ts +12 -5
- package/dist/ui/components/Toaster.svelte +1 -1
- package/dist/ui/components/money/MoneyDisplay.svelte +91 -18
- package/dist/ui/components/money/MoneyDisplay.svelte.d.ts +15 -3
- package/dist/ui/components/money/MoneyDisplay.svelte.test.d.ts +1 -1
- package/dist/ui/components/money/MoneyDisplay.svelte.test.js +45 -2
- package/dist/ui/components/money/MoneyInput.svelte +123 -42
- package/dist/ui/components/money/MoneyInput.svelte.d.ts +14 -5
- package/dist/ui/features/command-palette/CommandPalette.svelte +3 -3
- package/dist/ui/features/config/APP_CONFIGURATIONS.d.ts +29 -0
- package/dist/ui/features/config/APP_CONFIGURATIONS.js +38 -0
- package/dist/ui/features/config/CurrencySelector.svelte +10 -36
- package/dist/ui/features/config/LanguageSelector.svelte +10 -33
- package/dist/ui/features/config/ResourceSelector.svelte +92 -0
- package/dist/ui/features/config/ResourceSelector.svelte.d.ts +25 -0
- package/dist/ui/features/config/ThemeSelector.svelte +11 -34
- package/dist/ui/features/shortcuts/ShortcutBinder.svelte +17 -0
- package/dist/ui/features/shortcuts/ShortcutBinder.svelte.d.ts +7 -0
- package/dist/ui/features/shortcuts/ShortcutPalette.svelte +3 -3
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.js +8 -3
- package/dist/ui/layout/ConnectedNavigationPanel.svelte +7 -8
- package/dist/ui/layout/ConnectedNavigationPanel.svelte.d.ts +1 -1
- package/dist/ui/layout/ConnectedWorkspaceStrip.svelte +5 -3
- package/dist/ui/layout/ConnectedWorkspaceStrip.svelte.d.ts +1 -1
- package/dist/ui/layout/NavigationPanel.svelte +1 -1
- package/dist/ui/layout/NavigationPanel.svelte.d.ts +1 -1
- package/dist/ui/layout/WorkspaceLayout.svelte +9 -1
- package/dist/ui/layout/WorkspaceLayout.svelte.d.ts +7 -0
- package/dist/ui/layout/WorkspaceStrip.svelte +1 -1
- package/dist/ui/layout/WorkspaceStrip.svelte.d.ts +1 -1
- package/dist/ui/layout/connection-factory.d.ts +50 -0
- package/dist/ui/layout/connection-factory.js +58 -0
- package/dist/ui/layout/index.d.ts +2 -2
- package/dist/ui/layout/index.js +1 -1
- package/dist/ui/paraglide/README.md +53 -0
- package/dist/ui/paraglide/runtime.d.ts +105 -124
- package/dist/ui/paraglide/runtime.js +162 -127
- package/dist/ui/paraglide/server.d.ts +6 -17
- package/dist/ui/paraglide/server.js +11 -20
- package/dist/ui/primitives/DatePicker.svelte +1 -1
- package/package.json +8 -8
- package/dist/state/daisyui.d.ts +0 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// sdk/state/src/composables/useMoneyFilter.ts
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import { RUNE_LAB_CONTEXT } from "../context.ts";
|
|
4
|
+
import { useMoney } from "./useMoney.ts";
|
|
5
|
+
/**
|
|
6
|
+
* A reactive currency-aware filter composable.
|
|
7
|
+
* Handles min/max thresholds that auto-convert when the display currency changes.
|
|
8
|
+
*/
|
|
9
|
+
export function useMoneyFilter(options = {}) {
|
|
10
|
+
const currencyStore = getContext(RUNE_LAB_CONTEXT.currency);
|
|
11
|
+
const { format } = useMoney();
|
|
12
|
+
let min = $state(options.min ?? 0);
|
|
13
|
+
let max = $state(options.max ?? Infinity);
|
|
14
|
+
const unit = options.unit ?? "minor";
|
|
15
|
+
const autoConvertOnChange = options.autoConvertOnChange ?? true;
|
|
16
|
+
let lastCurrency = String(currencyStore.current);
|
|
17
|
+
$effect(() => {
|
|
18
|
+
const currentCurrency = String(currencyStore.current);
|
|
19
|
+
if (autoConvertOnChange && currentCurrency !== lastCurrency) {
|
|
20
|
+
if (currencyStore.canConvert) {
|
|
21
|
+
if (min !== 0) {
|
|
22
|
+
min = currencyStore.convertAmount(min, lastCurrency, currentCurrency);
|
|
23
|
+
}
|
|
24
|
+
if (max !== Infinity) {
|
|
25
|
+
max = currencyStore.convertAmount(max, lastCurrency, currentCurrency);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Reset if no rates available
|
|
30
|
+
min = 0;
|
|
31
|
+
max = Infinity;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
lastCurrency = currentCurrency;
|
|
35
|
+
});
|
|
36
|
+
function setMin(value) {
|
|
37
|
+
min = value;
|
|
38
|
+
}
|
|
39
|
+
function setMax(value) {
|
|
40
|
+
max = value;
|
|
41
|
+
}
|
|
42
|
+
function reset() {
|
|
43
|
+
min = 0;
|
|
44
|
+
max = Infinity;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks if an amount (in its entity currency) matches the current filter.
|
|
48
|
+
*/
|
|
49
|
+
function matches(amount, entityCurrency) {
|
|
50
|
+
const currentCurrency = String(currencyStore.current);
|
|
51
|
+
// Convert entity amount to display currency for comparison
|
|
52
|
+
const convertedAmount = currencyStore.convertAmount(amount, entityCurrency, currentCurrency);
|
|
53
|
+
return convertedAmount >= min && convertedAmount <= max;
|
|
54
|
+
}
|
|
55
|
+
const displayMin = $derived(format(min, undefined, unit));
|
|
56
|
+
const displayMax = $derived(max === Infinity ? "∞" : format(max, undefined, unit));
|
|
57
|
+
return {
|
|
58
|
+
get min() {
|
|
59
|
+
return min;
|
|
60
|
+
},
|
|
61
|
+
set min(v) {
|
|
62
|
+
min = v;
|
|
63
|
+
},
|
|
64
|
+
get max() {
|
|
65
|
+
return max;
|
|
66
|
+
},
|
|
67
|
+
set max(v) {
|
|
68
|
+
max = v;
|
|
69
|
+
},
|
|
70
|
+
get displayMin() {
|
|
71
|
+
return displayMin;
|
|
72
|
+
},
|
|
73
|
+
get displayMax() {
|
|
74
|
+
return displayMax;
|
|
75
|
+
},
|
|
76
|
+
setMin,
|
|
77
|
+
setMax,
|
|
78
|
+
reset,
|
|
79
|
+
matches,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { PersistenceDriver } from "
|
|
1
|
+
import type { PersistenceDriver } from "@internal/core";
|
|
2
2
|
export declare function usePersistence(): PersistenceDriver | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getApiStore, getAppStore, getCommandStore, getCurrencyStore, getLanguageStore, getLayoutStore, getShortcutStore, getThemeStore, getToastStore, type SessionStore } from "../index";
|
|
1
|
+
import { getApiStore, getAppStore, getCommandStore, getCurrencyStore, getLanguageStore, getLayoutStore, getShortcutStore, getThemeStore, getToastStore, type SessionStore } from "../index.ts";
|
|
2
2
|
export interface RuneLabContext {
|
|
3
3
|
app: ReturnType<typeof getAppStore>;
|
|
4
4
|
api: ReturnType<typeof getApiStore>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/lib/composables/useRuneLab.ts
|
|
2
|
-
import { getApiStore, getAppStore, getCommandStore, getCurrencyStore, getLanguageStore, getLayoutStore, getShortcutStore, getThemeStore, getToastStore, } from "../index";
|
|
2
|
+
import { getApiStore, getAppStore, getCommandStore, getCurrencyStore, getLanguageStore, getLayoutStore, getShortcutStore, getThemeStore, getToastStore, } from "../index.ts";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
-
import { RUNE_LAB_CONTEXT } from "../context";
|
|
4
|
+
import { RUNE_LAB_CONTEXT } from "../context.ts";
|
|
5
5
|
/**
|
|
6
6
|
* Retrieves all Rune Lab stores from the Svelte context tree.
|
|
7
7
|
* Must be called during component initialization (in the `<script>` block).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ShortcutConfig } from "@internal/core";
|
|
2
|
+
/**
|
|
3
|
+
* Declarative shortcut registration composable.
|
|
4
|
+
* Registers all shortcuts via ShortcutStore in a single `$effect` and
|
|
5
|
+
* automatically unregisters them when the component is destroyed.
|
|
6
|
+
*
|
|
7
|
+
* Must be called at component initialization time (top-level in `<script>`),
|
|
8
|
+
* not inside event handlers.
|
|
9
|
+
*
|
|
10
|
+
* @param configs - Array of ShortcutConfig from sdk/core
|
|
11
|
+
* @returns Object with `registered` getter for the list of registered IDs
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { useShortcuts } from 'rune-lab/state';
|
|
17
|
+
*
|
|
18
|
+
* useShortcuts([
|
|
19
|
+
* {
|
|
20
|
+
* id: "save:document",
|
|
21
|
+
* keys: "ctrl+s",
|
|
22
|
+
* handler: (e) => { e.preventDefault(); save(); },
|
|
23
|
+
* label: "Save document",
|
|
24
|
+
* category: "Editing",
|
|
25
|
+
* },
|
|
26
|
+
* ]);
|
|
27
|
+
* </script>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function useShortcuts(configs: ShortcutConfig[]): {
|
|
31
|
+
/** IDs of all shortcuts registered by this composable */
|
|
32
|
+
readonly registered: string[];
|
|
33
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// sdk/state/src/composables/useShortcuts.ts
|
|
2
|
+
// Declarative shortcut registration with automatic lifecycle cleanup.
|
|
3
|
+
// Bridges ShortcutConfig (sdk/core) → ShortcutStore (sdk/state).
|
|
4
|
+
import { getShortcutStore } from "../shortcuts.svelte.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Declarative shortcut registration composable.
|
|
7
|
+
* Registers all shortcuts via ShortcutStore in a single `$effect` and
|
|
8
|
+
* automatically unregisters them when the component is destroyed.
|
|
9
|
+
*
|
|
10
|
+
* Must be called at component initialization time (top-level in `<script>`),
|
|
11
|
+
* not inside event handlers.
|
|
12
|
+
*
|
|
13
|
+
* @param configs - Array of ShortcutConfig from sdk/core
|
|
14
|
+
* @returns Object with `registered` getter for the list of registered IDs
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <script>
|
|
19
|
+
* import { useShortcuts } from 'rune-lab/state';
|
|
20
|
+
*
|
|
21
|
+
* useShortcuts([
|
|
22
|
+
* {
|
|
23
|
+
* id: "save:document",
|
|
24
|
+
* keys: "ctrl+s",
|
|
25
|
+
* handler: (e) => { e.preventDefault(); save(); },
|
|
26
|
+
* label: "Save document",
|
|
27
|
+
* category: "Editing",
|
|
28
|
+
* },
|
|
29
|
+
* ]);
|
|
30
|
+
* </script>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useShortcuts(configs) {
|
|
34
|
+
const shortcutStore = getShortcutStore();
|
|
35
|
+
// Convert ShortcutConfig[] to ShortcutEntry[] respecting the `when` predicate
|
|
36
|
+
function toEntries(configs) {
|
|
37
|
+
return configs.map((config) => ({
|
|
38
|
+
id: config.id,
|
|
39
|
+
keys: config.keys,
|
|
40
|
+
handler: (event) => {
|
|
41
|
+
// Check `when` predicate before firing
|
|
42
|
+
if (config.when && !config.when())
|
|
43
|
+
return;
|
|
44
|
+
config.handler(event);
|
|
45
|
+
},
|
|
46
|
+
label: config.label ?? config.id,
|
|
47
|
+
category: config.category ?? "General",
|
|
48
|
+
scope: config.scope ?? "global",
|
|
49
|
+
enabled: true,
|
|
50
|
+
hidden: !config.label, // Hide from palette if no label
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
// Register all shortcuts
|
|
54
|
+
const entries = toEntries(configs);
|
|
55
|
+
const ids = entries.map((e) => e.id);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
shortcutStore.register(entry);
|
|
58
|
+
}
|
|
59
|
+
// Auto-cleanup using $effect.root for lifecycle management
|
|
60
|
+
$effect(() => {
|
|
61
|
+
// This effect runs on mount — shortcuts are already registered above.
|
|
62
|
+
// Return cleanup that unregisters all shortcuts on destroy.
|
|
63
|
+
return () => {
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
shortcutStore.unregister(id);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
/** IDs of all shortcuts registered by this composable */
|
|
71
|
+
get registered() {
|
|
72
|
+
return ids;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/state/context.d.ts
CHANGED
package/dist/state/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type PersistenceDriver } from "
|
|
2
|
-
export type ConfigStore<T extends
|
|
1
|
+
import { type PersistenceDriver } from "@internal/core";
|
|
2
|
+
export type ConfigStore<T extends object> = {
|
|
3
3
|
current: T[keyof T];
|
|
4
4
|
available: T[];
|
|
5
5
|
set: (id: T[keyof T]) => void;
|
|
@@ -8,15 +8,7 @@ export type ConfigStore<T extends ConfigItem> = {
|
|
|
8
8
|
/** Append additional items (e.g. custom themes/currencies from the consuming app) */
|
|
9
9
|
addItems: (newItems: T[]) => void;
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
* Generic configuration store factory
|
|
13
|
-
* Creates type-safe stores for theme, language, currency, etc.
|
|
14
|
-
* with validation, persistence, and utilities
|
|
15
|
-
*/
|
|
16
|
-
export interface ConfigItem {
|
|
17
|
-
[key: string]: any;
|
|
18
|
-
}
|
|
19
|
-
export interface ConfigStoreOptions<T extends ConfigItem> {
|
|
11
|
+
export interface ConfigStoreOptions<T extends object> {
|
|
20
12
|
/** Array of available items */
|
|
21
13
|
items: readonly T[];
|
|
22
14
|
/** Storage key used by the persistence driver */
|
|
@@ -33,23 +25,4 @@ export interface ConfigStoreOptions<T extends ConfigItem> {
|
|
|
33
25
|
/**
|
|
34
26
|
* Creates a reactive configuration store with persistence
|
|
35
27
|
*/
|
|
36
|
-
export declare function createConfigStore<T extends
|
|
37
|
-
current: T[keyof T];
|
|
38
|
-
available: T[];
|
|
39
|
-
/**
|
|
40
|
-
* Set current item with validation
|
|
41
|
-
*/
|
|
42
|
-
set(id: T[keyof T]): void;
|
|
43
|
-
/**
|
|
44
|
-
* Get item by id
|
|
45
|
-
*/
|
|
46
|
-
get(id: T[keyof T]): T | undefined;
|
|
47
|
-
/**
|
|
48
|
-
* Get property from current or specified item
|
|
49
|
-
*/
|
|
50
|
-
getProp<K extends keyof T>(prop: K, id?: T[keyof T]): T[K] | undefined;
|
|
51
|
-
/**
|
|
52
|
-
* Append additional items (deduplicates by idKey)
|
|
53
|
-
*/
|
|
54
|
-
addItems(newItems: T[]): void;
|
|
55
|
-
};
|
|
28
|
+
export declare function createConfigStore<T extends object>(options: ConfigStoreOptions<T>): ConfigStore<T>;
|
|
@@ -1,61 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
import { createInMemoryDriver } from "./persistence/drivers";
|
|
1
|
+
import { createInMemoryDriver } from "./persistence/drivers.ts";
|
|
3
2
|
import { DEV } from "esm-env";
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* Internal class implementation for configuration stores.
|
|
5
|
+
* Stable class definition avoids potential issues with multiple definitions in SSR.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
7
|
+
class ConfigStoreImpl {
|
|
8
|
+
current = $state(null);
|
|
9
|
+
available = $state([]);
|
|
10
|
+
#options;
|
|
11
|
+
#driver;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.#options = options;
|
|
14
|
+
const { items, idKey, storageKey, driver = createInMemoryDriver() } = options;
|
|
15
|
+
this.#driver = driver;
|
|
16
|
+
this.available = [...items];
|
|
17
|
+
this.current = items[0]?.[idKey] ?? "";
|
|
18
|
+
const saved = this.#driver.get(storageKey);
|
|
19
|
+
// Only load saved if it actually exists in our available items
|
|
20
|
+
if (saved && this.get(saved)) {
|
|
21
|
+
this.current = saved;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const item = this.get(id);
|
|
29
|
-
if (!item) {
|
|
30
|
-
console.warn(`${displayName} "${id}" not found`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
this.current = id;
|
|
34
|
-
driver.set(storageKey, String(id));
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Get item by id
|
|
38
|
-
*/
|
|
39
|
-
get(id) {
|
|
40
|
-
return this.available.find((item) => item[idKey] === id);
|
|
23
|
+
if (DEV) {
|
|
24
|
+
console.log(`${options.icon ?? "⚙️"} ${options.displayName} configured:`, {
|
|
25
|
+
current: this.current,
|
|
26
|
+
});
|
|
41
27
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set current item with validation
|
|
31
|
+
*/
|
|
32
|
+
set(id) {
|
|
33
|
+
const item = this.get(id);
|
|
34
|
+
if (!item) {
|
|
35
|
+
console.warn(`${this.#options.displayName} "${id}" not found`);
|
|
36
|
+
return;
|
|
48
37
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
this.current = id;
|
|
39
|
+
this.#driver.set(this.#options.storageKey, String(id));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get item by id
|
|
43
|
+
*/
|
|
44
|
+
get(id) {
|
|
45
|
+
const idKey = this.#options.idKey;
|
|
46
|
+
return this.available.find((item) => item[idKey] === id);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get property from current or specified item
|
|
50
|
+
*/
|
|
51
|
+
getProp(prop, id) {
|
|
52
|
+
const targetId = id ?? this.current;
|
|
53
|
+
return this.get(targetId)?.[prop];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Append additional items (deduplicates by idKey)
|
|
57
|
+
*/
|
|
58
|
+
addItems(newItems) {
|
|
59
|
+
const idKey = this.#options.idKey;
|
|
60
|
+
for (const item of newItems) {
|
|
61
|
+
if (!this.get(item[idKey])) {
|
|
62
|
+
this.available.push(item);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
|
-
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates a reactive configuration store with persistence
|
|
69
|
+
*/
|
|
70
|
+
export function createConfigStore(options) {
|
|
71
|
+
return new ConfigStoreImpl(options);
|
|
61
72
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConfigStore, createConfigStore } from "./createConfigStore.svelte.ts";
|
|
1
2
|
/**
|
|
2
3
|
* Currency configuration
|
|
3
4
|
* Based on ISO 4217 currency codes
|
|
@@ -7,22 +8,25 @@ export interface Currency {
|
|
|
7
8
|
symbol: string;
|
|
8
9
|
decimals: number;
|
|
9
10
|
}
|
|
10
|
-
import type { PersistenceDriver } from "
|
|
11
|
+
import type { PersistenceDriver } from "@internal/core";
|
|
12
|
+
import { type ExchangeRateStore } from "./exchange-rate.svelte.ts";
|
|
11
13
|
export interface CurrencyStoreOptions {
|
|
12
14
|
driver?: PersistenceDriver | (() => PersistenceDriver | undefined);
|
|
13
15
|
/** Additional custom currencies to append to the built-in set */
|
|
14
16
|
customCurrencies?: Currency[];
|
|
15
17
|
/** Default currency code if no persisted value exists */
|
|
16
18
|
defaultCurrency?: string;
|
|
19
|
+
/** Wired exchange rate store for conversions */
|
|
20
|
+
exchangeRateStore?: ExchangeRateStore;
|
|
17
21
|
}
|
|
18
|
-
export declare function createCurrencyStore(driverOrOptions?: PersistenceDriver | (() => PersistenceDriver | undefined) | CurrencyStoreOptions): {
|
|
19
|
-
addCurrency: (meta: Currency, dineroDef?:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
export declare function createCurrencyStore(driverOrOptions?: PersistenceDriver | (() => PersistenceDriver | undefined) | CurrencyStoreOptions): ReturnType<typeof createConfigStore<Currency>> & {
|
|
23
|
+
addCurrency: (meta: Currency, dineroDef?: unknown) => void;
|
|
24
|
+
convertAmount: (amount: number, fromCode: string, toCode?: string) => number;
|
|
25
|
+
readonly canConvert: boolean;
|
|
22
26
|
};
|
|
23
27
|
export type CurrencyStore = ReturnType<typeof createCurrencyStore>;
|
|
24
|
-
export declare function getCurrencyStore(): {
|
|
25
|
-
addCurrency: (meta: Currency, dineroDef?:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
export declare function getCurrencyStore(): ConfigStore<Currency> & {
|
|
29
|
+
addCurrency: (meta: Currency, dineroDef?: unknown) => void;
|
|
30
|
+
convertAmount: (amount: number, fromCode: string, toCode?: string) => number;
|
|
31
|
+
readonly canConvert: boolean;
|
|
28
32
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createConfigStore, } from "./createConfigStore.svelte";
|
|
1
|
+
import { createConfigStore, } from "./createConfigStore.svelte.ts";
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
|
-
import { RUNE_LAB_CONTEXT } from "./context";
|
|
4
|
-
import { registerCurrency } from "
|
|
3
|
+
import { RUNE_LAB_CONTEXT } from "./context.ts";
|
|
4
|
+
import { registerCurrency } from "@internal/core";
|
|
5
5
|
/**
|
|
6
6
|
* Helper to build a minimal Dinero definition from Currency metadata.
|
|
7
7
|
* Assumes base 10 (standard decimal) for auto-registration.
|
|
@@ -26,6 +26,7 @@ const CURRENCIES = [
|
|
|
26
26
|
{ code: "KRW", symbol: "₩", decimals: 0 },
|
|
27
27
|
{ code: "AED", symbol: "د.إ", decimals: 2 },
|
|
28
28
|
];
|
|
29
|
+
import { resolveDriver } from "./persistence/provider.ts";
|
|
29
30
|
export function createCurrencyStore(driverOrOptions) {
|
|
30
31
|
// Normalize overloaded argument
|
|
31
32
|
const opts = driverOrOptions && typeof driverOrOptions === "object" &&
|
|
@@ -34,9 +35,7 @@ export function createCurrencyStore(driverOrOptions) {
|
|
|
34
35
|
: {
|
|
35
36
|
driver: driverOrOptions,
|
|
36
37
|
};
|
|
37
|
-
const resolvedDriver =
|
|
38
|
-
? opts.driver()
|
|
39
|
-
: opts.driver;
|
|
38
|
+
const resolvedDriver = resolveDriver(opts.driver);
|
|
40
39
|
const store = createConfigStore({
|
|
41
40
|
items: CURRENCIES,
|
|
42
41
|
storageKey: "currency",
|
|
@@ -45,6 +44,7 @@ export function createCurrencyStore(driverOrOptions) {
|
|
|
45
44
|
icon: "💰",
|
|
46
45
|
driver: resolvedDriver,
|
|
47
46
|
});
|
|
47
|
+
const exchangeRateStore = opts.exchangeRateStore;
|
|
48
48
|
/**
|
|
49
49
|
* Extension: Atomic currency registration.
|
|
50
50
|
* Updates both the Dinero registry (core) and the reactive store (UI).
|
|
@@ -57,6 +57,18 @@ export function createCurrencyStore(driverOrOptions) {
|
|
|
57
57
|
registerCurrency(meta.code, def);
|
|
58
58
|
store.addItems([meta]);
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Converts an amount from one currency to another.
|
|
62
|
+
* Defaults to converting to the current store currency.
|
|
63
|
+
*/
|
|
64
|
+
function convertAmount(amount, fromCode, toCode) {
|
|
65
|
+
const target = toCode ?? String(store.current);
|
|
66
|
+
if (fromCode === target)
|
|
67
|
+
return amount;
|
|
68
|
+
if (!exchangeRateStore || !exchangeRateStore.hasRates)
|
|
69
|
+
return amount;
|
|
70
|
+
return exchangeRateStore.convertAmount(amount, fromCode, target);
|
|
71
|
+
}
|
|
60
72
|
// Append and auto-register custom currencies if provided
|
|
61
73
|
if (opts.customCurrencies?.length) {
|
|
62
74
|
for (const c of opts.customCurrencies) {
|
|
@@ -70,10 +82,14 @@ export function createCurrencyStore(driverOrOptions) {
|
|
|
70
82
|
store.set(opts.defaultCurrency);
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
// Explicitly attach method to ensure prototype is preserved and method is available
|
|
86
|
+
// across different build environments.
|
|
87
|
+
Object.defineProperties(store, {
|
|
88
|
+
addCurrency: { value: addCurrency },
|
|
89
|
+
convertAmount: { value: convertAmount },
|
|
90
|
+
canConvert: { get: () => !!exchangeRateStore?.hasRates },
|
|
91
|
+
});
|
|
92
|
+
return store;
|
|
77
93
|
}
|
|
78
94
|
export function getCurrencyStore() {
|
|
79
95
|
return getContext(RUNE_LAB_CONTEXT.currency);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createCurrencyStore } from "./currency.svelte.ts";
|
|
3
|
+
describe("Currency Store", () => {
|
|
4
|
+
it("should have prototype methods from ConfigStore", () => {
|
|
5
|
+
const store = createCurrencyStore();
|
|
6
|
+
// These come from the ConfigStore class prototype
|
|
7
|
+
expect(typeof store.get).toBe("function");
|
|
8
|
+
expect(typeof store.set).toBe("function");
|
|
9
|
+
expect(typeof store.getProp).toBe("function");
|
|
10
|
+
expect(typeof store.addItems).toBe("function");
|
|
11
|
+
// These are instance properties
|
|
12
|
+
expect(store.current).toBeDefined();
|
|
13
|
+
expect(Array.isArray(store.available)).toBe(true);
|
|
14
|
+
// This is the extension method
|
|
15
|
+
expect(typeof store.addCurrency).toBe("function");
|
|
16
|
+
});
|
|
17
|
+
it("should correctly use prototype methods", () => {
|
|
18
|
+
const store = createCurrencyStore();
|
|
19
|
+
const usd = store.get("USD");
|
|
20
|
+
expect(usd).toBeDefined();
|
|
21
|
+
expect(usd?.code).toBe("USD");
|
|
22
|
+
expect(usd?.symbol).toBe("$");
|
|
23
|
+
});
|
|
24
|
+
it("should add a new currency and keep methods", () => {
|
|
25
|
+
const store = createCurrencyStore();
|
|
26
|
+
store.addCurrency({ code: "BTC", symbol: "₿", decimals: 8 });
|
|
27
|
+
const btc = store.get("BTC");
|
|
28
|
+
expect(btc).toBeDefined();
|
|
29
|
+
expect(btc?.code).toBe("BTC");
|
|
30
|
+
expect(btc?.decimals).toBe(8);
|
|
31
|
+
// Methods still exist after interaction
|
|
32
|
+
expect(typeof store.get).toBe("function");
|
|
33
|
+
expect(typeof store.set).toBe("function");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type RateMap, type ScaledRate } from "@internal/core";
|
|
2
|
+
export declare class ExchangeRateStore {
|
|
3
|
+
#private;
|
|
4
|
+
/**
|
|
5
|
+
* The current exchange rates relative to the base currency.
|
|
6
|
+
*/
|
|
7
|
+
get rates(): RateMap;
|
|
8
|
+
/**
|
|
9
|
+
* The base currency all rates are relative to.
|
|
10
|
+
*/
|
|
11
|
+
get baseCurrency(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if exchange rates have been loaded.
|
|
14
|
+
*/
|
|
15
|
+
get hasRates(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Set exchange rates from a human-readable float map.
|
|
18
|
+
* @param base - The base currency code (e.g., "USD")
|
|
19
|
+
* @param rawRates - A map of currency codes to float rates (e.g., { MXN: 17.23 })
|
|
20
|
+
*/
|
|
21
|
+
setRates(base: string, rawRates: Record<string, number>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Directly set pre-scaled rates.
|
|
24
|
+
*/
|
|
25
|
+
setScaledRates(base: string, rates: RateMap): void;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the exchange rate between two currencies.
|
|
28
|
+
* Returns a ScaledRate representing how many units of toCode are in 1 unit of fromCode.
|
|
29
|
+
*/
|
|
30
|
+
getRate(fromCode: string, toCode: string): ScaledRate | number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Internal conversion logic that handles triangulation through base currency.
|
|
33
|
+
*/
|
|
34
|
+
convertAmount(amount: number, fromCode: string, toCode: string): number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Factory to create the ExchangeRateStore.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createExchangeRateStore(): ExchangeRateStore;
|
|
40
|
+
/**
|
|
41
|
+
* Consumer to retrieve the ExchangeRateStore from context.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getExchangeRateStore(): ExchangeRateStore;
|