@open-mercato/ui 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3260.1.dc719cac84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
- package/dist/ai/AiAssistantLauncher.js +8 -0
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +226 -58
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiDock.js +3 -2
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/ModelPicker.js +186 -0
- package/dist/ai/ModelPicker.js.map +7 -0
- package/dist/ai/index.js +3 -1
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiChat.js +12 -16
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/CrudForm.js +46 -129
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +10 -3
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/EmptyState.js +1 -47
- package/dist/backend/EmptyState.js.map +2 -2
- package/dist/backend/FilterOverlay.js +31 -26
- package/dist/backend/FilterOverlay.js.map +2 -2
- package/dist/backend/FlashMessages.js +23 -25
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/JsonBuilder.js +4 -2
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/LogList.js +49 -0
- package/dist/backend/LogList.js.map +7 -0
- package/dist/backend/crud/CollapsibleGroup.js +13 -3
- package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +80 -32
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +60 -6
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/DatePicker.js +6 -142
- package/dist/backend/inputs/DatePicker.js.map +2 -2
- package/dist/backend/inputs/DateTimePicker.js +6 -182
- package/dist/backend/inputs/DateTimePicker.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +16 -19
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/inputs/TimeInput.js +1 -1
- package/dist/backend/inputs/TimeInput.js.map +2 -2
- package/dist/backend/inputs/TimePicker.js +83 -72
- package/dist/backend/inputs/TimePicker.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +2 -2
- package/dist/backend/utils/customFieldForms.js.map +1 -1
- package/dist/portal/components/PortalEmptyState.js +13 -9
- package/dist/portal/components/PortalEmptyState.js.map +2 -2
- package/dist/primitives/accordion.js +137 -0
- package/dist/primitives/accordion.js.map +7 -0
- package/dist/primitives/alert.js +180 -12
- package/dist/primitives/alert.js.map +2 -2
- package/dist/primitives/calendar.js +122 -32
- package/dist/primitives/calendar.js.map +2 -2
- package/dist/primitives/compact-select.js +47 -0
- package/dist/primitives/compact-select.js.map +7 -0
- package/dist/primitives/counter-input.js +174 -0
- package/dist/primitives/counter-input.js.map +7 -0
- package/dist/primitives/date-picker-helpers.js +79 -0
- package/dist/primitives/date-picker-helpers.js.map +7 -0
- package/dist/primitives/date-picker.js +216 -0
- package/dist/primitives/date-picker.js.map +7 -0
- package/dist/primitives/date-range-picker.js +239 -0
- package/dist/primitives/date-range-picker.js.map +7 -0
- package/dist/primitives/digit-input.js +157 -0
- package/dist/primitives/digit-input.js.map +7 -0
- package/dist/primitives/empty-state.js +93 -0
- package/dist/primitives/empty-state.js.map +7 -0
- package/dist/primitives/inline-input.js +29 -0
- package/dist/primitives/inline-input.js.map +7 -0
- package/dist/primitives/inline-select.js +41 -0
- package/dist/primitives/inline-select.js.map +7 -0
- package/dist/primitives/notification-stack.js +116 -0
- package/dist/primitives/notification-stack.js.map +7 -0
- package/dist/primitives/notification.js +62 -0
- package/dist/primitives/notification.js.map +7 -0
- package/dist/primitives/rich-editor.js +1230 -0
- package/dist/primitives/rich-editor.js.map +7 -0
- package/dist/primitives/select.js +1 -0
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/skeleton.js +55 -0
- package/dist/primitives/skeleton.js.map +7 -0
- package/dist/primitives/tag-input.js +186 -0
- package/dist/primitives/tag-input.js.map +7 -0
- package/dist/primitives/tag.js +66 -13
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/time-picker.js +793 -0
- package/dist/primitives/time-picker.js.map +7 -0
- package/package.json +4 -3
- package/src/ai/AiAssistantLauncher.tsx +8 -0
- package/src/ai/AiChat.tsx +229 -11
- package/src/ai/AiDock.tsx +3 -2
- package/src/ai/ModelPicker.tsx +248 -0
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.test.tsx +180 -0
- package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
- package/src/ai/__tests__/AiDock.test.tsx +12 -1
- package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/useAiChat.ts +29 -16
- package/src/backend/CrudForm.tsx +61 -113
- package/src/backend/DataTable.tsx +17 -5
- package/src/backend/EmptyState.tsx +13 -63
- package/src/backend/FilterOverlay.tsx +38 -22
- package/src/backend/FlashMessages.tsx +23 -24
- package/src/backend/JsonBuilder.tsx +4 -2
- package/src/backend/LogList.tsx +109 -0
- package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
- package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
- package/src/backend/__tests__/LogList.test.tsx +163 -0
- package/src/backend/crud/CollapsibleGroup.tsx +17 -3
- package/src/backend/detail/InlineEditors.tsx +78 -25
- package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
- package/src/backend/inputs/ComboboxInput.tsx +81 -9
- package/src/backend/inputs/DatePicker.tsx +28 -155
- package/src/backend/inputs/DateTimePicker.tsx +25 -193
- package/src/backend/inputs/TagsInput.tsx +10 -13
- package/src/backend/inputs/TimeInput.tsx +2 -1
- package/src/backend/inputs/TimePicker.tsx +99 -70
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
- package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
- package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
- package/src/backend/utils/customFieldForms.ts +2 -2
- package/src/portal/components/PortalEmptyState.tsx +27 -22
- package/src/primitives/__tests__/accordion.test.tsx +197 -0
- package/src/primitives/__tests__/alert.test.tsx +204 -0
- package/src/primitives/__tests__/compact-select.test.tsx +98 -0
- package/src/primitives/__tests__/counter-input.test.tsx +165 -0
- package/src/primitives/__tests__/date-picker.test.tsx +221 -0
- package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
- package/src/primitives/__tests__/digit-input.test.tsx +168 -0
- package/src/primitives/__tests__/empty-state.test.tsx +154 -0
- package/src/primitives/__tests__/inline-input.test.tsx +100 -0
- package/src/primitives/__tests__/inline-select.test.tsx +103 -0
- package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
- package/src/primitives/__tests__/notification.test.tsx +119 -0
- package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
- package/src/primitives/__tests__/skeleton.test.tsx +76 -0
- package/src/primitives/__tests__/tag-input.test.tsx +198 -0
- package/src/primitives/__tests__/tag.test.tsx +123 -0
- package/src/primitives/__tests__/time-picker.test.tsx +392 -0
- package/src/primitives/accordion.tsx +225 -0
- package/src/primitives/alert.tsx +234 -24
- package/src/primitives/calendar.tsx +146 -38
- package/src/primitives/compact-select.tsx +59 -0
- package/src/primitives/counter-input.tsx +211 -0
- package/src/primitives/date-picker-helpers.ts +96 -0
- package/src/primitives/date-picker.tsx +290 -0
- package/src/primitives/date-range-picker.tsx +281 -0
- package/src/primitives/digit-input.tsx +208 -0
- package/src/primitives/empty-state.tsx +140 -0
- package/src/primitives/inline-input.tsx +55 -0
- package/src/primitives/inline-select.tsx +74 -0
- package/src/primitives/notification-stack.tsx +185 -0
- package/src/primitives/notification.tsx +136 -0
- package/src/primitives/rich-editor.tsx +1578 -0
- package/src/primitives/select.tsx +1 -0
- package/src/primitives/skeleton.tsx +70 -0
- package/src/primitives/tag-input.tsx +223 -0
- package/src/primitives/tag.tsx +69 -10
- package/src/primitives/time-picker.tsx +1059 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/primitives/time-picker.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, 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-brand-violet/10 text-brand-violet 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 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 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={effectiveState === 'active' ? 'text-brand-violet/60' : 'text-muted-foreground/60'}>\n {display.suffix}\n </span>\n ) : null}\n </span>\n {right ? (\n <span className=\"flex min-w-0 items-center justify-end gap-1\">\n <span>{right.main}</span>\n {right.suffix ? (\n <span className={effectiveState === 'active' ? 'text-brand-violet/60' : 'text-muted-foreground/60'}>\n {right.suffix}\n </span>\n ) : null}\n </span>\n ) : null}\n {effectiveState === 'active' ? (\n <Check className=\"size-3.5 shrink-0 text-brand-violet\" 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-lg 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-brand-violet/10 text-brand-violet 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.5 rounded-lg px-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 'flex items-center gap-2 overflow-x-auto py-3.5',\n '[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden',\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-[348px] flex-col overflow-hidden rounded-[20px] border border-border bg-background',\n 'shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03),0px_24px_24px_-12px_rgba(51,51,51,0.04),0px_12px_12px_-6px_rgba(51,51,51,0.04)]',\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-brand-violet transition-colors hover:bg-brand-violet/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-brand-violet/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
|
+
"mappings": "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",
|
|
6
|
+
"names": ["hours"]
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3260.1.dc719cac84",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
"@dnd-kit/core": "^6.3.1",
|
|
128
128
|
"@dnd-kit/sortable": "^10.0.0",
|
|
129
129
|
"@dnd-kit/utilities": "^3.2.2",
|
|
130
|
+
"@radix-ui/react-accordion": "^1.2.4",
|
|
130
131
|
"@radix-ui/react-popover": "^1.1.6",
|
|
131
132
|
"@radix-ui/react-radio-group": "^1.2.3",
|
|
132
133
|
"@radix-ui/react-select": "^2.1.6",
|
|
@@ -140,12 +141,12 @@
|
|
|
140
141
|
"remark-gfm": "^4.0.1"
|
|
141
142
|
},
|
|
142
143
|
"peerDependencies": {
|
|
143
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
144
|
+
"@open-mercato/shared": "0.6.1-develop.3260.1.dc719cac84",
|
|
144
145
|
"react": ">=18.0.0",
|
|
145
146
|
"react-dom": ">=18.0.0"
|
|
146
147
|
},
|
|
147
148
|
"devDependencies": {
|
|
148
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
149
|
+
"@open-mercato/shared": "0.6.1-develop.3260.1.dc719cac84",
|
|
149
150
|
"@testing-library/dom": "^10.4.1",
|
|
150
151
|
"@testing-library/jest-dom": "^6.9.1",
|
|
151
152
|
"@testing-library/react": "^16.3.1",
|
|
@@ -131,6 +131,7 @@ const DEFAULT_AGENTS_ENDPOINT = '/api/ai_assistant/ai/agents'
|
|
|
131
131
|
const DEFAULT_HEALTH_ENDPOINT = '/api/ai_assistant/health'
|
|
132
132
|
const AI_ASSISTANT_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/overview'
|
|
133
133
|
const AI_ASSISTANT_SETTINGS_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/settings'
|
|
134
|
+
export const AI_ASSISTANT_LAUNCHER_OPEN_EVENT = 'om:open-ai-assistant-launcher'
|
|
134
135
|
|
|
135
136
|
function isMutationCapable(policy: string | null | undefined): boolean {
|
|
136
137
|
return policy === 'confirm-required' || policy === 'destructive-confirm-required'
|
|
@@ -319,6 +320,13 @@ export function AiAssistantLauncher({
|
|
|
319
320
|
setPickerOpen(true)
|
|
320
321
|
}, [])
|
|
321
322
|
|
|
323
|
+
React.useEffect(() => {
|
|
324
|
+
if (typeof window === 'undefined') return
|
|
325
|
+
const listener = () => openPicker()
|
|
326
|
+
window.addEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)
|
|
327
|
+
return () => window.removeEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)
|
|
328
|
+
}, [openPicker])
|
|
329
|
+
|
|
322
330
|
const handleSelectAgent = React.useCallback((agent: AiAssistantLauncherAgent) => {
|
|
323
331
|
if (dock.state.assistant?.agent === agent.id) {
|
|
324
332
|
dock.dock(dock.state.assistant)
|
package/src/ai/AiChat.tsx
CHANGED
|
@@ -26,7 +26,13 @@ import { Button } from '../primitives/button'
|
|
|
26
26
|
import { IconButton } from '../primitives/icon-button'
|
|
27
27
|
import { Label } from '../primitives/label'
|
|
28
28
|
import { Textarea } from '../primitives/textarea'
|
|
29
|
+
import { apiCall } from '../backend/utils/apiCall'
|
|
29
30
|
import { parseAiContentSegments } from './AiMessageContent'
|
|
31
|
+
import {
|
|
32
|
+
ModelPicker,
|
|
33
|
+
type ModelPickerProvider,
|
|
34
|
+
type ModelPickerValue,
|
|
35
|
+
} from './ModelPicker'
|
|
30
36
|
import { RecordCard } from './records/RecordCard'
|
|
31
37
|
import {
|
|
32
38
|
defaultAiUiPartRegistry,
|
|
@@ -48,6 +54,113 @@ import { useAiShortcuts } from './useAiShortcuts'
|
|
|
48
54
|
// browsers). Images larger than this still upload + send to the LLM as inline
|
|
49
55
|
// base64 server-side; only the in-chat preview is dropped on reload.
|
|
50
56
|
const PREVIEW_DATA_URL_MAX_BYTES = 2 * 1024 * 1024
|
|
57
|
+
const COMPACT_FOOTER_MAX_WIDTH = 640
|
|
58
|
+
|
|
59
|
+
const MODEL_PICKER_STORAGE_PREFIX = 'om-ai-model-picker:'
|
|
60
|
+
|
|
61
|
+
function readModelPickerValue(agentId: string): ModelPickerValue | null {
|
|
62
|
+
if (typeof window === 'undefined') return null
|
|
63
|
+
try {
|
|
64
|
+
const raw = window.localStorage.getItem(`${MODEL_PICKER_STORAGE_PREFIX}${agentId}`)
|
|
65
|
+
if (!raw) return null
|
|
66
|
+
const parsed = JSON.parse(raw) as unknown
|
|
67
|
+
if (
|
|
68
|
+
parsed &&
|
|
69
|
+
typeof parsed === 'object' &&
|
|
70
|
+
typeof (parsed as Record<string, unknown>).providerId === 'string' &&
|
|
71
|
+
typeof (parsed as Record<string, unknown>).modelId === 'string'
|
|
72
|
+
) {
|
|
73
|
+
const value = parsed as ModelPickerValue
|
|
74
|
+
return { providerId: value.providerId, modelId: value.modelId }
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeModelPickerValue(agentId: string, value: ModelPickerValue | null): void {
|
|
83
|
+
if (typeof window === 'undefined') return
|
|
84
|
+
try {
|
|
85
|
+
const key = `${MODEL_PICKER_STORAGE_PREFIX}${agentId}`
|
|
86
|
+
if (value === null) {
|
|
87
|
+
window.localStorage.removeItem(key)
|
|
88
|
+
} else {
|
|
89
|
+
window.localStorage.setItem(key, JSON.stringify({ providerId: value.providerId, modelId: value.modelId }))
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Quota exceeded / privacy mode — silently ignore.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ModelsApiResponse {
|
|
97
|
+
agentId: string
|
|
98
|
+
allowRuntimeModelOverride: boolean
|
|
99
|
+
defaultProviderId: string | null
|
|
100
|
+
defaultModelId: string | null
|
|
101
|
+
defaultProviderName?: string | null
|
|
102
|
+
defaultModelName?: string | null
|
|
103
|
+
providers: ModelPickerProvider[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function useAgentModels(agent: string): {
|
|
107
|
+
providers: ModelPickerProvider[]
|
|
108
|
+
allowRuntimeModelOverride: boolean
|
|
109
|
+
defaultLabel: string | null
|
|
110
|
+
loaded: boolean
|
|
111
|
+
} {
|
|
112
|
+
const [providers, setProviders] = React.useState<ModelPickerProvider[]>([])
|
|
113
|
+
const [allowRuntimeModelOverride, setAllowRuntimeModelOverride] = React.useState(false)
|
|
114
|
+
const [defaultLabel, setDefaultLabel] = React.useState<string | null>(null)
|
|
115
|
+
const [loaded, setLoaded] = React.useState(false)
|
|
116
|
+
|
|
117
|
+
React.useEffect(() => {
|
|
118
|
+
const modelsUrl = `/api/ai_assistant/ai/agents/${encodeURIComponent(agent)}/models`
|
|
119
|
+
setLoaded(false)
|
|
120
|
+
setDefaultLabel(null)
|
|
121
|
+
void apiCall<ModelsApiResponse>(modelsUrl).then((result) => {
|
|
122
|
+
if (!result.ok || !result.result) {
|
|
123
|
+
setLoaded(true)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
setAllowRuntimeModelOverride(result.result.allowRuntimeModelOverride)
|
|
127
|
+
setProviders(result.result.providers)
|
|
128
|
+
setDefaultLabel(
|
|
129
|
+
result.result.defaultProviderName && result.result.defaultModelName
|
|
130
|
+
? `${result.result.defaultProviderName} / ${result.result.defaultModelName}`
|
|
131
|
+
: result.result.defaultProviderId && result.result.defaultModelId
|
|
132
|
+
? `${result.result.defaultProviderId} / ${result.result.defaultModelId}`
|
|
133
|
+
: null,
|
|
134
|
+
)
|
|
135
|
+
setLoaded(true)
|
|
136
|
+
})
|
|
137
|
+
}, [agent])
|
|
138
|
+
|
|
139
|
+
return { providers, allowRuntimeModelOverride, defaultLabel, loaded }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function firstAvailableModelPickerValue(
|
|
143
|
+
providers: ModelPickerProvider[],
|
|
144
|
+
): ModelPickerValue | null {
|
|
145
|
+
for (const provider of providers) {
|
|
146
|
+
const model = provider.models[0]
|
|
147
|
+
if (model) {
|
|
148
|
+
return { providerId: provider.id, modelId: model.id }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isModelPickerValueAvailable(
|
|
155
|
+
value: ModelPickerValue,
|
|
156
|
+
providers: ModelPickerProvider[],
|
|
157
|
+
): boolean {
|
|
158
|
+
return providers.some(
|
|
159
|
+
(provider) =>
|
|
160
|
+
provider.id === value.providerId &&
|
|
161
|
+
provider.models.some((model) => model.id === value.modelId),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
51
164
|
|
|
52
165
|
async function readFileAsDataUrl(file: File): Promise<string | undefined> {
|
|
53
166
|
if (!file.type.startsWith('image/')) return undefined
|
|
@@ -158,6 +271,8 @@ export interface AiChatProps {
|
|
|
158
271
|
welcomeTitle?: string
|
|
159
272
|
/** Welcome description shown below the heading. */
|
|
160
273
|
welcomeDescription?: string
|
|
274
|
+
/** Initial compact composer state used before the footer has been measured. */
|
|
275
|
+
defaultCompactFooter?: boolean
|
|
161
276
|
}
|
|
162
277
|
|
|
163
278
|
interface ServerEmittedUiPartRef {
|
|
@@ -718,6 +833,7 @@ export function AiChat({
|
|
|
718
833
|
contextItems,
|
|
719
834
|
welcomeTitle,
|
|
720
835
|
welcomeDescription,
|
|
836
|
+
defaultCompactFooter = false,
|
|
721
837
|
}: AiChatProps) {
|
|
722
838
|
const t = useT()
|
|
723
839
|
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
@@ -740,6 +856,26 @@ export function AiChat({
|
|
|
740
856
|
const [pendingFiles, setPendingFiles] = React.useState<PendingAttachment[]>([])
|
|
741
857
|
const upload = useAiChatUpload()
|
|
742
858
|
const isUploading = upload.busy
|
|
859
|
+
const footerRef = React.useRef<HTMLDivElement | null>(null)
|
|
860
|
+
const [isCompactFooter, setIsCompactFooter] = React.useState(defaultCompactFooter)
|
|
861
|
+
|
|
862
|
+
React.useEffect(() => {
|
|
863
|
+
const element = footerRef.current
|
|
864
|
+
if (!element || typeof ResizeObserver === 'undefined') return
|
|
865
|
+
|
|
866
|
+
const updateCompactState = (width: number) => {
|
|
867
|
+
setIsCompactFooter(width < COMPACT_FOOTER_MAX_WIDTH)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
updateCompactState(element.getBoundingClientRect().width)
|
|
871
|
+
const observer = new ResizeObserver((entries) => {
|
|
872
|
+
const entry = entries[0]
|
|
873
|
+
if (!entry) return
|
|
874
|
+
updateCompactState(entry.contentRect.width)
|
|
875
|
+
})
|
|
876
|
+
observer.observe(element)
|
|
877
|
+
return () => observer.disconnect()
|
|
878
|
+
}, [])
|
|
743
879
|
|
|
744
880
|
const uploadedAttachmentIds = React.useMemo(
|
|
745
881
|
() =>
|
|
@@ -754,6 +890,66 @@ export function AiChat({
|
|
|
754
890
|
[attachmentIds, uploadedAttachmentIds],
|
|
755
891
|
)
|
|
756
892
|
|
|
893
|
+
const {
|
|
894
|
+
providers: modelProviders,
|
|
895
|
+
allowRuntimeModelOverride,
|
|
896
|
+
defaultLabel: modelDefaultLabel,
|
|
897
|
+
loaded: modelProvidersLoaded,
|
|
898
|
+
} = useAgentModels(agent)
|
|
899
|
+
|
|
900
|
+
const [modelPickerValue, setModelPickerValue] = React.useState<ModelPickerValue | null>(() =>
|
|
901
|
+
readModelPickerValue(agent),
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
const effectiveModelPickerValue = React.useMemo(() => {
|
|
905
|
+
if (!modelProvidersLoaded || !allowRuntimeModelOverride || modelProviders.length === 0) {
|
|
906
|
+
return null
|
|
907
|
+
}
|
|
908
|
+
if (modelPickerValue && isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
|
|
909
|
+
return modelPickerValue
|
|
910
|
+
}
|
|
911
|
+
return firstAvailableModelPickerValue(modelProviders)
|
|
912
|
+
}, [
|
|
913
|
+
allowRuntimeModelOverride,
|
|
914
|
+
modelPickerValue,
|
|
915
|
+
modelProviders,
|
|
916
|
+
modelProvidersLoaded,
|
|
917
|
+
])
|
|
918
|
+
|
|
919
|
+
React.useEffect(() => {
|
|
920
|
+
setModelPickerValue(readModelPickerValue(agent))
|
|
921
|
+
}, [agent])
|
|
922
|
+
|
|
923
|
+
React.useEffect(() => {
|
|
924
|
+
if (!modelProvidersLoaded) return
|
|
925
|
+
if (!allowRuntimeModelOverride || modelProviders.length === 0) {
|
|
926
|
+
if (modelPickerValue !== null) {
|
|
927
|
+
setModelPickerValue(null)
|
|
928
|
+
writeModelPickerValue(agent, null)
|
|
929
|
+
}
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
if (modelPickerValue && !isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
|
|
933
|
+
const fallback = firstAvailableModelPickerValue(modelProviders)
|
|
934
|
+
setModelPickerValue(fallback)
|
|
935
|
+
writeModelPickerValue(agent, fallback)
|
|
936
|
+
}
|
|
937
|
+
}, [
|
|
938
|
+
agent,
|
|
939
|
+
allowRuntimeModelOverride,
|
|
940
|
+
modelPickerValue,
|
|
941
|
+
modelProviders,
|
|
942
|
+
modelProvidersLoaded,
|
|
943
|
+
])
|
|
944
|
+
|
|
945
|
+
const handleModelPickerChange = React.useCallback(
|
|
946
|
+
(value: ModelPickerValue | null) => {
|
|
947
|
+
setModelPickerValue(value)
|
|
948
|
+
writeModelPickerValue(agent, value)
|
|
949
|
+
},
|
|
950
|
+
[agent],
|
|
951
|
+
)
|
|
952
|
+
|
|
757
953
|
const chat = useAiChat({
|
|
758
954
|
agent,
|
|
759
955
|
apiPath,
|
|
@@ -763,6 +959,8 @@ export function AiChat({
|
|
|
763
959
|
initialMessages,
|
|
764
960
|
onError,
|
|
765
961
|
conversationId,
|
|
962
|
+
providerOverride: effectiveModelPickerValue?.providerId ?? null,
|
|
963
|
+
modelOverride: effectiveModelPickerValue?.modelId ?? null,
|
|
766
964
|
})
|
|
767
965
|
|
|
768
966
|
const isStreaming = chat.status === 'streaming'
|
|
@@ -1065,15 +1263,15 @@ export function AiChat({
|
|
|
1065
1263
|
return (
|
|
1066
1264
|
<section
|
|
1067
1265
|
className={cn(
|
|
1068
|
-
'flex h-full min-h-[320px] flex-col gap-3 rounded-lg border border-border bg-background p-3',
|
|
1266
|
+
'flex h-full min-h-[320px] min-w-0 flex-col gap-3 overflow-hidden rounded-lg border border-border bg-background p-3',
|
|
1069
1267
|
className,
|
|
1070
1268
|
)}
|
|
1071
1269
|
aria-label={t('ai_assistant.chat.regionLabel', 'AI chat')}
|
|
1072
1270
|
data-ai-chat-agent={agent}
|
|
1073
1271
|
data-ai-chat-conversation-id={chat.conversationId}
|
|
1074
1272
|
>
|
|
1075
|
-
<div className="flex items-center justify-between gap-2 border-b border-border pb-2">
|
|
1076
|
-
<div className="flex flex-1 items-center gap-2 text-xs text-muted-foreground">
|
|
1273
|
+
<div className="flex min-w-0 items-center justify-between gap-2 border-b border-border pb-2">
|
|
1274
|
+
<div className="flex min-w-0 flex-1 items-center gap-2 text-xs text-muted-foreground">
|
|
1077
1275
|
{contextItems && contextItems.length > 0 ? (
|
|
1078
1276
|
<ContextItemsPill items={contextItems} />
|
|
1079
1277
|
) : (
|
|
@@ -1100,7 +1298,7 @@ export function AiChat({
|
|
|
1100
1298
|
role="log"
|
|
1101
1299
|
aria-live="polite"
|
|
1102
1300
|
aria-label={t('ai_assistant.chat.transcriptLabel', 'Chat transcript')}
|
|
1103
|
-
className="flex-1 space-y-2 overflow-y-auto pr-1"
|
|
1301
|
+
className="min-w-0 flex-1 space-y-2 overflow-y-auto overflow-x-hidden pr-1"
|
|
1104
1302
|
>
|
|
1105
1303
|
{chat.messages.length === 0 ? (
|
|
1106
1304
|
<WelcomeState
|
|
@@ -1153,7 +1351,7 @@ export function AiChat({
|
|
|
1153
1351
|
) : null}
|
|
1154
1352
|
|
|
1155
1353
|
<form
|
|
1156
|
-
className="flex flex-col gap-2"
|
|
1354
|
+
className="flex min-w-0 flex-col gap-2"
|
|
1157
1355
|
onSubmit={(event) => {
|
|
1158
1356
|
event.preventDefault()
|
|
1159
1357
|
handleSubmit()
|
|
@@ -1203,7 +1401,7 @@ export function AiChat({
|
|
|
1203
1401
|
onKeyDown={handleKeyDown}
|
|
1204
1402
|
rows={3}
|
|
1205
1403
|
aria-label={t('ai_assistant.chat.composerLabel', 'Message composer')}
|
|
1206
|
-
className="resize-none"
|
|
1404
|
+
className="min-w-0 resize-none"
|
|
1207
1405
|
/>
|
|
1208
1406
|
<input
|
|
1209
1407
|
ref={fileInputRef}
|
|
@@ -1214,8 +1412,13 @@ export function AiChat({
|
|
|
1214
1412
|
onChange={handleFileSelect}
|
|
1215
1413
|
data-ai-chat-file-input=""
|
|
1216
1414
|
/>
|
|
1217
|
-
<div
|
|
1218
|
-
|
|
1415
|
+
<div
|
|
1416
|
+
ref={footerRef}
|
|
1417
|
+
className="flex min-w-0 items-center justify-between gap-2"
|
|
1418
|
+
data-ai-chat-footer=""
|
|
1419
|
+
data-ai-chat-footer-compact={isCompactFooter ? 'true' : 'false'}
|
|
1420
|
+
>
|
|
1421
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
1219
1422
|
<IconButton
|
|
1220
1423
|
type="button"
|
|
1221
1424
|
variant="ghost"
|
|
@@ -1226,7 +1429,19 @@ export function AiChat({
|
|
|
1226
1429
|
>
|
|
1227
1430
|
<Paperclip className="size-4" aria-hidden />
|
|
1228
1431
|
</IconButton>
|
|
1229
|
-
|
|
1432
|
+
{allowRuntimeModelOverride && modelProviders.length > 0 ? (
|
|
1433
|
+
<ModelPicker
|
|
1434
|
+
agentId={agent}
|
|
1435
|
+
value={effectiveModelPickerValue}
|
|
1436
|
+
onChange={handleModelPickerChange}
|
|
1437
|
+
availableProviders={modelProviders}
|
|
1438
|
+
disabled={isBusy}
|
|
1439
|
+
compact={isCompactFooter}
|
|
1440
|
+
defaultLabel={modelDefaultLabel}
|
|
1441
|
+
className="shrink-0"
|
|
1442
|
+
/>
|
|
1443
|
+
) : null}
|
|
1444
|
+
<p className={cn('text-xs text-muted-foreground', isCompactFooter && 'hidden')}>
|
|
1230
1445
|
{hasUploadingFiles || isUploading
|
|
1231
1446
|
? t(
|
|
1232
1447
|
'ai_assistant.chat.uploadingHint',
|
|
@@ -1238,7 +1453,7 @@ export function AiChat({
|
|
|
1238
1453
|
)}
|
|
1239
1454
|
</p>
|
|
1240
1455
|
</div>
|
|
1241
|
-
<div className="flex items-center gap-2">
|
|
1456
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
1242
1457
|
{isStreaming ? (
|
|
1243
1458
|
<IconButton
|
|
1244
1459
|
type="button"
|
|
@@ -1269,9 +1484,12 @@ export function AiChat({
|
|
|
1269
1484
|
? t('ai_assistant.chat.sendWaitingForUpload', 'Waiting for upload to finish…')
|
|
1270
1485
|
: undefined
|
|
1271
1486
|
}
|
|
1487
|
+
className={cn(isCompactFooter && 'w-8 px-0')}
|
|
1272
1488
|
>
|
|
1273
1489
|
<Send className="size-4" aria-hidden />
|
|
1274
|
-
|
|
1490
|
+
{!isCompactFooter ? (
|
|
1491
|
+
<span>{t('ai_assistant.chat.send', 'Send message')}</span>
|
|
1492
|
+
) : null}
|
|
1275
1493
|
</Button>
|
|
1276
1494
|
</div>
|
|
1277
1495
|
</div>
|
package/src/ai/AiDock.tsx
CHANGED
|
@@ -381,7 +381,7 @@ function AiDockPanel({
|
|
|
381
381
|
// primary surface (full-screen sheet) and a fixed side panel would
|
|
382
382
|
// crowd the viewport.
|
|
383
383
|
'hidden lg:flex',
|
|
384
|
-
'fixed top-0 right-0 z-overlay h-svh flex-col border-l bg-background shadow-lg',
|
|
384
|
+
'fixed top-0 right-0 z-overlay h-svh min-w-0 flex-col overflow-hidden border-l bg-background shadow-lg',
|
|
385
385
|
collapsed ? 'w-12' : '',
|
|
386
386
|
)}
|
|
387
387
|
style={collapsed ? undefined : { width }}
|
|
@@ -478,7 +478,7 @@ function DockedChatBody({ assistant }: { assistant: AiDockedAssistant }) {
|
|
|
478
478
|
return (
|
|
479
479
|
<>
|
|
480
480
|
<ChatPaneTabs agentId={assistant.agent} className="border-b" />
|
|
481
|
-
<div className="min-h-0 flex-1" data-ai-dock-chat-container="">
|
|
481
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-hidden" data-ai-dock-chat-container="">
|
|
482
482
|
{session ? (
|
|
483
483
|
<React.Suspense fallback={null}>
|
|
484
484
|
<LazyAiChat
|
|
@@ -491,6 +491,7 @@ function DockedChatBody({ assistant }: { assistant: AiDockedAssistant }) {
|
|
|
491
491
|
conversationId={session.conversationId}
|
|
492
492
|
pageContext={assistant.pageContext}
|
|
493
493
|
className="h-full"
|
|
494
|
+
defaultCompactFooter
|
|
494
495
|
placeholder={assistant.placeholder}
|
|
495
496
|
suggestions={assistant.suggestions}
|
|
496
497
|
contextItems={assistant.contextItems}
|