@franco_roura/module-01 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +64 -2
  4. package/dist/charts.d.ts +3 -0
  5. package/dist/charts.js +3 -0
  6. package/dist/components/alert-dialog.d.ts +19 -0
  7. package/dist/components/alert-dialog.js +101 -0
  8. package/dist/components/alert-dialog.js.map +1 -0
  9. package/dist/components/amount.d.ts +24 -0
  10. package/dist/components/amount.js +27 -0
  11. package/dist/components/amount.js.map +1 -0
  12. package/dist/components/appearance-pickers.d.ts +9 -0
  13. package/dist/components/appearance-pickers.js +65 -0
  14. package/dist/components/appearance-pickers.js.map +1 -0
  15. package/dist/components/badge.d.ts +10 -0
  16. package/dist/components/badge.js +28 -0
  17. package/dist/components/badge.js.map +1 -0
  18. package/dist/components/budget-meter.d.ts +18 -0
  19. package/dist/components/budget-meter.js +38 -0
  20. package/dist/components/budget-meter.js.map +1 -0
  21. package/dist/components/button.d.ts +13 -0
  22. package/dist/components/button.js +54 -0
  23. package/dist/components/button.js.map +1 -0
  24. package/dist/components/calendar.d.ts +7 -0
  25. package/dist/components/calendar.js +52 -0
  26. package/dist/components/calendar.js.map +1 -0
  27. package/dist/components/category-icon.d.ts +33 -0
  28. package/dist/components/category-icon.js +49 -0
  29. package/dist/components/category-icon.js.map +1 -0
  30. package/dist/components/chart.d.ts +44 -0
  31. package/dist/components/chart.js +147 -0
  32. package/dist/components/chart.js.map +1 -0
  33. package/dist/components/charts.d.ts +52 -0
  34. package/dist/components/charts.js +482 -0
  35. package/dist/components/charts.js.map +1 -0
  36. package/dist/components/checkbox.d.ts +5 -0
  37. package/dist/components/checkbox.js +21 -0
  38. package/dist/components/checkbox.js.map +1 -0
  39. package/dist/components/combobox.d.ts +32 -0
  40. package/dist/components/combobox.js +100 -0
  41. package/dist/components/combobox.js.map +1 -0
  42. package/dist/components/command.d.ts +19 -0
  43. package/dist/components/command.js +86 -0
  44. package/dist/components/command.js.map +1 -0
  45. package/dist/components/cta-banner.d.ts +14 -0
  46. package/dist/components/cta-banner.js +39 -0
  47. package/dist/components/cta-banner.js.map +1 -0
  48. package/dist/components/date-picker.d.ts +19 -0
  49. package/dist/components/date-picker.js +107 -0
  50. package/dist/components/date-picker.js.map +1 -0
  51. package/dist/components/dialog.d.ts +18 -0
  52. package/dist/components/dialog.js +99 -0
  53. package/dist/components/dialog.js.map +1 -0
  54. package/dist/components/dropdown-menu.d.ts +26 -0
  55. package/dist/components/dropdown-menu.js +123 -0
  56. package/dist/components/dropdown-menu.js.map +1 -0
  57. package/dist/components/field.d.ts +30 -0
  58. package/dist/components/field.js +53 -0
  59. package/dist/components/field.js.map +1 -0
  60. package/dist/components/footer.d.ts +18 -0
  61. package/dist/components/footer.js +40 -0
  62. package/dist/components/footer.js.map +1 -0
  63. package/dist/components/fx-converter.d.ts +22 -0
  64. package/dist/components/fx-converter.js +91 -0
  65. package/dist/components/fx-converter.js.map +1 -0
  66. package/dist/components/hero.d.ts +28 -0
  67. package/dist/components/hero.js +83 -0
  68. package/dist/components/hero.js.map +1 -0
  69. package/dist/components/hud.d.ts +41 -0
  70. package/dist/components/hud.js +108 -0
  71. package/dist/components/hud.js.map +1 -0
  72. package/dist/components/input.d.ts +4 -0
  73. package/dist/components/input.js +14 -0
  74. package/dist/components/input.js.map +1 -0
  75. package/dist/components/label.d.ts +5 -0
  76. package/dist/components/label.js +15 -0
  77. package/dist/components/label.js.map +1 -0
  78. package/dist/components/media-card.d.ts +29 -0
  79. package/dist/components/media-card.js +80 -0
  80. package/dist/components/media-card.js.map +1 -0
  81. package/dist/components/money-input.d.ts +10 -0
  82. package/dist/components/money-input.js +30 -0
  83. package/dist/components/money-input.js.map +1 -0
  84. package/dist/components/multi-combobox.d.ts +27 -0
  85. package/dist/components/multi-combobox.js +101 -0
  86. package/dist/components/multi-combobox.js.map +1 -0
  87. package/dist/components/nav-bar.d.ts +21 -0
  88. package/dist/components/nav-bar.js +44 -0
  89. package/dist/components/nav-bar.js.map +1 -0
  90. package/dist/components/panel.d.ts +27 -0
  91. package/dist/components/panel.js +49 -0
  92. package/dist/components/panel.js.map +1 -0
  93. package/dist/components/popover.d.ts +11 -0
  94. package/dist/components/popover.js +56 -0
  95. package/dist/components/popover.js.map +1 -0
  96. package/dist/components/select.d.ts +16 -0
  97. package/dist/components/select.js +128 -0
  98. package/dist/components/select.js.map +1 -0
  99. package/dist/components/separator.d.ts +5 -0
  100. package/dist/components/separator.js +17 -0
  101. package/dist/components/separator.js.map +1 -0
  102. package/dist/components/sheet.d.ts +15 -0
  103. package/dist/components/sheet.js +87 -0
  104. package/dist/components/sheet.js.map +1 -0
  105. package/dist/components/skeleton.d.ts +3 -0
  106. package/dist/components/skeleton.js +13 -0
  107. package/dist/components/skeleton.js.map +1 -0
  108. package/dist/components/sonner.d.ts +5 -0
  109. package/dist/components/sonner.js +44 -0
  110. package/dist/components/sonner.js.map +1 -0
  111. package/dist/components/spec-readout.d.ts +11 -0
  112. package/dist/components/spec-readout.js +25 -0
  113. package/dist/components/spec-readout.js.map +1 -0
  114. package/dist/components/split-flap.d.ts +12 -0
  115. package/dist/components/split-flap.js +77 -0
  116. package/dist/components/split-flap.js.map +1 -0
  117. package/dist/components/tabs.d.ts +12 -0
  118. package/dist/components/tabs.js +47 -0
  119. package/dist/components/tabs.js.map +1 -0
  120. package/dist/components/tag.d.ts +16 -0
  121. package/dist/components/tag.js +26 -0
  122. package/dist/components/tag.js.map +1 -0
  123. package/dist/components/terminal.d.ts +10 -0
  124. package/dist/components/terminal.js +20 -0
  125. package/dist/components/terminal.js.map +1 -0
  126. package/dist/components/textarea.d.ts +4 -0
  127. package/dist/components/textarea.js +13 -0
  128. package/dist/components/textarea.js.map +1 -0
  129. package/dist/components/timeline-entry.d.ts +26 -0
  130. package/dist/components/timeline-entry.js +63 -0
  131. package/dist/components/timeline-entry.js.map +1 -0
  132. package/dist/components/toggle-group.d.ts +10 -0
  133. package/dist/components/toggle-group.js +49 -0
  134. package/dist/components/toggle-group.js.map +1 -0
  135. package/dist/components/toggle.d.ts +10 -0
  136. package/dist/components/toggle.js +37 -0
  137. package/dist/components/toggle.js.map +1 -0
  138. package/dist/components/tooltip.d.ts +8 -0
  139. package/dist/components/tooltip.js +36 -0
  140. package/dist/components/tooltip.js.map +1 -0
  141. package/dist/finance.d.ts +10 -0
  142. package/dist/finance.js +10 -0
  143. package/dist/index.d.ts +33 -0
  144. package/dist/index.js +33 -0
  145. package/dist/lib/category-icons.d.ts +80 -0
  146. package/dist/lib/category-icons.js +102 -0
  147. package/dist/lib/category-icons.js.map +1 -0
  148. package/dist/lib/fault.d.ts +7 -0
  149. package/dist/lib/fault.js +28 -0
  150. package/dist/lib/fault.js.map +1 -0
  151. package/dist/lib/form-flow.d.ts +15 -0
  152. package/dist/lib/form-flow.js +50 -0
  153. package/dist/lib/form-flow.js.map +1 -0
  154. package/dist/lib/fx.d.ts +39 -0
  155. package/dist/lib/fx.js +98 -0
  156. package/dist/lib/fx.js.map +1 -0
  157. package/dist/lib/money.d.ts +19 -0
  158. package/dist/lib/money.js +55 -0
  159. package/dist/lib/money.js.map +1 -0
  160. package/dist/lib/use-media-query.d.ts +3 -0
  161. package/dist/lib/use-media-query.js +15 -0
  162. package/dist/lib/use-media-query.js.map +1 -0
  163. package/dist/lib/utils.d.ts +3 -0
  164. package/dist/lib/utils.js +26 -0
  165. package/dist/lib/utils.js.map +1 -0
  166. package/dist/site.d.ts +10 -0
  167. package/dist/site.js +10 -0
  168. package/dist/styles.css +2 -0
  169. package/dist/theme.css +391 -0
  170. package/dist/tokens.css +181 -0
  171. package/guidelines/AlertDialog.md +9 -0
  172. package/guidelines/Amount.md +10 -0
  173. package/guidelines/AppearancePickers.md +8 -0
  174. package/guidelines/Badge.md +11 -0
  175. package/guidelines/BudgetMeter.md +11 -0
  176. package/guidelines/Button.md +16 -0
  177. package/guidelines/Calendar.md +8 -0
  178. package/guidelines/CategoryIcon.md +10 -0
  179. package/guidelines/Charts.md +12 -0
  180. package/guidelines/Checkbox.md +8 -0
  181. package/guidelines/Combobox.md +18 -0
  182. package/guidelines/Command.md +8 -0
  183. package/guidelines/Crosshair.md +7 -0
  184. package/guidelines/CtaBanner.md +8 -0
  185. package/guidelines/DatePicker.md +11 -0
  186. package/guidelines/Dialog.md +9 -0
  187. package/guidelines/DropdownMenu.md +9 -0
  188. package/guidelines/Field.md +22 -0
  189. package/guidelines/Footer.md +7 -0
  190. package/guidelines/FxConverter.md +12 -0
  191. package/guidelines/GlassPanel.md +8 -0
  192. package/guidelines/Grill.md +7 -0
  193. package/guidelines/Hero.md +9 -0
  194. package/guidelines/HudLabel.md +8 -0
  195. package/guidelines/Input.md +9 -0
  196. package/guidelines/Label.md +7 -0
  197. package/guidelines/MediaCard.md +8 -0
  198. package/guidelines/MoneyInput.md +10 -0
  199. package/guidelines/MultiCombobox.md +11 -0
  200. package/guidelines/NavBar.md +10 -0
  201. package/guidelines/Panel.md +10 -0
  202. package/guidelines/Popover.md +8 -0
  203. package/guidelines/README.md +100 -0
  204. package/guidelines/SectionHeader.md +9 -0
  205. package/guidelines/Select.md +9 -0
  206. package/guidelines/Separator.md +8 -0
  207. package/guidelines/Sheet.md +9 -0
  208. package/guidelines/Skeleton.md +8 -0
  209. package/guidelines/SpecReadout.md +7 -0
  210. package/guidelines/SplitFlap.md +9 -0
  211. package/guidelines/StatusLed.md +8 -0
  212. package/guidelines/Tabs.md +9 -0
  213. package/guidelines/Tag.md +9 -0
  214. package/guidelines/Terminal.md +7 -0
  215. package/guidelines/Textarea.md +8 -0
  216. package/guidelines/TickRuler.md +8 -0
  217. package/guidelines/TimelineEntry.md +8 -0
  218. package/guidelines/Toaster.md +9 -0
  219. package/guidelines/Toggle.md +9 -0
  220. package/guidelines/Tooltip.md +8 -0
  221. package/package.json +103 -4
  222. package/tokens/tokens.json +739 -0
@@ -0,0 +1,33 @@
1
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger } from "./components/alert-dialog.js";
2
+ import { Badge, badgeVariants } from "./components/badge.js";
3
+ import { Button, buttonVariants } from "./components/button.js";
4
+ import { Calendar } from "./components/calendar.js";
5
+ import { Checkbox } from "./components/checkbox.js";
6
+ import { Combobox, ComboboxGroup } from "./components/combobox.js";
7
+ import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from "./components/command.js";
8
+ import { DatePicker, dateLabel, parseISODate, toISODate } from "./components/date-picker.js";
9
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./components/dialog.js";
10
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./components/dropdown-menu.js";
11
+ import { FaultLed, Field, FieldError, FieldLabel } from "./components/field.js";
12
+ import { Crosshair, Grill, HudLabel, SectionHeader, StatusLed, TickRuler } from "./components/hud.js";
13
+ import { Input } from "./components/input.js";
14
+ import { Label } from "./components/label.js";
15
+ import { MultiCombobox } from "./components/multi-combobox.js";
16
+ import { NavBar, NavItem } from "./components/nav-bar.js";
17
+ import { GlassPanel, HingeTab, Panel, PanelProps } from "./components/panel.js";
18
+ import { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "./components/popover.js";
19
+ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "./components/select.js";
20
+ import { Separator } from "./components/separator.js";
21
+ import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./components/sheet.js";
22
+ import { Skeleton } from "./components/skeleton.js";
23
+ import { Toaster } from "./components/sonner.js";
24
+ import { Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants } from "./components/tabs.js";
25
+ import { Textarea } from "./components/textarea.js";
26
+ import { Toggle, toggleVariants } from "./components/toggle.js";
27
+ import { ToggleGroup, ToggleGroupItem } from "./components/toggle-group.js";
28
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/tooltip.js";
29
+ import { FAULT_MS, fault } from "./lib/fault.js";
30
+ import { flowFields, focusFirstField, focusNextField } from "./lib/form-flow.js";
31
+ import { useMediaQuery } from "./lib/use-media-query.js";
32
+ import { cn } from "./lib/utils.js";
33
+ export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Badge, Button, Calendar, Checkbox, Combobox, ComboboxGroup, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, Crosshair, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FAULT_MS, FaultLed, Field, FieldError, FieldLabel, GlassPanel, Grill, HingeTab, HudLabel, Input, Label, MultiCombobox, NavBar, NavItem, Panel, PanelProps, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, SectionHeader, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Skeleton, StatusLed, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, TickRuler, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, cn, dateLabel, fault, flowFields, focusFirstField, focusNextField, parseISODate, tabsListVariants, toISODate, toggleVariants, useMediaQuery };
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger } from "./components/alert-dialog.js";
2
+ import { Badge, badgeVariants } from "./components/badge.js";
3
+ import { Button, buttonVariants } from "./components/button.js";
4
+ import { Calendar } from "./components/calendar.js";
5
+ import { Checkbox } from "./components/checkbox.js";
6
+ import { Combobox } from "./components/combobox.js";
7
+ import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from "./components/command.js";
8
+ import { DatePicker, dateLabel, parseISODate, toISODate } from "./components/date-picker.js";
9
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./components/dialog.js";
10
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./components/dropdown-menu.js";
11
+ import { FaultLed, Field, FieldError, FieldLabel } from "./components/field.js";
12
+ import { Crosshair, Grill, HudLabel, SectionHeader, StatusLed, TickRuler } from "./components/hud.js";
13
+ import { Input } from "./components/input.js";
14
+ import { Label } from "./components/label.js";
15
+ import { MultiCombobox } from "./components/multi-combobox.js";
16
+ import { NavBar } from "./components/nav-bar.js";
17
+ import { GlassPanel, HingeTab, Panel } from "./components/panel.js";
18
+ import { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "./components/popover.js";
19
+ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "./components/select.js";
20
+ import { Separator } from "./components/separator.js";
21
+ import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./components/sheet.js";
22
+ import { Skeleton } from "./components/skeleton.js";
23
+ import { Toaster } from "./components/sonner.js";
24
+ import { Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants } from "./components/tabs.js";
25
+ import { Textarea } from "./components/textarea.js";
26
+ import { Toggle, toggleVariants } from "./components/toggle.js";
27
+ import { ToggleGroup, ToggleGroupItem } from "./components/toggle-group.js";
28
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/tooltip.js";
29
+ import { FAULT_MS, fault } from "./lib/fault.js";
30
+ import { flowFields, focusFirstField, focusNextField } from "./lib/form-flow.js";
31
+ import { useMediaQuery } from "./lib/use-media-query.js";
32
+ import { cn } from "./lib/utils.js";
33
+ export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Badge, Button, Calendar, Checkbox, Combobox, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, Crosshair, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FAULT_MS, FaultLed, Field, FieldError, FieldLabel, GlassPanel, Grill, HingeTab, HudLabel, Input, Label, MultiCombobox, NavBar, Panel, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, SectionHeader, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Skeleton, StatusLed, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, TickRuler, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, cn, dateLabel, fault, flowFields, focusFirstField, focusNextField, parseISODate, tabsListVariants, toISODate, toggleVariants, useMediaQuery };
@@ -0,0 +1,80 @@
1
+ import { LucideIcon } from "lucide-react";
2
+ export declare const categoryIcons: {
3
+ "arrow-left-right": LucideIcon;
4
+ baby: LucideIcon;
5
+ banknote: LucideIcon;
6
+ beer: LucideIcon;
7
+ bike: LucideIcon;
8
+ "book-open": LucideIcon;
9
+ briefcase: LucideIcon;
10
+ "building-2": LucideIcon;
11
+ bus: LucideIcon;
12
+ car: LucideIcon;
13
+ cat: LucideIcon;
14
+ "circle-dollar-sign": LucideIcon;
15
+ "circle-help": LucideIcon;
16
+ cigarette: LucideIcon;
17
+ coffee: LucideIcon;
18
+ coins: LucideIcon;
19
+ dog: LucideIcon;
20
+ droplet: LucideIcon;
21
+ dumbbell: LucideIcon;
22
+ ellipsis: LucideIcon;
23
+ "file-text": LucideIcon;
24
+ film: LucideIcon;
25
+ flame: LucideIcon;
26
+ "flower-2": LucideIcon;
27
+ fuel: LucideIcon;
28
+ "gamepad-2": LucideIcon;
29
+ gift: LucideIcon;
30
+ "graduation-cap": LucideIcon;
31
+ hammer: LucideIcon;
32
+ "hand-coins": LucideIcon;
33
+ "heart-pulse": LucideIcon;
34
+ house: LucideIcon;
35
+ landmark: LucideIcon;
36
+ laptop: LucideIcon;
37
+ leaf: LucideIcon;
38
+ lightbulb: LucideIcon;
39
+ music: LucideIcon;
40
+ package: LucideIcon;
41
+ palette: LucideIcon;
42
+ "paw-print": LucideIcon;
43
+ percent: LucideIcon;
44
+ "piggy-bank": LucideIcon;
45
+ pill: LucideIcon;
46
+ plane: LucideIcon;
47
+ receipt: LucideIcon;
48
+ scissors: LucideIcon;
49
+ "shield-check": LucideIcon;
50
+ shirt: LucideIcon;
51
+ "shopping-bag": LucideIcon;
52
+ "shopping-cart": LucideIcon;
53
+ smartphone: LucideIcon;
54
+ sofa: LucideIcon;
55
+ sparkles: LucideIcon;
56
+ stethoscope: LucideIcon;
57
+ tent: LucideIcon;
58
+ ticket: LucideIcon;
59
+ "train-front": LucideIcon;
60
+ "tram-front": LucideIcon;
61
+ "trending-up": LucideIcon;
62
+ umbrella: LucideIcon;
63
+ "undo-2": LucideIcon;
64
+ users: LucideIcon;
65
+ utensils: LucideIcon;
66
+ volleyball: LucideIcon;
67
+ wallet: LucideIcon;
68
+ wifi: LucideIcon;
69
+ wine: LucideIcon;
70
+ wrench: LucideIcon;
71
+ zap: LucideIcon;
72
+ };
73
+ export type CategoryIconName = keyof typeof categoryIcons;
74
+ export declare function categoryIcon(name: string): LucideIcon;
75
+ /** Category/label palette token names (see tokens.json `cat-*`). */
76
+ export declare const categoryColors: readonly ["cat-red", "cat-orange", "cat-amber", "cat-yellow", "cat-lime", "cat-green", "cat-teal", "cat-cyan", "cat-sky", "cat-blue", "cat-indigo", "cat-violet", "cat-pink", "cat-rose", "cat-brown", "cat-slate"];
77
+ export type CategoryColor = (typeof categoryColors)[number];
78
+ /** CSS color for a palette token name; unknown names fall back to slate. */
79
+ export declare function categoryColorVar(color: string): string;
80
+ //# sourceMappingURL=category-icons.d.ts.map
@@ -0,0 +1,102 @@
1
+ import { ArrowLeftRight, Baby, Banknote, Beer, Bike, BookOpen, Briefcase, Building2, Bus, Car, Cat, Cigarette, CircleDollarSign, CircleHelp, Coffee, Coins, Dog, Droplet, Dumbbell, Ellipsis, FileText, Film, Flame, Flower2, Fuel, Gamepad2, Gift, GraduationCap, Hammer, HandCoins, HeartPulse, House, Landmark, Laptop, Leaf, Lightbulb, Music, Package, Palette, PawPrint, Percent, PiggyBank, Pill, Plane, Receipt, Scissors, ShieldCheck, Shirt, ShoppingBag, ShoppingCart, Smartphone, Sofa, Sparkles, Stethoscope, Tent, Ticket, TrainFront, TramFront, TrendingUp, Umbrella, Undo2, Users, Utensils, Volleyball, Wallet, Wifi, Wine, Wrench, Zap } from "lucide-react";
2
+ // src/lib/category-icons.ts
3
+ const categoryIcons = {
4
+ "arrow-left-right": ArrowLeftRight,
5
+ baby: Baby,
6
+ banknote: Banknote,
7
+ beer: Beer,
8
+ bike: Bike,
9
+ "book-open": BookOpen,
10
+ briefcase: Briefcase,
11
+ "building-2": Building2,
12
+ bus: Bus,
13
+ car: Car,
14
+ cat: Cat,
15
+ "circle-dollar-sign": CircleDollarSign,
16
+ "circle-help": CircleHelp,
17
+ cigarette: Cigarette,
18
+ coffee: Coffee,
19
+ coins: Coins,
20
+ dog: Dog,
21
+ droplet: Droplet,
22
+ dumbbell: Dumbbell,
23
+ ellipsis: Ellipsis,
24
+ "file-text": FileText,
25
+ film: Film,
26
+ flame: Flame,
27
+ "flower-2": Flower2,
28
+ fuel: Fuel,
29
+ "gamepad-2": Gamepad2,
30
+ gift: Gift,
31
+ "graduation-cap": GraduationCap,
32
+ hammer: Hammer,
33
+ "hand-coins": HandCoins,
34
+ "heart-pulse": HeartPulse,
35
+ house: House,
36
+ landmark: Landmark,
37
+ laptop: Laptop,
38
+ leaf: Leaf,
39
+ lightbulb: Lightbulb,
40
+ music: Music,
41
+ package: Package,
42
+ palette: Palette,
43
+ "paw-print": PawPrint,
44
+ percent: Percent,
45
+ "piggy-bank": PiggyBank,
46
+ pill: Pill,
47
+ plane: Plane,
48
+ receipt: Receipt,
49
+ scissors: Scissors,
50
+ "shield-check": ShieldCheck,
51
+ shirt: Shirt,
52
+ "shopping-bag": ShoppingBag,
53
+ "shopping-cart": ShoppingCart,
54
+ smartphone: Smartphone,
55
+ sofa: Sofa,
56
+ sparkles: Sparkles,
57
+ stethoscope: Stethoscope,
58
+ tent: Tent,
59
+ ticket: Ticket,
60
+ "train-front": TrainFront,
61
+ "tram-front": TramFront,
62
+ "trending-up": TrendingUp,
63
+ umbrella: Umbrella,
64
+ "undo-2": Undo2,
65
+ users: Users,
66
+ utensils: Utensils,
67
+ volleyball: Volleyball,
68
+ wallet: Wallet,
69
+ wifi: Wifi,
70
+ wine: Wine,
71
+ wrench: Wrench,
72
+ zap: Zap
73
+ };
74
+ function categoryIcon(name) {
75
+ return categoryIcons[name] ?? CircleHelp;
76
+ }
77
+ /** Category/label palette token names (see tokens.json `cat-*`). */
78
+ const categoryColors = [
79
+ "cat-red",
80
+ "cat-orange",
81
+ "cat-amber",
82
+ "cat-yellow",
83
+ "cat-lime",
84
+ "cat-green",
85
+ "cat-teal",
86
+ "cat-cyan",
87
+ "cat-sky",
88
+ "cat-blue",
89
+ "cat-indigo",
90
+ "cat-violet",
91
+ "cat-pink",
92
+ "cat-rose",
93
+ "cat-brown",
94
+ "cat-slate"
95
+ ];
96
+ /** CSS color for a palette token name; unknown names fall back to slate. */
97
+ function categoryColorVar(color) {
98
+ return `var(--color-${categoryColors.includes(color) ? color : "cat-slate"})`;
99
+ }
100
+ export { categoryColorVar, categoryColors, categoryIcon, categoryIcons };
101
+
102
+ //# sourceMappingURL=category-icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"category-icons.js","names":[],"sources":["../../src/lib/category-icons.ts"],"sourcesContent":["// Curated Lucide icons for categories. Stored in the database by kebab-case name; anything not in\n// this map renders as \"circle-help\". Keep the list small enough for a picker grid.\n\nimport {\n ArrowLeftRight,\n Baby,\n Banknote,\n Beer,\n Bike,\n BookOpen,\n Briefcase,\n Building2,\n Bus,\n Car,\n Cat,\n Cigarette,\n CircleDollarSign,\n CircleHelp,\n Coffee,\n Coins,\n Dog,\n Droplet,\n Dumbbell,\n Ellipsis,\n FileText,\n Film,\n Flame,\n Flower2,\n Fuel,\n Gamepad2,\n Gift,\n GraduationCap,\n Hammer,\n HandCoins,\n HeartPulse,\n House,\n Landmark,\n Laptop,\n Leaf,\n Lightbulb,\n type LucideIcon,\n Music,\n Package,\n Palette,\n PawPrint,\n Percent,\n PiggyBank,\n Pill,\n Plane,\n Receipt,\n Scissors,\n ShieldCheck,\n Shirt,\n ShoppingBag,\n ShoppingCart,\n Smartphone,\n Sofa,\n Sparkles,\n Stethoscope,\n Tent,\n Ticket,\n TrainFront,\n TramFront,\n TrendingUp,\n Umbrella,\n Undo2,\n Users,\n Utensils,\n Volleyball,\n Wallet,\n Wifi,\n Wine,\n Wrench,\n Zap,\n} from \"lucide-react\";\n\nexport const categoryIcons = {\n \"arrow-left-right\": ArrowLeftRight,\n baby: Baby,\n banknote: Banknote,\n beer: Beer,\n bike: Bike,\n \"book-open\": BookOpen,\n briefcase: Briefcase,\n \"building-2\": Building2,\n bus: Bus,\n car: Car,\n cat: Cat,\n \"circle-dollar-sign\": CircleDollarSign,\n \"circle-help\": CircleHelp,\n cigarette: Cigarette,\n coffee: Coffee,\n coins: Coins,\n dog: Dog,\n droplet: Droplet,\n dumbbell: Dumbbell,\n ellipsis: Ellipsis,\n \"file-text\": FileText,\n film: Film,\n flame: Flame,\n \"flower-2\": Flower2,\n fuel: Fuel,\n \"gamepad-2\": Gamepad2,\n gift: Gift,\n \"graduation-cap\": GraduationCap,\n hammer: Hammer,\n \"hand-coins\": HandCoins,\n \"heart-pulse\": HeartPulse,\n house: House,\n landmark: Landmark,\n laptop: Laptop,\n leaf: Leaf,\n lightbulb: Lightbulb,\n music: Music,\n package: Package,\n palette: Palette,\n \"paw-print\": PawPrint,\n percent: Percent,\n \"piggy-bank\": PiggyBank,\n pill: Pill,\n plane: Plane,\n receipt: Receipt,\n scissors: Scissors,\n \"shield-check\": ShieldCheck,\n shirt: Shirt,\n \"shopping-bag\": ShoppingBag,\n \"shopping-cart\": ShoppingCart,\n smartphone: Smartphone,\n sofa: Sofa,\n sparkles: Sparkles,\n stethoscope: Stethoscope,\n tent: Tent,\n ticket: Ticket,\n \"train-front\": TrainFront,\n \"tram-front\": TramFront,\n \"trending-up\": TrendingUp,\n umbrella: Umbrella,\n \"undo-2\": Undo2,\n users: Users,\n utensils: Utensils,\n volleyball: Volleyball,\n wallet: Wallet,\n wifi: Wifi,\n wine: Wine,\n wrench: Wrench,\n zap: Zap,\n} satisfies Record<string, LucideIcon>;\n\nexport type CategoryIconName = keyof typeof categoryIcons;\n\nexport function categoryIcon(name: string): LucideIcon {\n return (categoryIcons as Record<string, LucideIcon>)[name] ?? CircleHelp;\n}\n\n/** Category/label palette token names (see tokens.json `cat-*`). */\nexport const categoryColors = [\n \"cat-red\",\n \"cat-orange\",\n \"cat-amber\",\n \"cat-yellow\",\n \"cat-lime\",\n \"cat-green\",\n \"cat-teal\",\n \"cat-cyan\",\n \"cat-sky\",\n \"cat-blue\",\n \"cat-indigo\",\n \"cat-violet\",\n \"cat-pink\",\n \"cat-rose\",\n \"cat-brown\",\n \"cat-slate\",\n] as const;\n\nexport type CategoryColor = (typeof categoryColors)[number];\n\n/** CSS color for a palette token name; unknown names fall back to slate. */\nexport function categoryColorVar(color: string): string {\n const known = (categoryColors as readonly string[]).includes(color) ? color : \"cat-slate\";\n return `var(--color-${known})`;\n}\n"],"mappings":";;AA4EA,MAAa,gBAAgB;CAC3B,oBAAoB;CACpB,MAAM;CACN,UAAU;CACV,MAAM;CACN,MAAM;CACN,aAAa;CACb,WAAW;CACX,cAAc;CACd,KAAK;CACL,KAAK;CACL,KAAK;CACL,sBAAsB;CACtB,eAAe;CACf,WAAW;CACX,QAAQ;CACR,OAAO;CACP,KAAK;CACL,SAAS;CACT,UAAU;CACV,UAAU;CACV,aAAa;CACb,MAAM;CACN,OAAO;CACP,YAAY;CACZ,MAAM;CACN,aAAa;CACb,MAAM;CACN,kBAAkB;CAClB,QAAQ;CACR,cAAc;CACd,eAAe;CACf,OAAO;CACP,UAAU;CACV,QAAQ;CACR,MAAM;CACN,WAAW;CACX,OAAO;CACP,SAAS;CACT,SAAS;CACT,aAAa;CACb,SAAS;CACT,cAAc;CACd,MAAM;CACN,OAAO;CACP,SAAS;CACT,UAAU;CACV,gBAAgB;CAChB,OAAO;CACP,gBAAgB;CAChB,iBAAiB;CACjB,YAAY;CACZ,MAAM;CACN,UAAU;CACV,aAAa;CACb,MAAM;CACN,QAAQ;CACR,eAAe;CACf,cAAc;CACd,eAAe;CACf,UAAU;CACV,UAAU;CACV,OAAO;CACP,UAAU;CACV,YAAY;CACZ,QAAQ;CACR,MAAM;CACN,MAAM;CACN,QAAQ;CACR,KAAK;AACP;AAIA,SAAgB,aAAa,MAA0B;CACrD,OAAQ,cAA6C,SAAS;AAChE;;AAGA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAKA,SAAgB,iBAAiB,OAAuB;CAEtD,OAAO,eADQ,eAAqC,SAAS,KAAK,IAAI,QAAQ,YAClD;AAC9B"}
@@ -0,0 +1,7 @@
1
+ /** Longest fault animation (ms); the attribute is removed after it. */
2
+ export declare const FAULT_MS = 900;
3
+ /** Faults a control. Returns the parts that were animated. */
4
+ export declare function fault(target: Element | string | null | undefined, opts?: {
5
+ focus?: boolean;
6
+ }): HTMLElement[];
7
+ //# sourceMappingURL=fault.d.ts.map
@@ -0,0 +1,28 @@
1
+ // src/lib/fault.ts
2
+ /** Longest fault animation (ms); the attribute is removed after it. */
3
+ const FAULT_MS = 900;
4
+ const timers = /* @__PURE__ */ new WeakMap();
5
+ function restart(part) {
6
+ const t = timers.get(part);
7
+ if (t !== void 0) window.clearTimeout(t);
8
+ part.removeAttribute("data-fault");
9
+ part.offsetWidth;
10
+ part.setAttribute("data-fault", "");
11
+ timers.set(part, window.setTimeout(() => {
12
+ part.removeAttribute("data-fault");
13
+ timers.delete(part);
14
+ }, 900));
15
+ }
16
+ /** Faults a control. Returns the parts that were animated. */
17
+ function fault(target, opts = {}) {
18
+ if (typeof document === "undefined" || !target) return [];
19
+ const el = typeof target === "string" ? document.getElementById(target) : target;
20
+ if (!(el instanceof HTMLElement)) return [];
21
+ const parts = el.matches("[data-fault-group]") ? Array.from(el.querySelectorAll("[data-fault-part]")) : [el.closest("[data-fault-part]") ?? el];
22
+ for (const p of parts) restart(p);
23
+ if (opts.focus !== false) (el.matches("input, button, select, textarea, [tabindex]") ? el : el.querySelector("input, button, select, textarea, [tabindex]"))?.focus({ preventScroll: false });
24
+ return parts;
25
+ }
26
+ export { FAULT_MS, fault };
27
+
28
+ //# sourceMappingURL=fault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fault.js","names":[],"sources":["../../src/lib/fault.ts"],"sourcesContent":["// Fault feedback: makes a control visibly reject input (servo jitter + danger ping, see the\n// \"Fault feedback\" section of styles/kit.css) and moves focus to it.\n//\n// Targets: an element or an element id. A target inside a [data-fault-part] (e.g. the input in\n// a MoneyInput) faults that whole part; a [data-fault-group] (e.g. the FxConverter) faults every\n// part inside it.\n\n/** Longest fault animation (ms); the attribute is removed after it. */\nexport const FAULT_MS = 900;\n\nconst timers = new WeakMap<HTMLElement, number>();\n\nfunction restart(part: HTMLElement) {\n const t = timers.get(part);\n if (t !== undefined) window.clearTimeout(t);\n part.removeAttribute(\"data-fault\");\n // Force a reflow so the animation starts again even if it was still running.\n void part.offsetWidth;\n part.setAttribute(\"data-fault\", \"\");\n timers.set(\n part,\n window.setTimeout(() => {\n part.removeAttribute(\"data-fault\");\n timers.delete(part);\n }, FAULT_MS),\n );\n}\n\n/** Faults a control. Returns the parts that were animated. */\nexport function fault(target: Element | string | null | undefined, opts: { focus?: boolean } = {}) {\n if (typeof document === \"undefined\" || !target) return [];\n const el = typeof target === \"string\" ? document.getElementById(target) : target;\n if (!(el instanceof HTMLElement)) return [];\n\n const parts = el.matches(\"[data-fault-group]\")\n ? Array.from(el.querySelectorAll<HTMLElement>(\"[data-fault-part]\"))\n : [el.closest<HTMLElement>(\"[data-fault-part]\") ?? el];\n for (const p of parts) restart(p);\n\n if (opts.focus !== false) {\n const focusable = el.matches(\"input, button, select, textarea, [tabindex]\")\n ? el\n : el.querySelector<HTMLElement>(\"input, button, select, textarea, [tabindex]\");\n focusable?.focus({ preventScroll: false });\n }\n return parts;\n}\n"],"mappings":";;AAQA,MAAa,WAAW;AAExB,MAAM,yBAAS,IAAI,QAA6B;AAEhD,SAAS,QAAQ,MAAmB;CAClC,MAAM,IAAI,OAAO,IAAI,IAAI;CACzB,IAAI,MAAM,KAAA,GAAW,OAAO,aAAa,CAAC;CAC1C,KAAK,gBAAgB,YAAY;CAEjC,KAAU;CACV,KAAK,aAAa,cAAc,EAAE;CAClC,OAAO,IACL,MACA,OAAO,iBAAiB;EACtB,KAAK,gBAAgB,YAAY;EACjC,OAAO,OAAO,IAAI;CACpB,GAAA,GAAW,CACb;AACF;;AAGA,SAAgB,MAAM,QAA6C,OAA4B,CAAC,GAAG;CACjG,IAAI,OAAO,aAAa,eAAe,CAAC,QAAQ,OAAO,CAAC;CACxD,MAAM,KAAK,OAAO,WAAW,WAAW,SAAS,eAAe,MAAM,IAAI;CAC1E,IAAI,EAAE,cAAc,cAAc,OAAO,CAAC;CAE1C,MAAM,QAAQ,GAAG,QAAQ,oBAAoB,IACzC,MAAM,KAAK,GAAG,iBAA8B,mBAAmB,CAAC,IAChE,CAAC,GAAG,QAAqB,mBAAmB,KAAK,EAAE;CACvD,KAAK,MAAM,KAAK,OAAO,QAAQ,CAAC;CAEhC,IAAI,KAAK,UAAU,OAIjB,CAHkB,GAAG,QAAQ,6CAA6C,IACtE,KACA,GAAG,cAA2B,6CAA6C,EAAA,EACpE,MAAM,EAAE,eAAe,MAAM,CAAC;CAE3C,OAAO;AACT"}
@@ -0,0 +1,15 @@
1
+ /** Fields inside a container, in DOM (tab) order. */
2
+ export declare function flowFields(root: ParentNode): HTMLElement[];
3
+ /**
4
+ * Focuses the first field inside a container. If that field is an empty picker (marked
5
+ * [data-flow-autoopen]), it also opens it once the container's entrance animation has finished,
6
+ * so the list doesn't anchor to a trigger that is still sliding in. Returns whether a field was
7
+ * found.
8
+ */
9
+ export declare function focusFirstField(root: ParentNode | null | undefined): boolean;
10
+ /**
11
+ * Moves focus to the field after `from` within its form (or dialog). Falls back to the form's
12
+ * submit button, then to `from` itself. Returns the element that got focus.
13
+ */
14
+ export declare function focusNextField(from: HTMLElement | null | undefined): HTMLElement | null;
15
+ //# sourceMappingURL=form-flow.d.ts.map
@@ -0,0 +1,50 @@
1
+ // src/lib/form-flow.ts
2
+ const FIELD_SELECTOR = [
3
+ "input:not([type=hidden]):not([type=checkbox]):not([type=radio])",
4
+ "textarea",
5
+ "select",
6
+ "[role=combobox]",
7
+ "[data-flow-field]"
8
+ ].map((s) => `${s}:not([disabled]):not([aria-disabled=true])`).join(",");
9
+ function visible(el) {
10
+ return el.getClientRects().length > 0 && getComputedStyle(el).visibility !== "hidden";
11
+ }
12
+ /** Fields inside a container, in DOM (tab) order. */
13
+ function flowFields(root) {
14
+ return Array.from(root.querySelectorAll(FIELD_SELECTOR)).filter((el) => visible(el) && !el.closest("[data-flow-skip]"));
15
+ }
16
+ /**
17
+ * Focuses the first field inside a container. If that field is an empty picker (marked
18
+ * [data-flow-autoopen]), it also opens it once the container's entrance animation has finished,
19
+ * so the list doesn't anchor to a trigger that is still sliding in. Returns whether a field was
20
+ * found.
21
+ */
22
+ function focusFirstField(root) {
23
+ const first = root ? flowFields(root)[0] : void 0;
24
+ if (!first) return false;
25
+ first.focus();
26
+ if (first.hasAttribute("data-flow-autoopen")) openWhenSettled(first);
27
+ return true;
28
+ }
29
+ function openWhenSettled(trigger) {
30
+ const running = trigger.closest("[role=dialog]")?.getAnimations?.() ?? [];
31
+ Promise.allSettled(running.map((a) => a.finished)).then(() => {
32
+ if (trigger.isConnected && document.activeElement === trigger) trigger.click();
33
+ });
34
+ }
35
+ /**
36
+ * Moves focus to the field after `from` within its form (or dialog). Falls back to the form's
37
+ * submit button, then to `from` itself. Returns the element that got focus.
38
+ */
39
+ function focusNextField(from) {
40
+ if (!from) return null;
41
+ const scope = from.closest("form, [data-flow-scope], [role=dialog]") ?? from.ownerDocument.body;
42
+ const fields = flowFields(scope);
43
+ const index = fields.findIndex((f) => f === from || f.contains(from) || from.contains(f));
44
+ const next = (index >= 0 ? fields[index + 1] : void 0) ?? scope.querySelector("button[type=submit]:not([disabled])") ?? from;
45
+ next.focus();
46
+ return next;
47
+ }
48
+ export { flowFields, focusFirstField, focusNextField };
49
+
50
+ //# sourceMappingURL=form-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-flow.js","names":[],"sources":["../../src/lib/form-flow.ts"],"sourcesContent":["// Form flow: keyboard focus moves through a form the way a person fills it in.\n//\n// - Dialogs and sheets focus their first field when they open (not the close button or tabs).\n// - Single-choice controls (Select, Combobox, DatePicker) move focus to the next field after a\n// choice is committed, instead of returning it to their own trigger.\n// - Tab follows DOM order, so fields are laid out in the order they are filled.\n//\n// A \"field\" is any enabled text input, textarea, native select, combobox trigger, or element\n// marked [data-flow-field]. Checkboxes, tabs and plain buttons are not fields.\n\nconst FIELD_SELECTOR = [\n \"input:not([type=hidden]):not([type=checkbox]):not([type=radio])\",\n \"textarea\",\n \"select\",\n \"[role=combobox]\",\n \"[data-flow-field]\",\n]\n .map((s) => `${s}:not([disabled]):not([aria-disabled=true])`)\n .join(\",\");\n\nfunction visible(el: HTMLElement) {\n return el.getClientRects().length > 0 && getComputedStyle(el).visibility !== \"hidden\";\n}\n\n/** Fields inside a container, in DOM (tab) order. */\nexport function flowFields(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FIELD_SELECTOR)).filter(\n (el) => visible(el) && !el.closest(\"[data-flow-skip]\"),\n );\n}\n\n/**\n * Focuses the first field inside a container. If that field is an empty picker (marked\n * [data-flow-autoopen]), it also opens it once the container's entrance animation has finished,\n * so the list doesn't anchor to a trigger that is still sliding in. Returns whether a field was\n * found.\n */\nexport function focusFirstField(root: ParentNode | null | undefined): boolean {\n const first = root ? flowFields(root)[0] : undefined;\n if (!first) return false;\n first.focus();\n if (first.hasAttribute(\"data-flow-autoopen\")) openWhenSettled(first);\n return true;\n}\n\nfunction openWhenSettled(trigger: HTMLElement) {\n const scope = trigger.closest<HTMLElement>(\"[role=dialog]\");\n const running = scope?.getAnimations?.() ?? [];\n void Promise.allSettled(running.map((a) => a.finished)).then(() => {\n // Only if the person hasn't moved on in the meantime.\n if (trigger.isConnected && document.activeElement === trigger) trigger.click();\n });\n}\n\n/**\n * Moves focus to the field after `from` within its form (or dialog). Falls back to the form's\n * submit button, then to `from` itself. Returns the element that got focus.\n */\nexport function focusNextField(from: HTMLElement | null | undefined): HTMLElement | null {\n if (!from) return null;\n const scope =\n from.closest<HTMLElement>(\"form, [data-flow-scope], [role=dialog]\") ?? from.ownerDocument.body;\n const fields = flowFields(scope);\n const index = fields.findIndex((f) => f === from || f.contains(from) || from.contains(f));\n const next =\n (index >= 0 ? fields[index + 1] : undefined) ??\n scope.querySelector<HTMLElement>(\"button[type=submit]:not([disabled])\") ??\n from;\n next.focus();\n return next;\n}\n"],"mappings":";AAUA,MAAM,iBAAiB;CACrB;CACA;CACA;CACA;CACA;AACF,CAAC,CACE,KAAK,MAAM,GAAG,EAAE,2CAA2C,CAAC,CAC5D,KAAK,GAAG;AAEX,SAAS,QAAQ,IAAiB;CAChC,OAAO,GAAG,eAAe,CAAC,CAAC,SAAS,KAAK,iBAAiB,EAAE,CAAC,CAAC,eAAe;AAC/E;;AAGA,SAAgB,WAAW,MAAiC;CAC1D,OAAO,MAAM,KAAK,KAAK,iBAA8B,cAAc,CAAC,CAAC,CAAC,QACnE,OAAO,QAAQ,EAAE,KAAK,CAAC,GAAG,QAAQ,kBAAkB,CACvD;AACF;;;;;;;AAQA,SAAgB,gBAAgB,MAA8C;CAC5E,MAAM,QAAQ,OAAO,WAAW,IAAI,CAAC,CAAC,KAAK,KAAA;CAC3C,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,MAAM;CACZ,IAAI,MAAM,aAAa,oBAAoB,GAAG,gBAAgB,KAAK;CACnE,OAAO;AACT;AAEA,SAAS,gBAAgB,SAAsB;CAE7C,MAAM,UADQ,QAAQ,QAAqB,eACvB,CAAC,EAAE,gBAAgB,KAAK,CAAC;CAC7C,QAAa,WAAW,QAAQ,KAAK,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW;EAEjE,IAAI,QAAQ,eAAe,SAAS,kBAAkB,SAAS,QAAQ,MAAM;CAC/E,CAAC;AACH;;;;;AAMA,SAAgB,eAAe,MAA0D;CACvF,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,QACJ,KAAK,QAAqB,wCAAwC,KAAK,KAAK,cAAc;CAC5F,MAAM,SAAS,WAAW,KAAK;CAC/B,MAAM,QAAQ,OAAO,WAAW,MAAM,MAAM,QAAQ,EAAE,SAAS,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC;CACxF,MAAM,QACH,SAAS,IAAI,OAAO,QAAQ,KAAK,KAAA,MAClC,MAAM,cAA2B,qCAAqC,KACtE;CACF,KAAK,MAAM;CACX,OAAO;AACT"}
@@ -0,0 +1,39 @@
1
+ export type FxField = "from" | "to" | "rate";
2
+ export type FxState = {
3
+ from: string;
4
+ to: string;
5
+ /** What the rate input shows (6 decimals when computed). */
6
+ rate: string;
7
+ /** Full-precision rate (10 decimals) sent to the API; equals `rate` when typed by the user. */
8
+ ratePrecise: string;
9
+ /** Touch order, oldest first. Always holds all three fields. */
10
+ order: FxField[];
11
+ };
12
+ export type FxCurrencies = {
13
+ fromMinorUnits: number;
14
+ toMinorUnits: number;
15
+ };
16
+ /** Decimal places kept for rates in storage (numeric(20,10)). */
17
+ export declare const RATE_SCALE = 10;
18
+ /** Decimal places shown in the rate input when the rate is computed. */
19
+ export declare const RATE_DISPLAY = 6;
20
+ export declare function initialFxState(): FxState;
21
+ /** Parses a positive decimal rate into a bigint scaled by 10^RATE_SCALE. */
22
+ export declare function parseRate(input: string): bigint | null;
23
+ /** Rounds n / d to the nearest integer, ties to even (banker's rounding). n, d > 0. */
24
+ export declare function divRoundHalfEven(n: bigint, d: bigint): bigint;
25
+ /** to_minor = from_minor × rate, converting between the currencies' decimal places. */
26
+ export declare function convertFromTo(fromMinor: bigint, rateScaled: bigint, c: FxCurrencies): bigint;
27
+ /** from_minor = to_minor ÷ rate. */
28
+ export declare function convertToFrom(toMinor: bigint, rateScaled: bigint, c: FxCurrencies): bigint;
29
+ /** rate = to ÷ from, scaled by 10^RATE_SCALE. */
30
+ export declare function deriveRate(fromMinor: bigint, toMinor: bigint, c: FxCurrencies): bigint;
31
+ /** Applies a user edit to one field and recomputes the least recently touched other field. */
32
+ export declare function editFx(state: FxState, field: FxField, raw: string, c: FxCurrencies): FxState;
33
+ /** The values to submit, or null while the form is incomplete. */
34
+ export declare function fxResult(state: FxState, c: FxCurrencies): {
35
+ fromMinor: bigint;
36
+ toMinor: bigint;
37
+ rate: string;
38
+ } | null;
39
+ //# sourceMappingURL=fx.d.ts.map
package/dist/lib/fx.js ADDED
@@ -0,0 +1,98 @@
1
+ import { minorToDecimal, parseAmount } from "./money.js";
2
+ // src/lib/fx.ts
3
+ /** Decimal places kept for rates in storage (numeric(20,10)). */
4
+ const RATE_SCALE = 10;
5
+ /** Decimal places shown in the rate input when the rate is computed. */
6
+ const RATE_DISPLAY = 6;
7
+ const SCALE = 10n ** BigInt(10);
8
+ const pow10 = (n) => 10n ** BigInt(n);
9
+ function initialFxState() {
10
+ return {
11
+ from: "",
12
+ to: "",
13
+ rate: "",
14
+ ratePrecise: "",
15
+ order: [
16
+ "rate",
17
+ "to",
18
+ "from"
19
+ ]
20
+ };
21
+ }
22
+ /** Parses a positive decimal rate into a bigint scaled by 10^RATE_SCALE. */
23
+ function parseRate(input) {
24
+ const s = input.trim().replace(/,/g, "");
25
+ if (!/^\d+(\.\d*)?$|^\.\d+$/.test(s)) return null;
26
+ const [whole = "0", frac = ""] = s.split(".");
27
+ const scaled = BigInt(whole || "0") * SCALE + BigInt(frac.slice(0, 10).padEnd(10, "0") || "0");
28
+ return scaled > 0n ? scaled : null;
29
+ }
30
+ /** Rounds n / d to the nearest integer, ties to even (banker's rounding). n, d > 0. */
31
+ function divRoundHalfEven(n, d) {
32
+ const q = n / d;
33
+ const twice = n % d * 2n;
34
+ if (twice > d || twice === d && q % 2n === 1n) return q + 1n;
35
+ return q;
36
+ }
37
+ function formatRate(scaled, decimals) {
38
+ const rounded = divRoundHalfEven(scaled, pow10(10 - decimals));
39
+ return minorToDecimal(rounded, decimals);
40
+ }
41
+ /** to_minor = from_minor × rate, converting between the currencies' decimal places. */
42
+ function convertFromTo(fromMinor, rateScaled, c) {
43
+ return divRoundHalfEven(fromMinor * rateScaled * pow10(c.toMinorUnits), SCALE * pow10(c.fromMinorUnits));
44
+ }
45
+ /** from_minor = to_minor ÷ rate. */
46
+ function convertToFrom(toMinor, rateScaled, c) {
47
+ return divRoundHalfEven(toMinor * SCALE * pow10(c.fromMinorUnits), rateScaled * pow10(c.toMinorUnits));
48
+ }
49
+ /** rate = to ÷ from, scaled by 10^RATE_SCALE. */
50
+ function deriveRate(fromMinor, toMinor, c) {
51
+ return divRoundHalfEven(toMinor * SCALE * pow10(c.fromMinorUnits), fromMinor * pow10(c.toMinorUnits));
52
+ }
53
+ /** Applies a user edit to one field and recomputes the least recently touched other field. */
54
+ function editFx(state, field, raw, c) {
55
+ const order = [...state.order.filter((f) => f !== field), field];
56
+ const next = {
57
+ ...state,
58
+ order,
59
+ [field]: raw
60
+ };
61
+ if (field === "rate") next.ratePrecise = raw;
62
+ const target = order.find((f) => f !== field);
63
+ const from = parseAmount(next.from, c.fromMinorUnits);
64
+ const to = parseAmount(next.to, c.toMinorUnits);
65
+ const rate = parseRate(next.ratePrecise);
66
+ switch (target) {
67
+ case "to":
68
+ next.to = from !== null && rate !== null ? minorToDecimal(convertFromTo(from, rate, c), c.toMinorUnits) : "";
69
+ break;
70
+ case "from":
71
+ next.from = to !== null && rate !== null ? minorToDecimal(convertToFrom(to, rate, c), c.fromMinorUnits) : "";
72
+ break;
73
+ case "rate": if (from !== null && to !== null && from > 0n && to > 0n) {
74
+ const r = deriveRate(from, to, c);
75
+ next.ratePrecise = formatRate(r, 10);
76
+ next.rate = formatRate(r, 6);
77
+ } else {
78
+ next.rate = "";
79
+ next.ratePrecise = "";
80
+ }
81
+ }
82
+ return next;
83
+ }
84
+ /** The values to submit, or null while the form is incomplete. */
85
+ function fxResult(state, c) {
86
+ const from = parseAmount(state.from, c.fromMinorUnits);
87
+ const to = parseAmount(state.to, c.toMinorUnits);
88
+ const rate = parseRate(state.ratePrecise);
89
+ if (from === null || to === null || rate === null || from <= 0n || to <= 0n) return null;
90
+ return {
91
+ fromMinor: from,
92
+ toMinor: to,
93
+ rate: formatRate(rate, 10)
94
+ };
95
+ }
96
+ export { RATE_DISPLAY, RATE_SCALE, convertFromTo, convertToFrom, deriveRate, divRoundHalfEven, editFx, fxResult, initialFxState, parseRate };
97
+
98
+ //# sourceMappingURL=fx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fx.js","names":[],"sources":["../../src/lib/fx.ts"],"sourcesContent":["// The three-field currency converter used for cross-currency transfers.\n//\n// Fields: `from` (amount leaving the source wallet), `to` (amount arriving in the target wallet)\n// and `rate` (target units per 1 source unit). When the user edits one field, the least recently\n// touched of the other two is recomputed from the two most recent ones:\n// to = from × rate, from = to ÷ rate, rate = to ÷ from\n// Before anything is touched the order (oldest first) is rate, to, from, so typing both amounts\n// derives the rate — the usual case when copying numbers from a bank statement.\n\nimport { minorToDecimal, parseAmount } from \"./money\";\n\nexport type FxField = \"from\" | \"to\" | \"rate\";\n\nexport type FxState = {\n from: string;\n to: string;\n /** What the rate input shows (6 decimals when computed). */\n rate: string;\n /** Full-precision rate (10 decimals) sent to the API; equals `rate` when typed by the user. */\n ratePrecise: string;\n /** Touch order, oldest first. Always holds all three fields. */\n order: FxField[];\n};\n\nexport type FxCurrencies = { fromMinorUnits: number; toMinorUnits: number };\n\n/** Decimal places kept for rates in storage (numeric(20,10)). */\nexport const RATE_SCALE = 10;\n/** Decimal places shown in the rate input when the rate is computed. */\nexport const RATE_DISPLAY = 6;\n\nconst SCALE = 10n ** BigInt(RATE_SCALE);\nconst pow10 = (n: number) => 10n ** BigInt(n);\n\nexport function initialFxState(): FxState {\n return { from: \"\", to: \"\", rate: \"\", ratePrecise: \"\", order: [\"rate\", \"to\", \"from\"] };\n}\n\n/** Parses a positive decimal rate into a bigint scaled by 10^RATE_SCALE. */\nexport function parseRate(input: string): bigint | null {\n const s = input.trim().replace(/,/g, \"\");\n if (!/^\\d+(\\.\\d*)?$|^\\.\\d+$/.test(s)) return null;\n const [whole = \"0\", frac = \"\"] = s.split(\".\");\n const scaled =\n BigInt(whole || \"0\") * SCALE + BigInt(frac.slice(0, RATE_SCALE).padEnd(RATE_SCALE, \"0\") || \"0\");\n return scaled > 0n ? scaled : null;\n}\n\n/** Rounds n / d to the nearest integer, ties to even (banker's rounding). n, d > 0. */\nexport function divRoundHalfEven(n: bigint, d: bigint): bigint {\n const q = n / d;\n const r = n % d;\n const twice = r * 2n;\n if (twice > d || (twice === d && q % 2n === 1n)) return q + 1n;\n return q;\n}\n\nfunction formatRate(scaled: bigint, decimals: number): string {\n const rounded = divRoundHalfEven(scaled, pow10(RATE_SCALE - decimals));\n return minorToDecimal(rounded, decimals);\n}\n\n/** to_minor = from_minor × rate, converting between the currencies' decimal places. */\nexport function convertFromTo(fromMinor: bigint, rateScaled: bigint, c: FxCurrencies): bigint {\n return divRoundHalfEven(\n fromMinor * rateScaled * pow10(c.toMinorUnits),\n SCALE * pow10(c.fromMinorUnits),\n );\n}\n\n/** from_minor = to_minor ÷ rate. */\nexport function convertToFrom(toMinor: bigint, rateScaled: bigint, c: FxCurrencies): bigint {\n return divRoundHalfEven(\n toMinor * SCALE * pow10(c.fromMinorUnits),\n rateScaled * pow10(c.toMinorUnits),\n );\n}\n\n/** rate = to ÷ from, scaled by 10^RATE_SCALE. */\nexport function deriveRate(fromMinor: bigint, toMinor: bigint, c: FxCurrencies): bigint {\n return divRoundHalfEven(\n toMinor * SCALE * pow10(c.fromMinorUnits),\n fromMinor * pow10(c.toMinorUnits),\n );\n}\n\n/** Applies a user edit to one field and recomputes the least recently touched other field. */\nexport function editFx(state: FxState, field: FxField, raw: string, c: FxCurrencies): FxState {\n const order = [...state.order.filter((f) => f !== field), field];\n const next: FxState = { ...state, order, [field]: raw };\n if (field === \"rate\") next.ratePrecise = raw;\n\n // The other two fields, oldest first; recompute the oldest.\n const target = order.find((f) => f !== field) as FxField;\n\n const from = parseAmount(next.from, c.fromMinorUnits);\n const to = parseAmount(next.to, c.toMinorUnits);\n const rate = parseRate(next.ratePrecise);\n\n switch (target) {\n case \"to\":\n next.to =\n from !== null && rate !== null\n ? minorToDecimal(convertFromTo(from, rate, c), c.toMinorUnits)\n : \"\";\n break;\n case \"from\":\n next.from =\n to !== null && rate !== null\n ? minorToDecimal(convertToFrom(to, rate, c), c.fromMinorUnits)\n : \"\";\n break;\n case \"rate\": {\n if (from !== null && to !== null && from > 0n && to > 0n) {\n const r = deriveRate(from, to, c);\n next.ratePrecise = formatRate(r, RATE_SCALE);\n next.rate = formatRate(r, RATE_DISPLAY);\n } else {\n next.rate = \"\";\n next.ratePrecise = \"\";\n }\n break;\n }\n }\n return next;\n}\n\n/** The values to submit, or null while the form is incomplete. */\nexport function fxResult(\n state: FxState,\n c: FxCurrencies,\n): { fromMinor: bigint; toMinor: bigint; rate: string } | null {\n const from = parseAmount(state.from, c.fromMinorUnits);\n const to = parseAmount(state.to, c.toMinorUnits);\n const rate = parseRate(state.ratePrecise);\n if (from === null || to === null || rate === null || from <= 0n || to <= 0n) return null;\n return { fromMinor: from, toMinor: to, rate: formatRate(rate, RATE_SCALE) };\n}\n"],"mappings":";;;AA2BA,MAAa,aAAa;;AAE1B,MAAa,eAAe;AAE5B,MAAM,QAAQ,OAAO,OAAA,EAAiB;AACtC,MAAM,SAAS,MAAc,OAAO,OAAO,CAAC;AAE5C,SAAgB,iBAA0B;CACxC,OAAO;EAAE,MAAM;EAAI,IAAI;EAAI,MAAM;EAAI,aAAa;EAAI,OAAO;GAAC;GAAQ;GAAM;EAAM;CAAE;AACtF;;AAGA,SAAgB,UAAU,OAA8B;CACtD,MAAM,IAAI,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,EAAE;CACvC,IAAI,CAAC,wBAAwB,KAAK,CAAC,GAAG,OAAO;CAC7C,MAAM,CAAC,QAAQ,KAAK,OAAO,MAAM,EAAE,MAAM,GAAG;CAC5C,MAAM,SACJ,OAAO,SAAS,GAAG,IAAI,QAAQ,OAAO,KAAK,MAAM,GAAA,EAAa,CAAC,CAAC,OAAA,IAAmB,GAAG,KAAK,GAAG;CAChG,OAAO,SAAS,KAAK,SAAS;AAChC;;AAGA,SAAgB,iBAAiB,GAAW,GAAmB;CAC7D,MAAM,IAAI,IAAI;CAEd,MAAM,QADI,IAAI,IACI;CAClB,IAAI,QAAQ,KAAM,UAAU,KAAK,IAAI,OAAO,IAAK,OAAO,IAAI;CAC5D,OAAO;AACT;AAEA,SAAS,WAAW,QAAgB,UAA0B;CAC5D,MAAM,UAAU,iBAAiB,QAAQ,MAAA,KAAmB,QAAQ,CAAC;CACrE,OAAO,eAAe,SAAS,QAAQ;AACzC;;AAGA,SAAgB,cAAc,WAAmB,YAAoB,GAAyB;CAC5F,OAAO,iBACL,YAAY,aAAa,MAAM,EAAE,YAAY,GAC7C,QAAQ,MAAM,EAAE,cAAc,CAChC;AACF;;AAGA,SAAgB,cAAc,SAAiB,YAAoB,GAAyB;CAC1F,OAAO,iBACL,UAAU,QAAQ,MAAM,EAAE,cAAc,GACxC,aAAa,MAAM,EAAE,YAAY,CACnC;AACF;;AAGA,SAAgB,WAAW,WAAmB,SAAiB,GAAyB;CACtF,OAAO,iBACL,UAAU,QAAQ,MAAM,EAAE,cAAc,GACxC,YAAY,MAAM,EAAE,YAAY,CAClC;AACF;;AAGA,SAAgB,OAAO,OAAgB,OAAgB,KAAa,GAA0B;CAC5F,MAAM,QAAQ,CAAC,GAAG,MAAM,MAAM,QAAQ,MAAM,MAAM,KAAK,GAAG,KAAK;CAC/D,MAAM,OAAgB;EAAE,GAAG;EAAO;GAAQ,QAAQ;CAAI;CACtD,IAAI,UAAU,QAAQ,KAAK,cAAc;CAGzC,MAAM,SAAS,MAAM,MAAM,MAAM,MAAM,KAAK;CAE5C,MAAM,OAAO,YAAY,KAAK,MAAM,EAAE,cAAc;CACpD,MAAM,KAAK,YAAY,KAAK,IAAI,EAAE,YAAY;CAC9C,MAAM,OAAO,UAAU,KAAK,WAAW;CAEvC,QAAQ,QAAR;EACE,KAAK;GACH,KAAK,KACH,SAAS,QAAQ,SAAS,OACtB,eAAe,cAAc,MAAM,MAAM,CAAC,GAAG,EAAE,YAAY,IAC3D;GACN;EACF,KAAK;GACH,KAAK,OACH,OAAO,QAAQ,SAAS,OACpB,eAAe,cAAc,IAAI,MAAM,CAAC,GAAG,EAAE,cAAc,IAC3D;GACN;EACF,KAAK,QACH,IAAI,SAAS,QAAQ,OAAO,QAAQ,OAAO,MAAM,KAAK,IAAI;GACxD,MAAM,IAAI,WAAW,MAAM,IAAI,CAAC;GAChC,KAAK,cAAc,WAAW,GAAA,EAAa;GAC3C,KAAK,OAAO,WAAW,GAAA,CAAe;EACxC,OAAO;GACL,KAAK,OAAO;GACZ,KAAK,cAAc;EACrB;CAGJ;CACA,OAAO;AACT;;AAGA,SAAgB,SACd,OACA,GAC6D;CAC7D,MAAM,OAAO,YAAY,MAAM,MAAM,EAAE,cAAc;CACrD,MAAM,KAAK,YAAY,MAAM,IAAI,EAAE,YAAY;CAC/C,MAAM,OAAO,UAAU,MAAM,WAAW;CACxC,IAAI,SAAS,QAAQ,OAAO,QAAQ,SAAS,QAAQ,QAAQ,MAAM,MAAM,IAAI,OAAO;CACpF,OAAO;EAAE,WAAW;EAAM,SAAS;EAAI,MAAM,WAAW,MAAA,EAAgB;CAAE;AAC5E"}
@@ -0,0 +1,19 @@
1
+ export type SignDisplay = "auto" | "always" | "never";
2
+ /** Exact decimal string for a minor-unit amount: (123456n, 2) -> "1234.56". */
3
+ export declare function minorToDecimal(amount: bigint, minorUnits: number): string;
4
+ /**
5
+ * Parses user input like "1,234.5" or "1234.56" into minor units. Returns null for anything
6
+ * that isn't a plain non-negative decimal with at most `minorUnits` decimals.
7
+ */
8
+ export declare function parseAmount(input: string, minorUnits: number): bigint | null;
9
+ /**
10
+ * Formats an amount the way the app shows money: "+1,234.56 USD", "−42.05 USD", "0.00 USD".
11
+ * Uses a true minus sign (U+2212) so negatives align with plus signs in mono type.
12
+ */
13
+ export declare function formatMoney(amount: bigint, currency: string, minorUnits: number, opts?: {
14
+ sign?: SignDisplay;
15
+ code?: boolean;
16
+ }): string;
17
+ /** Plain input-friendly value without grouping: 123456n, 2 -> "1234.56". */
18
+ export declare function formatAmountInput(amount: bigint | null, minorUnits: number): string;
19
+ //# sourceMappingURL=money.d.ts.map
@@ -0,0 +1,55 @@
1
+ // src/lib/money.ts
2
+ const pow10 = (n) => 10n ** BigInt(n);
3
+ /** Exact decimal string for a minor-unit amount: (123456n, 2) -> "1234.56". */
4
+ function minorToDecimal(amount, minorUnits) {
5
+ const neg = amount < 0n;
6
+ const abs = neg ? -amount : amount;
7
+ if (minorUnits === 0) return `${neg ? "-" : ""}${abs}`;
8
+ const scale = pow10(minorUnits);
9
+ const whole = abs / scale;
10
+ const frac = (abs % scale).toString().padStart(minorUnits, "0");
11
+ return `${neg ? "-" : ""}${whole}.${frac}`;
12
+ }
13
+ /**
14
+ * Parses user input like "1,234.5" or "1234.56" into minor units. Returns null for anything
15
+ * that isn't a plain non-negative decimal with at most `minorUnits` decimals.
16
+ */
17
+ function parseAmount(input, minorUnits) {
18
+ const s = input.trim().replace(/,/g, "");
19
+ if (!/^\d+(\.\d*)?$|^\.\d+$/.test(s)) return null;
20
+ const [whole = "0", frac = ""] = s.split(".");
21
+ if (frac.length > minorUnits) return null;
22
+ return BigInt(whole || "0") * pow10(minorUnits) + BigInt(frac.padEnd(minorUnits, "0") || "0");
23
+ }
24
+ const formatters = /* @__PURE__ */ new Map();
25
+ function formatter(minorUnits) {
26
+ let f = formatters.get(minorUnits);
27
+ if (!f) {
28
+ f = new Intl.NumberFormat("en-US", {
29
+ minimumFractionDigits: minorUnits,
30
+ maximumFractionDigits: minorUnits,
31
+ signDisplay: "never"
32
+ });
33
+ formatters.set(minorUnits, f);
34
+ }
35
+ return f;
36
+ }
37
+ /**
38
+ * Formats an amount the way the app shows money: "+1,234.56 USD", "−42.05 USD", "0.00 USD".
39
+ * Uses a true minus sign (U+2212) so negatives align with plus signs in mono type.
40
+ */
41
+ function formatMoney(amount, currency, minorUnits, opts = {}) {
42
+ const { sign = "auto", code = true } = opts;
43
+ const digits = formatter(minorUnits).format(minorToDecimal(amount < 0n ? -amount : amount, minorUnits));
44
+ let prefix = "";
45
+ if (amount < 0n && sign !== "never") prefix = "−";
46
+ else if (amount > 0n && sign === "always") prefix = "+";
47
+ return `${prefix}${digits}${code ? ` ${currency}` : ""}`;
48
+ }
49
+ /** Plain input-friendly value without grouping: 123456n, 2 -> "1234.56". */
50
+ function formatAmountInput(amount, minorUnits) {
51
+ return amount === null ? "" : minorToDecimal(amount, minorUnits);
52
+ }
53
+ export { formatAmountInput, formatMoney, minorToDecimal, parseAmount };
54
+
55
+ //# sourceMappingURL=money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.js","names":[],"sources":["../../src/lib/money.ts"],"sourcesContent":["// Money helpers. Amounts are integer minor units (bigint, matching protobuf int64) plus the\n// currency's number of decimal places. Never use floats for money.\n\nexport type SignDisplay = \"auto\" | \"always\" | \"never\";\n\nconst pow10 = (n: number) => 10n ** BigInt(n);\n\n/** Exact decimal string for a minor-unit amount: (123456n, 2) -> \"1234.56\". */\nexport function minorToDecimal(amount: bigint, minorUnits: number): string {\n const neg = amount < 0n;\n const abs = neg ? -amount : amount;\n if (minorUnits === 0) return `${neg ? \"-\" : \"\"}${abs}`;\n const scale = pow10(minorUnits);\n const whole = abs / scale;\n const frac = (abs % scale).toString().padStart(minorUnits, \"0\");\n return `${neg ? \"-\" : \"\"}${whole}.${frac}`;\n}\n\n/**\n * Parses user input like \"1,234.5\" or \"1234.56\" into minor units. Returns null for anything\n * that isn't a plain non-negative decimal with at most `minorUnits` decimals.\n */\nexport function parseAmount(input: string, minorUnits: number): bigint | null {\n const s = input.trim().replace(/,/g, \"\");\n if (!/^\\d+(\\.\\d*)?$|^\\.\\d+$/.test(s)) return null;\n const [whole = \"0\", frac = \"\"] = s.split(\".\");\n if (frac.length > minorUnits) return null;\n return BigInt(whole || \"0\") * pow10(minorUnits) + BigInt(frac.padEnd(minorUnits, \"0\") || \"0\");\n}\n\nconst formatters = new Map<number, Intl.NumberFormat>();\nfunction formatter(minorUnits: number) {\n let f = formatters.get(minorUnits);\n if (!f) {\n f = new Intl.NumberFormat(\"en-US\", {\n minimumFractionDigits: minorUnits,\n maximumFractionDigits: minorUnits,\n signDisplay: \"never\",\n });\n formatters.set(minorUnits, f);\n }\n return f;\n}\n\n/**\n * Formats an amount the way the app shows money: \"+1,234.56 USD\", \"−42.05 USD\", \"0.00 USD\".\n * Uses a true minus sign (U+2212) so negatives align with plus signs in mono type.\n */\nexport function formatMoney(\n amount: bigint,\n currency: string,\n minorUnits: number,\n opts: { sign?: SignDisplay; code?: boolean } = {},\n): string {\n const { sign = \"auto\", code = true } = opts;\n // Intl formats decimal strings exactly (no float rounding).\n const digits = formatter(minorUnits).format(\n minorToDecimal(amount < 0n ? -amount : amount, minorUnits) as unknown as number,\n );\n let prefix = \"\";\n if (amount < 0n && sign !== \"never\") prefix = \"−\";\n else if (amount > 0n && sign === \"always\") prefix = \"+\";\n return `${prefix}${digits}${code ? ` ${currency}` : \"\"}`;\n}\n\n/** Plain input-friendly value without grouping: 123456n, 2 -> \"1234.56\". */\nexport function formatAmountInput(amount: bigint | null, minorUnits: number): string {\n return amount === null ? \"\" : minorToDecimal(amount, minorUnits);\n}\n"],"mappings":";AAKA,MAAM,SAAS,MAAc,OAAO,OAAO,CAAC;;AAG5C,SAAgB,eAAe,QAAgB,YAA4B;CACzE,MAAM,MAAM,SAAS;CACrB,MAAM,MAAM,MAAM,CAAC,SAAS;CAC5B,IAAI,eAAe,GAAG,OAAO,GAAG,MAAM,MAAM,KAAK;CACjD,MAAM,QAAQ,MAAM,UAAU;CAC9B,MAAM,QAAQ,MAAM;CACpB,MAAM,QAAQ,MAAM,MAAA,CAAO,SAAS,CAAC,CAAC,SAAS,YAAY,GAAG;CAC9D,OAAO,GAAG,MAAM,MAAM,KAAK,MAAM,GAAG;AACtC;;;;;AAMA,SAAgB,YAAY,OAAe,YAAmC;CAC5E,MAAM,IAAI,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,EAAE;CACvC,IAAI,CAAC,wBAAwB,KAAK,CAAC,GAAG,OAAO;CAC7C,MAAM,CAAC,QAAQ,KAAK,OAAO,MAAM,EAAE,MAAM,GAAG;CAC5C,IAAI,KAAK,SAAS,YAAY,OAAO;CACrC,OAAO,OAAO,SAAS,GAAG,IAAI,MAAM,UAAU,IAAI,OAAO,KAAK,OAAO,YAAY,GAAG,KAAK,GAAG;AAC9F;AAEA,MAAM,6BAAa,IAAI,IAA+B;AACtD,SAAS,UAAU,YAAoB;CACrC,IAAI,IAAI,WAAW,IAAI,UAAU;CACjC,IAAI,CAAC,GAAG;EACN,IAAI,IAAI,KAAK,aAAa,SAAS;GACjC,uBAAuB;GACvB,uBAAuB;GACvB,aAAa;EACf,CAAC;EACD,WAAW,IAAI,YAAY,CAAC;CAC9B;CACA,OAAO;AACT;;;;;AAMA,SAAgB,YACd,QACA,UACA,YACA,OAA+C,CAAC,GACxC;CACR,MAAM,EAAE,OAAO,QAAQ,OAAO,SAAS;CAEvC,MAAM,SAAS,UAAU,UAAU,CAAC,CAAC,OACnC,eAAe,SAAS,KAAK,CAAC,SAAS,QAAQ,UAAU,CAC3D;CACA,IAAI,SAAS;CACb,IAAI,SAAS,MAAM,SAAS,SAAS,SAAS;MACzC,IAAI,SAAS,MAAM,SAAS,UAAU,SAAS;CACpD,OAAO,GAAG,SAAS,SAAS,OAAO,IAAI,aAAa;AACtD;;AAGA,SAAgB,kBAAkB,QAAuB,YAA4B;CACnF,OAAO,WAAW,OAAO,KAAK,eAAe,QAAQ,UAAU;AACjE"}