Skip to content

Commit 79c64a0

Browse files
Prithvi-Webclaude
andcommitted
fix(tour): the welcome tour's card leaves the map's match bar and footer readable - at 1024 x 700 and 1440 x 900 it covered the end of the bar's sentence and the size key, since it stood clear of controls alone and neither line is one (ninth browser pass M02)
tourPlace now stands the card clear of the Treemap's match bar while it says something (.tm-querybar:not(.idle)) and of the map's footer (.tm-foot), as J04 has it stand clear of a control: at the place nearest its rest that covers neither, never lower than its rest. Idle, the bar is only its saved chips, which are controls already, and the rest of its row is the map's. The bar comes, goes and changes its words under a card already placed, so tmSyncQueryBar asks for a placement (tourPlaceSoon, one a frame, nothing while the tour is down). layoutQa measures it with BQA's geometry: at 1024 x 700 (bar y 475-562, footer y 590-655) and 1440 x 900 (bar y 739-784, footer y 812-856) the card stands 12 px above the bar, clear of both; with the bar hidden, 12 px above the footer; over an idle bar alone it rests. placeTour's stub now finds an element that says what it is only by a selector that matches it. Mutants M02-a (bar not avoided), b (footer not avoided), c (a 4 px gap), d (the idle bar avoided), f (no placement asked by the bar), g (asked only as the bar comes), h and i (the bar's own shown/hidden rule broken), each RED, restored by SHA-256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1cbc189 commit 79c64a0

5 files changed

Lines changed: 112 additions & 9 deletions

File tree

‎public/index.html‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,7 +2372,8 @@
23722372
buttons (review F1.12). One place, so the two cannot part again. --tour-b
23732373
is its offset from the window's bottom, 86 px at rest; the page moves it
23742374
where the card would cover a control on screen (tourPlace, seventh browser
2375-
pass J04: at 1024 × 700 it lay over the Dashboard's Browse… and Scan). */
2375+
pass J04: at 1024 × 700 it lay over the Dashboard's Browse… and Scan), or
2376+
the map's match bar or footer (ninth browser pass M02). */
23762377
body { --tour-r: 18px; --tour-w: min(360px, calc(100vw - 32px)); --tour-b: 86px; }
23772378
.tour-card { pointer-events: auto; position: absolute; right: var(--tour-r); bottom: var(--tour-b);
23782379
width: var(--tour-w); padding: 15px 16px;
@@ -25603,6 +25604,10 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
2560325604
// Idle — saved chips and nothing else — the bar draws no card over the
2560425605
// map and lets the pointer through around its chips (review F1.15).
2560525606
bar.classList.toggle('idle', !typed && !hasMessage && !hasStage);
25607+
// The welcome tour's card stands clear of the bar (ninth browser pass M02),
25608+
// and the bar has just come, gone or changed its words under it. Guarded: a
25609+
// page reader may lift this function without the placement.
25610+
if (typeof tourPlaceSoon === 'function') tourPlaceSoon();
2560625611
}
2560725612

2560825613
/**
@@ -33872,11 +33877,17 @@ <h4>${icon('cart', 16)}Staged — deleted only when you say so</h4>
3387233877
which the stylesheet reads. Never lower than its rest: the window's
3387333878
bottom-right corner is the cart pill's and the toasts'. Where nothing is in
3387433879
the way, or nowhere is clear (a column full of controls), it rests. A
33875-
canvas is the view's surface, not a control: over the map the card rests. */
33880+
canvas is the view's surface, not a control: over the map alone the card
33881+
rests. Two lines on the map are read, not pressed, and it stands clear of
33882+
them too: the match bar while it says something, and the map's footer —
33883+
at 1024 × 700 and 1440 × 900 it covered the end of the bar's sentence and
33884+
the size key (ninth browser pass M02). Idle, the bar is only its saved
33885+
chips, which are controls, and the rest of its row is the map's. */
3387633886
const TOUR_REST_BOTTOM = 86; // --tour-b at rest (080-suggestions-reclaim.css)
3387733887
const TOUR_GAP = 12; // between the card and the control it stands clear of
3387833888
const TOUR_EDGE = 8; // the least room kept between it and the window's top
33879-
const TOUR_CONTROLS = '.view.active button, .view.active input, .view.active select, .view.active textarea, .view.active summary, .view.active [tabindex="0"], #emptyState button';
33889+
const TOUR_CONTROLS = '.view.active button, .view.active input, .view.active select, .view.active textarea, .view.active summary, .view.active [tabindex="0"], #emptyState button'
33890+
+ ', .view.active .tm-querybar:not(.idle), .view.active .tm-foot'; // what the map says (M02)
3388033891

3388133892
function tourPlace() {
3388233893
if (!tour.active || $('tourOverlay').hidden) return;

‎src/ui/app/105-query-grammar-in-the-highlight-box.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ function tmSyncQueryBar() {
223223
// Idle — saved chips and nothing else — the bar draws no card over the
224224
// map and lets the pointer through around its chips (review F1.15).
225225
bar.classList.toggle('idle', !typed && !hasMessage && !hasStage);
226+
// The welcome tour's card stands clear of the bar (ninth browser pass M02),
227+
// and the bar has just come, gone or changed its words under it. Guarded: a
228+
// page reader may lift this function without the placement.
229+
if (typeof tourPlaceSoon === 'function') tourPlaceSoon();
226230
}
227231

228232
/**

‎src/ui/app/170-guided-first-run.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,17 @@ function refusedFolderWords() {
337337
which the stylesheet reads. Never lower than its rest: the window's
338338
bottom-right corner is the cart pill's and the toasts'. Where nothing is in
339339
the way, or nowhere is clear (a column full of controls), it rests. A
340-
canvas is the view's surface, not a control: over the map the card rests. */
340+
canvas is the view's surface, not a control: over the map alone the card
341+
rests. Two lines on the map are read, not pressed, and it stands clear of
342+
them too: the match bar while it says something, and the map's footer —
343+
at 1024 × 700 and 1440 × 900 it covered the end of the bar's sentence and
344+
the size key (ninth browser pass M02). Idle, the bar is only its saved
345+
chips, which are controls, and the rest of its row is the map's. */
341346
const TOUR_REST_BOTTOM = 86; // --tour-b at rest (080-suggestions-reclaim.css)
342347
const TOUR_GAP = 12; // between the card and the control it stands clear of
343348
const TOUR_EDGE = 8; // the least room kept between it and the window's top
344-
const TOUR_CONTROLS = '.view.active button, .view.active input, .view.active select, .view.active textarea, .view.active summary, .view.active [tabindex="0"], #emptyState button';
349+
const TOUR_CONTROLS = '.view.active button, .view.active input, .view.active select, .view.active textarea, .view.active summary, .view.active [tabindex="0"], #emptyState button'
350+
+ ', .view.active .tm-querybar:not(.idle), .view.active .tm-foot'; // what the map says (M02)
345351

346352
function tourPlace() {
347353
if (!tour.active || $('tourOverlay').hidden) return;

‎src/ui/styles/080-suggestions-reclaim.css‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@
171171
buttons (review F1.12). One place, so the two cannot part again. --tour-b
172172
is its offset from the window's bottom, 86 px at rest; the page moves it
173173
where the card would cover a control on screen (tourPlace, seventh browser
174-
pass J04: at 1024 × 700 it lay over the Dashboard's Browse… and Scan). */
174+
pass J04: at 1024 × 700 it lay over the Dashboard's Browse… and Scan), or
175+
the map's match bar or footer (ninth browser pass M02). */
175176
body { --tour-r: 18px; --tour-w: min(360px, calc(100vw - 32px)); --tour-b: 86px; }
176177
.tour-card { pointer-events: auto; position: absolute; right: var(--tour-r); bottom: var(--tour-b);
177178
width: var(--tour-w); padding: 15px 16px;

‎tests/layoutQa.test.ts‎

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -938,7 +938,34 @@ test('while the cart is open the welcome tour\'s card covers none of it — at e
938938
/* ══════════════ J04 — the coach card covers no control (seventh browser pass) ══════════════ */
939939

940940
/** A box on screen, in CSS px from the window's top-left; `tag` its element's (a canvas is a surface, not a control). */
941-
type Box = { left: number; top: number; right: number; bottom: number; tag?: string };
941+
type Box = { left: number; top: number; right: number; bottom: number; tag?: string; is?: Is };
942+
943+
/**
944+
* What an element is, for a selector to find it: its tag, id and classes, and whether it lies in the view that is up
945+
* (`.view.active`). A box without one is found by every selector, as each control here always was.
946+
*/
947+
type Is = { tag: string; id?: string; classes?: string[]; inActiveView?: boolean };
948+
/** Whether `is` matches one compound selector: a tag, `#id`, `.class`es and `:not(.class)`es; an attribute matches none of these. */
949+
function compoundMatches(is: Is, compound: string): boolean {
950+
const nots = [...compound.matchAll(/:not\(\.([\w-]+)\)/g)].map((m) => m[1]);
951+
let rest = compound.replace(/:not\(\.[\w-]+\)/g, '');
952+
if (/[[:]/.test(rest)) return false;
953+
const tag = /^[a-z]+/i.exec(rest)?.[0] ?? '';
954+
rest = rest.slice(tag.length);
955+
const has = new Set(is.classes ?? []);
956+
return (!tag || tag.toUpperCase() === is.tag)
957+
&& [...rest.matchAll(/#([\w-]+)/g)].every((m) => m[1] === is.id)
958+
&& [...rest.matchAll(/\.([\w-]+)/g)].every((m) => has.has(m[1]))
959+
&& nots.every((c) => !has.has(c));
960+
}
961+
/** Whether `is` matches a selector list: in each piece the last compound is the element, and `.view.active` before it the view that is up. */
962+
function selectorMatches(is: Is, list: string): boolean {
963+
return list.split(',').some((piece) => {
964+
const parts = piece.trim().split(/\s+/);
965+
const own = parts.pop()!;
966+
return parts.every((p) => p === '.view.active' && !!is.inActiveView) && compoundMatches(is, own);
967+
});
968+
}
942969

943970
/**
944971
* The page's tourPlace over a W × H window, the card `cardH` px tall in its
@@ -954,14 +981,14 @@ function placeTour(W: number, H: number, cardH: number, controls: Box[]) {
954981
const restPx = sheetPx(body.get('--tour-b'), '--tour-b');
955982
const bottom = () => (props['--tour-b'] ? sheetPx(props['--tour-b'], 'the --tour-b set') : restPx);
956983
const box = () => ({ left: right - width, right, width, height: cardH, top: H - bottom() - cardH, bottom: H - bottom() });
957-
const els = controls.map((c) => ({ tagName: c.tag ?? 'BUTTON', getClientRects: () => [c], getBoundingClientRect: () => c }));
984+
const els = controls.map((c) => ({ tagName: c.tag ?? 'BUTTON', is: c.is, getClientRects: () => [c], getBoundingClientRect: () => c }));
958985
let asked = '';
959986
const fns = runPageDecls<{ tourPlace(): void }>(['TOUR_REST_BOTTOM', 'TOUR_GAP', 'TOUR_EDGE', 'TOUR_CONTROLS', 'tourPlace'], {
960987
tour: { active: true },
961988
$: (id: string) => (id === 'tourCard' ? { getBoundingClientRect: box } : id === 'tourOverlay' ? { hidden: false } : null),
962989
document: {
963990
body: { style: { setProperty: (k: string, v: string) => { props[k] = v; }, removeProperty: (k: string) => { delete props[k]; } } },
964-
querySelectorAll: (sel: string) => { asked = sel; return els; },
991+
querySelectorAll: (sel: string) => { asked = sel; return els.filter((e) => !e.is || selectorMatches(e.is, sel)); },
965992
},
966993
window: { innerHeight: H },
967994
}, '{ tourPlace }');
@@ -1079,6 +1106,60 @@ test('the tour card is placed at the next frame, once however often that is aske
10791106
assert.equal(frames.length, 0, 'with the tour down, no frame is asked for');
10801107
});
10811108

1109+
/* ══════════════ M02 — the coach card leaves the map's match bar and footer readable (ninth browser pass) ══════════════ */
1110+
1111+
/** The Treemap's match bar while it says something, and its idle form: only its chips, which are controls of their own. */
1112+
const matchBar = (b: Omit<Box, 'tag' | 'is'>, idle = false): Box =>
1113+
({ ...b, tag: 'DIV', is: { tag: 'DIV', id: 'tmQueryBar', classes: idle ? ['tm-querybar', 'idle'] : ['tm-querybar'], inActiveView: true } });
1114+
/** The map's footer: the size key, the status line and the hint. */
1115+
const mapFoot = (b: Omit<Box, 'tag' | 'is'>): Box => ({ ...b, tag: 'DIV', is: { tag: 'DIV', classes: ['tm-foot'], inActiveView: true } });
1116+
1117+
test('the welcome tour\'s card leaves the map\'s match bar and its footer readable — at 1024 × 700 and 1440 × 900 it covered the end of the bar\'s sentence and the size key — and stands clear of each as it does of a control (ninth browser pass M02)', () => {
1118+
// BQA's ninth pass, Step 2 of 4 (195 px) on the Treemap with a query: at 1024 × 700 the bar is y 475–562 (x 288–966)
1119+
// and the footer's three lines y 590–655; the card at rest, x 646–1006, y 419–614, covered about 320 px of the bar.
1120+
const tm1024: Box[] = [
1121+
{ left: 433, top: 20, right: 746, bottom: 50 }, { left: 766, top: 20, right: 952, bottom: 50, tag: 'INPUT' }, { left: 973, top: 20, right: 1005, bottom: 50 }, // view switch, the box, ✦
1122+
{ left: 712, top: 58, right: 778, bottom: 88 }, { left: 785, top: 58, right: 837, bottom: 88 }, { left: 897, top: 58, right: 989, bottom: 88 }, // Live, Lens, Export
1123+
{ left: 345, top: 115, right: 770, bottom: 131, tag: 'INPUT' }, // the time slider
1124+
{ left: 268, top: 196, right: 988, bottom: 580, tag: 'CANVAS' }, // the map: a surface, not a control
1125+
{ left: 310, top: 483, right: 390, bottom: 511 }, { left: 400, top: 483, right: 493, bottom: 511 }, // Save view, Stage 4 files: left of the card's column
1126+
];
1127+
const bar1024 = matchBar({ left: 288, top: 475, right: 966, bottom: 562 });
1128+
const foot1024 = mapFoot({ left: 268, top: 590, right: 1004, bottom: 655 });
1129+
// 1440 × 900 (BQA's shot): the bar y 739–784 (x 590–1183), the footer y 812–856; the card at rest x 1062–1422, y 619–814.
1130+
const bar1440 = matchBar({ left: 590, top: 739, right: 1183, bottom: 784 });
1131+
const foot1440 = mapFoot({ left: 268, top: 812, right: 1404, bottom: 856 });
1132+
const placed = ([
1133+
['1024 × 700', placeTour(1024, 700, 195, [...tm1024, bar1024, foot1024]), bar1024, foot1024],
1134+
['1440 × 900', placeTour(1440, 900, 195, [{ left: 268, top: 162, right: 1404, bottom: 790, tag: 'CANVAS' }, bar1440, foot1440]), bar1440, foot1440],
1135+
] as const).map(([size, at, bar, foot]) => ({ size, readable: !overlaps(at.card, bar) && !overlaps(at.card, foot), bottom: at.bottom, card: `y ${at.card.top}–${at.card.bottom}` }));
1136+
assert.deepEqual(placed.map(({ size, readable, bottom }) => ({ size, readable, bottom })),
1137+
[{ size: '1024 × 700', readable: true, bottom: 700 - 475 + 12 }, { size: '1440 × 900', readable: true, bottom: 900 - 739 + 12 }],
1138+
`the card leaves the bar and the footer readable, 12 px above the bar — the place nearest its rest that is clear of both (${placed.map((p) => `${p.size}: ${p.card}`).join('; ')})`);
1139+
// With nothing said in the bar it is hidden — not on screen — and the card stands clear of the footer alone.
1140+
const quiet = placeTour(1024, 700, 195, [...tm1024, foot1024]);
1141+
assert.equal(quiet.bottom, 700 - 590 + 12, 'the bar hidden: 12 px above the footer');
1142+
// The idle bar (saved chips only) draws no card over the map: its chips are controls, found as controls, and the rest
1143+
// of its row is the map's. Over it, with the footer below the card, the card rests.
1144+
const chipsOnly = placeTour(1440, 900, 195, [matchBar({ left: 590, top: 739, right: 1183, bottom: 784 }, true), mapFoot({ left: 268, top: 830, right: 1404, bottom: 870 })]);
1145+
assert.equal(chipsOnly.bottom, 86, 'over the idle bar\'s empty row, with nothing else under it, it rests');
1146+
});
1147+
1148+
test('the match bar coming, going or changing its words asks for the coach card to be placed again — the bar grows and shrinks under a card already placed (ninth browser pass M02)', () => {
1149+
let asked = 0;
1150+
const el = (o: Record<string, unknown> = {}) => ({ hidden: true, value: '', childElementCount: 0, classList: { toggle: () => {} }, ...o });
1151+
const els: Record<string, ReturnType<typeof el>> = { tmQueryBar: el(), tmSavedViews: el(), tmQueryErr: el({ hidden: false }), tmStageMatches: el(), tmSearch: el({ value: 'size>1gb' }), tmSaveView: el() };
1152+
const sync = runPageDecls<() => void>(['tmSyncQueryBar'], { $: (id: string) => els[id], state: { view: 'treemap' }, tourPlaceSoon: () => { asked++; } }, 'tmSyncQueryBar');
1153+
sync();
1154+
assert.equal(els.tmQueryBar.hidden, false, 'control: the bar is up, saying something');
1155+
assert.equal(asked, 1, 'and the card is placed again');
1156+
els.tmQueryErr.hidden = true;
1157+
els.tmSearch.value = '';
1158+
sync();
1159+
assert.equal(els.tmQueryBar.hidden, true, 'control: with nothing to say, the bar goes');
1160+
assert.equal(asked, 2, 'and the card is placed again as it goes');
1161+
});
1162+
10821163
/* ══════════════ B26 — the map refits when a row around it changes height ══════════════ */
10831164

10841165
/** The treemap's measured height, with a row of `above` px pushing it down, and whether it redrew. */

0 commit comments

Comments
 (0)