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.
- package/.claude/settings.local.json +29 -2
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +81 -0
- package/.github/workflows/release.yml +30 -0
- package/CHANGELOG.md +92 -20
- package/CLAUDE.md +196 -15
- package/CONTRIBUTING.md +3 -1
- package/README.md +80 -61
- package/RELEASE.md +91 -0
- package/TODO.md +77 -0
- package/dist/build-config.d.ts +3 -3
- package/dist/components/deleteConfirmModal.component.d.ts +7 -0
- package/dist/components/deleteConfirmModal.component.d.ts.map +1 -0
- package/dist/components/paneEditor.component.d.ts +9 -13
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +50 -35
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +61 -28
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +56 -27
- package/dist/components/workspaceList.component.d.ts.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.LICENSE.txt +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models/workspace.model.d.ts +118 -76
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +26 -0
- package/dist/providers/config.provider.d.ts +8 -8
- package/dist/providers/settings.provider.d.ts +7 -7
- package/dist/providers/settings.provider.d.ts.map +1 -1
- package/dist/providers/toolbar.provider.d.ts +23 -12
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +28 -0
- package/dist/services/startupCommand.service.d.ts.map +1 -0
- package/dist/services/workspaceBackground.service.d.ts +38 -0
- package/dist/services/workspaceBackground.service.d.ts.map +1 -0
- package/dist/services/workspaceEditor.service.d.ts +46 -24
- package/dist/services/workspaceEditor.service.d.ts.map +1 -1
- package/docs/DESIGN.md +57 -0
- package/docs/SESSION-2026-01-14-S1-DESIGN.md +134 -0
- package/docs/marketing_status.md +92 -0
- package/mockups/index.html +162 -0
- package/mockups/s1-tight-sharp.html +522 -0
- package/mockups/shared/base.css +216 -0
- package/mockups/v06-tabbed.html +643 -0
- package/package.json +3 -7
- package/screenshots/editor.png +0 -0
- package/screenshots/pane-edit.png +0 -0
- package/scripts/build-dev.js +2 -1
- package/scripts/build-prod.js +40 -0
- package/src/components/deleteConfirmModal.component.ts +23 -0
- package/src/components/paneEditor.component.pug +27 -43
- package/src/components/paneEditor.component.scss +37 -85
- package/src/components/paneEditor.component.ts +6 -16
- package/src/components/splitPreview.component.pug +36 -5
- package/src/components/splitPreview.component.scss +78 -45
- package/src/components/splitPreview.component.ts +83 -18
- package/src/components/workspaceEditor.component.pug +162 -74
- package/src/components/workspaceEditor.component.scss +261 -108
- package/src/components/workspaceEditor.component.ts +294 -31
- package/src/components/workspaceList.component.pug +32 -41
- package/src/components/workspaceList.component.scss +89 -74
- package/src/components/workspaceList.component.ts +181 -44
- package/src/index.ts +6 -0
- package/src/models/workspace.model.ts +113 -8
- package/src/providers/settings.provider.ts +2 -2
- package/src/providers/toolbar.provider.ts +113 -13
- package/src/services/startupCommand.service.ts +140 -0
- package/src/services/workspaceBackground.service.ts +167 -0
- package/src/services/workspaceEditor.service.ts +134 -65
- package/src/styles/_index.scss +3 -0
- package/src/styles/_mixins.scss +180 -0
- package/src/styles/_variables.scss +67 -0
- package/RELEASE_PLAN.md +0 -161
- package/screenshots/workspace-edit.png +0 -0
|
@@ -6,82 +6,128 @@ import {
|
|
|
6
6
|
WorkspaceSplit,
|
|
7
7
|
isWorkspaceSplit,
|
|
8
8
|
generateUUID,
|
|
9
|
+
deepClone,
|
|
9
10
|
TabbyProfile,
|
|
10
11
|
TabbyRecoveryToken,
|
|
11
12
|
TabbySplitLayoutProfile,
|
|
12
13
|
} from '../models/workspace.model'
|
|
13
14
|
import { CONFIG_KEY, DISPLAY_NAME } from '../build-config'
|
|
15
|
+
import { PendingCommand } from './startupCommand.service'
|
|
14
16
|
|
|
15
17
|
@Injectable({ providedIn: 'root' })
|
|
16
18
|
export class WorkspaceEditorService {
|
|
19
|
+
private cachedProfiles: TabbyProfile[] | null = null
|
|
20
|
+
private cacheTimestamp: number = 0
|
|
21
|
+
private readonly CACHE_TTL = 30000 // 30 seconds
|
|
22
|
+
|
|
17
23
|
constructor(
|
|
18
24
|
private config: ConfigService,
|
|
19
25
|
private notifications: NotificationsService,
|
|
20
26
|
private profilesService: ProfilesService
|
|
21
27
|
) {}
|
|
22
28
|
|
|
29
|
+
private async getCachedProfiles(): Promise<TabbyProfile[]> {
|
|
30
|
+
const now = Date.now()
|
|
31
|
+
if (!this.cachedProfiles || now - this.cacheTimestamp > this.CACHE_TTL) {
|
|
32
|
+
this.cachedProfiles = (await this.profilesService.getProfiles()) as TabbyProfile[]
|
|
33
|
+
this.cacheTimestamp = now
|
|
34
|
+
}
|
|
35
|
+
return this.cachedProfiles
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returns all saved workspaces from config. */
|
|
23
39
|
getWorkspaces(): Workspace[] {
|
|
24
|
-
return this.config.store[CONFIG_KEY]?.workspaces ?? []
|
|
40
|
+
return this.config.store?.[CONFIG_KEY]?.workspaces ?? []
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Saves the workspace list to config.
|
|
45
|
+
* @throws Error if config store is not initialized
|
|
46
|
+
*/
|
|
47
|
+
async saveWorkspaces(workspaces: Workspace[]): Promise<void> {
|
|
48
|
+
if (!this.config.store?.[CONFIG_KEY]) {
|
|
49
|
+
throw new Error('Config store not initialized')
|
|
50
|
+
}
|
|
28
51
|
this.config.store[CONFIG_KEY].workspaces = workspaces
|
|
29
|
-
this.
|
|
30
|
-
return await this.saveConfig()
|
|
52
|
+
await this.saveConfig()
|
|
31
53
|
}
|
|
32
54
|
|
|
55
|
+
/** Adds a new workspace and shows notification. */
|
|
33
56
|
async addWorkspace(workspace: Workspace): Promise<void> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
try {
|
|
58
|
+
const workspaces = this.getWorkspaces()
|
|
59
|
+
workspaces.push(workspace)
|
|
60
|
+
await this.saveWorkspaces(workspaces)
|
|
61
|
+
this.notifications.info(`Workspace "${workspace.name}" created`)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.notifications.error(`Failed to create workspace "${workspace.name}"`)
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
38
66
|
}
|
|
39
67
|
|
|
68
|
+
/** Updates an existing workspace by ID and shows notification. */
|
|
40
69
|
async updateWorkspace(workspace: Workspace): Promise<void> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
try {
|
|
71
|
+
const workspaces = this.getWorkspaces()
|
|
72
|
+
const index = workspaces.findIndex((w) => w.id === workspace.id)
|
|
73
|
+
if (index !== -1) {
|
|
74
|
+
workspaces[index] = workspace
|
|
75
|
+
await this.saveWorkspaces(workspaces)
|
|
76
|
+
this.notifications.info(`Workspace "${workspace.name}" updated`)
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.notifications.error(`Failed to update workspace "${workspace.name}"`)
|
|
80
|
+
throw error
|
|
47
81
|
}
|
|
48
82
|
}
|
|
49
83
|
|
|
84
|
+
/** Deletes a workspace by ID and shows notification. */
|
|
50
85
|
async deleteWorkspace(workspaceId: string): Promise<void> {
|
|
51
86
|
const workspaces = this.getWorkspaces()
|
|
52
87
|
const workspace = workspaces.find((w) => w.id === workspaceId)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
88
|
+
try {
|
|
89
|
+
const filtered = workspaces.filter((w) => w.id !== workspaceId)
|
|
90
|
+
await this.saveWorkspaces(filtered)
|
|
91
|
+
if (workspace) {
|
|
92
|
+
this.notifications.info(`Workspace "${workspace.name}" deleted`)
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.notifications.error(`Failed to delete workspace "${workspace?.name || workspaceId}"`)
|
|
96
|
+
throw error
|
|
57
97
|
}
|
|
58
98
|
}
|
|
59
99
|
|
|
100
|
+
/** Returns all local shell profiles available for use in workspaces. */
|
|
60
101
|
async getAvailableProfiles(): Promise<TabbyProfile[]> {
|
|
61
102
|
const allProfiles = await this.profilesService.getProfiles()
|
|
62
103
|
return allProfiles.filter(
|
|
63
|
-
(p) =>
|
|
104
|
+
(p) =>
|
|
105
|
+
(p.type === 'local' || p.type?.startsWith('local:')) &&
|
|
106
|
+
!p.id?.startsWith('split-layout:')
|
|
64
107
|
) as TabbyProfile[]
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
110
|
+
/**
|
|
111
|
+
* Cleanup orphaned profiles from previous plugin versions.
|
|
112
|
+
* Call this once on plugin init.
|
|
113
|
+
*/
|
|
114
|
+
cleanupOrphanedProfiles(): void {
|
|
115
|
+
if (!this.config.store?.profiles) {
|
|
116
|
+
return
|
|
75
117
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
profiles
|
|
118
|
+
const profiles: TabbyProfile[] = this.config.store.profiles
|
|
119
|
+
const prefix = `split-layout:${CONFIG_KEY}:`
|
|
120
|
+
const filtered = profiles.filter((p) => !p.id?.startsWith(prefix))
|
|
121
|
+
if (filtered.length !== profiles.length) {
|
|
122
|
+
this.config.store.profiles = filtered
|
|
123
|
+
this.config.save()
|
|
124
|
+
console.log(`[${DISPLAY_NAME}] Cleaned up ${profiles.length - filtered.length} orphaned profiles`)
|
|
81
125
|
}
|
|
82
126
|
}
|
|
83
127
|
|
|
84
|
-
|
|
128
|
+
/** Generates a Tabby split-layout profile from a workspace for opening. */
|
|
129
|
+
async generateTabbyProfile(workspace: Workspace): Promise<TabbySplitLayoutProfile> {
|
|
130
|
+
await this.getCachedProfiles()
|
|
85
131
|
const safeName = this.sanitizeForProfileId(workspace.name)
|
|
86
132
|
return {
|
|
87
133
|
id: `split-layout:${CONFIG_KEY}:${safeName}:${workspace.id}`,
|
|
@@ -92,26 +138,27 @@ export class WorkspaceEditorService {
|
|
|
92
138
|
color: workspace.color,
|
|
93
139
|
isBuiltin: false,
|
|
94
140
|
options: {
|
|
95
|
-
recoveryToken: this.generateRecoveryToken(workspace.root),
|
|
141
|
+
recoveryToken: this.generateRecoveryToken(workspace.root, workspace.name, workspace.id),
|
|
96
142
|
},
|
|
97
143
|
}
|
|
98
144
|
}
|
|
99
145
|
|
|
100
|
-
private generateRecoveryToken(split: WorkspaceSplit): TabbyRecoveryToken {
|
|
146
|
+
private generateRecoveryToken(split: WorkspaceSplit, workspaceName: string, workspaceId: string): TabbyRecoveryToken {
|
|
101
147
|
return {
|
|
102
148
|
type: 'app:split-tab',
|
|
103
149
|
orientation: split.orientation === 'horizontal' ? 'h' : 'v',
|
|
104
150
|
ratios: split.ratios,
|
|
151
|
+
workspaceId,
|
|
105
152
|
children: split.children.map((child) => {
|
|
106
153
|
if (isWorkspaceSplit(child)) {
|
|
107
|
-
return this.generateRecoveryToken(child)
|
|
154
|
+
return this.generateRecoveryToken(child, workspaceName, workspaceId)
|
|
108
155
|
}
|
|
109
|
-
return this.generatePaneToken(child)
|
|
156
|
+
return this.generatePaneToken(child, workspaceName, workspaceId)
|
|
110
157
|
}),
|
|
111
158
|
}
|
|
112
159
|
}
|
|
113
160
|
|
|
114
|
-
private generatePaneToken(pane: WorkspacePane): TabbyRecoveryToken {
|
|
161
|
+
private generatePaneToken(pane: WorkspacePane, workspaceName: string, workspaceId: string): TabbyRecoveryToken {
|
|
115
162
|
const baseProfile = this.getProfileById(pane.profileId)
|
|
116
163
|
|
|
117
164
|
if (!baseProfile) {
|
|
@@ -138,19 +185,8 @@ export class WorkspaceEditorService {
|
|
|
138
185
|
runAsAdministrator: false,
|
|
139
186
|
}
|
|
140
187
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
const cmd = baseProfile.options?.command || ''
|
|
144
|
-
if (cmd.includes('nu.exe') || baseProfile.name?.toLowerCase().includes('nushell')) {
|
|
145
|
-
options.args = ['-e', pane.startupCommand]
|
|
146
|
-
} else if (cmd.includes('powershell') || cmd.includes('pwsh')) {
|
|
147
|
-
options.args = ['-NoExit', '-Command', pane.startupCommand]
|
|
148
|
-
} else if (cmd.includes('cmd.exe')) {
|
|
149
|
-
options.args = ['/K', pane.startupCommand]
|
|
150
|
-
} else {
|
|
151
|
-
options.args = ['-c', pane.startupCommand]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
188
|
+
// Note: startupCommand is handled via sendInput() in StartupCommandService
|
|
189
|
+
// to avoid re-execution when Tabby splits the pane
|
|
154
190
|
|
|
155
191
|
const profile = {
|
|
156
192
|
id: baseProfile.id,
|
|
@@ -160,7 +196,7 @@ export class WorkspaceEditorService {
|
|
|
160
196
|
options,
|
|
161
197
|
icon: baseProfile.icon || '',
|
|
162
198
|
color: baseProfile.color || '',
|
|
163
|
-
disableDynamicTitle:
|
|
199
|
+
disableDynamicTitle: true,
|
|
164
200
|
weight: 0,
|
|
165
201
|
isBuiltin: false,
|
|
166
202
|
isTemplate: false,
|
|
@@ -168,21 +204,28 @@ export class WorkspaceEditorService {
|
|
|
168
204
|
behaviorOnSessionEnd: 'auto',
|
|
169
205
|
}
|
|
170
206
|
|
|
207
|
+
// tabTitle: workspace name (what user sees)
|
|
208
|
+
// tabCustomTitle: pane.id (for matching in StartupCommandService)
|
|
209
|
+
// workspaceId: for duplicate detection after Tabby recovery
|
|
210
|
+
const cwd = pane.cwd || baseProfile.options?.cwd || ''
|
|
171
211
|
return {
|
|
172
212
|
type: 'app:local-tab',
|
|
173
213
|
profile,
|
|
174
214
|
savedState: false,
|
|
175
|
-
tabTitle:
|
|
176
|
-
tabCustomTitle: pane.
|
|
177
|
-
|
|
215
|
+
tabTitle: workspaceName,
|
|
216
|
+
tabCustomTitle: pane.id,
|
|
217
|
+
workspaceId,
|
|
218
|
+
disableDynamicTitle: true,
|
|
219
|
+
cwd,
|
|
178
220
|
}
|
|
179
221
|
}
|
|
180
222
|
|
|
223
|
+
/** Creates a deep copy of a workspace with new IDs. */
|
|
181
224
|
duplicateWorkspace(workspace: Workspace): Workspace {
|
|
182
|
-
const clone =
|
|
225
|
+
const clone = deepClone(workspace)
|
|
183
226
|
clone.id = generateUUID()
|
|
184
227
|
clone.name = `${workspace.name} (Copy)`
|
|
185
|
-
clone.
|
|
228
|
+
clone.launchOnStartup = false
|
|
186
229
|
this.regenerateIds(clone.root)
|
|
187
230
|
return clone
|
|
188
231
|
}
|
|
@@ -207,22 +250,48 @@ export class WorkspaceEditorService {
|
|
|
207
250
|
}
|
|
208
251
|
|
|
209
252
|
private getProfileById(profileId: string): TabbyProfile | undefined {
|
|
210
|
-
const
|
|
211
|
-
|
|
253
|
+
const isLocalType = (type: string) => type === 'local' || type?.startsWith('local:')
|
|
254
|
+
|
|
255
|
+
// First: check user profiles in config
|
|
256
|
+
const userProfiles: TabbyProfile[] = this.config.store?.profiles ?? []
|
|
257
|
+
const found = userProfiles.find((p) => p.id === profileId && isLocalType(p.type))
|
|
258
|
+
if (found) return found
|
|
259
|
+
|
|
260
|
+
// Fallback: check cached profiles (includes built-ins)
|
|
261
|
+
return this.cachedProfiles?.find((p) => p.id === profileId && isLocalType(p.type))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Collects all startup commands from panes in a workspace. */
|
|
265
|
+
collectStartupCommands(workspace: Workspace): PendingCommand[] {
|
|
266
|
+
const commands: PendingCommand[] = []
|
|
267
|
+
this.collectCommandsFromNode(workspace.root, workspace.name, commands)
|
|
268
|
+
return commands
|
|
212
269
|
}
|
|
213
270
|
|
|
214
|
-
|
|
215
|
-
|
|
271
|
+
private collectCommandsFromNode(
|
|
272
|
+
node: WorkspacePane | WorkspaceSplit,
|
|
273
|
+
workspaceName: string,
|
|
274
|
+
commands: PendingCommand[]
|
|
275
|
+
): void {
|
|
276
|
+
if (isWorkspaceSplit(node)) {
|
|
277
|
+
for (const child of node.children) {
|
|
278
|
+
this.collectCommandsFromNode(child, workspaceName, commands)
|
|
279
|
+
}
|
|
280
|
+
} else if (node.startupCommand) {
|
|
281
|
+
commands.push({
|
|
282
|
+
paneId: node.id,
|
|
283
|
+
command: node.startupCommand,
|
|
284
|
+
originalTitle: workspaceName,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
216
287
|
}
|
|
217
288
|
|
|
218
|
-
private async saveConfig(): Promise<
|
|
289
|
+
private async saveConfig(): Promise<void> {
|
|
219
290
|
try {
|
|
220
291
|
await this.config.save()
|
|
221
|
-
return true
|
|
222
292
|
} catch (error) {
|
|
223
|
-
this.notifications.error('Failed to save configuration')
|
|
224
293
|
console.error('TabbySpaces save error:', error)
|
|
225
|
-
|
|
294
|
+
throw error
|
|
226
295
|
}
|
|
227
296
|
}
|
|
228
297
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
@use 'variables' as *;
|
|
2
|
+
|
|
3
|
+
// ======================
|
|
4
|
+
// LAYOUT MIXINS
|
|
5
|
+
// ======================
|
|
6
|
+
|
|
7
|
+
@mixin flex-center {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ======================
|
|
14
|
+
// FORM INPUT MIXIN
|
|
15
|
+
// ======================
|
|
16
|
+
|
|
17
|
+
@mixin form-input($bg: var(--theme-bg)) {
|
|
18
|
+
padding: $spacing-sm $spacing-md;
|
|
19
|
+
border-radius: $radius-sm;
|
|
20
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
21
|
+
background: $bg;
|
|
22
|
+
color: var(--theme-fg);
|
|
23
|
+
font-size: $font-md;
|
|
24
|
+
|
|
25
|
+
&:focus {
|
|
26
|
+
outline: none;
|
|
27
|
+
border-color: var(--theme-primary);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&::placeholder {
|
|
31
|
+
color: var(--theme-fg-more, $fallback-fg-more);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ======================
|
|
36
|
+
// FORM LABEL MIXIN (S1: Uppercase compact)
|
|
37
|
+
// ======================
|
|
38
|
+
|
|
39
|
+
@mixin form-label {
|
|
40
|
+
display: block;
|
|
41
|
+
font-size: $font-xs;
|
|
42
|
+
color: var(--theme-fg-less);
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.5px;
|
|
45
|
+
margin-bottom: $spacing-xs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ======================
|
|
49
|
+
// BUTTON MIXINS
|
|
50
|
+
// ======================
|
|
51
|
+
|
|
52
|
+
@mixin toolbar-btn {
|
|
53
|
+
padding: $spacing-sm $spacing-md;
|
|
54
|
+
background: var(--theme-bg);
|
|
55
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
56
|
+
border-radius: $radius-sm;
|
|
57
|
+
color: var(--theme-fg);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
transition: all $transition-fast;
|
|
60
|
+
font-size: 0.85rem;
|
|
61
|
+
|
|
62
|
+
&:hover:not(:disabled) {
|
|
63
|
+
background: var(--theme-bg-more-more);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:disabled {
|
|
67
|
+
opacity: 0.4;
|
|
68
|
+
cursor: not-allowed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&.danger {
|
|
72
|
+
color: var(--theme-danger, $color-danger);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@mixin btn-success {
|
|
77
|
+
background: $color-success;
|
|
78
|
+
border-color: $color-success;
|
|
79
|
+
color: white;
|
|
80
|
+
|
|
81
|
+
&:hover {
|
|
82
|
+
background: $color-success-hover;
|
|
83
|
+
border-color: $color-success-hover;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@mixin btn-base {
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: $spacing-sm;
|
|
91
|
+
padding: $spacing-sm $spacing-lg;
|
|
92
|
+
font-size: $font-sm;
|
|
93
|
+
border-radius: $radius-sm;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
transition: all $transition-fast;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@mixin btn-ghost {
|
|
99
|
+
@include btn-base;
|
|
100
|
+
background: transparent;
|
|
101
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
102
|
+
color: var(--theme-fg);
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
background: var(--theme-bg-more);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@mixin btn-primary {
|
|
110
|
+
@include btn-base;
|
|
111
|
+
background: var(--theme-primary);
|
|
112
|
+
border: 1px solid var(--theme-primary);
|
|
113
|
+
color: white;
|
|
114
|
+
|
|
115
|
+
&:hover {
|
|
116
|
+
filter: brightness(1.1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@mixin icon-btn-sm($size: 24px) {
|
|
121
|
+
width: $size;
|
|
122
|
+
height: $size;
|
|
123
|
+
@include flex-center;
|
|
124
|
+
background: var(--theme-bg);
|
|
125
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
126
|
+
border-radius: $radius-sm;
|
|
127
|
+
color: var(--theme-fg-more, $fallback-fg-more);
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
transition: all $transition-fast;
|
|
131
|
+
|
|
132
|
+
&:hover:not(:disabled) {
|
|
133
|
+
border-color: var(--theme-primary);
|
|
134
|
+
color: var(--theme-primary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
&.danger:hover:not(:disabled) {
|
|
138
|
+
border-color: var(--theme-danger);
|
|
139
|
+
color: var(--theme-danger);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
&:disabled {
|
|
143
|
+
opacity: 0.4;
|
|
144
|
+
cursor: not-allowed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ======================
|
|
149
|
+
// OVERLAY/MODAL MIXINS
|
|
150
|
+
// ======================
|
|
151
|
+
|
|
152
|
+
@mixin full-overlay($z-index: $z-modal-overlay) {
|
|
153
|
+
position: fixed;
|
|
154
|
+
top: 0;
|
|
155
|
+
left: 0;
|
|
156
|
+
right: 0;
|
|
157
|
+
bottom: 0;
|
|
158
|
+
z-index: $z-index;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ======================
|
|
162
|
+
// TEXT UTILITIES
|
|
163
|
+
// ======================
|
|
164
|
+
|
|
165
|
+
@mixin text-ellipsis {
|
|
166
|
+
white-space: nowrap;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
text-overflow: ellipsis;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ======================
|
|
172
|
+
// DROPDOWN/POPUP
|
|
173
|
+
// ======================
|
|
174
|
+
|
|
175
|
+
@mixin dropdown-panel {
|
|
176
|
+
background: var(--theme-bg-more);
|
|
177
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
178
|
+
border-radius: $radius-md;
|
|
179
|
+
box-shadow: $shadow-dropdown;
|
|
180
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ======================
|
|
2
|
+
// SPACING SCALE (S1: Tight)
|
|
3
|
+
// ======================
|
|
4
|
+
$spacing-xs: 2px; // toolbar group gap, tiny margins
|
|
5
|
+
$spacing-sm: 4px; // gaps, small padding
|
|
6
|
+
$spacing-md: 6px; // default gaps, button padding
|
|
7
|
+
$spacing-lg: 10px; // section gaps, form spacing
|
|
8
|
+
$spacing-xl: 14px; // container padding, large gaps
|
|
9
|
+
$spacing-2xl: 18px; // modal padding, section margins
|
|
10
|
+
|
|
11
|
+
// ======================
|
|
12
|
+
// BORDER RADIUS SCALE (S1: Sharp)
|
|
13
|
+
// ======================
|
|
14
|
+
$radius-xs: 2px; // tiny elements
|
|
15
|
+
$radius-sm: 2px; // buttons, small elements
|
|
16
|
+
$radius-md: 3px; // inputs, dropdowns
|
|
17
|
+
$radius-lg: 4px; // cards, containers, modals
|
|
18
|
+
|
|
19
|
+
// ======================
|
|
20
|
+
// CUSTOM COLORS
|
|
21
|
+
// ======================
|
|
22
|
+
// These extend Tabby's --theme-* variables
|
|
23
|
+
$color-success: #10b981;
|
|
24
|
+
$color-success-hover: #059669;
|
|
25
|
+
$color-warning: #f59e0b;
|
|
26
|
+
$color-danger: #ef4444;
|
|
27
|
+
|
|
28
|
+
// Fallback values for Tabby theme variables that may not be defined
|
|
29
|
+
$fallback-border: rgba(255, 255, 255, 0.1);
|
|
30
|
+
$fallback-fg-more: rgba(255, 255, 255, 0.3);
|
|
31
|
+
|
|
32
|
+
// ======================
|
|
33
|
+
// SHADOWS (S1: Flat - no shadows)
|
|
34
|
+
// ======================
|
|
35
|
+
$shadow-dropdown: none;
|
|
36
|
+
$shadow-context-menu: none;
|
|
37
|
+
$shadow-modal: none;
|
|
38
|
+
$overlay-bg: rgba(0, 0, 0, 0.7);
|
|
39
|
+
|
|
40
|
+
// ======================
|
|
41
|
+
// PREVIEW COMPONENT
|
|
42
|
+
// ======================
|
|
43
|
+
$preview-height: 140px;
|
|
44
|
+
$nested-split-bg: rgba(255, 255, 255, 0.02);
|
|
45
|
+
$selected-pane-gradient-start: rgba(59, 130, 246, 0.08);
|
|
46
|
+
$selected-pane-gradient-end: rgba(59, 130, 246, 0.04);
|
|
47
|
+
|
|
48
|
+
// ======================
|
|
49
|
+
// FONT SIZES (S1: Compact)
|
|
50
|
+
// ======================
|
|
51
|
+
$font-xs: 0.7rem; // 11px - sublabels, hints
|
|
52
|
+
$font-sm: 0.8rem; // 13px - labels, secondary text
|
|
53
|
+
$font-md: 0.85rem; // 14px - default body
|
|
54
|
+
|
|
55
|
+
// ======================
|
|
56
|
+
// Z-INDEX SCALE
|
|
57
|
+
// ======================
|
|
58
|
+
$z-dropdown: 100;
|
|
59
|
+
$z-modal-overlay: 1100;
|
|
60
|
+
$z-context-menu-overlay: 1200;
|
|
61
|
+
$z-context-menu: 1201;
|
|
62
|
+
|
|
63
|
+
// ======================
|
|
64
|
+
// TRANSITIONS
|
|
65
|
+
// ======================
|
|
66
|
+
$transition-fast: 0.15s;
|
|
67
|
+
$transition-normal: 0.2s;
|