@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,92 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import dynamic from "next/dynamic";
5
+ import { LoadingMessage } from "../detail/LoadingMessage.js";
6
+ const isTestEnv = typeof process !== "undefined" && process.env.NODE_ENV === "test";
7
+ const MarkdownEditorTestStub = ({ value, onChange }) => /* @__PURE__ */ jsx(
8
+ "textarea",
9
+ {
10
+ className: "min-h-[160px] w-full rounded border px-3 py-2 text-sm",
11
+ value: value ?? "",
12
+ onChange: (event) => onChange?.(event.target.value)
13
+ }
14
+ );
15
+ const UiMarkdownEditor = isTestEnv ? MarkdownEditorTestStub : dynamic(() => import("@uiw/react-md-editor"), {
16
+ ssr: false,
17
+ loading: () => /* @__PURE__ */ jsx(
18
+ LoadingMessage,
19
+ {
20
+ label: "Loading editor...",
21
+ className: "min-h-[220px] justify-center"
22
+ }
23
+ )
24
+ });
25
+ let markdownPluginsPromise = null;
26
+ async function loadMarkdownPlugins() {
27
+ if (isTestEnv) return [];
28
+ if (!markdownPluginsPromise) {
29
+ markdownPluginsPromise = import("remark-gfm").then((mod) => [mod.default ?? mod]).catch(() => []);
30
+ }
31
+ return markdownPluginsPromise;
32
+ }
33
+ function SwitchableMarkdownInput({
34
+ value,
35
+ onChange,
36
+ isMarkdownEnabled,
37
+ disableMarkdown,
38
+ height = 220,
39
+ placeholder,
40
+ rows = 3,
41
+ textareaRef,
42
+ onTextareaInput,
43
+ textareaClassName,
44
+ editorWrapperClassName,
45
+ editorClassName,
46
+ disabled,
47
+ remarkPlugins
48
+ }) {
49
+ const [localPlugins, setLocalPlugins] = React.useState([]);
50
+ React.useEffect(() => {
51
+ if (remarkPlugins) return;
52
+ let active = true;
53
+ void loadMarkdownPlugins().then((plugins) => {
54
+ if (active) setLocalPlugins(plugins);
55
+ });
56
+ return () => {
57
+ active = false;
58
+ };
59
+ }, [remarkPlugins]);
60
+ const resolvedPlugins = remarkPlugins ?? localPlugins;
61
+ const editorWrapperClasses = editorWrapperClassName ?? "w-full rounded-lg border border-muted-foreground/20 bg-background p-2";
62
+ const editorClasses = editorClassName ?? "w-full";
63
+ const textareaClasses = textareaClassName ?? "w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary";
64
+ if (isMarkdownEnabled && !disableMarkdown) {
65
+ return /* @__PURE__ */ jsx("div", { className: editorWrapperClasses, children: /* @__PURE__ */ jsx("div", { "data-color-mode": "light", className: editorClasses, children: /* @__PURE__ */ jsx(
66
+ UiMarkdownEditor,
67
+ {
68
+ value,
69
+ height,
70
+ onChange: (nextValue) => onChange(typeof nextValue === "string" ? nextValue : ""),
71
+ previewOptions: resolvedPlugins.length ? { remarkPlugins: resolvedPlugins } : void 0
72
+ }
73
+ ) }) });
74
+ }
75
+ return /* @__PURE__ */ jsx(
76
+ "textarea",
77
+ {
78
+ ref: textareaRef,
79
+ rows,
80
+ className: textareaClasses,
81
+ placeholder,
82
+ value,
83
+ onChange: (event) => onChange(event.target.value),
84
+ onInput: onTextareaInput,
85
+ disabled
86
+ }
87
+ );
88
+ }
89
+ export {
90
+ SwitchableMarkdownInput
91
+ };
92
+ //# sourceMappingURL=SwitchableMarkdownInput.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/inputs/SwitchableMarkdownInput.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport type { PluggableList } from 'unified'\nimport { LoadingMessage } from '../detail/LoadingMessage'\n\nexport type SwitchableMarkdownInputProps = {\n value: string\n onChange: (value: string) => void\n isMarkdownEnabled: boolean\n disableMarkdown?: boolean\n height?: number\n placeholder?: string\n rows?: number\n textareaRef?: React.Ref<HTMLTextAreaElement>\n onTextareaInput?: React.FormEventHandler<HTMLTextAreaElement>\n textareaClassName?: string\n editorWrapperClassName?: string\n editorClassName?: string\n disabled?: boolean\n remarkPlugins?: PluggableList\n}\n\ntype UiMarkdownEditorProps = {\n value?: string\n height?: number\n onChange?: (value?: string) => void\n previewOptions?: { remarkPlugins?: unknown[] }\n}\n\nconst isTestEnv = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'\n\nconst MarkdownEditorTestStub: React.ComponentType<UiMarkdownEditorProps> = ({ value, onChange }) => (\n <textarea\n className=\"min-h-[160px] w-full rounded border px-3 py-2 text-sm\"\n value={value ?? ''}\n onChange={(event) => onChange?.(event.target.value)}\n />\n)\n\nconst UiMarkdownEditor = isTestEnv\n ? MarkdownEditorTestStub\n : (dynamic(() => import('@uiw/react-md-editor'), {\n ssr: false,\n loading: () => (\n <LoadingMessage\n label=\"Loading editor...\"\n className=\"min-h-[220px] justify-center\"\n />\n ),\n }) as unknown as React.ComponentType<UiMarkdownEditorProps>)\n\nlet markdownPluginsPromise: Promise<PluggableList> | null = null\n\nasync function loadMarkdownPlugins(): Promise<PluggableList> {\n if (isTestEnv) return []\n if (!markdownPluginsPromise) {\n markdownPluginsPromise = import('remark-gfm')\n .then((mod) => [mod.default ?? mod] as PluggableList)\n .catch(() => [])\n }\n return markdownPluginsPromise\n}\n\nexport function SwitchableMarkdownInput({\n value,\n onChange,\n isMarkdownEnabled,\n disableMarkdown,\n height = 220,\n placeholder,\n rows = 3,\n textareaRef,\n onTextareaInput,\n textareaClassName,\n editorWrapperClassName,\n editorClassName,\n disabled,\n remarkPlugins,\n}: SwitchableMarkdownInputProps) {\n const [localPlugins, setLocalPlugins] = React.useState<PluggableList>([])\n\n React.useEffect(() => {\n if (remarkPlugins) return\n let active = true\n void loadMarkdownPlugins().then((plugins) => {\n if (active) setLocalPlugins(plugins)\n })\n return () => { active = false }\n }, [remarkPlugins])\n\n const resolvedPlugins = remarkPlugins ?? localPlugins\n const editorWrapperClasses =\n editorWrapperClassName ?? 'w-full rounded-lg border border-muted-foreground/20 bg-background p-2'\n const editorClasses = editorClassName ?? 'w-full'\n const textareaClasses =\n textareaClassName\n ?? 'w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'\n\n if (isMarkdownEnabled && !disableMarkdown) {\n return (\n <div className={editorWrapperClasses}>\n <div data-color-mode=\"light\" className={editorClasses}>\n <UiMarkdownEditor\n value={value}\n height={height}\n onChange={(nextValue) => onChange(typeof nextValue === 'string' ? nextValue : '')}\n previewOptions={resolvedPlugins.length ? { remarkPlugins: resolvedPlugins } : undefined}\n />\n </div>\n </div>\n )\n }\n\n return (\n <textarea\n ref={textareaRef}\n rows={rows}\n className={textareaClasses}\n placeholder={placeholder}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n onInput={onTextareaInput}\n disabled={disabled}\n />\n )\n}\n"],
5
+ "mappings": ";AAkCE;AAhCF,YAAY,WAAW;AACvB,OAAO,aAAa;AAEpB,SAAS,sBAAsB;AA0B/B,MAAM,YAAY,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;AAE7E,MAAM,yBAAqE,CAAC,EAAE,OAAO,SAAS,MAC5F;AAAA,EAAC;AAAA;AAAA,IACC,WAAU;AAAA,IACV,OAAO,SAAS;AAAA,IAChB,UAAU,CAAC,UAAU,WAAW,MAAM,OAAO,KAAK;AAAA;AACpD;AAGF,MAAM,mBAAmB,YACrB,yBACC,QAAQ,MAAM,OAAO,sBAAsB,GAAG;AAAA,EAC7C,KAAK;AAAA,EACL,SAAS,MACP;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,WAAU;AAAA;AAAA,EACZ;AAEJ,CAAC;AAEL,IAAI,yBAAwD;AAE5D,eAAe,sBAA8C;AAC3D,MAAI,UAAW,QAAO,CAAC;AACvB,MAAI,CAAC,wBAAwB;AAC3B,6BAAyB,OAAO,YAAY,EACzC,KAAK,CAAC,QAAQ,CAAC,IAAI,WAAW,GAAG,CAAkB,EACnD,MAAM,MAAM,CAAC,CAAC;AAAA,EACnB;AACA,SAAO;AACT;AAEO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,CAAC,CAAC;AAExE,QAAM,UAAU,MAAM;AACpB,QAAI,cAAe;AACnB,QAAI,SAAS;AACb,SAAK,oBAAoB,EAAE,KAAK,CAAC,YAAY;AAC3C,UAAI,OAAQ,iBAAgB,OAAO;AAAA,IACrC,CAAC;AACD,WAAO,MAAM;AAAE,eAAS;AAAA,IAAM;AAAA,EAChC,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,kBAAkB,iBAAiB;AACzC,QAAM,uBACJ,0BAA0B;AAC5B,QAAM,gBAAgB,mBAAmB;AACzC,QAAM,kBACJ,qBACG;AAEL,MAAI,qBAAqB,CAAC,iBAAiB;AACzC,WACE,oBAAC,SAAI,WAAW,sBACd,8BAAC,SAAI,mBAAgB,SAAQ,WAAW,eACtC;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,UAAU,CAAC,cAAc,SAAS,OAAO,cAAc,WAAW,YAAY,EAAE;AAAA,QAChF,gBAAgB,gBAAgB,SAAS,EAAE,eAAe,gBAAgB,IAAI;AAAA;AAAA,IAChF,GACF,GACF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,MAChD,SAAS;AAAA,MACT;AAAA;AAAA,EACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,222 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ function normalizeOptions(input) {
6
+ if (!Array.isArray(input)) return [];
7
+ return input.map((option) => {
8
+ if (typeof option === "string") {
9
+ const trimmed = option.trim();
10
+ if (!trimmed) return null;
11
+ return { value: trimmed, label: trimmed };
12
+ }
13
+ const value = typeof option.value === "string" ? option.value.trim() : "";
14
+ if (!value) return null;
15
+ return {
16
+ value,
17
+ label: option.label?.trim() || value,
18
+ description: option.description ?? null
19
+ };
20
+ }).filter((option) => !!option);
21
+ }
22
+ function TagsInput({
23
+ value,
24
+ onChange,
25
+ placeholder,
26
+ suggestions,
27
+ loadSuggestions,
28
+ selectedOptions,
29
+ resolveLabel,
30
+ resolveDescription,
31
+ autoFocus,
32
+ disabled = false,
33
+ allowCustomValues = true
34
+ }) {
35
+ const t = useT();
36
+ const [input, setInput] = React.useState("");
37
+ const [asyncOptions, setAsyncOptions] = React.useState([]);
38
+ const [loading, setLoading] = React.useState(false);
39
+ const [touched, setTouched] = React.useState(false);
40
+ const staticOptions = React.useMemo(() => normalizeOptions(suggestions), [suggestions]);
41
+ const selectedOptionList = React.useMemo(
42
+ () => normalizeOptions(selectedOptions),
43
+ [selectedOptions]
44
+ );
45
+ const optionMap = React.useMemo(() => {
46
+ const map = /* @__PURE__ */ new Map();
47
+ const register = (option) => {
48
+ if (!map.has(option.value)) {
49
+ map.set(option.value, option);
50
+ }
51
+ };
52
+ staticOptions.forEach(register);
53
+ asyncOptions.forEach(register);
54
+ selectedOptionList.forEach(register);
55
+ value.forEach((val) => {
56
+ if (map.has(val)) return;
57
+ map.set(val, {
58
+ value: val,
59
+ label: resolveLabel?.(val) ?? val,
60
+ description: resolveDescription?.(val) ?? null
61
+ });
62
+ });
63
+ return map;
64
+ }, [asyncOptions, resolveDescription, resolveLabel, selectedOptionList, staticOptions, value]);
65
+ const availableOptions = React.useMemo(() => {
66
+ return Array.from(optionMap.values()).filter((option) => !value.includes(option.value));
67
+ }, [optionMap, value]);
68
+ const filteredSuggestions = React.useMemo(() => {
69
+ const query = input.toLowerCase().trim();
70
+ if (!query) return availableOptions.slice(0, 8);
71
+ return availableOptions.filter((option) => {
72
+ const labelMatch = option.label.toLowerCase().includes(query);
73
+ const descMatch = option.description?.toLowerCase().includes(query);
74
+ return labelMatch || Boolean(descMatch);
75
+ });
76
+ }, [availableOptions, input]);
77
+ React.useEffect(() => {
78
+ if (!loadSuggestions || !touched || disabled) return;
79
+ const query = input.trim();
80
+ let cancelled = false;
81
+ const handle = window.setTimeout(async () => {
82
+ setLoading(true);
83
+ try {
84
+ const items = await loadSuggestions(query);
85
+ if (!cancelled) {
86
+ setAsyncOptions(normalizeOptions(items));
87
+ }
88
+ } finally {
89
+ if (!cancelled) setLoading(false);
90
+ }
91
+ }, 200);
92
+ return () => {
93
+ cancelled = true;
94
+ window.clearTimeout(handle);
95
+ };
96
+ }, [disabled, input, loadSuggestions, touched]);
97
+ const addValue = React.useCallback(
98
+ (nextValue) => {
99
+ if (disabled) return;
100
+ const trimmed = nextValue.trim();
101
+ if (!trimmed) return;
102
+ if (value.includes(trimmed)) return;
103
+ onChange([...value, trimmed]);
104
+ },
105
+ [disabled, onChange, value]
106
+ );
107
+ const findOptionForInput = React.useCallback(
108
+ (raw) => {
109
+ const query = raw.trim().toLowerCase();
110
+ if (!query) return null;
111
+ for (const option of optionMap.values()) {
112
+ if (option.value === raw.trim()) return option;
113
+ if (option.label.toLowerCase() === query) return option;
114
+ }
115
+ return null;
116
+ },
117
+ [optionMap]
118
+ );
119
+ const addTag = React.useCallback(
120
+ (raw) => {
121
+ if (disabled) return;
122
+ const option = findOptionForInput(raw);
123
+ if (option) {
124
+ addValue(option.value);
125
+ return;
126
+ }
127
+ if (!allowCustomValues) return;
128
+ addValue(raw);
129
+ },
130
+ [addValue, allowCustomValues, disabled, findOptionForInput]
131
+ );
132
+ const removeTag = React.useCallback(
133
+ (tag) => {
134
+ if (disabled) return;
135
+ onChange(value.filter((candidate) => candidate !== tag));
136
+ },
137
+ [disabled, onChange, value]
138
+ );
139
+ return /* @__PURE__ */ jsx(
140
+ "div",
141
+ {
142
+ className: [
143
+ "w-full rounded border px-2 py-1",
144
+ disabled ? "bg-muted text-muted-foreground/80 cursor-not-allowed" : ""
145
+ ].filter(Boolean).join(" "),
146
+ "aria-disabled": disabled || void 0,
147
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
148
+ value.map((tag) => {
149
+ const option = optionMap.get(tag);
150
+ const label = option?.label ?? tag;
151
+ const description = option?.description;
152
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 rounded-sm bg-muted px-2 py-0.5 text-xs", children: [
153
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col items-start leading-tight", children: [
154
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: label }),
155
+ description ? /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: description }) : null
156
+ ] }),
157
+ /* @__PURE__ */ jsx(
158
+ "button",
159
+ {
160
+ type: "button",
161
+ className: "opacity-60 transition-opacity hover:opacity-100",
162
+ onClick: () => removeTag(tag),
163
+ disabled,
164
+ children: "\xD7"
165
+ }
166
+ )
167
+ ] }, tag);
168
+ }),
169
+ /* @__PURE__ */ jsx(
170
+ "input",
171
+ {
172
+ className: "flex-1 min-w-[120px] border-0 py-1 text-sm outline-none disabled:bg-transparent",
173
+ value: input,
174
+ placeholder: placeholder || t("ui.inputs.tagsInput.placeholder", "Add tag and press Enter"),
175
+ autoFocus,
176
+ "data-crud-focus-target": "",
177
+ disabled,
178
+ onFocus: () => setTouched(true),
179
+ onChange: (event) => {
180
+ setTouched(true);
181
+ setInput(event.target.value);
182
+ },
183
+ onKeyDown: (event) => {
184
+ if (disabled) return;
185
+ if (event.key === "Enter" || event.key === ",") {
186
+ event.preventDefault();
187
+ addTag(input);
188
+ setInput("");
189
+ } else if (event.key === "Backspace" && input === "" && value.length > 0) {
190
+ removeTag(value[value.length - 1]);
191
+ }
192
+ },
193
+ onBlur: () => {
194
+ if (disabled) return;
195
+ addTag(input);
196
+ setInput("");
197
+ }
198
+ }
199
+ ),
200
+ loading && touched ? /* @__PURE__ */ jsx("div", { className: "basis-full mt-1 text-xs text-muted-foreground", children: "Loading suggestions\u2026" }) : null,
201
+ !loading && filteredSuggestions.length ? /* @__PURE__ */ jsx("div", { className: "basis-full mt-1 flex flex-col gap-1", children: filteredSuggestions.map((option) => /* @__PURE__ */ jsxs(
202
+ "button",
203
+ {
204
+ type: "button",
205
+ className: "flex flex-col items-start rounded border px-1.5 py-1 text-xs transition hover:bg-muted",
206
+ onMouseDown: (event) => event.preventDefault(),
207
+ onClick: () => addValue(option.value),
208
+ children: [
209
+ /* @__PURE__ */ jsx("span", { children: option.label }),
210
+ option.description ? /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: option.description }) : null
211
+ ]
212
+ },
213
+ option.value
214
+ )) }) : null
215
+ ] })
216
+ }
217
+ );
218
+ }
219
+ export {
220
+ TagsInput
221
+ };
222
+ //# sourceMappingURL=TagsInput.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/inputs/TagsInput.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type TagsInputOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type TagsInputProps = {\n value: string[]\n onChange: (next: string[]) => void\n placeholder?: string\n suggestions?: Array<string | TagsInputOption>\n loadSuggestions?: (query?: string) => Promise<Array<string | TagsInputOption>>\n selectedOptions?: TagsInputOption[]\n resolveLabel?: (value: string) => string\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n}\n\nfunction normalizeOptions(input?: Array<string | TagsInputOption>): TagsInputOption[] {\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 TagsInputOption => !!option)\n}\n\nexport function TagsInput({\n value,\n onChange,\n placeholder,\n suggestions,\n loadSuggestions,\n selectedOptions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n}: TagsInputProps) {\n const t = useT()\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<TagsInputOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n\n const staticOptions = React.useMemo(() => normalizeOptions(suggestions), [suggestions])\n const selectedOptionList = React.useMemo(\n () => normalizeOptions(selectedOptions),\n [selectedOptions]\n )\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, TagsInputOption>()\n const register = (option: TagsInputOption) => {\n if (!map.has(option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n selectedOptionList.forEach(register)\n value.forEach((val) => {\n if (map.has(val)) return\n map.set(val, {\n value: val,\n label: resolveLabel?.(val) ?? val,\n description: resolveDescription?.(val) ?? null,\n })\n })\n return map\n }, [asyncOptions, resolveDescription, resolveLabel, selectedOptionList, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values()).filter((option) => !value.includes(option.value))\n }, [optionMap, value])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions.slice(0, 8)\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(async () => {\n setLoading(true)\n try {\n const items = await loadSuggestions(query)\n if (!cancelled) {\n setAsyncOptions(normalizeOptions(items))\n }\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 const addValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n const trimmed = nextValue.trim()\n if (!trimmed) return\n if (value.includes(trimmed)) return\n onChange([...value, trimmed])\n },\n [disabled, onChange, value]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): TagsInputOption | 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 addTag = React.useCallback(\n (raw: string) => {\n if (disabled) return\n const option = findOptionForInput(raw)\n if (option) {\n addValue(option.value)\n return\n }\n if (!allowCustomValues) return\n addValue(raw)\n },\n [addValue, allowCustomValues, disabled, findOptionForInput]\n )\n\n const removeTag = React.useCallback(\n (tag: string) => {\n if (disabled) return\n onChange(value.filter((candidate) => candidate !== tag))\n },\n [disabled, onChange, value]\n )\n\n return (\n <div\n className={[\n 'w-full rounded border px-2 py-1',\n disabled ? 'bg-muted text-muted-foreground/80 cursor-not-allowed' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n aria-disabled={disabled || undefined}\n >\n <div className=\"flex flex-wrap gap-1\">\n {value.map((tag) => {\n const option = optionMap.get(tag)\n const label = option?.label ?? tag\n const description = option?.description\n return (\n <span key={tag} className=\"inline-flex items-center gap-2 rounded-sm bg-muted px-2 py-0.5 text-xs\">\n <span className=\"flex flex-col items-start leading-tight\">\n <span className=\"whitespace-nowrap\">{label}</span>\n {description ? (\n <span className=\"text-[10px] text-muted-foreground\">{description}</span>\n ) : null}\n </span>\n <button\n type=\"button\"\n className=\"opacity-60 transition-opacity hover:opacity-100\"\n onClick={() => removeTag(tag)}\n disabled={disabled}\n >\n \u00D7\n </button>\n </span>\n )\n })}\n <input\n className=\"flex-1 min-w-[120px] border-0 py-1 text-sm outline-none disabled:bg-transparent\"\n value={input}\n placeholder={placeholder || t('ui.inputs.tagsInput.placeholder', 'Add tag and press Enter')}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => setTouched(true)}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n }}\n onKeyDown={(event) => {\n if (disabled) return\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault()\n addTag(input)\n setInput('')\n } else if (event.key === 'Backspace' && input === '' && value.length > 0) {\n removeTag(value[value.length - 1])\n }\n }}\n onBlur={() => {\n if (disabled) return\n addTag(input)\n setInput('')\n }}\n />\n {loading && touched ? (\n <div className=\"basis-full mt-1 text-xs text-muted-foreground\">Loading suggestions\u2026</div>\n ) : null}\n {!loading && filteredSuggestions.length ? (\n <div className=\"basis-full mt-1 flex flex-col gap-1\">\n {filteredSuggestions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n className=\"flex flex-col items-start rounded border px-1.5 py-1 text-xs transition hover:bg-muted\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => addValue(option.value)}\n >\n <span>{option.label}</span>\n {option.description ? (\n <span className=\"text-[10px] text-muted-foreground\">{option.description}</span>\n ) : null}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./PhoneNumberField.js";
2
+ export * from "./TagsInput.js";
3
+ export * from "./LookupSelect.js";
4
+ export * from "./ComboboxInput.js";
5
+ export * from "./SwitchableMarkdownInput.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/inputs/index.ts"],
4
+ "sourcesContent": ["export * from './PhoneNumberField'\nexport * from './TagsInput'\nexport * from './LookupSelect'\nexport * from './ComboboxInput'\nexport * from './SwitchableMarkdownInput'\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
6
+ "names": []
7
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Undo2 } from "lucide-react";
5
+ import { useRouter } from "next/navigation";
6
+ import { Button } from "../../primitives/button.js";
7
+ import { apiCall } from "../utils/apiCall.js";
8
+ import { flash } from "../FlashMessages.js";
9
+ import { useLastOperation, markUndoSuccess } from "./store.js";
10
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
11
+ function LastOperationBanner() {
12
+ const t = useT();
13
+ const operation = useLastOperation();
14
+ const [pendingToken, setPendingToken] = React.useState(null);
15
+ const router = useRouter();
16
+ if (!operation) return null;
17
+ const rawLabel = operation.actionLabel ?? operation.commandId;
18
+ const translatedLabel = t(rawLabel);
19
+ const label = translatedLabel === rawLabel ? rawLabel : translatedLabel;
20
+ const isPending = pendingToken === operation.undoToken;
21
+ async function handleUndo() {
22
+ const undoToken = operation?.undoToken;
23
+ if (!undoToken || isPending) return;
24
+ setPendingToken(undoToken);
25
+ try {
26
+ const call = await apiCall("/api/audit_logs/audit-logs/actions/undo", {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify({ undoToken })
30
+ });
31
+ if (!call.ok) {
32
+ const message = call.result && typeof call.result.error === "string" && call.result.error || "";
33
+ throw new Error(message || t("audit_logs.banner.undo_failed", "Failed to undo"));
34
+ }
35
+ markUndoSuccess(undoToken);
36
+ flash(t("audit_logs.banner.undo_success"), "success");
37
+ router.refresh();
38
+ if (typeof window !== "undefined") {
39
+ try {
40
+ const isJSDOM = typeof navigator !== "undefined" && typeof navigator.userAgent === "string" ? navigator.userAgent.toLowerCase().includes("jsdom") : false;
41
+ if (!isJSDOM && typeof window.location?.reload === "function") {
42
+ window.location.reload();
43
+ }
44
+ } catch {
45
+ }
46
+ }
47
+ } catch (err) {
48
+ const message = err instanceof Error && err.message ? err.message : t("audit_logs.banner.undo_error");
49
+ flash(message, "error");
50
+ } finally {
51
+ setPendingToken(null);
52
+ }
53
+ }
54
+ return /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900", children: [
55
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 truncate", children: [
56
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-amber-950", children: t("audit_logs.banner.last_operation") }),
57
+ /* @__PURE__ */ jsx("span", { className: "ml-2 truncate text-amber-900", children: label })
58
+ ] }),
59
+ /* @__PURE__ */ jsxs(
60
+ Button,
61
+ {
62
+ variant: "outline",
63
+ size: "sm",
64
+ onClick: () => {
65
+ void handleUndo();
66
+ },
67
+ disabled: isPending,
68
+ className: "border-amber-300 text-amber-900 hover:bg-amber-100",
69
+ children: [
70
+ /* @__PURE__ */ jsx(Undo2, { className: "mr-1 size-4", "aria-hidden": "true" }),
71
+ isPending ? t("audit_logs.actions.undoing") : t("audit_logs.banner.undo")
72
+ ]
73
+ }
74
+ )
75
+ ] });
76
+ }
77
+ export {
78
+ LastOperationBanner
79
+ };
80
+ //# sourceMappingURL=LastOperationBanner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/operations/LastOperationBanner.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Undo2 } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '../utils/apiCall'\nimport { flash } from '../FlashMessages'\nimport { useLastOperation, markUndoSuccess } from './store'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport function LastOperationBanner() {\n const t = useT()\n const operation = useLastOperation()\n const [pendingToken, setPendingToken] = React.useState<string | null>(null)\n const router = useRouter()\n\n if (!operation) return null\n\n const rawLabel = operation.actionLabel ?? operation.commandId\n const translatedLabel = t(rawLabel)\n const label = translatedLabel === rawLabel ? rawLabel : translatedLabel\n const isPending = pendingToken === operation.undoToken\n\n async function handleUndo() {\n const undoToken = operation?.undoToken\n if (!undoToken || isPending) return\n setPendingToken(undoToken)\n try {\n const call = await apiCall<Record<string, unknown>>('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ undoToken }),\n })\n if (!call.ok) {\n const message =\n (call.result && typeof call.result.error === 'string' && call.result.error) ||\n ''\n throw new Error(message || t('audit_logs.banner.undo_failed', 'Failed to undo'))\n }\n markUndoSuccess(undoToken)\n flash(t('audit_logs.banner.undo_success'), 'success')\n router.refresh()\n if (typeof window !== 'undefined') {\n try {\n const isJSDOM = typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string'\n ? navigator.userAgent.toLowerCase().includes('jsdom')\n : false\n if (!isJSDOM && typeof window.location?.reload === 'function') {\n window.location.reload()\n }\n } catch {\n // noop in non-browser or jsdom environments\n }\n }\n } catch (err) {\n const message = err instanceof Error && err.message ? err.message : t('audit_logs.banner.undo_error')\n flash(message, 'error')\n } finally {\n setPendingToken(null)\n }\n }\n\n return (\n <div className=\"mb-4 flex items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900\">\n <div className=\"min-w-0 truncate\">\n <span className=\"font-medium text-amber-950\">\n {t('audit_logs.banner.last_operation')}\n </span>\n <span className=\"ml-2 truncate text-amber-900\">\n {label}\n </span>\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void handleUndo() }}\n disabled={isPending}\n className=\"border-amber-300 text-amber-900 hover:bg-amber-100\"\n >\n <Undo2 className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {isPending ? t('audit_logs.actions.undoing') : t('audit_logs.banner.undo')}\n </Button>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAgEM,SACE,KADF;AA/DN,YAAY,WAAW;AACvB,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,YAAY;AAEd,SAAS,sBAAsB;AACpC,QAAM,IAAI,KAAK;AACf,QAAM,YAAY,iBAAiB;AACnC,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,IAAI;AAC1E,QAAM,SAAS,UAAU;AAEzB,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,WAAW,UAAU,eAAe,UAAU;AACpD,QAAM,kBAAkB,EAAE,QAAQ;AAClC,QAAM,QAAQ,oBAAoB,WAAW,WAAW;AACxD,QAAM,YAAY,iBAAiB,UAAU;AAE7C,iBAAe,aAAa;AAC1B,UAAM,YAAY,WAAW;AAC7B,QAAI,CAAC,aAAa,UAAW;AAC7B,oBAAgB,SAAS;AACzB,QAAI;AACF,YAAM,OAAO,MAAM,QAAiC,2CAA2C;AAAA,QAC7F,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;AAAA,MACpC,CAAC;AACD,UAAI,CAAC,KAAK,IAAI;AACZ,cAAM,UACH,KAAK,UAAU,OAAO,KAAK,OAAO,UAAU,YAAY,KAAK,OAAO,SACrE;AACF,cAAM,IAAI,MAAM,WAAW,EAAE,iCAAiC,gBAAgB,CAAC;AAAA,MACjF;AACA,sBAAgB,SAAS;AACzB,YAAM,EAAE,gCAAgC,GAAG,SAAS;AACpD,aAAO,QAAQ;AACf,UAAI,OAAO,WAAW,aAAa;AACjC,YAAI;AACF,gBAAM,UAAU,OAAO,cAAc,eAAe,OAAO,UAAU,cAAc,WAC/E,UAAU,UAAU,YAAY,EAAE,SAAS,OAAO,IAClD;AACJ,cAAI,CAAC,WAAW,OAAO,OAAO,UAAU,WAAW,YAAY;AAC7D,mBAAO,SAAS,OAAO;AAAA,UACzB;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,SAAS,IAAI,UAAU,IAAI,UAAU,EAAE,8BAA8B;AACpG,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,sBAAgB,IAAI;AAAA,IACtB;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,gIACb;AAAA,yBAAC,SAAI,WAAU,oBACb;AAAA,0BAAC,UAAK,WAAU,8BACb,YAAE,kCAAkC,GACvC;AAAA,MACA,oBAAC,UAAK,WAAU,gCACb,iBACH;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM;AAAE,eAAK,WAAW;AAAA,QAAE;AAAA,QACnC,UAAU;AAAA,QACV,WAAU;AAAA,QAEV;AAAA,8BAAC,SAAM,WAAU,eAAc,eAAY,QAAO;AAAA,UACjD,YAAY,EAAE,4BAA4B,IAAI,EAAE,wBAAwB;AAAA;AAAA;AAAA,IAC3E;AAAA,KACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,183 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const DEFAULT_STATE = { stack: [], undone: [] };
4
+ const STORAGE_KEY = "om:last-operations:v1";
5
+ const STACK_LIMIT = 20;
6
+ const LAST_OPERATION_TTL_MS = 6e4;
7
+ const STACK_RETENTION_MS = 10 * 6e4;
8
+ let internalState = DEFAULT_STATE;
9
+ if (typeof window !== "undefined") {
10
+ internalState = loadState();
11
+ }
12
+ const emitter = new EventTarget();
13
+ function now() {
14
+ return typeof performance !== "undefined" && performance.now ? Math.round(performance.timeOrigin + performance.now()) : Date.now();
15
+ }
16
+ function loadState() {
17
+ try {
18
+ const raw = window.localStorage.getItem(STORAGE_KEY);
19
+ if (!raw) return DEFAULT_STATE;
20
+ const parsed = JSON.parse(raw);
21
+ if (!parsed || typeof parsed !== "object") return DEFAULT_STATE;
22
+ const stack = Array.isArray(parsed.stack) ? parsed.stack.filter(isValidEntry).map(hydrateEntry) : [];
23
+ const undone = Array.isArray(parsed.undone) ? parsed.undone.filter(isValidEntry).map((raw2) => {
24
+ const hydrated = hydrateEntry(raw2);
25
+ const candidate = raw2;
26
+ const undoneAt = typeof candidate.undoneAt === "number" ? candidate.undoneAt : now();
27
+ return { ...hydrated, undoneAt };
28
+ }) : [];
29
+ return pruneState({ stack, undone });
30
+ } catch {
31
+ return DEFAULT_STATE;
32
+ }
33
+ }
34
+ function isValidEntry(entry) {
35
+ if (entry == null || typeof entry !== "object") return false;
36
+ const candidate = entry;
37
+ return typeof candidate.id === "string" && typeof candidate.undoToken === "string" && typeof candidate.commandId === "string" && typeof candidate.receivedAt === "number" && typeof candidate.executedAt === "string";
38
+ }
39
+ function hydrateEntry(entry) {
40
+ const source = entry;
41
+ return {
42
+ id: String(source.id),
43
+ undoToken: String(source.undoToken),
44
+ commandId: String(source.commandId),
45
+ actionLabel: typeof source.actionLabel === "string" ? source.actionLabel : source.actionLabel === null ? null : null,
46
+ resourceKind: typeof source.resourceKind === "string" ? source.resourceKind : null,
47
+ resourceId: typeof source.resourceId === "string" ? source.resourceId : null,
48
+ executedAt: typeof source.executedAt === "string" ? source.executedAt : new Date(source.receivedAt || now()).toISOString(),
49
+ receivedAt: typeof source.receivedAt === "number" ? source.receivedAt : now()
50
+ };
51
+ }
52
+ function persist(state) {
53
+ if (typeof window === "undefined") return;
54
+ try {
55
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
56
+ } catch {
57
+ }
58
+ }
59
+ function pruneState(state) {
60
+ const timestamp = now();
61
+ const stack = state.stack.filter((entry, index, arr) => {
62
+ const duplicateIndex = arr.findIndex((candidate) => candidate.id === entry.id || candidate.undoToken === entry.undoToken);
63
+ if (duplicateIndex !== index) return false;
64
+ return timestamp - entry.receivedAt <= STACK_RETENTION_MS;
65
+ }).sort((a, b) => a.receivedAt - b.receivedAt).slice(-STACK_LIMIT);
66
+ const undone = state.undone.filter((entry) => timestamp - entry.undoneAt <= STACK_RETENTION_MS).sort((a, b) => a.undoneAt - b.undoneAt).slice(-STACK_LIMIT);
67
+ const next = { stack, undone };
68
+ return next;
69
+ }
70
+ function emit() {
71
+ emitter.dispatchEvent(new Event("change"));
72
+ }
73
+ function updateState(updater) {
74
+ const next = pruneState(updater(internalState));
75
+ internalState = next;
76
+ persist(next);
77
+ emit();
78
+ }
79
+ function subscribe(listener) {
80
+ const wrapped = () => listener();
81
+ emitter.addEventListener("change", wrapped);
82
+ return () => emitter.removeEventListener("change", wrapped);
83
+ }
84
+ function getClientSnapshot() {
85
+ internalState = pruneState(internalState);
86
+ return internalState;
87
+ }
88
+ function useOperationStore(selector) {
89
+ return React.useSyncExternalStore(
90
+ subscribe,
91
+ () => selector(getClientSnapshot()),
92
+ () => selector(DEFAULT_STATE)
93
+ );
94
+ }
95
+ function pushOperation(meta) {
96
+ if (typeof window === "undefined") return;
97
+ updateState((prev) => {
98
+ const entry = {
99
+ ...meta,
100
+ receivedAt: now()
101
+ };
102
+ const stack = prev.stack.filter((item) => item.id !== entry.id && item.undoToken !== entry.undoToken);
103
+ stack.push(entry);
104
+ return { stack, undone: [] };
105
+ });
106
+ }
107
+ function markUndoSuccess(undoToken) {
108
+ if (typeof window === "undefined") return;
109
+ const removed = [];
110
+ updateState((prev) => {
111
+ const stack = prev.stack.filter((entry) => {
112
+ if (entry.undoToken === undoToken) {
113
+ removed.push(entry);
114
+ return false;
115
+ }
116
+ return true;
117
+ });
118
+ const undone = removed.length ? [...prev.undone, ...removed.map((entry) => ({ ...entry, undoneAt: now() }))] : prev.undone;
119
+ return { stack, undone };
120
+ });
121
+ }
122
+ function markRedoConsumed(logId) {
123
+ if (typeof window === "undefined") return;
124
+ updateState((prev) => ({
125
+ stack: prev.stack,
126
+ undone: prev.undone.filter((entry) => entry.id !== logId)
127
+ }));
128
+ }
129
+ function getLastOperation() {
130
+ const state = getClientSnapshot();
131
+ if (!state.stack.length) return null;
132
+ const last = state.stack[state.stack.length - 1];
133
+ const lastExecuted = Date.parse(last.executedAt);
134
+ const cutoff = now() - LAST_OPERATION_TTL_MS;
135
+ if (Number.isFinite(lastExecuted) && lastExecuted < cutoff) return null;
136
+ if (!Number.isFinite(lastExecuted) && last.receivedAt < cutoff) return null;
137
+ return last;
138
+ }
139
+ function useLastOperation() {
140
+ return useOperationStore(getLastOperationFromState);
141
+ }
142
+ function getLastOperationFromState(state) {
143
+ if (!state.stack.length) return null;
144
+ const last = state.stack[state.stack.length - 1];
145
+ const timestamp = now();
146
+ const executedAt = Date.parse(last.executedAt);
147
+ const cutoff = timestamp - LAST_OPERATION_TTL_MS;
148
+ if (Number.isFinite(executedAt)) {
149
+ return executedAt >= cutoff ? last : null;
150
+ }
151
+ return last.receivedAt >= cutoff ? last : null;
152
+ }
153
+ function useRedoCandidate() {
154
+ return useOperationStore((state) => state.undone.length ? state.undone[state.undone.length - 1] : null);
155
+ }
156
+ function hasRedoCandidate(logId) {
157
+ const state = getClientSnapshot();
158
+ if (!state.undone.length) return false;
159
+ const top = state.undone[state.undone.length - 1];
160
+ return top.id === logId;
161
+ }
162
+ function clearAllOperations() {
163
+ if (typeof window === "undefined") return;
164
+ internalState = DEFAULT_STATE;
165
+ persist(internalState);
166
+ emit();
167
+ }
168
+ const operationStackConstants = {
169
+ LAST_OPERATION_TTL_MS
170
+ };
171
+ export {
172
+ clearAllOperations,
173
+ getLastOperation,
174
+ hasRedoCandidate,
175
+ markRedoConsumed,
176
+ markUndoSuccess,
177
+ operationStackConstants,
178
+ pushOperation,
179
+ useLastOperation,
180
+ useOperationStore,
181
+ useRedoCandidate
182
+ };
183
+ //# sourceMappingURL=store.js.map