Files
fofos-admin/web/ViewMode.tsx
T
pastilhasandClaude Opus 5 3669d2a405 open in View, not Edit
Opening a section is nearly always to LOOK at something — to check what a change did, or to see where
the site stands before deciding anything. Editing is the deliberate act, so it is the one that should
cost a click. Defaulting the other way puts a wall of controls in front of the thing they act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 02:30:06 +01:00

104 lines
4.4 KiB
TypeScript

import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
/**
* How the open section is being looked at: editing or just viewing, and at what width.
*
* ── Why a context, and the same shape as SaveTarget ──
*
* The controls live in DetailPanel's bar and the thing they control lives in the section. DetailPanel
* deliberately knows nothing about the sections beyond a map of keys to components, so the section
* announces itself rather than being handed props — exactly as it does for Save.
*
* ── Why a section has to OPT IN ──
*
* Most sections have no preview. Media is a file browser, Administration is a connection form, Messages
* is an inbox — offering to view any of them at tablet width is offering something that means nothing,
* and a control that does nothing teaches people that controls do nothing.
*
* So the bar shows these only when the open section says it has a preview, the same way Save renders
* nothing when there is nothing to save.
*/
export type Mode = 'edit' | 'view';
export type DeviceName = 'desktop' | 'tablet' | 'mobile';
/**
* The widths a preview is rendered at, and why these numbers.
*
* They are REAL viewport sizes, and the preview is scaled down to fit rather than being given whatever
* width the panel happens to have. That distinction is the whole point: a "Desktop" preview squeezed
* into a 900px panel would render the site's tablet layout and call it desktop, which is precisely the
* lie a preview exists to prevent.
*
* mobile 390 — the iPhone width the site's own slot table already assumes (see media/slots.ts,
* where MOBILE is 342: this minus the layout's 48px of padding)
* tablet 834 — iPad portrait, and deliberately ABOVE the site's `md:` breakpoint at 768, so it
* shows the desktop navigation rather than the drawer. A tablet at 768 exactly sits
* on the boundary and is the least informative width available.
* desktop 1440 — a common laptop, comfortably past the 1280 the content column caps at, so the
* margins either side are visible rather than implied.
*/
export const DEVICES: Record<DeviceName, { label: string; width: number; height: number }> = {
desktop: { label: 'Desktop', width: 1440, height: 900 },
tablet: { label: 'Tablet', width: 834, height: 1112 },
mobile: { label: 'Mobile', width: 390, height: 844 },
};
type ViewModeValue = {
mode: Mode;
device: DeviceName;
setMode: (mode: Mode) => void;
setDevice: (device: DeviceName) => void;
/** True while a section with a preview is open. The bar shows its controls only then. */
previewable: boolean;
announcePreviewable: (has: boolean) => void;
};
const ViewModeContext = createContext<ViewModeValue>({
mode: 'edit',
device: 'desktop',
setMode: () => {},
setDevice: () => {},
previewable: false,
announcePreviewable: () => {},
});
export const ViewModeProvider = ({ children }: { children: ReactNode }) => {
/*
View, not Edit.
Opening a section is nearly always to LOOK at something — to check what a change did, or to see the
state of the site before deciding anything. Editing is the deliberate act, so it is the one that
takes a click. Defaulting the other way puts a wall of controls in front of the thing they act on.
*/
const [mode, setMode] = useState<Mode>('view');
const [device, setDevice] = useState<DeviceName>('desktop');
const [previewable, setPreviewable] = useState(false);
// Stable, so the effect in `usePreviewable` is not re-run by this provider re-rendering.
const announcePreviewable = useCallback((has: boolean) => setPreviewable(has), []);
return (
<ViewModeContext.Provider value={{ mode, device, setMode, setDevice, previewable, announcePreviewable }}>
{children}
</ViewModeContext.Provider>
);
};
/** Read by a section: how it is being looked at. */
export const useViewMode = (): ViewModeValue => useContext(ViewModeContext);
/**
* Called by a section that HAS a preview, so the bar offers the controls.
*
* The cleanup clears it, so a section that unmounts leaves no controls behind for a preview that is no
* longer on screen.
*/
export const usePreviewable = (): void => {
const { announcePreviewable } = useContext(ViewModeContext);
useEffect(() => {
announcePreviewable(true);
return () => announcePreviewable(false);
}, [announcePreviewable]);
};