mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-04 03:57:03 +00:00
440 lines
29 KiB
Markdown
440 lines
29 KiB
Markdown
# The design system: `@margin/tokens` and `@margin/fonts`
|
|
|
|
Scope: CSS custom properties, the base layer, themes and the six bundled faces. The React primitives
|
|
that consume them are in [ui-kit.md](ui-kit.md); the glyphs are `@margin/icons`. Short names:
|
|
`margin` = `python/margin`, `calendar` = `python/margin-caledar`, `editor` = `rust/margin-editor`,
|
|
`mail` = `rust/margin-mail`, all under `/Users/pj/Workspace/projects`. Line numbers as of 2026-09-06.
|
|
|
|
## Where the four token sets actually are
|
|
|
|
They are not four palettes. Calendar, the only app that does not depend on `margin-shared` at all and
|
|
so had every opportunity to drift, matches `shared/css/tokens.css` on 49 of the 52 values it declares
|
|
in common. Absent are `--font-book`, `--pane-sidebar`, `--measure` and `--sidebar`, which a calendar
|
|
has no use for and no sidebar to paint. One value genuinely differs, `--ink-faint`.
|
|
|
|
So this is not a reconciliation. Three apps already import the shared file
|
|
(margin/src/styles/tokens.css:3, editor/src/styles/tokens.css:5, mail/src/styles/tokens.css:6) and
|
|
layer on top; the fourth is a hand-copy. The work is to move twenty tokens that two or three apps
|
|
each declared separately into the one place, correct one value that is wrong in shared and right in
|
|
two apps, and delete four copies of a reset.
|
|
|
|
What drift exists is almost entirely in margin, the only app with no test suite. Colour literals
|
|
outside each app's token layer: margin 16 hex and 18 rgba, calendar 0 and 1 (a box-shadow at
|
|
app.css:507), editor 0 and 0, mail 0 and 0. Margin's are mostly tokens it never adopted, named under
|
|
each group below.
|
|
|
|
`--ink-faint` is the one that matters, because it is a legibility defect and not a preference.
|
|
Shared's `#9b9484` measures 2.91:1 on `--paper`, 2.56:1 on `--shell` and 2.43:1 on `--sidebar`, and
|
|
the dark `#756d5e` 3.39:1 on `--paper`. Calendar (tokens.css:73, :126) and mail (mail.css:86, :142)
|
|
independently moved to `#6e675b` and `#8e8677`, measuring 5.40 / 4.75 / 4.50 light and
|
|
4.81 / 5.14 / 4.51 dark. Margin and editor still ship the failing value.
|
|
|
|
## The contract for `@margin/tokens`
|
|
|
|
Fifty six names: thirty five are what `shared/css/tokens.css` declares today, twenty one are
|
|
promotions. Every one is declared in both palettes or in neither, because a token defined in light
|
|
and missing in dark is the failure editor's `src/theme.test.ts` catches, and that test moves into the
|
|
package.
|
|
|
|
### Colour
|
|
|
|
| Token | Light | Dark | Status |
|
|
| --- | --- | --- | --- |
|
|
| `--paper` | `#fcfbf7` | `#1d1a16` | shared |
|
|
| `--shell` | `#f1ece2` | `#151310` | shared |
|
|
| `--sidebar` | `#ece6da` | `#1a1713` | shared |
|
|
| `--raised` | `#fbfaf6` | `#232019` | shared |
|
|
| `--ink` | `#23201b` | `#ece6da` | shared |
|
|
| `--ink-soft` | `#6b6458` | `#a89f8e` | shared |
|
|
| `--ink-faint` | `#6e675b` | `#8e8677` | **corrected**, was `#9b9484` / `#756d5e` |
|
|
| `--line` | `#e3ddce` | `#2c2823` | shared |
|
|
| `--line-strong` | `#d6cfbd` | `#3a352d` | shared |
|
|
| `--accent` | `#2a2622` | `#efe9dc` | shared |
|
|
| `--accent-ink` | `#100e0b` | `#ffffff` | shared |
|
|
| `--accent-wash` | `rgba(35, 32, 27, 0.08)` | `rgba(239, 233, 220, 0.12)` | shared |
|
|
| `--accent-contrast` | `#faf7f0` | `#1a1714` | shared |
|
|
| `--selection` | `rgba(35, 32, 27, 0.14)` | `rgba(239, 233, 220, 0.18)` | shared |
|
|
| `--glass` | `rgba(252, 251, 247, 0.86)` | `rgba(33, 30, 25, 0.88)` | shared |
|
|
| `--scrim` | `rgba(35, 32, 27, 0.28)` | `rgba(0, 0, 0, 0.58)` | **promoted** |
|
|
| `--danger` | `#b4453a` | `#d97a6f` | shared |
|
|
| `--danger-ink` | `#963327` | `#e58e84` | shared |
|
|
| `--danger-wash` | `rgba(180, 69, 58, 0.1)` | `rgba(217, 122, 111, 0.16)` | shared |
|
|
| `--danger-contrast` | `#faf7f0` | `#1a1714` | shared |
|
|
| `--hue-1` to `--hue-8` | `#6f6194` `#47705f` `#a06044` `#4e6484` `#856434` `#955767` `#3a6b78` `#667141` | `#a396c8` `#7fae98` `#d19a7c` `#8ba2c4` `#c4a267` `#cb909e` `#75a9b6` `#a2ae77` | **promoted** |
|
|
| `--pdf-page` | `#fff` | `#fff` | **promoted**, identical in both on purpose |
|
|
|
|
`--ink-faint` is the only promotion that changes what margin and editor look like, and it will be
|
|
visible: every date stamp, word count, nav label and field label in both gets darker.
|
|
|
|
`--scrim`: calendar tokens.css:84/:138 and mail mail.css:88/:145 agree exactly; editor tokens.css:26
|
|
has `rgba(0, 0, 0, 0.5)` dark. Two to one, and both of the two wrote the reason down (a light scrim
|
|
over a dark page dims nothing, so panels float on shadow alone). Margin writes the light value into
|
|
`.overlay` at app.css:1275 and a second, unexplained `rgba(35, 32, 27, 0.32)` into `.export-overlay`
|
|
at app.css:1449; both become `var(--scrim)`.
|
|
|
|
The hue ramp is calendar's `--cal-1..8` (tokens.css:106-113, :160-167) and mail's `--hue-1..8`
|
|
(mail.css:114-121, :159-166), the same sixteen hexes under two names. Mail's wins, because `--cal-`
|
|
names a product; calendar renames on adoption, keeping its unrelated `--cal-h`, `--cal-sat` and
|
|
`--cal-fill` HSL derivations in grid.css. `--pdf-page` editor tokenised at tokens.css:22 because it
|
|
was a literal in two stylesheets; margin still has that literal at app.css:1238.
|
|
|
|
Not promoted, one consumer each: mail's `--check` / `--check-tick`, `--note-surface`, `--row-hover`,
|
|
`--row-selected`, `--card-ring`, `--message-*`; calendar's `--grid-*`, `--fold-*`, `--event-*`;
|
|
editor's `--code-*` and `--doc-rule*`; margin's twelve proofing colours at app.css:2367-2451. The
|
|
proofing pair is worth comparing separately, since editor's styles/proofing.css draws the same
|
|
feature out of `--danger`.
|
|
|
|
### Type scale
|
|
|
|
| Token | Value | Status |
|
|
| --- | --- | --- |
|
|
| `--font-ui` | `"Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif` | shared |
|
|
| `--font-book` | `"Literata", Georgia, "Times New Roman", serif` | shared |
|
|
| `--font-heading` | `"Literata", Georgia, "Times New Roman", serif` | shared |
|
|
| `--font-mono` | `ui-monospace, "SF Mono", Menlo, monospace` | **promoted** |
|
|
| `--t-1` | `11px` | shared |
|
|
| `--t-2` | `12px` | shared |
|
|
| `--t-3` | `13px` | shared |
|
|
| `--t-4` | `15px` | shared |
|
|
| `--t-5` | `16px` | **promoted** |
|
|
| `--measure` | `46em` | shared |
|
|
|
|
`--t-5` is identical in calendar tokens.css:34, editor tokens.css:13 and mail mail.css:17, and
|
|
calendar's and mail's comments are nearly word for word the same (iOS Safari zooms a focused field
|
|
under 16px and the locked viewport gives no way back). `--font-mono` is a token only in editor
|
|
(tokens.css:9); calendar writes `ui-monospace, SFMono-Regular, Menlo, monospace` as a literal at
|
|
details.css:183 and rich.css:42, so editor's wins as the one already tokenised.
|
|
|
|
`--font-ui`, `--font-book` and `--font-heading` are the three slots an app may override on the root
|
|
at runtime, and the contract says so: editor writes `--font-book` per document from
|
|
`store/useDocumentFonts.ts:101`, mail writes the other two from a setting in index.html:31-32. It is
|
|
why `.panel-head h2` takes `--font-heading` below.
|
|
|
|
### Spacing
|
|
|
|
There is no spacing scale in any of the four and this plan does not invent one. Padding and gap are
|
|
literals everywhere and inconsistent (panel bodies `20px 22px 24px`, heads `16px 14px 16px 22px`,
|
|
feet `14px 22px`), and what does repeat repeats inside a rule that is itself being shared. A scale
|
|
imposed now would be a rewrite of eighteen thousand lines of CSS with no defect behind it.
|
|
|
|
### Radius
|
|
|
|
| Token | Value | Status |
|
|
| --- | --- | --- |
|
|
| `--r-sm` | `5px` | shared |
|
|
| `--r-md` | `8px` | shared |
|
|
| `--r-lg` | `12px` | shared |
|
|
| `--r-pill` | `999px` | **promoted** |
|
|
|
|
`--r-pill` is identical in calendar tokens.css:8, editor tokens.css:10 and mail mail.css:13: four
|
|
apps, one value, three declarations and nine literals in the fourth.
|
|
|
|
### Shadow
|
|
|
|
| Token | Light | Dark | Status |
|
|
| --- | --- | --- | --- |
|
|
| `--shadow-page` | `0 1px 2px rgba(35, 32, 27, 0.06), 0 14px 30px rgba(35, 32, 27, 0.09)` | `0 1px 2px rgba(0, 0, 0, 0.4), 0 14px 34px rgba(0, 0, 0, 0.5)` | shared |
|
|
| `--shadow-pop` | `0 8px 24px rgba(35, 32, 27, 0.14)` | `0 8px 24px rgba(0, 0, 0, 0.55)` | shared |
|
|
| `--shadow-raised` | `0 1px 2px rgba(35, 32, 27, 0.05)` | `0 1px 2px rgba(0, 0, 0, 0.35)` | **promoted** |
|
|
|
|
Only editor declares `--shadow-raised` (tokens.css:15, :27); margin writes the light value literally
|
|
at app.css:272 and :1839, and calendar's one rgba literal, `0 1px 2px rgba(0, 0, 0, 0.06)` at
|
|
app.css:507, is a fourth alpha for the same effect. Margin's `.card:hover` shadow at app.css:1547 is
|
|
a two layer lift neither token expresses and stays a literal.
|
|
|
|
### Motion
|
|
|
|
| Token | Value | Status |
|
|
| --- | --- | --- |
|
|
| `--ease` | `cubic-bezier(0.22, 0.61, 0.36, 1)` | shared |
|
|
| `--dur` | `120ms` | **new** |
|
|
|
|
`--dur` is the one name here that exists nowhere today, and it is a promotion of a measured constant
|
|
rather than an invented scale: `120ms` appears 54 times in margin's CSS, 64 in calendar's, 83 in
|
|
editor's and 58 in mail's, and every other duration in all four together under 40 times. The shared
|
|
sheets use it; app sweeps happen when somebody next touches the rule.
|
|
|
|
### Layout metrics
|
|
|
|
| Token | Value | Status |
|
|
| --- | --- | --- |
|
|
| `--titlebar-h` | `46px` | shared |
|
|
| `--pane-sidebar` | `248px` | shared |
|
|
| `--traffic-pad` | `84px` | **promoted** |
|
|
| `--touch-h` | `44px` | **promoted** |
|
|
| `--phonebar-h` | `48px` | **promoted** |
|
|
| `--tabbar-h` | `56px` | **promoted** |
|
|
| `--sheet-max-h` | `88dvh` | **promoted** |
|
|
|
|
All five promotions are byte-identical where they exist, with near-identical comments:
|
|
`--traffic-pad` at calendar tokens.css:11, editor tokens.css:11, mail mail.css:36; `--touch-h` at
|
|
calendar tokens.css:22, editor tokens.css:12, mail mail.css:31; the phone trio at calendar
|
|
tokens.css:16-26 and mail mail.css:26-34.
|
|
|
|
Margin has no `--traffic-pad` and hardcodes the lane at app.css:75, `padding: 0 14px 0 84px` on
|
|
`.titlebar`, with no `data-traffic` gate. The other three gate it (calendar app.css:126, editor
|
|
app.css:97, mail screens/header.css:24) and calendar's comment at app.css:121-125 says what the
|
|
ungated version costs: 84px of nothing on Linux, Windows, an iPad and a browser, pushing the centred
|
|
title off centre. Margin also redefines `--titlebar-h` at app.css:2881 as
|
|
`calc(46px + env(safe-area-inset-top))`; it becomes `calc(var(--titlebar-h) + var(--safe-top))`,
|
|
which is calendar's `.titlebar` at app.css:106.
|
|
|
|
Not promoted: `--row-h`, a 48px calendar grid row (calendar tokens.css:44) and a 46px message list
|
|
row (mail mail.css:45). One name, two meanings, and the clearest reason not to promote geometry.
|
|
|
|
### Safe area
|
|
|
|
| Token | Value | Status |
|
|
| --- | --- | --- |
|
|
| `--safe-top` | `env(safe-area-inset-top, 0px)` | **promoted** |
|
|
| `--safe-bottom` | `env(safe-area-inset-bottom, 0px)` | **promoted** |
|
|
|
|
Calendar tokens.css:16-17 and mail mail.css:26-27, identical. The `0px` fallback is what makes a
|
|
desktop read both as zero without a media query; margin and editor read `env()` inline.
|
|
|
|
## The base layer
|
|
|
|
Parsing each app.css into selector and body pairs and comparing bodies exactly, five rules are
|
|
byte-identical in all four with no exceptions: `*` (`box-sizing: border-box`), `html, body, #root`
|
|
(`height: 100%`), the eleven line `body` block, `::selection`, and `:focus-visible`, the same three
|
|
lines at margin app.css:43, calendar app.css:80, editor app.css:51 and mail app.css:92.
|
|
|
|
`@margin/tokens` ships `css/tokens.css`, `css/base.css` and `css/overlay.css` plus a `css/index.css`
|
|
importing all three in that order. `index.css` is what an app imports, first, before anything else;
|
|
the split exists so the audit script can parse the token file alone, not so an app can pick.
|
|
|
|
`css/base.css` holds those five plus the rules below, taking the version named:
|
|
|
|
| Rule | Winner | What the losers are missing |
|
|
| --- | --- | --- |
|
|
| `html` | calendar app.css:37, mail app.css:23 | `-webkit-tap-highlight-color: transparent`, without which a webview reads every tap as the start of a selection |
|
|
| `button` | mail app.css:70 | `font-size: inherit` (margin app.css:34, calendar app.css:63, editor app.css:34 are otherwise identical) |
|
|
| `input, textarea, select` | calendar app.css:72, editor app.css:43, mail app.css:80 | margin has no such rule, so its fields take the webview's own font |
|
|
| `svg { display: block }` | mail app.css:88 | the inline baseline gap under every glyph, patched per component in the other three |
|
|
| `.app` | calendar app.css:86, editor app.css:58, mail app.css:100 | margin app.css:59 still uses `height: 100vh; height: 100dvh` |
|
|
| `:root[data-touch]` selection rules, `:root[data-phone]` field size | calendar app.css:44-60, mail app.css:44-61 | absent in margin and editor |
|
|
| `::-webkit-scrollbar` and its three parts | margin app.css:693-712 | the other three take the webview default |
|
|
|
|
Two need a sentence. On `.app`, calendar's comment at app.css:89-97 explains that UIKit shrinks the
|
|
layout viewport by the safe areas while leaving it anchored at y 0, so a viewport unit lays the tab
|
|
bar out below the clip `body { position: fixed }` imposes, where it is invisible and untappable. The
|
|
scrollbar is 11px, thumb in `--line-strong` with a 3px transparent border and
|
|
`background-clip: content-box`, hover `--ink-faint`, transparent track.
|
|
|
|
`css/overlay.css` holds the box every app builds its dialogs from, the largest near duplicate in the
|
|
four codebases. `.overlay` is byte-identical at calendar app.css:330, editor app.css:451 and mail
|
|
app.css:110, and margin app.css:1271 is the same rule with the scrim written out. `.panel` and
|
|
`.panel-body` are byte-identical in all four (margin app.css:1283, calendar app.css:342, editor
|
|
app.css:463, mail app.css:129). `.panel-foot` is identical at margin app.css:1622, calendar
|
|
app.css:415 and editor app.css:502, and mail app.css:168 adds `flex: none`, which ships.
|
|
`.overlay[data-align="top"] { align-items: start; padding-top: 12vh }` appears at calendar
|
|
palette.css:4, editor palette.css:9 and mail app.css:124; margin has no palette and gains an inert
|
|
rule.
|
|
|
|
Two reconciliations in the head. Editor's `padding: 16px 16px 16px 22px` beats the other three's
|
|
`16px 14px 16px 22px` on editor's own argument at app.css:474-478: on the closing end sits a 28px
|
|
icon button carrying a 16px glyph 6px in from its own edge, so 16 plus 6 puts the cross on the same
|
|
right edge as the body text and the footer buttons, and all four put a close button there. And
|
|
`.panel-head h2` takes `var(--font-heading)`, as calendar app.css:401 and mail app.css:152 do, not
|
|
`var(--font-book)` as margin app.css:1301 and editor app.css:489 do. Both resolve to Literata today,
|
|
but editor overwrites `--font-book` on the root per document, so a document set in EB Garamond
|
|
currently retitles every dialog in EB Garamond, and a dialog title is chrome. Mail's other head
|
|
extras ship: `flex: none`, `gap: 10px`, and `flex: 1; min-width: 0` on the `h2` so a long title
|
|
truncates rather than shoving the close button off the edge.
|
|
|
|
Phone docking is written twice today, identically and with the same comments, at calendar
|
|
app.css:353-390 and mail app.css:174-212: `:root[data-phone]` variants of `.overlay` (z-index 50,
|
|
padding 0, `place-items: end center`), `.panel` (full width, `--sheet-max-h`, top border only, radius
|
|
`var(--r-lg) var(--r-lg) 0 0`, `padding-bottom: var(--safe-bottom)`, `animation: sheet-up 180ms
|
|
var(--ease)`) and `.panel-body` (`overscroll-behavior: contain`), plus `@keyframes sheet-up`. All of
|
|
it ships, including mail's `@media (prefers-reduced-motion: reduce)` guard, which calendar lacks:
|
|
mail has 11 such guards, editor 1, calendar 0 and margin 0, while calendar and margin both animate
|
|
with nothing. Every keyframe in the shared sheets carries a guard, and the apps inherit that with
|
|
it.
|
|
|
|
## Theming
|
|
|
|
`data-theme` on the root, written from JavaScript, never a `@media (prefers-color-scheme)` rule in
|
|
CSS. All four already do this: one decision taken four times, and the right one, since the system
|
|
preference is an input to the setting rather than the setting itself and a user who picked light at
|
|
four in the afternoon keeps it.
|
|
|
|
`color-scheme` is declared, in both shared palette blocks: `light` on
|
|
`:root, :root[data-theme="light"]` and `dark` on `:root[data-theme="dark"]`. Only editor has it today
|
|
(themes.css:24-31), which is why the caret, the native form controls and the default scrollbar come
|
|
out light on a dark page in the other three. Calendar's workaround at create.css:53 and :57 is
|
|
deleted as redundant. Mail's rules on `.msg-frame` at screens/pane.css:249-261 stay: an embedded
|
|
document takes its scheme from the element embedding it, and a sender's HTML is pinned `only light`.
|
|
|
|
The multi-palette design stays local to editor, which ships seven palettes (`light`, `sepia`, `mist`,
|
|
`contrast`, `dark`, `graphite`, `midnight`) in a 323 line themes.css, each block declaring the same
|
|
thirty five properties and enforced by `src/theme.test.ts`. Generalising it means every app's
|
|
stylesheet answering `sepia` and `midnight` or falling through to the light `:root` block, and for
|
|
calendar that is seven variants each of `--grid-*`, `--fold-*`, `--event-*` and the hue ramp, roughly
|
|
140 new declarations for a feature it has not asked for. The mechanism is shared, not the palettes,
|
|
and an app that later wants sepia adds a block and a row in its list.
|
|
|
|
One piece of themes.css does move. Lines 307-322 copy six hexes of the shared palettes so the
|
|
picker's preview tiles can draw them, with a comment saying they are copied because "this repo may
|
|
not reach into" margin-shared. It can, and does. The two shared palette blocks gain
|
|
`.theme-swatch[data-theme="light"]` and `.theme-swatch[data-theme="dark"]` to their selector lists,
|
|
which gives a swatch all twenty colours rather than six and deletes both blocks.
|
|
|
|
The hook lives in `@margin/hooks`, per [repo-layout.md](repo-layout.md), and covers both shapes:
|
|
|
|
useTheme<T extends string>(options: {
|
|
key: string; // "marginmail-theme"
|
|
themes: readonly T[]; // ["light", "dark"], or editor's seven
|
|
schemeOf?: (id: T) => "light" | "dark";
|
|
}): { theme: T; choice: T | "system"; scheme: "light" | "dark";
|
|
setChoice(next: T | "system"): void }
|
|
|
|
It reads `data-theme` off the root first (the boot script has already run), then storage, then
|
|
`matchMedia`. Storage goes through try/catch, because a webview with storage denied should still
|
|
theme and merely forget between launches; editor's theme.ts:105-122 has that and the other three do
|
|
not. It remembers a light half and a dark half so `"system"` lands on the palettes the user actually
|
|
picks, and registers the `matchMedia` listener. Margin, calendar and mail pass a two element list and
|
|
get what they have now plus the storage guard; editor passes seven and its `Preference` type goes.
|
|
|
|
The boot script is the other half and cannot import a bundle, so `@margin/hooks` exports
|
|
`themeBootScript(options)` returning the source as a string, injected by each app's Vite config
|
|
through `transformIndexHtml`. It writes `data-theme`, `data-phone` and `data-touch`, the part
|
|
identical across calendar index.html:12-22 and mail index.html:12-22, and each app appends its own
|
|
lines: calendar's `data-view`, mail's `data-no-pane` and font slots, editor's `data-sidebar` and
|
|
`data-width`, margin's `--measure` and pane widths.
|
|
|
|
## Fonts
|
|
|
|
`@margin/fonts` is today's `shared/src/fonts.ts` (233 lines: `BUNDLED_FONTS`, `FontRef`, `FontPair`,
|
|
`FONT_PAIRINGS`, `fontStack`, `fontsUsed`), `shared/css/fonts.css` (twelve `@font-face` rules over
|
|
six families), `shared/fonts/` (twelve variable TTFs and six OFL notices, 13.8 MB) and
|
|
`shared/bin/sync-fonts.mjs`, moved intact. Nothing about it is wrong. All 18 files in `shared/fonts`
|
|
are byte-identical to the copies in margin, editor and mail's `public/fonts` (verified 18/18 each
|
|
with `cmp`), and calendar's 4 are byte-identical too.
|
|
|
|
The vendored copies stay and the reason narrows. Margin's `src-tauri/src/pdf.rs:9-20` reads all
|
|
twelve variable files with `include_bytes!` from `../../public/fonts`, so the bytes must be at a path
|
|
cargo can see before any npm install has run. Editor no longer does: `src-tauri/src/pdf.rs:33-40`
|
|
reads ten static instances out of `src-tauri/fonts`, and calendar and mail read no fonts from Rust at
|
|
all. The package's comment saying "both apps' pdf.rs" is out of date by one app; the copies now exist
|
|
for margin's exporter and for the webview, which serves them from `/fonts/Literata-VF.ttf`.
|
|
|
|
`sync-fonts` keeps its behaviour, which is right: copy every `.ttf` and `.txt`, or with `--check`
|
|
compare and exit 1 on any difference, reporting rather than deleting a file the app has and the
|
|
package does not (lines 53-59). It gains one flag, `--core`, restricting the set to Literata and
|
|
Hanken Grotesk and their two OFL notices, and `@margin/fonts` exports two stylesheets to match:
|
|
`css/fonts.css` with all twelve faces and `css/core.css` with the four. Otherwise an app declares
|
|
faces it does not vendor and `fonts:check` passes against a `@font-face` pointing at a 404.
|
|
|
|
Calendar is what changes. It has no dependency on the package, no `fonts:sync` or `fonts:check`, no
|
|
way to notice drift, and 31 lines of hand-written `@font-face` in `src/styles/fonts.css` that are
|
|
character for character `shared/css/fonts.css:17-47`. It ships no OFL notices, which is the one real
|
|
problem here, since shipping a face without its licence is a licensing defect. It takes the
|
|
dependency, replaces fonts.css with `@import "@margin/fonts/css/core.css"` and adds the two scripts
|
|
running `--core`, gaining two OFL notices and 4 KB rather than 11.7 MB it has no picker to offer.
|
|
|
|
## Per app
|
|
|
|
**margin.** Deletes tokens.css:1-3 (the import moves to `@margin/tokens/css/index.css`), keeping the
|
|
file for `--pane-dock: 384px` alone, and deletes `src/styles/fonts.css` for
|
|
`@margin/fonts/css/fonts.css`. Deletes app.css:1-58 (the reset, keeping the two app-specific
|
|
`:focus-visible` overrides at :47-57), :59-65 (`.app`), :693-712 (the scrollbar, which moves up),
|
|
:1271-1316 and :1622-1628 (overlay and panel), about 110 lines. Adopts `--scrim`, `--shadow-raised`,
|
|
`--r-pill`, `--traffic-pad`, `--safe-top`, `--pdf-page` and `--dur` in place of 30-odd literals, and
|
|
gains the `data-traffic` gate it never had. `src/theme.ts` goes for the hook. This app changes most,
|
|
being the one that never adopted the last three rounds of shared work.
|
|
|
|
**calendar.** Deletes tokens.css:1-36 and :63-146 (the `:root` block and both palettes), keeping the
|
|
grid geometry and the `--grid-*`, `--fold-*` and `--event-*` blocks: 84 of 168 lines. Renames
|
|
`--cal-1..8` to `--hue-1..8` at its 20-odd call sites. Deletes `src/styles/fonts.css`, app.css:1-99
|
|
and :329-420, plus create.css:53 and :57. Gains a shared dependency for the first time, plus
|
|
`fonts:sync --core` and `fonts:check --core`, and two OFL notices in `public/fonts`.
|
|
|
|
**editor.** Deletes tokens.css:8-28 (the shape and scale block, `--scrim`, `--shadow-raised`,
|
|
`--pdf-page`), keeping the document scale and code surface from :30 down: 21 of 108 lines. Deletes
|
|
app.css:1-62 and :451-508, and themes.css:24-31 and :296-322. Its `src/theme.test.ts` moves into
|
|
`@margin/tokens` as the audit script and gains the other three apps as inputs; `src/theme.ts`
|
|
collapses into the hook, keeping only `THEMES` and the labels. `src-tauri/fonts` is untouched: a
|
|
typesetting decision, in [typesetting.md](typesetting.md).
|
|
|
|
**mail.** Deletes `src/styles/app.css` entirely, all 212 lines, the cleanest single deletion in this
|
|
plan: the file is the reset, the window, the overlay and the phone docking and nothing else. Deletes
|
|
mail.css:10-36 (`--r-pill`, `--t-5`, phone chrome, `--traffic-pad`) and :86-88 and :142-145 (the
|
|
`--ink-faint` and `--scrim` overrides, now upstream), keeping mail geometry, the row surfaces, the
|
|
note surface, `--check`, `--card-ring` and `--message-*`: 120 of 167 lines. tokens.css becomes a two
|
|
line seam and `src/theme.ts` goes for the hook.
|
|
|
|
Every app keeps exactly one app-local token file, and the rule mail's seam already states becomes the
|
|
family rule: nothing outside the token layer may declare a token, and nothing outside it may write a
|
|
literal colour, radius or size.
|
|
|
|
## Order, and what proves each step
|
|
|
|
Nothing starts until mail and editor's 123 uncommitted files each are committed and mail has a
|
|
remote. [risks.md](risks.md) treats that as a precondition and it is.
|
|
|
|
**1. Correct the values in place.** Change `--ink-faint` in `python/margin/shared/css/tokens.css` and
|
|
add the twenty other promotions to that same file, before anything moves repository. The dependency
|
|
is still a symlink here, so margin, editor and mail see it on the next dev restart and each visual
|
|
diff has exactly one cause. Check: run the contrast assertion over every ink-on-surface pair in both
|
|
palettes and require 4.5:1; screenshot margin's chapter list and editor's document tree in both
|
|
themes and confirm the only difference is faint ink getting darker.
|
|
|
|
**2. Land the audit script.** Promote editor's `src/theme.test.ts` into the package. It parses the
|
|
stylesheets rather than rendering them, builds the set of properties each palette declares, and fails
|
|
when a block is short or a `data-theme` block has no `color-scheme`. Extend it with the literal lint
|
|
risks.md asks for: any hex or `rgba(` outside the token layer is a failure. Check: it reports 16 hex
|
|
and 18 rgba in margin, 1 rgba in calendar, 0 and 0 in editor and mail. After step 5, four zeroes.
|
|
|
|
**3. Calendar joins.** Add the dependency, delete its palette blocks, import the shared tokens,
|
|
rename `--cal-*`, take `core.css` and the two font scripts. Check: the audit reports zero drift
|
|
between calendar and shared; `pnpm fonts:check --core` passes; `pnpm test:ui` passes, in particular
|
|
`tests/legibility.spec.ts`, which renders the week grid in both themes and would catch an hour axis
|
|
that lost its contrast. Compare the week grid on a Monday with overlapping events.
|
|
|
|
**4. Extract the base sheet.** `css/base.css` and `css/overlay.css`, one app at a time in the order
|
|
mail, editor, calendar, margin: mail first because its version is the superset and its app.css is a
|
|
clean delete, margin last because it needs the most reconciliation and has no suite to catch a
|
|
mistake. Check per app: mail `tests/shell.spec.ts` and `tests/kit.spec.ts`; editor
|
|
`tests/smoke.spec.ts` and `tests/_rule.spec.ts`; calendar `tests/overlays.spec.ts`,
|
|
`tests/phone.spec.ts` and `tests/touch.spec.ts`. The screen to compare is a dialog over the main view
|
|
at 1280 wide and at 390: mail's Settings sheet over the thread list, editor's Document Setup over an
|
|
open document, calendar's quick create over the week, margin's Settings panel over the chapter list.
|
|
Margin has no Playwright config, so its check is a manual pair of screenshots per theme; giving it a
|
|
suite belongs in [testing.md](testing.md) and should happen before this step rather than after.
|
|
|
|
**5. Land the scrollbar and `--dur`.** Separately from step 4, because the scrollbar is the one base
|
|
rule that changes layout: an 11px classic scrollbar where the webview drew an overlay one takes 11px
|
|
from whatever it is inside. On macOS it only shows under "Show scroll bars: Always", which is why
|
|
nobody noticed margin had it. Check: screenshot every scrolling surface with that setting forced on,
|
|
specifically mail's virtualised list, calendar's grid and editor's tree.
|
|
|
|
**6. Rename and repackage.** `margin-shared` becomes `@margin/tokens` and `@margin/fonts` in the new
|
|
repository and all four apps move from a `file:` path to a version. This changes no CSS and should
|
|
produce no visual diff at all, which is the point of doing it last. Check: the audit and all three
|
|
suites pass unchanged, and `pnpm install` succeeds in a fresh clone of each app with no sibling
|
|
checkout present, which is the failure that motivated the whole exercise.
|
|
|
|
## What stays per app
|
|
|
|
Editor keeps its document scale (`--doc-h1` through `--doc-h6`, `--doc-size`, `--doc-lead`), its four
|
|
measures, its sheet padding, its code surface and its six syntax hues, and it keeps themes.css. Those
|
|
51 tokens describe a page of prose, and a calendar and a mail client have no page.
|
|
|
|
Mail keeps its geometry (`--list-w`, `--row-h`, `--avatar`, `--pile-h`, `--compose-w`, `--feed-w`),
|
|
its row surfaces, `--note-surface`, `--check`, `--card-ring` and the five `--message-*` values. The
|
|
`--message-*` set must not be promoted: it is the light palette's values written a second time,
|
|
deliberately outside both theme blocks, because HTML mail is authored for a white page and a
|
|
newsletter that sets a dark text colour and no background is unreadable on a dark surface. A shared
|
|
token that ignores the theme would contradict the contract.
|
|
|
|
Calendar keeps its grid geometry and the `--grid-*`, `--fold-*` and `--event-*` palettes, plus the
|
|
per-event HSL derivations at grid.css:184-223, a colour system inside a colour system that exists
|
|
because a calendar draws dozens of tinted blocks at once. Margin keeps `--pane-dock: 384px`, the
|
|
proofing colours at app.css:2367-2451, and the device frame's `--dv-*` set, which comes from
|
|
`components/DeviceFrame.tsx:22-24` rather than from CSS at all.
|
|
|
|
And the thing not to share at all: geometry by name. A shared `--row-h` would be a token every
|
|
consumer overrides, which is a name with no value in it, and the next reader would reasonably assume
|
|
the calendar grid and the mail list were meant to line up.
|