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)

OP OpenVibers committed 15 days ago · 45e0b352ae · 4 files, +59 −1

With unsaved edits, As code opens a sheet: the theme as edited, compiled by openvibe-shared tokens.js (loaded on first use), a platform choice, copy and a named download. Without edits it still opens /tokens for the theme.

Changes

ddiff --git a/public/css/design.css b/public/css/design.css
iindex d6a8a2f..17ed232 100644
---- a/public/css/design.css
++++ b/public/css/design.css
@@@ -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; }
ddiff --git a/public/js/studio.js b/public/js/studio.js
iindex 54d767a..e900919 100644
---- a/public/js/studio.js
++++ b/public/js/studio.js
@@@ -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})` : '';
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 11d9242..b5c4448 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -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>
ddiff --git a/test/tokens.test.js b/test/tokens.test.js
iindex 7e3ec53..0158cc0 100644
---- a/test/tokens.test.js
++++ b/test/tokens.test.js
@@@ -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');
});