openvibers/openvibe.design Commits
Every glyph as a CSS class: /icons.css (#124)
Changes
@@@ -10,6 +10,7 @@* GET /icons/<name>.xml an Android VectorDrawable, with the maker's colour and style* GET /icons/<name>/favicon.zip a favicon set from the maker's choices: SVG, PNGs, manifest, the <head> lines* GET /ov-icon.js (also /icons.js) the <ov-icon name="…"> web component: any page, no sign-in (L50 step 2)++ * GET /icons.css[?names=a,b] every glyph as a CSS class (icon-<name>, is-solid for the solid), through a mask* GET /icons/pack/<file> the fonts (WOFF2, TTF, OTF), the sprite, the CSS, the code points, and* ov-glyphs-svg.zip with every glyph* GET /icons/font-awesome-alternative how to switch, and what keeps working@@@ -155,6 +156,38 @@ function createIconRoutes(ctx) {res.set('Cache-Control', 'public, max-age=86400').set('Access-Control-Allow-Origin', '*').attachment(req.params.file).type('application/gzip').send(body); // attachment() would type it by extension});++ // Every glyph as a CSS class (L50: "a CSS file of every mask class"): <i class="icon-search"></i> paints the glyph in++ // the text's colour through a mask, with no markup and no script; add "is-solid" for the solid drawing where there is++ // one. ?names=search,heart keeps the file to those. One rule carries the shape; each glyph only sets its picture.++ let iconsCss = null;++ function cssFor(names) {++ const enc = (svg) => `data:image/svg+xml,${svg.replace(/"/g, "'").replace(/[<>#%{}\n]/g, (c) => encodeURIComponent(c))}`;++ const pic = (name, style) => {++ const svg = formats.inlineSvg(name, ex.options({ size: 24, style }));++ return svg ? enc(svg.replace(/currentColor/g, '#000').replace(' aria-hidden="true"', '')) : '';++ };++ const out = [++ '/* OpenVibe glyphs as CSS classes (openvibe.design/icons.css). <i class="icon-search"></i>; add is-solid for the solid',++ ' drawing. Painted in currentColor through a mask, so the text colour is the icon colour. Free to use (see /icons). */',++ // A name that is not a glyph draws nothing (a transparent picture), never a filled square.++ '[class^="icon-"],[class*=" icon-"]{--ovi:linear-gradient(transparent,transparent);display:inline-block;width:1em;height:1em;vertical-align:-.125em;background-color:currentColor;'++ + '-webkit-mask:var(--ovi) center/contain no-repeat;mask:var(--ovi) center/contain no-repeat}',++ ];++ for (const name of names) {++ const line = pic(name, 'line');++ if (!line) continue;++ out.push(`.icon-${name}{--ovi:url("${line}")}`);++ const solid = pic(name, 'solid');++ if (solid && solid !== line) out.push(`.icon-${name}.is-solid{--ovi:url("${solid}")}`);++ }++ return `${out.join('\n')}\n`;++ }++ r.get('/icons.css', (req, res) => {++ const want = String(req.query.names || '').split(',').map((x) => x.trim()).filter((x) => /^[a-z][a-z0-9-]{0,40}$/.test(x) && glyphs.path(x));++ const body = want.length ? cssFor([...new Set(want)]) : (iconsCss = iconsCss || cssFor(glyphs.NAMES));++ res.set(OPEN).set('Cache-Control', 'public, max-age=86400').type('text/css').send(body);++ });++r.get(['/ov-icon.js', '/icons.js'], (_req, res) => {res.set(OPEN).set('Cache-Control', 'public, max-age=86400').type('application/javascript').send(ovIcon());});@@@ -116,7 +116,7 @@ function glyphPage({ base = 'https://openvibe.design', name, opts = ex.options()fa.length ? way('Font Awesome markup', `<script src="${base}/shared/ov-glyphs.js" defer></script>\n${faTag}`, 'gp-fa', 'Keep your markup; the drop-in draws ours.') : '',cp ? way('The font', `<link rel="stylesheet" href="${base}/shared/ov-glyphs-font.css">\n<i class="ovi ovi-${name}"></i>`, 'gp-font') : '',way('The sprite', `<svg width="24" height="24"><use href="${base}/shared/ov-glyphs.svg#ovg-${name}"/></svg>`, 'gp-sprite'),-- way('CSS, no markup', formats.cssMask(name, at24), 'gp-mask', 'A mask painted in the text’s colour: <code><span class="icon-${esc(name)}"></span></code>.'),++ way('CSS, no markup', formats.cssMask(name, at24), 'gp-mask', `A mask painted in the text’s colour: <code><span class="icon-${esc(name)}"></span></code>. Or link it, with any others: <code><link rel="stylesheet" href="${esc(base)}/icons.css?names=${esc(name)}"></code>; add <code>is-solid</code> for the solid drawing.`),way('Data URI', formats.dataUri(name, at24), 'gp-data', 'For an <code><img></code> or a CSS <code>url()</code>, in a fixed colour.'),way('ES module', `import ${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} from '${base}/icons/${name}.mjs';\n// { name, line, solid }: the drawings, for a component of your own`, 'gp-mjs'),way('Node', `require('openvibe-shared/glyphs').svg('${name}', { size: 24 })`, 'gp-node'),nnew file mode 100644@@@ -0,0 +1,37 @@++'use strict';++// Every glyph as a CSS class (plan L50: "a CSS file of every mask class"): /icons.css gives icon-<name> for every glyph,++// is-solid where a solid drawing exists, one shared rule for the shape, and ?names= for a smaller file. Open to any site.++const assert = require('node:assert/strict');++const { boot, check, done } = require('./helpers/boot');++const glyphs = require('openvibe-shared/glyphs');++++(async () => {++ const t = await boot();++ try {++ await check('the whole set: one rule for the shape, a class per glyph, solids where they differ', async () => {++ const res = await t.get('/icons.css');++ assert.equal(res.status, 200);++ assert.match(String(res.headers.get('content-type')), /text\/css/);++ assert.equal(res.headers.get('access-control-allow-origin'), '*', 'any site may link it');++ const css = res.text;++ assert.ok(css.includes('[class^="icon-"],[class*=" icon-"]{--ovi:linear-gradient(transparent,transparent);display:inline-block;width:1em;height:1em;'), 'one rule for the shape');++ assert.ok(css.includes('mask:var(--ovi) center/contain no-repeat'));++ assert.ok(css.includes('{--ovi:linear-gradient(transparent,transparent);'), 'a name that is not a glyph draws nothing, not a square');++ const classes = css.match(/^\.icon-[a-z0-9-]+\{--ovi:url\("data:image\/svg\+xml,/gm) || [];++ assert.equal(classes.length, glyphs.NAMES.length, 'a class for every glyph');++ assert.ok(/^\.icon-search\.is-solid\{--ovi:url\(/m.test(css), 'search has a solid');++ assert.ok(!/^\.icon-check\.is-solid\{/m.test(css), 'a glyph of lines only has no solid rule');++ assert.ok(!/currentColor/.test(css.split('\n').slice(3).join('\n')), 'the pictures are masks: plain black shapes');++ });++++ await check('?names= keeps the file to those glyphs, and ignores what is not one', async () => {++ const res = await t.get('/icons.css?names=search,heart,search,not-a-glyph,%3Cx%3E');++ assert.equal(res.status, 200);++ const rules = res.text.match(/^\.icon-[a-z0-9-]+\{/gm) || [];++ assert.deepEqual(rules, ['.icon-search{', '.icon-heart{'], 'each once, in order, nothing else');++ });++ } finally {++ await t.close();++ }++ done();++})().catch((err) => { console.error(err); process.exit(1); });
@@ -10,6 +10,7 @@
@@ -10,6 +10,7 @@
* GET /icons/<name>.xml an Android VectorDrawable, with the maker's colour and style
* GET /icons/<name>.xml an Android VectorDrawable, with the maker's colour and style
* GET /icons/<name>/favicon.zip a favicon set from the maker's choices: SVG, PNGs, manifest, the <head> lines
* GET /icons/<name>/favicon.zip a favicon set from the maker's choices: SVG, PNGs, manifest, the <head> lines
* GET /ov-icon.js (also /icons.js) the <ov-icon name="…"> web component: any page, no sign-in (L50 step 2)
* GET /ov-icon.js (also /icons.js) the <ov-icon name="…"> web component: any page, no sign-in (L50 step 2)
+ * GET /icons.css[?names=a,b] every glyph as a CSS class (icon-<name>, is-solid for the solid), through a mask
* GET /icons/pack/<file> the fonts (WOFF2, TTF, OTF), the sprite, the CSS, the code points, and
* GET /icons/pack/<file> the fonts (WOFF2, TTF, OTF), the sprite, the CSS, the code points, and
* ov-glyphs-svg.zip with every glyph
* ov-glyphs-svg.zip with every glyph
* GET /icons/font-awesome-alternative how to switch, and what keeps working
* GET /icons/font-awesome-alternative how to switch, and what keeps working
@@ -155,6 +156,38 @@ function createIconRoutes(ctx) {
@@ -155,6 +156,38 @@ function createIconRoutes(ctx) {
res.set('Cache-Control', 'public, max-age=86400').set('Access-Control-Allow-Origin', '*').attachment(req.params.file).type('application/gzip').send(body); // attachment() would type it by extension
res.set('Cache-Control', 'public, max-age=86400').set('Access-Control-Allow-Origin', '*').attachment(req.params.file).type('application/gzip').send(body); // attachment() would type it by extension
});
});
+ // Every glyph as a CSS class (L50: "a CSS file of every mask class"): <i class="icon-search"></i> paints the glyph in
+ // the text's colour through a mask, with no markup and no script; add "is-solid" for the solid drawing where there is
+ // one. ?names=search,heart keeps the file to those. One rule carries the shape; each glyph only sets its picture.
+ let iconsCss = null;
+ function cssFor(names) {
+ const enc = (svg) => `data:image/svg+xml,${svg.replace(/"/g, "'").replace(/[<>#%{}\n]/g, (c) => encodeURIComponent(c))}`;
+ const pic = (name, style) => {
+ const svg = formats.inlineSvg(name, ex.options({ size: 24, style }));
+ return svg ? enc(svg.replace(/currentColor/g, '#000').replace(' aria-hidden="true"', '')) : '';
+ };
+ const out = [
+ '/* OpenVibe glyphs as CSS classes (openvibe.design/icons.css). <i class="icon-search"></i>; add is-solid for the solid',
+ ' drawing. Painted in currentColor through a mask, so the text colour is the icon colour. Free to use (see /icons). */',
+ // A name that is not a glyph draws nothing (a transparent picture), never a filled square.
+ '[class^="icon-"],[class*=" icon-"]{--ovi:linear-gradient(transparent,transparent);display:inline-block;width:1em;height:1em;vertical-align:-.125em;background-color:currentColor;'
+ + '-webkit-mask:var(--ovi) center/contain no-repeat;mask:var(--ovi) center/contain no-repeat}',
+ ];
+ for (const name of names) {
+ const line = pic(name, 'line');
+ if (!line) continue;
+ out.push(`.icon-${name}{--ovi:url("${line}")}`);
+ const solid = pic(name, 'solid');
+ if (solid && solid !== line) out.push(`.icon-${name}.is-solid{--ovi:url("${solid}")}`);
+ }
+ return `${out.join('\n')}\n`;
+ }
+ r.get('/icons.css', (req, res) => {
+ const want = String(req.query.names || '').split(',').map((x) => x.trim()).filter((x) => /^[a-z][a-z0-9-]{0,40}$/.test(x) && glyphs.path(x));
+ const body = want.length ? cssFor([...new Set(want)]) : (iconsCss = iconsCss || cssFor(glyphs.NAMES));
+ res.set(OPEN).set('Cache-Control', 'public, max-age=86400').type('text/css').send(body);
+ });
+
r.get(['/ov-icon.js', '/icons.js'], (_req, res) => {
r.get(['/ov-icon.js', '/icons.js'], (_req, res) => {
res.set(OPEN).set('Cache-Control', 'public, max-age=86400').type('application/javascript').send(ovIcon());
res.set(OPEN).set('Cache-Control', 'public, max-age=86400').type('application/javascript').send(ovIcon());
});
});
@@ -116,7 +116,7 @@ function glyphPage({ base = 'https://openvibe.design', name, opts = ex.options()
@@ -116,7 +116,7 @@ function glyphPage({ base = 'https://openvibe.design', name, opts = ex.options()
fa.length ? way('Font Awesome markup', `<script src="${base}/shared/ov-glyphs.js" defer></script>\n${faTag}`, 'gp-fa', 'Keep your markup; the drop-in draws ours.') : '',
fa.length ? way('Font Awesome markup', `<script src="${base}/shared/ov-glyphs.js" defer></script>\n${faTag}`, 'gp-fa', 'Keep your markup; the drop-in draws ours.') : '',
cp ? way('The font', `<link rel="stylesheet" href="${base}/shared/ov-glyphs-font.css">\n<i class="ovi ovi-${name}"></i>`, 'gp-font') : '',
cp ? way('The font', `<link rel="stylesheet" href="${base}/shared/ov-glyphs-font.css">\n<i class="ovi ovi-${name}"></i>`, 'gp-font') : '',
way('The sprite', `<svg width="24" height="24"><use href="${base}/shared/ov-glyphs.svg#ovg-${name}"/></svg>`, 'gp-sprite'),
way('The sprite', `<svg width="24" height="24"><use href="${base}/shared/ov-glyphs.svg#ovg-${name}"/></svg>`, 'gp-sprite'),
- way('CSS, no markup', formats.cssMask(name, at24), 'gp-mask', 'A mask painted in the text’s colour: <code><span class="icon-${esc(name)}"></span></code>.'),
+ way('CSS, no markup', formats.cssMask(name, at24), 'gp-mask', `A mask painted in the text’s colour: <code><span class="icon-${esc(name)}"></span></code>. Or link it, with any others: <code><link rel="stylesheet" href="${esc(base)}/icons.css?names=${esc(name)}"></code>; add <code>is-solid</code> for the solid drawing.`),
way('Data URI', formats.dataUri(name, at24), 'gp-data', 'For an <code><img></code> or a CSS <code>url()</code>, in a fixed colour.'),
way('Data URI', formats.dataUri(name, at24), 'gp-data', 'For an <code><img></code> or a CSS <code>url()</code>, in a fixed colour.'),
way('ES module', `import ${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} from '${base}/icons/${name}.mjs';\n// { name, line, solid }: the drawings, for a component of your own`, 'gp-mjs'),
way('ES module', `import ${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} from '${base}/icons/${name}.mjs';\n// { name, line, solid }: the drawings, for a component of your own`, 'gp-mjs'),
way('Node', `require('openvibe-shared/glyphs').svg('${name}', { size: 24 })`, 'gp-node'),
way('Node', `require('openvibe-shared/glyphs').svg('${name}', { size: 24 })`, 'gp-node'),
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
+'use strict';
+// Every glyph as a CSS class (plan L50: "a CSS file of every mask class"): /icons.css gives icon-<name> for every glyph,
+// is-solid where a solid drawing exists, one shared rule for the shape, and ?names= for a smaller file. Open to any site.
+const assert = require('node:assert/strict');
+const { boot, check, done } = require('./helpers/boot');
+const glyphs = require('openvibe-shared/glyphs');
+
+(async () => {
+ const t = await boot();
+ try {
+ await check('the whole set: one rule for the shape, a class per glyph, solids where they differ', async () => {
+ const res = await t.get('/icons.css');
+ assert.equal(res.status, 200);
+ assert.match(String(res.headers.get('content-type')), /text\/css/);
+ assert.equal(res.headers.get('access-control-allow-origin'), '*', 'any site may link it');
+ const css = res.text;
+ assert.ok(css.includes('[class^="icon-"],[class*=" icon-"]{--ovi:linear-gradient(transparent,transparent);display:inline-block;width:1em;height:1em;'), 'one rule for the shape');
+ assert.ok(css.includes('mask:var(--ovi) center/contain no-repeat'));
+ assert.ok(css.includes('{--ovi:linear-gradient(transparent,transparent);'), 'a name that is not a glyph draws nothing, not a square');
+ const classes = css.match(/^\.icon-[a-z0-9-]+\{--ovi:url\("data:image\/svg\+xml,/gm) || [];
+ assert.equal(classes.length, glyphs.NAMES.length, 'a class for every glyph');
+ assert.ok(/^\.icon-search\.is-solid\{--ovi:url\(/m.test(css), 'search has a solid');
+ assert.ok(!/^\.icon-check\.is-solid\{/m.test(css), 'a glyph of lines only has no solid rule');
+ assert.ok(!/currentColor/.test(css.split('\n').slice(3).join('\n')), 'the pictures are masks: plain black shapes');
+ });
+
+ await check('?names= keeps the file to those glyphs, and ignores what is not one', async () => {
+ const res = await t.get('/icons.css?names=search,heart,search,not-a-glyph,%3Cx%3E');
+ assert.equal(res.status, 200);
+ const rules = res.text.match(/^\.icon-[a-z0-9-]+\{/gm) || [];
+ assert.deepEqual(rules, ['.icon-search{', '.icon-heart{'], 'each once, in order, nothing else');
+ });
+ } finally {
+ await t.close();
+ }
+ done();
+})().catch((err) => { console.error(err); process.exit(1); });