Files
Haven/public/css/voice.css
T

812 lines
23 KiB
CSS

/* ═══════════════════════════════════════════════════════════
VOICE UI
═══════════════════════════════════════════════════════════ */
[data-theme="fallout"] .btn-voice { border: 1px solid rgba(20, 254, 23, 0.3); }
[data-theme="fallout"] .btn-voice.active { background: #14fe17; color: #0a0e08; text-shadow: none; }
[data-theme="crt"] .btn-voice { border: 1px solid rgba(255, 176, 0, 0.3); border-radius: 0; }
[data-theme="win95"] .btn-voice { background: #bfbfbf; color: #000; border: 3px outset #fff; border-radius: 0; }
[data-theme="win95"] .btn-voice:hover { background: #dfdfdf; }
[data-theme="win95"] .btn-voice.active { background: #bfbfbf; color: #000; border: 3px inset #808080; }
[data-theme="win95"] .btn-voice.btn-danger { background: #bfbfbf; color: #cc0000; border: 3px outset #fff; font-weight: 700; }
[data-theme="win95"] .voice-panel-btn {
background: #bfbfbf; color: #000; border: 3px outset #fff;
border-radius: 0 !important; width: 32px; height: 32px;
}
[data-theme="win95"] .voice-panel-btn:hover { background: #dfdfdf; color: #000; }
[data-theme="win95"] .voice-panel-btn:active,
[data-theme="win95"] .voice-panel-btn.active,
[data-theme="win95"] .voice-panel-btn.muted,
[data-theme="win95"] .voice-panel-btn.sharing { border: 3px inset #808080; color: #000; background: #bfbfbf; }
[data-theme="win95"] .voice-panel-leave { background: #bfbfbf; color: #cc0000; border: 3px outset #fff; }
[data-theme="win95"] .voice-panel-leave:hover { background: #dfdfdf; color: #cc0000; }
[data-theme="win95"] .voice-header-btn { background: #bfbfbf; color: #000; border: 2px outset #fff; border-radius: 0 !important; }
[data-theme="win95"] .voice-header-btn:hover { background: #dfdfdf; color: #000; }
[data-theme="win95"] .voice-header-btn.active { border: 2px inset #808080; background: #bfbfbf; }
[data-theme="win95"] .voice-header-leave { background: #bfbfbf; color: #cc0000; }
[data-theme="win95"] .voice-header-leave:hover { background: #dfdfdf; color: #cc0000; }
/* ── Channel voice indicator (sidebar) ─── */
.channel-voice-indicator {
display: flex;
align-items: center;
gap: 3px;
font-size: 11px;
color: var(--success);
flex-shrink: 0;
}
.channel-voice-indicator .voice-icon { font-size: 12px; }
/* ── Voice users under channels (sidebar) ─── */
.channel-voice-users {
display: flex;
flex-direction: column;
padding: 2px 0 4px 40px;
gap: 1px;
}
.channel-voice-user {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
padding: 2px 8px;
border-radius: 4px;
cursor: default;
}
.channel-voice-user:hover { color: var(--text-secondary); background: var(--bg-hover); }
.channel-voice-user .cvu-mic,
.channel-voice-user .cvu-deafen { font-size: 10px; flex-shrink: 0; position: relative; display: inline-block; opacity: 0.35; }
.channel-voice-user .cvu-mic { color: var(--success); }
.channel-voice-user .cvu-mic.is-muted,
.channel-voice-user .cvu-deafen.is-deafened { opacity: 1; }
/* Shared muted/deafened red strikethrough */
.cvu-mic.is-muted::after,
.cvu-deafen.is-deafened::after,
.voice-status-icon.is-muted::after,
.voice-status-icon.is-deafened::after {
content: '';
position: absolute;
top: 50%; left: 5%;
width: 90%; height: 1.5px;
background: #c00;
transform: rotate(-45deg);
pointer-events: none;
}
/* When voice indicator is present, don't double-auto-margin */
.channel-voice-indicator ~ .channel-more-btn {
margin-left: 2px;
}
.voice-controls { display: flex; gap: 5px; flex-shrink: 0; align-items: center; margin-left: auto; }
.btn-voice {
padding: 5px 12px;
border-radius: var(--radius);
font-size: 13px;
font-weight: 500;
transition: all var(--transition);
background: var(--bg-tertiary);
color: var(--text-secondary);
line-height: 1.3;
height: 30px;
display: inline-flex;
align-items: center;
box-sizing: border-box;
}
.btn-voice:hover { background: var(--bg-hover); color: var(--text-primary); }
.btn-voice.active { background: var(--success); color: var(--success-text); }
.btn-voice.btn-danger { background: var(--danger); color: var(--danger-text); padding: 5px 10px; font-size: 14px; }
.btn-voice.btn-danger:hover { background: #d93636; }
.btn-voice.muted { background: var(--warning); color: var(--warning-text); }
.btn-voice.sharing {
background: var(--danger) !important;
color: var(--danger-text) !important;
}
.voice-active-indicator {
display: none;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
white-space: nowrap;
}
.voice-indicator-icon { font-size: 13px; }
.voice-indicator-text { font-size: 12px; }
.voice-dropdown-backdrop { display: none; }
.voice-dropdown-panel {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 6px;
flex-direction: column;
gap: 4px;
padding: 8px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
z-index: 100;
min-width: 140px;
}
.voice-dropdown-panel .btn-voice {
width: 100%;
text-align: left;
white-space: nowrap;
}
.voice-dropdown-panel .voice-ns-wrap {
width: 100%;
}
.voice-ns-wrap {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
border-radius: var(--radius);
background: var(--bg-tertiary);
}
.ns-label {
font-size: 14px;
line-height: 1;
cursor: default;
}
.ns-slider {
width: 60px;
height: 14px;
-webkit-appearance: none;
appearance: none;
background: transparent;
border-radius: 2px;
outline: none;
cursor: pointer;
}
.ns-slider::-webkit-slider-runnable-track {
background: var(--track-bg, var(--bg-hover, rgba(255,255,255,0.3)));
height: 6px;
border-radius: 3px;
border: 1px solid var(--border);
}
.ns-slider::-moz-range-track {
background: var(--track-bg, var(--bg-hover, rgba(255,255,255,0.3)));
height: 6px;
border-radius: 3px;
border: 1px solid var(--border);
}
.ns-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
margin-top: -5px;
box-shadow: 0 0 6px var(--accent-glow, rgba(124,92,252,0.4));
border: 2px solid var(--bg-secondary);
}
.ns-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: 2px solid var(--bg-secondary);
box-shadow: 0 0 6px var(--accent-glow, rgba(124,92,252,0.4));
}
.mic-meter-row {
margin-top: 2px;
}
.mic-meter {
position: relative;
flex: 1;
height: 8px;
min-width: 60px;
background: rgba(255,255,255,0.08);
border-radius: 4px;
border: 1px solid var(--border);
overflow: hidden;
}
.mic-meter-fill {
height: 100%;
width: 0%;
border-radius: 3px;
background: linear-gradient(90deg, #43b581 0%, #43b581 50%, #faa61a 75%, #f04747 100%);
transition: width 0.05s linear;
will-change: width;
}
.mic-meter-threshold {
position: absolute;
top: -1px;
bottom: -1px;
width: 2px;
background: #fff;
opacity: 0.7;
border-radius: 1px;
left: 10%;
pointer-events: none;
box-shadow: 0 0 3px rgba(0,0,0,0.5);
}
.right-sidebar-voice {
flex-shrink: 1;
overflow-y: auto;
min-height: 0;
}
.user-dot.voice { background: var(--voice-dot-color, var(--accent)); }
/* ═══════════════════════════════════════════════════════════
VOICE VOLUME SLIDERS
═══════════════════════════════════════════════════════════ */
/* nowrap, not wrap: a wrapping flex line pushes whole items to a second row
rather than shrinking them, so as soon as a user picked up a badge (LIVE,
📹, 👁) the status icons dropped onto a row of their own, and because they
carry margin-left:auto they sat at the far right of it, reading as an empty
second row. The name already ellipsises (flex:1 + min-width:0 in
style.css), so keeping one line just truncates the name instead. */
.voice-user-item { display: flex; align-items: center; flex-wrap: nowrap; min-width: 0; }
.voice-status-icons { display: flex; gap: 2px; margin-left: auto; align-items: center; flex-shrink: 0; }
.voice-status-icon { font-size: 12px; position: relative; display: inline-block; opacity: 0.25; line-height: 1; }
.voice-status-icon.is-muted,
.voice-status-icon.is-deafened { opacity: 1; }
@keyframes voice-pulse {
from { filter: drop-shadow(0 0 2px var(--accent)); }
to { filter: drop-shadow(0 0 5px var(--accent)); }
}
.channel-voice-user.talking {
background: color-mix(in srgb, var(--accent) 12%, transparent);
color: var(--accent);
font-weight: 600;
border-radius: var(--radius-sm);
}
.channel-voice-user.talking .cvu-mic {
animation: voice-pulse 0.8s ease-in-out infinite alternate;
}
.voice-user-item.talking {
background: color-mix(in srgb, var(--voice-dot-color, var(--accent)) 12%, transparent);
border-radius: var(--radius-sm);
}
.voice-user-item.talking .user-dot.voice {
box-shadow: 0 0 6px 2px var(--voice-dot-color, var(--accent));
animation: voice-pulse 0.8s ease-in-out infinite alternate;
}
.voice-user-item.talking .user-item-name {
color: var(--accent);
font-weight: 600;
}
/* ═══════════════════════════════════════════════════════════
VOICE USER MENU (per-user volume/mute/deafen)
═══════════════════════════════════════════════════════════ */
/* The "..." only renders on other people's rows. It was already hidden with
opacity:0 until hover, but opacity does not free the space: it stayed in the
flex flow with margin-left:auto and its own padding, so every other user's
badge and status icons stopped a button-width short of where your own row's
did. Nothing lined up down the column.
Taking it out of the flow and reserving the same slot on every row (see the
padding-right on .voice-user-item below) makes the columns agree whether or
not a row has a button. pointer-events keeps the invisible button from
swallowing clicks in that corner. */
.voice-user-menu-btn {
position: absolute; /* .user-item is position: relative */
right: 2px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 16px;
cursor: pointer;
padding: 0;
line-height: 1;
letter-spacing: 1px;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s, color 0.15s, background 0.15s;
}
/* Reserve the slot on every row, so rows without a button match those with one. */
.user-item.voice-user-item { padding-right: 20px; }
.voice-user-item:hover .voice-user-menu-btn,
.voice-user-menu-btn:focus-visible { opacity: 1; pointer-events: auto; }
.voice-user-menu-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
.voice-user-menu {
position: fixed;
z-index: 10002;
min-width: 200px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 6px 24px rgba(0,0,0,0.5);
padding: 8px;
animation: gear-menu-in 0.12s ease-out;
}
.voice-user-menu-header {
font-size: 12px;
font-weight: 700;
color: var(--text-primary);
padding: 0 4px 6px;
border-bottom: 1px solid var(--border);
margin-bottom: 6px;
}
.voice-user-menu-row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
}
.voice-user-menu-label {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.voice-user-vol-slider {
flex: 1;
min-width: 60px;
}
.voice-user-vol-value {
font-size: 11px;
color: var(--text-muted);
min-width: 32px;
text-align: right;
}
.voice-user-menu-actions {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
}
.voice-user-menu-action {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
font-size: 12px;
color: var(--text-secondary);
background: none;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
width: 100%;
text-align: left;
transition: background 0.1s;
}
.voice-user-menu-action:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.voice-user-menu-action.active {
background: rgba(231, 76, 60, 0.15);
color: #e74c3c;
}
.voice-user-menu-hint {
padding: 4px 8px;
color: var(--text-muted);
font-size: 11px;
line-height: 1.5;
border-top: 1px solid var(--border-light);
}
/* ═══════════════════════════════════════════════════════════
VOICE CONTROLS PANEL (right sidebar, below voice users)
═══════════════════════════════════════════════════════════ */
.voice-panel {
display: none;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--border);
background: var(--bg-tertiary);
flex-shrink: 0;
}
.voice-panel-btn {
width: 34px;
height: 34px;
border-radius: 50%;
font-size: 15px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
color: var(--text-secondary);
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
line-height: 1;
}
.voice-panel-btn:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--accent); }
.voice-panel-btn:disabled { opacity: 0.32; cursor: not-allowed; filter: grayscale(0.6); }
.voice-panel-btn:disabled:hover { background: transparent; border-color: var(--border); color: var(--text-muted); }
.voice-panel-btn.muted { background: var(--warning); color: var(--warning-text); border-color: var(--warning); position: relative; }
.voice-panel-btn.muted::after {
content: '';
position: absolute;
top: 50%; left: 18%;
width: 64%; height: 2px;
background: #c00;
transform: rotate(-45deg);
pointer-events: none;
}
.voice-panel-btn.sharing { background: var(--success); color: var(--success-text); border-color: var(--success); }
.voice-panel-btn.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.voice-panel-divider {
width: 1px;
height: 22px;
background: var(--border);
margin: 0 2px;
flex-shrink: 0;
}
.voice-panel-leave { width: 28px; height: 28px; font-size: 12px; background: var(--danger); color: var(--danger-text); border-color: var(--danger); }
.voice-panel-leave:hover { background: #d93636; color: #fff; border-color: #d93636; }
.voice-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.voice-panel-header .panel-title { margin-bottom: 0; }
.voice-header-actions { display: flex; gap: 4px; align-items: center; }
.voice-header-btn {
width: 22px;
height: 22px;
border-radius: 6px;
font-size: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
color: var(--text-secondary);
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
line-height: 1;
flex-shrink: 0;
}
.voice-header-btn:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--accent); }
.voice-header-btn.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.voice-header-btn.muted { background: var(--warning); color: var(--warning-text); border-color: var(--warning); position: relative; }
.voice-header-btn.muted::after {
content: '';
position: absolute;
top: 50%; left: 18%;
width: 64%; height: 2px;
background: #c00;
transform: rotate(-45deg);
pointer-events: none;
}
.voice-header-leave { background: var(--danger); color: var(--danger-text); border-color: var(--danger); }
.voice-header-leave:hover { background: #d93636; color: #fff; border-color: #d93636; }
.voice-settings-panel {
padding: 8px 12px;
background: var(--bg-tertiary);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.voice-settings-row {
display: flex;
align-items: center;
gap: 8px;
}
.voice-settings-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.voice-settings-panel .ns-slider {
flex: 1;
width: auto;
min-width: 60px;
}
.voice-settings-divider {
height: 1px;
background: var(--border);
margin: 6px 0;
}
.voice-settings-section-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
margin-bottom: 4px;
}
.voice-settings-select {
flex: 1;
min-width: 70px;
padding: 2px 6px;
font-size: 11px;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
outline: none;
cursor: pointer;
max-width: 180px;
text-overflow: ellipsis;
}
.voice-settings-select:focus {
border-color: var(--accent);
}
.voice-settings-row .voice-settings-select {
margin-bottom: 2px;
}
.bitrate-stepper {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
min-width: 0;
}
.bitrate-step-btn {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
line-height: 1;
color: var(--text-primary);
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
padding: 0;
}
.bitrate-step-btn:hover {
border-color: var(--accent);
}
.bitrate-value {
min-width: 76px;
width: 86px;
text-align: center;
font-size: 11px;
font-weight: 600;
font-family: inherit;
color: var(--text-primary);
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 2px 6px;
white-space: nowrap;
outline: none;
}
.bitrate-value:focus {
border-color: var(--accent);
}
/* ═══════════════════════════════════════════════════════════
PERSISTENT VOICE BAR (sidebar bottom)
═══════════════════════════════════════════════════════════ */
.voice-bar {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
background: var(--bg-tertiary);
color: var(--text-primary);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
font-size: 11px;
font-weight: 600;
min-width: 0;
overflow: hidden;
}
.voice-bar-top {
display: flex;
align-items: flex-start;
gap: 8px;
}
.voice-bar-status {
display: flex;
align-items: flex-start;
gap: 8px;
min-width: 0;
flex: 1;
}
.voice-bar-icon {
font-size: 12px;
color: var(--text-secondary);
margin-top: 1px;
}
.voice-bar-status-copy {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.voice-bar-status-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.voice-bar-channel {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
font-weight: 700;
color: var(--text-secondary);
}
.voice-bar-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.voice-bar-leave {
margin-left: auto;
background: transparent;
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 6px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.voice-bar-leave:hover {
background: var(--bg-hover);
border-color: var(--accent);
color: var(--text-primary);
}
.voice-bar-badges {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 4px;
}
.voice-bar-badge {
padding: 2px 6px;
border-radius: 999px;
background: var(--bg-hover);
border: 1px solid var(--border);
color: var(--text-secondary);
font-size: 10px;
font-weight: 700;
line-height: 1.2;
}
/* Push to mute held in Haven Desktop: the muted mic gets a ring and the voice
bar says why, so a held key does not read as a mute that stays. */
.voice-header-btn.ptm-held,
.voice-panel-btn.ptm-held { box-shadow: 0 0 0 2px var(--danger, #e74c3c); }
.voice-bar-badge-ptm {
background: var(--warning);
border-color: var(--warning);
color: var(--warning-text);
}
@media (max-width: 768px) {
.btn-voice { padding: 5px 10px; font-size: 12px; }
.voice-label-text { display: none; }
#voice-join-btn { display: none !important; }
.mobile-voice-join { display: flex !important; }
}
@media (max-width: 480px) {
.voice-controls { flex-shrink: 0; }
.voice-controls { gap: 4px; flex-wrap: nowrap; flex-shrink: 0; overflow: visible; }
.btn-voice {
padding: 4px 8px;
font-size: 12px;
min-width: 0;
}
.voice-label-text { display: none; }
.voice-panel {
gap: 6px;
padding: 6px 8px;
}
.voice-panel-btn {
width: 36px;
height: 36px;
font-size: 16px;
}
}
@media (max-width: 360px) {
.btn-voice { font-size: 11px; padding: 5px 6px; }
.voice-controls { gap: 3px; }
}
@media (hover: none) {
.btn-voice { min-height: 36px; }
}
[data-fontsize="small"] .voice-user-item .user-item-name { font-size: 11px; }
[data-fontsize="large"] .voice-user-item .user-item-name { font-size: 16px; }
[data-fontsize="x-large"] .voice-user-item .user-item-name { font-size: 19px; }
#voice-panel.mod-float {
position: fixed !important;
bottom: 80px; right: 24px;
width: 220px;
z-index: 600;
border-radius: 12px;
border: 1px solid var(--border-light);
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
background: var(--bg-secondary);
}
#voice-panel.mod-voice-bottom {
position: fixed !important;
bottom: 0; left: 50%;
transform: translateX(-50%);
width: 400px; max-width: 90vw;
z-index: 600;
border-radius: 12px 12px 0 0;
border: 1px solid var(--border-light);
border-bottom: none;
box-shadow: 0 -4px 24px rgba(0,0,0,0.3);
background: var(--bg-secondary);
}
#voice-panel.mod-voice-left {
border-top: 1px solid var(--border);
}
@media (max-width: 600px) {
.right-sidebar .voice-panel {
padding-bottom: max(env(safe-area-inset-bottom, 0px), 8px);
}
}