Public: anyone can read this; the crew can change it.

The theme picker in the layout draws §5's field and button, and .theme-option, .theme-apply leave the markup #4738

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4700-the-theme-picker-in-the-layout-draws-5-s → main 1 commit fork

  • Merged. scout/4700-the-theme-picker-in-the-layout-draws-5-s was merged into main as db509f8.

Review

Sign in to review this change.
4 files changed +23 −15
src/web/client.ts +3 −3
@@ -1305,13 +1305,13 @@ export const shellJs = /* js */ `
13051305 if (!m || !m.matches || !m.matches('details.menu[open]')) return;
13061306 closeMenus(m);
13071307 var first = m.querySelector('.menu-panel .menu-item, .menu-panel input');
1308 if (first && !first.closest('.theme-option')) first.focus();
1308 if (first && !first.closest('.theme-form')) first.focus();
13091309 }, true);
13101310 doc.addEventListener('keydown', function (e) {
13111311 if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
13121312 var panel = e.target && e.target.closest ? e.target.closest('.menu-panel') : null;
13131313 if (!panel) return;
1314 var items = Array.prototype.slice.call(panel.querySelectorAll('.menu-item, .theme-option input'));
1314 var items = Array.prototype.slice.call(panel.querySelectorAll('.menu-item, .theme-form input'));
13151315 var at = items.indexOf(doc.activeElement);
13161316 var next = items[(at + (e.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
13171317 if (next) { next.focus(); e.preventDefault(); }
@@ -1320,7 +1320,7 @@ export const shellJs = /* js */ `
13201320 // ---- theme: apply instantly, persist once the choice settles ----
13211321 var themeForm = doc.querySelector('.theme-form');
13221322 if (themeForm) {
1323 var apply = themeForm.querySelector('.theme-apply');
1323 var apply = themeForm.querySelector('button[type=submit]');
13241324 if (apply) apply.hidden = true;
13251325 var pending = 0;
13261326 themeForm.addEventListener('change', function () {
src/web/layout.ts +9 −10
@@ -121,7 +121,7 @@ export interface PageOptions {
121121 /** The theme choice as a panel section: lives in a menu, never in the bar. */
122122 function themeForm(active: ThemeName | undefined): Html {
123123 const option = (value: string, label: string, checked: boolean) =>
124 html`<label class="theme-option">
124 html`<label class="check">
125125 <input
126126 type="radio"
127127 name="theme"
@@ -130,17 +130,16 @@ function themeForm(active: ThemeName | undefined): Html {
130130 />
131131 <span class="swatch" data-t="${value}"></span>
132132 ${label}
133 <span class="tick" aria-hidden="true">${icon("check")}</span>
134133 </label>`;
135134 return html`<form class="theme-form" method="post" action="/theme">
136 <span class="menu-title">Theme</span>
137 ${option("system", "System", active === undefined)}
138 ${THEME_NAMES.map((name) =>
139 option(name, THEMES[name].label, active === name),
140 )}
141 <button class="btn theme-apply" data-size="sm" type="submit">
142 Apply
143 </button>
135 <fieldset class="field">
136 <legend>Theme</legend>
137 ${option("system", "System", active === undefined)}
138 ${THEME_NAMES.map((name) =>
139 option(name, THEMES[name].label, active === name),
140 )}
141 </fieldset>
142 ${btn("Apply", { size: "sm" })}
144143 </form>`;
145144 }
146145
test/flow-pipeline.test.ts +2 −2
@@ -290,10 +290,10 @@ describe("/flow: the forge shows its own bottleneck", () => {
290290 expect(panel).toContain('action="/theme"');
291291 });
292292
293 test("the shell script wires the theme form by its new class and guards focus on theme options", () => {
293 test("the shell script wires the theme form by its new class and guards focus on the theme form", () => {
294294 expect(shellJs).toContain("doc.querySelector('.theme-form')");
295295 expect(shellJs).not.toContain(".theme-menu form");
296 expect(shellJs).toContain("first.closest('.theme-option')");
296 expect(shellJs).toContain("first.closest('.theme-form')");
297297 });
298298
299299 const flowCommand = (user?: User) => {
test/web.test.ts +9
@@ -655,6 +655,15 @@ describe("themes", () => {
655655 for (const name of THEME_NAMES) {
656656 expect(text).toContain(`value="${name}"`);
657657 }
658 // §5: each choice is a labelled radio, the button a plain btn; no bespoke classes.
659 const form = text.slice(text.indexOf('action="/theme"'));
660 const end = form.indexOf("</form>");
661 const panel = form.slice(0, end);
662 expect(panel.match(/<label class="check">/g)?.length).toBe(THEME_NAMES.length + 1);
663 expect(panel.match(/type="radio"/g)?.length).toBe(THEME_NAMES.length + 1);
664 expect(panel).toMatch(/<button\s+type="submit"[\s\S]*class="btn"[\s\S]*Apply/);
665 expect(text).not.toContain("theme-option");
666 expect(text).not.toContain("theme-apply");
658667 // Critical paint before the stylesheet: system pair (light default + dark
659668 // media), then a rule per theme so a toggle repaints without a reload.
660669 const boot = text.match(/<style nonce="[^"]+">([^<]*)<\/style>/)?.[1];

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs