payload-better-editor 1.0.0

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/admin/ErrorBoundary.d.ts +17 -0
  4. package/dist/admin/ErrorBoundary.js +62 -0
  5. package/dist/admin/ErrorBoundary.js.map +1 -0
  6. package/dist/admin/LiveEditorOverlay.d.ts +12 -0
  7. package/dist/admin/LiveEditorOverlay.js +160 -0
  8. package/dist/admin/LiveEditorOverlay.js.map +1 -0
  9. package/dist/admin/LiveEditorToggle.d.ts +7 -0
  10. package/dist/admin/LiveEditorToggle.js +84 -0
  11. package/dist/admin/LiveEditorToggle.js.map +1 -0
  12. package/dist/admin/PreviewFrame.d.ts +22 -0
  13. package/dist/admin/PreviewFrame.js +137 -0
  14. package/dist/admin/PreviewFrame.js.map +1 -0
  15. package/dist/admin/PreviewToolbar.d.ts +16 -0
  16. package/dist/admin/PreviewToolbar.js +90 -0
  17. package/dist/admin/PreviewToolbar.js.map +1 -0
  18. package/dist/admin/SettingsBanner.d.ts +3 -0
  19. package/dist/admin/SettingsBanner.js +105 -0
  20. package/dist/admin/SettingsBanner.js.map +1 -0
  21. package/dist/admin/ViewportToggle.d.ts +7 -0
  22. package/dist/admin/ViewportToggle.js +79 -0
  23. package/dist/admin/ViewportToggle.js.map +1 -0
  24. package/dist/admin/blocks/AddBlockDrawer.d.ts +9 -0
  25. package/dist/admin/blocks/AddBlockDrawer.js +16 -0
  26. package/dist/admin/blocks/AddBlockDrawer.js.map +1 -0
  27. package/dist/admin/blocks/BlockActionsToolbar.d.ts +15 -0
  28. package/dist/admin/blocks/BlockActionsToolbar.js +102 -0
  29. package/dist/admin/blocks/BlockActionsToolbar.js.map +1 -0
  30. package/dist/admin/blocks/BlockEmptyState.d.ts +6 -0
  31. package/dist/admin/blocks/BlockEmptyState.js +26 -0
  32. package/dist/admin/blocks/BlockEmptyState.js.map +1 -0
  33. package/dist/admin/blocks/BlockHeader.d.ts +7 -0
  34. package/dist/admin/blocks/BlockHeader.js +32 -0
  35. package/dist/admin/blocks/BlockHeader.js.map +1 -0
  36. package/dist/admin/blocks/schema.d.ts +19 -0
  37. package/dist/admin/blocks/schema.js +80 -0
  38. package/dist/admin/blocks/schema.js.map +1 -0
  39. package/dist/admin/blocks/useBlockActions.d.ts +24 -0
  40. package/dist/admin/blocks/useBlockActions.js +100 -0
  41. package/dist/admin/blocks/useBlockActions.js.map +1 -0
  42. package/dist/admin/icons.d.ts +24 -0
  43. package/dist/admin/icons.js +36 -0
  44. package/dist/admin/icons.js.map +1 -0
  45. package/dist/admin/sidebar/BlockSettingsTab.d.ts +10 -0
  46. package/dist/admin/sidebar/BlockSettingsTab.js +153 -0
  47. package/dist/admin/sidebar/BlockSettingsTab.js.map +1 -0
  48. package/dist/admin/sidebar/DocumentFieldsTab.d.ts +8 -0
  49. package/dist/admin/sidebar/DocumentFieldsTab.js +38 -0
  50. package/dist/admin/sidebar/DocumentFieldsTab.js.map +1 -0
  51. package/dist/admin/sidebar/DocumentMetaTab.d.ts +2 -0
  52. package/dist/admin/sidebar/DocumentMetaTab.js +11 -0
  53. package/dist/admin/sidebar/DocumentMetaTab.js.map +1 -0
  54. package/dist/admin/sidebar/DocumentSettingsTab.d.ts +2 -0
  55. package/dist/admin/sidebar/DocumentSettingsTab.js +48 -0
  56. package/dist/admin/sidebar/DocumentSettingsTab.js.map +1 -0
  57. package/dist/admin/sidebar/Sidebar.d.ts +10 -0
  58. package/dist/admin/sidebar/Sidebar.js +92 -0
  59. package/dist/admin/sidebar/Sidebar.js.map +1 -0
  60. package/dist/client.d.ts +34 -0
  61. package/dist/client.js +30 -0
  62. package/dist/client.js.map +1 -0
  63. package/dist/global.d.ts +4 -0
  64. package/dist/global.js +200 -0
  65. package/dist/global.js.map +1 -0
  66. package/dist/hooks/useAddBlockDrawer.d.ts +14 -0
  67. package/dist/hooks/useAddBlockDrawer.js +26 -0
  68. package/dist/hooks/useAddBlockDrawer.js.map +1 -0
  69. package/dist/hooks/useBlockActionMessages.d.ts +8 -0
  70. package/dist/hooks/useBlockActionMessages.js +107 -0
  71. package/dist/hooks/useBlockActionMessages.js.map +1 -0
  72. package/dist/hooks/useDocConfig.d.ts +6 -0
  73. package/dist/hooks/useDocConfig.js +18 -0
  74. package/dist/hooks/useDocConfig.js.map +1 -0
  75. package/dist/hooks/useFocusTrap.d.ts +2 -0
  76. package/dist/hooks/useFocusTrap.js +84 -0
  77. package/dist/hooks/useFocusTrap.js.map +1 -0
  78. package/dist/hooks/useFullscreenOverlay.d.ts +2 -0
  79. package/dist/hooks/useFullscreenOverlay.js +30 -0
  80. package/dist/hooks/useFullscreenOverlay.js.map +1 -0
  81. package/dist/hooks/useIframeResizeObserver.d.ts +2 -0
  82. package/dist/hooks/useIframeResizeObserver.js +20 -0
  83. package/dist/hooks/useIframeResizeObserver.js.map +1 -0
  84. package/dist/hooks/useLatestRef.d.ts +6 -0
  85. package/dist/hooks/useLatestRef.js +12 -0
  86. package/dist/hooks/useLatestRef.js.map +1 -0
  87. package/dist/hooks/useMainWrapperPortal.d.ts +1 -0
  88. package/dist/hooks/useMainWrapperPortal.js +64 -0
  89. package/dist/hooks/useMainWrapperPortal.js.map +1 -0
  90. package/dist/hooks/useOverlayKeyboard.d.ts +6 -0
  91. package/dist/hooks/useOverlayKeyboard.js +43 -0
  92. package/dist/hooks/useOverlayKeyboard.js.map +1 -0
  93. package/dist/hooks/usePreviewBinding.d.ts +28 -0
  94. package/dist/hooks/usePreviewBinding.js +108 -0
  95. package/dist/hooks/usePreviewBinding.js.map +1 -0
  96. package/dist/hooks/usePreviewHandleDrag.d.ts +11 -0
  97. package/dist/hooks/usePreviewHandleDrag.js +53 -0
  98. package/dist/hooks/usePreviewHandleDrag.js.map +1 -0
  99. package/dist/hooks/usePreviewSelectionSync.d.ts +15 -0
  100. package/dist/hooks/usePreviewSelectionSync.js +80 -0
  101. package/dist/hooks/usePreviewSelectionSync.js.map +1 -0
  102. package/dist/hooks/usePreviewSettingsSync.d.ts +17 -0
  103. package/dist/hooks/usePreviewSettingsSync.js +55 -0
  104. package/dist/hooks/usePreviewSettingsSync.js.map +1 -0
  105. package/dist/hooks/useSidebarResize.d.ts +8 -0
  106. package/dist/hooks/useSidebarResize.js +101 -0
  107. package/dist/hooks/useSidebarResize.js.map +1 -0
  108. package/dist/hooks/useViewportState.d.ts +10 -0
  109. package/dist/hooks/useViewportState.js +44 -0
  110. package/dist/hooks/useViewportState.js.map +1 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +104 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/internal/constants.d.ts +22 -0
  115. package/dist/internal/constants.js +38 -0
  116. package/dist/internal/constants.js.map +1 -0
  117. package/dist/internal/dom.d.ts +4 -0
  118. package/dist/internal/dom.js +6 -0
  119. package/dist/internal/dom.js.map +1 -0
  120. package/dist/internal/iframe.d.ts +5 -0
  121. package/dist/internal/iframe.js +12 -0
  122. package/dist/internal/iframe.js.map +1 -0
  123. package/dist/internal/limits.d.ts +9 -0
  124. package/dist/internal/limits.js +11 -0
  125. package/dist/internal/limits.js.map +1 -0
  126. package/dist/internal/path.d.ts +5 -0
  127. package/dist/internal/path.js +12 -0
  128. package/dist/internal/path.js.map +1 -0
  129. package/dist/internal/postmessage.d.ts +3 -0
  130. package/dist/internal/postmessage.js +21 -0
  131. package/dist/internal/postmessage.js.map +1 -0
  132. package/dist/internal/storage-keys.d.ts +8 -0
  133. package/dist/internal/storage-keys.js +9 -0
  134. package/dist/internal/storage-keys.js.map +1 -0
  135. package/dist/internal/storage.d.ts +2 -0
  136. package/dist/internal/storage.js +20 -0
  137. package/dist/internal/storage.js.map +1 -0
  138. package/dist/preview/HoverToolbar.d.ts +8 -0
  139. package/dist/preview/HoverToolbar.js +48 -0
  140. package/dist/preview/HoverToolbar.js.map +1 -0
  141. package/dist/preview/HoverToolbarController.d.ts +31 -0
  142. package/dist/preview/HoverToolbarController.js +160 -0
  143. package/dist/preview/HoverToolbarController.js.map +1 -0
  144. package/dist/preview/hover-css.d.ts +11 -0
  145. package/dist/preview/hover-css.js +94 -0
  146. package/dist/preview/hover-css.js.map +1 -0
  147. package/dist/preview/installClickToFocus.d.ts +6 -0
  148. package/dist/preview/installClickToFocus.js +21 -0
  149. package/dist/preview/installClickToFocus.js.map +1 -0
  150. package/dist/preview/installHoverStyles.d.ts +2 -0
  151. package/dist/preview/installHoverStyles.js +15 -0
  152. package/dist/preview/installHoverStyles.js.map +1 -0
  153. package/dist/preview/protocol.d.ts +11 -0
  154. package/dist/preview/protocol.js +19 -0
  155. package/dist/preview/protocol.js.map +1 -0
  156. package/dist/preview/toolbar-position.d.ts +20 -0
  157. package/dist/preview/toolbar-position.js +22 -0
  158. package/dist/preview/toolbar-position.js.map +1 -0
  159. package/dist/providers/BetterEditorConfigProvider.d.ts +14 -0
  160. package/dist/providers/BetterEditorConfigProvider.js +26 -0
  161. package/dist/providers/BetterEditorConfigProvider.js.map +1 -0
  162. package/dist/providers/OverlayProviders.d.ts +8 -0
  163. package/dist/providers/OverlayProviders.js +22 -0
  164. package/dist/providers/OverlayProviders.js.map +1 -0
  165. package/dist/state/useBetterEditorSettings.d.ts +18 -0
  166. package/dist/state/useBetterEditorSettings.js +65 -0
  167. package/dist/state/useBetterEditorSettings.js.map +1 -0
  168. package/dist/state/useEditorHistory.d.ts +16 -0
  169. package/dist/state/useEditorHistory.js +157 -0
  170. package/dist/state/useEditorHistory.js.map +1 -0
  171. package/dist/styles/blocks-tab.css +163 -0
  172. package/dist/styles/overlay.css +133 -0
  173. package/dist/styles/preview.css +211 -0
  174. package/dist/styles/settings-banner.css +73 -0
  175. package/dist/styles/sidebar.css +88 -0
  176. package/dist/types.d.ts +41 -0
  177. package/dist/types.js +3 -0
  178. package/dist/types.js.map +1 -0
  179. package/dist/version.d.ts +1 -0
  180. package/dist/version.js +6 -0
  181. package/dist/version.js.map +1 -0
  182. package/package.json +117 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useBlockActionMessages.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { useAllFormFields, useForm } from '@payloadcms/ui'\nimport { listenForParentInbound } from '../internal/postmessage'\nimport { splitFieldPath } from '../internal/path'\nimport type { FormState } from 'payload'\nimport { useEditorHistory } from '../state/useEditorHistory'\nimport { useLatestRef } from './useLatestRef'\n\nconst ID_SUFFIX = '.id'\n\nconst buildIdIndex = (fields: FormState): Map<string, string> => {\n const map = new Map<string, string>()\n for (const key of Object.keys(fields)) {\n if (!key.endsWith(ID_SUFFIX)) continue\n const value = fields[key]?.value\n if (typeof value === 'string' && value.length > 0) {\n map.set(value, key.slice(0, -ID_SUFFIX.length))\n }\n }\n return map\n}\n\nexport type UseBlockActionMessagesArgs = {\n selectedBlockPath: string | null\n setSelectedBlockPath: (path: string | null) => void\n}\n\nexport type UseBlockActionMessagesReturn = {\n addBelowRequestId: number\n}\n\nexport const useBlockActionMessages = ({\n setSelectedBlockPath,\n}: UseBlockActionMessagesArgs): UseBlockActionMessagesReturn => {\n const [addBelowRequestId, setAddBelowRequestId] = useState<number>(0)\n\n const [allFields] = useAllFormFields()\n const idIndex = useMemo(() => buildIdIndex(allFields as FormState), [allFields])\n const { dispatchFields } = useForm()\n const history = useEditorHistory()\n\n // Refs let the postMessage listener stay bound across renders without\n // missing the latest form state, dispatcher, or history commit.\n const allFieldsRef = useLatestRef(allFields)\n const idIndexRef = useLatestRef(idIndex)\n const dispatchFieldsRef = useLatestRef(dispatchFields)\n const historyRef = useLatestRef(history)\n const setSelectedBlockPathRef = useLatestRef(setSelectedBlockPath)\n\n // Bind the postMessage listener exactly once. All four state inputs flow\n // through stable refs (above), so a re-bind is never needed.\n useEffect(\n () =>\n listenForParentInbound((data) => {\n const fields = allFieldsRef.current as FormState\n const path = idIndexRef.current.get(data.id) ?? null\n if (!path) return\n\n const select = setSelectedBlockPathRef.current\n const dispatch = dispatchFieldsRef.current\n const { commit } = historyRef.current\n\n if (data.type === 'focus-block') {\n select(path)\n return\n }\n\n if (data.action === 'add') {\n // Monotonic counter — BlockSettingsTab compares the latest id against\n // its lastHandledRequestRef. Date.now() risked collisions on\n // double-clicks landing in the same millisecond.\n select(path)\n setAddBelowRequestId((id) => id + 1)\n return\n }\n\n const split = splitFieldPath(path)\n if (!split) return\n const { parent: parentPath, index: rowIndex } = split\n const rows = fields[parentPath]?.rows\n const rowCount = Array.isArray(rows) ? rows.length : 0\n if (rowIndex < 0 || rowIndex >= rowCount) return\n\n switch (data.action) {\n case 'move-up':\n if (rowIndex === 0) return\n commit(() =>\n dispatch({\n type: 'MOVE_ROW',\n path: parentPath,\n moveFromIndex: rowIndex,\n moveToIndex: rowIndex - 1,\n }),\n )\n select(`${parentPath}.${rowIndex - 1}`)\n break\n case 'move-down':\n if (rowIndex >= rowCount - 1) return\n commit(() =>\n dispatch({\n type: 'MOVE_ROW',\n path: parentPath,\n moveFromIndex: rowIndex,\n moveToIndex: rowIndex + 1,\n }),\n )\n select(`${parentPath}.${rowIndex + 1}`)\n break\n case 'duplicate':\n commit(() => dispatch({ type: 'DUPLICATE_ROW', path: parentPath, rowIndex }))\n select(`${parentPath}.${rowIndex + 1}`)\n break\n case 'delete':\n commit(() => dispatch({ type: 'REMOVE_ROW', path: parentPath, rowIndex }))\n select(null)\n break\n }\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n [],\n )\n\n return { addBelowRequestId }\n}\n"],"names":["useEffect","useMemo","useState","useAllFormFields","useForm","listenForParentInbound","splitFieldPath","useEditorHistory","useLatestRef","ID_SUFFIX","buildIdIndex","fields","map","Map","key","Object","keys","endsWith","value","length","set","slice","useBlockActionMessages","setSelectedBlockPath","addBelowRequestId","setAddBelowRequestId","allFields","idIndex","dispatchFields","history","allFieldsRef","idIndexRef","dispatchFieldsRef","historyRef","setSelectedBlockPathRef","data","current","path","get","id","select","dispatch","commit","type","action","split","parent","parentPath","index","rowIndex","rows","rowCount","Array","isArray","moveFromIndex","moveToIndex"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { ClientField } from 'payload';
2
+ export type UseDocConfigReturn = {
3
+ fields: ClientField[] | undefined;
4
+ slug: string;
5
+ };
6
+ export declare const useDocConfig: () => UseDocConfigReturn;
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { useMemo } from 'react';
3
+ import { useDocumentInfo } from '@payloadcms/ui';
4
+ export const useDocConfig = ()=>{
5
+ const { docConfig } = useDocumentInfo();
6
+ return useMemo(()=>{
7
+ const fields = docConfig && 'fields' in docConfig ? docConfig.fields : undefined;
8
+ const slug = docConfig && 'slug' in docConfig ? docConfig.slug : '';
9
+ return {
10
+ fields,
11
+ slug
12
+ };
13
+ }, [
14
+ docConfig
15
+ ]);
16
+ };
17
+
18
+ //# sourceMappingURL=useDocConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useDocConfig.ts"],"sourcesContent":["'use client'\n\nimport { useMemo } from 'react'\nimport type { ClientField } from 'payload'\nimport { useDocumentInfo } from '@payloadcms/ui'\n\nexport type UseDocConfigReturn = {\n fields: ClientField[] | undefined\n slug: string\n}\n\nexport const useDocConfig = (): UseDocConfigReturn => {\n const { docConfig } = useDocumentInfo()\n return useMemo(() => {\n const fields =\n docConfig && 'fields' in docConfig ? (docConfig.fields as ClientField[]) : undefined\n const slug = docConfig && 'slug' in docConfig ? docConfig.slug : ''\n return { fields, slug }\n }, [docConfig])\n}\n"],"names":["useMemo","useDocumentInfo","useDocConfig","docConfig","fields","undefined","slug"],"mappings":"AAAA;AAEA,SAASA,OAAO,QAAQ,QAAO;AAE/B,SAASC,eAAe,QAAQ,iBAAgB;AAOhD,OAAO,MAAMC,eAAe;IAC1B,MAAM,EAAEC,SAAS,EAAE,GAAGF;IACtB,OAAOD,QAAQ;QACb,MAAMI,SACJD,aAAa,YAAYA,YAAaA,UAAUC,MAAM,GAAqBC;QAC7E,MAAMC,OAAOH,aAAa,UAAUA,YAAYA,UAAUG,IAAI,GAAG;QACjE,OAAO;YAAEF;YAAQE;QAAK;IACxB,GAAG;QAACH;KAAU;AAChB,EAAC"}
@@ -0,0 +1,2 @@
1
+ import { type RefObject } from 'react';
2
+ export declare const useFocusTrap: (ref: RefObject<HTMLElement | null>, active?: boolean) => void;
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'a[href]',
5
+ 'area[href]',
6
+ 'button:not([disabled])',
7
+ 'input:not([disabled]):not([type="hidden"])',
8
+ 'select:not([disabled])',
9
+ 'textarea:not([disabled])',
10
+ '[tabindex]:not([tabindex="-1"])',
11
+ 'audio[controls]',
12
+ 'video[controls]',
13
+ '[contenteditable]:not([contenteditable="false"])'
14
+ ].join(',');
15
+ const isVisible = (el)=>{
16
+ if (el.hidden) return false;
17
+ const rects = el.getClientRects();
18
+ return rects.length > 0;
19
+ };
20
+ const getFocusable = (root)=>{
21
+ const nodes = root.querySelectorAll(FOCUSABLE_SELECTOR);
22
+ const out = [];
23
+ for (const n of nodes)if (isVisible(n)) out.push(n);
24
+ return out;
25
+ };
26
+ export const useFocusTrap = (ref, active = true)=>{
27
+ useEffect(()=>{
28
+ if (!active) return;
29
+ const root = ref.current;
30
+ if (!root) return;
31
+ const previouslyFocused = document.activeElement ?? null;
32
+ // Initial focus: prefer first interactive child; fall back to root itself
33
+ // so keyboard users land inside the dialog.
34
+ const focusables = getFocusable(root);
35
+ if (focusables.length > 0) {
36
+ focusables[0].focus();
37
+ } else {
38
+ if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1');
39
+ root.focus();
40
+ }
41
+ const onKey = (e)=>{
42
+ if (e.key !== 'Tab') return;
43
+ const items = getFocusable(root);
44
+ if (items.length === 0) {
45
+ e.preventDefault();
46
+ root.focus();
47
+ return;
48
+ }
49
+ const first = items[0];
50
+ const last = items[items.length - 1];
51
+ const activeEl = document.activeElement;
52
+ // If focus has escaped the trap entirely, pull it back to a safe edge.
53
+ if (!activeEl || !root.contains(activeEl)) {
54
+ e.preventDefault();
55
+ (e.shiftKey ? last : first).focus();
56
+ return;
57
+ }
58
+ if (e.shiftKey && activeEl === first) {
59
+ e.preventDefault();
60
+ last.focus();
61
+ } else if (!e.shiftKey && activeEl === last) {
62
+ e.preventDefault();
63
+ first.focus();
64
+ }
65
+ };
66
+ document.addEventListener('keydown', onKey, true);
67
+ return ()=>{
68
+ document.removeEventListener('keydown', onKey, true);
69
+ // Best-effort restore — the original element may have been removed
70
+ // from the DOM while the overlay was open.
71
+ if (previouslyFocused && previouslyFocused.isConnected) {
72
+ try {
73
+ previouslyFocused.focus();
74
+ } catch {
75
+ /* element no longer focusable */ }
76
+ }
77
+ };
78
+ }, [
79
+ ref,
80
+ active
81
+ ]);
82
+ };
83
+
84
+ //# sourceMappingURL=useFocusTrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useFocusTrap.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, type RefObject } from 'react'\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n].join(',')\n\nconst isVisible = (el: HTMLElement): boolean => {\n if (el.hidden) return false\n const rects = el.getClientRects()\n return rects.length > 0\n}\n\nconst getFocusable = (root: HTMLElement): HTMLElement[] => {\n const nodes = root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)\n const out: HTMLElement[] = []\n for (const n of nodes) if (isVisible(n)) out.push(n)\n return out\n}\n\nexport const useFocusTrap = (\n ref: RefObject<HTMLElement | null>,\n active: boolean = true,\n): void => {\n useEffect(() => {\n if (!active) return\n const root = ref.current\n if (!root) return\n\n const previouslyFocused = (document.activeElement as HTMLElement | null) ?? null\n\n // Initial focus: prefer first interactive child; fall back to root itself\n // so keyboard users land inside the dialog.\n const focusables = getFocusable(root)\n if (focusables.length > 0) {\n focusables[0].focus()\n } else {\n if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1')\n root.focus()\n }\n\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Tab') return\n const items = getFocusable(root)\n if (items.length === 0) {\n e.preventDefault()\n root.focus()\n return\n }\n const first = items[0]\n const last = items[items.length - 1]\n const activeEl = document.activeElement as HTMLElement | null\n // If focus has escaped the trap entirely, pull it back to a safe edge.\n if (!activeEl || !root.contains(activeEl)) {\n e.preventDefault()\n ;(e.shiftKey ? last : first).focus()\n return\n }\n if (e.shiftKey && activeEl === first) {\n e.preventDefault()\n last.focus()\n } else if (!e.shiftKey && activeEl === last) {\n e.preventDefault()\n first.focus()\n }\n }\n\n document.addEventListener('keydown', onKey, true)\n return () => {\n document.removeEventListener('keydown', onKey, true)\n // Best-effort restore — the original element may have been removed\n // from the DOM while the overlay was open.\n if (previouslyFocused && previouslyFocused.isConnected) {\n try {\n previouslyFocused.focus()\n } catch {\n /* element no longer focusable */\n }\n }\n }\n }, [ref, active])\n}\n"],"names":["useEffect","FOCUSABLE_SELECTOR","join","isVisible","el","hidden","rects","getClientRects","length","getFocusable","root","nodes","querySelectorAll","out","n","push","useFocusTrap","ref","active","current","previouslyFocused","document","activeElement","focusables","focus","hasAttribute","setAttribute","onKey","e","key","items","preventDefault","first","last","activeEl","contains","shiftKey","addEventListener","removeEventListener","isConnected"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAwB,QAAO;AAEjD,MAAMC,qBAAqB;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAACC,IAAI,CAAC;AAEP,MAAMC,YAAY,CAACC;IACjB,IAAIA,GAAGC,MAAM,EAAE,OAAO;IACtB,MAAMC,QAAQF,GAAGG,cAAc;IAC/B,OAAOD,MAAME,MAAM,GAAG;AACxB;AAEA,MAAMC,eAAe,CAACC;IACpB,MAAMC,QAAQD,KAAKE,gBAAgB,CAAcX;IACjD,MAAMY,MAAqB,EAAE;IAC7B,KAAK,MAAMC,KAAKH,MAAO,IAAIR,UAAUW,IAAID,IAAIE,IAAI,CAACD;IAClD,OAAOD;AACT;AAEA,OAAO,MAAMG,eAAe,CAC1BC,KACAC,SAAkB,IAAI;IAEtBlB,UAAU;QACR,IAAI,CAACkB,QAAQ;QACb,MAAMR,OAAOO,IAAIE,OAAO;QACxB,IAAI,CAACT,MAAM;QAEX,MAAMU,oBAAoB,AAACC,SAASC,aAAa,IAA2B;QAE5E,0EAA0E;QAC1E,4CAA4C;QAC5C,MAAMC,aAAad,aAAaC;QAChC,IAAIa,WAAWf,MAAM,GAAG,GAAG;YACzBe,UAAU,CAAC,EAAE,CAACC,KAAK;QACrB,OAAO;YACL,IAAI,CAACd,KAAKe,YAAY,CAAC,aAAaf,KAAKgB,YAAY,CAAC,YAAY;YAClEhB,KAAKc,KAAK;QACZ;QAEA,MAAMG,QAAQ,CAACC;YACb,IAAIA,EAAEC,GAAG,KAAK,OAAO;YACrB,MAAMC,QAAQrB,aAAaC;YAC3B,IAAIoB,MAAMtB,MAAM,KAAK,GAAG;gBACtBoB,EAAEG,cAAc;gBAChBrB,KAAKc,KAAK;gBACV;YACF;YACA,MAAMQ,QAAQF,KAAK,CAAC,EAAE;YACtB,MAAMG,OAAOH,KAAK,CAACA,MAAMtB,MAAM,GAAG,EAAE;YACpC,MAAM0B,WAAWb,SAASC,aAAa;YACvC,uEAAuE;YACvE,IAAI,CAACY,YAAY,CAACxB,KAAKyB,QAAQ,CAACD,WAAW;gBACzCN,EAAEG,cAAc;gBACdH,CAAAA,EAAEQ,QAAQ,GAAGH,OAAOD,KAAI,EAAGR,KAAK;gBAClC;YACF;YACA,IAAII,EAAEQ,QAAQ,IAAIF,aAAaF,OAAO;gBACpCJ,EAAEG,cAAc;gBAChBE,KAAKT,KAAK;YACZ,OAAO,IAAI,CAACI,EAAEQ,QAAQ,IAAIF,aAAaD,MAAM;gBAC3CL,EAAEG,cAAc;gBAChBC,MAAMR,KAAK;YACb;QACF;QAEAH,SAASgB,gBAAgB,CAAC,WAAWV,OAAO;QAC5C,OAAO;YACLN,SAASiB,mBAAmB,CAAC,WAAWX,OAAO;YAC/C,mEAAmE;YACnE,2CAA2C;YAC3C,IAAIP,qBAAqBA,kBAAkBmB,WAAW,EAAE;gBACtD,IAAI;oBACFnB,kBAAkBI,KAAK;gBACzB,EAAE,OAAM;gBACN,+BAA+B,GACjC;YACF;QACF;IACF,GAAG;QAACP;QAAKC;KAAO;AAClB,EAAC"}
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const useFullscreenOverlay: (isFullscreen: boolean, onExitFullscreen: () => void) => React.RefObject<HTMLDivElement | null>;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import React, { useEffect, useRef } from 'react';
3
+ export const useFullscreenOverlay = (isFullscreen, onExitFullscreen)=>{
4
+ const overlayRef = useRef(null);
5
+ // Mutable ref so the fullscreenchange listener can call the latest handler
6
+ // without re-binding when the consumer recreates its callback.
7
+ const onExitRef = useRef(onExitFullscreen);
8
+ onExitRef.current = onExitFullscreen;
9
+ useEffect(()=>{
10
+ const root = overlayRef.current;
11
+ if (!root) return;
12
+ if (isFullscreen && !document.fullscreenElement) {
13
+ // Some browsers reject requestFullscreen outside a user gesture.
14
+ root.requestFullscreen?.().catch(()=>{});
15
+ } else if (!isFullscreen && document.fullscreenElement === root) {
16
+ document.exitFullscreen?.().catch(()=>{});
17
+ }
18
+ if (!isFullscreen) return;
19
+ const onFsChange = ()=>{
20
+ if (!document.fullscreenElement) onExitRef.current();
21
+ };
22
+ document.addEventListener('fullscreenchange', onFsChange);
23
+ return ()=>document.removeEventListener('fullscreenchange', onFsChange);
24
+ }, [
25
+ isFullscreen
26
+ ]);
27
+ return overlayRef;
28
+ };
29
+
30
+ //# sourceMappingURL=useFullscreenOverlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useFullscreenOverlay.ts"],"sourcesContent":["'use client'\n\nimport React, { useEffect, useRef } from 'react'\n\nexport const useFullscreenOverlay = (\n isFullscreen: boolean,\n onExitFullscreen: () => void,\n): React.RefObject<HTMLDivElement | null> => {\n const overlayRef = useRef<HTMLDivElement | null>(null)\n\n // Mutable ref so the fullscreenchange listener can call the latest handler\n // without re-binding when the consumer recreates its callback.\n const onExitRef = useRef(onExitFullscreen)\n onExitRef.current = onExitFullscreen\n\n useEffect(() => {\n const root = overlayRef.current\n if (!root) return\n\n if (isFullscreen && !document.fullscreenElement) {\n // Some browsers reject requestFullscreen outside a user gesture.\n root.requestFullscreen?.().catch(() => {})\n } else if (!isFullscreen && document.fullscreenElement === root) {\n document.exitFullscreen?.().catch(() => {})\n }\n\n if (!isFullscreen) return\n\n const onFsChange = () => {\n if (!document.fullscreenElement) onExitRef.current()\n }\n document.addEventListener('fullscreenchange', onFsChange)\n return () => document.removeEventListener('fullscreenchange', onFsChange)\n }, [isFullscreen])\n\n return overlayRef\n}\n"],"names":["React","useEffect","useRef","useFullscreenOverlay","isFullscreen","onExitFullscreen","overlayRef","onExitRef","current","root","document","fullscreenElement","requestFullscreen","catch","exitFullscreen","onFsChange","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,OAAOA,SAASC,SAAS,EAAEC,MAAM,QAAQ,QAAO;AAEhD,OAAO,MAAMC,uBAAuB,CAClCC,cACAC;IAEA,MAAMC,aAAaJ,OAA8B;IAEjD,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAMK,YAAYL,OAAOG;IACzBE,UAAUC,OAAO,GAAGH;IAEpBJ,UAAU;QACR,MAAMQ,OAAOH,WAAWE,OAAO;QAC/B,IAAI,CAACC,MAAM;QAEX,IAAIL,gBAAgB,CAACM,SAASC,iBAAiB,EAAE;YAC/C,iEAAiE;YACjEF,KAAKG,iBAAiB,KAAKC,MAAM,KAAO;QAC1C,OAAO,IAAI,CAACT,gBAAgBM,SAASC,iBAAiB,KAAKF,MAAM;YAC/DC,SAASI,cAAc,KAAKD,MAAM,KAAO;QAC3C;QAEA,IAAI,CAACT,cAAc;QAEnB,MAAMW,aAAa;YACjB,IAAI,CAACL,SAASC,iBAAiB,EAAEJ,UAAUC,OAAO;QACpD;QACAE,SAASM,gBAAgB,CAAC,oBAAoBD;QAC9C,OAAO,IAAML,SAASO,mBAAmB,CAAC,oBAAoBF;IAChE,GAAG;QAACX;KAAa;IAEjB,OAAOE;AACT,EAAC"}
@@ -0,0 +1,2 @@
1
+ import { type RefObject } from 'react';
2
+ export declare const useIframeResizeObserver: (iframeRef: RefObject<HTMLIFrameElement | null>, onIframeWidthChange?: (width: number) => void) => void;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ export const useIframeResizeObserver = (iframeRef, onIframeWidthChange)=>{
4
+ useEffect(()=>{
5
+ const iframe = iframeRef.current;
6
+ if (!iframe || !onIframeWidthChange || typeof ResizeObserver === 'undefined') return;
7
+ onIframeWidthChange(iframe.clientWidth);
8
+ const ro = new ResizeObserver((entries)=>{
9
+ const w = entries[0]?.contentRect?.width;
10
+ if (typeof w === 'number') onIframeWidthChange(Math.round(w));
11
+ });
12
+ ro.observe(iframe);
13
+ return ()=>ro.disconnect();
14
+ }, [
15
+ iframeRef,
16
+ onIframeWidthChange
17
+ ]);
18
+ };
19
+
20
+ //# sourceMappingURL=useIframeResizeObserver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useIframeResizeObserver.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, type RefObject } from 'react'\n\nexport const useIframeResizeObserver = (\n iframeRef: RefObject<HTMLIFrameElement | null>,\n onIframeWidthChange?: (width: number) => void,\n): void => {\n useEffect(() => {\n const iframe = iframeRef.current\n if (!iframe || !onIframeWidthChange || typeof ResizeObserver === 'undefined') return\n onIframeWidthChange(iframe.clientWidth)\n const ro = new ResizeObserver((entries) => {\n const w = entries[0]?.contentRect?.width\n if (typeof w === 'number') onIframeWidthChange(Math.round(w))\n })\n ro.observe(iframe)\n return () => ro.disconnect()\n }, [iframeRef, onIframeWidthChange])\n}\n"],"names":["useEffect","useIframeResizeObserver","iframeRef","onIframeWidthChange","iframe","current","ResizeObserver","clientWidth","ro","entries","w","contentRect","width","Math","round","observe","disconnect"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAwB,QAAO;AAEjD,OAAO,MAAMC,0BAA0B,CACrCC,WACAC;IAEAH,UAAU;QACR,MAAMI,SAASF,UAAUG,OAAO;QAChC,IAAI,CAACD,UAAU,CAACD,uBAAuB,OAAOG,mBAAmB,aAAa;QAC9EH,oBAAoBC,OAAOG,WAAW;QACtC,MAAMC,KAAK,IAAIF,eAAe,CAACG;YAC7B,MAAMC,IAAID,OAAO,CAAC,EAAE,EAAEE,aAAaC;YACnC,IAAI,OAAOF,MAAM,UAAUP,oBAAoBU,KAAKC,KAAK,CAACJ;QAC5D;QACAF,GAAGO,OAAO,CAACX;QACX,OAAO,IAAMI,GAAGQ,UAAU;IAC5B,GAAG;QAACd;QAAWC;KAAoB;AACrC,EAAC"}
@@ -0,0 +1,6 @@
1
+ import { type MutableRefObject } from 'react';
2
+ /**
3
+ * Mirror a value into a ref so long-lived listeners / effects can read the
4
+ * latest value without re-binding.
5
+ */
6
+ export declare const useLatestRef: <T>(value: T) => MutableRefObject<T>;
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+ import { useRef } from 'react';
3
+ /**
4
+ * Mirror a value into a ref so long-lived listeners / effects can read the
5
+ * latest value without re-binding.
6
+ */ export const useLatestRef = (value)=>{
7
+ const ref = useRef(value);
8
+ ref.current = value;
9
+ return ref;
10
+ };
11
+
12
+ //# sourceMappingURL=useLatestRef.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useLatestRef.ts"],"sourcesContent":["'use client'\n\nimport { useRef, type MutableRefObject } from 'react'\n\n/**\n * Mirror a value into a ref so long-lived listeners / effects can read the\n * latest value without re-binding.\n */\nexport const useLatestRef = <T>(value: T): MutableRefObject<T> => {\n const ref = useRef(value)\n ref.current = value\n return ref\n}\n"],"names":["useRef","useLatestRef","value","ref","current"],"mappings":"AAAA;AAEA,SAASA,MAAM,QAA+B,QAAO;AAErD;;;CAGC,GACD,OAAO,MAAMC,eAAe,CAAIC;IAC9B,MAAMC,MAAMH,OAAOE;IACnBC,IAAIC,OAAO,GAAGF;IACd,OAAOC;AACT,EAAC"}
@@ -0,0 +1 @@
1
+ export declare const useMainWrapperPortal: (enabled: boolean, adminPortalSelector?: string) => HTMLElement | null;
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ const DEFAULT_ADMIN_WRAPPER_SELECTOR = 'main[class*="collection-edit"] [class*="__main-wrapper"], main[class*="global-edit"] [class*="__main-wrapper"]';
4
+ // WeakSet so multiple plugin instances on the same origin each get their
5
+ // own warning instead of falling silent after the first one fires.
6
+ const warnedDocs = new WeakSet();
7
+ const resolveMountNode = (selector)=>{
8
+ if (typeof document === 'undefined') return null;
9
+ const target = document.querySelector(selector);
10
+ if (target) return target;
11
+ if (process.env.NODE_ENV !== 'production' && !warnedDocs.has(document)) {
12
+ warnedDocs.add(document);
13
+ console.warn(`[better-editor] No element matched "${selector}" — the Payload admin DOM may have changed. Falling back to <main> / <body>. You can override the selector via BetterEditorConfig.adminPortalSelector.`);
14
+ }
15
+ return document.querySelector('main') ?? document.body ?? null;
16
+ };
17
+ export const useMainWrapperPortal = (enabled, adminPortalSelector)=>{
18
+ const [mountNode, setMountNode] = useState(null);
19
+ useEffect(()=>{
20
+ if (!enabled || typeof document === 'undefined') return;
21
+ const main = resolveMountNode(adminPortalSelector ?? DEFAULT_ADMIN_WRAPPER_SELECTOR);
22
+ if (!main) return;
23
+ const html = document.documentElement;
24
+ const body = document.body;
25
+ const prev = {
26
+ position: main.style.position,
27
+ overflow: main.style.overflow,
28
+ height: main.style.height,
29
+ htmlOverflow: html.style.overflow,
30
+ bodyOverflow: body.style.overflow
31
+ };
32
+ if (!main.style.position) main.style.position = 'relative';
33
+ main.style.overflow = 'hidden';
34
+ // Lock outer page scroll so wheel events from iframe/sidebar don't
35
+ // also scroll the admin shell underneath.
36
+ html.style.overflow = 'hidden';
37
+ body.style.overflow = 'hidden';
38
+ // Without this clamp the wrapper grows with the underlying form
39
+ // (~thousands of pixels) and the iframe + sidebar never get their
40
+ // own scroll containers.
41
+ const updateHeight = ()=>{
42
+ const top = main.getBoundingClientRect().top;
43
+ main.style.height = `${Math.max(0, window.innerHeight - top)}px`;
44
+ };
45
+ updateHeight();
46
+ window.addEventListener('resize', updateHeight);
47
+ setMountNode(main);
48
+ return ()=>{
49
+ window.removeEventListener('resize', updateHeight);
50
+ main.style.position = prev.position;
51
+ main.style.overflow = prev.overflow;
52
+ main.style.height = prev.height;
53
+ html.style.overflow = prev.htmlOverflow;
54
+ body.style.overflow = prev.bodyOverflow;
55
+ setMountNode(null);
56
+ };
57
+ }, [
58
+ enabled,
59
+ adminPortalSelector
60
+ ]);
61
+ return mountNode;
62
+ };
63
+
64
+ //# sourceMappingURL=useMainWrapperPortal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useMainWrapperPortal.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\n\nconst DEFAULT_ADMIN_WRAPPER_SELECTOR =\n 'main[class*=\"collection-edit\"] [class*=\"__main-wrapper\"], main[class*=\"global-edit\"] [class*=\"__main-wrapper\"]'\n\n// WeakSet so multiple plugin instances on the same origin each get their\n// own warning instead of falling silent after the first one fires.\nconst warnedDocs = new WeakSet<Document>()\n\nconst resolveMountNode = (selector: string): HTMLElement | null => {\n if (typeof document === 'undefined') return null\n const target = document.querySelector<HTMLElement>(selector)\n if (target) return target\n\n if (process.env.NODE_ENV !== 'production' && !warnedDocs.has(document)) {\n warnedDocs.add(document)\n\n console.warn(\n `[better-editor] No element matched \"${selector}\" — the Payload admin DOM may have changed. Falling back to <main> / <body>. You can override the selector via BetterEditorConfig.adminPortalSelector.`,\n )\n }\n\n return document.querySelector<HTMLElement>('main') ?? document.body ?? null\n}\n\nexport const useMainWrapperPortal = (\n enabled: boolean,\n adminPortalSelector?: string,\n): HTMLElement | null => {\n const [mountNode, setMountNode] = useState<HTMLElement | null>(null)\n\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') return\n\n const main = resolveMountNode(adminPortalSelector ?? DEFAULT_ADMIN_WRAPPER_SELECTOR)\n if (!main) return\n\n const html = document.documentElement\n const body = document.body\n\n const prev = {\n position: main.style.position,\n overflow: main.style.overflow,\n height: main.style.height,\n htmlOverflow: html.style.overflow,\n bodyOverflow: body.style.overflow,\n }\n\n if (!main.style.position) main.style.position = 'relative'\n main.style.overflow = 'hidden'\n\n // Lock outer page scroll so wheel events from iframe/sidebar don't\n // also scroll the admin shell underneath.\n html.style.overflow = 'hidden'\n body.style.overflow = 'hidden'\n\n // Without this clamp the wrapper grows with the underlying form\n // (~thousands of pixels) and the iframe + sidebar never get their\n // own scroll containers.\n const updateHeight = () => {\n const top = main.getBoundingClientRect().top\n main.style.height = `${Math.max(0, window.innerHeight - top)}px`\n }\n updateHeight()\n window.addEventListener('resize', updateHeight)\n\n setMountNode(main)\n\n return () => {\n window.removeEventListener('resize', updateHeight)\n main.style.position = prev.position\n main.style.overflow = prev.overflow\n main.style.height = prev.height\n html.style.overflow = prev.htmlOverflow\n body.style.overflow = prev.bodyOverflow\n setMountNode(null)\n }\n }, [enabled, adminPortalSelector])\n\n return mountNode\n}\n"],"names":["useEffect","useState","DEFAULT_ADMIN_WRAPPER_SELECTOR","warnedDocs","WeakSet","resolveMountNode","selector","document","target","querySelector","process","env","NODE_ENV","has","add","console","warn","body","useMainWrapperPortal","enabled","adminPortalSelector","mountNode","setMountNode","main","html","documentElement","prev","position","style","overflow","height","htmlOverflow","bodyOverflow","updateHeight","top","getBoundingClientRect","Math","max","window","innerHeight","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAE3C,MAAMC,iCACJ;AAEF,yEAAyE;AACzE,mEAAmE;AACnE,MAAMC,aAAa,IAAIC;AAEvB,MAAMC,mBAAmB,CAACC;IACxB,IAAI,OAAOC,aAAa,aAAa,OAAO;IAC5C,MAAMC,SAASD,SAASE,aAAa,CAAcH;IACnD,IAAIE,QAAQ,OAAOA;IAEnB,IAAIE,QAAQC,GAAG,CAACC,QAAQ,KAAK,gBAAgB,CAACT,WAAWU,GAAG,CAACN,WAAW;QACtEJ,WAAWW,GAAG,CAACP;QAEfQ,QAAQC,IAAI,CACV,CAAC,oCAAoC,EAAEV,SAAS,sJAAsJ,CAAC;IAE3M;IAEA,OAAOC,SAASE,aAAa,CAAc,WAAWF,SAASU,IAAI,IAAI;AACzE;AAEA,OAAO,MAAMC,uBAAuB,CAClCC,SACAC;IAEA,MAAM,CAACC,WAAWC,aAAa,GAAGrB,SAA6B;IAE/DD,UAAU;QACR,IAAI,CAACmB,WAAW,OAAOZ,aAAa,aAAa;QAEjD,MAAMgB,OAAOlB,iBAAiBe,uBAAuBlB;QACrD,IAAI,CAACqB,MAAM;QAEX,MAAMC,OAAOjB,SAASkB,eAAe;QACrC,MAAMR,OAAOV,SAASU,IAAI;QAE1B,MAAMS,OAAO;YACXC,UAAUJ,KAAKK,KAAK,CAACD,QAAQ;YAC7BE,UAAUN,KAAKK,KAAK,CAACC,QAAQ;YAC7BC,QAAQP,KAAKK,KAAK,CAACE,MAAM;YACzBC,cAAcP,KAAKI,KAAK,CAACC,QAAQ;YACjCG,cAAcf,KAAKW,KAAK,CAACC,QAAQ;QACnC;QAEA,IAAI,CAACN,KAAKK,KAAK,CAACD,QAAQ,EAAEJ,KAAKK,KAAK,CAACD,QAAQ,GAAG;QAChDJ,KAAKK,KAAK,CAACC,QAAQ,GAAG;QAEtB,mEAAmE;QACnE,0CAA0C;QAC1CL,KAAKI,KAAK,CAACC,QAAQ,GAAG;QACtBZ,KAAKW,KAAK,CAACC,QAAQ,GAAG;QAEtB,gEAAgE;QAChE,kEAAkE;QAClE,yBAAyB;QACzB,MAAMI,eAAe;YACnB,MAAMC,MAAMX,KAAKY,qBAAqB,GAAGD,GAAG;YAC5CX,KAAKK,KAAK,CAACE,MAAM,GAAG,GAAGM,KAAKC,GAAG,CAAC,GAAGC,OAAOC,WAAW,GAAGL,KAAK,EAAE,CAAC;QAClE;QACAD;QACAK,OAAOE,gBAAgB,CAAC,UAAUP;QAElCX,aAAaC;QAEb,OAAO;YACLe,OAAOG,mBAAmB,CAAC,UAAUR;YACrCV,KAAKK,KAAK,CAACD,QAAQ,GAAGD,KAAKC,QAAQ;YACnCJ,KAAKK,KAAK,CAACC,QAAQ,GAAGH,KAAKG,QAAQ;YACnCN,KAAKK,KAAK,CAACE,MAAM,GAAGJ,KAAKI,MAAM;YAC/BN,KAAKI,KAAK,CAACC,QAAQ,GAAGH,KAAKK,YAAY;YACvCd,KAAKW,KAAK,CAACC,QAAQ,GAAGH,KAAKM,YAAY;YACvCV,aAAa;QACf;IACF,GAAG;QAACH;QAASC;KAAoB;IAEjC,OAAOC;AACT,EAAC"}
@@ -0,0 +1,6 @@
1
+ import { useEditorHistory } from '../state/useEditorHistory';
2
+ export type UseOverlayKeyboardArgs = {
3
+ onClose: () => void;
4
+ history: ReturnType<typeof useEditorHistory>;
5
+ };
6
+ export declare const useOverlayKeyboard: ({ onClose, history }: UseOverlayKeyboardArgs) => void;
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ import { useLatestRef } from './useLatestRef';
4
+ const EDITABLE_TAGS = new Set([
5
+ 'INPUT',
6
+ 'TEXTAREA',
7
+ 'SELECT'
8
+ ]);
9
+ // Don't hijack Escape away from text inputs / native dropdowns / rich text
10
+ // editors — users expect it to clear/blur the field first.
11
+ const isEditableTarget = (el)=>!!el && (EDITABLE_TAGS.has(el.tagName) || el.isContentEditable === true);
12
+ export const useOverlayKeyboard = ({ onClose, history })=>{
13
+ const handlersRef = useLatestRef({
14
+ onClose,
15
+ history
16
+ });
17
+ useEffect(()=>{
18
+ const onKey = (e)=>{
19
+ if (e.key === 'Escape') {
20
+ if (isEditableTarget(document.activeElement)) return;
21
+ handlersRef.current.onClose();
22
+ return;
23
+ }
24
+ if (!(e.metaKey || e.ctrlKey)) return;
25
+ const k = e.key.toLowerCase();
26
+ if (k === 'z') {
27
+ e.preventDefault();
28
+ const { history: h } = handlersRef.current;
29
+ if (e.shiftKey) h.redo();
30
+ else h.undo();
31
+ } else if (k === 'y') {
32
+ e.preventDefault();
33
+ handlersRef.current.history.redo();
34
+ }
35
+ };
36
+ window.addEventListener('keydown', onKey);
37
+ return ()=>window.removeEventListener('keydown', onKey);
38
+ }, [
39
+ handlersRef
40
+ ]);
41
+ };
42
+
43
+ //# sourceMappingURL=useOverlayKeyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useOverlayKeyboard.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\nimport { useEditorHistory } from '../state/useEditorHistory'\nimport { useLatestRef } from './useLatestRef'\n\nexport type UseOverlayKeyboardArgs = {\n onClose: () => void\n history: ReturnType<typeof useEditorHistory>\n}\n\nconst EDITABLE_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT'])\n\n// Don't hijack Escape away from text inputs / native dropdowns / rich text\n// editors — users expect it to clear/blur the field first.\nconst isEditableTarget = (el: Element | null): boolean =>\n !!el && (EDITABLE_TAGS.has(el.tagName) || (el as HTMLElement).isContentEditable === true)\n\nexport const useOverlayKeyboard = ({ onClose, history }: UseOverlayKeyboardArgs): void => {\n const handlersRef = useLatestRef({ onClose, history })\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n if (isEditableTarget(document.activeElement)) return\n handlersRef.current.onClose()\n return\n }\n if (!(e.metaKey || e.ctrlKey)) return\n const k = e.key.toLowerCase()\n if (k === 'z') {\n e.preventDefault()\n const { history: h } = handlersRef.current\n if (e.shiftKey) h.redo()\n else h.undo()\n } else if (k === 'y') {\n e.preventDefault()\n handlersRef.current.history.redo()\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [handlersRef])\n}\n"],"names":["useEffect","useLatestRef","EDITABLE_TAGS","Set","isEditableTarget","el","has","tagName","isContentEditable","useOverlayKeyboard","onClose","history","handlersRef","onKey","e","key","document","activeElement","current","metaKey","ctrlKey","k","toLowerCase","preventDefault","h","shiftKey","redo","undo","window","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAQ,QAAO;AAEjC,SAASC,YAAY,QAAQ,iBAAgB;AAO7C,MAAMC,gBAAgB,IAAIC,IAAI;IAAC;IAAS;IAAY;CAAS;AAE7D,2EAA2E;AAC3E,2DAA2D;AAC3D,MAAMC,mBAAmB,CAACC,KACxB,CAAC,CAACA,MAAOH,CAAAA,cAAcI,GAAG,CAACD,GAAGE,OAAO,KAAK,AAACF,GAAmBG,iBAAiB,KAAK,IAAG;AAEzF,OAAO,MAAMC,qBAAqB,CAAC,EAAEC,OAAO,EAAEC,OAAO,EAA0B;IAC7E,MAAMC,cAAcX,aAAa;QAAES;QAASC;IAAQ;IAEpDX,UAAU;QACR,MAAMa,QAAQ,CAACC;YACb,IAAIA,EAAEC,GAAG,KAAK,UAAU;gBACtB,IAAIX,iBAAiBY,SAASC,aAAa,GAAG;gBAC9CL,YAAYM,OAAO,CAACR,OAAO;gBAC3B;YACF;YACA,IAAI,CAAEI,CAAAA,EAAEK,OAAO,IAAIL,EAAEM,OAAO,AAAD,GAAI;YAC/B,MAAMC,IAAIP,EAAEC,GAAG,CAACO,WAAW;YAC3B,IAAID,MAAM,KAAK;gBACbP,EAAES,cAAc;gBAChB,MAAM,EAAEZ,SAASa,CAAC,EAAE,GAAGZ,YAAYM,OAAO;gBAC1C,IAAIJ,EAAEW,QAAQ,EAAED,EAAEE,IAAI;qBACjBF,EAAEG,IAAI;YACb,OAAO,IAAIN,MAAM,KAAK;gBACpBP,EAAES,cAAc;gBAChBX,YAAYM,OAAO,CAACP,OAAO,CAACe,IAAI;YAClC;QACF;QACAE,OAAOC,gBAAgB,CAAC,WAAWhB;QACnC,OAAO,IAAMe,OAAOE,mBAAmB,CAAC,WAAWjB;IACrD,GAAG;QAACD;KAAY;AAClB,EAAC"}
@@ -0,0 +1,28 @@
1
+ import { type RefObject } from 'react';
2
+ import { HoverToolbarController } from '../preview/HoverToolbarController';
3
+ import type { BlockActionMessage } from '../preview/protocol';
4
+ export type PreviewBindingSettings = {
5
+ hoverColorTopLevel: string;
6
+ hoverColorNested: string;
7
+ hoverOutlineWidth: number;
8
+ showHoverToolbar: boolean;
9
+ hoverToolbarPosition: import('../internal/constants').HoverToolbarPosition;
10
+ };
11
+ export type UsePreviewBindingArgs = {
12
+ iframeRef: RefObject<HTMLIFrameElement | null>;
13
+ settings: PreviewBindingSettings;
14
+ interactModeRef: RefObject<boolean>;
15
+ onFocusBlock: (id: string) => void;
16
+ onBlockAction: (id: string, action: BlockActionMessage['action']) => void;
17
+ onLoadingChange: (loading: boolean) => void;
18
+ };
19
+ export type UsePreviewBindingReturn = {
20
+ controllerRef: RefObject<HoverToolbarController | null>;
21
+ isBoundRef: RefObject<boolean>;
22
+ };
23
+ /**
24
+ * Owns the iframe load → install styles + click handler + hover toolbar
25
+ * lifecycle. Idempotent: tears down previous bindings before installing
26
+ * new ones, and unbinds on unmount.
27
+ */
28
+ export declare const usePreviewBinding: ({ iframeRef, settings, interactModeRef, onFocusBlock, onBlockAction, onLoadingChange, }: UsePreviewBindingArgs) => UsePreviewBindingReturn;
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ import { HoverToolbarController } from '../preview/HoverToolbarController';
4
+ import { installClickToFocus } from '../preview/installClickToFocus';
5
+ import { installHoverStyles } from '../preview/installHoverStyles';
6
+ import { BLOCK_ID_SELECTOR } from '../internal/dom';
7
+ import { getSameOriginDocument } from '../internal/iframe';
8
+ import { useLatestRef } from './useLatestRef';
9
+ /**
10
+ * Owns the iframe load → install styles + click handler + hover toolbar
11
+ * lifecycle. Idempotent: tears down previous bindings before installing
12
+ * new ones, and unbinds on unmount.
13
+ */ export const usePreviewBinding = ({ iframeRef, settings, interactModeRef, onFocusBlock, onBlockAction, onLoadingChange })=>{
14
+ const teardownRef = useRef(null);
15
+ const controllerRef = useRef(null);
16
+ const isBoundRef = useRef(false);
17
+ // One-shot flags so dev-only console warnings don't repeat on every
18
+ // iframe re-load during a single editor session.
19
+ const warnedMissingBlocksRef = useRef(false);
20
+ const warnedCrossOriginRef = useRef(false);
21
+ const settingsRef = useLatestRef(settings);
22
+ const onFocusBlockRef = useLatestRef(onFocusBlock);
23
+ const onBlockActionRef = useLatestRef(onBlockAction);
24
+ const onLoadingChangeRef = useLatestRef(onLoadingChange);
25
+ const bindToDocument = useCallback((doc)=>{
26
+ teardownRef.current?.();
27
+ controllerRef.current?.destroy();
28
+ controllerRef.current = null;
29
+ const s = settingsRef.current;
30
+ const removeStyles = installHoverStyles(doc, {
31
+ topColor: s.hoverColorTopLevel,
32
+ nestedColor: s.hoverColorNested,
33
+ outlineWidth: s.hoverOutlineWidth
34
+ });
35
+ const removeClick = installClickToFocus(doc, (id)=>onFocusBlockRef.current(id), {
36
+ isEnabled: ()=>!interactModeRef.current
37
+ });
38
+ if (s.showHoverToolbar) {
39
+ controllerRef.current = new HoverToolbarController(doc, {
40
+ position: s.hoverToolbarPosition,
41
+ outlineWidth: s.hoverOutlineWidth,
42
+ onAction: (id, action)=>onBlockActionRef.current(id, action)
43
+ });
44
+ }
45
+ // Dev-only sanity check: zero [data-better-editor-id] elements means
46
+ // the consumer forgot to spread getBlockProps() on their block wrappers
47
+ // (or the page just has no blocks yet — both look identical from here).
48
+ // Warn at most once per editor session to avoid console spam.
49
+ if (process.env.NODE_ENV !== 'production' && !warnedMissingBlocksRef.current) {
50
+ const blockCount = doc.querySelectorAll(BLOCK_ID_SELECTOR).length;
51
+ if (blockCount === 0) {
52
+ warnedMissingBlocksRef.current = true;
53
+ console.warn("[better-editor] no [data-better-editor-id] elements found in the preview iframe — if your page has blocks, wrap them with `getBlockProps(block)` from 'payload-better-editor/client' so click-to-edit works.");
54
+ }
55
+ }
56
+ isBoundRef.current = true;
57
+ teardownRef.current = ()=>{
58
+ removeStyles();
59
+ removeClick();
60
+ controllerRef.current?.destroy();
61
+ controllerRef.current = null;
62
+ isBoundRef.current = false;
63
+ };
64
+ }, // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable
65
+ []);
66
+ // Bind once on mount; teardown on unmount. All inputs flow through
67
+ // stable refs, so the load listener doesn't need to re-attach.
68
+ useEffect(()=>{
69
+ const iframe = iframeRef.current;
70
+ if (!iframe) return;
71
+ const onLoad = ()=>{
72
+ const doc = getSameOriginDocument(iframe);
73
+ if (!doc) {
74
+ onLoadingChangeRef.current(false);
75
+ if (process.env.NODE_ENV !== 'production' && !warnedCrossOriginRef.current) {
76
+ warnedCrossOriginRef.current = true;
77
+ console.warn('[better-editor] preview iframe is cross-origin — click-to-edit, hover styles, and the in-iframe toolbar are disabled. Serve your preview URL from the same origin as the Payload admin.');
78
+ }
79
+ return;
80
+ }
81
+ // Fresh iframes report readyState='complete' on their initial
82
+ // `about:blank` document before `src` has navigated. Skip and wait
83
+ // for the real `load` event so we don't bind to an empty body.
84
+ const href = doc.location.href;
85
+ if (!href || href === 'about:blank') return;
86
+ onLoadingChangeRef.current(false);
87
+ bindToDocument(doc);
88
+ };
89
+ if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
90
+ onLoad();
91
+ }
92
+ iframe.addEventListener('load', onLoad);
93
+ return ()=>{
94
+ iframe.removeEventListener('load', onLoad);
95
+ teardownRef.current?.();
96
+ teardownRef.current = null;
97
+ controllerRef.current?.destroy();
98
+ controllerRef.current = null;
99
+ };
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable
101
+ }, []);
102
+ return {
103
+ controllerRef,
104
+ isBoundRef
105
+ };
106
+ };
107
+
108
+ //# sourceMappingURL=usePreviewBinding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/usePreviewBinding.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useRef, type RefObject } from 'react'\nimport { HoverToolbarController } from '../preview/HoverToolbarController'\nimport { installClickToFocus } from '../preview/installClickToFocus'\nimport { installHoverStyles } from '../preview/installHoverStyles'\nimport type { BlockActionMessage } from '../preview/protocol'\nimport { BLOCK_ID_SELECTOR } from '../internal/dom'\nimport { getSameOriginDocument } from '../internal/iframe'\nimport { useLatestRef } from './useLatestRef'\n\nexport type PreviewBindingSettings = {\n hoverColorTopLevel: string\n hoverColorNested: string\n hoverOutlineWidth: number\n showHoverToolbar: boolean\n hoverToolbarPosition: import('../internal/constants').HoverToolbarPosition\n}\n\nexport type UsePreviewBindingArgs = {\n iframeRef: RefObject<HTMLIFrameElement | null>\n settings: PreviewBindingSettings\n interactModeRef: RefObject<boolean>\n onFocusBlock: (id: string) => void\n onBlockAction: (id: string, action: BlockActionMessage['action']) => void\n onLoadingChange: (loading: boolean) => void\n}\n\nexport type UsePreviewBindingReturn = {\n controllerRef: RefObject<HoverToolbarController | null>\n isBoundRef: RefObject<boolean>\n}\n\n/**\n * Owns the iframe load → install styles + click handler + hover toolbar\n * lifecycle. Idempotent: tears down previous bindings before installing\n * new ones, and unbinds on unmount.\n */\nexport const usePreviewBinding = ({\n iframeRef,\n settings,\n interactModeRef,\n onFocusBlock,\n onBlockAction,\n onLoadingChange,\n}: UsePreviewBindingArgs): UsePreviewBindingReturn => {\n const teardownRef = useRef<(() => void) | null>(null)\n const controllerRef = useRef<HoverToolbarController | null>(null)\n const isBoundRef = useRef(false)\n // One-shot flags so dev-only console warnings don't repeat on every\n // iframe re-load during a single editor session.\n const warnedMissingBlocksRef = useRef(false)\n const warnedCrossOriginRef = useRef(false)\n const settingsRef = useLatestRef(settings)\n const onFocusBlockRef = useLatestRef(onFocusBlock)\n const onBlockActionRef = useLatestRef(onBlockAction)\n const onLoadingChangeRef = useLatestRef(onLoadingChange)\n\n const bindToDocument = useCallback(\n (doc: Document) => {\n teardownRef.current?.()\n controllerRef.current?.destroy()\n controllerRef.current = null\n\n const s = settingsRef.current\n\n const removeStyles = installHoverStyles(doc, {\n topColor: s.hoverColorTopLevel,\n nestedColor: s.hoverColorNested,\n outlineWidth: s.hoverOutlineWidth,\n })\n const removeClick = installClickToFocus(doc, (id) => onFocusBlockRef.current(id), {\n isEnabled: () => !interactModeRef.current,\n })\n\n if (s.showHoverToolbar) {\n controllerRef.current = new HoverToolbarController(doc, {\n position: s.hoverToolbarPosition,\n outlineWidth: s.hoverOutlineWidth,\n onAction: (id, action) => onBlockActionRef.current(id, action),\n })\n }\n\n // Dev-only sanity check: zero [data-better-editor-id] elements means\n // the consumer forgot to spread getBlockProps() on their block wrappers\n // (or the page just has no blocks yet — both look identical from here).\n // Warn at most once per editor session to avoid console spam.\n if (process.env.NODE_ENV !== 'production' && !warnedMissingBlocksRef.current) {\n const blockCount = doc.querySelectorAll(BLOCK_ID_SELECTOR).length\n if (blockCount === 0) {\n warnedMissingBlocksRef.current = true\n console.warn(\n \"[better-editor] no [data-better-editor-id] elements found in the preview iframe — if your page has blocks, wrap them with `getBlockProps(block)` from 'payload-better-editor/client' so click-to-edit works.\",\n )\n }\n }\n\n isBoundRef.current = true\n teardownRef.current = () => {\n removeStyles()\n removeClick()\n controllerRef.current?.destroy()\n controllerRef.current = null\n isBoundRef.current = false\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n [],\n )\n\n // Bind once on mount; teardown on unmount. All inputs flow through\n // stable refs, so the load listener doesn't need to re-attach.\n useEffect(() => {\n const iframe = iframeRef.current\n if (!iframe) return\n\n const onLoad = () => {\n const doc = getSameOriginDocument(iframe)\n if (!doc) {\n onLoadingChangeRef.current(false)\n if (process.env.NODE_ENV !== 'production' && !warnedCrossOriginRef.current) {\n warnedCrossOriginRef.current = true\n console.warn(\n '[better-editor] preview iframe is cross-origin — click-to-edit, hover styles, and the in-iframe toolbar are disabled. Serve your preview URL from the same origin as the Payload admin.',\n )\n }\n return\n }\n // Fresh iframes report readyState='complete' on their initial\n // `about:blank` document before `src` has navigated. Skip and wait\n // for the real `load` event so we don't bind to an empty body.\n const href = doc.location.href\n if (!href || href === 'about:blank') return\n onLoadingChangeRef.current(false)\n bindToDocument(doc)\n }\n\n if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {\n onLoad()\n }\n iframe.addEventListener('load', onLoad)\n\n return () => {\n iframe.removeEventListener('load', onLoad)\n teardownRef.current?.()\n teardownRef.current = null\n controllerRef.current?.destroy()\n controllerRef.current = null\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n }, [])\n\n return { controllerRef, isBoundRef }\n}\n"],"names":["useCallback","useEffect","useRef","HoverToolbarController","installClickToFocus","installHoverStyles","BLOCK_ID_SELECTOR","getSameOriginDocument","useLatestRef","usePreviewBinding","iframeRef","settings","interactModeRef","onFocusBlock","onBlockAction","onLoadingChange","teardownRef","controllerRef","isBoundRef","warnedMissingBlocksRef","warnedCrossOriginRef","settingsRef","onFocusBlockRef","onBlockActionRef","onLoadingChangeRef","bindToDocument","doc","current","destroy","s","removeStyles","topColor","hoverColorTopLevel","nestedColor","hoverColorNested","outlineWidth","hoverOutlineWidth","removeClick","id","isEnabled","showHoverToolbar","position","hoverToolbarPosition","onAction","action","process","env","NODE_ENV","blockCount","querySelectorAll","length","console","warn","iframe","onLoad","href","location","contentDocument","readyState","addEventListener","removeEventListener"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export type UsePreviewHandleDragOptions = {
3
+ resizable: boolean;
4
+ viewportWidth?: number | null;
5
+ onResize?: (next: number) => void;
6
+ };
7
+ export type UsePreviewHandleDragReturn = {
8
+ isResizing: boolean;
9
+ onHandleMouseDown: (side: 'left' | 'right') => (e: React.MouseEvent) => void;
10
+ };
11
+ export declare const usePreviewHandleDrag: ({ resizable, viewportWidth, onResize, }: UsePreviewHandleDragOptions) => UsePreviewHandleDragReturn;
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { clampViewport } from '../internal/limits';
4
+ export const usePreviewHandleDrag = ({ resizable, viewportWidth, onResize })=>{
5
+ const [isResizing, setIsResizing] = useState(false);
6
+ // Track in-flight drag so unmount mid-drag can release body styles + listeners.
7
+ const dragCleanupRef = useRef(null);
8
+ const isMountedRef = useRef(true);
9
+ useEffect(()=>{
10
+ isMountedRef.current = true;
11
+ return ()=>{
12
+ isMountedRef.current = false;
13
+ dragCleanupRef.current?.();
14
+ };
15
+ }, []);
16
+ const onHandleMouseDown = useCallback((side)=>(e)=>{
17
+ if (!resizable || !onResize || !viewportWidth) return;
18
+ e.preventDefault();
19
+ const startX = e.clientX;
20
+ const startWidth = viewportWidth;
21
+ // Iframe is centered; dragging either edge by N px symmetrically grows
22
+ // the width by 2N. Right handle: positive delta increases width.
23
+ const dir = side === 'right' ? 2 : -2;
24
+ setIsResizing(true);
25
+ const onMove = (ev)=>{
26
+ onResize(clampViewport(startWidth + (ev.clientX - startX) * dir));
27
+ };
28
+ const cleanup = ()=>{
29
+ window.removeEventListener('mousemove', onMove);
30
+ window.removeEventListener('mouseup', onUp);
31
+ document.body.style.cursor = '';
32
+ document.body.style.userSelect = '';
33
+ if (isMountedRef.current) setIsResizing(false);
34
+ dragCleanupRef.current = null;
35
+ };
36
+ const onUp = ()=>cleanup();
37
+ dragCleanupRef.current = cleanup;
38
+ document.body.style.cursor = 'ew-resize';
39
+ document.body.style.userSelect = 'none';
40
+ window.addEventListener('mousemove', onMove);
41
+ window.addEventListener('mouseup', onUp);
42
+ }, [
43
+ resizable,
44
+ onResize,
45
+ viewportWidth
46
+ ]);
47
+ return {
48
+ isResizing,
49
+ onHandleMouseDown
50
+ };
51
+ };
52
+
53
+ //# sourceMappingURL=usePreviewHandleDrag.js.map