@eifi1/ui-kit 0.10.0 → 0.12.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 +66 -37
- 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/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -4
- package/dist/components/amount-input.js +5 -2
- 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 +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +107 -3
- package/dist/components/button-group.js +60 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +17 -4
- package/dist/components/calendar-heatmap.d.ts +162 -0
- package/dist/components/calendar-heatmap.js +336 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +66 -2
- package/dist/components/choice-card.js +108 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- 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 +52 -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-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 +104 -12
- 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 +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +69 -0
- package/dist/components/field.js +75 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +51 -208
- package/dist/components/file-dropzone.d.ts +50 -2
- package/dist/components/floating-panel.d.ts +156 -4
- package/dist/components/floating-panel.js +193 -29
- 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/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 -3
- package/dist/components/list.js +39 -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 +13 -2
- package/dist/components/menu-item.js +11 -2
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +82 -5
- package/dist/components/mini-calendar.js +212 -64
- 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 +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +67 -0
- package/dist/components/nav-pills.js +69 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +26 -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 +27 -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 +17 -4
- 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 +2 -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/progress-bar.d.ts +73 -3
- package/dist/components/progress-bar.js +110 -8
- 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/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +50 -1
- package/dist/components/signed-amount.d.ts +121 -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 +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +77 -11
- package/dist/components/table.js +85 -17
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +50 -1
- package/dist/components/toggle-group.d.ts +28 -0
- package/dist/components/toggle-group.js +58 -9
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +52 -729
- package/dist/components/ui.js +296 -27
- 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/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.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-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -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 +50 -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 +17 -4
- package/dist/i18n/defaults.js +33 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +47 -310
- 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 +17 -4
- package/dist/i18n/locales/de-CH.d.ts +17 -4
- package/dist/i18n/locales/de-informal.d.ts +17 -4
- package/dist/i18n/locales/de.d.ts +17 -4
- package/dist/i18n/locales/de.js +165 -4
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +17 -4
- package/dist/i18n/locales/es.js +162 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -4
- package/dist/i18n/locales/fr.js +162 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -4
- package/dist/i18n/locales/hu.js +162 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -4
- package/dist/i18n/locales/it.js +162 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -4
- package/dist/i18n/locales/zh.js +162 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -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 +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +50 -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/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +63 -1
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- 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 +84 -3
- package/dist/shell/topbar-action-menu.js +115 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +58 -2
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -1
- package/dist/wizard/wizard-context.d.ts +30 -1
- package/dist/wizard/wizard-context.js +23 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +51 -3
- package/package.json +1 -1
- 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/alert-banner.tsx +36 -7
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +129 -4
- package/src/components/calendar-heatmap.tsx +599 -0
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +208 -1
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +231 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +188 -0
- package/src/components/floating-panel.tsx +380 -16
- package/src/components/form-actions.tsx +191 -0
- 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 +93 -18
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +29 -3
- package/src/components/mini-calendar.tsx +317 -57
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +152 -0
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +241 -13
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/series-chart.tsx +21 -0
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +186 -23
- package/src/components/text-link.tsx +248 -0
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +106 -13
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +572 -44
- package/src/components/use-table-state.ts +265 -33
- package/src/components/user-avatar.tsx +57 -3
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -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 +32 -0
- package/src/i18n/kit-labels.tsx +104 -8
- package/src/i18n/locales/de.ts +162 -0
- package/src/i18n/locales/es.ts +159 -0
- package/src/i18n/locales/fr.ts +162 -0
- package/src/i18n/locales/hu.ts +160 -0
- package/src/i18n/locales/it.ts +160 -0
- package/src/i18n/locales/zh.ts +159 -0
- package/src/index.ts +64 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +8 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +260 -49
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +50 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
2
|
+
import type { ErrorInfo, ReactNode } from "react";
|
|
3
|
+
import { AlertTriangle } from "lucide-react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { Button, EmptyState } from "./ui";
|
|
6
|
+
import { Disclosure } from "./disclosure";
|
|
7
|
+
|
|
8
|
+
/** The words of the default fallback. */
|
|
9
|
+
export interface ErrorBoundaryLabels {
|
|
10
|
+
/** The fallback's heading. */
|
|
11
|
+
title: string;
|
|
12
|
+
/** The line under it. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** The button that clears the error and renders the children again. */
|
|
15
|
+
retry: string;
|
|
16
|
+
/** The disclosure that opens the error's own text (`showDetails`). */
|
|
17
|
+
details: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_ERROR_BOUNDARY_LABELS: ErrorBoundaryLabels = {
|
|
21
|
+
title: "Something went wrong",
|
|
22
|
+
message: "This part of the page could not be shown. Try again, or reload the page.",
|
|
23
|
+
retry: "Try again",
|
|
24
|
+
details: "Error details",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What was thrown, as flat strings, read ONCE when it arrives.
|
|
29
|
+
*
|
|
30
|
+
* React stores whatever was thrown, hostile or not — `throw { get message() { throw … } }`,
|
|
31
|
+
* `throw { name: Symbol() }`, `throw undefined` — and reading one of those during the
|
|
32
|
+
* fallback's own render would throw INSIDE the boundary, escalating to the next one up
|
|
33
|
+
* (keksdose's error-boundary.tsx learned this as feedback #160: at the app level there is
|
|
34
|
+
* none, and the result is the white screen the boundary exists to prevent).
|
|
35
|
+
*/
|
|
36
|
+
export interface ErrorBoundaryDetails {
|
|
37
|
+
name: string;
|
|
38
|
+
message: string;
|
|
39
|
+
stack?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function read(get: () => unknown): string | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const v = get();
|
|
45
|
+
return typeof v === "string" ? v : v === undefined || v === null ? undefined : String(v);
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** {@link ErrorBoundaryDetails} of any thrown value, without ever throwing. */
|
|
52
|
+
export function describeThrown(error: unknown): ErrorBoundaryDetails {
|
|
53
|
+
if (typeof error === "object" && error !== null) {
|
|
54
|
+
const e = error as { name?: unknown; message?: unknown; stack?: unknown };
|
|
55
|
+
return {
|
|
56
|
+
name: read(() => e.name) ?? "Error",
|
|
57
|
+
message: read(() => e.message) ?? "",
|
|
58
|
+
stack: read(() => e.stack),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { name: "Error", message: read(() => error) ?? "" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a render-prop `fallback` is handed. */
|
|
65
|
+
export interface ErrorBoundaryFallbackProps {
|
|
66
|
+
/** The thrown value, as it was thrown. Read it with care — see {@link describeThrown}. */
|
|
67
|
+
error: unknown;
|
|
68
|
+
details: ErrorBoundaryDetails;
|
|
69
|
+
/** Clear the error and render the children again. */
|
|
70
|
+
reset: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ErrorBoundaryProps {
|
|
74
|
+
children?: ReactNode;
|
|
75
|
+
/** Replaces the default fallback: a node, or a render function handed the error and
|
|
76
|
+
* a `reset`. */
|
|
77
|
+
fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
|
|
78
|
+
/** Called once per caught error — log it, file a crash report. */
|
|
79
|
+
onError?: (error: unknown, info: ErrorInfo) => void;
|
|
80
|
+
/** Called when the error is cleared, by Retry, `reset` or a `resetKeys` change —
|
|
81
|
+
* drop a cache, refetch. */
|
|
82
|
+
onReset?: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* When any of these changes (compared with `Object.is`) AFTER an error was caught, the
|
|
85
|
+
* error is cleared and the children render again — a change in the same update that
|
|
86
|
+
* threw does not count (it is what the error was shown under) — pass the route path so leaving a broken page does not
|
|
87
|
+
* carry its fallback to the next one (keksdose's `resetKey`).
|
|
88
|
+
*/
|
|
89
|
+
resetKeys?: ReadonlyArray<unknown>;
|
|
90
|
+
/** A disclosure under the message with the error's name, message and stack — for an
|
|
91
|
+
* internal tool, or while developing. Off by default: a stack means nothing to a user. */
|
|
92
|
+
showDetails?: boolean;
|
|
93
|
+
/** The default fallback's heading level. Default `h2`: the fallback often IS the page. */
|
|
94
|
+
headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
|
|
95
|
+
/** Classes for the default fallback's box (`m-8 min-h-[400px]`, kastlan). */
|
|
96
|
+
className?: string;
|
|
97
|
+
labels?: Partial<ErrorBoundaryLabels>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ErrorBoundaryState {
|
|
101
|
+
/** A flag of its own, so `throw undefined` is still an error shown. */
|
|
102
|
+
failed: boolean;
|
|
103
|
+
error: unknown;
|
|
104
|
+
details: ErrorBoundaryDetails | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const CLEAR: ErrorBoundaryState = { failed: false, error: null, details: null };
|
|
108
|
+
|
|
109
|
+
function keysChanged(a: ReadonlyArray<unknown> | undefined, b: ReadonlyArray<unknown> | undefined): boolean {
|
|
110
|
+
if (a === b) return false;
|
|
111
|
+
if (!a || !b || a.length !== b.length) return true;
|
|
112
|
+
return a.some((v, i) => !Object.is(v, b[i]));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Catch a render error below it and show a fallback instead of an empty page.
|
|
117
|
+
*
|
|
118
|
+
* kastlan (shared/components/error-boundary.tsx) and keksdose (app/error-boundary.tsx)
|
|
119
|
+
* each built one on `EmptyState tone="danger"`: a warning glyph, a title, a line of
|
|
120
|
+
* explanation and a Retry. This is that fallback, translated through the provider
|
|
121
|
+
* (`errorBoundary.*`), with the hooks an app hangs its own behaviour on — `onError`
|
|
122
|
+
* for the crash report, `resetKeys` for the route change, `fallback` for anything else.
|
|
123
|
+
*
|
|
124
|
+
* The fallback is `role="alert"`: a page replaced by an error is news, and a reader
|
|
125
|
+
* that was on it is told.
|
|
126
|
+
*/
|
|
127
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
128
|
+
state: ErrorBoundaryState = CLEAR;
|
|
129
|
+
|
|
130
|
+
static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {
|
|
131
|
+
return { failed: true, error, details: describeThrown(error) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
componentDidCatch(error: unknown, info: ErrorInfo): void {
|
|
135
|
+
this.props.onError?.(error, info);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
componentDidUpdate(prev: ErrorBoundaryProps, prevState: ErrorBoundaryState): void {
|
|
139
|
+
// Only a change AFTER the error was caught clears it (react-error-boundary's rule).
|
|
140
|
+
// `prevState.failed` is false in the update that caught it: that update may well be
|
|
141
|
+
// the one that changed the keys too — a navigation to a page that throws, with
|
|
142
|
+
// `resetKeys={[pathname]}` — and resetting then would fire `onReset` for nothing and
|
|
143
|
+
// render the failing child a second time. From then on `prev.resetKeys` are the
|
|
144
|
+
// keys the error was shown under, so any change clears it.
|
|
145
|
+
if (this.state.failed && prevState.failed && keysChanged(prev.resetKeys, this.props.resetKeys)) {
|
|
146
|
+
this.reset();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
reset = (): void => {
|
|
151
|
+
this.props.onReset?.();
|
|
152
|
+
this.setState(CLEAR);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
render(): ReactNode {
|
|
156
|
+
const { failed, error, details } = this.state;
|
|
157
|
+
if (!failed || !details) return this.props.children;
|
|
158
|
+
const { fallback } = this.props;
|
|
159
|
+
if (typeof fallback === "function") return fallback({ error, details, reset: this.reset });
|
|
160
|
+
if (fallback !== undefined) return fallback;
|
|
161
|
+
return (
|
|
162
|
+
<ErrorFallback
|
|
163
|
+
details={details}
|
|
164
|
+
reset={this.reset}
|
|
165
|
+
showDetails={this.props.showDetails ?? false}
|
|
166
|
+
headingAs={this.props.headingAs ?? "h2"}
|
|
167
|
+
className={this.props.className}
|
|
168
|
+
labels={this.props.labels}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ErrorFallback({
|
|
175
|
+
details,
|
|
176
|
+
reset,
|
|
177
|
+
showDetails,
|
|
178
|
+
headingAs,
|
|
179
|
+
className,
|
|
180
|
+
labels,
|
|
181
|
+
}: {
|
|
182
|
+
details: ErrorBoundaryDetails;
|
|
183
|
+
reset: () => void;
|
|
184
|
+
showDetails: boolean;
|
|
185
|
+
headingAs: NonNullable<ErrorBoundaryProps["headingAs"]>;
|
|
186
|
+
className?: string;
|
|
187
|
+
labels?: Partial<ErrorBoundaryLabels>;
|
|
188
|
+
}) {
|
|
189
|
+
const text = useKitLabels("errorBoundary", DEFAULT_ERROR_BOUNDARY_LABELS, labels);
|
|
190
|
+
return (
|
|
191
|
+
<EmptyState
|
|
192
|
+
role="alert"
|
|
193
|
+
tone="danger"
|
|
194
|
+
headingAs={headingAs}
|
|
195
|
+
className={className}
|
|
196
|
+
icon={<AlertTriangle />}
|
|
197
|
+
title={text.title}
|
|
198
|
+
hint={text.message}
|
|
199
|
+
action={
|
|
200
|
+
<>
|
|
201
|
+
<Button variant="secondary" onClick={reset}>
|
|
202
|
+
{text.retry}
|
|
203
|
+
</Button>
|
|
204
|
+
{showDetails && (
|
|
205
|
+
<Disclosure variant="bare" title={text.details} className="basis-full text-start">
|
|
206
|
+
<pre className="max-h-60 overflow-auto whitespace-pre-wrap break-words text-xs text-[var(--text-secondary)]">
|
|
207
|
+
{details.message ? `${details.name}: ${details.message}` : details.name}
|
|
208
|
+
{details.stack ? `\n\n${details.stack}` : null}
|
|
209
|
+
</pre>
|
|
210
|
+
</Disclosure>
|
|
211
|
+
)}
|
|
212
|
+
</>
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A field group with its label ABOVE the control: label, control, hint, error.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Field label="IBAN" required hint="22 characters" error={errors.iban}>
|
|
6
|
+
* {(ids) => <Input {...ids} value={iban} onChange={…} />}
|
|
7
|
+
* </Field>
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* The kit's `Input` and `Select` float their label inside the field, and the pickers
|
|
11
|
+
* draw theirs the same way. That is the wrong shape for a control the kit did not
|
|
12
|
+
* render, a radio group, a range slider, or a form that sets its labels above the
|
|
13
|
+
* fields throughout — which is what kastlan's wizards are, and why it kept its own
|
|
14
|
+
* `WizardField` after the kit dropped the one it had never rendered. This is that
|
|
15
|
+
* group, with the wiring the old one left to the caller: the label names the control
|
|
16
|
+
* through `htmlFor`, and the control is described by the hint and the error that are
|
|
17
|
+
* actually on screen.
|
|
18
|
+
*
|
|
19
|
+
* ## Wiring
|
|
20
|
+
*
|
|
21
|
+
* Pass a function as `children` and it receives {@link FieldControlProps} — `id`,
|
|
22
|
+
* `aria-describedby`, `aria-invalid` and `aria-required` — to spread on the control.
|
|
23
|
+
* Plain children render as they are, for a control that wires itself (a checkbox that
|
|
24
|
+
* carries its own label, a group with `aria-labelledby`); `htmlFor` then points the
|
|
25
|
+
* label at the control's own id, and the hint and error are on screen but describe
|
|
26
|
+
* nothing — the same as the `WizardField` this replaces.
|
|
27
|
+
*
|
|
28
|
+
* ## Decoupled
|
|
29
|
+
*
|
|
30
|
+
* Deliberately knows neither the wizard nor react-hook-form: `error` is a plain
|
|
31
|
+
* value. Pass `wizard.fieldErrors.iban`, a step-local error, or
|
|
32
|
+
* `fieldState.error?.message` from a `FormField` render — the markup is the same
|
|
33
|
+
* whichever of them gates the step, so a step can move between them without its
|
|
34
|
+
* fields changing. (Inside a react-hook-form form, `FormItem` + `FormLabel` from
|
|
35
|
+
* `@eifi1/ui-kit/rhf` do the same job with the ids taken from the form.)
|
|
36
|
+
*
|
|
37
|
+
* The error is NOT `role="alert"`, for the reason the kit's `error` prop gives (see
|
|
38
|
+
* `useFieldError` in components/ui.tsx): it is read when focus reaches the control.
|
|
39
|
+
*/
|
|
40
|
+
import { useId, type ReactNode } from "react";
|
|
41
|
+
import { cn } from "../lib/cn";
|
|
42
|
+
import { Label } from "./ui";
|
|
43
|
+
|
|
44
|
+
/** What a `Field` hands its render-prop children, to spread on the control. */
|
|
45
|
+
export interface FieldControlProps {
|
|
46
|
+
/** The id the label's `htmlFor` points at. */
|
|
47
|
+
id: string;
|
|
48
|
+
/** The ids of the hint and the error that are rendered, or `undefined` when
|
|
49
|
+
* neither is. */
|
|
50
|
+
"aria-describedby": string | undefined;
|
|
51
|
+
/** `true` while the field has an error, `undefined` otherwise. */
|
|
52
|
+
"aria-invalid": true | undefined;
|
|
53
|
+
/** `true` when the field is `required`. The label's star is `aria-hidden`; this
|
|
54
|
+
* is what a screen reader hears instead. */
|
|
55
|
+
"aria-required": true | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The second argument of a `Field`'s render-prop children: what a GROUP needs that
|
|
60
|
+
* a single control does not. `htmlFor` names an `<input>`, but a `<label>` pointing
|
|
61
|
+
* at a `role="radiogroup"` div names nothing — so a group (a {@link ToggleGroup})
|
|
62
|
+
* takes `aria-labelledby={labelId}` instead. Kept out of {@link FieldControlProps}
|
|
63
|
+
* so the spread every existing control takes is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export interface FieldRenderMeta {
|
|
66
|
+
/** The label element's id, or `undefined` when the field has no label. */
|
|
67
|
+
labelId: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where the label is visible. `"visible"` (default); `"sr-only"`: never drawn, still
|
|
72
|
+
* the control's name; `"below-sm"` / `"below-md"` / `"below-lg"`: drawn only under
|
|
73
|
+
* that breakpoint and screen-reader-only from it up.
|
|
74
|
+
*
|
|
75
|
+
* For table-like editors — kastlan's journal entry lines
|
|
76
|
+
* (accounting/components/wizard/journal-entry-lines-step.tsx) — where a desktop
|
|
77
|
+
* row sits under column headers that already say "Account" and "Amount", but the
|
|
78
|
+
* phone layout stacks each line as a card and needs the labels back. The name stays
|
|
79
|
+
* on the control at every width; only the ink changes.
|
|
80
|
+
*/
|
|
81
|
+
export type FieldLabelVisibility = "visible" | "sr-only" | "below-sm" | "below-md" | "below-lg";
|
|
82
|
+
|
|
83
|
+
// Literal class strings so Tailwind's scanner sees each one.
|
|
84
|
+
const LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {
|
|
85
|
+
visible: undefined,
|
|
86
|
+
"sr-only": "sr-only",
|
|
87
|
+
"below-sm": "sm:sr-only",
|
|
88
|
+
"below-md": "md:sr-only",
|
|
89
|
+
"below-lg": "lg:sr-only",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export interface FieldProps {
|
|
93
|
+
/** The label above the control. Omit it for a group whose control is labelled
|
|
94
|
+
* some other way; the hint and error still render. */
|
|
95
|
+
label?: ReactNode;
|
|
96
|
+
/** Draws the kit's required mark after the label, and sets `aria-required` on the
|
|
97
|
+
* render-prop control. */
|
|
98
|
+
required?: boolean;
|
|
99
|
+
/** Muted helper text below the control. */
|
|
100
|
+
hint?: ReactNode;
|
|
101
|
+
/** The error below the control. `null`, `false` and `""` are no error — what a
|
|
102
|
+
* `touched && errors.x` evaluates to on the happy path. */
|
|
103
|
+
error?: ReactNode;
|
|
104
|
+
/** The control's id. Defaults to a generated one; set it when the control carries
|
|
105
|
+
* its own id (plain children). */
|
|
106
|
+
htmlFor?: string;
|
|
107
|
+
/** Dims the label with its control. */
|
|
108
|
+
disabled?: boolean;
|
|
109
|
+
/** The label's size: `sm` for a dense row, `md` otherwise. */
|
|
110
|
+
labelSize?: "sm" | "md";
|
|
111
|
+
/** See {@link FieldLabelVisibility}. */
|
|
112
|
+
labelVisibility?: FieldLabelVisibility;
|
|
113
|
+
/** Wrapper className. */
|
|
114
|
+
className?: string;
|
|
115
|
+
/** The control. A function receives {@link FieldControlProps} to spread on it, and
|
|
116
|
+
* {@link FieldRenderMeta} (the label's id, for a group). */
|
|
117
|
+
children: ReactNode | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isShown = (node: ReactNode) =>
|
|
121
|
+
node !== undefined && node !== null && node !== false && node !== "";
|
|
122
|
+
|
|
123
|
+
export function Field({
|
|
124
|
+
label,
|
|
125
|
+
required,
|
|
126
|
+
hint,
|
|
127
|
+
error,
|
|
128
|
+
htmlFor,
|
|
129
|
+
disabled,
|
|
130
|
+
labelSize,
|
|
131
|
+
labelVisibility = "visible",
|
|
132
|
+
className,
|
|
133
|
+
children,
|
|
134
|
+
}: FieldProps) {
|
|
135
|
+
const generated = useId();
|
|
136
|
+
const id = htmlFor ?? `${generated}-control`;
|
|
137
|
+
const hintId = `${generated}-hint`;
|
|
138
|
+
const errorId = `${generated}-error`;
|
|
139
|
+
const labelId = `${generated}-label`;
|
|
140
|
+
const hasHint = isShown(hint);
|
|
141
|
+
const hasError = isShown(error);
|
|
142
|
+
const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
|
|
143
|
+
const control: FieldControlProps = {
|
|
144
|
+
id,
|
|
145
|
+
"aria-describedby": describedBy || undefined,
|
|
146
|
+
"aria-invalid": hasError || undefined,
|
|
147
|
+
"aria-required": required || undefined,
|
|
148
|
+
};
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
data-slot="field"
|
|
152
|
+
className={cn(
|
|
153
|
+
"grid gap-1.5",
|
|
154
|
+
// A hidden label is `position: absolute`; `relative` keeps its containing
|
|
155
|
+
// block here rather than the page's (see sr-only-containment.test).
|
|
156
|
+
labelVisibility !== "visible" && "relative",
|
|
157
|
+
className,
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{label !== undefined && (
|
|
161
|
+
<Label
|
|
162
|
+
id={labelId}
|
|
163
|
+
htmlFor={id}
|
|
164
|
+
required={required}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
size={labelSize}
|
|
167
|
+
data-error={hasError || undefined}
|
|
168
|
+
className={cn("data-[error=true]:text-[var(--danger)]", LABEL_VISIBILITY[labelVisibility])}
|
|
169
|
+
>
|
|
170
|
+
{label}
|
|
171
|
+
</Label>
|
|
172
|
+
)}
|
|
173
|
+
{typeof children === "function"
|
|
174
|
+
? children(control, { labelId: label !== undefined ? labelId : undefined })
|
|
175
|
+
: children}
|
|
176
|
+
{hasHint && (
|
|
177
|
+
<p id={hintId} className="text-[11px] leading-tight text-[var(--text-muted)]">
|
|
178
|
+
{hint}
|
|
179
|
+
</p>
|
|
180
|
+
)}
|
|
181
|
+
{hasError && (
|
|
182
|
+
<p id={errorId} className="text-[11px] leading-tight text-[var(--danger)]">
|
|
183
|
+
{error}
|
|
184
|
+
</p>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|