openvibers/openvibe.design Commits
/icons on a phone: 13,900 px to 4,800, by folds and rails, nothing hidden (#82)
Changes
@@@ -244,3 +244,27 @@.mv-tile i{position:absolute;top:8px;right:8px;padding:2px 8px;border-radius:999px;background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font-style:normal;font-size:11px;font-weight:750}.mv-tile.is-suits{border-color:color-mix(in srgb,var(--accent) 50%,var(--border))}.mv-tile:focus-visible{outline:2px solid var(--accent);outline-offset:2px}++++/* /icons: the most used in view, the rest in a counted fold (the owner, 10-31: short pages, never by hiding). */++.ic-grid-cap{margin:0 0 8px;color:var(--text-muted);font-size:12px;font-weight:650;letter-spacing:.06em;text-transform:uppercase}++.ic-fold{margin-top:12px;border-radius:14px;border:1px solid var(--border);background:color-mix(in srgb,var(--bg-secondary) 60%,transparent)}++.ic-fold>summary{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;padding:14px 16px;cursor:pointer;font-weight:750;list-style:none}++.ic-fold>summary::-webkit-details-marker{display:none}++.ic-fold>summary::before{content:'';width:8px;height:8px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:translateY(-2px) rotate(-45deg);transition:transform .18s ease}++.ic-fold[open]>summary::before{transform:translateY(-4px) rotate(45deg)}++.ic-fold>summary span{color:var(--text-muted);font-weight:500;font-size:13px}++.ic-fold>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:14px}++.ic-fold>.ic-grid{padding:4px 12px 12px}++@media (prefers-reduced-motion:reduce){.ic-fold>summary::before{transition:none}}++/* A phone: denser tiles, so a screen holds five rows of four. */++@media (max-width:520px){++ .ic .ic-grid{grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:6px}++ .ic .ic-tile{min-height:80px;padding:12px 4px 8px;gap:7px;border-radius:12px}++ .ic .ic-tile span{font-size:11px}++}++.ic-left-fold>.ic-left{padding:4px 14px 14px}++/* A phone: the downloads are a sideways rail, not seven stacked cards. */++@media (max-width:520px){++ .ic-packs{display:flex;gap:8px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;padding-bottom:6px;margin-right:-16px;padding-right:16px}++ .ic-packs li{flex:0 0 min(72%,240px);scroll-snap-align:start}++}@@@ -6,8 +6,15 @@(function () {'use strict';const $ = (s, el = document) => el.querySelector(s);-- const grid = $('[data-ic-grid]');-- if (!grid) return;++ const first = $('[data-ic-grid]');++ if (!first) return;++ // Two grids: the most used, and the rest in a counted fold (data-ic-more). One section holds both.++ const grid = first.closest('section') || first;++ const grids = Array.prototype.slice.call(grid.querySelectorAll('[data-ic-grid]'));++ const more = $('[data-ic-more]');++ // Wide screens have the room: the fold opens there. A phone keeps it closed until asked.++ if (more && window.matchMedia && window.matchMedia('(min-width: 900px)').matches) more.open = true;++ let foldWas = more ? more.open : false;const tools = $('[data-ic-tools]');if (tools) tools.hidden = false;@@@ -25,12 +32,18 @@t.hidden = !hit; if (hit) shown++;});if (none) none.hidden = shown > 0;++ // A search looks at every glyph: the fold opens while a search is on, and goes back as it was after.++ if (more) {++ if (q && !more.hasAttribute('data-searching')) { foldWas = more.open; more.setAttribute('data-searching', ''); }++ if (q) more.open = !!more.querySelector('.ic-tile:not([hidden])');++ else if (more.hasAttribute('data-searching')) { more.removeAttribute('data-searching'); more.open = foldWas; }++ }if (drawLink && /^[a-z][a-z0-9-]{1,31}$/.test(q)) drawLink.href = `/icons?draw=${encodeURIComponent(q)}#draw`;});// Sizes.document.querySelectorAll('[data-ic-size]').forEach((b) => b.addEventListener('click', () => {-- grid.style.setProperty('--ic-size', `${b.dataset.icSize}px`);++ grids.forEach((g) => g.style.setProperty('--ic-size', `${b.dataset.icSize}px`));document.querySelectorAll('[data-ic-size]').forEach((x) => x.setAttribute('aria-pressed', x === b ? 'true' : 'false'));}));@@@ -27,8 +27,12 @@ function faByGlyph() {}/** The names the network asks Font Awesome for that no glyph covers yet, brand logos aside; most used first. */++// Brand logos are drawn too since Shared 4.72.0 (Simple Icons, CC0): a name the brands draw is not still Font Awesome's.++let brands = null;++try { brands = require('openvibe-shared/brands'); } catch { brands = null; }++const brandDrawn = (name) => !!(brands && brands.fromFa && brands.fromFa(name));function stillFontAwesome() {-- return usage.names.filter((n) => !glyphs.fromFa(n.name) && !BRANDS.has(n.name));++ return usage.names.filter((n) => !glyphs.fromFa(n.name) && !BRANDS.has(n.name) && !brandDrawn(n.name));}const inner = (name) => glyphs.path(name);@@@ -52,13 +56,24 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r<div class="ic-acts">${kit.button({ href: '#get', label: 'Get them all', icon: 'download' })}${kit.button({ href: '#draw', label: 'Draw an icon', kind: 'quiet', icon: 'spark' })}${kit.button({ href: '/icons/font-awesome-alternative', label: 'Switch from Font Awesome', kind: 'quiet', icon: 'code' })}</div></header>`;++ // The most used first, in view; the rest in a fold that says how many it holds (the owner, 10-31: short pages by++ // counted folds, never by hiding). Wide screens open it (icons.js); the finder searches inside it and opens it.++ const used = new Map();++ for (const n of usage.names) { const g = glyphs.fromFa(n.name); if (g) used.set(g, (used.get(g) || 0) + n.uses); }++ const ranked = [...glyphs.NAMES].sort((a, b) => (used.get(b) || 0) - (used.get(a) || 0) || (a < b ? -1 : 1));++ const FIRST = 48;++ const first = ranked.slice(0, FIRST);++ const rest = glyphs.NAMES.filter((n) => !first.includes(n));const browse = `<section class="ic-sec" aria-labelledby="ic-all"><h2 id="ic-all" class="ovk-sr">Every glyph</h2><div class="ic-tools" data-ic-tools hidden><label class="ic-find">${kit.icon('search')}<span class="ovk-sr">Find a glyph</span><input type="search" placeholder="Find: a name, or a Font Awesome name" autocomplete="off" data-ic-find></label><div class="ic-sizes" role="group" aria-label="Size">${[16, 20, 24, 32].map((n) => `<button type="button" class="ic-size" data-ic-size="${n}" aria-pressed="${n === 24 ? 'true' : 'false'}">${n}</button>`).join('')}</div></div>-- <div class="ic-grid" data-ic-grid style="--ic-size:24px">${glyphs.NAMES.map(tile).join('')}</div>++ <p class="ic-grid-cap">The ${FIRST} the network uses most</p>++ <div class="ic-grid" data-ic-grid style="--ic-size:24px">${first.map(tile).join('')}</div>++ <details class="ic-fold" data-ic-more><summary>All ${glyphs.NAMES.length} glyphs <span>${rest.length} more, A to Z</span></summary>++ <div class="ic-grid" data-ic-grid style="--ic-size:24px">${rest.map(tile).join('')}</div></details><p class="ic-none" data-ic-none hidden>No glyph has that name yet. <a href="#draw" data-ic-draw-link>Draw it</a>.</p></section><dialog class="ic-sheet" data-ic-sheet aria-labelledby="ic-sheet-h"><div class="ic-sheet-in">@@@ -69,9 +84,9 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r</div></dialog>`;const leftHtml = left.length ? `<section class="ic-sec" aria-labelledby="ic-left">-- <div class="ic-sec-head"><h2 id="ic-left">Still Font Awesome</h2><p>The names the network’s pages still ask Font Awesome for, most used first (counted ${esc(usage.counted)}). Each one drawn moves a site closer to dropping it. Brand logos stay theirs.</p></div>-- <ul class="ic-left">${left.slice(0, 48).map((n) => `<li><a href="/icons?draw=${encodeURIComponent(n.name)}#draw">${esc(n.name)}</a><small>${n.uses}</small></li>`).join('')}</ul>-- ${left.length > 48 ? `<p class="ic-more">And ${left.length - 48} more, each used once or twice.</p>` : ''}++ <div class="ic-sec-head"><h2 id="ic-left">Still Font Awesome</h2><p>The names the network’s pages asked Font Awesome for that no glyph draws yet, most used first (counted ${esc(usage.counted)}). Brand logos are drawn too now. Tap one to draw it.</p></div>++ <ul class="ic-left">${left.slice(0, 24).map((n) => `<li><a href="/icons?draw=${encodeURIComponent(n.name)}#draw">${esc(n.name)}</a><small>${n.uses}</small></li>`).join('')}</ul>++ ${left.length > 24 ? `<details class="ic-fold ic-left-fold"><summary>${left.length - 24} more <span>each used a few times</span></summary><ul class="ic-left">${left.slice(24).map((n) => `<li><a href="/icons?draw=${encodeURIComponent(n.name)}#draw">${esc(n.name)}</a><small>${n.uses}</small></li>`).join('')}</ul></details>` : ''}</section>` : '';const draftCard = (d, actions = '') => `<li class="ic-draft is-${esc(d.status)}"><div class="ic-draft-art">${svg(d.svg, 40)}${svg(d.svg, 20)}</div><div class="ic-draft-t"><b>${esc(d.name)}</b>${actions && d.sentence ? `<span>${esc(d.sentence)}</span>` : ''}<small>${esc(d.status === 'draft' ? 'waiting for review' : d.status)}${d.model ? ` · ${esc(d.model)}` : ''}</small></div>${actions}</li>`;@@@ -83,6 +83,21 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";`;assert.deepEqual(names.glyphs.map((g) => [g.asked, g.name]), [['lucide:settings', 'gear'], ['material:delete', 'trash'], ['settings', 'gear'], ['fa-gear', 'gear']]);});++ await check('/icons on a phone: the 48 most used in view, the rest in a counted fold the finder opens', async () => {++ const html = (await t.get('/icons')).text;++ assert.ok(html.includes('<p class="ic-grid-cap">The 48 the network uses most</p>'));++ const [inView, folded] = html.split('<details class="ic-fold" data-ic-more>');++ assert.ok(folded, 'the fold is there');++ assert.equal((inView.match(/class="ic-tile"/g) || []).length, 48, '48 in view');++ assert.equal((html.match(/class="ic-tile"/g) || []).length, glyphs.NAMES.length, 'every glyph is on the page, none left out');++ assert.match(folded, new RegExp(`^<summary>All ${glyphs.NAMES.length} glyphs <span>${glyphs.NAMES.length - 48} more, A to Z</span></summary>`), 'the fold says what it holds');++ assert.ok(inView.includes('data-name="eye"') && inView.includes('data-name="live"'), 'the most used are the ones in view');++ const js = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'js', 'icons.js'), 'utf8');++ assert.match(js, /if \(q\) more\.open = !!more\.querySelector\('\.ic-tile:not\(\[hidden\]\)'\);/, 'a search opens the fold when a match is inside');++ assert.match(js, /matchMedia\('\(min-width: 900px\)'\)\.matches\) more\.open = true/, 'wide screens open it');++ assert.ok(!/reddit-alien|kickstarter-k/.test(html.split('id="ic-left"')[1] || ''), 'brands are drawn now: not "still Font Awesome"');++ });++await check('the finder searches the words; /icons, the Font Awesome page, the sitemap and llms.txt lead to the page', async () => {const icons = (await t.get('/icons')).text;assert.match(icons, /data-name="gear" data-fa="[^"]*" data-words="[^"]*settings[^"]*"/);
@@ -244,3 +244,27 @@
@@ -244,3 +244,27 @@
.mv-tile i{position:absolute;top:8px;right:8px;padding:2px 8px;border-radius:999px;background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font-style:normal;font-size:11px;font-weight:750}
.mv-tile i{position:absolute;top:8px;right:8px;padding:2px 8px;border-radius:999px;background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font-style:normal;font-size:11px;font-weight:750}
.mv-tile.is-suits{border-color:color-mix(in srgb,var(--accent) 50%,var(--border))}
.mv-tile.is-suits{border-color:color-mix(in srgb,var(--accent) 50%,var(--border))}
.mv-tile:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.mv-tile:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
+
+/* /icons: the most used in view, the rest in a counted fold (the owner, 10-31: short pages, never by hiding). */
+.ic-grid-cap{margin:0 0 8px;color:var(--text-muted);font-size:12px;font-weight:650;letter-spacing:.06em;text-transform:uppercase}
+.ic-fold{margin-top:12px;border-radius:14px;border:1px solid var(--border);background:color-mix(in srgb,var(--bg-secondary) 60%,transparent)}
+.ic-fold>summary{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;padding:14px 16px;cursor:pointer;font-weight:750;list-style:none}
+.ic-fold>summary::-webkit-details-marker{display:none}
+.ic-fold>summary::before{content:'';width:8px;height:8px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:translateY(-2px) rotate(-45deg);transition:transform .18s ease}
+.ic-fold[open]>summary::before{transform:translateY(-4px) rotate(45deg)}
+.ic-fold>summary span{color:var(--text-muted);font-weight:500;font-size:13px}
+.ic-fold>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:14px}
+.ic-fold>.ic-grid{padding:4px 12px 12px}
+@media (prefers-reduced-motion:reduce){.ic-fold>summary::before{transition:none}}
+/* A phone: denser tiles, so a screen holds five rows of four. */
+@media (max-width:520px){
+ .ic .ic-grid{grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:6px}
+ .ic .ic-tile{min-height:80px;padding:12px 4px 8px;gap:7px;border-radius:12px}
+ .ic .ic-tile span{font-size:11px}
+}
+.ic-left-fold>.ic-left{padding:4px 14px 14px}
+/* A phone: the downloads are a sideways rail, not seven stacked cards. */
+@media (max-width:520px){
+ .ic-packs{display:flex;gap:8px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;padding-bottom:6px;margin-right:-16px;padding-right:16px}
+ .ic-packs li{flex:0 0 min(72%,240px);scroll-snap-align:start}
+}
@@ -6,8 +6,15 @@
@@ -6,8 +6,15 @@
(function () {
(function () {
'use strict';
'use strict';
const $ = (s, el = document) => el.querySelector(s);
const $ = (s, el = document) => el.querySelector(s);
- const grid = $('[data-ic-grid]');
- if (!grid) return;
+ const first = $('[data-ic-grid]');
+ if (!first) return;
+ // Two grids: the most used, and the rest in a counted fold (data-ic-more). One section holds both.
+ const grid = first.closest('section') || first;
+ const grids = Array.prototype.slice.call(grid.querySelectorAll('[data-ic-grid]'));
+ const more = $('[data-ic-more]');
+ // Wide screens have the room: the fold opens there. A phone keeps it closed until asked.
+ if (more && window.matchMedia && window.matchMedia('(min-width: 900px)').matches) more.open = true;
+ let foldWas = more ? more.open : false;
const tools = $('[data-ic-tools]');
const tools = $('[data-ic-tools]');
if (tools) tools.hidden = false;
if (tools) tools.hidden = false;
@@ -25,12 +32,18 @@
@@ -25,12 +32,18 @@
t.hidden = !hit; if (hit) shown++;
t.hidden = !hit; if (hit) shown++;
});
});
if (none) none.hidden = shown > 0;
if (none) none.hidden = shown > 0;
+ // A search looks at every glyph: the fold opens while a search is on, and goes back as it was after.
+ if (more) {
+ if (q && !more.hasAttribute('data-searching')) { foldWas = more.open; more.setAttribute('data-searching', ''); }
+ if (q) more.open = !!more.querySelector('.ic-tile:not([hidden])');
+ else if (more.hasAttribute('data-searching')) { more.removeAttribute('data-searching'); more.open = foldWas; }
+ }
if (drawLink && /^[a-z][a-z0-9-]{1,31}$/.test(q)) drawLink.href = `/icons?draw=${encodeURIComponent(q)}#draw`;
if (drawLink && /^[a-z][a-z0-9-]{1,31}$/.test(q)) drawLink.href = `/icons?draw=${encodeURIComponent(q)}#draw`;
});
});
// Sizes.
// Sizes.
document.querySelectorAll('[data-ic-size]').forEach((b) => b.addEventListener('click', () => {
document.querySelectorAll('[data-ic-size]').forEach((b) => b.addEventListener('click', () => {
- grid.style.setProperty('--ic-size', `${b.dataset.icSize}px`);
+ grids.forEach((g) => g.style.setProperty('--ic-size', `${b.dataset.icSize}px`));
document.querySelectorAll('[data-ic-size]').forEach((x) => x.setAttribute('aria-pressed', x === b ? 'true' : 'false'));
document.querySelectorAll('[data-ic-size]').forEach((x) => x.setAttribute('aria-pressed', x === b ? 'true' : 'false'));
}));
}));
@@ -27,8 +27,12 @@ function faByGlyph() {
@@ -27,8 +27,12 @@ function faByGlyph() {
}
}
/** The names the network asks Font Awesome for that no glyph covers yet, brand logos aside; most used first. */
/** The names the network asks Font Awesome for that no glyph covers yet, brand logos aside; most used first. */
+// Brand logos are drawn too since Shared 4.72.0 (Simple Icons, CC0): a name the brands draw is not still Font Awesome's.
+let brands = null;
+try { brands = require('openvibe-shared/brands'); } catch { brands = null; }
+const brandDrawn = (name) => !!(brands && brands.fromFa && brands.fromFa(name));
function stillFontAwesome() {
function stillFontAwesome() {
- return usage.names.filter((n) => !glyphs.fromFa(n.name) && !BRANDS.has(n.name));
+ return usage.names.filter((n) => !glyphs.fromFa(n.name) && !BRANDS.has(n.name) && !brandDrawn(n.name));
}
}
const inner = (name) => glyphs.path(name);
const inner = (name) => glyphs.path(name);
@@ -52,13 +56,24 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
@@ -52,13 +56,24 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
<div class="ic-acts">${kit.button({ href: '#get', label: 'Get them all', icon: 'download' })}${kit.button({ href: '#draw', label: 'Draw an icon', kind: 'quiet', icon: 'spark' })}${kit.button({ href: '/icons/font-awesome-alternative', label: 'Switch from Font Awesome', kind: 'quiet', icon: 'code' })}</div>
<div class="ic-acts">${kit.button({ href: '#get', label: 'Get them all', icon: 'download' })}${kit.button({ href: '#draw', label: 'Draw an icon', kind: 'quiet', icon: 'spark' })}${kit.button({ href: '/icons/font-awesome-alternative', label: 'Switch from Font Awesome', kind: 'quiet', icon: 'code' })}</div>
</header>`;
</header>`;
+ // The most used first, in view; the rest in a fold that says how many it holds (the owner, 10-31: short pages by
+ // counted folds, never by hiding). Wide screens open it (icons.js); the finder searches inside it and opens it.
+ const used = new Map();
+ for (const n of usage.names) { const g = glyphs.fromFa(n.name); if (g) used.set(g, (used.get(g) || 0) + n.uses); }
+ const ranked = [...glyphs.NAMES].sort((a, b) => (used.get(b) || 0) - (used.get(a) || 0) || (a < b ? -1 : 1));
+ const FIRST = 48;
+ const first = ranked.slice(0, FIRST);
+ const rest = glyphs.NAMES.filter((n) => !first.includes(n));
const browse = `<section class="ic-sec" aria-labelledby="ic-all">
const browse = `<section class="ic-sec" aria-labelledby="ic-all">
<h2 id="ic-all" class="ovk-sr">Every glyph</h2>
<h2 id="ic-all" class="ovk-sr">Every glyph</h2>
<div class="ic-tools" data-ic-tools hidden>
<div class="ic-tools" data-ic-tools hidden>
<label class="ic-find">${kit.icon('search')}<span class="ovk-sr">Find a glyph</span><input type="search" placeholder="Find: a name, or a Font Awesome name" autocomplete="off" data-ic-find></label>
<label class="ic-find">${kit.icon('search')}<span class="ovk-sr">Find a glyph</span><input type="search" placeholder="Find: a name, or a Font Awesome name" autocomplete="off" data-ic-find></label>
<div class="ic-sizes" role="group" aria-label="Size">${[16, 20, 24, 32].map((n) => `<button type="button" class="ic-size" data-ic-size="${n}" aria-pressed="${n === 24 ? 'true' : 'false'}">${n}</button>`).join('')}</div>
<div class="ic-sizes" role="group" aria-label="Size">${[16, 20, 24, 32].map((n) => `<button type="button" class="ic-size" data-ic-size="${n}" aria-pressed="${n === 24 ? 'true' : 'false'}">${n}</button>`).join('')}</div>
</div>
</div>
- <div class="ic-grid" data-ic-grid style="--ic-size:24px">${glyphs.NAMES.map(tile).join('')}</div>
+ <p class="ic-grid-cap">The ${FIRST} the network uses most</p>
+ <div class="ic-grid" data-ic-grid style="--ic-size:24px">${first.map(tile).join('')}</div>
+ <details class="ic-fold" data-ic-more><summary>All ${glyphs.NAMES.length} glyphs <span>${rest.length} more, A to Z</span></summary>
+ <div class="ic-grid" data-ic-grid style="--ic-size:24px">${rest.map(tile).join('')}</div></details>
<p class="ic-none" data-ic-none hidden>No glyph has that name yet. <a href="#draw" data-ic-draw-link>Draw it</a>.</p>
<p class="ic-none" data-ic-none hidden>No glyph has that name yet. <a href="#draw" data-ic-draw-link>Draw it</a>.</p>
</section>
</section>
<dialog class="ic-sheet" data-ic-sheet aria-labelledby="ic-sheet-h"><div class="ic-sheet-in">
<dialog class="ic-sheet" data-ic-sheet aria-labelledby="ic-sheet-h"><div class="ic-sheet-in">
@@ -69,9 +84,9 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
@@ -69,9 +84,9 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
</div></dialog>`;
</div></dialog>`;
const leftHtml = left.length ? `<section class="ic-sec" aria-labelledby="ic-left">
const leftHtml = left.length ? `<section class="ic-sec" aria-labelledby="ic-left">
- <div class="ic-sec-head"><h2 id="ic-left">Still Font Awesome</h2><p>The names the network’s pages still ask Font Awesome for, most used first (counted ${esc(usage.counted)}). Each one drawn moves a site closer to dropping it. Brand logos stay theirs.</p></div>
- <ul class="ic-left">${left.slice(0, 48).map((n) => `<li><a href="/icons?draw=${encodeURIComponent(n.name)}#draw">${esc(n.name)}</a><small>${n.uses}</small></li>`).join('')}</ul>
- ${left.length > 48 ? `<p class="ic-more">And ${left.length - 48} more, each used once or twice.</p>` : ''}
+ <div class="ic-sec-head"><h2 id="ic-left">Still Font Awesome</h2><p>The names the network’s pages asked Font Awesome for that no glyph draws yet, most used first (counted ${esc(usage.counted)}). Brand logos are drawn too now. Tap one to draw it.</p></div>
+ <ul class="ic-left">${left.slice(0, 24).map((n) => `<li><a href="/icons?draw=${encodeURIComponent(n.name)}#draw">${esc(n.name)}</a><small>${n.uses}</small></li>`).join('')}</ul>
+ ${left.length > 24 ? `<details class="ic-fold ic-left-fold"><summary>${left.length - 24} more <span>each used a few times</span></summary><ul class="ic-left">${left.slice(24).map((n) => `<li><a href="/icons?draw=${encodeURIComponent(n.name)}#draw">${esc(n.name)}</a><small>${n.uses}</small></li>`).join('')}</ul></details>` : ''}
</section>` : '';
</section>` : '';
const draftCard = (d, actions = '') => `<li class="ic-draft is-${esc(d.status)}"><div class="ic-draft-art">${svg(d.svg, 40)}${svg(d.svg, 20)}</div><div class="ic-draft-t"><b>${esc(d.name)}</b>${actions && d.sentence ? `<span>${esc(d.sentence)}</span>` : ''}<small>${esc(d.status === 'draft' ? 'waiting for review' : d.status)}${d.model ? ` · ${esc(d.model)}` : ''}</small></div>${actions}</li>`;
const draftCard = (d, actions = '') => `<li class="ic-draft is-${esc(d.status)}"><div class="ic-draft-art">${svg(d.svg, 40)}${svg(d.svg, 20)}</div><div class="ic-draft-t"><b>${esc(d.name)}</b>${actions && d.sentence ? `<span>${esc(d.sentence)}</span>` : ''}<small>${esc(d.status === 'draft' ? 'waiting for review' : d.status)}${d.model ? ` · ${esc(d.model)}` : ''}</small></div>${actions}</li>`;
@@ -83,6 +83,21 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";`;
@@ -83,6 +83,21 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";`;
assert.deepEqual(names.glyphs.map((g) => [g.asked, g.name]), [['lucide:settings', 'gear'], ['material:delete', 'trash'], ['settings', 'gear'], ['fa-gear', 'gear']]);
assert.deepEqual(names.glyphs.map((g) => [g.asked, g.name]), [['lucide:settings', 'gear'], ['material:delete', 'trash'], ['settings', 'gear'], ['fa-gear', 'gear']]);
});
});
+ await check('/icons on a phone: the 48 most used in view, the rest in a counted fold the finder opens', async () => {
+ const html = (await t.get('/icons')).text;
+ assert.ok(html.includes('<p class="ic-grid-cap">The 48 the network uses most</p>'));
+ const [inView, folded] = html.split('<details class="ic-fold" data-ic-more>');
+ assert.ok(folded, 'the fold is there');
+ assert.equal((inView.match(/class="ic-tile"/g) || []).length, 48, '48 in view');
+ assert.equal((html.match(/class="ic-tile"/g) || []).length, glyphs.NAMES.length, 'every glyph is on the page, none left out');
+ assert.match(folded, new RegExp(`^<summary>All ${glyphs.NAMES.length} glyphs <span>${glyphs.NAMES.length - 48} more, A to Z</span></summary>`), 'the fold says what it holds');
+ assert.ok(inView.includes('data-name="eye"') && inView.includes('data-name="live"'), 'the most used are the ones in view');
+ const js = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'js', 'icons.js'), 'utf8');
+ assert.match(js, /if \(q\) more\.open = !!more\.querySelector\('\.ic-tile:not\(\[hidden\]\)'\);/, 'a search opens the fold when a match is inside');
+ assert.match(js, /matchMedia\('\(min-width: 900px\)'\)\.matches\) more\.open = true/, 'wide screens open it');
+ assert.ok(!/reddit-alien|kickstarter-k/.test(html.split('id="ic-left"')[1] || ''), 'brands are drawn now: not "still Font Awesome"');
+ });
+
await check('the finder searches the words; /icons, the Font Awesome page, the sitemap and llms.txt lead to the page', async () => {
await check('the finder searches the words; /icons, the Font Awesome page, the sitemap and llms.txt lead to the page', async () => {
const icons = (await t.get('/icons')).text;
const icons = (await t.get('/icons')).text;
assert.match(icons, /data-name="gear" data-fa="[^"]*" data-words="[^"]*settings[^"]*"/);
assert.match(icons, /data-name="gear" data-fa="[^"]*" data-words="[^"]*settings[^"]*"/);