@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
|
@@ -2,24 +2,37 @@ import { useId, useState } from "react";
|
|
|
2
2
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { Button, Card, FIELD_WRITABLE_LOOK, Input } from "./ui";
|
|
4
4
|
import { UserAvatar } from "./user-avatar";
|
|
5
|
+
import { CopyButton } from "./copy-button";
|
|
6
|
+
import { QrCode } from "./qr-code";
|
|
5
7
|
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
8
|
+
import { useAccountSettingsLabels } from "./account-settings-labels";
|
|
9
|
+
import type {
|
|
10
|
+
PasswordSettingLabels,
|
|
11
|
+
ProfileSettingLabels,
|
|
12
|
+
TwoFactorSettingLabels,
|
|
13
|
+
} from "./account-settings-labels";
|
|
14
|
+
|
|
15
|
+
// The label types moved beside the namespace's defaults (0.12.0); re-exported here so
|
|
16
|
+
// every existing `import type { ProfileSettingLabels }` keeps resolving.
|
|
17
|
+
export type {
|
|
18
|
+
AccountSettingsLabels,
|
|
19
|
+
PasskeysSettingLabels,
|
|
20
|
+
PasswordSettingLabels,
|
|
21
|
+
ProfileSettingLabels,
|
|
22
|
+
TwoFactorSettingLabels,
|
|
23
|
+
} from "./account-settings-labels";
|
|
24
|
+
export { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "./account-settings-labels";
|
|
6
25
|
|
|
7
26
|
/**
|
|
8
27
|
* App-agnostic account-settings SECTIONS (feedback #333). The presentation lives
|
|
9
28
|
* in @hb/ui; each app injects its own auth-API handlers + translated labels and
|
|
10
29
|
* arranges these next to its own app-specific cards (appearance, budgets, …).
|
|
11
30
|
* Every section is a self-contained <Card> owning its transient form state.
|
|
31
|
+
*
|
|
32
|
+
* Every `labels` prop is optional since 0.12.0: the strings come from the provider's
|
|
33
|
+
* `accountSettings` namespace, then English (see account-settings-labels.ts).
|
|
12
34
|
*/
|
|
13
35
|
|
|
14
|
-
export interface ProfileSettingLabels {
|
|
15
|
-
title: string;
|
|
16
|
-
email: string;
|
|
17
|
-
role: string;
|
|
18
|
-
memberSince: string;
|
|
19
|
-
displayName: string;
|
|
20
|
-
save: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
36
|
export function ProfileSetting({
|
|
24
37
|
name,
|
|
25
38
|
email,
|
|
@@ -29,7 +42,7 @@ export function ProfileSetting({
|
|
|
29
42
|
onChange,
|
|
30
43
|
onSave,
|
|
31
44
|
saving,
|
|
32
|
-
labels,
|
|
45
|
+
labels: labelsProp,
|
|
33
46
|
}: {
|
|
34
47
|
name?: string | null;
|
|
35
48
|
email?: string | null;
|
|
@@ -39,8 +52,10 @@ export function ProfileSetting({
|
|
|
39
52
|
onChange: (value: string) => void;
|
|
40
53
|
onSave: () => void;
|
|
41
54
|
saving?: boolean;
|
|
42
|
-
labels:
|
|
55
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */
|
|
56
|
+
labels?: Partial<ProfileSettingLabels>;
|
|
43
57
|
}) {
|
|
58
|
+
const labels = useAccountSettingsLabels("profile", labelsProp);
|
|
44
59
|
const dirty = value.trim().length > 0 && value.trim() !== (name ?? "");
|
|
45
60
|
// Generated, not the literal "display-name" it was: two of these on one page (an
|
|
46
61
|
// admin editing someone else's profile beside their own) shared an id, and the
|
|
@@ -52,7 +67,12 @@ export function ProfileSetting({
|
|
|
52
67
|
<UserAvatar name={name} email={email} size="lg" />
|
|
53
68
|
<div className="min-w-0">
|
|
54
69
|
<div className="text-sm font-medium">{labels.title}</div>
|
|
55
|
-
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
70
|
+
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
71
|
+
{/* `email` was a required key that nothing rendered; it names the address
|
|
72
|
+
for a screen reader, which otherwise hears a bare string under a title. */}
|
|
73
|
+
<span className="sr-only">{labels.email}: </span>
|
|
74
|
+
{email ?? "—"}
|
|
75
|
+
</div>
|
|
56
76
|
</div>
|
|
57
77
|
</div>
|
|
58
78
|
<div className="grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-sm">
|
|
@@ -75,28 +95,20 @@ export function ProfileSetting({
|
|
|
75
95
|
);
|
|
76
96
|
}
|
|
77
97
|
|
|
78
|
-
export interface PasswordSettingLabels {
|
|
79
|
-
title: string;
|
|
80
|
-
current: string;
|
|
81
|
-
next: string;
|
|
82
|
-
confirm: string;
|
|
83
|
-
submit: string;
|
|
84
|
-
tooShort: string;
|
|
85
|
-
mismatch: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
98
|
export function PasswordSetting({
|
|
89
99
|
onSubmit,
|
|
90
100
|
pending,
|
|
91
101
|
minLength = 8,
|
|
92
|
-
labels,
|
|
102
|
+
labels: labelsProp,
|
|
93
103
|
}: {
|
|
94
104
|
/** Called with the validated (current, new) pair; return a promise to auto-clear on success. */
|
|
95
105
|
onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;
|
|
96
106
|
pending?: boolean;
|
|
97
107
|
minLength?: number;
|
|
98
|
-
labels:
|
|
108
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */
|
|
109
|
+
labels?: Partial<PasswordSettingLabels>;
|
|
99
110
|
}) {
|
|
111
|
+
const labels = useAccountSettingsLabels("password", labelsProp);
|
|
100
112
|
const [current, setCurrent] = useState("");
|
|
101
113
|
const [next, setNext] = useState("");
|
|
102
114
|
const [confirm, setConfirm] = useState("");
|
|
@@ -136,34 +148,35 @@ export function PasswordSetting({
|
|
|
136
148
|
);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
/**
|
|
152
|
+
* What the app's setup call returned.
|
|
153
|
+
*
|
|
154
|
+
* `otpauthUri` (0.12.0) is what an authenticator backend actually hands out; the kit
|
|
155
|
+
* draws the QR code itself ({@link QrCode}) and reads the manual-entry key out of the
|
|
156
|
+
* URI's `secret` parameter when `secret` is not given. The `qrSvg` shape — a
|
|
157
|
+
* base64-encoded SVG image — is what the section took before, which made every app
|
|
158
|
+
* render a QR library to a static string and base64 it only for the kit to draw it back.
|
|
159
|
+
*/
|
|
160
|
+
export type TwoFactorSetupData =
|
|
161
|
+
| { qrSvg: string; secret: string }
|
|
162
|
+
| { otpauthUri: string; secret?: string };
|
|
163
|
+
|
|
164
|
+
/** The `secret` parameter of an `otpauth://` URI, or null. */
|
|
165
|
+
function otpauthSecret(uri: string): string | null {
|
|
166
|
+
const match = /[?&]secret=([^&#]*)/i.exec(uri);
|
|
167
|
+
if (!match) return null;
|
|
168
|
+
try {
|
|
169
|
+
return decodeURIComponent(match[1]) || null;
|
|
170
|
+
} catch {
|
|
171
|
+
return match[1] || null;
|
|
172
|
+
}
|
|
161
173
|
}
|
|
162
174
|
|
|
163
|
-
/**
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
/** "JBSW Y3DP EHPK 3PXP": the key in groups of four, which is how an authenticator's
|
|
176
|
+
* manual-entry screen shows it and how a person copies it by eye. */
|
|
177
|
+
function groupSecret(secret: string): string {
|
|
178
|
+
return secret.replace(/\s+/g, "").replace(/(.{4})(?=.)/g, "$1 ");
|
|
179
|
+
}
|
|
167
180
|
|
|
168
181
|
export function TwoFactorSetting({
|
|
169
182
|
enabled,
|
|
@@ -172,17 +185,23 @@ export function TwoFactorSetting({
|
|
|
172
185
|
onEnable,
|
|
173
186
|
onDisable,
|
|
174
187
|
busy,
|
|
175
|
-
|
|
188
|
+
renderQr,
|
|
189
|
+
labels: labelsProp,
|
|
176
190
|
}: {
|
|
177
191
|
enabled: boolean;
|
|
178
|
-
/**
|
|
179
|
-
setup:
|
|
192
|
+
/** What the app's setup call returned; null before setup starts. */
|
|
193
|
+
setup: TwoFactorSetupData | null;
|
|
180
194
|
onStartSetup: () => void;
|
|
181
195
|
onEnable: (code: string) => void;
|
|
182
196
|
onDisable: (password: string, code: string) => void;
|
|
183
197
|
busy?: boolean;
|
|
184
|
-
|
|
198
|
+
/** Draw the code yourself (a branded QR, a library you already ship). Given the
|
|
199
|
+
* `otpauthUri`; the kit's own {@link QrCode} otherwise. Unused with `qrSvg`. */
|
|
200
|
+
renderQr?: (otpauthUri: string) => ReactNode;
|
|
201
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { twoFactor } }}>` > English. */
|
|
202
|
+
labels?: Partial<TwoFactorSettingLabels>;
|
|
185
203
|
}) {
|
|
204
|
+
const labels = useAccountSettingsLabels("twoFactor", labelsProp);
|
|
186
205
|
const [code, setCode] = useState("");
|
|
187
206
|
const [password, setPassword] = useState("");
|
|
188
207
|
// Browsers autofill one-time-code fields with the saved username; keep the
|
|
@@ -195,6 +214,9 @@ export function TwoFactorSetting({
|
|
|
195
214
|
// Only for the "Status: Enabled" composition — the punctuation between a field's
|
|
196
215
|
// name and its value is the language's (see `CommonLabels.fieldValue`).
|
|
197
216
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
217
|
+
const secret = setup
|
|
218
|
+
? (setup.secret ?? ("otpauthUri" in setup ? otpauthSecret(setup.otpauthUri) : null))
|
|
219
|
+
: null;
|
|
198
220
|
|
|
199
221
|
return (
|
|
200
222
|
<Card className="p-4 space-y-3">
|
|
@@ -216,15 +238,40 @@ export function TwoFactorSetting({
|
|
|
216
238
|
their quiet zone. On the dark theme a surface-coloured backing makes it
|
|
217
239
|
slow to acquire, and on a warm light preset it lowers the contrast ratio
|
|
218
240
|
the spec is written against. This is the one place in the kit where a
|
|
219
|
-
literal colour is the correct answer.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
literal colour is the correct answer. `data-private`: the code IS the
|
|
242
|
+
second factor, and demo mode blurs it like any other secret. */}
|
|
243
|
+
{"otpauthUri" in setup ? (
|
|
244
|
+
// No backing of its own: QrCode draws its quiet zone white inside the SVG.
|
|
245
|
+
<div data-private className="inline-block overflow-hidden rounded-md">
|
|
246
|
+
{renderQr ? (
|
|
247
|
+
renderQr(setup.otpauthUri)
|
|
248
|
+
) : (
|
|
249
|
+
<QrCode value={setup.otpauthUri} label={labels.qrAlt} className="size-48" />
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
) : (
|
|
253
|
+
<div data-private className="inline-block rounded-md bg-white p-2">
|
|
254
|
+
<img
|
|
255
|
+
alt={labels.qrAlt}
|
|
256
|
+
src={`data:image/svg+xml;base64,${setup.qrSvg}`}
|
|
257
|
+
className="h-48 w-48"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
{secret && (
|
|
262
|
+
<div className="space-y-1">
|
|
263
|
+
<div className="text-xs text-[var(--text-secondary)]">{labels.secretHint}</div>
|
|
264
|
+
<div className="flex items-center gap-1">
|
|
265
|
+
{/* `dir="ltr"`: a base32 key is Latin letters and digits in a fixed
|
|
266
|
+
order, and in an RTL page the groups would otherwise be laid out
|
|
267
|
+
right to left. */}
|
|
268
|
+
<code data-private dir="ltr" className="break-all font-mono text-xs select-all">
|
|
269
|
+
{groupSecret(secret)}
|
|
270
|
+
</code>
|
|
271
|
+
<CopyButton text={secret} label={labels.copySecret} size="xs" tone="muted" />
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
228
275
|
<Input
|
|
229
276
|
inputMode="numeric"
|
|
230
277
|
autoComplete="one-time-code"
|
|
@@ -140,6 +140,31 @@ interface AlertBannerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "on
|
|
|
140
140
|
variant?: "box" | "inline" | "strip";
|
|
141
141
|
/** See {@link AlertSize}. Default `md`. */
|
|
142
142
|
size?: AlertSize;
|
|
143
|
+
/**
|
|
144
|
+
* `inline` only (0.11.0): lay the message out as a BLOCK — a full-width `flex` row,
|
|
145
|
+
* the glyph on the text's FIRST line when it wraps — instead of the `inline-flex`,
|
|
146
|
+
* centred line it is by default. keksdose writes `className="flex"` on nearly every
|
|
147
|
+
* inline banner it has (accounts-page:564, holdings-panel:631, import-map-step:66/314,
|
|
148
|
+
* guest-key-control:84) to get a line of its own under a total or a field, and a
|
|
149
|
+
* two-line hint there then centres its glyph between the lines.
|
|
150
|
+
*
|
|
151
|
+
* An option rather than the new default, because the inline-flex is load-bearing for
|
|
152
|
+
* the callers the variant was made for: lenkbank's corner-motion toolbar
|
|
153
|
+
* (corner-motion.tsx:178) sets it as a `shrink-0` item beside the controls, kastlan's
|
|
154
|
+
* import page and keksdose's rules page place it in a run of content — a block would
|
|
155
|
+
* take the whole row in the second kind, and top-align the first against a taller
|
|
156
|
+
* control. Ignored by `box` and `strip`, which are blocks already.
|
|
157
|
+
*/
|
|
158
|
+
block?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* `false` opts an inline banner out of its live region (no `role="alert"` /
|
|
161
|
+
* `role="status"`), for a message that is static page content rather than the answer
|
|
162
|
+
* to something the user just did: keksdose's CAMT review step (camt-review-step:214)
|
|
163
|
+
* renders "unresolved rows block the import" as part of the step, and as
|
|
164
|
+
* `role="alert"` a screen reader interrupted with it on every mount. A `role` you pass
|
|
165
|
+
* still wins. Default `true`; the box and the strip are role-less either way.
|
|
166
|
+
*/
|
|
167
|
+
live?: boolean;
|
|
143
168
|
/**
|
|
144
169
|
* An opaque, raised surface (a shadow, and no translucency in dark mode) for a
|
|
145
170
|
* notice that floats over the page — see {@link TONE_ELEVATED}. `box` and `strip`
|
|
@@ -200,6 +225,8 @@ export function AlertBanner({
|
|
|
200
225
|
tone = "danger",
|
|
201
226
|
variant = "box",
|
|
202
227
|
size = "md",
|
|
228
|
+
block = false,
|
|
229
|
+
live = true,
|
|
203
230
|
elevated = false,
|
|
204
231
|
action,
|
|
205
232
|
icon,
|
|
@@ -217,11 +244,13 @@ export function AlertBanner({
|
|
|
217
244
|
const strip = variant === "strip";
|
|
218
245
|
const sm = size === "sm";
|
|
219
246
|
const interactive = href !== undefined || onClick !== undefined;
|
|
220
|
-
|
|
247
|
+
// A block-laid inline banner behaves like a box for layout: a row that may wrap.
|
|
248
|
+
const inlineRow = inline && !block;
|
|
249
|
+
const resolvedRole = role ?? (inline && live ? (tone === "danger" ? "alert" : "status") : undefined);
|
|
221
250
|
|
|
222
251
|
const Glyph = TONE_GLYPH[tone];
|
|
223
252
|
// Onto the first line of a box's text: 2px at 14px/20px, 1px at 12px/16px.
|
|
224
|
-
const nudge =
|
|
253
|
+
const nudge = inlineRow || strip ? undefined : sm ? "mt-px" : "mt-0.5";
|
|
225
254
|
const glyph =
|
|
226
255
|
icon === null ? null : icon !== undefined ? (
|
|
227
256
|
<span
|
|
@@ -239,7 +268,7 @@ export function AlertBanner({
|
|
|
239
268
|
// column, so the action drops under it instead. A box may wrap its text, so it
|
|
240
269
|
// top-aligns and nudges the glyph onto the first line.
|
|
241
270
|
const row = cn(
|
|
242
|
-
|
|
271
|
+
inlineRow ? "inline-flex items-center" : strip ? "flex flex-wrap items-center gap-y-1" : "flex items-start",
|
|
243
272
|
sm ? "gap-1.5 text-xs" : inline ? "gap-1.5 text-sm" : "gap-2 text-sm",
|
|
244
273
|
);
|
|
245
274
|
const frame = inline
|
|
@@ -254,7 +283,7 @@ export function AlertBanner({
|
|
|
254
283
|
const content = (
|
|
255
284
|
<>
|
|
256
285
|
{glyph}
|
|
257
|
-
<span className={cn(!
|
|
286
|
+
<span className={cn(!inlineRow && "flex-1", "min-w-0 text-start")}>{children}</span>
|
|
258
287
|
{interactive && (
|
|
259
288
|
<ChevronRight
|
|
260
289
|
aria-hidden
|
|
@@ -288,7 +317,7 @@ export function AlertBanner({
|
|
|
288
317
|
// `relative z-10` so it stays clickable over a whole-row banner's stretched target.
|
|
289
318
|
const trailing =
|
|
290
319
|
action !== undefined && action !== null ? (
|
|
291
|
-
<div className={cn("relative z-10 flex shrink-0 items-center gap-2", !strip && !
|
|
320
|
+
<div className={cn("relative z-10 flex shrink-0 items-center gap-2", !strip && !inlineRow && "self-center", strip && "ms-auto")}>
|
|
292
321
|
{action}
|
|
293
322
|
</div>
|
|
294
323
|
) : null;
|
|
@@ -319,12 +348,12 @@ export function AlertBanner({
|
|
|
319
348
|
? cn(
|
|
320
349
|
"flex min-w-0 flex-1 text-start",
|
|
321
350
|
sm ? "gap-1.5" : "gap-2",
|
|
322
|
-
|
|
351
|
+
inlineRow || strip ? "items-center" : "items-start",
|
|
323
352
|
"outline-none after:absolute after:inset-0 after:content-['']",
|
|
324
353
|
strip ? "after:rounded-none" : "after:rounded-md",
|
|
325
354
|
"focus-visible:after:ring-2 focus-visible:after:ring-[var(--brand)]",
|
|
326
355
|
)
|
|
327
|
-
: cn(row, !
|
|
356
|
+
: cn(row, !inlineRow && "w-full", frame, TONE_TEXT[tone], hover, focusRing, className);
|
|
328
357
|
// The div's attributes are the row element's: an `id`, a test id or an
|
|
329
358
|
// `aria-describedby` belongs on the thing that takes focus. The live-region role is
|
|
330
359
|
// NOT carried over — a button or a link that is also a `status` is neither.
|
|
@@ -38,6 +38,15 @@ interface AmountInputProps {
|
|
|
38
38
|
className?: string;
|
|
39
39
|
id?: string;
|
|
40
40
|
ariaLabel?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —
|
|
43
|
+
* `{(ids) => <AmountInput {...ids} … />}` — is typed and reaches the input: the
|
|
44
|
+
* hint and error are described, a required amount is announced as required, and
|
|
45
|
+
* `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.
|
|
46
|
+
*/
|
|
47
|
+
"aria-describedby"?: string;
|
|
48
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
49
|
+
"aria-required"?: boolean | "true" | "false";
|
|
41
50
|
/** Every user-facing string this component and the two it composes own, so a
|
|
42
51
|
* translating host can supply its own. Each key optional, and each a per-field
|
|
43
52
|
* override of the kit-wide translation in `<UiKitProvider labels>`: `currency`
|
|
@@ -183,9 +192,10 @@ function isResultOf(previous: string, text: string): boolean {
|
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
186
|
-
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid, className, id, ariaLabel, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames }, ref) => {
|
|
195
|
+
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames }, ref) => {
|
|
187
196
|
const generatedId = useId();
|
|
188
197
|
const fieldId = id ?? generatedId;
|
|
198
|
+
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
189
199
|
const editable = !!onCurrencyChange;
|
|
190
200
|
// On phones we suppress the OS keyboard (inputMode="none" below) and show our
|
|
191
201
|
// own calculator numpad, so the desktop popover trigger is hidden. The
|
|
@@ -350,6 +360,8 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
350
360
|
invalid && FIELD_INVALID,
|
|
351
361
|
)}
|
|
352
362
|
aria-invalid={invalid || undefined}
|
|
363
|
+
aria-describedby={ariaDescribedBy}
|
|
364
|
+
aria-required={ariaRequired}
|
|
353
365
|
/>
|
|
354
366
|
{label !== undefined && (
|
|
355
367
|
// The display shape drops the label VISUALLY, not from the accessibility
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ImgHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { ImageOff } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
7
|
+
import { SKELETON_CLASS } from "./skeleton";
|
|
8
|
+
import { useAuthedSrc } from "../hooks/use-authed-src";
|
|
9
|
+
import type { AuthedFetcher, UseAuthedSrcResult } from "../hooks/use-authed-src";
|
|
10
|
+
|
|
11
|
+
/** The two states an image can be in besides "there". */
|
|
12
|
+
export interface AuthedImageLabels {
|
|
13
|
+
/** Read out while the bytes are on their way (the skeleton itself is silent). */
|
|
14
|
+
loading: string;
|
|
15
|
+
/** Shown, small, in place of an image that could not be fetched or decoded. */
|
|
16
|
+
loadError: string;
|
|
17
|
+
/** The failed tile's accessible name, given the image's alt. */
|
|
18
|
+
failedImage: (alt: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_AUTHED_IMAGE_LABELS: AuthedImageLabels = {
|
|
22
|
+
loading: "Loading image…",
|
|
23
|
+
loadError: "Image couldn’t be loaded",
|
|
24
|
+
failedImage: (alt) => `${alt}: image couldn’t be loaded`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface AuthedImageProps
|
|
28
|
+
extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children"> {
|
|
29
|
+
/** The image's URL — an API path when a {@link fetcher} is given. */
|
|
30
|
+
src: string | null | undefined;
|
|
31
|
+
/** Required: an image without a text alternative is a gap a reader falls into.
|
|
32
|
+
* `""` for a purely decorative one. */
|
|
33
|
+
alt: string;
|
|
34
|
+
/** The app's authenticated GET (see {@link useAuthedSrc}). Left out, `src` is loaded
|
|
35
|
+
* by the browser as usual and this is an `<img>` with loading and error states. */
|
|
36
|
+
fetcher?: AuthedFetcher | null;
|
|
37
|
+
/** Classes for the box around the image (the placeholders fill it). */
|
|
38
|
+
wrapperClassName?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Wrap the image in a link to itself, opening in a new tab — kastlan's
|
|
41
|
+
* `AuthedImage` did, because an object URL is the only address of a protected image
|
|
42
|
+
* a browser tab can open. The link's href is the `blob:` URL, never the API path.
|
|
43
|
+
*/
|
|
44
|
+
link?: boolean;
|
|
45
|
+
/** Replaces the default skeleton while loading. */
|
|
46
|
+
loadingFallback?: ReactNode;
|
|
47
|
+
/** Replaces the default "couldn't load" tile. */
|
|
48
|
+
errorFallback?: ReactNode;
|
|
49
|
+
labels?: Partial<AuthedImageLabels>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An image with a loading state, an error state and — given a `fetcher` — the app's
|
|
54
|
+
* authentication. Replaces kastlan's `shared/components/authed-image.tsx`, which
|
|
55
|
+
* rendered NOTHING until the blob arrived and nothing at all if it never did: a card
|
|
56
|
+
* with a photo slot either had the photo or was silently shorter.
|
|
57
|
+
*
|
|
58
|
+
* Two failures are one state here: the fetch failing (401, 404, offline) and the
|
|
59
|
+
* browser failing to decode what arrived (`onError` on the `<img>`, e.g. a PDF served
|
|
60
|
+
* where an image was expected).
|
|
61
|
+
*/
|
|
62
|
+
export function AuthedImage({
|
|
63
|
+
src,
|
|
64
|
+
alt,
|
|
65
|
+
fetcher,
|
|
66
|
+
wrapperClassName,
|
|
67
|
+
className,
|
|
68
|
+
link,
|
|
69
|
+
loadingFallback,
|
|
70
|
+
errorFallback,
|
|
71
|
+
labels: labelsProp,
|
|
72
|
+
onLoad,
|
|
73
|
+
onError,
|
|
74
|
+
...rest
|
|
75
|
+
}: AuthedImageProps) {
|
|
76
|
+
const labels = useKitLabels("authedImage", DEFAULT_AUTHED_IMAGE_LABELS, labelsProp);
|
|
77
|
+
const fetched = useAuthedSrc(src, { fetcher });
|
|
78
|
+
return (
|
|
79
|
+
<ImageStates
|
|
80
|
+
fetched={fetched}
|
|
81
|
+
alt={alt}
|
|
82
|
+
labels={labels}
|
|
83
|
+
className={className}
|
|
84
|
+
wrapperClassName={wrapperClassName}
|
|
85
|
+
link={link}
|
|
86
|
+
loadingFallback={loadingFallback}
|
|
87
|
+
errorFallback={errorFallback}
|
|
88
|
+
imgProps={rest}
|
|
89
|
+
onLoad={onLoad}
|
|
90
|
+
onError={onError}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The rendering half of {@link AuthedImage}, for a caller that already holds a
|
|
97
|
+
* {@link useAuthedSrc} result (the lightbox needs the object URL for its download link
|
|
98
|
+
* as well). Internal to the package.
|
|
99
|
+
*/
|
|
100
|
+
export function ImageStates({
|
|
101
|
+
fetched,
|
|
102
|
+
alt,
|
|
103
|
+
labels,
|
|
104
|
+
className,
|
|
105
|
+
wrapperClassName,
|
|
106
|
+
link,
|
|
107
|
+
loadingFallback,
|
|
108
|
+
errorFallback,
|
|
109
|
+
imgProps,
|
|
110
|
+
onLoad,
|
|
111
|
+
onError,
|
|
112
|
+
}: {
|
|
113
|
+
fetched: UseAuthedSrcResult;
|
|
114
|
+
alt: string;
|
|
115
|
+
labels: AuthedImageLabels;
|
|
116
|
+
className?: string;
|
|
117
|
+
wrapperClassName?: string;
|
|
118
|
+
link?: boolean;
|
|
119
|
+
loadingFallback?: ReactNode;
|
|
120
|
+
errorFallback?: ReactNode;
|
|
121
|
+
imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children" | "onLoad" | "onError">;
|
|
122
|
+
onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
|
|
123
|
+
onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
|
|
124
|
+
}) {
|
|
125
|
+
// Decode state, per src: keyed so a new picture starts over rather than inheriting
|
|
126
|
+
// the previous one's "loaded" or "broken".
|
|
127
|
+
const [decode, setDecode] = useState<{ src: string | null; state: "loaded" | "error" } | null>(null);
|
|
128
|
+
const decoded = decode && decode.src === fetched.src ? decode.state : "pending";
|
|
129
|
+
const failed = fetched.status === "error" || decoded === "error";
|
|
130
|
+
const waiting = !failed && (fetched.status === "loading" || (fetched.src !== null && decoded === "pending"));
|
|
131
|
+
|
|
132
|
+
const img = fetched.src !== null && !failed && (
|
|
133
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- load/error are resource events, not interactions
|
|
134
|
+
<img
|
|
135
|
+
{...imgProps}
|
|
136
|
+
src={fetched.src}
|
|
137
|
+
alt={alt}
|
|
138
|
+
onLoad={(e) => {
|
|
139
|
+
setDecode({ src: fetched.src, state: "loaded" });
|
|
140
|
+
onLoad?.(e);
|
|
141
|
+
}}
|
|
142
|
+
onError={(e) => {
|
|
143
|
+
setDecode({ src: fetched.src, state: "error" });
|
|
144
|
+
onError?.(e);
|
|
145
|
+
}}
|
|
146
|
+
className={cn(
|
|
147
|
+
// Kept in the tree while it decodes (the browser needs it there to load at
|
|
148
|
+
// all), but invisible, so the skeleton is what shows.
|
|
149
|
+
decoded === "pending" && "opacity-0",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<span
|
|
157
|
+
data-image-state={failed ? "error" : waiting ? "loading" : fetched.status === "idle" ? "idle" : "ready"}
|
|
158
|
+
className={cn("relative block overflow-hidden", wrapperClassName)}
|
|
159
|
+
aria-busy={waiting || undefined}
|
|
160
|
+
>
|
|
161
|
+
{link && img && fetched.src ? (
|
|
162
|
+
<a href={fetched.src} target="_blank" rel="noreferrer noopener" className="block h-full w-full">
|
|
163
|
+
{img}
|
|
164
|
+
</a>
|
|
165
|
+
) : (
|
|
166
|
+
img
|
|
167
|
+
)}
|
|
168
|
+
{waiting &&
|
|
169
|
+
(loadingFallback ?? (
|
|
170
|
+
<span className={cn(SKELETON_CLASS, "absolute inset-0 block rounded-none")}>
|
|
171
|
+
<span className="sr-only">{labels.loading}</span>
|
|
172
|
+
</span>
|
|
173
|
+
))}
|
|
174
|
+
{failed &&
|
|
175
|
+
(errorFallback ?? (
|
|
176
|
+
<span
|
|
177
|
+
// `role="img"` with the alt as its name: the picture that failed still
|
|
178
|
+
// occupies its place in the reading order, and "couldn't be loaded" is
|
|
179
|
+
// the one fact a reader needs about it.
|
|
180
|
+
role="img"
|
|
181
|
+
aria-label={alt ? labels.failedImage(alt) : labels.loadError}
|
|
182
|
+
className="flex h-full min-h-16 w-full flex-col items-center justify-center gap-1 bg-[var(--bg-hover)] p-2 text-center text-xs text-[var(--text-muted)]"
|
|
183
|
+
>
|
|
184
|
+
<ImageOff aria-hidden className="size-5 shrink-0" />
|
|
185
|
+
<span aria-hidden className="line-clamp-2">{labels.loadError}</span>
|
|
186
|
+
</span>
|
|
187
|
+
))}
|
|
188
|
+
</span>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -2,7 +2,8 @@ import { Fragment, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
3
3
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
6
|
+
import { pickLinkRenderer, RenderedKitLink } from "./text-link";
|
|
6
7
|
|
|
7
8
|
export interface BreadcrumbsLabels {
|
|
8
9
|
/** The `nav` landmark's name — what a screen reader lists it as. */
|
|
@@ -35,7 +36,8 @@ export interface BreadcrumbLinkProps {
|
|
|
35
36
|
|
|
36
37
|
export interface BreadcrumbsProps extends Omit<ComponentPropsWithoutRef<"nav">, "children"> {
|
|
37
38
|
items: BreadcrumbItem[];
|
|
38
|
-
/** Your router's link. Default `<
|
|
39
|
+
/** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app
|
|
40
|
+
* `href`), then `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */
|
|
39
41
|
renderLink?: (props: BreadcrumbLinkProps) => ReactElement;
|
|
40
42
|
/**
|
|
41
43
|
* The mark between crumbs. Default a chevron, mirrored in RTL — a chevron pointing
|
|
@@ -78,6 +80,7 @@ export function Breadcrumbs({
|
|
|
78
80
|
...rest
|
|
79
81
|
}: BreadcrumbsProps) {
|
|
80
82
|
const labels = useKitLabels("breadcrumbs", DEFAULT_BREADCRUMBS_LABELS, labelsProp);
|
|
83
|
+
const kitLink = useKitLink();
|
|
81
84
|
const [expanded, setExpanded] = useState(false);
|
|
82
85
|
// The "…" button vanishes when pressed; focus goes to the first crumb it revealed,
|
|
83
86
|
// not back to the top of the document.
|
|
@@ -121,8 +124,11 @@ export function Breadcrumbs({
|
|
|
121
124
|
);
|
|
122
125
|
} else if (item.href !== undefined) {
|
|
123
126
|
const props: BreadcrumbLinkProps = { href: item.href, className: linkClass, children: item.label };
|
|
127
|
+
const kit = pickLinkRenderer<BreadcrumbLinkProps>(undefined, kitLink, item.href);
|
|
124
128
|
content = renderLink ? (
|
|
125
129
|
renderLink(props)
|
|
130
|
+
) : kit ? (
|
|
131
|
+
<RenderedKitLink render={kit} props={props} />
|
|
126
132
|
) : (
|
|
127
133
|
<a href={props.href} className={props.className}>
|
|
128
134
|
{props.children}
|