From 377168550c8a7f6d28e9d10db6b9801e23da85d2 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 28 May 2026 00:12:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(words):=20drawer=20follows=20emptiness=20?= =?UTF-8?q?=E2=80=94=20Insert=20when=20empty,=20Inspect=20when=20filled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-clarified rule for the right drawer: when the current block is empty, the drawer shows ONLY the constructors (Insert panel) — no point in showing format/block-scope options on a blank line. When the block has any content, the inspector for that content type appears (paragraph + text-align, heading, image, code, etc.). Implementation: - New helper `isEmptyEditableTextBlock(block)` next to the slash command catalog. It returns true for paragraph / heading / quote / code-without-language whose single text child is the empty string. Atomic blocks (image / table / list) never qualify — they always carry structure. - `WordsDrawerProvider.modes` derivation gates the scope-stack computation behind `!isEmptyTextBlock(currentNode)`. Empty text blocks fall through directly to `['default']` (just Insert); blocks with content build the scope stack as before (format / block / cell / row / table / list-item / list) and Insert collapses at the bottom of the stack. - Atomic blocks (image / code with language) still push their scope unconditionally because their "content" lives in attributes, not children. Before: clicking "+" → empty paragraph inserted → caret on it → drawer showed `['block', 'default']` with Block expanded (text-align controls for an empty line — useless), Insert collapsed below. After: clicking "+" → empty paragraph inserted → caret on it → drawer shows `['default']` only (Insert expanded). As soon as the user types a character, Block panel appears above with Insert collapsed underneath. 152/152 soma words tests still pass; `npm run check` clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/words/words-provider.svelte.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/uix/soma/components/words/words-provider.svelte.ts b/src/uix/soma/components/words/words-provider.svelte.ts index e80898032..eabf25bc3 100644 --- a/src/uix/soma/components/words/words-provider.svelte.ts +++ b/src/uix/soma/components/words/words-provider.svelte.ts @@ -2645,12 +2645,24 @@ export class WordsDrawerProvider { const scopeStack: WordsDrawerMode[] = []; + // Emptiness gate — if the current block is a text-style block + // with no content, skip the scope entirely so the drawer only + // shows Insert (no point in formatting an empty line). Atomic + // blocks (image/table/code-with-language) always have content + // and pass through. Per user spec: "cuando el bloque está vacío + // solo se muestran los constructores; cuando ya tiene contenido, + // las opciones del objeto". + const currentIdx = sel?.anchor.path[0] ?? this.provider.selectedBlockIndex ?? -1; + const currentNode = + currentIdx >= 0 ? this.provider.document.children[currentIdx] : undefined; + const isEmptyTextBlock = isEmptyEditableTextBlock(currentNode); + // Atomic blocks (no inline format, no parent scopes) if (block === 'image') { scopeStack.push('image'); } else if (block === 'code') { scopeStack.push('code'); - } else if (sel) { + } else if (sel && !isEmptyTextBlock) { // Inline-text format is the most specific scope. Only meaningful // when there's a non-collapsed selection AND the current block // can host inline marks (excludes 'code' which is plain text). @@ -2941,6 +2953,39 @@ const DEFAULT_SLASH_COMMANDS = [ } ] as const satisfies readonly WordsSlashCommandItem[]; +/** + * "Empty text block" check used by the drawer's modes derivation + * (Insert ↔ Inspect rule): if the current block is a text-style + * block with no content, the drawer shows ONLY the Insert panel + * instead of the scope inspector. Paragraph / heading / quote / + * code-without-language qualify when their single text child is + * the empty string. Atomic blocks (image / table / list) never + * "look empty" — they always carry structure or content. + */ +function isEmptyEditableTextBlock(block: unknown): boolean { + if (!block || typeof block !== 'object') return false; + const b = block as { + type?: string; + language?: string; + children?: readonly { type?: string; text?: string }[]; + }; + if ( + b.type !== 'paragraph' && + b.type !== 'heading' && + b.type !== 'quote' && + b.type !== 'code' + ) { + return false; + } + // A code block with a language already carries intent — even an + // empty body deserves the Code inspector so the user can change it. + if (b.type === 'code' && b.language) return false; + const kids = b.children ?? []; + if (kids.length !== 1) return false; + const only = kids[0]; + return only?.type === 'text' && (only.text ?? '') === ''; +} + function filterSlashCommandItems(query: string): readonly WordsSlashCommandItem[] { const normalized = query.trim().toLowerCase(); if (!normalized) return DEFAULT_SLASH_COMMANDS;