Icons
Medice ships 599 icons behind one typed <Icon name="…" /> component, with no icon dependency. They are inlined into the HTML at build time, so nothing lands in client JavaScript and there is no sprite sheet to request.
The API
<Icon name="stethoscope" />
<Icon name="heart-pulse" size="lg" class="text-primary" />
<Icon name="calendar-check" title="Booked" />
name is typed as IconName, a union of all 599, so a typo is a build error rather than an empty square.
size is a variant — sm (16px), md (20px, the default), lg (24px) and xl (32px). Any other size is a size-* utility on the class.
The geometry uses currentColor, so recolor with text-* utilities. text-primary on the icon, or on any ancestor, is all it takes.
Icons are decorative by default: the rendered <svg> carries aria-hidden="true" unless you pass a title, in which case it gets role="img" and an accessible name. That default is the right one — most icons in the theme sit beside a text label that already names the thing, and a screen reader announcing both is noise.
The split registry
Three files, and the split exists to prevent one specific failure.
icons.ts is generated — 571 glyphs, rewritten wholesale by a regeneration pipeline that lives outside this repository. Anything hand-written in it is lost on the next run.
custom.ts is hand-maintained — 28 glyphs the pipeline has no source for. The medical vocabulary is here (stethoscope, heart-pulse, first-aid, bone, brain, ear, eye, stomach, droplet, pipette, flask, baby) along with the marks the generated set’s partial category frames missed (clock, star-filled, calendar-check, map-pin, award).
registry.ts merges them and owns the types:
export const ICONS = { ...GENERATED_ICONS, ...CUSTOM_ICONS };
export type IconName = keyof typeof ICONS;
export const iconNames = Object.keys(ICONS).sort() as IconName[];
The collision rule is additions win: a name in both files resolves to the custom one, so a hand override is deliberate rather than a silent no-op. registry.test.ts pins that direction.
Why the generated file does not export the types
IconName and iconNames are owned by registry.ts and deliberately not re-exported from icons.ts.
Beside a merged registry, the generated file’s own pair would be a second, smaller universe one auto-completed import away from the right one. An accidental from "./icons" would type-check cleanly while rejecting every custom name and vouching for glyphs an override had since replaced — and nothing would flag it.
The “don’t re-emit these” contract lives in the generated file’s own header, because the generator is outside this repo and the header is the only place it can be told.
Count the registry, never quote a comment
The number above — 599 — was computed with Object.keys(ICONS).length, not read off a header. This is worth doing yourself if a count matters to you:
node --experimental-strip-types -e "import('./src/components/svg/icons/registry.ts').then(m => console.log(m.iconNames.length))"
A sibling theme once had three documents citing each other in a circle — the wiki said 504, the component README agreed, and the generated file’s header had been “corrected” to 497 — while the dev catalog rendered the true number off iconNames.length the whole time. A count is a fact you can compute in one line; anything you cannot recompute is a rumour.
The two failures in this system are invisible at runtime, which is why the check exists: a dropped custom glyph and a merge that resolves the wrong way round both render an empty <svg>, and no screenshot distinguishes that from a subtle icon.
Browsing them
/examples/ui renders the whole set with names, off iconNames. It is the fastest way to find the glyph you want, and it cannot fall out of step with the registry because it reads it.
Adding an icon
Add the entry to custom.ts, never to icons.ts — the generated file is overwritten on the next regeneration run.
The value is the inner markup of a 24×24 <svg>: the paths only, without the wrapper, using currentColor for stroke or fill so the glyph follows the token layer. Icon.astro supplies the viewBox, the sizing and the accessibility attributes.
Then use it. The type union picks it up immediately, and registry.test.ts covers the merge.
Where icons are named in data
Several config files carry icon names as data — a specialty’s icon, a fact pill’s icon. Those fields are typed as IconName, so a config file naming a glyph that does not exist fails astro check rather than rendering a blank space on a card.