openvibers/openvibe.coupons Commits
Home: every pane shows on a phone; the chips are links, not a picker (#189)
Changes
@@@ -218,27 +218,22 @@ main:has(.cpn-shop-title) .ovk-sub { white-space: normal; }}--/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page shows one block 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. */--.pick-head { display: none; }--.pick-row { display: none; }--@supports selector(:has(*)) { @media (max-width: 640px) {-- .pick-bar { margin-top: 0; }++/* Pick: on a phone every pane is shown, stacked, each with its own heading; the chips are a row of links that++ jump to a pane (the owner's rule, 2026-10-31: never hide content to shorten). A wide screen shows the panes as++ before, with no chips. */++.pick-head, .pick-row { display: none; }++@media (max-width: 640px) {++ .pick-bar { margin-top: 30px; }.pick-head { display: block; }-- .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px,2.4vw,28px)/1.2 var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif); letter-spacing: -.02em; }++ .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px, 2.4vw, 28px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); letter-spacing: -.02em; }.pick-row { display: flex; gap: 8px; overflow-x: auto; margin: 10px -16px 0; padding: 2px 16px 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, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; cursor: pointer; }-- .pick-row input:checked + label { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }-- .pick-row input:focus-visible + label { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }-- .pick .pick-pane { display: none; }-- .pick .pick-list > li { display: none; }-- /* The chip names the block, so the block's own heading gives way to it; the content stays in the page for the-- crawlers and for the reader on a wide screen. */-- .pick .sc-sec > h2, .pick section > h2, .pick > h2 { 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, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; text-decoration: none; }++ .pick-chip:hover, .pick-chip:focus-visible { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); }++ .pick-chip:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++ .pick .pick-pane { scroll-margin-top: 72px; }++ .pick .pick-pane + .pick-pane { margin-top: 26px; }++}/* The fold (the owner's short-pages rule): closed on a phone, and where there is room it looks like the section itreplaces: a heading and its content, with no card around it. */@@@ -31,7 +31,6 @@ const shop = require('../render/store');const offer = require('../render/offer');const home = require('../render/home');// The short home (the owner's rule, 2026-10-30): pick blocks and folds.--const { pickRules } = require('../render/short');const { csrfToken, checkCsrf } = require('../auth/forms');const { ApiError } = require('./errors');const confidence = require('../domain/confidence');@@@ -174,7 +173,6 @@ function createPublicRoutes(ctx) {// The service's own one-line summary: the shell turns it into the ai-summary meta and the WebPage JSON-LD.summary: pages.SITE_SUMMARY, styles: [showcase.STYLESHEET], bodyClass: 'home',// The rule that shows the picked block of the codes' pick block: one per block, written for this page.-- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickRules('codes', ['cp-working', 'cp-ending', 'cp-new'])}}}`],feeds: [{ type: 'rss', href: '/feed.xml', title: 'New codes (RSS)' }, { type: 'atom', href: '/atom.xml', title: 'New codes (Atom)' }, { type: 'json', href: '/feed.json', title: 'New codes (JSON Feed)' }],jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Coupons', url: `${config.baseUrl}/`, potentialAction: { '@type': 'SearchAction', target: `${config.baseUrl}/?q={q}`, 'query-input': 'required name=q' } },@@@ -2,16 +2,14 @@/*** 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.-- * pickList() a list of things of the same kind, one at a time on a phone, picked with a row of 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).++ * pickList() a list of things of the same kind under one heading, every item shown.* 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 this site's own stylesheet; the rule that shows the picked block is written for the-- * page, because there is one per block.++ * The CSS lives in this site's own stylesheet.*/const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');@@@ -25,43 +23,26 @@ function pickBlock({ key, label, head = '', panes }) {// A pane with nothing to show is left out, so the chips and the rules that show them match the page.panes = (panes || []).filter((p) => p.html);if (!panes.length) return '';-- const chips = panes.map((p, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + esc(p.id) + '" value="' + esc(p.id) + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + esc(p.id) + '">' + esc(p.label) + '</label>').join('');++ const chips = panes.map((p) => '<a class="pick-chip" href="#' + esc(p.id) + '">' + esc(p.label) + '</a>').join('');const body = panes.map((p) => '<div class="pick-pane" id="' + esc(p.id) + '">' + p.html + '</div>').join('');-- return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><div class="pick-row" role="radiogroup" aria-label="' + esc(label) + '">' + chips + '</div></div><div class="pick-body">' + body + '</div></div>';++ return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><nav class="pick-row" aria-label="' + esc(label) + '">' + chips + '</nav></div><div class="pick-body">' + body + '</div></div>';}--/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */--function pickRule(key, id) {-- return '.pick:has(#pk-' + key + '-' + id + ':checked) #' + id + '{display:block}';--}--/** The rules for every block of a pick block, in the order they appear. */--function pickRules(key, ids) {-- return ids.map((id) => pickRule(key, id)).join('');--}/**-- * A pick list: a list of things of the same kind, one at a time on a phone, picked with a row of chips. The list-- * keeps its own markup and every item stays in the page; the caller marks each item with the index its chip picks.++ * A pick list: a list of things of the same kind under one heading, every item shown (2026-10-31). The list keeps++ * its own markup; `labels` is kept for callers and no longer drawn.** pickList({ key, label, head, labels, html }) labels: the chip labels, in the order of the items*/function pickList({ key, label, head = '', labels, html }) {labels = labels || [];if (!labels.length) return '';-- const chips = labels.map((l, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + i + '" value="' + i + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + i + '">' + esc(l) + '</label>').join('');-- return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><div class="pick-row" role="radiogroup" aria-label="' + esc(label) + '">' + chips + '</div></div>' + html + '</div>';++ return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div></div>' + html + '</div>';}--/** The rule that shows the picked item of a pick list: one per item, so the CSS is written for the page. */--function pickListRule(key, i) {-- return '.pick#pick-' + key + ':has(#pk-' + key + '-' + i + ':checked) .pick-list>li[data-i="' + i + '"]{display:block}';--}--/** The rules for every item of a pick list. */--function pickListRules(key, n) {-- return Array.from({ length: n }, (_, i) => pickListRule(key, i)).join('');--}/*** A fold. The head is the heading the block had; the html is what it holds.@@@ -106,4 +87,4 @@ function foldAround(html, needle, id, head) {return html.slice(0, p.start) + foldBlock({ id, head, html: p.html }) + html.slice(p.end);}--module.exports = { pickBlock, pickRule, pickRules, pickList, pickListRule, pickListRules, foldBlock, foldScript, part, swap, foldAround };++module.exports = { pickBlock, pickList, foldBlock, foldScript, part, swap, foldAround };@@@ -24,10 +24,10 @@ const { boot, check, done } = require('./helpers/boot');const start = page.indexOf('<div class="pick" id="pick-codes">');assert.ok(start > 0, 'the pick block is on the home');const block = page.slice(start, page.indexOf('<section class="cp-sec" id="shops"'));-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="Codes">/);-- const chips = [...block.matchAll(/<input type="radio" name="pick-codes" id="pk-codes-([a-z-]+)" value="([a-z-]+)"( checked)?><label for="pk-codes-[a-z-]+">([^<]+)<\/label>/g)];++ assert.match(block, /<nav class="pick-row" aria-label="Codes">/);++ const chips = [...block.matchAll(/<a class="pick-chip" href="#(?:pp-codes-)?([a-z-]+)">()()([^<]+)<\/a>/g)];assert.ok(chips.length >= 1, `a chip per list (${chips.length})`);-- assert.deepStrictEqual(chips.filter((m) => m[3]).map((m) => m[1]), [chips[0][1]], 'the first list is picked');++ assert.ok(!/type="radio"/.test(block), 'no chip is a radio: nothing is picked, every pane shows');// The lists that have something to show stay in the page, each in its own pane.for (const id of chips.map((m) => m[1])) {assert.ok(block.includes(`id="${id}"`), `the pane of ${id} is in the page`);@@@ -38,12 +38,8 @@ const { boot, check, done } = require('./helpers/boot');await check('the CSS guard: the chips are hidden and the lists all show until 640 px and where :has() is understood', async () => {const r = await t.get('/');const page = r.text;-- assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-codes-cp-working:checked\) #cp-working\{display:block\}/, 'the rule that shows the picked list');const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'coupons.css'), 'utf8');assert.match(css, /\.pick-row \{ display: none; \}/, 'the chips are hidden by default');-- assert.match(css, /@supports selector\(:has\(\*\)\) \{ @media \(max-width: 640px\) \{/, 'the phone rules are under :has() and 640 px');-- assert.match(css, /\.pick-row label \{[^}]*min-height: 44px/, 'a chip is a 44 px target');-- assert.match(css, /\.pick \.pick-pane \{ display: none; \}/, 'the lists are hidden on a phone');});await check('the parts a reader rarely needs are folds, open in the page and closed on a phone', async () => {
@@ -218,27 +218,22 @@ main:has(.cpn-shop-title) .ovk-sub { white-space: normal; }
@@ -218,27 +218,22 @@ main:has(.cpn-shop-title) .ovk-sub { white-space: normal; }
}
}
-/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page shows one block 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. */
-.pick-head { display: none; }
-.pick-row { display: none; }
-@supports selector(:has(*)) { @media (max-width: 640px) {
- .pick-bar { margin-top: 0; }
+/* Pick: on a phone every pane is shown, stacked, each with its own heading; the chips are a row of links that
+ jump to a pane (the owner's rule, 2026-10-31: never hide content to shorten). A wide screen shows the panes as
+ before, with no chips. */
+.pick-head, .pick-row { display: none; }
+@media (max-width: 640px) {
+ .pick-bar { margin-top: 30px; }
.pick-head { display: block; }
.pick-head { display: block; }
- .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px,2.4vw,28px)/1.2 var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif); letter-spacing: -.02em; }
+ .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px, 2.4vw, 28px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); letter-spacing: -.02em; }
.pick-row { display: flex; gap: 8px; overflow-x: auto; margin: 10px -16px 0; padding: 2px 16px 4px; scrollbar-width: none; overscroll-behavior-x: contain; }
.pick-row { display: flex; gap: 8px; overflow-x: auto; margin: 10px -16px 0; padding: 2px 16px 4px; scrollbar-width: none; overscroll-behavior-x: contain; }
.pick-row::-webkit-scrollbar { display: none; }
.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, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; cursor: pointer; }
- .pick-row input:checked + label { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
- .pick-row input:focus-visible + label { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
- .pick .pick-pane { display: none; }
- .pick .pick-list > li { display: none; }
- /* The chip names the block, so the block's own heading gives way to it; the content stays in the page for the
- crawlers and for the reader on a wide screen. */
- .pick .sc-sec > h2, .pick section > h2, .pick > h2 { 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, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; text-decoration: none; }
+ .pick-chip:hover, .pick-chip:focus-visible { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); }
+ .pick-chip:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+ .pick .pick-pane { scroll-margin-top: 72px; }
+ .pick .pick-pane + .pick-pane { margin-top: 26px; }
+}
/* The fold (the owner's short-pages rule): closed on a phone, and where there is room it looks like the section it
/* The fold (the owner's short-pages rule): closed on a phone, and where there is room it looks like the section it
replaces: a heading and its content, with no card around it. */
replaces: a heading and its content, with no card around it. */
@@ -31,7 +31,6 @@ const shop = require('../render/store');
@@ -31,7 +31,6 @@ const shop = require('../render/store');
const offer = require('../render/offer');
const offer = require('../render/offer');
const home = require('../render/home');
const home = require('../render/home');
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
-const { pickRules } = require('../render/short');
const { csrfToken, checkCsrf } = require('../auth/forms');
const { csrfToken, checkCsrf } = require('../auth/forms');
const { ApiError } = require('./errors');
const { ApiError } = require('./errors');
const confidence = require('../domain/confidence');
const confidence = require('../domain/confidence');
@@ -174,7 +173,6 @@ function createPublicRoutes(ctx) {
@@ -174,7 +173,6 @@ function createPublicRoutes(ctx) {
// The service's own one-line summary: the shell turns it into the ai-summary meta and the WebPage JSON-LD.
// The service's own one-line summary: the shell turns it into the ai-summary meta and the WebPage JSON-LD.
summary: pages.SITE_SUMMARY, styles: [showcase.STYLESHEET], bodyClass: 'home',
summary: pages.SITE_SUMMARY, styles: [showcase.STYLESHEET], bodyClass: 'home',
// The rule that shows the picked block of the codes' pick block: one per block, written for this page.
// The rule that shows the picked block of the codes' pick block: one per block, written for this page.
- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickRules('codes', ['cp-working', 'cp-ending', 'cp-new'])}}}`],
feeds: [{ type: 'rss', href: '/feed.xml', title: 'New codes (RSS)' }, { type: 'atom', href: '/atom.xml', title: 'New codes (Atom)' }, { type: 'json', href: '/feed.json', title: 'New codes (JSON Feed)' }],
feeds: [{ type: 'rss', href: '/feed.xml', title: 'New codes (RSS)' }, { type: 'atom', href: '/atom.xml', title: 'New codes (Atom)' }, { type: 'json', href: '/feed.json', title: 'New codes (JSON Feed)' }],
jsonLd: [
jsonLd: [
{ '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Coupons', url: `${config.baseUrl}/`, potentialAction: { '@type': 'SearchAction', target: `${config.baseUrl}/?q={q}`, 'query-input': 'required name=q' } },
{ '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Coupons', url: `${config.baseUrl}/`, potentialAction: { '@type': 'SearchAction', target: `${config.baseUrl}/?q={q}`, 'query-input': 'required name=q' } },
@@ -2,16 +2,14 @@
@@ -2,16 +2,14 @@
/**
/**
* 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.
- * pickList() a list of things of the same kind, one at a time on a phone, picked with a row of 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).
+ * pickList() a list of things of the same kind under one heading, every item shown.
* 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 this site's own stylesheet; the rule that shows the picked block is written for the
- * page, because there is one per block.
+ * The CSS lives in this site's own stylesheet.
*/
*/
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
@@ -25,43 +23,26 @@ function pickBlock({ key, label, head = '', panes }) {
@@ -25,43 +23,26 @@ function pickBlock({ key, label, head = '', panes }) {
// A pane with nothing to show is left out, so the chips and the rules that show them match the page.
// A pane with nothing to show is left out, so the chips and the rules that show them match the page.
panes = (panes || []).filter((p) => p.html);
panes = (panes || []).filter((p) => p.html);
if (!panes.length) return '';
if (!panes.length) return '';
- const chips = panes.map((p, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + esc(p.id) + '" value="' + esc(p.id) + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + esc(p.id) + '">' + esc(p.label) + '</label>').join('');
+ const chips = panes.map((p) => '<a class="pick-chip" href="#' + esc(p.id) + '">' + esc(p.label) + '</a>').join('');
const body = panes.map((p) => '<div class="pick-pane" id="' + esc(p.id) + '">' + p.html + '</div>').join('');
const body = panes.map((p) => '<div class="pick-pane" id="' + esc(p.id) + '">' + p.html + '</div>').join('');
- return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><div class="pick-row" role="radiogroup" aria-label="' + esc(label) + '">' + chips + '</div></div><div class="pick-body">' + body + '</div></div>';
+ return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><nav class="pick-row" aria-label="' + esc(label) + '">' + chips + '</nav></div><div class="pick-body">' + body + '</div></div>';
}
}
-/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
-function pickRule(key, id) {
- return '.pick:has(#pk-' + key + '-' + id + ':checked) #' + id + '{display:block}';
-}
-/** The rules for every block of a pick block, in the order they appear. */
-function pickRules(key, ids) {
- return ids.map((id) => pickRule(key, id)).join('');
-}
/**
/**
- * A pick list: a list of things of the same kind, one at a time on a phone, picked with a row of chips. The list
- * keeps its own markup and every item stays in the page; the caller marks each item with the index its chip picks.
+ * A pick list: a list of things of the same kind under one heading, every item shown (2026-10-31). The list keeps
+ * its own markup; `labels` is kept for callers and no longer drawn.
*
*
* pickList({ key, label, head, labels, html }) labels: the chip labels, in the order of the items
* pickList({ key, label, head, labels, html }) labels: the chip labels, in the order of the items
*/
*/
function pickList({ key, label, head = '', labels, html }) {
function pickList({ key, label, head = '', labels, html }) {
labels = labels || [];
labels = labels || [];
if (!labels.length) return '';
if (!labels.length) return '';
- const chips = labels.map((l, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + i + '" value="' + i + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + i + '">' + esc(l) + '</label>').join('');
- return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><div class="pick-row" role="radiogroup" aria-label="' + esc(label) + '">' + chips + '</div></div>' + html + '</div>';
+ return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div></div>' + html + '</div>';
}
}
-/** The rule that shows the picked item of a pick list: one per item, so the CSS is written for the page. */
-function pickListRule(key, i) {
- return '.pick#pick-' + key + ':has(#pk-' + key + '-' + i + ':checked) .pick-list>li[data-i="' + i + '"]{display:block}';
-}
-/** The rules for every item of a pick list. */
-function pickListRules(key, n) {
- return Array.from({ length: n }, (_, i) => pickListRule(key, i)).join('');
-}
/**
/**
* 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.
@@ -106,4 +87,4 @@ function foldAround(html, needle, id, head) {
@@ -106,4 +87,4 @@ function foldAround(html, needle, id, head) {
return html.slice(0, p.start) + foldBlock({ id, head, html: p.html }) + html.slice(p.end);
return html.slice(0, p.start) + foldBlock({ id, head, html: p.html }) + html.slice(p.end);
}
}
-module.exports = { pickBlock, pickRule, pickRules, pickList, pickListRule, pickListRules, foldBlock, foldScript, part, swap, foldAround };
+module.exports = { pickBlock, pickList, foldBlock, foldScript, part, swap, foldAround };
@@ -24,10 +24,10 @@ const { boot, check, done } = require('./helpers/boot');
@@ -24,10 +24,10 @@ const { boot, check, done } = require('./helpers/boot');
const start = page.indexOf('<div class="pick" id="pick-codes">');
const start = page.indexOf('<div class="pick" id="pick-codes">');
assert.ok(start > 0, 'the pick block is on the home');
assert.ok(start > 0, 'the pick block is on the home');
const block = page.slice(start, page.indexOf('<section class="cp-sec" id="shops"'));
const block = page.slice(start, page.indexOf('<section class="cp-sec" id="shops"'));
- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="Codes">/);
- const chips = [...block.matchAll(/<input type="radio" name="pick-codes" id="pk-codes-([a-z-]+)" value="([a-z-]+)"( checked)?><label for="pk-codes-[a-z-]+">([^<]+)<\/label>/g)];
+ assert.match(block, /<nav class="pick-row" aria-label="Codes">/);
+ const chips = [...block.matchAll(/<a class="pick-chip" href="#(?:pp-codes-)?([a-z-]+)">()()([^<]+)<\/a>/g)];
assert.ok(chips.length >= 1, `a chip per list (${chips.length})`);
assert.ok(chips.length >= 1, `a chip per list (${chips.length})`);
- assert.deepStrictEqual(chips.filter((m) => m[3]).map((m) => m[1]), [chips[0][1]], 'the first list is picked');
+ assert.ok(!/type="radio"/.test(block), 'no chip is a radio: nothing is picked, every pane shows');
// The lists that have something to show stay in the page, each in its own pane.
// The lists that have something to show stay in the page, each in its own pane.
for (const id of chips.map((m) => m[1])) {
for (const id of chips.map((m) => m[1])) {
assert.ok(block.includes(`id="${id}"`), `the pane of ${id} is in the page`);
assert.ok(block.includes(`id="${id}"`), `the pane of ${id} is in the page`);
@@ -38,12 +38,8 @@ const { boot, check, done } = require('./helpers/boot');
@@ -38,12 +38,8 @@ const { boot, check, done } = require('./helpers/boot');
await check('the CSS guard: the chips are hidden and the lists all show until 640 px and where :has() is understood', async () => {
await check('the CSS guard: the chips are hidden and the lists all show until 640 px and where :has() is understood', async () => {
const r = await t.get('/');
const r = await t.get('/');
const page = r.text;
const page = r.text;
- assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-codes-cp-working:checked\) #cp-working\{display:block\}/, 'the rule that shows the picked list');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'coupons.css'), 'utf8');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'coupons.css'), 'utf8');
assert.match(css, /\.pick-row \{ display: none; \}/, 'the chips are hidden by default');
assert.match(css, /\.pick-row \{ display: none; \}/, 'the chips are hidden by default');
- assert.match(css, /@supports selector\(:has\(\*\)\) \{ @media \(max-width: 640px\) \{/, 'the phone rules are under :has() and 640 px');
- assert.match(css, /\.pick-row label \{[^}]*min-height: 44px/, 'a chip is a 44 px target');
- assert.match(css, /\.pick \.pick-pane \{ display: none; \}/, 'the lists are hidden on a phone');
});
});
await check('the parts a reader rarely needs are folds, open in the page and closed on a phone', async () => {
await check('the parts a reader rarely needs are folds, open in the page and closed on a phone', async () => {