@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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/CrudForm.js +19 -6
  3. package/dist/backend/CrudForm.js.map +2 -2
  4. package/dist/backend/DataTable.js +61 -9
  5. package/dist/backend/DataTable.js.map +2 -2
  6. package/dist/backend/RowActions.js +54 -5
  7. package/dist/backend/RowActions.js.map +2 -2
  8. package/dist/backend/charts/KpiCard.js +2 -1
  9. package/dist/backend/charts/KpiCard.js.map +2 -2
  10. package/dist/backend/detail/AttachmentsSection.js +17 -5
  11. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  12. package/dist/backend/detail/NotesSection.js +38 -20
  13. package/dist/backend/detail/NotesSection.js.map +2 -2
  14. package/dist/backend/dev/DevRuntimeDiagnosticsBanner.js +11 -1
  15. package/dist/backend/dev/DevRuntimeDiagnosticsBanner.js.map +2 -2
  16. package/dist/backend/icons/lucideRegistry.generated.js +26 -0
  17. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  18. package/dist/backend/injection/eventBridge.js +33 -0
  19. package/dist/backend/injection/eventBridge.js.map +2 -2
  20. package/dist/backend/inputs/LookupSelect.js +20 -3
  21. package/dist/backend/inputs/LookupSelect.js.map +2 -2
  22. package/dist/backend/inputs/PhoneNumberField.js +1 -1
  23. package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
  24. package/dist/backend/inputs/TagsInput.js +20 -2
  25. package/dist/backend/inputs/TagsInput.js.map +2 -2
  26. package/dist/backend/messages/EmailThreadsPanel.js +2 -1
  27. package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
  28. package/dist/backend/messages/MessageComposer.js.map +2 -2
  29. package/dist/backend/messages/index.js.map +2 -2
  30. package/dist/backend/messages/message-compose-form-groups.js +24 -0
  31. package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
  32. package/dist/backend/messages/useMessageCompose.js +27 -0
  33. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  34. package/dist/backend/messages/useMessageComposeOperations.js +1 -0
  35. package/dist/backend/messages/useMessageComposeOperations.js.map +2 -2
  36. package/dist/backend/notifications/NotificationItem.js +8 -1
  37. package/dist/backend/notifications/NotificationItem.js.map +2 -2
  38. package/dist/portal/PortalShell.js +2 -2
  39. package/dist/portal/PortalShell.js.map +2 -2
  40. package/dist/primitives/calendar.js +69 -14
  41. package/dist/primitives/calendar.js.map +2 -2
  42. package/package.json +3 -3
  43. package/src/backend/CrudForm.tsx +30 -8
  44. package/src/backend/DataTable.tsx +92 -19
  45. package/src/backend/RowActions.tsx +114 -6
  46. package/src/backend/__tests__/AttachmentsSection.upload.test.tsx +183 -0
  47. package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +139 -2
  48. package/src/backend/__tests__/CrudForm.hiddenGroups.test.tsx +1 -1
  49. package/src/backend/__tests__/DataTable.extensions.test.tsx +30 -0
  50. package/src/backend/__tests__/DataTable.perspectiveReconcile.test.tsx +320 -2
  51. package/src/backend/__tests__/DevRuntimeDiagnosticsBanner.test.tsx +117 -0
  52. package/src/backend/__tests__/NotesSection.test.tsx +138 -0
  53. package/src/backend/__tests__/RowActions.test.tsx +202 -0
  54. package/src/backend/charts/KpiCard.tsx +1 -0
  55. package/src/backend/charts/__tests__/KpiCard.test.tsx +20 -0
  56. package/src/backend/detail/AttachmentsSection.tsx +17 -5
  57. package/src/backend/detail/NotesSection.tsx +122 -66
  58. package/src/backend/dev/DevRuntimeDiagnosticsBanner.tsx +24 -1
  59. package/src/backend/icons/lucideRegistry.generated.tsx +26 -0
  60. package/src/backend/injection/__tests__/eventBridge.test.tsx +47 -1
  61. package/src/backend/injection/eventBridge.ts +35 -0
  62. package/src/backend/inputs/LookupSelect.tsx +32 -3
  63. package/src/backend/inputs/PhoneNumberField.tsx +1 -1
  64. package/src/backend/inputs/TagsInput.tsx +24 -3
  65. package/src/backend/inputs/__tests__/LookupSelect.test.tsx +166 -1
  66. package/src/backend/inputs/__tests__/PhoneNumberField.typeahead.test.tsx +43 -0
  67. package/src/backend/inputs/__tests__/TagsInput.test.tsx +81 -0
  68. package/src/backend/messages/EmailThreadsPanel.tsx +21 -1
  69. package/src/backend/messages/MessageComposer.tsx +1 -0
  70. package/src/backend/messages/__tests__/EmailThreadsPanel.senderLabel.test.tsx +99 -0
  71. package/src/backend/messages/__tests__/MessageComposer.test.tsx +44 -0
  72. package/src/backend/messages/__tests__/useMessageCompose.test.tsx +69 -0
  73. package/src/backend/messages/__tests__/useMessageComposeSender.test.tsx +208 -0
  74. package/src/backend/messages/index.ts +1 -0
  75. package/src/backend/messages/message-compose-form-groups.tsx +41 -0
  76. package/src/backend/messages/message-composer.types.ts +26 -0
  77. package/src/backend/messages/useMessageCompose.ts +46 -0
  78. package/src/backend/messages/useMessageComposeOperations.ts +8 -0
  79. package/src/backend/notifications/NotificationItem.tsx +7 -0
  80. package/src/backend/notifications/__tests__/NotificationItem.customRenderer.test.tsx +139 -0
  81. package/src/portal/PortalShell.tsx +7 -4
  82. package/src/portal/__tests__/PortalShell.test.tsx +29 -1
  83. package/src/primitives/__tests__/calendar.test.tsx +174 -1
  84. 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": "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",
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.7296.1.2111d779db",
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.7296.1.2111d779db",
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.7296.1.2111d779db",
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",
@@ -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. CrudForm hydrates
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 (injectedFieldIdSet.has(fieldId)) continue
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, injectedFieldIdSet, cfFields])
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 injectedFieldIdSet) {
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
- injectedFieldIdSet,
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 injectedFieldIdSet) {
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 injectedFieldIdSet) {
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
- export function DataTable<T extends RowData>({
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
- ? { ...normalized, filters: getCurrentSettings().filters }
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 snapshot: PerspectiveSnapshot = { perspectiveId: nextId, settings: normalized, updatedAt: Date.now() }
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
- // When the host page wired an advanced-filter tree (`advancedFilter.onApplyTree`),
2183
- // the host owns filter persistence — typically by hydrating from / writing to the
2184
- // URL (see CRM People/Companies/Deals lazy useState initializers + URL writer
2185
- // effects). The mount-time snapshot from a prior session can be arbitrarily
2186
- // stale and MUST NOT override the host's filter — including the empty case
2187
- // (Clear all → refresh would otherwise resurrect the previously-saved rules).
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: !!advancedFilter?.onApplyTree,
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: !!advancedFilter?.onApplyTree } : undefined,
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: !!advancedFilter?.onApplyTree,
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
- btnRef.current?.focus()
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
- onClick={(e) => { e.stopPropagation(); setOpen(true); requestAnimationFrame(updatePosition) }}
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
- className="fixed w-44 max-w-[calc(100vw-1rem)] rounded-md border bg-background p-1 shadow focus-visible:outline-none z-dropdown"
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
- className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-destructive' : ''}`}
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()