openvibers/openvibe.chat Commits

The three upload dialogs are one sheet; the widget picker is readable on a light page (#459)

OP OpenVibers committed 4 days ago · 9614854e14 · 6 files, +58 −18

Vibe and the support inbox use the kit sheet (a bottom sheet on a phone, a centred dialog on a computer, a head that
swipes down, the kit icon button to close). The embedded widget has no kit: its stylesheet draws the same sheet from
the bottom of its frame, and gives the kit picker the widget colours. On a light page the picker label was close to
invisible there: it reads --text-primary, which the widget never set.

Changes

ddiff --git a/public/web/embed.css b/public/web/embed.css
iindex 189f4c1..0f110ce 100644
---- a/public/web/embed.css
++++ b/public/web/embed.css
@@@ -2,6 +2,9 @@
dark by the visitor's own preference; the inbox's colour (--accent, set on <body>) marks its buttons and their bubbles. */
:root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }
@media (prefers-color-scheme: dark) { :root { --bg: #0b1020; --panel: #121a2c; --text: #e8edf6; --muted: #98a6bd; --line: #223049; --mine: #1a2440; color-scheme: dark; } }
++/* The kit's picker and progress (openvibe-shared ov-upload.css) read the network's colour names: give them the widget's,
++ so their words are readable on a light page as well as a dark one. */
++:root { --text-primary: var(--text); --text-secondary: var(--muted); --text-muted: var(--muted); --border: var(--line); }
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg); color: var(--text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
@@@ -90,9 +93,14 @@ a { color: var(--accent); }
.em-pic img { display: block; width: 240px; max-width: 100%; height: auto; max-height: 200px; border-radius: 12px; border: 1px solid var(--line); object-fit: cover; } /* a definite width: the link is as wide as the picture, so it lines up under its bubble */
/* The shared picker in a sheet: the panel's own colours, the kit's card and meter inside. */
--dialog.em-upload { width: min(420px, 94vw); padding: 14px; border: 1px solid var(--line); border-radius: 16px; background: var(--bg); color: var(--text); }
--dialog.em-upload::backdrop { background: rgba(0, 0, 0, .45); }
--.em-upload-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
--.em-upload-head h2 { margin: 0; font-size: 1rem; }
--.em-upload-head button { border: 0; background: transparent; color: inherit; font: inherit; font-size: 1.2rem; cursor: pointer; padding: 4px 8px; }
++/* Send a picture: a sheet from the bottom of the widget (it has no kit of its own): a head with a full-size close
++ button, the picker under it, room for the home bar. */
++dialog.em-upload { width: 100%; max-width: 100%; max-height: 85%; margin: auto 0 0; padding: 0; box-sizing: border-box; border: 0; border-top: 1px solid var(--line); border-radius: 16px 16px 0 0; background: var(--bg); color: var(--text); overflow: hidden; }
++dialog.em-upload[open] { display: flex; flex-direction: column; }
++dialog.em-upload::backdrop { background: rgba(0, 0, 0, .5); }
++.em-upload-head { display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 16px; border-bottom: 1px solid var(--line); }
++.em-upload-head h2 { flex: 1; margin: 0; font-size: 1rem; }
++.em-upload-head button { display: inline-grid; place-items: center; width: 44px; height: 44px; flex: none; border: 0; border-radius: 12px; background: transparent; color: inherit; cursor: pointer; }
++.em-upload-head button:hover { background: color-mix(in srgb, currentColor 8%, transparent); }
++.em-upload-body { padding: 14px 16px calc(16px + env(safe-area-inset-bottom, 0px)); overflow-y: auto; }
[data-em-progress] { margin-top: 8px; }
ddiff --git a/public/web/inbox.css b/public/web/inbox.css
iindex c986d6f..39169a0 100644
---- a/public/web/inbox.css
++++ b/public/web/inbox.css
@@@ -87,9 +87,6 @@
.ib-screen button { justify-self: end; padding: 4px 10px; border-radius: 999px; border: 1px solid #374151; background: transparent; color: #e5e7eb; font: inherit; font-size: .8rem; cursor: pointer; }
/* The shared picker in a sheet for the inbox's reply box. */
--dialog.ib-upload { width: min(420px, 94vw); padding: 16px; border: 1px solid var(--oc-line); border-radius: 16px; background: var(--oc-panel); color: var(--oc-text); }
--dialog.ib-upload::backdrop { background: rgba(0, 0, 0, .45); }
--.ib-upload-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
--.ib-upload-head h2 { margin: 0; font-size: 1rem; }
--.ib-upload-head button { border: 0; background: transparent; color: inherit; font: inherit; font-size: 1.2rem; cursor: pointer; padding: 4px 8px; }
++/* Send a picture: the kit's sheet (a bottom sheet on a phone), a little narrower than its 560px on a computer. */
++.ovk-sheet.ib-upload { width: min(440px, 100vw); }
[data-ib-progress] { margin-top: 8px; }
ddiff --git a/public/web/vibe.css b/public/web/vibe.css
iindex 12321d0..f20b9df 100644
---- a/public/web/vibe.css
++++ b/public/web/vibe.css
@@@ -274,9 +274,6 @@
@media (max-width: 1179px) { .vbk .vbc-wide { display: none !important; } }
/* The shared picker in a sheet over the Vibe composer. */
--dialog.vb-upload { width: min(440px, 94vw); padding: 16px; border: 1px solid var(--oc-line); border-radius: 16px; background: var(--oc-panel); color: var(--oc-text); }
--dialog.vb-upload::backdrop { background: rgba(0, 0, 0, .45); }
--.vb-upload-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
--.vb-upload-head h2 { margin: 0; font-size: 1rem; }
--.vb-upload-head button { border: 0; background: transparent; color: inherit; font: inherit; font-size: 1.2rem; cursor: pointer; padding: 4px 8px; }
++/* Add a file: the kit's sheet (a bottom sheet on a phone), a little narrower than its 560px on a computer. */
++.ovk-sheet.vb-upload { width: min(440px, 100vw); }
[data-vb-progress] { max-width: var(--vb-col); margin: 0 auto 8px; }
ddiff --git a/server/web/inbox-pages.js b/server/web/inbox-pages.js
iindex dfde06d..06878cd 100644
---- a/server/web/inbox-pages.js
++++ b/server/web/inbox-pages.js
@@@ -69,7 +69,7 @@ function embedHtml(inbox) {
${pictures.enabled() ? '<button type="button" class="em-attach" id="em-attach" aria-label="Send a picture" title="Send a picture"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="3"/><circle cx="9" cy="10" r="2"/><path d="m21 16-5-5-9 9"/></svg></button>' : ''}
<textarea id="em-input" rows="1" maxlength="4000" placeholder="Write a message" aria-label="Your message"></textarea>
<button type="submit" aria-label="Send">&#8593;</button></form>
--${pictures.enabled() ? `<dialog class="em-upload" data-em-upload-dialog aria-labelledby="em-upload-h"><div class="em-upload-head"><h2 id="em-upload-h">Send a picture</h2><button type="button" data-em-upload-close aria-label="Close">✕</button></div><div data-em-upload data-accept="image/jpeg,image/png,image/gif,image/webp,image/avif"></div><div data-em-progress hidden></div></dialog>` : ''}
++${pictures.enabled() ? `<dialog class="em-upload" data-em-upload-dialog aria-labelledby="em-upload-h"><div class="em-upload-head"><h2 id="em-upload-h">Send a picture</h2><button type="button" data-em-upload-close aria-label="Close"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div><div class="em-upload-body"><div data-em-upload data-accept="image/jpeg,image/png,image/gif,image/webp,image/avif"></div><div data-em-progress hidden></div></div></dialog>` : ''}
<p class="em-foot">Chat by <a href="https://openvibe.chat" target="_blank" rel="noopener">OpenVibe</a>${Number(inbox.ai_enabled) ? ' · Vibe can get things wrong' : ''}<span id="em-account"></span></p>
</div>
<noscript><p style="padding:16px">This chat needs JavaScript.</p></noscript>
@@@ -251,7 +251,7 @@ ${list}
// The reply box: the kit's composer, with a picture, a translation and the call above it while one is open.
function replyBox({ inbox, current, actor, saved }) {
-- const pic = pictures.enabled() ? `<button type="button" class="ovt-btn ib-pic-btn" id="ib-pic" title="Send a picture" aria-label="Send a picture">${thread.icon('attach')}<span class="ovt-sr">Send a picture</span></button><dialog class="ib-upload" data-ib-upload-dialog aria-labelledby="ib-upload-h"><div class="ib-upload-head"><h2 id="ib-upload-h">Send a picture</h2><button type="button" data-ib-upload-close aria-label="Close">✕</button></div><div data-ib-upload data-accept="image/jpeg,image/png,image/gif,image/webp,image/avif"></div><div data-ib-progress hidden></div></dialog>` : '';
++ const pic = pictures.enabled() ? `<button type="button" class="ovt-btn ib-pic-btn" id="ib-pic" title="Send a picture" aria-label="Send a picture">${thread.icon('attach')}<span class="ovt-sr">Send a picture</span></button><dialog class="ovk-sheet ib-upload" data-ib-upload-dialog aria-labelledby="ib-upload-h"><div class="ovk-sheet-head" data-ov-sheet-head><h2 id="ib-upload-h">Send a picture</h2><button type="button" class="ovk-icon-btn" data-ib-upload-close aria-label="Close">${thread.icon('close')}</button></div><div class="ovk-sheet-body"><div data-ib-upload data-accept="image/jpeg,image/png,image/gif,image/webp,image/avif"></div><div data-ib-progress hidden></div></div></dialog>` : '';
const tr = `<label class="ib-tr-pick" hidden><span>Translate my reply into</span> <select id="ib-tr-to"><option value="">a language…</option>${TR_LANGS.map(([v, l]) => `<option value="${v}">${l}</option>`).join('')}</select></label>`;
const call = `<figure class="ib-screen" id="ib-screen" hidden><figcaption id="ib-screen-who"></figcaption><video id="ib-screen-video" autoplay playsinline muted></video><button type="button" id="ib-screen-full">Full screen</button></figure><div class="ib-callbar" id="ib-callbar" role="group" aria-label="Voice call" hidden><span class="ib-call-text" id="ib-call-text"></span><button type="button" id="ib-call-join" hidden>Join the call</button><button type="button" id="ib-call-mute" aria-pressed="false" hidden>Mute</button><button type="button" class="ib-call-end" id="ib-call-end">End call</button></div>`;
return call + thread.composer({
ddiff --git a/server/web/vibe-pages.js b/server/web/vibe-pages.js
iindex 4975ca8..00cb63b 100644
---- a/server/web/vibe-pages.js
++++ b/server/web/vibe-pages.js
@@@ -207,7 +207,7 @@ function composer({ slug, signedIn, enabled, prefill = '', engine = null, models
const label = pics ? 'Add a picture or a text file' : 'Add a text file';
const attach = signedIn && enabled
? `<button class="ovt-btn vb-attach" type="button" data-vb-attach aria-label="${label}" title="${label}">${ICON.attach}</button>
--<dialog class="vb-upload" data-vb-upload-dialog aria-labelledby="vb-upload-h"><div class="vb-upload-head"><h2 id="vb-upload-h">${pics ? 'Add a picture or a text file' : 'Add a text file'}</h2><button type="button" class="vb-upload-close" data-vb-upload-close aria-label="Close">✕</button></div><div data-vb-upload data-accept="${pics ? 'image/jpeg,image/png,image/gif,image/webp,image/avif,' : ''}${TEXT_FILES}"></div></dialog>`
++<dialog class="ovk-sheet vb-upload" data-vb-upload-dialog aria-labelledby="vb-upload-h"><div class="ovk-sheet-head" data-ov-sheet-head><h2 id="vb-upload-h">${pics ? 'Add a picture or a text file' : 'Add a text file'}</h2><button type="button" class="ovk-icon-btn" data-vb-upload-close aria-label="Close">${thread.icon('close')}</button></div><div class="ovk-sheet-body"><div data-vb-upload data-accept="${pics ? 'image/jpeg,image/png,image/gif,image/webp,image/avif,' : ''}${TEXT_FILES}"></div></div></dialog>`
: '';
const modes = `<fieldset class="vb-modes" aria-label="What Vibe does with it">${MODES.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>`;
const fine = !enabled ? 'Vibe is resting right now. Try again later.' : engine && engine.own ? `Vibe thinks with ${esc(engine.you)} (<a href="/settings#oc-ai-key">change</a>). It can get things wrong: check what matters.` : 'Vibe can get things wrong. Check what matters.';
ddiff --git a/test/upload-sheets.test.js b/test/upload-sheets.test.js
nnew file mode 100644
iindex 0000000..9732413
---- /dev/null
++++ b/test/upload-sheets.test.js
@@@ -0,0 +1,38 @@
++'use strict';
++/**
++ * The three places Chat asks for a file (Vibe's "Add a picture or a text file", the support inbox's "Send a picture",
++ * and the embedded widget's) are one sheet:
++ * - Vibe and the inbox are on the network's kit, so theirs is the kit's sheet (a bottom sheet on a phone, a centred
++ * dialog on a computer; its head can be swiped down), with the kit's icon button to close it;
++ * - the widget on someone else's site has no kit, so embed.css draws the same sheet from the bottom of its frame,
++ * and gives the kit's picker the widget's own colours (its label was near-invisible on a light page: the picker
++ * reads --text-primary, which the widget never set).
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { suite } = require('./helpers');
++const t = suite('upload-sheets');
++const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
++
++t('Vibe and the inbox use the kit sheet', () => {
++ const vibe = read('server/web/vibe-pages.js'), inbox = read('server/web/inbox-pages.js');
++ assert.match(vibe, /<dialog class="ovk-sheet vb-upload" data-vb-upload-dialog[^>]*><div class="ovk-sheet-head" data-ov-sheet-head>/);
++ assert.match(vibe, /<button type="button" class="ovk-icon-btn" data-vb-upload-close aria-label="Close">\$\{thread\.icon\('close'\)\}<\/button>/);
++ assert.match(vibe, /<div class="ovk-sheet-body"><div data-vb-upload /);
++ assert.match(inbox, /<dialog class="ovk-sheet ib-upload" data-ib-upload-dialog[^>]*><div class="ovk-sheet-head" data-ov-sheet-head>/);
++ assert.match(inbox, /<div class="ovk-sheet-body"><div data-ib-upload [^>]*><\/div><div data-ib-progress hidden><\/div><\/div><\/dialog>/);
++ for (const f of ['public/web/vibe.css', 'public/web/inbox.css']) assert.ok(!/upload-head/.test(read(f)), `${f}: no second copy of the sheet's styles`);
++});
++
++t('the widget draws its own sheet and lends the picker its colours', () => {
++ const css = read('public/web/embed.css'), page = read('server/web/inbox-pages.js');
++ assert.match(css, /dialog\.em-upload \{[^}]*margin: auto 0 0;[^}]*border-radius: 16px 16px 0 0;/, 'from the bottom of the frame');
++ assert.match(css, /\.em-upload-head button \{[^}]*width: 44px; height: 44px;/, 'a full-size close button');
++ assert.match(css, /\.em-upload-body \{[^}]*env\(safe-area-inset-bottom/, 'room for the home bar');
++ assert.match(css, /:root \{ --text-primary: var\(--text\); --text-secondary: var\(--muted\); --text-muted: var\(--muted\); --border: var\(--line\); \}/, 'the picker reads the widget\'s colours');
++ assert.match(page, /<dialog class="em-upload"[^>]*><div class="em-upload-head"><h2 id="em-upload-h">Send a picture<\/h2><button type="button" data-em-upload-close aria-label="Close"><svg /);
++ assert.ok(!page.includes('aria-label="Close">✕'), 'no text glyph for a close button');
++});
++
++t.run();