diff --git a/CLAUDE.md b/CLAUDE.md
index 1d884993..f471b663 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -825,8 +825,15 @@ native ``, no Acrylic or Accent opacity slider. Spec and plan:
Switch (on: `--p-text-soft` track, `--p-bg` knob), the pressed Segmented option and the row
buttons (Default apps, Clear) are the terminal core's own `fields.tsx` since #292 (PrismTerminal
#42 holds them there), and no page keeps a copy. Selection marks (style cards, swatches) and
- Reset links are not buttons and keep the accent, as does the Win+E switch; the chosen rail page
- is grey since #292. `settings/settingsControls.test.ts` holds it.
+ Reset links are not buttons and keep the accent; the chosen rail page is grey since #292.
+ `settings/settingsControls.test.ts` holds it. **EVERY SWITCH IS THE CORE'S** (#318; owner,
+ 2026-10-07: "toggles differ in look i like the teal with black not the green and white", then
+ "it should depend on the theme so only teal on the teal theme"): the Win+E row (once its own
+ accent-and-white exception), the phone server switch and the player menu's toggles use the
+ core's `Switch` or its `SWITCH_ON` / `SWITCH_KNOB_ON`, and no file outside the core draws a
+ `role="switch"`. The LOOK is the core's call: from core 0.26.0 (PrismTerminal #139) an on switch
+ is `--p-sel-bg` with an `--p-on-accent` knob. The `settingsLook` e2e holds every switch on the
+ Explorer page to one track and one knob colour when on.
- **NOTHING WEARS A FOCUS BOX; FOCUS IS THE HOVER'S FILL** (#272; owner, 2026-10-04, of a white box
round the title bar's menu button: "remove the focus effect. go through the ui and remove focus
effects like this"). `index.css`'s base-layer rule takes Chromium's ring off every focused
diff --git a/package-lock.json b/package-lock.json
index 222becf4..85792dcf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "prism",
- "version": "0.95.0",
+ "version": "0.95.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prism",
- "version": "0.95.0",
+ "version": "0.95.1",
"license": "MIT",
"dependencies": {
"@codemirror/commands": "^6.11.0",
diff --git a/package.json b/package.json
index e66be39d..c182ff68 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "prism",
- "version": "0.95.0",
+ "version": "0.95.1",
"description": "A fast, universal media viewer for Windows.",
"author": "Max",
"license": "MIT",
diff --git a/src/renderer/src/components/PhoneDialog.tsx b/src/renderer/src/components/PhoneDialog.tsx
index c1e170de..7131392d 100644
--- a/src/renderer/src/components/PhoneDialog.tsx
+++ b/src/renderer/src/components/PhoneDialog.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState, type JSX } from 'react'
import type { PhoneState } from '@shared/types'
+import { Switch } from 'prism-term-core/renderer/settings/fields'
import { Dialog } from './Dialog'
/**
@@ -53,22 +54,9 @@ export function PhoneDialog({
{/* Said BEFORE the switch goes on: a per-user installer cannot add the
firewall rule, and a declined prompt is a phone that cannot connect
diff --git a/src/renderer/src/components/PlayerMenu.tsx b/src/renderer/src/components/PlayerMenu.tsx
index 273275e5..ac4b92d0 100644
--- a/src/renderer/src/components/PlayerMenu.tsx
+++ b/src/renderer/src/components/PlayerMenu.tsx
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState, type JSX } from 'react'
+import { SWITCH_KNOB_ON, SWITCH_ON } from 'prism-term-core/renderer/settings/fields'
import type { MediaControls } from '../lib/useMediaControls'
import { setPlayerPref, usePlayerPrefs } from '../lib/playerPrefs'
import type { SubTrackInfo } from '../lib/useSubtitles'
@@ -33,8 +34,10 @@ function Toggle({
>
{label}
diff --git a/src/renderer/src/components/settings/WinEShortcutRow.tsx b/src/renderer/src/components/settings/WinEShortcutRow.tsx
index 0273c496..ddbfb68a 100644
--- a/src/renderer/src/components/settings/WinEShortcutRow.tsx
+++ b/src/renderer/src/components/settings/WinEShortcutRow.tsx
@@ -1,5 +1,6 @@
import type { WinEShortcutStatus } from '@shared/winEShortcut'
import { useEffect, useRef, useState, type JSX } from 'react'
+import { Switch } from 'prism-term-core/renderer/settings/fields'
import { SettingRow } from 'prism-term-core/renderer/settings/layout/SettingRow'
import { appOpt } from './appOptions'
import { iconPath } from './icons'
@@ -11,8 +12,12 @@ import { iconPath } from './icons'
* live state lives in the subtext, so a sighted user and a screen reader get
* the same words), and a live region carries it only while there is one.
*
- * The switch keeps the ACCENT when on, the one switch in Settings that does
- * (#202's existing exception): it hands a Windows shortcut to Prism.
+ * The switch is the core's, like every other (#318; owner, 2026-10-07:
+ * "toggles differ in look i like the teal with black not the green and
+ * white", then "it should depend on the theme"). It used to draw its own,
+ * the accent with a white knob (#202's exception), the one switch in
+ * Settings that did; the core's on switch now wears the style's accent with
+ * the ink chosen for it, so there is nothing left to be an exception for.
*/
export function WinEShortcutRow(): JSX.Element {
const [status, setStatus] = useState({
@@ -94,20 +99,12 @@ export function WinEShortcutRow(): JSX.Element {
{said}
)}
-
+ />
)
}
diff --git a/src/renderer/src/components/settings/settingsControls.test.ts b/src/renderer/src/components/settings/settingsControls.test.ts
index 7f0e9744..ac166e4a 100644
--- a/src/renderer/src/components/settings/settingsControls.test.ts
+++ b/src/renderer/src/components/settings/settingsControls.test.ts
@@ -25,15 +25,27 @@ describe('settings controls', () => {
})
// Still accented, since they are not buttons: the chosen card's ring, the
- // chosen swatch's ring, the band slider's thumb, the progress bars drawn
- // inside a style's preview card, and the Win+E switch (its existing
- // exception: it hands a Windows shortcut to Prism).
+ // chosen swatch's ring, the band slider's thumb and the progress bars drawn
+ // inside a style's preview card. The Win+E switch was an exception until
+ // #318; it is the core's switch now, like every other.
it('the accent is only on marks, never on a button', () => {
- const allowed = ['cards.tsx', 'ColourSchemes.tsx', 'MediaPage.tsx', 'TransportMini.tsx', 'WinEShortcutRow.tsx']
+ const allowed = ['cards.tsx', 'ColourSchemes.tsx', 'MediaPage.tsx', 'TransportMini.tsx']
const worn = files.filter(({ src }) => ACCENT.test(src)).map(({ f }) => f)
expect(worn.filter((f) => !allowed.includes(f))).toEqual([])
})
+ // EVERY SWITCH IS THE CORE'S (#318; owner, 2026-10-07: "toggles differ in
+ // look"). The Win+E row and the phone server each drew their own, the accent
+ // with a white knob, beside the core's. No file in the renderer may draw a
+ // switch of its own: one look, and the core decides it.
+ it("every switch in the app is the core's", () => {
+ const root = join(__dirname, '..', '..')
+ const own = (readdirSync(root, { recursive: true }) as string[])
+ .filter((f) => /\.tsx$/.test(f) && !/\.test\.tsx$/.test(f))
+ .filter((f) => /role="switch"/.test(readFileSync(join(root, f), 'utf8')))
+ expect(own).toEqual([])
+ })
+
it('Default apps and Clear are row buttons', () => {
const explorer = files.find(({ f }) => f === 'ExplorerPage.tsx')!.src
expect(explorer).toMatch(/id="default-apps"[\s\S]{0,120}className=\{ROW_BUTTON\}/)
diff --git a/tools/e2e/run.mjs b/tools/e2e/run.mjs
index a57bb7ee..f296637b 100644
--- a/tools/e2e/run.mjs
+++ b/tools/e2e/run.mjs
@@ -14292,6 +14292,27 @@ const settingsLookOf = (win) =>
}
})
+// EVERY SWITCH IS ONE SWITCH (#318; owner, 2026-10-07: "toggles differ in
+// look i like the teal with black not the green and white"). The switches on
+// the page, grouped by state: each group must be ONE look (the button's and
+// the knob's classes, the track's and the knob's computed colours). The Win+E
+// row drew its own, the accent with a white knob, beside the core's.
+const switchLooksOf = (win) =>
+ win.evaluate(() => {
+ const bg = (el) => getComputedStyle(el).backgroundColor
+ return [...document.querySelectorAll('[data-settings-page] [role="switch"]')].map((sw) => {
+ const knob = sw.firstElementChild
+ return {
+ label: sw.getAttribute('aria-label'),
+ on: sw.getAttribute('aria-checked') === 'true',
+ look: JSON.stringify([sw.className, knob?.className ?? '', bg(sw), knob ? bg(knob) : '']),
+ // The markup with the state's own colour classes taken out: the same
+ // for every switch drawn by the one component, on or off.
+ shape: JSON.stringify([sw.className, knob?.className ?? ''].map((c) => c.split(/\s+/).filter((t) => !/^(enabled:hover:|disabled:opacity-)?(bg|brightness|opacity)-/.test(t)).join(' ')))
+ }
+ })
+ })
+
/**
* THE SETTINGS LOOK (#292; owner, 2026-10-05: the approved v1 "Grouped cards",
* with no accent bar on the chosen rail item). Every page in a dark and a
@@ -14360,6 +14381,23 @@ async function settingsLookScenario(fixtures) {
await win.mouse.move(5, 5)
await sleep(450)
const name = view ? `${page}-${view}` : page
+ if (page === 'explorer') {
+ const sws = await switchLooksOf(win)
+ for (const state of [true, false]) {
+ const group = sws.filter((x) => x.on === state)
+ const looks = [...new Set(group.map((x) => x.look))]
+ ok(
+ looks.length <= 1,
+ `${scheme} explorer: every ${state ? 'on' : 'off'} switch has one look (${group.map((x) => x.label).join(', ')}: ${looks.join(' | ')})`
+ )
+ }
+ ok(sws.some((x) => x.on), `${scheme} explorer: an on switch is measured (${sws.filter((x) => x.on).map((x) => x.label).join(', ')})`)
+ // Win+E is off and unavailable outside a packaged build, so it cannot
+ // be measured on: it is held to the same markup as the others, which
+ // is the core's component and so the core's on look.
+ const winE = sws.find((x) => x.label === 'Open in place of File Explorer')
+ ok(!!winE && sws.every((x) => x.shape === winE.shape), `${scheme} explorer: the Win+E switch is the core's (${winE?.shape})`)
+ }
const m = await settingsLookOf(win)
ok(m.label >= 4.5 && m.sub >= 4.5, `${scheme} ${name}: label and subtext read on the panel (${m.label.toFixed(1)}:1, ${m.sub.toFixed(1)}:1)`)
ok(m.icon >= 3, `${scheme} ${name}: the icon reads 3:1 on its tile (${m.icon.toFixed(1)}:1)`)