@eifi1/ui-kit 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -37
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +107 -3
- package/dist/components/button-group.js +60 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +17 -4
- package/dist/components/calendar-heatmap.d.ts +162 -0
- package/dist/components/calendar-heatmap.js +336 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +66 -2
- package/dist/components/choice-card.js +108 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +52 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +104 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +69 -0
- package/dist/components/field.js +75 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +51 -208
- package/dist/components/file-dropzone.d.ts +50 -2
- package/dist/components/floating-panel.d.ts +156 -4
- package/dist/components/floating-panel.js +193 -29
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +37 -3
- package/dist/components/list.js +39 -14
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +13 -2
- package/dist/components/menu-item.js +11 -2
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +82 -5
- package/dist/components/mini-calendar.js +212 -64
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +67 -0
- package/dist/components/nav-pills.js +69 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +26 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +27 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +17 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +73 -3
- package/dist/components/progress-bar.js +110 -8
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +50 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +77 -11
- package/dist/components/table.js +85 -17
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +50 -1
- package/dist/components/toggle-group.d.ts +28 -0
- package/dist/components/toggle-group.js +58 -9
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +52 -729
- package/dist/components/ui.js +296 -27
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +50 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +17 -4
- package/dist/i18n/defaults.js +33 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +47 -310
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
- package/dist/i18n/locales/de-CH.d.ts +17 -4
- package/dist/i18n/locales/de-informal.d.ts +17 -4
- package/dist/i18n/locales/de.d.ts +17 -4
- package/dist/i18n/locales/de.js +165 -4
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +17 -4
- package/dist/i18n/locales/es.js +162 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -4
- package/dist/i18n/locales/fr.js +162 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -4
- package/dist/i18n/locales/hu.js +162 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -4
- package/dist/i18n/locales/it.js +162 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -4
- package/dist/i18n/locales/zh.js +162 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +50 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +63 -1
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +84 -3
- package/dist/shell/topbar-action-menu.js +115 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +58 -2
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -1
- package/dist/wizard/wizard-context.d.ts +30 -1
- package/dist/wizard/wizard-context.js +23 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +51 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +129 -4
- package/src/components/calendar-heatmap.tsx +599 -0
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +208 -1
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +231 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +188 -0
- package/src/components/floating-panel.tsx +380 -16
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +93 -18
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +29 -3
- package/src/components/mini-calendar.tsx +317 -57
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +152 -0
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +241 -13
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/series-chart.tsx +21 -0
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +186 -23
- package/src/components/text-link.tsx +248 -0
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +106 -13
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +572 -44
- package/src/components/use-table-state.ts +265 -33
- package/src/components/user-avatar.tsx +57 -3
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +32 -0
- package/src/i18n/kit-labels.tsx +104 -8
- package/src/i18n/locales/de.ts +162 -0
- package/src/i18n/locales/es.ts +159 -0
- package/src/i18n/locales/fr.ts +162 -0
- package/src/i18n/locales/hu.ts +160 -0
- package/src/i18n/locales/it.ts +160 -0
- package/src/i18n/locales/zh.ts +159 -0
- package/src/index.ts +64 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +8 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +260 -49
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +50 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -3,6 +3,8 @@ import type { MouseEvent, ReactElement, ReactNode, Ref } from "react";
|
|
|
3
3
|
import { Check } from "lucide-react";
|
|
4
4
|
import type { LucideIcon } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
|
+
import { useKitLink } from "../i18n/kit-labels";
|
|
7
|
+
import { pickLinkRenderer } from "./text-link";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does
|
|
@@ -43,7 +45,17 @@ interface MenuItemBaseProps {
|
|
|
43
45
|
icon?: LucideIcon;
|
|
44
46
|
/** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */
|
|
45
47
|
leading?: ReactNode;
|
|
46
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Right after the label text, before the gap that pushes {@link trailing} to the
|
|
50
|
+
* row's end: keksdose's budget switcher tags a budget "Guest" or "Shared" with a
|
|
51
|
+
* `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end
|
|
52
|
+
* where a count or a check mark sits. Hand-placed inside `children` it was truncated
|
|
53
|
+
* with the name — a long budget name ate the chip first. Here the label truncates
|
|
54
|
+
* and the badge never shrinks, so it stays visible beside what is left of the name.
|
|
55
|
+
* It is part of the row's accessible name ("Household Shared"), so keep it text.
|
|
56
|
+
*/
|
|
57
|
+
badge?: ReactNode;
|
|
58
|
+
/** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */
|
|
47
59
|
trailing?: ReactNode;
|
|
48
60
|
/** `danger` for the row that destroys or signs out. Colour is not the whole message:
|
|
49
61
|
* the label must still say what the row does. */
|
|
@@ -92,7 +104,8 @@ export type MenuItemProps = MenuItemBaseProps &
|
|
|
92
104
|
/** Makes the row a link — keksdose's "Manage budgets" and account-menu rows. */
|
|
93
105
|
href: string;
|
|
94
106
|
/** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads
|
|
95
|
-
* a single-page app. Default `<
|
|
107
|
+
* a single-page app. Default: the `<UiKitProvider linkComponent>`, then `<a>`.
|
|
108
|
+
* The same API as `Chip` and `StatTile`. */
|
|
96
109
|
renderLink?: (props: MenuItemLinkProps) => ReactElement;
|
|
97
110
|
/** Runs on the way (close the menu). */
|
|
98
111
|
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
@@ -123,6 +136,7 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
|
|
|
123
136
|
children,
|
|
124
137
|
icon: Icon,
|
|
125
138
|
leading,
|
|
139
|
+
badge,
|
|
126
140
|
trailing,
|
|
127
141
|
tone = "default",
|
|
128
142
|
disabled = false,
|
|
@@ -137,6 +151,7 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
|
|
|
137
151
|
},
|
|
138
152
|
ref,
|
|
139
153
|
) {
|
|
154
|
+
const kitLink = useKitLink();
|
|
140
155
|
const on = href !== undefined ? !!current : !!checked;
|
|
141
156
|
const look = cn(
|
|
142
157
|
MENU_ITEM_ROW,
|
|
@@ -151,6 +166,16 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
|
|
|
151
166
|
{Icon && <Icon className="size-4 shrink-0" aria-hidden />}
|
|
152
167
|
{leading}
|
|
153
168
|
<span className="min-w-0 truncate">{children}</span>
|
|
169
|
+
{badge != null && badge !== false && (
|
|
170
|
+
<>
|
|
171
|
+
{/* A space between the name and the badge in the accessible name ("Household
|
|
172
|
+
Shared", not "HouseholdShared"); whitespace between flex items is not
|
|
173
|
+
laid out, so it costs nothing on screen. */}{" "}
|
|
174
|
+
<span data-menu-item-badge="" className="flex shrink-0 items-center">
|
|
175
|
+
{badge}
|
|
176
|
+
</span>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
154
179
|
</span>
|
|
155
180
|
{(trailing != null || on) && (
|
|
156
181
|
<span className="flex shrink-0 items-center gap-2">
|
|
@@ -172,7 +197,8 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
|
|
|
172
197
|
"aria-current": current ? "page" : undefined,
|
|
173
198
|
children: body,
|
|
174
199
|
};
|
|
175
|
-
|
|
200
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
201
|
+
if (render) return <RenderedLink render={render} {...linkProps} />;
|
|
176
202
|
const { children: linkBody, ...anchorProps } = linkProps;
|
|
177
203
|
return <a {...anchorProps}>{linkBody}</a>;
|
|
178
204
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef, KeyboardEvent } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { horizontalStep } from "../lib/direction";
|
|
6
6
|
import { useAnnounce } from "../hooks/use-announce";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
addMonthsClamped,
|
|
9
|
+
localeWeekStart,
|
|
10
|
+
monthKey,
|
|
11
|
+
parseIsoDate,
|
|
12
|
+
sameYmd,
|
|
13
|
+
toLocalIso,
|
|
14
|
+
} from "../lib/dates";
|
|
8
15
|
import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels";
|
|
9
16
|
|
|
10
17
|
/**
|
|
@@ -80,8 +87,10 @@ const DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {
|
|
|
80
87
|
* before this, a closed prop list dropped both (audit §api-design).
|
|
81
88
|
*/
|
|
82
89
|
export interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSelect"> {
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
/** The selection's start as "YYYY-MM-DD"; `""` (the default) for none. */
|
|
91
|
+
from?: string;
|
|
92
|
+
/** The selection's end; `""` (the default) for none. */
|
|
93
|
+
to?: string;
|
|
85
94
|
/**
|
|
86
95
|
* BCP 47 tag for the month caption, the weekday heads, the day numbers and the
|
|
87
96
|
* spoken dates. Optional since the kit grew `<UiKitProvider locale>`: a calendar
|
|
@@ -122,47 +131,104 @@ export interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
122
131
|
* merely by existing.
|
|
123
132
|
*/
|
|
124
133
|
focusOnOpen?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* `"sm"` (default) is the compact picker: 32px round days, the size the date
|
|
136
|
+
* pickers and the data-table filter draw. `"lg"` is a MONTH VIEW for a page — tall
|
|
137
|
+
* cells in a ruled grid, the day number in the top corner and {@link renderDay}'s
|
|
138
|
+
* content under it — the calendar a planner page hand-built before this existed.
|
|
139
|
+
* Both are the same grid underneath: the same roles, roving tabindex, keys, RTL
|
|
140
|
+
* mirroring and week start.
|
|
141
|
+
*/
|
|
142
|
+
size?: "sm" | "lg";
|
|
143
|
+
/**
|
|
144
|
+
* Custom content for one day: events, an amount, a dot. Called for every day of the
|
|
145
|
+
* month on show; return `null` for none.
|
|
146
|
+
*
|
|
147
|
+
* ⚠️ **It renders INSIDE the day's button** — a dot under the number at `"sm"`, a
|
|
148
|
+
* column under it at `"lg"` — so it must not be interactive: no links, no buttons,
|
|
149
|
+
* no inputs. A control inside a control is invalid HTML, and a screen reader lands
|
|
150
|
+
* on the day and never reaches what is inside it. Events that are themselves
|
|
151
|
+
* clickable are shown here as plain text, and the DAY is what is clicked: pass
|
|
152
|
+
* `mode="single"` and let `onSelect` open a day panel (a drawer, a popover) that
|
|
153
|
+
* lists that day's events as real links.
|
|
154
|
+
*
|
|
155
|
+
* The day's accessible name stays the full date; this content is attached to it as
|
|
156
|
+
* its DESCRIPTION (`aria-describedby`), so "Monday, 14 September 2026" is read
|
|
157
|
+
* first and "Rent due · Plumber" after it. Anything decorative (a coloured dot)
|
|
158
|
+
* should be `aria-hidden` or carry an `sr-only` word for what it means.
|
|
159
|
+
*/
|
|
160
|
+
renderDay?: (day: Date, state: MiniCalendarDayState) => ReactNode;
|
|
161
|
+
/**
|
|
162
|
+
* The month on show, as "YYYY-MM". FOLLOWED rather than controlled, exactly like
|
|
163
|
+
* `from`/`to`: when it changes the grid moves there, and in between the user pages
|
|
164
|
+
* freely — every move to another month is reported through {@link onMonthChange}.
|
|
165
|
+
* For a month page whose own header (a month picker, a Today button) drives the
|
|
166
|
+
* calendar. Left out, the grid opens on the selection or today.
|
|
167
|
+
*/
|
|
168
|
+
month?: string;
|
|
169
|
+
/** Called with "YYYY-MM" whenever the USER moves the grid to another month (the
|
|
170
|
+
* arrows, PageUp/PageDown, arrowing off the edge). */
|
|
171
|
+
onMonthChange?: (month: string) => void;
|
|
172
|
+
/**
|
|
173
|
+
* Hide the built-in previous/next arrows and month caption, for a page that draws
|
|
174
|
+
* its own. The caption is kept for assistive technology (visually hidden), because
|
|
175
|
+
* it is what names the grid.
|
|
176
|
+
*/
|
|
177
|
+
hideNavigation?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Fill the leading and trailing cells with the neighbouring months' days, muted —
|
|
180
|
+
* the wall-calendar look, where the first row does not start on a hole.
|
|
181
|
+
*
|
|
182
|
+
* They are real days, not decoration: each is named with its whole date, honours
|
|
183
|
+
* `min`/`max`, and a click selects it and moves the grid to its month (focus goes
|
|
184
|
+
* with it). They are NOT stops of the roving tabindex — the keyboard already reaches
|
|
185
|
+
* them by arrowing off the month's edge, which moves the grid the same way — and
|
|
186
|
+
* `renderDay` is not called for them, so a day's events are drawn once, in its own
|
|
187
|
+
* month. Default `false`: the cells stay empty.
|
|
188
|
+
*/
|
|
189
|
+
showOutsideDays?: boolean;
|
|
125
190
|
/** Extra classes for the calendar's root. */
|
|
126
191
|
className?: string;
|
|
127
192
|
}
|
|
128
193
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (typeof firstDay === "number") return (firstDay % 7) as WeekDay;
|
|
148
|
-
} catch {
|
|
149
|
-
// A malformed tag throws from `Intl.Locale`; the calendar still has to render.
|
|
150
|
-
}
|
|
151
|
-
return 1;
|
|
194
|
+
/** What {@link MiniCalendarProps.renderDay} is told about the day it draws. */
|
|
195
|
+
export interface MiniCalendarDayState {
|
|
196
|
+
/** The day as "YYYY-MM-DD". */
|
|
197
|
+
iso: string;
|
|
198
|
+
/** The day number as drawn, in the calendar's locale. */
|
|
199
|
+
label: string;
|
|
200
|
+
today: boolean;
|
|
201
|
+
/** Outside `min`/`max`. */
|
|
202
|
+
disabled: boolean;
|
|
203
|
+
/** Part of the selection — an end or a day between. */
|
|
204
|
+
selected: boolean;
|
|
205
|
+
rangeStart: boolean;
|
|
206
|
+
rangeEnd: boolean;
|
|
207
|
+
/** Strictly between the two ends of a range. */
|
|
208
|
+
inRange: boolean;
|
|
209
|
+
/** The day that holds the tab stop. */
|
|
210
|
+
active: boolean;
|
|
211
|
+
size: "sm" | "lg";
|
|
152
212
|
}
|
|
153
213
|
|
|
214
|
+
export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
215
|
+
|
|
154
216
|
/** Where `d` sits in a week that starts on `weekStart` (the first column = 0). */
|
|
155
217
|
const weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;
|
|
156
218
|
|
|
157
|
-
/** Move `d` by whole months
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
/** Move `d` by whole months without rolling over (see {@link addMonthsClamped}). */
|
|
220
|
+
const stepMonths = addMonthsClamped;
|
|
221
|
+
|
|
222
|
+
/** A month-view cell: tall enough for two or three event lines, shorter on a phone. */
|
|
223
|
+
const LG_CELL_HEIGHT = "min-h-16 md:min-h-24";
|
|
224
|
+
|
|
225
|
+
/** The 1st of a "YYYY-MM" month, or null when absent or malformed. */
|
|
226
|
+
const monthStart = (key: string | undefined) => (key ? parseIsoDate(`${key}-01`) : null);
|
|
227
|
+
|
|
228
|
+
/** `day`'s day of the month in `first`'s month, clamped to that month's length. */
|
|
229
|
+
function sameDayIn(first: Date, day: Date): Date {
|
|
230
|
+
const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
|
|
231
|
+
return new Date(first.getFullYear(), first.getMonth(), Math.min(day.getDate(), last));
|
|
166
232
|
}
|
|
167
233
|
|
|
168
234
|
/**
|
|
@@ -187,8 +253,8 @@ function stepMonths(d: Date, months: number): Date {
|
|
|
187
253
|
* readable, and the click handler is what refuses them.
|
|
188
254
|
*/
|
|
189
255
|
export function MiniCalendar({
|
|
190
|
-
from,
|
|
191
|
-
to,
|
|
256
|
+
from = "",
|
|
257
|
+
to = "",
|
|
192
258
|
locale: localeProp,
|
|
193
259
|
weekStartsOn,
|
|
194
260
|
onSelect,
|
|
@@ -197,9 +263,16 @@ export function MiniCalendar({
|
|
|
197
263
|
max,
|
|
198
264
|
labels: labelsProp,
|
|
199
265
|
focusOnOpen,
|
|
266
|
+
size = "sm",
|
|
267
|
+
renderDay,
|
|
268
|
+
month,
|
|
269
|
+
onMonthChange,
|
|
270
|
+
hideNavigation,
|
|
271
|
+
showOutsideDays = false,
|
|
200
272
|
className,
|
|
201
273
|
...rest
|
|
202
274
|
}: MiniCalendarProps) {
|
|
275
|
+
const lg = size === "lg";
|
|
203
276
|
const labels = useKitLabels("miniCalendar", DEFAULT_MINI_CALENDAR_LABELS, labelsProp);
|
|
204
277
|
const locale = useKitLocale(localeProp);
|
|
205
278
|
const providerWeekStart = useKitWeekStart();
|
|
@@ -232,7 +305,11 @@ export function MiniCalendar({
|
|
|
232
305
|
* so a bounded calendar opens on a day it will actually let you pick.
|
|
233
306
|
*/
|
|
234
307
|
const [activeIso, setActiveIso] = useState(() => {
|
|
235
|
-
|
|
308
|
+
let start = fromDate ?? toDate ?? new Date();
|
|
309
|
+
// A `month` wins over the selection's month: the page asked for THAT month. The
|
|
310
|
+
// selection (or today) is kept when it falls inside it, else the 1st.
|
|
311
|
+
const first = monthStart(month);
|
|
312
|
+
if (first && monthKey(start) !== month) start = first;
|
|
236
313
|
if (minDate && start < minDate) return toLocalIso(minDate);
|
|
237
314
|
if (maxDate && start > maxDate) return toLocalIso(maxDate);
|
|
238
315
|
return toLocalIso(start);
|
|
@@ -253,17 +330,37 @@ export function MiniCalendar({
|
|
|
253
330
|
if (next && toLocalIso(next) !== activeIso) setActiveIso(toLocalIso(next));
|
|
254
331
|
}
|
|
255
332
|
|
|
333
|
+
// …and the caller's month, the same way. Only a CHANGE moves the grid, so paging
|
|
334
|
+
// with the arrows is not snapped back by a parent that ignores `onMonthChange`.
|
|
335
|
+
const [lastMonth, setLastMonth] = useState(month);
|
|
336
|
+
if (lastMonth !== month) {
|
|
337
|
+
setLastMonth(month);
|
|
338
|
+
const first = monthStart(month);
|
|
339
|
+
const current = parseIsoDate(activeIso) ?? new Date();
|
|
340
|
+
if (first && monthKey(current) !== month) {
|
|
341
|
+
// Keep the day of the month, clamped: the 31st in a 30-day month is the 30th.
|
|
342
|
+
setActiveIso(toLocalIso(sameDayIn(first, current)));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
256
346
|
const activeDate = parseIsoDate(activeIso) ?? new Date();
|
|
257
347
|
const view = { year: activeDate.getFullYear(), month: activeDate.getMonth() };
|
|
258
348
|
|
|
259
349
|
const firstOfMonth = new Date(view.year, view.month, 1);
|
|
260
350
|
const lastOfMonth = new Date(view.year, view.month + 1, 0);
|
|
261
|
-
// Leading blanks up to the first of the month, counted from the week's first day
|
|
351
|
+
// Leading blanks up to the first of the month, counted from the week's first day —
|
|
352
|
+
// or the previous month's last days, with `showOutsideDays` (and the next month's
|
|
353
|
+
// first ones at the end). `new Date(y, m, 0)` is the day before the 1st, so the
|
|
354
|
+
// offsets below roll into the neighbouring month by themselves.
|
|
262
355
|
const startWeekday = weekdayIndex(firstOfMonth, weekStart);
|
|
263
356
|
const cells: (Date | null)[] = [];
|
|
264
|
-
for (let i = 0; i < startWeekday; i++)
|
|
357
|
+
for (let i = 0; i < startWeekday; i++) {
|
|
358
|
+
cells.push(showOutsideDays ? new Date(view.year, view.month, i - startWeekday + 1) : null);
|
|
359
|
+
}
|
|
265
360
|
for (let d = 1; d <= lastOfMonth.getDate(); d++) cells.push(new Date(view.year, view.month, d));
|
|
266
|
-
|
|
361
|
+
for (let extra = 1; cells.length % 7 !== 0; extra++) {
|
|
362
|
+
cells.push(showOutsideDays ? new Date(view.year, view.month + 1, extra) : null);
|
|
363
|
+
}
|
|
267
364
|
const weeks: (Date | null)[][] = [];
|
|
268
365
|
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
|
|
269
366
|
|
|
@@ -281,6 +378,7 @@ export function MiniCalendar({
|
|
|
281
378
|
d.setDate(ref.getDate() + i);
|
|
282
379
|
return {
|
|
283
380
|
narrow: d.toLocaleDateString(locale, { weekday: "narrow" }),
|
|
381
|
+
short: d.toLocaleDateString(locale, { weekday: "short" }),
|
|
284
382
|
long: d.toLocaleDateString(locale, { weekday: "long" }),
|
|
285
383
|
};
|
|
286
384
|
});
|
|
@@ -312,6 +410,8 @@ export function MiniCalendar({
|
|
|
312
410
|
if (iso === activeIso) return;
|
|
313
411
|
if (focus) pendingFocus.current = iso;
|
|
314
412
|
setActiveIso(iso);
|
|
413
|
+
const key = monthKey(d);
|
|
414
|
+
if (key !== monthKey(activeDate)) onMonthChange?.(key);
|
|
315
415
|
};
|
|
316
416
|
|
|
317
417
|
/**
|
|
@@ -414,7 +514,13 @@ export function MiniCalendar({
|
|
|
414
514
|
// `className` does everywhere else in the kit; `...rest` first, as in the rest of
|
|
415
515
|
// this wave, so nothing from outside can take the grid's own wiring away.
|
|
416
516
|
<div {...rest} className={cn("select-none", className)}>
|
|
417
|
-
|
|
517
|
+
{hideNavigation ? (
|
|
518
|
+
// Still in the tree: the caption is what names the grid.
|
|
519
|
+
<div id={monthId} className="sr-only-fixed">
|
|
520
|
+
{monthLabel}
|
|
521
|
+
</div>
|
|
522
|
+
) : (
|
|
523
|
+
<div className={cn("flex items-center justify-between", lg ? "pb-2" : "pb-1")}>
|
|
418
524
|
<button
|
|
419
525
|
type="button"
|
|
420
526
|
onClick={() => moveMonth(-1)}
|
|
@@ -431,7 +537,12 @@ export function MiniCalendar({
|
|
|
431
537
|
<div
|
|
432
538
|
id={monthId}
|
|
433
539
|
aria-live="polite"
|
|
434
|
-
className=
|
|
540
|
+
className={cn(
|
|
541
|
+
"capitalize",
|
|
542
|
+
lg
|
|
543
|
+
? "text-base font-semibold text-[var(--text-primary)]"
|
|
544
|
+
: "text-xs font-medium text-[var(--text-secondary)]",
|
|
545
|
+
)}
|
|
435
546
|
>
|
|
436
547
|
{monthLabel}
|
|
437
548
|
</div>
|
|
@@ -444,44 +555,136 @@ export function MiniCalendar({
|
|
|
444
555
|
<ChevronRight className="size-4 rtl:-scale-x-100" aria-hidden />
|
|
445
556
|
</button>
|
|
446
557
|
</div>
|
|
558
|
+
)}
|
|
447
559
|
<div
|
|
448
560
|
ref={gridRef}
|
|
449
561
|
role="grid"
|
|
450
562
|
aria-labelledby={monthId}
|
|
451
563
|
aria-describedby={mode === "range" ? hintId : undefined}
|
|
452
|
-
className=
|
|
564
|
+
className={cn(
|
|
565
|
+
"grid",
|
|
566
|
+
// The month view is RULED: a 1px gap over a border-coloured ground draws the
|
|
567
|
+
// lines between the cells, the way a wall calendar does.
|
|
568
|
+
lg
|
|
569
|
+
? "gap-px overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--border)]"
|
|
570
|
+
: "gap-y-0.5",
|
|
571
|
+
)}
|
|
453
572
|
>
|
|
454
573
|
<div
|
|
455
574
|
role="row"
|
|
456
|
-
className=
|
|
575
|
+
className={cn(
|
|
576
|
+
"grid grid-cols-7 text-center font-medium",
|
|
577
|
+
lg
|
|
578
|
+
? "gap-px text-xs text-[var(--text-muted)]"
|
|
579
|
+
: "text-[10px] uppercase text-[var(--text-placeholder)]",
|
|
580
|
+
)}
|
|
457
581
|
>
|
|
458
582
|
{weekdayLabels.map((w, i) => (
|
|
459
|
-
<div
|
|
460
|
-
{
|
|
583
|
+
<div
|
|
584
|
+
key={i}
|
|
585
|
+
role="columnheader"
|
|
586
|
+
aria-label={w.long}
|
|
587
|
+
className={lg ? "bg-[var(--bg-surface)] py-2" : "py-0.5"}
|
|
588
|
+
>
|
|
589
|
+
{lg ? w.short : w.narrow}
|
|
461
590
|
</div>
|
|
462
591
|
))}
|
|
463
592
|
</div>
|
|
464
593
|
{weeks.map((week, wi) => (
|
|
465
594
|
// No column gap: the days of a range join into ONE band. With a gap each day
|
|
466
595
|
// was its own tinted box, and a selected fortnight read as fourteen chips.
|
|
467
|
-
<div key={wi} role="row" className="grid grid-cols-7">
|
|
596
|
+
<div key={wi} role="row" className={cn("grid grid-cols-7", lg && "gap-px")}>
|
|
468
597
|
{week.map((d, di) => {
|
|
469
598
|
// The padding cells still have to BE cells, or the rows are ragged and
|
|
470
599
|
// a screen reader's "column 3" stops meaning Wednesday.
|
|
471
|
-
if (!d)
|
|
600
|
+
if (!d) {
|
|
601
|
+
return (
|
|
602
|
+
<div
|
|
603
|
+
key={di}
|
|
604
|
+
role="gridcell"
|
|
605
|
+
className={lg ? cn(LG_CELL_HEIGHT, "bg-[var(--bg-surface-2)]") : "h-8"}
|
|
606
|
+
/>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
472
609
|
const iso = toLocalIso(d);
|
|
473
610
|
const disabled = isDisabled(d);
|
|
611
|
+
// A neighbouring month's day (`showOutsideDays`): a muted, selectable
|
|
612
|
+
// cell outside the tab order — see the prop.
|
|
613
|
+
if (d.getMonth() !== view.month) {
|
|
614
|
+
return (
|
|
615
|
+
<div
|
|
616
|
+
key={di}
|
|
617
|
+
role="none"
|
|
618
|
+
className={cn(
|
|
619
|
+
"relative flex",
|
|
620
|
+
lg ? cn("min-w-0", LG_CELL_HEIGHT) : "h-8 items-center justify-center",
|
|
621
|
+
)}
|
|
622
|
+
>
|
|
623
|
+
<button
|
|
624
|
+
type="button"
|
|
625
|
+
role="gridcell"
|
|
626
|
+
data-iso={iso}
|
|
627
|
+
data-outside=""
|
|
628
|
+
tabIndex={-1}
|
|
629
|
+
aria-label={labels.day(formatDay(d))}
|
|
630
|
+
aria-selected={!disabled && inRange(d)}
|
|
631
|
+
aria-disabled={disabled || undefined}
|
|
632
|
+
onClick={() => {
|
|
633
|
+
if (disabled) return;
|
|
634
|
+
// Focus follows into the month the click moves the grid to:
|
|
635
|
+
// this button is gone after the next commit.
|
|
636
|
+
pendingFocus.current = iso;
|
|
637
|
+
handleClick(d);
|
|
638
|
+
}}
|
|
639
|
+
onKeyDown={onDayKeyDown}
|
|
640
|
+
className={cn(
|
|
641
|
+
"text-xs tabular-nums text-[var(--text-placeholder)] transition-colors",
|
|
642
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]",
|
|
643
|
+
disabled ? "cursor-not-allowed opacity-60" : "hover:bg-[var(--bg-hover)]",
|
|
644
|
+
lg
|
|
645
|
+
? cn("flex w-full min-w-0 items-start bg-[var(--bg-surface-2)] p-1.5", LG_CELL_HEIGHT)
|
|
646
|
+
: "size-8 rounded-full",
|
|
647
|
+
)}
|
|
648
|
+
>
|
|
649
|
+
{lg ? (
|
|
650
|
+
<span className="inline-flex size-6 items-center justify-center">
|
|
651
|
+
{dayNumber.format(d.getDate())}
|
|
652
|
+
</span>
|
|
653
|
+
) : (
|
|
654
|
+
dayNumber.format(d.getDate())
|
|
655
|
+
)}
|
|
656
|
+
</button>
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
474
660
|
const isStart = !disabled && fromDate && sameYmd(d, fromDate);
|
|
475
661
|
const isEnd = !disabled && toDate && sameYmd(d, toDate);
|
|
476
662
|
const isToday = sameYmd(d, today);
|
|
477
663
|
const isInRange = !disabled && inRange(d) && !isStart && !isEnd;
|
|
478
664
|
const isActive = iso === activeIso;
|
|
665
|
+
const isSelected = Boolean(isStart || isEnd || isInRange);
|
|
666
|
+
const label = dayNumber.format(d.getDate());
|
|
667
|
+
const content = renderDay?.(d, {
|
|
668
|
+
iso,
|
|
669
|
+
label,
|
|
670
|
+
today: isToday,
|
|
671
|
+
disabled,
|
|
672
|
+
selected: isSelected,
|
|
673
|
+
rangeStart: Boolean(isStart),
|
|
674
|
+
rangeEnd: Boolean(isEnd),
|
|
675
|
+
inRange: isInRange,
|
|
676
|
+
active: isActive,
|
|
677
|
+
size,
|
|
678
|
+
});
|
|
679
|
+
const hasContent = content != null && content !== false && content !== "";
|
|
680
|
+
const contentId = `${id}-day-${iso}`;
|
|
479
681
|
// The band behind the day: full width between the ends, half width at an
|
|
480
682
|
// end (so the round endpoint sits ON the band's tip), rounded where a row
|
|
481
683
|
// or the month breaks it. Only when both ends exist and differ — a lone
|
|
482
|
-
// start is a dot, not a band of one.
|
|
684
|
+
// start is a dot, not a band of one. The month view tints whole cells
|
|
685
|
+
// instead, so it has no band.
|
|
483
686
|
const banded =
|
|
484
|
-
hasBand && !disabled && (isStart || isEnd || isInRange);
|
|
687
|
+
!lg && hasBand && !disabled && (isStart || isEnd || isInRange);
|
|
485
688
|
const lastOfMonth = d.getDate() === daysInMonth(d);
|
|
486
689
|
const band = banded
|
|
487
690
|
? cn(
|
|
@@ -494,7 +697,14 @@ export function MiniCalendar({
|
|
|
494
697
|
)
|
|
495
698
|
: null;
|
|
496
699
|
return (
|
|
497
|
-
<div
|
|
700
|
+
<div
|
|
701
|
+
key={di}
|
|
702
|
+
role="none"
|
|
703
|
+
className={cn(
|
|
704
|
+
"relative flex",
|
|
705
|
+
lg ? "min-w-0" : "h-8 items-center justify-center",
|
|
706
|
+
)}
|
|
707
|
+
>
|
|
498
708
|
{band && <span aria-hidden className={band} />}
|
|
499
709
|
<button
|
|
500
710
|
key={di}
|
|
@@ -506,12 +716,28 @@ export function MiniCalendar({
|
|
|
506
716
|
// control, which is the same reason the pattern exists.
|
|
507
717
|
tabIndex={isActive ? 0 : -1}
|
|
508
718
|
aria-label={labels.day(formatDay(d))}
|
|
509
|
-
|
|
719
|
+
// The custom content is the day's DESCRIPTION: `aria-label` replaces
|
|
720
|
+
// the button's text as its name, so without this link a screen
|
|
721
|
+
// reader would hear the date and never the events drawn under it.
|
|
722
|
+
aria-describedby={hasContent ? contentId : undefined}
|
|
723
|
+
aria-selected={isSelected}
|
|
510
724
|
aria-disabled={disabled || undefined}
|
|
511
725
|
aria-current={isToday ? "date" : undefined}
|
|
512
726
|
onClick={() => handleClick(d)}
|
|
513
727
|
onKeyDown={onDayKeyDown}
|
|
514
|
-
className={
|
|
728
|
+
className={
|
|
729
|
+
lg
|
|
730
|
+
? cn(
|
|
731
|
+
"flex w-full min-w-0 flex-col items-stretch gap-1 overflow-hidden p-1.5 text-start text-xs transition-colors",
|
|
732
|
+
LG_CELL_HEIGHT,
|
|
733
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]",
|
|
734
|
+
disabled
|
|
735
|
+
? "cursor-not-allowed bg-[var(--bg-surface)] text-[var(--text-placeholder)]"
|
|
736
|
+
: isSelected
|
|
737
|
+
? "bg-[var(--brand-bg)] text-[var(--text-primary)] hover:bg-[var(--brand-bg-hover)]"
|
|
738
|
+
: "bg-[var(--bg-surface)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
739
|
+
)
|
|
740
|
+
: cn(
|
|
515
741
|
"relative size-8 rounded-full text-xs tabular-nums transition-colors",
|
|
516
742
|
// A grid you can walk with the keyboard needs to show where the
|
|
517
743
|
// keyboard IS. Inset because the cells sit half a pixel apart and
|
|
@@ -537,9 +763,43 @@ export function MiniCalendar({
|
|
|
537
763
|
? "font-medium text-[var(--brand-muted)] hover:bg-[var(--brand-bg-hover)]"
|
|
538
764
|
: "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
539
765
|
isToday && !isStart && !isEnd && !isInRange && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
|
|
540
|
-
)
|
|
766
|
+
)
|
|
767
|
+
}
|
|
541
768
|
>
|
|
542
|
-
{
|
|
769
|
+
{lg ? (
|
|
770
|
+
<>
|
|
771
|
+
<span
|
|
772
|
+
className={cn(
|
|
773
|
+
"inline-flex size-6 shrink-0 items-center justify-center self-start rounded-full tabular-nums",
|
|
774
|
+
isStart || isEnd
|
|
775
|
+
? "bg-[var(--brand)] font-semibold text-[var(--brand-contrast)]"
|
|
776
|
+
: isToday && !disabled
|
|
777
|
+
? "font-semibold text-[var(--brand)] ring-1 ring-inset ring-[var(--brand)]"
|
|
778
|
+
: isInRange && "font-medium text-[var(--brand-muted)]",
|
|
779
|
+
)}
|
|
780
|
+
>
|
|
781
|
+
{label}
|
|
782
|
+
</span>
|
|
783
|
+
{hasContent && (
|
|
784
|
+
<span id={contentId} className="flex min-w-0 flex-col gap-0.5">
|
|
785
|
+
{content}
|
|
786
|
+
</span>
|
|
787
|
+
)}
|
|
788
|
+
</>
|
|
789
|
+
) : (
|
|
790
|
+
<>
|
|
791
|
+
{label}
|
|
792
|
+
{/* Under the number, inside the circle: room for a dot or two. */}
|
|
793
|
+
{hasContent && (
|
|
794
|
+
<span
|
|
795
|
+
id={contentId}
|
|
796
|
+
className="pointer-events-none absolute inset-x-0 bottom-0.5 flex justify-center gap-px leading-none"
|
|
797
|
+
>
|
|
798
|
+
{content}
|
|
799
|
+
</span>
|
|
800
|
+
)}
|
|
801
|
+
</>
|
|
802
|
+
)}
|
|
543
803
|
</button>
|
|
544
804
|
</div>
|
|
545
805
|
);
|