diff --git a/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.html b/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.html index 298f8a9..898c4d2 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.html +++ b/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.html @@ -13,32 +13,34 @@ } @else { - - + + -
- @if (showSearchBox) { - - Search recordings - - - - } -
+ @if (!isMobileView()) { +
+ @if (showSearchBox) { + + Search recordings + + + + } +
+ } @if (showSelection && selectedRecordings().size > 0) { @@ -118,6 +120,72 @@
+ + + @if (loading) {
@@ -139,119 +207,101 @@
} @else { - -
- - - @if (showSelection) { - - - - - } + @if (isMobileView()) { + +
+ @for (recording of recordings; track recording.recordingId) { +
+ +
+
+ @if (showSelection && canSelectRecording(recording)) { + + + } - - @if (showRoomInfo) { - -
- - - } - - - - - - - - - - + - - - - - - - - - - - - - - - - - + + } + + } @else { + +
+
- - - - @if (canSelectRecording(recording)) { - - - } - Room -
- {{ recording.roomName }} - {{ recording.roomId }} - + @if (showRoomInfo) { +
+
{{ recording.roomName }}
+
{{ recording.recordingId }}
+
+ }
-
Status -
- - {{ getStatusIcon(recording.status) }} - - {{ getStatusLabel(recording.status) }} +
+
+ + {{ getStatusIcon(recording.status) }} + + {{ getStatusLabel(recording.status) }} +
+
-
Start Date - @if (recording.startDate) { -
- {{ recording.startDate | date: 'mediumDate' }} - {{ recording.startDate | date: 'shortTime' }} + +
+
+
+ schedule +
+
Start Date
+ @if (recording.startDate) { +
+ {{ recording.startDate | date: 'mediumDate' }} +
+
{{ recording.startDate | date: 'shortTime' }}
+ } @else { +
-
+ } +
+
+ +
+ timer +
+
Duration
+
{{ formatDuration(recording.duration) }}
+
+
+ +
+ storage +
+
Size
+
{{ formatFileSize(recording.size) }}
+
+
- } @else { - - - } -
Duration - {{ formatDuration(recording.duration) }} - Size - {{ formatFileSize(recording.size) }} - Actions -
- + +
@if (canPlayRecording(recording)) { } - @if (canDownloadRecording(recording)) { } @else { - - + - @if (canDeleteRecording(recording)) {
-
+ + @if (showSelection) { + + + + + } - - -
+ + + + @if (canSelectRecording(recording)) { + + + } +
-
+ + @if (showRoomInfo) { + + Room + +
+ {{ recording.roomName }} + {{ recording.recordingId }} +
+ +
+ } + + + + Status + +
+ + {{ getStatusIcon(recording.status) }} + + {{ getStatusLabel(recording.status) }} +
+ +
+ + + + Start Date + + @if (recording.startDate) { +
+ {{ recording.startDate | date: 'mediumDate' }} + {{ recording.startDate | date: 'shortTime' }} +
+ } @else { + - + } + +
+ + + + Duration + + {{ formatDuration(recording.duration) }} + + + + + + Size + + {{ formatFileSize(recording.size) }} + + + + + + Actions + +
+ + @if (canPlayRecording(recording)) { + + } + + + @if (canDownloadRecording(recording)) { + + } + + @if (isRecordingFailed(recording)) { + + } @else { + + + + + + + @if (canDeleteRecording(recording)) { + + + } + + } +
+ +
+ + + + + + } @if (showLoadMore) { diff --git a/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.scss b/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.scss index 70d5621..2a89714 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.scss @@ -1,45 +1,333 @@ @import '../../../../../../src/assets/styles/design-tokens'; -// Use utility classes for recordings toolbar -.recordings-toolbar { - @extend .ov-data-toolbar; +// === MOBILE CARDS VIEW === +.cards-container { + @include ov-grid-responsive(280px); + gap: var(--ov-meet-spacing-md); + padding: var(--ov-meet-spacing-md) 0; - .toolbar-left { - ::ng-deep .search-btn { - padding: var(--ov-meet-spacing-sm); - } + @include ov-mobile-down { + grid-template-columns: 1fr; + gap: var(--ov-meet-spacing-sm); + padding: var(--ov-meet-spacing-sm) 0; + } +} + +.recording-card { + @include ov-card; + @include ov-theme-transition; + display: flex; + flex-direction: column; + gap: var(--ov-meet-spacing-md); + position: relative; + + &.selected-card { + background-color: color-mix(in srgb, var(--ov-meet-color-primary) 8%, var(--ov-meet-card-background)); + border: 1px solid var(--ov-meet-color-primary); + box-shadow: var(--ov-meet-card-shadow-hover); } - .toolbar-right { - gap: var(--ov-meet-spacing-sm); + &:hover:not(.selected-card) { + @include ov-hover-lift(-2px); + } - ::ng-deep .refresh-btn { - padding: var(--ov-meet-spacing-sm); + @include ov-mobile-down { + gap: var(--ov-meet-spacing-sm); + } +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: var(--ov-meet-spacing-md); + + .card-header-left { + display: flex; + align-items: flex-start; + gap: var(--ov-meet-spacing-sm); + flex: 1; + min-width: 0; + } + + .card-checkbox { + margin: 0; + align-self: flex-start; + margin-top: 2px; + } + + .status-info { + flex-shrink: 0; + } +} + +.recording-details { + display: flex; + flex-direction: column; + gap: var(--ov-meet-spacing-md); + + @include ov-mobile-down { + gap: var(--ov-meet-spacing-sm); + } +} + +.detail-item { + display: flex; + align-items: flex-start; + gap: var(--ov-meet-spacing-sm); + + .detail-icon { + @include ov-icon(sm); + color: var(--ov-meet-text-secondary); + margin-top: 2px; + flex-shrink: 0; + } + + .detail-content { + flex: 1; + min-width: 0; + + .detail-label { + font-size: var(--ov-meet-font-size-xs); + font-weight: var(--ov-meet-font-weight-medium); + color: var(--ov-meet-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 2px; } - ::ng-deep .clear-btn { - padding: var(--ov-meet-spacing-sm); + .detail-value { + font-size: var(--ov-meet-font-size-sm); + font-weight: var(--ov-meet-font-weight-medium); + color: var(--ov-meet-text-primary); + line-height: var(--ov-meet-line-height-tight); + + &.no-data { + @extend .ov-no-data; + } + } + + .detail-time { + font-size: var(--ov-meet-font-size-xs); + color: var(--ov-meet-text-secondary); + margin-top: 2px; + } + } +} + +.action-button { + &.play-button mat-icon { + color: var(--ov-meet-color-success); + } + + &.download-button mat-icon { + color: var(--ov-meet-color-primary); + } + + &.delete-button mat-icon { + color: var(--ov-meet-color-error); + } + + &.more-button mat-icon { + color: var(--ov-meet-text-secondary); + } +} + +.card-actions { + @extend .ov-action-buttons; + justify-content: flex-end; + padding-top: var(--ov-meet-spacing-sm); + border-top: 1px solid var(--ov-meet-border-color-light); + margin-top: var(--ov-meet-spacing-sm); + + .action-button { + @include ov-icon(lg); + + padding: 0; + width: 40px; + height: 40px; + + mat-icon { + font-size: 24px !important; + width: 24px !important; + height: 24px !important; + } + } + + @include ov-mobile-down { + gap: var(--ov-meet-spacing-xs); + } + + @media (max-width: 480px) { + .card-actions { + height: auto; + } + } +} + +// === TOOLBAR AND SHARED UTILITIES === +.recordings-toolbar { + @extend .ov-data-toolbar; + + // Responsive toolbar adjustments + @include ov-tablet-down { + .toolbar-center { + z-index: 1000; + position: fixed; + + .batch-actions { + justify-content: center; + + button { + padding: 0; + } + } + } + } + + @include ov-mobile-down { + flex-wrap: wrap; + gap: var(--ov-meet-spacing-sm); + + .toolbar-left { + order: 2; + width: 100%; + } + + .toolbar-right { + order: 1; + width: 100%; + justify-content: flex-end; + + .refresh-btn { + padding: 0; + } } } } -// Use utility classes for search field, selection info, and batch actions .search-field { @extend .ov-search-field; } -.selection-info { - @extend .ov-selection-info; -} - .batch-actions { @extend .ov-batch-actions; - ::ng-deep button { - padding: var(--ov-meet-spacing-sm); +} + +// === DESKTOP TABLE VIEW === +.table-container { + @extend .ov-table-container; + margin-top: 0; + + @include ov-tablet-down { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } } -// Filters Menu Styling +.recordings-table { + @extend .ov-data-table; + min-width: 600px; + + .mat-mdc-header-cell { + &.room-header { + min-width: 200px; + } + + &.select-header { + width: 48px; + min-width: 48px; + max-width: 48px; + } + + &.actions-header { + min-width: 140px; + } + } + + .mat-mdc-cell { + &.select-cell { + width: 48px; + min-width: 48px; + max-width: 48px; + } + } + + @include ov-tablet-down { + .mat-column-duration, + .mat-column-size { + display: none; + } + } + + @media (max-width: 900px) { + .mat-column-startDate .time { + display: none; + } + } +} + +// === SHARED COMPONENTS === +.room-info { + @extend .ov-info-display; + flex: 1; + min-width: 0; + + .room-name { + @include ov-text-truncate; + font-weight: var(--ov-meet-font-weight-medium); + margin-bottom: 2px; + + .cards-container & { + font-size: var(--ov-meet-font-size-md); + line-height: var(--ov-meet-line-height-tight); + } + } + + .room-id { + @include ov-text-truncate; + font-size: var(--ov-meet-font-size-xs); + color: var(--ov-meet-text-secondary); + + .cards-container & { + font-size: var(--ov-meet-font-size-xxs); + opacity: 0.8; + } + } +} + +.status-badge { + @extend .ov-status-badge; + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-xs); + font-weight: var(--ov-meet-font-weight-medium); + text-transform: uppercase; + letter-spacing: 0.5px; + + .status-icon { + @include ov-icon(sm); + margin: 0; + } + + .status-label { + font-size: var(--ov-meet-font-size-xxs); + .cards-container & { + font-size: var(--ov-meet-font-size-xs); + } + } + + @include ov-mobile-down { + padding: var(--ov-meet-spacing-xxs) var(--ov-meet-spacing-xs); + font-size: var(--ov-meet-font-size-xs); + + .status-icon { + @include ov-icon(xs); + } + } +} + +// === UTILITY EXTENSIONS === .filters-menu { @extend .ov-filters-menu; } @@ -48,130 +336,63 @@ @extend .ov-loading-container; } -// Enhanced Table Container -.table-container { - @extend .ov-table-container; - margin-top: 0; // Remove top margin since toolbar is attached -} - -// Toolbar + Table Integration -.recordings-toolbar + .table-container { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-top: none; - box-shadow: var(--ov-meet-shadow-sm); -} - -// Ensure proper spacing when no toolbar -:host:not(:has(.recordings-toolbar)) .table-container { - margin-top: var(--ov-meet-spacing-md); -} - -.recordings-table { - @extend .ov-data-table; - - // Header customizations - .mat-mdc-header-cell { - &.room-header { - @extend .primary-header; - } - - &.actions-header { - @extend .actions-header; - } - } - - // Cell customizations - .mat-mdc-cell { - &.room-cell { - @extend .primary-cell; - } - - &.actions-cell { - @extend .actions-cell; - } - } -} - -// Room information -.room-info { - @extend .ov-info-display; - - .room-name { - @extend .primary-text; - } - - .room-id { - @extend .secondary-text, .monospace-text; - } - - // .filename { - // @extend .secondary-text, .monospace-text; - // } -} - -// Status badge -.status-badge { - @extend .ov-status-badge; - padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm); - border-radius: var(--ov-meet-radius-sm); - font-size: var(--ov-meet-font-size-xs); - width: fit-content; - - .status-icon { - @include ov-icon(sm); - } -} - -// Date information .date-info { @extend .ov-date-info; } -.no-data { - @extend .ov-no-data; -} - -// Action buttons .action-buttons { @extend .ov-action-buttons; - ::ng-deep button { - padding: var(--ov-meet-spacing-sm); - } - - .mat-mdc-icon-button { - &:hover { - background-color: transparent; + @include ov-tablet-down { + .play-btn, + .download-btn { + display: none; } } } -// Menu item styles .delete-action { @extend .ov-delete-action; } -// Empty State Styling .no-recordings-state { @extend .ov-empty-state; .getting-started-actions { - @extend .action-buttons; + @include ov-mobile-down { + flex-direction: column; + gap: var(--ov-meet-spacing-sm); - .refresh-recordings-btn { - @extend .refresh-btn; - } - - .clear-filters-btn { - @extend .refresh-btn; + button { + width: 100%; + padding: var(--ov-meet-spacing-md); + } } } } -// Apply focus states for accessibility +// === ACCESSIBILITY AND ANIMATIONS === .mat-mdc-checkbox, .mat-mdc-icon-button, .mat-mdc-button { @extend .ov-focus-visible; } + +@include ov-mobile-down { + .mat-mdc-icon-button { + min-width: 44px; + min-height: 44px; + } +} + +.recording-card { + animation: fadeIn 0.3s ease-out; + + &:nth-child(even) { + animation-delay: 0.1s; + } + + &:nth-child(odd) { + animation-delay: 0.05s; + } +} diff --git a/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.ts b/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.ts index 483c515..10f7888 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/components/recording-lists/recording-lists.component.ts @@ -1,15 +1,5 @@ import { CommonModule, DatePipe } from '@angular/common'; -import { - Component, - EventEmitter, - HostBinding, - Input, - OnChanges, - OnInit, - Output, - signal, - SimpleChanges -} from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, signal, SimpleChanges } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; @@ -26,7 +16,7 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MeetRecordingInfo, MeetRecordingStatus } from '@lib/typings/ce'; import { formatBytes, formatDurationToHMS } from '@lib/utils'; -import { debounceTime, distinctUntilChanged } from 'rxjs'; +import { ViewportService } from '@lib/services'; export interface RecordingTableAction { recordings: MeetRecordingInfo[]; @@ -91,13 +81,6 @@ export class RecordingListsComponent implements OnInit, OnChanges { @Input() showLoadMore = false; @Input() loading = false; @Input() initialFilters: { nameFilter: string; statusFilter: string } = { nameFilter: '', statusFilter: '' }; - - // Host binding for styling when recordings are selected - @HostBinding('class.has-selections') - get hasSelections(): boolean { - return this.selectedRecordings().size > 0; - } - // Output events @Output() recordingAction = new EventEmitter(); @Output() filterChange = new EventEmitter<{ nameFilter: string; statusFilter: string }>(); @@ -150,7 +133,11 @@ export class RecordingListsComponent implements OnInit, OnChanges { DOWNLOADABLE: [MeetRecordingStatus.COMPLETE] as readonly MeetRecordingStatus[] } as const; - constructor() {} + constructor(private viewportService: ViewportService) {} + + get isMobileView() { + return this.viewportService.isMobileView; + } ngOnInit() { this.setupFilters(); diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.scss b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.scss index 354c9da..3bc46b8 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.scss @@ -187,20 +187,4 @@ } } } -} - -// Animation classes -.fade-in { - animation: fadeIn 0.3s ease-out; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} +} \ No newline at end of file diff --git a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss index 0d08aae..518502f 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss @@ -134,6 +134,7 @@ .recordings-content { margin-top: var(--ov-meet-spacing-md); + padding: var(--ov-meet-spacing-sm); } .loading-card { diff --git a/frontend/projects/shared-meet-components/src/lib/services/index.ts b/frontend/projects/shared-meet-components/src/lib/services/index.ts index d447206..04e0133 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/index.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/index.ts @@ -12,4 +12,5 @@ export * from './navigation.service'; export * from './notification.service'; export * from './session-storage.service'; export * from './theme.service'; +export * from './viewport.service'; export * from './wizard-state.service'; diff --git a/frontend/projects/shared-meet-components/src/lib/services/viewport.service.ts b/frontend/projects/shared-meet-components/src/lib/services/viewport.service.ts new file mode 100644 index 0000000..660ca22 --- /dev/null +++ b/frontend/projects/shared-meet-components/src/lib/services/viewport.service.ts @@ -0,0 +1,259 @@ +import { Injectable, signal, computed, OnDestroy } from '@angular/core'; +import { fromEvent, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +/** + * Viewport size categories based on design system breakpoints + */ +export type ViewportSize = 'mobile' | 'tablet' | 'desktop' | 'wide'; + +/** + * Device orientation type + */ +export type DeviceOrientation = 'portrait' | 'landscape'; + +/** + * Viewport information interface + */ +export interface ViewportInfo { + width: number; + height: number; + size: ViewportSize; + orientation: DeviceOrientation; + isMobile: boolean; + isTablet: boolean; + isDesktop: boolean; + isWide: boolean; + isTouchDevice: boolean; +} + +/** + * Service for responsive viewport detection and device type identification. + * Provides reactive signals and utilities for building responsive interfaces. + */ +@Injectable({ + providedIn: 'root' +}) +export class ViewportService implements OnDestroy { + // Design system breakpoints + private readonly BREAKPOINTS = { + mobile: 480, + tablet: 768, + desktop: 1024, + wide: 1200 + } as const; + + // Reactive signals + private readonly _width = signal(this.getCurrentWidth()); + private readonly _height = signal(this.getCurrentHeight()); + private readonly _isTouchDevice = signal(this.detectTouchDevice()); + + // Cleanup subject + private readonly destroy$ = new Subject(); + + constructor() { + this.initializeResizeListener(); + } + + // ==== PUBLIC REACTIVE SIGNALS ==== + + /** + * Current viewport width (reactive) + */ + readonly width = this._width.asReadonly(); + + /** + * Current viewport height (reactive) + */ + readonly height = this._height.asReadonly(); + + /** + * Whether device supports touch interactions (reactive) + */ + readonly isTouchDevice = this._isTouchDevice.asReadonly(); + + /** + * Current viewport size category (computed) + */ + readonly viewportSize = computed(() => { + const width = this._width(); + if (width >= this.BREAKPOINTS.wide) return 'wide'; + if (width >= this.BREAKPOINTS.desktop) return 'desktop'; + if (width >= this.BREAKPOINTS.tablet) return 'tablet'; + return 'mobile'; + }); + + /** + * Device orientation (computed) + */ + readonly orientation = computed(() => { + return this._width() > this._height() ? 'landscape' : 'portrait'; + }); + + /** + * Whether current viewport is mobile size (computed) + */ + readonly isMobile = computed(() => this.viewportSize() === 'mobile'); + + /** + * Whether current viewport is tablet size (computed) + */ + readonly isTablet = computed(() => this.viewportSize() === 'tablet'); + + /** + * Whether current viewport is desktop size (computed) + */ + readonly isDesktop = computed(() => this.viewportSize() === 'desktop'); + + /** + * Whether current viewport is wide desktop size (computed) + */ + readonly isWide = computed(() => this.viewportSize() === 'wide'); + + /** + * Whether current viewport is mobile or smaller (computed) + */ + readonly isMobileView = computed(() => this._width() < this.BREAKPOINTS.tablet); + + /** + * Whether current viewport is tablet or smaller (computed) + */ + readonly isTabletDown = computed(() => this._width() < this.BREAKPOINTS.desktop); + + /** + * Whether current viewport is tablet or larger (computed) + */ + readonly isTabletUp = computed(() => this._width() >= this.BREAKPOINTS.tablet); + + /** + * Whether current viewport is desktop or larger (computed) + */ + readonly isDesktopUp = computed(() => this._width() >= this.BREAKPOINTS.desktop); + + /** + * Complete viewport information (computed) + */ + readonly viewportInfo = computed(() => ({ + width: this._width(), + height: this._height(), + size: this.viewportSize(), + orientation: this.orientation(), + isMobile: this.isMobile(), + isTablet: this.isTablet(), + isDesktop: this.isDesktop(), + isWide: this.isWide(), + isTouchDevice: this._isTouchDevice() + })); + + // ==== PUBLIC UTILITY METHODS ==== + + /** + * Check if viewport matches specific size + */ + matchesSize(size: ViewportSize): boolean { + return this.viewportSize() === size; + } + + /** + * Check if viewport is smaller than specified size + */ + isSmallerThan(size: ViewportSize): boolean { + const currentWidth = this._width(); + return currentWidth < this.BREAKPOINTS[size]; + } + + /** + * Check if viewport is larger than specified size + */ + isLargerThan(size: ViewportSize): boolean { + const currentWidth = this._width(); + return currentWidth >= this.BREAKPOINTS[size]; + } + + /** + * Get responsive grid columns based on viewport and content count + */ + getGridColumns(itemCount = 0): string { + if (this.isMobileView()) { + return 'single-column'; + } + if (this.isTablet()) { + return itemCount > 6 ? 'two-columns' : 'single-column'; + } + return itemCount > 10 ? 'three-columns' : 'two-columns'; + } + + /** + * Get appropriate icon size for current viewport + */ + getIconSize(): 'small' | 'medium' | 'large' { + if (this.isMobileView()) return 'medium'; + if (this.isTablet()) return 'small'; + return 'small'; + } + + /** + * Get appropriate spacing size for current viewport + */ + getSpacing(): 'compact' | 'comfortable' | 'spacious' { + if (this.isMobileView()) return 'compact'; + if (this.isTablet()) return 'comfortable'; + return 'spacious'; + } + + /** + * Check if device is in landscape mode (mobile context) + */ + isLandscape(): boolean { + return this.orientation() === 'landscape'; + } + + /** + * Check if device is in portrait mode + */ + isPortrait(): boolean { + return this.orientation() === 'portrait'; + } + + /** + * Get breakpoint value for specified size + */ + getBreakpoint(size: keyof typeof this.BREAKPOINTS): number { + return this.BREAKPOINTS[size]; + } + + // ==== PRIVATE METHODS ==== + + private getCurrentWidth(): number { + return typeof window !== 'undefined' ? window.innerWidth : 1024; + } + + private getCurrentHeight(): number { + return typeof window !== 'undefined' ? window.innerHeight : 768; + } + + private detectTouchDevice(): boolean { + if (typeof window === 'undefined') return false; + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; + } + + private initializeResizeListener(): void { + if (typeof window === 'undefined') return; + + fromEvent(window, 'resize') + .pipe( + debounceTime(150), // Debounce for performance + distinctUntilChanged(), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this._width.set(this.getCurrentWidth()); + this._height.set(this.getCurrentHeight()); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} \ No newline at end of file diff --git a/frontend/src/assets/styles/_utilities.scss b/frontend/src/assets/styles/_utilities.scss index 85dfe8b..38c3018 100644 --- a/frontend/src/assets/styles/_utilities.scss +++ b/frontend/src/assets/styles/_utilities.scss @@ -1006,6 +1006,7 @@ overflow: auto; button { @include ov-button-base; + padding: var(--ov-meet-spacing-xs); } @include ov-tablet-down { padding: var(--ov-meet-spacing-md);