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.
Files changed (76) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.github/workflows/ci.yml +26 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +81 -0
  5. package/.github/workflows/release.yml +30 -0
  6. package/CHANGELOG.md +46 -0
  7. package/CLAUDE.md +33 -0
  8. package/CONTRIBUTING.md +3 -1
  9. package/README.md +21 -18
  10. package/TODO.md +5 -0
  11. package/dist/build-config.d.ts +3 -3
  12. package/dist/components/deleteConfirmModal.component.d.ts +7 -0
  13. package/dist/components/deleteConfirmModal.component.d.ts.map +1 -0
  14. package/dist/components/paneEditor.component.d.ts +9 -18
  15. package/dist/components/paneEditor.component.d.ts.map +1 -1
  16. package/dist/components/splitPreview.component.d.ts +50 -50
  17. package/dist/components/splitPreview.component.d.ts.map +1 -1
  18. package/dist/components/workspaceEditor.component.d.ts +61 -54
  19. package/dist/components/workspaceEditor.component.d.ts.map +1 -1
  20. package/dist/components/workspaceList.component.d.ts +56 -39
  21. package/dist/components/workspaceList.component.d.ts.map +1 -1
  22. package/dist/index.d.ts +6 -6
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.LICENSE.txt +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/models/workspace.model.d.ts +118 -78
  28. package/dist/models/workspace.model.d.ts.map +1 -1
  29. package/dist/package.json +1 -1
  30. package/dist/providers/config.provider.d.ts +8 -8
  31. package/dist/providers/settings.provider.d.ts +7 -7
  32. package/dist/providers/toolbar.provider.d.ts +23 -15
  33. package/dist/providers/toolbar.provider.d.ts.map +1 -1
  34. package/dist/services/startupCommand.service.d.ts +27 -19
  35. package/dist/services/startupCommand.service.d.ts.map +1 -1
  36. package/dist/services/workspaceBackground.service.d.ts +38 -0
  37. package/dist/services/workspaceBackground.service.d.ts.map +1 -0
  38. package/dist/services/workspaceEditor.service.d.ts +46 -32
  39. package/dist/services/workspaceEditor.service.d.ts.map +1 -1
  40. package/docs/DESIGN.md +57 -0
  41. package/docs/SESSION-2026-01-14-S1-DESIGN.md +134 -0
  42. package/mockups/index.html +162 -0
  43. package/mockups/s1-tight-sharp.html +522 -0
  44. package/mockups/shared/base.css +216 -0
  45. package/mockups/v06-tabbed.html +643 -0
  46. package/package.json +2 -1
  47. package/screenshots/editor.png +0 -0
  48. package/scripts/build-dev.js +2 -1
  49. package/scripts/build-prod.js +2 -1
  50. package/src/components/deleteConfirmModal.component.ts +23 -0
  51. package/src/components/paneEditor.component.pug +27 -43
  52. package/src/components/paneEditor.component.scss +37 -85
  53. package/src/components/paneEditor.component.ts +4 -32
  54. package/src/components/splitPreview.component.pug +0 -9
  55. package/src/components/splitPreview.component.scss +46 -70
  56. package/src/components/splitPreview.component.ts +15 -25
  57. package/src/components/workspaceEditor.component.pug +140 -112
  58. package/src/components/workspaceEditor.component.scss +270 -202
  59. package/src/components/workspaceEditor.component.ts +161 -85
  60. package/src/components/workspaceList.component.pug +31 -51
  61. package/src/components/workspaceList.component.scss +86 -77
  62. package/src/components/workspaceList.component.ts +89 -34
  63. package/src/index.ts +4 -0
  64. package/src/models/workspace.model.ts +80 -2
  65. package/src/providers/toolbar.provider.ts +78 -9
  66. package/src/services/startupCommand.service.ts +30 -32
  67. package/src/services/workspaceBackground.service.ts +167 -0
  68. package/src/services/workspaceEditor.service.ts +77 -40
  69. package/src/styles/_index.scss +3 -0
  70. package/src/styles/_mixins.scss +180 -0
  71. package/src/styles/_variables.scss +67 -0
  72. package/TEST_MCP.md +0 -176
  73. package/cdp-click.js +0 -22
  74. package/cdp-test.js +0 -28
  75. package/screenshots/pane-edit.png +0 -0
  76. 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
- const iconPicker = this.elementRef.nativeElement.querySelector('.icon-picker')
46
- if (iconPicker && !iconPicker.contains(event.target as Node)) {
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
- // Reset component state when workspace input changes
85
- this.selectedPaneId = null
86
- this.editingPane = null
87
- this.showPaneEditor = false
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 updatePaneInTree(node: WorkspaceSplit, updatedPane: WorkspacePane): boolean {
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.updatePaneInTree(child, updatedPane)) {
184
- return true
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(this.workspace.root, pane, orientation)
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
- for (let i = 0; i < node.children.length; i++) {
210
- const child = node.children[i]
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 // Copy profile from source pane
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[i] = newSplit
224
- this.recalculateRatios(node)
301
+ ctx.node.children[ctx.index] = newSplit
302
+ this.recalculateRatios(ctx.node)
225
303
  return true
226
304
  }
227
- }
228
- return false
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(this.workspace.root, pane, direction, null)
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(this.workspace.root, pane, direction, null)
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
- 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
- }
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
- 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
- }
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
- return false
440
+ return true
441
+ })
366
442
  }
367
443
 
368
444
  }
@@ -1,57 +1,37 @@
1
1
  .workspace-list-container
2
- .workspace-list-header
3
- h3 Workspace Editor
4
- button.btn.btn-primary(type='button', (click)='createWorkspace()')
5
- i.fas.fa-plus
6
- | New Workspace
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
- //- Editor (above list)
9
- workspace-editor(
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
- //- Workspace list
19
- .workspace-list(*ngIf='workspaces.length > 0')
20
- .workspace-item(
21
- *ngFor='let workspace of workspaces',
22
- [class.selected]='isSelected(workspace)',
23
- (click)='selectWorkspace(workspace)'
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 &middot;
33
- span {{ getOrientationLabel(workspace) }}
34
- span.separator(*ngIf='workspace.launchOnStartup') &middot;
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
- .workspace-empty(*ngIf='workspaces.length === 0 && !isCreatingNew')
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 "New Workspace" to create your first split-layout workspace.
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: 20px;
4
+ padding: $spacing-2xl;
3
5
  }
4
6
 
5
- .workspace-list-header {
7
+ // Tab bar
8
+ .tab-bar {
6
9
  display: flex;
7
- justify-content: space-between;
8
- align-items: center;
9
- margin-bottom: 20px;
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
- h3 {
12
- margin: 0;
13
- font-size: 1.5rem;
21
+ &::-webkit-scrollbar-track {
22
+ background: var(--theme-bg-more);
14
23
  }
15
- }
16
24
 
17
- .workspace-list {
18
- display: flex;
19
- flex-direction: column;
20
- gap: 12px;
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
- .workspace-item {
35
+ .tab {
24
36
  display: flex;
25
- justify-content: space-between;
26
37
  align-items: center;
27
- padding: 16px;
28
- background: var(--theme-bg-more);
29
- border-radius: 8px;
30
- border: 1px solid var(--theme-border);
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
- &.selected {
39
- border-color: var(--theme-primary);
40
- border-left-width: 3px;
41
- background: var(--theme-bg-more-more);
42
- box-shadow: 0 0 0 1px var(--theme-primary) inset;
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
- .workspace-info {
47
- display: flex;
48
- align-items: center;
49
- gap: 16px;
63
+ .tab-icon {
64
+ font-size: 12px;
50
65
  }
51
66
 
52
- .workspace-icon {
53
- font-size: 24px;
54
- width: 40px;
55
- text-align: center;
67
+ .tab-name {
68
+ flex: 1;
69
+ white-space: nowrap;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
56
72
  }
57
73
 
58
- .workspace-details {
74
+ .tab-close {
75
+ width: 16px;
76
+ height: 16px;
59
77
  display: flex;
60
- flex-direction: column;
61
- gap: 4px;
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
- .workspace-name {
65
- font-size: 1.1rem;
66
- font-weight: 500;
85
+ &:hover {
86
+ background: var(--theme-danger);
87
+ color: white;
88
+ }
67
89
  }
68
90
 
69
- .workspace-meta {
70
- font-size: 0.85rem;
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
- .separator {
74
- margin: 0 6px;
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
- .workspace-actions {
87
- display: flex;
88
- gap: 4px;
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
- .btn-link {
91
- padding: 8px;
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
- &.open-btn:hover {
101
- color: var(--theme-success, #10b981);
118
+ p {
119
+ margin: $spacing-md 0;
102
120
  }
103
121
 
104
- &.text-danger:hover {
105
- color: var(--theme-danger);
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
- }