openvibers/openvibe.codes Commits

Setup on a phone: nothing wider than the screen, commands wrap with Copy (#272)

OP OpenVibers committed 9 days ago · f955eebafd · 5 files, +55 −14

* Setup on a phone: nothing wider than the screen, commands wrap with Copy

The installer's one long line widened the whole page to 595 px at 390 px (the grid's
column grew to its min-content). The page, its head, each step's body and the pairing
steps now have shrinkable columns, and a command wraps beside a Copy button (the
clipboard, else the command selected). On a phone the step chips wrap so none is cut,
the state tag sits beside the title, and cards are tighter.

* The Copy buttons' script loads on /setup only (js/setup.js), not on every page

page() takes a page's own scripts as it takes its stylesheets, so the home's JS stays
inside its budget.

Changes

ddiff --git a/public/css/setup.css b/public/css/setup.css
iindex e8cecd5..4abc3cf 100644
---- a/public/css/setup.css
++++ b/public/css/setup.css
@@@ -1,13 +1,12 @@
/* Codes' guided setup (/setup, 2026-11-03): the progress, the step chips, and each step's card. Loaded on /setup only. */
--.c4-setup{display:grid;gap:0}
--.c4-setup-head{display:grid;gap:12px;margin:0 0 16px}
++.c4-setup{display:grid;grid-template-columns:minmax(0,1fr);gap:0;min-width:0}
++.c4-setup-head{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;margin:0 0 16px;min-width:0}
.c4-setup-head .c4-title{margin:0}
.c4-setup-progress{display:flex;align-items:center;gap:12px}
.c4-setup-bar{flex:1;max-width:420px;height:8px;border-radius:99px;background:var(--c4-quiet-bg);overflow:hidden}
.c4-setup-bar>span{display:block;height:100%;border-radius:99px;background:var(--c4-accent);transition:width .4s cubic-bezier(.2,.8,.2,1)}
.c4-setup-count{color:var(--c4-muted);font-size:13px;font-weight:600;white-space:nowrap}
--.c4-setup-nav{display:flex;gap:8px;margin:0;padding:2px 0 4px;list-style:none;overflow-x:auto;scrollbar-width:none}
--.c4-setup-nav::-webkit-scrollbar{display:none}
++.c4-setup-nav{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:2px 0 4px;list-style:none}
.c4-setup-chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 14px 0 6px;border:1px solid var(--c4-line);border-radius:999px;background:var(--c4-surface);color:var(--c4-ink);font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
.c4-setup-chip-n{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--c4-quiet-bg);color:var(--c4-muted);font-size:12px;font-weight:700}
.c4-setup-chip-n svg,.c4-setup-n svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
@@@ -22,14 +21,19 @@
.c4-setup-step.is-done .c4-setup-n{background:var(--c4-ok-bg);color:var(--c4-ok-ink)}
.c4-setup-step-title h2{margin:0;font-size:17px;letter-spacing:-.01em}
.c4-setup-step-title p{margin:4px 0 0;color:var(--c4-muted);font-size:13px;line-height:1.45}
--.c4-setup-body{margin:14px 0 0 54px;display:grid;gap:12px;min-width:0}
++.c4-setup-body{margin:14px 0 0 54px;display:grid;grid-template-columns:minmax(0,1fr);gap:12px;min-width:0}
.c4-setup-body .c4-hint{margin:0}
.c4-setup-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
--.c4-setup-how{margin:0;padding-left:20px;display:grid;gap:10px;font-size:13.5px;line-height:1.5}
++.c4-setup-how{margin:0;padding-left:20px;display:grid;grid-template-columns:minmax(0,1fr);gap:10px;font-size:13.5px;line-height:1.5}
++.c4-setup-how>li{min-width:0}
.c4-setup-how a{color:var(--c4-accent)}
.c4-setup-cmd{display:grid;gap:4px;margin-top:6px;min-width:0}
.c4-setup-cmd-label{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--c4-muted)}
--.c4-setup-cmd code{display:block;padding:10px 12px;border-radius:9px;background:var(--c4-ghost);border:1px solid var(--c4-line);font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--c4-ink);overflow-x:auto;white-space:pre;user-select:all;-webkit-user-select:all}
++.c4-setup-cmd-box{display:flex;align-items:flex-start;gap:8px;min-width:0;padding:8px 8px 8px 12px;border-radius:10px;background:var(--c4-ghost);border:1px solid var(--c4-line)}
++.c4-setup-cmd code{flex:1;min-width:0;padding:4px 0;font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--c4-ink);white-space:pre-wrap;overflow-wrap:anywhere;user-select:all;-webkit-user-select:all}
++.c4-setup-copy{flex:none;min-height:32px;padding:0 12px;border:1px solid var(--c4-line);border-radius:8px;background:var(--c4-surface);color:var(--c4-ink);font:inherit;font-size:12px;font-weight:650;cursor:pointer}
++.c4-setup-copy:hover{border-color:var(--c4-accent);color:var(--c4-accent)}
++.c4-setup-copy:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}
.c4-setup-cmd code:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}
.c4-setup-agent{scroll-margin-top:84px}
.c4-setup-agent:target{background:var(--c4-open-bg);border-radius:10px}
@@@ -40,10 +44,22 @@
.c4-setup-ready p{margin:0 0 12px;color:var(--c4-muted)}
.c4-setup-link{margin:0 0 14px}
@media (max-width:640px){
-- .c4-setup-step-head{grid-template-columns:34px minmax(0,1fr);gap:12px}
-- .c4-setup-step-head>.c4-pill{grid-column:2;justify-self:start}
++ .c4-setup .c4-card{padding:16px}
++ .c4-setup-head{gap:10px}
++ .c4-setup-head .c4-title h1{font-size:26px}
++ .c4-setup-progress{gap:10px}
++ .c4-setup-bar{max-width:none}
++ /* Every step's chip on the screen at once: they wrap, none is cut at the edge. */
++ .c4-setup-nav{gap:6px}
++ .c4-setup-chip{min-height:36px;padding:0 12px 0 4px;font-size:12.5px}
++ .c4-setup-chip-n{width:26px;height:26px}
++ .c4-setup-step-head{grid-template-columns:34px minmax(0,1fr) auto;gap:10px}
++ .c4-setup-step-title h2{font-size:16px}
++ .c4-setup-step-title p{font-size:12.5px}
.c4-setup-n{width:34px;height:34px;font-size:14px}
-- .c4-setup-body{margin-left:0}
++ .c4-setup-body{margin:12px 0 0;gap:10px}
++ .c4-setup-how{padding-left:18px}
.c4-setup-agent-help{padding-left:0}
++ .c4-setup-actions>.c4-button,.c4-setup-actions>.c4-opill{flex:1 1 auto;justify-content:center}
}
@media (prefers-reduced-motion:reduce){.c4-setup-bar>span{transition:none}}
ddiff --git a/public/js/setup.js b/public/js/setup.js
nnew file mode 100644
iindex 0000000..cf5b429
---- /dev/null
++++ b/public/js/setup.js
@@@ -0,0 +1,16 @@
++// /setup's own script (loaded there only). Copy buttons (the guided setup's commands): the code beside the button goes to the clipboard, and the button says so.
++// Where the clipboard is refused, the command is selected instead, for the person to copy.
++(function () {
++ 'use strict';
++ document.addEventListener('click', function (e) {
++ var btn = e.target && e.target.closest ? e.target.closest('[data-c4-copy]') : null;
++ if (!btn) return;
++ var code = btn.parentNode && btn.parentNode.querySelector('code');
++ if (!code) return;
++ var text = code.textContent || '';
++ var said = function (word) { btn.textContent = word; window.setTimeout(function () { btn.textContent = 'Copy'; }, 1600); };
++ var select = function () { var r = document.createRange(); r.selectNodeContents(code); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r); said('Selected'); };
++ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { said('Copied'); }, select);
++ else select();
++ });
++})();
ddiff --git a/server/http/c4.js b/server/http/c4.js
iindex 4a815ab..4306c94 100644
---- a/server/http/c4.js
++++ b/server/http/c4.js
@@@ -93,12 +93,13 @@ function createC4Routes(ctx) {
const page = (req, res, title, body, extra = {}) => {
// A page may add its own stylesheet (the guided setup's), so c4.css stays the size every page pays for.
const css = ['css/c4.css', ...(Array.isArray(extra.css) ? extra.css : [])];
++ const own = Array.isArray(extra.scripts) ? extra.scripts : [];
if (viewer(req) && ctx.chatShell && !extra.index) {
const here = '/' + (String(req.path || '/').split('/')[1] || '');
-- return ctx.chatShell(req, res, { title: title || 'Codes', body: String(body), css, scripts: ['js/c4.js'], here });
++ return ctx.chatShell(req, res, { title: title || 'Codes', body: String(body), css, scripts: ['js/c4.js', ...own], here });
}
const wantsScript = req.path === '/' || /^\/tasks\/tsk_/.test(req.path) || /^\/(agents|machines|conductor)/.test(req.path);
-- return send(res, 200, { viewer: req.viewer, config, path: req.originalUrl, ...(title ? { title: `${title} · Codes` } : {}), scripts: wantsScript ? ['js/c4.js'] : [], body: raw(body), ...extra, css });
++ return send(res, 200, { viewer: req.viewer, config, path: req.originalUrl, ...(title ? { title: `${title} · Codes` } : {}), body: raw(body), ...extra, css, scripts: [...(wantsScript ? ['js/c4.js'] : []), ...own] });
};
const viewer = (req) => req.viewer && req.viewer.kind === 'user' && req.viewer.subject;
/**
@@@ -847,7 +848,7 @@ ${conductorSection}
// The guided setup (/setup): machine, agents, keys, accounts and choices, each step read live. Its home-page card
// reads the database only, through ctx.setupProgress (the chat app's home asks it).
const setupRoutes = require('./setup').createSetupRoutes(ctx, {
-- page: (req, res, title, body) => page(req, res, title, body, { css: ['css/setup.css'] }),
++ page: (req, res, title, body) => page(req, res, title, body, { css: ['css/setup.css'], scripts: ['js/setup.js'] }),
wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note, machinesOf, usageMeter, SAFETY_OPTIONS, YOUR_AI, PROVIDER_NAMES,
});
r.use(setupRoutes);
ddiff --git a/server/http/setup.js b/server/http/setup.js
iindex 444133f..71a0c90 100644
---- a/server/http/setup.js
++++ b/server/http/setup.js
@@@ -33,7 +33,8 @@ function createSetupRoutes(ctx, kit) {
const db = store.db;
const r = asyncRouter();
-- const command = (text, label) => `<div class="c4-setup-cmd"><span class="c4-setup-cmd-label">${esc(label)}</span><code tabindex="0">${esc(text)}</code></div>`;
++ // A command to type on the machine: it wraps on a phone (never a page wider than the screen), and Copy copies it.
++ const command = (text, label) => `<div class="c4-setup-cmd"><span class="c4-setup-cmd-label">${esc(label)}</span><div class="c4-setup-cmd-box"><code tabindex="0">${esc(text)}</code><button type="button" class="c4-setup-copy" data-c4-copy>Copy</button></div></div>`;
/** Everything the page says, read once per view. */
async function readAll(req, res) {
ddiff --git a/test/setup.test.js b/test/setup.test.js
iindex d7be539..a1ddb25 100644
---- a/test/setup.test.js
++++ b/test/setup.test.js
@@@ -38,6 +38,13 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
assert.match(res.text, /curl -fsSL https:\/\/openvibe\.bot\/install \| sh -s -- --worker/);
assert.match(res.text, /npm install -g @openai\/codex/, 'an agent that is not there says how to install it');
assert.match(res.text, /href="\/css\/setup\.css|css\/setup\.css/, 'the page brings its own stylesheet');
++ assert.match(res.text, /js\/setup\.js/, 'and its own script (the Copy buttons), which no other page pays for');
++ assert.match(res.text, /<code tabindex="0">curl -fsSL[^<]*<\/code><button type="button" class="c4-setup-copy" data-c4-copy>Copy<\/button>/, 'a command has its Copy button');
++ // A phone never gets a page wider than its screen (2026-11-03: the installer's one long line widened the
++ // whole grid to 595 px at 390): the page's columns may shrink, and a command wraps.
++ const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'setup.css'), 'utf8');
++ for (const rule of ['.c4-setup{display:grid;grid-template-columns:minmax(0,1fr)', '.c4-setup-head{display:grid;grid-template-columns:minmax(0,1fr)', '.c4-setup-body{margin:14px 0 0 54px;display:grid;grid-template-columns:minmax(0,1fr)']) assert.ok(css.includes(rule), rule);
++ assert.match(css, /\.c4-setup-cmd code\{[^}]*white-space:pre-wrap;overflow-wrap:anywhere/);
const home = await t.get('/', { as: ada });
assert.match(home.text, /class="ch-setup-nudge" href="\/setup"/);
assert.match(home.text, /0 of 3 done · next: Your machine/);