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