Files
airship/packages/editor-icons/ICONS.md
T
Nayan cebab498c7 feat(editor-icons): add the icon normaliser and package shell
The generator reads assets/*.svg and emits one module. Icons arrive from several
sets with different viewboxes, stroke widths and fill conventions; normalising at
build time means the overlay renders them all from a single code path.
2026-07-26 22:51:00 +05:30

33 KiB
Raw Blame History

budget_kb, icons
budget_kb icons
340
chev-down chev-right chev-up caret-down drag grab plus minus close check check-bold search more settings eye eye-off list-view grid-view question clipboard save play align-left align-h-center align-right align-top align-v-center align-bottom distribute-h distribute-v tidy-up flip-h flip-v rotation al-horizontal al-vertical al-wrap al-add al-remove al-baseline al-absolute gap-h gap-v pad-h pad-v pad-individual pad-top pad-right pad-bottom pad-left size-hug size-fixed size-fill size-min-w size-max-w size-min-h size-max-h aspect-lock aspect-unlock fit text-size text-line-height text-letter-spacing text-paragraph-spacing text-indent text-align-left text-align-center text-align-right text-align-justify text-align-top text-align-middle text-align-bottom text-bold text-italic text-underline text-strike text-uppercase text-lowercase text-titlecase text-truncate text-list text-resize-fixed text-resize-width text-resize-height opacity blend-mode overflow-clip overflow-visible corners-independent corner-tl corner-tr corner-bl corner-br fill-solid fill-gradient fill-image fill-mixed eyedropper gradient-linear gradient-radial gradient-angular rotate-ccw stroke stroke-width stroke-custom border-top border-right border-bottom border-left border-none stroke-solid stroke-dash effects effect-drop-shadow effect-inner-shadow effect-layer-blur effect-bg-blur blur spread grid grid-columns grid-rows grid-uniform constraints-h constraints-v layer-frame layer-group layer-text layer-component layer-instance layer-vector layer-section layer-slice tool-move tool-hand tool-frame tool-text tool-inspect tool-measure tool-comment tool-scale dev-brackets keyboard history crop annotate actions component image insert bulb-on pencil doc-plus command globe design edit-mode preview present folder libraries code version-current version-branch version-merged device-mobile device-tablet style-fill style-text style-effect style-grid shadow-top shadow-bottom shadow-left shadow-right shadow-top-left shadow-top-right shadow-bottom-left shadow-bottom-right shadow-spread shadow-min shadow-mid shadow-max inner-shadow-top inner-shadow-bottom inner-shadow-left inner-shadow-right inner-shadow-top-left inner-shadow-top-right inner-shadow-bottom-left inner-shadow-bottom-right inner-shadow-spread blur-small border-top-bottom border-left-right border-top-left border-top-right border-bottom-left border-bottom-right border-top-left-right border-bottom-left-right border-top-left-bottom border-top-right-bottom stroke-cap-none stroke-cap-round stroke-cap-square stroke-join-round stroke-join-square stroke-arrow-line stroke-arrow-triangle stroke-arrow-reversed stroke-arrow-circle stroke-arrow-diamond text-h1 text-h2 text-small-caps text-caps text-subscript text-superscript text-underline-dashed text-underline-dotted text-underline-wave text-skip-ink text-skip-ink-off text-trim text-trim-none text-code text-code-block text-num-normal text-num-mono-lower text-num-mono-upper text-num-prop-lower text-num-prop-upper text-style-replace text-style-update text-resize-auto var-add var-color var-string var-boolean var-text var-value var-settings var-mode var-apply var-style-apply var-info var-mapping proto-click proto-drag proto-mouse-down proto-mouse-up proto-mouse-enter proto-mouse-leave proto-hover proto-press proto-delay proto-conditional proto-none proto-back proto-navigate proto-scroll-to proto-open-link proto-change-to proto-open-overlay proto-close-overlay proto-swap-overlay proto-overlay-bottom proto-overlay-left proto-set-variable proto-time-set proto-time-jump proto-gamepad blend-off blend-on gradient-diamond adjust-contrast adjust-exposure adjust-saturation adjust-temperature adjust-tint adjust-highlights adjust-shadows shape-rect shape-ellipse shape-line shape-arrow shape-polygon shape-star shape-pen shape-bend shape-paint shapes count-polygon count-star angle mask flatten bool-union bool-subtract bool-intersect bool-exclude layer-align-h-top layer-align-h-center layer-align-h-bottom layer-align-v-left layer-align-v-center layer-align-v-right layer-align-wrap-left layer-align-wrap-right layer-wrap-center grid-column grid-row comment-message comment-resolve comment-mention comment-emoji comment-image comment-new thumb-up thumb-down heart bulb bulb-off view spotlight team-library indeterminate select-using ready-for-dev language square rectangle portrait connection person people company key attention public edit disconnect minimize missing-font syncing logo panel-left panel-right dot gutter lock unlock claude codex opencode
others/down-arrow-small.svg others/right-arrow-small.svg others/up-arrow.svg others/dropdown-arrow.svg others/drag.svg others/grab.svg general/add.svg general/remove.svg general/x.svg general/check.svg general/check-bold.svg general/search.svg general/more-actions.svg general/setting.svg appearance/show.svg appearance/hide.svg general/show-as-list.svg general/show-as-grid.svg others/question-mark.svg others/clipboard.svg others/save.svg others/play.svg position/align-vertical-left.svg position/align-vertical-center.svg position/align-vertical-right.svg position/align-horizontal-top.svg position/align-horizontal-center.svg position/align-horizontal-bottom.svg position/distribute-horizontal-spacing.svg position/distribute-vertical-spacing.svg position/tidy-up.svg alignment/flip-horizontal.svg alignment/flip-vertical.svg alignment/rotation.svg auto-layout/horizontal-layout.svg auto-layout/vertical-layout.svg auto-layout/wrap.svg auto-layout/use-auto-layout.svg auto-layout/remove-auto-layout.svg auto-layout/icon-16-autolayout-alignment-baseline.svg auto-layout/absolute.svg auto-layout/horizontal-gap-between-objects.svg auto-layout/vertical-gap-between-objects.svg auto-layout/horizontal-padding.svg auto-layout/vertical-padding.svg auto-layout/individual-padding.svg auto-layout/top-padding.svg auto-layout/right-padding.svg auto-layout/bottom-padding.svg auto-layout/left-padding.svg auto-layout/hug-content.svg auto-layout/fixed-width.svg auto-layout/resize-to-fit.svg auto-layout/add-min-width.svg auto-layout/add-max-width.svg auto-layout/add-min-height.svg auto-layout/add-max-height.svg auto-layout/lock-aspect-ratio.svg auto-layout/unlock-aspect-ratio.svg auto-layout/resize-to-fit-1.svg text/text-font-size.svg text/text-line-height.svg text/text-letter-spacing.svg text/text-paragraph-spacing.svg text/text-paragraph-indent.svg text/text-align-left.svg text/text-align-center.svg text/text-align-right.svg text/text-align-justified.svg text/text-align-top.svg text/text-align-middle.svg text/text-align-bottom.svg text/bold.svg text/italic.svg text/underline.svg text/strike-through-false.svg text/uppercase.svg text/lowercase.svg text/title-case.svg text/truncate-text.svg text/number-list.svg text/text-resize-fixed.svg text/text-resize-width.svg text/text-resize-height.svg appearance/opacity.svg appearance/apply-blend-mode.svg frame/overflow-clip.svg frame/overflow-display.svg appearance/independent-corners.svg appearance/top-left-corner-radius.svg appearance/top-right-corner-radius.svg appearance/bottom-left-corner-radius.svg appearance/bottom-right-corner-radius.svg fill/fill-solid.svg fill/fill-gradient-linear.svg fill/fill-image.svg fill/fill-mixed.svg fill/eyedropper.svg fill/gradient-linear.svg fill/gradient-radial.svg fill/gradient-angular.svg fill/rotate.svg stroke/border.svg stroke/stroke-width.svg stroke/stroke-custom.svg stroke/border-top.svg stroke/border-right.svg stroke/border-bottom.svg stroke/border-left.svg stroke/border-none.svg stroke/stroke-solid.svg stroke/stroke-dash.svg effects/effects.svg effects/effect-setting-drop-shadow.svg effects/effect-setting-inner-shadow.svg effects/effect-setting-layer-blur.svg effects/effect-setting-background-blur.svg effects/blur.svg effects/spread.svg grid/grid.svg grid/layout-grid-columns.svg grid/layout-grid-rows.svg grid/layout-grid-uniform.svg constraints/constraints-horizontal.svg constraints/constraints-vertical.svg layers/frame.svg layers/group.svg layers/text.svg layers/layer-component.svg layers/layer-instance.svg layers/layer-complex-vector.svg layers/section.svg layers/slice.svg toolbar/move.svg toolbar/hand-tool.svg toolbar/frame.svg toolbar/edit-text.svg toolbar/inspect.svg toolbar/measure.svg toolbar/comment.svg toolbar/scale.svg toolbar/dev-brackets.svg toolbar/keyboard-shortcuts.svg toolbar/version-history.svg toolbar/crop.svg toolbar/annotate.svg toolbar/actions.svg toolbar/component.svg toolbar/image.svg toolbar/insert.svg others/bulb-on.svg toolbar/pencil.svg toolbar/doc-plus.svg toolbar/command.svg share/public.svg icons/design.svg sidebar-left/edit.svg sidebar-left/preview.svg sidebar-left/present.svg sidebar-left/folder.svg sidebar-left/libraries.svg others/code.svg others/version-current.svg others/version-new-branch.svg others/version-merged.svg others/mobile.svg others/tablet.svg style/style-fill.svg style/style-text.svg style/style-effect.svg style/style-grid.svg effects/drop-shadow-top.svg effects/drop-shadow-bottom.svg effects/drop-shadow-left.svg effects/drop-shadow-right.svg effects/drop-shadow-top-left.svg effects/drop-shadow-top-right.svg effects/drop-shadow-bottom-left.svg effects/drop-shadow-bottom-right.svg effects/drop-shadow-spread.svg effects/drop-shadow-min.svg effects/drop-shadow-mid.svg effects/drop-shadow-max.svg effects/inner-shadow-top.svg effects/inner-shadow-bottom.svg effects/inner-shadow-left.svg effects/inner-shadow-right.svg effects/inner-shadow-top-left.svg effects/inner-shadow-top-right.svg effects/inner-shadow-bottom-left.svg effects/inner-shadow-bottom-right.svg effects/inner-shadow-spread.svg effects/layer-blur-small.svg stroke/border-top-bottom.svg stroke/border-left-right.svg stroke/border-top-left.svg stroke/border-top-right.svg stroke/border-bottom-left.svg stroke/border-bottom-right.svg stroke/border-top-left-right.svg stroke/border-bottom-left-right.svg stroke/border-top-left-bottom.svg stroke/border-top-right-bottom.svg stroke/stroke-cap-none.svg stroke/stroke-cap-round.svg stroke/stroke-cap-square.svg stroke/stroke-round.svg stroke/stroke-square.svg stroke/stroke-line-arrow.svg stroke/stroke-triangle-arrow.svg stroke/stroke-reversed-triangle.svg stroke/stroke-circle-arrow.svg stroke/stroke-diamond-arrow.svg text/h1.svg text/h2.svg text/small-caps.svg text/text-caps.svg text/subscript.svg text/superscript.svg text/underline-dashed.svg text/underline-dotted.svg text/underline-wave.svg text/skip-ink-true.svg text/skip-ink-false.svg text/leading-trim.svg text/no-trim.svg text/code-snippet.svg text/code-block.svg text/normal-number.svg text/monospace-lowercase-number.svg text/monospace-uppercase-number.svg text/proportional-lowercase-number.svg text/proportional-uppercase-number.svg text/replace-text-style.svg text/update-text-style.svg text/text-resize-height-1.svg variable/add-variable.svg variable/variable-color.svg variable/variable-string.svg variable/variable-boolean.svg variable/variable-text.svg variable/variable-value.svg variable/variable-setting.svg prototype/set-variable-mode.svg general/apply-variable.svg general/apply-style-and-variable.svg others/variable-info.svg others/variable-mapping.svg prototype/on-click.svg prototype/on-drag.svg prototype/mouse-down.svg prototype/mouse-up.svg prototype/mouse-enter.svg prototype/mouse-leave.svg prototype/while-hovering.svg prototype/while-pressing.svg prototype/after-delay.svg prototype/conditional.svg prototype/none.svg prototype/back.svg prototype/navigate-to.svg prototype/scroll-to.svg prototype/open-link.svg prototype/change-to.svg prototype/open-overlay.svg prototype/close-overlay.svg prototype/swap-overlay.svg prototype/layout-overlay-bottom.svg prototype/layout-overlay-left.svg prototype/set-variable.svg prototype/action-time-set.svg prototype/action-time-jump.svg prototype/gamepad.svg color-picker/blendmode.svg color-picker/blendmode-active.svg color-picker/gradient-diamond.svg color-picker/contrast.svg color-picker/exposure.svg color-picker/saturation.svg color-picker/temperature.svg color-picker/tint.svg color-picker/highlights.svg color-picker/shadows.svg toolbar/rectangle.svg toolbar/ellipse.svg toolbar/line.svg toolbar/arrow.svg toolbar/polygon.svg toolbar/star.svg toolbar/pen.svg toolbar/vector-bend.svg toolbar/vector-paint.svg toolbar/shapes.svg frame/count-polygon.svg frame/count-star.svg frame/angle.svg frame/mask.svg frame/flatten-selection.svg frame/boolean-union.svg frame/boolean-subtract.svg frame/boolean-intersect.svg frame/boolean-exclude.svg layers/horizontal-layout/align-top.svg layers/horizontal-layout/align-center.svg layers/horizontal-layout/align-bottom.svg layers/vertical-layout/align-left.svg layers/vertical-layout/align-center.svg layers/vertical-layout/align-right.svg layers/wrap-layout/align-left.svg layers/wrap-layout/align-right.svg layers/layer-wrap-align-center.svg grid/column.svg grid/row.svg comment/message.svg comment/make-as-resolved.svg comment/mention.svg comment/add-emoji.svg comment/add-image.svg toolbar/comment-new.svg others/thumb-up.svg others/thumb-down.svg others/heart.svg others/bulb-on.svg others/bulb-off.svg general/view.svg general/spotlight.svg general/team-library.svg general/intermediate.svg general/select-item-using.svg general/mark-as-ready-for-dev.svg others/language.svg others/square.svg others/rectangle.svg others/portrait.svg others/creating-a-connection.svg share/person.svg share/people.svg share/company.svg share/key.svg share/attention.svg share/public.svg sidebar-left/edit.svg sidebar-left/disconnect.svg sidebar-left/minimize-ui.svg sidebar-left/missing-font.svg sidebar-left/saving-syncing.svg local/logo.svg local/panel-left.svg local/panel-right.svg local/dot.svg local/gutter.svg local/lock.svg local/unlock.svg local/claude.svg local/codex.svg local/opencode.svg

Airship editor icons

The overlay's icon set. This front-matter is the canonical manifest: it maps a stable, semantic slug to a file under assets/ui/. scripts/gen.mjs reads it and emits src/generated/icons.ts, exactly the way @airship/editor-tokens turns EDITOR.md into src/generated/editor.ts.

edit ICONS.md → pnpm --filter @airship/editor-icons gen → src/generated/icons.ts

Never edit the generated file.

Naming

Slugs are kebab-case and semantic, not source-verbatim — text-size, not text font size. Where a bare noun would be ambiguous across panels, the slug carries its domain as a prefix:

Prefix Domain Example
al- Auto layout al-horizontal, al-remove
pad- / gap- Spacing controls pad-individual, gap-v
size- Resizing modes size-hug, size-fill
text- The Text section text-line-height
corner- / border- Per-side/per-corner toggles corner-tl, border-none
layer- Layer-kind glyphs in the tree layer-component
tool- Toolbar tools tool-hand
effect- Effect types effect-inner-shadow

Add an icon by adding a line here, not by importing an SVG at a call site. The budget check below is the only thing keeping the bundle honest, and it can only see what goes through the manifest.

Budget

budget_kb caps the total normalised markup. The generator prints a per-category size report and fails the build when the manifest exceeds it. Measured figures for the full corpus, after normalisation:

Set Icons Raw Gzipped
This manifest 348 ~312 KB ~87 KB
Everything in assets/ 509 ~468 KB ~124 KB

The manifest went from 173 slugs to 348 in one pass, and budget_kb from 160 to 320 with it. That doubled cold-start icon cost by ~40 KB gzipped, and it was a deliberate trade: the sections added alongside it (filters, media, vector, scope, tokens, the gradient editor) were reaching for glyphs that did not exist, and the failure mode when a slug is missing is not a fallback — resolve() throws, or, worse, the name lands in a text slot and renders as literal characters.

320 → 340 was the second raise, when normalisation stopped exempting the 24×24 majority and every glyph gained a refit <g>. That is +16.4 KB of markup but only +1.8 KB gzipped — the wrappers are near-identical strings, which is the case compression is best at, and compression is what these bytes actually ship under. The alternative was baking the transform into the path coordinates, which costs no bytes but makes the refit invisible and unauditable; the wrapper can be read, diffed, and re-measured, and the generator does re-measure it.

What is still excluded, and why: the application-specific glyphs with no meaning in a DOM editor — component libraries and their variants, plugin and widget marks, presentation and whiteboard tools, collaboration affordances, captioning, engineering-handoff states. That is roughly 120 more unique glyphs and about 100 KB. They are one manifest line each if the product ever grows a use for them.

Raising budget_kb is a decision, not a formality — the overlay bundle is re-fetched on every page load (no-store, it changes with every rebuild), so icon bytes are cold-start bytes. They are affordable only because serveAirshipAsset compresses (packages/server/src/proxy.ts).

A single object literal is retained wholesale by esbuild, so the registry cannot be tree-shaken: icon(name) is called with computed names in the layers tree and in the descriptor tables, so it has to be eager. Curation plus the budget check is the control, not the bundler.

What the generator normalises

  1. Every glyph is refit onto one 24×24 box at one optical size. Each icon's artwork is measured, then wrapped in a translate(…) scale(…) that centres it at 12.8 units. After this pass every icon reports box: 24 and one icon(name, "md") means one optical size everywhere. A file with no viewBox is a hard failure, because there is nothing safe to assume.

    This used to exempt anything already 24×24, on the reasoning that those glyphs are drawn on the pixel grid and rescaling would only blur them — the set insets its artwork heavily, the span is about 12.8 units, and that inset is the set's visual rhythm.

    That is true of the corpus and false of its members. Measured properly the artwork spans 7 to 24 units: median 13.0, p90 16.0. The exemption covered 300 of 346 glyphs, so optical size was free-floating almost everywhere, and icons from opposite ends of that spread ended up side by side — the bottom bar had rotation at 7.0 next to doc-plus at 18.0, a 2.6× difference in painted ink at the same nominal size.

    Two details the measurement has to get right, both of which the old coordinate-pair scan did not:

    • Curves contribute their extrema, not their control points, and H/V take a single operand. 297 of the 346 glyphs use H/V; a pair-wise scan drops those operands, and plus's M6.5 11.5H17.5 measured 0×0 — which under a general refit would have divided by zero and erased the glyph.
    • <defs>, <clipPath> and <mask> are excluded. Their geometry is structural rather than painted: toolbar/move.svg's mask is a full-bleed rect spanning 17 of its 24 units, which is not the size of the cursor it reveals.

    Stroked glyphs are fitted to 12.8 of painted extent, not geometry — their centrelines land at 12.8 − STROKE_REF so the stroke's outer edge finishes where a filled glyph's silhouette would. See normalisation step 3.

    The generator re-measures its own output and fails the build if any glyph is more than 0.01 units off the reference. That check is the thing that was missing: the old exemption was never verified, so the drift was invisible.

  2. Paint becomes currentColor. #1B1F24 (481 fills + 36 strokes), #007BE5 and black are rewritten. white is left alone — those 18 are <clipPath>/<mask> rects that must stay opaque — as is the single #F24822, which is a semantic red.

  3. Stroke weight is pinned. 26 of the 346 glyphs are stroked and none declares a stroke-width, so they inherit 1 user unit — which the refit <g> then scales along with the geometry. Left alone that makes stroke weight a function of how large the source happened to be drawn: chev-down came out at 3.02 units against its neighbours' 1.0. The generator writes stroke-width="STROKE_REF / scale" on the group, which cancels the transform exactly, so every stroked glyph paints the same weight.

  4. Paint tone is completed, never overwritten. The two-tone is the most recognisable property of the set — the Constraints and Auto Layout glyphs draw their frame at 0.3 and the meaningful part at 0.9 — and flattening it collapses them into mush, so any opacity the source states is left exactly alone.

    What the source doesn't state is filled in. The grammar was only applied to part of the corpus: 146 icons had at least one element with no opacity at all, painting at 100% beside neighbours at 90%. Those get 0.9, the subject tone. It is applied per element rather than on the refit <g>, because inherited opacity multiplies with the child's own — a group at 0.9 would drag every 0.3 context path to 0.27 and silently re-tune the two-tone.

    All 356 icons now carry an explicit tone; 44 keep a 0.3 context layer.

  5. Element ids are namespaced to ap-<slug>-<id>. Only 19 files carry ids, but two search icons on one page would otherwise share — and break — a clip path.

  6. Coordinates round to 2 dp, which takes the corpus from ~600 KB of path data to ~442 KB for no visible change.

Marks we draw ourselves

Ten slugs under assets/local/ are not from the imported set. They go through the same manifest and the same normalisation as everything else — which is the point. They previously lived as hand-authored strings in the overlay's icons.ts, inset "roughly 4..20" (a 16-unit span) to match the set by eye. The set's actual reference is 12.8, so the brand mark, both panel toggles and all three agent logos rendered about 25% oversized, and there were two competing conventions with nothing reconciling them.

  • logo — Airship's mark: an A cut from two slabs. It has been a four-point spark and then a flying saucer. The spark was the house style of every AI feature shipped since 2023, so the one mark meant to say Airship was the one saying the least; the saucer said the name, but a pictogram in a dock head is a small illustration wherever you put it, and it kept reading as a drawing rather than as an identity. A monogram is the thing that stays a monogram at 16px.

    Two shapes. The main slab is the A's right leg carried up through a mitred apex; the second is the left leg, detached, its flat top landing exactly on the first slab's inner edge. The counter is therefore open at the foot — a V of negative space rather than a closed triangle — which is what keeps the mark from filling in at small sizes, where a closed counter of this proportion would be the first thing to clog.

    Everything follows from two numbers: every edge that is not a foot or the leg's top runs at dx/dy = 0.58, and the horizontal bar width is 4.53. That one slope, mirrored about the centre, is what makes the apex symmetric and the two legs read as the same slab. Two consequences of it are worth not "tidying" away later: the apex sits at x=12, and the left leg's top-right corner also lands at x=12, directly beneath it. Neither was placed — both fall out of the slope, and they are why the mark feels centred despite being made of two shapes that are not.

    Solid, where the imported set is outline, deliberately: it sits beside plus, history and rotate-ccw in the dock head and should be the heaviest thing in that row. It carries no two-tone. The set's 0.9/0.3 split marks subject against context, and a logo is all subject — dimming a limb of it would draw a distinction the mark does not have. One compound path under the default nonzero rule: the two subpaths wind the same way and meet at exactly one point, so nonzero unions them and the junction needs no hand-fitted geometry.

  • panel-left / panel-right — a framed rect with a gutter on the side its panel is on, so the glyph reads as the layout it toggles. The gutter is the 0.9 tone, the frame the 0.3 — the same two-tone grammar the imported set uses.

  • dot — a solid dot for neutral / in-progress status.

  • gutter — the elbow rail for the chat timeline's result line, a drawn ⎿. The character itself (U+23BF) is absent from the latin font subsets we self-host, so it would fall out to a system face with different metrics inside a gutter whose entire job is alignment, and tofu where no fallback has it.

  • lock / unlock — the layers tree's lock pair, filled. share/lock.svg and share/lock-open.svg are stroke-expansion exports: a <mask> holding the padlock silhouette, and a visible path that is only the ~1px ring around it. The layers tree draws its glyphs at xs (16px), where that ring scales to two thirds of a pixel — it antialiases to a grey smudge, and sits next to the eye and the layer-kind glyphs, which are filled. These are the mask's own silhouette painted directly, with the shackle as an even-odd counter, so the pair matches the family it lives in.

  • claude / codex / opencode — third-party product marks, for the agent picker. Unlike the rest these are not ours to redraw; they name a backend in the UI. Path data from @lobehub/icons-static-svg; the marks remain the trademarks of Anthropic, OpenAI and the opencode project respectively.

Sizing

Every glyph's artwork spans 12.8 of the 24 units, centred — so the visible mark runs x/y 5.6..18.4. A 24-box glyph rendered at 16 px therefore yields a ~8.5 px mark, which is why icon() defaults to 24, not 16, and why the --ap-icon-size-* scale starts at 16 rather than 12. Do not pass raw pixel numbers at call sites; use the named sizes so the scale stays swappable.

Normalising by bounding box makes every glyph occupy the same box, which is not quite the same as the same optical weight: a solid disc filling 12.8 units reads heavier than a thin chevron spanning the same 12.8. Where that matters the fix belongs at the call site, as a deliberate smaller size — not as a hand-tuned exception inside the set, which is how the drift started.

Deliberately excluded

prototype/ (26 — no prototyping surface), comment/ (7 — no comment backend), variable/ (8), frame/ boolean operations (a DOM editor cannot union paths), icons/ product marks (3 of 4 — design is kept as the Edit tab's glyph; the other three name applications Airship is not), most of share/ (lock, unlock and globe are the exceptions) and most of sidebar-left/.

color-picker/ is excluded as a duplicate: its 21 files are byte-identical to fill/.

The 25 constraints/constraints-N.svg anchor-matrix variants are excluded on purpose. That widget composes procedurally from a frame plus an anchor, which is what Airship does — see the Constraints control. Inlining the matrix would cost ~20 KB to hard-code every state of something better expressed as two booleans.