Files
fofos-admin/web/DeviceToggle.tsx
T
pastilhasandClaude Opus 5 d218d3c7a8 the device toggle is three labelled tabs
Icons alone told three things apart by proportion at 12 pixels. Full width, one
of three filled, and the word from DEVICES.

The `size` prop is gone with the bar that needed the small one, and so is the
second copy inside Spacing — the panel header scopes the whole editor now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:41:09 +01:00

85 lines
3.2 KiB
TypeScript

import { Monitor, Smartphone, Tablet } from 'lucide-react';
import { useCallback } from 'react';
import { useSearchParams } from 'react-router';
import { Button } from '@/components/ui/button';
import { DEVICES, type DeviceName } from './ViewMode';
/**
* Which width everything is showing, and the control for changing it.
*
* ── Why this reads the URL and not the context ──
*
* `useViewMode` is a context, and the editor is a SIBLING panel: an element built inside the detail
* panel resolves its context where it is RENDERED, not where it was created, so a toggle handed to the
* editor would look up a tree that has no provider in it and quietly show the default forever.
*
* The device already lives in `?device=`, which every panel can read. So this is the shared controller
* the whole screen wants and there is nothing to share: the URL is the state, three components read it,
* and they cannot disagree because there is only one of it.
*
* That is also why the preview follows along for free. It is not subscribed to this control; it reads
* the same parameter.
*/
export const useDevice = (): [DeviceName, (next: DeviceName) => void] => {
const [params, setParams] = useSearchParams();
const raw = params.get('device');
const device: DeviceName = raw === 'tablet' || raw === 'mobile' ? raw : 'desktop';
const setDevice = useCallback(
(next: DeviceName) => {
const search = new URLSearchParams(params);
search.set('device', next);
setParams(search, { replace: true });
},
[params, setParams],
);
return [device, setDevice];
};
const OPTIONS: { value: DeviceName; Icon: typeof Monitor }[] = [
{ value: 'desktop', Icon: Monitor },
{ value: 'tablet', Icon: Tablet },
{ value: 'mobile', Icon: Smartphone },
];
/**
* The control itself: three tabs across the width of whatever holds it.
*
* ── One shape, not two ──
*
* There was a `size` prop. `bar` was a small icon-only cluster for the panel bar across the top of the
* detail panel, and that bar is gone — so what is left is the one shape, and a prop with a single
* value is a decision nobody makes.
*
* ── Labelled, and spread ──
*
* The icons alone were a rebus: a rectangle, a slightly narrower rectangle, and a narrower one still,
* telling three things apart by proportion at 12 pixels. They read as tabs now because they behave
* like tabs — one of three, the whole width, the chosen one filled — and the word is what makes the
* 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.
*/
export const DeviceToggle = () => {
const [device, setDevice] = useDevice();
return (
<div className="flex w-full items-center rounded-md border border-border p-0.5">
{OPTIONS.map(({ value, Icon }) => (
<Button
key={value}
size="sm"
variant={device === value ? 'secondary' : 'ghost'}
className="h-7 flex-1 gap-1.5 text-xs"
onClick={() => setDevice(value)}
aria-label={DEVICES[value].label}
>
<Icon className="size-3.5" />
{DEVICES[value].label}
</Button>
))}
</div>
);
};