@blorkfield/blork-tabs 0.3.1 → 0.4.0
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/README.md +98 -3
- package/dist/index.cjs +89 -8
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +87 -8
- package/dist/styles.css +20 -0
- package/package.json +1 -1
- package/src/AutoHideManager.ts +21 -1
- package/src/DragManager.ts +13 -13
- package/src/Panel.ts +37 -0
- package/src/SnapChain.ts +45 -0
- package/src/TabManager.ts +19 -3
- package/src/index.ts +3 -0
- package/src/types.ts +18 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A framework-agnostic tab/panel management system with snapping and docking capab
|
|
|
8
8
|
- **Anchor Docking** - Dock panels to predefined screen positions
|
|
9
9
|
- **Drag Modes** - Drag entire groups or detach individual panels
|
|
10
10
|
- **Collapse/Expand** - Panels can be collapsed with automatic repositioning
|
|
11
|
+
- **Pin** - Pin panels to prevent dragging and opt out of auto-hide
|
|
11
12
|
- **Auto-Hide** - Panels can hide after inactivity and show on interaction
|
|
12
13
|
- **Event System** - Subscribe to drag, snap, and collapse events
|
|
13
14
|
- **Fully Typed** - Complete TypeScript support
|
|
@@ -54,6 +55,7 @@ manager.registerPanel('my-panel', document.getElementById('my-panel'), {
|
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
// Position panels and create snap chains
|
|
58
|
+
// positionPanelsFromRight takes IDs right-to-left; createSnapChain takes them left-to-right
|
|
57
59
|
manager.positionPanelsFromRight(['tools', 'settings']);
|
|
58
60
|
manager.createSnapChain(['settings', 'tools']);
|
|
59
61
|
|
|
@@ -99,6 +101,40 @@ const manager = new TabManager({
|
|
|
99
101
|
});
|
|
100
102
|
```
|
|
101
103
|
|
|
104
|
+
## Panel Configuration
|
|
105
|
+
|
|
106
|
+
Each panel accepts the following options via `addPanel(config)`:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
manager.addPanel({
|
|
110
|
+
id: 'my-panel', // Required. Unique identifier.
|
|
111
|
+
title: 'My Panel', // Header text.
|
|
112
|
+
width: 300, // Panel width in px (default: 300).
|
|
113
|
+
initialPosition: { x: 100, y: 100 }, // Starting position. Auto-placed if omitted.
|
|
114
|
+
content: '<div>...</div>', // HTML string or HTMLElement for the content area.
|
|
115
|
+
|
|
116
|
+
// Collapse
|
|
117
|
+
startCollapsed: true, // Whether the panel starts collapsed (default: true).
|
|
118
|
+
collapsible: true, // Show collapse button (default: true).
|
|
119
|
+
|
|
120
|
+
// Pin
|
|
121
|
+
pinnable: false, // Show pin button in header (default: false).
|
|
122
|
+
startPinned: false, // Initial pin state (default: false).
|
|
123
|
+
|
|
124
|
+
// Drag
|
|
125
|
+
draggable: true, // Allow dragging (default: true).
|
|
126
|
+
detachable: true, // Show detach grip for single-panel drag (default: true).
|
|
127
|
+
|
|
128
|
+
// Auto-hide overrides (see Auto-Hide section)
|
|
129
|
+
startHidden: false, // Override global startHidden for this panel.
|
|
130
|
+
autoHideDelay: undefined, // Override global autoHideDelay (0 = disable auto-hide).
|
|
131
|
+
|
|
132
|
+
// Z-index
|
|
133
|
+
zIndex: 1000, // Default z-index (default: 1000).
|
|
134
|
+
dragZIndex: 1002, // Z-index while dragging (default: 1002).
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
102
138
|
## API Reference
|
|
103
139
|
|
|
104
140
|
### TabManager
|
|
@@ -115,12 +151,23 @@ const manager = new TabManager({
|
|
|
115
151
|
- `getSnapChain(panelId)` - Get all panels in same chain
|
|
116
152
|
- `snap(leftPanelId, rightPanelId)` - Manually snap two panels
|
|
117
153
|
- `detach(panelId)` - Detach panel from chain
|
|
118
|
-
- `createSnapChain(panelIds)` - Create chain from panel IDs
|
|
154
|
+
- `createSnapChain(panelIds)` - Create chain from panel IDs (left-to-right order)
|
|
119
155
|
- `updatePositions()` - Recalculate snapped positions
|
|
120
156
|
|
|
121
157
|
#### Positioning
|
|
122
|
-
|
|
123
|
-
|
|
158
|
+
|
|
159
|
+
`positionPanelsFromRight` and `positionPanelsFromLeft` take panel IDs in **opposite orders**:
|
|
160
|
+
|
|
161
|
+
- `positionPanelsFromRight(panelIds, gap?)` — first ID is placed at the **right edge**, rest go leftward. Pass IDs **right-to-left**.
|
|
162
|
+
- `positionPanelsFromLeft(panelIds, gap?)` — first ID is placed at the **left edge**, rest go rightward. Pass IDs **left-to-right**.
|
|
163
|
+
|
|
164
|
+
Because `createSnapChain` uses left-to-right order, you'll typically reverse the array between the two calls:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// Visual order left-to-right: properties, tools, settings
|
|
168
|
+
manager.positionPanelsFromRight(['settings', 'tools', 'properties']); // right-to-left
|
|
169
|
+
manager.createSnapChain(['properties', 'tools', 'settings']); // left-to-right
|
|
170
|
+
```
|
|
124
171
|
|
|
125
172
|
#### Anchors
|
|
126
173
|
- `addAnchor(config)` - Add custom anchor
|
|
@@ -149,10 +196,54 @@ const manager = new TabManager({
|
|
|
149
196
|
| `snap:panel` | Panels snapped together |
|
|
150
197
|
| `snap:anchor` | Panels snapped to anchor |
|
|
151
198
|
| `panel:detached` | Panel detached from chain |
|
|
199
|
+
| `panel:pin` | Panel pinned/unpinned |
|
|
152
200
|
| `panel:collapse` | Panel collapsed/expanded |
|
|
153
201
|
| `panel:show` | Panel became visible (auto-hide) |
|
|
154
202
|
| `panel:hide` | Panel became hidden (auto-hide) |
|
|
155
203
|
|
|
204
|
+
## Drag Modes
|
|
205
|
+
|
|
206
|
+
Each panel header has two drag zones:
|
|
207
|
+
|
|
208
|
+
- **Detach grip** (small handle on the left of the header) — drags only that panel. It is detached from its snap chain and moved independently.
|
|
209
|
+
- **Title / header area** — drags the entire connected snap chain as a group.
|
|
210
|
+
|
|
211
|
+
Set `detachable: false` to hide the grip and make the header always drag the group.
|
|
212
|
+
|
|
213
|
+
## Pinning
|
|
214
|
+
|
|
215
|
+
Pinning locks a panel in place and exempts it from auto-hide. Enable the pin button with `pinnable: true`:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
manager.addPanel({
|
|
219
|
+
id: 'hud',
|
|
220
|
+
title: 'HUD',
|
|
221
|
+
pinnable: true,
|
|
222
|
+
startPinned: true, // start already pinned
|
|
223
|
+
autoHideDelay: 5000, // pin will override this
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### What pinning does
|
|
228
|
+
|
|
229
|
+
- **Prevents dragging** — a pinned panel cannot be moved, even when dragging its header.
|
|
230
|
+
- **Immune to auto-hide** — a pinned panel is always visible regardless of the `autoHideDelay`. Pinning cancels any pending hide timer and shows the panel if it was hidden. Unpinning restarts the timer.
|
|
231
|
+
- **Splits snap chains on drag** — if a pinned panel sits in the middle of a chain and you drag a neighbour, the chain severs at the pin boundary. Only the panels on the same side as the grabbed panel move; the pinned panel and everything beyond it stay put.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Chain: A — B — [P pinned] — C — D
|
|
235
|
+
// Grabbing B moves [A, B] and severs the B↔P bond.
|
|
236
|
+
// Grabbing C moves [C, D] and severs the P↔C bond.
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Events
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
manager.on('panel:pin', ({ panel, isPinned }) => {
|
|
243
|
+
console.log(`${panel.id} is now ${isPinned ? 'pinned' : 'unpinned'}`);
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
156
247
|
## Multi-Section Panel Content
|
|
157
248
|
|
|
158
249
|
When creating panels with multiple sections (like a command menu with categories), put all sections within a **single `content` string**. Do not use multiple panels or multiple content wrappers—this breaks scrolling and causes content cutoff.
|
|
@@ -231,6 +322,10 @@ manager.addPanel({
|
|
|
231
322
|
| `false` | `3000` | Starts visible, hides after 3s of inactivity |
|
|
232
323
|
| `true` | `3000` | Starts hidden, shows on activity, hides after 3s of inactivity |
|
|
233
324
|
|
|
325
|
+
### Pin interaction
|
|
326
|
+
|
|
327
|
+
Pinning a panel overrides auto-hide entirely for that panel — it stays visible regardless of inactivity. Unpinning re-enables the timer. Each panel is tracked independently; pinning one panel in a group has no effect on its neighbours.
|
|
328
|
+
|
|
234
329
|
### Events
|
|
235
330
|
|
|
236
331
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(index_exports, {
|
|
|
39
39
|
getDefaultZIndex: () => getDefaultZIndex,
|
|
40
40
|
getDragZIndex: () => getDragZIndex,
|
|
41
41
|
getLeftmostPanel: () => getLeftmostPanel,
|
|
42
|
+
getMovingGroupRespectingPins: () => getMovingGroupRespectingPins,
|
|
42
43
|
getPanelDimensions: () => getPanelDimensions,
|
|
43
44
|
getPanelPosition: () => getPanelPosition,
|
|
44
45
|
getRightmostPanel: () => getRightmostPanel,
|
|
@@ -50,6 +51,7 @@ __export(index_exports, {
|
|
|
50
51
|
snapPanels: () => snapPanels,
|
|
51
52
|
snapPanelsToTarget: () => snapPanelsToTarget,
|
|
52
53
|
toggleCollapse: () => toggleCollapse,
|
|
54
|
+
togglePin: () => togglePin,
|
|
53
55
|
unsnap: () => unsnap,
|
|
54
56
|
updateSnappedPositions: () => updateSnappedPositions
|
|
55
57
|
});
|
|
@@ -199,6 +201,8 @@ function setupHoverEnlarge(config) {
|
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
// src/Panel.ts
|
|
204
|
+
var PIN_ICON_UNPINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
|
|
205
|
+
var PIN_ICON_PINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;transform:rotate(90deg)"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
|
|
202
206
|
function createPanelElement(config, classes) {
|
|
203
207
|
const element = document.createElement("div");
|
|
204
208
|
element.className = classes.panel;
|
|
@@ -221,6 +225,14 @@ function createPanelElement(config, classes) {
|
|
|
221
225
|
title.textContent = config.title;
|
|
222
226
|
header.appendChild(title);
|
|
223
227
|
}
|
|
228
|
+
let pinButton = null;
|
|
229
|
+
if (config.pinnable === true) {
|
|
230
|
+
pinButton = document.createElement("button");
|
|
231
|
+
pinButton.className = classes.pinButton;
|
|
232
|
+
pinButton.id = `${config.id}-pin-btn`;
|
|
233
|
+
pinButton.innerHTML = config.startPinned === true ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
234
|
+
header.appendChild(pinButton);
|
|
235
|
+
}
|
|
224
236
|
let collapseButton = null;
|
|
225
237
|
if (config.collapsible !== false) {
|
|
226
238
|
collapseButton = document.createElement("button");
|
|
@@ -247,6 +259,7 @@ function createPanelElement(config, classes) {
|
|
|
247
259
|
return {
|
|
248
260
|
element,
|
|
249
261
|
dragHandle: header,
|
|
262
|
+
pinButton,
|
|
250
263
|
collapseButton,
|
|
251
264
|
contentWrapper,
|
|
252
265
|
detachGrip
|
|
@@ -255,12 +268,14 @@ function createPanelElement(config, classes) {
|
|
|
255
268
|
function createPanelState(config, classes, globalConfig) {
|
|
256
269
|
let element;
|
|
257
270
|
let dragHandle;
|
|
271
|
+
let pinButton;
|
|
258
272
|
let collapseButton;
|
|
259
273
|
let contentWrapper;
|
|
260
274
|
let detachGrip;
|
|
261
275
|
if (config.element) {
|
|
262
276
|
element = config.element;
|
|
263
277
|
dragHandle = config.dragHandle ?? element.querySelector(`.${classes.panelHeader}`);
|
|
278
|
+
pinButton = config.pinButton ?? element.querySelector(`.${classes.pinButton}`);
|
|
264
279
|
collapseButton = config.collapseButton ?? element.querySelector(`.${classes.collapseButton}`);
|
|
265
280
|
contentWrapper = config.contentWrapper ?? element.querySelector(`.${classes.panelContent}`);
|
|
266
281
|
detachGrip = config.detachGrip ?? element.querySelector(`.${classes.detachGrip}`);
|
|
@@ -268,6 +283,7 @@ function createPanelState(config, classes, globalConfig) {
|
|
|
268
283
|
const created = createPanelElement(config, classes);
|
|
269
284
|
element = created.element;
|
|
270
285
|
dragHandle = created.dragHandle;
|
|
286
|
+
pinButton = created.pinButton;
|
|
271
287
|
collapseButton = created.collapseButton;
|
|
272
288
|
contentWrapper = created.contentWrapper;
|
|
273
289
|
detachGrip = created.detachGrip;
|
|
@@ -281,9 +297,11 @@ function createPanelState(config, classes, globalConfig) {
|
|
|
281
297
|
id: config.id,
|
|
282
298
|
element,
|
|
283
299
|
dragHandle,
|
|
300
|
+
pinButton,
|
|
284
301
|
collapseButton,
|
|
285
302
|
contentWrapper,
|
|
286
303
|
detachGrip,
|
|
304
|
+
isPinned: config.startPinned === true,
|
|
287
305
|
isCollapsed: config.startCollapsed !== false,
|
|
288
306
|
snappedTo: null,
|
|
289
307
|
snappedFrom: null,
|
|
@@ -305,6 +323,14 @@ function toggleCollapse(state, classes, collapsed) {
|
|
|
305
323
|
}
|
|
306
324
|
return newState;
|
|
307
325
|
}
|
|
326
|
+
function togglePin(state, pinned) {
|
|
327
|
+
const newState = pinned ?? !state.isPinned;
|
|
328
|
+
state.isPinned = newState;
|
|
329
|
+
if (state.pinButton) {
|
|
330
|
+
state.pinButton.innerHTML = newState ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
331
|
+
}
|
|
332
|
+
return newState;
|
|
333
|
+
}
|
|
308
334
|
function showPanel(state, classes) {
|
|
309
335
|
if (!state.isHidden) return;
|
|
310
336
|
state.isHidden = false;
|
|
@@ -482,6 +508,28 @@ function snapPanels(leftPanel, rightPanel) {
|
|
|
482
508
|
leftPanel.snappedTo = rightPanel.id;
|
|
483
509
|
rightPanel.snappedFrom = leftPanel.id;
|
|
484
510
|
}
|
|
511
|
+
function getMovingGroupRespectingPins(grabbedPanel, panels) {
|
|
512
|
+
if (grabbedPanel.isPinned) return [];
|
|
513
|
+
const fullGroup = getConnectedGroup(grabbedPanel, panels);
|
|
514
|
+
const grabbedIndex = fullGroup.indexOf(grabbedPanel);
|
|
515
|
+
const leftPanels = [];
|
|
516
|
+
for (let i = grabbedIndex; i >= 0; i--) {
|
|
517
|
+
if (fullGroup[i].isPinned) {
|
|
518
|
+
unsnap(fullGroup[i], fullGroup[i + 1]);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
leftPanels.unshift(fullGroup[i]);
|
|
522
|
+
}
|
|
523
|
+
const rightPanels = [];
|
|
524
|
+
for (let i = grabbedIndex + 1; i < fullGroup.length; i++) {
|
|
525
|
+
if (fullGroup[i].isPinned) {
|
|
526
|
+
unsnap(fullGroup[i - 1], fullGroup[i]);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
rightPanels.push(fullGroup[i]);
|
|
530
|
+
}
|
|
531
|
+
return [...leftPanels, ...rightPanels];
|
|
532
|
+
}
|
|
485
533
|
function unsnap(leftPanel, rightPanel) {
|
|
486
534
|
if (leftPanel.snappedTo === rightPanel.id) {
|
|
487
535
|
leftPanel.snappedTo = null;
|
|
@@ -509,18 +557,18 @@ var DragManager = class {
|
|
|
509
557
|
startDrag(e, panel, mode) {
|
|
510
558
|
e.preventDefault();
|
|
511
559
|
e.stopPropagation();
|
|
512
|
-
const connectedPanels = getConnectedGroup(panel, this.panels);
|
|
513
|
-
const initialGroupPositions = /* @__PURE__ */ new Map();
|
|
514
|
-
for (const p of connectedPanels) {
|
|
515
|
-
const rect2 = p.element.getBoundingClientRect();
|
|
516
|
-
initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
|
|
517
|
-
}
|
|
518
560
|
let movingPanels;
|
|
519
561
|
if (mode === "single") {
|
|
520
562
|
detachFromGroup(panel, this.panels);
|
|
521
563
|
movingPanels = [panel];
|
|
522
564
|
} else {
|
|
523
|
-
movingPanels =
|
|
565
|
+
movingPanels = getMovingGroupRespectingPins(panel, this.panels);
|
|
566
|
+
if (movingPanels.length === 0) return;
|
|
567
|
+
}
|
|
568
|
+
const initialGroupPositions = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const p of movingPanels) {
|
|
570
|
+
const rect2 = p.element.getBoundingClientRect();
|
|
571
|
+
initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
|
|
524
572
|
}
|
|
525
573
|
const rect = panel.element.getBoundingClientRect();
|
|
526
574
|
this.activeDrag = {
|
|
@@ -1010,6 +1058,7 @@ var AutoHideManager = class {
|
|
|
1010
1058
|
*/
|
|
1011
1059
|
handleActivity() {
|
|
1012
1060
|
for (const panel of this.panels.values()) {
|
|
1061
|
+
if (panel.isPinned) continue;
|
|
1013
1062
|
if (this.pausedPanels.has(panel.id)) continue;
|
|
1014
1063
|
if (panel.resolvedAutoHideDelay !== void 0 || panel.isHidden) {
|
|
1015
1064
|
this.show(panel, "activity");
|
|
@@ -1070,9 +1119,27 @@ var AutoHideManager = class {
|
|
|
1070
1119
|
*/
|
|
1071
1120
|
hide(panel, trigger) {
|
|
1072
1121
|
if (panel.isHidden) return;
|
|
1122
|
+
if (panel.isPinned) return;
|
|
1073
1123
|
hidePanel(panel, this.classes);
|
|
1074
1124
|
this.callbacks.onHide?.(panel, trigger);
|
|
1075
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Called when a panel's pin state changes.
|
|
1128
|
+
* Pinning cancels the hide timer and reveals the panel if hidden.
|
|
1129
|
+
* Unpinning restarts the timer if the panel participates in auto-hide.
|
|
1130
|
+
*/
|
|
1131
|
+
onPanelPinChanged(panel) {
|
|
1132
|
+
if (panel.isPinned) {
|
|
1133
|
+
this.clearTimer(panel.id);
|
|
1134
|
+
if (panel.isHidden) {
|
|
1135
|
+
this.show(panel, "api");
|
|
1136
|
+
}
|
|
1137
|
+
} else {
|
|
1138
|
+
if (!panel.isHidden && panel.resolvedAutoHideDelay !== void 0) {
|
|
1139
|
+
this.scheduleHide(panel);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1076
1143
|
/**
|
|
1077
1144
|
* Initialize a newly added panel's auto-hide state
|
|
1078
1145
|
*/
|
|
@@ -1127,6 +1194,7 @@ function generateClasses(prefix) {
|
|
|
1127
1194
|
panelContent: `${prefix}-content`,
|
|
1128
1195
|
panelContentCollapsed: `${prefix}-content-collapsed`,
|
|
1129
1196
|
detachGrip: `${prefix}-detach-grip`,
|
|
1197
|
+
pinButton: `${prefix}-pin-btn`,
|
|
1130
1198
|
collapseButton: `${prefix}-collapse-btn`,
|
|
1131
1199
|
snapPreview: `${prefix}-snap-preview`,
|
|
1132
1200
|
snapPreviewVisible: `${prefix}-snap-preview-visible`,
|
|
@@ -1217,6 +1285,7 @@ var TabManager = class {
|
|
|
1217
1285
|
id,
|
|
1218
1286
|
element,
|
|
1219
1287
|
dragHandle: options.dragHandle,
|
|
1288
|
+
pinButton: options.pinButton,
|
|
1220
1289
|
collapseButton: options.collapseButton,
|
|
1221
1290
|
contentWrapper: options.contentWrapper,
|
|
1222
1291
|
detachGrip: options.detachGrip,
|
|
@@ -1306,15 +1375,25 @@ var TabManager = class {
|
|
|
1306
1375
|
this.emit("panel:collapse", { panel: state, isCollapsed: newState });
|
|
1307
1376
|
});
|
|
1308
1377
|
}
|
|
1378
|
+
if (state.pinButton) {
|
|
1379
|
+
state.pinButton.addEventListener("click", () => {
|
|
1380
|
+
const isPinned = togglePin(state);
|
|
1381
|
+
this.autoHideManager.onPanelPinChanged(state);
|
|
1382
|
+
this.emit("panel:pin", { panel: state, isPinned });
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1309
1385
|
if (state.detachGrip) {
|
|
1310
1386
|
state.detachGrip.addEventListener("mousedown", (e) => {
|
|
1387
|
+
if (state.isPinned) return;
|
|
1311
1388
|
this.dragManager.startDrag(e, state, "single");
|
|
1312
1389
|
});
|
|
1313
1390
|
}
|
|
1314
1391
|
state.dragHandle.addEventListener("mousedown", (e) => {
|
|
1315
|
-
|
|
1392
|
+
const target = e.target;
|
|
1393
|
+
if (e.target === state.collapseButton || e.target === state.detachGrip || state.pinButton && state.pinButton.contains(target)) {
|
|
1316
1394
|
return;
|
|
1317
1395
|
}
|
|
1396
|
+
if (state.isPinned) return;
|
|
1318
1397
|
this.dragManager.startDrag(e, state, "group");
|
|
1319
1398
|
});
|
|
1320
1399
|
}
|
|
@@ -1575,6 +1654,7 @@ var TabManager = class {
|
|
|
1575
1654
|
getDefaultZIndex,
|
|
1576
1655
|
getDragZIndex,
|
|
1577
1656
|
getLeftmostPanel,
|
|
1657
|
+
getMovingGroupRespectingPins,
|
|
1578
1658
|
getPanelDimensions,
|
|
1579
1659
|
getPanelPosition,
|
|
1580
1660
|
getRightmostPanel,
|
|
@@ -1586,6 +1666,7 @@ var TabManager = class {
|
|
|
1586
1666
|
snapPanels,
|
|
1587
1667
|
snapPanelsToTarget,
|
|
1588
1668
|
toggleCollapse,
|
|
1669
|
+
togglePin,
|
|
1589
1670
|
unsnap,
|
|
1590
1671
|
updateSnappedPositions
|
|
1591
1672
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -52,6 +52,8 @@ interface PanelConfig {
|
|
|
52
52
|
title?: string;
|
|
53
53
|
/** Initial width (default: 300) */
|
|
54
54
|
width?: number;
|
|
55
|
+
/** Initial pin state (default: false) */
|
|
56
|
+
startPinned?: boolean;
|
|
55
57
|
/** Initial collapsed state (default: true) */
|
|
56
58
|
startCollapsed?: boolean;
|
|
57
59
|
/** Initial position (optional - will be auto-positioned if not provided) */
|
|
@@ -65,12 +67,16 @@ interface PanelConfig {
|
|
|
65
67
|
element?: HTMLDivElement;
|
|
66
68
|
/** Custom drag handle element */
|
|
67
69
|
dragHandle?: HTMLDivElement;
|
|
70
|
+
/** Custom pin button element */
|
|
71
|
+
pinButton?: HTMLButtonElement;
|
|
68
72
|
/** Custom collapse button element */
|
|
69
73
|
collapseButton?: HTMLButtonElement;
|
|
70
74
|
/** Custom content wrapper element */
|
|
71
75
|
contentWrapper?: HTMLDivElement;
|
|
72
76
|
/** Custom detach grip element */
|
|
73
77
|
detachGrip?: HTMLDivElement;
|
|
78
|
+
/** Whether panel can be pinned (default: false) */
|
|
79
|
+
pinnable?: boolean;
|
|
74
80
|
/** Whether panel can be collapsed (default: true) */
|
|
75
81
|
collapsible?: boolean;
|
|
76
82
|
/** Whether panel can be detached from group (default: true) */
|
|
@@ -111,12 +117,16 @@ interface PanelState {
|
|
|
111
117
|
element: HTMLDivElement;
|
|
112
118
|
/** Drag handle element */
|
|
113
119
|
dragHandle: HTMLDivElement;
|
|
120
|
+
/** Pin button element (if pinable) */
|
|
121
|
+
pinButton: HTMLButtonElement | null;
|
|
114
122
|
/** Collapse button element (if collapsible) */
|
|
115
123
|
collapseButton: HTMLButtonElement | null;
|
|
116
124
|
/** Content wrapper element */
|
|
117
125
|
contentWrapper: HTMLDivElement;
|
|
118
126
|
/** Detach grip element (if detachable) */
|
|
119
127
|
detachGrip: HTMLDivElement | null;
|
|
128
|
+
/** Whether panel is currently pinned */
|
|
129
|
+
isPinned: boolean;
|
|
120
130
|
/** Whether panel is currently collapsed */
|
|
121
131
|
isCollapsed: boolean;
|
|
122
132
|
/** ID of panel this is snapped to on its right (outgoing link) */
|
|
@@ -226,6 +236,8 @@ interface TabManagerEvents {
|
|
|
226
236
|
'snap:anchor': AnchorSnapEvent;
|
|
227
237
|
/** Fired when a panel is detached from group */
|
|
228
238
|
'panel:detached': PanelDetachedEvent;
|
|
239
|
+
/** Fired when panel pin state changes */
|
|
240
|
+
'panel:pin': PanelPinEvent;
|
|
229
241
|
/** Fired when panel collapse state changes */
|
|
230
242
|
'panel:collapse': PanelCollapseEvent;
|
|
231
243
|
/** Fired when a panel becomes visible (auto-hide) */
|
|
@@ -269,6 +281,10 @@ interface PanelDetachedEvent {
|
|
|
269
281
|
panel: PanelState;
|
|
270
282
|
previousGroup: PanelState[];
|
|
271
283
|
}
|
|
284
|
+
interface PanelPinEvent {
|
|
285
|
+
panel: PanelState;
|
|
286
|
+
isPinned: boolean;
|
|
287
|
+
}
|
|
272
288
|
interface PanelCollapseEvent {
|
|
273
289
|
panel: PanelState;
|
|
274
290
|
isCollapsed: boolean;
|
|
@@ -295,6 +311,7 @@ interface CSSClasses {
|
|
|
295
311
|
panelContent: string;
|
|
296
312
|
panelContentCollapsed: string;
|
|
297
313
|
detachGrip: string;
|
|
314
|
+
pinButton: string;
|
|
298
315
|
collapseButton: string;
|
|
299
316
|
snapPreview: string;
|
|
300
317
|
snapPreviewVisible: string;
|
|
@@ -403,6 +420,7 @@ declare class TabManager {
|
|
|
403
420
|
*/
|
|
404
421
|
registerPanel(id: string, element: HTMLDivElement, options?: {
|
|
405
422
|
dragHandle?: HTMLDivElement;
|
|
423
|
+
pinButton?: HTMLButtonElement;
|
|
406
424
|
collapseButton?: HTMLButtonElement;
|
|
407
425
|
contentWrapper?: HTMLDivElement;
|
|
408
426
|
detachGrip?: HTMLDivElement;
|
|
@@ -734,6 +752,12 @@ declare class AutoHideManager {
|
|
|
734
752
|
* Hide a panel
|
|
735
753
|
*/
|
|
736
754
|
hide(panel: PanelState, trigger: 'timeout' | 'api'): void;
|
|
755
|
+
/**
|
|
756
|
+
* Called when a panel's pin state changes.
|
|
757
|
+
* Pinning cancels the hide timer and reveals the panel if hidden.
|
|
758
|
+
* Unpinning restarts the timer if the panel participates in auto-hide.
|
|
759
|
+
*/
|
|
760
|
+
onPanelPinChanged(panel: PanelState): void;
|
|
737
761
|
/**
|
|
738
762
|
* Initialize a newly added panel's auto-hide state
|
|
739
763
|
*/
|
|
@@ -810,6 +834,7 @@ declare function setupHoverEnlarge(config: HoverEnlargeConfig): void;
|
|
|
810
834
|
declare function createPanelElement(config: PanelConfig, classes: CSSClasses): {
|
|
811
835
|
element: HTMLDivElement;
|
|
812
836
|
dragHandle: HTMLDivElement;
|
|
837
|
+
pinButton: HTMLButtonElement | null;
|
|
813
838
|
collapseButton: HTMLButtonElement | null;
|
|
814
839
|
contentWrapper: HTMLDivElement;
|
|
815
840
|
detachGrip: HTMLDivElement | null;
|
|
@@ -825,6 +850,10 @@ declare function createPanelState(config: PanelConfig, classes: CSSClasses, glob
|
|
|
825
850
|
* Toggle panel collapse state
|
|
826
851
|
*/
|
|
827
852
|
declare function toggleCollapse(state: PanelState, classes: CSSClasses, collapsed?: boolean): boolean;
|
|
853
|
+
/**
|
|
854
|
+
* Toggle panel pin state
|
|
855
|
+
*/
|
|
856
|
+
declare function togglePin(state: PanelState, pinned?: boolean): boolean;
|
|
828
857
|
/**
|
|
829
858
|
* Show a hidden panel
|
|
830
859
|
*/
|
|
@@ -909,9 +938,21 @@ declare function areInSameChain(panel1: PanelState, panel2: PanelState, panels:
|
|
|
909
938
|
* Establish a snap relationship between two panels
|
|
910
939
|
*/
|
|
911
940
|
declare function snapPanels(leftPanel: PanelState, rightPanel: PanelState): void;
|
|
941
|
+
/**
|
|
942
|
+
* Get the subset of connected panels that should move when a panel is grabbed,
|
|
943
|
+
* stopping at any pinned panel in the chain.
|
|
944
|
+
*
|
|
945
|
+
* Pinned panels act as immoveable barriers — the chain splits at each one,
|
|
946
|
+
* and only the panels on the same side as the grabbed panel are returned.
|
|
947
|
+
* The snap bonds at each pin boundary are severed as a side effect.
|
|
948
|
+
*
|
|
949
|
+
* Example: chain [A, B, P(pinned), C, D], grab B → returns [A, B], unseats B↔P
|
|
950
|
+
* Example: chain [A, P(pinned), B, C], grab C → returns [B, C], unseats P↔B
|
|
951
|
+
*/
|
|
952
|
+
declare function getMovingGroupRespectingPins(grabbedPanel: PanelState, panels: Map<string, PanelState>): PanelState[];
|
|
912
953
|
/**
|
|
913
954
|
* Break the snap relationship between two specific panels
|
|
914
955
|
*/
|
|
915
956
|
declare function unsnap(leftPanel: PanelState, rightPanel: PanelState): void;
|
|
916
957
|
|
|
917
|
-
export { type AnchorConfig, AnchorManager, type AnchorPreset, type AnchorSnapEvent, type AnchorSnapResult, type AnchorState, type AutoHideCallbacks, AutoHideManager, type Bounds, type CSSClasses, type DebugLog, type DebugLogConfig, type DebugLogLevel, type DebugPanel, type DebugPanelConfig, type DragEndEvent, DragManager, type DragMode, type DragMoveEvent, type DragStartEvent, type DragState, type EventListener, type PanelAddedEvent, type PanelCollapseEvent, type PanelConfig, type PanelDetachedEvent, type PanelHideEvent, type PanelRemovedEvent, type PanelShowEvent, type PanelSnapEvent, type PanelState, type Position, type ResolvedTabManagerConfig, SnapPreview, type SnapSide, type SnapTarget, TabManager, type TabManagerConfig, type TabManagerEvents, areInSameChain, createDebugLog, createDebugPanelContent, createDebugPanelInterface, createPanelElement, createPanelState, createPresetAnchor, detachFromGroup, findSnapTarget, getConnectedGroup, getDefaultAnchorConfigs, getDefaultZIndex, getDragZIndex, getLeftmostPanel, getPanelDimensions, getPanelPosition, getRightmostPanel, hidePanel, setPanelPosition, setPanelZIndex, setupHoverEnlarge, showPanel, snapPanels, snapPanelsToTarget, toggleCollapse, unsnap, updateSnappedPositions };
|
|
958
|
+
export { type AnchorConfig, AnchorManager, type AnchorPreset, type AnchorSnapEvent, type AnchorSnapResult, type AnchorState, type AutoHideCallbacks, AutoHideManager, type Bounds, type CSSClasses, type DebugLog, type DebugLogConfig, type DebugLogLevel, type DebugPanel, type DebugPanelConfig, type DragEndEvent, DragManager, type DragMode, type DragMoveEvent, type DragStartEvent, type DragState, type EventListener, type PanelAddedEvent, type PanelCollapseEvent, type PanelConfig, type PanelDetachedEvent, type PanelHideEvent, type PanelPinEvent, type PanelRemovedEvent, type PanelShowEvent, type PanelSnapEvent, type PanelState, type Position, type ResolvedTabManagerConfig, SnapPreview, type SnapSide, type SnapTarget, TabManager, type TabManagerConfig, type TabManagerEvents, areInSameChain, createDebugLog, createDebugPanelContent, createDebugPanelInterface, createPanelElement, createPanelState, createPresetAnchor, detachFromGroup, findSnapTarget, getConnectedGroup, getDefaultAnchorConfigs, getDefaultZIndex, getDragZIndex, getLeftmostPanel, getMovingGroupRespectingPins, getPanelDimensions, getPanelPosition, getRightmostPanel, hidePanel, setPanelPosition, setPanelZIndex, setupHoverEnlarge, showPanel, snapPanels, snapPanelsToTarget, toggleCollapse, togglePin, unsnap, updateSnappedPositions };
|
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ interface PanelConfig {
|
|
|
52
52
|
title?: string;
|
|
53
53
|
/** Initial width (default: 300) */
|
|
54
54
|
width?: number;
|
|
55
|
+
/** Initial pin state (default: false) */
|
|
56
|
+
startPinned?: boolean;
|
|
55
57
|
/** Initial collapsed state (default: true) */
|
|
56
58
|
startCollapsed?: boolean;
|
|
57
59
|
/** Initial position (optional - will be auto-positioned if not provided) */
|
|
@@ -65,12 +67,16 @@ interface PanelConfig {
|
|
|
65
67
|
element?: HTMLDivElement;
|
|
66
68
|
/** Custom drag handle element */
|
|
67
69
|
dragHandle?: HTMLDivElement;
|
|
70
|
+
/** Custom pin button element */
|
|
71
|
+
pinButton?: HTMLButtonElement;
|
|
68
72
|
/** Custom collapse button element */
|
|
69
73
|
collapseButton?: HTMLButtonElement;
|
|
70
74
|
/** Custom content wrapper element */
|
|
71
75
|
contentWrapper?: HTMLDivElement;
|
|
72
76
|
/** Custom detach grip element */
|
|
73
77
|
detachGrip?: HTMLDivElement;
|
|
78
|
+
/** Whether panel can be pinned (default: false) */
|
|
79
|
+
pinnable?: boolean;
|
|
74
80
|
/** Whether panel can be collapsed (default: true) */
|
|
75
81
|
collapsible?: boolean;
|
|
76
82
|
/** Whether panel can be detached from group (default: true) */
|
|
@@ -111,12 +117,16 @@ interface PanelState {
|
|
|
111
117
|
element: HTMLDivElement;
|
|
112
118
|
/** Drag handle element */
|
|
113
119
|
dragHandle: HTMLDivElement;
|
|
120
|
+
/** Pin button element (if pinable) */
|
|
121
|
+
pinButton: HTMLButtonElement | null;
|
|
114
122
|
/** Collapse button element (if collapsible) */
|
|
115
123
|
collapseButton: HTMLButtonElement | null;
|
|
116
124
|
/** Content wrapper element */
|
|
117
125
|
contentWrapper: HTMLDivElement;
|
|
118
126
|
/** Detach grip element (if detachable) */
|
|
119
127
|
detachGrip: HTMLDivElement | null;
|
|
128
|
+
/** Whether panel is currently pinned */
|
|
129
|
+
isPinned: boolean;
|
|
120
130
|
/** Whether panel is currently collapsed */
|
|
121
131
|
isCollapsed: boolean;
|
|
122
132
|
/** ID of panel this is snapped to on its right (outgoing link) */
|
|
@@ -226,6 +236,8 @@ interface TabManagerEvents {
|
|
|
226
236
|
'snap:anchor': AnchorSnapEvent;
|
|
227
237
|
/** Fired when a panel is detached from group */
|
|
228
238
|
'panel:detached': PanelDetachedEvent;
|
|
239
|
+
/** Fired when panel pin state changes */
|
|
240
|
+
'panel:pin': PanelPinEvent;
|
|
229
241
|
/** Fired when panel collapse state changes */
|
|
230
242
|
'panel:collapse': PanelCollapseEvent;
|
|
231
243
|
/** Fired when a panel becomes visible (auto-hide) */
|
|
@@ -269,6 +281,10 @@ interface PanelDetachedEvent {
|
|
|
269
281
|
panel: PanelState;
|
|
270
282
|
previousGroup: PanelState[];
|
|
271
283
|
}
|
|
284
|
+
interface PanelPinEvent {
|
|
285
|
+
panel: PanelState;
|
|
286
|
+
isPinned: boolean;
|
|
287
|
+
}
|
|
272
288
|
interface PanelCollapseEvent {
|
|
273
289
|
panel: PanelState;
|
|
274
290
|
isCollapsed: boolean;
|
|
@@ -295,6 +311,7 @@ interface CSSClasses {
|
|
|
295
311
|
panelContent: string;
|
|
296
312
|
panelContentCollapsed: string;
|
|
297
313
|
detachGrip: string;
|
|
314
|
+
pinButton: string;
|
|
298
315
|
collapseButton: string;
|
|
299
316
|
snapPreview: string;
|
|
300
317
|
snapPreviewVisible: string;
|
|
@@ -403,6 +420,7 @@ declare class TabManager {
|
|
|
403
420
|
*/
|
|
404
421
|
registerPanel(id: string, element: HTMLDivElement, options?: {
|
|
405
422
|
dragHandle?: HTMLDivElement;
|
|
423
|
+
pinButton?: HTMLButtonElement;
|
|
406
424
|
collapseButton?: HTMLButtonElement;
|
|
407
425
|
contentWrapper?: HTMLDivElement;
|
|
408
426
|
detachGrip?: HTMLDivElement;
|
|
@@ -734,6 +752,12 @@ declare class AutoHideManager {
|
|
|
734
752
|
* Hide a panel
|
|
735
753
|
*/
|
|
736
754
|
hide(panel: PanelState, trigger: 'timeout' | 'api'): void;
|
|
755
|
+
/**
|
|
756
|
+
* Called when a panel's pin state changes.
|
|
757
|
+
* Pinning cancels the hide timer and reveals the panel if hidden.
|
|
758
|
+
* Unpinning restarts the timer if the panel participates in auto-hide.
|
|
759
|
+
*/
|
|
760
|
+
onPanelPinChanged(panel: PanelState): void;
|
|
737
761
|
/**
|
|
738
762
|
* Initialize a newly added panel's auto-hide state
|
|
739
763
|
*/
|
|
@@ -810,6 +834,7 @@ declare function setupHoverEnlarge(config: HoverEnlargeConfig): void;
|
|
|
810
834
|
declare function createPanelElement(config: PanelConfig, classes: CSSClasses): {
|
|
811
835
|
element: HTMLDivElement;
|
|
812
836
|
dragHandle: HTMLDivElement;
|
|
837
|
+
pinButton: HTMLButtonElement | null;
|
|
813
838
|
collapseButton: HTMLButtonElement | null;
|
|
814
839
|
contentWrapper: HTMLDivElement;
|
|
815
840
|
detachGrip: HTMLDivElement | null;
|
|
@@ -825,6 +850,10 @@ declare function createPanelState(config: PanelConfig, classes: CSSClasses, glob
|
|
|
825
850
|
* Toggle panel collapse state
|
|
826
851
|
*/
|
|
827
852
|
declare function toggleCollapse(state: PanelState, classes: CSSClasses, collapsed?: boolean): boolean;
|
|
853
|
+
/**
|
|
854
|
+
* Toggle panel pin state
|
|
855
|
+
*/
|
|
856
|
+
declare function togglePin(state: PanelState, pinned?: boolean): boolean;
|
|
828
857
|
/**
|
|
829
858
|
* Show a hidden panel
|
|
830
859
|
*/
|
|
@@ -909,9 +938,21 @@ declare function areInSameChain(panel1: PanelState, panel2: PanelState, panels:
|
|
|
909
938
|
* Establish a snap relationship between two panels
|
|
910
939
|
*/
|
|
911
940
|
declare function snapPanels(leftPanel: PanelState, rightPanel: PanelState): void;
|
|
941
|
+
/**
|
|
942
|
+
* Get the subset of connected panels that should move when a panel is grabbed,
|
|
943
|
+
* stopping at any pinned panel in the chain.
|
|
944
|
+
*
|
|
945
|
+
* Pinned panels act as immoveable barriers — the chain splits at each one,
|
|
946
|
+
* and only the panels on the same side as the grabbed panel are returned.
|
|
947
|
+
* The snap bonds at each pin boundary are severed as a side effect.
|
|
948
|
+
*
|
|
949
|
+
* Example: chain [A, B, P(pinned), C, D], grab B → returns [A, B], unseats B↔P
|
|
950
|
+
* Example: chain [A, P(pinned), B, C], grab C → returns [B, C], unseats P↔B
|
|
951
|
+
*/
|
|
952
|
+
declare function getMovingGroupRespectingPins(grabbedPanel: PanelState, panels: Map<string, PanelState>): PanelState[];
|
|
912
953
|
/**
|
|
913
954
|
* Break the snap relationship between two specific panels
|
|
914
955
|
*/
|
|
915
956
|
declare function unsnap(leftPanel: PanelState, rightPanel: PanelState): void;
|
|
916
957
|
|
|
917
|
-
export { type AnchorConfig, AnchorManager, type AnchorPreset, type AnchorSnapEvent, type AnchorSnapResult, type AnchorState, type AutoHideCallbacks, AutoHideManager, type Bounds, type CSSClasses, type DebugLog, type DebugLogConfig, type DebugLogLevel, type DebugPanel, type DebugPanelConfig, type DragEndEvent, DragManager, type DragMode, type DragMoveEvent, type DragStartEvent, type DragState, type EventListener, type PanelAddedEvent, type PanelCollapseEvent, type PanelConfig, type PanelDetachedEvent, type PanelHideEvent, type PanelRemovedEvent, type PanelShowEvent, type PanelSnapEvent, type PanelState, type Position, type ResolvedTabManagerConfig, SnapPreview, type SnapSide, type SnapTarget, TabManager, type TabManagerConfig, type TabManagerEvents, areInSameChain, createDebugLog, createDebugPanelContent, createDebugPanelInterface, createPanelElement, createPanelState, createPresetAnchor, detachFromGroup, findSnapTarget, getConnectedGroup, getDefaultAnchorConfigs, getDefaultZIndex, getDragZIndex, getLeftmostPanel, getPanelDimensions, getPanelPosition, getRightmostPanel, hidePanel, setPanelPosition, setPanelZIndex, setupHoverEnlarge, showPanel, snapPanels, snapPanelsToTarget, toggleCollapse, unsnap, updateSnappedPositions };
|
|
958
|
+
export { type AnchorConfig, AnchorManager, type AnchorPreset, type AnchorSnapEvent, type AnchorSnapResult, type AnchorState, type AutoHideCallbacks, AutoHideManager, type Bounds, type CSSClasses, type DebugLog, type DebugLogConfig, type DebugLogLevel, type DebugPanel, type DebugPanelConfig, type DragEndEvent, DragManager, type DragMode, type DragMoveEvent, type DragStartEvent, type DragState, type EventListener, type PanelAddedEvent, type PanelCollapseEvent, type PanelConfig, type PanelDetachedEvent, type PanelHideEvent, type PanelPinEvent, type PanelRemovedEvent, type PanelShowEvent, type PanelSnapEvent, type PanelState, type Position, type ResolvedTabManagerConfig, SnapPreview, type SnapSide, type SnapTarget, TabManager, type TabManagerConfig, type TabManagerEvents, areInSameChain, createDebugLog, createDebugPanelContent, createDebugPanelInterface, createPanelElement, createPanelState, createPresetAnchor, detachFromGroup, findSnapTarget, getConnectedGroup, getDefaultAnchorConfigs, getDefaultZIndex, getDragZIndex, getLeftmostPanel, getMovingGroupRespectingPins, getPanelDimensions, getPanelPosition, getRightmostPanel, hidePanel, setPanelPosition, setPanelZIndex, setupHoverEnlarge, showPanel, snapPanels, snapPanelsToTarget, toggleCollapse, togglePin, unsnap, updateSnappedPositions };
|
package/dist/index.js
CHANGED
|
@@ -142,6 +142,8 @@ function setupHoverEnlarge(config) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// src/Panel.ts
|
|
145
|
+
var PIN_ICON_UNPINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
|
|
146
|
+
var PIN_ICON_PINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;transform:rotate(90deg)"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
|
|
145
147
|
function createPanelElement(config, classes) {
|
|
146
148
|
const element = document.createElement("div");
|
|
147
149
|
element.className = classes.panel;
|
|
@@ -164,6 +166,14 @@ function createPanelElement(config, classes) {
|
|
|
164
166
|
title.textContent = config.title;
|
|
165
167
|
header.appendChild(title);
|
|
166
168
|
}
|
|
169
|
+
let pinButton = null;
|
|
170
|
+
if (config.pinnable === true) {
|
|
171
|
+
pinButton = document.createElement("button");
|
|
172
|
+
pinButton.className = classes.pinButton;
|
|
173
|
+
pinButton.id = `${config.id}-pin-btn`;
|
|
174
|
+
pinButton.innerHTML = config.startPinned === true ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
175
|
+
header.appendChild(pinButton);
|
|
176
|
+
}
|
|
167
177
|
let collapseButton = null;
|
|
168
178
|
if (config.collapsible !== false) {
|
|
169
179
|
collapseButton = document.createElement("button");
|
|
@@ -190,6 +200,7 @@ function createPanelElement(config, classes) {
|
|
|
190
200
|
return {
|
|
191
201
|
element,
|
|
192
202
|
dragHandle: header,
|
|
203
|
+
pinButton,
|
|
193
204
|
collapseButton,
|
|
194
205
|
contentWrapper,
|
|
195
206
|
detachGrip
|
|
@@ -198,12 +209,14 @@ function createPanelElement(config, classes) {
|
|
|
198
209
|
function createPanelState(config, classes, globalConfig) {
|
|
199
210
|
let element;
|
|
200
211
|
let dragHandle;
|
|
212
|
+
let pinButton;
|
|
201
213
|
let collapseButton;
|
|
202
214
|
let contentWrapper;
|
|
203
215
|
let detachGrip;
|
|
204
216
|
if (config.element) {
|
|
205
217
|
element = config.element;
|
|
206
218
|
dragHandle = config.dragHandle ?? element.querySelector(`.${classes.panelHeader}`);
|
|
219
|
+
pinButton = config.pinButton ?? element.querySelector(`.${classes.pinButton}`);
|
|
207
220
|
collapseButton = config.collapseButton ?? element.querySelector(`.${classes.collapseButton}`);
|
|
208
221
|
contentWrapper = config.contentWrapper ?? element.querySelector(`.${classes.panelContent}`);
|
|
209
222
|
detachGrip = config.detachGrip ?? element.querySelector(`.${classes.detachGrip}`);
|
|
@@ -211,6 +224,7 @@ function createPanelState(config, classes, globalConfig) {
|
|
|
211
224
|
const created = createPanelElement(config, classes);
|
|
212
225
|
element = created.element;
|
|
213
226
|
dragHandle = created.dragHandle;
|
|
227
|
+
pinButton = created.pinButton;
|
|
214
228
|
collapseButton = created.collapseButton;
|
|
215
229
|
contentWrapper = created.contentWrapper;
|
|
216
230
|
detachGrip = created.detachGrip;
|
|
@@ -224,9 +238,11 @@ function createPanelState(config, classes, globalConfig) {
|
|
|
224
238
|
id: config.id,
|
|
225
239
|
element,
|
|
226
240
|
dragHandle,
|
|
241
|
+
pinButton,
|
|
227
242
|
collapseButton,
|
|
228
243
|
contentWrapper,
|
|
229
244
|
detachGrip,
|
|
245
|
+
isPinned: config.startPinned === true,
|
|
230
246
|
isCollapsed: config.startCollapsed !== false,
|
|
231
247
|
snappedTo: null,
|
|
232
248
|
snappedFrom: null,
|
|
@@ -248,6 +264,14 @@ function toggleCollapse(state, classes, collapsed) {
|
|
|
248
264
|
}
|
|
249
265
|
return newState;
|
|
250
266
|
}
|
|
267
|
+
function togglePin(state, pinned) {
|
|
268
|
+
const newState = pinned ?? !state.isPinned;
|
|
269
|
+
state.isPinned = newState;
|
|
270
|
+
if (state.pinButton) {
|
|
271
|
+
state.pinButton.innerHTML = newState ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
272
|
+
}
|
|
273
|
+
return newState;
|
|
274
|
+
}
|
|
251
275
|
function showPanel(state, classes) {
|
|
252
276
|
if (!state.isHidden) return;
|
|
253
277
|
state.isHidden = false;
|
|
@@ -425,6 +449,28 @@ function snapPanels(leftPanel, rightPanel) {
|
|
|
425
449
|
leftPanel.snappedTo = rightPanel.id;
|
|
426
450
|
rightPanel.snappedFrom = leftPanel.id;
|
|
427
451
|
}
|
|
452
|
+
function getMovingGroupRespectingPins(grabbedPanel, panels) {
|
|
453
|
+
if (grabbedPanel.isPinned) return [];
|
|
454
|
+
const fullGroup = getConnectedGroup(grabbedPanel, panels);
|
|
455
|
+
const grabbedIndex = fullGroup.indexOf(grabbedPanel);
|
|
456
|
+
const leftPanels = [];
|
|
457
|
+
for (let i = grabbedIndex; i >= 0; i--) {
|
|
458
|
+
if (fullGroup[i].isPinned) {
|
|
459
|
+
unsnap(fullGroup[i], fullGroup[i + 1]);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
leftPanels.unshift(fullGroup[i]);
|
|
463
|
+
}
|
|
464
|
+
const rightPanels = [];
|
|
465
|
+
for (let i = grabbedIndex + 1; i < fullGroup.length; i++) {
|
|
466
|
+
if (fullGroup[i].isPinned) {
|
|
467
|
+
unsnap(fullGroup[i - 1], fullGroup[i]);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
rightPanels.push(fullGroup[i]);
|
|
471
|
+
}
|
|
472
|
+
return [...leftPanels, ...rightPanels];
|
|
473
|
+
}
|
|
428
474
|
function unsnap(leftPanel, rightPanel) {
|
|
429
475
|
if (leftPanel.snappedTo === rightPanel.id) {
|
|
430
476
|
leftPanel.snappedTo = null;
|
|
@@ -452,18 +498,18 @@ var DragManager = class {
|
|
|
452
498
|
startDrag(e, panel, mode) {
|
|
453
499
|
e.preventDefault();
|
|
454
500
|
e.stopPropagation();
|
|
455
|
-
const connectedPanels = getConnectedGroup(panel, this.panels);
|
|
456
|
-
const initialGroupPositions = /* @__PURE__ */ new Map();
|
|
457
|
-
for (const p of connectedPanels) {
|
|
458
|
-
const rect2 = p.element.getBoundingClientRect();
|
|
459
|
-
initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
|
|
460
|
-
}
|
|
461
501
|
let movingPanels;
|
|
462
502
|
if (mode === "single") {
|
|
463
503
|
detachFromGroup(panel, this.panels);
|
|
464
504
|
movingPanels = [panel];
|
|
465
505
|
} else {
|
|
466
|
-
movingPanels =
|
|
506
|
+
movingPanels = getMovingGroupRespectingPins(panel, this.panels);
|
|
507
|
+
if (movingPanels.length === 0) return;
|
|
508
|
+
}
|
|
509
|
+
const initialGroupPositions = /* @__PURE__ */ new Map();
|
|
510
|
+
for (const p of movingPanels) {
|
|
511
|
+
const rect2 = p.element.getBoundingClientRect();
|
|
512
|
+
initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
|
|
467
513
|
}
|
|
468
514
|
const rect = panel.element.getBoundingClientRect();
|
|
469
515
|
this.activeDrag = {
|
|
@@ -953,6 +999,7 @@ var AutoHideManager = class {
|
|
|
953
999
|
*/
|
|
954
1000
|
handleActivity() {
|
|
955
1001
|
for (const panel of this.panels.values()) {
|
|
1002
|
+
if (panel.isPinned) continue;
|
|
956
1003
|
if (this.pausedPanels.has(panel.id)) continue;
|
|
957
1004
|
if (panel.resolvedAutoHideDelay !== void 0 || panel.isHidden) {
|
|
958
1005
|
this.show(panel, "activity");
|
|
@@ -1013,9 +1060,27 @@ var AutoHideManager = class {
|
|
|
1013
1060
|
*/
|
|
1014
1061
|
hide(panel, trigger) {
|
|
1015
1062
|
if (panel.isHidden) return;
|
|
1063
|
+
if (panel.isPinned) return;
|
|
1016
1064
|
hidePanel(panel, this.classes);
|
|
1017
1065
|
this.callbacks.onHide?.(panel, trigger);
|
|
1018
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Called when a panel's pin state changes.
|
|
1069
|
+
* Pinning cancels the hide timer and reveals the panel if hidden.
|
|
1070
|
+
* Unpinning restarts the timer if the panel participates in auto-hide.
|
|
1071
|
+
*/
|
|
1072
|
+
onPanelPinChanged(panel) {
|
|
1073
|
+
if (panel.isPinned) {
|
|
1074
|
+
this.clearTimer(panel.id);
|
|
1075
|
+
if (panel.isHidden) {
|
|
1076
|
+
this.show(panel, "api");
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
if (!panel.isHidden && panel.resolvedAutoHideDelay !== void 0) {
|
|
1080
|
+
this.scheduleHide(panel);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1019
1084
|
/**
|
|
1020
1085
|
* Initialize a newly added panel's auto-hide state
|
|
1021
1086
|
*/
|
|
@@ -1070,6 +1135,7 @@ function generateClasses(prefix) {
|
|
|
1070
1135
|
panelContent: `${prefix}-content`,
|
|
1071
1136
|
panelContentCollapsed: `${prefix}-content-collapsed`,
|
|
1072
1137
|
detachGrip: `${prefix}-detach-grip`,
|
|
1138
|
+
pinButton: `${prefix}-pin-btn`,
|
|
1073
1139
|
collapseButton: `${prefix}-collapse-btn`,
|
|
1074
1140
|
snapPreview: `${prefix}-snap-preview`,
|
|
1075
1141
|
snapPreviewVisible: `${prefix}-snap-preview-visible`,
|
|
@@ -1160,6 +1226,7 @@ var TabManager = class {
|
|
|
1160
1226
|
id,
|
|
1161
1227
|
element,
|
|
1162
1228
|
dragHandle: options.dragHandle,
|
|
1229
|
+
pinButton: options.pinButton,
|
|
1163
1230
|
collapseButton: options.collapseButton,
|
|
1164
1231
|
contentWrapper: options.contentWrapper,
|
|
1165
1232
|
detachGrip: options.detachGrip,
|
|
@@ -1249,15 +1316,25 @@ var TabManager = class {
|
|
|
1249
1316
|
this.emit("panel:collapse", { panel: state, isCollapsed: newState });
|
|
1250
1317
|
});
|
|
1251
1318
|
}
|
|
1319
|
+
if (state.pinButton) {
|
|
1320
|
+
state.pinButton.addEventListener("click", () => {
|
|
1321
|
+
const isPinned = togglePin(state);
|
|
1322
|
+
this.autoHideManager.onPanelPinChanged(state);
|
|
1323
|
+
this.emit("panel:pin", { panel: state, isPinned });
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1252
1326
|
if (state.detachGrip) {
|
|
1253
1327
|
state.detachGrip.addEventListener("mousedown", (e) => {
|
|
1328
|
+
if (state.isPinned) return;
|
|
1254
1329
|
this.dragManager.startDrag(e, state, "single");
|
|
1255
1330
|
});
|
|
1256
1331
|
}
|
|
1257
1332
|
state.dragHandle.addEventListener("mousedown", (e) => {
|
|
1258
|
-
|
|
1333
|
+
const target = e.target;
|
|
1334
|
+
if (e.target === state.collapseButton || e.target === state.detachGrip || state.pinButton && state.pinButton.contains(target)) {
|
|
1259
1335
|
return;
|
|
1260
1336
|
}
|
|
1337
|
+
if (state.isPinned) return;
|
|
1261
1338
|
this.dragManager.startDrag(e, state, "group");
|
|
1262
1339
|
});
|
|
1263
1340
|
}
|
|
@@ -1517,6 +1594,7 @@ export {
|
|
|
1517
1594
|
getDefaultZIndex,
|
|
1518
1595
|
getDragZIndex,
|
|
1519
1596
|
getLeftmostPanel,
|
|
1597
|
+
getMovingGroupRespectingPins,
|
|
1520
1598
|
getPanelDimensions,
|
|
1521
1599
|
getPanelPosition,
|
|
1522
1600
|
getRightmostPanel,
|
|
@@ -1528,6 +1606,7 @@ export {
|
|
|
1528
1606
|
snapPanels,
|
|
1529
1607
|
snapPanelsToTarget,
|
|
1530
1608
|
toggleCollapse,
|
|
1609
|
+
togglePin,
|
|
1531
1610
|
unsnap,
|
|
1532
1611
|
updateSnappedPositions
|
|
1533
1612
|
};
|
package/dist/styles.css
CHANGED
|
@@ -83,6 +83,26 @@
|
|
|
83
83
|
background: var(--blork-tabs-accent, #4a90d9);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/* Pin Button */
|
|
87
|
+
.blork-tabs-pin-btn {
|
|
88
|
+
width: 24px;
|
|
89
|
+
height: 24px;
|
|
90
|
+
border: none;
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: var(--blork-tabs-header-color, #e0e0e0);
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
padding: 0;
|
|
99
|
+
transition: background 0.2s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.blork-tabs-pin-btn:hover {
|
|
103
|
+
background: rgba(255, 255, 255, 0.1);
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
/* Collapse Button */
|
|
87
107
|
.blork-tabs-collapse-btn {
|
|
88
108
|
width: 24px;
|
package/package.json
CHANGED
package/src/AutoHideManager.ts
CHANGED
|
@@ -50,9 +50,10 @@ export class AutoHideManager {
|
|
|
50
50
|
*/
|
|
51
51
|
private handleActivity(): void {
|
|
52
52
|
for (const panel of this.panels.values()) {
|
|
53
|
+
// Pinned panels are immune to activity-based show/hide
|
|
54
|
+
if (panel.isPinned) continue;
|
|
53
55
|
// Skip paused panels - they handle their own timing
|
|
54
56
|
if (this.pausedPanels.has(panel.id)) continue;
|
|
55
|
-
|
|
56
57
|
// Only process panels that participate in auto-hide
|
|
57
58
|
if (panel.resolvedAutoHideDelay !== undefined || panel.isHidden) {
|
|
58
59
|
this.show(panel, 'activity');
|
|
@@ -120,10 +121,29 @@ export class AutoHideManager {
|
|
|
120
121
|
*/
|
|
121
122
|
hide(panel: PanelState, trigger: 'timeout' | 'api'): void {
|
|
122
123
|
if (panel.isHidden) return;
|
|
124
|
+
if (panel.isPinned) return; // Pinned panels are always visible
|
|
123
125
|
hidePanel(panel, this.classes);
|
|
124
126
|
this.callbacks.onHide?.(panel, trigger);
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Called when a panel's pin state changes.
|
|
131
|
+
* Pinning cancels the hide timer and reveals the panel if hidden.
|
|
132
|
+
* Unpinning restarts the timer if the panel participates in auto-hide.
|
|
133
|
+
*/
|
|
134
|
+
onPanelPinChanged(panel: PanelState): void {
|
|
135
|
+
if (panel.isPinned) {
|
|
136
|
+
this.clearTimer(panel.id);
|
|
137
|
+
if (panel.isHidden) {
|
|
138
|
+
this.show(panel, 'api');
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
if (!panel.isHidden && panel.resolvedAutoHideDelay !== undefined) {
|
|
142
|
+
this.scheduleHide(panel);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
127
147
|
/**
|
|
128
148
|
* Initialize a newly added panel's auto-hide state
|
|
129
149
|
*/
|
package/src/DragManager.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
ResolvedTabManagerConfig,
|
|
14
14
|
} from './types';
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
getMovingGroupRespectingPins,
|
|
17
17
|
detachFromGroup,
|
|
18
18
|
findSnapTarget,
|
|
19
19
|
snapPanelsToTarget,
|
|
@@ -76,24 +76,24 @@ export class DragManager {
|
|
|
76
76
|
e.preventDefault();
|
|
77
77
|
e.stopPropagation();
|
|
78
78
|
|
|
79
|
-
const connectedPanels = getConnectedGroup(panel, this.panels);
|
|
80
|
-
|
|
81
|
-
// Store initial positions
|
|
82
|
-
const initialGroupPositions = new Map<string, Position>();
|
|
83
|
-
for (const p of connectedPanels) {
|
|
84
|
-
const rect = p.element.getBoundingClientRect();
|
|
85
|
-
initialGroupPositions.set(p.id, { x: rect.left, y: rect.top });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
79
|
let movingPanels: PanelState[];
|
|
89
80
|
|
|
90
81
|
if (mode === 'single') {
|
|
91
|
-
// Detach this panel from its group
|
|
82
|
+
// Detach this panel from its group and move it alone
|
|
92
83
|
detachFromGroup(panel, this.panels);
|
|
93
84
|
movingPanels = [panel];
|
|
94
85
|
} else {
|
|
95
|
-
//
|
|
96
|
-
|
|
86
|
+
// Group drag: collect panels on either side of the grabbed panel,
|
|
87
|
+
// splitting at any pinned panel and severing those bonds
|
|
88
|
+
movingPanels = getMovingGroupRespectingPins(panel, this.panels);
|
|
89
|
+
if (movingPanels.length === 0) return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Store initial positions for the panels that will actually move
|
|
93
|
+
const initialGroupPositions = new Map<string, Position>();
|
|
94
|
+
for (const p of movingPanels) {
|
|
95
|
+
const rect = p.element.getBoundingClientRect();
|
|
96
|
+
initialGroupPositions.set(p.id, { x: rect.left, y: rect.top });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const rect = panel.element.getBoundingClientRect();
|
package/src/Panel.ts
CHANGED
|
@@ -9,6 +9,9 @@ import type {
|
|
|
9
9
|
CSSClasses,
|
|
10
10
|
} from './types';
|
|
11
11
|
|
|
12
|
+
const PIN_ICON_UNPINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
|
|
13
|
+
const PIN_ICON_PINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;transform:rotate(90deg)"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Creates the default panel DOM structure
|
|
14
17
|
*/
|
|
@@ -18,6 +21,7 @@ export function createPanelElement(
|
|
|
18
21
|
): {
|
|
19
22
|
element: HTMLDivElement;
|
|
20
23
|
dragHandle: HTMLDivElement;
|
|
24
|
+
pinButton: HTMLButtonElement | null;
|
|
21
25
|
collapseButton: HTMLButtonElement | null;
|
|
22
26
|
contentWrapper: HTMLDivElement;
|
|
23
27
|
detachGrip: HTMLDivElement | null;
|
|
@@ -50,6 +54,16 @@ export function createPanelElement(
|
|
|
50
54
|
header.appendChild(title);
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
// Pin button (only if explicitly pinnable)
|
|
58
|
+
let pinButton: HTMLButtonElement | null = null;
|
|
59
|
+
if (config.pinnable === true) {
|
|
60
|
+
pinButton = document.createElement('button');
|
|
61
|
+
pinButton.className = classes.pinButton;
|
|
62
|
+
pinButton.id = `${config.id}-pin-btn`;
|
|
63
|
+
pinButton.innerHTML = config.startPinned === true ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
64
|
+
header.appendChild(pinButton);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
// Collapse button (if collapsible)
|
|
54
68
|
let collapseButton: HTMLButtonElement | null = null;
|
|
55
69
|
if (config.collapsible !== false) {
|
|
@@ -84,6 +98,7 @@ export function createPanelElement(
|
|
|
84
98
|
return {
|
|
85
99
|
element,
|
|
86
100
|
dragHandle: header,
|
|
101
|
+
pinButton,
|
|
87
102
|
collapseButton,
|
|
88
103
|
contentWrapper,
|
|
89
104
|
detachGrip,
|
|
@@ -100,6 +115,7 @@ export function createPanelState(
|
|
|
100
115
|
): PanelState {
|
|
101
116
|
let element: HTMLDivElement;
|
|
102
117
|
let dragHandle: HTMLDivElement;
|
|
118
|
+
let pinButton: HTMLButtonElement | null;
|
|
103
119
|
let collapseButton: HTMLButtonElement | null;
|
|
104
120
|
let contentWrapper: HTMLDivElement;
|
|
105
121
|
let detachGrip: HTMLDivElement | null;
|
|
@@ -108,6 +124,7 @@ export function createPanelState(
|
|
|
108
124
|
// Use existing DOM elements
|
|
109
125
|
element = config.element;
|
|
110
126
|
dragHandle = config.dragHandle ?? (element.querySelector(`.${classes.panelHeader}`) as HTMLDivElement);
|
|
127
|
+
pinButton = config.pinButton ?? (element.querySelector(`.${classes.pinButton}`) as HTMLButtonElement | null);
|
|
111
128
|
collapseButton = config.collapseButton ?? (element.querySelector(`.${classes.collapseButton}`) as HTMLButtonElement | null);
|
|
112
129
|
contentWrapper = config.contentWrapper ?? (element.querySelector(`.${classes.panelContent}`) as HTMLDivElement);
|
|
113
130
|
detachGrip = config.detachGrip ?? (element.querySelector(`.${classes.detachGrip}`) as HTMLDivElement | null);
|
|
@@ -116,6 +133,7 @@ export function createPanelState(
|
|
|
116
133
|
const created = createPanelElement(config, classes);
|
|
117
134
|
element = created.element;
|
|
118
135
|
dragHandle = created.dragHandle;
|
|
136
|
+
pinButton = created.pinButton;
|
|
119
137
|
collapseButton = created.collapseButton;
|
|
120
138
|
contentWrapper = created.contentWrapper;
|
|
121
139
|
detachGrip = created.detachGrip;
|
|
@@ -136,9 +154,11 @@ export function createPanelState(
|
|
|
136
154
|
id: config.id,
|
|
137
155
|
element,
|
|
138
156
|
dragHandle,
|
|
157
|
+
pinButton,
|
|
139
158
|
collapseButton,
|
|
140
159
|
contentWrapper,
|
|
141
160
|
detachGrip,
|
|
161
|
+
isPinned: config.startPinned === true,
|
|
142
162
|
isCollapsed: config.startCollapsed !== false,
|
|
143
163
|
snappedTo: null,
|
|
144
164
|
snappedFrom: null,
|
|
@@ -172,6 +192,23 @@ export function toggleCollapse(
|
|
|
172
192
|
return newState;
|
|
173
193
|
}
|
|
174
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Toggle panel pin state
|
|
197
|
+
*/
|
|
198
|
+
export function togglePin(
|
|
199
|
+
state: PanelState,
|
|
200
|
+
pinned?: boolean
|
|
201
|
+
): boolean {
|
|
202
|
+
const newState = pinned ?? !state.isPinned;
|
|
203
|
+
state.isPinned = newState;
|
|
204
|
+
|
|
205
|
+
if (state.pinButton) {
|
|
206
|
+
state.pinButton.innerHTML = newState ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return newState;
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
/**
|
|
176
213
|
* Show a hidden panel
|
|
177
214
|
*/
|
package/src/SnapChain.ts
CHANGED
|
@@ -273,6 +273,51 @@ export function snapPanels(
|
|
|
273
273
|
rightPanel.snappedFrom = leftPanel.id;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Get the subset of connected panels that should move when a panel is grabbed,
|
|
278
|
+
* stopping at any pinned panel in the chain.
|
|
279
|
+
*
|
|
280
|
+
* Pinned panels act as immoveable barriers — the chain splits at each one,
|
|
281
|
+
* and only the panels on the same side as the grabbed panel are returned.
|
|
282
|
+
* The snap bonds at each pin boundary are severed as a side effect.
|
|
283
|
+
*
|
|
284
|
+
* Example: chain [A, B, P(pinned), C, D], grab B → returns [A, B], unseats B↔P
|
|
285
|
+
* Example: chain [A, P(pinned), B, C], grab C → returns [B, C], unseats P↔B
|
|
286
|
+
*/
|
|
287
|
+
export function getMovingGroupRespectingPins(
|
|
288
|
+
grabbedPanel: PanelState,
|
|
289
|
+
panels: Map<string, PanelState>
|
|
290
|
+
): PanelState[] {
|
|
291
|
+
if (grabbedPanel.isPinned) return [];
|
|
292
|
+
|
|
293
|
+
const fullGroup = getConnectedGroup(grabbedPanel, panels);
|
|
294
|
+
const grabbedIndex = fullGroup.indexOf(grabbedPanel);
|
|
295
|
+
|
|
296
|
+
// Walk left from the grabbed panel, stopping before any pinned panel
|
|
297
|
+
const leftPanels: PanelState[] = [];
|
|
298
|
+
for (let i = grabbedIndex; i >= 0; i--) {
|
|
299
|
+
if (fullGroup[i].isPinned) {
|
|
300
|
+
// Sever the bond between this pinned panel and the next moveable panel
|
|
301
|
+
unsnap(fullGroup[i], fullGroup[i + 1]);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
leftPanels.unshift(fullGroup[i]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Walk right from the grabbed panel, stopping before any pinned panel
|
|
308
|
+
const rightPanels: PanelState[] = [];
|
|
309
|
+
for (let i = grabbedIndex + 1; i < fullGroup.length; i++) {
|
|
310
|
+
if (fullGroup[i].isPinned) {
|
|
311
|
+
// Sever the bond between the last moveable panel and this pinned panel
|
|
312
|
+
unsnap(fullGroup[i - 1], fullGroup[i]);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
rightPanels.push(fullGroup[i]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return [...leftPanels, ...rightPanels];
|
|
319
|
+
}
|
|
320
|
+
|
|
276
321
|
/**
|
|
277
322
|
* Break the snap relationship between two specific panels
|
|
278
323
|
*/
|
package/src/TabManager.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
DebugLog,
|
|
25
25
|
} from './types';
|
|
26
26
|
import { createDebugPanelContent, createDebugPanelInterface, createDebugLog, setupHoverEnlarge, DebugPanelElements } from './DebugPanel';
|
|
27
|
-
import { createPanelState, toggleCollapse, setPanelPosition } from './Panel';
|
|
27
|
+
import { createPanelState, toggleCollapse, togglePin, setPanelPosition } from './Panel';
|
|
28
28
|
import { getConnectedGroup, detachFromGroup, updateSnappedPositions, snapPanels } from './SnapChain';
|
|
29
29
|
import { DragManager } from './DragManager';
|
|
30
30
|
import { AnchorManager } from './AnchorManager';
|
|
@@ -58,6 +58,7 @@ function generateClasses(prefix: string): CSSClasses {
|
|
|
58
58
|
panelContent: `${prefix}-content`,
|
|
59
59
|
panelContentCollapsed: `${prefix}-content-collapsed`,
|
|
60
60
|
detachGrip: `${prefix}-detach-grip`,
|
|
61
|
+
pinButton: `${prefix}-pin-btn`,
|
|
61
62
|
collapseButton: `${prefix}-collapse-btn`,
|
|
62
63
|
snapPreview: `${prefix}-snap-preview`,
|
|
63
64
|
snapPreviewVisible: `${prefix}-snap-preview-visible`,
|
|
@@ -181,6 +182,7 @@ export class TabManager {
|
|
|
181
182
|
element: HTMLDivElement,
|
|
182
183
|
options: {
|
|
183
184
|
dragHandle?: HTMLDivElement;
|
|
185
|
+
pinButton?: HTMLButtonElement;
|
|
184
186
|
collapseButton?: HTMLButtonElement;
|
|
185
187
|
contentWrapper?: HTMLDivElement;
|
|
186
188
|
detachGrip?: HTMLDivElement;
|
|
@@ -191,6 +193,7 @@ export class TabManager {
|
|
|
191
193
|
id,
|
|
192
194
|
element,
|
|
193
195
|
dragHandle: options.dragHandle,
|
|
196
|
+
pinButton: options.pinButton,
|
|
194
197
|
collapseButton: options.collapseButton,
|
|
195
198
|
contentWrapper: options.contentWrapper,
|
|
196
199
|
detachGrip: options.detachGrip,
|
|
@@ -309,22 +312,35 @@ export class TabManager {
|
|
|
309
312
|
});
|
|
310
313
|
}
|
|
311
314
|
|
|
315
|
+
// Pin button
|
|
316
|
+
if (state.pinButton) {
|
|
317
|
+
state.pinButton.addEventListener('click', () => {
|
|
318
|
+
const isPinned = togglePin(state);
|
|
319
|
+
this.autoHideManager.onPanelPinChanged(state);
|
|
320
|
+
this.emit('panel:pin', { panel: state, isPinned });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
312
324
|
// Detach grip - single panel drag
|
|
313
325
|
if (state.detachGrip) {
|
|
314
326
|
state.detachGrip.addEventListener('mousedown', (e) => {
|
|
327
|
+
if (state.isPinned) return;
|
|
315
328
|
this.dragManager.startDrag(e, state, 'single');
|
|
316
329
|
});
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
// Main drag handle - group drag
|
|
320
333
|
state.dragHandle.addEventListener('mousedown', (e) => {
|
|
321
|
-
// Ignore if clicking on collapse button or detach grip
|
|
334
|
+
// Ignore if clicking on collapse button, pin button, or detach grip
|
|
335
|
+
const target = e.target as Node;
|
|
322
336
|
if (
|
|
323
337
|
e.target === state.collapseButton ||
|
|
324
|
-
e.target === state.detachGrip
|
|
338
|
+
e.target === state.detachGrip ||
|
|
339
|
+
(state.pinButton && state.pinButton.contains(target))
|
|
325
340
|
) {
|
|
326
341
|
return;
|
|
327
342
|
}
|
|
343
|
+
if (state.isPinned) return;
|
|
328
344
|
this.dragManager.startDrag(e, state, 'group');
|
|
329
345
|
});
|
|
330
346
|
}
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ export {
|
|
|
48
48
|
createPanelElement,
|
|
49
49
|
createPanelState,
|
|
50
50
|
toggleCollapse,
|
|
51
|
+
togglePin,
|
|
51
52
|
showPanel,
|
|
52
53
|
hidePanel,
|
|
53
54
|
setPanelPosition,
|
|
@@ -59,6 +60,7 @@ export {
|
|
|
59
60
|
} from './Panel';
|
|
60
61
|
export {
|
|
61
62
|
getConnectedGroup,
|
|
63
|
+
getMovingGroupRespectingPins,
|
|
62
64
|
detachFromGroup,
|
|
63
65
|
findSnapTarget,
|
|
64
66
|
snapPanelsToTarget,
|
|
@@ -103,6 +105,7 @@ export type {
|
|
|
103
105
|
PanelSnapEvent,
|
|
104
106
|
AnchorSnapEvent,
|
|
105
107
|
PanelDetachedEvent,
|
|
108
|
+
PanelPinEvent,
|
|
106
109
|
PanelCollapseEvent,
|
|
107
110
|
PanelShowEvent,
|
|
108
111
|
PanelHideEvent,
|
package/src/types.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface PanelConfig {
|
|
|
57
57
|
title?: string;
|
|
58
58
|
/** Initial width (default: 300) */
|
|
59
59
|
width?: number;
|
|
60
|
+
/** Initial pin state (default: false) */
|
|
61
|
+
startPinned?: boolean;
|
|
60
62
|
/** Initial collapsed state (default: true) */
|
|
61
63
|
startCollapsed?: boolean;
|
|
62
64
|
/** Initial position (optional - will be auto-positioned if not provided) */
|
|
@@ -67,12 +69,16 @@ export interface PanelConfig {
|
|
|
67
69
|
element?: HTMLDivElement;
|
|
68
70
|
/** Custom drag handle element */
|
|
69
71
|
dragHandle?: HTMLDivElement;
|
|
72
|
+
/** Custom pin button element */
|
|
73
|
+
pinButton?: HTMLButtonElement;
|
|
70
74
|
/** Custom collapse button element */
|
|
71
75
|
collapseButton?: HTMLButtonElement;
|
|
72
76
|
/** Custom content wrapper element */
|
|
73
77
|
contentWrapper?: HTMLDivElement;
|
|
74
78
|
/** Custom detach grip element */
|
|
75
79
|
detachGrip?: HTMLDivElement;
|
|
80
|
+
/** Whether panel can be pinned (default: false) */
|
|
81
|
+
pinnable?: boolean;
|
|
76
82
|
/** Whether panel can be collapsed (default: true) */
|
|
77
83
|
collapsible?: boolean;
|
|
78
84
|
/** Whether panel can be detached from group (default: true) */
|
|
@@ -126,12 +132,16 @@ export interface PanelState {
|
|
|
126
132
|
element: HTMLDivElement;
|
|
127
133
|
/** Drag handle element */
|
|
128
134
|
dragHandle: HTMLDivElement;
|
|
135
|
+
/** Pin button element (if pinable) */
|
|
136
|
+
pinButton: HTMLButtonElement | null;
|
|
129
137
|
/** Collapse button element (if collapsible) */
|
|
130
138
|
collapseButton: HTMLButtonElement | null;
|
|
131
139
|
/** Content wrapper element */
|
|
132
140
|
contentWrapper: HTMLDivElement;
|
|
133
141
|
/** Detach grip element (if detachable) */
|
|
134
142
|
detachGrip: HTMLDivElement | null;
|
|
143
|
+
/** Whether panel is currently pinned */
|
|
144
|
+
isPinned: boolean;
|
|
135
145
|
/** Whether panel is currently collapsed */
|
|
136
146
|
isCollapsed: boolean;
|
|
137
147
|
/** ID of panel this is snapped to on its right (outgoing link) */
|
|
@@ -254,6 +264,8 @@ export interface TabManagerEvents {
|
|
|
254
264
|
'snap:anchor': AnchorSnapEvent;
|
|
255
265
|
/** Fired when a panel is detached from group */
|
|
256
266
|
'panel:detached': PanelDetachedEvent;
|
|
267
|
+
/** Fired when panel pin state changes */
|
|
268
|
+
'panel:pin': PanelPinEvent;
|
|
257
269
|
/** Fired when panel collapse state changes */
|
|
258
270
|
'panel:collapse': PanelCollapseEvent;
|
|
259
271
|
/** Fired when a panel becomes visible (auto-hide) */
|
|
@@ -306,6 +318,11 @@ export interface PanelDetachedEvent {
|
|
|
306
318
|
previousGroup: PanelState[];
|
|
307
319
|
}
|
|
308
320
|
|
|
321
|
+
export interface PanelPinEvent {
|
|
322
|
+
panel: PanelState;
|
|
323
|
+
isPinned: boolean;
|
|
324
|
+
}
|
|
325
|
+
|
|
309
326
|
export interface PanelCollapseEvent {
|
|
310
327
|
panel: PanelState;
|
|
311
328
|
isCollapsed: boolean;
|
|
@@ -338,6 +355,7 @@ export interface CSSClasses {
|
|
|
338
355
|
panelContent: string;
|
|
339
356
|
panelContentCollapsed: string;
|
|
340
357
|
detachGrip: string;
|
|
358
|
+
pinButton: string;
|
|
341
359
|
collapseButton: string;
|
|
342
360
|
snapPreview: string;
|
|
343
361
|
snapPreviewVisible: string;
|