the toggle drops its labels when the panel is narrow

A container query, not a screen one: this sits in a workspace panel whose width
is a divider somebody drags, so the window says nothing about the room. Below
240px the icons stand alone and the word is the tooltip.

Every editor section starts open. It opened on one, from ?tab=, which is what
tabs want — these are not tabs, and a collapsed column asks somebody to go
looking for the setting they came for. ?tab= is gone with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 07:44:15 +01:00
co-authored by Claude Opus 5
parent d218d3c7a8
commit c0c88a434d
2 changed files with 25 additions and 21 deletions
+13 -2
View File
@@ -60,12 +60,20 @@ const OPTIONS: { value: DeviceName; Icon: typeof Monitor }[] = [
* choice legible without hovering for a tooltip.
*
* The label comes from DEVICES, which is also what the preview names in its own caption. One list.
*
* ── Narrow, it goes back to icons ──
*
* A CONTAINER query, not a screen one: this sits in a workspace panel whose width is a divider the
* user drags, so the window's width says nothing about the room there is. `@container` on the row
* makes the row itself the thing measured.
*
* The word is then the `title`, so the answer is still one hover away rather than gone.
*/
export const DeviceToggle = () => {
const [device, setDevice] = useDevice();
return (
<div className="flex w-full items-center rounded-md border border-border p-0.5">
<div className="@container flex w-full items-center rounded-md border border-border p-0.5">
{OPTIONS.map(({ value, Icon }) => (
<Button
key={value}
@@ -74,9 +82,12 @@ export const DeviceToggle = () => {
className="h-7 flex-1 gap-1.5 text-xs"
onClick={() => setDevice(value)}
aria-label={DEVICES[value].label}
title={DEVICES[value].label}
>
<Icon className="size-3.5" />
{DEVICES[value].label}
{/* Three labels need about 240px between them. Below that the icons stand alone and the
word is the tooltip. */}
<span className="hidden @[15rem]:inline">{DEVICES[value].label}</span>
</Button>
))}
</div>
+12 -19
View File
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { useSearchParams } from 'react-router';
import { useClient } from 'hooks/useClient';
import { readableError } from '../errors';
import { useFlash } from '../useFlash';
@@ -110,6 +109,9 @@ const themeProperties = (draft: Layout): Record<string, string | null> => ({
'--font-display': draft.font_display && `"${draft.font_display}", sans-serif`,
});
/** Every section, open from the start. The values below must match the AccordionItems. */
const SECTIONS_OPEN = ['colours', 'typefaces', 'padding'];
const COLOURS = [
{ key: 'background' as const, label: 'Background', fallback: '#ffffff' },
{ key: 'foreground' as const, label: 'Text', fallback: '#000000' },
@@ -124,23 +126,7 @@ export const LayoutPanel = () => {
const [stored, setStored] = useState<Layout | null>(null);
const [draft, setDraft] = useState<Layout | null>(null);
const [busy, setBusy] = useState(false);
const [params, setParams] = useSearchParams();
const devPort = useDevPort();
/** Controlled, because the grid stack below decides visibility rather than Radix unmounting. */
/*
The open tab, in the URL — `?tab=padding`.
Same rule as the mode and the device: it is addressable. "Look at the typefaces" is a link somebody
sends, and a reload that drops you back on Colours loses the place you were working in.
Absent means Colours, the first one, so an ordinary link carries nothing.
*/
const tab = params.get('tab') ?? 'colours';
const setTab = (next: string) => {
const search = new URLSearchParams(params);
search.set('tab', next);
setParams(search, { replace: true });
};
const [error, setError] = useState<string | null>(null);
const [saved, flashSaved] = useFlash();
@@ -263,7 +249,14 @@ export const LayoutPanel = () => {
}
setEditorRef.current(
<Accordion type="multiple" defaultValue={[tab]} className="flex flex-col gap-2">
/*
Every section open to begin with.
It opened on one, from `?tab=`, which is what a set of TABS wants — one at a time, and which
one is addressable. These are not tabs: they are the whole of what this screen decides, and a
column that starts collapsed asks somebody to go looking for the setting they came for.
*/
<Accordion type="multiple" defaultValue={SECTIONS_OPEN} className="flex flex-col gap-2">
<AccordionItem value="colours" className="rounded-lg border border-border px-4">
<AccordionTrigger className="text-xs">Colors</AccordionTrigger>
<AccordionContent>
@@ -357,7 +350,7 @@ export const LayoutPanel = () => {
</AccordionItem>
</Accordion>
);
}, [draft, tab, device]);
}, [draft, device]);
// Cleared on unmount, or the panel goes on offering Layout's controls over somebody else's page.
useEffect(() => () => setEditorRef.current(null), []);