refactor(morfo): los 25 knobs visuales salen del contrato — comentados, no borrados

Morfo es el contrato ENTRE capas, y un knob de pintura lo lee UNA: el CSS del
propio componente. La doctrina lo dice en tres sitios, y ninguno lo escribi yo:

  · morfo.md §«What morfo does NOT contain» → «Visual variants / recipes →
    src/uix/eidos/ — Layer-specific, not shared»
  · morfo.md §Backlog 2026-08-13 → `undeclaredState` es «the escape hatch for
    attrs OUTSIDE the contract — a visual wrapper's `data-size`, a presentation
    flag like `data-sheet`», y LANZA si el attr resulta estar declarado
  · theming/reference.md §39 → la familia `data-variant`/`data-size` es un
    wrapper attr eidos-only; regla de pulgar: morfo declara un attr solo cuando
    su prop cruza la frontera de SOMA

Ninguno de los 25 cruza: los seis componentes no tienen capa soma.

  avatar        9  size · variant x2 · color x2 · radius · position · dot · ring
  s-text        6  style · truncate · clamp · canvas · italic · underline
  image         5  fit · position · radius · size · placeholder
  avatar-group  2  size · stacking
  skeleton      2  shape · animated
  qr-code       1  cell-shape

COMENTADOS, NO BORRADOS, por orden del autor, y cada bloque lleva su porque con
las tres citas. Dos motivos que valen la pena: son la huella de lo que el morfo
prometia, y son el enum que `eidos-lint` podra volver a leer el dia que sepa
tomarlo de la union TS (`AvatarSize = Extract<Size, …>`), que es donde vive de
verdad. Hasta entonces esos 25 enums pierden la comprobacion del CSS — el codigo
sigue tipado.

LO QUE SE QUEDA, y por que: los `data-status` de `avatar` (x3) e `image` (x4) los
produce `ImageProvider` de `$soma/layers` — `avatar.svelte:24`,
`avatar-image.svelte:3` — asi que su driver SI cruza, que es exactamente la
condicion que nombra la regla de pulgar.

FUERA DEL BARRIDO, y tampoco son knobs: `separator` y `button-group` declaran
`data-orientation` junto a un `aria-orientation` del MISMO prop (es el espejo CSS
de una propiedad ARIA que el morfo ya posee); `chart` usa `data-chart-axis` para
discriminar dos instancias de la misma parte (identidad estructural); y
`barcode` usa `data-symbology` para decir que estandar codifica el simbolo.

Medido: los seis morfos parsean, `eidos-lint` invalid 0 en todos, y el
desplazamiento esperado — avatar pasa de 24 morfo-backed a 7 y de 46 a 63
eidos-only. morfo:check PASS en los seis (nunca los miro: son attrs desnudos,
no `data-{kebab}-*`). component:audit los seis PASS. check 69 = base intacta.
morfo:vocabulary OK. smoke 311/311.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent b72a564918
commit 998d72191e

@ -35,17 +35,33 @@ export const avatarGroupMorfo = {
role: 'group',
optional: false,
data: [
{
attr: 'data-stacking',
values: ['side-by-side', 'overlap', 'overlap-reverse'],
value: v.propRef('stacking')
},
{
attr: 'data-size',
values: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
value: v.propRef('size'),
severity: 'optional'
}
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live.
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary. These cross nothing:
// this component has no soma layer.
//
// {
// attr: 'data-stacking',
// values: ['side-by-side', 'overlap', 'overlap-reverse'],
// value: v.propRef('stacking')
// },
// {
// attr: 'data-size',
// values: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
// value: v.propRef('size'),
// severity: 'optional'
// }
],
aria: []
},

@ -14,62 +14,83 @@ export const avatarMorfo = {
defaultElement: 'span',
optional: false,
data: [
{
attr: 'data-size',
values: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
value: v.propRef('size')
},
{
attr: 'data-variant',
values: ['solid', 'soft', 'outline'],
value: v.propRef('variant')
},
{
// Optional severity acknowledges the documented escape
// hatch: the wrapper accepts any CSS color string
// (hex, oklch, rgb, etc.) and routes non-canonical
// values through `data-color-custom` + a
// `--avatar-color-custom` inline CSS variable instead.
// When the consumer passes a canonical role
// (`primary`, `affirm`, etc.) the enum value lands
// here; when they pass a custom color, this attr is
// absent by design (the recipe knows to fall through
// to the custom path). Symmetric with `Avatar.Badge`,
// which already declared this attr as optional.
attr: 'data-color',
values: [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
],
value: v.propRef('color'),
severity: 'optional'
},
{
attr: 'data-radius',
values: ['full', 'md', 'sm', 'none'],
value: v.propRef('radius')
},
{
attr: 'data-status',
values: ['idle', 'loading', 'loaded', 'error'],
value: v.propRef('status')
},
{
// Ring (visible outline halo). Values match the prop:
// 'solid' → filled stroke,
// 'soft' → soft halo via box-shadow + color-mix.
attr: 'data-ring',
values: ['solid', 'soft'],
value: v.propRef('ring'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'ring' }
}
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live
// (`AvatarSize`, `AvatarVariant`, `AvatarBadgePosition`, …).
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary.
//
// `data-status` STAYS (here and on Image / Fallback): it is produced by
// `ImageProvider` from `$soma/layers` — see `avatar.svelte:24` and
// `avatar-image.svelte:3` — so its driver DOES cross the boundary, the
// exact condition the thumb rule names.
//
// {
// attr: 'data-size',
// values: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
// value: v.propRef('size')
// },
// {
// attr: 'data-variant',
// values: ['solid', 'soft', 'outline'],
// value: v.propRef('variant')
// },
// {
// // Optional severity acknowledges the documented escape
// // hatch: the wrapper accepts any CSS color string
// // (hex, oklch, rgb, etc.) and routes non-canonical
// // values through `data-color-custom` + a
// // `--avatar-color-custom` inline CSS variable instead.
// // When the consumer passes a canonical role
// // (`primary`, `affirm`, etc.) the enum value lands
// // here; when they pass a custom color, this attr is
// // absent by design (the recipe knows to fall through
// // to the custom path). Symmetric with `Avatar.Badge`,
// // which already declared this attr as optional.
// attr: 'data-color',
// values: [
// 'primary',
// 'secondary',
// 'neutral',
// 'affirm',
// 'fulfill',
// 'risk',
// 'threat',
// 'loss'
// ],
// value: v.propRef('color'),
// severity: 'optional'
// },
// {
// attr: 'data-radius',
// values: ['full', 'md', 'sm', 'none'],
// value: v.propRef('radius')
// },
// {
// // Ring (visible outline halo). Values match the prop:
// // 'solid' → filled stroke,
// // 'soft' → soft halo via box-shadow + color-mix.
// attr: 'data-ring',
// values: ['solid', 'soft'],
// value: v.propRef('ring'),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'ring' }
// }
],
aria: []
},
@ -118,37 +139,53 @@ export const avatarMorfo = {
defaultElement: 'span',
optional: true,
data: [
{
attr: 'data-position',
values: ['top-start', 'top-end', 'bottom-start', 'bottom-end'],
value: v.propRef('position')
},
{
attr: 'data-variant',
values: ['soft', 'solid', 'outline'],
value: v.propRef('variant')
},
{
attr: 'data-color',
values: [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
],
value: v.propRef('color'),
severity: 'optional'
},
{
attr: 'data-dot',
value: v.literal(''),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'dot' }
}
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live
// (`AvatarSize`, `AvatarVariant`, `AvatarBadgePosition`, …).
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary.
//
// {
// attr: 'data-position',
// values: ['top-start', 'top-end', 'bottom-start', 'bottom-end'],
// value: v.propRef('position')
// },
// {
// attr: 'data-variant',
// values: ['soft', 'solid', 'outline'],
// value: v.propRef('variant')
// },
// {
// attr: 'data-color',
// values: [
// 'primary',
// 'secondary',
// 'neutral',
// 'affirm',
// 'fulfill',
// 'risk',
// 'threat',
// 'loss'
// ],
// value: v.propRef('color'),
// severity: 'optional'
// },
// {
// attr: 'data-dot',
// value: v.literal(''),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'dot' }
// }
],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}

@ -56,37 +56,57 @@ export const imageMorfo = {
attr: 'data-status',
values: ['idle', 'loading', 'loaded', 'error'],
value: v.stateRef('idle')
},
{
attr: 'data-fit',
values: ['cover', 'contain', 'fill', 'none', 'scale-down'],
value: v.propRef('fit'),
severity: 'optional'
},
{
attr: 'data-position',
values: ['center', 'top', 'bottom', 'start', 'end'],
value: v.propRef('position'),
severity: 'optional'
},
{
attr: 'data-radius',
values: ['none', 'sm', 'md', 'lg', 'full'],
value: v.propRef('radius'),
severity: 'optional'
},
{
attr: 'data-size',
values: ['xs', 'sm', 'md', 'lg', 'xl'],
value: v.propRef('size'),
severity: 'optional'
},
{
attr: 'data-placeholder',
values: ['skeleton', 'color', 'none'],
value: v.propRef('placeholder'),
severity: 'optional'
}
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live.
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary. These cross nothing:
// this component has no soma layer.
//
// `data-status` STAYS: it is produced by `ImageProvider` from
// `$soma/layers`, so its driver does cross the boundary — the exact
// condition the thumb rule names.
//
// {
// attr: 'data-fit',
// values: ['cover', 'contain', 'fill', 'none', 'scale-down'],
// value: v.propRef('fit'),
// severity: 'optional'
// },
// {
// attr: 'data-position',
// values: ['center', 'top', 'bottom', 'start', 'end'],
// value: v.propRef('position'),
// severity: 'optional'
// },
// {
// attr: 'data-radius',
// values: ['none', 'sm', 'md', 'lg', 'full'],
// value: v.propRef('radius'),
// severity: 'optional'
// },
// {
// attr: 'data-size',
// values: ['xs', 'sm', 'md', 'lg', 'xl'],
// value: v.propRef('size'),
// severity: 'optional'
// },
// {
// attr: 'data-placeholder',
// values: ['skeleton', 'color', 'none'],
// value: v.propRef('placeholder'),
// severity: 'optional'
// }
],
aria: []
},

@ -35,7 +35,23 @@ export const qrCodeMorfo = {
role: 'img',
optional: false,
data: [
{ attr: 'data-cell-shape', values: ['square', 'rounded', 'dots'] }
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live.
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary. These cross nothing:
// this component has no soma layer.
//
// { attr: 'data-cell-shape', values: ['square', 'rounded', 'dots'] }
],
aria: [
{

@ -37,42 +37,58 @@ export const sTextMorfo = {
defaultElement: 'span',
optional: false,
data: [
{
attr: 'data-style',
values: ['body', 'prose', 'label', 'caption'],
value: v.propRef('style'),
severity: 'optional'
},
{
attr: 'data-truncate',
value: v.literal(''),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'truncate' }
},
{
attr: 'data-clamp',
value: v.literal(''),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'clamp' }
},
{
attr: 'data-canvas',
value: v.literal(''),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'text' }
},
{
attr: 'data-italic',
value: v.literal(''),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'italic' }
},
{
attr: 'data-underline',
value: v.literal(''),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'underline' }
}
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live.
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary. These cross nothing:
// this component has no soma layer.
//
// {
// attr: 'data-style',
// values: ['body', 'prose', 'label', 'caption'],
// value: v.propRef('style'),
// severity: 'optional'
// },
// {
// attr: 'data-truncate',
// value: v.literal(''),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'truncate' }
// },
// {
// attr: 'data-clamp',
// value: v.literal(''),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'clamp' }
// },
// {
// attr: 'data-canvas',
// value: v.literal(''),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'text' }
// },
// {
// attr: 'data-italic',
// value: v.literal(''),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'italic' }
// },
// {
// attr: 'data-underline',
// value: v.literal(''),
// severity: 'optional',
// condition: { when: 'prop-truthy', prop: 'underline' }
// }
],
aria: []
},

@ -31,8 +31,24 @@ export const skeletonMorfo = {
role: 'status',
optional: false,
data: [
{ attr: 'data-shape', value: v.propRef('shape'), severity: 'optional' },
{ attr: 'data-animated', value: v.propRef('animated'), severity: 'optional' }
// ── Visual wrapper knobs — OUT of the contract (2026-08-15) ────────
// Commented, not deleted: the record of what this morfo used to promise,
// and of the enums eidos-lint could read again the day it takes them from
// the TS unions instead — which is where they actually live.
//
// Morfo is the CROSS-LAYER contract, and a paint knob is read by ONE
// layer: this component's own CSS.
// · morfo.md §"What morfo does NOT contain" → "Visual variants /
// recipes → src/uix/eidos/ — Layer-specific, not shared"
// · morfo.md §Backlog 2026-08-13 → `undeclaredState` is "the escape
// hatch for attrs OUTSIDE the contract — a visual wrapper's
// `data-size`, a presentation flag like `data-sheet`"
// · theming/reference.md §39 → morfo declares an attr only when its
// driving prop crosses the SOMA boundary. These cross nothing:
// this component has no soma layer.
//
// { attr: 'data-shape', value: v.propRef('shape'), severity: 'optional' },
// { attr: 'data-animated', value: v.propRef('animated'), severity: 'optional' }
],
aria: [
{

Loading…
Cancel
Save

Powered by TurnKey Linux.