gh-here 3.0.3 → 3.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 (47) hide show
  1. package/.env +0 -0
  2. package/lib/constants.js +21 -16
  3. package/lib/content-search.js +212 -0
  4. package/lib/error-handler.js +39 -28
  5. package/lib/file-utils.js +438 -287
  6. package/lib/git.js +11 -55
  7. package/lib/gitignore.js +70 -41
  8. package/lib/renderers.js +17 -33
  9. package/lib/server.js +73 -196
  10. package/lib/symbol-parser.js +600 -0
  11. package/package.json +1 -1
  12. package/public/app.js +135 -68
  13. package/public/css/components/buttons.css +423 -0
  14. package/public/css/components/forms.css +171 -0
  15. package/public/css/components/modals.css +286 -0
  16. package/public/css/components/notifications.css +36 -0
  17. package/public/css/file-table.css +318 -0
  18. package/public/css/file-tree.css +269 -0
  19. package/public/css/file-viewer.css +1259 -0
  20. package/public/css/layout.css +372 -0
  21. package/public/css/main.css +35 -0
  22. package/public/css/reset.css +64 -0
  23. package/public/css/search.css +694 -0
  24. package/public/css/symbol-outline.css +279 -0
  25. package/public/css/variables.css +135 -0
  26. package/public/js/constants.js +50 -34
  27. package/public/js/content-search-handler.js +551 -0
  28. package/public/js/file-viewer.js +437 -0
  29. package/public/js/focus-mode.js +280 -0
  30. package/public/js/inline-search.js +659 -0
  31. package/public/js/modal-manager.js +14 -28
  32. package/public/js/symbol-outline.js +454 -0
  33. package/public/js/utils.js +152 -94
  34. package/.claude/settings.local.json +0 -30
  35. package/SAMPLE.md +0 -287
  36. package/lib/validation.js +0 -77
  37. package/public/app.js.backup +0 -1902
  38. package/public/highlight.css +0 -121
  39. package/public/js/draft-manager.js +0 -36
  40. package/public/js/editor-manager.js +0 -159
  41. package/public/styles.css +0 -2727
  42. package/test.js +0 -138
  43. package/tests/draftManager.test.js +0 -241
  44. package/tests/fileTypeDetection.test.js +0 -111
  45. package/tests/httpService.test.js +0 -268
  46. package/tests/languageDetection.test.js +0 -145
  47. package/tests/pathUtils.test.js +0 -136
package/public/app.js CHANGED
@@ -1,16 +1,30 @@
1
1
  /**
2
2
  * Main application entry point
3
3
  * Coordinates all modules and initializes the application
4
+ * @module app
4
5
  */
5
6
 
6
- import { ThemeManager } from './js/theme-manager.js';
7
- import { SearchHandler } from './js/search-handler.js';
8
- import { KeyboardHandler } from './js/keyboard-handler.js';
7
+ // ============================================================================
8
+ // Imports (alpha-sorted)
9
+ // ============================================================================
10
+
11
+ import { ContentSearchHandler } from './js/content-search-handler.js';
12
+ import { copyToClipboard } from './js/clipboard-utils.js';
9
13
  import { FileTreeNavigator } from './js/file-tree.js';
14
+ import { FileViewer } from './js/file-viewer.js';
15
+ import { FocusMode } from './js/focus-mode.js';
16
+ import { InlineSearch } from './js/inline-search.js';
17
+ import { KeyboardHandler } from './js/keyboard-handler.js';
10
18
  import { NavigationHandler } from './js/navigation.js';
11
19
  import { PathUtils } from './js/utils.js';
20
+ import { SearchHandler } from './js/search-handler.js';
12
21
  import { showNotification } from './js/notification.js';
13
- import { copyToClipboard } from './js/clipboard-utils.js';
22
+ import { SymbolOutline } from './js/symbol-outline.js';
23
+ import { ThemeManager } from './js/theme-manager.js';
24
+
25
+ // ============================================================================
26
+ // Application Class
27
+ // ============================================================================
14
28
 
15
29
  class Application {
16
30
  constructor() {
@@ -20,6 +34,11 @@ class Application {
20
34
  this.fileTree = null;
21
35
  this.navigationHandler = null;
22
36
  this.lastSelectedLine = null;
37
+ this.fileViewer = null;
38
+ this.inlineSearch = null;
39
+ this.focusMode = null;
40
+ this.contentSearch = null;
41
+ this.symbolOutline = null;
23
42
  }
24
43
 
25
44
  init() {
@@ -32,6 +51,9 @@ class Application {
32
51
  // Re-initialize components after client-side navigation
33
52
  document.addEventListener('content-loaded', () => {
34
53
  try {
54
+ // Cleanup existing components before re-initializing
55
+ this.cleanupComponents();
56
+
35
57
  // Re-initialize theme manager listeners (button might be re-rendered)
36
58
  if (this.themeManager) {
37
59
  this.themeManager.setupListeners();
@@ -57,8 +79,12 @@ class Application {
57
79
 
58
80
  this.setupGlobalEventListeners();
59
81
  this.setupGitignoreToggle();
60
- this.setupFileOperations();
61
82
  this.highlightLinesFromHash();
83
+ this.initializeFileViewer();
84
+ this.initializeInlineSearch();
85
+ this.initializeFocusMode();
86
+ this.initializeContentSearch();
87
+ this.initializeSymbolOutline();
62
88
  } catch (error) {
63
89
  console.error('Error re-initializing components:', error);
64
90
  }
@@ -84,8 +110,84 @@ class Application {
84
110
 
85
111
  this.setupGlobalEventListeners();
86
112
  this.setupGitignoreToggle();
87
- this.setupFileOperations();
88
113
  this.highlightLinesFromHash();
114
+ this.initializeFileViewer();
115
+ this.initializeInlineSearch();
116
+ this.initializeFocusMode();
117
+ this.initializeContentSearch();
118
+ this.initializeSymbolOutline();
119
+ }
120
+
121
+ // ========================================================================
122
+ // Component Initialization
123
+ // ========================================================================
124
+
125
+ /**
126
+ * Check if current page is a file view page
127
+ */
128
+ isFileViewPage() {
129
+ const fileContent = document.querySelector('.file-content');
130
+ return fileContent?.querySelector('pre code.hljs.with-line-numbers') !== null;
131
+ }
132
+
133
+ /**
134
+ * Initialize Monaco-based file viewer
135
+ */
136
+ initializeFileViewer() {
137
+ if (!this.isFileViewPage()) return;
138
+
139
+ // Wait for Monaco to be ready
140
+ if (typeof require === 'undefined' || !window.monacoReady) {
141
+ // Wait a bit and try again
142
+ setTimeout(() => this.initializeFileViewer(), 100);
143
+ return;
144
+ }
145
+
146
+ if (!this.fileViewer) {
147
+ this.fileViewer = new FileViewer();
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Initialize inline search component
153
+ * Note: Skipped when Monaco viewer is active (Monaco has built-in search)
154
+ */
155
+ initializeInlineSearch() {
156
+ if (!this.isFileViewPage()) return;
157
+
158
+ // Skip if Monaco viewer is active
159
+ if (document.querySelector('.monaco-file-viewer')) {
160
+ return;
161
+ }
162
+
163
+ if (!this.inlineSearch) {
164
+ this.inlineSearch = new InlineSearch();
165
+ }
166
+ this.inlineSearch.init();
167
+ }
168
+
169
+ initializeFocusMode() {
170
+ if (!this.focusMode) {
171
+ this.focusMode = new FocusMode();
172
+ }
173
+ this.focusMode.init();
174
+ }
175
+
176
+ /**
177
+ * Initialize symbol outline panel
178
+ */
179
+ initializeSymbolOutline() {
180
+ // Cleanup previous instance
181
+ if (this.symbolOutline) {
182
+ this.symbolOutline.destroy();
183
+ this.symbolOutline = null;
184
+ }
185
+
186
+ // Only initialize on file view pages
187
+ if (!this.isFileViewPage()) return;
188
+
189
+ this.symbolOutline = new SymbolOutline();
190
+ this.symbolOutline.init();
89
191
  }
90
192
 
91
193
  setupGlobalEventListeners() {
@@ -193,68 +295,6 @@ class Application {
193
295
  }
194
296
 
195
297
 
196
- setupFileOperations() {
197
- document.addEventListener('click', async e => {
198
- if (e.target.closest('.delete-btn')) {
199
- const btn = e.target.closest('.delete-btn');
200
- const itemPath = btn.dataset.path;
201
- const itemName = btn.dataset.name;
202
- const isDirectory = btn.dataset.isDirectory === 'true';
203
-
204
- const message = `Are you sure you want to delete ${isDirectory ? 'folder' : 'file'} "${itemName}"?`;
205
- if (!confirm(message)) {
206
- return;
207
- }
208
-
209
- try {
210
- const response = await fetch('/api/delete', {
211
- method: 'POST',
212
- headers: { 'Content-Type': 'application/json' },
213
- body: JSON.stringify({ path: itemPath })
214
- });
215
-
216
- if (!response.ok) {
217
- throw new Error('Delete failed');
218
- }
219
-
220
- showNotification(`${isDirectory ? 'Folder' : 'File'} deleted successfully`, 'success');
221
- setTimeout(() => window.location.reload(), 600);
222
- } catch (error) {
223
- showNotification('Failed to delete item', 'error');
224
- }
225
- }
226
-
227
- if (e.target.closest('.rename-btn')) {
228
- const btn = e.target.closest('.rename-btn');
229
- const itemPath = btn.dataset.path;
230
- const currentName = btn.dataset.name;
231
- const isDirectory = btn.dataset.isDirectory === 'true';
232
-
233
- const newName = prompt(`Rename ${isDirectory ? 'folder' : 'file'}:`, currentName);
234
- if (!newName || newName.trim() === currentName) {
235
- return;
236
- }
237
-
238
- try {
239
- const response = await fetch('/api/rename', {
240
- method: 'POST',
241
- headers: { 'Content-Type': 'application/json' },
242
- body: JSON.stringify({ path: itemPath, newName: newName.trim() })
243
- });
244
-
245
- if (!response.ok) {
246
- throw new Error('Rename failed');
247
- }
248
-
249
- showNotification(`Renamed to "${newName.trim()}"`, 'success');
250
- setTimeout(() => window.location.reload(), 600);
251
- } catch (error) {
252
- showNotification('Failed to rename item', 'error');
253
- }
254
- }
255
- });
256
- }
257
-
258
298
  showDiffViewer(filePath) {
259
299
  // Simplified - redirect to diff view
260
300
  const url = new URL(window.location.href);
@@ -332,6 +372,33 @@ class Application {
332
372
  firstLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
333
373
  }
334
374
  }
375
+
376
+ initializeContentSearch() {
377
+ if (!this.contentSearch) {
378
+ this.contentSearch = new ContentSearchHandler();
379
+ }
380
+ this.contentSearch.init();
381
+ }
382
+
383
+ /**
384
+ * Cleanup components before navigation/re-initialization
385
+ */
386
+ cleanupComponents() {
387
+ // Cleanup file viewer
388
+ if (this.fileViewer && typeof this.fileViewer.destroy === 'function') {
389
+ this.fileViewer.destroy();
390
+ this.fileViewer = null;
391
+ }
392
+
393
+ // Cleanup inline search
394
+ if (this.inlineSearch && typeof this.inlineSearch.destroy === 'function') {
395
+ this.inlineSearch.destroy();
396
+ this.inlineSearch = null;
397
+ }
398
+
399
+ // Focus mode persists across navigations, no cleanup needed
400
+ // Content search persists across navigations, no cleanup needed
401
+ }
335
402
  }
336
403
 
337
404
  const app = new Application();
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Button Styles
3
+ * All button variants and toggle controls
4
+ */
5
+
6
+ /* ========================================
7
+ Base Button
8
+ ======================================== */
9
+
10
+ .btn {
11
+ align-items: center;
12
+ backdrop-filter: blur(10px);
13
+ border: 1px solid;
14
+ border-radius: var(--radius-lg);
15
+ box-shadow: var(--shadow-sm);
16
+ cursor: pointer;
17
+ display: inline-flex;
18
+ font-size: 13px;
19
+ font-weight: 600;
20
+ gap: 8px;
21
+ justify-content: center;
22
+ letter-spacing: -0.01em;
23
+ overflow: hidden;
24
+ padding: 8px 18px;
25
+ position: relative;
26
+ transition: all var(--transition-base);
27
+ }
28
+
29
+ .btn::before {
30
+ background: rgba(255, 255, 255, 0.1);
31
+ border-radius: 50%;
32
+ content: '';
33
+ height: 0;
34
+ left: 50%;
35
+ position: absolute;
36
+ top: 50%;
37
+ transform: translate(-50%, -50%);
38
+ transition: width 0.4s, height 0.4s;
39
+ width: 0;
40
+ }
41
+
42
+ .btn:hover::before {
43
+ height: 300px;
44
+ width: 300px;
45
+ }
46
+
47
+ /* ========================================
48
+ Button Variants
49
+ ======================================== */
50
+
51
+ .btn-primary {
52
+ background: linear-gradient(135deg, var(--bg-elevated) 0%, var(--bg-card) 100%);
53
+ border-color: var(--border-primary);
54
+ color: var(--text-primary);
55
+ }
56
+
57
+ .btn-primary:hover {
58
+ background: linear-gradient(135deg, var(--hover-bg) 0%, var(--bg-elevated) 100%);
59
+ border-color: var(--link-color);
60
+ box-shadow: var(--shadow-md), var(--shadow-glow);
61
+ transform: translateY(-2px);
62
+ }
63
+
64
+ .btn-primary:active {
65
+ box-shadow: var(--shadow-sm);
66
+ transform: translateY(0);
67
+ }
68
+
69
+ .btn-outline {
70
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-elevated) 100%);
71
+ border-color: var(--border-primary);
72
+ color: var(--text-primary);
73
+ }
74
+
75
+ .btn-outline:hover {
76
+ background: linear-gradient(135deg, var(--hover-bg) 0%, var(--bg-elevated) 100%);
77
+ border-color: var(--link-color);
78
+ box-shadow: var(--shadow-md), var(--shadow-glow);
79
+ transform: translateY(-2px);
80
+ }
81
+
82
+ .btn-success {
83
+ background: linear-gradient(135deg, var(--accent-success) 0%, #10b981 100%);
84
+ border-color: rgba(255, 255, 255, 0.1);
85
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3), 0 0 0 1px rgba(16, 185, 129, 0.1);
86
+ color: #ffffff;
87
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
88
+ }
89
+
90
+ .btn-success:hover {
91
+ background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
92
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4), 0 0 0 1px rgba(16, 185, 129, 0.2), 0 0 20px rgba(16, 185, 129, 0.2);
93
+ transform: translateY(-2px);
94
+ }
95
+
96
+ .btn-success:active {
97
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
98
+ transform: translateY(0);
99
+ }
100
+
101
+ .btn-secondary {
102
+ background: var(--bg-primary);
103
+ border-color: var(--border-primary);
104
+ box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04);
105
+ color: var(--text-primary);
106
+ }
107
+
108
+ .btn-secondary:hover {
109
+ background: var(--hover-bg);
110
+ border-color: var(--text-secondary);
111
+ }
112
+
113
+ /* ========================================
114
+ Button Icons
115
+ ======================================== */
116
+
117
+ .btn-icon {
118
+ fill: currentColor;
119
+ height: 14px;
120
+ margin-right: 6px;
121
+ vertical-align: text-bottom;
122
+ width: 14px;
123
+ }
124
+
125
+ .btn-chevron {
126
+ fill: currentColor;
127
+ height: 16px;
128
+ width: 16px;
129
+ }
130
+
131
+ /* ========================================
132
+ Sidebar Button
133
+ ======================================== */
134
+
135
+ .btn-sidebar {
136
+ align-items: center;
137
+ background: var(--bg-primary);
138
+ border: 1px solid var(--border-primary);
139
+ border-radius: 6px;
140
+ color: var(--text-primary);
141
+ cursor: pointer;
142
+ display: flex;
143
+ font-size: 14px;
144
+ height: 28px;
145
+ justify-content: center;
146
+ padding: 5px 12px;
147
+ transition: all 0.15s ease;
148
+ width: 100%;
149
+ }
150
+
151
+ .btn-sidebar:hover {
152
+ background-color: var(--bg-tertiary);
153
+ border-color: var(--text-secondary);
154
+ }
155
+
156
+ /* ========================================
157
+ Toggle Buttons
158
+ ======================================== */
159
+
160
+ .theme-toggle,
161
+ .gitignore-toggle,
162
+ .edit-btn {
163
+ align-items: center;
164
+ backdrop-filter: blur(10px);
165
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-elevated) 100%);
166
+ border: 1px solid var(--border-subtle);
167
+ border-radius: var(--radius-lg);
168
+ box-shadow: var(--shadow-sm);
169
+ color: var(--text-primary);
170
+ cursor: pointer;
171
+ display: flex;
172
+ font-size: 14px;
173
+ height: 36px;
174
+ justify-content: center;
175
+ margin: 0;
176
+ min-width: 36px;
177
+ overflow: hidden;
178
+ padding: 8px 14px;
179
+ position: relative;
180
+ transition: all var(--transition-base);
181
+ }
182
+
183
+ .theme-toggle:hover,
184
+ .gitignore-toggle:hover,
185
+ .edit-btn:hover {
186
+ background: linear-gradient(135deg, var(--hover-bg) 0%, var(--bg-elevated) 100%);
187
+ border-color: var(--link-color);
188
+ box-shadow: var(--shadow-md), var(--shadow-glow);
189
+ transform: translateY(-2px);
190
+ }
191
+
192
+ .theme-toggle:active,
193
+ .gitignore-toggle:active,
194
+ .edit-btn:active {
195
+ background: linear-gradient(135deg, var(--hover-bg-strong) 0%, var(--hover-bg) 100%);
196
+ box-shadow: var(--shadow-sm);
197
+ transform: translateY(0) scale(0.98);
198
+ }
199
+
200
+ .gitignore-toggle.showing-ignored {
201
+ background: linear-gradient(135deg, rgba(88, 166, 255, 0.12) 0%, rgba(88, 166, 255, 0.08) 100%);
202
+ border-color: var(--link-color);
203
+ box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.2);
204
+ color: var(--link-color);
205
+ }
206
+
207
+ .gitignore-toggle.showing-ignored:hover {
208
+ background: linear-gradient(135deg, rgba(88, 166, 255, 0.18) 0%, rgba(88, 166, 255, 0.12) 100%);
209
+ box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.3), var(--shadow-md);
210
+ }
211
+
212
+ .theme-icon,
213
+ .gitignore-icon,
214
+ .edit-icon {
215
+ fill: currentColor;
216
+ height: 16px;
217
+ width: 16px;
218
+ }
219
+
220
+ /* ========================================
221
+ View Toggle
222
+ ======================================== */
223
+
224
+ .view-toggle {
225
+ background: var(--bg-primary);
226
+ border: 1px solid var(--border-primary);
227
+ border-radius: var(--radius-md);
228
+ box-shadow: var(--shadow-sm);
229
+ display: inline-flex;
230
+ overflow: hidden;
231
+ transition: all var(--transition-base);
232
+ }
233
+
234
+ .view-toggle:hover {
235
+ border-color: var(--border-secondary);
236
+ box-shadow: var(--shadow-md);
237
+ }
238
+
239
+ .view-btn {
240
+ align-items: center;
241
+ background: transparent;
242
+ border: none;
243
+ color: var(--text-secondary);
244
+ cursor: pointer;
245
+ display: inline-flex;
246
+ font-size: 13px;
247
+ font-weight: 500;
248
+ gap: 6px;
249
+ padding: 7px 14px;
250
+ position: relative;
251
+ text-decoration: none;
252
+ transition: all var(--transition-base);
253
+ white-space: nowrap;
254
+ }
255
+
256
+ .view-btn::before {
257
+ background: var(--hover-bg);
258
+ content: '';
259
+ inset: 0;
260
+ opacity: 0;
261
+ position: absolute;
262
+ transition: opacity var(--transition-base);
263
+ }
264
+
265
+ .view-btn:hover {
266
+ color: var(--text-primary);
267
+ }
268
+
269
+ .view-btn:hover::before {
270
+ opacity: 1;
271
+ }
272
+
273
+ .view-btn.active {
274
+ background: linear-gradient(135deg, var(--link-color) 0%, var(--link-hover) 100%);
275
+ box-shadow: 0 2px 4px rgba(88, 166, 255, 0.3);
276
+ color: white;
277
+ }
278
+
279
+ .view-btn.active::before {
280
+ display: none;
281
+ }
282
+
283
+ .view-btn.active:hover {
284
+ background: linear-gradient(135deg, var(--link-hover) 0%, var(--link-color) 100%);
285
+ box-shadow: 0 4px 8px rgba(88, 166, 255, 0.4);
286
+ transform: translateY(-1px);
287
+ }
288
+
289
+ .view-btn + .view-btn {
290
+ border-left: 1px solid var(--border-primary);
291
+ }
292
+
293
+ .view-icon {
294
+ fill: currentColor;
295
+ flex-shrink: 0;
296
+ height: 16px;
297
+ width: 16px;
298
+ }
299
+
300
+ /* ========================================
301
+ Branch Button
302
+ ======================================== */
303
+
304
+ .branch-button {
305
+ align-items: center;
306
+ background: linear-gradient(135deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
307
+ border: 1px solid var(--border-primary);
308
+ border-radius: var(--radius-lg);
309
+ box-shadow: var(--shadow-sm);
310
+ color: var(--text-primary);
311
+ cursor: pointer;
312
+ display: inline-flex;
313
+ font-size: 13px;
314
+ font-weight: 600;
315
+ gap: 8px;
316
+ overflow: hidden;
317
+ padding: 8px 16px;
318
+ position: relative;
319
+ transition: all var(--transition-base);
320
+ }
321
+
322
+ .branch-button::before {
323
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
324
+ content: '';
325
+ height: 100%;
326
+ left: -100%;
327
+ position: absolute;
328
+ top: 0;
329
+ transition: left var(--transition-slow);
330
+ width: 100%;
331
+ }
332
+
333
+ .branch-button:hover::before {
334
+ left: 100%;
335
+ }
336
+
337
+ .branch-button:hover {
338
+ background: linear-gradient(135deg, var(--hover-bg) 0%, var(--bg-elevated) 100%);
339
+ border-color: var(--link-color);
340
+ box-shadow: var(--shadow-md), var(--shadow-glow);
341
+ transform: translateY(-2px);
342
+ }
343
+
344
+ .branch-button:active {
345
+ box-shadow: var(--shadow-sm);
346
+ transform: translateY(0);
347
+ }
348
+
349
+ .branch-count,
350
+ .tag-count {
351
+ align-items: center;
352
+ color: var(--text-secondary);
353
+ display: flex;
354
+ font-size: 14px;
355
+ gap: 6px;
356
+ }
357
+
358
+ .branch-name {
359
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
360
+ font-size: 12px;
361
+ font-weight: 600;
362
+ }
363
+
364
+ /* ========================================
365
+ Quick Action Buttons
366
+ ======================================== */
367
+
368
+ .quick-actions {
369
+ backdrop-filter: blur(8px);
370
+ background: var(--bg-secondary);
371
+ border-radius: var(--radius-sm);
372
+ display: flex;
373
+ gap: 4px;
374
+ opacity: 0;
375
+ padding: 2px;
376
+ position: absolute;
377
+ right: 8px;
378
+ top: 50%;
379
+ transform: translateY(-50%);
380
+ transition: all var(--transition-base);
381
+ z-index: 10;
382
+ }
383
+
384
+ .file-row:hover .quick-actions {
385
+ opacity: 1;
386
+ transform: translateY(-50%) scale(1);
387
+ }
388
+
389
+ .quick-btn {
390
+ align-items: center;
391
+ background: var(--bg-primary);
392
+ border: 1px solid var(--border-primary);
393
+ border-radius: var(--radius-sm);
394
+ box-shadow: var(--shadow-sm);
395
+ color: var(--text-secondary);
396
+ cursor: pointer;
397
+ display: flex;
398
+ height: 28px;
399
+ justify-content: center;
400
+ padding: 5px 7px;
401
+ text-decoration: none;
402
+ transition: all var(--transition-base);
403
+ width: 28px;
404
+ }
405
+
406
+ .quick-btn:hover {
407
+ background: var(--hover-bg);
408
+ border-color: var(--border-secondary);
409
+ box-shadow: var(--shadow-md);
410
+ color: var(--text-primary);
411
+ transform: translateY(-2px) scale(1.05);
412
+ }
413
+
414
+ .quick-btn:active {
415
+ box-shadow: var(--shadow-sm);
416
+ transform: translateY(0) scale(1);
417
+ }
418
+
419
+ .quick-icon {
420
+ fill: currentColor;
421
+ height: 12px;
422
+ width: 12px;
423
+ }