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;