vimd 0.5.1 → 0.5.2

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.
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  :root {
6
+ /* Sidebar */
6
7
  --sidebar-bg: #252526;
7
8
  --sidebar-text: #cccccc;
8
9
  --sidebar-text-muted: #858585;
@@ -10,11 +11,24 @@
10
11
  --sidebar-selected: #094771;
11
12
  --sidebar-border: #3c3c3c;
12
13
  --sidebar-header-bg: #3c3c3c;
13
- --sidebar-width: 250px;
14
+ --sidebar-width: 280px;
14
15
  --sidebar-min-width: 150px;
15
16
  --sidebar-max-width: 500px;
16
- --toggle-bar-width: 20px;
17
17
  --resizer-width: 4px;
18
+
19
+ /* Panel header (light theme) */
20
+ --panel-header-border: #e0e0e0;
21
+ --panel-filename-color: #666;
22
+ --panel-close-color: #999;
23
+ --panel-close-hover: #333;
24
+ --panel-active-color: #007acc;
25
+
26
+ /* Context menu */
27
+ --context-menu-bg: #ffffff;
28
+ --context-menu-border: #e0e0e0;
29
+ --context-menu-hover: #f5f5f5;
30
+ --context-menu-text: #333;
31
+ --context-menu-shadow: rgba(0, 0, 0, 0.15);
18
32
  }
19
33
 
20
34
  * {
@@ -24,6 +38,8 @@
24
38
  html, body {
25
39
  margin: 0;
26
40
  padding: 0;
41
+ width: 100%;
42
+ max-width: none;
27
43
  height: 100%;
28
44
  overflow: hidden;
29
45
  }
@@ -31,27 +47,12 @@ html, body {
31
47
  /* Container */
32
48
  .vimd-container {
33
49
  display: flex;
50
+ width: 100vw;
34
51
  height: 100vh;
35
52
  overflow: hidden;
36
53
  background: #1e1e1e;
37
54
  }
38
55
 
39
- /* Toggle bar (visible when sidebar is collapsed) */
40
- .vimd-sidebar-toggle-bar {
41
- width: var(--toggle-bar-width);
42
- min-width: var(--toggle-bar-width);
43
- background: var(--sidebar-bg);
44
- border-right: 1px solid var(--sidebar-border);
45
- display: none;
46
- flex-direction: column;
47
- align-items: center;
48
- padding-top: 8px;
49
- }
50
-
51
- .vimd-sidebar-toggle-bar.visible {
52
- display: flex;
53
- }
54
-
55
56
  /* Sidebar */
56
57
  .vimd-sidebar {
57
58
  width: var(--sidebar-width);
@@ -63,15 +64,6 @@ html, body {
63
64
  flex-direction: column;
64
65
  position: relative;
65
66
  border-right: 1px solid var(--sidebar-border);
66
- transition: width 0.15s ease, min-width 0.15s ease, opacity 0.15s ease;
67
- }
68
-
69
- .vimd-sidebar.collapsed {
70
- width: 0;
71
- min-width: 0;
72
- border-right: none;
73
- overflow: hidden;
74
- opacity: 0;
75
67
  }
76
68
 
77
69
  /* Sidebar header */
@@ -98,26 +90,6 @@ html, body {
98
90
  flex: 1;
99
91
  }
100
92
 
101
- /* Toggle button */
102
- .vimd-toggle-btn {
103
- background: transparent;
104
- border: none;
105
- color: var(--sidebar-text);
106
- cursor: pointer;
107
- padding: 4px;
108
- display: flex;
109
- align-items: center;
110
- justify-content: center;
111
- border-radius: 3px;
112
- opacity: 0.7;
113
- transition: opacity 0.1s, background 0.1s;
114
- }
115
-
116
- .vimd-toggle-btn:hover {
117
- opacity: 1;
118
- background: var(--sidebar-hover);
119
- }
120
-
121
93
  /* File tree */
122
94
  .vimd-tree {
123
95
  flex: 1;
@@ -238,10 +210,117 @@ html, body {
238
210
 
239
211
  /* Preview area */
240
212
  .vimd-preview {
213
+ flex: 1;
214
+ display: flex;
215
+ overflow: hidden;
216
+ background: #ffffff;
217
+ }
218
+
219
+ /* Panel */
220
+ .vimd-panel {
221
+ flex: 1;
222
+ display: flex;
223
+ flex-direction: column;
224
+ overflow: hidden;
225
+ min-width: 100px;
226
+ }
227
+
228
+ /* Split view: Panel 1 (initial 720px, resizable) */
229
+ .vimd-panel.split-panel1 {
230
+ flex: none;
231
+ /* width is set by JS */
232
+ }
233
+
234
+ /* Split view: Panel 2 (takes remaining space) */
235
+ .vimd-panel.split-panel2 {
236
+ flex: 1;
237
+ }
238
+
239
+ .vimd-panel-body {
241
240
  flex: 1;
242
241
  overflow-y: auto;
243
242
  overflow-x: hidden;
244
- background: #ffffff;
243
+ }
244
+
245
+ /* Panel header */
246
+ .vimd-panel-header {
247
+ display: none;
248
+ align-items: center;
249
+ justify-content: flex-start;
250
+ gap: 8px;
251
+ height: 28px;
252
+ padding: 4px 16px;
253
+ background: transparent;
254
+ border-bottom: 1px solid var(--panel-header-border);
255
+ position: relative;
256
+ flex-shrink: 0;
257
+ }
258
+
259
+ .vimd-panel-header.visible {
260
+ display: flex;
261
+ }
262
+
263
+ .vimd-panel-header.active::after {
264
+ content: '';
265
+ position: absolute;
266
+ bottom: 0;
267
+ left: 0;
268
+ right: 0;
269
+ height: 2px;
270
+ background: var(--panel-active-color);
271
+ }
272
+
273
+ .vimd-panel-filename {
274
+ font-size: 12px;
275
+ color: var(--panel-filename-color);
276
+ overflow: hidden;
277
+ text-overflow: ellipsis;
278
+ white-space: nowrap;
279
+ max-width: calc(100% - 30px);
280
+ }
281
+
282
+ .vimd-panel-filename:hover {
283
+ overflow: visible;
284
+ white-space: normal;
285
+ word-break: break-all;
286
+ }
287
+
288
+ .vimd-panel-close {
289
+ background: none;
290
+ border: none;
291
+ font-size: 16px;
292
+ color: var(--panel-close-color);
293
+ cursor: pointer;
294
+ flex-shrink: 0;
295
+ width: 20px;
296
+ height: 20px;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ border-radius: 3px;
301
+ }
302
+
303
+ .vimd-panel-close:hover {
304
+ color: var(--panel-close-hover);
305
+ background: rgba(0, 0, 0, 0.05);
306
+ }
307
+
308
+ /* Panel resizer (between panels) */
309
+ .vimd-panel-resizer {
310
+ width: 4px;
311
+ background: transparent;
312
+ cursor: ew-resize;
313
+ flex-shrink: 0;
314
+ display: none;
315
+ }
316
+
317
+ .vimd-panel-resizer.visible {
318
+ display: block;
319
+ }
320
+
321
+ .vimd-panel-resizer:hover,
322
+ .vimd-panel-resizer.active {
323
+ background: var(--panel-active-color);
245
324
  }
246
325
 
247
326
  /* Welcome screen */
@@ -274,10 +353,15 @@ html, body {
274
353
  .vimd-content {
275
354
  display: none;
276
355
  padding: 32px;
277
- max-width: 900px;
356
+ max-width: 720px;
278
357
  margin: 0 auto;
279
358
  }
280
359
 
360
+ .vimd-content pre,
361
+ .vimd-content table {
362
+ overflow-x: auto;
363
+ }
364
+
281
365
  .vimd-content.visible {
282
366
  display: block;
283
367
  }
@@ -307,3 +391,35 @@ html, body {
307
391
  margin: 0;
308
392
  font-size: 14px;
309
393
  }
394
+
395
+ /* Context menu */
396
+ .vimd-context-menu {
397
+ display: none;
398
+ position: fixed;
399
+ background: var(--context-menu-bg);
400
+ border: 1px solid var(--context-menu-border);
401
+ border-radius: 4px;
402
+ box-shadow: 0 2px 8px var(--context-menu-shadow);
403
+ min-width: 150px;
404
+ z-index: 1000;
405
+ padding: 4px 0;
406
+ }
407
+
408
+ .vimd-context-menu.visible {
409
+ display: block;
410
+ }
411
+
412
+ .vimd-context-item {
413
+ padding: 6px 12px;
414
+ cursor: pointer;
415
+ font-size: 13px;
416
+ color: var(--context-menu-text);
417
+ }
418
+
419
+ .vimd-context-item:hover {
420
+ background: var(--context-menu-hover);
421
+ }
422
+
423
+ .vimd-context-item.hidden {
424
+ display: none;
425
+ }
@@ -6,25 +6,53 @@
6
6
 
7
7
  // Storage keys
8
8
  var STORAGE_KEY_EXPANDED = 'vimd-folder-expanded';
9
- var STORAGE_KEY_WIDTH = 'vimd-sidebar-width';
9
+ var STORAGE_KEY_SIDEBAR_WIDTH = 'vimd-sidebar-width';
10
+ var STORAGE_KEY_STATE = 'vimd-folder-mode-state';
10
11
 
11
12
  // State
12
13
  var fileTree = [];
13
- var currentPath = null;
14
14
  var ws = null;
15
15
  var expandedFolders = new Set();
16
- var isResizing = false;
16
+ var isSidebarResizing = false;
17
+ var isPanelResizing = false;
18
+
19
+ // Panel state
20
+ var state = {
21
+ isSplitView: false,
22
+ panels: [
23
+ { file: null },
24
+ { file: null }
25
+ ],
26
+ activePanel: 0,
27
+ panelWidth: 720
28
+ };
17
29
 
18
30
  // DOM elements
19
31
  var sidebar = document.getElementById('sidebar');
20
- var toggleBar = document.getElementById('toggle-bar');
21
- var toggleBtn = document.getElementById('toggle-btn');
22
- var toggleBtnCollapsed = document.getElementById('toggle-btn-collapsed');
23
32
  var fileTreeEl = document.getElementById('file-tree');
33
+ var preview = document.getElementById('preview');
34
+ var sidebarResizer = document.getElementById('resizer');
35
+
36
+ // Panel elements
37
+ var panel1 = document.getElementById('panel1');
38
+ var panel1Header = document.getElementById('panel1-header');
39
+ var panel1Filename = document.getElementById('panel1-filename');
40
+ var panel1Close = document.getElementById('panel1-close');
24
41
  var welcome = document.getElementById('welcome');
25
42
  var welcomeMessage = document.getElementById('welcome-message');
26
43
  var content = document.getElementById('content');
27
- var resizer = document.getElementById('resizer');
44
+
45
+ // Panel 2 elements (created dynamically)
46
+ var panel2 = null;
47
+ var panel2Header = null;
48
+ var panel2Filename = null;
49
+ var panel2Close = null;
50
+ var panel2Content = null;
51
+ var panelResizer = null;
52
+
53
+ // Context menu
54
+ var contextMenu = document.getElementById('context-menu');
55
+ var contextTarget = null;
28
56
 
29
57
  /**
30
58
  * Initialize the application
@@ -34,6 +62,7 @@
34
62
  connectWebSocket();
35
63
  setupEventListeners();
36
64
  handleInitialPath();
65
+ updatePanelUI();
37
66
  }
38
67
 
39
68
  /**
@@ -53,13 +82,27 @@
53
82
 
54
83
  // Load sidebar width
55
84
  try {
56
- var width = localStorage.getItem(STORAGE_KEY_WIDTH);
85
+ var width = localStorage.getItem(STORAGE_KEY_SIDEBAR_WIDTH);
57
86
  if (width) {
58
87
  sidebar.style.width = width + 'px';
59
88
  }
60
89
  } catch (e) {
61
90
  console.warn('[vimd] Failed to load sidebar width:', e);
62
91
  }
92
+
93
+ // Load panel state
94
+ try {
95
+ var savedState = localStorage.getItem(STORAGE_KEY_STATE);
96
+ if (savedState) {
97
+ var parsed = JSON.parse(savedState);
98
+ state.isSplitView = parsed.isSplitView || false;
99
+ state.panels = parsed.panels || [{ file: null }, { file: null }];
100
+ state.activePanel = parsed.activePanel || 0;
101
+ state.panelWidth = parsed.panelWidth || 720;
102
+ }
103
+ } catch (e) {
104
+ console.warn('[vimd] Failed to load panel state:', e);
105
+ }
63
106
  }
64
107
 
65
108
  /**
@@ -80,13 +123,29 @@
80
123
  try {
81
124
  var width = parseInt(sidebar.style.width, 10);
82
125
  if (width) {
83
- localStorage.setItem(STORAGE_KEY_WIDTH, width.toString());
126
+ localStorage.setItem(STORAGE_KEY_SIDEBAR_WIDTH, width.toString());
84
127
  }
85
128
  } catch (e) {
86
129
  console.warn('[vimd] Failed to save sidebar width:', e);
87
130
  }
88
131
  }
89
132
 
133
+ /**
134
+ * Save panel state to localStorage
135
+ */
136
+ function savePanelState() {
137
+ try {
138
+ localStorage.setItem(STORAGE_KEY_STATE, JSON.stringify({
139
+ isSplitView: state.isSplitView,
140
+ panels: state.panels,
141
+ activePanel: state.activePanel,
142
+ panelWidth: state.panelWidth
143
+ }));
144
+ } catch (e) {
145
+ console.warn('[vimd] Failed to save panel state:', e);
146
+ }
147
+ }
148
+
90
149
  /**
91
150
  * Connect to WebSocket server
92
151
  */
@@ -126,6 +185,8 @@
126
185
  fileTree = msg.data;
127
186
  renderTree();
128
187
  updateWelcomeMessage();
188
+ // Restore files after tree loads
189
+ restoreFilesFromState();
129
190
  break;
130
191
 
131
192
  case 'content':
@@ -141,11 +202,7 @@
141
202
  break;
142
203
 
143
204
  case 'fileDeleted':
144
- if (currentPath === msg.data.path) {
145
- showWelcome();
146
- currentPath = null;
147
- updateURL('/');
148
- }
205
+ handleFileDeleted(msg.data.path);
149
206
  break;
150
207
 
151
208
  default:
@@ -153,6 +210,96 @@
153
210
  }
154
211
  }
155
212
 
213
+ /**
214
+ * Restore files from saved state
215
+ */
216
+ function restoreFilesFromState() {
217
+ // Check if saved files exist
218
+ var panel1File = state.panels[0].file;
219
+ var panel2File = state.panels[1].file;
220
+
221
+ // Reset if files don't exist
222
+ if (panel1File && !fileExists(panel1File)) {
223
+ state.panels[0].file = null;
224
+ }
225
+ if (panel2File && !fileExists(panel2File)) {
226
+ state.panels[1].file = null;
227
+ }
228
+
229
+ // If both files are gone, reset to single panel welcome
230
+ if (!state.panels[0].file && !state.panels[1].file) {
231
+ state.isSplitView = false;
232
+ state.activePanel = 0;
233
+ savePanelState();
234
+ updatePanelUI();
235
+ return;
236
+ }
237
+
238
+ // Restore split view if needed
239
+ if (state.isSplitView && state.panels[1].file) {
240
+ createPanel2();
241
+ }
242
+
243
+ // Restore panel 1 file
244
+ if (state.panels[0].file) {
245
+ requestFile(state.panels[0].file, 0);
246
+ }
247
+
248
+ // Restore panel 2 file
249
+ if (state.isSplitView && state.panels[1].file) {
250
+ requestFile(state.panels[1].file, 1);
251
+ }
252
+
253
+ updatePanelUI();
254
+ }
255
+
256
+ /**
257
+ * Check if file exists in tree
258
+ */
259
+ function fileExists(path) {
260
+ function search(nodes) {
261
+ for (var i = 0; i < nodes.length; i++) {
262
+ var node = nodes[i];
263
+ if (node.type === 'file' && node.path === path) {
264
+ return true;
265
+ }
266
+ if (node.type === 'folder' && node.children) {
267
+ if (search(node.children)) {
268
+ return true;
269
+ }
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+ return search(fileTree);
275
+ }
276
+
277
+ /**
278
+ * Handle file deleted event
279
+ */
280
+ function handleFileDeleted(path) {
281
+ var needsUpdate = false;
282
+
283
+ if (state.panels[0].file === path) {
284
+ state.panels[0].file = null;
285
+ needsUpdate = true;
286
+ }
287
+ if (state.panels[1].file === path) {
288
+ state.panels[1].file = null;
289
+ needsUpdate = true;
290
+ }
291
+
292
+ if (needsUpdate) {
293
+ // If both panels are empty, close split view
294
+ if (!state.panels[0].file && !state.panels[1].file) {
295
+ closeSplitView();
296
+ } else {
297
+ updatePanelUI();
298
+ savePanelState();
299
+ }
300
+ }
301
+ }
302
+
156
303
  /**
157
304
  * Render the file tree
158
305
  */
@@ -168,10 +315,8 @@
168
315
  fileTreeEl.appendChild(el);
169
316
  });
170
317
 
171
- // Restore selection if current path exists
172
- if (currentPath) {
173
- updateSelection(currentPath);
174
- }
318
+ // Restore selection
319
+ updateSelection();
175
320
  }
176
321
 
177
322
  /**
@@ -185,6 +330,7 @@
185
330
  item.className = 'vimd-tree-item';
186
331
  item.setAttribute('data-path', node.path);
187
332
  item.setAttribute('data-depth', depth.toString());
333
+ item.setAttribute('data-type', node.type);
188
334
 
189
335
  if (node.type === 'folder') {
190
336
  // Folder node
@@ -254,6 +400,13 @@
254
400
  selectFile(node.path);
255
401
  });
256
402
 
403
+ // Context menu handler for file
404
+ item.addEventListener('contextmenu', function(e) {
405
+ e.preventDefault();
406
+ e.stopPropagation();
407
+ showContextMenu(e, node.path);
408
+ });
409
+
257
410
  container.appendChild(item);
258
411
  }
259
412
 
@@ -296,75 +449,133 @@
296
449
  }
297
450
 
298
451
  /**
299
- * Select a file
452
+ * Select a file (open in active panel)
300
453
  */
301
454
  function selectFile(path) {
302
- if (currentPath === path) {
303
- return;
304
- }
455
+ openFileInPanel(path, state.activePanel);
456
+ }
305
457
 
306
- // Update selection
307
- updateSelection(path);
458
+ /**
459
+ * Open file in specific panel
460
+ */
461
+ function openFileInPanel(path, panelIndex) {
462
+ state.panels[panelIndex].file = path;
463
+ state.activePanel = panelIndex;
464
+ requestFile(path, panelIndex);
465
+ updateSelection();
466
+ updatePanelUI();
467
+ savePanelState();
468
+ updateURL();
469
+ }
308
470
 
309
- // Send to server
471
+ /**
472
+ * Request file content from server
473
+ */
474
+ function requestFile(path, panelIndex) {
310
475
  if (ws && ws.readyState === WebSocket.OPEN) {
311
- ws.send(JSON.stringify({ type: 'selectFile', path: path }));
476
+ ws.send(JSON.stringify({
477
+ type: 'selectFile',
478
+ path: path,
479
+ panelIndex: panelIndex
480
+ }));
312
481
  }
313
-
314
- // Update URL
315
- updateURL('/' + encodeURIComponent(path).replace(/%2F/g, '/'));
316
-
317
- currentPath = path;
318
482
  }
319
483
 
320
484
  /**
321
485
  * Update visual selection in tree
322
486
  */
323
- function updateSelection(path) {
487
+ function updateSelection() {
324
488
  // Remove previous selection
325
489
  var selected = fileTreeEl.querySelectorAll('.vimd-tree-item.selected');
326
490
  selected.forEach(function(el) {
327
491
  el.classList.remove('selected');
328
492
  });
329
493
 
330
- // Add new selection
331
- var item = fileTreeEl.querySelector('.vimd-tree-item[data-path="' + path + '"]');
332
- if (item) {
333
- item.classList.add('selected');
334
- }
494
+ // Add selection for all open files
495
+ state.panels.forEach(function(panel) {
496
+ if (panel.file) {
497
+ var item = fileTreeEl.querySelector('.vimd-tree-item[data-path="' + panel.file + '"]');
498
+ if (item) {
499
+ item.classList.add('selected');
500
+ }
501
+ }
502
+ });
335
503
  }
336
504
 
337
505
  /**
338
506
  * Show file content
339
507
  */
340
508
  function showContent(path, html) {
341
- welcome.classList.add('hidden');
342
- content.classList.add('visible');
343
- content.innerHTML = html;
509
+ // Find which panel this content belongs to
510
+ var panelIndex = -1;
511
+ for (var i = 0; i < state.panels.length; i++) {
512
+ if (state.panels[i].file === path) {
513
+ panelIndex = i;
514
+ break;
515
+ }
516
+ }
344
517
 
345
- // Update selection
346
- updateSelection(path);
347
- currentPath = path;
518
+ if (panelIndex === -1) {
519
+ // If not found, use active panel
520
+ panelIndex = state.activePanel;
521
+ state.panels[panelIndex].file = path;
522
+ }
523
+
524
+ if (panelIndex === 0) {
525
+ welcome.classList.add('hidden');
526
+ content.classList.add('visible');
527
+ content.innerHTML = html;
528
+ panel1Filename.textContent = getFileName(path);
529
+ panel1Header.classList.add('visible');
530
+ } else if (panelIndex === 1 && panel2Content) {
531
+ panel2Content.innerHTML = html;
532
+ panel2Content.classList.add('visible');
533
+ panel2Filename.textContent = getFileName(path);
534
+ panel2Header.classList.add('visible');
535
+ }
536
+
537
+ updatePanelUI();
348
538
 
349
539
  // Re-render MathJax if available
350
540
  if (window.MathJax && window.MathJax.typeset) {
351
- window.MathJax.typeset([content]);
541
+ if (panelIndex === 0) {
542
+ window.MathJax.typeset([content]);
543
+ } else if (panel2Content) {
544
+ window.MathJax.typeset([panel2Content]);
545
+ }
352
546
  }
353
547
  }
354
548
 
549
+ /**
550
+ * Get file name from path
551
+ */
552
+ function getFileName(path) {
553
+ return path.split('/').pop();
554
+ }
555
+
355
556
  /**
356
557
  * Show welcome screen
357
558
  */
358
- function showWelcome() {
359
- content.classList.remove('visible');
360
- content.innerHTML = '';
361
- welcome.classList.remove('hidden');
559
+ function showWelcome(panelIndex) {
560
+ if (panelIndex === 0 || panelIndex === undefined) {
561
+ content.classList.remove('visible');
562
+ content.innerHTML = '';
563
+ welcome.classList.remove('hidden');
564
+ panel1Header.classList.remove('visible');
565
+ panel1Filename.textContent = '';
566
+ }
567
+ if (panelIndex === 1 && panel2Content) {
568
+ panel2Content.classList.remove('visible');
569
+ panel2Content.innerHTML = '';
570
+ panel2Header.classList.remove('visible');
571
+ panel2Filename.textContent = '';
572
+ }
362
573
  }
363
574
 
364
575
  /**
365
576
  * Show error message
366
577
  */
367
- function showError(type, message) {
578
+ function showError(_type, message) {
368
579
  welcome.classList.add('hidden');
369
580
  content.classList.add('visible');
370
581
  content.innerHTML = '<div class="vimd-error"><h2>Error</h2><p>' + escapeHtml(message) + '</p></div>';
@@ -384,8 +595,13 @@
384
595
  /**
385
596
  * Update URL without page reload
386
597
  */
387
- function updateURL(path) {
388
- history.pushState({ path: path }, '', path);
598
+ function updateURL() {
599
+ var path = state.panels[state.activePanel].file;
600
+ if (path) {
601
+ history.pushState({ path: path }, '', '/' + encodeURIComponent(path).replace(/%2F/g, '/'));
602
+ } else {
603
+ history.pushState({ path: '/' }, '', '/');
604
+ }
389
605
  }
390
606
 
391
607
  /**
@@ -393,12 +609,14 @@
393
609
  */
394
610
  function handleInitialPath() {
395
611
  var path = decodeURIComponent(location.pathname.slice(1));
396
- if (path && path !== '') {
612
+ if (path && path !== '' && !state.panels[0].file) {
397
613
  // Wait for tree to load then select file
398
614
  var checkTree = setInterval(function() {
399
615
  if (fileTree.length > 0) {
400
616
  clearInterval(checkTree);
401
- selectFile(path);
617
+ if (fileExists(path)) {
618
+ selectFile(path);
619
+ }
402
620
  }
403
621
  }, 100);
404
622
 
@@ -413,86 +631,370 @@
413
631
  * Setup event listeners
414
632
  */
415
633
  function setupEventListeners() {
416
- // Sidebar toggle buttons
417
- toggleBtn.addEventListener('click', function() {
418
- collapseSidebar();
419
- });
420
-
421
- toggleBtnCollapsed.addEventListener('click', function() {
422
- expandSidebar();
423
- });
424
-
425
- // Keyboard shortcut (Ctrl+B or Cmd+B)
426
- document.addEventListener('keydown', function(e) {
427
- if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
428
- e.preventDefault();
429
- if (sidebar.classList.contains('collapsed')) {
430
- expandSidebar();
431
- } else {
432
- collapseSidebar();
433
- }
434
- }
435
- });
436
-
437
- // Resizer
438
- resizer.addEventListener('mousedown', function(e) {
634
+ // Sidebar resizer
635
+ sidebarResizer.addEventListener('mousedown', function(e) {
439
636
  e.preventDefault();
440
- isResizing = true;
441
- resizer.classList.add('active');
637
+ isSidebarResizing = true;
638
+ sidebarResizer.classList.add('active');
442
639
  document.body.style.cursor = 'ew-resize';
443
640
  document.body.style.userSelect = 'none';
444
641
  });
445
642
 
446
643
  document.addEventListener('mousemove', function(e) {
447
- if (!isResizing) return;
644
+ if (isSidebarResizing) {
645
+ var width = e.clientX;
646
+ var minWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-min-width'), 10) || 150;
647
+ var maxWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-max-width'), 10) || 500;
648
+
649
+ if (width >= minWidth && width <= maxWidth) {
650
+ sidebar.style.width = width + 'px';
651
+ }
652
+ }
448
653
 
449
- var width = e.clientX;
450
- var minWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-min-width'), 10) || 150;
451
- var maxWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-max-width'), 10) || 500;
654
+ if (isPanelResizing && panelResizer) {
655
+ var sidebarWidth = sidebar.offsetWidth;
656
+ var panelWidth = e.clientX - sidebarWidth;
657
+ var minPanelWidth = 100;
658
+ var maxPanelWidth = preview.offsetWidth - minPanelWidth - 4; // 4px for resizer
452
659
 
453
- if (width >= minWidth && width <= maxWidth) {
454
- sidebar.style.width = width + 'px';
660
+ if (panelWidth >= minPanelWidth && panelWidth <= maxPanelWidth) {
661
+ panel1.style.width = panelWidth + 'px';
662
+ state.panelWidth = panelWidth;
663
+ }
455
664
  }
456
665
  });
457
666
 
458
667
  document.addEventListener('mouseup', function() {
459
- if (isResizing) {
460
- isResizing = false;
461
- resizer.classList.remove('active');
668
+ if (isSidebarResizing) {
669
+ isSidebarResizing = false;
670
+ sidebarResizer.classList.remove('active');
462
671
  document.body.style.cursor = '';
463
672
  document.body.style.userSelect = '';
464
673
  saveSidebarWidth();
465
674
  }
675
+
676
+ if (isPanelResizing) {
677
+ isPanelResizing = false;
678
+ if (panelResizer) {
679
+ panelResizer.classList.remove('active');
680
+ }
681
+ document.body.style.cursor = '';
682
+ document.body.style.userSelect = '';
683
+ savePanelState();
684
+ }
685
+ });
686
+
687
+ // Panel 1 close button
688
+ panel1Close.addEventListener('click', function(e) {
689
+ e.stopPropagation();
690
+ closePanel(0);
691
+ });
692
+
693
+ // Panel 1 activation
694
+ panel1.addEventListener('click', function() {
695
+ setActivePanel(0);
696
+ });
697
+
698
+ // Context menu
699
+ document.addEventListener('click', function() {
700
+ hideContextMenu();
466
701
  });
467
702
 
468
703
  // Browser history
469
704
  window.addEventListener('popstate', function(e) {
470
705
  if (e.state && e.state.path) {
471
- var path = e.state.path.slice(1); // Remove leading /
472
- if (path) {
473
- selectFile(path);
706
+ var path = e.state.path;
707
+ if (path === '/') {
708
+ closePanel(state.activePanel);
474
709
  } else {
475
- showWelcome();
476
- currentPath = null;
710
+ path = path.replace(/^\//, '');
711
+ if (fileExists(path)) {
712
+ selectFile(path);
713
+ }
477
714
  }
478
715
  }
479
716
  });
480
717
  }
481
718
 
482
719
  /**
483
- * Collapse sidebar
720
+ * Set active panel
721
+ */
722
+ function setActivePanel(index) {
723
+ if (state.activePanel !== index) {
724
+ state.activePanel = index;
725
+ updatePanelUI();
726
+ savePanelState();
727
+ updateURL();
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Close panel
733
+ */
734
+ function closePanel(index) {
735
+ if (state.isSplitView) {
736
+ if (index === 0) {
737
+ // Move panel 2 to panel 1
738
+ state.panels[0].file = state.panels[1].file;
739
+ state.panels[1].file = null;
740
+
741
+ // Move content from panel 2 to panel 1
742
+ if (panel2Content && state.panels[0].file) {
743
+ content.innerHTML = panel2Content.innerHTML;
744
+ content.classList.add('visible');
745
+ welcome.classList.add('hidden');
746
+ panel1Filename.textContent = getFileName(state.panels[0].file);
747
+ panel1Header.classList.add('visible');
748
+ } else {
749
+ showWelcome(0);
750
+ }
751
+ }
752
+
753
+ // Close split view
754
+ closeSplitView();
755
+ } else {
756
+ // Single panel - show welcome
757
+ state.panels[0].file = null;
758
+ showWelcome(0);
759
+ savePanelState();
760
+ updateURL();
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Close split view
766
+ */
767
+ function closeSplitView() {
768
+ state.isSplitView = false;
769
+ state.panels[1].file = null;
770
+ state.activePanel = 0;
771
+
772
+ // Remove panel 2
773
+ if (panel2) {
774
+ panel2.remove();
775
+ panel2 = null;
776
+ panel2Header = null;
777
+ panel2Filename = null;
778
+ panel2Close = null;
779
+ panel2Content = null;
780
+ }
781
+
782
+ // Remove resizer
783
+ if (panelResizer) {
784
+ panelResizer.remove();
785
+ panelResizer = null;
786
+ }
787
+
788
+ // Reset panel 1 to single panel mode
789
+ panel1.classList.remove('split-panel1');
790
+ panel1.style.width = '';
791
+
792
+ updatePanelUI();
793
+ savePanelState();
794
+ }
795
+
796
+ /**
797
+ * Create panel 2 for split view
798
+ */
799
+ function createPanel2() {
800
+ if (panel2) return;
801
+
802
+ // Create resizer
803
+ panelResizer = document.createElement('div');
804
+ panelResizer.className = 'vimd-panel-resizer visible';
805
+ panelResizer.addEventListener('mousedown', function(e) {
806
+ e.preventDefault();
807
+ isPanelResizing = true;
808
+ panelResizer.classList.add('active');
809
+ document.body.style.cursor = 'ew-resize';
810
+ document.body.style.userSelect = 'none';
811
+ });
812
+
813
+ // Create panel 2
814
+ panel2 = document.createElement('div');
815
+ panel2.className = 'vimd-panel split-panel2';
816
+ panel2.id = 'panel2';
817
+
818
+ panel2Header = document.createElement('div');
819
+ panel2Header.className = 'vimd-panel-header';
820
+ panel2Header.id = 'panel2-header';
821
+
822
+ panel2Filename = document.createElement('span');
823
+ panel2Filename.className = 'vimd-panel-filename';
824
+ panel2Filename.id = 'panel2-filename';
825
+
826
+ panel2Close = document.createElement('button');
827
+ panel2Close.className = 'vimd-panel-close';
828
+ panel2Close.id = 'panel2-close';
829
+ panel2Close.title = '閉じる';
830
+ panel2Close.innerHTML = '&times;';
831
+ panel2Close.addEventListener('click', function(e) {
832
+ e.stopPropagation();
833
+ closePanel(1);
834
+ });
835
+
836
+ panel2Header.appendChild(panel2Filename);
837
+ panel2Header.appendChild(panel2Close);
838
+
839
+ var panel2Body = document.createElement('div');
840
+ panel2Body.className = 'vimd-panel-body';
841
+
842
+ panel2Content = document.createElement('article');
843
+ panel2Content.className = 'vimd-content markdown-body';
844
+ panel2Content.id = 'content2';
845
+
846
+ panel2Body.appendChild(panel2Content);
847
+ panel2.appendChild(panel2Header);
848
+ panel2.appendChild(panel2Body);
849
+
850
+ // Panel 2 activation
851
+ panel2.addEventListener('click', function() {
852
+ setActivePanel(1);
853
+ });
854
+
855
+ // Insert into DOM
856
+ preview.appendChild(panelResizer);
857
+ preview.appendChild(panel2);
858
+
859
+ // Set panel 1 to split mode
860
+ panel1.classList.add('split-panel1');
861
+ panel1.style.width = state.panelWidth + 'px';
862
+ }
863
+
864
+ /**
865
+ * Open split view
866
+ */
867
+ function openSplitView(path) {
868
+ if (!state.isSplitView) {
869
+ state.isSplitView = true;
870
+ state.panelWidth = 720; // Reset to default
871
+ createPanel2();
872
+ }
873
+
874
+ openFileInPanel(path, 1);
875
+ }
876
+
877
+ /**
878
+ * Show context menu
879
+ */
880
+ function showContextMenu(e, path) {
881
+ contextTarget = path;
882
+
883
+ // Update menu items visibility
884
+ var openItem = contextMenu.querySelector('[data-action="open"]');
885
+ var openSplitItem = contextMenu.querySelector('[data-action="open-split"]');
886
+ var openPanel1Item = contextMenu.querySelector('[data-action="open-panel1"]');
887
+ var openPanel2Item = contextMenu.querySelector('[data-action="open-panel2"]');
888
+
889
+ if (state.isSplitView) {
890
+ // 2 panels mode
891
+ openItem.classList.add('hidden');
892
+ openSplitItem.classList.add('hidden');
893
+ openPanel1Item.classList.remove('hidden');
894
+ openPanel2Item.classList.remove('hidden');
895
+ } else if (state.panels[0].file) {
896
+ // 1 panel with file
897
+ openItem.classList.remove('hidden');
898
+ openSplitItem.classList.remove('hidden');
899
+ openPanel1Item.classList.add('hidden');
900
+ openPanel2Item.classList.add('hidden');
901
+ } else {
902
+ // Welcome screen
903
+ openItem.classList.remove('hidden');
904
+ openSplitItem.classList.add('hidden');
905
+ openPanel1Item.classList.add('hidden');
906
+ openPanel2Item.classList.add('hidden');
907
+ }
908
+
909
+ // Position menu
910
+ contextMenu.style.left = e.clientX + 'px';
911
+ contextMenu.style.top = e.clientY + 'px';
912
+ contextMenu.classList.add('visible');
913
+
914
+ // Adjust position if off-screen
915
+ var rect = contextMenu.getBoundingClientRect();
916
+ if (rect.right > window.innerWidth) {
917
+ contextMenu.style.left = (window.innerWidth - rect.width - 5) + 'px';
918
+ }
919
+ if (rect.bottom > window.innerHeight) {
920
+ contextMenu.style.top = (window.innerHeight - rect.height - 5) + 'px';
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Hide context menu
484
926
  */
485
- function collapseSidebar() {
486
- sidebar.classList.add('collapsed');
487
- toggleBar.classList.add('visible');
927
+ function hideContextMenu() {
928
+ contextMenu.classList.remove('visible');
929
+ contextTarget = null;
488
930
  }
489
931
 
490
932
  /**
491
- * Expand sidebar
933
+ * Handle context menu action
492
934
  */
493
- function expandSidebar() {
494
- sidebar.classList.remove('collapsed');
495
- toggleBar.classList.remove('visible');
935
+ function handleContextAction(action) {
936
+ if (!contextTarget) return;
937
+
938
+ switch (action) {
939
+ case 'open':
940
+ selectFile(contextTarget);
941
+ break;
942
+ case 'open-split':
943
+ openSplitView(contextTarget);
944
+ break;
945
+ case 'open-panel1':
946
+ openFileInPanel(contextTarget, 0);
947
+ break;
948
+ case 'open-panel2':
949
+ openFileInPanel(contextTarget, 1);
950
+ break;
951
+ }
952
+
953
+ hideContextMenu();
954
+ }
955
+
956
+ // Setup context menu item click handlers
957
+ contextMenu.addEventListener('click', function(e) {
958
+ var item = e.target.closest('.vimd-context-item');
959
+ if (item) {
960
+ var action = item.getAttribute('data-action');
961
+ handleContextAction(action);
962
+ }
963
+ });
964
+
965
+ /**
966
+ * Update panel UI based on state
967
+ */
968
+ function updatePanelUI() {
969
+ // Update active panel indicator
970
+ panel1Header.classList.remove('active');
971
+ if (panel2Header) {
972
+ panel2Header.classList.remove('active');
973
+ }
974
+
975
+ if (state.isSplitView) {
976
+ if (state.activePanel === 0) {
977
+ panel1Header.classList.add('active');
978
+ } else if (panel2Header) {
979
+ panel2Header.classList.add('active');
980
+ }
981
+ }
982
+
983
+ // Update panel 1 header visibility
984
+ if (state.panels[0].file) {
985
+ panel1Header.classList.add('visible');
986
+ } else {
987
+ panel1Header.classList.remove('visible');
988
+ }
989
+
990
+ // Update panel 2 header visibility
991
+ if (panel2Header) {
992
+ if (state.panels[1].file) {
993
+ panel2Header.classList.add('visible');
994
+ } else {
995
+ panel2Header.classList.remove('visible');
996
+ }
997
+ }
496
998
  }
497
999
 
498
1000
  /**
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="generator" content="vimd">
7
7
  <title>vimd - {{folder_name}}</title>
8
- <style>
9
- {{folder_mode_css}}
10
- </style>
11
8
  <style id="theme-styles">
12
9
  {{theme_css}}
13
10
  </style>
11
+ <style>
12
+ {{folder_mode_css}}
13
+ </style>
14
14
  {{#if math_enabled}}
15
15
  <style>
16
16
  /* Block math centering */
@@ -40,24 +40,10 @@
40
40
  </head>
41
41
  <body>
42
42
  <div class="vimd-container">
43
- <!-- Sidebar toggle bar (visible when sidebar is collapsed) -->
44
- <div class="vimd-sidebar-toggle-bar" id="toggle-bar">
45
- <button class="vimd-toggle-btn" id="toggle-btn-collapsed" title="Open Sidebar (Ctrl+B)">
46
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
47
- <path d="M6 2L10 8L6 14" stroke="currentColor" stroke-width="1.5" fill="none"/>
48
- </svg>
49
- </button>
50
- </div>
51
-
52
43
  <!-- Sidebar -->
53
44
  <aside class="vimd-sidebar" id="sidebar">
54
45
  <div class="vimd-sidebar-header">
55
46
  <span class="vimd-folder-name">エクスプローラー</span>
56
- <button class="vimd-toggle-btn" id="toggle-btn" title="Close Sidebar (Ctrl+B)">
57
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
58
- <path d="M10 2L6 8L10 14" stroke="currentColor" stroke-width="1.5" fill="none"/>
59
- </svg>
60
- </button>
61
47
  </div>
62
48
  <div class="vimd-tree" id="file-tree">
63
49
  <!-- File tree will be rendered by JavaScript -->
@@ -67,18 +53,35 @@
67
53
 
68
54
  <!-- Preview -->
69
55
  <main class="vimd-preview" id="preview">
70
- <div class="vimd-welcome" id="welcome">
71
- <div class="vimd-welcome-content">
72
- <h1>vimd</h1>
73
- <p id="welcome-message">Select a file from the sidebar</p>
56
+ <!-- Panel 1 -->
57
+ <div class="vimd-panel" id="panel1">
58
+ <div class="vimd-panel-header" id="panel1-header">
59
+ <span class="vimd-panel-filename" id="panel1-filename"></span>
60
+ <button class="vimd-panel-close" id="panel1-close" title="閉じる">&times;</button>
61
+ </div>
62
+ <div class="vimd-panel-body">
63
+ <div class="vimd-welcome" id="welcome">
64
+ <div class="vimd-welcome-content">
65
+ <h1>vimd</h1>
66
+ <p id="welcome-message">Select a file from the sidebar</p>
67
+ </div>
68
+ </div>
69
+ <article class="vimd-content markdown-body" id="content">
70
+ <!-- File content will be rendered here -->
71
+ </article>
74
72
  </div>
75
73
  </div>
76
- <article class="vimd-content markdown-body" id="content">
77
- <!-- File content will be rendered here -->
78
- </article>
79
74
  </main>
80
75
  </div>
81
76
 
77
+ <!-- Context Menu -->
78
+ <div class="vimd-context-menu" id="context-menu">
79
+ <div class="vimd-context-item" data-action="open">開く</div>
80
+ <div class="vimd-context-item" data-action="open-split">分割画面で開く</div>
81
+ <div class="vimd-context-item" data-action="open-panel1">パネル1で開く</div>
82
+ <div class="vimd-context-item" data-action="open-panel2">パネル2で開く</div>
83
+ </div>
84
+
82
85
  <script>
83
86
  {{folder_mode_js}}
84
87
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimd",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Real-time Markdown preview tool with pandoc (view markdown)",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="generator" content="vimd">
7
7
  <title>vimd - {{folder_name}}</title>
8
- <style>
9
- {{folder_mode_css}}
10
- </style>
11
8
  <style id="theme-styles">
12
9
  {{theme_css}}
13
10
  </style>
11
+ <style>
12
+ {{folder_mode_css}}
13
+ </style>
14
14
  {{#if math_enabled}}
15
15
  <style>
16
16
  /* Block math centering */
@@ -40,24 +40,10 @@
40
40
  </head>
41
41
  <body>
42
42
  <div class="vimd-container">
43
- <!-- Sidebar toggle bar (visible when sidebar is collapsed) -->
44
- <div class="vimd-sidebar-toggle-bar" id="toggle-bar">
45
- <button class="vimd-toggle-btn" id="toggle-btn-collapsed" title="Open Sidebar (Ctrl+B)">
46
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
47
- <path d="M6 2L10 8L6 14" stroke="currentColor" stroke-width="1.5" fill="none"/>
48
- </svg>
49
- </button>
50
- </div>
51
-
52
43
  <!-- Sidebar -->
53
44
  <aside class="vimd-sidebar" id="sidebar">
54
45
  <div class="vimd-sidebar-header">
55
46
  <span class="vimd-folder-name">エクスプローラー</span>
56
- <button class="vimd-toggle-btn" id="toggle-btn" title="Close Sidebar (Ctrl+B)">
57
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
58
- <path d="M10 2L6 8L10 14" stroke="currentColor" stroke-width="1.5" fill="none"/>
59
- </svg>
60
- </button>
61
47
  </div>
62
48
  <div class="vimd-tree" id="file-tree">
63
49
  <!-- File tree will be rendered by JavaScript -->
@@ -67,18 +53,35 @@
67
53
 
68
54
  <!-- Preview -->
69
55
  <main class="vimd-preview" id="preview">
70
- <div class="vimd-welcome" id="welcome">
71
- <div class="vimd-welcome-content">
72
- <h1>vimd</h1>
73
- <p id="welcome-message">Select a file from the sidebar</p>
56
+ <!-- Panel 1 -->
57
+ <div class="vimd-panel" id="panel1">
58
+ <div class="vimd-panel-header" id="panel1-header">
59
+ <span class="vimd-panel-filename" id="panel1-filename"></span>
60
+ <button class="vimd-panel-close" id="panel1-close" title="閉じる">&times;</button>
61
+ </div>
62
+ <div class="vimd-panel-body">
63
+ <div class="vimd-welcome" id="welcome">
64
+ <div class="vimd-welcome-content">
65
+ <h1>vimd</h1>
66
+ <p id="welcome-message">Select a file from the sidebar</p>
67
+ </div>
68
+ </div>
69
+ <article class="vimd-content markdown-body" id="content">
70
+ <!-- File content will be rendered here -->
71
+ </article>
74
72
  </div>
75
73
  </div>
76
- <article class="vimd-content markdown-body" id="content">
77
- <!-- File content will be rendered here -->
78
- </article>
79
74
  </main>
80
75
  </div>
81
76
 
77
+ <!-- Context Menu -->
78
+ <div class="vimd-context-menu" id="context-menu">
79
+ <div class="vimd-context-item" data-action="open">開く</div>
80
+ <div class="vimd-context-item" data-action="open-split">分割画面で開く</div>
81
+ <div class="vimd-context-item" data-action="open-panel1">パネル1で開く</div>
82
+ <div class="vimd-context-item" data-action="open-panel2">パネル2で開く</div>
83
+ </div>
84
+
82
85
  <script>
83
86
  {{folder_mode_js}}
84
87
  </script>