@blorkfield/blork-tabs 0.1.3
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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/index.cjs +1194 -0
- package/dist/index.d.cts +669 -0
- package/dist/index.d.ts +669 -0
- package/dist/index.js +1143 -0
- package/dist/styles.css +186 -0
- package/package.json +62 -0
- package/src/AnchorManager.ts +395 -0
- package/src/DragManager.ts +251 -0
- package/src/Panel.ts +211 -0
- package/src/SnapChain.ts +289 -0
- package/src/SnapPreview.ts +91 -0
- package/src/TabManager.ts +507 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +105 -0
- package/src/types.ts +320 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blorkfield/blork-tabs - TabManager
|
|
3
|
+
* Main orchestrator for the tab/panel management system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
TabManagerConfig,
|
|
8
|
+
ResolvedTabManagerConfig,
|
|
9
|
+
PanelConfig,
|
|
10
|
+
PanelState,
|
|
11
|
+
AnchorConfig,
|
|
12
|
+
AnchorState,
|
|
13
|
+
AnchorPreset,
|
|
14
|
+
CSSClasses,
|
|
15
|
+
TabManagerEvents,
|
|
16
|
+
EventListener,
|
|
17
|
+
SnapTarget,
|
|
18
|
+
AnchorSnapResult,
|
|
19
|
+
DragState,
|
|
20
|
+
Position,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { createPanelState, toggleCollapse, setPanelPosition } from './Panel';
|
|
23
|
+
import { getConnectedGroup, detachFromGroup, updateSnappedPositions, snapPanels } from './SnapChain';
|
|
24
|
+
import { DragManager } from './DragManager';
|
|
25
|
+
import { AnchorManager } from './AnchorManager';
|
|
26
|
+
import { SnapPreview } from './SnapPreview';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default configuration values
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_CONFIG: ResolvedTabManagerConfig = {
|
|
32
|
+
snapThreshold: 50,
|
|
33
|
+
panelGap: 0,
|
|
34
|
+
panelMargin: 16,
|
|
35
|
+
anchorThreshold: 80,
|
|
36
|
+
defaultPanelWidth: 300,
|
|
37
|
+
container: document.body,
|
|
38
|
+
initializeDefaultAnchors: true,
|
|
39
|
+
classPrefix: 'blork-tabs',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate CSS class names from prefix
|
|
44
|
+
*/
|
|
45
|
+
function generateClasses(prefix: string): CSSClasses {
|
|
46
|
+
return {
|
|
47
|
+
panel: `${prefix}-panel`,
|
|
48
|
+
panelHeader: `${prefix}-header`,
|
|
49
|
+
panelTitle: `${prefix}-title`,
|
|
50
|
+
panelContent: `${prefix}-content`,
|
|
51
|
+
panelContentCollapsed: `${prefix}-content-collapsed`,
|
|
52
|
+
detachGrip: `${prefix}-detach-grip`,
|
|
53
|
+
collapseButton: `${prefix}-collapse-btn`,
|
|
54
|
+
snapPreview: `${prefix}-snap-preview`,
|
|
55
|
+
snapPreviewVisible: `${prefix}-snap-preview-visible`,
|
|
56
|
+
anchorIndicator: `${prefix}-anchor-indicator`,
|
|
57
|
+
anchorIndicatorVisible: `${prefix}-anchor-indicator-visible`,
|
|
58
|
+
anchorIndicatorActive: `${prefix}-anchor-indicator-active`,
|
|
59
|
+
dragging: `${prefix}-dragging`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main TabManager class - orchestrates all panel, snap, and anchor functionality
|
|
65
|
+
*/
|
|
66
|
+
export class TabManager {
|
|
67
|
+
private config: ResolvedTabManagerConfig;
|
|
68
|
+
private classes: CSSClasses;
|
|
69
|
+
private panels: Map<string, PanelState> = new Map();
|
|
70
|
+
private dragManager: DragManager;
|
|
71
|
+
private anchorManager: AnchorManager;
|
|
72
|
+
private snapPreview: SnapPreview;
|
|
73
|
+
private eventListeners: Map<string, Set<EventListener<unknown>>> = new Map();
|
|
74
|
+
|
|
75
|
+
constructor(userConfig: TabManagerConfig = {}) {
|
|
76
|
+
// Merge user config with defaults
|
|
77
|
+
this.config = {
|
|
78
|
+
...DEFAULT_CONFIG,
|
|
79
|
+
...userConfig,
|
|
80
|
+
container: userConfig.container ?? document.body,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.classes = generateClasses(this.config.classPrefix);
|
|
84
|
+
|
|
85
|
+
// Initialize managers
|
|
86
|
+
this.anchorManager = new AnchorManager(this.config, this.classes);
|
|
87
|
+
this.snapPreview = new SnapPreview(
|
|
88
|
+
this.config.container,
|
|
89
|
+
this.classes,
|
|
90
|
+
this.panels
|
|
91
|
+
);
|
|
92
|
+
this.dragManager = new DragManager(
|
|
93
|
+
this.panels,
|
|
94
|
+
this.config,
|
|
95
|
+
{
|
|
96
|
+
onDragStart: this.handleDragStart.bind(this),
|
|
97
|
+
onDragMove: this.handleDragMove.bind(this),
|
|
98
|
+
onDragEnd: this.handleDragEnd.bind(this),
|
|
99
|
+
findAnchorTarget: (panels) => this.anchorManager.findNearestAnchor(panels),
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Initialize default anchors if configured
|
|
104
|
+
if (this.config.initializeDefaultAnchors) {
|
|
105
|
+
this.anchorManager.addDefaultAnchors();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ==================== Panel Management ====================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add a new panel
|
|
113
|
+
*/
|
|
114
|
+
addPanel(panelConfig: PanelConfig): PanelState {
|
|
115
|
+
const state = createPanelState(panelConfig, this.classes);
|
|
116
|
+
|
|
117
|
+
// Add to container if new element
|
|
118
|
+
if (!panelConfig.element && !this.config.container.contains(state.element)) {
|
|
119
|
+
this.config.container.appendChild(state.element);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Set up event handlers
|
|
123
|
+
this.setupPanelEvents(state);
|
|
124
|
+
|
|
125
|
+
// Store panel
|
|
126
|
+
this.panels.set(state.id, state);
|
|
127
|
+
|
|
128
|
+
// Set initial position if provided
|
|
129
|
+
if (panelConfig.initialPosition) {
|
|
130
|
+
setPanelPosition(state, panelConfig.initialPosition.x, panelConfig.initialPosition.y);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.emit('panel:added', { panel: state });
|
|
134
|
+
|
|
135
|
+
return state;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register an existing panel element
|
|
140
|
+
*/
|
|
141
|
+
registerPanel(
|
|
142
|
+
id: string,
|
|
143
|
+
element: HTMLDivElement,
|
|
144
|
+
options: {
|
|
145
|
+
dragHandle?: HTMLDivElement;
|
|
146
|
+
collapseButton?: HTMLButtonElement;
|
|
147
|
+
contentWrapper?: HTMLDivElement;
|
|
148
|
+
detachGrip?: HTMLDivElement;
|
|
149
|
+
startCollapsed?: boolean;
|
|
150
|
+
} = {}
|
|
151
|
+
): PanelState {
|
|
152
|
+
return this.addPanel({
|
|
153
|
+
id,
|
|
154
|
+
element,
|
|
155
|
+
dragHandle: options.dragHandle,
|
|
156
|
+
collapseButton: options.collapseButton,
|
|
157
|
+
contentWrapper: options.contentWrapper,
|
|
158
|
+
detachGrip: options.detachGrip,
|
|
159
|
+
startCollapsed: options.startCollapsed,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Remove a panel
|
|
165
|
+
*/
|
|
166
|
+
removePanel(id: string): boolean {
|
|
167
|
+
const panel = this.panels.get(id);
|
|
168
|
+
if (!panel) return false;
|
|
169
|
+
|
|
170
|
+
// Detach from any snap chain
|
|
171
|
+
detachFromGroup(panel, this.panels);
|
|
172
|
+
|
|
173
|
+
// Remove from DOM if we created it
|
|
174
|
+
if (!panel.config.element) {
|
|
175
|
+
panel.element.remove();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.panels.delete(id);
|
|
179
|
+
this.emit('panel:removed', { panelId: id });
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get a panel by ID
|
|
186
|
+
*/
|
|
187
|
+
getPanel(id: string): PanelState | undefined {
|
|
188
|
+
return this.panels.get(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all panels
|
|
193
|
+
*/
|
|
194
|
+
getAllPanels(): PanelState[] {
|
|
195
|
+
return Array.from(this.panels.values());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Set up event handlers for a panel
|
|
200
|
+
*/
|
|
201
|
+
private setupPanelEvents(state: PanelState): void {
|
|
202
|
+
// Collapse button
|
|
203
|
+
if (state.collapseButton) {
|
|
204
|
+
state.collapseButton.addEventListener('click', () => {
|
|
205
|
+
const newState = toggleCollapse(state, this.classes);
|
|
206
|
+
updateSnappedPositions(this.panels, this.config);
|
|
207
|
+
this.emit('panel:collapse', { panel: state, isCollapsed: newState });
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Detach grip - single panel drag
|
|
212
|
+
if (state.detachGrip) {
|
|
213
|
+
state.detachGrip.addEventListener('mousedown', (e) => {
|
|
214
|
+
this.dragManager.startDrag(e, state, 'single');
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Main drag handle - group drag
|
|
219
|
+
state.dragHandle.addEventListener('mousedown', (e) => {
|
|
220
|
+
// Ignore if clicking on collapse button or detach grip
|
|
221
|
+
if (
|
|
222
|
+
e.target === state.collapseButton ||
|
|
223
|
+
e.target === state.detachGrip
|
|
224
|
+
) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.dragManager.startDrag(e, state, 'group');
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ==================== Snap Chain Management ====================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get all panels in the same snap chain as the given panel
|
|
235
|
+
*/
|
|
236
|
+
getSnapChain(panelId: string): PanelState[] {
|
|
237
|
+
const panel = this.panels.get(panelId);
|
|
238
|
+
if (!panel) return [];
|
|
239
|
+
return getConnectedGroup(panel, this.panels);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Manually snap two panels together
|
|
244
|
+
*/
|
|
245
|
+
snap(leftPanelId: string, rightPanelId: string): boolean {
|
|
246
|
+
const leftPanel = this.panels.get(leftPanelId);
|
|
247
|
+
const rightPanel = this.panels.get(rightPanelId);
|
|
248
|
+
|
|
249
|
+
if (!leftPanel || !rightPanel) return false;
|
|
250
|
+
|
|
251
|
+
snapPanels(leftPanel, rightPanel);
|
|
252
|
+
updateSnappedPositions(this.panels, this.config);
|
|
253
|
+
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Detach a panel from its snap chain
|
|
259
|
+
*/
|
|
260
|
+
detach(panelId: string): boolean {
|
|
261
|
+
const panel = this.panels.get(panelId);
|
|
262
|
+
if (!panel) return false;
|
|
263
|
+
|
|
264
|
+
const previousGroup = getConnectedGroup(panel, this.panels);
|
|
265
|
+
detachFromGroup(panel, this.panels);
|
|
266
|
+
|
|
267
|
+
this.emit('panel:detached', { panel, previousGroup });
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update snapped positions (call after collapse/expand or resize)
|
|
274
|
+
*/
|
|
275
|
+
updatePositions(): void {
|
|
276
|
+
updateSnappedPositions(this.panels, this.config);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ==================== Anchor Management ====================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Add a custom anchor
|
|
283
|
+
*/
|
|
284
|
+
addAnchor(config: AnchorConfig): AnchorState {
|
|
285
|
+
return this.anchorManager.addAnchor(config);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Add a preset anchor
|
|
290
|
+
*/
|
|
291
|
+
addPresetAnchor(preset: AnchorPreset): AnchorState {
|
|
292
|
+
return this.anchorManager.addPresetAnchor(preset);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Remove an anchor
|
|
297
|
+
*/
|
|
298
|
+
removeAnchor(id: string): boolean {
|
|
299
|
+
return this.anchorManager.removeAnchor(id);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get all anchors
|
|
304
|
+
*/
|
|
305
|
+
getAnchors(): AnchorState[] {
|
|
306
|
+
return this.anchorManager.getAnchors();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ==================== Drag Callbacks ====================
|
|
310
|
+
|
|
311
|
+
private handleDragStart(state: DragState): void {
|
|
312
|
+
this.anchorManager.showIndicators(null);
|
|
313
|
+
this.emit('drag:start', {
|
|
314
|
+
panel: state.grabbedPanel,
|
|
315
|
+
mode: state.mode,
|
|
316
|
+
movingPanels: state.movingPanels,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private handleDragMove(
|
|
321
|
+
state: DragState,
|
|
322
|
+
position: Position,
|
|
323
|
+
snapTarget: SnapTarget | null,
|
|
324
|
+
anchorResult: AnchorSnapResult | null
|
|
325
|
+
): void {
|
|
326
|
+
// Update snap preview
|
|
327
|
+
this.snapPreview.update(snapTarget);
|
|
328
|
+
|
|
329
|
+
// Update anchor indicators
|
|
330
|
+
this.anchorManager.showIndicators(anchorResult?.anchor ?? null);
|
|
331
|
+
|
|
332
|
+
this.emit('drag:move', {
|
|
333
|
+
panel: state.grabbedPanel,
|
|
334
|
+
position,
|
|
335
|
+
snapTarget,
|
|
336
|
+
anchorTarget: anchorResult,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private handleDragEnd(
|
|
341
|
+
state: DragState,
|
|
342
|
+
snapTarget: SnapTarget | null,
|
|
343
|
+
anchorResult: AnchorSnapResult | null
|
|
344
|
+
): void {
|
|
345
|
+
this.snapPreview.hide();
|
|
346
|
+
this.anchorManager.hideIndicators();
|
|
347
|
+
|
|
348
|
+
const rect = state.grabbedPanel.element.getBoundingClientRect();
|
|
349
|
+
|
|
350
|
+
if (snapTarget) {
|
|
351
|
+
const targetPanel = this.panels.get(snapTarget.targetId);
|
|
352
|
+
if (targetPanel) {
|
|
353
|
+
this.emit('snap:panel', {
|
|
354
|
+
movingPanels: state.movingPanels,
|
|
355
|
+
targetPanel,
|
|
356
|
+
side: snapTarget.side,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} else if (anchorResult) {
|
|
360
|
+
this.emit('snap:anchor', {
|
|
361
|
+
movingPanels: state.movingPanels,
|
|
362
|
+
anchor: anchorResult.anchor,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.emit('drag:end', {
|
|
367
|
+
panel: state.grabbedPanel,
|
|
368
|
+
finalPosition: { x: rect.left, y: rect.top },
|
|
369
|
+
snappedToPanel: snapTarget !== null,
|
|
370
|
+
snappedToAnchor: anchorResult !== null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ==================== Event System ====================
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Subscribe to an event
|
|
378
|
+
*/
|
|
379
|
+
on<K extends keyof TabManagerEvents>(
|
|
380
|
+
event: K,
|
|
381
|
+
listener: EventListener<TabManagerEvents[K]>
|
|
382
|
+
): () => void {
|
|
383
|
+
if (!this.eventListeners.has(event)) {
|
|
384
|
+
this.eventListeners.set(event, new Set());
|
|
385
|
+
}
|
|
386
|
+
this.eventListeners.get(event)!.add(listener as EventListener<unknown>);
|
|
387
|
+
|
|
388
|
+
// Return unsubscribe function
|
|
389
|
+
return () => this.off(event, listener);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Unsubscribe from an event
|
|
394
|
+
*/
|
|
395
|
+
off<K extends keyof TabManagerEvents>(
|
|
396
|
+
event: K,
|
|
397
|
+
listener: EventListener<TabManagerEvents[K]>
|
|
398
|
+
): void {
|
|
399
|
+
this.eventListeners.get(event)?.delete(listener as EventListener<unknown>);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Emit an event
|
|
404
|
+
*/
|
|
405
|
+
private emit<K extends keyof TabManagerEvents>(
|
|
406
|
+
event: K,
|
|
407
|
+
data: TabManagerEvents[K]
|
|
408
|
+
): void {
|
|
409
|
+
const listeners = this.eventListeners.get(event);
|
|
410
|
+
if (listeners) {
|
|
411
|
+
for (const listener of listeners) {
|
|
412
|
+
listener(data);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ==================== Positioning ====================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Position panels in a row from right edge
|
|
421
|
+
*/
|
|
422
|
+
positionPanelsFromRight(panelIds: string[], gap = 0): void {
|
|
423
|
+
let rightEdge = window.innerWidth - this.config.panelMargin;
|
|
424
|
+
|
|
425
|
+
for (const id of panelIds) {
|
|
426
|
+
const state = this.panels.get(id);
|
|
427
|
+
if (!state) continue;
|
|
428
|
+
|
|
429
|
+
const width = state.element.offsetWidth;
|
|
430
|
+
setPanelPosition(state, rightEdge - width, this.config.panelMargin);
|
|
431
|
+
rightEdge -= width + gap;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Position panels in a row from left edge
|
|
437
|
+
*/
|
|
438
|
+
positionPanelsFromLeft(panelIds: string[], gap = 0): void {
|
|
439
|
+
let leftEdge = this.config.panelMargin;
|
|
440
|
+
|
|
441
|
+
for (const id of panelIds) {
|
|
442
|
+
const state = this.panels.get(id);
|
|
443
|
+
if (!state) continue;
|
|
444
|
+
|
|
445
|
+
setPanelPosition(state, leftEdge, this.config.panelMargin);
|
|
446
|
+
leftEdge += state.element.offsetWidth + gap;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Set up initial snap chain for a list of panels (left to right)
|
|
452
|
+
*/
|
|
453
|
+
createSnapChain(panelIds: string[]): void {
|
|
454
|
+
for (let i = 0; i < panelIds.length - 1; i++) {
|
|
455
|
+
const leftPanel = this.panels.get(panelIds[i]);
|
|
456
|
+
const rightPanel = this.panels.get(panelIds[i + 1]);
|
|
457
|
+
|
|
458
|
+
if (leftPanel && rightPanel) {
|
|
459
|
+
snapPanels(leftPanel, rightPanel);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ==================== Configuration ====================
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get the current configuration
|
|
468
|
+
*/
|
|
469
|
+
getConfig(): ResolvedTabManagerConfig {
|
|
470
|
+
return { ...this.config };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get the CSS classes used
|
|
475
|
+
*/
|
|
476
|
+
getClasses(): CSSClasses {
|
|
477
|
+
return { ...this.classes };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if dragging is currently active
|
|
482
|
+
*/
|
|
483
|
+
isDragging(): boolean {
|
|
484
|
+
return this.dragManager.isActive();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ==================== Cleanup ====================
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Destroy the TabManager and clean up resources
|
|
491
|
+
*/
|
|
492
|
+
destroy(): void {
|
|
493
|
+
this.dragManager.destroy();
|
|
494
|
+
this.anchorManager.destroy();
|
|
495
|
+
this.snapPreview.destroy();
|
|
496
|
+
|
|
497
|
+
// Remove panels we created
|
|
498
|
+
for (const panel of this.panels.values()) {
|
|
499
|
+
if (!panel.config.element) {
|
|
500
|
+
panel.element.remove();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this.panels.clear();
|
|
505
|
+
this.eventListeners.clear();
|
|
506
|
+
}
|
|
507
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blorkfield/blork-tabs
|
|
3
|
+
* A framework-agnostic tab/panel management system with snapping and docking
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { TabManager } from '@blorkfield/blork-tabs';
|
|
8
|
+
* import '@blorkfield/blork-tabs/styles.css';
|
|
9
|
+
*
|
|
10
|
+
* const manager = new TabManager({
|
|
11
|
+
* snapThreshold: 50,
|
|
12
|
+
* panelGap: 0,
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // Create panels programmatically
|
|
16
|
+
* manager.addPanel({
|
|
17
|
+
* id: 'settings',
|
|
18
|
+
* title: 'Settings',
|
|
19
|
+
* content: '<div>Settings content</div>',
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Or register existing DOM elements
|
|
23
|
+
* manager.registerPanel('my-panel', document.getElementById('my-panel'), {
|
|
24
|
+
* dragHandle: document.getElementById('my-panel-header'),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Set up snap chains
|
|
28
|
+
* manager.createSnapChain(['panel1', 'panel2', 'panel3']);
|
|
29
|
+
*
|
|
30
|
+
* // Listen to events
|
|
31
|
+
* manager.on('snap:panel', ({ movingPanels, targetPanel }) => {
|
|
32
|
+
* console.log('Panels snapped!');
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Main class export
|
|
38
|
+
export { TabManager } from './TabManager';
|
|
39
|
+
|
|
40
|
+
// Sub-module exports for advanced usage
|
|
41
|
+
export { AnchorManager, getDefaultAnchorConfigs, createPresetAnchor } from './AnchorManager';
|
|
42
|
+
export { DragManager } from './DragManager';
|
|
43
|
+
export { SnapPreview } from './SnapPreview';
|
|
44
|
+
export {
|
|
45
|
+
createPanelElement,
|
|
46
|
+
createPanelState,
|
|
47
|
+
toggleCollapse,
|
|
48
|
+
setPanelPosition,
|
|
49
|
+
getPanelPosition,
|
|
50
|
+
getPanelDimensions,
|
|
51
|
+
setPanelZIndex,
|
|
52
|
+
getDefaultZIndex,
|
|
53
|
+
getDragZIndex,
|
|
54
|
+
} from './Panel';
|
|
55
|
+
export {
|
|
56
|
+
getConnectedGroup,
|
|
57
|
+
detachFromGroup,
|
|
58
|
+
findSnapTarget,
|
|
59
|
+
snapPanelsToTarget,
|
|
60
|
+
updateSnappedPositions,
|
|
61
|
+
getLeftmostPanel,
|
|
62
|
+
getRightmostPanel,
|
|
63
|
+
areInSameChain,
|
|
64
|
+
snapPanels,
|
|
65
|
+
unsnap,
|
|
66
|
+
} from './SnapChain';
|
|
67
|
+
|
|
68
|
+
// Type exports
|
|
69
|
+
export type {
|
|
70
|
+
// Configuration
|
|
71
|
+
TabManagerConfig,
|
|
72
|
+
ResolvedTabManagerConfig,
|
|
73
|
+
PanelConfig,
|
|
74
|
+
AnchorConfig,
|
|
75
|
+
AnchorPreset,
|
|
76
|
+
|
|
77
|
+
// State
|
|
78
|
+
PanelState,
|
|
79
|
+
DragState,
|
|
80
|
+
DragMode,
|
|
81
|
+
AnchorState,
|
|
82
|
+
|
|
83
|
+
// Results
|
|
84
|
+
SnapTarget,
|
|
85
|
+
SnapSide,
|
|
86
|
+
AnchorSnapResult,
|
|
87
|
+
Position,
|
|
88
|
+
Bounds,
|
|
89
|
+
|
|
90
|
+
// Events
|
|
91
|
+
TabManagerEvents,
|
|
92
|
+
PanelAddedEvent,
|
|
93
|
+
PanelRemovedEvent,
|
|
94
|
+
DragStartEvent,
|
|
95
|
+
DragMoveEvent,
|
|
96
|
+
DragEndEvent,
|
|
97
|
+
PanelSnapEvent,
|
|
98
|
+
AnchorSnapEvent,
|
|
99
|
+
PanelDetachedEvent,
|
|
100
|
+
PanelCollapseEvent,
|
|
101
|
+
EventListener,
|
|
102
|
+
|
|
103
|
+
// CSS
|
|
104
|
+
CSSClasses,
|
|
105
|
+
} from './types';
|