Skip to content

Commit 08bd5f3

Browse files
fix(masthead): keep ~/anyplot.ai visible on xs for short breadcrumbs (#7891)
## Summary Reported on `/palette`: the page looks empty up top because on mobile the masthead bar drops the `~/anyplot.ai` root marker and leaves a single tiny "palette" label hanging in an otherwise empty row. The xs-hide behavior exists so long breadcrumbs like `scatter-basic-annotated · python · matplotlib` don't truncate — but it's overkill for reserved single-segment pages (`/palette`, `/about`, `/mcp`, `/stats`, …). ## Change Compute the xs-effective length of the breadcrumb (using the short-form labels for lang/lib segments where applicable). When that total is under 15 chars and we're not on the landing page, keep the `~/anyplot.ai` marker and its leading ` · ` separator visible on xs. Long breadcrumbs keep the existing compact behavior. Threshold per user request: shorter than 15 chars ⇒ keep the prefix. ### Examples | Route | xs label length | xs behavior | |---|---|---| | `/palette` | 7 | `~/anyplot.ai · palette` | | `/about` | 5 | `~/anyplot.ai · about` | | `/mcp` | 3 | `~/anyplot.ai · mcp` | | `/scatter-basic` | 13 | `~/anyplot.ai · scatter-basic` | | `/scatter-basic-annotated` | 23 | `scatter-basic-annotated` (compact, unchanged) | | `/scatter-basic/python/matplotlib` | 13+2+10 = 25 (with shorts) | compact, unchanged | ## Test plan - [x] `yarn test src/components/MastheadRule.test.tsx` — added 2 tests covering both branches (short ⇒ inline display, long ⇒ xs `display:none` media query) - [x] `yarn test` — all 513 tests pass - [x] `yarn type-check` — clean - [x] `yarn lint` — clean (2 pre-existing warnings, unrelated) - [ ] Visual check on mobile viewport at `/palette` after deploy https://claude.ai/code/session_01H3pPMxN66vWWiD1cYS1bcz --- _Generated by [Claude Code](https://claude.ai/code/session_017cw5Lc18tDxEEpXoKPXFcD)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 60cf518 commit 08bd5f3

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

app/src/components/MastheadRule.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { MemoryRouter } from 'react-router-dom';
3+
import { ThemeProvider, createTheme } from '@mui/material/styles';
4+
import { render as rtlRender } from '@testing-library/react';
5+
import type { ReactElement } from 'react';
26
import { render, screen, userEvent } from '../test-utils';
37

48
const trackEvent = vi.fn();
@@ -17,6 +21,15 @@ vi.mock('../hooks', async () => {
1721

1822
import { MastheadRule } from './MastheadRule';
1923

24+
function renderAt(initialEntry: string, ui: ReactElement) {
25+
const theme = createTheme();
26+
return rtlRender(
27+
<ThemeProvider theme={theme}>
28+
<MemoryRouter initialEntries={[initialEntry]}>{ui}</MemoryRouter>
29+
</ThemeProvider>
30+
);
31+
}
32+
2033
describe('MastheadRule', () => {
2134
beforeEach(() => {
2235
trackEvent.mockClear();
@@ -47,4 +60,32 @@ describe('MastheadRule', () => {
4760
await user.click(screen.getByText('v1.2.3'));
4861
expect(trackEvent).toHaveBeenCalledWith('nav_click', { source: 'masthead_release', target: 'v1.2.3' });
4962
});
63+
64+
it('keeps the `~/anyplot.ai` root marker visible on xs for short breadcrumbs', () => {
65+
const { container } = renderAt('/palette', <MastheadRule />);
66+
const rootMarker = container.querySelector<HTMLAnchorElement>('a[href="/"]');
67+
expect(rootMarker).not.toBeNull();
68+
const styles = collectStylesFor(rootMarker!);
69+
// Unconditional `display:inline` — no xs hide rule.
70+
expect(styles).toMatch(/display:\s*inline/);
71+
expect(styles).not.toMatch(/display:\s*none/);
72+
});
73+
74+
it('hides the `~/anyplot.ai` root marker on xs for long breadcrumbs', () => {
75+
const { container } = renderAt('/scatter-basic-annotated/python/matplotlib', <MastheadRule />);
76+
const rootMarker = container.querySelector<HTMLAnchorElement>('a[href="/"]');
77+
expect(rootMarker).not.toBeNull();
78+
const styles = collectStylesFor(rootMarker!);
79+
// MUI compiles `{ xs: 'none', sm: 'inline' }` into a media-query block
80+
// with display:none at the xs (min-width:0px) breakpoint.
81+
expect(styles).toMatch(/@media\s*\(min-width:\s*0px\)[^}]*display:\s*none/);
82+
});
5083
});
84+
85+
function collectStylesFor(el: Element): string {
86+
const classes = el.className.split(/\s+/).filter(Boolean);
87+
return Array.from(document.querySelectorAll('style'))
88+
.map((s) => s.textContent ?? '')
89+
.filter((css) => classes.some((c) => css.includes('.' + c)))
90+
.join('\n');
91+
}

app/src/components/MastheadRule.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ export function MastheadRule() {
123123
// Pick one random comment style per browser session (stable across client-side nav).
124124
const [randomIdx] = useState(() => Math.floor(Math.random() * COMMENT_POOL.length));
125125

126+
// Short single-segment breadcrumbs (e.g. `/palette`, `/about`, `/mcp`) leave
127+
// plenty of room on xs — hiding `~/anyplot.ai` there strands the breadcrumb
128+
// as a tiny label and makes the masthead look empty. Keep the root marker
129+
// visible whenever the xs-effective breadcrumb is under 15 chars.
130+
const xsBreadcrumbLength = isLanding
131+
? 0
132+
: segments.reduce((sum, s) => {
133+
const xsLabel = s.short && s.short !== s.label ? s.short : s.label;
134+
return sum + xsLabel.length;
135+
}, 0);
136+
const keepRootMarkerOnXs = !isLanding && xsBreadcrumbLength < 15;
137+
const rootMarkerDisplay = keepRootMarkerOnXs ? 'inline' : { xs: 'none', sm: 'inline' };
138+
126139
// Determine whether the center comment shows, what it says, and in which syntax.
127140
// - Landing: random delim + brand claim
128141
// - Spec routes (/:specId[/:language[/:library]]): random or language-matched delim,
@@ -182,12 +195,14 @@ export function MastheadRule() {
182195
}}>
183196
{/* Root marker — hidden on xs (where the NavBar logo `any.plot()` below
184197
already anchors the brand) so the breadcrumb has room for the
185-
spec-id + lang + lib without truncating. */}
198+
spec-id + lang + lib without truncating. Short single-segment
199+
breadcrumbs override this and keep the marker on xs (see
200+
keepRootMarkerOnXs above). */}
186201
<Box
187202
component={RouterLink}
188203
to="/"
189204
onClick={() => trackEvent('nav_click', { source: 'masthead_logo', target: '/' })}
190-
sx={{ ...linkSx, display: { xs: 'none', sm: 'inline' } }}
205+
sx={{ ...linkSx, display: rootMarkerDisplay }}
191206
>
192207
~/anyplot.ai
193208
</Box>
@@ -238,9 +253,10 @@ export function MastheadRule() {
238253
);
239254
return (
240255
<Box key={`${seg.label}-${i}`} component="span">
241-
{/* First separator hides on xs because the logo is hidden too;
256+
{/* First separator follows the root marker's xs visibility —
257+
showing it without the marker would dangle a stray ` · `;
242258
later separators always show. */}
243-
<Box component="span" sx={i === 0 ? { display: { xs: 'none', sm: 'inline' } } : undefined}>
259+
<Box component="span" sx={i === 0 ? { display: rootMarkerDisplay } : undefined}>
244260
{' · '}
245261
</Box>
246262
{seg.to ? (

0 commit comments

Comments
 (0)