import { DeesElement, css, cssManager, customElement, html, property, state, type TemplateResult, } from '@design.estate/dees-element'; import { mobileComponentStyles } from '../../00componentstyles.js'; import { demoFunc } from './dees-mobile-applayout.demo.js'; export type TNavigationDirection = 'forward' | 'back' | 'none'; declare global { interface HTMLElementTagNameMap { 'dees-mobile-applayout': DeesMobileApplayout; } } @customElement('dees-mobile-applayout') export class DeesMobileApplayout extends DeesElement { public static demo = demoFunc; @property({ type: Boolean, reflect: true, attribute: 'keyboard-visible' }) accessor keyboardVisible: boolean = false; @property({ type: Boolean }) accessor showNavigation: boolean = true; @property({ type: Boolean }) accessor isIOS: boolean = false; @property({ type: Boolean }) accessor isPWA: boolean = false; @state() accessor navigationDirection: TNavigationDirection = 'none'; @state() accessor isTransitioning: boolean = false; private keyboardBlurTimeout?: number; public static styles = [ cssManager.defaultStyles, mobileComponentStyles, css` :host { display: flex; flex-direction: column; min-height: 100vh; background: var(--dees-background); } .app-layout { display: grid; grid-template-rows: auto 1fr auto; grid-template-areas: "spacer" "content" "navigation"; height: 100vh; overflow: hidden; } .ios-keyboard-spacer { grid-area: spacer; height: 0; transition: height 300ms ease-out; background: var(--dees-background); } .ios-keyboard-spacer.active { height: 340px; } /* Mobile-first: smooth scrolling behavior for keyboard visibility */ :host([keyboard-visible]) .main-content { overflow-y: auto; -webkit-overflow-scrolling: touch; } /* Main content area */ .main-content { grid-area: content; overflow: hidden; position: relative; } /* View transition container */ .view-container { position: relative; width: 100%; height: 100%; overflow: hidden; } .view-wrapper { position: absolute; inset: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; will-change: transform, opacity; } /* Forward navigation: entering view slides from right */ .view-wrapper.entering.forward { animation: slideInFromRight 300ms ease-out forwards; } /* Forward navigation: exiting view slides to left with fade */ .view-wrapper.exiting.forward { animation: slideOutToLeft 300ms ease-out forwards; } /* Back navigation: entering view slides from left */ .view-wrapper.entering.back { animation: slideInFromLeft 300ms ease-out forwards; } /* Back navigation: exiting view slides to right */ .view-wrapper.exiting.back { animation: slideOutToRight 300ms ease-out forwards; } /* No animation */ .view-wrapper.entering.none { opacity: 1; transform: none; } @keyframes slideInFromRight { from { transform: translateX(100%); } to { transform: translateX(0); } } @keyframes slideOutToLeft { from { transform: translateX(0); opacity: 1; } to { transform: translateX(-30%); opacity: 0; } } @keyframes slideInFromLeft { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes slideOutToRight { from { transform: translateX(0); } to { transform: translateX(100%); } } /* Bottom navigation */ .navigation-slot { grid-area: navigation; z-index: var(--dees-z-sticky, 200); } /* Mobile-first: hide bottom navigation when keyboard is visible */ :host([keyboard-visible]) .navigation-slot { display: none; } /* Desktop: show navigation even with keyboard */ @media (min-width: 641px) { :host([keyboard-visible]) .navigation-slot { display: block; } } /* Mobile-first: allow overflow during drag */ :host-context(body.dragging) .app-layout { overflow: visible !important; } :host-context(body.dragging) .main-content { overflow: visible !important; } /* Desktop: maintain normal overflow behavior during drag */ @media (min-width: 641px) { :host-context(body.dragging) .app-layout { overflow: hidden; } :host-context(body.dragging) .main-content { overflow-y: auto; } } .loading { display: flex; align-items: center; justify-content: center; min-height: 300px; color: var(--dees-muted-foreground); } .spinner { width: 3rem; height: 3rem; border: 3px solid var(--dees-border); border-top-color: var(--dees-primary); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } `, ]; async connectedCallback() { await super.connectedCallback(); // Listen for keyboard events this.addEventListener('input-focus', this.handleInputFocus as EventListener); this.addEventListener('input-blur', this.handleInputBlur as EventListener); // Detect iOS PWA this.detectEnvironment(); // Listen for viewport changes to detect keyboard (iOS PWA only) if (this.isIOS && this.isPWA && 'visualViewport' in window) { window.visualViewport?.addEventListener('resize', this.handleViewportResize); this.handleViewportResize(); } } async disconnectedCallback() { await super.disconnectedCallback(); this.removeEventListener('input-focus', this.handleInputFocus as EventListener); this.removeEventListener('input-blur', this.handleInputBlur as EventListener); if (this.isIOS && this.isPWA && 'visualViewport' in window) { window.visualViewport?.removeEventListener('resize', this.handleViewportResize); } } private detectEnvironment() { // Detect iOS const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // Detect PWA mode const isPWA = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone === true; this.isIOS = isIOS; this.isPWA = isPWA; } private handleInputFocus = () => { if (this.keyboardBlurTimeout) { clearTimeout(this.keyboardBlurTimeout); this.keyboardBlurTimeout = undefined; } if (window.innerWidth <= 640) { this.keyboardVisible = true; } }; private handleInputBlur = () => { if (this.keyboardBlurTimeout) { clearTimeout(this.keyboardBlurTimeout); } if (window.innerWidth <= 640) { this.keyboardBlurTimeout = window.setTimeout(() => { this.keyboardVisible = false; this.keyboardBlurTimeout = undefined; }, 150); } }; private handleViewportResize = () => { if (window.visualViewport) { const viewport = window.visualViewport; const keyboardHeight = window.innerHeight - viewport.height; if (keyboardHeight > 50) { this.keyboardVisible = true; } else { this.keyboardVisible = false; } } }; /** * Navigate with animation transition * Call this method when changing views */ public navigateWithTransition(direction: TNavigationDirection) { if (this.isTransitioning) return; this.navigationDirection = direction; this.isTransitioning = true; // Clear transition state after animation setTimeout(() => { this.isTransitioning = false; this.navigationDirection = 'none'; }, 300); } public render(): TemplateResult { const showKeyboardSpacer = this.keyboardVisible && this.isIOS && this.isPWA; return html`
${this.showNavigation ? html`
` : ''}
`; } }