@eifi1/ui-kit 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -34
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +40 -4
- package/dist/components/amount-input.js +50 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +26 -18
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +18 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +125 -27
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +40 -20
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +281 -0
- package/dist/components/error-boundary.js +302 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +37 -4
- package/dist/components/list.js +35 -14
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +42 -20
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +20 -5
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +5 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +122 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +6 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +238 -0
- package/dist/components/text-link.js +144 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +35 -0
- package/dist/components/toggle-group.js +43 -14
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +53 -3
- package/dist/components/toggle-legend.js +54 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +364 -34
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +21 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +173 -0
- package/dist/feedback/feedback-thread.js +264 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +164 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +161 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +161 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +161 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +161 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +161 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +39 -19
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
- package/dist/lib/format.d.ts +162 -0
- package/dist/lib/format.js +151 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +283 -0
- package/dist/rhf/fields.js +631 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +120 -57
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +21 -4
- package/dist/shell/topbar-action-menu.js +18 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +30 -5
- package/dist/wizard/wizard-step.js +34 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +6 -6
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +109 -10
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +14 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +33 -9
- package/src/components/combobox.tsx +33 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +230 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +25 -7
- package/src/components/error-boundary.tsx +640 -0
- package/src/components/field-sync.tsx +48 -40
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +82 -19
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +65 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +27 -7
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +17 -1
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +10 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +295 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +11 -3
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +356 -0
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +93 -20
- package/src/components/toggle-legend.tsx +108 -2
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +707 -49
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +49 -7
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +510 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +118 -8
- package/src/i18n/locales/de.ts +161 -0
- package/src/i18n/locales/es.ts +158 -0
- package/src/i18n/locales/fr.ts +161 -0
- package/src/i18n/locales/hu.ts +159 -0
- package/src/i18n/locales/it.ts +159 -0
- package/src/i18n/locales/zh.ts +158 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +348 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1135 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +153 -3
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +32 -4
- package/src/shell.ts +2 -0
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +75 -6
- package/tokens.css +12 -0
|
@@ -3,6 +3,19 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
5
5
|
|
|
6
|
+
/** The words a progress bar adds of its own. */
|
|
7
|
+
export interface ProgressBarLabels {
|
|
8
|
+
/** Shown after the value when `max={null}`: a quota with no ceiling. */
|
|
9
|
+
unlimited: string;
|
|
10
|
+
/** The `overage` line: how far past `max` the value is. `amount` is formatted. */
|
|
11
|
+
overLimit: (amount: string) => string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_PROGRESS_BAR_LABELS: ProgressBarLabels = {
|
|
15
|
+
unlimited: "Unlimited",
|
|
16
|
+
overLimit: (amount) => `${amount} over the limit`,
|
|
17
|
+
};
|
|
18
|
+
|
|
6
19
|
/**
|
|
7
20
|
* `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the
|
|
8
21
|
* names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what
|
|
@@ -82,6 +95,13 @@ export interface ProgressBarSegment {
|
|
|
82
95
|
label?: ReactNode;
|
|
83
96
|
/** A stable React key, when `label` is not a string. */
|
|
84
97
|
key?: string;
|
|
98
|
+
/**
|
|
99
|
+
* List it in the `legend` but leave it out of the bar — its value, its share of the
|
|
100
|
+
* `aria-valuetext`, and the sum. keksdose's cut card (reports/cut-card:132) names
|
|
101
|
+
* EVERY bucket under its bar, the empty ones included, while the bar draws only the
|
|
102
|
+
* ones with a share: pass the empty buckets as `legendOnly` entries.
|
|
103
|
+
*/
|
|
104
|
+
legendOnly?: boolean;
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
@@ -89,7 +109,14 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
89
109
|
* and nobody knows how much is left (an import that reports no count). */
|
|
90
110
|
value?: number;
|
|
91
111
|
min?: number;
|
|
92
|
-
|
|
112
|
+
/**
|
|
113
|
+
* The top of the scale (default 100). `null` is a quota with NO ceiling — kastlan's
|
|
114
|
+
* billing usage on an unlimited plan (usage-progress-bar:25): nothing to fill, so no
|
|
115
|
+
* bar is drawn; the label row shows the value and "Unlimited" (`progressBar.unlimited`)
|
|
116
|
+
* as plain text. `formatValue` is then called with `max` = `Infinity`; without it,
|
|
117
|
+
* the value is a plain number in the kit's locale.
|
|
118
|
+
*/
|
|
119
|
+
max?: number | null;
|
|
93
120
|
/**
|
|
94
121
|
* `progress` (default): a task moving toward completion — `role="progressbar"`.
|
|
95
122
|
* `meter`: a share of a known whole that is not going anywhere (budget used, disk
|
|
@@ -126,6 +153,25 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
126
153
|
/** With `segments`: a list under the bar naming each part with its swatch and value
|
|
127
154
|
* — the table view of the bar, in text tokens rather than the segment colour. */
|
|
128
155
|
legend?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* What a legend row shows at its end, in place of the part's formatted value — which
|
|
158
|
+
* it is handed, with the segment and its index. keksdose's cut card shows the share
|
|
159
|
+
* AND a private outflow amount per bucket (cut-card:150), which one figure could not:
|
|
160
|
+
* `legendValue={(seg, share) => <>{share} <Amount … /></>}`.
|
|
161
|
+
*/
|
|
162
|
+
legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;
|
|
163
|
+
/** A muted line under the bar — the plan's reset date, "3 of 5 seats in use". It
|
|
164
|
+
* describes the bar (`aria-describedby`). */
|
|
165
|
+
hint?: ReactNode;
|
|
166
|
+
/**
|
|
167
|
+
* When `value` is past `max`, say by how much, in a danger-coloured line under the bar
|
|
168
|
+
* (kastlan usage-progress-bar:46): `true` prints `progressBar.overLimit` with the
|
|
169
|
+
* excess as a plain number; a function renders the line itself from the excess (a
|
|
170
|
+
* currency, "2 seats"). The bar stays full at `max` either way — colour it with
|
|
171
|
+
* `tone="danger"` if it should read as over. Ignored for `segments` and `max={null}`.
|
|
172
|
+
*/
|
|
173
|
+
overage?: boolean | ((over: number) => ReactNode);
|
|
174
|
+
labels?: Partial<ProgressBarLabels>;
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
/**
|
|
@@ -148,7 +194,7 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
148
194
|
export function ProgressBar({
|
|
149
195
|
value,
|
|
150
196
|
min = 0,
|
|
151
|
-
max = 100,
|
|
197
|
+
max: maxProp = 100,
|
|
152
198
|
variant = "progress",
|
|
153
199
|
label,
|
|
154
200
|
showValue = false,
|
|
@@ -158,21 +204,65 @@ export function ProgressBar({
|
|
|
158
204
|
locale,
|
|
159
205
|
segments,
|
|
160
206
|
legend = false,
|
|
207
|
+
legendValue,
|
|
208
|
+
hint,
|
|
209
|
+
overage,
|
|
210
|
+
labels,
|
|
161
211
|
className,
|
|
162
212
|
"aria-label": ariaLabel,
|
|
163
213
|
"aria-labelledby": ariaLabelledBy,
|
|
214
|
+
"aria-describedby": ariaDescribedBy,
|
|
164
215
|
...rest
|
|
165
216
|
}: ProgressBarProps) {
|
|
166
217
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
218
|
+
const text = useKitLabels("progressBar", DEFAULT_PROGRESS_BAR_LABELS, labels);
|
|
167
219
|
const kitLocale = useKitLocale(locale);
|
|
168
220
|
const labelId = useId();
|
|
221
|
+
const hintId = useId();
|
|
222
|
+
const hintNode =
|
|
223
|
+
hint != null ? (
|
|
224
|
+
<p id={hintId} data-part="hint" className="mt-1 text-xs text-[var(--text-muted)]">
|
|
225
|
+
{hint}
|
|
226
|
+
</p>
|
|
227
|
+
) : null;
|
|
228
|
+
|
|
229
|
+
if (maxProp === null) {
|
|
230
|
+
// No ceiling: no bar, no role — a meter with no maximum is not one. The value and
|
|
231
|
+
// "Unlimited" are the content, so they are plain visible text.
|
|
232
|
+
const shown =
|
|
233
|
+
value === undefined || !Number.isFinite(value)
|
|
234
|
+
? undefined
|
|
235
|
+
: formatValue
|
|
236
|
+
? formatValue(value, Infinity, min)
|
|
237
|
+
: new Intl.NumberFormat(kitLocale).format(value);
|
|
238
|
+
return (
|
|
239
|
+
<div {...rest} className={cn("min-w-0", className)} data-state="unlimited">
|
|
240
|
+
<div className="flex items-baseline gap-2 text-sm">
|
|
241
|
+
{label != null && (
|
|
242
|
+
<span id={labelId} className="min-w-0 text-[var(--text-secondary)]">
|
|
243
|
+
{label}
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
<span data-part="unlimited" className="ms-auto shrink-0 tabular-nums text-[var(--text-muted)]">
|
|
247
|
+
{shown !== undefined && `${shown} · `}
|
|
248
|
+
{text.unlimited}
|
|
249
|
+
</span>
|
|
250
|
+
</div>
|
|
251
|
+
{hintNode}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const max = maxProp;
|
|
169
256
|
|
|
170
257
|
const stacked = segments !== undefined;
|
|
171
258
|
const meter = stacked || variant === "meter";
|
|
172
259
|
const span = max - min;
|
|
173
260
|
const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);
|
|
261
|
+
// Colours are dealt over EVERY segment, legend-only ones included, so an entry keeps
|
|
262
|
+
// its swatch whether or not it is in the bar this time.
|
|
174
263
|
const fills = stacked ? segmentFills(segments) : [];
|
|
175
|
-
const
|
|
264
|
+
const barParts = stacked ? segments.filter((seg) => !seg.legendOnly) : [];
|
|
265
|
+
const partTotal = stacked ? min + barParts.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;
|
|
176
266
|
const format = (v: number) =>
|
|
177
267
|
formatValue
|
|
178
268
|
? formatValue(v, max, min)
|
|
@@ -180,7 +270,7 @@ export function ProgressBar({
|
|
|
180
270
|
span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,
|
|
181
271
|
);
|
|
182
272
|
const partTexts = stacked
|
|
183
|
-
?
|
|
273
|
+
? barParts.map((seg) => {
|
|
184
274
|
const text = format(min + partValue(seg.value));
|
|
185
275
|
return typeof seg.label === "string" || typeof seg.label === "number"
|
|
186
276
|
? common.fieldValue(String(seg.label), text)
|
|
@@ -199,6 +289,19 @@ export function ProgressBar({
|
|
|
199
289
|
? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts)
|
|
200
290
|
: valueText;
|
|
201
291
|
|
|
292
|
+
const over =
|
|
293
|
+
overage && !stacked && value !== undefined && Number.isFinite(value) && value > max ? value - max : 0;
|
|
294
|
+
const overageNode =
|
|
295
|
+
over > 0 ? (
|
|
296
|
+
<p id={`${hintId}-over`} data-part="overage" className="mt-1 text-xs text-[var(--danger)]">
|
|
297
|
+
{typeof overage === "function"
|
|
298
|
+
? overage(over)
|
|
299
|
+
: text.overLimit(new Intl.NumberFormat(kitLocale).format(over))}
|
|
300
|
+
</p>
|
|
301
|
+
) : null;
|
|
302
|
+
const describedBy =
|
|
303
|
+
[ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(" ") || undefined;
|
|
304
|
+
|
|
202
305
|
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);
|
|
203
306
|
const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));
|
|
204
307
|
|
|
@@ -225,6 +328,7 @@ export function ProgressBar({
|
|
|
225
328
|
role={meter ? "meter" : "progressbar"}
|
|
226
329
|
aria-label={name}
|
|
227
330
|
aria-labelledby={labelledBy}
|
|
331
|
+
aria-describedby={describedBy}
|
|
228
332
|
aria-valuemin={indeterminate ? undefined : min}
|
|
229
333
|
aria-valuemax={indeterminate ? undefined : max}
|
|
230
334
|
aria-valuenow={clamped}
|
|
@@ -238,20 +342,22 @@ export function ProgressBar({
|
|
|
238
342
|
// The parts in a flex row with a 2px gap, so two neighbouring steps of one
|
|
239
343
|
// hue are parted by the track rather than by their difference in lightness.
|
|
240
344
|
<div className="flex h-full w-full gap-0.5">
|
|
241
|
-
{segments.map((seg, i) =>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
345
|
+
{segments.map((seg, i) =>
|
|
346
|
+
seg.legendOnly ? null : (
|
|
347
|
+
<div
|
|
348
|
+
key={seg.key ?? i}
|
|
349
|
+
data-part="segment"
|
|
350
|
+
className={cn(
|
|
351
|
+
"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
|
|
352
|
+
fills[i],
|
|
353
|
+
)}
|
|
354
|
+
style={{
|
|
355
|
+
width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,
|
|
356
|
+
backgroundColor: seg.className ? undefined : seg.color,
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
),
|
|
360
|
+
)}
|
|
255
361
|
</div>
|
|
256
362
|
) : indeterminate ? (
|
|
257
363
|
<div
|
|
@@ -294,12 +400,16 @@ export function ProgressBar({
|
|
|
294
400
|
<span className="min-w-0 flex-1 truncate text-[var(--text-secondary)]">{seg.label}</span>
|
|
295
401
|
)}
|
|
296
402
|
<span className="ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]">
|
|
297
|
-
{
|
|
403
|
+
{legendValue
|
|
404
|
+
? legendValue(seg, format(min + partValue(seg.value)), i)
|
|
405
|
+
: format(min + partValue(seg.value))}
|
|
298
406
|
</span>
|
|
299
407
|
</li>
|
|
300
408
|
))}
|
|
301
409
|
</ul>
|
|
302
410
|
)}
|
|
411
|
+
{overageNode}
|
|
412
|
+
{hintNode}
|
|
303
413
|
</div>
|
|
304
414
|
);
|
|
305
415
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { encodeQr } from "../lib/qr-encode";
|
|
5
|
+
import type { QrErrorCorrection } from "../lib/qr-encode";
|
|
6
|
+
|
|
7
|
+
export interface QrCodeProps extends Omit<ComponentPropsWithoutRef<"svg">, "children" | "viewBox"> {
|
|
8
|
+
/** The text to encode (UTF-8) — an `otpauth://` URI, a URL, a pairing code. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** The accessible name. Without it the code is decorative (`aria-hidden`): say what
|
|
11
|
+
* it is for, not what it encodes — nobody wants a URI read out. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Default `"M"`, raised automatically where the size allows it. */
|
|
14
|
+
errorCorrection?: QrErrorCorrection;
|
|
15
|
+
/** The quiet zone, in modules. The spec asks for 4; less reads worse. Default 4. */
|
|
16
|
+
margin?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A QR code as one inline SVG — no canvas, no data URL, no dependency (see
|
|
21
|
+
* `lib/qr-encode.ts` for why the encoder lives in the kit).
|
|
22
|
+
*
|
|
23
|
+
* Sized by CSS (`className="size-48"`); the SVG scales without blurring because every
|
|
24
|
+
* module is a whole unit of the viewBox and `shape-rendering` is `crispEdges`.
|
|
25
|
+
*
|
|
26
|
+
* **Black on white, always — the one literal colour pair in the kit.** A camera looks
|
|
27
|
+
* for the contrast between the modules and their quiet zone; on the dark theme a
|
|
28
|
+
* surface-coloured background makes a code slow to acquire, and an inverted one is not
|
|
29
|
+
* read at all by several authenticator apps. The quiet zone is part of the SVG, so the
|
|
30
|
+
* code carries its own white border wherever it is placed.
|
|
31
|
+
*
|
|
32
|
+
* A value too long for any QR version (≈2.9 kB) renders nothing rather than throwing
|
|
33
|
+
* in render.
|
|
34
|
+
*/
|
|
35
|
+
export function QrCode({
|
|
36
|
+
value,
|
|
37
|
+
label,
|
|
38
|
+
errorCorrection = "M",
|
|
39
|
+
margin = 4,
|
|
40
|
+
className,
|
|
41
|
+
...rest
|
|
42
|
+
}: QrCodeProps) {
|
|
43
|
+
const path = useMemo(() => {
|
|
44
|
+
try {
|
|
45
|
+
const { modules, size } = encodeQr(value, { errorCorrection });
|
|
46
|
+
// One path, one sub-path per horizontal run of dark modules: a few hundred
|
|
47
|
+
// commands rather than one <rect> per module.
|
|
48
|
+
let d = "";
|
|
49
|
+
for (let y = 0; y < size; y++) {
|
|
50
|
+
for (let x = 0; x < size; ) {
|
|
51
|
+
if (!modules[y][x]) {
|
|
52
|
+
x++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let run = 1;
|
|
56
|
+
while (x + run < size && modules[y][x + run]) run++;
|
|
57
|
+
d += `M${x + margin} ${y + margin}h${run}v1h-${run}z`;
|
|
58
|
+
x += run;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { d, extent: size + margin * 2 };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}, [value, errorCorrection, margin]);
|
|
66
|
+
|
|
67
|
+
if (!path) return null;
|
|
68
|
+
return (
|
|
69
|
+
<svg
|
|
70
|
+
{...rest}
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
viewBox={`0 0 ${path.extent} ${path.extent}`}
|
|
73
|
+
shapeRendering="crispEdges"
|
|
74
|
+
role={label ? "img" : undefined}
|
|
75
|
+
aria-label={label}
|
|
76
|
+
aria-hidden={label ? undefined : true}
|
|
77
|
+
className={cn("block", className)}
|
|
78
|
+
>
|
|
79
|
+
<rect width={path.extent} height={path.extent} fill="#ffffff" />
|
|
80
|
+
<path d={path.d} fill="#000000" />
|
|
81
|
+
</svg>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { InputHTMLAttributes } from "react";
|
|
|
3
3
|
import { Search, X } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { FIELD_BASE } from "./ui";
|
|
6
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
6
7
|
|
|
7
8
|
interface SearchFieldOwnProps
|
|
8
9
|
extends Omit<
|
|
@@ -23,10 +24,22 @@ interface SearchFieldOwnProps
|
|
|
23
24
|
* absent, so a consumer can migrate one call site at a time.
|
|
24
25
|
*/
|
|
25
26
|
label?: string;
|
|
26
|
-
/** Names the clear button.
|
|
27
|
-
*
|
|
28
|
-
* complaint that produced the button on three of these four screens. */
|
|
27
|
+
/** Names the clear button. Default: the catalogue's `common.clear`, in the
|
|
28
|
+
* provider's language. */
|
|
29
29
|
clearLabel?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Draw the "×" that empties the field once there is something in it. Default
|
|
32
|
+
* `true`.
|
|
33
|
+
*
|
|
34
|
+
* It used to appear only when a `clearLabel` was passed, and every kastlan call
|
|
35
|
+
* site passed one — lease-list-page.tsx:91, trial-balance-page.tsx:~196,
|
|
36
|
+
* account-ledger-page.tsx:~131 and data-table/resource-list-page.tsx:256 each
|
|
37
|
+
* spelled `clearLabel={t("table.clear")}` — so the opt-in was boilerplate: a filter
|
|
38
|
+
* you can type into and not untype is the complaint that produced the button in
|
|
39
|
+
* the first place, and the catalogue already names it. `false` is for a field
|
|
40
|
+
* whose clearing lives elsewhere (a "Reset filters" beside it).
|
|
41
|
+
*/
|
|
42
|
+
clearable?: boolean;
|
|
30
43
|
/**
|
|
31
44
|
* `"field"` (default): the bordered filter box that sits on a page.
|
|
32
45
|
*
|
|
@@ -96,7 +109,8 @@ export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(functi
|
|
|
96
109
|
value,
|
|
97
110
|
onChange,
|
|
98
111
|
label,
|
|
99
|
-
clearLabel,
|
|
112
|
+
clearLabel: clearLabelProp,
|
|
113
|
+
clearable = true,
|
|
100
114
|
className,
|
|
101
115
|
inputClassName,
|
|
102
116
|
placeholder,
|
|
@@ -107,6 +121,8 @@ export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(functi
|
|
|
107
121
|
ref,
|
|
108
122
|
) {
|
|
109
123
|
const innerRef = useRef<HTMLInputElement>(null);
|
|
124
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
125
|
+
const clearLabel = clearable ? (clearLabelProp ?? common.clear) : undefined;
|
|
110
126
|
// Whichever spelling the caller used. It is the accessible name AND the placeholder
|
|
111
127
|
// fallback, so a box named only by `aria-label` still says what it is before anyone
|
|
112
128
|
// has typed in it — the same deal `label` has always had.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { ArrowDown, ArrowUp, Minus } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { formatMoney, formatNumber, formatPercent } from "../lib/format";
|
|
5
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
6
|
+
import { DEFAULT_STAT_TILE_LABELS } from "./stat-tile";
|
|
7
|
+
import type { StatTileLabels } from "./stat-tile";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The text colours a figure can carry, token-backed — `StatTile`'s tones, as a class
|
|
11
|
+
* any span can take.
|
|
12
|
+
*
|
|
13
|
+
* - `default` — the primary text colour. `muted` — the secondary, quiet one.
|
|
14
|
+
* - `income` / `expense` / `net` — the money trio (CVD-safe teal / amber / violet).
|
|
15
|
+
* - `success` / `warning` / `danger` / `info` — a verdict: a rent rise is the tenant's
|
|
16
|
+
* bad news (kastlan lease-columns), a vote "for" is green (meeting-agenda-tab).
|
|
17
|
+
* - `brand` — the app's accent.
|
|
18
|
+
*
|
|
19
|
+
* Colour is never the only carrier: pair a verdict with an arrow, a word, or a sign.
|
|
20
|
+
*/
|
|
21
|
+
export type TextTone =
|
|
22
|
+
| "default"
|
|
23
|
+
| "muted"
|
|
24
|
+
| "brand"
|
|
25
|
+
| "income"
|
|
26
|
+
| "expense"
|
|
27
|
+
| "net"
|
|
28
|
+
| "success"
|
|
29
|
+
| "warning"
|
|
30
|
+
| "danger"
|
|
31
|
+
| "info";
|
|
32
|
+
|
|
33
|
+
const TEXT_TONE: Record<TextTone, string> = {
|
|
34
|
+
default: "text-[var(--text-primary)]",
|
|
35
|
+
muted: "text-[var(--text-muted)]",
|
|
36
|
+
brand: "text-[var(--brand)]",
|
|
37
|
+
income: "text-[var(--money-income)]",
|
|
38
|
+
expense: "text-[var(--money-expense)]",
|
|
39
|
+
net: "text-[var(--money-net)]",
|
|
40
|
+
success: "text-[var(--success)]",
|
|
41
|
+
warning: "text-[var(--warning)]",
|
|
42
|
+
danger: "text-[var(--danger)]",
|
|
43
|
+
info: "text-[var(--info)]",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The class that colours text in `tone` — for the spans kastlan colours by hand with
|
|
48
|
+
* `text-[var(--danger)]` / `text-emerald-600` (settlement-table, changelog-page,
|
|
49
|
+
* meeting-agenda-tab), so the palette they draw from is the kit's tokens.
|
|
50
|
+
*/
|
|
51
|
+
export function toneTextClass(tone: TextTone): string {
|
|
52
|
+
return TEXT_TONE[tone];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ToneProps extends ComponentPropsWithoutRef<"span"> {
|
|
56
|
+
tone: TextTone;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A `<span>` in a {@link TextTone} — `<Tone tone="success">{votesFor}</Tone>`. */
|
|
60
|
+
export function Tone({ tone, className, ...rest }: ToneProps) {
|
|
61
|
+
return <span {...rest} className={cn(TEXT_TONE[tone], className)} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── Signed amounts ──────────────────────────────────────────────────────── */
|
|
65
|
+
|
|
66
|
+
/** Which way is +1: `up` for a figure where more is good news (income, savings),
|
|
67
|
+
* `down` where less is (a rent the tenant pays, a cost). */
|
|
68
|
+
export type GoodDirection = "up" | "down";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How a signed figure is coloured:
|
|
72
|
+
* - `signed` (default) — money read by its own sign: above zero `income`, below zero
|
|
73
|
+
* `expense`, zero muted. A balance, a net.
|
|
74
|
+
* - `income` / `expense` / `neutral` — one colour whatever the sign (`neutral` is the
|
|
75
|
+
* text's own colour).
|
|
76
|
+
* - `verdict` — the change judged by {@link SignedAmountProps.goodDirection}: good
|
|
77
|
+
* news `success`, bad news `danger`, zero muted. Setting `goodDirection` implies it.
|
|
78
|
+
*/
|
|
79
|
+
export type SignedAmountTone = "signed" | "income" | "expense" | "neutral" | "verdict";
|
|
80
|
+
|
|
81
|
+
/** What a signed figure says to a screen reader, whose readers disagree about a
|
|
82
|
+
* leading "+" (most skip it) and a "−" (some read "dash", some nothing). */
|
|
83
|
+
export interface SignedAmountLabels {
|
|
84
|
+
/** A figure above zero. `amount` is formatted and unsigned. */
|
|
85
|
+
positive: (amount: string) => string;
|
|
86
|
+
/** A figure below zero. `amount` is formatted and unsigned. */
|
|
87
|
+
negative: (amount: string) => string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const DEFAULT_SIGNED_AMOUNT_LABELS: SignedAmountLabels = {
|
|
91
|
+
positive: (amount) => `plus ${amount}`,
|
|
92
|
+
negative: (amount) => `minus ${amount}`,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
interface SignedFormatProps {
|
|
96
|
+
/** ISO 4217 code: format as money. */
|
|
97
|
+
currency?: string;
|
|
98
|
+
/** `"percent"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does. */
|
|
99
|
+
unit?: "value" | "percent";
|
|
100
|
+
/** Compact notation: 12 400 → "12K". */
|
|
101
|
+
compact?: boolean;
|
|
102
|
+
/** Fraction digits, as `formatNumber`'s `digits`. */
|
|
103
|
+
digits?: number | { min?: number; max?: number };
|
|
104
|
+
/** Formats the UNSIGNED magnitude, replacing the `Intl` default outright. The sign
|
|
105
|
+
* is the component's. */
|
|
106
|
+
format?: (magnitude: number) => string;
|
|
107
|
+
locale?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function useMagnitudeFormatter({ currency, unit, compact, digits, format, locale }: SignedFormatProps) {
|
|
111
|
+
const kitLocale = useKitLocale(locale);
|
|
112
|
+
return (magnitude: number): string => {
|
|
113
|
+
if (format) return format(magnitude);
|
|
114
|
+
if (unit === "percent") return formatPercent(magnitude, { locale: kitLocale, compact, digits });
|
|
115
|
+
if (currency) return formatMoney(magnitude, currency, { locale: kitLocale, compact, digits });
|
|
116
|
+
return formatNumber(magnitude, { locale: kitLocale, compact, digits });
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function directionOf(value: number): "up" | "down" | "flat" {
|
|
121
|
+
return value > 0 ? "up" : value < 0 ? "down" : "flat";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function signedToneClass(
|
|
125
|
+
tone: SignedAmountTone,
|
|
126
|
+
dir: "up" | "down" | "flat",
|
|
127
|
+
goodDirection: GoodDirection | undefined,
|
|
128
|
+
): string {
|
|
129
|
+
if (tone === "verdict" || (goodDirection && tone === "signed")) {
|
|
130
|
+
if (dir === "flat" || !goodDirection) return TEXT_TONE.muted;
|
|
131
|
+
return dir === goodDirection ? TEXT_TONE.success : TEXT_TONE.danger;
|
|
132
|
+
}
|
|
133
|
+
if (tone === "signed") return dir === "flat" ? TEXT_TONE.muted : dir === "up" ? TEXT_TONE.income : TEXT_TONE.expense;
|
|
134
|
+
if (tone === "neutral") return "";
|
|
135
|
+
return TEXT_TONE[tone];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ARROW = { up: ArrowUp, down: ArrowDown, flat: Minus } as const;
|
|
139
|
+
|
|
140
|
+
export interface SignedAmountProps
|
|
141
|
+
extends SignedFormatProps,
|
|
142
|
+
Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
143
|
+
value: number;
|
|
144
|
+
/** See {@link SignedAmountTone}. Default `signed`, or `verdict` with `goodDirection`. */
|
|
145
|
+
tone?: SignedAmountTone;
|
|
146
|
+
/** Which way is good news — colours by verdict (`success` / `danger`). */
|
|
147
|
+
goodDirection?: GoodDirection;
|
|
148
|
+
/** Print "+" before a positive figure (default `true`). A negative always shows its
|
|
149
|
+
* minus; zero shows neither. */
|
|
150
|
+
showPlus?: boolean;
|
|
151
|
+
/** An arrow before the figure (up / down / a dash at zero) — so the direction does not
|
|
152
|
+
* ride on colour alone. Decorative; the words say the same. */
|
|
153
|
+
arrow?: boolean;
|
|
154
|
+
/** Tag the figure `data-private`, for the host's demo-mode blur. */
|
|
155
|
+
sensitive?: boolean;
|
|
156
|
+
labels?: Partial<SignedAmountLabels>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A figure with a sign that matters: a balance, a rent change, a net.
|
|
161
|
+
*
|
|
162
|
+
* kastlan writes it by hand in five places — `{change > 0 ? "+" : ""}{formatCurrency(
|
|
163
|
+
* change)}` in a span coloured by a ternary (lease-columns:190, settlement-table:58,
|
|
164
|
+
* the tenancy `formatPct`) — and each picks its own colours for the same verdict.
|
|
165
|
+
*
|
|
166
|
+
* The visible figure (sign, digits, arrow) is hidden from assistive tech and spoken as
|
|
167
|
+
* words instead ("plus CHF 50.00" / "minus CHF 50.00", `signedAmount.*`): a "+" is
|
|
168
|
+
* skipped by most screen readers, and `Intl`'s "−" is read as "dash" or not at all.
|
|
169
|
+
*/
|
|
170
|
+
export function SignedAmount({
|
|
171
|
+
value,
|
|
172
|
+
currency,
|
|
173
|
+
unit,
|
|
174
|
+
compact,
|
|
175
|
+
digits,
|
|
176
|
+
format,
|
|
177
|
+
locale,
|
|
178
|
+
tone = "signed",
|
|
179
|
+
goodDirection,
|
|
180
|
+
showPlus = true,
|
|
181
|
+
arrow = false,
|
|
182
|
+
sensitive = false,
|
|
183
|
+
labels,
|
|
184
|
+
className,
|
|
185
|
+
...rest
|
|
186
|
+
}: SignedAmountProps) {
|
|
187
|
+
const text = useKitLabels("signedAmount", DEFAULT_SIGNED_AMOUNT_LABELS, labels);
|
|
188
|
+
const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
|
|
189
|
+
const finite = Number.isFinite(value);
|
|
190
|
+
const dir = finite ? directionOf(value) : "flat";
|
|
191
|
+
const amount = finite ? fmt(Math.abs(value)) : "—";
|
|
192
|
+
// U+2212, the typographic minus: the same width as "+", so a column of signed figures
|
|
193
|
+
// lines up under `tabular-nums`.
|
|
194
|
+
const sign = dir === "up" ? (showPlus ? "+" : "") : dir === "down" ? "−" : "";
|
|
195
|
+
const spoken = dir === "up" ? text.positive(amount) : dir === "down" ? text.negative(amount) : amount;
|
|
196
|
+
const Icon = ARROW[dir];
|
|
197
|
+
return (
|
|
198
|
+
<span
|
|
199
|
+
{...rest}
|
|
200
|
+
data-private={sensitive ? "" : undefined}
|
|
201
|
+
data-direction={dir}
|
|
202
|
+
className={cn(
|
|
203
|
+
"relative inline-flex items-center gap-0.5 whitespace-nowrap tabular-nums",
|
|
204
|
+
signedToneClass(tone, dir, goodDirection),
|
|
205
|
+
className,
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
208
|
+
{arrow && <Icon aria-hidden className="size-[1em] shrink-0" />}
|
|
209
|
+
<span aria-hidden>
|
|
210
|
+
{sign}
|
|
211
|
+
{amount}
|
|
212
|
+
</span>
|
|
213
|
+
<span className="sr-only">{spoken}</span>
|
|
214
|
+
</span>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
219
|
+
/** The change. Above zero is "up". */
|
|
220
|
+
value: number;
|
|
221
|
+
/** Which way is good news. Unset, the change is stated and not judged (muted); set,
|
|
222
|
+
* it is coloured `success` / `danger` and its sentence says "(better)" / "(worse)". */
|
|
223
|
+
goodDirection?: GoodDirection;
|
|
224
|
+
/** What it is measured against, after it: "vs last month". */
|
|
225
|
+
label?: ReactNode;
|
|
226
|
+
/** Hide the arrow (shown by default, as on a StatTile). At zero the arrow is a flat
|
|
227
|
+
* dash BESIDE the figure ("– 0.00 %"), never in place of it. */
|
|
228
|
+
arrow?: boolean;
|
|
229
|
+
sensitive?: boolean;
|
|
230
|
+
/** The `statTile` namespace's direction words — a delta says the same as a tile's. */
|
|
231
|
+
labels?: Partial<Pick<StatTileLabels, "increase" | "decrease" | "unchanged" | "better" | "worse">>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* `StatTile`'s delta on its own: an arrow, the unsigned magnitude, and a sentence for
|
|
236
|
+
* screen readers ("Up 12% (worse)"), coloured only when `goodDirection` judges it —
|
|
237
|
+
* users up and cost up are the same arrow until someone says which is good.
|
|
238
|
+
*
|
|
239
|
+
* For a change shown outside a tile: kastlan's rent-change column
|
|
240
|
+
* (lease-columns:219, a "+1.25%" chip), the breakdown rows of the rent calculator
|
|
241
|
+
* (breakdown-row:5, a trend icon coloured by hand).
|
|
242
|
+
*/
|
|
243
|
+
export function Delta({
|
|
244
|
+
value,
|
|
245
|
+
currency,
|
|
246
|
+
unit,
|
|
247
|
+
compact,
|
|
248
|
+
digits,
|
|
249
|
+
format,
|
|
250
|
+
locale,
|
|
251
|
+
goodDirection,
|
|
252
|
+
label,
|
|
253
|
+
arrow = true,
|
|
254
|
+
sensitive = false,
|
|
255
|
+
labels,
|
|
256
|
+
className,
|
|
257
|
+
...rest
|
|
258
|
+
}: DeltaProps) {
|
|
259
|
+
const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
|
|
260
|
+
const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
|
|
261
|
+
if (!Number.isFinite(value)) return null;
|
|
262
|
+
const amount = fmt(Math.abs(value));
|
|
263
|
+
const dir = directionOf(value);
|
|
264
|
+
const Icon = ARROW[dir];
|
|
265
|
+
let spoken = dir === "up" ? text.increase(amount) : dir === "down" ? text.decrease(amount) : text.unchanged;
|
|
266
|
+
let tone = TEXT_TONE.muted;
|
|
267
|
+
if (goodDirection && dir !== "flat") {
|
|
268
|
+
const good = dir === goodDirection;
|
|
269
|
+
spoken = good ? text.better(spoken) : text.worse(spoken);
|
|
270
|
+
tone = good ? TEXT_TONE.success : TEXT_TONE.danger;
|
|
271
|
+
}
|
|
272
|
+
return (
|
|
273
|
+
<span
|
|
274
|
+
{...rest}
|
|
275
|
+
data-direction={dir}
|
|
276
|
+
className={cn("relative inline-flex flex-wrap items-center gap-x-1.5 tabular-nums", className)}
|
|
277
|
+
>
|
|
278
|
+
<span data-private={sensitive ? "" : undefined} className={cn("inline-flex items-center gap-0.5 font-medium", tone)}>
|
|
279
|
+
{arrow && <Icon className="size-[1em] shrink-0" aria-hidden />}
|
|
280
|
+
{/* The figure at zero too, beside the flat dash (kastlan feedback #48). Before
|
|
281
|
+
this a flat delta with an arrow drew the dash ALONE, as a StatTile does — so
|
|
282
|
+
a rent-change column that printed "0.00 %" for an unchanged lease showed a
|
|
283
|
+
bare "–" in its place, and a bare dash is what a table prints for a MISSING
|
|
284
|
+
value: "unchanged" and "not known" looked the same. The Minus icon is the
|
|
285
|
+
neutral indicator, the muted tone the neutral colour, and the figure says
|
|
286
|
+
how much (none) in the column's own digits. A value that is missing never
|
|
287
|
+
reaches here — a non-finite one renders nothing, above. Visible change for
|
|
288
|
+
every `<Delta value={0}>` with the default arrow: "–" became "– 0 %". */}
|
|
289
|
+
<span aria-hidden>{amount}</span>
|
|
290
|
+
<span className="sr-only">{spoken}</span>
|
|
291
|
+
</span>
|
|
292
|
+
{label != null && <span className="text-[var(--text-muted)]">{label}</span>}
|
|
293
|
+
</span>
|
|
294
|
+
);
|
|
295
|
+
}
|