Replaces #36 and #72, which asked for the same thing from two ends and disagreed about the answer.
density is comfortable | compact and drives four variables — and they are all gaps:
| variable |
comfortable |
compact |
what it spaces |
--space-card |
14 px |
10 px |
the padding inside a card |
--space-grid |
12 px |
8 px |
the gap between cards |
--space-section |
28 px |
18 px |
the gap between "Pinned" / "Today" / … |
--space-canvas |
24 px |
16 px |
the margin around the whole canvas |
What does not move: height: 150px, hard-coded on the card and again on the ghost card; minmax(220px, 1fr) on the grid; and three TypeScript constants, SNIPPET_LINES = 3, MAX_VISIBLE_TAGS = 2, MAX_VISIBLE_ITEMS = 2.
So compact gives you the same cards, showing the same three lines, packed closer together. You gain cards on screen, never content per card. There is no answer anywhere in the panel to "show me more of the note".
The decision: one knob, not two
#36 wanted a separate cardSize setting and argued density must stay about gaps. #72 argued, two paragraphs into itself, that "three knobs that all change how much fits on screen is one more than a user can hold in their head". The second is right, and it settles the first: nobody wonders whether they want fewer gutters or fewer lines. They want to see more, or less. That is one axis.
So density gains a third step and stops being only about gaps. A step decides the gaps and the card's height and what fits inside it. Gmail has shipped exactly this for fifteen years.
What a step has to drive, together
⚠️ Everything below derives from the one step, or it breaks:
- the four gap variables, as today;
height on the card and on the ghost card, which repeats the 150 px;
SNIPPET_LINES, MAX_VISIBLE_TAGS, MAX_VISIBLE_ITEMS;
- probably the grid's
minmax(220px, 1fr) — a taller card that stays 220 px wide is a narrow column, not a bigger card.
The clipping is silent, and that is the trap. .card-items is overflow: hidden, so items that no longer fit simply vanish — while hiddenItemCount keeps counting from MAX_VISIBLE_ITEMS, so the "+N more" badge states a number that does not match what is on screen. Raise the height without raising the constant and the card lies.
Good news on the layout side: .card-items is anchored to the bottom (inset: 0 plus justify-content: flex-end), precisely because what precedes it has no fixed height. A variable card height does not disturb it; only the hard-coded 34px footer band in its padding is worth a second look.
⚠️ This is no longer "one line and four CSS variables"
The three constants are read in TypeScript, not CSS, so the step has to reach the component as a signal — SettingsStore.density() already is one — rather than only as a :root variable. The comment above the density block in styles.scss currently asserts the opposite ("four variables rather than one global factor") and has to be rewritten with it.
Keep what makes it cheap on the settings side: a field on AppSettings, one SettingsStore.setting(key, codec) line, and the CSS variables on :root in styles.scss — not a :root block inside a component's SCSS, which Angular's emulated encapsulation rewrites into a selector that never matches <html>.
Labels
"Comfortable / Compact" describes spacing, and stops being true once the step changes content. Three steps need three names that say how much of a note you see, not how tight the padding is.
Order
After #71. That one collapses the badge row into the title line and frees about two lines of body at unchanged height. Sizing a "large" step against a card still paying a full line for its language badge would be sizing against a layout that is about to change.
Watch the canvas while measuring
More lines per card means more DOM and more highlight.js work per card, on a canvas that renders every note with no virtualisation. The largest step is where that starts to be felt, and it is worth measuring rather than assuming.
Done when: the panel offers one choice that visibly changes how much of a note is on screen, every dependent value follows from that single step, no item is clipped without the badge agreeing, and the labels say what the choice does.
Replaces #36 and #72, which asked for the same thing from two ends and disagreed about the answer.
densityiscomfortable | compactand drives four variables — and they are all gaps:--space-card--space-grid--space-section--space-canvasWhat does not move:
height: 150px, hard-coded on the card and again on the ghost card;minmax(220px, 1fr)on the grid; and three TypeScript constants,SNIPPET_LINES = 3,MAX_VISIBLE_TAGS = 2,MAX_VISIBLE_ITEMS = 2.So compact gives you the same cards, showing the same three lines, packed closer together. You gain cards on screen, never content per card. There is no answer anywhere in the panel to "show me more of the note".
The decision: one knob, not two
#36 wanted a separate
cardSizesetting and argued density must stay about gaps. #72 argued, two paragraphs into itself, that "three knobs that all change how much fits on screen is one more than a user can hold in their head". The second is right, and it settles the first: nobody wonders whether they want fewer gutters or fewer lines. They want to see more, or less. That is one axis.So density gains a third step and stops being only about gaps. A step decides the gaps and the card's height and what fits inside it. Gmail has shipped exactly this for fifteen years.
What a step has to drive, together
heighton the card and on the ghost card, which repeats the 150 px;SNIPPET_LINES,MAX_VISIBLE_TAGS,MAX_VISIBLE_ITEMS;minmax(220px, 1fr)— a taller card that stays 220 px wide is a narrow column, not a bigger card.The clipping is silent, and that is the trap.
.card-itemsisoverflow: hidden, so items that no longer fit simply vanish — whilehiddenItemCountkeeps counting fromMAX_VISIBLE_ITEMS, so the "+N more" badge states a number that does not match what is on screen. Raise the height without raising the constant and the card lies.Good news on the layout side:
.card-itemsis anchored to the bottom (inset: 0plusjustify-content: flex-end), precisely because what precedes it has no fixed height. A variable card height does not disturb it; only the hard-coded34pxfooter band in its padding is worth a second look.The three constants are read in TypeScript, not CSS, so the step has to reach the component as a signal —
SettingsStore.density()already is one — rather than only as a:rootvariable. The comment above the density block instyles.scsscurrently asserts the opposite ("four variables rather than one global factor") and has to be rewritten with it.Keep what makes it cheap on the settings side: a field on
AppSettings, oneSettingsStore.setting(key, codec)line, and the CSS variables on:rootinstyles.scss— not a:rootblock inside a component's SCSS, which Angular's emulated encapsulation rewrites into a selector that never matches<html>.Labels
"Comfortable / Compact" describes spacing, and stops being true once the step changes content. Three steps need three names that say how much of a note you see, not how tight the padding is.
Order
After #71. That one collapses the badge row into the title line and frees about two lines of body at unchanged height. Sizing a "large" step against a card still paying a full line for its language badge would be sizing against a layout that is about to change.
Watch the canvas while measuring
More lines per card means more DOM and more highlight.js work per card, on a canvas that renders every note with no virtualisation. The largest step is where that starts to be felt, and it is worth measuring rather than assuming.
Done when: the panel offers one choice that visibly changes how much of a note is on screen, every dependent value follows from that single step, no item is clipped without the badge agreeing, and the labels say what the choice does.