# Icons and the smallest UI primitives
Audit of `margin` (`/Users/pj/Workspace/projects/python/margin`), `margin-calendar`
(`/Users/pj/Workspace/projects/python/margin-caledar`), `margin-docs`
(`/Users/pj/Workspace/projects/rust/margin-editor`) and `margin-mail`
(`/Users/pj/Workspace/projects/rust/margin-mail`) against the partial shared package at
`/Users/pj/Workspace/projects/python/margin/shared`.
## The shared package as it stands
`shared/src/icons.ts` exports twelve paths plus `SUN_DISC`. Its header comment says the two apps
kept drifting, that a path is a design decision, and that `Icon` is deliberately not shared because
sharing it "would make this package depend on React for twenty four lines, and a component is where
an app is entitled to differ."
Two of those three claims no longer hold.
The React argument is wrong on the mechanics. `shared/package.json` has no `dependencies` block at
all, no build step, and every app resolves the TypeScript source through its own bundler. All four
apps are on `react: ^19.1.0`. A `peerDependencies` entry costs zero bytes and installs nothing;
it is a version assertion, not a dependency.
"An app is entitled to differ" is contradicted by the code. Three of the four `Icon.tsx` files are
byte identical (md5 `0ec1a568818f20ed8eed8ad46fbaa2b1`):
`/Users/pj/Workspace/projects/python/margin/src/components/Icon.tsx`,
`/Users/pj/Workspace/projects/python/margin-caledar/src/components/Icon.tsx`,
`/Users/pj/Workspace/projects/rust/margin-editor/src/components/Icon.tsx`. In two years nobody has
exercised the entitlement.
Also worth noting: `margin-calendar` does not consume `margin-shared` at all. There is no
`margin-shared` line in `/Users/pj/Workspace/projects/python/margin-caledar/package.json`, and
`src/styles/tokens.css` is a hand copy of the shared file's values. Everything below that looks
like calendar drifting away from the family traces back to this one fact.
## The Icon component, line by line
All four render the same SVG: `viewBox="0 0 24 24"`, `fill="none"`, `stroke="currentColor"`,
`strokeWidth="1.6"`, `strokeLinecap="round"`, `strokeLinejoin="round"`, `size = 16` default,
`{children ?? }`. Props are `d?: string`, `size?: number`, `children?: ReactNode`.
`margin-mail`'s at `/Users/pj/Workspace/projects/rust/margin-mail/src/ui/Icon.tsx:12` is the only
one that differs, in four ways, all of them improvements:
- `export interface IconProps` rather than a private `interface` (line 4).
- `className="icon"` (line 15), which is what lets CSS reach the element.
- `aria-hidden="true"` (line 24). The other three emit an unlabelled SVG into the accessibility
tree at every one of their 142 combined call sites.
- `import "./Icon.css"` (line 2), whose entire contents are `.icon { flex: none; }`
(`Icon.css:2-4`).
There is no alignment handling in any of the four components. No `display`, no `vertical-align`,
no `shape-rendering`, no `vector-effect`, no transform.
## Alignment: the thing that keeps being fixed four times
Across all four repos there are **zero** occurrences of `shape-rendering`, `vector-effect`,
`crispEdges`, `geometricPrecision`, or a `translate(0.5 0.5)` style half pixel offset. The
alignment problem is not sub-pixel rasterisation. It is the two ordinary CSS facts about an inline
SVG: it sits on the text baseline, and it is a flex item that will shrink.
Five different fixes exist for those two facts, and only one app fixes them centrally.
`margin-mail` fixes both once:
- `/Users/pj/Workspace/projects/rust/margin-mail/src/styles/app.css:88` `svg { display: block; }`
This is the only global SVG rule in the suite. The other three apps have no `svg` selector at
document level at all.
- `/Users/pj/Workspace/projects/rust/margin-mail/src/ui/Icon.css:2` `.icon { flex: none; }`
The other three patch it per site:
- `/Users/pj/Workspace/projects/python/margin-caledar/src/styles/overlays.css:36`
`.panel-note[data-icon] svg { flex: none; transform: translateY(2px); }`
- `/Users/pj/Workspace/projects/python/margin-caledar/src/styles/details.css:114`
`.details-row[data-block] > svg { margin-top: 2px; }`
- `/Users/pj/Workspace/projects/rust/margin-editor/src/styles/tree.css:473`
`.start-row svg { align-self: center; color: var(--ink-faint); }`
A `translateY(2px)` and a `margin-top: 2px` in the same repo, for the same symptom, four files
apart. Neither is wrong; both exist because the baseline was never dealt with at the root.
The residual case is real and survives the global fix: an icon inside an `align-items: baseline`
row still needs `align-self: center`. `margin-mail` hits it too, at
`/Users/pj/Workspace/projects/rust/margin-mail/src/ui/Row.css:127` (`.row-mark { flex: none;
display: inline-flex; align-self: center; }`), which is the same declaration as margin-docs'
`tree.css:473`. There are 20 `align-items: baseline` rules across the four apps, so this is a
recurring shape, not an exception.
Icon size is not a shared decision and probably should not become one. `margin-mail` never uses the
16px default (zero bare ``, nine distinct explicit sizes from 10 to 20). The other
three lean on the default heavily: 33 bare call sites in margin-docs, 14 in calendar, 12 in margin.
## Glyph inventory
151 path definitions across the suite, 132 distinct strings, 13 of which appear in more than one
app. The shared set covers 13. Per app, unique path strings: shared 13, margin 30, calendar 31,
margin-docs 47, margin-mail 30.
Only `margin-mail` keeps its glyphs in a module (`src/ui/icons.ts`, 30 named constants, five
re-exported from `margin-shared/icons` at line 14). `margin-docs` names its toolbar and titlebar
glyphs as module constants but writes six more inline. `margin` and `margin-calendar` are almost
entirely inline `d="M..."` in JSX.
Shared-set uptake is thin: `margin` uses eleven of the twelve; `margin-docs` uses seven
(`SIDEBAR`, `SEARCH`, `SPELLING`, `GRAMMAR`, `EXPORT`, `MORE`, `CHECK`, `WIDTH`); `margin-mail`
re-exports five; `margin-calendar` uses none.
### Same concept, different path
The important cases, with the exact strings.
**SEARCH.** Shared `icons.ts:21` is `M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4`. Calendar
`components/Header.tsx:22` is `M11 19a8 8 0 100-16 8 8 0 000 16zM21 21l-4.35-4.35`. Different lens
radius (7 vs 8) and a different handle. This is the exact divergence the shared package's header
comment says it exists to prevent, still present because calendar never joined.
**MORE.** Shared `icons.ts:57` is three dots, `M5 12h.01M12 12h.01M19 12h.01`. Calendar
`components/PhoneBar.tsx:29` uses the same name for a hamburger, `M4 7h16M4 12h16M4 17h16`. A
straight name collision on two unrelated glyphs.
**SUN.** Shared splits it: `SUN_RAYS` (`icons.ts:53`) with a `SUN_DISC` circle at `r: 4`, rays
starting at `M12 2v2`. Calendar `Header.tsx:24` is one path with an `r=5` disc and rays at
`M12 1v2M12 21v2M4.2 4.2...`. Different construction and different geometry.
**HEADING.** `margin/src/editor/FloatingToolbar.tsx:126` is `M5 5v14M5 12h8M13 5v14`.
`margin-editor/src/editor/Toolbar.tsx:184` is `M7 5v14M7 12h10M17 5v14`, with a comment at line 182
that says exactly why: "The H used to run from x=5 to x=13 in a 24 unit box, so it sat left of
centre in a round button that every other glyph here is centred in." One app fixed the optical
centring; the other still has the bug. This is the "fix the alignment separately in each app"
complaint, at the glyph level, with the fix already written down in one repo.
**BULLET LIST.** `margin/src/editor/FloatingToolbar.tsx:128` puts the bullets at x=3.5:
`M8 6h12M8 12h12M8 18h12M3.5 6h.01M3.5 12h.01M3.5 18h.01`.
`margin-editor/src/editor/Toolbar.tsx:185` puts them at x=4: `...M4 6h.01M4 12h.01M4 18h.01`. A half
unit apart on otherwise identical rules.
**TRASH.** `margin/src/components/RowMenu.tsx:153` and `margin-editor/src/components/Sidebar.tsx:46`
agree: `M5 7h14M10 7V5h4v2M7 7l1 13h8l1-13M10 11v6M14 11v6`. `margin-mail/src/ui/icons.ts:41` is a
different drawing: `M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0
0 1-1l1-13M10 11v6M14 11v6`. Wider (4 to 20 rather than 5 to 19) and with rounded corners.
**LINK.** margin `FloatingToolbar.tsx:153` and margin-docs `Toolbar.tsx:192` agree on `l2-2`.
Calendar `EventDetails.tsx:46` and `EventEditor.tsx:42` use `l3-3`, a longer link arm.
**REFRESH.** margin `BackupSettings.tsx:67` is `M21 12a9 9 0 1 1-2.6-6.4M21 4v5h-5`. Calendar
`Header.tsx:23` is `M21 12a9 9 0 11-3-6.7M21 3v6h-6`. Different arc endpoint and a different arrow.
**CHECK.** Shared `icons.ts:61` is `M20 6L9 17l-5-5`, used by margin-docs `WidthMenu.tsx:177` at
size 14. margin-docs also draws its own at `components/Settings.tsx:131`,
`d="M5 12.5l4.5 4.5L19 7"` at size 13. One app, two ticks.
**BOLD and ITALIC.** margin renders letterforms, `B` and `I`
(`FloatingToolbar.tsx:123-124`). margin-docs draws paths, `BOLD_D` and `ITALIC_D`
(`Toolbar.tsx:177-178`). Same toolbar, same button, two different answers to what a bold button is.
### Same drawing, different spelling
These render identically and are only string-level drift, but they are what makes a `grep` for
duplication useless.
- **CLOSE**, five spellings: shared `M6 6l12 12M18 6L6 18`; `M18 6L6 18M6 6l12 12` in calendar
`EventDetails.tsx:38`, margin-docs `Recents.tsx:23`, `Sidebar.tsx:47`, `Toolbar.tsx:194`,
`Settings.tsx:253`, margin `FindBar.tsx:248`; `M18 6 6 18M6 6l12 12` in calendar
`overlayShell.tsx:14`.
- **MOON**: shared `A9 9 0 1 1 11.2 3` versus calendar `A9 9 0 1111.2 3`. Packed arc flags, same
curve.
- **CLOCK**: calendar `EventDetails.tsx:44` `M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0M12 7v5l3 2`
versus mail `icons.ts:46` `M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 7v5l3 2`.
- **CHEVRON_RIGHT**: calendar `M9 18l6-6-6-6` versus mail `M9 6l6 6-6 6`. Drawn from opposite ends.
- **DUPLICATE**: margin `RowMenu.tsx:142` `M9 9h11v11h-11z M6 15V5h9` versus margin-docs
`Sidebar.tsx:42` `M9 9h11v11H9z M6 15V5h9`.
### Exact duplicates that are not in the shared set
`PLUS` (`M12 5v14M5 12h14`) is defined independently in all four apps. `CHEVRON_UP`/`CHEVRON_DOWN`
(`M6 15l6-6 6 6` / `M6 9l6 6 6-6`) three times. Vertical dots (`M12 5h.01M12 12h.01M12 19h.01`),
`MINUS`, `HR`, `IMAGE`, `BLOCKQUOTE` twice each, always margin and margin-docs.
## The icon button: eleven rules for one control
All four share a byte-identical `button` reset (`margin app.css:34`, `calendar app.css:63`,
`docs app.css:34`, `mail app.css:70`, the last adding `font-size: inherit`). On top of it:
| App | Class | Size | Radius | Idle | Hover |
| --- | --- | --- | --- | --- | --- |
| margin | `.icon-btn` (`app.css:103`) | 30 | `--r-sm` | `--ink-soft` | `--accent-wash` |
| margin | `.find-btn` (`app.css:2255`) | 26 | `--r-sm` | `--ink-soft` | `--accent-wash` |
| margin | `.row-menu-btn` (`app.css:393`) | 22 | `--r-sm` | `--ink-faint` | `--accent-wash` |
| calendar | `.icon-button` (`app.css:145`) | 28 | `--r-sm` | `--ink-soft` | `--accent-wash` |
| calendar | `.details-close` (`details.css:269`) | 26 | `--r-sm` | `--ink-faint` | `--accent-wash` |
| docs | `.icon-button` (`app.css:138`) | 28 | `--r-sm` | `--ink-soft` | `--accent-wash` |
| docs | `.find-btn` (`tree.css:585`) | 26 | `--r-sm` | `--ink-soft` | `--accent-wash` |
| docs | `.start-forget` (`tree.css:450`) | 26 | `--r-sm` | `--ink-faint` | `--accent-wash` |
| docs | `.row-menu-btn` (`app.css:363`) | 22 | `--r-sm` | `--ink-faint` | `--accent-wash` |
| docs | `.tree-twisty` (`tree.css:189`) | 16 | `--r-sm` | `--ink-faint` | `--accent-wash` |
| mail | `.button[data-icon-only][data-variant="ghost"]` | 28 via `aspect-ratio: 1` | `--r-sm` | `--ink-soft` | `--accent-wash` |
Every one of them is `display: grid; place-items: center` (except `.start-forget`, which spells it
out as flex, and mail, which is inline-flex) with the same radius token, the same hover wash and
one of two colour tokens. `margin`'s `.row-menu-btn` and margin-docs' `.row-menu-btn` are the same
block copied verbatim into two repos. 26px appears four times across three apps.
The name is the only thing that reliably differs: `.icon-btn` in margin, `.icon-button` in the
other two.
The "on" state is where they genuinely disagree. margin `app.css:118` and margin-docs
`app.css:156` are the same three declarations (`color: var(--accent); background:
var(--accent-wash); box-shadow: inset 0 0 0 1px var(--line-strong)`) under two attribute names,
`data-on="true"` and `data-active="true"`. Calendar `app.css:160` drops the ring and uses
`--ink`. Mail `Button.css:69` makes `[data-active]` identical to `:hover`, so an open panel's
button and a hovered button are the same picture. Four apps, four answers, two of them pixel
identical under different attribute names. Attribute usage is mixed inside every app too: margin
21 `data-on` and 2 `data-active`, calendar 6 and 4, docs 12 and 9, mail 9 and 8.
There is no icon-button component anywhere except `margin-mail`. 54 call sites across the three
older apps hand-write ``: margin 18, calendar 16, margin-docs 20. `margin-mail` has 115 `