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.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/dist/core/design-tokens/props.d.ts +52 -0
  3. package/dist/core/design-tokens/props.d.ts.map +1 -0
  4. package/dist/core/design-tokens/props.js +34 -0
  5. package/dist/core/exchange-rate/strategies.d.ts +52 -0
  6. package/dist/core/exchange-rate/strategies.d.ts.map +1 -0
  7. package/dist/core/exchange-rate/strategies.js +72 -0
  8. package/dist/core/index.d.ts +8 -3
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +8 -3
  11. package/dist/core/internal/message-resolver.d.ts +1 -1
  12. package/dist/core/internal/message-resolver.d.ts.map +1 -1
  13. package/dist/core/layout/types.d.ts +60 -0
  14. package/dist/core/layout/types.d.ts.map +1 -0
  15. package/dist/core/layout/types.js +4 -0
  16. package/dist/core/money/index.d.ts +1 -1
  17. package/dist/core/money/index.d.ts.map +1 -1
  18. package/dist/core/money/index.js +1 -1
  19. package/dist/core/money/money-primitive.d.ts +101 -0
  20. package/dist/core/money/money-primitive.d.ts.map +1 -0
  21. package/dist/core/money/money-primitive.js +161 -0
  22. package/dist/core/money/money.d.ts +74 -2
  23. package/dist/core/money/money.d.ts.map +1 -1
  24. package/dist/core/money/money.js +120 -2
  25. package/dist/core/shortcuts/types.d.ts +60 -0
  26. package/dist/core/shortcuts/types.d.ts.map +1 -0
  27. package/dist/core/shortcuts/types.js +4 -0
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.js +6 -4
  30. package/dist/state/api.svelte.js +2 -2
  31. package/dist/state/app.svelte.js +1 -1
  32. package/dist/state/auth/index.d.ts +2 -2
  33. package/dist/state/auth/index.js +1 -1
  34. package/dist/state/auth/session.svelte.d.ts +1 -1
  35. package/dist/state/auth/session.svelte.js +7 -5
  36. package/dist/state/auth/types.d.ts +1 -1
  37. package/dist/state/cart.svelte.d.ts +1 -1
  38. package/dist/state/cart.svelte.js +1 -1
  39. package/dist/state/commands.svelte.d.ts +7 -7
  40. package/dist/state/commands.svelte.js +1 -1
  41. package/dist/state/composables/useMoney.d.ts +19 -3
  42. package/dist/state/composables/useMoney.js +70 -6
  43. package/dist/state/composables/useMoneyFilter.d.ts +20 -0
  44. package/dist/state/composables/useMoneyFilter.js +81 -0
  45. package/dist/state/composables/usePersistence.d.ts +1 -1
  46. package/dist/state/composables/usePersistence.js +1 -1
  47. package/dist/state/composables/useRuneLab.d.ts +1 -1
  48. package/dist/state/composables/useRuneLab.js +2 -2
  49. package/dist/state/composables/useShortcuts.d.ts +33 -0
  50. package/dist/state/composables/useShortcuts.js +75 -0
  51. package/dist/state/context.d.ts +1 -0
  52. package/dist/state/context.js +1 -0
  53. package/dist/state/createConfigStore.svelte.d.ts +4 -31
  54. package/dist/state/createConfigStore.svelte.js +62 -51
  55. package/dist/state/currency.svelte.d.ts +13 -9
  56. package/dist/state/currency.svelte.js +26 -10
  57. package/dist/state/currency.test.d.ts +1 -0
  58. package/dist/state/currency.test.js +35 -0
  59. package/dist/state/exchange-rate.svelte.d.ts +43 -0
  60. package/dist/state/exchange-rate.svelte.js +145 -0
  61. package/dist/state/exchange-rate.test.d.ts +1 -0
  62. package/dist/state/exchange-rate.test.js +75 -0
  63. package/dist/state/index.d.ts +26 -19
  64. package/dist/state/index.js +25 -18
  65. package/dist/state/language.svelte.d.ts +3 -10
  66. package/dist/state/language.svelte.js +4 -5
  67. package/dist/state/layout.svelte.d.ts +1 -1
  68. package/dist/state/layout.svelte.js +4 -4
  69. package/dist/state/persistence/drivers.d.ts +1 -1
  70. package/dist/state/persistence/drivers.js +9 -7
  71. package/dist/state/persistence/drivers.test.d.ts +1 -0
  72. package/dist/state/persistence/drivers.test.js +79 -0
  73. package/dist/state/persistence/provider.d.ts +23 -0
  74. package/dist/state/persistence/provider.js +43 -0
  75. package/dist/state/persistence/provider.test.d.ts +1 -0
  76. package/dist/state/persistence/provider.test.js +51 -0
  77. package/dist/state/registry/index.d.ts +44 -0
  78. package/dist/state/registry/index.js +58 -0
  79. package/dist/state/registry/registry.test.d.ts +1 -0
  80. package/dist/state/registry/registry.test.js +112 -0
  81. package/dist/state/registry/types.d.ts +20 -0
  82. package/dist/state/registry/types.js +3 -0
  83. package/dist/state/shortcuts.svelte.js +4 -4
  84. package/dist/state/theme.svelte.d.ts +3 -10
  85. package/dist/state/theme.svelte.js +8 -8
  86. package/dist/state/toast-bridge.d.ts +1 -1
  87. package/dist/state/toast.svelte.js +1 -1
  88. package/dist/ui/components/ApiMonitor.svelte +2 -2
  89. package/dist/ui/components/Icon.svelte +1 -1
  90. package/dist/ui/components/RuneProvider.svelte +28 -8
  91. package/dist/ui/components/RuneProvider.svelte.d.ts +12 -5
  92. package/dist/ui/components/Toaster.svelte +1 -1
  93. package/dist/ui/components/money/MoneyDisplay.svelte +91 -18
  94. package/dist/ui/components/money/MoneyDisplay.svelte.d.ts +15 -3
  95. package/dist/ui/components/money/MoneyDisplay.svelte.test.d.ts +1 -1
  96. package/dist/ui/components/money/MoneyDisplay.svelte.test.js +45 -2
  97. package/dist/ui/components/money/MoneyInput.svelte +123 -42
  98. package/dist/ui/components/money/MoneyInput.svelte.d.ts +14 -5
  99. package/dist/ui/features/command-palette/CommandPalette.svelte +3 -3
  100. package/dist/ui/features/config/APP_CONFIGURATIONS.d.ts +29 -0
  101. package/dist/ui/features/config/APP_CONFIGURATIONS.js +38 -0
  102. package/dist/ui/features/config/CurrencySelector.svelte +10 -36
  103. package/dist/ui/features/config/LanguageSelector.svelte +10 -33
  104. package/dist/ui/features/config/ResourceSelector.svelte +92 -0
  105. package/dist/ui/features/config/ResourceSelector.svelte.d.ts +25 -0
  106. package/dist/ui/features/config/ThemeSelector.svelte +11 -34
  107. package/dist/ui/features/shortcuts/ShortcutBinder.svelte +17 -0
  108. package/dist/ui/features/shortcuts/ShortcutBinder.svelte.d.ts +7 -0
  109. package/dist/ui/features/shortcuts/ShortcutPalette.svelte +3 -3
  110. package/dist/ui/index.d.ts +5 -1
  111. package/dist/ui/index.js +8 -3
  112. package/dist/ui/layout/ConnectedNavigationPanel.svelte +7 -8
  113. package/dist/ui/layout/ConnectedNavigationPanel.svelte.d.ts +1 -1
  114. package/dist/ui/layout/ConnectedWorkspaceStrip.svelte +5 -3
  115. package/dist/ui/layout/ConnectedWorkspaceStrip.svelte.d.ts +1 -1
  116. package/dist/ui/layout/NavigationPanel.svelte +1 -1
  117. package/dist/ui/layout/NavigationPanel.svelte.d.ts +1 -1
  118. package/dist/ui/layout/WorkspaceLayout.svelte +9 -1
  119. package/dist/ui/layout/WorkspaceLayout.svelte.d.ts +7 -0
  120. package/dist/ui/layout/WorkspaceStrip.svelte +1 -1
  121. package/dist/ui/layout/WorkspaceStrip.svelte.d.ts +1 -1
  122. package/dist/ui/layout/connection-factory.d.ts +50 -0
  123. package/dist/ui/layout/connection-factory.js +58 -0
  124. package/dist/ui/layout/index.d.ts +2 -2
  125. package/dist/ui/layout/index.js +1 -1
  126. package/dist/ui/paraglide/README.md +53 -0
  127. package/dist/ui/paraglide/runtime.d.ts +105 -124
  128. package/dist/ui/paraglide/runtime.js +162 -127
  129. package/dist/ui/paraglide/server.d.ts +6 -17
  130. package/dist/ui/paraglide/server.js +11 -20
  131. package/dist/ui/primitives/DatePicker.svelte +1 -1
  132. package/package.json +8 -8
  133. package/dist/state/daisyui.d.ts +0 -4
@@ -1,69 +1,142 @@
1
1
  <script lang="ts">
2
- import { getLanguageStore, getCurrencyStore } from "rune-lab/state";
3
- import { formatAmount, toMinorUnit, type ISO4217Code } from "rune-lab/core";
2
+ import { getLanguageStore, getCurrencyStore } from "@internal/state";
3
+ import {
4
+ formatAmount,
5
+ toMinorUnit,
6
+ type ISO4217Code,
7
+ MoneyPrimitive,
8
+ } from "@internal/core";
9
+ import { DEV } from "esm-env";
4
10
 
5
11
  let {
6
- amount,
7
- unit = 'minor',
12
+ amount: amountProp,
13
+ money,
14
+ unit = "minor",
8
15
  fallback = "—",
9
16
  currency,
17
+ sourceCurrency,
18
+ showSourceCurrency = false,
19
+ noRatesFallback,
10
20
  locale,
11
21
  compact = false,
12
22
  } = $props<{
13
23
  /** Amount in minor or major units (see unit prop) */
14
- amount: number | null | undefined;
15
- /**
24
+ amount?: number | null | undefined;
25
+ /**
26
+ * MoneyPrimitive instance. When provided, amount, currency, and unit
27
+ * are derived from the primitive. Takes precedence over raw amount.
28
+ */
29
+ money?: MoneyPrimitive;
30
+ /**
16
31
  * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
32
+ * @deprecated Use `money` prop with MoneyPrimitive instead.
17
33
  * Defaults to 'minor' for backward compatibility.
18
34
  */
19
- unit?: 'major' | 'minor';
35
+ unit?: "major" | "minor";
20
36
  /** String to display if amount is null, undefined, or NaN. Defaults to "—" */
21
37
  fallback?: string;
22
38
  /** Override currency code (defaults to CurrencyStore.current) */
23
39
  currency?: ISO4217Code | string;
40
+ /** The currency the amount is stored in (e.g. MXN) */
41
+ sourceCurrency?: ISO4217Code | string;
42
+ /** Show the original currency as a label */
43
+ showSourceCurrency?: boolean;
44
+ /** Fallback text if conversion is needed but rates are missing */
45
+ noRatesFallback?: string;
24
46
  /** Override locale (defaults to LanguageStore.current) */
25
47
  locale?: string;
26
48
  /** Use compact notation (e.g., $1.2M) */
27
49
  compact?: boolean;
28
50
  }>();
29
51
 
52
+ // Dev warning for deprecated usage
53
+ $effect(() => {
54
+ if (DEV && money && unit !== "minor") {
55
+ console.warn(
56
+ "[MoneyDisplay] The `unit` prop is deprecated when using `money: MoneyPrimitive`. " +
57
+ "The unit is derived from the MoneyPrimitive instance.",
58
+ );
59
+ }
60
+ });
61
+
62
+ // Derive effective values from MoneyPrimitive or legacy props
63
+ const amount = $derived(money ? money.minor : amountProp);
64
+ const effectiveSourceCurrency = $derived(
65
+ money ? money.currencyCode : sourceCurrency,
66
+ );
67
+
30
68
  const currencyStore = getCurrencyStore();
31
69
  const languageStore = getLanguageStore();
32
70
 
33
- const resolvedCurrency = $derived(
71
+ const resolvedDisplayCurrency = $derived(
34
72
  currency ?? String(currencyStore.current),
35
73
  );
36
74
 
75
+ const resolvedSourceCurrency = $derived(
76
+ effectiveSourceCurrency ?? resolvedDisplayCurrency,
77
+ );
78
+
37
79
  const resolvedLocale = $derived(
38
80
  locale ?? (String(languageStore.current) || "en"),
39
81
  );
40
82
 
41
- const currencyMeta = $derived(currencyStore.get(resolvedCurrency));
83
+ const currencyMeta = $derived(currencyStore.get(resolvedDisplayCurrency));
42
84
  const decimals = $derived(currencyMeta?.decimals ?? 2);
43
85
 
44
86
  const formatted = $derived.by(() => {
45
87
  // If amount is null/undefined/NaN, use fallback
46
- if (amount === null || amount === undefined || (typeof amount === 'number' && isNaN(amount))) {
88
+ if (
89
+ amount === null ||
90
+ amount === undefined ||
91
+ (typeof amount === "number" && isNaN(amount))
92
+ ) {
47
93
  return fallback;
48
94
  }
49
95
 
50
- // Convert to minor units if necessary
51
- const minorAmount = unit === 'major'
52
- ? toMinorUnit(Number(amount), resolvedCurrency)
53
- : amount;
96
+ // Convert to minor units if necessary (in source currency)
97
+ const minorAmount =
98
+ unit === "major"
99
+ ? toMinorUnit(Number(amount), resolvedSourceCurrency)
100
+ : amount;
101
+
102
+ // Conversion logic
103
+ let displayAmount = Number(minorAmount);
104
+ let displayCurrency = resolvedDisplayCurrency;
105
+
106
+ if (resolvedSourceCurrency !== resolvedDisplayCurrency) {
107
+ if (currencyStore.canConvert) {
108
+ displayAmount = currencyStore.convertAmount(
109
+ Number(minorAmount),
110
+ resolvedSourceCurrency,
111
+ resolvedDisplayCurrency,
112
+ );
113
+ } else if (noRatesFallback) {
114
+ return noRatesFallback;
115
+ } else {
116
+ // Fallback to source currency display if rates missing
117
+ displayCurrency = resolvedSourceCurrency;
118
+ }
119
+ }
54
120
 
55
121
  if (compact) {
56
- const majorUnits = Number(minorAmount) / Math.pow(10, decimals);
122
+ const displayDecimals =
123
+ currencyStore.get(displayCurrency)?.decimals ?? 2;
124
+ const majorUnits = displayAmount / Math.pow(10, displayDecimals);
57
125
  return new Intl.NumberFormat(resolvedLocale, {
58
126
  style: "currency",
59
- currency: resolvedCurrency,
127
+ currency: displayCurrency,
60
128
  notation: "compact",
61
129
  maximumFractionDigits: 1,
62
130
  }).format(majorUnits);
63
131
  }
64
132
 
65
- return formatAmount(minorAmount, resolvedCurrency, resolvedLocale);
133
+ return formatAmount(displayAmount, displayCurrency, resolvedLocale);
66
134
  });
67
135
  </script>
68
136
 
69
- <data value={amount} class="rl-money-display tabular-nums">{formatted}</data>
137
+ <data value={amount} class="rl-money-display tabular-nums">
138
+ {formatted}
139
+ {#if showSourceCurrency && sourceCurrency && sourceCurrency !== resolvedDisplayCurrency}
140
+ <small class="opacity-50 ml-1">({sourceCurrency})</small>
141
+ {/if}
142
+ </data>
@@ -1,16 +1,28 @@
1
- import { type ISO4217Code } from "rune-lab/core";
1
+ import { type ISO4217Code, MoneyPrimitive } from "@internal/core";
2
2
  type $$ComponentProps = {
3
3
  /** Amount in minor or major units (see unit prop) */
4
- amount: number | null | undefined;
4
+ amount?: number | null | undefined;
5
+ /**
6
+ * MoneyPrimitive instance. When provided, amount, currency, and unit
7
+ * are derived from the primitive. Takes precedence over raw amount.
8
+ */
9
+ money?: MoneyPrimitive;
5
10
  /**
6
11
  * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
12
+ * @deprecated Use `money` prop with MoneyPrimitive instead.
7
13
  * Defaults to 'minor' for backward compatibility.
8
14
  */
9
- unit?: 'major' | 'minor';
15
+ unit?: "major" | "minor";
10
16
  /** String to display if amount is null, undefined, or NaN. Defaults to "—" */
11
17
  fallback?: string;
12
18
  /** Override currency code (defaults to CurrencyStore.current) */
13
19
  currency?: ISO4217Code | string;
20
+ /** The currency the amount is stored in (e.g. MXN) */
21
+ sourceCurrency?: ISO4217Code | string;
22
+ /** Show the original currency as a label */
23
+ showSourceCurrency?: boolean;
24
+ /** Fallback text if conversion is needed but rates are missing */
25
+ noRatesFallback?: string;
14
26
  /** Override locale (defaults to LanguageStore.current) */
15
27
  locale?: string;
16
28
  /** Use compact notation (e.g., $1.2M) */
@@ -1 +1 @@
1
- export {};
1
+ import "@testing-library/jest-dom/vitest";
@@ -1,11 +1,26 @@
1
1
  import { render, screen } from "@testing-library/svelte";
2
+ import "@testing-library/jest-dom/vitest";
2
3
  import { describe, expect, it } from "vitest";
3
4
  import MoneyDisplay from "./MoneyDisplay.svelte";
4
- import { RUNE_LAB_CONTEXT } from "rune-lab/state";
5
+ import { RUNE_LAB_CONTEXT } from "@internal/state";
5
6
  // Mock stores for context
6
7
  const mockCurrencyStore = {
7
8
  current: "USD",
8
- get: (code) => ({ symbol: "$", decimals: 2 }),
9
+ get: (code) => {
10
+ if (code === "JPY")
11
+ return { symbol: "¥", decimals: 0 };
12
+ if (code === "MXN")
13
+ return { symbol: "$", decimals: 2 };
14
+ return { symbol: "$", decimals: 2 };
15
+ },
16
+ canConvert: true,
17
+ convertAmount: (amount, from, to) => {
18
+ if (from === "USD" && to === "MXN")
19
+ return amount * 20;
20
+ if (from === "MXN" && to === "USD")
21
+ return amount / 20;
22
+ return amount;
23
+ }
9
24
  };
10
25
  const mockLanguageStore = {
11
26
  current: "en-US",
@@ -71,4 +86,32 @@ describe("MoneyDisplay.svelte", () => {
71
86
  });
72
87
  expect(screen.getByText(/\$1\.2M/)).toBeInTheDocument();
73
88
  });
89
+ it("converts from sourceCurrency to current display currency", () => {
90
+ // 2000 MXN -> 100 USD
91
+ render(MoneyDisplay, {
92
+ props: { amount: 2000, unit: "major", sourceCurrency: "MXN" },
93
+ context,
94
+ });
95
+ expect(screen.getByText(/\$100\.00/)).toBeInTheDocument();
96
+ });
97
+ it("shows source currency label when showSourceCurrency is true", () => {
98
+ render(MoneyDisplay, {
99
+ props: { amount: 2000, unit: "major", sourceCurrency: "MXN", showSourceCurrency: true },
100
+ context,
101
+ });
102
+ expect(screen.getByText("(MXN)")).toBeInTheDocument();
103
+ });
104
+ it("falls back to source currency when conversion is impossible", () => {
105
+ const contextNoConvert = new Map(context);
106
+ contextNoConvert.set(RUNE_LAB_CONTEXT.currency, {
107
+ ...mockCurrencyStore,
108
+ canConvert: false
109
+ });
110
+ render(MoneyDisplay, {
111
+ props: { amount: 2000, unit: "major", sourceCurrency: "MXN" },
112
+ context: contextNoConvert,
113
+ });
114
+ // Should show $2,000.00 (MXN)
115
+ expect(screen.getByText(/\$2,000\.00/)).toBeInTheDocument();
116
+ });
74
117
  });
@@ -3,25 +3,34 @@
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";
6
+ import type { ISO4217Code } from "@internal/core";
7
+ import { MoneyPrimitive } from "@internal/core";
7
8
 
8
9
  export interface MoneyInputProps {
9
10
  /** Current value in minor or major units (see unit prop) */
10
11
  amount?: number | null | undefined;
11
- /**
12
+ /**
13
+ * MoneyPrimitive instance. When provided, initial amount and currency
14
+ * are derived from the primitive. Read-only input — output still fires oninput().
15
+ */
16
+ money?: MoneyPrimitive;
17
+ /**
12
18
  * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
19
+ * @deprecated Use `money` prop with MoneyPrimitive instead.
13
20
  * Defaults to 'minor' for backward compatibility.
14
21
  */
15
- unit?: 'major' | 'minor';
22
+ unit?: "major" | "minor";
16
23
  /** Override currency code (defaults to CurrencyStore.current) */
17
24
  currency?: ISO4217Code | string;
18
- /** Minimum value in same units as amount */
25
+ /** The currency the amount is stored in (e.g. MXN) */
26
+ storageCurrency?: ISO4217Code | string;
27
+ /** Minimum value in same units as amount (in storageCurrency) */
19
28
  min?: number;
20
- /** Maximum value in same units as amount */
29
+ /** Maximum value in same units as amount (in storageCurrency) */
21
30
  max?: number;
22
31
  /** Placeholder text */
23
32
  placeholder?: string;
24
- /** Fired when the value changes (unit matches the unit prop) */
33
+ /** Fired when the value changes (unit matches the unit prop, in storageCurrency) */
25
34
  oninput?: (val: number) => void;
26
35
  /** Input disabled state */
27
36
  disabled?: boolean;
@@ -29,13 +38,16 @@
29
38
  </script>
30
39
 
31
40
  <script lang="ts">
32
- import { getCurrencyStore } from "rune-lab/state";
33
- import { toMinorUnit } from "rune-lab/core";
41
+ import { getCurrencyStore } from "@internal/state";
42
+ import { toMinorUnit } from "@internal/core";
43
+ import { DEV } from "esm-env";
34
44
 
35
45
  let {
36
46
  amount = $bindable(0),
37
- unit = 'minor',
47
+ money,
48
+ unit = "minor",
38
49
  currency,
50
+ storageCurrency,
39
51
  min,
40
52
  max,
41
53
  placeholder = "0.00",
@@ -45,19 +57,60 @@
45
57
 
46
58
  const currencyStore = getCurrencyStore();
47
59
 
48
- const resolvedCurrency = $derived(
49
- currency ?? String(currencyStore.current),
60
+ // Dev warning for deprecated usage
61
+ $effect(() => {
62
+ if (DEV && money && unit !== "minor") {
63
+ console.warn(
64
+ "[MoneyInput] The `unit` prop is deprecated when using `money: MoneyPrimitive`. " +
65
+ "The unit is derived from the MoneyPrimitive instance.",
66
+ );
67
+ }
68
+ });
69
+
70
+ // Seed initial values from MoneyPrimitive if provided
71
+ $effect(() => {
72
+ if (money && amount === 0) {
73
+ amount = money.minor;
74
+ }
75
+ });
76
+
77
+ const resolvedDisplayCurrency = $derived(
78
+ money?.currencyCode ?? currency ?? String(currencyStore.current),
50
79
  );
51
80
 
52
- const currencyMeta = $derived(currencyStore.get(resolvedCurrency));
53
- const symbol = $derived(currencyMeta?.symbol ?? "$");
54
- const decimals = $derived(currencyMeta?.decimals ?? 2);
81
+ const resolvedStorageCurrency = $derived(
82
+ storageCurrency ?? money?.currencyCode ?? resolvedDisplayCurrency,
83
+ );
55
84
 
56
- // Convert to minor units internally for consistent arithmetic if needed,
57
- // but here we just need to display it.
85
+ const displayMeta = $derived(currencyStore.get(resolvedDisplayCurrency));
86
+ const symbol = $derived(displayMeta?.symbol ?? "$");
87
+ const decimals = $derived(displayMeta?.decimals ?? 2);
88
+
89
+ // The display value is reactive to amount (which is in storage currency)
58
90
  const displayValue = $derived.by(() => {
59
- const val = amount ?? 0;
60
- if (unit === 'major') {
91
+ let val = amount ?? 0;
92
+
93
+ // If storage !== display, convert for display
94
+ if (
95
+ resolvedStorageCurrency !== resolvedDisplayCurrency &&
96
+ currencyStore.canConvert
97
+ ) {
98
+ const minorAmount =
99
+ unit === "major"
100
+ ? toMinorUnit(val, resolvedStorageCurrency)
101
+ : val;
102
+ const convertedMinor = currencyStore.convertAmount(
103
+ Number(minorAmount),
104
+ resolvedStorageCurrency,
105
+ resolvedDisplayCurrency,
106
+ );
107
+
108
+ if (decimals === 0) return String(Math.round(convertedMinor));
109
+ return (convertedMinor / Math.pow(10, decimals)).toFixed(decimals);
110
+ }
111
+
112
+ // Standard display (no conversion or no rates)
113
+ if (unit === "major") {
61
114
  return Number(val).toFixed(decimals);
62
115
  }
63
116
  if (decimals === 0) return String(val);
@@ -69,7 +122,11 @@
69
122
  const target = e.target as HTMLInputElement;
70
123
  const raw = target.value.replace(/[^0-9.,-]/g, "");
71
124
 
72
- if (!raw) return;
125
+ if (!raw) {
126
+ amount = 0;
127
+ oninput?.(0);
128
+ return;
129
+ }
73
130
 
74
131
  // String-based parsing: split on decimal point
75
132
  const parts = raw.split(".");
@@ -79,33 +136,57 @@
79
136
  let fractionalPart = (parts[1] || "").slice(0, decimals);
80
137
  fractionalPart = fractionalPart.padEnd(decimals, "0");
81
138
 
82
- // Combine as integer minor units no floats involved
139
+ // Combine as integer minor units (in display currency)
83
140
  const combined =
84
141
  decimals === 0 ? integerPart : integerPart + fractionalPart;
85
142
 
86
- let cents = parseInt(combined, 10);
87
- if (isNaN(cents)) return;
88
-
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);
143
+ let displayCents = parseInt(combined, 10);
144
+ if (isNaN(displayCents)) return;
145
+
146
+ // Convert back to storage currency
147
+ let storageCents = displayCents;
148
+ if (
149
+ resolvedStorageCurrency !== resolvedDisplayCurrency &&
150
+ currencyStore.canConvert
151
+ ) {
152
+ storageCents = currencyStore.convertAmount(
153
+ displayCents,
154
+ resolvedDisplayCurrency,
155
+ resolvedStorageCurrency,
156
+ );
157
+ }
158
+
159
+ // Apply constraints in storage currency minor units
160
+ let finalStorageCents = storageCents;
161
+
162
+ const storageDecimals =
163
+ currencyStore.get(resolvedStorageCurrency)?.decimals ?? 2;
164
+ const minCents =
165
+ min !== undefined
166
+ ? unit === "major"
167
+ ? toMinorUnit(min, resolvedStorageCurrency)
168
+ : min
169
+ : undefined;
170
+ const maxCents =
171
+ max !== undefined
172
+ ? unit === "major"
173
+ ? toMinorUnit(max, resolvedStorageCurrency)
174
+ : max
175
+ : undefined;
176
+
177
+ if (minCents !== undefined)
178
+ finalStorageCents = Math.max(finalStorageCents, minCents);
179
+ if (maxCents !== undefined)
180
+ finalStorageCents = Math.min(finalStorageCents, maxCents);
181
+
182
+ if (unit === "major") {
183
+ const majorValue =
184
+ finalStorageCents / Math.pow(10, storageDecimals);
101
185
  amount = majorValue;
102
186
  oninput?.(majorValue);
103
187
  } 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);
188
+ amount = finalStorageCents;
189
+ oninput?.(finalStorageCents);
109
190
  }
110
191
  }
111
192
  </script>
@@ -123,9 +204,9 @@
123
204
  {placeholder}
124
205
  {disabled}
125
206
  oninput={handleInput}
126
- aria-label={`Amount in ${resolvedCurrency}`}
207
+ aria-label={`Amount in ${resolvedDisplayCurrency}`}
127
208
  />
128
209
  <span class="text-base-content/30 text-xs font-mono select-none"
129
- >{resolvedCurrency}</span
210
+ >{resolvedDisplayCurrency}</span
130
211
  >
131
212
  </label>
@@ -1,21 +1,30 @@
1
- import type { ISO4217Code } from "rune-lab/core";
1
+ import type { ISO4217Code } from "@internal/core";
2
+ import { MoneyPrimitive } from "@internal/core";
2
3
  export interface MoneyInputProps {
3
4
  /** Current value in minor or major units (see unit prop) */
4
5
  amount?: number | null | undefined;
6
+ /**
7
+ * MoneyPrimitive instance. When provided, initial amount and currency
8
+ * are derived from the primitive. Read-only input — output still fires oninput().
9
+ */
10
+ money?: MoneyPrimitive;
5
11
  /**
6
12
  * Whether the amount is in 'major' (e.g., pesos) or 'minor' (e.g., centavos) units.
13
+ * @deprecated Use `money` prop with MoneyPrimitive instead.
7
14
  * Defaults to 'minor' for backward compatibility.
8
15
  */
9
- unit?: 'major' | 'minor';
16
+ unit?: "major" | "minor";
10
17
  /** Override currency code (defaults to CurrencyStore.current) */
11
18
  currency?: ISO4217Code | string;
12
- /** Minimum value in same units as amount */
19
+ /** The currency the amount is stored in (e.g. MXN) */
20
+ storageCurrency?: ISO4217Code | string;
21
+ /** Minimum value in same units as amount (in storageCurrency) */
13
22
  min?: number;
14
- /** Maximum value in same units as amount */
23
+ /** Maximum value in same units as amount (in storageCurrency) */
15
24
  max?: number;
16
25
  /** Placeholder text */
17
26
  placeholder?: string;
18
- /** Fired when the value changes (unit matches the unit prop) */
27
+ /** Fired when the value changes (unit matches the unit prop, in storageCurrency) */
19
28
  oninput?: (val: number) => void;
20
29
  /** Input disabled state */
21
30
  disabled?: boolean;
@@ -1,9 +1,9 @@
1
1
  <!-- src/client/sdk/ui/src/features/config/CommandPalette.svelte -->
2
2
  <script lang="ts">
3
3
  import { onMount, tick } from "svelte";
4
- import { getCommandStore, type Command } from "rune-lab/state";
5
- import { getShortcutStore } from "rune-lab/state";
6
- import { Icon } from "rune-lab/ui";
4
+ import { getCommandStore, type Command } from "@internal/state";
5
+ import { getShortcutStore } from "@internal/state";
6
+ import { Icon } from "../../../mod";
7
7
 
8
8
  let { shortcutKey = "shift+k" } = $props<{ shortcutKey?: string }>();
9
9
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Describes a single configuration dimension
3
+ * (e.g., theme, language, currency) for the generic selector system.
4
+ */
5
+ export interface ConfigDimension {
6
+ /** Unique key matching the RUNE_LAB_CONTEXT key */
7
+ readonly key: string;
8
+ /** Key used to look up the ConfigStore from context */
9
+ readonly storeKey: string;
10
+ /** Property name used as the item identifier */
11
+ readonly idKey: string;
12
+ /** Emoji used for display / logging */
13
+ readonly icon: string;
14
+ /** Human-readable label for settings panels */
15
+ readonly label: string;
16
+ }
17
+ /**
18
+ * All built-in configuration dimensions.
19
+ * Used by `AppSettingSelector` and settings panels to enumerate available
20
+ * config pickers without hard-coding them.
21
+ *
22
+ * @example
23
+ * ```svelte
24
+ * {#each APP_CONFIGURATIONS as dim}
25
+ * <ResourceSelector store={stores[dim.storeKey]} idKey={dim.idKey} ... />
26
+ * {/each}
27
+ * ```
28
+ */
29
+ export declare const APP_CONFIGURATIONS: readonly ConfigDimension[];
@@ -0,0 +1,38 @@
1
+ // sdk/ui/src/lib/features/config/APP_CONFIGURATIONS.ts
2
+ // Single source of truth for all configuration dimensions.
3
+ // Frozen at import time — must not be mutated at runtime.
4
+ /**
5
+ * All built-in configuration dimensions.
6
+ * Used by `AppSettingSelector` and settings panels to enumerate available
7
+ * config pickers without hard-coding them.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * {#each APP_CONFIGURATIONS as dim}
12
+ * <ResourceSelector store={stores[dim.storeKey]} idKey={dim.idKey} ... />
13
+ * {/each}
14
+ * ```
15
+ */
16
+ export const APP_CONFIGURATIONS = Object.freeze([
17
+ {
18
+ key: "theme",
19
+ storeKey: "theme",
20
+ idKey: "name",
21
+ icon: "🎨",
22
+ label: "Theme",
23
+ },
24
+ {
25
+ key: "language",
26
+ storeKey: "language",
27
+ idKey: "code",
28
+ icon: "🌍",
29
+ label: "Language",
30
+ },
31
+ {
32
+ key: "currency",
33
+ storeKey: "currency",
34
+ idKey: "code",
35
+ icon: "💰",
36
+ label: "Currency",
37
+ },
38
+ ]);