openvibers/openvibe.live Commits
Live passes the browser check: paste screenshots from openvibe.community load (img-src), and the chat resize grip is a full ARIA separator (orientation and value) (#45)
Changes
@@@ -35,6 +35,20 @@function reset(mode) { write(MODES[mode].key, 0); setSize(mode, 0); settle(); }++ // A focusable separator is a value control (WAI-ARIA): it says its orientation and the size it sets, in pixels,++ // kept current through every drag, key, reset and layout change.++ function sync(g, sidebar) {++ if (!g || !sidebar) return;++ const mode = modeOf(); const m = MODES[mode]; const r = sidebar.getBoundingClientRect();++ const now = Math.round(m.axis === 'x' ? r.width : r.height);++ g.setAttribute('aria-orientation', m.axis === 'x' ? 'vertical' : 'horizontal');++ g.setAttribute('aria-valuemin', String(m.min));++ g.setAttribute('aria-valuemax', String(Math.round(Math.max(m.min, m.max()))));++ g.setAttribute('aria-valuenow', String(clamp(now, m.min, Math.max(m.min, m.max()))));++ g.setAttribute('aria-valuetext', `Chat ${now} pixels ${m.axis === 'x' ? 'wide' : 'tall'}`);++ }++ function syncAll() { document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach((s) => sync(s._ovGrip, s)); }++function bind(sidebar) {if (!sidebar || sidebar._ovGrip) return;const g = document.createElement('div');@@@ -45,6 +59,7 @@g.title = 'Drag to resize the chat · double-click for the default size';sidebar.prepend(g);sidebar._ovGrip = g;++ sync(g, sidebar);let drag = null, lastTap = 0;g.addEventListener('pointerdown', (e) => {@@@ -64,6 +79,7 @@// The sheet may go below its minimum while dragging: letting go there closes it.drag.cur = Math.round(Math.max(drag.mode === 'sheet' ? 60 : m.min, Math.min(m.max(), drag.from + delta)));const p = page(); if (p) p.style.setProperty(m.prop, `${drag.cur}px`);++ sync(g, sidebar);});const end = (e) => {if (!drag || (e && e.pointerId !== drag.id)) return;@@@ -85,31 +101,34 @@}write(m.key, clamp(d.cur, m.min, m.max()));settle();++ sync(g, sidebar);};g.addEventListener('pointerup', end);g.addEventListener('pointercancel', end);g.addEventListener('lostpointercapture', end);-- g.addEventListener('dblclick', () => reset(modeOf()));++ g.addEventListener('dblclick', () => { reset(modeOf()); sync(g, sidebar); });g.addEventListener('keydown', (e) => {const mode = modeOf(); const m = MODES[mode];-- if (e.key === 'Enter') { e.preventDefault(); reset(mode); return; }++ if (e.key === 'Enter') { e.preventDefault(); reset(mode); sync(g, sidebar); return; }const grow = m.axis === 'x' ? { ArrowLeft: 1, ArrowRight: -1 } : { ArrowUp: 1, ArrowDown: -1 };if (!(e.key in grow)) return;e.preventDefault();const r = sidebar.getBoundingClientRect();const v = clamp((m.axis === 'x' ? r.width : r.height) + grow[e.key] * 24, m.min, m.max());setSize(mode, v); write(m.key, v); settle();++ sync(g, sidebar);});}function init() {applySaved();document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach(bind);++ syncAll();}init();document.addEventListener('ov:fragment', init);// A size saved on a wide window can be too wide for a smaller one: re-clamp when the window changes.let t = 0;-- window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(applySaved, 150); });++ window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(() => { applySaved(); syncAll(); }, 150); });window.ChannelChatResize = { init, reset: () => reset(modeOf()) };})();@@@ -371,7 +371,7 @@ app.use(helmet({// i.ytimg.com / i.vimeocdn.com carry media-request thumbnails. Without them// every queued item renders as a broken image, which is what the media// request tab was doing for every YouTube link.-- imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com"],++ imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com", "https://openvibe.community"], // paste screenshots (OpenVibe.Community owns pastes)// https://cloudflareinsights.com is where Cloudflare Web Analytics' beacon (script-src static.cloudflareinsights.com) reports.connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://openvibe.events", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", "https://cloudflareinsights.com", whipConnectSrc],// VODs/clips play from openvibe.media (the /api proxies 302 there), which may
@@ -35,6 +35,20 @@
@@ -35,6 +35,20 @@
function reset(mode) { write(MODES[mode].key, 0); setSize(mode, 0); settle(); }
function reset(mode) { write(MODES[mode].key, 0); setSize(mode, 0); settle(); }
+ // A focusable separator is a value control (WAI-ARIA): it says its orientation and the size it sets, in pixels,
+ // kept current through every drag, key, reset and layout change.
+ function sync(g, sidebar) {
+ if (!g || !sidebar) return;
+ const mode = modeOf(); const m = MODES[mode]; const r = sidebar.getBoundingClientRect();
+ const now = Math.round(m.axis === 'x' ? r.width : r.height);
+ g.setAttribute('aria-orientation', m.axis === 'x' ? 'vertical' : 'horizontal');
+ g.setAttribute('aria-valuemin', String(m.min));
+ g.setAttribute('aria-valuemax', String(Math.round(Math.max(m.min, m.max()))));
+ g.setAttribute('aria-valuenow', String(clamp(now, m.min, Math.max(m.min, m.max()))));
+ g.setAttribute('aria-valuetext', `Chat ${now} pixels ${m.axis === 'x' ? 'wide' : 'tall'}`);
+ }
+ function syncAll() { document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach((s) => sync(s._ovGrip, s)); }
+
function bind(sidebar) {
function bind(sidebar) {
if (!sidebar || sidebar._ovGrip) return;
if (!sidebar || sidebar._ovGrip) return;
const g = document.createElement('div');
const g = document.createElement('div');
@@ -45,6 +59,7 @@
@@ -45,6 +59,7 @@
g.title = 'Drag to resize the chat · double-click for the default size';
g.title = 'Drag to resize the chat · double-click for the default size';
sidebar.prepend(g);
sidebar.prepend(g);
sidebar._ovGrip = g;
sidebar._ovGrip = g;
+ sync(g, sidebar);
let drag = null, lastTap = 0;
let drag = null, lastTap = 0;
g.addEventListener('pointerdown', (e) => {
g.addEventListener('pointerdown', (e) => {
@@ -64,6 +79,7 @@
@@ -64,6 +79,7 @@
// The sheet may go below its minimum while dragging: letting go there closes it.
// The sheet may go below its minimum while dragging: letting go there closes it.
drag.cur = Math.round(Math.max(drag.mode === 'sheet' ? 60 : m.min, Math.min(m.max(), drag.from + delta)));
drag.cur = Math.round(Math.max(drag.mode === 'sheet' ? 60 : m.min, Math.min(m.max(), drag.from + delta)));
const p = page(); if (p) p.style.setProperty(m.prop, `${drag.cur}px`);
const p = page(); if (p) p.style.setProperty(m.prop, `${drag.cur}px`);
+ sync(g, sidebar);
});
});
const end = (e) => {
const end = (e) => {
if (!drag || (e && e.pointerId !== drag.id)) return;
if (!drag || (e && e.pointerId !== drag.id)) return;
@@ -85,31 +101,34 @@
@@ -85,31 +101,34 @@
}
}
write(m.key, clamp(d.cur, m.min, m.max()));
write(m.key, clamp(d.cur, m.min, m.max()));
settle();
settle();
+ sync(g, sidebar);
};
};
g.addEventListener('pointerup', end);
g.addEventListener('pointerup', end);
g.addEventListener('pointercancel', end);
g.addEventListener('pointercancel', end);
g.addEventListener('lostpointercapture', end);
g.addEventListener('lostpointercapture', end);
- g.addEventListener('dblclick', () => reset(modeOf()));
+ g.addEventListener('dblclick', () => { reset(modeOf()); sync(g, sidebar); });
g.addEventListener('keydown', (e) => {
g.addEventListener('keydown', (e) => {
const mode = modeOf(); const m = MODES[mode];
const mode = modeOf(); const m = MODES[mode];
- if (e.key === 'Enter') { e.preventDefault(); reset(mode); return; }
+ if (e.key === 'Enter') { e.preventDefault(); reset(mode); sync(g, sidebar); return; }
const grow = m.axis === 'x' ? { ArrowLeft: 1, ArrowRight: -1 } : { ArrowUp: 1, ArrowDown: -1 };
const grow = m.axis === 'x' ? { ArrowLeft: 1, ArrowRight: -1 } : { ArrowUp: 1, ArrowDown: -1 };
if (!(e.key in grow)) return;
if (!(e.key in grow)) return;
e.preventDefault();
e.preventDefault();
const r = sidebar.getBoundingClientRect();
const r = sidebar.getBoundingClientRect();
const v = clamp((m.axis === 'x' ? r.width : r.height) + grow[e.key] * 24, m.min, m.max());
const v = clamp((m.axis === 'x' ? r.width : r.height) + grow[e.key] * 24, m.min, m.max());
setSize(mode, v); write(m.key, v); settle();
setSize(mode, v); write(m.key, v); settle();
+ sync(g, sidebar);
});
});
}
}
function init() {
function init() {
applySaved();
applySaved();
document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach(bind);
document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach(bind);
+ syncAll();
}
}
init();
init();
document.addEventListener('ov:fragment', init);
document.addEventListener('ov:fragment', init);
// A size saved on a wide window can be too wide for a smaller one: re-clamp when the window changes.
// A size saved on a wide window can be too wide for a smaller one: re-clamp when the window changes.
let t = 0;
let t = 0;
- window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(applySaved, 150); });
+ window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(() => { applySaved(); syncAll(); }, 150); });
window.ChannelChatResize = { init, reset: () => reset(modeOf()) };
window.ChannelChatResize = { init, reset: () => reset(modeOf()) };
})();
})();
@@ -371,7 +371,7 @@ app.use(helmet({
@@ -371,7 +371,7 @@ app.use(helmet({
// i.ytimg.com / i.vimeocdn.com carry media-request thumbnails. Without them
// i.ytimg.com / i.vimeocdn.com carry media-request thumbnails. Without them
// every queued item renders as a broken image, which is what the media
// every queued item renders as a broken image, which is what the media
// request tab was doing for every YouTube link.
// request tab was doing for every YouTube link.
- imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com"],
+ imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com", "https://openvibe.community"], // paste screenshots (OpenVibe.Community owns pastes)
// https://cloudflareinsights.com is where Cloudflare Web Analytics' beacon (script-src static.cloudflareinsights.com) reports.
// https://cloudflareinsights.com is where Cloudflare Web Analytics' beacon (script-src static.cloudflareinsights.com) reports.
connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://openvibe.events", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", "https://cloudflareinsights.com", whipConnectSrc],
connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://openvibe.events", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", "https://cloudflareinsights.com", whipConnectSrc],
// VODs/clips play from openvibe.media (the /api proxies 302 there), which may
// VODs/clips play from openvibe.media (the /api proxies 302 there), which may