@open-mercato/ui 0.6.7 → 0.6.8-develop.6874.1.982d6097d8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -2
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +2 -54
  5. package/dist/ai/AiChat.js +26 -7
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/LoopDisabledBanner.js +1 -1
  8. package/dist/ai/LoopDisabledBanner.js.map +2 -2
  9. package/dist/ai/parts/ConfirmationCard.js +4 -4
  10. package/dist/ai/parts/ConfirmationCard.js.map +2 -2
  11. package/dist/ai/parts/FieldDiffCard.js +2 -2
  12. package/dist/ai/parts/FieldDiffCard.js.map +2 -2
  13. package/dist/ai/parts/MutationResultCard.js +3 -3
  14. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  15. package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
  16. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
  17. package/dist/backend/AppShell.js +43 -4
  18. package/dist/backend/AppShell.js.map +2 -2
  19. package/dist/backend/AuthSessionGuard.js +14 -0
  20. package/dist/backend/AuthSessionGuard.js.map +2 -2
  21. package/dist/backend/BackendChromeProvider.js +6 -1
  22. package/dist/backend/BackendChromeProvider.js.map +2 -2
  23. package/dist/backend/CrudForm.js +6 -4
  24. package/dist/backend/CrudForm.js.map +2 -2
  25. package/dist/backend/DataTable.js +20 -18
  26. package/dist/backend/DataTable.js.map +2 -2
  27. package/dist/backend/JsonBuilder.js +3 -3
  28. package/dist/backend/JsonBuilder.js.map +2 -2
  29. package/dist/backend/NextStepCallout.js +4 -4
  30. package/dist/backend/NextStepCallout.js.map +1 -1
  31. package/dist/backend/PerspectiveSidebar.js +1 -1
  32. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  33. package/dist/backend/ProfileDropdown.js +2 -1
  34. package/dist/backend/ProfileDropdown.js.map +2 -2
  35. package/dist/backend/RowActions.js +2 -2
  36. package/dist/backend/RowActions.js.map +2 -2
  37. package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
  38. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  39. package/dist/backend/crud/useGroupOrder.js +14 -21
  40. package/dist/backend/crud/useGroupOrder.js.map +2 -2
  41. package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
  42. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
  43. package/dist/backend/dashboard/DashboardScreen.js +3 -3
  44. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  45. package/dist/backend/detail/AddressTiles.js +4 -5
  46. package/dist/backend/detail/AddressTiles.js.map +2 -2
  47. package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
  48. package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
  49. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  50. package/dist/backend/detail/AttachmentsSection.js +1 -1
  51. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  52. package/dist/backend/detail/NotesSection.js +1 -1
  53. package/dist/backend/detail/NotesSection.js.map +2 -2
  54. package/dist/backend/detail/TagsSection.js +1 -1
  55. package/dist/backend/detail/TagsSection.js.map +2 -2
  56. package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
  57. package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
  58. package/dist/backend/forms/FormHeader.js +1 -2
  59. package/dist/backend/forms/FormHeader.js.map +2 -2
  60. package/dist/backend/icons/lucideRegistry.generated.js +1 -25
  61. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  62. package/dist/backend/icons/lucideRegistry.js +3 -3
  63. package/dist/backend/icons/lucideRegistry.js.map +2 -2
  64. package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
  65. package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
  66. package/dist/backend/injection/spotIds.js +23 -18
  67. package/dist/backend/injection/spotIds.js.map +2 -2
  68. package/dist/backend/inputs/ComboboxInput.js.map +1 -1
  69. package/dist/backend/messages/EmailThreadsPanel.js +1 -1
  70. package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
  71. package/dist/backend/perspectiveState.js +34 -0
  72. package/dist/backend/perspectiveState.js.map +7 -0
  73. package/dist/backend/schedule/ScheduleAgenda.js +3 -3
  74. package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
  75. package/dist/backend/schedule/ScheduleGrid.js +3 -3
  76. package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
  77. package/dist/backend/schedule/ScheduleToolbar.js +87 -70
  78. package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
  79. package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
  80. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  81. package/dist/backend/sidebar/customization-helpers.js +30 -0
  82. package/dist/backend/sidebar/customization-helpers.js.map +2 -2
  83. package/dist/backend/utils/apiCall.js +21 -1
  84. package/dist/backend/utils/apiCall.js.map +2 -2
  85. package/dist/backend/utils/customFieldDefs.js.map +2 -2
  86. package/dist/backend/utils/customFieldForms.js +10 -9
  87. package/dist/backend/utils/customFieldForms.js.map +2 -2
  88. package/dist/backend/utils/fetchAllPages.js +57 -0
  89. package/dist/backend/utils/fetchAllPages.js.map +7 -0
  90. package/dist/backend/utils/nav.js +5 -2
  91. package/dist/backend/utils/nav.js.map +2 -2
  92. package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
  93. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  94. package/dist/frontend/LanguageSwitcher.js +2 -1
  95. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  96. package/dist/portal/PortalContext.js +1 -1
  97. package/dist/portal/PortalContext.js.map +2 -2
  98. package/dist/portal/hooks/portalBridgeStatus.js +25 -0
  99. package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
  100. package/dist/portal/hooks/usePortalEventBridge.js +6 -0
  101. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  102. package/dist/portal/hooks/usePortalNotifications.js +42 -4
  103. package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
  104. package/dist/portal/utils/nav.js +2 -3
  105. package/dist/portal/utils/nav.js.map +2 -2
  106. package/dist/primitives/ErrorNotice.js +1 -1
  107. package/dist/primitives/ErrorNotice.js.map +2 -2
  108. package/dist/primitives/Notice.js +1 -1
  109. package/dist/primitives/Notice.js.map +1 -1
  110. package/dist/primitives/button.js +9 -4
  111. package/dist/primitives/button.js.map +2 -2
  112. package/dist/theme/ThemeProvider.js +1 -1
  113. package/dist/theme/ThemeProvider.js.map +2 -2
  114. package/dist/theme/index.js +3 -0
  115. package/dist/theme/index.js.map +2 -2
  116. package/dist/theme/theme-init-script.js +17 -0
  117. package/dist/theme/theme-init-script.js.map +7 -0
  118. package/figma/alert.figma.tsx +35 -0
  119. package/figma/badge.figma.tsx +33 -0
  120. package/figma/button.figma.tsx +33 -0
  121. package/figma/checkbox.figma.tsx +28 -0
  122. package/figma/drawer.figma.tsx +40 -0
  123. package/figma/input-variants.figma.tsx +58 -0
  124. package/figma/radio.figma.tsx +16 -0
  125. package/figma/select.figma.tsx +24 -0
  126. package/figma/status-badge.figma.tsx +24 -0
  127. package/figma/switch.figma.tsx +28 -0
  128. package/figma/tabs.figma.tsx +22 -0
  129. package/figma/tag.figma.tsx +27 -0
  130. package/figma.config.json +6 -0
  131. package/jest.config.cjs +2 -1
  132. package/package.json +21 -18
  133. package/scripts/lucideRegistrySource.cjs +133 -0
  134. package/src/ai/AiChat.tsx +36 -7
  135. package/src/ai/LoopDisabledBanner.tsx +1 -1
  136. package/src/ai/__tests__/AiChat.test.tsx +31 -0
  137. package/src/ai/parts/ConfirmationCard.tsx +4 -4
  138. package/src/ai/parts/FieldDiffCard.tsx +2 -2
  139. package/src/ai/parts/MutationResultCard.tsx +3 -3
  140. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
  141. package/src/backend/AppShell.tsx +55 -4
  142. package/src/backend/AuthSessionGuard.tsx +29 -0
  143. package/src/backend/BackendChromeProvider.tsx +19 -1
  144. package/src/backend/CrudForm.tsx +18 -4
  145. package/src/backend/DataTable.tsx +36 -19
  146. package/src/backend/JsonBuilder.tsx +3 -3
  147. package/src/backend/NextStepCallout.tsx +4 -4
  148. package/src/backend/PerspectiveSidebar.tsx +1 -1
  149. package/src/backend/ProfileDropdown.tsx +1 -0
  150. package/src/backend/RowActions.tsx +2 -2
  151. package/src/backend/__tests__/AppShell.test.tsx +65 -7
  152. package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
  153. package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
  154. package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
  155. package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
  156. package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
  157. package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
  158. package/src/backend/__tests__/nav-utils.test.ts +139 -0
  159. package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
  160. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
  161. package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
  162. package/src/backend/crud/useGroupOrder.ts +30 -25
  163. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
  164. package/src/backend/dashboard/DashboardScreen.tsx +3 -3
  165. package/src/backend/detail/AddressTiles.tsx +3 -6
  166. package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
  167. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  168. package/src/backend/detail/AttachmentsSection.tsx +1 -1
  169. package/src/backend/detail/NotesSection.tsx +1 -1
  170. package/src/backend/detail/TagsSection.tsx +1 -1
  171. package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
  172. package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
  173. package/src/backend/forms/FormHeader.tsx +1 -2
  174. package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
  175. package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
  176. package/src/backend/icons/lucideRegistry.ts +2 -2
  177. package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
  178. package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
  179. package/src/backend/injection/spotIds.ts +23 -18
  180. package/src/backend/inputs/ComboboxInput.tsx +1 -1
  181. package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
  182. package/src/backend/perspectiveState.ts +44 -0
  183. package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
  184. package/src/backend/schedule/ScheduleGrid.tsx +3 -3
  185. package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
  186. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
  187. package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
  188. package/src/backend/sidebar/customization-helpers.ts +47 -0
  189. package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
  190. package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
  191. package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
  192. package/src/backend/utils/apiCall.ts +29 -1
  193. package/src/backend/utils/customFieldDefs.ts +1 -0
  194. package/src/backend/utils/customFieldForms.ts +15 -11
  195. package/src/backend/utils/fetchAllPages.ts +93 -0
  196. package/src/backend/utils/nav.ts +22 -2
  197. package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
  198. package/src/frontend/LanguageSwitcher.tsx +1 -0
  199. package/src/portal/PortalContext.tsx +1 -1
  200. package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
  201. package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
  202. package/src/portal/hooks/portalBridgeStatus.ts +29 -0
  203. package/src/portal/hooks/usePortalEventBridge.ts +6 -0
  204. package/src/portal/hooks/usePortalNotifications.ts +55 -12
  205. package/src/portal/utils/__tests__/nav.test.ts +2 -2
  206. package/src/portal/utils/nav.ts +3 -4
  207. package/src/primitives/ErrorNotice.tsx +1 -1
  208. package/src/primitives/Notice.tsx +5 -5
  209. package/src/primitives/__tests__/button.test.tsx +70 -0
  210. package/src/primitives/button.tsx +10 -4
  211. package/src/theme/ThemeProvider.tsx +1 -2
  212. package/src/theme/__tests__/theme-init-script.test.ts +70 -0
  213. package/src/theme/index.ts +1 -0
  214. package/src/theme/theme-init-script.ts +23 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/inputs/ComboboxInput.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps \u2014 resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -66,7 +66,7 @@ function EmailThreadsPanel({
66
66
  ] })
67
67
  ] }),
68
68
  !canCompose && composeDisabledHint ? /* @__PURE__ */ jsx("div", { children: composeDisabledHint }) : null,
69
- error ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null,
69
+ error ? /* @__PURE__ */ jsx(Alert, { status: "error", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null,
70
70
  loading && threads.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground", children: [
71
71
  /* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
72
72
  t("ui.email.threads.loading", "Loading conversations\u2026")
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/messages/EmailThreadsPanel.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ArrowDownLeft, ArrowUpRight, Check, Mail, RefreshCw, Reply, TriangleAlert } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { Badge } from '../../primitives/badge'\nimport { Spinner } from '../../primitives/spinner'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { EmptyState } from '../EmptyState'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type EmailThreadDirection = 'inbound' | 'outbound'\n\n/**\n * Optimistic delivery status for a just-sent outbound message. Server-confirmed\n * messages leave this `undefined` and render without a status indicator.\n */\nexport type EmailThreadMessageStatus = 'sending' | 'sent' | 'failed'\n\nexport type EmailThreadMessage = {\n id: string\n messageId: string | null\n rfcMessageId: string | null\n references: string[]\n direction: EmailThreadDirection\n fromName: string | null\n fromEmail: string | null\n to: string[]\n cc: string[]\n subject: string | null\n bodyText: string | null\n sentAt: string\n providerKey: string | null\n /** Optimistic send status; absent on server-confirmed messages. */\n status?: EmailThreadMessageStatus\n /** Human-readable failure reason shown when `status === 'failed'`. */\n statusError?: string | null\n}\n\nexport type EmailThread = {\n threadKey: string\n subject: string | null\n preview: string | null\n participants: string[]\n lastMessageAt: string\n messageCount: number\n providerKey: string | null\n lastDirection: EmailThreadDirection\n messages: EmailThreadMessage[]\n}\n\nexport type EmailThreadsPanelProps = {\n threads: EmailThread[]\n loading?: boolean\n error?: string | null\n /** When false, compose/reply controls are hidden and `composeDisabledHint` is shown. */\n canCompose?: boolean\n composeDisabledHint?: React.ReactNode\n onComposeNew?: () => void\n onReply?: (thread: EmailThread) => void\n onRefresh?: () => void\n /** Invoked when the user retries a message whose `status === 'failed'`. */\n onRetry?: (message: EmailThreadMessage) => void\n className?: string\n}\n\nfunction formatWhen(value: string): string {\n return formatDateTime(value) ?? value\n}\n\n/**\n * Entity-agnostic Gmail-style email thread viewer. The host supplies the\n * already-fetched threads plus compose/reply/refresh handlers; this component\n * owns only the master/detail layout, selection, and conversation rendering, so\n * it can be dropped onto the CRM Person page, a Company page, or any module.\n */\nexport function EmailThreadsPanel({\n threads,\n loading = false,\n error = null,\n canCompose = false,\n composeDisabledHint,\n onComposeNew,\n onReply,\n onRefresh,\n onRetry,\n className,\n}: EmailThreadsPanelProps) {\n const t = useT()\n const [selectedKey, setSelectedKey] = React.useState<string | null>(null)\n\n // Keep a valid selection as the thread set changes (refresh, new email).\n React.useEffect(() => {\n if (threads.length === 0) {\n setSelectedKey(null)\n return\n }\n setSelectedKey((current) =>\n current && threads.some((thread) => thread.threadKey === current)\n ? current\n : threads[0].threadKey,\n )\n }, [threads])\n\n const selected = React.useMemo(\n () => threads.find((thread) => thread.threadKey === selectedKey) ?? null,\n [threads, selectedKey],\n )\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t('ui.email.threads.count', '{count} conversations', { count: threads.length })}\n </div>\n <div className=\"flex items-center gap-2\">\n {onRefresh ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n onClick={onRefresh}\n disabled={loading}\n >\n <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />\n {t('ui.email.threads.refresh', 'Refresh')}\n </Button>\n ) : null}\n {canCompose && onComposeNew ? (\n <Button type=\"button\" size=\"sm\" className=\"gap-2\" onClick={onComposeNew}>\n <Mail className=\"h-4 w-4\" />\n {t('ui.email.threads.new', 'New email')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}\n\n {error ? (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n {loading && threads.length === 0 ? (\n <div className=\"flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('ui.email.threads.loading', 'Loading conversations\u2026')}\n </div>\n ) : threads.length === 0 ? (\n <EmptyState\n icon={<Mail className=\"h-6 w-6\" />}\n title={t('ui.email.threads.empty.title', 'No emails yet')}\n description={t(\n 'ui.email.threads.empty.description',\n 'Emails you send or receive with this contact will appear here, grouped into conversations.',\n )}\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-3\">\n {/* Master: thread list */}\n <div className=\"md:col-span-1\">\n <ul className=\"max-h-96 divide-y divide-border overflow-y-auto rounded-md border border-border\">\n {threads.map((thread) => {\n const isSelected = thread.threadKey === selectedKey\n return (\n <li key={thread.threadKey}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setSelectedKey(thread.threadKey)}\n className={cn(\n 'h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-none p-3 text-left hover:bg-muted',\n isSelected ? 'bg-muted' : 'bg-transparent',\n )}\n aria-current={isSelected}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {thread.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatWhen(thread.lastMessageAt)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-muted-foreground\">\n {thread.participants.join(', ') ||\n t('ui.email.threads.unknownParticipant', 'Unknown sender')}\n </span>\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {thread.messageCount}\n </Badge>\n </div>\n {thread.preview ? (\n <span className=\"line-clamp-1 text-xs text-muted-foreground\">\n {thread.preview}\n </span>\n ) : null}\n </Button>\n </li>\n )\n })}\n </ul>\n </div>\n\n {/* Detail: conversation */}\n <div className=\"md:col-span-2\">\n {selected ? (\n <div className=\"flex flex-col gap-3 rounded-md border border-border p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {selected.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </h3>\n {canCompose && onReply ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0 gap-2\"\n onClick={() => onReply(selected)}\n >\n <Reply className=\"h-4 w-4\" />\n {t('ui.email.threads.reply', 'Reply')}\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-col gap-3\">\n {selected.messages.map((message) => (\n <EmailMessageCard key={message.id} message={message} onRetry={onRetry} />\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-center rounded-md border border-border p-10 text-sm text-muted-foreground\">\n {t('ui.email.threads.selectPrompt', 'Select a conversation to read it.')}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction EmailMessageCard({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n const isOutbound = message.direction === 'outbound'\n const fromLabel = message.fromName\n ? `${message.fromName}${message.fromEmail ? ` <${message.fromEmail}>` : ''}`\n : message.fromEmail ?? t('ui.email.threads.unknownParticipant', 'Unknown sender')\n return (\n <div\n className={`rounded-md border border-border p-3 ${isOutbound ? 'bg-accent' : 'bg-muted'}`}\n >\n <div className=\"mb-2 flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {isOutbound ? (\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n ) : (\n <ArrowDownLeft className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"font-medium text-foreground\">\n {isOutbound ? t('ui.email.threads.you', 'You') : fromLabel}\n </span>\n {message.to.length > 0 ? (\n <span className=\"truncate\">\n {t('ui.email.threads.toLabel', 'to {recipients}', {\n recipients: message.to.join(', '),\n })}\n </span>\n ) : null}\n </div>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatWhen(message.sentAt)}</span>\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">\n {message.bodyText || t('ui.email.threads.noBody', '(no content)')}\n </div>\n <EmailMessageStatus message={message} onRetry={onRetry} />\n </div>\n )\n}\n\n/**\n * Renders the optimistic send-status footer for an outbound message: a spinner\n * while sending, a success check once confirmed, or an error with a Retry action\n * on delivery failure. Server-confirmed messages have no `status` and render\n * nothing here.\n */\nfunction EmailMessageStatus({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n if (message.status === 'sending') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-muted-foreground\" aria-live=\"polite\">\n <Spinner className=\"h-3 w-3\" />\n {t('ui.email.threads.status.sending', 'Sending\u2026')}\n </div>\n )\n }\n if (message.status === 'sent') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-status-success-text\" aria-live=\"polite\">\n <Check className=\"h-3.5 w-3.5 text-status-success-icon\" />\n {t('ui.email.threads.status.sent', 'Sent')}\n </div>\n )\n }\n if (message.status === 'failed') {\n return (\n <div\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n aria-live=\"polite\"\n >\n <span className=\"flex items-center gap-1.5\">\n <TriangleAlert className=\"h-3.5 w-3.5 text-status-error-icon\" />\n {message.statusError || t('ui.email.threads.status.failed', 'Not delivered')}\n </span>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto px-2 py-0.5 text-xs\"\n onClick={() => onRetry(message)}\n >\n {t('ui.email.threads.status.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )\n }\n return null\n}\n\nexport default EmailThreadsPanel\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ArrowDownLeft, ArrowUpRight, Check, Mail, RefreshCw, Reply, TriangleAlert } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { Badge } from '../../primitives/badge'\nimport { Spinner } from '../../primitives/spinner'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { EmptyState } from '../EmptyState'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type EmailThreadDirection = 'inbound' | 'outbound'\n\n/**\n * Optimistic delivery status for a just-sent outbound message. Server-confirmed\n * messages leave this `undefined` and render without a status indicator.\n */\nexport type EmailThreadMessageStatus = 'sending' | 'sent' | 'failed'\n\nexport type EmailThreadMessage = {\n id: string\n messageId: string | null\n rfcMessageId: string | null\n references: string[]\n direction: EmailThreadDirection\n fromName: string | null\n fromEmail: string | null\n to: string[]\n cc: string[]\n subject: string | null\n bodyText: string | null\n sentAt: string\n providerKey: string | null\n /** Optimistic send status; absent on server-confirmed messages. */\n status?: EmailThreadMessageStatus\n /** Human-readable failure reason shown when `status === 'failed'`. */\n statusError?: string | null\n}\n\nexport type EmailThread = {\n threadKey: string\n subject: string | null\n preview: string | null\n participants: string[]\n lastMessageAt: string\n messageCount: number\n providerKey: string | null\n lastDirection: EmailThreadDirection\n messages: EmailThreadMessage[]\n}\n\nexport type EmailThreadsPanelProps = {\n threads: EmailThread[]\n loading?: boolean\n error?: string | null\n /** When false, compose/reply controls are hidden and `composeDisabledHint` is shown. */\n canCompose?: boolean\n composeDisabledHint?: React.ReactNode\n onComposeNew?: () => void\n onReply?: (thread: EmailThread) => void\n onRefresh?: () => void\n /** Invoked when the user retries a message whose `status === 'failed'`. */\n onRetry?: (message: EmailThreadMessage) => void\n className?: string\n}\n\nfunction formatWhen(value: string): string {\n return formatDateTime(value) ?? value\n}\n\n/**\n * Entity-agnostic Gmail-style email thread viewer. The host supplies the\n * already-fetched threads plus compose/reply/refresh handlers; this component\n * owns only the master/detail layout, selection, and conversation rendering, so\n * it can be dropped onto the CRM Person page, a Company page, or any module.\n */\nexport function EmailThreadsPanel({\n threads,\n loading = false,\n error = null,\n canCompose = false,\n composeDisabledHint,\n onComposeNew,\n onReply,\n onRefresh,\n onRetry,\n className,\n}: EmailThreadsPanelProps) {\n const t = useT()\n const [selectedKey, setSelectedKey] = React.useState<string | null>(null)\n\n // Keep a valid selection as the thread set changes (refresh, new email).\n React.useEffect(() => {\n if (threads.length === 0) {\n setSelectedKey(null)\n return\n }\n setSelectedKey((current) =>\n current && threads.some((thread) => thread.threadKey === current)\n ? current\n : threads[0].threadKey,\n )\n }, [threads])\n\n const selected = React.useMemo(\n () => threads.find((thread) => thread.threadKey === selectedKey) ?? null,\n [threads, selectedKey],\n )\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t('ui.email.threads.count', '{count} conversations', { count: threads.length })}\n </div>\n <div className=\"flex items-center gap-2\">\n {onRefresh ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n onClick={onRefresh}\n disabled={loading}\n >\n <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />\n {t('ui.email.threads.refresh', 'Refresh')}\n </Button>\n ) : null}\n {canCompose && onComposeNew ? (\n <Button type=\"button\" size=\"sm\" className=\"gap-2\" onClick={onComposeNew}>\n <Mail className=\"h-4 w-4\" />\n {t('ui.email.threads.new', 'New email')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}\n\n {error ? (\n <Alert status=\"error\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n {loading && threads.length === 0 ? (\n <div className=\"flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('ui.email.threads.loading', 'Loading conversations\u2026')}\n </div>\n ) : threads.length === 0 ? (\n <EmptyState\n icon={<Mail className=\"h-6 w-6\" />}\n title={t('ui.email.threads.empty.title', 'No emails yet')}\n description={t(\n 'ui.email.threads.empty.description',\n 'Emails you send or receive with this contact will appear here, grouped into conversations.',\n )}\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-3\">\n {/* Master: thread list */}\n <div className=\"md:col-span-1\">\n <ul className=\"max-h-96 divide-y divide-border overflow-y-auto rounded-md border border-border\">\n {threads.map((thread) => {\n const isSelected = thread.threadKey === selectedKey\n return (\n <li key={thread.threadKey}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setSelectedKey(thread.threadKey)}\n className={cn(\n 'h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-none p-3 text-left hover:bg-muted',\n isSelected ? 'bg-muted' : 'bg-transparent',\n )}\n aria-current={isSelected}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {thread.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatWhen(thread.lastMessageAt)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-muted-foreground\">\n {thread.participants.join(', ') ||\n t('ui.email.threads.unknownParticipant', 'Unknown sender')}\n </span>\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {thread.messageCount}\n </Badge>\n </div>\n {thread.preview ? (\n <span className=\"line-clamp-1 text-xs text-muted-foreground\">\n {thread.preview}\n </span>\n ) : null}\n </Button>\n </li>\n )\n })}\n </ul>\n </div>\n\n {/* Detail: conversation */}\n <div className=\"md:col-span-2\">\n {selected ? (\n <div className=\"flex flex-col gap-3 rounded-md border border-border p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {selected.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </h3>\n {canCompose && onReply ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0 gap-2\"\n onClick={() => onReply(selected)}\n >\n <Reply className=\"h-4 w-4\" />\n {t('ui.email.threads.reply', 'Reply')}\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-col gap-3\">\n {selected.messages.map((message) => (\n <EmailMessageCard key={message.id} message={message} onRetry={onRetry} />\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-center rounded-md border border-border p-10 text-sm text-muted-foreground\">\n {t('ui.email.threads.selectPrompt', 'Select a conversation to read it.')}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction EmailMessageCard({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n const isOutbound = message.direction === 'outbound'\n const fromLabel = message.fromName\n ? `${message.fromName}${message.fromEmail ? ` <${message.fromEmail}>` : ''}`\n : message.fromEmail ?? t('ui.email.threads.unknownParticipant', 'Unknown sender')\n return (\n <div\n className={`rounded-md border border-border p-3 ${isOutbound ? 'bg-accent' : 'bg-muted'}`}\n >\n <div className=\"mb-2 flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {isOutbound ? (\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n ) : (\n <ArrowDownLeft className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"font-medium text-foreground\">\n {isOutbound ? t('ui.email.threads.you', 'You') : fromLabel}\n </span>\n {message.to.length > 0 ? (\n <span className=\"truncate\">\n {t('ui.email.threads.toLabel', 'to {recipients}', {\n recipients: message.to.join(', '),\n })}\n </span>\n ) : null}\n </div>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatWhen(message.sentAt)}</span>\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">\n {message.bodyText || t('ui.email.threads.noBody', '(no content)')}\n </div>\n <EmailMessageStatus message={message} onRetry={onRetry} />\n </div>\n )\n}\n\n/**\n * Renders the optimistic send-status footer for an outbound message: a spinner\n * while sending, a success check once confirmed, or an error with a Retry action\n * on delivery failure. Server-confirmed messages have no `status` and render\n * nothing here.\n */\nfunction EmailMessageStatus({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n if (message.status === 'sending') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-muted-foreground\" aria-live=\"polite\">\n <Spinner className=\"h-3 w-3\" />\n {t('ui.email.threads.status.sending', 'Sending\u2026')}\n </div>\n )\n }\n if (message.status === 'sent') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-status-success-text\" aria-live=\"polite\">\n <Check className=\"h-3.5 w-3.5 text-status-success-icon\" />\n {t('ui.email.threads.status.sent', 'Sent')}\n </div>\n )\n }\n if (message.status === 'failed') {\n return (\n <div\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n aria-live=\"polite\"\n >\n <span className=\"flex items-center gap-1.5\">\n <TriangleAlert className=\"h-3.5 w-3.5 text-status-error-icon\" />\n {message.statusError || t('ui.email.threads.status.failed', 'Not delivered')}\n </span>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto px-2 py-0.5 text-xs\"\n onClick={() => onRetry(message)}\n >\n {t('ui.email.threads.status.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )\n }\n return null\n}\n\nexport default EmailThreadsPanel\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,34 @@
1
+ const PERSPECTIVE_COOKIE_PREFIX = "om_table_perspective";
2
+ const PERSPECTIVE_STORAGE_PREFIX = "om_table_perspective_snapshot";
3
+ function clearAllPerspectiveState() {
4
+ if (typeof window !== "undefined") {
5
+ try {
6
+ const storage = window.localStorage;
7
+ const staleKeys = [];
8
+ for (let index = 0; index < storage.length; index += 1) {
9
+ const key = storage.key(index);
10
+ if (key && key.startsWith(`${PERSPECTIVE_STORAGE_PREFIX}:`)) staleKeys.push(key);
11
+ }
12
+ for (const key of staleKeys) storage.removeItem(key);
13
+ } catch {
14
+ }
15
+ }
16
+ if (typeof document !== "undefined") {
17
+ try {
18
+ const cookies = document.cookie ? document.cookie.split(";") : [];
19
+ for (const cookie of cookies) {
20
+ const name = cookie.split("=")[0]?.trim();
21
+ if (name && name.startsWith(`${PERSPECTIVE_COOKIE_PREFIX}:`)) {
22
+ document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
23
+ }
24
+ }
25
+ } catch {
26
+ }
27
+ }
28
+ }
29
+ export {
30
+ PERSPECTIVE_COOKIE_PREFIX,
31
+ PERSPECTIVE_STORAGE_PREFIX,
32
+ clearAllPerspectiveState
33
+ };
34
+ //# sourceMappingURL=perspectiveState.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backend/perspectiveState.ts"],
4
+ "sourcesContent": ["export const PERSPECTIVE_COOKIE_PREFIX = 'om_table_perspective'\nexport const PERSPECTIVE_STORAGE_PREFIX = 'om_table_perspective_snapshot'\n\n/**\n * Purge every browser-local DataTable perspective snapshot and active-view cookie\n * across all tables in this origin. Snapshots persist the unsaved/live table state\n * (column widths, order, visibility) keyed only by `tableId`, so they are shared by\n * every account that signs in on the same browser profile. Call this at the auth\n * identity boundary so one user's unsaved layout never carries over to a different\n * user \u2014 including a different tenant \u2014 reusing the same browser tab (#4185).\n *\n * Lives here rather than in DataTable so the backend shell can call it without\n * pulling the whole table implementation into the bundle of every backend page.\n * `@open-mercato/ui/backend/DataTable` re-exports it, which is the published\n * import path and must stay stable.\n */\nexport function clearAllPerspectiveState(): void {\n if (typeof window !== 'undefined') {\n try {\n const storage = window.localStorage\n const staleKeys: string[] = []\n for (let index = 0; index < storage.length; index += 1) {\n const key = storage.key(index)\n if (key && key.startsWith(`${PERSPECTIVE_STORAGE_PREFIX}:`)) staleKeys.push(key)\n }\n for (const key of staleKeys) storage.removeItem(key)\n } catch {\n // private mode / quota errors are non-fatal\n }\n }\n if (typeof document !== 'undefined') {\n try {\n const cookies = document.cookie ? document.cookie.split(';') : []\n for (const cookie of cookies) {\n const name = cookie.split('=')[0]?.trim()\n if (name && name.startsWith(`${PERSPECTIVE_COOKIE_PREFIX}:`)) {\n document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`\n }\n }\n } catch {\n // ignore \u2014 cookie access can throw in sandboxed contexts\n }\n }\n}\n"],
5
+ "mappings": "AAAO,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;AAenC,SAAS,2BAAiC;AAC/C,MAAI,OAAO,WAAW,aAAa;AACjC,QAAI;AACF,YAAM,UAAU,OAAO;AACvB,YAAM,YAAsB,CAAC;AAC7B,eAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACtD,cAAM,MAAM,QAAQ,IAAI,KAAK;AAC7B,YAAI,OAAO,IAAI,WAAW,GAAG,0BAA0B,GAAG,EAAG,WAAU,KAAK,GAAG;AAAA,MACjF;AACA,iBAAW,OAAO,UAAW,SAAQ,WAAW,GAAG;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,OAAO,aAAa,aAAa;AACnC,QAAI;AACF,YAAM,UAAU,SAAS,SAAS,SAAS,OAAO,MAAM,GAAG,IAAI,CAAC;AAChE,iBAAW,UAAU,SAAS;AAC5B,cAAM,OAAO,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AACxC,YAAI,QAAQ,KAAK,WAAW,GAAG,yBAAyB,GAAG,GAAG;AAC5D,mBAAS,SAAS,GAAG,IAAI;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -47,9 +47,9 @@ function getStatusLabel(status, t) {
47
47
  return null;
48
48
  }
49
49
  function getKindStyles(kind) {
50
- if (kind === "event") return "border-blue-500/40 bg-blue-500/10 text-blue-950";
51
- if (kind === "exception") return "border-amber-500/40 bg-amber-500/10 text-amber-950";
52
- return "border-emerald-500/40 bg-emerald-500/10 text-emerald-950";
50
+ if (kind === "event") return "border-status-info-border bg-status-info-bg text-status-info-text";
51
+ if (kind === "exception") return "border-status-warning-border bg-status-warning-bg text-status-warning-text";
52
+ return "border-status-success-border bg-status-success-bg text-status-success-text";
53
53
  }
54
54
  function ScheduleAgenda({ items, range, timezone, onItemClick, onSlotClick, className }) {
55
55
  const t = useT();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/schedule/ScheduleAgenda.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-blue-500/40 bg-blue-500/10 text-blue-950'\n if (kind === 'exception') return 'border-amber-500/40 bg-amber-500/10 text-amber-950'\n return 'border-emerald-500/40 bg-emerald-500/10 text-emerald-950'\n}\n\nexport type ScheduleAgendaProps = {\n items: ScheduleItem[]\n range: ScheduleRange\n timezone?: string\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n className?: string\n}\n\nexport function ScheduleAgenda({ items, range, timezone, onItemClick, onSlotClick, className }: ScheduleAgendaProps) {\n const t = useT()\n const days = React.useMemo(() => eachDay(range.start, range.end), [range])\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n\n return (\n <div className={cn('space-y-4', className)}>\n {days.map((day) => {\n const dayItems = expandedItems.filter((item) => overlapsDay(item, day))\n const slotStart = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9, 0, 0)\n const slotEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10, 0, 0)\n return (\n <div key={day.toISOString()} className=\"rounded-xl border bg-card p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm font-semibold text-foreground\">{formatDayLabel(day)}</div>\n {onSlotClick ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSlotClick({ start: slotStart, end: slotEnd })}\n >\n {t('schedule.actions.add', 'Add')}\n </Button>\n ) : null}\n </div>\n <div className=\"mt-3 space-y-2\">\n {dayItems.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-3 text-xs text-muted-foreground\">\n {t('schedule.emptyDay', 'No scheduled items')}\n </div>\n ) : (\n dayItems.map((item) => {\n const statusLabel = getStatusLabel(item.status, t)\n return (\n <button\n key={item.id}\n type=\"button\"\n className={cn(\n 'flex w-full cursor-pointer flex-col gap-2 rounded-lg border px-3 py-2 text-left text-xs transition hover:shadow-sm',\n getKindStyles(item.kind)\n )}\n onClick={() => onItemClick?.(item)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-semibold\">{item.title}</span>\n {statusLabel ? <Badge variant=\"secondary\">{statusLabel}</Badge> : null}\n </div>\n <div className=\"flex items-center justify-between text-overline text-muted-foreground\">\n <span>{formatTimeRange(item, timezone)}</span>\n <span className=\"capitalize\">{item.kind}</span>\n </div>\n </button>\n )\n })\n )}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'\n if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'\n return 'border-status-success-border bg-status-success-bg text-status-success-text'\n}\n\nexport type ScheduleAgendaProps = {\n items: ScheduleItem[]\n range: ScheduleRange\n timezone?: string\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n className?: string\n}\n\nexport function ScheduleAgenda({ items, range, timezone, onItemClick, onSlotClick, className }: ScheduleAgendaProps) {\n const t = useT()\n const days = React.useMemo(() => eachDay(range.start, range.end), [range])\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n\n return (\n <div className={cn('space-y-4', className)}>\n {days.map((day) => {\n const dayItems = expandedItems.filter((item) => overlapsDay(item, day))\n const slotStart = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9, 0, 0)\n const slotEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10, 0, 0)\n return (\n <div key={day.toISOString()} className=\"rounded-xl border bg-card p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm font-semibold text-foreground\">{formatDayLabel(day)}</div>\n {onSlotClick ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSlotClick({ start: slotStart, end: slotEnd })}\n >\n {t('schedule.actions.add', 'Add')}\n </Button>\n ) : null}\n </div>\n <div className=\"mt-3 space-y-2\">\n {dayItems.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-3 text-xs text-muted-foreground\">\n {t('schedule.emptyDay', 'No scheduled items')}\n </div>\n ) : (\n dayItems.map((item) => {\n const statusLabel = getStatusLabel(item.status, t)\n return (\n <button\n key={item.id}\n type=\"button\"\n className={cn(\n 'flex w-full cursor-pointer flex-col gap-2 rounded-lg border px-3 py-2 text-left text-xs transition hover:shadow-sm',\n getKindStyles(item.kind)\n )}\n onClick={() => onItemClick?.(item)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-semibold\">{item.title}</span>\n {statusLabel ? <Badge variant=\"secondary\">{statusLabel}</Badge> : null}\n </div>\n <div className=\"flex items-center justify-between text-overline text-muted-foreground\">\n <span>{formatTimeRange(item, timezone)}</span>\n <span className=\"capitalize\">{item.kind}</span>\n </div>\n </button>\n )\n })\n )}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -47,9 +47,9 @@ function getStatusLabel(status, t) {
47
47
  return null;
48
48
  }
49
49
  function getKindStyles(kind) {
50
- if (kind === "event") return "border-blue-500/40 bg-blue-500/10 text-blue-950";
51
- if (kind === "exception") return "border-amber-500/40 bg-amber-500/10 text-amber-950";
52
- return "border-emerald-500/40 bg-emerald-500/10 text-emerald-950";
50
+ if (kind === "event") return "border-status-info-border bg-status-info-bg text-status-info-text";
51
+ if (kind === "exception") return "border-status-warning-border bg-status-warning-bg text-status-warning-text";
52
+ return "border-status-success-border bg-status-success-bg text-status-success-text";
53
53
  }
54
54
  function ScheduleGrid({ items, range, timezone, onItemClick, onSlotClick, className }) {
55
55
  const t = useT();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/schedule/ScheduleGrid.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-blue-500/40 bg-blue-500/10 text-blue-950'\n if (kind === 'exception') return 'border-amber-500/40 bg-amber-500/10 text-amber-950'\n return 'border-emerald-500/40 bg-emerald-500/10 text-emerald-950'\n}\n\nexport type ScheduleGridProps = {\n items: ScheduleItem[]\n range: ScheduleRange\n timezone?: string\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n className?: string\n}\n\nexport function ScheduleGrid({ items, range, timezone, onItemClick, onSlotClick, className }: ScheduleGridProps) {\n const t = useT()\n const days = React.useMemo(() => eachDay(range.start, range.end), [range])\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n\n return (\n <div className={cn('grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3', className)}>\n {days.map((day) => {\n const dayItems = expandedItems.filter((item) => overlapsDay(item, day))\n const slotStart = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9, 0, 0)\n const slotEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10, 0, 0)\n return (\n <div key={day.toISOString()} className=\"rounded-xl border bg-card p-4 shadow-sm\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm font-semibold text-foreground\">{formatDayLabel(day)}</div>\n {onSlotClick ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSlotClick({ start: slotStart, end: slotEnd })}\n >\n {t('schedule.actions.add', 'Add')}\n </Button>\n ) : null}\n </div>\n <div className=\"mt-3 space-y-2\">\n {dayItems.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-3 text-xs text-muted-foreground\">\n {t('schedule.emptyDay', 'No scheduled items')}\n </div>\n ) : (\n dayItems.map((item) => {\n const statusLabel = getStatusLabel(item.status, t)\n return (\n <button\n key={item.id}\n type=\"button\"\n className={cn(\n 'flex w-full cursor-pointer flex-col gap-2 rounded-lg border px-3 py-2 text-left text-xs transition hover:shadow-sm',\n getKindStyles(item.kind)\n )}\n onClick={() => onItemClick?.(item)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-semibold\">{item.title}</span>\n {statusLabel ? <Badge variant=\"secondary\">{statusLabel}</Badge> : null}\n </div>\n <div className=\"flex items-center justify-between text-overline text-muted-foreground\">\n <span>{formatTimeRange(item, timezone)}</span>\n <span className=\"capitalize\">{item.kind}</span>\n </div>\n </button>\n )\n })\n )}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'\n if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'\n return 'border-status-success-border bg-status-success-bg text-status-success-text'\n}\n\nexport type ScheduleGridProps = {\n items: ScheduleItem[]\n range: ScheduleRange\n timezone?: string\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n className?: string\n}\n\nexport function ScheduleGrid({ items, range, timezone, onItemClick, onSlotClick, className }: ScheduleGridProps) {\n const t = useT()\n const days = React.useMemo(() => eachDay(range.start, range.end), [range])\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n\n return (\n <div className={cn('grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3', className)}>\n {days.map((day) => {\n const dayItems = expandedItems.filter((item) => overlapsDay(item, day))\n const slotStart = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9, 0, 0)\n const slotEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10, 0, 0)\n return (\n <div key={day.toISOString()} className=\"rounded-xl border bg-card p-4 shadow-sm\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm font-semibold text-foreground\">{formatDayLabel(day)}</div>\n {onSlotClick ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSlotClick({ start: slotStart, end: slotEnd })}\n >\n {t('schedule.actions.add', 'Add')}\n </Button>\n ) : null}\n </div>\n <div className=\"mt-3 space-y-2\">\n {dayItems.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-3 text-xs text-muted-foreground\">\n {t('schedule.emptyDay', 'No scheduled items')}\n </div>\n ) : (\n dayItems.map((item) => {\n const statusLabel = getStatusLabel(item.status, t)\n return (\n <button\n key={item.id}\n type=\"button\"\n className={cn(\n 'flex w-full cursor-pointer flex-col gap-2 rounded-lg border px-3 py-2 text-left text-xs transition hover:shadow-sm',\n getKindStyles(item.kind)\n )}\n onClick={() => onItemClick?.(item)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-semibold\">{item.title}</span>\n {statusLabel ? <Badge variant=\"secondary\">{statusLabel}</Badge> : null}\n </div>\n <div className=\"flex items-center justify-between text-overline text-muted-foreground\">\n <span>{formatTimeRange(item, timezone)}</span>\n <span className=\"capitalize\">{item.kind}</span>\n </div>\n </button>\n )\n })\n )}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,19 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { Button } from "../../primitives/button.js";
5
- import { Input } from "../../primitives/input.js";
4
+ import { DateRangePicker } from "../../primitives/date-range-picker.js";
5
+ import { IconButton } from "../../primitives/icon-button.js";
6
+ import {
7
+ SegmentedControl,
8
+ SegmentedControlItem
9
+ } from "../../primitives/segmented-control.js";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue
16
+ } from "../../primitives/select.js";
6
17
  import { cn } from "@open-mercato/shared/lib/utils";
7
18
  import { useT } from "@open-mercato/shared/lib/i18n/context";
8
19
  import { addDays } from "date-fns/addDays";
@@ -24,16 +35,19 @@ const VIEW_OPTIONS = [
24
35
  { id: "month", labelKey: "schedule.view.month", fallback: "Month" },
25
36
  { id: "agenda", labelKey: "schedule.view.agenda", fallback: "Agenda" }
26
37
  ];
27
- function formatDateInputValue(value) {
28
- const year = value.getFullYear();
29
- const month = String(value.getMonth() + 1).padStart(2, "0");
30
- const day = String(value.getDate()).padStart(2, "0");
31
- return `${year}-${month}-${day}`;
32
- }
33
- function parseDateInputValue(value, fallback) {
34
- if (!value) return fallback;
35
- const next = /* @__PURE__ */ new Date(`${value}T00:00:00`);
36
- return Number.isNaN(next.getTime()) ? fallback : next;
38
+ function getTimezoneOptions(current) {
39
+ const options = /* @__PURE__ */ new Set();
40
+ if (current) options.add(current);
41
+ options.add("UTC");
42
+ const resolved = Intl.DateTimeFormat().resolvedOptions().timeZone;
43
+ if (resolved) options.add(resolved);
44
+ const intlWithSupportedValues = Intl;
45
+ if (typeof intlWithSupportedValues.supportedValuesOf === "function") {
46
+ intlWithSupportedValues.supportedValuesOf("timeZone").forEach((timezone) => {
47
+ if (timezone) options.add(timezone);
48
+ });
49
+ }
50
+ return Array.from(options).sort((a, b) => a.localeCompare(b));
37
51
  }
38
52
  function ScheduleToolbar({
39
53
  view,
@@ -103,70 +117,73 @@ function ScheduleToolbar({
103
117
  const nextStart = startOfDay(addDays(range.start, multiplier * rangeLength));
104
118
  onRangeChange({ start: nextStart, end: endOfDay(addDays(nextStart, rangeLength - 1)) });
105
119
  }, [onRangeChange, range.start, rangeLength, view]);
106
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3 rounded-xl border bg-card p-4 md:flex-row md:items-center md:justify-between", className), children: [
107
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: VIEW_OPTIONS.map((option) => /* @__PURE__ */ jsx(
108
- Button,
120
+ const timezoneOptions = React.useMemo(() => getTimezoneOptions(timezone), [timezone]);
121
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-wrap items-center gap-x-4 gap-y-2 rounded-xl border bg-card p-3", className), children: [
122
+ /* @__PURE__ */ jsx(
123
+ SegmentedControl,
109
124
  {
110
- variant: view === option.id ? "default" : "outline",
111
- size: "sm",
112
- onClick: () => {
113
- if (option.id === view) return;
114
- onViewChange(option.id);
115
- onRangeChange(deriveRangeForView(/* @__PURE__ */ new Date(), option.id));
125
+ value: view,
126
+ onValueChange: (value) => {
127
+ const nextView = value;
128
+ if (nextView === view) return;
129
+ onViewChange(nextView);
130
+ onRangeChange(deriveRangeForView(/* @__PURE__ */ new Date(), nextView));
116
131
  },
117
- children: t(option.labelKey, option.fallback)
118
- },
119
- option.id
120
- )) }),
121
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
122
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => shiftRange("prev"), "aria-label": t("schedule.range.prev", "Previous"), children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4", "aria-hidden": true }) }),
123
- /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-foreground", children: rangeLabel }),
124
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => shiftRange("next"), "aria-label": t("schedule.range.next", "Next"), children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4", "aria-hidden": true }) })
132
+ "aria-label": t("schedule.view.label", "Schedule view"),
133
+ className: "shrink-0",
134
+ children: VIEW_OPTIONS.map((option) => /* @__PURE__ */ jsx(SegmentedControlItem, { value: option.id, children: t(option.labelKey, option.fallback) }, option.id))
135
+ }
136
+ ),
137
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
138
+ /* @__PURE__ */ jsx(
139
+ IconButton,
140
+ {
141
+ type: "button",
142
+ variant: "outline",
143
+ onClick: () => shiftRange("prev"),
144
+ "aria-label": t("schedule.range.prev", "Previous"),
145
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4", "aria-hidden": true })
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 whitespace-nowrap px-1 text-sm font-medium text-foreground", children: rangeLabel }),
149
+ /* @__PURE__ */ jsx(
150
+ IconButton,
151
+ {
152
+ type: "button",
153
+ variant: "outline",
154
+ onClick: () => shiftRange("next"),
155
+ "aria-label": t("schedule.range.next", "Next"),
156
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4", "aria-hidden": true })
157
+ }
158
+ )
125
159
  ] }),
126
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
127
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
128
- /* @__PURE__ */ jsx("span", { children: t("schedule.range.start", "Start") }),
129
- /* @__PURE__ */ jsx(
130
- Input,
131
- {
132
- type: "date",
133
- value: formatDateInputValue(range.start),
134
- onChange: (event) => {
135
- const nextStart = parseDateInputValue(event.target.value, range.start);
136
- onRangeChange({ start: nextStart, end: range.end });
137
- },
138
- className: "h-8 w-full sm:w-[140px]"
139
- }
140
- )
141
- ] }),
142
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
143
- /* @__PURE__ */ jsx("span", { children: t("schedule.range.end", "End") }),
144
- /* @__PURE__ */ jsx(
145
- Input,
146
- {
147
- type: "date",
148
- value: formatDateInputValue(range.end),
149
- onChange: (event) => {
150
- const nextEnd = parseDateInputValue(event.target.value, range.end);
151
- onRangeChange({ start: range.start, end: nextEnd });
152
- },
153
- className: "h-8 w-full sm:w-[140px]"
154
- }
155
- )
156
- ] }),
157
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
158
- /* @__PURE__ */ jsx("span", { children: t("schedule.range.timezone", "Timezone") }),
160
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 md:ml-auto", children: [
161
+ /* @__PURE__ */ jsx(
162
+ DateRangePicker,
163
+ {
164
+ value: range,
165
+ onChange: (next) => {
166
+ if (!next) return;
167
+ onRangeChange({ start: startOfDay(next.start), end: endOfDay(next.end) });
168
+ },
169
+ size: "sm",
170
+ showPresets: false,
171
+ numberOfMonths: 2,
172
+ "aria-label": t("schedule.range.label", "Date range")
173
+ }
174
+ ),
175
+ onTimezoneChange ? /* @__PURE__ */ jsxs(Select, { value: timezone ?? void 0, onValueChange: onTimezoneChange, children: [
159
176
  /* @__PURE__ */ jsx(
160
- Input,
177
+ SelectTrigger,
161
178
  {
162
- type: "text",
163
- value: timezone ?? "",
164
- onChange: (event) => onTimezoneChange?.(event.target.value),
165
- className: "h-8 w-full sm:w-[180px]",
166
- placeholder: t("schedule.range.timezone.placeholder", "UTC")
179
+ size: "sm",
180
+ className: "w-auto min-w-[10rem]",
181
+ "aria-label": t("schedule.range.timezone", "Timezone"),
182
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("schedule.range.timezone.placeholder", "UTC") })
167
183
  }
168
- )
169
- ] })
184
+ ),
185
+ /* @__PURE__ */ jsx(SelectContent, { children: timezoneOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option, children: option }, option)) })
186
+ ] }) : null
170
187
  ] })
171
188
  ] });
172
189
  }