@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/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
|
*/
|
|
@@ -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 };
|
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
|
}
|
|
@@ -1497,6 +1574,80 @@ var TabManager = class {
|
|
|
1497
1574
|
this.debugPanelElements.clear();
|
|
1498
1575
|
}
|
|
1499
1576
|
};
|
|
1577
|
+
|
|
1578
|
+
// src/TagButton.ts
|
|
1579
|
+
function createTagButton(label, config = {}) {
|
|
1580
|
+
const { defaultActive = false, inputs = [], onChange } = config;
|
|
1581
|
+
const hasInputs = inputs.length > 0;
|
|
1582
|
+
const el = document.createElement(hasInputs ? "div" : "button");
|
|
1583
|
+
el.className = "blork-tabs-tag-btn";
|
|
1584
|
+
if (hasInputs) {
|
|
1585
|
+
el.setAttribute("role", "button");
|
|
1586
|
+
el.setAttribute("tabindex", "0");
|
|
1587
|
+
}
|
|
1588
|
+
el.appendChild(document.createTextNode(label));
|
|
1589
|
+
const inputElements = [];
|
|
1590
|
+
if (hasInputs) {
|
|
1591
|
+
const inputsContainer = document.createElement("span");
|
|
1592
|
+
inputsContainer.className = "blork-tabs-tag-inputs";
|
|
1593
|
+
for (const inputConfig of inputs) {
|
|
1594
|
+
if (inputConfig.label) {
|
|
1595
|
+
const labelSpan = document.createElement("span");
|
|
1596
|
+
labelSpan.textContent = inputConfig.label;
|
|
1597
|
+
inputsContainer.appendChild(labelSpan);
|
|
1598
|
+
}
|
|
1599
|
+
if (inputConfig.type === "select") {
|
|
1600
|
+
const select = document.createElement("select");
|
|
1601
|
+
select.className = "blork-tabs-tag-select";
|
|
1602
|
+
for (const opt of inputConfig.options ?? []) {
|
|
1603
|
+
const option = document.createElement("option");
|
|
1604
|
+
option.value = opt.value;
|
|
1605
|
+
option.textContent = opt.label;
|
|
1606
|
+
select.appendChild(option);
|
|
1607
|
+
}
|
|
1608
|
+
select.addEventListener("click", (e) => e.stopPropagation());
|
|
1609
|
+
select.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
1610
|
+
inputsContainer.appendChild(select);
|
|
1611
|
+
inputElements.push(select);
|
|
1612
|
+
} else {
|
|
1613
|
+
const input = document.createElement("input");
|
|
1614
|
+
input.type = "number";
|
|
1615
|
+
input.className = "blork-tabs-tag-input";
|
|
1616
|
+
if (inputConfig.defaultValue !== void 0) input.value = String(inputConfig.defaultValue);
|
|
1617
|
+
if (inputConfig.step !== void 0) input.step = String(inputConfig.step);
|
|
1618
|
+
if (inputConfig.min !== void 0) input.min = String(inputConfig.min);
|
|
1619
|
+
if (inputConfig.max !== void 0) input.max = String(inputConfig.max);
|
|
1620
|
+
input.addEventListener("click", (e) => e.stopPropagation());
|
|
1621
|
+
inputsContainer.appendChild(input);
|
|
1622
|
+
inputElements.push(input);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
el.appendChild(inputsContainer);
|
|
1626
|
+
}
|
|
1627
|
+
let active = defaultActive;
|
|
1628
|
+
if (active) el.classList.add("active");
|
|
1629
|
+
const setActive = (value) => {
|
|
1630
|
+
active = value;
|
|
1631
|
+
el.classList.toggle("active", active);
|
|
1632
|
+
onChange?.(active);
|
|
1633
|
+
};
|
|
1634
|
+
el.addEventListener("click", () => setActive(!active));
|
|
1635
|
+
el.addEventListener("keydown", (e) => {
|
|
1636
|
+
const ke = e;
|
|
1637
|
+
if (ke.key === "Enter" || ke.key === " ") {
|
|
1638
|
+
ke.preventDefault();
|
|
1639
|
+
setActive(!active);
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
return {
|
|
1643
|
+
element: el,
|
|
1644
|
+
isActive: () => active,
|
|
1645
|
+
setActive,
|
|
1646
|
+
toggle: () => setActive(!active),
|
|
1647
|
+
getValue: (index) => inputElements[index]?.value ?? "",
|
|
1648
|
+
getInput: (index) => inputElements[index]
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1500
1651
|
export {
|
|
1501
1652
|
AnchorManager,
|
|
1502
1653
|
AutoHideManager,
|
|
@@ -1510,6 +1661,7 @@ export {
|
|
|
1510
1661
|
createPanelElement,
|
|
1511
1662
|
createPanelState,
|
|
1512
1663
|
createPresetAnchor,
|
|
1664
|
+
createTagButton,
|
|
1513
1665
|
detachFromGroup,
|
|
1514
1666
|
findSnapTarget,
|
|
1515
1667
|
getConnectedGroup,
|
|
@@ -1517,6 +1669,7 @@ export {
|
|
|
1517
1669
|
getDefaultZIndex,
|
|
1518
1670
|
getDragZIndex,
|
|
1519
1671
|
getLeftmostPanel,
|
|
1672
|
+
getMovingGroupRespectingPins,
|
|
1520
1673
|
getPanelDimensions,
|
|
1521
1674
|
getPanelPosition,
|
|
1522
1675
|
getRightmostPanel,
|
|
@@ -1528,6 +1681,7 @@ export {
|
|
|
1528
1681
|
snapPanels,
|
|
1529
1682
|
snapPanelsToTarget,
|
|
1530
1683
|
toggleCollapse,
|
|
1684
|
+
togglePin,
|
|
1531
1685
|
unsnap,
|
|
1532
1686
|
updateSnappedPositions
|
|
1533
1687
|
};
|
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;
|
|
@@ -200,6 +220,88 @@
|
|
|
200
220
|
transition: none !important;
|
|
201
221
|
}
|
|
202
222
|
|
|
223
|
+
/* Tag Button */
|
|
224
|
+
.blork-tabs-tag-btn {
|
|
225
|
+
padding: 4px 10px;
|
|
226
|
+
background: var(--blork-tabs-panel-bg, #1a1a2e);
|
|
227
|
+
border: 1px solid #3a3a5a;
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
color: #aaa;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
font-size: 11px;
|
|
232
|
+
font-family: monospace;
|
|
233
|
+
transition: all 0.15s;
|
|
234
|
+
user-select: none;
|
|
235
|
+
display: inline-flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 6px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.blork-tabs-tag-btn:hover {
|
|
241
|
+
border-color: #5a5a7a;
|
|
242
|
+
color: #ccc;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.blork-tabs-tag-btn.active {
|
|
246
|
+
border-color: var(--blork-tabs-accent, #4a90d9);
|
|
247
|
+
color: var(--blork-tabs-accent, #4a90d9);
|
|
248
|
+
box-shadow: 0 0 8px rgba(74, 144, 217, 0.5), 0 0 2px rgba(74, 144, 217, 0.8);
|
|
249
|
+
text-shadow: 0 0 8px rgba(74, 144, 217, 0.9);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Tag inputs (hidden until button is active) */
|
|
253
|
+
.blork-tabs-tag-inputs {
|
|
254
|
+
display: none;
|
|
255
|
+
align-items: center;
|
|
256
|
+
gap: 3px;
|
|
257
|
+
font-size: 10px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.blork-tabs-tag-btn.active .blork-tabs-tag-inputs {
|
|
261
|
+
display: inline-flex;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.blork-tabs-tag-input {
|
|
265
|
+
width: 36px;
|
|
266
|
+
background: transparent;
|
|
267
|
+
border: none;
|
|
268
|
+
border-bottom: 1px solid #555;
|
|
269
|
+
color: inherit;
|
|
270
|
+
font-size: 10px;
|
|
271
|
+
font-family: monospace;
|
|
272
|
+
text-align: center;
|
|
273
|
+
padding: 0 2px;
|
|
274
|
+
outline: none;
|
|
275
|
+
-moz-appearance: textfield;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.blork-tabs-tag-input::-webkit-outer-spin-button,
|
|
279
|
+
.blork-tabs-tag-input::-webkit-inner-spin-button {
|
|
280
|
+
-webkit-appearance: none;
|
|
281
|
+
margin: 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.blork-tabs-tag-btn.active .blork-tabs-tag-input {
|
|
285
|
+
border-bottom-color: rgba(74, 144, 217, 0.6);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.blork-tabs-tag-select {
|
|
289
|
+
background: var(--blork-tabs-panel-bg, #1a1a2e);
|
|
290
|
+
border: none;
|
|
291
|
+
border-bottom: 1px solid #555;
|
|
292
|
+
color: #ccc;
|
|
293
|
+
font-size: 10px;
|
|
294
|
+
font-family: monospace;
|
|
295
|
+
outline: none;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
max-width: 80px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.blork-tabs-tag-btn.active .blork-tabs-tag-select {
|
|
301
|
+
border-bottom-color: rgba(74, 144, 217, 0.6);
|
|
302
|
+
color: var(--blork-tabs-accent, #4a90d9);
|
|
303
|
+
}
|
|
304
|
+
|
|
203
305
|
/* Debug Panel Log */
|
|
204
306
|
.blork-tabs-debug-log {
|
|
205
307
|
min-height: 100px;
|
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
|
*/
|