icons
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 27s

This commit is contained in:
mats committed 2026-09-22 14:24:14 +02:00
1 parent ec02a6ddaf
commit aab7931142
3 files changed
+286 -90

No files matched your search

+101 -18
View File
@@ -34,6 +34,7 @@ window.__dbg = S; // POC debug handle
const $ = (sel) => document.querySelector(sel);
const els = {
voiceView: $('#voice-view'),
stage: $('#stage'), screens: $('#screens'),
spatial: $('#spatial'), status: $('#voice-status'), statusDot: $('#status-dot'),
statusRow: $('#status-row'), nick: $('#nick'),
@@ -50,6 +51,13 @@ const shareButtons = [...document.querySelectorAll('#btn-share, .mirror-share')]
const statusTextEls = [...document.querySelectorAll('#voice-status, #mobile-status-text')];
const statusDotEls = [...document.querySelectorAll('#status-dot, #mobile-status-dot')];
function mIcon(name, extraClass = '') {
const span = document.createElement('span');
span.className = `material-symbols-outlined${extraClass ? ` ${extraClass}` : ''}`;
span.textContent = name;
return span;
}
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
@@ -784,9 +792,27 @@ function renderTiles() {
const meBadges = document.createElement('span');
meBadges.className = 'user-line-badges';
if (S.muted) meBadges.innerHTML += '<span class="badge-mute" title="Muted">&#127908;&#65039;&#8205;&#10060;</span>';
if (S.deafened) meBadges.innerHTML += '<span class="badge-deafen" title="Deafened">&#128066;&#10060;</span>';
if (S.sharing) meBadges.innerHTML += '<span class="badge-share" title="Sharing screen">&#128421;&#65039;</span>';
if (S.muted) {
const b = document.createElement('span');
b.className = 'badge-mute';
b.title = 'Muted';
b.appendChild(mIcon('mic_off'));
meBadges.appendChild(b);
}
if (S.deafened) {
const b = document.createElement('span');
b.className = 'badge-deafen';
b.title = 'Deafened';
b.appendChild(mIcon('headset_off'));
meBadges.appendChild(b);
}
if (S.sharing) {
const b = document.createElement('span');
b.className = 'badge-share';
b.title = 'Sharing screen';
b.appendChild(mIcon('present_to_all'));
meBadges.appendChild(b);
}
meItem.append(meAv, meName, meBadges);
roster.appendChild(meItem);
@@ -808,8 +834,20 @@ function renderTiles() {
if (!peer.pc || peer.pc.connectionState !== 'connected') {
badges.innerHTML += '<span class="badge-conn" title="Connecting...">…</span>';
}
if (peer.state.muted) badges.innerHTML += '<span class="badge-mute" title="Muted">&#127908;&#65039;&#8205;&#10060;</span>';
if (peer.state.sharing) badges.innerHTML += '<span class="badge-share" title="Sharing screen">&#128421;&#65039;</span>';
if (peer.state.muted) {
const b = document.createElement('span');
b.className = 'badge-mute';
b.title = 'Muted';
b.appendChild(mIcon('mic_off'));
badges.appendChild(b);
}
if (peer.state.sharing) {
const b = document.createElement('span');
b.className = 'badge-share';
b.title = 'Sharing screen';
b.appendChild(mIcon('present_to_all'));
badges.appendChild(b);
}
// Volume hover popup
const volPop = document.createElement('div');
@@ -858,8 +896,20 @@ function renderTiles() {
const badges = document.createElement('span');
badges.className = 'user-line-badges';
if (user.state?.muted) badges.innerHTML += '<span class="badge-mute" title="Muted">&#127908;&#65039;&#8205;&#10060;</span>';
if (user.state?.sharing) badges.innerHTML += '<span class="badge-share" title="Sharing screen">&#128421;&#65039;</span>';
if (user.state?.muted) {
const b = document.createElement('span');
b.className = 'badge-mute';
b.title = 'Muted';
b.appendChild(mIcon('mic_off'));
badges.appendChild(b);
}
if (user.state?.sharing) {
const b = document.createElement('span');
b.className = 'badge-share';
b.title = 'Sharing screen';
b.appendChild(mIcon('present_to_all'));
badges.appendChild(b);
}
item.append(av, name, badges);
roster.appendChild(item);
@@ -878,7 +928,9 @@ function activeShares() {
const shares = [];
if (S.sharing && S.screenStream) shares.push({ key: 'me', name: `${S.name} (you)`, stream: S.screenStream });
for (const peer of S.peers.values()) {
if (peer.screenStream) shares.push({ key: peer.id, name: peer.name, stream: peer.screenStream });
if (peer.state.sharing && peer.screenStream) {
shares.push({ key: peer.id, name: peer.name, stream: peer.screenStream });
}
}
return shares;
}
@@ -893,6 +945,8 @@ function renderScreens() {
}
}
if (!els.screens) return;
els.screens.innerHTML = '';
els.screens.classList.toggle('multi', activeShares().length > 1);
for (const share of activeShares()) {
@@ -902,7 +956,8 @@ function renderScreens() {
ph.className = 'screen-card popped';
const btn = document.createElement('button');
btn.className = 'pop-btn';
btn.textContent = '⧉ re-embed';
btn.appendChild(mIcon('open_in_new'));
btn.appendChild(document.createTextNode(' re-embed'));
btn.onclick = () => closePopout(share.key);
const label = document.createElement('div');
label.className = 'label';
@@ -926,11 +981,12 @@ function renderScreens() {
label.appendChild(document.createTextNode(share.name));
const micBadge = document.createElement('span');
micBadge.className = 'tile-mic-badge';
micBadge.appendChild(mIcon('mic_off'));
label.appendChild(micBadge);
const pop = document.createElement('button');
pop.className = 'pop-btn';
pop.title = 'Pop out in new window';
pop.textContent = '⧉';
pop.appendChild(mIcon('open_in_new'));
pop.onclick = () => popOutShare(share);
card.append(video, label, pop);
els.screens.appendChild(card);
@@ -1428,8 +1484,24 @@ function mxRenderTimeline() {
els.mxMessages.scrollTop = els.mxMessages.scrollHeight;
}
function mxShowPanel() {
function switchToChatView() {
if (els.voiceView) els.voiceView.classList.add('hidden');
if (els.mxPanel) els.mxPanel.classList.remove('hidden');
document.querySelectorAll('.channel.text').forEach((c) => c.classList.add('active'));
if (els.mxMessages) els.mxMessages.scrollTop = els.mxMessages.scrollHeight;
if (els.mxInput) els.mxInput.focus();
}
function switchToVoiceView() {
if (els.mxPanel) els.mxPanel.classList.add('hidden');
if (els.voiceView) els.voiceView.classList.remove('hidden');
document.querySelectorAll('.channel.text').forEach((c) => c.classList.remove('active'));
// Trigger Cytoscape redraw/resize because voice-view was unhidden
if (typeof cy !== 'undefined') cy.resize();
}
function mxShowPanel() {
switchToChatView();
}
function mxSend(text) {
@@ -1462,12 +1534,13 @@ mxInit();
document.querySelectorAll('.channel.text').forEach((el) => {
el.onclick = () => {
if (Mx.roomId) { mxShowPanel(); return; }
closeSidebar();
if (Mx.roomId) { switchToChatView(); return; }
if (Mx.client) { toast('Connecting to Matrix…'); return; }
mxStartLogin();
};
});
$('#mx-close')?.addEventListener('click', () => els.mxPanel.classList.add('hidden'));
$('#mx-close')?.addEventListener('click', () => switchToVoiceView());
$('#mx-send')?.addEventListener('click', () => {
mxSend(els.mxInput.value);
els.mxInput.value = '';
@@ -1486,12 +1559,15 @@ function updateMicButton() {
const off = S.muted || !S.micStream;
for (const btn of micButtons) {
btn.classList.toggle('off', off);
btn.textContent = off ? '\u{1F507}' : '\u{1F3A4}'; // 🔇 muted/no-mic vs 🎤 live
btn.replaceChildren(mIcon(off ? 'mic_off' : 'mic'));
btn.disabled = !S.micStream;
}
}
function updateDeafenButton() {
for (const btn of deafenButtons) btn.classList.toggle('off', S.deafened);
for (const btn of deafenButtons) {
btn.classList.toggle('off', S.deafened);
btn.replaceChildren(mIcon(S.deafened ? 'headset_off' : 'headphones'));
}
}
function sendState() {
send({ type: 'state', state: { muted: S.muted, deafened: S.deafened, sharing: S.sharing } });
@@ -1524,9 +1600,15 @@ $('#btn-settings').onclick = openSettings;
els.nick.value = S.name;
els.nick.addEventListener('change', () => {
const trimmed = els.nick.value.trim();
if (!trimmed) { els.nick.value = S.name; return; } // nickname can't be blanked out
S.name = trimmed;
const v = els.nick.value.trim();
if (v) S.name = v;
els.nick.value = S.name;
localStorage.setItem(LS_NAME, S.name);
setAvatar(els.meAvatar, S.name, S.myId || 'me');
if (S.ws && S.ws.readyState === 1) send({ type: 'rename', name: S.name });
renderTiles();
});
els.nick.addEventListener('blur', () => {
els.nick.value = S.name;
localStorage.setItem(LS_NAME, S.name);
setAvatar(els.meAvatar, S.name, S.myId || 'me');
@@ -1536,6 +1618,7 @@ setAvatar(els.meAvatar, S.name, 'me');
document.querySelectorAll('.channel.voice').forEach((el) => {
el.onclick = () => {
closeSidebar();
switchToVoiceView();
if (S.room === el.dataset.room) return;
cancelReconnect();
document.querySelectorAll('.channel.voice').forEach((c) => c.classList.remove('active'));
+48 -39
View File
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>gibbz voice — POC</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg" />
@@ -28,7 +29,7 @@
<body>
<!-- mobile top bar: hamburger opens the channel drawer, shows live status -->
<header id="mobile-topbar">
<button id="btn-menu" title="Channels">&#9776;</button>
<button id="btn-menu" title="Channels"><span class="material-symbols-outlined">menu</span></button>
<img id="mobile-logo" src="/icons/icon.svg" alt="" width="24" height="24" />
<span id="mobile-title">gibbz voice</span>
<span class="status-row" id="mobile-status-row">
@@ -46,12 +47,12 @@
gibbz voice <span class="poc-tag">POC</span>
</header>
<div class="channel-group-label">Text channels</div>
<div class="channel text" data-mx-room="gibbz-general">#
<div class="channel text" data-mx-room="gibbz-general"><span class="material-symbols-outlined channel-icon">tag</span>
general <span class="hint">matrix</span></div>
<div class="channel-group-label">Voice channels</div>
<div class="channel voice" data-room="general">&#128266; General <span class="count" data-room-count="general"></span></div>
<div class="channel voice" data-room="gaming">&#128266; Gaming <span class="count" data-room-count="gaming"></span></div>
<div class="channel voice" data-room="lounge">&#128266; Lounge <span class="count" data-room-count="lounge"></span></div>
<div class="channel voice" data-room="general"><span class="material-symbols-outlined channel-icon">volume_up</span> General <span class="count" data-room-count="general"></span></div>
<div class="channel voice" data-room="gaming"><span class="material-symbols-outlined channel-icon">volume_up</span> Gaming <span class="count" data-room-count="gaming"></span></div>
<div class="channel voice" data-room="lounge"><span class="material-symbols-outlined channel-icon">volume_up</span> Lounge <span class="count" data-room-count="lounge"></span></div>
<footer id="user-panel">
<div class="me">
@@ -65,42 +66,62 @@
</div>
</div>
<div class="user-buttons">
<button id="btn-mic" class="ctl" title="Toggle mic (M)">&#127908;</button>
<button id="btn-deafen" class="ctl" title="Toggle deafen (D)">&#128064;</button>
<button id="btn-share" class="ctl" title="Share your screen (S)">&#128421;&#65039;</button>
<button id="btn-settings" class="ctl" title="Voice &amp; ICE settings">&#9881;&#65039;</button>
<button id="btn-help" class="ctl" title="How to use gibbz voice">&#10067;</button>
<button id="btn-mic" class="ctl" title="Toggle mic (M)"><span class="material-symbols-outlined">mic</span></button>
<button id="btn-deafen" class="ctl" title="Toggle deafen (D)"><span class="material-symbols-outlined">headphones</span></button>
<button id="btn-share" class="ctl" title="Share your screen (S)"><span class="material-symbols-outlined">present_to_all</span></button>
<button id="btn-settings" class="ctl" title="Voice &amp; ICE settings"><span class="material-symbols-outlined">settings</span></button>
<button id="btn-help" class="ctl" title="How to use gibbz voice"><span class="material-symbols-outlined">help</span></button>
</div>
</footer>
</aside>
<!-- main stage -->
<!-- main content area -->
<main id="main">
<div id="stage">
<div id="screens"></div>
<div id="stage-empty">
<div class="big-icon">&#128421;&#65039;</div>
<h2>No active screen shares</h2>
<p>When someone shares their screen, it will appear here. Click the screen button below or press <b>S</b> to share.</p>
<!-- voice stage and spatial audio view -->
<div id="voice-view">
<div id="stage">
<div id="screens"></div>
<div id="stage-empty">
<div class="big-icon"><span class="material-symbols-outlined">present_to_all</span></div>
<h2>No active screen shares</h2>
<p>When someone shares their screen, it will appear here. Click the screen button below or press <b>S</b> to share.</p>
</div>
</div>
<div id="spatial-wrap">
<div class="spatial-label">
<span>SPATIAL AUDIO</span>
<span class="hint">drag your avatar to move; drag peers for local positioning &bull; scroll to zoom</span>
<button id="btn-spatial-fit" class="spatial-action-btn" title="Fit all users in view"><span class="material-symbols-outlined">fit_screen</span> Fit View</button>
</div>
<div id="spatial"></div>
</div>
</div>
<div id="spatial-wrap">
<div class="spatial-label">
<span>SPATIAL AUDIO</span>
<span class="hint">drag your avatar to move; drag peers for local positioning &bull; scroll to zoom</span>
<button id="btn-spatial-fit" class="spatial-action-btn" title="Fit all users in view">&#x26F6; Fit View</button>
<!-- matrix text chat view -->
<div id="mx-panel" class="hidden">
<div class="mx-header">
<div class="mx-header-info">
<span class="material-symbols-outlined channel-icon">tag</span>
<span class="mx-header-title">general</span>
<span class="poc-tag">matrix · experimental</span>
</div>
<button id="mx-close" class="ghost" title="Back to Voice Stage"><span class="material-symbols-outlined">close</span></button>
</div>
<div id="mx-messages"></div>
<div class="mx-composer">
<textarea id="mx-input" rows="1" placeholder="Message #general (Enter to send)"></textarea>
<button id="mx-send">Send</button>
</div>
<div id="spatial"></div>
</div>
</main>
</div>
<!-- mobile bottom bar: always-reachable call controls, mirror the sidebar's -->
<div id="mobile-controls">
<button class="ctl mirror-mic" title="Toggle mic (M)">&#127908;</button>
<button class="ctl mirror-deafen" title="Toggle deafen (D)">&#128064;</button>
<button class="ctl mirror-share" title="Share your screen (S)">&#128421;&#65039;</button>
<button class="ctl mirror-mic" title="Toggle mic (M)"><span class="material-symbols-outlined">mic</span></button>
<button class="ctl mirror-deafen" title="Toggle deafen (D)"><span class="material-symbols-outlined">headphones</span></button>
<button class="ctl mirror-share" title="Share your screen (S)"><span class="material-symbols-outlined">present_to_all</span></button>
</div>
<!-- settings modal -->
@@ -138,7 +159,7 @@
<li>Click a <b>voice channel</b> in the sidebar to join it — click another to switch.</li>
<li><b>Mic</b>, <b>Deafen</b> and <b>Share screen</b> are the buttons bottom-left, or press <b>M</b> / <b>D</b> / <b>S</b>.</li>
<li>On the <b>spatial audio</b> map, drag your dot around — you hear people relative to where they stand.</li>
<li>Screen shares show up at the top of the main area; pop one into its own window with <b>&#10726;</b>.</li>
<li>Screen shares show up at the top of the main area; pop one into its own window with <span class="material-symbols-outlined inline-icon">open_in_new</span>.</li>
</ul>
<div class="modal-actions">
@@ -148,18 +169,6 @@
</div>
</div>
<div id="mx-panel" class="hidden">
<div class="mx-header">
<span>#general <span class="poc-tag">matrix · experimental</span></span>
<button id="mx-close" class="ghost" title="Close">&times;</button>
</div>
<div id="mx-messages"></div>
<div class="mx-composer">
<textarea id="mx-input" rows="1" placeholder="Message #general (Enter to send)"></textarea>
<button id="mx-send">Send</button>
</div>
</div>
<div id="reconnect-banner" class="hidden" role="alert">
<div class="reconnect-spinner"></div>
<span id="reconnect-text">Connection lost</span>
+137 -33
View File
@@ -22,6 +22,41 @@ body {
color: var(--text-0);
overflow: hidden;
}
/* Material Symbols font configuration */
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 20px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
user-select: none;
vertical-align: middle;
}
.inline-icon {
font-size: 16px;
vertical-align: -2px;
}
.channel-icon {
font-size: 18px;
color: var(--text-2);
flex-shrink: 0;
}
.channel:hover .channel-icon,
.channel.active .channel-icon {
color: var(--text-0);
}
#app { display: flex; height: 100vh; }
/* sidebar */
@@ -46,12 +81,14 @@ body {
.channel {
margin: 1px 8px; padding: 7px 8px; border-radius: 6px; color: var(--text-1);
cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
display: flex; align-items: center; gap: 6px;
}
.channel:hover { background: var(--bg-3); color: var(--text-0); }
.channel.text { color: var(--text-2); }
.channel.text.active { background: rgba(88,101,242,.25); color: var(--text-0); }
.channel.text .hint { font-size: 10px; margin-left: 4px; }
.channel.voice.active { background: rgba(88,101,242,.25); color: var(--text-0); }
.channel .count { float: right; color: var(--text-2); font-size: 12px; }
.channel .count { margin-left: auto; color: var(--text-2); font-size: 12px; }
#user-panel {
margin-top: auto; background: var(--bg-0); padding: 8px;
@@ -85,17 +122,16 @@ body {
width: 28px; height: 28px; border-radius: 6px; cursor: pointer; position: relative;
display: grid; place-items: center; padding: 0;
}
.ctl .material-symbols-outlined { font-size: 20px; }
.ctl:hover { background: var(--bg-3); color: var(--text-0); }
.ctl.off { color: var(--red); }
.ctl.off::after {
content: ""; position: absolute; left: 5px; top: 13px; width: 18px; height: 2px;
background: var(--red); transform: rotate(-40deg); border-radius: 1px;
}
.ctl:disabled { opacity: .35; cursor: not-allowed; }
.ctl.active { color: var(--green); }
/* main */
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; height: 100%; }
#voice-view { flex: 1; display: flex; flex-direction: column; min-width: 0; height: 100%; }
#voice-view.hidden { display: none; }
#stage {
flex: 1; display: flex; flex-direction: column; padding: 16px; gap: 16px;
overflow: auto; position: relative;
@@ -105,6 +141,7 @@ body {
text-align: center; gap: 8px; color: var(--text-1); padding: 24px;
}
#stage-empty .big-icon { font-size: 56px; }
#stage-empty .big-icon .material-symbols-outlined { font-size: 64px; opacity: 0.6; }
#stage-empty h2 { font-size: 20px; color: var(--text-0); }
#stage-empty p { max-width: 420px; color: var(--text-2); }
#stage.empty #stage-empty { display: grid; }
@@ -120,8 +157,10 @@ body {
.pop-btn {
position: absolute; right: 8px; top: 8px; z-index: 2;
background: rgba(0,0,0,.65); color: var(--text-0); border: none; cursor: pointer;
border-radius: 5px; padding: 3px 8px; font-size: 13px; font-weight: 600;
border-radius: 5px; padding: 4px 8px; font-size: 12px; font-weight: 600;
display: inline-flex; align-items: center; gap: 4px;
}
.pop-btn .material-symbols-outlined { font-size: 16px; }
.pop-btn:hover { background: var(--brand); }
.screen-card {
position: relative; background: #000; border-radius: var(--radius); overflow: hidden;
@@ -131,12 +170,12 @@ body {
.screen-card .label {
position: absolute; left: 8px; bottom: 8px; background: rgba(0,0,0,.65);
padding: 3px 8px; border-radius: 5px; font-size: 12px; font-weight: 600;
display: inline-flex; align-items: center;
}
.screen-card .label .live { color: var(--red); font-weight: 800; margin-right: 5px; }
.screen-card.speaking { box-shadow: 0 0 0 3px var(--green), 0 8px 24px rgba(0,0,0,.4); }
.tile-mic-badge { margin-left: 6px; }
.tile-mic-badge.muted::before { content: "\1F507"; } /* 🔇 */
.tile-mic-badge:not(.muted)::before { content: ""; }
.tile-mic-badge { margin-left: 6px; display: inline-flex; align-items: center; }
.tile-mic-badge .material-symbols-outlined { font-size: 15px; color: var(--red); }
/* channel user lines */
.channel-users {
@@ -180,15 +219,27 @@ body {
gap: 4px;
font-size: 12px;
}
.user-line-badges .material-symbols-outlined {
font-size: 15px;
}
.badge-conn {
color: var(--yellow);
font-weight: bold;
}
.badge-mute {
font-size: 11px;
color: var(--red);
display: inline-flex;
align-items: center;
}
.badge-deafen {
color: var(--red);
display: inline-flex;
align-items: center;
}
.badge-share {
font-size: 11px;
color: var(--brand);
display: inline-flex;
align-items: center;
}
/* hover volume popup */
@@ -327,39 +378,94 @@ body {
.howto-list { display: flex; flex-direction: column; gap: 8px; padding-left: 18px; font-size: 13px; color: var(--text-1); }
.howto-list li { line-height: 1.4; }
/* matrix text chat panel */
/* matrix text chat panel (main view) */
#mx-panel {
position: fixed; top: 0; right: 0; bottom: 0; width: 340px; max-width: 90vw;
background: var(--bg-1); border-left: 1px solid rgba(0,0,0,.35);
display: flex; flex-direction: column; z-index: 40;
box-shadow: -8px 0 24px rgba(0,0,0,.45);
flex: 1;
width: 100%;
height: 100%;
background: var(--bg-2);
display: flex;
flex-direction: column;
overflow: hidden;
}
#mx-panel.hidden { display: none; }
.mx-header {
padding: 12px 14px; font-weight: 700; font-size: 14px;
display: flex; align-items: center; justify-content: space-between;
border-bottom: 1px solid rgba(0,0,0,.25); flex-shrink: 0;
padding: 14px 16px;
font-weight: 700;
font-size: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0,0,0,.25);
box-shadow: 0 1px 0 rgba(0,0,0,.06);
flex-shrink: 0;
background: var(--bg-2);
}
.mx-header-info {
display: flex;
align-items: center;
gap: 8px;
}
.mx-header-title {
color: var(--text-0);
font-weight: 700;
}
.mx-header button.ghost {
background: transparent; border: none; color: var(--text-1);
font-size: 20px; line-height: 1; cursor: pointer; padding: 0 4px;
background: transparent;
border: none;
color: var(--text-1);
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
padding: 0;
}
.mx-header button.ghost:hover { color: var(--text-0); }
#mx-messages { flex: 1; overflow: auto; padding: 10px 14px; display: flex; flex-direction: column; gap: 8px; }
.mx-msg { font-size: 13px; line-height: 1.4; }
.mx-sender { font-weight: 700; color: var(--text-0); margin-right: 6px; }
.mx-header button.ghost:hover {
background: var(--bg-3);
color: var(--text-0);
}
#mx-messages {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.mx-msg { font-size: 14px; line-height: 1.45; }
.mx-sender { font-weight: 700; color: var(--text-0); margin-right: 8px; }
.mx-body { color: var(--text-1); white-space: pre-wrap; overflow-wrap: anywhere; }
.mx-composer {
display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid rgba(0,0,0,.25); flex-shrink: 0;
display: flex;
gap: 10px;
padding: 14px 20px;
border-top: 1px solid rgba(0,0,0,.2);
background: var(--bg-2);
flex-shrink: 0;
}
.mx-composer textarea {
flex: 1; resize: none; background: var(--bg-0); border: 1px solid var(--bg-4);
border-radius: 6px; color: var(--text-0); padding: 8px 10px; font: inherit; max-height: 100px;
flex: 1;
resize: none;
background: var(--bg-0);
border: 1px solid var(--bg-4);
border-radius: 8px;
color: var(--text-0);
padding: 10px 14px;
font: inherit;
max-height: 120px;
}
.mx-composer textarea:focus { outline: none; border-color: var(--brand); }
.mx-composer button {
background: var(--brand); border: none; color: #fff; border-radius: 6px;
padding: 0 14px; font-weight: 600; cursor: pointer;
background: var(--brand);
border: none;
color: #fff;
border-radius: 8px;
padding: 0 18px;
font-weight: 600;
cursor: pointer;
}
/* reconnect banner */
@@ -467,7 +573,5 @@ code { font-family: ui-monospace, monospace; font-size: 12px; }
/* modals already clamp to 92vw; just tighten padding a bit */
#settings-modal, #welcome-modal { padding: 16px; }
#mx-panel { width: 100vw; }
#toasts { left: 12px; right: 12px; max-width: none; }
}