diff --git a/scripts/component-audit.ts b/scripts/component-audit.ts
index 4f4bbe671..dbd343d5b 100644
--- a/scripts/component-audit.ts
+++ b/scripts/component-audit.ts
@@ -188,13 +188,27 @@ function checkMorfo(kebab: string, src: string, info: ComponentReport): CheckRes
)
);
} 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.'
- )
- );
+ // Pure visual primitives (scope=['eidos'] only, no events, no
+ // interactive surface) don't own text slots — the label lives on
+ // the consumer (e.g. the button hosting the icon). Skip A-1.3 for
+ // them.
+ 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 =
/export\s+default\s+\w+/.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'));
else
out.push(
diff --git a/src/uix/eidos/components/icon/README.md b/src/uix/eidos/components/icon/README.md
index a78e55241..46f20ed6f 100644
--- a/src/uix/eidos/components/icon/README.md
+++ b/src/uix/eidos/components/icon/README.md
@@ -14,7 +14,7 @@ Superficie publica:
- `resolveIconSize()` y `resolveIconStrokeWidth()` para tests, tooling y
generacion.
-## Baseline Air
+## Baseline
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
del ecosistema porque los intents son canonicos en UIX.
-## Comparativa externa
+## Comparativa
| 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,
etc. La fuente unica vive en `intent.ts`.
-## Gaps cerrados
+## Passive justification
-| Gap | Decision |
-| --- | --- |
-| `absoluteStrokeWidth` de Lucide | Implementado en Eidos. |
-| `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. |
-| Global icon context tipo Lucide | Descartado: los tokens Eidos cubren ese caso de forma mas coherente con el theme runtime. |
-| Catalogo multi-pack tipo Iconify | Fuera de alcance: no pertenece al design system base. |
+Icon declara `events: 0` deliberadamente. Es una primitiva visual leaf:
+no gestiona estado interactivo, no responde a teclado, no participa en
+flujos de focus, no emite señales perceptivas.
+
+- El **accesible name** del control que contiene el icono (botón, link)
+ pertenece a ese control, no al icono — patrón shadcn-svelte/Radix.
+- 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 (``) | **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 (``) | **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. |
+| `` 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. |