@eifi1/ui-kit 0.4.1 → 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 +89 -19
- 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 +74 -19
- package/dist/components/dropdown.js +72 -22
- 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 +21 -7
- package/dist/components/file-dropzone.js +28 -7
- 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 +21 -2
- package/dist/components/number-input.js +2 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +25 -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 +5 -3
- package/dist/components/user-avatar.js +4 -2
- 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 +14 -1
- package/dist/theme/palette-presets.js +231 -3
- 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 +66 -13
- 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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
import { HoverMenu } from "../components/hover-menu";
|
|
4
|
+
import { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
|
|
5
|
+
import { Tooltip } from "../components/tooltip";
|
|
6
|
+
|
|
7
|
+
export interface OptionSwitcherOption<T extends string> {
|
|
8
|
+
value: T;
|
|
9
|
+
label: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A top-bar hover menu that switches between a small set of options, with a
|
|
14
|
+
* check on the active one and an optional heading. Generic glue over
|
|
15
|
+
* {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment
|
|
16
|
+
* picker. Controlled: the app owns `value` and `onSelect`.
|
|
17
|
+
*/
|
|
18
|
+
export function OptionSwitcherMenu<T extends string>({
|
|
19
|
+
icon,
|
|
20
|
+
ariaLabel,
|
|
21
|
+
title,
|
|
22
|
+
heading,
|
|
23
|
+
options,
|
|
24
|
+
value,
|
|
25
|
+
onSelect,
|
|
26
|
+
}: {
|
|
27
|
+
icon: ReactNode;
|
|
28
|
+
ariaLabel: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
heading?: string;
|
|
31
|
+
options: OptionSwitcherOption<T>[];
|
|
32
|
+
value: T;
|
|
33
|
+
onSelect: (value: T) => void;
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<HoverMenu
|
|
37
|
+
ariaLabel={ariaLabel}
|
|
38
|
+
trigger={({ toggle }) => (
|
|
39
|
+
<Tooltip label={title} portal>
|
|
40
|
+
<button type="button" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>
|
|
41
|
+
{icon}
|
|
42
|
+
</button>
|
|
43
|
+
</Tooltip>
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{(close) => (
|
|
47
|
+
<ul className="py-1">
|
|
48
|
+
{heading && (
|
|
49
|
+
<li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
50
|
+
{heading}
|
|
51
|
+
</li>
|
|
52
|
+
)}
|
|
53
|
+
{options.map((opt) => (
|
|
54
|
+
<li key={opt.value}>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => {
|
|
58
|
+
onSelect(opt.value);
|
|
59
|
+
close();
|
|
60
|
+
}}
|
|
61
|
+
className={TOPBAR_MENU_ITEM_CLASS}
|
|
62
|
+
>
|
|
63
|
+
<span>{opt.label}</span>
|
|
64
|
+
{value === opt.value && <Check className="size-4 text-[var(--text-secondary)]" />}
|
|
65
|
+
</button>
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
)}
|
|
70
|
+
</HoverMenu>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { FlaskConical } from "lucide-react";
|
|
2
|
+
import { OptionSwitcherMenu } from "./option-switcher-menu";
|
|
3
|
+
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
4
|
+
|
|
5
|
+
export interface RoleSwitcherProps<R extends string> {
|
|
6
|
+
roles: readonly R[];
|
|
7
|
+
value: R;
|
|
8
|
+
onChange: (role: R) => void;
|
|
9
|
+
/** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
heading?: string;
|
|
12
|
+
/** The trigger's tooltip. Default: `topBar.role(value)`. */
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A reusable user-role switcher (feedback #337): a thin wrapper over
|
|
18
|
+
* {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in
|
|
19
|
+
* the top bar with just the role list + current value + a handler. Typically
|
|
20
|
+
* dev-only / UI-only, but the app decides where and whether to render it.
|
|
21
|
+
*
|
|
22
|
+
* The flask wears `--warning` rather than a fixed `amber-500`: it is a caution
|
|
23
|
+
* marker — the app is not showing you your own role — and the token carries that
|
|
24
|
+
* amber across both themes instead of pinning one hue onto every palette.
|
|
25
|
+
*/
|
|
26
|
+
export function RoleSwitcher<R extends string>({
|
|
27
|
+
roles,
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
ariaLabel,
|
|
31
|
+
heading,
|
|
32
|
+
title,
|
|
33
|
+
}: RoleSwitcherProps<R>) {
|
|
34
|
+
const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });
|
|
35
|
+
return (
|
|
36
|
+
<OptionSwitcherMenu<R>
|
|
37
|
+
ariaLabel={labels.switchRole}
|
|
38
|
+
title={title ?? labels.role(value)}
|
|
39
|
+
heading={heading}
|
|
40
|
+
icon={<FlaskConical className="size-5 text-[var(--warning)]" />}
|
|
41
|
+
options={roles.map((r) => ({ value: r, label: r }))}
|
|
42
|
+
value={value}
|
|
43
|
+
onSelect={onChange}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `extends ComponentPropsWithoutRef<"header">` because this IS the `<header>`, and it is
|
|
6
|
+
* the `sticky top-0` element itself: a consumer cannot wrap it to add an attribute
|
|
7
|
+
* without the sticking moving to the wrapper. The one that matters most is `aria-label`
|
|
8
|
+
* — a `<header>` is a `banner` landmark, and a landmark with no name is how two of them
|
|
9
|
+
* become indistinguishable in a screen reader's landmark list.
|
|
10
|
+
*/
|
|
11
|
+
export interface TopBarProps extends ComponentPropsWithoutRef<"header"> {
|
|
12
|
+
brand: ReactNode;
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The sticky app header frame: a `brand` slot on the left and an `actions` slot
|
|
19
|
+
* on the right (theme/palette/language controls, account menu, etc.). Domain-free
|
|
20
|
+
* — each app composes its own actions from the shared controls plus its own menus.
|
|
21
|
+
*/
|
|
22
|
+
export function TopBar({ brand, actions, className, ...rest }: TopBarProps) {
|
|
23
|
+
return (
|
|
24
|
+
<header
|
|
25
|
+
{...rest}
|
|
26
|
+
className={cn(
|
|
27
|
+
// z-40 keeps the app header above sticky page content — notably the
|
|
28
|
+
// expanded transaction editor (z-30), which on mobile shares the page
|
|
29
|
+
// scroll and otherwise slid over the header while scrolling (feedback
|
|
30
|
+
// #320). Modals/tour (z-50/60) still sit above it.
|
|
31
|
+
"sticky top-0 z-40 flex h-12 items-center justify-between gap-2 border-b border-[var(--border)] bg-[var(--bg-surface)] px-3 md:px-6",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<div className="flex min-w-0 items-center gap-2">{brand}</div>
|
|
36
|
+
<div className="flex items-center gap-1">{actions}</div>
|
|
37
|
+
</header>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { HoverMenu } from "../components/hover-menu";
|
|
5
|
+
import { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
|
|
9
|
+
* `link` navigates via react-router, and a `divider` draws a rule. Non-divider
|
|
10
|
+
* rows close the menu automatically when chosen. `kind` may be omitted for
|
|
11
|
+
* actions, keeping the common case terse.
|
|
12
|
+
*/
|
|
13
|
+
export type TopBarMenuEntry =
|
|
14
|
+
| { kind?: "action"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; onSelect: () => void }
|
|
15
|
+
| { kind: "link"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; to: string }
|
|
16
|
+
| { kind: "divider"; key: string };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
|
|
20
|
+
* with an optional uppercase heading and divider rules. Generic glue over the
|
|
21
|
+
* shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —
|
|
22
|
+
* so those call sites describe their rows as data instead of rebuilding the
|
|
23
|
+
* trigger + list markup. For a checkmarked value-switcher use
|
|
24
|
+
* {@link OptionSwitcherMenu} instead.
|
|
25
|
+
*/
|
|
26
|
+
export function TopBarActionMenu({
|
|
27
|
+
icon,
|
|
28
|
+
ariaLabel,
|
|
29
|
+
heading,
|
|
30
|
+
entries,
|
|
31
|
+
panelClassName,
|
|
32
|
+
}: {
|
|
33
|
+
icon: ReactNode;
|
|
34
|
+
ariaLabel: string;
|
|
35
|
+
heading?: string;
|
|
36
|
+
entries: TopBarMenuEntry[];
|
|
37
|
+
panelClassName?: string;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<HoverMenu
|
|
41
|
+
ariaLabel={ariaLabel}
|
|
42
|
+
trigger={({ toggle }) => (
|
|
43
|
+
<button type="button" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>
|
|
44
|
+
{icon}
|
|
45
|
+
</button>
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{(close) => (
|
|
49
|
+
<ul className={cn("py-1", panelClassName)}>
|
|
50
|
+
{heading && (
|
|
51
|
+
<li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
52
|
+
{heading}
|
|
53
|
+
</li>
|
|
54
|
+
)}
|
|
55
|
+
{entries.map((entry) => {
|
|
56
|
+
if (entry.kind === "divider") {
|
|
57
|
+
return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
|
|
58
|
+
}
|
|
59
|
+
const content = (
|
|
60
|
+
<span className="flex w-full items-center gap-2">
|
|
61
|
+
{entry.icon}
|
|
62
|
+
{entry.label}
|
|
63
|
+
{entry.trailing && <span className="ml-auto pl-2">{entry.trailing}</span>}
|
|
64
|
+
</span>
|
|
65
|
+
);
|
|
66
|
+
return (
|
|
67
|
+
<li key={entry.key}>
|
|
68
|
+
{entry.kind === "link" ? (
|
|
69
|
+
<Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>
|
|
70
|
+
{content}
|
|
71
|
+
</Link>
|
|
72
|
+
) : (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={() => {
|
|
76
|
+
entry.onSelect();
|
|
77
|
+
close();
|
|
78
|
+
}}
|
|
79
|
+
className={TOPBAR_MENU_ITEM_CLASS}
|
|
80
|
+
>
|
|
81
|
+
{content}
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
</li>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</ul>
|
|
88
|
+
)}
|
|
89
|
+
</HoverMenu>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Check, Globe, Moon, Palette, Sun } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { HoverMenu } from "../components/hover-menu";
|
|
4
|
+
import type { PalettePreset } from "../theme/palette-presets";
|
|
5
|
+
import type { ThemeMode } from "../theme/theme-store";
|
|
6
|
+
import { Tooltip } from "../components/tooltip";
|
|
7
|
+
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
8
|
+
|
|
9
|
+
/** Shared square icon-button styling for top-bar triggers, so app-owned controls
|
|
10
|
+
* (account/feedback menus) line up with the shared ones. */
|
|
11
|
+
export const TOPBAR_TRIGGER_CLASS =
|
|
12
|
+
"size-9 rounded-md flex items-center justify-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
|
|
13
|
+
|
|
14
|
+
/** Shared row styling for the top-bar hover-menu items. */
|
|
15
|
+
export const TOPBAR_MENU_ITEM_CLASS =
|
|
16
|
+
"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
|
|
17
|
+
|
|
18
|
+
/** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */
|
|
19
|
+
export function ThemeToggle({
|
|
20
|
+
mode,
|
|
21
|
+
onToggle,
|
|
22
|
+
ariaLabel,
|
|
23
|
+
title,
|
|
24
|
+
className,
|
|
25
|
+
}: {
|
|
26
|
+
mode: ThemeMode;
|
|
27
|
+
onToggle: () => void;
|
|
28
|
+
/** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */
|
|
29
|
+
ariaLabel?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
className?: string;
|
|
32
|
+
}) {
|
|
33
|
+
// This had no default at all, so a toggle rendered without `ariaLabel` was an
|
|
34
|
+
// icon button with no name — a reader heard "button" and nothing else.
|
|
35
|
+
const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });
|
|
36
|
+
const Icon = mode === "dark" ? Moon : Sun;
|
|
37
|
+
return (
|
|
38
|
+
<Tooltip label={title} portal>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={onToggle}
|
|
42
|
+
aria-label={labels.theme}
|
|
43
|
+
className={cn(TOPBAR_TRIGGER_CLASS, className)}
|
|
44
|
+
>
|
|
45
|
+
<Icon className="size-5" />
|
|
46
|
+
</button>
|
|
47
|
+
</Tooltip>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Appearance-preset switcher: a hover menu of palette presets with per-theme
|
|
52
|
+
* colour swatches. Controlled — the app owns the active id and applies it. */
|
|
53
|
+
export function PaletteMenu({
|
|
54
|
+
palettes,
|
|
55
|
+
activeId,
|
|
56
|
+
mode,
|
|
57
|
+
onSelect,
|
|
58
|
+
ariaLabel,
|
|
59
|
+
heading,
|
|
60
|
+
}: {
|
|
61
|
+
palettes: PalettePreset[];
|
|
62
|
+
activeId: string;
|
|
63
|
+
mode: ThemeMode;
|
|
64
|
+
onSelect: (id: string) => void;
|
|
65
|
+
/** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */
|
|
66
|
+
ariaLabel?: string;
|
|
67
|
+
/** Optional small heading row above the list. */
|
|
68
|
+
heading?: string;
|
|
69
|
+
}) {
|
|
70
|
+
const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });
|
|
71
|
+
const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;
|
|
72
|
+
return (
|
|
73
|
+
<HoverMenu
|
|
74
|
+
ariaLabel={labels.palette}
|
|
75
|
+
trigger={({ toggle }) => (
|
|
76
|
+
<Tooltip label={activeName} portal>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={toggle}
|
|
80
|
+
aria-label={labels.palette}
|
|
81
|
+
className={TOPBAR_TRIGGER_CLASS}
|
|
82
|
+
>
|
|
83
|
+
{/* A DATA colour, not a semantic one: the palette mark is the
|
|
84
|
+
"colour" control, so it carries a colour, and which hue is
|
|
85
|
+
arbitrary. `--chart-1` is the violet end of the CVD-safe
|
|
86
|
+
categorical ramp and flips per theme — a fixed `violet-500` did
|
|
87
|
+
not, and stayed a cool violet island on a warm page. */}
|
|
88
|
+
<Palette className="size-5 text-[var(--chart-1)]" />
|
|
89
|
+
</button>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{(close) => (
|
|
94
|
+
<ul className="w-60 py-1">
|
|
95
|
+
{heading && (
|
|
96
|
+
<li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
97
|
+
{heading}
|
|
98
|
+
</li>
|
|
99
|
+
)}
|
|
100
|
+
{palettes.map((p) => {
|
|
101
|
+
const ts = p[mode];
|
|
102
|
+
const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];
|
|
103
|
+
return (
|
|
104
|
+
<li key={p.id}>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => {
|
|
108
|
+
onSelect(p.id);
|
|
109
|
+
close();
|
|
110
|
+
}}
|
|
111
|
+
className={TOPBAR_MENU_ITEM_CLASS}
|
|
112
|
+
>
|
|
113
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
114
|
+
<span className="flex shrink-0 gap-0.5">
|
|
115
|
+
{/* Deliberately a neutral at 10% and not a token: this hairline
|
|
116
|
+
sits on an ARBITRARY preset colour, so it has to read against
|
|
117
|
+
whatever that preset paints, which no themed border does. */}
|
|
118
|
+
{swatches.map((c, i) => (
|
|
119
|
+
<span
|
|
120
|
+
key={i}
|
|
121
|
+
className="size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10"
|
|
122
|
+
style={{ background: c }}
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</span>
|
|
126
|
+
<span className="flex min-w-0 flex-col text-left">
|
|
127
|
+
<span className="truncate">{p.name}</span>
|
|
128
|
+
<span className="truncate text-[11px] text-[var(--text-placeholder)]">
|
|
129
|
+
{p.blurb}
|
|
130
|
+
</span>
|
|
131
|
+
</span>
|
|
132
|
+
</span>
|
|
133
|
+
{activeId === p.id && (
|
|
134
|
+
<Check className="size-4 shrink-0 text-[var(--text-secondary)]" />
|
|
135
|
+
)}
|
|
136
|
+
</button>
|
|
137
|
+
</li>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</ul>
|
|
141
|
+
)}
|
|
142
|
+
</HoverMenu>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface LanguageOption {
|
|
147
|
+
code: string;
|
|
148
|
+
label: string;
|
|
149
|
+
/** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */
|
|
150
|
+
country: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Language switcher: a hover menu of options with flags and a check on the
|
|
154
|
+
* active one. Controlled — the app owns the current code and change handler.
|
|
155
|
+
*
|
|
156
|
+
* The trigger is the *active* option's flag, not an icon: lucide's `Languages`
|
|
157
|
+
* glyph (文A) is the same mark Chrome puts in the omnibox for "translate this
|
|
158
|
+
* page", so it read as machine-translation rather than "pick the UI language"
|
|
159
|
+
* (Keksdose feedback #121). The flag also shows which language is currently on,
|
|
160
|
+
* which no icon can, and costs the same width on a crowded mobile bar. A globe
|
|
161
|
+
* covers the case where `current` matches nothing in `options`. */
|
|
162
|
+
export function LanguageMenu({
|
|
163
|
+
options,
|
|
164
|
+
current,
|
|
165
|
+
onChange,
|
|
166
|
+
ariaLabel,
|
|
167
|
+
}: {
|
|
168
|
+
options: LanguageOption[];
|
|
169
|
+
current: string | undefined;
|
|
170
|
+
onChange: (code: string) => void;
|
|
171
|
+
/** Default: `topBar.language` from the {@link UiKitProvider}, else English. */
|
|
172
|
+
ariaLabel?: string;
|
|
173
|
+
}) {
|
|
174
|
+
const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
|
|
175
|
+
const active = options.find((o) => o.code === current);
|
|
176
|
+
return (
|
|
177
|
+
<HoverMenu
|
|
178
|
+
ariaLabel={labels.language}
|
|
179
|
+
trigger={({ toggle }) => (
|
|
180
|
+
<Tooltip label={active?.label ?? labels.language} portal>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={toggle}
|
|
184
|
+
aria-label={labels.language}
|
|
185
|
+
className={TOPBAR_TRIGGER_CLASS}
|
|
186
|
+
>
|
|
187
|
+
{active ? (
|
|
188
|
+
<span
|
|
189
|
+
aria-hidden
|
|
190
|
+
className={`fi fi-${active.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}
|
|
191
|
+
/>
|
|
192
|
+
) : (
|
|
193
|
+
<Globe className="size-5" />
|
|
194
|
+
)}
|
|
195
|
+
</button>
|
|
196
|
+
</Tooltip>
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{(close) => (
|
|
200
|
+
<ul className="py-1">
|
|
201
|
+
{options.map((lang) => {
|
|
202
|
+
const isActive = lang.code === current;
|
|
203
|
+
return (
|
|
204
|
+
<li key={lang.code}>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => {
|
|
208
|
+
onChange(lang.code);
|
|
209
|
+
close();
|
|
210
|
+
}}
|
|
211
|
+
className={TOPBAR_MENU_ITEM_CLASS}
|
|
212
|
+
>
|
|
213
|
+
<span className="flex items-center gap-2">
|
|
214
|
+
<span
|
|
215
|
+
aria-hidden
|
|
216
|
+
className={`fi fi-${lang.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}
|
|
217
|
+
/>
|
|
218
|
+
{lang.label}
|
|
219
|
+
</span>
|
|
220
|
+
{isActive && <Check className="size-4 text-[var(--text-secondary)]" />}
|
|
221
|
+
</button>
|
|
222
|
+
</li>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
</ul>
|
|
226
|
+
)}
|
|
227
|
+
</HoverMenu>
|
|
228
|
+
);
|
|
229
|
+
}
|
package/src/shell.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// `@eifi1/ui-kit/shell` — the composable app chrome.
|
|
2
|
+
//
|
|
3
|
+
// A re-slicing of the main barrel, not a new API. This entry point requires
|
|
4
|
+
// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.
|
|
5
|
+
export * from "./shell/topbar-controls";
|
|
6
|
+
export * from "./shell/top-bar";
|
|
7
|
+
export * from "./shell/app-shell";
|
|
8
|
+
export * from "./shell/option-switcher-menu";
|
|
9
|
+
export * from "./shell/role-switcher";
|
|
10
|
+
export * from "./shell/topbar-action-menu";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Chart colour system. Single source of truth lives in the design tokens
|
|
2
|
+
// (:root / .dark in tokens.css): brand = indigo, money = teal/amber/purple,
|
|
3
|
+
// chart-1..9 = Paul Tol "Muted" (CVD-safe across protan/deutan/tritan).
|
|
4
|
+
// Returning CSS vars here makes every Recharts series theme-aware for free — the
|
|
5
|
+
// SVG fill/stroke resolves --chart-N against the current light/dark token set.
|
|
6
|
+
|
|
7
|
+
import { contrast, parseHex as parseRgbHex } from "./color";
|
|
8
|
+
|
|
9
|
+
const PALETTE_SIZE = 9;
|
|
10
|
+
|
|
11
|
+
/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var. */
|
|
12
|
+
export function paletteFor(index: number): string {
|
|
13
|
+
return `var(--chart-${(index % PALETTE_SIZE) + 1})`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow
|
|
17
|
+
cues at the call site so colour is never the only signal (CVD). */
|
|
18
|
+
export const CHART_COLORS = {
|
|
19
|
+
income: "var(--money-income)",
|
|
20
|
+
expense: "var(--money-expense)",
|
|
21
|
+
net: "var(--money-net)",
|
|
22
|
+
assigned: "var(--brand)",
|
|
23
|
+
activity: "var(--money-expense)",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
// ── Raw hex stops, by theme ────────────────────────────────────────────────
|
|
27
|
+
// The heatmaps interpolate colours in JS (CSS vars can't be lerped), so they
|
|
28
|
+
// need concrete hex per theme — i.e. the same colours DEFAULT_PRESET already
|
|
29
|
+
// holds, written down a second time.
|
|
30
|
+
//
|
|
31
|
+
// They are duplicated rather than imported because these two objects are the whole
|
|
32
|
+
// of the `@eifi1/ui-kit/chart` subpath's colour layer, and importing the preset bank
|
|
33
|
+
// would pull every preset's full token set into that chunk to read two of them. What
|
|
34
|
+
// keeps the copies together is `chart-palette.test.ts` — "has not drifted from
|
|
35
|
+
// DEFAULT_PRESET" — which is not optional: they HAD drifted, five of the six light
|
|
36
|
+
// heat stops, and the light `neutral` was still Tailwind's slate-100, a cool grey on
|
|
37
|
+
// the warm cream page this theme has used since #328.
|
|
38
|
+
export const PALETTE_HEX = {
|
|
39
|
+
light: ["#332288", "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677", "#882255", "#aa4499"],
|
|
40
|
+
dark: ["#7e72d6", "#9fd8f2", "#5fc4b0", "#3fa45f", "#bfbf5e", "#e8dda0", "#e08c9a", "#c25a78", "#cc78be"],
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/** A heatmap's interpolation stops for one theme. Open string fields so any
|
|
44
|
+
palette preset (not just the built-in default) can supply its own stops. */
|
|
45
|
+
export interface HeatStops {
|
|
46
|
+
neutral: string;
|
|
47
|
+
under: string;
|
|
48
|
+
over: string;
|
|
49
|
+
seqLow: string;
|
|
50
|
+
seqHigh: string;
|
|
51
|
+
empty: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Diverging (variance: under←0→over) and sequential (spending intensity)
|
|
55
|
+
heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */
|
|
56
|
+
export const HEATMAP_HEX = {
|
|
57
|
+
light: {
|
|
58
|
+
neutral: "#f1e7d2", // the cream surface itself, not a cool grey
|
|
59
|
+
under: "#0b6650", // DEFAULT_PRESET.light.moneyIncome
|
|
60
|
+
over: "#9c6418", // DEFAULT_PRESET.light.moneyExpense, warmed to the page
|
|
61
|
+
seqLow: "#f6ecd0",
|
|
62
|
+
seqHigh: "#9c6418",
|
|
63
|
+
empty: "rgba(148,163,184,0.14)",
|
|
64
|
+
},
|
|
65
|
+
dark: {
|
|
66
|
+
neutral: "#363a5a", // one step above --border on the indigo-noir surface
|
|
67
|
+
under: "#22c3b6", // DEFAULT_PRESET.dark.moneyIncome
|
|
68
|
+
// Brighter than dark.moneyExpense (#e4b035) on purpose: a gradient endpoint is
|
|
69
|
+
// meant to be the loudest thing in its scale, and it is a fill, not a figure.
|
|
70
|
+
over: "#fbbf24",
|
|
71
|
+
seqLow: "#3a2c14", // dim amber, sits just above the page
|
|
72
|
+
seqHigh: "#fbbf24",
|
|
73
|
+
empty: "rgba(148,163,184,0.10)",
|
|
74
|
+
},
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
/** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */
|
|
78
|
+
export function lerpHex(a: string, b: string, t: number): string {
|
|
79
|
+
const pa = parseHex(a) ?? UNREADABLE;
|
|
80
|
+
const pb = parseHex(b) ?? UNREADABLE;
|
|
81
|
+
const c = (i: number) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));
|
|
82
|
+
return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The two inks a cell/treemap label can be set in. Near-black and near-white rather
|
|
86
|
+
than #000/#fff: at full black-on-full-white the label stops looking like part of the
|
|
87
|
+
same type system as the rest of the page. */
|
|
88
|
+
const INK_DARK = "#1a1a1a";
|
|
89
|
+
const INK_LIGHT = "#f8fafc";
|
|
90
|
+
|
|
91
|
+
/** Pick a legible text colour (near-black / near-white) for a solid hex fill, by
|
|
92
|
+
MEASURING both against it and keeping the one that wins.
|
|
93
|
+
|
|
94
|
+
This used to flip on Rec.601 luma > 0.6, which is a brightness heuristic and not a
|
|
95
|
+
contrast measure — the two disagree on six of the eighteen default chart hues, a
|
|
96
|
+
third of the ramp, and on each of those the label got the worse of the two inks.
|
|
97
|
+
`#44aa99` is the plainest case: luma calls it dark, so it took white, which measures
|
|
98
|
+
2.69:1 on it; near-black measures 6.18:1. keksdose measured this too and forked the
|
|
99
|
+
function locally for its treemap, leaving its other two call sites on this one.
|
|
100
|
+
|
|
101
|
+
A fill this cannot read gets `currentColor` — inherit, i.e. whatever the
|
|
102
|
+
surrounding text already uses. That is the only answer that is right on both
|
|
103
|
+
themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*
|
|
104
|
+
.empty` is an `rgba()` string, and the variance heatmap does pass it here for any
|
|
105
|
+
category with zero assigned and positive activity — a refund). Picking a constant
|
|
106
|
+
instead is what produced the earlier bug: the unparsed fill fell through to the
|
|
107
|
+
light ink, near-white on a near-white cell, and the figure was invisible. */
|
|
108
|
+
export function textOn(hex: string): string {
|
|
109
|
+
const rgb = parseRgbHex(hex);
|
|
110
|
+
if (!rgb) return "currentColor";
|
|
111
|
+
// Ties go to the dark ink: at equal measured contrast it is the one that keeps a
|
|
112
|
+
// small label looking like type rather than like a glow.
|
|
113
|
+
return contrast(rgb, INK_DARK) >= contrast(rgb, INK_LIGHT) ? INK_DARK : INK_LIGHT;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Mid-grey, for a stop {@link lerpHex} has to interpolate but cannot read. It has
|
|
117
|
+
* to return SOME valid colour, and a grey is the one that misleads least. */
|
|
118
|
+
const UNREADABLE: [number, number, number] = [0x80, 0x80, 0x80];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `#rgb` or `#rrggbb` to components, or **null** when it is neither.
|
|
122
|
+
*
|
|
123
|
+
* A tuple-shaped shim over `color.ts`'s parser, which is the same function and is
|
|
124
|
+
* already the one `contrast` validates with — so {@link lerpHex} and {@link textOn}
|
|
125
|
+
* cannot disagree about what counts as a colour.
|
|
126
|
+
*
|
|
127
|
+
* The **null** is the whole point of it. This used to `parseInt` whatever it was given
|
|
128
|
+
* and hand the NaNs on, and both callers propagated them silently: {@link lerpHex}
|
|
129
|
+
* produced the literal string `"#nannannan"`, and {@link textOn} fell through to the
|
|
130
|
+
* LIGHT label colour.
|
|
131
|
+
*
|
|
132
|
+
* That is not hypothetical — a non-hex value ships in this very module.
|
|
133
|
+
* `HEATMAP_HEX.*.empty` is an `rgba(…)` string, and the variance heatmap paints a
|
|
134
|
+
* figure on an `empty` cell whenever a category has zero assigned and positive
|
|
135
|
+
* activity (a refund). On the light theme the cell is near-white and the label came
|
|
136
|
+
* back near-white: the number was there, and unreadable.
|
|
137
|
+
*/
|
|
138
|
+
function parseHex(hex: string): [number, number, number] | null {
|
|
139
|
+
const rgb = parseRgbHex(hex);
|
|
140
|
+
return rgb ? [rgb.r, rgb.g, rgb.b] : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function clamp01(t: number): number {
|
|
144
|
+
return t < 0 ? 0 : t > 1 ? 1 : t;
|
|
145
|
+
}
|