Icons¶
Source: design/tokens/icon.tokens.json. Sizes are tokens (icon.size.sm, .md, .lg); the
mapping of semantic names to glyphs is stored as the extension ds.icons on the group node icon,
because DTCG 2025.10 knows no icon type and forbids custom types
(https://www.designtokens.org/tr/2025.10/format/, retrieved 2026-10-10 [verified]).
Choice: Phosphor¶
| Criterion | Phosphor | Lucide | Material Symbols |
|---|---|---|---|
| License | MIT [verified] | ISC [verified, npm lucide-static] |
Apache-2.0 [verified, npm material-symbols] |
| Stroke weights | thin, light, regular, bold, fill, duotone [verified] | one stroke form, width adjustable | variable axes (fill, weight) |
| Filled variant for "active" | yes (fill) |
no | yes |
| Effect | friendly, geometric, distinctive | neutral, common in the generic look | instantly recognizable as Google/Android |
| Flutter | package phosphor_flutter 2.1.0, last published two years ago [verified] |
community packages | package material_symbols_icons |
Evidence: https://github.com/phosphor-icons/core (MIT), package @phosphor-icons/core@2.1.1 (license
MIT, folder assets/<weight>), https://pub.dev/packages/phosphor_flutter (version 2.1.0,
publisher phosphoricons.com, styles Thin, Light, Regular, Bold, Fill, Duotone); all retrieved
2026-10-10.
Reasons: The filled variant shows "active" and "state reached" without a second icon set; the
weight bold stays readable at small sizes; the shapes match the open shapes of
Atkinson Hyperlegible. Material Symbols would make the app look like Android; Lucide has no
filled variant.
Integration without an outdated package. Because phosphor_flutter has been dormant for two
years, packages/design_system integrates Phosphor itself: the package build takes the SVGs of the
used glyphs from @phosphor-icons/core in a pinned version and generates an icon font with only
these glyphs from them (or vector assets with vector_graphics_compiler, flutter.dev); the ticket
"Icons in the design system package" makes the choice after a comparison of size and sharpness. The
generated class DsIcons provides the semantic names (DsIcons.triggerWorking). The preview uses
the same SVGs from tools/design-tokens/vendor/phosphor/ (with the MIT license text).
Integration (M4-05): compiled vector assets¶
The ticket built both candidates once from the same 128 SVGs (every mapped glyph in its mapped
weight and in fill, every space icon in regular, @phosphor-icons/core 2.1.1) and compared
them with the SVG rendered by flutter_svg in a Flutter test (Flutter 3.47.7, dart:ui
pictures without a widget tree, alpha channel per pixel, 2026-10-15):
- (a) a TrueType icon font with only these glyphs, code points from
U+E000in sorted order of<weight>/<glyph>, cubic curves turned into quadratic ones within 1/2048 em, no hinting; - (b) one
vector_graphics_compiler1.3.1 file per glyph and weight.
Pixel difference: mean absolute difference of the alpha value in percent of full coverage, over all 128 glyphs; "worst glyph" is the glyph with the largest mean, "max pixel" the largest single pixel difference. The reference covers about 27 % of the pixels at every size.
| Candidate | Bytes | Size, ratio | Mean | Worst glyph | Max pixel |
|---|---|---|---|---|---|
| font | 34,564 (one file) | 16 px, x1 | 2.97 % | 6.04 % | 47.1 % |
| font | 20 px, x1 | 3.43 % | 7.23 % | 60.4 % | |
| font | 24 px, x1 | 3.12 % | 5.97 % | 62.4 % | |
| font | 16 px, x2 | 3.09 % | 5.41 % | 69.4 % | |
| font | 20 px, x2 | 3.22 % | 5.80 % | 86.7 % | |
| font | 24 px, x2 | 3.20 % | 5.60 % | 90.6 % | |
| vectors | 74,608 (128 files) | every size and ratio | 0 % | 0 % | 0 % |
Choice: vectors (docs/DECISIONS.md D-M4-05-1). At 16 px they are clearly sharper: they match
the SVG reference pixel for pixel (flutter_svg compiles SVGs with the same compiler at run
time), while the font differs by about 3 % on average and up to 47 % in single edge pixels,
because the text engine places and anti-aliases the outline on its line box. The font would be
smaller, but the size rule only decides a tie. Only the vectors are committed.
tools/design-tokens/tokens.py icons (also part of build) compiles
packages/design_system/assets/icons/<weight>/<glyph>.vec with vector_graphics_compiler from
the workspace lock (offline, optimizers off, so no native library of the host is involved) and
writes lib/src/generated/ds_icon_assets.g.dart (DsIconAssets.lookup, dsSemanticIcons),
assets/icons/LICENSE (the Phosphor MIT text) and assets/icons/MANIFEST.json (package
version, compiler version, SHA-256 of every source SVG and asset, and total_bytes, the asset
size of the current mapping). make tokens rebuilds them in memory and fails on a stale, extra
or missing asset and on a mapped glyph without an SVG, naming the semantic name, the glyph and
the weight. DsIcon draws a glyph at icon.size.*; it needs a label or decorative: true, and
it spins the three spinning glyphs once per motion.duration.pulse only in full motion.
Rules¶
- Icons only through semantic names. A component asks for
trigger.working, notcircle-notch. Swapping the glyph is a change in one place. - No emoji as an icon. Emoji exist only as free reactions of people. Status is never an emoji and never a reaction.
- Never an icon alone. Every icon with meaning has a caption or a screen reader label (see components.md). Color is never the only feature; states differ in shape.
- Weights:
regularas the default,fillfor active and for reached final states,boldfor small control glyphs (arrows, plus, close). - Spinning only for
trigger.working,run.runningandstate.reconnecting, and only in thefullmotion context.
Mapping (excerpt)¶
Full list: design/tokens/icon.tokens.json, resolved in
design/build/tokens.resolved.json.
| Semantic name | Glyph | Weight | Meaning |
|---|---|---|---|
trigger.received |
check |
regular | Server has accepted the triggering message |
trigger.queued |
hourglass-medium |
regular | Run waits in the queue |
trigger.working |
circle-notch |
bold, spins | Agent is working |
trigger.done |
check-circle |
fill | Run finished successfully |
trigger.failed |
x-circle |
fill | Run or trigger failed |
trigger.followup |
arrow-bend-down-right |
regular | Follow-up delivered to the running run |
run.waiting |
bell-ringing |
fill | Run waits for an answer (waiting_input) |
run.failed |
warning-octagon |
fill | Run failed |
run.cancelled |
prohibit |
regular | Run canceled |
step.tool_call, step.subagent, step.workflow, step.background_job |
wrench, robot, flow-arrow, clock-countdown |
regular | Step kinds (run_steps.kind) |
channel.text, channel.forum |
hash, chats-circle |
bold, regular | Channel kinds |
billing.personal, billing.organization |
user-circle, buildings |
fill | Backend policy badge |
security.step-up |
shield-check |
fill | Fresh confirmation needed, approval cards |
action.stop |
stop-circle |
fill | Stop run |
The trigger states replace the earlier status markers from Discord one to one: received, queue, in progress, finished, error, follow-up. Their shapes differ so that they stay distinguishable even without color and at small sizes.
Space icons¶
spaces.icon stores a glyph name from the list space-icons in icon.tokens.json
(for example headset, code, megaphone, flask). The server checks against the same list
(from design/build/tokens.resolved.json, ticket "Allowed color and icon keys in the server");
an emoji or a free name is rejected.