diff --git a/web/ViewMode.tsx b/web/ViewMode.tsx index d5fb6b6..5c56f0d 100644 --- a/web/ViewMode.tsx +++ b/web/ViewMode.tsx @@ -1,4 +1,5 @@ import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'; +import { useSearchParams } from 'react-router'; /** * How the open section is being looked at: editing or just viewing, and at what width. @@ -17,6 +18,13 @@ import { createContext, useCallback, useContext, useEffect, useState, type React * * 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. + * + * ── Where each piece of state lives ── + * + * The MODE is in the URL: it is addressable, and a link to a page being edited is a different thing + * from a link to the page. The DEVICE is not, yet — it is a way of looking rather than a thing being + * looked at, and nobody has wanted to send one. That distinction is worth keeping deliberate rather + * than letting both drift into whichever place was convenient. */ export type Mode = 'edit' | 'view'; @@ -68,14 +76,34 @@ const ViewModeContext = createContext({ }); export const ViewModeProvider = ({ children }: { children: ReactNode }) => { - /* - View, not Edit. + const [params, setParams] = useSearchParams(); - 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. + /* + The mode lives in the URL — `?mode=edit`. + + docs/navigation-audit.md draws the line: anything ADDRESSABLE goes in the URL. Whether you are + editing a page or looking at it is exactly that. It survives a reload, it can be sent to somebody, + and a link to a page being edited is a different thing from a link to the page. + + `mode`, not `view`. The parameter names the AXIS and the value names the state, so the pair reads + as `mode=edit` and `mode=view` rather than the tautological `view=view`. + + Absent means View, which is the default and the common case — so an ordinary link carries nothing, + and the parameter only appears once somebody has chosen. */ - const [mode, setMode] = useState('view'); + const mode: Mode = params.get('mode') === 'edit' ? 'edit' : 'view'; + + const setMode = useCallback( + (next: Mode) => { + const search = new URLSearchParams(params); + // Written even for the default, once somebody has touched it: a URL that says `mode=view` is + // unambiguous, and a toggle that sometimes leaves no trace reads as a toggle that failed. + search.set('mode', next); + setParams(search, { replace: true }); + }, + [params, setParams], + ); + const [device, setDevice] = useState('desktop'); const [previewable, setPreviewable] = useState(false); diff --git a/web/sections/LayoutPanel.tsx b/web/sections/LayoutPanel.tsx index ac43dc1..e1acca0 100644 --- a/web/sections/LayoutPanel.tsx +++ b/web/sections/LayoutPanel.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useClient } from 'hooks/useClient'; import { readableError } from '../errors'; import { useFlash } from '../useFlash'; @@ -24,7 +25,8 @@ import { SitePreview, useDevPort } from '../SitePreview'; * content. A panel offering to rearrange them offers a power that should not exist, and every use of it * would be a decision made in the wrong place. * - * What is left is the part that IS a content decision: the colours and the typefaces. + * What is left is the part that IS a content decision: the colours and the typefaces, as tabs — one at + * a time, so the preview keeps the height. * * ── The preview is the point ── * @@ -221,19 +223,28 @@ export const LayoutPanel = () => { {/* - The controls, and only in Edit. + The controls as TABS, and only in Edit. - `shrink-0` so they never give up height to the preview, `max-h-[55%]` so they can never take - the whole panel either — on a short window the preview would otherwise be squeezed to nothing, - which is the failure this whole change is about. Past that they scroll on their own rather than - scrolling the preview off the top. + They were two stacked cards, which took as much height as both of them together — and every + pixel of it came off the preview, which is the thing being judged. One at a time costs a click + and gives the page back most of the panel. + + `shrink-0` so they never take height from the preview, and no `max-h` any more: one tab is short + enough that it does not need capping, which is the other thing this bought. */} {mode === 'edit' && ( -
- {/* ── Colours ──────────────────────────────────────────────────────────────────────────── */} -
-

Colours

+ + + + Colours + + + Typefaces + + + + <>
{COLOURS.map(({ key, label, fallback }) => (
@@ -262,12 +273,11 @@ export const LayoutPanel = () => {
))}
-
- - {/* ── Typefaces ────────────────────────────────────────────────────────────────────────── */} -
-

Typefaces

+ + + + <>
{ROLES.map(({ key, label, hint }) => (
@@ -292,9 +302,11 @@ export const LayoutPanel = () => {
))}
-
-
+ + + )} + ); };