feat: Response mobile layout support v1
Some checks failed
Queue Release Build / prepare (push) Successful in 30s
Deploy Web Apps / deploy (push) Successful in 7m8s
Queue Release Build / build-windows (push) Successful in 28m11s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-linux (push) Has started running

This commit is contained in:
2026-05-18 02:25:16 +02:00
parent ecb1a4b3a0
commit 181fedc7ec
42 changed files with 2333 additions and 343 deletions

View File

@@ -0,0 +1,102 @@
import {
Component,
HostListener,
computed,
input,
output,
signal
} from '@angular/core';
import { ThemeNodeDirective } from '../../../domains/theme';
/**
* Mobile bottom-sheet container.
*
* Renders a backdrop + a panel anchored to the bottom of the viewport that slides up from below.
* Intended for use on phone-sized viewports where context menus, action sheets, and confirmation
* dialogs are better presented as bottom sheets than as floating popovers or centered modals.
*
* The component is layout-only: callers project their content via `<ng-content>` and listen for
* the `dismissed` output to close themselves. Drag-to-dismiss is supported via touch gestures.
*
* Desktop callers should not render this component; use the original popover/modal layout instead.
*
* @example
* ```html
* @if (isMobile()) {
* <app-bottom-sheet (dismissed)="close()">
* <my-menu-items />
* </app-bottom-sheet>
* }
* ```
*/
@Component({
selector: 'app-bottom-sheet',
standalone: true,
imports: [ThemeNodeDirective],
templateUrl: './bottom-sheet.component.html',
styleUrl: './bottom-sheet.component.scss'
})
export class BottomSheetComponent {
/** Optional title rendered at the top of the sheet. Omit for an unlabeled action sheet. */
readonly title = input<string | null>(null);
/** Optional ARIA label when no visible title is provided. */
readonly ariaLabel = input<string>('Menu');
/** Emits when the user dismisses the sheet (backdrop tap, swipe-down, or Escape). */
readonly dismissed = output<undefined>();
/** Pixels the sheet is currently dragged downward. Drives the translate transform. */
protected readonly dragOffset = signal(0);
/** Visible transform offset in CSS pixels (only positive values move the sheet down). */
protected readonly translateY = computed(() => Math.max(0, this.dragOffset()));
private touchStartY: number | null = null;
@HostListener('document:keydown.escape')
protected onEscape(): void {
this.dismissed.emit(undefined);
}
protected onBackdropClick(): void {
this.dismissed.emit(undefined);
}
protected onHandleTouchStart(event: TouchEvent): void {
const touch = event.touches[0];
if (!touch) {
return;
}
this.touchStartY = touch.clientY;
}
protected onHandleTouchMove(event: TouchEvent): void {
if (this.touchStartY === null) {
return;
}
const touch = event.touches[0];
if (!touch) {
return;
}
const delta = touch.clientY - this.touchStartY;
// Only allow dragging downward; ignore upward drags.
this.dragOffset.set(Math.max(0, delta));
}
protected onHandleTouchEnd(): void {
// Dismiss if the user dragged the sheet down by more than 80px; otherwise snap back.
if (this.dragOffset() > 80) {
this.dismissed.emit(undefined);
}
this.touchStartY = null;
this.dragOffset.set(0);
}
}