@eifi1/ui-kit 0.9.0 → 0.11.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 +37 -23
- package/dist/components/alert-banner.d.ts +57 -5
- package/dist/components/alert-banner.js +63 -17
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +6 -0
- package/dist/components/breadcrumbs.d.ts +60 -0
- package/dist/components/breadcrumbs.js +88 -0
- package/dist/components/breadcrumbs.js.map +1 -0
- package/dist/components/bulk-action-bar.d.ts +112 -0
- package/dist/components/bulk-action-bar.js +196 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +6 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +31 -5
- package/dist/components/chip.js +85 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +14 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +22 -2
- package/dist/components/date-picker.js +11 -3
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +43 -6
- package/dist/components/description-list.js +91 -11
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +7 -0
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +57 -2
- package/dist/components/disclosure.js +24 -12
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +4 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +23 -4
- package/dist/components/full-bleed-dialog.js +11 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/list.d.ts +187 -0
- package/dist/components/list.js +221 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +109 -0
- package/dist/components/menu-item.js +88 -0
- package/dist/components/menu-item.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +23 -1
- package/dist/components/modal.js +36 -10
- package/dist/components/modal.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +6 -0
- package/dist/components/number-input.d.ts +6 -0
- package/dist/components/numpad-sheet.d.ts +6 -0
- package/dist/components/page-contents.js +1 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.d.ts +37 -0
- package/dist/components/page-header.js +36 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +3 -0
- package/dist/components/series-chart-labels.js +2 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +32 -0
- package/dist/components/series-chart.js +141 -4
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +53 -0
- package/dist/components/status-dot.js +57 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +78 -13
- package/dist/components/table.js +84 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +70 -0
- package/dist/components/text.js +24 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toast.d.ts +167 -0
- package/dist/components/toast.js +226 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle-group.d.ts +44 -1
- package/dist/components/toggle-group.js +73 -4
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +46 -15
- package/dist/components/tooltip.js +87 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +158 -7
- package/dist/components/ui.js +191 -10
- package/dist/components/ui.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/hooks/use-close-transition.d.ts +7 -1
- package/dist/hooks/use-close-transition.js +5 -2
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.js +1 -1
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +6 -0
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +11 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
- package/dist/i18n/locales/de-CH.d.ts +6 -0
- package/dist/i18n/locales/de-informal.d.ts +6 -0
- package/dist/i18n/locales/de.d.ts +6 -0
- package/dist/i18n/locales/de.js +30 -2
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +6 -0
- package/dist/i18n/locales/es.js +30 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +6 -0
- package/dist/i18n/locales/fr.js +30 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +6 -0
- package/dist/i18n/locales/hu.js +30 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +6 -0
- package/dist/i18n/locales/it.js +30 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +6 -0
- package/dist/i18n/locales/zh.js +30 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +27 -15
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +25 -0
- package/dist/lib/clipping.js +17 -0
- package/dist/lib/clipping.js.map +1 -0
- 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/rhf/form.d.ts +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 +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.d.ts +19 -2
- package/dist/search/command-palette.js +16 -5
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +26 -1
- package/dist/search/global-search.js +29 -13
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/stepper-nav.js +1 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +2 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +1 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +16 -12
- package/src/components/alert-banner.tsx +149 -21
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +362 -0
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/chip.tsx +128 -7
- package/src/components/choice-card.tsx +97 -1
- package/src/components/copy-button.tsx +29 -7
- package/src/components/data-table.tsx +7 -0
- package/src/components/date-picker.tsx +49 -1
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +93 -21
- package/src/components/field.tsx +137 -0
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +443 -0
- package/src/components/menu-item.tsx +235 -0
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/modal.tsx +80 -17
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +274 -3
- package/src/components/status-dot.tsx +107 -0
- package/src/components/table.tsx +176 -17
- package/src/components/text.tsx +97 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +128 -4
- package/src/components/tooltip.tsx +200 -100
- package/src/components/ui.tsx +381 -14
- package/src/components/user-avatar.tsx +57 -3
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/kit-labels.tsx +10 -0
- package/src/i18n/locales/de.ts +29 -0
- package/src/i18n/locales/es.ts +29 -0
- package/src/i18n/locales/fr.ts +29 -0
- package/src/i18n/locales/hu.ts +29 -0
- package/src/i18n/locales/it.ts +29 -0
- package/src/i18n/locales/zh.ts +29 -0
- package/src/index.ts +31 -0
- package/src/lib/clipping.ts +34 -0
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/search/command-palette.tsx +42 -5
- package/src/search/global-search.tsx +59 -18
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/stepper-nav.tsx +2 -1
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-context.tsx +9 -0
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -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,92 @@ 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;
|
|
125
178
|
/** Extra classes for the calendar's root. */
|
|
126
179
|
className?: string;
|
|
127
180
|
}
|
|
128
181
|
|
|
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;
|
|
182
|
+
/** What {@link MiniCalendarProps.renderDay} is told about the day it draws. */
|
|
183
|
+
export interface MiniCalendarDayState {
|
|
184
|
+
/** The day as "YYYY-MM-DD". */
|
|
185
|
+
iso: string;
|
|
186
|
+
/** The day number as drawn, in the calendar's locale. */
|
|
187
|
+
label: string;
|
|
188
|
+
today: boolean;
|
|
189
|
+
/** Outside `min`/`max`. */
|
|
190
|
+
disabled: boolean;
|
|
191
|
+
/** Part of the selection — an end or a day between. */
|
|
192
|
+
selected: boolean;
|
|
193
|
+
rangeStart: boolean;
|
|
194
|
+
rangeEnd: boolean;
|
|
195
|
+
/** Strictly between the two ends of a range. */
|
|
196
|
+
inRange: boolean;
|
|
197
|
+
/** The day that holds the tab stop. */
|
|
198
|
+
active: boolean;
|
|
199
|
+
size: "sm" | "lg";
|
|
152
200
|
}
|
|
153
201
|
|
|
202
|
+
export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
203
|
+
|
|
154
204
|
/** Where `d` sits in a week that starts on `weekStart` (the first column = 0). */
|
|
155
205
|
const weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;
|
|
156
206
|
|
|
157
|
-
/** Move `d` by whole months
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
207
|
+
/** Move `d` by whole months without rolling over (see {@link addMonthsClamped}). */
|
|
208
|
+
const stepMonths = addMonthsClamped;
|
|
209
|
+
|
|
210
|
+
/** A month-view cell: tall enough for two or three event lines, shorter on a phone. */
|
|
211
|
+
const LG_CELL_HEIGHT = "min-h-16 md:min-h-24";
|
|
212
|
+
|
|
213
|
+
/** The 1st of a "YYYY-MM" month, or null when absent or malformed. */
|
|
214
|
+
const monthStart = (key: string | undefined) => (key ? parseIsoDate(`${key}-01`) : null);
|
|
215
|
+
|
|
216
|
+
/** `day`'s day of the month in `first`'s month, clamped to that month's length. */
|
|
217
|
+
function sameDayIn(first: Date, day: Date): Date {
|
|
218
|
+
const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
|
|
219
|
+
return new Date(first.getFullYear(), first.getMonth(), Math.min(day.getDate(), last));
|
|
166
220
|
}
|
|
167
221
|
|
|
168
222
|
/**
|
|
@@ -187,8 +241,8 @@ function stepMonths(d: Date, months: number): Date {
|
|
|
187
241
|
* readable, and the click handler is what refuses them.
|
|
188
242
|
*/
|
|
189
243
|
export function MiniCalendar({
|
|
190
|
-
from,
|
|
191
|
-
to,
|
|
244
|
+
from = "",
|
|
245
|
+
to = "",
|
|
192
246
|
locale: localeProp,
|
|
193
247
|
weekStartsOn,
|
|
194
248
|
onSelect,
|
|
@@ -197,9 +251,15 @@ export function MiniCalendar({
|
|
|
197
251
|
max,
|
|
198
252
|
labels: labelsProp,
|
|
199
253
|
focusOnOpen,
|
|
254
|
+
size = "sm",
|
|
255
|
+
renderDay,
|
|
256
|
+
month,
|
|
257
|
+
onMonthChange,
|
|
258
|
+
hideNavigation,
|
|
200
259
|
className,
|
|
201
260
|
...rest
|
|
202
261
|
}: MiniCalendarProps) {
|
|
262
|
+
const lg = size === "lg";
|
|
203
263
|
const labels = useKitLabels("miniCalendar", DEFAULT_MINI_CALENDAR_LABELS, labelsProp);
|
|
204
264
|
const locale = useKitLocale(localeProp);
|
|
205
265
|
const providerWeekStart = useKitWeekStart();
|
|
@@ -232,7 +292,11 @@ export function MiniCalendar({
|
|
|
232
292
|
* so a bounded calendar opens on a day it will actually let you pick.
|
|
233
293
|
*/
|
|
234
294
|
const [activeIso, setActiveIso] = useState(() => {
|
|
235
|
-
|
|
295
|
+
let start = fromDate ?? toDate ?? new Date();
|
|
296
|
+
// A `month` wins over the selection's month: the page asked for THAT month. The
|
|
297
|
+
// selection (or today) is kept when it falls inside it, else the 1st.
|
|
298
|
+
const first = monthStart(month);
|
|
299
|
+
if (first && monthKey(start) !== month) start = first;
|
|
236
300
|
if (minDate && start < minDate) return toLocalIso(minDate);
|
|
237
301
|
if (maxDate && start > maxDate) return toLocalIso(maxDate);
|
|
238
302
|
return toLocalIso(start);
|
|
@@ -253,6 +317,19 @@ export function MiniCalendar({
|
|
|
253
317
|
if (next && toLocalIso(next) !== activeIso) setActiveIso(toLocalIso(next));
|
|
254
318
|
}
|
|
255
319
|
|
|
320
|
+
// …and the caller's month, the same way. Only a CHANGE moves the grid, so paging
|
|
321
|
+
// with the arrows is not snapped back by a parent that ignores `onMonthChange`.
|
|
322
|
+
const [lastMonth, setLastMonth] = useState(month);
|
|
323
|
+
if (lastMonth !== month) {
|
|
324
|
+
setLastMonth(month);
|
|
325
|
+
const first = monthStart(month);
|
|
326
|
+
const current = parseIsoDate(activeIso) ?? new Date();
|
|
327
|
+
if (first && monthKey(current) !== month) {
|
|
328
|
+
// Keep the day of the month, clamped: the 31st in a 30-day month is the 30th.
|
|
329
|
+
setActiveIso(toLocalIso(sameDayIn(first, current)));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
256
333
|
const activeDate = parseIsoDate(activeIso) ?? new Date();
|
|
257
334
|
const view = { year: activeDate.getFullYear(), month: activeDate.getMonth() };
|
|
258
335
|
|
|
@@ -281,6 +358,7 @@ export function MiniCalendar({
|
|
|
281
358
|
d.setDate(ref.getDate() + i);
|
|
282
359
|
return {
|
|
283
360
|
narrow: d.toLocaleDateString(locale, { weekday: "narrow" }),
|
|
361
|
+
short: d.toLocaleDateString(locale, { weekday: "short" }),
|
|
284
362
|
long: d.toLocaleDateString(locale, { weekday: "long" }),
|
|
285
363
|
};
|
|
286
364
|
});
|
|
@@ -312,6 +390,8 @@ export function MiniCalendar({
|
|
|
312
390
|
if (iso === activeIso) return;
|
|
313
391
|
if (focus) pendingFocus.current = iso;
|
|
314
392
|
setActiveIso(iso);
|
|
393
|
+
const key = monthKey(d);
|
|
394
|
+
if (key !== monthKey(activeDate)) onMonthChange?.(key);
|
|
315
395
|
};
|
|
316
396
|
|
|
317
397
|
/**
|
|
@@ -414,7 +494,13 @@ export function MiniCalendar({
|
|
|
414
494
|
// `className` does everywhere else in the kit; `...rest` first, as in the rest of
|
|
415
495
|
// this wave, so nothing from outside can take the grid's own wiring away.
|
|
416
496
|
<div {...rest} className={cn("select-none", className)}>
|
|
417
|
-
|
|
497
|
+
{hideNavigation ? (
|
|
498
|
+
// Still in the tree: the caption is what names the grid.
|
|
499
|
+
<div id={monthId} className="sr-only-fixed">
|
|
500
|
+
{monthLabel}
|
|
501
|
+
</div>
|
|
502
|
+
) : (
|
|
503
|
+
<div className={cn("flex items-center justify-between", lg ? "pb-2" : "pb-1")}>
|
|
418
504
|
<button
|
|
419
505
|
type="button"
|
|
420
506
|
onClick={() => moveMonth(-1)}
|
|
@@ -431,7 +517,12 @@ export function MiniCalendar({
|
|
|
431
517
|
<div
|
|
432
518
|
id={monthId}
|
|
433
519
|
aria-live="polite"
|
|
434
|
-
className=
|
|
520
|
+
className={cn(
|
|
521
|
+
"capitalize",
|
|
522
|
+
lg
|
|
523
|
+
? "text-base font-semibold text-[var(--text-primary)]"
|
|
524
|
+
: "text-xs font-medium text-[var(--text-secondary)]",
|
|
525
|
+
)}
|
|
435
526
|
>
|
|
436
527
|
{monthLabel}
|
|
437
528
|
</div>
|
|
@@ -444,31 +535,57 @@ export function MiniCalendar({
|
|
|
444
535
|
<ChevronRight className="size-4 rtl:-scale-x-100" aria-hidden />
|
|
445
536
|
</button>
|
|
446
537
|
</div>
|
|
538
|
+
)}
|
|
447
539
|
<div
|
|
448
540
|
ref={gridRef}
|
|
449
541
|
role="grid"
|
|
450
542
|
aria-labelledby={monthId}
|
|
451
543
|
aria-describedby={mode === "range" ? hintId : undefined}
|
|
452
|
-
className=
|
|
544
|
+
className={cn(
|
|
545
|
+
"grid",
|
|
546
|
+
// The month view is RULED: a 1px gap over a border-coloured ground draws the
|
|
547
|
+
// lines between the cells, the way a wall calendar does.
|
|
548
|
+
lg
|
|
549
|
+
? "gap-px overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--border)]"
|
|
550
|
+
: "gap-y-0.5",
|
|
551
|
+
)}
|
|
453
552
|
>
|
|
454
553
|
<div
|
|
455
554
|
role="row"
|
|
456
|
-
className=
|
|
555
|
+
className={cn(
|
|
556
|
+
"grid grid-cols-7 text-center font-medium",
|
|
557
|
+
lg
|
|
558
|
+
? "gap-px text-xs text-[var(--text-muted)]"
|
|
559
|
+
: "text-[10px] uppercase text-[var(--text-placeholder)]",
|
|
560
|
+
)}
|
|
457
561
|
>
|
|
458
562
|
{weekdayLabels.map((w, i) => (
|
|
459
|
-
<div
|
|
460
|
-
{
|
|
563
|
+
<div
|
|
564
|
+
key={i}
|
|
565
|
+
role="columnheader"
|
|
566
|
+
aria-label={w.long}
|
|
567
|
+
className={lg ? "bg-[var(--bg-surface)] py-2" : "py-0.5"}
|
|
568
|
+
>
|
|
569
|
+
{lg ? w.short : w.narrow}
|
|
461
570
|
</div>
|
|
462
571
|
))}
|
|
463
572
|
</div>
|
|
464
573
|
{weeks.map((week, wi) => (
|
|
465
574
|
// No column gap: the days of a range join into ONE band. With a gap each day
|
|
466
575
|
// was its own tinted box, and a selected fortnight read as fourteen chips.
|
|
467
|
-
<div key={wi} role="row" className="grid grid-cols-7">
|
|
576
|
+
<div key={wi} role="row" className={cn("grid grid-cols-7", lg && "gap-px")}>
|
|
468
577
|
{week.map((d, di) => {
|
|
469
578
|
// The padding cells still have to BE cells, or the rows are ragged and
|
|
470
579
|
// a screen reader's "column 3" stops meaning Wednesday.
|
|
471
|
-
if (!d)
|
|
580
|
+
if (!d) {
|
|
581
|
+
return (
|
|
582
|
+
<div
|
|
583
|
+
key={di}
|
|
584
|
+
role="gridcell"
|
|
585
|
+
className={lg ? cn(LG_CELL_HEIGHT, "bg-[var(--bg-surface-2)]") : "h-8"}
|
|
586
|
+
/>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
472
589
|
const iso = toLocalIso(d);
|
|
473
590
|
const disabled = isDisabled(d);
|
|
474
591
|
const isStart = !disabled && fromDate && sameYmd(d, fromDate);
|
|
@@ -476,12 +593,29 @@ export function MiniCalendar({
|
|
|
476
593
|
const isToday = sameYmd(d, today);
|
|
477
594
|
const isInRange = !disabled && inRange(d) && !isStart && !isEnd;
|
|
478
595
|
const isActive = iso === activeIso;
|
|
596
|
+
const isSelected = Boolean(isStart || isEnd || isInRange);
|
|
597
|
+
const label = dayNumber.format(d.getDate());
|
|
598
|
+
const content = renderDay?.(d, {
|
|
599
|
+
iso,
|
|
600
|
+
label,
|
|
601
|
+
today: isToday,
|
|
602
|
+
disabled,
|
|
603
|
+
selected: isSelected,
|
|
604
|
+
rangeStart: Boolean(isStart),
|
|
605
|
+
rangeEnd: Boolean(isEnd),
|
|
606
|
+
inRange: isInRange,
|
|
607
|
+
active: isActive,
|
|
608
|
+
size,
|
|
609
|
+
});
|
|
610
|
+
const hasContent = content != null && content !== false && content !== "";
|
|
611
|
+
const contentId = `${id}-day-${iso}`;
|
|
479
612
|
// The band behind the day: full width between the ends, half width at an
|
|
480
613
|
// end (so the round endpoint sits ON the band's tip), rounded where a row
|
|
481
614
|
// or the month breaks it. Only when both ends exist and differ — a lone
|
|
482
|
-
// start is a dot, not a band of one.
|
|
615
|
+
// start is a dot, not a band of one. The month view tints whole cells
|
|
616
|
+
// instead, so it has no band.
|
|
483
617
|
const banded =
|
|
484
|
-
hasBand && !disabled && (isStart || isEnd || isInRange);
|
|
618
|
+
!lg && hasBand && !disabled && (isStart || isEnd || isInRange);
|
|
485
619
|
const lastOfMonth = d.getDate() === daysInMonth(d);
|
|
486
620
|
const band = banded
|
|
487
621
|
? cn(
|
|
@@ -494,7 +628,14 @@ export function MiniCalendar({
|
|
|
494
628
|
)
|
|
495
629
|
: null;
|
|
496
630
|
return (
|
|
497
|
-
<div
|
|
631
|
+
<div
|
|
632
|
+
key={di}
|
|
633
|
+
role="none"
|
|
634
|
+
className={cn(
|
|
635
|
+
"relative flex",
|
|
636
|
+
lg ? "min-w-0" : "h-8 items-center justify-center",
|
|
637
|
+
)}
|
|
638
|
+
>
|
|
498
639
|
{band && <span aria-hidden className={band} />}
|
|
499
640
|
<button
|
|
500
641
|
key={di}
|
|
@@ -506,12 +647,28 @@ export function MiniCalendar({
|
|
|
506
647
|
// control, which is the same reason the pattern exists.
|
|
507
648
|
tabIndex={isActive ? 0 : -1}
|
|
508
649
|
aria-label={labels.day(formatDay(d))}
|
|
509
|
-
|
|
650
|
+
// The custom content is the day's DESCRIPTION: `aria-label` replaces
|
|
651
|
+
// the button's text as its name, so without this link a screen
|
|
652
|
+
// reader would hear the date and never the events drawn under it.
|
|
653
|
+
aria-describedby={hasContent ? contentId : undefined}
|
|
654
|
+
aria-selected={isSelected}
|
|
510
655
|
aria-disabled={disabled || undefined}
|
|
511
656
|
aria-current={isToday ? "date" : undefined}
|
|
512
657
|
onClick={() => handleClick(d)}
|
|
513
658
|
onKeyDown={onDayKeyDown}
|
|
514
|
-
className={
|
|
659
|
+
className={
|
|
660
|
+
lg
|
|
661
|
+
? cn(
|
|
662
|
+
"flex w-full min-w-0 flex-col items-stretch gap-1 overflow-hidden p-1.5 text-start text-xs transition-colors",
|
|
663
|
+
LG_CELL_HEIGHT,
|
|
664
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]",
|
|
665
|
+
disabled
|
|
666
|
+
? "cursor-not-allowed bg-[var(--bg-surface)] text-[var(--text-placeholder)]"
|
|
667
|
+
: isSelected
|
|
668
|
+
? "bg-[var(--brand-bg)] text-[var(--text-primary)] hover:bg-[var(--brand-bg-hover)]"
|
|
669
|
+
: "bg-[var(--bg-surface)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
670
|
+
)
|
|
671
|
+
: cn(
|
|
515
672
|
"relative size-8 rounded-full text-xs tabular-nums transition-colors",
|
|
516
673
|
// A grid you can walk with the keyboard needs to show where the
|
|
517
674
|
// keyboard IS. Inset because the cells sit half a pixel apart and
|
|
@@ -537,9 +694,43 @@ export function MiniCalendar({
|
|
|
537
694
|
? "font-medium text-[var(--brand-muted)] hover:bg-[var(--brand-bg-hover)]"
|
|
538
695
|
: "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
539
696
|
isToday && !isStart && !isEnd && !isInRange && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
|
|
540
|
-
)
|
|
697
|
+
)
|
|
698
|
+
}
|
|
541
699
|
>
|
|
542
|
-
{
|
|
700
|
+
{lg ? (
|
|
701
|
+
<>
|
|
702
|
+
<span
|
|
703
|
+
className={cn(
|
|
704
|
+
"inline-flex size-6 shrink-0 items-center justify-center self-start rounded-full tabular-nums",
|
|
705
|
+
isStart || isEnd
|
|
706
|
+
? "bg-[var(--brand)] font-semibold text-[var(--brand-contrast)]"
|
|
707
|
+
: isToday && !disabled
|
|
708
|
+
? "font-semibold text-[var(--brand)] ring-1 ring-inset ring-[var(--brand)]"
|
|
709
|
+
: isInRange && "font-medium text-[var(--brand-muted)]",
|
|
710
|
+
)}
|
|
711
|
+
>
|
|
712
|
+
{label}
|
|
713
|
+
</span>
|
|
714
|
+
{hasContent && (
|
|
715
|
+
<span id={contentId} className="flex min-w-0 flex-col gap-0.5">
|
|
716
|
+
{content}
|
|
717
|
+
</span>
|
|
718
|
+
)}
|
|
719
|
+
</>
|
|
720
|
+
) : (
|
|
721
|
+
<>
|
|
722
|
+
{label}
|
|
723
|
+
{/* Under the number, inside the circle: room for a dot or two. */}
|
|
724
|
+
{hasContent && (
|
|
725
|
+
<span
|
|
726
|
+
id={contentId}
|
|
727
|
+
className="pointer-events-none absolute inset-x-0 bottom-0.5 flex justify-center gap-px leading-none"
|
|
728
|
+
>
|
|
729
|
+
{content}
|
|
730
|
+
</span>
|
|
731
|
+
)}
|
|
732
|
+
</>
|
|
733
|
+
)}
|
|
543
734
|
</button>
|
|
544
735
|
</div>
|
|
545
736
|
);
|
package/src/components/modal.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useRef, useState } from "react";
|
|
1
|
+
import { createContext, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import type {
|
|
4
4
|
ComponentPropsWithoutRef,
|
|
@@ -11,7 +11,7 @@ import { cn } from "../lib/cn";
|
|
|
11
11
|
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
12
12
|
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
|
|
13
13
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
14
|
-
import { useCloseTransition } from "../hooks/use-close-transition";
|
|
14
|
+
import { OVERLAY_EXIT_MS, prefersReducedMotion, useCloseTransition } from "../hooks/use-close-transition";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Drag the panel by its top strip, in pointer space (dev#460).
|
|
@@ -40,6 +40,9 @@ function useDragOffset(enabled: boolean) {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
|
+
/** Back to where the panel opens. A `Modal` with an `open` prop outlives its own
|
|
44
|
+
* closes, so "the position resets when the dialog closes" needs saying. */
|
|
45
|
+
reset: () => setOffset(null),
|
|
43
46
|
style: offset ? { transform: `translate(${offset.x}px, ${offset.y}px)` } : undefined,
|
|
44
47
|
handlers: !enabled
|
|
45
48
|
? undefined
|
|
@@ -120,6 +123,28 @@ export function useBackdropClose(onClose: () => void) {
|
|
|
120
123
|
export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"> {
|
|
121
124
|
/** Invoked on backdrop click and on Escape. */
|
|
122
125
|
onClose: () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Who decides whether the dialog is up, when the caller wants to keep it MOUNTED.
|
|
128
|
+
*
|
|
129
|
+
* Left out, the dialog is open for as long as it is mounted — `{open && <Modal/>}`,
|
|
130
|
+
* which is every caller before this prop and stays exactly as it was. That idiom has
|
|
131
|
+
* one blind spot: a close the CALLER decides on (a save that succeeded, a
|
|
132
|
+
* `setOpen(false)` from outside) unmounts the panel on the spot, with no exit,
|
|
133
|
+
* because there is nothing left to animate. kastlan's `FormModal` was nothing but an
|
|
134
|
+
* `if (!open) return null` gate in front of this.
|
|
135
|
+
*
|
|
136
|
+
* Passed, the component stays mounted while closed and renders nothing, and a flip
|
|
137
|
+
* to `false` plays the same exit a dismissal does before the panel goes. The focus
|
|
138
|
+
* trap, the scroll lock and the Back-gesture history entry are held only while
|
|
139
|
+
* `open` is true — they are released the moment it turns false, so focus is back on
|
|
140
|
+
* the trigger while the panel is still lowering. A dismissal (Escape, the backdrop,
|
|
141
|
+
* Back, the frame's X) animates first and then calls `onClose`; the `false` that
|
|
142
|
+
* comes back from it is not animated a second time.
|
|
143
|
+
*
|
|
144
|
+
* The body is unmounted once the exit has finished, as it is today, so a form inside
|
|
145
|
+
* still starts fresh on the next open.
|
|
146
|
+
*/
|
|
147
|
+
open?: boolean;
|
|
123
148
|
/** `"alertdialog"` for a dialog that interrupts to ask a question the user must
|
|
124
149
|
* answer (a confirmation) — screen readers announce it with more urgency. Only the
|
|
125
150
|
* two dialog roles are allowed: the panel stays modal either way. */
|
|
@@ -183,6 +208,7 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
|
|
|
183
208
|
*/
|
|
184
209
|
export function Modal({
|
|
185
210
|
onClose,
|
|
211
|
+
open,
|
|
186
212
|
children,
|
|
187
213
|
size = "md",
|
|
188
214
|
className,
|
|
@@ -196,37 +222,68 @@ export function Modal({
|
|
|
196
222
|
}: ModalProps) {
|
|
197
223
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
198
224
|
const drag = useDragOffset(Boolean(draggable));
|
|
225
|
+
// Without `open`, mounted means open — the constant `true` every hook below used to
|
|
226
|
+
// be given. With it, `open` itself.
|
|
227
|
+
const active = open ?? true;
|
|
228
|
+
|
|
229
|
+
// Whether the last close came through `requestClose`, i.e. has ALREADY been
|
|
230
|
+
// animated. Set in the same batch as the caller's `onClose`, so the render that sees
|
|
231
|
+
// `open` turn false also sees this and does not lower the panel a second time.
|
|
232
|
+
const [dismissed, setDismissed] = useState(false);
|
|
233
|
+
// The exit of a close the caller decided on (only reachable with `open`). Adjusted
|
|
234
|
+
// during render when `open` flips — `Collapse`'s pattern — so the very render that
|
|
235
|
+
// closes it already paints the `-out` classes rather than a frame of nothing.
|
|
236
|
+
const [exiting, setExiting] = useState(false);
|
|
237
|
+
const [prevOpen, setPrevOpen] = useState(open);
|
|
238
|
+
if (open !== prevOpen) {
|
|
239
|
+
setPrevOpen(open);
|
|
240
|
+
setDismissed(false);
|
|
241
|
+
setExiting(open === false && prevOpen === true && !dismissed && !prefersReducedMotion());
|
|
242
|
+
if (open) drag.reset();
|
|
243
|
+
}
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!exiting) return;
|
|
246
|
+
const timer = setTimeout(() => setExiting(false), OVERLAY_EXIT_MS);
|
|
247
|
+
return () => clearTimeout(timer);
|
|
248
|
+
}, [exiting]);
|
|
249
|
+
|
|
199
250
|
// The panel lowers itself before the caller unmounts it (live #320 rework). Every
|
|
200
251
|
// dismissal below goes through `requestClose`; `onClose` still does the closing, one
|
|
201
252
|
// animation later. A caller that closes the dialog ITSELF — after a save, say —
|
|
202
|
-
// unmounts with no exit
|
|
203
|
-
const { closing, requestClose } = useCloseTransition(
|
|
204
|
-
|
|
253
|
+
// unmounts with no exit unless it passes `open`, which is what that prop is for.
|
|
254
|
+
const { closing, requestClose } = useCloseTransition(() => {
|
|
255
|
+
setDismissed(true);
|
|
256
|
+
onClose();
|
|
257
|
+
});
|
|
258
|
+
// A panel on its way out after `open` turned false is already closed: a press on
|
|
259
|
+
// its fading backdrop must not report a second close.
|
|
260
|
+
const backdropClose = useBackdropClose(active ? requestClose : () => {});
|
|
205
261
|
// Back means the same as Escape here. On a phone Escape doesn't exist, so without
|
|
206
262
|
// this the only way out of a dialog is finding its close button — and Back, the
|
|
207
263
|
// gesture everyone reaches for, navigated the page underneath instead (#172).
|
|
208
|
-
//
|
|
209
|
-
useOverlayHistory(
|
|
264
|
+
// Held only while open: a closed `open={false}` dialog must not own a history entry.
|
|
265
|
+
useOverlayHistory(active, requestClose);
|
|
210
266
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
useBodyScrollLock(true);
|
|
267
|
+
// Through the shared hook and not by hand: this component's own save/restore copy
|
|
268
|
+
// was one half of the pair that left the page permanently unscrollable when a dialog
|
|
269
|
+
// containing an open sheet was closed — see the note in use-body-scroll-lock.ts.
|
|
270
|
+
useBodyScrollLock(active);
|
|
216
271
|
|
|
217
272
|
// Focus the panel itself (not the first field) so opening doesn't pop the mobile
|
|
218
273
|
// keyboard, while still moving focus into the dialog for keyboard and screen-reader
|
|
219
274
|
// users. Through the shared hook since this component is where it came from — the
|
|
220
275
|
// hook adds per-keystroke recomputation, a guard on restoring to a detached node,
|
|
221
|
-
// and nesting, none of which this copy had.
|
|
222
|
-
|
|
276
|
+
// and nesting, none of which this copy had. Released when `open` turns false, which
|
|
277
|
+
// hands focus back to the trigger while the panel is still on its way out.
|
|
278
|
+
useFocusTrap(panelRef, { active, initialFocus: "container" });
|
|
279
|
+
const leaving = closing || exiting;
|
|
223
280
|
|
|
224
281
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
225
282
|
onKeyDown?.(e);
|
|
226
283
|
if (e.defaultPrevented) return;
|
|
227
284
|
if (e.key === "Escape") {
|
|
228
285
|
e.stopPropagation();
|
|
229
|
-
requestClose();
|
|
286
|
+
if (active) requestClose();
|
|
230
287
|
return;
|
|
231
288
|
}
|
|
232
289
|
// Tab containment lives in useFocusTrap, which listens on the panel itself.
|
|
@@ -237,13 +294,19 @@ export function Modal({
|
|
|
237
294
|
// bar (a z-30 context) could paint BELOW a sibling z-30 sticky element like the
|
|
238
295
|
// expanded transaction editor (feedback #320). The tour/command-palette overlays
|
|
239
296
|
// already portal for the same reason.
|
|
297
|
+
if (!active && !exiting) return null;
|
|
240
298
|
return createPortal(
|
|
241
299
|
<div
|
|
300
|
+
// Out of the accessibility tree for the length of a caller-driven exit: focus is
|
|
301
|
+
// already back on the page, and an `aria-modal` panel that is leaving would hide
|
|
302
|
+
// that page from a screen reader for 220ms. Not `inert` — that would let a tap
|
|
303
|
+
// fall through the fading backdrop to the row underneath.
|
|
304
|
+
aria-hidden={exiting || undefined}
|
|
242
305
|
className={cn(
|
|
243
306
|
// Live #320. The panel below rises only where it is bottom-anchored: from md up
|
|
244
307
|
// it is centred, and a centred box sliding up from off-screen reads as a
|
|
245
308
|
// different component arriving rather than as the same one settling.
|
|
246
|
-
|
|
309
|
+
leaving ? "animate-overlay-out" : "animate-overlay",
|
|
247
310
|
"fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center",
|
|
248
311
|
fullBleed ? "p-0 md:p-4" : "p-4",
|
|
249
312
|
)}
|
|
@@ -270,7 +333,7 @@ export function Modal({
|
|
|
270
333
|
// the ones where the panel is being dragged.
|
|
271
334
|
style={{ ...style, ...drag.style }}
|
|
272
335
|
className={cn(
|
|
273
|
-
|
|
336
|
+
leaving ? "animate-sheet-out md:animate-none" : "animate-sheet md:animate-none",
|
|
274
337
|
"w-full rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm outline-none",
|
|
275
338
|
{ md: "max-w-md", lg: "max-w-lg", xl: "max-w-3xl" }[size],
|
|
276
339
|
// A panel taller than the screen has to scroll ITSELF. The backdrop is
|