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
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:
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user