From be15bbe4f6b1852f1aaaa100b4c5aebe37489ec9 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 3 Jun 2026 02:08:06 +0200 Subject: [PATCH] words: preview visible cue + gutter active-block tint + no-caret command guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues found while testing the menu bar: - Preview "did nothing": clicking Preview does switch to read-only (contenteditable=false), but a read-only surface looks identical to an editable one, so the change was invisible. Now `[data-mode='preview']` collapses the bar's formatting groups, leaving just the view controls (inspector + Edit/Preview) — the bar visibly simplifies. - Gutter active block: the dashed outline read poorly. Replace it with a faint primary-tinted fill + a 3px accent bar on the left edge (toward the grip), so the block the handle acts on is obvious. - Bold / insert "don't work" with no caret: a selection-dependent command fired from the menu bar (outside the editable) no-op'd when the editor was never focused — `toggleMark` bailed on a null selection, and `insertBlockOfType` silently fell back to the last block. Add a TipTap `.focus()`-style guard `ensureCaret()` in the provider: when `selection` is null it drops a collapsed caret at the document start, so marks arm (stored mark → next typed text) and inserts land predictably after the first block. Wired into `runCommandName` (marks / align) and `insertBlockOfType`. The provider freezes the selection across blur, so this only fires on a never-focused editor. Verified in-browser: Preview hides the format groups (toggle stays); the hovered block shows the tinted fill + accent; a fresh editor's Bold now arms + places a caret. Words tests 477/477 pass; check clean for the touched files. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/uix/eidos/components/words/words.css | 14 +++++++++++-- .../components/words/words-provider.svelte.ts | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/uix/eidos/components/words/words.css b/src/uix/eidos/components/words/words.css index f4e7f771e..36ec0e5b6 100644 --- a/src/uix/eidos/components/words/words.css +++ b/src/uix/eidos/components/words/words.css @@ -124,6 +124,13 @@ color: var(--color-primary-solid); background: color-mix(in srgb, var(--color-primary-solid) 12%, transparent); } +/* Preview is read-only — the surface alone looks identical to edit, so make + the switch obvious: collapse the formatting groups, leaving the bar as just + the view controls (inspector toggle + Edit/Preview). */ +[data-words][data-mode='preview'] [data-words-menubar-group] { + display: none; +} + /* Park the docked inspector panels below the bar so they don't cover it. */ [data-words]:has([data-words-menubar]) [data-words-sidebar], [data-words]:has([data-words-menubar]) [data-words-drawer-panel] { @@ -172,13 +179,16 @@ transform: translateX(1px) scale(1.18); } -/* dashed outline emphasising the block the handle acts on */ +/* Active-block highlight while the handle acts on a block: a faint tinted + fill + a solid accent bar on the left edge (toward the gutter grip). Reads + as "this is the block" far better than the old dashed outline. */ [data-words-block-outline] { position: absolute; z-index: 1; pointer-events: none; - border: 1px dashed var(--words-border-hover); + background: color-mix(in srgb, var(--color-primary-solid) 5%, transparent); border-radius: var(--words-radius-sm); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--color-primary-solid) 40%, transparent); animation: words-outline-in var(--words-transition-duration) var(--words-transition-ease); } @keyframes words-outline-in { diff --git a/src/uix/soma/components/words/words-provider.svelte.ts b/src/uix/soma/components/words/words-provider.svelte.ts index 0f07363dc..f81263a51 100644 --- a/src/uix/soma/components/words/words-provider.svelte.ts +++ b/src/uix/soma/components/words/words-provider.svelte.ts @@ -26,6 +26,7 @@ import type { } from './types'; import { comparePath, type WordsPath } from './engine/path'; import { + createCollapsedSelection, isCollapsedSelection, normalizeRange, type WordsPoint, @@ -747,6 +748,9 @@ export class WordsProvider { return; } + // A mark / block command from a surface outside the editable (toolbar) + // needs a caret to act on — arm one if the editor was never focused. + this.ensureCaret(); const command = this.commandFromName(name, opts.level); if (!command) return; const changed = this.applyCommand(command); @@ -1506,6 +1510,19 @@ export class WordsProvider { return true; } + /** + * TipTap's `.chain().focus()` equivalent: a selection-dependent command + * invoked from a surface OUTSIDE the editable (the menu bar, a panel) + * needs a caret to act on. The provider freezes the selection across blur, + * so this only fires on a never-focused editor — it drops a collapsed + * caret at the document start so marks / inserts apply there instead of + * silently no-op'ing. No-op if a selection already exists. + */ + private ensureCaret() { + if (this.selection) return; + this.setSelectionPublic(createCollapsedSelection([0, 0], 0)); + } + private isInsideWordsTool(element: HTMLElement): boolean { // Internal-tool predicate for the focus scope. Any tool that // the user clicks WITHOUT intending to leave the editor must @@ -1714,6 +1731,9 @@ export class WordsProvider { * `toggleList`. Caret lands at the start of the new block. */ insertBlockOfType(id: WordsSlashCommandId, target?: HTMLElement): boolean { + // With no caret the insert would silently fall back to the last block; + // arm a caret at the doc start so a toolbar insert lands predictably. + this.ensureCaret(); const sel = this.selection; const doc = this.history.present.document; const currentIdx =