@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 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
- - `positionPanelsFromRight(panelIds, gap?)` - Position from right edge
123
- - `positionPanelsFromLeft(panelIds, gap?)` - Position from left edge
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 = connectedPanels;
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
- if (e.target === state.collapseButton || e.target === state.detachGrip) {
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 };