diff --git a/CLAUDE.md b/CLAUDE.md index e75e12cf..9eef5ce9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,13 @@ native ``, no Acrylic or Accent opacity slider. Spec and plan: browser paints no ground; its address row, list, preview slot and status paint `--p-bg`, the places panel (and `.browse-viewer-places`) `--p-side`, the Settings rail's and project tree's colour, one coat each. Every catalogue theme's panel differs from its ground. The `sidebarGround` e2e holds both. + **THE ADDRESS ROW WEARS IT TOO** (#306; owner, 2026-10-07, of a mockup with the row in the panel + colour: "yes make this the same color as the sidebar"): back/forward/up/refresh, the address field, + preview and search, across the window (`.folder-browser > .browse-toolbar`, and + `.browse-viewer-toolbar` over an opened file), `--p-side`, its hairline kept, so only the list, + preview slot and status are `--p-bg`. The address field's fill and edge step off the ROW + (`--p-side-flat`), not the page: off the page they sat at 1.01:1 on the row on every dark style + (MEASURED). Near-black is still judged by the page (`nearBlackField(bg, text, row)`). ## Scope diff --git a/package-lock.json b/package-lock.json index c5797b71..fd813750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prism", - "version": "0.93.0", + "version": "0.93.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prism", - "version": "0.93.0", + "version": "0.93.1", "license": "MIT", "dependencies": { "@codemirror/commands": "^6.11.0", diff --git a/package.json b/package.json index af0b71d2..dba9f3bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prism", - "version": "0.93.0", + "version": "0.93.1", "description": "A fast, universal media viewer for Windows.", "author": "Max", "license": "MIT", diff --git a/src/renderer/src/components/browse/BrowseToolbar.tsx b/src/renderer/src/components/browse/BrowseToolbar.tsx index 0b44730c..ad472486 100644 --- a/src/renderer/src/components/browse/BrowseToolbar.tsx +++ b/src/renderer/src/components/browse/BrowseToolbar.tsx @@ -199,7 +199,7 @@ export function BrowseToolbar(props: Props): JSX.Element { name={crumb.name} color="var(--p-tree-zip)" size={14} - bg="var(--p-control)" + bg="var(--p-field)" /> )} {crumb.name} diff --git a/src/renderer/src/components/browse/browse.css b/src/renderer/src/components/browse/browse.css index c319973c..45e8a7fa 100644 --- a/src/renderer/src/components/browse/browse.css +++ b/src/renderer/src/components/browse/browse.css @@ -25,16 +25,22 @@ see-through coats are an opaque slab on glass): the address row, the list, the preview slot and the status line --p-bg, the places panel --p-side. The four cover the grid between them, with no gap. A peeking panel keeps - its flat --p-side-flat (below), being laid over the list. */ + its flat --p-side-flat (below), being laid over the list. + THE ADDRESS ROW WEARS IT TOO (#306; owner, 2026-10-07, of a mockup with + the row in the panel colour: "yes make this the same color as the + sidebar"): back, forward, up, refresh, the address field, the preview + toggle and search, across the whole window, so the sidebar colour runs + along the top and down the left and the list is the one --p-bg area. + Its hairline stays. Still one coat: the row is its own grid cell. */ .folder-browser > .browse-toolbar, +.folder-browser > .browse-places { + background-color: var(--p-side); +} .folder-browser > .browse-list-area, .folder-browser > .browse-preview-slot, .folder-browser > .browse-status { background-color: var(--p-bg); } -.folder-browser > .browse-places { - background-color: var(--p-side); -} .folder-browser[data-preview] { grid-template-columns: var(--browse-places-width) minmax(0, 1fr) var( --browse-preview-width, @@ -176,9 +182,9 @@ min-height: 48px; flex-shrink: 0; color: var(--p-text); - /* No ground of its own (#294): it sits on the folder browser's ground, or - on .browse-viewer-toolbar's over a file, and a second see-through coat - of --p-bg made the address bar an opaque band on a glass style. */ + /* No ground in this rule (#294): the folder browser paints its cell above + (#306: --p-side), .browse-viewer-toolbar paints it over a file, and a + second see-through coat made the address bar an opaque band on glass. */ font-family: 'Segoe UI', sans-serif; font-size: 15px; } @@ -225,7 +231,8 @@ "make the url box more visible"). The breadcrumb sat on the toolbar with no box, so only the search looked like somewhere to type. Both wear this: one height, radius, fill and edge, from the --p-field tokens (lib/fieldColours: - on a near-black ground a darker fill and an edge that carries the shape). */ + on a near-black ground a darker fill and an edge that carries the shape). + Both are stepped off the ROW's ground, the sidebar colour (#306). */ .browse-field { height: 36px; border: 1px solid var(--p-field-edge); diff --git a/src/renderer/src/components/browse/workspace.css b/src/renderer/src/components/browse/workspace.css index 70968956..21e1c0ae 100644 --- a/src/renderer/src/components/browse/workspace.css +++ b/src/renderer/src/components/browse/workspace.css @@ -4,9 +4,11 @@ .browse-surface-host { z-index: 1; } +/* Over an opened file the address row is the same row, so the same sidebar + colour (#306), one coat: the .browse-toolbar inside paints nothing here. */ .browse-viewer-toolbar { flex-shrink: 0; - background: var(--p-bg); + background: var(--p-side); } .browse-viewer-toolbar > .browse-toolbar { height: 48px; diff --git a/src/renderer/src/lib/fieldColours.test.ts b/src/renderer/src/lib/fieldColours.test.ts index d0d4f0e4..42cc47e5 100644 --- a/src/renderer/src/lib/fieldColours.test.ts +++ b/src/renderer/src/lib/fieldColours.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { contrastOf, hintOn, isNearBlack, nearBlackField } from './fieldColours' +import { contrastOf, controlFieldOn, hintOn, isNearBlack, nearBlackField } from './fieldColours' import { STYLES, variablesFor } from './theme' const byId = (id: string) => STYLES.find((s) => s.id === id)! @@ -38,17 +38,36 @@ describe('the toolbar fields (#267)', () => { expect(contrastOf('#707174', '#e7e7e8')).toBeLessThan(4.5) }) - it('Void wears the dark field; Paper keeps the fill it had', () => { + it('Void wears the dark field; Paper the control step; both off the row (#306)', () => { const v = variablesFor(byId('new-void')) - expect(v['--p-field']).toBe('#060606') - expect(contrastOf(v['--p-field-edge'], '#000000')).toBeGreaterThanOrEqual(1.6) - expect(contrastOf(v['--p-field-edge'], '#000000')).toBeLessThan(1.8) + const vRow = v['--p-side-flat'] + expect(vRow).toBe('#070707') + expect(v['--p-field']).toBe(nearBlackField('#000000', '#e8eaf0', vRow)!.fill) + expect(contrastOf(v['--p-field-edge'], vRow)).toBeGreaterThanOrEqual(1.6) + expect(contrastOf(v['--p-field-edge'], vRow)).toBeLessThan(1.8) const p = variablesFor(byId('paper')) - expect(p['--p-field']).toBe(p['--p-control']) - expect(p['--p-field']).toBe('#e7e7e8') + expect(p['--p-field']).toBe(controlFieldOn(p['--p-side-flat'], '#1b1d21', true)) expect(p['--p-field-edge']).toBe(p['--p-divider']) }) + it('a near-black style is judged by its page, its field coloured off the row', () => { + // Aurora's page is near-black, its sidebar (#121419) is not: it keeps + // the quiet field, stepped off the sidebar. + const f = nearBlackField('#0b0d12', '#e8eaf0', '#121419')! + expect(f).not.toBeNull() + expect(contrastOf(f.fill, '#121419')).toBeGreaterThan(1.02) + expect(contrastOf(f.edge, '#121419')).toBeGreaterThanOrEqual(1.6) + }) + + it('every shipped style: the address field stands off the row it sits on (#306)', () => { + // MEASURED before: 1.01:1 on every dark style, the fill a step off the + // page and the row a step off it the other way. + for (const s of STYLES) { + const v = variablesFor(s, true) + expect(contrastOf(v['--p-field'], v['--p-side-flat']), s.id).toBeGreaterThan(1.03) + } + }) + it('every shipped style: the field text and hint read on the field', () => { for (const s of STYLES) { const v = variablesFor(s, true) diff --git a/src/renderer/src/lib/fieldColours.ts b/src/renderer/src/lib/fieldColours.ts index 0e1864a2..f33583b2 100644 --- a/src/renderer/src/lib/fieldColours.ts +++ b/src/renderer/src/lib/fieldColours.ts @@ -78,16 +78,29 @@ export interface FieldColours { /** * The field's colours on a near-black `ground` (both flat hex), or null where * the style's own control fill and edge stay in force. + * + * `row` is the ground the field actually SITS on, when that is not the page's + * (#306: the address row wears the sidebar colour, a step off the page). The + * style is judged near-black by its page, so a black style whose sidebar is + * lifted keeps its quiet field; the colours step off the row, or a fill one + * shade off the page vanished into a sidebar one shade off it the other way + * (MEASURED: 1.01:1 on Void and Aurora). */ -export function nearBlackField(ground: string, text: string): FieldColours | null { +export function nearBlackField(ground: string, text: string, row: string = ground): FieldColours | null { if (!isNearBlack(ground)) return null return { - fill: mixHex(ground, text, FILL_STEP), - edge: lineAt(ground, text, EDGE), - edgeHover: lineAt(ground, text, EDGE_HOVER) + fill: mixHex(row, text, FILL_STEP), + edge: lineAt(row, text, EDGE), + edgeHover: lineAt(row, text, EDGE_HOVER) } } +/** The field's fill anywhere else: the control step (3.5% towards the text on + * dark, 9% on light), taken from the ground the field sits on. */ +export function controlFieldOn(row: string, text: string, light: boolean): string { + return mixHex(row, text, light ? 0.09 : 0.035) +} + /** A hint (the search's placeholder) held to 4.5:1 on the field it sits in: * the dim ink is chosen against the page, and on a light style's field fill * it fell to 3.95:1 (MEASURED on Paper). Walks the hint towards the text diff --git a/src/renderer/src/lib/theme.ts b/src/renderer/src/lib/theme.ts index fc811106..2c07fc05 100644 --- a/src/renderer/src/lib/theme.ts +++ b/src/renderer/src/lib/theme.ts @@ -14,7 +14,7 @@ import { withAlpha } from 'prism-term-core/renderer/lib/colour' import { accentAlphaOf, fillOf } from './accentAlpha' -import { hintOn, nearBlackField } from './fieldColours' +import { controlFieldOn, hintOn, nearBlackField } from './fieldColours' import { THEME_STYLES } from './themes/catalogue' import { migrateThemeStorage, RETIRED_KEY } from './themes/migrate' import { RETIRED_MAP } from './themes/retired' @@ -781,9 +781,12 @@ export function variablesFor(input: Style, opaque = false): Record { places = await groundOf('.folder-browser > .browse-places') list = await groundOf('.folder-browser > .browse-list-area') - return places === side + row = await groundOf('.folder-browser > .browse-toolbar') + return places === side && row === side }, 4000, 100) ok(places !== null && places !== list, `${style}: the places panel's ground is not the list's (${places} vs ${list})`) ok(places === side, `${style}: the places panel wears the sidebar colour (${places}, --p-side ${side})`) + // 3. THE ADDRESS ROW (#306; owner, 2026-10-07: "yes make this the same + // color as the sidebar"): the places panel's ground, not the list's, + // its hairline kept, and its field still a box on it. + ok(row === places, `${style}: the address row wears the places panel's ground (${row} vs ${places})`) + ok(row !== list, `${style}: and not the list's (${row} vs ${list})`) + const rowLook = await win.evaluate(() => { + const tb = document.querySelector('.folder-browser > .browse-toolbar') + const field = tb?.querySelector('nav.browse-path') + const s = tb ? getComputedStyle(tb) : null + return { + line: s ? { w: parseFloat(s.borderBottomWidth), c: s.borderBottomColor } : null, + field: field ? getComputedStyle(field).backgroundColor : null, + wide: tb ? Math.abs(tb.getBoundingClientRect().width - document.querySelector('.folder-browser').getBoundingClientRect().width) < 1 : false + } + }) + ok(rowLook.wide, `${style}: the row runs across the whole browser`) + ok(rowLook.line && rowLook.line.w > 0 && rowLook.line.c !== row, `${style}: the row keeps its bottom hairline (${JSON.stringify(rowLook.line)})`) + ok(rowLook.field && rowLook.field !== row, `${style}: the address field is not the row's colour (${rowLook.field} on ${row})`) await win.mouse.move(2, 400) await sleep(300) await win.screenshot({ path: join(SHOTS, `sidebar-ground-${style}.png`) }) + const vw = await win.evaluate(() => window.innerWidth) + await win.screenshot({ path: join(SHOTS, `address-row-${style}.png`), clip: { x: 0, y: 0, width: vw, height: 220 } }) await settingsPage(win, 'appearance') await win.waitForSelector('[data-settings-page] > nav', { timeout: 10000 }) const rail = await groundOf('[data-settings-page] > nav') ok(rail === places, `${style}: and that is the Settings rail's ground (${rail})`) + ok(rail === row, `${style}: and the address row's (${row})`) await win.click('[aria-label="Settings"]') await sleep(300) } @@ -9954,13 +9977,17 @@ async function addressFieldScenario(fixtures) { const cr = contrast(rgb(l.crumb), rgb(l.path.fill)) ok(cr >= 4.5, `${name}: a name reads on the field (${cr.toFixed(2)}:1)`) } - // On Void the fill is DARKER than the old control step (rgb 8,8,8), still - // a step off the black, and a quiet edge carries the box (owner, - // 2026-10-04: "the white border stands out too much on the black theme"). - ok(lum(rgb(v.path.fill)) < lum([8, 8, 8]) && lum(rgb(v.path.fill)) > 0, `Void: the field is a darker grey than before (${v.path.fill})`) + // On Void the fill is only a shade off the row it sits on, and a quiet + // edge carries the box (owner, 2026-10-04: "the white border stands out + // too much on the black theme"). Since #306 the row is the sidebar + // colour, and both step off THAT: a fill stepped off the page sat at + // 1.01:1 on the row (MEASURED), a field with no shape but its edge. + const vFill = contrast(rgb(v.path.fill), rgb(v.ground)) + ok(vFill > 1.02 && vFill < 1.1, `Void: the field is a shade off the row, not a slab (${v.path.fill} on ${v.ground}, ${vFill.toFixed(3)}:1)`) const edge = contrast(rgb(v.path.edge), rgb(v.ground)) ok(edge >= 1.5 && edge < 1.9, `Void: the field's edge is a quiet line, not a white frame (${edge.toFixed(2)}:1)`) - ok(p.path.fill === 'rgb(231, 231, 232)', `Paper: the field wears the control fill (${p.path.fill})`) + const pFill = contrast(rgb(p.path.fill), rgb(p.ground)) + ok(pFill > 1.1 && lum(rgb(p.path.fill)) < lum(rgb(p.ground)), `Paper: the field wears the control step off the row (${p.path.fill} on ${p.ground}, ${pFill.toFixed(3)}:1)`) ok(v.path.fill !== p.path.fill, 'Void and Paper fill the field differently') // A hover strengthens the edge and leaves the fill alone. const pathBox = win.locator('.folder-browser [data-testid="browse-toolbar"] nav.browse-path')