@open-mercato/ui 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435

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 (93) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/ai/parts/MutationResultCard.js +1 -14
  3. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  4. package/dist/backend/CrudForm.js +52 -96
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/DataTable.js +14 -40
  7. package/dist/backend/DataTable.js.map +2 -2
  8. package/dist/backend/FlashMessages.js +1 -25
  9. package/dist/backend/FlashMessages.js.map +2 -2
  10. package/dist/backend/columns/ColumnChooserPanel.js +3 -4
  11. package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
  12. package/dist/backend/confirm-dialog/ConfirmDialog.js +0 -11
  13. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  14. package/dist/backend/detail/InlineEditors.js +6 -17
  15. package/dist/backend/detail/InlineEditors.js.map +2 -2
  16. package/dist/backend/filters/FilterFieldPicker.js +2 -3
  17. package/dist/backend/filters/FilterFieldPicker.js.map +2 -2
  18. package/dist/backend/forms/ActionsDropdown.js +2 -52
  19. package/dist/backend/forms/ActionsDropdown.js.map +2 -2
  20. package/dist/backend/inputs/PhoneNumberField.js +5 -17
  21. package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
  22. package/dist/backend/inputs/TagsInput.js +1 -4
  23. package/dist/backend/inputs/TagsInput.js.map +2 -2
  24. package/dist/backend/inputs/index.js +0 -2
  25. package/dist/backend/inputs/index.js.map +2 -2
  26. package/dist/backend/messages/useMessageCompose.js +10 -29
  27. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  28. package/dist/backend/notifications/NotificationDispatcher.js +0 -5
  29. package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
  30. package/dist/backend/notifications/useNotificationActions.js +2 -13
  31. package/dist/backend/notifications/useNotificationActions.js.map +2 -2
  32. package/dist/backend/utils/api.js +4 -25
  33. package/dist/backend/utils/api.js.map +2 -2
  34. package/dist/backend/version-history/VersionHistoryPanel.js +2 -18
  35. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  36. package/dist/theme/QueryProvider.js +1 -7
  37. package/dist/theme/QueryProvider.js.map +2 -2
  38. package/package.json +4 -4
  39. package/src/ai/parts/MutationResultCard.tsx +1 -30
  40. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +0 -48
  41. package/src/backend/CrudForm.tsx +60 -121
  42. package/src/backend/DataTable.tsx +14 -54
  43. package/src/backend/FlashMessages.tsx +1 -34
  44. package/src/backend/__tests__/ActionsDropdown.test.tsx +0 -105
  45. package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +4 -85
  46. package/src/backend/__tests__/FlashMessages.test.tsx +0 -65
  47. package/src/backend/__tests__/VersionHistoryPanel.test.tsx +1 -81
  48. package/src/backend/columns/ColumnChooserPanel.tsx +3 -4
  49. package/src/backend/confirm-dialog/ConfirmDialog.tsx +0 -16
  50. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +0 -29
  51. package/src/backend/detail/InlineEditors.tsx +6 -28
  52. package/src/backend/detail/__tests__/InlineEditors.test.tsx +6 -121
  53. package/src/backend/filters/FilterFieldPicker.tsx +2 -3
  54. package/src/backend/forms/ActionsDropdown.tsx +2 -71
  55. package/src/backend/inputs/PhoneNumberField.tsx +11 -17
  56. package/src/backend/inputs/TagsInput.tsx +0 -4
  57. package/src/backend/inputs/__tests__/TagsInput.test.tsx +0 -41
  58. package/src/backend/inputs/index.ts +0 -2
  59. package/src/backend/messages/__tests__/useMessageCompose.test.tsx +1 -106
  60. package/src/backend/messages/message-composer.types.ts +0 -2
  61. package/src/backend/messages/useMessageCompose.ts +13 -34
  62. package/src/backend/notifications/NotificationDispatcher.ts +0 -5
  63. package/src/backend/notifications/__tests__/useNotificationActions.test.tsx +1 -86
  64. package/src/backend/notifications/useNotificationActions.ts +4 -19
  65. package/src/backend/utils/__tests__/api.test.ts +0 -67
  66. package/src/backend/utils/api.ts +4 -33
  67. package/src/backend/version-history/VersionHistoryPanel.tsx +2 -24
  68. package/src/theme/QueryProvider.tsx +1 -7
  69. package/dist/backend/crud/injectedFieldGroups.js +0 -50
  70. package/dist/backend/crud/injectedFieldGroups.js.map +0 -7
  71. package/dist/backend/inputs/LookupPickers.js +0 -149
  72. package/dist/backend/inputs/LookupPickers.js.map +0 -7
  73. package/dist/backend/inputs/lookupSources.js +0 -84
  74. package/dist/backend/inputs/lookupSources.js.map +0 -7
  75. package/dist/backend/utils/groupLabels.js +0 -16
  76. package/dist/backend/utils/groupLabels.js.map +0 -7
  77. package/src/backend/__tests__/CrudForm.disabledFields.test.tsx +0 -57
  78. package/src/backend/__tests__/CrudForm.embeddedExtraActions.test.tsx +0 -114
  79. package/src/backend/__tests__/CrudForm.fieldChangeRenders.test.tsx +0 -127
  80. package/src/backend/__tests__/DataTable.injectedColumnPlacement.test.tsx +0 -146
  81. package/src/backend/__tests__/next-dynamic-specifier.test.ts +0 -23
  82. package/src/backend/columns/__tests__/ColumnChooserPanel.groupLabels.test.tsx +0 -72
  83. package/src/backend/crud/__tests__/injectedFieldGroups.test.ts +0 -90
  84. package/src/backend/crud/injectedFieldGroups.ts +0 -75
  85. package/src/backend/filters/__tests__/FilterFieldPicker.groupLabels.test.tsx +0 -77
  86. package/src/backend/inputs/LookupPickers.tsx +0 -181
  87. package/src/backend/inputs/__tests__/LookupPickers.test.tsx +0 -120
  88. package/src/backend/inputs/__tests__/PhoneNumberField.renders.test.tsx +0 -35
  89. package/src/backend/inputs/__tests__/lookupSources.test.ts +0 -87
  90. package/src/backend/inputs/lookupSources.ts +0 -114
  91. package/src/backend/notifications/__tests__/NotificationDispatcher.navigate.test.ts +0 -76
  92. package/src/backend/utils/groupLabels.ts +0 -26
  93. package/src/theme/__tests__/QueryProvider.i18n.test.tsx +0 -40
@@ -24,26 +24,8 @@ function isSameOriginFlashNavigation() {
24
24
  return false;
25
25
  }
26
26
  }
27
- const PENDING_FLASH_MAX_AGE_MS = 5e3;
28
- let listeningFlashHostCount = 0;
29
- let pendingFlash = null;
30
- function readPendingFlash() {
31
- const pending = pendingFlash;
32
- if (!pending) return null;
33
- if (Date.now() - pending.queuedAt > PENDING_FLASH_MAX_AGE_MS) {
34
- pendingFlash = null;
35
- return null;
36
- }
37
- setTimeout(() => {
38
- if (pendingFlash === pending) pendingFlash = null;
39
- }, 0);
40
- return pending;
41
- }
42
27
  function flash(message, type = "info") {
43
28
  if (typeof window === "undefined") return;
44
- if (listeningFlashHostCount === 0) {
45
- pendingFlash = { message, type, queuedAt: Date.now() };
46
- }
47
29
  const evt = new CustomEvent("flash", { detail: { message, type } });
48
30
  window.dispatchEvent(evt);
49
31
  }
@@ -188,13 +170,7 @@ function FlashMessagesInner() {
188
170
  showFlash(text, t);
189
171
  };
190
172
  window.addEventListener("flash", handler);
191
- listeningFlashHostCount += 1;
192
- const pending = readPendingFlash();
193
- if (pending) showFlash(pending.message, pending.type);
194
- return () => {
195
- listeningFlashHostCount -= 1;
196
- window.removeEventListener("flash", handler);
197
- };
173
+ return () => window.removeEventListener("flash", handler);
198
174
  }, [showFlash]);
199
175
  const handleDismiss = React.useCallback(() => {
200
176
  clearDismissTimer();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/backend/FlashMessages.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Alert, type AlertStatus } from '../primitives/alert'\n\nexport type FlashKind = 'success' | 'error' | 'warning' | 'info'\n\nconst flashKindToAlertStatus: Record<FlashKind, AlertStatus> = {\n success: 'success',\n error: 'error',\n warning: 'warning',\n info: 'information',\n}\n\nfunction normalizeFlashKind(value: string | null | undefined): FlashKind {\n if (value && Object.prototype.hasOwnProperty.call(flashKindToAlertStatus, value)) {\n return value as FlashKind\n }\n return 'success'\n}\n\n// A URL-supplied flash is only trustworthy when the navigation that carried it\n// originated from the same origin (an in-app POST->GET redirect or client-side\n// navigation). Cross-origin links \u2014 the phishing/content-spoofing vector \u2014 must\n// not be allowed to render attacker-controlled copy inside an authoritative banner.\nfunction isSameOriginFlashNavigation(): boolean {\n if (typeof window === 'undefined') return false\n const referrer = document.referrer\n // No referrer (direct load, bookmark, or referrer stripped) is ambiguous, not a\n // cross-origin redirect we can attribute to an attacker page; allow it.\n if (!referrer) return true\n try {\n return new URL(referrer).origin === window.location.origin\n } catch {\n return false\n }\n}\n\n// A page that flashes from a mount effect (e.g. reading an OAuth callback's\n// result params) can run before any `<FlashMessages />` host has attached its\n// listener: on a full page load hydration is split across Suspense boundaries,\n// so the page's effects may commit ahead of the hosts' and the event would be\n// dropped. Keep the latest undelivered message briefly and let the hosts that\n// start listening in the same commit show it, so the primary-host election\n// stays in sync.\nconst PENDING_FLASH_MAX_AGE_MS = 5000\nlet listeningFlashHostCount = 0\nlet pendingFlash: { message: string; type: FlashKind; queuedAt: number } | null = null\n\nfunction readPendingFlash(): { message: string; type: FlashKind } | null {\n const pending = pendingFlash\n if (!pending) return null\n if (Date.now() - pending.queuedAt > PENDING_FLASH_MAX_AGE_MS) {\n pendingFlash = null\n return null\n }\n setTimeout(() => {\n if (pendingFlash === pending) pendingFlash = null\n }, 0)\n return pending\n}\n\n// Programmatic API to show a flash message without navigation.\n// Consumers can import { flash } and call flash('text', 'error').\nexport function flash(message: string, type: FlashKind = 'info') {\n if (typeof window === 'undefined') return\n if (listeningFlashHostCount === 0) {\n pendingFlash = { message, type, queuedAt: Date.now() }\n }\n const evt = new CustomEvent('flash', { detail: { message, type } })\n window.dispatchEvent(evt)\n}\n\n// Two hosts mount `<FlashMessages />` over the same page: `FrontendLayout`\n// wraps the entire app (backend routes included, via `AppProviders`) and\n// `AppShell` mounts its own copy inside `<main>`. Both listen to the same\n// `flash` event, so every backend toast rendered twice \u2014 two stacked fixed\n// overlays, each with its own dismiss button, so dismissing one left the other\n// behind. Both mount points are public surfaces third-party apps rely on, so\n// instead of deleting one, only the first host to mount renders; the others\n// keep their state in sync and take over if it unmounts.\nconst mountedFlashHostIds: symbol[] = []\nconst flashHostListeners = new Set<() => void>()\n\nfunction notifyFlashHostListeners() {\n for (const listener of flashHostListeners) listener()\n}\n\nfunction subscribeFlashHosts(listener: () => void): () => void {\n flashHostListeners.add(listener)\n return () => {\n flashHostListeners.delete(listener)\n }\n}\n\nfunction getPrimaryFlashHostId(): symbol | null {\n return mountedFlashHostIds[0] ?? null\n}\n\nfunction getServerFlashHostId(): symbol | null {\n return null\n}\n\nfunction useIsPrimaryFlashHost(): boolean {\n const hostIdRef = React.useRef<symbol | null>(null)\n if (hostIdRef.current === null) hostIdRef.current = Symbol('om-flash-host')\n const hostId = hostIdRef.current\n\n React.useEffect(() => {\n mountedFlashHostIds.push(hostId)\n notifyFlashHostListeners()\n return () => {\n const index = mountedFlashHostIds.indexOf(hostId)\n if (index >= 0) mountedFlashHostIds.splice(index, 1)\n notifyFlashHostListeners()\n }\n }, [hostId])\n\n return React.useSyncExternalStore(subscribeFlashHosts, getPrimaryFlashHostId, getServerFlashHostId) === hostId\n}\n\ntype HistoryMethod = History['pushState']\n\nfunction useLocationKey() {\n const [locationKey, setLocationKey] = React.useState(() => {\n if (typeof window === 'undefined') return ''\n return window.location.href\n })\n const locationKeyRef = React.useRef(locationKey)\n\n React.useEffect(() => {\n locationKeyRef.current = locationKey\n }, [locationKey])\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n let active = true\n const scheduleUpdate = (href: string) => {\n const run = () => {\n if (!active) return\n if (locationKeyRef.current === href) return\n locationKeyRef.current = href\n setLocationKey(href)\n }\n if (typeof queueMicrotask === 'function') {\n queueMicrotask(run)\n } else {\n setTimeout(run, 0)\n }\n }\n const updateLocation = () => {\n if (!active) return\n const href = window.location.href\n if (href === locationKeyRef.current) return\n scheduleUpdate(href)\n }\n\n const deferredUpdateLocation = () => {\n setTimeout(updateLocation, 0)\n }\n\n const originalPush: HistoryMethod = window.history.pushState.bind(window.history)\n const originalReplace: HistoryMethod = window.history.replaceState.bind(window.history)\n\n const pushState: HistoryMethod = (...args) => {\n originalPush(...args)\n deferredUpdateLocation()\n }\n\n const replaceState: HistoryMethod = (...args) => {\n originalReplace(...args)\n deferredUpdateLocation()\n }\n\n window.history.pushState = pushState\n window.history.replaceState = replaceState\n window.addEventListener('popstate', updateLocation)\n window.addEventListener('hashchange', updateLocation)\n updateLocation()\n\n return () => {\n active = false\n window.history.pushState = originalPush\n window.history.replaceState = originalReplace\n window.removeEventListener('popstate', updateLocation)\n window.removeEventListener('hashchange', updateLocation)\n }\n }, [])\n\n return locationKey\n}\n\nfunction FlashMessagesInner() {\n const [msg, setMsg] = React.useState<string | null>(null)\n const [kind, setKind] = React.useState<FlashKind>('info')\n const isPrimaryHost = useIsPrimaryFlashHost()\n const locationKey = useLocationKey()\n const dismissTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const clearDismissTimer = React.useCallback(() => {\n if (dismissTimerRef.current) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n }, [])\n\n const showFlash = React.useCallback((message: string, type: FlashKind) => {\n clearDismissTimer()\n setMsg(message)\n setKind(type)\n dismissTimerRef.current = setTimeout(() => {\n dismissTimerRef.current = null\n setMsg(null)\n }, 3000)\n }, [clearDismissTimer])\n\n React.useEffect(() => {\n return () => {\n clearDismissTimer()\n }\n }, [clearDismissTimer])\n\n // Read flash from URL on any navigation change (client-side too)\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const url = new URL(window.location.href)\n const message = url.searchParams.get('flash')\n const type = normalizeFlashKind(url.searchParams.get('type'))\n if (message) {\n // Always strip the params so a spoofed link does not linger in history,\n // but only render the banner for same-origin navigations.\n const trusted = isSameOriginFlashNavigation()\n url.searchParams.delete('flash')\n url.searchParams.delete('type')\n window.history.replaceState({}, '', url.toString())\n if (trusted) {\n showFlash(message, type)\n }\n }\n }, [locationKey, showFlash])\n\n // Listen for programmatic flash events\n React.useEffect(() => {\n const handler = (e: Event) => {\n const ce = e as CustomEvent<{ message?: string; type?: FlashKind }>\n const text = ce.detail?.message\n const t = ce.detail?.type || 'info'\n if (!text) return\n showFlash(text, t)\n }\n window.addEventListener('flash', handler as EventListener)\n listeningFlashHostCount += 1\n const pending = readPendingFlash()\n if (pending) showFlash(pending.message, pending.type)\n return () => {\n listeningFlashHostCount -= 1\n window.removeEventListener('flash', handler as EventListener)\n }\n }, [showFlash])\n\n const handleDismiss = React.useCallback(() => {\n clearDismissTimer()\n setMsg(null)\n }, [clearDismissTimer])\n\n if (!msg || !isPrimaryHost) return null\n\n return (\n <div className=\"pointer-events-none fixed left-3 right-3 top-3 z-toast sm:left-auto sm:right-4 sm:w-[380px]\">\n <div className=\"pointer-events-auto\">\n <Alert\n status={flashKindToAlertStatus[kind]}\n size=\"sm\"\n dismissible\n onDismiss={handleDismiss}\n className=\"shadow-md\"\n >\n {msg}\n </Alert>\n </div>\n </div>\n )\n}\n\nexport function FlashMessages() {\n return (\n <React.Suspense fallback={null}>\n <FlashMessagesInner />\n </React.Suspense>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Alert, type AlertStatus } from '../primitives/alert'\n\nexport type FlashKind = 'success' | 'error' | 'warning' | 'info'\n\nconst flashKindToAlertStatus: Record<FlashKind, AlertStatus> = {\n success: 'success',\n error: 'error',\n warning: 'warning',\n info: 'information',\n}\n\nfunction normalizeFlashKind(value: string | null | undefined): FlashKind {\n if (value && Object.prototype.hasOwnProperty.call(flashKindToAlertStatus, value)) {\n return value as FlashKind\n }\n return 'success'\n}\n\n// A URL-supplied flash is only trustworthy when the navigation that carried it\n// originated from the same origin (an in-app POST->GET redirect or client-side\n// navigation). Cross-origin links \u2014 the phishing/content-spoofing vector \u2014 must\n// not be allowed to render attacker-controlled copy inside an authoritative banner.\nfunction isSameOriginFlashNavigation(): boolean {\n if (typeof window === 'undefined') return false\n const referrer = document.referrer\n // No referrer (direct load, bookmark, or referrer stripped) is ambiguous, not a\n // cross-origin redirect we can attribute to an attacker page; allow it.\n if (!referrer) return true\n try {\n return new URL(referrer).origin === window.location.origin\n } catch {\n return false\n }\n}\n\n// Programmatic API to show a flash message without navigation.\n// Consumers can import { flash } and call flash('text', 'error').\nexport function flash(message: string, type: FlashKind = 'info') {\n if (typeof window === 'undefined') return\n const evt = new CustomEvent('flash', { detail: { message, type } })\n window.dispatchEvent(evt)\n}\n\n// Two hosts mount `<FlashMessages />` over the same page: `FrontendLayout`\n// wraps the entire app (backend routes included, via `AppProviders`) and\n// `AppShell` mounts its own copy inside `<main>`. Both listen to the same\n// `flash` event, so every backend toast rendered twice \u2014 two stacked fixed\n// overlays, each with its own dismiss button, so dismissing one left the other\n// behind. Both mount points are public surfaces third-party apps rely on, so\n// instead of deleting one, only the first host to mount renders; the others\n// keep their state in sync and take over if it unmounts.\nconst mountedFlashHostIds: symbol[] = []\nconst flashHostListeners = new Set<() => void>()\n\nfunction notifyFlashHostListeners() {\n for (const listener of flashHostListeners) listener()\n}\n\nfunction subscribeFlashHosts(listener: () => void): () => void {\n flashHostListeners.add(listener)\n return () => {\n flashHostListeners.delete(listener)\n }\n}\n\nfunction getPrimaryFlashHostId(): symbol | null {\n return mountedFlashHostIds[0] ?? null\n}\n\nfunction getServerFlashHostId(): symbol | null {\n return null\n}\n\nfunction useIsPrimaryFlashHost(): boolean {\n const hostIdRef = React.useRef<symbol | null>(null)\n if (hostIdRef.current === null) hostIdRef.current = Symbol('om-flash-host')\n const hostId = hostIdRef.current\n\n React.useEffect(() => {\n mountedFlashHostIds.push(hostId)\n notifyFlashHostListeners()\n return () => {\n const index = mountedFlashHostIds.indexOf(hostId)\n if (index >= 0) mountedFlashHostIds.splice(index, 1)\n notifyFlashHostListeners()\n }\n }, [hostId])\n\n return React.useSyncExternalStore(subscribeFlashHosts, getPrimaryFlashHostId, getServerFlashHostId) === hostId\n}\n\ntype HistoryMethod = History['pushState']\n\nfunction useLocationKey() {\n const [locationKey, setLocationKey] = React.useState(() => {\n if (typeof window === 'undefined') return ''\n return window.location.href\n })\n const locationKeyRef = React.useRef(locationKey)\n\n React.useEffect(() => {\n locationKeyRef.current = locationKey\n }, [locationKey])\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n let active = true\n const scheduleUpdate = (href: string) => {\n const run = () => {\n if (!active) return\n if (locationKeyRef.current === href) return\n locationKeyRef.current = href\n setLocationKey(href)\n }\n if (typeof queueMicrotask === 'function') {\n queueMicrotask(run)\n } else {\n setTimeout(run, 0)\n }\n }\n const updateLocation = () => {\n if (!active) return\n const href = window.location.href\n if (href === locationKeyRef.current) return\n scheduleUpdate(href)\n }\n\n const deferredUpdateLocation = () => {\n setTimeout(updateLocation, 0)\n }\n\n const originalPush: HistoryMethod = window.history.pushState.bind(window.history)\n const originalReplace: HistoryMethod = window.history.replaceState.bind(window.history)\n\n const pushState: HistoryMethod = (...args) => {\n originalPush(...args)\n deferredUpdateLocation()\n }\n\n const replaceState: HistoryMethod = (...args) => {\n originalReplace(...args)\n deferredUpdateLocation()\n }\n\n window.history.pushState = pushState\n window.history.replaceState = replaceState\n window.addEventListener('popstate', updateLocation)\n window.addEventListener('hashchange', updateLocation)\n updateLocation()\n\n return () => {\n active = false\n window.history.pushState = originalPush\n window.history.replaceState = originalReplace\n window.removeEventListener('popstate', updateLocation)\n window.removeEventListener('hashchange', updateLocation)\n }\n }, [])\n\n return locationKey\n}\n\nfunction FlashMessagesInner() {\n const [msg, setMsg] = React.useState<string | null>(null)\n const [kind, setKind] = React.useState<FlashKind>('info')\n const isPrimaryHost = useIsPrimaryFlashHost()\n const locationKey = useLocationKey()\n const dismissTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const clearDismissTimer = React.useCallback(() => {\n if (dismissTimerRef.current) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n }, [])\n\n const showFlash = React.useCallback((message: string, type: FlashKind) => {\n clearDismissTimer()\n setMsg(message)\n setKind(type)\n dismissTimerRef.current = setTimeout(() => {\n dismissTimerRef.current = null\n setMsg(null)\n }, 3000)\n }, [clearDismissTimer])\n\n React.useEffect(() => {\n return () => {\n clearDismissTimer()\n }\n }, [clearDismissTimer])\n\n // Read flash from URL on any navigation change (client-side too)\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const url = new URL(window.location.href)\n const message = url.searchParams.get('flash')\n const type = normalizeFlashKind(url.searchParams.get('type'))\n if (message) {\n // Always strip the params so a spoofed link does not linger in history,\n // but only render the banner for same-origin navigations.\n const trusted = isSameOriginFlashNavigation()\n url.searchParams.delete('flash')\n url.searchParams.delete('type')\n window.history.replaceState({}, '', url.toString())\n if (trusted) {\n showFlash(message, type)\n }\n }\n }, [locationKey, showFlash])\n\n // Listen for programmatic flash events\n React.useEffect(() => {\n const handler = (e: Event) => {\n const ce = e as CustomEvent<{ message?: string; type?: FlashKind }>\n const text = ce.detail?.message\n const t = ce.detail?.type || 'info'\n if (!text) return\n showFlash(text, t)\n }\n window.addEventListener('flash', handler as EventListener)\n return () => window.removeEventListener('flash', handler as EventListener)\n }, [showFlash])\n\n const handleDismiss = React.useCallback(() => {\n clearDismissTimer()\n setMsg(null)\n }, [clearDismissTimer])\n\n if (!msg || !isPrimaryHost) return null\n\n return (\n <div className=\"pointer-events-none fixed left-3 right-3 top-3 z-toast sm:left-auto sm:right-4 sm:w-[380px]\">\n <div className=\"pointer-events-auto\">\n <Alert\n status={flashKindToAlertStatus[kind]}\n size=\"sm\"\n dismissible\n onDismiss={handleDismiss}\n className=\"shadow-md\"\n >\n {msg}\n </Alert>\n </div>\n </div>\n )\n}\n\nexport function FlashMessages() {\n return (\n <React.Suspense fallback={null}>\n <FlashMessagesInner />\n </React.Suspense>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,7 +7,6 @@ import { IconButton } from "../../primitives/icon-button.js";
7
7
  import { Switch } from "../../primitives/switch.js";
8
8
  import { SearchInput } from "../../primitives/search-input.js";
9
9
  import { useT } from "@open-mercato/shared/lib/i18n/context";
10
- import { UNGROUPED_GROUP_ID, resolveGroupLabel } from "../utils/groupLabels.js";
11
10
  import {
12
11
  DndContext,
13
12
  closestCenter,
@@ -98,13 +97,13 @@ function ColumnChooserSection({
98
97
  const filtered = availableColumns.filter((c) => !visibleSet.has(c.key)).filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery));
99
98
  const groups = /* @__PURE__ */ new Map();
100
99
  for (const col of filtered) {
101
- const group = col.group || UNGROUPED_GROUP_ID;
100
+ const group = col.group || t("ui.columnChooser.ungrouped", "Other");
102
101
  const list = groups.get(group) ?? [];
103
102
  list.push(col);
104
103
  groups.set(group, list);
105
104
  }
106
105
  return groups;
107
- }, [availableColumns, searchQuery, visibleSet]);
106
+ }, [availableColumns, searchQuery, visibleSet, t]);
108
107
  const toggleGroup = React.useCallback((group) => {
109
108
  setExpandedGroups((prev) => {
110
109
  const next = new Set(prev);
@@ -179,7 +178,7 @@ function ColumnChooserSection({
179
178
  onClick: () => toggleGroup(group),
180
179
  children: [
181
180
  /* @__PURE__ */ jsx(ChevronRight, { className: `size-4 transition-transform ${isCollapsed ? "" : "rotate-90"}` }),
182
- /* @__PURE__ */ jsx("span", { children: resolveGroupLabel(t, group, "ui.columnChooser.ungrouped", "Other") })
181
+ /* @__PURE__ */ jsx("span", { children: group })
183
182
  ]
184
183
  }
185
184
  ),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/columns/ColumnChooserPanel.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { GripVertical, X, ChevronRight } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Switch } from '../../primitives/switch'\nimport { Input } from '../../primitives/input'\nimport { SearchInput } from '../../primitives/search-input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { UNGROUPED_GROUP_ID, resolveGroupLabel } from '../utils/groupLabels'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\n\nexport type ColumnChooserField = {\n key: string\n label: string\n group: string\n defaultVisible?: boolean\n alwaysVisible?: boolean\n}\n\nexport type ColumnChooserSectionProps = {\n availableColumns: ColumnChooserField[]\n visibleColumnKeys: string[]\n columnOrder: string[]\n onToggleColumn: (key: string) => void\n onReorderColumns: (newOrder: string[]) => void\n dndContextId?: string\n}\n\nexport type ColumnChooserPanelProps = ColumnChooserSectionProps & {\n open: boolean\n onOpenChange: (open: boolean) => void\n}\n\nfunction SortableColumnItem({\n column,\n onToggle,\n}: {\n column: ColumnChooserField\n onToggle: (key: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: column.key })\n\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n className=\"flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50\"\n >\n <span className=\"cursor-grab text-muted-foreground\" {...attributes} {...listeners}>\n <GripVertical className=\"size-4\" />\n </span>\n <span className=\"truncate flex-1 min-w-0\">{column.label}</span>\n <Switch\n checked\n disabled={column.alwaysVisible}\n onCheckedChange={() => onToggle(column.key)}\n className=\"shrink-0 scale-90\"\n />\n </div>\n )\n}\n\nexport function ColumnChooserSection({\n availableColumns,\n visibleColumnKeys,\n columnOrder,\n onToggleColumn,\n onReorderColumns,\n dndContextId = 'column-chooser',\n}: ColumnChooserSectionProps) {\n const t = useT()\n const [searchQuery, setSearchQuery] = React.useState('')\n const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set())\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const visibleSet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys])\n\n const selectedColumns = React.useMemo(() => {\n const ordered: ColumnChooserField[] = []\n for (const key of columnOrder) {\n const col = availableColumns.find((c) => c.key === key)\n if (col && visibleSet.has(key)) ordered.push(col)\n }\n for (const col of availableColumns) {\n if (visibleSet.has(col.key) && !ordered.some((o) => o.key === col.key)) {\n ordered.push(col)\n }\n }\n if (!searchQuery) return ordered\n const lowerQuery = searchQuery.toLowerCase()\n return ordered.filter((c) => c.label.toLowerCase().includes(lowerQuery))\n }, [availableColumns, visibleSet, columnOrder, searchQuery])\n\n const groupedAvailable = React.useMemo(() => {\n const lowerQuery = searchQuery.toLowerCase()\n const filtered = availableColumns\n .filter((c) => !visibleSet.has(c.key))\n .filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery))\n\n const groups = new Map<string, ColumnChooserField[]>()\n for (const col of filtered) {\n const group = col.group || UNGROUPED_GROUP_ID\n const list = groups.get(group) ?? []\n list.push(col)\n groups.set(group, list)\n }\n return groups\n }, [availableColumns, searchQuery, visibleSet])\n\n const toggleGroup = React.useCallback((group: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev)\n if (next.has(group)) next.delete(group)\n else next.add(group)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = selectedColumns.findIndex((c) => c.key === active.id)\n const newIndex = selectedColumns.findIndex((c) => c.key === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n const reordered = [...selectedColumns]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n onReorderColumns(reordered.map((c) => c.key))\n }, [selectedColumns, onReorderColumns])\n\n const handleHideAll = React.useCallback(() => {\n for (const col of selectedColumns) {\n if (!col.alwaysVisible) onToggleColumn(col.key)\n }\n }, [selectedColumns, onToggleColumn])\n\n return (\n <div className=\"flex flex-col\">\n <div className=\"px-4 py-3 border-t\">\n <SearchInput\n size=\"sm\"\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={setSearchQuery}\n />\n </div>\n\n <div>\n {selectedColumns.length > 0 ? (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-baseline gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('ui.columnChooser.shown', 'Shown')}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {selectedColumns.length}/{availableColumns.length}\n </span>\n </div>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto px-0 text-sm text-muted-foreground hover:text-foreground\"\n onClick={handleHideAll}\n >\n {t('ui.columnChooser.hideAll', 'Hide all')}\n </Button>\n </div>\n <DndContext id={dndContextId} sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={selectedColumns.map((c) => c.key)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-2\">\n {selectedColumns.map((col) => (\n <SortableColumnItem key={col.key} column={col} onToggle={onToggleColumn} />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n </div>\n ) : null}\n\n <div className=\"border-t px-4 py-3 mt-4 mb-4\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3\">\n {t('ui.columnChooser.available', 'Available columns')}\n </div>\n {Array.from(groupedAvailable.entries()).map(([group, columns]) => {\n const isCollapsed = !searchQuery && !expandedGroups.has(group)\n return (\n <div key={group} className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start gap-2 h-auto px-1 py-2 text-xs font-medium uppercase text-muted-foreground\"\n onClick={() => toggleGroup(group)}\n >\n <ChevronRight className={`size-4 transition-transform ${isCollapsed ? '' : 'rotate-90'}`} />\n <span>{resolveGroupLabel(t, group, 'ui.columnChooser.ungrouped', 'Other')}</span>\n </Button>\n {!isCollapsed ? (\n <div className=\"space-y-2 mt-2\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex items-center gap-2 rounded pl-7 pr-2 py-1.5 text-sm hover:bg-muted/50 cursor-pointer\"\n onClick={() => { if (!col.alwaysVisible) onToggleColumn(col.key) }}\n >\n <span className=\"truncate flex-1 min-w-0\">{col.label}</span>\n <Switch\n checked={false}\n disabled={col.alwaysVisible}\n onCheckedChange={() => onToggleColumn(col.key)}\n onClick={(e) => e.stopPropagation()}\n className=\"shrink-0 scale-90\"\n />\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport function ColumnChooserPanel({\n open,\n onOpenChange,\n ...sectionProps\n}: ColumnChooserPanelProps) {\n const t = useT()\n React.useEffect(() => {\n if (!open) return\n if (typeof document === 'undefined') return\n document.body.dataset.columnChooserOpen = 'true'\n return () => {\n delete document.body.dataset.columnChooserOpen\n }\n }, [open])\n if (!open) return null\n return (\n <div className=\"fixed inset-y-0 right-0 z-modal w-80 border-l bg-background shadow-lg flex flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"font-semibold text-sm\">\n {t('ui.columnChooser.title', 'Columns')}\n </h3>\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => onOpenChange(false)} aria-label={t('ui.columnChooser.close', 'Close')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"flex-1 overflow-auto\">\n <ColumnChooserSection {...sectionProps} />\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { GripVertical, X, ChevronRight } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Switch } from '../../primitives/switch'\nimport { Input } from '../../primitives/input'\nimport { SearchInput } from '../../primitives/search-input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\n\nexport type ColumnChooserField = {\n key: string\n label: string\n group: string\n defaultVisible?: boolean\n alwaysVisible?: boolean\n}\n\nexport type ColumnChooserSectionProps = {\n availableColumns: ColumnChooserField[]\n visibleColumnKeys: string[]\n columnOrder: string[]\n onToggleColumn: (key: string) => void\n onReorderColumns: (newOrder: string[]) => void\n dndContextId?: string\n}\n\nexport type ColumnChooserPanelProps = ColumnChooserSectionProps & {\n open: boolean\n onOpenChange: (open: boolean) => void\n}\n\nfunction SortableColumnItem({\n column,\n onToggle,\n}: {\n column: ColumnChooserField\n onToggle: (key: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: column.key })\n\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n className=\"flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50\"\n >\n <span className=\"cursor-grab text-muted-foreground\" {...attributes} {...listeners}>\n <GripVertical className=\"size-4\" />\n </span>\n <span className=\"truncate flex-1 min-w-0\">{column.label}</span>\n <Switch\n checked\n disabled={column.alwaysVisible}\n onCheckedChange={() => onToggle(column.key)}\n className=\"shrink-0 scale-90\"\n />\n </div>\n )\n}\n\nexport function ColumnChooserSection({\n availableColumns,\n visibleColumnKeys,\n columnOrder,\n onToggleColumn,\n onReorderColumns,\n dndContextId = 'column-chooser',\n}: ColumnChooserSectionProps) {\n const t = useT()\n const [searchQuery, setSearchQuery] = React.useState('')\n const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set())\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const visibleSet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys])\n\n const selectedColumns = React.useMemo(() => {\n const ordered: ColumnChooserField[] = []\n for (const key of columnOrder) {\n const col = availableColumns.find((c) => c.key === key)\n if (col && visibleSet.has(key)) ordered.push(col)\n }\n for (const col of availableColumns) {\n if (visibleSet.has(col.key) && !ordered.some((o) => o.key === col.key)) {\n ordered.push(col)\n }\n }\n if (!searchQuery) return ordered\n const lowerQuery = searchQuery.toLowerCase()\n return ordered.filter((c) => c.label.toLowerCase().includes(lowerQuery))\n }, [availableColumns, visibleSet, columnOrder, searchQuery])\n\n const groupedAvailable = React.useMemo(() => {\n const lowerQuery = searchQuery.toLowerCase()\n const filtered = availableColumns\n .filter((c) => !visibleSet.has(c.key))\n .filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery))\n\n const groups = new Map<string, ColumnChooserField[]>()\n for (const col of filtered) {\n const group = col.group || t('ui.columnChooser.ungrouped', 'Other')\n const list = groups.get(group) ?? []\n list.push(col)\n groups.set(group, list)\n }\n return groups\n }, [availableColumns, searchQuery, visibleSet, t])\n\n const toggleGroup = React.useCallback((group: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev)\n if (next.has(group)) next.delete(group)\n else next.add(group)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = selectedColumns.findIndex((c) => c.key === active.id)\n const newIndex = selectedColumns.findIndex((c) => c.key === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n const reordered = [...selectedColumns]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n onReorderColumns(reordered.map((c) => c.key))\n }, [selectedColumns, onReorderColumns])\n\n const handleHideAll = React.useCallback(() => {\n for (const col of selectedColumns) {\n if (!col.alwaysVisible) onToggleColumn(col.key)\n }\n }, [selectedColumns, onToggleColumn])\n\n return (\n <div className=\"flex flex-col\">\n <div className=\"px-4 py-3 border-t\">\n <SearchInput\n size=\"sm\"\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={setSearchQuery}\n />\n </div>\n\n <div>\n {selectedColumns.length > 0 ? (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-baseline gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('ui.columnChooser.shown', 'Shown')}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {selectedColumns.length}/{availableColumns.length}\n </span>\n </div>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto px-0 text-sm text-muted-foreground hover:text-foreground\"\n onClick={handleHideAll}\n >\n {t('ui.columnChooser.hideAll', 'Hide all')}\n </Button>\n </div>\n <DndContext id={dndContextId} sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={selectedColumns.map((c) => c.key)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-2\">\n {selectedColumns.map((col) => (\n <SortableColumnItem key={col.key} column={col} onToggle={onToggleColumn} />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n </div>\n ) : null}\n\n <div className=\"border-t px-4 py-3 mt-4 mb-4\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3\">\n {t('ui.columnChooser.available', 'Available columns')}\n </div>\n {Array.from(groupedAvailable.entries()).map(([group, columns]) => {\n const isCollapsed = !searchQuery && !expandedGroups.has(group)\n return (\n <div key={group} className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start gap-2 h-auto px-1 py-2 text-xs font-medium uppercase text-muted-foreground\"\n onClick={() => toggleGroup(group)}\n >\n <ChevronRight className={`size-4 transition-transform ${isCollapsed ? '' : 'rotate-90'}`} />\n <span>{group}</span>\n </Button>\n {!isCollapsed ? (\n <div className=\"space-y-2 mt-2\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex items-center gap-2 rounded pl-7 pr-2 py-1.5 text-sm hover:bg-muted/50 cursor-pointer\"\n onClick={() => { if (!col.alwaysVisible) onToggleColumn(col.key) }}\n >\n <span className=\"truncate flex-1 min-w-0\">{col.label}</span>\n <Switch\n checked={false}\n disabled={col.alwaysVisible}\n onCheckedChange={() => onToggleColumn(col.key)}\n onClick={(e) => e.stopPropagation()}\n className=\"shrink-0 scale-90\"\n />\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport function ColumnChooserPanel({\n open,\n onOpenChange,\n ...sectionProps\n}: ColumnChooserPanelProps) {\n const t = useT()\n React.useEffect(() => {\n if (!open) return\n if (typeof document === 'undefined') return\n document.body.dataset.columnChooserOpen = 'true'\n return () => {\n delete document.body.dataset.columnChooserOpen\n }\n }, [open])\n if (!open) return null\n return (\n <div className=\"fixed inset-y-0 right-0 z-modal w-80 border-l bg-background shadow-lg flex flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"font-semibold text-sm\">\n {t('ui.columnChooser.title', 'Columns')}\n </h3>\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => onOpenChange(false)} aria-label={t('ui.columnChooser.close', 'Close')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"flex-1 overflow-auto\">\n <ColumnChooserSection {...sectionProps} />\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -25,7 +25,6 @@ function ConfirmDialog({
25
25
  const [internalOpen, setInternalOpen] = React.useState(false);
26
26
  const cancelButtonRef = React.useRef(null);
27
27
  const confirmButtonRef = React.useRef(null);
28
- const restoreAriaHiddenRef = React.useRef(false);
29
28
  const [portalTarget, setPortalTarget] = React.useState(null);
30
29
  const reactId = React.useId();
31
30
  const titleId = `confirm-dialog-title-${reactId}`;
@@ -64,20 +63,10 @@ function ConfirmDialog({
64
63
  }
65
64
  }, 0);
66
65
  }
67
- if (dialog.getAttribute("aria-hidden") === "true") {
68
- dialog.removeAttribute("aria-hidden");
69
- restoreAriaHiddenRef.current = true;
70
- }
71
66
  } else {
72
67
  if (dialog.open) {
73
68
  dialog.close();
74
69
  }
75
- if (restoreAriaHiddenRef.current) {
76
- restoreAriaHiddenRef.current = false;
77
- if (dialog.hasAttribute("data-aria-hidden")) {
78
- dialog.setAttribute("aria-hidden", "true");
79
- }
80
- }
81
70
  }
82
71
  }, [open, resolvedCancelText, portalTarget]);
83
72
  React.useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/confirm-dialog/ConfirmDialog.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 { Button } from \"@open-mercato/ui/primitives/button\";\nimport { IconButton } from \"../../primitives/icon-button\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { Loader2, X } from \"lucide-react\";\n\nexport type ConfirmDialogProps = {\n /** Whether the dialog is open (controlled mode \u2014 used by useConfirmDialog) */\n open?: boolean;\n /** Callback when open state changes (controlled mode) */\n onOpenChange?: (open: boolean) => void;\n /** Callback when user confirms */\n onConfirm: () => void | Promise<void>;\n /** Callback when user cancels (optional, defaults to closing) */\n onCancel?: () => void;\n /** Dialog title. Defaults to i18n key \"ui.dialogs.confirm.defaultTitle\" (\"Are you sure?\") */\n title?: string;\n /** Dialog body text / description */\n text?: string;\n /** Confirm button label. Defaults to i18n \"ui.dialogs.confirm.confirmText\" (\"Confirm\").\n * Pass `false` to hide the confirm button entirely. */\n confirmText?: string | false;\n /** Cancel button label. Defaults to i18n \"ui.dialogs.confirm.cancelText\" (\"Cancel\").\n * Pass `false` to hide the cancel button entirely. */\n cancelText?: string | false;\n /** Visual variant \u2014 \"destructive\" renders the confirm button in red */\n variant?: \"default\" | \"destructive\";\n /** Whether the confirm button shows a loading spinner.\n * Useful for async onConfirm handlers (e.g., waiting for API response before closing). */\n loading?: boolean;\n /** Trigger element \u2014 when provided, component manages its own open state (declarative mode).\n * Clicking the trigger opens the dialog. */\n trigger?: React.ReactNode;\n};\n\nexport function ConfirmDialog({\n open: controlledOpen,\n onOpenChange,\n onConfirm,\n onCancel,\n title,\n text,\n confirmText,\n cancelText,\n variant = \"default\",\n loading = false,\n trigger,\n}: ConfirmDialogProps) {\n const t = useT();\n const dialogRef = React.useRef<HTMLDialogElement>(null);\n const [internalOpen, setInternalOpen] = React.useState(false);\n const cancelButtonRef = React.useRef<HTMLButtonElement>(null);\n const confirmButtonRef = React.useRef<HTMLButtonElement>(null);\n const restoreAriaHiddenRef = React.useRef(false);\n const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(null);\n // Unique IDs so multiple ConfirmDialog instances on the same page don't\n // collide and make `aria-labelledby` resolve to the wrong dialog's title.\n const reactId = React.useId();\n const titleId = `confirm-dialog-title-${reactId}`;\n const descriptionId = `confirm-dialog-description-${reactId}`;\n\n // Determine if we're in controlled mode (open prop provided) or declarative mode (trigger provided)\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n const setOpen = isControlled\n ? onOpenChange || (() => {})\n : setInternalOpen;\n const handleCancelCallback = React.useCallback(() => {\n if (!isControlled) {\n onCancel?.();\n }\n }, [isControlled, onCancel]);\n\n // Default text values from i18n\n const resolvedTitle =\n title ?? t(\"ui.dialogs.confirm.defaultTitle\", \"Are you sure?\");\n const resolvedConfirmText =\n confirmText === false\n ? false\n : confirmText ?? t(\"ui.dialogs.confirm.confirmText\", \"Confirm\");\n const resolvedCancelText =\n cancelText === false\n ? false\n : cancelText ?? t(\"ui.dialogs.confirm.cancelText\", \"Cancel\");\n const closeAriaLabel = t(\"ui.dialog.close.ariaLabel\", \"Close\");\n const requestClose = React.useCallback(() => {\n setOpen(false);\n handleCancelCallback();\n }, [setOpen, handleCancelCallback]);\n\n React.useLayoutEffect(() => {\n setPortalTarget(document.body);\n }, []);\n\n // Handle dialog open/close with native showModal/close\n React.useLayoutEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (open) {\n if (!dialog.open) {\n dialog.showModal();\n // Focus cancel button (safe default) or confirm if no cancel\n setTimeout(() => {\n if (resolvedCancelText !== false && cancelButtonRef.current) {\n cancelButtonRef.current.focus();\n } else if (confirmButtonRef.current) {\n confirmButtonRef.current.focus();\n }\n }, 0);\n }\n // A modal opened underneath (e.g. a Radix Dialog) marks every sibling in\n // <body> `aria-hidden`, including this portalled element. Shown on top\n // of that modal it must stay reachable for assistive technology.\n if (dialog.getAttribute(\"aria-hidden\") === \"true\") {\n dialog.removeAttribute(\"aria-hidden\");\n restoreAriaHiddenRef.current = true;\n }\n } else {\n if (dialog.open) {\n dialog.close();\n }\n if (restoreAriaHiddenRef.current) {\n restoreAriaHiddenRef.current = false;\n // Hand the attribute back only while the modal that hid us is still\n // open; its own cleanup has otherwise already removed the marker.\n if (dialog.hasAttribute(\"data-aria-hidden\")) {\n dialog.setAttribute(\"aria-hidden\", \"true\");\n }\n }\n }\n }, [open, resolvedCancelText, portalTarget]);\n\n // Handle native cancel event (Escape key)\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const handleCancel = (e: Event) => {\n // Prevent close if loading\n if (loading) {\n e.preventDefault();\n return;\n }\n requestClose();\n };\n\n dialog.addEventListener(\"cancel\", handleCancel);\n return () => dialog.removeEventListener(\"cancel\", handleCancel);\n }, [loading, requestClose]);\n\n React.useEffect(() => {\n if (!open) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n if (!(event.target instanceof Node) || !dialogRef.current?.contains(event.target)) return;\n event.preventDefault();\n event.stopPropagation();\n if (!loading) requestClose();\n };\n\n window.addEventListener(\"keydown\", handleEscape, true);\n return () => window.removeEventListener(\"keydown\", handleEscape, true);\n }, [loading, open, requestClose]);\n\n // Handle backdrop click\n const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {\n // Only close if clicking directly on the dialog (backdrop), not its children\n if (e.target === dialogRef.current && !loading) {\n requestClose();\n }\n };\n\n // Handle keyboard shortcuts\n React.useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl+Enter confirms\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\" && !loading) {\n e.preventDefault();\n handleConfirm();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, loading]);\n\n const handleConfirm = async () => {\n await onConfirm();\n // Don't auto-close if loading \u2014 let the parent control when to close\n if (!loading) {\n setOpen(false);\n }\n };\n\n const handleCancel = () => {\n requestClose();\n };\n\n const handleTriggerClick = () => {\n if (trigger) {\n setOpen(true);\n }\n };\n\n const dialogElement = (\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n aria-labelledby={titleId}\n aria-describedby={text ? descriptionId : undefined}\n onClick={handleBackdropClick}\n className={cn(\n // Reset dialog defaults\n \"m-0 p-0 max-w-none bg-transparent border-none pointer-events-auto\",\n // Backdrop styling\n \"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity\",\n // Mobile: bottom sheet\n \"fixed inset-x-0 bottom-0 top-auto w-full\",\n // Desktop: centered\n \"sm:inset-auto sm:mx-auto sm:my-auto sm:max-w-md sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2\",\n // Animation with reduced motion support\n \"motion-safe:open:animate-in motion-safe:open:fade-in-0 motion-safe:open:slide-in-from-bottom-4\",\n \"sm:motion-safe:open:slide-in-from-bottom-0 sm:motion-safe:open:zoom-in-95\",\n // Duration\n \"motion-safe:open:duration-300\"\n )}\n >\n <div\n role=\"document\"\n className={cn(\n // Panel container\n \"flex flex-col gap-4 rounded-t-2xl border-t bg-card p-6 text-foreground shadow-lg\",\n \"sm:rounded-xl sm:border\",\n // Relative positioning for close button\n \"relative\"\n )}\n >\n {/* Close button */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={loading}\n aria-label={closeAriaLabel}\n className=\"absolute right-4 top-4 opacity-70 hover:opacity-100\"\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n\n {/* Title */}\n <h2\n id={titleId}\n className={cn(\n \"text-sm font-medium leading-snug tracking-tight pr-6\",\n // Mobile: centered, Desktop: left-aligned\n \"text-center sm:text-left\"\n )}\n >\n {resolvedTitle}\n </h2>\n\n {/* Description (optional) */}\n {text && (\n <p\n id={descriptionId}\n className=\"text-sm font-medium leading-snug text-muted-foreground\"\n >\n {text}\n </p>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n {resolvedCancelText !== false && (\n <Button\n ref={cancelButtonRef}\n variant=\"outline\"\n onClick={handleCancel}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {resolvedCancelText}\n </Button>\n )}\n {resolvedConfirmText !== false && (\n <Button\n ref={confirmButtonRef}\n variant={variant === \"destructive\" ? \"destructive-solid\" : \"default\"}\n onClick={handleConfirm}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {resolvedConfirmText}\n </Button>\n )}\n </div>\n </div>\n </dialog>\n );\n\n return (\n <>\n {trigger && (\n <div onClick={handleTriggerClick} className=\"inline-block\" role=\"presentation\">\n {trigger}\n </div>\n )}\n\n {portalTarget ? createPortal(dialogElement, portalTarget) : null}\n </>\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 { Button } from \"@open-mercato/ui/primitives/button\";\nimport { IconButton } from \"../../primitives/icon-button\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { Loader2, X } from \"lucide-react\";\n\nexport type ConfirmDialogProps = {\n /** Whether the dialog is open (controlled mode \u2014 used by useConfirmDialog) */\n open?: boolean;\n /** Callback when open state changes (controlled mode) */\n onOpenChange?: (open: boolean) => void;\n /** Callback when user confirms */\n onConfirm: () => void | Promise<void>;\n /** Callback when user cancels (optional, defaults to closing) */\n onCancel?: () => void;\n /** Dialog title. Defaults to i18n key \"ui.dialogs.confirm.defaultTitle\" (\"Are you sure?\") */\n title?: string;\n /** Dialog body text / description */\n text?: string;\n /** Confirm button label. Defaults to i18n \"ui.dialogs.confirm.confirmText\" (\"Confirm\").\n * Pass `false` to hide the confirm button entirely. */\n confirmText?: string | false;\n /** Cancel button label. Defaults to i18n \"ui.dialogs.confirm.cancelText\" (\"Cancel\").\n * Pass `false` to hide the cancel button entirely. */\n cancelText?: string | false;\n /** Visual variant \u2014 \"destructive\" renders the confirm button in red */\n variant?: \"default\" | \"destructive\";\n /** Whether the confirm button shows a loading spinner.\n * Useful for async onConfirm handlers (e.g., waiting for API response before closing). */\n loading?: boolean;\n /** Trigger element \u2014 when provided, component manages its own open state (declarative mode).\n * Clicking the trigger opens the dialog. */\n trigger?: React.ReactNode;\n};\n\nexport function ConfirmDialog({\n open: controlledOpen,\n onOpenChange,\n onConfirm,\n onCancel,\n title,\n text,\n confirmText,\n cancelText,\n variant = \"default\",\n loading = false,\n trigger,\n}: ConfirmDialogProps) {\n const t = useT();\n const dialogRef = React.useRef<HTMLDialogElement>(null);\n const [internalOpen, setInternalOpen] = React.useState(false);\n const cancelButtonRef = React.useRef<HTMLButtonElement>(null);\n const confirmButtonRef = React.useRef<HTMLButtonElement>(null);\n const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(null);\n // Unique IDs so multiple ConfirmDialog instances on the same page don't\n // collide and make `aria-labelledby` resolve to the wrong dialog's title.\n const reactId = React.useId();\n const titleId = `confirm-dialog-title-${reactId}`;\n const descriptionId = `confirm-dialog-description-${reactId}`;\n\n // Determine if we're in controlled mode (open prop provided) or declarative mode (trigger provided)\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n const setOpen = isControlled\n ? onOpenChange || (() => {})\n : setInternalOpen;\n const handleCancelCallback = React.useCallback(() => {\n if (!isControlled) {\n onCancel?.();\n }\n }, [isControlled, onCancel]);\n\n // Default text values from i18n\n const resolvedTitle =\n title ?? t(\"ui.dialogs.confirm.defaultTitle\", \"Are you sure?\");\n const resolvedConfirmText =\n confirmText === false\n ? false\n : confirmText ?? t(\"ui.dialogs.confirm.confirmText\", \"Confirm\");\n const resolvedCancelText =\n cancelText === false\n ? false\n : cancelText ?? t(\"ui.dialogs.confirm.cancelText\", \"Cancel\");\n const closeAriaLabel = t(\"ui.dialog.close.ariaLabel\", \"Close\");\n const requestClose = React.useCallback(() => {\n setOpen(false);\n handleCancelCallback();\n }, [setOpen, handleCancelCallback]);\n\n React.useLayoutEffect(() => {\n setPortalTarget(document.body);\n }, []);\n\n // Handle dialog open/close with native showModal/close\n React.useLayoutEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (open) {\n if (!dialog.open) {\n dialog.showModal();\n // Focus cancel button (safe default) or confirm if no cancel\n setTimeout(() => {\n if (resolvedCancelText !== false && cancelButtonRef.current) {\n cancelButtonRef.current.focus();\n } else if (confirmButtonRef.current) {\n confirmButtonRef.current.focus();\n }\n }, 0);\n }\n } else {\n if (dialog.open) {\n dialog.close();\n }\n }\n }, [open, resolvedCancelText, portalTarget]);\n\n // Handle native cancel event (Escape key)\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const handleCancel = (e: Event) => {\n // Prevent close if loading\n if (loading) {\n e.preventDefault();\n return;\n }\n requestClose();\n };\n\n dialog.addEventListener(\"cancel\", handleCancel);\n return () => dialog.removeEventListener(\"cancel\", handleCancel);\n }, [loading, requestClose]);\n\n React.useEffect(() => {\n if (!open) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n if (!(event.target instanceof Node) || !dialogRef.current?.contains(event.target)) return;\n event.preventDefault();\n event.stopPropagation();\n if (!loading) requestClose();\n };\n\n window.addEventListener(\"keydown\", handleEscape, true);\n return () => window.removeEventListener(\"keydown\", handleEscape, true);\n }, [loading, open, requestClose]);\n\n // Handle backdrop click\n const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {\n // Only close if clicking directly on the dialog (backdrop), not its children\n if (e.target === dialogRef.current && !loading) {\n requestClose();\n }\n };\n\n // Handle keyboard shortcuts\n React.useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl+Enter confirms\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\" && !loading) {\n e.preventDefault();\n handleConfirm();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, loading]);\n\n const handleConfirm = async () => {\n await onConfirm();\n // Don't auto-close if loading \u2014 let the parent control when to close\n if (!loading) {\n setOpen(false);\n }\n };\n\n const handleCancel = () => {\n requestClose();\n };\n\n const handleTriggerClick = () => {\n if (trigger) {\n setOpen(true);\n }\n };\n\n const dialogElement = (\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n aria-labelledby={titleId}\n aria-describedby={text ? descriptionId : undefined}\n onClick={handleBackdropClick}\n className={cn(\n // Reset dialog defaults\n \"m-0 p-0 max-w-none bg-transparent border-none pointer-events-auto\",\n // Backdrop styling\n \"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity\",\n // Mobile: bottom sheet\n \"fixed inset-x-0 bottom-0 top-auto w-full\",\n // Desktop: centered\n \"sm:inset-auto sm:mx-auto sm:my-auto sm:max-w-md sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2\",\n // Animation with reduced motion support\n \"motion-safe:open:animate-in motion-safe:open:fade-in-0 motion-safe:open:slide-in-from-bottom-4\",\n \"sm:motion-safe:open:slide-in-from-bottom-0 sm:motion-safe:open:zoom-in-95\",\n // Duration\n \"motion-safe:open:duration-300\"\n )}\n >\n <div\n role=\"document\"\n className={cn(\n // Panel container\n \"flex flex-col gap-4 rounded-t-2xl border-t bg-card p-6 text-foreground shadow-lg\",\n \"sm:rounded-xl sm:border\",\n // Relative positioning for close button\n \"relative\"\n )}\n >\n {/* Close button */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={loading}\n aria-label={closeAriaLabel}\n className=\"absolute right-4 top-4 opacity-70 hover:opacity-100\"\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n\n {/* Title */}\n <h2\n id={titleId}\n className={cn(\n \"text-sm font-medium leading-snug tracking-tight pr-6\",\n // Mobile: centered, Desktop: left-aligned\n \"text-center sm:text-left\"\n )}\n >\n {resolvedTitle}\n </h2>\n\n {/* Description (optional) */}\n {text && (\n <p\n id={descriptionId}\n className=\"text-sm font-medium leading-snug text-muted-foreground\"\n >\n {text}\n </p>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n {resolvedCancelText !== false && (\n <Button\n ref={cancelButtonRef}\n variant=\"outline\"\n onClick={handleCancel}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {resolvedCancelText}\n </Button>\n )}\n {resolvedConfirmText !== false && (\n <Button\n ref={confirmButtonRef}\n variant={variant === \"destructive\" ? \"destructive-solid\" : \"default\"}\n onClick={handleConfirm}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {resolvedConfirmText}\n </Button>\n )}\n </div>\n </div>\n </dialog>\n );\n\n return (\n <>\n {trigger && (\n <div onClick={handleTriggerClick} className=\"inline-block\" role=\"presentation\">\n {trigger}\n </div>\n )}\n\n {portalTarget ? createPortal(dialogElement, portalTarget) : null}\n </>\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["handleCancel"]
7
7
  }
@@ -20,7 +20,6 @@ import { mapCrudServerErrorToFormErrors } from "../utils/serverErrors.js";
20
20
  import { MarkdownPreview } from "../markdown/index.js";
21
21
  import { createLogger } from "@open-mercato/shared/lib/logger";
22
22
  const logger = createLogger("ui");
23
- const INLINE_TRIGGER_HIDDEN_CLASSES = "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100";
24
23
  function resolveInlineErrorMessage(err, fallbackMessage, translate) {
25
24
  const raw = resolveRawInlineErrorMessage(err, fallbackMessage);
26
25
  return translate(raw, raw);
@@ -113,12 +112,11 @@ function InlineTextEditor({
113
112
  );
114
113
  const triggerClasses = cn(
115
114
  "shrink-0 transition-opacity duration-150",
116
- editing ? "opacity-100" : INLINE_TRIGGER_HIDDEN_CLASSES,
115
+ editing ? "opacity-100" : "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100",
117
116
  variant === "muted" ? "h-8 w-8" : null,
118
117
  triggerClassName ?? null
119
118
  );
120
119
  const triggerSize = variant === "plain" ? "icon" : "sm";
121
- const triggerLabel = editing ? t("ui.detail.inline.cancel", "Cancel") : t("ui.detail.inline.edit", "Edit");
122
120
  const setEditingSafe = React.useCallback(
123
121
  (next) => {
124
122
  setEditing(next);
@@ -343,9 +341,7 @@ function InlineTextEditor({
343
341
  const next = !editing;
344
342
  setEditingSafe(next);
345
343
  },
346
- "aria-label": triggerLabel,
347
- title: triggerLabel,
348
- children: editing ? /* @__PURE__ */ jsx(X, { className: "h-4 w-4", "aria-hidden": true }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4", "aria-hidden": true })
344
+ children: editing ? /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
349
345
  }
350
346
  )
351
347
  ] }) });
@@ -469,10 +465,9 @@ function InlineMultilineEditor({
469
465
  );
470
466
  const triggerClasses = cn(
471
467
  "transition-opacity duration-150",
472
- editing ? "opacity-100" : INLINE_TRIGGER_HIDDEN_CLASSES,
468
+ editing ? "opacity-100" : "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100",
473
469
  triggerClassName ?? null
474
470
  );
475
- const triggerLabel = editing ? t("ui.detail.inline.cancel", "Cancel") : t("ui.detail.inline.edit", "Edit");
476
471
  const handleSave = React.useCallback(async () => {
477
472
  const trimmed = draft.trim();
478
473
  const validationError = adjustError(draft);
@@ -596,9 +591,7 @@ function InlineMultilineEditor({
596
591
  event.stopPropagation();
597
592
  setEditing((state) => !state);
598
593
  },
599
- "aria-label": triggerLabel,
600
- title: triggerLabel,
601
- children: editing ? /* @__PURE__ */ jsx(X, { className: "h-4 w-4", "aria-hidden": true }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4", "aria-hidden": true })
594
+ children: editing ? /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
602
595
  }
603
596
  )
604
597
  ] }) });
@@ -632,11 +625,10 @@ function InlineSelectEditor({
632
625
  );
633
626
  const triggerClasses = cn(
634
627
  "shrink-0 transition-opacity duration-150",
635
- editing ? "opacity-100" : INLINE_TRIGGER_HIDDEN_CLASSES,
628
+ editing ? "opacity-100" : "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100",
636
629
  variant === "muted" ? "h-8 w-8" : null,
637
630
  triggerClassName ?? null
638
631
  );
639
- const triggerLabel = editing ? t("ui.detail.inline.cancel", "Cancel") : t("ui.detail.inline.edit", "Edit");
640
632
  const handleSave = React.useCallback(async () => {
641
633
  setSaving(true);
642
634
  try {
@@ -699,15 +691,12 @@ function InlineSelectEditor({
699
691
  event.stopPropagation();
700
692
  setEditing((state) => !state);
701
693
  },
702
- "aria-label": triggerLabel,
703
- title: triggerLabel,
704
- children: editing ? /* @__PURE__ */ jsx(X, { className: "h-4 w-4", "aria-hidden": true }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4", "aria-hidden": true })
694
+ children: editing ? /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
705
695
  }
706
696
  )
707
697
  ] }) });
708
698
  }
709
699
  export {
710
- INLINE_TRIGGER_HIDDEN_CLASSES,
711
700
  InlineMultilineEditor,
712
701
  InlineSelectEditor,
713
702
  InlineTextEditor,