20 KiB
palabras — ESQUEMA del documento (spec para ratificación)
2026-07-14. El esquema es el contrato completo del documento: lo que la app le pasa a
<Palabras>para gobernar CÓMO SE VE el documento y QUÉ SE PUEDE EDITAR en él — independiente del tema de la página anfitriona (el editor puede vivir en un backoffice cuyo look no tiene nada que ver con el documento que se redacta). Estado: RATIFICADA por el usuario (2026-07-14, «empieza») — este documento es el contrato que la implementación ejecuta. Las propuestas de las puertas ⚠️ del §12 quedan aceptadas tal como están escritas (temperature entra al modelo del motor; field-types custom → v2; ambos niveles de extensión; @nombre sí). Lo implementado antes de la firma (30a67533) era la base parcial del §11.
1. Las seis piezas del esquema
| Pieza | Qué declara | A dónde proyecta |
|---|---|---|
fonts |
Las fuentes ADMITIDAS en el documento (grupos con label; pesos disponibles por fuente) | Dropdown Familia del panel (y validación del valor fontFamily de los overrides) |
sizes |
La escala tipográfica: tamaño por elemento (text, h1…h6, quote, code, image.caption…) |
Var CSS por elemento + el valor "heredado" que muestra el panel |
palette |
Los colores predefinidos del documento, CON NOMBRE (referenciables) | Swatches del ColorPicker + referencias @nombre desde cualquier estilo |
editable |
QUÉ puede editarse por elemento (lista blanca de campos del panel, con granularidad de sub-control) | Gobierna el panel: campos ofrecidos, secciones visibles |
styles |
El estilo base COMPLETO de cada elemento y sus variantes/sub-partes (§2–§3) | Vars CSS + reglas por elemento en el lienzo; el «↺» del panel vuelve aquí |
doc |
El lienzo del documento: medida, fondo, ritmo entre bloques, selección, placeholder, caret | Vars CSS en la raíz del documento |
Entrada: schemeStyle acepta nombre registrado ('default', registerPalabrasScheme),
objeto PalabrasScheme inline, o string en notación compacta (§9 — se distingue
de un nombre porque contiene =). Un esquema parcial rellena desde el default.
2. Dimensiones transversales (todo elemento las admite)
El estilo de CUALQUIER elemento puede declarar:
| Grupo | Propiedades | Notas |
|---|---|---|
| Tipografía | font (familia) · size · weight · style (normal/italic) · lh (interlineado) · ls (tracking) · ws (word-spacing) · transform (none/upper/lower/capitalize) · wrap (wrap/nowrap/balance/pretty) · tabular (cifras tabulares) |
El mismo vocabulario que el mega-control Tipografía del panel — esquema y panel hablan igual |
| Color | color · bg |
Valores: hex/rgb/nombre CSS o @nombre de la paleta |
| Espaciado | margin · pad — 1/2/4 valores CSS-like (pad=12, pad=8 12, pad=8 12 4 12) o por lado (margin-top=24) |
px por defecto; admite unidades (1.5em) |
| Borde | border (3 solid #f00 o @acento) · por lado (border-left=…) · radius (1 o 4 valores) |
|
| Otros | shadow (token sm/md/lg o valor CSS) · opacity · measure (max-width del texto, en bloques de texto) |
3. Inventario por elemento (transversales + ESPECÍFICAS de cada uno)
| Elemento / sub-parte | Específicas además de §2 | Ejemplo (notación §7) |
|---|---|---|
doc (el lienzo) |
measure (ancho de línea del contenido, ej. 68ch) · gap (ritmo vertical entre bloques) · bg (fondo del documento) · pad (margen interior del lienzo) · selection (color/fondo del texto seleccionado) · caret (color) · placeholder (color/estilo del texto fantasma) |
doc{measure=68ch; gap=12; bg=#fff} |
text (párrafo base) |
indent (sangría de primera línea) |
text{font=Georgia; lh=1.6; color=@tinta} |
h1…h6 |
margin-top/margin-bottom propios (el ritmo de cada nivel) |
h1{weight=700; lh=1.2; margin-top=24} |
list |
indent (por nivel de anidado) · item-gap (espacio entre items) · marker (carácter/estilo del bullet) · marker-color |
list{indent=24; item-gap=4; marker-color=@tinta3} |
list.ordered |
numbering default (1/a/A/i/I) |
list.ordered{numbering=1} |
list.check |
box (tamaño del checkbox) · box-color · box-radius · checked-color · tachado del texto marcado sí/no |
list.check{box=16; box-color=@acento} |
quote |
border-left (la barra: grosor/estilo/color) — o el lado que sea · comillas decorativas sí/no |
quote{border-left=3 solid @acento; pad=12} |
quote.cite (atribución/fuente) |
tipografía y color propios | quote.cite{style=italic; size=14; color=@tinta3} |
code (bloque) |
tab-size · wrap (ajustar vs scroll) · lines (numeración de líneas on/off + color) |
code{font=Courier; bg=#f6f8fa; radius=6; wrap=off} |
code.tokens |
el tema del highlighter: color por token — keyword · string · number · comment · function · tag · attr · punct (los que emite el tokenizador) |
code.tokens{keyword=#cf222e; string=#0a3069; comment=#6e7781} |
callout (base) |
title como sub-parte (tipografía propia) · icono sí/no ⚠️ |
callout{radius=8; pad=12} |
callout.{intent} (×6: neutral/affirm/fulfill/risk/threat/loss) |
CADA intent declara su bg · border · color · title-color — aquí vive el mapa intent→píxeles |
callout.risk{bg=#fff1f0; border-left=3 solid #cf222e} |
image |
shadow · radius · border · bg (letterbox del fit) · fit default (fill/fit/crop/tile) · full-width default · los AJUSTES del componente ImageAdjustments del framework (el panel lo COMPONE — regla #1): brightness · contrast · saturation · temperature · hue · blur · grayscale · sepia — el esquema declara sus valores por defecto (el punto de partida del documento; el usuario ajusta por bloque encima) y editable gobierna cuáles se ofrecen (granular adjustments.*, §6) ⚠️ |
image{radius=8; shadow=sm; brightness=1.05; saturation=1.1} |
image.caption |
tipografía/color/align propios del pie | image.caption{size=13; color=@tinta3; align=center} |
table |
border exterior · border-inner (retícula interior) · radius |
table{border=1 solid #d0d7de; radius=6} |
table.cell |
pad · valign default |
table.cell{pad=8 10} |
table.header (fila Y columna de cabecera) |
bg · color · weight — su look propio |
table.header{bg=#f6f8fa; weight=600} |
table.zebra |
fondo alterno de filas (off si no se declara) | table.zebra{bg=#fafbfc} |
columns |
gap default · rule (regla divisoria: grosor/estilo/color, off por defecto) · col-pad |
columns{gap=24; rule=1 solid #eee} |
divider |
height (grosor) · style (solid/dashed/dotted) · color · gap (espacio vertical) · width (100% o menos, centrado) |
divider{height=1; color=#d0d7de; gap=24} |
link (inline) |
color · underline (grosor offset, o off) · hover-color · visited-color ⚠️ |
link{color=@acento; underline=1 2} |
mark.* (inline) |
mark.bold (el peso concreto) · mark.code (código inline: bg/color/radius/pad/tamaño relativo) · mark.highlight (fondo default del subrayador) · mark.underline/mark.strike (estilo/grosor/color) · mark.sub/mark.sup (tamaño/offset) |
mark.bold=700; mark.code{bg=#f6f8fa; radius=4} |
embed (y bloques de PLUGIN) |
clave abierta: cualquier bloque registrado puede recibir estilo por su type (transversales §2 + aspect para embed) |
embed{radius=8; aspect=16/9} |
⚠️ marcan las dudas concretas: icono de callout (¿lo gobierna el esquema o el chrome?),
visited/hover de links (¿v1 o sobra?).
4. fonts — las fuentes admitidas
fonts{
Georgia = Georgia, 'Times New Roman', serif;
Helvetica = 'Helvetica Neue', Arial, sans-serif @ 400,700; ← pesos disponibles
Courier = 'Courier New', monospace
}
- Cada entrada: label = stack CSS (+ opcional
@ pesos). En objeto: losPalabrasFontGroupde hoy (con grupos con título). - Proyectan al dropdown Familia; la primera es la base si
text{font}no dice otra. - El grupo «Tema» (var(--font-family-*)) DEJA de existir por defecto — doctrina backoffice. Una app puede añadirlo explícitamente si quiere.
5. palette — colores con nombre, referenciables
palette{tinta=#1f2328; tinta2=#57606a; acento=#0969da; papel=#ffffff; peligro=#cf222e}
- Proyecta a los swatches del picker (el nombre = tooltip/aria del swatch).
- Cualquier valor de color del esquema puede referenciar
@nombre— cambiaracentore-tinta todo lo que lo usa. (En objeto:palette: [{name, value}]; se admite el string pelado sin nombre.)
6. editable — qué se puede editar (gobierna el panel)
editable{
image = src, alt, adjustments.saturation, adjustments.blur; ← imagen: SOLO esto
heading = level, children, align;
paragraph = children, typography.size, color; ← granular: sub-claves con punto
divider = ← vacío: panel solo-lectura del tipo
}
- Lista blanca por tipo: tipo no declarado → panel completo (como hoy).
- Granularidad con punto:
typography.size,typography.tracking… apagan sub-controles individuales del mega-control Tipografía (typographya secas = todo él);adjustments.saturation,adjustments.blur… hacen lo mismo con los sliders del componenteImageAdjustments(adjustmentsa secas = los 8). - Tipo declarado VACÍO = nada editable (el panel muestra el tipo pero sin campos).
6b. marks + blocks — qué puede APLICAR/INSERTAR (gobiernan bubble y slash)
editable gobierna el PANEL. Las otras dos superficies tienen su propia lista blanca
top-level (misma doctrina «sin declarar = todo permitido»):
marks = bold, italic, underline, strike, code, sub, sup, fontSize, fontFamily;
blocks = paragraph, heading, list, quote; ← el slash / «+» solo ofrecen estos
marks— marcas inline que el usuario puede aplicar (bubble + atajos): booleanas (bold/italic/underline/strike/code/sub/sup) y el tamaño/familia paramétricos (fontSize/fontFamily). Sin declarar → todas; declarada VACÍA → ninguna. Los VALORES de familia/tamaño salen defonts/sizes. (El COLOR se gobierna aparte, víaeditable+ la paleta — no por esta lista.)blocks— tipos de bloque insertables (slash + menús «+»/Insertar), en vocabulario canónico (headingcubre h1–h6,listlas tres listas). Sin declarar → todos los registrados; declarada VACÍA → ninguno. El slash se filtra en el MOTOR (optinsertableBlocksdel provider) para que el índice de teclado no derive; los menús eidos filtran víaschemeAllowsBlocksobreinsertableBlockTypes(scheme).
7. El panel: las TRES clases de propiedades
Formalización (hoy es convención por consts compartidas; pasa a contrato):
| Clase | Tab | Qué es | Ejemplos |
|---|---|---|---|
| Específicas semánticas del elemento | Contenido | Qué ES y qué dice el bloque | level/texto/enlace (heading) · src/alt/caption (imagen) · intent/título (callout) · kind/start/numbering (lista) · cabeceras (tabla) · cite (cita) · language (código) |
| Comunes (transversales §2) | Diseño — primero, secciones idénticas en todos los tipos | El estilo que cualquier bloque admite | Disposición (alineación/márgenes/relleno) · Tipografía (el mega-control) · Colores (texto/fondo) |
| Específicas de diseño del elemento | Diseño — después, sección propia del tipo | El estilo que SOLO ese elemento tiene | «Imagen»: fit + los 8 ajustes (ImageAdjustments) + radius/sombra · «Tabla»: bordes/cabecera/zebra · «Separador»: grosor/estilo/color · «Columnas»: gap/regla |
| (Avanzadas) | Avanzado | Ancla + flags de export (spec de interacción L4) + extensiones (§8) | anchor · hideOnExport (cuando el motor la tenga) |
Reglas: las secciones comunes son LAS MISMAS consts para todos (nunca se duplican
por tipo); la sección específica de diseño lleva el NOMBRE del elemento; editable
(§6) y la notación (§9) respetan esta clasificación tal cual (las claves granulares
apuntan a campos de cualquiera de las tres clases).
8. Extensión de los paneles — quitar y añadir (programador y diseñador)
Dos actores, dos niveles, orden de aplicación fijo:
catálogo del REGISTRY (app) → −editable (esquema) → +fields (esquema)
8a. Nivel APP — el programador (código, boot)
// Panel COMPLETO de un tipo nuevo (p. ej. el bloque embed, que es plugin):
registerPalabrasPanel({ type: 'embed', label: '…', contenido: […], diseño: […] });
// Modificar el panel de un tipo existente:
extendPalabrasPanel('heading', {
contenido: {
add: [{ key: 'cta', type: 'text', label: '…' }], // añadir campo…
after: 'level', // …con posición (o append)
remove: ['link'] // quitar del catálogo de la app
},
diseño: { addSection: { title: '…', fields: […] } },
avanzado: { add: […] }
});
- Mismo patrón que los plugins de bloque (
defaultPalabrasSchema.register) y los esquemas (registerPalabrasScheme): registro en boot, sin tocar el core. removea nivel app ≠editabledel esquema: el programador define el CATÁLOGO de su app (global); el diseñador restringe POR DOCUMENTO.- ⚠️ Field-types custom (registrar un CONTROL nuevo — componente Svelte —
bajo un
typepropio, para campos que los 12 tipos básicos no cubren): propuesto para v2, la puerta queda declarada.
8b. Nivel DOCUMENTO — el diseñador (el esquema, sin código)
- Quitar:
editable(§6) — lista blanca por tipo, granular. - Añadir:
fields— campos declarativos con los tipos básicos (text/textarea/number/toggle/select/color/slider/length), sobre una prop libre del bloque. El motor YA persiste props desconocidas (updateBlockAtPathconserva y exporta), así que un campo declarado en el esquema funciona end-to-end sin una línea de código:
heading.fields{
cta{type=text; label=Texto del CTA; tab=avanzado};
urgente{type=toggle; label=Urgente; tab=contenido}
}
tab= contenido/diseño/avanzado (default avanzado);optionspara select; min/max/step para number/slider. En objeto:scheme.fields = { heading: [PalabrasFieldDef…] }.
9. La notación compacta (gramática) — mapeo 1:1 con el objeto
esquema := decl (';' decl)*
decl := escala | bloque | asignación
escala := elemento '=' número → sizes.{elemento} (h1=32)
bloque := ruta '{' (prop | bloque) (';' …)* '}' → styles.{ruta} (code{padding=4})
prop := nombre '=' valor — nombres cortos de §2/§3
ruta := elemento ('.' subparte)* — callout.risk · code.tokens · mark.bold
asignación := ruta '=' valor — atajo de un solo valor (mark.bold=700)
valor := número | dimensión | color | '@'paleta | ident | valor ' ' valor
;separa; los saltos de línea son equivalentes a;. Espacios libres.- Números pelados = px (salvo props adimensionales:
lh,weight,opacity). fonts{…}palette{…}editable{…}doc{…}son bloques con las formas de §4–§6.- El parser emite EXACTAMENTE un
PalabrasScheme— todo lo expresable en notación es expresable en objeto y viceversa. Errores de parse: se ignora la declaración inválida y se avisa por consola dev (el documento nunca revienta por un typo).
Ejemplo completo (el «default» expresado en notación)
palette{tinta=#1f2328; tinta2=#57606a; tinta3=#8c959f; acento=#0969da; papel=#ffffff}
fonts{Georgia=Georgia,'Times New Roman',serif; Helvetica='Helvetica Neue',Arial,sans-serif; Courier='Courier New',monospace}
doc{measure=68ch; gap=12; bg=@papel}
text=17; h1=32; h2=26; h3=22; h4=19; h5=17; h6=16; quote=18; code=14
text{font=Georgia; lh=1.6; color=@tinta}
h1{weight=700; lh=1.2} h2{weight=700; lh=1.25} h3{weight=600; lh=1.3}
quote{color=@tinta2; border-left=3 solid #d0d7de; pad=12}
quote.cite{style=italic; size=14; color=@tinta3}
code{font=Courier; bg=#f6f8fa; radius=6; pad=12}
code.tokens{keyword=#cf222e; string=#0a3069; comment=#6e7781; number=#0550ae}
callout{radius=8; pad=12}
callout.risk{bg=#fff1f0; border-left=3 solid #cf222e}
callout.fulfill{bg=#dafbe1; border-left=3 solid #1a7f37}
list{indent=24; item-gap=4}
image{radius=6} image.caption{size=13; color=@tinta3; align=center}
table{border=1 solid #d0d7de} table.cell{pad=8 10} table.header{bg=#f6f8fa; weight=600}
divider{height=1; color=#d0d7de; gap=24}
link{color=@acento; underline=1 2}
mark.bold=700 mark.code{bg=#f6f8fa; radius=4}
10. Qué genera cada pieza (proyección)
| Pieza | Proyección |
|---|---|
styles.{el} transversales |
Vars --palabras-scheme-{el}-{prop} en el doc root + regla por elemento en palabras.css — TODAS las props declaradas, no un subconjunto |
styles de sub-partes/variantes |
Selectores existentes del render: callout.{intent} → [data-palabras-callout-intent='…'] · code.tokens.* → los spans de token del highlighter · table.header → th · image.caption → figcaption · mark.* → los tags/spans de marks |
doc |
Vars en la raíz: measure → max-width del content; gap → margen entre bloques; selection → ::selection; placeholder/caret → sus reglas |
sizes |
--palabras-scheme-{el}-size (+ el valor «heredado» del panel) |
palette |
Presets del picker + resolución de @nombre en el parser/resolver (a valor concreto en las vars) |
fonts |
Dropdown Familia (grupos) + primera fuente = base implícita |
editable |
Filtrado de campos/secciones del panel + sub-controles de Tipografía y de ImageAdjustments (adjustments.*) |
styles.image (ajustes) |
Defaults de los 8 sliders de ImageAdjustments en el panel + el filter CSS base del elemento imagen (el override por bloque compone encima) |
Los overrides por bloque (block.visual) SIEMPRE ganan al esquema; el «↺» los
limpia y el bloque vuelve al esquema. Cambiar de esquema re-tinta todo lo no-overrideado.
11. Estado actual vs esta spec (honesto)
Ya existe (base de 30a67533): el tipo con fonts/sizes/palette/editable/styles,
nombre-u-objeto con relleno parcial, registro, doctrina backoffice (aislamiento del
host), proyección de text/h1–h6/quote/code/callout-base, paleta→picker, whitelist
de panel (sin granularidad), muerte del proxy getComputedStyle.
Falta TODO lo demás de esta spec: la notación compacta (el parser), doc (medida/
ritmo/selección/placeholder/caret), paleta con nombre + @referencias, granularidad
de editable (sub-controles), y los estilos de list (+ordered/check), quote.cite,
code.tokens, callout por intent, image(+caption), table (+cell/header/zebra),
columns(+rule), divider, link, marks y bloques de plugin.
12. Ratificación (lo que es tuyo)
- El inventario §3: tacha dimensiones que sobren, añade las que falten.
- Los ⚠️: icono del callout en el esquema sí/no ·
hover/visitedde link v1 sí/no. - ⚠️ Ajustes de imagen — mapeo motor↔componente: el componente canónico
(
ImageAdjustments) hablasaturation / hue / temperature; el modelo del MOTOR (ImageBlock) hoy tienesaturate / hueRotatey no tienetemperature. Propuesta: el panel COMPONEImageAdjustments(vocabulario del componente), mapeado a las props existentes del modelo, ytemperaturese AÑADE al modelo del motor (validador + render con su aproximación CSS) para paridad completa. ¿OK, o temperature fuera del modelo en v1? - Las tres clases del panel (§7): la asignación Contenido/Diseño-comunes/ Diseño-específicas/Avanzado — ¿así?
- Extensión de paneles (§8):
registerPalabrasPanel+extendPalabrasPanel(nivel app) yfieldsdeclarativos del esquema (nivel documento, sin código) — ¿ambos niveles sí? · ⚠️ ¿la posiciónafter/beforeal añadir importa, o basta append? · ⚠️ field-types CUSTOM (control propio como componente) → propuesto v2. - Los nombres cortos de la notación (§2:
lh/ls/ws/pad/bg…) — ¿te valen así? @nombrepara referenciar la paleta — ¿sí?- Con tu firma: se implementa TODO (tipo ampliado + parser + proyección completa +
las tres clases + el mecanismo de extensión + granularidad del panel — incluido
componer
ImageAdjustmentsen el panel de imagen), con la demo ejercitando cada pieza y verificación por valores.