some more fixes

This commit is contained in:
pj committed 2026-10-03 22:48:49 +05:30
1 parent 7a3c04f170
commit 2b86a407ba
63 files changed
+12172 -44

No files matched your search

+380
View File
@@ -0,0 +1,380 @@
# 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 ?? <path d={d} />}`. 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 `<Icon d=... />`, 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>B</b>` and `<i>I</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 `<button className="icon-btn|icon-button" title=... onClick=...><Icon
d={...} /></button>`: margin 18, calendar 16, margin-docs 20. `margin-mail` has 115 `<Button>`
usages and 16 `iconOnly` ones, and `Button.tsx:57` supplies the accessible name automatically:
`aria-label={label ?? (iconOnly ? title : undefined)}`.
The floating toolbar button is forked in the worst way. `margin app.css:893` `.tool` and
`margin-editor app.css:623` `.tool` are the same rule except one writes `border-radius: 999px` and
the other `border-radius: var(--r-pill)`. The same literal-versus-token split repeats on
`.editor-toolbar` (`margin app.css:889` vs `docs app.css:619`). The JS helpers differ only in
arity: `margin/src/editor/FloatingToolbar.tsx:109` is a render-scoped arrow with four positional
params; `margin-editor/src/editor/Toolbar.tsx:156` is a module function with the same four plus
`disabled`. Both carry the identical `onMouseDown={(e) => e.preventDefault()}`.
## Focus, disabled, tooltips, badges, spinners, keycaps
**Focus rings** are the one thing all four already agree on, byte for byte:
`:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }` at
`margin app.css:43`, `calendar app.css:80`, `docs app.css:51`, `mail app.css:92`. Nobody ships a
polyfill or does keyboard-versus-mouse detection. The divergence is in the exceptions: 18 sites
across the suite write `outline: none` with no replacement, and only `margin-mail` invents a second
ring colour (`screens/settings.css:467`, `outline: 2px solid var(--accent-wash)` at 1px offset).
The round-control case is handled twice and missed once: calendar `create.css:445` and docs
`toolbar.css:157` both use a two-layer box-shadow (`0 0 0 2px var(--paper), 0 0 0 3px
var(--accent)` and `0 0 0 1.5px ...` respectively, radii disagree), while mail's 15px round swatch
has no focus rule and gets the square outline that calendar's comment at `create.css:435` warns
about. `/Users/pj/Workspace/projects/python/margin/src/focus.ts` is the only focus-trap module in
the family; the other three have none.
**Disabled** has no agreement at all: five opacity values across four apps. margin uses 0.4, 0.5
and 0.6 in one file; calendar uses 0.45; margin-docs uses 0.4, 0.45 and 0.5; margin-mail mostly
abandons opacity for `color: var(--ink-faint); background: var(--raised)`
(`ui/Button.css:84`), which is the same recipe calendar reached independently at
`overlays.css:85`. Only one site in the suite pairs `:disabled` with `pointer-events: none`
(`docs export-preview.css:75`).
**Tooltips** do not exist as a component in any app. All four use the native `title` attribute:
44, 40, 66 and 71 occurrences. margin is the outlier on labelling, 44 `title` against 5
`aria-label`, so most of its icon buttons are unnamed to a screen reader; the other three run
21/43/42. The text generator is forked: margin-docs `Titlebar.tsx:105` `shortcutTitle(id)` returns
a whole string, calendar `Header.tsx:33` `hint(command)` returns a leading-space suffix, and margin
hardcodes `title="Find (⌘F)"` (`EditorView.tsx:294`) and `"Link (⌘K)"`
(`FloatingToolbar.tsx:153`), which are not platform aware.
**Badges** share one recipe and disagree on every number: a wash-tinted micro chip at
`padding: 1px 5|6|9px; background: var(--accent-wash); color: var(--ink-faint); font-size:
var(--t-1)`, in calendar `details.css:93`, `overlays.css:487`, `agenda.css:90` and mail
`tour.css:127`, with the radius `--r-sm` in calendar and `--r-pill` in mail. Status dots come in
5, 6, 7, 8 and 9px, and `border-radius: 50%` and `var(--r-pill)` are both used within one repo
(`docs app.css:132` vs `toolbar.css:311`). margin and margin-docs share three copy-pasted classes
verbatim: `.dirty-dot`, `.preview-count`, `.find-count`. `font-variant-numeric: tabular-nums` on
counts is used by all four.
**Loading** is the deepest split, and it is a product decision rather than an oversight. margin
and margin-docs have rotating spinners (`margin app.css:1461` `.spinner`, plus a byte-identical
`backup-spin` duplicate of `spin` at `:2711`; `docs export-preview.css:139` `.preview-spinner`).
`margin-calendar` has no spinner, no skeleton and no loading keyframes at all; it expresses
pending state as `[data-busy]` and `[data-pending]` on the content itself. `margin-mail` bans
spinners in three separate comments and uses bars and skeletons instead. Reduced motion is handled
in three different ways: margin has no guard at all, docs slows the spinner from 0.7s to 2.4s, mail
disables outright in five places. Do not try to unify this; the four apps mean different things.
**Keycaps.** calendar `palette.css:104` `.key` and mail `ui/Key.css:1` + `[data-size="md"]` are the
same chip: bordered, `--raised`, `--r-sm`, `--t-1`, `min-width: 20px`, `padding: 2px 6px`,
`line-height: 1.4`. margin-docs `tree.css:744` `.key-cap` is a different chip, wash-filled with no
border at `--t-2` and weight 600. margin has no chip, one rule
(`app.css:2191` `.esc-hint kbd`). Only `margin-mail` has a `Key` component
(`ui/Key.tsx:15`), only mail puts a cap on ordinary buttons, and only mail hides caps on phones
(`Key.css:28`). Underneath, `keys/bindings.ts` in calendar, docs and mail declare identical `isMac`
and `PRIMARY_LABEL` lines and an identical eight-entry `NAMED` map, then implement `keyLabel()`
three different ways: calendar (`:110`) cannot express `⌘⇧F` at all, docs (`:243`) infers shift
from case, mail (`:579`) treats shift as a first-class modifier. margin has no bindings table.
## Titlebar and window chrome
All four are Tauri v2 with `"titleBarStyle": "Overlay"` and native traffic lights. Nobody draws
window controls, nobody sets `decorations`, `hiddenTitle`, `transparent` or `macOSPrivateApi`, and
nobody uses `startDragging` or `-webkit-app-region`; every drag region is the
`data-tauri-drag-region` attribute.
The `.titlebar` rule is the same nine declarations in all four
(`margin app.css:67`, `calendar app.css:102`, `docs app.css:76`, `mail header.css:7`):
`flex: none; position: relative; z-index: 45; height: var(--titlebar-h); display: grid;
grid-template-columns: 1fr auto 1fr; align-items: center; background: var(--shell); border-bottom:
1px solid var(--line)`. Differences: margin and calendar and mail set `user-select: none`, docs
does not; calendar folds `--safe-top` into the height and padding.
The lane for the traffic lights is 84px in all four and is reserved three different ways.
`margin app.css:74` hardcodes it in `padding: 0 14px 0 84px`, unconditionally, with no token and no
platform gate, so Linux and Windows get a dead 84px lane. Calendar (`app.css:126`) and mail
(`header.css:24`) put `padding-left: var(--traffic-pad)` on the row under `:root[data-traffic]`.
margin-docs puts it on the child instead, `:root[data-traffic] .titlebar .lead { margin-left:
calc(var(--traffic-pad) - 14px) }` (`app.css:97`), with a comment explaining that padding on the
row pushed the centred title 35px right of the middle. Calendar's view switcher and mail's
`<Segment>` are both in centre columns and are subject to exactly that offset.
Only margin-docs has native code. `/Users/pj/Workspace/projects/rust/margin-editor/src-tauri/src/titlebar.rs`
resizes the `NSTitlebarContainerView` on `Resized`, `Focused` and `ThemeChanged` so the lights
centre in a 46px row, with a `const TITLEBAR_H: f64 = 46.0` at line 79 that duplicates
`--titlebar-h: 46px` from `shared/css/tokens.css:24`. Calendar and mail instead set
`"trafficLightPosition": { "x": 9, "y": 25 }` in `tauri.conf.json` and never reapply. margin does
neither, so its lights sit at the macOS default, roughly 7px high in a 46px row, which is the
misalignment `titlebar.rs` was written to fix.
`--traffic-pad: 84px` is declared four times (`calendar tokens.css:11` and `:60`,
`docs tokens.css:11`, `mail mail.css:36`) and is not in `margin-shared`. So is
`--r-pill: 999px` and `--touch-h: 44px`, three copies each. margin declares none of them and
inlines the literals.
## What to share, and what not to
**Share, high confidence:**
1. `Icon` itself. Three byte-identical copies plus one strictly better fourth. Move
`margin-mail`'s version (className, `aria-hidden`, exported props type) to
`shared/src/Icon.tsx` with `react` as a peer dependency. The stated reason not to has no
mechanical basis.
2. The two lines of alignment that go with it: `svg { display: block }` and `.icon { flex: none }`,
as `shared/css/icon.css`. This is the fix that has been made five different ways in four repos
and is the direct answer to "I keep fixing icon alignment separately."
3. The rest of the glyphs. Promote `PLUS`, `CHEVRON_UP/DOWN/LEFT/RIGHT`, vertical dots, `MINUS`,
`HR`, `IMAGE`, `BLOCKQUOTE`, `TRASH`, `LINK`, `REFRESH`, `CLOCK`, `DOCUMENT`, `COPY`,
`EXTERNAL`, `BOLD`, `ITALIC`, `HEADING`, `BULLET_LIST` into `shared/src/icons.ts`, picking the
better drawing where they have drifted (margin-docs' `HEADING_D` and `BULLET_LIST_D`, the
margin/margin-docs `TRASH` and `LINK`, the shared `SEARCH` and `MORE` and `SUN`). Then delete
every inline `d="M..."` from JSX. This turns 151 definitions into roughly 60.
4. `--r-pill`, `--touch-h` and `--traffic-pad` into `shared/css/tokens.css`. Three copies each of a
single number, and in `--traffic-pad`'s case a number that the Rust in one repo has to agree
with.
5. The keycap. Move `margin-mail`'s `Key.tsx` and `Key.css`; calendar's `.key` is already the same
chip, and margin-docs' `.key-cap` is a divergence that should be resolved rather than kept.
6. `keyLabel`, `normalizeCombo`, `PRIMARY_LABEL` and the `NAMED` map. Three near-identical
implementations of the same twenty lines with three different bugs. `margin-mail`'s is the
correct one. This is not strictly a UI primitive, but it is why the caps and titles disagree.
**Share, but the shape needs deciding first:**
7. The icon button. Eleven rules for one control is the clearest duplication in the audit, but
`margin-mail`'s `Button` bundles size, variant, keycap and icon into one component, while the
other three want a flat class they can put on any element. The tractable move is to share the
CSS (a `.icon-button` at 28px with `--r-sm`, `--accent-wash` hover, and a settled `[data-on]`
ring) and let each app keep its own JSX for now. Renaming margin's `.icon-btn` and settling on
one of `data-on` or `data-active` is a prerequisite either way.
8. The `.titlebar` grid rule and the traffic lane. The nine declarations are common; the lane
mechanism is not, and margin-docs' child-margin version is the correct one. `titlebar.rs`
belongs in a shared Rust crate eventually, but that is a bigger move than this audit covers.
**Do not share:**
- Spinners and loading states. margin spins, calendar refuses to have any loading affordance,
margin-mail bans spinners on the record. These are four different product positions, not four
copies of one decision.
- Badges, chips and pills. The wash-chip recipe recurs, but every app's numbers are tuned to its
own density (a calendar all-day chip is a layout unit, not a badge). Sharing the tokens is
enough.
- Focus ring exceptions. The global rule is already shared through the tokens; the 18 `outline:
none` sites are each local judgement calls, and margin-docs is the only app that writes down why.
- The floating editor toolbar. It exists in two apps only, and its `.tool` is a different control
from `.icon-button` (a min-width pill that holds a letterform as often as a glyph). Worth
de-duplicating between margin and margin-docs, not worth putting in a package the calendar and
mail apps import.
**Prerequisite for all of it:** `margin-calendar` has to depend on `margin-shared`. It is one line
in its `package.json` (`"margin-shared": "file:../margin/shared"`) and deleting its hand-copied
`tokens.css` values. Every calendar-specific divergence in this document, `SEARCH`, `MORE`, `SUN`,
`MOON`, `LINK`, `REFRESH`, the duplicated palette, follows from the fact that it never joined.