diff --git a/CLAUDE.md b/CLAUDE.md index 078069e..1f59fd7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2069,9 +2069,26 @@ one that closes the queue — the reported defect moved one press later, which looks exactly like a press that did nothing. **And the way out is 44px on a phone.** With the panel spanning the -whole width the scrim has no uncovered pixels at all, so the close -button is the only pointer route out of a full-screen surface; it was -**25×21px**. +whole width there is no scrim there at all, so the close button is the +only pointer route out of a full-screen surface; it was **25×21px**. + +**The scrim is drawn only where it can be tapped** (#171). Below 600px +`.panel-content` is `width: 100%`, so the scrim sat entirely underneath +an opaque panel — measured at 424×439, host, panel and scrim all +424×318 — dimming nothing and dismissing nothing while wearing +`cursor: pointer`. #24's tap-outside-to-close cannot exist on a surface +with no outside, and the screen above is what answers it instead: back, +and a 44px close button. The alternative — a gutter, which is the +drawer pattern — was declined, because it buys the affordance by taking +width off a full-screen surface on a 424px viewport. Two things about +it are load-bearing. Its **existence** is `matchMedia`, not +`display: none`, on `job-band`'s rule: a hidden scrim is still an +element carrying the dismissal handler. And **the 600–899 band is +untouched**, where the panel is a 320px column of a wider content area +and the scrim has real uncovered pixels — which is why the e2e half +asserts *absence* at 424×439 rather than clicking, since a phone-width +case that clicks the scrim's centre hits the panel and passes on the +broken build. What this does **not** fix is `page-header` overflowing on its own: at 900×600 "New Smart Playlist" is still clipped to 114 of 162px with diff --git a/e2e/specs/queue-as-a-screen.spec.ts b/e2e/specs/queue-as-a-screen.spec.ts index 5767a91..dfb23f1 100644 --- a/e2e/specs/queue-as-a-screen.spec.ts +++ b/e2e/specs/queue-as-a-screen.spec.ts @@ -179,8 +179,8 @@ test.describe('the queue is a screen where it covers the content', () => { */ /** - * With the panel spanning the whole width the scrim has no uncovered - * pixels, so the close button is the only pointer route out of a + * With the panel spanning the whole width there is no scrim here at + * all (#171), so the close button is the only pointer route out of a * full-screen surface. Measured at 424×439 before #55: **25×21px**. */ test('offers a way out a thumb can hit', async ({ app }) => { @@ -194,6 +194,33 @@ test.describe('the queue is a screen where it covers the content', () => { expect(box!.width).toBeGreaterThanOrEqual(44); expect(box!.height).toBeGreaterThanOrEqual(44); }); + + /** + * #171 — and it draws no scrim, because there is nowhere to tap. + * + * `.panel-content` is `width: 100%` here, so the scrim sat entirely + * underneath it: measured at 424×439, host, panel and scrim all + * 424×318. #24's tap-outside-to-close cannot exist on a surface with + * no outside, and a `cursor: pointer` layer nobody can reach is a + * claim the component cannot keep. + * + * Asserted as absence rather than by clicking, for the reason the + * issue gives: a naive phone case clicks the scrim's centre and hits + * the panel, so it passes on the build this exists to fail. The scrim + * is still real between 600 and 899px, which `queue-overlay.spec.ts` + * asserts at 900×600 by clicking it. + */ + test('draws no scrim, because a screen has no outside to tap', async ({ + app, + }) => { + await openTheQueue(app); + + const scrim = await queue(app).evaluate( + (el) => el.shadowRoot!.querySelector('.scrim') !== null, + ); + + expect(scrim).toBe(false); + }); }); /** diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index f33a603..164eb09 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -13,6 +13,7 @@ import type { MenuSurface } from '../menu-surface/menu-surface'; import '../menu-surface/menu-surface'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import { QueueController } from '@store/controllers/queue-controller'; +import { PHONE_QUERY } from '@utils/breakpoints'; import { creditStore } from '@store/credit-store'; import { describeQueueSource, @@ -120,6 +121,25 @@ export class QueuePanel @property({ type: Boolean, reflect: true }) overlay = false; + /** + * Phone width, from `matchMedia` rather than from a media query, + * because it decides whether the scrim *exists* (#171) — + * `job-band`'s rule, and a stylesheet cannot express it: a + * `display: none` scrim is still an element with a click handler. + * + * Below 600px the panel spans the whole content area, so the scrim + * has no uncovered pixels: measured at 424x439, host, panel and + * scrim are all 424x318 with the scrim entirely underneath. It dims + * nothing and dismisses nothing there, and the queue is a *screen* + * at that width anyway (#55) — back and a 44px close button are its + * ways out. Between 600 and 899 the panel is a 320px column of a + * wider content area, the scrim is reachable, and #24's + * tap-outside-to-close is real; that band is untouched. + */ + @state() private phone = false; + + private phoneQuery?: MediaQueryList; + @state() private isDragging = false; @@ -391,6 +411,8 @@ export class QueuePanel display: none; } + /* Overlay only, and above 600px only -- see the phone field, + which is where that half is decided (#171). */ .scrim { position: absolute; inset: 0; @@ -414,10 +436,10 @@ export class QueuePanel /* A screen's way out has to be hittable with a thumb. Measured at 424x439 before #55: these were **25x21px**, - and with the panel spanning the whole width the scrim - underneath has no uncovered pixels at all -- so it was - the only pointer route out of a full-screen surface. - Back answers it now as well, which is the other half. + and with the panel spanning the whole width there is no + scrim here at all (#171) -- so this is the only pointer + route out of a full-screen surface. Back answers it now + as well, which is the other half. Sized only in overlay mode: inline these sit in a 320px column beside the content, where a mouse is what reaches @@ -847,6 +869,12 @@ export class QueuePanel // desktop width rather than the minimum. this.updateOverlayMode(); + // Read here rather than in a field initialiser, so a test can + // install its own matchMedia before the element is created. + this.phoneQuery = window.matchMedia?.(PHONE_QUERY); + this.phone = this.phoneQuery?.matches ?? false; + this.phoneQuery?.addEventListener('change', this.onPhoneMedia); + if (this.parentElement) { this.spaceObserver = new ResizeObserver(() => this.updateOverlayMode(), @@ -889,6 +917,8 @@ export class QueuePanel this.creditsUnsub = undefined; this.spaceObserver?.disconnect(); this.spaceObserver = undefined; + this.phoneQuery?.removeEventListener('change', this.onPhoneMedia); + this.phoneQuery = undefined; document.removeEventListener('keydown', this.onOverlayKeydown); document.removeEventListener( 'mousemove', @@ -955,6 +985,10 @@ export class QueuePanel this.overlay = available - this.panelWidth < MAIN_PANEL_FLOOR; }; + private onPhoneMedia = (e: MediaQueryListEvent): void => { + this.phone = e.matches; + }; + /** * Escape closes a scrimmed overlay, which is the one keyboard rule * every dialog in this app already follows. @@ -1991,7 +2025,7 @@ export class QueuePanel const tracks = this.queue.tracks; return html` - ${this.overlay + ${this.overlay && !this.phone ? html`