Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
772c71c49f |
@@ -4912,49 +4912,3 @@ bridge leaves the wizard up with its "Get Started" button correctly
|
|||||||
disabled — it gates on a directory chosen *in the wizard*, and the
|
disabled — it gates on a directory chosen *in the wizard*, and the
|
||||||
existing-library check runs once, on mount. A reload clears it. Nothing
|
existing-library check runs once, on mount. A reload clears it. Nothing
|
||||||
is broken; it cost twenty minutes of believing a tap had been swallowed.
|
is broken; it cost twenty minutes of believing a tap had been swallowed.
|
||||||
|
|
||||||
## The sheet's scroll fade, and where a scrim may not go (measured 2026-08-23, headless)
|
|
||||||
|
|
||||||
#207's answer. The affordance is two background layers on
|
|
||||||
`wa-dialog::part(body)` and the conditionality is
|
|
||||||
`background-attachment`, not a scroll listener: a cover of the sheet's
|
|
||||||
own colour painted at the end of the *content* (`local`) over a shadow
|
|
||||||
pinned to the box (`scroll`), so the cover scrolls up and hides the
|
|
||||||
shadow exactly when there is nothing more to see.
|
|
||||||
|
|
||||||
Measured at 424x360 (which is where a menu overflows on `main`, since
|
|
||||||
`main` does not yet carry #67's eighth item — at 424x439 the track
|
|
||||||
list's seven items are `scrollHeight` 364 against `clientHeight` 364,
|
|
||||||
fitting exactly). Pixel at x=300, dark ramp, `bgElevated` `#343a40`:
|
|
||||||
|
|
||||||
| y | before | more below | at the end of the list |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 330 | 52,58,64 | 50,56,62 | 52,58,64 |
|
|
||||||
| 340 | 52,58,64 | 43,48,53 | 52,58,64 |
|
|
||||||
| 350 | 52,58,64 | 33,37,40 | 52,58,64 |
|
|
||||||
| 359 | 52,58,64 | 22,24,27 | 52,58,64 |
|
|
||||||
|
|
||||||
Three things worth keeping.
|
|
||||||
|
|
||||||
**A menu that fits draws nothing**, which is the same measurement: at
|
|
||||||
424x439 the sheet is flat 52,58,64 to its bottom edge, because with no
|
|
||||||
overflow the `local` layer's positioning area *is* the padding box and
|
|
||||||
the cover lands on top of the shadow.
|
|
||||||
|
|
||||||
**A scrim over a menu row is that row's text surface**, so the 4.5:1
|
|
||||||
rule reaches it and this is why the curve is steep rather than linear.
|
|
||||||
A row is 48px with its label centred; 32px of scrim already down to a
|
|
||||||
quarter strength at 14px puts about 0.06 at the label. Checked on the
|
|
||||||
light ramp (`bgElevated` `#e9ecef`, text `#212529`) by overriding the
|
|
||||||
two custom properties on `:root`: background at the label 205,207,210,
|
|
||||||
which is **9.9:1**. The first draft — a linear 48px at 0.8 — put ~0.375
|
|
||||||
on that label, 5.0:1, passing but visibly greyed. The bottom few pixels
|
|
||||||
go to ~2.4:1 in either draft and are deliberately below where any
|
|
||||||
label of a *fully visible* row sits; a label that lands there belongs
|
|
||||||
to the half-cut row, which is the thing being signalled.
|
|
||||||
|
|
||||||
**A dark scrim on a dark surface reads far worse in a shrunk screenshot
|
|
||||||
than on screen.** The first two probes (24px/0.45, then 32px/0.75) were
|
|
||||||
measurably present — 52,58,64 down to 30,33,37 — and invisible in the
|
|
||||||
inline preview. Crop the bottom 70px and scale it up before judging;
|
|
||||||
the pixel values are the honest answer either way.
|
|
||||||
|
|||||||
@@ -262,6 +262,26 @@ real store code. A binding carries an **ID**, not a name
|
|||||||
`yellowjacket/backend/home.Service.GetShelves`), so the fake derives
|
`yellowjacket/backend/home.Service.GetShelves`), so the fake derives
|
||||||
that map from the generated tree rather than writing it down.
|
that map from the generated tree rather than writing it down.
|
||||||
|
|
||||||
|
**A test file does not get its own origin, so `setup.ts` clears
|
||||||
|
`localStorage` between tests.** `@vitest/browser-playwright` opens one
|
||||||
|
BrowserContext per session and runs several files in it one after
|
||||||
|
another, so everything a component persists — the track list's sort and
|
||||||
|
column widths, the cover size, `now-playing`'s scroll mode — is still
|
||||||
|
there when the next file mounts the same component. Which files share a
|
||||||
|
tab, and in what order, changes run to run, so the symptom is a spec
|
||||||
|
that fails about one test in three and passes every time it is run on
|
||||||
|
its own: #138 cost three scheduled runs, one of them a PR whose diff
|
||||||
|
held no frontend code at all. Measured on the build before the fix, a
|
||||||
|
single full run started **24** tests with storage already set. Two
|
||||||
|
things follow. The clear is safe precisely because the leak is
|
||||||
|
sequential — files in a session do not overlap, so it cannot wipe
|
||||||
|
storage a concurrently-running file is in the middle of using — and it
|
||||||
|
belongs in `setup.ts` rather than in the specs that write, because the
|
||||||
|
spec that *reads* is never the one that knows. And a spec whose
|
||||||
|
assertion depends on an order still **states that order** rather than
|
||||||
|
inheriting a default, or the next change to a default is the same
|
||||||
|
mystery again.
|
||||||
|
|
||||||
**`frontend/bindings/` is generated by `wails3`, not `go generate`**, so
|
**`frontend/bindings/` is generated by `wails3`, not `go generate`**, so
|
||||||
the pre-commit codegen check does not cover it. `make bindings-check`
|
the pre-commit codegen check does not cover it. `make bindings-check`
|
||||||
(~3.5 s warm, ~20 s on a cold build cache, also a pre-commit hook)
|
(~3.5 s warm, ~20 s on a cold build cache, also a pre-commit hook)
|
||||||
@@ -1402,7 +1422,7 @@ descendants. On the reference device the main panel spans 0-318 of a
|
|||||||
items cut off, with no way to reach them. `showModal()` is Chrome 37
|
items cut off, with no way to reach them. `showModal()` is Chrome 37
|
||||||
and uses the real top layer, so a dialog is immune by construction.
|
and uses the real top layer, so a dialog is immune by construction.
|
||||||
|
|
||||||
Seven things about it are load-bearing.
|
Six things about it are load-bearing.
|
||||||
|
|
||||||
**"Dialogs are fine" needed checking, because every other dialog in
|
**"Dialogs are fine" needed checking, because every other dialog in
|
||||||
this app is mounted in `index.html`** — outside `.main-panel` — so it
|
this app is mounted in `index.html`** — outside `.main-panel` — so it
|
||||||
@@ -1431,25 +1451,6 @@ doing nothing, which reads as the gesture breaking. `menu-dismiss` is
|
|||||||
that signal; the three surfaces that do not use `ContextMenuController`
|
that signal; the three surfaces that do not use `ContextMenuController`
|
||||||
bind it themselves.
|
bind it themselves.
|
||||||
|
|
||||||
**A sheet that scrolls says so, and `background-attachment` is what
|
|
||||||
asks whether it does** (#207). The sheet is capped at 85vh — a surface
|
|
||||||
covering the whole screen is a page, not a sheet — so a long menu's
|
|
||||||
body scrolls, and for three phases it scrolled *silently*: measured at
|
|
||||||
424x439, eight items ended at y=470 with the fold at 439, and where the
|
|
||||||
cut lands on a row boundary the sheet ends in a clean edge that reads
|
|
||||||
as the end of the list. The fade is two background layers on
|
|
||||||
`wa-dialog::part(body)` — a shadow pinned to the box (`scroll`) under a
|
|
||||||
cover of the sheet's own colour painted at the end of the *content*
|
|
||||||
(`local`), which scrolls up over the shadow exactly when there is
|
|
||||||
nothing more to see. So it is absent on a menu that fits, present the
|
|
||||||
moment one does not, and gone again at the end of the list, with no
|
|
||||||
scroll listener and nothing reaching into `wa-dialog`'s shadow root for
|
|
||||||
the scroller. **The curve is steep because the rows under it stay
|
|
||||||
live**: a scrim over a menu item is that item's text surface, and the
|
|
||||||
4.5:1 rule applies to it — 32px already down to a quarter strength at
|
|
||||||
14px spends its weight below the last legible label, measured at 9.9:1
|
|
||||||
on the light ramp, whose `bgElevated` is `#e9ecef`.
|
|
||||||
|
|
||||||
**The playlist submenu is a sheet too, and it had to be.** It is a
|
**The playlist submenu is a sheet too, and it had to be.** It is a
|
||||||
`placement="right-start"` flyout, and making the menu full-width moved
|
`placement="right-start"` flyout, and making the menu full-width moved
|
||||||
its anchor — measured at x −182 to 0, entirely off-screen, so "Add to
|
its anchor — measured at x −182 to 0, entirely off-screen, so "Add to
|
||||||
|
|||||||
@@ -155,55 +155,10 @@ export class MenuSurface extends LitElement {
|
|||||||
bottom was at y=452 on a 439px screen -- the one row a
|
bottom was at y=452 on a 439px screen -- the one row a
|
||||||
destructive action is most likely to be. The cap has to stay
|
destructive action is most likely to be. The cap has to stay
|
||||||
(a sheet covering the whole screen is a page, not a sheet),
|
(a sheet covering the whole screen is a page, not a sheet),
|
||||||
so the body is what gives.
|
so the body is what gives. */
|
||||||
|
|
||||||
**And a body that scrolls says so** (#207). Scrolling was the
|
|
||||||
whole of the fix above, which left the last item reachable
|
|
||||||
and nothing on screen admitting it was there -- measured at
|
|
||||||
424x439, eight items ending at y=470 with the fold at 439,
|
|
||||||
and worse when the cut lands on a row boundary, where the
|
|
||||||
sheet ends in a clean edge that reads as the end of the list.
|
|
||||||
|
|
||||||
Two layers, and the *order* is what asks the question: a
|
|
||||||
shadow pinned to the bottom of the box (attachment scroll),
|
|
||||||
and over it a cover of the sheet's own colour painted at the
|
|
||||||
end of the *content* (attachment local), which therefore
|
|
||||||
scrolls up over the shadow and hides it exactly when there is
|
|
||||||
nothing more to see. So the affordance is absent on a menu
|
|
||||||
that fits, present the moment one does not, and gone again at
|
|
||||||
the end of the list -- with no scroll listener, no
|
|
||||||
measurement, and nothing reaching into wa-dialog's shadow
|
|
||||||
root for the scroller. background-attachment is Chrome 4;
|
|
||||||
the reference device is Chrome 113.
|
|
||||||
|
|
||||||
**The curve is steep because the rows under it stay live.**
|
|
||||||
A scrim over a menu item is that item's text surface, and
|
|
||||||
this app's rule is that text clears 4.5:1 on every surface it
|
|
||||||
can sit on -- which the light ramp, whose bgElevated is
|
|
||||||
#e9ecef, is what makes non-theoretical. A row is 48px with
|
|
||||||
its label centred, so 32px of scrim that is already down to
|
|
||||||
a quarter strength at 14px reaches y-centre at about 0.06 and
|
|
||||||
spends its weight on the strip below the last legible label.
|
|
||||||
Measured on the dark ramp at x=300, flat 52,58,64 throughout
|
|
||||||
before: 50,56,62 at y=330, 33,37,40 at y=350 and 22,24,27 at
|
|
||||||
the bottom edge, and flat again at the end of the list. The
|
|
||||||
light ramp puts 9.9:1 on the last label. */
|
|
||||||
wa-dialog::part(body) {
|
wa-dialog::part(body) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
var(--yj-bg-elevated, #343a40),
|
|
||||||
var(--yj-bg-elevated, #343a40)
|
|
||||||
)
|
|
||||||
bottom / 100% 32px no-repeat local,
|
|
||||||
linear-gradient(
|
|
||||||
to top,
|
|
||||||
rgba(0, 0, 0, 0.6) 0%,
|
|
||||||
rgba(0, 0, 0, 0.25) 45%,
|
|
||||||
rgba(0, 0, 0, 0) 100%
|
|
||||||
)
|
|
||||||
bottom / 100% 32px no-repeat scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A sheet is dragged at with a thumb, so it says where its top
|
/* A sheet is dragged at with a thumb, so it says where its top
|
||||||
|
|||||||
@@ -206,56 +206,6 @@ describe('menu-surface', () => {
|
|||||||
expect(dismissed, 'no menu-dismiss reached the document').toBe(1);
|
expect(dismissed, 'no menu-dismiss reached the document').toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* The scroll affordance (#207), and this is the mechanism again
|
|
||||||
* rather than the symptom.
|
|
||||||
*
|
|
||||||
* The sheet's body has scrolled since #60 and said nothing about
|
|
||||||
* it: measured at 424x439, eight items ended at y=470 with the
|
|
||||||
* fold at 439, and where the cut lands on a row boundary the sheet
|
|
||||||
* ends in a clean edge that reads as the end of the list.
|
|
||||||
*
|
|
||||||
* What makes the fade *conditional* — absent on a menu that fits,
|
|
||||||
* present the moment one does not, gone again at the end of the
|
|
||||||
* list — is `background-attachment`, not a scroll listener: a cover
|
|
||||||
* of the sheet's own colour is painted at the end of the content
|
|
||||||
* and attached `local`, over a shadow pinned to the box and
|
|
||||||
* attached `scroll`. So the pair of attachments *is* the feature,
|
|
||||||
* and it is what this asserts. The rendered result was measured in
|
|
||||||
* the harness (dark ramp 52,58,64 flat before; 52,57,63 at the last
|
|
||||||
* label and 22,24,27 at the bottom edge with more below; flat again
|
|
||||||
* at the end of the list) and is on the PR.
|
|
||||||
*/
|
|
||||||
it('paints the fade only while there is more below', async () => {
|
|
||||||
const el = await surfaceWithPanel();
|
|
||||||
|
|
||||||
const wrapper = el.shadowRoot?.querySelector('wa-dialog');
|
|
||||||
|
|
||||||
await (wrapper as HTMLElement & { updateComplete: Promise<unknown> })
|
|
||||||
.updateComplete;
|
|
||||||
|
|
||||||
const body = wrapper?.shadowRoot?.querySelector('[part~="body"]');
|
|
||||||
|
|
||||||
expect(body, 'no body part to scroll').not.toBeNull();
|
|
||||||
|
|
||||||
const style = getComputedStyle(body as Element);
|
|
||||||
|
|
||||||
expect(style.overflowY, 'the body is what gives, not the cap').toBe(
|
|
||||||
'auto',
|
|
||||||
);
|
|
||||||
|
|
||||||
// The cover scrolls with the content; the shadow does not. Either
|
|
||||||
// one alone is a fade that is always there or never there.
|
|
||||||
expect(
|
|
||||||
style.backgroundAttachment,
|
|
||||||
'the cover must be local and the shadow must not',
|
|
||||||
).toBe('local, scroll');
|
|
||||||
|
|
||||||
// Both sit at the bottom, or the cover hides nothing.
|
|
||||||
expect(style.backgroundPosition).toBe('50% 100%, 50% 100%');
|
|
||||||
expect(style.backgroundSize).toBe('100% 32px, 100% 32px');
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog with no accessible name is what `utils/name-dialog.ts`
|
* A dialog with no accessible name is what `utils/name-dialog.ts`
|
||||||
* exists for; here the name is already written on the panel, so no
|
* exists for; here the name is already written on the panel, so no
|
||||||
|
|||||||
@@ -161,6 +161,15 @@ describe('double-clicking a row in the track list', () => {
|
|||||||
localStorage.removeItem('track-list-column-widths');
|
localStorage.removeItem('track-list-column-widths');
|
||||||
|
|
||||||
el = await fixture<LitElement>('track-list', { externalTracks: LIST });
|
el = await fixture<LitElement>('track-list', { externalTracks: LIST });
|
||||||
|
|
||||||
|
// Say which order is being asserted rather than inheriting one.
|
||||||
|
// `restoreSortPreferences()` runs in `connectedCallback`, so the
|
||||||
|
// list opens in whatever sort was last persisted -- and
|
||||||
|
// `track-11` sorts before `track-3` by title, which is the shape
|
||||||
|
// of #138. The row below is the fixture's third track only while
|
||||||
|
// nothing is sorting the list.
|
||||||
|
(el as unknown as { sortField: string | null }).sortField = null;
|
||||||
|
|
||||||
el.style.display = 'block';
|
el.style.display = 'block';
|
||||||
el.style.height = '600px';
|
el.style.height = '600px';
|
||||||
await flush();
|
await flush();
|
||||||
@@ -172,6 +181,10 @@ describe('double-clicking a row in the track list', () => {
|
|||||||
const rows = shadowAll(el, '.track-row');
|
const rows = shadowAll(el, '.track-row');
|
||||||
const row = rows.find((r) => r.getAttribute('data-index') === '3');
|
const row = rows.find((r) => r.getAttribute('data-index') === '3');
|
||||||
|
|
||||||
|
// Stated first, so a list that is not in the order this asserts
|
||||||
|
// fails by saying so rather than as an off-by-eight file path.
|
||||||
|
expect(row?.getAttribute('data-file-path')).toBe('/music/track-3.mp3');
|
||||||
|
|
||||||
dblclick(row!);
|
dblclick(row!);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,23 @@ document.body.style.margin = '0';
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetHarness();
|
resetHarness();
|
||||||
|
|
||||||
|
// A test file does not get its own origin. `@vitest/browser-playwright`
|
||||||
|
// opens one BrowserContext per session and runs several files in it,
|
||||||
|
// one after another, so everything a component persists — the track
|
||||||
|
// list's sort and column widths, the cover size, `now-playing`'s
|
||||||
|
// scroll mode — is still there when the next file mounts the same
|
||||||
|
// component. That is invisible until it is intermittent, because
|
||||||
|
// which files share a tab and in what order changes run to run: it
|
||||||
|
// cost #138 three scheduled runs, one of them a PR with no frontend
|
||||||
|
// code in it at all.
|
||||||
|
//
|
||||||
|
// Clearing here rather than in the specs that write is deliberate —
|
||||||
|
// the spec that *reads* is never the one that knows. It is safe for
|
||||||
|
// the same reason the leak exists: files in a session are
|
||||||
|
// sequential, so this cannot wipe storage a concurrent file is in
|
||||||
|
// the middle of using.
|
||||||
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user