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}