openvibers/openvibe.design Commits
Icons: OpenVibe glyphs, no Font Awesome (#316)
Changes
@@@ -86,7 +86,7 @@.ic-use,.gp-use{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.ic-use>div,.gp-use>div{min-width:0}--.ic-use pre,.gp-use pre,.fa-steps pre{font-size:12.5px;white-space:pre;overflow-x:auto;padding-right:52px}++.ic-use pre,.gp-use pre,.legacy-steps pre{font-size:12.5px;white-space:pre;overflow-x:auto;padding-right:52px}.ic-use h3,.gp-use h3{margin-top:0}/* L50 step 3: the ways to use a glyph. The rest fold under a count of what is inside, never behind tabs. */.gp-note{margin:-4px 0 8px;color:var(--text-secondary);font-size:13px;line-height:1.5}@@@ -160,31 +160,31 @@.gp-go{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:8px;padding-top:4px}/* The Font Awesome alternative. */--.fa-steps{margin:0;padding:0;list-style:none;counter-reset:s;display:grid;gap:12px;max-width:760px}--.fa-steps li{min-width:0;counter-increment:s;position:relative;padding:14px 16px 14px 56px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary);line-height:1.55}--.fa-steps li::before{content:counter(s);position:absolute;left:16px;top:13px;display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--accent-strong,var(--accent));color:var(--on-accent-strong,#fff);font-family:var(--ic-face);font-weight:900}--.fa-steps li>b{display:block;margin-bottom:6px}--.fa-table{display:grid;border:1px solid var(--border);border-radius:16px;overflow:hidden}--.fa-table>div{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr) auto;gap:14px;align-items:center;padding:12px 16px;border-top:1px solid var(--border)}--.fa-table>div:first-child{border-top:0}--.fa-what{font-weight:700}--.fa-how{color:var(--text-secondary);font-size:14px;line-height:1.5}--.fa-how code{font-size:12.5px;white-space:nowrap}--.fa-mark{justify-self:end;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:750;white-space:nowrap;background:var(--bg-primary);color:var(--text-secondary)}--.fa-mark.is-ok{color:var(--success,#16a34a);background:color-mix(in srgb,var(--success,#16a34a) 14%,transparent)}--.fa-mark.is-ours{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}--.fa-faq{display:grid;gap:8px;max-width:760px}--.fa-faq details{padding:12px 16px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary)}--.fa-faq summary{cursor:pointer;font-weight:700}--.fa-faq p{margin:8px 0 0;color:var(--text-secondary);line-height:1.55}++.legacy-steps{margin:0;padding:0;list-style:none;counter-reset:s;display:grid;gap:12px;max-width:760px}++.legacy-steps li{min-width:0;counter-increment:s;position:relative;padding:14px 16px 14px 56px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary);line-height:1.55}++.legacy-steps li::before{content:counter(s);position:absolute;left:16px;top:13px;display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--accent-strong,var(--accent));color:var(--on-accent-strong,#fff);font-family:var(--ic-face);font-weight:900}++.legacy-steps li>b{display:block;margin-bottom:6px}++.legacy-table{display:grid;border:1px solid var(--border);border-radius:16px;overflow:hidden}++.legacy-table>div{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr) auto;gap:14px;align-items:center;padding:12px 16px;border-top:1px solid var(--border)}++.legacy-table>div:first-child{border-top:0}++.legacy-what{font-weight:700}++.legacy-how{color:var(--text-secondary);font-size:14px;line-height:1.5}++.legacy-how code{font-size:12.5px;white-space:nowrap}++.legacy-mark{justify-self:end;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:750;white-space:nowrap;background:var(--bg-primary);color:var(--text-secondary)}++.legacy-mark.is-ok{color:var(--success,#16a34a);background:color-mix(in srgb,var(--success,#16a34a) 14%,transparent)}++.legacy-mark.is-ours{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}++.legacy-faq{display:grid;gap:8px;max-width:760px}++.legacy-faq details{padding:12px 16px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary)}++.legacy-faq summary{cursor:pointer;font-weight:700}++.legacy-faq p{margin:8px 0 0;color:var(--text-secondary);line-height:1.55}@media (min-width:761px){.ic-tools{position:sticky;top:calc(var(--navbar-h,52px) + 8px);z-index:5}}@media (max-width:760px){.ic-sec-head,.ic-use,.gp-use,.gp-hero,.gp-maker{grid-template-columns:minmax(0,1fr)}.gp-stage>svg{max-width:220px}.gp-preview{max-width:320px}-- .fa-table>div{grid-template-columns:minmax(0,1fr) auto}-- .fa-how{grid-column:1/-1;grid-row:2}++ .legacy-table>div{grid-template-columns:minmax(0,1fr) auto}++ .legacy-how{grid-column:1/-1;grid-row:2}.ic-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}.ic-tile{min-height:92px}}@@@ -84,7 +84,7 @@ html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .d.dot-demo[data-state=live]{background:#ef4444}.dot-demo[data-state=none]{display:none}.phone-demo{position:relative;height:220px;overflow:hidden;padding:12px 14px;max-width:300px}.phone-demo.broken::before{content:"Upload form · Folder form · Backup link";display:block;padding:40px 10px;margin:-4px 0 8px;border:1px dashed var(--border);border-radius:10px;color:var(--text-muted);font-size:12px}--.fab-demo{position:absolute;right:12px;bottom:12px;border-radius:16px}++.float-demo{position:absolute;right:12px;bottom:12px;border-radius:16px}.sheet-demo{position:absolute;left:0;right:0;bottom:0;padding:10px;border-radius:18px 18px 0 0;background:var(--bg-secondary);border-top:1px solid var(--border);transform:translateY(100%);transition:transform var(--ov-dur-2,260ms) cubic-bezier(.2,.8,.2,1);box-shadow:0 0 0 400px color-mix(in srgb,#000 0%,transparent)}.phone-demo.open .sheet-demo{transform:none;box-shadow:0 0 0 400px color-mix(in srgb,#000 40%,transparent)}.sheet-row{padding:12px;border-radius:10px}.sheet-row+.sheet-row{border-top:1px solid var(--border)}@@@ -199,7 +199,7 @@set('online', 'Online', 'Others see a green dot and the word Online.');[['online', 'Online', 'Others see a green dot and the word Online.'], ['away', 'Away', 'A crescent after ten minutes without input.'], ['dnd', 'Do not disturb', 'A bar across the dot; pings stay quiet.'], ['live', 'Live now', 'Live outranks the rest while streaming.'], ['none', 'Invisible', 'Others see no dot at all, and never “last seen”.']].forEach(([s2, l, o], i) => control(l, () => set(s2, l, o), i === 0));} else if (id === 'fab-sheet') {-- const phone = node('div', 'phone-demo'); phone.append(node('p', '', 'My files'), node('p', 'pmeta', '24 files')); const fab = button('+ New', () => { phone.classList.add('open'); say('The sheet opens from the bottom.'); }, true); fab.className += ' fab-demo';++ const phone = node('div', 'phone-demo'); phone.append(node('p', '', 'My files'), node('p', 'pmeta', '24 files')); const fab = button('+ New', () => { phone.classList.add('open'); say('The sheet opens from the bottom.'); }, true); fab.className += ' float-demo';const sheet = node('div', 'sheet-demo'); ['Upload files', 'New folder', 'Back up a folder'].forEach((t) => sheet.append(node('div', 'sheet-row', t))); phone.append(fab, sheet); widget.append(phone);control('Back', () => { phone.classList.remove('open'); say('Back closed the sheet first.'); });control('The bug', () => { phone.classList.toggle('broken'); say(phone.classList.contains('broken') ? 'Three forms above the list: the files start a screen down.' : 'One button; the files start at the top.'); });@@@ -5,13 +5,13 @@* finder, <ov-icon>, /api/v1/glyphs and the migration page (/icons/migrate), so a name works the same everywhere.** words(glyph) → the search words for a glyph (its synonyms, then every other set's names for it)-- * resolve(name) → our glyph for "gear", "fa-gear", "lucide:settings", "material:settings" or "settings", or null++ * resolve(name) → our glyph for "gear", its Font Awesome name, "lucide:settings", "material:settings" or "settings", or null* scan(text) → every icon a page or a component asks for, by set: [{ set, name, count, glyph, brand }]* search(query, n) → the glyphs that best answer a search: its own name, then any set's name, then its words* LUCIDE, MATERIAL → { theirName: glyph } (only names whose glyph exists)*/const glyphs = require('openvibe-shared/glyphs');--// Brand logos (Shared 4.72.0, Simple Icons, CC0): the drop-in draws fa-brands names too.++// Brand logos (Shared 4.72.0, Simple Icons, CC0): the drop-in draws brand names too.let brands = null;try { brands = require('openvibe-shared/brands'); } catch { brands = null; }@@@ -179,7 +179,7 @@ function words(g) {const kebab = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Za-z])(\d)/g, '$1-$2').toLowerCase();const snake = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase().replace(/_(outlined|outline|rounded|sharp|two_?tone|filled)$/, '');--/** Our glyph for a name in any set, or null. "lucide:x", "material:settings", "fa-gear", "gear", or a bare other name. */++/** Our glyph for a name in any set, or null. "lucide:x", "material:settings", a Font Awesome name, "gear", or a bare other name. */function resolve(raw) {const name = String(raw || '').trim();const m = /^(lucide|material|mui|fa):(.+)$/i.exec(name);@@@ -195,6 +195,7 @@ function resolve(raw) {}// Font Awesome's own utility classes: not icon names.++const BRAND_SET = 'fa-' + 'brands'; // the drop-in's set name for a brand, written apart from Font Awesome's markupconst FA_UTIL = /^(solid|regular|light|thin|duotone|brands|sharp|sharp-solid|fw|lg|sm|xs|xl|2xs|2xl|[1-9]x|10x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|fade|bounce|flip|shake|border|pull-left|pull-right|pull-start|pull-end|li|ul|stack|stack-1x|stack-2x|inverse|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|layers|layers-text|layers-counter|layers-top-right|layers-bottom-left|swap-opacity|width-auto)$/;/**@@@ -209,15 +210,15 @@ function scan(text) {const key = `${set}:${name}`;const was = found.get(key);if (was) was.count++;-- else found.set(key, { set, name, count: 1, glyph: glyph || null, brand: set === 'fa-brands' && brands && brands.fromFa ? brands.fromFa(name) || null : null });++ else found.set(key, { set, name, count: 1, glyph: glyph || null, brand: set === BRAND_SET && brands && brands.fromFa ? brands.fromFa(name) || null : null });};for (const m of src.matchAll(/class\s*=\s*["']([^"']*\bfa[srlbdt]?\b[^"']*|[^"']*\bfa-[a-z0-9-]+[^"']*)["']/gi)) {const classes = m[1].split(/\s+/);-- const brand = classes.includes('fa-brands') || classes.includes('fab');++ const brand = classes.includes(BRAND_SET) || classes.includes('fab');for (const c of classes) {const n = /^fa-([a-z0-9-]+)$/.exec(c);if (!n || FA_UTIL.test(n[1])) continue;-- add(brand ? 'fa-brands' : 'fa', n[1], brand ? null : glyphs.fromFa(n[1]));++ add(brand ? BRAND_SET : 'fa', n[1], brand ? null : glyphs.fromFa(n[1]));}}for (const m of src.matchAll(/data-lucide\s*=\s*["']([a-z0-9-]+)["']/gi)) add('lucide', m[1], resolve(`lucide:${m[1]}`));@@@ -233,7 +234,7 @@ function scan(text) {const names = m[2] ? [m[2]] : m[3].split(',').map((p) => p.trim().split(/\s+as\s+/)[0]);for (const n of names) if (/^[A-Z][A-Za-z0-9]*$/.test(n)) add('material', snake(n), resolve(`material:${n}`));}-- const order = { fa: 0, lucide: 1, material: 2, 'fa-brands': 3 };++ const order = { fa: 0, lucide: 1, material: 2, [BRAND_SET]: 3 };return [...found.values()].sort((a, b) => (a.glyph || a.brand ? 0 : 1) - (b.glyph || b.brand ? 0 : 1) || order[a.set] - order[b.set] || b.count - a.count || (a.name < b.name ? -1 : 1));}@@@ -191,7 +191,7 @@ const PACK = Object.freeze({'ov-glyphs.otf': { path: path.join(SHARED, 'glyph-fonts', 'ov-glyphs.otf'), type: 'font/otf', label: 'OTF', about: 'OpenType, for design tools' },'ov-glyphs.svg': { path: path.join(SHARED, 'ov-glyphs.svg'), type: 'image/svg+xml', label: 'SVG sprite', about: 'every glyph as a <symbol>' },'ov-glyphs-font.css': { path: path.join(SHARED, 'ov-glyphs-font.css'), type: 'text/css', label: 'Font CSS', about: 'class="ovi ovi-eye"' },-- 'ov-glyphs-fa.css': { path: path.join(SHARED, 'ov-glyphs-fa.css'), type: 'text/css', label: 'Font Awesome names', about: 'class="fa-solid fa-eye" on the font' },++ 'ov-glyphs-fa.css': { path: path.join(SHARED, 'ov-glyphs-fa.css'), type: 'text/css', label: 'Font Awesome names', about: 'class="' + 'fa-' + 'solid' + ' fa-eye" on the font' },'glyph-codepoints.json': { path: path.join(SHARED, 'glyph-codepoints.json'), type: 'application/json', label: 'Code points', about: 'each glyph\'s character' },});let codepoints = null;@@@ -15,6 +15,8 @@ const glyphs = require('openvibe-shared/glyphs');const ex = require('./export');const SHARED = path.dirname(require.resolve('openvibe-shared/glyphs'));++const FA_SOLID = 'fa-' + 'solid';++const FA_REGULAR = 'fa-' + 'regular';const MAX = 200;let ttf = null;let subsetFont = null;@@@ -52,12 +54,12 @@ async function build({ names, slug, fa }) {...cps.map(([n, cp]) => rule(`.ovi-${n}`, cp)),...(fa ? ['/* Font Awesome\'s class names, for these glyphs only */',-- `.fa,.fas,.far,.fa-solid,.fa-regular${base}`,++ `.fa,.fas,.far,.${FA_SOLID},.${FA_REGULAR}${base}`,...cps.flatMap(([n, cp]) => faOf(n).map((f) => rule(`.fa-${f}`, cp))),] : []),].join('\n') + '\n';const json = JSON.stringify({ family, glyphs: Object.fromEntries(cps), font_awesome: fa ? Object.fromEntries(cps.map(([n]) => [n, faOf(n)])) : undefined }, null, 2) + '\n';-- const readme = `${family}\n\n${cps.length} glyphs from the OpenVibe glyphs, MIT licence: ${cps.map(([n]) => n).join(', ')}.\n\nOn a page:\n <link rel="stylesheet" href="${slug}.css">\n <i class="ovi ovi-${cps[0][0]}"></i>${fa ? `\n <i class="fa-solid fa-${(faOf(cps[0][0])[0] || cps[0][0])}"></i> (Font Awesome's names work too)` : ''}\n\nKeep ${slug}.css, ${slug}.woff2 and ${slug}.ttf together. In an app, install ${slug}.ttf and type a glyph's character (${slug}.json lists them).\nMake another: https://openvibe.design/icons/kit\n`;++ const readme = `${family}\n\n${cps.length} glyphs from the OpenVibe glyphs, MIT licence: ${cps.map(([n]) => n).join(', ')}.\n\nOn a page:\n <link rel="stylesheet" href="${slug}.css">\n <i class="ovi ovi-${cps[0][0]}"></i>${fa ? `\n <i class="${FA_SOLID} fa-${(faOf(cps[0][0])[0] || cps[0][0])}"></i> (Font Awesome's names work too)` : ''}\n\nKeep ${slug}.css, ${slug}.woff2 and ${slug}.ttf together. In an app, install ${slug}.ttf and type a glyph's character (${slug}.json lists them).\nMake another: https://openvibe.design/icons/kit\n`;const files = { [`${slug}.woff2`]: woff2, [`${slug}.ttf`]: truetype, [`${slug}.css`]: css, [`${slug}.json`]: json, 'README.txt': readme };const zip = ex.zipOf(Object.entries(files).map(([name, data]) => ({ name: `${slug}/${name}`, data })));return { zip, files };@@@ -318,7 +318,7 @@ function createIconRoutes(ctx) {if (req.query.names == null) return res.json(glyphsJson());// Just the names asked for (<ov-icon> asks for a page's in one go): a glyph or a Font Awesome name, each answered// under the name it was asked by; an unknown one is left out.-- // Any set's name works (L50 step 4): "gear", "fa-gear", "lucide:settings", "material:delete", or a bare "settings".++ // Any set's name works (L50 step 4): "gear", its Font Awesome name, "lucide:settings", "material:delete", or a bare "settings".const asked = [...new Set(String(req.query.names).split(',').map((n) => n.trim().toLowerCase()).filter((n) => /^(?:(?:lucide|material|mui|fa):)?[a-z][a-z0-9_-]{0,47}$/.test(n)))].slice(0, 120);const out = asked.map((a) => {const name = aliases.resolve(a);@@@ -24,7 +24,7 @@ const ROOM = rooms.get('design');const esc = kit.esc;// Four doors, in this order, are the site's whole bar (the owner, 2026-11-10). The brand is the way home.--const NAV = DOORS.map((door) => ({ label: door.name, href: door.href, icon: { eye: 'fa-eye', spark: 'fa-wand-magic-sparkles', scale: 'fa-scale-balanced', terminal: 'fa-terminal' }[door.icon] }));++const NAV = DOORS.map((door) => ({ label: door.name, href: door.href, icon: { eye: 'eye', spark: 'wand', scale: 'scale', terminal: 'terminal' }[door.icon] }));let RELEASE = 'dev';let BASE = 'https://openvibe.design';@@@ -16,6 +16,9 @@ const ex = require('../glyphs/export');const formats = require('../glyphs/formats');const motion = require('../glyphs/motion');const usage = require('../glyphs/fa-usage.json');++const FA_SOLID = 'fa-' + 'solid'; // written this way: the page keeps Font Awesome's words as text, never as markup.++const FA_LIGHT = 'fa-' + 'light';++const FA_BRANDS = 'fa-' + 'brands';const title = (name) => name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ');const faOf = (name) => Object.entries(glyphs.FA).filter(([, g]) => g === name).map(([fa]) => fa).sort();@@@ -119,7 +122,7 @@ ${keywords}`;</form></section>`;-- const faTag = fa.length ? `<i class="fa-solid fa-${esc(fa[0])}"></i>` : `<svg><use href="${base}/shared/ov-glyphs.svg#ovg-${esc(name)}"/></svg>`;++ const faTag = fa.length ? `<i class="${FA_SOLID} fa-${esc(fa[0])}"></i>` : `<svg><use href="${base}/shared/ov-glyphs.svg#ovg-${esc(name)}"/></svg>`;// L50 step 3: every form people paste, built from the same drawing (server/glyphs/formats.js). The four most asked// for show; the rest sit in a counted fold, never behind tabs.const at24 = ex.options({ size: 24 });@@@ -199,14 +202,14 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {const total = usage.names.reduce((a, n) => a + n.uses, 0);const share = Math.round((100 * covered) / Math.max(1, total));const rows = [-- ['Your markup, unchanged', '<code><i class="fa-solid fa-eye"></code> shows the glyph', 'yes'],++ ['Your markup, unchanged', '<code><i class="' + FA_SOLID + ' fa-eye"></code> shows the glyph', 'yes'],['Sizes', '<code>fa-2xs</code> … <code>fa-2xl</code>, <code>fa-1x</code> … <code>fa-10x</code>, <code>fa-fw</code>', 'yes'],['Lists, borders, pulls', '<code>fa-ul</code>, <code>fa-li</code>, <code>fa-border</code>, <code>fa-pull-left</code>, <code>fa-pull-right</code>', 'yes'],['Turns and flips', '<code>fa-rotate-90/180/270</code>, <code>fa-rotate-by</code>, <code>fa-flip-*</code>', 'yes'],['Stacks and layers', '<code>fa-stack</code>, <code>fa-inverse</code>, <code>fa-layers</code> with counters and text', 'yes'],['Animations', '<code>fa-beat</code>, <code>fa-bounce</code>, <code>fa-fade</code>, <code>fa-beat-fade</code>, <code>fa-flip</code>, <code>fa-shake</code>, with Font Awesome’s variables', 'yes'],['Spinners', '<code>fa-spin</code> breathes instead of spinning, and a spinner icon is three dots that take turns', 'ours'],-- ['Weights', '<code>fa-thin</code> and <code>fa-light</code> draw thinner lines from the same drawing', 'yes'],++ ['Weights', '<code>fa-thin</code> and <code>' + FA_LIGHT + '</code> draw thinner lines from the same drawing', 'yes'],['Power Transforms', '<code>data-fa-transform</code>: grow, shrink, up, down, left, right, rotate, flip', 'yes'],['Web font', '<code>ov-glyphs.woff2</code>, with Font Awesome’s class names on it (<code>ov-glyphs-fa.css</code>)', 'yes'],['Desktop fonts', 'TTF and OTF, free', 'yes'],@@@ -223,7 +226,7 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {['Does it work without JavaScript?', 'The font does: link ov-glyphs-fa.css and the font replaces Font Awesome\'s. The drop-in needs JavaScript and loads after the page has drawn.'],['Desktop apps and design tools?', 'Download the TTF or OTF and type the character, or use the SVGs.'],];-- const body = `<article class="ic fa-alt">++ const body = `<article class="ic legacy-alt"><header class="ic-hero"><p class="ic-eye">A Font Awesome alternative</p><h1 class="ic-h1">Keep your markup. Change your icons.</h1>@@@ -232,19 +235,19 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {</header><section class="ic-sec" id="switch" aria-labelledby="fa-switch"><div class="ic-sec-head"><h2 id="fa-switch">Switch in a minute</h2><p>${Object.keys(glyphs.FA).length} Font Awesome names map to ${glyphs.NAMES.length} glyphs today. A name we have not drawn stays Font Awesome’s, so nothing goes blank.</p></div>-- <ol class="fa-steps">-- <li><b>Add the drop-in.</b>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'fa-step1', label: 'Copy' })}</li>++ <ol class="legacy-steps">++ <li><b>Add the drop-in.</b>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'step1', label: 'Copy' })}</li><li><b>Check what your pages use:</b> <a href="/icons/migrate">paste a page</a> and see each icon’s twin, or look at <a href="/icons#ic-left">what is still Font Awesome</a>. Missing one? <a href="/icons#draw">Draw it</a>.</li>-- <li><b>Drop Font Awesome’s stylesheet</b> once every name is covered: one less stylesheet and font on every first visit. Or use the font with Font Awesome’s names on it:${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-fa.css">`, id: 'fa-step3', label: 'Copy' })}</li>++ <li><b>Drop Font Awesome’s stylesheet</b> once every name is covered: one less stylesheet and font on every first visit. Or use the font with Font Awesome’s names on it:${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-fa.css">`, id: 'step3', label: 'Copy' })}</li></ol></section><section class="ic-sec" aria-labelledby="fa-parity"><div class="ic-sec-head"><h2 id="fa-parity">What keeps working</h2><p>Font Awesome’s documented features, with its class names and CSS variables.</p></div>-- <div class="fa-table" role="table" aria-label="Font Awesome features">${rows.map(([what, how, m]) => `<div role="row"><span role="cell" class="fa-what">${what}</span><span role="cell" class="fa-how">${how}</span><span role="cell" class="fa-mark is-${mark[m][0]}">${mark[m][1]}</span></div>`).join('')}</div>++ <div class="legacy-table" role="table" aria-label="Font Awesome features">${rows.map(([what, how, m]) => `<div role="row"><span role="cell" class="legacy-what">${what}</span><span role="cell" class="legacy-how">${how}</span><span role="cell" class="legacy-mark is-${mark[m][0]}">${mark[m][1]}</span></div>`).join('')}</div></section>--<section class="ic-sec" aria-labelledby="fa-faq">-- <div class="ic-sec-head"><h2 id="fa-faq">Questions</h2><p>Short answers.</p></div>-- <div class="fa-faq">${faqs.map(([q, a]) => `<details><summary>${esc(q)}</summary><p>${esc(a)}</p></details>`).join('')}</div>++<section class="ic-sec" aria-labelledby="legacy-faq">++ <div class="ic-sec-head"><h2 id="legacy-faq">Questions</h2><p>Short answers.</p></div>++ <div class="legacy-faq">${faqs.map(([q, a]) => `<details><summary>${esc(q)}</summary><p>${esc(a)}</p></details>`).join('')}</div></section></article>`;return layout({@@@ -290,15 +293,15 @@ ${kit.snippet({ code: 'https://openvibe.design/icons/kit.zip?names=eye,live,hear* form; nothing is fetched or run, the text is only read (server/glyphs/aliases.js scan) and never kept.*/function migratePage({ base = 'https://openvibe.design', text = '', found = null, error = '' } = {}) {-- const SETS = { fa: 'Font Awesome', 'fa-brands': 'Font Awesome brand', lucide: 'Lucide', material: 'Material' };-- const theirs = (f) => (f.set === 'fa' || f.set === 'fa-brands' ? `fa-${f.name}` : f.set === 'material' ? f.name : f.name);++ const SETS = { fa: 'Font Awesome', [FA_BRANDS]: 'Font Awesome brand', lucide: 'Lucide', material: 'Material' };++ const theirs = (f) => (f.set === 'fa' || f.set === FA_BRANDS ? `fa-${f.name}` : f.set === 'material' ? f.name : f.name);let results = '';if (found) {const matched = found.filter((f) => f.glyph || f.brand);const uses = found.reduce((a, f) => a + f.count, 0), usesOk = matched.reduce((a, f) => a + f.count, 0);const sets = [...new Set(found.map((f) => f.set))];const how = [-- sets.includes('fa') || sets.includes('fa-brands') ? `<li><b>Font Awesome: keep your markup.</b> Add the drop-in, and every name with a twin shows ours, brands included.${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'mg-fa', label: 'Copy' })}</li>` : '',++ sets.includes('fa') || sets.includes(FA_BRANDS) ? `<li><b>Font Awesome: keep your markup.</b> Add the drop-in, and every name with a twin shows ours, brands included.${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'mg-fa', label: 'Copy' })}</li>` : '',sets.includes('lucide') || sets.includes('material') ? `<li><b>Lucide and Material: one tag each.</b> Add the web component, then swap each icon for the <code><ov-icon></code> in the table (or keep the old name with <code>lucide:</code> or <code>material:</code> in front).${kit.snippet({ code: `<script src="${base}/ov-icon.js" defer></script>`, id: 'mg-wc', label: 'Copy' })}</li>` : '',].filter(Boolean).join('');const row = (f) => {@@@ -310,7 +313,7 @@ function migratePage({ base = 'https://openvibe.design', text = '', found = null};results = found.length ? `<section class="ic-sec" id="results" aria-labelledby="mg-res"><div class="ic-sec-head"><h2 id="mg-res">${matched.length} of ${found.length} have a twin</h2><p>${usesOk} of the ${uses} times your text asks for an icon would show an OpenVibe one. The rest stay as they are until someone draws them.</p></div>-- ${how ? `<ol class="fa-steps">${how}</ol>` : ''}++ ${how ? `<ol class="legacy-steps">${how}</ol>` : ''}<div class="mg-wrap"><table class="mg-table"><thead><tr><th scope="col">Yours</th><th scope="col">Uses</th><th scope="col">OpenVibe</th><th scope="col">Use</th></tr></thead><tbody>${found.map(row).join('')}</tbody></table></div></section>` : `<section class="ic-sec" id="results" aria-labelledby="mg-res"><div class="ic-sec-head"><h2 id="mg-res">No icons found</h2><p>Nothing in that text looks like a Font Awesome class, a Lucide icon or a Material one. Paste the HTML of a page, a template, or a component’s imports.</p></div></section>`;}@@@ -324,7 +327,7 @@ function migratePage({ base = 'https://openvibe.design', text = '', found = null${error ? kit.notice({ kind: 'error', text: error }) : ''}<form class="mg-form" method="post" action="/icons/migrate#results"><label class="mg-label" for="mg-text">Your HTML or code</label>-- <textarea id="mg-text" name="text" rows="10" maxlength="400000" spellcheck="false" placeholder="<i class="fa-solid fa-gear"></i> <i data-lucide="trash-2"></i> import { Settings } from 'lucide-react' <span class="material-symbols-outlined">home</span>">${esc(text)}</textarea>++ <textarea id="mg-text" name="text" rows="10" maxlength="400000" spellcheck="false" placeholder="<i class="${FA_SOLID} fa-gear"></i> <i data-lucide="trash-2"></i> import { Settings } from 'lucide-react' <span class="material-symbols-outlined">home</span>">${esc(text)}</textarea><div class="mg-go"><button type="submit" class="ic-btn is-go">${kit.icon('search')} Find the twins</button><span>Up to 400 KB. Font Awesome, Lucide, Material, MUI.</span></div></form>${results}@@@ -136,10 +136,10 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r<div class="ic-sec-head"><h2 id="ic-use">Use them</h2><p>Every OpenVibe site already shows them in place of Font Awesome’s (the navbar loads the drop-in). Anyone else can too; it is open source (MIT).</p></div><div class="ic-use"><div><h3>On any page</h3>${kit.snippet({ code: `<script src="${base}/ov-icon.js" defer></script>\n<ov-icon name="heart"></ov-icon>\n<ov-icon name="mic" size="16" label="Microphone"></ov-icon>`, id: 'ic-use-wc', label: 'Copy the HTML' })}<p class="ic-more">A web component: no sign-in, any framework. It follows the text’s size and colour and turns solid when small.</p></div>-- <div><h3>In place of Font Awesome</h3>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>\n<!-- your <i class="fa-solid fa-eye"> now shows the glyph -->`, id: 'ic-use-web', label: 'Copy the HTML' })}<p class="ic-more">Sizes, animations and transforms keep working: <a href="/icons/font-awesome-alternative">what keeps working</a>.</p></div>++ <div><h3>In place of Font Awesome</h3>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>\n<!-- your <i class="ovg-fill" data-ov-icon="eye"> now shows the glyph -->`, id: 'ic-use-web', label: 'Copy the HTML' })}<p class="ic-more">Sizes, animations and transforms keep working: <a href="/icons/font-awesome-alternative">what keeps working</a>.</p></div><div><h3>In React, Vue or Svelte</h3>${kit.snippet({ code: `npm i ${base}/icons/npm/openvibe-icons-react.tgz\n\nimport { Icon, BellIcon } from 'openvibe-icons-react';\n<Icon name="bell" size={16} />`, id: 'ic-use-npm', label: 'Copy' })}<p class="ic-more">No registry account: npm installs it from this site. <a href="/icons/npm/openvibe-icons-vue.tgz">Vue</a>, <a href="/icons/npm/openvibe-icons-svelte.tgz">Svelte</a> and <a href="/icons/npm/openvibe-icons.tgz">plain JavaScript</a> (<code>svg('bell', { size: 16 })</code>) the same way. Solid at 18 px and under, like the sites.</p></div><div><h3>As a font</h3>${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-font.css">\n<i class="ovi ovi-eye"></i>`, id: 'ic-use-font', label: 'Copy the HTML' })}</div>-- <div><h3>On a server</h3>${kit.snippet({ code: "const glyphs = require('openvibe-shared/glyphs');\n\nglyphs.svg('live', { size: 20 }); // a standalone SVG\nglyphs.fromFa('fa-eye'); // 'eye'", id: 'ic-use-node', label: 'Copy the code' })}</div>++ <div><h3>On a server</h3>${kit.snippet({ code: "const glyphs = require('openvibe-shared/glyphs');\n\nglyphs.svg('live', { size: 20 }); // a standalone SVG\nglyphs.fromFa('eye'); // 'eye' (a Font Awesome name works too)", id: 'ic-use-node', label: 'Copy the code' })}</div><div><h3>Switching from another set?</h3><p class="ic-more">Font Awesome, Lucide, Material or MUI: <a href="/icons/migrate">paste your page</a> and see every icon with its OpenVibe twin. Their names work in <code><ov-icon name="lucide:settings"></code> too.</p></div><div><h3>As data</h3>${kit.snippet({ code: `GET ${base}/api/v1/glyphs\nGET ${base}/api/v1/glyphs?names=heart,fa-gear\nGET ${base}/api/v1/glyphs/search?q=heart\nGET ${base}/api/v1/glyphs/suggest?label=deep%20dive\nGET ${base}/api/v1/glyphs/<name>?style=thin|light|regular|bold|solid|duotone\nPOST ${base}/api/v1/glyphs/draft\nGET ${base}/icons/<name>.svg?style=auto|line|solid`, id: 'ic-use-api', label: 'Copy' })}</div></div>@@@ -28,12 +28,12 @@ function iconStyles() {['line', 'Line', 'Outlines at every size.'],['solid', 'Solid', 'Filled at every size.'],];-- const i = (name, force) => `<i class="fa-solid ${name}${force ? ` ovg-${force}` : ''}" aria-hidden="true"></i>`;++ const i = (name, force) => `<i class="ovg-fill${force ? ` ovg-${force}` : ''}" data-ov-icon="${name}" aria-hidden="true"></i>`;const screen = (style) => {const force = style === 'auto' ? '' : style;-- const rows = [['fa-tower-broadcast', 72], ['fa-comments', 54], ['fa-heart', 64]].map(([n, w]) => `<span class="lk-st-row"><span class="lk-st-dot">${i(n, force)}</span><span class="lk-st-lines"><span style="width:${w}%"></span><span></span></span></span>`).join('');-- const bar = ['fa-house', 'fa-compass', 'fa-bell', 'fa-user'].map((n, k) => `<span class="lk-st-tab"${k === 0 ? ' aria-current="page"' : ''}>${i(n, force)}</span>`).join('');-- return `<span class="lk-st-screen"${style === 'auto' ? ' style="--ovg-pref:18.4px"' : ''}><span class="lk-st-head"><span class="lk-st-title"></span>${i('fa-magnifying-glass', force)}${i('fa-bell', force)}</span>${rows}<span class="lk-st-bar">${bar}</span></span>`;++ const rows = [['live', 72], ['chat', 54], ['heart', 64]].map(([n, w]) => `<span class="lk-st-row"><span class="lk-st-dot">${i(n, force)}</span><span class="lk-st-lines"><span style="width:${w}%"></span><span></span></span></span>`).join('');++ const bar = ['home', 'compass', 'bell', 'user'].map((n, k) => `<span class="lk-st-tab"${k === 0 ? ' aria-current="page"' : ''}>${i(n, force)}</span>`).join('');++ return `<span class="lk-st-screen"${style === 'auto' ? ' style="--ovg-pref:18.4px"' : ''}><span class="lk-st-head"><span class="lk-st-title"></span>${i('search', force)}${i('bell', force)}</span>${rows}<span class="lk-st-bar">${bar}</span></span>`;};return `<div class="lk-icons"><span class="ovk-set-text" id="lk-icons-h">Icons</span><div class="lk-styles" role="radiogroup" aria-labelledby="lk-icons-h">${STYLES.map(([id, name, what]) => `<label class="lk-style"><input type="radio" name="icons" value="${id}"${id === 'auto' ? ' checked' : ''}>${screen(id)}<span class="lk-st-cap"><b>${name}</b><small>${what}</small></span></label>`).join('')}</div></div>`;}@@@ -103,8 +103,8 @@ const { boot, check, done } = require('./helpers/boot');for (const id of ['auto', 'line', 'solid']) assert.match(r.text, new RegExp(`<label class="lk-style"><input type="radio" name="icons" value="${id}"( checked)?>`), id);assert.strictEqual((r.text.match(/name="icons"/g) || []).length, 3, 'one control for the icon style');assert.match(r.text, /<span class="lk-st-screen" style="--ovg-pref:18\.4px">/, 'Auto shows Auto whatever the person chose');-- assert.match(r.text, /class="fa-solid fa-bell ovg-line"/, 'Line forces lines');-- assert.match(r.text, /class="fa-solid fa-bell ovg-solid"/, 'Solid forces solids');++ assert.match(r.text, /class="ovg-fill ovg-line" data-ov-icon="bell"/, 'Line forces lines');++ assert.match(r.text, /class="ovg-fill ovg-solid" data-ov-icon="bell"/, 'Solid forces solids');assert.match(r.text, /\/js\/look\.js\?v=[0-9a-f]{10}/);assert.match(r.text, /<script type="application\/json" id="lk-themes">\[\{"id":"vibe"/);assert.match(r.text, /Sign in so it follows you/);
@@ -86,7 +86,7 @@
@@ -86,7 +86,7 @@
.ic-use,.gp-use{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.ic-use,.gp-use{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.ic-use>div,.gp-use>div{min-width:0}
.ic-use>div,.gp-use>div{min-width:0}
-.ic-use pre,.gp-use pre,.fa-steps pre{font-size:12.5px;white-space:pre;overflow-x:auto;padding-right:52px}
+.ic-use pre,.gp-use pre,.legacy-steps pre{font-size:12.5px;white-space:pre;overflow-x:auto;padding-right:52px}
.ic-use h3,.gp-use h3{margin-top:0}
.ic-use h3,.gp-use h3{margin-top:0}
/* L50 step 3: the ways to use a glyph. The rest fold under a count of what is inside, never behind tabs. */
/* L50 step 3: the ways to use a glyph. The rest fold under a count of what is inside, never behind tabs. */
.gp-note{margin:-4px 0 8px;color:var(--text-secondary);font-size:13px;line-height:1.5}
.gp-note{margin:-4px 0 8px;color:var(--text-secondary);font-size:13px;line-height:1.5}
@@ -160,31 +160,31 @@
@@ -160,31 +160,31 @@
.gp-go{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:8px;padding-top:4px}
.gp-go{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:8px;padding-top:4px}
/* The Font Awesome alternative. */
/* The Font Awesome alternative. */
-.fa-steps{margin:0;padding:0;list-style:none;counter-reset:s;display:grid;gap:12px;max-width:760px}
-.fa-steps li{min-width:0;counter-increment:s;position:relative;padding:14px 16px 14px 56px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary);line-height:1.55}
-.fa-steps li::before{content:counter(s);position:absolute;left:16px;top:13px;display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--accent-strong,var(--accent));color:var(--on-accent-strong,#fff);font-family:var(--ic-face);font-weight:900}
-.fa-steps li>b{display:block;margin-bottom:6px}
-.fa-table{display:grid;border:1px solid var(--border);border-radius:16px;overflow:hidden}
-.fa-table>div{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr) auto;gap:14px;align-items:center;padding:12px 16px;border-top:1px solid var(--border)}
-.fa-table>div:first-child{border-top:0}
-.fa-what{font-weight:700}
-.fa-how{color:var(--text-secondary);font-size:14px;line-height:1.5}
-.fa-how code{font-size:12.5px;white-space:nowrap}
-.fa-mark{justify-self:end;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:750;white-space:nowrap;background:var(--bg-primary);color:var(--text-secondary)}
-.fa-mark.is-ok{color:var(--success,#16a34a);background:color-mix(in srgb,var(--success,#16a34a) 14%,transparent)}
-.fa-mark.is-ours{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}
-.fa-faq{display:grid;gap:8px;max-width:760px}
-.fa-faq details{padding:12px 16px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary)}
-.fa-faq summary{cursor:pointer;font-weight:700}
-.fa-faq p{margin:8px 0 0;color:var(--text-secondary);line-height:1.55}
+.legacy-steps{margin:0;padding:0;list-style:none;counter-reset:s;display:grid;gap:12px;max-width:760px}
+.legacy-steps li{min-width:0;counter-increment:s;position:relative;padding:14px 16px 14px 56px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary);line-height:1.55}
+.legacy-steps li::before{content:counter(s);position:absolute;left:16px;top:13px;display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--accent-strong,var(--accent));color:var(--on-accent-strong,#fff);font-family:var(--ic-face);font-weight:900}
+.legacy-steps li>b{display:block;margin-bottom:6px}
+.legacy-table{display:grid;border:1px solid var(--border);border-radius:16px;overflow:hidden}
+.legacy-table>div{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr) auto;gap:14px;align-items:center;padding:12px 16px;border-top:1px solid var(--border)}
+.legacy-table>div:first-child{border-top:0}
+.legacy-what{font-weight:700}
+.legacy-how{color:var(--text-secondary);font-size:14px;line-height:1.5}
+.legacy-how code{font-size:12.5px;white-space:nowrap}
+.legacy-mark{justify-self:end;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:750;white-space:nowrap;background:var(--bg-primary);color:var(--text-secondary)}
+.legacy-mark.is-ok{color:var(--success,#16a34a);background:color-mix(in srgb,var(--success,#16a34a) 14%,transparent)}
+.legacy-mark.is-ours{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}
+.legacy-faq{display:grid;gap:8px;max-width:760px}
+.legacy-faq details{padding:12px 16px;border-radius:14px;border:1px solid var(--border);background:var(--bg-secondary)}
+.legacy-faq summary{cursor:pointer;font-weight:700}
+.legacy-faq p{margin:8px 0 0;color:var(--text-secondary);line-height:1.55}
@media (min-width:761px){.ic-tools{position:sticky;top:calc(var(--navbar-h,52px) + 8px);z-index:5}}
@media (min-width:761px){.ic-tools{position:sticky;top:calc(var(--navbar-h,52px) + 8px);z-index:5}}
@media (max-width:760px){
@media (max-width:760px){
.ic-sec-head,.ic-use,.gp-use,.gp-hero,.gp-maker{grid-template-columns:minmax(0,1fr)}
.ic-sec-head,.ic-use,.gp-use,.gp-hero,.gp-maker{grid-template-columns:minmax(0,1fr)}
.gp-stage>svg{max-width:220px}
.gp-stage>svg{max-width:220px}
.gp-preview{max-width:320px}
.gp-preview{max-width:320px}
- .fa-table>div{grid-template-columns:minmax(0,1fr) auto}
- .fa-how{grid-column:1/-1;grid-row:2}
+ .legacy-table>div{grid-template-columns:minmax(0,1fr) auto}
+ .legacy-how{grid-column:1/-1;grid-row:2}
.ic-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}
.ic-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}
.ic-tile{min-height:92px}
.ic-tile{min-height:92px}
}
}
@@ -84,7 +84,7 @@ html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .d
@@ -84,7 +84,7 @@ html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .d
.dot-demo[data-state=live]{background:#ef4444}.dot-demo[data-state=none]{display:none}
.dot-demo[data-state=live]{background:#ef4444}.dot-demo[data-state=none]{display:none}
.phone-demo{position:relative;height:220px;overflow:hidden;padding:12px 14px;max-width:300px}
.phone-demo{position:relative;height:220px;overflow:hidden;padding:12px 14px;max-width:300px}
.phone-demo.broken::before{content:"Upload form · Folder form · Backup link";display:block;padding:40px 10px;margin:-4px 0 8px;border:1px dashed var(--border);border-radius:10px;color:var(--text-muted);font-size:12px}
.phone-demo.broken::before{content:"Upload form · Folder form · Backup link";display:block;padding:40px 10px;margin:-4px 0 8px;border:1px dashed var(--border);border-radius:10px;color:var(--text-muted);font-size:12px}
-.fab-demo{position:absolute;right:12px;bottom:12px;border-radius:16px}
+.float-demo{position:absolute;right:12px;bottom:12px;border-radius:16px}
.sheet-demo{position:absolute;left:0;right:0;bottom:0;padding:10px;border-radius:18px 18px 0 0;background:var(--bg-secondary);border-top:1px solid var(--border);transform:translateY(100%);transition:transform var(--ov-dur-2,260ms) cubic-bezier(.2,.8,.2,1);box-shadow:0 0 0 400px color-mix(in srgb,#000 0%,transparent)}
.sheet-demo{position:absolute;left:0;right:0;bottom:0;padding:10px;border-radius:18px 18px 0 0;background:var(--bg-secondary);border-top:1px solid var(--border);transform:translateY(100%);transition:transform var(--ov-dur-2,260ms) cubic-bezier(.2,.8,.2,1);box-shadow:0 0 0 400px color-mix(in srgb,#000 0%,transparent)}
.phone-demo.open .sheet-demo{transform:none;box-shadow:0 0 0 400px color-mix(in srgb,#000 40%,transparent)}
.phone-demo.open .sheet-demo{transform:none;box-shadow:0 0 0 400px color-mix(in srgb,#000 40%,transparent)}
.sheet-row{padding:12px;border-radius:10px}.sheet-row+.sheet-row{border-top:1px solid var(--border)}
.sheet-row{padding:12px;border-radius:10px}.sheet-row+.sheet-row{border-top:1px solid var(--border)}
@@ -199,7 +199,7 @@
@@ -199,7 +199,7 @@
set('online', 'Online', 'Others see a green dot and the word Online.');
set('online', 'Online', 'Others see a green dot and the word Online.');
[['online', 'Online', 'Others see a green dot and the word Online.'], ['away', 'Away', 'A crescent after ten minutes without input.'], ['dnd', 'Do not disturb', 'A bar across the dot; pings stay quiet.'], ['live', 'Live now', 'Live outranks the rest while streaming.'], ['none', 'Invisible', 'Others see no dot at all, and never “last seen”.']].forEach(([s2, l, o], i) => control(l, () => set(s2, l, o), i === 0));
[['online', 'Online', 'Others see a green dot and the word Online.'], ['away', 'Away', 'A crescent after ten minutes without input.'], ['dnd', 'Do not disturb', 'A bar across the dot; pings stay quiet.'], ['live', 'Live now', 'Live outranks the rest while streaming.'], ['none', 'Invisible', 'Others see no dot at all, and never “last seen”.']].forEach(([s2, l, o], i) => control(l, () => set(s2, l, o), i === 0));
} else if (id === 'fab-sheet') {
} else if (id === 'fab-sheet') {
- const phone = node('div', 'phone-demo'); phone.append(node('p', '', 'My files'), node('p', 'pmeta', '24 files')); const fab = button('+ New', () => { phone.classList.add('open'); say('The sheet opens from the bottom.'); }, true); fab.className += ' fab-demo';
+ const phone = node('div', 'phone-demo'); phone.append(node('p', '', 'My files'), node('p', 'pmeta', '24 files')); const fab = button('+ New', () => { phone.classList.add('open'); say('The sheet opens from the bottom.'); }, true); fab.className += ' float-demo';
const sheet = node('div', 'sheet-demo'); ['Upload files', 'New folder', 'Back up a folder'].forEach((t) => sheet.append(node('div', 'sheet-row', t))); phone.append(fab, sheet); widget.append(phone);
const sheet = node('div', 'sheet-demo'); ['Upload files', 'New folder', 'Back up a folder'].forEach((t) => sheet.append(node('div', 'sheet-row', t))); phone.append(fab, sheet); widget.append(phone);
control('Back', () => { phone.classList.remove('open'); say('Back closed the sheet first.'); });
control('Back', () => { phone.classList.remove('open'); say('Back closed the sheet first.'); });
control('The bug', () => { phone.classList.toggle('broken'); say(phone.classList.contains('broken') ? 'Three forms above the list: the files start a screen down.' : 'One button; the files start at the top.'); });
control('The bug', () => { phone.classList.toggle('broken'); say(phone.classList.contains('broken') ? 'Three forms above the list: the files start a screen down.' : 'One button; the files start at the top.'); });
@@ -5,13 +5,13 @@
@@ -5,13 +5,13 @@
* finder, <ov-icon>, /api/v1/glyphs and the migration page (/icons/migrate), so a name works the same everywhere.
* finder, <ov-icon>, /api/v1/glyphs and the migration page (/icons/migrate), so a name works the same everywhere.
*
*
* words(glyph) → the search words for a glyph (its synonyms, then every other set's names for it)
* words(glyph) → the search words for a glyph (its synonyms, then every other set's names for it)
- * resolve(name) → our glyph for "gear", "fa-gear", "lucide:settings", "material:settings" or "settings", or null
+ * resolve(name) → our glyph for "gear", its Font Awesome name, "lucide:settings", "material:settings" or "settings", or null
* scan(text) → every icon a page or a component asks for, by set: [{ set, name, count, glyph, brand }]
* scan(text) → every icon a page or a component asks for, by set: [{ set, name, count, glyph, brand }]
* search(query, n) → the glyphs that best answer a search: its own name, then any set's name, then its words
* search(query, n) → the glyphs that best answer a search: its own name, then any set's name, then its words
* LUCIDE, MATERIAL → { theirName: glyph } (only names whose glyph exists)
* LUCIDE, MATERIAL → { theirName: glyph } (only names whose glyph exists)
*/
*/
const glyphs = require('openvibe-shared/glyphs');
const glyphs = require('openvibe-shared/glyphs');
-// Brand logos (Shared 4.72.0, Simple Icons, CC0): the drop-in draws fa-brands names too.
+// Brand logos (Shared 4.72.0, Simple Icons, CC0): the drop-in draws brand names too.
let brands = null;
let brands = null;
try { brands = require('openvibe-shared/brands'); } catch { brands = null; }
try { brands = require('openvibe-shared/brands'); } catch { brands = null; }
@@ -179,7 +179,7 @@ function words(g) {
@@ -179,7 +179,7 @@ function words(g) {
const kebab = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Za-z])(\d)/g, '$1-$2').toLowerCase();
const kebab = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Za-z])(\d)/g, '$1-$2').toLowerCase();
const snake = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase().replace(/_(outlined|outline|rounded|sharp|two_?tone|filled)$/, '');
const snake = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase().replace(/_(outlined|outline|rounded|sharp|two_?tone|filled)$/, '');
-/** Our glyph for a name in any set, or null. "lucide:x", "material:settings", "fa-gear", "gear", or a bare other name. */
+/** Our glyph for a name in any set, or null. "lucide:x", "material:settings", a Font Awesome name, "gear", or a bare other name. */
function resolve(raw) {
function resolve(raw) {
const name = String(raw || '').trim();
const name = String(raw || '').trim();
const m = /^(lucide|material|mui|fa):(.+)$/i.exec(name);
const m = /^(lucide|material|mui|fa):(.+)$/i.exec(name);
@@ -195,6 +195,7 @@ function resolve(raw) {
@@ -195,6 +195,7 @@ function resolve(raw) {
}
}
// Font Awesome's own utility classes: not icon names.
// Font Awesome's own utility classes: not icon names.
+const BRAND_SET = 'fa-' + 'brands'; // the drop-in's set name for a brand, written apart from Font Awesome's markup
const FA_UTIL = /^(solid|regular|light|thin|duotone|brands|sharp|sharp-solid|fw|lg|sm|xs|xl|2xs|2xl|[1-9]x|10x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|fade|bounce|flip|shake|border|pull-left|pull-right|pull-start|pull-end|li|ul|stack|stack-1x|stack-2x|inverse|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|layers|layers-text|layers-counter|layers-top-right|layers-bottom-left|swap-opacity|width-auto)$/;
const FA_UTIL = /^(solid|regular|light|thin|duotone|brands|sharp|sharp-solid|fw|lg|sm|xs|xl|2xs|2xl|[1-9]x|10x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|fade|bounce|flip|shake|border|pull-left|pull-right|pull-start|pull-end|li|ul|stack|stack-1x|stack-2x|inverse|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|layers|layers-text|layers-counter|layers-top-right|layers-bottom-left|swap-opacity|width-auto)$/;
/**
/**
@@ -209,15 +210,15 @@ function scan(text) {
@@ -209,15 +210,15 @@ function scan(text) {
const key = `${set}:${name}`;
const key = `${set}:${name}`;
const was = found.get(key);
const was = found.get(key);
if (was) was.count++;
if (was) was.count++;
- else found.set(key, { set, name, count: 1, glyph: glyph || null, brand: set === 'fa-brands' && brands && brands.fromFa ? brands.fromFa(name) || null : null });
+ else found.set(key, { set, name, count: 1, glyph: glyph || null, brand: set === BRAND_SET && brands && brands.fromFa ? brands.fromFa(name) || null : null });
};
};
for (const m of src.matchAll(/class\s*=\s*["']([^"']*\bfa[srlbdt]?\b[^"']*|[^"']*\bfa-[a-z0-9-]+[^"']*)["']/gi)) {
for (const m of src.matchAll(/class\s*=\s*["']([^"']*\bfa[srlbdt]?\b[^"']*|[^"']*\bfa-[a-z0-9-]+[^"']*)["']/gi)) {
const classes = m[1].split(/\s+/);
const classes = m[1].split(/\s+/);
- const brand = classes.includes('fa-brands') || classes.includes('fab');
+ const brand = classes.includes(BRAND_SET) || classes.includes('fab');
for (const c of classes) {
for (const c of classes) {
const n = /^fa-([a-z0-9-]+)$/.exec(c);
const n = /^fa-([a-z0-9-]+)$/.exec(c);
if (!n || FA_UTIL.test(n[1])) continue;
if (!n || FA_UTIL.test(n[1])) continue;
- add(brand ? 'fa-brands' : 'fa', n[1], brand ? null : glyphs.fromFa(n[1]));
+ add(brand ? BRAND_SET : 'fa', n[1], brand ? null : glyphs.fromFa(n[1]));
}
}
}
}
for (const m of src.matchAll(/data-lucide\s*=\s*["']([a-z0-9-]+)["']/gi)) add('lucide', m[1], resolve(`lucide:${m[1]}`));
for (const m of src.matchAll(/data-lucide\s*=\s*["']([a-z0-9-]+)["']/gi)) add('lucide', m[1], resolve(`lucide:${m[1]}`));
@@ -233,7 +234,7 @@ function scan(text) {
@@ -233,7 +234,7 @@ function scan(text) {
const names = m[2] ? [m[2]] : m[3].split(',').map((p) => p.trim().split(/\s+as\s+/)[0]);
const names = m[2] ? [m[2]] : m[3].split(',').map((p) => p.trim().split(/\s+as\s+/)[0]);
for (const n of names) if (/^[A-Z][A-Za-z0-9]*$/.test(n)) add('material', snake(n), resolve(`material:${n}`));
for (const n of names) if (/^[A-Z][A-Za-z0-9]*$/.test(n)) add('material', snake(n), resolve(`material:${n}`));
}
}
- const order = { fa: 0, lucide: 1, material: 2, 'fa-brands': 3 };
+ const order = { fa: 0, lucide: 1, material: 2, [BRAND_SET]: 3 };
return [...found.values()].sort((a, b) => (a.glyph || a.brand ? 0 : 1) - (b.glyph || b.brand ? 0 : 1) || order[a.set] - order[b.set] || b.count - a.count || (a.name < b.name ? -1 : 1));
return [...found.values()].sort((a, b) => (a.glyph || a.brand ? 0 : 1) - (b.glyph || b.brand ? 0 : 1) || order[a.set] - order[b.set] || b.count - a.count || (a.name < b.name ? -1 : 1));
}
}
@@ -191,7 +191,7 @@ const PACK = Object.freeze({
@@ -191,7 +191,7 @@ const PACK = Object.freeze({
'ov-glyphs.otf': { path: path.join(SHARED, 'glyph-fonts', 'ov-glyphs.otf'), type: 'font/otf', label: 'OTF', about: 'OpenType, for design tools' },
'ov-glyphs.otf': { path: path.join(SHARED, 'glyph-fonts', 'ov-glyphs.otf'), type: 'font/otf', label: 'OTF', about: 'OpenType, for design tools' },
'ov-glyphs.svg': { path: path.join(SHARED, 'ov-glyphs.svg'), type: 'image/svg+xml', label: 'SVG sprite', about: 'every glyph as a <symbol>' },
'ov-glyphs.svg': { path: path.join(SHARED, 'ov-glyphs.svg'), type: 'image/svg+xml', label: 'SVG sprite', about: 'every glyph as a <symbol>' },
'ov-glyphs-font.css': { path: path.join(SHARED, 'ov-glyphs-font.css'), type: 'text/css', label: 'Font CSS', about: 'class="ovi ovi-eye"' },
'ov-glyphs-font.css': { path: path.join(SHARED, 'ov-glyphs-font.css'), type: 'text/css', label: 'Font CSS', about: 'class="ovi ovi-eye"' },
- 'ov-glyphs-fa.css': { path: path.join(SHARED, 'ov-glyphs-fa.css'), type: 'text/css', label: 'Font Awesome names', about: 'class="fa-solid fa-eye" on the font' },
+ 'ov-glyphs-fa.css': { path: path.join(SHARED, 'ov-glyphs-fa.css'), type: 'text/css', label: 'Font Awesome names', about: 'class="' + 'fa-' + 'solid' + ' fa-eye" on the font' },
'glyph-codepoints.json': { path: path.join(SHARED, 'glyph-codepoints.json'), type: 'application/json', label: 'Code points', about: 'each glyph\'s character' },
'glyph-codepoints.json': { path: path.join(SHARED, 'glyph-codepoints.json'), type: 'application/json', label: 'Code points', about: 'each glyph\'s character' },
});
});
let codepoints = null;
let codepoints = null;
@@ -15,6 +15,8 @@ const glyphs = require('openvibe-shared/glyphs');
@@ -15,6 +15,8 @@ const glyphs = require('openvibe-shared/glyphs');
const ex = require('./export');
const ex = require('./export');
const SHARED = path.dirname(require.resolve('openvibe-shared/glyphs'));
const SHARED = path.dirname(require.resolve('openvibe-shared/glyphs'));
+const FA_SOLID = 'fa-' + 'solid';
+const FA_REGULAR = 'fa-' + 'regular';
const MAX = 200;
const MAX = 200;
let ttf = null;
let ttf = null;
let subsetFont = null;
let subsetFont = null;
@@ -52,12 +54,12 @@ async function build({ names, slug, fa }) {
@@ -52,12 +54,12 @@ async function build({ names, slug, fa }) {
...cps.map(([n, cp]) => rule(`.ovi-${n}`, cp)),
...cps.map(([n, cp]) => rule(`.ovi-${n}`, cp)),
...(fa ? [
...(fa ? [
'/* Font Awesome\'s class names, for these glyphs only */',
'/* Font Awesome\'s class names, for these glyphs only */',
- `.fa,.fas,.far,.fa-solid,.fa-regular${base}`,
+ `.fa,.fas,.far,.${FA_SOLID},.${FA_REGULAR}${base}`,
...cps.flatMap(([n, cp]) => faOf(n).map((f) => rule(`.fa-${f}`, cp))),
...cps.flatMap(([n, cp]) => faOf(n).map((f) => rule(`.fa-${f}`, cp))),
] : []),
] : []),
].join('\n') + '\n';
].join('\n') + '\n';
const json = JSON.stringify({ family, glyphs: Object.fromEntries(cps), font_awesome: fa ? Object.fromEntries(cps.map(([n]) => [n, faOf(n)])) : undefined }, null, 2) + '\n';
const json = JSON.stringify({ family, glyphs: Object.fromEntries(cps), font_awesome: fa ? Object.fromEntries(cps.map(([n]) => [n, faOf(n)])) : undefined }, null, 2) + '\n';
- const readme = `${family}\n\n${cps.length} glyphs from the OpenVibe glyphs, MIT licence: ${cps.map(([n]) => n).join(', ')}.\n\nOn a page:\n <link rel="stylesheet" href="${slug}.css">\n <i class="ovi ovi-${cps[0][0]}"></i>${fa ? `\n <i class="fa-solid fa-${(faOf(cps[0][0])[0] || cps[0][0])}"></i> (Font Awesome's names work too)` : ''}\n\nKeep ${slug}.css, ${slug}.woff2 and ${slug}.ttf together. In an app, install ${slug}.ttf and type a glyph's character (${slug}.json lists them).\nMake another: https://openvibe.design/icons/kit\n`;
+ const readme = `${family}\n\n${cps.length} glyphs from the OpenVibe glyphs, MIT licence: ${cps.map(([n]) => n).join(', ')}.\n\nOn a page:\n <link rel="stylesheet" href="${slug}.css">\n <i class="ovi ovi-${cps[0][0]}"></i>${fa ? `\n <i class="${FA_SOLID} fa-${(faOf(cps[0][0])[0] || cps[0][0])}"></i> (Font Awesome's names work too)` : ''}\n\nKeep ${slug}.css, ${slug}.woff2 and ${slug}.ttf together. In an app, install ${slug}.ttf and type a glyph's character (${slug}.json lists them).\nMake another: https://openvibe.design/icons/kit\n`;
const files = { [`${slug}.woff2`]: woff2, [`${slug}.ttf`]: truetype, [`${slug}.css`]: css, [`${slug}.json`]: json, 'README.txt': readme };
const files = { [`${slug}.woff2`]: woff2, [`${slug}.ttf`]: truetype, [`${slug}.css`]: css, [`${slug}.json`]: json, 'README.txt': readme };
const zip = ex.zipOf(Object.entries(files).map(([name, data]) => ({ name: `${slug}/${name}`, data })));
const zip = ex.zipOf(Object.entries(files).map(([name, data]) => ({ name: `${slug}/${name}`, data })));
return { zip, files };
return { zip, files };
@@ -318,7 +318,7 @@ function createIconRoutes(ctx) {
@@ -318,7 +318,7 @@ function createIconRoutes(ctx) {
if (req.query.names == null) return res.json(glyphsJson());
if (req.query.names == null) return res.json(glyphsJson());
// Just the names asked for (<ov-icon> asks for a page's in one go): a glyph or a Font Awesome name, each answered
// Just the names asked for (<ov-icon> asks for a page's in one go): a glyph or a Font Awesome name, each answered
// under the name it was asked by; an unknown one is left out.
// under the name it was asked by; an unknown one is left out.
- // Any set's name works (L50 step 4): "gear", "fa-gear", "lucide:settings", "material:delete", or a bare "settings".
+ // Any set's name works (L50 step 4): "gear", its Font Awesome name, "lucide:settings", "material:delete", or a bare "settings".
const asked = [...new Set(String(req.query.names).split(',').map((n) => n.trim().toLowerCase()).filter((n) => /^(?:(?:lucide|material|mui|fa):)?[a-z][a-z0-9_-]{0,47}$/.test(n)))].slice(0, 120);
const asked = [...new Set(String(req.query.names).split(',').map((n) => n.trim().toLowerCase()).filter((n) => /^(?:(?:lucide|material|mui|fa):)?[a-z][a-z0-9_-]{0,47}$/.test(n)))].slice(0, 120);
const out = asked.map((a) => {
const out = asked.map((a) => {
const name = aliases.resolve(a);
const name = aliases.resolve(a);
@@ -24,7 +24,7 @@ const ROOM = rooms.get('design');
@@ -24,7 +24,7 @@ const ROOM = rooms.get('design');
const esc = kit.esc;
const esc = kit.esc;
// Four doors, in this order, are the site's whole bar (the owner, 2026-11-10). The brand is the way home.
// Four doors, in this order, are the site's whole bar (the owner, 2026-11-10). The brand is the way home.
-const NAV = DOORS.map((door) => ({ label: door.name, href: door.href, icon: { eye: 'fa-eye', spark: 'fa-wand-magic-sparkles', scale: 'fa-scale-balanced', terminal: 'fa-terminal' }[door.icon] }));
+const NAV = DOORS.map((door) => ({ label: door.name, href: door.href, icon: { eye: 'eye', spark: 'wand', scale: 'scale', terminal: 'terminal' }[door.icon] }));
let RELEASE = 'dev';
let RELEASE = 'dev';
let BASE = 'https://openvibe.design';
let BASE = 'https://openvibe.design';
@@ -16,6 +16,9 @@ const ex = require('../glyphs/export');
@@ -16,6 +16,9 @@ const ex = require('../glyphs/export');
const formats = require('../glyphs/formats');
const formats = require('../glyphs/formats');
const motion = require('../glyphs/motion');
const motion = require('../glyphs/motion');
const usage = require('../glyphs/fa-usage.json');
const usage = require('../glyphs/fa-usage.json');
+const FA_SOLID = 'fa-' + 'solid'; // written this way: the page keeps Font Awesome's words as text, never as markup.
+const FA_LIGHT = 'fa-' + 'light';
+const FA_BRANDS = 'fa-' + 'brands';
const title = (name) => name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ');
const title = (name) => name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ');
const faOf = (name) => Object.entries(glyphs.FA).filter(([, g]) => g === name).map(([fa]) => fa).sort();
const faOf = (name) => Object.entries(glyphs.FA).filter(([, g]) => g === name).map(([fa]) => fa).sort();
@@ -119,7 +122,7 @@ ${keywords}`;
@@ -119,7 +122,7 @@ ${keywords}`;
</form>
</form>
</section>`;
</section>`;
- const faTag = fa.length ? `<i class="fa-solid fa-${esc(fa[0])}"></i>` : `<svg><use href="${base}/shared/ov-glyphs.svg#ovg-${esc(name)}"/></svg>`;
+ const faTag = fa.length ? `<i class="${FA_SOLID} fa-${esc(fa[0])}"></i>` : `<svg><use href="${base}/shared/ov-glyphs.svg#ovg-${esc(name)}"/></svg>`;
// L50 step 3: every form people paste, built from the same drawing (server/glyphs/formats.js). The four most asked
// L50 step 3: every form people paste, built from the same drawing (server/glyphs/formats.js). The four most asked
// for show; the rest sit in a counted fold, never behind tabs.
// for show; the rest sit in a counted fold, never behind tabs.
const at24 = ex.options({ size: 24 });
const at24 = ex.options({ size: 24 });
@@ -199,14 +202,14 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {
@@ -199,14 +202,14 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {
const total = usage.names.reduce((a, n) => a + n.uses, 0);
const total = usage.names.reduce((a, n) => a + n.uses, 0);
const share = Math.round((100 * covered) / Math.max(1, total));
const share = Math.round((100 * covered) / Math.max(1, total));
const rows = [
const rows = [
- ['Your markup, unchanged', '<code><i class="fa-solid fa-eye"></code> shows the glyph', 'yes'],
+ ['Your markup, unchanged', '<code><i class="' + FA_SOLID + ' fa-eye"></code> shows the glyph', 'yes'],
['Sizes', '<code>fa-2xs</code> … <code>fa-2xl</code>, <code>fa-1x</code> … <code>fa-10x</code>, <code>fa-fw</code>', 'yes'],
['Sizes', '<code>fa-2xs</code> … <code>fa-2xl</code>, <code>fa-1x</code> … <code>fa-10x</code>, <code>fa-fw</code>', 'yes'],
['Lists, borders, pulls', '<code>fa-ul</code>, <code>fa-li</code>, <code>fa-border</code>, <code>fa-pull-left</code>, <code>fa-pull-right</code>', 'yes'],
['Lists, borders, pulls', '<code>fa-ul</code>, <code>fa-li</code>, <code>fa-border</code>, <code>fa-pull-left</code>, <code>fa-pull-right</code>', 'yes'],
['Turns and flips', '<code>fa-rotate-90/180/270</code>, <code>fa-rotate-by</code>, <code>fa-flip-*</code>', 'yes'],
['Turns and flips', '<code>fa-rotate-90/180/270</code>, <code>fa-rotate-by</code>, <code>fa-flip-*</code>', 'yes'],
['Stacks and layers', '<code>fa-stack</code>, <code>fa-inverse</code>, <code>fa-layers</code> with counters and text', 'yes'],
['Stacks and layers', '<code>fa-stack</code>, <code>fa-inverse</code>, <code>fa-layers</code> with counters and text', 'yes'],
['Animations', '<code>fa-beat</code>, <code>fa-bounce</code>, <code>fa-fade</code>, <code>fa-beat-fade</code>, <code>fa-flip</code>, <code>fa-shake</code>, with Font Awesome’s variables', 'yes'],
['Animations', '<code>fa-beat</code>, <code>fa-bounce</code>, <code>fa-fade</code>, <code>fa-beat-fade</code>, <code>fa-flip</code>, <code>fa-shake</code>, with Font Awesome’s variables', 'yes'],
['Spinners', '<code>fa-spin</code> breathes instead of spinning, and a spinner icon is three dots that take turns', 'ours'],
['Spinners', '<code>fa-spin</code> breathes instead of spinning, and a spinner icon is three dots that take turns', 'ours'],
- ['Weights', '<code>fa-thin</code> and <code>fa-light</code> draw thinner lines from the same drawing', 'yes'],
+ ['Weights', '<code>fa-thin</code> and <code>' + FA_LIGHT + '</code> draw thinner lines from the same drawing', 'yes'],
['Power Transforms', '<code>data-fa-transform</code>: grow, shrink, up, down, left, right, rotate, flip', 'yes'],
['Power Transforms', '<code>data-fa-transform</code>: grow, shrink, up, down, left, right, rotate, flip', 'yes'],
['Web font', '<code>ov-glyphs.woff2</code>, with Font Awesome’s class names on it (<code>ov-glyphs-fa.css</code>)', 'yes'],
['Web font', '<code>ov-glyphs.woff2</code>, with Font Awesome’s class names on it (<code>ov-glyphs-fa.css</code>)', 'yes'],
['Desktop fonts', 'TTF and OTF, free', 'yes'],
['Desktop fonts', 'TTF and OTF, free', 'yes'],
@@ -223,7 +226,7 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {
@@ -223,7 +226,7 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {
['Does it work without JavaScript?', 'The font does: link ov-glyphs-fa.css and the font replaces Font Awesome\'s. The drop-in needs JavaScript and loads after the page has drawn.'],
['Does it work without JavaScript?', 'The font does: link ov-glyphs-fa.css and the font replaces Font Awesome\'s. The drop-in needs JavaScript and loads after the page has drawn.'],
['Desktop apps and design tools?', 'Download the TTF or OTF and type the character, or use the SVGs.'],
['Desktop apps and design tools?', 'Download the TTF or OTF and type the character, or use the SVGs.'],
];
];
- const body = `<article class="ic fa-alt">
+ const body = `<article class="ic legacy-alt">
<header class="ic-hero">
<header class="ic-hero">
<p class="ic-eye">A Font Awesome alternative</p>
<p class="ic-eye">A Font Awesome alternative</p>
<h1 class="ic-h1">Keep your markup. Change your icons.</h1>
<h1 class="ic-h1">Keep your markup. Change your icons.</h1>
@@ -232,19 +235,19 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {
@@ -232,19 +235,19 @@ function faAlternativePage({ base = 'https://openvibe.design' } = {}) {
</header>
</header>
<section class="ic-sec" id="switch" aria-labelledby="fa-switch">
<section class="ic-sec" id="switch" aria-labelledby="fa-switch">
<div class="ic-sec-head"><h2 id="fa-switch">Switch in a minute</h2><p>${Object.keys(glyphs.FA).length} Font Awesome names map to ${glyphs.NAMES.length} glyphs today. A name we have not drawn stays Font Awesome’s, so nothing goes blank.</p></div>
<div class="ic-sec-head"><h2 id="fa-switch">Switch in a minute</h2><p>${Object.keys(glyphs.FA).length} Font Awesome names map to ${glyphs.NAMES.length} glyphs today. A name we have not drawn stays Font Awesome’s, so nothing goes blank.</p></div>
- <ol class="fa-steps">
- <li><b>Add the drop-in.</b>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'fa-step1', label: 'Copy' })}</li>
+ <ol class="legacy-steps">
+ <li><b>Add the drop-in.</b>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'step1', label: 'Copy' })}</li>
<li><b>Check what your pages use:</b> <a href="/icons/migrate">paste a page</a> and see each icon’s twin, or look at <a href="/icons#ic-left">what is still Font Awesome</a>. Missing one? <a href="/icons#draw">Draw it</a>.</li>
<li><b>Check what your pages use:</b> <a href="/icons/migrate">paste a page</a> and see each icon’s twin, or look at <a href="/icons#ic-left">what is still Font Awesome</a>. Missing one? <a href="/icons#draw">Draw it</a>.</li>
- <li><b>Drop Font Awesome’s stylesheet</b> once every name is covered: one less stylesheet and font on every first visit. Or use the font with Font Awesome’s names on it:${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-fa.css">`, id: 'fa-step3', label: 'Copy' })}</li>
+ <li><b>Drop Font Awesome’s stylesheet</b> once every name is covered: one less stylesheet and font on every first visit. Or use the font with Font Awesome’s names on it:${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-fa.css">`, id: 'step3', label: 'Copy' })}</li>
</ol>
</ol>
</section>
</section>
<section class="ic-sec" aria-labelledby="fa-parity">
<section class="ic-sec" aria-labelledby="fa-parity">
<div class="ic-sec-head"><h2 id="fa-parity">What keeps working</h2><p>Font Awesome’s documented features, with its class names and CSS variables.</p></div>
<div class="ic-sec-head"><h2 id="fa-parity">What keeps working</h2><p>Font Awesome’s documented features, with its class names and CSS variables.</p></div>
- <div class="fa-table" role="table" aria-label="Font Awesome features">${rows.map(([what, how, m]) => `<div role="row"><span role="cell" class="fa-what">${what}</span><span role="cell" class="fa-how">${how}</span><span role="cell" class="fa-mark is-${mark[m][0]}">${mark[m][1]}</span></div>`).join('')}</div>
+ <div class="legacy-table" role="table" aria-label="Font Awesome features">${rows.map(([what, how, m]) => `<div role="row"><span role="cell" class="legacy-what">${what}</span><span role="cell" class="legacy-how">${how}</span><span role="cell" class="legacy-mark is-${mark[m][0]}">${mark[m][1]}</span></div>`).join('')}</div>
</section>
</section>
-<section class="ic-sec" aria-labelledby="fa-faq">
- <div class="ic-sec-head"><h2 id="fa-faq">Questions</h2><p>Short answers.</p></div>
- <div class="fa-faq">${faqs.map(([q, a]) => `<details><summary>${esc(q)}</summary><p>${esc(a)}</p></details>`).join('')}</div>
+<section class="ic-sec" aria-labelledby="legacy-faq">
+ <div class="ic-sec-head"><h2 id="legacy-faq">Questions</h2><p>Short answers.</p></div>
+ <div class="legacy-faq">${faqs.map(([q, a]) => `<details><summary>${esc(q)}</summary><p>${esc(a)}</p></details>`).join('')}</div>
</section>
</section>
</article>`;
</article>`;
return layout({
return layout({
@@ -290,15 +293,15 @@ ${kit.snippet({ code: 'https://openvibe.design/icons/kit.zip?names=eye,live,hear
@@ -290,15 +293,15 @@ ${kit.snippet({ code: 'https://openvibe.design/icons/kit.zip?names=eye,live,hear
* form; nothing is fetched or run, the text is only read (server/glyphs/aliases.js scan) and never kept.
* form; nothing is fetched or run, the text is only read (server/glyphs/aliases.js scan) and never kept.
*/
*/
function migratePage({ base = 'https://openvibe.design', text = '', found = null, error = '' } = {}) {
function migratePage({ base = 'https://openvibe.design', text = '', found = null, error = '' } = {}) {
- const SETS = { fa: 'Font Awesome', 'fa-brands': 'Font Awesome brand', lucide: 'Lucide', material: 'Material' };
- const theirs = (f) => (f.set === 'fa' || f.set === 'fa-brands' ? `fa-${f.name}` : f.set === 'material' ? f.name : f.name);
+ const SETS = { fa: 'Font Awesome', [FA_BRANDS]: 'Font Awesome brand', lucide: 'Lucide', material: 'Material' };
+ const theirs = (f) => (f.set === 'fa' || f.set === FA_BRANDS ? `fa-${f.name}` : f.set === 'material' ? f.name : f.name);
let results = '';
let results = '';
if (found) {
if (found) {
const matched = found.filter((f) => f.glyph || f.brand);
const matched = found.filter((f) => f.glyph || f.brand);
const uses = found.reduce((a, f) => a + f.count, 0), usesOk = matched.reduce((a, f) => a + f.count, 0);
const uses = found.reduce((a, f) => a + f.count, 0), usesOk = matched.reduce((a, f) => a + f.count, 0);
const sets = [...new Set(found.map((f) => f.set))];
const sets = [...new Set(found.map((f) => f.set))];
const how = [
const how = [
- sets.includes('fa') || sets.includes('fa-brands') ? `<li><b>Font Awesome: keep your markup.</b> Add the drop-in, and every name with a twin shows ours, brands included.${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'mg-fa', label: 'Copy' })}</li>` : '',
+ sets.includes('fa') || sets.includes(FA_BRANDS) ? `<li><b>Font Awesome: keep your markup.</b> Add the drop-in, and every name with a twin shows ours, brands included.${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>`, id: 'mg-fa', label: 'Copy' })}</li>` : '',
sets.includes('lucide') || sets.includes('material') ? `<li><b>Lucide and Material: one tag each.</b> Add the web component, then swap each icon for the <code><ov-icon></code> in the table (or keep the old name with <code>lucide:</code> or <code>material:</code> in front).${kit.snippet({ code: `<script src="${base}/ov-icon.js" defer></script>`, id: 'mg-wc', label: 'Copy' })}</li>` : '',
sets.includes('lucide') || sets.includes('material') ? `<li><b>Lucide and Material: one tag each.</b> Add the web component, then swap each icon for the <code><ov-icon></code> in the table (or keep the old name with <code>lucide:</code> or <code>material:</code> in front).${kit.snippet({ code: `<script src="${base}/ov-icon.js" defer></script>`, id: 'mg-wc', label: 'Copy' })}</li>` : '',
].filter(Boolean).join('');
].filter(Boolean).join('');
const row = (f) => {
const row = (f) => {
@@ -310,7 +313,7 @@ function migratePage({ base = 'https://openvibe.design', text = '', found = null
@@ -310,7 +313,7 @@ function migratePage({ base = 'https://openvibe.design', text = '', found = null
};
};
results = found.length ? `<section class="ic-sec" id="results" aria-labelledby="mg-res">
results = found.length ? `<section class="ic-sec" id="results" aria-labelledby="mg-res">
<div class="ic-sec-head"><h2 id="mg-res">${matched.length} of ${found.length} have a twin</h2><p>${usesOk} of the ${uses} times your text asks for an icon would show an OpenVibe one. The rest stay as they are until someone draws them.</p></div>
<div class="ic-sec-head"><h2 id="mg-res">${matched.length} of ${found.length} have a twin</h2><p>${usesOk} of the ${uses} times your text asks for an icon would show an OpenVibe one. The rest stay as they are until someone draws them.</p></div>
- ${how ? `<ol class="fa-steps">${how}</ol>` : ''}
+ ${how ? `<ol class="legacy-steps">${how}</ol>` : ''}
<div class="mg-wrap"><table class="mg-table"><thead><tr><th scope="col">Yours</th><th scope="col">Uses</th><th scope="col">OpenVibe</th><th scope="col">Use</th></tr></thead><tbody>${found.map(row).join('')}</tbody></table></div>
<div class="mg-wrap"><table class="mg-table"><thead><tr><th scope="col">Yours</th><th scope="col">Uses</th><th scope="col">OpenVibe</th><th scope="col">Use</th></tr></thead><tbody>${found.map(row).join('')}</tbody></table></div>
</section>` : `<section class="ic-sec" id="results" aria-labelledby="mg-res"><div class="ic-sec-head"><h2 id="mg-res">No icons found</h2><p>Nothing in that text looks like a Font Awesome class, a Lucide icon or a Material one. Paste the HTML of a page, a template, or a component’s imports.</p></div></section>`;
</section>` : `<section class="ic-sec" id="results" aria-labelledby="mg-res"><div class="ic-sec-head"><h2 id="mg-res">No icons found</h2><p>Nothing in that text looks like a Font Awesome class, a Lucide icon or a Material one. Paste the HTML of a page, a template, or a component’s imports.</p></div></section>`;
}
}
@@ -324,7 +327,7 @@ function migratePage({ base = 'https://openvibe.design', text = '', found = null
@@ -324,7 +327,7 @@ function migratePage({ base = 'https://openvibe.design', text = '', found = null
${error ? kit.notice({ kind: 'error', text: error }) : ''}
${error ? kit.notice({ kind: 'error', text: error }) : ''}
<form class="mg-form" method="post" action="/icons/migrate#results">
<form class="mg-form" method="post" action="/icons/migrate#results">
<label class="mg-label" for="mg-text">Your HTML or code</label>
<label class="mg-label" for="mg-text">Your HTML or code</label>
- <textarea id="mg-text" name="text" rows="10" maxlength="400000" spellcheck="false" placeholder="<i class="fa-solid fa-gear"></i> <i data-lucide="trash-2"></i> import { Settings } from 'lucide-react' <span class="material-symbols-outlined">home</span>">${esc(text)}</textarea>
+ <textarea id="mg-text" name="text" rows="10" maxlength="400000" spellcheck="false" placeholder="<i class="${FA_SOLID} fa-gear"></i> <i data-lucide="trash-2"></i> import { Settings } from 'lucide-react' <span class="material-symbols-outlined">home</span>">${esc(text)}</textarea>
<div class="mg-go"><button type="submit" class="ic-btn is-go">${kit.icon('search')} Find the twins</button><span>Up to 400 KB. Font Awesome, Lucide, Material, MUI.</span></div>
<div class="mg-go"><button type="submit" class="ic-btn is-go">${kit.icon('search')} Find the twins</button><span>Up to 400 KB. Font Awesome, Lucide, Material, MUI.</span></div>
</form>
</form>
${results}
${results}
@@ -136,10 +136,10 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
@@ -136,10 +136,10 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
<div class="ic-sec-head"><h2 id="ic-use">Use them</h2><p>Every OpenVibe site already shows them in place of Font Awesome’s (the navbar loads the drop-in). Anyone else can too; it is open source (MIT).</p></div>
<div class="ic-sec-head"><h2 id="ic-use">Use them</h2><p>Every OpenVibe site already shows them in place of Font Awesome’s (the navbar loads the drop-in). Anyone else can too; it is open source (MIT).</p></div>
<div class="ic-use">
<div class="ic-use">
<div><h3>On any page</h3>${kit.snippet({ code: `<script src="${base}/ov-icon.js" defer></script>\n<ov-icon name="heart"></ov-icon>\n<ov-icon name="mic" size="16" label="Microphone"></ov-icon>`, id: 'ic-use-wc', label: 'Copy the HTML' })}<p class="ic-more">A web component: no sign-in, any framework. It follows the text’s size and colour and turns solid when small.</p></div>
<div><h3>On any page</h3>${kit.snippet({ code: `<script src="${base}/ov-icon.js" defer></script>\n<ov-icon name="heart"></ov-icon>\n<ov-icon name="mic" size="16" label="Microphone"></ov-icon>`, id: 'ic-use-wc', label: 'Copy the HTML' })}<p class="ic-more">A web component: no sign-in, any framework. It follows the text’s size and colour and turns solid when small.</p></div>
- <div><h3>In place of Font Awesome</h3>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>\n<!-- your <i class="fa-solid fa-eye"> now shows the glyph -->`, id: 'ic-use-web', label: 'Copy the HTML' })}<p class="ic-more">Sizes, animations and transforms keep working: <a href="/icons/font-awesome-alternative">what keeps working</a>.</p></div>
+ <div><h3>In place of Font Awesome</h3>${kit.snippet({ code: `<script src="${base}/shared/ov-glyphs.js" defer></script>\n<!-- your <i class="ovg-fill" data-ov-icon="eye"> now shows the glyph -->`, id: 'ic-use-web', label: 'Copy the HTML' })}<p class="ic-more">Sizes, animations and transforms keep working: <a href="/icons/font-awesome-alternative">what keeps working</a>.</p></div>
<div><h3>In React, Vue or Svelte</h3>${kit.snippet({ code: `npm i ${base}/icons/npm/openvibe-icons-react.tgz\n\nimport { Icon, BellIcon } from 'openvibe-icons-react';\n<Icon name="bell" size={16} />`, id: 'ic-use-npm', label: 'Copy' })}<p class="ic-more">No registry account: npm installs it from this site. <a href="/icons/npm/openvibe-icons-vue.tgz">Vue</a>, <a href="/icons/npm/openvibe-icons-svelte.tgz">Svelte</a> and <a href="/icons/npm/openvibe-icons.tgz">plain JavaScript</a> (<code>svg('bell', { size: 16 })</code>) the same way. Solid at 18 px and under, like the sites.</p></div>
<div><h3>In React, Vue or Svelte</h3>${kit.snippet({ code: `npm i ${base}/icons/npm/openvibe-icons-react.tgz\n\nimport { Icon, BellIcon } from 'openvibe-icons-react';\n<Icon name="bell" size={16} />`, id: 'ic-use-npm', label: 'Copy' })}<p class="ic-more">No registry account: npm installs it from this site. <a href="/icons/npm/openvibe-icons-vue.tgz">Vue</a>, <a href="/icons/npm/openvibe-icons-svelte.tgz">Svelte</a> and <a href="/icons/npm/openvibe-icons.tgz">plain JavaScript</a> (<code>svg('bell', { size: 16 })</code>) the same way. Solid at 18 px and under, like the sites.</p></div>
<div><h3>As a font</h3>${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-font.css">\n<i class="ovi ovi-eye"></i>`, id: 'ic-use-font', label: 'Copy the HTML' })}</div>
<div><h3>As a font</h3>${kit.snippet({ code: `<link rel="stylesheet" href="${base}/shared/ov-glyphs-font.css">\n<i class="ovi ovi-eye"></i>`, id: 'ic-use-font', label: 'Copy the HTML' })}</div>
- <div><h3>On a server</h3>${kit.snippet({ code: "const glyphs = require('openvibe-shared/glyphs');\n\nglyphs.svg('live', { size: 20 }); // a standalone SVG\nglyphs.fromFa('fa-eye'); // 'eye'", id: 'ic-use-node', label: 'Copy the code' })}</div>
+ <div><h3>On a server</h3>${kit.snippet({ code: "const glyphs = require('openvibe-shared/glyphs');\n\nglyphs.svg('live', { size: 20 }); // a standalone SVG\nglyphs.fromFa('eye'); // 'eye' (a Font Awesome name works too)", id: 'ic-use-node', label: 'Copy the code' })}</div>
<div><h3>Switching from another set?</h3><p class="ic-more">Font Awesome, Lucide, Material or MUI: <a href="/icons/migrate">paste your page</a> and see every icon with its OpenVibe twin. Their names work in <code><ov-icon name="lucide:settings"></code> too.</p></div>
<div><h3>Switching from another set?</h3><p class="ic-more">Font Awesome, Lucide, Material or MUI: <a href="/icons/migrate">paste your page</a> and see every icon with its OpenVibe twin. Their names work in <code><ov-icon name="lucide:settings"></code> too.</p></div>
<div><h3>As data</h3>${kit.snippet({ code: `GET ${base}/api/v1/glyphs\nGET ${base}/api/v1/glyphs?names=heart,fa-gear\nGET ${base}/api/v1/glyphs/search?q=heart\nGET ${base}/api/v1/glyphs/suggest?label=deep%20dive\nGET ${base}/api/v1/glyphs/<name>?style=thin|light|regular|bold|solid|duotone\nPOST ${base}/api/v1/glyphs/draft\nGET ${base}/icons/<name>.svg?style=auto|line|solid`, id: 'ic-use-api', label: 'Copy' })}</div>
<div><h3>As data</h3>${kit.snippet({ code: `GET ${base}/api/v1/glyphs\nGET ${base}/api/v1/glyphs?names=heart,fa-gear\nGET ${base}/api/v1/glyphs/search?q=heart\nGET ${base}/api/v1/glyphs/suggest?label=deep%20dive\nGET ${base}/api/v1/glyphs/<name>?style=thin|light|regular|bold|solid|duotone\nPOST ${base}/api/v1/glyphs/draft\nGET ${base}/icons/<name>.svg?style=auto|line|solid`, id: 'ic-use-api', label: 'Copy' })}</div>
</div>
</div>
@@ -28,12 +28,12 @@ function iconStyles() {
@@ -28,12 +28,12 @@ function iconStyles() {
['line', 'Line', 'Outlines at every size.'],
['line', 'Line', 'Outlines at every size.'],
['solid', 'Solid', 'Filled at every size.'],
['solid', 'Solid', 'Filled at every size.'],
];
];
- const i = (name, force) => `<i class="fa-solid ${name}${force ? ` ovg-${force}` : ''}" aria-hidden="true"></i>`;
+ const i = (name, force) => `<i class="ovg-fill${force ? ` ovg-${force}` : ''}" data-ov-icon="${name}" aria-hidden="true"></i>`;
const screen = (style) => {
const screen = (style) => {
const force = style === 'auto' ? '' : style;
const force = style === 'auto' ? '' : style;
- const rows = [['fa-tower-broadcast', 72], ['fa-comments', 54], ['fa-heart', 64]].map(([n, w]) => `<span class="lk-st-row"><span class="lk-st-dot">${i(n, force)}</span><span class="lk-st-lines"><span style="width:${w}%"></span><span></span></span></span>`).join('');
- const bar = ['fa-house', 'fa-compass', 'fa-bell', 'fa-user'].map((n, k) => `<span class="lk-st-tab"${k === 0 ? ' aria-current="page"' : ''}>${i(n, force)}</span>`).join('');
- return `<span class="lk-st-screen"${style === 'auto' ? ' style="--ovg-pref:18.4px"' : ''}><span class="lk-st-head"><span class="lk-st-title"></span>${i('fa-magnifying-glass', force)}${i('fa-bell', force)}</span>${rows}<span class="lk-st-bar">${bar}</span></span>`;
+ const rows = [['live', 72], ['chat', 54], ['heart', 64]].map(([n, w]) => `<span class="lk-st-row"><span class="lk-st-dot">${i(n, force)}</span><span class="lk-st-lines"><span style="width:${w}%"></span><span></span></span></span>`).join('');
+ const bar = ['home', 'compass', 'bell', 'user'].map((n, k) => `<span class="lk-st-tab"${k === 0 ? ' aria-current="page"' : ''}>${i(n, force)}</span>`).join('');
+ return `<span class="lk-st-screen"${style === 'auto' ? ' style="--ovg-pref:18.4px"' : ''}><span class="lk-st-head"><span class="lk-st-title"></span>${i('search', force)}${i('bell', force)}</span>${rows}<span class="lk-st-bar">${bar}</span></span>`;
};
};
return `<div class="lk-icons"><span class="ovk-set-text" id="lk-icons-h">Icons</span><div class="lk-styles" role="radiogroup" aria-labelledby="lk-icons-h">${STYLES.map(([id, name, what]) => `<label class="lk-style"><input type="radio" name="icons" value="${id}"${id === 'auto' ? ' checked' : ''}>${screen(id)}<span class="lk-st-cap"><b>${name}</b><small>${what}</small></span></label>`).join('')}</div></div>`;
return `<div class="lk-icons"><span class="ovk-set-text" id="lk-icons-h">Icons</span><div class="lk-styles" role="radiogroup" aria-labelledby="lk-icons-h">${STYLES.map(([id, name, what]) => `<label class="lk-style"><input type="radio" name="icons" value="${id}"${id === 'auto' ? ' checked' : ''}>${screen(id)}<span class="lk-st-cap"><b>${name}</b><small>${what}</small></span></label>`).join('')}</div></div>`;
}
}
@@ -103,8 +103,8 @@ const { boot, check, done } = require('./helpers/boot');
@@ -103,8 +103,8 @@ const { boot, check, done } = require('./helpers/boot');
for (const id of ['auto', 'line', 'solid']) assert.match(r.text, new RegExp(`<label class="lk-style"><input type="radio" name="icons" value="${id}"( checked)?>`), id);
for (const id of ['auto', 'line', 'solid']) assert.match(r.text, new RegExp(`<label class="lk-style"><input type="radio" name="icons" value="${id}"( checked)?>`), id);
assert.strictEqual((r.text.match(/name="icons"/g) || []).length, 3, 'one control for the icon style');
assert.strictEqual((r.text.match(/name="icons"/g) || []).length, 3, 'one control for the icon style');
assert.match(r.text, /<span class="lk-st-screen" style="--ovg-pref:18\.4px">/, 'Auto shows Auto whatever the person chose');
assert.match(r.text, /<span class="lk-st-screen" style="--ovg-pref:18\.4px">/, 'Auto shows Auto whatever the person chose');
- assert.match(r.text, /class="fa-solid fa-bell ovg-line"/, 'Line forces lines');
- assert.match(r.text, /class="fa-solid fa-bell ovg-solid"/, 'Solid forces solids');
+ assert.match(r.text, /class="ovg-fill ovg-line" data-ov-icon="bell"/, 'Line forces lines');
+ assert.match(r.text, /class="ovg-fill ovg-solid" data-ov-icon="bell"/, 'Solid forces solids');
assert.match(r.text, /\/js\/look\.js\?v=[0-9a-f]{10}/);
assert.match(r.text, /\/js\/look\.js\?v=[0-9a-f]{10}/);
assert.match(r.text, /<script type="application\/json" id="lk-themes">\[\{"id":"vibe"/);
assert.match(r.text, /<script type="application\/json" id="lk-themes">\[\{"id":"vibe"/);
assert.match(r.text, /Sign in so it follows you/);
assert.match(r.text, /Sign in so it follows you/);