@open-mercato/ui 0.8.1-develop.7296.1.2111d779db → 0.8.1-develop.7441.1.b42958b98c
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/.turbo/turbo-build.log +1 -1
- package/dist/backend/CrudForm.js +19 -6
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +61 -9
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/RowActions.js +54 -5
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/charts/KpiCard.js +2 -1
- package/dist/backend/charts/KpiCard.js.map +2 -2
- package/dist/backend/detail/AttachmentsSection.js +17 -5
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +38 -20
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/dev/DevRuntimeDiagnosticsBanner.js +11 -1
- package/dist/backend/dev/DevRuntimeDiagnosticsBanner.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +26 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/injection/eventBridge.js +33 -0
- package/dist/backend/injection/eventBridge.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +20 -3
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +1 -1
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +20 -2
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/messages/EmailThreadsPanel.js +2 -1
- package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
- package/dist/backend/messages/MessageComposer.js.map +2 -2
- package/dist/backend/messages/index.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +24 -0
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +27 -0
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/backend/messages/useMessageComposeOperations.js +1 -0
- package/dist/backend/messages/useMessageComposeOperations.js.map +2 -2
- package/dist/backend/notifications/NotificationItem.js +8 -1
- package/dist/backend/notifications/NotificationItem.js.map +2 -2
- package/dist/portal/PortalShell.js +2 -2
- package/dist/portal/PortalShell.js.map +2 -2
- package/dist/primitives/calendar.js +69 -14
- package/dist/primitives/calendar.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +30 -8
- package/src/backend/DataTable.tsx +92 -19
- package/src/backend/RowActions.tsx +114 -6
- package/src/backend/__tests__/AttachmentsSection.upload.test.tsx +183 -0
- package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +139 -2
- package/src/backend/__tests__/CrudForm.hiddenGroups.test.tsx +1 -1
- package/src/backend/__tests__/DataTable.extensions.test.tsx +30 -0
- package/src/backend/__tests__/DataTable.perspectiveReconcile.test.tsx +320 -2
- package/src/backend/__tests__/DevRuntimeDiagnosticsBanner.test.tsx +117 -0
- package/src/backend/__tests__/NotesSection.test.tsx +138 -0
- package/src/backend/__tests__/RowActions.test.tsx +202 -0
- package/src/backend/charts/KpiCard.tsx +1 -0
- package/src/backend/charts/__tests__/KpiCard.test.tsx +20 -0
- package/src/backend/detail/AttachmentsSection.tsx +17 -5
- package/src/backend/detail/NotesSection.tsx +122 -66
- package/src/backend/dev/DevRuntimeDiagnosticsBanner.tsx +24 -1
- package/src/backend/icons/lucideRegistry.generated.tsx +26 -0
- package/src/backend/injection/__tests__/eventBridge.test.tsx +47 -1
- package/src/backend/injection/eventBridge.ts +35 -0
- package/src/backend/inputs/LookupSelect.tsx +32 -3
- package/src/backend/inputs/PhoneNumberField.tsx +1 -1
- package/src/backend/inputs/TagsInput.tsx +24 -3
- package/src/backend/inputs/__tests__/LookupSelect.test.tsx +166 -1
- package/src/backend/inputs/__tests__/PhoneNumberField.typeahead.test.tsx +43 -0
- package/src/backend/inputs/__tests__/TagsInput.test.tsx +81 -0
- package/src/backend/messages/EmailThreadsPanel.tsx +21 -1
- package/src/backend/messages/MessageComposer.tsx +1 -0
- package/src/backend/messages/__tests__/EmailThreadsPanel.senderLabel.test.tsx +99 -0
- package/src/backend/messages/__tests__/MessageComposer.test.tsx +44 -0
- package/src/backend/messages/__tests__/useMessageCompose.test.tsx +69 -0
- package/src/backend/messages/__tests__/useMessageComposeSender.test.tsx +208 -0
- package/src/backend/messages/index.ts +1 -0
- package/src/backend/messages/message-compose-form-groups.tsx +41 -0
- package/src/backend/messages/message-composer.types.ts +26 -0
- package/src/backend/messages/useMessageCompose.ts +46 -0
- package/src/backend/messages/useMessageComposeOperations.ts +8 -0
- package/src/backend/notifications/NotificationItem.tsx +7 -0
- package/src/backend/notifications/__tests__/NotificationItem.customRenderer.test.tsx +139 -0
- package/src/portal/PortalShell.tsx +7 -4
- package/src/portal/__tests__/PortalShell.test.tsx +29 -1
- package/src/primitives/__tests__/calendar.test.tsx +174 -1
- package/src/primitives/calendar.tsx +168 -96
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/calendar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { DayPicker, useDayPicker } from 'react-day-picker'\nimport type { DayPickerProps, CalendarMonth } from 'react-day-picker'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { format } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CompactButton } from './compact-button'\n\nexport type CalendarProps = DayPickerProps & { daySize?: 36 | 40 }\n\nexport type CalendarMonthSelectorProps = React.HTMLAttributes<HTMLDivElement> & {\n month: Date\n locale?: Locale\n onPreviousMonth?: () => void\n onNextMonth?: () => void\n disabledPrevious?: boolean\n disabledNext?: boolean\n}\n\nexport function CalendarMonthSelector({\n month, locale, onPreviousMonth, onNextMonth, disabledPrevious, disabledNext, className, ...props\n}: CalendarMonthSelectorProps) {\n const t = useT()\n return (\n <div data-slot=\"calendar-month-selector\" className={cn('flex h-9 items-center gap-1.5 rounded-md bg-muted p-1.5', className)} {...props}>\n {onPreviousMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onPreviousMonth} disabled={disabledPrevious} aria-label={t('ui.calendar.previousMonth', 'Previous month')}><ChevronLeft className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n <span className=\"min-w-0 flex-1 text-center text-sm font-medium text-muted-foreground\" aria-live=\"polite\">{format(month, 'MMMM yyyy', locale ? { locale } : undefined)}</span>\n {onNextMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onNextMonth} disabled={disabledNext} aria-label={t('ui.calendar.nextMonth', 'Next month')}><ChevronRight className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n </div>\n )\n}\n\n// `LLLL`/`LLL`, not `MMMM`/`MMM`: these labels name a month on its own, and languages that\n// inflect month names \u2014 Polish among the five this repo ships \u2014 spell that differently from a\n// month inside a full date. `MMMM` is the inflected (genitive) form, so a Polish header read\n// \"stycznia\" (\"of January\") where it should read \"stycze\u0144\". The two tokens are identical in\n// English, which is why the bug was invisible until a non-English tenant opened a picker.\nconst STANDALONE_MONTH_YEAR = 'LLLL yyyy'\nconst STANDALONE_MONTH_SHORT = 'LLL'\n\nconst navButtonClassName = cn(\n 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',\n 'border border-border bg-background text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground hover:border-input',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',\n)\n\nfunction MonthNavButton({\n direction,\n locale,\n}: {\n direction: 'prev' | 'next'\n locale?: Locale\n}) {\n const dayPicker = useDayPicker() as unknown as {\n previousMonth?: Date\n nextMonth?: Date\n goToMonth?: (month: Date) => void\n }\n const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth\n const Icon = direction === 'prev' ? ChevronLeft : ChevronRight\n const targetLabel = format(target ?? new Date(), STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const ariaLabel = `Go to ${direction === 'prev' ? 'previous' : 'next'} month: ${targetLabel}`\n return (\n <button\n type=\"button\"\n disabled={!target}\n aria-label={ariaLabel}\n onClick={() => {\n if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)\n }}\n className={navButtonClassName}\n >\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\nfunction buildMonthCaption(\n locale: Locale | undefined,\n totalMonths: number,\n onOpenMonthGrid: (() => void) | null,\n) {\n return function MonthCaption({\n calendarMonth,\n displayIndex,\n }: {\n calendarMonth: CalendarMonth\n displayIndex?: number\n }) {\n const label = format(calendarMonth.date, STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const index = typeof displayIndex === 'number' ? displayIndex : 0\n // For multi-month layouts (e.g. range pickers) only the leftmost month\n // exposes the previous-month chevron and only the rightmost exposes the\n // next-month chevron. Navigation is always global across all visible\n // months, so showing both arrows on every month is confusing.\n const showPrev = index === 0\n const showNext = index === totalMonths - 1\n // The month grid (fast navigation) is only offered on single-month\n // calendars; on the leftmost caption it owns the click target.\n const labelInteractive = onOpenMonthGrid !== null && index === 0\n return (\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n {showPrev ? (\n <MonthNavButton direction=\"prev\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n {labelInteractive ? (\n <button\n type=\"button\"\n onClick={onOpenMonthGrid ?? undefined}\n aria-label={`${label} \u2013 open month and year navigation`}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {label}\n </button>\n ) : (\n <div\n className=\"flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium\"\n aria-live=\"polite\"\n >\n {label}\n </div>\n )}\n {showNext ? (\n <MonthNavButton direction=\"next\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n </div>\n )\n }\n}\n\nfunction MonthGrid({\n initialYear,\n selectedMonth,\n locale,\n onSelectMonth,\n onClose,\n}: {\n initialYear: number\n selectedMonth: Date\n locale?: Locale\n onSelectMonth: (month: Date) => void\n onClose: () => void\n}) {\n const [year, setYear] = React.useState(initialYear)\n const today = new Date()\n const monthLabels = React.useMemo(\n () =>\n Array.from({ length: 12 }, (_, monthIndex) =>\n format(new Date(year, monthIndex, 1), STANDALONE_MONTH_SHORT, locale ? { locale } : undefined),\n ),\n [year, locale],\n )\n const yearLabel = format(new Date(year, 0, 1), 'yyyy', locale ? { locale } : undefined)\n return (\n <div\n className=\"absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3\"\n role=\"dialog\"\n aria-label=\"Select month and year\"\n >\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n <button\n type=\"button\"\n aria-label={`Go to previous year: ${year - 1}`}\n onClick={() => setYear((current) => current - 1)}\n className={navButtonClassName}\n >\n <ChevronLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={`${yearLabel} \u2013 back to day selection`}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {yearLabel}\n </button>\n <button\n type=\"button\"\n aria-label={`Go to next year: ${year + 1}`}\n onClick={() => setYear((current) => current + 1)}\n className={navButtonClassName}\n >\n <ChevronRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n <div className=\"grid grid-cols-3 gap-2\">\n {monthLabels.map((monthLabel, monthIndex) => {\n const isSelected =\n selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex\n const isCurrentMonth =\n today.getFullYear() === year && today.getMonth() === monthIndex\n return (\n <button\n key={monthIndex}\n type=\"button\"\n aria-pressed={isSelected}\n onClick={() => onSelectMonth(new Date(year, monthIndex, 1))}\n className={cn(\n 'h-9 rounded-md text-sm font-normal transition-colors',\n 'inline-flex items-center justify-center',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n isCurrentMonth && !isSelected && 'font-semibold text-primary',\n isSelected && '!bg-primary !text-primary-foreground hover:!bg-primary',\n )}\n >\n {monthLabel}\n </button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function Calendar({\n className,\n classNames,\n showOutsideDays = true,\n daySize = 36,\n fixedWeeks = true,\n locale,\n components,\n numberOfMonths = 1,\n pagedNavigation = true,\n month,\n defaultMonth,\n onMonthChange,\n ...props\n}: CalendarProps) {\n // The month/year grid (fast navigation) is only meaningful for single-month\n // calendars; multi-month layouts (range pickers) keep the static caption.\n const monthGridEnabled = numberOfMonths === 1\n const [displayMonth, setDisplayMonth] = React.useState<Date>(\n () => month ?? defaultMonth ?? new Date(),\n )\n const [showMonthGrid, setShowMonthGrid] = React.useState(false)\n\n // Honor a controlled `month` prop when consumers drive navigation externally.\n React.useEffect(() => {\n if (month) setDisplayMonth(month)\n }, [month])\n\n const handleMonthChange = React.useCallback(\n (next: Date) => {\n setDisplayMonth(next)\n onMonthChange?.(next)\n },\n [onMonthChange],\n )\n\n const handleSelectMonth = React.useCallback(\n (next: Date) => {\n handleMonthChange(next)\n setShowMonthGrid(false)\n },\n [handleMonthChange],\n )\n\n const monthCaption = React.useMemo(\n () =>\n buildMonthCaption(\n locale as Locale | undefined,\n numberOfMonths,\n monthGridEnabled ? () => setShowMonthGrid(true) : null,\n ),\n [locale, numberOfMonths, monthGridEnabled],\n )\n\n return (\n <div className={monthGridEnabled ? 'relative' : 'contents'}>\n <div\n className={monthGridEnabled && showMonthGrid ? 'pointer-events-none' : 'contents'}\n aria-hidden={monthGridEnabled && showMonthGrid ? true : undefined}\n >\n <DayPicker\n showOutsideDays={showOutsideDays}\n fixedWeeks={fixedWeeks}\n pagedNavigation={pagedNavigation}\n locale={locale}\n numberOfMonths={numberOfMonths}\n month={displayMonth}\n onMonthChange={handleMonthChange}\n className={cn('p-3', className)}\n classNames={{\n months: 'flex flex-col sm:flex-row gap-4',\n month: 'space-y-2',\n month_caption: '',\n caption_label: 'sr-only',\n nav: 'sr-only',\n month_grid: 'w-full border-collapse',\n weekdays: 'flex',\n weekday: cn('text-muted-foreground rounded-md font-normal text-xs', daySize === 40 ? 'w-10' : 'w-9'),\n weeks: 'w-full border-collapse',\n week: 'flex w-full mt-1',\n day: cn('text-center text-sm p-0 relative focus-within:relative focus-within:z-20', daySize === 40 ? 'size-10' : 'size-9'),\n day_button: cn(\n 'p-0 font-normal aria-selected:opacity-100',\n daySize === 40 ? 'size-10' : 'size-9',\n 'inline-flex items-center justify-center rounded-md text-sm',\n 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',\n // Focus indicator is a soft accent fill instead of a ring overlay \u2014 keyboard\n // users get a visible cue, but mouse-click focus does not leave a stuck ring\n // on top of the selected cell.\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:bg-accent focus-visible:text-accent-foreground',\n ),\n // React-day-picker v9 applies `classNames.selected` / `range_*` to the day\n // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent\n // fill visible through interaction, we (a) paint the cell with the desired\n // bg/text, and (b) force the inner button to render transparent \u2014 so the\n // button's own hover/focus-visible bg overrides cannot cover the cell fill.\n selected: cn(\n '!bg-primary !text-primary-foreground rounded-md',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_start: cn(\n '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_end: cn(\n '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_middle: cn(\n '!bg-accent !text-accent-foreground !rounded-none',\n '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',\n ),\n today: 'font-semibold text-primary rounded-md',\n outside:\n 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n disabled: 'text-muted-foreground opacity-50',\n hidden: 'invisible',\n ...classNames,\n }}\n components={{\n MonthCaption: monthCaption,\n ...components,\n }}\n {...props}\n />\n </div>\n {monthGridEnabled && showMonthGrid ? (\n <MonthGrid\n initialYear={displayMonth.getFullYear()}\n selectedMonth={displayMonth}\n locale={locale as Locale | undefined}\n onSelectMonth={handleSelectMonth}\n onClose={() => setShowMonthGrid(false)}\n />\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { DayPicker, useDayPicker } from 'react-day-picker'\nimport type { DayPickerProps, CalendarMonth } from 'react-day-picker'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { format } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useOptionalT, useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { createTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { CompactButton } from './compact-button'\n\nexport type CalendarProps = DayPickerProps & { daySize?: 36 | 40 }\n\nexport type CalendarMonthSelectorProps = React.HTMLAttributes<HTMLDivElement> & {\n month: Date\n locale?: Locale\n onPreviousMonth?: () => void\n onNextMonth?: () => void\n disabledPrevious?: boolean\n disabledNext?: boolean\n}\n\nexport function CalendarMonthSelector({\n month, locale, onPreviousMonth, onNextMonth, disabledPrevious, disabledNext, className, ...props\n}: CalendarMonthSelectorProps) {\n const t = useT()\n return (\n <div data-slot=\"calendar-month-selector\" className={cn('flex h-9 items-center gap-1.5 rounded-md bg-muted p-1.5', className)} {...props}>\n {onPreviousMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onPreviousMonth} disabled={disabledPrevious} aria-label={t('ui.calendar.previousMonth', 'Previous month')}><ChevronLeft className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n <span className=\"min-w-0 flex-1 text-center text-sm font-medium text-muted-foreground\" aria-live=\"polite\">{format(month, 'MMMM yyyy', locale ? { locale } : undefined)}</span>\n {onNextMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onNextMonth} disabled={disabledNext} aria-label={t('ui.calendar.nextMonth', 'Next month')}><ChevronRight className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n </div>\n )\n}\n\n// `LLLL`/`LLL`, not `MMMM`/`MMM`: these labels name a month on its own, and languages that\n// inflect month names \u2014 Polish among the five this repo ships \u2014 spell that differently from a\n// month inside a full date. `MMMM` is the inflected (genitive) form, so a Polish header read\n// \"stycznia\" (\"of January\") where it should read \"stycze\u0144\". The two tokens are identical in\n// English, which is why the bug was invisible until a non-English tenant opened a picker.\nconst STANDALONE_MONTH_YEAR = 'LLLL yyyy'\nconst STANDALONE_MONTH_SHORT = 'LLL'\n\nconst fallbackTranslate = createTranslator({})\n\nfunction useCalendarT(): TranslateFn {\n return useOptionalT() ?? fallbackTranslate\n}\n\ntype CalendarLabels = DayPickerProps['labels']\n\nconst CalendarLabelsContext = React.createContext<CalendarLabels>(undefined)\n\nconst navButtonClassName = cn(\n 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',\n 'border border-border bg-background text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground hover:border-input',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',\n)\n\nfunction MonthNavButton({\n direction,\n locale,\n}: {\n direction: 'prev' | 'next'\n locale?: Locale\n}) {\n const t = useCalendarT()\n const labels = React.useContext(CalendarLabelsContext)\n const dayPicker = useDayPicker() as unknown as {\n previousMonth?: Date\n nextMonth?: Date\n goToMonth?: (month: Date) => void\n }\n const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth\n const Icon = direction === 'prev' ? ChevronLeft : ChevronRight\n const targetLabel = format(target ?? new Date(), STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const customLabel = direction === 'prev' ? labels?.labelPrevious : labels?.labelNext\n const ariaLabel = customLabel\n ? customLabel(target)\n : direction === 'prev'\n ? t('ui.calendar.goToPreviousMonth', 'Go to previous month: {month}', { month: targetLabel })\n : t('ui.calendar.goToNextMonth', 'Go to next month: {month}', { month: targetLabel })\n return (\n <button\n type=\"button\"\n disabled={!target}\n aria-label={ariaLabel}\n onClick={() => {\n if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)\n }}\n className={navButtonClassName}\n >\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\nfunction buildMonthCaption(\n locale: Locale | undefined,\n totalMonths: number,\n onOpenMonthGrid: (() => void) | null,\n monthGridTriggerRef: React.Ref<HTMLButtonElement>,\n) {\n return function MonthCaption({\n calendarMonth,\n displayIndex,\n }: {\n calendarMonth: CalendarMonth\n displayIndex?: number\n }) {\n const t = useCalendarT()\n const label = format(calendarMonth.date, STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const index = typeof displayIndex === 'number' ? displayIndex : 0\n // For multi-month layouts (e.g. range pickers) only the leftmost month\n // exposes the previous-month chevron and only the rightmost exposes the\n // next-month chevron. Navigation is always global across all visible\n // months, so showing both arrows on every month is confusing.\n const showPrev = index === 0\n const showNext = index === totalMonths - 1\n // The month grid (fast navigation) is only offered on single-month\n // calendars; on the leftmost caption it owns the click target.\n const labelInteractive = onOpenMonthGrid !== null && index === 0\n return (\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n {showPrev ? (\n <MonthNavButton direction=\"prev\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n {labelInteractive ? (\n <button\n ref={monthGridTriggerRef}\n type=\"button\"\n onClick={onOpenMonthGrid ?? undefined}\n aria-label={t('ui.calendar.openMonthYearNavigation', '{month} \u2013 open month and year navigation', { month: label })}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {label}\n </button>\n ) : (\n <div\n className=\"flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium\"\n aria-live=\"polite\"\n >\n {label}\n </div>\n )}\n {showNext ? (\n <MonthNavButton direction=\"next\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n </div>\n )\n }\n}\n\nfunction MonthGrid({\n initialYear,\n selectedMonth,\n locale,\n onSelectMonth,\n onClose,\n onRestoreFocus,\n}: {\n initialYear: number\n selectedMonth: Date\n locale?: Locale\n onSelectMonth: (month: Date) => void\n onClose: () => void\n onRestoreFocus: () => void\n}) {\n const t = useCalendarT()\n const [year, setYear] = React.useState(initialYear)\n const gridRef = React.useRef<HTMLDivElement>(null)\n const selectedMonthRef = React.useRef<HTMLButtonElement>(null)\n React.useEffect(() => {\n selectedMonthRef.current?.focus()\n return onRestoreFocus\n }, [onRestoreFocus])\n\n React.useEffect(() => {\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || !(event.target instanceof Node) || !gridRef.current?.contains(event.target)) return\n event.preventDefault()\n event.stopPropagation()\n onClose()\n }\n window.addEventListener('keydown', handleEscape, true)\n return () => window.removeEventListener('keydown', handleEscape, true)\n }, [onClose])\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Tab') return\n\n const buttons = event.currentTarget.querySelectorAll<HTMLButtonElement>('button:not(:disabled)')\n const firstButton = buttons[0]\n const lastButton = buttons[buttons.length - 1]\n if (event.shiftKey && document.activeElement === firstButton) {\n event.preventDefault()\n lastButton?.focus()\n } else if (!event.shiftKey && document.activeElement === lastButton) {\n event.preventDefault()\n firstButton?.focus()\n }\n }\n const today = new Date()\n const monthLabels = React.useMemo(\n () =>\n Array.from({ length: 12 }, (_, monthIndex) =>\n format(new Date(year, monthIndex, 1), STANDALONE_MONTH_SHORT, locale ? { locale } : undefined),\n ),\n [year, locale],\n )\n const yearLabel = format(new Date(year, 0, 1), 'yyyy', locale ? { locale } : undefined)\n return (\n <div\n ref={gridRef}\n className=\"absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3\"\n role=\"dialog\"\n aria-label={t('ui.calendar.selectMonthAndYear', 'Select month and year')}\n onKeyDown={handleKeyDown}\n >\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n <button\n type=\"button\"\n aria-label={t('ui.calendar.goToPreviousYear', 'Go to previous year: {year}', { year: year - 1 })}\n onClick={() => setYear((current) => current - 1)}\n className={navButtonClassName}\n >\n <ChevronLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={t('ui.calendar.backToDaySelection', '{year} \u2013 back to day selection', { year: yearLabel })}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {yearLabel}\n </button>\n <button\n type=\"button\"\n aria-label={t('ui.calendar.goToNextYear', 'Go to next year: {year}', { year: year + 1 })}\n onClick={() => setYear((current) => current + 1)}\n className={navButtonClassName}\n >\n <ChevronRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n <div className=\"grid grid-cols-3 gap-2\">\n {monthLabels.map((monthLabel, monthIndex) => {\n const isSelected =\n selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex\n const isCurrentMonth =\n today.getFullYear() === year && today.getMonth() === monthIndex\n return (\n <button\n key={monthIndex}\n ref={monthIndex === selectedMonth.getMonth() ? selectedMonthRef : undefined}\n type=\"button\"\n aria-pressed={isSelected}\n onClick={() => onSelectMonth(new Date(year, monthIndex, 1))}\n className={cn(\n 'h-9 rounded-md text-sm font-normal transition-colors',\n 'inline-flex items-center justify-center',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n isCurrentMonth && !isSelected && 'font-semibold text-primary',\n isSelected && '!bg-primary !text-primary-foreground hover:!bg-primary',\n )}\n >\n {monthLabel}\n </button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function Calendar({\n className,\n classNames,\n showOutsideDays = true,\n daySize = 36,\n fixedWeeks = true,\n locale,\n components,\n numberOfMonths = 1,\n pagedNavigation = true,\n month,\n defaultMonth,\n onMonthChange,\n labels,\n ...props\n}: CalendarProps) {\n // The month/year grid (fast navigation) is only meaningful for single-month\n // calendars; multi-month layouts (range pickers) keep the static caption.\n const monthGridEnabled = numberOfMonths === 1\n const [displayMonth, setDisplayMonth] = React.useState<Date>(\n () => month ?? defaultMonth ?? new Date(),\n )\n const [showMonthGrid, setShowMonthGrid] = React.useState(false)\n const monthGridTriggerRef = React.useRef<HTMLButtonElement>(null)\n const restoreDayViewFocus = React.useCallback(() => {\n monthGridTriggerRef.current?.focus()\n }, [])\n\n // Honor a controlled `month` prop when consumers drive navigation externally.\n React.useEffect(() => {\n if (month) setDisplayMonth(month)\n }, [month])\n\n const handleMonthChange = React.useCallback(\n (next: Date) => {\n setDisplayMonth(next)\n onMonthChange?.(next)\n },\n [onMonthChange],\n )\n\n const handleSelectMonth = React.useCallback(\n (next: Date) => {\n handleMonthChange(next)\n setShowMonthGrid(false)\n },\n [handleMonthChange],\n )\n\n const monthCaption = React.useMemo(\n () =>\n buildMonthCaption(\n locale as Locale | undefined,\n numberOfMonths,\n monthGridEnabled ? () => setShowMonthGrid(true) : null,\n monthGridTriggerRef,\n ),\n [locale, numberOfMonths, monthGridEnabled],\n )\n\n return (\n <CalendarLabelsContext.Provider value={labels}>\n <div className={monthGridEnabled ? 'relative' : 'contents'}>\n <div\n className={monthGridEnabled && showMonthGrid ? 'invisible pointer-events-none' : 'contents'}\n aria-hidden={monthGridEnabled && showMonthGrid ? true : undefined}\n inert={monthGridEnabled && showMonthGrid ? true : undefined}\n >\n <DayPicker\n showOutsideDays={showOutsideDays}\n fixedWeeks={fixedWeeks}\n pagedNavigation={pagedNavigation}\n hideNavigation\n locale={locale}\n labels={labels}\n numberOfMonths={numberOfMonths}\n month={displayMonth}\n onMonthChange={handleMonthChange}\n className={cn('p-3', className)}\n classNames={{\n months: 'flex flex-col sm:flex-row gap-4',\n month: 'space-y-2',\n month_caption: '',\n caption_label: 'sr-only',\n nav: 'sr-only',\n month_grid: 'w-full border-collapse',\n weekdays: 'flex',\n weekday: cn('text-muted-foreground rounded-md font-normal text-xs', daySize === 40 ? 'w-10' : 'w-9'),\n weeks: 'w-full border-collapse',\n week: 'flex w-full mt-1',\n day: cn('text-center text-sm p-0 relative focus-within:relative focus-within:z-20', daySize === 40 ? 'size-10' : 'size-9'),\n day_button: cn(\n 'p-0 font-normal aria-selected:opacity-100',\n daySize === 40 ? 'size-10' : 'size-9',\n 'inline-flex items-center justify-center rounded-md text-sm',\n 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',\n // Focus indicator is a soft accent fill instead of a ring overlay \u2014 keyboard\n // users get a visible cue, but mouse-click focus does not leave a stuck ring\n // on top of the selected cell.\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:bg-accent focus-visible:text-accent-foreground',\n ),\n // React-day-picker v9 applies `classNames.selected` / `range_*` to the day\n // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent\n // fill visible through interaction, we (a) paint the cell with the desired\n // bg/text, and (b) force the inner button to render transparent \u2014 so the\n // button's own hover/focus-visible bg overrides cannot cover the cell fill.\n selected: cn(\n '!bg-primary !text-primary-foreground rounded-md',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_start: cn(\n '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_end: cn(\n '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_middle: cn(\n '!bg-accent !text-accent-foreground !rounded-none',\n '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',\n ),\n today: 'font-semibold text-primary rounded-md',\n outside:\n 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n disabled: 'text-muted-foreground opacity-50',\n hidden: 'invisible',\n ...classNames,\n }}\n components={{\n MonthCaption: monthCaption,\n ...components,\n }}\n {...props}\n />\n </div>\n {monthGridEnabled && showMonthGrid ? (\n <MonthGrid\n initialYear={displayMonth.getFullYear()}\n selectedMonth={displayMonth}\n locale={locale as Locale | undefined}\n onSelectMonth={handleSelectMonth}\n onClose={() => setShowMonthGrid(false)}\n onRestoreFocus={restoreDayViewFocus}\n />\n ) : null}\n </div>\n </CalendarLabelsContext.Provider>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.8.1-develop.
|
|
3
|
+
"version": "0.8.1-develop.7441.1.b42958b98c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.8.1-develop.7441.1.b42958b98c",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.8.1-develop.7441.1.b42958b98c",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -1991,6 +1991,27 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1991
1991
|
() => new Set(injectedFieldDefinitions.map((definition) => definition.id)),
|
|
1992
1992
|
[injectedFieldDefinitions],
|
|
1993
1993
|
)
|
|
1994
|
+
// An injected field may reuse a host-declared field id to replace the host's built-in
|
|
1995
|
+
// input for that field (the later, injected entry wins in `fieldById`) — e.g.
|
|
1996
|
+
// `customer_groups`' `GroupPickerField` replacing catalog's plain-text `customerGroupId`.
|
|
1997
|
+
// Such a field is still a host schema field, so its value must reach `coreValues` and the
|
|
1998
|
+
// host `onSubmit` payload. Only an injected field with NO host-declared counterpart is an
|
|
1999
|
+
// "extra" field that is stripped before schema validation/submission (widgets still see
|
|
2000
|
+
// it via `onBeforeSave`/`onSave`). See UPGRADE_NOTES.md (0.8.1). Host fields declared
|
|
2001
|
+
// inline in `groups[].fields` count as host-declared too.
|
|
2002
|
+
const hostFieldIdSet = React.useMemo(() => {
|
|
2003
|
+
const ids = new Set(fields.map((field) => field.id))
|
|
2004
|
+
for (const group of groups ?? []) {
|
|
2005
|
+
for (const entry of group.fields ?? []) {
|
|
2006
|
+
if (typeof entry !== 'string') ids.add(entry.id)
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return ids
|
|
2010
|
+
}, [fields, groups])
|
|
2011
|
+
const injectedOnlyFieldIdSet = React.useMemo(
|
|
2012
|
+
() => new Set(Array.from(injectedFieldIdSet).filter((id) => !hostFieldIdSet.has(id))),
|
|
2013
|
+
[injectedFieldIdSet, hostFieldIdSet],
|
|
2014
|
+
)
|
|
1994
2015
|
|
|
1995
2016
|
const injectedCrudFields = React.useMemo<CrudField[]>(() => {
|
|
1996
2017
|
return injectedFieldDefinitions.map((definition) => {
|
|
@@ -2043,8 +2064,9 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2043
2064
|
}, [allFields])
|
|
2044
2065
|
|
|
2045
2066
|
// Declared base fields whose id is a dot-path (e.g. `metadata.category`).
|
|
2046
|
-
// Injected and custom (cf) fields already get dot-path/cf-path hydration and
|
|
2047
|
-
// are excluded here so their dedicated handling is preserved
|
|
2067
|
+
// Injected-only and custom (cf) fields already get dot-path/cf-path hydration and
|
|
2068
|
+
// are excluded here so their dedicated handling is preserved; an injected field
|
|
2069
|
+
// that reuses a host field id stays a host field and is collapsed. CrudForm hydrates
|
|
2048
2070
|
// these flat keys from nested initial values on load and projects them back into
|
|
2049
2071
|
// nested objects before schema.safeParse on submit. See issue #2503.
|
|
2050
2072
|
const dotPathBaseFieldIds = React.useMemo(() => {
|
|
@@ -2053,13 +2075,13 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2053
2075
|
for (const field of allFields) {
|
|
2054
2076
|
const fieldId = field.id
|
|
2055
2077
|
if (!fieldId.includes('.')) continue
|
|
2056
|
-
if (
|
|
2078
|
+
if (injectedOnlyFieldIdSet.has(fieldId)) continue
|
|
2057
2079
|
if (fieldId.startsWith('cf_') || fieldId.startsWith('cf:')) continue
|
|
2058
2080
|
if (cfFieldIds.has(fieldId)) continue
|
|
2059
2081
|
ids.add(fieldId)
|
|
2060
2082
|
}
|
|
2061
2083
|
return ids
|
|
2062
|
-
}, [allFields,
|
|
2084
|
+
}, [allFields, injectedOnlyFieldIdSet, cfFields])
|
|
2063
2085
|
|
|
2064
2086
|
const validateFieldOnBlur = React.useCallback(async (
|
|
2065
2087
|
fieldId: string,
|
|
@@ -2132,7 +2154,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2132
2154
|
delete widgetValues[hiddenId]
|
|
2133
2155
|
}
|
|
2134
2156
|
const coreValues = { ...widgetValues }
|
|
2135
|
-
for (const injectedId of
|
|
2157
|
+
for (const injectedId of injectedOnlyFieldIdSet) {
|
|
2136
2158
|
delete coreValues[injectedId]
|
|
2137
2159
|
}
|
|
2138
2160
|
const result = schema.safeParse(collapseDotPathFields(coreValues, dotPathBaseFieldIds))
|
|
@@ -2181,7 +2203,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2181
2203
|
hiddenBaseFieldIds,
|
|
2182
2204
|
hiddenGroupFieldIds,
|
|
2183
2205
|
hiddenInjectedFieldIds,
|
|
2184
|
-
|
|
2206
|
+
injectedOnlyFieldIdSet,
|
|
2185
2207
|
mapDefsForValidation,
|
|
2186
2208
|
schema,
|
|
2187
2209
|
t,
|
|
@@ -3050,7 +3072,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3050
3072
|
delete widgetValues[hiddenId]
|
|
3051
3073
|
}
|
|
3052
3074
|
const coreValues = { ...widgetValues }
|
|
3053
|
-
for (const injectedId of
|
|
3075
|
+
for (const injectedId of injectedOnlyFieldIdSet) {
|
|
3054
3076
|
delete coreValues[injectedId]
|
|
3055
3077
|
}
|
|
3056
3078
|
if (customEntity) {
|
|
@@ -3089,7 +3111,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3089
3111
|
if (result.data) {
|
|
3090
3112
|
submitValues = result.data as TValues
|
|
3091
3113
|
const projectedCoreValues = { ...(result.data as Record<string, unknown>) }
|
|
3092
|
-
for (const injectedId of
|
|
3114
|
+
for (const injectedId of injectedOnlyFieldIdSet) {
|
|
3093
3115
|
delete projectedCoreValues[injectedId]
|
|
3094
3116
|
}
|
|
3095
3117
|
if (customEntity) {
|
|
@@ -63,6 +63,7 @@ import type {
|
|
|
63
63
|
InjectionRowActionDefinition,
|
|
64
64
|
} from '@open-mercato/shared/modules/widgets/injection'
|
|
65
65
|
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
66
|
+
import { useRegisteredComponent } from './injection/useRegisteredComponent'
|
|
66
67
|
import { dataTableExtensionSpotId, extensionSpotChildId } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
67
68
|
import { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
68
69
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
@@ -1252,7 +1253,7 @@ function ViewSwitcherDropdown({
|
|
|
1252
1253
|
)
|
|
1253
1254
|
}
|
|
1254
1255
|
|
|
1255
|
-
|
|
1256
|
+
function DataTableImpl<T extends RowData>({
|
|
1256
1257
|
columns,
|
|
1257
1258
|
data,
|
|
1258
1259
|
toolbar,
|
|
@@ -1396,6 +1397,11 @@ export function DataTable<T extends RowData>({
|
|
|
1396
1397
|
// cannot serve here: applying a snapshot rewrites the stored copy with a
|
|
1397
1398
|
// fresh `Date.now()`, which would make every server row look older than it is.
|
|
1398
1399
|
const hydratedSnapshotRef = React.useRef<PerspectiveSnapshot | null>(null)
|
|
1400
|
+
const hydratedAdvancedFilterRestoredRef = React.useRef(false)
|
|
1401
|
+
const pendingPerspectiveFiltersRef = React.useRef<{ filters: PerspectiveSettings['filters'] } | null>(null)
|
|
1402
|
+
React.useLayoutEffect(() => {
|
|
1403
|
+
pendingPerspectiveFiltersRef.current = null
|
|
1404
|
+
}, [advancedFilter?.value, perspectiveTableId])
|
|
1399
1405
|
const initialSettingsSource = perspectiveConfig?.initialState?.initialSettings ?? null
|
|
1400
1406
|
// Memoized on the host's own object: `sanitizePerspectiveSettings` returns a
|
|
1401
1407
|
// fresh result on every call, so without this every effect keyed on the
|
|
@@ -2032,6 +2038,12 @@ export function DataTable<T extends RowData>({
|
|
|
2032
2038
|
},
|
|
2033
2039
|
) => {
|
|
2034
2040
|
const normalized = sanitizePerspectiveSettings(settings) ?? {}
|
|
2041
|
+
if (!options?.preserveAdvancedFilter && advancedFilter?.onApplyTree) {
|
|
2042
|
+
hydratedAdvancedFilterRestoredRef.current = false
|
|
2043
|
+
pendingPerspectiveFiltersRef.current = {
|
|
2044
|
+
filters: isPersistedFilterTree(normalized.filters) ? normalized.filters : undefined,
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2035
2047
|
// The applied settings replace the baseline outright, so the defaults hidden under
|
|
2036
2048
|
// the *previous* view must not leak into it. The pass below re-runs and re-folds
|
|
2037
2049
|
// whatever this view leaves undecided.
|
|
@@ -2042,7 +2054,12 @@ export function DataTable<T extends RowData>({
|
|
|
2042
2054
|
// filter change the user never made.
|
|
2043
2055
|
setViewBaseline(
|
|
2044
2056
|
options?.preserveAdvancedFilter
|
|
2045
|
-
? {
|
|
2057
|
+
? {
|
|
2058
|
+
...normalized,
|
|
2059
|
+
filters: pendingPerspectiveFiltersRef.current
|
|
2060
|
+
? pendingPerspectiveFiltersRef.current.filters
|
|
2061
|
+
: getCurrentSettings().filters,
|
|
2062
|
+
}
|
|
2046
2063
|
: normalized,
|
|
2047
2064
|
)
|
|
2048
2065
|
if (normalized.columnOrder && normalized.columnOrder.length) {
|
|
@@ -2110,7 +2127,14 @@ export function DataTable<T extends RowData>({
|
|
|
2110
2127
|
if (perspectiveTableId) {
|
|
2111
2128
|
writePerspectiveCookie(perspectiveTableId, nextId)
|
|
2112
2129
|
if (nextId) {
|
|
2113
|
-
const
|
|
2130
|
+
const previousSnapshot = readPerspectiveSnapshot(perspectiveTableId) ?? initialSnapshotRef.current
|
|
2131
|
+
const preserveClearedFilter = options?.preserveAdvancedFilter
|
|
2132
|
+
&& !getCurrentSettings().filters
|
|
2133
|
+
&& !previousSnapshot?.settings.filters
|
|
2134
|
+
const snapshotSettings = preserveClearedFilter
|
|
2135
|
+
? { ...normalized, filters: undefined }
|
|
2136
|
+
: normalized
|
|
2137
|
+
const snapshot: PerspectiveSnapshot = { perspectiveId: nextId, settings: snapshotSettings, updatedAt: Date.now() }
|
|
2114
2138
|
writePerspectiveSnapshot(perspectiveTableId, snapshot)
|
|
2115
2139
|
initialSnapshotRef.current = snapshot
|
|
2116
2140
|
} else if (normalized.columnSizing && Object.keys(normalized.columnSizing).length) {
|
|
@@ -2175,29 +2199,45 @@ export function DataTable<T extends RowData>({
|
|
|
2175
2199
|
if (!perspectiveTableId) return
|
|
2176
2200
|
if (snapshotHydratedTableRef.current === perspectiveTableId) return
|
|
2177
2201
|
snapshotHydratedTableRef.current = perspectiveTableId
|
|
2202
|
+
hydratedAdvancedFilterRestoredRef.current = false
|
|
2178
2203
|
const snapshot = readPerspectiveSnapshot(perspectiveTableId)
|
|
2179
2204
|
if (!snapshot) return
|
|
2180
2205
|
initialSnapshotRef.current = snapshot
|
|
2181
2206
|
hydratedSnapshotRef.current = snapshot
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
// The snapshot still drives non-filter settings: column order, visibility,
|
|
2189
|
-
// sorting, search. Explicit perspective selection and "No view" go through
|
|
2190
|
-
// a different applyPerspectiveSettings call without this option, so they
|
|
2191
|
-
// still update the host filter as expected.
|
|
2192
|
-
const preserveAdvancedFilter = !!advancedFilter?.onApplyTree
|
|
2207
|
+
const restoredTree = isPersistedFilterTree(snapshot.settings.filters)
|
|
2208
|
+
? deserializeTreeFromPersist(snapshot.settings.filters)
|
|
2209
|
+
: null
|
|
2210
|
+
const preserveAdvancedFilter = !!advancedFilter?.onApplyTree && (
|
|
2211
|
+
advancedFilter.value.root.children.length > 0 || !restoredTree?.root.children.length
|
|
2212
|
+
)
|
|
2193
2213
|
applyPerspectiveSettings(
|
|
2194
2214
|
snapshot.settings,
|
|
2195
2215
|
snapshot.perspectiveId ?? null,
|
|
2196
2216
|
{ preserveAdvancedFilter },
|
|
2197
2217
|
)
|
|
2218
|
+
hydratedAdvancedFilterRestoredRef.current = !!advancedFilter?.onApplyTree && !preserveAdvancedFilter
|
|
2198
2219
|
initialPerspectiveAppliedRef.current = true
|
|
2199
2220
|
}, [perspectiveTableId, applyPerspectiveSettings, advancedFilter])
|
|
2200
2221
|
|
|
2222
|
+
const previousAdvancedFilterRef = React.useRef<{
|
|
2223
|
+
tableId: string | null
|
|
2224
|
+
hasRules: boolean
|
|
2225
|
+
} | null>(null)
|
|
2226
|
+
const advancedFilterHasRules = !!advancedFilter?.value.root.children.length
|
|
2227
|
+
React.useEffect(() => {
|
|
2228
|
+
const previous = previousAdvancedFilterRef.current
|
|
2229
|
+
previousAdvancedFilterRef.current = { tableId: perspectiveTableId, hasRules: advancedFilterHasRules }
|
|
2230
|
+
if (!perspectiveTableId || !advancedFilter?.onApplyTree) return
|
|
2231
|
+
if (previous?.tableId !== perspectiveTableId || !previous.hasRules || advancedFilterHasRules) return
|
|
2232
|
+
const snapshot = readPerspectiveSnapshot(perspectiveTableId) ?? initialSnapshotRef.current
|
|
2233
|
+
if (!snapshot) return
|
|
2234
|
+
const settings = { ...snapshot.settings }
|
|
2235
|
+
delete settings.filters
|
|
2236
|
+
const clearedSnapshot: PerspectiveSnapshot = { ...snapshot, settings, updatedAt: Date.now() }
|
|
2237
|
+
writePerspectiveSnapshot(perspectiveTableId, clearedSnapshot)
|
|
2238
|
+
initialSnapshotRef.current = clearedSnapshot
|
|
2239
|
+
}, [perspectiveTableId, advancedFilterHasRules, advancedFilter?.onApplyTree])
|
|
2240
|
+
|
|
2201
2241
|
type SavePerspectivePayload = {
|
|
2202
2242
|
name: string
|
|
2203
2243
|
isDefault: boolean
|
|
@@ -2716,11 +2756,23 @@ export function DataTable<T extends RowData>({
|
|
|
2716
2756
|
React.useLayoutEffect(() => {
|
|
2717
2757
|
if (!canUsePerspectives) return
|
|
2718
2758
|
if (!perspectiveTableId) return
|
|
2759
|
+
if (
|
|
2760
|
+
(initialSnapshotRef.current || initialPerspectiveAppliedRef.current)
|
|
2761
|
+
&& serverReconciledTableRef.current === perspectiveTableId
|
|
2762
|
+
) return
|
|
2719
2763
|
|
|
2720
2764
|
const source = perspectiveData ?? perspectiveConfig?.initialState?.response
|
|
2721
2765
|
if (!source) return
|
|
2722
2766
|
|
|
2723
2767
|
let orphanedSnapshotDropped = false
|
|
2768
|
+
const currentFilters = pendingPerspectiveFiltersRef.current
|
|
2769
|
+
? pendingPerspectiveFiltersRef.current.filters
|
|
2770
|
+
: getCurrentSettings().filters
|
|
2771
|
+
const restoredFilterStillActive = hydratedAdvancedFilterRestoredRef.current && diffPerspectiveSettings(
|
|
2772
|
+
{ filters: currentFilters },
|
|
2773
|
+
{ filters: hydratedSnapshotRef.current?.settings.filters },
|
|
2774
|
+
).length === 0
|
|
2775
|
+
const preserveAdvancedFilter = !!advancedFilter?.onApplyTree && !restoredFilterStillActive
|
|
2724
2776
|
|
|
2725
2777
|
const tryResolve = (id: string | null | undefined): PerspectiveDto | RolePerspectiveDto | undefined => {
|
|
2726
2778
|
if (!id) return undefined
|
|
@@ -2736,7 +2788,6 @@ export function DataTable<T extends RowData>({
|
|
|
2736
2788
|
// localStorage entry (#5113). One-shot matters as much as reconciling at
|
|
2737
2789
|
// all — a later refetch must not clobber edits made after mount.
|
|
2738
2790
|
if (initialSnapshotRef.current || initialPerspectiveAppliedRef.current) {
|
|
2739
|
-
if (serverReconciledTableRef.current === perspectiveTableId) return
|
|
2740
2791
|
serverReconciledTableRef.current = perspectiveTableId
|
|
2741
2792
|
// A snapshot only speaks for the active perspective: once the user has
|
|
2742
2793
|
// picked a different view, reconciling the mount-time one would undo that
|
|
@@ -2768,7 +2819,7 @@ export function DataTable<T extends RowData>({
|
|
|
2768
2819
|
// selection: on a host that owns filter persistence through the URL,
|
|
2769
2820
|
// it must not overwrite the filter currently on screen.
|
|
2770
2821
|
applyPerspectiveSettings(local.settings, local.id, {
|
|
2771
|
-
preserveAdvancedFilter
|
|
2822
|
+
preserveAdvancedFilter,
|
|
2772
2823
|
})
|
|
2773
2824
|
initialPerspectiveAppliedRef.current = true
|
|
2774
2825
|
}
|
|
@@ -2808,7 +2859,7 @@ export function DataTable<T extends RowData>({
|
|
|
2808
2859
|
applyPerspectiveSettings(
|
|
2809
2860
|
target.settings,
|
|
2810
2861
|
target.id,
|
|
2811
|
-
orphanedSnapshotDropped ? { preserveAdvancedFilter
|
|
2862
|
+
orphanedSnapshotDropped ? { preserveAdvancedFilter } : undefined,
|
|
2812
2863
|
)
|
|
2813
2864
|
} else if (orphanedSnapshotDropped) {
|
|
2814
2865
|
// Nothing is left to fall back to — the deleted view was the only one. The
|
|
@@ -2820,11 +2871,11 @@ export function DataTable<T extends RowData>({
|
|
|
2820
2871
|
// another session — so it must not clear the filter a host that owns
|
|
2821
2872
|
// filter persistence through the URL currently has on screen.
|
|
2822
2873
|
applyPerspectiveSettings({}, null, {
|
|
2823
|
-
preserveAdvancedFilter
|
|
2874
|
+
preserveAdvancedFilter,
|
|
2824
2875
|
})
|
|
2825
2876
|
}
|
|
2826
2877
|
initialPerspectiveAppliedRef.current = true
|
|
2827
|
-
}, [canUsePerspectives, perspectiveData, perspectiveTableId, perspectiveConfig, applyPerspectiveSettings, activePerspectiveId, advancedFilter?.onApplyTree])
|
|
2878
|
+
}, [canUsePerspectives, perspectiveData, perspectiveTableId, perspectiveConfig, applyPerspectiveSettings, activePerspectiveId, advancedFilter?.onApplyTree, getCurrentSettings])
|
|
2828
2879
|
|
|
2829
2880
|
const scrollTableIntoView = React.useCallback(() => {
|
|
2830
2881
|
const rect = containerRef.current?.getBoundingClientRect()
|
|
@@ -4004,3 +4055,25 @@ export function DataTable<T extends RowData>({
|
|
|
4004
4055
|
</TooltipProvider>
|
|
4005
4056
|
)
|
|
4006
4057
|
}
|
|
4058
|
+
|
|
4059
|
+
const DataTableFallback = DataTableImpl as unknown as React.ComponentType<DataTableProps<RowData>>
|
|
4060
|
+
|
|
4061
|
+
/**
|
|
4062
|
+
* Resolves the public DataTable component handle before mounting the table so
|
|
4063
|
+
* registered props transforms, wrappers, and replacements execute at the real
|
|
4064
|
+
* host boundary rather than being exposed only as a diagnostic DOM attribute.
|
|
4065
|
+
*/
|
|
4066
|
+
export function DataTable<T extends RowData>(props: DataTableProps<T>) {
|
|
4067
|
+
const extensionTableId = props.perspective?.tableId
|
|
4068
|
+
?? props.extensionTableId
|
|
4069
|
+
?? (props.injectionSpotId?.startsWith('data-table:')
|
|
4070
|
+
? props.injectionSpotId.slice('data-table:'.length)
|
|
4071
|
+
: null)
|
|
4072
|
+
const handle = props.replacementHandle
|
|
4073
|
+
?? ComponentReplacementHandles.dataTable(extensionTableId ?? 'unknown')
|
|
4074
|
+
const Resolved = useRegisteredComponent<DataTableProps<T>>(
|
|
4075
|
+
handle,
|
|
4076
|
+
DataTableFallback as unknown as React.ComponentType<DataTableProps<T>>,
|
|
4077
|
+
)
|
|
4078
|
+
return <Resolved {...props} />
|
|
4079
|
+
}
|
|
@@ -19,9 +19,26 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
19
19
|
const btnRef = React.useRef<HTMLButtonElement>(null)
|
|
20
20
|
const menuRef = React.useRef<HTMLDivElement>(null)
|
|
21
21
|
const hoverTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
22
|
+
/**
|
|
23
|
+
* Whether this open should pull focus into the menu.
|
|
24
|
+
*
|
|
25
|
+
* Only a deliberate open does — a click, or Enter/Space on the trigger, which
|
|
26
|
+
* the browser also reports as a click. The menu opens on hover too, and taking
|
|
27
|
+
* focus on a hover open meant brushing the pointer over ⋯ while typing in a
|
|
28
|
+
* list's search field pulled the caret out of the field (#6771).
|
|
29
|
+
*
|
|
30
|
+
* Consumed on use, so repositioning the panel mid-scroll cannot re-trigger it.
|
|
31
|
+
*/
|
|
32
|
+
const focusOnOpenRef = React.useRef(false)
|
|
22
33
|
const [anchorRect, setAnchorRect] = React.useState<DOMRect | null>(null)
|
|
23
34
|
const [direction, setDirection] = React.useState<'down' | 'up'>('down')
|
|
24
35
|
|
|
36
|
+
/** The menu's items in DOM order, as focusable elements. */
|
|
37
|
+
const getFocusableItems = React.useCallback((): HTMLElement[] => {
|
|
38
|
+
if (!menuRef.current) return []
|
|
39
|
+
return Array.from(menuRef.current.querySelectorAll<HTMLElement>('[role="menuitem"]'))
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
25
42
|
const updatePosition = React.useCallback(() => {
|
|
26
43
|
if (!btnRef.current) return
|
|
27
44
|
const rect = btnRef.current.getBoundingClientRect()
|
|
@@ -42,10 +59,60 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
function onKey(e: KeyboardEvent) {
|
|
62
|
+
// This listener is on `document`, and the menu can be open while the user
|
|
63
|
+
// is working somewhere else entirely — it opens on hover. So every key
|
|
64
|
+
// below is scoped to "focus is actually in this menu", otherwise a
|
|
65
|
+
// hover-opened menu would swallow the arrows and Home/End of someone
|
|
66
|
+
// typing in the list's search field (#6771). The trigger counts, so
|
|
67
|
+
// ArrowDown from it still walks into the menu.
|
|
68
|
+
const active = document.activeElement
|
|
69
|
+
const focusWithin = Boolean(
|
|
70
|
+
(menuRef.current && active && menuRef.current.contains(active)) || (active && active === btnRef.current),
|
|
71
|
+
)
|
|
45
72
|
if (e.key === 'Escape') {
|
|
46
73
|
setOpen(false)
|
|
47
|
-
|
|
74
|
+
// Only pull focus back if it was ours to begin with.
|
|
75
|
+
if (focusWithin) btnRef.current?.focus()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (!focusWithin) return
|
|
79
|
+
// Arrow keys move between items instead of doing nothing (#6718). Without
|
|
80
|
+
// this the only way to reach an item was to Tab through it — and since
|
|
81
|
+
// every row's trigger sits in the tab order, reaching the first item of a
|
|
82
|
+
// six-row table took eleven Tab presses.
|
|
83
|
+
const focusables = getFocusableItems()
|
|
84
|
+
if (!focusables.length) return
|
|
85
|
+
const activeIndex = focusables.indexOf(document.activeElement as HTMLElement)
|
|
86
|
+
let nextIndex: number | null = null
|
|
87
|
+
switch (e.key) {
|
|
88
|
+
case 'ArrowDown':
|
|
89
|
+
nextIndex = activeIndex < 0 ? 0 : (activeIndex + 1) % focusables.length
|
|
90
|
+
break
|
|
91
|
+
case 'ArrowUp':
|
|
92
|
+
nextIndex = activeIndex < 0
|
|
93
|
+
? focusables.length - 1
|
|
94
|
+
: (activeIndex - 1 + focusables.length) % focusables.length
|
|
95
|
+
break
|
|
96
|
+
case 'Home':
|
|
97
|
+
nextIndex = 0
|
|
98
|
+
break
|
|
99
|
+
case 'End':
|
|
100
|
+
nextIndex = focusables.length - 1
|
|
101
|
+
break
|
|
102
|
+
case 'Tab':
|
|
103
|
+
// A menu is modal for the keyboard: Tab must not escape it into the
|
|
104
|
+
// next row's trigger, which is what made the menu feel unreachable.
|
|
105
|
+
e.preventDefault()
|
|
106
|
+
nextIndex = e.shiftKey
|
|
107
|
+
? (activeIndex <= 0 ? focusables.length - 1 : activeIndex - 1)
|
|
108
|
+
: (activeIndex < 0 || activeIndex === focusables.length - 1 ? 0 : activeIndex + 1)
|
|
109
|
+
break
|
|
110
|
+
default:
|
|
111
|
+
return
|
|
48
112
|
}
|
|
113
|
+
if (nextIndex === null) return
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
focusables[nextIndex]?.focus()
|
|
49
116
|
}
|
|
50
117
|
function onScrollOrResize() {
|
|
51
118
|
updatePosition()
|
|
@@ -60,7 +127,28 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
60
127
|
window.removeEventListener('scroll', onScrollOrResize, true)
|
|
61
128
|
window.removeEventListener('resize', onScrollOrResize)
|
|
62
129
|
}
|
|
63
|
-
}, [open, updatePosition])
|
|
130
|
+
}, [open, updatePosition, getFocusableItems])
|
|
131
|
+
|
|
132
|
+
// Move focus into the menu so the arrow keys have somewhere to start, and so a
|
|
133
|
+
// screen reader announces the item rather than the still-focused trigger.
|
|
134
|
+
//
|
|
135
|
+
// Keyed on `anchorRect` rather than deferred with a timer: the panel only
|
|
136
|
+
// renders once `updatePosition` has measured the trigger, which is a second
|
|
137
|
+
// render, so on the first pass there is nothing to focus yet. This effect
|
|
138
|
+
// re-runs when that measurement lands, by which point the portal has
|
|
139
|
+
// committed. A `requestAnimationFrame` would also have waited for it, but
|
|
140
|
+
// browsers throttle rAF in a background tab, so the focus could silently never
|
|
141
|
+
// happen — a timer-free dependency cannot be throttled.
|
|
142
|
+
React.useEffect(() => {
|
|
143
|
+
if (!open) {
|
|
144
|
+
focusOnOpenRef.current = false
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (!anchorRect || !focusOnOpenRef.current) return
|
|
148
|
+
focusOnOpenRef.current = false
|
|
149
|
+
const [first] = getFocusableItems()
|
|
150
|
+
first?.focus()
|
|
151
|
+
}, [open, anchorRect, getFocusableItems])
|
|
64
152
|
|
|
65
153
|
// Cleanup timeout on unmount
|
|
66
154
|
React.useEffect(() => {
|
|
@@ -100,7 +188,9 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
100
188
|
variant="ghost"
|
|
101
189
|
aria-haspopup="menu"
|
|
102
190
|
aria-expanded={open}
|
|
103
|
-
|
|
191
|
+
// A click — and Enter/Space on the trigger, which the browser reports as
|
|
192
|
+
// one — is a deliberate open, so it takes focus. Hover does not (#6771).
|
|
193
|
+
onClick={(e) => { e.stopPropagation(); focusOnOpenRef.current = true; setOpen(true); requestAnimationFrame(updatePosition) }}
|
|
104
194
|
>
|
|
105
195
|
<span aria-hidden="true">⋯</span>
|
|
106
196
|
<span className="sr-only">{t('ui.rowActions.openActions', 'Open actions')}</span>
|
|
@@ -109,7 +199,22 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
109
199
|
<div
|
|
110
200
|
ref={menuRef}
|
|
111
201
|
role="menu"
|
|
112
|
-
|
|
202
|
+
// Sized to its longest item instead of a fixed 11rem: the items are
|
|
203
|
+
// Buttons, which are `whitespace-nowrap`, so a label wider than the box
|
|
204
|
+
// overflowed its right border — "Zarejestruj push ponownie" in Polish
|
|
205
|
+
// overhung by ~10px (#6719). `min-w-44` keeps the old width as a floor;
|
|
206
|
+
// past `max-w-xs` the items wrap instead of growing further.
|
|
207
|
+
//
|
|
208
|
+
// `flex flex-col` is load-bearing for that, not cosmetic. Button is
|
|
209
|
+
// `inline-flex`, so under a block panel the items are inline-level and
|
|
210
|
+
// `max-content` is the width of them all laid end to end — 523px here,
|
|
211
|
+
// past the cap, so the panel was pinned at `max-w-xs` whatever the
|
|
212
|
+
// labels said. They only looked stacked because `w-full` forced each
|
|
213
|
+
// onto its own line. Blockifying them as flex items makes `max-content`
|
|
214
|
+
// the widest item, which is what `w-max` was meant to measure.
|
|
215
|
+
//
|
|
216
|
+
// Same fix, same shape as ActionsDropdown (#3580).
|
|
217
|
+
className="fixed flex flex-col w-max min-w-44 max-w-xs rounded-md border bg-background p-1 shadow focus-visible:outline-none z-dropdown"
|
|
113
218
|
style={{
|
|
114
219
|
top: direction === 'down' ? anchorRect.bottom + 8 : anchorRect.top - 8,
|
|
115
220
|
left: Math.min(anchorRect.right, window.innerWidth - 8),
|
|
@@ -123,7 +228,7 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
123
228
|
<a
|
|
124
229
|
key={idx}
|
|
125
230
|
href={it.href}
|
|
126
|
-
className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-destructive' : ''}`}
|
|
231
|
+
className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent whitespace-normal ${it.destructive ? 'text-destructive' : ''}`}
|
|
127
232
|
role="menuitem"
|
|
128
233
|
onClick={(event) => {
|
|
129
234
|
event.stopPropagation()
|
|
@@ -138,7 +243,10 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
138
243
|
type="button"
|
|
139
244
|
variant="ghost"
|
|
140
245
|
size="sm"
|
|
141
|
-
|
|
246
|
+
// `whitespace-normal h-auto` so a label that reaches the panel's
|
|
247
|
+
// max width wraps to a second line rather than overflowing: the
|
|
248
|
+
// Button primitive is `whitespace-nowrap` by default (#6719).
|
|
249
|
+
className={`w-full justify-start rounded-none font-normal whitespace-normal h-auto py-1.5 text-left ${it.destructive ? 'text-destructive' : ''}`}
|
|
142
250
|
role="menuitem"
|
|
143
251
|
onClick={(event) => {
|
|
144
252
|
event.stopPropagation()
|