# Icons The library ships a small, curated set of [Phosphor](https://phosphoricons.com) icons as a **tree-shakeable subpath export**. Library chrome (disclosure carets, close ×, the modified-tab dot, scroll/step arrows) draws from this set, and you can import any of them for your own buttons. ```tsx import { Copy, Lock, Trash } from '@dolphinspired/solid-stage/icons'; ``` Each icon is a `Component<{ size?; class? }>` carrying a `stage-icon stage-icon-` class, so it sizes off `--stage-icon-size` and inherits `currentColor`. Phosphor glyphs are **filled vector shapes** (not strokes), so they stay crisp at small chrome sizes. Unused icons drop via tree-shaking. ## The `IconComponent` contract Anywhere the library takes an icon — `IconButton`, `IconToggle`, `Section`'s `headerActions`, `IconButtonField`, etc. — the prop type is the **empty-props component shape**: ```ts type IconComponent = Component; // = (props: {}) => JSX.Element ``` i.e. *any component renderable with no required props*. The library never passes props to an icon — it's **sized and coloured entirely in CSS** via the `.stage-icon` class (`--stage-icon-size` / `--stage-color-icon`). The contract is deliberately this loose so every common icon shape satisfies it: 1. **A bundled icon** — `import { Copy } from '@dolphinspired/solid-stage/icons'` (no extra dependency; already carries `.stage-icon`). 2. **An Iconify pack via `unplugin-icons`** — components typed `Component` (see the recipe below). 3. **Your own icon component** — `lucide-solid`, a hand-rolled SVG, anything. ```tsx import { Heart } from 'lucide-solid'; ``` > An earlier contract was `Component<{ size? }>`, which *rejected* SVG-attribute > icon components (Iconify/unplugin) — their props share no keys with `{ size? }`. > Sizing lives in CSS regardless, so the contract dropped the `size` prop. ## "Any icon by name" — Iconify via `unplugin-icons` For the "I just want any Phosphor icon, by name, without hand-maintaining an adapter" case, wire [`unplugin-icons`](https://github.com/unplugin/unplugin-icons) into your app's build. Tree-shaken SVG (only the icons you import ship), any [Iconify](https://icon-sets.iconify.design) pack, themed to match the bundled set. The library ships a pre-configured preset, **`stageIcons()`** (the `@dolphinspired/solid-stage/vite` entry), so you don't hardcode the library's `.stage-icon` class in your build config: ```ts // vite.config.ts import { stageIcons } from '@dolphinspired/solid-stage/vite'; plugins: [solid(), stageIcons()], // = Solid compiler + every icon tagged .stage-icon ``` ```jsonc // tsconfig.json — type the virtual modules { "compilerOptions": { "types": ["unplugin-icons/types/solid"] } } ``` ```sh pnpm add -D unplugin-icons @iconify-json/ph # the preset's peer + your pack(s) ``` ```tsx // then anywhere — weight is a name suffix: -bold / -fill / -duotone / -thin / -light import HorseBold from '~icons/ph/horse-bold'; // matches IconComponent ``` `stageIcons(options)` forwards any [`unplugin-icons` option](https://github.com/unplugin/unplugin-icons#configuration) (yours override the `compiler` / `defaultClass` defaults). Prefer not to take the preset? It's exactly `Icons({ compiler: 'solid', defaultClass: 'stage-icon' })` from `unplugin-icons/vite` — use that directly and skip the peer dependency. Domain glyphs (a model picker's smileys, media-transport) belong here — see below. Every icon slot uses this same `IconComponent` contract — `IconButton`, `IconToggle`, `Section`'s `headerActions`, `IconButtonField`, `ActionButton`'s `icon`, `ButtonGroupOption`'s `icon`, `SelectFieldAction`'s `icon`. Always pass the component itself (`icon={Copy}`), not a thunk. (A `() => ` thunk still type-checks — a zero-arg function satisfies the contract — but the component form is the convention.) ## Bundled set The current set (import names are PascalCase): **Chrome glyphs** — `Info`, `Plus`, `X`, `CaretUp`, `CaretDown`, `CaretLeft`, `CaretRight`, `CaretDoubleLeft`, `CaretDoubleRight`, `CircleFill`, `DotsSixVertical`, `ArrowsIn`, `ArrowsInSimple`. **Common editor icons** — `Copy`, `Lock`, `Stack`, `FloppyDisk`, `ArrowCounterClockwise`, `Trash`, `Eye`, `EyeSlash`, `GridNine`, `Crosshair`, `TextT`, `Hash`. `InfoIcon` (the field info bubble glyph) is also exported from the main entry. ## Styling Set the icon size globally via `--stage-icon-size` (it derives from the size tier: 12 / 16 / 20px at small / normal / large). Target a specific glyph with its class: ```css .stage-icon-trash { color: var(--stage-color-error); } ``` ## Adding to the set (maintainers) The set is generated from the `@phosphor-icons/core` dev dependency into `src/icons/generated/` (committed) by `scripts/gen-icons.mjs`, driven by the `scripts/icons.json` manifest. Add a name to the manifest and regenerate. The artwork is vendored under Phosphor's MIT licence (retained in `NOTICE`). ### What belongs in the bundled set The bundled set is the **chrome's own vocabulary** — a glyph earns a place only when **a solid-stage component renders it** (disclosure carets, the drag grip, the `arrows-in-simple` redock action, close ×, the modified-tab dot, scroll/step arrows). Bundling those guarantees the chrome looks right with no extra dependency, at a consistent weight, tree-shaken. **Domain icons are not bundled** — media-transport (play / pause / skip), app-specific marks (a model picker's smileys), or any glyph used only by the consumer's own UI. There's no principled stopping point once we add icons for one consumer's feature (record → eject → upload → folder …), and the result would be a hand-maintained Phosphor subset strictly worse than the consumer importing `lucide-solid` / `@phosphor-icons/web` directly. The [`IconComponent` contract](#the-iconcomponent-contract) already covers this: any `Component<{ size? }>` works, so consumers bring their own domain glyphs with zero maintenance cost to the library. If a glyph later becomes one a library component renders, it earns its place then — not before.