rune-lab 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,6 +140,45 @@ also scan the `rune-lab` dist output:
140
140
  > component classes used by `rune-lab` will be included in your build and theme
141
141
  > switching will work across library components and your own code alike.
142
142
 
143
+ ## Money & Currency
144
+
145
+ Rune Lab provides a robust, Dinero.js-backed money layer that handles precision
146
+ arithmetic and locale-aware formatting.
147
+
148
+ ### MoneyDisplay
149
+
150
+ ```svelte
151
+ <script>
152
+ import { MoneyDisplay } from "rune-lab";
153
+ </script>
154
+
155
+ <!-- Minor units (default): $150.00 -->
156
+ <MoneyDisplay amount={15000} currency="USD" />
157
+
158
+ <!-- Major units: $150.00 -->
159
+ <MoneyDisplay amount={150} unit="major" currency="USD" />
160
+
161
+ <!-- Compact notation: $1.2M -->
162
+ <MoneyDisplay amount={1200000} unit="major" compact />
163
+
164
+ <!-- Null handling with fallback: "—" -->
165
+ <MoneyDisplay amount={null} fallback="N/A" />
166
+ ```
167
+
168
+ ### MoneyInput
169
+
170
+ A masked input that prevents floating-point precision errors by working
171
+ exclusively with integers.
172
+
173
+ ```svelte
174
+ <script>
175
+ import { MoneyInput } from "rune-lab";
176
+ let price = $state(150.00);
177
+ </script>
178
+
179
+ <MoneyInput bind:amount={price} unit="major" currency="USD" />
180
+ ```
181
+
143
182
  ## Persistence Drivers
144
183
 
145
184
  Rune Lab provides built-in drivers to remember user preferences (like theme,
@@ -1,2 +1,2 @@
1
- export { addMoney, createMoney, CURRENCY_MAP, type Dinero, type DineroCurrency, formatAmount, formatMoney, multiplyMoney, subtractMoney, } from "./money";
1
+ export { addMoney, createMoney, CURRENCY_MAP, type Dinero, type DineroCurrency, formatAmount, formatMoney, type ISO4217Code, multiplyMoney, registerCurrency, safeAmount, subtractMoney, toMinorUnit, } from "./money";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sdk/core/src/money/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,YAAY,EACZ,WAAW,EACX,aAAa,EACb,aAAa,GACd,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sdk/core/src/money/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,YAAY,EACZ,WAAW,EACX,KAAK,WAAW,EAChB,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC"}
@@ -1,2 +1,2 @@
1
1
  // sdk/core/src/money/index.ts
2
- export { addMoney, createMoney, CURRENCY_MAP, formatAmount, formatMoney, multiplyMoney, subtractMoney, } from "./money";
2
+ export { addMoney, createMoney, CURRENCY_MAP, formatAmount, formatMoney, multiplyMoney, registerCurrency, safeAmount, subtractMoney, toMinorUnit, } from "./money";
@@ -4,11 +4,31 @@ import { type Dinero, type DineroCurrency } from "dinero.js";
4
4
  */
5
5
  export declare const CURRENCY_MAP: Record<string, DineroCurrency<number>>;
6
6
  /**
7
- * Create a Dinero monetary value from an amount in minor units (cents)
8
- * @param amount - Amount in minor units (e.g., 1234 = $12.34 for USD)
7
+ * Derived type of all built-in ISO 4217 currency codes.
8
+ * Enables IDE autocomplete while allowing any string for dynamic currencies.
9
+ */
10
+ export type ISO4217Code = keyof typeof CURRENCY_MAP;
11
+ /**
12
+ * Register a new currency in CURRENCY_MAP atomically.
13
+ * Ensures the core registry and any store-level registries stay in sync.
14
+ */
15
+ export declare function registerCurrency(code: string, currency: DineroCurrency<number>): void;
16
+ /**
17
+ * Normalizes an unknown input into a valid integer for Dinero.js.
18
+ * Exported so consumers can apply the same defensive coercion.
19
+ */
20
+ export declare function safeAmount(amount: unknown): number;
21
+ /**
22
+ * Converts a major-unit amount (e.g., pesos) to a minor-unit integer (e.g., centavos).
23
+ * Uses the currency's exponent from CURRENCY_MAP.
24
+ */
25
+ export declare function toMinorUnit(amount: unknown, currencyCode: ISO4217Code | string): number;
26
+ /**
27
+ * Create a Dinero monetary value from an amount.
28
+ * @param amount - Amount (normalized internally via safeAmount)
9
29
  * @param currencyCode - ISO 4217 currency code (e.g., "USD")
10
30
  */
11
- export declare function createMoney(amount: number, currencyCode: string): Dinero<number>;
31
+ export declare function createMoney(amount: unknown, currencyCode: ISO4217Code | string): Dinero<number>;
12
32
  /**
13
33
  * Format a Dinero value as a locale-aware string
14
34
  * @param money - Dinero monetary value
@@ -17,10 +37,10 @@ export declare function createMoney(amount: number, currencyCode: string): Diner
17
37
  */
18
38
  export declare function formatMoney(money: Dinero<number>, locale?: string, currencyCode?: string): string;
19
39
  /**
20
- * Format a raw amount (minor units) as a locale-aware currency string
21
- * Convenience wrapper around createMoney + formatMoney
40
+ * Format a raw amount as a locale-aware currency string.
41
+ * Convenience wrapper around createMoney + formatMoney.
22
42
  */
23
- export declare function formatAmount(amount: number, currencyCode: string, locale?: string): string;
43
+ export declare function formatAmount(amount: unknown, currencyCode: ISO4217Code | string, locale?: string): string;
24
44
  /**
25
45
  * Add two monetary values (must be same currency)
26
46
  */
@@ -1 +1 @@
1
- {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../../src/sdk/core/src/money/money.ts"],"names":[],"mappings":"AAIA,OAAO,EAML,KAAK,MAAM,EAEX,KAAK,cAAc,EAYpB,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,CAY/D,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,MAAM,CAAC,MAAM,CAAC,CAQhB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EACrB,MAAM,GAAE,MAAgB,EACxB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAeR;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,MAAgB,GACvB,MAAM,CAGR;AAED;;GAEG;AACH,wBAAgB,QAAQ,CACtB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EACjB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAChB,MAAM,CAAC,MAAM,CAAC,CAEhB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EACjB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAChB,MAAM,CAAC,MAAM,CAAC,CAEhB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EACrB,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,CAAC,CAEhB;AAGD,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../../src/sdk/core/src/money/money.ts"],"names":[],"mappings":"AAIA,OAAO,EAML,KAAK,MAAM,EAEX,KAAK,cAAc,EAYpB,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,CAY/D,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,YAAY,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,GAC/B,IAAI,CAEN;AA2BD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAElD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,WAAW,GAAG,MAAM,GACjC,MAAM,CAUR;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,WAAW,GAAG,MAAM,GACjC,MAAM,CAAC,MAAM,CAAC,CAQhB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EACrB,MAAM,GAAE,MAAgB,EACxB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAeR;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,WAAW,GAAG,MAAM,EAClC,MAAM,GAAE,MAAgB,GACvB,MAAM,CAGR;AAED;;GAEG;AACH,wBAAgB,QAAQ,CACtB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EACjB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAChB,MAAM,CAAC,MAAM,CAAC,CAEhB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EACjB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAChB,MAAM,CAAC,MAAM,CAAC,CAEhB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EACrB,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,CAAC,CAEhB;AAGD,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC"}
@@ -21,16 +21,69 @@ export const CURRENCY_MAP = {
21
21
  INR,
22
22
  };
23
23
  /**
24
- * Create a Dinero monetary value from an amount in minor units (cents)
25
- * @param amount - Amount in minor units (e.g., 1234 = $12.34 for USD)
24
+ * Register a new currency in CURRENCY_MAP atomically.
25
+ * Ensures the core registry and any store-level registries stay in sync.
26
+ */
27
+ export function registerCurrency(code, currency) {
28
+ CURRENCY_MAP[code] = currency;
29
+ }
30
+ /**
31
+ * Normalizes an unknown input into a valid finite number.
32
+ * Handles null, undefined, bigints, and SurrealDB Decimal objects via toString().
33
+ */
34
+ function toNumber(amount) {
35
+ if (amount === null || amount === undefined)
36
+ return 0;
37
+ if (typeof amount === "number")
38
+ return Number.isFinite(amount) ? amount : 0;
39
+ if (typeof amount === "bigint")
40
+ return Number(amount);
41
+ // Handle SurrealDB Decimal (which has a toString method) or other objects
42
+ if (typeof amount === "object") {
43
+ try {
44
+ // SurrealDB Decimal.toString() returns a numeric string
45
+ const str = String(amount);
46
+ const parsed = parseFloat(str);
47
+ return Number.isFinite(parsed) ? parsed : 0;
48
+ }
49
+ catch {
50
+ return 0;
51
+ }
52
+ }
53
+ const coerced = Number(amount);
54
+ return Number.isFinite(coerced) ? coerced : 0;
55
+ }
56
+ /**
57
+ * Normalizes an unknown input into a valid integer for Dinero.js.
58
+ * Exported so consumers can apply the same defensive coercion.
59
+ */
60
+ export function safeAmount(amount) {
61
+ return Math.round(toNumber(amount));
62
+ }
63
+ /**
64
+ * Converts a major-unit amount (e.g., pesos) to a minor-unit integer (e.g., centavos).
65
+ * Uses the currency's exponent from CURRENCY_MAP.
66
+ */
67
+ export function toMinorUnit(amount, currencyCode) {
68
+ const currency = CURRENCY_MAP[currencyCode];
69
+ if (!currency) {
70
+ throw new Error(`Unknown currency code: ${currencyCode}. Register it first via registerCurrency().`);
71
+ }
72
+ const base = Array.isArray(currency.base) ? currency.base[0] : currency.base;
73
+ const factor = Math.pow(Number(base), Number(currency.exponent));
74
+ return Math.round(toNumber(amount) * factor);
75
+ }
76
+ /**
77
+ * Create a Dinero monetary value from an amount.
78
+ * @param amount - Amount (normalized internally via safeAmount)
26
79
  * @param currencyCode - ISO 4217 currency code (e.g., "USD")
27
80
  */
28
81
  export function createMoney(amount, currencyCode) {
29
82
  const currency = CURRENCY_MAP[currencyCode];
30
83
  if (!currency) {
31
- throw new Error(`Unknown currency code: ${currencyCode}. Add it to CURRENCY_MAP.`);
84
+ throw new Error(`Unknown currency code: ${currencyCode}. Register it first via registerCurrency().`);
32
85
  }
33
- return dinero({ amount, currency });
86
+ return dinero({ amount: safeAmount(amount), currency });
34
87
  }
35
88
  /**
36
89
  * Format a Dinero value as a locale-aware string
@@ -52,8 +105,8 @@ export function formatMoney(money, locale = "en-US", currencyCode) {
52
105
  });
53
106
  }
54
107
  /**
55
- * Format a raw amount (minor units) as a locale-aware currency string
56
- * Convenience wrapper around createMoney + formatMoney
108
+ * Format a raw amount as a locale-aware currency string.
109
+ * Convenience wrapper around createMoney + formatMoney.
57
110
  */
58
111
  export function formatAmount(amount, currencyCode, locale = "en-US") {
59
112
  const money = createMoney(amount, currencyCode);
package/dist/index.js CHANGED
@@ -2,4 +2,4 @@ export * from "rune-lab/core";
2
2
  export * from "rune-lab/state";
3
3
  export * from "rune-lab/ui";
4
4
 
5
- export const version = () => "0.2.3";
5
+ export const version = () => "0.3.0";
@@ -25,9 +25,16 @@ export declare class AppStore {
25
25
  homepage: string;
26
26
  customIcons: Record<string, string>;
27
27
  /**
28
- * Initialize app store with metadata
28
+ * Initialize app store with metadata.
29
+ *
30
+ * @contract init() is idempotent. Call it once at app startup via RuneProvider.
31
+ * Subsequent calls are silently ignored to maintain stability across SSR/CSR cycles.
29
32
  */
30
33
  init(data: Partial<AppData>): void;
34
+ /**
35
+ * @internal Test-only. Resets initialization guard.
36
+ */
37
+ __reset(): void;
31
38
  /**
32
39
  * Get full app information object
33
40
  */
@@ -18,7 +18,10 @@ export class AppStore {
18
18
  customIcons = $state({});
19
19
  #initialized = false;
20
20
  /**
21
- * Initialize app store with metadata
21
+ * Initialize app store with metadata.
22
+ *
23
+ * @contract init() is idempotent. Call it once at app startup via RuneProvider.
24
+ * Subsequent calls are silently ignored to maintain stability across SSR/CSR cycles.
22
25
  */
23
26
  init(data) {
24
27
  if (this.#initialized) {
@@ -43,6 +46,12 @@ export class AppStore {
43
46
  this.homepage = data.homepage;
44
47
  this.#initialized = true;
45
48
  }
49
+ /**
50
+ * @internal Test-only. Resets initialization guard.
51
+ */
52
+ __reset() {
53
+ this.#initialized = false;
54
+ }
46
55
  /**
47
56
  * Get full app information object
48
57
  */
@@ -1,15 +1,15 @@
1
- import { type Dinero } from "rune-lab/core";
1
+ import { type Dinero, type ISO4217Code } from "rune-lab/core";
2
2
  /**
3
3
  * Context-aware money composable.
4
4
  * Reads CurrencyStore and LanguageStore from rune-lab context.
5
5
  *
6
6
  * Usage:
7
7
  * const { format, toDinero, add, subtract } = useMoney();
8
- * const display = format(15000); // "$150.00" (based on current currency + locale)
8
+ * const display = format(150.00, undefined, 'major'); // "$150.00"
9
9
  */
10
10
  export declare function useMoney(): {
11
- toDinero: (amount: number, currencyCode?: string) => Dinero<number>;
12
- format: (amount: number, currencyCode?: string) => string;
13
- add: (a: number, b: number, currencyCode?: string) => Dinero<number>;
14
- subtract: (a: number, b: number, currencyCode?: string) => Dinero<number>;
11
+ toDinero: (amount: number | null | undefined, currencyCode?: ISO4217Code | string, unit?: "major" | "minor") => Dinero<number>;
12
+ format: (amount: number | null | undefined, currencyCode?: ISO4217Code | string, unit?: "major" | "minor") => string;
13
+ add: (a: number, b: number, currencyCode?: ISO4217Code | string, unit?: "major" | "minor") => Dinero<number>;
14
+ subtract: (a: number, b: number, currencyCode?: ISO4217Code | string, unit?: "major" | "minor") => Dinero<number>;
15
15
  };
@@ -2,47 +2,61 @@
2
2
  // Context-aware money composable that reads CurrencyStore + LanguageStore
3
3
  import { getContext } from "svelte";
4
4
  import { RUNE_LAB_CONTEXT } from "../context";
5
- import { addMoney, createMoney, formatMoney, subtractMoney, } from "rune-lab/core";
5
+ import { addMoney, createMoney, formatMoney, subtractMoney, toMinorUnit, } from "rune-lab/core";
6
6
  /**
7
7
  * Context-aware money composable.
8
8
  * Reads CurrencyStore and LanguageStore from rune-lab context.
9
9
  *
10
10
  * Usage:
11
11
  * const { format, toDinero, add, subtract } = useMoney();
12
- * const display = format(15000); // "$150.00" (based on current currency + locale)
12
+ * const display = format(150.00, undefined, 'major'); // "$150.00"
13
13
  */
14
14
  export function useMoney() {
15
15
  const currencyStore = getContext(RUNE_LAB_CONTEXT.currency);
16
16
  const languageStore = getContext(RUNE_LAB_CONTEXT.language);
17
17
  /**
18
- * Convert minor-unit amount to a Dinero object using current currency
18
+ * Convert amount to a Dinero object using current currency
19
19
  */
20
- function toDinero(amount, currencyCode) {
20
+ function toDinero(amount, currencyCode, unit = "minor") {
21
21
  const code = currencyCode ?? String(currencyStore.current);
22
- return createMoney(amount, code);
22
+ const minorAmount = unit === "major" && amount !== null && amount !== undefined
23
+ ? toMinorUnit(Number(amount), code)
24
+ : amount;
25
+ return createMoney(minorAmount, code);
23
26
  }
24
27
  /**
25
- * Format an amount (minor units) as a locale-aware currency string
28
+ * Format an amount as a locale-aware currency string
26
29
  */
27
- function format(amount, currencyCode) {
30
+ function format(amount, currencyCode, unit = "minor") {
31
+ if (amount === null || amount === undefined ||
32
+ (typeof amount === "number" && isNaN(amount))) {
33
+ return "—";
34
+ }
28
35
  const code = currencyCode ?? String(currencyStore.current);
29
36
  const locale = String(languageStore.current) || "en";
30
- const money = createMoney(amount, code);
37
+ const minorAmount = unit === "major"
38
+ ? toMinorUnit(Number(amount), code)
39
+ : amount;
40
+ const money = createMoney(minorAmount, code);
31
41
  return formatMoney(money, locale, code);
32
42
  }
33
43
  /**
34
- * Add two amounts (minor units) in the current currency
44
+ * Add two amounts in the current currency
35
45
  */
36
- function add(a, b, currencyCode) {
46
+ function add(a, b, currencyCode, unit = "minor") {
37
47
  const code = currencyCode ?? String(currencyStore.current);
38
- return addMoney(createMoney(a, code), createMoney(b, code));
48
+ const aMoney = toDinero(a, code, unit);
49
+ const bMoney = toDinero(b, code, unit);
50
+ return addMoney(aMoney, bMoney);
39
51
  }
40
52
  /**
41
- * Subtract two amounts (minor units) in the current currency
53
+ * Subtract two amounts in the current currency
42
54
  */
43
- function subtract(a, b, currencyCode) {
55
+ function subtract(a, b, currencyCode, unit = "minor") {
44
56
  const code = currencyCode ?? String(currencyStore.current);
45
- return subtractMoney(createMoney(a, code), createMoney(b, code));
57
+ const aMoney = toDinero(a, code, unit);
58
+ const bMoney = toDinero(b, code, unit);
59
+ return subtractMoney(aMoney, bMoney);
46
60
  }
47
61
  return { toDinero, format, add, subtract };
48
62
  }
@@ -1,4 +1,3 @@
1
- import { type ConfigStore } from "./createConfigStore.svelte";
2
1
  /**
3
2
  * Currency configuration
4
3
  * Based on ISO 4217 currency codes
@@ -17,11 +16,13 @@ export interface CurrencyStoreOptions {
17
16
  defaultCurrency?: string;
18
17
  }
19
18
  export declare function createCurrencyStore(driverOrOptions?: PersistenceDriver | (() => PersistenceDriver | undefined) | CurrencyStoreOptions): {
19
+ addCurrency: (meta: Currency, dineroDef?: any) => void;
20
+ current: string | number;
21
+ available: Currency[];
22
+ };
23
+ export type CurrencyStore = ReturnType<typeof createCurrencyStore>;
24
+ export declare function getCurrencyStore(): {
25
+ addCurrency: (meta: Currency, dineroDef?: any) => void;
20
26
  current: string | number;
21
27
  available: Currency[];
22
- set(id: string | number): void;
23
- get(id: string | number): Currency | undefined;
24
- getProp<K extends keyof Currency>(prop: K, id?: string | number | undefined): Currency[K] | undefined;
25
- addItems(newItems: Currency[]): void;
26
28
  };
27
- export declare function getCurrencyStore(): ConfigStore<Currency>;
@@ -1,6 +1,18 @@
1
1
  import { createConfigStore, } from "./createConfigStore.svelte";
2
2
  import { getContext } from "svelte";
3
3
  import { RUNE_LAB_CONTEXT } from "./context";
4
+ import { registerCurrency } from "rune-lab/core";
5
+ /**
6
+ * Helper to build a minimal Dinero definition from Currency metadata.
7
+ * Assumes base 10 (standard decimal) for auto-registration.
8
+ */
9
+ function buildDineroDef(meta) {
10
+ return {
11
+ code: meta.code,
12
+ base: 10,
13
+ exponent: meta.decimals,
14
+ };
15
+ }
4
16
  const CURRENCIES = [
5
17
  { code: "USD", symbol: "$", decimals: 2 },
6
18
  { code: "EUR", symbol: "€", decimals: 2 },
@@ -33,8 +45,23 @@ export function createCurrencyStore(driverOrOptions) {
33
45
  icon: "💰",
34
46
  driver: resolvedDriver,
35
47
  });
36
- // Append custom currencies if provided
48
+ /**
49
+ * Extension: Atomic currency registration.
50
+ * Updates both the Dinero registry (core) and the reactive store (UI).
51
+ *
52
+ * @remarks Custom currencies with non-decimal base systems must use
53
+ * registerCurrency() from @internal/core explicitly before addItems().
54
+ */
55
+ function addCurrency(meta, dineroDef) {
56
+ const def = dineroDef || buildDineroDef(meta);
57
+ registerCurrency(meta.code, def);
58
+ store.addItems([meta]);
59
+ }
60
+ // Append and auto-register custom currencies if provided
37
61
  if (opts.customCurrencies?.length) {
62
+ for (const c of opts.customCurrencies) {
63
+ registerCurrency(c.code, buildDineroDef(c));
64
+ }
38
65
  store.addItems(opts.customCurrencies);
39
66
  }
40
67
  // Set default currency if provided and no persisted value found
@@ -43,7 +70,10 @@ export function createCurrencyStore(driverOrOptions) {
43
70
  store.set(opts.defaultCurrency);
44
71
  }
45
72
  }
46
- return store;
73
+ return {
74
+ ...store,
75
+ addCurrency,
76
+ };
47
77
  }
48
78
  export function getCurrencyStore() {
49
79
  return getContext(RUNE_LAB_CONTEXT.currency);
@@ -8,7 +8,7 @@ export { type AppData, AppStore, createAppStore, getAppStore, } from "./app.svel
8
8
  export { createLayoutStore, getLayoutStore, LayoutStore, type NavigationItem, type NavigationSection, type WorkspaceItem, } from "./layout.svelte";
9
9
  export { ApiStore, type ConnectionState, createApiStore, getApiStore, } from "./api.svelte";
10
10
  export { createLanguageStore, getLanguageStore, type Language, } from "./language.svelte";
11
- export { createCurrencyStore, type Currency, type CurrencyStoreOptions, getCurrencyStore, } from "./currency.svelte";
11
+ export { createCurrencyStore, type Currency, type CurrencyStore, type CurrencyStoreOptions, getCurrencyStore, } from "./currency.svelte";
12
12
  export { createToastStore, getToastStore, ToastStore } from "./toast.svelte";
13
13
  export { type Command, CommandStore, createCommandStore, getCommandStore, } from "./commands.svelte";
14
14
  export { createShortcutStore, getShortcutStore, LAYOUT_SHORTCUTS, type ShortcutEntry, shortcutListener, type ShortcutMeta, ShortcutStore, } from "./shortcuts.svelte";
@@ -14,11 +14,21 @@
14
14
  size = "w-5 h-5",
15
15
  class: className = "",
16
16
  icons = {},
17
+ provider = "svg",
18
+ fill = 0,
19
+ weight = 400,
20
+ grade = 0,
21
+ opsz = 24,
17
22
  } = $props<{
18
23
  name: string;
19
24
  size?: string;
20
25
  class?: string;
21
26
  icons?: Record<string, string>;
27
+ provider?: "svg" | "material";
28
+ fill?: 0 | 1;
29
+ weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700;
30
+ grade?: -25 | 0 | 200;
31
+ opsz?: 20 | 24 | 40 | 48;
22
32
  }>();
23
33
 
24
34
  let appStore: ReturnType<typeof getAppStore> | undefined;
@@ -74,16 +84,25 @@
74
84
  });
75
85
  </script>
76
86
 
77
- <svg
78
- xmlns="http://www.w3.org/2000/svg"
79
- viewBox="0 0 24 24"
80
- fill="none"
81
- stroke="currentColor"
82
- stroke-width="2"
83
- stroke-linecap="round"
84
- stroke-linejoin="round"
85
- class="{size} {className}"
86
- >
87
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
88
- {@html path}
89
- </svg>
87
+ {#if provider === "material"}
88
+ <span
89
+ class="material-symbols-outlined {size} {className} select-none"
90
+ style="font-variation-settings: 'FILL' {fill}, 'wght' {weight}, 'GRAD' {grade}, 'opsz' {opsz}; display: inline-flex; align-items: center; justify-content: center;"
91
+ >
92
+ {name}
93
+ </span>
94
+ {:else}
95
+ <svg
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ viewBox="0 0 24 24"
98
+ fill="none"
99
+ stroke="currentColor"
100
+ stroke-width="2"
101
+ stroke-linecap="round"
102
+ stroke-linejoin="round"
103
+ class="{size} {className}"
104
+ >
105
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
106
+ {@html path}
107
+ </svg>
108
+ {/if}
@@ -3,6 +3,11 @@ type $$ComponentProps = {
3
3
  size?: string;
4
4
  class?: string;
5
5
  icons?: Record<string, string>;
6
+ provider?: "svg" | "material";
7
+ fill?: 0 | 1;
8
+ weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700;
9
+ grade?: -25 | 0 | 200;
10
+ opsz?: 20 | 24 | 40 | 48;
6
11
  };
7
12
  declare const Icon: import("svelte").Component<$$ComponentProps, {}, "">;
8
13
  type Icon = ReturnType<typeof Icon>;
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import { DEV } from "esm-env";
3
+ import { portal } from "../actions/portal";
4
+
5
+ /**
6
+ * Kyntharil — Reactive Value Inspector
7
+ * A dev-mode-only component that displays a floating panel of reactive values.
8
+ */
9
+ let { observe = {} } = $props<{
10
+ observe?: Record<string, unknown>;
11
+ }>();
12
+ </script>
13
+
14
+ {#if DEV}
15
+ <div
16
+ use:portal
17
+ class="rl-kyntharil fixed bottom-4 right-4 z-[9999] max-w-sm max-h-[80vh] overflow-y-auto bg-base-300 border border-base-content/10 rounded-lg shadow-2xl p-4 font-mono text-xs select-none pointer-events-auto"
18
+ >
19
+ <div class="flex items-center justify-between mb-2 border-b border-base-content/10 pb-2">
20
+ <span class="font-bold text-primary flex items-center gap-1">
21
+ <span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
22
+ Kyntharil DevTools
23
+ </span>
24
+ <span class="opacity-50 text-[10px] uppercase tracking-wider">Reactive Observer</span>
25
+ </div>
26
+
27
+ <div class="space-y-2">
28
+ {#each Object.entries(observe) as [label, value]}
29
+ <div class="flex flex-col gap-0.5">
30
+ <span class="text-base-content/50 text-[10px]">{label}</span>
31
+ <pre class="bg-base-100 p-1.5 rounded overflow-x-auto text-success whitespace-pre-wrap break-all">{JSON.stringify(value, null, 2)}</pre>
32
+ </div>
33
+ {:else}
34
+ <div class="text-base-content/30 italic text-center py-4">No values observed</div>
35
+ {/each}
36
+ </div>
37
+ </div>
38
+ {/if}
39
+
40
+ <style>
41
+ .rl-kyntharil::-webkit-scrollbar {
42
+ width: 4px;
43
+ }
44
+ .rl-kyntharil::-webkit-scrollbar-thumb {
45
+ background: rgba(0, 0, 0, 0.1);
46
+ border-radius: 10px;
47
+ }
48
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ observe?: Record<string, unknown>;
3
+ };
4
+ declare const Kyntharil: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type Kyntharil = ReturnType<typeof Kyntharil>;
6
+ export default Kyntharil;
@@ -32,7 +32,8 @@
32
32
  dictionary?: Record<string, any>;
33
33
  locales?: readonly string[];
34
34
  onLanguageChange?: (code: string) => void;
35
- onThemeChange?: (name: string) => void;
35
+ /** Icon provider to use. 'material' injects Google Material Symbols font link. */
36
+ icons?: 'material' | 'none';
36
37
 
37
38
  // Theming (DaisyUI)
38
39
  /** Additional custom themes to register alongside the built-in DaisyUI set */
@@ -182,6 +183,9 @@
182
183
  {#each metaTags as meta}
183
184
  <meta name={meta.name} content={meta.content} />
184
185
  {/each}
186
+ {#if config.icons === "material"}
187
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
188
+ {/if}
185
189
  {/if}
186
190
  </svelte:head>
187
191
 
@@ -14,7 +14,8 @@ export interface RuneLabConfig {
14
14
  dictionary?: Record<string, any>;
15
15
  locales?: readonly string[];
16
16
  onLanguageChange?: (code: string) => void;
17
- onThemeChange?: (name: string) => void;
17
+ /** Icon provider to use. 'material' injects Google Material Symbols font link. */
18
+ icons?: 'material' | 'none';
18
19
  /** Additional custom themes to register alongside the built-in DaisyUI set */
19
20
  customThemes?: Theme[];
20
21
  /** Theme to use when no persisted value exists (after system preference detection) */
@@ -1,31 +1,31 @@
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
1
  <script lang="ts">
20
2
  import { getLanguageStore, getCurrencyStore } from "rune-lab/state";
21
- import { formatAmount } from "rune-lab/core";
3
+ import { formatAmount, toMinorUnit, type ISO4217Code } from "rune-lab/core";
22
4
 
23
5
  let {
24
6
  amount,
7
+ unit = 'minor',
8
+ fallback = "—",
25
9
  currency,
26
10
  locale,
27
11
  compact = false,
28
- }: MoneyDisplayProps = $props();
12
+ } = $props<{
13
+ /** Amount in minor or major units (see unit prop) */
14
+ amount: number | null | undefined;
15
+ /**
16
+ * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
17
+ * Defaults to 'minor' for backward compatibility.
18
+ */
19
+ unit?: 'major' | 'minor';
20
+ /** String to display if amount is null, undefined, or NaN. Defaults to "—" */
21
+ fallback?: string;
22
+ /** Override currency code (defaults to CurrencyStore.current) */
23
+ currency?: ISO4217Code | string;
24
+ /** Override locale (defaults to LanguageStore.current) */
25
+ locale?: string;
26
+ /** Use compact notation (e.g., $1.2M) */
27
+ compact?: boolean;
28
+ }>();
29
29
 
30
30
  const currencyStore = getCurrencyStore();
31
31
  const languageStore = getLanguageStore();
@@ -38,13 +38,22 @@
38
38
  locale ?? (String(languageStore.current) || "en"),
39
39
  );
40
40
 
41
- // M-02 FIX: Derive decimals from the RESOLVED currency, not the store's current
42
41
  const currencyMeta = $derived(currencyStore.get(resolvedCurrency));
43
42
  const decimals = $derived(currencyMeta?.decimals ?? 2);
44
43
 
45
44
  const formatted = $derived.by(() => {
45
+ // If amount is null/undefined/NaN, use fallback
46
+ if (amount === null || amount === undefined || (typeof amount === 'number' && isNaN(amount))) {
47
+ return fallback;
48
+ }
49
+
50
+ // Convert to minor units if necessary
51
+ const minorAmount = unit === 'major'
52
+ ? toMinorUnit(Number(amount), resolvedCurrency)
53
+ : amount;
54
+
46
55
  if (compact) {
47
- const majorUnits = amount / Math.pow(10, decimals);
56
+ const majorUnits = Number(minorAmount) / Math.pow(10, decimals);
48
57
  return new Intl.NumberFormat(resolvedLocale, {
49
58
  style: "currency",
50
59
  currency: resolvedCurrency,
@@ -52,7 +61,8 @@
52
61
  maximumFractionDigits: 1,
53
62
  }).format(majorUnits);
54
63
  }
55
- return formatAmount(amount, resolvedCurrency, resolvedLocale);
64
+
65
+ return formatAmount(minorAmount, resolvedCurrency, resolvedLocale);
56
66
  });
57
67
  </script>
58
68
 
@@ -1,13 +1,21 @@
1
- export interface MoneyDisplayProps {
2
- /** Amount in minor units (e.g., 15000 = $150.00 for USD) */
3
- amount: number;
1
+ import { type ISO4217Code } from "rune-lab/core";
2
+ type $$ComponentProps = {
3
+ /** Amount in minor or major units (see unit prop) */
4
+ amount: number | null | undefined;
5
+ /**
6
+ * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
7
+ * Defaults to 'minor' for backward compatibility.
8
+ */
9
+ unit?: 'major' | 'minor';
10
+ /** String to display if amount is null, undefined, or NaN. Defaults to "—" */
11
+ fallback?: string;
4
12
  /** Override currency code (defaults to CurrencyStore.current) */
5
- currency?: string;
13
+ currency?: ISO4217Code | string;
6
14
  /** Override locale (defaults to LanguageStore.current) */
7
15
  locale?: string;
8
- /** Use compact notation (e.g., $150K) */
16
+ /** Use compact notation (e.g., $1.2M) */
9
17
  compact?: boolean;
10
- }
11
- declare const MoneyDisplay: import("svelte").Component<MoneyDisplayProps, {}, "">;
18
+ };
19
+ declare const MoneyDisplay: import("svelte").Component<$$ComponentProps, {}, "">;
12
20
  type MoneyDisplay = ReturnType<typeof MoneyDisplay>;
13
21
  export default MoneyDisplay;
@@ -0,0 +1,74 @@
1
+ import { render, screen } from "@testing-library/svelte";
2
+ import { describe, expect, it } from "vitest";
3
+ import MoneyDisplay from "./MoneyDisplay.svelte";
4
+ import { RUNE_LAB_CONTEXT } from "rune-lab/state";
5
+ // Mock stores for context
6
+ const mockCurrencyStore = {
7
+ current: "USD",
8
+ get: (code) => ({ symbol: "$", decimals: 2 }),
9
+ };
10
+ const mockLanguageStore = {
11
+ current: "en-US",
12
+ };
13
+ const context = new Map([
14
+ [RUNE_LAB_CONTEXT.currency, mockCurrencyStore],
15
+ [RUNE_LAB_CONTEXT.language, mockLanguageStore],
16
+ ]);
17
+ describe("MoneyDisplay.svelte", () => {
18
+ it("renders fallback when amount is null", () => {
19
+ render(MoneyDisplay, {
20
+ props: { amount: null, fallback: "N/A" },
21
+ context,
22
+ });
23
+ expect(screen.getByText("N/A")).toBeInTheDocument();
24
+ });
25
+ it("renders fallback when amount is undefined", () => {
26
+ render(MoneyDisplay, {
27
+ props: { amount: undefined, fallback: "N/A" },
28
+ context,
29
+ });
30
+ expect(screen.getByText("N/A")).toBeInTheDocument();
31
+ });
32
+ it("renders fallback when amount is NaN", () => {
33
+ render(MoneyDisplay, {
34
+ props: { amount: NaN, fallback: "N/A" },
35
+ context,
36
+ });
37
+ expect(screen.getByText("N/A")).toBeInTheDocument();
38
+ });
39
+ it("renders default fallback when amount is NaN and no fallback prop provided", () => {
40
+ render(MoneyDisplay, {
41
+ props: { amount: NaN },
42
+ context,
43
+ });
44
+ expect(screen.getByText("—")).toBeInTheDocument();
45
+ });
46
+ it("renders zero correctly when passed literal 0", () => {
47
+ render(MoneyDisplay, {
48
+ props: { amount: 0 },
49
+ context,
50
+ });
51
+ expect(screen.getByText(/\$0\.00/)).toBeInTheDocument();
52
+ });
53
+ it('handles unit="major"', () => {
54
+ render(MoneyDisplay, {
55
+ props: { amount: 125.50, unit: "major", currency: "USD" },
56
+ context,
57
+ });
58
+ expect(screen.getByText(/\$125\.50/)).toBeInTheDocument();
59
+ });
60
+ it('handles unit="minor" (default)', () => {
61
+ render(MoneyDisplay, {
62
+ props: { amount: 12550, unit: "minor", currency: "USD" },
63
+ context,
64
+ });
65
+ expect(screen.getByText(/\$125\.50/)).toBeInTheDocument();
66
+ });
67
+ it("supports compact notation", () => {
68
+ render(MoneyDisplay, {
69
+ props: { amount: 1200000, unit: "major", currency: "USD", compact: true },
70
+ context,
71
+ });
72
+ expect(screen.getByText(/\$1\.2M/)).toBeInTheDocument();
73
+ });
74
+ });
@@ -3,19 +3,26 @@
3
3
  No floats cross the boundary — values are always integer minor units.
4
4
  -->
5
5
  <script module lang="ts">
6
+ import type { ISO4217Code } from "rune-lab/core";
7
+
6
8
  export interface MoneyInputProps {
7
- /** Current value in minor units (e.g., 15000 = $150.00) */
8
- value?: number;
9
+ /** Current value in minor or major units (see unit prop) */
10
+ amount?: number | null | undefined;
11
+ /**
12
+ * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
13
+ * Defaults to 'minor' for backward compatibility.
14
+ */
15
+ unit?: 'major' | 'minor';
9
16
  /** Override currency code (defaults to CurrencyStore.current) */
10
- currency?: string;
11
- /** Minimum value in minor units */
17
+ currency?: ISO4217Code | string;
18
+ /** Minimum value in same units as amount */
12
19
  min?: number;
13
- /** Maximum value in minor units */
20
+ /** Maximum value in same units as amount */
14
21
  max?: number;
15
22
  /** Placeholder text */
16
23
  placeholder?: string;
17
- /** Fired when the value changes (in minor units) */
18
- oninput?: (cents: number) => void;
24
+ /** Fired when the value changes (unit matches the unit prop) */
25
+ oninput?: (val: number) => void;
19
26
  /** Input disabled state */
20
27
  disabled?: boolean;
21
28
  }
@@ -23,9 +30,11 @@
23
30
 
24
31
  <script lang="ts">
25
32
  import { getCurrencyStore } from "rune-lab/state";
33
+ import { toMinorUnit } from "rune-lab/core";
26
34
 
27
35
  let {
28
- value = 0,
36
+ amount = $bindable(0),
37
+ unit = 'minor',
29
38
  currency,
30
39
  min,
31
40
  max,
@@ -40,22 +49,22 @@
40
49
  currency ?? String(currencyStore.current),
41
50
  );
42
51
 
43
- // M-02 parallel: derive symbol/decimals from resolved currency, not store current
44
52
  const currencyMeta = $derived(currencyStore.get(resolvedCurrency));
45
53
  const symbol = $derived(currencyMeta?.symbol ?? "$");
46
54
  const decimals = $derived(currencyMeta?.decimals ?? 2);
47
55
 
48
- // Convert minor units display string
56
+ // Convert to minor units internally for consistent arithmetic if needed,
57
+ // but here we just need to display it.
49
58
  const displayValue = $derived.by(() => {
50
- if (decimals === 0) return String(value);
59
+ const val = amount ?? 0;
60
+ if (unit === 'major') {
61
+ return Number(val).toFixed(decimals);
62
+ }
63
+ if (decimals === 0) return String(val);
51
64
  const divisor = Math.pow(10, decimals);
52
- return (value / divisor).toFixed(decimals);
65
+ return (Number(val) / divisor).toFixed(decimals);
53
66
  });
54
67
 
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
68
  function handleInput(e: Event) {
60
69
  const target = e.target as HTMLInputElement;
61
70
  const raw = target.value.replace(/[^0-9.,-]/g, "");
@@ -77,11 +86,27 @@
77
86
  let cents = parseInt(combined, 10);
78
87
  if (isNaN(cents)) return;
79
88
 
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);
89
+ // Apply constraints in minor units
90
+ let finalCents = cents;
91
+
92
+ if (unit === 'major') {
93
+ // If we are working in major units, we need to convert min/max to cents for comparison
94
+ const minCents = min !== undefined ? toMinorUnit(min, resolvedCurrency) : undefined;
95
+ const maxCents = max !== undefined ? toMinorUnit(max, resolvedCurrency) : undefined;
96
+
97
+ if (minCents !== undefined) finalCents = Math.max(finalCents, minCents);
98
+ if (maxCents !== undefined) finalCents = Math.min(finalCents, maxCents);
99
+
100
+ const majorValue = finalCents / Math.pow(10, decimals);
101
+ amount = majorValue;
102
+ oninput?.(majorValue);
103
+ } else {
104
+ if (min !== undefined) finalCents = Math.max(finalCents, min);
105
+ if (max !== undefined) finalCents = Math.min(finalCents, max);
106
+
107
+ amount = finalCents;
108
+ oninput?.(finalCents);
109
+ }
85
110
  }
86
111
  </script>
87
112
 
@@ -1,19 +1,25 @@
1
+ import type { ISO4217Code } from "rune-lab/core";
1
2
  export interface MoneyInputProps {
2
- /** Current value in minor units (e.g., 15000 = $150.00) */
3
- value?: number;
3
+ /** Current value in minor or major units (see unit prop) */
4
+ amount?: number | null | undefined;
5
+ /**
6
+ * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
7
+ * Defaults to 'minor' for backward compatibility.
8
+ */
9
+ unit?: 'major' | 'minor';
4
10
  /** Override currency code (defaults to CurrencyStore.current) */
5
- currency?: string;
6
- /** Minimum value in minor units */
11
+ currency?: ISO4217Code | string;
12
+ /** Minimum value in same units as amount */
7
13
  min?: number;
8
- /** Maximum value in minor units */
14
+ /** Maximum value in same units as amount */
9
15
  max?: number;
10
16
  /** Placeholder text */
11
17
  placeholder?: string;
12
- /** Fired when the value changes (in minor units) */
13
- oninput?: (cents: number) => void;
18
+ /** Fired when the value changes (unit matches the unit prop) */
19
+ oninput?: (val: number) => void;
14
20
  /** Input disabled state */
15
21
  disabled?: boolean;
16
22
  }
17
- declare const MoneyInput: import("svelte").Component<MoneyInputProps, {}, "">;
23
+ declare const MoneyInput: import("svelte").Component<MoneyInputProps, {}, "amount">;
18
24
  type MoneyInput = ReturnType<typeof MoneyInput>;
19
25
  export default MoneyInput;
@@ -3,6 +3,7 @@ export { default as RuneProvider } from "./components/RuneProvider.svelte";
3
3
  export { default as Icon } from "./components/Icon.svelte";
4
4
  export { default as Toaster } from "./components/Toaster.svelte";
5
5
  export { default as ApiMonitor } from "./components/ApiMonitor.svelte";
6
+ export { default as Kyntharil } from "./components/Kyntharil.svelte";
6
7
  export { default as CommandPalette } from "./features/command-palette/CommandPalette.svelte";
7
8
  export { default as ShortcutPalette } from "./features/shortcuts/ShortcutPalette.svelte";
8
9
  export { default as AppSettingSelector } from "./features/config/AppSettingSelector.svelte";
package/dist/ui/index.js CHANGED
@@ -6,6 +6,7 @@ export { default as RuneProvider } from "./components/RuneProvider.svelte";
6
6
  export { default as Icon } from "./components/Icon.svelte";
7
7
  export { default as Toaster } from "./components/Toaster.svelte";
8
8
  export { default as ApiMonitor } from "./components/ApiMonitor.svelte";
9
+ export { default as Kyntharil } from "./components/Kyntharil.svelte";
9
10
  // ── Features ──────────────────────────────────────────────────────────────────
10
11
  export { default as CommandPalette } from "./features/command-palette/CommandPalette.svelte";
11
12
  export { default as ShortcutPalette } from "./features/shortcuts/ShortcutPalette.svelte";
@@ -80,6 +80,17 @@
80
80
  onMount(() => {
81
81
  layoutStore.init({ namespace });
82
82
 
83
+ // Apply global scroll lock while WorkspaceLayout is mounted
84
+ const originalHtmlOverflow = document.documentElement.style.overflow;
85
+ const originalBodyOverflow = document.body.style.overflow;
86
+ const originalHtmlHeight = document.documentElement.style.height;
87
+ const originalBodyHeight = document.body.style.height;
88
+
89
+ document.documentElement.style.overflow = "hidden";
90
+ document.body.style.overflow = "hidden";
91
+ document.documentElement.style.height = "100%";
92
+ document.body.style.height = "100%";
93
+
83
94
  // Register default layout shortcuts
84
95
  const shortcuts = [
85
96
  {
@@ -103,6 +114,12 @@
103
114
  }
104
115
 
105
116
  return () => {
117
+ // Restore original styles
118
+ document.documentElement.style.overflow = originalHtmlOverflow;
119
+ document.body.style.overflow = originalBodyOverflow;
120
+ document.documentElement.style.height = originalHtmlHeight;
121
+ document.body.style.height = originalBodyHeight;
122
+
106
123
  shortcutStore.unregister(LAYOUT_SHORTCUTS.TOGGLE_NAV.id);
107
124
  shortcutStore.unregister(LAYOUT_SHORTCUTS.TOGGLE_DETAIL.id);
108
125
  };
@@ -203,16 +220,6 @@
203
220
  </div>
204
221
 
205
222
  <style>
206
- /* Global lock for entire screen, relying on WorkspaceLayout scroll areas */
207
- :global(html),
208
- :global(body) {
209
- overflow: hidden;
210
- height: 100%;
211
- width: 100%;
212
- margin: 0;
213
- padding: 0;
214
- }
215
-
216
223
  :global(.rl-layout) {
217
224
  --rl-strip-width: 72px;
218
225
  --rl-nav-width: 240px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune-lab",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Modern toolkit for Svelte 5 Runes applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,38 +43,40 @@
43
43
  "./package.json": "./package.json"
44
44
  },
45
45
  "peerDependencies": {
46
- "svelte": "^5.53.7",
47
- "tailwindcss": "^4.2.1",
48
- "daisyui": "^5.5.19"
46
+ "svelte": "^5.54.1",
47
+ "tailwindcss": "^4.2.2",
48
+ "daisyui": "^5.5.19",
49
+ "dinero.js": "npm:dinero.js@^2.0.2",
50
+ "better-auth": "^1.5.5"
49
51
  },
50
52
  "peerDependenciesMeta": {
51
53
  "better-auth": {
52
54
  "optional": true
53
- },
54
- "dinero.js": {
55
- "optional": true
56
55
  }
57
56
  },
58
57
  "dependencies": {
59
- "hotkeys-js": "npm:hotkeys-js@^4.0.2",
60
- "better-auth": "^1.5.2",
61
- "dinero.js": "npm:dinero.js@^2.0.0"
58
+ "gwa": "^0.0.1",
59
+ "hotkeys-js": "npm:hotkeys-js@^4.0.2"
62
60
  },
63
61
  "devDependencies": {
64
- "@inlang/paraglide-js": "npm:@inlang/paraglide-js@^2.13.1",
62
+ "@inlang/paraglide-js": "npm:@inlang/paraglide-js@^2.15.1",
63
+ "@internal/core": "0.2.0",
64
+ "@internal/state": "0.2.0",
65
+ "@internal/ui": "0.2.0",
65
66
  "@sveltejs/adapter-static": "npm:@sveltejs/adapter-static@^3.0.10",
66
- "@sveltejs/kit": "npm:@sveltejs/kit@^2.53.4",
67
- "@sveltejs/vite-plugin-svelte": "npm:@sveltejs/vite-plugin-svelte@^7.0.0",
67
+ "@sveltejs/kit": "npm:@sveltejs/kit@^2.55.0",
68
68
  "@sveltejs/package": "^2.5.7",
69
+ "@sveltejs/vite-plugin-svelte": "npm:@sveltejs/vite-plugin-svelte@^7.0.0",
69
70
  "@tailwindcss/forms": "npm:@tailwindcss/forms@^0.5.11",
70
71
  "@tailwindcss/typography": "npm:@tailwindcss/typography@^0.5.19",
71
- "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.2.1",
72
- "svelte-check": "npm:svelte-check@^4.4.4",
73
- "typescript": "npm:typescript@^5.9.3",
74
- "vite": "npm:vite@^7.3.1",
72
+ "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.2.2",
73
+ "@testing-library/jest-dom": "^6.9.1",
74
+ "@testing-library/svelte": "^5.3.1",
75
+ "jsdom": "^29.0.1",
75
76
  "publint": "^0.3.18",
76
- "@internal/core": "0.2.0",
77
- "@internal/state": "0.2.0",
78
- "@internal/ui": "0.2.0"
77
+ "svelte-check": "npm:svelte-check@^4.4.5",
78
+ "typescript": "npm:typescript@^5.9.3",
79
+ "vite": "npm:vite@^8.0.1",
80
+ "vitest": "^4.1.0"
79
81
  }
80
82
  }