<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/cyclomatic-complexity -->
<aside class="flex h-full min-h-0 flex-col bg-card">
  <div
    appThemeNode="roomPanelHeader"
    class="shrink-0 border-b border-border px-3 py-3"
  >
    @if (panelMode() === 'channels') {
      <div class="flex items-center gap-3">
        <div class="grid h-9 w-9 place-items-center overflow-hidden rounded-md bg-secondary text-sm font-semibold text-foreground">
          @if (currentRoom()?.icon) {
            <div
              aria-hidden="true"
              class="h-full w-full bg-cover bg-center bg-no-repeat"
              [style.backgroundImage]="'url(' + currentRoom()!.icon + ')'"
            ></div>
          } @else {
            {{ currentRoom()?.name?.charAt(0)?.toUpperCase() || '#' }}
          }
        </div>

        <div class="min-w-0 flex-1">
          <p class="truncate text-sm font-semibold text-foreground">{{ currentRoom()?.name || 'Server' }}</p>
          <p class="truncate text-xs text-muted-foreground">{{ currentRoom()?.description || 'Choose a text channel or jump into voice.' }}</p>
        </div>
      </div>
    } @else {
      <div class="flex items-center gap-3">
        <div class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-muted-foreground">
          <ng-icon
            name="lucideUsers"
            class="h-4 w-4"
          />
        </div>

        <div class="min-w-0 flex-1">
          <p class="text-sm font-semibold text-foreground">{{ knownUserCount() }} members</p>
          <p class="text-xs text-muted-foreground">{{ onlineRoomUsers().length + (currentUser() ? 1 : 0) }} online right now</p>
        </div>
      </div>
    }
  </div>

  <!-- Channels View -->
  @if (panelMode() === 'channels') {
    <div class="min-h-0 flex-1 overflow-auto">
      <!-- Text Channels -->
      <section
        appThemeNode="roomTextChannelsSection"
        class="px-2 py-3"
      >
        <div class="mb-2 flex items-center justify-between px-1">
          <h4 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Text Channels</h4>
          @if (canManageChannels()) {
            <button
              (click)="createChannel('text')"
              class="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
              title="Create Text Channel"
            >
              <ng-icon
                name="lucidePlus"
                class="w-3.5 h-3.5"
              />
            </button>
          }
        </div>
        <div class="space-y-1">
          @for (ch of textChannels(); track ch.id) {
            <button
              appThemeNode="roomTextChannelItem"
              class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors"
              [class.bg-secondary]="activeChannelId() === ch.id"
              [class.text-foreground]="activeChannelId() === ch.id"
              [class.font-medium]="activeChannelId() === ch.id"
              [class.text-foreground/60]="activeChannelId() !== ch.id"
              [class.hover:bg-secondary/70]="activeChannelId() !== ch.id"
              [class.hover:text-foreground/80]="activeChannelId() !== ch.id"
              (click)="selectTextChannel(ch.id)"
              (contextmenu)="openChannelContextMenu($event, ch)"
              data-channel-type="text"
              [attr.data-channel-name]="ch.name"
            >
              <span class="text-muted-foreground text-base">#</span>
              @if (renamingChannelId() === ch.id) {
                <input
                  #renameInput
                  type="text"
                  [value]="ch.name"
                  [class.border-destructive]="renamingChannelId() === ch.id && !!channelNameError()"
                  [title]="renamingChannelId() === ch.id ? (channelNameError() ?? '') : ''"
                  (keydown.enter)="confirmRename($event)"
                  (keydown.escape)="cancelRename()"
                  (blur)="confirmRename($event)"
                  (input)="clearChannelNameError()"
                  class="flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
                  (click)="$event.stopPropagation()"
                />
              } @else {
                <span class="flex-1 truncate">{{ ch.name }}</span>
              }

              @if (channelUnreadCount(ch.id) > 0) {
                <span class="ml-auto rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] font-semibold text-primary">
                  {{ formatUnreadCount(channelUnreadCount(ch.id)) }}
                </span>
              }
            </button>
          }
        </div>
      </section>

      <!-- Voice Channels -->
      <section
        appThemeNode="roomVoiceChannelsSection"
        class="border-t border-border px-2 py-3"
      >
        <div class="mb-2 flex items-center justify-between px-1">
          <h4 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Voice Channels</h4>
          @if (canManageChannels()) {
            <button
              (click)="createChannel('voice')"
              class="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
              title="Create Voice Channel"
            >
              <ng-icon
                name="lucidePlus"
                class="w-3.5 h-3.5"
              />
            </button>
          }
        </div>
        @if (!voiceEnabled()) {
          <p class="px-2 py-2 text-sm text-muted-foreground">Voice is disabled by host</p>
        }
        <div class="space-y-1">
          @for (ch of voiceChannels(); track ch.id) {
            <div
              class="rounded-md transition-colors"
              [class.bg-primary/10]="dragTargetVoiceChannelId() === ch.id"
              [class.ring-1]="dragTargetVoiceChannelId() === ch.id"
              [class.ring-primary/40]="dragTargetVoiceChannelId() === ch.id"
              (dragover)="onVoiceChannelDragOver($event, ch.id)"
              (dragleave)="onVoiceChannelDragLeave(ch.id)"
              (drop)="onVoiceChannelDrop($event, ch.id)"
            >
              <button
                appThemeNode="roomVoiceChannelItem"
                class="flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-secondary/60"
                (click)="joinVoice(ch.id)"
                (contextmenu)="openChannelContextMenu($event, ch)"
                [class.bg-secondary]="isCurrentRoom(ch.id)"
                [disabled]="!voiceEnabled()"
                [title]="isCurrentRoom(ch.id) ? 'Open stream workspace' : 'Join voice channel'"
                data-channel-type="voice"
                [attr.data-channel-name]="ch.name"
              >
                <span class="flex items-center gap-2 text-foreground/80">
                  <ng-icon
                    name="lucideMic"
                    class="w-4 h-4 text-muted-foreground"
                  />
                  @if (renamingChannelId() === ch.id) {
                    <input
                      #renameInput
                      type="text"
                      [value]="ch.name"
                      [class.border-destructive]="renamingChannelId() === ch.id && !!channelNameError()"
                      [title]="renamingChannelId() === ch.id ? (channelNameError() ?? '') : ''"
                      (keydown.enter)="confirmRename($event)"
                      (keydown.escape)="cancelRename()"
                      (blur)="confirmRename($event)"
                      (input)="clearChannelNameError()"
                      class="flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
                      (click)="$event.stopPropagation()"
                    />
                  } @else {
                    <span>{{ ch.name }}</span>
                  }
                </span>

                @if (isCurrentRoom(ch.id)) {
                  <span class="rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary">
                    {{ isVoiceWorkspaceExpanded() ? 'Open' : 'View' }}
                  </span>
                } @else if (voiceOccupancy(ch.id) > 0) {
                  <span class="text-xs text-muted-foreground">{{ voiceOccupancy(ch.id) }}</span>
                }
              </button>
              <!-- Voice users connected to this channel -->
              @if (voiceUsersInRoom(ch.id).length > 0) {
                <div
                  class="mt-1 space-y-1 border-l border-border pb-1 pl-2"
                  style="margin-left: 0.91rem"
                >
                  @for (u of voiceUsersInRoom(ch.id); track u.id) {
                    <div
                      appThemeNode="roomVoiceUserItem"
                      class="flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-secondary/50"
                      [class.cursor-pointer]="canDragVoiceUser(u)"
                      [class.opacity-60]="draggedVoiceUserId() === (u.id || u.oderId)"
                      [draggable]="canDragVoiceUser(u)"
                      (dragstart)="onVoiceUserDragStart($event, u)"
                      (dragend)="onVoiceUserDragEnd()"
                      (contextmenu)="openVoiceUserVolumeMenu($event, u)"
                    >
                      <app-user-avatar
                        [name]="u.displayName"
                        [avatarUrl]="u.avatarUrl"
                        size="xs"
                        [ringClass]="getVoiceUserRingClass(u)"
                      />
                      <span class="text-sm text-foreground/80 truncate flex-1">{{ u.displayName }}</span>
                      <!-- Connectivity warning -->
                      @if (hasConnectivityIssue(u)) {
                        <ng-icon
                          name="lucideAlertTriangle"
                          class="w-3.5 h-3.5 text-amber-500 shrink-0"
                          title="Connection issue - this user may not hear all participants. Consider adding a TURN server in Settings -> Network."
                        />
                      }
                      <!-- Ping latency indicator -->
                      @if (u.id !== currentUser()?.id) {
                        <span
                          class="w-2 h-2 rounded-full shrink-0"
                          [class]="getPingColorClass(u)"
                          [title]="getPeerLatency(u) !== null ? getPeerLatency(u) + ' ms' : 'Measuring...'"
                        ></span>
                      }
                      @if (isUserStreaming(u.oderId || u.id)) {
                        <button
                          (click)="viewStream(u.oderId || u.id); $event.stopPropagation()"
                          class="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold bg-red-500 text-white rounded animate-pulse hover:bg-red-600 transition-colors"
                        >
                          <ng-icon
                            [name]="getUserLiveIconName(u.oderId || u.id)"
                            class="w-2.5 h-2.5"
                          />
                          LIVE
                        </button>
                      }
                      @if (u.voiceState?.isMuted) {
                        <ng-icon
                          name="lucideMicOff"
                          class="w-4 h-4 text-muted-foreground"
                        />
                      }
                      @if (isUserLocallyMuted(u)) {
                        <ng-icon
                          name="lucideVolumeX"
                          class="w-4 h-4 text-destructive"
                          title="Muted by you"
                        />
                      }
                    </div>
                  }
                </div>
              }
            </div>
          }
        </div>
      </section>

      @if (pluginChannelSections().length > 0 || pluginMenuActions().length > 0 || pluginSidePanels().length > 0) {
        <section
          class="border-t border-border px-2 py-3"
          data-testid="plugin-room-side-panel"
        >
          <div class="mb-2 flex items-center justify-between gap-2 px-1">
            <h4 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Plugins</h4>
            <button
              type="button"
              class="inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-secondary/60 hover:text-foreground"
              aria-haspopup="menu"
              title="View plugins"
              (click)="openPluginActionMenu($event)"
            >
              <ng-icon
                name="lucidePackage"
                class="h-3.5 w-3.5"
              />
              <span>View plugins</span>
            </button>
          </div>

          @if (pluginChannelSections().length > 0) {
            <div class="space-y-1">
              @for (record of pluginChannelSections(); track record.id) {
                <button
                  class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-foreground/70 transition-colors hover:bg-secondary/60 hover:text-foreground"
                  [title]="record.pluginId"
                >
                  <ng-icon
                    [name]="record.contribution.type === 'video' ? 'lucideVideo' : 'lucideHash'"
                    class="h-4 w-4 text-muted-foreground"
                  />
                  <span class="min-w-0 flex-1 truncate">{{ record.contribution.label }}</span>
                </button>
              }
            </div>
          }

          @if (pluginSidePanels().length > 0) {
            <div class="mt-3 space-y-2">
              @for (record of pluginSidePanels(); track record.id) {
                <article class="max-h-64 overflow-auto rounded-md border border-border bg-background/40 p-2">
                  <p class="mb-2 truncate text-xs font-medium text-muted-foreground">{{ record.contribution.label }}</p>
                  <app-plugin-render-host [render]="record.contribution.render" />
                </article>
              }
            </div>
          }
        </section>
      }
    </div>
  }

  <!-- Users View -->
  @if (panelMode() === 'users') {
    <div class="flex-1 overflow-auto px-2 py-3">
      <!-- Current User (You) -->
      @if (currentUser()) {
        <div class="mb-4">
          <h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">You</h4>
          <div
            class="flex items-center gap-2 rounded-md bg-secondary/60 px-3 py-2 hover:bg-secondary/80 transition-colors cursor-pointer"
            role="button"
            tabindex="0"
            (click)="openProfileCard($event, currentUser()!, true); $event.stopPropagation()"
            (keydown.enter)="openProfileCard($event, currentUser()!, true); $event.stopPropagation()"
            (keydown.space)="openProfileCard($event, currentUser()!, true); $event.preventDefault(); $event.stopPropagation()"
          >
            <app-user-avatar
              [name]="currentUser()?.displayName || '?'"
              [avatarUrl]="currentUser()?.avatarUrl"
              size="sm"
              [status]="currentUser()?.status"
              [showStatusBadge]="true"
            />
            <div class="flex-1 min-w-0">
              <p class="text-sm text-foreground truncate">{{ currentUser()?.displayName }}</p>
              @if (currentUser()?.gameActivity; as activity) {
                <p class="mt-0.5 flex items-center gap-1 truncate text-[10px] text-muted-foreground">
                  <ng-icon
                    name="lucideGamepad2"
                    class="h-2.5 w-2.5 shrink-0"
                  />
                  @if (activity.store?.url) {
                    <button
                      type="button"
                      class="truncate text-left hover:text-foreground hover:underline"
                      (click)="openGameStore($event, activity)"
                      (dblclick)="$event.stopPropagation()"
                      (keydown.enter)="$event.stopPropagation()"
                      (keydown.space)="$event.stopPropagation()"
                    >
                      Playing {{ activity.name }}
                    </button>
                  } @else {
                    <span class="truncate">Playing {{ activity.name }}</span>
                  }
                  <span class="shrink-0">{{ gameActivityElapsed(currentUser()) }}</span>
                </p>
              }
              <div class="flex items-center gap-2">
                @if (currentUser()?.voiceState?.isConnected) {
                  <p class="text-[10px] text-muted-foreground flex items-center gap-1">
                    <ng-icon
                      name="lucideMic"
                      class="w-2.5 h-2.5"
                    />
                    In voice
                  </p>
                }
                @if (currentUser() && isUserStreaming(currentUser()!.oderId || currentUser()!.id)) {
                  <button
                    class="text-[10px] bg-red-500 text-white px-1.5 py-0.5 rounded-sm font-medium flex items-center gap-1 animate-pulse hover:bg-red-600 transition-colors"
                    (click)="viewStream(currentUser()!.oderId || currentUser()!.id); $event.stopPropagation()"
                  >
                    <ng-icon
                      [name]="getUserLiveIconName(currentUser()!.oderId || currentUser()!.id)"
                      class="w-2.5 h-2.5"
                    />
                    LIVE
                  </button>
                }
              </div>
            </div>
          </div>
        </div>
      }

      <!-- Other Online Users -->
      @if (onlineRoomUsers().length > 0) {
        <div class="mb-4">
          <h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">Online - {{ onlineRoomUsers().length }}</h4>
          <div class="space-y-1">
            @for (user of onlineRoomUsers(); track user.id) {
              <div
                class="group/user flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-secondary/50 cursor-pointer"
                [attr.data-testid]="'room-user-card-' + (user.oderId || user.id)"
                role="button"
                tabindex="0"
                (contextmenu)="openUserContextMenu($event, user)"
                (click)="openUserCard($event, user)"
                (dblclick)="openDirectMessage($event, user)"
                (keydown.enter)="openUserCard($event, user)"
                (keydown.space)="openUserCard($event, user); $event.preventDefault()"
              >
                <app-user-avatar
                  [name]="user.displayName"
                  [avatarUrl]="user.avatarUrl"
                  size="sm"
                  [status]="user.status"
                  [showStatusBadge]="true"
                />
                <div class="flex-1 min-w-0">
                  <div class="flex items-center gap-1.5">
                    <p class="text-sm text-foreground truncate">{{ user.displayName }}</p>
                    @if (user.role === 'host') {
                      <span class="text-[10px] bg-yellow-500/20 text-yellow-400 px-1 py-0.5 rounded font-medium">Owner</span>
                    } @else if (user.role === 'admin') {
                      <span class="text-[10px] bg-blue-500/20 text-blue-400 px-1 py-0.5 rounded font-medium">Admin</span>
                    } @else if (user.role === 'moderator') {
                      <span class="text-[10px] bg-green-500/20 text-green-400 px-1 py-0.5 rounded font-medium">Mod</span>
                    }
                  </div>
                  @if (user.gameActivity; as activity) {
                    <p class="mt-0.5 flex items-center gap-1 truncate text-[10px] text-muted-foreground">
                      <ng-icon
                        name="lucideGamepad2"
                        class="h-2.5 w-2.5 shrink-0"
                      />
                      @if (activity.store?.url) {
                        <button
                          type="button"
                          class="truncate text-left hover:text-foreground hover:underline"
                          (click)="openGameStore($event, activity)"
                          (dblclick)="$event.stopPropagation()"
                          (keydown.enter)="$event.stopPropagation()"
                          (keydown.space)="$event.stopPropagation()"
                        >
                          Playing {{ activity.name }}
                        </button>
                      } @else {
                        <span class="truncate">Playing {{ activity.name }}</span>
                      }
                      <span class="shrink-0">{{ gameActivityElapsed(user) }}</span>
                    </p>
                  }
                  <div class="flex items-center gap-2">
                    @if (user.voiceState?.isConnected) {
                      <p class="text-[10px] text-muted-foreground flex items-center gap-1">
                        <ng-icon
                          name="lucideMic"
                          class="w-2.5 h-2.5"
                        />
                        In voice
                      </p>
                    }
                    @if (isUserStreaming(user.oderId || user.id)) {
                      <button
                        (click)="viewStream(user.oderId || user.id); $event.stopPropagation()"
                        class="text-[10px] bg-red-500 text-white px-1.5 py-0.5 rounded-sm font-medium hover:bg-red-600 transition-colors flex items-center gap-1 animate-pulse"
                      >
                        <ng-icon
                          [name]="getUserLiveIconName(user.oderId || user.id)"
                          class="w-2.5 h-2.5"
                        />
                        LIVE
                      </button>
                    }
                  </div>
                </div>
                <button
                  type="button"
                  class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-card hover:text-foreground group-hover/user:opacity-100 focus:opacity-100"
                  title="Message"
                  [attr.aria-label]="'Message ' + user.displayName"
                  (click)="openDirectMessage($event, user)"
                  (dblclick)="$event.stopPropagation()"
                >
                  <ng-icon
                    name="lucideMessageSquare"
                    class="h-4 w-4"
                  />
                </button>
              </div>
            }
          </div>
        </div>
      }

      <!-- Offline Users -->
      @if (offlineRoomMembers().length > 0) {
        <div class="mb-4">
          <h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">Offline - {{ offlineRoomMembers().length }}</h4>
          <div class="space-y-1">
            @for (member of offlineRoomMembers(); track member.oderId || member.id) {
              <div
                class="group/user flex items-center gap-2 rounded-md px-3 py-2 opacity-80 hover:bg-secondary/30 transition-colors cursor-pointer"
                [attr.data-testid]="'room-user-card-' + (member.oderId || member.id)"
                role="button"
                tabindex="0"
                (click)="openMemberCard($event, member)"
                (dblclick)="openDirectMessageForMember($event, member)"
                (keydown.enter)="openMemberCard($event, member)"
                (keydown.space)="openMemberCard($event, member); $event.preventDefault()"
              >
                <app-user-avatar
                  [name]="member.displayName"
                  [avatarUrl]="member.avatarUrl"
                  size="sm"
                  status="disconnected"
                  [showStatusBadge]="true"
                />
                <div class="flex-1 min-w-0">
                  <div class="flex items-center gap-1.5">
                    <p class="text-sm text-foreground/80 truncate">{{ member.displayName }}</p>
                    @if (member.role === 'host') {
                      <span class="text-[10px] bg-yellow-500/20 text-yellow-400 px-1 py-0.5 rounded font-medium">Owner</span>
                    } @else if (member.role === 'admin') {
                      <span class="text-[10px] bg-blue-500/20 text-blue-400 px-1 py-0.5 rounded font-medium">Admin</span>
                    } @else if (member.role === 'moderator') {
                      <span class="text-[10px] bg-green-500/20 text-green-400 px-1 py-0.5 rounded font-medium">Mod</span>
                    }
                  </div>
                  <p class="text-[10px] text-muted-foreground">Offline</p>
                </div>
                <button
                  type="button"
                  class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-card hover:text-foreground group-hover/user:opacity-100 focus:opacity-100"
                  title="Message"
                  [attr.aria-label]="'Message ' + member.displayName"
                  (click)="openDirectMessageForMember($event, member)"
                  (dblclick)="$event.stopPropagation()"
                >
                  <ng-icon
                    name="lucideMessageSquare"
                    class="h-4 w-4"
                  />
                </button>
              </div>
            }
          </div>
        </div>
      }

      <!-- No other users message -->
      @if (onlineRoomUsers().length === 0 && offlineRoomMembers().length === 0) {
        <div class="text-center py-4 text-muted-foreground">
          <p class="text-sm">No other users in this server</p>
        </div>
      }
    </div>
  }

  <!-- Voice controls pinned to sidebar bottom (hidden when floating controls visible) -->
  @if (panelMode() === 'channels' && showVoiceControls() && voiceEnabled()) {
    @if (localUserHasDesync()) {
      <div class="mx-2 mb-1 flex shrink-0 items-center gap-2 rounded-md bg-amber-500/15 px-3 py-2 text-xs text-amber-400">
        <ng-icon
          name="lucideAlertTriangle"
          class="w-4 h-4 shrink-0"
        />
        <span>You may have connectivity issues. Adding a TURN server in Settings -> Network may help.</span>
      </div>
    }
    <div
      class="shrink-0 border-t border-border px-2 py-3"
      [class.invisible]="showFloatingControls()"
    >
      <app-voice-controls />
    </div>
  }
</aside>

<!-- Channel context menu -->
@if (panelMode() === 'channels' && showChannelMenu()) {
  <app-context-menu
    [x]="channelMenuX()"
    [y]="channelMenuY()"
    (closed)="closeChannelMenu()"
    [width]="'w-44'"
  >
    <button
      (click)="resyncMessages()"
      class="context-menu-item"
    >
      Resync Messages
    </button>
    @if (contextChannel()?.type === 'text') {
      <button
        (click)="toggleChannelNotifications()"
        class="context-menu-item"
      >
        {{ isContextChannelMuted() ? 'Unmute Notifications' : 'Mute Notifications' }}
      </button>
    }
    @if (canManageChannels()) {
      <div class="context-menu-divider"></div>
      <button
        (click)="startRename()"
        class="context-menu-item"
      >
        Rename Channel
      </button>
      <button
        (click)="deleteChannel()"
        class="context-menu-item-danger"
      >
        Delete Channel
      </button>
    }
  </app-context-menu>
}

<!-- User context menu (kick / role management) -->
@if (panelMode() === 'users' && showUserMenu()) {
  <app-context-menu
    [x]="userMenuX()"
    [y]="userMenuY()"
    (closed)="closeUserMenu()"
  >
    @if (contextMenuUser(); as selectedUser) {
      @if (canChangeUserRole(selectedUser) && selectedUser.role === 'member') {
        <button
          (click)="changeUserRole('moderator')"
          class="context-menu-item"
        >
          Promote to Moderator
        </button>
        <button
          (click)="changeUserRole('admin')"
          class="context-menu-item"
        >
          Promote to Admin
        </button>
      }
      @if (canChangeUserRole(selectedUser) && selectedUser.role === 'moderator') {
        <button
          (click)="changeUserRole('admin')"
          class="context-menu-item"
        >
          Promote to Admin
        </button>
        <button
          (click)="changeUserRole('member')"
          class="context-menu-item"
        >
          Demote to Member
        </button>
      }
      @if (canChangeUserRole(selectedUser) && selectedUser.role === 'admin') {
        <button
          (click)="changeUserRole('member')"
          class="context-menu-item"
        >
          Demote to Member
        </button>
      }
      @if (canChangeUserRole(selectedUser) && canKickUser(selectedUser)) {
        <div class="context-menu-divider"></div>
      }
      @if (canKickUser(selectedUser)) {
        <button
          (click)="kickUserAction()"
          class="context-menu-item-danger"
        >
          Kick User
        </button>
      }
      @if (!canChangeUserRole(selectedUser) && !canKickUser(selectedUser)) {
        <div class="context-menu-empty">No actions available</div>
      }
    }
  </app-context-menu>
}

<!-- Per-user volume context menu -->
@if (panelMode() === 'channels' && showVolumeMenu()) {
  <app-user-volume-menu
    [x]="volumeMenuX()"
    [y]="volumeMenuY()"
    [peerId]="volumeMenuPeerId()"
    [displayName]="volumeMenuDisplayName()"
    (closed)="showVolumeMenu.set(false)"
  />
}

<!-- Create channel dialog -->
@if (panelMode() === 'channels' && showCreateChannelDialog()) {
  <app-confirm-dialog
    [title]="'Create ' + (createChannelType() === 'text' ? 'Text' : 'Voice') + ' Channel'"
    confirmLabel="Create"
    (confirmed)="confirmCreateChannel()"
    (cancelled)="cancelCreateChannel()"
  >
    <input
      type="text"
      [(ngModel)]="newChannelName"
      placeholder="Channel name"
      class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
      [class.border-destructive]="!!channelNameError()"
      (ngModelChange)="clearChannelNameError()"
      (keydown.enter)="confirmCreateChannel()"
    />
    @if (channelNameError()) {
      <p class="mt-2 text-sm text-destructive">{{ channelNameError() }}</p>
    }
  </app-confirm-dialog>
}
