@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.
- package/README.md +17 -12
- package/dist/chart.d.ts +3 -3
- package/dist/components/amount-input.d.ts +10 -3
- package/dist/components/amount-input.js +165 -156
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +14 -3
- package/dist/components/authed-image.js +32 -23
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/button-group.d.ts +3 -3
- package/dist/components/calculator.d.ts +3 -3
- package/dist/components/copy-button.d.ts +3 -3
- package/dist/components/facing-pair.d.ts +3 -3
- package/dist/components/file-button.d.ts +3 -3
- package/dist/components/file-button.js +67 -31
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -3
- package/dist/components/form-actions.d.ts +4 -4
- package/dist/components/form-actions.js +18 -6
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +53 -2
- package/dist/components/line-items.js +73 -7
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +99 -5
- package/dist/components/list.js +103 -38
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +19 -3
- package/dist/components/loading-state.js +14 -3
- package/dist/components/loading-state.js.map +1 -1
- package/dist/components/money-field.d.ts +3 -3
- package/dist/components/number-field.d.ts +3 -3
- package/dist/components/number-input.d.ts +3 -3
- package/dist/components/number-input.js +4 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -3
- package/dist/components/passkeys-setting.d.ts +30 -10
- package/dist/components/passkeys-setting.js +19 -11
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +12 -3
- package/dist/components/progress-bar.js +21 -11
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +3 -3
- package/dist/components/settings-fields.d.ts +3 -3
- package/dist/components/signed-amount.d.ts +28 -4
- package/dist/components/signed-amount.js +17 -8
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.d.ts +31 -2
- package/dist/components/stat-tile.js +37 -6
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -1
- package/dist/components/status-dot.js +20 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/table.d.ts +18 -13
- package/dist/components/table.js +20 -5
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +3 -3
- package/dist/components/time-input.d.ts +3 -3
- package/dist/components/toggle-legend.d.ts +12 -1
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +3 -3
- package/dist/components/ui.js +149 -45
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +24 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js +6 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +1 -1
- package/dist/{feedback-attachment-BYAHJMOr.d.ts → feedback-attachment-WFiXJ8We.d.ts} +21 -8
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +3 -3
- package/dist/i18n/defaults.d.ts +3 -3
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -3
- package/dist/i18n/locales/de-CH.d.ts +3 -3
- package/dist/i18n/locales/de-informal.d.ts +3 -3
- package/dist/i18n/locales/de.d.ts +3 -3
- package/dist/i18n/locales/de.js +1 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -3
- package/dist/i18n/locales/es.js +1 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -3
- package/dist/i18n/locales/fr.js +1 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -3
- package/dist/i18n/locales/hu.js +1 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -3
- package/dist/i18n/locales/it.js +1 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -3
- package/dist/i18n/locales/zh.js +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/{kit-labels-BD2F7sOz.d.ts → kit-labels-DE--LmCk.d.ts} +153 -12
- package/dist/lib/format.d.ts +18 -3
- package/dist/lib/format.js +2 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -3
- package/dist/rhf/form.d.ts +3 -3
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -3
- package/dist/shell/app-shell.d.ts +3 -3
- package/dist/shell/top-bar-brand.d.ts +3 -3
- package/dist/shell.d.ts +3 -3
- package/dist/wizard/stepper-nav.d.ts +3 -3
- package/dist/wizard.d.ts +3 -3
- package/package.json +1 -1
- package/src/components/amount-input.tsx +39 -15
- package/src/components/authed-image.tsx +20 -2
- package/src/components/file-button.tsx +85 -27
- package/src/components/form-actions.tsx +86 -8
- package/src/components/line-items.tsx +131 -14
- package/src/components/list.tsx +240 -50
- package/src/components/loading-state.tsx +35 -4
- package/src/components/number-input.tsx +18 -2
- package/src/components/passkeys-setting.tsx +45 -18
- package/src/components/progress-bar.tsx +46 -21
- package/src/components/signed-amount.tsx +47 -8
- package/src/components/stat-tile.tsx +76 -6
- package/src/components/status-dot.tsx +31 -0
- package/src/components/swatch-picker.tsx +3 -1
- package/src/components/table.tsx +42 -15
- package/src/components/toggle-legend.tsx +21 -4
- package/src/components/ui.tsx +273 -63
- package/src/feedback/feedback-attachment.tsx +45 -14
- package/src/feedback/feedback-dialog.tsx +31 -16
- package/src/i18n/locales/de.ts +1 -1
- package/src/i18n/locales/es.ts +1 -1
- package/src/i18n/locales/fr.ts +1 -1
- package/src/i18n/locales/hu.ts +1 -1
- package/src/i18n/locales/it.ts +1 -1
- package/src/i18n/locales/zh.ts +1 -1
- package/src/lib/format.ts +24 -3
- 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-
|
|
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
|
|
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
|
-
|
|
746
|
-
|
|
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 {
|
|
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 };
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -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 };
|
package/dist/lib/format.js
CHANGED
|
@@ -147,7 +147,8 @@ function currencyMinorDigits(currency) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
function roundToCurrency(value, currency, digits) {
|
|
150
|
-
const
|
|
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;
|
package/dist/lib/format.js.map
CHANGED
|
@@ -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":[]}
|
package/dist/rhf/fields.d.ts
CHANGED
|
@@ -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 {
|
|
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-
|
|
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';
|
package/dist/rhf/form.d.ts
CHANGED
|
@@ -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 {
|
|
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-
|
|
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":";
|
|
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-
|
|
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-
|
|
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 {
|
|
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-
|
|
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';
|