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 =