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.
Files changed (43) hide show
  1. package/dist/components/Sidebar.d.ts +8 -0
  2. package/dist/components/Sidebar.d.ts.map +1 -0
  3. package/dist/components/Sidebar.js +6 -0
  4. package/dist/components/Sidebar.js.map +1 -0
  5. package/dist/components/SimpleLivePreview.js +1 -1
  6. package/dist/components/SimpleLivePreview.js.map +1 -1
  7. package/dist/layouts/Layout.js +1 -1
  8. package/dist/layouts/Layout.js.map +1 -1
  9. package/dist/pages/Code.d.ts.map +1 -1
  10. package/dist/pages/Code.js +2 -2
  11. package/dist/pages/Code.js.map +1 -1
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +81 -11
  14. package/dist/server.js.map +1 -1
  15. package/dist/services/terminal.d.ts +25 -1
  16. package/dist/services/terminal.d.ts.map +1 -1
  17. package/dist/services/terminal.js +135 -6
  18. package/dist/services/terminal.js.map +1 -1
  19. package/dist/services/websocket.d.ts +45 -0
  20. package/dist/services/websocket.d.ts.map +1 -0
  21. package/dist/services/websocket.js +306 -0
  22. package/dist/services/websocket.js.map +1 -0
  23. package/dist/static/components/Sidebar.js +225 -0
  24. package/dist/static/components/SimpleLivePreview.js +73 -53
  25. package/dist/static/components/Terminal.js +141 -61
  26. package/dist/static/signals/SidebarSignal.js +123 -0
  27. package/dist/static/signals/TerminalSignal.js +137 -2
  28. package/dist/static/styles/main.css +111 -25
  29. package/package.json +6 -2
  30. package/src/components/Sidebar.tsx +92 -0
  31. package/src/components/SimpleLivePreview.tsx +4 -4
  32. package/src/layouts/Layout.tsx +1 -1
  33. package/src/pages/Code.tsx +18 -145
  34. package/src/server.tsx +97 -12
  35. package/src/services/terminal.ts +164 -6
  36. package/src/services/websocket.ts +374 -0
  37. package/src/static/components/Sidebar.js +225 -0
  38. package/src/static/components/SimpleLivePreview.js +73 -53
  39. package/src/static/components/Terminal.js +141 -61
  40. package/src/static/signals/SidebarSignal.js +123 -0
  41. package/src/static/signals/TerminalSignal.js +137 -2
  42. package/src/static/styles/main.css +111 -25
  43. package/tailwind.config.ts +10 -0
@@ -1,4 +1,6 @@
1
1
  // SimpleLivePreview component JavaScript
2
+ import { sidebarStore } from '/signals/SidebarSignal.js';
3
+
2
4
  class SimpleLivePreviewManager {
3
5
  constructor(id = 'simple-preview') {
4
6
  this.id = id;
@@ -17,8 +19,8 @@ class SimpleLivePreviewManager {
17
19
  // Get preview port from iframe data attribute
18
20
  this.previewPort = this.iframe?.getAttribute('data-preview-port') || 5173;
19
21
 
20
- // State
21
- this.isSidebarHidden = false;
22
+ // Reference to the sidebar store
23
+ this.store = sidebarStore;
22
24
 
23
25
  this.init();
24
26
  }
@@ -34,12 +36,15 @@ class SimpleLivePreviewManager {
34
36
 
35
37
  // Set up event listeners
36
38
  this.setupEventListeners();
39
+
40
+ // Subscribe to sidebar store changes
41
+ this.subscribeToStore();
37
42
  }
38
43
 
39
44
  setupEventListeners() {
40
45
  // Hide sidebar toggle (both sidebar and floating button)
41
- this.hideSidebarBtn?.addEventListener('click', () => this.toggleSidebarVisibility());
42
- this.floatingHideSidebarBtn?.addEventListener('click', () => this.toggleSidebarVisibility());
46
+ this.hideSidebarBtn?.addEventListener('click', () => this.store.toggle());
47
+ this.floatingHideSidebarBtn?.addEventListener('click', () => this.store.toggle());
43
48
 
44
49
  // Refresh button
45
50
  this.refreshBtn?.addEventListener('click', () => this.refreshIframe());
@@ -62,6 +67,21 @@ class SimpleLivePreviewManager {
62
67
  this.loadUrl();
63
68
  });
64
69
 
70
+ // Listen for events from Sidebar component
71
+ document.addEventListener('preview:refresh', () => this.refreshIframe());
72
+ document.addEventListener('preview:loadUrl', (e) => {
73
+ if (e.detail && e.detail.path !== undefined) {
74
+ this.loadUrlFromPath(e.detail.path);
75
+ }
76
+ });
77
+
78
+ // Legacy: Listen for sidebar state changes (for backward compatibility)
79
+ document.addEventListener('sidebar:stateChanged', (e) => {
80
+ if (e.detail) {
81
+ this.handleSidebarStateChange(e.detail);
82
+ }
83
+ });
84
+
65
85
  // Keyboard shortcuts
66
86
  document.addEventListener('keydown', (e) => {
67
87
  // Cmd/Ctrl + R for refresh
@@ -69,11 +89,6 @@ class SimpleLivePreviewManager {
69
89
  e.preventDefault();
70
90
  this.refreshIframe();
71
91
  }
72
- // Cmd/Ctrl + B to toggle sidebar panel
73
- if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
74
- e.preventDefault();
75
- this.toggleSidebarVisibility();
76
- }
77
92
  });
78
93
 
79
94
  // Handle iframe load errors (for X-Frame-Options violations)
@@ -129,55 +144,41 @@ class SimpleLivePreviewManager {
129
144
  }
130
145
  }
131
146
 
132
- toggleSidebarVisibility() {
133
- this.isSidebarHidden = !this.isSidebarHidden;
147
+ subscribeToStore() {
148
+ // Subscribe to sidebar store changes
149
+ this.store.shouldShowFloatingButton.subscribe(() => this.updateFloatingButton());
150
+ this.store.isOpen.subscribe(() => this.updateFloatingButton());
134
151
 
135
- const sidebarPane = document.getElementById('sidebar-pane');
136
- const previewPane = document.getElementById(this.id);
152
+ // Initial update
153
+ this.updateFloatingButton();
154
+ }
155
+
156
+ updateFloatingButton() {
157
+ const floatingBtn = this.floatingHideSidebarBtn;
158
+ const floatingIcon = this.floatingHideSidebarIcon;
159
+ const shouldShow = this.store.shouldShowFloatingButton.value;
137
160
 
138
- if (sidebarPane && previewPane) {
139
- if (this.isSidebarHidden) {
140
- // Hide sidebar pane and make preview full width
141
- sidebarPane.style.display = 'none';
142
- previewPane.classList.remove('flex-1');
143
- previewPane.classList.add('w-full');
144
-
145
- // Update button icons and titles
146
- if (this.hideSidebarIcon) {
147
- this.hideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
148
- }
149
- if (this.floatingHideSidebarIcon) {
150
- this.floatingHideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
151
- }
152
- if (this.hideSidebarBtn) {
153
- this.hideSidebarBtn.setAttribute('title', 'Show Sidebar');
154
- }
155
- if (this.floatingHideSidebarBtn) {
156
- this.floatingHideSidebarBtn.setAttribute('title', 'Show Sidebar');
161
+ if (floatingBtn) {
162
+ if (shouldShow) {
163
+ // Show floating button when sidebar is closed
164
+ floatingBtn.style.display = 'flex';
165
+ // Update icon and title
166
+ if (floatingIcon) {
167
+ floatingIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
157
168
  }
169
+ floatingBtn.setAttribute('title', 'Show Sidebar');
158
170
  } else {
159
- // Show sidebar pane and restore original widths
160
- sidebarPane.style.display = '';
161
- previewPane.classList.remove('w-full');
162
- previewPane.classList.add('flex-1');
163
-
164
- // Update button icons and titles
165
- if (this.hideSidebarIcon) {
166
- this.hideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple');
167
- }
168
- if (this.floatingHideSidebarIcon) {
169
- this.floatingHideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple');
170
- }
171
- if (this.hideSidebarBtn) {
172
- this.hideSidebarBtn.setAttribute('title', 'Hide Sidebar');
173
- }
174
- if (this.floatingHideSidebarBtn) {
175
- this.floatingHideSidebarBtn.setAttribute('title', 'Hide Sidebar');
176
- }
171
+ // Hide floating button when sidebar is open
172
+ floatingBtn.style.display = 'none';
177
173
  }
178
174
  }
179
175
  }
180
176
 
177
+ toggleSidebarVisibility() {
178
+ // Use the store to toggle
179
+ this.store.toggle();
180
+ }
181
+
181
182
  refreshIframe() {
182
183
  if (this.iframe) {
183
184
  console.log('Refreshing iframe...');
@@ -191,7 +192,13 @@ class SimpleLivePreviewManager {
191
192
  loadUrl() {
192
193
  if (this.urlInput && this.iframe) {
193
194
  const path = this.urlInput.value.trim();
194
- console.log('loadUrl called with path:', path);
195
+ this.loadUrlFromPath(path);
196
+ }
197
+ }
198
+
199
+ loadUrlFromPath(path) {
200
+ if (this.iframe) {
201
+ console.log('loadUrlFromPath called with path:', path);
195
202
 
196
203
  // Check if it's an external URL (starts with http:// or https://)
197
204
  if (path.startsWith('http://') || path.startsWith('https://')) {
@@ -201,8 +208,10 @@ class SimpleLivePreviewManager {
201
208
  // Open external URLs in a new tab
202
209
  console.log('Opening external URL in new tab:', path);
203
210
  window.open(path, '_blank');
204
- // Clear the input
205
- this.urlInput.value = '';
211
+ // Clear the input if it exists
212
+ if (this.urlInput) {
213
+ this.urlInput.value = '';
214
+ }
206
215
  return;
207
216
  }
208
217
  }
@@ -211,9 +220,20 @@ class SimpleLivePreviewManager {
211
220
  const newUrl = path ? baseUrl + '/' + path.replace(/^\//, '') : baseUrl;
212
221
  console.log('Loading URL in iframe:', newUrl);
213
222
  this.iframe.src = newUrl;
223
+
224
+ // Update input if it exists
225
+ if (this.urlInput) {
226
+ this.urlInput.value = path;
227
+ }
214
228
  }
215
229
  }
216
230
 
231
+ handleSidebarStateChange(state) {
232
+ // Legacy method for backward compatibility
233
+ console.log('Legacy sidebar state changed:', state);
234
+ // The floating button is now handled by updateFloatingButton()
235
+ }
236
+
217
237
  handleIframeError() {
218
238
  // Get the current iframe src and open it in a new tab if it's external
219
239
  if (this.iframe && this.iframe.src) {
@@ -1,6 +1,7 @@
1
- // Terminal component JavaScript using Xterm.js
1
+ // Terminal component JavaScript using Xterm.js with WebSocket
2
2
  import { Terminal } from 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm';
3
3
  import { terminalStore } from '/signals/TerminalSignal.js';
4
+
4
5
  class TerminalManager {
5
6
  constructor(terminalId) {
6
7
  this.terminalId = terminalId;
@@ -25,8 +26,12 @@ class TerminalManager {
25
26
  terminalStore.addTerminal(this.index);
26
27
  terminalStore.updateTerminalStatus(terminalId, 'connecting');
27
28
 
28
- this.eventSource = null;
29
+ // WebSocket connection
30
+ this.websocket = null;
29
31
  this.terminal = null;
32
+ this.reconnectAttempts = 0;
33
+ this.maxReconnectAttempts = 5;
34
+ this.reconnectDelay = 1000; // Start with 1 second
30
35
 
31
36
  this.init();
32
37
  }
@@ -48,7 +53,7 @@ class TerminalManager {
48
53
  // Set up event listeners
49
54
  this.setupEventListeners();
50
55
 
51
- // Connect to terminal stream
56
+ // Connect to terminal via WebSocket
52
57
  this.connectToTerminal();
53
58
  }
54
59
 
@@ -136,65 +141,147 @@ class TerminalManager {
136
141
  }
137
142
 
138
143
  sendInput(data) {
139
- // Send input directly to shell stdin
140
- fetch(`/terminal/input/${this.sessionId}`, {
141
- method: 'POST',
142
- headers: {
143
- 'Content-Type': 'application/json',
144
- },
145
- body: JSON.stringify({ input: data })
146
- })
147
- .catch(error => {
148
- console.error('Error sending input:', error);
149
- });
144
+ // Send input via WebSocket
145
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
146
+ const message = {
147
+ type: 'input',
148
+ sessionId: this.sessionId,
149
+ terminalId: this.terminalId,
150
+ data: data
151
+ };
152
+ this.websocket.send(JSON.stringify(message));
153
+ } else {
154
+ console.error('WebSocket not connected, cannot send input');
155
+ this.updateStatus('Disconnected');
156
+ terminalStore.updateTerminalStatus(this.terminalId, 'disconnected');
157
+ }
150
158
  }
151
159
 
152
160
  connectToTerminal() {
153
- if (this.eventSource) {
154
- this.eventSource.close();
161
+ if (this.websocket) {
162
+ this.websocket.close();
155
163
  }
156
164
 
157
165
  this.updateStatus('Connecting...');
158
166
 
159
- this.eventSource = new EventSource(`/terminal/stream/${this.sessionId}`);
167
+ // Create WebSocket connection
168
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
169
+ const wsUrl = `${protocol}//${window.location.host}/terminal/ws`;
160
170
 
161
- this.eventSource.onopen = () => {
162
- this.updateStatus('Ready');
163
- terminalStore.updateTerminalStatus(this.terminalId, 'connected');
171
+ console.log(`Connecting to WebSocket: ${wsUrl}`);
172
+ this.websocket = new WebSocket(wsUrl);
173
+
174
+ this.websocket.onopen = () => {
175
+ console.log('WebSocket connected, joining terminal session');
176
+ this.reconnectAttempts = 0;
177
+ this.reconnectDelay = 1000;
178
+
179
+ // Join the terminal session
180
+ const joinMessage = {
181
+ type: 'join',
182
+ sessionId: this.sessionId,
183
+ terminalId: this.terminalId
184
+ };
185
+ this.websocket.send(JSON.stringify(joinMessage));
164
186
  };
165
187
 
166
- this.eventSource.onmessage = (event) => {
188
+ this.websocket.onmessage = (event) => {
167
189
  try {
168
190
  const data = JSON.parse(event.data);
191
+ console.log('Received WebSocket message:', data.type);
169
192
 
170
- if (data.type === 'output') {
171
- this.terminal.write(data.content);
172
- } else if (data.type === 'error') {
173
- this.terminal.write(`\x1b[31m${data.content}\x1b[0m`);
174
- } else if (data.type === 'exit') {
175
- this.terminal.writeln(`\x1b[90mProcess exited with code ${data.code}\x1b[0m`);
176
- this.terminal.write('\x1b[32m$ \x1b[0m');
177
- } else if (data.type === 'connected') {
178
- // Terminal connected - shell will show its own prompt
179
- console.log('Terminal connected');
193
+ switch (data.type) {
194
+ case 'connected':
195
+ this.updateStatus('Ready');
196
+ terminalStore.updateTerminalStatus(this.terminalId, 'connected');
197
+ console.log('Terminal session joined successfully');
198
+ // Dispatch global status for cross-tab sync
199
+ document.dispatchEvent(new CustomEvent('terminal:global_status', {
200
+ detail: { status: 'connected' }
201
+ }));
202
+ break;
203
+
204
+ case 'output':
205
+ if (data.content) {
206
+ this.terminal.write(data.content);
207
+ }
208
+ break;
209
+
210
+ case 'error':
211
+ if (data.content) {
212
+ this.terminal.write(`\x1b[31m${data.content}\x1b[0m`);
213
+ } else if (data.data) {
214
+ this.terminal.write(`\x1b[31m${data.data}\x1b[0m`);
215
+ }
216
+ break;
217
+
218
+ case 'exit':
219
+ this.terminal.writeln(`\x1b[90mProcess exited with code ${data.code || 0}\x1b[0m`);
220
+ this.terminal.write('\x1b[32m$ \x1b[0m');
221
+ break;
222
+
223
+ case 'clients_count':
224
+ console.log(`${data.count} clients connected to this session`);
225
+ // Dispatch event for TerminalSignal to handle cross-tab sync
226
+ document.dispatchEvent(new CustomEvent('terminal:clients_count', {
227
+ detail: {
228
+ sessionId: this.sessionId,
229
+ count: data.count
230
+ }
231
+ }));
232
+ break;
233
+
234
+ case 'session_closed':
235
+ console.log('Terminal session was closed');
236
+ this.updateStatus('Session Closed');
237
+ terminalStore.updateTerminalStatus(this.terminalId, 'closed');
238
+ // Dispatch event for TerminalSignal to handle cross-tab sync
239
+ document.dispatchEvent(new CustomEvent('terminal:session_closed', {
240
+ detail: {
241
+ sessionId: this.sessionId
242
+ }
243
+ }));
244
+ break;
245
+
246
+ case 'pong':
247
+ // Connection health check response
248
+ break;
249
+
250
+ default:
251
+ console.log('Unknown message type:', data.type, data);
180
252
  }
181
253
  } catch (error) {
182
- console.error('Error parsing terminal data:', error);
254
+ console.error('Error parsing WebSocket message:', error);
183
255
  }
184
256
  };
185
257
 
186
- this.eventSource.onerror = (error) => {
187
- console.error('Terminal stream error:', error);
258
+ this.websocket.onerror = (error) => {
259
+ console.error('WebSocket error:', error);
260
+ this.updateStatus('Connection Error');
261
+ terminalStore.updateTerminalStatus(this.terminalId, 'error');
262
+ };
263
+
264
+ this.websocket.onclose = (event) => {
265
+ console.log('WebSocket closed:', event.code, event.reason);
188
266
  this.updateStatus('Disconnected');
189
267
  terminalStore.updateTerminalStatus(this.terminalId, 'disconnected');
190
268
 
191
- // Attempt to reconnect after a delay
192
- setTimeout(() => {
193
- if (this.eventSource.readyState === EventSource.CLOSED) {
194
- terminalStore.updateTerminalStatus(this.terminalId, 'connecting');
269
+ // Attempt to reconnect with exponential backoff
270
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
271
+ this.reconnectAttempts++;
272
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${this.reconnectDelay}ms`);
273
+
274
+ setTimeout(() => {
195
275
  this.connectToTerminal();
196
- }
197
- }, 3000);
276
+ }, this.reconnectDelay);
277
+
278
+ // Exponential backoff: double the delay each time, max 10 seconds
279
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000);
280
+ } else {
281
+ console.error('Max reconnection attempts reached');
282
+ this.updateStatus('Connection Failed');
283
+ terminalStore.updateTerminalStatus(this.terminalId, 'failed');
284
+ }
198
285
  };
199
286
  }
200
287
 
@@ -213,32 +300,25 @@ class TerminalManager {
213
300
  }
214
301
 
215
302
  async destroy() {
216
- if (this.eventSource) {
217
- this.eventSource.close();
303
+ // Send leave message to WebSocket before closing
304
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
305
+ const leaveMessage = {
306
+ type: 'leave',
307
+ sessionId: this.sessionId,
308
+ terminalId: this.terminalId
309
+ };
310
+ this.websocket.send(JSON.stringify(leaveMessage));
311
+ }
312
+
313
+ if (this.websocket) {
314
+ this.websocket.close();
218
315
  }
219
316
  if (this.terminal) {
220
317
  this.terminal.dispose();
221
318
  }
222
319
 
223
- // Destroy server-side terminal session
224
- const sessionId = terminalStore.getSessionId(this.terminalId);
225
- if (sessionId) {
226
- try {
227
- const response = await fetch(`/terminal/session/${sessionId}`, {
228
- method: 'DELETE'
229
- });
230
-
231
- if (response.ok) {
232
- console.log(`Terminal session ${sessionId} destroyed on server`);
233
- } else {
234
- console.warn(`Failed to destroy terminal session ${sessionId} on server:`, await response.text());
235
- }
236
- } catch (error) {
237
- console.error(`Error destroying terminal session ${sessionId}:`, error);
238
- }
239
- } else {
240
- console.warn(`No sessionId found for terminal ${this.terminalId}`);
241
- }
320
+ // Note: We don't destroy the server-side session anymore since other tabs might be using it
321
+ // The WebSocket service will manage session cleanup when all clients disconnect
242
322
 
243
323
  // Remove from store
244
324
  terminalStore.removeTerminal(this.terminalId);
@@ -0,0 +1,123 @@
1
+ import { signal, computed } from 'https://esm.sh/@preact/signals@1.2.2';
2
+
3
+ // Sidebar state management
4
+ class SidebarStore {
5
+ constructor() {
6
+ // Core state signals
7
+ this.isOpen = signal(true); // Default to open on desktop
8
+ this.isMobile = signal(false); // Track if we're in mobile viewport
9
+ this.mobileBreakpoint = signal(768); // Breakpoint for mobile/desktop detection
10
+
11
+ // Computed values
12
+ this.shouldShowBackdrop = computed(() => this.isMobile.value && this.isOpen.value);
13
+ this.shouldShowMobileToggle = computed(() => this.isMobile.value && !this.isOpen.value);
14
+ this.shouldShowFloatingButton = computed(() => !this.isMobile.value && !this.isOpen.value);
15
+
16
+ // Initialize mobile detection
17
+ this.initializeMobileDetection();
18
+ }
19
+
20
+ // Initialize mobile detection and set initial state
21
+ initializeMobileDetection() {
22
+ if (typeof window !== 'undefined') {
23
+ this.updateMobileState();
24
+
25
+ // Listen for window resize
26
+ window.addEventListener('resize', () => {
27
+ this.updateMobileState();
28
+ });
29
+ }
30
+ }
31
+
32
+ // Update mobile state based on window width
33
+ updateMobileState() {
34
+ if (typeof window === 'undefined') return;
35
+
36
+ const wasMobile = this.isMobile.value;
37
+ const nowMobile = window.innerWidth < this.mobileBreakpoint.value;
38
+
39
+ this.isMobile.value = nowMobile;
40
+
41
+ // Handle initial state or breakpoint transitions
42
+ if (wasMobile === undefined) {
43
+ // First time: desktop shows sidebar, mobile hides it
44
+ this.isOpen.value = !nowMobile;
45
+ } else if (wasMobile !== nowMobile) {
46
+ // Breakpoint changed
47
+ if (wasMobile && !nowMobile) {
48
+ // Mobile to desktop: show sidebar
49
+ this.isOpen.value = true;
50
+ } else if (!wasMobile && nowMobile) {
51
+ // Desktop to mobile: hide sidebar
52
+ this.isOpen.value = false;
53
+ }
54
+ }
55
+ }
56
+
57
+ // Actions for managing sidebar state
58
+ toggle() {
59
+ this.isOpen.value = !this.isOpen.value;
60
+ console.log(`Sidebar toggled: ${this.isOpen.value ? 'open' : 'closed'}`);
61
+ }
62
+
63
+ open() {
64
+ this.isOpen.value = true;
65
+ console.log('Sidebar opened');
66
+ }
67
+
68
+ close() {
69
+ this.isOpen.value = false;
70
+ console.log('Sidebar closed');
71
+ }
72
+
73
+ // Set mobile breakpoint
74
+ setMobileBreakpoint(breakpoint) {
75
+ this.mobileBreakpoint.value = breakpoint;
76
+ this.updateMobileState();
77
+ }
78
+
79
+ // Get current state (useful for components that need to read state once)
80
+ getState() {
81
+ return {
82
+ isOpen: this.isOpen.value,
83
+ isMobile: this.isMobile.value,
84
+ mobileBreakpoint: this.mobileBreakpoint.value,
85
+ shouldShowBackdrop: this.shouldShowBackdrop.value,
86
+ shouldShowMobileToggle: this.shouldShowMobileToggle.value,
87
+ shouldShowFloatingButton: this.shouldShowFloatingButton.value
88
+ };
89
+ }
90
+
91
+ // Debug helpers
92
+ logState() {
93
+ console.log('Sidebar Store State:', this.getState());
94
+ }
95
+
96
+ // Subscribe to state changes (for components that need to react to changes)
97
+ onStateChange(callback) {
98
+ // Create an effect that runs when any sidebar state changes
99
+ const unsubscribe = () => {
100
+ // This is a simple implementation - in a full app you might want more granular subscriptions
101
+ const state = this.getState();
102
+ callback(state);
103
+ };
104
+
105
+ // Subscribe to all relevant signals
106
+ this.isOpen.subscribe(unsubscribe);
107
+ this.isMobile.subscribe(unsubscribe);
108
+
109
+ // Return unsubscribe function
110
+ return () => {
111
+ // Note: Preact signals don't have a direct unsubscribe method
112
+ // In a production app, you'd want to implement proper cleanup
113
+ };
114
+ }
115
+ }
116
+
117
+ // Create and export singleton instance
118
+ export const sidebarStore = new SidebarStore();
119
+
120
+ // Make it available globally for debugging
121
+ if (typeof window !== 'undefined') {
122
+ window.sidebarStore = sidebarStore;
123
+ }