style: Consistent backdrop and create server in server-rail
wider server rail with larger icons ans slightly animated.
This commit is contained in:
@@ -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 -->
|
||||
@if (lightboxAttachment()) {
|
||||
<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()"
|
||||
(contextmenu)="openImageContextMenu($event, lightboxAttachment()!)"
|
||||
(keydown.escape)="closeLightbox()"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@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">
|
||||
<section
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucidePhone, lucidePhoneOff } from '@ng-icons/lucide';
|
||||
import { UserAvatarComponent } from '../../../../shared';
|
||||
import { ModalBackdropComponent, UserAvatarComponent } from '../../../../shared';
|
||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import { User } from '../../../../shared-kernel';
|
||||
import { DirectCallService } from '../../application/services/direct-call.service';
|
||||
@@ -21,7 +21,8 @@ import { DirectCallSession, participantToUser } from '../../domain/models/direct
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
UserAvatarComponent
|
||||
UserAvatarComponent,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
|
||||
@@ -432,10 +432,10 @@
|
||||
</div>
|
||||
|
||||
@if (serverInstallDialog(); as dialog) {
|
||||
<div
|
||||
class="fixed inset-0 z-[80] bg-black/60"
|
||||
role="presentation"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="80"
|
||||
[dismissable]="false"
|
||||
/>
|
||||
<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"
|
||||
role="dialog"
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
} from '../../../../shared-kernel';
|
||||
import { selectCurrentRoom, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
|
||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import { ModalBackdropComponent } from '../../../../shared';
|
||||
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
||||
import { PluginStoreService } from '../../application/services/plugin-store.service';
|
||||
import type {
|
||||
@@ -60,7 +61,8 @@ interface ServerPluginInstallDialog {
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ChatMessageMarkdownComponent,
|
||||
NgIcon
|
||||
NgIcon,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<div
|
||||
class="fixed inset-0 z-[112] bg-black/70 backdrop-blur-sm"
|
||||
(click)="cancelled.emit(undefined)"
|
||||
(keydown.enter)="cancelled.emit(undefined)"
|
||||
(keydown.space)="cancelled.emit(undefined)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close profile image editor"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="112"
|
||||
ariaLabel="Close profile image editor"
|
||||
(dismissed)="cancelled.emit(undefined)"
|
||||
/>
|
||||
|
||||
<div class="fixed inset-0 z-[113] flex items-center justify-center p-4 pointer-events-none">
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ProfileAvatarFacade } from '../../application/services/profile-avatar.facade';
|
||||
import { ModalBackdropComponent } from '../../../../shared';
|
||||
import {
|
||||
EditableProfileAvatarSource,
|
||||
ProcessedProfileAvatar,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
@Component({
|
||||
selector: 'app-profile-avatar-editor',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, ModalBackdropComponent],
|
||||
templateUrl: './profile-avatar-editor.component.html'
|
||||
})
|
||||
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;
|
||||
}
|
||||
|
||||
const CATEGORY_PRESETS: ServerCategoryPreset[] = [
|
||||
export const CATEGORY_PRESETS: ServerCategoryPreset[] = [
|
||||
{ id: 'gaming', label: 'Gaming', topic: 'gaming' },
|
||||
{ id: 'music', label: 'Music', topic: 'music' },
|
||||
{ id: 'coding', label: 'Coding', topic: 'coding' },
|
||||
|
||||
@@ -290,10 +290,10 @@
|
||||
}
|
||||
|
||||
@if (pluginConsentDialog(); as dialog) {
|
||||
<div
|
||||
class="fixed inset-0 z-50 bg-black/50"
|
||||
role="presentation"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="50"
|
||||
[dismissable]="false"
|
||||
/>
|
||||
<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"
|
||||
role="dialog"
|
||||
@@ -469,11 +469,11 @@
|
||||
</section>
|
||||
|
||||
@if (pluginConsentReadme(); as readme) {
|
||||
<div
|
||||
class="fixed inset-0 z-[52] bg-black/60"
|
||||
role="presentation"
|
||||
(click)="closePluginConsentReadme()"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="52"
|
||||
ariaLabel="Close readme"
|
||||
(dismissed)="closePluginConsentReadme()"
|
||||
/>
|
||||
<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"
|
||||
role="dialog"
|
||||
|
||||
@@ -49,6 +49,7 @@ import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import {
|
||||
ConfirmDialogComponent,
|
||||
LeaveServerDialogComponent,
|
||||
ModalBackdropComponent,
|
||||
type LeaveServerDialogResult
|
||||
} from '../../../../shared';
|
||||
import { ChatMessageMarkdownComponent } from '../../../chat';
|
||||
@@ -89,7 +90,8 @@ export interface ServerDiscoverySection {
|
||||
NgIcon,
|
||||
ChatMessageMarkdownComponent,
|
||||
ConfirmDialogComponent,
|
||||
LeaveServerDialogComponent
|
||||
LeaveServerDialogComponent,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
viewProviders: [
|
||||
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 -->
|
||||
<button
|
||||
appThemeNode="serversRailCreateButton"
|
||||
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"
|
||||
(click)="goToDashboard()"
|
||||
>
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<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]="
|
||||
callAvatarUrls(call).length > 0
|
||||
? 'bg-emerald-950 text-white shadow-sm hover:bg-emerald-900'
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<ng-icon
|
||||
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>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@
|
||||
<button
|
||||
appThemeNode="serversRailItem"
|
||||
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'"
|
||||
[title]="room.name"
|
||||
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
|
||||
@@ -135,6 +135,29 @@
|
||||
</button>
|
||||
</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
|
||||
@@ -235,3 +258,10 @@
|
||||
(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 { NavigationEnd, Router } from '@angular/router';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucidePhone } from '@ng-icons/lucide';
|
||||
import { lucidePhone, lucidePlus } from '@ng-icons/lucide';
|
||||
import {
|
||||
EMPTY,
|
||||
Subject,
|
||||
@@ -37,6 +37,7 @@ import { NotificationsFacade } from '../../../domains/notifications';
|
||||
import { DirectCallService, DirectCallSession } from '../../../domains/direct-call';
|
||||
import { DmRailComponent } from '../../../domains/direct-message/feature/dm-rail/dm-rail.component';
|
||||
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 { hasRoomBanForUser } from '../../../domains/access-control';
|
||||
import {
|
||||
@@ -56,12 +57,13 @@ const ACTIVATION_DEBOUNCE_MS = 150;
|
||||
NgIcon,
|
||||
ConfirmDialogComponent,
|
||||
ContextMenuComponent,
|
||||
CreateServerDialogComponent,
|
||||
DmRailComponent,
|
||||
LeaveServerDialogComponent,
|
||||
ThemeNodeDirective,
|
||||
UserBarComponent
|
||||
],
|
||||
viewProviders: [provideIcons({ lucidePhone })],
|
||||
viewProviders: [provideIcons({ lucidePhone, lucidePlus })],
|
||||
templateUrl: './servers-rail.component.html'
|
||||
})
|
||||
export class ServersRailComponent {
|
||||
@@ -90,6 +92,7 @@ export class ServersRailComponent {
|
||||
contextRoom = signal<Room | null>(null);
|
||||
optimisticSelectedRoomId = signal<string | null>(null);
|
||||
showLeaveConfirm = signal(false);
|
||||
showCreateDialog = signal(false);
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||
bannedRoomLookup = signal<Record<string, boolean>>({});
|
||||
@@ -257,6 +260,14 @@ export class ServersRailComponent {
|
||||
this.router.navigate(['/dashboard']);
|
||||
}
|
||||
|
||||
openCreateDialog(): void {
|
||||
this.showCreateDialog.set(true);
|
||||
}
|
||||
|
||||
closeCreateDialog(): void {
|
||||
this.showCreateDialog.set(false);
|
||||
}
|
||||
|
||||
joinSavedRoom(room: Room): void {
|
||||
const targetRoom = this.savedRooms().find((savedRoom) => savedRoom.id === room.id) ?? room;
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@if (isOpen() && !isThemeStudioFullscreen()) {
|
||||
<!-- Backdrop (hidden on mobile where the modal is full-screen) -->
|
||||
<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-0]="!animating()"
|
||||
(click)="onBackdropClick()"
|
||||
|
||||
@@ -272,10 +272,10 @@
|
||||
}
|
||||
|
||||
@if (requiredPluginRequirements().length > 0 && currentRoom()) {
|
||||
<div
|
||||
class="fixed inset-0 z-[80] bg-black/60"
|
||||
role="presentation"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="80"
|
||||
[dismissable]="false"
|
||||
/>
|
||||
<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"
|
||||
role="dialog"
|
||||
|
||||
@@ -41,7 +41,7 @@ import { ServerDirectoryFacade } from '../../../domains/server-directory';
|
||||
import { PlatformService } from '../../../core/platform';
|
||||
import { clearStoredCurrentUserId } from '../../../core/storage/current-user-storage';
|
||||
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 { VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
@@ -59,7 +59,8 @@ import { getPluginInstallScope } from '../../../domains/plugins/domain/logic/plu
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
LeaveServerDialogComponent,
|
||||
ThemeNodeDirective
|
||||
ThemeNodeDirective,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({ lucideMinus,
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<!-- Dimmed backdrop. Tap to dismiss. -->
|
||||
<div
|
||||
class="fixed inset-0 z-[140] bg-black/40 backdrop-blur-sm"
|
||||
(click)="onBackdropClick()"
|
||||
(keydown.enter)="onBackdropClick()"
|
||||
(keydown.space)="onBackdropClick()"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="140"
|
||||
ariaLabel="Close"
|
||||
(dismissed)="onBackdropClick()"
|
||||
/>
|
||||
|
||||
<!--
|
||||
Bottom sheet panel. Slides up from the bottom of the viewport. Drag the top handle downward
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||
|
||||
/**
|
||||
* Mobile bottom-sheet container.
|
||||
@@ -32,7 +33,7 @@ import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
@Component({
|
||||
selector: 'app-bottom-sheet',
|
||||
standalone: true,
|
||||
imports: [ThemeNodeDirective],
|
||||
imports: [ThemeNodeDirective, ModalBackdropComponent],
|
||||
templateUrl: './bottom-sheet.component.html',
|
||||
styleUrl: './bottom-sheet.component.scss'
|
||||
})
|
||||
|
||||
@@ -37,15 +37,11 @@
|
||||
</app-bottom-sheet>
|
||||
} @else {
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-40 bg-black/30"
|
||||
(click)="cancelled.emit(undefined)"
|
||||
(keydown.enter)="cancelled.emit(undefined)"
|
||||
(keydown.space)="cancelled.emit(undefined)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close dialog"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="40"
|
||||
ariaLabel="Close dialog"
|
||||
(dismissed)="cancelled.emit(undefined)"
|
||||
/>
|
||||
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
|
||||
@@ -8,11 +8,16 @@ import {
|
||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
import { ViewportService } from '../../../core/platform';
|
||||
import { BottomSheetComponent } from '../bottom-sheet/bottom-sheet.component';
|
||||
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog',
|
||||
standalone: true,
|
||||
imports: [ThemeNodeDirective, BottomSheetComponent],
|
||||
imports: [
|
||||
ThemeNodeDirective,
|
||||
BottomSheetComponent,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
templateUrl: './confirm-dialog.component.html',
|
||||
host: {
|
||||
style: 'display: contents;'
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<div
|
||||
class="fixed inset-0 z-40 bg-black/30"
|
||||
(click)="cancelled.emit(undefined)"
|
||||
(keydown.enter)="cancelled.emit(undefined)"
|
||||
(keydown.space)="cancelled.emit(undefined)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close dialog"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="40"
|
||||
ariaLabel="Close dialog"
|
||||
(dismissed)="cancelled.emit(undefined)"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||
import {
|
||||
Room,
|
||||
RoomMember,
|
||||
@@ -22,7 +23,11 @@ export interface LeaveServerDialogResult {
|
||||
@Component({
|
||||
selector: 'app-leave-server-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
templateUrl: './leave-server-dialog.component.html'
|
||||
})
|
||||
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
|
||||
class="fixed inset-0 z-[110] bg-black/70 backdrop-blur-sm"
|
||||
(click)="cancelled.emit(undefined)"
|
||||
(keydown.enter)="cancelled.emit(undefined)"
|
||||
(keydown.space)="cancelled.emit(undefined)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close screen share quality dialog"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="110"
|
||||
ariaLabel="Close screen share quality dialog"
|
||||
(dismissed)="cancelled.emit(undefined)"
|
||||
/>
|
||||
|
||||
<div class="fixed inset-0 z-[111] flex items-center justify-center p-4 pointer-events-none">
|
||||
<div
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ScreenShareQuality, SCREEN_SHARE_QUALITY_OPTIONS } from '../../../domains/screen-share';
|
||||
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-screen-share-quality-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, ModalBackdropComponent],
|
||||
templateUrl: './screen-share-quality-dialog.component.html'
|
||||
})
|
||||
export class ScreenShareQualityDialogComponent implements OnInit {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
@if (request(); as pickerRequest) {
|
||||
<div
|
||||
class="fixed inset-0 z-[110] bg-black/70 backdrop-blur-sm"
|
||||
(click)="cancel()"
|
||||
(keydown.enter)="cancel()"
|
||||
(keydown.space)="cancel()"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close source picker"
|
||||
></div>
|
||||
<app-modal-backdrop
|
||||
[zIndex]="110"
|
||||
ariaLabel="Close source picker"
|
||||
(dismissed)="cancel()"
|
||||
/>
|
||||
|
||||
<div class="fixed inset-0 z-[111] flex items-center justify-center p-4 pointer-events-none">
|
||||
<section
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ScreenShareSourcePickerService
|
||||
} from '../../../domains/screen-share';
|
||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
import { ModalBackdropComponent } from '../modal-backdrop/modal-backdrop.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-screen-share-source-picker',
|
||||
@@ -22,7 +23,8 @@ import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgOptimizedImage,
|
||||
ThemeNodeDirective
|
||||
ThemeNodeDirective,
|
||||
ModalBackdropComponent
|
||||
],
|
||||
templateUrl: './screen-share-source-picker.component.html',
|
||||
styleUrl: './screen-share-source-picker.component.scss',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Shared reusable UI components barrel.
|
||||
*/
|
||||
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 { UserAvatarComponent } from './components/user-avatar/user-avatar.component';
|
||||
export { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component';
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
@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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user