@open-mercato/ui 0.8.1-develop.7215.1.60786cf836 → 0.8.1-develop.7219.1.f96cb57e70

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,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.7215.1.60786cf836",
3
+ "version": "0.8.1-develop.7219.1.f96cb57e70",
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.7215.1.60786cf836",
158
+ "@open-mercato/shared": "0.8.1-develop.7219.1.f96cb57e70",
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.7215.1.60786cf836",
165
+ "@open-mercato/shared": "0.8.1-develop.7219.1.f96cb57e70",
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",
@@ -130,4 +130,19 @@ function buildLucideRegistrySource(resolvedIcons) {
130
130
  return normalizeSourceText(sourceFile.getFullText())
131
131
  }
132
132
 
133
- module.exports = { buildLucideRegistrySource }
133
+ // Drift decision for the committed registry, kept here rather than inline in
134
+ // build.mjs so it is reachable from Jest: build.mjs's top level awaits a full
135
+ // package build, so nothing can import it to exercise this comparison. The
136
+ // caller owns reporting and the exit code; this only decides.
137
+ function checkLucideRegistryDrift(committedSource, generatedSource, registryPath = GENERATED_FILE_NAME) {
138
+ if (committedSource === generatedSource) return { drifted: false }
139
+
140
+ return {
141
+ drifted: true,
142
+ message: `Lucide icon registry is out of date: ${registryPath}\n`
143
+ + 'A referenced icon name was added or removed without regenerating it.\n'
144
+ + 'Run `yarn workspace @open-mercato/ui build` and commit the result.',
145
+ }
146
+ }
147
+
148
+ module.exports = { buildLucideRegistrySource, checkLucideRegistryDrift }
@@ -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
+ })
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
  import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
6
6
  import { discoverResolvedIcons } from '../../../../scripts/lucideIconDiscovery.cjs'
7
- import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
7
+ import { buildLucideRegistrySource, checkLucideRegistryDrift } from '../../../../scripts/lucideRegistrySource.cjs'
8
8
  import jestConfig from '../../../../jest.config.cjs'
9
9
 
10
10
  const packageDir = join(__dirname, '..', '..', '..', '..')
@@ -272,6 +272,32 @@ describe('committed lucideRegistry.generated.tsx', () => {
272
272
  })
273
273
  })
274
274
 
275
+ // `yarn ui:icons:check` runs this decision unfiltered in CI, where only the in-sync
276
+ // branch is ever exercised — every PR runs it against a registry that matches. The
277
+ // branch that does the work is the drifted one, so it is asserted here instead.
278
+ describe('checkLucideRegistryDrift', () => {
279
+ const source = buildLucideRegistrySource(populatedFixture)
280
+
281
+ it('reports no drift when the committed file matches the emitter', () => {
282
+ expect(checkLucideRegistryDrift(source, source)).toEqual({ drifted: false })
283
+ })
284
+
285
+ it('reports drift when an icon is missing from the committed file', () => {
286
+ const stale = buildLucideRegistrySource(populatedFixture.slice(0, -1))
287
+ expect(checkLucideRegistryDrift(stale, source).drifted).toBe(true)
288
+ })
289
+
290
+ it('reports drift when the committed file has never been written', () => {
291
+ expect(checkLucideRegistryDrift('', source).drifted).toBe(true)
292
+ })
293
+
294
+ it('names the drifted file and the command that regenerates it', () => {
295
+ const drift = checkLucideRegistryDrift('', source, '/repo/lucideRegistry.generated.tsx')
296
+ expect(drift.message).toContain('/repo/lucideRegistry.generated.tsx')
297
+ expect(drift.message).toContain('yarn workspace @open-mercato/ui build')
298
+ })
299
+ })
300
+
275
301
  describe('jest transform scope', () => {
276
302
  const transformPatterns = Object.keys(jestConfig.transform).map((key) => new RegExp(key))
277
303
 
@@ -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
+ })