Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import {
4 LitElement,
5 html,
6} from "//shared.localhost:8888/third_party/lit/lit-all.min.js";
7
8export class PanelOverview extends LitElement {
9 static properties = {
10 open: { type: Boolean, reflect: true },
11 panels: { type: Array },
12 rootWidth: { type: Number },
13 rootHeight: { type: Number },
14 };
15
16 constructor() {
17 super();
18 this.open = false;
19 this.panels = [];
20 this.rootWidth = 0;
21 this.rootHeight = 0;
22 this.boundKeyHandler = this.handleKeyDown.bind(this);
23 }
24
25 connectedCallback() {
26 super.connectedCallback();
27 document.addEventListener("keydown", this.boundKeyHandler);
28 }
29
30 disconnectedCallback() {
31 super.disconnectedCallback();
32 document.removeEventListener("keydown", this.boundKeyHandler);
33 }
34
35 handleKeyDown(e) {
36 if (this.open && e.key === "Escape") {
37 this.close();
38 }
39 }
40
41 handleContainerClick(e) {
42 // Close if clicking on the background container (not a thumbnail)
43 if (e.target === e.currentTarget) {
44 this.close();
45 }
46 }
47
48 handleThumbnailClick(e, thumb) {
49 e.stopPropagation();
50 this.dispatchEvent(
51 new CustomEvent("overview-select", {
52 bubbles: true,
53 composed: true,
54 detail: {
55 webviewId: thumb.webviewId,
56 panelIndex: thumb.panelIndex,
57 },
58 }),
59 );
60 this.close();
61 }
62
63 close() {
64 this.open = false;
65 this.dispatchEvent(
66 new CustomEvent("overview-close", {
67 bubbles: true,
68 composed: true,
69 }),
70 );
71 }
72
73 // Calculate scale factor to fit all panels
74 calculateScale() {
75 if (!this.panels.length || !this.rootWidth || !this.rootHeight) {
76 return 1;
77 }
78
79 const gap = 16;
80 const availableWidth = this.rootWidth * 0.9;
81 const availableHeight = this.rootHeight * 0.8;
82
83 let totalUnscaledWidth = 0;
84 let maxUnscaledHeight = 0;
85 for (const panel of this.panels) {
86 totalUnscaledWidth += panel.width;
87 maxUnscaledHeight = Math.max(maxUnscaledHeight, panel.height);
88 }
89
90 const numPanels = this.panels.length;
91 const scaleForWidth =
92 (availableWidth - (numPanels - 1) * gap) / totalUnscaledWidth;
93 const scaleForHeight = availableHeight / maxUnscaledHeight;
94 return Math.min(scaleForWidth, scaleForHeight);
95 }
96
97 renderThumbnail(thumb, scale) {
98 const style = `
99 left: ${thumb.x * scale}px;
100 top: ${thumb.y * scale}px;
101 width: ${thumb.width * scale}px;
102 height: ${thumb.height * scale}px;
103 `;
104
105 const bgColor = thumb.themeColor || "var(--bg-header)";
106 const labelStyle = `
107 background-color: ${bgColor};
108 color: contrast-color(${bgColor});
109 `;
110
111 return html`
112 <div
113 class="thumbnail ${thumb.active ? "active" : ""}"
114 style=${style}
115 @click=${(e) => this.handleThumbnailClick(e, thumb)}
116 >
117 ${thumb.screenshotUrl
118 ? html`<img
119 src=${thumb.screenshotUrl}
120 alt=${thumb.title || "Webview"}
121 />`
122 : ""}
123 <div class="label" style=${labelStyle}>
124 ${thumb.title || "Untitled"}
125 </div>
126 </div>
127 `;
128 }
129
130 renderPanel(panel, scale) {
131 const wrapperStyle = `
132 width: ${panel.width * scale}px;
133 height: ${panel.height * scale}px;
134 `;
135 const panelStyle = `
136 width: ${panel.width * scale}px;
137 height: ${panel.height * scale}px;
138 `;
139
140 return html`
141 <div class="panel-wrapper" style=${wrapperStyle}>
142 <div class="panel" style=${panelStyle}>
143 ${panel.thumbnails.map((thumb) => this.renderThumbnail(thumb, scale))}
144 </div>
145 </div>
146 `;
147 }
148
149 render() {
150 const scale = this.calculateScale();
151
152 return html`
153 <link rel="stylesheet" href="overview.css" />
154 <div class="container" @click=${this.handleContainerClick}>
155 <div class="panels">
156 ${this.panels.map((panel) => this.renderPanel(panel, scale))}
157 </div>
158 </div>
159 `;
160 }
161}
162
163customElements.define("panel-overview", PanelOverview);