@prenta/admin 1.9.2 → 1.10.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 (85) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/AdminRoot.d.ts +10 -0
  3. package/dist/AdminRoot.d.ts.map +1 -1
  4. package/dist/AdminRoot.js +2 -1
  5. package/dist/AdminRoot.js.map +1 -1
  6. package/dist/__tests__/lib/theme-chrome.test.d.ts +2 -0
  7. package/dist/__tests__/lib/theme-chrome.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/theme-chrome.test.js +104 -0
  9. package/dist/__tests__/lib/theme-chrome.test.js.map +1 -0
  10. package/dist/__tests__/views/editor-top-bar.render.test.js +11 -0
  11. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  12. package/dist/__tests__/views/theme-chrome-frame.render.test.d.ts +2 -0
  13. package/dist/__tests__/views/theme-chrome-frame.render.test.d.ts.map +1 -0
  14. package/dist/__tests__/views/theme-chrome-frame.render.test.js +236 -0
  15. package/dist/__tests__/views/theme-chrome-frame.render.test.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/theme-chrome.d.ts +87 -0
  21. package/dist/lib/theme-chrome.d.ts.map +1 -0
  22. package/dist/lib/theme-chrome.js +117 -0
  23. package/dist/lib/theme-chrome.js.map +1 -0
  24. package/dist/prenta-admin.css +1 -1
  25. package/dist/views/page-editor/CanvasSurface.d.ts +14 -2
  26. package/dist/views/page-editor/CanvasSurface.d.ts.map +1 -1
  27. package/dist/views/page-editor/CanvasSurface.js +30 -14
  28. package/dist/views/page-editor/CanvasSurface.js.map +1 -1
  29. package/dist/views/page-editor/EditorCanvas.d.ts +12 -1
  30. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  31. package/dist/views/page-editor/EditorCanvas.js +22 -5
  32. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  33. package/dist/views/page-editor/EditorTopBar.d.ts +5 -1
  34. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  35. package/dist/views/page-editor/EditorTopBar.js +3 -2
  36. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  37. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  38. package/dist/views/page-editor/PageSectionEditor.js +30 -2
  39. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  40. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.d.ts +21 -0
  41. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.d.ts.map +1 -0
  42. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.js +34 -0
  43. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.js.map +1 -0
  44. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.d.ts +12 -0
  45. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.d.ts.map +1 -0
  46. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.js +12 -0
  47. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.js.map +1 -0
  48. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.d.ts +11 -0
  49. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.d.ts.map +1 -0
  50. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.js +15 -0
  51. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.js.map +1 -0
  52. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.d.ts +18 -0
  53. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.d.ts.map +1 -0
  54. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.js +23 -0
  55. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.js.map +1 -0
  56. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.d.ts +11 -0
  57. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.d.ts.map +1 -0
  58. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.js +82 -0
  59. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.js.map +1 -0
  60. package/dist/views/page-editor/theme-chrome/index.d.ts +9 -0
  61. package/dist/views/page-editor/theme-chrome/index.d.ts.map +1 -0
  62. package/dist/views/page-editor/theme-chrome/index.js +6 -0
  63. package/dist/views/page-editor/theme-chrome/index.js.map +1 -0
  64. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.d.ts +3 -0
  65. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.d.ts.map +1 -0
  66. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.js +17 -0
  67. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.js.map +1 -0
  68. package/package.json +4 -4
  69. package/src/AdminRoot.tsx +14 -1
  70. package/src/__tests__/lib/theme-chrome.test.ts +132 -0
  71. package/src/__tests__/views/editor-top-bar.render.test.tsx +13 -0
  72. package/src/__tests__/views/theme-chrome-frame.render.test.tsx +304 -0
  73. package/src/index.ts +17 -0
  74. package/src/lib/theme-chrome.ts +172 -0
  75. package/src/views/page-editor/CanvasSurface.tsx +70 -24
  76. package/src/views/page-editor/EditorCanvas.tsx +89 -15
  77. package/src/views/page-editor/EditorTopBar.tsx +10 -0
  78. package/src/views/page-editor/PageSectionEditor.tsx +42 -0
  79. package/src/views/page-editor/theme-chrome/ThemeChromeFrame.tsx +119 -0
  80. package/src/views/page-editor/theme-chrome/ThemeChromeHealth.tsx +66 -0
  81. package/src/views/page-editor/theme-chrome/ThemeChromeModeToggle.tsx +63 -0
  82. package/src/views/page-editor/theme-chrome/ThemeChromeProvider.tsx +43 -0
  83. package/src/views/page-editor/theme-chrome/ThemeChromeSlot.tsx +235 -0
  84. package/src/views/page-editor/theme-chrome/index.ts +12 -0
  85. package/src/views/page-editor/theme-chrome/useThemeChromeMode.ts +24 -0
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { THEME_CHROME_MAIN_ID, } from '../../../lib/theme-chrome.js';
5
+ import { ThemeChromeSlot } from './ThemeChromeSlot.js';
6
+ export function ThemeChromeFrame({ spec, mode, breakpoint, locale, dir, document, onEditChrome, onHealthChange, children, }) {
7
+ const [headerStatus, setHeaderStatus] = useState('placeholder');
8
+ const [footerStatus, setFooterStatus] = useState('placeholder');
9
+ const [headerH1, setHeaderH1] = useState(false);
10
+ const [footerH1, setFooterH1] = useState(false);
11
+ const chromeProps = useMemo(() => ({
12
+ breakpoint,
13
+ mode,
14
+ preview: true,
15
+ locale,
16
+ dir,
17
+ document,
18
+ onEditChrome,
19
+ }), [breakpoint, mode, locale, dir, document, onEditChrome]);
20
+ const onHeaderOutline = useCallback((hasH1) => setHeaderH1(hasH1), []);
21
+ const onFooterOutline = useCallback((hasH1) => setFooterH1(hasH1), []);
22
+ useEffect(() => {
23
+ onHealthChange?.({
24
+ header: headerStatus,
25
+ footer: footerStatus,
26
+ outlineWarning: headerH1 || footerH1,
27
+ });
28
+ }, [headerStatus, footerStatus, headerH1, footerH1, onHealthChange]);
29
+ const headerProps = { ...chromeProps, region: 'header' };
30
+ const footerProps = { ...chromeProps, region: 'footer' };
31
+ const frameClass = mode === 'frame' ? 'flex h-full min-h-0 flex-col' : 'flex min-h-0 flex-col';
32
+ return (_jsxs("div", { className: frameClass, "data-prenta-theme-chrome-mode": mode, "data-prenta-theme-chrome-locale": locale, "data-prenta-theme-chrome-dir": dir, "data-prenta-theme-chrome-path": document.path, children: [_jsx("a", { href: `#${THEME_CHROME_MAIN_ID}`, className: "bg-primary text-primary-foreground sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-30 focus:rounded-md focus:px-3 focus:py-1.5 focus:text-sm", children: "Skip to content" }), _jsx("div", { className: mode === 'frame' ? 'shrink-0' : undefined, children: _jsx(ThemeChromeSlot, { region: "header", spec: spec, chromeProps: headerProps, onOutlineIssue: onHeaderOutline, onStatus: setHeaderStatus }) }), _jsx("div", { id: THEME_CHROME_MAIN_ID, className: mode === 'frame' ? 'min-h-0 flex-1 overflow-auto' : undefined, children: children }), _jsx("div", { className: mode === 'frame' ? 'shrink-0' : undefined, children: _jsx(ThemeChromeSlot, { region: "footer", spec: spec, chromeProps: footerProps, onOutlineIssue: onFooterOutline, onStatus: setFooterStatus }) })] }));
33
+ }
34
+ //# sourceMappingURL=ThemeChromeFrame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeFrame.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeFrame.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { ThemeChromeSlotStatus } from '../../../lib/theme-chrome.js';
2
+ export interface ThemeChromeHealthProps {
3
+ locale: string;
4
+ dir: 'ltr' | 'rtl';
5
+ header: ThemeChromeSlotStatus;
6
+ footer: ThemeChromeSlotStatus;
7
+ outlineWarning: boolean;
8
+ navLinkCount?: number;
9
+ brandPresent?: boolean;
10
+ }
11
+ export declare function ThemeChromeHealth({ locale, dir, header, footer, outlineWarning, navLinkCount, brandPresent, }: ThemeChromeHealthProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=ThemeChromeHealth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeHealth.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeHealth.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AAEzE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,KAAK,GAAG,KAAK,CAAA;IAClB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,MAAM,EAAE,qBAAqB,CAAA;IAC7B,cAAc,EAAE,OAAO,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AASD,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,GAAG,EACH,MAAM,EACN,MAAM,EACN,cAAc,EACd,YAAY,EACZ,YAAY,GACb,EAAE,sBAAsB,2CAoCxB"}
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ const STATUS_LABEL = {
4
+ ok: 'ok',
5
+ preview: 'preview',
6
+ fallback: 'fallback',
7
+ placeholder: 'View site',
8
+ };
9
+ export function ThemeChromeHealth({ locale, dir, header, footer, outlineWarning, navLinkCount, brandPresent, }) {
10
+ return (_jsxs("p", { className: "text-muted-foreground mb-3 flex flex-wrap items-center justify-center gap-x-1.5 gap-y-1 text-[11px] font-medium tracking-[0.3px]", "data-prenta-theme-chrome-health": "", "aria-live": "polite", children: [_jsxs("span", { className: "text-foreground/70 uppercase", children: [locale, " \u00B7 ", dir.toUpperCase()] }), _jsx("span", { "aria-hidden": true, children: "|" }), _jsxs("span", { children: ["Header ", STATUS_LABEL[header]] }), _jsx("span", { "aria-hidden": true, children: "|" }), _jsxs("span", { children: ["Footer ", STATUS_LABEL[footer]] }), typeof navLinkCount === 'number' ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "|" }), _jsxs("span", { children: ["Nav: ", navLinkCount, " links"] })] })) : null, brandPresent === true ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "|" }), _jsx("span", { children: "Brand" })] })) : null, outlineWarning ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "|" }), _jsx("span", { className: "text-destructive", "data-prenta-theme-chrome-outline-warning": "", children: "Chrome uses H1 \u2014 page outline will be wrong" })] })) : null] }));
11
+ }
12
+ //# sourceMappingURL=ThemeChromeHealth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeHealth.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeHealth.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAcZ,MAAM,YAAY,GAA0C;IAC1D,EAAE,EAAE,IAAI;IACR,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,UAAU;IACpB,WAAW,EAAE,WAAW;CACzB,CAAA;AAED,MAAM,UAAU,iBAAiB,CAAC,EAChC,MAAM,EACN,GAAG,EACH,MAAM,EACN,MAAM,EACN,cAAc,EACd,YAAY,EACZ,YAAY,GACW;IACvB,OAAO,CACL,aACE,SAAS,EAAC,kIAAkI,qCAC5G,EAAE,eACxB,QAAQ,aAElB,gBAAM,SAAS,EAAC,8BAA8B,aAC3C,MAAM,cAAK,GAAG,CAAC,WAAW,EAAE,IACxB,EACP,oDAA0B,EAC1B,sCAAc,YAAY,CAAC,MAAM,CAAC,IAAQ,EAC1C,oDAA0B,EAC1B,sCAAc,YAAY,CAAC,MAAM,CAAC,IAAQ,EACzC,OAAO,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAClC,8BACE,oDAA0B,EAC1B,oCAAY,YAAY,cAAc,IACrC,CACJ,CAAC,CAAC,CAAC,IAAI,EACP,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CACvB,8BACE,oDAA0B,EAC1B,mCAAkB,IACjB,CACJ,CAAC,CAAC,CAAC,IAAI,EACP,cAAc,CAAC,CAAC,CAAC,CAChB,8BACE,oDAA0B,EAC1B,eAAM,SAAS,EAAC,kBAAkB,8CAA0C,EAAE,iEAEvE,IACN,CACJ,CAAC,CAAC,CAAC,IAAI,IACN,CACL,CAAA;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ThemeChromeMode } from '../../../lib/theme-chrome.js';
2
+ export interface ThemeChromeModeToggleProps {
3
+ mode: ThemeChromeMode;
4
+ onChange: (mode: ThemeChromeMode) => void;
5
+ }
6
+ /**
7
+ * Page = real document scroll (sellable preview). Frame = pinned chrome
8
+ * for authoring. Frame is a working view, not the live page.
9
+ */
10
+ export declare function ThemeChromeModeToggle({ mode, onChange }: ThemeChromeModeToggleProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=ThemeChromeModeToggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeModeToggle.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeModeToggle.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAEnE,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,eAAe,CAAA;IACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;CAC1C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,0BAA0B,2CAqBnF"}
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Page = real document scroll (sellable preview). Frame = pinned chrome
5
+ * for authoring. Frame is a working view, not the live page.
6
+ */
7
+ export function ThemeChromeModeToggle({ mode, onChange }) {
8
+ return (_jsxs("div", { role: "group", "aria-label": "Canvas layout. Page scrolls like the live site. Frame pins the header and footer.", className: "border-border bg-card inline-flex h-[34px] overflow-hidden rounded-lg border", children: [_jsx(ModeButton, { label: "Page", pressed: mode === 'flow', title: "Page \u2014 header, bands, and footer scroll together like the live site.", onClick: () => onChange('flow') }), _jsx(ModeButton, { label: "Frame", pressed: mode === 'frame', title: "Frame \u2014 header and footer stay pinned while bands scroll. Authoring view, not the live page.", onClick: () => onChange('frame') })] }));
9
+ }
10
+ function ModeButton({ label, pressed, title, onClick, }) {
11
+ return (_jsx("button", { type: "button", "aria-pressed": pressed, title: title, onClick: onClick, className: `h-full px-2.5 text-[12.5px] font-medium transition-colors ${pressed
12
+ ? 'bg-primary text-primary-foreground'
13
+ : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}`, children: label }));
14
+ }
15
+ //# sourceMappingURL=ThemeChromeModeToggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeModeToggle.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeModeToggle.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AASZ;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAA8B;IAClF,OAAO,CACL,eACE,IAAI,EAAC,OAAO,gBACD,mFAAmF,EAC9F,SAAS,EAAC,8EAA8E,aAExF,KAAC,UAAU,IACT,KAAK,EAAC,MAAM,EACZ,OAAO,EAAE,IAAI,KAAK,MAAM,EACxB,KAAK,EAAC,2EAAsE,EAC5E,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAC/B,EACF,KAAC,UAAU,IACT,KAAK,EAAC,OAAO,EACb,OAAO,EAAE,IAAI,KAAK,OAAO,EACzB,KAAK,EAAC,mGAA8F,EACpG,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,GAChC,IACE,CACP,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,OAAO,EACP,KAAK,EACL,OAAO,GAMR;IACC,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,kBACC,OAAO,EACrB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,6DACT,OAAO;YACL,CAAC,CAAC,oCAAoC;YACtC,CAAC,CAAC,oEACN,EAAE,YAED,KAAK,GACC,CACV,CAAA;AACH,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { AdminThemeChrome, AdminThemeChromeProps } from '../../../lib/theme-chrome.js';
3
+ export declare function ThemeChromeProvider({ value, children, }: {
4
+ value: AdminThemeChrome | null;
5
+ children: ReactNode;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ /** The `themeChrome` prop passed to `<AdminRoot>`, or null when omitted. */
8
+ export declare function useThemeChromeSpec(): AdminThemeChrome | null;
9
+ export declare function ThemeChromeRenderProvider({ value, children, }: {
10
+ value: AdminThemeChromeProps;
11
+ children: ReactNode;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ /**
14
+ * Fields passed to a consumer `SiteHeader` / `SiteFooter` so they can
15
+ * highlight the current path and locale without prop-drilling.
16
+ */
17
+ export declare function useThemeChromeContext(): AdminThemeChromeProps | null;
18
+ //# sourceMappingURL=ThemeChromeProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeProvider.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AAI3F,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC9B,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAEA;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,gBAAgB,GAAG,IAAI,CAE5D;AAID,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,qBAAqB,CAAA;IAC5B,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAIA;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,qBAAqB,GAAG,IAAI,CAEpE"}
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ const ThemeChromeSpecContext = createContext(null);
5
+ export function ThemeChromeProvider({ value, children, }) {
6
+ return _jsx(ThemeChromeSpecContext.Provider, { value: value, children: children });
7
+ }
8
+ /** The `themeChrome` prop passed to `<AdminRoot>`, or null when omitted. */
9
+ export function useThemeChromeSpec() {
10
+ return useContext(ThemeChromeSpecContext);
11
+ }
12
+ const ThemeChromeRenderContext = createContext(null);
13
+ export function ThemeChromeRenderProvider({ value, children, }) {
14
+ return (_jsx(ThemeChromeRenderContext.Provider, { value: value, children: children }));
15
+ }
16
+ /**
17
+ * Fields passed to a consumer `SiteHeader` / `SiteFooter` so they can
18
+ * highlight the current path and locale without prop-drilling.
19
+ */
20
+ export function useThemeChromeContext() {
21
+ return useContext(ThemeChromeRenderContext);
22
+ }
23
+ //# sourceMappingURL=ThemeChromeProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeProvider.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeProvider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAA;AAGjE,MAAM,sBAAsB,GAAG,aAAa,CAA0B,IAAI,CAAC,CAAA;AAE3E,MAAM,UAAU,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,KAAC,sBAAsB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAmC,CAAA;AACpG,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,kBAAkB;IAChC,OAAO,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAC3C,CAAC;AAED,MAAM,wBAAwB,GAAG,aAAa,CAA+B,IAAI,CAAC,CAAA;AAElF,MAAM,UAAU,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,CACL,KAAC,wBAAwB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAqC,CAChG,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,UAAU,CAAC,wBAAwB,CAAC,CAAA;AAC7C,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type AdminThemeChrome, type AdminThemeChromeProps, type ThemeChromeSlotStatus } from '../../../lib/theme-chrome.js';
2
+ interface ThemeChromeSlotProps {
3
+ region: 'header' | 'footer';
4
+ spec: AdminThemeChrome;
5
+ chromeProps: AdminThemeChromeProps;
6
+ onOutlineIssue: (hasH1: boolean) => void;
7
+ onStatus: (status: ThemeChromeSlotStatus) => void;
8
+ }
9
+ export declare function ThemeChromeSlot({ region, spec, chromeProps, onOutlineIssue, onStatus, }: ThemeChromeSlotProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=ThemeChromeSlot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeSlot.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeSlot.tsx"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,8BAA8B,CAAA;AAGrC,UAAU,oBAAoB;IAC5B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,IAAI,EAAE,gBAAgB,CAAA;IACtB,WAAW,EAAE,qBAAqB,CAAA;IAClC,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAA;CAClD;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,IAAI,EACJ,WAAW,EACX,cAAc,EACd,QAAQ,GACT,EAAE,oBAAoB,2CAsFtB"}
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
4
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
5
+ import { Pencil } from 'lucide-react';
6
+ import { ErrorBoundary } from '../../../components/ErrorBoundary.js';
7
+ import { adminPortalContainer } from '../../../lib/portal-container.js';
8
+ import { closestAnchor, isSameOriginPreviewSrc, } from '../../../lib/theme-chrome.js';
9
+ import { ThemeChromeRenderProvider } from './ThemeChromeProvider.js';
10
+ export function ThemeChromeSlot({ region, spec, chromeProps, onOutlineIssue, onStatus, }) {
11
+ const rootRef = useRef(null);
12
+ const [menuOpen, setMenuOpen] = useState(false);
13
+ useLayoutEffect(() => {
14
+ onOutlineIssue(Boolean(rootRef.current?.querySelector('h1')));
15
+ });
16
+ const openEditMenu = useCallback(() => {
17
+ setMenuOpen(true);
18
+ }, []);
19
+ const onClickCapture = useCallback((event) => {
20
+ const target = event.target;
21
+ if (target instanceof Element && target.closest('[data-prenta-theme-chrome-edit]')) {
22
+ return;
23
+ }
24
+ if (closestAnchor(target)) {
25
+ event.preventDefault();
26
+ event.stopPropagation();
27
+ }
28
+ openEditMenu();
29
+ }, [openEditMenu]);
30
+ const onAuxClickCapture = useCallback((event) => {
31
+ if (closestAnchor(event.target)) {
32
+ event.preventDefault();
33
+ event.stopPropagation();
34
+ }
35
+ }, []);
36
+ const label = region === 'header' ? 'Site header' : 'Site footer';
37
+ const renderer = region === 'header' ? spec.header : spec.footer;
38
+ const previewSrc = region === 'header' ? spec.previewSrc?.header : spec.previewSrc?.footer;
39
+ return (_jsx(ThemeChromeRenderProvider, { value: chromeProps, children: _jsxs("div", { ref: rootRef, "data-prenta-theme-chrome": region, "aria-label": label, className: "relative", onClickCapture: onClickCapture, onAuxClickCapture: onAuxClickCapture, children: [_jsx(ErrorBoundary, { fallback: _jsx(ChromeFallback, { region: region, spec: spec, chromeProps: chromeProps, previewSrc: previewSrc, onStatus: onStatus }), children: _jsx(ChromeBody, { region: region, renderer: renderer, fallback: spec.fallback, previewSrc: previewSrc, chromeProps: chromeProps, onStatus: onStatus }) }), _jsxs(DropdownMenu.Root, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx("button", { type: "button", "data-prenta-theme-chrome-edit": "", "aria-label": `Edit ${label.toLowerCase()}`, title: `Edit ${label.toLowerCase()}`, className: "border-border bg-card text-foreground hover:bg-accent focus-visible:outline-ring absolute top-2 right-2 z-20 inline-flex h-9 w-9 items-center justify-center rounded-md border shadow-sm", children: _jsx(Pencil, { size: 16, "aria-hidden": true }) }) }), _jsx(ChromeEditMenu, { onEditChrome: chromeProps.onEditChrome })] })] }) }));
40
+ }
41
+ function ChromeEditMenu({ onEditChrome }) {
42
+ const menu = (_jsxs(DropdownMenu.Content, { align: "end", sideOffset: 6, className: "border-border bg-popover z-[120] min-w-[12rem] rounded-[10px] border p-[5px] shadow-[0_12px_34px_rgba(26,24,39,0.17)]", children: [_jsx(ChromeEditItem, { onSelect: () => onEditChrome('navigations'), label: "Edit navigation" }), _jsx(ChromeEditItem, { onSelect: () => onEditChrome('appearance'), label: "Edit appearance" }), _jsx(ChromeEditItem, { onSelect: () => onEditChrome('settings'), label: "Edit site settings" })] }));
43
+ const portalRoot = adminPortalContainer();
44
+ return portalRoot ? (_jsx(DropdownMenu.Portal, { container: portalRoot, children: menu })) : (menu);
45
+ }
46
+ function ChromeEditItem({ onSelect, label }) {
47
+ return (_jsx(DropdownMenu.Item, { onSelect: onSelect, className: "hover:bg-accent focus:bg-accent cursor-pointer rounded-md px-2.5 py-1.5 text-sm outline-none", children: label }));
48
+ }
49
+ function ChromeBody({ region, renderer, fallback, previewSrc, chromeProps, onStatus, }) {
50
+ useLayoutEffect(() => {
51
+ if (renderer)
52
+ onStatus('ok');
53
+ }, [renderer, onStatus]);
54
+ if (renderer) {
55
+ return _jsx(_Fragment, { children: renderer(chromeProps) });
56
+ }
57
+ return (_jsx(ChromeFallback, { region: region, spec: { fallback, previewSrc: previewSrc ? { [region]: previewSrc } : undefined }, chromeProps: chromeProps, previewSrc: previewSrc, onStatus: onStatus }));
58
+ }
59
+ function ChromeFallback({ region, spec, chromeProps, previewSrc, onStatus, }) {
60
+ const usePreview = Boolean(previewSrc && isSameOriginPreviewSrc(previewSrc));
61
+ const useFallback = Boolean(!usePreview && spec.fallback);
62
+ useLayoutEffect(() => {
63
+ if (usePreview)
64
+ onStatus('preview');
65
+ else if (useFallback)
66
+ onStatus('fallback');
67
+ else
68
+ onStatus('placeholder');
69
+ }, [usePreview, useFallback, onStatus]);
70
+ if (usePreview && previewSrc) {
71
+ return (_jsx("iframe", { src: previewSrc, title: region === 'header' ? 'Site header preview' : 'Site footer preview', sandbox: "allow-same-origin", className: "block w-full border-0" }));
72
+ }
73
+ if (useFallback && spec.fallback) {
74
+ return _jsx(_Fragment, { children: spec.fallback(chromeProps) });
75
+ }
76
+ return _jsx(ChromePlaceholder, { region: region });
77
+ }
78
+ function ChromePlaceholder({ region }) {
79
+ const label = region === 'header' ? 'Site header' : 'Site footer';
80
+ return (_jsxs("div", { "data-prenta-theme-chrome-placeholder": region, className: "border-border bg-muted text-muted-foreground flex flex-col items-start gap-1 border-b px-6 py-4 text-left text-sm", children: [_jsx("p", { className: "text-foreground font-medium", children: label }), _jsxs("p", { children: ["This site has not supplied a client-safe ", region, " for the canvas. Use", ' ', _jsx("span", { className: "text-foreground", children: "View site" }), " to see the live page."] })] }));
81
+ }
82
+ //# sourceMappingURL=ThemeChromeSlot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeChromeSlot.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/ThemeChromeSlot.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export { ThemeChromeProvider, useThemeChromeSpec, useThemeChromeContext, } from './ThemeChromeProvider.js';
2
+ export { ThemeChromeFrame } from './ThemeChromeFrame.js';
3
+ export type { ThemeChromeFrameProps, ThemeChromeHealthState } from './ThemeChromeFrame.js';
4
+ export { ThemeChromeHealth } from './ThemeChromeHealth.js';
5
+ export type { ThemeChromeHealthProps } from './ThemeChromeHealth.js';
6
+ export { ThemeChromeModeToggle } from './ThemeChromeModeToggle.js';
7
+ export type { ThemeChromeModeToggleProps } from './ThemeChromeModeToggle.js';
8
+ export { useThemeChromeMode } from './useThemeChromeMode.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA"}
@@ -0,0 +1,6 @@
1
+ export { ThemeChromeProvider, useThemeChromeSpec, useThemeChromeContext, } from './ThemeChromeProvider.js';
2
+ export { ThemeChromeFrame } from './ThemeChromeFrame.js';
3
+ export { ThemeChromeHealth } from './ThemeChromeHealth.js';
4
+ export { ThemeChromeModeToggle } from './ThemeChromeModeToggle.js';
5
+ export { useThemeChromeMode } from './useThemeChromeMode.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAE1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA"}
@@ -0,0 +1,3 @@
1
+ import { type ThemeChromeMode } from '../../../lib/theme-chrome.js';
2
+ export declare function useThemeChromeMode(defaultMode?: ThemeChromeMode): readonly [ThemeChromeMode, (next: ThemeChromeMode) => void];
3
+ //# sourceMappingURL=useThemeChromeMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useThemeChromeMode.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/useThemeChromeMode.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAA;AAErC,wBAAgB,kBAAkB,CAAC,WAAW,GAAE,eAAwB,qCAQnC,eAAe,WAMnD"}
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { readStoredChromeMode, writeStoredChromeMode, } from '../../../lib/theme-chrome.js';
4
+ export function useThemeChromeMode(defaultMode = 'flow') {
5
+ const [mode, setModeState] = useState(defaultMode);
6
+ useEffect(() => {
7
+ const stored = readStoredChromeMode();
8
+ if (stored)
9
+ setModeState(stored);
10
+ }, []);
11
+ const setMode = useCallback((next) => {
12
+ setModeState(next);
13
+ writeStoredChromeMode(next);
14
+ }, []);
15
+ return [mode, setMode];
16
+ }
17
+ //# sourceMappingURL=useThemeChromeMode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useThemeChromeMode.js","sourceRoot":"","sources":["../../../../src/views/page-editor/theme-chrome/useThemeChromeMode.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACxD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,8BAA8B,CAAA;AAErC,MAAM,UAAU,kBAAkB,CAAC,cAA+B,MAAM;IACtE,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAkB,WAAW,CAAC,CAAA;IAEnE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;QACrC,IAAI,MAAM;YAAE,YAAY,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,IAAqB,EAAE,EAAE;QACpD,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,qBAAqB,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CAAC,IAAI,EAAE,OAAO,CAAU,CAAA;AACjC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prenta/admin",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -98,16 +98,16 @@
98
98
  "tailwindcss": "^4.0.0",
99
99
  "typescript": "^5.7.0",
100
100
  "vitest": "^4.1.0",
101
- "@prenta/core": "5.0.2",
102
101
  "@prenta/component-blocks": "0.3.1",
102
+ "@prenta/core": "5.0.3",
103
103
  "@prenta/plugin-ai": "1.0.0",
104
104
  "@prenta/plugin-blocks": "0.2.0",
105
105
  "@prenta/plugin-commerce": "0.2.1",
106
106
  "@prenta/plugin-forms": "0.1.0",
107
- "@prenta/plugin-navigation": "0.3.0",
108
107
  "@prenta/plugin-media": "0.2.0",
108
+ "@prenta/plugin-navigation": "0.3.0",
109
109
  "@prenta/plugin-seo": "0.1.2",
110
- "@prenta/sections-react": "0.17.26"
110
+ "@prenta/sections-react": "0.17.27"
111
111
  },
112
112
  "scripts": {
113
113
  "build": "tsc --project tsconfig.json && npx @tailwindcss/cli -i src/styles/build-input.css -o dist/prenta-admin.css --minify",
package/src/AdminRoot.tsx CHANGED
@@ -47,6 +47,8 @@ import {
47
47
  AdminSectionRenderersProvider,
48
48
  type AdminSectionRendererMap,
49
49
  } from './views/page-editor/sections/index.js'
50
+ import { ThemeChromeProvider } from './views/page-editor/theme-chrome/index.js'
51
+ import type { AdminThemeChrome } from './lib/theme-chrome.js'
50
52
  import { Trash } from './views/Trash.js'
51
53
  import { PagesListView } from './views/Pages/PagesListView.js'
52
54
  import { PageTagManager } from './views/Pages/PageTagManager.js'
@@ -131,6 +133,15 @@ export interface AdminRootProps {
131
133
  * the admin route, or the canvas renders correct markup unstyled.
132
134
  */
133
135
  sectionRenderers?: AdminSectionRendererMap
136
+ /**
137
+ * Site header/footer painted around page-editor bands. Client-safe React
138
+ * only — never put these components on `prenta.config.ts` (that file is
139
+ * server-loaded). Omit to keep today's sections-only canvas.
140
+ *
141
+ * Default mode is in-flow (real page scroll). Frame mode is an authoring
142
+ * toggle. View site remains the pixel-perfect and head-tag check.
143
+ */
144
+ themeChrome?: AdminThemeChrome
134
145
  /** Browser Sentry DSN for admin error reporting (e.g. `NEXT_PUBLIC_SENTRY_DSN`). */
135
146
  sentryDsn?: string
136
147
  }
@@ -855,7 +866,9 @@ export function AdminRoot(props: AdminRootProps) {
855
866
  <ThemeProvider defaultDarkMode={defaultDarkMode}>
856
867
  <LocaleProvider config={props.config}>
857
868
  <AdminSectionRenderersProvider value={props.sectionRenderers ?? EMPTY_RENDERERS}>
858
- <AdminShell {...props} />
869
+ <ThemeChromeProvider value={props.themeChrome ?? null}>
870
+ <AdminShell {...props} />
871
+ </ThemeChromeProvider>
859
872
  </AdminSectionRenderersProvider>
860
873
  </LocaleProvider>
861
874
  </ThemeProvider>
@@ -0,0 +1,132 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it } from 'vitest'
3
+ import { auditPage } from '@prenta/core/seo/page-audit'
4
+ import {
5
+ chromeContainsH1,
6
+ closestAnchor,
7
+ isSameOriginPreviewSrc,
8
+ isThemeChromeEnabled,
9
+ resolveThemeChromeDir,
10
+ resolveThemeChromeLocale,
11
+ resolveThemeChromePath,
12
+ themeChromeEditPath,
13
+ } from '../../lib/theme-chrome.js'
14
+
15
+ describe('isThemeChromeEnabled', () => {
16
+ const spec = {}
17
+
18
+ it('is off when the AdminRoot prop is omitted', () => {
19
+ expect(isThemeChromeEnabled(null, undefined, 'pages')).toBe(false)
20
+ })
21
+
22
+ it('defaults to pages only', () => {
23
+ expect(isThemeChromeEnabled(spec, undefined, 'pages')).toBe(true)
24
+ expect(isThemeChromeEnabled(spec, undefined, 'posts')).toBe(false)
25
+ })
26
+
27
+ it('honours the config kill-switch', () => {
28
+ expect(isThemeChromeEnabled(spec, false, 'pages')).toBe(false)
29
+ expect(isThemeChromeEnabled(spec, { pages: false }, 'pages')).toBe(false)
30
+ expect(isThemeChromeEnabled({ surfaces: ['pages', 'posts'] }, { posts: false }, 'posts')).toBe(
31
+ false,
32
+ )
33
+ })
34
+ })
35
+
36
+ describe('isSameOriginPreviewSrc', () => {
37
+ it('accepts same-origin relative paths', () => {
38
+ expect(isSameOriginPreviewSrc('/preview/chrome/header')).toBe(true)
39
+ })
40
+
41
+ it('rejects protocol-relative and off-origin URLs', () => {
42
+ expect(isSameOriginPreviewSrc('//evil.example/x')).toBe(false)
43
+ expect(isSameOriginPreviewSrc('https://evil.example/header', 'https://cms.example')).toBe(false)
44
+ expect(isSameOriginPreviewSrc('javascript:alert(1)')).toBe(false)
45
+ expect(isSameOriginPreviewSrc('data:text/html,hi')).toBe(false)
46
+ })
47
+
48
+ it('accepts an absolute URL on the same origin', () => {
49
+ expect(
50
+ isSameOriginPreviewSrc('https://cms.example/preview/chrome/header', 'https://cms.example'),
51
+ ).toBe(true)
52
+ })
53
+ })
54
+
55
+ describe('theme chrome locale and path', () => {
56
+ it('prefers document locale, then active, then default', () => {
57
+ expect(
58
+ resolveThemeChromeLocale({
59
+ documentLocale: 'ar',
60
+ activeLocale: 'en',
61
+ defaultLocale: 'de',
62
+ }),
63
+ ).toBe('ar')
64
+ expect(resolveThemeChromeLocale({ activeLocale: 'fr', defaultLocale: 'en' })).toBe('fr')
65
+ expect(resolveThemeChromeLocale({})).toBe('en')
66
+ })
67
+
68
+ it('resolves dir from i18n locale list', () => {
69
+ expect(resolveThemeChromeDir('ar', [{ code: 'ar', dir: 'rtl' }])).toBe('rtl')
70
+ expect(resolveThemeChromeDir('en', [{ code: 'en' }])).toBe('ltr')
71
+ })
72
+
73
+ it('uses the public path, never an admin route', () => {
74
+ expect(resolveThemeChromePath('/about', 'about')).toBe('/about')
75
+ expect(resolveThemeChromePath('', 'home')).toBe('/')
76
+ expect(resolveThemeChromePath('contact')).toBe('/contact')
77
+ expect(resolveThemeChromePath('')).toBe('/')
78
+ })
79
+
80
+ it('maps edit targets to existing admin routes', () => {
81
+ expect(themeChromeEditPath('navigations')).toBe('/navigations')
82
+ expect(themeChromeEditPath('appearance')).toBe('/settings?tab=appearance')
83
+ expect(themeChromeEditPath('settings')).toBe('/settings?tab=general')
84
+ })
85
+ })
86
+
87
+ describe('chrome outline helpers', () => {
88
+ it('detects an h1 inside chrome', () => {
89
+ const root = document.createElement('div')
90
+ root.innerHTML = '<header><h1>Site</h1></header>'
91
+ expect(chromeContainsH1(root)).toBe(true)
92
+ root.innerHTML = '<header><p>Site</p></header>'
93
+ expect(chromeContainsH1(root)).toBe(false)
94
+ })
95
+
96
+ it('finds the closest anchor', () => {
97
+ const root = document.createElement('div')
98
+ root.innerHTML = '<a href="/about"><span>About</span></a>'
99
+ const span = root.querySelector('span')
100
+ expect(closestAnchor(span)?.getAttribute('href')).toBe('/about')
101
+ expect(closestAnchor(root)).toBeNull()
102
+ })
103
+ })
104
+
105
+ describe('page-audit stays body-only', () => {
106
+ it('does not pass brand-up-front just because a brand name exists', () => {
107
+ const result = auditPage({
108
+ sections: [
109
+ {
110
+ id: 's1',
111
+ sectionType: 'text',
112
+ name: 'Body',
113
+ visible: true,
114
+ content: { body: 'A long enough opening that still never names the company at all.' },
115
+ settings: {},
116
+ },
117
+ ],
118
+ seo: {
119
+ title: 'About',
120
+ description: 'About the firm',
121
+ slug: 'about',
122
+ keyword: '',
123
+ schema: 'WebPage',
124
+ robots: { index: true, snippet: true },
125
+ },
126
+ llmsTxt: true,
127
+ brand: 'Acme Legal',
128
+ })
129
+ const brand = result.geo.checks.find((c) => c.id === 'brand-up-front')
130
+ expect(brand?.outcome).toBe('fail')
131
+ })
132
+ })
@@ -241,4 +241,17 @@ describe('EditorTopBar history button', () => {
241
241
  expect(screen.getByRole('button', { name: 'Post settings' })).toBeTruthy()
242
242
  expect(screen.queryByRole('button', { name: 'Page settings' })).toBeNull()
243
243
  })
244
+
245
+ it('hides the Page / Frame toggle until theme chrome is on', () => {
246
+ render(<EditorTopBar {...baseProps} />)
247
+ expect(screen.queryByRole('group', { name: /Canvas layout/ })).toBeNull()
248
+ })
249
+
250
+ it('toggles Page and Frame when theme chrome is enabled', () => {
251
+ const onChromeMode = vi.fn()
252
+ render(<EditorTopBar {...baseProps} chromeMode="flow" onChromeMode={onChromeMode} />)
253
+ expect(screen.getByRole('button', { name: /^Page$/ }).getAttribute('aria-pressed')).toBe('true')
254
+ fireEvent.click(screen.getByRole('button', { name: /^Frame$/ }))
255
+ expect(onChromeMode).toHaveBeenCalledWith('frame')
256
+ })
244
257
  })