Fix Meet room tile layout and show account avatars when camera is off.
Deploy Ladill Meet / deploy (push) Successful in 50s

Move the participant name strip below the speaker area instead of overlaying it, and use Ladill profile photos on no-video tiles before falling back to initials.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-01 22:42:49 +00:00
co-authored by Cursor
parent aed7094fed
commit 0a8cd2b4f9
3 changed files with 123 additions and 23 deletions
+32 -9
View File
@@ -59,16 +59,46 @@ html.meet-room-page {
} }
.meet-room-page .meet-stage--plenary .meet-stage-spotlight { .meet-room-page .meet-stage--plenary .meet-stage-spotlight {
flex: 1 1 auto; flex: 1 1 0;
min-height: min(70vh, 100%); min-height: 0;
display: flex;
flex-direction: column;
} }
.meet-room-page .meet-stage--plenary .meet-stage-spotlight-tile { .meet-room-page .meet-stage--plenary .meet-stage-spotlight-tile {
height: 100%; height: 100%;
width: 100%; width: 100%;
max-height: 100%;
aspect-ratio: auto; aspect-ratio: auto;
} }
.meet-room-page [data-participant-tile] {
display: flex;
flex-direction: column;
min-height: 0;
}
.meet-room-page [data-tile-body] {
position: relative;
min-height: 0;
flex: 1 1 0;
}
.meet-room-page [data-tile-footer] {
flex-shrink: 0;
}
.meet-room-page .meet-audience-strip {
display: flex;
align-items: center;
flex-shrink: 0;
align-self: stretch;
min-height: 2.75rem;
margin-top: 0.25rem;
padding: 0.5rem 0 0.25rem;
border-top: 1px solid rgb(30 41 59 / 0.8);
}
.meet-room-page .meet-stage--panel #meet-stage-spotlight, .meet-room-page .meet-stage--panel #meet-stage-spotlight,
.meet-room-page .meet-stage--panel #meet-audience-strip { .meet-room-page .meet-stage--panel #meet-audience-strip {
display: none; display: none;
@@ -87,13 +117,6 @@ html.meet-room-page {
min-height: 12rem; min-height: 12rem;
} }
.meet-room-page .meet-audience-strip {
display: flex;
align-items: center;
min-height: 2.75rem;
padding: 0.25rem 0;
}
.meet-room-page .meet-audience-avatar { .meet-room-page .meet-audience-avatar {
position: relative; position: relative;
display: flex; display: flex;
+89 -12
View File
@@ -540,6 +540,82 @@ function meetRoom() {
}; };
}, },
participantForIdentity(identity) {
return this.sessionParticipants.find((entry) => entry.uuid === identity);
},
displayNameForParticipant(participant) {
const person = this.participantForIdentity(participant.identity);
return person?.display_name || participant.name || participant.identity;
},
avatarUrlForParticipant(participant) {
const person = this.participantForIdentity(participant.identity);
if (person?.avatar_url) {
return person.avatar_url;
}
if (person && ['host', 'co_host'].includes(person.role) && this.roomHost?.avatar_url) {
return this.roomHost.avatar_url;
}
if (this.roomHost?.user_ref && person?.user_ref === this.roomHost.user_ref && this.roomHost.avatar_url) {
return this.roomHost.avatar_url;
}
return null;
},
updateTileAvatarCircle(tile, participant) {
const circle = tile.querySelector('[data-tile-avatar-circle]');
if (!circle) {
return;
}
const displayName = this.displayNameForParticipant(participant);
const avatarUrl = this.avatarUrlForParticipant(participant);
const isAudioTile = tile.classList.contains('meet-audio-tile');
const sizeClass = isAudioTile ? 'h-[4.5rem] w-[4.5rem] text-xl' : 'h-20 w-20 text-2xl';
circle.replaceChildren();
if (avatarUrl) {
circle.className = `flex ${sizeClass} shrink-0 items-center justify-center overflow-hidden rounded-full shadow-lg`;
circle.style.background = '';
const img = document.createElement('img');
img.src = avatarUrl;
img.alt = '';
img.className = 'h-full w-full object-cover';
circle.appendChild(img);
return;
}
const hue = this.avatarHue(displayName);
circle.className = `flex ${sizeClass} shrink-0 items-center justify-center rounded-full font-semibold text-white shadow-lg`;
circle.style.background = `linear-gradient(135deg, hsl(${hue} 45% 42%), hsl(${(hue + 40) % 360} 50% 28%))`;
circle.textContent = this.participantInitials(displayName);
},
refreshParticipantTileAvatars() {
if (!room) {
return;
}
[room.localParticipant, ...room.remoteParticipants.values()].forEach((participant) => {
const tile = this.findParticipantTile(participant.identity);
if (tile) {
this.updateTileAvatarCircle(tile, participant);
tile.querySelector('[data-tile-name]')?.replaceChildren(
document.createTextNode(this.displayNameForParticipant(participant)),
);
}
});
},
isSpeakerPerson(person) { isSpeakerPerson(person) {
if (!person) return false; if (!person) return false;
@@ -1020,14 +1096,16 @@ function meetRoom() {
tile = document.createElement('div'); tile = document.createElement('div');
tile.dataset.participantTile = identity; tile.dataset.participantTile = identity;
tile.className = this.audioOnly tile.className = this.audioOnly
? 'meet-audio-tile relative flex aspect-square w-full max-w-[9rem] flex-col items-center justify-center overflow-hidden rounded-2xl bg-slate-900' ? 'meet-audio-tile relative flex aspect-square w-full max-w-[9rem] flex-col overflow-hidden rounded-2xl bg-slate-900'
: 'relative aspect-video w-full overflow-hidden rounded-xl bg-slate-900'; : 'relative flex aspect-video w-full flex-col overflow-hidden rounded-xl bg-slate-900 min-h-0';
tile.innerHTML = ` tile.innerHTML = `
<div data-tile-stage class="absolute inset-0 flex items-center justify-center"></div> <div data-tile-body class="relative min-h-0 flex-1">
<div data-tile-avatar class="absolute inset-0 flex flex-col items-center justify-center gap-3 px-4"> <div data-tile-stage class="absolute inset-0 flex items-center justify-center"></div>
<div data-tile-avatar-circle class="flex h-20 w-20 items-center justify-center rounded-full text-2xl font-semibold text-white shadow-lg"></div> <div data-tile-avatar class="absolute inset-0 flex items-center justify-center">
<div data-tile-avatar-circle class="flex h-20 w-20 shrink-0 items-center justify-center rounded-full text-2xl font-semibold text-white shadow-lg"></div>
</div>
</div> </div>
<div class="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-3 pb-2 pt-8"> <div data-tile-footer class="relative flex shrink-0 items-center justify-between gap-2 border-t border-slate-800/80 bg-slate-950/95 px-3 py-2">
<span data-tile-name class="truncate text-xs font-medium text-white"></span> <span data-tile-name class="truncate text-xs font-medium text-white"></span>
<div class="flex shrink-0 items-center gap-1.5"> <div class="flex shrink-0 items-center gap-1.5">
<span data-tile-speaking class="hidden meet-speaking-badge rounded-full bg-emerald-500/90 p-1 text-white" title="Speaking"> <span data-tile-speaking class="hidden meet-speaking-badge rounded-full bg-emerald-500/90 p-1 text-white" title="Speaking">
@@ -1053,13 +1131,10 @@ function meetRoom() {
tile.classList.toggle('meet-stage-spotlight-tile', container.id === 'meet-stage-spotlight'); tile.classList.toggle('meet-stage-spotlight-tile', container.id === 'meet-stage-spotlight');
tile.classList.toggle('meet-stage-panel-tile', container.id === 'video-grid' && this.stageLayout === 'panel'); tile.classList.toggle('meet-stage-panel-tile', container.id === 'video-grid' && this.stageLayout === 'panel');
tile.classList.toggle('aspect-video', container.id !== 'meet-stage-spotlight' && !this.audioOnly);
const displayName = participant.name || participant.identity; this.updateTileAvatarCircle(tile, participant);
const hue = this.avatarHue(displayName); tile.querySelector('[data-tile-name]').textContent = this.displayNameForParticipant(participant);
const circle = tile.querySelector('[data-tile-avatar-circle]');
circle.style.background = `linear-gradient(135deg, hsl(${hue} 45% 42%), hsl(${(hue + 40) % 360} 50% 28%))`;
circle.textContent = this.participantInitials(displayName);
tile.querySelector('[data-tile-name]').textContent = displayName;
const speaking = this.activeSpeakerIds.includes(identity); const speaking = this.activeSpeakerIds.includes(identity);
tile.classList.toggle('meet-tile-speaking', speaking); tile.classList.toggle('meet-tile-speaking', speaking);
@@ -1100,6 +1175,7 @@ function meetRoom() {
} }
avatar.classList.remove('hidden'); avatar.classList.remove('hidden');
this.updateTileAvatarCircle(tile, participant);
stage.innerHTML = ''; stage.innerHTML = '';
}, },
@@ -1727,6 +1803,7 @@ function meetRoom() {
if (data.participants) { if (data.participants) {
this.sessionParticipants = data.participants; this.sessionParticipants = data.participants;
this.pruneTilesFromSession(); this.pruneTilesFromSession();
this.refreshParticipantTileAvatars();
} }
if (typeof data.uses_stage_layout === 'boolean' || typeof data.stage_layout === 'string') { if (typeof data.uses_stage_layout === 'boolean' || typeof data.stage_layout === 'string') {
this.mergeStageConfig(data); this.mergeStageConfig(data);
+2 -2
View File
@@ -217,8 +217,8 @@
</p> </p>
</div> </div>
<main class="relative min-h-0 min-w-0 flex-1 p-4"> <main class="relative flex min-h-0 min-w-0 flex-1 flex-col p-4 pb-2">
<div id="meet-stage" class="flex h-full min-h-0 flex-col gap-3" :class="usesStageLayout ? 'meet-stage meet-stage--' + stageLayout : ''"> <div id="meet-stage" class="flex h-full min-h-0 flex-1 flex-col gap-3" :class="usesStageLayout ? 'meet-stage meet-stage--' + stageLayout : ''">
<div id="meet-stage-spotlight" class="meet-stage-spotlight relative min-h-0 flex-1 overflow-hidden rounded-xl bg-slate-900"></div> <div id="meet-stage-spotlight" class="meet-stage-spotlight relative min-h-0 flex-1 overflow-hidden rounded-xl bg-slate-900"></div>
<div id="video-grid" class="grid min-h-0 gap-2 sm:grid-cols-2 lg:grid-cols-3" :class="audioOnly ? 'meet-audio-grid' : (usesStageLayout ? 'meet-stage-panel-grid flex-1' : 'h-full')"></div> <div id="video-grid" class="grid min-h-0 gap-2 sm:grid-cols-2 lg:grid-cols-3" :class="audioOnly ? 'meet-audio-grid' : (usesStageLayout ? 'meet-stage-panel-grid flex-1' : 'h-full')"></div>
<div id="meet-audience-strip" class="meet-audience-strip shrink-0"></div> <div id="meet-audience-strip" class="meet-audience-strip shrink-0"></div>