@open-mercato/ui 0.8.1-develop.7207.1.901ef7e47a → 0.8.1-develop.7218.1.9f22882799

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.
@@ -1,3 +1,3 @@
1
1
  Generated lucide registry with 169 icons -> /home/runner/work/open-mercato/open-mercato/packages/ui/src/backend/icons/lucideRegistry.generated.tsx
2
- [build:ui] found 449 entry points
2
+ [build:ui] found 450 entry points
3
3
  [build:ui] built successfully
@@ -8,6 +8,7 @@ import {
8
8
  TimePicker as TimePickerPrimitive,
9
9
  formatTimePickerDisplay
10
10
  } from "../../primitives/time-picker.js";
11
+ import { useTimeDisplayFormat } from "../../primitives/date-locale.js";
11
12
  function currentHHMM() {
12
13
  const now = /* @__PURE__ */ new Date();
13
14
  const hour = String(now.getHours()).padStart(2, "0");
@@ -27,16 +28,19 @@ function TimePicker({
27
28
  // button already exits the popover without committing, which is what users mean
28
29
  // by "Clear" in most flows. Pass `showClearButton={true}` to opt back in when
29
30
  // you need an explicit "set value to null" action distinct from "dismiss".
30
- showClearButton = false
31
+ showClearButton = false,
32
+ format
31
33
  }) {
32
34
  const t = useT();
35
+ const localeFormat = useTimeDisplayFormat();
36
+ const timeFormat = format ?? localeFormat;
33
37
  const [open, setOpen] = React.useState(false);
34
38
  const placeholderText = placeholder ?? t("ui.timePicker.placeholder", "Pick a time");
35
39
  const nowText = t("ui.timePicker.nowButton", "Now");
36
40
  const clearText = t("ui.timePicker.clearButton", "Clear");
37
41
  const isInteractive = !disabled && !readOnly;
38
42
  const displayValue = value ? (() => {
39
- const { main, suffix } = formatTimePickerDisplay(value, "12h");
43
+ const { main, suffix } = formatTimePickerDisplay(value, timeFormat);
40
44
  return suffix ? `${main} ${suffix}` : main;
41
45
  })() : null;
42
46
  const triggerButton = /* @__PURE__ */ jsxs(
@@ -67,7 +71,7 @@ function TimePicker({
67
71
  const pinnedTopActions = [];
68
72
  if (showNowButton) {
69
73
  const nowValue = currentHHMM();
70
- const { main: nowMain, suffix: nowSuffix } = formatTimePickerDisplay(nowValue, "12h");
74
+ const { main: nowMain, suffix: nowSuffix } = formatTimePickerDisplay(nowValue, timeFormat);
71
75
  pinnedTopActions.push({
72
76
  label: nowText,
73
77
  icon: /* @__PURE__ */ jsx(ClockIcon, { "aria-hidden": "true" }),
@@ -98,6 +102,7 @@ function TimePicker({
98
102
  onChange(next);
99
103
  },
100
104
  intervalMinutes: Math.max(1, minuteStep),
105
+ format: timeFormat,
101
106
  showHeader: false,
102
107
  showFooter: true,
103
108
  headerPlaceholder: placeholderText,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/inputs/TimePicker.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ClockIcon } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n TimePicker as TimePickerPrimitive,\n formatTimePickerDisplay,\n} from '../../primitives/time-picker'\n\nexport type TimePickerProps = {\n value?: string | null\n onChange: (time: string | null) => void\n placeholder?: string\n disabled?: boolean\n readOnly?: boolean\n className?: string\n minuteStep?: number\n showNowButton?: boolean\n showClearButton?: boolean\n}\n\nfunction currentHHMM(): string {\n const now = new Date()\n const hour = String(now.getHours()).padStart(2, '0')\n const minute = String(now.getMinutes()).padStart(2, '0')\n return `${hour}:${minute}`\n}\n\n/**\n * Legacy popover-anchored time picker. Preserved for backward compatibility with\n * existing CrudForm `type: 'time'` consumers and DateTimePicker. Internally a shim\n * over the new `<TimePicker>` primitive from `@open-mercato/ui/primitives/time-picker`.\n *\n * `minuteStep` maps to `intervalMinutes` (default 30 for sane slot list size).\n * `showNowButton` / `showClearButton` render as `legacyFooterActions`.\n */\nexport function TimePicker({\n value,\n onChange,\n placeholder,\n disabled = false,\n readOnly = false,\n className,\n minuteStep = 30,\n showNowButton = true,\n // Default changed 2026-05-11 from `true` \u2192 `false`: the new primitive's Cancel\n // button already exits the popover without committing, which is what users mean\n // by \"Clear\" in most flows. Pass `showClearButton={true}` to opt back in when\n // you need an explicit \"set value to null\" action distinct from \"dismiss\".\n showClearButton = false,\n}: TimePickerProps) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n\n const placeholderText = placeholder ?? t('ui.timePicker.placeholder', 'Pick a time')\n const nowText = t('ui.timePicker.nowButton', 'Now')\n const clearText = t('ui.timePicker.clearButton', 'Clear')\n const isInteractive = !disabled && !readOnly\n\n // Render the trigger label in the same 12h \"HH:MM AM/PM\" format as the slot list\n // inside the popover so users see one consistent representation.\n const displayValue = value\n ? (() => {\n const { main, suffix } = formatTimePickerDisplay(value, '12h')\n return suffix ? `${main} ${suffix}` : main\n })()\n : null\n\n const triggerButton = (\n <button\n type=\"button\"\n data-crud-focus-target=\"\"\n disabled={disabled}\n aria-haspopup=\"dialog\"\n className={cn(\n 'w-full h-10 flex items-center gap-2 rounded-md border bg-background px-3 text-sm text-left transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed',\n readOnly && 'cursor-default opacity-70',\n !value && 'text-muted-foreground',\n className,\n )}\n onClick={!isInteractive ? (event) => event.preventDefault() : undefined}\n >\n <ClockIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex-1 truncate\">{displayValue ?? placeholderText}</span>\n </button>\n )\n\n if (!isInteractive) {\n return triggerButton\n }\n\n const pinnedTopActions: Array<{\n label: string\n onClick: () => void\n icon?: React.ReactNode\n rightText?: string\n }> = []\n if (showNowButton) {\n const nowValue = currentHHMM()\n const { main: nowMain, suffix: nowSuffix } = formatTimePickerDisplay(nowValue, '12h')\n pinnedTopActions.push({\n label: nowText,\n icon: <ClockIcon aria-hidden=\"true\" />,\n rightText: nowSuffix ? `${nowMain} ${nowSuffix}` : nowMain,\n onClick: () => {\n onChange(nowValue)\n setOpen(false)\n },\n })\n }\n\n const legacyFooterActions: Array<{ label: string; onClick: () => void; variant: 'link' | 'muted' }> = []\n if (showClearButton) {\n legacyFooterActions.push({\n label: clearText,\n onClick: () => {\n onChange(null)\n setOpen(false)\n },\n variant: 'muted',\n })\n }\n\n return (\n <TimePickerPrimitive\n value={value ?? null}\n onChange={(next) => onChange(next)}\n onApply={(next) => {\n onChange(next)\n }}\n intervalMinutes={Math.max(1, minuteStep)}\n // Trigger button already displays current value \u2014 don't duplicate it\n // inside the popover header.\n showHeader={false}\n showFooter\n headerPlaceholder={placeholderText}\n pinnedTopActions={pinnedTopActions.length > 0 ? pinnedTopActions : undefined}\n legacyFooterActions={legacyFooterActions.length > 0 ? legacyFooterActions : undefined}\n trigger={triggerButton}\n open={open}\n onOpenChange={setOpen}\n disabled={disabled}\n />\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ClockIcon } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n TimePicker as TimePickerPrimitive,\n formatTimePickerDisplay,\n type TimeFormat,\n} from '../../primitives/time-picker'\nimport { useTimeDisplayFormat } from '../../primitives/date-locale'\n\nexport type TimePickerProps = {\n value?: string | null\n onChange: (time: string | null) => void\n placeholder?: string\n disabled?: boolean\n readOnly?: boolean\n className?: string\n minuteStep?: number\n showNowButton?: boolean\n showClearButton?: boolean\n /**\n * Clock convention for the trigger label and the slot list. Defaults to the one the active\n * locale writes time in \u2014 `12h` for English and Korean, `24h` for Polish, German and Spanish \u2014\n * so a tenant does not read an AM/PM clock its language never uses. Pass an explicit value to\n * pin one regardless of locale.\n */\n format?: TimeFormat\n}\n\nfunction currentHHMM(): string {\n const now = new Date()\n const hour = String(now.getHours()).padStart(2, '0')\n const minute = String(now.getMinutes()).padStart(2, '0')\n return `${hour}:${minute}`\n}\n\n/**\n * Legacy popover-anchored time picker. Preserved for backward compatibility with\n * existing CrudForm `type: 'time'` consumers and DateTimePicker. Internally a shim\n * over the new `<TimePicker>` primitive from `@open-mercato/ui/primitives/time-picker`.\n *\n * `minuteStep` maps to `intervalMinutes` (default 30 for sane slot list size).\n * `showNowButton` / `showClearButton` render as `legacyFooterActions`.\n */\nexport function TimePicker({\n value,\n onChange,\n placeholder,\n disabled = false,\n readOnly = false,\n className,\n minuteStep = 30,\n showNowButton = true,\n // Default changed 2026-05-11 from `true` \u2192 `false`: the new primitive's Cancel\n // button already exits the popover without committing, which is what users mean\n // by \"Clear\" in most flows. Pass `showClearButton={true}` to opt back in when\n // you need an explicit \"set value to null\" action distinct from \"dismiss\".\n showClearButton = false,\n format,\n}: TimePickerProps) {\n const t = useT()\n const localeFormat = useTimeDisplayFormat()\n const timeFormat = format ?? localeFormat\n const [open, setOpen] = React.useState(false)\n\n const placeholderText = placeholder ?? t('ui.timePicker.placeholder', 'Pick a time')\n const nowText = t('ui.timePicker.nowButton', 'Now')\n const clearText = t('ui.timePicker.clearButton', 'Clear')\n const isInteractive = !disabled && !readOnly\n\n // Render the trigger label in the same format as the slot list inside the popover\n // so users see one consistent representation.\n const displayValue = value\n ? (() => {\n const { main, suffix } = formatTimePickerDisplay(value, timeFormat)\n return suffix ? `${main} ${suffix}` : main\n })()\n : null\n\n const triggerButton = (\n <button\n type=\"button\"\n data-crud-focus-target=\"\"\n disabled={disabled}\n aria-haspopup=\"dialog\"\n className={cn(\n 'w-full h-10 flex items-center gap-2 rounded-md border bg-background px-3 text-sm text-left transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed',\n readOnly && 'cursor-default opacity-70',\n !value && 'text-muted-foreground',\n className,\n )}\n onClick={!isInteractive ? (event) => event.preventDefault() : undefined}\n >\n <ClockIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex-1 truncate\">{displayValue ?? placeholderText}</span>\n </button>\n )\n\n if (!isInteractive) {\n return triggerButton\n }\n\n const pinnedTopActions: Array<{\n label: string\n onClick: () => void\n icon?: React.ReactNode\n rightText?: string\n }> = []\n if (showNowButton) {\n const nowValue = currentHHMM()\n const { main: nowMain, suffix: nowSuffix } = formatTimePickerDisplay(nowValue, timeFormat)\n pinnedTopActions.push({\n label: nowText,\n icon: <ClockIcon aria-hidden=\"true\" />,\n rightText: nowSuffix ? `${nowMain} ${nowSuffix}` : nowMain,\n onClick: () => {\n onChange(nowValue)\n setOpen(false)\n },\n })\n }\n\n const legacyFooterActions: Array<{ label: string; onClick: () => void; variant: 'link' | 'muted' }> = []\n if (showClearButton) {\n legacyFooterActions.push({\n label: clearText,\n onClick: () => {\n onChange(null)\n setOpen(false)\n },\n variant: 'muted',\n })\n }\n\n return (\n <TimePickerPrimitive\n value={value ?? null}\n onChange={(next) => onChange(next)}\n onApply={(next) => {\n onChange(next)\n }}\n intervalMinutes={Math.max(1, minuteStep)}\n format={timeFormat}\n // Trigger button already displays current value \u2014 don't duplicate it\n // inside the popover header.\n showHeader={false}\n showFooter\n headerPlaceholder={placeholderText}\n pinnedTopActions={pinnedTopActions.length > 0 ? pinnedTopActions : undefined}\n legacyFooterActions={legacyFooterActions.length > 0 ? legacyFooterActions : undefined}\n trigger={triggerButton}\n open={open}\n onOpenChange={setOpen}\n disabled={disabled}\n />\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -24,6 +24,8 @@ function CalendarMonthSelector({
24
24
  onNextMonth ? /* @__PURE__ */ jsx(CompactButton, { appearance: "white", size: 24, className: "shadow-xs", onClick: onNextMonth, disabled: disabledNext, "aria-label": t("ui.calendar.nextMonth", "Next month"), children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-5", "aria-hidden": "true" }) }) : /* @__PURE__ */ jsx("span", { className: "size-6 shrink-0", "aria-hidden": "true" })
25
25
  ] });
26
26
  }
27
+ const STANDALONE_MONTH_YEAR = "LLLL yyyy";
28
+ const STANDALONE_MONTH_SHORT = "LLL";
27
29
  const navButtonClassName = cn(
28
30
  "h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0",
29
31
  "border border-border bg-background text-muted-foreground transition-colors",
@@ -38,7 +40,7 @@ function MonthNavButton({
38
40
  const dayPicker = useDayPicker();
39
41
  const target = direction === "prev" ? dayPicker.previousMonth : dayPicker.nextMonth;
40
42
  const Icon = direction === "prev" ? ChevronLeft : ChevronRight;
41
- const targetLabel = format(target ?? /* @__PURE__ */ new Date(), "MMMM yyyy", locale ? { locale } : void 0);
43
+ const targetLabel = format(target ?? /* @__PURE__ */ new Date(), STANDALONE_MONTH_YEAR, locale ? { locale } : void 0);
42
44
  const ariaLabel = `Go to ${direction === "prev" ? "previous" : "next"} month: ${targetLabel}`;
43
45
  return /* @__PURE__ */ jsx(
44
46
  "button",
@@ -59,7 +61,7 @@ function buildMonthCaption(locale, totalMonths, onOpenMonthGrid) {
59
61
  calendarMonth,
60
62
  displayIndex
61
63
  }) {
62
- const label = format(calendarMonth.date, "MMMM yyyy", locale ? { locale } : void 0);
64
+ const label = format(calendarMonth.date, STANDALONE_MONTH_YEAR, locale ? { locale } : void 0);
63
65
  const index = typeof displayIndex === "number" ? displayIndex : 0;
64
66
  const showPrev = index === 0;
65
67
  const showNext = index === totalMonths - 1;
@@ -103,7 +105,7 @@ function MonthGrid({
103
105
  const monthLabels = React.useMemo(
104
106
  () => Array.from(
105
107
  { length: 12 },
106
- (_, monthIndex) => format(new Date(year, monthIndex, 1), "MMM", locale ? { locale } : void 0)
108
+ (_, monthIndex) => format(new Date(year, monthIndex, 1), STANDALONE_MONTH_SHORT, locale ? { locale } : void 0)
107
109
  ),
108
110
  [year, locale]
109
111
  );
@@ -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\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(), 'MMMM yyyy', 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, 'MMMM yyyy', 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), 'MMM', 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": ";AA4BI,SAE4L,KAF5L;AA1BJ,YAAY,WAAW;AACvB,SAAS,WAAW,oBAAoB;AAExC,SAAS,aAAa,oBAAoB;AAC1C,SAAS,cAAc;AAEvB,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,qBAAqB;AAavB,SAAS,sBAAsB;AAAA,EACpC;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAiB;AAAA,EAAa;AAAA,EAAkB;AAAA,EAAc;AAAA,EAAW,GAAG;AAC7F,GAA+B;AAC7B,QAAM,IAAI,KAAK;AACf,SACE,qBAAC,SAAI,aAAU,2BAA0B,WAAW,GAAG,2DAA2D,SAAS,GAAI,GAAG,OAC/H;AAAA,sBACG,oBAAC,iBAAc,YAAW,SAAQ,MAAM,IAAI,WAAU,aAAY,SAAS,iBAAiB,UAAU,kBAAkB,cAAY,EAAE,6BAA6B,gBAAgB,GAAG,8BAAC,eAAY,WAAU,UAAS,eAAY,QAAO,GAAE,IAC3O,oBAAC,UAAK,WAAU,mBAAkB,eAAY,QAAO;AAAA,IACzD,oBAAC,UAAK,WAAU,wEAAuE,aAAU,UAAU,iBAAO,OAAO,aAAa,SAAS,EAAE,OAAO,IAAI,MAAS,GAAE;AAAA,IACtK,cACG,oBAAC,iBAAc,YAAW,SAAQ,MAAM,IAAI,WAAU,aAAY,SAAS,aAAa,UAAU,cAAc,cAAY,EAAE,yBAAyB,YAAY,GAAG,8BAAC,gBAAa,WAAU,UAAS,eAAY,QAAO,GAAE,IAC5N,oBAAC,UAAK,WAAU,mBAAkB,eAAY,QAAO;AAAA,KAC3D;AAEJ;AAEA,MAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AACF,GAGG;AACD,QAAM,YAAY,aAAa;AAK/B,QAAM,SAAS,cAAc,SAAS,UAAU,gBAAgB,UAAU;AAC1E,QAAM,OAAO,cAAc,SAAS,cAAc;AAClD,QAAM,cAAc,OAAO,UAAU,oBAAI,KAAK,GAAG,aAAa,SAAS,EAAE,OAAO,IAAI,MAAS;AAC7F,QAAM,YAAY,SAAS,cAAc,SAAS,aAAa,MAAM,WAAW,WAAW;AAC3F,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,UAAU,CAAC;AAAA,MACX,cAAY;AAAA,MACZ,SAAS,MAAM;AACb,YAAI,UAAU,UAAU,UAAW,WAAU,UAAU,MAAM;AAAA,MAC/D;AAAA,MACA,WAAW;AAAA,MAEX,8BAAC,QAAK,WAAU,WAAU,eAAY,QAAO;AAAA;AAAA,EAC/C;AAEJ;AAEA,SAAS,kBACP,QACA,aACA,iBACA;AACA,SAAO,SAAS,aAAa;AAAA,IAC3B;AAAA,IACA;AAAA,EACF,GAGG;AACD,UAAM,QAAQ,OAAO,cAAc,MAAM,aAAa,SAAS,EAAE,OAAO,IAAI,MAAS;AACrF,UAAM,QAAQ,OAAO,iBAAiB,WAAW,eAAe;AAKhE,UAAM,WAAW,UAAU;AAC3B,UAAM,WAAW,UAAU,cAAc;AAGzC,UAAM,mBAAmB,oBAAoB,QAAQ,UAAU;AAC/D,WACE,qBAAC,SAAI,WAAU,gDACZ;AAAA,iBACC,oBAAC,kBAAe,WAAU,QAAO,QAAgB,IAEjD,oBAAC,SAAI,WAAU,oBAAmB,eAAY,QAAO;AAAA,MAEtD,mBACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,mBAAmB;AAAA,UAC5B,cAAY,GAAG,KAAK;AAAA,UACpB,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,aAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MAED,WACC,oBAAC,kBAAe,WAAU,QAAO,QAAgB,IAEjD,oBAAC,SAAI,WAAU,oBAAmB,eAAY,QAAO;AAAA,OAEzD;AAAA,EAEJ;AACF;AAEA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,WAAW;AAClD,QAAM,QAAQ,oBAAI,KAAK;AACvB,QAAM,cAAc,MAAM;AAAA,IACxB,MACE,MAAM;AAAA,MAAK,EAAE,QAAQ,GAAG;AAAA,MAAG,CAAC,GAAG,eAC7B,OAAO,IAAI,KAAK,MAAM,YAAY,CAAC,GAAG,OAAO,SAAS,EAAE,OAAO,IAAI,MAAS;AAAA,IAC9E;AAAA,IACF,CAAC,MAAM,MAAM;AAAA,EACf;AACA,QAAM,YAAY,OAAO,IAAI,KAAK,MAAM,GAAG,CAAC,GAAG,QAAQ,SAAS,EAAE,OAAO,IAAI,MAAS;AACtF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAW;AAAA,MAEX;AAAA,6BAAC,SAAI,WAAU,gDACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,cAAY,wBAAwB,OAAO,CAAC;AAAA,cAC5C,SAAS,MAAM,QAAQ,CAAC,YAAY,UAAU,CAAC;AAAA,cAC/C,WAAW;AAAA,cAEX,8BAAC,eAAY,WAAU,WAAU,eAAY,QAAO;AAAA;AAAA,UACtD;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,cAAY,GAAG,SAAS;AAAA,cACxB,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,cAAY,oBAAoB,OAAO,CAAC;AAAA,cACxC,SAAS,MAAM,QAAQ,CAAC,YAAY,UAAU,CAAC;AAAA,cAC/C,WAAW;AAAA,cAEX,8BAAC,gBAAa,WAAU,WAAU,eAAY,QAAO;AAAA;AAAA,UACvD;AAAA,WACF;AAAA,QACA,oBAAC,SAAI,WAAU,0BACZ,sBAAY,IAAI,CAAC,YAAY,eAAe;AAC3C,gBAAM,aACJ,cAAc,YAAY,MAAM,QAAQ,cAAc,SAAS,MAAM;AACvE,gBAAM,iBACJ,MAAM,YAAY,MAAM,QAAQ,MAAM,SAAS,MAAM;AACvD,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,gBAAc;AAAA,cACd,SAAS,MAAM,cAAc,IAAI,KAAK,MAAM,YAAY,CAAC,CAAC;AAAA,cAC1D,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,kBAAkB,CAAC,cAAc;AAAA,gBACjC,cAAc;AAAA,cAChB;AAAA,cAEC;AAAA;AAAA,YAbI;AAAA,UAcP;AAAA,QAEJ,CAAC,GACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,UAAU;AAAA,EACV,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAGhB,QAAM,mBAAmB,mBAAmB;AAC5C,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM;AAAA,IAC5C,MAAM,SAAS,gBAAgB,oBAAI,KAAK;AAAA,EAC1C;AACA,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAG9D,QAAM,UAAU,MAAM;AACpB,QAAI,MAAO,iBAAgB,KAAK;AAAA,EAClC,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,oBAAoB,MAAM;AAAA,IAC9B,CAAC,SAAe;AACd,sBAAgB,IAAI;AACpB,sBAAgB,IAAI;AAAA,IACtB;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,QAAM,oBAAoB,MAAM;AAAA,IAC9B,CAAC,SAAe;AACd,wBAAkB,IAAI;AACtB,uBAAiB,KAAK;AAAA,IACxB;AAAA,IACA,CAAC,iBAAiB;AAAA,EACpB;AAEA,QAAM,eAAe,MAAM;AAAA,IACzB,MACE;AAAA,MACE;AAAA,MACA;AAAA,MACA,mBAAmB,MAAM,iBAAiB,IAAI,IAAI;AAAA,IACpD;AAAA,IACF,CAAC,QAAQ,gBAAgB,gBAAgB;AAAA,EAC3C;AAEA,SACE,qBAAC,SAAI,WAAW,mBAAmB,aAAa,YAC9C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,oBAAoB,gBAAgB,wBAAwB;AAAA,QACvE,eAAa,oBAAoB,gBAAgB,OAAO;AAAA,QAExD;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,OAAO;AAAA,YACP,eAAe;AAAA,YACf,WAAW,GAAG,OAAO,SAAS;AAAA,YAC9B,YAAY;AAAA,cACV,QAAQ;AAAA,cACR,OAAO;AAAA,cACP,eAAe;AAAA,cACf,eAAe;AAAA,cACf,KAAK;AAAA,cACL,YAAY;AAAA,cACZ,UAAU;AAAA,cACV,SAAS,GAAG,wDAAwD,YAAY,KAAK,SAAS,KAAK;AAAA,cACnG,OAAO;AAAA,cACP,MAAM;AAAA,cACN,KAAK,GAAG,4EAA4E,YAAY,KAAK,YAAY,QAAQ;AAAA,cACzH,YAAY;AAAA,gBACV;AAAA,gBACA,YAAY,KAAK,YAAY;AAAA,gBAC7B;AAAA,gBACA;AAAA;AAAA;AAAA;AAAA,gBAIA;AAAA,gBACA;AAAA,cACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMA,UAAU;AAAA,gBACR;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,aAAa;AAAA,gBACX;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,cAAc;AAAA,gBACZ;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,cACP,SACE;AAAA,cACF,UAAU;AAAA,cACV,QAAQ;AAAA,cACR,GAAG;AAAA,YACL;AAAA,YACA,YAAY;AAAA,cACV,cAAc;AAAA,cACd,GAAG;AAAA,YACL;AAAA,YACC,GAAG;AAAA;AAAA,QACN;AAAA;AAAA,IACF;AAAA,IACC,oBAAoB,gBACnB;AAAA,MAAC;AAAA;AAAA,QACC,aAAa,aAAa,YAAY;AAAA,QACtC,eAAe;AAAA,QACf;AAAA,QACA,eAAe;AAAA,QACf,SAAS,MAAM,iBAAiB,KAAK;AAAA;AAAA,IACvC,IACE;AAAA,KACN;AAEJ;",
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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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { de } from "date-fns/locale/de";
3
+ import { enUS } from "date-fns/locale/en-US";
4
+ import { es } from "date-fns/locale/es";
5
+ import { ko } from "date-fns/locale/ko";
6
+ import { pl } from "date-fns/locale/pl";
7
+ import { useOptionalLocale } from "@open-mercato/shared/lib/i18n/context";
8
+ const DATE_FNS_LOCALES = {
9
+ de,
10
+ en: enUS,
11
+ es,
12
+ ko,
13
+ pl
14
+ };
15
+ function resolveDateFnsLocale(locale) {
16
+ const code = locale?.split("-")[0]?.toLowerCase();
17
+ return code ? DATE_FNS_LOCALES[code] : void 0;
18
+ }
19
+ function useDateFnsLocale() {
20
+ return resolveDateFnsLocale(useOptionalLocale());
21
+ }
22
+ const timeFormatCache = /* @__PURE__ */ new Map();
23
+ function deriveTimeDisplayFormat(locale) {
24
+ if (!locale) return "12h";
25
+ const cached = timeFormatCache.get(locale);
26
+ if (cached) return cached;
27
+ let resolved = "12h";
28
+ try {
29
+ if (new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions().hour12 === false) {
30
+ resolved = "24h";
31
+ }
32
+ } catch {
33
+ resolved = "12h";
34
+ }
35
+ timeFormatCache.set(locale, resolved);
36
+ return resolved;
37
+ }
38
+ function useTimeDisplayFormat() {
39
+ return deriveTimeDisplayFormat(useOptionalLocale());
40
+ }
41
+ export {
42
+ deriveTimeDisplayFormat,
43
+ resolveDateFnsLocale,
44
+ useDateFnsLocale,
45
+ useTimeDisplayFormat
46
+ };
47
+ //# sourceMappingURL=date-locale.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/primitives/date-locale.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nimport type { Locale as DateFnsLocale } from 'date-fns/locale'\nimport { de } from 'date-fns/locale/de'\nimport { enUS } from 'date-fns/locale/en-US'\nimport { es } from 'date-fns/locale/es'\nimport { ko } from 'date-fns/locale/ko'\nimport { pl } from 'date-fns/locale/pl'\nimport { useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'\n\nexport type TimeDisplayFormat = '12h' | '24h'\n\n/**\n * The app locale is a bare language code (`'pl'`), but `react-day-picker` and every\n * date-fns formatter want a `Locale` object \u2014 the thing that knows month names and,\n * critically, `weekStartsOn`. Without one the picker falls back to the bundled `enUS`:\n * English month/weekday names and a Sunday-first week for every tenant.\n *\n * The five entries mirror `locales` in `@open-mercato/shared/lib/i18n/config`. They are\n * imported statically rather than loaded on demand because a picker renders its calendar\n * synchronously \u2014 an async locale would flash an English month header first.\n */\nconst DATE_FNS_LOCALES: Record<string, DateFnsLocale> = {\n de,\n en: enUS,\n es,\n ko,\n pl,\n}\n\n/**\n * The date-fns locale for an app locale code, or `undefined` when there is none to apply.\n *\n * `undefined` rather than `enUS` for an unknown code: the consumers pass this straight into\n * a `locale?: Locale` prop, and leaving it unset keeps their own documented default instead\n * of pinning English on a deployment that added a locale this map has not caught up with.\n */\nexport function resolveDateFnsLocale(locale?: string | null): DateFnsLocale | undefined {\n const code = locale?.split('-')[0]?.toLowerCase()\n return code ? DATE_FNS_LOCALES[code] : undefined\n}\n\n/** The date-fns locale for the active `I18nProvider` locale, or `undefined` outside a provider. */\nexport function useDateFnsLocale(): DateFnsLocale | undefined {\n return resolveDateFnsLocale(useOptionalLocale())\n}\n\n/**\n * Whether a locale writes the time of day on a 12- or 24-hour clock.\n *\n * Asked of `Intl` rather than a \"not English means 24h\" heuristic, because that heuristic is\n * wrong for two of the five locales this repo ships: Korean is a 12-hour locale (\uC624\uC804/\uC624\uD6C4), as\n * is English, while Polish, German and Spanish are not. `12h` is the fallback when `Intl` cannot\n * answer, matching the `TimePicker` primitive's own default.\n */\n// Constructing an `Intl.DateTimeFormat` costs ~40x using one, and this runs on every render of\n// every time picker \u2014 so the answer is kept per locale, exactly as `date-format.ts` caches its\n// formatters. The key space is the locales a deployment ships and the answer never changes.\nconst timeFormatCache = new Map<string, TimeDisplayFormat>()\n\nexport function deriveTimeDisplayFormat(locale?: string | null): TimeDisplayFormat {\n if (!locale) return '12h'\n const cached = timeFormatCache.get(locale)\n if (cached) return cached\n let resolved: TimeDisplayFormat = '12h'\n try {\n if (new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12 === false) {\n resolved = '24h'\n }\n } catch {\n resolved = '12h'\n }\n timeFormatCache.set(locale, resolved)\n return resolved\n}\n\n/** The clock convention of the active `I18nProvider` locale; `12h` outside a provider. */\nexport function useTimeDisplayFormat(): TimeDisplayFormat {\n return deriveTimeDisplayFormat(useOptionalLocale())\n}\n"],
5
+ "mappings": ";AAGA,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,UAAU;AACnB,SAAS,UAAU;AACnB,SAAS,yBAAyB;AAclC,MAAM,mBAAkD;AAAA,EACtD;AAAA,EACA,IAAI;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AACF;AASO,SAAS,qBAAqB,QAAmD;AACtF,QAAM,OAAO,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG,YAAY;AAChD,SAAO,OAAO,iBAAiB,IAAI,IAAI;AACzC;AAGO,SAAS,mBAA8C;AAC5D,SAAO,qBAAqB,kBAAkB,CAAC;AACjD;AAaA,MAAM,kBAAkB,oBAAI,IAA+B;AAEpD,SAAS,wBAAwB,QAA2C;AACjF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,gBAAgB,IAAI,MAAM;AACzC,MAAI,OAAQ,QAAO;AACnB,MAAI,WAA8B;AAClC,MAAI;AACF,QAAI,IAAI,KAAK,eAAe,QAAQ,EAAE,MAAM,UAAU,CAAC,EAAE,gBAAgB,EAAE,WAAW,OAAO;AAC3F,iBAAW;AAAA,IACb;AAAA,EACF,QAAQ;AACN,eAAW;AAAA,EACb;AACA,kBAAgB,IAAI,QAAQ,QAAQ;AACpC,SAAO;AACT;AAGO,SAAS,uBAA0C;AACxD,SAAO,wBAAwB,kBAAkB,CAAC;AACpD;",
6
+ "names": []
7
+ }
@@ -10,6 +10,7 @@ import { Button } from "./button.js";
10
10
  import { LinkButton } from "./link-button.js";
11
11
  import { TimeInput } from "../backend/inputs/TimeInput.js";
12
12
  import { formatWithPublicDateFormat, resolvePublicDateFormat, resolvePublicDateTimeFormat } from "./date-format.js";
13
+ import { useDateFnsLocale } from "./date-locale.js";
13
14
  function extractTime(date) {
14
15
  return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
15
16
  }
@@ -44,7 +45,7 @@ function DatePicker({
44
45
  align = "start",
45
46
  minDate,
46
47
  maxDate,
47
- locale,
48
+ locale: localeProp,
48
49
  displayFormat,
49
50
  className,
50
51
  popoverClassName,
@@ -56,6 +57,8 @@ function DatePicker({
56
57
  }) {
57
58
  const resolvedCloseOnSelect = closeOnSelect ?? footer === "today-clear";
58
59
  const t = useT();
60
+ const appLocale = useDateFnsLocale();
61
+ const locale = localeProp ?? appLocale;
59
62
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
60
63
  const open = controlledOpen ?? uncontrolledOpen;
61
64
  const setOpen = React.useCallback((next) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/date-picker.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { Locale } from 'date-fns/locale'\nimport { CalendarIcon } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Popover, PopoverContent, PopoverTrigger } from './popover'\nimport { Calendar } from './calendar'\nimport { Button } from './button'\nimport { LinkButton } from './link-button'\nimport { TimeInput } from '../backend/inputs/TimeInput'\nimport { formatWithPublicDateFormat, resolvePublicDateFormat, resolvePublicDateTimeFormat } from './date-format'\n\nexport type DatePickerFooter = 'apply-cancel' | 'today-clear' | 'none'\n\nexport type DatePickerProps = {\n value?: Date | null\n onChange: (value: Date | null) => void\n placeholder?: string\n size?: 'sm' | 'default'\n disabled?: boolean\n readOnly?: boolean\n /** Controlled open state of the popover. Omit for the default uncontrolled\n * behavior. Pair with `onOpenChange` to drive it externally (e.g. closing the\n * picker when a surrounding dialog scrolls). */\n open?: boolean\n onOpenChange?: (open: boolean) => void\n /**\n * Footer mode in the popover.\n * - `'apply-cancel'` (default, Figma-aligned): selecting a day stages a draft;\n * Apply commits and closes; Cancel reverts and closes.\n * - `'today-clear'`: legacy behavior \u2014 Today commits today, Clear sets null.\n * - `'none'`: no footer; selecting a day commits immediately and closes.\n */\n footer?: DatePickerFooter\n /**\n * Whether selecting a day commits the value and closes the popover.\n * - In `footer='apply-cancel'`: ignored (commit happens via Apply).\n * - In `footer='today-clear'`: default `true` for backward compatibility.\n * - In `footer='none'`: always `true` (popover always closes after selection).\n */\n closeOnSelect?: boolean\n /**\n * Whether to show the \"Today\" link in `footer='today-clear'` mode. Default `true`.\n * Only meaningful when `footer='today-clear'`.\n */\n showTodayButton?: boolean\n /**\n * Whether to show the \"Clear\" link in `footer='today-clear'` mode. Default `true`.\n * Only meaningful when `footer='today-clear'`.\n */\n showClearButton?: boolean\n withTime?: boolean\n minuteStep?: number\n align?: 'start' | 'center' | 'end'\n minDate?: Date\n maxDate?: Date\n locale?: Locale\n displayFormat?: string\n className?: string\n popoverClassName?: string\n id?: string\n name?: string\n required?: boolean\n 'aria-label'?: string\n 'aria-describedby'?: string\n}\n\nfunction extractTime(date: Date): string {\n return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`\n}\n\nfunction applyTimeToDate(base: Date, time: string): Date {\n const [hourStr = '0', minuteStr = '0'] = time.split(':')\n const hour = parseInt(hourStr, 10)\n const minute = parseInt(minuteStr, 10)\n const next = new Date(base)\n next.setHours(isNaN(hour) ? 0 : hour, isNaN(minute) ? 0 : minute, 0, 0)\n return next\n}\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nexport function DatePicker({\n value,\n onChange,\n placeholder,\n size = 'default',\n disabled = false,\n readOnly = false,\n open: controlledOpen,\n onOpenChange,\n footer = 'apply-cancel',\n closeOnSelect,\n showTodayButton = true,\n showClearButton = true,\n withTime = false,\n minuteStep = 1,\n align = 'start',\n minDate,\n maxDate,\n locale,\n displayFormat,\n className,\n popoverClassName,\n id,\n name,\n required,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n}: DatePickerProps) {\n const resolvedCloseOnSelect = closeOnSelect ?? footer === 'today-clear'\n const t = useT()\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)\n const open = controlledOpen ?? uncontrolledOpen\n const setOpen = React.useCallback((next: boolean) => {\n if (controlledOpen === undefined) setUncontrolledOpen(next)\n onOpenChange?.(next)\n }, [controlledOpen, onOpenChange])\n const [draft, setDraft] = React.useState<Date | null>(value ?? null)\n\n const isInteractive = !disabled && !readOnly\n const useDraft = footer === 'apply-cancel'\n\n React.useEffect(() => {\n if (open) setDraft(value ?? null)\n }, [open, value])\n\n const resolvedFormat = withTime\n ? resolvePublicDateTimeFormat(locale, displayFormat)\n : resolvePublicDateFormat(locale, displayFormat)\n const placeholderText = placeholder ?? (\n withTime\n ? t('ui.dateTimePicker.placeholder', 'Pick date and time')\n : t('ui.datePicker.placeholder', 'Pick a date')\n )\n\n const formattedValue = React.useMemo(() => {\n if (!value) return null\n return formatWithPublicDateFormat(value, resolvedFormat, locale)\n }, [value, resolvedFormat, locale])\n\n const handleDaySelect = React.useCallback(\n (day: Date | undefined) => {\n if (!day || !isInteractive) return\n const reference = withTime ? (useDraft ? draft : value) : null\n const base = withTime\n ? applyTimeToDate(day, reference ? extractTime(reference) : '00:00')\n : startOfDay(day)\n if (useDraft) {\n setDraft(base)\n } else {\n onChange(base)\n if (footer === 'none' || resolvedCloseOnSelect) setOpen(false)\n }\n },\n [isInteractive, withTime, useDraft, draft, value, footer, resolvedCloseOnSelect, onChange, setOpen],\n )\n\n const handleTimeChange = React.useCallback(\n (time: string) => {\n const target = useDraft ? draft : value\n if (!target) return\n const next = applyTimeToDate(target, time)\n if (useDraft) setDraft(next)\n else onChange(next)\n },\n [useDraft, draft, value, onChange],\n )\n\n const handleApply = React.useCallback(() => {\n onChange(draft)\n setOpen(false)\n }, [draft, onChange, setOpen])\n\n const handleCancel = React.useCallback(() => {\n setDraft(value ?? null)\n setOpen(false)\n }, [value, setOpen])\n\n const handleToday = React.useCallback(() => {\n const today = withTime ? new Date() : startOfDay(new Date())\n onChange(today)\n setOpen(false)\n }, [withTime, onChange, setOpen])\n\n const handleClear = React.useCallback(() => {\n onChange(null)\n setOpen(false)\n }, [onChange, setOpen])\n\n const disabledMatcher = React.useMemo(() => {\n if (!minDate && !maxDate) return undefined\n const matchers: import('react-day-picker').Matcher[] = []\n if (minDate) matchers.push({ before: minDate })\n if (maxDate) matchers.push({ after: maxDate })\n return matchers\n }, [minDate, maxDate])\n\n const heightClass = size === 'sm' ? 'h-8 px-2.5 text-xs' : 'h-9 px-3 text-sm'\n const selectedDate = useDraft ? draft : value\n\n return (\n <Popover open={open} onOpenChange={isInteractive ? setOpen : undefined}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n id={id}\n name={name}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedBy}\n aria-haspopup=\"dialog\"\n aria-required={required ? true : undefined}\n data-crud-focus-target=\"\"\n data-slot=\"date-picker-trigger\"\n disabled={disabled}\n className={cn(\n 'w-full inline-flex items-center gap-2 rounded-md border border-input bg-background shadow-xs transition-colors text-left',\n 'focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground',\n 'hover:bg-muted/40',\n 'disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled disabled:cursor-not-allowed',\n 'aria-invalid:border-destructive',\n heightClass,\n readOnly && 'cursor-default opacity-70',\n !formattedValue && 'text-muted-foreground',\n className,\n )}\n onClick={isInteractive ? undefined : (event) => event.preventDefault()}\n >\n <CalendarIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"flex-1 truncate\">\n {formattedValue ?? placeholderText}\n </span>\n </button>\n </PopoverTrigger>\n <PopoverContent align={align} className={cn('w-auto p-0', popoverClassName)}>\n <Calendar\n mode=\"single\"\n selected={selectedDate ?? undefined}\n defaultMonth={selectedDate ?? undefined}\n onSelect={handleDaySelect}\n locale={locale}\n disabled={disabledMatcher}\n autoFocus\n />\n {withTime && (\n <div className=\"flex items-center gap-2 border-t px-3 py-2\">\n <span className=\"text-sm text-muted-foreground shrink-0\">\n {t('ui.datePicker.timeLabel', 'Time')}:\n </span>\n <TimeInput\n value={selectedDate ? extractTime(selectedDate) : undefined}\n onChange={handleTimeChange}\n minuteStep={minuteStep}\n disabled={!isInteractive || !selectedDate}\n />\n </div>\n )}\n {footer === 'apply-cancel' && (\n <div className=\"flex items-center justify-end gap-2 border-t px-3 py-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleCancel}>\n {t('ui.datePicker.cancelButton', 'Cancel')}\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={handleApply}>\n {t('ui.datePicker.applyButton', 'Apply')}\n </Button>\n </div>\n )}\n {footer === 'today-clear' && (showTodayButton || showClearButton) && (\n <div className=\"flex items-center justify-between gap-2 border-t px-3 py-2\">\n {showTodayButton ? (\n <LinkButton type=\"button\" variant=\"primary\" onClick={handleToday}>\n {t('ui.datePicker.todayButton', 'Today')}\n </LinkButton>\n ) : null}\n {showClearButton ? (\n <LinkButton type=\"button\" variant=\"gray\" onClick={handleClear} className=\"ml-auto\">\n {t('ui.datePicker.clearButton', 'Clear')}\n </LinkButton>\n ) : null}\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { Locale } from 'date-fns/locale'\nimport { CalendarIcon } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Popover, PopoverContent, PopoverTrigger } from './popover'\nimport { Calendar } from './calendar'\nimport { Button } from './button'\nimport { LinkButton } from './link-button'\nimport { TimeInput } from '../backend/inputs/TimeInput'\nimport { formatWithPublicDateFormat, resolvePublicDateFormat, resolvePublicDateTimeFormat } from './date-format'\nimport { useDateFnsLocale } from './date-locale'\n\nexport type DatePickerFooter = 'apply-cancel' | 'today-clear' | 'none'\n\nexport type DatePickerProps = {\n value?: Date | null\n onChange: (value: Date | null) => void\n placeholder?: string\n size?: 'sm' | 'default'\n disabled?: boolean\n readOnly?: boolean\n /** Controlled open state of the popover. Omit for the default uncontrolled\n * behavior. Pair with `onOpenChange` to drive it externally (e.g. closing the\n * picker when a surrounding dialog scrolls). */\n open?: boolean\n onOpenChange?: (open: boolean) => void\n /**\n * Footer mode in the popover.\n * - `'apply-cancel'` (default, Figma-aligned): selecting a day stages a draft;\n * Apply commits and closes; Cancel reverts and closes.\n * - `'today-clear'`: legacy behavior \u2014 Today commits today, Clear sets null.\n * - `'none'`: no footer; selecting a day commits immediately and closes.\n */\n footer?: DatePickerFooter\n /**\n * Whether selecting a day commits the value and closes the popover.\n * - In `footer='apply-cancel'`: ignored (commit happens via Apply).\n * - In `footer='today-clear'`: default `true` for backward compatibility.\n * - In `footer='none'`: always `true` (popover always closes after selection).\n */\n closeOnSelect?: boolean\n /**\n * Whether to show the \"Today\" link in `footer='today-clear'` mode. Default `true`.\n * Only meaningful when `footer='today-clear'`.\n */\n showTodayButton?: boolean\n /**\n * Whether to show the \"Clear\" link in `footer='today-clear'` mode. Default `true`.\n * Only meaningful when `footer='today-clear'`.\n */\n showClearButton?: boolean\n withTime?: boolean\n minuteStep?: number\n align?: 'start' | 'center' | 'end'\n minDate?: Date\n maxDate?: Date\n /**\n * date-fns locale for month/weekday names and the first day of the week.\n * Defaults to the active `I18nProvider` locale (`undefined` outside a\n * provider, matching pre-existing behavior); pass one explicitly to pin it\n * regardless of the app locale.\n */\n locale?: Locale\n displayFormat?: string\n className?: string\n popoverClassName?: string\n id?: string\n name?: string\n required?: boolean\n 'aria-label'?: string\n 'aria-describedby'?: string\n}\n\nfunction extractTime(date: Date): string {\n return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`\n}\n\nfunction applyTimeToDate(base: Date, time: string): Date {\n const [hourStr = '0', minuteStr = '0'] = time.split(':')\n const hour = parseInt(hourStr, 10)\n const minute = parseInt(minuteStr, 10)\n const next = new Date(base)\n next.setHours(isNaN(hour) ? 0 : hour, isNaN(minute) ? 0 : minute, 0, 0)\n return next\n}\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nexport function DatePicker({\n value,\n onChange,\n placeholder,\n size = 'default',\n disabled = false,\n readOnly = false,\n open: controlledOpen,\n onOpenChange,\n footer = 'apply-cancel',\n closeOnSelect,\n showTodayButton = true,\n showClearButton = true,\n withTime = false,\n minuteStep = 1,\n align = 'start',\n minDate,\n maxDate,\n locale: localeProp,\n displayFormat,\n className,\n popoverClassName,\n id,\n name,\n required,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n}: DatePickerProps) {\n const resolvedCloseOnSelect = closeOnSelect ?? footer === 'today-clear'\n const t = useT()\n // Callers may pin a locale explicitly (e.g. a field-level override); absent\n // that, fall back to the active app locale so every consumer of this\n // primitive \u2014 not just the ones that remember to thread a `locale` prop \u2014\n // renders a localized month/weekday grid instead of silently defaulting to\n // English. Outside an `I18nProvider` this resolves to `undefined`, same as\n // not passing a locale at all today.\n const appLocale = useDateFnsLocale()\n const locale = localeProp ?? appLocale\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)\n const open = controlledOpen ?? uncontrolledOpen\n const setOpen = React.useCallback((next: boolean) => {\n if (controlledOpen === undefined) setUncontrolledOpen(next)\n onOpenChange?.(next)\n }, [controlledOpen, onOpenChange])\n const [draft, setDraft] = React.useState<Date | null>(value ?? null)\n\n const isInteractive = !disabled && !readOnly\n const useDraft = footer === 'apply-cancel'\n\n React.useEffect(() => {\n if (open) setDraft(value ?? null)\n }, [open, value])\n\n const resolvedFormat = withTime\n ? resolvePublicDateTimeFormat(locale, displayFormat)\n : resolvePublicDateFormat(locale, displayFormat)\n const placeholderText = placeholder ?? (\n withTime\n ? t('ui.dateTimePicker.placeholder', 'Pick date and time')\n : t('ui.datePicker.placeholder', 'Pick a date')\n )\n\n const formattedValue = React.useMemo(() => {\n if (!value) return null\n return formatWithPublicDateFormat(value, resolvedFormat, locale)\n }, [value, resolvedFormat, locale])\n\n const handleDaySelect = React.useCallback(\n (day: Date | undefined) => {\n if (!day || !isInteractive) return\n const reference = withTime ? (useDraft ? draft : value) : null\n const base = withTime\n ? applyTimeToDate(day, reference ? extractTime(reference) : '00:00')\n : startOfDay(day)\n if (useDraft) {\n setDraft(base)\n } else {\n onChange(base)\n if (footer === 'none' || resolvedCloseOnSelect) setOpen(false)\n }\n },\n [isInteractive, withTime, useDraft, draft, value, footer, resolvedCloseOnSelect, onChange, setOpen],\n )\n\n const handleTimeChange = React.useCallback(\n (time: string) => {\n const target = useDraft ? draft : value\n if (!target) return\n const next = applyTimeToDate(target, time)\n if (useDraft) setDraft(next)\n else onChange(next)\n },\n [useDraft, draft, value, onChange],\n )\n\n const handleApply = React.useCallback(() => {\n onChange(draft)\n setOpen(false)\n }, [draft, onChange, setOpen])\n\n const handleCancel = React.useCallback(() => {\n setDraft(value ?? null)\n setOpen(false)\n }, [value, setOpen])\n\n const handleToday = React.useCallback(() => {\n const today = withTime ? new Date() : startOfDay(new Date())\n onChange(today)\n setOpen(false)\n }, [withTime, onChange, setOpen])\n\n const handleClear = React.useCallback(() => {\n onChange(null)\n setOpen(false)\n }, [onChange, setOpen])\n\n const disabledMatcher = React.useMemo(() => {\n if (!minDate && !maxDate) return undefined\n const matchers: import('react-day-picker').Matcher[] = []\n if (minDate) matchers.push({ before: minDate })\n if (maxDate) matchers.push({ after: maxDate })\n return matchers\n }, [minDate, maxDate])\n\n const heightClass = size === 'sm' ? 'h-8 px-2.5 text-xs' : 'h-9 px-3 text-sm'\n const selectedDate = useDraft ? draft : value\n\n return (\n <Popover open={open} onOpenChange={isInteractive ? setOpen : undefined}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n id={id}\n name={name}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedBy}\n aria-haspopup=\"dialog\"\n aria-required={required ? true : undefined}\n data-crud-focus-target=\"\"\n data-slot=\"date-picker-trigger\"\n disabled={disabled}\n className={cn(\n 'w-full inline-flex items-center gap-2 rounded-md border border-input bg-background shadow-xs transition-colors text-left',\n 'focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground',\n 'hover:bg-muted/40',\n 'disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled disabled:cursor-not-allowed',\n 'aria-invalid:border-destructive',\n heightClass,\n readOnly && 'cursor-default opacity-70',\n !formattedValue && 'text-muted-foreground',\n className,\n )}\n onClick={isInteractive ? undefined : (event) => event.preventDefault()}\n >\n <CalendarIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"flex-1 truncate\">\n {formattedValue ?? placeholderText}\n </span>\n </button>\n </PopoverTrigger>\n <PopoverContent align={align} className={cn('w-auto p-0', popoverClassName)}>\n <Calendar\n mode=\"single\"\n selected={selectedDate ?? undefined}\n defaultMonth={selectedDate ?? undefined}\n onSelect={handleDaySelect}\n locale={locale}\n disabled={disabledMatcher}\n autoFocus\n />\n {withTime && (\n <div className=\"flex items-center gap-2 border-t px-3 py-2\">\n <span className=\"text-sm text-muted-foreground shrink-0\">\n {t('ui.datePicker.timeLabel', 'Time')}:\n </span>\n <TimeInput\n value={selectedDate ? extractTime(selectedDate) : undefined}\n onChange={handleTimeChange}\n minuteStep={minuteStep}\n disabled={!isInteractive || !selectedDate}\n />\n </div>\n )}\n {footer === 'apply-cancel' && (\n <div className=\"flex items-center justify-end gap-2 border-t px-3 py-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleCancel}>\n {t('ui.datePicker.cancelButton', 'Cancel')}\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={handleApply}>\n {t('ui.datePicker.applyButton', 'Apply')}\n </Button>\n </div>\n )}\n {footer === 'today-clear' && (showTodayButton || showClearButton) && (\n <div className=\"flex items-center justify-between gap-2 border-t px-3 py-2\">\n {showTodayButton ? (\n <LinkButton type=\"button\" variant=\"primary\" onClick={handleToday}>\n {t('ui.datePicker.todayButton', 'Today')}\n </LinkButton>\n ) : null}\n {showClearButton ? (\n <LinkButton type=\"button\" variant=\"gray\" onClick={handleClear} className=\"ml-auto\">\n {t('ui.datePicker.clearButton', 'Clear')}\n </LinkButton>\n ) : null}\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/time-picker.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, CircleCheck, ChevronLeft, ChevronRight, Clock, X } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from './button'\nimport { Popover, PopoverContent, PopoverTrigger } from './popover'\n\nexport type TimePickerValue = string | null\n\nexport type TimePickerStatusVariant = 'available' | 'busy' | 'in-meeting' | 'offline'\n\ntype SlotInteractiveState = 'default' | 'hover' | 'active' | 'disabled'\ntype DurationInteractiveState = SlotInteractiveState\ntype StatusInteractiveState = 'default' | 'hover' | 'selected' | 'disabled'\n\ntype TimeFormat = '12h' | '24h'\n\ntype LegacyFooterAction = {\n label: string\n onClick: () => void\n variant?: 'link' | 'muted'\n}\n\ntype PinnedTopAction = {\n label: string\n onClick: () => void\n icon?: React.ReactNode\n /** Optional secondary text rendered right-aligned (e.g. the current time next to `Now`). */\n rightText?: string\n}\n\ntype DurationOption = {\n value: number\n label?: string\n disabled?: boolean\n}\n\ntype StatusOption = {\n variant: TimePickerStatusVariant\n label?: string\n disabled?: boolean\n}\n\nfunction padTwo(input: number): string {\n return String(input).padStart(2, '0')\n}\n\nfunction parseTime(value: string | null | undefined): { hour: number; minute: number } | null {\n if (!value) return null\n const parts = value.split(':')\n if (parts.length < 2) return null\n const hour = parseInt(parts[0] ?? '0', 10)\n const minute = parseInt(parts[1] ?? '0', 10)\n if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null\n return {\n hour: Math.max(0, Math.min(23, hour)),\n minute: Math.max(0, Math.min(59, minute)),\n }\n}\n\nfunction formatSlotDisplay(value: string, format: TimeFormat): { main: string; suffix: string | null } {\n const parsed = parseTime(value)\n if (!parsed) return { main: value, suffix: null }\n if (format === '24h') {\n return { main: `${padTwo(parsed.hour)}:${padTwo(parsed.minute)}`, suffix: null }\n }\n const hour12 = parsed.hour === 0 ? 12 : parsed.hour > 12 ? parsed.hour - 12 : parsed.hour\n const suffix = parsed.hour < 12 ? 'AM' : 'PM'\n return { main: `${padTwo(hour12)}:${padTwo(parsed.minute)}`, suffix }\n}\n\nfunction generateSlots(startTime: string, endTime: string, intervalMinutes: number): string[] {\n const start = parseTime(startTime)\n const end = parseTime(endTime)\n if (!start || !end) return []\n const step = intervalMinutes > 0 ? intervalMinutes : 30\n const out: string[] = []\n const startTotal = start.hour * 60 + start.minute\n const endTotal = end.hour * 60 + end.minute\n if (endTotal < startTotal) return []\n for (let m = startTotal; m <= endTotal; m += step) {\n const h = Math.floor(m / 60)\n const min = m % 60\n out.push(`${padTwo(h)}:${padTwo(min)}`)\n }\n return out\n}\n\n/**\n * Format an HH:MM 24h string for display, splitting into the main digits and an\n * optional AM/PM suffix. Exported so consumers (e.g. the legacy `backend/inputs/TimePicker`\n * shim) can render their trigger label in the same format as the slot list.\n */\nexport function formatTimePickerDisplay(\n value: string,\n format: '12h' | '24h' = '12h',\n): { main: string; suffix: string | null } {\n return formatSlotDisplay(value, format)\n}\n\nexport function formatDuration(minutes: number, options?: { short?: boolean }): string {\n const short = options?.short ?? true\n if (!Number.isFinite(minutes) || minutes <= 0) return short ? '0 min' : '0 minutes'\n if (minutes < 60) return short ? `${minutes} min` : `${minutes} minutes`\n if (minutes % (24 * 60) === 0) {\n const days = minutes / (24 * 60)\n return short ? (days === 1 ? '1 day' : `${days} days`) : days === 1 ? '1 day' : `${days} days`\n }\n if (minutes % 60 === 0) {\n const hours = minutes / 60\n return short ? (hours === 1 ? '1 hour' : `${hours} hours`) : hours === 1 ? '1 hour' : `${hours} hours`\n }\n const hours = Math.floor(minutes / 60)\n const rest = minutes - hours * 60\n return short\n ? `${hours}h ${rest}m`\n : `${hours} hour${hours === 1 ? '' : 's'} ${rest} minute${rest === 1 ? '' : 's'}`\n}\n\nconst defaultStatusLabels: Record<TimePickerStatusVariant, string> = {\n available: 'Available',\n busy: 'Busy',\n 'in-meeting': 'In meeting',\n offline: 'Offline',\n}\n\nconst statusColorMap: Record<\n TimePickerStatusVariant,\n { dot: string; bgSoft: string; border: string; text: string }\n> = {\n available: {\n dot: 'bg-status-success-icon',\n bgSoft: 'bg-status-success-bg',\n border: 'border-status-success-border',\n text: 'text-status-success-text',\n },\n busy: {\n dot: 'bg-status-error-icon',\n bgSoft: 'bg-status-error-bg',\n border: 'border-status-error-border',\n text: 'text-status-error-text',\n },\n 'in-meeting': {\n dot: 'bg-status-warning-icon',\n bgSoft: 'bg-status-warning-bg',\n border: 'border-status-warning-border',\n text: 'text-status-warning-text',\n },\n offline: {\n dot: 'bg-muted-foreground',\n bgSoft: 'bg-muted',\n border: 'border-border',\n text: 'text-muted-foreground',\n },\n}\n\nconst slotVariants = cva(\n 'flex h-9 w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors text-left',\n {\n variants: {\n state: {\n default: 'bg-background hover:bg-muted/50 text-muted-foreground cursor-pointer',\n hover: 'bg-muted/50 text-muted-foreground cursor-pointer',\n active: 'bg-muted text-foreground cursor-pointer',\n disabled: 'bg-background text-muted-foreground/40 cursor-not-allowed',\n },\n },\n defaultVariants: { state: 'default' },\n },\n)\n\nexport type TimePickerSlotProps = {\n value: string\n label?: string\n rightText?: string\n /** Position of the selection indicator between or after the two time labels. */\n checkPosition?: 'right' | 'center'\n state?: SlotInteractiveState\n selected?: boolean\n disabled?: boolean\n format?: TimeFormat\n onSelect?: (value: string) => void\n className?: string\n 'aria-label'?: string\n}\n\nexport const TimePickerSlot = React.forwardRef<HTMLButtonElement, TimePickerSlotProps>(\n (\n {\n value,\n label,\n rightText,\n checkPosition = 'right',\n state,\n selected,\n disabled,\n format = '12h',\n onSelect,\n className,\n 'aria-label': ariaLabel,\n },\n ref,\n ) => {\n const effectiveState: SlotInteractiveState = disabled\n ? 'disabled'\n : selected\n ? 'active'\n : (state ?? 'default')\n const display = label != null ? { main: label, suffix: null as string | null } : formatSlotDisplay(value, format)\n const right = rightText ? formatSlotDisplay(rightText, format) : null\n const isInteractive = effectiveState !== 'disabled'\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(slotVariants({ state: effectiveState }), className)}\n onClick={isInteractive ? () => onSelect?.(value) : undefined}\n disabled={!isInteractive}\n aria-label={ariaLabel ?? value}\n aria-pressed={effectiveState === 'active'}\n data-slot=\"time-picker-slot\"\n data-state={effectiveState}\n >\n <span className=\"flex min-w-0 flex-1 items-center gap-1\">\n <span>{display.main}</span>\n {display.suffix ? (\n <span className=\"text-muted-foreground\">\n {display.suffix}\n </span>\n ) : null}\n </span>\n {effectiveState === 'active' && checkPosition === 'center' ? (\n <CircleCheck data-slot=\"time-picker-slot-check\" className=\"size-3.5 shrink-0 fill-accent-indigo text-background\" aria-hidden=\"true\" />\n ) : null}\n {right ? (\n <span className={cn('flex min-w-0 items-center justify-end gap-1', checkPosition === 'center' && 'flex-1')}>\n <span>{right.main}</span>\n {right.suffix ? (\n <span className=\"text-muted-foreground\">\n {right.suffix}\n </span>\n ) : null}\n </span>\n ) : null}\n {effectiveState === 'active' && checkPosition === 'right' ? (\n <CircleCheck data-slot=\"time-picker-slot-check\" className=\"size-3.5 shrink-0 fill-accent-indigo text-background\" aria-hidden=\"true\" />\n ) : null}\n </button>\n )\n },\n)\nTimePickerSlot.displayName = 'TimePickerSlot'\n\nconst durationChipVariants = cva(\n 'inline-flex h-7 items-center justify-center gap-1.5 rounded-md px-2.5 text-sm font-medium transition-colors whitespace-nowrap',\n {\n variants: {\n state: {\n default:\n 'bg-background border border-border shadow-xs text-muted-foreground hover:bg-muted/40 cursor-pointer',\n hover: 'bg-muted/40 border border-border text-muted-foreground cursor-pointer',\n active: 'bg-status-info-bg text-status-info-text cursor-pointer',\n disabled:\n 'bg-background border border-border text-muted-foreground/40 cursor-not-allowed shadow-xs',\n },\n },\n defaultVariants: { state: 'default' },\n },\n)\n\nexport type TimePickerDurationChipProps = {\n value: number\n label?: string\n state?: DurationInteractiveState\n selected?: boolean\n disabled?: boolean\n onSelect?: (value: number) => void\n className?: string\n}\n\nexport const TimePickerDurationChip = React.forwardRef<HTMLButtonElement, TimePickerDurationChipProps>(\n (\n { value, label, state, selected, disabled, onSelect, className },\n ref,\n ) => {\n const effectiveState: DurationInteractiveState = disabled\n ? 'disabled'\n : selected\n ? 'active'\n : (state ?? 'default')\n const isInteractive = effectiveState !== 'disabled'\n const displayLabel = label ?? formatDuration(value)\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(durationChipVariants({ state: effectiveState }), className)}\n onClick={isInteractive ? () => onSelect?.(value) : undefined}\n disabled={!isInteractive}\n aria-pressed={effectiveState === 'active'}\n data-slot=\"time-picker-duration-chip\"\n data-state={effectiveState}\n >\n {effectiveState === 'active' ? (\n <Check className=\"size-3 shrink-0\" aria-hidden=\"true\" />\n ) : null}\n <span>{displayLabel}</span>\n </button>\n )\n },\n)\nTimePickerDurationChip.displayName = 'TimePickerDurationChip'\n\nconst statusChipBaseVariants = cva(\n 'inline-flex h-7 items-center justify-center gap-1 rounded-md pl-1.5 pr-2.5 text-sm font-medium transition-colors whitespace-nowrap border',\n {\n variants: {\n state: {\n default: '',\n hover: '',\n selected: '',\n disabled: 'cursor-not-allowed opacity-60',\n },\n },\n defaultVariants: { state: 'default' },\n },\n)\n\nexport type TimePickerStatusChipProps = {\n variant: TimePickerStatusVariant\n label?: string\n state?: StatusInteractiveState\n selected?: boolean\n disabled?: boolean\n icon?: React.ReactNode\n onSelect?: (variant: TimePickerStatusVariant) => void\n className?: string\n}\n\nexport const TimePickerStatusChip = React.forwardRef<HTMLButtonElement, TimePickerStatusChipProps>(\n (\n { variant, label, state, selected, disabled, icon, onSelect, className },\n ref,\n ) => {\n const effectiveState: StatusInteractiveState = disabled\n ? 'disabled'\n : selected\n ? 'selected'\n : (state ?? 'default')\n const colors = statusColorMap[variant]\n const isInteractive = effectiveState !== 'disabled'\n const displayLabel = label ?? defaultStatusLabels[variant]\n\n const stateClasses =\n effectiveState === 'selected'\n ? cn(colors.bgSoft, colors.border, colors.text)\n : effectiveState === 'hover'\n ? cn('bg-muted/30 border-border', colors.text, 'cursor-pointer')\n : effectiveState === 'disabled'\n ? cn('bg-background border-border', colors.text)\n : cn('bg-background border-border text-muted-foreground hover:bg-muted/30 cursor-pointer')\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(statusChipBaseVariants({ state: effectiveState }), stateClasses, className)}\n onClick={isInteractive ? () => onSelect?.(variant) : undefined}\n disabled={!isInteractive}\n aria-pressed={effectiveState === 'selected'}\n data-slot=\"time-picker-status-chip\"\n data-state={effectiveState}\n data-variant={variant}\n >\n {icon ?? (\n <span\n className={cn('inline-block size-1.5 shrink-0 rounded-full', colors.dot)}\n aria-hidden=\"true\"\n />\n )}\n <span>{displayLabel}</span>\n </button>\n )\n },\n)\nTimePickerStatusChip.displayName = 'TimePickerStatusChip'\n\n/**\n * Wraps a horizontally-scrollable row with prev/next arrow buttons that appear\n * only when overflow is present. Hides the native scrollbar and reserves padding\n * on each side proportional to the visible arrow so chips never sit underneath\n * the arrow button. Exported for consumers that need the same UX (e.g. inline\n * duration chip row inside a meeting/activity form).\n */\nexport function HorizontalScrollRow({\n className,\n contentClassName,\n children,\n ariaLabel,\n arrowSize = 'default',\n scrollLeftAriaLabel = 'Scroll left',\n scrollRightAriaLabel = 'Scroll right',\n}: {\n className?: string\n /** Override classes for the inner scroll container (e.g. tighter padding when inlined into a form row). */\n contentClassName?: string\n children: React.ReactNode\n ariaLabel?: string\n /** Arrow button size. `default` = 28px circle, `sm` = 24px (denser rows). */\n arrowSize?: 'default' | 'sm'\n /** Override the left arrow's aria-label. Default is English 'Scroll left'. */\n scrollLeftAriaLabel?: string\n /** Override the right arrow's aria-label. Default is English 'Scroll right'. */\n scrollRightAriaLabel?: string\n}) {\n const scrollRef = React.useRef<HTMLDivElement>(null)\n const [canScrollLeft, setCanScrollLeft] = React.useState(false)\n const [canScrollRight, setCanScrollRight] = React.useState(false)\n\n const refreshScrollState = React.useCallback(() => {\n const el = scrollRef.current\n if (!el) return\n const { scrollLeft, scrollWidth, clientWidth } = el\n setCanScrollLeft(scrollLeft > 1)\n setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1)\n }, [])\n\n React.useEffect(() => {\n refreshScrollState()\n const el = scrollRef.current\n if (!el) return\n el.addEventListener('scroll', refreshScrollState, { passive: true })\n const ro = new ResizeObserver(refreshScrollState)\n ro.observe(el)\n return () => {\n el.removeEventListener('scroll', refreshScrollState)\n ro.disconnect()\n }\n }, [refreshScrollState])\n\n const scrollBy = React.useCallback((direction: 'left' | 'right') => {\n const el = scrollRef.current\n if (!el) return\n const amount = Math.max(80, Math.floor(el.clientWidth * 0.6))\n el.scrollBy({ left: direction === 'left' ? -amount : amount, behavior: 'smooth' })\n }, [])\n\n const arrowButtonClass =\n arrowSize === 'sm' ? 'size-6 [&_svg]:size-3.5' : 'size-7 [&_svg]:size-4'\n\n return (\n <div className={cn('relative', className)} data-slot=\"time-picker-scroll-row\">\n {/* Outer gutter \u2014 reserves room for the absolute-positioned chevron buttons.\n The scroll viewport lives INSIDE this gutter so chips can never slide\n underneath the arrows \u2014 moving scroll offsets only shift content inside\n the viewport, not the chevron clearance. */}\n <div\n className={cn(\n 'transition-[padding]',\n canScrollLeft ? 'pl-9' : 'pl-4',\n canScrollRight ? 'pr-9' : 'pr-4',\n )}\n >\n <div\n ref={scrollRef}\n className={cn(\n 'scrollbar-hide flex items-center gap-2 overflow-x-auto py-3.5',\n contentClassName,\n )}\n role=\"group\"\n aria-label={ariaLabel}\n >\n {children}\n </div>\n </div>\n {/* Edge fades \u2014 soft gradient at the boundary between the gutter and the\n scroll viewport, so chips sliding into the viewport dissolve cleanly\n before reaching the arrow's edge. */}\n {canScrollLeft ? (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-y-0 left-9 w-6 bg-gradient-to-r from-background to-transparent\"\n />\n ) : null}\n {canScrollRight ? (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-y-0 right-9 w-6 bg-gradient-to-l from-background to-transparent\"\n />\n ) : null}\n {canScrollLeft ? (\n <button\n type=\"button\"\n onClick={() => scrollBy('left')}\n aria-label={scrollLeftAriaLabel}\n className={cn(\n 'absolute left-1 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n arrowButtonClass,\n )}\n data-slot=\"time-picker-scroll-left\"\n >\n <ChevronLeft aria-hidden=\"true\" />\n </button>\n ) : null}\n {canScrollRight ? (\n <button\n type=\"button\"\n onClick={() => scrollBy('right')}\n aria-label={scrollRightAriaLabel}\n className={cn(\n 'absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n arrowButtonClass,\n )}\n data-slot=\"time-picker-scroll-right\"\n >\n <ChevronRight aria-hidden=\"true\" />\n </button>\n ) : null}\n </div>\n )\n}\n\nfunction useControllable<T>(controlled: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [internal, setInternal] = React.useState<T>(defaultValue)\n const isControlled = controlled !== undefined\n const value = isControlled ? (controlled as T) : internal\n const setValue = React.useCallback(\n (next: T) => {\n if (!isControlled) setInternal(next)\n onChange?.(next)\n },\n [isControlled, onChange],\n )\n return [value, setValue] as const\n}\n\nexport type TimePickerProps = {\n value?: TimePickerValue\n defaultValue?: TimePickerValue\n onChange?: (value: TimePickerValue) => void\n\n showHeader?: boolean\n headerIcon?: React.ReactNode\n headerTitle?: string\n headerPlaceholder?: string\n onClose?: () => void\n\n durations?: DurationOption[]\n activeDuration?: number\n defaultActiveDuration?: number\n onDurationChange?: (value: number) => void\n\n statuses?: StatusOption[]\n activeStatus?: TimePickerStatusVariant\n defaultActiveStatus?: TimePickerStatusVariant\n onStatusChange?: (variant: TimePickerStatusVariant) => void\n statusLabel?: string\n\n slots?: string[]\n startTime?: string\n endTime?: string\n intervalMinutes?: number\n format?: TimeFormat\n slotRightText?: (slot: string) => string | undefined\n slotLabel?: string\n maxHeight?: number\n\n showFooter?: boolean\n cancelLabel?: string\n applyLabel?: string\n onCancel?: () => void\n onApply?: (value: TimePickerValue) => void\n legacyFooterActions?: LegacyFooterAction[]\n /**\n * Quick-action rows rendered above the scrollable slot list \u2014 used for\n * \"Now\"-style shortcuts that stay visible regardless of scroll position.\n * Styled like slots but with primary-tinted foreground.\n */\n pinnedTopActions?: PinnedTopAction[]\n\n trigger?: React.ReactNode\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n popoverAlign?: 'start' | 'center' | 'end'\n popoverSide?: 'top' | 'right' | 'bottom' | 'left'\n\n className?: string\n disabled?: boolean\n 'aria-label'?: string\n}\n\nfunction TimePickerCard({\n value,\n setValue,\n showHeader,\n headerIcon,\n headerTitle,\n headerPlaceholder,\n onClose,\n closeAriaLabel,\n format,\n durationOptions,\n activeDuration,\n setActiveDuration,\n onDurationChange,\n durationsAriaLabel,\n scrollLeftAriaLabel,\n scrollRightAriaLabel,\n statusOptions,\n activeStatus,\n setActiveStatus,\n onStatusChange,\n statusLabel,\n slotList,\n slotLabel,\n slotRightText,\n maxHeight,\n showFooter,\n cancelLabel,\n applyLabel,\n handleCancel,\n handleApply,\n legacyFooterActions,\n pinnedTopActions,\n disabled,\n className,\n ariaLabel,\n slotListRef,\n}: {\n value: TimePickerValue\n setValue: (next: TimePickerValue) => void\n showHeader: boolean\n headerIcon: React.ReactNode\n headerTitle: string | undefined\n headerPlaceholder: string\n onClose: (() => void) | undefined\n closeAriaLabel: string\n format: TimeFormat\n durationOptions: DurationOption[] | undefined\n activeDuration: number | undefined\n setActiveDuration: (next: number) => void\n onDurationChange: ((value: number) => void) | undefined\n durationsAriaLabel: string\n scrollLeftAriaLabel: string\n scrollRightAriaLabel: string\n statusOptions: StatusOption[] | undefined\n activeStatus: TimePickerStatusVariant | undefined\n setActiveStatus: (next: TimePickerStatusVariant) => void\n onStatusChange: ((variant: TimePickerStatusVariant) => void) | undefined\n statusLabel: string\n slotList: string[]\n slotLabel: string | undefined\n slotRightText: ((slot: string) => string | undefined) | undefined\n maxHeight: number\n showFooter: boolean\n cancelLabel: string\n applyLabel: string\n handleCancel: () => void\n handleApply: () => void\n legacyFooterActions: LegacyFooterAction[] | undefined\n pinnedTopActions: PinnedTopAction[] | undefined\n disabled: boolean\n className: string | undefined\n ariaLabel: string\n slotListRef: React.RefObject<HTMLDivElement | null>\n}) {\n const headerDisplay = React.useMemo(() => {\n if (headerTitle != null) return { main: headerTitle, suffix: null as string | null }\n if (value) return formatSlotDisplay(value, format)\n return { main: headerPlaceholder, suffix: null as string | null }\n }, [headerTitle, value, format, headerPlaceholder])\n\n const handleSlotKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return\n const target = event.target as HTMLElement\n const slotButton = target.closest<HTMLButtonElement>('[data-slot=\"time-picker-slot\"]')\n if (!slotButton) return\n const container = slotListRef.current\n if (!container) return\n const all = Array.from(\n container.querySelectorAll<HTMLButtonElement>('[data-slot=\"time-picker-slot\"]:not(:disabled)'),\n )\n const currentIndex = all.indexOf(slotButton)\n if (currentIndex === -1) return\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n const next = all[Math.min(all.length - 1, currentIndex + 1)]\n next?.focus()\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n const prev = all[Math.max(0, currentIndex - 1)]\n prev?.focus()\n } else if (event.key === 'Home') {\n event.preventDefault()\n all[0]?.focus()\n } else if (event.key === 'End') {\n event.preventDefault()\n all[all.length - 1]?.focus()\n }\n },\n [disabled, slotListRef],\n )\n\n return (\n <div\n className={cn(\n 'flex w-80 flex-col overflow-hidden rounded-xl border border-border bg-background shadow-xl',\n className,\n )}\n role=\"dialog\"\n aria-label={ariaLabel}\n aria-disabled={disabled || undefined}\n data-slot=\"time-picker\"\n >\n {showHeader ? (\n <div\n className=\"flex items-center gap-2 border-b border-border px-4 py-3.5\"\n data-slot=\"time-picker-header\"\n >\n <span className=\"flex shrink-0 items-center text-muted-foreground\" aria-hidden=\"true\">\n {headerIcon ?? <Clock className=\"size-5\" aria-hidden=\"true\" />}\n </span>\n <p className=\"flex-1 min-w-0 truncate text-base font-medium text-muted-foreground\">\n <span>{headerDisplay.main}</span>\n {headerDisplay.suffix ? <span className=\"ml-1 text-muted-foreground/60\">{headerDisplay.suffix}</span> : null}\n </p>\n {onClose ? (\n <button\n type=\"button\"\n onClick={onClose}\n disabled={disabled}\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40 disabled:pointer-events-none disabled:opacity-60\"\n aria-label={closeAriaLabel}\n data-slot=\"time-picker-close\"\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </button>\n ) : null}\n </div>\n ) : null}\n\n {durationOptions && durationOptions.length > 0 ? (\n <HorizontalScrollRow\n className=\"border-b border-border\"\n ariaLabel={durationsAriaLabel}\n scrollLeftAriaLabel={scrollLeftAriaLabel}\n scrollRightAriaLabel={scrollRightAriaLabel}\n >\n {durationOptions.map((option) => (\n <TimePickerDurationChip\n key={option.value}\n value={option.value}\n label={option.label}\n disabled={disabled || option.disabled}\n selected={activeDuration === option.value}\n onSelect={(next) => {\n setActiveDuration(next)\n onDurationChange?.(next)\n }}\n />\n ))}\n </HorizontalScrollRow>\n ) : null}\n\n {statusOptions && statusOptions.length > 0 ? (\n <div\n className=\"flex flex-col gap-2 border-b border-border px-4 py-3.5\"\n data-slot=\"time-picker-status-row\"\n >\n <p className=\"text-sm font-medium text-muted-foreground\">{statusLabel}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {statusOptions.map((option) => (\n <TimePickerStatusChip\n key={option.variant}\n variant={option.variant}\n label={option.label}\n disabled={disabled || option.disabled}\n selected={activeStatus === option.variant}\n onSelect={(next) => {\n setActiveStatus(next)\n onStatusChange?.(next)\n }}\n />\n ))}\n </div>\n </div>\n ) : null}\n\n {pinnedTopActions && pinnedTopActions.length > 0 ? (\n <div\n className=\"flex flex-col gap-0.5 border-b border-border p-2\"\n data-slot=\"time-picker-pinned-top\"\n >\n {pinnedTopActions.map((action) => (\n <button\n key={action.label}\n type=\"button\"\n onClick={action.onClick}\n disabled={disabled}\n className=\"flex h-9 w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/10 cursor-pointer disabled:pointer-events-none disabled:opacity-60\"\n data-slot=\"time-picker-pinned-action\"\n >\n {action.icon ? (\n <span className=\"flex shrink-0 items-center [&_svg]:size-4\" aria-hidden=\"true\">\n {action.icon}\n </span>\n ) : null}\n <span className=\"flex-1 text-left\">{action.label}</span>\n {action.rightText ? (\n <span className=\"text-primary/60\">{action.rightText}</span>\n ) : null}\n </button>\n ))}\n </div>\n ) : null}\n\n <div\n className=\"flex flex-col gap-0.5 overflow-y-auto p-2\"\n style={{ maxHeight }}\n ref={slotListRef}\n onKeyDown={handleSlotKeyDown}\n data-slot=\"time-picker-slots\"\n >\n {slotLabel ? (\n <p className=\"px-3 pt-1 pb-2 text-sm font-medium text-muted-foreground\">{slotLabel}</p>\n ) : null}\n {slotList.length === 0 ? (\n <p className=\"px-3 py-4 text-center text-sm text-muted-foreground\">No time slots available.</p>\n ) : (\n slotList.map((slot) => (\n <TimePickerSlot\n key={slot}\n value={slot}\n format={format}\n rightText={slotRightText?.(slot)}\n selected={value === slot}\n disabled={disabled}\n onSelect={(next) => setValue(next)}\n />\n ))\n )}\n </div>\n\n {showFooter ? (\n <div\n className=\"flex flex-wrap items-center justify-end gap-2 border-t border-border px-4 py-3\"\n data-slot=\"time-picker-footer\"\n >\n {legacyFooterActions && legacyFooterActions.length > 0 ? (\n <div className=\"mr-auto flex items-center gap-1\">\n {legacyFooterActions.map((action) => (\n <Button\n key={action.label}\n type=\"button\"\n variant={action.variant === 'muted' ? 'muted' : 'ghost'}\n size=\"sm\"\n onClick={action.onClick}\n disabled={disabled}\n data-slot=\"time-picker-legacy-action\"\n >\n {action.label}\n </Button>\n ))}\n <span aria-hidden=\"true\" className=\"ml-2 mr-1 h-5 w-px bg-border\" />\n </div>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={disabled}\n data-slot=\"time-picker-cancel\"\n >\n {cancelLabel}\n </Button>\n <Button\n type=\"button\"\n variant=\"default\"\n size=\"sm\"\n onClick={handleApply}\n disabled={disabled}\n data-slot=\"time-picker-apply\"\n >\n {applyLabel}\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\nexport function TimePicker({\n value: valueProp,\n defaultValue = null,\n onChange,\n showHeader = true,\n headerIcon,\n headerTitle,\n headerPlaceholder,\n onClose,\n durations,\n activeDuration: activeDurationProp,\n defaultActiveDuration,\n onDurationChange,\n statuses,\n activeStatus: activeStatusProp,\n defaultActiveStatus,\n onStatusChange,\n statusLabel,\n slots: slotsProp,\n startTime = '00:00',\n endTime = '23:30',\n intervalMinutes = 30,\n format = '12h',\n slotRightText,\n slotLabel,\n maxHeight = 280,\n showFooter = true,\n cancelLabel,\n applyLabel,\n onCancel,\n onApply,\n legacyFooterActions,\n pinnedTopActions,\n trigger,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n popoverAlign = 'start',\n popoverSide = 'bottom',\n className,\n disabled = false,\n 'aria-label': ariaLabel,\n}: TimePickerProps) {\n const t = useT()\n const resolvedHeaderPlaceholder = headerPlaceholder ?? t('ui.timePicker.placeholder', 'Pick a time')\n const resolvedStatusLabel = statusLabel ?? t('ui.timePicker.statusLabel', 'Select status')\n const resolvedCancelLabel = cancelLabel ?? t('ui.timePicker.cancelButton', 'Cancel')\n const resolvedApplyLabel = applyLabel ?? t('ui.timePicker.applyButton', 'Apply')\n const resolvedAriaLabel = ariaLabel ?? t('ui.timePicker.label', 'Time picker')\n const resolvedCloseAriaLabel = t('ui.timePicker.closeButton', 'Close')\n const resolvedDurationsAriaLabel = t('ui.timePicker.durationsRowLabel', 'Quick duration')\n const resolvedScrollLeftAriaLabel = t('ui.timePicker.scrollLeft', 'Scroll left')\n const resolvedScrollRightAriaLabel = t('ui.timePicker.scrollRight', 'Scroll right')\n const [value, setValue] = useControllable<TimePickerValue>(valueProp, defaultValue, onChange)\n const [activeDuration, setActiveDuration] = useControllable<number | undefined>(\n activeDurationProp,\n defaultActiveDuration,\n )\n const [activeStatus, setActiveStatus] = useControllable<TimePickerStatusVariant | undefined>(\n activeStatusProp,\n defaultActiveStatus,\n )\n const [open, setOpen] = useControllable<boolean>(openProp, defaultOpen, onOpenChange)\n\n const openSnapshotRef = React.useRef<TimePickerValue>(value)\n const slotListRef = React.useRef<HTMLDivElement | null>(null)\n\n const slotList = React.useMemo(() => {\n if (slotsProp && slotsProp.length > 0) return slotsProp\n return generateSlots(startTime, endTime, intervalMinutes)\n }, [slotsProp, startTime, endTime, intervalMinutes])\n\n const setOpenWithSnapshot = React.useCallback(\n (next: boolean) => {\n if (next && !open) {\n openSnapshotRef.current = value\n }\n setOpen(next)\n },\n [open, setOpen, value],\n )\n\n const handleApply = React.useCallback(() => {\n onApply?.(value)\n if (trigger) setOpenWithSnapshot(false)\n }, [onApply, setOpenWithSnapshot, trigger, value])\n\n const handleCancel = React.useCallback(() => {\n onCancel?.()\n if (trigger) {\n setValue(openSnapshotRef.current)\n setOpenWithSnapshot(false)\n }\n }, [onCancel, setOpenWithSnapshot, setValue, trigger])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Escape') {\n handleCancel()\n } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n handleApply()\n }\n },\n [handleApply, handleCancel],\n )\n\n const card = (\n <div onKeyDown={handleKeyDown}>\n <TimePickerCard\n value={value}\n setValue={setValue}\n showHeader={showHeader}\n headerIcon={headerIcon}\n headerTitle={headerTitle}\n headerPlaceholder={resolvedHeaderPlaceholder}\n onClose={onClose}\n closeAriaLabel={resolvedCloseAriaLabel}\n format={format}\n durationOptions={durations}\n activeDuration={activeDuration}\n setActiveDuration={setActiveDuration}\n onDurationChange={onDurationChange}\n durationsAriaLabel={resolvedDurationsAriaLabel}\n scrollLeftAriaLabel={resolvedScrollLeftAriaLabel}\n scrollRightAriaLabel={resolvedScrollRightAriaLabel}\n statusOptions={statuses}\n activeStatus={activeStatus}\n setActiveStatus={setActiveStatus}\n onStatusChange={onStatusChange}\n statusLabel={resolvedStatusLabel}\n slotList={slotList}\n slotLabel={slotLabel}\n slotRightText={slotRightText}\n maxHeight={maxHeight}\n showFooter={showFooter}\n cancelLabel={resolvedCancelLabel}\n applyLabel={resolvedApplyLabel}\n handleCancel={handleCancel}\n handleApply={handleApply}\n legacyFooterActions={legacyFooterActions}\n pinnedTopActions={pinnedTopActions}\n disabled={disabled}\n className={className}\n ariaLabel={resolvedAriaLabel}\n slotListRef={slotListRef}\n />\n </div>\n )\n\n if (!trigger) return card\n\n return (\n <Popover open={open} onOpenChange={setOpenWithSnapshot}>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n <PopoverContent\n align={popoverAlign}\n side={popoverSide}\n className=\"w-auto border-none bg-transparent p-0 shadow-none\"\n >\n {card}\n </PopoverContent>\n </Popover>\n )\n}\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, CircleCheck, ChevronLeft, ChevronRight, Clock, X } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from './button'\nimport { Popover, PopoverContent, PopoverTrigger } from './popover'\n\nexport type TimePickerValue = string | null\n\nexport type TimePickerStatusVariant = 'available' | 'busy' | 'in-meeting' | 'offline'\n\ntype SlotInteractiveState = 'default' | 'hover' | 'active' | 'disabled'\ntype DurationInteractiveState = SlotInteractiveState\ntype StatusInteractiveState = 'default' | 'hover' | 'selected' | 'disabled'\n\nexport type TimeFormat = '12h' | '24h'\n\ntype LegacyFooterAction = {\n label: string\n onClick: () => void\n variant?: 'link' | 'muted'\n}\n\ntype PinnedTopAction = {\n label: string\n onClick: () => void\n icon?: React.ReactNode\n /** Optional secondary text rendered right-aligned (e.g. the current time next to `Now`). */\n rightText?: string\n}\n\ntype DurationOption = {\n value: number\n label?: string\n disabled?: boolean\n}\n\ntype StatusOption = {\n variant: TimePickerStatusVariant\n label?: string\n disabled?: boolean\n}\n\nfunction padTwo(input: number): string {\n return String(input).padStart(2, '0')\n}\n\nfunction parseTime(value: string | null | undefined): { hour: number; minute: number } | null {\n if (!value) return null\n const parts = value.split(':')\n if (parts.length < 2) return null\n const hour = parseInt(parts[0] ?? '0', 10)\n const minute = parseInt(parts[1] ?? '0', 10)\n if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null\n return {\n hour: Math.max(0, Math.min(23, hour)),\n minute: Math.max(0, Math.min(59, minute)),\n }\n}\n\nfunction formatSlotDisplay(value: string, format: TimeFormat): { main: string; suffix: string | null } {\n const parsed = parseTime(value)\n if (!parsed) return { main: value, suffix: null }\n if (format === '24h') {\n return { main: `${padTwo(parsed.hour)}:${padTwo(parsed.minute)}`, suffix: null }\n }\n const hour12 = parsed.hour === 0 ? 12 : parsed.hour > 12 ? parsed.hour - 12 : parsed.hour\n const suffix = parsed.hour < 12 ? 'AM' : 'PM'\n return { main: `${padTwo(hour12)}:${padTwo(parsed.minute)}`, suffix }\n}\n\nfunction generateSlots(startTime: string, endTime: string, intervalMinutes: number): string[] {\n const start = parseTime(startTime)\n const end = parseTime(endTime)\n if (!start || !end) return []\n const step = intervalMinutes > 0 ? intervalMinutes : 30\n const out: string[] = []\n const startTotal = start.hour * 60 + start.minute\n const endTotal = end.hour * 60 + end.minute\n if (endTotal < startTotal) return []\n for (let m = startTotal; m <= endTotal; m += step) {\n const h = Math.floor(m / 60)\n const min = m % 60\n out.push(`${padTwo(h)}:${padTwo(min)}`)\n }\n return out\n}\n\n/**\n * Format an HH:MM 24h string for display, splitting into the main digits and an\n * optional AM/PM suffix. Exported so consumers (e.g. the legacy `backend/inputs/TimePicker`\n * shim) can render their trigger label in the same format as the slot list.\n */\nexport function formatTimePickerDisplay(\n value: string,\n format: '12h' | '24h' = '12h',\n): { main: string; suffix: string | null } {\n return formatSlotDisplay(value, format)\n}\n\nexport function formatDuration(minutes: number, options?: { short?: boolean }): string {\n const short = options?.short ?? true\n if (!Number.isFinite(minutes) || minutes <= 0) return short ? '0 min' : '0 minutes'\n if (minutes < 60) return short ? `${minutes} min` : `${minutes} minutes`\n if (minutes % (24 * 60) === 0) {\n const days = minutes / (24 * 60)\n return short ? (days === 1 ? '1 day' : `${days} days`) : days === 1 ? '1 day' : `${days} days`\n }\n if (minutes % 60 === 0) {\n const hours = minutes / 60\n return short ? (hours === 1 ? '1 hour' : `${hours} hours`) : hours === 1 ? '1 hour' : `${hours} hours`\n }\n const hours = Math.floor(minutes / 60)\n const rest = minutes - hours * 60\n return short\n ? `${hours}h ${rest}m`\n : `${hours} hour${hours === 1 ? '' : 's'} ${rest} minute${rest === 1 ? '' : 's'}`\n}\n\nconst defaultStatusLabels: Record<TimePickerStatusVariant, string> = {\n available: 'Available',\n busy: 'Busy',\n 'in-meeting': 'In meeting',\n offline: 'Offline',\n}\n\nconst statusColorMap: Record<\n TimePickerStatusVariant,\n { dot: string; bgSoft: string; border: string; text: string }\n> = {\n available: {\n dot: 'bg-status-success-icon',\n bgSoft: 'bg-status-success-bg',\n border: 'border-status-success-border',\n text: 'text-status-success-text',\n },\n busy: {\n dot: 'bg-status-error-icon',\n bgSoft: 'bg-status-error-bg',\n border: 'border-status-error-border',\n text: 'text-status-error-text',\n },\n 'in-meeting': {\n dot: 'bg-status-warning-icon',\n bgSoft: 'bg-status-warning-bg',\n border: 'border-status-warning-border',\n text: 'text-status-warning-text',\n },\n offline: {\n dot: 'bg-muted-foreground',\n bgSoft: 'bg-muted',\n border: 'border-border',\n text: 'text-muted-foreground',\n },\n}\n\nconst slotVariants = cva(\n 'flex h-9 w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors text-left',\n {\n variants: {\n state: {\n default: 'bg-background hover:bg-muted/50 text-muted-foreground cursor-pointer',\n hover: 'bg-muted/50 text-muted-foreground cursor-pointer',\n active: 'bg-muted text-foreground cursor-pointer',\n disabled: 'bg-background text-muted-foreground/40 cursor-not-allowed',\n },\n },\n defaultVariants: { state: 'default' },\n },\n)\n\nexport type TimePickerSlotProps = {\n value: string\n label?: string\n rightText?: string\n /** Position of the selection indicator between or after the two time labels. */\n checkPosition?: 'right' | 'center'\n state?: SlotInteractiveState\n selected?: boolean\n disabled?: boolean\n format?: TimeFormat\n onSelect?: (value: string) => void\n className?: string\n 'aria-label'?: string\n}\n\nexport const TimePickerSlot = React.forwardRef<HTMLButtonElement, TimePickerSlotProps>(\n (\n {\n value,\n label,\n rightText,\n checkPosition = 'right',\n state,\n selected,\n disabled,\n format = '12h',\n onSelect,\n className,\n 'aria-label': ariaLabel,\n },\n ref,\n ) => {\n const effectiveState: SlotInteractiveState = disabled\n ? 'disabled'\n : selected\n ? 'active'\n : (state ?? 'default')\n const display = label != null ? { main: label, suffix: null as string | null } : formatSlotDisplay(value, format)\n const right = rightText ? formatSlotDisplay(rightText, format) : null\n const isInteractive = effectiveState !== 'disabled'\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(slotVariants({ state: effectiveState }), className)}\n onClick={isInteractive ? () => onSelect?.(value) : undefined}\n disabled={!isInteractive}\n aria-label={ariaLabel ?? value}\n aria-pressed={effectiveState === 'active'}\n data-slot=\"time-picker-slot\"\n data-state={effectiveState}\n >\n <span className=\"flex min-w-0 flex-1 items-center gap-1\">\n <span>{display.main}</span>\n {display.suffix ? (\n <span className=\"text-muted-foreground\">\n {display.suffix}\n </span>\n ) : null}\n </span>\n {effectiveState === 'active' && checkPosition === 'center' ? (\n <CircleCheck data-slot=\"time-picker-slot-check\" className=\"size-3.5 shrink-0 fill-accent-indigo text-background\" aria-hidden=\"true\" />\n ) : null}\n {right ? (\n <span className={cn('flex min-w-0 items-center justify-end gap-1', checkPosition === 'center' && 'flex-1')}>\n <span>{right.main}</span>\n {right.suffix ? (\n <span className=\"text-muted-foreground\">\n {right.suffix}\n </span>\n ) : null}\n </span>\n ) : null}\n {effectiveState === 'active' && checkPosition === 'right' ? (\n <CircleCheck data-slot=\"time-picker-slot-check\" className=\"size-3.5 shrink-0 fill-accent-indigo text-background\" aria-hidden=\"true\" />\n ) : null}\n </button>\n )\n },\n)\nTimePickerSlot.displayName = 'TimePickerSlot'\n\nconst durationChipVariants = cva(\n 'inline-flex h-7 items-center justify-center gap-1.5 rounded-md px-2.5 text-sm font-medium transition-colors whitespace-nowrap',\n {\n variants: {\n state: {\n default:\n 'bg-background border border-border shadow-xs text-muted-foreground hover:bg-muted/40 cursor-pointer',\n hover: 'bg-muted/40 border border-border text-muted-foreground cursor-pointer',\n active: 'bg-status-info-bg text-status-info-text cursor-pointer',\n disabled:\n 'bg-background border border-border text-muted-foreground/40 cursor-not-allowed shadow-xs',\n },\n },\n defaultVariants: { state: 'default' },\n },\n)\n\nexport type TimePickerDurationChipProps = {\n value: number\n label?: string\n state?: DurationInteractiveState\n selected?: boolean\n disabled?: boolean\n onSelect?: (value: number) => void\n className?: string\n}\n\nexport const TimePickerDurationChip = React.forwardRef<HTMLButtonElement, TimePickerDurationChipProps>(\n (\n { value, label, state, selected, disabled, onSelect, className },\n ref,\n ) => {\n const effectiveState: DurationInteractiveState = disabled\n ? 'disabled'\n : selected\n ? 'active'\n : (state ?? 'default')\n const isInteractive = effectiveState !== 'disabled'\n const displayLabel = label ?? formatDuration(value)\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(durationChipVariants({ state: effectiveState }), className)}\n onClick={isInteractive ? () => onSelect?.(value) : undefined}\n disabled={!isInteractive}\n aria-pressed={effectiveState === 'active'}\n data-slot=\"time-picker-duration-chip\"\n data-state={effectiveState}\n >\n {effectiveState === 'active' ? (\n <Check className=\"size-3 shrink-0\" aria-hidden=\"true\" />\n ) : null}\n <span>{displayLabel}</span>\n </button>\n )\n },\n)\nTimePickerDurationChip.displayName = 'TimePickerDurationChip'\n\nconst statusChipBaseVariants = cva(\n 'inline-flex h-7 items-center justify-center gap-1 rounded-md pl-1.5 pr-2.5 text-sm font-medium transition-colors whitespace-nowrap border',\n {\n variants: {\n state: {\n default: '',\n hover: '',\n selected: '',\n disabled: 'cursor-not-allowed opacity-60',\n },\n },\n defaultVariants: { state: 'default' },\n },\n)\n\nexport type TimePickerStatusChipProps = {\n variant: TimePickerStatusVariant\n label?: string\n state?: StatusInteractiveState\n selected?: boolean\n disabled?: boolean\n icon?: React.ReactNode\n onSelect?: (variant: TimePickerStatusVariant) => void\n className?: string\n}\n\nexport const TimePickerStatusChip = React.forwardRef<HTMLButtonElement, TimePickerStatusChipProps>(\n (\n { variant, label, state, selected, disabled, icon, onSelect, className },\n ref,\n ) => {\n const effectiveState: StatusInteractiveState = disabled\n ? 'disabled'\n : selected\n ? 'selected'\n : (state ?? 'default')\n const colors = statusColorMap[variant]\n const isInteractive = effectiveState !== 'disabled'\n const displayLabel = label ?? defaultStatusLabels[variant]\n\n const stateClasses =\n effectiveState === 'selected'\n ? cn(colors.bgSoft, colors.border, colors.text)\n : effectiveState === 'hover'\n ? cn('bg-muted/30 border-border', colors.text, 'cursor-pointer')\n : effectiveState === 'disabled'\n ? cn('bg-background border-border', colors.text)\n : cn('bg-background border-border text-muted-foreground hover:bg-muted/30 cursor-pointer')\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(statusChipBaseVariants({ state: effectiveState }), stateClasses, className)}\n onClick={isInteractive ? () => onSelect?.(variant) : undefined}\n disabled={!isInteractive}\n aria-pressed={effectiveState === 'selected'}\n data-slot=\"time-picker-status-chip\"\n data-state={effectiveState}\n data-variant={variant}\n >\n {icon ?? (\n <span\n className={cn('inline-block size-1.5 shrink-0 rounded-full', colors.dot)}\n aria-hidden=\"true\"\n />\n )}\n <span>{displayLabel}</span>\n </button>\n )\n },\n)\nTimePickerStatusChip.displayName = 'TimePickerStatusChip'\n\n/**\n * Wraps a horizontally-scrollable row with prev/next arrow buttons that appear\n * only when overflow is present. Hides the native scrollbar and reserves padding\n * on each side proportional to the visible arrow so chips never sit underneath\n * the arrow button. Exported for consumers that need the same UX (e.g. inline\n * duration chip row inside a meeting/activity form).\n */\nexport function HorizontalScrollRow({\n className,\n contentClassName,\n children,\n ariaLabel,\n arrowSize = 'default',\n scrollLeftAriaLabel = 'Scroll left',\n scrollRightAriaLabel = 'Scroll right',\n}: {\n className?: string\n /** Override classes for the inner scroll container (e.g. tighter padding when inlined into a form row). */\n contentClassName?: string\n children: React.ReactNode\n ariaLabel?: string\n /** Arrow button size. `default` = 28px circle, `sm` = 24px (denser rows). */\n arrowSize?: 'default' | 'sm'\n /** Override the left arrow's aria-label. Default is English 'Scroll left'. */\n scrollLeftAriaLabel?: string\n /** Override the right arrow's aria-label. Default is English 'Scroll right'. */\n scrollRightAriaLabel?: string\n}) {\n const scrollRef = React.useRef<HTMLDivElement>(null)\n const [canScrollLeft, setCanScrollLeft] = React.useState(false)\n const [canScrollRight, setCanScrollRight] = React.useState(false)\n\n const refreshScrollState = React.useCallback(() => {\n const el = scrollRef.current\n if (!el) return\n const { scrollLeft, scrollWidth, clientWidth } = el\n setCanScrollLeft(scrollLeft > 1)\n setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1)\n }, [])\n\n React.useEffect(() => {\n refreshScrollState()\n const el = scrollRef.current\n if (!el) return\n el.addEventListener('scroll', refreshScrollState, { passive: true })\n const ro = new ResizeObserver(refreshScrollState)\n ro.observe(el)\n return () => {\n el.removeEventListener('scroll', refreshScrollState)\n ro.disconnect()\n }\n }, [refreshScrollState])\n\n const scrollBy = React.useCallback((direction: 'left' | 'right') => {\n const el = scrollRef.current\n if (!el) return\n const amount = Math.max(80, Math.floor(el.clientWidth * 0.6))\n el.scrollBy({ left: direction === 'left' ? -amount : amount, behavior: 'smooth' })\n }, [])\n\n const arrowButtonClass =\n arrowSize === 'sm' ? 'size-6 [&_svg]:size-3.5' : 'size-7 [&_svg]:size-4'\n\n return (\n <div className={cn('relative', className)} data-slot=\"time-picker-scroll-row\">\n {/* Outer gutter \u2014 reserves room for the absolute-positioned chevron buttons.\n The scroll viewport lives INSIDE this gutter so chips can never slide\n underneath the arrows \u2014 moving scroll offsets only shift content inside\n the viewport, not the chevron clearance. */}\n <div\n className={cn(\n 'transition-[padding]',\n canScrollLeft ? 'pl-9' : 'pl-4',\n canScrollRight ? 'pr-9' : 'pr-4',\n )}\n >\n <div\n ref={scrollRef}\n className={cn(\n 'scrollbar-hide flex items-center gap-2 overflow-x-auto py-3.5',\n contentClassName,\n )}\n role=\"group\"\n aria-label={ariaLabel}\n >\n {children}\n </div>\n </div>\n {/* Edge fades \u2014 soft gradient at the boundary between the gutter and the\n scroll viewport, so chips sliding into the viewport dissolve cleanly\n before reaching the arrow's edge. */}\n {canScrollLeft ? (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-y-0 left-9 w-6 bg-gradient-to-r from-background to-transparent\"\n />\n ) : null}\n {canScrollRight ? (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-y-0 right-9 w-6 bg-gradient-to-l from-background to-transparent\"\n />\n ) : null}\n {canScrollLeft ? (\n <button\n type=\"button\"\n onClick={() => scrollBy('left')}\n aria-label={scrollLeftAriaLabel}\n className={cn(\n 'absolute left-1 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n arrowButtonClass,\n )}\n data-slot=\"time-picker-scroll-left\"\n >\n <ChevronLeft aria-hidden=\"true\" />\n </button>\n ) : null}\n {canScrollRight ? (\n <button\n type=\"button\"\n onClick={() => scrollBy('right')}\n aria-label={scrollRightAriaLabel}\n className={cn(\n 'absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n arrowButtonClass,\n )}\n data-slot=\"time-picker-scroll-right\"\n >\n <ChevronRight aria-hidden=\"true\" />\n </button>\n ) : null}\n </div>\n )\n}\n\nfunction useControllable<T>(controlled: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [internal, setInternal] = React.useState<T>(defaultValue)\n const isControlled = controlled !== undefined\n const value = isControlled ? (controlled as T) : internal\n const setValue = React.useCallback(\n (next: T) => {\n if (!isControlled) setInternal(next)\n onChange?.(next)\n },\n [isControlled, onChange],\n )\n return [value, setValue] as const\n}\n\nexport type TimePickerProps = {\n value?: TimePickerValue\n defaultValue?: TimePickerValue\n onChange?: (value: TimePickerValue) => void\n\n showHeader?: boolean\n headerIcon?: React.ReactNode\n headerTitle?: string\n headerPlaceholder?: string\n onClose?: () => void\n\n durations?: DurationOption[]\n activeDuration?: number\n defaultActiveDuration?: number\n onDurationChange?: (value: number) => void\n\n statuses?: StatusOption[]\n activeStatus?: TimePickerStatusVariant\n defaultActiveStatus?: TimePickerStatusVariant\n onStatusChange?: (variant: TimePickerStatusVariant) => void\n statusLabel?: string\n\n slots?: string[]\n startTime?: string\n endTime?: string\n intervalMinutes?: number\n format?: TimeFormat\n slotRightText?: (slot: string) => string | undefined\n slotLabel?: string\n maxHeight?: number\n\n showFooter?: boolean\n cancelLabel?: string\n applyLabel?: string\n onCancel?: () => void\n onApply?: (value: TimePickerValue) => void\n legacyFooterActions?: LegacyFooterAction[]\n /**\n * Quick-action rows rendered above the scrollable slot list \u2014 used for\n * \"Now\"-style shortcuts that stay visible regardless of scroll position.\n * Styled like slots but with primary-tinted foreground.\n */\n pinnedTopActions?: PinnedTopAction[]\n\n trigger?: React.ReactNode\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n popoverAlign?: 'start' | 'center' | 'end'\n popoverSide?: 'top' | 'right' | 'bottom' | 'left'\n\n className?: string\n disabled?: boolean\n 'aria-label'?: string\n}\n\nfunction TimePickerCard({\n value,\n setValue,\n showHeader,\n headerIcon,\n headerTitle,\n headerPlaceholder,\n onClose,\n closeAriaLabel,\n format,\n durationOptions,\n activeDuration,\n setActiveDuration,\n onDurationChange,\n durationsAriaLabel,\n scrollLeftAriaLabel,\n scrollRightAriaLabel,\n statusOptions,\n activeStatus,\n setActiveStatus,\n onStatusChange,\n statusLabel,\n slotList,\n slotLabel,\n slotRightText,\n maxHeight,\n showFooter,\n cancelLabel,\n applyLabel,\n handleCancel,\n handleApply,\n legacyFooterActions,\n pinnedTopActions,\n disabled,\n className,\n ariaLabel,\n slotListRef,\n}: {\n value: TimePickerValue\n setValue: (next: TimePickerValue) => void\n showHeader: boolean\n headerIcon: React.ReactNode\n headerTitle: string | undefined\n headerPlaceholder: string\n onClose: (() => void) | undefined\n closeAriaLabel: string\n format: TimeFormat\n durationOptions: DurationOption[] | undefined\n activeDuration: number | undefined\n setActiveDuration: (next: number) => void\n onDurationChange: ((value: number) => void) | undefined\n durationsAriaLabel: string\n scrollLeftAriaLabel: string\n scrollRightAriaLabel: string\n statusOptions: StatusOption[] | undefined\n activeStatus: TimePickerStatusVariant | undefined\n setActiveStatus: (next: TimePickerStatusVariant) => void\n onStatusChange: ((variant: TimePickerStatusVariant) => void) | undefined\n statusLabel: string\n slotList: string[]\n slotLabel: string | undefined\n slotRightText: ((slot: string) => string | undefined) | undefined\n maxHeight: number\n showFooter: boolean\n cancelLabel: string\n applyLabel: string\n handleCancel: () => void\n handleApply: () => void\n legacyFooterActions: LegacyFooterAction[] | undefined\n pinnedTopActions: PinnedTopAction[] | undefined\n disabled: boolean\n className: string | undefined\n ariaLabel: string\n slotListRef: React.RefObject<HTMLDivElement | null>\n}) {\n const headerDisplay = React.useMemo(() => {\n if (headerTitle != null) return { main: headerTitle, suffix: null as string | null }\n if (value) return formatSlotDisplay(value, format)\n return { main: headerPlaceholder, suffix: null as string | null }\n }, [headerTitle, value, format, headerPlaceholder])\n\n const handleSlotKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return\n const target = event.target as HTMLElement\n const slotButton = target.closest<HTMLButtonElement>('[data-slot=\"time-picker-slot\"]')\n if (!slotButton) return\n const container = slotListRef.current\n if (!container) return\n const all = Array.from(\n container.querySelectorAll<HTMLButtonElement>('[data-slot=\"time-picker-slot\"]:not(:disabled)'),\n )\n const currentIndex = all.indexOf(slotButton)\n if (currentIndex === -1) return\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n const next = all[Math.min(all.length - 1, currentIndex + 1)]\n next?.focus()\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n const prev = all[Math.max(0, currentIndex - 1)]\n prev?.focus()\n } else if (event.key === 'Home') {\n event.preventDefault()\n all[0]?.focus()\n } else if (event.key === 'End') {\n event.preventDefault()\n all[all.length - 1]?.focus()\n }\n },\n [disabled, slotListRef],\n )\n\n return (\n <div\n className={cn(\n 'flex w-80 flex-col overflow-hidden rounded-xl border border-border bg-background shadow-xl',\n className,\n )}\n role=\"dialog\"\n aria-label={ariaLabel}\n aria-disabled={disabled || undefined}\n data-slot=\"time-picker\"\n >\n {showHeader ? (\n <div\n className=\"flex items-center gap-2 border-b border-border px-4 py-3.5\"\n data-slot=\"time-picker-header\"\n >\n <span className=\"flex shrink-0 items-center text-muted-foreground\" aria-hidden=\"true\">\n {headerIcon ?? <Clock className=\"size-5\" aria-hidden=\"true\" />}\n </span>\n <p className=\"flex-1 min-w-0 truncate text-base font-medium text-muted-foreground\">\n <span>{headerDisplay.main}</span>\n {headerDisplay.suffix ? <span className=\"ml-1 text-muted-foreground/60\">{headerDisplay.suffix}</span> : null}\n </p>\n {onClose ? (\n <button\n type=\"button\"\n onClick={onClose}\n disabled={disabled}\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40 disabled:pointer-events-none disabled:opacity-60\"\n aria-label={closeAriaLabel}\n data-slot=\"time-picker-close\"\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </button>\n ) : null}\n </div>\n ) : null}\n\n {durationOptions && durationOptions.length > 0 ? (\n <HorizontalScrollRow\n className=\"border-b border-border\"\n ariaLabel={durationsAriaLabel}\n scrollLeftAriaLabel={scrollLeftAriaLabel}\n scrollRightAriaLabel={scrollRightAriaLabel}\n >\n {durationOptions.map((option) => (\n <TimePickerDurationChip\n key={option.value}\n value={option.value}\n label={option.label}\n disabled={disabled || option.disabled}\n selected={activeDuration === option.value}\n onSelect={(next) => {\n setActiveDuration(next)\n onDurationChange?.(next)\n }}\n />\n ))}\n </HorizontalScrollRow>\n ) : null}\n\n {statusOptions && statusOptions.length > 0 ? (\n <div\n className=\"flex flex-col gap-2 border-b border-border px-4 py-3.5\"\n data-slot=\"time-picker-status-row\"\n >\n <p className=\"text-sm font-medium text-muted-foreground\">{statusLabel}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {statusOptions.map((option) => (\n <TimePickerStatusChip\n key={option.variant}\n variant={option.variant}\n label={option.label}\n disabled={disabled || option.disabled}\n selected={activeStatus === option.variant}\n onSelect={(next) => {\n setActiveStatus(next)\n onStatusChange?.(next)\n }}\n />\n ))}\n </div>\n </div>\n ) : null}\n\n {pinnedTopActions && pinnedTopActions.length > 0 ? (\n <div\n className=\"flex flex-col gap-0.5 border-b border-border p-2\"\n data-slot=\"time-picker-pinned-top\"\n >\n {pinnedTopActions.map((action) => (\n <button\n key={action.label}\n type=\"button\"\n onClick={action.onClick}\n disabled={disabled}\n className=\"flex h-9 w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/10 cursor-pointer disabled:pointer-events-none disabled:opacity-60\"\n data-slot=\"time-picker-pinned-action\"\n >\n {action.icon ? (\n <span className=\"flex shrink-0 items-center [&_svg]:size-4\" aria-hidden=\"true\">\n {action.icon}\n </span>\n ) : null}\n <span className=\"flex-1 text-left\">{action.label}</span>\n {action.rightText ? (\n <span className=\"text-primary/60\">{action.rightText}</span>\n ) : null}\n </button>\n ))}\n </div>\n ) : null}\n\n <div\n className=\"flex flex-col gap-0.5 overflow-y-auto p-2\"\n style={{ maxHeight }}\n ref={slotListRef}\n onKeyDown={handleSlotKeyDown}\n data-slot=\"time-picker-slots\"\n >\n {slotLabel ? (\n <p className=\"px-3 pt-1 pb-2 text-sm font-medium text-muted-foreground\">{slotLabel}</p>\n ) : null}\n {slotList.length === 0 ? (\n <p className=\"px-3 py-4 text-center text-sm text-muted-foreground\">No time slots available.</p>\n ) : (\n slotList.map((slot) => (\n <TimePickerSlot\n key={slot}\n value={slot}\n format={format}\n rightText={slotRightText?.(slot)}\n selected={value === slot}\n disabled={disabled}\n onSelect={(next) => setValue(next)}\n />\n ))\n )}\n </div>\n\n {showFooter ? (\n <div\n className=\"flex flex-wrap items-center justify-end gap-2 border-t border-border px-4 py-3\"\n data-slot=\"time-picker-footer\"\n >\n {legacyFooterActions && legacyFooterActions.length > 0 ? (\n <div className=\"mr-auto flex items-center gap-1\">\n {legacyFooterActions.map((action) => (\n <Button\n key={action.label}\n type=\"button\"\n variant={action.variant === 'muted' ? 'muted' : 'ghost'}\n size=\"sm\"\n onClick={action.onClick}\n disabled={disabled}\n data-slot=\"time-picker-legacy-action\"\n >\n {action.label}\n </Button>\n ))}\n <span aria-hidden=\"true\" className=\"ml-2 mr-1 h-5 w-px bg-border\" />\n </div>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={disabled}\n data-slot=\"time-picker-cancel\"\n >\n {cancelLabel}\n </Button>\n <Button\n type=\"button\"\n variant=\"default\"\n size=\"sm\"\n onClick={handleApply}\n disabled={disabled}\n data-slot=\"time-picker-apply\"\n >\n {applyLabel}\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\nexport function TimePicker({\n value: valueProp,\n defaultValue = null,\n onChange,\n showHeader = true,\n headerIcon,\n headerTitle,\n headerPlaceholder,\n onClose,\n durations,\n activeDuration: activeDurationProp,\n defaultActiveDuration,\n onDurationChange,\n statuses,\n activeStatus: activeStatusProp,\n defaultActiveStatus,\n onStatusChange,\n statusLabel,\n slots: slotsProp,\n startTime = '00:00',\n endTime = '23:30',\n intervalMinutes = 30,\n format = '12h',\n slotRightText,\n slotLabel,\n maxHeight = 280,\n showFooter = true,\n cancelLabel,\n applyLabel,\n onCancel,\n onApply,\n legacyFooterActions,\n pinnedTopActions,\n trigger,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n popoverAlign = 'start',\n popoverSide = 'bottom',\n className,\n disabled = false,\n 'aria-label': ariaLabel,\n}: TimePickerProps) {\n const t = useT()\n const resolvedHeaderPlaceholder = headerPlaceholder ?? t('ui.timePicker.placeholder', 'Pick a time')\n const resolvedStatusLabel = statusLabel ?? t('ui.timePicker.statusLabel', 'Select status')\n const resolvedCancelLabel = cancelLabel ?? t('ui.timePicker.cancelButton', 'Cancel')\n const resolvedApplyLabel = applyLabel ?? t('ui.timePicker.applyButton', 'Apply')\n const resolvedAriaLabel = ariaLabel ?? t('ui.timePicker.label', 'Time picker')\n const resolvedCloseAriaLabel = t('ui.timePicker.closeButton', 'Close')\n const resolvedDurationsAriaLabel = t('ui.timePicker.durationsRowLabel', 'Quick duration')\n const resolvedScrollLeftAriaLabel = t('ui.timePicker.scrollLeft', 'Scroll left')\n const resolvedScrollRightAriaLabel = t('ui.timePicker.scrollRight', 'Scroll right')\n const [value, setValue] = useControllable<TimePickerValue>(valueProp, defaultValue, onChange)\n const [activeDuration, setActiveDuration] = useControllable<number | undefined>(\n activeDurationProp,\n defaultActiveDuration,\n )\n const [activeStatus, setActiveStatus] = useControllable<TimePickerStatusVariant | undefined>(\n activeStatusProp,\n defaultActiveStatus,\n )\n const [open, setOpen] = useControllable<boolean>(openProp, defaultOpen, onOpenChange)\n\n const openSnapshotRef = React.useRef<TimePickerValue>(value)\n const slotListRef = React.useRef<HTMLDivElement | null>(null)\n\n const slotList = React.useMemo(() => {\n if (slotsProp && slotsProp.length > 0) return slotsProp\n return generateSlots(startTime, endTime, intervalMinutes)\n }, [slotsProp, startTime, endTime, intervalMinutes])\n\n const setOpenWithSnapshot = React.useCallback(\n (next: boolean) => {\n if (next && !open) {\n openSnapshotRef.current = value\n }\n setOpen(next)\n },\n [open, setOpen, value],\n )\n\n const handleApply = React.useCallback(() => {\n onApply?.(value)\n if (trigger) setOpenWithSnapshot(false)\n }, [onApply, setOpenWithSnapshot, trigger, value])\n\n const handleCancel = React.useCallback(() => {\n onCancel?.()\n if (trigger) {\n setValue(openSnapshotRef.current)\n setOpenWithSnapshot(false)\n }\n }, [onCancel, setOpenWithSnapshot, setValue, trigger])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Escape') {\n handleCancel()\n } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n handleApply()\n }\n },\n [handleApply, handleCancel],\n )\n\n const card = (\n <div onKeyDown={handleKeyDown}>\n <TimePickerCard\n value={value}\n setValue={setValue}\n showHeader={showHeader}\n headerIcon={headerIcon}\n headerTitle={headerTitle}\n headerPlaceholder={resolvedHeaderPlaceholder}\n onClose={onClose}\n closeAriaLabel={resolvedCloseAriaLabel}\n format={format}\n durationOptions={durations}\n activeDuration={activeDuration}\n setActiveDuration={setActiveDuration}\n onDurationChange={onDurationChange}\n durationsAriaLabel={resolvedDurationsAriaLabel}\n scrollLeftAriaLabel={resolvedScrollLeftAriaLabel}\n scrollRightAriaLabel={resolvedScrollRightAriaLabel}\n statusOptions={statuses}\n activeStatus={activeStatus}\n setActiveStatus={setActiveStatus}\n onStatusChange={onStatusChange}\n statusLabel={resolvedStatusLabel}\n slotList={slotList}\n slotLabel={slotLabel}\n slotRightText={slotRightText}\n maxHeight={maxHeight}\n showFooter={showFooter}\n cancelLabel={resolvedCancelLabel}\n applyLabel={resolvedApplyLabel}\n handleCancel={handleCancel}\n handleApply={handleApply}\n legacyFooterActions={legacyFooterActions}\n pinnedTopActions={pinnedTopActions}\n disabled={disabled}\n className={className}\n ariaLabel={resolvedAriaLabel}\n slotListRef={slotListRef}\n />\n </div>\n )\n\n if (!trigger) return card\n\n return (\n <Popover open={open} onOpenChange={setOpenWithSnapshot}>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n <PopoverContent\n align={popoverAlign}\n side={popoverSide}\n className=\"w-auto border-none bg-transparent p-0 shadow-none\"\n >\n {card}\n </PopoverContent>\n </Popover>\n )\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["hours"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.8.1-develop.7207.1.901ef7e47a",
3
+ "version": "0.8.1-develop.7218.1.9f22882799",
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.7207.1.901ef7e47a",
158
+ "@open-mercato/shared": "0.8.1-develop.7218.1.9f22882799",
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.7207.1.901ef7e47a",
165
+ "@open-mercato/shared": "0.8.1-develop.7218.1.9f22882799",
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",
@@ -7,9 +7,38 @@ jest.mock('remark-gfm', () => ({ __esModule: true, default: {} }))
7
7
 
8
8
  import * as React from 'react'
9
9
  import { renderToString } from 'react-dom/server'
10
+ import { pl } from 'date-fns/locale/pl'
10
11
  import { CrudForm, type CrudField } from '../CrudForm'
11
12
  import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
12
13
 
14
+ function renderFormWithLocale(
15
+ fields: CrudField[],
16
+ locale: string,
17
+ initialValues?: Record<string, unknown>,
18
+ ): string {
19
+ return renderToString(
20
+ React.createElement(
21
+ I18nProvider as React.ComponentType<{
22
+ locale: string
23
+ dict: Record<string, string>
24
+ children: React.ReactNode
25
+ }>,
26
+ { locale, dict: {} },
27
+ React.createElement(CrudForm as React.ComponentType<{
28
+ title: string
29
+ fields: CrudField[]
30
+ initialValues?: Record<string, unknown>
31
+ onSubmit: () => void
32
+ }>, {
33
+ title: 'Form',
34
+ fields,
35
+ initialValues,
36
+ onSubmit: () => {},
37
+ })
38
+ )
39
+ )
40
+ }
41
+
13
42
  function renderForm(
14
43
  fields: CrudField[],
15
44
  initialValues?: Record<string, unknown>,
@@ -97,3 +126,36 @@ describe('CrudForm — datetime field types render correct picker component', ()
97
126
  expect(html).toContain('02:30 PM')
98
127
  })
99
128
  })
129
+
130
+ describe('CrudForm — date and time fields agree on the app locale', () => {
131
+ const DATE_AND_TIME: CrudField[] = [
132
+ { id: 'due_date', label: 'Due Date', type: 'datepicker' },
133
+ { id: 'sync_time', label: 'Sync Time', type: 'time' },
134
+ ]
135
+ const VALUES = { due_date: '2026-01-15', sync_time: '14:30' }
136
+
137
+ // A date field and a time field sitting in the same form must not disagree about which
138
+ // language they speak: before #5942 the date picker defaulted to English while the time
139
+ // picker defaulted to a 12h clock, and fixing only one of them is worse than fixing neither.
140
+ it('localizes both fields for a non-English app', () => {
141
+ const html = renderFormWithLocale(DATE_AND_TIME, 'pl', VALUES)
142
+ expect(html).toContain('15 sty 2026')
143
+ expect(html).toContain('14:30')
144
+ expect(html).not.toContain('PM')
145
+ })
146
+
147
+ it('leaves both fields on the English defaults for an English app', () => {
148
+ const html = renderFormWithLocale(DATE_AND_TIME, 'en', VALUES)
149
+ expect(html).toContain('Jan 15, 2026')
150
+ expect(html).toContain('02:30 PM')
151
+ })
152
+
153
+ it('lets a field pin its own locale ahead of the app locale', () => {
154
+ const html = renderFormWithLocale(
155
+ [{ id: 'due_date', label: 'Due Date', type: 'datepicker', locale: pl }],
156
+ 'en',
157
+ { due_date: '2026-01-15' },
158
+ )
159
+ expect(html).toContain('15 sty 2026')
160
+ })
161
+ })
@@ -7,7 +7,9 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import {
8
8
  TimePicker as TimePickerPrimitive,
9
9
  formatTimePickerDisplay,
10
+ type TimeFormat,
10
11
  } from '../../primitives/time-picker'
12
+ import { useTimeDisplayFormat } from '../../primitives/date-locale'
11
13
 
12
14
  export type TimePickerProps = {
13
15
  value?: string | null
@@ -19,6 +21,13 @@ export type TimePickerProps = {
19
21
  minuteStep?: number
20
22
  showNowButton?: boolean
21
23
  showClearButton?: boolean
24
+ /**
25
+ * Clock convention for the trigger label and the slot list. Defaults to the one the active
26
+ * locale writes time in — `12h` for English and Korean, `24h` for Polish, German and Spanish —
27
+ * so a tenant does not read an AM/PM clock its language never uses. Pass an explicit value to
28
+ * pin one regardless of locale.
29
+ */
30
+ format?: TimeFormat
22
31
  }
23
32
 
24
33
  function currentHHMM(): string {
@@ -50,8 +59,11 @@ export function TimePicker({
50
59
  // by "Clear" in most flows. Pass `showClearButton={true}` to opt back in when
51
60
  // you need an explicit "set value to null" action distinct from "dismiss".
52
61
  showClearButton = false,
62
+ format,
53
63
  }: TimePickerProps) {
54
64
  const t = useT()
65
+ const localeFormat = useTimeDisplayFormat()
66
+ const timeFormat = format ?? localeFormat
55
67
  const [open, setOpen] = React.useState(false)
56
68
 
57
69
  const placeholderText = placeholder ?? t('ui.timePicker.placeholder', 'Pick a time')
@@ -59,11 +71,11 @@ export function TimePicker({
59
71
  const clearText = t('ui.timePicker.clearButton', 'Clear')
60
72
  const isInteractive = !disabled && !readOnly
61
73
 
62
- // Render the trigger label in the same 12h "HH:MM AM/PM" format as the slot list
63
- // inside the popover so users see one consistent representation.
74
+ // Render the trigger label in the same format as the slot list inside the popover
75
+ // so users see one consistent representation.
64
76
  const displayValue = value
65
77
  ? (() => {
66
- const { main, suffix } = formatTimePickerDisplay(value, '12h')
78
+ const { main, suffix } = formatTimePickerDisplay(value, timeFormat)
67
79
  return suffix ? `${main} ${suffix}` : main
68
80
  })()
69
81
  : null
@@ -101,7 +113,7 @@ export function TimePicker({
101
113
  }> = []
102
114
  if (showNowButton) {
103
115
  const nowValue = currentHHMM()
104
- const { main: nowMain, suffix: nowSuffix } = formatTimePickerDisplay(nowValue, '12h')
116
+ const { main: nowMain, suffix: nowSuffix } = formatTimePickerDisplay(nowValue, timeFormat)
105
117
  pinnedTopActions.push({
106
118
  label: nowText,
107
119
  icon: <ClockIcon aria-hidden="true" />,
@@ -133,6 +145,7 @@ export function TimePicker({
133
145
  onChange(next)
134
146
  }}
135
147
  intervalMinutes={Math.max(1, minuteStep)}
148
+ format={timeFormat}
136
149
  // Trigger button already displays current value — don't duplicate it
137
150
  // inside the popover header.
138
151
  showHeader={false}
@@ -3,6 +3,7 @@ jest.mock('next/navigation', () => ({
3
3
  }))
4
4
  jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
5
5
  useT: () => (_key: string, fallback: string) => fallback,
6
+ useOptionalLocale: () => undefined,
6
7
  }))
7
8
 
8
9
  import * as React from 'react'
@@ -1,14 +1,20 @@
1
1
  jest.mock('next/navigation', () => ({
2
2
  useRouter: () => ({ push: jest.fn() }),
3
3
  }))
4
+ let mockLocale: string | undefined
4
5
  jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
5
6
  useT: () => (_key: string, fallback: string) => fallback,
7
+ useOptionalLocale: () => mockLocale,
6
8
  }))
7
9
 
8
10
  import * as React from 'react'
9
11
  import { renderToString } from 'react-dom/server'
10
12
  import { TimePicker } from '../TimePicker'
11
13
 
14
+ beforeEach(() => {
15
+ mockLocale = 'en'
16
+ })
17
+
12
18
  // Note: Tests for Now/Clear button visibility and click behaviour require opening
13
19
  // the Radix Popover (portal content is not rendered by renderToString when closed).
14
20
  // Those tests are covered by integration tests (TC-DTP-002) post-migration.
@@ -28,6 +34,31 @@ describe('TimePicker SSR render', () => {
28
34
  expect(html).toContain('02:30 PM')
29
35
  })
30
36
 
37
+ it('shows a 24h value for a locale that writes time on a 24-hour clock', () => {
38
+ mockLocale = 'pl'
39
+ const html = render(<TimePicker value="14:30" onChange={jest.fn()} />)
40
+ expect(html).toContain('14:30')
41
+ expect(html).not.toContain('PM')
42
+ })
43
+
44
+ it('keeps the 12h clock for a locale that uses AM/PM', () => {
45
+ mockLocale = 'ko'
46
+ const html = render(<TimePicker value="14:30" onChange={jest.fn()} />)
47
+ expect(html).toContain('02:30 PM')
48
+ })
49
+
50
+ it('lets an explicit format prop override the locale default', () => {
51
+ mockLocale = 'pl'
52
+ const html = render(<TimePicker value="14:30" onChange={jest.fn()} format="12h" />)
53
+ expect(html).toContain('02:30 PM')
54
+ })
55
+
56
+ it('falls back to the 12h clock when rendered outside an i18n provider', () => {
57
+ mockLocale = undefined
58
+ const html = render(<TimePicker value="14:30" onChange={jest.fn()} />)
59
+ expect(html).toContain('02:30 PM')
60
+ })
61
+
31
62
  it('renders trigger button with aria-haspopup="dialog"', () => {
32
63
  const html = render(<TimePicker onChange={jest.fn()} />)
33
64
  expect(html).toContain('aria-haspopup="dialog"')
@@ -0,0 +1,53 @@
1
+ import { de } from 'date-fns/locale/de'
2
+ import { enUS } from 'date-fns/locale/en-US'
3
+ import { es } from 'date-fns/locale/es'
4
+ import { ko } from 'date-fns/locale/ko'
5
+ import { pl } from 'date-fns/locale/pl'
6
+ import { deriveTimeDisplayFormat, resolveDateFnsLocale } from '../date-locale'
7
+
8
+ describe('resolveDateFnsLocale', () => {
9
+ it('maps every locale the app ships to its date-fns locale', () => {
10
+ expect(resolveDateFnsLocale('en')).toBe(enUS)
11
+ expect(resolveDateFnsLocale('pl')).toBe(pl)
12
+ expect(resolveDateFnsLocale('de')).toBe(de)
13
+ expect(resolveDateFnsLocale('es')).toBe(es)
14
+ expect(resolveDateFnsLocale('ko')).toBe(ko)
15
+ })
16
+
17
+ it('resolves a region-qualified code by its language', () => {
18
+ expect(resolveDateFnsLocale('pl-PL')).toBe(pl)
19
+ expect(resolveDateFnsLocale('en-GB')).toBe(enUS)
20
+ expect(resolveDateFnsLocale('DE')).toBe(de)
21
+ })
22
+
23
+ it('returns undefined for an unmapped or absent locale so consumers keep their own default', () => {
24
+ expect(resolveDateFnsLocale('fr')).toBeUndefined()
25
+ expect(resolveDateFnsLocale('')).toBeUndefined()
26
+ expect(resolveDateFnsLocale(null)).toBeUndefined()
27
+ expect(resolveDateFnsLocale(undefined)).toBeUndefined()
28
+ })
29
+
30
+ it('resolves a locale that starts the week on Monday', () => {
31
+ expect(resolveDateFnsLocale('pl')?.options?.weekStartsOn).toBe(1)
32
+ expect(resolveDateFnsLocale('en')?.options?.weekStartsOn).toBe(0)
33
+ })
34
+ })
35
+
36
+ describe('deriveTimeDisplayFormat', () => {
37
+ it('reports 24h for locales that write time on a 24-hour clock', () => {
38
+ expect(deriveTimeDisplayFormat('pl')).toBe('24h')
39
+ expect(deriveTimeDisplayFormat('de')).toBe('24h')
40
+ expect(deriveTimeDisplayFormat('es')).toBe('24h')
41
+ })
42
+
43
+ it('reports 12h for locales that use AM/PM — including Korean', () => {
44
+ expect(deriveTimeDisplayFormat('en')).toBe('12h')
45
+ expect(deriveTimeDisplayFormat('ko')).toBe('12h')
46
+ })
47
+
48
+ it('falls back to 12h when there is no locale or Intl cannot read it', () => {
49
+ expect(deriveTimeDisplayFormat(undefined)).toBe('12h')
50
+ expect(deriveTimeDisplayFormat(null)).toBe('12h')
51
+ expect(deriveTimeDisplayFormat('not a locale tag')).toBe('12h')
52
+ })
53
+ })
@@ -1,12 +1,13 @@
1
1
  import * as React from 'react'
2
2
  import { act, fireEvent, render, screen } from '@testing-library/react'
3
+ import { pl } from 'date-fns/locale/pl'
3
4
  import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { DatePicker } from '../date-picker'
5
6
  import { DatePicker as LegacyDatePicker } from '../../backend/inputs/DatePicker'
6
7
  import { DateTimePicker as LegacyDateTimePicker } from '../../backend/inputs/DateTimePicker'
7
8
 
8
- function renderWithI18n(ui: React.ReactElement) {
9
- return render(<I18nProvider locale="en" dict={{}}>{ui}</I18nProvider>)
9
+ function renderWithI18n(ui: React.ReactElement, locale = 'en') {
10
+ return render(<I18nProvider locale={locale} dict={{}}>{ui}</I18nProvider>)
10
11
  }
11
12
 
12
13
  function getTrigger(): HTMLElement {
@@ -244,6 +245,57 @@ describe('DatePicker primitive', () => {
244
245
  })
245
246
  })
246
247
 
248
+ describe('DatePicker calendar under a non-English locale', () => {
249
+ async function openPolishCalendar() {
250
+ renderWithI18n(
251
+ <DatePicker value={new Date(2026, 0, 15)} onChange={() => {}} locale={pl} />,
252
+ )
253
+ await openPopover()
254
+ }
255
+
256
+ it('names the month in the standalone form, not the genitive one used inside a date', async () => {
257
+ await openPolishCalendar()
258
+ expect(screen.getByText(/styczeń 2026/)).toBeInTheDocument()
259
+ expect(screen.queryByText(/stycznia 2026/)).not.toBeInTheDocument()
260
+ })
261
+
262
+ it('labels the month navigation buttons with the standalone month form', async () => {
263
+ await openPolishCalendar()
264
+ const navLabels = screen
265
+ .getAllByRole('button')
266
+ .map((button) => button.getAttribute('aria-label') ?? '')
267
+ .filter((label) => /^Go to (previous|next) month: /.test(label))
268
+ expect(navLabels).toEqual([
269
+ 'Go to previous month: grudzień 2025',
270
+ 'Go to next month: luty 2026',
271
+ ])
272
+ })
273
+
274
+ // 2025-12-29 is a Monday and 2025-12-28 the Sunday before it, so the first cell of the
275
+ // January 2026 grid says which day the calendar treats as the start of the week.
276
+ it('starts the week on the day the locale starts it on', async () => {
277
+ await openPolishCalendar()
278
+ expect(screen.getAllByRole('gridcell')[0]).toHaveAttribute('data-day', '2025-12-29')
279
+ })
280
+
281
+ it('keeps the picker default week start when no locale is passed', async () => {
282
+ renderWithI18n(<DatePicker value={new Date(2026, 0, 15)} onChange={() => {}} />)
283
+ await openPopover()
284
+ expect(screen.getAllByRole('gridcell')[0]).toHaveAttribute('data-day', '2025-12-28')
285
+ })
286
+
287
+ // A call site that forgets to thread a `locale` prop is exactly the #5942 bug: without
288
+ // this fallback the picker silently renders English/Sunday-first for every tenant that
289
+ // does not explicitly pass one. `date-picker.tsx` must default to the active app locale
290
+ // so future call sites cannot regress the same way.
291
+ it('falls back to the app locale when no explicit locale prop is passed', async () => {
292
+ renderWithI18n(<DatePicker value={new Date(2026, 0, 15)} onChange={() => {}} />, 'pl')
293
+ await openPopover()
294
+ expect(screen.getByText(/styczeń 2026/)).toBeInTheDocument()
295
+ expect(screen.getAllByRole('gridcell')[0]).toHaveAttribute('data-day', '2025-12-29')
296
+ })
297
+ })
298
+
247
299
  describe('DatePicker backwards-compat shims', () => {
248
300
  it('backend/inputs/DatePicker defaults to the Figma-aligned Apply/Cancel footer', async () => {
249
301
  renderWithI18n(<LegacyDatePicker value={null} onChange={() => {}} />)
@@ -38,6 +38,14 @@ export function CalendarMonthSelector({
38
38
  )
39
39
  }
40
40
 
41
+ // `LLLL`/`LLL`, not `MMMM`/`MMM`: these labels name a month on its own, and languages that
42
+ // inflect month names — Polish among the five this repo ships — spell that differently from a
43
+ // month inside a full date. `MMMM` is the inflected (genitive) form, so a Polish header read
44
+ // "stycznia" ("of January") where it should read "styczeń". The two tokens are identical in
45
+ // English, which is why the bug was invisible until a non-English tenant opened a picker.
46
+ const STANDALONE_MONTH_YEAR = 'LLLL yyyy'
47
+ const STANDALONE_MONTH_SHORT = 'LLL'
48
+
41
49
  const navButtonClassName = cn(
42
50
  'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',
43
51
  'border border-border bg-background text-muted-foreground transition-colors',
@@ -60,7 +68,7 @@ function MonthNavButton({
60
68
  }
61
69
  const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth
62
70
  const Icon = direction === 'prev' ? ChevronLeft : ChevronRight
63
- const targetLabel = format(target ?? new Date(), 'MMMM yyyy', locale ? { locale } : undefined)
71
+ const targetLabel = format(target ?? new Date(), STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)
64
72
  const ariaLabel = `Go to ${direction === 'prev' ? 'previous' : 'next'} month: ${targetLabel}`
65
73
  return (
66
74
  <button
@@ -89,7 +97,7 @@ function buildMonthCaption(
89
97
  calendarMonth: CalendarMonth
90
98
  displayIndex?: number
91
99
  }) {
92
- const label = format(calendarMonth.date, 'MMMM yyyy', locale ? { locale } : undefined)
100
+ const label = format(calendarMonth.date, STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)
93
101
  const index = typeof displayIndex === 'number' ? displayIndex : 0
94
102
  // For multi-month layouts (e.g. range pickers) only the leftmost month
95
103
  // exposes the previous-month chevron and only the rightmost exposes the
@@ -156,7 +164,7 @@ function MonthGrid({
156
164
  const monthLabels = React.useMemo(
157
165
  () =>
158
166
  Array.from({ length: 12 }, (_, monthIndex) =>
159
- format(new Date(year, monthIndex, 1), 'MMM', locale ? { locale } : undefined),
167
+ format(new Date(year, monthIndex, 1), STANDALONE_MONTH_SHORT, locale ? { locale } : undefined),
160
168
  ),
161
169
  [year, locale],
162
170
  )
@@ -0,0 +1,80 @@
1
+ "use client"
2
+
3
+ import type { Locale as DateFnsLocale } from 'date-fns/locale'
4
+ import { de } from 'date-fns/locale/de'
5
+ import { enUS } from 'date-fns/locale/en-US'
6
+ import { es } from 'date-fns/locale/es'
7
+ import { ko } from 'date-fns/locale/ko'
8
+ import { pl } from 'date-fns/locale/pl'
9
+ import { useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'
10
+
11
+ export type TimeDisplayFormat = '12h' | '24h'
12
+
13
+ /**
14
+ * The app locale is a bare language code (`'pl'`), but `react-day-picker` and every
15
+ * date-fns formatter want a `Locale` object — the thing that knows month names and,
16
+ * critically, `weekStartsOn`. Without one the picker falls back to the bundled `enUS`:
17
+ * English month/weekday names and a Sunday-first week for every tenant.
18
+ *
19
+ * The five entries mirror `locales` in `@open-mercato/shared/lib/i18n/config`. They are
20
+ * imported statically rather than loaded on demand because a picker renders its calendar
21
+ * synchronously — an async locale would flash an English month header first.
22
+ */
23
+ const DATE_FNS_LOCALES: Record<string, DateFnsLocale> = {
24
+ de,
25
+ en: enUS,
26
+ es,
27
+ ko,
28
+ pl,
29
+ }
30
+
31
+ /**
32
+ * The date-fns locale for an app locale code, or `undefined` when there is none to apply.
33
+ *
34
+ * `undefined` rather than `enUS` for an unknown code: the consumers pass this straight into
35
+ * a `locale?: Locale` prop, and leaving it unset keeps their own documented default instead
36
+ * of pinning English on a deployment that added a locale this map has not caught up with.
37
+ */
38
+ export function resolveDateFnsLocale(locale?: string | null): DateFnsLocale | undefined {
39
+ const code = locale?.split('-')[0]?.toLowerCase()
40
+ return code ? DATE_FNS_LOCALES[code] : undefined
41
+ }
42
+
43
+ /** The date-fns locale for the active `I18nProvider` locale, or `undefined` outside a provider. */
44
+ export function useDateFnsLocale(): DateFnsLocale | undefined {
45
+ return resolveDateFnsLocale(useOptionalLocale())
46
+ }
47
+
48
+ /**
49
+ * Whether a locale writes the time of day on a 12- or 24-hour clock.
50
+ *
51
+ * Asked of `Intl` rather than a "not English means 24h" heuristic, because that heuristic is
52
+ * wrong for two of the five locales this repo ships: Korean is a 12-hour locale (오전/오후), as
53
+ * is English, while Polish, German and Spanish are not. `12h` is the fallback when `Intl` cannot
54
+ * answer, matching the `TimePicker` primitive's own default.
55
+ */
56
+ // Constructing an `Intl.DateTimeFormat` costs ~40x using one, and this runs on every render of
57
+ // every time picker — so the answer is kept per locale, exactly as `date-format.ts` caches its
58
+ // formatters. The key space is the locales a deployment ships and the answer never changes.
59
+ const timeFormatCache = new Map<string, TimeDisplayFormat>()
60
+
61
+ export function deriveTimeDisplayFormat(locale?: string | null): TimeDisplayFormat {
62
+ if (!locale) return '12h'
63
+ const cached = timeFormatCache.get(locale)
64
+ if (cached) return cached
65
+ let resolved: TimeDisplayFormat = '12h'
66
+ try {
67
+ if (new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12 === false) {
68
+ resolved = '24h'
69
+ }
70
+ } catch {
71
+ resolved = '12h'
72
+ }
73
+ timeFormatCache.set(locale, resolved)
74
+ return resolved
75
+ }
76
+
77
+ /** The clock convention of the active `I18nProvider` locale; `12h` outside a provider. */
78
+ export function useTimeDisplayFormat(): TimeDisplayFormat {
79
+ return deriveTimeDisplayFormat(useOptionalLocale())
80
+ }
@@ -11,6 +11,7 @@ import { Button } from './button'
11
11
  import { LinkButton } from './link-button'
12
12
  import { TimeInput } from '../backend/inputs/TimeInput'
13
13
  import { formatWithPublicDateFormat, resolvePublicDateFormat, resolvePublicDateTimeFormat } from './date-format'
14
+ import { useDateFnsLocale } from './date-locale'
14
15
 
15
16
  export type DatePickerFooter = 'apply-cancel' | 'today-clear' | 'none'
16
17
 
@@ -56,6 +57,12 @@ export type DatePickerProps = {
56
57
  align?: 'start' | 'center' | 'end'
57
58
  minDate?: Date
58
59
  maxDate?: Date
60
+ /**
61
+ * date-fns locale for month/weekday names and the first day of the week.
62
+ * Defaults to the active `I18nProvider` locale (`undefined` outside a
63
+ * provider, matching pre-existing behavior); pass one explicitly to pin it
64
+ * regardless of the app locale.
65
+ */
59
66
  locale?: Locale
60
67
  displayFormat?: string
61
68
  className?: string
@@ -104,7 +111,7 @@ export function DatePicker({
104
111
  align = 'start',
105
112
  minDate,
106
113
  maxDate,
107
- locale,
114
+ locale: localeProp,
108
115
  displayFormat,
109
116
  className,
110
117
  popoverClassName,
@@ -116,6 +123,14 @@ export function DatePicker({
116
123
  }: DatePickerProps) {
117
124
  const resolvedCloseOnSelect = closeOnSelect ?? footer === 'today-clear'
118
125
  const t = useT()
126
+ // Callers may pin a locale explicitly (e.g. a field-level override); absent
127
+ // that, fall back to the active app locale so every consumer of this
128
+ // primitive — not just the ones that remember to thread a `locale` prop —
129
+ // renders a localized month/weekday grid instead of silently defaulting to
130
+ // English. Outside an `I18nProvider` this resolves to `undefined`, same as
131
+ // not passing a locale at all today.
132
+ const appLocale = useDateFnsLocale()
133
+ const locale = localeProp ?? appLocale
119
134
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
120
135
  const open = controlledOpen ?? uncontrolledOpen
121
136
  const setOpen = React.useCallback((next: boolean) => {
@@ -16,7 +16,7 @@ type SlotInteractiveState = 'default' | 'hover' | 'active' | 'disabled'
16
16
  type DurationInteractiveState = SlotInteractiveState
17
17
  type StatusInteractiveState = 'default' | 'hover' | 'selected' | 'disabled'
18
18
 
19
- type TimeFormat = '12h' | '24h'
19
+ export type TimeFormat = '12h' | '24h'
20
20
 
21
21
  type LegacyFooterAction = {
22
22
  label: string