- Date: 2026-06-06
- Status: Accepted
- Phase / Feature: Foundation v1 · icons
- Amends: ADR 0036 §4 ("100 icons custom-designed"), ADR 0046 §1/§2 ("v1.x custom-designed → 100")
- Closes: T7.2 + T7.3 design pass (now realized by import, not in-house drawing)
Context
ADR 0036 §4 promised 100 custom-designed icons across 14 categories, with Solid/Line variants and RTL-aware flags. ADR 0046 §3 sequenced the design pass priority list. T7.4 shipped the per-icon pipeline that any source-of-truth can feed.
After shipping the pipeline + the spec, the design pass itself stayed parked — the work needs a designer sitting in Figma drawing 200+ SVGs with optical alignment + variant pairing. ARNO ships without that pass today.
Two paths to unblock:
- Wait for the design pass. Foundation stays at 7 essentials until a designer cycles through ~100 icon × 2 variant × RTL review.
- Import from a community icon library. Trade visual-identity custom intent for ship velocity. Designer can replace specific icons later via the ADR 0041 brand_icon upload flow.
This ADR picks path 2 for v1, names Phosphor as the source, and sequences the eventual override path. ADR 0036 §4's "custom-designed" promise is amended to "sourced today, replaceable per project".
Decision
§1 — Phosphor over Lucide / Heroicons / Feather
Phosphor (https://phosphoricons.com (opens in a new tab), MIT license) chosen for three reasons:
- Paired variants ship together. Phosphor's
regular(outline) +fill(solid) weights cover ADR 0036 §4's Solid/Line promise in one library, in one consistent style. Lucide is outline-only — adopting Lucide would force us back to designing solid weights ourselves. Heroicons ships both but its visual language (Apple-derivative) clashes with the rest of Foundation. - Wider category coverage. Phosphor ships 1200+ icons across 6 weights; ADR 0046 §3 priority list is fully covered by Phosphor's catalog. No category gaps require fallback design.
- MIT license + clean attribution. No restrictive clauses, no reserved name (unlike SIL OFL fonts). Required attribution lives in
packages/foundation/icons/LICENSE-PHOSPHOR.md.
§2 — Import 100 (actually 108) icons mapped to ADR 0046 §3 priority categories
packages/foundation/build-foundation-icons.mjs is the single source of truth. The script lists every Foundation icon as a triple { arno: <Foundation name>, phosphor: <Phosphor name>, rtl: 'flip' | 'keep' }, reads Phosphor's regular + fill SVG markup, and codegen-emits one .tsx per icon + an updated Icon.tsx catch-all REGISTRY + an updated sprite generator.
Category coverage (per ADR 0046 §3):
- Essentials (7) — check / x / chevron-down / person / alert / info / error. Replace the prior hand-drawn versions so all 100 share the same visual language.
- Directional (13) — chevrons + arrows + corners. RTL flag set per direction (left/right arrows flip; up/down stay).
- Action verbs (28) — copy/edit/save/trash/plus/minus/settings/search/filter/etc.
- Status + feedback (10) — bell/alert-circle/check-circle/help/zap/sun/moon/etc.
- File / format (10) — file/folder/image/cloud/etc.
- Communication (7) — mail/chat/phone/send/etc.
- User / identity (6) — users/shield/crown/user-plus/etc.
- Time (3) — calendar/clock/history.
- Layout / structure (8) — grid/list/sidebar/columns/etc.
- Tools / misc (16) — code/terminal/globe/flag/etc.
Total: 108 (originally planned 100; landed slightly higher because Phosphor's catalog naturally covered a few neighboring concepts cheaply).
§3 — Codegen is the contract
The script is the source of truth. Editing a generated .tsx by hand will be overwritten next run. To change an icon: edit ICONS in build-foundation-icons.mjs, re-run node build-foundation-icons.mjs, commit the diff.
Generator outputs:
packages/foundation/src/icons/<Name>Icon.tsxper iconpackages/foundation/src/icons/index.tsre-exportspackages/foundation/src/components/Icon.tsxIconName union + REGISTRYpackages/foundation/build-icons.mjssprite generator (regenerated with current path data)
The _shared.tsx IconBase + RTL behavior (ADR 0040) remain unchanged; viewBox bumped from 0 0 24 24 to 0 0 256 256 to match Phosphor's coordinate system.
§4 — Per-project override path
A designer who wants Foundation's check to look different in their project uploads a replacement via the ADR 0041 brand_icon flow under the same name. Render precedence in the eventual editor surface picks brand over Foundation when both exist (ADR 0041 §"Open questions" — same answer).
This is the "custom-designed at the seam, sourced in the middle" pattern. Foundation ships a coherent default; the brand layer carries identity.
§5 — Replacement / upgrade path
If a future custom-designed in-house pass ships (Foundation v2 §6 priority), the same generator script can swap source from Phosphor to the internal design system in one edit. Per-icon API + sprite IDs stay stable across the swap; consumers don't break.
Anti-patterns explicit
- Do not hand-edit any generated
.tsxfile. The next regen overwrites your work + the contract is documented at the top of each file. - Do not import other Phosphor weights (thin/light/bold/duotone) at the Foundation layer. Solid + Line is the API contract per ADR 0036 §4; offering more weights is a v2 conversation, not a "while we're here" addition.
- Do not mix sources mid-set (some icons Phosphor, others Lucide). Visual coherence over per-icon-best-pick.
- Do not ship icons that don't have both regular + fill in Phosphor. If a needed concept lacks a fill variant, the brand-icon upload flow is the answer.
Open questions / parking
- Custom in-house pass for select icons (brand-defining marks like the logo or product-specific glyphs) — parked behind ADR 0046 v2 work + the brand_icon upload route.
- Phosphor version bumps. When Phosphor releases a new major (path-data changes), re-running the generator updates the set. Bumping is opt-in; the lockfile pins the current version.
- Tree-shaking the import-time devDep.
@phosphor-icons/coreis only used at codegen time and lives in devDependencies. Runtime bundle has no Phosphor dependency.