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/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/bin/mdv.js +140 -57
- package/package.json +4 -4
- package/scripts/setup-macos-app.sh +3 -3
- package/src/api/file.js +140 -111
- package/src/api/pdf.js +24 -27
- package/src/api/tree.js +35 -28
- package/src/api/upload.js +26 -25
- package/src/rendering/index.js +26 -25
- package/src/rendering/markdown.js +27 -40
- package/src/server.js +53 -66
- package/src/static/app.js +107 -140
- package/src/static/index.html +48 -25
- package/src/static/styles.css +95 -169
- package/src/utils/fileTypes.js +99 -90
- package/src/utils/path.js +11 -14
- package/src/watcher.js +38 -48
- package/src/websocket.js +17 -13
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'
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
elements.
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
|
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
|
}
|
package/src/static/index.html
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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"
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
97
|
+
<!-- Tab bar -->
|
|
98
|
+
<div class="tab-bar" id="tabBar" role="tablist" aria-label="Open files"></div>
|
|
77
99
|
|
|
78
|
-
|
|
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"
|
|
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">
|