# 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 `