Icons
Urbic has no icon dependency. src/components/svg/icons/ holds 574 inlined 24×24 SVGs behind one typed component, and nothing about them reaches client JavaScript — icons inline into HTML at build time.
Using it
---
import { Icon } from "@components/svg/icons";
---
<Icon name="activity" />
<Icon name="trash-01" size="lg" class="text-error" />
<Icon name="search-01" title="Search" />
name is a typed IconName, so an illegal name fails astro check and autocomplete lists every glyph. size is sm, md, lg or xl — size-4 through size-8, default md — or pass class="size-6" and skip the variant.
Geometry is currentColor, so recolour with any text-* token and dark mode is free. Brand marks are filled silhouettes with the negative space as a path knockout, so they flip with the theme too — they render in one tone, not in brand colours.
Icons are decorative by default (aria-hidden). Pass title to give one an accessible name; it adds role="img" and a <title> element.
The component follows the same primitive contract as everything in ui/: data-slot="icon", an exported tv() config, native svg props plus variant props, merged class, tokens only.
What is in the registry
The bulk are Stratis UI line icons — General, Arrows, Media & Devices, Alerts, Security, Images, Files, Charts, Development, Communication and Editor. On top of those is a social and brand set of filled marks: facebook, instagram, tiktok, threads, messenger, whatsapp, telegram, behance, github, discord, linkedin, slack, line, apple, google, pinterest, google-play and bluesky.
The two-file split
icons.ts is auto-generated and must not be hand-edited. It maps each name to inner SVG markup and exports the IconName union plus an iconNames array. It carries 571 glyphs.
registry.ts is the hand-written seam on top: the generated map, plus a small EXTRAS block of glyphs the Figma source has no analogue for, merged into one map, one union, one component. Three glyphs live there today, which is how the registry reaches 574.
The alternative — a second <IconExtra /> with its own map — was tried and reverted. It duplicated the component verbatim, split IconName in two, and forced every call site to carry two optional icon fields and two conditional renders for what is one concept. Merging keeps the generator’s contract (it only ever rewrites icons.ts) and keeps call sites unaware that some glyphs arrived by a different route.
Adding to EXTRAS is a last resort. Check the generated registry first. Only hand-author when the source file has no equivalent at all, and match the generated contract exactly: a 24×24 viewBox, stroke-width 2, geometry in currentColor, no ids, no wrapper <g>. The spread order lets a generated glyph of the same name win, so a stale extra shadows nothing — but carrying dead ones is still worth avoiding.
When not to use a registry icon
Sections/Project/galleryModes.ts inlines its own path data rather than reaching for <Icon>, and that is the right call rather than a lapse: the registry has no grid or column glyph, so there was nothing to reuse. The primitive-first rule cuts the other way when the primitive does not exist. Those glyphs are still drawn in currentColor so a chip that inverts on :checked takes its icon with it.
The size ceiling
The whole registry is one module, roughly 450 KB at this count. It stays build-time — icons inline into HTML and nothing lands in client JS — but every icon’s markup loads even if a page uses one.
The stated ceiling and upgrade path: importing the registry in a client <script> would ship all of it, and at that point the move is per-file .svg imports as Astro native SVG components, or an SVG sprite. Nothing in Urbic does that today.
Regenerating or adding glyphs
Each category is one frame of the source Figma file. To re-pull one or add another:
- Call the Figma MCP
get_design_contexton the category frame node — one call per frame, with per-column pulls as the fallback if a frame response is too large. - Collect
{ id, name, url }into amanifest.json, caching and deduping by node id — thedata-namevalues are not unique in the source. - Run
node generate.mjs. It downloads each SVG, normalizes colour tocurrentColor, flattens bare<g>wrappers, strips ids, scale-to-fits the few off-grid viewBoxes, dedupes by cleaned content against the existing registry, and rewritesicons.ts.
The generator and its cleaner live in the scratchpad used to build the set, and the full runbook is in src/components/svg/icons/README.md.
src/components/svg/ is package territory — a CMS update replaces it wholesale — so if you add extras, expect to reapply them or contribute them upstream.