treesap 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Sidebar.d.ts +8 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.js +6 -0
- package/dist/components/Sidebar.js.map +1 -0
- package/dist/components/SimpleLivePreview.js +1 -1
- package/dist/components/SimpleLivePreview.js.map +1 -1
- package/dist/layouts/Layout.js +1 -1
- package/dist/layouts/Layout.js.map +1 -1
- package/dist/pages/Code.d.ts.map +1 -1
- package/dist/pages/Code.js +2 -2
- package/dist/pages/Code.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +81 -11
- package/dist/server.js.map +1 -1
- package/dist/services/terminal.d.ts +25 -1
- package/dist/services/terminal.d.ts.map +1 -1
- package/dist/services/terminal.js +135 -6
- package/dist/services/terminal.js.map +1 -1
- package/dist/services/websocket.d.ts +45 -0
- package/dist/services/websocket.d.ts.map +1 -0
- package/dist/services/websocket.js +306 -0
- package/dist/services/websocket.js.map +1 -0
- package/dist/static/components/Sidebar.js +225 -0
- package/dist/static/components/SimpleLivePreview.js +73 -53
- package/dist/static/components/Terminal.js +141 -61
- package/dist/static/signals/SidebarSignal.js +123 -0
- package/dist/static/signals/TerminalSignal.js +137 -2
- package/dist/static/styles/main.css +111 -25
- package/package.json +6 -2
- package/src/components/Sidebar.tsx +92 -0
- package/src/components/SimpleLivePreview.tsx +4 -4
- package/src/layouts/Layout.tsx +1 -1
- package/src/pages/Code.tsx +18 -145
- package/src/server.tsx +97 -12
- package/src/services/terminal.ts +164 -6
- package/src/services/websocket.ts +374 -0
- package/src/static/components/Sidebar.js +225 -0
- package/src/static/components/SimpleLivePreview.js +73 -53
- package/src/static/components/Terminal.js +141 -61
- package/src/static/signals/SidebarSignal.js +123 -0
- package/src/static/signals/TerminalSignal.js +137 -2
- package/src/static/styles/main.css +111 -25
- package/tailwind.config.ts +10 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { TerminalService, type TerminalSession } from './terminal.js';
|
|
5
|
+
import type { IncomingMessage } from 'http';
|
|
6
|
+
import type { Server } from 'http';
|
|
7
|
+
|
|
8
|
+
export interface WebSocketClient {
|
|
9
|
+
id: string;
|
|
10
|
+
ws: WebSocket;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
terminalId?: string;
|
|
13
|
+
lastPing: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WebSocketMessage {
|
|
17
|
+
type: 'join' | 'leave' | 'input' | 'ping' | 'pong';
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
terminalId?: string;
|
|
20
|
+
data?: string;
|
|
21
|
+
timestamp?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class WebSocketTerminalService {
|
|
25
|
+
private static wss: WebSocketServer | null = null;
|
|
26
|
+
private static clients = new Map<string, WebSocketClient>();
|
|
27
|
+
private static sessionClients = new Map<string, Set<string>>(); // sessionId -> Set<clientId>
|
|
28
|
+
|
|
29
|
+
static initialize(server: Server) {
|
|
30
|
+
this.wss = new WebSocketServer({
|
|
31
|
+
server,
|
|
32
|
+
path: '/terminal/ws'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.wss.on('connection', (ws: WebSocket, request: IncomingMessage) => {
|
|
36
|
+
const clientId = uuidv4();
|
|
37
|
+
const client: WebSocketClient = {
|
|
38
|
+
id: clientId,
|
|
39
|
+
ws,
|
|
40
|
+
lastPing: new Date()
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.clients.set(clientId, client);
|
|
44
|
+
console.log(`WebSocket client connected: ${clientId}`);
|
|
45
|
+
|
|
46
|
+
// Set up message handler
|
|
47
|
+
ws.on('message', (data: Buffer) => {
|
|
48
|
+
this.handleMessage(clientId, data);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Set up disconnect handler
|
|
52
|
+
ws.on('close', () => {
|
|
53
|
+
this.handleDisconnect(clientId);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Set up error handler
|
|
57
|
+
ws.on('error', (error) => {
|
|
58
|
+
console.error(`WebSocket error for client ${clientId}:`, error);
|
|
59
|
+
this.handleDisconnect(clientId);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Send initial connection success
|
|
63
|
+
this.sendToClient(clientId, {
|
|
64
|
+
type: 'pong',
|
|
65
|
+
timestamp: Date.now()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Set up ping/pong for connection health
|
|
69
|
+
this.setupPingPong(clientId);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log('WebSocket server initialized for terminal connections');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static setupPingPong(clientId: string) {
|
|
76
|
+
const client = this.clients.get(clientId);
|
|
77
|
+
if (!client) return;
|
|
78
|
+
|
|
79
|
+
const pingInterval = setInterval(() => {
|
|
80
|
+
if (client.ws.readyState === WebSocket.OPEN) {
|
|
81
|
+
client.ws.ping();
|
|
82
|
+
client.lastPing = new Date();
|
|
83
|
+
} else {
|
|
84
|
+
clearInterval(pingInterval);
|
|
85
|
+
}
|
|
86
|
+
}, 30000); // Ping every 30 seconds
|
|
87
|
+
|
|
88
|
+
client.ws.on('pong', () => {
|
|
89
|
+
client.lastPing = new Date();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
client.ws.on('close', () => {
|
|
93
|
+
clearInterval(pingInterval);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static handleMessage(clientId: string, data: Buffer) {
|
|
98
|
+
try {
|
|
99
|
+
const message: WebSocketMessage = JSON.parse(data.toString());
|
|
100
|
+
const client = this.clients.get(clientId);
|
|
101
|
+
|
|
102
|
+
if (!client) {
|
|
103
|
+
console.error(`Client ${clientId} not found`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (message.type) {
|
|
108
|
+
case 'join':
|
|
109
|
+
this.handleJoin(clientId, message);
|
|
110
|
+
break;
|
|
111
|
+
case 'leave':
|
|
112
|
+
this.handleLeave(clientId, message);
|
|
113
|
+
break;
|
|
114
|
+
case 'input':
|
|
115
|
+
this.handleInput(clientId, message);
|
|
116
|
+
break;
|
|
117
|
+
case 'ping':
|
|
118
|
+
this.sendToClient(clientId, {
|
|
119
|
+
type: 'pong',
|
|
120
|
+
timestamp: Date.now()
|
|
121
|
+
});
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
console.warn(`Unknown message type: ${message.type}`);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`Error parsing WebSocket message from ${clientId}:`, error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static handleJoin(clientId: string, message: WebSocketMessage) {
|
|
132
|
+
const client = this.clients.get(clientId);
|
|
133
|
+
if (!client || !message.sessionId) return;
|
|
134
|
+
|
|
135
|
+
console.log(`Client ${clientId} joining session ${message.sessionId}`);
|
|
136
|
+
|
|
137
|
+
// Remove client from any existing session
|
|
138
|
+
if (client.sessionId) {
|
|
139
|
+
this.removeClientFromSession(clientId, client.sessionId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get or create terminal session
|
|
143
|
+
let session = TerminalService.getSession(message.sessionId);
|
|
144
|
+
if (!session) {
|
|
145
|
+
console.log(`Creating new terminal session: ${message.sessionId}`);
|
|
146
|
+
session = TerminalService.createSession(message.sessionId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Update client info
|
|
150
|
+
client.sessionId = message.sessionId;
|
|
151
|
+
client.terminalId = message.terminalId;
|
|
152
|
+
|
|
153
|
+
// Add client to session tracking
|
|
154
|
+
this.addClientToSession(clientId, message.sessionId);
|
|
155
|
+
|
|
156
|
+
// Set up output listener for this session (if not already set up)
|
|
157
|
+
this.setupSessionOutputListener(message.sessionId);
|
|
158
|
+
|
|
159
|
+
// Send connection confirmation
|
|
160
|
+
this.sendToClient(clientId, {
|
|
161
|
+
type: 'connected',
|
|
162
|
+
sessionId: message.sessionId,
|
|
163
|
+
timestamp: Date.now()
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Notify all clients in this session about client count
|
|
167
|
+
this.broadcastClientCount(message.sessionId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private static handleLeave(clientId: string, message: WebSocketMessage) {
|
|
171
|
+
const client = this.clients.get(clientId);
|
|
172
|
+
if (!client) return;
|
|
173
|
+
|
|
174
|
+
console.log(`Client ${clientId} leaving session ${client.sessionId}`);
|
|
175
|
+
|
|
176
|
+
if (client.sessionId) {
|
|
177
|
+
this.removeClientFromSession(clientId, client.sessionId);
|
|
178
|
+
this.broadcastClientCount(client.sessionId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
client.sessionId = undefined;
|
|
182
|
+
client.terminalId = undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static handleInput(clientId: string, message: WebSocketMessage) {
|
|
186
|
+
const client = this.clients.get(clientId);
|
|
187
|
+
if (!client || !message.sessionId || message.data === undefined) return;;
|
|
188
|
+
|
|
189
|
+
// Get the terminal session
|
|
190
|
+
const session = TerminalService.getSession(message.sessionId);
|
|
191
|
+
if (!session) {
|
|
192
|
+
console.error(`Session ${message.sessionId} not found`);
|
|
193
|
+
this.sendToClient(clientId, {
|
|
194
|
+
type: 'error',
|
|
195
|
+
data: 'Terminal session not found',
|
|
196
|
+
timestamp: Date.now()
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Send input directly to PTY (raw input like key presses)
|
|
202
|
+
try {
|
|
203
|
+
session.lastActivity = new Date();
|
|
204
|
+
session.process.write(message.data);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(`Failed to send input to session ${message.sessionId}:`, error);
|
|
207
|
+
this.sendToClient(clientId, {
|
|
208
|
+
type: 'error',
|
|
209
|
+
data: 'Failed to send input to terminal',
|
|
210
|
+
timestamp: Date.now()
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private static handleDisconnect(clientId: string) {
|
|
216
|
+
const client = this.clients.get(clientId);
|
|
217
|
+
console.log(`WebSocket client disconnected: ${clientId}`);
|
|
218
|
+
|
|
219
|
+
if (client?.sessionId) {
|
|
220
|
+
this.removeClientFromSession(clientId, client.sessionId);
|
|
221
|
+
this.broadcastClientCount(client.sessionId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.clients.delete(clientId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static addClientToSession(clientId: string, sessionId: string) {
|
|
228
|
+
if (!this.sessionClients.has(sessionId)) {
|
|
229
|
+
this.sessionClients.set(sessionId, new Set());
|
|
230
|
+
}
|
|
231
|
+
this.sessionClients.get(sessionId)!.add(clientId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private static removeClientFromSession(clientId: string, sessionId: string) {
|
|
235
|
+
const clientSet = this.sessionClients.get(sessionId);
|
|
236
|
+
if (clientSet) {
|
|
237
|
+
clientSet.delete(clientId);
|
|
238
|
+
if (clientSet.size === 0) {
|
|
239
|
+
this.sessionClients.delete(sessionId);
|
|
240
|
+
// Clean up output listener if no clients are connected
|
|
241
|
+
this.cleanupSessionOutputListener(sessionId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static setupSessionOutputListener(sessionId: string) {
|
|
247
|
+
const session = TerminalService.getSession(sessionId);
|
|
248
|
+
if (!session) return;
|
|
249
|
+
|
|
250
|
+
// Check if listener already exists
|
|
251
|
+
if (session.eventEmitter.listenerCount('output') > 0) {
|
|
252
|
+
return; // Listener already set up
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const handleOutput = (data: any) => {
|
|
256
|
+
this.broadcastToSession(sessionId, data);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
session.eventEmitter.on('output', handleOutput);
|
|
260
|
+
console.log(`Set up output listener for session ${sessionId}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private static cleanupSessionOutputListener(sessionId: string) {
|
|
264
|
+
const session = TerminalService.getSession(sessionId);
|
|
265
|
+
if (!session) return;
|
|
266
|
+
|
|
267
|
+
session.eventEmitter.removeAllListeners('output');
|
|
268
|
+
console.log(`Cleaned up output listener for session ${sessionId}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private static broadcastToSession(sessionId: string, data: any) {
|
|
272
|
+
const clientIds = this.sessionClients.get(sessionId);
|
|
273
|
+
if (!clientIds) return;
|
|
274
|
+
|
|
275
|
+
const message = {
|
|
276
|
+
...data,
|
|
277
|
+
timestamp: Date.now()
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
for (const clientId of clientIds) {
|
|
281
|
+
this.sendToClient(clientId, message);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static broadcastClientCount(sessionId: string) {
|
|
286
|
+
const clientIds = this.sessionClients.get(sessionId);
|
|
287
|
+
const count = clientIds ? clientIds.size : 0;
|
|
288
|
+
|
|
289
|
+
if (clientIds) {
|
|
290
|
+
for (const clientId of clientIds) {
|
|
291
|
+
this.sendToClient(clientId, {
|
|
292
|
+
type: 'clients_count',
|
|
293
|
+
count,
|
|
294
|
+
timestamp: Date.now()
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private static sendToClient(clientId: string, message: any) {
|
|
301
|
+
const client = this.clients.get(clientId);
|
|
302
|
+
if (!client || client.ws.readyState !== WebSocket.OPEN) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
client.ws.send(JSON.stringify(message));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error(`Error sending message to client ${clientId}:`, error);
|
|
310
|
+
this.handleDisconnect(clientId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// API methods for external control
|
|
315
|
+
static getSessionClients(sessionId: string): string[] {
|
|
316
|
+
const clientSet = this.sessionClients.get(sessionId);
|
|
317
|
+
return clientSet ? Array.from(clientSet) : [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static sendCommandToSession(sessionId: string, command: string): boolean {
|
|
321
|
+
// Send command to terminal
|
|
322
|
+
const success = TerminalService.executeCommand(sessionId, command);
|
|
323
|
+
|
|
324
|
+
if (success) {
|
|
325
|
+
// The output will be automatically broadcast to all connected clients
|
|
326
|
+
// via the output listener
|
|
327
|
+
console.log(`Command sent to session ${sessionId}: ${command.substring(0, 50)}...`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return success;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static getActiveSessions(): Array<{ sessionId: string; clientCount: number }> {
|
|
334
|
+
return Array.from(this.sessionClients.entries()).map(([sessionId, clients]) => ({
|
|
335
|
+
sessionId,
|
|
336
|
+
clientCount: clients.size
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static getConnectedClients(): number {
|
|
341
|
+
return this.clients.size;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static closeSession(sessionId: string) {
|
|
345
|
+
const clientIds = this.sessionClients.get(sessionId);
|
|
346
|
+
if (clientIds) {
|
|
347
|
+
// Notify all clients that the session is closing
|
|
348
|
+
for (const clientId of clientIds) {
|
|
349
|
+
this.sendToClient(clientId, {
|
|
350
|
+
type: 'session_closed',
|
|
351
|
+
sessionId,
|
|
352
|
+
timestamp: Date.now()
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Clean up client tracking
|
|
357
|
+
this.sessionClients.delete(sessionId);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Clean up the terminal session
|
|
361
|
+
TerminalService.destroySession(sessionId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
static cleanup() {
|
|
365
|
+
if (this.wss) {
|
|
366
|
+
console.log('Closing WebSocket server...');
|
|
367
|
+
this.wss.close();
|
|
368
|
+
this.wss = null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.clients.clear();
|
|
372
|
+
this.sessionClients.clear();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Sidebar component JavaScript for responsive behavior
|
|
2
|
+
import { sidebarStore } from '/signals/SidebarSignal.js';
|
|
3
|
+
|
|
4
|
+
class SidebarManager {
|
|
5
|
+
constructor(id = 'sidebar') {
|
|
6
|
+
this.id = id;
|
|
7
|
+
|
|
8
|
+
// DOM elements
|
|
9
|
+
this.backdrop = document.getElementById(`${id}-backdrop`);
|
|
10
|
+
this.pane = document.getElementById(`${id}-pane`);
|
|
11
|
+
this.closeBtn = document.getElementById(`${id}-close-btn`);
|
|
12
|
+
this.refreshBtn = document.getElementById('live-preview-refresh-btn');
|
|
13
|
+
this.urlInput = document.getElementById('live-preview-url-input');
|
|
14
|
+
this.loadBtn = document.getElementById('live-preview-load-btn');
|
|
15
|
+
|
|
16
|
+
// Reference to the sidebar store
|
|
17
|
+
this.store = sidebarStore;
|
|
18
|
+
|
|
19
|
+
this.init();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
init() {
|
|
23
|
+
console.log('Initializing Sidebar:', this.id);
|
|
24
|
+
console.log('Elements found:', {
|
|
25
|
+
backdrop: !!this.backdrop,
|
|
26
|
+
pane: !!this.pane,
|
|
27
|
+
closeBtn: !!this.closeBtn,
|
|
28
|
+
refreshBtn: !!this.refreshBtn,
|
|
29
|
+
urlInput: !!this.urlInput,
|
|
30
|
+
loadBtn: !!this.loadBtn
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Set up event listeners
|
|
34
|
+
this.setupEventListeners();
|
|
35
|
+
|
|
36
|
+
// Subscribe to store state changes
|
|
37
|
+
this.subscribeToStore();
|
|
38
|
+
|
|
39
|
+
// Initial state update
|
|
40
|
+
this.updateSidebarState();
|
|
41
|
+
this.updateMobileToggle();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setupEventListeners() {
|
|
45
|
+
// Mobile close button
|
|
46
|
+
this.closeBtn?.addEventListener('click', () => this.store.close());
|
|
47
|
+
|
|
48
|
+
// Backdrop click to close
|
|
49
|
+
this.backdrop?.addEventListener('click', () => this.store.close());
|
|
50
|
+
|
|
51
|
+
// Mobile toggle button (in main layout)
|
|
52
|
+
const mobileToggle = document.getElementById('mobile-sidebar-toggle');
|
|
53
|
+
mobileToggle?.addEventListener('click', () => this.store.toggle());
|
|
54
|
+
|
|
55
|
+
// Refresh button
|
|
56
|
+
this.refreshBtn?.addEventListener('click', () => this.refreshPreview());
|
|
57
|
+
|
|
58
|
+
// URL navigation
|
|
59
|
+
this.loadBtn?.addEventListener('click', (e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
this.loadUrl();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.urlInput?.addEventListener('keypress', (e) => {
|
|
65
|
+
if (e.key === 'Enter') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
this.loadUrl();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Keyboard shortcuts
|
|
72
|
+
document.addEventListener('keydown', (e) => {
|
|
73
|
+
// Escape key to close sidebar on mobile
|
|
74
|
+
if (e.key === 'Escape' && this.store.isMobile.value && this.store.isOpen.value) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
this.store.close();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Cmd/Ctrl + B to toggle sidebar
|
|
80
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
this.store.toggle();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Listen for custom events to maintain backward compatibility
|
|
87
|
+
document.addEventListener('sidebar:toggle', () => this.store.toggle());
|
|
88
|
+
document.addEventListener('sidebar:open', () => this.store.open());
|
|
89
|
+
document.addEventListener('sidebar:close', () => this.store.close());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
subscribeToStore() {
|
|
93
|
+
// Subscribe to store changes and update UI accordingly
|
|
94
|
+
this.store.isOpen.subscribe(() => this.updateSidebarState());
|
|
95
|
+
this.store.isMobile.subscribe(() => this.updateSidebarState());
|
|
96
|
+
this.store.shouldShowBackdrop.subscribe(() => this.updateSidebarState());
|
|
97
|
+
this.store.shouldShowMobileToggle.subscribe(() => this.updateMobileToggle());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
updateSidebarState() {
|
|
101
|
+
if (!this.pane || !this.backdrop) return;
|
|
102
|
+
|
|
103
|
+
const isOpen = this.store.isOpen.value;
|
|
104
|
+
const isMobile = this.store.isMobile.value;
|
|
105
|
+
const shouldShowBackdrop = this.store.shouldShowBackdrop.value;
|
|
106
|
+
|
|
107
|
+
if (isMobile) {
|
|
108
|
+
// Mobile behavior: overlay
|
|
109
|
+
if (shouldShowBackdrop) {
|
|
110
|
+
// Show backdrop
|
|
111
|
+
this.backdrop.classList.remove('opacity-0', 'pointer-events-none');
|
|
112
|
+
this.backdrop.classList.add('opacity-100');
|
|
113
|
+
|
|
114
|
+
// Prevent body scroll
|
|
115
|
+
document.body.style.overflow = 'hidden';
|
|
116
|
+
} else {
|
|
117
|
+
// Hide backdrop
|
|
118
|
+
this.backdrop.classList.remove('opacity-100');
|
|
119
|
+
this.backdrop.classList.add('opacity-0', 'pointer-events-none');
|
|
120
|
+
|
|
121
|
+
// Restore body scroll
|
|
122
|
+
document.body.style.overflow = '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isOpen) {
|
|
126
|
+
// Show sidebar
|
|
127
|
+
this.pane.classList.remove('-translate-x-full');
|
|
128
|
+
this.pane.classList.add('translate-x-0');
|
|
129
|
+
} else {
|
|
130
|
+
// Hide sidebar
|
|
131
|
+
this.pane.classList.remove('translate-x-0');
|
|
132
|
+
this.pane.classList.add('-translate-x-full');
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Desktop behavior: side panel
|
|
136
|
+
// Hide backdrop (not needed on desktop)
|
|
137
|
+
this.backdrop.classList.add('opacity-0', 'pointer-events-none');
|
|
138
|
+
|
|
139
|
+
// Restore body scroll
|
|
140
|
+
document.body.style.overflow = '';
|
|
141
|
+
|
|
142
|
+
if (isOpen) {
|
|
143
|
+
// Show sidebar
|
|
144
|
+
this.pane.classList.remove('-translate-x-full');
|
|
145
|
+
this.pane.classList.add('translate-x-0');
|
|
146
|
+
this.pane.style.display = '';
|
|
147
|
+
} else {
|
|
148
|
+
// Hide sidebar completely on desktop
|
|
149
|
+
this.pane.style.display = 'none';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateMobileToggle() {
|
|
155
|
+
const mobileToggle = document.getElementById('mobile-sidebar-toggle');
|
|
156
|
+
const shouldShow = this.store.shouldShowMobileToggle.value;
|
|
157
|
+
|
|
158
|
+
if (mobileToggle) {
|
|
159
|
+
mobileToggle.style.display = shouldShow ? 'flex' : 'none';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
refreshPreview() {
|
|
164
|
+
// Dispatch event for SimpleLivePreview to handle
|
|
165
|
+
document.dispatchEvent(new CustomEvent('preview:refresh'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
loadUrl() {
|
|
169
|
+
if (this.urlInput) {
|
|
170
|
+
const path = this.urlInput.value.trim();
|
|
171
|
+
// Dispatch event for SimpleLivePreview to handle
|
|
172
|
+
document.dispatchEvent(new CustomEvent('preview:loadUrl', {
|
|
173
|
+
detail: { path }
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Public API methods
|
|
179
|
+
getState() {
|
|
180
|
+
return this.store.getState();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
destroy() {
|
|
184
|
+
// Restore body scroll
|
|
185
|
+
document.body.style.overflow = '';
|
|
186
|
+
|
|
187
|
+
// Clean up is handled by the signal store
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Auto-initialize when script loads
|
|
192
|
+
console.log('Sidebar.js loaded, looking for sidebar containers...');
|
|
193
|
+
|
|
194
|
+
function initializeSidebar() {
|
|
195
|
+
// Look for sapling-islands containing sidebar content
|
|
196
|
+
const saplingIslands = document.querySelectorAll('sapling-island');
|
|
197
|
+
|
|
198
|
+
for (const island of saplingIslands) {
|
|
199
|
+
// Look for sidebar pane div
|
|
200
|
+
const sidebarPane = island.querySelector('div[id$="-pane"]');
|
|
201
|
+
if (sidebarPane && sidebarPane.id.includes('sidebar')) {
|
|
202
|
+
const sidebarId = sidebarPane.id.replace('-pane', '');
|
|
203
|
+
console.log('Found Sidebar component with ID:', sidebarId);
|
|
204
|
+
|
|
205
|
+
// Create and store manager
|
|
206
|
+
const manager = new SidebarManager(sidebarId);
|
|
207
|
+
window.sidebarManager = manager; // Make globally available
|
|
208
|
+
|
|
209
|
+
break; // Only one sidebar per page
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Initialize immediately since Sapling islands are ready
|
|
215
|
+
initializeSidebar();
|
|
216
|
+
|
|
217
|
+
// Make available globally
|
|
218
|
+
window.SidebarManager = SidebarManager;
|
|
219
|
+
|
|
220
|
+
// Cleanup on page unload
|
|
221
|
+
window.addEventListener('beforeunload', () => {
|
|
222
|
+
if (window.sidebarManager) {
|
|
223
|
+
window.sidebarManager.destroy();
|
|
224
|
+
}
|
|
225
|
+
});
|