@eifi1/ui-kit 0.15.5 → 0.16.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 (140) hide show
  1. package/README.md +17 -12
  2. package/dist/chart.d.ts +3 -3
  3. package/dist/components/amount-input.d.ts +10 -3
  4. package/dist/components/amount-input.js +165 -156
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/authed-image.d.ts +14 -3
  7. package/dist/components/authed-image.js +32 -23
  8. package/dist/components/authed-image.js.map +1 -1
  9. package/dist/components/button-group.d.ts +3 -3
  10. package/dist/components/calculator.d.ts +3 -3
  11. package/dist/components/copy-button.d.ts +3 -3
  12. package/dist/components/facing-pair.d.ts +3 -3
  13. package/dist/components/file-button.d.ts +3 -3
  14. package/dist/components/file-button.js +67 -31
  15. package/dist/components/file-button.js.map +1 -1
  16. package/dist/components/file-dropzone.d.ts +3 -3
  17. package/dist/components/form-actions.d.ts +4 -4
  18. package/dist/components/form-actions.js +18 -6
  19. package/dist/components/form-actions.js.map +1 -1
  20. package/dist/components/line-items.d.ts +53 -2
  21. package/dist/components/line-items.js +73 -7
  22. package/dist/components/line-items.js.map +1 -1
  23. package/dist/components/list.d.ts +99 -5
  24. package/dist/components/list.js +103 -38
  25. package/dist/components/list.js.map +1 -1
  26. package/dist/components/loading-state.d.ts +19 -3
  27. package/dist/components/loading-state.js +14 -3
  28. package/dist/components/loading-state.js.map +1 -1
  29. package/dist/components/money-field.d.ts +3 -3
  30. package/dist/components/number-field.d.ts +3 -3
  31. package/dist/components/number-input.d.ts +3 -3
  32. package/dist/components/number-input.js +4 -2
  33. package/dist/components/number-input.js.map +1 -1
  34. package/dist/components/numpad-sheet.d.ts +3 -3
  35. package/dist/components/passkeys-setting.d.ts +30 -10
  36. package/dist/components/passkeys-setting.js +19 -11
  37. package/dist/components/passkeys-setting.js.map +1 -1
  38. package/dist/components/progress-bar.d.ts +12 -3
  39. package/dist/components/progress-bar.js +21 -11
  40. package/dist/components/progress-bar.js.map +1 -1
  41. package/dist/components/series-chart.d.ts +3 -3
  42. package/dist/components/settings-fields.d.ts +3 -3
  43. package/dist/components/signed-amount.d.ts +28 -4
  44. package/dist/components/signed-amount.js +17 -8
  45. package/dist/components/signed-amount.js.map +1 -1
  46. package/dist/components/stat-tile.d.ts +31 -2
  47. package/dist/components/stat-tile.js +37 -6
  48. package/dist/components/stat-tile.js.map +1 -1
  49. package/dist/components/status-dot.d.ts +4 -1
  50. package/dist/components/status-dot.js +20 -1
  51. package/dist/components/status-dot.js.map +1 -1
  52. package/dist/components/swatch-picker.d.ts +3 -1
  53. package/dist/components/swatch-picker.js.map +1 -1
  54. package/dist/components/table.d.ts +18 -13
  55. package/dist/components/table.js +20 -5
  56. package/dist/components/table.js.map +1 -1
  57. package/dist/components/text-link.d.ts +3 -3
  58. package/dist/components/time-input.d.ts +3 -3
  59. package/dist/components/toggle-legend.d.ts +12 -1
  60. package/dist/components/toggle-legend.js +6 -2
  61. package/dist/components/toggle-legend.js.map +1 -1
  62. package/dist/components/ui.d.ts +3 -3
  63. package/dist/components/ui.js +149 -45
  64. package/dist/components/ui.js.map +1 -1
  65. package/dist/feedback/feedback-attachment.d.ts +1 -1
  66. package/dist/feedback/feedback-attachment.js +24 -11
  67. package/dist/feedback/feedback-attachment.js.map +1 -1
  68. package/dist/feedback/feedback-dialog.d.ts +1 -1
  69. package/dist/feedback/feedback-dialog.js +6 -5
  70. package/dist/feedback/feedback-dialog.js.map +1 -1
  71. package/dist/feedback/feedback-inbox.d.ts +1 -1
  72. package/dist/feedback/feedback-thread.d.ts +1 -1
  73. package/dist/{feedback-attachment-BYAHJMOr.d.ts → feedback-attachment-WFiXJ8We.d.ts} +21 -8
  74. package/dist/feedback.d.ts +1 -1
  75. package/dist/hooks/use-file-drop.d.ts +3 -3
  76. package/dist/i18n/defaults.d.ts +3 -3
  77. package/dist/i18n/kit-labels.d.ts +2 -2
  78. package/dist/i18n/locales/de-CH-informal.d.ts +3 -3
  79. package/dist/i18n/locales/de-CH.d.ts +3 -3
  80. package/dist/i18n/locales/de-informal.d.ts +3 -3
  81. package/dist/i18n/locales/de.d.ts +3 -3
  82. package/dist/i18n/locales/de.js +1 -1
  83. package/dist/i18n/locales/de.js.map +1 -1
  84. package/dist/i18n/locales/es.d.ts +3 -3
  85. package/dist/i18n/locales/es.js +1 -1
  86. package/dist/i18n/locales/es.js.map +1 -1
  87. package/dist/i18n/locales/fr.d.ts +3 -3
  88. package/dist/i18n/locales/fr.js +1 -1
  89. package/dist/i18n/locales/fr.js.map +1 -1
  90. package/dist/i18n/locales/hu.d.ts +3 -3
  91. package/dist/i18n/locales/hu.js +1 -1
  92. package/dist/i18n/locales/hu.js.map +1 -1
  93. package/dist/i18n/locales/it.d.ts +3 -3
  94. package/dist/i18n/locales/it.js +1 -1
  95. package/dist/i18n/locales/it.js.map +1 -1
  96. package/dist/i18n/locales/zh.d.ts +3 -3
  97. package/dist/i18n/locales/zh.js +1 -1
  98. package/dist/i18n/locales/zh.js.map +1 -1
  99. package/dist/index.d.ts +10 -10
  100. package/dist/{kit-labels-BD2F7sOz.d.ts → kit-labels-DE--LmCk.d.ts} +153 -12
  101. package/dist/lib/format.d.ts +18 -3
  102. package/dist/lib/format.js +2 -1
  103. package/dist/lib/format.js.map +1 -1
  104. package/dist/rhf/fields.d.ts +3 -3
  105. package/dist/rhf/form.d.ts +3 -3
  106. package/dist/rhf/line-items.js.map +1 -1
  107. package/dist/rhf.d.ts +3 -3
  108. package/dist/shell/app-shell.d.ts +3 -3
  109. package/dist/shell/top-bar-brand.d.ts +3 -3
  110. package/dist/shell.d.ts +3 -3
  111. package/dist/wizard/stepper-nav.d.ts +3 -3
  112. package/dist/wizard.d.ts +3 -3
  113. package/package.json +1 -1
  114. package/src/components/amount-input.tsx +39 -15
  115. package/src/components/authed-image.tsx +20 -2
  116. package/src/components/file-button.tsx +85 -27
  117. package/src/components/form-actions.tsx +86 -8
  118. package/src/components/line-items.tsx +131 -14
  119. package/src/components/list.tsx +240 -50
  120. package/src/components/loading-state.tsx +35 -4
  121. package/src/components/number-input.tsx +18 -2
  122. package/src/components/passkeys-setting.tsx +45 -18
  123. package/src/components/progress-bar.tsx +46 -21
  124. package/src/components/signed-amount.tsx +47 -8
  125. package/src/components/stat-tile.tsx +76 -6
  126. package/src/components/status-dot.tsx +31 -0
  127. package/src/components/swatch-picker.tsx +3 -1
  128. package/src/components/table.tsx +42 -15
  129. package/src/components/toggle-legend.tsx +21 -4
  130. package/src/components/ui.tsx +273 -63
  131. package/src/feedback/feedback-attachment.tsx +45 -14
  132. package/src/feedback/feedback-dialog.tsx +31 -16
  133. package/src/i18n/locales/de.ts +1 -1
  134. package/src/i18n/locales/es.ts +1 -1
  135. package/src/i18n/locales/fr.ts +1 -1
  136. package/src/i18n/locales/hu.ts +1 -1
  137. package/src/i18n/locales/it.ts +1 -1
  138. package/src/i18n/locales/zh.ts +1 -1
  139. package/src/lib/format.ts +24 -3
  140. package/src/rhf/line-items.tsx +6 -0
@@ -24,7 +24,7 @@ import { SwatchPickerLabels } from './components/swatch-picker.js';
24
24
  import { IconPickerLabels } from './components/icon-picker.js';
25
25
  import { DialogFrameLabels } from './components/dialog-frame.js';
26
26
  import { MeasuredGridLabels } from './components/measured-grid.js';
27
- import { f as FeedbackAttachmentFieldLabels, q as FeedbackDialogTextLabels } from './feedback-attachment-BYAHJMOr.js';
27
+ import { f as FeedbackAttachmentFieldLabels, q as FeedbackDialogTextLabels } from './feedback-attachment-WFiXJ8We.js';
28
28
  import { FeedbackThreadLabels, FeedbackComposerLabels } from './feedback/feedback-thread.js';
29
29
  import { AccountSettingsLabels } from './components/account-settings-labels.js';
30
30
  import { ConfirmDialogLabels } from './components/confirm-dialog.js';
@@ -33,6 +33,7 @@ import { BulkActionBarLabels } from './components/bulk-action-bar.js';
33
33
  import { ListLabels } from './components/list.js';
34
34
  import { BreadcrumbsLabels } from './components/breadcrumbs.js';
35
35
  import { ToastLabels } from './components/toast.js';
36
+ import { LucideIcon } from 'lucide-react';
36
37
  import { DescriptionListLabels } from './components/description-list.js';
37
38
  import { LineItemsLabels } from './components/line-items.js';
38
39
  import { ProgressBarLabels } from './components/progress-bar.js';
@@ -376,6 +377,36 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
376
377
  tooltipSide?: TooltipSide;
377
378
  /** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */
378
379
  tooltipPortal?: boolean;
380
+ /**
381
+ * Mount the `label` bubble only while it is up — {@link Tooltip}'s `lazy`. Default
382
+ * `true` since 0.16.0; `false` is the always-mounted in-place bubble of before.
383
+ *
384
+ * Why the label bubble can be lazy when Tooltip's default is not: this bubble
385
+ * describes nothing. It is wrapped round the button in a fragment precisely so that
386
+ * it is NOT the button's `aria-describedby` (it would repeat the button's own name),
387
+ * so there is no description for a lazy mount to take away — the name is the
388
+ * `aria-label` either way, read in browse mode and on focus alike. What the
389
+ * always-mounted bubble did do was sit in the DOM as a `role="tooltip"` node full of
390
+ * text: every IconButton in a row added one to `getAllByRole("tooltip")` and its label
391
+ * to the row's `textContent`, and two keksdose tests tripped over that (0.15.5 P6).
392
+ * The cost is the one {@link Tooltip}'s "Lazy in place" note names — a test that
393
+ * looked the bubble up without hovering now has to hover (or focus) first; pass
394
+ * `false` while it is migrated. A `disabledReason` bubble is unaffected: that one
395
+ * stays mounted as it was.
396
+ */
397
+ tooltipLazy?: boolean;
398
+ /**
399
+ * Busy — the action is in flight: the glyph is swapped for a spinner of the same
400
+ * size, the button is `aria-busy`, and clicks (and the Enter/Space and form
401
+ * submission they stand for) are swallowed, so a second press cannot start the
402
+ * action twice. {@link Button}'s `pending`, with the same contract: `aria-disabled`
403
+ * rather than `disabled`, so the button that was pressed keeps its focus. The name
404
+ * (`label` / `aria-label`) stays — `aria-busy` is what says it is working. A SWAP
405
+ * rather than Button's overlay: the box is square and fixed, so there is no width
406
+ * for the glyph to hold, and a spinner drawn over a glyph would be two marks in one
407
+ * 20px square.
408
+ */
409
+ pending?: boolean;
379
410
  /**
380
411
  * Why the action is not available — {@link Button}'s `disabledReason`, on the icon
381
412
  * button: keksdose's accounts page cannot hide an account with a balance or delete
@@ -422,7 +453,7 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
422
453
  * `disabledStyle` are a button's and are typed out: a link is not a toggle, and a link
423
454
  * that explains why it cannot be followed is a job for a disabled BUTTON.
424
455
  */
425
- interface IconButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "type" | "href">, Pick<IconButtonProps, "variant" | "size" | "glyphSize" | "badge" | "stretch" | "shape" | "toneColor" | "tone" | "quiet" | "stopPropagation" | "label" | "tooltip" | "tooltipSide" | "tooltipPortal"> {
456
+ interface IconButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "type" | "href">, Pick<IconButtonProps, "variant" | "size" | "glyphSize" | "badge" | "stretch" | "shape" | "toneColor" | "tone" | "quiet" | "stopPropagation" | "label" | "tooltip" | "tooltipSide" | "tooltipPortal" | "tooltipLazy"> {
426
457
  href: string;
427
458
  /** See {@link ButtonLinkProps.renderLink}. */
428
459
  renderLink?: KitLinkComponent;
@@ -440,6 +471,7 @@ interface IconButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElemen
440
471
  pressed?: never;
441
472
  disabledReason?: never;
442
473
  disabledStyle?: never;
474
+ pending?: never;
443
475
  }
444
476
  /**
445
477
  * `toneColor`: one CSS colour, or one per theme.
@@ -664,7 +696,9 @@ interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "siz
664
696
  * TEXT (a string or a number) is a caption instead, and goes UNDER the field
665
697
  * like every other field's hint, attached through `aria-describedby`: the label
666
698
  * line is 11px of strip shared with the label, and a sentence placed there was
667
- * set in the value's type on top of both the label and the value. */
699
+ * set in the value's type on top of both the label and the value. On an
700
+ * UNLABELLED Select (no label line) a FieldHint sits at the field's end edge,
701
+ * outside the box. */
668
702
  hint?: ReactNode;
669
703
  /**
670
704
  * `"sm"`: a 28px, 12px-type select for a toolbar or a table header, where the
@@ -722,6 +756,13 @@ interface CardProps extends ComponentPropsWithoutRef<"div"> {
722
756
  * Left out, the default card keeps no padding (its CardHeader / CardContent own the
723
757
  * rhythm) and `inset` keeps its `p-3`; passing it sets theirs too. A caller's `p-*`
724
758
  * in `className` still wins.
759
+ *
760
+ * Whenever the card carries a padding — this prop, or the `inset` / `outline` one —
761
+ * CardHeader, CardContent and CardFooter drop their own `px-6` / `pt-6` / `pb-6`, so
762
+ * the card's padding is the only one. Before 0.16 they kept it regardless and a
763
+ * `padding="md"` card with parts was padded twice (40px a side); keksdose's settings
764
+ * cards zeroed all three parts by hand (`<CardHeader className="p-0">`). A padding set
765
+ * only through `className` cannot be seen from the parts: use the prop.
725
766
  */
726
767
  padding?: "none" | "sm" | "md";
727
768
  /**
@@ -735,21 +776,56 @@ interface CardProps extends ComponentPropsWithoutRef<"div"> {
735
776
  * selectors.
736
777
  */
737
778
  tone?: CardTone;
779
+ /**
780
+ * How loud the `tone` frame is. `"strong"`: a 2px border in the tone's strong
781
+ * colour — `--danger-border-strong` for `danger`, the tone's own text colour
782
+ * (`--warning`, `--info`, `--success`) for the others, which have no
783
+ * `-border-strong` token of their own. For the one tile on a page that guards
784
+ * something destructive or security-relevant: keksdose's password / delete-account
785
+ * tile (shared/components/password-danger-card.tsx) wrote `toneFrameClass(tone)` over
786
+ * the Card by hand for the 2px rule, and `p-[15px]` under it so the content did not
787
+ * move. Here the extra pixel comes off the padding the same way (`padding` or the
788
+ * variant's own), so a strong card's content sits exactly where a soft one's does.
789
+ *
790
+ * Default `"soft"`: the 1px `-border` it has always been. No effect without `tone`,
791
+ * nor on `inset`, which has no border to make louder (its tone is a fill).
792
+ */
793
+ toneStrength?: "soft" | "strong";
738
794
  }
739
795
  type CardTone = "warning" | "danger" | "info" | "success";
740
- declare function Card({ className, children, flush, variant, tone, padding, ...rest }: CardProps): react.JSX.Element;
796
+ declare function Card({ className, children, flush, variant, tone, toneStrength, padding, ...rest }: CardProps): react.JSX.Element;
741
797
  /** The sub-parts add nothing to a `<div>`'s props — they are the SAME element with a
742
- * `data-slot` and a padding rhythm — so each name is an alias rather than an empty
798
+ * `data-slot` and a padding rhythm — so most names are aliases rather than an empty
743
799
  * interface pretending to be more. They exist so a consumer's own wrapper can say
744
800
  * `CardHeaderProps` instead of `ComponentProps<typeof CardHeader>`. */
745
- type CardHeaderProps = ComponentPropsWithoutRef<"div">;
746
- type CardTitleProps = ComponentPropsWithoutRef<"div">;
801
+ interface CardHeaderProps extends ComponentPropsWithoutRef<"div"> {
802
+ /**
803
+ * Below the `sm` breakpoint, put the {@link CardAction} UNDER the title and
804
+ * description instead of beside them. For an action that is wider than an icon —
805
+ * a "Set up two-factor" button, a checkbox and a filter — which, kept in the
806
+ * top-end column on a 390px phone, squeezed the title into a ribbon two words wide
807
+ * (keksdose's settings cards). Off by default: an icon action (the wizard summary's
808
+ * pencil) is narrow enough to stay beside the title at every width.
809
+ */
810
+ stackAction?: boolean;
811
+ }
812
+ type CardTitleLevel = "div" | "h2" | "h3" | "h4";
813
+ interface CardTitleProps extends ComponentPropsWithoutRef<"div"> {
814
+ /**
815
+ * The element the title is: `"div"` (default) or a heading level. Five of keksdose's
816
+ * settings cards nested their own `<h2>` / `<h3>` INSIDE CardTitle to put the card in
817
+ * the page's outline — a heading in a div that looks like a heading. The heading
818
+ * takes the title's look unchanged (the reset leaves `h2`–`h4` at inherited size and
819
+ * weight), so only the outline changes.
820
+ */
821
+ as?: CardTitleLevel;
822
+ }
747
823
  type CardDescriptionProps = ComponentPropsWithoutRef<"div">;
748
824
  type CardActionProps = ComponentPropsWithoutRef<"div">;
749
825
  type CardContentProps = ComponentPropsWithoutRef<"div">;
750
826
  type CardFooterProps = ComponentPropsWithoutRef<"div">;
751
- declare function CardHeader({ className, ...props }: CardHeaderProps): react.JSX.Element;
752
- declare function CardTitle({ className, ...props }: CardTitleProps): react.JSX.Element;
827
+ declare function CardHeader({ className, stackAction, ...props }: CardHeaderProps): react.JSX.Element;
828
+ declare function CardTitle({ className, as: Tag, ...props }: CardTitleProps): react.JSX.Element;
753
829
  declare function CardDescription({ className, ...props }: CardDescriptionProps): react.JSX.Element;
754
830
  declare function CardAction({ className, ...props }: CardActionProps): react.JSX.Element;
755
831
  declare function CardContent({ className, ...props }: CardContentProps): react.JSX.Element;
@@ -1233,6 +1309,15 @@ interface FileButtonProps extends Omit<UseFilePickerOptions, "disabled">, Omit<B
1233
1309
  * Off by default: a button that silently takes drops is a surprise on a page that
1234
1310
  * has a real drop target elsewhere. */
1235
1311
  droppable?: boolean;
1312
+ /**
1313
+ * Show what was picked beside the button: the file's name, or "3 files selected"
1314
+ * (`filePicker.selected`) for several — a native `<input type="file">` says this,
1315
+ * and a bare button that takes a file and says nothing leaves the user to trust it.
1316
+ * The read-out is a polite live region and describes the button, so it is heard
1317
+ * when it changes and again on focus. It holds the last ACCEPTED pick: a refused
1318
+ * file leaves it as it was. Off by default, so the button's layout is unchanged.
1319
+ */
1320
+ showFileName?: boolean;
1236
1321
  }
1237
1322
  /**
1238
1323
  * {@link useFilePicker} behind a {@link Button}: `variant` and every other button prop
@@ -1330,6 +1415,22 @@ type FormActionsAlign = "start" | "center" | "end" | "between";
1330
1415
  * FullBleedDialog's `footer`), which already pads and rules it.
1331
1416
  */
1332
1417
  type FormActionsPlacement = "inline" | "sticky" | "dialog";
1418
+ /**
1419
+ * What a `sticky` row sticks to — see {@link FormActionsProps.stickyWithin}.
1420
+ * - `viewport` (default): the page's (or AppShell pane's) bottom edge, lifted onto
1421
+ * the phone's bottom nav by `--app-nav-h`.
1422
+ * - `container`: the bottom of whatever scroll container it is in — a dialog body, a
1423
+ * side pane, a panel with `overflow-y-auto` — at `bottom: 0`, with no nav offset.
1424
+ */
1425
+ type FormActionsStickyWithin = "viewport" | "container";
1426
+ /**
1427
+ * Extra attributes for the save button — an `id`, `data-*` hooks for tests or
1428
+ * analytics, an `aria-describedby`. The props FormActions drives itself (type, click,
1429
+ * disabled, variant, form, children, the busy state) are not among them.
1430
+ */
1431
+ type FormActionsSubmitProps = Omit<ButtonProps, "type" | "onClick" | "disabled" | "disabledReason" | "variant" | "form" | "children" | "aria-busy"> & {
1432
+ [key: `data-${string}`]: string | number | boolean | undefined;
1433
+ };
1333
1434
  /** The start-side action of {@link FormActionsProps.destructive}, as data. */
1334
1435
  interface FormActionsDestructive {
1335
1436
  label: ReactNode;
@@ -1353,10 +1454,21 @@ interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
1353
1454
  cancelLabel?: ReactNode;
1354
1455
  /** The save button's text while `pending` ("Saving…"). Default: `submitLabel`. */
1355
1456
  pendingLabel?: ReactNode;
1457
+ /**
1458
+ * A Lucide icon before the save button's text. While `pending` the spinner takes
1459
+ * ITS place rather than being added in front of it — an icon written into
1460
+ * `submitLabel` itself stays, so the button would show the spinner AND the glyph,
1461
+ * two statuses side by side (keksdose's crop Apply needed a `pendingLabel` copy of
1462
+ * its text only to drop the check mark).
1463
+ */
1464
+ submitIcon?: LucideIcon;
1465
+ /** Attributes passed through to the save button: `id`, `data-*`, `aria-*`, a
1466
+ * `className`. See {@link FormActionsSubmitProps}. */
1467
+ submitProps?: FormActionsSubmitProps;
1356
1468
  /**
1357
1469
  * The save is running: a spinner in the save button, `aria-busy` on it, and the
1358
1470
  * button disabled, so a second click cannot send the form twice. Cancel stays
1359
- * usable.
1471
+ * usable. The spinner goes in front of the text, or in place of {@link submitIcon}.
1360
1472
  */
1361
1473
  pending?: boolean;
1362
1474
  /** Disable save for a reason of the form's own (an unbalanced entry). */
@@ -1373,10 +1485,39 @@ interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
1373
1485
  * is `between`-aligned unless `align` says otherwise.
1374
1486
  */
1375
1487
  destructive?: ReactNode | FormActionsDestructive;
1488
+ /**
1489
+ * Something neutral at the START of the row — a caption ("Last saved 2 min ago",
1490
+ * "Drag the corners to crop"), a "View activity" link. After {@link destructive}
1491
+ * when both are given; like it, makes the row `between`-aligned unless `align` says
1492
+ * otherwise. `children` are for further ACTIONS, beside Cancel.
1493
+ */
1494
+ start?: ReactNode;
1376
1495
  /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */
1377
1496
  align?: FormActionsAlign;
1378
1497
  /** See {@link FormActionsPlacement}. Default `inline`. */
1379
1498
  placement?: FormActionsPlacement;
1499
+ /**
1500
+ * With `placement="sticky"`: what the row sticks to. Default `viewport`.
1501
+ *
1502
+ * CSS sticky always sticks to the NEAREST scroll container, so the default row
1503
+ * already holds inside a scrolling pane — but it offsets itself by `--app-nav-h`,
1504
+ * which AppShell publishes on the document: inside a pane or a portalled dialog
1505
+ * body on a phone it then floats a nav's height above that container's bottom,
1506
+ * with the content scrolling through the gap. `container` drops that offset.
1507
+ *
1508
+ * What neither value can fix, because it is the layout, not the row:
1509
+ * - sticky moves only within its PARENT box. Inside a DataTable expansion row the
1510
+ * parent is the expansion cell, so the row pins to the table's own scroller
1511
+ * (`overflow-auto`, bounded by `maxBodyHeight`) and only while the cell spans
1512
+ * that scroller's bottom edge — not to the page.
1513
+ * - any `overflow` other than `visible`/`clip` between the row and the scroller
1514
+ * you mean (an `overflow-x-auto` wrapper, an `overflow-hidden` card) becomes the
1515
+ * scroll container; if THAT box does not scroll, the row never sticks.
1516
+ * - the offset is resolved against the scroller's content box, so a scroller with
1517
+ * bottom padding leaves that strip uncovered; give the row a negative `bottom`
1518
+ * through `style` and the same amount back as padding.
1519
+ */
1520
+ stickyWithin?: FormActionsStickyWithin;
1380
1521
  /** The `id` of the `<form>` the save button submits, when the row is rendered
1381
1522
  * outside it (a dialog's footer slot). */
1382
1523
  form?: string;
@@ -1398,7 +1539,7 @@ interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
1398
1539
  * The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`
1399
1540
  * so the button's name stays its text; `aria-busy` is what says it is working.
1400
1541
  */
1401
- declare function FormActions({ onCancel, onSubmit, submitLabel, cancelLabel, pendingLabel, pending, submitDisabled, submitDisabledReason, submitVariant, destructive, align, placement, form, children, className, style, ...rest }: FormActionsProps): react.JSX.Element;
1542
+ declare function FormActions({ onCancel, onSubmit, submitLabel, cancelLabel, pendingLabel, submitIcon: SubmitIcon, submitProps, pending, submitDisabled, submitDisabledReason, submitVariant, destructive, start: startSlot, align, placement, stickyWithin, form, children, className, style, ...rest }: FormActionsProps): react.JSX.Element;
1402
1543
 
1403
1544
  /**
1404
1545
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -1755,4 +1896,4 @@ declare function useKitFileLabels(prop?: Partial<FileLabels>): FileLabels;
1755
1896
  */
1756
1897
  declare function missingKitLabels(labels: UiKitLabelOverrides | undefined, reference: UiKitLabels): string[];
1757
1898
 
1758
- export { FIELD_INVALID as $, type AppShellLabels as A, Button as B, type CalculatorLabels as C, type CopyButtonProps as D, type CurrencyLabels as E, DEFAULT_APP_SHELL_LABELS as F, DEFAULT_CALCULATOR_LABELS as G, DEFAULT_COMBOBOX_LABELS as H, DEFAULT_COMMON_LABELS as I, DEFAULT_COPY_BUTTON_LABELS as J, DEFAULT_CURRENCY_LABELS as K, DEFAULT_DATE_PICKER_LABELS as L, DEFAULT_FILE_LABELS as M, DEFAULT_FILE_PICKER_LABELS as N, DEFAULT_FORM_ACTIONS_LABELS as O, DEFAULT_MULTI_SELECT_LABELS as P, DEFAULT_PASSWORD_REVEAL_LABELS as Q, DEFAULT_PICKER_SHEET_LABELS as R, DEFAULT_SWIPEABLE_ROW_LABELS as S, DEFAULT_TABS_LABELS as T, DEFAULT_TOP_BAR_LABELS as U, type DatePickerLabels as V, EmptyState as W, type EmptyStateProps as X, FIELD_BASE as Y, FIELD_DISPLAY as Z, FIELD_FLOATING_PAD as _, type ButtonClassesOptions as a, buttonClasses as a$, FIELD_TRIGGER as a0, FIELD_WRITABLE_LOOK as a1, FLOATING_INPUT_CLASS as a2, FLOATING_LABEL_CLASS as a3, FLOATING_LABEL_STATIC as a4, FieldChevron as a5, type FieldChevronProps as a6, FieldHint as a7, type FieldHintProps as a8, FieldLabel as a9, type KitLinkComponent as aA, type KitLinkProps as aB, Label as aC, type LabelOverride as aD, type LabelProps as aE, type MultiSelectLabels as aF, PHONE_QUERY as aG, type PasswordRevealLabels as aH, type PickerSheetLabels as aI, Select as aJ, type SelectProps as aK, Spinner as aL, type SpinnerProps as aM, type SwipeableRowLabels as aN, type TabItem as aO, Tabs as aP, type TabsLabels as aQ, type TabsProps as aR, Textarea as aS, type TextareaProps as aT, type TopBarLabels as aU, type UiKitLabelOverrides as aV, type UiKitLabels as aW, UiKitProvider as aX, type UiKitProviderProps as aY, type UseFilePickerOptions as aZ, type UseFilePickerReturn as a_, type FieldLabelProps as aa, FileButton as ab, type FileButtonProps as ac, type FileLabels as ad, type FilePickerLabels as ae, type FileRejection as af, type FileRejectionReason as ag, type FileScreenOptions as ah, FloatingField as ai, type FloatingFieldProps as aj, FormActions as ak, type FormActionsAlign as al, type FormActionsDestructive as am, type FormActionsLabels as an, type FormActionsPlacement as ao, type FormActionsProps as ap, IconButton as aq, type IconButtonGlyphSize as ar, type IconButtonLinkProps as as, type IconButtonProps as at, type IconButtonSize as au, type IconButtonTone as av, type IconButtonToneColor as aw, type IconButtonVariant as ax, Input as ay, type InputProps as az, type ButtonLinkProps as b, formatFileSize as b0, matchesAccept as b1, missingKitLabels as b2, resolvePasswordRevealLabels as b3, useFilePicker as b4, useKitChartTooltipPlacement as b5, useKitFileLabels as b6, useKitLabelOverrides as b7, useKitLabels as b8, useKitLink as b9, useKitLocale as ba, useKitWeekStart as bb, type FilePickHandler as bc, formatAccept as bd, judgePick as be, screenFiles as bf, summariseRejections as bg, type ButtonProps as c, type ButtonSize as d, type ButtonTone as e, type ButtonVariant as f, Card as g, CardAction as h, type CardActionProps as i, CardContent as j, type CardContentProps as k, CardDescription as l, type CardDescriptionProps as m, CardFooter as n, type CardFooterProps as o, CardHeader as p, type CardHeaderProps as q, type CardProps as r, CardTitle as s, type CardTitleProps as t, type CardTone as u, type ChartTooltipPlacement as v, type ComboboxLabels as w, type CommonLabels as x, CopyButton as y, type CopyButtonLabels as z };
1899
+ export { FIELD_FLOATING_PAD as $, type AppShellLabels as A, Button as B, type CalculatorLabels as C, type CopyButtonLabels as D, type CopyButtonProps as E, type CurrencyLabels as F, DEFAULT_APP_SHELL_LABELS as G, DEFAULT_CALCULATOR_LABELS as H, DEFAULT_COMBOBOX_LABELS as I, DEFAULT_COMMON_LABELS as J, DEFAULT_COPY_BUTTON_LABELS as K, DEFAULT_CURRENCY_LABELS as L, DEFAULT_DATE_PICKER_LABELS as M, DEFAULT_FILE_LABELS as N, DEFAULT_FILE_PICKER_LABELS as O, DEFAULT_FORM_ACTIONS_LABELS as P, DEFAULT_MULTI_SELECT_LABELS as Q, DEFAULT_PASSWORD_REVEAL_LABELS as R, DEFAULT_PICKER_SHEET_LABELS as S, DEFAULT_SWIPEABLE_ROW_LABELS as T, DEFAULT_TABS_LABELS as U, DEFAULT_TOP_BAR_LABELS as V, type DatePickerLabels as W, EmptyState as X, type EmptyStateProps as Y, FIELD_BASE as Z, FIELD_DISPLAY as _, type ButtonClassesOptions as a, type UiKitProviderProps as a$, FIELD_INVALID as a0, FIELD_TRIGGER as a1, FIELD_WRITABLE_LOOK as a2, FLOATING_INPUT_CLASS as a3, FLOATING_LABEL_CLASS as a4, FLOATING_LABEL_STATIC as a5, FieldChevron as a6, type FieldChevronProps as a7, FieldHint as a8, type FieldHintProps as a9, type IconButtonVariant as aA, Input as aB, type InputProps as aC, type KitLinkComponent as aD, type KitLinkProps as aE, Label as aF, type LabelOverride as aG, type LabelProps as aH, type MultiSelectLabels as aI, PHONE_QUERY as aJ, type PasswordRevealLabels as aK, type PickerSheetLabels as aL, Select as aM, type SelectProps as aN, Spinner as aO, type SpinnerProps as aP, type SwipeableRowLabels as aQ, type TabItem as aR, Tabs as aS, type TabsLabels as aT, type TabsProps as aU, Textarea as aV, type TextareaProps as aW, type TopBarLabels as aX, type UiKitLabelOverrides as aY, type UiKitLabels as aZ, UiKitProvider as a_, FieldLabel as aa, type FieldLabelProps as ab, FileButton as ac, type FileButtonProps as ad, type FileLabels as ae, type FilePickerLabels as af, type FileRejection as ag, type FileRejectionReason as ah, type FileScreenOptions as ai, FloatingField as aj, type FloatingFieldProps as ak, FormActions as al, type FormActionsAlign as am, type FormActionsDestructive as an, type FormActionsLabels as ao, type FormActionsPlacement as ap, type FormActionsProps as aq, type FormActionsStickyWithin as ar, type FormActionsSubmitProps as as, IconButton as at, type IconButtonGlyphSize as au, type IconButtonLinkProps as av, type IconButtonProps as aw, type IconButtonSize as ax, type IconButtonTone as ay, type IconButtonToneColor as az, type ButtonLinkProps as b, type UseFilePickerOptions as b0, type UseFilePickerReturn as b1, buttonClasses as b2, formatFileSize as b3, matchesAccept as b4, missingKitLabels as b5, resolvePasswordRevealLabels as b6, useFilePicker as b7, useKitChartTooltipPlacement as b8, useKitFileLabels as b9, useKitLabelOverrides as ba, useKitLabels as bb, useKitLink as bc, useKitLocale as bd, useKitWeekStart as be, type FilePickHandler as bf, formatAccept as bg, judgePick as bh, screenFiles as bi, summariseRejections as bj, type ButtonProps as c, type ButtonSize as d, type ButtonTone as e, type ButtonVariant as f, Card as g, CardAction as h, type CardActionProps as i, CardContent as j, type CardContentProps as k, CardDescription as l, type CardDescriptionProps as m, CardFooter as n, type CardFooterProps as o, CardHeader as p, type CardHeaderProps as q, type CardProps as r, CardTitle as s, type CardTitleLevel as t, type CardTitleProps as u, type CardTone as v, type ChartTooltipPlacement as w, type ComboboxLabels as x, type CommonLabels as y, CopyButton as z };
@@ -164,13 +164,28 @@ declare function useKitFormat(locale?: string): KitFormat;
164
164
  * AmountInput settles a typed figure by, for a host that needs it outside the field.
165
165
  */
166
166
  declare function currencyMinorDigits(currency: string | null | undefined): number | undefined;
167
+ /** The options form of {@link roundToCurrency}'s third argument. */
168
+ interface RoundToCurrencyOptions {
169
+ /** Decimals to round to, whatever the currency — the plain `digits` argument. */
170
+ digits?: number;
171
+ /**
172
+ * Decimals for a currency `Intl` cannot resolve — no code yet (`null`, `""`), or a
173
+ * malformed one ("EURO", "€"). Left out, such a value comes back unchanged, as
174
+ * before. Note that `Intl` resolves ANY well-formed three-letter code, a made-up
175
+ * "XYZ" included (to 2), so this is the case of a missing or broken code, not of a
176
+ * rare currency.
177
+ */
178
+ fallbackDigits?: number;
179
+ }
167
180
  /**
168
181
  * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,
169
182
  * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *
170
183
  * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency
171
184
  * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an
172
- * FX estimate set into an AmountInput showed four decimals).
185
+ * FX estimate set into an AmountInput showed four decimals) — unless the options form
186
+ * gives `fallbackDigits`: `roundToCurrency(v, code, { fallbackDigits: 2 })` rounds a
187
+ * figure whose currency is not chosen yet to cents rather than leaving 0.1 + 0.2 as is.
173
188
  */
174
- declare function roundToCurrency(value: number, currency: string | null | undefined, digits?: number): number;
189
+ declare function roundToCurrency(value: number, currency: string | null | undefined, digits?: number | RoundToCurrencyOptions): number;
175
190
 
176
- export { type DateInput, EMPTY_FORMATTED_VALUE, type FormatDateOptions, type FormatDateStyle, type FormatDigits, type FormatMoneyOptions, type FormatNumberBaseOptions, type FormatNumberOptions, type FormatPercentOptions, type FormatRelativeTimeOptions, type KitFormat, currencyMinorDigits, formatDate, formatMoney, formatNumber, formatPercent, formatRelativeTime, roundToCurrency, toDate, useKitFormat };
191
+ export { type DateInput, EMPTY_FORMATTED_VALUE, type FormatDateOptions, type FormatDateStyle, type FormatDigits, type FormatMoneyOptions, type FormatNumberBaseOptions, type FormatNumberOptions, type FormatPercentOptions, type FormatRelativeTimeOptions, type KitFormat, type RoundToCurrencyOptions, currencyMinorDigits, formatDate, formatMoney, formatNumber, formatPercent, formatRelativeTime, roundToCurrency, toDate, useKitFormat };
@@ -147,7 +147,8 @@ function currencyMinorDigits(currency) {
147
147
  }
148
148
  }
149
149
  function roundToCurrency(value, currency, digits) {
150
- const places = digits ?? currencyMinorDigits(currency);
150
+ const options = typeof digits === "object" && digits !== null ? digits : { digits };
151
+ const places = options.digits ?? currencyMinorDigits(currency) ?? options.fallbackDigits;
151
152
  if (places === void 0 || !Number.isFinite(value)) return value;
152
153
  const magnitude = Number(`${Math.round(Number(`${Math.abs(value)}e${places}`))}e-${places}`);
153
154
  return value < 0 ? -magnitude : magnitude;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as\n * `empty` (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or\n * \"Invalid Date\".\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\n/** The options every number formatter shares (money and percent carry their own unit). */\nexport interface FormatNumberBaseOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n}\n\n/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */\nexport interface FormatNumberOptions extends FormatNumberBaseOptions {\n /**\n * A unit printed after the figure, joined by a narrow no-break space (U+202F):\n * \"3.400 N\", \"12 kg\", \"4,5 m²\". lenkbank (P8) prints forces, masses and lengths in\n * every table and summary, and each call site glued `${n} N` together with a plain\n * space — which wraps the \"N\" onto the next line in a narrow column and reads loose\n * beside the digit grouping. The narrow no-break space is the SI brochure's and the\n * Swiss typesetting rule's separator between value and unit, and it never breaks.\n *\n * Free text, not `Intl`'s `style: \"unit\"`: that one only knows the ECMA-402\n * sanctioned units (no \"N\", no \"kN\", no \"m³/h\"), so an app would still be gluing.\n * A missing value prints `empty` ALONE — \"— N\" says there is a newton-shaped\n * nothing, which is noise in a column that already heads its unit.\n */\n unit?: string;\n}\n\n/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */\nconst UNIT_SEPARATOR = \"\\u202F\";\n\nexport interface FormatMoneyOptions extends FormatNumberBaseOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberBaseOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\nfunction isMissing(value: number | null | undefined): value is null | undefined {\n // ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing\n // figure, and \"∞\" printed in a money or measurement column reads like data.\n return value === null || value === undefined || !Number.isFinite(value);\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\" — with `unit`, \"3.400 N\". */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const figure = numberFormatter(options.locale, numberOptions(options)).format(value);\n return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(\n value: number | null | undefined,\n currency: string,\n options: FormatMoneyOptions = {},\n): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n return numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n }).format(value);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? value / 100 : value;\n return numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n }).format(ratio);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. */\n timeZone?: string;\n}\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\nfunction dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (\n value: number | null | undefined,\n currency: string,\n options?: WithoutLocale<FormatMoneyOptions>,\n ) => string;\n formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),\n }),\n [kitLocale],\n );\n}\n\n/* ── Currency minor units ──────────────────────────────────────────────────── */\n\n/**\n * The decimals a currency is written with — its minor unit: CHF and EUR 2, JPY 0,\n * KWD 3 — from `Intl`, or `undefined` for a code `Intl` does not accept. The rule\n * AmountInput settles a typed figure by, for a host that needs it outside the field.\n */\nexport function currencyMinorDigits(currency: string | null | undefined): number | undefined {\n if (!currency) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions().maximumFractionDigits;\n } catch {\n return undefined;\n }\n}\n\n/**\n * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,\n * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *\n * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency\n * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an\n * FX estimate set into an AmountInput showed four decimals).\n */\nexport function roundToCurrency(value: number, currency: string | null | undefined, digits?: number): number {\n const places = digits ?? currencyMinorDigits(currency);\n if (places === undefined || !Number.isFinite(value)) return value;\n const magnitude = Number(`${Math.round(Number(`${Math.abs(value)}e${places}`))}e-${places}`);\n return value < 0 ? -magnitude : magnitude;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as\n * `empty` (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or\n * \"Invalid Date\".\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\n/** The options every number formatter shares (money and percent carry their own unit). */\nexport interface FormatNumberBaseOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n}\n\n/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */\nexport interface FormatNumberOptions extends FormatNumberBaseOptions {\n /**\n * A unit printed after the figure, joined by a narrow no-break space (U+202F):\n * \"3.400 N\", \"12 kg\", \"4,5 m²\". lenkbank (P8) prints forces, masses and lengths in\n * every table and summary, and each call site glued `${n} N` together with a plain\n * space — which wraps the \"N\" onto the next line in a narrow column and reads loose\n * beside the digit grouping. The narrow no-break space is the SI brochure's and the\n * Swiss typesetting rule's separator between value and unit, and it never breaks.\n *\n * Free text, not `Intl`'s `style: \"unit\"`: that one only knows the ECMA-402\n * sanctioned units (no \"N\", no \"kN\", no \"m³/h\"), so an app would still be gluing.\n * A missing value prints `empty` ALONE — \"— N\" says there is a newton-shaped\n * nothing, which is noise in a column that already heads its unit.\n */\n unit?: string;\n}\n\n/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */\nconst UNIT_SEPARATOR = \"\\u202F\";\n\nexport interface FormatMoneyOptions extends FormatNumberBaseOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberBaseOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\nfunction isMissing(value: number | null | undefined): value is null | undefined {\n // ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing\n // figure, and \"∞\" printed in a money or measurement column reads like data.\n return value === null || value === undefined || !Number.isFinite(value);\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\" — with `unit`, \"3.400 N\". */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const figure = numberFormatter(options.locale, numberOptions(options)).format(value);\n return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(\n value: number | null | undefined,\n currency: string,\n options: FormatMoneyOptions = {},\n): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n return numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n }).format(value);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? value / 100 : value;\n return numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n }).format(ratio);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. */\n timeZone?: string;\n}\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\nfunction dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (\n value: number | null | undefined,\n currency: string,\n options?: WithoutLocale<FormatMoneyOptions>,\n ) => string;\n formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),\n }),\n [kitLocale],\n );\n}\n\n/* ── Currency minor units ──────────────────────────────────────────────────── */\n\n/**\n * The decimals a currency is written with — its minor unit: CHF and EUR 2, JPY 0,\n * KWD 3 — from `Intl`, or `undefined` for a code `Intl` does not accept. The rule\n * AmountInput settles a typed figure by, for a host that needs it outside the field.\n */\nexport function currencyMinorDigits(currency: string | null | undefined): number | undefined {\n if (!currency) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions().maximumFractionDigits;\n } catch {\n return undefined;\n }\n}\n\n/** The options form of {@link roundToCurrency}'s third argument. */\nexport interface RoundToCurrencyOptions {\n /** Decimals to round to, whatever the currency — the plain `digits` argument. */\n digits?: number;\n /**\n * Decimals for a currency `Intl` cannot resolve — no code yet (`null`, `\"\"`), or a\n * malformed one (\"EURO\", \"€\"). Left out, such a value comes back unchanged, as\n * before. Note that `Intl` resolves ANY well-formed three-letter code, a made-up\n * \"XYZ\" included (to 2), so this is the case of a missing or broken code, not of a\n * rare currency.\n */\n fallbackDigits?: number;\n}\n\n/**\n * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,\n * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *\n * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency\n * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an\n * FX estimate set into an AmountInput showed four decimals) — unless the options form\n * gives `fallbackDigits`: `roundToCurrency(v, code, { fallbackDigits: 2 })` rounds a\n * figure whose currency is not chosen yet to cents rather than leaving 0.1 + 0.2 as is.\n */\nexport function roundToCurrency(\n value: number,\n currency: string | null | undefined,\n digits?: number | RoundToCurrencyOptions,\n): number {\n const options = typeof digits === \"object\" && digits !== null ? digits : { digits };\n const places = options.digits ?? currencyMinorDigits(currency) ?? options.fallbackDigits;\n if (places === undefined || !Number.isFinite(value)) return value;\n const magnitude = Number(`${Math.round(Number(`${Math.abs(value)}e${places}`))}e-${places}`);\n return value < 0 ? -magnitude : magnitude;\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement } from 'react';
3
3
  import { FieldValues, FieldPath, ControllerProps, ControllerRenderProps, ControllerFieldState } from 'react-hook-form';
4
- import { aK as SelectProps, az as InputProps, aT as TextareaProps } from '../kit-labels-BD2F7sOz.js';
4
+ import { aN as SelectProps, aC as InputProps, aW as TextareaProps } from '../kit-labels-DE--LmCk.js';
5
5
  import { NumberFieldProps } from '../components/number-field.js';
6
6
  import { DatePickerProps } from '../components/date-picker.js';
7
7
  import { CheckboxProps } from '../components/checkbox.js';
@@ -39,7 +39,7 @@ import '../components/dialog-frame.js';
39
39
  import '../components/modal.js';
40
40
  import '../hooks/use-search-param-state.js';
41
41
  import '../components/measured-grid.js';
42
- import '../feedback-attachment-BYAHJMOr.js';
42
+ import '../feedback-attachment-WFiXJ8We.js';
43
43
  import '../feedback/feedback-thread.js';
44
44
  import '../feedback/feedback-inbox.js';
45
45
  import '../components/account-settings-labels.js';
@@ -47,12 +47,12 @@ import '../components/confirm-dialog.js';
47
47
  import '../components/floating-panel.js';
48
48
  import '../components/bulk-action-bar.js';
49
49
  import '../components/list.js';
50
+ import '../components/signed-amount.js';
50
51
  import '../components/breadcrumbs.js';
51
52
  import '../components/toast.js';
52
53
  import '../components/description-list.js';
53
54
  import '../components/line-items.js';
54
55
  import '../components/progress-bar.js';
55
- import '../components/signed-amount.js';
56
56
  import '../components/error-boundary.js';
57
57
  import '../components/authed-image.js';
58
58
  import '../hooks/use-authed-src.js';
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { ReactElement, ComponentProps } from 'react';
3
3
  import * as react_hook_form from 'react-hook-form';
4
4
  import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
5
- import { aE as LabelProps } from '../kit-labels-BD2F7sOz.js';
5
+ import { aH as LabelProps } from '../kit-labels-DE--LmCk.js';
6
6
  import '../components/data-table-labels.js';
7
7
  import '../components/mini-calendar.js';
8
8
  import '../components/calendar-heatmap.js';
@@ -34,7 +34,7 @@ import '../components/dialog-frame.js';
34
34
  import '../components/modal.js';
35
35
  import '../hooks/use-search-param-state.js';
36
36
  import '../components/measured-grid.js';
37
- import '../feedback-attachment-BYAHJMOr.js';
37
+ import '../feedback-attachment-WFiXJ8We.js';
38
38
  import '../feedback/feedback-thread.js';
39
39
  import '../feedback/feedback-inbox.js';
40
40
  import '../components/account-settings-labels.js';
@@ -42,12 +42,12 @@ import '../components/confirm-dialog.js';
42
42
  import '../components/floating-panel.js';
43
43
  import '../components/bulk-action-bar.js';
44
44
  import '../components/list.js';
45
+ import '../components/signed-amount.js';
45
46
  import '../components/breadcrumbs.js';
46
47
  import '../components/toast.js';
47
48
  import '../components/description-list.js';
48
49
  import '../components/line-items.js';
49
50
  import '../components/progress-bar.js';
50
- import '../components/signed-amount.js';
51
51
  import '../components/error-boundary.js';
52
52
  import '../components/authed-image.js';
53
53
  import '../hooks/use-authed-src.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rhf/line-items.tsx"],"sourcesContent":["/**\n * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <RhfLineItems\n * name=\"components\"\n * newItem={() => ({ type: \"net_rent\", amount: null, validFrom: \"\" })}\n * minItems={1}\n * columns={[\n * { key: \"type\", header: \"Type\", render: ({ name, label }) => (\n * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />\n * ) },\n * { key: \"amount\", header: \"Amount\", width: \"8rem\", align: \"end\", render: ({ name, label }) => (\n * <RhfMoneyField name={`${name}.amount`} ariaLabel={label} />\n * ) },\n * ]}\n * totals={{ amount: formatMoney(total) }}\n * />\n * ```\n *\n * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),\n * Add appends `newItem()`, remove is `remove(index)`, and an error on the array\n * itself — the `rules` here, or a resolver's `lines: \"At least two lines\"` — is the\n * list's `error`. Each cell's `render` gets `name`, the row's path\n * (`\"components.2\"`), to bind its field to.\n */\nimport type { ReactNode } from \"react\";\nimport {\n useFieldArray,\n useFormState,\n type FieldArray,\n type FieldArrayPath,\n type FieldArrayWithId,\n type FieldValues,\n type UseFieldArrayProps,\n} from \"react-hook-form\";\nimport {\n LineItems,\n type LineItemCellContext,\n type LineItemsColumn,\n type LineItemsProps,\n} from \"../components/line-items\";\n\n/** A cell's context, with the row's path in the form. */\nexport interface RhfLineItemCellContext<TItem, TArrayName extends string>\n extends LineItemCellContext<TItem> {\n /** The row's path: `${name}.${index}` — bind the cell's field to\n * `` `${name}.amount` ``. */\n name: `${TArrayName}.${number}`;\n}\n\nexport interface RhfLineItemsColumn<TItem, TArrayName extends string>\n extends Omit<LineItemsColumn<TItem>, \"render\"> {\n render: (ctx: RhfLineItemCellContext<TItem, TArrayName>) => ReactNode;\n}\n\nexport interface RhfLineItemsProps<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends Omit<\n LineItemsProps<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>,\n \"items\" | \"columns\" | \"onAdd\" | \"onRemove\" | \"getKey\"\n > {\n name: TArrayName;\n /** `form.control`. Optional under `<Form {...form}>`. */\n control?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"control\"];\n /** `useFieldArray`'s `rules` (`minLength`, `validate` …); its message is the list's\n * error. */\n rules?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"rules\"];\n columns: readonly RhfLineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">, TArrayName>[];\n /** A new row's values. Omitted: no add button. */\n newItem?: () => FieldArray<TFieldValues, TArrayName>;\n /** `false`: no remove buttons. Default `true`. */\n removable?: boolean;\n}\n\n/** The message of an error at `path` in react-hook-form's errors tree: the array's\n * own (`root`, from `rules`) or one a resolver put on the array. */\nfunction arrayError(errors: unknown, path: string): string | undefined {\n let node: unknown = errors;\n for (const key of path.split(\".\")) {\n if (node === null || typeof node !== \"object\") return undefined;\n node = (node as Record<string, unknown>)[key];\n }\n if (node === null || typeof node !== \"object\") return undefined;\n const { root, message } = node as { root?: { message?: unknown }; message?: unknown };\n const text = root?.message ?? message;\n return typeof text === \"string\" && text !== \"\" ? text : undefined;\n}\n\n/** {@link LineItems} bound to a field array. See the module note. */\nexport function RhfLineItems<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n columns,\n newItem,\n removable = true,\n error,\n ...rest\n}: RhfLineItemsProps<TFieldValues, TArrayName, TTransformed>) {\n const { fields, append, remove } = useFieldArray<TFieldValues, TArrayName, \"id\", TTransformed>({\n control,\n name,\n rules,\n });\n const { errors } = useFormState({ control, name: name as never });\n const bound: LineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>[] = columns.map(\n ({ render, ...column }) => ({\n ...column,\n render: (ctx) => render({ ...ctx, name: `${name}.${ctx.index}` as `${TArrayName}.${number}` }),\n }),\n );\n return (\n <LineItems\n {...rest}\n items={fields}\n columns={bound}\n getKey={(field) => field.id}\n onAdd={newItem ? () => append(newItem()) : undefined}\n onRemove={removable ? (index) => remove(index) : undefined}\n error={error ?? arrayError(errors, name)}\n />\n );\n}\n"],"mappings":";AAuHI;AA5FJ;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,OAIK;AAsCP,SAAS,WAAW,QAAiB,MAAkC;AACrE,MAAI,OAAgB;AACpB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,WAAQ,KAAiC,GAAG;AAAA,EAC9C;AACA,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,QAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAM,OAAO,MAAM,WAAW;AAC9B,SAAO,OAAO,SAAS,YAAY,SAAS,KAAK,OAAO;AAC1D;AAGO,SAAS,aAId;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAA8D;AAC5D,QAAM,EAAE,QAAQ,QAAQ,OAAO,IAAI,cAA4D;AAAA,IAC7F;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,EAAE,OAAO,IAAI,aAAa,EAAE,SAAS,KAAoB,CAAC;AAChE,QAAM,QAA6E,QAAQ;AAAA,IACzF,CAAC,EAAE,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC1B,GAAG;AAAA,MACH,QAAQ,CAAC,QAAQ,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,GAAgC,CAAC;AAAA,IAC/F;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ,CAAC,UAAU,MAAM;AAAA,MACzB,OAAO,UAAU,MAAM,OAAO,QAAQ,CAAC,IAAI;AAAA,MAC3C,UAAU,YAAY,CAAC,UAAU,OAAO,KAAK,IAAI;AAAA,MACjD,OAAO,SAAS,WAAW,QAAQ,IAAI;AAAA;AAAA,EACzC;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/rhf/line-items.tsx"],"sourcesContent":["/**\n * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <RhfLineItems\n * name=\"components\"\n * newItem={() => ({ type: \"net_rent\", amount: null, validFrom: \"\" })}\n * minItems={1}\n * columns={[\n * { key: \"type\", header: \"Type\", render: ({ name, label }) => (\n * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />\n * ) },\n * { key: \"amount\", header: \"Amount\", width: \"8rem\", align: \"end\", render: ({ name, label }) => (\n * <RhfMoneyField name={`${name}.amount`} ariaLabel={label} />\n * ) },\n * ]}\n * totals={{ amount: formatMoney(total) }}\n * />\n * ```\n *\n * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),\n * Add appends `newItem()`, remove is `remove(index)`, and an error on the array\n * itself — the `rules` here, or a resolver's `lines: \"At least two lines\"` — is the\n * list's `error`. Each cell's `render` gets `name`, the row's path\n * (`\"components.2\"`), to bind its field to.\n *\n * `summary`, `fieldLabels` and `narrowColumns` pass through to {@link LineItems}. With\n * `fieldLabels=\"floating\"` a cell's `fieldLabel` is the column's name; the bound\n * fields' own `label` is a `FormLabel` ABOVE the control, so for the floating\n * one render the kit field through `RhfField`:\n * `render={({ field, invalid }) => <Input label={fieldLabel} aria-label={label} {...field} invalid={invalid} />}`.\n */\nimport type { ReactNode } from \"react\";\nimport {\n useFieldArray,\n useFormState,\n type FieldArray,\n type FieldArrayPath,\n type FieldArrayWithId,\n type FieldValues,\n type UseFieldArrayProps,\n} from \"react-hook-form\";\nimport {\n LineItems,\n type LineItemCellContext,\n type LineItemsColumn,\n type LineItemsProps,\n} from \"../components/line-items\";\n\n/** A cell's context, with the row's path in the form. */\nexport interface RhfLineItemCellContext<TItem, TArrayName extends string>\n extends LineItemCellContext<TItem> {\n /** The row's path: `${name}.${index}` — bind the cell's field to\n * `` `${name}.amount` ``. */\n name: `${TArrayName}.${number}`;\n}\n\nexport interface RhfLineItemsColumn<TItem, TArrayName extends string>\n extends Omit<LineItemsColumn<TItem>, \"render\"> {\n render: (ctx: RhfLineItemCellContext<TItem, TArrayName>) => ReactNode;\n}\n\nexport interface RhfLineItemsProps<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends Omit<\n LineItemsProps<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>,\n \"items\" | \"columns\" | \"onAdd\" | \"onRemove\" | \"getKey\"\n > {\n name: TArrayName;\n /** `form.control`. Optional under `<Form {...form}>`. */\n control?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"control\"];\n /** `useFieldArray`'s `rules` (`minLength`, `validate` …); its message is the list's\n * error. */\n rules?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"rules\"];\n columns: readonly RhfLineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">, TArrayName>[];\n /** A new row's values. Omitted: no add button. */\n newItem?: () => FieldArray<TFieldValues, TArrayName>;\n /** `false`: no remove buttons. Default `true`. */\n removable?: boolean;\n}\n\n/** The message of an error at `path` in react-hook-form's errors tree: the array's\n * own (`root`, from `rules`) or one a resolver put on the array. */\nfunction arrayError(errors: unknown, path: string): string | undefined {\n let node: unknown = errors;\n for (const key of path.split(\".\")) {\n if (node === null || typeof node !== \"object\") return undefined;\n node = (node as Record<string, unknown>)[key];\n }\n if (node === null || typeof node !== \"object\") return undefined;\n const { root, message } = node as { root?: { message?: unknown }; message?: unknown };\n const text = root?.message ?? message;\n return typeof text === \"string\" && text !== \"\" ? text : undefined;\n}\n\n/** {@link LineItems} bound to a field array. See the module note. */\nexport function RhfLineItems<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n columns,\n newItem,\n removable = true,\n error,\n ...rest\n}: RhfLineItemsProps<TFieldValues, TArrayName, TTransformed>) {\n const { fields, append, remove } = useFieldArray<TFieldValues, TArrayName, \"id\", TTransformed>({\n control,\n name,\n rules,\n });\n const { errors } = useFormState({ control, name: name as never });\n const bound: LineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>[] = columns.map(\n ({ render, ...column }) => ({\n ...column,\n render: (ctx) => render({ ...ctx, name: `${name}.${ctx.index}` as `${TArrayName}.${number}` }),\n }),\n );\n return (\n <LineItems\n {...rest}\n items={fields}\n columns={bound}\n getKey={(field) => field.id}\n onAdd={newItem ? () => append(newItem()) : undefined}\n onRemove={removable ? (index) => remove(index) : undefined}\n error={error ?? arrayError(errors, name)}\n />\n );\n}\n"],"mappings":";AA6HI;AA5FJ;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,OAIK;AAsCP,SAAS,WAAW,QAAiB,MAAkC;AACrE,MAAI,OAAgB;AACpB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,WAAQ,KAAiC,GAAG;AAAA,EAC9C;AACA,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,QAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAM,OAAO,MAAM,WAAW;AAC9B,SAAO,OAAO,SAAS,YAAY,SAAS,KAAK,OAAO;AAC1D;AAGO,SAAS,aAId;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAA8D;AAC5D,QAAM,EAAE,QAAQ,QAAQ,OAAO,IAAI,cAA4D;AAAA,IAC7F;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,EAAE,OAAO,IAAI,aAAa,EAAE,SAAS,KAAoB,CAAC;AAChE,QAAM,QAA6E,QAAQ;AAAA,IACzF,CAAC,EAAE,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC1B,GAAG;AAAA,MACH,QAAQ,CAAC,QAAQ,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,GAAgC,CAAC;AAAA,IAC/F;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ,CAAC,UAAU,MAAM;AAAA,MACzB,OAAO,UAAU,MAAM,OAAO,QAAQ,CAAC,IAAI;AAAA,MAC3C,UAAU,YAAY,CAAC,UAAU,OAAO,KAAK,IAAI;AAAA,MACjD,OAAO,SAAS,WAAW,QAAQ,IAAI;AAAA;AAAA,EACzC;AAEJ;","names":[]}
package/dist/rhf.d.ts CHANGED
@@ -4,7 +4,7 @@ export { RhfCheckbox, RhfCheckboxProps, RhfCombobox, RhfComboboxProps, RhfDateFi
4
4
  export { RhfLineItemCellContext, RhfLineItems, RhfLineItemsColumn, RhfLineItemsProps } from './rhf/line-items.js';
5
5
  import 'react';
6
6
  import 'react-hook-form';
7
- import './kit-labels-BD2F7sOz.js';
7
+ import './kit-labels-DE--LmCk.js';
8
8
  import './components/data-table-labels.js';
9
9
  import './components/mini-calendar.js';
10
10
  import './components/calendar-heatmap.js';
@@ -36,7 +36,7 @@ import './components/dialog-frame.js';
36
36
  import './components/modal.js';
37
37
  import './hooks/use-search-param-state.js';
38
38
  import './components/measured-grid.js';
39
- import './feedback-attachment-BYAHJMOr.js';
39
+ import './feedback-attachment-WFiXJ8We.js';
40
40
  import './feedback/feedback-thread.js';
41
41
  import './feedback/feedback-inbox.js';
42
42
  import './components/account-settings-labels.js';
@@ -44,12 +44,12 @@ import './components/confirm-dialog.js';
44
44
  import './components/floating-panel.js';
45
45
  import './components/bulk-action-bar.js';
46
46
  import './components/list.js';
47
+ import './components/signed-amount.js';
47
48
  import './components/breadcrumbs.js';
48
49
  import './components/toast.js';
49
50
  import './components/description-list.js';
50
51
  import './components/line-items.js';
51
52
  import './components/progress-bar.js';
52
- import './components/signed-amount.js';
53
53
  import './components/error-boundary.js';
54
54
  import './components/authed-image.js';
55
55
  import './hooks/use-authed-src.js';
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  import { LucideIcon } from 'lucide-react';
4
- import { aA as KitLinkComponent } from '../kit-labels-BD2F7sOz.js';
4
+ import { aD as KitLinkComponent } from '../kit-labels-DE--LmCk.js';
5
5
  import '../components/data-table-labels.js';
6
6
  import '../components/mini-calendar.js';
7
7
  import '../components/calendar-heatmap.js';
@@ -32,7 +32,7 @@ import '../components/dialog-frame.js';
32
32
  import '../components/modal.js';
33
33
  import '../hooks/use-search-param-state.js';
34
34
  import '../components/measured-grid.js';
35
- import '../feedback-attachment-BYAHJMOr.js';
35
+ import '../feedback-attachment-WFiXJ8We.js';
36
36
  import '../feedback/feedback-thread.js';
37
37
  import '../feedback/feedback-inbox.js';
38
38
  import '../components/account-settings-labels.js';
@@ -40,12 +40,12 @@ import '../components/confirm-dialog.js';
40
40
  import '../components/floating-panel.js';
41
41
  import '../components/bulk-action-bar.js';
42
42
  import '../components/list.js';
43
+ import '../components/signed-amount.js';
43
44
  import '../components/breadcrumbs.js';
44
45
  import '../components/toast.js';
45
46
  import '../components/description-list.js';
46
47
  import '../components/line-items.js';
47
48
  import '../components/progress-bar.js';
48
- import '../components/signed-amount.js';
49
49
  import '../components/error-boundary.js';
50
50
  import '../components/authed-image.js';
51
51
  import '../hooks/use-authed-src.js';