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,28 +1,41 @@
1
- import { Component, Input, Output, EventEmitter } from '@angular/core'
1
+ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core'
2
2
  import {
3
3
  WorkspaceSplit,
4
4
  WorkspacePane,
5
+ TabbyProfile,
5
6
  isWorkspaceSplit,
6
7
  } from '../models/workspace.model'
7
- import { WorkspaceEditorService } from '../services/workspaceEditor.service'
8
8
 
9
9
  @Component({
10
10
  selector: 'split-preview',
11
11
  template: require('./splitPreview.component.pug'),
12
12
  styles: [require('./splitPreview.component.scss')],
13
13
  })
14
- export class SplitPreviewComponent {
14
+ export class SplitPreviewComponent implements OnChanges {
15
15
  @Input() split!: WorkspaceSplit
16
16
  @Input() depth = 0
17
- @Output() paneClick = new EventEmitter<WorkspacePane>()
17
+ @Input() selectedPaneId: string | null = null
18
+ @Input() profiles: TabbyProfile[] = []
19
+ @Output() paneEdit = new EventEmitter<WorkspacePane>()
18
20
  @Output() splitHorizontal = new EventEmitter<WorkspacePane>()
19
21
  @Output() splitVertical = new EventEmitter<WorkspacePane>()
20
22
  @Output() removePane = new EventEmitter<WorkspacePane>()
23
+ @Output() addLeft = new EventEmitter<WorkspacePane>()
24
+ @Output() addRight = new EventEmitter<WorkspacePane>()
25
+ @Output() addTop = new EventEmitter<WorkspacePane>()
26
+ @Output() addBottom = new EventEmitter<WorkspacePane>()
21
27
 
22
28
  contextMenuPane: WorkspacePane | null = null
23
29
  contextMenuPosition = { x: 0, y: 0 }
24
30
 
25
- constructor(private workspaceService: WorkspaceEditorService) {}
31
+ constructor(private cdr: ChangeDetectorRef) {}
32
+
33
+ ngOnChanges(changes: SimpleChanges): void {
34
+ // Clear context menu when split input changes to avoid stale state
35
+ if (changes['split']) {
36
+ this.closeContextMenu()
37
+ }
38
+ }
26
39
 
27
40
  isPane(child: WorkspacePane | WorkspaceSplit): boolean {
28
41
  return !isWorkspaceSplit(child)
@@ -45,7 +58,13 @@ export class SplitPreviewComponent {
45
58
  }
46
59
 
47
60
  onPaneClick(pane: WorkspacePane): void {
48
- this.paneClick.emit(pane)
61
+ this.paneEdit.emit(pane)
62
+ }
63
+
64
+ truncate(text: string, maxLength: number): string {
65
+ return text.length > maxLength
66
+ ? text.substring(0, maxLength) + '...'
67
+ : text
49
68
  }
50
69
 
51
70
  onContextMenu(event: MouseEvent, pane: WorkspacePane): void {
@@ -56,6 +75,14 @@ export class SplitPreviewComponent {
56
75
 
57
76
  closeContextMenu(): void {
58
77
  this.contextMenuPane = null
78
+ this.cdr.detectChanges()
79
+ }
80
+
81
+ onEdit(): void {
82
+ if (this.contextMenuPane) {
83
+ this.paneEdit.emit(this.contextMenuPane)
84
+ this.closeContextMenu()
85
+ }
59
86
  }
60
87
 
61
88
  onSplitH(): void {
@@ -72,29 +99,51 @@ export class SplitPreviewComponent {
72
99
  }
73
100
  }
74
101
 
75
- onRemove(): void {
102
+ onAddLeft(): void {
76
103
  if (this.contextMenuPane) {
77
- this.removePane.emit(this.contextMenuPane)
104
+ this.addLeft.emit(this.contextMenuPane)
78
105
  this.closeContextMenu()
79
106
  }
80
107
  }
81
108
 
82
- getProfileName(profileId: string): string {
83
- return this.workspaceService.getProfileName(profileId) ?? 'Select profile'
109
+ onAddRight(): void {
110
+ if (this.contextMenuPane) {
111
+ this.addRight.emit(this.contextMenuPane)
112
+ this.closeContextMenu()
113
+ }
84
114
  }
85
115
 
86
- getPaneLabel(pane: WorkspacePane): string {
87
- if (pane.title) return pane.title
88
- if (pane.startupCommand) {
89
- const cmd = pane.startupCommand
90
- return cmd.length > 20 ? cmd.substring(0, 17) + '...' : cmd
116
+ onAddTop(): void {
117
+ if (this.contextMenuPane) {
118
+ this.addTop.emit(this.contextMenuPane)
119
+ this.closeContextMenu()
91
120
  }
92
- return this.getProfileName(pane.profileId)
121
+ }
122
+
123
+ onAddBottom(): void {
124
+ if (this.contextMenuPane) {
125
+ this.addBottom.emit(this.contextMenuPane)
126
+ this.closeContextMenu()
127
+ }
128
+ }
129
+
130
+ onRemove(): void {
131
+ if (this.contextMenuPane) {
132
+ this.removePane.emit(this.contextMenuPane)
133
+ this.closeContextMenu()
134
+ }
135
+ }
136
+
137
+ getPaneLabel(pane: WorkspacePane): string {
138
+ if (!pane.profileId) return 'Select profile'
139
+
140
+ const profile = this.profiles.find(p => p.id === pane.profileId)
141
+ return profile?.name || 'Select profile'
93
142
  }
94
143
 
95
144
  // Pass-through events from nested splits
96
- onNestedPaneClick(pane: WorkspacePane): void {
97
- this.paneClick.emit(pane)
145
+ onNestedPaneEdit(pane: WorkspacePane): void {
146
+ this.paneEdit.emit(pane)
98
147
  }
99
148
 
100
149
  onNestedSplitH(pane: WorkspacePane): void {
@@ -105,6 +154,22 @@ export class SplitPreviewComponent {
105
154
  this.splitVertical.emit(pane)
106
155
  }
107
156
 
157
+ onNestedAddLeft(pane: WorkspacePane): void {
158
+ this.addLeft.emit(pane)
159
+ }
160
+
161
+ onNestedAddRight(pane: WorkspacePane): void {
162
+ this.addRight.emit(pane)
163
+ }
164
+
165
+ onNestedAddTop(pane: WorkspacePane): void {
166
+ this.addTop.emit(pane)
167
+ }
168
+
169
+ onNestedAddBottom(pane: WorkspacePane): void {
170
+ this.addBottom.emit(pane)
171
+ }
172
+
108
173
  onNestedRemove(pane: WorkspacePane): void {
109
174
  this.removePane.emit(pane)
110
175
  }
@@ -1,84 +1,172 @@
1
- .workspace-editor-overlay((click)='onCancel()')
2
- .workspace-editor-modal((click)='$event.stopPropagation()')
3
- .modal-header
4
- h4 {{ workspace.id ? 'Edit' : 'New' }} Workspace
5
- button.btn.btn-link.close-btn(type='button', (click)='onCancel()')
6
- i.fas.fa-times
1
+ //- Section 1: Workspace Settings
2
+ .editor-section
3
+ .section-title
4
+ i.fas.fa-cog
5
+ | Workspace Settings
7
6
 
8
- .modal-body
9
- .form-row
10
- .form-group.flex-grow
11
- label Name
12
- input.form-control(
13
- type='text',
14
- [(ngModel)]='workspace.name',
15
- placeholder='Workspace name'
16
- )
7
+ .form-row
8
+ .form-group
9
+ label Name
10
+ input.form-control(
11
+ #nameInput,
12
+ type='text',
13
+ [(ngModel)]='workspace.name',
14
+ placeholder='Workspace name'
15
+ )
17
16
 
18
- .form-row
19
- .form-group
20
- label Icon
21
- .icon-selector
22
- button.icon-option(
23
- *ngFor='let icon of availableIcons',
24
- type='button',
25
- [class.selected]='workspace.icon === icon',
26
- (click)='workspace.icon = icon'
27
- )
28
- i.fas([class]='"fa-" + icon')
29
-
30
- .form-group
31
- label Color
32
- input.form-control.color-input(
33
- type='color',
34
- [(ngModel)]='workspace.color'
35
- )
17
+ .form-group.auto-width
18
+ label Icon
19
+ .dropdown-trigger((click)='toggleIconDropdown()')
20
+ span.dropdown-icon([style.color]='workspace.color')
21
+ i.fas([class]='"fa-" + workspace.icon')
22
+ span.dropdown-chevron
23
+ i.fas.fa-chevron-down
24
+ .icon-dropdown(*ngIf='iconDropdownOpen')
25
+ button.icon-option(
26
+ *ngFor='let icon of availableIcons',
27
+ type='button',
28
+ [class.selected]='workspace.icon === icon',
29
+ [style.color]='workspace.icon === icon ? workspace.color : null',
30
+ (click)='selectIcon(icon)'
31
+ )
32
+ i.fas([class]='"fa-" + icon')
36
33
 
37
- .form-row
38
- .form-group
39
- label
40
- input(type='checkbox', [(ngModel)]='workspace.isDefault')
41
- | Launch on startup
34
+ .form-group.auto-width
35
+ label Color
36
+ .color-trigger
37
+ span.dropdown-color([style.background]='workspace.color')
38
+ input.color-input-hidden(
39
+ type='color',
40
+ [(ngModel)]='workspace.color'
41
+ )
42
+ span.dropdown-chevron
43
+ i.fas.fa-chevron-down
42
44
 
43
- .form-section
44
- .section-header
45
- h5 Layout Preview
46
- .orientation-toggle
47
- button.btn.btn-sm(
45
+ .form-group.auto-width
46
+ label Background
47
+ .background-picker
48
+ .background-trigger((click)='toggleBackgroundDropdown()')
49
+ span.background-preview([style.background]='workspace.background?.value || "transparent"')
50
+ i.fas.fa-ban(*ngIf='!workspace.background?.value')
51
+ span.dropdown-chevron
52
+ i.fas.fa-chevron-down
53
+ .background-dropdown(*ngIf='backgroundDropdownOpen')
54
+ .preset-grid
55
+ button.preset-option(
56
+ *ngFor='let preset of backgroundPresets',
48
57
  type='button',
49
- [class.btn-primary]='workspace.root.orientation === "horizontal"',
50
- [class.btn-outline-secondary]='workspace.root.orientation !== "horizontal"',
51
- (click)='setOrientation("horizontal")'
58
+ [class.selected]='isBackgroundSelected(preset)',
59
+ [style.background]='preset.value || "var(--theme-bg)"',
60
+ (click)='selectBackgroundPreset(preset)'
52
61
  )
53
- i.fas.fa-arrows-alt-h
54
- | Horizontal
55
- button.btn.btn-sm(
56
- type='button',
57
- [class.btn-primary]='workspace.root.orientation === "vertical"',
58
- [class.btn-outline-secondary]='workspace.root.orientation !== "vertical"',
59
- (click)='setOrientation("vertical")'
62
+ i.fas.fa-ban(*ngIf='preset.type === "none"')
63
+ .custom-input-wrapper
64
+ input.form-control.custom-bg-input(
65
+ type='text',
66
+ [(ngModel)]='customBackgroundValue',
67
+ placeholder='Custom CSS gradient...',
68
+ (blur)='applyCustomBackground()'
60
69
  )
61
- i.fas.fa-arrows-alt-v
62
- | Vertical
63
70
 
64
- split-preview(
65
- [split]='workspace.root',
66
- (paneClick)='selectPane($event)',
67
- (splitHorizontal)='splitPane($event, "horizontal")',
68
- (splitVertical)='splitPane($event, "vertical")',
69
- (removePane)='removePane($event)'
70
- )
71
+ //- Section 2: Split Layout
72
+ .editor-section
73
+ .section-title
74
+ i.fas.fa-columns
75
+ | Split Layout
76
+
77
+ .split-preview-container
78
+ //- Preview toolbar
79
+ .preview-toolbar
80
+ button.preview-btn(
81
+ type='button',
82
+ title='Split Horizontal',
83
+ [disabled]='!selectedPaneId',
84
+ (click)='splitSelectedPane("horizontal")'
85
+ )
86
+ i.fas.fa-grip-lines-vertical
87
+ button.preview-btn(
88
+ type='button',
89
+ title='Split Vertical',
90
+ [disabled]='!selectedPaneId',
91
+ (click)='splitSelectedPane("vertical")'
92
+ )
93
+ i.fas.fa-grip-lines
94
+ span.toolbar-separator
95
+ button.preview-btn(
96
+ type='button',
97
+ title='Add Left',
98
+ [disabled]='!selectedPaneId',
99
+ (click)='addPane("left")'
100
+ )
101
+ i.fas.fa-arrow-left
102
+ button.preview-btn(
103
+ type='button',
104
+ title='Add Right',
105
+ [disabled]='!selectedPaneId',
106
+ (click)='addPane("right")'
107
+ )
108
+ i.fas.fa-arrow-right
109
+ button.preview-btn(
110
+ type='button',
111
+ title='Add Top',
112
+ [disabled]='!selectedPaneId',
113
+ (click)='addPane("top")'
114
+ )
115
+ i.fas.fa-arrow-up
116
+ button.preview-btn(
117
+ type='button',
118
+ title='Add Bottom',
119
+ [disabled]='!selectedPaneId',
120
+ (click)='addPane("bottom")'
121
+ )
122
+ i.fas.fa-arrow-down
123
+ span.toolbar-separator
124
+ button.preview-btn.danger(
125
+ type='button',
126
+ title='Remove pane',
127
+ [disabled]='!selectedPaneId || !canRemovePane()',
128
+ (click)='removeSelectedPane()'
129
+ )
130
+ i.fas.fa-trash
131
+
132
+ //- Split preview
133
+ .layout-preview((click)='onPreviewBackgroundClick()')
134
+ split-preview(
135
+ [split]='workspace.root',
136
+ [selectedPaneId]='selectedPaneId',
137
+ [profiles]='profiles',
138
+ (paneEdit)='editPane($event)',
139
+ (splitHorizontal)='splitPane($event, "horizontal")',
140
+ (splitVertical)='splitPane($event, "vertical")',
141
+ (addLeft)='addPaneFromEvent($event, "left")',
142
+ (addRight)='addPaneFromEvent($event, "right")',
143
+ (addTop)='addPaneFromEvent($event, "top")',
144
+ (addBottom)='addPaneFromEvent($event, "bottom")',
145
+ (removePane)='removePane($event)'
146
+ )
71
147
 
72
- .modal-footer
73
- button.btn.btn-secondary(type='button', (click)='onCancel()') Cancel
74
- button.btn.btn-primary(type='button', (click)='onSave()', [disabled]='!workspace.name?.trim()')
75
- i.fas.fa-save
76
- | Save Workspace
148
+ //- Inline pane editor
149
+ pane-editor(
150
+ *ngIf='showPaneEditor && editingPane',
151
+ [pane]='editingPane',
152
+ [profiles]='profiles',
153
+ (close)='closePaneEditor()'
154
+ )
77
155
 
78
- pane-editor(
79
- *ngIf='showPaneEditor && selectedPane',
80
- [pane]='selectedPane',
81
- [profiles]='profiles',
82
- (save)='onPaneSave($event)',
83
- (cancel)='closePaneEditor()'
84
- )
156
+ //- Action buttons
157
+ .action-buttons
158
+ .checkbox-group
159
+ input(type='checkbox', [(ngModel)]='workspace.launchOnStartup', id='launchStartup')
160
+ label(for='launchStartup') Launch on startup
161
+ .action-buttons-right
162
+ button.btn.btn-ghost(type='button', (click)='onCancel()')
163
+ i.fas.fa-xmark
164
+ | Cancel
165
+ button.btn.btn-success(
166
+ type='button',
167
+ (click)='onSave()',
168
+ [disabled]='!workspace.name?.trim() || !hasUnsavedChanges'
169
+ )
170
+ i.fas.fa-check
171
+ | Save
172
+ span.unsaved-indicator(*ngIf='hasUnsavedChanges') *