openvibers/openvibe.design Commits
Studio: As code with your edits — the edited theme compiled in the page for the web, Android, iOS, Flutter and Figma (#19)
Changes
@@@ -461,3 +461,9 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.cp-facts dd { margin: 0; overflow-wrap: anywhere; }.cp-facts code { font-size: .84rem; }@media (min-width: 900px) { .cp-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }++++/* The studio's "As code" sheet (your edits, compiled in the page). */++.ds-code-pick { display: flex; align-items: center; gap: 10px; margin: 0 0 8px; font-weight: 700; }++.ds-code-pick select { min-height: 44px; padding: 0 12px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-input); color: var(--ovk-text); font: inherit; }++#ds-code-sheet .ovk-snip pre { max-height: 52vh; white-space: pre; overflow: auto; }++.ds-code-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 10px 0 0; }@@@ -128,6 +128,43 @@$$('.ds-field input[type=color]').forEach((inp) => inp.addEventListener('input', () => setCore(inp.getAttribute('data-var'), inp.value)));$$('.ds-field input[data-hex]').forEach((inp) => inp.addEventListener('change', () => setCore(inp.getAttribute('data-hex'), inp.value)));$('#ds-reset').addEventListener('click', () => { edited = null; paint(); say('Back to the theme as it was.'); });++++ // ── As code, with the edits: compiled here (openvibe-shared tokens.js, loaded on first use); no edits → /tokens ──++ const codeLink = $('#ds-code');++ const sheet = $('#ds-code-sheet');++ let tokensLib = null;++ const loadTokens = () => (tokensLib ? Promise.resolve(tokensLib) : new Promise((resolve, reject) => {++ const s = document.createElement('script');++ s.src = codeLink.getAttribute('data-tokens-src');++ s.onload = () => { tokensLib = window.OpenVibeTokens; if (tokensLib) resolve(tokensLib); else reject(new Error('tokens')); };++ s.onerror = reject;++ document.head.appendChild(s);++ }));++ let codeFile = { name: 'theme.txt', text: '' };++ function showCode() {++ const target = $('#ds-code-target').value;++ const doc = tokensLib.fromTheme({ name: `${base.name} edited`, slug: `${base.id}-edited`, mode: base.mode, description: `${base.name}, as edited in the OpenVibe studio`, variables: current() });++ codeFile = { name: tokensLib.fileName(doc, target), text: tokensLib.compile(doc, target) || '' };++ const out = $('#ds-code-out code') || $('#ds-code-out');++ out.textContent = codeFile.text;++ $('#ds-code-dl').textContent = `Download ${codeFile.name}`;++ }++ if (codeLink && sheet && typeof sheet.showModal === 'function') {++ codeLink.addEventListener('click', (e) => {++ if (!edited || !base) return; // nothing edited: the theme as it is, on /tokens++ e.preventDefault();++ loadTokens().then(() => { showCode(); sheet.showModal(); }).catch(() => { location.href = codeLink.href; });++ });++ $('#ds-code-target').addEventListener('change', showCode);++ $('#ds-code-dl').addEventListener('click', () => {++ const a = document.createElement('a');++ a.href = URL.createObjectURL(new Blob([codeFile.text], { type: 'text/plain' }));++ a.download = codeFile.name;++ document.body.appendChild(a);++ a.click();++ setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000);++ });++ }$('#ds-cvd').addEventListener('change', (e) => {const v = e.target.value;previews.style.filter = v === 'gray' ? 'grayscale(1)' : v ? `url(#ds-cvd-${v})` : '';@@@ -16,6 +16,8 @@ const kit = require('openvibe-shared/kit');const sc = require('openvibe-shared/showcase');const rooms = require('openvibe-shared/rooms');const { iframeHtml } = require('../arena/sanitize');++const ovServe = require('openvibe-shared/serve');++const { TARGETS } = require('openvibe-shared/tokens');const { layout, esc, NAME } = layoutLib;const ROOM = rooms.get('design');@@@ -133,6 +135,15 @@ function home({ themes } = {}) {return layout({ title: null, description: 'Your look, everywhere: one theme across every OpenVibe site, the same theme as code for the web, Android and iOS, and AI design on it, ranked honestly by the Design Arena.', path: '/', body: `<div class="ds-wrap">${body}</div>`, active: '/' });}++/** The studio's "As code" sheet: the edited theme compiled in the page (public/js/studio.js). */++function codeSheet() {++ const options = TARGETS.map((t) => `<option value="${esc(t.id)}">${esc(t.label)}</option>`).join('');++ return kit.sheet({ id: 'ds-code-sheet', title: 'Your edits, as code', body: `<label class="ds-code-pick">Platform <select id="ds-code-target">${options}</select></label>++<p class="ds-fine" id="ds-code-note">The theme as you have edited it, light or dark as it is, ready for your own app. Nothing is saved.</p>++${kit.snippet({ code: '', id: 'ds-code-out', label: 'Copy the code' })}++<p class="ds-code-acts"><button type="button" class="ovk-btn ovk-btn--quiet ovk-btn--small" id="ds-code-dl">Download</button> <a class="ds-fine" href="/tokens" id="ds-code-all">Every theme as code</a></p>` });++}++function studio({ themes, me = {} } = {}) {const all = listOf(themes).filter((t) => t && t.id);const signedIn = !!me.signedIn;@@@ -154,7 +165,8 @@ ${signedIn ? `<p class="ds-sign ds-fine">Signed in as <b>${esc((me.user && (me.u<h3>Checks</h3><ul class="ds-checks" id="ds-checks" aria-live="polite"><li class="ds-fine">Pick a theme to check it.</li></ul><div class="ds-actions"><button type="button" class="ovk-btn ovk-btn--primary" id="ds-use" disabled>${signedIn ? 'Use this theme' : 'Sign in to use it'}</button><button type="button" class="ovk-btn ovk-btn--quiet" id="ds-reset" disabled>Undo my edits</button>--<a class="ovk-btn ovk-btn--quiet" id="ds-code" href="/tokens" title="This theme for the web, Android, iOS, Flutter and Figma">As code</a></div>++<a class="ovk-btn ovk-btn--quiet" id="ds-code" href="/tokens" data-tokens-src="${esc(ovServe.url('tokens.js'))}" title="This theme, with your edits, for the web, Android, iOS, Flutter and Figma">As code</a></div>++${codeSheet()}<div id="ds-try" hidden>${tryOn('')}</div><p class="ds-status" id="ds-status" role="status"></p><details class="ds-submit"><summary>Share it in the gallery</summary><form id="ds-submit-form"><label>Name <input type="text" id="ds-name" maxlength="60" minlength="2" required placeholder="Cozy autumn"></label>@@@ -86,6 +86,9 @@ const { boot, check, done } = require('./helpers/boot');const page = (await t.get('/t/vibe')).text;assert.match(page, /href="https:\/\/openvibe\.live\/\?ov-try=vibe"/);assert.match(page, /href="https:\/\/openvibe\.chat\/\?ov-try=vibe"/);++ const studioPage = (await t.get('/studio')).text;++ assert.match(studioPage, /id="ds-code" href="\/tokens" data-tokens-src="\/shared\/tokens\.js\?v=[0-9a-f]+"/, 'the studio can compile edits in the page');++ assert.match(studioPage, /<dialog class="ovk-sheet" id="ds-code-sheet"[\s\S]*<option value="compose">Android \(Jetpack Compose\)<\/option>/);assert.match((await t.get('/studio')).text, /<div id="ds-try" hidden>[\s\S]*data-try="https:\/\/openvibe\.live\/"/, 'the studio points them at the picked theme');});
@@ -461,3 +461,9 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -461,3 +461,9 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.cp-facts dd { margin: 0; overflow-wrap: anywhere; }
.cp-facts dd { margin: 0; overflow-wrap: anywhere; }
.cp-facts code { font-size: .84rem; }
.cp-facts code { font-size: .84rem; }
@media (min-width: 900px) { .cp-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
@media (min-width: 900px) { .cp-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
+
+/* The studio's "As code" sheet (your edits, compiled in the page). */
+.ds-code-pick { display: flex; align-items: center; gap: 10px; margin: 0 0 8px; font-weight: 700; }
+.ds-code-pick select { min-height: 44px; padding: 0 12px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-input); color: var(--ovk-text); font: inherit; }
+#ds-code-sheet .ovk-snip pre { max-height: 52vh; white-space: pre; overflow: auto; }
+.ds-code-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 10px 0 0; }
@@ -128,6 +128,43 @@
@@ -128,6 +128,43 @@
$$('.ds-field input[type=color]').forEach((inp) => inp.addEventListener('input', () => setCore(inp.getAttribute('data-var'), inp.value)));
$$('.ds-field input[type=color]').forEach((inp) => inp.addEventListener('input', () => setCore(inp.getAttribute('data-var'), inp.value)));
$$('.ds-field input[data-hex]').forEach((inp) => inp.addEventListener('change', () => setCore(inp.getAttribute('data-hex'), inp.value)));
$$('.ds-field input[data-hex]').forEach((inp) => inp.addEventListener('change', () => setCore(inp.getAttribute('data-hex'), inp.value)));
$('#ds-reset').addEventListener('click', () => { edited = null; paint(); say('Back to the theme as it was.'); });
$('#ds-reset').addEventListener('click', () => { edited = null; paint(); say('Back to the theme as it was.'); });
+
+ // ── As code, with the edits: compiled here (openvibe-shared tokens.js, loaded on first use); no edits → /tokens ──
+ const codeLink = $('#ds-code');
+ const sheet = $('#ds-code-sheet');
+ let tokensLib = null;
+ const loadTokens = () => (tokensLib ? Promise.resolve(tokensLib) : new Promise((resolve, reject) => {
+ const s = document.createElement('script');
+ s.src = codeLink.getAttribute('data-tokens-src');
+ s.onload = () => { tokensLib = window.OpenVibeTokens; if (tokensLib) resolve(tokensLib); else reject(new Error('tokens')); };
+ s.onerror = reject;
+ document.head.appendChild(s);
+ }));
+ let codeFile = { name: 'theme.txt', text: '' };
+ function showCode() {
+ const target = $('#ds-code-target').value;
+ const doc = tokensLib.fromTheme({ name: `${base.name} edited`, slug: `${base.id}-edited`, mode: base.mode, description: `${base.name}, as edited in the OpenVibe studio`, variables: current() });
+ codeFile = { name: tokensLib.fileName(doc, target), text: tokensLib.compile(doc, target) || '' };
+ const out = $('#ds-code-out code') || $('#ds-code-out');
+ out.textContent = codeFile.text;
+ $('#ds-code-dl').textContent = `Download ${codeFile.name}`;
+ }
+ if (codeLink && sheet && typeof sheet.showModal === 'function') {
+ codeLink.addEventListener('click', (e) => {
+ if (!edited || !base) return; // nothing edited: the theme as it is, on /tokens
+ e.preventDefault();
+ loadTokens().then(() => { showCode(); sheet.showModal(); }).catch(() => { location.href = codeLink.href; });
+ });
+ $('#ds-code-target').addEventListener('change', showCode);
+ $('#ds-code-dl').addEventListener('click', () => {
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(new Blob([codeFile.text], { type: 'text/plain' }));
+ a.download = codeFile.name;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
+ });
+ }
$('#ds-cvd').addEventListener('change', (e) => {
$('#ds-cvd').addEventListener('change', (e) => {
const v = e.target.value;
const v = e.target.value;
previews.style.filter = v === 'gray' ? 'grayscale(1)' : v ? `url(#ds-cvd-${v})` : '';
previews.style.filter = v === 'gray' ? 'grayscale(1)' : v ? `url(#ds-cvd-${v})` : '';
@@ -16,6 +16,8 @@ const kit = require('openvibe-shared/kit');
@@ -16,6 +16,8 @@ const kit = require('openvibe-shared/kit');
const sc = require('openvibe-shared/showcase');
const sc = require('openvibe-shared/showcase');
const rooms = require('openvibe-shared/rooms');
const rooms = require('openvibe-shared/rooms');
const { iframeHtml } = require('../arena/sanitize');
const { iframeHtml } = require('../arena/sanitize');
+const ovServe = require('openvibe-shared/serve');
+const { TARGETS } = require('openvibe-shared/tokens');
const { layout, esc, NAME } = layoutLib;
const { layout, esc, NAME } = layoutLib;
const ROOM = rooms.get('design');
const ROOM = rooms.get('design');
@@ -133,6 +135,15 @@ function home({ themes } = {}) {
@@ -133,6 +135,15 @@ function home({ themes } = {}) {
return layout({ title: null, description: 'Your look, everywhere: one theme across every OpenVibe site, the same theme as code for the web, Android and iOS, and AI design on it, ranked honestly by the Design Arena.', path: '/', body: `<div class="ds-wrap">${body}</div>`, active: '/' });
return layout({ title: null, description: 'Your look, everywhere: one theme across every OpenVibe site, the same theme as code for the web, Android and iOS, and AI design on it, ranked honestly by the Design Arena.', path: '/', body: `<div class="ds-wrap">${body}</div>`, active: '/' });
}
}
+/** The studio's "As code" sheet: the edited theme compiled in the page (public/js/studio.js). */
+function codeSheet() {
+ const options = TARGETS.map((t) => `<option value="${esc(t.id)}">${esc(t.label)}</option>`).join('');
+ return kit.sheet({ id: 'ds-code-sheet', title: 'Your edits, as code', body: `<label class="ds-code-pick">Platform <select id="ds-code-target">${options}</select></label>
+<p class="ds-fine" id="ds-code-note">The theme as you have edited it, light or dark as it is, ready for your own app. Nothing is saved.</p>
+${kit.snippet({ code: '', id: 'ds-code-out', label: 'Copy the code' })}
+<p class="ds-code-acts"><button type="button" class="ovk-btn ovk-btn--quiet ovk-btn--small" id="ds-code-dl">Download</button> <a class="ds-fine" href="/tokens" id="ds-code-all">Every theme as code</a></p>` });
+}
+
function studio({ themes, me = {} } = {}) {
function studio({ themes, me = {} } = {}) {
const all = listOf(themes).filter((t) => t && t.id);
const all = listOf(themes).filter((t) => t && t.id);
const signedIn = !!me.signedIn;
const signedIn = !!me.signedIn;
@@ -154,7 +165,8 @@ ${signedIn ? `<p class="ds-sign ds-fine">Signed in as <b>${esc((me.user && (me.u
@@ -154,7 +165,8 @@ ${signedIn ? `<p class="ds-sign ds-fine">Signed in as <b>${esc((me.user && (me.u
<h3>Checks</h3><ul class="ds-checks" id="ds-checks" aria-live="polite"><li class="ds-fine">Pick a theme to check it.</li></ul>
<h3>Checks</h3><ul class="ds-checks" id="ds-checks" aria-live="polite"><li class="ds-fine">Pick a theme to check it.</li></ul>
<div class="ds-actions"><button type="button" class="ovk-btn ovk-btn--primary" id="ds-use" disabled>${signedIn ? 'Use this theme' : 'Sign in to use it'}</button>
<div class="ds-actions"><button type="button" class="ovk-btn ovk-btn--primary" id="ds-use" disabled>${signedIn ? 'Use this theme' : 'Sign in to use it'}</button>
<button type="button" class="ovk-btn ovk-btn--quiet" id="ds-reset" disabled>Undo my edits</button>
<button type="button" class="ovk-btn ovk-btn--quiet" id="ds-reset" disabled>Undo my edits</button>
-<a class="ovk-btn ovk-btn--quiet" id="ds-code" href="/tokens" title="This theme for the web, Android, iOS, Flutter and Figma">As code</a></div>
+<a class="ovk-btn ovk-btn--quiet" id="ds-code" href="/tokens" data-tokens-src="${esc(ovServe.url('tokens.js'))}" title="This theme, with your edits, for the web, Android, iOS, Flutter and Figma">As code</a></div>
+${codeSheet()}
<div id="ds-try" hidden>${tryOn('')}</div>
<div id="ds-try" hidden>${tryOn('')}</div>
<p class="ds-status" id="ds-status" role="status"></p>
<p class="ds-status" id="ds-status" role="status"></p>
<details class="ds-submit"><summary>Share it in the gallery</summary><form id="ds-submit-form"><label>Name <input type="text" id="ds-name" maxlength="60" minlength="2" required placeholder="Cozy autumn"></label>
<details class="ds-submit"><summary>Share it in the gallery</summary><form id="ds-submit-form"><label>Name <input type="text" id="ds-name" maxlength="60" minlength="2" required placeholder="Cozy autumn"></label>
@@ -86,6 +86,9 @@ const { boot, check, done } = require('./helpers/boot');
@@ -86,6 +86,9 @@ const { boot, check, done } = require('./helpers/boot');
const page = (await t.get('/t/vibe')).text;
const page = (await t.get('/t/vibe')).text;
assert.match(page, /href="https:\/\/openvibe\.live\/\?ov-try=vibe"/);
assert.match(page, /href="https:\/\/openvibe\.live\/\?ov-try=vibe"/);
assert.match(page, /href="https:\/\/openvibe\.chat\/\?ov-try=vibe"/);
assert.match(page, /href="https:\/\/openvibe\.chat\/\?ov-try=vibe"/);
+ const studioPage = (await t.get('/studio')).text;
+ assert.match(studioPage, /id="ds-code" href="\/tokens" data-tokens-src="\/shared\/tokens\.js\?v=[0-9a-f]+"/, 'the studio can compile edits in the page');
+ assert.match(studioPage, /<dialog class="ovk-sheet" id="ds-code-sheet"[\s\S]*<option value="compose">Android \(Jetpack Compose\)<\/option>/);
assert.match((await t.get('/studio')).text, /<div id="ds-try" hidden>[\s\S]*data-try="https:\/\/openvibe\.live\/"/, 'the studio points them at the picked theme');
assert.match((await t.get('/studio')).text, /<div id="ds-try" hidden>[\s\S]*data-try="https:\/\/openvibe\.live\/"/, 'the studio points them at the picked theme');
});
});