Rewild Your Web
web browser dweb
at main 187 lines 4.8 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3import { 4 LitElement, 5 html, 6 css, 7} from "//shared.localhost:8888/third_party/lit/lit-all.min.js"; 8 9export class MobileOverview extends LitElement { 10 static properties = { 11 open: { type: Boolean, reflect: true }, 12 tabs: { type: Array }, 13 activeTabId: { type: String }, 14 }; 15 16 static styles = css` 17 @import url(//system.localhost:8888/mobile_overview.css); 18 `; 19 20 constructor() { 21 super(); 22 this.open = false; 23 this.tabs = []; 24 this.activeTabId = null; 25 26 // Touch state for swipe-to-close 27 this.swipeState = null; 28 } 29 30 handleOverlayClick(e) { 31 if (e.target.classList.contains("overlay")) { 32 this.close(); 33 } 34 } 35 36 close() { 37 this.open = false; 38 this.dispatchEvent(new CustomEvent("overview-close", { bubbles: true })); 39 } 40 41 handleTabClick(tab) { 42 this.dispatchEvent( 43 new CustomEvent("tab-select", { 44 bubbles: true, 45 detail: { tabId: tab.id }, 46 }), 47 ); 48 this.close(); 49 } 50 51 handleCloseTab(e, tab) { 52 e.stopPropagation(); 53 54 // Animate the card closing 55 const card = e.currentTarget.closest(".tab-card"); 56 card.classList.add("closing"); 57 58 setTimeout(() => { 59 this.dispatchEvent( 60 new CustomEvent("tab-close", { 61 bubbles: true, 62 detail: { tabId: tab.id }, 63 }), 64 ); 65 }, 300); 66 } 67 68 handleNewTab() { 69 this.dispatchEvent(new CustomEvent("tab-new", { bubbles: true })); 70 this.close(); 71 } 72 73 handleHome() { 74 this.dispatchEvent(new CustomEvent("tab-home", { bubbles: true })); 75 this.close(); 76 } 77 78 // Touch handlers for swipe-up-to-close on cards 79 handleTouchStart(e, tab) { 80 const touch = e.touches[0]; 81 this.swipeState = { 82 tab, 83 startY: touch.clientY, 84 currentY: touch.clientY, 85 element: e.currentTarget, 86 }; 87 } 88 89 handleTouchMove(e) { 90 if (!this.swipeState) { 91 return; 92 } 93 94 const touch = e.touches[0]; 95 this.swipeState.currentY = touch.clientY; 96 const deltaY = this.swipeState.currentY - this.swipeState.startY; 97 98 // Only allow upward swipe (close) 99 if (deltaY < 0) { 100 this.swipeState.element.style.transform = `translateY(${deltaY}px)`; 101 this.swipeState.element.style.opacity = Math.max(0, 1 + deltaY / 150); 102 } 103 } 104 105 handleTouchEnd(e) { 106 if (!this.swipeState) { 107 return; 108 } 109 110 const deltaY = this.swipeState.currentY - this.swipeState.startY; 111 const element = this.swipeState.element; 112 const tab = this.swipeState.tab; 113 114 if (deltaY < -80) { 115 // Close threshold reached 116 element.classList.add("closing"); 117 setTimeout(() => { 118 this.dispatchEvent( 119 new CustomEvent("tab-close", { 120 bubbles: true, 121 detail: { tabId: tab.id }, 122 }), 123 ); 124 }, 300); 125 } else { 126 // Snap back 127 element.style.transform = ""; 128 element.style.opacity = ""; 129 } 130 131 this.swipeState = null; 132 } 133 134 render() { 135 let tabText = this.tabs.length > 1 ? `${this.tabs.length} Views` : `1 View`; 136 137 return html` 138 <div class="overlay" @click=${this.handleOverlayClick}></div> 139 <div class="container"> 140 <div class="header"> 141 <span class="header-title">${tabText}</span> 142 </div> 143 144 <div class="grid"> 145 ${this.tabs.map( 146 (tab) => html` 147 <div 148 class="tab-card ${tab.id === this.activeTabId ? "active" : ""}" 149 @click=${() => this.handleTabClick(tab)} 150 @touchstart=${(e) => this.handleTouchStart(e, tab)} 151 @touchmove=${this.handleTouchMove} 152 @touchend=${this.handleTouchEnd} 153 > 154 ${tab.screenshotUrl 155 ? html`<img 156 class="tab-screenshot" 157 src="${tab.screenshotUrl}" 158 alt="" 159 />` 160 : html`<div class="tab-screenshot-placeholder"> 161 <lucide-icon name="globe"></lucide-icon> 162 </div>`} 163 <div class="tab-info"> 164 <img class="tab-favicon" src="${tab.favicon || ""}" alt="" /> 165 <span class="tab-title">${tab.title || "Untitled"}</span> 166 </div> 167 <button 168 class="close-button" 169 @click=${(e) => this.handleCloseTab(e, tab)} 170 > 171 <lucide-icon name="x"></lucide-icon> 172 </button> 173 </div> 174 `, 175 )} 176 177 <div class="home-card" @click=${this.handleHome}> 178 <lucide-icon name="house"></lucide-icon> 179 <span>Home</span> 180 </div> 181 </div> 182 </div> 183 `; 184 } 185} 186 187customElements.define("mobile-overview", MobileOverview);