@blorkfield/blork-tabs 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/DebugPanel.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * In-browser debug log panel for environments without console access
4
4
  */
5
5
 
6
- import type { DebugPanelConfig, DebugPanel, DebugLogLevel, PanelState, CSSClasses } from './types';
6
+ import type { DebugPanelConfig, DebugPanel, DebugLog, DebugLogConfig, DebugLogLevel, PanelState, CSSClasses } from './types';
7
7
 
8
8
  export interface DebugPanelElements {
9
9
  logContainer: HTMLDivElement;
@@ -30,7 +30,8 @@ export function createDebugPanelContent(
30
30
  }
31
31
 
32
32
  /**
33
- * Create the interface for interacting with a debug panel
33
+ * Create the interface for interacting with a debug panel.
34
+ * Uses the shared createDebugLogInterface internally.
34
35
  */
35
36
  export function createDebugPanelInterface(
36
37
  panel: PanelState,
@@ -38,12 +39,46 @@ export function createDebugPanelInterface(
38
39
  config: DebugPanelConfig,
39
40
  classes: CSSClasses
40
41
  ): DebugPanel {
42
+ const debugLog = createDebugLogInterface(elements.logContainer, config, classes);
43
+ return {
44
+ panel,
45
+ ...debugLog,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Escape HTML special characters to prevent XSS
51
+ */
52
+ function escapeHtml(str: string): string {
53
+ const div = document.createElement('div');
54
+ div.textContent = str;
55
+ return div.innerHTML;
56
+ }
57
+
58
+ /**
59
+ * Shared interface config for debug log creation
60
+ */
61
+ interface DebugLogInterfaceConfig {
62
+ maxEntries?: number;
63
+ showTimestamps?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Create the logging interface for a debug log container.
68
+ * This is the shared implementation used by both standalone panels and embedded logs.
69
+ */
70
+ export function createDebugLogInterface(
71
+ logContainer: HTMLElement,
72
+ config: DebugLogInterfaceConfig,
73
+ classes: CSSClasses
74
+ ): DebugLog {
41
75
  const maxEntries = config.maxEntries ?? 50;
42
76
  const showTimestamps = config.showTimestamps ?? false;
77
+ const entryClass = classes.debugLogEntry;
43
78
 
44
79
  function addEntry(level: DebugLogLevel, eventName: string, data?: Record<string, unknown>): void {
45
80
  const entry = document.createElement('div');
46
- entry.className = classes.debugLogEntry;
81
+ entry.className = entryClass;
47
82
 
48
83
  // Add level-specific class for color coding
49
84
  if (level === 'warn') entry.classList.add(classes.debugLogEntryWarn);
@@ -69,30 +104,165 @@ export function createDebugPanelInterface(
69
104
  }
70
105
 
71
106
  entry.innerHTML = html;
72
- elements.logContainer.appendChild(entry);
73
- elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
107
+ logContainer.appendChild(entry);
108
+ logContainer.scrollTop = logContainer.scrollHeight;
74
109
 
75
- // Remove oldest entries if over limit
76
- while (elements.logContainer.children.length > maxEntries) {
77
- elements.logContainer.removeChild(elements.logContainer.children[0]);
110
+ // Remove oldest entries if over limit (only count actual log entries, not the close button)
111
+ const entries = logContainer.querySelectorAll(`.${entryClass}`);
112
+ if (entries.length > maxEntries) {
113
+ const toRemove = entries.length - maxEntries;
114
+ for (let i = 0; i < toRemove; i++) {
115
+ entries[i].remove();
116
+ }
78
117
  }
79
118
  }
80
119
 
120
+ function clearEntries(): void {
121
+ // Only remove log entries, preserve the close button
122
+ const entries = logContainer.querySelectorAll(`.${entryClass}`);
123
+ entries.forEach(entry => entry.remove());
124
+ }
125
+
81
126
  return {
82
- panel,
83
127
  log: (name, data) => addEntry('log', name, data),
84
128
  info: (name, data) => addEntry('info', name, data),
85
129
  warn: (name, data) => addEntry('warn', name, data),
86
130
  error: (name, data) => addEntry('error', name, data),
87
- clear: () => { elements.logContainer.innerHTML = ''; }
131
+ clear: clearEntries,
88
132
  };
89
133
  }
90
134
 
135
+ export interface DebugLogSetup {
136
+ debugLog: DebugLog;
137
+ logContainer: HTMLDivElement;
138
+ }
139
+
91
140
  /**
92
- * Escape HTML special characters to prevent XSS
141
+ * Create an embeddable debug log in any container element
93
142
  */
94
- function escapeHtml(str: string): string {
95
- const div = document.createElement('div');
96
- div.textContent = str;
97
- return div.innerHTML;
143
+ export function createDebugLog(
144
+ container: HTMLElement,
145
+ config: DebugLogConfig,
146
+ classes: CSSClasses
147
+ ): DebugLogSetup {
148
+ // Create log container
149
+ const logContainer = document.createElement('div');
150
+ logContainer.className = classes.debugLog;
151
+ container.appendChild(logContainer);
152
+
153
+ // Use shared interface creation
154
+ const debugLog = createDebugLogInterface(logContainer, config, classes);
155
+
156
+ return { debugLog, logContainer };
157
+ }
158
+
159
+ export interface HoverEnlargeConfig {
160
+ /** The log container element (.blork-tabs-debug-log) to enlarge */
161
+ logContainer: HTMLElement;
162
+ /** Hover delay in ms (0 = disable) */
163
+ hoverDelay: number;
164
+ /** Container to append backdrop to */
165
+ backdropContainer: HTMLElement;
166
+ /** CSS classes */
167
+ classes: CSSClasses;
168
+ /** Called when hovering starts */
169
+ onHoverStart?: () => void;
170
+ /** Called when hovering ends (without enlarging) */
171
+ onHoverEnd?: () => void;
172
+ /** Called when enlarged view is closed */
173
+ onClose?: () => void;
174
+ }
175
+
176
+ /**
177
+ * Set up hover-to-enlarge behavior for a debug log container.
178
+ * Works the same for both standalone debug panels and embedded logs.
179
+ */
180
+ export function setupHoverEnlarge(config: HoverEnlargeConfig): void {
181
+ const { logContainer, hoverDelay, backdropContainer, classes, onHoverStart, onHoverEnd, onClose } = config;
182
+
183
+ // Add debug panel class for hover border effect
184
+ logContainer.classList.add(classes.debugPanel);
185
+
186
+ // Create close button inside the log container
187
+ const closeBtn = document.createElement('button');
188
+ closeBtn.className = classes.debugClearButton;
189
+ closeBtn.textContent = '×';
190
+ closeBtn.title = 'Close enlarged view';
191
+ logContainer.appendChild(closeBtn);
192
+
193
+ let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
194
+ let isEnlarged = false;
195
+ let backdrop: HTMLDivElement | null = null;
196
+ let originalParent: HTMLElement | null = null;
197
+ let placeholder: Comment | null = null;
198
+
199
+ const closeEnlarged = () => {
200
+ if (!isEnlarged) return;
201
+ isEnlarged = false;
202
+ logContainer.classList.remove(classes.debugPanelEnlarged);
203
+
204
+ // Move log container back to original parent
205
+ if (originalParent && placeholder) {
206
+ originalParent.insertBefore(logContainer, placeholder);
207
+ placeholder.remove();
208
+ placeholder = null;
209
+ originalParent = null;
210
+ }
211
+
212
+ if (backdrop) {
213
+ backdrop.remove();
214
+ backdrop = null;
215
+ }
216
+ onClose?.();
217
+ };
218
+
219
+ const openEnlarged = () => {
220
+ if (isEnlarged) return;
221
+ isEnlarged = true;
222
+
223
+ // Create backdrop
224
+ backdrop = document.createElement('div');
225
+ backdrop.className = classes.debugBackdrop;
226
+ backdropContainer.appendChild(backdrop);
227
+
228
+ // Click backdrop to close
229
+ backdrop.addEventListener('click', closeEnlarged);
230
+
231
+ // Move log container to body to escape parent stacking context
232
+ originalParent = logContainer.parentElement;
233
+ placeholder = document.createComment('debug-log-placeholder');
234
+ originalParent?.insertBefore(placeholder, logContainer);
235
+ backdropContainer.appendChild(logContainer);
236
+
237
+ // Add enlarged class to log container
238
+ logContainer.classList.add(classes.debugPanelEnlarged);
239
+ };
240
+
241
+ // Hover to start enlarge timer
242
+ logContainer.addEventListener('mouseenter', () => {
243
+ onHoverStart?.();
244
+ if (isEnlarged) return;
245
+ if (hoverDelay > 0) {
246
+ hoverTimeout = setTimeout(() => {
247
+ openEnlarged();
248
+ }, hoverDelay);
249
+ }
250
+ });
251
+
252
+ // Cancel timer if mouse leaves
253
+ logContainer.addEventListener('mouseleave', () => {
254
+ if (hoverTimeout) {
255
+ clearTimeout(hoverTimeout);
256
+ hoverTimeout = null;
257
+ }
258
+ if (!isEnlarged) {
259
+ onHoverEnd?.();
260
+ }
261
+ });
262
+
263
+ // Close button closes enlarged view
264
+ closeBtn.addEventListener('click', (e) => {
265
+ e.stopPropagation();
266
+ closeEnlarged();
267
+ });
98
268
  }
package/src/TabManager.ts CHANGED
@@ -20,8 +20,10 @@ import type {
20
20
  Position,
21
21
  DebugPanelConfig,
22
22
  DebugPanel,
23
+ DebugLogConfig,
24
+ DebugLog,
23
25
  } from './types';
24
- import { createDebugPanelContent, createDebugPanelInterface, DebugPanelElements } from './DebugPanel';
26
+ import { createDebugPanelContent, createDebugPanelInterface, createDebugLog, setupHoverEnlarge, DebugPanelElements } from './DebugPanel';
25
27
  import { createPanelState, toggleCollapse, setPanelPosition } from './Panel';
26
28
  import { getConnectedGroup, detachFromGroup, updateSnappedPositions, snapPanels } from './SnapChain';
27
29
  import { DragManager } from './DragManager';
@@ -253,80 +255,45 @@ export class TabManager {
253
255
 
254
256
  const state = this.addPanel(panelConfig);
255
257
 
256
- // Add debug panel class for hover effects
257
- state.element.classList.add(this.classes.debugPanel);
258
-
259
- // Add close button (×) to header - used to close enlarged view
260
- const closeBtn = document.createElement('button');
261
- closeBtn.className = this.classes.debugClearButton;
262
- closeBtn.textContent = '×';
263
- closeBtn.title = 'Close enlarged view';
264
- if (state.collapseButton) {
265
- state.collapseButton.parentElement?.insertBefore(closeBtn, state.collapseButton);
266
- }
267
- elements.clearButton = closeBtn;
268
-
269
258
  this.debugPanelElements.set(state.id, elements);
270
259
 
271
260
  const debugPanel = createDebugPanelInterface(state, elements, config, this.classes);
272
261
 
273
- // Set up hover-to-enlarge behavior (5 second delay)
274
- let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
275
- let isEnlarged = false;
276
- let backdrop: HTMLDivElement | null = null;
277
- const enlargedClass = this.classes.debugPanelEnlarged;
278
- const backdropClass = this.classes.debugBackdrop;
279
-
280
- const closeEnlarged = () => {
281
- if (!isEnlarged) return;
282
- isEnlarged = false;
283
- state.element.classList.remove(enlargedClass);
284
- if (backdrop) {
285
- backdrop.remove();
286
- backdrop = null;
287
- }
288
- };
289
-
290
- const openEnlarged = () => {
291
- if (isEnlarged) return;
292
- isEnlarged = true;
293
-
294
- // Create backdrop
295
- backdrop = document.createElement('div');
296
- backdrop.className = backdropClass;
297
- this.config.container.appendChild(backdrop);
298
-
299
- // Click backdrop to close
300
- backdrop.addEventListener('click', closeEnlarged);
301
-
302
- // Add enlarged class
303
- state.element.classList.add(enlargedClass);
304
- };
305
-
306
- // Hover to start enlarge timer (only when not already enlarged)
307
- state.element.addEventListener('mouseenter', () => {
308
- if (isEnlarged) return;
309
- hoverTimeout = setTimeout(() => {
310
- openEnlarged();
311
- }, 5000);
312
- });
262
+ // Set up hover-to-enlarge behavior on the log container (shared with embedded logs)
263
+ const hoverDelay = config.hoverDelay ?? 5000;
264
+ if (hoverDelay > 0) {
265
+ setupHoverEnlarge({
266
+ logContainer: elements.logContainer,
267
+ hoverDelay,
268
+ backdropContainer: this.config.container,
269
+ classes: this.classes,
270
+ onHoverStart: () => this.autoHideManager.pauseTimer(state.id),
271
+ onHoverEnd: () => this.autoHideManager.resumeTimer(state.id),
272
+ onClose: () => this.autoHideManager.resumeTimer(state.id),
273
+ });
274
+ }
313
275
 
314
- // Cancel timer if mouse leaves before 5 seconds (but don't close if already enlarged)
315
- state.element.addEventListener('mouseleave', () => {
316
- if (hoverTimeout) {
317
- clearTimeout(hoverTimeout);
318
- hoverTimeout = null;
319
- }
320
- // Don't close on mouseleave - only × or backdrop click closes
321
- });
276
+ return debugPanel;
277
+ }
322
278
 
323
- // × button closes enlarged view
324
- closeBtn.addEventListener('click', (e) => {
325
- e.stopPropagation();
326
- closeEnlarged();
327
- });
279
+ /**
280
+ * Create an embeddable debug log in any container element
281
+ */
282
+ createDebugLog(container: HTMLElement, config: DebugLogConfig = {}): DebugLog {
283
+ const { debugLog, logContainer } = createDebugLog(container, config, this.classes);
284
+
285
+ // Set up hover-to-enlarge behavior using shared helper (same as standalone panel)
286
+ const hoverDelay = config.hoverDelay ?? 5000;
287
+ if (hoverDelay > 0) {
288
+ setupHoverEnlarge({
289
+ logContainer,
290
+ hoverDelay,
291
+ backdropContainer: this.config.container,
292
+ classes: this.classes,
293
+ });
294
+ }
328
295
 
329
- return debugPanel;
296
+ return debugLog;
330
297
  }
331
298
 
332
299
  /**
package/src/index.ts CHANGED
@@ -42,7 +42,7 @@ export { AnchorManager, getDefaultAnchorConfigs, createPresetAnchor } from './An
42
42
  export { DragManager } from './DragManager';
43
43
  export { SnapPreview } from './SnapPreview';
44
44
  export { AutoHideManager } from './AutoHideManager';
45
- export { createDebugPanelContent, createDebugPanelInterface } from './DebugPanel';
45
+ export { createDebugPanelContent, createDebugPanelInterface, createDebugLog, setupHoverEnlarge } from './DebugPanel';
46
46
  export type { AutoHideCallbacks } from './AutoHideManager';
47
47
  export {
48
48
  createPanelElement,
@@ -113,5 +113,7 @@ export type {
113
113
 
114
114
  // Debug Panel
115
115
  DebugPanel,
116
+ DebugLog,
117
+ DebugLogConfig,
116
118
  DebugLogLevel,
117
119
  } from './types';
package/src/types.ts CHANGED
@@ -370,6 +370,20 @@ export interface DebugPanelConfig extends Omit<PanelConfig, 'content'> {
370
370
  maxEntries?: number;
371
371
  /** Show timestamps on entries (default: false) */
372
372
  showTimestamps?: boolean;
373
+ /** Milliseconds to hover before enlarging (default: 5000, 0 = disable) */
374
+ hoverDelay?: number;
375
+ }
376
+
377
+ /**
378
+ * Configuration for creating an embeddable debug log
379
+ */
380
+ export interface DebugLogConfig {
381
+ /** Maximum log entries before oldest are removed (default: 50) */
382
+ maxEntries?: number;
383
+ /** Show timestamps on entries (default: false) */
384
+ showTimestamps?: boolean;
385
+ /** Milliseconds to hover before enlarging (default: 5000, 0 = disable) */
386
+ hoverDelay?: number;
373
387
  }
374
388
 
375
389
  /**
@@ -394,3 +408,19 @@ export interface DebugPanel {
394
408
  /** The underlying panel state */
395
409
  panel: PanelState;
396
410
  }
411
+
412
+ /**
413
+ * Interface for an embeddable debug log (without panel)
414
+ */
415
+ export interface DebugLog {
416
+ /** Log an event (alias for info) */
417
+ log(eventName: string, data?: Record<string, unknown>): void;
418
+ /** Log an info event (blue) */
419
+ info(eventName: string, data?: Record<string, unknown>): void;
420
+ /** Log a warning event (yellow) */
421
+ warn(eventName: string, data?: Record<string, unknown>): void;
422
+ /** Log an error event (red) */
423
+ error(eventName: string, data?: Record<string, unknown>): void;
424
+ /** Clear all log entries */
425
+ clear(): void;
426
+ }