openvibers/openvibe.live Commits
Channel chat resizes in every layout, with a mouse, a finger or the keyboard; deploy cards in chat fold to one line.
Changes
@@@ -1464,8 +1464,7 @@ body.controls-docked .messenger-toggle { z-index: 1100; }/* ── Hide / show the chat (desktop) ─────────────────────────── */.chat-header .ch-chat-collapse { margin-right: 2px; }@media (min-width: 769px) {-- #page-channel.chat-collapsed .stream-layout > .chat-sidebar,-- #page-channel.chat-collapsed .chat-resize-handle { display: none !important; }++ #page-channel.chat-collapsed .stream-layout > .chat-sidebar { display: none !important; }#page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: chChatReveal .32s cubic-bezier(.22,1,.36,1); }}@keyframes chChatReveal { from { opacity: 0; transform: translateX(36px); } to { opacity: 1; transform: none; } }@@@ -1482,3 +1481,31 @@ body.controls-docked .messenger-toggle { z-index: 1100; }.ch-chat-reopen[hidden] { display: none; }@media (max-width: 768px) { .ch-chat-collapse, .ch-chat-reopen { display: none !important; } }@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none; } }++++/* ── Resizing the chat (public/js/channel-chat-resize.js) ─────++ One grip per chat: on its left edge beside the stream (width), on its top edge as a bottom sheet (height). */++#page-channel .stream-layout > .chat-sidebar { width: var(--chat-w); }++@media (max-width: 1180px) and (orientation: landscape) { #page-channel .stream-layout > .chat-sidebar { width: var(--chat-ow, min(420px, 44vw)); } }++@media (max-width: 1180px) and (orientation: portrait) { #page-channel .stream-layout > .chat-sidebar { width: 100%; height: var(--chat-sheet-h, 55dvh); max-height: 92dvh; } }++#page-channel.chat-resizing, #page-channel.chat-resizing * { user-select: none; }++#page-channel.chat-resizing .chat-sidebar { transition: none !important; }++#page-channel.chat-resizing iframe, #page-channel.chat-resizing video { pointer-events: none; }++.chat-grip { position: absolute; z-index: 30; touch-action: none; outline: none; }++.chat-grip::before, .chat-grip::after { content: ''; position: absolute; pointer-events: none; transition: opacity .15s, background-color .15s; }++@media (min-width: 1181px), (max-width: 1180px) and (orientation: landscape) {++ .chat-grip { top: 0; bottom: 0; left: -6px; width: 12px; cursor: col-resize; }++ .chat-grip::before { left: 5px; top: 0; bottom: 0; width: 2px; background: var(--accent); opacity: 0; }++ .chat-grip::after { left: 4px; top: 50%; width: 4px; height: 44px; margin-top: -22px; border-radius: 4px; background: color-mix(in srgb, var(--text-muted) 60%, transparent); opacity: 0; }++ .chat-sidebar:hover > .chat-grip::after { opacity: .75; }++ .chat-grip:hover::before, .chat-grip.dragging::before, .chat-grip:focus-visible::before { opacity: .55; }++ .chat-grip:hover::after, .chat-grip.dragging::after, .chat-grip:focus-visible::after { opacity: 1; background: var(--accent); }++}++@media (max-width: 1180px) and (orientation: portrait) {++ .chat-grip { top: 0; left: 0; right: 0; height: 24px; cursor: row-resize; }++ .chat-grip::after { left: 50%; top: 8px; width: 44px; height: 5px; margin-left: -22px; border-radius: 5px; background: color-mix(in srgb, var(--text-muted) 60%, transparent); }++ .chat-grip:hover::after, .chat-grip.dragging::after, .chat-grip:focus-visible::after { background: var(--accent); }++ /* The grip draws the sheet's handle now (the header's own pill was only a picture of one). */++ #page-channel .chat-sidebar .chat-header::before { display: none; }++ #page-channel .chat-sidebar .chat-header { padding-top: 18px; }++}++@media (hover: none) { .chat-sidebar > .chat-grip::after { opacity: .75; } }@@@ -117,6 +117,7 @@],"js": ["/js/channel-socials.js",++ "/js/channel-chat-resize.js","/js/app-channel.js"]},@@@ -142,7 +142,6 @@<div class="ch-weather-widget ch-weather-full" id="ch-weather-widget" style="display:none"></div></div><!-- Chat Resize Handle -->-- <div class="chat-resize-handle" id="chat-resize-handle" title="Drag to resize chat"></div><!-- Chat Sidebar --><div class="chat-sidebar" id="chat-sidebar"><div class="chat-header">nnew file mode 100644@@@ -0,0 +1,115 @@++/*++ OpenVibe.Live — resize the channel page's chat with a mouse, a finger, a pen or the keyboard, in each layout it has:++++ side 1181px and wider: the column beside the stream; the grip on its left edge sets the width++ overlay 1180px and narrower, landscape: the panel over the right side; the same grip sets its width++ sheet 1180px and narrower, portrait: the bottom sheet; the grip on its top edge sets the height, and++ letting go most of the way down closes the sheet++++ Double-click or double-tap a grip (or press Enter on it) for the default size; arrow keys move it 24px.++ Each layout remembers its own size (localStorage), applied as CSS variables on #page-channel, which++ public/css/features/channel.css reads: --chat-w, --chat-ow, --chat-sheet-h.++*/++(function () {++ 'use strict';++ const WIDE = window.matchMedia('(min-width: 1181px)');++ const LAND = window.matchMedia('(orientation: landscape)');++ const MODES = {++ side: { key: 'openvibe_chat_width', prop: '--chat-w', axis: 'x', min: 260, max: () => Math.min(window.innerWidth * 0.5, 760) },++ overlay: { key: 'openvibe_chat_overlay_w', prop: '--chat-ow', axis: 'x', min: 260, max: () => window.innerWidth * 0.7 },++ sheet: { key: 'openvibe_chat_sheet_h', prop: '--chat-sheet-h', axis: 'y', min: 180, max: () => window.innerHeight * 0.92 },++ };++ const page = () => document.getElementById('page-channel');++ const modeOf = () => (WIDE.matches ? 'side' : LAND.matches ? 'overlay' : 'sheet');++ const clamp = (v, a, b) => Math.round(Math.max(a, Math.min(b, v)));++ const read = (k) => { try { return parseInt(localStorage.getItem(k), 10) || 0; } catch { return 0; } };++ const write = (k, v) => { try { if (v) localStorage.setItem(k, String(Math.round(v))); else localStorage.removeItem(k); } catch { /* private mode */ } };++++ function setSize(mode, v) {++ const p = page(); const m = MODES[mode];++ if (!p) return;++ if (v) p.style.setProperty(m.prop, `${clamp(v, m.min, m.max())}px`); else p.style.removeProperty(m.prop);++ }++ function applySaved() { for (const mode of Object.keys(MODES)) setSize(mode, read(MODES[mode].key)); }++ function settle() { window.dispatchEvent(new Event('resize')); } // the player and the diagrams re-measure++++ function reset(mode) { write(MODES[mode].key, 0); setSize(mode, 0); settle(); }++++ function bind(sidebar) {++ if (!sidebar || sidebar._ovGrip) return;++ const g = document.createElement('div');++ g.className = 'chat-grip';++ g.setAttribute('role', 'separator');++ g.setAttribute('aria-label', 'Resize chat');++ g.tabIndex = 0;++ g.title = 'Drag to resize the chat · double-click for the default size';++ sidebar.prepend(g);++ sidebar._ovGrip = g;++++ let drag = null, lastTap = 0;++ g.addEventListener('pointerdown', (e) => {++ if (e.pointerType === 'mouse' && e.button !== 0) return;++ const mode = modeOf(); const r = sidebar.getBoundingClientRect();++ drag = { mode, id: e.pointerId, x: e.clientX, y: e.clientY, from: MODES[mode].axis === 'x' ? r.width : r.height, cur: 0, moved: false };++ try { g.setPointerCapture(e.pointerId); } catch { /* */ }++ g.classList.add('dragging');++ page()?.classList.add('chat-resizing');++ e.preventDefault();++ });++ g.addEventListener('pointermove', (e) => {++ if (!drag || e.pointerId !== drag.id) return;++ const m = MODES[drag.mode];++ const delta = m.axis === 'x' ? drag.x - e.clientX : drag.y - e.clientY; // the chat sits right / at the bottom++ if (Math.abs(delta) > 3) drag.moved = true;++ // 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`);++ });++ const end = (e) => {++ if (!drag || (e && e.pointerId !== drag.id)) return;++ const d = drag; drag = null;++ g.classList.remove('dragging');++ page()?.classList.remove('chat-resizing');++ if (!d.moved) {++ // A tap: two in a row restore the default size.++ const now = Date.now();++ if (now - lastTap < 320) { lastTap = 0; reset(d.mode); } else lastTap = now;++ return;++ }++ const m = MODES[d.mode];++ if (d.mode === 'sheet' && d.cur < m.min) {++ setSize('sheet', read(m.key)); // keep the last real height for next time++ const s = document.querySelector('#page-channel .chat-sidebar.mobile-chat-open');++ if (s && typeof window.toggleMobileChat === 'function') window.toggleMobileChat();++ return;++ }++ write(m.key, clamp(d.cur, m.min, m.max()));++ settle();++ };++ g.addEventListener('pointerup', end);++ g.addEventListener('pointercancel', end);++ g.addEventListener('lostpointercapture', end);++ g.addEventListener('dblclick', () => reset(modeOf()));++ g.addEventListener('keydown', (e) => {++ const mode = modeOf(); const m = MODES[mode];++ if (e.key === 'Enter') { e.preventDefault(); reset(mode); 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();++ });++ }++++ function init() {++ applySaved();++ document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach(bind);++ }++ 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.ChannelChatResize = { init, reset: () => reset(modeOf()) };++})();@@@ -3952,8 +3952,9 @@ function _deployCss() {if (document.getElementById('chat-deploy-css')) return;const st = document.createElement('style'); st.id = 'chat-deploy-css';st.textContent = `.chat-msg.system.chat-deploy,.chat-msg.system.chat-deploy *{font-style:normal}--.chat-deploy{margin:8px 0;padding:10px 12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--accent,#3b82f6) 28%,transparent);background:color-mix(in srgb,var(--accent,#3b82f6) 6%,transparent);color:var(--text-secondary,#a8b3c4);font-size:.84rem;line-height:1.45;container-type:inline-size}--.chat-deploy-h{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;color:var(--text-primary,#e6edf7)}.chat-deploy-h b{font-weight:700}.chat-deploy-t{font-size:.76rem;color:var(--text-muted,#7d8aa0)}++.chat-deploy{margin:6px 0;padding:7px 10px;border-radius:12px;border:1px solid color-mix(in srgb,var(--accent,#3b82f6) 28%,transparent);background:color-mix(in srgb,var(--accent,#3b82f6) 6%,transparent);color:var(--text-secondary,#a8b3c4);font-size:.84rem;line-height:1.45;container-type:inline-size}++.chat-deploy-h{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;width:100%;margin:0;padding:0;border:0;background:none;font:inherit;text-align:left;cursor:pointer;color:var(--text-primary,#e6edf7)}.chat-deploy-h::after{content:'▾';margin-left:auto;align-self:center;font-size:.7rem;color:var(--text-muted,#7d8aa0);transition:transform .2s}.chat-deploy:not(.is-collapsed) .chat-deploy-h::after{transform:rotate(180deg)}.chat-deploy-h:focus-visible{outline:2px solid var(--accent,#3b82f6);outline-offset:3px;border-radius:6px}++.chat-deploy.is-collapsed ul,.chat-deploy.is-collapsed .chat-deploy-more,.chat-deploy.is-collapsed .chat-deploy-f a{display:none}.chat-deploy.is-collapsed .chat-deploy-f{margin-top:6px;padding-top:0;border-top:0}.chat-deploy.is-collapsed .chat-deploy-f:not(:has(.chat-deploy-reload)){display:none}.chat-deploy-h b{font-weight:700}.chat-deploy-t{font-size:.76rem;color:var(--text-muted,#7d8aa0)}.chat-deploy ul{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:4px}.chat-deploy li{display:grid;grid-template-columns:4.6em minmax(0,1fr);gap:8px;align-items:baseline}.chat-deploy li.is-hidden{display:none}.chat-deploy.is-open li.is-hidden{display:grid}.chat-deploy li a{font-family:ui-monospace,Menlo,monospace;font-size:.72rem;color:var(--accent-light,var(--accent,#60a5fa));text-decoration:none}@@@ -3986,14 +3987,19 @@ function renderDeployCard(data, opts = {}) {const id = data.id != null ? String(data.id) : '';let el = id ? container.querySelector('.chat-deploy[data-deploy-id="' + id.replace(/[^\w-]/g, '') + '"]') : null;const wasOpen = el && el.classList.contains('is-open');++ // One line until opened: a run of deploys is context, not the conversation (the list and notes are a tap away).++ const wasExpanded = el && !el.classList.contains('is-collapsed');if (!el) { el = document.createElement('div'); el.className = 'chat-msg system chat-deploy'; if (id) el.dataset.deployId = id.replace(/[^\w-]/g, ''); }const range = first.getTime() === last.getTime() || Math.abs(last - first) < 60000 ? _deployTime(last) : _deployTime(first, first.toDateString() !== last.toDateString()) + ' – ' + _deployTime(last, first.toDateString() !== last.toDateString());const deploys = Number(data.deploys) || 1;const rest = commits.length - SHOW;-- el.innerHTML = `<div class="chat-deploy-h"><b>🚀 ${commits.length} update${commits.length === 1 ? '' : 's'} shipped</b><span class="chat-deploy-t" title="${esc(last.toLocaleString())}">${esc(range)}${deploys > 1 ? ' · ' + deploys + ' deploys' : ''}</span></div>++ el.innerHTML = `<button type="button" class="chat-deploy-h" aria-expanded="${wasExpanded ? 'true' : 'false'}"><b>🚀 ${commits.length} update${commits.length === 1 ? '' : 's'} shipped</b><span class="chat-deploy-t" title="${esc(last.toLocaleString())}">${esc(range)}${deploys > 1 ? ' · ' + deploys + ' deploys' : ''}</span></button><ul>${commits.map((c, i) => `<li class="${i >= SHOW ? 'is-hidden' : ''}${fresh.has(c.hash) ? ' is-fresh' : ''}">${c.hash ? `<a href="https://github.com/OpenVibers/OpenVibe.Live/commit/${esc(c.hash)}" target="_blank" rel="noopener">${esc(c.short || String(c.hash).slice(0, 7))}</a>` : '<i></i>'}<span>${esc(c.subject)}</span></li>`).join('')}</ul><div class="chat-deploy-f">${rest > 0 ? `<button type="button" class="chat-deploy-more" aria-expanded="${wasOpen ? 'true' : 'false'}">${wasOpen ? 'Show fewer' : `Show ${rest} more`}</button>` : ''}<span class="chat-deploy-a"><a href="/updates" target="_blank" rel="noopener">Patch notes</a>${opts.live ? '<button type="button" class="chat-deploy-reload" title="Load the new version">Reload to update</button>' : ''}</span></div>`;if (wasOpen) el.classList.add('is-open');++ el.classList.toggle('is-collapsed', !wasExpanded);++ const head = el.querySelector('.chat-deploy-h');++ head.addEventListener('click', () => { const open = el.classList.toggle('is-collapsed') === false; head.setAttribute('aria-expanded', String(open)); });const more = el.querySelector('.chat-deploy-more');if (more) more.addEventListener('click', () => { const open = el.classList.toggle('is-open'); more.textContent = open ? 'Show fewer' : `Show ${rest} more`; more.setAttribute('aria-expanded', String(open)); });const reload = el.querySelector('.chat-deploy-reload');@@@ -7436,10 +7442,9 @@ function _fcwStopResize() {}// Page markup that arrives later (ov-loader.js fragments) gets the same one-time setup the--// boot-time markup got: font-size classes and scroll containment, the resize handle, GIF buttons.++// boot-time markup got: font-size classes and scroll containment, GIF buttons.document.addEventListener('ov:fragment', () => {try { applyChatSettings(); } catch { /* */ }-- try { initChatResize(); } catch { /* */ }if (typeof initGifPickers === 'function') { try { initGifPickers(); } catch { /* */ } }});@@@ -7670,60 +7675,7 @@ function closeChatUsersPanel(btn) {(window.handleChatLinkClick / showLinkContextMenu / OpenVibeChat.linkify),shared with the /kiosk page. */--/* ══════════════════════════════════════════════════════════════-- CHAT RESIZE HANDLE — drag to change chat sidebar width-- ══════════════════════════════════════════════════════════════ */--function initChatResize() {-- const handle = document.getElementById('chat-resize-handle');-- const sidebar = document.getElementById('chat-sidebar');-- if (!handle || !sidebar || handle._ovResizeBound) return;-- handle._ovResizeBound = true;---- const MIN_W = 250;-- const MAX_RATIO = 0.5; // max 50% of viewport---- // Restore saved width-- const saved = localStorage.getItem('openvibe_chat_width');-- if (saved) {-- const w = parseInt(saved, 10);-- if (w >= MIN_W && w <= window.innerWidth * MAX_RATIO) {-- sidebar.style.width = w + 'px';-- }-- }---- let dragging = false;-- let startX = 0;-- let startW = 0;---- handle.addEventListener('mousedown', (e) => {-- e.preventDefault();-- dragging = true;-- startX = e.clientX;-- startW = sidebar.offsetWidth;-- handle.classList.add('dragging');-- document.body.style.cursor = 'col-resize';-- document.body.style.userSelect = 'none';-- });---- document.addEventListener('mousemove', (e) => {-- if (!dragging) return;-- // Chat is on the right, so dragging left increases width-- const diff = startX - e.clientX;-- const maxW = window.innerWidth * MAX_RATIO;-- const newW = Math.max(MIN_W, Math.min(maxW, startW + diff));-- sidebar.style.width = newW + 'px';-- });---- document.addEventListener('mouseup', () => {-- if (!dragging) return;-- dragging = false;-- handle.classList.remove('dragging');-- document.body.style.cursor = '';-- document.body.style.userSelect = '';-- localStorage.setItem('openvibe_chat_width', sidebar.offsetWidth);-- });--}--initChatResize();++/* Resizing the channel page's chat (every layout, mouse and touch): public/js/channel-chat-resize.js. */// Reply / translate buttons and folded Friendly-chat groups, for every chat surface: one listener.(function _wireChatRowActions() {
@@ -1464,8 +1464,7 @@ body.controls-docked .messenger-toggle { z-index: 1100; }
@@ -1464,8 +1464,7 @@ body.controls-docked .messenger-toggle { z-index: 1100; }
/* ── Hide / show the chat (desktop) ─────────────────────────── */
/* ── Hide / show the chat (desktop) ─────────────────────────── */
.chat-header .ch-chat-collapse { margin-right: 2px; }
.chat-header .ch-chat-collapse { margin-right: 2px; }
@media (min-width: 769px) {
@media (min-width: 769px) {
- #page-channel.chat-collapsed .stream-layout > .chat-sidebar,
- #page-channel.chat-collapsed .chat-resize-handle { display: none !important; }
+ #page-channel.chat-collapsed .stream-layout > .chat-sidebar { display: none !important; }
#page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: chChatReveal .32s cubic-bezier(.22,1,.36,1); }
#page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: chChatReveal .32s cubic-bezier(.22,1,.36,1); }
}
}
@keyframes chChatReveal { from { opacity: 0; transform: translateX(36px); } to { opacity: 1; transform: none; } }
@keyframes chChatReveal { from { opacity: 0; transform: translateX(36px); } to { opacity: 1; transform: none; } }
@@ -1482,3 +1481,31 @@ body.controls-docked .messenger-toggle { z-index: 1100; }
@@ -1482,3 +1481,31 @@ body.controls-docked .messenger-toggle { z-index: 1100; }
.ch-chat-reopen[hidden] { display: none; }
.ch-chat-reopen[hidden] { display: none; }
@media (max-width: 768px) { .ch-chat-collapse, .ch-chat-reopen { display: none !important; } }
@media (max-width: 768px) { .ch-chat-collapse, .ch-chat-reopen { display: none !important; } }
@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none; } }
@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none; } }
+
+/* ── Resizing the chat (public/js/channel-chat-resize.js) ─────
+ One grip per chat: on its left edge beside the stream (width), on its top edge as a bottom sheet (height). */
+#page-channel .stream-layout > .chat-sidebar { width: var(--chat-w); }
+@media (max-width: 1180px) and (orientation: landscape) { #page-channel .stream-layout > .chat-sidebar { width: var(--chat-ow, min(420px, 44vw)); } }
+@media (max-width: 1180px) and (orientation: portrait) { #page-channel .stream-layout > .chat-sidebar { width: 100%; height: var(--chat-sheet-h, 55dvh); max-height: 92dvh; } }
+#page-channel.chat-resizing, #page-channel.chat-resizing * { user-select: none; }
+#page-channel.chat-resizing .chat-sidebar { transition: none !important; }
+#page-channel.chat-resizing iframe, #page-channel.chat-resizing video { pointer-events: none; }
+.chat-grip { position: absolute; z-index: 30; touch-action: none; outline: none; }
+.chat-grip::before, .chat-grip::after { content: ''; position: absolute; pointer-events: none; transition: opacity .15s, background-color .15s; }
+@media (min-width: 1181px), (max-width: 1180px) and (orientation: landscape) {
+ .chat-grip { top: 0; bottom: 0; left: -6px; width: 12px; cursor: col-resize; }
+ .chat-grip::before { left: 5px; top: 0; bottom: 0; width: 2px; background: var(--accent); opacity: 0; }
+ .chat-grip::after { left: 4px; top: 50%; width: 4px; height: 44px; margin-top: -22px; border-radius: 4px; background: color-mix(in srgb, var(--text-muted) 60%, transparent); opacity: 0; }
+ .chat-sidebar:hover > .chat-grip::after { opacity: .75; }
+ .chat-grip:hover::before, .chat-grip.dragging::before, .chat-grip:focus-visible::before { opacity: .55; }
+ .chat-grip:hover::after, .chat-grip.dragging::after, .chat-grip:focus-visible::after { opacity: 1; background: var(--accent); }
+}
+@media (max-width: 1180px) and (orientation: portrait) {
+ .chat-grip { top: 0; left: 0; right: 0; height: 24px; cursor: row-resize; }
+ .chat-grip::after { left: 50%; top: 8px; width: 44px; height: 5px; margin-left: -22px; border-radius: 5px; background: color-mix(in srgb, var(--text-muted) 60%, transparent); }
+ .chat-grip:hover::after, .chat-grip.dragging::after, .chat-grip:focus-visible::after { background: var(--accent); }
+ /* The grip draws the sheet's handle now (the header's own pill was only a picture of one). */
+ #page-channel .chat-sidebar .chat-header::before { display: none; }
+ #page-channel .chat-sidebar .chat-header { padding-top: 18px; }
+}
+@media (hover: none) { .chat-sidebar > .chat-grip::after { opacity: .75; } }
@@ -117,6 +117,7 @@
@@ -117,6 +117,7 @@
],
],
"js": [
"js": [
"/js/channel-socials.js",
"/js/channel-socials.js",
+ "/js/channel-chat-resize.js",
"/js/app-channel.js"
"/js/app-channel.js"
]
]
},
},
@@ -142,7 +142,6 @@
@@ -142,7 +142,6 @@
<div class="ch-weather-widget ch-weather-full" id="ch-weather-widget" style="display:none"></div>
<div class="ch-weather-widget ch-weather-full" id="ch-weather-widget" style="display:none"></div>
</div>
</div>
<!-- Chat Resize Handle -->
<!-- Chat Resize Handle -->
- <div class="chat-resize-handle" id="chat-resize-handle" title="Drag to resize chat"></div>
<!-- Chat Sidebar -->
<!-- Chat Sidebar -->
<div class="chat-sidebar" id="chat-sidebar">
<div class="chat-sidebar" id="chat-sidebar">
<div class="chat-header">
<div class="chat-header">
new file mode 100644
new file mode 100644
@@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
+/*
+ OpenVibe.Live — resize the channel page's chat with a mouse, a finger, a pen or the keyboard, in each layout it has:
+
+ side 1181px and wider: the column beside the stream; the grip on its left edge sets the width
+ overlay 1180px and narrower, landscape: the panel over the right side; the same grip sets its width
+ sheet 1180px and narrower, portrait: the bottom sheet; the grip on its top edge sets the height, and
+ letting go most of the way down closes the sheet
+
+ Double-click or double-tap a grip (or press Enter on it) for the default size; arrow keys move it 24px.
+ Each layout remembers its own size (localStorage), applied as CSS variables on #page-channel, which
+ public/css/features/channel.css reads: --chat-w, --chat-ow, --chat-sheet-h.
+*/
+(function () {
+ 'use strict';
+ const WIDE = window.matchMedia('(min-width: 1181px)');
+ const LAND = window.matchMedia('(orientation: landscape)');
+ const MODES = {
+ side: { key: 'openvibe_chat_width', prop: '--chat-w', axis: 'x', min: 260, max: () => Math.min(window.innerWidth * 0.5, 760) },
+ overlay: { key: 'openvibe_chat_overlay_w', prop: '--chat-ow', axis: 'x', min: 260, max: () => window.innerWidth * 0.7 },
+ sheet: { key: 'openvibe_chat_sheet_h', prop: '--chat-sheet-h', axis: 'y', min: 180, max: () => window.innerHeight * 0.92 },
+ };
+ const page = () => document.getElementById('page-channel');
+ const modeOf = () => (WIDE.matches ? 'side' : LAND.matches ? 'overlay' : 'sheet');
+ const clamp = (v, a, b) => Math.round(Math.max(a, Math.min(b, v)));
+ const read = (k) => { try { return parseInt(localStorage.getItem(k), 10) || 0; } catch { return 0; } };
+ const write = (k, v) => { try { if (v) localStorage.setItem(k, String(Math.round(v))); else localStorage.removeItem(k); } catch { /* private mode */ } };
+
+ function setSize(mode, v) {
+ const p = page(); const m = MODES[mode];
+ if (!p) return;
+ if (v) p.style.setProperty(m.prop, `${clamp(v, m.min, m.max())}px`); else p.style.removeProperty(m.prop);
+ }
+ function applySaved() { for (const mode of Object.keys(MODES)) setSize(mode, read(MODES[mode].key)); }
+ function settle() { window.dispatchEvent(new Event('resize')); } // the player and the diagrams re-measure
+
+ function reset(mode) { write(MODES[mode].key, 0); setSize(mode, 0); settle(); }
+
+ function bind(sidebar) {
+ if (!sidebar || sidebar._ovGrip) return;
+ const g = document.createElement('div');
+ g.className = 'chat-grip';
+ g.setAttribute('role', 'separator');
+ g.setAttribute('aria-label', 'Resize chat');
+ g.tabIndex = 0;
+ g.title = 'Drag to resize the chat · double-click for the default size';
+ sidebar.prepend(g);
+ sidebar._ovGrip = g;
+
+ let drag = null, lastTap = 0;
+ g.addEventListener('pointerdown', (e) => {
+ if (e.pointerType === 'mouse' && e.button !== 0) return;
+ const mode = modeOf(); const r = sidebar.getBoundingClientRect();
+ drag = { mode, id: e.pointerId, x: e.clientX, y: e.clientY, from: MODES[mode].axis === 'x' ? r.width : r.height, cur: 0, moved: false };
+ try { g.setPointerCapture(e.pointerId); } catch { /* */ }
+ g.classList.add('dragging');
+ page()?.classList.add('chat-resizing');
+ e.preventDefault();
+ });
+ g.addEventListener('pointermove', (e) => {
+ if (!drag || e.pointerId !== drag.id) return;
+ const m = MODES[drag.mode];
+ const delta = m.axis === 'x' ? drag.x - e.clientX : drag.y - e.clientY; // the chat sits right / at the bottom
+ if (Math.abs(delta) > 3) drag.moved = true;
+ // 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`);
+ });
+ const end = (e) => {
+ if (!drag || (e && e.pointerId !== drag.id)) return;
+ const d = drag; drag = null;
+ g.classList.remove('dragging');
+ page()?.classList.remove('chat-resizing');
+ if (!d.moved) {
+ // A tap: two in a row restore the default size.
+ const now = Date.now();
+ if (now - lastTap < 320) { lastTap = 0; reset(d.mode); } else lastTap = now;
+ return;
+ }
+ const m = MODES[d.mode];
+ if (d.mode === 'sheet' && d.cur < m.min) {
+ setSize('sheet', read(m.key)); // keep the last real height for next time
+ const s = document.querySelector('#page-channel .chat-sidebar.mobile-chat-open');
+ if (s && typeof window.toggleMobileChat === 'function') window.toggleMobileChat();
+ return;
+ }
+ write(m.key, clamp(d.cur, m.min, m.max()));
+ settle();
+ };
+ g.addEventListener('pointerup', end);
+ g.addEventListener('pointercancel', end);
+ g.addEventListener('lostpointercapture', end);
+ g.addEventListener('dblclick', () => reset(modeOf()));
+ g.addEventListener('keydown', (e) => {
+ const mode = modeOf(); const m = MODES[mode];
+ if (e.key === 'Enter') { e.preventDefault(); reset(mode); 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();
+ });
+ }
+
+ function init() {
+ applySaved();
+ document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach(bind);
+ }
+ 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.ChannelChatResize = { init, reset: () => reset(modeOf()) };
+})();
@@ -3952,8 +3952,9 @@ function _deployCss() {
@@ -3952,8 +3952,9 @@ function _deployCss() {
if (document.getElementById('chat-deploy-css')) return;
if (document.getElementById('chat-deploy-css')) return;
const st = document.createElement('style'); st.id = 'chat-deploy-css';
const st = document.createElement('style'); st.id = 'chat-deploy-css';
st.textContent = `.chat-msg.system.chat-deploy,.chat-msg.system.chat-deploy *{font-style:normal}
st.textContent = `.chat-msg.system.chat-deploy,.chat-msg.system.chat-deploy *{font-style:normal}
-.chat-deploy{margin:8px 0;padding:10px 12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--accent,#3b82f6) 28%,transparent);background:color-mix(in srgb,var(--accent,#3b82f6) 6%,transparent);color:var(--text-secondary,#a8b3c4);font-size:.84rem;line-height:1.45;container-type:inline-size}
-.chat-deploy-h{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;color:var(--text-primary,#e6edf7)}.chat-deploy-h b{font-weight:700}.chat-deploy-t{font-size:.76rem;color:var(--text-muted,#7d8aa0)}
+.chat-deploy{margin:6px 0;padding:7px 10px;border-radius:12px;border:1px solid color-mix(in srgb,var(--accent,#3b82f6) 28%,transparent);background:color-mix(in srgb,var(--accent,#3b82f6) 6%,transparent);color:var(--text-secondary,#a8b3c4);font-size:.84rem;line-height:1.45;container-type:inline-size}
+.chat-deploy-h{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;width:100%;margin:0;padding:0;border:0;background:none;font:inherit;text-align:left;cursor:pointer;color:var(--text-primary,#e6edf7)}.chat-deploy-h::after{content:'▾';margin-left:auto;align-self:center;font-size:.7rem;color:var(--text-muted,#7d8aa0);transition:transform .2s}.chat-deploy:not(.is-collapsed) .chat-deploy-h::after{transform:rotate(180deg)}.chat-deploy-h:focus-visible{outline:2px solid var(--accent,#3b82f6);outline-offset:3px;border-radius:6px}
+.chat-deploy.is-collapsed ul,.chat-deploy.is-collapsed .chat-deploy-more,.chat-deploy.is-collapsed .chat-deploy-f a{display:none}.chat-deploy.is-collapsed .chat-deploy-f{margin-top:6px;padding-top:0;border-top:0}.chat-deploy.is-collapsed .chat-deploy-f:not(:has(.chat-deploy-reload)){display:none}.chat-deploy-h b{font-weight:700}.chat-deploy-t{font-size:.76rem;color:var(--text-muted,#7d8aa0)}
.chat-deploy ul{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:4px}
.chat-deploy ul{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:4px}
.chat-deploy li{display:grid;grid-template-columns:4.6em minmax(0,1fr);gap:8px;align-items:baseline}.chat-deploy li.is-hidden{display:none}.chat-deploy.is-open li.is-hidden{display:grid}
.chat-deploy li{display:grid;grid-template-columns:4.6em minmax(0,1fr);gap:8px;align-items:baseline}.chat-deploy li.is-hidden{display:none}.chat-deploy.is-open li.is-hidden{display:grid}
.chat-deploy li a{font-family:ui-monospace,Menlo,monospace;font-size:.72rem;color:var(--accent-light,var(--accent,#60a5fa));text-decoration:none}
.chat-deploy li a{font-family:ui-monospace,Menlo,monospace;font-size:.72rem;color:var(--accent-light,var(--accent,#60a5fa));text-decoration:none}
@@ -3986,14 +3987,19 @@ function renderDeployCard(data, opts = {}) {
@@ -3986,14 +3987,19 @@ function renderDeployCard(data, opts = {}) {
const id = data.id != null ? String(data.id) : '';
const id = data.id != null ? String(data.id) : '';
let el = id ? container.querySelector('.chat-deploy[data-deploy-id="' + id.replace(/[^\w-]/g, '') + '"]') : null;
let el = id ? container.querySelector('.chat-deploy[data-deploy-id="' + id.replace(/[^\w-]/g, '') + '"]') : null;
const wasOpen = el && el.classList.contains('is-open');
const wasOpen = el && el.classList.contains('is-open');
+ // One line until opened: a run of deploys is context, not the conversation (the list and notes are a tap away).
+ const wasExpanded = el && !el.classList.contains('is-collapsed');
if (!el) { el = document.createElement('div'); el.className = 'chat-msg system chat-deploy'; if (id) el.dataset.deployId = id.replace(/[^\w-]/g, ''); }
if (!el) { el = document.createElement('div'); el.className = 'chat-msg system chat-deploy'; if (id) el.dataset.deployId = id.replace(/[^\w-]/g, ''); }
const range = first.getTime() === last.getTime() || Math.abs(last - first) < 60000 ? _deployTime(last) : _deployTime(first, first.toDateString() !== last.toDateString()) + ' – ' + _deployTime(last, first.toDateString() !== last.toDateString());
const range = first.getTime() === last.getTime() || Math.abs(last - first) < 60000 ? _deployTime(last) : _deployTime(first, first.toDateString() !== last.toDateString()) + ' – ' + _deployTime(last, first.toDateString() !== last.toDateString());
const deploys = Number(data.deploys) || 1;
const deploys = Number(data.deploys) || 1;
const rest = commits.length - SHOW;
const rest = commits.length - SHOW;
- el.innerHTML = `<div class="chat-deploy-h"><b>🚀 ${commits.length} update${commits.length === 1 ? '' : 's'} shipped</b><span class="chat-deploy-t" title="${esc(last.toLocaleString())}">${esc(range)}${deploys > 1 ? ' · ' + deploys + ' deploys' : ''}</span></div>
+ el.innerHTML = `<button type="button" class="chat-deploy-h" aria-expanded="${wasExpanded ? 'true' : 'false'}"><b>🚀 ${commits.length} update${commits.length === 1 ? '' : 's'} shipped</b><span class="chat-deploy-t" title="${esc(last.toLocaleString())}">${esc(range)}${deploys > 1 ? ' · ' + deploys + ' deploys' : ''}</span></button>
<ul>${commits.map((c, i) => `<li class="${i >= SHOW ? 'is-hidden' : ''}${fresh.has(c.hash) ? ' is-fresh' : ''}">${c.hash ? `<a href="https://github.com/OpenVibers/OpenVibe.Live/commit/${esc(c.hash)}" target="_blank" rel="noopener">${esc(c.short || String(c.hash).slice(0, 7))}</a>` : '<i></i>'}<span>${esc(c.subject)}</span></li>`).join('')}</ul>
<ul>${commits.map((c, i) => `<li class="${i >= SHOW ? 'is-hidden' : ''}${fresh.has(c.hash) ? ' is-fresh' : ''}">${c.hash ? `<a href="https://github.com/OpenVibers/OpenVibe.Live/commit/${esc(c.hash)}" target="_blank" rel="noopener">${esc(c.short || String(c.hash).slice(0, 7))}</a>` : '<i></i>'}<span>${esc(c.subject)}</span></li>`).join('')}</ul>
<div class="chat-deploy-f">${rest > 0 ? `<button type="button" class="chat-deploy-more" aria-expanded="${wasOpen ? 'true' : 'false'}">${wasOpen ? 'Show fewer' : `Show ${rest} more`}</button>` : ''}<span class="chat-deploy-a"><a href="/updates" target="_blank" rel="noopener">Patch notes</a>${opts.live ? '<button type="button" class="chat-deploy-reload" title="Load the new version">Reload to update</button>' : ''}</span></div>`;
<div class="chat-deploy-f">${rest > 0 ? `<button type="button" class="chat-deploy-more" aria-expanded="${wasOpen ? 'true' : 'false'}">${wasOpen ? 'Show fewer' : `Show ${rest} more`}</button>` : ''}<span class="chat-deploy-a"><a href="/updates" target="_blank" rel="noopener">Patch notes</a>${opts.live ? '<button type="button" class="chat-deploy-reload" title="Load the new version">Reload to update</button>' : ''}</span></div>`;
if (wasOpen) el.classList.add('is-open');
if (wasOpen) el.classList.add('is-open');
+ el.classList.toggle('is-collapsed', !wasExpanded);
+ const head = el.querySelector('.chat-deploy-h');
+ head.addEventListener('click', () => { const open = el.classList.toggle('is-collapsed') === false; head.setAttribute('aria-expanded', String(open)); });
const more = el.querySelector('.chat-deploy-more');
const more = el.querySelector('.chat-deploy-more');
if (more) more.addEventListener('click', () => { const open = el.classList.toggle('is-open'); more.textContent = open ? 'Show fewer' : `Show ${rest} more`; more.setAttribute('aria-expanded', String(open)); });
if (more) more.addEventListener('click', () => { const open = el.classList.toggle('is-open'); more.textContent = open ? 'Show fewer' : `Show ${rest} more`; more.setAttribute('aria-expanded', String(open)); });
const reload = el.querySelector('.chat-deploy-reload');
const reload = el.querySelector('.chat-deploy-reload');
@@ -7436,10 +7442,9 @@ function _fcwStopResize() {
@@ -7436,10 +7442,9 @@ function _fcwStopResize() {
}
}
// Page markup that arrives later (ov-loader.js fragments) gets the same one-time setup the
// Page markup that arrives later (ov-loader.js fragments) gets the same one-time setup the
-// boot-time markup got: font-size classes and scroll containment, the resize handle, GIF buttons.
+// boot-time markup got: font-size classes and scroll containment, GIF buttons.
document.addEventListener('ov:fragment', () => {
document.addEventListener('ov:fragment', () => {
try { applyChatSettings(); } catch { /* */ }
try { applyChatSettings(); } catch { /* */ }
- try { initChatResize(); } catch { /* */ }
if (typeof initGifPickers === 'function') { try { initGifPickers(); } catch { /* */ } }
if (typeof initGifPickers === 'function') { try { initGifPickers(); } catch { /* */ } }
});
});
@@ -7670,60 +7675,7 @@ function closeChatUsersPanel(btn) {
@@ -7670,60 +7675,7 @@ function closeChatUsersPanel(btn) {
(window.handleChatLinkClick / showLinkContextMenu / OpenVibeChat.linkify),
(window.handleChatLinkClick / showLinkContextMenu / OpenVibeChat.linkify),
shared with the /kiosk page. */
shared with the /kiosk page. */
-/* ══════════════════════════════════════════════════════════════
- CHAT RESIZE HANDLE — drag to change chat sidebar width
- ══════════════════════════════════════════════════════════════ */
-function initChatResize() {
- const handle = document.getElementById('chat-resize-handle');
- const sidebar = document.getElementById('chat-sidebar');
- if (!handle || !sidebar || handle._ovResizeBound) return;
- handle._ovResizeBound = true;
-
- const MIN_W = 250;
- const MAX_RATIO = 0.5; // max 50% of viewport
-
- // Restore saved width
- const saved = localStorage.getItem('openvibe_chat_width');
- if (saved) {
- const w = parseInt(saved, 10);
- if (w >= MIN_W && w <= window.innerWidth * MAX_RATIO) {
- sidebar.style.width = w + 'px';
- }
- }
-
- let dragging = false;
- let startX = 0;
- let startW = 0;
-
- handle.addEventListener('mousedown', (e) => {
- e.preventDefault();
- dragging = true;
- startX = e.clientX;
- startW = sidebar.offsetWidth;
- handle.classList.add('dragging');
- document.body.style.cursor = 'col-resize';
- document.body.style.userSelect = 'none';
- });
-
- document.addEventListener('mousemove', (e) => {
- if (!dragging) return;
- // Chat is on the right, so dragging left increases width
- const diff = startX - e.clientX;
- const maxW = window.innerWidth * MAX_RATIO;
- const newW = Math.max(MIN_W, Math.min(maxW, startW + diff));
- sidebar.style.width = newW + 'px';
- });
-
- document.addEventListener('mouseup', () => {
- if (!dragging) return;
- dragging = false;
- handle.classList.remove('dragging');
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- localStorage.setItem('openvibe_chat_width', sidebar.offsetWidth);
- });
-}
-initChatResize();
+/* Resizing the channel page's chat (every layout, mouse and touch): public/js/channel-chat-resize.js. */
// Reply / translate buttons and folded Friendly-chat groups, for every chat surface: one listener.
// Reply / translate buttons and folded Friendly-chat groups, for every chat surface: one listener.
(function _wireChatRowActions() {
(function _wireChatRowActions() {