From 1e47be8c1348ef32ea45ae8ddaef47c53be0a090 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 15:06:12 +0000 Subject: [PATCH] fix(web-standalone): product-bar follow-up from the #146 review Two review passes over the merge of #146 found a handful of things that all sit in the same file, so they go in one PR. User-visible - The primary button was the bare `.step-panel button`, so every other button in a panel had to out-specify it. The Step 1 entry tabs never reset its min-height and stood 38px tall instead of 28px. Primary is a class now (`.button-primary`, next to `.button-secondary` and `.button-muted`), the tabs, step heads, candidate cards and import row lose their exemption rules, and the intro's "Got it" uses the same class instead of a copy of the rule. - Primaries and secondaries get 8px vertical padding: a label that wraps in the 300px column was 42px tall with the text flush against both edges; it is 58px with the inset now. - The brand title opens dutchmeshcore.nl in a new tab like every other outbound link in the bar. It is where a click lands mid-resolve, and the page's state is not saved. - The language and theme menus kept role="menu" and menuitemradio without the arrow-key handling those roles promise, while the apps menu had dropped them for that reason. All three are plain disclosures now: aria-expanded plus aria-controls on the trigger, aria-pressed on the chosen option. AGENTS.md - The dropdowns open and close through the `hidden` attribute instead of an `.open` class flipping display. - `#ffffff` on the primaries becomes `var(--on-accent)`. Not `--white`: #f4f4f5 on the fill is 4.27:1, under AA; pure white is 4.7:1. Dead code and stale comments - The first `.topbar .dd-trig, .topbar-meta .is-icon` block and its hover were overridden wholesale by the quiet-role rule later at equal specificity; only the sizing and the mono face survive. - `.controls-card button.is-icon` matched nothing since #142. - Tokens nothing read: --bar-h, --bar-bg, --heading-shadow, and the three opacity tokens behind body::before/::after, which painted nothing at opacity 0 but were still composited with a mask and a blend mode. - The comment for the deleted `.topbar { z-index: 2 }` moves to the shell rule it was about; the heading and phone-bar comments now match the code. - The window resize listener for syncNavHeight only registers when there is no ResizeObserver. Verified in Chromium at 1280px and 375px against main with the same fonts: tabs 38 -> 28px, wrapped label 42px/0 inset -> 58px/8px, step heads 53px both, all three dropdowns open, swap, close on Escape and outside click, theme switch to light and back, no console errors. pytest 13 passed; accuracy harness --hop1-only 85 unchanged against main. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01H6fJpaq2uZ94Z2FW53yGtR --- web-standalone/changelog.json | 6 + web-standalone/index.html | 241 ++++++++++++---------------------- 2 files changed, 92 insertions(+), 155 deletions(-) diff --git a/web-standalone/changelog.json b/web-standalone/changelog.json index a21c658..3ba475c 100644 --- a/web-standalone/changelog.json +++ b/web-standalone/changelog.json @@ -1,4 +1,10 @@ [ + { + "id": "2026-09-30-product-bar-followup", + "date": "2026-09-30", + "title": "Step 1 tabs back to size", + "body": "The Step 1 tabs and step headers are compact again, and the title opens the community site in a new tab." + }, { "id": "2026-09-11-review-fixes", "date": "2026-09-11", diff --git a/web-standalone/index.html b/web-standalone/index.html index 7d66473..6e8db6c 100644 --- a/web-standalone/index.html +++ b/web-standalone/index.html @@ -67,6 +67,9 @@ --red: #df2020; --white: #f4f4f5; --blue: #1976e8; + /* Text on the accent fill. Not --white: #f4f4f5 on the fill is 4.27:1, + under AA for 16px text; pure white is 4.7:1. */ + --on-accent: #ffffff; --green: #3ecf8e; --yellow: #f5c542; @@ -106,9 +109,6 @@ --surface-2: #0e1117; --surface-shadow: none; --body-bg: none; - --sheen-opacity: 0; - --grid-opacity: 0; - --scan-opacity: 0; /* App-level surfaces the guide does not name. Defined in both roots so no rule below has to know which theme is running. Values sit on the @@ -119,8 +119,6 @@ --scrim: rgba(3, 4, 6, 0.62); --raised: rgba(255, 255, 255, 0.10); /* Shared product bar (STYLE-GUIDE §9, #142). */ - --bar-h: 34px; - --bar-bg: rgba(3, 4, 6, 0.74); --glass-bg: rgba(18, 21, 28, 0.88); /* The bar is sticky: content scrolls UNDER it, so it needs a fill of its own plus a blur, not a translucent surface token (STYLE-GUIDE §9). */ @@ -133,9 +131,6 @@ --font-sans: 'IBM Plex Sans', 'Segoe UI', system-ui, -apple-system, Roboto, sans-serif; --font-head: 'Aldrich', 'Segoe UI', system-ui, sans-serif; --font-mono: 'IBM Plex Mono', ui-monospace, Consolas, 'Fira Mono', monospace; - /* A hard 3px drop shadow reads as a game title, not as a section - heading in a tool (#144). */ - --heading-shadow: none; --fs-sm: 12px; --fs-md: 14px; --fs-lg: 16px; @@ -181,18 +176,12 @@ --surface-2: #f4f6fb; --surface-shadow: none; --body-bg: none; - --sheen-opacity: 0; - --grid-opacity: 0; - --scan-opacity: 0; - - --heading-shadow: none; --field-bg: #ffffff; --menu-bg: #ffffff; --chip-bg: #eef0f8; --scrim: rgba(26, 28, 46, 0.35); --raised: rgba(20, 30, 60, 0.06); - --bar-bg: rgba(255, 255, 255, 0.82); --glass-bg: rgba(255, 255, 255, 0.94); --navbar-bg: rgba(255, 255, 255, 0.90); --navbar-shadow: 0 10px 26px rgba(20, 30, 60, 0.10); @@ -215,47 +204,17 @@ position: relative; } - /* Grid and scanlines (STYLE-GUIDE §6). Both sit under the content and - above the ground, and both go to zero opacity in light mode. */ - body::before, - body::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; - } - body::before { - background-image: - linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.024) 1px, transparent 1px); - background-size: 42px 42px; - -webkit-mask-image: radial-gradient(ellipse at 50% 30%, #000 0%, transparent 78%); - mask-image: radial-gradient(ellipse at 50% 30%, #000 0%, transparent 78%); - opacity: var(--grid-opacity); - } - body::after { - background-image: repeating-linear-gradient(0deg, - rgba(255, 255, 255, 0.043), - rgba(255, 255, 255, 0.043) 1px, - transparent 1px, - transparent 4px); - mix-blend-mode: overlay; - opacity: var(--scan-opacity); - } - /* Everything the operator touches sits above the two overlays. */ + /* The shell is a stacking context of its own, so the sticky topbar + (z-index 30, below) can outrank it rather than tie with it: its + dropdowns hang down over the map strip, and at equal z-index the later + element in the document wins — the strip's buttons cut straight through + the theme and What's new panels. */ .shell, .intro-overlay, .loading-overlay { position: relative; z-index: 1; } - /* The topbar has to outrank the shell, not tie with it: its dropdowns hang - down over the map strip, and at equal z-index the later element in the - document wins — so the strip's buttons cut straight through the theme - and What's new panels. */ - /* Headings and the brand run in the display face, uppercased, with a hard - drop shadow that light mode drops (STYLE-GUIDE §3). */ /* The display face carries the brand, not every section heading (#144). Uppercase Aldrich on each panel title made three shouted words per card; panel titles are sentence-case body type, which is what a tool's chrome @@ -328,11 +287,12 @@ /* The dropdown pattern the bar's three shared controls use. */ .dd { position: relative; } .dd-caret { font-size: 0.65rem; opacity: 0.7; } + /* Open or closed is the menu's `hidden` attribute (AGENTS.md §7); the + band script toggles it and mirrors it on the trigger's aria-expanded. */ .dd-menu { position: absolute; right: 0; top: calc(100% + 6px); - display: none; min-width: 168px; padding: 6px; background: var(--menu-bg); @@ -341,7 +301,6 @@ box-shadow: var(--surface-shadow); z-index: 40; } - .dd.open .dd-menu { display: block; } .topbar .dd-menu button { display: flex; align-items: center; @@ -361,7 +320,7 @@ .topbar .dd-menu button:hover { background: var(--surface-hover); } .dd-menu .dd-ico { width: 20px; text-align: center; } .dd-menu .chk { margin-left: auto; color: var(--accent); visibility: hidden; } - .dd-menu button[aria-checked="true"] .chk { visibility: visible; } + .dd-menu button[aria-pressed="true"] .chk { visibility: visible; } /* App switcher: a glass sheet of monogram tiles. */ .dmc-apps .dd-menu { @@ -515,31 +474,21 @@ margin: 0 2px; flex: 0 0 auto; } + /* Only what the quiet role (below) does not decide: the bar's controls + are at least a 28px square, in the mono face. Shape, fill, border and + hover come from the role rule, which is later at equal specificity and + used to silently override a full copy of them here. */ .topbar .dd-trig, .topbar-meta .is-icon { display: inline-flex; align-items: center; justify-content: center; gap: 5px; - height: 28px; min-width: 28px; - padding: 0 8px; margin: 0; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--chip-bg); - color: var(--text-muted); font-family: var(--font-mono); font-size: 0.72rem; line-height: 1; - cursor: pointer; - } - .topbar .dd-trig:hover, - .topbar-meta .is-icon:hover { - color: var(--text); - border-color: var(--accent); - background: var(--chip-bg); - filter: none; } /* Icons carry currentColor, so they follow the theme and the hover state instead of sitting there as OS emoji at their own size and hue. */ @@ -710,24 +659,15 @@ .controls-card .layer-chip { font-size: 0.78rem; } - /* The help and What's new buttons stay together at the right, and wrap - as one unit. Wrapping them separately left the bell alone at the far - left of a second row on a phone, with the panel anchored to it opening - off-screen (#112). */ + /* The strip's trailing controls stay together at the right and wrap as + one unit (#112). The help and What's new buttons that used to sit here + moved into the bar (#142). */ .controls-meta { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; } - .controls-card button.is-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - padding: 0; - line-height: 1; - } /* "What's new" changelog (#22): unread dot on the button, dropdown panel positioned relative to the wrapper. */ @@ -829,7 +769,6 @@ border: 1px solid var(--border); margin-bottom: 14px; } - /* Beats `.step-panel button` (0,1,1) on the accent fill and the margin. */ .step-panel .entry-tabs .entry-tab { margin: 0; border: 0; @@ -853,7 +792,6 @@ } .step-panel .entry-tabs .entry-tab:hover { color: var(--text); - filter: none; } /* The uncommon half of Step 1, closed until asked for (#133). */ @@ -895,7 +833,7 @@ align-items: stretch; margin-bottom: 14px; } - .step-panel .import-row button { margin-top: 0; } + .step-panel .import-row .button-primary { margin-top: 0; } /* Tighter help text to keep the step panels compact. */ /* Three roles under a field, and they have to look like three things (#132). Before this they were all one colour within a pixel of each @@ -1014,9 +952,6 @@ /* The head is the control that folds the panel (#135). Step 1 already collapsed on its own when a cluster locked, but there was no way to open it again and nothing said what it held. */ - /* `.step-panel button` (0,1,1) paints every button in the panel with the - accent fill, so the head has to outspecify it or it renders as a big - green primary action. */ .step-panel .step-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -1192,8 +1127,6 @@ font: inherit; } - /* Primary actions (the workflow buttons) carry the accent. */ - .source-panel button, /* ── Three button roles, and nothing else (#144) ──────────────────── The app had 28 visible buttons drawn in 14 looks at 9 heights, with 37 bordered elements. Panels, buttons and chips all shouted at the same @@ -1205,26 +1138,36 @@ The rule doing most of the work: a border is for a container, not for a control. Quiet controls carry a fill instead and get their border back - on hover and focus, so nothing looks dead. */ - .step-panel button { - margin-top: 12px; + on hover and focus, so nothing looks dead. + + Each role is a class. Primary used to be the bare `.step-panel button`, + which every other button in a panel (tabs, step heads, candidate cards) + then had to out-specify; the tabs and heads never reset its min-height + and stood 38px tall. */ + .button-primary, + .button-secondary { min-height: 38px; border: 0; border-radius: var(--radius); - padding: 0 16px; + /* Vertical padding too: a label that wraps to two lines in the 300px + column is taller than the min-height and needs the inset. */ + padding: 8px 16px; font: inherit; font-weight: 600; - color: #ffffff; + cursor: pointer; + } + .button-primary { /* White on the brand blue #1976e8 is 4.38:1, under AA for 16px text; the fill is 4% darker (#145). */ + color: var(--on-accent); background: var(--accent-fill); - cursor: pointer; } /* Hover darkens (#145). Brightening dropped white text on the light fill from 4.67:1 to 4.09:1; darkening raises it instead. */ - .source-panel button:hover, - .step-panel button:hover { filter: brightness(0.92); } - .step-panel .step-head:hover { filter: none; } + .button-primary:hover { filter: brightness(0.92); } + /* A step panel stacks its actions under the fields. */ + .step-panel .button-primary, + .step-panel .button-secondary { margin-top: 12px; } /* Utility actions (reset / view switches) are quiet ghost buttons. */ .utility-panel button, @@ -1257,23 +1200,19 @@ } /* Secondary: outline, no fill. */ - .button-secondary, - .step-panel button.button-secondary { + .button-secondary { background: transparent; color: var(--text); border: 1px solid var(--border); font-weight: 500; } - .button-secondary:hover, - .step-panel button.button-secondary:hover { + .button-secondary:hover { background: var(--surface-hover); border-color: var(--border-strong); - filter: none; } /* Quiet: a fill and no border at all. */ .button-muted, - .step-panel button.button-muted, .controls-card .layer-chip, .topbar .dd-trig, .topbar-meta .is-icon { @@ -1288,7 +1227,6 @@ cursor: pointer; } .button-muted:hover, - .step-panel button.button-muted:hover, .controls-card .layer-chip:hover, .topbar .dd-trig:hover, .topbar-meta .is-icon:hover { @@ -1412,12 +1350,10 @@ /* A candidate card is a selectable LIST ITEM, not a primary action, so it gets a quiet surface with a selected state. Every selector here is - scoped under .step-panel deliberately: the cards are + @@ -2140,7 +2059,9 @@ below them. Same four things it prescribes — DMC brand, app switcher, language, theme — merged into the app's own bar instead of above it. -->
- + + Dutch MeshCore MeshCore Triangulator @@ -2155,9 +2076,11 @@ - -
+ + @@ -2307,7 +2230,7 @@

Unknown Node Estimate

- +

Run a weighted observer estimate first. Terrain and LOS are an optional refinement after that.

@@ -6890,21 +6813,28 @@

Unknown Node Estimate

} if (topbarEl && window.ResizeObserver) { new ResizeObserver(syncNavHeight).observe(topbarEl); + } else { + // Without ResizeObserver a window resize is the only signal; with it + // the observer already fires for every change of the bar's height. + window.addEventListener("resize", syncNavHeight); } - window.addEventListener("resize", syncNavHeight); syncNavHeight(); // ── The band's dropdowns (#142, STYLE-GUIDE §9) ───────────────────── // One behaviour for all three: a trigger toggles, opening one closes the // others, and an outside click or Escape closes whatever is open. + // Open is the menu's `hidden` attribute (AGENTS.md §7), mirrored on the + // trigger's aria-expanded; there is no class to keep in step with it. function closeBandDropdowns(except) { - document.querySelectorAll(".topbar .dd.open").forEach((dd) => { + document.querySelectorAll(".topbar .dd").forEach((dd) => { if (dd === except) return; + const menu = dd.querySelector(".dd-menu"); + if (!menu || menu.hidden) return; const trigger = dd.querySelector(".dd-trig"); // Hiding a menu that holds focus drops it to (#145), so hand it // back to the trigger, where a keyboard user expects to be. const heldFocus = dd.contains(document.activeElement) && document.activeElement !== trigger; - dd.classList.remove("open"); + menu.hidden = true; if (trigger) { trigger.setAttribute("aria-expanded", "false"); if (heldFocus) trigger.focus(); @@ -6914,15 +6844,16 @@

Unknown Node Estimate

document.querySelectorAll(".topbar .dd").forEach((dd) => { const trigger = dd.querySelector(".dd-trig"); - if (!trigger) return; + const menu = dd.querySelector(".dd-menu"); + if (!trigger || !menu) return; // No stopPropagation (#145): the click has to reach document, where the // What's new panel and the relief popover listen for clicks outside // themselves. The outside-click handler below already ignores clicks that // land inside a .dd, so this toggle is not undone by it. trigger.addEventListener("click", () => { - const opening = !dd.classList.contains("open"); + const opening = menu.hidden; closeBandDropdowns(dd); - dd.classList.toggle("open", opening); + menu.hidden = !opening; trigger.setAttribute("aria-expanded", opening ? "true" : "false"); }); }); @@ -6957,7 +6888,7 @@

Unknown Node Estimate

: ''; } document.querySelectorAll("[data-theme-choice]").forEach((button) => { - button.setAttribute("aria-checked", button.dataset.themeChoice === choice ? "true" : "false"); + button.setAttribute("aria-pressed", button.dataset.themeChoice === choice ? "true" : "false"); }); // The map's own controls and labels are painted by MapLibre, not CSS. refresh3dData();