payload-better-editor 1.0.3 → 1.1.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 (45) hide show
  1. package/dist/admin/ErrorBoundary.d.ts +1 -1
  2. package/dist/admin/LiveEditorOverlay.js +6 -7
  3. package/dist/admin/LiveEditorOverlay.js.map +1 -1
  4. package/dist/admin/PreviewFrame.d.ts +3 -3
  5. package/dist/admin/PreviewFrame.js +4 -6
  6. package/dist/admin/PreviewFrame.js.map +1 -1
  7. package/dist/admin/PreviewToolbar.d.ts +3 -3
  8. package/dist/admin/PreviewToolbar.js +7 -13
  9. package/dist/admin/PreviewToolbar.js.map +1 -1
  10. package/dist/admin/ViewportToggle.js.map +1 -1
  11. package/dist/admin/WidthChip.d.ts +4 -0
  12. package/dist/admin/WidthChip.js +30 -0
  13. package/dist/admin/WidthChip.js.map +1 -0
  14. package/dist/admin/icons.d.ts +24 -23
  15. package/dist/admin/icons.js +488 -30
  16. package/dist/admin/icons.js.map +1 -1
  17. package/dist/admin/sidebar/Sidebar.d.ts +1 -1
  18. package/dist/admin/sidebar/Sidebar.js +8 -2
  19. package/dist/admin/sidebar/Sidebar.js.map +1 -1
  20. package/dist/admin/sidebar/ValidationSummary.d.ts +6 -0
  21. package/dist/admin/sidebar/ValidationSummary.js +48 -0
  22. package/dist/admin/sidebar/ValidationSummary.js.map +1 -0
  23. package/dist/admin/sidebar/validation.d.ts +10 -0
  24. package/dist/admin/sidebar/validation.js +28 -0
  25. package/dist/admin/sidebar/validation.js.map +1 -0
  26. package/dist/hooks/usePreviewBinding.js +5 -3
  27. package/dist/hooks/usePreviewBinding.js.map +1 -1
  28. package/dist/hooks/usePreviewHandleDrag.js +9 -20
  29. package/dist/hooks/usePreviewHandleDrag.js.map +1 -1
  30. package/dist/hooks/useSidebarResize.js +8 -16
  31. package/dist/hooks/useSidebarResize.js.map +1 -1
  32. package/dist/internal/drag.d.ts +12 -0
  33. package/dist/internal/drag.js +46 -0
  34. package/dist/internal/drag.js.map +1 -0
  35. package/dist/internal/iframe.d.ts +4 -0
  36. package/dist/internal/iframe.js +7 -1
  37. package/dist/internal/iframe.js.map +1 -1
  38. package/dist/styles/sidebar.css +53 -0
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +3 -4
  41. package/dist/version.js.map +1 -1
  42. package/package.json +4 -3
  43. package/dist/hooks/useIframeResizeObserver.d.ts +0 -2
  44. package/dist/hooks/useIframeResizeObserver.js +0 -20
  45. package/dist/hooks/useIframeResizeObserver.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useMemo } from 'react';
4
+ import { useAllFormFields } from '@payloadcms/ui';
5
+ import { collectFieldErrors } from './validation';
6
+ // Sidebar banner listing invalid fields across all blocks.
7
+ export const ValidationSummary = ({ blocksField, onSelectPath })=>{
8
+ const [fields] = useAllFormFields();
9
+ const errors = useMemo(()=>collectFieldErrors(fields, blocksField), [
10
+ fields,
11
+ blocksField
12
+ ]);
13
+ if (errors.length === 0) return null;
14
+ return /*#__PURE__*/ _jsxs("div", {
15
+ className: "better-editor-sidebar__errors",
16
+ role: "region",
17
+ "aria-label": "Validation errors",
18
+ children: [
19
+ /*#__PURE__*/ _jsx("p", {
20
+ className: "better-editor-sidebar__errors-title",
21
+ children: errors.length === 1 ? '1 field needs attention' : `${errors.length} fields need attention`
22
+ }),
23
+ /*#__PURE__*/ _jsx("ul", {
24
+ className: "better-editor-sidebar__errors-list",
25
+ children: errors.map((error)=>/*#__PURE__*/ _jsxs("li", {
26
+ className: "better-editor-sidebar__errors-item",
27
+ children: [
28
+ error.blockPath ? /*#__PURE__*/ _jsx("button", {
29
+ type: "button",
30
+ className: "better-editor-sidebar__errors-jump",
31
+ onClick: ()=>onSelectPath(error.blockPath),
32
+ children: error.label
33
+ }) : /*#__PURE__*/ _jsx("span", {
34
+ className: "better-editor-sidebar__errors-label",
35
+ children: error.label
36
+ }),
37
+ /*#__PURE__*/ _jsx("span", {
38
+ className: "better-editor-sidebar__errors-message",
39
+ children: error.message
40
+ })
41
+ ]
42
+ }, error.path))
43
+ })
44
+ ]
45
+ });
46
+ };
47
+
48
+ //# sourceMappingURL=ValidationSummary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/sidebar/ValidationSummary.tsx"],"sourcesContent":["'use client'\n\nimport React, { useMemo } from 'react'\nimport { useAllFormFields } from '@payloadcms/ui'\nimport type { FormState } from 'payload'\nimport { collectFieldErrors } from './validation'\n\nexport type ValidationSummaryProps = {\n blocksField: string\n onSelectPath: (path: string | null) => void\n}\n\n// Sidebar banner listing invalid fields across all blocks.\nexport const ValidationSummary: React.FC<ValidationSummaryProps> = ({\n blocksField,\n onSelectPath,\n}) => {\n const [fields] = useAllFormFields()\n const errors = useMemo(\n () => collectFieldErrors(fields as FormState, blocksField),\n [fields, blocksField],\n )\n\n if (errors.length === 0) return null\n\n return (\n <div className=\"better-editor-sidebar__errors\" role=\"region\" aria-label=\"Validation errors\">\n <p className=\"better-editor-sidebar__errors-title\">\n {errors.length === 1 ? '1 field needs attention' : `${errors.length} fields need attention`}\n </p>\n <ul className=\"better-editor-sidebar__errors-list\">\n {errors.map((error) => (\n <li key={error.path} className=\"better-editor-sidebar__errors-item\">\n {error.blockPath ? (\n <button\n type=\"button\"\n className=\"better-editor-sidebar__errors-jump\"\n onClick={() => onSelectPath(error.blockPath)}\n >\n {error.label}\n </button>\n ) : (\n <span className=\"better-editor-sidebar__errors-label\">{error.label}</span>\n )}\n <span className=\"better-editor-sidebar__errors-message\">{error.message}</span>\n </li>\n ))}\n </ul>\n </div>\n )\n}\n"],"names":["React","useMemo","useAllFormFields","collectFieldErrors","ValidationSummary","blocksField","onSelectPath","fields","errors","length","div","className","role","aria-label","p","ul","map","error","li","blockPath","button","type","onClick","label","span","message","path"],"mappings":"AAAA;;AAEA,OAAOA,SAASC,OAAO,QAAQ,QAAO;AACtC,SAASC,gBAAgB,QAAQ,iBAAgB;AAEjD,SAASC,kBAAkB,QAAQ,eAAc;AAOjD,2DAA2D;AAC3D,OAAO,MAAMC,oBAAsD,CAAC,EAClEC,WAAW,EACXC,YAAY,EACb;IACC,MAAM,CAACC,OAAO,GAAGL;IACjB,MAAMM,SAASP,QACb,IAAME,mBAAmBI,QAAqBF,cAC9C;QAACE;QAAQF;KAAY;IAGvB,IAAIG,OAAOC,MAAM,KAAK,GAAG,OAAO;IAEhC,qBACE,MAACC;QAAIC,WAAU;QAAgCC,MAAK;QAASC,cAAW;;0BACtE,KAACC;gBAAEH,WAAU;0BACVH,OAAOC,MAAM,KAAK,IAAI,4BAA4B,GAAGD,OAAOC,MAAM,CAAC,sBAAsB,CAAC;;0BAE7F,KAACM;gBAAGJ,WAAU;0BACXH,OAAOQ,GAAG,CAAC,CAACC,sBACX,MAACC;wBAAoBP,WAAU;;4BAC5BM,MAAME,SAAS,iBACd,KAACC;gCACCC,MAAK;gCACLV,WAAU;gCACVW,SAAS,IAAMhB,aAAaW,MAAME,SAAS;0CAE1CF,MAAMM,KAAK;+CAGd,KAACC;gCAAKb,WAAU;0CAAuCM,MAAMM,KAAK;;0CAEpE,KAACC;gCAAKb,WAAU;0CAAyCM,MAAMQ,OAAO;;;uBAZ/DR,MAAMS,IAAI;;;;AAkB7B,EAAC"}
@@ -0,0 +1,10 @@
1
+ import type { FormState } from 'payload';
2
+ export type FieldError = {
3
+ path: string;
4
+ label: string;
5
+ message: string;
6
+ /** Top-level block path to select on click, or null for non-block fields. */
7
+ blockPath: string | null;
8
+ };
9
+ export declare const toBlockPath: (fieldPath: string, blocksField: string) => string | null;
10
+ export declare const collectFieldErrors: (fields: FormState, blocksField: string) => FieldError[];
@@ -0,0 +1,28 @@
1
+ const lastSegment = (path)=>{
2
+ const parts = path.split('.');
3
+ return parts[parts.length - 1] || path;
4
+ };
5
+ // Top-level block (`layout.2`) a field path belongs to, or null if outside the blocks field.
6
+ export const toBlockPath = (fieldPath, blocksField)=>{
7
+ if (!fieldPath.startsWith(`${blocksField}.`)) return null;
8
+ const index = fieldPath.slice(blocksField.length + 1).split('.')[0];
9
+ return /^\d+$/.test(index) ? `${blocksField}.${index}` : null;
10
+ };
11
+ // Require a message: leaf errors carry one, containers/groups don't — keeps
12
+ // spurious parent rows out.
13
+ export const collectFieldErrors = (fields, blocksField)=>{
14
+ const out = [];
15
+ for (const [path, field] of Object.entries(fields)){
16
+ if (field.valid === false && typeof field.errorMessage === 'string') {
17
+ out.push({
18
+ path,
19
+ label: lastSegment(path),
20
+ message: field.errorMessage,
21
+ blockPath: toBlockPath(path, blocksField)
22
+ });
23
+ }
24
+ }
25
+ return out;
26
+ };
27
+
28
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/sidebar/validation.ts"],"sourcesContent":["import type { FormState } from 'payload'\n\nexport type FieldError = {\n path: string\n label: string\n message: string\n /** Top-level block path to select on click, or null for non-block fields. */\n blockPath: string | null\n}\n\nconst lastSegment = (path: string): string => {\n const parts = path.split('.')\n return parts[parts.length - 1] || path\n}\n\n// Top-level block (`layout.2`) a field path belongs to, or null if outside the blocks field.\nexport const toBlockPath = (fieldPath: string, blocksField: string): string | null => {\n if (!fieldPath.startsWith(`${blocksField}.`)) return null\n const index = fieldPath.slice(blocksField.length + 1).split('.')[0]\n return /^\\d+$/.test(index) ? `${blocksField}.${index}` : null\n}\n\n// Require a message: leaf errors carry one, containers/groups don't — keeps\n// spurious parent rows out.\nexport const collectFieldErrors = (fields: FormState, blocksField: string): FieldError[] => {\n const out: FieldError[] = []\n for (const [path, field] of Object.entries(fields)) {\n if (field.valid === false && typeof field.errorMessage === 'string') {\n out.push({\n path,\n label: lastSegment(path),\n message: field.errorMessage,\n blockPath: toBlockPath(path, blocksField),\n })\n }\n }\n return out\n}\n"],"names":["lastSegment","path","parts","split","length","toBlockPath","fieldPath","blocksField","startsWith","index","slice","test","collectFieldErrors","fields","out","field","Object","entries","valid","errorMessage","push","label","message","blockPath"],"mappings":"AAUA,MAAMA,cAAc,CAACC;IACnB,MAAMC,QAAQD,KAAKE,KAAK,CAAC;IACzB,OAAOD,KAAK,CAACA,MAAME,MAAM,GAAG,EAAE,IAAIH;AACpC;AAEA,6FAA6F;AAC7F,OAAO,MAAMI,cAAc,CAACC,WAAmBC;IAC7C,IAAI,CAACD,UAAUE,UAAU,CAAC,GAAGD,YAAY,CAAC,CAAC,GAAG,OAAO;IACrD,MAAME,QAAQH,UAAUI,KAAK,CAACH,YAAYH,MAAM,GAAG,GAAGD,KAAK,CAAC,IAAI,CAAC,EAAE;IACnE,OAAO,QAAQQ,IAAI,CAACF,SAAS,GAAGF,YAAY,CAAC,EAAEE,OAAO,GAAG;AAC3D,EAAC;AAED,4EAA4E;AAC5E,4BAA4B;AAC5B,OAAO,MAAMG,qBAAqB,CAACC,QAAmBN;IACpD,MAAMO,MAAoB,EAAE;IAC5B,KAAK,MAAM,CAACb,MAAMc,MAAM,IAAIC,OAAOC,OAAO,CAACJ,QAAS;QAClD,IAAIE,MAAMG,KAAK,KAAK,SAAS,OAAOH,MAAMI,YAAY,KAAK,UAAU;YACnEL,IAAIM,IAAI,CAAC;gBACPnB;gBACAoB,OAAOrB,YAAYC;gBACnBqB,SAASP,MAAMI,YAAY;gBAC3BI,WAAWlB,YAAYJ,MAAMM;YAC/B;QACF;IACF;IACA,OAAOO;AACT,EAAC"}
@@ -81,12 +81,14 @@ import { useLatestRef } from './useLatestRef';
81
81
  // Fresh iframes report readyState='complete' on their initial
82
82
  // `about:blank` document before `src` has navigated. Skip and wait
83
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;
84
+ // `doc.URL` is a non-nullable string on Document — safer than
85
+ // `doc.location.href`, which Firefox can briefly expose as null.
86
+ if (!doc.URL || doc.URL === 'about:blank') return;
86
87
  onLoadingChangeRef.current(false);
87
88
  bindToDocument(doc);
88
89
  };
89
- if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
90
+ const initialDoc = getSameOriginDocument(iframe);
91
+ if (initialDoc && initialDoc.readyState === 'complete') {
90
92
  onLoad();
91
93
  }
92
94
  iframe.addEventListener('load', onLoad);
@@ -1 +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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"}
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 // `doc.URL` is a non-nullable string on Document — safer than\n // `doc.location.href`, which Firefox can briefly expose as null.\n if (!doc.URL || doc.URL === 'about:blank') return\n onLoadingChangeRef.current(false)\n bindToDocument(doc)\n }\n\n const initialDoc = getSameOriginDocument(iframe)\n if (initialDoc && initialDoc.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","URL","initialDoc","readyState","addEventListener","removeEventListener"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
  import React, { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { clampViewport } from '../internal/limits';
4
+ import { startHorizontalDrag } from '../internal/drag';
4
5
  export const usePreviewHandleDrag = ({ resizable, viewportWidth, onResize })=>{
5
6
  const [isResizing, setIsResizing] = useState(false);
6
- // Track in-flight drag so unmount mid-drag can release body styles + listeners.
7
7
  const dragCleanupRef = useRef(null);
8
8
  const isMountedRef = useRef(true);
9
9
  useEffect(()=>{
@@ -18,27 +18,16 @@ export const usePreviewHandleDrag = ({ resizable, viewportWidth, onResize })=>{
18
18
  e.preventDefault();
19
19
  const startX = e.clientX;
20
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.
21
+ // Centered iframe grows by 2px per dragged edge px.
23
22
  const dir = side === 'right' ? 2 : -2;
24
23
  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);
24
+ dragCleanupRef.current = startHorizontalDrag('ew-resize', {
25
+ onUpdate: (clientX)=>onResize(clampViewport(startWidth + (clientX - startX) * dir)),
26
+ onEnd: ()=>{
27
+ if (isMountedRef.current) setIsResizing(false);
28
+ dragCleanupRef.current = null;
29
+ }
30
+ });
42
31
  }, [
43
32
  resizable,
44
33
  onResize,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/usePreviewHandleDrag.ts"],"sourcesContent":["'use client'\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { clampViewport } from '../internal/limits'\n\nexport type UsePreviewHandleDragOptions = {\n resizable: boolean\n viewportWidth?: number | null\n onResize?: (next: number) => void\n}\n\nexport type UsePreviewHandleDragReturn = {\n isResizing: boolean\n onHandleMouseDown: (side: 'left' | 'right') => (e: React.MouseEvent) => void\n}\n\nexport const usePreviewHandleDrag = ({\n resizable,\n viewportWidth,\n onResize,\n}: UsePreviewHandleDragOptions): UsePreviewHandleDragReturn => {\n const [isResizing, setIsResizing] = useState(false)\n\n // Track in-flight drag so unmount mid-drag can release body styles + listeners.\n const dragCleanupRef = useRef<(() => void) | null>(null)\n const isMountedRef = useRef(true)\n useEffect(() => {\n isMountedRef.current = true\n return () => {\n isMountedRef.current = false\n dragCleanupRef.current?.()\n }\n }, [])\n\n const onHandleMouseDown = useCallback(\n (side: 'left' | 'right') => (e: React.MouseEvent) => {\n if (!resizable || !onResize || !viewportWidth) return\n e.preventDefault()\n const startX = e.clientX\n const startWidth = viewportWidth\n // Iframe is centered; dragging either edge by N px symmetrically grows\n // the width by 2N. Right handle: positive delta increases width.\n const dir = side === 'right' ? 2 : -2\n setIsResizing(true)\n const onMove = (ev: MouseEvent) => {\n onResize(clampViewport(startWidth + (ev.clientX - startX) * dir))\n }\n const cleanup = () => {\n window.removeEventListener('mousemove', onMove)\n window.removeEventListener('mouseup', onUp)\n document.body.style.cursor = ''\n document.body.style.userSelect = ''\n if (isMountedRef.current) setIsResizing(false)\n dragCleanupRef.current = null\n }\n const onUp = () => cleanup()\n dragCleanupRef.current = cleanup\n document.body.style.cursor = 'ew-resize'\n document.body.style.userSelect = 'none'\n window.addEventListener('mousemove', onMove)\n window.addEventListener('mouseup', onUp)\n },\n [resizable, onResize, viewportWidth],\n )\n\n return { isResizing, onHandleMouseDown }\n}\n"],"names":["React","useCallback","useEffect","useRef","useState","clampViewport","usePreviewHandleDrag","resizable","viewportWidth","onResize","isResizing","setIsResizing","dragCleanupRef","isMountedRef","current","onHandleMouseDown","side","e","preventDefault","startX","clientX","startWidth","dir","onMove","ev","cleanup","window","removeEventListener","onUp","document","body","style","cursor","userSelect","addEventListener"],"mappings":"AAAA;AAEA,OAAOA,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACvE,SAASC,aAAa,QAAQ,qBAAoB;AAalD,OAAO,MAAMC,uBAAuB,CAAC,EACnCC,SAAS,EACTC,aAAa,EACbC,QAAQ,EACoB;IAC5B,MAAM,CAACC,YAAYC,cAAc,GAAGP,SAAS;IAE7C,gFAAgF;IAChF,MAAMQ,iBAAiBT,OAA4B;IACnD,MAAMU,eAAeV,OAAO;IAC5BD,UAAU;QACRW,aAAaC,OAAO,GAAG;QACvB,OAAO;YACLD,aAAaC,OAAO,GAAG;YACvBF,eAAeE,OAAO;QACxB;IACF,GAAG,EAAE;IAEL,MAAMC,oBAAoBd,YACxB,CAACe,OAA2B,CAACC;YAC3B,IAAI,CAACV,aAAa,CAACE,YAAY,CAACD,eAAe;YAC/CS,EAAEC,cAAc;YAChB,MAAMC,SAASF,EAAEG,OAAO;YACxB,MAAMC,aAAab;YACnB,uEAAuE;YACvE,iEAAiE;YACjE,MAAMc,MAAMN,SAAS,UAAU,IAAI,CAAC;YACpCL,cAAc;YACd,MAAMY,SAAS,CAACC;gBACdf,SAASJ,cAAcgB,aAAa,AAACG,CAAAA,GAAGJ,OAAO,GAAGD,MAAK,IAAKG;YAC9D;YACA,MAAMG,UAAU;gBACdC,OAAOC,mBAAmB,CAAC,aAAaJ;gBACxCG,OAAOC,mBAAmB,CAAC,WAAWC;gBACtCC,SAASC,IAAI,CAACC,KAAK,CAACC,MAAM,GAAG;gBAC7BH,SAASC,IAAI,CAACC,KAAK,CAACE,UAAU,GAAG;gBACjC,IAAIpB,aAAaC,OAAO,EAAEH,cAAc;gBACxCC,eAAeE,OAAO,GAAG;YAC3B;YACA,MAAMc,OAAO,IAAMH;YACnBb,eAAeE,OAAO,GAAGW;YACzBI,SAASC,IAAI,CAACC,KAAK,CAACC,MAAM,GAAG;YAC7BH,SAASC,IAAI,CAACC,KAAK,CAACE,UAAU,GAAG;YACjCP,OAAOQ,gBAAgB,CAAC,aAAaX;YACrCG,OAAOQ,gBAAgB,CAAC,WAAWN;QACrC,GACA;QAACrB;QAAWE;QAAUD;KAAc;IAGtC,OAAO;QAAEE;QAAYK;IAAkB;AACzC,EAAC"}
1
+ {"version":3,"sources":["../../src/hooks/usePreviewHandleDrag.ts"],"sourcesContent":["'use client'\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { clampViewport } from '../internal/limits'\nimport { startHorizontalDrag } from '../internal/drag'\n\nexport type UsePreviewHandleDragOptions = {\n resizable: boolean\n viewportWidth?: number | null\n onResize?: (next: number) => void\n}\n\nexport type UsePreviewHandleDragReturn = {\n isResizing: boolean\n onHandleMouseDown: (side: 'left' | 'right') => (e: React.MouseEvent) => void\n}\n\nexport const usePreviewHandleDrag = ({\n resizable,\n viewportWidth,\n onResize,\n}: UsePreviewHandleDragOptions): UsePreviewHandleDragReturn => {\n const [isResizing, setIsResizing] = useState(false)\n\n const dragCleanupRef = useRef<(() => void) | null>(null)\n const isMountedRef = useRef(true)\n useEffect(() => {\n isMountedRef.current = true\n return () => {\n isMountedRef.current = false\n dragCleanupRef.current?.()\n }\n }, [])\n\n const onHandleMouseDown = useCallback(\n (side: 'left' | 'right') => (e: React.MouseEvent) => {\n if (!resizable || !onResize || !viewportWidth) return\n e.preventDefault()\n const startX = e.clientX\n const startWidth = viewportWidth\n // Centered iframe grows by 2px per dragged edge px.\n const dir = side === 'right' ? 2 : -2\n setIsResizing(true)\n dragCleanupRef.current = startHorizontalDrag('ew-resize', {\n onUpdate: (clientX) => onResize(clampViewport(startWidth + (clientX - startX) * dir)),\n onEnd: () => {\n if (isMountedRef.current) setIsResizing(false)\n dragCleanupRef.current = null\n },\n })\n },\n [resizable, onResize, viewportWidth],\n )\n\n return { isResizing, onHandleMouseDown }\n}\n"],"names":["React","useCallback","useEffect","useRef","useState","clampViewport","startHorizontalDrag","usePreviewHandleDrag","resizable","viewportWidth","onResize","isResizing","setIsResizing","dragCleanupRef","isMountedRef","current","onHandleMouseDown","side","e","preventDefault","startX","clientX","startWidth","dir","onUpdate","onEnd"],"mappings":"AAAA;AAEA,OAAOA,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACvE,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,mBAAmB,QAAQ,mBAAkB;AAatD,OAAO,MAAMC,uBAAuB,CAAC,EACnCC,SAAS,EACTC,aAAa,EACbC,QAAQ,EACoB;IAC5B,MAAM,CAACC,YAAYC,cAAc,GAAGR,SAAS;IAE7C,MAAMS,iBAAiBV,OAA4B;IACnD,MAAMW,eAAeX,OAAO;IAC5BD,UAAU;QACRY,aAAaC,OAAO,GAAG;QACvB,OAAO;YACLD,aAAaC,OAAO,GAAG;YACvBF,eAAeE,OAAO;QACxB;IACF,GAAG,EAAE;IAEL,MAAMC,oBAAoBf,YACxB,CAACgB,OAA2B,CAACC;YAC3B,IAAI,CAACV,aAAa,CAACE,YAAY,CAACD,eAAe;YAC/CS,EAAEC,cAAc;YAChB,MAAMC,SAASF,EAAEG,OAAO;YACxB,MAAMC,aAAab;YACnB,oDAAoD;YACpD,MAAMc,MAAMN,SAAS,UAAU,IAAI,CAAC;YACpCL,cAAc;YACdC,eAAeE,OAAO,GAAGT,oBAAoB,aAAa;gBACxDkB,UAAU,CAACH,UAAYX,SAASL,cAAciB,aAAa,AAACD,CAAAA,UAAUD,MAAK,IAAKG;gBAChFE,OAAO;oBACL,IAAIX,aAAaC,OAAO,EAAEH,cAAc;oBACxCC,eAAeE,OAAO,GAAG;gBAC3B;YACF;QACF,GACA;QAACP;QAAWE;QAAUD;KAAc;IAGtC,OAAO;QAAEE;QAAYK;IAAkB;AACzC,EAAC"}
@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { DEFAULT_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, SIDEBAR_KEYBOARD_STEP_PX, SIDEBAR_KEYBOARD_STEP_LARGE_PX, STORAGE_DEBOUNCE_MS } from '../internal/constants';
4
4
  import { useBetterEditorConfig } from '../providers/BetterEditorConfigProvider';
5
5
  import { readNumber, writeString } from '../internal/storage';
6
+ import { startHorizontalDrag } from '../internal/drag';
6
7
  const clampSidebar = (n)=>Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, n));
7
8
  export const useSidebarResize = (sidebarPosition)=>{
8
9
  const { storageKeys } = useBetterEditorConfig();
@@ -50,22 +51,13 @@ export const useSidebarResize = (sidebarPosition)=>{
50
51
  const startWidth = widthRef.current;
51
52
  const direction = positionRef.current === 'right' ? -1 : 1;
52
53
  setIsResizing(true);
53
- document.body.style.cursor = 'col-resize';
54
- document.body.style.userSelect = 'none';
55
- const onMove = (ev)=>{
56
- setSidebarWidth(clampSidebar(startWidth + (ev.clientX - startX) * direction));
57
- };
58
- const cleanup = ()=>{
59
- window.removeEventListener('mousemove', onMove);
60
- window.removeEventListener('mouseup', cleanup);
61
- document.body.style.cursor = '';
62
- document.body.style.userSelect = '';
63
- setIsResizing(false);
64
- dragCleanupRef.current = null;
65
- };
66
- dragCleanupRef.current = cleanup;
67
- window.addEventListener('mousemove', onMove);
68
- window.addEventListener('mouseup', cleanup);
54
+ dragCleanupRef.current = startHorizontalDrag('col-resize', {
55
+ onUpdate: (clientX)=>setSidebarWidth(clampSidebar(startWidth + (clientX - startX) * direction)),
56
+ onEnd: ()=>{
57
+ setIsResizing(false);
58
+ dragCleanupRef.current = null;
59
+ }
60
+ });
69
61
  }, []);
70
62
  // Arrow keys nudge the handle for keyboard users (WCAG 2.1 separator
71
63
  // semantics). Direction matches the mouse drag — Right when the sidebar
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/useSidebarResize.ts"],"sourcesContent":["'use client'\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n DEFAULT_SIDEBAR_WIDTH,\n MAX_SIDEBAR_WIDTH,\n MIN_SIDEBAR_WIDTH,\n SIDEBAR_KEYBOARD_STEP_PX,\n SIDEBAR_KEYBOARD_STEP_LARGE_PX,\n STORAGE_DEBOUNCE_MS,\n} from '../internal/constants'\nimport { useBetterEditorConfig } from '../providers/BetterEditorConfigProvider'\nimport { readNumber, writeString } from '../internal/storage'\n\nconst clampSidebar = (n: number): number =>\n Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, n))\n\nexport type UseSidebarResizeReturn = {\n sidebarWidth: number\n isResizing: boolean\n onResizeStart: (e: React.MouseEvent<HTMLDivElement>) => void\n onResizeKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void\n}\n\nexport const useSidebarResize = (\n sidebarPosition: 'left' | 'right',\n): UseSidebarResizeReturn => {\n const { storageKeys } = useBetterEditorConfig()\n const [sidebarWidth, setSidebarWidth] = useState<number>(() =>\n readNumber(storageKeys.sidebarWidth, DEFAULT_SIDEBAR_WIDTH, clampSidebar),\n )\n const [isResizing, setIsResizing] = useState(false)\n\n const widthRef = useRef(sidebarWidth)\n widthRef.current = sidebarWidth\n\n const positionRef = useRef(sidebarPosition)\n positionRef.current = sidebarPosition\n\n const dragCleanupRef = useRef<(() => void) | null>(null)\n const writeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n // Debounce storage writes — drag fires 60×/s while keyboard nudges fire\n // per keystroke. Either way we only want the final value persisted.\n const hydratedRef = useRef(false)\n useEffect(() => {\n if (!hydratedRef.current) {\n hydratedRef.current = true\n return\n }\n if (writeTimerRef.current) clearTimeout(writeTimerRef.current)\n writeTimerRef.current = setTimeout(() => {\n writeString(storageKeys.sidebarWidth, String(sidebarWidth))\n writeTimerRef.current = null\n }, STORAGE_DEBOUNCE_MS)\n return () => {\n if (writeTimerRef.current) clearTimeout(writeTimerRef.current)\n }\n }, [sidebarWidth, storageKeys.sidebarWidth])\n\n // Release listeners + body styles + flush pending storage write on unmount.\n useEffect(\n () => () => {\n dragCleanupRef.current?.()\n if (writeTimerRef.current) {\n clearTimeout(writeTimerRef.current)\n writeString(storageKeys.sidebarWidth, String(widthRef.current))\n }\n },\n [storageKeys.sidebarWidth],\n )\n\n const onResizeStart = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n e.preventDefault()\n const startX = e.clientX\n const startWidth = widthRef.current\n const direction = positionRef.current === 'right' ? -1 : 1\n\n setIsResizing(true)\n document.body.style.cursor = 'col-resize'\n document.body.style.userSelect = 'none'\n\n const onMove = (ev: MouseEvent) => {\n setSidebarWidth(clampSidebar(startWidth + (ev.clientX - startX) * direction))\n }\n const cleanup = () => {\n window.removeEventListener('mousemove', onMove)\n window.removeEventListener('mouseup', cleanup)\n document.body.style.cursor = ''\n document.body.style.userSelect = ''\n setIsResizing(false)\n dragCleanupRef.current = null\n }\n dragCleanupRef.current = cleanup\n window.addEventListener('mousemove', onMove)\n window.addEventListener('mouseup', cleanup)\n }, [])\n\n // Arrow keys nudge the handle for keyboard users (WCAG 2.1 separator\n // semantics). Direction matches the mouse drag — Right when the sidebar\n // sits on the right grows the sidebar inward.\n const onResizeKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n const direction = positionRef.current === 'right' ? -1 : 1\n let delta = 0\n if (e.key === 'ArrowLeft') delta = -SIDEBAR_KEYBOARD_STEP_PX * direction\n else if (e.key === 'ArrowRight') delta = SIDEBAR_KEYBOARD_STEP_PX * direction\n else if (e.key === 'PageUp') delta = -SIDEBAR_KEYBOARD_STEP_LARGE_PX * direction\n else if (e.key === 'PageDown') delta = SIDEBAR_KEYBOARD_STEP_LARGE_PX * direction\n else if (e.key === 'Home') {\n e.preventDefault()\n setSidebarWidth(MIN_SIDEBAR_WIDTH)\n return\n } else if (e.key === 'End') {\n e.preventDefault()\n setSidebarWidth(MAX_SIDEBAR_WIDTH)\n return\n }\n if (delta === 0) return\n e.preventDefault()\n setSidebarWidth((w) => clampSidebar(w + delta))\n }, [])\n\n return { sidebarWidth, isResizing, onResizeStart, onResizeKeyDown }\n}\n"],"names":["React","useCallback","useEffect","useRef","useState","DEFAULT_SIDEBAR_WIDTH","MAX_SIDEBAR_WIDTH","MIN_SIDEBAR_WIDTH","SIDEBAR_KEYBOARD_STEP_PX","SIDEBAR_KEYBOARD_STEP_LARGE_PX","STORAGE_DEBOUNCE_MS","useBetterEditorConfig","readNumber","writeString","clampSidebar","n","Math","min","max","useSidebarResize","sidebarPosition","storageKeys","sidebarWidth","setSidebarWidth","isResizing","setIsResizing","widthRef","current","positionRef","dragCleanupRef","writeTimerRef","hydratedRef","clearTimeout","setTimeout","String","onResizeStart","e","preventDefault","startX","clientX","startWidth","direction","document","body","style","cursor","userSelect","onMove","ev","cleanup","window","removeEventListener","addEventListener","onResizeKeyDown","delta","key","w"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/hooks/useSidebarResize.ts"],"sourcesContent":["'use client'\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n DEFAULT_SIDEBAR_WIDTH,\n MAX_SIDEBAR_WIDTH,\n MIN_SIDEBAR_WIDTH,\n SIDEBAR_KEYBOARD_STEP_PX,\n SIDEBAR_KEYBOARD_STEP_LARGE_PX,\n STORAGE_DEBOUNCE_MS,\n} from '../internal/constants'\nimport { useBetterEditorConfig } from '../providers/BetterEditorConfigProvider'\nimport { readNumber, writeString } from '../internal/storage'\nimport { startHorizontalDrag } from '../internal/drag'\n\nconst clampSidebar = (n: number): number =>\n Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, n))\n\nexport type UseSidebarResizeReturn = {\n sidebarWidth: number\n isResizing: boolean\n onResizeStart: (e: React.MouseEvent<HTMLDivElement>) => void\n onResizeKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void\n}\n\nexport const useSidebarResize = (\n sidebarPosition: 'left' | 'right',\n): UseSidebarResizeReturn => {\n const { storageKeys } = useBetterEditorConfig()\n const [sidebarWidth, setSidebarWidth] = useState<number>(() =>\n readNumber(storageKeys.sidebarWidth, DEFAULT_SIDEBAR_WIDTH, clampSidebar),\n )\n const [isResizing, setIsResizing] = useState(false)\n\n const widthRef = useRef(sidebarWidth)\n widthRef.current = sidebarWidth\n\n const positionRef = useRef(sidebarPosition)\n positionRef.current = sidebarPosition\n\n const dragCleanupRef = useRef<(() => void) | null>(null)\n const writeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n // Debounce storage writes — drag fires 60×/s while keyboard nudges fire\n // per keystroke. Either way we only want the final value persisted.\n const hydratedRef = useRef(false)\n useEffect(() => {\n if (!hydratedRef.current) {\n hydratedRef.current = true\n return\n }\n if (writeTimerRef.current) clearTimeout(writeTimerRef.current)\n writeTimerRef.current = setTimeout(() => {\n writeString(storageKeys.sidebarWidth, String(sidebarWidth))\n writeTimerRef.current = null\n }, STORAGE_DEBOUNCE_MS)\n return () => {\n if (writeTimerRef.current) clearTimeout(writeTimerRef.current)\n }\n }, [sidebarWidth, storageKeys.sidebarWidth])\n\n // Release listeners + body styles + flush pending storage write on unmount.\n useEffect(\n () => () => {\n dragCleanupRef.current?.()\n if (writeTimerRef.current) {\n clearTimeout(writeTimerRef.current)\n writeString(storageKeys.sidebarWidth, String(widthRef.current))\n }\n },\n [storageKeys.sidebarWidth],\n )\n\n const onResizeStart = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n e.preventDefault()\n const startX = e.clientX\n const startWidth = widthRef.current\n const direction = positionRef.current === 'right' ? -1 : 1\n setIsResizing(true)\n dragCleanupRef.current = startHorizontalDrag('col-resize', {\n onUpdate: (clientX) => setSidebarWidth(clampSidebar(startWidth + (clientX - startX) * direction)),\n onEnd: () => {\n setIsResizing(false)\n dragCleanupRef.current = null\n },\n })\n }, [])\n\n // Arrow keys nudge the handle for keyboard users (WCAG 2.1 separator\n // semantics). Direction matches the mouse drag — Right when the sidebar\n // sits on the right grows the sidebar inward.\n const onResizeKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n const direction = positionRef.current === 'right' ? -1 : 1\n let delta = 0\n if (e.key === 'ArrowLeft') delta = -SIDEBAR_KEYBOARD_STEP_PX * direction\n else if (e.key === 'ArrowRight') delta = SIDEBAR_KEYBOARD_STEP_PX * direction\n else if (e.key === 'PageUp') delta = -SIDEBAR_KEYBOARD_STEP_LARGE_PX * direction\n else if (e.key === 'PageDown') delta = SIDEBAR_KEYBOARD_STEP_LARGE_PX * direction\n else if (e.key === 'Home') {\n e.preventDefault()\n setSidebarWidth(MIN_SIDEBAR_WIDTH)\n return\n } else if (e.key === 'End') {\n e.preventDefault()\n setSidebarWidth(MAX_SIDEBAR_WIDTH)\n return\n }\n if (delta === 0) return\n e.preventDefault()\n setSidebarWidth((w) => clampSidebar(w + delta))\n }, [])\n\n return { sidebarWidth, isResizing, onResizeStart, onResizeKeyDown }\n}\n"],"names":["React","useCallback","useEffect","useRef","useState","DEFAULT_SIDEBAR_WIDTH","MAX_SIDEBAR_WIDTH","MIN_SIDEBAR_WIDTH","SIDEBAR_KEYBOARD_STEP_PX","SIDEBAR_KEYBOARD_STEP_LARGE_PX","STORAGE_DEBOUNCE_MS","useBetterEditorConfig","readNumber","writeString","startHorizontalDrag","clampSidebar","n","Math","min","max","useSidebarResize","sidebarPosition","storageKeys","sidebarWidth","setSidebarWidth","isResizing","setIsResizing","widthRef","current","positionRef","dragCleanupRef","writeTimerRef","hydratedRef","clearTimeout","setTimeout","String","onResizeStart","e","preventDefault","startX","clientX","startWidth","direction","onUpdate","onEnd","onResizeKeyDown","delta","key","w"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export type CoalescedDragHandlers = {
2
+ /** Runs at most once per animation frame with the latest pointer clientX. */
3
+ onUpdate: (clientX: number) => void;
4
+ /** Runs once when the drag ends or is torn down, after the final flush. */
5
+ onEnd?: () => void;
6
+ };
7
+ /**
8
+ * Horizontal pointer drag: window mousemove/mouseup listeners that coalesce
9
+ * moves into one onUpdate per frame and flush the final position on release.
10
+ * Sets the body cursor + disables text selection. Returns a function to end it.
11
+ */
12
+ export declare const startHorizontalDrag: (cursor: string, { onUpdate, onEnd }: CoalescedDragHandlers) => (() => void);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Horizontal pointer drag: window mousemove/mouseup listeners that coalesce
3
+ * moves into one onUpdate per frame and flush the final position on release.
4
+ * Sets the body cursor + disables text selection. Returns a function to end it.
5
+ */ export const startHorizontalDrag = (cursor, { onUpdate, onEnd })=>{
6
+ let rafId = 0;
7
+ let pendingX = 0;
8
+ let hasPending = false;
9
+ const flush = ()=>{
10
+ if (!hasPending) return;
11
+ hasPending = false;
12
+ onUpdate(pendingX);
13
+ };
14
+ const onMove = (e)=>{
15
+ pendingX = e.clientX;
16
+ hasPending = true;
17
+ if (!rafId) {
18
+ rafId = window.requestAnimationFrame(()=>{
19
+ rafId = 0;
20
+ flush();
21
+ });
22
+ }
23
+ };
24
+ let ended = false;
25
+ const end = ()=>{
26
+ if (ended) return;
27
+ ended = true;
28
+ window.removeEventListener('mousemove', onMove);
29
+ window.removeEventListener('mouseup', end);
30
+ if (rafId) {
31
+ window.cancelAnimationFrame(rafId);
32
+ rafId = 0;
33
+ }
34
+ flush();
35
+ document.body.style.cursor = '';
36
+ document.body.style.userSelect = '';
37
+ onEnd?.();
38
+ };
39
+ document.body.style.cursor = cursor;
40
+ document.body.style.userSelect = 'none';
41
+ window.addEventListener('mousemove', onMove);
42
+ window.addEventListener('mouseup', end);
43
+ return end;
44
+ };
45
+
46
+ //# sourceMappingURL=drag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internal/drag.ts"],"sourcesContent":["export type CoalescedDragHandlers = {\n /** Runs at most once per animation frame with the latest pointer clientX. */\n onUpdate: (clientX: number) => void\n /** Runs once when the drag ends or is torn down, after the final flush. */\n onEnd?: () => void\n}\n\n/**\n * Horizontal pointer drag: window mousemove/mouseup listeners that coalesce\n * moves into one onUpdate per frame and flush the final position on release.\n * Sets the body cursor + disables text selection. Returns a function to end it.\n */\nexport const startHorizontalDrag = (\n cursor: string,\n { onUpdate, onEnd }: CoalescedDragHandlers,\n): (() => void) => {\n let rafId = 0\n let pendingX = 0\n let hasPending = false\n\n const flush = (): void => {\n if (!hasPending) return\n hasPending = false\n onUpdate(pendingX)\n }\n\n const onMove = (e: MouseEvent): void => {\n pendingX = e.clientX\n hasPending = true\n if (!rafId) {\n rafId = window.requestAnimationFrame(() => {\n rafId = 0\n flush()\n })\n }\n }\n\n let ended = false\n const end = (): void => {\n if (ended) return\n ended = true\n window.removeEventListener('mousemove', onMove)\n window.removeEventListener('mouseup', end)\n if (rafId) {\n window.cancelAnimationFrame(rafId)\n rafId = 0\n }\n flush()\n document.body.style.cursor = ''\n document.body.style.userSelect = ''\n onEnd?.()\n }\n\n document.body.style.cursor = cursor\n document.body.style.userSelect = 'none'\n window.addEventListener('mousemove', onMove)\n window.addEventListener('mouseup', end)\n\n return end\n}\n"],"names":["startHorizontalDrag","cursor","onUpdate","onEnd","rafId","pendingX","hasPending","flush","onMove","e","clientX","window","requestAnimationFrame","ended","end","removeEventListener","cancelAnimationFrame","document","body","style","userSelect","addEventListener"],"mappings":"AAOA;;;;CAIC,GACD,OAAO,MAAMA,sBAAsB,CACjCC,QACA,EAAEC,QAAQ,EAAEC,KAAK,EAAyB;IAE1C,IAAIC,QAAQ;IACZ,IAAIC,WAAW;IACf,IAAIC,aAAa;IAEjB,MAAMC,QAAQ;QACZ,IAAI,CAACD,YAAY;QACjBA,aAAa;QACbJ,SAASG;IACX;IAEA,MAAMG,SAAS,CAACC;QACdJ,WAAWI,EAAEC,OAAO;QACpBJ,aAAa;QACb,IAAI,CAACF,OAAO;YACVA,QAAQO,OAAOC,qBAAqB,CAAC;gBACnCR,QAAQ;gBACRG;YACF;QACF;IACF;IAEA,IAAIM,QAAQ;IACZ,MAAMC,MAAM;QACV,IAAID,OAAO;QACXA,QAAQ;QACRF,OAAOI,mBAAmB,CAAC,aAAaP;QACxCG,OAAOI,mBAAmB,CAAC,WAAWD;QACtC,IAAIV,OAAO;YACTO,OAAOK,oBAAoB,CAACZ;YAC5BA,QAAQ;QACV;QACAG;QACAU,SAASC,IAAI,CAACC,KAAK,CAAClB,MAAM,GAAG;QAC7BgB,SAASC,IAAI,CAACC,KAAK,CAACC,UAAU,GAAG;QACjCjB;IACF;IAEAc,SAASC,IAAI,CAACC,KAAK,CAAClB,MAAM,GAAGA;IAC7BgB,SAASC,IAAI,CAACC,KAAK,CAACC,UAAU,GAAG;IACjCT,OAAOU,gBAAgB,CAAC,aAAab;IACrCG,OAAOU,gBAAgB,CAAC,WAAWP;IAEnC,OAAOA;AACT,EAAC"}
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Reads `iframe.contentDocument` defensively. Throws cross-origin —
3
3
  * returning `null` lets callers bail out without try/catch boilerplate.
4
+ *
5
+ * Firefox can hand back a Document whose `location` is `null` during the
6
+ * transient pre-navigation phase of a freshly mounted iframe. Treat that
7
+ * as "not ready" so callers don't trip a TypeError on `doc.location.href`.
4
8
  */
5
9
  export declare const getSameOriginDocument: (iframe: HTMLIFrameElement) => Document | null;
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Reads `iframe.contentDocument` defensively. Throws cross-origin —
3
3
  * returning `null` lets callers bail out without try/catch boilerplate.
4
+ *
5
+ * Firefox can hand back a Document whose `location` is `null` during the
6
+ * transient pre-navigation phase of a freshly mounted iframe. Treat that
7
+ * as "not ready" so callers don't trip a TypeError on `doc.location.href`.
4
8
  */ export const getSameOriginDocument = (iframe)=>{
5
9
  try {
6
- return iframe.contentDocument;
10
+ const doc = iframe.contentDocument;
11
+ if (!doc || !doc.location) return null;
12
+ return doc;
7
13
  } catch {
8
14
  return null;
9
15
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/internal/iframe.ts"],"sourcesContent":["/**\n * Reads `iframe.contentDocument` defensively. Throws cross-origin —\n * returning `null` lets callers bail out without try/catch boilerplate.\n */\nexport const getSameOriginDocument = (iframe: HTMLIFrameElement): Document | null => {\n try {\n return iframe.contentDocument\n } catch {\n return null\n }\n}\n"],"names":["getSameOriginDocument","iframe","contentDocument"],"mappings":"AAAA;;;CAGC,GACD,OAAO,MAAMA,wBAAwB,CAACC;IACpC,IAAI;QACF,OAAOA,OAAOC,eAAe;IAC/B,EAAE,OAAM;QACN,OAAO;IACT;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/internal/iframe.ts"],"sourcesContent":["/**\n * Reads `iframe.contentDocument` defensively. Throws cross-origin —\n * returning `null` lets callers bail out without try/catch boilerplate.\n *\n * Firefox can hand back a Document whose `location` is `null` during the\n * transient pre-navigation phase of a freshly mounted iframe. Treat that\n * as \"not ready\" so callers don't trip a TypeError on `doc.location.href`.\n */\nexport const getSameOriginDocument = (iframe: HTMLIFrameElement): Document | null => {\n try {\n const doc = iframe.contentDocument\n if (!doc || !doc.location) return null\n return doc\n } catch {\n return null\n }\n}\n"],"names":["getSameOriginDocument","iframe","doc","contentDocument","location"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,MAAMA,wBAAwB,CAACC;IACpC,IAAI;QACF,MAAMC,MAAMD,OAAOE,eAAe;QAClC,IAAI,CAACD,OAAO,CAACA,IAAIE,QAAQ,EAAE,OAAO;QAClC,OAAOF;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF,EAAC"}
@@ -73,6 +73,59 @@
73
73
  border-bottom-color: var(--theme-text);
74
74
  }
75
75
 
76
+ /* Validation summary — surfaces invalid fields across all blocks. */
77
+ .better-editor-sidebar__errors {
78
+ margin: 12px 16px 0;
79
+ padding: 10px 12px;
80
+ border: 1px solid var(--theme-error-250, #f1b0ad);
81
+ background: var(--theme-error-50, #fdeceb);
82
+ border-radius: 4px;
83
+ font-size: 13px;
84
+ line-height: 1.4;
85
+ }
86
+
87
+ .better-editor-sidebar__errors-title {
88
+ margin: 0 0 6px;
89
+ font-weight: 600;
90
+ color: var(--theme-error-600, #b22a22);
91
+ }
92
+
93
+ .better-editor-sidebar__errors-list {
94
+ margin: 0;
95
+ padding: 0;
96
+ list-style: none;
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 4px;
100
+ }
101
+
102
+ .better-editor-sidebar__errors-item {
103
+ display: flex;
104
+ flex-wrap: wrap;
105
+ gap: 4px 8px;
106
+ align-items: baseline;
107
+ }
108
+
109
+ .better-editor-sidebar__errors-jump {
110
+ padding: 0;
111
+ border: 0;
112
+ background: none;
113
+ font: inherit;
114
+ font-weight: 600;
115
+ color: var(--theme-error-600, #b22a22);
116
+ text-decoration: underline;
117
+ cursor: pointer;
118
+ }
119
+
120
+ .better-editor-sidebar__errors-label {
121
+ font-weight: 600;
122
+ color: var(--theme-error-600, #b22a22);
123
+ }
124
+
125
+ .better-editor-sidebar__errors-message {
126
+ color: var(--theme-elevation-600, #555);
127
+ }
128
+
76
129
  /* Visually hidden but available to assistive technology — used by the
77
130
  selection live region. Standard SR-only recipe. */
78
131
  .better-editor-sr-only {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.0.3";
1
+ export declare const VERSION = "1.1.0";
package/dist/version.js CHANGED
@@ -1,6 +1,5 @@
1
- // Single source of truth — re-exported from index.ts as `VERSION` and used
2
- // by client components (e.g. the settings footer) without dragging the
3
- // whole server entry into the client bundle.
4
- export const VERSION = '1.0.3';
1
+ // Package version, re-exported as VERSION; read by the settings banner without
2
+ // pulling the server entry into the client bundle.
3
+ export const VERSION = '1.1.0';
5
4
 
6
5
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// Single source of truth — re-exported from index.ts as `VERSION` and used\n// by client components (e.g. the settings footer) without dragging the\n// whole server entry into the client bundle.\nexport const VERSION = '1.0.3'\n"],"names":["VERSION"],"mappings":"AAAA,2EAA2E;AAC3E,uEAAuE;AACvE,6CAA6C;AAC7C,OAAO,MAAMA,UAAU,QAAO"}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// Package version, re-exported as VERSION; read by the settings banner without\n// pulling the server entry into the client bundle.\nexport const VERSION = '1.1.0'\n"],"names":["VERSION"],"mappings":"AAAA,+EAA+E;AAC/E,mDAAmD;AACnD,OAAO,MAAMA,UAAU,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-better-editor",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Block editor plugin for Payload CMS that adds a side-by-side live-preview iframe and sidebar to the edit view.",
5
5
  "license": "MIT",
6
6
  "author": "scorpio-99",
@@ -65,7 +65,6 @@
65
65
  "prepublishOnly": "pnpm clean && pnpm build"
66
66
  },
67
67
  "peerDependencies": {
68
- "lucide-react": ">=0.577.0",
69
68
  "payload": ">=3.81.0",
70
69
  "react": "^19.0.0",
71
70
  "react-dom": "^19.0.0"
@@ -75,13 +74,15 @@
75
74
  "@payloadcms/ui": "3.81.0",
76
75
  "@swc/cli": "0.6.0",
77
76
  "@swc/core": "1.10.7",
77
+ "@testing-library/dom": "^10.4.1",
78
+ "@testing-library/react": "^16.3.0",
78
79
  "@types/node": "22.5.4",
79
80
  "@types/react": "19.2.1",
80
81
  "@types/react-dom": "19.2.1",
81
82
  "copyfiles": "2.4.1",
82
83
  "eslint": "^9.16.0",
83
84
  "eslint-plugin-react-hooks": "^5.2.0",
84
- "lucide-react": "^0.577.0",
85
+ "jsdom": "^26.1.0",
85
86
  "payload": "3.81.0",
86
87
  "react": "19.2.1",
87
88
  "react-dom": "19.2.1",
@@ -1,2 +0,0 @@
1
- import { type RefObject } from 'react';
2
- export declare const useIframeResizeObserver: (iframeRef: RefObject<HTMLIFrameElement | null>, onIframeWidthChange?: (width: number) => void) => void;
@@ -1,20 +0,0 @@
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
@@ -1 +0,0 @@
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"}