@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,1032 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import dynamic from "next/dynamic";
5
+ import { AppearanceDialog } from "@open-mercato/core/modules/customers/components/detail/AppearanceDialog";
6
+ import { ArrowUpRightSquare, FileCode, Loader2, Palette, Pencil, Plus, Trash2 } from "lucide-react";
7
+ import { Button } from "@open-mercato/ui/primitives/button";
8
+ import { flash } from "../FlashMessages.js";
9
+ import { SwitchableMarkdownInput } from "../inputs/SwitchableMarkdownInput.js";
10
+ import { ErrorMessage } from "./ErrorMessage.js";
11
+ import { LoadingMessage } from "./LoadingMessage.js";
12
+ import { TabEmptyState } from "./TabEmptyState.js";
13
+ const isTestEnv = typeof process !== "undefined" && process.env.NODE_ENV === "test";
14
+ const MarkdownPreviewComponent = isTestEnv ? ({ children, className }) => /* @__PURE__ */ jsx("div", { className, children }) : dynamic(() => import("react-markdown").then((mod) => mod.default), {
15
+ ssr: false,
16
+ loading: () => null
17
+ });
18
+ let markdownPluginsPromise = null;
19
+ async function loadMarkdownPlugins() {
20
+ if (isTestEnv) return [];
21
+ if (!markdownPluginsPromise) {
22
+ markdownPluginsPromise = import("remark-gfm").then((mod) => [mod.default ?? mod]).catch(() => []);
23
+ }
24
+ return markdownPluginsPromise;
25
+ }
26
+ function generateTempId() {
27
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
28
+ return `tmp_${Math.random().toString(36).slice(2)}`;
29
+ }
30
+ function formatDateTime(value) {
31
+ if (!value) return null;
32
+ const date = new Date(value);
33
+ if (Number.isNaN(date.getTime())) return null;
34
+ return date.toLocaleString();
35
+ }
36
+ function formatRelativeTime(value) {
37
+ if (!value) return null;
38
+ const date = new Date(value);
39
+ if (Number.isNaN(date.getTime())) return null;
40
+ const now = Date.now();
41
+ const diffSeconds = (date.getTime() - now) / 1e3;
42
+ const absSeconds = Math.abs(diffSeconds);
43
+ const rtf = typeof Intl !== "undefined" && typeof Intl.RelativeTimeFormat === "function" ? new Intl.RelativeTimeFormat(void 0, { numeric: "auto" }) : null;
44
+ const format = (unit, divisor) => {
45
+ const valueToFormat = Math.round(diffSeconds / divisor);
46
+ if (rtf) return rtf.format(valueToFormat, unit);
47
+ const suffix = valueToFormat <= 0 ? "ago" : "from now";
48
+ const magnitude = Math.abs(valueToFormat);
49
+ return `${magnitude} ${unit}${magnitude === 1 ? "" : "s"} ${suffix}`;
50
+ };
51
+ if (absSeconds < 45) return format("second", 1);
52
+ if (absSeconds < 45 * 60) return format("minute", 60);
53
+ if (absSeconds < 24 * 60 * 60) return format("hour", 60 * 60);
54
+ if (absSeconds < 7 * 24 * 60 * 60) return format("day", 24 * 60 * 60);
55
+ if (absSeconds < 30 * 24 * 60 * 60) return format("week", 7 * 24 * 60 * 60);
56
+ if (absSeconds < 365 * 24 * 60 * 60) return format("month", 30 * 24 * 60 * 60);
57
+ return format("year", 365 * 24 * 60 * 60);
58
+ }
59
+ function TimelineItemHeader({
60
+ title,
61
+ subtitle,
62
+ timestamp,
63
+ fallbackTimestampLabel,
64
+ icon,
65
+ color,
66
+ iconSize = "md",
67
+ className,
68
+ renderIcon,
69
+ renderColor
70
+ }) {
71
+ const wrapperSize = iconSize === "sm" ? "h-6 w-6" : "h-8 w-8";
72
+ const iconSizeClass = iconSize === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";
73
+ const resolvedTimestamp = React.useMemo(() => {
74
+ if (subtitle) return subtitle;
75
+ if (!timestamp) return fallbackTimestampLabel ?? null;
76
+ const value = typeof timestamp === "string" ? timestamp : timestamp.toISOString();
77
+ const date = new Date(value);
78
+ if (Number.isNaN(date.getTime())) return fallbackTimestampLabel ?? null;
79
+ const now = Date.now();
80
+ const diff = Math.abs(now - date.getTime());
81
+ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
82
+ const relativeLabel = diff <= THIRTY_DAYS_MS ? formatRelativeTime(value) : null;
83
+ const absoluteLabel = formatDateTime(value);
84
+ if (relativeLabel) {
85
+ return /* @__PURE__ */ jsx("span", { title: absoluteLabel ?? void 0, children: relativeLabel });
86
+ }
87
+ return absoluteLabel ?? fallbackTimestampLabel ?? null;
88
+ }, [fallbackTimestampLabel, subtitle, timestamp]);
89
+ return /* @__PURE__ */ jsxs("div", { className: ["flex items-start gap-3", className].filter(Boolean).join(" "), children: [
90
+ icon && renderIcon ? /* @__PURE__ */ jsx("span", { className: ["inline-flex items-center justify-center rounded border border-border bg-muted/40", wrapperSize].join(" "), children: renderIcon(icon, iconSizeClass) }) : null,
91
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
92
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
93
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: title }),
94
+ color && renderColor ? renderColor(color, "h-3 w-3 rounded-full border border-border") : null
95
+ ] }),
96
+ resolvedTimestamp ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: resolvedTimestamp }) : null
97
+ ] })
98
+ ] });
99
+ }
100
+ function sanitizeHexColor(value) {
101
+ if (!value) return null;
102
+ const trimmed = value.trim();
103
+ return /^#([0-9a-f]{6})$/i.test(trimmed) ? trimmed.toLowerCase() : null;
104
+ }
105
+ function mapCommentSummary(input) {
106
+ const data = typeof input === "object" && input !== null ? input : {};
107
+ const id = typeof data.id === "string" ? data.id : generateTempId();
108
+ const body = typeof data.body === "string" ? data.body : "";
109
+ const createdAt = typeof data.createdAt === "string" ? data.createdAt : typeof data.created_at === "string" ? data.created_at : (/* @__PURE__ */ new Date()).toISOString();
110
+ const authorUserId = typeof data.authorUserId === "string" ? data.authorUserId : typeof data.author_user_id === "string" ? data.author_user_id : null;
111
+ const authorName = typeof data.authorName === "string" ? data.authorName : typeof data.author_name === "string" ? data.author_name : null;
112
+ const authorEmail = typeof data.authorEmail === "string" ? data.authorEmail : typeof data.author_email === "string" ? data.author_email : null;
113
+ const dealId = typeof data.dealId === "string" ? data.dealId : typeof data.deal_id === "string" ? data.deal_id : null;
114
+ const dealTitle = typeof data.dealTitle === "string" ? data.dealTitle : typeof data.deal_title === "string" ? data.deal_title : null;
115
+ const appearanceIcon = typeof data.appearanceIcon === "string" ? data.appearanceIcon : typeof data.appearance_icon === "string" ? data.appearance_icon : null;
116
+ const appearanceColor = typeof data.appearanceColor === "string" ? data.appearanceColor : typeof data.appearance_color === "string" ? data.appearance_color : null;
117
+ return {
118
+ id,
119
+ body,
120
+ createdAt,
121
+ authorUserId,
122
+ authorName,
123
+ authorEmail,
124
+ dealId,
125
+ dealTitle,
126
+ appearanceIcon,
127
+ appearanceColor
128
+ };
129
+ }
130
+ function NotesSection({
131
+ entityId,
132
+ dealId,
133
+ emptyLabel,
134
+ viewerUserId,
135
+ viewerName,
136
+ viewerEmail,
137
+ addActionLabel,
138
+ emptyState,
139
+ onActionChange,
140
+ translator,
141
+ labelPrefix = "customers.people.detail.notes",
142
+ inlineLabelPrefix = "customers.people.detail.inline",
143
+ onLoadingChange,
144
+ dealOptions,
145
+ entityOptions,
146
+ dataAdapter,
147
+ dataContext,
148
+ renderIcon,
149
+ renderColor,
150
+ iconSuggestions,
151
+ readMarkdownPreference,
152
+ writeMarkdownPreference,
153
+ disableMarkdown
154
+ }) {
155
+ const t = React.useMemo(() => translator ?? ((key, fallback) => fallback ?? key), [translator]);
156
+ const label = React.useCallback(
157
+ (suffix, fallback, params) => t(`${labelPrefix}.${suffix}`, fallback, params),
158
+ [labelPrefix, t]
159
+ );
160
+ const inlineLabel = React.useCallback(
161
+ (suffix, fallback, params) => t(`${inlineLabelPrefix}.${suffix}`, fallback, params),
162
+ [inlineLabelPrefix, t]
163
+ );
164
+ const [markdownPlugins, setMarkdownPlugins] = React.useState([]);
165
+ React.useEffect(() => {
166
+ if (isTestEnv) return;
167
+ let mounted = true;
168
+ void loadMarkdownPlugins().then((plugins) => {
169
+ if (!mounted) return;
170
+ setMarkdownPlugins(plugins);
171
+ });
172
+ return () => {
173
+ mounted = false;
174
+ };
175
+ }, []);
176
+ const normalizedDealOptions = React.useMemo(() => {
177
+ if (!Array.isArray(dealOptions)) return [];
178
+ const seen = /* @__PURE__ */ new Set();
179
+ return dealOptions.map((option) => {
180
+ if (!option || typeof option !== "object") return null;
181
+ const id = typeof option.id === "string" ? option.id.trim() : "";
182
+ if (!id || seen.has(id)) return null;
183
+ const label2 = typeof option.label === "string" && option.label.trim().length ? option.label.trim() : id;
184
+ seen.add(id);
185
+ return { id, label: label2 };
186
+ }).filter((option) => !!option);
187
+ }, [dealOptions]);
188
+ const dealLabelMap = React.useMemo(() => {
189
+ const map = /* @__PURE__ */ new Map();
190
+ normalizedDealOptions.forEach((option) => {
191
+ map.set(option.id, option.label);
192
+ });
193
+ return map;
194
+ }, [normalizedDealOptions]);
195
+ const normalizedEntityOptions = React.useMemo(() => {
196
+ if (!Array.isArray(entityOptions)) return [];
197
+ const seen = /* @__PURE__ */ new Set();
198
+ return entityOptions.map((option) => {
199
+ if (!option || typeof option !== "object") return null;
200
+ const id = typeof option.id === "string" ? option.id.trim() : "";
201
+ if (!id || seen.has(id)) return null;
202
+ const label2 = typeof option.label === "string" && option.label.trim().length ? option.label.trim() : id;
203
+ seen.add(id);
204
+ return { id, label: label2 };
205
+ }).filter((option) => !!option);
206
+ }, [entityOptions]);
207
+ const [selectedDealId, setSelectedDealId] = React.useState(() => {
208
+ const initial = typeof dealId === "string" ? dealId.trim() : "";
209
+ return initial;
210
+ });
211
+ React.useEffect(() => {
212
+ const initial = typeof dealId === "string" ? dealId.trim() : "";
213
+ if (initial !== selectedDealId) {
214
+ setSelectedDealId(initial);
215
+ }
216
+ }, [dealId, selectedDealId]);
217
+ const [selectedEntityId, setSelectedEntityId] = React.useState(() => {
218
+ if (normalizedEntityOptions.length) return normalizedEntityOptions[0].id;
219
+ return typeof entityId === "string" ? entityId : "";
220
+ });
221
+ React.useEffect(() => {
222
+ if (normalizedEntityOptions.length) {
223
+ if (!normalizedEntityOptions.some((option) => option.id === selectedEntityId)) {
224
+ setSelectedEntityId(normalizedEntityOptions[0].id);
225
+ }
226
+ } else {
227
+ const initial = typeof entityId === "string" ? entityId : "";
228
+ if (initial !== selectedEntityId) {
229
+ setSelectedEntityId(initial);
230
+ }
231
+ }
232
+ }, [entityId, normalizedEntityOptions, selectedEntityId]);
233
+ const resolvedEntityId = React.useMemo(() => {
234
+ if (normalizedEntityOptions.length) return selectedEntityId;
235
+ return typeof entityId === "string" ? entityId : "";
236
+ }, [entityId, normalizedEntityOptions, selectedEntityId]);
237
+ const resolvedDealId = React.useMemo(() => {
238
+ const trimmed = typeof selectedDealId === "string" ? selectedDealId.trim() : "";
239
+ return trimmed;
240
+ }, [selectedDealId]);
241
+ const hasEntity = resolvedEntityId.length > 0;
242
+ const [notes, setNotes] = React.useState([]);
243
+ const [isLoading, setIsLoading] = React.useState(() => Boolean(entityId || dealId));
244
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
245
+ const [loadError, setLoadError] = React.useState(null);
246
+ const pendingCounterRef = React.useRef(0);
247
+ const pushLoading = React.useCallback(() => {
248
+ pendingCounterRef.current += 1;
249
+ if (pendingCounterRef.current === 1) {
250
+ onLoadingChange?.(true);
251
+ }
252
+ }, [onLoadingChange]);
253
+ const popLoading = React.useCallback(() => {
254
+ pendingCounterRef.current = Math.max(0, pendingCounterRef.current - 1);
255
+ if (pendingCounterRef.current === 0) {
256
+ onLoadingChange?.(false);
257
+ }
258
+ }, [onLoadingChange]);
259
+ const [composerOpen, setComposerOpen] = React.useState(false);
260
+ const [draftBody, setDraftBody] = React.useState("");
261
+ const [draftIcon, setDraftIcon] = React.useState(null);
262
+ const [draftColor, setDraftColor] = React.useState(null);
263
+ const [isMarkdownEnabled, setIsMarkdownEnabled] = React.useState(false);
264
+ const textareaRef = React.useRef(null);
265
+ const formRef = React.useRef(null);
266
+ const focusComposer = React.useCallback(() => {
267
+ if (!hasEntity) return;
268
+ setComposerOpen(true);
269
+ window.requestAnimationFrame(() => {
270
+ if (isMarkdownEnabled) {
271
+ const markdownTextarea = formRef.current?.querySelector("textarea");
272
+ if (markdownTextarea instanceof HTMLTextAreaElement) {
273
+ markdownTextarea.focus();
274
+ markdownTextarea.scrollIntoView({ behavior: "smooth", block: "center" });
275
+ return;
276
+ }
277
+ }
278
+ const element = textareaRef.current;
279
+ if (!element) return;
280
+ element.focus();
281
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
282
+ });
283
+ }, [formRef, hasEntity, isMarkdownEnabled]);
284
+ const [appearanceDialogState, setAppearanceDialogState] = React.useState(null);
285
+ const [appearanceDialogSaving, setAppearanceDialogSaving] = React.useState(false);
286
+ const [appearanceDialogError, setAppearanceDialogError] = React.useState(null);
287
+ const [contentEditor, setContentEditor] = React.useState({ id: "", value: "" });
288
+ const [contentSavingId, setContentSavingId] = React.useState(null);
289
+ const [contentError, setContentError] = React.useState(null);
290
+ const contentTextareaRef = React.useRef(null);
291
+ const [visibleCount, setVisibleCount] = React.useState(0);
292
+ const [deletingNoteId, setDeletingNoteId] = React.useState(null);
293
+ React.useEffect(() => {
294
+ const queryEntityId = typeof entityId === "string" ? entityId : "";
295
+ const queryDealId = typeof dealId === "string" ? dealId : "";
296
+ if (!queryEntityId && !queryDealId) {
297
+ setNotes([]);
298
+ setLoadError(null);
299
+ setIsLoading(false);
300
+ return;
301
+ }
302
+ let cancelled = false;
303
+ setIsLoading(true);
304
+ setLoadError(null);
305
+ pushLoading();
306
+ async function loadNotes() {
307
+ try {
308
+ const mapped = await dataAdapter.list({
309
+ entityId: queryEntityId || null,
310
+ dealId: queryDealId || null,
311
+ context: dataContext
312
+ });
313
+ if (cancelled) return;
314
+ setNotes(mapped);
315
+ } catch (err) {
316
+ if (cancelled) return;
317
+ const message = err instanceof Error ? err.message : label("loadError", "Failed to load notes.");
318
+ setNotes([]);
319
+ setLoadError(message);
320
+ flash(message, "error");
321
+ } finally {
322
+ if (!cancelled) setIsLoading(false);
323
+ popLoading();
324
+ }
325
+ }
326
+ loadNotes().catch(() => {
327
+ });
328
+ return () => {
329
+ cancelled = true;
330
+ };
331
+ }, [dataAdapter, dataContext, dealId, entityId, popLoading, pushLoading, t]);
332
+ const youLabel = label("you", "You");
333
+ const viewerLabel = React.useMemo(() => viewerName ?? viewerEmail ?? null, [viewerEmail, viewerName]);
334
+ const handleMarkdownToggle = React.useCallback(() => {
335
+ setIsMarkdownEnabled((prev) => {
336
+ const next = !prev;
337
+ if (writeMarkdownPreference) {
338
+ writeMarkdownPreference(next);
339
+ }
340
+ return next;
341
+ });
342
+ }, [writeMarkdownPreference]);
343
+ React.useEffect(() => {
344
+ if (!onActionChange) return;
345
+ if (!notes.length) {
346
+ onActionChange(null);
347
+ return;
348
+ }
349
+ onActionChange({
350
+ label: addActionLabel,
351
+ onClick: focusComposer,
352
+ disabled: isSubmitting || isLoading || !hasEntity,
353
+ icon: /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" })
354
+ });
355
+ return () => onActionChange(null);
356
+ }, [onActionChange, addActionLabel, focusComposer, hasEntity, isLoading, isSubmitting, notes.length]);
357
+ const adjustTextareaSize = React.useCallback((element) => {
358
+ if (!element) return;
359
+ element.style.height = "auto";
360
+ element.style.height = `${element.scrollHeight}px`;
361
+ }, []);
362
+ React.useEffect(() => {
363
+ adjustTextareaSize(textareaRef.current);
364
+ }, [adjustTextareaSize, draftBody, isMarkdownEnabled, composerOpen]);
365
+ React.useEffect(() => {
366
+ const preference = readMarkdownPreference ? readMarkdownPreference() : null;
367
+ if (preference !== null) {
368
+ setIsMarkdownEnabled(preference);
369
+ }
370
+ }, [readMarkdownPreference]);
371
+ React.useEffect(() => {
372
+ if (!notes.length) {
373
+ setVisibleCount(0);
374
+ return;
375
+ }
376
+ const baseline = Math.min(5, notes.length);
377
+ setVisibleCount((prev) => {
378
+ if (prev >= notes.length) return prev;
379
+ return Math.min(Math.max(prev, baseline), notes.length);
380
+ });
381
+ }, [notes.length]);
382
+ React.useEffect(() => {
383
+ if (hasEntity) return;
384
+ setComposerOpen(false);
385
+ setDraftBody("");
386
+ setDraftIcon(null);
387
+ setDraftColor(null);
388
+ }, [hasEntity]);
389
+ const visibleNotes = React.useMemo(() => notes.slice(0, visibleCount), [notes, visibleCount]);
390
+ const hasVisibleNotes = React.useMemo(() => visibleCount > 0, [visibleCount]);
391
+ const loadMoreLabel = label("loadMore");
392
+ const handleCreateNote = React.useCallback(
393
+ async (input) => {
394
+ if (!hasEntity || !resolvedEntityId) {
395
+ flash(label("entityMissing", "Unable to determine current person."), "error");
396
+ return false;
397
+ }
398
+ const body = input.body.trim();
399
+ if (!body) {
400
+ focusComposer();
401
+ return false;
402
+ }
403
+ const icon = input.appearanceIcon && input.appearanceIcon.trim().length ? input.appearanceIcon.trim() : null;
404
+ const color = sanitizeHexColor(input.appearanceColor);
405
+ const targetDealId = resolvedDealId.length ? resolvedDealId : null;
406
+ const dealLabel = targetDealId ? dealLabelMap.get(targetDealId) ?? null : null;
407
+ setIsSubmitting(true);
408
+ pushLoading();
409
+ try {
410
+ const responseBody = await dataAdapter.create({
411
+ entityId: resolvedEntityId,
412
+ body,
413
+ appearanceIcon: icon,
414
+ appearanceColor: color,
415
+ dealId: targetDealId,
416
+ context: dataContext
417
+ }) ?? {};
418
+ setNotes((prev) => {
419
+ const viewerId = viewerUserId ?? null;
420
+ const resolvedAuthorId = typeof responseBody?.authorUserId === "string" ? responseBody.authorUserId : viewerId ?? null;
421
+ const resolvedAuthorName = (() => {
422
+ if (resolvedAuthorId && viewerId && resolvedAuthorId === viewerId) {
423
+ return youLabel;
424
+ }
425
+ return typeof responseBody?.authorName === "string" ? responseBody.authorName : viewerLabel;
426
+ })();
427
+ const resolvedAuthorEmail = (() => {
428
+ if (resolvedAuthorId && viewerId && resolvedAuthorId === viewerId) {
429
+ return viewerEmail ?? null;
430
+ }
431
+ return typeof responseBody?.authorEmail === "string" ? responseBody.authorEmail : null;
432
+ })();
433
+ const newNote = {
434
+ id: typeof responseBody?.id === "string" ? responseBody.id : generateTempId(),
435
+ body,
436
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
437
+ authorUserId: resolvedAuthorId,
438
+ authorName: resolvedAuthorName,
439
+ authorEmail: resolvedAuthorEmail,
440
+ dealId: targetDealId,
441
+ dealTitle: dealLabel,
442
+ appearanceIcon: icon,
443
+ appearanceColor: color
444
+ };
445
+ return [newNote, ...prev];
446
+ });
447
+ flash(label("success"), "success");
448
+ return true;
449
+ } catch (err) {
450
+ const message = err instanceof Error ? err.message : label("error");
451
+ flash(message, "error");
452
+ return false;
453
+ } finally {
454
+ setIsSubmitting(false);
455
+ popLoading();
456
+ }
457
+ },
458
+ [dataAdapter, dataContext, dealLabelMap, focusComposer, hasEntity, popLoading, pushLoading, resolvedDealId, resolvedEntityId, t, viewerEmail, viewerLabel, viewerUserId, youLabel]
459
+ );
460
+ const handleUpdateNote = React.useCallback(
461
+ async (noteId, patch) => {
462
+ const sanitizedBody = patch.body;
463
+ const sanitizedIcon = patch.appearanceIcon !== void 0 && patch.appearanceIcon !== null && patch.appearanceIcon.trim().length ? patch.appearanceIcon.trim() : patch.appearanceIcon === null ? null : void 0;
464
+ const sanitizedColor = patch.appearanceColor !== void 0 ? sanitizeHexColor(patch.appearanceColor ?? null) : void 0;
465
+ try {
466
+ await dataAdapter.update({
467
+ id: noteId,
468
+ patch: {
469
+ body: sanitizedBody,
470
+ appearanceIcon: sanitizedIcon,
471
+ appearanceColor: sanitizedColor
472
+ },
473
+ context: dataContext
474
+ });
475
+ setNotes((prev) => {
476
+ const nextComments = prev.map((comment) => {
477
+ if (comment.id !== noteId) return comment;
478
+ const next = { ...comment };
479
+ if (sanitizedBody !== void 0) next.body = sanitizedBody;
480
+ if (sanitizedIcon !== void 0) next.appearanceIcon = sanitizedIcon ?? null;
481
+ if (sanitizedColor !== void 0) next.appearanceColor = sanitizedColor ?? null;
482
+ return next;
483
+ });
484
+ return nextComments;
485
+ });
486
+ flash(label("updateSuccess"), "success");
487
+ } catch (error) {
488
+ const message = error instanceof Error ? error.message : label("updateError");
489
+ flash(message, "error");
490
+ throw error instanceof Error ? error : new Error(message);
491
+ }
492
+ },
493
+ [dataAdapter, dataContext, t]
494
+ );
495
+ const handleDeleteNote = React.useCallback(
496
+ async (note) => {
497
+ const confirmed = typeof window === "undefined" ? true : window.confirm(label("deleteConfirm", "Delete this note? This action cannot be undone."));
498
+ if (!confirmed) return;
499
+ setDeletingNoteId(note.id);
500
+ pushLoading();
501
+ try {
502
+ await dataAdapter.delete({ id: note.id, context: dataContext });
503
+ setNotes((prev) => prev.filter((existing) => existing.id !== note.id));
504
+ flash(label("deleteSuccess", "Note deleted"), "success");
505
+ } catch (err) {
506
+ const message = err instanceof Error ? err.message : label("deleteError", "Failed to delete note");
507
+ flash(message, "error");
508
+ } finally {
509
+ setDeletingNoteId(null);
510
+ popLoading();
511
+ }
512
+ },
513
+ [dataAdapter, dataContext, popLoading, pushLoading, t]
514
+ );
515
+ const handleSubmit = React.useCallback(
516
+ async (event) => {
517
+ event.preventDefault();
518
+ const created = await handleCreateNote({
519
+ body: draftBody,
520
+ appearanceIcon: draftIcon,
521
+ appearanceColor: draftColor
522
+ });
523
+ if (created) {
524
+ setDraftBody("");
525
+ setDraftIcon(null);
526
+ setDraftColor(null);
527
+ }
528
+ },
529
+ [draftBody, draftColor, draftIcon, handleCreateNote]
530
+ );
531
+ const handleLoadMore = React.useCallback(() => {
532
+ setVisibleCount((prev) => {
533
+ if (prev >= notes.length) return prev;
534
+ return Math.min(prev + 5, notes.length);
535
+ });
536
+ }, [notes.length]);
537
+ const handleAppearanceDialogSubmit = React.useCallback(async () => {
538
+ if (!appearanceDialogState) return;
539
+ setAppearanceDialogError(null);
540
+ const sanitizedIcon = appearanceDialogState.icon && appearanceDialogState.icon.trim().length ? appearanceDialogState.icon.trim() : null;
541
+ const sanitizedColor = sanitizeHexColor(appearanceDialogState.color ?? null);
542
+ if (appearanceDialogState.mode === "create") {
543
+ setDraftIcon(sanitizedIcon);
544
+ setDraftColor(sanitizedColor);
545
+ setAppearanceDialogState(null);
546
+ return;
547
+ }
548
+ setAppearanceDialogSaving(true);
549
+ try {
550
+ await handleUpdateNote(appearanceDialogState.noteId, {
551
+ appearanceIcon: sanitizedIcon,
552
+ appearanceColor: sanitizedColor
553
+ });
554
+ setAppearanceDialogState(null);
555
+ } catch (err) {
556
+ const message = err instanceof Error ? err.message : label("appearance.error", "Failed to update appearance.");
557
+ setAppearanceDialogError(message);
558
+ } finally {
559
+ setAppearanceDialogSaving(false);
560
+ }
561
+ }, [appearanceDialogState, handleUpdateNote, t]);
562
+ const handleAppearanceDialogClose = React.useCallback(() => {
563
+ if (appearanceDialogSaving) return;
564
+ setAppearanceDialogState(null);
565
+ setAppearanceDialogError(null);
566
+ }, [appearanceDialogSaving]);
567
+ const handleContentSave = React.useCallback(async () => {
568
+ if (!contentEditor.id) return;
569
+ const trimmed = contentEditor.value.trim();
570
+ if (!trimmed) {
571
+ setContentError(label("updateError", "Failed to update note"));
572
+ return;
573
+ }
574
+ setContentSavingId(contentEditor.id);
575
+ setContentError(null);
576
+ try {
577
+ await handleUpdateNote(contentEditor.id, { body: trimmed });
578
+ setContentEditor({ id: "", value: "" });
579
+ } catch (err) {
580
+ const message = err instanceof Error ? err.message : label("updateError", "Failed to update note");
581
+ setContentError(message);
582
+ } finally {
583
+ setContentSavingId(null);
584
+ }
585
+ }, [contentEditor, handleUpdateNote, t]);
586
+ const handleContentEditorKeyDown = React.useCallback(
587
+ (event) => {
588
+ if (!contentEditor.id) return;
589
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
590
+ event.preventDefault();
591
+ if (!contentSavingId) void handleContentSave();
592
+ return;
593
+ }
594
+ if (event.key === "Escape") {
595
+ event.preventDefault();
596
+ setContentEditor({ id: "", value: "" });
597
+ setContentError(null);
598
+ }
599
+ },
600
+ [contentEditor.id, contentSavingId, handleContentSave]
601
+ );
602
+ const handleComposerKeyDown = React.useCallback(
603
+ (event) => {
604
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
605
+ event.preventDefault();
606
+ formRef.current?.requestSubmit();
607
+ }
608
+ },
609
+ []
610
+ );
611
+ const handleContentKeyDown = React.useCallback(
612
+ (event, note) => {
613
+ if (event.key === "Enter" || event.key === " ") {
614
+ event.preventDefault();
615
+ setContentEditor({ id: note.id, value: note.body });
616
+ }
617
+ },
618
+ []
619
+ );
620
+ const noteAuthorLabel = React.useCallback(
621
+ (note) => {
622
+ if (note.authorUserId && viewerUserId && note.authorUserId === viewerUserId) {
623
+ return youLabel;
624
+ }
625
+ return note.authorName ?? note.authorEmail ?? youLabel;
626
+ },
627
+ [viewerUserId, youLabel]
628
+ );
629
+ const noteAppearanceLabels = React.useMemo(
630
+ () => ({
631
+ colorLabel: label("appearance.colorLabel"),
632
+ colorHelp: label("appearance.colorHelp"),
633
+ colorClearLabel: label("appearance.clearColor"),
634
+ iconLabel: label("appearance.iconLabel"),
635
+ iconPlaceholder: label("appearance.iconPlaceholder"),
636
+ iconPickerTriggerLabel: label("appearance.iconPicker"),
637
+ iconSearchPlaceholder: label("appearance.iconSearchPlaceholder"),
638
+ iconSearchEmptyLabel: label("appearance.iconSearchEmpty"),
639
+ iconSuggestionsLabel: label("appearance.iconSuggestions"),
640
+ iconClearLabel: label("appearance.iconClear"),
641
+ previewEmptyLabel: label("appearance.previewEmpty")
642
+ }),
643
+ [label]
644
+ );
645
+ const composerAuthor = React.useMemo(
646
+ () => youLabel,
647
+ [youLabel]
648
+ );
649
+ const composerHasAppearance = Boolean(draftIcon) || Boolean(draftColor);
650
+ const appearanceDialogOpen = appearanceDialogState !== null;
651
+ const editingAppearanceNoteId = appearanceDialogState?.mode === "edit" ? appearanceDialogState.noteId : null;
652
+ const addNoteShortcutLabel = label("addShortcut", "Add note \u2318\u23CE / Ctrl+Enter");
653
+ const saveAppearanceShortcutLabel = label("appearance.saveShortcut", "Save appearance \u2318\u23CE / Ctrl+Enter");
654
+ const composerSubmitLabel = addNoteShortcutLabel;
655
+ const appearanceDialogPrimaryLabel = saveAppearanceShortcutLabel;
656
+ const appearanceDialogSavingLabel = appearanceDialogState?.mode === "edit" ? label("appearance.saving") : label("saving", "Saving note\u2026");
657
+ return /* @__PURE__ */ jsxs("div", { className: "mt-0 space-y-2", children: [
658
+ /* @__PURE__ */ jsx(
659
+ "div",
660
+ {
661
+ className: [
662
+ "overflow-hidden rounded-xl transition-all duration-300 ease-out",
663
+ composerOpen ? "max-h-[1200px] bg-muted/10 p-4 opacity-100" : "pointer-events-none max-h-0 p-0 opacity-0"
664
+ ].join(" "),
665
+ "aria-hidden": !composerOpen,
666
+ children: composerOpen ? /* @__PURE__ */ jsxs(
667
+ "form",
668
+ {
669
+ ref: formRef,
670
+ onSubmit: handleSubmit,
671
+ onKeyDown: handleComposerKeyDown,
672
+ className: "space-y-3",
673
+ children: [
674
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
675
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: label("addLabel") }),
676
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [
677
+ /* @__PURE__ */ jsxs(
678
+ Button,
679
+ {
680
+ type: "button",
681
+ variant: "ghost",
682
+ size: "icon",
683
+ onClick: () => {
684
+ setAppearanceDialogError(null);
685
+ setAppearanceDialogState({ mode: "create", icon: draftIcon, color: draftColor });
686
+ },
687
+ disabled: isSubmitting || isLoading || !hasEntity,
688
+ children: [
689
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label("appearance.toggleOpen", "Customize appearance") }),
690
+ /* @__PURE__ */ jsx(Palette, { className: "h-4 w-4" })
691
+ ]
692
+ }
693
+ ),
694
+ disableMarkdown ? null : /* @__PURE__ */ jsx(
695
+ Button,
696
+ {
697
+ type: "button",
698
+ variant: isMarkdownEnabled ? "secondary" : "ghost",
699
+ size: "icon",
700
+ onClick: handleMarkdownToggle,
701
+ "aria-pressed": isMarkdownEnabled,
702
+ disabled: isSubmitting || isLoading,
703
+ children: /* @__PURE__ */ jsx(FileCode, { className: "h-4 w-4" })
704
+ }
705
+ ),
706
+ /* @__PURE__ */ jsx(
707
+ Button,
708
+ {
709
+ type: "button",
710
+ size: "sm",
711
+ variant: "ghost",
712
+ onClick: () => {
713
+ setComposerOpen(false);
714
+ setDraftBody("");
715
+ setDraftIcon(null);
716
+ setDraftColor(null);
717
+ },
718
+ disabled: isSubmitting || isLoading,
719
+ children: inlineLabel("cancel")
720
+ }
721
+ )
722
+ ] })
723
+ ] }),
724
+ normalizedEntityOptions.length || normalizedDealOptions.length ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
725
+ normalizedEntityOptions.length ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
726
+ /* @__PURE__ */ jsx(
727
+ "label",
728
+ {
729
+ htmlFor: "note-entity-select",
730
+ className: "text-xs font-medium text-muted-foreground",
731
+ children: label("fields.entity", "Assign to customer")
732
+ }
733
+ ),
734
+ /* @__PURE__ */ jsx(
735
+ "select",
736
+ {
737
+ id: "note-entity-select",
738
+ className: "h-9 rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
739
+ value: selectedEntityId,
740
+ onChange: (event) => setSelectedEntityId(event.target.value),
741
+ disabled: isSubmitting || isLoading || !normalizedEntityOptions.length,
742
+ children: normalizedEntityOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.label }, option.id))
743
+ }
744
+ )
745
+ ] }) : null,
746
+ normalizedDealOptions.length ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
747
+ /* @__PURE__ */ jsx(
748
+ "label",
749
+ {
750
+ htmlFor: "note-deal-select",
751
+ className: "text-xs font-medium text-muted-foreground",
752
+ children: label("fields.deal", "Link to deal (optional)")
753
+ }
754
+ ),
755
+ /* @__PURE__ */ jsxs(
756
+ "select",
757
+ {
758
+ id: "note-deal-select",
759
+ className: "h-9 rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
760
+ value: selectedDealId,
761
+ onChange: (event) => setSelectedDealId(event.target.value),
762
+ disabled: isSubmitting || isLoading,
763
+ children: [
764
+ /* @__PURE__ */ jsx("option", { value: "", children: label("fields.dealPlaceholder", "No linked deal") }),
765
+ normalizedDealOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.label }, option.id))
766
+ ]
767
+ }
768
+ )
769
+ ] }) : null
770
+ ] }) : null,
771
+ /* @__PURE__ */ jsx(
772
+ SwitchableMarkdownInput,
773
+ {
774
+ value: draftBody,
775
+ onChange: setDraftBody,
776
+ isMarkdownEnabled,
777
+ disableMarkdown,
778
+ rows: 1,
779
+ placeholder: label("placeholder"),
780
+ textareaRef,
781
+ onTextareaInput: (event) => adjustTextareaSize(event.currentTarget),
782
+ disabled: isSubmitting || isLoading || !hasEntity,
783
+ remarkPlugins: markdownPlugins
784
+ }
785
+ ),
786
+ composerHasAppearance ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 rounded-lg border border-dashed border-muted-foreground/40 px-3 py-2", children: [
787
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 text-sm", children: [
788
+ draftIcon && renderIcon ? /* @__PURE__ */ jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded border border-border bg-muted/40", children: renderIcon(draftIcon, "h-4 w-4") }) : null,
789
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: composerAuthor }),
790
+ draftColor && renderColor ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
791
+ renderColor(draftColor, "h-3.5 w-3.5 rounded-full border border-border"),
792
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium uppercase text-muted-foreground", children: draftColor })
793
+ ] }) : null
794
+ ] }),
795
+ /* @__PURE__ */ jsx(
796
+ Button,
797
+ {
798
+ type: "button",
799
+ size: "sm",
800
+ variant: "ghost",
801
+ onClick: () => {
802
+ setDraftIcon(null);
803
+ setDraftColor(null);
804
+ },
805
+ disabled: isSubmitting,
806
+ children: label("appearance.clearAll", "Clear")
807
+ }
808
+ )
809
+ ] }) : null,
810
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(
811
+ Button,
812
+ {
813
+ type: "submit",
814
+ size: "sm",
815
+ disabled: isSubmitting || isLoading || !hasEntity,
816
+ children: [
817
+ isSubmitting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null,
818
+ composerSubmitLabel
819
+ ]
820
+ }
821
+ ) })
822
+ ]
823
+ }
824
+ ) : null
825
+ }
826
+ ),
827
+ loadError ? /* @__PURE__ */ jsx(ErrorMessage, { label: loadError, className: "mt-3" }) : null,
828
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
829
+ isLoading ? /* @__PURE__ */ jsx(
830
+ LoadingMessage,
831
+ {
832
+ label: label("loading", "Loading notes\u2026"),
833
+ className: "border-0 bg-transparent p-0 py-8 justify-center"
834
+ }
835
+ ) : hasVisibleNotes ? visibleNotes.map((note) => {
836
+ const author = noteAuthorLabel(note);
837
+ const isAppearanceSaving = appearanceDialogSaving && editingAppearanceNoteId === note.id;
838
+ const isEditingContent = contentEditor.id === note.id;
839
+ const displayIcon = note.appearanceIcon ?? null;
840
+ const displayColor = note.appearanceColor ?? null;
841
+ const timestampValue = note.createdAt;
842
+ const fallbackTimestampLabel = formatDateTime(note.createdAt) ?? emptyLabel;
843
+ return /* @__PURE__ */ jsxs("div", { className: "group space-y-2 rounded-lg border bg-card p-4", children: [
844
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
845
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
846
+ /* @__PURE__ */ jsx(
847
+ TimelineItemHeader,
848
+ {
849
+ title: author,
850
+ timestamp: timestampValue,
851
+ fallbackTimestampLabel,
852
+ icon: displayIcon,
853
+ color: displayColor,
854
+ renderIcon,
855
+ renderColor
856
+ }
857
+ ),
858
+ note.dealId ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
859
+ /* @__PURE__ */ jsx(ArrowUpRightSquare, { className: "h-3.5 w-3.5" }),
860
+ /* @__PURE__ */ jsx(
861
+ "a",
862
+ {
863
+ href: `/backend/customers/deals/${encodeURIComponent(note.dealId)}`,
864
+ className: "font-medium text-foreground hover:underline",
865
+ children: note.dealTitle && note.dealTitle.length ? note.dealTitle : label("linkedDeal", "Linked deal")
866
+ }
867
+ )
868
+ ] }) : null
869
+ ] }),
870
+ /* @__PURE__ */ jsxs(
871
+ "div",
872
+ {
873
+ className: `flex items-center gap-2 transition-opacity ${isEditingContent ? "opacity-100" : "opacity-0 group-hover:opacity-100 focus-within:opacity-100"}`,
874
+ children: [
875
+ /* @__PURE__ */ jsx(
876
+ Button,
877
+ {
878
+ type: "button",
879
+ variant: "ghost",
880
+ size: "icon",
881
+ onClick: () => setContentEditor({ id: note.id, value: note.body }),
882
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
883
+ }
884
+ ),
885
+ /* @__PURE__ */ jsx(
886
+ Button,
887
+ {
888
+ type: "button",
889
+ variant: "ghost",
890
+ size: "icon",
891
+ onClick: (event) => {
892
+ event.stopPropagation();
893
+ setAppearanceDialogError(null);
894
+ setAppearanceDialogState({
895
+ mode: "edit",
896
+ noteId: note.id,
897
+ icon: note.appearanceIcon ?? null,
898
+ color: note.appearanceColor ?? null
899
+ });
900
+ },
901
+ disabled: appearanceDialogSaving && editingAppearanceNoteId === note.id,
902
+ children: isAppearanceSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Palette, { className: "h-4 w-4" })
903
+ }
904
+ ),
905
+ /* @__PURE__ */ jsx(
906
+ Button,
907
+ {
908
+ type: "button",
909
+ variant: "ghost",
910
+ size: "icon",
911
+ onClick: (event) => {
912
+ event.stopPropagation();
913
+ void handleDeleteNote(note);
914
+ },
915
+ disabled: deletingNoteId === note.id,
916
+ children: deletingNoteId === note.id ? /* @__PURE__ */ jsx("span", { className: "relative flex h-4 w-4 items-center justify-center text-destructive", children: /* @__PURE__ */ jsx("span", { className: "absolute h-4 w-4 animate-spin rounded-full border border-destructive border-t-transparent" }) }) : /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
917
+ }
918
+ )
919
+ ]
920
+ }
921
+ )
922
+ ] }),
923
+ isEditingContent ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", onKeyDown: handleContentEditorKeyDown, children: [
924
+ /* @__PURE__ */ jsx(
925
+ SwitchableMarkdownInput,
926
+ {
927
+ value: contentEditor.value,
928
+ onChange: (nextValue) => setContentEditor((prev) => ({ ...prev, value: nextValue })),
929
+ isMarkdownEnabled,
930
+ disableMarkdown,
931
+ rows: 3,
932
+ textareaRef: contentTextareaRef,
933
+ onTextareaInput: (event) => adjustTextareaSize(event.currentTarget),
934
+ textareaClassName: "w-full resize-none overflow-hidden rounded-md border border-border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
935
+ editorWrapperClassName: "w-full rounded-md border border-muted-foreground/20 bg-background p-2",
936
+ remarkPlugins: markdownPlugins
937
+ }
938
+ ),
939
+ contentError ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: contentError }) : null,
940
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
941
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: handleContentSave, disabled: contentSavingId === note.id, children: contentSavingId === note.id ? /* @__PURE__ */ jsxs(Fragment, { children: [
942
+ /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
943
+ label("saving")
944
+ ] }) : inlineLabel("saveShortcut") }),
945
+ disableMarkdown ? null : /* @__PURE__ */ jsx(
946
+ Button,
947
+ {
948
+ type: "button",
949
+ variant: "ghost",
950
+ size: "icon",
951
+ onClick: handleMarkdownToggle,
952
+ "aria-pressed": isMarkdownEnabled,
953
+ className: isMarkdownEnabled ? "text-primary" : void 0,
954
+ disabled: contentSavingId === note.id,
955
+ children: /* @__PURE__ */ jsx(FileCode, { className: "h-4 w-4" })
956
+ }
957
+ ),
958
+ /* @__PURE__ */ jsx(
959
+ Button,
960
+ {
961
+ type: "button",
962
+ size: "sm",
963
+ variant: "ghost",
964
+ onClick: () => setContentEditor({ id: "", value: "" }),
965
+ disabled: contentSavingId === note.id,
966
+ children: inlineLabel("cancel")
967
+ }
968
+ )
969
+ ] })
970
+ ] }) : /* @__PURE__ */ jsx(
971
+ "div",
972
+ {
973
+ role: "button",
974
+ tabIndex: 0,
975
+ className: "cursor-pointer text-sm",
976
+ onClick: () => setContentEditor({ id: note.id, value: note.body }),
977
+ onKeyDown: (event) => handleContentKeyDown(event, note),
978
+ children: /* @__PURE__ */ jsx(
979
+ MarkdownPreviewComponent,
980
+ {
981
+ remarkPlugins: markdownPlugins,
982
+ className: "break-words text-foreground [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs",
983
+ children: note.body
984
+ }
985
+ )
986
+ }
987
+ )
988
+ ] }, note.id);
989
+ }) : composerOpen ? null : /* @__PURE__ */ jsx(
990
+ TabEmptyState,
991
+ {
992
+ title: emptyState.title,
993
+ description: emptyState.description,
994
+ action: {
995
+ label: emptyState.actionLabel,
996
+ onClick: focusComposer,
997
+ disabled: isSubmitting || !hasEntity
998
+ }
999
+ }
1000
+ ),
1001
+ isLoading || visibleCount >= notes.length ? null : /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleLoadMore, children: loadMoreLabel }) })
1002
+ ] }),
1003
+ /* @__PURE__ */ jsx(
1004
+ AppearanceDialog,
1005
+ {
1006
+ open: appearanceDialogOpen,
1007
+ title: appearanceDialogState?.mode === "edit" ? label("appearance.edit") : label("appearance.toggleOpen", "Customize appearance"),
1008
+ icon: appearanceDialogState?.icon ?? null,
1009
+ color: appearanceDialogState?.color ?? null,
1010
+ labels: noteAppearanceLabels,
1011
+ iconSuggestions,
1012
+ onIconChange: (value) => setAppearanceDialogState((prev) => prev ? { ...prev, icon: value ?? null } : prev),
1013
+ onColorChange: (value) => setAppearanceDialogState((prev) => prev ? { ...prev, color: value ?? null } : prev),
1014
+ onSubmit: () => {
1015
+ void handleAppearanceDialogSubmit();
1016
+ },
1017
+ onClose: handleAppearanceDialogClose,
1018
+ isSaving: appearanceDialogSaving,
1019
+ errorMessage: appearanceDialogError,
1020
+ primaryLabel: appearanceDialogPrimaryLabel,
1021
+ savingLabel: appearanceDialogSavingLabel,
1022
+ cancelLabel: label("appearance.cancel")
1023
+ }
1024
+ )
1025
+ ] });
1026
+ }
1027
+ export {
1028
+ NotesSection,
1029
+ mapCommentSummary,
1030
+ sanitizeHexColor
1031
+ };
1032
+ //# sourceMappingURL=NotesSection.js.map