tabby-tabbyspaces 0.0.1 → 0.2.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 +29 -2
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +81 -0
- package/.github/workflows/release.yml +30 -0
- package/CHANGELOG.md +92 -20
- package/CLAUDE.md +196 -15
- package/CONTRIBUTING.md +3 -1
- package/README.md +80 -61
- package/RELEASE.md +91 -0
- package/TODO.md +77 -0
- package/dist/build-config.d.ts +3 -3
- package/dist/components/deleteConfirmModal.component.d.ts +7 -0
- package/dist/components/deleteConfirmModal.component.d.ts.map +1 -0
- package/dist/components/paneEditor.component.d.ts +9 -13
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +50 -35
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +61 -28
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +56 -27
- package/dist/components/workspaceList.component.d.ts.map +1 -1
- package/dist/index.d.ts +6 -6
- 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 +118 -76
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +26 -0
- package/dist/providers/config.provider.d.ts +8 -8
- package/dist/providers/settings.provider.d.ts +7 -7
- package/dist/providers/settings.provider.d.ts.map +1 -1
- package/dist/providers/toolbar.provider.d.ts +23 -12
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +28 -0
- package/dist/services/startupCommand.service.d.ts.map +1 -0
- package/dist/services/workspaceBackground.service.d.ts +38 -0
- package/dist/services/workspaceBackground.service.d.ts.map +1 -0
- package/dist/services/workspaceEditor.service.d.ts +46 -24
- package/dist/services/workspaceEditor.service.d.ts.map +1 -1
- package/docs/DESIGN.md +57 -0
- package/docs/SESSION-2026-01-14-S1-DESIGN.md +134 -0
- package/docs/marketing_status.md +92 -0
- package/mockups/index.html +162 -0
- package/mockups/s1-tight-sharp.html +522 -0
- package/mockups/shared/base.css +216 -0
- package/mockups/v06-tabbed.html +643 -0
- package/package.json +3 -7
- package/screenshots/editor.png +0 -0
- package/screenshots/pane-edit.png +0 -0
- package/scripts/build-dev.js +2 -1
- package/scripts/build-prod.js +40 -0
- package/src/components/deleteConfirmModal.component.ts +23 -0
- package/src/components/paneEditor.component.pug +27 -43
- package/src/components/paneEditor.component.scss +37 -85
- package/src/components/paneEditor.component.ts +6 -16
- package/src/components/splitPreview.component.pug +36 -5
- package/src/components/splitPreview.component.scss +78 -45
- package/src/components/splitPreview.component.ts +83 -18
- package/src/components/workspaceEditor.component.pug +162 -74
- package/src/components/workspaceEditor.component.scss +261 -108
- package/src/components/workspaceEditor.component.ts +294 -31
- package/src/components/workspaceList.component.pug +32 -41
- package/src/components/workspaceList.component.scss +89 -74
- package/src/components/workspaceList.component.ts +181 -44
- package/src/index.ts +6 -0
- package/src/models/workspace.model.ts +113 -8
- package/src/providers/settings.provider.ts +2 -2
- package/src/providers/toolbar.provider.ts +113 -13
- package/src/services/startupCommand.service.ts +140 -0
- package/src/services/workspaceBackground.service.ts +167 -0
- package/src/services/workspaceEditor.service.ts +134 -65
- package/src/styles/_index.scss +3 -0
- package/src/styles/_mixins.scss +180 -0
- package/src/styles/_variables.scss +67 -0
- package/RELEASE_PLAN.md +0 -161
- package/screenshots/workspace-edit.png +0 -0
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, SimpleChanges, HostListener, ElementRef, ViewChild, ChangeDetectorRef } from '@angular/core'
|
|
2
2
|
import {
|
|
3
3
|
Workspace,
|
|
4
4
|
WorkspacePane,
|
|
5
5
|
WorkspaceSplit,
|
|
6
|
+
WorkspaceBackground,
|
|
6
7
|
TabbyProfile,
|
|
7
8
|
isWorkspaceSplit,
|
|
8
9
|
createDefaultPane,
|
|
9
10
|
generateUUID,
|
|
11
|
+
BACKGROUND_PRESETS,
|
|
10
12
|
} from '../models/workspace.model'
|
|
13
|
+
|
|
14
|
+
interface TreeContext {
|
|
15
|
+
node: WorkspaceSplit
|
|
16
|
+
index: number
|
|
17
|
+
parent: WorkspaceSplit | null
|
|
18
|
+
child: WorkspacePane | WorkspaceSplit
|
|
19
|
+
}
|
|
11
20
|
import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
12
21
|
|
|
13
22
|
@Component({
|
|
@@ -15,12 +24,17 @@ import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
|
15
24
|
template: require('./workspaceEditor.component.pug'),
|
|
16
25
|
styles: [require('./workspaceEditor.component.scss')],
|
|
17
26
|
})
|
|
18
|
-
export class WorkspaceEditorComponent implements OnInit {
|
|
27
|
+
export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewInit {
|
|
19
28
|
@Input() workspace!: Workspace
|
|
29
|
+
@Input() autoFocus = false
|
|
30
|
+
@Input() hasUnsavedChanges = false
|
|
20
31
|
@Output() save = new EventEmitter<Workspace>()
|
|
21
32
|
@Output() cancel = new EventEmitter<void>()
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
@ViewChild('nameInput') nameInput!: ElementRef<HTMLInputElement>
|
|
35
|
+
|
|
36
|
+
selectedPaneId: string | null = null
|
|
37
|
+
editingPane: WorkspacePane | null = null
|
|
24
38
|
showPaneEditor = false
|
|
25
39
|
profiles: TabbyProfile[] = []
|
|
26
40
|
availableIcons = [
|
|
@@ -28,11 +42,141 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
28
42
|
'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
|
|
29
43
|
'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
|
|
30
44
|
]
|
|
45
|
+
iconDropdownOpen = false
|
|
46
|
+
backgroundPresets = BACKGROUND_PRESETS
|
|
47
|
+
backgroundDropdownOpen = false
|
|
48
|
+
customBackgroundValue = ''
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private workspaceService: WorkspaceEditorService,
|
|
52
|
+
private elementRef: ElementRef,
|
|
53
|
+
private cdr: ChangeDetectorRef
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
@HostListener('document:click', ['$event'])
|
|
57
|
+
onDocumentClick(event: MouseEvent): void {
|
|
58
|
+
// Check if click is outside the icon dropdown area (trigger + dropdown)
|
|
59
|
+
const dropdownTrigger = this.elementRef.nativeElement.querySelector('.dropdown-trigger')
|
|
60
|
+
const iconDropdown = this.elementRef.nativeElement.querySelector('.icon-dropdown')
|
|
61
|
+
const iconClickedInside = dropdownTrigger?.contains(event.target as Node) ||
|
|
62
|
+
iconDropdown?.contains(event.target as Node)
|
|
63
|
+
if (this.iconDropdownOpen && !iconClickedInside) {
|
|
64
|
+
this.iconDropdownOpen = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if click is outside the background dropdown area
|
|
68
|
+
const bgTrigger = this.elementRef.nativeElement.querySelector('.background-trigger')
|
|
69
|
+
const bgDropdown = this.elementRef.nativeElement.querySelector('.background-dropdown')
|
|
70
|
+
const bgClickedInside = bgTrigger?.contains(event.target as Node) ||
|
|
71
|
+
bgDropdown?.contains(event.target as Node)
|
|
72
|
+
if (this.backgroundDropdownOpen && !bgClickedInside) {
|
|
73
|
+
this.backgroundDropdownOpen = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toggleIconDropdown(): void {
|
|
78
|
+
this.iconDropdownOpen = !this.iconDropdownOpen
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
selectIcon(icon: string): void {
|
|
82
|
+
this.workspace.icon = icon
|
|
83
|
+
this.iconDropdownOpen = false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toggleBackgroundDropdown(): void {
|
|
87
|
+
this.backgroundDropdownOpen = !this.backgroundDropdownOpen
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
selectBackgroundPreset(preset: WorkspaceBackground): void {
|
|
91
|
+
if (preset.type === 'none') {
|
|
92
|
+
this.workspace.background = undefined
|
|
93
|
+
this.customBackgroundValue = ''
|
|
94
|
+
} else {
|
|
95
|
+
this.workspace.background = { ...preset }
|
|
96
|
+
this.customBackgroundValue = preset.value
|
|
97
|
+
}
|
|
98
|
+
this.backgroundDropdownOpen = false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
applyCustomBackground(): void {
|
|
102
|
+
const value = this.customBackgroundValue.trim()
|
|
103
|
+
if (value) {
|
|
104
|
+
this.workspace.background = {
|
|
105
|
+
type: 'gradient',
|
|
106
|
+
value
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this.workspace.background = undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clearBackground(): void {
|
|
114
|
+
this.workspace.background = undefined
|
|
115
|
+
this.customBackgroundValue = ''
|
|
116
|
+
}
|
|
31
117
|
|
|
32
|
-
|
|
118
|
+
isBackgroundSelected(preset: WorkspaceBackground): boolean {
|
|
119
|
+
if (preset.type === 'none') {
|
|
120
|
+
return !this.workspace.background || this.workspace.background.type === 'none'
|
|
121
|
+
}
|
|
122
|
+
return this.workspace.background?.value === preset.value
|
|
123
|
+
}
|
|
33
124
|
|
|
34
125
|
async ngOnInit(): Promise<void> {
|
|
35
126
|
this.profiles = await this.workspaceService.getAvailableProfiles()
|
|
127
|
+
this.initializeWorkspace()
|
|
128
|
+
this.cdr.detectChanges()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ngAfterViewInit(): void {
|
|
132
|
+
if (this.autoFocus) {
|
|
133
|
+
this.focusNameInput()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private focusNameInput(): void {
|
|
138
|
+
requestAnimationFrame(() => {
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
if (this.nameInput?.nativeElement) {
|
|
141
|
+
this.nameInput.nativeElement.focus()
|
|
142
|
+
this.nameInput.nativeElement.select()
|
|
143
|
+
}
|
|
144
|
+
}, 0)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
149
|
+
if (changes['workspace'] && !changes['workspace'].firstChange) {
|
|
150
|
+
const prevId = changes['workspace'].previousValue?.id
|
|
151
|
+
const currId = this.workspace.id
|
|
152
|
+
|
|
153
|
+
if (prevId !== currId) {
|
|
154
|
+
// Different workspace - reset everything and focus name input
|
|
155
|
+
this.selectedPaneId = null
|
|
156
|
+
this.editingPane = null
|
|
157
|
+
this.showPaneEditor = false
|
|
158
|
+
this.focusNameInput()
|
|
159
|
+
} else {
|
|
160
|
+
// Same workspace ID but different reference (after save/reload)
|
|
161
|
+
// Re-sync editingPane to point to pane in new object tree
|
|
162
|
+
if (this.selectedPaneId && this.showPaneEditor) {
|
|
163
|
+
this.editingPane = this.findPaneById(this.selectedPaneId)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Always reset dropdowns and sync background value
|
|
167
|
+
this.iconDropdownOpen = false
|
|
168
|
+
this.backgroundDropdownOpen = false
|
|
169
|
+
this.customBackgroundValue = this.workspace.background?.value || ''
|
|
170
|
+
this.initializeWorkspace()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle autoFocus change
|
|
174
|
+
if (changes['autoFocus']?.currentValue) {
|
|
175
|
+
this.focusNameInput()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private initializeWorkspace(): void {
|
|
36
180
|
if (!this.workspace.root) {
|
|
37
181
|
this.workspace.root = {
|
|
38
182
|
orientation: 'horizontal',
|
|
@@ -53,69 +197,127 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
53
197
|
this.cancel.emit()
|
|
54
198
|
}
|
|
55
199
|
|
|
56
|
-
|
|
57
|
-
this.
|
|
200
|
+
deselectPane(): void {
|
|
201
|
+
this.selectedPaneId = null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
onPreviewBackgroundClick(): void {
|
|
205
|
+
this.deselectPane()
|
|
206
|
+
this.closePaneEditor()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
editPane(pane: WorkspacePane): void {
|
|
210
|
+
this.selectedPaneId = pane.id
|
|
211
|
+
this.editingPane = pane
|
|
58
212
|
this.showPaneEditor = true
|
|
213
|
+
this.cdr.detectChanges()
|
|
59
214
|
}
|
|
60
215
|
|
|
61
216
|
closePaneEditor(): void {
|
|
62
217
|
this.showPaneEditor = false
|
|
63
|
-
this.
|
|
218
|
+
this.editingPane = null
|
|
219
|
+
this.cdr.detectChanges()
|
|
64
220
|
}
|
|
65
221
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.
|
|
222
|
+
// Helper functions
|
|
223
|
+
private findPaneById(id: string): WorkspacePane | null {
|
|
224
|
+
return this.findPaneInNode(this.workspace.root, id)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private findPaneInNode(node: WorkspaceSplit, id: string): WorkspacePane | null {
|
|
228
|
+
for (const child of node.children) {
|
|
229
|
+
if (isWorkspaceSplit(child)) {
|
|
230
|
+
const found = this.findPaneInNode(child, id)
|
|
231
|
+
if (found) return found
|
|
232
|
+
} else if (child.id === id) {
|
|
233
|
+
return child
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null
|
|
69
237
|
}
|
|
70
238
|
|
|
71
|
-
|
|
239
|
+
canRemovePane(): boolean {
|
|
240
|
+
return this.countPanes(this.workspace.root) > 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private countPanes(node: WorkspaceSplit): number {
|
|
244
|
+
return node.children.reduce((count, child) => {
|
|
245
|
+
return count + (isWorkspaceSplit(child) ? this.countPanes(child) : 1)
|
|
246
|
+
}, 0)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private walkTree(
|
|
250
|
+
node: WorkspaceSplit,
|
|
251
|
+
visitor: (ctx: TreeContext) => boolean,
|
|
252
|
+
parent: WorkspaceSplit | null = null
|
|
253
|
+
): boolean {
|
|
72
254
|
for (let i = 0; i < node.children.length; i++) {
|
|
73
255
|
const child = node.children[i]
|
|
256
|
+
const ctx: TreeContext = { node, index: i, parent, child }
|
|
257
|
+
|
|
74
258
|
if (isWorkspaceSplit(child)) {
|
|
75
|
-
if (this.
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
} else if (child.id === updatedPane.id) {
|
|
79
|
-
node.children[i] = updatedPane
|
|
259
|
+
if (this.walkTree(child, visitor, node)) return true
|
|
260
|
+
} else if (visitor(ctx)) {
|
|
80
261
|
return true
|
|
81
262
|
}
|
|
82
263
|
}
|
|
83
264
|
return false
|
|
84
265
|
}
|
|
85
266
|
|
|
267
|
+
private updatePaneInTree(updatedPane: WorkspacePane): boolean {
|
|
268
|
+
return this.walkTree(this.workspace.root, (ctx) => {
|
|
269
|
+
if ((ctx.child as WorkspacePane).id === updatedPane.id) {
|
|
270
|
+
ctx.node.children[ctx.index] = updatedPane
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
return false
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
86
277
|
splitPane(pane: WorkspacePane, orientation: 'horizontal' | 'vertical'): void {
|
|
87
|
-
this.splitPaneInTree(
|
|
278
|
+
this.splitPaneInTree(pane, orientation)
|
|
279
|
+
this.cdr.detectChanges()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
splitSelectedPane(orientation: 'horizontal' | 'vertical'): void {
|
|
283
|
+
if (!this.selectedPaneId) return
|
|
284
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
285
|
+
if (pane) this.splitPane(pane, orientation)
|
|
88
286
|
}
|
|
89
287
|
|
|
90
288
|
private splitPaneInTree(
|
|
91
|
-
node: WorkspaceSplit,
|
|
92
289
|
targetPane: WorkspacePane,
|
|
93
290
|
orientation: 'horizontal' | 'vertical'
|
|
94
291
|
): boolean {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (isWorkspaceSplit(child)) {
|
|
98
|
-
if (this.splitPaneInTree(child, targetPane, orientation)) {
|
|
99
|
-
return true
|
|
100
|
-
}
|
|
101
|
-
} else if (child.id === targetPane.id) {
|
|
292
|
+
return this.walkTree(this.workspace.root, (ctx) => {
|
|
293
|
+
if ((ctx.child as WorkspacePane).id === targetPane.id) {
|
|
102
294
|
const newPane = createDefaultPane()
|
|
103
|
-
newPane.profileId = child.profileId
|
|
295
|
+
newPane.profileId = (ctx.child as WorkspacePane).profileId
|
|
104
296
|
const newSplit: WorkspaceSplit = {
|
|
105
297
|
orientation,
|
|
106
298
|
ratios: [0.5, 0.5],
|
|
107
|
-
children: [child, newPane],
|
|
299
|
+
children: [ctx.child, newPane],
|
|
108
300
|
}
|
|
109
|
-
node.children[
|
|
110
|
-
this.recalculateRatios(node)
|
|
301
|
+
ctx.node.children[ctx.index] = newSplit
|
|
302
|
+
this.recalculateRatios(ctx.node)
|
|
111
303
|
return true
|
|
112
304
|
}
|
|
113
|
-
|
|
114
|
-
|
|
305
|
+
return false
|
|
306
|
+
})
|
|
115
307
|
}
|
|
116
308
|
|
|
117
309
|
removePane(pane: WorkspacePane): void {
|
|
310
|
+
if (this.selectedPaneId === pane.id) {
|
|
311
|
+
this.selectedPaneId = null
|
|
312
|
+
}
|
|
118
313
|
this.removePaneFromTree(this.workspace.root, pane)
|
|
314
|
+
this.cdr.detectChanges()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
removeSelectedPane(): void {
|
|
318
|
+
if (!this.selectedPaneId || !this.canRemovePane()) return
|
|
319
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
320
|
+
if (pane) this.removePane(pane)
|
|
119
321
|
}
|
|
120
322
|
|
|
121
323
|
private removePaneFromTree(node: WorkspaceSplit, targetPane: WorkspacePane): boolean {
|
|
@@ -156,6 +358,7 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
156
358
|
|
|
157
359
|
setOrientation(orientation: 'horizontal' | 'vertical'): void {
|
|
158
360
|
this.workspace.root.orientation = orientation
|
|
361
|
+
this.cdr.detectChanges()
|
|
159
362
|
}
|
|
160
363
|
|
|
161
364
|
updateRatio(index: number, value: number): void {
|
|
@@ -176,6 +379,66 @@ export class WorkspaceEditorComponent implements OnInit {
|
|
|
176
379
|
})
|
|
177
380
|
|
|
178
381
|
this.workspace.root.ratios = ratios
|
|
382
|
+
this.cdr.detectChanges()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add pane operations
|
|
386
|
+
addPane(direction: 'left' | 'right' | 'top' | 'bottom'): void {
|
|
387
|
+
if (!this.selectedPaneId) return
|
|
388
|
+
const pane = this.findPaneById(this.selectedPaneId)
|
|
389
|
+
if (!pane) return
|
|
390
|
+
this.addPaneInTree(pane, direction)
|
|
391
|
+
this.cdr.detectChanges()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
addPaneFromEvent(pane: WorkspacePane, direction: 'left' | 'right' | 'top' | 'bottom'): void {
|
|
395
|
+
this.addPaneInTree(pane, direction)
|
|
396
|
+
this.cdr.detectChanges()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private addPaneInTree(
|
|
400
|
+
targetPane: WorkspacePane,
|
|
401
|
+
direction: 'left' | 'right' | 'top' | 'bottom'
|
|
402
|
+
): boolean {
|
|
403
|
+
const isHorizontalAdd = direction === 'left' || direction === 'right'
|
|
404
|
+
const isBefore = direction === 'left' || direction === 'top'
|
|
405
|
+
const targetOrientation = isHorizontalAdd ? 'horizontal' : 'vertical'
|
|
406
|
+
|
|
407
|
+
return this.walkTree(this.workspace.root, (ctx) => {
|
|
408
|
+
if ((ctx.child as WorkspacePane).id !== targetPane.id) return false
|
|
409
|
+
|
|
410
|
+
const newPane = createDefaultPane()
|
|
411
|
+
newPane.profileId = (ctx.child as WorkspacePane).profileId
|
|
412
|
+
|
|
413
|
+
if (ctx.node.orientation === targetOrientation) {
|
|
414
|
+
// Same orientation: add as sibling
|
|
415
|
+
const insertIndex = isBefore ? ctx.index : ctx.index + 1
|
|
416
|
+
ctx.node.children.splice(insertIndex, 0, newPane)
|
|
417
|
+
this.recalculateRatios(ctx.node)
|
|
418
|
+
} else {
|
|
419
|
+
// Different orientation: wrap entire node in new split
|
|
420
|
+
const nodeCopy: WorkspaceSplit = {
|
|
421
|
+
orientation: ctx.node.orientation,
|
|
422
|
+
ratios: [...ctx.node.ratios],
|
|
423
|
+
children: [...ctx.node.children]
|
|
424
|
+
}
|
|
425
|
+
const wrapper: WorkspaceSplit = {
|
|
426
|
+
orientation: targetOrientation,
|
|
427
|
+
ratios: [0.5, 0.5],
|
|
428
|
+
children: isBefore ? [newPane, nodeCopy] : [nodeCopy, newPane]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (ctx.node === this.workspace.root) {
|
|
432
|
+
this.workspace.root = wrapper
|
|
433
|
+
} else if (ctx.parent) {
|
|
434
|
+
const nodeIndex = ctx.parent.children.indexOf(ctx.node)
|
|
435
|
+
if (nodeIndex !== -1) {
|
|
436
|
+
ctx.parent.children[nodeIndex] = wrapper
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return true
|
|
441
|
+
})
|
|
179
442
|
}
|
|
180
443
|
|
|
181
444
|
}
|
|
@@ -1,46 +1,37 @@
|
|
|
1
1
|
.workspace-list-container
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
//- Tab bar navigation
|
|
3
|
+
.tab-bar
|
|
4
|
+
.tab(
|
|
5
|
+
*ngFor='let tab of displayTabs; trackBy: trackByTab',
|
|
6
|
+
[class.active]='isTabSelected(tab)',
|
|
7
|
+
(click)='!tab.isNew && selectWorkspace(tab.workspace)'
|
|
8
|
+
)
|
|
9
|
+
span.tab-icon([style.color]='tab.workspace.color')
|
|
10
|
+
i.fas([class]='"fa-" + (tab.workspace.icon || "columns")')
|
|
11
|
+
span.tab-name {{ tab.workspace.name || 'New Workspace' }}
|
|
12
|
+
span.tab-close(
|
|
13
|
+
*ngIf='!tab.isNew',
|
|
14
|
+
(click)='deleteWorkspace($event, tab.workspace)',
|
|
15
|
+
title='Delete workspace'
|
|
16
|
+
)
|
|
17
|
+
i.fas.fa-xmark
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.workspace-info
|
|
11
|
-
.workspace-icon([style.color]='workspace.color')
|
|
12
|
-
i.fas([class]='"fa-" + (workspace.icon || "columns")')
|
|
13
|
-
.workspace-details
|
|
14
|
-
.workspace-name {{ workspace.name }}
|
|
15
|
-
.workspace-meta
|
|
16
|
-
span {{ getPaneCount(workspace) }} panes
|
|
17
|
-
span.separator ·
|
|
18
|
-
span {{ getOrientationLabel(workspace) }}
|
|
19
|
-
span.separator(*ngIf='workspace.isDefault') ·
|
|
20
|
-
span.badge.badge-primary(*ngIf='workspace.isDefault') default
|
|
19
|
+
.tab-new((click)='createWorkspace()', title='New workspace')
|
|
20
|
+
i.fas.fa-plus
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
i.fas.fa-copy
|
|
32
|
-
button.btn.btn-link(type='button', title='Edit', (click)='editWorkspace(workspace)')
|
|
33
|
-
i.fas.fa-edit
|
|
34
|
-
button.btn.btn-link.text-danger(type='button', title='Delete', (click)='deleteWorkspace(workspace)')
|
|
35
|
-
i.fas.fa-trash
|
|
22
|
+
//- Tab content (editor)
|
|
23
|
+
.tab-content(*ngIf='editingWorkspace')
|
|
24
|
+
workspace-editor(
|
|
25
|
+
[workspace]='editingWorkspace',
|
|
26
|
+
[autoFocus]='isCreatingNew',
|
|
27
|
+
[hasUnsavedChanges]='hasUnsavedChanges',
|
|
28
|
+
(save)='onEditorSave($event)',
|
|
29
|
+
(cancel)='onEditorCancel()'
|
|
30
|
+
)
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
//- Empty state (when no workspaces)
|
|
33
|
+
.tab-content.empty-state(*ngIf='!editingWorkspace && workspaces.length === 0')
|
|
38
34
|
p No workspaces configured yet.
|
|
39
|
-
p Click
|
|
40
|
-
|
|
41
|
-
workspace
|
|
42
|
-
*ngIf='showEditor',
|
|
43
|
-
[workspace]='editingWorkspace',
|
|
44
|
-
(save)='onEditorSave($event)',
|
|
45
|
-
(cancel)='closeEditor()'
|
|
46
|
-
)
|
|
35
|
+
p Click
|
|
36
|
+
strong +
|
|
37
|
+
| to create your first workspace.
|
|
@@ -1,112 +1,127 @@
|
|
|
1
|
+
@use '../styles/index' as *;
|
|
2
|
+
|
|
1
3
|
.workspace-list-container {
|
|
2
|
-
padding:
|
|
3
|
-
max-width: 800px;
|
|
4
|
+
padding: $spacing-2xl;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Tab bar
|
|
8
|
+
.tab-bar {
|
|
7
9
|
display: flex;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
background: var(--theme-bg-more);
|
|
11
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
12
|
+
border-bottom: none;
|
|
13
|
+
overflow-y: hidden;
|
|
14
|
+
overflow-x: auto;
|
|
15
|
+
|
|
16
|
+
// Thin scrollbar
|
|
17
|
+
&::-webkit-scrollbar {
|
|
18
|
+
height: 6px;
|
|
19
|
+
}
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
font-size: 1.5rem;
|
|
21
|
+
&::-webkit-scrollbar-track {
|
|
22
|
+
background: var(--theme-bg-more);
|
|
15
23
|
}
|
|
16
|
-
}
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
&::-webkit-scrollbar-thumb {
|
|
26
|
+
background: var(--theme-border, $fallback-border);
|
|
27
|
+
border-radius: 3px;
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
background: var(--theme-fg-more);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
.
|
|
35
|
+
.tab {
|
|
25
36
|
display: flex;
|
|
26
|
-
justify-content: space-between;
|
|
27
37
|
align-items: center;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
border-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
gap: $spacing-md;
|
|
39
|
+
padding: $spacing-md $spacing-lg;
|
|
40
|
+
border-right: 1px solid var(--theme-border, $fallback-border);
|
|
41
|
+
color: var(--theme-fg-more);
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
font-size: $font-sm;
|
|
44
|
+
min-width: 120px;
|
|
45
|
+
transition: background $transition-fast;
|
|
33
46
|
|
|
34
47
|
&:hover {
|
|
35
48
|
background: var(--theme-bg-more-more);
|
|
49
|
+
|
|
50
|
+
.tab-close {
|
|
51
|
+
opacity: 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&.active {
|
|
56
|
+
background: var(--theme-bg);
|
|
57
|
+
color: var(--theme-fg);
|
|
58
|
+
border-bottom: 2px solid var(--theme-primary);
|
|
59
|
+
margin-bottom: -1px;
|
|
36
60
|
}
|
|
37
61
|
}
|
|
38
62
|
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
align-items: center;
|
|
42
|
-
gap: 16px;
|
|
63
|
+
.tab-icon {
|
|
64
|
+
font-size: 12px;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
.tab-name {
|
|
68
|
+
flex: 1;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
49
72
|
}
|
|
50
73
|
|
|
51
|
-
.
|
|
74
|
+
.tab-close {
|
|
75
|
+
width: 16px;
|
|
76
|
+
height: 16px;
|
|
52
77
|
display: flex;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
opacity: 0;
|
|
81
|
+
font-size: 9px;
|
|
82
|
+
border-radius: $radius-xs;
|
|
83
|
+
transition: opacity $transition-fast, background $transition-fast;
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
&:hover {
|
|
86
|
+
background: var(--theme-danger);
|
|
87
|
+
color: white;
|
|
88
|
+
}
|
|
60
89
|
}
|
|
61
90
|
|
|
62
|
-
.
|
|
63
|
-
|
|
91
|
+
.tab-new {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
width: 36px;
|
|
64
96
|
color: var(--theme-fg-more);
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
transition: color $transition-fast, background $transition-fast;
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.badge {
|
|
71
|
-
font-size: 0.75rem;
|
|
72
|
-
padding: 2px 6px;
|
|
73
|
-
border-radius: 4px;
|
|
74
|
-
background: var(--theme-primary);
|
|
75
|
-
color: white;
|
|
100
|
+
&:hover {
|
|
101
|
+
background: var(--theme-bg-more-more);
|
|
102
|
+
color: var(--theme-primary);
|
|
76
103
|
}
|
|
77
104
|
}
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
// Tab content
|
|
107
|
+
.tab-content {
|
|
108
|
+
background: var(--theme-bg);
|
|
109
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
110
|
+
border-top: none;
|
|
111
|
+
padding: $spacing-xl;
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
&.empty-state {
|
|
114
|
+
text-align: center;
|
|
115
|
+
padding: $spacing-2xl;
|
|
85
116
|
color: var(--theme-fg-more);
|
|
86
|
-
opacity: 0.7;
|
|
87
|
-
transition: opacity 0.2s;
|
|
88
117
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
&.active {
|
|
94
|
-
color: gold;
|
|
95
|
-
opacity: 1;
|
|
118
|
+
p {
|
|
119
|
+
margin: $spacing-md 0;
|
|
96
120
|
}
|
|
97
121
|
|
|
98
|
-
|
|
99
|
-
color: var(--theme-
|
|
122
|
+
strong {
|
|
123
|
+
color: var(--theme-primary);
|
|
124
|
+
padding: 0 $spacing-xs;
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
}
|
|
103
|
-
|
|
104
|
-
.workspace-empty {
|
|
105
|
-
text-align: center;
|
|
106
|
-
padding: 40px;
|
|
107
|
-
color: var(--theme-fg-more);
|
|
108
|
-
|
|
109
|
-
p {
|
|
110
|
-
margin: 8px 0;
|
|
111
|
-
}
|
|
112
|
-
}
|