@blorkfield/blork-tabs 0.3.1 → 0.5.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 +165 -8
- package/dist/index.d.cts +103 -1
- package/dist/index.d.ts +103 -1
- package/dist/index.js +162 -8
- package/dist/styles.css +102 -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/TagButton.ts +145 -0
- package/src/index.ts +5 -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
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
createPanelElement: () => createPanelElement,
|
|
33
33
|
createPanelState: () => createPanelState,
|
|
34
34
|
createPresetAnchor: () => createPresetAnchor,
|
|
35
|
+
createTagButton: () => createTagButton,
|
|
35
36
|
detachFromGroup: () => detachFromGroup,
|
|
36
37
|
findSnapTarget: () => findSnapTarget,
|
|
37
38
|
getConnectedGroup: () => getConnectedGroup,
|
|
@@ -39,6 +40,7 @@ __export(index_exports, {
|
|
|
39
40
|
getDefaultZIndex: () => getDefaultZIndex,
|
|
40
41
|
getDragZIndex: () => getDragZIndex,
|
|
41
42
|
getLeftmostPanel: () => getLeftmostPanel,
|
|
43
|
+
getMovingGroupRespectingPins: () => getMovingGroupRespectingPins,
|
|
42
44
|
getPanelDimensions: () => getPanelDimensions,
|
|
43
45
|
getPanelPosition: () => getPanelPosition,
|
|
44
46
|
getRightmostPanel: () => getRightmostPanel,
|
|
@@ -50,6 +52,7 @@ __export(index_exports, {
|
|
|
50
52
|
snapPanels: () => snapPanels,
|
|
51
53
|
snapPanelsToTarget: () => snapPanelsToTarget,
|
|
52
54
|
toggleCollapse: () => toggleCollapse,
|
|
55
|
+
togglePin: () => togglePin,
|
|
53
56
|
unsnap: () => unsnap,
|
|
54
57
|
updateSnappedPositions: () => updateSnappedPositions
|
|
55
58
|
});
|
|
@@ -199,6 +202,8 @@ function setupHoverEnlarge(config) {
|
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
// src/Panel.ts
|
|
205
|
+
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>`;
|
|
206
|
+
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
207
|
function createPanelElement(config, classes) {
|
|
203
208
|
const element = document.createElement("div");
|
|
204
209
|
element.className = classes.panel;
|
|
@@ -221,6 +226,14 @@ function createPanelElement(config, classes) {
|
|
|
221
226
|
title.textContent = config.title;
|
|
222
227
|
header.appendChild(title);
|
|
223
228
|
}
|
|
229
|
+
let pinButton = null;
|
|
230
|
+
if (config.pinnable === true) {
|
|
231
|
+
pinButton = document.createElement("button");
|
|
232
|
+
pinButton.className = classes.pinButton;
|
|
233
|
+
pinButton.id = `${config.id}-pin-btn`;
|
|
234
|
+
pinButton.innerHTML = config.startPinned === true ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
235
|
+
header.appendChild(pinButton);
|
|
236
|
+
}
|
|
224
237
|
let collapseButton = null;
|
|
225
238
|
if (config.collapsible !== false) {
|
|
226
239
|
collapseButton = document.createElement("button");
|
|
@@ -247,6 +260,7 @@ function createPanelElement(config, classes) {
|
|
|
247
260
|
return {
|
|
248
261
|
element,
|
|
249
262
|
dragHandle: header,
|
|
263
|
+
pinButton,
|
|
250
264
|
collapseButton,
|
|
251
265
|
contentWrapper,
|
|
252
266
|
detachGrip
|
|
@@ -255,12 +269,14 @@ function createPanelElement(config, classes) {
|
|
|
255
269
|
function createPanelState(config, classes, globalConfig) {
|
|
256
270
|
let element;
|
|
257
271
|
let dragHandle;
|
|
272
|
+
let pinButton;
|
|
258
273
|
let collapseButton;
|
|
259
274
|
let contentWrapper;
|
|
260
275
|
let detachGrip;
|
|
261
276
|
if (config.element) {
|
|
262
277
|
element = config.element;
|
|
263
278
|
dragHandle = config.dragHandle ?? element.querySelector(`.${classes.panelHeader}`);
|
|
279
|
+
pinButton = config.pinButton ?? element.querySelector(`.${classes.pinButton}`);
|
|
264
280
|
collapseButton = config.collapseButton ?? element.querySelector(`.${classes.collapseButton}`);
|
|
265
281
|
contentWrapper = config.contentWrapper ?? element.querySelector(`.${classes.panelContent}`);
|
|
266
282
|
detachGrip = config.detachGrip ?? element.querySelector(`.${classes.detachGrip}`);
|
|
@@ -268,6 +284,7 @@ function createPanelState(config, classes, globalConfig) {
|
|
|
268
284
|
const created = createPanelElement(config, classes);
|
|
269
285
|
element = created.element;
|
|
270
286
|
dragHandle = created.dragHandle;
|
|
287
|
+
pinButton = created.pinButton;
|
|
271
288
|
collapseButton = created.collapseButton;
|
|
272
289
|
contentWrapper = created.contentWrapper;
|
|
273
290
|
detachGrip = created.detachGrip;
|
|
@@ -281,9 +298,11 @@ function createPanelState(config, classes, globalConfig) {
|
|
|
281
298
|
id: config.id,
|
|
282
299
|
element,
|
|
283
300
|
dragHandle,
|
|
301
|
+
pinButton,
|
|
284
302
|
collapseButton,
|
|
285
303
|
contentWrapper,
|
|
286
304
|
detachGrip,
|
|
305
|
+
isPinned: config.startPinned === true,
|
|
287
306
|
isCollapsed: config.startCollapsed !== false,
|
|
288
307
|
snappedTo: null,
|
|
289
308
|
snappedFrom: null,
|
|
@@ -305,6 +324,14 @@ function toggleCollapse(state, classes, collapsed) {
|
|
|
305
324
|
}
|
|
306
325
|
return newState;
|
|
307
326
|
}
|
|
327
|
+
function togglePin(state, pinned) {
|
|
328
|
+
const newState = pinned ?? !state.isPinned;
|
|
329
|
+
state.isPinned = newState;
|
|
330
|
+
if (state.pinButton) {
|
|
331
|
+
state.pinButton.innerHTML = newState ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
|
|
332
|
+
}
|
|
333
|
+
return newState;
|
|
334
|
+
}
|
|
308
335
|
function showPanel(state, classes) {
|
|
309
336
|
if (!state.isHidden) return;
|
|
310
337
|
state.isHidden = false;
|
|
@@ -482,6 +509,28 @@ function snapPanels(leftPanel, rightPanel) {
|
|
|
482
509
|
leftPanel.snappedTo = rightPanel.id;
|
|
483
510
|
rightPanel.snappedFrom = leftPanel.id;
|
|
484
511
|
}
|
|
512
|
+
function getMovingGroupRespectingPins(grabbedPanel, panels) {
|
|
513
|
+
if (grabbedPanel.isPinned) return [];
|
|
514
|
+
const fullGroup = getConnectedGroup(grabbedPanel, panels);
|
|
515
|
+
const grabbedIndex = fullGroup.indexOf(grabbedPanel);
|
|
516
|
+
const leftPanels = [];
|
|
517
|
+
for (let i = grabbedIndex; i >= 0; i--) {
|
|
518
|
+
if (fullGroup[i].isPinned) {
|
|
519
|
+
unsnap(fullGroup[i], fullGroup[i + 1]);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
leftPanels.unshift(fullGroup[i]);
|
|
523
|
+
}
|
|
524
|
+
const rightPanels = [];
|
|
525
|
+
for (let i = grabbedIndex + 1; i < fullGroup.length; i++) {
|
|
526
|
+
if (fullGroup[i].isPinned) {
|
|
527
|
+
unsnap(fullGroup[i - 1], fullGroup[i]);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
rightPanels.push(fullGroup[i]);
|
|
531
|
+
}
|
|
532
|
+
return [...leftPanels, ...rightPanels];
|
|
533
|
+
}
|
|
485
534
|
function unsnap(leftPanel, rightPanel) {
|
|
486
535
|
if (leftPanel.snappedTo === rightPanel.id) {
|
|
487
536
|
leftPanel.snappedTo = null;
|
|
@@ -509,18 +558,18 @@ var DragManager = class {
|
|
|
509
558
|
startDrag(e, panel, mode) {
|
|
510
559
|
e.preventDefault();
|
|
511
560
|
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
561
|
let movingPanels;
|
|
519
562
|
if (mode === "single") {
|
|
520
563
|
detachFromGroup(panel, this.panels);
|
|
521
564
|
movingPanels = [panel];
|
|
522
565
|
} else {
|
|
523
|
-
movingPanels =
|
|
566
|
+
movingPanels = getMovingGroupRespectingPins(panel, this.panels);
|
|
567
|
+
if (movingPanels.length === 0) return;
|
|
568
|
+
}
|
|
569
|
+
const initialGroupPositions = /* @__PURE__ */ new Map();
|
|
570
|
+
for (const p of movingPanels) {
|
|
571
|
+
const rect2 = p.element.getBoundingClientRect();
|
|
572
|
+
initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
|
|
524
573
|
}
|
|
525
574
|
const rect = panel.element.getBoundingClientRect();
|
|
526
575
|
this.activeDrag = {
|
|
@@ -1010,6 +1059,7 @@ var AutoHideManager = class {
|
|
|
1010
1059
|
*/
|
|
1011
1060
|
handleActivity() {
|
|
1012
1061
|
for (const panel of this.panels.values()) {
|
|
1062
|
+
if (panel.isPinned) continue;
|
|
1013
1063
|
if (this.pausedPanels.has(panel.id)) continue;
|
|
1014
1064
|
if (panel.resolvedAutoHideDelay !== void 0 || panel.isHidden) {
|
|
1015
1065
|
this.show(panel, "activity");
|
|
@@ -1070,9 +1120,27 @@ var AutoHideManager = class {
|
|
|
1070
1120
|
*/
|
|
1071
1121
|
hide(panel, trigger) {
|
|
1072
1122
|
if (panel.isHidden) return;
|
|
1123
|
+
if (panel.isPinned) return;
|
|
1073
1124
|
hidePanel(panel, this.classes);
|
|
1074
1125
|
this.callbacks.onHide?.(panel, trigger);
|
|
1075
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Called when a panel's pin state changes.
|
|
1129
|
+
* Pinning cancels the hide timer and reveals the panel if hidden.
|
|
1130
|
+
* Unpinning restarts the timer if the panel participates in auto-hide.
|
|
1131
|
+
*/
|
|
1132
|
+
onPanelPinChanged(panel) {
|
|
1133
|
+
if (panel.isPinned) {
|
|
1134
|
+
this.clearTimer(panel.id);
|
|
1135
|
+
if (panel.isHidden) {
|
|
1136
|
+
this.show(panel, "api");
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
if (!panel.isHidden && panel.resolvedAutoHideDelay !== void 0) {
|
|
1140
|
+
this.scheduleHide(panel);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1076
1144
|
/**
|
|
1077
1145
|
* Initialize a newly added panel's auto-hide state
|
|
1078
1146
|
*/
|
|
@@ -1127,6 +1195,7 @@ function generateClasses(prefix) {
|
|
|
1127
1195
|
panelContent: `${prefix}-content`,
|
|
1128
1196
|
panelContentCollapsed: `${prefix}-content-collapsed`,
|
|
1129
1197
|
detachGrip: `${prefix}-detach-grip`,
|
|
1198
|
+
pinButton: `${prefix}-pin-btn`,
|
|
1130
1199
|
collapseButton: `${prefix}-collapse-btn`,
|
|
1131
1200
|
snapPreview: `${prefix}-snap-preview`,
|
|
1132
1201
|
snapPreviewVisible: `${prefix}-snap-preview-visible`,
|
|
@@ -1217,6 +1286,7 @@ var TabManager = class {
|
|
|
1217
1286
|
id,
|
|
1218
1287
|
element,
|
|
1219
1288
|
dragHandle: options.dragHandle,
|
|
1289
|
+
pinButton: options.pinButton,
|
|
1220
1290
|
collapseButton: options.collapseButton,
|
|
1221
1291
|
contentWrapper: options.contentWrapper,
|
|
1222
1292
|
detachGrip: options.detachGrip,
|
|
@@ -1306,15 +1376,25 @@ var TabManager = class {
|
|
|
1306
1376
|
this.emit("panel:collapse", { panel: state, isCollapsed: newState });
|
|
1307
1377
|
});
|
|
1308
1378
|
}
|
|
1379
|
+
if (state.pinButton) {
|
|
1380
|
+
state.pinButton.addEventListener("click", () => {
|
|
1381
|
+
const isPinned = togglePin(state);
|
|
1382
|
+
this.autoHideManager.onPanelPinChanged(state);
|
|
1383
|
+
this.emit("panel:pin", { panel: state, isPinned });
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1309
1386
|
if (state.detachGrip) {
|
|
1310
1387
|
state.detachGrip.addEventListener("mousedown", (e) => {
|
|
1388
|
+
if (state.isPinned) return;
|
|
1311
1389
|
this.dragManager.startDrag(e, state, "single");
|
|
1312
1390
|
});
|
|
1313
1391
|
}
|
|
1314
1392
|
state.dragHandle.addEventListener("mousedown", (e) => {
|
|
1315
|
-
|
|
1393
|
+
const target = e.target;
|
|
1394
|
+
if (e.target === state.collapseButton || e.target === state.detachGrip || state.pinButton && state.pinButton.contains(target)) {
|
|
1316
1395
|
return;
|
|
1317
1396
|
}
|
|
1397
|
+
if (state.isPinned) return;
|
|
1318
1398
|
this.dragManager.startDrag(e, state, "group");
|
|
1319
1399
|
});
|
|
1320
1400
|
}
|
|
@@ -1554,6 +1634,80 @@ var TabManager = class {
|
|
|
1554
1634
|
this.debugPanelElements.clear();
|
|
1555
1635
|
}
|
|
1556
1636
|
};
|
|
1637
|
+
|
|
1638
|
+
// src/TagButton.ts
|
|
1639
|
+
function createTagButton(label, config = {}) {
|
|
1640
|
+
const { defaultActive = false, inputs = [], onChange } = config;
|
|
1641
|
+
const hasInputs = inputs.length > 0;
|
|
1642
|
+
const el = document.createElement(hasInputs ? "div" : "button");
|
|
1643
|
+
el.className = "blork-tabs-tag-btn";
|
|
1644
|
+
if (hasInputs) {
|
|
1645
|
+
el.setAttribute("role", "button");
|
|
1646
|
+
el.setAttribute("tabindex", "0");
|
|
1647
|
+
}
|
|
1648
|
+
el.appendChild(document.createTextNode(label));
|
|
1649
|
+
const inputElements = [];
|
|
1650
|
+
if (hasInputs) {
|
|
1651
|
+
const inputsContainer = document.createElement("span");
|
|
1652
|
+
inputsContainer.className = "blork-tabs-tag-inputs";
|
|
1653
|
+
for (const inputConfig of inputs) {
|
|
1654
|
+
if (inputConfig.label) {
|
|
1655
|
+
const labelSpan = document.createElement("span");
|
|
1656
|
+
labelSpan.textContent = inputConfig.label;
|
|
1657
|
+
inputsContainer.appendChild(labelSpan);
|
|
1658
|
+
}
|
|
1659
|
+
if (inputConfig.type === "select") {
|
|
1660
|
+
const select = document.createElement("select");
|
|
1661
|
+
select.className = "blork-tabs-tag-select";
|
|
1662
|
+
for (const opt of inputConfig.options ?? []) {
|
|
1663
|
+
const option = document.createElement("option");
|
|
1664
|
+
option.value = opt.value;
|
|
1665
|
+
option.textContent = opt.label;
|
|
1666
|
+
select.appendChild(option);
|
|
1667
|
+
}
|
|
1668
|
+
select.addEventListener("click", (e) => e.stopPropagation());
|
|
1669
|
+
select.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
1670
|
+
inputsContainer.appendChild(select);
|
|
1671
|
+
inputElements.push(select);
|
|
1672
|
+
} else {
|
|
1673
|
+
const input = document.createElement("input");
|
|
1674
|
+
input.type = "number";
|
|
1675
|
+
input.className = "blork-tabs-tag-input";
|
|
1676
|
+
if (inputConfig.defaultValue !== void 0) input.value = String(inputConfig.defaultValue);
|
|
1677
|
+
if (inputConfig.step !== void 0) input.step = String(inputConfig.step);
|
|
1678
|
+
if (inputConfig.min !== void 0) input.min = String(inputConfig.min);
|
|
1679
|
+
if (inputConfig.max !== void 0) input.max = String(inputConfig.max);
|
|
1680
|
+
input.addEventListener("click", (e) => e.stopPropagation());
|
|
1681
|
+
inputsContainer.appendChild(input);
|
|
1682
|
+
inputElements.push(input);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
el.appendChild(inputsContainer);
|
|
1686
|
+
}
|
|
1687
|
+
let active = defaultActive;
|
|
1688
|
+
if (active) el.classList.add("active");
|
|
1689
|
+
const setActive = (value) => {
|
|
1690
|
+
active = value;
|
|
1691
|
+
el.classList.toggle("active", active);
|
|
1692
|
+
onChange?.(active);
|
|
1693
|
+
};
|
|
1694
|
+
el.addEventListener("click", () => setActive(!active));
|
|
1695
|
+
el.addEventListener("keydown", (e) => {
|
|
1696
|
+
const ke = e;
|
|
1697
|
+
if (ke.key === "Enter" || ke.key === " ") {
|
|
1698
|
+
ke.preventDefault();
|
|
1699
|
+
setActive(!active);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
return {
|
|
1703
|
+
element: el,
|
|
1704
|
+
isActive: () => active,
|
|
1705
|
+
setActive,
|
|
1706
|
+
toggle: () => setActive(!active),
|
|
1707
|
+
getValue: (index) => inputElements[index]?.value ?? "",
|
|
1708
|
+
getInput: (index) => inputElements[index]
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1557
1711
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1558
1712
|
0 && (module.exports = {
|
|
1559
1713
|
AnchorManager,
|
|
@@ -1568,6 +1722,7 @@ var TabManager = class {
|
|
|
1568
1722
|
createPanelElement,
|
|
1569
1723
|
createPanelState,
|
|
1570
1724
|
createPresetAnchor,
|
|
1725
|
+
createTagButton,
|
|
1571
1726
|
detachFromGroup,
|
|
1572
1727
|
findSnapTarget,
|
|
1573
1728
|
getConnectedGroup,
|
|
@@ -1575,6 +1730,7 @@ var TabManager = class {
|
|
|
1575
1730
|
getDefaultZIndex,
|
|
1576
1731
|
getDragZIndex,
|
|
1577
1732
|
getLeftmostPanel,
|
|
1733
|
+
getMovingGroupRespectingPins,
|
|
1578
1734
|
getPanelDimensions,
|
|
1579
1735
|
getPanelPosition,
|
|
1580
1736
|
getRightmostPanel,
|
|
@@ -1586,6 +1742,7 @@ var TabManager = class {
|
|
|
1586
1742
|
snapPanels,
|
|
1587
1743
|
snapPanelsToTarget,
|
|
1588
1744
|
toggleCollapse,
|
|
1745
|
+
togglePin,
|
|
1589
1746
|
unsnap,
|
|
1590
1747
|
updateSnappedPositions
|
|
1591
1748
|
});
|
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
|
*/
|
|
@@ -799,6 +823,67 @@ interface HoverEnlargeConfig {
|
|
|
799
823
|
*/
|
|
800
824
|
declare function setupHoverEnlarge(config: HoverEnlargeConfig): void;
|
|
801
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Tag button: a toggleable pill-shaped button that optionally reveals
|
|
828
|
+
* inline inputs (numbers, selects) when active.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```typescript
|
|
832
|
+
* // Simple toggle
|
|
833
|
+
* const staticBtn = createTagButton('static');
|
|
834
|
+
* container.appendChild(staticBtn.element);
|
|
835
|
+
*
|
|
836
|
+
* // Toggle with parameterized inputs
|
|
837
|
+
* const govBtn = createTagButton('gravity_override', {
|
|
838
|
+
* inputs: [
|
|
839
|
+
* { label: 'x', defaultValue: 0, step: 0.1 },
|
|
840
|
+
* { label: 'y', defaultValue: -1, step: 0.1 },
|
|
841
|
+
* ],
|
|
842
|
+
* });
|
|
843
|
+
* container.appendChild(govBtn.element);
|
|
844
|
+
*
|
|
845
|
+
* // Read state when needed
|
|
846
|
+
* if (govBtn.isActive()) {
|
|
847
|
+
* const gx = parseFloat(govBtn.getValue(0));
|
|
848
|
+
* const gy = parseFloat(govBtn.getValue(1));
|
|
849
|
+
* }
|
|
850
|
+
* ```
|
|
851
|
+
*/
|
|
852
|
+
interface TagButtonNumberInputConfig {
|
|
853
|
+
type?: 'number';
|
|
854
|
+
/** Short label rendered before the input (e.g. 'x', 'y') */
|
|
855
|
+
label?: string;
|
|
856
|
+
defaultValue?: number;
|
|
857
|
+
step?: number;
|
|
858
|
+
min?: number;
|
|
859
|
+
max?: number;
|
|
860
|
+
}
|
|
861
|
+
interface TagButtonSelectInputConfig {
|
|
862
|
+
type: 'select';
|
|
863
|
+
label?: string;
|
|
864
|
+
options?: Array<{
|
|
865
|
+
value: string;
|
|
866
|
+
label: string;
|
|
867
|
+
}>;
|
|
868
|
+
}
|
|
869
|
+
type TagButtonInputConfig = TagButtonNumberInputConfig | TagButtonSelectInputConfig;
|
|
870
|
+
interface TagButtonConfig {
|
|
871
|
+
defaultActive?: boolean;
|
|
872
|
+
inputs?: TagButtonInputConfig[];
|
|
873
|
+
onChange?: (active: boolean) => void;
|
|
874
|
+
}
|
|
875
|
+
interface TagButton {
|
|
876
|
+
element: HTMLElement;
|
|
877
|
+
isActive(): boolean;
|
|
878
|
+
setActive(active: boolean): void;
|
|
879
|
+
toggle(): void;
|
|
880
|
+
/** Get the string value of the input at position index */
|
|
881
|
+
getValue(index: number): string;
|
|
882
|
+
/** Direct access to the underlying input or select element */
|
|
883
|
+
getInput(index: number): HTMLInputElement | HTMLSelectElement | undefined;
|
|
884
|
+
}
|
|
885
|
+
declare function createTagButton(label: string, config?: TagButtonConfig): TagButton;
|
|
886
|
+
|
|
802
887
|
/**
|
|
803
888
|
* @blorkfield/blork-tabs - Panel
|
|
804
889
|
* Individual panel component with collapse/expand functionality
|
|
@@ -810,6 +895,7 @@ declare function setupHoverEnlarge(config: HoverEnlargeConfig): void;
|
|
|
810
895
|
declare function createPanelElement(config: PanelConfig, classes: CSSClasses): {
|
|
811
896
|
element: HTMLDivElement;
|
|
812
897
|
dragHandle: HTMLDivElement;
|
|
898
|
+
pinButton: HTMLButtonElement | null;
|
|
813
899
|
collapseButton: HTMLButtonElement | null;
|
|
814
900
|
contentWrapper: HTMLDivElement;
|
|
815
901
|
detachGrip: HTMLDivElement | null;
|
|
@@ -825,6 +911,10 @@ declare function createPanelState(config: PanelConfig, classes: CSSClasses, glob
|
|
|
825
911
|
* Toggle panel collapse state
|
|
826
912
|
*/
|
|
827
913
|
declare function toggleCollapse(state: PanelState, classes: CSSClasses, collapsed?: boolean): boolean;
|
|
914
|
+
/**
|
|
915
|
+
* Toggle panel pin state
|
|
916
|
+
*/
|
|
917
|
+
declare function togglePin(state: PanelState, pinned?: boolean): boolean;
|
|
828
918
|
/**
|
|
829
919
|
* Show a hidden panel
|
|
830
920
|
*/
|
|
@@ -909,9 +999,21 @@ declare function areInSameChain(panel1: PanelState, panel2: PanelState, panels:
|
|
|
909
999
|
* Establish a snap relationship between two panels
|
|
910
1000
|
*/
|
|
911
1001
|
declare function snapPanels(leftPanel: PanelState, rightPanel: PanelState): void;
|
|
1002
|
+
/**
|
|
1003
|
+
* Get the subset of connected panels that should move when a panel is grabbed,
|
|
1004
|
+
* stopping at any pinned panel in the chain.
|
|
1005
|
+
*
|
|
1006
|
+
* Pinned panels act as immoveable barriers — the chain splits at each one,
|
|
1007
|
+
* and only the panels on the same side as the grabbed panel are returned.
|
|
1008
|
+
* The snap bonds at each pin boundary are severed as a side effect.
|
|
1009
|
+
*
|
|
1010
|
+
* Example: chain [A, B, P(pinned), C, D], grab B → returns [A, B], unseats B↔P
|
|
1011
|
+
* Example: chain [A, P(pinned), B, C], grab C → returns [B, C], unseats P↔B
|
|
1012
|
+
*/
|
|
1013
|
+
declare function getMovingGroupRespectingPins(grabbedPanel: PanelState, panels: Map<string, PanelState>): PanelState[];
|
|
912
1014
|
/**
|
|
913
1015
|
* Break the snap relationship between two specific panels
|
|
914
1016
|
*/
|
|
915
1017
|
declare function unsnap(leftPanel: PanelState, rightPanel: PanelState): void;
|
|
916
1018
|
|
|
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 };
|
|
1019
|
+
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, type TagButton, type TagButtonConfig, type TagButtonInputConfig, type TagButtonNumberInputConfig, type TagButtonSelectInputConfig, areInSameChain, createDebugLog, createDebugPanelContent, createDebugPanelInterface, createPanelElement, createPanelState, createPresetAnchor, createTagButton, detachFromGroup, findSnapTarget, getConnectedGroup, getDefaultAnchorConfigs, getDefaultZIndex, getDragZIndex, getLeftmostPanel, getMovingGroupRespectingPins, getPanelDimensions, getPanelPosition, getRightmostPanel, hidePanel, setPanelPosition, setPanelZIndex, setupHoverEnlarge, showPanel, snapPanels, snapPanelsToTarget, toggleCollapse, togglePin, unsnap, updateSnappedPositions };
|