# Icon
## Estado
`Icon` es un componente leaf visual de Eidos. No tiene contraparte Soma ni
Morfo porque no gestiona estado, ARIA relacional, teclado, foco ni eventos
perceptivos. Su contrato es SVG + tokens visuales.
Superficie publica:
- `Icon` default para SVGs custom.
- 1696 glyphs Lucide exportados como componentes Svelte.
- `IntentIcon` como mapa canonico `Intent -> glyph`.
- `resolveIconSize()` y `resolveIconStrokeWidth()` para tests, tooling y
generacion.
## Baseline
Air ya tenia un modulo `icon` completo:
| Capacidad | Air | Eidos |
| --- | --- | --- |
| SVG wrapper custom | Si, `Root` | Si, default `Icon` |
| Glyphs Lucide locales | Si | Si |
| Size canonico `xxs..xxl` | Si | Si |
| Stroke `auto/sm/md/lg` | Si | Si |
| `title` + `decorative` | Si | Si |
| Clase base | `.air-icon` scoped | `[data-icon]` recipe |
| Tokens | `--air-icon-*` | `--icon-*` |
| Intent glyphs | No | Si, `IntentIcon` |
| `absoluteStrokeWidth` | No | Si |
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
| Plataforma | Modelo | Eidos |
| --- | --- | --- |
| Lucide Svelte | Iconos standalone, props de size/color/stroke, tree-shaking, TS, accessibility y global styling. | Mismo modelo de componentes standalone, con tokens Eidos y `absoluteStrokeWidth`. |
| Radix Icons | Set React de iconos 15x15, imports individuales. | Mas flexible en size/stroke y disponible como componentes Svelte locales. |
| shadcn-svelte | No define una libreria de iconos propia; usa Lucide en los componentes y exige `aria-label` para botones icon-only. | Mismo criterio: el icono es visual; la accesibilidad del control icon-only vive en el componente que lo contiene. |
| unplugin-icons/Iconify | Iconos on-demand de muchos packs via plugin de build. | No es objetivo de Eidos competir en catalogo dinamico multi-pack; Eidos prioriza glyphs locales, tipados y themeables. |
Fuentes:
- [Lucide](https://lucide.dev/)
- [Lucide for Svelte](https://lucide.dev/guide/svelte)
- [Radix Icons](https://www.radix-ui.com/icons)
- [shadcn-svelte Button](https://www.shadcn-svelte.com/docs/components/button)
- [unplugin-icons](https://unplugin.unjs.io/showcase/unplugin-icons)
## Decisiones
### Size y stroke
Los tamanos canonicos (`xxs`, `xs`, `sm`, `md`, `lg`, `xl`, `xxl`) se
resuelven a tokens `--icon-size-*`. El default visual usa `--icon-size` y
`--icon-stroke-width`, de modo que el theme puede cambiar el icono base sin
tocar cada consumidor.
`absoluteStrokeWidth` sigue el patron de Lucide: cuando el size es numerico,
px o canonico, ajusta el `stroke-width` para mantener grosor visual estable al
escalar el SVG.
### Color
El wrapper usa `stroke="currentColor"` y `color: inherit`. El color pertenece al
contexto visual que contiene el icono, no al glyph. Si un consumidor necesita
color puntual, puede pasarlo como atributo SVG/CSS normal.
### Accesibilidad
El default es `decorative=true`, por tanto se renderiza `aria-hidden="true"`.
Cuando `decorative=false`, se renderiza `role="img"` y se puede aportar
`title` o atributos ARIA via passthrough. En botones icon-only, el label
accesible pertenece al boton, no al icono.
### IntentIcon
`IntentIcon` no sustituye a los glyphs. Es una ayuda doctrinal para superficies
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`.
## Passive justification
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. |