@eifi1/ui-kit 0.4.0 → 0.5.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/LICENSE +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +91 -20
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- 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 +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +96 -20
- package/dist/components/dropdown.js +77 -33
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +38 -6
- package/dist/components/file-dropzone.js +34 -9
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +28 -2
- package/dist/components/number-input.js +12 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +32 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +15 -4
- package/dist/components/user-avatar.js +5 -4
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +27 -5
- package/dist/theme/palette-presets.js +231 -33
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +67 -14
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { useEffect, useRef } from "react";
|
|
2
3
|
const SENTINEL_KEY = "__hbUiOverlayHistory";
|
|
3
4
|
const stack = [];
|
|
@@ -22,13 +23,36 @@ function forgetPushed(id) {
|
|
|
22
23
|
const at = pushed.findIndex((p) => p.id === id);
|
|
23
24
|
if (at >= 0) pushed.splice(at, 1);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
let standing = null;
|
|
27
|
+
function taggedSentinel() {
|
|
26
28
|
const state = typeof window !== "undefined" ? window.history.state : null;
|
|
27
29
|
if (!state || typeof state !== "object") return null;
|
|
28
30
|
const id = state[SENTINEL_KEY];
|
|
29
31
|
return typeof id === "string" ? id : null;
|
|
30
32
|
}
|
|
33
|
+
function retag(id) {
|
|
34
|
+
const prev = window.history.state;
|
|
35
|
+
window.history.replaceState(
|
|
36
|
+
{ ...prev && typeof prev === "object" ? prev : {}, [SENTINEL_KEY]: id },
|
|
37
|
+
""
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function currentSentinel() {
|
|
41
|
+
const tag = taggedSentinel();
|
|
42
|
+
if (tag !== null) {
|
|
43
|
+
standing = tag;
|
|
44
|
+
return tag;
|
|
45
|
+
}
|
|
46
|
+
if (standing === null || typeof window === "undefined") return null;
|
|
47
|
+
if (!pushed.some((p) => p.id === standing)) {
|
|
48
|
+
standing = null;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
retag(standing);
|
|
52
|
+
return standing;
|
|
53
|
+
}
|
|
31
54
|
function handlePop() {
|
|
55
|
+
standing = taggedSentinel();
|
|
32
56
|
if (pendingProgrammatic > 0) {
|
|
33
57
|
pendingProgrammatic -= 1;
|
|
34
58
|
return;
|
|
@@ -67,11 +91,13 @@ function useOverlayHistory(open, onClose) {
|
|
|
67
91
|
window.history.replaceState(marked, "");
|
|
68
92
|
for (const owed of inherited.slice(0, -1)) pushed.push({ ...owed, dead: true });
|
|
69
93
|
pushed.push({ id, href: currentHref(), dead: false });
|
|
94
|
+
standing = id;
|
|
70
95
|
} else {
|
|
71
96
|
const under = pushed.findIndex((p) => p.id === currentSentinel());
|
|
72
97
|
pushed.splice(under + 1);
|
|
73
98
|
window.history.pushState(marked, "");
|
|
74
99
|
pushed.push({ id, href: currentHref(), dead: false });
|
|
100
|
+
standing = id;
|
|
75
101
|
}
|
|
76
102
|
return () => {
|
|
77
103
|
const at = stack.indexOf(entry);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/use-overlay-history.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/**\n * Back closes the overlay, not the page (Keksdose feedback #172).\n *\n * On a phone the Back gesture is the universal \"dismiss this\" — so a dialog or a\n * full-screen search surface that ignores it doesn't merely fail to close: the\n * navigation lands somewhere else entirely, and on a route with nothing beneath it\n * that means leaving the app with the overlay still notionally open.\n *\n * The mechanism is one throwaway, SAME-URL history entry pushed while the overlay is\n * up. Back then pops a same-document entry — no route change, no re-render of the\n * page under it — and this hook turns that pop into `onClose()`. Same trick the\n * transactions page already uses for its unsaved-edit trap\n * (`use-soft-leave-guard.ts`); the two coexist because they use different marker\n * keys AND because `handlePop` only claims a pop that actually consumed OUR entry\n * — a foreign sentinel stacked on top of ours pops back ONTO us, and an overlay\n * that closed on that would vanish mid-edit (feedback #424/#426).\n *\n * Not a URL parameter, deliberately. Encoding \"a dialog is open\" in the address bar\n * makes it shareable and restorable, which is wrong for the overlays this covers —\n * nobody wants to send someone a link that opens a confirm dialog — and it would\n * fight the page-owned `?row=` / `f.*` params that ARE meaningful state.\n *\n * ## Ordering\n *\n * The stack is module-level because correctness here is about order ACROSS\n * instances: every mounted overlay hears the same `popstate`, but only the one whose\n * entry was actually popped may close. Overlays nest and unwind last-in-first-out —\n * a dialog opened from a search surface closes before the surface — so the top of\n * this stack is by construction the entry the browser just popped.\n *\n * ## Not undoing a real navigation\n *\n * The cleanup pops our entry when the overlay closes some other way (a button,\n * Escape, a backdrop click) so history doesn't silently accumulate dead entries. It\n * first checks that our marker is still the CURRENT entry: if the user followed a\n * link while the overlay was open, the router has pushed on top of us, and a blind\n * `history.back()` there would undo their navigation.\n *\n * ## The traversal is deferred\n *\n * And it is *scheduled* rather than issued, because `history.go()` lands a task\n * later while everything this module knows is written synchronously. See\n * {@link Unwind}: an overlay opening inside that window adopts the entry instead\n * of stacking a second one on it, which is what makes the hook survive React\n * StrictMode's mount → clean up → mount (steering-design feedback #49).\n */\nconst SENTINEL_KEY = \"__hbUiOverlayHistory\";\n\ninterface OverlayEntry {\n id: string;\n close: () => void;\n}\n\nconst stack: OverlayEntry[] = [];\n\n/**\n * The sentinels we have PUSHED and not yet unwound, oldest first.\n *\n * The stack above tracks live overlays; this tracks live history entries, and the\n * two come apart in exactly one case — which is the bug this list exists for.\n * Nested overlays that close in the same commit unwind parent-first, so the dialog's\n * cleanup runs while the sheet's entry is still on top of its own. It cannot call\n * `history.back()` there (that would consume the SHEET's entry, and the sheet is\n * about to consume it itself), so its entry was simply abandoned: a husk with no\n * live owner sitting between the user and the page.\n *\n * The user paid for it on the next Back press. It popped the husk, `handlePop` found\n * an empty stack and returned, and nothing visible happened — Back had to be pressed\n * twice to leave the page.\n *\n * So an abandoned entry is recorded as `dead` instead, and whichever overlay DOES\n * get to unwind pays the debt with a single `history.go(-n)`. `href` is what keeps\n * that safe: only dead entries pushed at the address we are still sitting at are\n * unwound, so a router navigation that landed between two sentinels is never\n * reversed on the user's behalf.\n */\ninterface PushedEntry {\n id: string;\n href: string;\n /** Its overlay is gone but the entry is not — someone above owes this pop. */\n dead: boolean;\n}\nconst pushed: PushedEntry[] = [];\n\n/** Entries a real Back press already consumed. Their overlay's cleanup still has to\n * run, and it must not mistake \"my entry is not current\" for \"my entry is buried\"\n * — it has no entry left at all. */\nconst consumed = new Set<string>();\n\n/** Pops WE caused (a cleanup unwinding its own entries) — their `popstate` is\n * bookkeeping, not a user gesture, and must not close a second overlay. A\n * `history.go(-n)` traversal fires exactly ONE popstate however far it travels. */\nlet pendingProgrammatic = 0;\nlet listening = false;\nlet nextId = 0;\n\n/**\n * The unwind a cleanup has asked for and not yet issued.\n *\n * **`history.go()` is asynchronous and everything above is written\n * synchronously**, which is the seam the whole of this block exists to close.\n * The traversal lands a task later, so an overlay that opens in the *same* tick\n * as one that closed used to push its sentinel on top of the entry that was\n * about to be popped — and the pop then took the NEW entry. The overlay left on\n * screen had no entry of its own (so Back no longer closed it) and the old one\n * stayed behind as a husk between the user and their page.\n *\n * That is not an exotic race. **React StrictMode remounts every effect** — mount,\n * clean up, mount again, all in one commit — so in development *every* dialog\n * anyone opened left a husk behind, the position drifted one entry further from\n * the page on each open, and the Back press that was meant to leave the page was\n * spent on something invisible instead (steering-design feedback #49).\n *\n * So the traversal is deferred by a task, and an overlay mounting inside that\n * window **adopts** the entry instead of pushing a second one (see the effect).\n * Adoption is `replaceState` on an entry that is already there: no traversal, no\n * window in which the count can be wrong, and the StrictMode remount costs\n * exactly nothing.\n */\ninterface Unwind {\n timer: ReturnType<typeof setTimeout>;\n /** The sentinel on the entry to leave — the top of `entries`. */\n id: string;\n href: string;\n /** That entry plus the abandoned ones directly beneath it, oldest first. */\n entries: PushedEntry[];\n}\nlet unwind: Unwind | null = null;\n\n/** Issue the deferred traversal, if the ground has not moved under it.\n *\n * A real Back press or a router navigation during the deferred task leaves us\n * somewhere that is not the entry we meant to leave, and going back from there\n * would take something that is not ours. The entries are then simply forgotten\n * rather than handed back as `dead`: a forgotten entry costs one dead Back\n * press, while an over-counted one costs the user a navigation they did make. */\nfunction runUnwind(): void {\n const job = unwind;\n unwind = null;\n if (!job) return;\n if (currentSentinel() !== job.id || currentHref() !== job.href) return;\n pendingProgrammatic += 1;\n window.history.go(-job.entries.length);\n}\n\nfunction currentHref(): string {\n return typeof window === \"undefined\" ? \"\" : window.location.href;\n}\n\nfunction forgetPushed(id: string): void {\n const at = pushed.findIndex((p) => p.id === id);\n if (at >= 0) pushed.splice(at, 1);\n}\n\nfunction currentSentinel(): string | null {\n const state = typeof window !== \"undefined\" ? window.history.state : null;\n if (!state || typeof state !== \"object\") return null;\n const id = (state as Record<string, unknown>)[SENTINEL_KEY];\n return typeof id === \"string\" ? id : null;\n}\n\nfunction handlePop() {\n if (pendingProgrammatic > 0) {\n pendingProgrammatic -= 1;\n return;\n }\n // Nothing open → an ordinary navigation, which is none of our business.\n const top = stack[stack.length - 1];\n if (!top) return;\n // We landed back ON the top overlay's own entry, so that entry was NOT what the\n // pop consumed — something stacked ABOVE it was (Keksdose feedback #424/#426).\n // The app layers a second same-URL sentinel of its own on top of ours for the\n // unsaved-edit trap (`use-soft-leave-guard.ts`), and it pops that sentinel every\n // time the form goes back to clean — twice-toggling one field was enough to\n // close the whole dialog. A pop is only ours when the entry we came to rest on\n // is no longer ours: either the layer below (a different overlay's marker) or\n // the plain page entry (none at all).\n if (currentSentinel() === top.id) return;\n stack.pop();\n // The browser consumed this entry, so the overlay's own cleanup has nothing left\n // to unwind and nothing to record as owed.\n consumed.add(top.id);\n forgetPushed(top.id);\n top.close();\n}\n\nfunction ensureListening() {\n if (listening || typeof window === \"undefined\") return;\n window.addEventListener(\"popstate\", handlePop);\n listening = true;\n}\n\n/**\n * Make the platform Back gesture dismiss an overlay.\n *\n * @param open whether the overlay is currently showing. Components that mount\n * only while open (the common case) pass `true`.\n * @param onClose the overlay's own close handler — the same one Escape calls.\n */\nexport function useOverlayHistory(open: boolean, onClose: () => void): void {\n // The listener fires outside React's render, so it reads the latest handler\n // through a ref rather than through the closure the effect captured.\n const closeRef = useRef(onClose);\n useEffect(() => {\n closeRef.current = onClose;\n });\n\n useEffect(() => {\n if (!open || typeof window === \"undefined\") return;\n ensureListening();\n const id = `overlay-${(nextId += 1)}`;\n const entry: OverlayEntry = { id, close: () => closeRef.current() };\n stack.push(entry);\n const prev = window.history.state;\n const marked = { ...(prev && typeof prev === \"object\" ? prev : {}), [SENTINEL_KEY]: id };\n // An entry that is on its way out and that we are still standing on is ours\n // to take over: same URL, same slot, only the marker changes. Pushing a\n // second one on top of it is what used to leave a husk on every StrictMode\n // remount — see {@link Unwind}.\n const adopting = unwind && unwind.id === currentSentinel() && unwind.href === currentHref();\n if (adopting && unwind) {\n clearTimeout(unwind.timer);\n const inherited = unwind.entries;\n unwind = null;\n // Preserve react-router's own `{usr,key,idx}` — we only swap the marker,\n // and the entry we are swapping it on is the one the router is already on.\n window.history.replaceState(marked, \"\");\n // The abandoned ones beneath come back owed; ours takes the top slot.\n for (const owed of inherited.slice(0, -1)) pushed.push({ ...owed, dead: true });\n pushed.push({ id, href: currentHref(), dead: false });\n } else {\n // A push DESTROYS every entry ahead of the one we are on, so anything we\n // still had recorded above our own position is gone. Kept, those would be\n // counted into a later `history.go(-n)` and the overshoot would come out\n // of a navigation the user actually made. Where we cannot tell which of\n // ours are beneath us — we are standing on a plain page entry, or on a\n // husk nobody tracks — none of them can be proven to be, so none survive.\n const under = pushed.findIndex((p) => p.id === currentSentinel());\n pushed.splice(under + 1);\n // Preserve react-router's own `{usr,key,idx}` — we only tack a marker on, and\n // the URL is unchanged, so the router treats popping this as a no-op re-render.\n window.history.pushState(marked, \"\");\n pushed.push({ id, href: currentHref(), dead: false });\n }\n return () => {\n const at = stack.indexOf(entry);\n if (at >= 0) stack.splice(at, 1);\n\n // A Back press already took this entry; there is nothing to unwind or to owe.\n if (consumed.delete(id)) return;\n\n const mine = pushed.findIndex((p) => p.id === id);\n // Only unwind an entry that is still THE current one. If a router push landed\n // on top of it, going back would undo the user's navigation instead — and if\n // a SIBLING overlay's sentinel landed on top, that overlay is about to unwind\n // and is the one that can take ours with it.\n if (currentSentinel() !== id) {\n if (mine >= 0) pushed[mine].dead = true;\n return;\n }\n\n // Ours plus every abandoned entry lying directly beneath it at this address.\n // One traversal, so one popstate, so one programmatic pop to absorb.\n const here = currentHref();\n let steps = 1;\n while (steps <= mine && pushed[mine - steps].dead && pushed[mine - steps].href === here) {\n steps += 1;\n }\n const leaving = pushed.splice(mine - (steps - 1), steps);\n // Scheduled, not issued: an overlay opening in this same tick takes the\n // entry over instead, which is what makes the hook survive a StrictMode\n // remount. An unwind already waiting is dropped rather than merged — it\n // was the entry ABOVE ours and something has since moved off it, so its\n // count can no longer be proven, and an over-counted traversal costs a\n // navigation the user made while an under-counted one costs one dead Back.\n if (unwind) clearTimeout(unwind.timer);\n unwind = { timer: setTimeout(runUnwind, 0), id, href: here, entries: leaving };\n };\n }, [open]);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/use-overlay-history.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/**\n * Back closes the overlay, not the page (Keksdose feedback #172).\n *\n * On a phone the Back gesture is the universal \"dismiss this\" — so a dialog or a\n * full-screen search surface that ignores it doesn't merely fail to close: the\n * navigation lands somewhere else entirely, and on a route with nothing beneath it\n * that means leaving the app with the overlay still notionally open.\n *\n * The mechanism is one throwaway, SAME-URL history entry pushed while the overlay is\n * up. Back then pops a same-document entry — no route change, no re-render of the\n * page under it — and this hook turns that pop into `onClose()`. Same trick the\n * transactions page already uses for its unsaved-edit trap\n * (`use-soft-leave-guard.ts`); the two coexist because they use different marker\n * keys AND because `handlePop` only claims a pop that actually consumed OUR entry\n * — a foreign sentinel stacked on top of ours pops back ONTO us, and an overlay\n * that closed on that would vanish mid-edit (feedback #424/#426).\n *\n * Not a URL parameter, deliberately. Encoding \"a dialog is open\" in the address bar\n * makes it shareable and restorable, which is wrong for the overlays this covers —\n * nobody wants to send someone a link that opens a confirm dialog — and it would\n * fight the page-owned `?row=` / `f.*` params that ARE meaningful state.\n *\n * ## Ordering\n *\n * The stack is module-level because correctness here is about order ACROSS\n * instances: every mounted overlay hears the same `popstate`, but only the one whose\n * entry was actually popped may close. Overlays nest and unwind last-in-first-out —\n * a dialog opened from a search surface closes before the surface — so the top of\n * this stack is by construction the entry the browser just popped.\n *\n * ## Not undoing a real navigation\n *\n * The cleanup pops our entry when the overlay closes some other way (a button,\n * Escape, a backdrop click) so history doesn't silently accumulate dead entries. It\n * first checks that our marker is still the CURRENT entry: if the user followed a\n * link while the overlay was open, the router has pushed on top of us, and a blind\n * `history.back()` there would undo their navigation.\n *\n * ## The traversal is deferred\n *\n * And it is *scheduled* rather than issued, because `history.go()` lands a task\n * later while everything this module knows is written synchronously. See\n * {@link Unwind}: an overlay opening inside that window adopts the entry instead\n * of stacking a second one on it, which is what makes the hook survive React\n * StrictMode's mount → clean up → mount (steering-design feedback #49).\n */\nconst SENTINEL_KEY = \"__hbUiOverlayHistory\";\n\ninterface OverlayEntry {\n id: string;\n close: () => void;\n}\n\nconst stack: OverlayEntry[] = [];\n\n/**\n * The sentinels we have PUSHED and not yet unwound, oldest first.\n *\n * The stack above tracks live overlays; this tracks live history entries, and the\n * two come apart in exactly one case — which is the bug this list exists for.\n * Nested overlays that close in the same commit unwind parent-first, so the dialog's\n * cleanup runs while the sheet's entry is still on top of its own. It cannot call\n * `history.back()` there (that would consume the SHEET's entry, and the sheet is\n * about to consume it itself), so its entry was simply abandoned: a husk with no\n * live owner sitting between the user and the page.\n *\n * The user paid for it on the next Back press. It popped the husk, `handlePop` found\n * an empty stack and returned, and nothing visible happened — Back had to be pressed\n * twice to leave the page.\n *\n * So an abandoned entry is recorded as `dead` instead, and whichever overlay DOES\n * get to unwind pays the debt with a single `history.go(-n)`. `href` is what keeps\n * that safe: only dead entries pushed at the address we are still sitting at are\n * unwound, so a router navigation that landed between two sentinels is never\n * reversed on the user's behalf.\n */\ninterface PushedEntry {\n id: string;\n href: string;\n /** Its overlay is gone but the entry is not — someone above owes this pop. */\n dead: boolean;\n}\nconst pushed: PushedEntry[] = [];\n\n/** Entries a real Back press already consumed. Their overlay's cleanup still has to\n * run, and it must not mistake \"my entry is not current\" for \"my entry is buried\"\n * — it has no entry left at all. */\nconst consumed = new Set<string>();\n\n/** Pops WE caused (a cleanup unwinding its own entries) — their `popstate` is\n * bookkeeping, not a user gesture, and must not close a second overlay. A\n * `history.go(-n)` traversal fires exactly ONE popstate however far it travels. */\nlet pendingProgrammatic = 0;\nlet listening = false;\nlet nextId = 0;\n\n/**\n * The unwind a cleanup has asked for and not yet issued.\n *\n * **`history.go()` is asynchronous and everything above is written\n * synchronously**, which is the seam the whole of this block exists to close.\n * The traversal lands a task later, so an overlay that opens in the *same* tick\n * as one that closed used to push its sentinel on top of the entry that was\n * about to be popped — and the pop then took the NEW entry. The overlay left on\n * screen had no entry of its own (so Back no longer closed it) and the old one\n * stayed behind as a husk between the user and their page.\n *\n * That is not an exotic race. **React StrictMode remounts every effect** — mount,\n * clean up, mount again, all in one commit — so in development *every* dialog\n * anyone opened left a husk behind, the position drifted one entry further from\n * the page on each open, and the Back press that was meant to leave the page was\n * spent on something invisible instead (steering-design feedback #49).\n *\n * So the traversal is deferred by a task, and an overlay mounting inside that\n * window **adopts** the entry instead of pushing a second one (see the effect).\n * Adoption is `replaceState` on an entry that is already there: no traversal, no\n * window in which the count can be wrong, and the StrictMode remount costs\n * exactly nothing.\n */\ninterface Unwind {\n timer: ReturnType<typeof setTimeout>;\n /** The sentinel on the entry to leave — the top of `entries`. */\n id: string;\n href: string;\n /** That entry plus the abandoned ones directly beneath it, oldest first. */\n entries: PushedEntry[];\n}\nlet unwind: Unwind | null = null;\n\n/** Issue the deferred traversal, if the ground has not moved under it.\n *\n * A real Back press or a router navigation during the deferred task leaves us\n * somewhere that is not the entry we meant to leave, and going back from there\n * would take something that is not ours. The entries are then simply forgotten\n * rather than handed back as `dead`: a forgotten entry costs one dead Back\n * press, while an over-counted one costs the user a navigation they did make. */\nfunction runUnwind(): void {\n const job = unwind;\n unwind = null;\n if (!job) return;\n if (currentSentinel() !== job.id || currentHref() !== job.href) return;\n pendingProgrammatic += 1;\n window.history.go(-job.entries.length);\n}\n\nfunction currentHref(): string {\n return typeof window === \"undefined\" ? \"\" : window.location.href;\n}\n\nfunction forgetPushed(id: string): void {\n const at = pushed.findIndex((p) => p.id === id);\n if (at >= 0) pushed.splice(at, 1);\n}\n\n/**\n * The sentinel this module believes is on the entry we are standing on.\n *\n * `history.state` is where the tag physically sits, because it is the only\n * per-entry storage a browser has — but it is not this module's storage, so\n * since the 2026-09-22 audit it is no longer read as the last word.\n * The ROUTER owns that object and writes it WHOLE: every\n * `setSearchParams(…, { replace: true })` stamps `{usr, key, idx}` over\n * whatever was there. A `DataTable` with `urlSync` does exactly that, on mount\n * and on every filter change — so an overlay opened from such a table had its\n * tag wiped off the entry it was standing on, and then:\n *\n * * its cleanup read \"not my entry\" and abandoned a live entry as a husk;\n * * the push path below could not find itself in `pushed`, so it discarded\n * every record it had (`splice(0)`) and nothing was ever unwound again;\n * * the StrictMode adoption never matched, so in development every dialog\n * pushed a second entry and the deferred `go(-1)` ate the live one — which\n * is Back no longer closing the overlay at all.\n *\n * So `standing` answers when the tag is gone: a MISSING tag on an entry we have\n * not left is a wipe to repair rather than a verdict. The tag is still read\n * FIRST wherever it is there, because it is the one thing that can see a FOREIGN\n * same-URL entry stacked above ours (feedback #424/#426, below) — module-level\n * bookkeeping cannot, since nobody tells us about someone else's `pushState`.\n */\nlet standing: string | null = null;\n\n/** The tag actually on the current entry, with no repair and no belief. */\nfunction taggedSentinel(): string | null {\n const state = typeof window !== \"undefined\" ? window.history.state : null;\n if (!state || typeof state !== \"object\") return null;\n const id = (state as Record<string, unknown>)[SENTINEL_KEY];\n return typeof id === \"string\" ? id : null;\n}\n\n/** Put our tag back on the current entry, keeping whatever the router has since\n * written there — we are a passenger in that object, not its owner. */\nfunction retag(id: string): void {\n const prev = window.history.state;\n window.history.replaceState(\n { ...(prev && typeof prev === \"object\" ? prev : {}), [SENTINEL_KEY]: id },\n \"\",\n );\n}\n\nfunction currentSentinel(): string | null {\n const tag = taggedSentinel();\n if (tag !== null) {\n standing = tag;\n return tag;\n }\n if (standing === null || typeof window === \"undefined\") return null;\n // Only ever re-stamp an entry we still hold a record for. Without that guard a\n // belief left over from an overlay that is long gone — a tab that has since\n // navigated, a test file that ran another case — would be stamped onto a\n // stranger's entry, and the next cleanup would traverse off it.\n if (!pushed.some((p) => p.id === standing)) {\n standing = null;\n return null;\n }\n retag(standing);\n return standing;\n}\n\nfunction handlePop() {\n // A pop MOVES us, so the belief is stale by definition: re-read it from the\n // entry we have landed on before anything below consults it, or the repair in\n // `currentSentinel` would stamp the entry we just left onto the one we are on.\n standing = taggedSentinel();\n if (pendingProgrammatic > 0) {\n pendingProgrammatic -= 1;\n return;\n }\n // Nothing open → an ordinary navigation, which is none of our business.\n const top = stack[stack.length - 1];\n if (!top) return;\n // We landed back ON the top overlay's own entry, so that entry was NOT what the\n // pop consumed — something stacked ABOVE it was (Keksdose feedback #424/#426).\n // The app layers a second same-URL sentinel of its own on top of ours for the\n // unsaved-edit trap (`use-soft-leave-guard.ts`), and it pops that sentinel every\n // time the form goes back to clean — twice-toggling one field was enough to\n // close the whole dialog. A pop is only ours when the entry we came to rest on\n // is no longer ours: either the layer below (a different overlay's marker) or\n // the plain page entry (none at all).\n if (currentSentinel() === top.id) return;\n stack.pop();\n // The browser consumed this entry, so the overlay's own cleanup has nothing left\n // to unwind and nothing to record as owed.\n consumed.add(top.id);\n forgetPushed(top.id);\n top.close();\n}\n\nfunction ensureListening() {\n if (listening || typeof window === \"undefined\") return;\n window.addEventListener(\"popstate\", handlePop);\n listening = true;\n}\n\n/**\n * Make the platform Back gesture dismiss an overlay.\n *\n * @param open whether the overlay is currently showing. Components that mount\n * only while open (the common case) pass `true`.\n * @param onClose the overlay's own close handler — the same one Escape calls.\n */\nexport function useOverlayHistory(open: boolean, onClose: () => void): void {\n // The listener fires outside React's render, so it reads the latest handler\n // through a ref rather than through the closure the effect captured.\n const closeRef = useRef(onClose);\n useEffect(() => {\n closeRef.current = onClose;\n });\n\n useEffect(() => {\n if (!open || typeof window === \"undefined\") return;\n ensureListening();\n const id = `overlay-${(nextId += 1)}`;\n const entry: OverlayEntry = { id, close: () => closeRef.current() };\n stack.push(entry);\n const prev = window.history.state;\n const marked = { ...(prev && typeof prev === \"object\" ? prev : {}), [SENTINEL_KEY]: id };\n // An entry that is on its way out and that we are still standing on is ours\n // to take over: same URL, same slot, only the marker changes. Pushing a\n // second one on top of it is what used to leave a husk on every StrictMode\n // remount — see {@link Unwind}.\n const adopting = unwind && unwind.id === currentSentinel() && unwind.href === currentHref();\n if (adopting && unwind) {\n clearTimeout(unwind.timer);\n const inherited = unwind.entries;\n unwind = null;\n // Preserve react-router's own `{usr,key,idx}` — we only swap the marker,\n // and the entry we are swapping it on is the one the router is already on.\n window.history.replaceState(marked, \"\");\n // The abandoned ones beneath come back owed; ours takes the top slot.\n for (const owed of inherited.slice(0, -1)) pushed.push({ ...owed, dead: true });\n pushed.push({ id, href: currentHref(), dead: false });\n standing = id;\n } else {\n // A push DESTROYS every entry ahead of the one we are on, so anything we\n // still had recorded above our own position is gone. Kept, those would be\n // counted into a later `history.go(-n)` and the overshoot would come out\n // of a navigation the user actually made. Where we cannot tell which of\n // ours are beneath us — we are standing on a plain page entry, or on a\n // husk nobody tracks — none of them can be proven to be, so none survive.\n const under = pushed.findIndex((p) => p.id === currentSentinel());\n pushed.splice(under + 1);\n // Preserve react-router's own `{usr,key,idx}` — we only tack a marker on, and\n // the URL is unchanged, so the router treats popping this as a no-op re-render.\n window.history.pushState(marked, \"\");\n pushed.push({ id, href: currentHref(), dead: false });\n standing = id;\n }\n return () => {\n const at = stack.indexOf(entry);\n if (at >= 0) stack.splice(at, 1);\n\n // A Back press already took this entry; there is nothing to unwind or to owe.\n if (consumed.delete(id)) return;\n\n const mine = pushed.findIndex((p) => p.id === id);\n // Only unwind an entry that is still THE current one. If a router push landed\n // on top of it, going back would undo the user's navigation instead — and if\n // a SIBLING overlay's sentinel landed on top, that overlay is about to unwind\n // and is the one that can take ours with it.\n if (currentSentinel() !== id) {\n if (mine >= 0) pushed[mine].dead = true;\n return;\n }\n\n // Ours plus every abandoned entry lying directly beneath it at this address.\n // One traversal, so one popstate, so one programmatic pop to absorb.\n const here = currentHref();\n let steps = 1;\n while (steps <= mine && pushed[mine - steps].dead && pushed[mine - steps].href === here) {\n steps += 1;\n }\n const leaving = pushed.splice(mine - (steps - 1), steps);\n // Scheduled, not issued: an overlay opening in this same tick takes the\n // entry over instead, which is what makes the hook survive a StrictMode\n // remount. An unwind already waiting is dropped rather than merged — it\n // was the entry ABOVE ours and something has since moved off it, so its\n // count can no longer be proven, and an over-counted traversal costs a\n // navigation the user made while an under-counted one costs one dead Back.\n if (unwind) clearTimeout(unwind.timer);\n unwind = { timer: setTimeout(runUnwind, 0), id, href: here, entries: leaving };\n };\n }, [open]);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/use-row-swipe.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { PointerEvent } from \"react\";\n\n// ---------- Mobile swipe gesture ----------\n//\n// Multi-stage half-swipe affordance (feedback #103, extended #9): drag a row\n// horizontally to commit an action; how FAR you drag picks which action, so a\n// single gesture can walk a chain of increasingly involved outcomes (e.g. on a\n// cleared row, a short left-swipe un-clears it, further un-accepts it, further\n// still deletes it). We track via Pointer Events, lock the gesture to the X axis\n// only after the user has moved more horizontally than vertically, and clamp the\n// drag to `maxDrag` so the row can't be pulled across the whole viewport.\n//\n// Each side is an ordered list of stages sorted by ascending `threshold` (px of\n// drag magnitude). On release the LAST stage whose threshold the drag passed is\n// committed; while dragging, `armedRightIndex`/`armedLeftIndex` report which\n// stage is currently armed so the reveal panel can preview its label/colour.\n\nexport interface SwipeStage {\n /** Drag magnitude (px) at which this stage arms. Stages must be sorted ascending. */\n threshold: number;\n onCommit: () => void;\n}\n\n/** Below this much horizontal travel a gesture is treated as a clumsy click, not\n * a swipe: the row still follows the pointer, but the click that follows is NOT\n * swallowed. Without it, `swallowClick` was set the instant the axis lock\n * resolved to \"x\" — 8px — so a slightly-draggy click on a control INSIDE a\n * swipeable row silently did nothing, which is a poor trade on rows whose\n * content is itself interactive (Keksdose feedback #144: budget rows edit their\n * assigned amount and their name by clicking them). */\nconst CLICK_SWALLOW_PX = 24;\n\n/** How far a drag may travel toward a side that has no actions. Zero — the old\n * behaviour — makes the row completely inert in that direction, which reads as\n * \"this row is not swipeable at all\" rather than \"there is nothing on this\n * side\"; that is exactly the report behind Keksdose feedback #144 (\"tried, but\n * there is nothing swipeable\"). A short, resisted travel that snaps back is the\n * conventional answer and the one that answers the question the drag asked. */\nconst EMPTY_SIDE_RUBBER_PX = 28;\n\n/** Resistance applied past a bound: the row keeps moving, at a fraction of the\n * pointer, so the gesture feels answered without ever looking committed. */\nfunction rubberBand(overshoot: number, limit: number): number {\n const eased = limit * (1 - Math.exp(-Math.abs(overshoot) / limit));\n return Math.sign(overshoot) * eased;\n}\n\nexport interface RowSwipeOptions {\n enabled: boolean;\n // Ordered stages per direction (ascending threshold). A longer drag commits a\n // later — usually more involved — stage. An empty/undefined side means that\n // direction rubber-bands a little and snaps back, committing nothing.\n leftStages?: SwipeStage[];\n rightStages?: SwipeStage[];\n maxDrag?: number;\n}\n\nexport interface RowSwipeReturn {\n dx: number;\n dragging: boolean;\n /** Highest armed stage index for the current drag (-1 when none armed). */\n armedRightIndex: number;\n armedLeftIndex: number;\n handlers: {\n onPointerDown: (e: PointerEvent) => void;\n onPointerMove: (e: PointerEvent) => void;\n onPointerUp: (e: PointerEvent) => void;\n onPointerCancel: (e: PointerEvent) => void;\n };\n consumeClick: () => boolean;\n}\n\n/** The highest index whose threshold the magnitude has reached (-1 if none).\n * Stages are ascending, so we can stop at the first one not yet reached. */\nfunction lastArmed(stages: SwipeStage[] | undefined, magnitude: number): number {\n if (!stages) return -1;\n let idx = -1;\n for (let i = 0; i < stages.length; i++) {\n if (magnitude >= stages[i].threshold) idx = i;\n else break;\n }\n return idx;\n}\n\nexport function useRowSwipe({\n enabled,\n leftStages,\n rightStages,\n maxDrag = 252,\n}: RowSwipeOptions): RowSwipeReturn {\n const [dx, setDx] = useState(0);\n const [dragging, setDragging] = useState(false);\n const startX = useRef(0);\n const startY = useRef(0);\n const axis = useRef<\"none\" | \"x\" | \"y\">(\"none\");\n const pointerId = useRef<number | null>(null);\n const dxRef = useRef(0);\n // True after the user has dragged horizontally far enough that the following\n // synthetic click should be swallowed (so a swipe doesn't also expand the row).\n const swallowClick = useRef(false);\n\n // Keep the latest stage arrays in refs so the memoised pointer handlers always\n // commit against the current plan rather than a stale closure — the arrays and\n // their onCommit closures are rebuilt on every render. Written in an effect (not\n // during render) so it satisfies react-hooks/refs; pointer events fire after the\n // commit, so the refs are always current by the time a gesture reads them.\n const leftRef = useRef(leftStages);\n const rightRef = useRef(rightStages);\n useEffect(() => {\n leftRef.current = leftStages;\n rightRef.current = rightStages;\n });\n\n const reset = useCallback(() => {\n setDx(0);\n dxRef.current = 0;\n setDragging(false);\n startX.current = 0;\n startY.current = 0;\n axis.current = \"none\";\n pointerId.current = null;\n }, []);\n\n const onPointerDown = useCallback(\n (e: PointerEvent) => {\n if (!enabled) return;\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n pointerId.current = e.pointerId;\n startX.current = e.clientX;\n startY.current = e.clientY;\n axis.current = \"none\";\n swallowClick.current = false;\n setDragging(true);\n },\n [enabled],\n );\n\n const onPointerMove = useCallback(\n (e: PointerEvent) => {\n if (pointerId.current !== e.pointerId) return;\n const rawDx = e.clientX - startX.current;\n const rawDy = e.clientY - startY.current;\n if (axis.current === \"none\") {\n if (Math.abs(rawDx) < 8 && Math.abs(rawDy) < 8) return;\n axis.current = Math.abs(rawDx) > Math.abs(rawDy) ? \"x\" : \"y\";\n if (axis.current === \"x\") {\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n } else {\n // Vertical scroll wins — bail out of the gesture entirely.\n reset();\n return;\n }\n }\n if (axis.current !== \"x\") return;\n if (Math.abs(rawDx) >= CLICK_SWALLOW_PX) swallowClick.current = true;\n // Toward a side that HAS actions the row tracks the pointer up to maxDrag.\n // Toward an empty one it rubber-bands a little and snaps back on release —\n // inert would be indistinguishable from \"not swipeable\".\n const hasLeft = (leftRef.current?.length ?? 0) > 0;\n const hasRight = (rightRef.current?.length ?? 0) > 0;\n let next: number;\n if (rawDx > 0) {\n next = hasRight ? Math.min(maxDrag, rawDx) : rubberBand(rawDx, EMPTY_SIDE_RUBBER_PX);\n } else if (rawDx < 0) {\n next = hasLeft ? Math.max(-maxDrag, rawDx) : rubberBand(rawDx, EMPTY_SIDE_RUBBER_PX);\n } else {\n next = 0;\n }\n dxRef.current = next;\n setDx(next);\n },\n [maxDrag, reset],\n );\n\n const onPointerUp = useCallback(\n (e: PointerEvent) => {\n if (pointerId.current !== e.pointerId) return;\n const finalDx = dxRef.current;\n const wasDragX = axis.current === \"x\";\n reset();\n if (!wasDragX) return;\n if (finalDx > 0) {\n const i = lastArmed(rightRef.current, finalDx);\n if (i >= 0) rightRef.current![i].onCommit();\n } else if (finalDx < 0) {\n const i = lastArmed(leftRef.current, -finalDx);\n if (i >= 0) leftRef.current![i].onCommit();\n }\n },\n [reset],\n );\n\n const onPointerCancel = useCallback(() => reset(), [reset]);\n\n const consumeClick = useCallback(() => {\n if (swallowClick.current) {\n swallowClick.current = false;\n return true;\n }\n return false;\n }, []);\n\n return {\n dx,\n dragging,\n armedRightIndex: lastArmed(rightStages, dx > 0 ? dx : 0),\n armedLeftIndex: lastArmed(leftStages, dx < 0 ? -dx : 0),\n handlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel },\n consumeClick,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/use-row-swipe.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { PointerEvent } from \"react\";\n\n// ---------- Mobile swipe gesture ----------\n//\n// Multi-stage half-swipe affordance (feedback #103, extended #9): drag a row\n// horizontally to commit an action; how FAR you drag picks which action, so a\n// single gesture can walk a chain of increasingly involved outcomes (e.g. on a\n// cleared row, a short left-swipe un-clears it, further un-accepts it, further\n// still deletes it). We track via Pointer Events, lock the gesture to the X axis\n// only after the user has moved more horizontally than vertically, and clamp the\n// drag to `maxDrag` so the row can't be pulled across the whole viewport.\n//\n// Each side is an ordered list of stages sorted by ascending `threshold` (px of\n// drag magnitude). On release the LAST stage whose threshold the drag passed is\n// committed; while dragging, `armedRightIndex`/`armedLeftIndex` report which\n// stage is currently armed so the reveal panel can preview its label/colour.\n\nexport interface SwipeStage {\n /** Drag magnitude (px) at which this stage arms. Stages must be sorted ascending. */\n threshold: number;\n onCommit: () => void;\n}\n\n/** Below this much horizontal travel a gesture is treated as a clumsy click, not\n * a swipe: the row still follows the pointer, but the click that follows is NOT\n * swallowed. Without it, `swallowClick` was set the instant the axis lock\n * resolved to \"x\" — 8px — so a slightly-draggy click on a control INSIDE a\n * swipeable row silently did nothing, which is a poor trade on rows whose\n * content is itself interactive (Keksdose feedback #144: budget rows edit their\n * assigned amount and their name by clicking them). */\nconst CLICK_SWALLOW_PX = 24;\n\n/** How far a drag may travel toward a side that has no actions. Zero — the old\n * behaviour — makes the row completely inert in that direction, which reads as\n * \"this row is not swipeable at all\" rather than \"there is nothing on this\n * side\"; that is exactly the report behind Keksdose feedback #144 (\"tried, but\n * there is nothing swipeable\"). A short, resisted travel that snaps back is the\n * conventional answer and the one that answers the question the drag asked. */\nconst EMPTY_SIDE_RUBBER_PX = 28;\n\n/** Resistance applied past a bound: the row keeps moving, at a fraction of the\n * pointer, so the gesture feels answered without ever looking committed. */\nfunction rubberBand(overshoot: number, limit: number): number {\n const eased = limit * (1 - Math.exp(-Math.abs(overshoot) / limit));\n return Math.sign(overshoot) * eased;\n}\n\nexport interface RowSwipeOptions {\n enabled: boolean;\n // Ordered stages per direction (ascending threshold). A longer drag commits a\n // later — usually more involved — stage. An empty/undefined side means that\n // direction rubber-bands a little and snaps back, committing nothing.\n leftStages?: SwipeStage[];\n rightStages?: SwipeStage[];\n maxDrag?: number;\n}\n\nexport interface RowSwipeReturn {\n dx: number;\n dragging: boolean;\n /** Highest armed stage index for the current drag (-1 when none armed). */\n armedRightIndex: number;\n armedLeftIndex: number;\n handlers: {\n onPointerDown: (e: PointerEvent) => void;\n onPointerMove: (e: PointerEvent) => void;\n onPointerUp: (e: PointerEvent) => void;\n onPointerCancel: (e: PointerEvent) => void;\n };\n consumeClick: () => boolean;\n}\n\n/** The highest index whose threshold the magnitude has reached (-1 if none).\n * Stages are ascending, so we can stop at the first one not yet reached. */\nfunction lastArmed(stages: SwipeStage[] | undefined, magnitude: number): number {\n if (!stages) return -1;\n let idx = -1;\n for (let i = 0; i < stages.length; i++) {\n if (magnitude >= stages[i].threshold) idx = i;\n else break;\n }\n return idx;\n}\n\nexport function useRowSwipe({\n enabled,\n leftStages,\n rightStages,\n maxDrag = 252,\n}: RowSwipeOptions): RowSwipeReturn {\n const [dx, setDx] = useState(0);\n const [dragging, setDragging] = useState(false);\n const startX = useRef(0);\n const startY = useRef(0);\n const axis = useRef<\"none\" | \"x\" | \"y\">(\"none\");\n const pointerId = useRef<number | null>(null);\n const dxRef = useRef(0);\n // True after the user has dragged horizontally far enough that the following\n // synthetic click should be swallowed (so a swipe doesn't also expand the row).\n const swallowClick = useRef(false);\n\n // Keep the latest stage arrays in refs so the memoised pointer handlers always\n // commit against the current plan rather than a stale closure — the arrays and\n // their onCommit closures are rebuilt on every render. Written in an effect (not\n // during render) so it satisfies react-hooks/refs; pointer events fire after the\n // commit, so the refs are always current by the time a gesture reads them.\n const leftRef = useRef(leftStages);\n const rightRef = useRef(rightStages);\n useEffect(() => {\n leftRef.current = leftStages;\n rightRef.current = rightStages;\n });\n\n const reset = useCallback(() => {\n setDx(0);\n dxRef.current = 0;\n setDragging(false);\n startX.current = 0;\n startY.current = 0;\n axis.current = \"none\";\n pointerId.current = null;\n }, []);\n\n const onPointerDown = useCallback(\n (e: PointerEvent) => {\n if (!enabled) return;\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n pointerId.current = e.pointerId;\n startX.current = e.clientX;\n startY.current = e.clientY;\n axis.current = \"none\";\n swallowClick.current = false;\n setDragging(true);\n },\n [enabled],\n );\n\n const onPointerMove = useCallback(\n (e: PointerEvent) => {\n if (pointerId.current !== e.pointerId) return;\n const rawDx = e.clientX - startX.current;\n const rawDy = e.clientY - startY.current;\n if (axis.current === \"none\") {\n if (Math.abs(rawDx) < 8 && Math.abs(rawDy) < 8) return;\n axis.current = Math.abs(rawDx) > Math.abs(rawDy) ? \"x\" : \"y\";\n if (axis.current === \"x\") {\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n } else {\n // Vertical scroll wins — bail out of the gesture entirely.\n reset();\n return;\n }\n }\n if (axis.current !== \"x\") return;\n if (Math.abs(rawDx) >= CLICK_SWALLOW_PX) swallowClick.current = true;\n // Toward a side that HAS actions the row tracks the pointer up to maxDrag.\n // Toward an empty one it rubber-bands a little and snaps back on release —\n // inert would be indistinguishable from \"not swipeable\".\n const hasLeft = (leftRef.current?.length ?? 0) > 0;\n const hasRight = (rightRef.current?.length ?? 0) > 0;\n let next: number;\n if (rawDx > 0) {\n next = hasRight ? Math.min(maxDrag, rawDx) : rubberBand(rawDx, EMPTY_SIDE_RUBBER_PX);\n } else if (rawDx < 0) {\n next = hasLeft ? Math.max(-maxDrag, rawDx) : rubberBand(rawDx, EMPTY_SIDE_RUBBER_PX);\n } else {\n next = 0;\n }\n dxRef.current = next;\n setDx(next);\n },\n [maxDrag, reset],\n );\n\n const onPointerUp = useCallback(\n (e: PointerEvent) => {\n if (pointerId.current !== e.pointerId) return;\n const finalDx = dxRef.current;\n const wasDragX = axis.current === \"x\";\n reset();\n if (!wasDragX) return;\n if (finalDx > 0) {\n const i = lastArmed(rightRef.current, finalDx);\n if (i >= 0) rightRef.current![i].onCommit();\n } else if (finalDx < 0) {\n const i = lastArmed(leftRef.current, -finalDx);\n if (i >= 0) leftRef.current![i].onCommit();\n }\n },\n [reset],\n );\n\n const onPointerCancel = useCallback(() => reset(), [reset]);\n\n const consumeClick = useCallback(() => {\n if (swallowClick.current) {\n swallowClick.current = false;\n return true;\n }\n return false;\n }, []);\n\n return {\n dx,\n dragging,\n armedRightIndex: lastArmed(rightStages, dx > 0 ? dx : 0),\n armedLeftIndex: lastArmed(leftStages, dx < 0 ? -dx : 0),\n handlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel },\n consumeClick,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UiKitLabels } from './kit-labels.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import '../components/data-table-labels.js';
|
|
4
|
+
import '../components/mini-calendar.js';
|
|
5
|
+
import '../components/popover.js';
|
|
6
|
+
import '../components/chip.js';
|
|
7
|
+
import 'lucide-react';
|
|
8
|
+
import '../components/field-sync.js';
|
|
9
|
+
import '../components/ui.js';
|
|
10
|
+
import '../wizard/types.js';
|
|
11
|
+
import '../tour/tour.js';
|
|
12
|
+
import '../search/command-palette.js';
|
|
13
|
+
import '../components/month-picker.js';
|
|
14
|
+
import '../components/page-contents.js';
|
|
15
|
+
import '../components/series-chart-labels.js';
|
|
16
|
+
import '../components/sparkline.js';
|
|
17
|
+
import '../components/stat-tile.js';
|
|
18
|
+
import '../components/signature-pad.js';
|
|
19
|
+
import '../components/password-strength.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The whole English tree in one object: what a translation is written against, and
|
|
23
|
+
* the `reference` for {@link missingKitLabels}.
|
|
24
|
+
*
|
|
25
|
+
* A separate module from the provider on purpose. Components import the provider's
|
|
26
|
+
* hooks; this file imports the components' defaults. Kept together they would be an
|
|
27
|
+
* import cycle through every component in the kit.
|
|
28
|
+
*/
|
|
29
|
+
declare const DEFAULT_UI_KIT_LABELS: UiKitLabels;
|
|
30
|
+
|
|
31
|
+
export { DEFAULT_UI_KIT_LABELS };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { DEFAULT_DATA_TABLE_LABELS } from "../components/data-table-labels.js";
|
|
3
|
+
import { DEFAULT_MINI_CALENDAR_LABELS } from "../components/mini-calendar.js";
|
|
4
|
+
import { DEFAULT_MONTH_PICKER_LABELS } from "../components/month-picker.js";
|
|
5
|
+
import { DEFAULT_PAGE_CONTENTS_LABELS } from "../components/page-contents.js";
|
|
6
|
+
import { DEFAULT_POPOVER_LABELS } from "../components/popover.js";
|
|
7
|
+
import { DEFAULT_CHIP_INPUT_LABELS } from "../components/chip.js";
|
|
8
|
+
import { DEFAULT_FIELD_SYNC_LABELS } from "../components/field-sync.js";
|
|
9
|
+
import { DEFAULT_PASSWORD_REVEAL_LABELS } from "../components/ui.js";
|
|
10
|
+
import { DEFAULT_WIZARD_LABELS } from "../wizard/types.js";
|
|
11
|
+
import { DEFAULT_TOUR_LABELS } from "../tour/tour.js";
|
|
12
|
+
import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette.js";
|
|
13
|
+
import { DEFAULT_SERIES_CHART_LABELS } from "../components/series-chart-labels.js";
|
|
14
|
+
import { DEFAULT_SPARKLINE_LABELS } from "../components/sparkline.js";
|
|
15
|
+
import { DEFAULT_STAT_TILE_LABELS } from "../components/stat-tile.js";
|
|
16
|
+
import { DEFAULT_SIGNATURE_PAD_LABELS } from "../components/signature-pad.js";
|
|
17
|
+
import { DEFAULT_PASSWORD_STRENGTH_LABELS } from "../components/password-strength.js";
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_APP_SHELL_LABELS,
|
|
20
|
+
DEFAULT_CALCULATOR_LABELS,
|
|
21
|
+
DEFAULT_COMBOBOX_LABELS,
|
|
22
|
+
DEFAULT_COMMON_LABELS,
|
|
23
|
+
DEFAULT_CURRENCY_LABELS,
|
|
24
|
+
DEFAULT_DATE_PICKER_LABELS,
|
|
25
|
+
DEFAULT_FILE_LABELS,
|
|
26
|
+
DEFAULT_MULTI_SELECT_LABELS,
|
|
27
|
+
DEFAULT_PICKER_SHEET_LABELS,
|
|
28
|
+
DEFAULT_SWIPEABLE_ROW_LABELS,
|
|
29
|
+
DEFAULT_TOP_BAR_LABELS
|
|
30
|
+
} from "./kit-labels.js";
|
|
31
|
+
const DEFAULT_UI_KIT_LABELS = {
|
|
32
|
+
common: DEFAULT_COMMON_LABELS,
|
|
33
|
+
dataTable: DEFAULT_DATA_TABLE_LABELS,
|
|
34
|
+
miniCalendar: DEFAULT_MINI_CALENDAR_LABELS,
|
|
35
|
+
datePicker: DEFAULT_DATE_PICKER_LABELS,
|
|
36
|
+
monthPicker: DEFAULT_MONTH_PICKER_LABELS,
|
|
37
|
+
popover: DEFAULT_POPOVER_LABELS,
|
|
38
|
+
combobox: DEFAULT_COMBOBOX_LABELS,
|
|
39
|
+
multiSelect: DEFAULT_MULTI_SELECT_LABELS,
|
|
40
|
+
calculator: DEFAULT_CALCULATOR_LABELS,
|
|
41
|
+
currency: DEFAULT_CURRENCY_LABELS,
|
|
42
|
+
chipInput: DEFAULT_CHIP_INPUT_LABELS,
|
|
43
|
+
fieldSync: DEFAULT_FIELD_SYNC_LABELS,
|
|
44
|
+
passwordReveal: DEFAULT_PASSWORD_REVEAL_LABELS,
|
|
45
|
+
appShell: DEFAULT_APP_SHELL_LABELS,
|
|
46
|
+
pageContents: DEFAULT_PAGE_CONTENTS_LABELS,
|
|
47
|
+
topBar: DEFAULT_TOP_BAR_LABELS,
|
|
48
|
+
pickerSheet: DEFAULT_PICKER_SHEET_LABELS,
|
|
49
|
+
swipeableRow: DEFAULT_SWIPEABLE_ROW_LABELS,
|
|
50
|
+
file: DEFAULT_FILE_LABELS,
|
|
51
|
+
wizard: DEFAULT_WIZARD_LABELS,
|
|
52
|
+
tour: DEFAULT_TOUR_LABELS,
|
|
53
|
+
commandPalette: DEFAULT_COMMAND_PALETTE_LABELS,
|
|
54
|
+
seriesChart: DEFAULT_SERIES_CHART_LABELS,
|
|
55
|
+
sparkline: DEFAULT_SPARKLINE_LABELS,
|
|
56
|
+
statTile: DEFAULT_STAT_TILE_LABELS,
|
|
57
|
+
signaturePad: DEFAULT_SIGNATURE_PAD_LABELS,
|
|
58
|
+
passwordStrength: DEFAULT_PASSWORD_STRENGTH_LABELS
|
|
59
|
+
};
|
|
60
|
+
export {
|
|
61
|
+
DEFAULT_UI_KIT_LABELS
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/defaults.ts"],"sourcesContent":["import { DEFAULT_DATA_TABLE_LABELS } from \"../components/data-table-labels\";\nimport { DEFAULT_MINI_CALENDAR_LABELS } from \"../components/mini-calendar\";\nimport { DEFAULT_MONTH_PICKER_LABELS } from \"../components/month-picker\";\nimport { DEFAULT_PAGE_CONTENTS_LABELS } from \"../components/page-contents\";\nimport { DEFAULT_POPOVER_LABELS } from \"../components/popover\";\nimport { DEFAULT_CHIP_INPUT_LABELS } from \"../components/chip\";\nimport { DEFAULT_FIELD_SYNC_LABELS } from \"../components/field-sync\";\nimport { DEFAULT_PASSWORD_REVEAL_LABELS } from \"../components/ui\";\nimport { DEFAULT_WIZARD_LABELS } from \"../wizard/types\";\nimport { DEFAULT_TOUR_LABELS } from \"../tour/tour\";\nimport { DEFAULT_COMMAND_PALETTE_LABELS } from \"../search/command-palette\";\nimport { DEFAULT_SERIES_CHART_LABELS } from \"../components/series-chart-labels\";\nimport { DEFAULT_SPARKLINE_LABELS } from \"../components/sparkline\";\nimport { DEFAULT_STAT_TILE_LABELS } from \"../components/stat-tile\";\nimport { DEFAULT_SIGNATURE_PAD_LABELS } from \"../components/signature-pad\";\nimport { DEFAULT_PASSWORD_STRENGTH_LABELS } from \"../components/password-strength\";\nimport {\n DEFAULT_APP_SHELL_LABELS,\n DEFAULT_CALCULATOR_LABELS,\n DEFAULT_COMBOBOX_LABELS,\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n DEFAULT_DATE_PICKER_LABELS,\n DEFAULT_FILE_LABELS,\n DEFAULT_MULTI_SELECT_LABELS,\n DEFAULT_PICKER_SHEET_LABELS,\n DEFAULT_SWIPEABLE_ROW_LABELS,\n DEFAULT_TOP_BAR_LABELS,\n} from \"./kit-labels\";\nimport type { UiKitLabels } from \"./kit-labels\";\n\n/**\n * The whole English tree in one object: what a translation is written against, and\n * the `reference` for {@link missingKitLabels}.\n *\n * A separate module from the provider on purpose. Components import the provider's\n * hooks; this file imports the components' defaults. Kept together they would be an\n * import cycle through every component in the kit.\n */\nexport const DEFAULT_UI_KIT_LABELS: UiKitLabels = {\n common: DEFAULT_COMMON_LABELS,\n dataTable: DEFAULT_DATA_TABLE_LABELS,\n miniCalendar: DEFAULT_MINI_CALENDAR_LABELS,\n datePicker: DEFAULT_DATE_PICKER_LABELS,\n monthPicker: DEFAULT_MONTH_PICKER_LABELS,\n popover: DEFAULT_POPOVER_LABELS,\n combobox: DEFAULT_COMBOBOX_LABELS,\n multiSelect: DEFAULT_MULTI_SELECT_LABELS,\n calculator: DEFAULT_CALCULATOR_LABELS,\n currency: DEFAULT_CURRENCY_LABELS,\n chipInput: DEFAULT_CHIP_INPUT_LABELS,\n fieldSync: DEFAULT_FIELD_SYNC_LABELS,\n passwordReveal: DEFAULT_PASSWORD_REVEAL_LABELS,\n appShell: DEFAULT_APP_SHELL_LABELS,\n pageContents: DEFAULT_PAGE_CONTENTS_LABELS,\n topBar: DEFAULT_TOP_BAR_LABELS,\n pickerSheet: DEFAULT_PICKER_SHEET_LABELS,\n swipeableRow: DEFAULT_SWIPEABLE_ROW_LABELS,\n file: DEFAULT_FILE_LABELS,\n wizard: DEFAULT_WIZARD_LABELS,\n tour: DEFAULT_TOUR_LABELS,\n commandPalette: DEFAULT_COMMAND_PALETTE_LABELS,\n seriesChart: DEFAULT_SERIES_CHART_LABELS,\n sparkline: DEFAULT_SPARKLINE_LABELS,\n statTile: DEFAULT_STAT_TILE_LABELS,\n signaturePad: DEFAULT_SIGNATURE_PAD_LABELS,\n passwordStrength: DEFAULT_PASSWORD_STRENGTH_LABELS,\n};\n"],"mappings":";AAAA,SAAS,iCAAiC;AAC1C,SAAS,oCAAoC;AAC7C,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAC1C,SAAS,iCAAiC;AAC1C,SAAS,sCAAsC;AAC/C,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,sCAAsC;AAC/C,SAAS,mCAAmC;AAC5C,SAAS,gCAAgC;AACzC,SAAS,gCAAgC;AACzC,SAAS,oCAAoC;AAC7C,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWA,MAAM,wBAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,WAAW;AAAA,EACX,UAAU;AAAA,EACV,cAAc;AAAA,EACd,kBAAkB;AACpB;","names":[]}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { DataTableLabels } from '../components/data-table-labels.js';
|
|
4
|
+
import { MiniCalendarLabels } from '../components/mini-calendar.js';
|
|
5
|
+
import { PopoverLabels } from '../components/popover.js';
|
|
6
|
+
import { ChipInputLabels } from '../components/chip.js';
|
|
7
|
+
import { FieldSyncLabels } from '../components/field-sync.js';
|
|
8
|
+
import { PasswordRevealLabels } from '../components/ui.js';
|
|
9
|
+
import { WizardLabels } from '../wizard/types.js';
|
|
10
|
+
import { TourLabels } from '../tour/tour.js';
|
|
11
|
+
import { CommandPaletteLabels } from '../search/command-palette.js';
|
|
12
|
+
import { MonthPickerLabels } from '../components/month-picker.js';
|
|
13
|
+
import { PageContentsLabels } from '../components/page-contents.js';
|
|
14
|
+
import { SeriesChartLabels } from '../components/series-chart-labels.js';
|
|
15
|
+
import { SparklineLabels } from '../components/sparkline.js';
|
|
16
|
+
import { StatTileLabels } from '../components/stat-tile.js';
|
|
17
|
+
import { SignaturePadLabels } from '../components/signature-pad.js';
|
|
18
|
+
import { PasswordStrengthLabels } from '../components/password-strength.js';
|
|
19
|
+
import 'lucide-react';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* EVERY string the kit renders, as one typed tree — and an optional provider that
|
|
23
|
+
* hands it to every component at once.
|
|
24
|
+
*
|
|
25
|
+
* WHY THIS EXISTS. The kit ships no catalogue and resolves no strings, and that stays
|
|
26
|
+
* true: the app supplies every word. What was wrong was HOW it had to supply them —
|
|
27
|
+
* one `labels` prop per component instance, under a different prop name per
|
|
28
|
+
* component (`labels`, `calendarLabels`, `passwordLabels`, `clearLabel`,
|
|
29
|
+
* `searchPlaceholder`, `ariaLabel` …). A translated app therefore rendered English
|
|
30
|
+
* wherever one call site forgot one prop, and nothing told it so: a German showcase
|
|
31
|
+
* still said "Rows per page", "Collapse sidebar" and "Popover" in seven languages.
|
|
32
|
+
*
|
|
33
|
+
* Now there is one key per string, addressed by a dot path (`dataTable.pageSize`,
|
|
34
|
+
* `datePicker.today`), and three sources in a fixed order of precedence:
|
|
35
|
+
*
|
|
36
|
+
* the component's own prop > <UiKitProvider labels> > English default
|
|
37
|
+
*
|
|
38
|
+
* A consumer writes its translation ONCE, as a `UiKitLabels` (or a partial of it),
|
|
39
|
+
* mounts `<UiKitProvider labels={…} locale={…}>` at the root, and every kit component
|
|
40
|
+
* below speaks that language — including the ones nested inside other kit components
|
|
41
|
+
* (the calendar inside the data table's date filter, the popover inside the date
|
|
42
|
+
* picker), which no prop at the outer call site could reach before.
|
|
43
|
+
*
|
|
44
|
+
* Messages that carry a value are FUNCTIONS of that value, never a template to fill:
|
|
45
|
+
* a number glued into an English sentence is untranslatable, because the grammar
|
|
46
|
+
* around it moves with it in most languages.
|
|
47
|
+
*/
|
|
48
|
+
/** The words several components share. A component's own namespace wins over these
|
|
49
|
+
* where both exist; these are what a new component reaches for first. */
|
|
50
|
+
interface CommonLabels {
|
|
51
|
+
close: string;
|
|
52
|
+
clear: string;
|
|
53
|
+
search: string;
|
|
54
|
+
done: string;
|
|
55
|
+
cancel: string;
|
|
56
|
+
save: string;
|
|
57
|
+
back: string;
|
|
58
|
+
next: string;
|
|
59
|
+
remove: string;
|
|
60
|
+
loading: string;
|
|
61
|
+
noResults: string;
|
|
62
|
+
/** "Name: value" — how a field's accessible name is composed with its value.
|
|
63
|
+
* A colon-and-space is not universal punctuation (French puts a space before
|
|
64
|
+
* the colon, Chinese uses a full-width one). */
|
|
65
|
+
fieldValue: (field: string, value: string) => string;
|
|
66
|
+
}
|
|
67
|
+
/** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
|
|
68
|
+
* namespace, `miniCalendar`. */
|
|
69
|
+
interface DatePickerLabels {
|
|
70
|
+
/** Accessible name of the popover panel the calendar opens in. */
|
|
71
|
+
panel: string;
|
|
72
|
+
/** The same, for `DateRangePicker`'s panel. */
|
|
73
|
+
rangePanel: string;
|
|
74
|
+
clear: string;
|
|
75
|
+
previousDay: string;
|
|
76
|
+
nextDay: string;
|
|
77
|
+
today: string;
|
|
78
|
+
}
|
|
79
|
+
/** The whole combobox family: `Combobox`, `EntityCombobox`,
|
|
80
|
+
* `MultiEntityCombobox`, `InlineEntityCombobox`. */
|
|
81
|
+
interface ComboboxLabels {
|
|
82
|
+
search: string;
|
|
83
|
+
noResults: string;
|
|
84
|
+
clear: string;
|
|
85
|
+
loading: string;
|
|
86
|
+
/** The "add this" row when free entry is allowed. */
|
|
87
|
+
create: (query: string) => string;
|
|
88
|
+
/** Trigger summary once more than one value is picked. */
|
|
89
|
+
selectedCount: (count: number) => string;
|
|
90
|
+
}
|
|
91
|
+
interface MultiSelectLabels {
|
|
92
|
+
search: string;
|
|
93
|
+
selectAll: string;
|
|
94
|
+
clear: string;
|
|
95
|
+
/** Trigger text when nothing is picked, which a multi-select reads as "all". */
|
|
96
|
+
all: string;
|
|
97
|
+
/** Trigger summary once some (but not all) values are picked. The English
|
|
98
|
+
* default is the bare count, which is what the trigger has always shown. */
|
|
99
|
+
selectedCount: (count: number) => string;
|
|
100
|
+
}
|
|
101
|
+
/** `CalculatorButton` (desktop popover keypad) and `NumberPadSheet` (phone sheet). */
|
|
102
|
+
interface CalculatorLabels {
|
|
103
|
+
/** The button that opens the calculator. */
|
|
104
|
+
open: string;
|
|
105
|
+
/** The popover / sheet itself. */
|
|
106
|
+
panel: string;
|
|
107
|
+
calculation: string;
|
|
108
|
+
backspace: string;
|
|
109
|
+
clear: string;
|
|
110
|
+
equals: string;
|
|
111
|
+
/** The primary key of the phone pad — visible text, not an aria-label. */
|
|
112
|
+
done: string;
|
|
113
|
+
/** Names of the operator keys and the decimal key. Their glyphs are not names a
|
|
114
|
+
* screen reader agrees on — "÷" is read as "division sign", "divided by" or
|
|
115
|
+
* nothing at all depending on the reader — so each says what it does. */
|
|
116
|
+
plus: string;
|
|
117
|
+
minus: string;
|
|
118
|
+
times: string;
|
|
119
|
+
divide: string;
|
|
120
|
+
decimal: string;
|
|
121
|
+
}
|
|
122
|
+
/** `CurrencySelect` and the currency half of `AmountInput`. */
|
|
123
|
+
interface CurrencyLabels {
|
|
124
|
+
currency: string;
|
|
125
|
+
search: string;
|
|
126
|
+
}
|
|
127
|
+
interface AppShellLabels {
|
|
128
|
+
collapse: string;
|
|
129
|
+
expand: string;
|
|
130
|
+
/** Accessible name of an inline group's disclosure button (`subNav="inline"`). */
|
|
131
|
+
toggleGroup: (groupLabel: string) => string;
|
|
132
|
+
}
|
|
133
|
+
interface TopBarLabels {
|
|
134
|
+
theme: string;
|
|
135
|
+
palette: string;
|
|
136
|
+
language: string;
|
|
137
|
+
switchRole: string;
|
|
138
|
+
/** The role switcher's tooltip, given the active role's name. */
|
|
139
|
+
role: (value: string) => string;
|
|
140
|
+
}
|
|
141
|
+
interface PickerSheetLabels {
|
|
142
|
+
close: string;
|
|
143
|
+
}
|
|
144
|
+
interface SwipeableRowLabels {
|
|
145
|
+
actions: string;
|
|
146
|
+
}
|
|
147
|
+
interface FileLabels {
|
|
148
|
+
/** A file size for display, given its size in BYTES. The default uses
|
|
149
|
+
* `Intl.NumberFormat`'s unit formatting in the provider's locale. */
|
|
150
|
+
size: (bytes: number) => string;
|
|
151
|
+
}
|
|
152
|
+
/** The complete tree. `UiKitProvider` takes any partial of it. */
|
|
153
|
+
interface UiKitLabels {
|
|
154
|
+
common: CommonLabels;
|
|
155
|
+
dataTable: DataTableLabels;
|
|
156
|
+
miniCalendar: MiniCalendarLabels;
|
|
157
|
+
datePicker: DatePickerLabels;
|
|
158
|
+
monthPicker: MonthPickerLabels;
|
|
159
|
+
popover: PopoverLabels;
|
|
160
|
+
combobox: ComboboxLabels;
|
|
161
|
+
multiSelect: MultiSelectLabels;
|
|
162
|
+
calculator: CalculatorLabels;
|
|
163
|
+
currency: CurrencyLabels;
|
|
164
|
+
chipInput: ChipInputLabels;
|
|
165
|
+
fieldSync: FieldSyncLabels;
|
|
166
|
+
passwordReveal: PasswordRevealLabels;
|
|
167
|
+
appShell: AppShellLabels;
|
|
168
|
+
pageContents: PageContentsLabels;
|
|
169
|
+
topBar: TopBarLabels;
|
|
170
|
+
pickerSheet: PickerSheetLabels;
|
|
171
|
+
swipeableRow: SwipeableRowLabels;
|
|
172
|
+
file: FileLabels;
|
|
173
|
+
wizard: WizardLabels;
|
|
174
|
+
tour: TourLabels;
|
|
175
|
+
commandPalette: CommandPaletteLabels;
|
|
176
|
+
seriesChart: SeriesChartLabels;
|
|
177
|
+
sparkline: SparklineLabels;
|
|
178
|
+
statTile: StatTileLabels;
|
|
179
|
+
signaturePad: SignaturePadLabels;
|
|
180
|
+
passwordStrength: PasswordStrengthLabels;
|
|
181
|
+
}
|
|
182
|
+
/** Any subset of the tree, one level deep — each namespace may be partial, and a
|
|
183
|
+
* namespace that holds a record (`dataTable.presets`) is merged key by key. */
|
|
184
|
+
type UiKitLabelOverrides = {
|
|
185
|
+
[K in keyof UiKitLabels]?: Partial<UiKitLabels[K]>;
|
|
186
|
+
};
|
|
187
|
+
declare const DEFAULT_COMMON_LABELS: CommonLabels;
|
|
188
|
+
declare const DEFAULT_DATE_PICKER_LABELS: DatePickerLabels;
|
|
189
|
+
declare const DEFAULT_COMBOBOX_LABELS: ComboboxLabels;
|
|
190
|
+
declare const DEFAULT_MULTI_SELECT_LABELS: MultiSelectLabels;
|
|
191
|
+
declare const DEFAULT_CALCULATOR_LABELS: CalculatorLabels;
|
|
192
|
+
declare const DEFAULT_CURRENCY_LABELS: CurrencyLabels;
|
|
193
|
+
declare const DEFAULT_APP_SHELL_LABELS: AppShellLabels;
|
|
194
|
+
declare const DEFAULT_TOP_BAR_LABELS: TopBarLabels;
|
|
195
|
+
declare const DEFAULT_PICKER_SHEET_LABELS: PickerSheetLabels;
|
|
196
|
+
declare const DEFAULT_SWIPEABLE_ROW_LABELS: SwipeableRowLabels;
|
|
197
|
+
/** Formats with the locale the provider was given (see {@link useKitFileLabels}).
|
|
198
|
+
* This static default has no locale to hand and falls back to the runtime's. */
|
|
199
|
+
declare const DEFAULT_FILE_LABELS: FileLabels;
|
|
200
|
+
/** "12 kB", "3.4 MB" — `Intl`'s unit formatting, so the digits, the decimal mark and
|
|
201
|
+
* the unit's spelling all follow `locale`. */
|
|
202
|
+
declare function formatFileSize(bytes: number, locale?: string): string;
|
|
203
|
+
interface UiKitProviderProps {
|
|
204
|
+
/** Any part of {@link UiKitLabels}. Missing keys fall back to English. */
|
|
205
|
+
labels?: UiKitLabelOverrides;
|
|
206
|
+
/**
|
|
207
|
+
* BCP 47 tag used by every kit component that formats something — dates, numbers,
|
|
208
|
+
* month and weekday names, file sizes — when it is not handed a `locale` prop of
|
|
209
|
+
* its own. Without a provider, and without a prop, those use the runtime default.
|
|
210
|
+
*/
|
|
211
|
+
locale?: string;
|
|
212
|
+
children: ReactNode;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Hand every kit component below this point its strings and its locale.
|
|
216
|
+
*
|
|
217
|
+
* Optional: a component outside any provider behaves exactly as it did before the
|
|
218
|
+
* provider existed. Nesting works as a merge — an inner provider overrides only what
|
|
219
|
+
* it names, so a page can re-label one table's `dataTable.table` without restating
|
|
220
|
+
* the language.
|
|
221
|
+
*/
|
|
222
|
+
declare function UiKitProvider({ labels, locale, children }: UiKitProviderProps): react.JSX.Element;
|
|
223
|
+
/** What the nearest provider says about one namespace — `undefined` outside one.
|
|
224
|
+
* For a component whose own resolver does more than a merge (the data table
|
|
225
|
+
* derives `columnsCount` from `columns`): feed `{ ...overrides, ...props }` to it. */
|
|
226
|
+
declare function useKitLabelOverrides<K extends keyof UiKitLabels>(ns: K): Partial<UiKitLabels[K]> | undefined;
|
|
227
|
+
/**
|
|
228
|
+
* One namespace, resolved: `defaults`, then the provider, then the component's own
|
|
229
|
+
* `prop`. Record-valued keys are merged key by key at each step.
|
|
230
|
+
*/
|
|
231
|
+
declare function useKitLabels<K extends keyof UiKitLabels>(ns: K, defaults: UiKitLabels[K], prop?: Partial<UiKitLabels[K]>): UiKitLabels[K];
|
|
232
|
+
/** The component's own `locale` prop, else the provider's, else `undefined` (which
|
|
233
|
+
* every `Intl` API reads as "the runtime's default"). */
|
|
234
|
+
declare function useKitLocale(prop?: string): string | undefined;
|
|
235
|
+
/** {@link DEFAULT_FILE_LABELS}, but formatting in the provider's locale. */
|
|
236
|
+
declare function useKitFileLabels(prop?: Partial<FileLabels>): FileLabels;
|
|
237
|
+
/**
|
|
238
|
+
* The dot paths of every key `labels` does NOT supply — the ones a translated app is
|
|
239
|
+
* still showing in English. Assert on it in the app's own test:
|
|
240
|
+
*
|
|
241
|
+
* expect(missingKitLabels(de)).toEqual([]);
|
|
242
|
+
*
|
|
243
|
+
* `reference` is the complete tree to check against — pass
|
|
244
|
+
* {@link DEFAULT_UI_KIT_LABELS} (src/i18n/defaults.ts). It is a parameter rather than an import
|
|
245
|
+
* because the defaults live beside their components, and this module is imported BY
|
|
246
|
+
* those components.
|
|
247
|
+
*/
|
|
248
|
+
declare function missingKitLabels(labels: UiKitLabelOverrides | undefined, reference: UiKitLabels): string[];
|
|
249
|
+
|
|
250
|
+
export { type AppShellLabels, type CalculatorLabels, type ComboboxLabels, type CommonLabels, type CurrencyLabels, DEFAULT_APP_SHELL_LABELS, DEFAULT_CALCULATOR_LABELS, DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, DEFAULT_CURRENCY_LABELS, DEFAULT_DATE_PICKER_LABELS, DEFAULT_FILE_LABELS, DEFAULT_MULTI_SELECT_LABELS, DEFAULT_PICKER_SHEET_LABELS, DEFAULT_SWIPEABLE_ROW_LABELS, DEFAULT_TOP_BAR_LABELS, type DatePickerLabels, type FileLabels, type MultiSelectLabels, type PickerSheetLabels, type SwipeableRowLabels, type TopBarLabels, type UiKitLabelOverrides, type UiKitLabels, UiKitProvider, type UiKitProviderProps, formatFileSize, missingKitLabels, useKitFileLabels, useKitLabelOverrides, useKitLabels, useKitLocale };
|