Skip to content

Add the mobile vocabulary (Phase 30 slice D) - #237

Merged
vicmaster merged 4 commits into
masterfrom
phase-30-slice-d
Nov 1, 2026
Merged

vicmaster merged 4 commits into
masterfrom
phase-30-slice-d

Conversation

@vicmaster

Copy link
Copy Markdown
Owner

Phase 30 slice D. Slice C gave a phone screen rules, but nothing to build one with: no status bar, no tab bar, no pinned positioning, and a root that rendered at a fixed height, so a long screen ended under the home indicator. This PR adds the mobile vocabulary and fixes the root render.

The root grows with its content

The document root keeps its artboard height and now grows when the content is taller: it renders height: <H>px; min-height: max-content. Its bottom padding, safeArea included, now follows the last child, and children keep their authored heights. Before this, a long screen ended inside the home-indicator inset, and sixteen 56px rows rendered 48px tall. A design that fits renders pixel-identical to before. A root child with height: "100%" fills whatever its siblings leave, so a web page's nav + main layout still fills the artboard. A root that sets overflow or minHeight keeps the author's choice.

Pinned positioning

position gains "fixed" (pinned to the screen) and "sticky" (sticks at its offset while its container scrolls), and nodes gain bottom and right offsets beside x and y. Fixed offsets are measured from the root's edges, so a pinned bar stays in the screen's column in a wider browser window or a wider capture. The renderer raises the root's top and bottom padding to at least the height of any pinned status bar, tab bar or home indicator, so content never starts or ends under chrome. With safeArea and a tab bar the bottom reservation is 83px (49 + 34), not 117.

Seven primitives

Type What it is
status-bar Device chrome pinned to the top, as tall as the root's top inset (47)
home-indicator The home pill pinned to the bottom, as tall as the bottom inset (34)
nav-bar A 44px bar (96 with largeTitle), with a back action and up to two trailing actions, each a 44×44 target
tab-bar Pinned to the bottom, 49 plus the inset; standard, floating or fab (a raised 56px centre action)
button primary, secondary, ghost or destructive; 32, 44 or 52 tall by size
list-row 44 tall (56 with a subtitle), with leading icon, initials or image and trailing text, chevron, toggle or badge
sheet A bottom sheet over a scrim, padded past the home indicator; its children scroll inside it

Each primitive takes its colours from the design tokens and falls back to neutral colours on an unthemed canvas. Labels truncate to one line with an ellipsis. Their sizes live in one module (src/primitives.ts) that the renderer and the rules both read, so a button's height cannot differ between what is drawn and what is judged. The viewport meta tag is now emitted, so the viewer's HTML page is not zoomed out on a phone; it changes no capture.

The rules learn the vocabulary

  • Touch targets. On a mobile canvas a button, a list row, each tab-bar tab and FAB, and each nav-bar action is measured by its own box. A size: "sm" button (32px) is an error there by design; the defaults all pass. Nodes inside a component instance are now judged as they render, and the finding names the instance.
  • mobile-tab-bar (new, warning). A tab bar with fewer than 2 or more than 5 tabs, a tab without a label, or a bar not pinned to the bottom.
  • Safe areas. The rule now places fixed and sticky content, treats pinned chrome as a reservation, and checks bars you pin yourself. Their own content must clear the inset, and the scroll must clear them: the finding names the height of the spacer to add to your content frame. A sheet's content is never judged.
  • Hand-built tap targets now name button as the fix.
  • Reuse. A repeated primitive counts as reuse, so a screen built from eight list rows is not told to extract components.
  • Unknown icons are also checked in the primitives' icon props.
  • Overlap. Detailed mode no longer reports siblings that content scrolls under, such as a tab bar or a sheet over a list. A bar pinned to the top over in-flow content is still an error.
  • Stress. A primitive label hitting its ellipsis reports an info. Two or more consecutive sibling list rows count as a list for empty and many.

Spec changes

The slice D build contract and its "As built" notes in docs/specs/PHASE-30-SPEC.md record every decision the FR rows left open. Two of them change what the spec said:

  • FR-C3's passing example, a bar with paddingBottom: 46, cannot pass, because padding of 32px or more renders smaller on a phone (46 renders about 12px). The documented idiom is padding under 32 plus a 34px spacer as the bar's last child.
  • A five-tab bar under i18n only truncates when its labels are long enough. Short labels still fit their slots after i18n's 1.4× stretch and correctly report nothing.

Docs

The batch_design, canvas_create, canvas_evaluate, canvas_stress and screenshot docstrings, INSTRUCTIONS, GOTCHAS, the README and a new "Mobile vocabulary" section in docs/GUIDELINES.md cover the primitives, pinning, the pinned-bar idiom and the rule changes. The slice C caveats about long screens are gone. test-discoverability.ts had never seen skeleton, chart or any slice D type, because a ; inside a comment cut the node-type match short; it now strips comments first.

Testing

  • New test-mobile-primitives.ts (Chrome-free) covers each primitive's markup, sizes, colour defaults, the root render, positioning CSS and the reservations.
  • New test-positioning.ts (Chrome) measures pinned bars, the root render, primitive sizes at 390 and 1440 in both themes, and render invariance.
  • test-mobile-rules.ts, test-mobile-rules-detailed.ts and test-stress-render.ts gained the slice D rule cases.
  • A verifier attacked the contract before the build and found nine wrong verdicts, all fixed in the contract. After the build, four verifiers, a docs audit and a cold-read of the guidelines found about thirty issues. Every confirmed issue was fixed, each with a test shown failing before the fix. A second round checked those fixes and caught a nav-bar title regression, now fixed. Mutation testing ran 91 mutants; the gaps it found have tests.
  • All 25 web structures render and score identically before and after this PR.
  • npm run test:full passes (111/111), and tsc is clean.

The root now grows with long content and keeps its end padding, closing slice C's long-screen gap. status-bar, home-indicator, nav-bar, tab-bar, button, list-row and sheet render from shared geometry in src/primitives.ts; the renderer reserves room for pinned chrome. The primitives join mobile-touch-target, mobile-tab-bar registers, and mobile-safe-area learns fixed, sticky and pinned bars.
The root keeps filling with a 100%-height child, pinned chrome stays in the root's column at any capture width, and sheet rows scroll instead of shrinking. Mobile rules read instances with unique ids; pinned siblings no longer count as overlaps; truncation infos cannot hide a stress clip. The discoverability guard now sees every node type.
Long nav titles truncate between actions instead of under them; right-pinned nodes measure from the root edge; only fixed-height frames clip the end-of-scroll check; a top-pinned bar over content is still an overlap; mobile rules expand instances after resolution, as the renderer does.
@vicmaster
vicmaster merged commit f2bca38 into master Nov 1, 2026
3 checks passed
@vicmaster
vicmaster deleted the phase-30-slice-d branch November 1, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant