Rewild Your Web
web browser dweb
at main 211 lines 5.9 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 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);