diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte
index b30b4ba0..059f0a9b 100644
--- a/configurator/src/App.svelte
+++ b/configurator/src/App.svelte
@@ -5,18 +5,20 @@
* Layout (desktop ≥ 1100px):
* ┌─────────────────────────────────────────────────────────────────┐
* │ Header (brand · search · basic/advanced · theme · preview) │
- * ├──────────┬───────────────────────────────────────┬──────────────┤
- * │ │ │ │
- * │ Sidebar │ Domain panel / WCAG tool │ Preview │
- * │ │ │ │
- * ├──────────┴───────────────────────────────────────┴──────────────┤
+ * ├──────────┬──┬────────────────────────────────────┬──┬───────────┤
+ * │ │ │ │ │ │
+ * │ Sidebar │↔│ Domain panel / WCAG tool │↔│ Preview │
+ * │ │ │ │ │ │
+ * ├──────────┴──┴────────────────────────────────────┴──┴───────────┤
* │ Output drawer (override CSS, copy / download / import) │
* └─────────────────────────────────────────────────────────────────┘
*
+ * The ↔ columns are 6 px drag handles — pointer-captured resize of the
+ * sidebar and preview pane widths. Widths persist in localStorage.
+ *
* Below 1100px the preview pane becomes a slide-over overlay (closed by
* default, opened with the header ◨ toggle, dismissed via the scrim).
- * Below 760px the sidebar collapses to a
- * compact icon-only rail and the search box widens to fill the header.
+ * Below 760px the sidebar collapses to a compact icon-only rail.
*
* State lives in the shared `ui` store; this component just wires the
* active domain to its panel.
@@ -35,6 +37,64 @@
import Cheatsheet from './components/Cheatsheet.svelte';
import Home from './components/Home.svelte';
+ // ── Pane-width resizing ───────────────────────────────────────────────────
+ const WIDTHS_KEY = 'slashed-configurator/pane-widths/v1';
+ const SIDEBAR_MIN = 160;
+ const SIDEBAR_MAX = 500;
+ const PREVIEW_MIN = 300;
+ const PREVIEW_MAX = 680;
+
+ function clampW(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
+
+ function loadWidths() {
+ if (typeof localStorage === 'undefined') return { sidebar: 240, preview: 440 };
+ try {
+ const raw = localStorage.getItem(WIDTHS_KEY);
+ if (raw) {
+ const p = JSON.parse(raw);
+ return {
+ sidebar: clampW(Number(p.sidebar) || 240, SIDEBAR_MIN, SIDEBAR_MAX),
+ preview: clampW(Number(p.preview) || 440, PREVIEW_MIN, PREVIEW_MAX),
+ };
+ }
+ } catch { /* ignore */ }
+ return { sidebar: 240, preview: 440 };
+ }
+
+ const _w = loadWidths();
+ let sidebarWidth = $state(_w.sidebar);
+ let previewWidth = $state(_w.preview);
+
+ /** Which pane is being dragged ('sidebar' | 'preview' | null). */
+ let dragging = $state(/** @type {'sidebar'|'preview'|null} */ (null));
+ let _dragStartX = 0;
+ let _dragStartW = 0;
+
+ function startResize(pane, /** @type {PointerEvent} */ e) {
+ e.currentTarget.setPointerCapture(e.pointerId);
+ dragging = pane;
+ _dragStartX = e.clientX;
+ _dragStartW = pane === 'sidebar' ? sidebarWidth : previewWidth;
+ e.preventDefault();
+ }
+
+ function onResizeMove(pane, /** @type {PointerEvent} */ e) {
+ if (dragging !== pane) return;
+ const dx = e.clientX - _dragStartX;
+ if (pane === 'sidebar') {
+ sidebarWidth = clampW(_dragStartW + dx, SIDEBAR_MIN, SIDEBAR_MAX);
+ } else {
+ // Preview handle sits LEFT of the preview pane: dragging right shrinks preview.
+ previewWidth = clampW(_dragStartW - dx, PREVIEW_MIN, PREVIEW_MAX);
+ }
+ }
+
+ function endResize() {
+ if (!dragging) return;
+ dragging = null;
+ try { localStorage.setItem(WIDTHS_KEY, JSON.stringify({ sidebar: sidebarWidth, preview: previewWidth })); } catch { /* ignore */ }
+ }
+
const home = $derived(ui.mode === 'basic' && ui.domain === 'home');
const domain = $derived(DOMAIN_BY_ID.get(ui.domain) ?? DOMAIN_BY_ID.get('colors'));
const tool = $derived(domain?.tool ?? '');
@@ -141,11 +201,33 @@