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
@@ -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
- async saveWorkspaces(workspaces: Workspace[]): Promise<boolean> {
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.syncTabbyProfiles(workspaces)
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
- const workspaces = this.getWorkspaces()
35
- workspaces.push(workspace)
36
- await this.saveWorkspaces(workspaces)
37
- this.notifications.info(`Workspace "${workspace.name}" created`)
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
- const workspaces = this.getWorkspaces()
42
- const index = workspaces.findIndex((w) => w.id === workspace.id)
43
- if (index !== -1) {
44
- workspaces[index] = workspace
45
- await this.saveWorkspaces(workspaces)
46
- this.notifications.info(`Workspace "${workspace.name}" updated`)
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
- const filtered = workspaces.filter((w) => w.id !== workspaceId)
54
- await this.saveWorkspaces(filtered)
55
- if (workspace) {
56
- this.notifications.info(`Workspace "${workspace.name}" deleted`)
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) => p.type === 'local' && !p.id?.startsWith('split-layout:')
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
- private syncTabbyProfiles(workspaces: Workspace[]): void {
68
- const profiles: (TabbyProfile | TabbySplitLayoutProfile)[] = this.config.store.profiles ?? []
69
-
70
- // Remove old plugin profiles (mutate in place)
71
- for (let i = profiles.length - 1; i >= 0; i--) {
72
- if (profiles[i].id?.startsWith(`split-layout:${CONFIG_KEY}:`)) {
73
- profiles.splice(i, 1)
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
- // Add new workspace profiles
78
- for (const workspace of workspaces) {
79
- const tabbyProfile = this.generateTabbyProfile(workspace)
80
- profiles.push(tabbyProfile)
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
- generateTabbyProfile(workspace: Workspace): TabbySplitLayoutProfile {
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
- // Handle startup command for different shells
142
- if (pane.startupCommand) {
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: false,
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: pane.title || '',
176
- tabCustomTitle: pane.title || '',
177
- disableDynamicTitle: !!pane.title,
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 = JSON.parse(JSON.stringify(workspace)) as Workspace
225
+ const clone = deepClone(workspace)
183
226
  clone.id = generateUUID()
184
227
  clone.name = `${workspace.name} (Copy)`
185
- clone.isDefault = false
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 profiles: TabbyProfile[] = this.config.store.profiles ?? []
211
- return profiles.find((p) => p.id === profileId && p.type === 'local')
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
- getProfileName(profileId: string): string | undefined {
215
- return this.getProfileById(profileId)?.name
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<boolean> {
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
- return false
294
+ throw error
226
295
  }
227
296
  }
228
297
  }
@@ -0,0 +1,3 @@
1
+ // Main entry point for shared styles
2
+ @forward 'variables';
3
+ @forward 'mixins';
@@ -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;