Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ native `<input type=color>`, no Acrylic or Accent opacity slider. Spec and plan:
"Colours of Volt": "dont have this show the theme name, just call that section colours"). The folder
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.
coat each. Every catalogue theme's panel differs from its ground EXCEPT VOID, which is all black
(#313; owner, 2026-10-07, of Background #000000 and Sidebar and tab bar colour #070707: "make void
fully black for both of these"): its panel is #000000, its panels told apart by its faint edges. A
saved draft colour the theme has since caught up with is dropped at load (`withoutOwn`), so a
hand-set black on Void shows no Reset and no lit Save changes. 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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "prism",
"version": "0.94.2",
"version": "0.94.3",
"description": "A fast, universal media viewer for Windows.",
"author": "Max",
"license": "MIT",
Expand Down
14 changes: 14 additions & 0 deletions src/renderer/src/components/browse/drive-rows.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ section[data-drive-style='tiles'] > h2 + .browse-drive-tiles {
.folder-browser .browse-place.browse-drive-tiles:not([aria-current]):not([data-menu]):hover {
background-color: color-mix(in srgb, var(--p-text) 7.5%, transparent);
}
/* On a black sidebar the same steps glare (owner, 2026-10-07, of the drive
tiles on Void: "too much of a contrast"): about half as strong, hover
included, the track still the clearest grey so the bar reads. */
:root[data-side='black'] .folder-browser .browse-place.browse-drive {
--drive-track: color-mix(in srgb, var(--p-text) 10%, transparent);
--drive-chip: color-mix(in srgb, var(--p-text) 4%, transparent);
}
:root[data-side='black'] .folder-browser .browse-place.browse-drive-tiles {
--drive-tile: color-mix(in srgb, var(--p-text) 2.5%, transparent);
--drive-chip: color-mix(in srgb, var(--p-text) 4%, transparent);
}
:root[data-side='black'] .folder-browser .browse-place.browse-drive-tiles:not([aria-current]):not([data-menu]):hover {
background-color: color-mix(in srgb, var(--p-text) 4.5%, transparent);
}
.folder-browser .browse-place.browse-drive-tiles[aria-current] {
border-color: transparent;
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/lib/fieldColours.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('the toolbar fields (#267)', () => {
it('Void wears the dark field; Paper the control step; both off the row (#306)', () => {
const v = variablesFor(byId('new-void'))
const vRow = v['--p-side-flat']
expect(vRow).toBe('#070707')
expect(vRow).toBe('#000000')
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)
Expand Down
65 changes: 55 additions & 10 deletions src/renderer/src/lib/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
withAlpha
} from 'prism-term-core/renderer/lib/colour'
import { accentAlphaOf, fillOf } from './accentAlpha'
import { controlFieldOn, hintOn, nearBlackField } from './fieldColours'
import { controlFieldOn, hintOn, isNearBlack, nearBlackField } from './fieldColours'
import { THEME_STYLES } from './themes/catalogue'
import { migrateThemeStorage, RETIRED_KEY } from './themes/migrate'
import { RETIRED_MAP } from './themes/retired'
Expand Down Expand Up @@ -1097,9 +1097,17 @@ function mirrorMode(mode: Mode): void {

function paint(style: Style, preview = false): void {
const r = document.documentElement.style
for (const [k, v] of Object.entries(variablesFor(style))) r.setProperty(k, v)
const vars = variablesFor(style)
for (const [k, v] of Object.entries(vars)) r.setProperty(k, v)

document.documentElement.dataset.mode = style.mode
// A sidebar that is black or near it (Void since #313) gets quieter raised
// surfaces: a step that reads as a soft lift on a grey panel is a glare on
// black (owner, 2026-10-07, of the drive tiles on Void: "too much of a
// contrast"). The CSS reads this; no colour is chosen here.
const side = vars['--p-side-flat']
if (side && isNearBlack(side)) document.documentElement.dataset.side = 'black'
else delete document.documentElement.dataset.side
// A translucent style needs the window itself to be transparent, which only
// the main process can arrange.
const translucent = style.material === 'acrylic' || style.material === 'mica'
Expand Down Expand Up @@ -1462,6 +1470,7 @@ function loadRetired(): string | null {
let current = load()
// A preset that has since been deleted leaves a dangling id; normalise it.
current = allStyles().some((s) => s.id === current) ? current : DEFAULT_STYLE
draft = settledDraft(draft)
let retired: string | null = loadRetired()
/** The theme the window is PREVIEWING (the wall's arrows), or null. */
let previewing: string | null = null
Expand Down Expand Up @@ -1636,27 +1645,62 @@ function commitDraft(next: Overrides): void {
apply()
}

/** Is this panel pick the style's own panels? Eight digits are judged on
* their spelling (see `withColour`), six on the colour. */
function chromeIsOwn(base: Style, value: string): boolean {
if (value.length !== 9) return isStylesOwn(base, 'chrome', value)
return [
base.sideOwn ? base.side : sideOf(base),
base.titleOwn ? base.title : titleOf(base),
base.tabsOwn && base.tabs ? base.tabs : tabsOf(base)
].every((c) => c.toLowerCase() === value.toLowerCase())
}

/** A colour role set on a draft, or cleared when it is the style's own. */
function withColour(next: Overrides, role: 'bg' | 'chrome' | 'accent', value: string): Overrides {
// A panel colour with an alpha of its own (eight digits, `ff` included) is
// judged on its spelling: `isStylesOwn` compares stored forms, where `ff`
// drops, so a solid panel on glass in the colour it already had read as the
// style's own put back and was thrown away (review of #251).
const base = byId(current)
const own =
role === 'chrome' && value.length === 9
? [
base.sideOwn ? base.side : sideOf(base),
base.titleOwn ? base.title : titleOf(base),
base.tabsOwn && base.tabs ? base.tabs : tabsOf(base)
].every((c) => c.toLowerCase() === value.toLowerCase())
: isStylesOwn(base, role, value)
const own = role === 'chrome' ? chromeIsOwn(base, value) : isStylesOwn(base, role, value)
if (role === 'chrome') return withChrome(next, own ? null : value)
if (own) delete next[role]
else next[role] = value
return next
}

/**
* A SAVED EDIT THE THEME HAS SINCE CAUGHT UP WITH IS NO EDIT (#313; owner,
* 2026-10-07, of Void: "make void fully black for both of these", having set
* its sidebar colour to #000000 by hand). A colour put back is judged when it
* is written (`withColour`); a theme that moves to a colour already in the
* draft is judged here, at load: each colour that now equals the style's own
* is dropped, so its Reset link and Save changes go. Pure, so it is testable;
* the same object back when nothing moved.
*/
export function withoutOwn(base: Style, o: Overrides): Overrides {
let next: Overrides | null = null
const drop = (keys: Array<keyof Overrides>): void => {
next = next ?? { ...o }
for (const k of keys) delete (next as Overrides)[k]
}
for (const role of ['bg', 'accent', 'text', 'folderIcon', 'selection'] as const) {
const v = o[role]
if (typeof v === 'string' && v && isStylesOwn(base, role, v)) drop([role])
}
if (o.side && o.side === o.title && o.side === o.tabs && chromeIsOwn(base, o.side)) drop(['side', 'title', 'tabs'])
return next ?? o
}

/** The draft as loaded, settled against the style on screen; written back
* when that dropped something, so storage says what the window shows. */
function settledDraft(o: Overrides): Overrides {
const next = withoutOwn(byId(current), o)
if (next !== o) saveJson(DRAFT_KEY, next)
return next
}

/**
* The Primary row's picker (decision 1). The colour's six digits are the
* background; its alpha is the Acrylic slider's level, written ONLY when the
Expand Down Expand Up @@ -1927,6 +1971,7 @@ if (typeof window !== 'undefined') {
presets = cleanPresets(loadJson<unknown>(PRESETS_KEY, []))
draft = cleanDraft(loadJson<Overrides>(DRAFT_KEY, {}))
current = load()
draft = withoutOwn(byId(current), draft)
retired = loadRetired()
version += 1
// The originating window already saved related accent preferences. Receiving
Expand Down
79 changes: 79 additions & 0 deletions src/renderer/src/lib/theme.voidBlack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { STYLES, variablesFor, withoutOwn, type Style } from './theme'
import { CATALOGUE } from './themes/catalogue'

// VOID IS ALL BLACK (#313; owner, 2026-10-07, of Void's Background #000000
// and "Sidebar and tab bar colour" #070707: "make void fully black for both of
// these"). The other themes keep a panel a step off their ground (#302).

const VOID = STYLES.find((s) => s.id === 'new-void') as Style

describe('Void', () => {
it('paints the sidebar, the title bar and the tab bar the black of its ground', () => {
const v = variablesFor(VOID)
expect(VOID.bg).toBe('#000000')
for (const k of ['--p-bg', '--p-side', '--p-side-flat', '--p-title', '--p-tabs', '--p-tab-active']) expect(v[k], k).toBe('#000000')
})

it('still draws its edges, its menus and its selection off the black', () => {
const v = variablesFor(VOID)
expect(v['--p-edge']).not.toBe('#000000')
expect(v['--p-raised']).toBe('#0f0f10')
expect(v['--p-line']).toBe('#151516')
expect(v['--p-sel-tint-seen']).not.toBe('#000000')
})

it('is the only theme whose panel is its ground', () => {
expect(CATALOGUE.filter((t) => t.panel === t.ground).map((t) => t.id)).toEqual(['new-void'])
})
})

describe('a saved edit the theme has caught up with', () => {
it('is no edit: a hand-set black sidebar on Void is dropped, the rest kept', () => {
const draft = { side: '#000000', title: '#000000', tabs: '#000000', accent: '#22aa66' }
expect(withoutOwn(VOID, draft)).toEqual({ accent: '#22aa66' })
expect(withoutOwn(VOID, { side: '#000000', title: '#000000', tabs: '#000000' })).toEqual({})
})

it('keeps a colour that differs, and hands the same draft back when nothing moved', () => {
const draft = { side: '#070707', title: '#070707', tabs: '#070707' }
expect(withoutOwn(VOID, draft)).toBe(draft)
// A panel with an alpha of its own is not the theme's solid panel.
const own = { side: '#00000080', title: '#00000080', tabs: '#00000080' }
expect(withoutOwn(VOID, own)).toBe(own)
})

it("drops the other roles when they are the style's own", () => {
expect(withoutOwn(VOID, { bg: '#000000', text: VOID.text, accent: VOID.accent })).toEqual({})
})
})

describe('at the store', () => {
beforeEach(() => {
vi.resetModules()
localStorage.clear()
vi.stubGlobal('window', new EventTarget())
})
afterEach(() => {
vi.unstubAllGlobals()
vi.resetModules()
})

it("the owner's hand-set black on Void opens as no edit: no Reset, Save changes not lit", async () => {
localStorage.setItem('prism.style', 'new-void')
localStorage.setItem('prism.style.v', '2')
localStorage.setItem('prism.style.draft', JSON.stringify({ side: '#000000', title: '#000000', tabs: '#000000' }))
const theme = await import('./theme')
expect(theme.currentStyle().id).toBe('new-void')
expect(theme.isEdited()).toBe(false)
expect(JSON.parse(localStorage.getItem('prism.style.draft') ?? 'null')).toEqual({})
})

it('an edit that still differs stays an edit', async () => {
localStorage.setItem('prism.style', 'new-void')
localStorage.setItem('prism.style.v', '2')
localStorage.setItem('prism.style.draft', JSON.stringify({ side: '#101010', title: '#101010', tabs: '#101010' }))
const theme = await import('./theme')
expect(theme.isEdited()).toBe(true)
})
})
2 changes: 1 addition & 1 deletion src/renderer/src/lib/themes/catalogue.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"corners": "2",
"edges": "faint",
"ground": "#000000",
"panel": "#070707",
"panel": "#000000",
"raised": "#0f0f10",
"line": "#151516",
"text": "#e8eaf0",
Expand Down
14 changes: 12 additions & 2 deletions tools/e2e/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9104,6 +9104,8 @@ async function sidebarPlacesScenario(fixtures) {
* panel's ground is not the list's and IS the Settings rail's: both are
* --p-side, the "Sidebar and tab bar colour". Before, the places panel was
* the folder browser's --p-bg. The see-through count of coats is seeThrough's.
* Void is the exception (#313; owner, 2026-10-07: "make void fully black for
* both of these"): its sidebar colour is its ground, so there both are black.
*/
async function sidebarGroundScenario(fixtures) {
console.log('sidebar ground')
Expand Down Expand Up @@ -9152,13 +9154,18 @@ async function sidebarGroundScenario(fixtures) {
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})`)
if (style === 'new-void')
// VOID IS ALL BLACK (#313; owner, 2026-10-07: "make void fully black
// for both of these"): its sidebar colour is its ground, #000000.
ok(places === 'rgb(0, 0, 0)' && list === 'rgb(0, 0, 0)', `${style}: the places panel and the list are both black (${places} vs ${list})`)
else 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})`)
// Void's list is black like its row (#313), so only the others differ.
if (style !== 'new-void') 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')
Expand All @@ -9182,6 +9189,9 @@ async function sidebarGroundScenario(fixtures) {
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.mouse.move(2, 400)
await sleep(600)
await win.screenshot({ path: join(SHOTS, `sidebar-ground-settings-${style}.png`) })
await win.click('[aria-label="Settings"]')
await sleep(300)
}
Expand Down
Loading