# 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. |