mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-05 04:27:03 +00:00
381 lines
25 KiB
Markdown
381 lines
25 KiB
Markdown
# 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.
|