one device control, read from the URL

Spacing's settings are per width, so the section has to say which width it is editing — and the answer
must be the one the preview is already showing, not a second selector able to disagree with it.

It reads `?device=` rather than the ViewMode context, and that is not a preference. The editor is a
SIBLING panel: an element built inside the detail panel resolves context where it is RENDERED, so a
toggle handed over would look up a tree with no provider in it and quietly show the default forever.

The URL is the shared state, which means there is nothing to share. Three components read one
parameter, so they cannot disagree, and the preview follows without being subscribed to anything — it
reads the same value. More editor sections can take the control by rendering it.

The bar's own toggle is now the same component. It had its own copy of the options and the handler,
which is exactly how two controls for one thing start to differ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 03:53:02 +01:00
co-authored by Claude Opus 5
parent 53d6a0324d
commit 852c53dd09
3 changed files with 99 additions and 29 deletions
+9 -29
View File
@@ -11,7 +11,8 @@ import { PageBuilderPanel } from './sections/PageBuilderPanel';
import { PublishButton } from './PublishButton';
import { SyncButton } from './SyncButton';
import { SaveTargetProvider, useSaveButton } from './SaveTarget';
import { DEVICES, ViewModeProvider, useViewMode, type DeviceName } from './ViewMode';
import { DEVICES, ViewModeProvider, useViewMode } from './ViewMode';
import { DeviceToggle } from './DeviceToggle';
import { MessagesPanel } from './MessagesPanel';
import { PlannedSection } from './PlannedSection';
import { useReady } from './useReady';
@@ -86,37 +87,16 @@ const SaveButton = () => {
/** The width the preview is rendered at. See DEVICES for why these three and not others. */
const DeviceToggle = () => {
const { previewable, device, setDevice } = useViewMode();
const BarDeviceToggle = () => {
const { previewable } = useViewMode();
if (!previewable) return null;
const options: { value: DeviceName; Icon: typeof Monitor }[] = [
{ value: 'desktop', Icon: Monitor },
{ value: 'tablet', Icon: Tablet },
{ value: 'mobile', Icon: Smartphone },
];
return (
<div className="flex items-center gap-0.5 rounded-md border border-border p-0.5">
{options.map(({ value, Icon }) => (
<Button
key={value}
size="sm"
variant={device === value ? 'secondary' : 'ghost'}
className="h-5 gap-1 px-2 text-xs"
onClick={() => setDevice(value)}
// The width is in the tooltip rather than the label. It is the thing somebody occasionally
// needs and never needs three of at once, and three widths in a row is a row of numbers.
title={`${DEVICES[value].label} — ${DEVICES[value].width}px`}
>
<Icon className="size-3" />
<span className="sr-only">{DEVICES[value].label}</span>
</Button>
))}
</div>
);
// The same control the editor sections use — see DeviceToggle, which reads `?device=` rather than a
// context so every panel on the screen shares one answer.
return <DeviceToggle size="bar" />;
};
export const DetailPanel = () => {
const [params] = useSearchParams();
const raw = params.get('section');
@@ -183,7 +163,7 @@ export const DetailPanel = () => {
<div className="flex flex-1 justify-start" />
<div className="flex justify-center">
<DeviceToggle />
<BarDeviceToggle />
</div>
<div className="flex flex-1 items-center justify-end gap-2">