@open-mercato/ui 0.6.4-develop.4371.1.8f3030407e → 0.6.4

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 (133) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +2 -1
  3. package/dist/backend/AppShell.js +6 -0
  4. package/dist/backend/AppShell.js.map +2 -2
  5. package/dist/backend/CrudForm.js +212 -85
  6. package/dist/backend/CrudForm.js.map +3 -3
  7. package/dist/backend/DataTable.js +77 -26
  8. package/dist/backend/DataTable.js.map +3 -3
  9. package/dist/backend/FlashMessages.js +21 -2
  10. package/dist/backend/FlashMessages.js.map +2 -2
  11. package/dist/backend/JsonBuilder.js +64 -41
  12. package/dist/backend/JsonBuilder.js.map +2 -2
  13. package/dist/backend/PerspectiveSidebar.js +8 -1
  14. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  15. package/dist/backend/RowActions.js +1 -1
  16. package/dist/backend/RowActions.js.map +2 -2
  17. package/dist/backend/conflicts/RecordConflictBanner.js +73 -0
  18. package/dist/backend/conflicts/RecordConflictBanner.js.map +7 -0
  19. package/dist/backend/conflicts/index.js +32 -0
  20. package/dist/backend/conflicts/index.js.map +7 -0
  21. package/dist/backend/conflicts/store.js +50 -0
  22. package/dist/backend/conflicts/store.js.map +7 -0
  23. package/dist/backend/detail/ActivitiesSection.js +1 -0
  24. package/dist/backend/detail/ActivitiesSection.js.map +2 -2
  25. package/dist/backend/detail/InlineEditors.js +2 -51
  26. package/dist/backend/detail/InlineEditors.js.map +3 -3
  27. package/dist/backend/detail/RecordNotFoundState.js +10 -5
  28. package/dist/backend/detail/RecordNotFoundState.js.map +2 -2
  29. package/dist/backend/filters/AdvancedFilterPanel.js +14 -0
  30. package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
  31. package/dist/backend/filters/FilteredEmptyResults.js +12 -4
  32. package/dist/backend/filters/FilteredEmptyResults.js.map +2 -2
  33. package/dist/backend/filters/ListEmptyState.js +44 -0
  34. package/dist/backend/filters/ListEmptyState.js.map +7 -0
  35. package/dist/backend/filters/SearchEmptyResults.js +28 -0
  36. package/dist/backend/filters/SearchEmptyResults.js.map +7 -0
  37. package/dist/backend/icons/lucideRegistry.generated.js +8 -0
  38. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  39. package/dist/backend/injection/useGuardedMutation.js +8 -1
  40. package/dist/backend/injection/useGuardedMutation.js.map +2 -2
  41. package/dist/backend/inputs/MarkdownField.js +20 -0
  42. package/dist/backend/inputs/MarkdownField.js.map +7 -0
  43. package/dist/backend/inputs/MdxEditorImpl.js +98 -0
  44. package/dist/backend/inputs/MdxEditorImpl.js.map +7 -0
  45. package/dist/backend/inputs/SwitchableMarkdownInput.js +6 -38
  46. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
  47. package/dist/backend/markdown/markdownToPlainText.js +29 -0
  48. package/dist/backend/markdown/markdownToPlainText.js.map +2 -2
  49. package/dist/backend/messages/EmailThreadsPanel.js +212 -0
  50. package/dist/backend/messages/EmailThreadsPanel.js.map +7 -0
  51. package/dist/backend/messages/index.js +4 -0
  52. package/dist/backend/messages/index.js.map +2 -2
  53. package/dist/backend/messages/mergeOptimisticEmailThreads.js +55 -0
  54. package/dist/backend/messages/mergeOptimisticEmailThreads.js.map +7 -0
  55. package/dist/backend/notifications/NotificationDispatcher.js +1 -0
  56. package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
  57. package/dist/backend/notifications/useNotificationsPoll.js +7 -0
  58. package/dist/backend/notifications/useNotificationsPoll.js.map +2 -2
  59. package/dist/backend/notifications/useNotificationsSse.js +8 -0
  60. package/dist/backend/notifications/useNotificationsSse.js.map +2 -2
  61. package/dist/backend/sidebar/SidebarCustomizationEditor.js +23 -7
  62. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  63. package/dist/backend/utils/optimisticLock.js +34 -0
  64. package/dist/backend/utils/optimisticLock.js.map +7 -0
  65. package/dist/portal/hooks/useTenantContext.js +1 -1
  66. package/dist/portal/hooks/useTenantContext.js.map +2 -2
  67. package/dist/primitives/alert.js.map +1 -1
  68. package/dist/primitives/checkbox.js +2 -1
  69. package/dist/primitives/checkbox.js.map +2 -2
  70. package/dist/types/css.d.js +1 -0
  71. package/dist/types/css.d.js.map +7 -0
  72. package/package.json +17 -13
  73. package/src/backend/AppShell.tsx +10 -0
  74. package/src/backend/CrudForm.tsx +320 -97
  75. package/src/backend/DataTable.tsx +113 -34
  76. package/src/backend/FlashMessages.tsx +31 -2
  77. package/src/backend/JsonBuilder.tsx +79 -26
  78. package/src/backend/PerspectiveSidebar.tsx +8 -1
  79. package/src/backend/RowActions.tsx +1 -1
  80. package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +0 -1
  81. package/src/backend/__tests__/CrudForm.datetime.test.tsx +0 -1
  82. package/src/backend/__tests__/CrudForm.defaults.test.tsx +0 -1
  83. package/src/backend/__tests__/CrudForm.dotPath.test.tsx +311 -0
  84. package/src/backend/__tests__/CrudForm.doubleSubmit.test.tsx +0 -1
  85. package/src/backend/__tests__/CrudForm.navigation.test.tsx +120 -1
  86. package/src/backend/__tests__/CrudForm.optimisticLock.test.tsx +287 -0
  87. package/src/backend/__tests__/CrudForm.render.test.tsx +74 -2
  88. package/src/backend/__tests__/CrudForm.sortable.test.tsx +0 -1
  89. package/src/backend/__tests__/CrudForm.transformData.test.tsx +0 -1
  90. package/src/backend/__tests__/CrudForm.unsavedNavigation.test.tsx +51 -3
  91. package/src/backend/__tests__/CrudForm.validation.test.tsx +0 -1
  92. package/src/backend/__tests__/CustomDataSection.test.tsx +0 -1
  93. package/src/backend/__tests__/FlashMessages.test.tsx +47 -0
  94. package/src/backend/__tests__/JsonBuilder.test.tsx +135 -0
  95. package/src/backend/__tests__/lazy-heavy-libraries.test.ts +16 -3
  96. package/src/backend/conflicts/RecordConflictBanner.tsx +77 -0
  97. package/src/backend/conflicts/__tests__/RecordConflictBanner.test.tsx +61 -0
  98. package/src/backend/conflicts/__tests__/store.test.ts +78 -0
  99. package/src/backend/conflicts/index.ts +49 -0
  100. package/src/backend/conflicts/store.ts +94 -0
  101. package/src/backend/detail/ActivitiesSection.tsx +1 -0
  102. package/src/backend/detail/InlineEditors.tsx +14 -76
  103. package/src/backend/detail/RecordNotFoundState.tsx +13 -6
  104. package/src/backend/detail/__tests__/RecordNotFoundState.test.tsx +80 -0
  105. package/src/backend/filters/AdvancedFilterPanel.tsx +27 -0
  106. package/src/backend/filters/FilteredEmptyResults.tsx +23 -10
  107. package/src/backend/filters/ListEmptyState.tsx +69 -0
  108. package/src/backend/filters/SearchEmptyResults.tsx +42 -0
  109. package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +98 -0
  110. package/src/backend/filters/__tests__/ListEmptyState.test.tsx +53 -0
  111. package/src/backend/icons/lucideRegistry.generated.tsx +8 -0
  112. package/src/backend/injection/__tests__/useGuardedMutation.optimisticLock.test.tsx +118 -0
  113. package/src/backend/injection/useGuardedMutation.ts +16 -1
  114. package/src/backend/inputs/MarkdownField.tsx +34 -0
  115. package/src/backend/inputs/MdxEditorImpl.tsx +113 -0
  116. package/src/backend/inputs/SwitchableMarkdownInput.tsx +5 -54
  117. package/src/backend/markdown/__tests__/markdownToPlainText.test.ts +13 -0
  118. package/src/backend/markdown/markdownToPlainText.ts +41 -0
  119. package/src/backend/messages/EmailThreadsPanel.tsx +351 -0
  120. package/src/backend/messages/__tests__/mergeOptimisticEmailThreads.test.ts +126 -0
  121. package/src/backend/messages/index.ts +12 -0
  122. package/src/backend/messages/mergeOptimisticEmailThreads.ts +84 -0
  123. package/src/backend/notifications/NotificationDispatcher.ts +2 -0
  124. package/src/backend/notifications/useNotificationsPoll.ts +5 -0
  125. package/src/backend/notifications/useNotificationsSse.ts +6 -0
  126. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +35 -11
  127. package/src/backend/utils/__tests__/optimisticLock.test.ts +126 -0
  128. package/src/backend/utils/optimisticLock.ts +70 -0
  129. package/src/portal/hooks/useTenantContext.ts +7 -2
  130. package/src/primitives/__tests__/checkbox.test.tsx +28 -0
  131. package/src/primitives/alert.tsx +1 -1
  132. package/src/primitives/checkbox.tsx +2 -1
  133. package/src/types/css.d.ts +5 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/backend/RowActions.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { IconButton } from '../primitives/icon-button'\nimport { Button } from '../primitives/button'\n\nexport type RowActionItem = {\n id?: string\n label: string\n onSelect?: () => void\n href?: string\n destructive?: boolean\n}\n\nexport function RowActions({ items = [] }: { items?: RowActionItem[] }) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const btnRef = React.useRef<HTMLButtonElement>(null)\n const menuRef = React.useRef<HTMLDivElement>(null)\n const hoverTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)\n const [anchorRect, setAnchorRect] = React.useState<DOMRect | null>(null)\n const [direction, setDirection] = React.useState<'down' | 'up'>('down')\n\n const updatePosition = React.useCallback(() => {\n if (!btnRef.current) return\n const rect = btnRef.current.getBoundingClientRect()\n setAnchorRect(rect)\n // Decide whether to open up or down based on available viewport space\n const spaceBelow = window.innerHeight - rect.bottom\n const spaceAbove = rect.top\n setDirection(spaceBelow < 180 && spaceAbove > spaceBelow ? 'up' : 'down')\n }, [])\n\n React.useEffect(() => {\n if (!open) return\n updatePosition()\n function onDocClick(e: MouseEvent) {\n const t = e.target as Node\n if (menuRef.current && !menuRef.current.contains(t) && btnRef.current && !btnRef.current.contains(t)) {\n setOpen(false)\n }\n }\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n setOpen(false)\n btnRef.current?.focus()\n }\n }\n function onScrollOrResize() {\n updatePosition()\n }\n document.addEventListener('mousedown', onDocClick)\n document.addEventListener('keydown', onKey)\n window.addEventListener('scroll', onScrollOrResize, true)\n window.addEventListener('resize', onScrollOrResize)\n return () => {\n document.removeEventListener('mousedown', onDocClick)\n document.removeEventListener('keydown', onKey)\n window.removeEventListener('scroll', onScrollOrResize, true)\n window.removeEventListener('resize', onScrollOrResize)\n }\n }, [open, updatePosition])\n\n // Cleanup timeout on unmount\n React.useEffect(() => {\n return () => {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current)\n }\n }\n }, [])\n\n if (items.length === 0) return null\n\n const handlePointerEnter = (event: React.PointerEvent) => {\n if (event.pointerType === 'touch') return\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current)\n }\n setOpen(true)\n }\n\n const handlePointerLeave = (event: React.PointerEvent) => {\n if (event.pointerType === 'touch') return\n hoverTimeoutRef.current = setTimeout(() => {\n setOpen(false)\n }, 150)\n }\n\n return (\n <div\n className=\"relative inline-block text-left\"\n onPointerEnter={handlePointerEnter}\n onPointerLeave={handlePointerLeave}\n >\n <IconButton\n ref={btnRef}\n type=\"button\"\n variant=\"ghost\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={(e) => { e.stopPropagation(); setOpen((v) => !v); requestAnimationFrame(updatePosition) }}\n >\n <span aria-hidden=\"true\">\u22EF</span>\n <span className=\"sr-only\">{t('ui.rowActions.openActions', 'Open actions')}</span>\n </IconButton>\n {open && anchorRect && createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n className=\"fixed w-44 max-w-[calc(100vw-1rem)] rounded-md border bg-background p-1 shadow focus-visible:outline-none z-dropdown\"\n style={{\n top: direction === 'down' ? anchorRect.bottom + 8 : anchorRect.top - 8,\n left: Math.min(anchorRect.right, window.innerWidth - 8),\n transform: `translate(-100%, ${direction === 'down' ? '0' : '-100%'})`,\n }}\n onPointerEnter={handlePointerEnter}\n onPointerLeave={handlePointerLeave}\n >\n {items.map((it, idx) => (\n it.href ? (\n <a\n key={idx}\n href={it.href}\n className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-red-600' : ''}`}\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n }}\n >\n {it.label}\n </a>\n ) : (\n <Button\n key={idx}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-red-600' : ''}`}\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n it.onSelect?.()\n }}\n >\n {it.label}\n </Button>\n )\n ))}\n </div>,\n document.body\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { IconButton } from '../primitives/icon-button'\nimport { Button } from '../primitives/button'\n\nexport type RowActionItem = {\n id?: string\n label: string\n onSelect?: () => void\n href?: string\n destructive?: boolean\n}\n\nexport function RowActions({ items = [] }: { items?: RowActionItem[] }) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const btnRef = React.useRef<HTMLButtonElement>(null)\n const menuRef = React.useRef<HTMLDivElement>(null)\n const hoverTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)\n const [anchorRect, setAnchorRect] = React.useState<DOMRect | null>(null)\n const [direction, setDirection] = React.useState<'down' | 'up'>('down')\n\n const updatePosition = React.useCallback(() => {\n if (!btnRef.current) return\n const rect = btnRef.current.getBoundingClientRect()\n setAnchorRect(rect)\n // Decide whether to open up or down based on available viewport space\n const spaceBelow = window.innerHeight - rect.bottom\n const spaceAbove = rect.top\n setDirection(spaceBelow < 180 && spaceAbove > spaceBelow ? 'up' : 'down')\n }, [])\n\n React.useEffect(() => {\n if (!open) return\n updatePosition()\n function onDocClick(e: MouseEvent) {\n const t = e.target as Node\n if (menuRef.current && !menuRef.current.contains(t) && btnRef.current && !btnRef.current.contains(t)) {\n setOpen(false)\n }\n }\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n setOpen(false)\n btnRef.current?.focus()\n }\n }\n function onScrollOrResize() {\n updatePosition()\n }\n document.addEventListener('mousedown', onDocClick)\n document.addEventListener('keydown', onKey)\n window.addEventListener('scroll', onScrollOrResize, true)\n window.addEventListener('resize', onScrollOrResize)\n return () => {\n document.removeEventListener('mousedown', onDocClick)\n document.removeEventListener('keydown', onKey)\n window.removeEventListener('scroll', onScrollOrResize, true)\n window.removeEventListener('resize', onScrollOrResize)\n }\n }, [open, updatePosition])\n\n // Cleanup timeout on unmount\n React.useEffect(() => {\n return () => {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current)\n }\n }\n }, [])\n\n if (items.length === 0) return null\n\n const handlePointerEnter = (event: React.PointerEvent) => {\n if (event.pointerType === 'touch') return\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current)\n }\n setOpen(true)\n }\n\n const handlePointerLeave = (event: React.PointerEvent) => {\n if (event.pointerType === 'touch') return\n hoverTimeoutRef.current = setTimeout(() => {\n setOpen(false)\n }, 150)\n }\n\n return (\n <div\n className=\"relative inline-block text-left\"\n onPointerEnter={handlePointerEnter}\n onPointerLeave={handlePointerLeave}\n >\n <IconButton\n ref={btnRef}\n type=\"button\"\n variant=\"ghost\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={(e) => { e.stopPropagation(); setOpen(true); requestAnimationFrame(updatePosition) }}\n >\n <span aria-hidden=\"true\">\u22EF</span>\n <span className=\"sr-only\">{t('ui.rowActions.openActions', 'Open actions')}</span>\n </IconButton>\n {open && anchorRect && createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n className=\"fixed w-44 max-w-[calc(100vw-1rem)] rounded-md border bg-background p-1 shadow focus-visible:outline-none z-dropdown\"\n style={{\n top: direction === 'down' ? anchorRect.bottom + 8 : anchorRect.top - 8,\n left: Math.min(anchorRect.right, window.innerWidth - 8),\n transform: `translate(-100%, ${direction === 'down' ? '0' : '-100%'})`,\n }}\n onPointerEnter={handlePointerEnter}\n onPointerLeave={handlePointerLeave}\n >\n {items.map((it, idx) => (\n it.href ? (\n <a\n key={idx}\n href={it.href}\n className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-red-600' : ''}`}\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n }}\n >\n {it.label}\n </a>\n ) : (\n <Button\n key={idx}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-red-600' : ''}`}\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n it.onSelect?.()\n }}\n >\n {it.label}\n </Button>\n )\n ))}\n </div>,\n document.body\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["t"]
7
7
  }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { RefreshCw, X } from "lucide-react";
5
+ import { Button } from "../../primitives/button.js";
6
+ import { IconButton } from "../../primitives/icon-button.js";
7
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
8
+ import { dismissRecordConflict, useRecordConflict } from "./store.js";
9
+ function RecordConflictBanner() {
10
+ const t = useT();
11
+ const conflict = useRecordConflict();
12
+ const handleRefresh = React.useCallback(() => {
13
+ const onRefresh = conflict?.onRefresh ?? null;
14
+ dismissRecordConflict();
15
+ if (onRefresh) {
16
+ onRefresh();
17
+ return;
18
+ }
19
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
20
+ try {
21
+ const isJSDOM = typeof navigator !== "undefined" && typeof navigator.userAgent === "string" ? navigator.userAgent.toLowerCase().includes("jsdom") : false;
22
+ if (!isJSDOM) window.location.reload();
23
+ } catch {
24
+ }
25
+ }
26
+ }, [conflict]);
27
+ if (!conflict) return null;
28
+ return /* @__PURE__ */ jsxs(
29
+ "div",
30
+ {
31
+ role: "alert",
32
+ "data-testid": "record-conflict-banner",
33
+ className: "mb-4 flex items-center justify-between gap-3 rounded-md border border-status-error-border bg-status-error-bg pl-3 pr-2 py-2 text-sm text-status-error-text shadow-xs sm:pr-3",
34
+ children: [
35
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
36
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-status-error-text", children: conflict.title ?? t("ui.forms.conflict.title", "Record changed") }),
37
+ /* @__PURE__ */ jsx("span", { className: "ml-2 text-status-error-text", children: conflict.message })
38
+ ] }),
39
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
40
+ /* @__PURE__ */ jsxs(
41
+ Button,
42
+ {
43
+ type: "button",
44
+ variant: "outline",
45
+ size: "sm",
46
+ onClick: handleRefresh,
47
+ className: "border-status-error-border bg-status-error-bg text-status-error-text hover:bg-status-error-border hover:text-status-error-text px-2.5 sm:px-3",
48
+ children: [
49
+ /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1 size-4", "aria-hidden": "true" }),
50
+ t("ui.forms.conflict.refresh", "Refresh")
51
+ ]
52
+ }
53
+ ),
54
+ /* @__PURE__ */ jsx(
55
+ IconButton,
56
+ {
57
+ type: "button",
58
+ variant: "ghost",
59
+ size: "sm",
60
+ "aria-label": t("ui.forms.conflict.dismiss", "Dismiss"),
61
+ onClick: () => dismissRecordConflict(),
62
+ children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
63
+ }
64
+ )
65
+ ] })
66
+ ]
67
+ }
68
+ );
69
+ }
70
+ export {
71
+ RecordConflictBanner
72
+ };
73
+ //# sourceMappingURL=RecordConflictBanner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/conflicts/RecordConflictBanner.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { RefreshCw, X } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { dismissRecordConflict, useRecordConflict } from './store'\n\n/**\n * Persistent, error-styled bar that surfaces an optimistic-lock conflict\n * (\"this record was modified by someone else\"). Rendered once in `AppShell`\n * next to the undo `LastOperationBanner`, it is the unified, clearly-visible\n * surface for the conflict across every form (CrudForm, useGuardedMutation,\n * and custom pages all push through `conflicts/store`). Unlike the undo bar it\n * does NOT auto-dismiss \u2014 the user resolves it by refreshing or dismissing.\n */\nexport function RecordConflictBanner() {\n const t = useT()\n const conflict = useRecordConflict()\n\n const handleRefresh = React.useCallback(() => {\n const onRefresh = conflict?.onRefresh ?? null\n dismissRecordConflict()\n if (onRefresh) {\n onRefresh()\n return\n }\n if (typeof window !== 'undefined' && typeof window.location?.reload === 'function') {\n try {\n const isJSDOM = typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string'\n ? navigator.userAgent.toLowerCase().includes('jsdom')\n : false\n if (!isJSDOM) window.location.reload()\n } catch {\n // noop in non-browser / jsdom environments\n }\n }\n }, [conflict])\n\n if (!conflict) return null\n\n return (\n <div\n role=\"alert\"\n data-testid=\"record-conflict-banner\"\n className=\"mb-4 flex items-center justify-between gap-3 rounded-md border border-status-error-border bg-status-error-bg pl-3 pr-2 py-2 text-sm text-status-error-text shadow-xs sm:pr-3\"\n >\n <div className=\"min-w-0\">\n <span className=\"font-medium text-status-error-text\">\n {conflict.title ?? t('ui.forms.conflict.title', 'Record changed')}\n </span>\n <span className=\"ml-2 text-status-error-text\">{conflict.message}</span>\n </div>\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleRefresh}\n className=\"border-status-error-border bg-status-error-bg text-status-error-text hover:bg-status-error-border hover:text-status-error-text px-2.5 sm:px-3\"\n >\n <RefreshCw className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {t('ui.forms.conflict.refresh', 'Refresh')}\n </Button>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.forms.conflict.dismiss', 'Dismiss')}\n onClick={() => dismissRecordConflict()}\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": ";AA+CM,SACE,KADF;AA9CN,YAAY,WAAW;AACvB,SAAS,WAAW,SAAS;AAC7B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,uBAAuB,yBAAyB;AAUlD,SAAS,uBAAuB;AACrC,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,kBAAkB;AAEnC,QAAM,gBAAgB,MAAM,YAAY,MAAM;AAC5C,UAAM,YAAY,UAAU,aAAa;AACzC,0BAAsB;AACtB,QAAI,WAAW;AACb,gBAAU;AACV;AAAA,IACF;AACA,QAAI,OAAO,WAAW,eAAe,OAAO,OAAO,UAAU,WAAW,YAAY;AAClF,UAAI;AACF,cAAM,UAAU,OAAO,cAAc,eAAe,OAAO,UAAU,cAAc,WAC/E,UAAU,UAAU,YAAY,EAAE,SAAS,OAAO,IAClD;AACJ,YAAI,CAAC,QAAS,QAAO,SAAS,OAAO;AAAA,MACvC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,MAAI,CAAC,SAAU,QAAO;AAEtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,eAAY;AAAA,MACZ,WAAU;AAAA,MAEV;AAAA,6BAAC,SAAI,WAAU,WACb;AAAA,8BAAC,UAAK,WAAU,sCACb,mBAAS,SAAS,EAAE,2BAA2B,gBAAgB,GAClE;AAAA,UACA,oBAAC,UAAK,WAAU,+BAA+B,mBAAS,SAAQ;AAAA,WAClE;AAAA,QACA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cAEV;AAAA,oCAAC,aAAU,WAAU,eAAc,eAAY,QAAO;AAAA,gBACrD,EAAE,6BAA6B,SAAS;AAAA;AAAA;AAAA,UAC3C;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,cAAY,EAAE,6BAA6B,SAAS;AAAA,cACpD,SAAS,MAAM,sBAAsB;AAAA,cAErC,8BAAC,KAAE,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,UAC3C;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,32 @@
1
+ import { extractOptimisticLockConflict } from "../utils/optimisticLock.js";
2
+ import { showRecordConflict } from "./store.js";
3
+ import {
4
+ showRecordConflict as showRecordConflict2,
5
+ dismissRecordConflict,
6
+ useRecordConflict,
7
+ getRecordConflictForTest
8
+ } from "./store.js";
9
+ import { RecordConflictBanner } from "./RecordConflictBanner.js";
10
+ function surfaceRecordConflict(error, t, options = {}) {
11
+ const conflict = extractOptimisticLockConflict(error);
12
+ if (!conflict) return false;
13
+ showRecordConflict({
14
+ message: t(
15
+ "ui.forms.flash.recordModified",
16
+ "This record was modified by someone else. Refresh and try again."
17
+ ),
18
+ title: options.title ?? null,
19
+ currentUpdatedAt: conflict.currentUpdatedAt,
20
+ onRefresh: options.onRefresh ?? null
21
+ });
22
+ return true;
23
+ }
24
+ export {
25
+ RecordConflictBanner,
26
+ dismissRecordConflict,
27
+ getRecordConflictForTest,
28
+ showRecordConflict2 as showRecordConflict,
29
+ surfaceRecordConflict,
30
+ useRecordConflict
31
+ };
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/conflicts/index.ts"],
4
+ "sourcesContent": ["import { extractOptimisticLockConflict } from '../utils/optimisticLock'\nimport { showRecordConflict } from './store'\n\nexport {\n showRecordConflict,\n dismissRecordConflict,\n useRecordConflict,\n getRecordConflictForTest,\n type RecordConflictEntry,\n type ShowRecordConflictInput,\n} from './store'\nexport { RecordConflictBanner } from './RecordConflictBanner'\n\ntype Translate = (key: string, fallback?: string) => string\n\nexport type SurfaceRecordConflictOptions = {\n /** Custom refresh handler. Omit to let the banner reload the page. */\n onRefresh?: (() => void) | null\n /** Localized title override; the banner falls back to a generic title. */\n title?: string | null\n}\n\n/**\n * If `error` is an OSS optimistic-lock conflict (HTTP 409 with\n * `code: 'optimistic_lock_conflict'`), push the localized \"record modified\"\n * message onto the shared conflict bar and return `true`. Otherwise return\n * `false` so callers can fall back to their normal error handling.\n *\n * This is the single entry point every form uses, so the conflict surfaces the\n * same persistent, error-styled bar everywhere.\n */\nexport function surfaceRecordConflict(\n error: unknown,\n t: Translate,\n options: SurfaceRecordConflictOptions = {},\n): boolean {\n const conflict = extractOptimisticLockConflict(error)\n if (!conflict) return false\n showRecordConflict({\n message: t(\n 'ui.forms.flash.recordModified',\n 'This record was modified by someone else. Refresh and try again.',\n ),\n title: options.title ?? null,\n currentUpdatedAt: conflict.currentUpdatedAt,\n onRefresh: options.onRefresh ?? null,\n })\n return true\n}\n"],
5
+ "mappings": "AAAA,SAAS,qCAAqC;AAC9C,SAAS,0BAA0B;AAEnC;AAAA,EACE,sBAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,4BAA4B;AAoB9B,SAAS,sBACd,OACA,GACA,UAAwC,CAAC,GAChC;AACT,QAAM,WAAW,8BAA8B,KAAK;AACpD,MAAI,CAAC,SAAU,QAAO;AACtB,qBAAmB;AAAA,IACjB,SAAS;AAAA,MACP;AAAA,MACA;AAAA,IACF;AAAA,IACA,OAAO,QAAQ,SAAS;AAAA,IACxB,kBAAkB,SAAS;AAAA,IAC3B,WAAW,QAAQ,aAAa;AAAA,EAClC,CAAC;AACD,SAAO;AACT;",
6
+ "names": ["showRecordConflict"]
7
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ let internalEntry = null;
4
+ let sequence = 0;
5
+ const emitter = new EventTarget();
6
+ function emit() {
7
+ emitter.dispatchEvent(new Event("change"));
8
+ }
9
+ function subscribe(listener) {
10
+ const wrapped = () => listener();
11
+ emitter.addEventListener("change", wrapped);
12
+ return () => emitter.removeEventListener("change", wrapped);
13
+ }
14
+ function nextId() {
15
+ sequence += 1;
16
+ return `conflict:${sequence}`;
17
+ }
18
+ function showRecordConflict(input) {
19
+ internalEntry = {
20
+ id: nextId(),
21
+ message: input.message,
22
+ title: input.title ?? null,
23
+ currentUpdatedAt: input.currentUpdatedAt ?? null,
24
+ onRefresh: input.onRefresh ?? null,
25
+ createdAt: typeof Date !== "undefined" ? Date.now() : 0
26
+ };
27
+ emit();
28
+ }
29
+ function dismissRecordConflict() {
30
+ if (internalEntry === null) return;
31
+ internalEntry = null;
32
+ emit();
33
+ }
34
+ function useRecordConflict() {
35
+ return React.useSyncExternalStore(
36
+ subscribe,
37
+ () => internalEntry,
38
+ () => null
39
+ );
40
+ }
41
+ function getRecordConflictForTest() {
42
+ return internalEntry;
43
+ }
44
+ export {
45
+ dismissRecordConflict,
46
+ getRecordConflictForTest,
47
+ showRecordConflict,
48
+ useRecordConflict
49
+ };
50
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/conflicts/store.ts"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\n\n/**\n * In-memory store for the unified record-conflict bar.\n *\n * When a mutation fails with an OSS optimistic-lock conflict (HTTP 409 with\n * `code: 'optimistic_lock_conflict'`), forms surface it through this store so a\n * persistent, error-styled bar \u2014 the `RecordConflictBanner` rendered once in\n * `AppShell` \u2014 shows the \"this record was modified\" message app-wide, instead\n * of a transient toast that scrolls away. Mirrors the undo `LastOperationBanner`\n * pattern but for the error case, and is shared by `CrudForm`,\n * `useGuardedMutation`, and any custom page, so every form behaves the same.\n *\n * Unlike the operations store this is NOT persisted to localStorage \u2014 it holds\n * a live `onRefresh` callback and a conflict only matters for the current page\n * session.\n */\nexport type RecordConflictEntry = {\n id: string\n /** Localized, user-facing message (e.g. ui.forms.flash.recordModified). */\n message: string\n /** Optional localized title; the banner falls back to a generic title. */\n title?: string | null\n /** The server's current `updated_at`, for diagnostics / future diffing. */\n currentUpdatedAt?: string | null\n /**\n * Invoked when the user clicks \"Refresh\". When omitted the banner reloads\n * the page (the safe default \u2014 re-fetches the latest server state).\n */\n onRefresh?: (() => void) | null\n createdAt: number\n}\n\nlet internalEntry: RecordConflictEntry | null = null\nlet sequence = 0\n\nconst emitter = new EventTarget()\n\nfunction emit(): void {\n emitter.dispatchEvent(new Event('change'))\n}\n\nfunction subscribe(listener: () => void): () => void {\n const wrapped = () => listener()\n emitter.addEventListener('change', wrapped)\n return () => emitter.removeEventListener('change', wrapped)\n}\n\nfunction nextId(): string {\n sequence += 1\n return `conflict:${sequence}`\n}\n\nexport type ShowRecordConflictInput = {\n message: string\n title?: string | null\n currentUpdatedAt?: string | null\n onRefresh?: (() => void) | null\n}\n\n/** Show (or replace) the active record-conflict bar. */\nexport function showRecordConflict(input: ShowRecordConflictInput): void {\n internalEntry = {\n id: nextId(),\n message: input.message,\n title: input.title ?? null,\n currentUpdatedAt: input.currentUpdatedAt ?? null,\n onRefresh: input.onRefresh ?? null,\n createdAt: typeof Date !== 'undefined' ? Date.now() : 0,\n }\n emit()\n}\n\n/** Clear the active record-conflict bar. */\nexport function dismissRecordConflict(): void {\n if (internalEntry === null) return\n internalEntry = null\n emit()\n}\n\n/** Read the active conflict (null when none). Subscribes to changes. */\nexport function useRecordConflict(): RecordConflictEntry | null {\n return React.useSyncExternalStore(\n subscribe,\n () => internalEntry,\n () => null,\n )\n}\n\n/** Test-only accessor for the current entry without React. */\nexport function getRecordConflictForTest(): RecordConflictEntry | null {\n return internalEntry\n}\n"],
5
+ "mappings": ";AACA,YAAY,WAAW;AAiCvB,IAAI,gBAA4C;AAChD,IAAI,WAAW;AAEf,MAAM,UAAU,IAAI,YAAY;AAEhC,SAAS,OAAa;AACpB,UAAQ,cAAc,IAAI,MAAM,QAAQ,CAAC;AAC3C;AAEA,SAAS,UAAU,UAAkC;AACnD,QAAM,UAAU,MAAM,SAAS;AAC/B,UAAQ,iBAAiB,UAAU,OAAO;AAC1C,SAAO,MAAM,QAAQ,oBAAoB,UAAU,OAAO;AAC5D;AAEA,SAAS,SAAiB;AACxB,cAAY;AACZ,SAAO,YAAY,QAAQ;AAC7B;AAUO,SAAS,mBAAmB,OAAsC;AACvE,kBAAgB;AAAA,IACd,IAAI,OAAO;AAAA,IACX,SAAS,MAAM;AAAA,IACf,OAAO,MAAM,SAAS;AAAA,IACtB,kBAAkB,MAAM,oBAAoB;AAAA,IAC5C,WAAW,MAAM,aAAa;AAAA,IAC9B,WAAW,OAAO,SAAS,cAAc,KAAK,IAAI,IAAI;AAAA,EACxD;AACA,OAAK;AACP;AAGO,SAAS,wBAA8B;AAC5C,MAAI,kBAAkB,KAAM;AAC5B,kBAAgB;AAChB,OAAK;AACP;AAGO,SAAS,oBAAgD;AAC9D,SAAO,MAAM;AAAA,IACX;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACF;AAGO,SAAS,2BAAuD;AACrE,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -226,6 +226,7 @@ function ActivityForm({
226
226
  allowInlineCreate: true,
227
227
  appearanceLabels,
228
228
  selectClassName: "w-full",
229
+ sortOptions: "none",
229
230
  manageHref
230
231
  }
231
232
  )