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 // 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);