tabby-tabbyspaces 0.0.1 → 0.1.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/.claude/settings.local.json +28 -2
- package/CHANGELOG.md +46 -20
- package/CLAUDE.md +163 -15
- package/README.md +71 -61
- package/RELEASE.md +91 -0
- package/TEST_MCP.md +176 -0
- package/TODO.md +72 -0
- package/cdp-click.js +22 -0
- package/cdp-test.js +28 -0
- package/dist/components/paneEditor.component.d.ts +6 -1
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +22 -7
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +30 -4
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +21 -9
- package/dist/components/workspaceList.component.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.LICENSE.txt +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models/workspace.model.d.ts +4 -2
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +26 -0
- package/dist/providers/settings.provider.d.ts.map +1 -1
- package/dist/providers/toolbar.provider.d.ts +4 -1
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +20 -0
- package/dist/services/startupCommand.service.d.ts.map +1 -0
- package/dist/services/workspaceEditor.service.d.ts +11 -3
- package/dist/services/workspaceEditor.service.d.ts.map +1 -1
- package/docs/marketing_status.md +92 -0
- package/package.json +2 -7
- package/screenshots/editor.png +0 -0
- package/screenshots/pane-edit.png +0 -0
- package/scripts/build-prod.js +39 -0
- package/src/components/paneEditor.component.pug +2 -2
- package/src/components/paneEditor.component.ts +19 -1
- package/src/components/splitPreview.component.pug +45 -5
- package/src/components/splitPreview.component.scss +79 -22
- package/src/components/splitPreview.component.ts +91 -16
- package/src/components/workspaceEditor.component.pug +130 -70
- package/src/components/workspaceEditor.component.scss +205 -120
- package/src/components/workspaceEditor.component.ts +193 -6
- package/src/components/workspaceList.component.pug +31 -20
- package/src/components/workspaceList.component.scss +12 -6
- package/src/components/workspaceList.component.ts +116 -34
- package/src/index.ts +2 -0
- package/src/models/workspace.model.ts +33 -6
- package/src/providers/settings.provider.ts +2 -2
- package/src/providers/toolbar.provider.ts +41 -10
- package/src/services/startupCommand.service.ts +142 -0
- package/src/services/workspaceEditor.service.ts +70 -38
- package/test_cdp.py +50 -0
- package/RELEASE_PLAN.md +0 -161
- package/screenshots/workspace-edit.png +0 -0
|
@@ -1,169 +1,254 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
bottom:
|
|
7
|
-
background: rgba(0, 0, 0, 0.6);
|
|
8
|
-
display: flex;
|
|
9
|
-
align-items: center;
|
|
10
|
-
justify-content: center;
|
|
11
|
-
z-index: 1000;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.workspace-editor-modal {
|
|
15
|
-
background: var(--theme-bg);
|
|
16
|
-
border-radius: 12px;
|
|
17
|
-
width: 90%;
|
|
18
|
-
max-width: 700px;
|
|
19
|
-
max-height: 90vh;
|
|
20
|
-
overflow: hidden;
|
|
21
|
-
display: flex;
|
|
22
|
-
flex-direction: column;
|
|
23
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.modal-header {
|
|
27
|
-
display: flex;
|
|
28
|
-
justify-content: space-between;
|
|
29
|
-
align-items: center;
|
|
30
|
-
padding: 16px 20px;
|
|
31
|
-
border-bottom: 1px solid var(--theme-border);
|
|
1
|
+
// Workspace editor (inline)
|
|
2
|
+
.workspace-editor-inline {
|
|
3
|
+
background: var(--theme-bg-more);
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
border: 1px solid var(--theme-border);
|
|
6
|
+
margin-bottom: 16px;
|
|
32
7
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
font-size: 1.25rem;
|
|
8
|
+
.editor-body {
|
|
9
|
+
padding: 16px;
|
|
36
10
|
}
|
|
37
11
|
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
.editor-footer {
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
align-items: center;
|
|
16
|
+
padding: 10px 16px;
|
|
17
|
+
border-top: 1px solid var(--theme-border);
|
|
18
|
+
background: var(--theme-bg);
|
|
19
|
+
border-radius: 0 0 8px 8px;
|
|
20
|
+
|
|
21
|
+
.startup-checkbox {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 6px;
|
|
25
|
+
font-size: 0.85rem;
|
|
26
|
+
color: var(--theme-fg-more);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
|
|
29
|
+
input {
|
|
30
|
+
margin: 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
.editor-actions {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
|
|
38
|
+
.btn-success {
|
|
39
|
+
background: #10b981;
|
|
40
|
+
border-color: #10b981;
|
|
41
|
+
color: white;
|
|
42
|
+
|
|
43
|
+
&:hover {
|
|
44
|
+
background: #059669;
|
|
45
|
+
border-color: #059669;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.unsaved-indicator {
|
|
50
|
+
color: #f59e0b;
|
|
51
|
+
font-weight: bold;
|
|
52
|
+
margin-left: 2px;
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
overflow-y: auto;
|
|
52
|
-
flex: 1;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.form-row {
|
|
58
|
+
// Shared editor content
|
|
59
|
+
.editor-top-row {
|
|
56
60
|
display: flex;
|
|
57
|
-
|
|
61
|
+
align-items: flex-start;
|
|
62
|
+
gap: 12px;
|
|
58
63
|
margin-bottom: 16px;
|
|
59
|
-
|
|
60
|
-
&:last-child {
|
|
61
|
-
margin-bottom: 0;
|
|
62
|
-
}
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
.color-picker {
|
|
67
|
+
.color-input {
|
|
68
|
+
width: 40px;
|
|
69
|
+
height: 40px;
|
|
70
|
+
padding: 2px;
|
|
71
|
+
border: 1px solid var(--theme-border);
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
background: var(--theme-bg);
|
|
75
|
+
|
|
76
|
+
&::-webkit-color-swatch-wrapper {
|
|
77
|
+
padding: 2px;
|
|
78
|
+
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
&::-webkit-color-swatch {
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
border: none;
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
.
|
|
85
|
-
|
|
87
|
+
.name-input {
|
|
88
|
+
flex: 1;
|
|
89
|
+
min-width: 150px;
|
|
90
|
+
padding: 10px 12px;
|
|
86
91
|
border-radius: 6px;
|
|
87
92
|
border: 1px solid var(--theme-border);
|
|
88
|
-
background: var(--theme-bg
|
|
93
|
+
background: var(--theme-bg);
|
|
89
94
|
color: var(--theme-fg);
|
|
90
|
-
font-size:
|
|
95
|
+
font-size: 1rem;
|
|
96
|
+
font-weight: 500;
|
|
91
97
|
|
|
92
98
|
&:focus {
|
|
93
99
|
outline: none;
|
|
94
100
|
border-color: var(--theme-primary);
|
|
95
101
|
}
|
|
96
|
-
}
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
cursor: pointer;
|
|
103
|
+
&::placeholder {
|
|
104
|
+
color: var(--theme-fg-more);
|
|
105
|
+
font-weight: 400;
|
|
106
|
+
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
.icon-
|
|
106
|
-
|
|
107
|
-
flex-wrap: wrap;
|
|
108
|
-
gap: 4px;
|
|
109
|
-
}
|
|
109
|
+
.icon-picker {
|
|
110
|
+
position: relative;
|
|
110
111
|
|
|
111
|
-
.icon-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
.icon-trigger {
|
|
113
|
+
width: 40px;
|
|
114
|
+
height: 40px;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
border: 1px solid var(--theme-border);
|
|
119
|
+
border-radius: 6px;
|
|
120
|
+
background: var(--theme-bg);
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
font-size: 1.1rem;
|
|
123
|
+
transition: background 0.15s;
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
&:hover {
|
|
126
|
+
background: var(--theme-bg-more);
|
|
127
|
+
}
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
.icon-dropdown {
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 100%;
|
|
133
|
+
right: 0;
|
|
134
|
+
margin-top: 4px;
|
|
135
|
+
padding: 8px;
|
|
136
|
+
background: var(--theme-bg-more);
|
|
137
|
+
border: 1px solid var(--theme-border);
|
|
138
|
+
border-radius: 6px;
|
|
139
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
140
|
+
z-index: 100;
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: repeat(6, 28px);
|
|
143
|
+
gap: 4px;
|
|
133
144
|
}
|
|
134
|
-
}
|
|
135
145
|
|
|
136
|
-
.
|
|
137
|
-
|
|
146
|
+
.icon-option {
|
|
147
|
+
width: 28px;
|
|
148
|
+
height: 28px;
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
border: 1px solid transparent;
|
|
153
|
+
border-radius: 4px;
|
|
154
|
+
background: transparent;
|
|
155
|
+
color: var(--theme-fg-more);
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
transition: all 0.15s;
|
|
158
|
+
font-size: 0.85rem;
|
|
159
|
+
|
|
160
|
+
&:hover {
|
|
161
|
+
background: var(--theme-bg-more-more);
|
|
162
|
+
color: var(--theme-fg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&.selected {
|
|
166
|
+
border-color: currentColor;
|
|
167
|
+
background: var(--theme-bg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
138
171
|
|
|
139
|
-
|
|
172
|
+
// Layout section
|
|
173
|
+
.layout-section {
|
|
174
|
+
.layout-header {
|
|
140
175
|
display: flex;
|
|
141
176
|
justify-content: space-between;
|
|
142
177
|
align-items: center;
|
|
143
|
-
margin-bottom:
|
|
178
|
+
margin-bottom: 8px;
|
|
179
|
+
}
|
|
144
180
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
181
|
+
.layout-title {
|
|
182
|
+
font-size: 0.8rem;
|
|
183
|
+
font-weight: 500;
|
|
184
|
+
color: var(--theme-fg-more);
|
|
185
|
+
text-transform: uppercase;
|
|
186
|
+
letter-spacing: 0.5px;
|
|
149
187
|
}
|
|
150
188
|
}
|
|
151
189
|
|
|
152
|
-
|
|
190
|
+
// Layout toolbar
|
|
191
|
+
.layout-toolbar {
|
|
153
192
|
display: flex;
|
|
154
|
-
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 4px;
|
|
195
|
+
padding: 8px 0;
|
|
196
|
+
margin-bottom: 8px;
|
|
155
197
|
|
|
156
|
-
.
|
|
198
|
+
.toolbar-group {
|
|
157
199
|
display: flex;
|
|
158
200
|
align-items: center;
|
|
159
|
-
gap:
|
|
201
|
+
gap: 2px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.toolbar-label {
|
|
205
|
+
font-size: 0.75rem;
|
|
206
|
+
color: var(--theme-fg-more);
|
|
207
|
+
margin-right: 4px;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
letter-spacing: 0.3px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.toolbar-spacer {
|
|
213
|
+
flex: 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.toolbar-divider {
|
|
217
|
+
width: 1px;
|
|
218
|
+
height: 20px;
|
|
219
|
+
background: var(--theme-border);
|
|
220
|
+
margin: 0 4px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.toolbar-btn {
|
|
224
|
+
padding: 4px 8px;
|
|
225
|
+
background: var(--theme-bg);
|
|
226
|
+
border: 1px solid var(--theme-border);
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
color: var(--theme-fg);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition: all 0.15s;
|
|
231
|
+
font-size: 0.85rem;
|
|
232
|
+
|
|
233
|
+
&:hover:not(:disabled) {
|
|
234
|
+
background: var(--theme-bg-more-more);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
&:disabled {
|
|
238
|
+
opacity: 0.4;
|
|
239
|
+
cursor: not-allowed;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
&.danger {
|
|
243
|
+
color: var(--theme-danger, #ef4444);
|
|
244
|
+
}
|
|
160
245
|
}
|
|
161
246
|
}
|
|
162
247
|
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
248
|
+
.layout-preview-container {
|
|
249
|
+
background: var(--theme-bg);
|
|
250
|
+
border-radius: 6px;
|
|
251
|
+
padding: 8px;
|
|
252
|
+
min-height: 120px;
|
|
253
|
+
max-height: 180px;
|
|
169
254
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, SimpleChanges, HostListener, ElementRef, ViewChild } from '@angular/core'
|
|
2
2
|
import {
|
|
3
3
|
Workspace,
|
|
4
4
|
WorkspacePane,
|
|
@@ -15,12 +15,17 @@ import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
|
15
15
|
template: require('./workspaceEditor.component.pug'),
|
|
16
16
|
styles: [require('./workspaceEditor.component.scss')],
|
|
17
17
|
})
|
|
18
|
-
export class WorkspaceEditorComponent implements OnInit {
|
|
18
|
+
export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewInit {
|
|
19
19
|
@Input() workspace!: Workspace
|
|
20
|
+
@Input() autoFocus = false
|
|
21
|
+
@Input() hasUnsavedChanges = false
|
|
20
22
|
@Output() save = new EventEmitter<Workspace>()
|
|
21
23
|
@Output() cancel = new EventEmitter<void>()
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
@ViewChild('nameInput') nameInput!: ElementRef<HTMLInputElement>
|
|
26
|
+
|
|
27
|
+
selectedPaneId: string | null = null
|
|
28
|
+
editingPane: WorkspacePane | null = null
|
|
24
29
|
showPaneEditor = false
|
|
25
30
|
profiles: TabbyProfile[] = []
|
|
26
31
|
availableIcons = [
|
|
@@ -28,11 +33,69 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
28
33
|
'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
|
|
29
34
|
'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
|
|
30
35
|
]
|
|
36
|
+
iconDropdownOpen = false
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private workspaceService: WorkspaceEditorService,
|
|
40
|
+
private elementRef: ElementRef
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
@HostListener('document:click', ['$event'])
|
|
44
|
+
onDocumentClick(event: MouseEvent): void {
|
|
45
|
+
const iconPicker = this.elementRef.nativeElement.querySelector('.icon-picker')
|
|
46
|
+
if (iconPicker && !iconPicker.contains(event.target as Node)) {
|
|
47
|
+
this.iconDropdownOpen = false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
toggleIconDropdown(): void {
|
|
52
|
+
this.iconDropdownOpen = !this.iconDropdownOpen
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
selectIcon(icon: string): void {
|
|
56
|
+
this.workspace.icon = icon
|
|
57
|
+
this.iconDropdownOpen = false
|
|
58
|
+
}
|
|
33
59
|
|
|
34
60
|
async ngOnInit(): Promise<void> {
|
|
35
61
|
this.profiles = await this.workspaceService.getAvailableProfiles()
|
|
62
|
+
this.initializeWorkspace()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ngAfterViewInit(): void {
|
|
66
|
+
if (this.autoFocus) {
|
|
67
|
+
this.focusNameInput()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private focusNameInput(): void {
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (this.nameInput?.nativeElement) {
|
|
75
|
+
this.nameInput.nativeElement.focus()
|
|
76
|
+
this.nameInput.nativeElement.select()
|
|
77
|
+
}
|
|
78
|
+
}, 0)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
83
|
+
if (changes['workspace'] && !changes['workspace'].firstChange) {
|
|
84
|
+
// Reset component state when workspace input changes
|
|
85
|
+
this.selectedPaneId = null
|
|
86
|
+
this.editingPane = null
|
|
87
|
+
this.showPaneEditor = false
|
|
88
|
+
this.iconDropdownOpen = false
|
|
89
|
+
this.initializeWorkspace()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle autoFocus change
|
|
93
|
+
if (changes['autoFocus']?.currentValue) {
|
|
94
|
+
this.focusNameInput()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private initializeWorkspace(): void {
|
|
36
99
|
if (!this.workspace.root) {
|
|
37
100
|
this.workspace.root = {
|
|
38
101
|
orientation: 'horizontal',
|
|
@@ -54,13 +117,31 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
54
117
|
}
|
|
55
118
|
|
|
56
119
|
selectPane(pane: WorkspacePane): void {
|
|
57
|
-
this.
|
|
120
|
+
this.selectedPaneId = pane.id
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
deselectPane(): void {
|
|
124
|
+
this.selectedPaneId = null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onPreviewBackgroundClick(): void {
|
|
128
|
+
this.deselectPane()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
editPane(pane: WorkspacePane): void {
|
|
132
|
+
this.editingPane = pane
|
|
58
133
|
this.showPaneEditor = true
|
|
59
134
|
}
|
|
60
135
|
|
|
136
|
+
editSelectedPane(): void {
|
|
137
|
+
if (!this.selectedPaneId) return
|
|
138
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
139
|
+
if (pane) this.editPane(pane)
|
|
140
|
+
}
|
|
141
|
+
|
|
61
142
|
closePaneEditor(): void {
|
|
62
143
|
this.showPaneEditor = false
|
|
63
|
-
this.
|
|
144
|
+
this.editingPane = null
|
|
64
145
|
}
|
|
65
146
|
|
|
66
147
|
onPaneSave(pane: WorkspacePane): void {
|
|
@@ -68,6 +149,33 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
68
149
|
this.closePaneEditor()
|
|
69
150
|
}
|
|
70
151
|
|
|
152
|
+
// Helper functions
|
|
153
|
+
private findPaneById(id: string): WorkspacePane | null {
|
|
154
|
+
return this.findPaneInNode(this.workspace.root, id)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private findPaneInNode(node: WorkspaceSplit, id: string): WorkspacePane | null {
|
|
158
|
+
for (const child of node.children) {
|
|
159
|
+
if (isWorkspaceSplit(child)) {
|
|
160
|
+
const found = this.findPaneInNode(child, id)
|
|
161
|
+
if (found) return found
|
|
162
|
+
} else if (child.id === id) {
|
|
163
|
+
return child
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
canRemovePane(): boolean {
|
|
170
|
+
return this.countPanes(this.workspace.root) > 1
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private countPanes(node: WorkspaceSplit): number {
|
|
174
|
+
return node.children.reduce((count, child) => {
|
|
175
|
+
return count + (isWorkspaceSplit(child) ? this.countPanes(child) : 1)
|
|
176
|
+
}, 0)
|
|
177
|
+
}
|
|
178
|
+
|
|
71
179
|
private updatePaneInTree(node: WorkspaceSplit, updatedPane: WorkspacePane): boolean {
|
|
72
180
|
for (let i = 0; i < node.children.length; i++) {
|
|
73
181
|
const child = node.children[i]
|
|
@@ -87,6 +195,12 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
87
195
|
this.splitPaneInTree(this.workspace.root, pane, orientation)
|
|
88
196
|
}
|
|
89
197
|
|
|
198
|
+
splitSelectedPane(orientation: 'horizontal' | 'vertical'): void {
|
|
199
|
+
if (!this.selectedPaneId) return
|
|
200
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
201
|
+
if (pane) this.splitPane(pane, orientation)
|
|
202
|
+
}
|
|
203
|
+
|
|
90
204
|
private splitPaneInTree(
|
|
91
205
|
node: WorkspaceSplit,
|
|
92
206
|
targetPane: WorkspacePane,
|
|
@@ -115,9 +229,18 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
115
229
|
}
|
|
116
230
|
|
|
117
231
|
removePane(pane: WorkspacePane): void {
|
|
232
|
+
if (this.selectedPaneId === pane.id) {
|
|
233
|
+
this.selectedPaneId = null
|
|
234
|
+
}
|
|
118
235
|
this.removePaneFromTree(this.workspace.root, pane)
|
|
119
236
|
}
|
|
120
237
|
|
|
238
|
+
removeSelectedPane(): void {
|
|
239
|
+
if (!this.selectedPaneId || !this.canRemovePane()) return
|
|
240
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
241
|
+
if (pane) this.removePane(pane)
|
|
242
|
+
}
|
|
243
|
+
|
|
121
244
|
private removePaneFromTree(node: WorkspaceSplit, targetPane: WorkspacePane): boolean {
|
|
122
245
|
for (let i = 0; i < node.children.length; i++) {
|
|
123
246
|
const child = node.children[i]
|
|
@@ -178,4 +301,68 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
178
301
|
this.workspace.root.ratios = ratios
|
|
179
302
|
}
|
|
180
303
|
|
|
304
|
+
// Add pane operations
|
|
305
|
+
addPane(direction: 'left' | 'right' | 'top' | 'bottom'): void {
|
|
306
|
+
if (!this.selectedPaneId) return
|
|
307
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
308
|
+
if (!pane) return
|
|
309
|
+
this.addPaneInTree(this.workspace.root, pane, direction, null)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
addPaneFromEvent(pane: WorkspacePane, direction: 'left' | 'right' | 'top' | 'bottom'): void {
|
|
313
|
+
this.addPaneInTree(this.workspace.root, pane, direction, null)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private addPaneInTree(
|
|
317
|
+
node: WorkspaceSplit,
|
|
318
|
+
targetPane: WorkspacePane,
|
|
319
|
+
direction: 'left' | 'right' | 'top' | 'bottom',
|
|
320
|
+
parentNode: WorkspaceSplit | null
|
|
321
|
+
): boolean {
|
|
322
|
+
const isHorizontalAdd = direction === 'left' || direction === 'right'
|
|
323
|
+
const isBefore = direction === 'left' || direction === 'top'
|
|
324
|
+
const targetOrientation = isHorizontalAdd ? 'horizontal' : 'vertical'
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
327
|
+
const child = node.children[i]
|
|
328
|
+
|
|
329
|
+
if (isWorkspaceSplit(child)) {
|
|
330
|
+
if (this.addPaneInTree(child, targetPane, direction, node)) return true
|
|
331
|
+
} else if (child.id === targetPane.id) {
|
|
332
|
+
const newPane = createDefaultPane()
|
|
333
|
+
newPane.profileId = child.profileId
|
|
334
|
+
|
|
335
|
+
if (node.orientation === targetOrientation) {
|
|
336
|
+
// Same orientation: add as sibling
|
|
337
|
+
const insertIndex = isBefore ? i : i + 1
|
|
338
|
+
node.children.splice(insertIndex, 0, newPane)
|
|
339
|
+
this.recalculateRatios(node)
|
|
340
|
+
} else {
|
|
341
|
+
// Different orientation: wrap entire node in new split
|
|
342
|
+
const nodeCopy: WorkspaceSplit = {
|
|
343
|
+
orientation: node.orientation,
|
|
344
|
+
ratios: [...node.ratios],
|
|
345
|
+
children: [...node.children]
|
|
346
|
+
}
|
|
347
|
+
const wrapper: WorkspaceSplit = {
|
|
348
|
+
orientation: targetOrientation,
|
|
349
|
+
ratios: [0.5, 0.5],
|
|
350
|
+
children: isBefore ? [newPane, nodeCopy] : [nodeCopy, newPane]
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (node === this.workspace.root) {
|
|
354
|
+
this.workspace.root = wrapper
|
|
355
|
+
} else if (parentNode) {
|
|
356
|
+
const nodeIndex = parentNode.children.indexOf(node)
|
|
357
|
+
if (nodeIndex !== -1) {
|
|
358
|
+
parentNode.children[nodeIndex] = wrapper
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return false
|
|
366
|
+
}
|
|
367
|
+
|
|
181
368
|
}
|