@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/dist/index.cjs ADDED
@@ -0,0 +1,1194 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AnchorManager: () => AnchorManager,
24
+ DragManager: () => DragManager,
25
+ SnapPreview: () => SnapPreview,
26
+ TabManager: () => TabManager,
27
+ areInSameChain: () => areInSameChain,
28
+ createPanelElement: () => createPanelElement,
29
+ createPanelState: () => createPanelState,
30
+ createPresetAnchor: () => createPresetAnchor,
31
+ detachFromGroup: () => detachFromGroup,
32
+ findSnapTarget: () => findSnapTarget,
33
+ getConnectedGroup: () => getConnectedGroup,
34
+ getDefaultAnchorConfigs: () => getDefaultAnchorConfigs,
35
+ getDefaultZIndex: () => getDefaultZIndex,
36
+ getDragZIndex: () => getDragZIndex,
37
+ getLeftmostPanel: () => getLeftmostPanel,
38
+ getPanelDimensions: () => getPanelDimensions,
39
+ getPanelPosition: () => getPanelPosition,
40
+ getRightmostPanel: () => getRightmostPanel,
41
+ setPanelPosition: () => setPanelPosition,
42
+ setPanelZIndex: () => setPanelZIndex,
43
+ snapPanels: () => snapPanels,
44
+ snapPanelsToTarget: () => snapPanelsToTarget,
45
+ toggleCollapse: () => toggleCollapse,
46
+ unsnap: () => unsnap,
47
+ updateSnappedPositions: () => updateSnappedPositions
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
51
+ // src/Panel.ts
52
+ function createPanelElement(config, classes) {
53
+ const element = document.createElement("div");
54
+ element.className = classes.panel;
55
+ element.id = `${config.id}-panel`;
56
+ element.style.width = `${config.width ?? 300}px`;
57
+ element.style.zIndex = `${config.zIndex ?? 1e3}`;
58
+ const header = document.createElement("div");
59
+ header.className = classes.panelHeader;
60
+ header.id = `${config.id}-header`;
61
+ let detachGrip = null;
62
+ if (config.detachable !== false) {
63
+ detachGrip = document.createElement("div");
64
+ detachGrip.className = classes.detachGrip;
65
+ detachGrip.id = `${config.id}-detach-grip`;
66
+ header.appendChild(detachGrip);
67
+ }
68
+ if (config.title) {
69
+ const title = document.createElement("span");
70
+ title.className = classes.panelTitle;
71
+ title.textContent = config.title;
72
+ header.appendChild(title);
73
+ }
74
+ let collapseButton = null;
75
+ if (config.collapsible !== false) {
76
+ collapseButton = document.createElement("button");
77
+ collapseButton.className = classes.collapseButton;
78
+ collapseButton.id = `${config.id}-collapse-btn`;
79
+ collapseButton.textContent = config.startCollapsed !== false ? "+" : "\u2212";
80
+ header.appendChild(collapseButton);
81
+ }
82
+ element.appendChild(header);
83
+ const contentWrapper = document.createElement("div");
84
+ contentWrapper.className = classes.panelContent;
85
+ contentWrapper.id = `${config.id}-content`;
86
+ if (config.startCollapsed !== false) {
87
+ contentWrapper.classList.add(classes.panelContentCollapsed);
88
+ }
89
+ if (config.content) {
90
+ if (typeof config.content === "string") {
91
+ contentWrapper.innerHTML = config.content;
92
+ } else {
93
+ contentWrapper.appendChild(config.content);
94
+ }
95
+ }
96
+ element.appendChild(contentWrapper);
97
+ return {
98
+ element,
99
+ dragHandle: header,
100
+ collapseButton,
101
+ contentWrapper,
102
+ detachGrip
103
+ };
104
+ }
105
+ function createPanelState(config, classes) {
106
+ let element;
107
+ let dragHandle;
108
+ let collapseButton;
109
+ let contentWrapper;
110
+ let detachGrip;
111
+ if (config.element) {
112
+ element = config.element;
113
+ dragHandle = config.dragHandle ?? element.querySelector(`.${classes.panelHeader}`);
114
+ collapseButton = config.collapseButton ?? element.querySelector(`.${classes.collapseButton}`);
115
+ contentWrapper = config.contentWrapper ?? element.querySelector(`.${classes.panelContent}`);
116
+ detachGrip = config.detachGrip ?? element.querySelector(`.${classes.detachGrip}`);
117
+ } else {
118
+ const created = createPanelElement(config, classes);
119
+ element = created.element;
120
+ dragHandle = created.dragHandle;
121
+ collapseButton = created.collapseButton;
122
+ contentWrapper = created.contentWrapper;
123
+ detachGrip = created.detachGrip;
124
+ }
125
+ return {
126
+ id: config.id,
127
+ element,
128
+ dragHandle,
129
+ collapseButton,
130
+ contentWrapper,
131
+ detachGrip,
132
+ isCollapsed: config.startCollapsed !== false,
133
+ snappedTo: null,
134
+ snappedFrom: null,
135
+ config
136
+ };
137
+ }
138
+ function toggleCollapse(state, classes, collapsed) {
139
+ const newState = collapsed ?? !state.isCollapsed;
140
+ state.isCollapsed = newState;
141
+ if (newState) {
142
+ state.contentWrapper.classList.add(classes.panelContentCollapsed);
143
+ } else {
144
+ state.contentWrapper.classList.remove(classes.panelContentCollapsed);
145
+ }
146
+ if (state.collapseButton) {
147
+ state.collapseButton.textContent = newState ? "+" : "\u2212";
148
+ }
149
+ return newState;
150
+ }
151
+ function setPanelPosition(state, x, y) {
152
+ state.element.style.left = `${x}px`;
153
+ state.element.style.top = `${y}px`;
154
+ state.element.style.right = "auto";
155
+ }
156
+ function getPanelPosition(state) {
157
+ const rect = state.element.getBoundingClientRect();
158
+ return { x: rect.left, y: rect.top };
159
+ }
160
+ function getPanelDimensions(state) {
161
+ return {
162
+ width: state.element.offsetWidth,
163
+ height: state.element.offsetHeight
164
+ };
165
+ }
166
+ function setPanelZIndex(state, zIndex) {
167
+ state.element.style.zIndex = `${zIndex}`;
168
+ }
169
+ function getDefaultZIndex(state) {
170
+ return state.config.zIndex ?? 1e3;
171
+ }
172
+ function getDragZIndex(state) {
173
+ return state.config.dragZIndex ?? 1002;
174
+ }
175
+
176
+ // src/SnapChain.ts
177
+ function getConnectedGroup(startPanel, panels) {
178
+ const group = [];
179
+ const visited = /* @__PURE__ */ new Set();
180
+ let current = startPanel;
181
+ while (current && !visited.has(current.id)) {
182
+ visited.add(current.id);
183
+ group.unshift(current);
184
+ if (current.snappedFrom) {
185
+ current = panels.get(current.snappedFrom);
186
+ } else {
187
+ break;
188
+ }
189
+ }
190
+ current = startPanel.snappedTo ? panels.get(startPanel.snappedTo) : void 0;
191
+ while (current && !visited.has(current.id)) {
192
+ visited.add(current.id);
193
+ group.push(current);
194
+ if (current.snappedTo) {
195
+ current = panels.get(current.snappedTo);
196
+ } else {
197
+ break;
198
+ }
199
+ }
200
+ return group;
201
+ }
202
+ function detachFromGroup(panel, panels) {
203
+ if (panel.snappedTo) {
204
+ const rightPanel = panels.get(panel.snappedTo);
205
+ if (rightPanel) {
206
+ rightPanel.snappedFrom = null;
207
+ }
208
+ panel.snappedTo = null;
209
+ }
210
+ if (panel.snappedFrom) {
211
+ const leftPanel = panels.get(panel.snappedFrom);
212
+ if (leftPanel) {
213
+ leftPanel.snappedTo = null;
214
+ }
215
+ panel.snappedFrom = null;
216
+ }
217
+ }
218
+ function findSnapTarget(movingPanels, panels, config) {
219
+ if (movingPanels.length === 0) return null;
220
+ const leftmostPanel = movingPanels[0];
221
+ const leftmostRect = leftmostPanel.element.getBoundingClientRect();
222
+ let totalWidth = 0;
223
+ for (const p of movingPanels) {
224
+ totalWidth += p.element.offsetWidth + config.panelGap;
225
+ }
226
+ totalWidth -= config.panelGap;
227
+ const movingIds = new Set(movingPanels.map((p) => p.id));
228
+ const x = leftmostRect.left;
229
+ const y = leftmostRect.top;
230
+ for (const [id, targetState] of panels) {
231
+ if (movingIds.has(id)) continue;
232
+ const targetRect = targetState.element.getBoundingClientRect();
233
+ const verticalOverlap = Math.abs(y - targetRect.top) < config.snapThreshold * 2;
234
+ if (!verticalOverlap) continue;
235
+ const snapToLeftX = targetRect.left - totalWidth - config.panelGap;
236
+ if (Math.abs(x - snapToLeftX) < config.snapThreshold && !targetState.snappedFrom) {
237
+ return { targetId: id, side: "left", x: snapToLeftX, y: targetRect.top };
238
+ }
239
+ const snapToRightX = targetRect.right + config.panelGap;
240
+ if (Math.abs(x - snapToRightX) < config.snapThreshold && !targetState.snappedTo) {
241
+ return { targetId: id, side: "right", x: snapToRightX, y: targetRect.top };
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function snapPanelsToTarget(movingPanels, targetId, side, x, y, panels, config) {
247
+ const targetState = panels.get(targetId);
248
+ if (!targetState) return;
249
+ const leftmostPanel = movingPanels[0];
250
+ const rightmostPanel = movingPanels[movingPanels.length - 1];
251
+ let currentX = x;
252
+ for (const p of movingPanels) {
253
+ p.element.style.left = `${currentX}px`;
254
+ p.element.style.top = `${y}px`;
255
+ currentX += p.element.offsetWidth + config.panelGap;
256
+ }
257
+ if (side === "left") {
258
+ rightmostPanel.snappedTo = targetId;
259
+ targetState.snappedFrom = rightmostPanel.id;
260
+ } else {
261
+ leftmostPanel.snappedFrom = targetId;
262
+ targetState.snappedTo = leftmostPanel.id;
263
+ }
264
+ }
265
+ function updateSnappedPositions(panels, config) {
266
+ let rightmost = null;
267
+ for (const state of panels.values()) {
268
+ if (state.snappedTo === null && state.snappedFrom !== null) {
269
+ rightmost = state;
270
+ break;
271
+ }
272
+ }
273
+ if (!rightmost) {
274
+ for (const state of panels.values()) {
275
+ if (state.snappedFrom !== null) {
276
+ rightmost = state;
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ if (!rightmost) return;
282
+ let current = rightmost;
283
+ while (current && current.snappedFrom) {
284
+ const leftPanel = panels.get(current.snappedFrom);
285
+ if (!leftPanel) break;
286
+ const currentRect = current.element.getBoundingClientRect();
287
+ leftPanel.element.style.left = `${currentRect.left - leftPanel.element.offsetWidth - config.panelGap}px`;
288
+ leftPanel.element.style.top = `${currentRect.top}px`;
289
+ current = leftPanel;
290
+ }
291
+ }
292
+ function getLeftmostPanel(panel, panels) {
293
+ let current = panel;
294
+ while (current.snappedFrom) {
295
+ const left = panels.get(current.snappedFrom);
296
+ if (!left) break;
297
+ current = left;
298
+ }
299
+ return current;
300
+ }
301
+ function getRightmostPanel(panel, panels) {
302
+ let current = panel;
303
+ while (current.snappedTo) {
304
+ const right = panels.get(current.snappedTo);
305
+ if (!right) break;
306
+ current = right;
307
+ }
308
+ return current;
309
+ }
310
+ function areInSameChain(panel1, panel2, panels) {
311
+ const chain = getConnectedGroup(panel1, panels);
312
+ return chain.some((p) => p.id === panel2.id);
313
+ }
314
+ function snapPanels(leftPanel, rightPanel) {
315
+ leftPanel.snappedTo = rightPanel.id;
316
+ rightPanel.snappedFrom = leftPanel.id;
317
+ }
318
+ function unsnap(leftPanel, rightPanel) {
319
+ if (leftPanel.snappedTo === rightPanel.id) {
320
+ leftPanel.snappedTo = null;
321
+ }
322
+ if (rightPanel.snappedFrom === leftPanel.id) {
323
+ rightPanel.snappedFrom = null;
324
+ }
325
+ }
326
+
327
+ // src/DragManager.ts
328
+ var DragManager = class {
329
+ constructor(panels, config, callbacks) {
330
+ this.activeDrag = null;
331
+ this.panels = panels;
332
+ this.config = config;
333
+ this.callbacks = callbacks;
334
+ this.boundMouseMove = this.handleMouseMove.bind(this);
335
+ this.boundMouseUp = this.handleMouseUp.bind(this);
336
+ document.addEventListener("mousemove", this.boundMouseMove);
337
+ document.addEventListener("mouseup", this.boundMouseUp);
338
+ }
339
+ /**
340
+ * Start a drag operation
341
+ */
342
+ startDrag(e, panel, mode) {
343
+ e.preventDefault();
344
+ e.stopPropagation();
345
+ const connectedPanels = getConnectedGroup(panel, this.panels);
346
+ const initialGroupPositions = /* @__PURE__ */ new Map();
347
+ for (const p of connectedPanels) {
348
+ const rect2 = p.element.getBoundingClientRect();
349
+ initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
350
+ }
351
+ let movingPanels;
352
+ if (mode === "single") {
353
+ detachFromGroup(panel, this.panels);
354
+ movingPanels = [panel];
355
+ } else {
356
+ movingPanels = connectedPanels;
357
+ }
358
+ const rect = panel.element.getBoundingClientRect();
359
+ this.activeDrag = {
360
+ grabbedPanel: panel,
361
+ offsetX: e.clientX - rect.left,
362
+ offsetY: e.clientY - rect.top,
363
+ initialGroupPositions,
364
+ movingPanels,
365
+ mode
366
+ };
367
+ for (const p of movingPanels) {
368
+ setPanelZIndex(p, getDragZIndex(p));
369
+ }
370
+ document.body.style.userSelect = "none";
371
+ this.callbacks.onDragStart?.(this.activeDrag);
372
+ }
373
+ /**
374
+ * Handle mouse movement during drag
375
+ */
376
+ handleMouseMove(e) {
377
+ if (!this.activeDrag) return;
378
+ const { grabbedPanel, movingPanels, initialGroupPositions, mode } = this.activeDrag;
379
+ const panel = grabbedPanel.element;
380
+ const x = e.clientX - this.activeDrag.offsetX;
381
+ const y = e.clientY - this.activeDrag.offsetY;
382
+ const maxX = window.innerWidth - panel.offsetWidth;
383
+ const maxY = window.innerHeight - panel.offsetHeight;
384
+ const clampedX = Math.max(0, Math.min(x, maxX));
385
+ const clampedY = Math.max(0, Math.min(y, maxY));
386
+ panel.style.left = `${clampedX}px`;
387
+ panel.style.top = `${clampedY}px`;
388
+ if (mode === "group" && movingPanels.length > 1) {
389
+ const grabbedInitialPos = initialGroupPositions.get(grabbedPanel.id);
390
+ const deltaX = clampedX - grabbedInitialPos.x;
391
+ const deltaY = clampedY - grabbedInitialPos.y;
392
+ for (const p of movingPanels) {
393
+ if (p === grabbedPanel) continue;
394
+ const initialPos = initialGroupPositions.get(p.id);
395
+ const newX = Math.max(
396
+ 0,
397
+ Math.min(initialPos.x + deltaX, window.innerWidth - p.element.offsetWidth)
398
+ );
399
+ const newY = Math.max(
400
+ 0,
401
+ Math.min(initialPos.y + deltaY, window.innerHeight - p.element.offsetHeight)
402
+ );
403
+ p.element.style.left = `${newX}px`;
404
+ p.element.style.top = `${newY}px`;
405
+ }
406
+ }
407
+ const snapTarget = findSnapTarget(movingPanels, this.panels, this.config);
408
+ const anchorResult = snapTarget ? null : this.callbacks.findAnchorTarget?.(movingPanels) ?? null;
409
+ this.callbacks.onDragMove?.(
410
+ this.activeDrag,
411
+ { x: clampedX, y: clampedY },
412
+ snapTarget,
413
+ anchorResult
414
+ );
415
+ }
416
+ /**
417
+ * Handle mouse up - finalize drag
418
+ */
419
+ handleMouseUp(_e) {
420
+ if (!this.activeDrag) return;
421
+ const { movingPanels } = this.activeDrag;
422
+ const snapTarget = findSnapTarget(movingPanels, this.panels, this.config);
423
+ let anchorResult = null;
424
+ if (snapTarget) {
425
+ snapPanelsToTarget(
426
+ movingPanels,
427
+ snapTarget.targetId,
428
+ snapTarget.side,
429
+ snapTarget.x,
430
+ snapTarget.y,
431
+ this.panels,
432
+ this.config
433
+ );
434
+ } else {
435
+ anchorResult = this.callbacks.findAnchorTarget?.(movingPanels) ?? null;
436
+ if (anchorResult) {
437
+ for (let i = 0; i < movingPanels.length; i++) {
438
+ movingPanels[i].element.style.left = `${anchorResult.positions[i].x}px`;
439
+ movingPanels[i].element.style.top = `${anchorResult.positions[i].y}px`;
440
+ }
441
+ }
442
+ }
443
+ for (const p of movingPanels) {
444
+ setPanelZIndex(p, getDefaultZIndex(p));
445
+ }
446
+ document.body.style.userSelect = "";
447
+ this.callbacks.onDragEnd?.(this.activeDrag, snapTarget, anchorResult);
448
+ this.activeDrag = null;
449
+ }
450
+ /**
451
+ * Check if a drag is currently in progress
452
+ */
453
+ isActive() {
454
+ return this.activeDrag !== null;
455
+ }
456
+ /**
457
+ * Get the current drag state
458
+ */
459
+ getState() {
460
+ return this.activeDrag;
461
+ }
462
+ /**
463
+ * Clean up event listeners
464
+ */
465
+ destroy() {
466
+ document.removeEventListener("mousemove", this.boundMouseMove);
467
+ document.removeEventListener("mouseup", this.boundMouseUp);
468
+ }
469
+ };
470
+
471
+ // src/AnchorManager.ts
472
+ function getDefaultAnchorConfigs(config) {
473
+ const { panelMargin, defaultPanelWidth } = config;
474
+ return [
475
+ {
476
+ id: "top-left",
477
+ getPosition: () => ({ x: panelMargin, y: panelMargin })
478
+ },
479
+ {
480
+ id: "top-right",
481
+ getPosition: () => ({
482
+ x: window.innerWidth - panelMargin - defaultPanelWidth,
483
+ y: panelMargin
484
+ })
485
+ },
486
+ {
487
+ id: "bottom-left",
488
+ getPosition: () => ({
489
+ x: panelMargin,
490
+ y: window.innerHeight - panelMargin
491
+ })
492
+ },
493
+ {
494
+ id: "bottom-right",
495
+ getPosition: () => ({
496
+ x: window.innerWidth - panelMargin - defaultPanelWidth,
497
+ y: window.innerHeight - panelMargin
498
+ })
499
+ },
500
+ {
501
+ id: "top-center",
502
+ getPosition: () => ({
503
+ x: (window.innerWidth - defaultPanelWidth) / 2,
504
+ y: panelMargin
505
+ })
506
+ }
507
+ ];
508
+ }
509
+ function createPresetAnchor(preset, config) {
510
+ const { panelMargin, defaultPanelWidth } = config;
511
+ const presets = {
512
+ "top-left": () => ({ x: panelMargin, y: panelMargin }),
513
+ "top-right": () => ({
514
+ x: window.innerWidth - panelMargin - defaultPanelWidth,
515
+ y: panelMargin
516
+ }),
517
+ "top-center": () => ({
518
+ x: (window.innerWidth - defaultPanelWidth) / 2,
519
+ y: panelMargin
520
+ }),
521
+ "bottom-left": () => ({
522
+ x: panelMargin,
523
+ y: window.innerHeight - panelMargin
524
+ }),
525
+ "bottom-right": () => ({
526
+ x: window.innerWidth - panelMargin - defaultPanelWidth,
527
+ y: window.innerHeight - panelMargin
528
+ }),
529
+ "bottom-center": () => ({
530
+ x: (window.innerWidth - defaultPanelWidth) / 2,
531
+ y: window.innerHeight - panelMargin
532
+ }),
533
+ "center-left": () => ({
534
+ x: panelMargin,
535
+ y: window.innerHeight / 2
536
+ }),
537
+ "center-right": () => ({
538
+ x: window.innerWidth - panelMargin - defaultPanelWidth,
539
+ y: window.innerHeight / 2
540
+ })
541
+ };
542
+ return {
543
+ id: preset,
544
+ getPosition: presets[preset]
545
+ };
546
+ }
547
+ var AnchorManager = class {
548
+ constructor(config, classes) {
549
+ this.anchors = [];
550
+ this.config = config;
551
+ this.classes = classes;
552
+ this.container = config.container;
553
+ window.addEventListener("resize", this.updateIndicatorPositions.bind(this));
554
+ }
555
+ /**
556
+ * Create an anchor indicator element
557
+ */
558
+ createIndicator() {
559
+ const indicator = document.createElement("div");
560
+ indicator.className = this.classes.anchorIndicator;
561
+ this.container.appendChild(indicator);
562
+ return indicator;
563
+ }
564
+ /**
565
+ * Add an anchor point
566
+ */
567
+ addAnchor(anchorConfig) {
568
+ const indicator = anchorConfig.showIndicator !== false ? this.createIndicator() : null;
569
+ const state = {
570
+ config: anchorConfig,
571
+ indicator
572
+ };
573
+ this.anchors.push(state);
574
+ this.updateIndicatorPosition(state);
575
+ return state;
576
+ }
577
+ /**
578
+ * Add multiple default anchors
579
+ */
580
+ addDefaultAnchors() {
581
+ const defaults = getDefaultAnchorConfigs(this.config);
582
+ for (const config of defaults) {
583
+ this.addAnchor(config);
584
+ }
585
+ }
586
+ /**
587
+ * Add anchor from preset
588
+ */
589
+ addPresetAnchor(preset) {
590
+ return this.addAnchor(createPresetAnchor(preset, this.config));
591
+ }
592
+ /**
593
+ * Remove an anchor
594
+ */
595
+ removeAnchor(id) {
596
+ const index = this.anchors.findIndex((a) => a.config.id === id);
597
+ if (index === -1) return false;
598
+ const anchor = this.anchors[index];
599
+ anchor.indicator?.remove();
600
+ this.anchors.splice(index, 1);
601
+ return true;
602
+ }
603
+ /**
604
+ * Update position of a single indicator
605
+ */
606
+ updateIndicatorPosition(anchor) {
607
+ if (!anchor.indicator) return;
608
+ const pos = anchor.config.getPosition();
609
+ const indicator = anchor.indicator;
610
+ indicator.style.left = `${pos.x - 20}px`;
611
+ if (anchor.config.id.includes("bottom")) {
612
+ indicator.style.top = `${pos.y - 40}px`;
613
+ } else {
614
+ indicator.style.top = `${pos.y}px`;
615
+ }
616
+ }
617
+ /**
618
+ * Update all indicator positions (call on window resize)
619
+ */
620
+ updateIndicatorPositions() {
621
+ for (const anchor of this.anchors) {
622
+ this.updateIndicatorPosition(anchor);
623
+ }
624
+ }
625
+ /**
626
+ * Find the nearest anchor for a group of moving panels
627
+ */
628
+ findNearestAnchor(movingPanels) {
629
+ if (movingPanels.length === 0 || this.anchors.length === 0) return null;
630
+ let bestResult = null;
631
+ let bestDist = Infinity;
632
+ const firstRect = movingPanels[0].element.getBoundingClientRect();
633
+ const lastRect = movingPanels[movingPanels.length - 1].element.getBoundingClientRect();
634
+ const groupLeft = firstRect.left;
635
+ const groupRight = lastRect.right;
636
+ const groupTop = firstRect.top;
637
+ const groupBottom = firstRect.bottom;
638
+ for (const anchor of this.anchors) {
639
+ const anchorPos = anchor.config.getPosition();
640
+ const nearGroup = anchorPos.x >= groupLeft - this.config.anchorThreshold && anchorPos.x <= groupRight + this.config.anchorThreshold && anchorPos.y >= groupTop - this.config.anchorThreshold && anchorPos.y <= groupBottom + this.config.anchorThreshold;
641
+ if (!nearGroup) continue;
642
+ let closestPanelIdx = 0;
643
+ let closestDist = Infinity;
644
+ for (let i = 0; i < movingPanels.length; i++) {
645
+ const rect = movingPanels[i].element.getBoundingClientRect();
646
+ const gripX = rect.left;
647
+ const gripY = rect.top;
648
+ const dist = Math.sqrt(
649
+ Math.pow(gripX - anchorPos.x, 2) + Math.pow(gripY - anchorPos.y, 2)
650
+ );
651
+ if (dist < closestDist) {
652
+ closestDist = dist;
653
+ closestPanelIdx = i;
654
+ }
655
+ }
656
+ const anchorX = anchorPos.x;
657
+ const anchorY = anchor.config.id.includes("bottom") ? anchorPos.y - movingPanels[closestPanelIdx].element.offsetHeight : anchorPos.y;
658
+ const positions = [];
659
+ let leftX = anchorX;
660
+ for (let i = closestPanelIdx - 1; i >= 0; i--) {
661
+ leftX -= movingPanels[i].element.offsetWidth + this.config.panelGap;
662
+ }
663
+ let currentX = leftX;
664
+ for (let i = 0; i < movingPanels.length; i++) {
665
+ positions.push({ x: currentX, y: anchorY });
666
+ currentX += movingPanels[i].element.offsetWidth + this.config.panelGap;
667
+ }
668
+ const leftmostX = positions[0].x;
669
+ const rightmostX = positions[positions.length - 1].x + movingPanels[movingPanels.length - 1].element.offsetWidth;
670
+ if (leftmostX < 0 || rightmostX > window.innerWidth) {
671
+ for (let tryIdx = 0; tryIdx < movingPanels.length; tryIdx++) {
672
+ const tryPositions = [];
673
+ let tryLeftX = anchorX;
674
+ for (let i = tryIdx - 1; i >= 0; i--) {
675
+ tryLeftX -= movingPanels[i].element.offsetWidth + this.config.panelGap;
676
+ }
677
+ let tryCurrentX = tryLeftX;
678
+ for (let i = 0; i < movingPanels.length; i++) {
679
+ tryPositions.push({ x: tryCurrentX, y: anchorY });
680
+ tryCurrentX += movingPanels[i].element.offsetWidth + this.config.panelGap;
681
+ }
682
+ const tryLeftmostX = tryPositions[0].x;
683
+ const tryRightmostX = tryPositions[tryPositions.length - 1].x + movingPanels[movingPanels.length - 1].element.offsetWidth;
684
+ if (tryLeftmostX >= 0 && tryRightmostX <= window.innerWidth) {
685
+ if (closestDist < bestDist) {
686
+ bestDist = closestDist;
687
+ bestResult = {
688
+ anchor,
689
+ dockPanelIndex: tryIdx,
690
+ positions: tryPositions
691
+ };
692
+ }
693
+ break;
694
+ }
695
+ }
696
+ continue;
697
+ }
698
+ if (closestDist < bestDist) {
699
+ bestDist = closestDist;
700
+ bestResult = { anchor, dockPanelIndex: closestPanelIdx, positions };
701
+ }
702
+ }
703
+ return bestResult;
704
+ }
705
+ /**
706
+ * Show anchor indicators during drag
707
+ */
708
+ showIndicators(activeAnchor) {
709
+ for (const anchor of this.anchors) {
710
+ if (!anchor.indicator) continue;
711
+ if (activeAnchor === anchor) {
712
+ anchor.indicator.classList.add(
713
+ this.classes.anchorIndicatorVisible,
714
+ this.classes.anchorIndicatorActive
715
+ );
716
+ } else {
717
+ anchor.indicator.classList.add(this.classes.anchorIndicatorVisible);
718
+ anchor.indicator.classList.remove(this.classes.anchorIndicatorActive);
719
+ }
720
+ }
721
+ }
722
+ /**
723
+ * Hide all anchor indicators
724
+ */
725
+ hideIndicators() {
726
+ for (const anchor of this.anchors) {
727
+ if (!anchor.indicator) continue;
728
+ anchor.indicator.classList.remove(
729
+ this.classes.anchorIndicatorVisible,
730
+ this.classes.anchorIndicatorActive
731
+ );
732
+ }
733
+ }
734
+ /**
735
+ * Get all anchors
736
+ */
737
+ getAnchors() {
738
+ return [...this.anchors];
739
+ }
740
+ /**
741
+ * Get anchor by ID
742
+ */
743
+ getAnchor(id) {
744
+ return this.anchors.find((a) => a.config.id === id);
745
+ }
746
+ /**
747
+ * Clean up
748
+ */
749
+ destroy() {
750
+ window.removeEventListener("resize", this.updateIndicatorPositions.bind(this));
751
+ for (const anchor of this.anchors) {
752
+ anchor.indicator?.remove();
753
+ }
754
+ this.anchors = [];
755
+ }
756
+ };
757
+
758
+ // src/SnapPreview.ts
759
+ var SnapPreview = class {
760
+ constructor(container, classes, panels) {
761
+ this.element = null;
762
+ this.container = container;
763
+ this.classes = classes;
764
+ this.panels = panels;
765
+ }
766
+ /**
767
+ * Create the preview element lazily
768
+ */
769
+ ensureElement() {
770
+ if (!this.element) {
771
+ this.element = document.createElement("div");
772
+ this.element.className = this.classes.snapPreview;
773
+ this.container.appendChild(this.element);
774
+ }
775
+ return this.element;
776
+ }
777
+ /**
778
+ * Update the preview to show a snap target
779
+ */
780
+ show(snapTarget) {
781
+ const preview = this.ensureElement();
782
+ const targetState = this.panels.get(snapTarget.targetId);
783
+ if (!targetState) {
784
+ this.hide();
785
+ return;
786
+ }
787
+ const targetRect = targetState.element.getBoundingClientRect();
788
+ preview.style.top = `${targetRect.top}px`;
789
+ preview.style.height = `${targetRect.height}px`;
790
+ preview.style.left = `${snapTarget.side === "left" ? targetRect.left - 2 : targetRect.right - 2}px`;
791
+ preview.classList.add(this.classes.snapPreviewVisible);
792
+ }
793
+ /**
794
+ * Hide the preview
795
+ */
796
+ hide() {
797
+ if (this.element) {
798
+ this.element.classList.remove(this.classes.snapPreviewVisible);
799
+ }
800
+ }
801
+ /**
802
+ * Update based on current snap target (convenience method)
803
+ */
804
+ update(snapTarget) {
805
+ if (snapTarget) {
806
+ this.show(snapTarget);
807
+ } else {
808
+ this.hide();
809
+ }
810
+ }
811
+ /**
812
+ * Clean up
813
+ */
814
+ destroy() {
815
+ this.element?.remove();
816
+ this.element = null;
817
+ }
818
+ };
819
+
820
+ // src/TabManager.ts
821
+ var DEFAULT_CONFIG = {
822
+ snapThreshold: 50,
823
+ panelGap: 0,
824
+ panelMargin: 16,
825
+ anchorThreshold: 80,
826
+ defaultPanelWidth: 300,
827
+ container: document.body,
828
+ initializeDefaultAnchors: true,
829
+ classPrefix: "blork-tabs"
830
+ };
831
+ function generateClasses(prefix) {
832
+ return {
833
+ panel: `${prefix}-panel`,
834
+ panelHeader: `${prefix}-header`,
835
+ panelTitle: `${prefix}-title`,
836
+ panelContent: `${prefix}-content`,
837
+ panelContentCollapsed: `${prefix}-content-collapsed`,
838
+ detachGrip: `${prefix}-detach-grip`,
839
+ collapseButton: `${prefix}-collapse-btn`,
840
+ snapPreview: `${prefix}-snap-preview`,
841
+ snapPreviewVisible: `${prefix}-snap-preview-visible`,
842
+ anchorIndicator: `${prefix}-anchor-indicator`,
843
+ anchorIndicatorVisible: `${prefix}-anchor-indicator-visible`,
844
+ anchorIndicatorActive: `${prefix}-anchor-indicator-active`,
845
+ dragging: `${prefix}-dragging`
846
+ };
847
+ }
848
+ var TabManager = class {
849
+ constructor(userConfig = {}) {
850
+ this.panels = /* @__PURE__ */ new Map();
851
+ this.eventListeners = /* @__PURE__ */ new Map();
852
+ this.config = {
853
+ ...DEFAULT_CONFIG,
854
+ ...userConfig,
855
+ container: userConfig.container ?? document.body
856
+ };
857
+ this.classes = generateClasses(this.config.classPrefix);
858
+ this.anchorManager = new AnchorManager(this.config, this.classes);
859
+ this.snapPreview = new SnapPreview(
860
+ this.config.container,
861
+ this.classes,
862
+ this.panels
863
+ );
864
+ this.dragManager = new DragManager(
865
+ this.panels,
866
+ this.config,
867
+ {
868
+ onDragStart: this.handleDragStart.bind(this),
869
+ onDragMove: this.handleDragMove.bind(this),
870
+ onDragEnd: this.handleDragEnd.bind(this),
871
+ findAnchorTarget: (panels) => this.anchorManager.findNearestAnchor(panels)
872
+ }
873
+ );
874
+ if (this.config.initializeDefaultAnchors) {
875
+ this.anchorManager.addDefaultAnchors();
876
+ }
877
+ }
878
+ // ==================== Panel Management ====================
879
+ /**
880
+ * Add a new panel
881
+ */
882
+ addPanel(panelConfig) {
883
+ const state = createPanelState(panelConfig, this.classes);
884
+ if (!panelConfig.element && !this.config.container.contains(state.element)) {
885
+ this.config.container.appendChild(state.element);
886
+ }
887
+ this.setupPanelEvents(state);
888
+ this.panels.set(state.id, state);
889
+ if (panelConfig.initialPosition) {
890
+ setPanelPosition(state, panelConfig.initialPosition.x, panelConfig.initialPosition.y);
891
+ }
892
+ this.emit("panel:added", { panel: state });
893
+ return state;
894
+ }
895
+ /**
896
+ * Register an existing panel element
897
+ */
898
+ registerPanel(id, element, options = {}) {
899
+ return this.addPanel({
900
+ id,
901
+ element,
902
+ dragHandle: options.dragHandle,
903
+ collapseButton: options.collapseButton,
904
+ contentWrapper: options.contentWrapper,
905
+ detachGrip: options.detachGrip,
906
+ startCollapsed: options.startCollapsed
907
+ });
908
+ }
909
+ /**
910
+ * Remove a panel
911
+ */
912
+ removePanel(id) {
913
+ const panel = this.panels.get(id);
914
+ if (!panel) return false;
915
+ detachFromGroup(panel, this.panels);
916
+ if (!panel.config.element) {
917
+ panel.element.remove();
918
+ }
919
+ this.panels.delete(id);
920
+ this.emit("panel:removed", { panelId: id });
921
+ return true;
922
+ }
923
+ /**
924
+ * Get a panel by ID
925
+ */
926
+ getPanel(id) {
927
+ return this.panels.get(id);
928
+ }
929
+ /**
930
+ * Get all panels
931
+ */
932
+ getAllPanels() {
933
+ return Array.from(this.panels.values());
934
+ }
935
+ /**
936
+ * Set up event handlers for a panel
937
+ */
938
+ setupPanelEvents(state) {
939
+ if (state.collapseButton) {
940
+ state.collapseButton.addEventListener("click", () => {
941
+ const newState = toggleCollapse(state, this.classes);
942
+ updateSnappedPositions(this.panels, this.config);
943
+ this.emit("panel:collapse", { panel: state, isCollapsed: newState });
944
+ });
945
+ }
946
+ if (state.detachGrip) {
947
+ state.detachGrip.addEventListener("mousedown", (e) => {
948
+ this.dragManager.startDrag(e, state, "single");
949
+ });
950
+ }
951
+ state.dragHandle.addEventListener("mousedown", (e) => {
952
+ if (e.target === state.collapseButton || e.target === state.detachGrip) {
953
+ return;
954
+ }
955
+ this.dragManager.startDrag(e, state, "group");
956
+ });
957
+ }
958
+ // ==================== Snap Chain Management ====================
959
+ /**
960
+ * Get all panels in the same snap chain as the given panel
961
+ */
962
+ getSnapChain(panelId) {
963
+ const panel = this.panels.get(panelId);
964
+ if (!panel) return [];
965
+ return getConnectedGroup(panel, this.panels);
966
+ }
967
+ /**
968
+ * Manually snap two panels together
969
+ */
970
+ snap(leftPanelId, rightPanelId) {
971
+ const leftPanel = this.panels.get(leftPanelId);
972
+ const rightPanel = this.panels.get(rightPanelId);
973
+ if (!leftPanel || !rightPanel) return false;
974
+ snapPanels(leftPanel, rightPanel);
975
+ updateSnappedPositions(this.panels, this.config);
976
+ return true;
977
+ }
978
+ /**
979
+ * Detach a panel from its snap chain
980
+ */
981
+ detach(panelId) {
982
+ const panel = this.panels.get(panelId);
983
+ if (!panel) return false;
984
+ const previousGroup = getConnectedGroup(panel, this.panels);
985
+ detachFromGroup(panel, this.panels);
986
+ this.emit("panel:detached", { panel, previousGroup });
987
+ return true;
988
+ }
989
+ /**
990
+ * Update snapped positions (call after collapse/expand or resize)
991
+ */
992
+ updatePositions() {
993
+ updateSnappedPositions(this.panels, this.config);
994
+ }
995
+ // ==================== Anchor Management ====================
996
+ /**
997
+ * Add a custom anchor
998
+ */
999
+ addAnchor(config) {
1000
+ return this.anchorManager.addAnchor(config);
1001
+ }
1002
+ /**
1003
+ * Add a preset anchor
1004
+ */
1005
+ addPresetAnchor(preset) {
1006
+ return this.anchorManager.addPresetAnchor(preset);
1007
+ }
1008
+ /**
1009
+ * Remove an anchor
1010
+ */
1011
+ removeAnchor(id) {
1012
+ return this.anchorManager.removeAnchor(id);
1013
+ }
1014
+ /**
1015
+ * Get all anchors
1016
+ */
1017
+ getAnchors() {
1018
+ return this.anchorManager.getAnchors();
1019
+ }
1020
+ // ==================== Drag Callbacks ====================
1021
+ handleDragStart(state) {
1022
+ this.anchorManager.showIndicators(null);
1023
+ this.emit("drag:start", {
1024
+ panel: state.grabbedPanel,
1025
+ mode: state.mode,
1026
+ movingPanels: state.movingPanels
1027
+ });
1028
+ }
1029
+ handleDragMove(state, position, snapTarget, anchorResult) {
1030
+ this.snapPreview.update(snapTarget);
1031
+ this.anchorManager.showIndicators(anchorResult?.anchor ?? null);
1032
+ this.emit("drag:move", {
1033
+ panel: state.grabbedPanel,
1034
+ position,
1035
+ snapTarget,
1036
+ anchorTarget: anchorResult
1037
+ });
1038
+ }
1039
+ handleDragEnd(state, snapTarget, anchorResult) {
1040
+ this.snapPreview.hide();
1041
+ this.anchorManager.hideIndicators();
1042
+ const rect = state.grabbedPanel.element.getBoundingClientRect();
1043
+ if (snapTarget) {
1044
+ const targetPanel = this.panels.get(snapTarget.targetId);
1045
+ if (targetPanel) {
1046
+ this.emit("snap:panel", {
1047
+ movingPanels: state.movingPanels,
1048
+ targetPanel,
1049
+ side: snapTarget.side
1050
+ });
1051
+ }
1052
+ } else if (anchorResult) {
1053
+ this.emit("snap:anchor", {
1054
+ movingPanels: state.movingPanels,
1055
+ anchor: anchorResult.anchor
1056
+ });
1057
+ }
1058
+ this.emit("drag:end", {
1059
+ panel: state.grabbedPanel,
1060
+ finalPosition: { x: rect.left, y: rect.top },
1061
+ snappedToPanel: snapTarget !== null,
1062
+ snappedToAnchor: anchorResult !== null
1063
+ });
1064
+ }
1065
+ // ==================== Event System ====================
1066
+ /**
1067
+ * Subscribe to an event
1068
+ */
1069
+ on(event, listener) {
1070
+ if (!this.eventListeners.has(event)) {
1071
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
1072
+ }
1073
+ this.eventListeners.get(event).add(listener);
1074
+ return () => this.off(event, listener);
1075
+ }
1076
+ /**
1077
+ * Unsubscribe from an event
1078
+ */
1079
+ off(event, listener) {
1080
+ this.eventListeners.get(event)?.delete(listener);
1081
+ }
1082
+ /**
1083
+ * Emit an event
1084
+ */
1085
+ emit(event, data) {
1086
+ const listeners = this.eventListeners.get(event);
1087
+ if (listeners) {
1088
+ for (const listener of listeners) {
1089
+ listener(data);
1090
+ }
1091
+ }
1092
+ }
1093
+ // ==================== Positioning ====================
1094
+ /**
1095
+ * Position panels in a row from right edge
1096
+ */
1097
+ positionPanelsFromRight(panelIds, gap = 0) {
1098
+ let rightEdge = window.innerWidth - this.config.panelMargin;
1099
+ for (const id of panelIds) {
1100
+ const state = this.panels.get(id);
1101
+ if (!state) continue;
1102
+ const width = state.element.offsetWidth;
1103
+ setPanelPosition(state, rightEdge - width, this.config.panelMargin);
1104
+ rightEdge -= width + gap;
1105
+ }
1106
+ }
1107
+ /**
1108
+ * Position panels in a row from left edge
1109
+ */
1110
+ positionPanelsFromLeft(panelIds, gap = 0) {
1111
+ let leftEdge = this.config.panelMargin;
1112
+ for (const id of panelIds) {
1113
+ const state = this.panels.get(id);
1114
+ if (!state) continue;
1115
+ setPanelPosition(state, leftEdge, this.config.panelMargin);
1116
+ leftEdge += state.element.offsetWidth + gap;
1117
+ }
1118
+ }
1119
+ /**
1120
+ * Set up initial snap chain for a list of panels (left to right)
1121
+ */
1122
+ createSnapChain(panelIds) {
1123
+ for (let i = 0; i < panelIds.length - 1; i++) {
1124
+ const leftPanel = this.panels.get(panelIds[i]);
1125
+ const rightPanel = this.panels.get(panelIds[i + 1]);
1126
+ if (leftPanel && rightPanel) {
1127
+ snapPanels(leftPanel, rightPanel);
1128
+ }
1129
+ }
1130
+ }
1131
+ // ==================== Configuration ====================
1132
+ /**
1133
+ * Get the current configuration
1134
+ */
1135
+ getConfig() {
1136
+ return { ...this.config };
1137
+ }
1138
+ /**
1139
+ * Get the CSS classes used
1140
+ */
1141
+ getClasses() {
1142
+ return { ...this.classes };
1143
+ }
1144
+ /**
1145
+ * Check if dragging is currently active
1146
+ */
1147
+ isDragging() {
1148
+ return this.dragManager.isActive();
1149
+ }
1150
+ // ==================== Cleanup ====================
1151
+ /**
1152
+ * Destroy the TabManager and clean up resources
1153
+ */
1154
+ destroy() {
1155
+ this.dragManager.destroy();
1156
+ this.anchorManager.destroy();
1157
+ this.snapPreview.destroy();
1158
+ for (const panel of this.panels.values()) {
1159
+ if (!panel.config.element) {
1160
+ panel.element.remove();
1161
+ }
1162
+ }
1163
+ this.panels.clear();
1164
+ this.eventListeners.clear();
1165
+ }
1166
+ };
1167
+ // Annotate the CommonJS export names for ESM import in node:
1168
+ 0 && (module.exports = {
1169
+ AnchorManager,
1170
+ DragManager,
1171
+ SnapPreview,
1172
+ TabManager,
1173
+ areInSameChain,
1174
+ createPanelElement,
1175
+ createPanelState,
1176
+ createPresetAnchor,
1177
+ detachFromGroup,
1178
+ findSnapTarget,
1179
+ getConnectedGroup,
1180
+ getDefaultAnchorConfigs,
1181
+ getDefaultZIndex,
1182
+ getDragZIndex,
1183
+ getLeftmostPanel,
1184
+ getPanelDimensions,
1185
+ getPanelPosition,
1186
+ getRightmostPanel,
1187
+ setPanelPosition,
1188
+ setPanelZIndex,
1189
+ snapPanels,
1190
+ snapPanelsToTarget,
1191
+ toggleCollapse,
1192
+ unsnap,
1193
+ updateSnappedPositions
1194
+ });