split-dock 1.0.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/src/index.js ADDED
@@ -0,0 +1,115 @@
1
+ import { Panel } from './panel.js';
2
+ import { Dock } from './dock.js';
3
+ import { Frame } from './frame.js';
4
+
5
+ // Auto-load CSS
6
+ (function loadCSS() {
7
+ const existingLink = document.querySelector('link[href*="style.css"]');
8
+ if (!existingLink) {
9
+ const link = document.createElement('link');
10
+ link.rel = 'stylesheet';
11
+ link.href = new URL('./style.css', import.meta.url).href;
12
+ document.head.appendChild(link);
13
+ }
14
+ })();
15
+
16
+ // ID counter for generating unique IDs
17
+ let idCounter = 0;
18
+ export function generateId() {
19
+ return `sd-${Date.now()}-${++idCounter}`;
20
+ }
21
+
22
+ // Main framework
23
+ export class SplitDock {
24
+ constructor() {
25
+ this.rootFrames = [];
26
+ this.draggedPanel = null;
27
+ this.draggedFromDock = null;
28
+ this.dragGhost = null;
29
+ this.currentHoverDock = null;
30
+ }
31
+
32
+ // Initialize framework by loading frame elements from HTML
33
+ initialize() {
34
+ const rootElement = document.getElementById('app');
35
+ if (rootElement && rootElement.classList.contains('sd-frame')) {
36
+ const rootFrame = new Frame(rootElement, this);
37
+ this.rootFrames.push(rootFrame);
38
+ }
39
+
40
+ return this;
41
+ }
42
+
43
+ startDragging(panel, dock) {
44
+ this.draggedPanel = panel;
45
+ this.draggedFromDock = dock;
46
+
47
+ this.dragGhost = document.createElement('div');
48
+ this.dragGhost.className = 'drag-ghost';
49
+ this.dragGhost.textContent = panel.title;
50
+ document.body.appendChild(this.dragGhost);
51
+
52
+ document.addEventListener('dragover', this.updateDragGhost);
53
+ }
54
+
55
+ stopDragging() {
56
+ if (this.dragGhost) {
57
+ this.dragGhost.remove();
58
+ this.dragGhost = null;
59
+ }
60
+
61
+ document.removeEventListener('dragover', this.updateDragGhost);
62
+
63
+ // Clear drop indicators from the currently hovered dock only
64
+ if (this.currentHoverDock) {
65
+ this.currentHoverDock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
66
+ // Also clear tab reordering indicators
67
+ const titleElements = Array.from(this.currentHoverDock.navbar.querySelectorAll('.sd-panel-title'));
68
+ titleElements.forEach(el => el.classList.remove('drop-before', 'drop-after'));
69
+ }
70
+
71
+ this.draggedPanel = null;
72
+ this.draggedFromDock = null;
73
+ this.currentHoverDock = null;
74
+ }
75
+
76
+ updateDragGhost = (e) => {
77
+ if (this.dragGhost) {
78
+ this.dragGhost.style.left = (e.clientX + 10) + 'px';
79
+ this.dragGhost.style.top = (e.clientY + 10) + 'px';
80
+ }
81
+ }
82
+
83
+ destroy() {
84
+ // Cleanup drag ghost if exists
85
+ if (this.dragGhost) {
86
+ this.dragGhost.remove();
87
+ this.dragGhost = null;
88
+ }
89
+
90
+ // Remove event listeners
91
+ document.removeEventListener('dragover', this.updateDragGhost);
92
+
93
+ // Destroy all root frames
94
+ this.rootFrames.forEach(frame => frame.destroy());
95
+ this.rootFrames = [];
96
+
97
+ // Clear references
98
+ this.draggedPanel = null;
99
+ this.draggedFromDock = null;
100
+ this.currentHoverDock = null;
101
+ }
102
+ }
103
+
104
+ // Export classes to window object for backward compatibility
105
+ window.SplitDock = SplitDock;
106
+ window.SdPanel = Panel;
107
+ window.SdDock = Dock;
108
+ window.SdFrame = Frame;
109
+
110
+ // Auto-initialize when DOM is ready
111
+ document.addEventListener('DOMContentLoaded', () => {
112
+ const framework = new SplitDock();
113
+ framework.initialize();
114
+ window.splitDock = framework;
115
+ });
package/src/panel.js ADDED
@@ -0,0 +1,122 @@
1
+ import { generateId } from './index.js';
2
+
3
+ // Panel class - represents a single panel with title and content
4
+ export class Panel {
5
+ constructor(titleElement, contentElement) {
6
+ this.id = generateId();
7
+ this.dock = null;
8
+ this.eventListeners = [];
9
+
10
+ // Use existing elements from HTML or create new ones
11
+ if (titleElement && contentElement) {
12
+ this.titleElement = titleElement;
13
+ this.contentElement = contentElement;
14
+ this.title = titleElement.textContent.trim();
15
+ this.content = contentElement.innerHTML;
16
+ } else {
17
+ // Fallback for programmatic creation
18
+ this.title = titleElement || 'New Panel';
19
+ this.content = contentElement || '';
20
+ this.createElements();
21
+ }
22
+
23
+ this.setupElements();
24
+ }
25
+
26
+ createElements() {
27
+ this.titleElement = document.createElement('div');
28
+ this.titleElement.className = 'sd-panel-title';
29
+ this.titleElement.innerHTML = `
30
+ <span class="sd-panel-title-text">${this.title}</span>
31
+ <span class="sd-panel-close">×</span>
32
+ `;
33
+
34
+ this.contentElement = document.createElement('div');
35
+ this.contentElement.className = 'sd-panel-content';
36
+ this.contentElement.innerHTML = this.content;
37
+ }
38
+
39
+ setupElements() {
40
+ // Make title draggable and ensure it has close button
41
+ this.titleElement.setAttribute('draggable', 'true');
42
+ this.titleElement.draggable = true;
43
+
44
+ if (!this.titleElement.querySelector('.sd-panel-close')) {
45
+ const closeBtn = document.createElement('span');
46
+ closeBtn.className = 'sd-panel-close';
47
+ closeBtn.textContent = '×';
48
+ this.titleElement.appendChild(closeBtn);
49
+ }
50
+
51
+ this.setupEventListeners();
52
+ }
53
+
54
+ setupEventListeners() {
55
+ const clickHandler = (e) => {
56
+ if (!e.target.classList.contains('sd-panel-close') && this.dock) {
57
+ this.dock.setActivePanel(this);
58
+ }
59
+ };
60
+ this.titleElement.addEventListener('click', clickHandler);
61
+ this.eventListeners.push({ element: this.titleElement, event: 'click', handler: clickHandler });
62
+
63
+ const closeHandler = (e) => {
64
+ e.stopPropagation();
65
+ if (this.dock) {
66
+ this.dock.removePanel(this);
67
+ }
68
+ };
69
+ const closeBtn = this.titleElement.querySelector('.sd-panel-close');
70
+ closeBtn.addEventListener('click', closeHandler);
71
+ this.eventListeners.push({ element: closeBtn, event: 'click', handler: closeHandler });
72
+
73
+ const dragStartHandler = (e) => this.onDragStart(e);
74
+ this.titleElement.addEventListener('dragstart', dragStartHandler);
75
+ this.eventListeners.push({ element: this.titleElement, event: 'dragstart', handler: dragStartHandler });
76
+
77
+ const dragEndHandler = (e) => this.onDragEnd(e);
78
+ this.titleElement.addEventListener('dragend', dragEndHandler);
79
+ this.eventListeners.push({ element: this.titleElement, event: 'dragend', handler: dragEndHandler });
80
+ }
81
+
82
+ onDragStart(e) {
83
+ if (this.dock && this.dock.splitDock) {
84
+ this.dock.splitDock.startDragging(this, this.dock);
85
+ }
86
+ this.titleElement.classList.add('dragging');
87
+ e.dataTransfer.effectAllowed = 'move';
88
+ e.dataTransfer.setData('text/html', this.titleElement.innerHTML);
89
+ }
90
+
91
+ onDragEnd(e) {
92
+ if (this.dock && this.dock.splitDock) {
93
+ this.dock.splitDock.stopDragging();
94
+ }
95
+ this.titleElement.classList.remove('dragging');
96
+ }
97
+
98
+ activate() {
99
+ this.titleElement.classList.add('active');
100
+ this.contentElement.classList.add('active');
101
+ }
102
+
103
+ deactivate() {
104
+ this.titleElement.classList.remove('active');
105
+ this.contentElement.classList.remove('active');
106
+ }
107
+
108
+ remove() {
109
+ this.destroy();
110
+ this.titleElement.remove();
111
+ this.contentElement.remove();
112
+ }
113
+
114
+ destroy() {
115
+ // Remove all event listeners
116
+ this.eventListeners.forEach(({ element, event, handler }) => {
117
+ element.removeEventListener(event, handler);
118
+ });
119
+ this.eventListeners = [];
120
+ this.dock = null;
121
+ }
122
+ }
package/src/style.css ADDED
@@ -0,0 +1,276 @@
1
+ :root {
2
+ /* Color scheme */
3
+ --sd-bg-primary: #1e1e1e;
4
+ --sd-bg-secondary: #252526;
5
+ --sd-bg-tertiary: #2d2d30;
6
+ --sd-bg-hover: #37373d;
7
+ --sd-border-color: #3e3e42;
8
+ --sd-text-primary: #ffffff;
9
+ --sd-text-secondary: #cccccc;
10
+ --sd-text-muted: #6a6a6a;
11
+
12
+ /* Accent colors */
13
+ --sd-accent-primary: #1e88e5;
14
+ --sd-accent-dark: #0e639c;
15
+ --sd-accent-bg: rgba(14, 99, 156, 0.1);
16
+ --sd-accent-bg-medium: rgba(14, 99, 156, 0.3);
17
+ --sd-accent-bg-strong: rgba(14, 99, 156, 0.4);
18
+
19
+ /* Spacing */
20
+ --sd-padding-small: 8px;
21
+ --sd-padding-medium: 16px;
22
+ --sd-padding-large: 20px;
23
+
24
+ /* Sizes */
25
+ --sd-border-width: 1px;
26
+ --sd-border-width-thick: 3px;
27
+ --sd-handle-size: 4px;
28
+ --sd-min-dock-size: 100px;
29
+ --sd-scrollbar-size: 4px;
30
+
31
+ /* Transitions */
32
+ --sd-transition-fast: 0.2s;
33
+ }
34
+
35
+ * {
36
+ margin: 0;
37
+ padding: 0;
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ body {
42
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
43
+ height: 100vh;
44
+ overflow: hidden;
45
+ background: var(--sd-bg-primary);
46
+ }
47
+
48
+ /* Root frame only */
49
+ #app.sd-frame {
50
+ width: 100%;
51
+ height: 100vh;
52
+ }
53
+
54
+ /* Frame - can contain multiple docks in split view */
55
+ .sd-frame {
56
+ display: flex;
57
+ flex-direction: row;
58
+ flex: 1;
59
+ min-width: 0;
60
+ min-height: 0;
61
+ }
62
+
63
+ .sd-frame:last-child {
64
+ border-right: none;
65
+ }
66
+
67
+ .sd-frame.horizontal {
68
+ flex-direction: row;
69
+ }
70
+
71
+ .sd-frame.vertical {
72
+ flex-direction: column;
73
+ }
74
+
75
+ .sd-dock {
76
+ flex: 1;
77
+ display: flex;
78
+ flex-direction: column;
79
+ background: var(--sd-bg-secondary);
80
+ border: var(--sd-border-width) solid var(--sd-border-color);
81
+ position: relative;
82
+ min-width: var(--sd-min-dock-size);
83
+ min-height: var(--sd-min-dock-size);
84
+ }
85
+
86
+ .sd-dock-navbar {
87
+ display: flex;
88
+ background: var(--sd-bg-tertiary);
89
+ border-bottom: var(--sd-border-width) solid var(--sd-border-color);
90
+ overflow-x: auto;
91
+ overflow-y: hidden;
92
+ flex-shrink: 0;
93
+ }
94
+
95
+ .sd-dock-navbar::-webkit-scrollbar {
96
+ height: var(--sd-scrollbar-size);
97
+ }
98
+
99
+ .sd-dock-navbar::-webkit-scrollbar-thumb {
100
+ background: #555;
101
+ border-radius: 2px;
102
+ }
103
+
104
+ .sd-panel-title {
105
+ padding: var(--sd-padding-small) var(--sd-padding-medium);
106
+ background: var(--sd-bg-tertiary);
107
+ color: var(--sd-text-secondary);
108
+ border-right: var(--sd-border-width) solid var(--sd-border-color);
109
+ cursor: pointer;
110
+ user-select: none;
111
+ white-space: nowrap;
112
+ display: flex;
113
+ align-items: center;
114
+ gap: var(--sd-padding-small);
115
+ transition: background var(--sd-transition-fast);
116
+ }
117
+
118
+ .sd-panel-title:hover {
119
+ background: var(--sd-bg-hover);
120
+ }
121
+
122
+ .sd-panel-title.active {
123
+ background: var(--sd-bg-primary);
124
+ color: var(--sd-text-primary);
125
+ }
126
+
127
+ .sd-panel-title.dragging {
128
+ opacity: 0.5;
129
+ }
130
+
131
+ .sd-panel-title.drop-before {
132
+ border-left: var(--sd-border-width-thick) solid var(--sd-accent-primary);
133
+ }
134
+
135
+ .sd-panel-title.drop-after {
136
+ border-right: var(--sd-border-width-thick) solid var(--sd-accent-primary);
137
+ }
138
+
139
+ .sd-panel-title-text {
140
+ pointer-events: none;
141
+ }
142
+
143
+ .sd-panel-close {
144
+ width: var(--sd-padding-medium);
145
+ height: var(--sd-padding-medium);
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ border-radius: 3px;
150
+ font-size: var(--sd-padding-medium);
151
+ line-height: 1;
152
+ }
153
+
154
+ .sd-panel-close:hover {
155
+ background: #555;
156
+ }
157
+
158
+ .sd-dock-content {
159
+ flex: 1;
160
+ overflow: auto;
161
+ position: relative;
162
+ }
163
+
164
+ .sd-panel-content {
165
+ display: none;
166
+ padding: var(--sd-padding-large);
167
+ color: var(--sd-text-secondary);
168
+ height: 100%;
169
+ }
170
+
171
+ .sd-panel-content.active {
172
+ display: block;
173
+ }
174
+
175
+ /* Drop zones - 5 zones per dock */
176
+ .sd-dock.drop-center {
177
+ outline: var(--sd-border-width-thick) solid var(--sd-accent-dark);
178
+ outline-offset: calc(-1 * var(--sd-border-width-thick));
179
+ background: var(--sd-accent-bg);
180
+ }
181
+
182
+ .sd-dock.drop-navbar .sd-dock-navbar {
183
+ background: var(--sd-accent-bg-medium);
184
+ outline: 2px solid var(--sd-accent-primary);
185
+ outline-offset: -2px;
186
+ }
187
+
188
+ .sd-dock.drop-top::before,
189
+ .sd-dock.drop-bottom::before,
190
+ .sd-dock.drop-left::before,
191
+ .sd-dock.drop-right::before {
192
+ content: '';
193
+ position: absolute;
194
+ background: var(--sd-accent-bg-strong);
195
+ border: 2px dashed var(--sd-accent-primary);
196
+ pointer-events: none;
197
+ z-index: 1000;
198
+ }
199
+
200
+ .sd-dock.drop-top::before {
201
+ top: 0;
202
+ left: 0;
203
+ right: 0;
204
+ height: 30%;
205
+ }
206
+
207
+ .sd-dock.drop-bottom::before {
208
+ bottom: 0;
209
+ left: 0;
210
+ right: 0;
211
+ height: 30%;
212
+ }
213
+
214
+ .sd-dock.drop-left::before {
215
+ top: 0;
216
+ left: 0;
217
+ bottom: 0;
218
+ width: 30%;
219
+ }
220
+
221
+ .sd-dock.drop-right::before {
222
+ top: 0;
223
+ right: 0;
224
+ bottom: 0;
225
+ width: 30%;
226
+ }
227
+
228
+ /* Drag ghost */
229
+ .drag-ghost {
230
+ position: fixed;
231
+ background: var(--sd-bg-tertiary);
232
+ color: var(--sd-text-secondary);
233
+ padding: var(--sd-padding-small) var(--sd-padding-medium);
234
+ border: var(--sd-border-width) solid var(--sd-accent-dark);
235
+ border-radius: 4px;
236
+ pointer-events: none;
237
+ z-index: 10000;
238
+ opacity: 0.8;
239
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
240
+ }
241
+
242
+ /* Empty dock message */
243
+ .sd-dock-empty {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ color: var(--sd-text-muted);
248
+ font-size: 14px;
249
+ height: 100%;
250
+ }
251
+
252
+ /* Resize handles */
253
+ .sd-resize-handle {
254
+ position: relative;
255
+ flex-shrink: 0;
256
+ background: var(--sd-border-color);
257
+ z-index: 10;
258
+ }
259
+
260
+ .sd-resize-handle:hover {
261
+ background: var(--sd-accent-dark);
262
+ }
263
+
264
+ .sd-resize-handle:active {
265
+ background: var(--sd-accent-primary);
266
+ }
267
+
268
+ .sd-resize-handle.horizontal {
269
+ width: var(--sd-handle-size);
270
+ cursor: col-resize;
271
+ }
272
+
273
+ .sd-resize-handle.vertical {
274
+ height: var(--sd-handle-size);
275
+ cursor: row-resize;
276
+ }