Skip to main content

max / goingson

Lint that a described member never takes itself out of flow Rule 1 of the room-and-fallback ruling was the one construction the description layer cannot audit: a member with `position: absolute` contributes no width to the row it shares, so nothing can collide with it and nothing prevents the collision. It was live in styles.css for months as `.tab-group > .subview > .page-header` and cost the toolbar-over-pills overlap at 700 and 600. The gate matches only a selector whose last compound names a described member, so a rule for a child of one is that child's business, and it skips pseudo-elements, which are decoration rather than members. Legitimate layering -- modals, the scrim, .track-entry -- wears none of these names; anything that needs to goes in ALLOW with its reason. Verified both ways by reintroducing the historical selector and by allow-listing a synthetic one.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 21:26 UTC
Signed with PGP, not checked
Commit: c4ebabbc1d0247f7fc31faed6297a3aa0f1e6b44
Parent: e01dfe2
2 files changed, +41 insertions, -0 deletions
@@ -238,6 +238,9 @@
238 238 ### Native dialogs forbidden
239 239 `window.confirm`, `window.prompt`, and `window.alert` are banned. Use `GoingsOn.ui.showConfirmDialog`, `GoingsOn.ui.showPromptDialog`, and `GoingsOn.ui.showToast`. Native dialogs are disabled on iOS WKWebView and unstyled on all platforms. Lint rule `no-native-dialogs` enforces this.
240 240
241 + ### A described member stays in flow
242 + A group described by makeover-layout keeps every member in flow. A member never positions itself out of the row it shares: out of flow it contributes no width, so nothing can collide with it and nothing prevents the collision. Layering is the closed layer set (modals, the scrim, a drawer), not a member's own `position`. When a row runs out of room the answer is the group's fallback, not `position: absolute`. Lint rule `described-members-in-flow` enforces this for `.run`, `.page-header`, `.subview-head` and `.pill-nav`, and carries an allow-list for anything that has a reason.
243 +
241 244 ### Mobile is responsive CSS by default
242 245 JS branches on `GoingsOn.touch.isTouchDevice` (or media-query equivalents) require explicit justification documented here. Default is shared component + CSS layout reflow. (Pattern 5, Phase 6 architectural finding.)
243 246
@@ -98,6 +98,44 @@
98 98 echo " Lower INLINE_HANDLER_BUDGET in scripts/lint-frontend.sh to $inline_count to lock in the gain."
99 99 fi
100 100
101 + # 10. A described member stays in flow.
102 + # Rule 1 of wiki `layout-room-and-fallback`: every member of a described
103 + # group is in flow, and layering is the closed layer set rather than a
104 + # member positioning itself. `position: absolute` is the one construction
105 + # the description layer cannot audit -- it takes the member out of flow, so
106 + # it contributes no width to the row it shares, nothing can collide with it,
107 + # and therefore nothing prevents the collision. That was live in this file
108 + # for months (`.tab-group > .subview > .page-header`, removed 5fa511a) and
109 + # cost the toolbar-over-pills overlap at 700 and 600.
110 + #
111 + # Scope: only a selector whose LAST compound names a described member, so
112 + # `.page-header .some-child { position: absolute }` is the child's business
113 + # and is not flagged. Pseudo-elements are decoration, not members, and are
114 + # skipped. The app owns plenty of legitimate layering -- modals, the scrim,
115 + # `.track-entry` -- and none of it wears these class names; anything that
116 + # needs to may be added to ALLOW below WITH A REASON.
117 + DESCRIBED_MEMBERS='run|page-header|subview-head|pill-nav'
118 + hits=$(awk -v members="$DESCRIBED_MEMBERS" '
119 + # Allow-list: exact selector text, each with the reason it is layered.
120 + # (empty today -- add as `ALLOW[".selector"] = "why"`)
121 + BEGIN { split("", ALLOW) }
122 + /\{/ { sel = $0; sub(/\{.*/, "", sel); gsub(/^[ \t]+|[ \t]+$/, "", sel); if (sel != "") current = sel; next }
123 + /position:[ \t]*(absolute|fixed)/ {
124 + if (current == "") next
125 + n = split(current, parts, ",")
126 + for (i = 1; i <= n; i++) {
127 + one = parts[i]
128 + gsub(/^[ \t]+|[ \t]+$/, "", one)
129 + if (one in ALLOW) continue
130 + if (one ~ /::(before|after)/) continue
131 + last = one
132 + sub(/.*[ >+~]/, "", last)
133 + if (last ~ ("\\.(" members ")")) printf "%s:%d: %s { %s }\n", FILENAME, NR, one, $0
134 + }
135 + }
136 + ' "$SRC_CSS" | sed 's/[ \t]\+/ /g' || true)
137 + report "described-members-in-flow" "A described member takes itself out of flow. Give the group a Fallback instead, or add the selector to ALLOW in this rule with its reason." "$hits"
138 +
101 139 if [ $violations -eq 0 ]; then
102 140 echo "frontend lint: clean"
103 141 exit 0