fix(logo): widen navbar SVG viewBox so CORE/SCOPE wordmark fits (#1141 followup) (#1156)

Fixes #1141 follow-up — the visible-on-staging SCOPE→SCOP clip that the
prior PRs (#1137, #1141) intended to address but didn't.

## What was actually broken (ground truth from staging)

Staging at `http://20.109.157.39:80/` renders the inline navbar SVG
correctly — duotone CORE/SCOPE fills inherit page CSS vars, mobile
mark-only swap fires at ≤400px, customizer logo override path works.
Those parts of #1137 + #1141 landed cleanly.

What did **NOT** land: the SVG `viewBox` was never widened to fit the
rendered Aldrich wordmark. At every desktop viewport the SCOPE `<text
text-anchor="start" x="773.8">` produces a bbox extending to user-space
x≈1112, but the navbar `viewBox="170 10 860 280"` ends at x=1030.
Result: SCOPE renders as **SCOP** on every desktop load. CORE also
slightly overflows the left edge (bbox.x=153.7 < viewBox.x=170).

The original brief premise (mushroom emoji still in `index.html` +
`<img>`-loaded SVG monotone fallback on staging) does not match current
state — `public/index.html:45` already has the inline SVG, staging
renders it, and computed fills are duotone (`rgb(74,158,255)` vs
`rgb(109,179,255)`). The visible bug is geometric clipping, not CSS-var
inheritance or a mushroom revert.

## Fix (one-liner SVG geometry change)

- `public/index.html` — navbar `svg.brand-logo`: `viewBox="170 10 860
280"` → `viewBox="150 10 970 280"`; intrinsic `width="111"` →
`width="125"` (preserves ~36px nav row height).
- `public/style.css` — `.brand-logo { width }` 111px → 125px (desktop),
tablet `@media (max-width:900px)` pin 99px → 112px to keep the new
aspect ratio so wordmark still doesn't clip on tablets.
- `public/customize-v2.js` — `_setBrandLogoUrl` `<img>` swap dimensions
updated to match (when an operator overrides `branding.logoUrl`).

The `≤400px` mobile mark-only swap is unchanged — at narrow widths the
wordmark still hides entirely and the dedicated `.brand-mark-only` SVG
(no `<text>`) renders.

## TDD (red → green)

| commit | role |
|---|---|
| `16b7a60` | **RED** — `test-logo-theme-e2e.js` assertion #7: every
`CORE`/`SCOPE` `<text>` bbox must fit inside the SVG `viewBox`. Master
fails: `[{text:CORE, bboxX:153.7, bboxRight:426.2, vbX:170},
{text:SCOPE, bboxX:773.8, bboxRight:1111.5, vbRight:1030}]` |
| `0db473b` | **GREEN** — widen viewBox + width to fit |

Test exercises real `getBBox()` measurement on a headless Chromium DOM
with the Aldrich webfont loaded — not a unit-test fill string check. The
earlier #1141 tests asserted computed `fill` colors (which were correct)
but never measured rendered geometry; that's the gap.

## Visual proof

**Before** (master HEAD against staging, viewport 1280):

`/tmp/staging-logo-before-1280.png` — SCOPE clearly clipped to "SCOP".

**After** (this branch against local server, viewport 1280):

`/tmp/local-after-1280-screen.png` — full CORE / SCOPE rendered.

**Mobile (after, 375px)**: `/tmp/local-after-mobile.png` — mark-only SVG
(no wordmark, no clip).

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— all hard gates clean (PII, branch-scope, red-commit-genuine,
css-vars-defined, css-self-fallback, like-on-json, sync-migration), all
warnings clean (img-svg-ratio, themed-img-svg, fixture-coverage).

E2E assertion added: `test-logo-theme-e2e.js:286-310`
Browser verified: `/tmp/local-after-1280-screen.png` (local server) +
`/tmp/staging-logo-before-1280.png` (staging baseline).

---------

Co-authored-by: corescope-bot <bot@corescope.local>
This commit is contained in:
Kpa-clawbot
2026-05-06 23:39:10 -07:00
committed by GitHub
parent 652d4939ea
commit dfacfd0f6e
4 changed files with 44 additions and 7 deletions
+1 -1
View File
@@ -74,7 +74,7 @@
img.className = 'brand-logo';
img.setAttribute('src', url);
img.setAttribute('alt', alt || node.getAttribute('aria-label') || 'Brand');
img.setAttribute('width', '111');
img.setAttribute('width', '125');
img.setAttribute('height', '36');
node.parentNode.replaceChild(img, node);
} else {
+1 -1
View File
@@ -42,7 +42,7 @@
<nav class="top-nav" role="navigation" aria-label="Main navigation">
<div class="nav-left">
<a href="#/" class="nav-brand" aria-label="CoreScope home">
<svg class="brand-logo" xmlns="http://www.w3.org/2000/svg" width="111" height="36" viewBox="170 10 860 280" aria-hidden="true" focusable="false"><path d="M540 100 A 30 30 0 1 0 540 160" fill="none" stroke="var(--logo-accent)" stroke-width="8" opacity="1.00"/>
<svg class="brand-logo" xmlns="http://www.w3.org/2000/svg" width="125" height="36" viewBox="150 10 970 280" aria-hidden="true" focusable="false"><path d="M540 100 A 30 30 0 1 0 540 160" fill="none" stroke="var(--logo-accent)" stroke-width="8" opacity="1.00"/>
<path d="M540 73 A 57 57 0 1 0 540 187" fill="none" stroke="var(--logo-accent)" stroke-width="8" opacity="0.82"/>
<path d="M540 46 A 84 84 0 1 0 540 214" fill="none" stroke="var(--logo-accent)" stroke-width="8" opacity="0.64"/>
<path d="M540 19 A 111 111 0 1 0 540 241" fill="none" stroke="var(--logo-accent)" stroke-width="8" opacity="0.46"/>
+5 -4
View File
@@ -497,9 +497,10 @@ input[type="week"] {
height: 36px;
/* Width matches the inline SVG's effective content aspect (~3.08:1)
after cropping to the wordmark+nodes region in the navbar viewBox.
Pinned explicitly so layout doesn't shift if the SVG is replaced
(issue #1046 1115px viewport regression). */
width: 111px;
/* Pinned to match the SVG intrinsic width (#1141 followup: viewBox
widened to 150 10 970 280 to fit CORE/SCOPE wordmark; intrinsic
width 125 keeps the text legible at the same ~36px height). */
width: 125px;
max-height: 36px;
/* The logo is inlined into the DOM (PR #1137) so it inherits page
CSS variables; theme via --logo-text / --logo-accent / etc. defined
@@ -1469,7 +1470,7 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
@media (max-width: 900px) {
.panel-right { width: 320px; min-width: 320px; }
.nav-stats { display: none; }
.brand-logo { height: 32px; width: 99px; }
.brand-logo { height: 32px; width: 112px; }
.nav-link { padding: 14px 8px; font-size: 13px; }
.map-controls { width: 180px; font-size: 12px; }
}
+37 -1
View File
@@ -58,7 +58,7 @@ async function main() {
}
let passed = 0;
const total = 6;
const total = 7;
try {
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await context.newPage();
@@ -283,6 +283,42 @@ async function main() {
console.log(` ✅ mobile (360px): mark-only swap active (full hidden, mark visible, right=${mobile.visRectRight}px ≤ viewport ${mobile.viewportWidth}px)`);
passed++;
// 7. Desktop wordmark must NOT clip — every <text> element's bbox in
// user-space coords must lie fully inside the SVG's viewBox. The
// original navbar SVG ships with viewBox "170 10 860 280" (right
// edge x=1030), but the SCOPE <text> with text-anchor="start" at
// x=773.8 + width≈338 extends to x≈1111 — clipped to "SCOP" at
// every desktop viewport width. Fix: widen the viewBox so the
// wordmark fits.
await page.setViewportSize({ width: 1280, height: 800 });
await page.evaluate(() => { window.location.hash = '#/'; });
await page.waitForFunction(() => location.hash === '#/');
await page.waitForSelector('.nav-brand svg.brand-logo', { timeout: 8000 });
await page.waitForTimeout(150);
const clip = await page.evaluate(() => {
const svg = document.querySelector('.nav-brand svg.brand-logo');
if (!svg) return { error: '.nav-brand svg.brand-logo missing' };
const vb = (svg.getAttribute('viewBox') || '').split(/\s+/).map(Number);
if (vb.length !== 4) return { error: 'viewBox malformed: ' + svg.getAttribute('viewBox') };
const [vx, vy, vw, vh] = vb;
const offenders = [];
svg.querySelectorAll('text').forEach((t) => {
const tc = (t.textContent || '').trim();
if (tc !== 'CORE' && tc !== 'SCOPE') return;
const bb = t.getBBox();
if (bb.x < vx - 0.5 || bb.x + bb.width > vx + vw + 0.5) {
offenders.push({ text: tc, bboxX: bb.x, bboxRight: bb.x + bb.width, vbX: vx, vbRight: vx + vw });
}
});
return { viewBox: vb, offenders };
});
if (clip.error) fail(clip.error);
if (clip.offenders && clip.offenders.length) {
fail(`desktop: wordmark <text> overflows SVG viewBox (will be clipped): ${JSON.stringify(clip.offenders)}`);
}
console.log(` ✅ desktop (1280px): CORE/SCOPE bboxes fit inside viewBox ${JSON.stringify(clip.viewBox)}`);
passed++;
await browser.close();
console.log(`\ntest-logo-theme-e2e.js: ${passed}/${total} PASS`);
} catch (err) {