Rewild Your Web
web
browser
dweb
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 handleDone() {
79 this.close();
80 }
81
82 // Touch handlers for swipe-up-to-close on cards
83 handleTouchStart(e, tab) {
84 const touch = e.touches[0];
85 this.swipeState = {
86 tab,
87 startY: touch.clientY,
88 currentY: touch.clientY,
89 element: e.currentTarget,
90 };
91 }
92
93 handleTouchMove(e) {
94 if (!this.swipeState) {
95 return;
96 }
97
98 const touch = e.touches[0];
99 this.swipeState.currentY = touch.clientY;
100 const deltaY = this.swipeState.currentY - this.swipeState.startY;
101
102 // Only allow upward swipe (close)
103 if (deltaY < 0) {
104 this.swipeState.element.style.transform = `translateY(${deltaY}px)`;
105 this.swipeState.element.style.opacity = Math.max(0, 1 + deltaY / 150);
106 }
107 }
108
109 handleTouchEnd(e) {
110 if (!this.swipeState) {
111 return;
112 }
113
114 const deltaY = this.swipeState.currentY - this.swipeState.startY;
115 const element = this.swipeState.element;
116 const tab = this.swipeState.tab;
117
118 if (deltaY < -80) {
119 // Close threshold reached
120 element.classList.add("closing");
121 setTimeout(() => {
122 this.dispatchEvent(
123 new CustomEvent("tab-close", {
124 bubbles: true,
125 detail: { tabId: tab.id },
126 })
127 );
128 }, 300);
129 } else {
130 // Snap back
131 element.style.transform = "";
132 element.style.opacity = "";
133 }
134
135 this.swipeState = null;
136 }
137
138 render() {
139 let tabText = this.tabs.length > 1 ? `${this.tabs.length} Views` : `1 View`;
140
141 return html`
142 <div class="overlay" @click=${this.handleOverlayClick}></div>
143 <div class="container">
144 <div class="header">
145 <span class="header-title">${tabText}</span>
146 <div class="header-actions">
147 <button class="header-button" @click=${this.handleHome}>
148 <lucide-icon name="house"></lucide-icon>
149 </button>
150 <button class="header-button" @click=${this.handleDone}>
151 <lucide-icon name="check"></lucide-icon>
152 </button>
153 </div>
154 </div>
155
156 <div class="grid">
157 ${this.tabs.map(
158 (tab) => html`
159 <div
160 class="tab-card ${tab.id === this.activeTabId ? "active" : ""}"
161 @click=${() => this.handleTabClick(tab)}
162 @touchstart=${(e) => this.handleTouchStart(e, tab)}
163 @touchmove=${this.handleTouchMove}
164 @touchend=${this.handleTouchEnd}
165 >
166 ${tab.screenshotUrl
167 ? html`<img
168 class="tab-screenshot"
169 src="${tab.screenshotUrl}"
170 alt=""
171 />`
172 : html`<div class="tab-screenshot-placeholder">
173 <lucide-icon name="globe"></lucide-icon>
174 </div>`}
175 <div class="tab-info">
176 <img class="tab-favicon" src="${tab.favicon || ""}" alt="" />
177 <span class="tab-title">${tab.title || "Untitled"}</span>
178 </div>
179 <button
180 class="close-button"
181 @click=${(e) => this.handleCloseTab(e, tab)}
182 >
183 <lucide-icon name="x"></lucide-icon>
184 </button>
185 </div>
186 `
187 )}
188
189 <div class="home-card" @click=${this.handleHome}>
190 <lucide-icon name="house"></lucide-icon>
191 <span>Home</span>
192 </div>
193 </div>
194 </div>
195 `;
196 }
197}
198
199customElements.define("mobile-overview", MobileOverview);