spaces: Save was off the screen, and every block paid a row for its gutter - #287
Merged
Conversation
…utter
Two mobile layout defects, both measured on the shipped shell at 390×844 with
a coarse pointer, neither of them a regression — each had been there since the
surface it belongs to shipped.
THE TOPBAR OVERFLOWED AND CLIPPED THE PRIMARY ACTION.
`.sp-bar` measured 390px wide with a scrollWidth of 467, and the Save button's
right edge landed at x = 426 — 36px past the edge of the screen, on the one
control that must never be unreachable. The existing fold (the six secondary
actions into ⋯) was not enough: what survives it is still eleven controls'
worth of 40px touch targets.
The fix is the same mechanism, applied twice, and never a scroller:
· the fold now starts at the DRAWER breakpoint (820px) rather than 720.
Measured at 768 — an iPad in portrait — the bar laid out 795px and the save
caret ended 27px off the screen. 721–820 is exactly the band where the page
list is already an overlay competing for width.
· + Insert drops its word and keeps its icon (76.6px → 40px). The word is a
span now, not a bare text node, so it can be dropped at all.
· below 600px a phone also folds away the wordmark (About is the first item
in ⋯), the undo/redo pair (added to ⋯, carrying their shortcuts and their
disabled state) and the save caret (all four of its items are in ⋯ or in
About, and Save a copy / Export as Markdown are appended there).
· the status span leaves the flow on a phone, and status() clears its text
after the fade instead of only fading it. It is nowrap: "Reading view —
press Esc or the eye to edit" is ~250px, and once written the width was
held for the rest of the session.
bar scrollWidth vs clientWidth, and Save's right edge:
320 before 467 / 320, Save 425.6 after 320 / 320, Save 308
375 before 467 / 375, Save 425.6 after 375 / 375, Save 363
390 before 467 / 390, Save 425.6 after 390 / 390, Save 378
768 before 795 / 768, caret 794.6 after 768 / 768, caret 756
1280 before 1280 / 1280 after 1280 / 1280 (unchanged)
The document title beside it goes 18px → 54 (320), 18 → 109 (375),
18 → 124 (390), 18 → 240 (768).
EVERY BLOCK GOT A FULL GUTTER ROW.
The gutter stays on touch because there is no hover, but the rule that made it
visible also made it `position: static` — in the flow, 34px tall plus margin,
on every block. A one-line paragraph measured 68.4px, half of it affordances.
It is absolutely positioned again, the way it is on a desktop, inside a 44px
start padding on `.sp-main` reserved for it: visible at rest, out of the flow,
and carrying ONE control — the grip, whose bottom sheet already offers "Add
below", so the + was a second button for something a thumb could already
reach. Verified by tap: the grip is hit-testable at 320/375/390 and opens the
sheet with all six block actions.
one-line paragraph 68.4px → 32.4px
reading column 354px → 328px at 390 (the width it costs)
gutter static, 354×34, in flow → absolute, 34×32, at x = 4
Desktop is untouched: at 1280 the gutter is still hover-revealed with both
buttons, `.sp-main` still pads 28px, and the bar still carries the wordmark,
the secondary row, the Insert label and the save caret.
Shell 132,102 → 132,414 B, inside the existing 135,168 B ceiling (98.0%); no
budget change. Rigs updated to pin which TIER each fold lives in, that
isPhone() agrees with the stylesheet, and that the touch gutter is never put
back in the flow.
Follow-up to the previous commit, found by measuring an rtl DOCUMENT rather
than an rtl interface.
`.sp-main` is chrome and follows the interface direction. The gutter is
anchored to a block, and blocks follow the DOCUMENT's direction — renderPage
puts `theme.dir` on `.sp-page-inner` and pins the outer wrapper to ltr. So
reserving the gutter's margin as `padding-inline-start` on `.sp-main` sent the
padding one way while an rtl document sent the gutter the other: measured at
390px on a doc with `theme.dir: 'rtl'`, the gutter landed at x = 378…412 —
22px past the viewport — and `.sp-main` scrolled to 412 against a 390 client
width. A horizontally scrolling document is exactly the thing that makes a
phone unusable.
Reserved on `.sp-page-inner` instead, where it flips with the blocks it is for.
rtl at 390 gutter 378…412, main scrollWidth 412 → gutter 352…386, 390
ltr at 390 gutter x 4, block 328 wide, paragraph 32.4px — unchanged
ltr at 320 gutter x 4, block 258 wide — unchanged
ltr at 375 gutter x 4, block 313 wide — unchanged
ltr at 768 gutter x 4 → 10, block 706 → 694 (the page centres inside its
720px measure here, so the padding lands inside the column)
desktop 1280 untouched: no inner padding, .sp-main still 28px, gutter still
hover-revealed with both buttons
Shell unchanged at 132,414 B. The model rig now pins WHICH element reserves the
margin, and that the scroller does not.
Owner
Author
|
Follow-up commit
Shell unchanged at 132,414 B. The model rig now pins which element reserves the |
# Conflicts: # docs/DECISIONS.md
nyblnet
added a commit
that referenced
this pull request
Aug 18, 2026
The topbar fold (#287) and the view controls both added a FIFTH parameter to `menuItem`, and they mean different things: `off` is a command that exists but cannot run right now — undo/redo folded into ⋯ on a phone, disabled rather than hidden so the menu does not change shape as you edit — and `selected` is the choice a view is currently on. Taking either side would have silently dropped the other's five call sites into a boolean that no longer meant what they think. They are `state: { off?, selected? }` now, defaulted to `{}`, and the five call sites say which they mean: `{ off: !this.store.canUndo }`, `{ selected: f.key === now }`. A row can be neither and nothing yet is both; the object is what stops the next fifth meaning colliding too. Verified in the built shell rather than by typecheck alone — both meanings, in one build: the ⋯ menu carries `Undo (⌘Z)` and `Redo (⇧⌘Z)` with `sp-off` and `aria-disabled="true"`, and the tracker's Group menu marks `Status` with `sp-sel` and `aria-current="true"` while `Priority` carries neither. SIZE CEILING 140 → 144 KiB, and this commit is the one that needs it. With magic notes, daily notes and the mobile fold already in, the shell measures 142,674 B — 99.5% of the old ceiling, 686 B of headroom. That is not enough to ship: the payload is mostly a zlib block and zlib differs across node versions, and a past commit measured 130,095 B locally against 131,246 B on CI. This would have passed here and failed there. Gates: model 436/436 · agent 143/143 · calc 90/90 · journal 45/45 · i18n complete (238 × 8) · shell-gate OK · tsc clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two pre-existing mobile layout defects in
spaces/. Both measured on theshipped shell at a 390×844 viewport with a coarse pointer; both reproduce on
older builds, so neither is a regression.
1 — the topbar overflowed and clipped the primary action
.sp-barmeasured 390px wide with ascrollWidthof 467, and Save's rightedge landed at x = 426 — 36px past the screen edge, on the one control that
must never be unreachable. The existing fold (six secondary actions → ⋯) was
not enough: what survives it is still eleven controls' worth of 40px touch
targets.
Same mechanism, applied twice — no scroller, no smaller type:
(an iPad in portrait) the bar laid out 795px and the save caret ended 27px
off the screen; 721–820 is exactly the band where the page list is already an
overlay competing for width.
span now rather than a bare text node, so it can be dropped at all.
in ⋯), the undo/redo pair (added to ⋯ carrying their shortcuts and their
disabled state) and the save caret (all four of its items are in ⋯ or in
About; Save a copy / Export as Markdown are appended there).
status()clears its textafter the fade instead of only fading it. It is
nowrap— "Reading view —press Esc or the eye to edit" is ~250px, and once written the width was held
for the rest of the session, which was enough on its own to push Save back off
the screen.
The document title beside it goes 18px → 54 (320), 18 → 109 (375), 18 → 124
(390), 18 → 240 (768).
2 — every block got a full gutter row
The gutter stays on touch because there is no hover, but the rule that made it
visible also made it
position: static— in the flow, 34px tall plus margin,on every block. A one-line paragraph measured 68.4px, half of it
affordances.
It is absolutely positioned again, the way it is on a desktop, inside a 44px
start padding on
.sp-mainreserved for it: visible at rest (nothing todiscover it with on touch), out of the flow, carrying one control — the
grip, whose bottom sheet already offers "Add below", so the + was a second
button for something a thumb could already reach.
static, 354×34, in flowabsolute, 34×32, at x = 4Options weighed: shrinking it in place still leaves a row; reveal-on-tap hides
the only affordance a block has until you have already guessed it exists. The
margin is the only one that costs 0px of height and stays visible, and height
is the scarce axis on a phone.
Reachability verified by tap, not by eye: at 320/375/390 the grip is
hit-testable at its centre and opens the sheet with all six block actions
(Turn into…, Add below, Move up, Move down, Duplicate, Delete).
Verification
Measured in a real browser on the built single-file shell (a uniquely named
copy per build — that pane caches file URLs), at 320, 375, 390, 768 and 1280,
with the file path asserted in every measurement.
.sp-barscrollWidth === clientWidthat all five widthsinnerWidthat all five; no page-level horizontaloverflow (
documentElement.scrollWidth === innerWidth) on any pageboth buttons,
.sp-mainpadding 28px, wordmark / secondary row / Insertlabel / save caret all present, status static
Commands:
tsc -b,npm run build:single,scripts/shell-gate.mjs,test-spaces-model.ts(413/413),test-spaces-agent.ts(143/143),test-spaces-undo.ts(18/18),test-spaces-size.mjs,build-spaces-i18n.mjs --check.Size: 132,102 → 132,414 B (+312), inside the existing 135,168 B ceiling at
98.0%. No budget change, and no new user-visible strings — undo/redo reuse the
existing
Undo (⌘Z)/Redo (⇧⌘Z)keys and the folded save items reuse thecaret menu's, so all eight catalogs are already complete (
--checkpasses).Rigs updated so this cannot quietly come back:
test-spaces-model.tsnowpins which tier each fold lives in (rather than a hardcoded 720), that
isPhone()agrees with the stylesheet's phone breakpoint, and that the touchgutter is never put back in the flow.
Platform invariants: none touched — no format, splice, collab or kernel
change.
docs/DECISIONS.mdrecords the two breakpoint tiers and the touchgutter rule.
Not done
a 48px margin, which is 5% of a 390px screen taken from the prose; 34px is
the compromise, and the sheet it opens is full-size.
.sp-props) keep their gutters, which now overlay aneighbouring chip slightly — exactly what they already do on a desktop on
hover. Suppressing them there would remove the only way to reorder or delete
a field on a phone, so it is left alone.
a toast. A toast is a bigger surface than this PR should introduce.