@ytspar/devbar 1.0.0-canary.3c85c90 → 1.0.0-canary.4b73445

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.
@@ -10,8 +10,8 @@
10
10
  import { getThemeColors } from './constants.js';
11
11
  import type { ConsoleLog, DebugConfig, DevBarControl, GlobalDevBarOptions, OutlineNode, PageSchema, SweetlinkCommand, ThemeMode } from './types.js';
12
12
  export type { ConsoleLog, DebugConfig, SweetlinkCommand, OutlineNode, PageSchema, GlobalDevBarOptions, DevBarControl, ThemeMode, };
13
- export type { DevBarSettings, DevBarPosition, MetricsVisibility } from './settings.js';
14
- export { DEFAULT_SETTINGS, ACCENT_COLOR_PRESETS, getSettingsManager } from './settings.js';
13
+ export type { DevBarPosition, DevBarSettings, MetricsVisibility } from './settings.js';
14
+ export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
15
15
  interface EarlyConsoleCapture {
16
16
  errorCount: number;
17
17
  warningCount: number;
@@ -46,6 +46,8 @@ export declare class GlobalDevBar {
46
46
  private apiKeyStatus;
47
47
  private lastOutline;
48
48
  private lastSchema;
49
+ private savingOutline;
50
+ private savingSchema;
49
51
  private consoleFilter;
50
52
  private showOutlineModal;
51
53
  private showSchemaModal;
@@ -55,6 +57,10 @@ export declare class GlobalDevBar {
55
57
  private clsValue;
56
58
  private inpValue;
57
59
  private reconnectAttempts;
60
+ private readonly currentAppPort;
61
+ private readonly baseWsPort;
62
+ private wsVerified;
63
+ private serverProjectDir;
58
64
  private lastDotPosition;
59
65
  private reconnectTimeout;
60
66
  private screenshotTimeout;
@@ -231,6 +237,10 @@ export declare class GlobalDevBar {
231
237
  * Create the settings gear button
232
238
  */
233
239
  private createSettingsButton;
240
+ /**
241
+ * Create the compact mode toggle button with chevron icon
242
+ */
243
+ private createCompactToggleButton;
234
244
  /**
235
245
  * Create a settings section with title
236
246
  */
@@ -249,6 +259,30 @@ export declare class GlobalDevBar {
249
259
  private resetToDefaults;
250
260
  private renderCollapsed;
251
261
  private renderExpanded;
262
+ /** Base styles for tooltip containers */
263
+ private readonly TOOLTIP_BASE_STYLES;
264
+ /** Create a tooltip container element */
265
+ private createTooltipContainer;
266
+ /** Add a bold title to tooltip (metric name, feature name, etc.) */
267
+ private addTooltipTitle;
268
+ /** Add a description paragraph to tooltip */
269
+ private addTooltipDescription;
270
+ /** Add a muted uppercase section header to tooltip */
271
+ private addTooltipSectionHeader;
272
+ /** Add a colored row with dot + label + value (for thresholds) */
273
+ private addTooltipColoredRow;
274
+ /** Add an info row with label + value (for breakpoint details) */
275
+ private addTooltipInfoRow;
276
+ /** Position tooltip above the anchor element, adjusting for screen edges */
277
+ private positionTooltip;
278
+ /** Attach an HTML tooltip to an element with custom content builder */
279
+ private attachHtmlTooltip;
280
+ /** Attach a metric tooltip with title, description, and colored thresholds */
281
+ private attachMetricTooltip;
282
+ /** Attach a breakpoint tooltip showing current breakpoint and all breakpoint ranges */
283
+ private attachBreakpointTooltip;
284
+ /** Attach a simple info tooltip with title and description */
285
+ private attachInfoTooltip;
252
286
  /**
253
287
  * Create a console badge for error/warning counts
254
288
  */
@@ -8,14 +8,14 @@
8
8
  * to avoid React dependency conflicts in host applications.
9
9
  */
10
10
  import * as html2canvasModule from 'html2canvas-pro';
11
- import { BASE_RECONNECT_DELAY_MS, BUTTON_COLORS, CATEGORY_COLORS, CLIPBOARD_NOTIFICATION_MS, COLORS, DESIGN_REVIEW_NOTIFICATION_MS, DEVBAR_SCREENSHOT_QUALITY, FONT_MONO, getEffectiveTheme, getThemeColors, MAX_CONSOLE_LOGS, MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MS, SCREENSHOT_BLUR_DELAY_MS, SCREENSHOT_NOTIFICATION_MS, SCREENSHOT_SCALE, TAILWIND_BREAKPOINTS, TOOLTIP_STYLES, WS_PORT, } from './constants.js';
11
+ import { BASE_RECONNECT_DELAY_MS, BUTTON_COLORS, CATEGORY_COLORS, CLIPBOARD_NOTIFICATION_MS, COLORS, DESIGN_REVIEW_NOTIFICATION_MS, DEVBAR_SCREENSHOT_QUALITY, FONT_MONO, getEffectiveTheme, getTheme, getThemeColors, injectThemeCSS, MAX_CONSOLE_LOGS, MAX_PORT_RETRIES, MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MS, PORT_RETRY_DELAY_MS, PORT_SCAN_RESTART_DELAY_MS, SCREENSHOT_BLUR_DELAY_MS, SCREENSHOT_NOTIFICATION_MS, SCREENSHOT_SCALE, TAILWIND_BREAKPOINTS, TOOLTIP_STYLES, WS_PORT, WS_PORT_OFFSET, } from './constants.js';
12
12
  import { DebugLogger, normalizeDebugConfig } from './debug.js';
13
13
  import { extractDocumentOutline, outlineToMarkdown } from './outline.js';
14
14
  import { extractPageSchema, schemaToMarkdown } from './schema.js';
15
15
  import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager, } from './settings.js';
16
16
  import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, createSvgIcon, getButtonStyles, } from './ui/index.js';
17
17
  import { canvasToDataUrl, copyCanvasToClipboard, delay, formatArgs, prepareForCapture, } from './utils.js';
18
- export { DEFAULT_SETTINGS, ACCENT_COLOR_PRESETS, getSettingsManager } from './settings.js';
18
+ export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
19
19
  const html2canvas = (html2canvasModule.default ??
20
20
  html2canvasModule);
21
21
  const earlyConsoleCapture = (() => {
@@ -91,6 +91,8 @@ export class GlobalDevBar {
91
91
  this.apiKeyStatus = null;
92
92
  this.lastOutline = null;
93
93
  this.lastSchema = null;
94
+ this.savingOutline = false;
95
+ this.savingSchema = false;
94
96
  this.consoleFilter = null;
95
97
  // Modal states
96
98
  this.showOutlineModal = false;
@@ -101,6 +103,8 @@ export class GlobalDevBar {
101
103
  this.clsValue = 0;
102
104
  this.inpValue = 0;
103
105
  this.reconnectAttempts = 0;
106
+ this.wsVerified = false;
107
+ this.serverProjectDir = null;
104
108
  // Track the position of the connection indicator dot for smooth collapse
105
109
  this.lastDotPosition = null;
106
110
  this.reconnectTimeout = null;
@@ -127,11 +131,39 @@ export class GlobalDevBar {
127
131
  this.showSettingsPopover = false;
128
132
  // Overlay element for modals
129
133
  this.overlayElement = null;
134
+ // ============================================================================
135
+ // Tooltip Helpers (DRY system for HTML tooltips)
136
+ // ============================================================================
137
+ /** Base styles for tooltip containers */
138
+ this.TOOLTIP_BASE_STYLES = {
139
+ position: 'fixed',
140
+ zIndex: '10004',
141
+ backgroundColor: 'rgba(17, 24, 39, 0.98)',
142
+ border: `1px solid ${COLORS.border}`,
143
+ borderRadius: '6px',
144
+ padding: '10px 12px',
145
+ fontSize: '0.6875rem',
146
+ fontFamily: FONT_MONO,
147
+ maxWidth: '280px',
148
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
149
+ pointerEvents: 'none',
150
+ };
130
151
  // Initialize debug config first so we can log during construction
131
152
  this.debugConfig = normalizeDebugConfig(options.debug);
132
153
  this.debug = new DebugLogger(this.debugConfig);
133
154
  // Initialize settings manager
134
155
  this.settingsManager = getSettingsManager();
156
+ // Calculate app port from URL for multi-instance support
157
+ if (typeof window !== 'undefined') {
158
+ this.currentAppPort =
159
+ parseInt(window.location.port, 10) || (window.location.protocol === 'https:' ? 443 : 80);
160
+ // Calculate expected WS port (appPort + port offset) like SweetlinkBridge does
161
+ this.baseWsPort = this.currentAppPort > 0 ? this.currentAppPort + WS_PORT_OFFSET : WS_PORT;
162
+ }
163
+ else {
164
+ this.currentAppPort = 0;
165
+ this.baseWsPort = WS_PORT;
166
+ }
135
167
  this.options = {
136
168
  position: options.position ?? 'bottom-left',
137
169
  accentColor: options.accentColor ?? COLORS.primary,
@@ -345,27 +377,64 @@ export class GlobalDevBar {
345
377
  document.head.appendChild(style);
346
378
  }
347
379
  }
348
- connectWebSocket() {
380
+ connectWebSocket(port) {
349
381
  if (this.destroyed)
350
382
  return;
351
- this.debug.ws('Connecting to WebSocket', { port: WS_PORT });
352
- const ws = new WebSocket(`ws://localhost:${WS_PORT}`);
383
+ const targetPort = port ?? this.baseWsPort;
384
+ this.debug.ws('Connecting to WebSocket', { port: targetPort, appPort: this.currentAppPort });
385
+ const ws = new WebSocket(`ws://localhost:${targetPort}`);
353
386
  this.ws = ws;
387
+ this.wsVerified = false;
354
388
  ws.onopen = () => {
355
- this.sweetlinkConnected = true;
356
- this.reconnectAttempts = 0;
357
- this.debug.ws('WebSocket connected');
358
- // Update settings manager with WebSocket connection
359
- this.settingsManager.setWebSocket(ws);
360
- this.settingsManager.setConnected(true);
389
+ this.debug.ws('WebSocket socket opened, awaiting server-info');
361
390
  ws.send(JSON.stringify({ type: 'browser-client-ready' }));
362
- // Request settings from server
363
- ws.send(JSON.stringify({ type: 'load-settings' }));
364
- this.render();
365
391
  };
366
392
  ws.onmessage = async (event) => {
367
393
  try {
368
- const command = JSON.parse(event.data);
394
+ const message = JSON.parse(event.data);
395
+ // Handle server-info for port matching
396
+ if (message.type === 'server-info') {
397
+ const serverAppPort = message.appPort;
398
+ const serverMatchesApp = serverAppPort === null || serverAppPort === this.currentAppPort;
399
+ if (!serverMatchesApp) {
400
+ this.debug.ws('Server mismatch', {
401
+ serverAppPort,
402
+ currentAppPort: this.currentAppPort,
403
+ tryingNextPort: targetPort + 1,
404
+ });
405
+ ws.close();
406
+ // Try next port
407
+ const nextPort = targetPort + 1;
408
+ if (nextPort < this.baseWsPort + MAX_PORT_RETRIES) {
409
+ setTimeout(() => this.connectWebSocket(nextPort), PORT_RETRY_DELAY_MS);
410
+ }
411
+ else {
412
+ this.debug.ws('No matching server found, will retry from base port');
413
+ setTimeout(() => this.connectWebSocket(this.baseWsPort), PORT_SCAN_RESTART_DELAY_MS);
414
+ }
415
+ return;
416
+ }
417
+ // Server matches - mark as verified and connected
418
+ this.wsVerified = true;
419
+ this.sweetlinkConnected = true;
420
+ this.reconnectAttempts = 0;
421
+ this.serverProjectDir = message.projectDir ?? null;
422
+ this.debug.ws('Server verified', {
423
+ appPort: serverAppPort ?? 'any',
424
+ projectDir: this.serverProjectDir,
425
+ });
426
+ this.settingsManager.setWebSocket(ws);
427
+ this.settingsManager.setConnected(true);
428
+ ws.send(JSON.stringify({ type: 'load-settings' }));
429
+ this.render();
430
+ return;
431
+ }
432
+ // Ignore other commands until verified
433
+ if (!this.wsVerified) {
434
+ this.debug.ws('Ignoring command before verification', { type: message.type });
435
+ return;
436
+ }
437
+ const command = message;
369
438
  this.debug.ws('Received command', { type: command.type });
370
439
  await this.handleSweetlinkCommand(command);
371
440
  }
@@ -374,16 +443,21 @@ export class GlobalDevBar {
374
443
  }
375
444
  };
376
445
  ws.onclose = () => {
377
- this.sweetlinkConnected = false;
378
- this.settingsManager.setConnected(false);
379
- this.debug.ws('WebSocket disconnected');
380
- this.render();
381
- // Auto-reconnect with exponential backoff
382
- if (!this.destroyed && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
383
- const delayMs = BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempts;
384
- this.reconnectAttempts++;
385
- this.debug.ws('Scheduling reconnect', { attempt: this.reconnectAttempts, delayMs });
386
- this.reconnectTimeout = setTimeout(() => this.connectWebSocket(), Math.min(delayMs, MAX_RECONNECT_DELAY_MS));
446
+ // Only reset connection state if we were actually verified/connected
447
+ if (this.wsVerified) {
448
+ this.sweetlinkConnected = false;
449
+ this.wsVerified = false;
450
+ this.serverProjectDir = null;
451
+ this.settingsManager.setConnected(false);
452
+ this.debug.ws('WebSocket disconnected');
453
+ this.render();
454
+ // Auto-reconnect with exponential backoff (start from base port)
455
+ if (!this.destroyed && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
456
+ const delayMs = BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempts;
457
+ this.reconnectAttempts++;
458
+ this.debug.ws('Scheduling reconnect', { attempt: this.reconnectAttempts, delayMs });
459
+ this.reconnectTimeout = setTimeout(() => this.connectWebSocket(this.baseWsPort), Math.min(delayMs, MAX_RECONNECT_DELAY_MS));
460
+ }
387
461
  }
388
462
  };
389
463
  ws.onerror = () => {
@@ -579,6 +653,7 @@ export class GlobalDevBar {
579
653
  }, durationMs);
580
654
  break;
581
655
  case 'outline':
656
+ this.savingOutline = false;
582
657
  this.lastOutline = path;
583
658
  if (this.outlineTimeout)
584
659
  clearTimeout(this.outlineTimeout);
@@ -588,6 +663,7 @@ export class GlobalDevBar {
588
663
  }, durationMs);
589
664
  break;
590
665
  case 'schema':
666
+ this.savingSchema = false;
591
667
  this.lastSchema = path;
592
668
  if (this.schemaTimeout)
593
669
  clearTimeout(this.schemaTimeout);
@@ -720,7 +796,11 @@ export class GlobalDevBar {
720
796
  }
721
797
  });
722
798
  // durationThreshold filters out very short interactions
723
- this.inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 16 });
799
+ this.inpObserver.observe({
800
+ type: 'event',
801
+ buffered: true,
802
+ durationThreshold: 16,
803
+ });
724
804
  }
725
805
  catch (e) {
726
806
  console.warn('[GlobalDevBar] INP PerformanceObserver not supported', e);
@@ -777,12 +857,16 @@ export class GlobalDevBar {
777
857
  // Load stored theme preference from settings manager
778
858
  const settings = this.settingsManager.getSettings();
779
859
  this.themeMode = settings.themeMode;
860
+ // Inject the appropriate theme CSS variables on initial load
861
+ injectThemeCSS(getTheme(this.themeMode));
780
862
  this.debug.state('Theme loaded', { mode: this.themeMode });
781
863
  // Listen for system theme changes
782
864
  if (typeof window !== 'undefined' && window.matchMedia) {
783
865
  this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
784
866
  this.themeMediaHandler = () => {
785
867
  if (this.themeMode === 'system') {
868
+ // Re-inject theme CSS when system preference changes
869
+ injectThemeCSS(getTheme(this.themeMode));
786
870
  this.debug.state('System theme changed', {
787
871
  effectiveTheme: getEffectiveTheme(this.themeMode),
788
872
  });
@@ -809,6 +893,8 @@ export class GlobalDevBar {
809
893
  setThemeMode(mode) {
810
894
  this.themeMode = mode;
811
895
  this.settingsManager.saveSettings({ themeMode: mode });
896
+ // Inject the appropriate theme CSS variables
897
+ injectThemeCSS(getTheme(mode));
812
898
  this.debug.state('Theme mode changed', { mode, effectiveTheme: getEffectiveTheme(mode) });
813
899
  this.render();
814
900
  }
@@ -1062,9 +1148,13 @@ export class GlobalDevBar {
1062
1148
  this.render();
1063
1149
  }
1064
1150
  handleSaveOutline() {
1151
+ if (this.savingOutline)
1152
+ return; // Prevent repeated clicks
1065
1153
  const outline = extractDocumentOutline();
1066
1154
  const markdown = outlineToMarkdown(outline);
1067
1155
  if (this.ws?.readyState === WebSocket.OPEN) {
1156
+ this.savingOutline = true;
1157
+ this.render();
1068
1158
  this.ws.send(JSON.stringify({
1069
1159
  type: 'save-outline',
1070
1160
  data: {
@@ -1078,9 +1168,13 @@ export class GlobalDevBar {
1078
1168
  }
1079
1169
  }
1080
1170
  handleSaveSchema() {
1171
+ if (this.savingSchema)
1172
+ return; // Prevent repeated clicks
1081
1173
  const schema = extractPageSchema();
1082
1174
  const markdown = schemaToMarkdown(schema);
1083
1175
  if (this.ws?.readyState === WebSocket.OPEN) {
1176
+ this.savingSchema = true;
1177
+ this.render();
1084
1178
  this.ws.send(JSON.stringify({
1085
1179
  type: 'save-schema',
1086
1180
  data: {
@@ -1458,6 +1552,8 @@ export class GlobalDevBar {
1458
1552
  },
1459
1553
  onSave: () => this.handleSaveOutline(),
1460
1554
  sweetlinkConnected: this.sweetlinkConnected,
1555
+ isSaving: this.savingOutline,
1556
+ savedPath: this.lastOutline,
1461
1557
  });
1462
1558
  modal.appendChild(header);
1463
1559
  const content = createModalContent();
@@ -1544,6 +1640,8 @@ export class GlobalDevBar {
1544
1640
  },
1545
1641
  onSave: () => this.handleSaveSchema(),
1546
1642
  sweetlinkConnected: this.sweetlinkConnected,
1643
+ isSaving: this.savingSchema,
1644
+ savedPath: this.lastSchema,
1547
1645
  });
1548
1646
  modal.appendChild(header);
1549
1647
  const content = createModalContent();
@@ -1901,6 +1999,64 @@ export class GlobalDevBar {
1901
1999
  btn.appendChild(svg);
1902
2000
  return btn;
1903
2001
  }
2002
+ /**
2003
+ * Create the compact mode toggle button with chevron icon
2004
+ */
2005
+ createCompactToggleButton() {
2006
+ const btn = document.createElement('button');
2007
+ btn.type = 'button';
2008
+ btn.className = this.tooltipClass('right');
2009
+ const isCompact = this.compactMode;
2010
+ const tooltip = isCompact ? 'Expand (Cmd+Shift+M)' : 'Compact (Cmd+Shift+M)';
2011
+ btn.setAttribute('data-tooltip', tooltip);
2012
+ const { accentColor } = this.options;
2013
+ const iconColor = COLORS.textSecondary;
2014
+ Object.assign(btn.style, {
2015
+ display: 'flex',
2016
+ alignItems: 'center',
2017
+ justifyContent: 'center',
2018
+ width: '22px',
2019
+ height: '22px',
2020
+ minWidth: '22px',
2021
+ minHeight: '22px',
2022
+ flexShrink: '0',
2023
+ borderRadius: '50%',
2024
+ border: `1px solid ${accentColor}60`,
2025
+ backgroundColor: 'transparent',
2026
+ color: `${iconColor}99`,
2027
+ cursor: 'pointer',
2028
+ transition: 'all 150ms',
2029
+ });
2030
+ btn.onmouseenter = () => {
2031
+ btn.style.borderColor = accentColor;
2032
+ btn.style.backgroundColor = `${accentColor}20`;
2033
+ btn.style.color = iconColor;
2034
+ };
2035
+ btn.onmouseleave = () => {
2036
+ btn.style.borderColor = `${accentColor}60`;
2037
+ btn.style.backgroundColor = 'transparent';
2038
+ btn.style.color = `${iconColor}99`;
2039
+ };
2040
+ btn.onclick = () => {
2041
+ this.toggleCompactMode();
2042
+ };
2043
+ // Chevron icon SVG - points right when expanded, left when compact
2044
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2045
+ svg.setAttribute('width', '12');
2046
+ svg.setAttribute('height', '12');
2047
+ svg.setAttribute('viewBox', '0 0 24 24');
2048
+ svg.setAttribute('fill', 'none');
2049
+ svg.setAttribute('stroke', 'currentColor');
2050
+ svg.setAttribute('stroke-width', '2');
2051
+ svg.setAttribute('stroke-linecap', 'round');
2052
+ svg.setAttribute('stroke-linejoin', 'round');
2053
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
2054
+ // Left chevron (<) when expanded to shrink, right chevron (>) when compact to expand
2055
+ path.setAttribute('points', isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6');
2056
+ svg.appendChild(path);
2057
+ btn.appendChild(svg);
2058
+ return btn;
2059
+ }
1904
2060
  /**
1905
2061
  * Create a settings section with title
1906
2062
  */
@@ -2054,49 +2210,78 @@ export class GlobalDevBar {
2054
2210
  popover.appendChild(themeSection);
2055
2211
  // ========== DISPLAY SECTION ==========
2056
2212
  const displaySection = this.createSettingsSection('Display');
2057
- // Position dropdown
2213
+ // Position mini-map selector
2058
2214
  const positionRow = document.createElement('div');
2059
- Object.assign(positionRow.style, {
2060
- display: 'flex',
2061
- alignItems: 'center',
2062
- justifyContent: 'space-between',
2063
- marginBottom: '8px',
2215
+ Object.assign(positionRow.style, { marginBottom: '10px' });
2216
+ const posLabel = document.createElement('div');
2217
+ Object.assign(posLabel.style, {
2218
+ color: COLORS.text,
2219
+ fontSize: '0.6875rem',
2220
+ marginBottom: '6px',
2064
2221
  });
2065
- const posLabel = document.createElement('span');
2066
- Object.assign(posLabel.style, { color: COLORS.text, fontSize: '0.6875rem' });
2067
2222
  posLabel.textContent = 'Position';
2068
2223
  positionRow.appendChild(posLabel);
2069
- const posSelect = document.createElement('select');
2070
- Object.assign(posSelect.style, {
2071
- backgroundColor: 'rgba(10, 15, 26, 0.8)',
2072
- border: `1px solid ${color}40`,
2224
+ // Mini-map container
2225
+ const miniMap = document.createElement('div');
2226
+ Object.assign(miniMap.style, {
2227
+ position: 'relative',
2228
+ width: '100%',
2229
+ height: '50px',
2230
+ backgroundColor: 'rgba(10, 15, 26, 0.6)',
2231
+ border: `1px solid ${color}30`,
2073
2232
  borderRadius: '4px',
2074
- color: COLORS.text,
2075
- fontSize: '0.625rem',
2076
- padding: '4px 6px',
2077
- cursor: 'pointer',
2078
- fontFamily: FONT_MONO,
2079
2233
  });
2080
- const positions = [
2081
- { value: 'bottom-left', label: 'Bottom Left' },
2082
- { value: 'bottom-right', label: 'Bottom Right' },
2083
- { value: 'bottom-center', label: 'Bottom Center' },
2084
- { value: 'top-left', label: 'Top Left' },
2085
- { value: 'top-right', label: 'Top Right' },
2234
+ const positionConfigs = [
2235
+ { value: 'top-left', style: { top: '8px', left: '10%' }, title: 'Top Left' },
2236
+ { value: 'top-right', style: { top: '8px', right: '6%' }, title: 'Top Right' },
2237
+ { value: 'bottom-left', style: { bottom: '8px', left: '10%' }, title: 'Bottom Left' },
2238
+ { value: 'bottom-right', style: { bottom: '8px', right: '6%' }, title: 'Bottom Right' },
2239
+ {
2240
+ value: 'bottom-center',
2241
+ style: { bottom: '6px', left: '50%', transform: 'translateX(-50%)' },
2242
+ title: 'Bottom Center',
2243
+ },
2086
2244
  ];
2087
- positions.forEach(({ value, label }) => {
2088
- const option = document.createElement('option');
2089
- option.value = value;
2090
- option.textContent = label;
2091
- option.selected = this.options.position === value;
2092
- posSelect.appendChild(option);
2245
+ positionConfigs.forEach(({ value, style, title }) => {
2246
+ const indicator = document.createElement('button');
2247
+ const isActive = this.options.position === value;
2248
+ Object.assign(indicator.style, {
2249
+ position: 'absolute',
2250
+ width: '20px',
2251
+ height: '6px',
2252
+ backgroundColor: isActive ? accentColor : `${color}60`,
2253
+ border: `1px solid ${isActive ? accentColor : `${color}40`}`,
2254
+ borderRadius: '2px',
2255
+ cursor: 'pointer',
2256
+ padding: '0',
2257
+ transition: 'all 150ms',
2258
+ boxShadow: isActive ? `0 0 8px ${accentColor}60` : 'none',
2259
+ ...style,
2260
+ });
2261
+ indicator.title = title;
2262
+ indicator.onclick = () => {
2263
+ this.options.position = value;
2264
+ this.settingsManager.saveSettings({ position: value });
2265
+ this.render();
2266
+ };
2267
+ // Hover effect
2268
+ indicator.onmouseenter = () => {
2269
+ if (!isActive) {
2270
+ indicator.style.backgroundColor = accentColor;
2271
+ indicator.style.borderColor = accentColor;
2272
+ indicator.style.boxShadow = `0 0 6px ${accentColor}40`;
2273
+ }
2274
+ };
2275
+ indicator.onmouseleave = () => {
2276
+ if (!isActive) {
2277
+ indicator.style.backgroundColor = `${color}60`;
2278
+ indicator.style.borderColor = `${color}40`;
2279
+ indicator.style.boxShadow = 'none';
2280
+ }
2281
+ };
2282
+ miniMap.appendChild(indicator);
2093
2283
  });
2094
- posSelect.onchange = () => {
2095
- this.options.position = posSelect.value;
2096
- this.settingsManager.saveSettings({ position: this.options.position });
2097
- this.render();
2098
- };
2099
- positionRow.appendChild(posSelect);
2284
+ positionRow.appendChild(miniMap);
2100
2285
  displaySection.appendChild(positionRow);
2101
2286
  // Compact mode toggle
2102
2287
  displaySection.appendChild(this.createToggleRow('Compact Mode', this.compactMode, accentColor, () => {
@@ -2458,9 +2643,10 @@ export class GlobalDevBar {
2458
2643
  const bp = this.breakpointInfo.tailwindBreakpoint;
2459
2644
  const breakpointData = TAILWIND_BREAKPOINTS[bp];
2460
2645
  const bpSpan = document.createElement('span');
2461
- bpSpan.className = this.tooltipClass('left', 'devbar-item');
2646
+ bpSpan.className = 'devbar-item';
2462
2647
  Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
2463
- bpSpan.setAttribute('data-tooltip', `Tailwind Breakpoint: ${bp}\n${breakpointData?.label || ''}\n\nViewport: ${this.breakpointInfo.dimensions}\n\nBreakpoints:\nbase: <640px | sm: >=640px\nmd: >=768px | lg: >=1024px\nxl: >=1280px | 2xl: >=1536px`);
2648
+ // Use HTML tooltip for breakpoint info
2649
+ this.attachBreakpointTooltip(bpSpan, bp, this.breakpointInfo.dimensions, breakpointData?.label || '');
2464
2650
  let bpText = bp;
2465
2651
  if (bp !== 'base') {
2466
2652
  bpText =
@@ -2482,45 +2668,45 @@ export class GlobalDevBar {
2482
2668
  if (showMetrics.fcp) {
2483
2669
  addSeparator();
2484
2670
  const fcpSpan = document.createElement('span');
2485
- fcpSpan.className = this.tooltipClass('left', 'devbar-item');
2671
+ fcpSpan.className = 'devbar-item';
2486
2672
  Object.assign(fcpSpan.style, { opacity: '0.85', cursor: 'default' });
2487
- fcpSpan.setAttribute('data-tooltip', 'First Contentful Paint (FCP): Time until first text/image renders.\n\nGood: <1.8s\nNeeds work: 1.8-3s\nPoor: >3s');
2488
2673
  fcpSpan.textContent = `FCP ${this.perfStats.fcp}`;
2674
+ this.attachMetricTooltip(fcpSpan, 'First Contentful Paint (FCP)', 'Time until the first text or image renders on screen.', { good: '<1.8s', needsWork: '1.8-3s', poor: '>3s' });
2489
2675
  infoSection.appendChild(fcpSpan);
2490
2676
  }
2491
2677
  if (showMetrics.lcp) {
2492
2678
  addSeparator();
2493
2679
  const lcpSpan = document.createElement('span');
2494
- lcpSpan.className = this.tooltipClass('left', 'devbar-item');
2680
+ lcpSpan.className = 'devbar-item';
2495
2681
  Object.assign(lcpSpan.style, { opacity: '0.85', cursor: 'default' });
2496
- lcpSpan.setAttribute('data-tooltip', 'Largest Contentful Paint (LCP): Time until largest visible element renders.\n\nGood: <2.5s\nNeeds work: 2.5-4s\nPoor: >4s');
2497
2682
  lcpSpan.textContent = `LCP ${this.perfStats.lcp}`;
2683
+ this.attachMetricTooltip(lcpSpan, 'Largest Contentful Paint (LCP)', 'Time until the largest visible element renders on screen.', { good: '<2.5s', needsWork: '2.5-4s', poor: '>4s' });
2498
2684
  infoSection.appendChild(lcpSpan);
2499
2685
  }
2500
2686
  if (showMetrics.cls) {
2501
2687
  addSeparator();
2502
2688
  const clsSpan = document.createElement('span');
2503
- clsSpan.className = this.tooltipClass('left', 'devbar-item');
2689
+ clsSpan.className = 'devbar-item';
2504
2690
  Object.assign(clsSpan.style, { opacity: '0.85', cursor: 'default' });
2505
- clsSpan.setAttribute('data-tooltip', 'Cumulative Layout Shift (CLS): Visual stability score.\nHigher values mean more unexpected layout shifts.\n\nGood: <0.1\nNeeds work: 0.1-0.25\nPoor: >0.25');
2506
2691
  clsSpan.textContent = `CLS ${this.perfStats.cls}`;
2692
+ this.attachMetricTooltip(clsSpan, 'Cumulative Layout Shift (CLS)', 'Visual stability score. Higher values mean more unexpected layout shifts.', { good: '<0.1', needsWork: '0.1-0.25', poor: '>0.25' });
2507
2693
  infoSection.appendChild(clsSpan);
2508
2694
  }
2509
2695
  if (showMetrics.inp) {
2510
2696
  addSeparator();
2511
2697
  const inpSpan = document.createElement('span');
2512
- inpSpan.className = this.tooltipClass('left', 'devbar-item');
2698
+ inpSpan.className = 'devbar-item';
2513
2699
  Object.assign(inpSpan.style, { opacity: '0.85', cursor: 'default' });
2514
- inpSpan.setAttribute('data-tooltip', 'Interaction to Next Paint (INP): Responsiveness to user input.\nMeasures the longest interaction delay.\n\nGood: <200ms\nNeeds work: 200-500ms\nPoor: >500ms');
2515
2700
  inpSpan.textContent = `INP ${this.perfStats.inp}`;
2701
+ this.attachMetricTooltip(inpSpan, 'Interaction to Next Paint (INP)', 'Responsiveness to user input. Measures the longest interaction delay.', { good: '<200ms', needsWork: '200-500ms', poor: '>500ms' });
2516
2702
  infoSection.appendChild(inpSpan);
2517
2703
  }
2518
2704
  if (showMetrics.pageSize) {
2519
2705
  addSeparator();
2520
2706
  const sizeSpan = document.createElement('span');
2521
- sizeSpan.className = this.tooltipClass('left', 'devbar-item');
2707
+ sizeSpan.className = 'devbar-item';
2522
2708
  Object.assign(sizeSpan.style, { opacity: '0.7', cursor: 'default' });
2523
- sizeSpan.setAttribute('data-tooltip', 'Total page size (compressed/transferred).\nIncludes HTML, CSS, JS, images, and other resources.');
2709
+ this.attachInfoTooltip(sizeSpan, 'Total Page Size', 'Compressed/transferred size including HTML, CSS, JS, images, and other resources.');
2524
2710
  sizeSpan.textContent = this.perfStats.totalSize;
2525
2711
  infoSection.appendChild(sizeSpan);
2526
2712
  }
@@ -2546,6 +2732,7 @@ export class GlobalDevBar {
2546
2732
  actionsContainer.appendChild(this.createOutlineButton());
2547
2733
  actionsContainer.appendChild(this.createSchemaButton());
2548
2734
  actionsContainer.appendChild(this.createSettingsButton());
2735
+ actionsContainer.appendChild(this.createCompactToggleButton());
2549
2736
  mainRow.appendChild(actionsContainer);
2550
2737
  wrapper.appendChild(mainRow);
2551
2738
  // Render custom controls row if there are any
@@ -2600,6 +2787,207 @@ export class GlobalDevBar {
2600
2787
  wrapper.appendChild(customRow);
2601
2788
  }
2602
2789
  }
2790
+ /** Create a tooltip container element */
2791
+ createTooltipContainer() {
2792
+ const tooltip = document.createElement('div');
2793
+ tooltip.setAttribute('data-devbar', 'true');
2794
+ Object.assign(tooltip.style, this.TOOLTIP_BASE_STYLES);
2795
+ return tooltip;
2796
+ }
2797
+ /** Add a bold title to tooltip (metric name, feature name, etc.) */
2798
+ addTooltipTitle(container, title) {
2799
+ const titleEl = document.createElement('div');
2800
+ const accentColor = this.settingsManager.get('accentColor') || COLORS.primary;
2801
+ Object.assign(titleEl.style, {
2802
+ color: accentColor,
2803
+ fontWeight: '600',
2804
+ marginBottom: '4px',
2805
+ });
2806
+ titleEl.textContent = title;
2807
+ container.appendChild(titleEl);
2808
+ }
2809
+ /** Add a description paragraph to tooltip */
2810
+ addTooltipDescription(container, description) {
2811
+ const descEl = document.createElement('div');
2812
+ Object.assign(descEl.style, {
2813
+ color: COLORS.text,
2814
+ marginBottom: '10px',
2815
+ lineHeight: '1.4',
2816
+ });
2817
+ descEl.textContent = description;
2818
+ container.appendChild(descEl);
2819
+ }
2820
+ /** Add a muted uppercase section header to tooltip */
2821
+ addTooltipSectionHeader(container, header) {
2822
+ const headerEl = document.createElement('div');
2823
+ Object.assign(headerEl.style, {
2824
+ color: COLORS.textMuted,
2825
+ fontSize: '0.625rem',
2826
+ textTransform: 'uppercase',
2827
+ letterSpacing: '0.05em',
2828
+ marginBottom: '6px',
2829
+ });
2830
+ headerEl.textContent = header;
2831
+ container.appendChild(headerEl);
2832
+ }
2833
+ /** Add a colored row with dot + label + value (for thresholds) */
2834
+ addTooltipColoredRow(container, label, value, color, labelWidth = '70px') {
2835
+ const row = document.createElement('div');
2836
+ Object.assign(row.style, { display: 'flex', alignItems: 'center', gap: '8px' });
2837
+ const dot = document.createElement('span');
2838
+ Object.assign(dot.style, {
2839
+ width: '6px',
2840
+ height: '6px',
2841
+ borderRadius: '50%',
2842
+ backgroundColor: color,
2843
+ flexShrink: '0',
2844
+ });
2845
+ row.appendChild(dot);
2846
+ const labelSpan = document.createElement('span');
2847
+ Object.assign(labelSpan.style, {
2848
+ color,
2849
+ fontWeight: '500',
2850
+ minWidth: labelWidth,
2851
+ });
2852
+ labelSpan.textContent = label;
2853
+ row.appendChild(labelSpan);
2854
+ const valueSpan = document.createElement('span');
2855
+ Object.assign(valueSpan.style, { color: COLORS.textMuted });
2856
+ valueSpan.textContent = value;
2857
+ row.appendChild(valueSpan);
2858
+ container.appendChild(row);
2859
+ }
2860
+ /** Add an info row with label + value (for breakpoint details) */
2861
+ addTooltipInfoRow(container, label, value) {
2862
+ const row = document.createElement('div');
2863
+ Object.assign(row.style, {
2864
+ display: 'flex',
2865
+ gap: '8px',
2866
+ lineHeight: '1.4',
2867
+ });
2868
+ const labelSpan = document.createElement('span');
2869
+ Object.assign(labelSpan.style, { color: COLORS.textMuted });
2870
+ labelSpan.textContent = label;
2871
+ row.appendChild(labelSpan);
2872
+ const valueSpan = document.createElement('span');
2873
+ Object.assign(valueSpan.style, { color: COLORS.text });
2874
+ valueSpan.textContent = value;
2875
+ row.appendChild(valueSpan);
2876
+ container.appendChild(row);
2877
+ }
2878
+ /** Position tooltip above the anchor element, adjusting for screen edges */
2879
+ positionTooltip(tooltip, anchor) {
2880
+ const rect = anchor.getBoundingClientRect();
2881
+ tooltip.style.left = `${rect.left}px`;
2882
+ tooltip.style.bottom = `${window.innerHeight - rect.top + 8}px`;
2883
+ document.body.appendChild(tooltip);
2884
+ // Adjust if off-screen
2885
+ const tooltipRect = tooltip.getBoundingClientRect();
2886
+ if (tooltipRect.right > window.innerWidth - 10) {
2887
+ tooltip.style.left = `${window.innerWidth - tooltipRect.width - 10}px`;
2888
+ }
2889
+ if (tooltipRect.left < 10) {
2890
+ tooltip.style.left = '10px';
2891
+ }
2892
+ }
2893
+ /** Attach an HTML tooltip to an element with custom content builder */
2894
+ attachHtmlTooltip(element, buildContent) {
2895
+ let tooltipEl = null;
2896
+ element.onmouseenter = () => {
2897
+ tooltipEl = this.createTooltipContainer();
2898
+ buildContent(tooltipEl);
2899
+ this.positionTooltip(tooltipEl, element);
2900
+ };
2901
+ element.onmouseleave = () => {
2902
+ if (tooltipEl) {
2903
+ tooltipEl.remove();
2904
+ tooltipEl = null;
2905
+ }
2906
+ };
2907
+ }
2908
+ // ============================================================================
2909
+ // Tooltip Attachment Methods (specific tooltip types)
2910
+ // ============================================================================
2911
+ /** Attach a metric tooltip with title, description, and colored thresholds */
2912
+ attachMetricTooltip(element, title, description, thresholds) {
2913
+ this.attachHtmlTooltip(element, (tooltip) => {
2914
+ this.addTooltipTitle(tooltip, title);
2915
+ this.addTooltipDescription(tooltip, description);
2916
+ this.addTooltipSectionHeader(tooltip, 'Thresholds');
2917
+ const thresholdsContainer = document.createElement('div');
2918
+ Object.assign(thresholdsContainer.style, {
2919
+ display: 'flex',
2920
+ flexDirection: 'column',
2921
+ gap: '4px',
2922
+ });
2923
+ this.addTooltipColoredRow(thresholdsContainer, 'Good', thresholds.good, COLORS.primary);
2924
+ this.addTooltipColoredRow(thresholdsContainer, 'Needs work', thresholds.needsWork, COLORS.warning);
2925
+ this.addTooltipColoredRow(thresholdsContainer, 'Poor', thresholds.poor, COLORS.error);
2926
+ tooltip.appendChild(thresholdsContainer);
2927
+ });
2928
+ }
2929
+ /** Attach a breakpoint tooltip showing current breakpoint and all breakpoint ranges */
2930
+ attachBreakpointTooltip(element, breakpoint, dimensions, breakpointLabel) {
2931
+ this.attachHtmlTooltip(element, (tooltip) => {
2932
+ this.addTooltipTitle(tooltip, 'Tailwind Breakpoint');
2933
+ // Current breakpoint info
2934
+ const currentSection = document.createElement('div');
2935
+ Object.assign(currentSection.style, { marginBottom: '10px' });
2936
+ this.addTooltipInfoRow(currentSection, 'Current:', `${breakpoint} (${breakpointLabel})`);
2937
+ this.addTooltipInfoRow(currentSection, 'Viewport:', dimensions);
2938
+ tooltip.appendChild(currentSection);
2939
+ // Breakpoints reference
2940
+ this.addTooltipSectionHeader(tooltip, 'Breakpoints');
2941
+ const bpContainer = document.createElement('div');
2942
+ Object.assign(bpContainer.style, {
2943
+ display: 'flex',
2944
+ flexDirection: 'column',
2945
+ gap: '2px',
2946
+ fontSize: '0.625rem',
2947
+ });
2948
+ const breakpoints = [
2949
+ { name: 'base', range: '<640px' },
2950
+ { name: 'sm', range: '≥640px' },
2951
+ { name: 'md', range: '≥768px' },
2952
+ { name: 'lg', range: '≥1024px' },
2953
+ { name: 'xl', range: '≥1280px' },
2954
+ { name: '2xl', range: '≥1536px' },
2955
+ ];
2956
+ for (const bp of breakpoints) {
2957
+ const row = document.createElement('div');
2958
+ Object.assign(row.style, { display: 'flex', gap: '8px' });
2959
+ const nameSpan = document.createElement('span');
2960
+ Object.assign(nameSpan.style, {
2961
+ color: bp.name === breakpoint ? COLORS.primary : COLORS.textMuted,
2962
+ fontWeight: bp.name === breakpoint ? '600' : '400',
2963
+ minWidth: '32px',
2964
+ });
2965
+ nameSpan.textContent = bp.name;
2966
+ row.appendChild(nameSpan);
2967
+ const rangeSpan = document.createElement('span');
2968
+ Object.assign(rangeSpan.style, {
2969
+ color: bp.name === breakpoint ? COLORS.text : COLORS.textMuted,
2970
+ });
2971
+ rangeSpan.textContent = bp.range;
2972
+ row.appendChild(rangeSpan);
2973
+ bpContainer.appendChild(row);
2974
+ }
2975
+ tooltip.appendChild(bpContainer);
2976
+ });
2977
+ }
2978
+ /** Attach a simple info tooltip with title and description */
2979
+ attachInfoTooltip(element, title, description) {
2980
+ this.attachHtmlTooltip(element, (tooltip) => {
2981
+ this.addTooltipTitle(tooltip, title);
2982
+ const descEl = document.createElement('div');
2983
+ Object.assign(descEl.style, {
2984
+ color: COLORS.text,
2985
+ lineHeight: '1.4',
2986
+ });
2987
+ descEl.textContent = description;
2988
+ tooltip.appendChild(descEl);
2989
+ });
2990
+ }
2603
2991
  /**
2604
2992
  * Create a console badge for error/warning counts
2605
2993
  */
@@ -2638,6 +3026,9 @@ export class GlobalDevBar {
2638
3026
  btn.type = 'button';
2639
3027
  btn.className = this.tooltipClass('right');
2640
3028
  const hasSuccessState = this.copiedToClipboard || this.copiedPath || this.lastScreenshot;
3029
+ const isDisabled = this.capturing;
3030
+ // Grey out when not connected (save won't work, but clipboard still does)
3031
+ const isGreyedOut = !this.sweetlinkConnected && !hasSuccessState;
2641
3032
  const tooltip = this.getScreenshotTooltip();
2642
3033
  btn.setAttribute('data-tooltip', tooltip);
2643
3034
  Object.assign(btn.style, {
@@ -2654,11 +3045,11 @@ export class GlobalDevBar {
2654
3045
  borderColor: hasSuccessState ? accentColor : `${accentColor}80`,
2655
3046
  backgroundColor: hasSuccessState ? `${accentColor}33` : 'transparent',
2656
3047
  color: hasSuccessState ? accentColor : `${accentColor}99`,
2657
- cursor: !this.capturing ? 'pointer' : 'not-allowed',
2658
- opacity: '1',
3048
+ cursor: !isDisabled ? 'pointer' : 'not-allowed',
3049
+ opacity: isGreyedOut ? '0.4' : '1',
2659
3050
  transition: 'all 150ms',
2660
3051
  });
2661
- btn.disabled = this.capturing;
3052
+ btn.disabled = isDisabled;
2662
3053
  btn.onclick = (e) => {
2663
3054
  // If we have a saved screenshot path, clicking copies the path
2664
3055
  if (this.lastScreenshot && !e.shiftKey) {
@@ -2715,10 +3106,10 @@ export class GlobalDevBar {
2715
3106
  if (this.lastScreenshot) {
2716
3107
  return `Screenshot saved!\n${this.lastScreenshot}\n\nClick to copy path`;
2717
3108
  }
2718
- const baseTooltip = `Screenshot\n\nClick: Save to file\nShift+Click: Copy to clipboard\n\nKeyboard:\nCmd/Ctrl+Shift+S: Save\nCmd/Ctrl+Shift+C: Copy`;
2719
- return this.sweetlinkConnected
2720
- ? baseTooltip
2721
- : `${baseTooltip}\n\nWarning: Sweetlink not connected`;
3109
+ if (!this.sweetlinkConnected) {
3110
+ return `Screenshot (Disconnected)\n\nShift+Click: Copy to clipboard\n\n⚠ Sweetlink not connected\nSave to file unavailable`;
3111
+ }
3112
+ return `Screenshot\n\nClick: Save to file\nShift+Click: Copy to clipboard\n\nKeyboard:\nCmd/Ctrl+Shift+S: Save\nCmd/Ctrl+Shift+C: Copy`;
2722
3113
  }
2723
3114
  /**
2724
3115
  * Get the tooltip text for the AI review button based on current state
@@ -2778,9 +3169,16 @@ export class GlobalDevBar {
2778
3169
  const btn = document.createElement('button');
2779
3170
  btn.type = 'button';
2780
3171
  btn.className = this.tooltipClass('right');
2781
- const tooltip = this.lastOutline
2782
- ? `Outline saved to:\n${this.lastOutline}`
2783
- : `Document Outline\n\nView page heading structure and\nsave as markdown.`;
3172
+ let tooltip;
3173
+ if (this.lastOutline) {
3174
+ tooltip = `Outline saved to:\n${this.lastOutline}`;
3175
+ }
3176
+ else if (!this.sweetlinkConnected) {
3177
+ tooltip = `Document Outline\n\nView page heading structure.\n\n⚠ Sweetlink not connected\nSave to file unavailable`;
3178
+ }
3179
+ else {
3180
+ tooltip = `Document Outline\n\nView page heading structure and\nsave as markdown.`;
3181
+ }
2784
3182
  btn.setAttribute('data-tooltip', tooltip);
2785
3183
  const isActive = this.showOutlineModal || !!this.lastOutline;
2786
3184
  Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.outline, isActive, false));
@@ -2798,9 +3196,16 @@ export class GlobalDevBar {
2798
3196
  const btn = document.createElement('button');
2799
3197
  btn.type = 'button';
2800
3198
  btn.className = this.tooltipClass('right');
2801
- const tooltip = this.lastSchema
2802
- ? `Schema saved to:\n${this.lastSchema}`
2803
- : `Page Schema\n\nView JSON-LD, Open Graph, and\nother structured data.`;
3199
+ let tooltip;
3200
+ if (this.lastSchema) {
3201
+ tooltip = `Schema saved to:\n${this.lastSchema}`;
3202
+ }
3203
+ else if (!this.sweetlinkConnected) {
3204
+ tooltip = `Page Schema\n\nView JSON-LD, Open Graph, and\nother structured data.\n\n⚠ Sweetlink not connected\nSave to file unavailable`;
3205
+ }
3206
+ else {
3207
+ tooltip = `Page Schema\n\nView JSON-LD, Open Graph, and\nother structured data.`;
3208
+ }
2804
3209
  btn.setAttribute('data-tooltip', tooltip);
2805
3210
  const isActive = this.showSchemaModal || !!this.lastSchema;
2806
3211
  Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.schema, isActive, false));
@@ -11,8 +11,16 @@ export declare const MAX_RECONNECT_ATTEMPTS = 10;
11
11
  export declare const BASE_RECONNECT_DELAY_MS = 1000;
12
12
  /** Maximum delay between reconnection attempts (ms) */
13
13
  export declare const MAX_RECONNECT_DELAY_MS = 30000;
14
- /** Default WebSocket port for Sweetlink connection */
14
+ /** Default WebSocket port for Sweetlink connection (legacy fallback) */
15
15
  export declare const WS_PORT = 9223;
16
+ /** Port offset from app port to calculate WebSocket port (matches SweetlinkBridge) */
17
+ export declare const WS_PORT_OFFSET = 6223;
18
+ /** Maximum ports to try when scanning for matching server */
19
+ export declare const MAX_PORT_RETRIES = 10;
20
+ /** Delay between port scan attempts (ms) */
21
+ export declare const PORT_RETRY_DELAY_MS = 100;
22
+ /** Delay before restarting port scan from base after all ports fail (ms) */
23
+ export declare const PORT_SCAN_RESTART_DELAY_MS = 3000;
16
24
  /** Duration to show screenshot notification (ms) */
17
25
  export declare const SCREENSHOT_NOTIFICATION_MS = 3000;
18
26
  /** Duration to show clipboard notification (ms) */
@@ -130,28 +138,28 @@ export declare const DEVBAR_THEME: {
130
138
  };
131
139
  };
132
140
  export type DevBarTheme = typeof DEVBAR_THEME;
133
- /** Light theme variant - same structure, different colors */
141
+ /** Light theme variant - terminal aesthetic with light green tones */
134
142
  export declare const DEVBAR_THEME_LIGHT: {
135
143
  readonly colors: {
136
- readonly primary: "#059669";
137
- readonly primaryHover: "#047857";
138
- readonly primaryGlow: "rgba(5, 150, 105, 0.2)";
139
- readonly error: "#ef4444";
144
+ readonly primary: "#047857";
145
+ readonly primaryHover: "#065f46";
146
+ readonly primaryGlow: "rgba(4, 120, 87, 0.25)";
147
+ readonly error: "#dc2626";
140
148
  readonly warning: "#d97706";
141
149
  readonly info: "#2563eb";
142
- readonly purple: "#9333ea";
150
+ readonly purple: "#7c3aed";
143
151
  readonly cyan: "#0891b2";
144
152
  readonly pink: "#db2777";
145
153
  readonly lime: "#65a30d";
146
- readonly bg: "#f8fafc";
147
- readonly bgCard: "rgba(255, 255, 255, 0.98)";
148
- readonly bgElevated: "rgba(255, 255, 255, 0.99)";
149
- readonly bgInput: "rgba(241, 245, 249, 0.9)";
150
- readonly text: "#1e293b";
151
- readonly textSecondary: "#475569";
152
- readonly textMuted: "#64748b";
153
- readonly border: "rgba(5, 150, 105, 0.25)";
154
- readonly borderSubtle: "rgba(0, 0, 0, 0.06)";
154
+ readonly bg: "#ecfdf5";
155
+ readonly bgCard: "rgba(255, 255, 255, 0.85)";
156
+ readonly bgElevated: "rgba(255, 255, 255, 0.95)";
157
+ readonly bgInput: "rgba(236, 253, 245, 0.9)";
158
+ readonly text: "#064e3b";
159
+ readonly textSecondary: "#065f46";
160
+ readonly textMuted: "#047857";
161
+ readonly border: "rgba(4, 120, 87, 0.3)";
162
+ readonly borderSubtle: "rgba(4, 120, 87, 0.1)";
155
163
  };
156
164
  readonly fonts: {
157
165
  readonly mono: "'Departure Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
@@ -181,10 +189,10 @@ export declare const DEVBAR_THEME_LIGHT: {
181
189
  readonly lg: "12px";
182
190
  };
183
191
  readonly shadows: {
184
- readonly sm: "0 1px 2px rgba(0, 0, 0, 0.08)";
185
- readonly md: "0 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(5, 150, 105, 0.1)";
186
- readonly lg: "0 8px 32px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(5, 150, 105, 0.12)";
187
- readonly glow: "0 0 20px rgba(5, 150, 105, 0.1)";
192
+ readonly sm: "0 1px 2px rgba(4, 120, 87, 0.1)";
193
+ readonly md: "0 4px 12px rgba(4, 120, 87, 0.12), 0 0 0 1px rgba(4, 120, 87, 0.15)";
194
+ readonly lg: "0 8px 32px rgba(4, 120, 87, 0.15), 0 0 0 1px rgba(4, 120, 87, 0.2)";
195
+ readonly glow: "0 0 20px rgba(4, 120, 87, 0.15)";
188
196
  };
189
197
  readonly transitions: {
190
198
  readonly fast: "150ms";
package/dist/constants.js CHANGED
@@ -18,8 +18,16 @@ export const MAX_RECONNECT_DELAY_MS = 30000;
18
18
  // ============================================================================
19
19
  // WebSocket Settings
20
20
  // ============================================================================
21
- /** Default WebSocket port for Sweetlink connection */
21
+ /** Default WebSocket port for Sweetlink connection (legacy fallback) */
22
22
  export const WS_PORT = 9223;
23
+ /** Port offset from app port to calculate WebSocket port (matches SweetlinkBridge) */
24
+ export const WS_PORT_OFFSET = 6223;
25
+ /** Maximum ports to try when scanning for matching server */
26
+ export const MAX_PORT_RETRIES = 10;
27
+ /** Delay between port scan attempts (ms) */
28
+ export const PORT_RETRY_DELAY_MS = 100;
29
+ /** Delay before restarting port scan from base after all ports fail (ms) */
30
+ export const PORT_SCAN_RESTART_DELAY_MS = 3000;
23
31
  // ============================================================================
24
32
  // Notification Durations
25
33
  // ============================================================================
@@ -174,44 +182,44 @@ export const DEVBAR_THEME = {
174
182
  fast: '150ms',
175
183
  },
176
184
  };
177
- /** Light theme variant - same structure, different colors */
185
+ /** Light theme variant - terminal aesthetic with light green tones */
178
186
  export const DEVBAR_THEME_LIGHT = {
179
187
  colors: {
180
- // Primary accent (darker for light mode)
181
- primary: '#059669', // darker emerald
182
- primaryHover: '#047857',
183
- primaryGlow: 'rgba(5, 150, 105, 0.2)',
184
- // Semantic colors (same)
185
- error: PALETTE.red,
186
- warning: '#d97706', // darker amber
187
- info: '#2563eb', // darker blue
188
- // Extended palette (darker variants)
189
- purple: '#9333ea',
188
+ // Primary accent (darker emerald for contrast)
189
+ primary: '#047857', // darker emerald for better contrast
190
+ primaryHover: '#065f46',
191
+ primaryGlow: 'rgba(4, 120, 87, 0.25)',
192
+ // Semantic colors (adjusted for light bg)
193
+ error: '#dc2626',
194
+ warning: '#d97706',
195
+ info: '#2563eb',
196
+ // Extended palette (darker for light mode)
197
+ purple: '#7c3aed',
190
198
  cyan: '#0891b2',
191
199
  pink: '#db2777',
192
200
  lime: '#65a30d',
193
- // Backgrounds (light)
194
- bg: '#f8fafc',
195
- bgCard: 'rgba(255, 255, 255, 0.98)',
196
- bgElevated: 'rgba(255, 255, 255, 0.99)',
197
- bgInput: 'rgba(241, 245, 249, 0.9)',
201
+ // Backgrounds - terminal light green aesthetic
202
+ bg: '#ecfdf5', // very light mint/green
203
+ bgCard: 'rgba(255, 255, 255, 0.85)',
204
+ bgElevated: 'rgba(255, 255, 255, 0.95)',
205
+ bgInput: 'rgba(236, 253, 245, 0.9)', // light mint input
198
206
  // Text (dark on light)
199
- text: '#1e293b',
200
- textSecondary: '#475569',
201
- textMuted: '#64748b',
202
- // Borders (green-tinted)
203
- border: 'rgba(5, 150, 105, 0.25)',
204
- borderSubtle: 'rgba(0, 0, 0, 0.06)',
207
+ text: '#064e3b', // dark emerald text
208
+ textSecondary: '#065f46',
209
+ textMuted: '#047857',
210
+ // Borders (emerald-tinted)
211
+ border: 'rgba(4, 120, 87, 0.3)',
212
+ borderSubtle: 'rgba(4, 120, 87, 0.1)',
205
213
  },
206
214
  // Other properties same as dark theme
207
215
  fonts: DEVBAR_THEME.fonts,
208
216
  typography: DEVBAR_THEME.typography,
209
217
  radius: DEVBAR_THEME.radius,
210
218
  shadows: {
211
- sm: '0 1px 2px rgba(0, 0, 0, 0.08)',
212
- md: '0 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(5, 150, 105, 0.1)',
213
- lg: '0 8px 32px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(5, 150, 105, 0.12)',
214
- glow: '0 0 20px rgba(5, 150, 105, 0.1)',
219
+ sm: '0 1px 2px rgba(4, 120, 87, 0.1)',
220
+ md: '0 4px 12px rgba(4, 120, 87, 0.12), 0 0 0 1px rgba(4, 120, 87, 0.15)',
221
+ lg: '0 8px 32px rgba(4, 120, 87, 0.15), 0 0 0 1px rgba(4, 120, 87, 0.2)',
222
+ glow: '0 0 20px rgba(4, 120, 87, 0.15)',
215
223
  },
216
224
  transitions: DEVBAR_THEME.transitions,
217
225
  };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- export { BUTTON_COLORS, CATEGORY_COLORS, COLORS, DEVBAR_THEME, DEVBAR_THEME_LIGHT, type DevBarTheme, type DevBarThemeInput, FONT_MONO, generateBreakpointCSS, generateThemeCSSVars, getEffectiveTheme, getStoredThemeMode, getTheme, getThemeColors, injectThemeCSS, setStoredThemeMode, STORAGE_KEYS, TAILWIND_BREAKPOINTS, type TailwindBreakpoint, type ThemeColors, } from './constants.js';
1
+ export { type A11yState, type AxeResult, type AxeViolation, clearA11yCache, formatViolation, getBadgeColor, getCachedResult, getImpactColor, getViolationCounts, groupViolationsByImpact, isAxeLoaded, preloadAxe, runA11yAudit, } from './accessibility.js';
2
+ export { BUTTON_COLORS, CATEGORY_COLORS, COLORS, DEVBAR_THEME, DEVBAR_THEME_LIGHT, type DevBarTheme, type DevBarThemeInput, FONT_MONO, generateBreakpointCSS, generateThemeCSSVars, getEffectiveTheme, getStoredThemeMode, getTheme, getThemeColors, injectThemeCSS, STORAGE_KEYS, setStoredThemeMode, TAILWIND_BREAKPOINTS, type TailwindBreakpoint, type ThemeColors, } from './constants.js';
2
3
  export { DebugLogger, normalizeDebugConfig } from './debug.js';
3
4
  export { EARLY_CONSOLE_CAPTURE_SCRIPT } from './earlyConsoleCapture.js';
4
5
  export { destroyGlobalDevBar, earlyConsoleCapture, GlobalDevBar, getGlobalDevBar, initGlobalDevBar, } from './GlobalDevBar.js';
5
- export { initDebug, initFull, initMinimal, initPerformance, initResponsive, PRESET_DEBUG, PRESET_FULL, PRESET_MINIMAL, PRESET_PERFORMANCE, PRESET_RESPONSIVE, } from './presets.js';
6
6
  export { getHtml2Canvas, isHtml2CanvasLoaded, preloadHtml2Canvas } from './lazy/index.js';
7
+ export { formatBytes as formatNetworkBytes, formatDuration, getInitiatorColor, type NetworkEntry, NetworkMonitor, type NetworkState, } from './network.js';
7
8
  export { extractDocumentOutline, outlineToMarkdown } from './outline.js';
9
+ export { initDebug, initFull, initMinimal, initPerformance, initResponsive, PRESET_DEBUG, PRESET_FULL, PRESET_MINIMAL, PRESET_PERFORMANCE, PRESET_RESPONSIVE, } from './presets.js';
8
10
  export { extractPageSchema, schemaToMarkdown } from './schema.js';
9
- export { formatBytes as formatNetworkBytes, formatDuration, getInitiatorColor, NetworkMonitor, type NetworkEntry, type NetworkState, } from './network.js';
10
- export { beautifyJson, clearLocalStorage, clearSessionStorage, deleteCookie, deleteLocalStorageItem, deleteSessionStorageItem, formatStorageSummary, getCookies, getLocalStorage, getSessionStorage, getStorageData, setLocalStorageItem, setSessionStorageItem, type CookieItem, type StorageData, type StorageItem, } from './storage.js';
11
- export { clearA11yCache, formatViolation, getBadgeColor, getCachedResult, getImpactColor, getViolationCounts, groupViolationsByImpact, isAxeLoaded, preloadAxe, runA11yAudit, type A11yState, type AxeResult, type AxeViolation, } from './accessibility.js';
11
+ export { beautifyJson, type CookieItem, clearLocalStorage, clearSessionStorage, deleteCookie, deleteLocalStorageItem, deleteSessionStorageItem, formatStorageSummary, getCookies, getLocalStorage, getSessionStorage, getStorageData, type StorageData, type StorageItem, setLocalStorageItem, setSessionStorageItem, } from './storage.js';
12
12
  export type { ConsoleLog, DebugConfig, DevBarControl, GlobalDevBarOptions, OutlineNode, PageSchema, SweetlinkCommand, ThemeMode, } from './types.js';
13
13
  export { canvasToDataUrl, copyCanvasToClipboard, delay, formatArg, formatArgs, prepareForCapture, } from './utils.js';
package/dist/index.js CHANGED
@@ -1,25 +1,25 @@
1
1
  // DevBar - Development toolbar and utilities
2
2
  // Pure vanilla JavaScript - no framework dependencies
3
+ // Accessibility audit utilities
4
+ export { clearA11yCache, formatViolation, getBadgeColor, getCachedResult, getImpactColor, getViolationCounts, groupViolationsByImpact, isAxeLoaded, preloadAxe, runA11yAudit, } from './accessibility.js';
3
5
  // Re-export constants and theme utilities
4
- export { BUTTON_COLORS, CATEGORY_COLORS, COLORS, DEVBAR_THEME, DEVBAR_THEME_LIGHT, FONT_MONO, generateBreakpointCSS, generateThemeCSSVars, getEffectiveTheme, getStoredThemeMode, getTheme, getThemeColors, injectThemeCSS, setStoredThemeMode, STORAGE_KEYS, TAILWIND_BREAKPOINTS, } from './constants.js';
6
+ export { BUTTON_COLORS, CATEGORY_COLORS, COLORS, DEVBAR_THEME, DEVBAR_THEME_LIGHT, FONT_MONO, generateBreakpointCSS, generateThemeCSSVars, getEffectiveTheme, getStoredThemeMode, getTheme, getThemeColors, injectThemeCSS, STORAGE_KEYS, setStoredThemeMode, TAILWIND_BREAKPOINTS, } from './constants.js';
5
7
  // Debug utilities
6
8
  export { DebugLogger, normalizeDebugConfig } from './debug.js';
7
9
  // Early console capture script for injection
8
10
  export { EARLY_CONSOLE_CAPTURE_SCRIPT } from './earlyConsoleCapture.js';
9
11
  // Main vanilla JS devbar
10
12
  export { destroyGlobalDevBar, earlyConsoleCapture, GlobalDevBar, getGlobalDevBar, initGlobalDevBar, } from './GlobalDevBar.js';
11
- // Configuration presets
12
- export { initDebug, initFull, initMinimal, initPerformance, initResponsive, PRESET_DEBUG, PRESET_FULL, PRESET_MINIMAL, PRESET_PERFORMANCE, PRESET_RESPONSIVE, } from './presets.js';
13
13
  // Lazy loading utilities
14
14
  export { getHtml2Canvas, isHtml2CanvasLoaded, preloadHtml2Canvas } from './lazy/index.js';
15
+ // Network monitoring utilities
16
+ export { formatBytes as formatNetworkBytes, formatDuration, getInitiatorColor, NetworkMonitor, } from './network.js';
15
17
  // Re-export outline/schema functions
16
18
  export { extractDocumentOutline, outlineToMarkdown } from './outline.js';
19
+ // Configuration presets
20
+ export { initDebug, initFull, initMinimal, initPerformance, initResponsive, PRESET_DEBUG, PRESET_FULL, PRESET_MINIMAL, PRESET_PERFORMANCE, PRESET_RESPONSIVE, } from './presets.js';
17
21
  export { extractPageSchema, schemaToMarkdown } from './schema.js';
18
- // Network monitoring utilities
19
- export { formatBytes as formatNetworkBytes, formatDuration, getInitiatorColor, NetworkMonitor, } from './network.js';
20
22
  // Storage inspection utilities
21
23
  export { beautifyJson, clearLocalStorage, clearSessionStorage, deleteCookie, deleteLocalStorageItem, deleteSessionStorageItem, formatStorageSummary, getCookies, getLocalStorage, getSessionStorage, getStorageData, setLocalStorageItem, setSessionStorageItem, } from './storage.js';
22
- // Accessibility audit utilities
23
- export { clearA11yCache, formatViolation, getBadgeColor, getCachedResult, getImpactColor, getViolationCounts, groupViolationsByImpact, isAxeLoaded, preloadAxe, runA11yAudit, } from './accessibility.js';
24
24
  // Re-export utilities for external use
25
25
  export { canvasToDataUrl, copyCanvasToClipboard, delay, formatArg, formatArgs, prepareForCapture, } from './utils.js';
package/dist/presets.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Pre-configured options for common DevBar use cases.
5
5
  */
6
- import { GlobalDevBar } from './GlobalDevBar.js';
6
+ import { type GlobalDevBar } from './GlobalDevBar.js';
7
7
  import type { GlobalDevBarOptions } from './types.js';
8
8
  /**
9
9
  * Minimal preset - shows only essential features
@@ -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
@@ -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,7 +55,7 @@ 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 () => {
@@ -71,8 +73,17 @@ export function createModalHeader(config) {
71
73
  headerButtons.appendChild(copyBtn);
72
74
  // Save button (if Sweetlink connected)
73
75
  if (sweetlinkConnected && onSave) {
74
- const saveBtn = createStyledButton({ color, text: 'Save' });
75
- 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
+ }
76
87
  headerButtons.appendChild(saveBtn);
77
88
  }
78
89
  // Close button - use same padding as other buttons for consistent height
@@ -85,6 +96,39 @@ export function createModalHeader(config) {
85
96
  closeBtn.onclick = onClose;
86
97
  headerButtons.appendChild(closeBtn);
87
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
+ }
88
132
  return header;
89
133
  }
90
134
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ytspar/devbar",
3
- "version": "1.0.0-canary.3c85c90",
3
+ "version": "1.0.0-canary.4b73445",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Development toolbar and utilities with Sweetlink integration - pure vanilla JS, no framework dependencies",