openvibers/openvibe.design Commits

Tokens: a look's corners in every format (#95)

OP OpenVibers committed 12 days ago · f9ba48f99b · 3 files, +71 −3

The token set gains shape.radius 1-3, by the look's personality as kit.css sets them: the quiet looks square
(4/6/10), Neon Tokyo and Vapor round (14/20/28), every other look the kit's own corners (10/14/20). CSS custom
properties, Tailwind borderRadius ov-1..3, Style Dictionary dimensions, SwiftUI CGFloat, Compose dp and Android
dimens.

Changes

ddiff --git a/server/tokens/formats.js b/server/tokens/formats.js
iindex 649258b..e5a397f 100644
---- a/server/tokens/formats.js
++++ b/server/tokens/formats.js
@@@ -12,7 +12,7 @@
* androidXml(doc) an Android colors.xml
*/
const tokens = require('openvibe-shared/tokens');
--const { MOTION, BRAND, colourList } = require('./kit');
++const { MOTION, BRAND, colourList, radiusList } = require('./kit');
const metaOf = (doc) => (doc && doc.$extensions && doc.$extensions['network.openvibe']) || { name: 'Theme', slug: 'theme', mode: 'dark' };
const camel = (path) => String(path).split('.').map((p, i) => (i ? p[0].toUpperCase() + p.slice(1) : p)).join('').replace(/[^A-Za-z0-9]/g, '');
@@@ -61,6 +61,8 @@ function css(doc) {
lines.push(` /* the kit's motion (openvibe-shared/ov-fx) */`);
for (const d of MOTION.durations) lines.push(` ${d.css}: ${d.ms}ms;`);
for (const e of MOTION.easings) lines.push(` ${e.css}: cubic-bezier(${e.value.join(', ')});`);
++ lines.push(` /* the kit's shape (openvibe-shared kit.css) */`);
++ for (const r of radiusList(doc)) lines.push(` ${r.css}: ${r.px}px;`);
lines.push(` /* the mark */`);
for (const b of BRAND) lines.push(` ${b.css}: ${b.value};`);
lines.push(` color-scheme: ${m.mode === 'light' ? 'light' : 'dark'};`);
@@@ -84,7 +86,9 @@ function tailwind(doc) {
for (const d of MOTION.durations) transitionDuration[d.id] = `${d.ms}ms`;
const transitionTimingFunction = {};
for (const e of MOTION.easings) transitionTimingFunction[e.id] = `cubic-bezier(${e.value.join(', ')})`;
-- const config = { theme: { extend: { colors, transitionDuration, transitionTimingFunction } } };
++ const borderRadius = {};
++ for (const r of radiusList(doc)) borderRadius[`ov-${r.id}`] = `${r.px}px`;
++ const config = { theme: { extend: { colors, transitionDuration, transitionTimingFunction, borderRadius } } };
return `${header(doc, 'js')}\nmodule.exports = ${JSON.stringify(config, null, 4)};\n`;
}
@@@ -100,6 +104,7 @@ function styleDictionary(doc) {
for (const c of colourList(doc)) put(out, c.path, { value: c.value, type: 'color', description: c.description });
for (const d of MOTION.durations) put(out, `motion.duration.${d.id}`, { value: `${d.ms}ms`, type: 'duration', description: d.description });
for (const e of MOTION.easings) put(out, `motion.easing.${e.id}`, { value: e.value.slice(), type: 'cubicBezier', description: e.description });
++ for (const r of radiusList(doc)) put(out, `shape.radius.${r.id}`, { value: `${r.px}px`, type: 'dimension', description: r.description });
for (const b of BRAND) put(out, `brand.${b.id}`, { value: b.value, type: 'color', description: b.description });
return `${JSON.stringify(out, null, 2)}\n`;
}
@@@ -138,6 +143,10 @@ function swift(doc) {
lines.push(` /// The kit's motion: ${e.css}.`);
lines.push(` public static let ${e.name} = Animation.timingCurve(${e.value.join(', ')})`);
}
++ for (const r of radiusList(doc)) {
++ lines.push(` /// The kit's shape: ${r.css}, ${safe(r.description).toLowerCase()}.`);
++ lines.push(` public static let ${r.name}: CGFloat = ${r.px}`);
++ }
for (const b of BRAND) {
lines.push(` /// ${safe(b.description)}`);
lines.push(` public static let ${b.name} = Color(hex: 0x${argb(b.value).argb})`);
@@@ -155,6 +164,7 @@ function kotlin(doc) {
lines.push('');
lines.push('import androidx.compose.animation.core.CubicBezierEasing');
lines.push('import androidx.compose.ui.graphics.Color');
++ lines.push('import androidx.compose.ui.unit.dp');
lines.push('');
lines.push(`/** OpenVibe's tokens for ${safe(m.name)} (${m.mode === 'light' ? 'light' : 'dark'}). */`);
lines.push('object OVTokens {');
@@@ -172,6 +182,10 @@ function kotlin(doc) {
lines.push(` /** The kit's motion: ${e.css}. */`);
lines.push(` val ${e.name} = CubicBezierEasing(${e.value.map((n) => `${n}f`).join(', ')})`);
}
++ for (const r of radiusList(doc)) {
++ lines.push(` /** The kit's shape: ${r.css}, ${safe(r.description).toLowerCase()}. */`);
++ lines.push(` val ${r.name} = ${r.px}.dp`);
++ }
for (const b of BRAND) {
lines.push(` /** ${safe(b.description)} */`);
lines.push(` val ${b.name} = Color(0x${argb(b.value).argb})`);
@@@ -197,6 +211,10 @@ function androidXml(doc) {
lines.push(` <!-- ${safe(b.description)} -->`);
lines.push(` <color name="ov_brand_${b.id}">#${argb(b.value).rgb}</color>`);
}
++ for (const r of radiusList(doc)) {
++ lines.push(` <!-- The kit's shape: ${safe(r.description).toLowerCase()} -->`);
++ lines.push(` <dimen name="ov_radius_${r.id}">${r.px}dp</dimen>`);
++ }
lines.push('</resources>');
return `${lines.join('\n')}\n`;
}
ddiff --git a/server/tokens/kit.js b/server/tokens/kit.js
iindex f3c9903..fe629b8 100644
---- a/server/tokens/kit.js
++++ b/server/tokens/kit.js
@@@ -29,6 +29,21 @@ const MOTION = {
],
};
++// The kit's shape (openvibe-shared 4.81.0, kit.css --ov-radius-1…3): three corner radii, set by the look's personality.
++// The quiet looks are square, Neon Tokyo and Vapor round, every other look the kit's own corners.
++const SQUARE = new Set(['paper', 'solarized-light', 'nord', 'nord-light', 'matrix', 'hacker', 'high-contrast']);
++const ROUND = new Set(['neon-tokyo', 'vapor']);
++const RADII = { default: [10, 14, 20], square: [4, 6, 10], round: [14, 20, 28] };
++const RADIUS = [
++ { id: '1', name: 'radius1', css: '--ov-radius-1', description: 'Small things: chips, inputs, rows' },
++ { id: '2', name: 'radius2', css: '--ov-radius-2', description: 'Cards, sections, panels' },
++ { id: '3', name: 'radius3', css: '--ov-radius-3', description: 'Sheets, dialogs, big surfaces' },
++];
++/** The look's shape: 'square', 'round' or 'default', by its id (the same lists as kit.css). */
++const shapeOf = (theme) => { const id = String((theme || {}).slug || (theme || {}).id || ''); return SQUARE.has(id) ? 'square' : ROUND.has(id) ? 'round' : 'default'; };
++/** The look's three corner radii: [{ id, name, css, px, description }]. */
++const radii = (theme) => RADIUS.map((r, i) => ({ ...r, px: RADII[shapeOf(theme)][i] }));
++
// The mark's own two colours (openvibe-shared/ov-fx.js names them as the mark's O and V).
const BRAND = [
{ id: 'o', name: 'brandO', value: '#4f7cff', css: '--ov-brand-o', description: "The mark's O" },
@@@ -79,8 +94,17 @@ function tokenDocument(theme) {
},
color: {},
motion: { duration: {}, easing: {} },
++ shape: { radius: {} },
brand: {},
};
++ for (const r of radii(t)) {
++ doc.shape.radius[r.id] = {
++ $type: 'dimension',
++ $value: `${r.px}px`,
++ $description: r.description,
++ $extensions: { 'network.openvibe': { css: r.css, shape: shapeOf(t) } },
++ };
++ }
for (const c of colours(t)) {
put(doc.color, c.path.replace(/^color\./, ''), {
$type: 'color',
@@@ -132,4 +156,13 @@ function colourList(doc) {
return out;
}
--module.exports = { MOTION, BRAND, colours, tokenDocument, colourList };
++/** The corner radii of a document, in order, with their CSS names and pixels (the kit's own when the document has none). */
++function radiusList(doc) {
++ const own = (doc && doc.shape && doc.shape.radius) || {};
++ return RADIUS.map((r, i) => {
++ const px = parseInt(String((own[r.id] || {}).$value || ''), 10);
++ return { ...r, px: Number.isFinite(px) && px >= 0 && px <= 64 ? px : RADII.default[i] };
++ });
++}
++
++module.exports = { MOTION, BRAND, colours, tokenDocument, colourList, radiusList, shapeOf, radii };
ddiff --git a/test/token-files.test.js b/test/token-files.test.js
iindex 3fb77c4..7bf36f1 100644
---- a/test/token-files.test.js
++++ b/test/token-files.test.js
@@@ -36,6 +36,15 @@ const curveOf = (v) => { const m = /cubic-bezier\(([^)]+)\)/.exec(String(v)); re
}
});
++ await check('a look\'s shape: the quiet looks square, Neon round, the rest the kit\'s own corners', async () => {
++ const kitTokens = require('../server/tokens/kit');
++ const px = (theme) => kitTokens.radiusList(kitTokens.tokenDocument(theme)).map((r) => r.px);
++ assert.deepStrictEqual(px({ slug: 'paper' }), [4, 6, 10]);
++ assert.deepStrictEqual(px({ slug: 'neon-tokyo' }), [14, 20, 28]);
++ assert.deepStrictEqual(px({ slug: 'vibe' }), [10, 14, 20]);
++ assert.strictEqual(kitTokens.tokenDocument({ slug: 'paper' }).shape.radius['1'].$extensions['network.openvibe'].shape, 'square');
++ });
++
await check('the motion tokens are exactly the kit\'s', async () => {
assert.deepStrictEqual(Object.keys(KIT_VARS).sort(), ['--ov-dur-1', '--ov-dur-2', '--ov-dur-3', '--ov-dur-4', '--ov-ease-in-out', '--ov-ease-out', '--ov-ease-spring']);
assert.strictEqual(msOf(KIT_VARS['--ov-dur-1']), 120);
@@@ -113,7 +122,15 @@ const curveOf = (v) => { const m = /cubic-bezier\(([^)]+)\)/.exec(String(v)); re
assert.match(kt, /const val duration1 = 120L/);
assert.match(kt, /val easeOut = CubicBezierEasing\(0\.2f, 0\.8f, 0\.2f, 1f\)/);
++ // The kit's shape (openvibe-shared 4.81.0): Vibe has the kit's own corners in every format.
++ assert.match(css, /--ov-radius-1: 10px;\n --ov-radius-2: 14px;\n --ov-radius-3: 20px;/);
++ assert.strictEqual(preset.borderRadius['ov-2'], '14px');
++ assert.strictEqual(sd.shape.radius['3'].value, '20px');
++ assert.strictEqual(sd.shape.radius['3'].type, 'dimension');
++ assert.match(swift, /public static let radius1: CGFloat = 10/);
++ assert.match(kt, /val radius2 = 14\.dp/);
const xml = (await t.get('/tokens/vibe.xml')).text;
++ assert.match(xml, /<dimen name="ov_radius_3">20dp<\/dimen>/);
assert.match(xml, /<color name="ov_bg_base">#0A0F1C<\/color>/);
assert.match(xml, /<color name="ov_accent_base">#3B82F6<\/color>/);
assert.match(xml, /<color name="ov_brand_v">#8B5CF6<\/color>/);