tabby-tabbyspaces 0.0.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 +15 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +159 -0
- package/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/RELEASE_PLAN.md +161 -0
- package/dist/build-config.d.ts +4 -0
- package/dist/build-config.d.ts.map +1 -0
- package/dist/components/paneEditor.component.d.ts +14 -0
- package/dist/components/paneEditor.component.d.ts.map +1 -0
- package/dist/components/splitPreview.component.d.ts +36 -0
- package/dist/components/splitPreview.component.d.ts.map +1 -0
- package/dist/components/workspaceEditor.component.d.ts +29 -0
- package/dist/components/workspaceEditor.component.d.ts.map +1 -0
- package/dist/components/workspaceList.component.d.ts +28 -0
- package/dist/components/workspaceList.component.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.LICENSE.txt +43 -0
- package/dist/index.js.map +1 -0
- package/dist/models/workspace.model.d.ts +77 -0
- package/dist/models/workspace.model.d.ts.map +1 -0
- package/dist/providers/config.provider.d.ts +9 -0
- package/dist/providers/config.provider.d.ts.map +1 -0
- package/dist/providers/settings.provider.d.ts +8 -0
- package/dist/providers/settings.provider.d.ts.map +1 -0
- package/dist/providers/toolbar.provider.d.ts +13 -0
- package/dist/providers/toolbar.provider.d.ts.map +1 -0
- package/dist/services/workspaceEditor.service.d.ts +25 -0
- package/dist/services/workspaceEditor.service.d.ts.map +1 -0
- package/package.json +73 -0
- package/screenshots/editor.png +0 -0
- package/screenshots/pane-edit.png +0 -0
- package/screenshots/workspace-edit.png +0 -0
- package/scripts/build-dev.js +46 -0
- package/src/build-config.ts +8 -0
- package/src/components/paneEditor.component.pug +46 -0
- package/src/components/paneEditor.component.scss +112 -0
- package/src/components/paneEditor.component.ts +33 -0
- package/src/components/splitPreview.component.pug +45 -0
- package/src/components/splitPreview.component.scss +126 -0
- package/src/components/splitPreview.component.ts +111 -0
- package/src/components/workspaceEditor.component.pug +84 -0
- package/src/components/workspaceEditor.component.scss +169 -0
- package/src/components/workspaceEditor.component.ts +181 -0
- package/src/components/workspaceList.component.pug +46 -0
- package/src/components/workspaceList.component.scss +112 -0
- package/src/components/workspaceList.component.ts +124 -0
- package/src/index.ts +38 -0
- package/src/models/workspace.model.ts +126 -0
- package/src/providers/config.provider.ts +12 -0
- package/src/providers/settings.provider.ts +15 -0
- package/src/providers/toolbar.provider.ts +81 -0
- package/src/services/workspaceEditor.service.ts +228 -0
- package/tsconfig.json +29 -0
- package/webpack.config.js +62 -0
|
@@ -0,0 +1,84 @@
|
|
|
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
|
|
7
|
+
|
|
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
|
+
)
|
|
17
|
+
|
|
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
|
+
)
|
|
36
|
+
|
|
37
|
+
.form-row
|
|
38
|
+
.form-group
|
|
39
|
+
label
|
|
40
|
+
input(type='checkbox', [(ngModel)]='workspace.isDefault')
|
|
41
|
+
| Launch on startup
|
|
42
|
+
|
|
43
|
+
.form-section
|
|
44
|
+
.section-header
|
|
45
|
+
h5 Layout Preview
|
|
46
|
+
.orientation-toggle
|
|
47
|
+
button.btn.btn-sm(
|
|
48
|
+
type='button',
|
|
49
|
+
[class.btn-primary]='workspace.root.orientation === "horizontal"',
|
|
50
|
+
[class.btn-outline-secondary]='workspace.root.orientation !== "horizontal"',
|
|
51
|
+
(click)='setOrientation("horizontal")'
|
|
52
|
+
)
|
|
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")'
|
|
60
|
+
)
|
|
61
|
+
i.fas.fa-arrows-alt-v
|
|
62
|
+
| Vertical
|
|
63
|
+
|
|
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
|
+
|
|
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
|
|
77
|
+
|
|
78
|
+
pane-editor(
|
|
79
|
+
*ngIf='showPaneEditor && selectedPane',
|
|
80
|
+
[pane]='selectedPane',
|
|
81
|
+
[profiles]='profiles',
|
|
82
|
+
(save)='onPaneSave($event)',
|
|
83
|
+
(cancel)='closePaneEditor()'
|
|
84
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
.workspace-editor-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
background: rgba(0, 0, 0, 0.6);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 1000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.workspace-editor-modal {
|
|
15
|
+
background: var(--theme-bg);
|
|
16
|
+
border-radius: 12px;
|
|
17
|
+
width: 90%;
|
|
18
|
+
max-width: 700px;
|
|
19
|
+
max-height: 90vh;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.modal-header {
|
|
27
|
+
display: flex;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
align-items: center;
|
|
30
|
+
padding: 16px 20px;
|
|
31
|
+
border-bottom: 1px solid var(--theme-border);
|
|
32
|
+
|
|
33
|
+
h4 {
|
|
34
|
+
margin: 0;
|
|
35
|
+
font-size: 1.25rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.close-btn {
|
|
39
|
+
font-size: 1.25rem;
|
|
40
|
+
color: var(--theme-fg-more);
|
|
41
|
+
padding: 4px 8px;
|
|
42
|
+
|
|
43
|
+
&:hover {
|
|
44
|
+
color: var(--theme-fg);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.modal-body {
|
|
50
|
+
padding: 20px;
|
|
51
|
+
overflow-y: auto;
|
|
52
|
+
flex: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.form-row {
|
|
56
|
+
display: flex;
|
|
57
|
+
gap: 16px;
|
|
58
|
+
margin-bottom: 16px;
|
|
59
|
+
|
|
60
|
+
&:last-child {
|
|
61
|
+
margin-bottom: 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.form-group {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 6px;
|
|
69
|
+
|
|
70
|
+
&.flex-grow {
|
|
71
|
+
flex: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
label {
|
|
75
|
+
font-size: 0.9rem;
|
|
76
|
+
color: var(--theme-fg-more);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
input[type='checkbox'] {
|
|
80
|
+
margin-right: 8px;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.form-control {
|
|
85
|
+
padding: 8px 12px;
|
|
86
|
+
border-radius: 6px;
|
|
87
|
+
border: 1px solid var(--theme-border);
|
|
88
|
+
background: var(--theme-bg-more);
|
|
89
|
+
color: var(--theme-fg);
|
|
90
|
+
font-size: 0.95rem;
|
|
91
|
+
|
|
92
|
+
&:focus {
|
|
93
|
+
outline: none;
|
|
94
|
+
border-color: var(--theme-primary);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.color-input {
|
|
99
|
+
width: 60px;
|
|
100
|
+
height: 36px;
|
|
101
|
+
padding: 2px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.icon-selector {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
gap: 4px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.icon-option {
|
|
112
|
+
width: 36px;
|
|
113
|
+
height: 36px;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
border: 1px solid var(--theme-border);
|
|
118
|
+
border-radius: 6px;
|
|
119
|
+
background: var(--theme-bg-more);
|
|
120
|
+
color: var(--theme-fg-more);
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
transition: all 0.2s;
|
|
123
|
+
|
|
124
|
+
&:hover {
|
|
125
|
+
background: var(--theme-bg-more-more);
|
|
126
|
+
color: var(--theme-fg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&.selected {
|
|
130
|
+
border-color: var(--theme-primary);
|
|
131
|
+
background: var(--theme-primary);
|
|
132
|
+
color: white;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.form-section {
|
|
137
|
+
margin-top: 20px;
|
|
138
|
+
|
|
139
|
+
.section-header {
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: space-between;
|
|
142
|
+
align-items: center;
|
|
143
|
+
margin-bottom: 12px;
|
|
144
|
+
|
|
145
|
+
h5 {
|
|
146
|
+
margin: 0;
|
|
147
|
+
font-size: 1rem;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.orientation-toggle {
|
|
153
|
+
display: flex;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
|
|
156
|
+
.btn {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 6px;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.modal-footer {
|
|
164
|
+
display: flex;
|
|
165
|
+
justify-content: flex-end;
|
|
166
|
+
gap: 12px;
|
|
167
|
+
padding: 16px 20px;
|
|
168
|
+
border-top: 1px solid var(--theme-border);
|
|
169
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'
|
|
2
|
+
import {
|
|
3
|
+
Workspace,
|
|
4
|
+
WorkspacePane,
|
|
5
|
+
WorkspaceSplit,
|
|
6
|
+
TabbyProfile,
|
|
7
|
+
isWorkspaceSplit,
|
|
8
|
+
createDefaultPane,
|
|
9
|
+
generateUUID,
|
|
10
|
+
} from '../models/workspace.model'
|
|
11
|
+
import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'workspace-editor',
|
|
15
|
+
template: require('./workspaceEditor.component.pug'),
|
|
16
|
+
styles: [require('./workspaceEditor.component.scss')],
|
|
17
|
+
})
|
|
18
|
+
export class WorkspaceEditorComponent implements OnInit {
|
|
19
|
+
@Input() workspace!: Workspace
|
|
20
|
+
@Output() save = new EventEmitter<Workspace>()
|
|
21
|
+
@Output() cancel = new EventEmitter<void>()
|
|
22
|
+
|
|
23
|
+
selectedPane: WorkspacePane | null = null
|
|
24
|
+
showPaneEditor = false
|
|
25
|
+
profiles: TabbyProfile[] = []
|
|
26
|
+
availableIcons = [
|
|
27
|
+
'columns', 'terminal', 'code', 'folder', 'home', 'briefcase',
|
|
28
|
+
'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
|
|
29
|
+
'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
constructor(private workspaceService: WorkspaceEditorService) {}
|
|
33
|
+
|
|
34
|
+
async ngOnInit(): Promise<void> {
|
|
35
|
+
this.profiles = await this.workspaceService.getAvailableProfiles()
|
|
36
|
+
if (!this.workspace.root) {
|
|
37
|
+
this.workspace.root = {
|
|
38
|
+
orientation: 'horizontal',
|
|
39
|
+
ratios: [0.5, 0.5],
|
|
40
|
+
children: [createDefaultPane(), createDefaultPane()],
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onSave(): void {
|
|
46
|
+
if (!this.workspace.name?.trim()) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
this.save.emit(this.workspace)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onCancel(): void {
|
|
53
|
+
this.cancel.emit()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
selectPane(pane: WorkspacePane): void {
|
|
57
|
+
this.selectedPane = pane
|
|
58
|
+
this.showPaneEditor = true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
closePaneEditor(): void {
|
|
62
|
+
this.showPaneEditor = false
|
|
63
|
+
this.selectedPane = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onPaneSave(pane: WorkspacePane): void {
|
|
67
|
+
this.updatePaneInTree(this.workspace.root, pane)
|
|
68
|
+
this.closePaneEditor()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private updatePaneInTree(node: WorkspaceSplit, updatedPane: WorkspacePane): boolean {
|
|
72
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
73
|
+
const child = node.children[i]
|
|
74
|
+
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
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
splitPane(pane: WorkspacePane, orientation: 'horizontal' | 'vertical'): void {
|
|
87
|
+
this.splitPaneInTree(this.workspace.root, pane, orientation)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private splitPaneInTree(
|
|
91
|
+
node: WorkspaceSplit,
|
|
92
|
+
targetPane: WorkspacePane,
|
|
93
|
+
orientation: 'horizontal' | 'vertical'
|
|
94
|
+
): 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) {
|
|
102
|
+
const newPane = createDefaultPane()
|
|
103
|
+
newPane.profileId = child.profileId // Copy profile from source pane
|
|
104
|
+
const newSplit: WorkspaceSplit = {
|
|
105
|
+
orientation,
|
|
106
|
+
ratios: [0.5, 0.5],
|
|
107
|
+
children: [child, newPane],
|
|
108
|
+
}
|
|
109
|
+
node.children[i] = newSplit
|
|
110
|
+
this.recalculateRatios(node)
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removePane(pane: WorkspacePane): void {
|
|
118
|
+
this.removePaneFromTree(this.workspace.root, pane)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private removePaneFromTree(node: WorkspaceSplit, targetPane: WorkspacePane): boolean {
|
|
122
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
123
|
+
const child = node.children[i]
|
|
124
|
+
if (isWorkspaceSplit(child)) {
|
|
125
|
+
// Check if the pane is directly in this split
|
|
126
|
+
const paneIndex = child.children.findIndex(
|
|
127
|
+
(c) => !isWorkspaceSplit(c) && (c as WorkspacePane).id === targetPane.id
|
|
128
|
+
)
|
|
129
|
+
if (paneIndex !== -1 && child.children.length > 1) {
|
|
130
|
+
child.children.splice(paneIndex, 1)
|
|
131
|
+
this.recalculateRatios(child)
|
|
132
|
+
// If only one child remains, flatten
|
|
133
|
+
if (child.children.length === 1) {
|
|
134
|
+
node.children[i] = child.children[0]
|
|
135
|
+
}
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
if (this.removePaneFromTree(child, targetPane)) {
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
} else if (child.id === targetPane.id) {
|
|
142
|
+
if (node.children.length > 1) {
|
|
143
|
+
node.children.splice(i, 1)
|
|
144
|
+
this.recalculateRatios(node)
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private recalculateRatios(split: WorkspaceSplit): void {
|
|
153
|
+
const count = split.children.length
|
|
154
|
+
split.ratios = split.children.map(() => 1 / count)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setOrientation(orientation: 'horizontal' | 'vertical'): void {
|
|
158
|
+
this.workspace.root.orientation = orientation
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
updateRatio(index: number, value: number): void {
|
|
162
|
+
const ratios = [...this.workspace.root.ratios]
|
|
163
|
+
const diff = value - ratios[index]
|
|
164
|
+
|
|
165
|
+
if (index < ratios.length - 1) {
|
|
166
|
+
ratios[index] = value
|
|
167
|
+
ratios[index + 1] -= diff
|
|
168
|
+
} else {
|
|
169
|
+
ratios[index] = value
|
|
170
|
+
ratios[index - 1] -= diff
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Clamp values
|
|
174
|
+
ratios.forEach((r, i) => {
|
|
175
|
+
ratios[i] = Math.max(0.1, Math.min(0.9, r))
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
this.workspace.root.ratios = ratios
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
|
7
|
+
|
|
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 ·
|
|
18
|
+
span {{ getOrientationLabel(workspace) }}
|
|
19
|
+
span.separator(*ngIf='workspace.isDefault') ·
|
|
20
|
+
span.badge.badge-primary(*ngIf='workspace.isDefault') default
|
|
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
|
|
36
|
+
|
|
37
|
+
.workspace-empty(*ngIf='workspaces.length === 0')
|
|
38
|
+
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
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
.workspace-list-container {
|
|
2
|
+
padding: 20px;
|
|
3
|
+
max-width: 800px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.workspace-list-header {
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
align-items: center;
|
|
10
|
+
margin-bottom: 20px;
|
|
11
|
+
|
|
12
|
+
h3 {
|
|
13
|
+
margin: 0;
|
|
14
|
+
font-size: 1.5rem;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.workspace-list {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 12px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.workspace-item {
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
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;
|
|
33
|
+
|
|
34
|
+
&:hover {
|
|
35
|
+
background: var(--theme-bg-more-more);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.workspace-info {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 16px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.workspace-icon {
|
|
46
|
+
font-size: 24px;
|
|
47
|
+
width: 40px;
|
|
48
|
+
text-align: center;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.workspace-details {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 4px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.workspace-name {
|
|
58
|
+
font-size: 1.1rem;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.workspace-meta {
|
|
63
|
+
font-size: 0.85rem;
|
|
64
|
+
color: var(--theme-fg-more);
|
|
65
|
+
|
|
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;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.workspace-actions {
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 4px;
|
|
82
|
+
|
|
83
|
+
.btn-link {
|
|
84
|
+
padding: 8px;
|
|
85
|
+
color: var(--theme-fg-more);
|
|
86
|
+
opacity: 0.7;
|
|
87
|
+
transition: opacity 0.2s;
|
|
88
|
+
|
|
89
|
+
&:hover {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&.active {
|
|
94
|
+
color: gold;
|
|
95
|
+
opacity: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&.text-danger:hover {
|
|
99
|
+
color: var(--theme-danger);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
|
|
2
|
+
import { ConfigService } from 'tabby-core'
|
|
3
|
+
import { Subscription } from 'rxjs'
|
|
4
|
+
import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
5
|
+
import {
|
|
6
|
+
Workspace,
|
|
7
|
+
WorkspacePane,
|
|
8
|
+
WorkspaceSplit,
|
|
9
|
+
countPanes,
|
|
10
|
+
createDefaultWorkspace,
|
|
11
|
+
isWorkspaceSplit,
|
|
12
|
+
} from '../models/workspace.model'
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'workspace-list',
|
|
16
|
+
template: require('./workspaceList.component.pug'),
|
|
17
|
+
styles: [require('./workspaceList.component.scss')],
|
|
18
|
+
})
|
|
19
|
+
export class WorkspaceListComponent implements OnInit, OnDestroy {
|
|
20
|
+
workspaces: Workspace[] = []
|
|
21
|
+
editingWorkspace: Workspace | null = null
|
|
22
|
+
showEditor = false
|
|
23
|
+
private configSubscription: Subscription | null = null
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
public config: ConfigService,
|
|
27
|
+
private workspaceService: WorkspaceEditorService,
|
|
28
|
+
private cdr: ChangeDetectorRef
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
ngOnInit(): void {
|
|
32
|
+
this.loadWorkspaces()
|
|
33
|
+
this.configSubscription = this.config.changed$.subscribe(() => {
|
|
34
|
+
this.loadWorkspaces()
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ngOnDestroy(): void {
|
|
39
|
+
this.configSubscription?.unsubscribe()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
loadWorkspaces(): void {
|
|
43
|
+
this.workspaces = this.workspaceService.getWorkspaces()
|
|
44
|
+
this.cdr.detectChanges()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async createWorkspace(): Promise<void> {
|
|
48
|
+
const profiles = await this.workspaceService.getAvailableProfiles()
|
|
49
|
+
const defaultProfileId = profiles[0]?.id || ''
|
|
50
|
+
const workspace = createDefaultWorkspace()
|
|
51
|
+
this.setProfileForAllPanes(workspace.root, defaultProfileId)
|
|
52
|
+
this.editingWorkspace = workspace
|
|
53
|
+
this.showEditor = true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private setProfileForAllPanes(node: WorkspacePane | WorkspaceSplit, profileId: string): void {
|
|
57
|
+
if (isWorkspaceSplit(node)) {
|
|
58
|
+
node.children.forEach((child) => this.setProfileForAllPanes(child, profileId))
|
|
59
|
+
} else {
|
|
60
|
+
node.profileId = profileId
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
editWorkspace(workspace: Workspace): void {
|
|
65
|
+
this.editingWorkspace = JSON.parse(JSON.stringify(workspace))
|
|
66
|
+
this.showEditor = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async duplicateWorkspace(workspace: Workspace): Promise<void> {
|
|
70
|
+
const clone = this.workspaceService.duplicateWorkspace(workspace)
|
|
71
|
+
await this.workspaceService.addWorkspace(clone)
|
|
72
|
+
this.loadWorkspaces()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async deleteWorkspace(workspace: Workspace): Promise<void> {
|
|
76
|
+
console.log('[TabbySpaces] deleteWorkspace called', workspace.id)
|
|
77
|
+
if (confirm(`Delete workspace "${workspace.name}"?`)) {
|
|
78
|
+
console.log('[TabbySpaces] confirm = true, calling service.deleteWorkspace')
|
|
79
|
+
await this.workspaceService.deleteWorkspace(workspace.id)
|
|
80
|
+
console.log('[TabbySpaces] service.deleteWorkspace done, calling loadWorkspaces')
|
|
81
|
+
this.loadWorkspaces()
|
|
82
|
+
console.log('[TabbySpaces] loadWorkspaces done, workspaces:', this.workspaces.length)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async onEditorSave(workspace: Workspace): Promise<void> {
|
|
87
|
+
console.log('[TabbySpaces] onEditorSave called', workspace.id, workspace.name)
|
|
88
|
+
const existing = this.workspaces.find((w) => w.id === workspace.id)
|
|
89
|
+
console.log('[TabbySpaces] existing workspace?', !!existing)
|
|
90
|
+
if (existing) {
|
|
91
|
+
await this.workspaceService.updateWorkspace(workspace)
|
|
92
|
+
} else {
|
|
93
|
+
await this.workspaceService.addWorkspace(workspace)
|
|
94
|
+
}
|
|
95
|
+
console.log('[TabbySpaces] save done, calling loadWorkspaces')
|
|
96
|
+
this.loadWorkspaces()
|
|
97
|
+
console.log('[TabbySpaces] calling closeEditor')
|
|
98
|
+
this.closeEditor()
|
|
99
|
+
console.log('[TabbySpaces] closeEditor done, showEditor:', this.showEditor)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
closeEditor(): void {
|
|
103
|
+
console.log('[TabbySpaces] closeEditor called, showEditor before:', this.showEditor)
|
|
104
|
+
this.showEditor = false
|
|
105
|
+
this.editingWorkspace = null
|
|
106
|
+
this.cdr.detectChanges()
|
|
107
|
+
console.log('[TabbySpaces] closeEditor done, showEditor after:', this.showEditor)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getPaneCount(workspace: Workspace): number {
|
|
111
|
+
return countPanes(workspace.root)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getOrientationLabel(workspace: Workspace): string {
|
|
115
|
+
return workspace.root.orientation === 'horizontal' ? 'horizontal' : 'vertical'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async setAsDefault(workspace: Workspace): Promise<void> {
|
|
119
|
+
this.workspaces.forEach((w) => (w.isDefault = false))
|
|
120
|
+
workspace.isDefault = true
|
|
121
|
+
await this.workspaceService.saveWorkspaces(this.workspaces)
|
|
122
|
+
this.loadWorkspaces()
|
|
123
|
+
}
|
|
124
|
+
}
|