tabby-tabbyspaces 0.1.0 → 0.2.1
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 +2 -1
- 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 +46 -0
- package/CLAUDE.md +33 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +21 -18
- package/TODO.md +5 -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 -18
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +50 -50
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +61 -54
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +56 -39
- 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 -78
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +1 -1
- package/dist/providers/config.provider.d.ts +8 -8
- package/dist/providers/settings.provider.d.ts +7 -7
- package/dist/providers/toolbar.provider.d.ts +23 -15
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +27 -19
- package/dist/services/startupCommand.service.d.ts.map +1 -1
- 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 -32
- 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/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 +2 -1
- package/screenshots/editor.png +0 -0
- package/scripts/build-dev.js +2 -1
- package/scripts/build-prod.js +2 -1
- 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 +4 -32
- package/src/components/splitPreview.component.pug +0 -9
- package/src/components/splitPreview.component.scss +46 -70
- package/src/components/splitPreview.component.ts +15 -25
- package/src/components/workspaceEditor.component.pug +140 -112
- package/src/components/workspaceEditor.component.scss +270 -202
- package/src/components/workspaceEditor.component.ts +161 -85
- package/src/components/workspaceList.component.pug +31 -51
- package/src/components/workspaceList.component.scss +86 -77
- package/src/components/workspaceList.component.ts +89 -34
- package/src/index.ts +4 -0
- package/src/models/workspace.model.ts +80 -2
- package/src/providers/toolbar.provider.ts +78 -9
- package/src/services/startupCommand.service.ts +30 -32
- package/src/services/workspaceBackground.service.ts +167 -0
- package/src/services/workspaceEditor.service.ts +77 -40
- package/src/styles/_index.scss +3 -0
- package/src/styles/_mixins.scss +180 -0
- package/src/styles/_variables.scss +67 -0
- package/TEST_MCP.md +0 -176
- package/cdp-click.js +0 -22
- package/cdp-test.js +0 -28
- package/screenshots/pane-edit.png +0 -0
- package/test_cdp.py +0 -50
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, SimpleChanges, HostListener, ElementRef, ViewChild } 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({
|
|
@@ -34,18 +43,35 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
34
43
|
'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
|
|
35
44
|
]
|
|
36
45
|
iconDropdownOpen = false
|
|
46
|
+
backgroundPresets = BACKGROUND_PRESETS
|
|
47
|
+
backgroundDropdownOpen = false
|
|
48
|
+
customBackgroundValue = ''
|
|
37
49
|
|
|
38
50
|
constructor(
|
|
39
51
|
private workspaceService: WorkspaceEditorService,
|
|
40
|
-
private elementRef: ElementRef
|
|
52
|
+
private elementRef: ElementRef,
|
|
53
|
+
private cdr: ChangeDetectorRef
|
|
41
54
|
) {}
|
|
42
55
|
|
|
43
56
|
@HostListener('document:click', ['$event'])
|
|
44
57
|
onDocumentClick(event: MouseEvent): void {
|
|
45
|
-
|
|
46
|
-
|
|
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) {
|
|
47
64
|
this.iconDropdownOpen = false
|
|
48
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
|
+
}
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
toggleIconDropdown(): void {
|
|
@@ -57,9 +83,49 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
57
83
|
this.iconDropdownOpen = false
|
|
58
84
|
}
|
|
59
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
|
+
}
|
|
117
|
+
|
|
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
|
+
}
|
|
124
|
+
|
|
60
125
|
async ngOnInit(): Promise<void> {
|
|
61
126
|
this.profiles = await this.workspaceService.getAvailableProfiles()
|
|
62
127
|
this.initializeWorkspace()
|
|
128
|
+
this.cdr.detectChanges()
|
|
63
129
|
}
|
|
64
130
|
|
|
65
131
|
ngAfterViewInit(): void {
|
|
@@ -81,11 +147,26 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
81
147
|
|
|
82
148
|
ngOnChanges(changes: SimpleChanges): void {
|
|
83
149
|
if (changes['workspace'] && !changes['workspace'].firstChange) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
88
167
|
this.iconDropdownOpen = false
|
|
168
|
+
this.backgroundDropdownOpen = false
|
|
169
|
+
this.customBackgroundValue = this.workspace.background?.value || ''
|
|
89
170
|
this.initializeWorkspace()
|
|
90
171
|
}
|
|
91
172
|
|
|
@@ -116,37 +197,26 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
116
197
|
this.cancel.emit()
|
|
117
198
|
}
|
|
118
199
|
|
|
119
|
-
selectPane(pane: WorkspacePane): void {
|
|
120
|
-
this.selectedPaneId = pane.id
|
|
121
|
-
}
|
|
122
|
-
|
|
123
200
|
deselectPane(): void {
|
|
124
201
|
this.selectedPaneId = null
|
|
125
202
|
}
|
|
126
203
|
|
|
127
204
|
onPreviewBackgroundClick(): void {
|
|
128
205
|
this.deselectPane()
|
|
206
|
+
this.closePaneEditor()
|
|
129
207
|
}
|
|
130
208
|
|
|
131
209
|
editPane(pane: WorkspacePane): void {
|
|
210
|
+
this.selectedPaneId = pane.id
|
|
132
211
|
this.editingPane = pane
|
|
133
212
|
this.showPaneEditor = true
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
editSelectedPane(): void {
|
|
137
|
-
if (!this.selectedPaneId) return
|
|
138
|
-
const pane = this.findPaneById(this.selectedPaneId)
|
|
139
|
-
if (pane) this.editPane(pane)
|
|
213
|
+
this.cdr.detectChanges()
|
|
140
214
|
}
|
|
141
215
|
|
|
142
216
|
closePaneEditor(): void {
|
|
143
217
|
this.showPaneEditor = false
|
|
144
218
|
this.editingPane = null
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
onPaneSave(pane: WorkspacePane): void {
|
|
148
|
-
this.updatePaneInTree(this.workspace.root, pane)
|
|
149
|
-
this.closePaneEditor()
|
|
219
|
+
this.cdr.detectChanges()
|
|
150
220
|
}
|
|
151
221
|
|
|
152
222
|
// Helper functions
|
|
@@ -176,23 +246,37 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
176
246
|
}, 0)
|
|
177
247
|
}
|
|
178
248
|
|
|
179
|
-
private
|
|
249
|
+
private walkTree(
|
|
250
|
+
node: WorkspaceSplit,
|
|
251
|
+
visitor: (ctx: TreeContext) => boolean,
|
|
252
|
+
parent: WorkspaceSplit | null = null
|
|
253
|
+
): boolean {
|
|
180
254
|
for (let i = 0; i < node.children.length; i++) {
|
|
181
255
|
const child = node.children[i]
|
|
256
|
+
const ctx: TreeContext = { node, index: i, parent, child }
|
|
257
|
+
|
|
182
258
|
if (isWorkspaceSplit(child)) {
|
|
183
|
-
if (this.
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
} else if (child.id === updatedPane.id) {
|
|
187
|
-
node.children[i] = updatedPane
|
|
259
|
+
if (this.walkTree(child, visitor, node)) return true
|
|
260
|
+
} else if (visitor(ctx)) {
|
|
188
261
|
return true
|
|
189
262
|
}
|
|
190
263
|
}
|
|
191
264
|
return false
|
|
192
265
|
}
|
|
193
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
|
+
|
|
194
277
|
splitPane(pane: WorkspacePane, orientation: 'horizontal' | 'vertical'): void {
|
|
195
|
-
this.splitPaneInTree(
|
|
278
|
+
this.splitPaneInTree(pane, orientation)
|
|
279
|
+
this.cdr.detectChanges()
|
|
196
280
|
}
|
|
197
281
|
|
|
198
282
|
splitSelectedPane(orientation: 'horizontal' | 'vertical'): void {
|
|
@@ -202,30 +286,24 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
202
286
|
}
|
|
203
287
|
|
|
204
288
|
private splitPaneInTree(
|
|
205
|
-
node: WorkspaceSplit,
|
|
206
289
|
targetPane: WorkspacePane,
|
|
207
290
|
orientation: 'horizontal' | 'vertical'
|
|
208
291
|
): boolean {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (isWorkspaceSplit(child)) {
|
|
212
|
-
if (this.splitPaneInTree(child, targetPane, orientation)) {
|
|
213
|
-
return true
|
|
214
|
-
}
|
|
215
|
-
} else if (child.id === targetPane.id) {
|
|
292
|
+
return this.walkTree(this.workspace.root, (ctx) => {
|
|
293
|
+
if ((ctx.child as WorkspacePane).id === targetPane.id) {
|
|
216
294
|
const newPane = createDefaultPane()
|
|
217
|
-
newPane.profileId = child.profileId
|
|
295
|
+
newPane.profileId = (ctx.child as WorkspacePane).profileId
|
|
218
296
|
const newSplit: WorkspaceSplit = {
|
|
219
297
|
orientation,
|
|
220
298
|
ratios: [0.5, 0.5],
|
|
221
|
-
children: [child, newPane],
|
|
299
|
+
children: [ctx.child, newPane],
|
|
222
300
|
}
|
|
223
|
-
node.children[
|
|
224
|
-
this.recalculateRatios(node)
|
|
301
|
+
ctx.node.children[ctx.index] = newSplit
|
|
302
|
+
this.recalculateRatios(ctx.node)
|
|
225
303
|
return true
|
|
226
304
|
}
|
|
227
|
-
|
|
228
|
-
|
|
305
|
+
return false
|
|
306
|
+
})
|
|
229
307
|
}
|
|
230
308
|
|
|
231
309
|
removePane(pane: WorkspacePane): void {
|
|
@@ -233,6 +311,7 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
233
311
|
this.selectedPaneId = null
|
|
234
312
|
}
|
|
235
313
|
this.removePaneFromTree(this.workspace.root, pane)
|
|
314
|
+
this.cdr.detectChanges()
|
|
236
315
|
}
|
|
237
316
|
|
|
238
317
|
removeSelectedPane(): void {
|
|
@@ -279,6 +358,7 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
279
358
|
|
|
280
359
|
setOrientation(orientation: 'horizontal' | 'vertical'): void {
|
|
281
360
|
this.workspace.root.orientation = orientation
|
|
361
|
+
this.cdr.detectChanges()
|
|
282
362
|
}
|
|
283
363
|
|
|
284
364
|
updateRatio(index: number, value: number): void {
|
|
@@ -299,6 +379,7 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
299
379
|
})
|
|
300
380
|
|
|
301
381
|
this.workspace.root.ratios = ratios
|
|
382
|
+
this.cdr.detectChanges()
|
|
302
383
|
}
|
|
303
384
|
|
|
304
385
|
// Add pane operations
|
|
@@ -306,63 +387,58 @@ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewIni
|
|
|
306
387
|
if (!this.selectedPaneId) return
|
|
307
388
|
const pane = this.findPaneById(this.selectedPaneId)
|
|
308
389
|
if (!pane) return
|
|
309
|
-
this.addPaneInTree(
|
|
390
|
+
this.addPaneInTree(pane, direction)
|
|
391
|
+
this.cdr.detectChanges()
|
|
310
392
|
}
|
|
311
393
|
|
|
312
394
|
addPaneFromEvent(pane: WorkspacePane, direction: 'left' | 'right' | 'top' | 'bottom'): void {
|
|
313
|
-
this.addPaneInTree(
|
|
395
|
+
this.addPaneInTree(pane, direction)
|
|
396
|
+
this.cdr.detectChanges()
|
|
314
397
|
}
|
|
315
398
|
|
|
316
399
|
private addPaneInTree(
|
|
317
|
-
node: WorkspaceSplit,
|
|
318
400
|
targetPane: WorkspacePane,
|
|
319
|
-
direction: 'left' | 'right' | 'top' | 'bottom'
|
|
320
|
-
parentNode: WorkspaceSplit | null
|
|
401
|
+
direction: 'left' | 'right' | 'top' | 'bottom'
|
|
321
402
|
): boolean {
|
|
322
403
|
const isHorizontalAdd = direction === 'left' || direction === 'right'
|
|
323
404
|
const isBefore = direction === 'left' || direction === 'top'
|
|
324
405
|
const targetOrientation = isHorizontalAdd ? 'horizontal' : 'vertical'
|
|
325
406
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
ratios: [0.5, 0.5],
|
|
350
|
-
children: isBefore ? [newPane, nodeCopy] : [nodeCopy, newPane]
|
|
351
|
-
}
|
|
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
|
+
}
|
|
352
430
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
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
|
|
360
437
|
}
|
|
361
438
|
}
|
|
362
|
-
return true
|
|
363
439
|
}
|
|
364
|
-
|
|
365
|
-
|
|
440
|
+
return true
|
|
441
|
+
})
|
|
366
442
|
}
|
|
367
443
|
|
|
368
444
|
}
|
|
@@ -1,57 +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
|
-
*ngIf='editingWorkspace',
|
|
11
|
-
[workspace]='editingWorkspace',
|
|
12
|
-
[autoFocus]='isCreatingNew',
|
|
13
|
-
[hasUnsavedChanges]='hasUnsavedChanges',
|
|
14
|
-
(save)='onEditorSave($event)',
|
|
15
|
-
(cancel)='onEditorCancel()'
|
|
16
|
-
)
|
|
19
|
+
.tab-new((click)='createWorkspace()', title='New workspace')
|
|
20
|
+
i.fas.fa-plus
|
|
17
21
|
|
|
18
|
-
//-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
[
|
|
23
|
-
|
|
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()'
|
|
24
30
|
)
|
|
25
|
-
.workspace-info
|
|
26
|
-
.workspace-icon([style.color]='workspace.color')
|
|
27
|
-
i.fas([class]='"fa-" + (workspace.icon || "columns")')
|
|
28
|
-
.workspace-details
|
|
29
|
-
.workspace-name {{ workspace.name }}
|
|
30
|
-
.workspace-meta
|
|
31
|
-
span {{ getPaneCount(workspace) }} panes
|
|
32
|
-
span.separator ·
|
|
33
|
-
span {{ getOrientationLabel(workspace) }}
|
|
34
|
-
span.separator(*ngIf='workspace.launchOnStartup') ·
|
|
35
|
-
span.badge.badge-primary(*ngIf='workspace.launchOnStartup') startup
|
|
36
|
-
|
|
37
|
-
.workspace-actions
|
|
38
|
-
button.btn.btn-link.open-btn(
|
|
39
|
-
type='button',
|
|
40
|
-
title='Open',
|
|
41
|
-
[disabled]='openingWorkspaceId === workspace.id',
|
|
42
|
-
(click)='openWorkspace($event, workspace)'
|
|
43
|
-
)
|
|
44
|
-
i.fas(
|
|
45
|
-
[class.fa-external-link-alt]='openingWorkspaceId !== workspace.id',
|
|
46
|
-
[class.fa-spinner]='openingWorkspaceId === workspace.id',
|
|
47
|
-
[class.fa-spin]='openingWorkspaceId === workspace.id'
|
|
48
|
-
)
|
|
49
|
-
button.btn.btn-link(type='button', title='Duplicate', (click)='duplicateWorkspace($event, workspace)')
|
|
50
|
-
i.fas.fa-copy
|
|
51
|
-
button.btn.btn-link.text-danger(type='button', title='Delete', (click)='deleteWorkspace($event, workspace)')
|
|
52
|
-
i.fas.fa-trash
|
|
53
31
|
|
|
54
|
-
//- Empty state
|
|
55
|
-
.
|
|
32
|
+
//- Empty state (when no workspaces)
|
|
33
|
+
.tab-content.empty-state(*ngIf='!editingWorkspace && workspaces.length === 0')
|
|
56
34
|
p No workspaces configured yet.
|
|
57
|
-
p Click
|
|
35
|
+
p Click
|
|
36
|
+
strong +
|
|
37
|
+
| to create your first workspace.
|
|
@@ -1,118 +1,127 @@
|
|
|
1
|
+
@use '../styles/index' as *;
|
|
2
|
+
|
|
1
3
|
.workspace-list-container {
|
|
2
|
-
padding:
|
|
4
|
+
padding: $spacing-2xl;
|
|
3
5
|
}
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
// Tab bar
|
|
8
|
+
.tab-bar {
|
|
6
9
|
display: flex;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
font-size: 1.5rem;
|
|
21
|
+
&::-webkit-scrollbar-track {
|
|
22
|
+
background: var(--theme-bg-more);
|
|
14
23
|
}
|
|
15
|
-
}
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
}
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
.
|
|
35
|
+
.tab {
|
|
24
36
|
display: flex;
|
|
25
|
-
justify-content: space-between;
|
|
26
37
|
align-items: center;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
border-
|
|
30
|
-
|
|
31
|
-
transition: background 0.2s, border-color 0.2s;
|
|
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);
|
|
32
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
|
+
}
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
&.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
&.active {
|
|
56
|
+
background: var(--theme-bg);
|
|
57
|
+
color: var(--theme-fg);
|
|
58
|
+
border-bottom: 2px solid var(--theme-primary);
|
|
59
|
+
margin-bottom: -1px;
|
|
43
60
|
}
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
align-items: center;
|
|
49
|
-
gap: 16px;
|
|
63
|
+
.tab-icon {
|
|
64
|
+
font-size: 12px;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
.tab-name {
|
|
68
|
+
flex: 1;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
.
|
|
74
|
+
.tab-close {
|
|
75
|
+
width: 16px;
|
|
76
|
+
height: 16px;
|
|
59
77
|
display: flex;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
&:hover {
|
|
86
|
+
background: var(--theme-danger);
|
|
87
|
+
color: white;
|
|
88
|
+
}
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
.
|
|
70
|
-
|
|
91
|
+
.tab-new {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
width: 36px;
|
|
71
96
|
color: var(--theme-fg-more);
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
transition: color $transition-fast, background $transition-fast;
|
|
72
99
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.badge {
|
|
78
|
-
font-size: 0.75rem;
|
|
79
|
-
padding: 2px 6px;
|
|
80
|
-
border-radius: 4px;
|
|
81
|
-
background: var(--theme-primary);
|
|
82
|
-
color: white;
|
|
100
|
+
&:hover {
|
|
101
|
+
background: var(--theme-bg-more-more);
|
|
102
|
+
color: var(--theme-primary);
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
&.empty-state {
|
|
114
|
+
text-align: center;
|
|
115
|
+
padding: $spacing-2xl;
|
|
92
116
|
color: var(--theme-fg-more);
|
|
93
|
-
opacity: 0.7;
|
|
94
|
-
transition: opacity 0.2s, color 0.2s;
|
|
95
|
-
|
|
96
|
-
&:hover {
|
|
97
|
-
opacity: 1;
|
|
98
|
-
}
|
|
99
117
|
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
p {
|
|
119
|
+
margin: $spacing-md 0;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
color: var(--theme-
|
|
122
|
+
strong {
|
|
123
|
+
color: var(--theme-primary);
|
|
124
|
+
padding: 0 $spacing-xs;
|
|
106
125
|
}
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
|
-
|
|
110
|
-
.workspace-empty {
|
|
111
|
-
text-align: center;
|
|
112
|
-
padding: 40px;
|
|
113
|
-
color: var(--theme-fg-more);
|
|
114
|
-
|
|
115
|
-
p {
|
|
116
|
-
margin: 8px 0;
|
|
117
|
-
}
|
|
118
|
-
}
|