@eifi1/ui-kit 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -28
- package/dist/account/delete-account-setting.d.ts +8 -3
- package/dist/admin/admin-action-confirm.d.ts +8 -3
- package/dist/admin/invitations-panel.d.ts +8 -3
- package/dist/admin/invitations-panel.js +29 -26
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +8 -3
- package/dist/admin/role-select.d.ts +10 -5
- package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
- package/dist/admin/user-roster.d.ts +10 -5
- package/dist/admin/user-roster.js +8 -47
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +43 -8
- package/dist/auth/auth-errors.js +24 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/register-form.d.ts +8 -3
- package/dist/billing/billing-banner.d.ts +184 -0
- package/dist/billing/billing-banner.js +98 -0
- package/dist/billing/billing-banner.js.map +1 -0
- package/dist/billing/billing-labels.d.ts +100 -0
- package/dist/billing/billing-labels.js +74 -0
- package/dist/billing/billing-labels.js.map +1 -0
- package/dist/billing/billing-lock.d.ts +158 -0
- package/dist/billing/billing-lock.js +17 -0
- package/dist/billing/billing-lock.js.map +1 -0
- package/dist/billing/billing-standing.d.ts +149 -0
- package/dist/billing/billing-standing.js +28 -0
- package/dist/billing/billing-standing.js.map +1 -0
- package/dist/billing/plan-card.d.ts +248 -0
- package/dist/billing/plan-card.js +248 -0
- package/dist/billing/plan-card.js.map +1 -0
- package/dist/billing/plan-limit-notice.d.ts +156 -0
- package/dist/billing/plan-limit-notice.js +76 -0
- package/dist/billing/plan-limit-notice.js.map +1 -0
- package/dist/billing/plan-limit.d.ts +34 -0
- package/dist/billing/plan-limit.js +20 -0
- package/dist/billing/plan-limit.js.map +1 -0
- package/dist/billing/plan-price.d.ts +61 -0
- package/dist/billing/plan-price.js +45 -0
- package/dist/billing/plan-price.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +133 -0
- package/dist/billing/subscription-actions.js +28 -0
- package/dist/billing/subscription-actions.js.map +1 -0
- package/dist/billing/subscription-status-chip.d.ts +149 -0
- package/dist/billing/subscription-status-chip.js +48 -0
- package/dist/billing/subscription-status-chip.js.map +1 -0
- package/dist/chart.d.ts +9 -3
- package/dist/components/account-chips.d.ts +16 -4
- package/dist/components/account-chips.js +21 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +6 -1
- package/dist/components/alert-banner.js +4 -3
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +8 -3
- package/dist/components/amount-input.js +5 -5
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/appearance-labels.d.ts +32 -0
- package/dist/components/appearance-labels.js +18 -0
- package/dist/components/appearance-labels.js.map +1 -0
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/breadcrumbs.js +3 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +2 -2
- package/dist/components/bulk-action-bar.js +7 -12
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +8 -3
- package/dist/components/calculator.d.ts +8 -3
- package/dist/components/calculator.js +4 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +5 -5
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-height.d.ts +23 -0
- package/dist/components/chart-height.js +15 -0
- package/dist/components/chart-height.js.map +1 -0
- package/dist/components/chart-zoom.js +6 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/checkbox-group.js +2 -2
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/checkbox.d.ts +10 -0
- package/dist/components/checkbox.js +25 -10
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +19 -3
- package/dist/components/chip.js +47 -21
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +1 -1
- package/dist/components/choice-card.js +8 -7
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper.js +1 -1
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +8 -7
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +8 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +8 -3
- package/dist/components/copy-button.d.ts +10 -5
- package/dist/components/country-select.js +1 -1
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +8 -3
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +4 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-labels.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +123 -52
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +1 -1
- package/dist/components/date-picker.js +7 -5
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.js +2 -2
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +3 -2
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -1
- package/dist/components/dropdown.js +7 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -6
- package/dist/components/facing-pair.js +3 -2
- package/dist/components/facing-pair.js.map +1 -1
- package/dist/components/field-parts.d.ts +102 -6
- package/dist/components/field-parts.js +81 -6
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.d.ts +4 -1
- package/dist/components/field-sync.js +12 -2
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.js +2 -2
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +8 -3
- package/dist/components/file-dropzone.d.ts +8 -3
- package/dist/components/floating-panel.js +7 -4
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.d.ts +12 -0
- package/dist/components/focus-ring.js +8 -0
- package/dist/components/focus-ring.js.map +1 -0
- package/dist/components/form-actions.d.ts +8 -3
- package/dist/components/form-actions.js +33 -19
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +11 -1
- package/dist/components/full-bleed-dialog.js +3 -1
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/iban-input.d.ts +8 -3
- package/dist/components/image-grid.js +1 -1
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js +6 -3
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +8 -3
- package/dist/components/legal-page.d.ts +26 -5
- package/dist/components/legal-page.js +3 -1
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +8 -3
- package/dist/components/legal.js +2 -1
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js +24 -2
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +2 -1
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +5 -0
- package/dist/components/list.js +13 -9
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +3 -1
- package/dist/components/measured-grid.js +38 -20
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js +2 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +8 -4
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.d.ts +8 -3
- package/dist/components/month-picker.js +9 -5
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/nav-pills.js +1 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +8 -3
- package/dist/components/number-input.d.ts +11 -4
- package/dist/components/number-input.js +8 -6
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +8 -3
- package/dist/components/numpad-sheet.js +2 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +2 -2
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +2 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/phone-input.d.ts +8 -3
- package/dist/components/phone-input.js +2 -2
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/pie-chart.d.ts +8 -2
- package/dist/components/pie-chart.js +11 -7
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +9 -1
- package/dist/components/popover.js +8 -4
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +100 -0
- package/dist/components/row-actions.js +165 -0
- package/dist/components/row-actions.js.map +1 -0
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/search-field.js +3 -2
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-budget.d.ts +30 -7
- package/dist/components/series-chart-budget.js +12 -4
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +7 -1
- package/dist/components/series-chart-ticks.js +6 -0
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +58 -28
- package/dist/components/series-chart.js +42 -32
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +51 -6
- package/dist/components/settings-fields.js +37 -0
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +11 -6
- package/dist/components/share-card.js +2 -0
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.d.ts +4 -0
- package/dist/components/sign-chip.js +17 -4
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.d.ts +3 -1
- package/dist/components/signature-pad.js +8 -5
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/slider.js +7 -6
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.d.ts +5 -1
- package/dist/components/sparkline.js +4 -2
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.d.ts +17 -1
- package/dist/components/stat-tile.js +48 -20
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -0
- package/dist/components/swipeable-row.js +2 -1
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +21 -7
- package/dist/components/switch.js +24 -11
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.js +2 -2
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +8 -3
- package/dist/components/text-link.js +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +7 -2
- package/dist/components/text.js +5 -5
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +2 -1
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +8 -3
- package/dist/components/toast.d.ts +18 -2
- package/dist/components/toast.js +8 -5
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.d.ts +16 -1
- package/dist/components/toggle-group.js +40 -11
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +10 -1
- package/dist/components/toggle-legend.js +21 -7
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +4 -0
- package/dist/components/translation-review.d.ts +4 -0
- package/dist/components/translation-review.js +3 -3
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +3 -2
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +33 -16
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +8 -3
- package/dist/components/ui.js +202 -114
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +5 -1
- package/dist/components/user-avatar.js +1 -1
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
- package/dist/data-table.d.ts +1 -1
- package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-BvdQdbv3.d.ts} +605 -58
- package/dist/demo/demo-banner.js +1 -0
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +8 -3
- package/dist/feedback/feedback-context.d.ts +8 -3
- package/dist/feedback/feedback-crash.d.ts +8 -3
- package/dist/feedback/feedback-dialog.d.ts +8 -3
- package/dist/feedback/feedback-dialog.js +3 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +8 -3
- package/dist/feedback/feedback-inbox.js +2 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +8 -3
- package/dist/feedback/feedback-menu.d.ts +9 -4
- package/dist/feedback/feedback-record.d.ts +8 -3
- package/dist/feedback/feedback-row-detail.d.ts +8 -3
- package/dist/feedback/feedback-status-undo.d.ts +8 -3
- package/dist/feedback/feedback-submit.d.ts +8 -3
- package/dist/feedback/feedback-swipe.d.ts +8 -3
- package/dist/feedback/feedback-table.d.ts +8 -3
- package/dist/feedback/feedback-thread.d.ts +8 -3
- package/dist/feedback/feedback-thread.js +3 -3
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +8 -3
- package/dist/hooks/use-anchored-panel.d.ts +32 -3
- package/dist/hooks/use-anchored-panel.js +21 -3
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-breakpoint.d.ts +77 -0
- package/dist/hooks/use-breakpoint.js +50 -0
- package/dist/hooks/use-breakpoint.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +8 -3
- package/dist/hooks/use-large-text.d.ts +29 -0
- package/dist/hooks/use-large-text.js +20 -0
- package/dist/hooks/use-large-text.js.map +1 -0
- package/dist/hooks/use-windowed-rows.d.ts +52 -8
- package/dist/hooks/use-windowed-rows.js +94 -5
- package/dist/hooks/use-windowed-rows.js.map +1 -1
- package/dist/i18n/defaults.d.ts +8 -3
- package/dist/i18n/defaults.js +8 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +8 -3
- package/dist/i18n/german.js +89 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +8 -3
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -5
- package/dist/i18n/locales/de-CH.d.ts +8 -3
- package/dist/i18n/locales/en.d.ts +8 -3
- package/dist/i18n/locales/es.d.ts +8 -3
- package/dist/i18n/locales/es.js +84 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +8 -3
- package/dist/i18n/locales/fr.js +86 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +8 -3
- package/dist/i18n/locales/hu.js +86 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +8 -3
- package/dist/i18n/locales/it.js +87 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +8 -3
- package/dist/i18n/locales/zh.js +84 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +8 -3
- package/dist/i18n/review.js +15 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +27 -11
- package/dist/index.js +75 -4
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +8 -3
- package/dist/landing/landing-sections.d.ts +8 -3
- package/dist/landing/public-header.d.ts +9 -4
- package/dist/landing/public-header.js +3 -3
- package/dist/landing/public-header.js.map +1 -1
- package/dist/landing/routing.d.ts +8 -3
- package/dist/lib/cn.js +3 -2
- package/dist/lib/cn.js.map +1 -1
- package/dist/rhf/fields.d.ts +8 -3
- package/dist/rhf/fields.js +1 -1
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +8 -3
- package/dist/rhf/form.js +2 -2
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +8 -3
- package/dist/search/command-palette.d.ts +1 -1
- package/dist/search/command-palette.js +9 -8
- package/dist/search/command-palette.js.map +1 -1
- package/dist/settings/settings-layout.js +2 -1
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/settings-section.d.ts +8 -3
- package/dist/settings/use-account-appearance.d.ts +92 -0
- package/dist/settings/use-account-appearance.js +63 -0
- package/dist/settings/use-account-appearance.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +1 -1
- package/dist/settings/use-settings-route.js +2 -3
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.d.ts +12 -171
- package/dist/shell/app-shell.js +144 -28
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -1
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +8 -3
- package/dist/shell/top-bar-brand.js +3 -1
- package/dist/shell/top-bar-brand.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +4 -0
- package/dist/shell/topbar-action-menu.js +3 -3
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +4 -4
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +9 -5
- package/dist/theme/contrast-tokens.d.ts +41 -0
- package/dist/theme/contrast-tokens.js +59 -0
- package/dist/theme/contrast-tokens.js.map +1 -0
- package/dist/theme/contrast.d.ts +109 -0
- package/dist/theme/contrast.js +128 -0
- package/dist/theme/contrast.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +12 -1
- package/dist/theme/palette-presets.js +9 -4
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.d.ts +157 -0
- package/dist/theme/text-size.js +121 -0
- package/dist/theme/text-size.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +8 -3
- package/dist/wizard.d.ts +8 -3
- package/package.json +1 -1
- package/src/admin/invitations-panel.tsx +36 -29
- package/src/admin/user-roster.tsx +17 -72
- package/src/auth/auth-errors.ts +68 -7
- package/src/billing/billing-banner.tsx +205 -0
- package/src/billing/billing-labels.ts +224 -0
- package/src/billing/billing-lock.ts +75 -0
- package/src/billing/billing-standing.ts +72 -0
- package/src/billing/plan-card.tsx +458 -0
- package/src/billing/plan-limit-notice.tsx +145 -0
- package/src/billing/plan-limit.ts +53 -0
- package/src/billing/plan-price.ts +101 -0
- package/src/billing/subscription-actions.tsx +76 -0
- package/src/billing/subscription-status-chip.tsx +95 -0
- package/src/components/account-chips.tsx +36 -10
- package/src/components/alert-banner.tsx +11 -6
- package/src/components/amount-input.tsx +5 -5
- package/src/components/appearance-labels.ts +40 -0
- package/src/components/autocomplete.tsx +2 -2
- package/src/components/breadcrumbs.tsx +3 -3
- package/src/components/bulk-action-bar.tsx +11 -14
- package/src/components/calculator.tsx +4 -3
- package/src/components/calendar-heatmap.tsx +5 -5
- package/src/components/chart-height.ts +30 -0
- package/src/components/chart-zoom.tsx +6 -1
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +62 -20
- package/src/components/chip.tsx +105 -24
- package/src/components/choice-card.tsx +15 -10
- package/src/components/column-mapper.tsx +1 -1
- package/src/components/combobox-core.tsx +8 -7
- package/src/components/combobox.tsx +8 -8
- package/src/components/country-select.tsx +1 -1
- package/src/components/currency-select.tsx +3 -3
- package/src/components/data-table-filter-popover.tsx +7 -1
- package/src/components/data-table.tsx +160 -43
- package/src/components/date-picker.tsx +12 -9
- package/src/components/description-list.tsx +2 -2
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/disclosure.tsx +3 -2
- package/src/components/dropdown.tsx +10 -2
- package/src/components/facing-pair.ts +12 -5
- package/src/components/field-parts.tsx +190 -7
- package/src/components/field-sync.tsx +17 -3
- package/src/components/field.tsx +2 -2
- package/src/components/floating-panel.tsx +7 -4
- package/src/components/focus-ring.ts +12 -0
- package/src/components/form-actions.tsx +55 -18
- package/src/components/full-bleed-dialog.tsx +18 -4
- package/src/components/image-grid.tsx +1 -1
- package/src/components/inline-edit-field.tsx +6 -3
- package/src/components/legal-page.tsx +28 -2
- package/src/components/legal.tsx +19 -1
- package/src/components/lightbox.tsx +24 -2
- package/src/components/line-items.tsx +7 -1
- package/src/components/list.tsx +36 -10
- package/src/components/measured-grid.tsx +49 -24
- package/src/components/menu-item.tsx +2 -2
- package/src/components/mini-calendar.tsx +8 -4
- package/src/components/month-picker.tsx +9 -5
- package/src/components/nav-pills.tsx +1 -1
- package/src/components/number-input.tsx +14 -7
- package/src/components/numpad-sheet.tsx +2 -2
- package/src/components/one-time-code-input.tsx +2 -2
- package/src/components/page-contents.tsx +2 -2
- package/src/components/phone-input.tsx +2 -2
- package/src/components/pie-chart.tsx +19 -9
- package/src/components/popover.tsx +22 -5
- package/src/components/row-actions.tsx +344 -0
- package/src/components/scroll-area.tsx +1 -1
- package/src/components/search-field.tsx +4 -2
- package/src/components/series-chart-budget.ts +40 -8
- package/src/components/series-chart-ticks.ts +10 -0
- package/src/components/series-chart.tsx +123 -72
- package/src/components/settings-fields.tsx +81 -2
- package/src/components/share-card.tsx +10 -0
- package/src/components/sign-chip.tsx +28 -4
- package/src/components/signature-pad.tsx +13 -7
- package/src/components/slider.tsx +22 -9
- package/src/components/sparkline.tsx +11 -3
- package/src/components/stat-tile.tsx +70 -14
- package/src/components/swipeable-row.tsx +2 -1
- package/src/components/switch.tsx +57 -23
- package/src/components/table.tsx +2 -2
- package/src/components/text-link.tsx +1 -1
- package/src/components/text.tsx +11 -6
- package/src/components/tile-radio.tsx +4 -1
- package/src/components/toast.tsx +27 -7
- package/src/components/toggle-group.tsx +99 -12
- package/src/components/toggle-legend.tsx +53 -8
- package/src/components/translation-review.tsx +10 -5
- package/src/components/tree-view.tsx +3 -2
- package/src/components/treemap.tsx +27 -10
- package/src/components/ui.tsx +331 -145
- package/src/components/user-avatar.tsx +1 -1
- package/src/demo/demo-banner.tsx +5 -0
- package/src/feedback/feedback-dialog.tsx +3 -3
- package/src/feedback/feedback-inbox.tsx +2 -2
- package/src/feedback/feedback-thread.tsx +3 -3
- package/src/hooks/use-anchored-panel.ts +67 -5
- package/src/hooks/use-breakpoint.ts +121 -0
- package/src/hooks/use-large-text.ts +41 -0
- package/src/hooks/use-windowed-rows.ts +168 -12
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/german.ts +103 -0
- package/src/i18n/kit-labels.tsx +12 -0
- package/src/i18n/locales/es.ts +94 -0
- package/src/i18n/locales/fr.ts +98 -0
- package/src/i18n/locales/hu.ts +96 -0
- package/src/i18n/locales/it.ts +99 -0
- package/src/i18n/locales/zh.ts +90 -0
- package/src/i18n/review.ts +14 -0
- package/src/index.ts +74 -6
- package/src/landing/public-header.tsx +4 -4
- package/src/lib/cn.ts +13 -2
- package/src/rhf/fields.tsx +1 -1
- package/src/rhf/form.tsx +2 -2
- package/src/search/command-palette.tsx +10 -9
- package/src/settings/settings-layout.tsx +2 -1
- package/src/settings/use-account-appearance.ts +148 -0
- package/src/settings/use-settings-route.ts +3 -4
- package/src/shell/app-shell.tsx +278 -54
- package/src/shell/auth-layout.tsx +1 -1
- package/src/shell/option-switcher-menu.tsx +1 -1
- package/src/shell/top-bar-brand.tsx +3 -1
- package/src/shell/topbar-action-menu.tsx +3 -3
- package/src/shell/topbar-controls.tsx +4 -4
- package/src/theme/contrast-tokens.ts +112 -0
- package/src/theme/contrast.ts +224 -0
- package/src/theme/palette-presets.ts +24 -4
- package/src/theme/palette-store.ts +6 -1
- package/src/theme/text-size.ts +267 -0
- package/tokens.css +143 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { contrast, oklchToRgb, parseHex, rgbToOklch, toHex } from "./color";
|
|
2
|
+
import type { TokenSet } from "./palette-presets";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The "More contrast" step on a token set (docs/text-size-harmonization.md §5, §10.5).
|
|
6
|
+
* INTERNAL — `applyTokenSet` and the contrast layer share it; it is not re-exported.
|
|
7
|
+
*
|
|
8
|
+
* The step happens HERE, in JS, because the tokens it changes are written inline on
|
|
9
|
+
* `<html>` by `applyTokenSet`: an inline value beats any `[data-contrast=more]` rule and
|
|
10
|
+
* any `@media (prefers-contrast: more)` block, so a stylesheet cannot do it (§10.5, the
|
|
11
|
+
* keksdose review). `--text-placeholder` is a CSS-derived token, and it is written inline
|
|
12
|
+
* here too rather than redefined per attribute in tokens.css.
|
|
13
|
+
*
|
|
14
|
+
* What moves (§5):
|
|
15
|
+
* - `--text-muted` becomes the set's `--text-secondary`;
|
|
16
|
+
* - `--text-secondary` moves halfway to `--text-primary`;
|
|
17
|
+
* - `--text-placeholder` becomes the set's `--text-muted` (4.6–5.5:1 on every shipped
|
|
18
|
+
* preset, where the CSS formula gives 2.4–2.8:1) — the order primary > secondary >
|
|
19
|
+
* muted > placeholder survives the step;
|
|
20
|
+
* - `--border` moves toward `--text-primary` until it clears 3:1 against the worst of the
|
|
21
|
+
* three surfaces (WCAG 1.4.11, a field's boundary), and never less far than
|
|
22
|
+
* `--border-strong`'s 45 % — so every border is at least as strong as a "strong" one was.
|
|
23
|
+
* `--border-strong` stays derived in tokens.css and grows with it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The custom properties a contrast step writes. */
|
|
27
|
+
export const CONTRAST_STEPPED_VARS = ["--text-secondary", "--text-muted", "--text-placeholder", "--border"] as const;
|
|
28
|
+
|
|
29
|
+
export interface ContrastStep {
|
|
30
|
+
textSecondary: string;
|
|
31
|
+
textMuted: string;
|
|
32
|
+
textPlaceholder: string;
|
|
33
|
+
border: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Lab {
|
|
37
|
+
l: number;
|
|
38
|
+
a: number;
|
|
39
|
+
b: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toLab(hex: string): Lab | null {
|
|
43
|
+
const rgb = parseHex(hex);
|
|
44
|
+
if (!rgb) return null;
|
|
45
|
+
const { l, c, h } = rgbToOklch(rgb);
|
|
46
|
+
const rad = (h * Math.PI) / 180;
|
|
47
|
+
return { l, a: c * Math.cos(rad), b: c * Math.sin(rad) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `color-mix(in oklab, a p, b)` as a hex — the same formula tokens.css's derived roles
|
|
51
|
+
* use, so a stepped value sits where the stylesheet would have put it. A colour that is
|
|
52
|
+
* not hex (a consumer's own token set) comes back unmixed. */
|
|
53
|
+
function mix(a: string, b: string, p: number): string {
|
|
54
|
+
const A = toLab(a);
|
|
55
|
+
const B = toLab(b);
|
|
56
|
+
if (!A || !B) return a;
|
|
57
|
+
const l = A.l * p + B.l * (1 - p);
|
|
58
|
+
const x = A.a * p + B.a * (1 - p);
|
|
59
|
+
const y = A.b * p + B.b * (1 - p);
|
|
60
|
+
const c = Math.hypot(x, y);
|
|
61
|
+
const h = c < 1e-6 ? 0 : ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
|
|
62
|
+
return toHex(oklchToRgb({ l, c, h }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** WCAG 1.4.11 for a component's boundary. */
|
|
66
|
+
const BORDER_TARGET = 3;
|
|
67
|
+
/** `--border-strong` in tokens.css: 55 % border, 45 % text. */
|
|
68
|
+
const BORDER_STRONG_SHARE = 0.45;
|
|
69
|
+
|
|
70
|
+
function worstContrast(color: string, t: TokenSet): number {
|
|
71
|
+
return Math.min(contrast(color, t.bgPage), contrast(color, t.bgSurface), contrast(color, t.bgSurface2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function steppedBorder(t: TokenSet): string {
|
|
75
|
+
// The share of `--text-primary` in the border: at least --border-strong's, more until
|
|
76
|
+
// 3:1. Monotonic in the share (the mix walks from the border to the text, away from
|
|
77
|
+
// every surface), so a bisection finds the least that clears it.
|
|
78
|
+
let lo = BORDER_STRONG_SHARE;
|
|
79
|
+
if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t) >= BORDER_TARGET) return mix(t.border, t.textPrimary, 1 - lo);
|
|
80
|
+
let hi = 1;
|
|
81
|
+
for (let i = 0; i < 16; i++) {
|
|
82
|
+
const mid = (lo + hi) / 2;
|
|
83
|
+
if (worstContrast(mix(t.border, t.textPrimary, 1 - mid), t) >= BORDER_TARGET) hi = mid;
|
|
84
|
+
else lo = mid;
|
|
85
|
+
}
|
|
86
|
+
return mix(t.border, t.textPrimary, 1 - hi);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The four stepped values for `t`. Pure. */
|
|
90
|
+
export function contrastStep(t: TokenSet): ContrastStep {
|
|
91
|
+
return {
|
|
92
|
+
textSecondary: mix(t.textSecondary, t.textPrimary, 0.5),
|
|
93
|
+
textMuted: t.textSecondary,
|
|
94
|
+
textPlaceholder: t.textMuted,
|
|
95
|
+
border: steppedBorder(t),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ── The token set last written to an element ───────────────────────────── */
|
|
100
|
+
|
|
101
|
+
const lastWritten = new WeakMap<HTMLElement, TokenSet>();
|
|
102
|
+
|
|
103
|
+
/** Remember the set `applyTokenSet` wrote, so a contrast change can re-apply it. */
|
|
104
|
+
export function rememberTokenSet(el: HTMLElement, t: TokenSet): void {
|
|
105
|
+
lastWritten.set(el, t);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The set `applyTokenSet` last wrote to `el`, or undefined when nothing did — an app
|
|
109
|
+
* without the palette layer, whose base tokens come from tokens.css. */
|
|
110
|
+
export function rememberedTokenSet(el: HTMLElement): TokenSet | undefined {
|
|
111
|
+
return lastWritten.get(el);
|
|
112
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { create } from "zustand";
|
|
3
|
+
import { persist } from "zustand/middleware";
|
|
4
|
+
import { logger } from "../lib/logger";
|
|
5
|
+
import { readStored } from "../lib/safe-storage";
|
|
6
|
+
import { useMediaQuery } from "../hooks/use-media-query";
|
|
7
|
+
import { CONTRAST_STEPPED_VARS, contrastStep, rememberedTokenSet } from "./contrast-tokens";
|
|
8
|
+
import { applyTokenSet, DEFAULT_PRESET } from "./palette-presets";
|
|
9
|
+
import type { PersistedAppearanceOptions } from "./text-size";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* "More contrast" (docs/text-size-harmonization.md §2.4, §5, §10.5, §10.6): a switch
|
|
13
|
+
* beside the text size, on by choice or — on "System" — when the device asks for more
|
|
14
|
+
* contrast (`prefers-contrast: more`). Any text size.
|
|
15
|
+
*
|
|
16
|
+
* In force it is `<html data-contrast="more">` (else `"standard"`), which does two things:
|
|
17
|
+
* - the inline colour tokens are re-written one step stronger (`applyTokenSet`'s
|
|
18
|
+
* contrast flag — see contrast-tokens.ts for what moves and why it is JS);
|
|
19
|
+
* - tokens.css raises `--focus-ring-width` from 2 to 3 px and turns on the `contrast:`
|
|
20
|
+
* variant for anything else a component wants to change.
|
|
21
|
+
*
|
|
22
|
+
* The names below are frozen like the text size's: server-kit's `CONTRAST_MODES`, the
|
|
23
|
+
* stored format, the attribute.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The stored choices (§10.6): server-kit's `CONTRAST_MODES`. "system" IS a stored value
|
|
27
|
+
* — the PATCH rule refuses an explicit null, so null means only "never chosen". */
|
|
28
|
+
export const CONTRAST_MODES = ["system", "standard", "more"] as const;
|
|
29
|
+
|
|
30
|
+
export type ContrastMode = (typeof CONTRAST_MODES)[number];
|
|
31
|
+
|
|
32
|
+
/** What is in force on `<html data-contrast>`: a mode with "system" resolved. */
|
|
33
|
+
export type ContrastLevel = "standard" | "more";
|
|
34
|
+
|
|
35
|
+
const CONTRAST_ATTRIBUTE = "data-contrast";
|
|
36
|
+
const PREFERS_MORE_QUERY = "(prefers-contrast: more)";
|
|
37
|
+
|
|
38
|
+
export function isContrastMode(value: unknown): value is ContrastMode {
|
|
39
|
+
return value === "system" || value === "standard" || value === "more";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The mode in force: **this device's own choice → the account's → "system"** (§2.6).
|
|
43
|
+
* The one order the pre-paint call, the apply hook and `useAccountAppearance` share. */
|
|
44
|
+
export function resolveContrastMode(device: unknown, account?: unknown): ContrastMode {
|
|
45
|
+
if (isContrastMode(device)) return device;
|
|
46
|
+
if (isContrastMode(account)) return account;
|
|
47
|
+
return "system";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether the device asks for more contrast now. False without `matchMedia`. */
|
|
51
|
+
export function prefersMoreContrast(): boolean {
|
|
52
|
+
try {
|
|
53
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
54
|
+
? window.matchMedia(PREFERS_MORE_QUERY).matches
|
|
55
|
+
: false;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A mode to the level it puts on `<html>`; "system" asks the device (or takes
|
|
62
|
+
* `prefersMore`, when the caller already knows). */
|
|
63
|
+
export function contrastLevel(mode: ContrastMode, prefersMore: boolean = prefersMoreContrast()): ContrastLevel {
|
|
64
|
+
return mode === "more" || (mode === "system" && prefersMore) ? "more" : "standard";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The level `<html>` carries now; "standard" without one. */
|
|
68
|
+
export function readContrastLevel(): ContrastLevel {
|
|
69
|
+
if (typeof document === "undefined") return "standard";
|
|
70
|
+
return document.documentElement.getAttribute(CONTRAST_ATTRIBUTE) === "more" ? "more" : "standard";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Re-write the contrast-dependent tokens on `el` for its current `data-contrast`.
|
|
75
|
+
*
|
|
76
|
+
* With a palette layer (`applyTokenSet` has written a set here), that set is applied
|
|
77
|
+
* again and steps itself. Without one — an app whose base tokens come from tokens.css,
|
|
78
|
+
* like Kurvenschmiede's — the four stepped tokens are written over the stylesheet for
|
|
79
|
+
* the default preset in the current light/dark, or removed again.
|
|
80
|
+
*/
|
|
81
|
+
function reapplyContrastTokens(el: HTMLElement): void {
|
|
82
|
+
const written = rememberedTokenSet(el);
|
|
83
|
+
if (written) {
|
|
84
|
+
applyTokenSet(el, written);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const name of CONTRAST_STEPPED_VARS) el.style.removeProperty(name);
|
|
88
|
+
if (el.getAttribute(CONTRAST_ATTRIBUTE) !== "more") return;
|
|
89
|
+
const step = contrastStep(DEFAULT_PRESET[el.classList.contains("dark") ? "dark" : "light"]);
|
|
90
|
+
el.style.setProperty("--text-secondary", step.textSecondary);
|
|
91
|
+
el.style.setProperty("--text-muted", step.textMuted);
|
|
92
|
+
el.style.setProperty("--text-placeholder", step.textPlaceholder);
|
|
93
|
+
el.style.setProperty("--border", step.border);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Put `level` on `<html>` and re-write the colour tokens for it. */
|
|
97
|
+
export function applyContrast(level: ContrastLevel): void {
|
|
98
|
+
if (typeof document === "undefined") return;
|
|
99
|
+
const root = document.documentElement;
|
|
100
|
+
root.setAttribute(CONTRAST_ATTRIBUTE, level);
|
|
101
|
+
reapplyContrastTokens(root);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ── Storage: the frozen format, the pre-paint call ─────────────────────── */
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What a contrast store persists under its key — FROZEN, like the text size's:
|
|
108
|
+
*
|
|
109
|
+
* {"state":{"contrast":"more"},"version":1}
|
|
110
|
+
*
|
|
111
|
+
* `contrast` is one of {@link CONTRAST_MODES}, or `null` / absent when this device never
|
|
112
|
+
* chose.
|
|
113
|
+
*/
|
|
114
|
+
export interface PersistedContrast {
|
|
115
|
+
state?: { contrast?: ContrastMode | null };
|
|
116
|
+
version?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** This device's own choice under `storageKey`, or `null` when it never chose. */
|
|
120
|
+
export function readPersistedContrast(storageKey: string): ContrastMode | null {
|
|
121
|
+
try {
|
|
122
|
+
const blob = JSON.parse(readStored(storageKey) ?? "null") as PersistedContrast | null;
|
|
123
|
+
const mode = blob?.state?.contrast;
|
|
124
|
+
return isContrastMode(mode) ? mode : null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Pre-paint (§10.3, §10.5): this device's choice under `storageKey` — else the account's
|
|
132
|
+
* last known value, else "system" — resolved against `prefers-contrast` and applied
|
|
133
|
+
* BEFORE `createRoot`. Call it next to `applyPersistedPalette`, in either order: the
|
|
134
|
+
* palette steps itself when `data-contrast` is already set, and this re-applies the
|
|
135
|
+
* palette's set when it is not. Returns the level applied.
|
|
136
|
+
*/
|
|
137
|
+
export function applyPersistedContrast(storageKey: string, options: PersistedAppearanceOptions = {}): ContrastLevel {
|
|
138
|
+
let account: unknown;
|
|
139
|
+
try {
|
|
140
|
+
account = options.account?.();
|
|
141
|
+
} catch {
|
|
142
|
+
account = undefined;
|
|
143
|
+
}
|
|
144
|
+
const level = contrastLevel(resolveContrastMode(readPersistedContrast(storageKey), account));
|
|
145
|
+
applyContrast(level);
|
|
146
|
+
return level;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── The store ──────────────────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
export interface ContrastState {
|
|
152
|
+
/** This device's own choice (persisted), or `null` when it never chose — then the
|
|
153
|
+
* account's value applies, else "system". */
|
|
154
|
+
contrast: ContrastMode | null;
|
|
155
|
+
/** Store this device's choice; `null` forgets it. */
|
|
156
|
+
setContrast: (contrast: ContrastMode | null) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ApplyContrastOptions {
|
|
160
|
+
/** The account's value (`/auth/me`'s `contrast`): it applies while this device has no
|
|
161
|
+
* choice of its own. */
|
|
162
|
+
account?: string | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build a contrast store bound to a consumer-supplied localStorage key, shaped like
|
|
167
|
+
* `createThemeStore` and {@link createTextSizeStore} (§6). It holds the DEVICE's choice;
|
|
168
|
+
* the account's value comes in through `useApplyContrast({ account })`.
|
|
169
|
+
*
|
|
170
|
+
* `useApplyContrast` — mount it once near the root, beside `useApplyPalette`:
|
|
171
|
+
* - puts the level on `<html>` and steps the tokens;
|
|
172
|
+
* - on "System", follows `prefers-contrast: more` live (§10.5);
|
|
173
|
+
* - without a palette layer, re-writes the stepped tokens when the light/dark class
|
|
174
|
+
* changes, since they differ per theme.
|
|
175
|
+
*
|
|
176
|
+
* The persisted format is {@link PersistedContrast}, and frozen.
|
|
177
|
+
*/
|
|
178
|
+
export function createContrastStore(storageKey: string) {
|
|
179
|
+
const useContrastStore = create<ContrastState>()(
|
|
180
|
+
logger(
|
|
181
|
+
persist(
|
|
182
|
+
(set) => ({
|
|
183
|
+
contrast: null,
|
|
184
|
+
setContrast: (contrast) => set({ contrast: isContrastMode(contrast) ? contrast : null }),
|
|
185
|
+
}),
|
|
186
|
+
{
|
|
187
|
+
name: storageKey,
|
|
188
|
+
version: 1,
|
|
189
|
+
partialize: (s) => ({ contrast: s.contrast }),
|
|
190
|
+
merge: (persisted, current) => {
|
|
191
|
+
const value = (persisted as { contrast?: unknown } | null)?.contrast;
|
|
192
|
+
return { ...current, contrast: isContrastMode(value) ? value : null };
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
"contrast",
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
function useApplyContrast({ account }: ApplyContrastOptions = {}): ContrastLevel {
|
|
201
|
+
const device = useContrastStore((s) => s.contrast);
|
|
202
|
+
// Subscribed whatever the mode (hooks cannot be skipped); only "system" reads it.
|
|
203
|
+
const prefersMore = useMediaQuery(PREFERS_MORE_QUERY, false);
|
|
204
|
+
const level = contrastLevel(resolveContrastMode(device, account), prefersMore);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
applyContrast(level);
|
|
207
|
+
}, [level]);
|
|
208
|
+
// Without a palette layer the stepped tokens are per theme: follow `.dark`.
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (typeof MutationObserver === "undefined") return;
|
|
211
|
+
const root = document.documentElement;
|
|
212
|
+
const observer = new MutationObserver(() => {
|
|
213
|
+
if (!rememberedTokenSet(root)) reapplyContrastTokens(root);
|
|
214
|
+
});
|
|
215
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
216
|
+
return () => observer.disconnect();
|
|
217
|
+
}, []);
|
|
218
|
+
return level;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { useContrastStore, useApplyContrast };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type ContrastStore = ReturnType<typeof createContrastStore>;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// (the default preset here mirrors those exactly).
|
|
16
16
|
|
|
17
17
|
import type { HeatStops } from "./chart-palette";
|
|
18
|
+
import { contrastStep, rememberTokenSet } from "./contrast-tokens";
|
|
18
19
|
|
|
19
20
|
export interface TokenSet {
|
|
20
21
|
// Backgrounds & chrome
|
|
@@ -740,15 +741,34 @@ export function presetById(id: string): PalettePreset {
|
|
|
740
741
|
* have been silent and only a dead function would have noticed. The list is gone;
|
|
741
742
|
* the test beside this asserts instead that every field of a `TokenSet` reaches the
|
|
742
743
|
* element, which is what that list was standing in for.
|
|
744
|
+
*
|
|
745
|
+
* **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`
|
|
746
|
+
* the text and border tokens are written one step stronger, and `--text-placeholder` —
|
|
747
|
+
* otherwise derived in tokens.css — is written inline too; without it that inline
|
|
748
|
+
* placeholder is removed again, so the stylesheet's formula applies. The step is made
|
|
749
|
+
* HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.
|
|
750
|
+
*
|
|
751
|
+
* `contrast` defaults to the element's own `data-contrast="more"`, which the contrast
|
|
752
|
+
* layer (`applyContrast`, `useApplyContrast`) sets first. That is how the palette and the
|
|
753
|
+
* contrast cooperate without knowing each other: a palette change re-applies through
|
|
754
|
+
* here and keeps the contrast, and a contrast change re-applies the set remembered here.
|
|
743
755
|
*/
|
|
744
|
-
export function applyTokenSet(
|
|
756
|
+
export function applyTokenSet(
|
|
757
|
+
el: HTMLElement,
|
|
758
|
+
t: TokenSet,
|
|
759
|
+
contrast: boolean = el.getAttribute("data-contrast") === "more",
|
|
760
|
+
): void {
|
|
761
|
+
rememberTokenSet(el, t);
|
|
762
|
+
const step = contrast ? contrastStep(t) : null;
|
|
745
763
|
el.style.setProperty("--bg-page", t.bgPage);
|
|
746
764
|
el.style.setProperty("--bg-surface", t.bgSurface);
|
|
747
765
|
el.style.setProperty("--bg-surface-2", t.bgSurface2);
|
|
748
|
-
el.style.setProperty("--border", t.border);
|
|
766
|
+
el.style.setProperty("--border", step?.border ?? t.border);
|
|
749
767
|
el.style.setProperty("--text-primary", t.textPrimary);
|
|
750
|
-
el.style.setProperty("--text-secondary", t.textSecondary);
|
|
751
|
-
el.style.setProperty("--text-muted", t.textMuted);
|
|
768
|
+
el.style.setProperty("--text-secondary", step?.textSecondary ?? t.textSecondary);
|
|
769
|
+
el.style.setProperty("--text-muted", step?.textMuted ?? t.textMuted);
|
|
770
|
+
if (step) el.style.setProperty("--text-placeholder", step.textPlaceholder);
|
|
771
|
+
else el.style.removeProperty("--text-placeholder");
|
|
752
772
|
el.style.setProperty("--brand", t.brand);
|
|
753
773
|
el.style.setProperty("--brand-hover", t.brandHover);
|
|
754
774
|
el.style.setProperty("--brand-contrast", t.brandContrast);
|
|
@@ -60,7 +60,12 @@ export function createPaletteStore(
|
|
|
60
60
|
/** Apply the active preset's tokens to <html> as inline CSS custom properties,
|
|
61
61
|
* re-applying when the preset or the light/dark theme changes. Tailwind v4
|
|
62
62
|
* strips a bare root token block from the bundle, so this inline write from
|
|
63
|
-
* the TS source of truth is what actually drives the colours.
|
|
63
|
+
* the TS source of truth is what actually drives the colours.
|
|
64
|
+
*
|
|
65
|
+
* 0.32: keeps More contrast. `applyTokenSet` reads `<html data-contrast>`, which the
|
|
66
|
+
* contrast store sets, and steps the tokens itself; the contrast store re-applies
|
|
67
|
+
* the set written here when the contrast changes (docs/text-size-harmonization.md
|
|
68
|
+
* §10.5). Neither store needs the other. */
|
|
64
69
|
function useApplyPalette(): void {
|
|
65
70
|
const id = usePalette((s) => s.id);
|
|
66
71
|
const mode = useTheme((s) => s.mode);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import { create } from "zustand";
|
|
3
|
+
import { persist } from "zustand/middleware";
|
|
4
|
+
import { logger } from "../lib/logger";
|
|
5
|
+
import { readStored } from "../lib/safe-storage";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The text size (docs/text-size-harmonization.md §2.1, §3.1, §10.3, §10.6).
|
|
9
|
+
*
|
|
10
|
+
* ONE scale on `<html>`: `data-text-size="normal|large|xlarge"`, and `tokens.css` turns
|
|
11
|
+
* it into `font-size: 100% / 125% / 150%` — percent, never px, so "Normal" keeps
|
|
12
|
+
* whatever size the person set in the browser. Tailwind's spacing and type are rem, so
|
|
13
|
+
* about 90 % of every screen grows with that one declaration; the breakpoints follow too
|
|
14
|
+
* (tokens.css redefines `sm:` … `3xl:` per size, {@link useBreakpoint} answers the same
|
|
15
|
+
* in JS).
|
|
16
|
+
*
|
|
17
|
+
* The names below are the mechanism's public vocabulary and are FROZEN: an app's
|
|
18
|
+
* storage, its `/auth/me` column (server-kit's `TEXT_SIZES`) and the inline boot snippet
|
|
19
|
+
* all spell them this way.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The three steps, in order (§2.1, §10.6): server-kit's `TEXT_SIZES`, value for value. */
|
|
23
|
+
export const TEXT_SIZES = ["normal", "large", "xlarge"] as const;
|
|
24
|
+
|
|
25
|
+
export type TextSize = (typeof TEXT_SIZES)[number];
|
|
26
|
+
|
|
27
|
+
/** The root font size each step sets, as a factor of the browser's own (tokens.css). For
|
|
28
|
+
* what CSS cannot reach: a canvas or SVG chart's px constants, a windowed list's row
|
|
29
|
+
* estimate, a popover width in px (§3.2). */
|
|
30
|
+
export const TEXT_SCALE: Readonly<Record<TextSize, number>> = { normal: 1, large: 1.25, xlarge: 1.5 };
|
|
31
|
+
|
|
32
|
+
/** The attribute on `<html>` that carries the size. */
|
|
33
|
+
const TEXT_SIZE_ATTRIBUTE = "data-text-size";
|
|
34
|
+
|
|
35
|
+
export function isTextSize(value: unknown): value is TextSize {
|
|
36
|
+
return value === "normal" || value === "large" || value === "xlarge";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The size in force: **this device's own choice → the account's → Normal** (§2.2, §2.6).
|
|
41
|
+
* Each is taken only when it is one of {@link TEXT_SIZES}; anything else (a stale value,
|
|
42
|
+
* `null` = never chosen) falls through to the next. The one order the pre-paint call, the
|
|
43
|
+
* apply hook and `useAccountAppearance` all use, so the three can never disagree.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveTextSize(device: unknown, account?: unknown): TextSize {
|
|
46
|
+
if (isTextSize(device)) return device;
|
|
47
|
+
if (isTextSize(account)) return account;
|
|
48
|
+
return "normal";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ── <html data-text-size> and the subscribers that read it ─────────────── */
|
|
52
|
+
|
|
53
|
+
const listeners = new Set<() => void>();
|
|
54
|
+
let observer: MutationObserver | null = null;
|
|
55
|
+
|
|
56
|
+
function notify(): void {
|
|
57
|
+
for (const listener of listeners) listener();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function subscribe(listener: () => void): () => void {
|
|
61
|
+
listeners.add(listener);
|
|
62
|
+
// Also catches writers that are not this module — the inline boot snippet, an app
|
|
63
|
+
// setting the attribute by hand, a test.
|
|
64
|
+
if (!observer && typeof MutationObserver !== "undefined" && typeof document !== "undefined") {
|
|
65
|
+
observer = new MutationObserver(notify);
|
|
66
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: [TEXT_SIZE_ATTRIBUTE] });
|
|
67
|
+
}
|
|
68
|
+
return () => {
|
|
69
|
+
listeners.delete(listener);
|
|
70
|
+
if (listeners.size === 0) {
|
|
71
|
+
observer?.disconnect();
|
|
72
|
+
observer = null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The size `<html>` carries now; Normal without one (or outside a browser). */
|
|
78
|
+
export function readTextSize(): TextSize {
|
|
79
|
+
if (typeof document === "undefined") return "normal";
|
|
80
|
+
const value = document.documentElement.getAttribute(TEXT_SIZE_ATTRIBUTE);
|
|
81
|
+
return isTextSize(value) ? value : "normal";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Put `size` on `<html>`. Every {@link useTextSize} re-renders with it. */
|
|
85
|
+
export function applyTextSize(size: TextSize): void {
|
|
86
|
+
if (typeof document === "undefined") return;
|
|
87
|
+
const root = document.documentElement;
|
|
88
|
+
if (root.getAttribute(TEXT_SIZE_ATTRIBUTE) === size) return;
|
|
89
|
+
root.setAttribute(TEXT_SIZE_ATTRIBUTE, size);
|
|
90
|
+
notify();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TextSizeInfo {
|
|
94
|
+
size: TextSize;
|
|
95
|
+
/** {@link TEXT_SCALE} for `size`: 1, 1.25 or 1.5. */
|
|
96
|
+
scale: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The size in force on this page, and its scale (§3.1) — read from `<html>`, so it needs
|
|
101
|
+
* no store and works in every kit component: whoever applied the size (the app's store,
|
|
102
|
+
* the pre-paint call, the inline snippet), this follows it.
|
|
103
|
+
*
|
|
104
|
+
* Only for what CSS cannot do. A class that should differ at Large is a `large:`
|
|
105
|
+
* variant; a layout switch is a breakpoint, which already scales.
|
|
106
|
+
*/
|
|
107
|
+
export function useTextSize(): TextSizeInfo {
|
|
108
|
+
const size = useSyncExternalStore(subscribe, readTextSize, () => "normal" as const);
|
|
109
|
+
return useMemo(() => ({ size, scale: TEXT_SCALE[size] }), [size]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Storage: the frozen format, the pre-paint call ─────────────────────── */
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What a text-size store persists under its key — zustand's persist envelope, and FROZEN
|
|
116
|
+
* (§10.3) because an app's inline `<head>` snippet reads it before any module loads:
|
|
117
|
+
*
|
|
118
|
+
* {"state":{"size":"large"},"version":1}
|
|
119
|
+
*
|
|
120
|
+
* `size` is one of {@link TEXT_SIZES}, or `null` / absent when this device never chose.
|
|
121
|
+
* A future change adds a key; it never renames `state.size` or changes its values.
|
|
122
|
+
*/
|
|
123
|
+
export interface PersistedTextSize {
|
|
124
|
+
state?: { size?: TextSize | null };
|
|
125
|
+
version?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** This device's own choice under `storageKey`, or `null` when it never chose (or the
|
|
129
|
+
* storage is blocked, malformed, or holds a value that is not a size). */
|
|
130
|
+
export function readPersistedTextSize(storageKey: string): TextSize | null {
|
|
131
|
+
try {
|
|
132
|
+
const blob = JSON.parse(readStored(storageKey) ?? "null") as PersistedTextSize | null;
|
|
133
|
+
const size = blob?.state?.size;
|
|
134
|
+
return isTextSize(size) ? size : null;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface PersistedAppearanceOptions {
|
|
141
|
+
/**
|
|
142
|
+
* The account's last known value, for a device without a choice of its own (§10.3):
|
|
143
|
+
* what the app persisted with its user (keksdose keeps `/auth/me` in localStorage), so
|
|
144
|
+
* a cold boot paints the account's size instead of Normal and then jumping — offline
|
|
145
|
+
* too. Called once, guarded: a throw counts as "no account value".
|
|
146
|
+
*/
|
|
147
|
+
account?: () => string | null | undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readAccount(account: PersistedAppearanceOptions["account"]): unknown {
|
|
151
|
+
if (!account) return undefined;
|
|
152
|
+
try {
|
|
153
|
+
return account();
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Pre-paint (§3.1, §10.3): read this device's choice under `storageKey` — else the
|
|
161
|
+
* account's last known value — and put it on `<html>` BEFORE `createRoot`, next to
|
|
162
|
+
* `applyPersistedTheme`, so the first paint already has the right size. Returns the size
|
|
163
|
+
* applied.
|
|
164
|
+
*
|
|
165
|
+
* ```ts
|
|
166
|
+
* applyPersistedTextSize("keksdose-text-size", {
|
|
167
|
+
* account: () => readPersistedUser()?.text_size,
|
|
168
|
+
* });
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export function applyPersistedTextSize(storageKey: string, options: PersistedAppearanceOptions = {}): TextSize {
|
|
172
|
+
const size = resolveTextSize(readPersistedTextSize(storageKey), readAccount(options.account));
|
|
173
|
+
applyTextSize(size);
|
|
174
|
+
return size;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The same pre-paint as {@link applyPersistedTextSize}, as an inline `<head>` script for
|
|
179
|
+
* a boot splash painted from `index.html` before any module runs (§10.3). A function
|
|
180
|
+
* EXPRESSION: call it with the storage key and, optionally, a function returning the
|
|
181
|
+
* account's last known value.
|
|
182
|
+
*
|
|
183
|
+
* ```html
|
|
184
|
+
* <script>
|
|
185
|
+
* (function(k,a){…})("keksdose-text-size", function () {
|
|
186
|
+
* return JSON.parse(localStorage.getItem("keksdose-auth")).state.user.text_size;
|
|
187
|
+
* });
|
|
188
|
+
* </script>
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* Paste the constant's value in place of `(function(k,a){…})`. It reads the frozen
|
|
192
|
+
* {@link PersistedTextSize} format, never throws, and sets only `data-text-size`; the
|
|
193
|
+
* splash itself sizes in rem so the attribute reaches it (tokens.css, or the splash's own
|
|
194
|
+
* `html[data-text-size=large]{font-size:125%}` if it paints before the stylesheet).
|
|
195
|
+
*/
|
|
196
|
+
export const TEXT_SIZE_INLINE_SCRIPT =
|
|
197
|
+
"(function(k,a){function ok(v){return v==='normal'||v==='large'||v==='xlarge'}" +
|
|
198
|
+
"var v=null;try{var s=JSON.parse(localStorage.getItem(k)||'null');v=s&&s.state&&s.state.size}catch(e){}" +
|
|
199
|
+
"if(!ok(v)){v=null;try{v=a?a():null}catch(e){}}" +
|
|
200
|
+
"try{document.documentElement.setAttribute('data-text-size',ok(v)?v:'normal')}catch(e){}})";
|
|
201
|
+
|
|
202
|
+
/* ── The store ──────────────────────────────────────────────────────────── */
|
|
203
|
+
|
|
204
|
+
export interface TextSizeState {
|
|
205
|
+
/** This device's own choice (persisted), or `null` when it never chose — then the
|
|
206
|
+
* account's value applies (§2.2). */
|
|
207
|
+
size: TextSize | null;
|
|
208
|
+
/** Store this device's choice; `null` forgets it, so the account's applies again. */
|
|
209
|
+
setSize: (size: TextSize | null) => void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ApplyTextSizeOptions {
|
|
213
|
+
/** The account's value (`/auth/me`'s `text_size`): it applies while this device has no
|
|
214
|
+
* choice of its own. Leave it out where there is no account. */
|
|
215
|
+
account?: string | null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build a text-size store bound to a consumer-supplied localStorage key, shaped like
|
|
220
|
+
* `createThemeStore` (§6). It holds the DEVICE's choice only; the account's value comes
|
|
221
|
+
* in through `useApplyTextSize({ account })` and `useAccountAppearance`, and the size in
|
|
222
|
+
* force is always {@link resolveTextSize} of the two.
|
|
223
|
+
*
|
|
224
|
+
* - `useTextSizeStore` — the zustand hook: `size` (device choice or null) and `setSize`.
|
|
225
|
+
* - `useApplyTextSize({ account })` — the effect that puts the size in force on `<html>`.
|
|
226
|
+
* Mount it once near the root, beside `useApplyTheme`.
|
|
227
|
+
*
|
|
228
|
+
* The persisted format is {@link PersistedTextSize}, and frozen.
|
|
229
|
+
*/
|
|
230
|
+
export function createTextSizeStore(storageKey: string) {
|
|
231
|
+
const useTextSizeStore = create<TextSizeState>()(
|
|
232
|
+
logger(
|
|
233
|
+
persist(
|
|
234
|
+
(set) => ({
|
|
235
|
+
size: null,
|
|
236
|
+
setSize: (size) => set({ size: isTextSize(size) ? size : null }),
|
|
237
|
+
}),
|
|
238
|
+
{
|
|
239
|
+
name: storageKey,
|
|
240
|
+
version: 1,
|
|
241
|
+
partialize: (s) => ({ size: s.size }),
|
|
242
|
+
// A hand-edited or future value must not reach <html> as an unknown size.
|
|
243
|
+
merge: (persisted, current) => ({
|
|
244
|
+
...current,
|
|
245
|
+
size: isTextSize((persisted as { size?: unknown } | null)?.size)
|
|
246
|
+
? ((persisted as { size: TextSize }).size)
|
|
247
|
+
: null,
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
),
|
|
251
|
+
"text-size",
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
function useApplyTextSize({ account }: ApplyTextSizeOptions = {}): TextSize {
|
|
256
|
+
const device = useTextSizeStore((s) => s.size);
|
|
257
|
+
const size = resolveTextSize(device, account);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
applyTextSize(size);
|
|
260
|
+
}, [size]);
|
|
261
|
+
return size;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { useTextSizeStore, useApplyTextSize };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type TextSizeStore = ReturnType<typeof createTextSizeStore>;
|