diff --git a/web-previews/src/entrypoints/Inspector/UI/EditModeToggle/index.tsx b/web-previews/src/components/Browser/Toolbar/EditModeToggle/index.tsx
similarity index 80%
rename from web-previews/src/entrypoints/Inspector/UI/EditModeToggle/index.tsx
rename to web-previews/src/components/Browser/Toolbar/EditModeToggle/index.tsx
index 6de724a4..e1ced2a3 100644
--- a/web-previews/src/entrypoints/Inspector/UI/EditModeToggle/index.tsx
+++ b/web-previews/src/components/Browser/Toolbar/EditModeToggle/index.tsx
@@ -5,18 +5,21 @@ import {
TooltipContent,
TooltipTrigger,
} from 'datocms-react-ui';
+import type { ReactNode } from 'react';
import styles from './styles.module.css';
interface EditModeToggleProps {
value: boolean;
disabled: boolean;
- onChange: () => void;
+ onChange: (value: boolean) => void;
+ tooltip?: ReactNode;
}
export function EditModeToggle({
value,
disabled,
onChange,
+ tooltip,
}: EditModeToggleProps) {
return (
@@ -32,7 +35,7 @@ export function EditModeToggle({
-
+ {tooltip ?? }
);
diff --git a/web-previews/src/entrypoints/Inspector/UI/EditModeToggle/styles.module.css b/web-previews/src/components/Browser/Toolbar/EditModeToggle/styles.module.css
similarity index 100%
rename from web-previews/src/entrypoints/Inspector/UI/EditModeToggle/styles.module.css
rename to web-previews/src/components/Browser/Toolbar/EditModeToggle/styles.module.css
diff --git a/web-previews/src/entrypoints/Inspector/ContentLinkContext/index.tsx b/web-previews/src/entrypoints/Inspector/ContentLinkContext/index.tsx
index 8f8a506e..fab79594 100644
--- a/web-previews/src/entrypoints/Inspector/ContentLinkContext/index.tsx
+++ b/web-previews/src/entrypoints/Inspector/ContentLinkContext/index.tsx
@@ -12,14 +12,14 @@ import {
useState,
} from 'react';
import type { Frontend } from '../../../types';
-import { inspectorUrl } from '../../../utils/urls';
-import { normalizePathForVisualEditing } from '../normalizePathForVisualEditing';
import {
type ContentLinkMethods,
type ContentLinkState,
SYMBOL_FOR_PRIMARY_ENVIRONMENT,
-} from './types';
-import useContentLinkConnection from './useContentLinkConnection';
+} from '../../../utils/contentLink/types';
+import useContentLinkConnection from '../../../utils/contentLink/useContentLinkConnection';
+import { normalizePathForVisualEditing } from '../../../utils/normalizePathForVisualEditing';
+import { inspectorUrl } from '../../../utils/urls';
type IframeState = { path: string; key: string };
diff --git a/web-previews/src/entrypoints/Inspector/UI/index.tsx b/web-previews/src/entrypoints/Inspector/UI/index.tsx
index c2e841d2..353dbf83 100644
--- a/web-previews/src/entrypoints/Inspector/UI/index.tsx
+++ b/web-previews/src/entrypoints/Inspector/UI/index.tsx
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BrowserWrapper } from '../../../components/Browser/BrowserWrapper';
import { IframeContainer } from '../../../components/Browser/IframeContainer';
import { Toolbar } from '../../../components/Browser/Toolbar';
+import { EditModeToggle } from '../../../components/Browser/Toolbar/EditModeToggle';
import { ToolbarSlot } from '../../../components/Browser/Toolbar/ToolbarSlot';
import type { ViewportSize } from '../../../components/Browser/ViewportCustomizer';
import { ViewportCustomizer } from '../../../components/Browser/ViewportCustomizer';
@@ -17,11 +18,10 @@ import {
type Parameters,
type Viewport,
} from '../../../types';
+import { normalizePathForVisualEditing } from '../../../utils/normalizePathForVisualEditing';
import { inspectorUrl } from '../../../utils/urls';
import { useContentLink } from '../ContentLinkContext';
-import { normalizePathForVisualEditing } from '../normalizePathForVisualEditing';
import AddressBar from './AddressBar';
-import { EditModeToggle } from './EditModeToggle';
const UI: React.FC = () => {
const ctx = useCtx();
diff --git a/web-previews/src/entrypoints/SidebarFrame/index.tsx b/web-previews/src/entrypoints/SidebarFrame/index.tsx
index 27946bf5..430b9eef 100644
--- a/web-previews/src/entrypoints/SidebarFrame/index.tsx
+++ b/web-previews/src/entrypoints/SidebarFrame/index.tsx
@@ -13,6 +13,7 @@ import { BrowserWrapper } from '../../components/Browser/BrowserWrapper';
import styles from '../../components/Browser/BrowserWrapper/styles.module.css';
import { IframeContainer } from '../../components/Browser/IframeContainer';
import { Toolbar } from '../../components/Browser/Toolbar';
+import { EditModeToggle } from '../../components/Browser/Toolbar/EditModeToggle';
import { ToolbarButton } from '../../components/Browser/Toolbar/ToolbarButton';
import { ToolbarSlot } from '../../components/Browser/Toolbar/ToolbarSlot';
import type { ViewportSize } from '../../components/Browser/ViewportCustomizer';
@@ -26,12 +27,17 @@ import {
type Viewport,
} from '../../types';
import { useStatusByFrontend } from '../../utils/common';
+import { usePersistedSidebarVisualEditing } from '../../utils/persistedSidebarVisualEditing';
import { usePersistedSidebarWidth } from '../../utils/persistedWidth';
import {
extractRedirectFromDraftModePreviewUrl,
inspectorUrl,
} from '../../utils/urls';
import { PreviewLinkSelector } from './PreviewLinkSelector';
+import {
+ linkSupportsVisualEditing,
+ useSidebarContentLink,
+} from './useSidebarContentLink';
type PropTypes = {
ctx: RenderItemFormSidebarCtx;
@@ -73,6 +79,24 @@ const SidebarFrame = ({ ctx }: PropTypes) => {
usePersistedSidebarWidth(ctx.site);
+ const [editModeEnabled, setEditModeEnabled] =
+ usePersistedSidebarVisualEditing(ctx.site);
+
+ const { iframeRef } = useSidebarContentLink(ctx, editModeEnabled);
+
+ const currentFrontend = currentPreviewLink
+ ? frontends.find((f) => f.name === currentPreviewLink.frontendName)
+ : undefined;
+
+ const currentLinkSupportsVisualEditing = linkSupportsVisualEditing(
+ currentPreviewLink,
+ currentFrontend,
+ );
+
+ const editModeTooltip = currentLinkSupportsVisualEditing
+ ? 'Click elements in the preview to open their record editor'
+ : "This preview link's frontend doesn't support visual editing";
+
const allPreviewLinksWithFrontend = useMemo(() => {
if (!statusByFrontend) return [];
@@ -194,6 +218,14 @@ const SidebarFrame = ({ ctx }: PropTypes) => {
+
+
+
>
)}
@@ -210,6 +242,9 @@ const SidebarFrame = ({ ctx }: PropTypes) => {
.+?)(?:\/environments\/(?[^/]+))?\/editor\/item_types\/(?[^/]+)\/items\/(?[^/]+)\/edit#fieldPath=(?.+)$/;
+
+function itemEditorUrl(ctx: RenderItemFormSidebarCtx, info: EditUrlInfo) {
+ // Mirror inspectorUrl()'s environment-prefix convention (utils/urls.tsx), and
+ // match the record-editor route that DatoCMS edit URLs use (ends in /edit).
+ const prefix = ctx.isEnvironmentPrimary
+ ? ''
+ : `/environments/${ctx.environment}`;
+
+ const url = `${prefix}/editor/item_types/${info.itemTypeId}/items/${info.itemId}/edit`;
+
+ // The #fieldPath hash makes the Studio scroll to and highlight the specific
+ // field — essential when the clicked element belongs to the record already
+ // open in the form.
+ return info.fieldPath ? `${url}#fieldPath=${info.fieldPath}` : url;
+}
+
+/**
+ * Wires a sidebar preview iframe to @datocms/content-link. Unlike the Inspector
+ * (which opens records inside an inspector panel), a click here navigates the
+ * whole Studio to the record's editor page.
+ */
+export function useSidebarContentLink(
+ ctx: RenderItemFormSidebarCtx,
+ editModeEnabled: boolean,
+) {
+ const currentEnvironmentId = ctx.isEnvironmentPrimary
+ ? SYMBOL_FOR_PRIMARY_ENVIRONMENT
+ : ctx.environment;
+
+ const { iframeRef, connection } = useContentLinkConnection({
+ onInit: () => ({
+ editUrlRegExp: {
+ source: editUrlRegExp.source,
+ flags: editUrlRegExp.flags,
+ },
+ }),
+ // The sidebar has no "records in this page" panel to sync.
+ onStateChange: () => {},
+ onPing: () => {},
+ openItem: async (info) => {
+ // Ignore clicks on records that belong to a different environment.
+ if (info.environment !== currentEnvironmentId) {
+ return;
+ }
+
+ await ctx.navigateTo(itemEditorUrl(ctx, info));
+ },
+ });
+
+ // Keep the click-to-edit overlay in sync with the toggle. The connection
+ // itself stays alive regardless of the toggle, so flipping it never reloads
+ // the iframe — the user keeps whatever page they browsed to.
+ useEffect(() => {
+ if (connection.type !== 'connected') {
+ return;
+ }
+
+ connection.methods.setClickToEditEnabled(
+ editModeEnabled
+ ? { enabled: true, flash: { scrollToNearestTarget: false } }
+ : { enabled: false },
+ );
+ }, [connection, editModeEnabled]);
+
+ return { iframeRef };
+}
diff --git a/web-previews/src/entrypoints/Inspector/ContentLinkContext/types.ts b/web-previews/src/utils/contentLink/types.ts
similarity index 100%
rename from web-previews/src/entrypoints/Inspector/ContentLinkContext/types.ts
rename to web-previews/src/utils/contentLink/types.ts
diff --git a/web-previews/src/entrypoints/Inspector/ContentLinkContext/useContentLinkConnection.ts b/web-previews/src/utils/contentLink/useContentLinkConnection.ts
similarity index 100%
rename from web-previews/src/entrypoints/Inspector/ContentLinkContext/useContentLinkConnection.ts
rename to web-previews/src/utils/contentLink/useContentLinkConnection.ts
diff --git a/web-previews/src/entrypoints/Inspector/ContentLinkContext/useMethodProxy.ts b/web-previews/src/utils/contentLink/useMethodProxy.ts
similarity index 100%
rename from web-previews/src/entrypoints/Inspector/ContentLinkContext/useMethodProxy.ts
rename to web-previews/src/utils/contentLink/useMethodProxy.ts
diff --git a/web-previews/src/entrypoints/Inspector/normalizePathForVisualEditing.ts b/web-previews/src/utils/normalizePathForVisualEditing.ts
similarity index 100%
rename from web-previews/src/entrypoints/Inspector/normalizePathForVisualEditing.ts
rename to web-previews/src/utils/normalizePathForVisualEditing.ts
diff --git a/web-previews/src/utils/persistedSidebarVisualEditing.ts b/web-previews/src/utils/persistedSidebarVisualEditing.ts
new file mode 100644
index 00000000..c3397c6c
--- /dev/null
+++ b/web-previews/src/utils/persistedSidebarVisualEditing.ts
@@ -0,0 +1,34 @@
+import type { Site } from 'datocms-plugin-sdk';
+import { useCallback, useState } from 'react';
+
+function keyForSidebarVisualEditing(site: Site) {
+ return `site.${site.id}.sidebarVisualEditing`;
+}
+
+export function readSidebarVisualEditing(site: Site): boolean {
+ // Per-user preference, disabled by default (opt-in).
+ return localStorage.getItem(keyForSidebarVisualEditing(site)) === 'true';
+}
+
+function saveSidebarVisualEditing(site: Site, enabled: boolean) {
+ localStorage.setItem(keyForSidebarVisualEditing(site), String(enabled));
+}
+
+/**
+ * Per-user toggle for enabling visual-editing navigation inside the sidebar
+ * preview. Persisted in localStorage (keyed by site), mirroring the sidebar
+ * width preference.
+ */
+export function usePersistedSidebarVisualEditing(site: Site) {
+ const [enabled, setEnabled] = useState(() => readSidebarVisualEditing(site));
+
+ const setPersisted = useCallback(
+ (value: boolean) => {
+ setEnabled(value);
+ saveSidebarVisualEditing(site, value);
+ },
+ [site],
+ );
+
+ return [enabled, setPersisted] as const;
+}
diff --git a/web-previews/tsconfig.app.tsbuildinfo b/web-previews/tsconfig.app.tsbuildinfo
index c5a67190..073bb36d 100644
--- a/web-previews/tsconfig.app.tsbuildinfo
+++ b/web-previews/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/browser/browserwrapper/index.tsx","./src/components/browser/iframecontainer/iframestyles.ts","./src/components/browser/iframecontainer/index.tsx","./src/components/browser/iframecontainer/useiframescaling.ts","./src/components/browser/toolbar/index.tsx","./src/components/browser/toolbar/toolbarbutton/index.tsx","./src/components/browser/toolbar/toolbarslot/index.tsx","./src/components/browser/viewportcustomizer/index.tsx","./src/components/browser/viewportselector/index.tsx","./src/components/buttongroup/index.tsx","./src/entrypoints/configscreen/index.tsx","./src/entrypoints/configscreen/iconpickerinput/iconpickermodal.tsx","./src/entrypoints/configscreen/iconpickerinput/index.tsx","./src/entrypoints/configscreen/components/customheaderslist.tsx","./src/entrypoints/configscreen/components/frontendfielditem.tsx","./src/entrypoints/configscreen/components/previewlinkssection.tsx","./src/entrypoints/configscreen/components/previewlinkssettings.tsx","./src/entrypoints/configscreen/components/viewportfielditem.tsx","./src/entrypoints/configscreen/components/visualeditingsection.tsx","./src/entrypoints/inspector/index.tsx","./src/entrypoints/inspector/normalizepathforvisualediting.ts","./src/entrypoints/inspector/contentlinkcontext/index.tsx","./src/entrypoints/inspector/contentlinkcontext/types.ts","./src/entrypoints/inspector/contentlinkcontext/usecontentlinkconnection.ts","./src/entrypoints/inspector/contentlinkcontext/usemethodproxy.ts","./src/entrypoints/inspector/ui/index.tsx","./src/entrypoints/inspector/ui/addressbar/index.tsx","./src/entrypoints/inspector/ui/addressbar/frontendselector/index.tsx","./src/entrypoints/inspector/ui/editmodetoggle/index.tsx","./src/entrypoints/inspectorloading/index.tsx","./src/entrypoints/sidebarframe/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/frontendgroup/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/frontendpreviewlinks/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/trigger/index.tsx","./src/entrypoints/sidebarpanel/index.tsx","./src/entrypoints/wrongenvironmentpanel/index.tsx","./src/utils/common.ts","./src/utils/persistedwidth.ts","./src/utils/render.tsx","./src/utils/urls.tsx","./penpal.d.ts"],"version":"5.9.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/browser/browserwrapper/index.tsx","./src/components/browser/iframecontainer/iframestyles.ts","./src/components/browser/iframecontainer/index.tsx","./src/components/browser/iframecontainer/useiframescaling.ts","./src/components/browser/toolbar/index.tsx","./src/components/browser/toolbar/editmodetoggle/index.tsx","./src/components/browser/toolbar/toolbarbutton/index.tsx","./src/components/browser/toolbar/toolbarslot/index.tsx","./src/components/browser/viewportcustomizer/index.tsx","./src/components/browser/viewportselector/index.tsx","./src/components/buttongroup/index.tsx","./src/entrypoints/configscreen/index.tsx","./src/entrypoints/configscreen/iconpickerinput/iconpickermodal.tsx","./src/entrypoints/configscreen/iconpickerinput/index.tsx","./src/entrypoints/configscreen/components/customheaderslist.tsx","./src/entrypoints/configscreen/components/frontendfielditem.tsx","./src/entrypoints/configscreen/components/previewlinkssection.tsx","./src/entrypoints/configscreen/components/previewlinkssettings.tsx","./src/entrypoints/configscreen/components/viewportfielditem.tsx","./src/entrypoints/configscreen/components/visualeditingsection.tsx","./src/entrypoints/inspector/index.tsx","./src/entrypoints/inspector/contentlinkcontext/index.tsx","./src/entrypoints/inspector/ui/index.tsx","./src/entrypoints/inspector/ui/addressbar/index.tsx","./src/entrypoints/inspector/ui/addressbar/frontendselector/index.tsx","./src/entrypoints/inspectorloading/index.tsx","./src/entrypoints/sidebarframe/index.tsx","./src/entrypoints/sidebarframe/usesidebarcontentlink.ts","./src/entrypoints/sidebarframe/previewlinkselector/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/frontendgroup/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/frontendpreviewlinks/index.tsx","./src/entrypoints/sidebarframe/previewlinkselector/trigger/index.tsx","./src/entrypoints/sidebarpanel/index.tsx","./src/entrypoints/wrongenvironmentpanel/index.tsx","./src/utils/common.ts","./src/utils/normalizepathforvisualediting.ts","./src/utils/persistedsidebarvisualediting.ts","./src/utils/persistedwidth.ts","./src/utils/render.tsx","./src/utils/urls.tsx","./src/utils/contentlink/types.ts","./src/utils/contentlink/usecontentlinkconnection.ts","./src/utils/contentlink/usemethodproxy.ts","./penpal.d.ts"],"version":"5.9.3"}
\ No newline at end of file