From 495320f555427d6d48309f80fd19680996ee07c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 31 Aug 2026 05:20:53 +0100 Subject: [PATCH] the tab panels share one grid cell, so nothing jumps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Making Typefaces one row equalised the two tabs by luck, not by construction — the labels and the hints under the selects still differed, and any tab that grows a row would put the jump straight back. So every panel now sits in the SAME grid cell. The block is as tall as the tallest of them and stops moving entirely, and it stays right when a tab is added or one of them grows. Measuring instead would have meant a ResizeObserver, a re-measure after every edit, and a wrong answer for one frame after each. `col-start-1 row-start-1` gets the same number out of the layout engine for nothing. `invisible`, not `hidden`. An invisible element still takes its space, which IS the mechanism — and it is out of the tab order, so the controls behind the front panel cannot be reached by keyboard. The Tabs root is controlled now, because visibility is decided here rather than by Radix unmounting. Co-Authored-By: Claude Opus 5 (1M context) --- web/sections/LayoutPanel.tsx | 39 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/web/sections/LayoutPanel.tsx b/web/sections/LayoutPanel.tsx index 896216e..563c336 100644 --- a/web/sections/LayoutPanel.tsx +++ b/web/sections/LayoutPanel.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useClient } from 'hooks/useClient'; import { readableError } from '../errors'; import { useFlash } from '../useFlash'; @@ -114,6 +114,8 @@ export const LayoutPanel = () => { const [draft, setDraft] = useState(null); const [busy, setBusy] = useState(false); const devPort = useDevPort(); + /** Controlled, because the grid stack below decides visibility rather than Radix unmounting. */ + const [tab, setTab] = useState('colours'); const [error, setError] = useState(null); const [saved, flashSaved] = useFlash(); @@ -233,7 +235,7 @@ export const LayoutPanel = () => { enough that it does not need capping, which is the other thing this bought. */} {mode === 'edit' && ( - + Colours @@ -243,8 +245,25 @@ export const LayoutPanel = () => { - - <> + {/* + EVERY panel in the same grid cell, so the block is as tall as the TALLEST of them and stops + moving when the tab changes. + + Measuring the panels and setting a height would need a ResizeObserver, a re-measure on every + edit, and a wrong answer for one frame after each. `grid-area: 1/1` gets it from the layout + engine for nothing, and it stays right when a tab is added or one of them grows a row — + which is the case that made the first fix insufficient. + + `invisible` and not `hidden`: an invisible element still takes its space, which IS the + mechanism. It is also removed from the tab order, so the controls behind the front panel + cannot be reached by keyboard. + */} +
+
+<>
{COLOURS.map(({ key, label, fallback }) => (
@@ -274,10 +293,13 @@ export const LayoutPanel = () => { ))}
- +
- - <> +
+<> {/* Four across, one row. @@ -317,7 +339,8 @@ export const LayoutPanel = () => { ))}
-
+
+
)}