@vaam-apps/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/components/data/code.d.ts +37 -0
- package/dist/components/data/code.d.ts.map +1 -0
- package/dist/components/data/code.js +37 -0
- package/dist/components/data/code.js.map +1 -0
- package/dist/components/data/copy-button.d.ts +39 -0
- package/dist/components/data/copy-button.d.ts.map +1 -0
- package/dist/components/data/copy-button.js +50 -0
- package/dist/components/data/copy-button.js.map +1 -0
- package/dist/components/data/detail-row.d.ts +69 -0
- package/dist/components/data/detail-row.d.ts.map +1 -0
- package/dist/components/data/detail-row.js +27 -0
- package/dist/components/data/detail-row.js.map +1 -0
- package/dist/components/data/id-display.d.ts +33 -0
- package/dist/components/data/id-display.d.ts.map +1 -0
- package/dist/components/data/id-display.js +25 -0
- package/dist/components/data/id-display.js.map +1 -0
- package/dist/components/data/masked-value.d.ts +47 -0
- package/dist/components/data/masked-value.d.ts.map +1 -0
- package/dist/components/data/masked-value.js +46 -0
- package/dist/components/data/masked-value.js.map +1 -0
- package/dist/components/data/money.d.ts +28 -0
- package/dist/components/data/money.d.ts.map +1 -0
- package/dist/components/data/money.js +22 -0
- package/dist/components/data/money.js.map +1 -0
- package/dist/components/data/phone-display.d.ts +45 -0
- package/dist/components/data/phone-display.d.ts.map +1 -0
- package/dist/components/data/phone-display.js +23 -0
- package/dist/components/data/phone-display.js.map +1 -0
- package/dist/components/data/stat-tile.d.ts +33 -0
- package/dist/components/data/stat-tile.d.ts.map +1 -0
- package/dist/components/data/stat-tile.js +18 -0
- package/dist/components/data/stat-tile.js.map +1 -0
- package/dist/components/data/timestamp-display.d.ts +14 -0
- package/dist/components/data/timestamp-display.d.ts.map +1 -0
- package/dist/components/data/timestamp-display.js +77 -0
- package/dist/components/data/timestamp-display.js.map +1 -0
- package/dist/components/patterns/inline-banner.d.ts +22 -0
- package/dist/components/patterns/inline-banner.d.ts.map +1 -0
- package/dist/components/patterns/inline-banner.js +11 -0
- package/dist/components/patterns/inline-banner.js.map +1 -0
- package/dist/components/patterns/inline-empty-state.d.ts +20 -0
- package/dist/components/patterns/inline-empty-state.d.ts.map +1 -0
- package/dist/components/patterns/inline-empty-state.js +11 -0
- package/dist/components/patterns/inline-empty-state.js.map +1 -0
- package/dist/components/patterns/live-row.d.ts +23 -0
- package/dist/components/patterns/live-row.d.ts.map +1 -0
- package/dist/components/patterns/live-row.js +48 -0
- package/dist/components/patterns/live-row.js.map +1 -0
- package/dist/components/patterns/payload-inspector.d.ts +25 -0
- package/dist/components/patterns/payload-inspector.d.ts.map +1 -0
- package/dist/components/patterns/payload-inspector.js +33 -0
- package/dist/components/patterns/payload-inspector.js.map +1 -0
- package/dist/components/patterns/route-skeleton.d.ts +15 -0
- package/dist/components/patterns/route-skeleton.d.ts.map +1 -0
- package/dist/components/patterns/route-skeleton.js +7 -0
- package/dist/components/patterns/route-skeleton.js.map +1 -0
- package/dist/components/patterns/screen-layout.d.ts +14 -0
- package/dist/components/patterns/screen-layout.d.ts.map +1 -0
- package/dist/components/patterns/screen-layout.js +11 -0
- package/dist/components/patterns/screen-layout.js.map +1 -0
- package/dist/components/patterns/stale-write-banner.d.ts +26 -0
- package/dist/components/patterns/stale-write-banner.d.ts.map +1 -0
- package/dist/components/patterns/stale-write-banner.js +28 -0
- package/dist/components/patterns/stale-write-banner.js.map +1 -0
- package/dist/components/patterns/state-timeline.d.ts +58 -0
- package/dist/components/patterns/state-timeline.d.ts.map +1 -0
- package/dist/components/patterns/state-timeline.js +103 -0
- package/dist/components/patterns/state-timeline.js.map +1 -0
- package/dist/components/primitives/badge.d.ts +27 -0
- package/dist/components/primitives/badge.d.ts.map +1 -0
- package/dist/components/primitives/badge.js +33 -0
- package/dist/components/primitives/badge.js.map +1 -0
- package/dist/components/primitives/button.d.ts +12 -0
- package/dist/components/primitives/button.d.ts.map +1 -0
- package/dist/components/primitives/button.js +42 -0
- package/dist/components/primitives/button.js.map +1 -0
- package/dist/components/primitives/calendar.d.ts +39 -0
- package/dist/components/primitives/calendar.d.ts.map +1 -0
- package/dist/components/primitives/calendar.js +102 -0
- package/dist/components/primitives/calendar.js.map +1 -0
- package/dist/components/primitives/card.d.ts +24 -0
- package/dist/components/primitives/card.d.ts.map +1 -0
- package/dist/components/primitives/card.js +28 -0
- package/dist/components/primitives/card.js.map +1 -0
- package/dist/components/primitives/checkbox.d.ts +42 -0
- package/dist/components/primitives/checkbox.d.ts.map +1 -0
- package/dist/components/primitives/checkbox.js +31 -0
- package/dist/components/primitives/checkbox.js.map +1 -0
- package/dist/components/primitives/chip-select.d.ts +38 -0
- package/dist/components/primitives/chip-select.d.ts.map +1 -0
- package/dist/components/primitives/chip-select.js +25 -0
- package/dist/components/primitives/chip-select.js.map +1 -0
- package/dist/components/primitives/command-menu.d.ts +54 -0
- package/dist/components/primitives/command-menu.d.ts.map +1 -0
- package/dist/components/primitives/command-menu.js +24 -0
- package/dist/components/primitives/command-menu.js.map +1 -0
- package/dist/components/primitives/confirm-dialog.d.ts +52 -0
- package/dist/components/primitives/confirm-dialog.d.ts.map +1 -0
- package/dist/components/primitives/confirm-dialog.js +30 -0
- package/dist/components/primitives/confirm-dialog.js.map +1 -0
- package/dist/components/primitives/date-picker.d.ts +68 -0
- package/dist/components/primitives/date-picker.d.ts.map +1 -0
- package/dist/components/primitives/date-picker.js +107 -0
- package/dist/components/primitives/date-picker.js.map +1 -0
- package/dist/components/primitives/dialog.d.ts +50 -0
- package/dist/components/primitives/dialog.d.ts.map +1 -0
- package/dist/components/primitives/dialog.js +90 -0
- package/dist/components/primitives/dialog.js.map +1 -0
- package/dist/components/primitives/drawer.d.ts +71 -0
- package/dist/components/primitives/drawer.d.ts.map +1 -0
- package/dist/components/primitives/drawer.js +93 -0
- package/dist/components/primitives/drawer.js.map +1 -0
- package/dist/components/primitives/dropdown-menu.d.ts +40 -0
- package/dist/components/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/components/primitives/dropdown-menu.js +53 -0
- package/dist/components/primitives/dropdown-menu.js.map +1 -0
- package/dist/components/primitives/form-field.d.ts +94 -0
- package/dist/components/primitives/form-field.d.ts.map +1 -0
- package/dist/components/primitives/form-field.js +27 -0
- package/dist/components/primitives/form-field.js.map +1 -0
- package/dist/components/primitives/inline-confirm.d.ts +62 -0
- package/dist/components/primitives/inline-confirm.d.ts.map +1 -0
- package/dist/components/primitives/inline-confirm.js +11 -0
- package/dist/components/primitives/inline-confirm.js.map +1 -0
- package/dist/components/primitives/input.d.ts +4 -0
- package/dist/components/primitives/input.d.ts.map +1 -0
- package/dist/components/primitives/input.js +13 -0
- package/dist/components/primitives/input.js.map +1 -0
- package/dist/components/primitives/label.d.ts +26 -0
- package/dist/components/primitives/label.d.ts.map +1 -0
- package/dist/components/primitives/label.js +33 -0
- package/dist/components/primitives/label.js.map +1 -0
- package/dist/components/primitives/pagination.d.ts +54 -0
- package/dist/components/primitives/pagination.d.ts.map +1 -0
- package/dist/components/primitives/pagination.js +31 -0
- package/dist/components/primitives/pagination.js.map +1 -0
- package/dist/components/primitives/popover.d.ts +12 -0
- package/dist/components/primitives/popover.d.ts.map +1 -0
- package/dist/components/primitives/popover.js +22 -0
- package/dist/components/primitives/popover.js.map +1 -0
- package/dist/components/primitives/progress.d.ts +25 -0
- package/dist/components/primitives/progress.d.ts.map +1 -0
- package/dist/components/primitives/progress.js +25 -0
- package/dist/components/primitives/progress.js.map +1 -0
- package/dist/components/primitives/radio-group.d.ts +47 -0
- package/dist/components/primitives/radio-group.d.ts.map +1 -0
- package/dist/components/primitives/radio-group.js +11 -0
- package/dist/components/primitives/radio-group.js.map +1 -0
- package/dist/components/primitives/select.d.ts +39 -0
- package/dist/components/primitives/select.d.ts.map +1 -0
- package/dist/components/primitives/select.js +142 -0
- package/dist/components/primitives/select.js.map +1 -0
- package/dist/components/primitives/separator.d.ts +9 -0
- package/dist/components/primitives/separator.d.ts.map +1 -0
- package/dist/components/primitives/separator.js +16 -0
- package/dist/components/primitives/separator.js.map +1 -0
- package/dist/components/primitives/side-nav.d.ts +65 -0
- package/dist/components/primitives/side-nav.d.ts.map +1 -0
- package/dist/components/primitives/side-nav.js +100 -0
- package/dist/components/primitives/side-nav.js.map +1 -0
- package/dist/components/primitives/skeleton.d.ts +22 -0
- package/dist/components/primitives/skeleton.d.ts.map +1 -0
- package/dist/components/primitives/skeleton.js +25 -0
- package/dist/components/primitives/skeleton.js.map +1 -0
- package/dist/components/primitives/spinner.d.ts +20 -0
- package/dist/components/primitives/spinner.d.ts.map +1 -0
- package/dist/components/primitives/spinner.js +21 -0
- package/dist/components/primitives/spinner.js.map +1 -0
- package/dist/components/primitives/switch.d.ts +29 -0
- package/dist/components/primitives/switch.d.ts.map +1 -0
- package/dist/components/primitives/switch.js +24 -0
- package/dist/components/primitives/switch.js.map +1 -0
- package/dist/components/primitives/table.d.ts +23 -0
- package/dist/components/primitives/table.d.ts.map +1 -0
- package/dist/components/primitives/table.js +81 -0
- package/dist/components/primitives/table.js.map +1 -0
- package/dist/components/primitives/tabs.d.ts +38 -0
- package/dist/components/primitives/tabs.d.ts.map +1 -0
- package/dist/components/primitives/tabs.js +84 -0
- package/dist/components/primitives/tabs.js.map +1 -0
- package/dist/components/primitives/textarea.d.ts +4 -0
- package/dist/components/primitives/textarea.d.ts.map +1 -0
- package/dist/components/primitives/textarea.js +11 -0
- package/dist/components/primitives/textarea.js.map +1 -0
- package/dist/components/primitives/toast.d.ts +24 -0
- package/dist/components/primitives/toast.d.ts.map +1 -0
- package/dist/components/primitives/toast.js +58 -0
- package/dist/components/primitives/toast.js.map +1 -0
- package/dist/components/primitives/tooltip.d.ts +30 -0
- package/dist/components/primitives/tooltip.d.ts.map +1 -0
- package/dist/components/primitives/tooltip.js +6 -0
- package/dist/components/primitives/tooltip.js.map +1 -0
- package/dist/components/status/state-chip.d.ts +53 -0
- package/dist/components/status/state-chip.d.ts.map +1 -0
- package/dist/components/status/state-chip.js +8 -0
- package/dist/components/status/state-chip.js.map +1 -0
- package/dist/components/status/state-mark.d.ts +21 -0
- package/dist/components/status/state-mark.d.ts.map +1 -0
- package/dist/components/status/state-mark.js +90 -0
- package/dist/components/status/state-mark.js.map +1 -0
- package/dist/components/status/status-pill.d.ts +62 -0
- package/dist/components/status/status-pill.d.ts.map +1 -0
- package/dist/components/status/status-pill.js +65 -0
- package/dist/components/status/status-pill.js.map +1 -0
- package/dist/components/status/status-tokens.d.ts +110 -0
- package/dist/components/status/status-tokens.d.ts.map +1 -0
- package/dist/components/status/status-tokens.js +102 -0
- package/dist/components/status/status-tokens.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +11 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +78 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/money.d.ts +73 -0
- package/dist/lib/money.d.ts.map +1 -0
- package/dist/lib/money.js +123 -0
- package/dist/lib/money.js.map +1 -0
- package/dist/lib/omit-undefined.d.ts +17 -0
- package/dist/lib/omit-undefined.d.ts.map +1 -0
- package/dist/lib/omit-undefined.js +24 -0
- package/dist/lib/omit-undefined.js.map +1 -0
- package/package.json +100 -0
- package/src/components/data/code.tsx +43 -0
- package/src/components/data/copy-button.tsx +97 -0
- package/src/components/data/detail-row.tsx +113 -0
- package/src/components/data/id-display.tsx +49 -0
- package/src/components/data/masked-value.tsx +96 -0
- package/src/components/data/money.tsx +46 -0
- package/src/components/data/phone-display.tsx +60 -0
- package/src/components/data/stat-tile.tsx +68 -0
- package/src/components/data/timestamp-display.tsx +106 -0
- package/src/components/patterns/inline-banner.tsx +60 -0
- package/src/components/patterns/inline-empty-state.tsx +45 -0
- package/src/components/patterns/live-row.tsx +75 -0
- package/src/components/patterns/payload-inspector.tsx +157 -0
- package/src/components/patterns/route-skeleton.tsx +77 -0
- package/src/components/patterns/screen-layout.tsx +44 -0
- package/src/components/patterns/stale-write-banner.tsx +44 -0
- package/src/components/patterns/state-timeline.tsx +229 -0
- package/src/components/primitives/badge.tsx +40 -0
- package/src/components/primitives/button.tsx +54 -0
- package/src/components/primitives/calendar.tsx +136 -0
- package/src/components/primitives/card.tsx +70 -0
- package/src/components/primitives/checkbox.tsx +101 -0
- package/src/components/primitives/chip-select.tsx +105 -0
- package/src/components/primitives/command-menu.tsx +92 -0
- package/src/components/primitives/confirm-dialog.tsx +109 -0
- package/src/components/primitives/date-picker.tsx +271 -0
- package/src/components/primitives/dialog.tsx +195 -0
- package/src/components/primitives/drawer.tsx +326 -0
- package/src/components/primitives/dropdown-menu.tsx +128 -0
- package/src/components/primitives/form-field.tsx +136 -0
- package/src/components/primitives/inline-confirm.tsx +118 -0
- package/src/components/primitives/input.tsx +21 -0
- package/src/components/primitives/label.tsx +43 -0
- package/src/components/primitives/pagination.tsx +110 -0
- package/src/components/primitives/popover.tsx +41 -0
- package/src/components/primitives/progress.tsx +66 -0
- package/src/components/primitives/radio-group.tsx +90 -0
- package/src/components/primitives/select.tsx +254 -0
- package/src/components/primitives/separator.tsx +49 -0
- package/src/components/primitives/side-nav.tsx +253 -0
- package/src/components/primitives/skeleton.tsx +25 -0
- package/src/components/primitives/spinner.tsx +39 -0
- package/src/components/primitives/switch.tsx +74 -0
- package/src/components/primitives/table.tsx +157 -0
- package/src/components/primitives/tabs.tsx +163 -0
- package/src/components/primitives/textarea.tsx +21 -0
- package/src/components/primitives/toast.tsx +117 -0
- package/src/components/primitives/tooltip.tsx +38 -0
- package/src/components/status/state-chip.tsx +70 -0
- package/src/components/status/state-mark.tsx +154 -0
- package/src/components/status/status-pill.tsx +153 -0
- package/src/components/status/status-tokens.ts +173 -0
- package/src/index.ts +96 -0
- package/src/lib/cn.ts +168 -0
- package/src/lib/money.ts +164 -0
- package/src/lib/omit-undefined.ts +25 -0
- package/src/styles/theme.css +381 -0
package/src/lib/cn.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `tailwind-merge` taught about (a) this design system's own theme scales
|
|
6
|
+
* and (b) daisyUI's component modifiers.
|
|
7
|
+
*
|
|
8
|
+
* Stock `twMerge` only knows Tailwind's *default* theme and Tailwind's own
|
|
9
|
+
* utilities. Both gaps produce the same failure shape — a class that still
|
|
10
|
+
* parses, still looks plausible in the source, and quietly does nothing —
|
|
11
|
+
* and both were found by probing real class strings from this package
|
|
12
|
+
* rather than by reading the docs:
|
|
13
|
+
*
|
|
14
|
+
* **1. Custom font sizes were being deleted.** `--text-caption` &c. are
|
|
15
|
+
* theme keys stock `twMerge` has never heard of, so it classified
|
|
16
|
+
* `text-caption` by its fallback rule — as a *colour* — and then treated
|
|
17
|
+
* it as conflicting with the actual colour beside it:
|
|
18
|
+
*
|
|
19
|
+
* ```text
|
|
20
|
+
* twMerge("text-caption text-state-danger-fg") // → "text-state-danger-fg"
|
|
21
|
+
* twMerge("text-body text-foreground") // → "text-foreground"
|
|
22
|
+
* twMerge("text-title-sm text-foreground") // → "text-foreground"
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Those three strings are, verbatim, `FieldError`, `DetailList` and
|
|
26
|
+
* `CardHeader`. Every one of them has been rendering at the browser's
|
|
27
|
+
* default font size rather than its intended step, in both this package
|
|
28
|
+
* and any app consuming it, since `cn()` was written. Registering the
|
|
29
|
+
* scale under the `text` theme key is what separates the two groups again.
|
|
30
|
+
* This predates the tailwind-merge 3.x upgrade — v2.6.0 was checked
|
|
31
|
+
* directly and drops the same classes — so it is a latent bug being fixed
|
|
32
|
+
* here, not a regression being papered over.
|
|
33
|
+
*
|
|
34
|
+
* **2. Radius utilities did not conflict at all.** `rounded-field` /
|
|
35
|
+
* `rounded-box` / `rounded-selector` are daisyUI's own radius tiers, and
|
|
36
|
+
* stock `twMerge` left `rounded-sm rounded-field` as *both* classes, so
|
|
37
|
+
* the winner fell out of stylesheet order rather than call order — the
|
|
38
|
+
* opposite of what an override prop is for.
|
|
39
|
+
*
|
|
40
|
+
* **3. daisyUI component modifiers did not conflict either**, so
|
|
41
|
+
* `cn("btn btn-primary", "btn-ghost")` kept both. The group list below is
|
|
42
|
+
* adapted from the sibling `@vpay/ui` package, which had already derived
|
|
43
|
+
* it against daisyUI 5's compiled CSS; its central finding is preserved
|
|
44
|
+
* and worth restating, because it is counter-intuitive:
|
|
45
|
+
*
|
|
46
|
+
* > **Colour and style are two groups, not one, for `btn` and `badge`.**
|
|
47
|
+
* > `.btn-outline`/`.btn-dash` *read* the `--btn-color` variable that
|
|
48
|
+
* > `.btn-primary` sets, so `btn-primary btn-outline` is how daisyUI 5
|
|
49
|
+
* > spells "a primary outline button". Collapsed into one group,
|
|
50
|
+
* > `cn("btn-primary", "btn-outline")` returns `btn-outline` and the
|
|
51
|
+
* > colour silently vanishes.
|
|
52
|
+
*
|
|
53
|
+
* `ghost` and `link` stay with the *colours* rather than the styles,
|
|
54
|
+
* matching `@vpay/ui` and this package's own `buttonVariants`, where
|
|
55
|
+
* `ghost` is offered as an alternative to `primary` rather than a
|
|
56
|
+
* modifier on top of it.
|
|
57
|
+
*
|
|
58
|
+
* Scoped to what this package actually emits plus the daisyUI components a
|
|
59
|
+
* consumer is most likely to reach for directly. Add a group when a
|
|
60
|
+
* component gains a variant dimension, not ahead of one.
|
|
61
|
+
*/
|
|
62
|
+
type ExtraClassGroupId =
|
|
63
|
+
| "daisy-btn-variant"
|
|
64
|
+
| "daisy-btn-style"
|
|
65
|
+
| "daisy-btn-size"
|
|
66
|
+
| "daisy-badge-variant"
|
|
67
|
+
| "daisy-badge-style"
|
|
68
|
+
| "daisy-badge-size"
|
|
69
|
+
| "daisy-alert-variant"
|
|
70
|
+
| "daisy-alert-style"
|
|
71
|
+
| "daisy-input-variant"
|
|
72
|
+
| "daisy-select-variant"
|
|
73
|
+
| "daisy-select-size"
|
|
74
|
+
| "daisy-checkbox-variant"
|
|
75
|
+
| "daisy-checkbox-size"
|
|
76
|
+
| "daisy-radio-variant"
|
|
77
|
+
| "daisy-radio-size"
|
|
78
|
+
| "daisy-toggle-variant"
|
|
79
|
+
| "daisy-toggle-size"
|
|
80
|
+
| "daisy-card-size"
|
|
81
|
+
| "daisy-table-size"
|
|
82
|
+
| "daisy-tabs-style"
|
|
83
|
+
| "daisy-tabs-size"
|
|
84
|
+
| "daisy-progress-variant"
|
|
85
|
+
| "daisy-loading-type"
|
|
86
|
+
| "daisy-loading-size";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `--text-*` steps declared in `styles/theme.css`. Kept in sync with
|
|
90
|
+
* that file by `theme-tokens.test.ts`, which parses the stylesheet rather
|
|
91
|
+
* than trusting this list.
|
|
92
|
+
*/
|
|
93
|
+
const FONT_SIZES = [
|
|
94
|
+
"micro",
|
|
95
|
+
"caption",
|
|
96
|
+
"body",
|
|
97
|
+
"prose",
|
|
98
|
+
"title-sm",
|
|
99
|
+
"title",
|
|
100
|
+
"metric",
|
|
101
|
+
"metric-lg",
|
|
102
|
+
] as const;
|
|
103
|
+
|
|
104
|
+
/** daisyUI's radius tiers, which its own components read from. */
|
|
105
|
+
const RADII = ["selector", "field", "box"] as const;
|
|
106
|
+
|
|
107
|
+
const DAISY_COLOURS = [
|
|
108
|
+
"primary",
|
|
109
|
+
"secondary",
|
|
110
|
+
"accent",
|
|
111
|
+
"neutral",
|
|
112
|
+
"info",
|
|
113
|
+
"success",
|
|
114
|
+
"warning",
|
|
115
|
+
"error",
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
const DAISY_SIZES = ["xs", "sm", "md", "lg", "xl"] as const;
|
|
119
|
+
|
|
120
|
+
const twMerge = extendTailwindMerge<ExtraClassGroupId>({
|
|
121
|
+
extend: {
|
|
122
|
+
theme: {
|
|
123
|
+
// Registering these as *font sizes* is what stops `text-caption`
|
|
124
|
+
// being mistaken for a colour and dropped. See point 1 above.
|
|
125
|
+
text: [...FONT_SIZES],
|
|
126
|
+
radius: [...RADII],
|
|
127
|
+
},
|
|
128
|
+
classGroups: {
|
|
129
|
+
"daisy-btn-variant": [{ btn: [...DAISY_COLOURS, "ghost", "link"] }],
|
|
130
|
+
"daisy-btn-style": [{ btn: ["outline", "soft", "dash"] }],
|
|
131
|
+
"daisy-btn-size": [{ btn: [...DAISY_SIZES, "wide", "block", "square", "circle"] }],
|
|
132
|
+
"daisy-badge-variant": [{ badge: [...DAISY_COLOURS, "ghost"] }],
|
|
133
|
+
"daisy-badge-style": [{ badge: ["outline", "soft", "dash"] }],
|
|
134
|
+
"daisy-badge-size": [{ badge: [...DAISY_SIZES] }],
|
|
135
|
+
"daisy-alert-variant": [{ alert: ["info", "success", "warning", "error"] }],
|
|
136
|
+
"daisy-alert-style": [{ alert: ["outline", "soft", "dash"] }],
|
|
137
|
+
"daisy-input-variant": [{ input: [...DAISY_COLOURS, "ghost"] }],
|
|
138
|
+
"daisy-select-variant": [{ select: [...DAISY_COLOURS, "ghost"] }],
|
|
139
|
+
"daisy-select-size": [{ select: [...DAISY_SIZES] }],
|
|
140
|
+
"daisy-checkbox-variant": [{ checkbox: [...DAISY_COLOURS] }],
|
|
141
|
+
"daisy-checkbox-size": [{ checkbox: [...DAISY_SIZES] }],
|
|
142
|
+
"daisy-radio-variant": [{ radio: [...DAISY_COLOURS] }],
|
|
143
|
+
"daisy-radio-size": [{ radio: [...DAISY_SIZES] }],
|
|
144
|
+
"daisy-toggle-variant": [{ toggle: [...DAISY_COLOURS] }],
|
|
145
|
+
"daisy-toggle-size": [{ toggle: [...DAISY_SIZES] }],
|
|
146
|
+
"daisy-card-size": [{ card: [...DAISY_SIZES] }],
|
|
147
|
+
"daisy-table-size": [{ table: [...DAISY_SIZES] }],
|
|
148
|
+
"daisy-tabs-style": [{ tabs: ["box", "border", "lift"] }],
|
|
149
|
+
"daisy-tabs-size": [{ tabs: [...DAISY_SIZES] }],
|
|
150
|
+
"daisy-progress-variant": [{ progress: [...DAISY_COLOURS] }],
|
|
151
|
+
"daisy-loading-type": [{ loading: ["spinner", "dots", "ring", "ball", "bars", "infinity"] }],
|
|
152
|
+
"daisy-loading-size": [{ loading: [...DAISY_SIZES] }],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Merge class names, resolving conflicting utilities so the *last* one
|
|
159
|
+
* wins — Tailwind's own, this design system's custom scales, and daisyUI's
|
|
160
|
+
* component modifiers alike.
|
|
161
|
+
*/
|
|
162
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
163
|
+
return twMerge(clsx(inputs));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The font-size steps this package registers with `tailwind-merge`. Exported
|
|
167
|
+
* for `theme-tokens.test.ts`, which checks them against `theme.css`. */
|
|
168
|
+
export const REGISTERED_FONT_SIZES: readonly string[] = FONT_SIZES;
|
package/src/lib/money.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money formatting from minor units, without losing precision.
|
|
3
|
+
*
|
|
4
|
+
* # Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Money is the single most duplicated formatter in every application that
|
|
7
|
+
* handles it. The sibling payments codebase this package was generalised
|
|
8
|
+
* for had **four** independent implementations at the time of writing, in
|
|
9
|
+
* four packages, with four different signatures and three different
|
|
10
|
+
* hard-coded tables of which currencies have no decimal places — and one
|
|
11
|
+
* of them divided by a power of ten in floating point, which silently
|
|
12
|
+
* corrupts any amount past 2^53 minor units. All four also got the
|
|
13
|
+
* three-decimal currencies wrong, because every hand-written table
|
|
14
|
+
* remembers `JPY` and `XAF` and forgets `KWD` and `BHD`.
|
|
15
|
+
*
|
|
16
|
+
* # Three decisions, each load-bearing
|
|
17
|
+
*
|
|
18
|
+
* **Minor units in, never a decimal number.** An amount is an integer
|
|
19
|
+
* count of the currency's smallest unit — that is how it is stored, how
|
|
20
|
+
* it is transmitted, and the only representation that cannot be wrong by
|
|
21
|
+
* a rounding error. `bigint` and a decimal-digit `string` are accepted
|
|
22
|
+
* alongside `number` so an `i64` from a Rust backend survives the trip;
|
|
23
|
+
* JSON gives it to you as a string precisely because `number` cannot hold
|
|
24
|
+
* it.
|
|
25
|
+
*
|
|
26
|
+
* **The exponent comes from `Intl`, not from a table here.** `Intl`
|
|
27
|
+
* already ships the ISO 4217 exponent for every currency, so there is no
|
|
28
|
+
* list to maintain and no currency this gets wrong. Verified rather than
|
|
29
|
+
* assumed: `XAF`/`XOF`/`JPY`/`CLP`/`VND` resolve to 0 and `KWD`/`BHD` to
|
|
30
|
+
* 3, which is exactly the set hand-written tables miss.
|
|
31
|
+
*
|
|
32
|
+
* **The scaling is string surgery, never division.** The minor-unit
|
|
33
|
+
* integer is split into whole and fractional parts by digit position and
|
|
34
|
+
* handed to `Intl.NumberFormat` as a string, which accepts arbitrary
|
|
35
|
+
* precision. No value is ever converted to a float, so an amount larger
|
|
36
|
+
* than `Number.MAX_SAFE_INTEGER` formats exactly.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** An integer count of a currency's smallest unit. */
|
|
40
|
+
export type MinorUnits = number | bigint | string;
|
|
41
|
+
|
|
42
|
+
export interface FormatMoneyOptions {
|
|
43
|
+
/** BCP 47 tag. Defaults to the runtime's own locale.
|
|
44
|
+
*
|
|
45
|
+
* Pass one explicitly anywhere the output must be stable — a test, a
|
|
46
|
+
* server-rendered page whose HTML must match the client's hydration, or
|
|
47
|
+
* a value a human will compare against a bank statement. */
|
|
48
|
+
locale?: string | undefined;
|
|
49
|
+
/** `symbol` → `FCFA 5,000`; `code` → `XAF 5,000`; `none` → `5,000`.
|
|
50
|
+
*
|
|
51
|
+
* `code` is the honest default for an operator console: currency
|
|
52
|
+
* symbols are ambiguous across locales ($ alone names at least a dozen
|
|
53
|
+
* currencies) and an operator reconciling against a ledger needs the
|
|
54
|
+
* ISO code. */
|
|
55
|
+
display?: "symbol" | "code" | "none";
|
|
56
|
+
/** Force a sign even when positive, e.g. for a ledger delta. */
|
|
57
|
+
signDisplay?: Intl.NumberFormatOptions["signDisplay"];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DIGITS_ONLY = /^-?\d+$/;
|
|
61
|
+
|
|
62
|
+
/** Cache: constructing an `Intl.NumberFormat` is the expensive part, and a
|
|
63
|
+
* table renders the same currency once per row. */
|
|
64
|
+
const formatters = new Map<string, Intl.NumberFormat>();
|
|
65
|
+
|
|
66
|
+
function formatter(key: string, options: Intl.NumberFormatOptions, locale?: string) {
|
|
67
|
+
const cacheKey = `${locale ?? ""}|${key}`;
|
|
68
|
+
let found = formatters.get(cacheKey);
|
|
69
|
+
if (found === undefined) {
|
|
70
|
+
found = new Intl.NumberFormat(locale, options);
|
|
71
|
+
formatters.set(cacheKey, found);
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How many decimal places this currency has, per ISO 4217, as `Intl`
|
|
78
|
+
* knows it. `2` for a currency `Intl` has never heard of — the majority
|
|
79
|
+
* case, and the same fallback `Intl` itself uses.
|
|
80
|
+
*/
|
|
81
|
+
export function currencyExponent(currency: string, locale?: string): number {
|
|
82
|
+
return (
|
|
83
|
+
formatter(`exp:${currency}`, { style: "currency", currency }, locale).resolvedOptions()
|
|
84
|
+
.maximumFractionDigits ?? 2
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shifts a minor-unit integer right by `exponent` places, as a string.
|
|
90
|
+
*
|
|
91
|
+
* `"500"`, 2 → `"5.00"`; `"5"`, 2 → `"0.05"`; `"-5"`, 2 → `"-0.05"`;
|
|
92
|
+
* `"5000"`, 0 → `"5000"`.
|
|
93
|
+
*/
|
|
94
|
+
function shiftDecimal(digits: string, exponent: number): string {
|
|
95
|
+
const negative = digits.startsWith("-");
|
|
96
|
+
const abs = negative ? digits.slice(1) : digits;
|
|
97
|
+
const sign = negative ? "-" : "";
|
|
98
|
+
if (exponent === 0) return sign + abs;
|
|
99
|
+
const padded = abs.padStart(exponent + 1, "0");
|
|
100
|
+
const whole = padded.slice(0, padded.length - exponent);
|
|
101
|
+
const fraction = padded.slice(padded.length - exponent);
|
|
102
|
+
return `${sign}${whole}.${fraction}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `formatMoney(500000, "XAF")` → `"XAF 500,000"`.
|
|
107
|
+
* `formatMoney("1250", "USD")` → `"USD 12.50"`.
|
|
108
|
+
*
|
|
109
|
+
* @throws if `minorUnits` is not an integer. A non-integer minor-unit
|
|
110
|
+
* amount is always a bug — usually a caller that already divided — and
|
|
111
|
+
* silently rounding it here would hide the real error at the point where
|
|
112
|
+
* it is least visible.
|
|
113
|
+
*/
|
|
114
|
+
export function formatMoney(
|
|
115
|
+
minorUnits: MinorUnits,
|
|
116
|
+
currency: string,
|
|
117
|
+
options: FormatMoneyOptions = {},
|
|
118
|
+
): string {
|
|
119
|
+
const { locale, display = "code", signDisplay } = options;
|
|
120
|
+
|
|
121
|
+
let digits: string;
|
|
122
|
+
if (typeof minorUnits === "bigint") {
|
|
123
|
+
digits = minorUnits.toString();
|
|
124
|
+
} else if (typeof minorUnits === "number") {
|
|
125
|
+
if (!Number.isInteger(minorUnits)) {
|
|
126
|
+
throw new TypeError(
|
|
127
|
+
`formatMoney expects an integer count of minor units, got ${minorUnits} (${currency}). ` +
|
|
128
|
+
"Pass 1250 for USD 12.50, not 12.5.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
digits = minorUnits.toFixed(0);
|
|
132
|
+
} else {
|
|
133
|
+
const trimmed = minorUnits.trim();
|
|
134
|
+
if (!DIGITS_ONLY.test(trimmed)) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
`formatMoney expects an integer count of minor units, got ${JSON.stringify(minorUnits)} (${currency}).`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
digits = trimmed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const exponent = currencyExponent(currency, locale);
|
|
143
|
+
const value = shiftDecimal(digits, exponent);
|
|
144
|
+
|
|
145
|
+
const intlOptions: Intl.NumberFormatOptions =
|
|
146
|
+
display === "none"
|
|
147
|
+
? {
|
|
148
|
+
style: "decimal",
|
|
149
|
+
minimumFractionDigits: exponent,
|
|
150
|
+
maximumFractionDigits: exponent,
|
|
151
|
+
}
|
|
152
|
+
: {
|
|
153
|
+
style: "currency",
|
|
154
|
+
currency,
|
|
155
|
+
currencyDisplay: display,
|
|
156
|
+
};
|
|
157
|
+
if (signDisplay !== undefined) intlOptions.signDisplay = signDisplay;
|
|
158
|
+
|
|
159
|
+
return formatter(
|
|
160
|
+
`fmt:${currency}:${display}:${signDisplay ?? ""}:${exponent}`,
|
|
161
|
+
intlOptions,
|
|
162
|
+
locale,
|
|
163
|
+
).format(value as unknown as number);
|
|
164
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips every key whose value is explicitly `undefined`, and narrows the
|
|
3
|
+
* return type to match: `exactOptionalPropertyTypes` (this workspace's own
|
|
4
|
+
* `tsconfig`) makes `{ disabled?: boolean }` reject a spread whose *type*
|
|
5
|
+
* is `{ disabled?: boolean | undefined }` even when the value at runtime is
|
|
6
|
+
* always defined for the keys that matter — every `React.*HTMLAttributes`
|
|
7
|
+
* type declares its optional fields as `T | undefined` explicitly, but
|
|
8
|
+
* Headless UI's own prop types (`ListboxProps`, `MenuItemProps`, `LabelProps`,
|
|
9
|
+
* …) declare theirs as plain `T`. Spreading `...props` from a native-element
|
|
10
|
+
* prop type onto a Headless UI component hits this every time (found
|
|
11
|
+
* porting `select.tsx`/`dropdown-menu.tsx`/`label.tsx` off Radix, D3) —
|
|
12
|
+
* this is the one general fix rather than a one-off cast at each call site.
|
|
13
|
+
*/
|
|
14
|
+
export function omitUndefined<T extends Record<string, unknown>>(
|
|
15
|
+
props: T,
|
|
16
|
+
): { [K in keyof T]: Exclude<T[K], undefined> } {
|
|
17
|
+
const result = {} as { [K in keyof T]: Exclude<T[K], undefined> };
|
|
18
|
+
for (const key of Object.keys(props) as (keyof T)[]) {
|
|
19
|
+
const value = props[key];
|
|
20
|
+
if (value !== undefined) {
|
|
21
|
+
result[key] = value as Exclude<T[keyof T], undefined>;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|