Apply the design tokens across the stylesheet; one class name per thing (#282)

Colours, shadows, layers, transition durations, radii, font sizes and weights now come from theme.css instead of literals. Timestamps and reply previews use the quieter text colour. The status dot in the join dialogs and the voice panel's mic bars no longer share class names with the avatar's presence dot and the voice recorder's level bar. Dead rules removed; icons are drawn at their own optical size. A test fails if the stylesheet uses an undefined custom property.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
2026-10-08 13:50:21 -04:00
co-authored by Claude Sonnet 5.5
parent 9db25c8d73
commit 3cdfe37c6f
7 changed files with 329 additions and 330 deletions
+34
View File
@@ -271,6 +271,40 @@ const SCENES = [
await sleep(600)
}
},
{
// The date fields appear only with "A range" chosen; they are what had no border colour (#268).
name: 'modal-export-range',
setup: async (p) => {
await openChannel(p, '#general')
await p.evaluate(click('.header-nameplate'))
await sleep(300)
await p.evaluate(click('.context-menu-item', 'Export'))
await sleep(500)
await p.evaluate(`[...document.querySelectorAll('.reason-prompt label')].find((l) => l.innerText.includes('A range'))?.click()`)
await sleep(400)
}
},
{
// The pointer over a channel and over a member: how visible a hover is.
name: 'irc-hover-rows',
setup: async (p) => {
await openChannel(p, '#general')
const c = await centre(p, '.buffer-row', 'dev')
if (c) await mouse(p, 'mouseMoved', c.x, c.y)
await sleep(300)
},
clip: { x: 0, y: 44, width: 420, height: 300 }
},
{
name: 'irc-hover-member',
setup: async (p) => {
await openChannel(p, '#general')
const c = await centre(p, '.nick-row', 'carol')
if (c) await mouse(p, 'mouseMoved', c.x, c.y)
await sleep(300)
},
clip: { x: 860, y: 44, width: 420, height: 300 }
},
{
name: 'toasts',
setup: async (p) => {
+270 -325
View File
File diff suppressed because it is too large Load Diff
@@ -158,7 +158,7 @@ export function AddToConversation({ buffer }: { buffer: BufferEntry }): JSX.Elem
</span>
)}
<span className="ellipsis">{f.globalName || f.username}</span>
<span className={`presence-dot ${f.status || 'offline'}`} />
<span className={`status-dot ${f.status || 'offline'}`} />
</button>
))}
+4 -1
View File
@@ -20,7 +20,10 @@ export function Icon({ name, size = 20, color, fill, className, style }: Props):
width: size,
height: size,
color,
...(fill ? { fontVariationSettings: "'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24" } : {}),
// The glyph is drawn for its size: the font's optical-size axis runs
// 20-48, and small ones are given a little more weight, or they read
// as hairlines next to text.
fontVariationSettings: `'FILL' ${fill ? 1 : 0}, 'wght' ${size <= 14 ? 500 : 400}, 'GRAD' 0, 'opsz' ${Math.min(48, Math.max(20, size))}`,
...style
}}
>
+1 -1
View File
@@ -886,7 +886,7 @@ function DiscordJoin({ account }: { account: Account }): JSX.Element {
)}
<span className="ellipsis">{f.globalName || f.username}</span>
<span className="small muted ellipsis">{f.username}</span>
<span className={`presence-dot ${f.status || 'offline'}`} />
<span className={`status-dot ${f.status || 'offline'}`} />
</button>
))}
+2 -2
View File
@@ -57,9 +57,9 @@ export function VoicePanel(): JSX.Element | null {
<div className="voice-panel">
<div className="voice-panel-head">
<span className="voice-panel-state">
<span className="voice-meter" aria-hidden>
<span className="voice-level" aria-hidden>
{[0, 1, 2].map((i) => (
<span key={i} className={`voice-meter-bar${i < bars ? ' lit' : ''}`} />
<span key={i} className={`voice-level-bar${i < bars ? ' lit' : ''}`} />
))}
</span>
Voice Connected
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const css = ['theme.css', 'app.css'].map((f) => readFileSync(join(__dirname, f), 'utf8')).join('\n')
describe('the stylesheets', () => {
it('only use custom properties that something defines, or that carry a fallback', () => {
const defined = new Set([...css.matchAll(/(--[a-z0-9-]+)\s*:/gi)].map((m) => m[1]))
// `var(--x)` with no comma is a use with no fallback.
const used = [...css.matchAll(/var\((--[a-z0-9-]+)\s*\)/gi)].map((m) => m[1])
const missing = [...new Set(used.filter((name) => !defined.has(name)))]
// An undefined property draws a border in the text colour and text in
// whatever it inherited - which is how seven of them went unnoticed (#268).
expect(missing).toEqual([])
})
})