icon: README sections + Passive justification + audit refinements

Full walk of icon (passive eidos-only primitive):

- Morfo: validated correct as-is. Single `Provider` part with
  `archetype: 'image'`, conditional `aria-hidden` / `role="img"` driven
  by the `decorative` prop. Scope `['eidos']` is right (no soma
  counterpart). 0 events is right (leaf visual primitive).
- Eidos: 1696 lucide glyphs + IntentIcon. Default exported via the
  re-export pattern `export { default } from './icon.svelte';` —
  validates the audit refinement below.
- README sections renamed to canonical headers: `## Baseline Air` →
  `## Baseline`, `## Comparativa externa` → `## Comparativa`. New
  `## Passive justification` section explains why icon ships zero
  semantic events (leaf primitive — the accessible name and any
  perceptual signal belong to the containing control). `## Gaps
  cerrados` renamed to `## Gaps` and rewritten with disposition
  tokens (implementar / diferir / descartar).

Audit script refined for two false-positives surfaced by icon:

- A-1.3 (texts.label) now SKIPS pure visual primitives — components
  whose `scope` is exactly `['eidos']` AND that declare no events.
  Icons don't own text slots; the label lives on the consumer (the
  button hosting the icon, shadcn-svelte/Radix pattern). Reported as
  pass with "eidos-only passive primitive — no text slot".
- E-1.3 (default export) now recognizes the re-export form
  `export { default } from './foo.svelte';` in addition to the
  existing patterns (`export default X`, `export { X as default }`).

Audit: PASS 5 → 6. Icon flips to PASS. No regressions; the relaxed
rules don't change verdicts for any other component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent cb3dc8a520
commit 98d233310d

@ -188,13 +188,27 @@ function checkMorfo(kebab: string, src: string, info: ComponentReport): CheckRes
) )
); );
} else { } else {
out.push( // Pure visual primitives (scope=['eidos'] only, no events, no
fail( // interactive surface) don't own text slots — the label lives on
'A-1.3', // the consumer (e.g. the button hosting the icon). Skip A-1.3 for
'error', // them.
'No `texts.label` declared. Add `texts: { label: \"#?components.{kebab}.label|Fallback\" }` and ship the entry in src/uix/langs/components/{kebab}.ts.' const scopeMatch = src.match(/scope:\s*\[([^\]]+)\]/);
) const scopes = scopeMatch
); ? scopeMatch[1].split(',').map((s) => s.trim().replace(/['"]/g, ''))
: [];
const isEidosOnlyLeaf =
scopes.length === 1 && scopes[0] === 'eidos' && !/events:\s*\[/.test(src);
if (isEidosOnlyLeaf) {
out.push(pass('A-1.3', 'error', 'eidos-only passive primitive — no text slot'));
} else {
out.push(
fail(
'A-1.3',
'error',
'No `texts.label` declared. Add `texts: { label: \"#?components.{kebab}.label|Fallback\" }` and ship the entry in src/uix/langs/components/{kebab}.ts.'
)
);
}
} }
} }
@ -461,7 +475,9 @@ function checkEidos(kebab: string, info: ComponentReport): CheckResult[] {
const defaultExport = const defaultExport =
/export\s+default\s+\w+/.test(idx) || /export\s+default\s+\w+/.test(idx) ||
/export\s+default\s+function/.test(idx) || /export\s+default\s+function/.test(idx) ||
/export\s*\{\s*[A-Z]\w*\s+as\s+default\b/.test(idx); /export\s*\{\s*[A-Z]\w*\s+as\s+default\b/.test(idx) ||
// Re-exporting the default of another module: `export { default } from './x.svelte';`
/export\s*\{\s*default\s*\}\s*from/.test(idx);
if (namedExport && defaultExport) out.push(pass('E-1.3', 'error')); if (namedExport && defaultExport) out.push(pass('E-1.3', 'error'));
else else
out.push( out.push(

@ -14,7 +14,7 @@ Superficie publica:
- `resolveIconSize()` y `resolveIconStrokeWidth()` para tests, tooling y - `resolveIconSize()` y `resolveIconStrokeWidth()` para tests, tooling y
generacion. generacion.
## Baseline Air ## Baseline
Air ya tenia un modulo `icon` completo: Air ya tenia un modulo `icon` completo:
@ -34,7 +34,7 @@ Decision: Eidos conserva la ergonomia de Air, elimina el prefijo de capa y
centraliza la visual en tokens/recipes. `IntentIcon` es una extension propia centraliza la visual en tokens/recipes. `IntentIcon` es una extension propia
del ecosistema porque los intents son canonicos en UIX. del ecosistema porque los intents son canonicos en UIX.
## Comparativa externa ## Comparativa
| Plataforma | Modelo | Eidos | | Plataforma | Modelo | Eidos |
| --- | --- | --- | | --- | --- | --- |
@ -84,12 +84,32 @@ que deben expresar un `Intent` canonico (`neutral`, `affirm`, `fulfill`,
`risk`, `threat`, `loss`) sin duplicar tablas en Toast, Field, Banner, Dialog, `risk`, `threat`, `loss`) sin duplicar tablas en Toast, Field, Banner, Dialog,
etc. La fuente unica vive en `intent.ts`. etc. La fuente unica vive en `intent.ts`.
## Gaps cerrados ## Passive justification
| Gap | Decision | Icon declara `events: 0` deliberadamente. Es una primitiva visual leaf:
| --- | --- | no gestiona estado interactivo, no responde a teclado, no participa en
| `absoluteStrokeWidth` de Lucide | Implementado en Eidos. | flujos de focus, no emite señales perceptivas.
| `FolderProvider` generado erroneamente | Corregido a `FolderRoot`. |
| Dynamic icon by string | Diferido: solo se justifica si hay consumer real; puede resolverse con mapa explicito o tooling sin convertir `Icon` en service locator. | - El **accesible name** del control que contiene el icono (botón, link)
| Global icon context tipo Lucide | Descartado: los tokens Eidos cubren ese caso de forma mas coherente con el theme runtime. | pertenece a ese control, no al icono — patrón shadcn-svelte/Radix.
| Catalogo multi-pack tipo Iconify | Fuera de alcance: no pertenece al design system base. | - La selección de glyph es estática (import al componente que lo usa);
no hay un evento "icon changed" porque el cambio es del consumer.
- El feedback perceptivo de "el icono cambió" se emite desde el
componente que lo orquesta (Toast cambia de glyph al cambiar de
intent → Toast emite el sema event, no Icon).
Si una superficie necesita semántica al cambiar de icono, ese evento
vive en el componente padre (Toast, Field, Banner, Dialog), no en
Icon. Icon se mantiene pasivo para no duplicar responsabilidades.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `absoluteStrokeWidth` de Lucide | **implementar** | ✓ Ya implementado en Eidos. |
| `FolderProvider` generado erroneamente | **implementar** | ✓ Corregido a `FolderRoot`. |
| Dynamic icon by string (`<Icon name="check" />`) | **diferir** | Sólo se justifica si hay consumer real. Resoluble con mapa explícito o tooling sin convertir `Icon` en service locator. |
| Global icon context tipo Lucide (`<IconContext>`) | **descartar** | Los tokens Eidos (`--icon-size`, `--icon-stroke-width`) cubren ese caso de forma más coherente con el theme runtime. |
| Catálogo multi-pack tipo Iconify | **descartar** | Fuera de alcance: no pertenece al design system base. Catálogo dinámico multi-pack pertenece a tooling de build aparte. |
| `<Icon name="check" />` con autocompletado tipado | **diferir** | Tipo union de los 1696 nombres es factible (`IconName`) pero infla `.d.ts`. Sin caso de uso real. |
| Iconos animados (spin/pulse) propios | **diferir** | Estado vive en CSS del consumidor (botón con `data-loading`). Mientras un caso no pida glyph animado autocontenido, no se construye. |

Loading…
Cancel
Save

Powered by TurnKey Linux.