@happyvertical/smrt-ui 0.43.4 → 0.43.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +33 -0
- package/README.md +78 -4
- package/dist/components/display/CurrencyDisplay.contract.d.ts +7 -0
- package/dist/components/display/CurrencyDisplay.contract.d.ts.map +1 -0
- package/dist/components/display/CurrencyDisplay.contract.js +1 -0
- package/dist/components/display/CurrencyDisplay.svelte +148 -17
- package/dist/components/display/CurrencyDisplay.svelte.d.ts +8 -5
- package/dist/components/display/CurrencyDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/CurrencyDisplay.test.js +193 -3
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +20 -0
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +19 -0
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +1 -0
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +7 -0
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +19 -0
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +1 -0
- package/dist/components/display/currency-metadata.d.ts +2 -0
- package/dist/components/display/currency-metadata.d.ts.map +1 -0
- package/dist/components/display/currency-metadata.js +14 -0
- package/dist/components/forms/Checkbox.svelte +15 -6
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +45 -10
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/FilePicker.svelte +5 -2
- package/dist/components/forms/Form.svelte +57 -0
- package/dist/components/forms/Form.svelte.d.ts +2 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/Input.svelte +59 -13
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +33 -6
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -19
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/RadioGroup.svelte +14 -3
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/RangeSlider.svelte +61 -6
- package/dist/components/forms/RangeSlider.svelte.d.ts.map +1 -1
- package/dist/components/forms/SegmentedControl.svelte +33 -6
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +32 -9
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/Slider.svelte +26 -6
- package/dist/components/forms/Slider.svelte.d.ts.map +1 -1
- package/dist/components/forms/StagedControlReview.svelte +637 -0
- package/dist/components/forms/StagedControlReview.svelte.d.ts +13 -0
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -0
- package/dist/components/forms/Switch.svelte +16 -7
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/TagsInput.svelte +36 -8
- package/dist/components/forms/TagsInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/Textarea.svelte +20 -8
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/ToggleButton.svelte +20 -3
- package/dist/components/forms/ToggleButton.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/Form.test.js +106 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +668 -0
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte +22 -4
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +6 -2
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/composite-controls.test.js +96 -1
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +41 -0
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +3323 -15
- package/dist/components/forms/__tests__/core-controls.fixture.svelte +16 -5
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +1 -0
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/core-controls.test.js +223 -2
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +25 -0
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte +25 -0
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +21 -0
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +23 -0
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/staged-review.fixture.svelte +56 -0
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/control-interaction-context.d.ts +9 -1
- package/dist/components/forms/control-interaction-context.d.ts.map +1 -1
- package/dist/components/forms/control-interaction-context.js +16 -0
- package/dist/components/forms/control-interaction.d.ts +97 -7
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +1019 -92
- package/dist/components/forms/control-value-validation.d.ts +19 -0
- package/dist/components/forms/control-value-validation.d.ts.map +1 -0
- package/dist/components/forms/control-value-validation.js +152 -0
- package/dist/components/forms/index.d.ts +3 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +2 -1
- package/dist/components/forms/staged-control-review.d.ts +21 -0
- package/dist/components/forms/staged-control-review.d.ts.map +1 -0
- package/dist/components/forms/staged-control-review.js +1 -0
- package/dist/components/forms/use-control-registration.svelte.d.ts.map +1 -1
- package/dist/components/forms/use-control-registration.svelte.js +23 -4
- package/dist/i18n/strings.d.ts +3 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +3 -0
- package/dist/i18n/strings.ui.d.ts +18 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +19 -0
- package/dist/svelte/playground/InteractiveControlsPreview.svelte +3 -4
- package/dist/svelte/playground/InteractiveControlsPreview.svelte.d.ts.map +1 -1
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -112,6 +112,39 @@ other text pairing clears WCAG AA in both schemes.
|
|
|
112
112
|
interaction registry. Keep chat/voice transports outside this package;
|
|
113
113
|
mutations must retain sensitivity checks and the stage → confirmed apply
|
|
114
114
|
policy.
|
|
115
|
+
- Agent-originated form values remain proposals: the registry records their
|
|
116
|
+
provenance, timestamp, and revision, while `StagedControlReview` is the shared
|
|
117
|
+
review/apply surface mounted by both base and rich Forms. Agents can stage but
|
|
118
|
+
cannot self-confirm; only a local human handler submits confirmed apply,
|
|
119
|
+
discard, clear, or undo commands after the registry validates a trusted DOM
|
|
120
|
+
gesture while its event is actively dispatching. Retained events are rejected,
|
|
121
|
+
and the complete command or batch is snapshotted synchronously before gesture
|
|
122
|
+
consumption. Serialized or programmatic `source: 'user', confirmed: true`
|
|
123
|
+
input is not confirmation.
|
|
124
|
+
`StagedControlReview` marks its complete edited Apply value with
|
|
125
|
+
`reviewedValueIsCanonical: true`, but the marker grants no authority and is
|
|
126
|
+
honored only for a current staged entry under the registry's exact local
|
|
127
|
+
gesture proof. A control with proposal-relative preparation implements
|
|
128
|
+
`prepareReviewedValue` to validate/canonicalize the complete displayed value;
|
|
129
|
+
without that hook, marked edits still pass through ordinary `prepareValue`.
|
|
130
|
+
Only an unchanged value exactly equal to the stored staged canonical value can
|
|
131
|
+
reuse that trusted stored value without either hook.
|
|
132
|
+
Secret controls never accept staging, and sensitive/secret
|
|
133
|
+
values stay redacted in snapshots, events, policy callbacks, and review UI.
|
|
134
|
+
Batch execution is ordered best-effort and returns one result per command;
|
|
135
|
+
review-surface batch actions exclude stale and invalid proposals, which remain
|
|
136
|
+
visible with per-field feedback.
|
|
137
|
+
`executeBatch` is additive and optional for injected legacy registries; the
|
|
138
|
+
framework falls back to ordered `execute` calls without weakening the private
|
|
139
|
+
one-shot gesture proof on factory-created registries.
|
|
140
|
+
Rejecting async custom setters and clear handlers roll back by default;
|
|
141
|
+
controls that accept concurrent direct edits expose a monotonic revision and
|
|
142
|
+
user value from `getUserEditSnapshot()` so rollback restores newer human input;
|
|
143
|
+
fallible async setters expose an infallible `restoreValue()` path. Async hooks
|
|
144
|
+
issue nested commands through their optional final `ControlExtensionContext`;
|
|
145
|
+
same-control mutations are rejected there without timing out unrelated queued
|
|
146
|
+
commands. Context-aware setters implement the additive `setValueWithContext`
|
|
147
|
+
hook; legacy `setValue` is always invoked with exactly one argument.
|
|
115
148
|
|
|
116
149
|
## Gotchas
|
|
117
150
|
|
package/README.md
CHANGED
|
@@ -45,6 +45,34 @@ Svelte-free `/data-surface` entry exposes the registry contracts and shared
|
|
|
45
45
|
protocol limits for server adapters. The package root remains a compatibility
|
|
46
46
|
barrel.
|
|
47
47
|
|
|
48
|
+
### Currency display
|
|
49
|
+
|
|
50
|
+
`CurrencyDisplay` accepts ISO 4217 codes as a public `string` prop so persisted
|
|
51
|
+
Commerce currency fields can be passed directly. Codes are trimmed and
|
|
52
|
+
uppercased before `Intl.NumberFormat` formatting; the default remains CAD.
|
|
53
|
+
Malformed or unsupported codes render an accessible inline error instead of
|
|
54
|
+
throwing and interrupting a surrounding collection render.
|
|
55
|
+
With the default historical `unit="cents"` setting, amounts are interpreted as
|
|
56
|
+
the selected currency's ISO minor units (for example, 0 digits for JPY and 3
|
|
57
|
+
for BHD). Minor-unit amounts must be finite safe integers; fractional or unsafe
|
|
58
|
+
numeric values render an accessible inline error instead of being rounded.
|
|
59
|
+
`unit="dollars"` means the value is already in major units.
|
|
60
|
+
ISO fund, metal, test, and no-currency codes whose minor unit is `N.A.` require
|
|
61
|
+
`unit="dollars"`; the default minor-unit mode renders an accessible inline
|
|
62
|
+
error for those codes. Major-unit values for these codes use a stable two-digit
|
|
63
|
+
display policy across server and browser runtimes. CAD and USD retain their
|
|
64
|
+
symbol display; all other codes render their ISO code so SSR output does not
|
|
65
|
+
depend on runtime-specific symbol data.
|
|
66
|
+
|
|
67
|
+
```svelte
|
|
68
|
+
<script lang="ts">
|
|
69
|
+
import { CurrencyDisplay } from '@happyvertical/smrt-ui';
|
|
70
|
+
let invoiceCurrency: string = 'eur';
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<CurrencyDisplay amount={12345} currency={invoiceCurrency} />
|
|
74
|
+
```
|
|
75
|
+
|
|
48
76
|
## Component standard
|
|
49
77
|
|
|
50
78
|
Foundation components follow one contract:
|
|
@@ -99,10 +127,56 @@ without coupling controls to a transport:
|
|
|
99
127
|
```
|
|
100
128
|
|
|
101
129
|
Controls publish serializable metadata, constraints, options, sensitivity, and
|
|
102
|
-
capabilities. Adapters can focus, reveal, highlight, explain, validate,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
capabilities. Adapters can focus, reveal, highlight, explain, validate, and
|
|
131
|
+
stage reviewable proposals. Agents cannot apply, discard, clear, or undo;
|
|
132
|
+
those value-changing actions require a trusted local gesture handled by the
|
|
133
|
+
framework review surface. Secret/read-only controls reject agent mutations.
|
|
134
|
+
Staging remains separate so proposals never change user state before review.
|
|
135
|
+
Custom local review controls can call `executeLocalControlCommand` or
|
|
136
|
+
`executeLocalControlBatch` synchronously from their DOM handlers; the registry
|
|
137
|
+
requires the event to still be actively dispatching, snapshots the complete
|
|
138
|
+
command, and consumes the gesture before authorizing a value-changing command.
|
|
139
|
+
Retaining an event for later use is rejected even when it remains trusted.
|
|
140
|
+
Serialized or programmatic `source: 'user', confirmed: true` input is never
|
|
141
|
+
confirmation. Sensitive and secret values, validation details, failures, and
|
|
142
|
+
events remain redacted from every public surface.
|
|
143
|
+
The shared review surface marks its complete edited Apply value with
|
|
144
|
+
`reviewedValueIsCanonical: true`. That marker carries no authority: the registry
|
|
145
|
+
honors it only for a current staged entry after validating the exact command
|
|
146
|
+
under an actively dispatching local gesture. Controls with non-idempotent
|
|
147
|
+
proposal preparation can implement `prepareReviewedValue(value)` to validate or
|
|
148
|
+
canonicalize the complete displayed value without re-applying proposal-relative
|
|
149
|
+
behavior. Controls without that hook route marked edits through their ordinary
|
|
150
|
+
`prepareValue`, so a generic marker never bypasses custom normalization or
|
|
151
|
+
rejection. An unchanged value exactly equal to the stored staged canonical value
|
|
152
|
+
uses that trusted stored value directly.
|
|
153
|
+
Registries expose optional `refresh(formId)` notification for hosts whose live
|
|
154
|
+
metadata or runtime-state getters change without a registration event; it
|
|
155
|
+
updates subscribers without discarding an internal staged proposal.
|
|
156
|
+
`executeBatch` is an additive optional registry method; Forms fall back to
|
|
157
|
+
ordered `execute` calls for older injected registries. Factory-created
|
|
158
|
+
registries retain the framework's private, one-shot gesture proof, while an
|
|
159
|
+
older custom registry remains responsible for its pre-existing execution
|
|
160
|
+
policy and accepts review actions only from a trusted browser event.
|
|
161
|
+
Custom controls whose clear operation is intentionally idempotent should return
|
|
162
|
+
`true` from `clear()` to affirm that the unchanged cleared value was accepted.
|
|
163
|
+
Async custom setters and clear handlers are rolled back when they reject. A
|
|
164
|
+
control that permits direct edits while an async mutation is pending can expose
|
|
165
|
+
`getUserEditSnapshot()` and update its revision and value only for direct user
|
|
166
|
+
edits so rollback restores newer human input even if the handler mutates again
|
|
167
|
+
before rejecting. A fallible async setter should also expose `restoreValue()` as
|
|
168
|
+
an infallible state restoration path that does not repeat the external workflow.
|
|
169
|
+
Async policy, validation, setter, clear, and restoration hooks receive an
|
|
170
|
+
optional final `ControlExtensionContext`. Hooks that need to issue another
|
|
171
|
+
control command should use `extension.execute()`; it rejects a mutation of the
|
|
172
|
+
same control immediately, while commands from independent callers remain in
|
|
173
|
+
the normal ordered queue regardless of how long the hook takes. Existing hooks
|
|
174
|
+
that omit the additional argument remain compatible. Setters retain their exact
|
|
175
|
+
legacy `setValue(value)` invocation; a setter that needs this context implements
|
|
176
|
+
the additive `setValueWithContext(value, extension)` hook instead. A hook must
|
|
177
|
+
not await a same-control mutation through a captured registry reference: that
|
|
178
|
+
call is indistinguishable from an independent caller in browser runtimes and,
|
|
179
|
+
like any hook that never settles, can hold the ordered queue indefinitely.
|
|
106
180
|
|
|
107
181
|
## DataTable controller
|
|
108
182
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import type CurrencyDisplay from './CurrencyDisplay.svelte';
|
|
3
|
+
type Assert<T extends true> = T;
|
|
4
|
+
/** Compile-time guard for Commerce models whose currency field is `string`. */
|
|
5
|
+
export type CurrencyDisplayAcceptsCommerceCurrency = Assert<string extends NonNullable<ComponentProps<typeof CurrencyDisplay>['currency']> ? true : false>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=CurrencyDisplay.contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CurrencyDisplay.contract.d.ts","sourceRoot":"","sources":["../../../src/components/display/CurrencyDisplay.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,eAAe,MAAM,0BAA0B,CAAC;AAE5D,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEhC,+EAA+E;AAC/E,MAAM,MAAM,sCAAsC,GAAG,MAAM,CACzD,MAAM,SAAS,WAAW,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC,GAC1E,IAAI,GACJ,KAAK,CACV,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,19 +1,89 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { ISO_4217_MINOR_UNITS } from './currency-metadata.js';
|
|
3
|
+
|
|
4
|
+
interface NormalizedCurrency {
|
|
5
|
+
code: string;
|
|
6
|
+
minorUnitDigits: number | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeCurrencyCode(value: unknown): NormalizedCurrency | null {
|
|
10
|
+
if (typeof value !== 'string') return null;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (!/^[A-Za-z]{3}$/.test(trimmed)) return null;
|
|
13
|
+
const code = trimmed.toUpperCase();
|
|
14
|
+
|
|
15
|
+
const minorUnitDigits = ISO_4217_MINOR_UNITS.get(code);
|
|
16
|
+
return minorUnitDigits === undefined ? null : { code, minorUnitDigits };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function invalidCurrencyCode(value: unknown): string {
|
|
20
|
+
if (typeof value !== 'string') return '(non-string)';
|
|
21
|
+
const characters = Array.from(value.trim());
|
|
22
|
+
if (characters.length === 0) return '(empty)';
|
|
23
|
+
|
|
24
|
+
let diagnostic = '';
|
|
25
|
+
let consumed = 0;
|
|
26
|
+
for (const character of characters) {
|
|
27
|
+
const codePoint = character.codePointAt(0);
|
|
28
|
+
if (codePoint === undefined) continue;
|
|
29
|
+
const visibleCharacter =
|
|
30
|
+
codePoint >= 0x20 && codePoint <= 0x7e
|
|
31
|
+
? character.replace(/[a-z]/g, (ascii) => ascii.toUpperCase())
|
|
32
|
+
: `\\u{${codePoint.toString(16).toUpperCase()}}`;
|
|
33
|
+
if (diagnostic.length + visibleCharacter.length > 12) break;
|
|
34
|
+
diagnostic += visibleCharacter;
|
|
35
|
+
consumed += 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return consumed < characters.length ? `${diagnostic}…` : diagnostic;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isStringNumericLiteral(
|
|
42
|
+
value: string,
|
|
43
|
+
): value is Intl.StringNumericLiteral {
|
|
44
|
+
return /^(?:0|[1-9]\d*)\.\d+$/.test(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function exactMajorUnitValue(
|
|
48
|
+
amount: number,
|
|
49
|
+
minorUnitDigits: number,
|
|
50
|
+
): bigint | Intl.StringNumericLiteral | null {
|
|
51
|
+
const absoluteAmount = Math.abs(amount);
|
|
52
|
+
if (!Number.isSafeInteger(absoluteAmount)) return null;
|
|
53
|
+
|
|
54
|
+
const minorUnits = BigInt(absoluteAmount);
|
|
55
|
+
if (minorUnitDigits === 0) return minorUnits;
|
|
56
|
+
|
|
57
|
+
const scale = 10n ** BigInt(minorUnitDigits);
|
|
58
|
+
const exactValue = `${minorUnits / scale}.${(minorUnits % scale)
|
|
59
|
+
.toString()
|
|
60
|
+
.padStart(minorUnitDigits, '0')}`;
|
|
61
|
+
return isStringNumericLiteral(exactValue) ? exactValue : null;
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
1
65
|
<script lang="ts">
|
|
66
|
+
import { M } from '../../i18n/strings.js';
|
|
67
|
+
import { useI18n } from '../../i18n/use-i18n.js';
|
|
68
|
+
|
|
2
69
|
/**
|
|
3
70
|
* CurrencyDisplay - Formats and displays monetary values
|
|
4
71
|
*
|
|
5
72
|
* Displays formatted currency with configurable unit.
|
|
6
|
-
* Use `unit="cents"` (default) when amount is in
|
|
7
|
-
*
|
|
73
|
+
* Use `unit="cents"` (default) when amount is in the currency's minor units,
|
|
74
|
+
* or `unit="dollars"` when it is already in major units.
|
|
75
|
+
* Accepts ISO 4217 currency codes, normalized by trimming whitespace and
|
|
76
|
+
* uppercasing before locale formatting. Unsupported codes render an accessible
|
|
77
|
+
* inline error instead of throwing during a collection render.
|
|
8
78
|
*/
|
|
9
79
|
|
|
10
80
|
/** Props for CurrencyDisplay component */
|
|
11
81
|
export interface Props {
|
|
12
82
|
/** Amount value */
|
|
13
83
|
amount: number;
|
|
14
|
-
/**
|
|
15
|
-
currency?:
|
|
16
|
-
/** Whether amount is
|
|
84
|
+
/** ISO 4217 currency code. Whitespace is trimmed and letters are uppercased. */
|
|
85
|
+
currency?: string;
|
|
86
|
+
/** Whether amount is a safe integer of ISO minor units or a major-unit number */
|
|
17
87
|
unit?: 'cents' | 'dollars';
|
|
18
88
|
/** Show +/- sign for non-zero values */
|
|
19
89
|
showSign?: boolean;
|
|
@@ -38,19 +108,75 @@ const {
|
|
|
38
108
|
class: className = '',
|
|
39
109
|
}: Props = $props();
|
|
40
110
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
111
|
+
const { t } = useI18n();
|
|
112
|
+
|
|
113
|
+
interface FormattedCurrency {
|
|
114
|
+
text: string;
|
|
115
|
+
invalidCode: string | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Format amount using the platform's canonical currency formatter.
|
|
119
|
+
const formatted = $derived.by((): FormattedCurrency => {
|
|
120
|
+
const normalizedCurrency = normalizeCurrencyCode(currency);
|
|
121
|
+
if (!normalizedCurrency) {
|
|
122
|
+
const invalidCode = invalidCurrencyCode(currency);
|
|
123
|
+
return {
|
|
124
|
+
text: t(M['ui.currency_display.invalid_code'], { code: invalidCode }),
|
|
125
|
+
invalidCode,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
45
128
|
|
|
46
|
-
const
|
|
129
|
+
const formatOptions: Intl.NumberFormatOptions = {
|
|
47
130
|
style: 'currency',
|
|
48
|
-
currency,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
131
|
+
currency: normalizedCurrency.code,
|
|
132
|
+
// CAD and USD retain their historical symbol display. Using the ISO code
|
|
133
|
+
// for every other currency avoids ICU-dependent narrow-symbol differences
|
|
134
|
+
// between server and browser runtimes.
|
|
135
|
+
currencyDisplay:
|
|
136
|
+
normalizedCurrency.code === 'CAD' || normalizedCurrency.code === 'USD'
|
|
137
|
+
? 'symbol'
|
|
138
|
+
: 'code',
|
|
139
|
+
};
|
|
140
|
+
const displayDigits = normalizedCurrency.minorUnitDigits ?? 2;
|
|
141
|
+
formatOptions.minimumFractionDigits = displayDigits;
|
|
142
|
+
formatOptions.maximumFractionDigits = displayDigits;
|
|
143
|
+
|
|
144
|
+
let formatter: Intl.NumberFormat;
|
|
145
|
+
try {
|
|
146
|
+
formatter = new Intl.NumberFormat('en-CA', formatOptions);
|
|
147
|
+
} catch {
|
|
148
|
+
return {
|
|
149
|
+
text: t(M['ui.currency_display.invalid_code'], {
|
|
150
|
+
code: normalizedCurrency.code,
|
|
151
|
+
}),
|
|
152
|
+
invalidCode: normalizedCurrency.code,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
52
155
|
|
|
53
|
-
let
|
|
156
|
+
let majorAmount: number | bigint | Intl.StringNumericLiteral =
|
|
157
|
+
Math.abs(amount);
|
|
158
|
+
if (unit === 'cents') {
|
|
159
|
+
if (normalizedCurrency.minorUnitDigits == null) {
|
|
160
|
+
return {
|
|
161
|
+
text: t(M['ui.currency_display.no_minor_unit'], {
|
|
162
|
+
code: normalizedCurrency.code,
|
|
163
|
+
}),
|
|
164
|
+
invalidCode: normalizedCurrency.code,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const exactAmount = exactMajorUnitValue(
|
|
168
|
+
amount,
|
|
169
|
+
normalizedCurrency.minorUnitDigits,
|
|
170
|
+
);
|
|
171
|
+
if (exactAmount === null) {
|
|
172
|
+
return {
|
|
173
|
+
text: t(M['ui.currency_display.invalid_minor_unit_amount']),
|
|
174
|
+
invalidCode: normalizedCurrency.code,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
majorAmount = exactAmount;
|
|
178
|
+
}
|
|
179
|
+
let display = formatter.format(majorAmount);
|
|
54
180
|
|
|
55
181
|
// Add sign if requested
|
|
56
182
|
if (showSign && amount !== 0) {
|
|
@@ -60,7 +186,7 @@ const formatted = $derived.by(() => {
|
|
|
60
186
|
display = `-${display}`;
|
|
61
187
|
}
|
|
62
188
|
|
|
63
|
-
return display;
|
|
189
|
+
return { text: display, invalidCode: null };
|
|
64
190
|
});
|
|
65
191
|
|
|
66
192
|
// Determine color class
|
|
@@ -77,8 +203,9 @@ const colorClass = $derived.by(() => {
|
|
|
77
203
|
class:lg={size === 'lg'}
|
|
78
204
|
class:negative={colorClass === 'negative'}
|
|
79
205
|
class:positive={colorClass === 'positive'}
|
|
206
|
+
class:invalid={formatted.invalidCode !== null}
|
|
80
207
|
>
|
|
81
|
-
{formatted}
|
|
208
|
+
{formatted.text}
|
|
82
209
|
</span>
|
|
83
210
|
|
|
84
211
|
<style>
|
|
@@ -103,4 +230,8 @@ const colorClass = $derived.by(() => {
|
|
|
103
230
|
.currency-display.positive {
|
|
104
231
|
color: var(--smrt-color-tertiary, #16a34a);
|
|
105
232
|
}
|
|
233
|
+
|
|
234
|
+
.currency-display.invalid {
|
|
235
|
+
color: var(--smrt-color-error, #dc2626);
|
|
236
|
+
}
|
|
106
237
|
</style>
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
* CurrencyDisplay - Formats and displays monetary values
|
|
3
3
|
*
|
|
4
4
|
* Displays formatted currency with configurable unit.
|
|
5
|
-
* Use `unit="cents"` (default) when amount is in
|
|
6
|
-
*
|
|
5
|
+
* Use `unit="cents"` (default) when amount is in the currency's minor units,
|
|
6
|
+
* or `unit="dollars"` when it is already in major units.
|
|
7
|
+
* Accepts ISO 4217 currency codes, normalized by trimming whitespace and
|
|
8
|
+
* uppercasing before locale formatting. Unsupported codes render an accessible
|
|
9
|
+
* inline error instead of throwing during a collection render.
|
|
7
10
|
*/
|
|
8
11
|
/** Props for CurrencyDisplay component */
|
|
9
12
|
export interface Props {
|
|
10
13
|
/** Amount value */
|
|
11
14
|
amount: number;
|
|
12
|
-
/**
|
|
13
|
-
currency?:
|
|
14
|
-
/** Whether amount is
|
|
15
|
+
/** ISO 4217 currency code. Whitespace is trimmed and letters are uppercased. */
|
|
16
|
+
currency?: string;
|
|
17
|
+
/** Whether amount is a safe integer of ISO minor units or a major-unit number */
|
|
15
18
|
unit?: 'cents' | 'dollars';
|
|
16
19
|
/** Show +/- sign for non-zero values */
|
|
17
20
|
showSign?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CurrencyDisplay.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/display/CurrencyDisplay.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CurrencyDisplay.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/display/CurrencyDisplay.svelte.ts"],"names":[],"mappings":"AAsEA;;;;;;;;;GASG;AAEH,0CAA0C;AAC1C,MAAM,WAAW,KAAK;IACpB,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqHD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -8,20 +8,39 @@
|
|
|
8
8
|
* code, sign handling (negative/zero/showSign), highlight classes, size
|
|
9
9
|
* classes, and axe-cleanliness.
|
|
10
10
|
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
11
13
|
import { render, screen } from '@testing-library/svelte';
|
|
14
|
+
import { hydrate, unmount } from 'svelte';
|
|
15
|
+
import { createServer } from 'vite';
|
|
12
16
|
import { describe, expect, it } from 'vitest';
|
|
13
17
|
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
14
18
|
import CurrencyDisplay from '../CurrencyDisplay.svelte';
|
|
19
|
+
import { ISO_4217_MINOR_UNITS } from '../currency-metadata.js';
|
|
20
|
+
import CurrencyDisplayI18nHarness from './CurrencyDisplayI18nHarness.svelte';
|
|
21
|
+
import CurrencyDisplaySsrHarness from './CurrencyDisplaySsrHarness.svelte';
|
|
22
|
+
function metadataDigest(metadata) {
|
|
23
|
+
const canonical = [...metadata.entries()]
|
|
24
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
25
|
+
.map(([code, minorUnits]) => `${code}:${minorUnits ?? 'N.A.'}`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
28
|
+
}
|
|
15
29
|
/** Mirror the component's absolute-value currency formatting. */
|
|
16
|
-
function money(absDollars, currency = 'CAD') {
|
|
30
|
+
function money(absDollars, currency = 'CAD', minorUnitDigits) {
|
|
17
31
|
return new Intl.NumberFormat('en-CA', {
|
|
18
32
|
style: 'currency',
|
|
19
33
|
currency,
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
currencyDisplay: currency === 'CAD' || currency === 'USD' ? 'symbol' : 'code',
|
|
35
|
+
minimumFractionDigits: minorUnitDigits,
|
|
36
|
+
maximumFractionDigits: minorUnitDigits,
|
|
22
37
|
}).format(absDollars);
|
|
23
38
|
}
|
|
24
39
|
describe('CurrencyDisplay', () => {
|
|
40
|
+
it('matches the canonical digest of SIX List One 2026-01-01', () => {
|
|
41
|
+
expect(ISO_4217_MINOR_UNITS.size).toBe(178);
|
|
42
|
+
expect(metadataDigest(ISO_4217_MINOR_UNITS)).toBe('e1a3c502511fa784b38dd7ac2b4056d00f3f1a9f5781df93b0f2352f8eedc976');
|
|
43
|
+
});
|
|
25
44
|
it('formats cents into dollars by default', () => {
|
|
26
45
|
render(CurrencyDisplay, { props: { amount: 12345 } }); // cents → $123.45
|
|
27
46
|
expect(screen.getByText(money(123.45))).toBeInTheDocument();
|
|
@@ -44,6 +63,142 @@ describe('CurrencyDisplay', () => {
|
|
|
44
63
|
});
|
|
45
64
|
expect(screen.getByText(money(1000, 'USD'))).toBeInTheDocument();
|
|
46
65
|
});
|
|
66
|
+
it('formats an EUR amount through the public string currency prop', () => {
|
|
67
|
+
const commerceCurrency = 'EUR';
|
|
68
|
+
const { container } = render(CurrencyDisplay, {
|
|
69
|
+
props: { amount: 12345, currency: commerceCurrency },
|
|
70
|
+
});
|
|
71
|
+
expect(container.querySelector('span')?.textContent).toBe(money(123.45, 'EUR'));
|
|
72
|
+
});
|
|
73
|
+
it.each([
|
|
74
|
+
['JPY', 12345, '12,345'],
|
|
75
|
+
['BHD', 12345, '12.345'],
|
|
76
|
+
['IQD', 12345, '12.345'],
|
|
77
|
+
])('uses the ISO minor-unit scale for %s', (currency, amount, expected) => {
|
|
78
|
+
const { container } = render(CurrencyDisplay, {
|
|
79
|
+
props: { amount, currency },
|
|
80
|
+
});
|
|
81
|
+
expect(container.querySelector('span')?.textContent).toContain(expected);
|
|
82
|
+
});
|
|
83
|
+
it.each([
|
|
84
|
+
['IQD', 3, '9,007,199,254,740.991'],
|
|
85
|
+
['AFN', 2, '90,071,992,547,409.91'],
|
|
86
|
+
['CLF', 4, '900,719,925,474.0991'],
|
|
87
|
+
])('preserves the least-significant minor unit for large safe %s values', (currency, digits, expected) => {
|
|
88
|
+
const { container } = render(CurrencyDisplay, {
|
|
89
|
+
props: { amount: Number.MAX_SAFE_INTEGER, currency },
|
|
90
|
+
});
|
|
91
|
+
const text = container.querySelector('span')?.textContent;
|
|
92
|
+
expect(text).toContain(expected);
|
|
93
|
+
expect(text?.split('.').at(-1)).toHaveLength(digits);
|
|
94
|
+
});
|
|
95
|
+
it.each([
|
|
96
|
+
['positive fractional', 1.5],
|
|
97
|
+
['negative fractional', -1.5],
|
|
98
|
+
['unsafe positive integer', Number.MAX_SAFE_INTEGER + 1],
|
|
99
|
+
['unsafe negative integer', -(Number.MAX_SAFE_INTEGER + 1)],
|
|
100
|
+
['NaN', Number.NaN],
|
|
101
|
+
['positive infinity', Number.POSITIVE_INFINITY],
|
|
102
|
+
['negative infinity', Number.NEGATIVE_INFINITY],
|
|
103
|
+
])('rejects a %s amount in minor-unit mode', (_scenario, amount) => {
|
|
104
|
+
const { container } = render(CurrencyDisplay, {
|
|
105
|
+
props: { amount, currency: 'CAD' },
|
|
106
|
+
});
|
|
107
|
+
const display = container.querySelector('.currency-display');
|
|
108
|
+
expect(display).toHaveClass('invalid');
|
|
109
|
+
expect(display).toHaveTextContent('Invalid minor-unit amount');
|
|
110
|
+
});
|
|
111
|
+
it('preserves a negative safe integer through the exact string formatter path', () => {
|
|
112
|
+
const { container } = render(CurrencyDisplay, {
|
|
113
|
+
props: { amount: -Number.MAX_SAFE_INTEGER, currency: 'IQD' },
|
|
114
|
+
});
|
|
115
|
+
const text = container.querySelector('span')?.textContent;
|
|
116
|
+
expect(text).toContain('9,007,199,254,740.991');
|
|
117
|
+
expect(text?.startsWith('-IQD')).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it.each([
|
|
120
|
+
['cad', 'CAD'],
|
|
121
|
+
[' eur ', 'EUR'],
|
|
122
|
+
['ved', 'VED'],
|
|
123
|
+
['xad', 'XAD'],
|
|
124
|
+
])('normalizes the currency code %j to %s', (currency, normalized) => {
|
|
125
|
+
const { container } = render(CurrencyDisplay, {
|
|
126
|
+
props: { amount: 12345, currency },
|
|
127
|
+
});
|
|
128
|
+
expect(container.querySelector('span')?.textContent).toBe(money(123.45, normalized, 2));
|
|
129
|
+
});
|
|
130
|
+
it.each([
|
|
131
|
+
'US',
|
|
132
|
+
'AAA',
|
|
133
|
+
'ANG',
|
|
134
|
+
'BGN',
|
|
135
|
+
'CUC',
|
|
136
|
+
'HRK',
|
|
137
|
+
'SLL',
|
|
138
|
+
'ZWL',
|
|
139
|
+
'ZZZ',
|
|
140
|
+
'',
|
|
141
|
+
])('renders invalid code %j without throwing', (currency) => {
|
|
142
|
+
const { container } = render(CurrencyDisplay, {
|
|
143
|
+
props: { amount: 12345, currency },
|
|
144
|
+
});
|
|
145
|
+
const normalized = currency
|
|
146
|
+
.trim()
|
|
147
|
+
.replace(/[a-z]/g, (character) => character.toUpperCase()) || '(empty)';
|
|
148
|
+
const display = container.querySelector('.currency-display');
|
|
149
|
+
expect(display).toHaveClass('invalid');
|
|
150
|
+
expect(display).toHaveTextContent(`Invalid currency code: ${normalized}`);
|
|
151
|
+
});
|
|
152
|
+
it.each([
|
|
153
|
+
['uſd', 'U\\u{17F}D'],
|
|
154
|
+
['ıqd', '\\u{131}QD'],
|
|
155
|
+
['ßp', '\\u{DF}P'],
|
|
156
|
+
['U\u200bSD', 'U\\u{200B}SD'],
|
|
157
|
+
['USD\u202e', 'USD\\u{202E}'],
|
|
158
|
+
['U\u2066SD', 'U\\u{2066}SD'],
|
|
159
|
+
])('renders rejected non-ASCII input %j visibly as %s', (currency, diagnostic) => {
|
|
160
|
+
const { container } = render(CurrencyDisplay, {
|
|
161
|
+
props: { amount: 12345, currency },
|
|
162
|
+
});
|
|
163
|
+
expect(container.querySelector('.currency-display')).toHaveTextContent(`Invalid currency code: ${diagnostic}`);
|
|
164
|
+
});
|
|
165
|
+
it('bounds escaped astral input without splitting a code-point token', () => {
|
|
166
|
+
const { container } = render(CurrencyDisplay, {
|
|
167
|
+
props: { amount: 12345, currency: '😀😀' },
|
|
168
|
+
});
|
|
169
|
+
expect(container.querySelector('.currency-display')).toHaveTextContent('Invalid currency code: \\u{1F600}…');
|
|
170
|
+
});
|
|
171
|
+
it('bounds malformed currency text so one row cannot force table overflow', () => {
|
|
172
|
+
const { container } = render(CurrencyDisplay, {
|
|
173
|
+
props: { amount: 12345, currency: 'invalid-currency-code' },
|
|
174
|
+
});
|
|
175
|
+
expect(container.querySelector('.currency-display')).toHaveTextContent('Invalid currency code: INVALID-CURR…');
|
|
176
|
+
});
|
|
177
|
+
it('rejects a non-string currency from an untyped runtime caller without throwing', () => {
|
|
178
|
+
const { container } = render(CurrencyDisplay, {
|
|
179
|
+
// @ts-expect-error JavaScript callers can pass values outside the public type.
|
|
180
|
+
props: { amount: 12345, currency: null },
|
|
181
|
+
});
|
|
182
|
+
const display = container.querySelector('.currency-display');
|
|
183
|
+
expect(display).toHaveClass('invalid');
|
|
184
|
+
expect(display).toHaveTextContent('Invalid currency code: (non-string)');
|
|
185
|
+
});
|
|
186
|
+
it('requires major-unit input for ISO codes without a minor unit', async () => {
|
|
187
|
+
const { rerender } = render(CurrencyDisplay, {
|
|
188
|
+
props: { amount: 12.5, currency: 'XAU' },
|
|
189
|
+
});
|
|
190
|
+
const display = document.querySelector('.currency-display');
|
|
191
|
+
expect(display).toHaveClass('invalid');
|
|
192
|
+
expect(display).toHaveTextContent('Currency code has no minor unit: XAU');
|
|
193
|
+
await rerender({ amount: 12.5, currency: 'XAU', unit: 'dollars' });
|
|
194
|
+
expect(document.querySelector('.currency-display')?.textContent).toBe(money(12.5, 'XAU', 2));
|
|
195
|
+
});
|
|
196
|
+
it('resolves invalid-code prose through the active i18n snapshot', () => {
|
|
197
|
+
render(CurrencyDisplayI18nHarness);
|
|
198
|
+
expect(screen.getByText('Code monétaire invalide : ZZZ')).toBeVisible();
|
|
199
|
+
expect(screen.getByText('Devise sans unité mineure : XAU')).toBeVisible();
|
|
200
|
+
expect(screen.getByText('Montant en unité mineure invalide')).toBeVisible();
|
|
201
|
+
});
|
|
47
202
|
it('shows an explicit + sign for positive amounts when showSign is set', () => {
|
|
48
203
|
render(CurrencyDisplay, {
|
|
49
204
|
props: { amount: 1000, unit: 'dollars', showSign: true },
|
|
@@ -111,4 +266,39 @@ describe('CurrencyDisplay', () => {
|
|
|
111
266
|
});
|
|
112
267
|
await expectNoA11yViolations(container);
|
|
113
268
|
});
|
|
269
|
+
it('is axe-clean for an invalid currency code', async () => {
|
|
270
|
+
const { container } = render(CurrencyDisplay, {
|
|
271
|
+
props: { amount: 12345, currency: 'ZZZ' },
|
|
272
|
+
});
|
|
273
|
+
await expectNoA11yViolations(container);
|
|
274
|
+
});
|
|
275
|
+
it('renders and hydrates valid and invalid currencies safely', async () => {
|
|
276
|
+
const vite = await createServer({
|
|
277
|
+
appType: 'custom',
|
|
278
|
+
configFile: false,
|
|
279
|
+
plugins: [svelte()],
|
|
280
|
+
root: process.cwd(),
|
|
281
|
+
server: { middlewareMode: true },
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
const { default: SsrHarness } = await vite.ssrLoadModule('/src/components/display/__tests__/CurrencyDisplaySsrHarness.svelte');
|
|
285
|
+
const { render: renderSsr } = await vite.ssrLoadModule('svelte/server');
|
|
286
|
+
const result = renderSsr(SsrHarness);
|
|
287
|
+
expect(result.body).toContain(money(123.45, 'EUR'));
|
|
288
|
+
expect(result.body).toContain('Invalid currency code: ZZZ');
|
|
289
|
+
expect(result.body).toContain('Currency code has no minor unit: XAU');
|
|
290
|
+
const host = document.createElement('div');
|
|
291
|
+
host.innerHTML = result.body;
|
|
292
|
+
document.body.append(host);
|
|
293
|
+
const instance = hydrate(CurrencyDisplaySsrHarness, { target: host });
|
|
294
|
+
expect(host.textContent).toContain(money(123.45, 'EUR'));
|
|
295
|
+
expect(host.textContent).toContain('Invalid currency code: ZZZ');
|
|
296
|
+
expect(host.textContent).toContain('Currency code has no minor unit: XAU');
|
|
297
|
+
await unmount(instance);
|
|
298
|
+
host.remove();
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await vite.close();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
114
304
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createI18nContext, setI18nContext } from '../../../i18n/index.js';
|
|
3
|
+
import CurrencyDisplay from '../CurrencyDisplay.svelte';
|
|
4
|
+
|
|
5
|
+
setI18nContext(
|
|
6
|
+
createI18nContext({
|
|
7
|
+
locale: 'fr',
|
|
8
|
+
messages: {
|
|
9
|
+
'ui.currency_display.invalid_code': 'Code monétaire invalide : {code}',
|
|
10
|
+
'ui.currency_display.invalid_minor_unit_amount':
|
|
11
|
+
'Montant en unité mineure invalide',
|
|
12
|
+
'ui.currency_display.no_minor_unit': 'Devise sans unité mineure : {code}',
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<CurrencyDisplay amount={12345} currency="ZZZ" />
|
|
19
|
+
<CurrencyDisplay amount={12345} currency="XAU" />
|
|
20
|
+
<CurrencyDisplay amount={1.5} currency="CAD" />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const CurrencyDisplayI18nHarness: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type CurrencyDisplayI18nHarness = InstanceType<typeof CurrencyDisplayI18nHarness>;
|
|
18
|
+
export default CurrencyDisplayI18nHarness;
|
|
19
|
+
//# sourceMappingURL=CurrencyDisplayI18nHarness.svelte.d.ts.map
|