openvibers/openvibe.games Commits
Home: every board shows on a phone; the chips are links, not a picker (#251)
Changes
@@@ -397,52 +397,41 @@box-shadow: 0 8px 48px rgba(var(--accent-rgb, 59, 130, 246), 0.7);}}-- /* ── the home on a phone (the owner's short-pages rule, 2026-10-30) ──-- Pick: on a phone the home's blocks of the same kind are one at a time, picked with a row of chips.-- Radio inputs and :has(), no script: the chips and the one-block rule live under 640 px and only where-- :has() is understood, so a wider screen, or an older browser, shows every block and no chips. */++ /* ── the home on a phone ──++ Pick: every board is shown, stacked; on a phone the chips are a row of links that jump to one (the owner's++ rule, 2026-10-31: never hide content to shorten). The section has its own heading, so the pick's stays off. */.pick-head { display: none; }.pick-row { display: none; }.pick-pane { min-width: 0; }-- @supports selector(:has(*)) {-- @media (max-width: 640px) {-- .pick-head { display: block; }-- .pick-head h2 { margin: 0 0 4px; font-size: 1.25rem; }-- .pick-row {-- display: flex;-- gap: 8px;-- overflow-x: auto;-- margin: 10px -20px 0;-- padding: 2px 20px 4px;-- scrollbar-width: none;-- overscroll-behavior-x: contain;-- }-- .pick-row::-webkit-scrollbar { display: none; }-- .pick-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }-- .pick-row label {-- display: inline-flex;-- align-items: center;-- flex: none;-- min-height: 44px;-- padding: 0 15px;-- border-radius: 999px;-- border: 1px solid var(--border);-- color: var(--text-dim);-- font-size: 0.9rem;-- font-weight: 650;-- white-space: nowrap;-- cursor: pointer;-- }-- .pick-row input:checked + label {-- color: var(--text);-- border-color: var(--accent-light);-- background: rgba(var(--accent-rgb, 59, 130, 246), 0.12);-- }-- .pick-row input:focus-visible + label { outline: 2px solid var(--accent-light); outline-offset: 2px; }-- .pick-pane { display: none; }-- /* The chip names the block, so the block's own heading gives way to it. */-- .home-pick-sec > h2 { display: none; }++ @media (max-width: 640px) {++ .pick-row {++ display: flex;++ gap: 8px;++ overflow-x: auto;++ margin: 10px -20px 0;++ padding: 2px 20px 4px;++ scrollbar-width: none;++ overscroll-behavior-x: contain;}++ .pick-row::-webkit-scrollbar { display: none; }++ .pick-chip {++ display: inline-flex;++ align-items: center;++ flex: none;++ min-height: 44px;++ padding: 0 15px;++ border-radius: 999px;++ border: 1px solid var(--border);++ color: var(--text-dim);++ font-size: 0.9rem;++ font-weight: 650;++ white-space: nowrap;++ text-decoration: none;++ }++ .pick-chip:hover, .pick-chip:focus-visible { color: var(--text); border-color: var(--accent-light); }++ .pick-chip:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; }++ .pick .pick-pane { scroll-margin-top: 72px; }++ .pick .pick-pane + .pick-pane { margin-top: 18px; }}/* The fold: closed on a phone, and where there is room it looks like the section it replaces. */.gm-fold-block { margin: 0 auto 40px; max-width: 1100px; padding: 0 20px; }@@@ -4,7 +4,7 @@ import { SALVAGE_RUSH_PACK } from '@openvibe/content/salvage-rush'import type { CreatorWorld } from '@openvibe/persistence'import type { Logger } from '@openvibe/shared'import { escapeHtml } from './notFound.js'--import { foldBlock, pickBlock, pickRules, pickStyle } from './shortHome.js'++import { foldBlock, pickBlock } from './shortHome.js'import type { SteamGame } from '../steam/steamCatalog.js'/**@@@ -266,15 +266,10 @@ function boardsSection(d: HomeData): string {}),].filter((c): c is { label: string; html: string } => c !== null)if (!cards.length) return ''-- // The boards are blocks of the same kind: on a phone a row of chips picks one of them and the chip names it, so-- // one board is read at a time. Every board stays in the page.++ // The boards are blocks of the same kind: every board shown, and on a phone a row of chips jumps to one (the owner,++ // 2026-10-31: never hide content to shorten).const panes = cards.map((c, i) => ({ id: `board-${i}`, label: c.label, html: c.html }))-- const rules = pickRules(-- 'boards',-- panes.map((p) => p.id),-- )-- return `${pickStyle([rules])}--<section class="home-sec home-pick-sec" aria-labelledby="home-boards">++ return `<section class="home-sec home-pick-sec" aria-labelledby="home-boards"><h2 id="home-boards">Top players</h2><p class="home-lede">The servers score every round, so these boards are what really happened. Signed-in players get their name on them.</p>${pickBlock({ key: 'boards', label: 'The boards', head: 'Top players', panes })}@@@ -2,40 +2,31 @@ import { describe, expect, it } from 'vitest'import { readFileSync } from 'node:fs'import { join } from 'node:path'import { fileURLToPath } from 'node:url'--import { foldBlock, pickBlock, pickRules, pickStyle } from './shortHome.js'++import { foldBlock, pickBlock } from './shortHome.js'const root = fileURLToPath(new URL('../../../..', import.meta.url))/**-- * The home on a phone (the owner's short-pages rule, 2026-10-30): the blocks of the same kind are one pick block,-- * a row of chips picks one of them, and the parts a reader rarely needs are folds. Radio inputs and :has(), no script-- * for the chips; every block stays in the page for crawlers; a wider screen shows them all and no chips.++ * The home on a phone: the blocks of the same kind are one pick block, every block shown, and a row of chips jumps to++ * one (the owner, 2026-10-31: never hide content to shorten); the parts a reader rarely needs are folds. A wider++ * screen shows them all and no chips.*/describe('the home on a phone', () => {-- it('the boards are one pick block: a chip per board, the first one picked', () => {++ it('the boards are one pick block: every board shown, a chip per board that jumps to it', () => {const panes = [{ id: 'board-0', label: 'Scraplandia', html: '<article class="board-card">a</article>' },{ id: 'board-1', label: 'Salvage Rush', html: '<article class="board-card">b</article>' },]const html = pickBlock({ key: 'boards', label: 'The boards', head: 'Top players', panes })expect(html).toContain('<div class="pick" id="pick-boards">')-- expect(html).toContain('<div class="pick-row" role="radiogroup" aria-label="The boards">')-- const chips = [-- ...html.matchAll(-- /<input type="radio" name="pick-boards" id="pk-boards-([a-z0-9-]+)" value="\1"( checked)?><label for="pk-boards-\1">([^<]+)<\/label>/g,-- ),-- ]-- expect(chips.map((m) => m[1])).toEqual(['board-0', 'board-1'])-- expect(chips.filter((m) => m[2]).map((m) => m[1])).toEqual(['board-0'])++ expect(html).toContain('<nav class="pick-row" aria-label="The boards">')++ const chips = [...html.matchAll(/<a class="pick-chip" href="#(pp-boards-[a-z0-9-]+)">([^<]+)<\/a>/g)]++ expect(chips.map((m) => m[1])).toEqual(['pp-boards-board-0', 'pp-boards-board-1'])++ for (const [, id] of chips) expect(html).toContain(`<div class="pick-pane" id="${id}">`)++ expect(html).not.toMatch(/type="radio"/)expect((html.match(/class="pick-pane"/g) ?? []).length).toBe(2)-- // Every board stays in the page for the crawlers.expect(html).toContain('board-card">a')expect(html).toContain('board-card">b')-- // The rule that shows the picked board is written for this page.-- expect(pickRules('boards', ['board-0', 'board-1'])).toBe(-- '.pick:has(#pk-boards-board-0:checked) #pp-boards-board-0{display:block}.pick:has(#pk-boards-board-1:checked) #pp-boards-board-1{display:block}',-- )-- expect(pickStyle([pickRules('boards', ['board-0'])])).toContain('@media (max-width:640px)')})it('a fold is open in the page and closed on a phone by its own script', () => {@@@ -53,12 +44,11 @@ describe('the home on a phone', () => {expect(html).toContain("matchMedia('(max-width:640px)')")})-- it('the CSS guard: the chips are hidden and the blocks all show until 640 px and where :has() is understood', () => {++ it('the CSS: no chips where there is room, chip links on a phone, and no board is ever hidden', () => {const css = readFileSync(join(root, 'apps/client/index.html'), 'utf8')expect(css).toMatch(/\.pick-row \{ display: none; \}/)-- expect(css).toMatch(/@supports selector\(:has\(\*\)\) \{/)expect(css).toMatch(/@media \(max-width: 640px\) \{/)-- expect(css).toMatch(/\.pick-row label \{[\s\S]*?min-height: 44px/)-- expect(css).toMatch(/\.pick-pane \{ display: none; \}/)++ expect(css).toMatch(/\.pick-chip \{[\s\S]*?min-height: 44px/)++ expect(css).not.toMatch(/\.pick-pane \{ display: none; \}/)})})@@@ -1,15 +1,13 @@/*** The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.*-- * pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio-- * inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a-- * browser without :has()) shows them all and no chips.++ * pickBlock() several blocks of the same kind, all shown, each with its own heading; on a phone the chips are a++ * row of links that jump to them (the owner, 2026-10-31: never hide content to shorten).* foldBlock() a part a reader rarely needs, closed on a phone and open where there is room. The page's own markup* keeps it open, so a browser without the script shows it as before; the script closes it on a narrow* screen and remembers the reader's choice.*-- * The CSS for both lives in apps/client/index.html; the rule that shows the picked block is written for the page,-- * because there is one per block.++ * The CSS for both lives in apps/client/index.html.*/import { escapeHtml } from './notFound.js'@@@ -22,8 +20,8 @@ export interface PickPane {}/**-- * A pick block. The panes are the page's own blocks; the block's heading gives way to the chip that names it on a-- * phone, and the chips are gone on a wide screen, where every pane shows at once.++ * A pick block. The panes are the page's own blocks, every one shown; on a phone the chips jump to them, and on a wide++ * screen there are no chips.*/export function pickBlock(o: {key: string@@@ -34,30 +32,15 @@ export function pickBlock(o: {const panes = o.panes ?? []if (!panes.length) return ''const chips = panes-- .map(-- (p, i) =>-- `<input type="radio" name="pick-${esc(o.key)}" id="pk-${esc(o.key)}-${esc(p.id)}" value="${esc(p.id)}"${i === 0 ? ' checked' : ''}><label for="pk-${esc(o.key)}-${esc(p.id)}">${esc(p.label)}</label>`,-- )++ .map((p) => `<a class="pick-chip" href="#pp-${esc(o.key)}-${esc(p.id)}">${esc(p.label)}</a>`).join('')const body = panes.map((p) => `<div class="pick-pane" id="pp-${esc(o.key)}-${esc(p.id)}">${p.html}</div>`).join('')const heading = `<h2>${esc(o.head || o.label)}</h2>`-- return `<div class="pick" id="pick-${esc(o.key)}"><div class="pick-bar"><div class="pick-head">${heading}</div><div class="pick-row" role="radiogroup" aria-label="${esc(o.label)}">${chips}</div></div><div class="pick-body">${body}</div></div>`++ return `<div class="pick" id="pick-${esc(o.key)}"><div class="pick-bar"><div class="pick-head">${heading}</div><nav class="pick-row" aria-label="${esc(o.label)}">${chips}</nav></div><div class="pick-body">${body}</div></div>`}--/** The rules for every pane of a pick block, in the order they appear: one rule per pane, so the CSS is written for the page. */--export function pickRules(key: string, ids: string[]): string {-- return ids-- .map((id) => `.pick:has(#pk-${key}-${id}:checked) #pp-${key}-${id}{display:block}`)-- .join('')--}----/** The rule that shows the picked block, as a style tag the page carries. */--export function pickStyle(rules: string[]): string {-- if (!rules.length) return ''-- return `<style>@supports selector(:has(*)){@media (max-width:640px){${rules.join('')}}}</style>`--}/*** A fold. The head is the heading the block had; the html is what it holds.
@@ -397,52 +397,41 @@
@@ -397,52 +397,41 @@
box-shadow: 0 8px 48px rgba(var(--accent-rgb, 59, 130, 246), 0.7);
box-shadow: 0 8px 48px rgba(var(--accent-rgb, 59, 130, 246), 0.7);
}
}
}
}
- /* ── the home on a phone (the owner's short-pages rule, 2026-10-30) ──
- Pick: on a phone the home's blocks of the same kind are one at a time, picked with a row of chips.
- Radio inputs and :has(), no script: the chips and the one-block rule live under 640 px and only where
- :has() is understood, so a wider screen, or an older browser, shows every block and no chips. */
+ /* ── the home on a phone ──
+ Pick: every board is shown, stacked; on a phone the chips are a row of links that jump to one (the owner's
+ rule, 2026-10-31: never hide content to shorten). The section has its own heading, so the pick's stays off. */
.pick-head { display: none; }
.pick-head { display: none; }
.pick-row { display: none; }
.pick-row { display: none; }
.pick-pane { min-width: 0; }
.pick-pane { min-width: 0; }
- @supports selector(:has(*)) {
- @media (max-width: 640px) {
- .pick-head { display: block; }
- .pick-head h2 { margin: 0 0 4px; font-size: 1.25rem; }
- .pick-row {
- display: flex;
- gap: 8px;
- overflow-x: auto;
- margin: 10px -20px 0;
- padding: 2px 20px 4px;
- scrollbar-width: none;
- overscroll-behavior-x: contain;
- }
- .pick-row::-webkit-scrollbar { display: none; }
- .pick-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
- .pick-row label {
- display: inline-flex;
- align-items: center;
- flex: none;
- min-height: 44px;
- padding: 0 15px;
- border-radius: 999px;
- border: 1px solid var(--border);
- color: var(--text-dim);
- font-size: 0.9rem;
- font-weight: 650;
- white-space: nowrap;
- cursor: pointer;
- }
- .pick-row input:checked + label {
- color: var(--text);
- border-color: var(--accent-light);
- background: rgba(var(--accent-rgb, 59, 130, 246), 0.12);
- }
- .pick-row input:focus-visible + label { outline: 2px solid var(--accent-light); outline-offset: 2px; }
- .pick-pane { display: none; }
- /* The chip names the block, so the block's own heading gives way to it. */
- .home-pick-sec > h2 { display: none; }
+ @media (max-width: 640px) {
+ .pick-row {
+ display: flex;
+ gap: 8px;
+ overflow-x: auto;
+ margin: 10px -20px 0;
+ padding: 2px 20px 4px;
+ scrollbar-width: none;
+ overscroll-behavior-x: contain;
}
}
+ .pick-row::-webkit-scrollbar { display: none; }
+ .pick-chip {
+ display: inline-flex;
+ align-items: center;
+ flex: none;
+ min-height: 44px;
+ padding: 0 15px;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ color: var(--text-dim);
+ font-size: 0.9rem;
+ font-weight: 650;
+ white-space: nowrap;
+ text-decoration: none;
+ }
+ .pick-chip:hover, .pick-chip:focus-visible { color: var(--text); border-color: var(--accent-light); }
+ .pick-chip:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; }
+ .pick .pick-pane { scroll-margin-top: 72px; }
+ .pick .pick-pane + .pick-pane { margin-top: 18px; }
}
}
/* The fold: closed on a phone, and where there is room it looks like the section it replaces. */
/* The fold: closed on a phone, and where there is room it looks like the section it replaces. */
.gm-fold-block { margin: 0 auto 40px; max-width: 1100px; padding: 0 20px; }
.gm-fold-block { margin: 0 auto 40px; max-width: 1100px; padding: 0 20px; }
@@ -4,7 +4,7 @@ import { SALVAGE_RUSH_PACK } from '@openvibe/content/salvage-rush'
@@ -4,7 +4,7 @@ import { SALVAGE_RUSH_PACK } from '@openvibe/content/salvage-rush'
import type { CreatorWorld } from '@openvibe/persistence'
import type { CreatorWorld } from '@openvibe/persistence'
import type { Logger } from '@openvibe/shared'
import type { Logger } from '@openvibe/shared'
import { escapeHtml } from './notFound.js'
import { escapeHtml } from './notFound.js'
-import { foldBlock, pickBlock, pickRules, pickStyle } from './shortHome.js'
+import { foldBlock, pickBlock } from './shortHome.js'
import type { SteamGame } from '../steam/steamCatalog.js'
import type { SteamGame } from '../steam/steamCatalog.js'
/**
/**
@@ -266,15 +266,10 @@ function boardsSection(d: HomeData): string {
@@ -266,15 +266,10 @@ function boardsSection(d: HomeData): string {
}),
}),
].filter((c): c is { label: string; html: string } => c !== null)
].filter((c): c is { label: string; html: string } => c !== null)
if (!cards.length) return ''
if (!cards.length) return ''
- // The boards are blocks of the same kind: on a phone a row of chips picks one of them and the chip names it, so
- // one board is read at a time. Every board stays in the page.
+ // The boards are blocks of the same kind: every board shown, and on a phone a row of chips jumps to one (the owner,
+ // 2026-10-31: never hide content to shorten).
const panes = cards.map((c, i) => ({ id: `board-${i}`, label: c.label, html: c.html }))
const panes = cards.map((c, i) => ({ id: `board-${i}`, label: c.label, html: c.html }))
- const rules = pickRules(
- 'boards',
- panes.map((p) => p.id),
- )
- return `${pickStyle([rules])}
-<section class="home-sec home-pick-sec" aria-labelledby="home-boards">
+ return `<section class="home-sec home-pick-sec" aria-labelledby="home-boards">
<h2 id="home-boards">Top players</h2>
<h2 id="home-boards">Top players</h2>
<p class="home-lede">The servers score every round, so these boards are what really happened. Signed-in players get their name on them.</p>
<p class="home-lede">The servers score every round, so these boards are what really happened. Signed-in players get their name on them.</p>
${pickBlock({ key: 'boards', label: 'The boards', head: 'Top players', panes })}
${pickBlock({ key: 'boards', label: 'The boards', head: 'Top players', panes })}
@@ -2,40 +2,31 @@ import { describe, expect, it } from 'vitest'
@@ -2,40 +2,31 @@ import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { fileURLToPath } from 'node:url'
-import { foldBlock, pickBlock, pickRules, pickStyle } from './shortHome.js'
+import { foldBlock, pickBlock } from './shortHome.js'
const root = fileURLToPath(new URL('../../../..', import.meta.url))
const root = fileURLToPath(new URL('../../../..', import.meta.url))
/**
/**
- * The home on a phone (the owner's short-pages rule, 2026-10-30): the blocks of the same kind are one pick block,
- * a row of chips picks one of them, and the parts a reader rarely needs are folds. Radio inputs and :has(), no script
- * for the chips; every block stays in the page for crawlers; a wider screen shows them all and no chips.
+ * The home on a phone: the blocks of the same kind are one pick block, every block shown, and a row of chips jumps to
+ * one (the owner, 2026-10-31: never hide content to shorten); the parts a reader rarely needs are folds. A wider
+ * screen shows them all and no chips.
*/
*/
describe('the home on a phone', () => {
describe('the home on a phone', () => {
- it('the boards are one pick block: a chip per board, the first one picked', () => {
+ it('the boards are one pick block: every board shown, a chip per board that jumps to it', () => {
const panes = [
const panes = [
{ id: 'board-0', label: 'Scraplandia', html: '<article class="board-card">a</article>' },
{ id: 'board-0', label: 'Scraplandia', html: '<article class="board-card">a</article>' },
{ id: 'board-1', label: 'Salvage Rush', html: '<article class="board-card">b</article>' },
{ id: 'board-1', label: 'Salvage Rush', html: '<article class="board-card">b</article>' },
]
]
const html = pickBlock({ key: 'boards', label: 'The boards', head: 'Top players', panes })
const html = pickBlock({ key: 'boards', label: 'The boards', head: 'Top players', panes })
expect(html).toContain('<div class="pick" id="pick-boards">')
expect(html).toContain('<div class="pick" id="pick-boards">')
- expect(html).toContain('<div class="pick-row" role="radiogroup" aria-label="The boards">')
- const chips = [
- ...html.matchAll(
- /<input type="radio" name="pick-boards" id="pk-boards-([a-z0-9-]+)" value="\1"( checked)?><label for="pk-boards-\1">([^<]+)<\/label>/g,
- ),
- ]
- expect(chips.map((m) => m[1])).toEqual(['board-0', 'board-1'])
- expect(chips.filter((m) => m[2]).map((m) => m[1])).toEqual(['board-0'])
+ expect(html).toContain('<nav class="pick-row" aria-label="The boards">')
+ const chips = [...html.matchAll(/<a class="pick-chip" href="#(pp-boards-[a-z0-9-]+)">([^<]+)<\/a>/g)]
+ expect(chips.map((m) => m[1])).toEqual(['pp-boards-board-0', 'pp-boards-board-1'])
+ for (const [, id] of chips) expect(html).toContain(`<div class="pick-pane" id="${id}">`)
+ expect(html).not.toMatch(/type="radio"/)
expect((html.match(/class="pick-pane"/g) ?? []).length).toBe(2)
expect((html.match(/class="pick-pane"/g) ?? []).length).toBe(2)
- // Every board stays in the page for the crawlers.
expect(html).toContain('board-card">a')
expect(html).toContain('board-card">a')
expect(html).toContain('board-card">b')
expect(html).toContain('board-card">b')
- // The rule that shows the picked board is written for this page.
- expect(pickRules('boards', ['board-0', 'board-1'])).toBe(
- '.pick:has(#pk-boards-board-0:checked) #pp-boards-board-0{display:block}.pick:has(#pk-boards-board-1:checked) #pp-boards-board-1{display:block}',
- )
- expect(pickStyle([pickRules('boards', ['board-0'])])).toContain('@media (max-width:640px)')
})
})
it('a fold is open in the page and closed on a phone by its own script', () => {
it('a fold is open in the page and closed on a phone by its own script', () => {
@@ -53,12 +44,11 @@ describe('the home on a phone', () => {
@@ -53,12 +44,11 @@ describe('the home on a phone', () => {
expect(html).toContain("matchMedia('(max-width:640px)')")
expect(html).toContain("matchMedia('(max-width:640px)')")
})
})
- it('the CSS guard: the chips are hidden and the blocks all show until 640 px and where :has() is understood', () => {
+ it('the CSS: no chips where there is room, chip links on a phone, and no board is ever hidden', () => {
const css = readFileSync(join(root, 'apps/client/index.html'), 'utf8')
const css = readFileSync(join(root, 'apps/client/index.html'), 'utf8')
expect(css).toMatch(/\.pick-row \{ display: none; \}/)
expect(css).toMatch(/\.pick-row \{ display: none; \}/)
- expect(css).toMatch(/@supports selector\(:has\(\*\)\) \{/)
expect(css).toMatch(/@media \(max-width: 640px\) \{/)
expect(css).toMatch(/@media \(max-width: 640px\) \{/)
- expect(css).toMatch(/\.pick-row label \{[\s\S]*?min-height: 44px/)
- expect(css).toMatch(/\.pick-pane \{ display: none; \}/)
+ expect(css).toMatch(/\.pick-chip \{[\s\S]*?min-height: 44px/)
+ expect(css).not.toMatch(/\.pick-pane \{ display: none; \}/)
})
})
})
})
@@ -1,15 +1,13 @@
@@ -1,15 +1,13 @@
/**
/**
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
*
*
- * pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio
- * inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a
- * browser without :has()) shows them all and no chips.
+ * pickBlock() several blocks of the same kind, all shown, each with its own heading; on a phone the chips are a
+ * row of links that jump to them (the owner, 2026-10-31: never hide content to shorten).
* foldBlock() a part a reader rarely needs, closed on a phone and open where there is room. The page's own markup
* foldBlock() a part a reader rarely needs, closed on a phone and open where there is room. The page's own markup
* keeps it open, so a browser without the script shows it as before; the script closes it on a narrow
* keeps it open, so a browser without the script shows it as before; the script closes it on a narrow
* screen and remembers the reader's choice.
* screen and remembers the reader's choice.
*
*
- * The CSS for both lives in apps/client/index.html; the rule that shows the picked block is written for the page,
- * because there is one per block.
+ * The CSS for both lives in apps/client/index.html.
*/
*/
import { escapeHtml } from './notFound.js'
import { escapeHtml } from './notFound.js'
@@ -22,8 +20,8 @@ export interface PickPane {
@@ -22,8 +20,8 @@ export interface PickPane {
}
}
/**
/**
- * A pick block. The panes are the page's own blocks; the block's heading gives way to the chip that names it on a
- * phone, and the chips are gone on a wide screen, where every pane shows at once.
+ * A pick block. The panes are the page's own blocks, every one shown; on a phone the chips jump to them, and on a wide
+ * screen there are no chips.
*/
*/
export function pickBlock(o: {
export function pickBlock(o: {
key: string
key: string
@@ -34,30 +32,15 @@ export function pickBlock(o: {
@@ -34,30 +32,15 @@ export function pickBlock(o: {
const panes = o.panes ?? []
const panes = o.panes ?? []
if (!panes.length) return ''
if (!panes.length) return ''
const chips = panes
const chips = panes
- .map(
- (p, i) =>
- `<input type="radio" name="pick-${esc(o.key)}" id="pk-${esc(o.key)}-${esc(p.id)}" value="${esc(p.id)}"${i === 0 ? ' checked' : ''}><label for="pk-${esc(o.key)}-${esc(p.id)}">${esc(p.label)}</label>`,
- )
+ .map((p) => `<a class="pick-chip" href="#pp-${esc(o.key)}-${esc(p.id)}">${esc(p.label)}</a>`)
.join('')
.join('')
const body = panes
const body = panes
.map((p) => `<div class="pick-pane" id="pp-${esc(o.key)}-${esc(p.id)}">${p.html}</div>`)
.map((p) => `<div class="pick-pane" id="pp-${esc(o.key)}-${esc(p.id)}">${p.html}</div>`)
.join('')
.join('')
const heading = `<h2>${esc(o.head || o.label)}</h2>`
const heading = `<h2>${esc(o.head || o.label)}</h2>`
- return `<div class="pick" id="pick-${esc(o.key)}"><div class="pick-bar"><div class="pick-head">${heading}</div><div class="pick-row" role="radiogroup" aria-label="${esc(o.label)}">${chips}</div></div><div class="pick-body">${body}</div></div>`
+ return `<div class="pick" id="pick-${esc(o.key)}"><div class="pick-bar"><div class="pick-head">${heading}</div><nav class="pick-row" aria-label="${esc(o.label)}">${chips}</nav></div><div class="pick-body">${body}</div></div>`
}
}
-/** The rules for every pane of a pick block, in the order they appear: one rule per pane, so the CSS is written for the page. */
-export function pickRules(key: string, ids: string[]): string {
- return ids
- .map((id) => `.pick:has(#pk-${key}-${id}:checked) #pp-${key}-${id}{display:block}`)
- .join('')
-}
-
-/** The rule that shows the picked block, as a style tag the page carries. */
-export function pickStyle(rules: string[]): string {
- if (!rules.length) return ''
- return `<style>@supports selector(:has(*)){@media (max-width:640px){${rules.join('')}}}</style>`
-}
/**
/**
* A fold. The head is the heading the block had; the html is what it holds.
* A fold. The head is the heading the block had; the html is what it holds.