Primer componente F1 con comportamiento real (soma), y el que desbloquea el
block site-header (F2.1). Construido tras un fan-out de investigación (6
lectores) que fijó el mapa: template = FeedSentinelProvider, motor =
IntersectionObserver por el port bendecido de $adom, escritura de attrs por
syncAttrs (nunca dom.apply crudo). Alcance E-2 = suelo del dossier §P3.
- morfo: 2 partes (provider=box → data-sticky/data-edge/data-stuck ·
sentinel aria-hidden → data-edge); 0 eventos con justificación de COMPLEX
BEHAVIOR (apg: none); data-stuck presente/ausente + data-edge enum
espejan @container scroll-state(stuck: top/bottom) — este componente es el
polyfill cross-browser de ese contrato CSS futuro
- soma: un StickyProvider registra AMBAS partes en un runtime; el observer
vive en un $effect que RETORNA observeIntersection(...) como teardown
(mirror de feed); el callback async voltea stuck=$state; rootMargin
DERIVADO del offset (la línea de disparo del centinela ES la línea de pin
— acoplamiento matemático, offset obligatorio); root=scroll-host
(default viewport), Document coercionado a null; offset viaja como
--_sticky-offset (dato A8, no CSS visual — precedente meter)
- eidos: recipe fino que SOLO posiciona (position:sticky + inset lógico
desde --_sticky-offset + token --sticky-z-index + centinela layout-neutral
2px con margen negativo); el tratamiento del estado pegado es del
consumidor ([data-sticky][data-stuck]) — el recipe posiciona, no decora
- centinela = HERMANO de flujo del box (dos nodos raíz), no hijo (un hijo se
fija con el box y nunca detecta); orden según edge
- demo v2 9 tabs con scroll container real + readout de stuck vivo; READMEs
soma + eidos (Comparativa AntD/Mantine/Chrome-technique/headless/CSS ·
Decisiones · Passive justification · Gaps con disposición)
Verificación: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed/0
invalid ([data-stuck] sin regla = intencional, consumidor decora) ·
morfo:check limpio · svelte-check sin regresión propia · 5 tests: 4 unit
(rootMargin por borde, root null-coerce, flip de stuck, disabled, cleanup) +
1 REAL-IntersectionObserver en chromium (foreground: no-stuck→scroll→
stuck→scroll-back→no-stuck — prueba end-to-end de la geometría offset↔
rootMargin, lo que el pane suspendido no puede). Navegador: box pinea a los
12px del offset, recipe correcto.
Decisión declarada (test harness): sticky-provider.svelte.test.ts usa el
installSomaHarness compartido (translator identidad) como los otros 86 tests
de provider — TÉCNICAMENTE en tensión con la regla CLAUDE.md «never fake
translators; use createActiveUix». Son unit tests del provider; la
integración real la cubren contracts.test.ts + la verificación en navegador.
Seguido el precedente universal; flaggeado para tu ratificación.
Nota: nav sidebar de sticky (web/routes/uix/+layout@.svelte) fuera del commit
— el archivo arrastra un flip CRLF de sesión paralela (2125 líneas de ruido);
la entrada viaja en el árbol de trabajo. Registros langs/soma de sticky
reconstruidos sticky-only (los índices tenían entangle con aura sin commitear).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nuevo efecto `water` en $scene/effects + registro en el pack Ambient
(import = opt-in). Superficie de mármol de vetas incandescentes sobre
negro que se ondula bajo el ratón: cada tramo de puntero engendra un
anillo que se expande y refracta la superficie (modelo de anillos de
pixel-blast). 11 params override por token (P-4). Alta en el catálogo
demo (Tier B, 33 efectos) y en el README del pack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- #4: el guard capture-phase del BubbleMenu hacía preventDefault + restoreDomSelection
en TODO pointerdown del bubble, así que clickear el input de tamaño nunca lo
enfocaba (solo +/− servían). Ahora se salta el guard cuando el target es un control
de formulario nativo (input/textarea/select): el input se enfoca y como
syncSelectionFromDom CONGELA el modelo con foco fuera del editor, el bubble sigue
abierto y la marca aplica al rango. Los botones (marcas) no necesitan foco y siguen
con onclick; el resto de clicks siguen protegiendo el rango. Verificado Playwright:
teclear "48" + Enter aplica font-size:48px.
- #11: openLink saca el foco del editor al input de enlace; cerrarlo (aplicar O
cancelar con Escape) no lo devolvía → editor sin caret, teclado muerto hasta
reclicar. Ahora returnFocusToEditor() (api.focus + api.restoreCaret) en ambos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Find (#8): close() solo cambiaba `open`; la query/matches viven en el provider
raíz, así que cerrar dejaba los resaltados <mark data-palabras-find-match>
pintados sin UI para quitarlos (contradecía el contrato documentado). Como el
`find` del snippet no llega al close() del script exterior, muevo el Escape al
onkeydown del PANEL (dentro del snippet, con `find`) y limpio la query en ambos
cierres (✕ + Escape). Verificado Playwright: Escape cierra y borra los 7 marks.
- Find (#15): el contador podía mostrar N/M con N>M si los matches menguaban por
una edición directa (currentMatchIndex no se re-clampa). Clamp en el display.
- Slash (#9): 14 ítems, ~9 visibles; la navegación por teclado no hacía scroll al
activo → Enter insertaba algo invisible. Efecto (raf-diferido, iframe-safe) que
hace scrollIntoView del [data-active]. Verificado: ArrowUp→"Columnas" en vista.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cuatro defectos confirmados por el barrido:
- Panel (#5): tras clickear una tab (tabAuto=false), cambiar a un bloque sin
Contenido dejaba la tab en 'contenido' (trigger no renderiza) → panel en blanco
sin tab activa. El efecto de auto-landing ahora corrige una tab INVÁLIDA aunque
tabAuto=false (cae a la 1a disponible). Verificado Playwright: párrafo → Diseño.
- Inspector (#7): el draft de `src` derivaba `alt` de split('/')/split('.') →
basura base64 para data: URLs. Ahora se salta la derivación para data:.
- Spacing (#10): el override de enlace (localUnlink) se reseteaba por
JSON.stringify(value); dos bloques sin spacing serializan igual ('null') → el
override se filtraba entre ellos. Ahora resetea por identidad de bloque
(resetKey=api.activeBlockId), cableado en panel + inspector.
- Draft-field (#12): Enter llamaba confirm() y luego blur() que re-dispara
onblur=confirm antes de que `value` se propague → doble onCommit → 2 entradas
de historial. Guard de reentrada por el flag `editing`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tres defectos confirmados por el barrido en el mismo fichero:
- Click SECO (sin arrastrar) en un tirador de columna congelaba un ancho fijo
espurio: el guard comparaba liveWidth (float de getBoundingClientRect) contra
Math.round(startWidth), asi que cualquier columna sub-pixel disparaba el commit
(auto -> fijo) + entrada de historial. Ahora un flag `moved` (solo true si hubo
pointermove) hace del click un no-op real.
- Reflow forzado: onDragUp limpiaba el width inline y LEIA rects sync
(remeasure) → sync-after-write. Diferido via dom.measure (frame post-layout
coalescido), que ademas mide la geometria ya comprometida, no la transitoria.
- Sin pointercancel ni cleanup de desmontaje: una interrupcion (touch/pen cancel,
gesto) dejaba dragKey atascado (handles congeladas, remeasure off) + listeners
de window filtrados. Anadido onDragCancel (revierte sin commitear) + un $effect
cuyo cleanup libera un drag en vuelo al desmontar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tercera pieza F1 del plan blocks (alcance E-2, dossier §P3). Banner = tira
de anuncio de pagina; Callout = el aside del documento (split deliberado).
- morfo: 4 partes (provider role=note / icon / title / content) + texts con
TITULOS DEFAULT LOCALIZADOS por intent (note/tip/warning/caution — patron
GitHub de label visible: la semantica nunca viaja solo en color/icono);
0 eventos justificados (dismissible DIFERIDO a pasada soma+sema por la
regla de admision — dismiss ES un evento real)
- el hueco IMPORTANT resuelto con el modelo Radix, sin 5o enum: `intent`
(neutral|affirm|risk|threat — mapea NOTE/TIP/WARNING/CAUTION) + `color`
override SOLO bajo neutral (doctrina §4: intent evaluativo gana);
IMPORTANT = intent neutral + color + titulo propio (preset en la demo)
- pintura sobre la maquinaria C6/THM-2 (patron badge): forwarders por color
+ slots `_palette-track/text/solid` → el generador emite la cascada de 8
roles Y el forward presence-guarded al shared layer — 33 escalas y
colores custom con CERO CSS extra
- a11y: role=note NOMBRADO via aria-labelledby → Title SOLO mientras esta
montado (registro reactivo por context local); escalacion tipada
role=status|alert|none (el role=alert estatico de shadcn = bug de
referencia que NO copiamos); Title NO es heading (protege el outline que
escaneara anchor-nav); icono decorativo = mapa doctrinal IntentIcon
- fix cazado en navegador: el `+=` del registro leia el estado dentro del
tracking del $effect del hijo → effect_update_depth silencioso (contador
a -997, attr nunca estampado) — untrack() en register/cleanup; leccion
registrada en memoria (incidente 2 de la clase)
- recipe: grid con acento logico border-inline-start (RTL-correcto),
nesting tolerado (Docusaurus); demo v2 9 tabs con preset IMPORTANT +
PalettePicker showIntent=false; README completo
Verificacion: component:audit PASS 0E/0W · eidos-lint 8 morfo-backed/0
invalid · guards 37/37 · svelte-check 76E/51W = baseline exacto ·
navegador: «Nota»/«Atencion» localizados · risk=ambar hue 45-60 ·
IMPORTANT=plum hue 326 resuelto por shared layer · labelledby=titleId ·
cero errores de consola.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dos bugs confirmados por el barrido adversarial del editor:
1) El fantasma de respaldo + borde de requerido no se pintaban en los ITEMS de
lista: renderListItem nunca pasa por renderBlock (donde se estampa el
fallback), asi que un item sin traducir salia vacio sin la referencia del
idioma origen ni el aviso de requerido — aunque documentLangHasContent SI lo
cuenta, dejando el punto de completitud incoherente. Espejado el mismo
patron de renderBlock en el <li> via ctx.decorations.i18nFallback.
2) Un write externo a `value` que cambia activeLang se revertia solo:
syncExternalState actualizaba activeLangState pero NO opts.activeLang.current,
asi que el efecto de sync leia el prop viejo y llamaba setActiveLang(idioma
viejo). Ahora sincroniza el prop (como setActiveLang y el constructor).
Tests: render (li fallback + required flag) + provider (write externo con
activeLang distinto se honra, no revierte). 35 render + provider verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Segunda pieza F1 del plan blocks (alcance E-2 = suelo de paridad, dossier
§P3). Comparte el esqueleto de empty-state a proposito: EmptyState describe
AUSENCIA de datos, Result reporta un RESULTADO — un lenguaje de layout, dos
contratos (duplicacion consciente registrada en el README; revisable al 3er
consumidor).
- morfo: 6 partes display (provider/media/title/description/actions/extra;
extra SIN archetype — parte genuinamente propia), scope ['eidos'],
0 eventos justificados (el resultado ya OCURRIO antes de renderizar)
- status: enum de 7 = paridad AntD CON `warning` (decision firme del
dossier) y HTTP renombrados semanticos (forbidden/not-found/server-error,
nunca '404' stringly); default 'info'; data-status = attr eidos-only
- media default: compone el mapa doctrinal IntentIcon (success→fulfill ·
error→threat · warning→risk) + Info de catalogo; HTTP = codigo mono
grande NEUTRO aria-hidden (situaciones, no fallos — AntD jamas pinta 404
de rojo); children reemplazan el default entero (context local eidos-only
con getter reactivo)
- Title default h2 (vs h3 de empty-state — Result suele SER la pagina);
Actions con label→role=group; Extra alineado a inicio (detalle que lee)
- recipe: glifo display 3× xl bundle; tokens publicos
gap/actions-gap/measures + {status}-color como forwarders de rol
retintables; sin eje size (paridad AntD, gap diferido)
- demo v2 9 tabs con copy por status + entrada nav (Status) + README
(Comparativa · Decisiones · Passive justification · Gaps con disposicion)
Verificacion: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed + 5
eidos-only sancionados · recipe/api/visual-attrs 37/37 · morfo:check verde
(los 7 fallos listados son deuda foranea preexistente) · svelte-check
76E/51W = baseline exacto · navegador: success=fulfill verde 84px ·
error=threat rojo · not-found=«404» mono neutro, copy conmutando, cero
errores de consola.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El bubble tiene sub-vistas (typoOpen/linkOpen) que viven en el componente. Al
cerrarse solo se desmonta el contenido (el {#if bubble.open}), no el estado, asi
que la SIGUIENTE invocacion reabria directa en Tipografia/Enlace. Reseteo ambos
cuando la seleccion se colapsa (el bubble solo se muestra sobre un rango): una
edicion de rango preserva el rango -> no resetea; colapsar cierra el bubble ->
resetea. Repro Playwright: seleccionar->Aa->elegir fuente->colapsar->reseleccionar
= reabre en la fila de marcas (typoLeftover 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Al quitar el campo Texto, el parrafo se queda sin Contenido y el efecto de
seleccion de tab podia CAER en Avanzado (carrera: Diseño vacio un instante al
montar) y quedarse ahi. Ahora un flag tabAuto mantiene la tab en la primera
no-vacia (Contenido->Diseño->Avanzado) mientras el usuario no clicke una, robusto
a que las secciones lleguen tarde. Verificado: parrafo abre en Diseño (3/3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El bloque data-palabras-tabs fijaba una margin tight en la lista pero NO anulaba
el gap del recipe Tabs (--tabs-stack-gap, ~16px), que se sumaba encima flotando el
acordeon lejos de las pestañas. Anulado a 0 (la lista ya trae margin tight y el
contenido su hairline + padding). Hueco tabs->cabecera 28->12px. Verificado Playwright.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firmas del usuario (2026-07-21) tras el dossier de referencia:
- E-1: componente canonico nav-tree (F1.8, data-driven — precedente Menubar)
resuelve la colision sidebar-app vs arbol-docs/B-5; fiche nueva con
fase 0 obligada contra nuestro tree-view; docs-shell compone NavTree.
- E-2: suelo de paridad del dossier = alcance v1 (regla en intro F1).
- E-3: F2 pasa de 10 a 14 (banner · team · contact · content-section);
fiches nuevas + galeria actualizada (verificada en navegador: 28 badges).
- E-4: distribucion registry/llms.txt/MCP → iniciativa propia (next-features
§9; ese archivo sigue fuera del commit por diff foraneo).
- E-5: ⌘K = listener app-land documentado (nota en F4.1).
Doctrina: promotion path de architecture/blocks.md actualizado a 8.
Verificacion: docs:check 0/0 (512).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cuando una hoja de texto está sin traducir en el idioma activo, el editor ya no la
muestra VACÍA: pinta el contenido del primer OTRO idioma con texto (en orden de
`doc.languages`) como referencia atenuada — para que el autor vea QUÉ traducir. Y
si el idioma activo es REQUERIDO, un borde de precaución (intent `risk`) marca los
bloques que faltan.
Modelo A1 (estudio firmado): DISPLAY-ONLY, el documento NO se toca — la hoja sigue
vacía (sin traducir), el fantasma es un `::before` con el texto fuente; teclear lo
reemplaza (el provider quita el atributo en cuanto la hoja tiene contenido). Así
`langFilled`/`missingRequiredLangs` siguen siendo honestos (vacío = sin traducir).
- engine/i18n.ts: `computePalabrasI18nFallback(doc, requiredLangs)` → mapa por block
id { text (fuente), required } de las hojas sin traducir (visitor read-only).
- engine/render.ts: decoración `i18nFallback`; `renderBlock` estampa central
`data-palabras-i18n-fallback` + `data-palabras-untranslated` (sin tocar los specs).
- provider: computa el mapa en el derived `blocks` + `reqSig` invalida el memo al
cambiar los requeridos.
- palabras.css: fantasma atenuado (`::before` = attr) + borde dashed `--color-risk-*`.
95 tests (provider 62 · render 33) · 0 errores. Verificado Playwright: bloque solo-ES
→ en EN muestra «Solo en español» atenuado + borde ámbar (EN requerido).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encargo del usuario (2026-07-21): estudio de referencia para "estar al menos
a la par y cuanto menos superarlo". 6 agentes de investigación web en
paralelo sobre fuentes oficiales vivas (varias verificadas a nivel de código
fuente: sidebar.tsx del registry shadcn, styles.js de tailwind-typography,
sources de Mantine/Starlight/Docusaurus/Ark).
- docs/process/RESEARCH-blocks-references.md — el dossier: P1 catálogos de
marketing (conteos+variantes) · P2 aplicación (ProTable/ProLayout,
dashboard-01, Tremor 303, Clerk/Supabase) · P3 F1 ligeros (APIs a nivel de
prop; scroll-state(stuck) como vocabulario de plataforma; role=alert de
shadcn = bug a no copiar) · P4 prose+sidebar (not-prose/:where()+donut;
shadcn 23 partes) · P5 docs shells (Fumadocs benchmark; colision F1.7 vs
nav-tree/B-5; ruta svelte2tsx para props TS) · P6 ecosistema Svelte
(shadcn-svelte 58 blocks solo-app; hueco "sistema integrado" VACIO) +
sintesis: 5 conclusiones, enmiendas E-1..E-5 (presentadas, firmas
pendientes), 10 angulos de superacion.
- PLAN-blocks.md — regla nueva §4 (toda fase 0 contrasta contra el dossier),
puntero de colision en F1.7, registro.
Verificacion: docs:check 0/0 (512 docs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fuera el campo textarea «Texto»/«Código» del panel (Contenido) en heading /
paragraph / quote / code: duplica la edición inline del canvas, sin ventaja, ruido.
El párrafo queda sin campos de Contenido → se oculta la tab vacía y el panel abre
en Diseño (efecto que mantiene la tab activa en una no-vacía).
- Menubar: `align="center"` en los Panel/Content (soportado por el componente) →
submenús y paneles CENTRADOS bajo su opción de barra (verificado delta=0).
- Menubar: `sectionCols` pesa los `pills` por nº de opciones → el nivel del heading
(7: H1–H6·HC) gana un tier más ancho y cae en UNA línea (panel 328→418px, 1 fila).
Verificado Playwright: párrafo sin tab Contenido ni «Texto»; heading conserva
Contenido (level+enlace) sin «Texto»; paneles centrados; nivel en una línea. 0 errores.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tier nuevo aprobado 2026-07-21 (D-BLK firmadas; D-BLK.1 enmendada: vive en
src/uix/blocks/). Plan de ejecución completo en docs/process/PLAN-blocks.md.
- docs/architecture/blocks.md — doctrina E1 (regla de admisión canon-vs-block,
contrato B-1..B-11, promotion path); espejo de packs.md
- alias $blocks → src/uix/blocks (vite + svelte.config + tabla CLAUDE.md)
- src/uix/blocks/README.md — mapa del tier + template de README de block (B-9)
- scripts/blocks-check.ts + npm run blocks:check — guard mecánico del contrato
B con self-test de detectores (fixtures inline; 5532 archivos de
canon/arts/libs/packs escaneados para dirección de imports)
- web/routes/blocks/ — galería índice del catálogo planificado + layout de
bootstrap propio (+layout@.svelte, espejo mínimo del de /uix sin packs sema)
Verificación: docs:check 0/0 (511) · svelte-check 76E/51W = baseline exacto ·
blocks:check verde · galería comprobada en navegador (a11y-tree + estilos
computados en light y dark; captura de píxel bloqueada por panel suspendido).
Fuera del commit: fila E1 en docs/README.md y §8 en docs/next-features.md
(diff foráneo de otra sesión sin separar — ver registro del plan).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El rework Camino A (461be12d9, otra sesion) dejaba `multilingual` derivado de la
PROP `languages`; el schema `doc.languages` (9223956a9) estaba puesto pero sin
cablear. Ahora el DOCUMENTO se auto-describe:
- resolvedLanguages: `doc.languages` (si presente) manda; la prop = seed/override.
- constructor: deriva de `initialDoc.languages ?? prop`; SELLA `doc.languages` en el
doc cuando se siembra desde la prop (asi se auto-describe en adelante).
- foldPalabrasLanguages ahora sella `doc.languages` (predataba el campo).
- languageSpecs (etiquetas del switcher) reconciliado con resolvedLanguages.
2 tests nuevos (doc.languages SIN prop → multilingual; prop-seeded → sella
doc.languages). 62/62 provider · 502/502 motor · 0 errores. Verificado Playwright:
misma estructura en es/en (compartida por construccion), contenido por idioma.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base del rework de F-A al modelo de estructura COMPARTIDA (aditivo, sin cambio de
comportamiento; el provider aun usa el modelo-mapa hasta el rework):
- Block.i18n: contenido inline de los idiomas INACTIVOS por bloque (el activo en
`children`); solo hojas de texto. Sobrevive normalize/ops via spread.
- PalabrasDocument.languages: la LISTA de idiomas = FUENTE DE VERDAD de si el doc es
multilenguaje (el doc se auto-describe; la prop `languages` = override opcional).
- PalabrasDocument.activeLang: qué idioma vive en `children` (round-trip).
- normalize preserva languages + activeLang en la reconstruccion raiz (como scheme).
Handoff corregido: F-A shipeada = modelo-MAPA (permite divergencia, rechazado);
rework Camino A EN CURSO (esquema hecho, provider pendiente = tarea de manana).
0 errores palabras · normalize 5/5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El modelo anterior (N documentos independientes por idioma) permitia que la
ESTRUCTURA divergiera: borrar un bloque en un idioma no lo borraba en los
demas. Requisito del usuario: la estructura es unica; solo el TEXTO es por
idioma. Rehecho a UN solo documento con el contenido por-idioma en el bloque.
Modelo (correcto por construccion):
- `block.i18n?: Record<LangCode, Inline[]>` guarda el contenido de los idiomas
INACTIVOS; `block.children` = el activo. Hay UN arbol → las ops estructurales
(borrar/insertar/mover/tipo/tabla) aplican a TODOS los idiomas gratis, y el
undo es automatico (el i18n vive en el doc). `doc.activeLang` registra cual
esta en `children` (normalize lo preserva como `scheme`). El `i18n` sobrevive
normalize + ops via spread (validate no rechaza props extra).
- Cambiar de idioma = swap `children <-> i18n` en el arbol (switchDocumentLanguage).
- Solo el texto INLINE es por idioma; alt/caption de imagen, code.language y
callout.title quedan COMPARTIDOS en v1.
Contrato: `value` vuelve a `PalabrasDocument` (uno solo, no un mapa). Helpers
`foldPalabrasLanguages` (construye el doc desde docs-gemelos por idioma) y
`projectPalabrasLanguage` (extrae un idioma para render/export). El switcher y
la ubicacion (chunks 2-3) intactos.
engine/i18n.ts NUEVO. Provider reworkeado (fuera langHistories/map). Tests
reescritos: incluye el test CLAVE (borrar aplica a todos los idiomas) + insertar
= vacio-a-traducir en los demas + project round-trip. 60/60 provider · 570/570
motor · 0 errores de tipo. Verificado Playwright: borrar 1 bloque en es (4->3) →
en tambien 3, contenido es<->en distinto, 0 errores de consola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El contenido (331px) desbordaba el Popover (~284px, --popover-min-width-md) y el
viewport lo recortaba con scroll horizontal — la ultima columna de emojis salia
cortada, evidente en oscuro. Fixes:
- El Popover recibe su ancho via prop `width` = TOKEN `--emoji-picker-panel-width`
(23rem, token de componente al estilo color-picker content-width — NO hardcode).
El contenido pasa a inline-size:100%; nada desborda el popover.
- PopoverContent size="sm" (padding mas ajustado) + categorias en ToggleGroup
size="xs" → las 9 caben sin scroll (antes 6/9). Tonos size="sm".
- scrollbar-width:thin en el grid (el unico scroll, vertical, esperado).
Tamaños: iconos/glifos canonicos (Icon size, --size-lg/xl-font-size, ToggleGroup
size); grid-max-block-size min(50vh,17rem) replica el patron de command.css.
Verificado en claro Y oscuro (capturas): content Hscroll 0, strip Hscroll 0, 9
categorias visibles, celda 36px, un solo panel; component:audit PASS · eidos-lint
invalid 0 · recipe-contract 30/30 · check baseline 76.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ubica el switcher de idioma por modo (spec del usuario):
- menubar → al final de la barra (antes de la papelera).
- flotante/drawer → en las migas, tras la miga «Documento».
palabras-breadcrumb: prop `langSwitcher` (suprime el switcher en menubar, donde
la barra ya lo tiene). FIX de UX: `setActiveLang` limpia el bloque activo (el id
viejo no vive en el nuevo doc) → el breadcrumb se ocultaba y el switcher
desaparecia tras el primer cambio. Ahora, si es multilenguaje, el breadcrumb
muestra siempre al menos «Documento» + switcher (crumbs sinteticos).
CSS sobrio (tokens del chrome, 0 hex): segmentado con punto de completitud por
idioma, forma select para >3. Demo = multilenguaje es/en.
Verificado (Playwright): flotante=1 switcher (miga, persiste, cambia el
contenido es↔en), menubar=1 (barra, sin duplicado), 0 errores de consola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El `<Command>` eidos estampa data-depth="overlay" + data-list-surface, que
renderizaba su propio panel de paleta ⌘K (bg + borde + sombra + radio,
identico al del Popover) ANIDADO dentro del panel del picker — un panel dentro
de otro, evidente en modo oscuro. Ahora el contexto de busqueda viene del
Command.Provider HEADLESS de soma (sin chrome); Input/Empty siguen siendo las
sub-partes eidos estiladas.
Verificado en claro Y oscuro con capturas: [data-command] transparente
(0 borde, sin sombra) en ambos temas, un solo panel; busqueda 'rocket'->🚀 OK;
teclado/pick/tonos OK; check baseline 76.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nuevo componente emoji-picker y rediseño del escaparate de mensajeria.
emoji-picker (ruta 9 fases):
- Compone Popover (open/close/foco/portal) + Command (busqueda + grid + nav de
teclado + a11y combobox/listbox, su "grid mode" documentado) + dos ToggleGroup
(categorias + tono de piel). Delegated: sin eventos propios (D.1); el grid y
las celdas doble-registran Command.List/Item, conservando la a11y pero con
contrato visual propio (gana especificidad con selectores de 2 niveles, sin
tocar attrs [data-command-*] ajenos — eidos-lint limpio).
- Datos: $libs/emoji, dataset vendorizado de emojibase (MIT) horneado por
scripts/generate-emoji-data.ts (precedente lucide: JSON crudo en static/ +
script generador). ~1906 emoji con nombre CLDR + keywords, 316 con tono de
piel single-person. Grid acotado (categoria activa o resultados de busqueda),
sin virtualizacion (refinamiento v2). Helpers puros + test (12/12).
- morfo delegated (7 partes + header/footer de layout); soma (root + trigger +
content + grid + option + preview, provider-test 4/4); eidos (panel
empaquetado, receta con tokens --emoji-picker-*); langs es/en; demo 5 tabs
(excepcion delegated documentada); READMEs dobles.
Escaparate del bloque (web/routes/uix/demos/chat) — rehecho rico y correcto:
- Avatares por-autor con color solido (Avatar del ecosistema): antes gris
neutro indefinido, ahora teal/ambar/verde/indigo/primary bien contrastados.
- Dos columnas: chat-list (sidebar con badges + typing-en-lista) + sala.
- Conversacion rica: separador de dia, run agrupado, reaccion, reply citado,
recibos de lectura, typing con avatar, y el emoji-picker integrado en el
composer (inserta en el draft).
- Solape de burbujas (transitorio, estimateSize=48 con mensajes ~90-126px)
mitigado subiendo el estimate a 76.
Auditoria de tamanos/espacios: recipes del bloque limpios (todo con tokens
--space-*/--size-*/--color-*); unico literal (punto tono-capable) tokenizado.
Verificacion: check baseline 76 (0 nuevos) · component:audit emoji-picker PASS ·
eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · emoji lib
12/12 · provider-test 4/4 · Playwright PASS (busqueda 'rocket'->rocket+
astronautas, click->onSelect+cierra, teclado, tono oscuro 👋->👋🏿, picker abre
en el composer). Capturas claro miradas.
next-features §7 marca emoji-picker SHIPPED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
palabras-lang-switcher.svelte (eidos): swap del idioma ACTIVO de un documento
multilenguaje. Reflejo del switcher de field-langs: radiogroup segmentado (≤3
idiomas) o Popover con radiogroup (mas), cada opcion con punto de completitud
(langFilled) y teclado de radiogroup (flechas/Home/End). Lee la `api`; el motor
posee el estado.
Api: expone `languageSpecs` (specs crudos con label/dir) para que el switcher
honre etiquetas explicitas; LangCode/LangSpec/PalabrasLangValues re-exportados
del barrel soma. Compila 0 errores.
PENDIENTE (chunk 3): CSS + ubicacion (menubar al final · migas tras Documento) +
demo + verificacion en navegador.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Soporte multilenguaje del CONTENIDO a la field-langs, todo detras de la prop
`languages` (retrocompatible: sin ella, `value` sigue siendo PalabrasDocument).
Contrato:
- value: PalabrasDocument | Record<LangCode, PalabrasDocument>
- languages: LangSpec[] (presencia = modo multi; 1o = activo por defecto)
- requiredLangs: LangCode[] (validez; renombrado por colision con `required`)
- activeLang bindable + onLangChange; onValueChange sobre la union.
Provider:
- historia POR IDIOMA (langHistories): cambiar de idioma nunca cruza pilas de
undo; el activo es this.history, los inactivos en el mapa; seed en construccion
desde un snapshot del value map.
- writeExternalValue escribe el mapa ENTERO (raw) en multi; proxy-safe.
- syncExternalState resuelve el doc activo del value (doc|map).
- api: multilingual, languages, activeLang, setActiveLang, missingRequiredLangs,
langFilled; requiredLangs alimenta isInvalid.
Tests: 5 nuevos (modo+default · switch+writeback · historia por idioma ·
requiredLangs→validez · mono retrocompatible). 58/58 provider. check 0 en palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documenta el resultado (el solver de contraste era innecesario: el morph HEREDA
el contraste de donantes §40-compliant) en los docs de diseño/código:
- docs/rfcs/rfc-color-engine.md: §15 nueva (resolución Stage 2 con las 3 bancas
de evidencia + decisiones D1–D6), anotación de fases §11 (Phase 0 = tipos
diferidos D5; Phase 3 = base→seeds NO se hace, D2), footer + §14.
- src/arts/color/README.md: CONTRAST_PAIRS en la API + nota "contraste heredado,
no resuelto" con puntero a RFC §15 y reference §40.
- src/uix/eidos/COLOR_ENGINE_RFC.md (stub): deja de sobreafirmar que el tipo
ColorScaleSeed existe (está diferido, D5) + puntero a §15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up de 5aa8090ba. AA_TEXT_WCAG y STRONG_TEXT_STEP no tenían consumidor
externo: sus valores ya viajan en cada par (policy.wcag / policy.apcaCapStep),
así que exportarlos aparte duplicaba la fuente → consts internas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
syncExternalState comparaba `externalDocument !== current.document`: bajo un
binding `$state` (bind:value en el demo) el externo es un PROXY y el interno el
objeto crudo → Svelte avisa (state_proxy_equality_mismatch) en cada tecla, y
ademas el fast-path NUNCA cortocircuitaba (proxy !== raw es siempre true → el
deep-compare O(documento) corria igual, anulando el fix de perf del base64).
Cacheo `publishedExternal` = la vista del store (proxiada) al escribir, y comparo
proxy↔proxy. writeExternalValue: fuera el guard `!==` (asignar el mismo objeto es
no-op en Svelte) + cachea el read-back; publishState e init ruteados por el.
Verificado (Playwright, tecleando): 0 state_proxy warnings; el round-trip propio
cortocircuita de verdad. 53/53 provider verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Asimetria: t() (engine) y ts() (mono) devuelven el |fallback en SILENCIO,
pero ts() de engine iba via resolveValue, que emitia REFERENCE_FALLBACK_USED
(WARN) en cada miss-con-fallback → consola ruidosa en DEV para el patron
`#?key|fallback` (i18n gradual: fallback inline, claves de catalogo despues).
El |fallback es el default deliberado del autor, no una condicion avisable.
Gate del emit a `depth > 0`: solo avisa cuando una cadena de referencia
ANIDADA cae al fallback (ref interna rota), no en el lookup de nivel superior.
Alinea engine-ts con t() y mono-ts. Sin test que fijara el warn; 132/132 verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firma del usuario: dirección estética = SOBRIO-DOCUMENTAL (con firma-en-el-
momento) — DG-13. Consolidada en DESIGN.md §0.
FD.3 (spec visual) resultó YA REALIZADO al reconstruir el chrome limpio y
hacer el canvas scheme-driven; verificado en navegador (claro + oscuro):
- estructura: lienzo full-width, grip en carril superpuesto, panel flotante
con data-depth="overlay" (elevación del framework, cero z-index sueltos).
- medida del canvas: scheme-driven (--palabras-scheme-doc-measure, default
ancho completo; 66ch = preset del esquema).
- tokens del chrome: SIN sopa — palabras-chrome.css tiene 0 hex / 0 oklch /
0 ms crudos; color/espacio/duración por herencia de los tokens del framework.
- modo oscuro VERIFICADO: el chrome invierte por tokens (cero media-queries
hand-rolled); el documento conserva su esquema (doctrina back-office).
DESIGN.md: nuevo §11 (sistema visual realizado + verificado, pendiente de
firma) + §12 ABIERTO acotado (acento del tab activo · deuda menor de
tokenización · densidad/reduced-motion · FD.4 firma perceptual, requiere permiso).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primer paso de la puerta FD (PLAN-EJECUCION §FD.1): reúne en un canon
citable las decisiones de diseño YA firmadas, hoy dispersas entre
continuar-palabras.md, comentarios de código y commits. Pura consolidación
(nada nuevo), cada entrada con su fuente. Hermana de INTERACCION.md (FD.2,
ratificada) y ESQUEMA.md (contrato del documento).
Secciones §0-§10 = lo decidido; §11 ABIERTO = lo que FD.3 (mood) y FD.4
(firma perceptual) todavia deben firmar — no se prejuzga aqui.
Pendiente: tu revision (verify de FD.1 = consolidacion fiel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La whitelist `editable` ya decidía si el mega-control Tipografía aparece
ENTERO (schemeAllowsField); faltaba apagar filas INDIVIDUALES con las
claves con punto (`typography.size`…). Refleja el patrón que ya existe
para `adjustments` de imagen.
- PalabrasTypography gana `enabled: 'all' | readonly string[]`. UN solo
mecanismo de visibilidad: `show(key)` unifica el `essential` existente
(inspector corto de la barra) con el `enabled` del esquema — no dos ejes
{#if} paralelos sobre las mismas filas.
- Vocabulario canónico (11 filas, nombres semánticos cortos que extienden
los `size`/`tracking` ya fijados en ESQUEMA, no los nombres CSS):
font·style·weight·size·leading·tracking·word·measure·case·wrap·tabular.
Documentado en ESQUEMA §6 (tabla clave→fila→prop) + apuntado desde types.ts.
- Ambos consumidores (panel flotante/acoplado + inspector de barra) pasan
`enabled={schemeSubControls(scheme, kind, 'typography')}`.
- Test: `typography.size,typography.leading` → sufijos `['size','leading']`.
Verificado Playwright: párrafo gateado a size+leading muestra SOLO Tamaño +
Interlineado; encabezado sin declarar muestra las 11 filas (retrocompatible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El campo Familia del bubble mostraba el placeholder «Fuente» cuando el
rango no tenía override `fontFamily`, aunque el texto SÍ se pinta con la
fuente del esquema. Ahora muestra la fuente efectiva (nunca en blanco),
en paralelo con el tamaño efectivo ya existente (`effectiveSize`).
- `schemeFontFor(scheme, blockType, node)` en palabras-scheme.ts: la fuente
del elemento (`styles[el].fontFamily`) con fallback a la base `text`.
Casa con un `allFonts[].value` porque los estilos por defecto y las
opciones de fuente comparten las constantes SERIF/SANS/MONO.
- palabras-bubble.svelte: `effectiveFamily = currentFamily ?? schemeFontFor(...)`;
el Select de Familia usa `effectiveFamily` para value/data-empty/label.
- Tests: 2 casos de `schemeFontFor` (resolución elemento→text; override h1).
Verificado en Playwright: párrafo y encabezado muestran «Georgia»
(efectiva), sin `data-empty`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename + completa la utilidad de 013ef5afc:
- `DataUrlBlobUrls` → `DataUrlBlobCache` (nombre torpe: doble «Url» + plural);
fichero data-url-blobs.* → data-url-blob-cache.*.
- Añade `prune(keep)`: revoca + descarta los object URLs cuyo `blob:` URL ya no
está en `keep` (el set de `<img>` montados), liberando el blob de una imagen
borrada sin esperar a `dispose()`. Antes solo se revocaba todo al desmontar
(leak acotado a la sesión).
- palabras-image-blobs: tras el swap colecta los `img[src^="blob:"]` montados y
poda. Docs (imagenes-base64 / handoff) actualizados.
Verificado: 6 tests adom (incl. prune revoca-solo-no-keep) + check 0 errores;
Playwright: el tecleo NO se regresa por la query de poda (9.2ms). La lógica de
poda queda por unit test; el borrado end-to-end no se pudo conducir fiable en el
harness (mismo flake del menú/panel de imagen), cableado trivial sobre el effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Componente nuevo del bloque chat v2: el panel de lista de salas/DMs.
Compone el Listbox del ecosistema (doble registro id/ref compartido):
selección única + roving-tabindex + typeahead + role=option vienen de
Listbox; chat-list aporta la disposición de fila (avatar · título/preview ·
meta) y el estado visual data-unread (negrita + Badge compuesto).
- morfo: expression 'delegated', sin eventos propios (D.1 — el actor es el
Listbox compuesto); parts provider/item(data-unread)/avatar/title/preview/meta.
- soma: ChatListProvider crea ListboxProvider dentro (value string <-> string[]);
ChatListItemProvider per-instance (unread = boolean crudo); 4 clases de
contenido explicitas.
- eidos: rejilla avatar | (titulo/preview) | meta, avatar y meta abarcan las
dos filas; truncado; unread -> titulo en negrita. No neutraliza el arquetipo
item (la conversacion ES una opcion seleccionable). Sin @keyframes; ejes
logicos.
- demo: 5 salas, seleccion limpia unread, typing-en-lista (compone ChatTyping),
Badge de no-leidos; tabs live/system/api/morfo/a11y.
- READMEs dobles (eidos con Passive justification, delegated); next-features 7
marca chat-list SHIPPED.
Verificacion: check baseline 76 (0 nuevos) · component:audit PASS ·
eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · Playwright
PASS (role=listbox, 5 salas, aria-selected, unread=2, badges=2, typing-en-lista,
click + teclado seleccionan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Corrige la conclusión de la investigación (la hipótesis DOM-src era errónea; el
CPU profile dio samePalabrasDocument = 34%). imagenes-base64.md §Causa REAL +
handoff §2026-07-19: fast-path d96e4cde7 (20→12ms) + blob 013ef5afc (12→8ms).
Lección anotada: perfilar ANTES de teorizar en atascos de perf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan de ejecucion para sesion aparte, VERIFICADO adversarialmente (workflow de
7 agentes contra codigo y docs) antes de escribirse — corrige 5 afirmaciones
de la respuesta que lo origino:
- "por delante de Radix (hex sRGB)": FALSO — el propio RFC §14 registra Radix
con APCA y P3 ("Radix style"); la posicion del repo es "on par"; nuestra
paleta enviada es sRGB-equivalente (oklch = siblings de hex).
- "el solver satisface la tabla §40": el UNICO floor duro ratificado es
text-strong·12 ≥ 4.5; text·11 quedo tier blando (decision D1); solid·9 esta
anclado exacto a la semilla; el script de auditoria aun codifica la tabla
PRE-veredicto (consumirla contradiria los veredictos → F0 = tabla como datos).
- "los tipos ya existen en parte": ColorScaleSeed/Source son solo prosa del
RFC; config-seeds imposibles hoy (solo applyColorScheme roles-only +
generatePalette full-33 como bancos).
- "792 hex en base.ts": exacto en total pero en DOS ficheros (288 base.ts +
504 color-scales.ts); 4 escalas ya autoradas desde semillas offline.
- "nada downstream cambia": nombres si (contract.ts), pero hay pins de VALOR
disenados para dispararse (hex literales, flip-set de polaridad,
generated/base.css byte-exacto) + persistencia rechaza version ≠ 1.
El plan: F0 tabla ratificada como datos + harness sobre output generado ·
F1 solver (biseccion contra sRGB gamut-mapeado, salida hex, desviacion minima
del donante) · F2 validacion en bancos de semilla + navegador · F3 (gated)
migracion base→seeds con re-ratificacion de pins y pase visual. 6 decisiones
de usuario ABIERTAS al frente (target text·11, semantica base+ΔE, modo
on-solid, colocacion/alcance, tipos Fase 0, normalizacion de la tabla).
next-features §1 enlaza el plan. (El hunk ajeno de chat-list en next-features
queda fuera del commit — danza clean-desde-HEAD.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Utilidad transversal + su primer consumidor (palabras). Win SECUNDARIO sobre el
fast-path de d96e4cde7 (no es el fix del atasco; ese era el deep-compare).
- `$adom.DataUrlBlobUrls` (NUEVO, `arts/adom/data-url-blobs.ts`): caché stateful
que resuelve `data:` URLs a `blob:` object URLs (decodifica base64/percent),
cachea por identidad, `dispose()` revoca. Realm same-origin. 5 tests.
DECISIÓN de ubicación (valorada): NO va en `<Image>` — ningún consumidor de
`<Image>` pasa base64 (image-picker/file-upload ya usan createObjectURL de un
File; el resto URLs reales) → sería abstracción prematura. Es una utilidad
reutilizable que `<Image>`/ImageProvider pueden adoptar si algún día aparece un
consumidor base64.
- `palabras-image-blobs.svelte` (NUEVO): tras cada render swapea el `src` de los
`<img data:…>` del contenteditable por el blob (el MODELO conserva el base64 →
persistencia; el DOM queda ligero). Re-run por `blocks`; el `<img>` memoizado
mantiene el blob entre teclas.
Medido (base64 de 2MB, con el fast-path ya puesto): tecleo 12ms/char → 8ms/char
(el `src` de MB en el DOM sí añadía ~2-4ms). check 0 errores; Playwright: `src`
= `blob:` tras cargar, tecleo 8.1ms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CAUSA REAL del «atasco» de imágenes base64 (hallada por CPU profile: 34% de las
muestras). El sync del `bind:value` (`syncExternalState`) comparaba el `value`
bindeado contra el doc interno con `samePalabrasDocument` — una comparación DEEP
O(documento) — EN CADA cambio. Como el editor escribe `value` con la MISMA
referencia del doc interno (publishState → `this.document`), esa comparación
recorría el string base64 (MB) por tecla para nada.
Fix: un check de igualdad por REFERENCIA antes del deep-compare — el caso común
(nuestras propias ediciones) se resuelve en O(1); el `samePalabrasDocument` solo
corre cuando la referencia difiere de verdad (una escritura externa).
Medido (imagen base64 de 2MB en el modelo): tecleo 20ms/char → 12ms/char. NO era
el `src` grande en el DOM (mi hipótesis inicial, errónea) — era el deep-compare.
Verificado: provider 53 tests verdes; check 0 errores palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>