@ytspar/devbar 1.0.0-canary.bf42899 → 1.0.0-canary.c511f13

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.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * DevBar Settings Persistence
3
+ *
4
+ * Handles saving and loading DevBar settings with Sweetlink server persistence
5
+ * and localStorage fallback.
6
+ */
7
+ // ============================================================================
8
+ // Default Settings
9
+ // ============================================================================
10
+ /** Default accent color (emerald) */
11
+ export const DEFAULT_ACCENT_COLOR = '#10b981';
12
+ /** Preset accent colors for the color picker */
13
+ export const ACCENT_COLOR_PRESETS = [
14
+ { name: 'Emerald', value: '#10b981' },
15
+ { name: 'Blue', value: '#3b82f6' },
16
+ { name: 'Purple', value: '#a855f7' },
17
+ { name: 'Pink', value: '#ec4899' },
18
+ { name: 'Amber', value: '#f59e0b' },
19
+ { name: 'Cyan', value: '#06b6d4' },
20
+ ];
21
+ /**
22
+ * Default settings used when no saved settings exist
23
+ */
24
+ export const DEFAULT_SETTINGS = {
25
+ version: 1,
26
+ // Display
27
+ position: 'bottom-left',
28
+ themeMode: 'system',
29
+ compactMode: false,
30
+ accentColor: DEFAULT_ACCENT_COLOR,
31
+ // Features
32
+ showScreenshot: true,
33
+ showConsoleBadges: true,
34
+ showTooltips: true,
35
+ // Metrics visibility
36
+ showMetrics: {
37
+ breakpoint: true,
38
+ fcp: true,
39
+ lcp: true,
40
+ cls: true,
41
+ inp: true,
42
+ pageSize: true,
43
+ },
44
+ // Debug
45
+ debug: false,
46
+ };
47
+ // ============================================================================
48
+ // Storage Keys
49
+ // ============================================================================
50
+ /** LocalStorage key for DevBar settings fallback */
51
+ export const SETTINGS_STORAGE_KEY = 'devbar-settings';
52
+ /**
53
+ * SettingsManager handles loading and saving DevBar settings.
54
+ *
55
+ * Storage priority:
56
+ * 1. Sweetlink server (.devbar/settings.json) when connected
57
+ * 2. localStorage fallback when disconnected
58
+ *
59
+ * Settings are always saved to localStorage as a backup, ensuring
60
+ * settings persist even when Sweetlink is unavailable.
61
+ */
62
+ export class SettingsManager {
63
+ constructor() {
64
+ this.ws = null;
65
+ this.sweetlinkConnected = false;
66
+ this.changeCallbacks = [];
67
+ this.saveTimeout = null;
68
+ this.pendingLoadResolvers = [];
69
+ this.settings = { ...DEFAULT_SETTINGS };
70
+ }
71
+ /**
72
+ * Set the WebSocket connection for Sweetlink communication
73
+ */
74
+ setWebSocket(ws) {
75
+ this.ws = ws;
76
+ this.sweetlinkConnected = ws !== null && ws.readyState === WebSocket.OPEN;
77
+ }
78
+ /**
79
+ * Update connection status (called when WebSocket connects/disconnects)
80
+ */
81
+ setConnected(connected) {
82
+ this.sweetlinkConnected = connected;
83
+ }
84
+ /**
85
+ * Get current settings (synchronous)
86
+ */
87
+ getSettings() {
88
+ return { ...this.settings };
89
+ }
90
+ /**
91
+ * Get a specific setting value
92
+ */
93
+ get(key) {
94
+ return this.settings[key];
95
+ }
96
+ /**
97
+ * Load settings from storage
98
+ *
99
+ * When Sweetlink is connected, requests settings from the server.
100
+ * Otherwise, loads from localStorage.
101
+ */
102
+ async loadSettings() {
103
+ // Always start with localStorage to have immediate values
104
+ const localSettings = this.loadFromLocalStorage();
105
+ this.settings = localSettings;
106
+ // If Sweetlink is connected, request server settings
107
+ if (this.sweetlinkConnected && this.ws && this.ws.readyState === WebSocket.OPEN) {
108
+ try {
109
+ const serverSettings = await this.loadFromServer();
110
+ if (serverSettings) {
111
+ this.settings = serverSettings;
112
+ // Sync to localStorage as backup
113
+ this.saveToLocalStorage(this.settings);
114
+ }
115
+ }
116
+ catch (error) {
117
+ console.warn('[DevBar] Failed to load settings from server, using localStorage:', error);
118
+ }
119
+ }
120
+ return this.settings;
121
+ }
122
+ /**
123
+ * Handle settings loaded from server (called by WebSocket message handler)
124
+ */
125
+ handleSettingsLoaded(settings) {
126
+ if (settings) {
127
+ this.settings = this.migrateSettings(settings);
128
+ this.saveToLocalStorage(this.settings);
129
+ this.notifyChange();
130
+ }
131
+ // Resolve any pending load promises
132
+ const resolvers = this.pendingLoadResolvers;
133
+ this.pendingLoadResolvers = [];
134
+ for (const resolve of resolvers) {
135
+ resolve(this.settings);
136
+ }
137
+ }
138
+ /**
139
+ * Save settings (debounced)
140
+ *
141
+ * Saves to both Sweetlink server (if connected) and localStorage.
142
+ */
143
+ saveSettings(partial) {
144
+ // Merge with current settings
145
+ this.settings = { ...this.settings, ...partial };
146
+ // Debounce saves to avoid excessive writes
147
+ if (this.saveTimeout) {
148
+ clearTimeout(this.saveTimeout);
149
+ }
150
+ this.saveTimeout = setTimeout(() => {
151
+ this.saveTimeout = null;
152
+ this.performSave();
153
+ }, SettingsManager.SAVE_DEBOUNCE_MS);
154
+ }
155
+ /**
156
+ * Save settings immediately without debouncing
157
+ */
158
+ saveSettingsNow(partial) {
159
+ if (this.saveTimeout) {
160
+ clearTimeout(this.saveTimeout);
161
+ this.saveTimeout = null;
162
+ }
163
+ this.settings = { ...this.settings, ...partial };
164
+ this.performSave();
165
+ }
166
+ /**
167
+ * Reset settings to defaults
168
+ */
169
+ resetToDefaults() {
170
+ this.settings = { ...DEFAULT_SETTINGS };
171
+ this.performSave();
172
+ this.notifyChange();
173
+ }
174
+ /**
175
+ * Subscribe to settings changes
176
+ */
177
+ onChange(callback) {
178
+ this.changeCallbacks.push(callback);
179
+ return () => {
180
+ this.changeCallbacks = this.changeCallbacks.filter((cb) => cb !== callback);
181
+ };
182
+ }
183
+ // ============================================================================
184
+ // Private Methods
185
+ // ============================================================================
186
+ performSave() {
187
+ // Always save to localStorage as backup
188
+ this.saveToLocalStorage(this.settings);
189
+ // Save to server if connected (saveToServer checks readyState internally)
190
+ if (this.sweetlinkConnected) {
191
+ this.saveToServer(this.settings);
192
+ }
193
+ this.notifyChange();
194
+ }
195
+ notifyChange() {
196
+ for (const callback of this.changeCallbacks) {
197
+ try {
198
+ callback(this.settings);
199
+ }
200
+ catch (error) {
201
+ console.error('[DevBar] Settings change callback error:', error);
202
+ }
203
+ }
204
+ }
205
+ loadFromLocalStorage() {
206
+ if (typeof localStorage === 'undefined') {
207
+ return { ...DEFAULT_SETTINGS };
208
+ }
209
+ try {
210
+ const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
211
+ if (!stored) {
212
+ return { ...DEFAULT_SETTINGS };
213
+ }
214
+ const parsed = JSON.parse(stored);
215
+ return this.migrateSettings(parsed);
216
+ }
217
+ catch (error) {
218
+ console.warn('[DevBar] Failed to parse localStorage settings:', error);
219
+ return { ...DEFAULT_SETTINGS };
220
+ }
221
+ }
222
+ saveToLocalStorage(settings) {
223
+ if (typeof localStorage === 'undefined')
224
+ return;
225
+ try {
226
+ localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
227
+ }
228
+ catch (error) {
229
+ console.warn('[DevBar] Failed to save settings to localStorage:', error);
230
+ }
231
+ }
232
+ async loadFromServer() {
233
+ return new Promise((resolve) => {
234
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
235
+ resolve(null);
236
+ return;
237
+ }
238
+ // Store resolver to be called when settings-loaded message arrives
239
+ this.pendingLoadResolvers.push(resolve);
240
+ // Request settings from server
241
+ this.ws.send(JSON.stringify({ type: 'load-settings' }));
242
+ // Timeout after 5 seconds
243
+ setTimeout(() => {
244
+ const index = this.pendingLoadResolvers.indexOf(resolve);
245
+ if (index !== -1) {
246
+ this.pendingLoadResolvers.splice(index, 1);
247
+ resolve(null);
248
+ }
249
+ }, 5000);
250
+ });
251
+ }
252
+ saveToServer(settings) {
253
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
254
+ return;
255
+ this.ws.send(JSON.stringify({
256
+ type: 'save-settings',
257
+ data: { settings },
258
+ }));
259
+ }
260
+ /**
261
+ * Migrate settings from older versions and fill in missing defaults
262
+ */
263
+ migrateSettings(partial) {
264
+ // Merge partial settings over defaults, then handle nested objects specially
265
+ return {
266
+ ...DEFAULT_SETTINGS,
267
+ ...partial,
268
+ // Always use current schema version
269
+ version: 1,
270
+ // Deep merge showMetrics to preserve unset defaults
271
+ showMetrics: {
272
+ ...DEFAULT_SETTINGS.showMetrics,
273
+ ...partial.showMetrics,
274
+ },
275
+ };
276
+ }
277
+ }
278
+ /** Debounce delay for saving settings (ms) */
279
+ SettingsManager.SAVE_DEBOUNCE_MS = 300;
280
+ /**
281
+ * Singleton settings manager instance
282
+ */
283
+ let settingsManagerInstance = null;
284
+ /**
285
+ * Get the singleton SettingsManager instance
286
+ */
287
+ export function getSettingsManager() {
288
+ if (!settingsManagerInstance) {
289
+ settingsManagerInstance = new SettingsManager();
290
+ }
291
+ return settingsManagerInstance;
292
+ }
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * Re-exports all UI utilities.
5
5
  */
6
+ export { createStyledButton, getButtonStyles } from './buttons.js';
6
7
  export { createSvgIcon } from './icons.js';
7
- export { getButtonStyles, createStyledButton } from './buttons.js';
8
- export { createModalOverlay, createModalBox, createModalHeader, createModalContent, createEmptyMessage, createInfoBox, type ModalConfig } from './modals.js';
8
+ export { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, type ModalConfig, } from './modals.js';
package/dist/ui/index.js CHANGED
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * Re-exports all UI utilities.
5
5
  */
6
+ export { createStyledButton, getButtonStyles } from './buttons.js';
6
7
  export { createSvgIcon } from './icons.js';
7
- export { getButtonStyles, createStyledButton } from './buttons.js';
8
- export { createModalOverlay, createModalBox, createModalHeader, createModalContent, createEmptyMessage, createInfoBox } from './modals.js';
8
+ export { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, } from './modals.js';
@@ -13,6 +13,10 @@ export interface ModalConfig {
13
13
  onCopyMd: () => Promise<void>;
14
14
  onSave?: () => void;
15
15
  sweetlinkConnected: boolean;
16
+ /** Whether a save operation is in progress */
17
+ isSaving?: boolean;
18
+ /** Path where data was saved (shows confirmation) */
19
+ savedPath?: string | null;
16
20
  }
17
21
  /**
18
22
  * Create modal overlay with click-outside-to-close behavior
package/dist/ui/modals.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Modal creation utilities for the DevBar UI.
5
5
  */
6
- import { MODAL_OVERLAY_STYLES, MODAL_BOX_BASE_STYLES } from '../constants.js';
6
+ import { MODAL_BOX_BASE_STYLES, MODAL_OVERLAY_STYLES } from '../constants.js';
7
7
  import { createStyledButton } from './buttons.js';
8
8
  /**
9
9
  * Create modal overlay with click-outside-to-close behavior
@@ -34,7 +34,7 @@ export function createModalBox(color) {
34
34
  * Create modal header with title, copy/save/close buttons
35
35
  */
36
36
  export function createModalHeader(config) {
37
- const { color, title, onClose, onCopyMd, onSave, sweetlinkConnected } = config;
37
+ const { color, title, onClose, onCopyMd, onSave, sweetlinkConnected, isSaving, savedPath } = config;
38
38
  const header = document.createElement('div');
39
39
  Object.assign(header.style, {
40
40
  display: 'flex',
@@ -42,6 +42,8 @@ export function createModalHeader(config) {
42
42
  justifyContent: 'space-between',
43
43
  padding: '16px 20px',
44
44
  borderBottom: `1px solid ${color}40`,
45
+ flexWrap: 'wrap',
46
+ gap: '8px',
45
47
  });
46
48
  const titleEl = document.createElement('h2');
47
49
  Object.assign(titleEl.style, {
@@ -53,14 +55,16 @@ export function createModalHeader(config) {
53
55
  titleEl.textContent = title;
54
56
  header.appendChild(titleEl);
55
57
  const headerButtons = document.createElement('div');
56
- Object.assign(headerButtons.style, { display: 'flex', gap: '10px' });
58
+ Object.assign(headerButtons.style, { display: 'flex', gap: '10px', alignItems: 'center' });
57
59
  // Copy MD button
58
60
  const copyBtn = createStyledButton({ color, text: 'Copy MD' });
59
61
  copyBtn.onclick = async () => {
60
62
  try {
61
63
  await onCopyMd();
62
64
  copyBtn.textContent = 'Copied!';
63
- setTimeout(() => { copyBtn.textContent = 'Copy MD'; }, 1500);
65
+ setTimeout(() => {
66
+ copyBtn.textContent = 'Copy MD';
67
+ }, 1500);
64
68
  }
65
69
  catch {
66
70
  console.error('[GlobalDevBar] Failed to copy to clipboard');
@@ -69,8 +73,17 @@ export function createModalHeader(config) {
69
73
  headerButtons.appendChild(copyBtn);
70
74
  // Save button (if Sweetlink connected)
71
75
  if (sweetlinkConnected && onSave) {
72
- const saveBtn = createStyledButton({ color, text: 'Save' });
73
- saveBtn.onclick = onSave;
76
+ const saveBtn = createStyledButton({
77
+ color,
78
+ text: isSaving ? 'Saving...' : 'Save',
79
+ });
80
+ if (isSaving) {
81
+ saveBtn.style.opacity = '0.6';
82
+ saveBtn.style.cursor = 'not-allowed';
83
+ }
84
+ else {
85
+ saveBtn.onclick = onSave;
86
+ }
74
87
  headerButtons.appendChild(saveBtn);
75
88
  }
76
89
  // Close button - use same padding as other buttons for consistent height
@@ -83,6 +96,39 @@ export function createModalHeader(config) {
83
96
  closeBtn.onclick = onClose;
84
97
  headerButtons.appendChild(closeBtn);
85
98
  header.appendChild(headerButtons);
99
+ // Show saved path confirmation below buttons
100
+ if (savedPath) {
101
+ const savedConfirm = document.createElement('div');
102
+ Object.assign(savedConfirm.style, {
103
+ width: '100%',
104
+ marginTop: '4px',
105
+ padding: '8px 12px',
106
+ backgroundColor: `${color}15`,
107
+ border: `1px solid ${color}30`,
108
+ borderRadius: '6px',
109
+ fontSize: '0.75rem',
110
+ color: color,
111
+ display: 'flex',
112
+ alignItems: 'center',
113
+ gap: '6px',
114
+ });
115
+ // Checkmark icon
116
+ const checkmark = document.createElement('span');
117
+ checkmark.textContent = '✓';
118
+ Object.assign(checkmark.style, { fontWeight: '600' });
119
+ savedConfirm.appendChild(checkmark);
120
+ // Path text
121
+ const pathText = document.createElement('span');
122
+ Object.assign(pathText.style, {
123
+ color: '#9ca3af',
124
+ fontFamily: 'monospace',
125
+ fontSize: '0.6875rem',
126
+ wordBreak: 'break-all',
127
+ });
128
+ pathText.textContent = `Saved to ${savedPath}`;
129
+ savedConfirm.appendChild(pathText);
130
+ header.appendChild(savedConfirm);
131
+ }
86
132
  return header;
87
133
  }
88
134
  /**
@@ -138,7 +184,7 @@ export function createInfoBox(color, title, content) {
138
184
  box.appendChild(textEl);
139
185
  }
140
186
  else {
141
- content.forEach(el => box.appendChild(el));
187
+ content.forEach((el) => box.appendChild(el));
142
188
  }
143
189
  return box;
144
190
  }
package/dist/utils.d.ts CHANGED
@@ -8,4 +8,4 @@
8
8
  * that would break browser/test environments.
9
9
  */
10
10
  export { formatArg, formatArgs } from '@ytspar/sweetlink/browser/consoleCapture';
11
- export { canvasToDataUrl, prepareForCapture, delay, copyCanvasToClipboard, } from '@ytspar/sweetlink/browser/screenshotUtils';
11
+ export { canvasToDataUrl, copyCanvasToClipboard, delay, prepareForCapture, } from '@ytspar/sweetlink/browser/screenshotUtils';
package/dist/utils.js CHANGED
@@ -10,4 +10,4 @@
10
10
  // Re-export console formatting utilities from sweetlink's browser module
11
11
  export { formatArg, formatArgs } from '@ytspar/sweetlink/browser/consoleCapture';
12
12
  // Re-export screenshot utilities from sweetlink's browser module
13
- export { canvasToDataUrl, prepareForCapture, delay, copyCanvasToClipboard, } from '@ytspar/sweetlink/browser/screenshotUtils';
13
+ export { canvasToDataUrl, copyCanvasToClipboard, delay, prepareForCapture, } from '@ytspar/sweetlink/browser/screenshotUtils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ytspar/devbar",
3
- "version": "1.0.0-canary.bf42899",
3
+ "version": "1.0.0-canary.c511f13",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Development toolbar and utilities with Sweetlink integration - pure vanilla JS, no framework dependencies",