feat: Response mobile layout support v1
All checks were successful
Queue Release Build / prepare (push) Successful in 1m6s
Deploy Web Apps / deploy (push) Successful in 7m35s
Queue Release Build / build-windows (push) Successful in 29m57s
Queue Release Build / build-linux (push) Successful in 46m28s
Queue Release Build / finalize (push) Successful in 49s

This commit is contained in:
2026-05-18 02:25:16 +02:00
parent ecb1a4b3a0
commit dea114aed0
45 changed files with 2369 additions and 377 deletions

View File

@@ -1,6 +1,41 @@
<div class="flex h-full min-h-0 flex-col">
<div class="border-b border-border px-3 py-3">
<div class="flex flex-col gap-2 md:flex-row md:items-center">
<!--
Mobile-only header row:
[Back] ----- Search ----- [Settings]
Hidden on >=md where the original inline header (search bar + buttons) is used.
-->
<div class="mb-2 flex items-center gap-2 md:hidden">
<button
type="button"
aria-label="Back to server view"
class="grid h-11 w-11 shrink-0 place-items-center rounded-lg border border-border bg-secondary text-muted-foreground transition-colors hover:bg-secondary/80"
[class.invisible]="!canGoBack()"
[disabled]="!canGoBack()"
(click)="goBack()"
>
<ng-icon
name="lucideArrowLeft"
class="h-5 w-5"
/>
</button>
<h1 class="min-w-0 flex-1 truncate text-center text-base font-semibold text-foreground">Search</h1>
<button
type="button"
aria-label="Settings"
class="grid h-11 w-11 shrink-0 place-items-center rounded-lg border border-border bg-secondary text-muted-foreground transition-colors hover:bg-secondary/80"
(click)="openSettings()"
>
<ng-icon
name="lucideSettings"
class="h-5 w-5"
/>
</button>
</div>
<div class="flex flex-row items-center gap-2">
<div class="relative min-w-0 flex-1">
<ng-icon
name="lucideSearch"
@@ -16,6 +51,7 @@
/>
</div>
<!-- Create button is shown inline next to the search input on all sizes; Settings is desktop-only here (mobile uses the top header row above). -->
<div class="flex shrink-0 items-center gap-2">
<button
type="button"
@@ -27,12 +63,12 @@
name="lucidePlus"
class="h-4 w-4"
/>
Create
<span>Create</span>
</button>
<button
type="button"
class="grid h-10 w-10 place-items-center rounded-lg border border-border bg-secondary transition-colors hover:bg-secondary/80"
class="hidden h-10 w-10 place-items-center rounded-lg border border-border bg-secondary transition-colors hover:bg-secondary/80 md:grid"
title="Settings"
(click)="openSettings()"
>
@@ -60,13 +96,51 @@
}
</div>
<!-- Mobile tab strip: toggle between People and Servers panes (hidden on >=md) -->
<div
role="tablist"
aria-label="Search results"
class="flex border-b border-border md:hidden"
>
<button
type="button"
role="tab"
[attr.aria-selected]="mobileTab() === 'people'"
class="flex-1 px-3 py-2.5 text-sm font-medium transition-colors border-b-2"
[class.border-primary]="mobileTab() === 'people'"
[class.text-foreground]="mobileTab() === 'people'"
[class.border-transparent]="mobileTab() !== 'people'"
[class.text-muted-foreground]="mobileTab() !== 'people'"
(click)="mobileTab.set('people')"
>
People
</button>
<button
type="button"
role="tab"
[attr.aria-selected]="mobileTab() === 'servers'"
class="flex-1 px-3 py-2.5 text-sm font-medium transition-colors border-b-2"
[class.border-primary]="mobileTab() === 'servers'"
[class.text-foreground]="mobileTab() === 'servers'"
[class.border-transparent]="mobileTab() !== 'servers'"
[class.text-muted-foreground]="mobileTab() !== 'servers'"
(click)="mobileTab.set('servers')"
>
Servers ({{ searchResults().length }})
</button>
</div>
<div class="grid min-h-0 flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[minmax(300px,380px)_1fr]">
<app-user-search-list
class="min-h-0 overflow-y-auto border-b border-border lg:border-b-0 lg:border-r"
[class.hidden]="isMobile() && mobileTab() !== 'people'"
[searchQuery]="searchQuery"
/>
<section class="min-h-0 overflow-y-auto">
<section
class="min-h-0 overflow-y-auto"
[class.hidden]="isMobile() && mobileTab() !== 'servers'"
>
<div class="sticky top-0 z-10 flex items-center justify-between border-b border-border bg-background/95 px-3 py-2 backdrop-blur">
<div>
<h3 class="text-sm font-semibold text-foreground">Servers</h3>
@@ -215,7 +289,7 @@
} @else {
<button
type="button"
class="pointer-events-none scale-95 rounded-md bg-primary px-2.5 py-1.5 text-xs font-semibold text-primary-foreground opacity-0 transition-[opacity,transform] duration-75 ease-out hover:scale-100 hover:opacity-100 group-hover:pointer-events-auto group-hover:scale-100 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:scale-100 group-focus-within:opacity-100"
class="rounded-md bg-primary px-2.5 py-1.5 text-xs font-semibold text-primary-foreground transition-[opacity,transform] duration-75 ease-out md:pointer-events-none md:scale-95 md:opacity-0 md:hover:scale-100 md:hover:opacity-100 md:group-hover:pointer-events-auto md:group-hover:scale-100 md:group-hover:opacity-100 md:group-focus-within:pointer-events-auto md:group-focus-within:scale-100 md:group-focus-within:opacity-100"
[attr.aria-label]="'Join ' + server.name"
(click)="joinServer(server)"
>

View File

@@ -18,6 +18,7 @@ import {
} from 'rxjs';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideArrowLeft,
lucideExternalLink,
lucideFileText,
lucideSearch,
@@ -34,14 +35,15 @@ import {
selectSearchResults,
selectIsSearching,
selectRoomsError,
selectSavedRooms
selectSavedRooms,
selectCurrentRoom
} from '../../../../store/rooms/rooms.selectors';
import {
Room,
User,
type PluginRequirementSummary
} from '../../../../shared-kernel';
import { ExternalLinkService } from '../../../../core/platform';
import { ExternalLinkService, ViewportService } from '../../../../core/platform';
import { SettingsModalService } from '../../../../core/services/settings-modal.service';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { type ServerInfo } from '../../domain/models/server-directory.model';
@@ -83,6 +85,7 @@ interface JoinPluginConsentDialog {
],
viewProviders: [
provideIcons({
lucideArrowLeft,
lucideExternalLink,
lucideFileText,
lucideSearch,
@@ -110,14 +113,22 @@ export class ServerSearchComponent implements OnInit {
private webrtc = inject(RealtimeSessionFacade);
private pluginRequirements = inject(PluginRequirementService);
private pluginStore = inject(PluginStoreService);
private viewport = inject(ViewportService);
private searchSubject = new Subject<string>();
private banLookupRequestVersion = 0;
/** True on mobile breakpoints. Drives the tabbed mobile layout. */
readonly isMobile = this.viewport.isMobile;
/** Active mobile tab. Ignored on desktop where both panes are visible side-by-side. */
readonly mobileTab = signal<'people' | 'servers'>('servers');
searchQuery = '';
searchResults = this.store.selectSignal(selectSearchResults);
isSearching = this.store.selectSignal(selectIsSearching);
error = this.store.selectSignal(selectRoomsError);
savedRooms = this.store.selectSignal(selectSavedRooms);
currentRoom = this.store.selectSignal(selectCurrentRoom);
currentUser = this.store.selectSignal(selectCurrentUser);
activeEndpoints = this.serverDirectory.activeServers;
bannedServerLookup = signal<Record<string, boolean>>({});
@@ -235,6 +246,24 @@ export class ServerSearchComponent implements OnInit {
this.settingsModal.open('network');
}
/**
* Navigate back from the Search page to the chat-room view (server rail + current server).
* Prefers the current room; falls back to the first saved room. No-op when the user has not
* joined any servers.
*/
goBack(): void {
const target = this.currentRoom() ?? this.savedRooms()[0] ?? null;
if (target) {
this.store.dispatch(RoomsActions.viewServer({ room: target }));
}
}
/** True when the back button has a destination (user is in or has joined at least one server). */
canGoBack(): boolean {
return !!this.currentRoom() || this.savedRooms().length > 0;
}
/** Join a previously saved room by converting it to a ServerInfo payload. */
joinSavedRoom(room: Room): void {
this.openJoinedRoom(room);