Edit/View on the left, three widths in the middle

The bar is three groups now: how you are looking at it, at what width, and what to do with it. The
middle is centred against the BAR rather than against its neighbours, because the outer two change
width as sections come and go and a middle that drifted with them would never be in the same place
twice.

A section OPTS IN, the same way it opts into Save. Most have no preview — Media is a file browser,
Administration is a connection form, Messages is an inbox — and offering to view any of them at tablet
width offers something that means nothing. A control that does nothing teaches people that controls do
nothing.

The widths are REAL viewport sizes and the frame is SCALED to fit, never squeezed into whatever the
panel has. That distinction is the whole point: an iframe given the panel's width has the site's own
media queries answering the PANEL's size, so "Desktop" in a narrow panel renders the tablet layout and
labels it desktop — the one thing a preview must not do. Never scaled up either: a 390px phone blown
up to fill a wide panel is a phone nobody has, and every judgement made against it is wrong in the
direction of "looks fine".

390 is the iPhone width slots.ts already assumes. 834 is iPad portrait, deliberately above the site's
md: breakpoint at 768 so it shows the desktop nav rather than the drawer — 768 exactly sits on the
boundary and is the least informative width available. 1440 is past the 1280 the content column caps
at, so the margins either side are visible rather than implied.

View gives the preview the whole panel. That is what makes it worth having: at edit height a phone is
a letterbox, and a phone is the width somebody most wants to look at properly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 02:26:06 +01:00
co-authored by Claude Opus 5
parent 9565d24536
commit 143b34d2d2
3 changed files with 283 additions and 27 deletions
+94 -8
View File
@@ -1,5 +1,5 @@
import { useSearchParams } from 'react-router';
import { Check, Loader2 } from 'lucide-react';
import { Check, Eye, Loader2, Monitor, Pencil, Smartphone, Tablet } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { DEFAULT_SECTION, isSectionKey, sectionFor, sectionOpen, type SectionKey } from './sections';
import { AdministrationPanel } from './AdministrationPanel';
@@ -8,6 +8,7 @@ import { GalleriesPanel } from './sections/GalleriesPanel';
import { LayoutPanel } from './sections/LayoutPanel';
import { PublishButton } from './PublishButton';
import { SaveTargetProvider, useSaveButton } from './SaveTarget';
import { DEVICES, ViewModeProvider, useViewMode, type DeviceName, type Mode } from './ViewMode';
import { MessagesPanel } from './MessagesPanel';
import { PlannedSection } from './PlannedSection';
import { useReady } from './useReady';
@@ -79,6 +80,72 @@ const SaveButton = () => {
);
};
/**
* Editing, or just looking.
*
* Two buttons rather than one that toggles, so the current state is readable without knowing what the
* label means. A single button saying "View" has to be interpreted — is that what it is, or what it
* would become? — and that question is asked every time.
*/
const ModeToggle = () => {
const { previewable, mode, setMode } = useViewMode();
if (!previewable) return null;
const options: { value: Mode; label: string; Icon: typeof Pencil }[] = [
{ value: 'edit', label: 'Edit', Icon: Pencil },
{ value: 'view', label: 'View', Icon: Eye },
];
return (
<div className="flex items-center gap-0.5 rounded-md border border-border p-0.5">
{options.map(({ value, label, Icon }) => (
<Button
key={value}
size="sm"
variant={mode === value ? 'secondary' : 'ghost'}
className="h-5 gap-1 px-2 text-xs"
onClick={() => setMode(value)}
>
<Icon className="size-3" />
{label}
</Button>
))}
</div>
);
};
/** The width the preview is rendered at. See DEVICES for why these three and not others. */
const DeviceToggle = () => {
const { previewable, device, setDevice } = 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>
);
};
export const DetailPanel = () => {
const [params] = useSearchParams();
const raw = params.get('section');
@@ -120,14 +187,33 @@ export const DetailPanel = () => {
plugin and to the site it administers.
*/}
<SaveTargetProvider>
<div className="flex shrink-0 items-center justify-end gap-2 border-b border-border px-4 py-2">
{/* Left of Publish, and in that order for a reason: save what you wrote, then make it
public. A row that put them the other way round would read as the sequence backwards. */}
<SaveButton />
<PublishButton />
</div>
<ViewModeProvider>
{/*
Three groups: how you are looking at it, at what width, and what to do with it.
<div className="min-h-0 flex-1">{Panel ? <Panel /> : <PlannedSection section={current} />}</div>
The middle one is centred against the BAR rather than against what is beside it — the two
outer groups change width as sections come and go, and a middle that drifted with them
would never be in the same place twice.
*/}
<div className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-2">
<div className="flex flex-1 justify-start">
<ModeToggle />
</div>
<div className="flex justify-center">
<DeviceToggle />
</div>
<div className="flex flex-1 items-center justify-end gap-2">
{/* Left of Publish, and in that order for a reason: save what you wrote, then make it
public. A row that put them the other way round would read as the sequence backwards. */}
<SaveButton />
<PublishButton />
</div>
</div>
<div className="min-h-0 flex-1">{Panel ? <Panel /> : <PlannedSection section={current} />}</div>
</ViewModeProvider>
</SaveTargetProvider>
</div>
);