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.
Files changed (78) hide show
  1. package/.claude/settings.local.json +29 -2
  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 +92 -20
  7. package/CLAUDE.md +196 -15
  8. package/CONTRIBUTING.md +3 -1
  9. package/README.md +80 -61
  10. package/RELEASE.md +91 -0
  11. package/TODO.md +77 -0
  12. package/dist/build-config.d.ts +3 -3
  13. package/dist/components/deleteConfirmModal.component.d.ts +7 -0
  14. package/dist/components/deleteConfirmModal.component.d.ts.map +1 -0
  15. package/dist/components/paneEditor.component.d.ts +9 -13
  16. package/dist/components/paneEditor.component.d.ts.map +1 -1
  17. package/dist/components/splitPreview.component.d.ts +50 -35
  18. package/dist/components/splitPreview.component.d.ts.map +1 -1
  19. package/dist/components/workspaceEditor.component.d.ts +61 -28
  20. package/dist/components/workspaceEditor.component.d.ts.map +1 -1
  21. package/dist/components/workspaceList.component.d.ts +56 -27
  22. package/dist/components/workspaceList.component.d.ts.map +1 -1
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.LICENSE.txt +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/models/workspace.model.d.ts +118 -76
  29. package/dist/models/workspace.model.d.ts.map +1 -1
  30. package/dist/package.json +26 -0
  31. package/dist/providers/config.provider.d.ts +8 -8
  32. package/dist/providers/settings.provider.d.ts +7 -7
  33. package/dist/providers/settings.provider.d.ts.map +1 -1
  34. package/dist/providers/toolbar.provider.d.ts +23 -12
  35. package/dist/providers/toolbar.provider.d.ts.map +1 -1
  36. package/dist/services/startupCommand.service.d.ts +28 -0
  37. package/dist/services/startupCommand.service.d.ts.map +1 -0
  38. package/dist/services/workspaceBackground.service.d.ts +38 -0
  39. package/dist/services/workspaceBackground.service.d.ts.map +1 -0
  40. package/dist/services/workspaceEditor.service.d.ts +46 -24
  41. package/dist/services/workspaceEditor.service.d.ts.map +1 -1
  42. package/docs/DESIGN.md +57 -0
  43. package/docs/SESSION-2026-01-14-S1-DESIGN.md +134 -0
  44. package/docs/marketing_status.md +92 -0
  45. package/mockups/index.html +162 -0
  46. package/mockups/s1-tight-sharp.html +522 -0
  47. package/mockups/shared/base.css +216 -0
  48. package/mockups/v06-tabbed.html +643 -0
  49. package/package.json +3 -7
  50. package/screenshots/editor.png +0 -0
  51. package/screenshots/pane-edit.png +0 -0
  52. package/scripts/build-dev.js +2 -1
  53. package/scripts/build-prod.js +40 -0
  54. package/src/components/deleteConfirmModal.component.ts +23 -0
  55. package/src/components/paneEditor.component.pug +27 -43
  56. package/src/components/paneEditor.component.scss +37 -85
  57. package/src/components/paneEditor.component.ts +6 -16
  58. package/src/components/splitPreview.component.pug +36 -5
  59. package/src/components/splitPreview.component.scss +78 -45
  60. package/src/components/splitPreview.component.ts +83 -18
  61. package/src/components/workspaceEditor.component.pug +162 -74
  62. package/src/components/workspaceEditor.component.scss +261 -108
  63. package/src/components/workspaceEditor.component.ts +294 -31
  64. package/src/components/workspaceList.component.pug +32 -41
  65. package/src/components/workspaceList.component.scss +89 -74
  66. package/src/components/workspaceList.component.ts +181 -44
  67. package/src/index.ts +6 -0
  68. package/src/models/workspace.model.ts +113 -8
  69. package/src/providers/settings.provider.ts +2 -2
  70. package/src/providers/toolbar.provider.ts +113 -13
  71. package/src/services/startupCommand.service.ts +140 -0
  72. package/src/services/workspaceBackground.service.ts +167 -0
  73. package/src/services/workspaceEditor.service.ts +134 -65
  74. package/src/styles/_index.scss +3 -0
  75. package/src/styles/_mixins.scss +180 -0
  76. package/src/styles/_variables.scss +67 -0
  77. package/RELEASE_PLAN.md +0 -161
  78. 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
- selectedPane: WorkspacePane | null = null
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
- constructor(private workspaceService: WorkspaceEditorService) {}
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
- selectPane(pane: WorkspacePane): void {
57
- this.selectedPane = pane
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.selectedPane = null
218
+ this.editingPane = null
219
+ this.cdr.detectChanges()
64
220
  }
65
221
 
66
- onPaneSave(pane: WorkspacePane): void {
67
- this.updatePaneInTree(this.workspace.root, pane)
68
- this.closePaneEditor()
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
- private updatePaneInTree(node: WorkspaceSplit, updatedPane: WorkspacePane): boolean {
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.updatePaneInTree(child, updatedPane)) {
76
- return true
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(this.workspace.root, pane, orientation)
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
- for (let i = 0; i < node.children.length; i++) {
96
- const child = node.children[i]
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 // Copy profile from source pane
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[i] = newSplit
110
- this.recalculateRatios(node)
301
+ ctx.node.children[ctx.index] = newSplit
302
+ this.recalculateRatios(ctx.node)
111
303
  return true
112
304
  }
113
- }
114
- return false
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
- .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
- .workspace-list(*ngIf='workspaces.length > 0')
9
- .workspace-item(*ngFor='let workspace of workspaces')
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 &middot;
18
- span {{ getOrientationLabel(workspace) }}
19
- span.separator(*ngIf='workspace.isDefault') &middot;
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
- .workspace-actions
23
- button.btn.btn-link(
24
- type='button',
25
- title='Set as default',
26
- (click)='setAsDefault(workspace)',
27
- [class.active]='workspace.isDefault'
28
- )
29
- i.fas.fa-star
30
- button.btn.btn-link(type='button', title='Duplicate', (click)='duplicateWorkspace(workspace)')
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
- .workspace-empty(*ngIf='workspaces.length === 0')
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 "New Workspace" to create your first split-layout workspace.
40
-
41
- workspace-editor(
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: 20px;
3
- max-width: 800px;
4
+ padding: $spacing-2xl;
4
5
  }
5
6
 
6
- .workspace-list-header {
7
+ // Tab bar
8
+ .tab-bar {
7
9
  display: flex;
8
- justify-content: space-between;
9
- align-items: center;
10
- 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
+ }
11
20
 
12
- h3 {
13
- margin: 0;
14
- font-size: 1.5rem;
21
+ &::-webkit-scrollbar-track {
22
+ background: var(--theme-bg-more);
15
23
  }
16
- }
17
24
 
18
- .workspace-list {
19
- display: flex;
20
- flex-direction: column;
21
- 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
+ }
22
33
  }
23
34
 
24
- .workspace-item {
35
+ .tab {
25
36
  display: flex;
26
- justify-content: space-between;
27
37
  align-items: center;
28
- padding: 16px;
29
- background: var(--theme-bg-more);
30
- border-radius: 8px;
31
- border: 1px solid var(--theme-border);
32
- transition: background 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);
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
- .workspace-info {
40
- display: flex;
41
- align-items: center;
42
- gap: 16px;
63
+ .tab-icon {
64
+ font-size: 12px;
43
65
  }
44
66
 
45
- .workspace-icon {
46
- font-size: 24px;
47
- width: 40px;
48
- text-align: center;
67
+ .tab-name {
68
+ flex: 1;
69
+ white-space: nowrap;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
49
72
  }
50
73
 
51
- .workspace-details {
74
+ .tab-close {
75
+ width: 16px;
76
+ height: 16px;
52
77
  display: flex;
53
- flex-direction: column;
54
- gap: 4px;
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
- .workspace-name {
58
- font-size: 1.1rem;
59
- font-weight: 500;
85
+ &:hover {
86
+ background: var(--theme-danger);
87
+ color: white;
88
+ }
60
89
  }
61
90
 
62
- .workspace-meta {
63
- font-size: 0.85rem;
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
- .separator {
67
- margin: 0 6px;
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
- .workspace-actions {
80
- display: flex;
81
- 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;
82
112
 
83
- .btn-link {
84
- padding: 8px;
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
- &:hover {
90
- opacity: 1;
91
- }
92
-
93
- &.active {
94
- color: gold;
95
- opacity: 1;
118
+ p {
119
+ margin: $spacing-md 0;
96
120
  }
97
121
 
98
- &.text-danger:hover {
99
- color: var(--theme-danger);
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
- }