diff --git a/src/uix/eidos/components/palabras/palabras-bubble.svelte b/src/uix/eidos/components/palabras/palabras-bubble.svelte index 6cabd733f..5aa59a546 100644 --- a/src/uix/eidos/components/palabras/palabras-bubble.svelte +++ b/src/uix/eidos/components/palabras/palabras-bubble.svelte @@ -3,29 +3,46 @@ * Selection bubble — inline formatting over a text RANGE (INTERACCION.md §4.3). * * soma `Words.BubbleMenu` owns the hard parts: it floats above the STABLE - * selection (anti-flicker ~150ms), positions itself (`--_words-bubble-x/y` + - * `data-side`), and holds the don't-steal-the-selection invariant (pointerdown - * inside `[data-words-bubble-menu]` never collapses the range — the provider's - * focus scope lists it). This renders its CONTENT, composing framework - * primitives — no bespoke widgets: - * - marks ride `Words.CommandButton` (restores the DOM range before the command, - * reflects the active state via `data-active`), drawn as eidos `Button`s; - * - link is an inline draft input (Enter applies via `insertLink`, Escape cancels), - * with an unlink `CommandButton` when the caret sits on a link. + * selection, positions itself, and holds the don't-steal-the-selection invariant + * (pointerdown inside `[data-words-bubble-menu]` never collapses the range). This + * renders its CONTENT, composing framework primitives: + * - marks (B/I/U/S/code) ride `Words.CommandButton` (restores the range, reflects + * `data-active`); + * - link is an inline draft input; + * - «Aa» opens a RANGE-TYPOGRAPHY row (F2.4, §4.3's "tipografía de rango behind an + * overflow"): sub/sup + size + family, applied as engine marks via + * `applyCommand({type:'toggleMark', mark})` (same value toggles off, a new value + * replaces). The typography controls stay INSIDE the bubble / the Select's portal + * carries `data-words-external-tool`, so the range survives. * - * Per §4.3 the bubble is the POINTER path; ⌘B/I/U/⌘K work without it (L3 parity, - * not dependency). Turn-into lives in «/», colour in the panel (§4.4) — not here. - * Range typography (F2.4: size/family/sub/sup) + «→ propiedades» land next, behind - * an overflow. + * Per §4.3 the bubble is the POINTER path; ⌘B/I/U/⌘K work without it. Turn-into + * lives in «/», colour in the panel (§4.4) — not here. */ import { tick } from 'svelte'; import * as Words from '$soma/components/words'; import { ActiveEidos } from '$uix/eidos'; import { Button } from '$uix/eidos/components/button'; - import { Bold, Italic, Underline, Strikethrough, Code, Link, Unlink, Check } from '$uix/eidos/components/icon'; - import type { ProviderSnippetProps } from '$soma/components/words'; + import { NumberField } from '$uix/eidos/components/number-field'; + import { Select } from '$uix/eidos/components/select'; + import { + Bold, + Italic, + Underline, + Strikethrough, + Code, + Link, + Unlink, + Check, + Type, + Subscript, + Superscript, + ArrowLeft + } from '$uix/eidos/components/icon'; + import { PALABRAS_FONTS } from './panel-schema'; + import type { ProviderSnippetProps, WordsMark } from '$soma/components/words'; + import type { PalabrasScheme } from './types'; - let { api }: { api: ProviderSnippetProps } = $props(); + let { api, scheme }: { api: ProviderSnippetProps; scheme: PalabrasScheme } = $props(); const eidos = ActiveEidos.require(); const ts = (ref: string) => eidos.langs.ts(ref); @@ -38,9 +55,33 @@ { command: 'code', label: '#?components.palabras.mark.code|Código', icon: Code } ] as const; - // Link — inline draft. `insertLink` applies to the STABLE range the bubble keeps - // alive; focusing the input doesn't collapse it (it lives inside the focus scope). + // ── Range typography (F2.4) — engine marks over the live selection. ── + const fontGroups = $derived(scheme.fonts ?? PALABRAS_FONTS); + const allFonts = $derived(fontGroups.flatMap((g) => g.fonts)); + const marks = $derived(api.activeMarks); + const hasBool = (m: 'sub' | 'sup') => marks.includes(m); + const paramValue = (type: 'fontSize' | 'fontFamily'): T | undefined => { + const m = marks.find((x): x is Extract => typeof x === 'object' && x.type === type); + return m ? (m.value as T) : undefined; + }; + const currentSize = $derived(paramValue('fontSize')); + const currentFamily = $derived(paramValue('fontFamily')); + + function toggleBool(m: 'sub' | 'sup') { + api.applyCommand({ type: 'toggleMark', mark: m }); + } + function setSize(n: number | undefined) { + if (!n || n <= 0) return; + api.applyCommand({ type: 'toggleMark', mark: { type: 'fontSize', value: n } }); + } + function setFamily(v: string | undefined) { + if (!v) return; + api.applyCommand({ type: 'toggleMark', mark: { type: 'fontFamily', value: v } }); + } + + // Link — inline draft over the stable range. let linkOpen = $state(false); + let typoOpen = $state(false); let linkValue = $state(''); let linkInput = $state(null); @@ -66,6 +107,7 @@ linkOpen = false; } } + const keepSelection = (e: PointerEvent) => e.preventDefault(); @@ -89,12 +131,95 @@ size="sm" iconOnly aria-label={ts('#?components.palabras.action.apply|Aplicar')} - onpointerdown={(e: PointerEvent) => e.preventDefault()} + onpointerdown={keepSelection} onclick={applyLink} > {#snippet icon()}{/snippet} + {:else if typoOpen} +
+ + + + + +
+ + − + + + + +
+
+ +
+
{:else} {#each MARKS as mark (mark.command)} {@const Glyph = mark.icon} @@ -109,13 +234,23 @@ +