Calls: screen share, mic/cam toggles, status badges, pin + fullscreen, call timer
Screen sharing swaps the outgoing video track on every peer's sender with replaceTrack (same m-line, no renegotiation); the camera returns when the share stops, including via the browser's own "Stop sharing" bar. Mic and camera toggles flip track.enabled. A new Status signal kind broadcasts mic/cam/screen state so tiles can show badges; newcomers get it directed when their Join arrives. Tiles carry a name label, click to pin as a stage (a peer's screen share pins itself), double-click for fullscreen. The call bar has an elapsed timer and a participant count. Controls are icon-only inline SVGs on currentColor: plain with a strike when off, a soft glow and ticking LED when live. Red is reserved for leave. Also fixed along the way: the peer map was a non-reactive StoredValue, so the tile list never re-rendered when someone joined; ICE candidates that arrive before the remote description are now buffered instead of rejected; a peer whose connection fails or closes is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L3sD29ozDvqA7jozeZXZoB
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c1c337c2a2
commit
78e9c9ff94
+141
-9
@@ -534,8 +534,8 @@ body::before {
|
||||
.call-join {
|
||||
font-family: var(--mono);
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
color: var(--ink-0);
|
||||
background: var(--signal);
|
||||
border: none;
|
||||
@@ -554,26 +554,156 @@ body::before {
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.video-tile {
|
||||
.tile {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
background: var(--ink-0);
|
||||
border: 1px solid var(--line-hot);
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: border-color 120ms;
|
||||
}
|
||||
|
||||
.video-tile-local { border-color: var(--signal-dim); }
|
||||
.tile:hover { border-color: var(--signal-dim); }
|
||||
.tile.local { border-color: var(--signal-dim); }
|
||||
.tile.pinned { border-color: var(--signal); box-shadow: 0 0 0 1px var(--signal); }
|
||||
|
||||
.video-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: var(--ink-0);
|
||||
}
|
||||
|
||||
/* a shared screen is letterboxed, never cropped */
|
||||
.tile.screen .video-tile { object-fit: contain; }
|
||||
.tile.cam-off .video-tile { opacity: 0.25; }
|
||||
|
||||
/* stage mode: the pinned tile fills the row, everyone else in a strip */
|
||||
.video-grid.staged .tile.pinned {
|
||||
order: -1;
|
||||
flex: 1 0 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.video-grid.staged .tile.pinned .video-tile { object-fit: contain; }
|
||||
|
||||
.video-grid.staged .tile:not(.pinned) { width: 140px; height: 105px; }
|
||||
|
||||
.tile-label,
|
||||
.tile-flags {
|
||||
position: absolute;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 0.3rem 0.45rem;
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--ink-0) 75%, transparent);
|
||||
}
|
||||
|
||||
.tile-flags {
|
||||
top: 0.3rem;
|
||||
right: 0.3rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.flag {
|
||||
display: inline-flex;
|
||||
padding: 0.22rem;
|
||||
color: var(--text-dim);
|
||||
background: color-mix(in srgb, var(--ink-0) 80%, transparent);
|
||||
border: 1px solid var(--line-hot);
|
||||
}
|
||||
|
||||
.flag .call-icon { width: 12px; height: 12px; }
|
||||
|
||||
.flag.on { color: var(--ink-0); background: var(--signal); border-color: var(--signal); }
|
||||
|
||||
/* control bar */
|
||||
|
||||
.call-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem 1rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.call-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.call-timer { color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.call-count { display: inline-flex; align-items: center; gap: 0.3rem; font-variant-numeric: tabular-nums; }
|
||||
.call-count .call-icon { width: 14px; height: 14px; }
|
||||
|
||||
.call-ctls { display: inline-flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||
|
||||
.call-ctl {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--text);
|
||||
background: none;
|
||||
border: 1px solid var(--line-hot);
|
||||
padding: 0.5rem 0.7rem;
|
||||
cursor: pointer;
|
||||
transition: background 120ms, border-color 120ms, color 120ms;
|
||||
}
|
||||
|
||||
.call-ctl:hover { border-color: var(--signal-dim); }
|
||||
|
||||
/* capture controls: plain when off, and when live a soft glow with the
|
||||
same ticking LED as the bus status - "this is recording", not an alarm */
|
||||
.call-ctl .led { width: 6px; height: 6px; }
|
||||
.call-ctl.live {
|
||||
border-color: var(--signal-dim);
|
||||
box-shadow: 0 0 10px color-mix(in srgb, var(--signal) 30%, transparent);
|
||||
}
|
||||
.call-ctl.live .led {
|
||||
background: var(--signal);
|
||||
box-shadow: 0 0 8px var(--signal);
|
||||
animation: blink 1.4s steps(1) infinite;
|
||||
}
|
||||
|
||||
.call-ctl.on { color: var(--ink-0); background: var(--signal); border-color: var(--signal); }
|
||||
|
||||
.call-icon { display: block; width: 18px; height: 18px; }
|
||||
|
||||
/* strike: a background stroke in the control's own fill colour first, so
|
||||
the slash reads as cutting the glyph rather than overlapping it */
|
||||
.call-icon-strike-bg { stroke: var(--ink-1); stroke-width: 5; }
|
||||
.call-ctl.on .call-icon-strike-bg { stroke: var(--signal); }
|
||||
.flag .call-icon-strike-bg { stroke: var(--ink-0); }
|
||||
|
||||
.call-leave {
|
||||
align-self: flex-start;
|
||||
font-family: var(--mono);
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 0.95rem;
|
||||
line-height: 18px;
|
||||
color: var(--text);
|
||||
background: none;
|
||||
border: 1px solid var(--alarm);
|
||||
padding: 0.5rem 0.9rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 120ms;
|
||||
}
|
||||
@@ -618,6 +748,8 @@ body::before {
|
||||
.room-link { border-left: 0; border-bottom: 2px solid transparent; }
|
||||
.room-link.active { border-bottom-color: var(--signal); }
|
||||
.msg { grid-template-columns: auto 1fr; grid-template-rows: auto auto; }
|
||||
.tile, .video-grid.staged .tile:not(.pinned) { width: calc(50% - 0.3rem); height: auto; aspect-ratio: 4 / 3; }
|
||||
.video-grid.staged .tile.pinned { width: 100%; }
|
||||
.msg-time { grid-row: 1; order: 2; justify-self: end; }
|
||||
.msg-text { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user