rune-lab 0.2.2 → 0.2.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.
Files changed (75) hide show
  1. package/dist/core/index.d.ts +1 -0
  2. package/dist/core/index.d.ts.map +1 -1
  3. package/dist/core/index.js +1 -0
  4. package/dist/core/money/index.d.ts +2 -0
  5. package/dist/core/money/index.d.ts.map +1 -0
  6. package/dist/core/money/index.js +2 -0
  7. package/dist/core/money/money.d.ts +37 -0
  8. package/dist/core/money/money.d.ts.map +1 -0
  9. package/dist/core/money/money.js +79 -0
  10. package/dist/index.d.ts +0 -6
  11. package/dist/index.js +1 -7
  12. package/dist/state/auth/index.d.ts +2 -0
  13. package/dist/state/auth/index.js +2 -0
  14. package/dist/state/auth/session.svelte.d.ts +40 -0
  15. package/dist/state/auth/session.svelte.js +57 -0
  16. package/dist/state/auth/types.d.ts +30 -0
  17. package/dist/state/auth/types.js +3 -0
  18. package/dist/state/cart.svelte.d.ts +64 -0
  19. package/dist/state/cart.svelte.js +122 -0
  20. package/dist/state/composables/useMoney.d.ts +15 -0
  21. package/dist/state/composables/useMoney.js +48 -0
  22. package/dist/state/composables/useRuneLab.d.ts +3 -1
  23. package/dist/state/composables/useRuneLab.js +11 -0
  24. package/dist/state/context.d.ts +2 -0
  25. package/dist/state/context.js +2 -0
  26. package/dist/state/createConfigStore.svelte.d.ts +8 -3
  27. package/dist/state/createConfigStore.svelte.js +10 -0
  28. package/dist/state/currency.svelte.d.ts +9 -1
  29. package/dist/state/currency.svelte.js +28 -7
  30. package/dist/state/index.d.ts +7 -4
  31. package/dist/state/index.js +6 -2
  32. package/dist/state/language.svelte.d.ts +1 -0
  33. package/dist/state/persistence/drivers.d.ts +3 -1
  34. package/dist/state/persistence/drivers.js +5 -1
  35. package/dist/state/theme.svelte.d.ts +9 -1
  36. package/dist/state/theme.svelte.js +34 -8
  37. package/dist/ui/components/Icon.svelte +5 -2
  38. package/dist/ui/components/RuneProvider.svelte +59 -4
  39. package/dist/ui/components/RuneProvider.svelte.d.ts +19 -0
  40. package/dist/ui/components/money/MoneyDisplay.svelte +59 -0
  41. package/dist/ui/components/money/MoneyDisplay.svelte.d.ts +13 -0
  42. package/dist/ui/components/money/MoneyInput.svelte +106 -0
  43. package/dist/ui/components/money/MoneyInput.svelte.d.ts +19 -0
  44. package/dist/ui/components/money/index.d.ts +2 -0
  45. package/dist/ui/components/money/index.js +3 -0
  46. package/dist/ui/components/user/UserAvatar.svelte +66 -0
  47. package/dist/ui/components/user/UserAvatar.svelte.d.ts +13 -0
  48. package/dist/ui/components/user/UserProfile.svelte +79 -0
  49. package/dist/ui/components/user/UserProfile.svelte.d.ts +17 -0
  50. package/dist/ui/components/user/index.d.ts +2 -0
  51. package/dist/ui/components/user/index.js +3 -0
  52. package/dist/ui/features/notifications/NotificationBell.svelte +89 -0
  53. package/dist/ui/features/notifications/NotificationBell.svelte.d.ts +11 -0
  54. package/dist/ui/features/notifications/index.d.ts +1 -0
  55. package/dist/ui/features/notifications/index.js +2 -0
  56. package/dist/ui/index.d.ts +6 -0
  57. package/dist/ui/index.js +10 -0
  58. package/dist/ui/layout/WorkspaceLayout.svelte +9 -0
  59. package/dist/ui/paraglide/messages/_index.d.ts +4 -0
  60. package/dist/ui/paraglide/messages/_index.js +4 -0
  61. package/dist/ui/paraglide/messages/brl3.d.ts +17 -0
  62. package/dist/ui/paraglide/messages/brl3.js +85 -0
  63. package/dist/ui/paraglide/messages/cad3.d.ts +17 -0
  64. package/dist/ui/paraglide/messages/cad3.js +85 -0
  65. package/dist/ui/paraglide/messages/gbp3.d.ts +17 -0
  66. package/dist/ui/paraglide/messages/gbp3.js +85 -0
  67. package/dist/ui/paraglide/messages/inr3.d.ts +17 -0
  68. package/dist/ui/paraglide/messages/inr3.js +85 -0
  69. package/dist/ui/primitives/DatePicker.svelte +257 -0
  70. package/dist/ui/primitives/DatePicker.svelte.d.ts +17 -0
  71. package/dist/ui/primitives/index.d.ts +1 -0
  72. package/dist/ui/primitives/index.js +3 -0
  73. package/package.json +28 -18
  74. package/dist/state/config.d.ts +0 -4
  75. package/dist/state/config.js +0 -8
@@ -46,6 +46,16 @@ export function createConfigStore(options) {
46
46
  const targetId = id ?? this.current;
47
47
  return this.get(targetId)?.[prop];
48
48
  }
49
+ /**
50
+ * Append additional items (deduplicates by idKey)
51
+ */
52
+ addItems(newItems) {
53
+ for (const item of newItems) {
54
+ if (!this.get(item[idKey])) {
55
+ this.available.push(item);
56
+ }
57
+ }
58
+ }
49
59
  }
50
60
  return new ConfigStore();
51
61
  }
@@ -9,11 +9,19 @@ export interface Currency {
9
9
  decimals: number;
10
10
  }
11
11
  import type { PersistenceDriver } from "rune-lab/core";
12
- export declare function createCurrencyStore(driver?: PersistenceDriver | (() => PersistenceDriver | undefined)): {
12
+ export interface CurrencyStoreOptions {
13
+ driver?: PersistenceDriver | (() => PersistenceDriver | undefined);
14
+ /** Additional custom currencies to append to the built-in set */
15
+ customCurrencies?: Currency[];
16
+ /** Default currency code if no persisted value exists */
17
+ defaultCurrency?: string;
18
+ }
19
+ export declare function createCurrencyStore(driverOrOptions?: PersistenceDriver | (() => PersistenceDriver | undefined) | CurrencyStoreOptions): {
13
20
  current: string | number;
14
21
  available: Currency[];
15
22
  set(id: string | number): void;
16
23
  get(id: string | number): Currency | undefined;
17
24
  getProp<K extends keyof Currency>(prop: K, id?: string | number | undefined): Currency[K] | undefined;
25
+ addItems(newItems: Currency[]): void;
18
26
  };
19
27
  export declare function getCurrencyStore(): ConfigStore<Currency>;
@@ -4,26 +4,47 @@ import { RUNE_LAB_CONTEXT } from "./context";
4
4
  const CURRENCIES = [
5
5
  { code: "USD", symbol: "$", decimals: 2 },
6
6
  { code: "EUR", symbol: "€", decimals: 2 },
7
+ { code: "GBP", symbol: "£", decimals: 2 },
7
8
  { code: "MXN", symbol: "$", decimals: 2 },
9
+ { code: "CAD", symbol: "C$", decimals: 2 },
10
+ { code: "BRL", symbol: "R$", decimals: 2 },
11
+ { code: "INR", symbol: "₹", decimals: 2 },
8
12
  { code: "CNY", symbol: "¥", decimals: 2 },
9
13
  { code: "JPY", symbol: "¥", decimals: 0 },
10
14
  { code: "KRW", symbol: "₩", decimals: 0 },
11
15
  { code: "AED", symbol: "د.إ", decimals: 2 },
12
16
  ];
13
- export function createCurrencyStore(driver) {
14
- return createConfigStore({
17
+ export function createCurrencyStore(driverOrOptions) {
18
+ // Normalize overloaded argument
19
+ const opts = driverOrOptions && typeof driverOrOptions === "object" &&
20
+ "driver" in driverOrOptions
21
+ ? driverOrOptions
22
+ : {
23
+ driver: driverOrOptions,
24
+ };
25
+ const resolvedDriver = typeof opts.driver === "function"
26
+ ? opts.driver()
27
+ : opts.driver;
28
+ const store = createConfigStore({
15
29
  items: CURRENCIES,
16
30
  storageKey: "currency",
17
31
  displayName: "Currency",
18
32
  idKey: "code",
19
33
  icon: "💰",
20
- driver: typeof driver === "function" ? driver() : driver,
34
+ driver: resolvedDriver,
21
35
  });
36
+ // Append custom currencies if provided
37
+ if (opts.customCurrencies?.length) {
38
+ store.addItems(opts.customCurrencies);
39
+ }
40
+ // Set default currency if provided and no persisted value found
41
+ if (!resolvedDriver?.get("currency") && opts.defaultCurrency) {
42
+ if (store.get(opts.defaultCurrency)) {
43
+ store.set(opts.defaultCurrency);
44
+ }
45
+ }
46
+ return store;
22
47
  }
23
48
  export function getCurrencyStore() {
24
49
  return getContext(RUNE_LAB_CONTEXT.currency);
25
50
  }
26
- // Usage:
27
- // currencyStore.set("USD")
28
- // currencyStore.getProp("symbol") // "$"
29
- // currencyStore.getProp("decimals") // 2
@@ -2,15 +2,18 @@ export { RUNE_LAB_CONTEXT } from "./context";
2
2
  export { useRuneLab } from "./composables/useRuneLab";
3
3
  export type { RuneLabContext } from "./composables/useRuneLab";
4
4
  export { usePersistence } from "./composables/usePersistence";
5
- export { cookieDriver, createInMemoryDriver, inMemoryDriver, localStorageDriver, sessionStorageDriver, } from "./persistence/drivers";
5
+ export { useMoney } from "./composables/useMoney";
6
+ export { cookieDriver, createCookieDriver, createInMemoryDriver, inMemoryDriver, localStorageDriver, sessionStorageDriver, } from "./persistence/drivers";
6
7
  export { type AppData, AppStore, createAppStore, getAppStore, } from "./app.svelte";
7
8
  export { createLayoutStore, getLayoutStore, LayoutStore, type NavigationItem, type NavigationSection, type WorkspaceItem, } from "./layout.svelte";
8
9
  export { ApiStore, type ConnectionState, createApiStore, getApiStore, } from "./api.svelte";
9
10
  export { createLanguageStore, getLanguageStore, type Language, } from "./language.svelte";
10
- export { createCurrencyStore, type Currency, getCurrencyStore, } from "./currency.svelte";
11
+ export { createCurrencyStore, type Currency, type CurrencyStoreOptions, getCurrencyStore, } from "./currency.svelte";
11
12
  export { createToastStore, getToastStore, ToastStore } from "./toast.svelte";
12
13
  export { type Command, CommandStore, createCommandStore, getCommandStore, } from "./commands.svelte";
13
14
  export { createShortcutStore, getShortcutStore, LAYOUT_SHORTCUTS, type ShortcutEntry, shortcutListener, type ShortcutMeta, ShortcutStore, } from "./shortcuts.svelte";
14
- export { createThemeStore, getThemeStore, type Theme } from "./theme.svelte";
15
- export { type ConfigStore, createConfigStore, } from "./createConfigStore.svelte";
15
+ export { createThemeStore, getThemeStore, type Theme, type ThemeStoreOptions, } from "./theme.svelte";
16
+ export { type ConfigItem, type ConfigStore, type ConfigStoreOptions, createConfigStore, } from "./createConfigStore.svelte";
16
17
  export { createToastBridge, notify } from "./toast-bridge";
18
+ export { type CartEntry, type CartStore, type CartStoreConfig, createCartStore, getCartStore, } from "./cart.svelte";
19
+ export { type AuthConfig, createSessionStore, getSessionStore, type Session, SessionStore, type User, } from "./auth/index";
@@ -5,8 +5,9 @@ export { RUNE_LAB_CONTEXT } from "./context";
5
5
  // ── Composables ───────────────────────────────────────────────────────────────
6
6
  export { useRuneLab } from "./composables/useRuneLab";
7
7
  export { usePersistence } from "./composables/usePersistence";
8
+ export { useMoney } from "./composables/useMoney";
8
9
  // ── Persistence Drivers ───────────────────────────────────────────────────────
9
- export { cookieDriver, createInMemoryDriver, inMemoryDriver, localStorageDriver, sessionStorageDriver, } from "./persistence/drivers";
10
+ export { cookieDriver, createCookieDriver, createInMemoryDriver, inMemoryDriver, localStorageDriver, sessionStorageDriver, } from "./persistence/drivers";
10
11
  // Stores
11
12
  export { AppStore, createAppStore, getAppStore, } from "./app.svelte";
12
13
  export { createLayoutStore, getLayoutStore, LayoutStore, } from "./layout.svelte";
@@ -16,6 +17,9 @@ export { createCurrencyStore, getCurrencyStore, } from "./currency.svelte";
16
17
  export { createToastStore, getToastStore, ToastStore } from "./toast.svelte";
17
18
  export { CommandStore, createCommandStore, getCommandStore, } from "./commands.svelte";
18
19
  export { createShortcutStore, getShortcutStore, LAYOUT_SHORTCUTS, shortcutListener, ShortcutStore, } from "./shortcuts.svelte";
19
- export { createThemeStore, getThemeStore } from "./theme.svelte";
20
+ export { createThemeStore, getThemeStore, } from "./theme.svelte";
20
21
  export { createConfigStore, } from "./createConfigStore.svelte";
21
22
  export { createToastBridge, notify } from "./toast-bridge";
23
+ export { createCartStore, getCartStore, } from "./cart.svelte";
24
+ // ── Auth ──────────────────────────────────────────────────────────────────────
25
+ export { createSessionStore, getSessionStore, SessionStore, } from "./auth/index";
@@ -59,5 +59,6 @@ export declare function createLanguageStore(options?: LanguageStoreOptions): {
59
59
  set(id: string | undefined): void;
60
60
  get(id: string | undefined): Language | undefined;
61
61
  getProp<K extends keyof Language>(prop: K, id?: string | undefined): Language[K] | undefined;
62
+ addItems(newItems: Language[]): void;
62
63
  };
63
64
  export declare function getLanguageStore(): ConfigStore<Language>;
@@ -3,8 +3,10 @@ export declare function createInMemoryDriver(): PersistenceDriver;
3
3
  export declare const inMemoryDriver: PersistenceDriver;
4
4
  export declare const localStorageDriver: PersistenceDriver;
5
5
  export declare const sessionStorageDriver: PersistenceDriver;
6
- export declare const cookieDriver: (options?: {
6
+ export declare const createCookieDriver: (options?: {
7
7
  path?: string;
8
8
  maxAge?: number;
9
9
  sameSite?: "Lax" | "Strict" | "None";
10
10
  }) => PersistenceDriver;
11
+ /** Default cookie driver singleton (path='/') */
12
+ export declare const cookieDriver: PersistenceDriver;
@@ -42,7 +42,7 @@ export const sessionStorageDriver = {
42
42
  window.sessionStorage.removeItem(key);
43
43
  },
44
44
  };
45
- export const cookieDriver = (options = {}) => ({
45
+ export const createCookieDriver = (options = {}) => ({
46
46
  get: (key) => {
47
47
  if (!browser)
48
48
  return null;
@@ -67,3 +67,7 @@ export const cookieDriver = (options = {}) => ({
67
67
  document.cookie = `${key}=; max-age=0; path=${options.path || "/"}`;
68
68
  },
69
69
  });
70
+ /** Default cookie driver singleton (path='/') */
71
+ export const cookieDriver = createCookieDriver({
72
+ path: "/",
73
+ });
@@ -4,11 +4,19 @@ export interface Theme {
4
4
  icon: string;
5
5
  }
6
6
  import type { PersistenceDriver } from "rune-lab/core";
7
- export declare function createThemeStore(driver?: PersistenceDriver | (() => PersistenceDriver | undefined)): {
7
+ export interface ThemeStoreOptions {
8
+ driver?: PersistenceDriver | (() => PersistenceDriver | undefined);
9
+ /** Additional custom themes to append to the built-in DaisyUI set */
10
+ customThemes?: Theme[];
11
+ /** Fallback theme if no persisted value exists (after system preference check) */
12
+ defaultTheme?: string;
13
+ }
14
+ export declare function createThemeStore(driverOrOptions?: PersistenceDriver | (() => PersistenceDriver | undefined) | ThemeStoreOptions): {
8
15
  current: string;
9
16
  available: Theme[];
10
17
  set(id: string): void;
11
18
  get(id: string): Theme | undefined;
12
19
  getProp<K extends keyof Theme>(prop: K, id?: string | undefined): Theme[K] | undefined;
20
+ addItems(newItems: Theme[]): void;
13
21
  };
14
22
  export declare function getThemeStore(): ConfigStore<Theme>;
@@ -46,21 +46,47 @@ const THEMES = themeOrder.map((name) => ({
46
46
  icon: THEME_ICONS[name] ?? "🎨",
47
47
  }));
48
48
  import themeOrder from "daisyui/functions/themeOrder.js";
49
- export function createThemeStore(driver) {
50
- return createConfigStore({
49
+ import { BROWSER } from "esm-env";
50
+ export function createThemeStore(driverOrOptions) {
51
+ // Normalize overloaded argument
52
+ const opts = driverOrOptions && typeof driverOrOptions === "object" &&
53
+ "driver" in driverOrOptions
54
+ ? driverOrOptions
55
+ : {
56
+ driver: driverOrOptions,
57
+ };
58
+ const resolvedDriver = typeof opts.driver === "function"
59
+ ? opts.driver()
60
+ : opts.driver;
61
+ const store = createConfigStore({
51
62
  items: THEMES,
52
63
  storageKey: "theme",
53
64
  displayName: "Theme",
54
65
  idKey: "name",
55
66
  icon: "🎨",
56
- driver: typeof driver === "function" ? driver() : driver,
67
+ driver: resolvedDriver,
57
68
  });
69
+ // Append custom themes if provided
70
+ if (opts.customThemes?.length) {
71
+ store.addItems(opts.customThemes);
72
+ }
73
+ // System preference detection — only if no persisted value was loaded
74
+ if (!resolvedDriver?.get("theme") && BROWSER) {
75
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
76
+ const systemDefault = prefersDark ? "dark" : "light";
77
+ const chosen = opts.defaultTheme ?? systemDefault;
78
+ if (store.get(chosen)) {
79
+ store.set(chosen);
80
+ }
81
+ }
82
+ else if (!resolvedDriver?.get("theme") && opts.defaultTheme) {
83
+ // SSR / non-browser: use defaultTheme if provided
84
+ if (store.get(opts.defaultTheme)) {
85
+ store.set(opts.defaultTheme);
86
+ }
87
+ }
88
+ return store;
58
89
  }
59
90
  export function getThemeStore() {
60
91
  return getContext(RUNE_LAB_CONTEXT.theme);
61
92
  }
62
- // Usage:
63
- // themeStore.set("dark")
64
- // themeStore.get("dark")
65
- // themeStore.getProp("icon") // gets icon of current theme
66
- // themeStore.getProp("icon", "dark") // gets icon of specific theme
@@ -3,8 +3,11 @@
3
3
  import { DEV } from "esm-env";
4
4
 
5
5
  /**
6
- * Simple Icon component for Rune Lab
7
- * Centralizes SVG paths to reduce boilerplate
6
+ * Simple Icon component for Rune Lab.
7
+ * Centralizes SVG paths to reduce boilerplate.
8
+ *
9
+ * ⚠️ SECURITY: `customIcons` values (from `appStore.customIcons` or the `icons` prop)
10
+ * are rendered as **unsanitized HTML** via `{@html}`. Only pass trusted SVG path strings.
8
11
  */
9
12
  let {
10
13
  name,
@@ -10,11 +10,16 @@
10
10
  createLanguageStore,
11
11
  createCurrencyStore,
12
12
  createShortcutStore,
13
+ createCartStore,
14
+ createSessionStore,
15
+ type CartStoreConfig,
13
16
  } from "rune-lab/state";
14
17
  import type { PersistenceDriver } from "rune-lab/core";
15
18
  import { localStorageDriver } from "rune-lab/state";
16
19
  import { RUNE_LAB_CONTEXT } from "rune-lab/state";
17
20
  import type { AppData } from "rune-lab/state";
21
+ import type { Theme } from "rune-lab/state";
22
+ import type { Currency } from "rune-lab/state";
18
23
  import { CommandPalette, ShortcutPalette, Toaster } from "rune-lab/ui";
19
24
 
20
25
  export interface RuneLabConfig {
@@ -28,6 +33,30 @@
28
33
  locales?: readonly string[];
29
34
  onLanguageChange?: (code: string) => void;
30
35
  onThemeChange?: (name: string) => void;
36
+
37
+ // Theming (DaisyUI)
38
+ /** Additional custom themes to register alongside the built-in DaisyUI set */
39
+ customThemes?: Theme[];
40
+ /** Theme to use when no persisted value exists (after system preference detection) */
41
+ defaultTheme?: string;
42
+
43
+ // Currencies (Dinero.js)
44
+ /** Additional custom currencies to register */
45
+ currencies?: Currency[];
46
+ /** Default currency code when no persisted value exists */
47
+ defaultCurrency?: string;
48
+
49
+ // Cart (opt-in)
50
+ /** CartStore configuration — when provided, a CartStore is created and registered in context */
51
+ cart?: CartStoreConfig<any>;
52
+
53
+ // Auth (opt-in)
54
+ /** When true, creates and registers a SessionStore in context */
55
+ auth?: {
56
+ enabled?: boolean;
57
+ /** Callback when session changes (login, logout, expiry) */
58
+ onSessionChange?: (session: any) => void;
59
+ };
31
60
  }
32
61
 
33
62
  let { children, config = {} } = $props<{
@@ -46,13 +75,27 @@
46
75
  () => config.persistence ?? localStorageDriver,
47
76
  );
48
77
  const initialLocales = untrack(() => config.locales);
49
-
50
- const themeStore = createThemeStore(initialPersistence);
78
+ const initialCustomThemes = untrack(() => config.customThemes);
79
+ const initialDefaultTheme = untrack(() => config.defaultTheme);
80
+ const initialCustomCurrencies = untrack(() => config.currencies);
81
+ const initialDefaultCurrency = untrack(() => config.defaultCurrency);
82
+ const initialCartConfig = untrack(() => config.cart);
83
+ const initialAuthConfig = untrack(() => config.auth);
84
+
85
+ const themeStore = createThemeStore({
86
+ driver: initialPersistence,
87
+ customThemes: initialCustomThemes,
88
+ defaultTheme: initialDefaultTheme,
89
+ });
51
90
  const languageStore = createLanguageStore({
52
91
  driver: initialPersistence,
53
92
  locales: initialLocales,
54
93
  });
55
- const currencyStore = createCurrencyStore(initialPersistence);
94
+ const currencyStore = createCurrencyStore({
95
+ driver: initialPersistence,
96
+ customCurrencies: initialCustomCurrencies,
97
+ defaultCurrency: initialDefaultCurrency,
98
+ });
56
99
  const shortcutStore = createShortcutStore();
57
100
 
58
101
  // 2. Initialize Complex Stores (Dependency Injection)
@@ -66,7 +109,7 @@
66
109
  currencyStore,
67
110
  });
68
111
 
69
- // 3. Provide Contexts
112
+ // 3. Provide Core Contexts
70
113
  setContext(RUNE_LAB_CONTEXT.app, appStore);
71
114
  setContext(RUNE_LAB_CONTEXT.api, apiStore);
72
115
  setContext(RUNE_LAB_CONTEXT.toast, toastStore);
@@ -78,6 +121,18 @@
78
121
  setContext(RUNE_LAB_CONTEXT.commands, commandStore);
79
122
  setContext(RUNE_LAB_CONTEXT.persistence, initialPersistence);
80
123
 
124
+ // 4. Opt-in: CartStore (C-01 FIX)
125
+ if (initialCartConfig) {
126
+ const cartStore = createCartStore(initialCartConfig);
127
+ setContext(RUNE_LAB_CONTEXT.cart, cartStore);
128
+ }
129
+
130
+ // 5. Opt-in: SessionStore (C-02 FIX)
131
+ if (initialAuthConfig && initialAuthConfig.enabled !== false) {
132
+ const sessionStore = createSessionStore();
133
+ setContext(RUNE_LAB_CONTEXT.session, sessionStore);
134
+ }
135
+
81
136
  const initialDictionary = untrack(() => config.dictionary);
82
137
  if (initialDictionary) {
83
138
  setContext("rl:dictionary", initialDictionary);
@@ -1,6 +1,9 @@
1
1
  import { type Snippet } from "svelte";
2
+ import { type CartStoreConfig } from "rune-lab/state";
2
3
  import type { PersistenceDriver } from "rune-lab/core";
3
4
  import type { AppData } from "rune-lab/state";
5
+ import type { Theme } from "rune-lab/state";
6
+ import type { Currency } from "rune-lab/state";
4
7
  export interface RuneLabConfig {
5
8
  persistence?: PersistenceDriver;
6
9
  app?: Partial<AppData>;
@@ -12,6 +15,22 @@ export interface RuneLabConfig {
12
15
  locales?: readonly string[];
13
16
  onLanguageChange?: (code: string) => void;
14
17
  onThemeChange?: (name: string) => void;
18
+ /** Additional custom themes to register alongside the built-in DaisyUI set */
19
+ customThemes?: Theme[];
20
+ /** Theme to use when no persisted value exists (after system preference detection) */
21
+ defaultTheme?: string;
22
+ /** Additional custom currencies to register */
23
+ currencies?: Currency[];
24
+ /** Default currency code when no persisted value exists */
25
+ defaultCurrency?: string;
26
+ /** CartStore configuration — when provided, a CartStore is created and registered in context */
27
+ cart?: CartStoreConfig<any>;
28
+ /** When true, creates and registers a SessionStore in context */
29
+ auth?: {
30
+ enabled?: boolean;
31
+ /** Callback when session changes (login, logout, expiry) */
32
+ onSessionChange?: (session: any) => void;
33
+ };
15
34
  }
16
35
  type $$ComponentProps = {
17
36
  children: Snippet;
@@ -0,0 +1,59 @@
1
+ <!--
2
+ MoneyDisplay — Renders a monetary amount with locale-aware formatting.
3
+ Uses CurrencyStore + LanguageStore from context for defaults.
4
+ Zero domain knowledge.
5
+ -->
6
+ <script module lang="ts">
7
+ export interface MoneyDisplayProps {
8
+ /** Amount in minor units (e.g., 15000 = $150.00 for USD) */
9
+ amount: number;
10
+ /** Override currency code (defaults to CurrencyStore.current) */
11
+ currency?: string;
12
+ /** Override locale (defaults to LanguageStore.current) */
13
+ locale?: string;
14
+ /** Use compact notation (e.g., $150K) */
15
+ compact?: boolean;
16
+ }
17
+ </script>
18
+
19
+ <script lang="ts">
20
+ import { getLanguageStore, getCurrencyStore } from "rune-lab/state";
21
+ import { formatAmount } from "rune-lab/core";
22
+
23
+ let {
24
+ amount,
25
+ currency,
26
+ locale,
27
+ compact = false,
28
+ }: MoneyDisplayProps = $props();
29
+
30
+ const currencyStore = getCurrencyStore();
31
+ const languageStore = getLanguageStore();
32
+
33
+ const resolvedCurrency = $derived(
34
+ currency ?? String(currencyStore.current),
35
+ );
36
+
37
+ const resolvedLocale = $derived(
38
+ locale ?? (String(languageStore.current) || "en"),
39
+ );
40
+
41
+ // M-02 FIX: Derive decimals from the RESOLVED currency, not the store's current
42
+ const currencyMeta = $derived(currencyStore.get(resolvedCurrency));
43
+ const decimals = $derived(currencyMeta?.decimals ?? 2);
44
+
45
+ const formatted = $derived.by(() => {
46
+ if (compact) {
47
+ const majorUnits = amount / Math.pow(10, decimals);
48
+ return new Intl.NumberFormat(resolvedLocale, {
49
+ style: "currency",
50
+ currency: resolvedCurrency,
51
+ notation: "compact",
52
+ maximumFractionDigits: 1,
53
+ }).format(majorUnits);
54
+ }
55
+ return formatAmount(amount, resolvedCurrency, resolvedLocale);
56
+ });
57
+ </script>
58
+
59
+ <data value={amount} class="rl-money-display tabular-nums">{formatted}</data>
@@ -0,0 +1,13 @@
1
+ export interface MoneyDisplayProps {
2
+ /** Amount in minor units (e.g., 15000 = $150.00 for USD) */
3
+ amount: number;
4
+ /** Override currency code (defaults to CurrencyStore.current) */
5
+ currency?: string;
6
+ /** Override locale (defaults to LanguageStore.current) */
7
+ locale?: string;
8
+ /** Use compact notation (e.g., $150K) */
9
+ compact?: boolean;
10
+ }
11
+ declare const MoneyDisplay: import("svelte").Component<MoneyDisplayProps, {}, "">;
12
+ type MoneyDisplay = ReturnType<typeof MoneyDisplay>;
13
+ export default MoneyDisplay;
@@ -0,0 +1,106 @@
1
+ <!--
2
+ MoneyInput — Monetary value input that works in minor units (cents).
3
+ No floats cross the boundary — values are always integer minor units.
4
+ -->
5
+ <script module lang="ts">
6
+ export interface MoneyInputProps {
7
+ /** Current value in minor units (e.g., 15000 = $150.00) */
8
+ value?: number;
9
+ /** Override currency code (defaults to CurrencyStore.current) */
10
+ currency?: string;
11
+ /** Minimum value in minor units */
12
+ min?: number;
13
+ /** Maximum value in minor units */
14
+ max?: number;
15
+ /** Placeholder text */
16
+ placeholder?: string;
17
+ /** Fired when the value changes (in minor units) */
18
+ oninput?: (cents: number) => void;
19
+ /** Input disabled state */
20
+ disabled?: boolean;
21
+ }
22
+ </script>
23
+
24
+ <script lang="ts">
25
+ import { getCurrencyStore } from "rune-lab/state";
26
+
27
+ let {
28
+ value = 0,
29
+ currency,
30
+ min,
31
+ max,
32
+ placeholder = "0.00",
33
+ oninput,
34
+ disabled = false,
35
+ }: MoneyInputProps = $props();
36
+
37
+ const currencyStore = getCurrencyStore();
38
+
39
+ const resolvedCurrency = $derived(
40
+ currency ?? String(currencyStore.current),
41
+ );
42
+
43
+ // M-02 parallel: derive symbol/decimals from resolved currency, not store current
44
+ const currencyMeta = $derived(currencyStore.get(resolvedCurrency));
45
+ const symbol = $derived(currencyMeta?.symbol ?? "$");
46
+ const decimals = $derived(currencyMeta?.decimals ?? 2);
47
+
48
+ // Convert minor units → display string
49
+ const displayValue = $derived.by(() => {
50
+ if (decimals === 0) return String(value);
51
+ const divisor = Math.pow(10, decimals);
52
+ return (value / divisor).toFixed(decimals);
53
+ });
54
+
55
+ /**
56
+ * M-03 FIX: Parse user input to minor units using string-based integer parsing
57
+ * to avoid floating-point precision traps (e.g., 1.005 * 100 = 100.49999...)
58
+ */
59
+ function handleInput(e: Event) {
60
+ const target = e.target as HTMLInputElement;
61
+ const raw = target.value.replace(/[^0-9.,-]/g, "");
62
+
63
+ if (!raw) return;
64
+
65
+ // String-based parsing: split on decimal point
66
+ const parts = raw.split(".");
67
+ const integerPart = parts[0] || "0";
68
+
69
+ // Pad or truncate the fractional part to exactly `decimals` digits
70
+ let fractionalPart = (parts[1] || "").slice(0, decimals);
71
+ fractionalPart = fractionalPart.padEnd(decimals, "0");
72
+
73
+ // Combine as integer minor units — no floats involved
74
+ const combined =
75
+ decimals === 0 ? integerPart : integerPart + fractionalPart;
76
+
77
+ let cents = parseInt(combined, 10);
78
+ if (isNaN(cents)) return;
79
+
80
+ if (min !== undefined) cents = Math.max(cents, min);
81
+ if (max !== undefined) cents = Math.min(cents, max);
82
+
83
+ value = cents;
84
+ oninput?.(cents);
85
+ }
86
+ </script>
87
+
88
+ <label
89
+ class="input input-bordered flex items-center gap-1"
90
+ class:input-disabled={disabled}
91
+ >
92
+ <span class="text-base-content/50 font-medium select-none">{symbol}</span>
93
+ <input
94
+ type="text"
95
+ inputmode="decimal"
96
+ class="grow bg-transparent outline-none tabular-nums"
97
+ value={displayValue}
98
+ {placeholder}
99
+ {disabled}
100
+ oninput={handleInput}
101
+ aria-label={`Amount in ${resolvedCurrency}`}
102
+ />
103
+ <span class="text-base-content/30 text-xs font-mono select-none"
104
+ >{resolvedCurrency}</span
105
+ >
106
+ </label>
@@ -0,0 +1,19 @@
1
+ export interface MoneyInputProps {
2
+ /** Current value in minor units (e.g., 15000 = $150.00) */
3
+ value?: number;
4
+ /** Override currency code (defaults to CurrencyStore.current) */
5
+ currency?: string;
6
+ /** Minimum value in minor units */
7
+ min?: number;
8
+ /** Maximum value in minor units */
9
+ max?: number;
10
+ /** Placeholder text */
11
+ placeholder?: string;
12
+ /** Fired when the value changes (in minor units) */
13
+ oninput?: (cents: number) => void;
14
+ /** Input disabled state */
15
+ disabled?: boolean;
16
+ }
17
+ declare const MoneyInput: import("svelte").Component<MoneyInputProps, {}, "">;
18
+ type MoneyInput = ReturnType<typeof MoneyInput>;
19
+ export default MoneyInput;
@@ -0,0 +1,2 @@
1
+ export { default as MoneyDisplay } from "./MoneyDisplay.svelte";
2
+ export { default as MoneyInput } from "./MoneyInput.svelte";
@@ -0,0 +1,3 @@
1
+ // sdk/ui/src/lib/components/money/index.ts
2
+ export { default as MoneyDisplay } from "./MoneyDisplay.svelte";
3
+ export { default as MoneyInput } from "./MoneyInput.svelte";