openvibers/openvibe.chat Commits
The three upload dialogs are one sheet; the widget picker is readable on a light page (#459)
Changes
@@@ -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; }@@@ -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; }@@@ -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; }@@@ -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">↑</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({@@@ -207,7 +207,7 @@ function composer({ slug, signedIn, enabled, prefill = '', engine = null, modelsconst 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.';nnew file mode 100644@@@ -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();
@@ -2,6 +2,9 @@
@@ -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. */
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; }
: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; } }
@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; }
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
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; }
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); }
@@ -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 */
.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. */
/* 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; }
[data-em-progress] { margin-top: 8px; }
@@ -87,9 +87,6 @@
@@ -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; }
.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. */
/* 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; }
[data-ib-progress] { margin-top: 8px; }
@@ -274,9 +274,6 @@
@@ -274,9 +274,6 @@
@media (max-width: 1179px) { .vbk .vbc-wide { display: none !important; } }
@media (max-width: 1179px) { .vbk .vbc-wide { display: none !important; } }
/* The shared picker in a sheet over the Vibe composer. */
/* 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; }
[data-vb-progress] { max-width: var(--vb-col); margin: 0 auto 8px; }
@@ -69,7 +69,7 @@ function embedHtml(inbox) {
@@ -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>' : ''}
${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>
<textarea id="em-input" rows="1" maxlength="4000" placeholder="Write a message" aria-label="Your message"></textarea>
<button type="submit" aria-label="Send">↑</button></form>
<button type="submit" aria-label="Send">↑</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>
<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>
</div>
<noscript><p style="padding:16px">This chat needs JavaScript.</p></noscript>
<noscript><p style="padding:16px">This chat needs JavaScript.</p></noscript>
@@ -251,7 +251,7 @@ ${list}
@@ -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.
// 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 }) {
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 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>`;
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({
return call + thread.composer({
@@ -207,7 +207,7 @@ function composer({ slug, signedIn, enabled, prefill = '', engine = null, models
@@ -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 label = pics ? 'Add a picture or a text file' : 'Add a text file';
const attach = signedIn && enabled
const attach = signedIn && enabled
? `<button class="ovt-btn vb-attach" type="button" data-vb-attach aria-label="${label}" title="${label}">${ICON.attach}</button>
? `<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 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.';
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.';
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -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();