mdv-live 0.3.0 → 0.3.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.
package/src/static/app.js CHANGED
@@ -139,7 +139,6 @@
139
139
  return FILE_ICONS[iconName] || FILE_ICONS.default;
140
140
  }
141
141
 
142
- // Scroll position utilities
143
142
  function saveScrollPosition(element) {
144
143
  return element.scrollTop;
145
144
  }
@@ -150,7 +149,6 @@
150
149
  });
151
150
  }
152
151
 
153
- // API utilities
154
152
  async function apiRequest(url, options = {}) {
155
153
  const response = await fetch(url, options);
156
154
  const data = await response.json();
@@ -168,7 +166,6 @@
168
166
  });
169
167
  }
170
168
 
171
- // Tab path update utility (shared by rename and move)
172
169
  function updateTabPaths(oldPath, newPath) {
173
170
  let updated = false;
174
171
  const newName = newPath.split('/').pop();
@@ -306,12 +303,13 @@
306
303
  }
307
304
  };
308
305
 
309
- state.ws.onmessage = (event) => {
306
+ state.ws.onmessage = async (event) => {
310
307
  const data = JSON.parse(event.data);
311
308
  if (data.type === 'file_update' && state.activeTabIndex >= 0) {
312
309
  this.handleFileUpdate(data);
313
- } else if (data.type === 'tree_update' && data.tree) {
314
- FileTreeManager.update(data.tree);
310
+ } else if (data.type === 'tree_update') {
311
+ // tree_update を受信したらAPIから最新ツリーを取得
312
+ await FileTreeManager.refresh();
315
313
  }
316
314
  };
317
315
 
@@ -332,20 +330,17 @@
332
330
 
333
331
  handleFileUpdate(data) {
334
332
  const tab = state.tabs[state.activeTabIndex];
333
+
335
334
  if (data.fileType === 'image' && data.reload) {
336
335
  ContentRenderer.renderImage(tab.imageUrl, tab.name);
337
336
  return;
338
337
  }
338
+
339
339
  if (!data.content) return;
340
340
 
341
341
  tab.content = data.content;
342
- if (data.raw) {
343
- tab.raw = data.raw;
344
- }
345
- // Update Marp flag
346
- if (typeof data.isMarp !== 'undefined') {
347
- tab.isMarp = data.isMarp;
348
- }
342
+ if (data.raw) tab.raw = data.raw;
343
+ if (typeof data.isMarp !== 'undefined') tab.isMarp = data.isMarp;
349
344
 
350
345
  if (state.isEditMode) {
351
346
  if (!state.hasUnsavedChanges && data.raw) {
@@ -356,17 +351,16 @@
356
351
  restoreScrollPosition(textarea, currentScroll);
357
352
  }
358
353
  }
354
+ return;
355
+ }
356
+
357
+ if (tab.isMarp) {
358
+ if (data.css) tab.css = data.css;
359
+ ContentRenderer.renderMarp(data.content, tab.css);
359
360
  } else {
360
- // Marp slides: preserve current slide position
361
- if (tab.isMarp) {
362
- // Update css if provided
363
- if (data.css) tab.css = data.css;
364
- ContentRenderer.renderMarp(data.content, tab.css);
365
- } else {
366
- const currentScroll = saveScrollPosition(elements.content);
367
- ContentRenderer.render(data.content, data.fileType || tab.fileType);
368
- restoreScrollPosition(elements.content, currentScroll);
369
- }
361
+ const currentScroll = saveScrollPosition(elements.content);
362
+ ContentRenderer.render(data.content, data.fileType || tab.fileType);
363
+ restoreScrollPosition(elements.content, currentScroll);
370
364
  }
371
365
  }
372
366
  };
@@ -395,6 +389,17 @@
395
389
  elements.fileTree.innerHTML = '<div style="padding: 16px; color: var(--text-muted);">読み込みに失敗しました。<br><button onclick="location.reload()" style="margin-top: 8px; cursor: pointer;">再読み込み</button></div>';
396
390
  },
397
391
 
392
+ async refresh() {
393
+ try {
394
+ const response = await fetch('/api/tree');
395
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
396
+ const tree = await response.json();
397
+ await this.update(tree);
398
+ } catch (e) {
399
+ console.error('Failed to refresh tree:', e);
400
+ }
401
+ },
402
+
398
403
  async update(tree) {
399
404
  // 展開済みかつ読み込み済みのパスを保存
400
405
  const expandedPaths = new Set();
@@ -509,9 +514,9 @@
509
514
 
510
515
  const ContentRenderer = {
511
516
  render(htmlContent, fileType) {
512
- // コードファイル(非markdown)は専用のスタイルを適用
513
- const isCodeFile = fileType === 'code';
514
- const containerClass = isCodeFile ? 'markdown-body code-view-container' : 'markdown-body';
517
+ const containerClass = fileType === 'code'
518
+ ? 'markdown-body code-view-container'
519
+ : 'markdown-body';
515
520
  elements.content.innerHTML = `<div class="${containerClass}">${htmlContent}</div>`;
516
521
 
517
522
  elements.content.querySelectorAll('pre code').forEach(block => {
@@ -860,15 +865,17 @@
860
865
  state.activeTabIndex = -1;
861
866
  this.render();
862
867
  ContentRenderer.showWelcome();
863
- } else {
864
- if (state.activeTabIndex >= state.tabs.length) {
865
- state.activeTabIndex = state.tabs.length - 1;
866
- } else if (index < state.activeTabIndex) {
867
- state.activeTabIndex--;
868
- }
869
- this.render();
870
- this.renderActive();
868
+ FileTreeManager.updateHighlight();
869
+ return;
870
+ }
871
+
872
+ if (state.activeTabIndex >= state.tabs.length) {
873
+ state.activeTabIndex = state.tabs.length - 1;
874
+ } else if (index < state.activeTabIndex) {
875
+ state.activeTabIndex--;
871
876
  }
877
+ this.render();
878
+ this.renderActive();
872
879
  FileTreeManager.updateHighlight();
873
880
  },
874
881
 
@@ -900,36 +907,28 @@
900
907
  },
901
908
 
902
909
  renderByFileType(tab) {
903
- // Clean up Marp state when switching tabs
904
910
  ContentRenderer.cleanupMarp();
905
911
 
906
- // Marp slides
907
912
  if (tab.isMarp) {
908
913
  ContentRenderer.renderMarp(tab.content, tab.css);
909
914
  return;
910
915
  }
911
916
 
912
- switch (tab.fileType) {
913
- case 'image':
914
- ContentRenderer.renderImage(tab.imageUrl, tab.name);
915
- break;
916
- case 'pdf':
917
- ContentRenderer.renderPDF(tab.pdfUrl, tab.name);
918
- break;
919
- case 'video':
920
- ContentRenderer.renderVideo(tab.mediaUrl, tab.name);
921
- break;
922
- case 'audio':
923
- ContentRenderer.renderAudio(tab.mediaUrl, tab.name);
924
- break;
925
- case 'archive':
926
- case 'office':
927
- case 'executable':
928
- case 'binary':
929
- ContentRenderer.renderBinary(tab.name, tab.fileType);
930
- break;
931
- default:
932
- ContentRenderer.render(tab.content, tab.fileType);
917
+ const fileType = tab.fileType;
918
+ const binaryTypes = ['archive', 'office', 'executable', 'binary'];
919
+
920
+ if (fileType === 'image') {
921
+ ContentRenderer.renderImage(tab.imageUrl, tab.name);
922
+ } else if (fileType === 'pdf') {
923
+ ContentRenderer.renderPDF(tab.pdfUrl, tab.name);
924
+ } else if (fileType === 'video') {
925
+ ContentRenderer.renderVideo(tab.mediaUrl, tab.name);
926
+ } else if (fileType === 'audio') {
927
+ ContentRenderer.renderAudio(tab.mediaUrl, tab.name);
928
+ } else if (binaryTypes.includes(fileType)) {
929
+ ContentRenderer.renderBinary(tab.name, fileType);
930
+ } else {
931
+ ContentRenderer.render(tab.content, fileType);
933
932
  }
934
933
  }
935
934
  };
@@ -954,13 +953,8 @@
954
953
  },
955
954
 
956
955
  updateButton() {
957
- if (state.isEditMode) {
958
- elements.editToggle.classList.add('active');
959
- elements.editLabel.textContent = 'View';
960
- } else {
961
- elements.editToggle.classList.remove('active');
962
- elements.editLabel.textContent = 'Edit';
963
- }
956
+ elements.editToggle.classList.toggle('active', state.isEditMode);
957
+ elements.editLabel.textContent = state.isEditMode ? 'View' : 'Edit';
964
958
  },
965
959
 
966
960
  show() {
@@ -1051,21 +1045,15 @@
1051
1045
 
1052
1046
  elements.editorStatus.style.display = 'none';
1053
1047
 
1054
- // サーバーから最新のレンダリング済みコンテンツを取得
1055
1048
  try {
1056
1049
  const response = await fetch(`/api/file?path=${encodeURIComponent(tab.path)}`);
1057
1050
  const data = await response.json();
1058
- if (data.content) {
1059
- tab.content = data.content;
1060
- }
1061
- if (data.raw) {
1062
- tab.raw = data.raw;
1063
- }
1051
+ if (data.content) tab.content = data.content;
1052
+ if (data.raw) tab.raw = data.raw;
1064
1053
  } catch (e) {
1065
1054
  console.error('Failed to fetch updated content:', e);
1066
1055
  }
1067
1056
 
1068
- // WebSocket経由でファイル監視を再登録(重要)
1069
1057
  WebSocketManager.watchFile(tab.path);
1070
1058
 
1071
1059
  state.skipScrollRestore = true;
@@ -1169,7 +1157,6 @@
1169
1157
 
1170
1158
  const PrintManager = {
1171
1159
  isMarpPresentation() {
1172
- // Check if current content has .marpit class (Marp presentation)
1173
1160
  return !!elements.content.querySelector('.marpit');
1174
1161
  },
1175
1162
 
@@ -1179,10 +1166,8 @@
1179
1166
  const tab = state.tabs[state.activeTabIndex];
1180
1167
 
1181
1168
  if (this.isMarpPresentation()) {
1182
- // Marp → use marp-cli for PDF export
1183
1169
  await this.exportMarpPdf(tab.path);
1184
1170
  } else {
1185
- // Regular Markdown → use browser print
1186
1171
  this.browserPrint(tab.name);
1187
1172
  }
1188
1173
  },
@@ -1249,12 +1234,9 @@
1249
1234
 
1250
1235
  const ShutdownManager = {
1251
1236
  async shutdown() {
1252
- try {
1253
- elements.statusText.textContent = 'Stopping...';
1254
- await fetch('/api/shutdown', { method: 'POST' });
1255
- } catch (e) {
1256
- // Server stopped, connection will fail - expected
1257
- }
1237
+ elements.statusText.textContent = 'Stopping...';
1238
+ // Connection failure is expected when server stops
1239
+ fetch('/api/shutdown', { method: 'POST' }).catch(() => {});
1258
1240
  },
1259
1241
 
1260
1242
  init() {
@@ -1272,11 +1254,13 @@
1272
1254
 
1273
1255
  show(title, options = {}) {
1274
1256
  elements.dialogTitle.textContent = title;
1275
- elements.dialogInput.style.display = options.showInput ? 'block' : 'none';
1276
- elements.dialogMessage.textContent = options.message || '';
1277
- elements.dialogMessage.style.display = options.message ? 'block' : 'none';
1257
+ const hasInput = options.showInput;
1258
+ const hasMessage = options.message;
1259
+ elements.dialogInput.style.display = hasInput ? 'block' : 'none';
1260
+ elements.dialogMessage.textContent = hasMessage || '';
1261
+ elements.dialogMessage.style.display = hasMessage ? 'block' : 'none';
1278
1262
 
1279
- if (options.showInput) {
1263
+ if (hasInput) {
1280
1264
  elements.dialogInput.value = options.defaultValue || '';
1281
1265
  }
1282
1266
 
@@ -1288,7 +1272,7 @@
1288
1272
 
1289
1273
  elements.dialogOverlay.classList.remove('hidden');
1290
1274
 
1291
- if (options.showInput) {
1275
+ if (hasInput) {
1292
1276
  setTimeout(() => {
1293
1277
  elements.dialogInput.focus();
1294
1278
  elements.dialogInput.select();
@@ -1477,7 +1461,7 @@
1477
1461
  this.currentPath = path;
1478
1462
  this.isDirectory = isDir;
1479
1463
 
1480
- const items = this.getMenuItems(isDir, path);
1464
+ const items = this.getMenuItems(isDir);
1481
1465
  elements.contextMenu.innerHTML = items.map(item => {
1482
1466
  if (item.separator) {
1483
1467
  return '<div class="context-menu-separator"></div>';
@@ -1499,8 +1483,7 @@
1499
1483
  this.currentPath = null;
1500
1484
  },
1501
1485
 
1502
- getMenuItems(isDir, path) {
1503
- const pathDisplay = state.rootPath ? `${state.rootPath}/${path}` : path;
1486
+ getMenuItems(isDir) {
1504
1487
  if (isDir) {
1505
1488
  return [
1506
1489
  { label: '新規フォルダ', action: 'newFolder' },
@@ -1511,17 +1494,16 @@
1511
1494
  { separator: true },
1512
1495
  { label: '削除', action: 'delete', danger: true }
1513
1496
  ];
1514
- } else {
1515
- return [
1516
- { label: '開く', action: 'open' },
1517
- { label: 'ダウンロード', action: 'download' },
1518
- { separator: true },
1519
- { label: '名前を変更', action: 'rename' },
1520
- { label: 'パスをコピー', action: 'copyPath' },
1521
- { separator: true },
1522
- { label: '削除', action: 'delete', danger: true }
1523
- ];
1524
1497
  }
1498
+ return [
1499
+ { label: '開く', action: 'open' },
1500
+ { label: 'ダウンロード', action: 'download' },
1501
+ { separator: true },
1502
+ { label: '名前を変更', action: 'rename' },
1503
+ { label: 'パスをコピー', action: 'copyPath' },
1504
+ { separator: true },
1505
+ { label: '削除', action: 'delete', danger: true }
1506
+ ];
1525
1507
  },
1526
1508
 
1527
1509
  handleAction(action) {
@@ -1529,35 +1511,25 @@
1529
1511
  const isDir = this.isDirectory;
1530
1512
  this.hide();
1531
1513
 
1532
- switch (action) {
1533
- case 'open':
1534
- TabManager.open(path);
1535
- break;
1536
- case 'download':
1537
- FileOperationsManager.download(path);
1538
- break;
1539
- case 'rename':
1540
- FileOperationsManager.renameItem(path, isDir);
1541
- break;
1542
- case 'delete':
1543
- FileOperationsManager.deleteItem(path, isDir);
1544
- break;
1545
- case 'newFolder':
1546
- FileOperationsManager.createDirectory(path);
1547
- break;
1548
- case 'upload':
1549
- state.uploadTargetPath = path;
1550
- elements.fileInput.click();
1551
- break;
1552
- case 'copyPath':
1553
- const fullPath = state.rootPath ? `${state.rootPath}/${path}` : path;
1554
- navigator.clipboard.writeText(fullPath).then(() => {
1555
- console.log('パスをコピーしました:', fullPath);
1556
- }).catch(err => {
1557
- console.error('コピーに失敗:', err);
1558
- alert('パスのコピーに失敗しました');
1559
- });
1560
- break;
1514
+ if (action === 'open') {
1515
+ TabManager.open(path);
1516
+ } else if (action === 'download') {
1517
+ FileOperationsManager.download(path);
1518
+ } else if (action === 'rename') {
1519
+ FileOperationsManager.renameItem(path, isDir);
1520
+ } else if (action === 'delete') {
1521
+ FileOperationsManager.deleteItem(path, isDir);
1522
+ } else if (action === 'newFolder') {
1523
+ FileOperationsManager.createDirectory(path);
1524
+ } else if (action === 'upload') {
1525
+ state.uploadTargetPath = path;
1526
+ elements.fileInput.click();
1527
+ } else if (action === 'copyPath') {
1528
+ const fullPath = state.rootPath ? `${state.rootPath}/${path}` : path;
1529
+ navigator.clipboard.writeText(fullPath).catch(err => {
1530
+ console.error('コピーに失敗:', err);
1531
+ alert('パスのコピーに失敗しました');
1532
+ });
1561
1533
  }
1562
1534
  },
1563
1535
 
@@ -1831,6 +1803,7 @@
1831
1803
  }
1832
1804
 
1833
1805
  async function init() {
1806
+ // Initialize all managers
1834
1807
  ThemeManager.init();
1835
1808
  SidebarManager.init();
1836
1809
  ResizeHandler.init();
@@ -1841,7 +1814,7 @@
1841
1814
  ContextMenuManager.init();
1842
1815
  DragDropManager.init();
1843
1816
  KeyboardManager.init();
1844
- TabManager.render(); // 初期状態でタブバーを非表示
1817
+ TabManager.render();
1845
1818
 
1846
1819
  try {
1847
1820
  const infoResponse = await fetch('/api/info');
@@ -1854,20 +1827,14 @@
1854
1827
  await FileTreeManager.load();
1855
1828
  WebSocketManager.connect();
1856
1829
 
1857
- // ブラウザタブがアクティブになった時に最新データを取得
1830
+ // Refresh content when window regains focus
1831
+ const handleFocusChange = () => refreshCurrentTab();
1858
1832
  document.addEventListener('visibilitychange', () => {
1859
- if (document.visibilityState === 'visible') {
1860
- refreshCurrentTab();
1861
- }
1862
- });
1863
-
1864
- // ウィンドウがフォーカスされた時も取得
1865
- window.addEventListener('focus', () => {
1866
- refreshCurrentTab();
1833
+ if (document.visibilityState === 'visible') handleFocusChange();
1867
1834
  });
1835
+ window.addEventListener('focus', handleFocusChange);
1868
1836
 
1869
- const params = new URLSearchParams(window.location.search);
1870
- const initialFile = params.get('file');
1837
+ const initialFile = new URLSearchParams(window.location.search).get('file');
1871
1838
  if (initialFile) {
1872
1839
  TabManager.open(initialFile);
1873
1840
  }
@@ -4,80 +4,103 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>MDV - Markdown Viewer</title>
7
+
8
+ <!-- Favicon -->
7
9
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
10
  <link rel="icon" type="image/png" sizes="32x32" href="/static/images/icon-32.png">
9
11
  <link rel="icon" type="image/png" sizes="64x64" href="/static/images/icon-64.png">
10
12
  <link rel="apple-touch-icon" sizes="128x128" href="/static/images/icon-128.png">
13
+
14
+ <!-- Stylesheets -->
11
15
  <link id="hljs-theme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
12
16
  <link rel="stylesheet" href="/static/styles.css">
17
+
18
+ <!-- Theme initialization (FOUC prevention) -->
13
19
  <script>
14
- // テーマを即座に適用(FOUC防止)
15
- var __mdvTheme = localStorage.getItem('mdv-theme') || 'light';
20
+ (function() {
21
+ var theme = localStorage.getItem('mdv-theme') || 'light';
22
+ window.__mdvTheme = theme;
23
+ document.documentElement.dataset.theme = theme;
24
+ })();
16
25
  </script>
26
+
27
+ <!-- External libraries -->
17
28
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
18
29
  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
19
30
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
20
31
  </head>
21
32
  <body>
22
33
  <script>document.body.dataset.theme = __mdvTheme;</script>
34
+
23
35
  <div class="container">
24
- <aside class="sidebar" id="sidebar">
36
+ <!-- Sidebar -->
37
+ <aside class="sidebar" id="sidebar" aria-label="File browser">
25
38
  <div class="sidebar-header">
26
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
39
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
27
40
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
28
41
  </svg>
29
42
  <span>Files</span>
30
43
  </div>
31
- <div class="file-tree" id="fileTree"></div>
44
+ <nav class="file-tree" id="fileTree" aria-label="File tree"></nav>
32
45
  </aside>
33
46
 
34
- <div class="resize-handle" id="resizeHandle"></div>
47
+ <div class="resize-handle" id="resizeHandle" role="separator" aria-orientation="vertical"></div>
35
48
 
49
+ <!-- Main content -->
36
50
  <main class="main">
37
- <div class="toolbar">
38
- <button class="toolbar-btn" id="sidebarToggle" title="Toggle sidebar (Cmd+B)">
39
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
51
+ <!-- Toolbar -->
52
+ <div class="toolbar" role="toolbar" aria-label="Document actions">
53
+ <button class="toolbar-btn" id="sidebarToggle" title="Toggle sidebar (Cmd+B)" aria-label="Toggle sidebar">
54
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
40
55
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
41
56
  </svg>
42
57
  </button>
43
- <button class="toolbar-btn" id="themeToggle" title="Toggle theme">
44
- <svg id="sunIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
58
+
59
+ <button class="toolbar-btn" id="themeToggle" title="Toggle theme" aria-label="Toggle theme">
60
+ <svg id="sunIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
45
61
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
46
62
  </svg>
47
- <svg id="moonIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="display: none;">
63
+ <svg id="moonIcon" style="display: none;" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
48
64
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
49
65
  </svg>
50
66
  </button>
51
- <button class="toolbar-btn" id="editToggle" title="Toggle edit mode (Cmd+E)">
52
- <svg id="editIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
67
+
68
+ <button class="toolbar-btn" id="editToggle" title="Toggle edit mode (Cmd+E)" aria-label="Toggle edit mode">
69
+ <svg id="editIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
53
70
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
54
71
  </svg>
55
72
  <span id="editLabel">Edit</span>
56
73
  </button>
57
- <button class="toolbar-btn" id="printBtn" title="Print / Save PDF (Cmd+P)">
58
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
74
+
75
+ <button class="toolbar-btn" id="printBtn" title="Print / Save PDF (Cmd+P)" aria-label="Export to PDF">
76
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
59
77
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
60
78
  </svg>
61
79
  PDF
62
80
  </button>
81
+
63
82
  <div class="toolbar-spacer"></div>
83
+
64
84
  <span class="editor-status" id="editorStatus" style="display: none;">Ready</span>
85
+
65
86
  <div class="status">
66
87
  <span class="status-dot" id="statusDot"></span>
67
88
  <span id="statusText">Connected</span>
68
- <button class="shutdown-btn" id="shutdownBtn" title="Stop server">
69
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
89
+ <button class="shutdown-btn" id="shutdownBtn" title="Stop server" aria-label="Stop server">
90
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
70
91
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 11-12.728 0M12 3v9" />
71
92
  </svg>
72
93
  </button>
73
94
  </div>
74
95
  </div>
75
96
 
76
- <div class="tab-bar" id="tabBar"></div>
97
+ <!-- Tab bar -->
98
+ <div class="tab-bar" id="tabBar" role="tablist" aria-label="Open files"></div>
77
99
 
78
- <div class="content" id="content">
100
+ <!-- Content area -->
101
+ <div class="content" id="content" role="main">
79
102
  <div class="welcome">
80
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
103
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
81
104
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
82
105
  </svg>
83
106
  <h2>Select a file</h2>
@@ -89,14 +112,14 @@
89
112
  </div>
90
113
 
91
114
  <!-- Context Menu -->
92
- <div id="contextMenu" class="context-menu hidden"></div>
115
+ <div id="contextMenu" class="context-menu hidden" role="menu"></div>
93
116
 
94
117
  <!-- Dialog -->
95
- <div id="dialogOverlay" class="dialog-overlay hidden">
118
+ <div id="dialogOverlay" class="dialog-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="dialogTitle">
96
119
  <div class="dialog-content">
97
120
  <h3 id="dialogTitle"></h3>
98
121
  <input type="text" id="dialogInput" style="display: none;">
99
- <p id="dialogMessage" style="margin: 0; color: var(--text-secondary);"></p>
122
+ <p id="dialogMessage"></p>
100
123
  <div class="dialog-actions">
101
124
  <button class="btn-cancel" id="dialogCancel">キャンセル</button>
102
125
  <button class="btn-confirm" id="dialogConfirm">OK</button>
@@ -105,7 +128,7 @@
105
128
  </div>
106
129
 
107
130
  <!-- Upload Progress -->
108
- <div id="uploadOverlay" class="upload-progress-overlay hidden">
131
+ <div id="uploadOverlay" class="upload-progress-overlay hidden" role="progressbar" aria-valuemin="0" aria-valuemax="100">
109
132
  <div class="upload-progress-box">
110
133
  <div id="uploadFileName">アップロード中...</div>
111
134
  <div class="upload-progress-bar">