100 color schemes, a plainer README
Six more schemes by hand and 89 generated from a hue and an accent (npm run themes), with a readability test over all of them and a search box in the picker. The README describes the software plainly and covers text channels, the Rooms interface and the new settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+52
-1
@@ -359,7 +359,28 @@ try {
|
||||
await page.getByRole('radio', { name: 'Windows 95' }).click();
|
||||
assert.equal(await page.evaluate(() => document.documentElement.dataset.theme), 'win95');
|
||||
assert.equal(await page.locator('.rail').evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(0, 128, 128)');
|
||||
await page.getByRole('radio', { name: 'Dark' }).click();
|
||||
for (const [name, id, bg] of [['Forest', 'forest', 'rgb(22, 35, 28)'], ['Ember', 'ember', 'rgb(33, 25, 20)'], ['Ocean', 'ocean', 'rgb(15, 30, 44)'],
|
||||
['Plum', 'plum', 'rgb(32, 22, 40)'], ['Paper', 'paper', 'rgb(248, 243, 231)'], ['High contrast', 'contrast', 'rgb(0, 0, 0)']]) {
|
||||
await page.getByRole('radio', { name }).click();
|
||||
assert.equal(await page.evaluate(() => document.documentElement.dataset.theme), id);
|
||||
assert.equal(await page.locator('.chat').evaluate(el => getComputedStyle(el).backgroundColor), bg, `${name} colors the chat`);
|
||||
if (shots) await page.screenshot({ path: path.join(shots, `theme-pick-${id}.png`) });
|
||||
}
|
||||
// Generated schemes, and narrowing the list
|
||||
assert.equal(await page.getByRole('radiogroup', { name: 'Color scheme' }).getByRole('radio').count(), 100);
|
||||
await page.getByRole('button', { name: 'Light', exact: true }).click();
|
||||
assert.equal(await page.getByRole('radiogroup', { name: 'Color scheme' }).getByRole('radio').count(), 28);
|
||||
await page.getByRole('radio', { name: 'Lavender' }).click();
|
||||
assert.equal(await page.evaluate(() => document.documentElement.dataset.theme), 'lavender');
|
||||
if (shots) await page.screenshot({ path: path.join(shots, 'theme-pick-lavender.png') });
|
||||
await page.getByRole('button', { name: 'All 100' }).click();
|
||||
await page.getByLabel('Find a color scheme').fill('synth');
|
||||
assert.equal(await page.getByRole('radiogroup', { name: 'Color scheme' }).getByRole('radio').count(), 1);
|
||||
await page.getByRole('radio', { name: 'Synthwave' }).click();
|
||||
assert.equal(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('color-scheme').trim()), 'dark');
|
||||
if (shots) { await page.getByRole('button', { name: 'Done', exact: true }).click(); await page.screenshot({ path: path.join(shots, 'theme-synthwave.png') }); await page.getByTitle('Settings').click(); await page.getByRole('tab', { name: 'Appearance' }).click(); }
|
||||
await page.getByLabel('Find a color scheme').fill('');
|
||||
await page.getByRole('radio', { name: 'Dark', exact: true }).click();
|
||||
assert.equal(await page.evaluate(() => document.documentElement.dataset.theme), undefined);
|
||||
// Hints are off by default: the title under the pointer is set aside while hovered, and back afterwards
|
||||
const hintTarget = page.locator('.rail').getByRole('button', { name: 'Add a server' });
|
||||
@@ -1109,6 +1130,36 @@ try {
|
||||
await page.keyboard.press('Escape');
|
||||
console.log('ok: registered on the server');
|
||||
|
||||
// Giving the microphone back while muted (for Bluetooth headsets), and taking it again
|
||||
{
|
||||
const muteBtn = (name: 'Mute' | 'Unmute') => page.locator('.me').getByRole('button', { name, exact: true });
|
||||
if (!userState(newName)!.selfMute) {
|
||||
await muteBtn('Mute').click();
|
||||
await until2('muted', () => !!userState(newName)!.selfMute);
|
||||
}
|
||||
await page.getByTitle('Settings').click();
|
||||
assert.equal(await page.getByLabel('Give the microphone back while muted').isChecked(), false, 'off by default on a desktop');
|
||||
await page.getByLabel('Give the microphone back while muted').check();
|
||||
await page.getByText('The microphone is given back now.').waitFor();
|
||||
await page.getByRole('button', { name: 'Done', exact: true }).click();
|
||||
// Unmuting takes it again
|
||||
const heard = new Promise<void>(res => { const off = bob.on('voice', raw => { if (decodeVoice(raw)?.session === userState(newName)!.session) { off(); res(); } }); });
|
||||
await muteBtn('Unmute').click();
|
||||
await until2('unmuted', () => !userState(newName)!.selfMute);
|
||||
await page.getByTitle('Settings').click();
|
||||
await page.getByText('The microphone is open now.').waitFor();
|
||||
await page.getByLabel('Give the microphone back while muted').uncheck();
|
||||
await page.getByRole('button', { name: 'Done', exact: true }).click();
|
||||
// Push to talk is the mode at this point: hold the key until bob hears the fake microphone
|
||||
await page.locator('.chat header h1').click();
|
||||
await page.keyboard.down('Backquote');
|
||||
await within(heard, 'voice after taking the microphone again', 15000);
|
||||
await page.keyboard.up('Backquote');
|
||||
await muteBtn('Mute').click();
|
||||
await until2('muted again', () => !!userState(newName)!.selfMute);
|
||||
console.log('ok: microphone given back while muted and taken again on unmute');
|
||||
}
|
||||
|
||||
// ─── The Rooms interface: the same app arranged for phones ─────────────────
|
||||
{
|
||||
const before = page.viewportSize()!;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Every color scheme must stay readable: text on the surfaces it sits on, and the label on an accent button
|
||||
const css = ['themes.css', 'themes-generated.css'].map(f => readFileSync(path.resolve(import.meta.dirname, '../src', f), 'utf8')).join('\n');
|
||||
const settings = ['settings.svelte.ts', 'themes-generated.ts'].map(f => readFileSync(path.resolve(import.meta.dirname, '../src/lib', f), 'utf8')).join('\n');
|
||||
|
||||
function luminance(hex: string): number {
|
||||
const n = parseInt(hex.slice(1), 16);
|
||||
const [r, g, b] = [n >> 16, (n >> 8) & 255, n & 255].map(v => {
|
||||
const c = v / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
const contrast = (a: string, b: string) => {
|
||||
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
};
|
||||
|
||||
const themes = [...css.matchAll(/\[data-theme-preview="([\w-]+)"\] \{([^}]*)\}/g)].map(m => ({
|
||||
id: m[1],
|
||||
vars: Object.fromEntries([...m[2].matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6});/gi)].map(v => [v[1], v[2]]))
|
||||
}));
|
||||
|
||||
test('every scheme in the settings list has its colors, and the other way round', () => {
|
||||
const listed = [...settings.matchAll(/\{ id: '([\w-]+)', name:/g)].map(m => m[1]).sort();
|
||||
assert.deepEqual(themes.map(t => t.id).sort(), listed);
|
||||
assert.equal(listed.length, 100);
|
||||
});
|
||||
|
||||
for (const t of themes) {
|
||||
test(`${t.id}: text is readable`, () => {
|
||||
const v = t.vars;
|
||||
for (const bg of ['--bg-0', '--bg-1', '--bg-2']) {
|
||||
assert.ok(contrast(v['--text'], v[bg]) >= 7, `text on ${bg}: ${contrast(v['--text'], v[bg]).toFixed(1)}`);
|
||||
assert.ok(contrast(v['--text-dim'], v[bg]) >= 4.5, `dim text on ${bg}: ${contrast(v['--text-dim'], v[bg]).toFixed(1)}`);
|
||||
}
|
||||
assert.ok(contrast(v['--accent-text'], v['--accent']) >= 3.5, `accent label: ${contrast(v['--accent-text'], v['--accent']).toFixed(1)}`);
|
||||
assert.ok(contrast(v['--link'], v['--bg-2']) >= 4, `link: ${contrast(v['--link'], v['--bg-2']).toFixed(1)}`);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user