From 8d5c4797aded97a0bc56a091e634d4f764fd0a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 31 Aug 2026 05:00:21 +0100 Subject: [PATCH] the mode is in the URL, and the controls are tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?mode=edit docs/navigation-audit.md draws the line: anything ADDRESSABLE goes in the URL, and 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 `mode=edit` and `mode=view` rather than the tautological `view=view`. Absent means View — the default and the common case — so an ordinary link carries nothing and the parameter appears once somebody has chosen. The DEVICE deliberately stays in React state. It is a way of looking rather than a thing being looked at, and nobody has wanted to send one. Worth keeping that distinction deliberate rather than letting both drift into whichever place was convenient. COLOURS AND TYPEFACES ARE TABS. They were two stacked cards taking as much height as both 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. The 55% cap on the controls is gone with them: one tab is short enough not to need capping, which is the other thing this bought. Co-Authored-By: Claude Opus 5 (1M context) --- web/ViewMode.tsx | 40 ++++++++++++++++++++++++++----- web/sections/LayoutPanel.tsx | 46 +++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 23 deletions(-) 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 = () => {
))}
-
-
+ + + )} + ); };