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,48 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { DocumentFieldsTab } from './DocumentFieldsTab';
5
+ const isNotSidebar = (f)=>!('admin' in f) || f.admin?.position !== 'sidebar';
6
+ // The blocks tab owns layout-block editing; stripping `blocks` fields here
7
+ // avoids rendering them twice and keeps this tab focused on document meta.
8
+ const stripBlocks = (fields)=>{
9
+ const result = [];
10
+ for (const field of fields){
11
+ const type = field.type;
12
+ if (type === 'blocks') continue;
13
+ if (type === 'tabs') {
14
+ const newTabs = [];
15
+ for (const tab of field.tabs){
16
+ const inner = stripBlocks(tab.fields ?? []);
17
+ if (inner.length > 0) newTabs.push({
18
+ ...tab,
19
+ fields: inner
20
+ });
21
+ }
22
+ if (newTabs.length === 0) continue;
23
+ result.push({
24
+ ...field,
25
+ tabs: newTabs
26
+ });
27
+ continue;
28
+ }
29
+ if (type === 'collapsible' || type === 'row' || type === 'group') {
30
+ const inner = stripBlocks(field.fields);
31
+ if (inner.length === 0) continue;
32
+ result.push({
33
+ ...field,
34
+ fields: inner
35
+ });
36
+ continue;
37
+ }
38
+ result.push(field);
39
+ }
40
+ return result;
41
+ };
42
+ export const DocumentSettingsTab = ()=>/*#__PURE__*/ _jsx(DocumentFieldsTab, {
43
+ filter: isNotSidebar,
44
+ transform: stripBlocks,
45
+ emptyText: "No document-level fields found."
46
+ });
47
+
48
+ //# sourceMappingURL=DocumentSettingsTab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/sidebar/DocumentSettingsTab.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\nimport type { ClientField, ClientTab } from 'payload'\nimport { DocumentFieldsTab } from './DocumentFieldsTab'\n\nconst isNotSidebar = (f: ClientField): boolean =>\n !('admin' in f) || f.admin?.position !== 'sidebar'\n\n// The blocks tab owns layout-block editing; stripping `blocks` fields here\n// avoids rendering them twice and keeps this tab focused on document meta.\nconst stripBlocks = (fields: ClientField[]): ClientField[] => {\n const result: ClientField[] = []\n for (const field of fields) {\n const type = field.type\n\n if (type === 'blocks') continue\n\n if (type === 'tabs') {\n const newTabs: ClientTab[] = []\n for (const tab of field.tabs) {\n const inner = stripBlocks(tab.fields ?? [])\n if (inner.length > 0) newTabs.push({ ...tab, fields: inner })\n }\n if (newTabs.length === 0) continue\n result.push({ ...field, tabs: newTabs })\n continue\n }\n\n if (type === 'collapsible' || type === 'row' || type === 'group') {\n const inner = stripBlocks(field.fields)\n if (inner.length === 0) continue\n result.push({ ...field, fields: inner })\n continue\n }\n\n result.push(field)\n }\n return result\n}\n\nexport const DocumentSettingsTab: React.FC = () => (\n <DocumentFieldsTab\n filter={isNotSidebar}\n transform={stripBlocks}\n emptyText=\"No document-level fields found.\"\n />\n)\n"],"names":["React","DocumentFieldsTab","isNotSidebar","f","admin","position","stripBlocks","fields","result","field","type","newTabs","tab","tabs","inner","length","push","DocumentSettingsTab","filter","transform","emptyText"],"mappings":"AAAA;;AAEA,OAAOA,WAAW,QAAO;AAEzB,SAASC,iBAAiB,QAAQ,sBAAqB;AAEvD,MAAMC,eAAe,CAACC,IACpB,CAAE,CAAA,WAAWA,CAAAA,KAAMA,EAAEC,KAAK,EAAEC,aAAa;AAE3C,2EAA2E;AAC3E,2EAA2E;AAC3E,MAAMC,cAAc,CAACC;IACnB,MAAMC,SAAwB,EAAE;IAChC,KAAK,MAAMC,SAASF,OAAQ;QAC1B,MAAMG,OAAOD,MAAMC,IAAI;QAEvB,IAAIA,SAAS,UAAU;QAEvB,IAAIA,SAAS,QAAQ;YACnB,MAAMC,UAAuB,EAAE;YAC/B,KAAK,MAAMC,OAAOH,MAAMI,IAAI,CAAE;gBAC5B,MAAMC,QAAQR,YAAYM,IAAIL,MAAM,IAAI,EAAE;gBAC1C,IAAIO,MAAMC,MAAM,GAAG,GAAGJ,QAAQK,IAAI,CAAC;oBAAE,GAAGJ,GAAG;oBAAEL,QAAQO;gBAAM;YAC7D;YACA,IAAIH,QAAQI,MAAM,KAAK,GAAG;YAC1BP,OAAOQ,IAAI,CAAC;gBAAE,GAAGP,KAAK;gBAAEI,MAAMF;YAAQ;YACtC;QACF;QAEA,IAAID,SAAS,iBAAiBA,SAAS,SAASA,SAAS,SAAS;YAChE,MAAMI,QAAQR,YAAYG,MAAMF,MAAM;YACtC,IAAIO,MAAMC,MAAM,KAAK,GAAG;YACxBP,OAAOQ,IAAI,CAAC;gBAAE,GAAGP,KAAK;gBAAEF,QAAQO;YAAM;YACtC;QACF;QAEAN,OAAOQ,IAAI,CAACP;IACd;IACA,OAAOD;AACT;AAEA,OAAO,MAAMS,sBAAgC,kBAC3C,KAAChB;QACCiB,QAAQhB;QACRiB,WAAWb;QACXc,WAAU;OAEb"}
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export type SidebarProps = {
3
+ selectedBlockPath: string | null;
4
+ onClearSelection: () => void;
5
+ onSelectPath: (path: string | null) => void;
6
+ forceFullWidthFields: boolean;
7
+ blocksField: string;
8
+ addBelowRequestId?: number;
9
+ };
10
+ export declare const Sidebar: React.FC<SidebarProps>;
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useEffect, useState } from 'react';
4
+ import { DocumentSettingsTab } from './DocumentSettingsTab';
5
+ import { DocumentMetaTab } from './DocumentMetaTab';
6
+ import { BlockSettingsTab } from './BlockSettingsTab';
7
+ const TABS = [
8
+ {
9
+ key: 'page',
10
+ label: 'Page'
11
+ },
12
+ {
13
+ key: 'block',
14
+ label: 'Blocks'
15
+ },
16
+ {
17
+ key: 'settings',
18
+ label: 'Settings'
19
+ }
20
+ ];
21
+ const ROOT_CLASS = 'better-editor-sidebar';
22
+ const TAB_CLASS = `${ROOT_CLASS}__tab`;
23
+ const tabId = (key)=>`better-editor-tab-${key}`;
24
+ const panelId = (key)=>`better-editor-panel-${key}`;
25
+ const SidebarTab = ({ tabKey, label, active, onSelect })=>/*#__PURE__*/ _jsx("button", {
26
+ type: "button",
27
+ role: "tab",
28
+ id: tabId(tabKey),
29
+ "aria-selected": active,
30
+ "aria-controls": panelId(tabKey),
31
+ tabIndex: active ? 0 : -1,
32
+ className: active ? `${TAB_CLASS} ${TAB_CLASS}--active` : TAB_CLASS,
33
+ onClick: ()=>onSelect(tabKey),
34
+ children: label
35
+ });
36
+ export const Sidebar = ({ selectedBlockPath, onClearSelection, onSelectPath, forceFullWidthFields, blocksField, addBelowRequestId = 0 })=>{
37
+ const [tab, setTab] = useState('page');
38
+ // Auto-jump to the block tab when the iframe selects a new block.
39
+ useEffect(()=>{
40
+ if (selectedBlockPath) setTab('block');
41
+ }, [
42
+ selectedBlockPath
43
+ ]);
44
+ const className = forceFullWidthFields ? `${ROOT_CLASS} ${ROOT_CLASS}--force-full-width` : ROOT_CLASS;
45
+ return /*#__PURE__*/ _jsxs("div", {
46
+ className: className,
47
+ children: [
48
+ /*#__PURE__*/ _jsx("div", {
49
+ role: "tablist",
50
+ className: `${ROOT_CLASS}__tabs`,
51
+ children: TABS.map((t)=>/*#__PURE__*/ _jsx(SidebarTab, {
52
+ tabKey: t.key,
53
+ label: t.label,
54
+ active: tab === t.key,
55
+ onSelect: setTab
56
+ }, t.key))
57
+ }),
58
+ /*#__PURE__*/ _jsxs("div", {
59
+ className: `${ROOT_CLASS}__content`,
60
+ role: "tabpanel",
61
+ id: panelId(tab),
62
+ "aria-labelledby": tabId(tab),
63
+ tabIndex: 0,
64
+ children: [
65
+ tab === 'page' && /*#__PURE__*/ _jsx(DocumentSettingsTab, {}),
66
+ tab === 'block' && /*#__PURE__*/ _jsx(BlockSettingsTab, {
67
+ selectedBlockPath: selectedBlockPath,
68
+ onClearSelection: onClearSelection,
69
+ onSelectPath: onSelectPath,
70
+ blocksField: blocksField,
71
+ addBelowRequestId: addBelowRequestId
72
+ }),
73
+ tab === 'settings' && /*#__PURE__*/ _jsx(DocumentMetaTab, {})
74
+ ]
75
+ }),
76
+ /*#__PURE__*/ _jsx(SelectionAnnouncer, {
77
+ selectedBlockPath: selectedBlockPath
78
+ })
79
+ ]
80
+ });
81
+ };
82
+ // Off-screen live region announces block-selection changes to screen
83
+ // readers; visible UI updates handle sighted users via the tab switch.
84
+ const SelectionAnnouncer = ({ selectedBlockPath })=>/*#__PURE__*/ _jsx("div", {
85
+ role: "status",
86
+ "aria-live": "polite",
87
+ "aria-atomic": "true",
88
+ className: "better-editor-sr-only",
89
+ children: selectedBlockPath ? `Block selected: ${selectedBlockPath}` : 'No block selected'
90
+ });
91
+
92
+ //# sourceMappingURL=Sidebar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/sidebar/Sidebar.tsx"],"sourcesContent":["'use client'\n\nimport React, { useEffect, useState } from 'react'\nimport { DocumentSettingsTab } from './DocumentSettingsTab'\nimport { DocumentMetaTab } from './DocumentMetaTab'\nimport { BlockSettingsTab } from './BlockSettingsTab'\n\nexport type SidebarProps = {\n selectedBlockPath: string | null\n onClearSelection: () => void\n onSelectPath: (path: string | null) => void\n forceFullWidthFields: boolean\n blocksField: string\n addBelowRequestId?: number\n}\n\ntype TabKey = 'page' | 'block' | 'settings'\n\nconst TABS: ReadonlyArray<{ key: TabKey; label: string }> = [\n { key: 'page', label: 'Page' },\n { key: 'block', label: 'Blocks' },\n { key: 'settings', label: 'Settings' },\n]\n\nconst ROOT_CLASS = 'better-editor-sidebar'\nconst TAB_CLASS = `${ROOT_CLASS}__tab`\n\nconst tabId = (key: TabKey) => `better-editor-tab-${key}`\nconst panelId = (key: TabKey) => `better-editor-panel-${key}`\n\ntype SidebarTabProps = {\n tabKey: TabKey\n label: string\n active: boolean\n onSelect: (key: TabKey) => void\n}\n\nconst SidebarTab: React.FC<SidebarTabProps> = ({ tabKey, label, active, onSelect }) => (\n <button\n type=\"button\"\n role=\"tab\"\n id={tabId(tabKey)}\n aria-selected={active}\n aria-controls={panelId(tabKey)}\n tabIndex={active ? 0 : -1}\n className={active ? `${TAB_CLASS} ${TAB_CLASS}--active` : TAB_CLASS}\n onClick={() => onSelect(tabKey)}\n >\n {label}\n </button>\n)\n\nexport const Sidebar: React.FC<SidebarProps> = ({\n selectedBlockPath,\n onClearSelection,\n onSelectPath,\n forceFullWidthFields,\n blocksField,\n addBelowRequestId = 0,\n}) => {\n const [tab, setTab] = useState<TabKey>('page')\n\n // Auto-jump to the block tab when the iframe selects a new block.\n useEffect(() => {\n if (selectedBlockPath) setTab('block')\n }, [selectedBlockPath])\n\n const className = forceFullWidthFields\n ? `${ROOT_CLASS} ${ROOT_CLASS}--force-full-width`\n : ROOT_CLASS\n\n return (\n <div className={className}>\n <div role=\"tablist\" className={`${ROOT_CLASS}__tabs`}>\n {TABS.map((t) => (\n <SidebarTab\n key={t.key}\n tabKey={t.key}\n label={t.label}\n active={tab === t.key}\n onSelect={setTab}\n />\n ))}\n </div>\n\n <div\n className={`${ROOT_CLASS}__content`}\n role=\"tabpanel\"\n id={panelId(tab)}\n aria-labelledby={tabId(tab)}\n tabIndex={0}\n >\n {tab === 'page' && <DocumentSettingsTab />}\n {tab === 'block' && (\n <BlockSettingsTab\n selectedBlockPath={selectedBlockPath}\n onClearSelection={onClearSelection}\n onSelectPath={onSelectPath}\n blocksField={blocksField}\n addBelowRequestId={addBelowRequestId}\n />\n )}\n {tab === 'settings' && <DocumentMetaTab />}\n </div>\n\n <SelectionAnnouncer selectedBlockPath={selectedBlockPath} />\n </div>\n )\n}\n\n// Off-screen live region announces block-selection changes to screen\n// readers; visible UI updates handle sighted users via the tab switch.\nconst SelectionAnnouncer: React.FC<{ selectedBlockPath: string | null }> = ({\n selectedBlockPath,\n}) => (\n <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"better-editor-sr-only\">\n {selectedBlockPath ? `Block selected: ${selectedBlockPath}` : 'No block selected'}\n </div>\n)\n"],"names":["React","useEffect","useState","DocumentSettingsTab","DocumentMetaTab","BlockSettingsTab","TABS","key","label","ROOT_CLASS","TAB_CLASS","tabId","panelId","SidebarTab","tabKey","active","onSelect","button","type","role","id","aria-selected","aria-controls","tabIndex","className","onClick","Sidebar","selectedBlockPath","onClearSelection","onSelectPath","forceFullWidthFields","blocksField","addBelowRequestId","tab","setTab","div","map","t","aria-labelledby","SelectionAnnouncer","aria-live","aria-atomic"],"mappings":"AAAA;;AAEA,OAAOA,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAClD,SAASC,mBAAmB,QAAQ,wBAAuB;AAC3D,SAASC,eAAe,QAAQ,oBAAmB;AACnD,SAASC,gBAAgB,QAAQ,qBAAoB;AAarD,MAAMC,OAAsD;IAC1D;QAAEC,KAAK;QAAQC,OAAO;IAAO;IAC7B;QAAED,KAAK;QAASC,OAAO;IAAS;IAChC;QAAED,KAAK;QAAYC,OAAO;IAAW;CACtC;AAED,MAAMC,aAAa;AACnB,MAAMC,YAAY,GAAGD,WAAW,KAAK,CAAC;AAEtC,MAAME,QAAQ,CAACJ,MAAgB,CAAC,kBAAkB,EAAEA,KAAK;AACzD,MAAMK,UAAU,CAACL,MAAgB,CAAC,oBAAoB,EAAEA,KAAK;AAS7D,MAAMM,aAAwC,CAAC,EAAEC,MAAM,EAAEN,KAAK,EAAEO,MAAM,EAAEC,QAAQ,EAAE,iBAChF,KAACC;QACCC,MAAK;QACLC,MAAK;QACLC,IAAIT,MAAMG;QACVO,iBAAeN;QACfO,iBAAeV,QAAQE;QACvBS,UAAUR,SAAS,IAAI,CAAC;QACxBS,WAAWT,SAAS,GAAGL,UAAU,CAAC,EAAEA,UAAU,QAAQ,CAAC,GAAGA;QAC1De,SAAS,IAAMT,SAASF;kBAEvBN;;AAIL,OAAO,MAAMkB,UAAkC,CAAC,EAC9CC,iBAAiB,EACjBC,gBAAgB,EAChBC,YAAY,EACZC,oBAAoB,EACpBC,WAAW,EACXC,oBAAoB,CAAC,EACtB;IACC,MAAM,CAACC,KAAKC,OAAO,GAAGhC,SAAiB;IAEvC,kEAAkE;IAClED,UAAU;QACR,IAAI0B,mBAAmBO,OAAO;IAChC,GAAG;QAACP;KAAkB;IAEtB,MAAMH,YAAYM,uBACd,GAAGrB,WAAW,CAAC,EAAEA,WAAW,kBAAkB,CAAC,GAC/CA;IAEJ,qBACE,MAAC0B;QAAIX,WAAWA;;0BACd,KAACW;gBAAIhB,MAAK;gBAAUK,WAAW,GAAGf,WAAW,MAAM,CAAC;0BACjDH,KAAK8B,GAAG,CAAC,CAACC,kBACT,KAACxB;wBAECC,QAAQuB,EAAE9B,GAAG;wBACbC,OAAO6B,EAAE7B,KAAK;wBACdO,QAAQkB,QAAQI,EAAE9B,GAAG;wBACrBS,UAAUkB;uBAJLG,EAAE9B,GAAG;;0BAShB,MAAC4B;gBACCX,WAAW,GAAGf,WAAW,SAAS,CAAC;gBACnCU,MAAK;gBACLC,IAAIR,QAAQqB;gBACZK,mBAAiB3B,MAAMsB;gBACvBV,UAAU;;oBAETU,QAAQ,wBAAU,KAAC9B;oBACnB8B,QAAQ,yBACP,KAAC5B;wBACCsB,mBAAmBA;wBACnBC,kBAAkBA;wBAClBC,cAAcA;wBACdE,aAAaA;wBACbC,mBAAmBA;;oBAGtBC,QAAQ,4BAAc,KAAC7B;;;0BAG1B,KAACmC;gBAAmBZ,mBAAmBA;;;;AAG7C,EAAC;AAED,qEAAqE;AACrE,uEAAuE;AACvE,MAAMY,qBAAqE,CAAC,EAC1EZ,iBAAiB,EAClB,iBACC,KAACQ;QAAIhB,MAAK;QAASqB,aAAU;QAASC,eAAY;QAAOjB,WAAU;kBAChEG,oBAAoB,CAAC,gBAAgB,EAAEA,mBAAmB,GAAG"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The button rendered in `beforeDocumentControls` that toggles the
3
+ * Better Editor overlay. Auto-injected by the plugin factory; consumers
4
+ * usually don't render it directly.
5
+ */
6
+ export { LiveEditorToggle } from './admin/LiveEditorToggle';
7
+ export type { LiveEditorToggleProps } from './admin/LiveEditorToggle';
8
+ /**
9
+ * The full editor overlay (preview iframe + 3-tab sidebar). Mounted into
10
+ * the Payload admin shell when the toggle opens it. Exported so consumers
11
+ * can build a custom toggle / wrapper if they need to bypass the
12
+ * auto-injected button.
13
+ */
14
+ export { LiveEditorOverlay } from './admin/LiveEditorOverlay';
15
+ export type { LiveEditorOverlayProps } from './admin/LiveEditorOverlay';
16
+ /**
17
+ * Banner shown at the top of the `BetterEditorSettings` global. Wired up
18
+ * automatically; hide it via `betterEditor({ showSettingsBanner: false })`.
19
+ */
20
+ export { SettingsBanner } from './admin/SettingsBanner';
21
+ /**
22
+ * Spread these props on every block wrapper in your frontend so the
23
+ * Better Editor can target it. The plugin uses the resulting
24
+ * `data-better-editor-id` attribute for hover outlines, click-to-focus,
25
+ * the in-iframe action toolbar, and selection sync.
26
+ *
27
+ * @example
28
+ * import { getBlockProps } from 'payload-better-editor/client'
29
+ *
30
+ * <section {...getBlockProps(block)}>...</section>
31
+ */
32
+ export declare const getBlockProps: (block: {
33
+ id?: string | null;
34
+ }) => Record<string, string>;
package/dist/client.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The button rendered in `beforeDocumentControls` that toggles the
3
+ * Better Editor overlay. Auto-injected by the plugin factory; consumers
4
+ * usually don't render it directly.
5
+ */ export { LiveEditorToggle } from './admin/LiveEditorToggle';
6
+ /**
7
+ * The full editor overlay (preview iframe + 3-tab sidebar). Mounted into
8
+ * the Payload admin shell when the toggle opens it. Exported so consumers
9
+ * can build a custom toggle / wrapper if they need to bypass the
10
+ * auto-injected button.
11
+ */ export { LiveEditorOverlay } from './admin/LiveEditorOverlay';
12
+ /**
13
+ * Banner shown at the top of the `BetterEditorSettings` global. Wired up
14
+ * automatically; hide it via `betterEditor({ showSettingsBanner: false })`.
15
+ */ export { SettingsBanner } from './admin/SettingsBanner';
16
+ /**
17
+ * Spread these props on every block wrapper in your frontend so the
18
+ * Better Editor can target it. The plugin uses the resulting
19
+ * `data-better-editor-id` attribute for hover outlines, click-to-focus,
20
+ * the in-iframe action toolbar, and selection sync.
21
+ *
22
+ * @example
23
+ * import { getBlockProps } from 'payload-better-editor/client'
24
+ *
25
+ * <section {...getBlockProps(block)}>...</section>
26
+ */ export const getBlockProps = (block)=>block.id ? {
27
+ 'data-better-editor-id': block.id
28
+ } : {};
29
+
30
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * The button rendered in `beforeDocumentControls` that toggles the\n * Better Editor overlay. Auto-injected by the plugin factory; consumers\n * usually don't render it directly.\n */\nexport { LiveEditorToggle } from './admin/LiveEditorToggle'\nexport type { LiveEditorToggleProps } from './admin/LiveEditorToggle'\n\n/**\n * The full editor overlay (preview iframe + 3-tab sidebar). Mounted into\n * the Payload admin shell when the toggle opens it. Exported so consumers\n * can build a custom toggle / wrapper if they need to bypass the\n * auto-injected button.\n */\nexport { LiveEditorOverlay } from './admin/LiveEditorOverlay'\nexport type { LiveEditorOverlayProps } from './admin/LiveEditorOverlay'\n\n/**\n * Banner shown at the top of the `BetterEditorSettings` global. Wired up\n * automatically; hide it via `betterEditor({ showSettingsBanner: false })`.\n */\nexport { SettingsBanner } from './admin/SettingsBanner'\n\n/**\n * Spread these props on every block wrapper in your frontend so the\n * Better Editor can target it. The plugin uses the resulting\n * `data-better-editor-id` attribute for hover outlines, click-to-focus,\n * the in-iframe action toolbar, and selection sync.\n *\n * @example\n * import { getBlockProps } from 'payload-better-editor/client'\n *\n * <section {...getBlockProps(block)}>...</section>\n */\nexport const getBlockProps = (\n block: { id?: string | null },\n): Record<string, string> =>\n block.id ? { 'data-better-editor-id': block.id } : {}\n"],"names":["LiveEditorToggle","LiveEditorOverlay","SettingsBanner","getBlockProps","block","id"],"mappings":"AAAA;;;;CAIC,GACD,SAASA,gBAAgB,QAAQ,2BAA0B;AAG3D;;;;;CAKC,GACD,SAASC,iBAAiB,QAAQ,4BAA2B;AAG7D;;;CAGC,GACD,SAASC,cAAc,QAAQ,yBAAwB;AAEvD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,gBAAgB,CAC3BC,QAEAA,MAAMC,EAAE,GAAG;QAAE,yBAAyBD,MAAMC,EAAE;IAAC,IAAI,CAAC,EAAC"}
@@ -0,0 +1,4 @@
1
+ import type { GlobalConfig } from 'payload';
2
+ export declare const BETTER_EDITOR_SETTINGS_SLUG = "better-editor-settings";
3
+ export declare const BETTER_EDITOR_SETTINGS_BANNER_FIELD = "betterEditorSettingsBanner";
4
+ export declare const betterEditorSettingsGlobal: GlobalConfig;
package/dist/global.js ADDED
@@ -0,0 +1,200 @@
1
+ import { DEFAULT_BETTER_EDITOR_SETTINGS as D } from './internal/constants';
2
+ import { HOVER_OUTLINE_MAX, HOVER_OUTLINE_MIN, MOBILE_WIDTH_MAX, MOBILE_WIDTH_MIN, TABLET_WIDTH_MAX, TABLET_WIDTH_MIN } from './internal/limits';
3
+ export const BETTER_EDITOR_SETTINGS_SLUG = 'better-editor-settings';
4
+ export const BETTER_EDITOR_SETTINGS_BANNER_FIELD = 'betterEditorSettingsBanner';
5
+ // Mirrors the runtime regex in `preview/hover-css.ts` so the admin UI rejects
6
+ // values that would silently be skipped at render time.
7
+ const HOVER_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|rgba?\([^()\n\r]*\))$/i;
8
+ const validateHoverColor = (value)=>{
9
+ if (typeof value !== 'string' || value.length === 0) return 'Color is required';
10
+ if (!HOVER_COLOR_RE.test(value.trim())) {
11
+ return 'Must be a hex color (e.g. #3b82f6) or rgb()/rgba()';
12
+ }
13
+ return true;
14
+ };
15
+ const validateHoverOutline = (value)=>{
16
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 'Must be a number';
17
+ if (value < HOVER_OUTLINE_MIN || value > HOVER_OUTLINE_MAX) {
18
+ return `Must be between ${HOVER_OUTLINE_MIN} and ${HOVER_OUTLINE_MAX}`;
19
+ }
20
+ return true;
21
+ };
22
+ export const betterEditorSettingsGlobal = {
23
+ slug: BETTER_EDITOR_SETTINGS_SLUG,
24
+ label: 'Settings',
25
+ access: {
26
+ read: ()=>true
27
+ },
28
+ admin: {
29
+ group: 'Better Editor',
30
+ description: 'Editor-wide preferences for the Better Editor overlay.'
31
+ },
32
+ fields: [
33
+ {
34
+ name: 'betterEditorSettingsBanner',
35
+ type: 'ui',
36
+ admin: {
37
+ components: {
38
+ Field: 'payload-better-editor/client#SettingsBanner'
39
+ }
40
+ }
41
+ },
42
+ {
43
+ type: 'tabs',
44
+ tabs: [
45
+ {
46
+ label: 'Sidebar',
47
+ description: 'Where the sidebar sits and how its fields are stacked.',
48
+ fields: [
49
+ {
50
+ name: 'sidebarPosition',
51
+ type: 'select',
52
+ label: 'Position',
53
+ defaultValue: D.sidebarPosition,
54
+ options: [
55
+ {
56
+ label: 'Right',
57
+ value: 'right'
58
+ },
59
+ {
60
+ label: 'Left',
61
+ value: 'left'
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ name: 'forceFullWidthFields',
67
+ type: 'checkbox',
68
+ label: 'Stack fields full-width',
69
+ defaultValue: D.forceFullWidthFields,
70
+ admin: {
71
+ description: 'Override admin.width on sidebar fields so they always span the full row.'
72
+ }
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ label: 'Viewport',
78
+ description: 'Pixel widths for the Tablet and Mobile preview modes.',
79
+ fields: [
80
+ {
81
+ type: 'row',
82
+ fields: [
83
+ {
84
+ name: 'tabletWidth',
85
+ type: 'number',
86
+ label: 'Tablet (px)',
87
+ defaultValue: D.tabletWidth,
88
+ min: TABLET_WIDTH_MIN,
89
+ max: TABLET_WIDTH_MAX,
90
+ admin: {
91
+ width: '50%',
92
+ placeholder: String(D.tabletWidth)
93
+ }
94
+ },
95
+ {
96
+ name: 'mobileWidth',
97
+ type: 'number',
98
+ label: 'Mobile (px)',
99
+ defaultValue: D.mobileWidth,
100
+ min: MOBILE_WIDTH_MIN,
101
+ max: MOBILE_WIDTH_MAX,
102
+ admin: {
103
+ width: '50%',
104
+ placeholder: String(D.mobileWidth)
105
+ }
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ label: 'Outline',
113
+ description: 'Outline + tint shown on the hovered or selected block.',
114
+ fields: [
115
+ {
116
+ type: 'row',
117
+ fields: [
118
+ {
119
+ name: 'hoverColorTopLevel',
120
+ type: 'text',
121
+ label: 'Top-level color',
122
+ defaultValue: D.hoverColorTopLevel,
123
+ validate: validateHoverColor,
124
+ admin: {
125
+ width: '50%',
126
+ placeholder: D.hoverColorTopLevel,
127
+ description: 'Hex color (e.g. `#3b82f6`).'
128
+ }
129
+ },
130
+ {
131
+ name: 'hoverColorNested',
132
+ type: 'text',
133
+ label: 'Nested color',
134
+ defaultValue: D.hoverColorNested,
135
+ validate: validateHoverColor,
136
+ admin: {
137
+ width: '50%',
138
+ placeholder: D.hoverColorNested,
139
+ description: 'Hex color for blocks nested inside another block.'
140
+ }
141
+ }
142
+ ]
143
+ },
144
+ {
145
+ name: 'hoverOutlineWidth',
146
+ type: 'number',
147
+ label: 'Outline width (px)',
148
+ defaultValue: D.hoverOutlineWidth,
149
+ min: HOVER_OUTLINE_MIN,
150
+ max: HOVER_OUTLINE_MAX,
151
+ validate: validateHoverOutline,
152
+ admin: {
153
+ placeholder: String(D.hoverOutlineWidth),
154
+ description: `Outline thickness in pixels (${HOVER_OUTLINE_MIN}–${HOVER_OUTLINE_MAX}).`
155
+ }
156
+ }
157
+ ]
158
+ },
159
+ {
160
+ label: 'Toolbar',
161
+ description: 'Floating Move / Duplicate / Delete toolbar that appears on the selected block.',
162
+ fields: [
163
+ {
164
+ name: 'showHoverToolbar',
165
+ type: 'checkbox',
166
+ label: 'Enabled',
167
+ defaultValue: D.showHoverToolbar
168
+ },
169
+ {
170
+ name: 'hoverToolbarPosition',
171
+ type: 'select',
172
+ label: 'Anchor corner',
173
+ defaultValue: D.hoverToolbarPosition,
174
+ options: [
175
+ {
176
+ label: 'Top right',
177
+ value: 'top-right'
178
+ },
179
+ {
180
+ label: 'Top left',
181
+ value: 'top-left'
182
+ },
183
+ {
184
+ label: 'Bottom right',
185
+ value: 'bottom-right'
186
+ },
187
+ {
188
+ label: 'Bottom left',
189
+ value: 'bottom-left'
190
+ }
191
+ ]
192
+ }
193
+ ]
194
+ }
195
+ ]
196
+ }
197
+ ]
198
+ };
199
+
200
+ //# sourceMappingURL=global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/global.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\nimport { DEFAULT_BETTER_EDITOR_SETTINGS as D } from './internal/constants'\nimport {\n HOVER_OUTLINE_MAX,\n HOVER_OUTLINE_MIN,\n MOBILE_WIDTH_MAX,\n MOBILE_WIDTH_MIN,\n TABLET_WIDTH_MAX,\n TABLET_WIDTH_MIN,\n} from './internal/limits'\n\nexport const BETTER_EDITOR_SETTINGS_SLUG = 'better-editor-settings'\n\nexport const BETTER_EDITOR_SETTINGS_BANNER_FIELD = 'betterEditorSettingsBanner'\n\n// Mirrors the runtime regex in `preview/hover-css.ts` so the admin UI rejects\n// values that would silently be skipped at render time.\nconst HOVER_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|rgba?\\([^()\\n\\r]*\\))$/i\n\nconst validateHoverColor = (value: unknown): string | true => {\n if (typeof value !== 'string' || value.length === 0) return 'Color is required'\n if (!HOVER_COLOR_RE.test(value.trim())) {\n return 'Must be a hex color (e.g. #3b82f6) or rgb()/rgba()'\n }\n return true\n}\n\nconst validateHoverOutline = (value: unknown): string | true => {\n if (typeof value !== 'number' || !Number.isFinite(value)) return 'Must be a number'\n if (value < HOVER_OUTLINE_MIN || value > HOVER_OUTLINE_MAX) {\n return `Must be between ${HOVER_OUTLINE_MIN} and ${HOVER_OUTLINE_MAX}`\n }\n return true\n}\n\nexport const betterEditorSettingsGlobal: GlobalConfig = {\n slug: BETTER_EDITOR_SETTINGS_SLUG,\n label: 'Settings',\n access: {\n read: () => true,\n },\n admin: {\n group: 'Better Editor',\n description: 'Editor-wide preferences for the Better Editor overlay.',\n },\n\n fields: [\n {\n name: 'betterEditorSettingsBanner',\n type: 'ui',\n admin: {\n components: {\n Field: 'payload-better-editor/client#SettingsBanner',\n },\n },\n },\n {\n type: 'tabs',\n tabs: [\n {\n label: 'Sidebar',\n description: 'Where the sidebar sits and how its fields are stacked.',\n fields: [\n {\n name: 'sidebarPosition',\n type: 'select',\n label: 'Position',\n defaultValue: D.sidebarPosition,\n options: [\n { label: 'Right', value: 'right' },\n { label: 'Left', value: 'left' },\n ],\n },\n {\n name: 'forceFullWidthFields',\n type: 'checkbox',\n label: 'Stack fields full-width',\n defaultValue: D.forceFullWidthFields,\n admin: {\n description:\n 'Override admin.width on sidebar fields so they always span the full row.',\n },\n },\n ],\n },\n {\n label: 'Viewport',\n description: 'Pixel widths for the Tablet and Mobile preview modes.',\n fields: [\n {\n type: 'row',\n fields: [\n {\n name: 'tabletWidth',\n type: 'number',\n label: 'Tablet (px)',\n defaultValue: D.tabletWidth,\n min: TABLET_WIDTH_MIN,\n max: TABLET_WIDTH_MAX,\n admin: { width: '50%', placeholder: String(D.tabletWidth) },\n },\n {\n name: 'mobileWidth',\n type: 'number',\n label: 'Mobile (px)',\n defaultValue: D.mobileWidth,\n min: MOBILE_WIDTH_MIN,\n max: MOBILE_WIDTH_MAX,\n admin: { width: '50%', placeholder: String(D.mobileWidth) },\n },\n ],\n },\n ],\n },\n {\n label: 'Outline',\n description: 'Outline + tint shown on the hovered or selected block.',\n fields: [\n {\n type: 'row',\n fields: [\n {\n name: 'hoverColorTopLevel',\n type: 'text',\n label: 'Top-level color',\n defaultValue: D.hoverColorTopLevel,\n validate: validateHoverColor,\n admin: {\n width: '50%',\n placeholder: D.hoverColorTopLevel,\n description: 'Hex color (e.g. `#3b82f6`).',\n },\n },\n {\n name: 'hoverColorNested',\n type: 'text',\n label: 'Nested color',\n defaultValue: D.hoverColorNested,\n validate: validateHoverColor,\n admin: {\n width: '50%',\n placeholder: D.hoverColorNested,\n description: 'Hex color for blocks nested inside another block.',\n },\n },\n ],\n },\n {\n name: 'hoverOutlineWidth',\n type: 'number',\n label: 'Outline width (px)',\n defaultValue: D.hoverOutlineWidth,\n min: HOVER_OUTLINE_MIN,\n max: HOVER_OUTLINE_MAX,\n validate: validateHoverOutline,\n admin: {\n placeholder: String(D.hoverOutlineWidth),\n description: `Outline thickness in pixels (${HOVER_OUTLINE_MIN}–${HOVER_OUTLINE_MAX}).`,\n },\n },\n ],\n },\n {\n label: 'Toolbar',\n description:\n 'Floating Move / Duplicate / Delete toolbar that appears on the selected block.',\n fields: [\n {\n name: 'showHoverToolbar',\n type: 'checkbox',\n label: 'Enabled',\n defaultValue: D.showHoverToolbar,\n },\n {\n name: 'hoverToolbarPosition',\n type: 'select',\n label: 'Anchor corner',\n defaultValue: D.hoverToolbarPosition,\n options: [\n { label: 'Top right', value: 'top-right' },\n { label: 'Top left', value: 'top-left' },\n { label: 'Bottom right', value: 'bottom-right' },\n { label: 'Bottom left', value: 'bottom-left' },\n ],\n },\n ],\n },\n ],\n },\n ],\n}\n"],"names":["DEFAULT_BETTER_EDITOR_SETTINGS","D","HOVER_OUTLINE_MAX","HOVER_OUTLINE_MIN","MOBILE_WIDTH_MAX","MOBILE_WIDTH_MIN","TABLET_WIDTH_MAX","TABLET_WIDTH_MIN","BETTER_EDITOR_SETTINGS_SLUG","BETTER_EDITOR_SETTINGS_BANNER_FIELD","HOVER_COLOR_RE","validateHoverColor","value","length","test","trim","validateHoverOutline","Number","isFinite","betterEditorSettingsGlobal","slug","label","access","read","admin","group","description","fields","name","type","components","Field","tabs","defaultValue","sidebarPosition","options","forceFullWidthFields","tabletWidth","min","max","width","placeholder","String","mobileWidth","hoverColorTopLevel","validate","hoverColorNested","hoverOutlineWidth","showHoverToolbar","hoverToolbarPosition"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export type UseAddBlockDrawerArgs = {
2
+ drawerSlug: string;
3
+ /** Bumped externally to request the drawer to open. */
4
+ requestId: number;
5
+ /** Drawer should only open when a block is currently selected. */
6
+ selectedBlockPath: string | null;
7
+ };
8
+ /**
9
+ * Defers `toggleModal(drawerSlug)` to the next paint so the drawer is
10
+ * mounted before we open it (the sidebar may have just auto-switched to
11
+ * the Blocks tab). De-duplicates by `requestId` so a stale id can't
12
+ * re-trigger after the drawer has been closed.
13
+ */
14
+ export declare const useAddBlockDrawer: ({ drawerSlug, requestId, selectedBlockPath, }: UseAddBlockDrawerArgs) => void;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { useEffect, useRef } from 'react';
3
+ import { useModal } from '@payloadcms/ui';
4
+ /**
5
+ * Defers `toggleModal(drawerSlug)` to the next paint so the drawer is
6
+ * mounted before we open it (the sidebar may have just auto-switched to
7
+ * the Blocks tab). De-duplicates by `requestId` so a stale id can't
8
+ * re-trigger after the drawer has been closed.
9
+ */ export const useAddBlockDrawer = ({ drawerSlug, requestId, selectedBlockPath })=>{
10
+ const { toggleModal } = useModal();
11
+ const lastHandledRef = useRef(0);
12
+ useEffect(()=>{
13
+ if (!requestId || requestId === lastHandledRef.current) return;
14
+ if (!selectedBlockPath) return;
15
+ lastHandledRef.current = requestId;
16
+ const raf = requestAnimationFrame(()=>toggleModal(drawerSlug));
17
+ return ()=>cancelAnimationFrame(raf);
18
+ }, [
19
+ requestId,
20
+ selectedBlockPath,
21
+ toggleModal,
22
+ drawerSlug
23
+ ]);
24
+ };
25
+
26
+ //# sourceMappingURL=useAddBlockDrawer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useAddBlockDrawer.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { useModal } from '@payloadcms/ui'\n\nexport type UseAddBlockDrawerArgs = {\n drawerSlug: string\n /** Bumped externally to request the drawer to open. */\n requestId: number\n /** Drawer should only open when a block is currently selected. */\n selectedBlockPath: string | null\n}\n\n/**\n * Defers `toggleModal(drawerSlug)` to the next paint so the drawer is\n * mounted before we open it (the sidebar may have just auto-switched to\n * the Blocks tab). De-duplicates by `requestId` so a stale id can't\n * re-trigger after the drawer has been closed.\n */\nexport const useAddBlockDrawer = ({\n drawerSlug,\n requestId,\n selectedBlockPath,\n}: UseAddBlockDrawerArgs): void => {\n const { toggleModal } = useModal()\n const lastHandledRef = useRef(0)\n\n useEffect(() => {\n if (!requestId || requestId === lastHandledRef.current) return\n if (!selectedBlockPath) return\n lastHandledRef.current = requestId\n const raf = requestAnimationFrame(() => toggleModal(drawerSlug))\n return () => cancelAnimationFrame(raf)\n }, [requestId, selectedBlockPath, toggleModal, drawerSlug])\n}\n"],"names":["useEffect","useRef","useModal","useAddBlockDrawer","drawerSlug","requestId","selectedBlockPath","toggleModal","lastHandledRef","current","raf","requestAnimationFrame","cancelAnimationFrame"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,MAAM,QAAQ,QAAO;AACzC,SAASC,QAAQ,QAAQ,iBAAgB;AAUzC;;;;;CAKC,GACD,OAAO,MAAMC,oBAAoB,CAAC,EAChCC,UAAU,EACVC,SAAS,EACTC,iBAAiB,EACK;IACtB,MAAM,EAAEC,WAAW,EAAE,GAAGL;IACxB,MAAMM,iBAAiBP,OAAO;IAE9BD,UAAU;QACR,IAAI,CAACK,aAAaA,cAAcG,eAAeC,OAAO,EAAE;QACxD,IAAI,CAACH,mBAAmB;QACxBE,eAAeC,OAAO,GAAGJ;QACzB,MAAMK,MAAMC,sBAAsB,IAAMJ,YAAYH;QACpD,OAAO,IAAMQ,qBAAqBF;IACpC,GAAG;QAACL;QAAWC;QAAmBC;QAAaH;KAAW;AAC5D,EAAC"}
@@ -0,0 +1,8 @@
1
+ export type UseBlockActionMessagesArgs = {
2
+ selectedBlockPath: string | null;
3
+ setSelectedBlockPath: (path: string | null) => void;
4
+ };
5
+ export type UseBlockActionMessagesReturn = {
6
+ addBelowRequestId: number;
7
+ };
8
+ export declare const useBlockActionMessages: ({ setSelectedBlockPath, }: UseBlockActionMessagesArgs) => UseBlockActionMessagesReturn;
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { useAllFormFields, useForm } from '@payloadcms/ui';
4
+ import { listenForParentInbound } from '../internal/postmessage';
5
+ import { splitFieldPath } from '../internal/path';
6
+ import { useEditorHistory } from '../state/useEditorHistory';
7
+ import { useLatestRef } from './useLatestRef';
8
+ const ID_SUFFIX = '.id';
9
+ const buildIdIndex = (fields)=>{
10
+ const map = new Map();
11
+ for (const key of Object.keys(fields)){
12
+ if (!key.endsWith(ID_SUFFIX)) continue;
13
+ const value = fields[key]?.value;
14
+ if (typeof value === 'string' && value.length > 0) {
15
+ map.set(value, key.slice(0, -ID_SUFFIX.length));
16
+ }
17
+ }
18
+ return map;
19
+ };
20
+ export const useBlockActionMessages = ({ setSelectedBlockPath })=>{
21
+ const [addBelowRequestId, setAddBelowRequestId] = useState(0);
22
+ const [allFields] = useAllFormFields();
23
+ const idIndex = useMemo(()=>buildIdIndex(allFields), [
24
+ allFields
25
+ ]);
26
+ const { dispatchFields } = useForm();
27
+ const history = useEditorHistory();
28
+ // Refs let the postMessage listener stay bound across renders without
29
+ // missing the latest form state, dispatcher, or history commit.
30
+ const allFieldsRef = useLatestRef(allFields);
31
+ const idIndexRef = useLatestRef(idIndex);
32
+ const dispatchFieldsRef = useLatestRef(dispatchFields);
33
+ const historyRef = useLatestRef(history);
34
+ const setSelectedBlockPathRef = useLatestRef(setSelectedBlockPath);
35
+ // Bind the postMessage listener exactly once. All four state inputs flow
36
+ // through stable refs (above), so a re-bind is never needed.
37
+ useEffect(()=>listenForParentInbound((data)=>{
38
+ const fields = allFieldsRef.current;
39
+ const path = idIndexRef.current.get(data.id) ?? null;
40
+ if (!path) return;
41
+ const select = setSelectedBlockPathRef.current;
42
+ const dispatch = dispatchFieldsRef.current;
43
+ const { commit } = historyRef.current;
44
+ if (data.type === 'focus-block') {
45
+ select(path);
46
+ return;
47
+ }
48
+ if (data.action === 'add') {
49
+ // Monotonic counter — BlockSettingsTab compares the latest id against
50
+ // its lastHandledRequestRef. Date.now() risked collisions on
51
+ // double-clicks landing in the same millisecond.
52
+ select(path);
53
+ setAddBelowRequestId((id)=>id + 1);
54
+ return;
55
+ }
56
+ const split = splitFieldPath(path);
57
+ if (!split) return;
58
+ const { parent: parentPath, index: rowIndex } = split;
59
+ const rows = fields[parentPath]?.rows;
60
+ const rowCount = Array.isArray(rows) ? rows.length : 0;
61
+ if (rowIndex < 0 || rowIndex >= rowCount) return;
62
+ switch(data.action){
63
+ case 'move-up':
64
+ if (rowIndex === 0) return;
65
+ commit(()=>dispatch({
66
+ type: 'MOVE_ROW',
67
+ path: parentPath,
68
+ moveFromIndex: rowIndex,
69
+ moveToIndex: rowIndex - 1
70
+ }));
71
+ select(`${parentPath}.${rowIndex - 1}`);
72
+ break;
73
+ case 'move-down':
74
+ if (rowIndex >= rowCount - 1) return;
75
+ commit(()=>dispatch({
76
+ type: 'MOVE_ROW',
77
+ path: parentPath,
78
+ moveFromIndex: rowIndex,
79
+ moveToIndex: rowIndex + 1
80
+ }));
81
+ select(`${parentPath}.${rowIndex + 1}`);
82
+ break;
83
+ case 'duplicate':
84
+ commit(()=>dispatch({
85
+ type: 'DUPLICATE_ROW',
86
+ path: parentPath,
87
+ rowIndex
88
+ }));
89
+ select(`${parentPath}.${rowIndex + 1}`);
90
+ break;
91
+ case 'delete':
92
+ commit(()=>dispatch({
93
+ type: 'REMOVE_ROW',
94
+ path: parentPath,
95
+ rowIndex
96
+ }));
97
+ select(null);
98
+ break;
99
+ }
100
+ }), // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable
101
+ []);
102
+ return {
103
+ addBelowRequestId
104
+ };
105
+ };
106
+
107
+ //# sourceMappingURL=useBlockActionMessages.js.map