From 4cd54e21119b8b6b8f50c2b34d73a6e560d54840 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 28 May 2026 00:05:14 +0200 Subject: [PATCH] feat(words): drawer `side` prop (left | right) + overlays auto-mirror The companion drawer was hard-coded to the right column. Now `` flips the editor grid so the drawer takes the start column; the floating overlays (block handle grip, block inserter "+") automatically render on the OPPOSITE margin so they don't crash with the drawer's gutter. Default stays `'right'` so existing consumers see no change. Wiring: - Eidos `` gains `side?: 'left' | 'right'` (default 'right'). The value is passthrough'd as `data-side={side}` on the rendered drawer root so CSS + sibling overlays can read it from the DOM. - `words.css`: - `[data-words]:has([data-words-drawer][data-side='left'])` flips `grid-template-columns` + `grid-template-areas` so the drawer column sits at the inline-start. - `[data-words-drawer][data-side='left']` swaps the visual seam from `border-inline-start` to `border-inline-end`. - `[data-words-block-inserter][data-side='left'] [data-words-block-inserter-button]` overrides `left: -28px` to `right: -28px` so the "+" lands on the block's right margin. - Block-handle (`words-block-handle.svelte`): new `detectDrawerSide()` reads `[data-words-drawer]@data-side` once per measurement. The grip's `position.left` becomes `rect.right + 4` when drawer is on the left (was always `rect.left - 28`). - Block-inserter (`words-block-inserter.svelte`): same `$derived` read of the drawer's `data-side`, propagated as `data-side` on the inserter overlay element so the CSS rule above can flip the "+" button. - Demo: new `let drawerSide = $state<'left' | 'right'>('right')` + a chip control "drawer side: right | left". ``. Verified `npm run check` clean (pre-existing errors only). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../words/words-block-handle.svelte | 17 +++++++- .../words/words-block-inserter.svelte | 15 +++++++ .../components/words/words-drawer.svelte | 12 +++++- src/uix/eidos/components/words/words.css | 39 ++++++++++++++++--- web/routes/uix/components/words/+page.svelte | 17 +++++++- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/uix/eidos/components/words/words-block-handle.svelte b/src/uix/eidos/components/words/words-block-handle.svelte index 7ef0a0177..0e7c820c7 100644 --- a/src/uix/eidos/components/words/words-block-handle.svelte +++ b/src/uix/eidos/components/words/words-block-handle.svelte @@ -37,15 +37,30 @@ return Number.isInteger(n) && n >= 0 ? n : undefined; }); + /** + * Detect drawer side so the grip auto-flips to the OPPOSITE margin. + * Drawer on right (default) → grip on block's left. Drawer on left + * → grip on block's right (so it doesn't fight the drawer's gutter). + * Read from the DOM each measure — cheap, and reactive to layout + * changes (user toggling side prop). + */ + function detectDrawerSide(): 'left' | 'right' { + const drawer = document.querySelector('[data-words-drawer]'); + return drawer?.getAttribute('data-side') === 'left' ? 'left' : 'right'; + } + function measureFromEl(el: HTMLElement | null) { if (!el) { position = null; return; } const rect = el.getBoundingClientRect(); + const side = detectDrawerSide(); position = { top: rect.top + rect.height / 2 - 12, - left: rect.left - 28 + // Drawer on RIGHT → grip on block LEFT (left edge − 28). + // Drawer on LEFT → grip on block RIGHT (right edge + 4). + left: side === 'left' ? rect.right + 4 : rect.left - 28 }; } diff --git a/src/uix/eidos/components/words/words-block-inserter.svelte b/src/uix/eidos/components/words/words-block-inserter.svelte index 1da00639c..b6cde0f9a 100644 --- a/src/uix/eidos/components/words/words-block-inserter.svelte +++ b/src/uix/eidos/components/words/words-block-inserter.svelte @@ -210,12 +210,27 @@ }); seam = null; } + + /** + * Mirror the drawer's side so the "+" button auto-flips to the + * OPPOSITE gutter (CSS reads `data-side` to swap left:-28 vs + * right:-28). Drawer right (default) → "+" on left margin; + * drawer left → "+" on right margin. + */ + const drawerSide = $derived.by(() => { + // Read reactively each time seam changes — re-evaluating the + // DOM is cheap and the side rarely flips during a session. + if (!seam) return 'right' as const; + const drawer = document.querySelector('[data-words-drawer]'); + return drawer?.getAttribute('data-side') === 'left' ? 'left' : 'right'; + }); {#if seam}
diff --git a/src/uix/eidos/components/words/words-drawer.svelte b/src/uix/eidos/components/words/words-drawer.svelte index 0b943fb1a..3db89467b 100644 --- a/src/uix/eidos/components/words/words-drawer.svelte +++ b/src/uix/eidos/components/words/words-drawer.svelte @@ -47,9 +47,17 @@ open = $bindable(undefined), defaultOpen = true, disabled = false, + /** + * Which side of the editor the drawer occupies. Default `'right'`. + * When `'left'`, the editor grid flips so the drawer sits at the + * start of the inline axis; the floating overlays (block handle, + * block inserter "+" button) automatically render on the + * opposite margin to stay out of the drawer's gutter. + */ + side = 'right' as 'left' | 'right', children: customChildren, ...rest - }: Omit = $props(); + }: Omit & { side?: 'left' | 'right' } = $props(); const MODE_LABELS: Record = { default: 'Insert', @@ -153,7 +161,7 @@ } - + {#snippet children(d: DrawerSnippetProps)} {@const { modes, snippet, toggle: toggleDrawer } = d} diff --git a/src/uix/eidos/components/words/words.css b/src/uix/eidos/components/words/words.css index 67c437179..8df0a50da 100644 --- a/src/uix/eidos/components/words/words.css +++ b/src/uix/eidos/components/words/words.css @@ -125,12 +125,14 @@ opacity: var(--words-disabled-opacity); } -/* ── Companion Drawer (right-rail) ─────────────────────────────────────── +/* ── Companion Drawer ──────────────────────────────────────────────────── Layout: when the editor host contains a [data-words-drawer], place - the drawer in a dedicated right column that spans toolbar+content+ - status rows. Children without a grid-area placement (BubbleMenu / - SlashMenu / HiddenInput / find-replace etc.) are absolute-positioned - or hidden inputs that don't participate in the visual flow. */ + the drawer in a dedicated column that spans toolbar+content+ + status rows. Default side is right; `data-side='left'` on the drawer + flips the column order. Children without a grid-area placement + (BubbleMenu / SlashMenu / HiddenInput / find-replace etc.) are + absolute-positioned or hidden inputs that don't participate in the + visual flow. */ [data-words]:has([data-words-drawer]) { display: grid; grid-template-columns: minmax(0, 1fr) max-content; @@ -142,6 +144,18 @@ column-gap: 0; } +/* Left-side drawer — flip the column order so the drawer takes the + start column and the editor flow takes the rest. The drawer's + own border-inline-start (toward the content) is what visually + separates them; we swap it to border-inline-end below. */ +[data-words]:has([data-words-drawer][data-side='left']) { + grid-template-columns: max-content minmax(0, 1fr); + grid-template-areas: + 'drawer toolbar' + 'drawer content' + 'drawer status'; +} + [data-words]:has([data-words-drawer]) > [data-words-toolbar] { grid-area: toolbar; } @@ -172,6 +186,13 @@ transition: inline-size 160ms ease; } +/* Left-side drawer: the visual seam goes on the inline-end edge + (toward the editor content) instead of inline-start. */ +[data-words-drawer][data-side='left'] { + border-inline-start: 0; + border-inline-end: var(--words-border-width) solid var(--words-toolbar-border); +} + [data-words-drawer]:not([data-open]) { inline-size: 2.25rem; max-inline-size: 2.25rem; @@ -847,6 +868,14 @@ color: var(--_words-accent-on); } +/* When the drawer is on the LEFT side, flip the "+" button to the + block's right margin so it doesn't clash with the drawer's + gutter (mirrors the block-handle grip behaviour). */ +[data-words-block-inserter][data-side='left'] [data-words-block-inserter-button] { + left: auto; + right: -28px; +} + [data-words-block-inserter-line] { pointer-events: auto; flex: 1 1 auto; diff --git a/web/routes/uix/components/words/+page.svelte b/web/routes/uix/components/words/+page.svelte index 5c9614cc3..3497b6dd0 100644 --- a/web/routes/uix/components/words/+page.svelte +++ b/web/routes/uix/components/words/+page.svelte @@ -191,6 +191,7 @@ let showSlashMenu = $state(true); let showStatus = $state(true); let showDrawer = $state(true); + let drawerSide = $state<'left' | 'right'>('right'); let toolbarLayout = $state<'grouped' | 'inline'>('grouped'); let size = $state('md'); let variant = $state('surface'); @@ -1061,7 +1062,7 @@ {/if} {#if showDrawer} - + {/if} @@ -1195,6 +1196,20 @@ {/each} + {#each ['disabled', 'readonly', 'required', 'invalid', 'commitOnBlur', 'linkEditor', 'findReplace', 'bubbleMenu', 'slashMenu', 'status'] as key}