@herb-tools/dev-tools 0.7.4 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/dev-tools",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "Development tools for visual debugging in HTML+ERB templates",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  }
31
31
  },
32
32
  "dependencies": {
33
- "@herb-tools/core": "0.7.4"
33
+ "@herb-tools/core": "0.8.0"
34
34
  },
35
35
  "files": [
36
36
  "package.json",
@@ -15,10 +15,30 @@ export class HerbOverlay {
15
15
  private showingComponentOutlines = false;
16
16
  private menuOpen = false;
17
17
  private projectPath = '';
18
+ private preferredEditor = 'auto';
19
+ private defaultEditorFromServer = 'vscode';
18
20
  private currentlyHoveredERBElement: HTMLElement | null = null;
19
21
  private errorOverlay: ErrorOverlay | null = null;
20
22
 
21
23
  private static readonly SETTINGS_KEY = 'herb-dev-tools-settings';
24
+ private static readonly EDITOR_OPTIONS = [
25
+ { value: 'auto', label: 'Auto (from server via RAILS_EDITOR or EDITOR)' },
26
+ { value: 'atom', label: 'Atom' },
27
+ { value: 'cursor', label: 'Cursor' },
28
+ { value: 'emacs', label: 'Emacs' },
29
+ { value: 'idea', label: 'IntelliJ IDEA' },
30
+ { value: 'macvim', label: 'MacVim' },
31
+ { value: 'nova', label: 'Nova' },
32
+ { value: 'nvim', label: 'Neovim' },
33
+ { value: 'rubymine', label: 'RubyMine' },
34
+ { value: 'sublime', label: 'Sublime Text' },
35
+ { value: 'textmate', label: 'TextMate' },
36
+ { value: 'vim', label: 'Vim' },
37
+ { value: 'vscode', label: 'Visual Studio Code' },
38
+ { value: 'vscodium', label: 'VSCodium' },
39
+ { value: 'windsurf', label: 'Windsurf' },
40
+ { value: 'zed', label: 'Zed' },
41
+ ];
22
42
 
23
43
  constructor(private options: HerbDevToolsOptions = {}) {
24
44
  if (options.autoInit !== false) {
@@ -28,10 +48,12 @@ export class HerbOverlay {
28
48
 
29
49
  private init() {
30
50
  this.loadProjectPath();
51
+ this.loadDefaultEditor();
31
52
  this.loadSettings();
32
53
  this.injectMenu();
33
54
  this.setupMenuToggle();
34
55
  this.setupToggleSwitches();
56
+ this.setupEditorDropdown();
35
57
  this.initializeErrorOverlay();
36
58
  this.setupTurboListeners();
37
59
  this.applySettings();
@@ -50,6 +72,19 @@ export class HerbOverlay {
50
72
  }
51
73
  }
52
74
 
75
+ private loadDefaultEditor() {
76
+ const metaTag = document.querySelector('meta[name="herb-default-editor"]') as HTMLMetaElement;
77
+
78
+ if (metaTag?.content) {
79
+ const defaultEditor = metaTag.content.toLowerCase();
80
+ const isValidEditor = HerbOverlay.EDITOR_OPTIONS.some(option => option.value === defaultEditor);
81
+
82
+ if (isValidEditor) {
83
+ this.defaultEditorFromServer = defaultEditor;
84
+ }
85
+ }
86
+ }
87
+
53
88
  private loadSettings() {
54
89
  const savedSettings = localStorage.getItem(HerbOverlay.SETTINGS_KEY);
55
90
  if (savedSettings) {
@@ -63,6 +98,9 @@ export class HerbOverlay {
63
98
  this.showingPartialOutlines = settings.showingPartialOutlines || false;
64
99
  this.showingComponentOutlines = settings.showingComponentOutlines || false;
65
100
  this.menuOpen = settings.menuOpen || false;
101
+ if (settings.preferredEditor) {
102
+ this.preferredEditor = settings.preferredEditor;
103
+ }
66
104
  } catch (e) {
67
105
  console.warn('Failed to load Herb dev tools settings:', e);
68
106
  }
@@ -78,7 +116,8 @@ export class HerbOverlay {
78
116
  showingViewOutlines: this.showingViewOutlines,
79
117
  showingPartialOutlines: this.showingPartialOutlines,
80
118
  showingComponentOutlines: this.showingComponentOutlines,
81
- menuOpen: this.menuOpen
119
+ menuOpen: this.menuOpen,
120
+ preferredEditor: this.preferredEditor
82
121
  };
83
122
 
84
123
  localStorage.setItem(HerbOverlay.SETTINGS_KEY, JSON.stringify(settings));
@@ -170,6 +209,17 @@ export class HerbOverlay {
170
209
  </label>
171
210
  </div>
172
211
 
212
+ <div class="herb-editor-section">
213
+ <label class="herb-editor-label">
214
+ <span class="herb-editor-text">Editor</span>
215
+ <select id="herbEditorSelect" class="herb-editor-select">
216
+ ${HerbOverlay.EDITOR_OPTIONS.map(editor =>
217
+ `<option value="${editor.value}">${editor.label}</option>`
218
+ ).join('')}
219
+ </select>
220
+ </label>
221
+ </div>
222
+
173
223
  <div class="herb-disable-all-section">
174
224
  <button id="herbDisableAll" class="herb-disable-all-btn">Disable All</button>
175
225
  </div>
@@ -247,6 +297,7 @@ export class HerbOverlay {
247
297
  this.injectMenu();
248
298
  this.setupMenuToggle();
249
299
  this.setupToggleSwitches();
300
+ this.setupEditorDropdown();
250
301
  this.applySettings();
251
302
  this.updateMenuButtonState();
252
303
  }
@@ -336,6 +387,32 @@ export class HerbOverlay {
336
387
  }
337
388
  }
338
389
 
390
+ private setupEditorDropdown() {
391
+ const editorSelect = document.getElementById('herbEditorSelect') as HTMLSelectElement;
392
+
393
+ if (editorSelect) {
394
+ const autoOption = editorSelect.querySelector('option[value="auto"]') as HTMLOptionElement;
395
+
396
+ if (autoOption) {
397
+ const editorLabel = HerbOverlay.EDITOR_OPTIONS.find(opt => opt.value === this.defaultEditorFromServer)?.label || this.defaultEditorFromServer;
398
+ const metaTag = document.querySelector('meta[name="herb-default-editor"]') as HTMLMetaElement;
399
+
400
+ if (metaTag?.content) {
401
+ autoOption.textContent = `Auto (from server): ${editorLabel}`;
402
+ } else {
403
+ autoOption.textContent = `Auto (default): ${editorLabel}`;
404
+ }
405
+ }
406
+
407
+ editorSelect.value = this.preferredEditor;
408
+
409
+ editorSelect.addEventListener('change', () => {
410
+ this.preferredEditor = editorSelect.value;
411
+ this.saveSettings();
412
+ });
413
+ }
414
+ }
415
+
339
416
  private toggleViewOutlines(show?: boolean) {
340
417
  this.showingViewOutlines = show !== undefined ? show : !this.showingViewOutlines;
341
418
  const viewOutlines = document.querySelectorAll('[data-herb-debug-outline-type="view"], [data-herb-debug-outline-type*="view"]');
@@ -924,21 +1001,56 @@ export class HerbOverlay {
924
1001
  }
925
1002
  }
926
1003
 
1004
+ private getEditorUrl(editor: string, absolutePath: string, line: number, column: number): string {
1005
+ switch (editor) {
1006
+ case 'cursor':
1007
+ return `cursor://file/${absolutePath}:${line}:${column}`;
1008
+ case 'vscode':
1009
+ return `vscode://file/${absolutePath}:${line}:${column}`;
1010
+ case 'vscodium':
1011
+ return `vscodium://file/${absolutePath}:${line}:${column}`;
1012
+ case 'zed':
1013
+ return `zed://file/${absolutePath}:${line}:${column}`;
1014
+ case 'windsurf':
1015
+ return `windsurf://file/${absolutePath}:${line}:${column}`;
1016
+ case 'sublime':
1017
+ return `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1018
+ case 'atom':
1019
+ return `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`;
1020
+ case 'textmate':
1021
+ return `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1022
+ case 'emacs':
1023
+ return `emacs://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1024
+ case 'idea':
1025
+ return `idea://open?file=${absolutePath}&line=${line}&column=${column}`;
1026
+ case 'rubymine':
1027
+ return `x-mine://open?file=${absolutePath}&line=${line}&column=${column}`;
1028
+ case 'nova':
1029
+ return `nova://open?path=${absolutePath}&line=${line}&column=${column}`;
1030
+ case 'macvim':
1031
+ return `mvim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1032
+ case 'vim':
1033
+ return `vim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1034
+ case 'nvim':
1035
+ return `nvim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1036
+ default:
1037
+ return '';
1038
+ }
1039
+ }
1040
+
927
1041
  private openFileInEditor(file: string, line: number, column: number) {
928
1042
  const absolutePath = file.startsWith('/') ? file : (this.projectPath ? `${this.projectPath}/${file}` : file);
929
1043
 
930
- const editors = [
931
- `cursor://file/${absolutePath}:${line}:${column}`,
932
- `vscode://file/${absolutePath}:${line}:${column}`,
933
- `zed://file/${absolutePath}:${line}:${column}`,
934
- `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`,
935
- `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`,
936
- `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`,
937
- ];
938
-
939
- try {
940
- window.open(editors[0], '_self');
941
- } catch (error) {
1044
+ const editorToUse = this.preferredEditor === 'auto' ? this.defaultEditorFromServer : this.preferredEditor;
1045
+ const url = this.getEditorUrl(editorToUse, absolutePath, line, column);
1046
+
1047
+ if (url) {
1048
+ try {
1049
+ window.open(url, '_self');
1050
+ } catch (error) {
1051
+ console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
1052
+ }
1053
+ } else {
942
1054
  console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
943
1055
  }
944
1056
  }
package/src/styles.css CHANGED
@@ -429,6 +429,85 @@
429
429
  background: #7c3aed;
430
430
  }
431
431
 
432
+ .herb-editor-section {
433
+ padding: 16px 20px;
434
+ border-bottom: 1px solid #f3f4f6;
435
+ background: linear-gradient(135deg, #fafbfc 0%, #f8f9fa 100%);
436
+ position: relative;
437
+ overflow: hidden;
438
+ }
439
+
440
+ .herb-editor-section::before {
441
+ content: '';
442
+ position: absolute;
443
+ top: 0;
444
+ left: 0;
445
+ right: 0;
446
+ height: 2px;
447
+ background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.2), transparent);
448
+ }
449
+
450
+ .herb-editor-label {
451
+ display: flex;
452
+ flex-direction: column;
453
+ gap: 10px;
454
+ cursor: default;
455
+ }
456
+
457
+ .herb-editor-text {
458
+ font-size: 12px;
459
+ font-weight: 600;
460
+ color: #6b7280;
461
+ text-transform: uppercase;
462
+ letter-spacing: 0.5px;
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 6px;
466
+ }
467
+
468
+ .herb-editor-select {
469
+ width: 100%;
470
+ padding: 10px 36px 10px 12px;
471
+ background: white;
472
+ border: 1.5px solid #e5e7eb;
473
+ border-radius: 8px;
474
+ font-size: 13.5px;
475
+ font-weight: 500;
476
+ color: #1f2937;
477
+ cursor: pointer;
478
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
479
+ font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
480
+ appearance: none;
481
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
482
+ }
483
+
484
+ .herb-editor-select option {
485
+ font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
486
+ font-size: 14px;
487
+ font-weight: 400;
488
+ padding: 8px 12px;
489
+ }
490
+
491
+ .herb-editor-select:hover {
492
+ border-color: #8b5cf6;
493
+ background-color: #fafafa;
494
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(139, 92, 246, 0.1);
495
+ transform: translateY(-1px);
496
+ }
497
+
498
+ .herb-editor-select:focus {
499
+ outline: none;
500
+ border-color: #8b5cf6;
501
+ background-color: white;
502
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15), 0 2px 8px rgba(139, 92, 246, 0.1);
503
+ transform: translateY(-1px);
504
+ }
505
+
506
+ .herb-editor-select:active {
507
+ transform: translateY(0);
508
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
509
+ }
510
+
432
511
  .herb-disable-all-section {
433
512
  padding: 16px 20px;
434
513
  border-top: 1px solid #f3f4f6;