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 MobileNotificationSheet extends LitElement {
10 static properties = {
11 open: { type: Boolean, reflect: true },
12 notifications: { type: Array },
13 tabCount: { type: Number },
14 };
15
16 static styles = css`
17 @import url(//system.localhost:8888/mobile_notification_sheet.css);
18 `;
19
20 constructor() {
21 super();
22 this.open = false;
23 this.notifications = [];
24 this.tabCount = 1;
25
26 // Swipe state for individual notifications
27 this.swipeState = null;
28 }
29
30 formatTime(timestamp) {
31 if (!timestamp) {
32 return "";
33 }
34
35 const now = Date.now();
36 const diff = now - timestamp;
37 const minutes = Math.floor(diff / 60000);
38 const hours = Math.floor(diff / 3600000);
39
40 if (minutes < 1) {
41 return "Just now";
42 }
43 if (minutes < 60) {
44 return `${minutes}m ago`;
45 }
46 if (hours < 24) {
47 return `${hours}h ago`;
48 }
49 return new Date(timestamp).toLocaleDateString();
50 }
51
52 handleOverlayClick(e) {
53 if (e.target === e.currentTarget) {
54 this.close();
55 }
56 }
57
58 close() {
59 this.open = false;
60 this.dispatchEvent(new CustomEvent("sheet-closed", { bubbles: true }));
61 }
62
63 handleNotificationClick(notification) {
64 this.dispatchEvent(
65 new CustomEvent("notification-click", {
66 bubbles: true,
67 detail: { notification },
68 })
69 );
70 }
71
72 handleDismiss(e, notification) {
73 e.stopPropagation();
74 this.dispatchEvent(
75 new CustomEvent("notification-dismiss", {
76 bubbles: true,
77 detail: { notification },
78 })
79 );
80 }
81
82 handleClearAll() {
83 this.dispatchEvent(
84 new CustomEvent("notification-clear-all", { bubbles: true })
85 );
86 }
87
88 // Touch handlers for swipe-to-dismiss
89 handleTouchStart(e, notification) {
90 const touch = e.touches[0];
91 this.swipeState = {
92 notification,
93 startX: touch.clientX,
94 currentX: touch.clientX,
95 element: e.currentTarget,
96 };
97 e.currentTarget.classList.add("swiping");
98 }
99
100 handleTouchMove(e) {
101 if (!this.swipeState) {
102 return;
103 }
104
105 const touch = e.touches[0];
106 this.swipeState.currentX = touch.clientX;
107 const deltaX = this.swipeState.currentX - this.swipeState.startX;
108
109 // Only allow left swipe (dismiss)
110 if (deltaX < 0) {
111 this.swipeState.element.style.transform = `translateX(${deltaX}px)`;
112 this.swipeState.element.style.opacity = Math.max(0, 1 + deltaX / 200);
113 }
114 }
115
116 handleTouchEnd(e) {
117 if (!this.swipeState) {
118 return;
119 }
120
121 const deltaX = this.swipeState.currentX - this.swipeState.startX;
122 const element = this.swipeState.element;
123 const notification = this.swipeState.notification;
124
125 element.classList.remove("swiping");
126
127 if (deltaX < -100) {
128 // Dismiss threshold reached
129 element.style.transform = "translateX(-100%)";
130 element.style.opacity = "0";
131 setTimeout(() => {
132 this.handleDismiss(new Event("click"), notification);
133 }, 200);
134 } else {
135 // Snap back
136 element.style.transform = "";
137 element.style.opacity = "";
138 }
139
140 this.swipeState = null;
141 }
142
143 render() {
144 return html`
145 <div class="overlay" @click=${this.handleOverlayClick}></div>
146 <div class="sheet">
147 <div class="sheet-header">
148 <div class="status-info">
149 <lucide-icon name="layout-grid"></lucide-icon>
150 <span class="tab-count">${this.tabCount} tabs</span>
151 </div>
152 <button
153 class="clear-all-button"
154 @click=${this.handleClearAll}
155 ?disabled=${this.notifications.length === 0}
156 >
157 Clear All
158 </button>
159 </div>
160
161 <div class="notifications-list">
162 ${this.notifications.length === 0
163 ? html`
164 <div class="empty-state">
165 <lucide-icon name="bell-off"></lucide-icon>
166 <span class="empty-state-text">No notifications</span>
167 </div>
168 `
169 : this.notifications.map(
170 (notification) => html`
171 <div
172 class="notification-item"
173 @click=${() => this.handleNotificationClick(notification)}
174 @touchstart=${(e) => this.handleTouchStart(e, notification)}
175 @touchmove=${this.handleTouchMove}
176 @touchend=${this.handleTouchEnd}
177 >
178 <div class="notification-icon">
179 ${notification.iconUrl
180 ? html`<img src="${notification.iconUrl}" alt="" />`
181 : html`<lucide-icon name="bell"></lucide-icon>`}
182 </div>
183 <div class="notification-content">
184 <div class="notification-title">
185 ${notification.title || "Notification"}
186 </div>
187 ${notification.body
188 ? html`<div class="notification-body">
189 ${notification.body}
190 </div>`
191 : ""}
192 <div class="notification-time">
193 ${this.formatTime(notification.timestamp)}
194 </div>
195 </div>
196 <button
197 class="dismiss-button"
198 @click=${(e) => this.handleDismiss(e, notification)}
199 >
200 <lucide-icon name="x"></lucide-icon>
201 </button>
202 </div>
203 `
204 )}
205 </div>
206 </div>
207 `;
208 }
209}
210
211customElements.define("mobile-notification-sheet", MobileNotificationSheet);