style: Consistent backdrop and create server in server-rail
wider server rail with larger icons ans slightly animated.
This commit is contained in:
@@ -70,7 +70,7 @@ Both endpoints live in `server/src/routes/servers.ts` and **must be registered b
|
|||||||
- `ServerDirectoryService` → `ServerDirectoryFacade` expose `getFeaturedServers()` / `getTrendingServers()` as the domain boundary.
|
- `ServerDirectoryService` → `ServerDirectoryFacade` expose `getFeaturedServers()` / `getTrendingServers()` as the domain boundary.
|
||||||
- `FindServersComponent` (`/servers`) composes **Recently active** (the user's saved rooms, capped at 6), **Featured**, and **Trending** sections, all rendered through `app-server-browser` with `[showMyServers]="true"`.
|
- `FindServersComponent` (`/servers`) composes **Recently active** (the user's saved rooms, capped at 6), **Featured**, and **Trending** sections, all rendered through `app-server-browser` with `[showMyServers]="true"`.
|
||||||
- `DashboardComponent` (`/dashboard`) is a single-column landing page (max-width centered, no in-page sidebars): a header greeting (no emoji), a global search with `Ctrl+K` focus and localStorage-backed **Recent Searches** chips shown beneath it, three primary action cards (Find People → `/people`, Find Servers → `/servers`, Create Server → `/create-server` — one link each), and discovery panels **People you might know**, **Popular Servers**, **Your Friends**, and **Recently Active Servers**. Each list is capped at 5 (`DISCOVERY_LIMIT`). It loads `popularServers` on init from `getFeaturedServers(5)`, falling back to `getTrendingServers(5)` when featured is empty; reuses `app-friend-button` for Add and `app-user-avatar` for people rows. `peopleYouMightKnow` excludes existing friends (via `FriendService.friendIds()`); `friends` lists discovered people who are friends. "See all" header links route to the matching `/people` or `/servers` page (no duplicated footer links). Recent searches are recorded on Enter (deduped, most-recent-first, capped at 8) and persisted under `metoyou_dashboard_recent_searches`.
|
- `DashboardComponent` (`/dashboard`) is a single-column landing page (max-width centered, no in-page sidebars): a header greeting (no emoji), a global search with `Ctrl+K` focus and localStorage-backed **Recent Searches** chips shown beneath it, three primary action cards (Find People → `/people`, Find Servers → `/servers`, Create Server → `/create-server` — one link each), and discovery panels **People you might know**, **Popular Servers**, **Your Friends**, and **Recently Active Servers**. Each list is capped at 5 (`DISCOVERY_LIMIT`). It loads `popularServers` on init from `getFeaturedServers(5)`, falling back to `getTrendingServers(5)` when featured is empty; reuses `app-friend-button` for Add and `app-user-avatar` for people rows. `peopleYouMightKnow` excludes existing friends (via `FriendService.friendIds()`); `friends` lists discovered people who are friends. "See all" header links route to the matching `/people` or `/servers` page (no duplicated footer links). Recent searches are recorded on Enter (deduped, most-recent-first, capped at 8) and persisted under `metoyou_dashboard_recent_searches`.
|
||||||
- The servers-rail top button (`servers-rail.component`) is the **Dashboard** button (`lucideLayoutDashboard`, `title="Dashboard"`); its `goToDashboard()` handler deselects any active voice server and navigates to `/dashboard`. Creating a server is reached via the dashboard / `/create-server` link instead.
|
- The servers-rail top button (`servers-rail.component`) is the **Dashboard** button (`lucideLayoutDashboard`, `title="Dashboard"`); its `goToDashboard()` handler deselects any active voice server and navigates to `/dashboard`. A **Create a server** button (`lucidePlus`, `data-testid="server-rail-create"`) sits below the saved-server icons and opens `app-create-server-dialog` (a Toju modal on desktop / bottom sheet on mobile) which dispatches `RoomsActions.createRoom` directly; the dashboard / `/create-server` route remains as an alternative entry point. Rail icons (`h-12 w-12`, `md:h-11 w-11`) animate their corner radius on hover and `:active` for a Discord-style squircle effect.
|
||||||
- On mobile (`ViewportService.isMobile()`), `DashboardComponent`, `FindPeopleComponent` (`/people`), and `FindServersComponent` (`/servers`) each mount their page body inside a single `<swiper-container>` slide next to `app-servers-rail` (rail `shrink-0`, content `flex-1` with a left border), mirroring the chat-room / DM-workspace mobile layout so the primary navigation rail stays reachable. The page body is shared between the desktop and mobile branches via an `<ng-template #pageContent>` + `[ngTemplateOutlet]`, and each component declares `schemas: [CUSTOM_ELEMENTS_SCHEMA]` for the Swiper custom elements.
|
- On mobile (`ViewportService.isMobile()`), `DashboardComponent`, `FindPeopleComponent` (`/people`), and `FindServersComponent` (`/servers`) each mount their page body inside a single `<swiper-container>` slide next to `app-servers-rail` (rail `shrink-0`, content `flex-1` with a left border), mirroring the chat-room / DM-workspace mobile layout so the primary navigation rail stays reachable. The page body is shared between the desktop and mobile branches via an `<ng-template #pageContent>` + `[ngTemplateOutlet]`, and each component declares `schemas: [CUSTOM_ELEMENTS_SCHEMA]` for the Swiper custom elements.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus, @angular-eslint/template/prefer-ngsrc -->
|
<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus, @angular-eslint/template/prefer-ngsrc -->
|
||||||
@if (lightboxAttachment()) {
|
@if (lightboxAttachment()) {
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
(click)="closeLightbox()"
|
(click)="closeLightbox()"
|
||||||
(contextmenu)="openImageContextMenu($event, lightboxAttachment()!)"
|
(contextmenu)="openImageContextMenu($event, lightboxAttachment()!)"
|
||||||
(keydown.escape)="closeLightbox()"
|
(keydown.escape)="closeLightbox()"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@if (session()) {
|
@if (session()) {
|
||||||
<div class="fixed inset-0 z-[120] bg-black/60 backdrop-blur-sm"></div>
|
<app-modal-backdrop [zIndex]="120" [dismissable]="false" />
|
||||||
|
|
||||||
<div class="pointer-events-none fixed inset-0 z-[121] flex items-center justify-center p-4">
|
<div class="pointer-events-none fixed inset-0 z-[121] flex items-center justify-center p-4">
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
import { lucidePhone, lucidePhoneOff } from '@ng-icons/lucide';
|
import { lucidePhone, lucidePhoneOff } from '@ng-icons/lucide';
|
||||||
import { UserAvatarComponent } from '../../../../shared';
|
import { ModalBackdropComponent, UserAvatarComponent } from '../../../../shared';
|
||||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||||
import { User } from '../../../../shared-kernel';
|
import { User } from '../../../../shared-kernel';
|
||||||
import { DirectCallService } from '../../application/services/direct-call.service';
|
import { DirectCallService } from '../../application/services/direct-call.service';
|
||||||
@@ -21,7 +21,8 @@ import { DirectCallSession, participantToUser } from '../../domain/models/direct
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
NgIcon,
|
NgIcon,
|
||||||
UserAvatarComponent
|
UserAvatarComponent,
|
||||||
|
ModalBackdropComponent
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
provideIcons({
|
provideIcons({
|
||||||
|
|||||||
@@ -432,10 +432,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (serverInstallDialog(); as dialog) {
|
@if (serverInstallDialog(); as dialog) {
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[80] bg-black/60"
|
[zIndex]="80"
|
||||||
role="presentation"
|
[dismissable]="false"
|
||||||
></div>
|
/>
|
||||||
<section
|
<section
|
||||||
class="fixed left-1/2 top-1/2 z-[81] flex max-h-[min(42rem,calc(100vh-2rem))] w-[min(34rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
class="fixed left-1/2 top-1/2 z-[81] flex max-h-[min(42rem,calc(100vh-2rem))] w-[min(34rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import type {
|
|||||||
} from '../../../../shared-kernel';
|
} from '../../../../shared-kernel';
|
||||||
import { selectCurrentRoom, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
|
import { selectCurrentRoom, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
|
||||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||||
|
import { ModalBackdropComponent } from '../../../../shared';
|
||||||
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
||||||
import { PluginStoreService } from '../../application/services/plugin-store.service';
|
import { PluginStoreService } from '../../application/services/plugin-store.service';
|
||||||
import type {
|
import type {
|
||||||
@@ -60,7 +61,8 @@ interface ServerPluginInstallDialog {
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ChatMessageMarkdownComponent,
|
ChatMessageMarkdownComponent,
|
||||||
NgIcon
|
NgIcon,
|
||||||
|
ModalBackdropComponent
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
provideIcons({
|
provideIcons({
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[112] bg-black/70 backdrop-blur-sm"
|
[zIndex]="112"
|
||||||
(click)="cancelled.emit(undefined)"
|
ariaLabel="Close profile image editor"
|
||||||
(keydown.enter)="cancelled.emit(undefined)"
|
(dismissed)="cancelled.emit(undefined)"
|
||||||
(keydown.space)="cancelled.emit(undefined)"
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Close profile image editor"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 z-[113] flex items-center justify-center p-4 pointer-events-none">
|
<div class="fixed inset-0 z-[113] flex items-center justify-center p-4 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ProfileAvatarFacade } from '../../application/services/profile-avatar.facade';
|
import { ProfileAvatarFacade } from '../../application/services/profile-avatar.facade';
|
||||||
|
import { ModalBackdropComponent } from '../../../../shared';
|
||||||
import {
|
import {
|
||||||
EditableProfileAvatarSource,
|
EditableProfileAvatarSource,
|
||||||
ProcessedProfileAvatar,
|
ProcessedProfileAvatar,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile-avatar-editor',
|
selector: 'app-profile-avatar-editor',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, ModalBackdropComponent],
|
||||||
templateUrl: './profile-avatar-editor.component.html'
|
templateUrl: './profile-avatar-editor.component.html'
|
||||||
})
|
})
|
||||||
export class ProfileAvatarEditorComponent {
|
export class ProfileAvatarEditorComponent {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<!--
|
||||||
|
Server-creation modal. Two presentations matching the rest of Toju's dialogs:
|
||||||
|
- Mobile: rendered through `app-bottom-sheet` so it slides up from the bottom.
|
||||||
|
- Desktop: centered modal with backdrop.
|
||||||
|
-->
|
||||||
|
@if (isMobile()) {
|
||||||
|
<app-bottom-sheet
|
||||||
|
title="Create a server"
|
||||||
|
ariaLabel="Create a server"
|
||||||
|
(dismissed)="cancel()"
|
||||||
|
>
|
||||||
|
<ng-container *ngTemplateOutlet="form" />
|
||||||
|
<div class="flex gap-2 border-t border-border p-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-h-11 flex-1 rounded-lg bg-secondary px-3 py-2 text-sm text-foreground transition-colors hover:bg-secondary/80"
|
||||||
|
(click)="cancel()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-h-11 flex-1 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
[disabled]="!canCreate"
|
||||||
|
(click)="create()"
|
||||||
|
>
|
||||||
|
Create server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</app-bottom-sheet>
|
||||||
|
} @else {
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<app-modal-backdrop
|
||||||
|
[zIndex]="40"
|
||||||
|
ariaLabel="Close dialog"
|
||||||
|
(dismissed)="cancel()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Dialog -->
|
||||||
|
<div
|
||||||
|
appThemeNode="confirmDialogSurface"
|
||||||
|
class="fixed left-1/2 top-1/2 z-50 flex max-h-[88vh] w-[460px] max-w-[92vw] -translate-x-1/2 -translate-y-1/2 flex-col rounded-lg border border-border bg-card shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="border-b border-border p-4">
|
||||||
|
<h4 class="font-semibold text-foreground">Create a server</h4>
|
||||||
|
<p class="mt-0.5 text-xs text-muted-foreground">Your server is where you and your community hang out.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||||
|
<ng-container *ngTemplateOutlet="form" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 border-t border-border p-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 rounded-lg bg-secondary px-3 py-2 text-sm text-foreground transition-colors hover:bg-secondary/80"
|
||||||
|
(click)="cancel()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="create-server-dialog-submit"
|
||||||
|
type="button"
|
||||||
|
class="flex-1 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
[disabled]="!canCreate"
|
||||||
|
(click)="create()"
|
||||||
|
>
|
||||||
|
Create server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Shared form body for both presentations. -->
|
||||||
|
<ng-template #form>
|
||||||
|
<div class="space-y-5 p-4">
|
||||||
|
<div>
|
||||||
|
<span class="mb-2 block text-sm font-medium text-foreground">Pick a category</span>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@for (category of categories; track category.id) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border px-3 py-1.5 text-sm font-medium transition-colors"
|
||||||
|
[class.border-primary]="selectedCategoryId() === category.id"
|
||||||
|
[class.bg-primary/10]="selectedCategoryId() === category.id"
|
||||||
|
[class.text-primary]="selectedCategoryId() === category.id"
|
||||||
|
[class.border-border]="selectedCategoryId() !== category.id"
|
||||||
|
[class.bg-secondary]="selectedCategoryId() !== category.id"
|
||||||
|
[class.text-foreground]="selectedCategoryId() !== category.id"
|
||||||
|
(click)="selectCategory(category)"
|
||||||
|
>
|
||||||
|
{{ category.label }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="create-server-dialog-name"
|
||||||
|
class="mb-1 block text-sm font-medium text-foreground"
|
||||||
|
>Server name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="create-server-dialog-name"
|
||||||
|
type="text"
|
||||||
|
[ngModel]="name()"
|
||||||
|
(ngModelChange)="name.set($event)"
|
||||||
|
placeholder="My Awesome Server"
|
||||||
|
class="w-full rounded-lg border border-border bg-secondary px-3 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="create-server-dialog-description"
|
||||||
|
class="mb-1 block text-sm font-medium text-foreground"
|
||||||
|
>Description (optional)</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="create-server-dialog-description"
|
||||||
|
[ngModel]="description()"
|
||||||
|
(ngModelChange)="description.set($event)"
|
||||||
|
placeholder="What's your server about?"
|
||||||
|
rows="3"
|
||||||
|
class="w-full resize-none rounded-lg border border-border bg-secondary px-3 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center justify-between px-3 py-2.5 text-left text-sm font-medium text-foreground"
|
||||||
|
[attr.aria-expanded]="showAdvanced()"
|
||||||
|
(click)="toggleAdvanced()"
|
||||||
|
>
|
||||||
|
<span>Advanced settings</span>
|
||||||
|
<ng-icon
|
||||||
|
[name]="showAdvanced() ? 'lucideChevronUp' : 'lucideChevronDown'"
|
||||||
|
class="h-4 w-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (showAdvanced()) {
|
||||||
|
<div class="space-y-4 border-t border-border p-3">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="create-server-dialog-topic"
|
||||||
|
class="mb-1 block text-sm font-medium text-foreground"
|
||||||
|
>Topic (optional)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="create-server-dialog-topic"
|
||||||
|
type="text"
|
||||||
|
[ngModel]="topic()"
|
||||||
|
(ngModelChange)="topic.set($event)"
|
||||||
|
placeholder="gaming, music, coding..."
|
||||||
|
class="w-full rounded-lg border border-border bg-secondary px-3 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="create-server-dialog-signal-endpoint"
|
||||||
|
class="mb-1 block text-sm font-medium text-foreground"
|
||||||
|
>Signal server endpoint</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="create-server-dialog-signal-endpoint"
|
||||||
|
[ngModel]="sourceId()"
|
||||||
|
(ngModelChange)="sourceId.set($event)"
|
||||||
|
class="w-full rounded-lg border border-border bg-secondary px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
>
|
||||||
|
@for (endpoint of activeEndpoints(); track endpoint.id) {
|
||||||
|
<option [value]="endpoint.id">{{ endpoint.name }} ({{ endpoint.url }})</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">This endpoint handles all signaling for this server.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="create-server-dialog-private"
|
||||||
|
type="checkbox"
|
||||||
|
[ngModel]="isPrivate()"
|
||||||
|
(ngModelChange)="isPrivate.set($event)"
|
||||||
|
class="h-4 w-4 rounded border-border bg-secondary"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="create-server-dialog-private"
|
||||||
|
class="text-sm text-foreground"
|
||||||
|
>Private server</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="create-server-dialog-password"
|
||||||
|
class="mb-1 block text-sm font-medium text-foreground"
|
||||||
|
>Password (optional)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="create-server-dialog-password"
|
||||||
|
type="password"
|
||||||
|
[ngModel]="password()"
|
||||||
|
(ngModelChange)="password.set($event)"
|
||||||
|
placeholder="Leave blank to allow joining without a password"
|
||||||
|
class="w-full rounded-lg border border-border bg-secondary px-3 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">Users who already joined keep access even if you change the password later.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
vi,
|
||||||
|
beforeEach
|
||||||
|
} from 'vitest';
|
||||||
|
import { Injector, runInInjectionContext } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { CreateServerDialogComponent } from './create-server-dialog.component';
|
||||||
|
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||||
|
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
|
||||||
|
import { ViewportService } from '../../../../core/platform';
|
||||||
|
|
||||||
|
function installLocalStorageMock(): void {
|
||||||
|
const store = new Map<string, string>();
|
||||||
|
|
||||||
|
vi.stubGlobal('localStorage', {
|
||||||
|
getItem: (key: string) => store.get(key) ?? null,
|
||||||
|
setItem: (key: string, value: string) => store.set(key, String(value)),
|
||||||
|
removeItem: (key: string) => store.delete(key),
|
||||||
|
clear: () => store.clear(),
|
||||||
|
key: (index: number) => Array.from(store.keys())[index] ?? null,
|
||||||
|
get length() {
|
||||||
|
return store.size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHarness() {
|
||||||
|
const dispatch = vi.fn();
|
||||||
|
const store = { dispatch } as unknown as Store;
|
||||||
|
const router = { navigate: vi.fn() } as unknown as Router;
|
||||||
|
const serverDirectory = {
|
||||||
|
activeServers: () => [{ id: 'ep-1', name: 'Local', url: 'https://local.test' }]
|
||||||
|
} as unknown as ServerDirectoryFacade;
|
||||||
|
const viewport = { isMobile: () => false } as unknown as ViewportService;
|
||||||
|
const injector = Injector.create({
|
||||||
|
providers: [
|
||||||
|
CreateServerDialogComponent,
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: ServerDirectoryFacade, useValue: serverDirectory },
|
||||||
|
{ provide: ViewportService, useValue: viewport }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const component = runInInjectionContext(injector, () => injector.get(CreateServerDialogComponent));
|
||||||
|
|
||||||
|
return { component, dispatch, router };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CreateServerDialogComponent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
installLocalStorageMock();
|
||||||
|
localStorage.setItem('metoyou_currentUserId', 'user-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults the signal endpoint to the first active endpoint', () => {
|
||||||
|
const { component } = createHarness();
|
||||||
|
|
||||||
|
expect(component.sourceId()).toBe('ep-1');
|
||||||
|
expect(component.canCreate).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches createRoom and emits created with the form values', () => {
|
||||||
|
const { component, dispatch } = createHarness();
|
||||||
|
const created = vi.fn();
|
||||||
|
|
||||||
|
component.created.subscribe(created);
|
||||||
|
|
||||||
|
component.name.set('My Server');
|
||||||
|
component.description.set('A place');
|
||||||
|
component.topic.set('gaming');
|
||||||
|
component.isPrivate.set(true);
|
||||||
|
component.password.set('secret');
|
||||||
|
|
||||||
|
component.create();
|
||||||
|
|
||||||
|
const action = dispatch.mock.calls.find(([entry]) => entry.type === RoomsActions.createRoom.type)?.[0];
|
||||||
|
|
||||||
|
expect(action).toMatchObject({
|
||||||
|
name: 'My Server',
|
||||||
|
description: 'A place',
|
||||||
|
topic: 'gaming',
|
||||||
|
isPrivate: true,
|
||||||
|
password: 'secret',
|
||||||
|
sourceId: 'ep-1'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(created).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not dispatch or emit created when the name is blank', () => {
|
||||||
|
const { component, dispatch } = createHarness();
|
||||||
|
const created = vi.fn();
|
||||||
|
|
||||||
|
component.created.subscribe(created);
|
||||||
|
|
||||||
|
component.create();
|
||||||
|
|
||||||
|
expect(dispatch.mock.calls.some(([entry]) => entry.type === RoomsActions.createRoom.type)).toBe(false);
|
||||||
|
expect(created).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies a category preset to the topic and toggles it off', () => {
|
||||||
|
const { component } = createHarness();
|
||||||
|
const gaming = component.categories[0];
|
||||||
|
|
||||||
|
component.selectCategory(gaming);
|
||||||
|
expect(component.topic()).toBe(gaming.topic);
|
||||||
|
expect(component.selectedCategoryId()).toBe(gaming.id);
|
||||||
|
|
||||||
|
component.selectCategory(gaming);
|
||||||
|
expect(component.topic()).toBe('');
|
||||||
|
expect(component.selectedCategoryId()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits cancelled when cancelled', () => {
|
||||||
|
const { component } = createHarness();
|
||||||
|
const cancelled = vi.fn();
|
||||||
|
|
||||||
|
component.cancelled.subscribe(cancelled);
|
||||||
|
|
||||||
|
component.cancel();
|
||||||
|
|
||||||
|
expect(cancelled).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to login and does not dispatch when not logged in', () => {
|
||||||
|
const { component, dispatch, router } = createHarness();
|
||||||
|
|
||||||
|
localStorage.removeItem('metoyou_currentUserId');
|
||||||
|
component.name.set('My Server');
|
||||||
|
|
||||||
|
component.create();
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/login']);
|
||||||
|
expect(dispatch.mock.calls.some(([entry]) => entry.type === RoomsActions.createRoom.type)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/member-ordering */
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
HostListener,
|
||||||
|
inject,
|
||||||
|
output,
|
||||||
|
signal
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
|
import { lucideChevronDown, lucideChevronUp } from '@ng-icons/lucide';
|
||||||
|
|
||||||
|
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||||
|
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
|
||||||
|
import { ThemeNodeDirective } from '../../../theme';
|
||||||
|
import { ViewportService } from '../../../../core/platform';
|
||||||
|
import { BottomSheetComponent, ModalBackdropComponent } from '../../../../shared';
|
||||||
|
import { CATEGORY_PRESETS, ServerCategoryPreset } from '../create-server/create-server.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal presentation of the server-creation form. Mirrors the dedicated
|
||||||
|
* `/create-server` page but is rendered as a Toju modal (desktop) / bottom sheet
|
||||||
|
* (mobile) so it can be opened straight from the servers rail. Emits `created`
|
||||||
|
* once a room creation has been dispatched and `cancelled` when dismissed.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-server-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
NgIcon,
|
||||||
|
ThemeNodeDirective,
|
||||||
|
BottomSheetComponent,
|
||||||
|
ModalBackdropComponent
|
||||||
|
],
|
||||||
|
viewProviders: [provideIcons({ lucideChevronDown, lucideChevronUp })],
|
||||||
|
templateUrl: './create-server-dialog.component.html',
|
||||||
|
host: {
|
||||||
|
style: 'display: contents;'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class CreateServerDialogComponent {
|
||||||
|
private store = inject(Store);
|
||||||
|
private router = inject(Router);
|
||||||
|
private serverDirectory = inject(ServerDirectoryFacade);
|
||||||
|
|
||||||
|
readonly isMobile = inject(ViewportService).isMobile;
|
||||||
|
|
||||||
|
readonly created = output<undefined>();
|
||||||
|
readonly cancelled = output<undefined>();
|
||||||
|
|
||||||
|
readonly categories = CATEGORY_PRESETS;
|
||||||
|
activeEndpoints = this.serverDirectory.activeServers;
|
||||||
|
|
||||||
|
name = signal('');
|
||||||
|
description = signal('');
|
||||||
|
topic = signal('');
|
||||||
|
selectedCategoryId = signal<string | null>(null);
|
||||||
|
isPrivate = signal(false);
|
||||||
|
password = signal('');
|
||||||
|
sourceId = signal('');
|
||||||
|
showAdvanced = signal(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.sourceId.set(this.activeEndpoints()[0]?.id ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the form has enough to create a server. */
|
||||||
|
get canCreate(): boolean {
|
||||||
|
return this.name().trim().length > 0 && this.sourceId().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCategory(category: ServerCategoryPreset): void {
|
||||||
|
if (this.selectedCategoryId() === category.id) {
|
||||||
|
this.selectedCategoryId.set(null);
|
||||||
|
this.topic.set('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedCategoryId.set(category.id);
|
||||||
|
this.topic.set(category.topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAdvanced(): void {
|
||||||
|
this.showAdvanced.update((shown) => !shown);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keydown.escape')
|
||||||
|
cancel(): void {
|
||||||
|
this.cancelled.emit(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
if (!this.canCreate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||||
|
|
||||||
|
if (!currentUserId) {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.dispatch(
|
||||||
|
RoomsActions.createRoom({
|
||||||
|
name: this.name().trim(),
|
||||||
|
description: this.description().trim() || undefined,
|
||||||
|
topic: this.topic().trim() || undefined,
|
||||||
|
isPrivate: this.isPrivate(),
|
||||||
|
password: this.password().trim() || undefined,
|
||||||
|
sourceId: this.sourceId() || undefined
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.created.emit(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ export interface ServerCategoryPreset {
|
|||||||
topic: string;
|
topic: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_PRESETS: ServerCategoryPreset[] = [
|
export const CATEGORY_PRESETS: ServerCategoryPreset[] = [
|
||||||
{ id: 'gaming', label: 'Gaming', topic: 'gaming' },
|
{ id: 'gaming', label: 'Gaming', topic: 'gaming' },
|
||||||
{ id: 'music', label: 'Music', topic: 'music' },
|
{ id: 'music', label: 'Music', topic: 'music' },
|
||||||
{ id: 'coding', label: 'Coding', topic: 'coding' },
|
{ id: 'coding', label: 'Coding', topic: 'coding' },
|
||||||
|
|||||||
@@ -290,10 +290,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (pluginConsentDialog(); as dialog) {
|
@if (pluginConsentDialog(); as dialog) {
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-50 bg-black/50"
|
[zIndex]="50"
|
||||||
role="presentation"
|
[dismissable]="false"
|
||||||
></div>
|
/>
|
||||||
<section
|
<section
|
||||||
class="fixed left-1/2 top-1/2 z-[51] flex max-h-[min(42rem,calc(100vh-2rem))] w-[min(34rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
class="fixed left-1/2 top-1/2 z-[51] flex max-h-[min(42rem,calc(100vh-2rem))] w-[min(34rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -469,11 +469,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
@if (pluginConsentReadme(); as readme) {
|
@if (pluginConsentReadme(); as readme) {
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[52] bg-black/60"
|
[zIndex]="52"
|
||||||
role="presentation"
|
ariaLabel="Close readme"
|
||||||
(click)="closePluginConsentReadme()"
|
(dismissed)="closePluginConsentReadme()"
|
||||||
></div>
|
/>
|
||||||
<section
|
<section
|
||||||
class="fixed left-1/2 top-1/2 z-[53] flex max-h-[min(44rem,calc(100vh-2rem))] w-[min(44rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
class="fixed left-1/2 top-1/2 z-[53] flex max-h-[min(44rem,calc(100vh-2rem))] w-[min(44rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
|||||||
import {
|
import {
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
LeaveServerDialogComponent,
|
LeaveServerDialogComponent,
|
||||||
|
ModalBackdropComponent,
|
||||||
type LeaveServerDialogResult
|
type LeaveServerDialogResult
|
||||||
} from '../../../../shared';
|
} from '../../../../shared';
|
||||||
import { ChatMessageMarkdownComponent } from '../../../chat';
|
import { ChatMessageMarkdownComponent } from '../../../chat';
|
||||||
@@ -89,7 +90,8 @@ export interface ServerDiscoverySection {
|
|||||||
NgIcon,
|
NgIcon,
|
||||||
ChatMessageMarkdownComponent,
|
ChatMessageMarkdownComponent,
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
LeaveServerDialogComponent
|
LeaveServerDialogComponent,
|
||||||
|
ModalBackdropComponent
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
provideIcons({
|
provideIcons({
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<nav class="relative flex h-full min-w-14 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:w-full">
|
<nav class="relative flex h-full min-w-16 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:w-full">
|
||||||
<!-- Home / dashboard button -->
|
<!-- Home / dashboard button -->
|
||||||
<button
|
<button
|
||||||
appThemeNode="serversRailCreateButton"
|
appThemeNode="serversRailCreateButton"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex h-11 w-11 items-center justify-center overflow-hidden rounded-md bg-primary transition-colors hover:bg-primary/90 md:h-10 md:w-10"
|
class="flex h-12 w-12 items-center justify-center overflow-hidden rounded-2xl bg-primary transition-[border-radius,background-color] duration-150 ease-out hover:rounded-xl hover:bg-primary/90 active:rounded-lg md:h-11 md:w-11"
|
||||||
title="Dashboard"
|
title="Dashboard"
|
||||||
(click)="goToDashboard()"
|
(click)="goToDashboard()"
|
||||||
>
|
>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="relative z-10 grid h-11 w-11 place-items-center overflow-hidden rounded-xl transition-colors hover:rounded-lg md:h-10 md:w-10"
|
class="relative z-10 grid h-12 w-12 place-items-center overflow-hidden rounded-xl transition-[border-radius,background-color] duration-150 ease-out hover:rounded-lg active:rounded-2xl md:h-11 md:w-11"
|
||||||
[ngClass]="
|
[ngClass]="
|
||||||
callAvatarUrls(call).length > 0
|
callAvatarUrls(call).length > 0
|
||||||
? 'bg-emerald-950 text-white shadow-sm hover:bg-emerald-900'
|
? 'bg-emerald-950 text-white shadow-sm hover:bg-emerald-900'
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucidePhone"
|
name="lucidePhone"
|
||||||
class="relative z-10 h-[22px] w-[22px] drop-shadow md:h-5 md:w-5"
|
class="relative z-10 h-6 w-6 drop-shadow md:h-[22px] md:w-[22px]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<button
|
<button
|
||||||
appThemeNode="serversRailItem"
|
appThemeNode="serversRailItem"
|
||||||
type="button"
|
type="button"
|
||||||
class="relative z-10 flex h-11 w-11 cursor-pointer flex-shrink-0 items-center justify-center border border-transparent transition-[border-radius,box-shadow,background-color] duration-100 hover:rounded-lg hover:bg-card md:h-10 md:w-10"
|
class="relative z-10 flex h-12 w-12 cursor-pointer flex-shrink-0 items-center justify-center border border-transparent transition-[border-radius,box-shadow,background-color] duration-150 ease-out hover:rounded-lg hover:bg-card active:rounded-2xl md:h-11 md:w-11"
|
||||||
[ngClass]="isSelectedRoom(room) ? 'rounded-lg ring-2 ring-primary/40 bg-primary/10' : 'rounded-xl bg-card'"
|
[ngClass]="isSelectedRoom(room) ? 'rounded-lg ring-2 ring-primary/40 bg-primary/10' : 'rounded-xl bg-card'"
|
||||||
[title]="room.name"
|
[title]="room.name"
|
||||||
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
|
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
|
||||||
@@ -135,6 +135,29 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Separator between servers and create button -->
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="my-0.5 h-px w-12 shrink-0 bg-border/70 md:w-11"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Create server -->
|
||||||
|
<div class="group/create relative flex w-full shrink-0 justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative z-10 grid h-12 w-12 place-items-center rounded-xl bg-card text-emerald-500 transition-[border-radius,background-color,color] duration-150 ease-out hover:rounded-lg hover:bg-emerald-500 hover:text-white active:rounded-2xl md:h-11 md:w-11"
|
||||||
|
data-testid="server-rail-create"
|
||||||
|
title="Create a server"
|
||||||
|
aria-label="Create a server"
|
||||||
|
(click)="openCreateDialog()"
|
||||||
|
>
|
||||||
|
<ng-icon
|
||||||
|
name="lucidePlus"
|
||||||
|
class="h-6 w-6 md:h-[22px] md:w-[22px]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -235,3 +258,10 @@
|
|||||||
(cancelled)="cancelLeave()"
|
(cancelled)="cancelLeave()"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (showCreateDialog()) {
|
||||||
|
<app-create-server-dialog
|
||||||
|
(created)="closeCreateDialog()"
|
||||||
|
(cancelled)="closeCreateDialog()"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
import { lucidePhone } from '@ng-icons/lucide';
|
import { lucidePhone, lucidePlus } from '@ng-icons/lucide';
|
||||||
import {
|
import {
|
||||||
EMPTY,
|
EMPTY,
|
||||||
Subject,
|
Subject,
|
||||||
@@ -37,6 +37,7 @@ import { NotificationsFacade } from '../../../domains/notifications';
|
|||||||
import { DirectCallService, DirectCallSession } from '../../../domains/direct-call';
|
import { DirectCallService, DirectCallSession } from '../../../domains/direct-call';
|
||||||
import { DmRailComponent } from '../../../domains/direct-message/feature/dm-rail/dm-rail.component';
|
import { DmRailComponent } from '../../../domains/direct-message/feature/dm-rail/dm-rail.component';
|
||||||
import { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
|
import { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
|
||||||
|
import { CreateServerDialogComponent } from '../../../domains/server-directory/feature/create-server-dialog/create-server-dialog.component';
|
||||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||||
import { hasRoomBanForUser } from '../../../domains/access-control';
|
import { hasRoomBanForUser } from '../../../domains/access-control';
|
||||||
import {
|
import {
|
||||||
@@ -56,12 +57,13 @@ const ACTIVATION_DEBOUNCE_MS = 150;
|
|||||||
NgIcon,
|
NgIcon,
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
ContextMenuComponent,
|
ContextMenuComponent,
|
||||||
|
CreateServerDialogComponent,
|
||||||
DmRailComponent,
|
DmRailComponent,
|
||||||
LeaveServerDialogComponent,
|
LeaveServerDialogComponent,
|
||||||
ThemeNodeDirective,
|
ThemeNodeDirective,
|
||||||
UserBarComponent
|
UserBarComponent
|
||||||
],
|
],
|
||||||
viewProviders: [provideIcons({ lucidePhone })],
|
viewProviders: [provideIcons({ lucidePhone, lucidePlus })],
|
||||||
templateUrl: './servers-rail.component.html'
|
templateUrl: './servers-rail.component.html'
|
||||||
})
|
})
|
||||||
export class ServersRailComponent {
|
export class ServersRailComponent {
|
||||||
@@ -90,6 +92,7 @@ export class ServersRailComponent {
|
|||||||
contextRoom = signal<Room | null>(null);
|
contextRoom = signal<Room | null>(null);
|
||||||
optimisticSelectedRoomId = signal<string | null>(null);
|
optimisticSelectedRoomId = signal<string | null>(null);
|
||||||
showLeaveConfirm = signal(false);
|
showLeaveConfirm = signal(false);
|
||||||
|
showCreateDialog = signal(false);
|
||||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||||
bannedRoomLookup = signal<Record<string, boolean>>({});
|
bannedRoomLookup = signal<Record<string, boolean>>({});
|
||||||
@@ -257,6 +260,14 @@ export class ServersRailComponent {
|
|||||||
this.router.navigate(['/dashboard']);
|
this.router.navigate(['/dashboard']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openCreateDialog(): void {
|
||||||
|
this.showCreateDialog.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCreateDialog(): void {
|
||||||
|
this.showCreateDialog.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
joinSavedRoom(room: Room): void {
|
joinSavedRoom(room: Room): void {
|
||||||
const targetRoom = this.savedRooms().find((savedRoom) => savedRoom.id === room.id) ?? room;
|
const targetRoom = this.savedRooms().find((savedRoom) => savedRoom.id === room.id) ?? room;
|
||||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@if (isOpen() && !isThemeStudioFullscreen()) {
|
@if (isOpen() && !isThemeStudioFullscreen()) {
|
||||||
<!-- Backdrop (hidden on mobile where the modal is full-screen) -->
|
<!-- Backdrop (hidden on mobile where the modal is full-screen) -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-[90] hidden bg-black/80 backdrop-blur-sm transition-opacity duration-200 md:block"
|
class="fixed inset-0 z-[90] hidden bg-black/60 backdrop-blur-sm transition-opacity duration-200 md:block"
|
||||||
[class.opacity-100]="animating()"
|
[class.opacity-100]="animating()"
|
||||||
[class.opacity-0]="!animating()"
|
[class.opacity-0]="!animating()"
|
||||||
(click)="onBackdropClick()"
|
(click)="onBackdropClick()"
|
||||||
|
|||||||
@@ -272,10 +272,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (requiredPluginRequirements().length > 0 && currentRoom()) {
|
@if (requiredPluginRequirements().length > 0 && currentRoom()) {
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[80] bg-black/60"
|
[zIndex]="80"
|
||||||
role="presentation"
|
[dismissable]="false"
|
||||||
></div>
|
/>
|
||||||
<section
|
<section
|
||||||
class="fixed left-1/2 top-1/2 z-[81] flex max-h-[min(38rem,calc(100vh-2rem))] w-[min(32rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
class="fixed left-1/2 top-1/2 z-[81] flex max-h-[min(38rem,calc(100vh-2rem))] w-[min(32rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { ServerDirectoryFacade } from '../../../domains/server-directory';
|
|||||||
import { PlatformService } from '../../../core/platform';
|
import { PlatformService } from '../../../core/platform';
|
||||||
import { clearStoredCurrentUserId } from '../../../core/storage/current-user-storage';
|
import { clearStoredCurrentUserId } from '../../../core/storage/current-user-storage';
|
||||||
import { SettingsModalService } from '../../../core/services/settings-modal.service';
|
import { SettingsModalService } from '../../../core/services/settings-modal.service';
|
||||||
import { LeaveServerDialogComponent } from '../../../shared';
|
import { LeaveServerDialogComponent, ModalBackdropComponent } from '../../../shared';
|
||||||
import { Room, type PluginRequirementSummary } from '../../../shared-kernel';
|
import { Room, type PluginRequirementSummary } from '../../../shared-kernel';
|
||||||
import { VoiceWorkspaceService } from '../../../domains/voice-session';
|
import { VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||||
@@ -59,7 +59,8 @@ import { getPluginInstallScope } from '../../../domains/plugins/domain/logic/plu
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
NgIcon,
|
NgIcon,
|
||||||
LeaveServerDialogComponent,
|
LeaveServerDialogComponent,
|
||||||
ThemeNodeDirective
|
ThemeNodeDirective,
|
||||||
|
ModalBackdropComponent
|
||||||
],
|
],
|
||||||
viewProviders: [
|
viewProviders: [
|
||||||
provideIcons({ lucideMinus,
|
provideIcons({ lucideMinus,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<!-- Dimmed backdrop. Tap to dismiss. -->
|
<!-- Dimmed backdrop. Tap to dismiss. -->
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[140] bg-black/40 backdrop-blur-sm"
|
[zIndex]="140"
|
||||||
(click)="onBackdropClick()"
|
ariaLabel="Close"
|
||||||
(keydown.enter)="onBackdropClick()"
|
(dismissed)="onBackdropClick()"
|
||||||
(keydown.space)="onBackdropClick()"
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Close"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Bottom sheet panel. Slides up from the bottom of the viewport. Drag the top handle downward
|
Bottom sheet panel. Slides up from the bottom of the viewport. Drag the top handle downward
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
signal
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||||
|
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mobile bottom-sheet container.
|
* Mobile bottom-sheet container.
|
||||||
@@ -32,7 +33,7 @@ import { ThemeNodeDirective } from '../../../domains/theme';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bottom-sheet',
|
selector: 'app-bottom-sheet',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThemeNodeDirective],
|
imports: [ThemeNodeDirective, ModalBackdropComponent],
|
||||||
templateUrl: './bottom-sheet.component.html',
|
templateUrl: './bottom-sheet.component.html',
|
||||||
styleUrl: './bottom-sheet.component.scss'
|
styleUrl: './bottom-sheet.component.scss'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,15 +37,11 @@
|
|||||||
</app-bottom-sheet>
|
</app-bottom-sheet>
|
||||||
} @else {
|
} @else {
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop -->
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-40 bg-black/30"
|
[zIndex]="40"
|
||||||
(click)="cancelled.emit(undefined)"
|
ariaLabel="Close dialog"
|
||||||
(keydown.enter)="cancelled.emit(undefined)"
|
(dismissed)="cancelled.emit(undefined)"
|
||||||
(keydown.space)="cancelled.emit(undefined)"
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Close dialog"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Dialog -->
|
<!-- Dialog -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,11 +8,16 @@ import {
|
|||||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||||
import { ViewportService } from '../../../core/platform';
|
import { ViewportService } from '../../../core/platform';
|
||||||
import { BottomSheetComponent } from '../bottom-sheet/bottom-sheet.component';
|
import { BottomSheetComponent } from '../bottom-sheet/bottom-sheet.component';
|
||||||
|
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-confirm-dialog',
|
selector: 'app-confirm-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThemeNodeDirective, BottomSheetComponent],
|
imports: [
|
||||||
|
ThemeNodeDirective,
|
||||||
|
BottomSheetComponent,
|
||||||
|
ModalBackdropComponent
|
||||||
|
],
|
||||||
templateUrl: './confirm-dialog.component.html',
|
templateUrl: './confirm-dialog.component.html',
|
||||||
host: {
|
host: {
|
||||||
style: 'display: contents;'
|
style: 'display: contents;'
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-40 bg-black/30"
|
[zIndex]="40"
|
||||||
(click)="cancelled.emit(undefined)"
|
ariaLabel="Close dialog"
|
||||||
(keydown.enter)="cancelled.emit(undefined)"
|
(dismissed)="cancelled.emit(undefined)"
|
||||||
(keydown.space)="cancelled.emit(undefined)"
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Close dialog"
|
|
||||||
></div>
|
|
||||||
<div
|
<div
|
||||||
class="fixed left-1/2 top-1/2 z-50 w-[360px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-card shadow-lg"
|
class="fixed left-1/2 top-1/2 z-50 w-[360px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-card shadow-lg"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||||
import {
|
import {
|
||||||
Room,
|
Room,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
@@ -22,7 +23,11 @@ export interface LeaveServerDialogResult {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-leave-server-dialog',
|
selector: 'app-leave-server-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ModalBackdropComponent
|
||||||
|
],
|
||||||
templateUrl: './leave-server-dialog.component.html'
|
templateUrl: './leave-server-dialog.component.html'
|
||||||
})
|
})
|
||||||
export class LeaveServerDialogComponent {
|
export class LeaveServerDialogComponent {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
@if (dismissable()) {
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/60"
|
||||||
|
[class.backdrop-blur-sm]="blur()"
|
||||||
|
[style.z-index]="zIndex()"
|
||||||
|
(click)="dismiss()"
|
||||||
|
(keydown.enter)="dismiss()"
|
||||||
|
(keydown.space)="dismiss()"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
[attr.aria-label]="ariaLabel()"
|
||||||
|
></div>
|
||||||
|
} @else {
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/60"
|
||||||
|
[class.backdrop-blur-sm]="blur()"
|
||||||
|
[style.z-index]="zIndex()"
|
||||||
|
role="presentation"
|
||||||
|
></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
vi
|
||||||
|
} from 'vitest';
|
||||||
|
import { Injector, runInInjectionContext } from '@angular/core';
|
||||||
|
|
||||||
|
import { ModalBackdropComponent } from './modal-backdrop.component';
|
||||||
|
|
||||||
|
function createComponent(): ModalBackdropComponent {
|
||||||
|
const injector = Injector.create({ providers: [ModalBackdropComponent] });
|
||||||
|
|
||||||
|
return runInInjectionContext(injector, () => injector.get(ModalBackdropComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ModalBackdropComponent', () => {
|
||||||
|
it('exposes consistent defaults (centered z-index, blurred, dismissable)', () => {
|
||||||
|
const component = createComponent();
|
||||||
|
|
||||||
|
expect(component.zIndex()).toBe(50);
|
||||||
|
expect(component.blur()).toBe(true);
|
||||||
|
expect(component.dismissable()).toBe(true);
|
||||||
|
expect(component.ariaLabel()).toBe('Close');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits dismissed when the backdrop is dismissed', () => {
|
||||||
|
const component = createComponent();
|
||||||
|
const handler = vi.fn();
|
||||||
|
|
||||||
|
component.dismissed.subscribe(handler);
|
||||||
|
|
||||||
|
component.dismiss();
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
output
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared modal backdrop.
|
||||||
|
*
|
||||||
|
* Renders the full-viewport dimming scrim that sits behind centered dialogs, bottom sheets, and
|
||||||
|
* other modal surfaces. Centralizing it keeps the backdrop darkness (`bg-black/60`) and the
|
||||||
|
* `backdrop-blur-sm` treatment consistent across every modal in the app - callers only pick a
|
||||||
|
* stacking level and (optionally) opt out of dismiss-on-click.
|
||||||
|
*
|
||||||
|
* The component renders only the scrim; the modal content should be a sibling element with a
|
||||||
|
* higher `z-index` so it layers above the backdrop.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```html
|
||||||
|
* <app-modal-backdrop [zIndex]="110" ariaLabel="Close dialog" (dismissed)="cancel()" />
|
||||||
|
* <div class="fixed inset-0 z-[111] flex items-center justify-center"> ...dialog... </div>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-modal-backdrop',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './modal-backdrop.component.html'
|
||||||
|
})
|
||||||
|
export class ModalBackdropComponent {
|
||||||
|
/** Stacking level applied to the scrim. Place modal content one level above this value. */
|
||||||
|
readonly zIndex = input<number>(50);
|
||||||
|
|
||||||
|
/** Whether to apply the frosted `backdrop-blur-sm` treatment. Defaults to on for consistency. */
|
||||||
|
readonly blur = input<boolean>(true);
|
||||||
|
|
||||||
|
/** Whether clicking / pressing the backdrop dismisses the modal. Disable for blocking modals. */
|
||||||
|
readonly dismissable = input<boolean>(true);
|
||||||
|
|
||||||
|
/** Accessible label for the dismiss affordance when the backdrop is dismissable. */
|
||||||
|
readonly ariaLabel = input<string>('Close');
|
||||||
|
|
||||||
|
/** Emits when the user dismisses the backdrop (click, Enter, or Space). */
|
||||||
|
readonly dismissed = output<undefined>();
|
||||||
|
|
||||||
|
/** Emit a dismissal request. Wired to the backdrop's pointer and keyboard handlers. */
|
||||||
|
dismiss(): void {
|
||||||
|
this.dismissed.emit(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[110] bg-black/70 backdrop-blur-sm"
|
[zIndex]="110"
|
||||||
(click)="cancelled.emit(undefined)"
|
ariaLabel="Close screen share quality dialog"
|
||||||
(keydown.enter)="cancelled.emit(undefined)"
|
(dismissed)="cancelled.emit(undefined)"
|
||||||
(keydown.space)="cancelled.emit(undefined)"
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Close screen share quality dialog"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 z-[111] flex items-center justify-center p-4 pointer-events-none">
|
<div class="fixed inset-0 z-[111] flex items-center justify-center p-4 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ScreenShareQuality, SCREEN_SHARE_QUALITY_OPTIONS } from '../../../domains/screen-share';
|
import { ScreenShareQuality, SCREEN_SHARE_QUALITY_OPTIONS } from '../../../domains/screen-share';
|
||||||
|
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-screen-share-quality-dialog',
|
selector: 'app-screen-share-quality-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, ModalBackdropComponent],
|
||||||
templateUrl: './screen-share-quality-dialog.component.html'
|
templateUrl: './screen-share-quality-dialog.component.html'
|
||||||
})
|
})
|
||||||
export class ScreenShareQualityDialogComponent implements OnInit {
|
export class ScreenShareQualityDialogComponent implements OnInit {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
@if (request(); as pickerRequest) {
|
@if (request(); as pickerRequest) {
|
||||||
<div
|
<app-modal-backdrop
|
||||||
class="fixed inset-0 z-[110] bg-black/70 backdrop-blur-sm"
|
[zIndex]="110"
|
||||||
(click)="cancel()"
|
ariaLabel="Close source picker"
|
||||||
(keydown.enter)="cancel()"
|
(dismissed)="cancel()"
|
||||||
(keydown.space)="cancel()"
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Close source picker"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 z-[111] flex items-center justify-center p-4 pointer-events-none">
|
<div class="fixed inset-0 z-[111] flex items-center justify-center p-4 pointer-events-none">
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ScreenShareSourcePickerService
|
ScreenShareSourcePickerService
|
||||||
} from '../../../domains/screen-share';
|
} from '../../../domains/screen-share';
|
||||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||||
|
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-screen-share-source-picker',
|
selector: 'app-screen-share-source-picker',
|
||||||
@@ -22,7 +23,8 @@ import { ThemeNodeDirective } from '../../../domains/theme';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
NgOptimizedImage,
|
NgOptimizedImage,
|
||||||
ThemeNodeDirective
|
ThemeNodeDirective,
|
||||||
|
ModalBackdropComponent
|
||||||
],
|
],
|
||||||
templateUrl: './screen-share-source-picker.component.html',
|
templateUrl: './screen-share-source-picker.component.html',
|
||||||
styleUrl: './screen-share-source-picker.component.scss',
|
styleUrl: './screen-share-source-picker.component.scss',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Shared reusable UI components barrel.
|
* Shared reusable UI components barrel.
|
||||||
*/
|
*/
|
||||||
export { ContextMenuComponent } from './components/context-menu/context-menu.component';
|
export { ContextMenuComponent } from './components/context-menu/context-menu.component';
|
||||||
|
export { ModalBackdropComponent } from './components/modal-backdrop/modal-backdrop.component';
|
||||||
export { BottomSheetComponent } from './components/bottom-sheet/bottom-sheet.component';
|
export { BottomSheetComponent } from './components/bottom-sheet/bottom-sheet.component';
|
||||||
export { UserAvatarComponent } from './components/user-avatar/user-avatar.component';
|
export { UserAvatarComponent } from './components/user-avatar/user-avatar.component';
|
||||||
export { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component';
|
export { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component';
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
@import '@angular/cdk/overlay-prebuilt.css';
|
@import '@angular/cdk/overlay-prebuilt.css';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keep CDK overlay dark backdrops in sync with the `bg-black/60` darkness used by all
|
||||||
|
* component-level modal backdrops (confirm dialog, settings modal, bottom sheets, etc.).
|
||||||
|
* The prebuilt CDK class defaults to rgba(0, 0, 0, 0.32), which is noticeably lighter.
|
||||||
|
*/
|
||||||
|
.cdk-overlay-dark-backdrop {
|
||||||
|
background: rgb(0 0 0 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes profile-card-in {
|
@keyframes profile-card-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user