@rooode/dsh-plugin-preview 0.1.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/lib/client.js ADDED
@@ -0,0 +1,2200 @@
1
+ /**
2
+ * DeepSeek Harness - Markdown & Document Preview Client Bundle
3
+ * @rooode/dsh-plugin-preview / @deepseek-ai/dsh-client-ui-preview
4
+ *
5
+ * Provides a right-side multi-tab Markdown & file preview panel inspired by WorkBuddy (wb).
6
+ * Automatically intercepts .md, .markdown, .txt, .json, .yaml etc. clicks from chat deliverables & tool rows,
7
+ * rendering rich GFM Markdown, syntax-highlighted code blocks, TOC outline, LaTeX math, and split view,
8
+ * without popping up Windows Notepad.
9
+ */
10
+
11
+ window.__ModuleLoader__.load({
12
+ id: "@deepseek-ai/dsh-client-ui-preview",
13
+ factory: (require) => {
14
+ var module = { exports: {} };
15
+ var exports = module.exports;
16
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17
+
18
+ const React = require("react");
19
+ const ReactDOM = require("react-dom");
20
+ const { useState, useEffect, useRef, useMemo, useCallback, createElement: h, Fragment } = React;
21
+
22
+ // =========================================================================
23
+ // Storage & State Constants
24
+ // =========================================================================
25
+ const STORAGE_KEY_TABS = 'dsh:preview:tabs:v1';
26
+ const STORAGE_KEY_WIDTH = 'dsh:preview:panel_width:v1';
27
+ const DEFAULT_PANEL_WIDTH = 620;
28
+ const MIN_PANEL_WIDTH = 380;
29
+ const SUPPORTED_EXTENSIONS = new Set([
30
+ '.md', '.markdown', '.mdown', '.mkdn', '.mdwn',
31
+ '.txt', '.log', '.json', '.yaml', '.yml',
32
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss',
33
+ '.py', '.sh', '.bash', '.ps1', '.sql', '.toml', '.xml'
34
+ ]);
35
+
36
+ // =========================================================================
37
+ // Global Event Bus & State Store
38
+ // =========================================================================
39
+ let clientCtx = null;
40
+ let globalTabs = [];
41
+ let globalActiveTabId = null;
42
+ let globalIsPanelOpen = false;
43
+ let globalPanelWidth = DEFAULT_PANEL_WIDTH;
44
+ const stateListeners = new Set();
45
+
46
+ try {
47
+ const savedWidth = localStorage.getItem(STORAGE_KEY_WIDTH);
48
+ if (savedWidth) {
49
+ const parsed = parseInt(savedWidth, 10);
50
+ if (!isNaN(parsed) && parsed >= MIN_PANEL_WIDTH) {
51
+ globalPanelWidth = Math.min(parsed, Math.floor(window.innerWidth * 0.92));
52
+ }
53
+ }
54
+ } catch (e) {}
55
+
56
+ try {
57
+ const savedTabs = localStorage.getItem(STORAGE_KEY_TABS);
58
+ if (savedTabs) {
59
+ globalTabs = JSON.parse(savedTabs);
60
+ if (globalTabs.length > 0) {
61
+ globalActiveTabId = globalTabs[0].id;
62
+ }
63
+ }
64
+ } catch (e) {}
65
+
66
+ function notifyStateChange() {
67
+ for (const fn of stateListeners) {
68
+ try { fn(); } catch (err) { console.error('[Preview] State notify error:', err); }
69
+ }
70
+ }
71
+
72
+ function saveTabsToStorage() {
73
+ try {
74
+ localStorage.setItem(STORAGE_KEY_TABS, JSON.stringify(globalTabs.slice(0, 15)));
75
+ } catch (e) {}
76
+ }
77
+
78
+ function setPanelWidth(width) {
79
+ const maxW = Math.floor(window.innerWidth * 0.92);
80
+ globalPanelWidth = Math.max(MIN_PANEL_WIDTH, Math.min(width, maxW));
81
+ try {
82
+ localStorage.setItem(STORAGE_KEY_WIDTH, String(globalPanelWidth));
83
+ } catch (e) {}
84
+ notifyStateChange();
85
+ }
86
+
87
+ function setPanelOpen(open) {
88
+ globalIsPanelOpen = open;
89
+ notifyStateChange();
90
+ }
91
+
92
+ function getFileExtension(filePath) {
93
+ if (!filePath) return '';
94
+ const clean = filePath.split('#')[0].split('?')[0];
95
+ const match = clean.match(/\.([a-zA-Z0-9_-]+)$/);
96
+ return match ? ('.' + match[1].toLowerCase()) : '';
97
+ }
98
+
99
+ function getFileName(filePath) {
100
+ if (!filePath) return '未命名文件';
101
+ const clean = filePath.split('#')[0].split('?')[0].replace(/[\\/]+$/, '');
102
+ const parts = clean.split(/[\\/]/);
103
+ return parts[parts.length - 1] || filePath;
104
+ }
105
+
106
+ function isPreviewableFile(filePath) {
107
+ if (!filePath) return false;
108
+ const ext = getFileExtension(filePath);
109
+ return SUPPORTED_EXTENSIONS.has(ext);
110
+ }
111
+
112
+ function resolveAbsoluteFilePath(targetPath) {
113
+ if (!targetPath) return '';
114
+ let cleanPath = targetPath.trim();
115
+ if (cleanPath.startsWith('file:///')) {
116
+ cleanPath = decodeURIComponent(cleanPath.replace(/^file:\/\/\/?/, ''));
117
+ if (cleanPath.match(/^[a-zA-Z]:\//)) {
118
+ cleanPath = cleanPath.replace(/\//g, '\\');
119
+ }
120
+ }
121
+ const isWinAbs = /^[a-zA-Z]:[\\/]/.test(cleanPath);
122
+ const isPosixAbs = cleanPath.startsWith('/');
123
+ if (isWinAbs || isPosixAbs) {
124
+ return cleanPath;
125
+ }
126
+ let cwd = '';
127
+ if (clientCtx && clientCtx.sessions) {
128
+ try {
129
+ const snapshot = clientCtx.sessions.list.getSnapshot();
130
+ const currentSessionId = snapshot.current;
131
+ if (currentSessionId && snapshot.byId[currentSessionId]) {
132
+ cwd = snapshot.byId[currentSessionId].cwd || '';
133
+ }
134
+ } catch (e) {}
135
+ }
136
+ if (!cwd && clientCtx && clientCtx.workspaces) {
137
+ try {
138
+ const wsList = clientCtx.workspaces.list.getSnapshot();
139
+ if (wsList.items && wsList.items.length > 0) {
140
+ cwd = wsList.items[0].directory || '';
141
+ }
142
+ } catch (e) {}
143
+ }
144
+ if (cwd) {
145
+ const isWinCwd = /^[a-zA-Z]:[\\/]/.test(cwd);
146
+ const sep = isWinCwd ? '\\' : '/';
147
+ const normCwd = cwd.replace(/[\\/]+$/, '');
148
+ const normRel = cleanPath.replace(/^[\\/]+/, '');
149
+ return isWinCwd
150
+ ? `${normCwd}\\${normRel.replace(/\//g, '\\')}`
151
+ : `${normCwd}/${normRel}`;
152
+ }
153
+ return cleanPath;
154
+ }
155
+
156
+ // =========================================================================
157
+ // Open & Tab Management Functions
158
+ // =========================================================================
159
+ function openPreviewFile(rawPath, options = {}) {
160
+ const fullPath = resolveAbsoluteFilePath(rawPath);
161
+ const fileName = getFileName(fullPath);
162
+ const ext = getFileExtension(fullPath);
163
+ const tabId = 'tab:' + fullPath.toLowerCase();
164
+
165
+ const existingIndex = globalTabs.findIndex(t => t.id === tabId || t.filePath.toLowerCase() === fullPath.toLowerCase());
166
+ if (existingIndex >= 0) {
167
+ globalActiveTabId = globalTabs[existingIndex].id;
168
+ globalTabs[existingIndex].lastActiveAt = Date.now();
169
+ } else {
170
+ const newTab = {
171
+ id: tabId,
172
+ filePath: fullPath,
173
+ title: fileName,
174
+ extension: ext,
175
+ viewMode: options.viewMode || 'preview',
176
+ pinned: false,
177
+ createdAt: Date.now(),
178
+ lastActiveAt: Date.now(),
179
+ };
180
+ globalTabs = [newTab, ...globalTabs];
181
+ globalActiveTabId = tabId;
182
+ }
183
+ globalIsPanelOpen = true;
184
+ saveTabsToStorage();
185
+ notifyStateChange();
186
+ }
187
+
188
+ function closePreviewTab(tabId, e) {
189
+ if (e) e.stopPropagation();
190
+ const idx = globalTabs.findIndex(t => t.id === tabId);
191
+ if (idx === -1) return;
192
+ const nextTabs = globalTabs.filter(t => t.id !== tabId);
193
+ globalTabs = nextTabs;
194
+ if (globalActiveTabId === tabId) {
195
+ if (nextTabs.length > 0) {
196
+ const nextIdx = Math.min(idx, nextTabs.length - 1);
197
+ globalActiveTabId = nextTabs[nextIdx].id;
198
+ } else {
199
+ globalActiveTabId = null;
200
+ globalIsPanelOpen = false;
201
+ }
202
+ }
203
+ saveTabsToStorage();
204
+ notifyStateChange();
205
+ }
206
+
207
+ function closeAllTabs() {
208
+ globalTabs = [];
209
+ globalActiveTabId = null;
210
+ globalIsPanelOpen = false;
211
+ saveTabsToStorage();
212
+ notifyStateChange();
213
+ }
214
+
215
+ function switchActiveTab(tabId) {
216
+ globalActiveTabId = tabId;
217
+ const tab = globalTabs.find(t => t.id === tabId);
218
+ if (tab) tab.lastActiveAt = Date.now();
219
+ notifyStateChange();
220
+ }
221
+
222
+ function updateTabMode(tabId, mode) {
223
+ const tab = globalTabs.find(t => t.id === tabId);
224
+ if (tab) {
225
+ tab.viewMode = mode;
226
+ saveTabsToStorage();
227
+ notifyStateChange();
228
+ }
229
+ }
230
+
231
+ // =========================================================================
232
+ // Toast Notification Utility
233
+ // =========================================================================
234
+ let toastContainer = null;
235
+ function showToast(message, type = 'info') {
236
+ if (typeof document === 'undefined') return;
237
+ if (!toastContainer) {
238
+ toastContainer = document.createElement('div');
239
+ toastContainer.className = 'dsh-preview-toast-container';
240
+ document.body.appendChild(toastContainer);
241
+ }
242
+ const item = document.createElement('div');
243
+ item.className = `dsh-preview-toast dsh-preview-toast-${type}`;
244
+ const icon = type === 'success' ? '✓' : (type === 'error' ? '✕' : 'ℹ');
245
+ item.innerHTML = `<span class="dsh-preview-toast-icon">${icon}</span><span>${message}</span>`;
246
+ toastContainer.appendChild(item);
247
+ setTimeout(() => {
248
+ item.classList.add('dsh-preview-toast-show');
249
+ }, 10);
250
+ setTimeout(() => {
251
+ item.classList.remove('dsh-preview-toast-show');
252
+ setTimeout(() => item.remove(), 300);
253
+ }, 2500);
254
+ }
255
+
256
+ // =========================================================================
257
+ // File Fetcher via Node /api/preview/read
258
+ // =========================================================================
259
+ const fileCache = new Map();
260
+ async function fetchFileContent(filePath, forceRefresh = false) {
261
+ if (!filePath) return null;
262
+ const cacheKey = filePath.toLowerCase();
263
+ if (!forceRefresh && fileCache.has(cacheKey)) {
264
+ const cached = fileCache.get(cacheKey);
265
+ if (Date.now() - cached.timestamp < 10000) { // 10s fresh cache
266
+ return cached.data;
267
+ }
268
+ }
269
+
270
+ try {
271
+ const query = encodeURIComponent(filePath);
272
+ const res = await fetch(`/api/preview/read?path=${query}`);
273
+ if (!res.ok) {
274
+ const errData = await res.json().catch(() => ({}));
275
+ throw new Error(errData?.error?.message || `HTTP ${res.status}: 无法读取文件`);
276
+ }
277
+ const json = await res.json();
278
+ if (!json.ok || !json.file) {
279
+ throw new Error(json?.error?.message || '读取文件失败');
280
+ }
281
+ fileCache.set(cacheKey, { data: json.file, timestamp: Date.now() });
282
+ return json.file;
283
+ } catch (err) {
284
+ console.warn('[Preview] Fetch file error:', err);
285
+ return {
286
+ path: filePath,
287
+ displayPath: getFileName(filePath),
288
+ name: getFileName(filePath),
289
+ content: '',
290
+ size: 0,
291
+ mtime: Date.now(),
292
+ extension: getFileExtension(filePath),
293
+ lineCount: 0,
294
+ wordCount: 0,
295
+ charCount: 0,
296
+ error: err.message || '读取文件失败',
297
+ };
298
+ }
299
+ }
300
+
301
+ async function triggerOpenNative(filePath) {
302
+ try {
303
+ const res = await fetch('/api/preview/open-native', {
304
+ method: 'POST',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify({ path: filePath }),
307
+ });
308
+ if (res.ok) {
309
+ showToast('已在系统本地应用中打开', 'success');
310
+ } else {
311
+ showToast('打开外部应用失败', 'error');
312
+ }
313
+ } catch (e) {
314
+ showToast('调用系统打开器失败', 'error');
315
+ }
316
+ }
317
+
318
+ async function triggerRevealInExplorer(filePath) {
319
+ try {
320
+ const res = await fetch('/api/preview/reveal', {
321
+ method: 'POST',
322
+ headers: { 'Content-Type': 'application/json' },
323
+ body: JSON.stringify({ path: filePath }),
324
+ });
325
+ if (res.ok) {
326
+ showToast('已在文件管理器中定位', 'success');
327
+ } else {
328
+ showToast('定位文件失败', 'error');
329
+ }
330
+ } catch (e) {
331
+ showToast('定位文件失败', 'error');
332
+ }
333
+ }
334
+
335
+ // =========================================================================
336
+ // Markdown Parser & Rich HTML Renderer (Self-contained GFM + LaTeX Math)
337
+ // =========================================================================
338
+ function escapeHtml(str) {
339
+ if (!str) return '';
340
+ return String(str)
341
+ .replace(/&/g, '&amp;')
342
+ .replace(/</g, '&lt;')
343
+ .replace(/>/g, '&gt;')
344
+ .replace(/"/g, '&quot;')
345
+ .replace(/'/g, '&#39;');
346
+ }
347
+
348
+ function slugify(text) {
349
+ return String(text)
350
+ .toLowerCase()
351
+ .replace(/<[^>]*>/g, '')
352
+ .replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
353
+ .trim()
354
+ .replace(/\s+/g, '-');
355
+ }
356
+
357
+ function extractTocHeadings(markdownText) {
358
+ if (!markdownText) return [];
359
+ const lines = markdownText.split('\n');
360
+ const headings = [];
361
+ let inCodeBlock = false;
362
+
363
+ for (let i = 0; i < lines.length; i++) {
364
+ const line = lines[i].trim();
365
+ if (line.startsWith('```')) {
366
+ inCodeBlock = !inCodeBlock;
367
+ continue;
368
+ }
369
+ if (inCodeBlock) continue;
370
+
371
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
372
+ if (match) {
373
+ const level = match[1].length;
374
+ const rawText = match[2].trim();
375
+ const cleanText = rawText.replace(/\*\*|__|\*|_|`|~~/g, '');
376
+ const id = 'heading-' + slugify(cleanText) + '-' + headings.length;
377
+ headings.push({ level, text: cleanText, id, raw: rawText });
378
+ }
379
+ }
380
+ return headings;
381
+ }
382
+
383
+ function highlightCodeSyntax(code, lang = '') {
384
+ const escaped = escapeHtml(code);
385
+ const l = (lang || '').toLowerCase();
386
+ // Basic syntax highlighter keywords
387
+ const keywords = /\b(const|let|var|function|return|if|else|for|while|import|export|from|default|class|extends|new|this|async|await|try|catch|finally|throw|typeof|instanceof|interface|type|public|private|protected|static|readonly|abstract|implements|enum|as|is|in|of|def|self|None|True|False|elif|lambda|pass|struct|fn|mut|impl|pub|trait|match|use|package|func|select|defer|go|val|null|undefined|true|false)\b/g;
388
+ const strings = /(&quot;.*?&quot;|&#39;.*?&#39;|`.*?`)/g;
389
+ const comments = /(\/\/.*$|\/\*[\s\S]*?\*\/|#.*$)/gm;
390
+ const numbers = /\b(\d+(\.\d+)?)\b/g;
391
+
392
+ let highlighted = escaped
393
+ .replace(comments, '<span class="dsh-token-comment">$1</span>')
394
+ .replace(strings, '<span class="dsh-token-string">$1</span>')
395
+ .replace(keywords, '<span class="dsh-token-keyword">$1</span>')
396
+ .replace(numbers, '<span class="dsh-token-number">$1</span>');
397
+
398
+ return highlighted;
399
+ }
400
+
401
+ function renderMarkdownToHtml(markdownText) {
402
+ if (!markdownText) return '<div class="dsh-md-empty">暂无文档内容</div>';
403
+
404
+ try {
405
+ const codeBlocks = [];
406
+ const mathBlocks = [];
407
+ const inlineMaths = [];
408
+ let text = markdownText.replace(/\r\n/g, '\n');
409
+
410
+ // 1. Extract Block Math $$ ... $$
411
+ text = text.replace(/\$\$([\s\S]+?)\$\$/g, (_, math) => {
412
+ const id = mathBlocks.length;
413
+ mathBlocks.push(math.trim());
414
+ return `@@MATH_BLOCK_${id}@@`;
415
+ });
416
+
417
+ // 2. Extract Inline Math $ ... $
418
+ text = text.replace(/(?<!\$)\$(?!\$)(.+?)(?<!\$)\$(?!\$)/g, (_, math) => {
419
+ const id = inlineMaths.length;
420
+ inlineMaths.push(math.trim());
421
+ return `@@MATH_INLINE_${id}@@`;
422
+ });
423
+
424
+ // 3. Extract Fenced Code Blocks ```lang ... ```
425
+ text = text.replace(/```([a-zA-Z0-9_\-\.\+]*)\n([\s\S]*?)```/g, (_, lang, code) => {
426
+ const id = codeBlocks.length;
427
+ codeBlocks.push({ lang: (lang || 'text').trim(), code: code.replace(/\n$/, '') });
428
+ return `@@CODE_BLOCK_${id}@@`;
429
+ });
430
+
431
+ // 4. Parse GitHub Alerts / Callouts (> [!NOTE] etc.)
432
+ text = text.replace(/^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n((?:>.*(?:\n|$))*)/gim, (_, type, body) => {
433
+ const alertType = type.toUpperCase();
434
+ const cleanBody = body.replace(/^>\s?/gm, '').trim();
435
+ const titleMap = {
436
+ 'NOTE': '备注 (Note)',
437
+ 'TIP': '提示 (Tip)',
438
+ 'IMPORTANT': '要点 (Important)',
439
+ 'WARNING': '警告 (Warning)',
440
+ 'CAUTION': '注意 (Caution)',
441
+ };
442
+ const iconMap = {
443
+ 'NOTE': 'ℹ️',
444
+ 'TIP': '💡',
445
+ 'IMPORTANT': '📌',
446
+ 'WARNING': '⚠️',
447
+ 'CAUTION': '🚨',
448
+ };
449
+ return `<div class="dsh-alert dsh-alert-${alertType.toLowerCase()}">
450
+ <div class="dsh-alert-title"><span class="dsh-alert-icon">${iconMap[alertType] || 'ℹ️'}</span>${titleMap[alertType] || alertType}</div>
451
+ <div class="dsh-alert-body">${cleanBody}</div>
452
+ </div>\n\n`;
453
+ });
454
+
455
+ // 5. Parse Tables
456
+ const parseTableRow = (line) => {
457
+ let t = line.trim();
458
+ if (t.startsWith('|')) t = t.slice(1);
459
+ if (t.endsWith('|')) t = t.slice(0, -1);
460
+ return t.split('|').map(c => c.trim());
461
+ };
462
+
463
+ const tableRegex = /^(\|?.+\|.*)\n(\|?[\s\-:|]+\|.*)\n((?:\|?.*\|.*(?:\n|$))*)/gm;
464
+ text = text.replace(tableRegex, (match, headerLine, alignLine, bodyLines) => {
465
+ const headerCells = parseTableRow(headerLine);
466
+ const alignCells = parseTableRow(alignLine);
467
+ const aligns = alignCells.map(c => {
468
+ if (c.startsWith(':') && c.endsWith(':')) return 'center';
469
+ if (c.endsWith(':')) return 'right';
470
+ return 'left';
471
+ });
472
+
473
+ let tableHtml = '<div class="dsh-md-table-wrapper"><table><thead><tr>';
474
+ headerCells.forEach((hCell, i) => {
475
+ const align = aligns[i] || 'left';
476
+ tableHtml += `<th style="text-align:${align}">${formatInlineStyles(hCell)}</th>`;
477
+ });
478
+ tableHtml += '</tr></thead><tbody>';
479
+
480
+ const rows = bodyLines.trim().split('\n');
481
+ for (const row of rows) {
482
+ if (!row.trim()) continue;
483
+ const cells = parseTableRow(row);
484
+ tableHtml += '<tr>';
485
+ cells.forEach((cell, i) => {
486
+ const align = aligns[i] || 'left';
487
+ tableHtml += `<td style="text-align:${align}">${formatInlineStyles(cell)}</td>`;
488
+ });
489
+ tableHtml += '</tr>';
490
+ }
491
+ tableHtml += '</tbody></table></div>\n\n';
492
+ return tableHtml;
493
+ });
494
+
495
+ // 6. Split into blocks/paragraphs
496
+ const lines = text.split('\n');
497
+ let html = '';
498
+ let inList = false;
499
+ let listType = 'ul';
500
+ let inBlockquote = false;
501
+ let headingCount = 0;
502
+
503
+ for (let i = 0; i < lines.length; i++) {
504
+ let line = lines[i];
505
+
506
+ // Horizontal rule
507
+ if (/^(\-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
508
+ if (inList) { html += `</${listType}>`; inList = false; }
509
+ if (inBlockquote) { html += `</blockquote>`; inBlockquote = false; }
510
+ html += '<hr class="dsh-md-hr" />';
511
+ continue;
512
+ }
513
+
514
+ // Headings
515
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
516
+ if (headingMatch) {
517
+ if (inList) { html += `</${listType}>`; inList = false; }
518
+ if (inBlockquote) { html += `</blockquote>`; inBlockquote = false; }
519
+ const level = headingMatch[1].length;
520
+ const headingText = headingMatch[2].trim();
521
+ const cleanText = headingText.replace(/\*\*|__|\*|_|`|~~/g, '');
522
+ const id = 'heading-' + slugify(cleanText) + '-' + (headingCount++);
523
+ html += `<h${level} id="${id}" class="dsh-md-h dsh-md-h${level}">
524
+ <a href="#${id}" class="dsh-md-anchor" aria-hidden="true">#</a>
525
+ ${formatInlineStyles(headingText)}
526
+ </h${level}>`;
527
+ continue;
528
+ }
529
+
530
+ // Blockquotes (regular)
531
+ if (line.startsWith('> ') || line === '>') {
532
+ if (inList) { html += `</${listType}>`; inList = false; }
533
+ if (!inBlockquote) {
534
+ html += '<blockquote class="dsh-md-blockquote">';
535
+ inBlockquote = true;
536
+ }
537
+ const quoteContent = line.replace(/^>\s?/, '');
538
+ html += `<p>${formatInlineStyles(quoteContent)}</p>`;
539
+ continue;
540
+ } else if (inBlockquote) {
541
+ html += '</blockquote>';
542
+ inBlockquote = false;
543
+ }
544
+
545
+ // Task list & Lists
546
+ const taskMatch = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.+)$/);
547
+ const ulMatch = line.match(/^(\s*)[-*+]\s+(.+)$/);
548
+ const olMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
549
+
550
+ if (taskMatch) {
551
+ if (!inList) { html += '<ul class="dsh-md-list dsh-md-task-list">'; inList = true; listType = 'ul'; }
552
+ const checked = taskMatch[2].toLowerCase() === 'x';
553
+ const taskContent = taskMatch[3];
554
+ html += `<li class="dsh-md-task-item"><input type="checkbox" ${checked ? 'checked' : ''} disabled /><span>${formatInlineStyles(taskContent)}</span></li>`;
555
+ continue;
556
+ } else if (ulMatch) {
557
+ if (!inList || listType !== 'ul') {
558
+ if (inList) html += `</${listType}>`;
559
+ html += '<ul class="dsh-md-list">';
560
+ inList = true;
561
+ listType = 'ul';
562
+ }
563
+ html += `<li>${formatInlineStyles(ulMatch[2])}</li>`;
564
+ continue;
565
+ } else if (olMatch) {
566
+ if (!inList || listType !== 'ol') {
567
+ if (inList) html += `</${listType}>`;
568
+ html += '<ol class="dsh-md-list dsh-md-ol">';
569
+ inList = true;
570
+ listType = 'ol';
571
+ }
572
+ html += `<li>${formatInlineStyles(olMatch[3])}</li>`;
573
+ continue;
574
+ } else if (inList && line.trim() === '') {
575
+ html += `</${listType}>`;
576
+ inList = false;
577
+ }
578
+
579
+ // Empty lines
580
+ if (line.trim() === '') {
581
+ continue;
582
+ }
583
+
584
+ // If line is pre-rendered HTML (like tables, alert boxes, code block markers)
585
+ if (line.startsWith('@@CODE_BLOCK_') || line.startsWith('@@MATH_BLOCK_') || line.startsWith('<div class="dsh-')) {
586
+ html += line + '\n';
587
+ continue;
588
+ }
589
+
590
+ // Normal paragraph
591
+ html += `<p class="dsh-md-p">${formatInlineStyles(line)}</p>`;
592
+ }
593
+
594
+ if (inList) html += `</${listType}>`;
595
+ if (inBlockquote) html += `</blockquote>`;
596
+
597
+ // 7. Restore Code Blocks with Syntax Highlighter & Copy Button
598
+ html = html.replace(/@@CODE_BLOCK_(\d+)@@/g, (_, id) => {
599
+ const item = codeBlocks[parseInt(id, 10)];
600
+ if (!item) return '';
601
+ const langUpper = (item.lang || 'CODE').toUpperCase();
602
+ const codeEscaped = escapeHtml(item.code);
603
+ const codeHighlighted = highlightCodeSyntax(item.code, item.lang);
604
+ const copyData = encodeURIComponent(item.code);
605
+
606
+ return `<div class="dsh-code-card">
607
+ <div class="dsh-code-card-header">
608
+ <span class="dsh-code-lang-badge">${escapeHtml(langUpper)}</span>
609
+ <button type="button" class="dsh-code-copy-btn" data-code="${copyData}" onclick="window.__dsh_copy_code(this)">
610
+ <span class="dsh-copy-icon">📋</span> 复制
611
+ </button>
612
+ </div>
613
+ <pre class="dsh-code-pre"><code class="language-${escapeHtml(item.lang)}">${codeHighlighted}</code></pre>
614
+ </div>`;
615
+ });
616
+
617
+ // 8. Restore Math Blocks
618
+ html = html.replace(/@@MATH_BLOCK_(\d+)@@/g, (_, id) => {
619
+ const math = mathBlocks[parseInt(id, 10)] || '';
620
+ return `<div class="dsh-math-block">
621
+ <div class="dsh-math-content">$$\n${escapeHtml(math)}\n$$</div>
622
+ </div>`;
623
+ });
624
+
625
+ // 9. Restore Inline Maths
626
+ html = html.replace(/@@MATH_INLINE_(\d+)@@/g, (_, id) => {
627
+ const math = inlineMaths[parseInt(id, 10)] || '';
628
+ return `<span class="dsh-math-inline">$${escapeHtml(math)}$</span>`;
629
+ });
630
+
631
+ return html;
632
+ } catch (err) {
633
+ console.error('[Preview] Failed to parse Markdown:', err);
634
+ return `<pre class="dsh-code-pre">${escapeHtml(markdownText)}</pre>`;
635
+ }
636
+ }
637
+
638
+ function formatInlineStyles(text) {
639
+ if (!text) return '';
640
+ let s = escapeHtml(text);
641
+
642
+ // Inline code
643
+ s = s.replace(/`([^`]+)`/g, '<code class="dsh-md-inline-code">$1</code>');
644
+
645
+ // Bold & Italic
646
+ s = s.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
647
+ s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
648
+ s = s.replace(/__([^_]+)__/g, '<strong>$1</strong>');
649
+ s = s.replace(/\*([^*]+)\*/g, '<em>$1</em>');
650
+ s = s.replace(/_([^_]+)_/g, '<em>$1</em>');
651
+
652
+ // Strikethrough
653
+ s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>');
654
+
655
+ // Images ![alt](url)
656
+ s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
657
+ return `<span class="dsh-md-img-container"><img src="${src}" alt="${alt}" class="dsh-md-img" loading="lazy" /></span>`;
658
+ });
659
+
660
+ // Links [title](url)
661
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="dsh-md-link">$1 ↗</a>');
662
+
663
+ return s;
664
+ }
665
+
666
+ // Global copy code callback for inline onclick handlers
667
+ if (typeof window !== 'undefined') {
668
+ window.__dsh_copy_code = function(btn) {
669
+ try {
670
+ const raw = btn.getAttribute('data-code');
671
+ if (raw) {
672
+ const code = decodeURIComponent(raw);
673
+ navigator.clipboard.writeText(code).then(() => {
674
+ const origHtml = btn.innerHTML;
675
+ btn.innerHTML = '<span style="color:#22c55e">✓ 已复制</span>';
676
+ setTimeout(() => { btn.innerHTML = origHtml; }, 1800);
677
+ });
678
+ }
679
+ } catch (e) {
680
+ console.error('Copy code failed:', e);
681
+ }
682
+ };
683
+ }
684
+
685
+ // =========================================================================
686
+ // React Components: MarkdownPreviewView
687
+ // =========================================================================
688
+ function MarkdownPreviewView({ tab, fileInfo, isLoading, error, onRefresh, onOpenNative, onReveal }) {
689
+ const [viewMode, setViewMode] = useState(tab.viewMode || 'preview');
690
+ const [isTocOpen, setIsTocOpen] = useState(false);
691
+ const [searchQuery, setSearchQuery] = useState('');
692
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
693
+ const contentRef = useRef(null);
694
+ const sourceRef = useRef(null);
695
+
696
+ const content = fileInfo?.content || '';
697
+ const tocHeadings = useMemo(() => extractTocHeadings(content), [content]);
698
+
699
+ const renderedHtml = useMemo(() => {
700
+ if (!content) return '';
701
+ let html = renderMarkdownToHtml(content);
702
+ if (searchQuery.trim()) {
703
+ try {
704
+ const q = escapeHtml(searchQuery.trim());
705
+ const regex = new RegExp(`(${q})`, 'gi');
706
+ html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
707
+ } catch (e) {}
708
+ }
709
+ return html;
710
+ }, [content, searchQuery]);
711
+
712
+ const handleCopyAll = () => {
713
+ if (!content) return;
714
+ navigator.clipboard.writeText(content).then(() => {
715
+ showToast('已复制文档全文到剪贴板', 'success');
716
+ }).catch(() => {
717
+ showToast('复制失败', 'error');
718
+ });
719
+ };
720
+
721
+ const handleTocClick = (headingId) => {
722
+ if (!contentRef.current) return;
723
+ const el = contentRef.current.querySelector(`#${headingId}`);
724
+ if (el) {
725
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
726
+ }
727
+ };
728
+
729
+ const handleModeChange = (mode) => {
730
+ setViewMode(mode);
731
+ updateTabMode(tab.id, mode);
732
+ };
733
+
734
+ return h('div', { className: 'dsh-preview-content-container' },
735
+ // Toolbar
736
+ h('div', { className: 'dsh-preview-toolbar' },
737
+ // Left: View mode toggles
738
+ h('div', { className: 'dsh-preview-toolbar-left' },
739
+ h('div', { className: 'dsh-preview-mode-group' },
740
+ h('button', {
741
+ type: 'button',
742
+ className: `dsh-preview-mode-btn ${viewMode === 'preview' ? 'active' : ''}`,
743
+ onClick: () => handleModeChange('preview'),
744
+ title: '渲染预览模式 (Markdown Render)',
745
+ }, '📖 预览'),
746
+ h('button', {
747
+ type: 'button',
748
+ className: `dsh-preview-mode-btn ${viewMode === 'source' ? 'active' : ''}`,
749
+ onClick: () => handleModeChange('source'),
750
+ title: '源码模式 (Raw Source)',
751
+ }, '📝 源码'),
752
+ h('button', {
753
+ type: 'button',
754
+ className: `dsh-preview-mode-btn ${viewMode === 'split' ? 'active' : ''}`,
755
+ onClick: () => handleModeChange('split'),
756
+ title: '分栏对比模式 (Split View)',
757
+ }, '🌓 分栏')
758
+ ),
759
+
760
+ tocHeadings.length > 0 ? h('button', {
761
+ type: 'button',
762
+ className: `dsh-preview-tool-btn ${isTocOpen ? 'active' : ''}`,
763
+ onClick: () => setIsTocOpen(!isTocOpen),
764
+ title: isTocOpen ? '隐藏目录大纲' : '显示目录大纲 (TOC)',
765
+ }, `📑 大纲 (${tocHeadings.length})`) : null,
766
+
767
+ h('button', {
768
+ type: 'button',
769
+ className: `dsh-preview-tool-btn ${isSearchOpen ? 'active' : ''}`,
770
+ onClick: () => setIsSearchOpen(!isSearchOpen),
771
+ title: '在文档中查找',
772
+ }, '🔍 查找')
773
+ ),
774
+
775
+ // Right: Action buttons
776
+ h('div', { className: 'dsh-preview-toolbar-right' },
777
+ h('button', {
778
+ type: 'button',
779
+ className: 'dsh-preview-tool-btn',
780
+ onClick: handleCopyAll,
781
+ title: '一键复制全文',
782
+ }, '📋 复制'),
783
+
784
+ h('button', {
785
+ type: 'button',
786
+ className: 'dsh-preview-tool-btn',
787
+ onClick: onRefresh,
788
+ title: '从磁盘重新加载最新内容',
789
+ }, '🔄 刷新'),
790
+
791
+ h('button', {
792
+ type: 'button',
793
+ className: 'dsh-preview-tool-btn dsh-preview-tool-btn-accent',
794
+ onClick: () => onOpenNative(tab.filePath),
795
+ title: '在系统本地默认编辑器中打开 (记事本/VSCode)',
796
+ }, '🖥️ 本地打开'),
797
+
798
+ h('button', {
799
+ type: 'button',
800
+ className: 'dsh-preview-tool-btn',
801
+ onClick: () => onReveal(tab.filePath),
802
+ title: '在文件管理器中定位',
803
+ }, '📁 定位')
804
+ )
805
+ ),
806
+
807
+ // Search Bar (if open)
808
+ isSearchOpen ? h('div', { className: 'dsh-preview-search-bar' },
809
+ h('span', { className: 'dsh-search-icon' }, '🔍'),
810
+ h('input', {
811
+ type: 'text',
812
+ placeholder: '输入关键词在文档中搜索...',
813
+ value: searchQuery,
814
+ onChange: e => setSearchQuery(e.target.value),
815
+ className: 'dsh-preview-search-input',
816
+ autoFocus: true,
817
+ }),
818
+ searchQuery ? h('button', {
819
+ type: 'button',
820
+ className: 'dsh-search-clear-btn',
821
+ onClick: () => setSearchQuery(''),
822
+ }, '✕') : null
823
+ ) : null,
824
+
825
+ // Body area with optional TOC sidebar + Main content
826
+ h('div', { className: 'dsh-preview-body-layout' },
827
+ // TOC Sidebar
828
+ isTocOpen && tocHeadings.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
829
+ h('div', { className: 'dsh-preview-toc-header' },
830
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, '目录大纲 (TOC)'),
831
+ h('button', {
832
+ type: 'button',
833
+ className: 'dsh-toc-close-btn',
834
+ onClick: () => setIsTocOpen(false),
835
+ }, '✕')
836
+ ),
837
+ h('div', { className: 'dsh-preview-toc-list' },
838
+ tocHeadings.map((hItem, idx) =>
839
+ h('div', {
840
+ key: idx,
841
+ className: `dsh-toc-item dsh-toc-level-${hItem.level}`,
842
+ onClick: () => handleTocClick(hItem.id),
843
+ title: hItem.text,
844
+ },
845
+ h('span', { className: 'dsh-toc-bullet' }, '•'),
846
+ h('span', { className: 'dsh-toc-text' }, hItem.text)
847
+ )
848
+ )
849
+ )
850
+ ) : null,
851
+
852
+ // Main Viewer
853
+ h('div', { className: 'dsh-preview-main-scroll' },
854
+ isLoading ? h('div', { className: 'dsh-preview-loading' },
855
+ h('div', { className: 'dsh-preview-spinner' }),
856
+ h('span', null, '正在加载文档内容...')
857
+ ) : (error || fileInfo?.error) ? h('div', { className: 'dsh-preview-error-card' },
858
+ h('div', { className: 'dsh-error-icon' }, '⚠️'),
859
+ h('div', { className: 'dsh-error-title' }, '读取文档失败'),
860
+ h('div', { className: 'dsh-error-desc' }, error || fileInfo?.error),
861
+ h('div', { className: 'dsh-error-actions' },
862
+ h('button', { type: 'button', className: 'dsh-btn-retry', onClick: onRefresh }, '重试'),
863
+ h('button', { type: 'button', className: 'dsh-btn-native', onClick: () => onOpenNative(tab.filePath) }, '在外部打开')
864
+ )
865
+ ) : viewMode === 'preview' ? (
866
+ // 1. Preview Mode
867
+ h('div', {
868
+ ref: contentRef,
869
+ className: 'dsh-markdown-body',
870
+ dangerouslySetInnerHTML: { __html: renderedHtml },
871
+ })
872
+ ) : viewMode === 'source' ? (
873
+ // 2. Source Code Mode
874
+ h('div', { className: 'dsh-source-view' },
875
+ h('div', { className: 'dsh-source-line-numbers' },
876
+ (content.split('\n') || []).map((_, i) => h('div', { key: i }, i + 1))
877
+ ),
878
+ h('pre', { ref: sourceRef, className: 'dsh-source-pre' },
879
+ h('code', null, content)
880
+ )
881
+ )
882
+ ) : (
883
+ // 3. Split Mode
884
+ h('div', { className: 'dsh-split-view' },
885
+ h('div', { className: 'dsh-split-pane dsh-split-source' },
886
+ h('div', { className: 'dsh-split-pane-header' }, '📝 原始源码'),
887
+ h('div', { className: 'dsh-source-view' },
888
+ h('div', { className: 'dsh-source-line-numbers' },
889
+ (content.split('\n') || []).map((_, i) => h('div', { key: i }, i + 1))
890
+ ),
891
+ h('pre', { className: 'dsh-source-pre' },
892
+ h('code', null, content)
893
+ )
894
+ )
895
+ ),
896
+ h('div', { className: 'dsh-split-pane dsh-split-render' },
897
+ h('div', { className: 'dsh-split-pane-header' }, '📖 渲染预览'),
898
+ h('div', {
899
+ ref: contentRef,
900
+ className: 'dsh-markdown-body',
901
+ dangerouslySetInnerHTML: { __html: renderedHtml },
902
+ })
903
+ )
904
+ )
905
+ )
906
+ )
907
+ ),
908
+
909
+ // Status Footer
910
+ h('div', { className: 'dsh-preview-footer' },
911
+ h('div', { className: 'dsh-footer-left', title: fileInfo?.path || tab.filePath },
912
+ h('span', { className: 'dsh-footer-ext' }, (fileInfo?.extension || tab.extension || '.md').toUpperCase().replace('.', '')),
913
+ h('span', { className: 'dsh-footer-path' }, fileInfo?.path || tab.filePath)
914
+ ),
915
+ h('div', { className: 'dsh-footer-right' },
916
+ fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, `${(fileInfo.size / 1024).toFixed(1)} KB`) : null,
917
+ fileInfo?.lineCount ? h('span', { className: 'dsh-footer-stat' }, `${fileInfo.lineCount} 行`) : null,
918
+ fileInfo?.wordCount ? h('span', { className: 'dsh-footer-stat' }, `约 ${fileInfo.wordCount} 字`) : null
919
+ )
920
+ )
921
+ );
922
+ }
923
+
924
+ // =========================================================================
925
+ // React Components: Right Side Drawer / Shell Overlay Container
926
+ // =========================================================================
927
+ function PreviewDrawerRoot() {
928
+ const [tabs, setTabs] = useState(globalTabs);
929
+ const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
930
+ const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
931
+ const [width, setWidth] = useState(globalPanelWidth);
932
+ const [fileData, setFileData] = useState({});
933
+ const [loadingMap, setLoadingMap] = useState({});
934
+ const [errorMap, setErrorMap] = useState({});
935
+ const [isDragging, setIsDragging] = useState(false);
936
+ const dragStartX = useRef(0);
937
+ const dragStartWidth = useRef(0);
938
+
939
+ // Subscribe to global store changes
940
+ useEffect(() => {
941
+ const update = () => {
942
+ setTabs([...globalTabs]);
943
+ setActiveTabId(globalActiveTabId);
944
+ setIsOpen(globalIsPanelOpen);
945
+ setWidth(globalPanelWidth);
946
+ };
947
+ stateListeners.add(update);
948
+ return () => stateListeners.delete(update);
949
+ }, []);
950
+
951
+ const activeTab = useMemo(() => {
952
+ return tabs.find(t => t.id === activeTabId) || tabs[0] || null;
953
+ }, [tabs, activeTabId]);
954
+
955
+ // Load file content when active tab changes
956
+ const loadTabFile = useCallback(async (tab, force = false) => {
957
+ if (!tab || !tab.filePath) return;
958
+ const key = tab.filePath.toLowerCase();
959
+ setLoadingMap(m => ({ ...m, [key]: true }));
960
+ setErrorMap(m => ({ ...m, [key]: null }));
961
+ try {
962
+ const fileInfo = await fetchFileContent(tab.filePath, force);
963
+ if (fileInfo?.error) {
964
+ setErrorMap(m => ({ ...m, [key]: fileInfo.error }));
965
+ } else {
966
+ setFileData(d => ({ ...d, [key]: fileInfo }));
967
+ }
968
+ } catch (e) {
969
+ setErrorMap(m => ({ ...m, [key]: e.message }));
970
+ } finally {
971
+ setLoadingMap(m => ({ ...m, [key]: false }));
972
+ }
973
+ }, []);
974
+
975
+ useEffect(() => {
976
+ if (activeTab) {
977
+ loadTabFile(activeTab);
978
+ }
979
+ }, [activeTab, loadTabFile]);
980
+
981
+ // Drag Resize Handlers
982
+ const onResizePointerDown = useCallback((e) => {
983
+ e.preventDefault();
984
+ e.currentTarget.setPointerCapture(e.pointerId);
985
+ dragStartX.current = e.clientX;
986
+ dragStartWidth.current = globalPanelWidth;
987
+ setIsDragging(true);
988
+ }, []);
989
+
990
+ const onResizePointerMove = useCallback((e) => {
991
+ if (!isDragging) return;
992
+ const dx = dragStartX.current - e.clientX; // drag left increases width
993
+ const newWidth = dragStartWidth.current + dx;
994
+ setPanelWidth(newWidth);
995
+ }, [isDragging]);
996
+
997
+ const onResizePointerUp = useCallback((e) => {
998
+ if (isDragging) {
999
+ setIsDragging(false);
1000
+ try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
1001
+ }
1002
+ }, [isDragging]);
1003
+
1004
+ // Hotkey: Esc closes preview
1005
+ useEffect(() => {
1006
+ const onKeyDown = (e) => {
1007
+ if (e.key === 'Escape' && globalIsPanelOpen) {
1008
+ setPanelOpen(false);
1009
+ }
1010
+ };
1011
+ window.addEventListener('keydown', onKeyDown);
1012
+ return () => window.removeEventListener('keydown', onKeyDown);
1013
+ }, []);
1014
+
1015
+ if (!isOpen && tabs.length === 0) return null;
1016
+
1017
+ const activeKey = activeTab?.filePath?.toLowerCase();
1018
+ const currentFileInfo = activeKey ? fileData[activeKey] : null;
1019
+ const currentLoading = activeKey ? !!loadingMap[activeKey] : false;
1020
+ const currentError = activeKey ? errorMap[activeKey] : null;
1021
+
1022
+ return h(Fragment, null,
1023
+ // Floating Mini Button to Reopen (when closed but tabs exist)
1024
+ !isOpen && tabs.length > 0 ? h('div', {
1025
+ className: 'dsh-preview-float-pill',
1026
+ onClick: () => setPanelOpen(true),
1027
+ title: `重新展开文档预览 (${tabs.length} 个标签)`,
1028
+ },
1029
+ h('span', { className: 'dsh-float-pill-icon' }, '📝'),
1030
+ h('span', { className: 'dsh-float-pill-text' }, `预览: ${activeTab?.title || '文档'}`),
1031
+ h('span', { className: 'dsh-float-pill-badge' }, tabs.length)
1032
+ ) : null,
1033
+
1034
+ // Right-Side Slide Drawer
1035
+ isOpen ? h('div', {
1036
+ className: `dsh-preview-drawer-container ${isDragging ? 'dragging' : ''}`,
1037
+ style: { width: `${width}px` },
1038
+ },
1039
+ // Drag Handle on Left Border
1040
+ h('div', {
1041
+ className: 'dsh-preview-drag-handle',
1042
+ onPointerDown: onResizePointerDown,
1043
+ onPointerMove: onResizePointerMove,
1044
+ onPointerUp: onResizePointerUp,
1045
+ title: '左右拖拽调整预览面板宽度',
1046
+ }),
1047
+
1048
+ // Drawer Header with FileTabs
1049
+ h('div', { className: 'dsh-preview-drawer-header' },
1050
+ // Tabs Bar
1051
+ h('div', { className: 'dsh-preview-tabs-row' },
1052
+ tabs.map(tab => {
1053
+ const isActive = tab.id === activeTabId;
1054
+ const ext = (tab.extension || '').toLowerCase();
1055
+ const icon = ext === '.json' ? '{ }' : (ext === '.ts' || ext === '.js' ? 'TS' : '📝');
1056
+ return h('div', {
1057
+ key: tab.id,
1058
+ className: `dsh-preview-tab-item ${isActive ? 'active' : ''}`,
1059
+ onClick: () => switchActiveTab(tab.id),
1060
+ title: `${tab.title}\n${tab.filePath}`,
1061
+ },
1062
+ h('span', { className: 'dsh-tab-icon' }, icon),
1063
+ h('span', { className: 'dsh-tab-title' }, tab.title),
1064
+ h('button', {
1065
+ type: 'button',
1066
+ className: 'dsh-tab-close-btn',
1067
+ onClick: (e) => closePreviewTab(tab.id, e),
1068
+ title: '关闭此标签',
1069
+ }, '✕')
1070
+ );
1071
+ })
1072
+ ),
1073
+
1074
+ // Header Right Window Controls
1075
+ h('div', { className: 'dsh-preview-window-controls' },
1076
+ tabs.length > 1 ? h('button', {
1077
+ type: 'button',
1078
+ className: 'dsh-win-ctrl-btn',
1079
+ onClick: closeAllTabs,
1080
+ title: '关闭所有标签页',
1081
+ }, '✕ 全部') : null,
1082
+ h('button', {
1083
+ type: 'button',
1084
+ className: 'dsh-win-ctrl-btn dsh-win-ctrl-close',
1085
+ onClick: () => setPanelOpen(false),
1086
+ title: '收起预览面板 (Esc)',
1087
+ }, '✕')
1088
+ )
1089
+ ),
1090
+
1091
+ // Drawer Body
1092
+ activeTab ? h(MarkdownPreviewView, {
1093
+ tab: activeTab,
1094
+ fileInfo: currentFileInfo,
1095
+ isLoading: currentLoading,
1096
+ error: currentError,
1097
+ onRefresh: () => loadTabFile(activeTab, true),
1098
+ onOpenNative: triggerOpenNative,
1099
+ onReveal: triggerRevealInExplorer,
1100
+ }) : h('div', { className: 'dsh-preview-empty-state' }, '暂无打开的文件')
1101
+ ) : null
1102
+ );
1103
+ }
1104
+
1105
+ // =========================================================================
1106
+ // Global Link Click Listener & Workspace Interceptor
1107
+ // =========================================================================
1108
+ function setupFileInterceptors(ctx) {
1109
+ // 1. Intercept ctx.workspaces.openPath
1110
+ if (ctx && ctx.workspaces && typeof ctx.workspaces.openPath === 'function') {
1111
+ const originalOpenPath = ctx.workspaces.openPath.bind(ctx.workspaces);
1112
+ ctx.workspaces.openPath = async function(targetPath) {
1113
+ if (isPreviewableFile(targetPath)) {
1114
+ console.log('[Preview] Intercepted workspaces.openPath ->', targetPath);
1115
+ openPreviewFile(targetPath);
1116
+ return Promise.resolve();
1117
+ }
1118
+ return originalOpenPath(targetPath);
1119
+ };
1120
+ console.log('[Preview] Successfully hooked ctx.workspaces.openPath');
1121
+ }
1122
+
1123
+ // 2. Intercept DOM Clicks globally
1124
+ if (typeof document !== 'undefined') {
1125
+ document.addEventListener('click', (e) => {
1126
+ const target = e.target;
1127
+ if (!target || !target.closest) return;
1128
+
1129
+ // A. Click on ProducedFiles chip button or ToolRow summary button
1130
+ const button = target.closest('button');
1131
+ if (button) {
1132
+ const title = button.getAttribute('title') || '';
1133
+ const ariaLabel = button.getAttribute('aria-label') || '';
1134
+ const text = (button.textContent || '').trim();
1135
+
1136
+ const candidate = title || ariaLabel || text;
1137
+ if (isPreviewableFile(candidate)) {
1138
+ const fullPath = resolveAbsoluteFilePath(candidate);
1139
+ if (fullPath) {
1140
+ e.preventDefault();
1141
+ e.stopPropagation();
1142
+ console.log('[Preview] Intercepted button click ->', fullPath);
1143
+ openPreviewFile(fullPath);
1144
+ return;
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ // B. Click on <a> link pointing to .md or file://
1150
+ const anchor = target.closest('a');
1151
+ if (anchor) {
1152
+ const href = anchor.getAttribute('href') || '';
1153
+ if (href.startsWith('#') && !href.startsWith('#heading-')) return;
1154
+ if (href.startsWith('file://') || isPreviewableFile(href)) {
1155
+ e.preventDefault();
1156
+ e.stopPropagation();
1157
+ const fullPath = resolveAbsoluteFilePath(href);
1158
+ console.log('[Preview] Intercepted link click ->', fullPath);
1159
+ openPreviewFile(fullPath);
1160
+ }
1161
+ }
1162
+ }, true); // Use capture phase to intercept early!
1163
+ }
1164
+ }
1165
+
1166
+ // =========================================================================
1167
+ // Inject Custom Stylesheet
1168
+ // =========================================================================
1169
+ function injectStyles() {
1170
+ if (typeof document === 'undefined') return;
1171
+ if (document.getElementById('dsh-plugin-preview-styles')) return;
1172
+
1173
+ const style = document.createElement('style');
1174
+ style.id = 'dsh-plugin-preview-styles';
1175
+ style.textContent = `
1176
+ /* =====================================================================
1177
+ DeepSeek Harness Preview Plugin Styles (WorkBuddy FileTabs Inspired)
1178
+ ===================================================================== */
1179
+
1180
+ /* Floating Pill Button */
1181
+ .dsh-preview-float-pill {
1182
+ position: fixed;
1183
+ bottom: 84px;
1184
+ right: 20px;
1185
+ z-index: 9990;
1186
+ display: flex;
1187
+ align-items: center;
1188
+ gap: 8px;
1189
+ padding: 8px 14px;
1190
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
1191
+ color: var(--dsw-alias-label-primary, #0f172a);
1192
+ border: 1px solid var(--dsw-alias-border-l3, #cbd5e1);
1193
+ border-radius: 20px;
1194
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
1195
+ cursor: pointer;
1196
+ font-size: 13px;
1197
+ font-weight: 500;
1198
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1199
+ user-select: none;
1200
+ }
1201
+ body[data-ds-dark-theme] .dsh-preview-float-pill {
1202
+ background: var(--dsw-alias-bg-layer-2, #1e222d);
1203
+ color: var(--dsw-alias-label-primary, #e2e8f0);
1204
+ border-color: var(--dsw-alias-border-l3, #334155);
1205
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
1206
+ }
1207
+ .dsh-preview-float-pill:hover {
1208
+ transform: translateY(-2px);
1209
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
1210
+ box-shadow: 0 10px 24px rgba(65, 118, 230, 0.25);
1211
+ }
1212
+ .dsh-float-pill-badge {
1213
+ background: var(--dsw-static-deepseek-500, #4176e6);
1214
+ color: #fff;
1215
+ font-size: 11px;
1216
+ padding: 1px 6px;
1217
+ border-radius: 10px;
1218
+ font-weight: 600;
1219
+ }
1220
+
1221
+ /* Right-Side Drawer Container */
1222
+ .dsh-preview-drawer-container {
1223
+ position: fixed;
1224
+ top: 0;
1225
+ right: 0;
1226
+ bottom: 0;
1227
+ height: 100vh;
1228
+ z-index: 9995;
1229
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1230
+ color: var(--dsw-alias-label-primary, #0f172a);
1231
+ border-left: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
1232
+ box-shadow: -8px 0 32px rgba(0, 0, 0, 0.14);
1233
+ display: flex;
1234
+ flex-direction: column;
1235
+ box-sizing: border-box;
1236
+ overflow: hidden;
1237
+ animation: dshPreviewSlideIn 0.24s cubic-bezier(0.16, 1, 0.3, 1);
1238
+ }
1239
+ body[data-ds-dark-theme] .dsh-preview-drawer-container {
1240
+ background: var(--dsw-alias-bg-layer-1, #161922);
1241
+ color: var(--dsw-alias-label-primary, #e2e8f0);
1242
+ border-left-color: var(--dsw-alias-border-l3, #2d3748);
1243
+ box-shadow: -12px 0 40px rgba(0, 0, 0, 0.45);
1244
+ }
1245
+ .dsh-preview-drawer-container.dragging {
1246
+ user-select: none;
1247
+ transition: none;
1248
+ }
1249
+ @keyframes dshPreviewSlideIn {
1250
+ from { transform: translateX(100%); }
1251
+ to { transform: translateX(0); }
1252
+ }
1253
+
1254
+ /* Left Resize Handle */
1255
+ .dsh-preview-drag-handle {
1256
+ position: absolute;
1257
+ top: 0;
1258
+ left: -4px;
1259
+ bottom: 0;
1260
+ width: 8px;
1261
+ cursor: col-resize;
1262
+ z-index: 100;
1263
+ transition: background 0.15s;
1264
+ }
1265
+ .dsh-preview-drag-handle:hover,
1266
+ .dsh-preview-drawer-container.dragging .dsh-preview-drag-handle {
1267
+ background: var(--dsw-static-deepseek-500, #4176e6);
1268
+ }
1269
+
1270
+ /* Header & FileTabs */
1271
+ .dsh-preview-drawer-header {
1272
+ display: flex;
1273
+ align-items: center;
1274
+ justify-content: space-between;
1275
+ height: 38px;
1276
+ min-height: 38px;
1277
+ flex-shrink: 0;
1278
+ background: var(--dsw-alias-bg-layer-2, #f1f5f9);
1279
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1280
+ padding: 0 8px;
1281
+ overflow: hidden;
1282
+ box-sizing: border-box;
1283
+ }
1284
+ body[data-ds-dark-theme] .dsh-preview-drawer-header {
1285
+ background: var(--dsw-alias-bg-layer-2, #12151d);
1286
+ border-bottom-color: var(--dsw-alias-border-l2, #2d3748);
1287
+ }
1288
+ .dsh-preview-tabs-row {
1289
+ display: flex;
1290
+ align-items: flex-end;
1291
+ gap: 4px;
1292
+ overflow-x: auto;
1293
+ scrollbar-width: none;
1294
+ flex: 1;
1295
+ height: 100%;
1296
+ padding-top: 4px;
1297
+ }
1298
+ .dsh-preview-tabs-row::-webkit-scrollbar { display: none; }
1299
+
1300
+ .dsh-preview-tab-item {
1301
+ display: flex;
1302
+ align-items: center;
1303
+ gap: 6px;
1304
+ padding: 0 10px;
1305
+ height: 30px;
1306
+ border-radius: 6px 6px 0 0;
1307
+ font-size: 12px;
1308
+ color: var(--dsw-alias-label-secondary, #64748b);
1309
+ background: transparent;
1310
+ cursor: pointer;
1311
+ user-select: none;
1312
+ white-space: nowrap;
1313
+ max-width: 180px;
1314
+ transition: all 0.15s;
1315
+ border: 1px solid transparent;
1316
+ border-bottom: none;
1317
+ }
1318
+ .dsh-preview-tab-item:hover {
1319
+ background: rgba(0, 0, 0, 0.04);
1320
+ color: var(--dsw-alias-label-primary, #0f172a);
1321
+ }
1322
+ body[data-ds-dark-theme] .dsh-preview-tab-item:hover {
1323
+ background: rgba(255, 255, 255, 0.05);
1324
+ color: var(--dsw-alias-label-primary, #f1f5f9);
1325
+ }
1326
+ .dsh-preview-tab-item.active {
1327
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1328
+ color: var(--dsw-static-deepseek-500, #4176e6);
1329
+ font-weight: 600;
1330
+ border-color: var(--dsw-alias-border-l2, #e2e8f0);
1331
+ border-bottom: 2px solid var(--dsw-static-deepseek-500, #4176e6);
1332
+ box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.03);
1333
+ }
1334
+ body[data-ds-dark-theme] .dsh-preview-tab-item.active {
1335
+ background: var(--dsw-alias-bg-layer-1, #161922);
1336
+ border-color: var(--dsw-alias-border-l2, #2d3748);
1337
+ border-bottom: 2px solid var(--dsw-static-deepseek-500, #4176e6);
1338
+ box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.2);
1339
+ }
1340
+ .dsh-tab-icon {
1341
+ font-size: 12px;
1342
+ }
1343
+ .dsh-tab-title {
1344
+ overflow: hidden;
1345
+ text-overflow: ellipsis;
1346
+ white-space: nowrap;
1347
+ }
1348
+ .dsh-tab-close-btn {
1349
+ background: none;
1350
+ border: none;
1351
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
1352
+ font-size: 11px;
1353
+ cursor: pointer;
1354
+ padding: 2px 4px;
1355
+ border-radius: 4px;
1356
+ line-height: 1;
1357
+ }
1358
+ .dsh-tab-close-btn:hover {
1359
+ background: rgba(239, 68, 68, 0.15);
1360
+ color: #ef4444;
1361
+ }
1362
+
1363
+ .dsh-preview-window-controls {
1364
+ display: flex;
1365
+ align-items: center;
1366
+ gap: 6px;
1367
+ padding-left: 8px;
1368
+ height: 100%;
1369
+ }
1370
+ .dsh-win-ctrl-btn {
1371
+ background: transparent;
1372
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
1373
+ color: var(--dsw-alias-label-secondary, #64748b);
1374
+ font-size: 11px;
1375
+ padding: 3px 8px;
1376
+ border-radius: 4px;
1377
+ cursor: pointer;
1378
+ transition: all 0.15s;
1379
+ }
1380
+ .dsh-win-ctrl-btn:hover {
1381
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1382
+ color: var(--dsw-alias-label-primary, #0f172a);
1383
+ }
1384
+ body[data-ds-dark-theme] .dsh-win-ctrl-btn {
1385
+ border-color: var(--dsw-alias-border-l2, #334155);
1386
+ color: var(--dsw-alias-label-secondary, #94a3b8);
1387
+ }
1388
+ body[data-ds-dark-theme] .dsh-win-ctrl-btn:hover {
1389
+ background: var(--dsw-alias-bg-layer-3, #283042);
1390
+ color: var(--dsw-alias-label-primary, #f1f5f9);
1391
+ }
1392
+ .dsh-win-ctrl-close:hover {
1393
+ background: #ef4444 !important;
1394
+ border-color: #ef4444 !important;
1395
+ color: #fff !important;
1396
+ }
1397
+
1398
+ /* Content Container */
1399
+ .dsh-preview-content-container {
1400
+ display: flex;
1401
+ flex-direction: column;
1402
+ flex: 1;
1403
+ min-height: 0;
1404
+ overflow: hidden;
1405
+ box-sizing: border-box;
1406
+ }
1407
+
1408
+ /* Toolbar */
1409
+ .dsh-preview-toolbar {
1410
+ display: flex;
1411
+ align-items: center;
1412
+ justify-content: space-between;
1413
+ padding: 6px 12px;
1414
+ height: 38px;
1415
+ min-height: 38px;
1416
+ flex-shrink: 0;
1417
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
1418
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1419
+ gap: 10px;
1420
+ box-sizing: border-box;
1421
+ }
1422
+ body[data-ds-dark-theme] .dsh-preview-toolbar {
1423
+ background: var(--dsw-alias-bg-layer-2, #161a24);
1424
+ border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
1425
+ }
1426
+ .dsh-preview-toolbar-left,
1427
+ .dsh-preview-toolbar-right {
1428
+ display: flex;
1429
+ align-items: center;
1430
+ gap: 6px;
1431
+ flex-shrink: 0;
1432
+ white-space: nowrap;
1433
+ }
1434
+
1435
+ .dsh-preview-mode-group {
1436
+ display: flex;
1437
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1438
+ padding: 2px;
1439
+ border-radius: 6px;
1440
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1441
+ flex-shrink: 0;
1442
+ }
1443
+ body[data-ds-dark-theme] .dsh-preview-mode-group {
1444
+ background: var(--dsw-alias-bg-layer-3, #0d1017);
1445
+ border-color: var(--dsw-alias-border-l2, #252d3d);
1446
+ }
1447
+ .dsh-preview-mode-btn {
1448
+ background: transparent;
1449
+ border: none;
1450
+ color: var(--dsw-alias-label-secondary, #64748b);
1451
+ font-size: 11px;
1452
+ padding: 4px 9px;
1453
+ border-radius: 4px;
1454
+ cursor: pointer;
1455
+ transition: all 0.15s;
1456
+ white-space: nowrap;
1457
+ flex-shrink: 0;
1458
+ }
1459
+ body[data-ds-dark-theme] .dsh-preview-mode-btn {
1460
+ color: var(--dsw-alias-label-secondary, #94a3b8);
1461
+ }
1462
+ .dsh-preview-mode-btn.active {
1463
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1464
+ color: var(--dsw-alias-label-primary, #0f172a);
1465
+ font-weight: 600;
1466
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1467
+ }
1468
+ body[data-ds-dark-theme] .dsh-preview-mode-btn.active {
1469
+ background: var(--dsw-alias-bg-layer-2, #1e2433);
1470
+ color: var(--dsw-alias-label-primary, #f8fafc);
1471
+ box-shadow: 0 1px 4px rgba(0,0,0,0.25);
1472
+ }
1473
+ .dsh-preview-tool-btn {
1474
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1475
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
1476
+ color: var(--dsw-alias-label-primary, #334155);
1477
+ font-size: 11px;
1478
+ padding: 4px 9px;
1479
+ border-radius: 5px;
1480
+ cursor: pointer;
1481
+ transition: all 0.15s;
1482
+ display: flex;
1483
+ align-items: center;
1484
+ gap: 4px;
1485
+ white-space: nowrap;
1486
+ flex-shrink: 0;
1487
+ }
1488
+ body[data-ds-dark-theme] .dsh-preview-tool-btn {
1489
+ background: var(--dsw-alias-bg-layer-2, #1e2433);
1490
+ border-color: var(--dsw-alias-border-l2, #2d3748);
1491
+ color: var(--dsw-alias-label-secondary, #cbd5e1);
1492
+ }
1493
+ .dsh-preview-tool-btn:hover {
1494
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1495
+ color: var(--dsw-static-deepseek-500, #4176e6);
1496
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
1497
+ }
1498
+ body[data-ds-dark-theme] .dsh-preview-tool-btn:hover {
1499
+ background: var(--dsw-alias-bg-layer-3, #293347);
1500
+ color: #fff;
1501
+ }
1502
+ .dsh-preview-tool-btn.active {
1503
+ background: rgba(65, 118, 230, 0.12);
1504
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
1505
+ color: var(--dsw-static-deepseek-500, #4176e6);
1506
+ font-weight: 600;
1507
+ }
1508
+ .dsh-preview-tool-btn-accent {
1509
+ background: rgba(65, 118, 230, 0.1);
1510
+ border-color: rgba(65, 118, 230, 0.35);
1511
+ color: var(--dsw-static-deepseek-500, #2563eb);
1512
+ font-weight: 500;
1513
+ }
1514
+ body[data-ds-dark-theme] .dsh-preview-tool-btn-accent {
1515
+ background: rgba(65, 118, 230, 0.15);
1516
+ border-color: rgba(65, 118, 230, 0.4);
1517
+ color: #60a5fa;
1518
+ }
1519
+ .dsh-preview-tool-btn-accent:hover {
1520
+ background: var(--dsw-static-deepseek-500, #4176e6) !important;
1521
+ color: #fff !important;
1522
+ }
1523
+
1524
+ /* Search Bar */
1525
+ .dsh-preview-search-bar {
1526
+ display: flex;
1527
+ align-items: center;
1528
+ gap: 8px;
1529
+ padding: 6px 12px;
1530
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1531
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1532
+ }
1533
+ body[data-ds-dark-theme] .dsh-preview-search-bar {
1534
+ background: var(--dsw-alias-bg-layer-3, #0d1017);
1535
+ border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
1536
+ }
1537
+ .dsh-preview-search-input {
1538
+ flex: 1;
1539
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1540
+ border: 1px solid var(--dsw-alias-border-l3, #cbd5e1);
1541
+ color: var(--dsw-alias-label-primary, #0f172a);
1542
+ font-size: 12px;
1543
+ padding: 4px 10px;
1544
+ border-radius: 4px;
1545
+ outline: none;
1546
+ }
1547
+ body[data-ds-dark-theme] .dsh-preview-search-input {
1548
+ background: var(--dsw-alias-bg-layer-2, #1a1f2c);
1549
+ border-color: var(--dsw-alias-border-l3, #334155);
1550
+ color: #f1f5f9;
1551
+ }
1552
+ .dsh-preview-search-input:focus {
1553
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
1554
+ }
1555
+ .dsh-search-clear-btn {
1556
+ background: none;
1557
+ border: none;
1558
+ color: var(--dsw-alias-label-secondary, #64748b);
1559
+ cursor: pointer;
1560
+ }
1561
+ mark.dsh-search-highlight {
1562
+ background: #fde047;
1563
+ color: #000;
1564
+ padding: 1px 3px;
1565
+ border-radius: 2px;
1566
+ }
1567
+
1568
+ /* Body Layout & TOC */
1569
+ .dsh-preview-body-layout {
1570
+ display: flex;
1571
+ flex: 1;
1572
+ overflow: hidden;
1573
+ position: relative;
1574
+ }
1575
+ .dsh-preview-toc-sidebar {
1576
+ width: 220px;
1577
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
1578
+ border-right: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1579
+ display: flex;
1580
+ flex-direction: column;
1581
+ flex-shrink: 0;
1582
+ animation: dshTocSlide 0.2s ease;
1583
+ }
1584
+ body[data-ds-dark-theme] .dsh-preview-toc-sidebar {
1585
+ background: var(--dsw-alias-bg-layer-2, #161922);
1586
+ border-right-color: var(--dsw-alias-border-l2, #262c3b);
1587
+ }
1588
+ @keyframes dshTocSlide {
1589
+ from { width: 0; opacity: 0; }
1590
+ to { width: 220px; opacity: 1; }
1591
+ }
1592
+ .dsh-preview-toc-header {
1593
+ display: flex;
1594
+ align-items: center;
1595
+ justify-content: space-between;
1596
+ padding: 10px 12px;
1597
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1598
+ color: var(--dsw-alias-label-secondary, #64748b);
1599
+ }
1600
+ body[data-ds-dark-theme] .dsh-preview-toc-header {
1601
+ border-bottom-color: var(--dsw-alias-border-l2, #262c3b);
1602
+ color: var(--dsw-alias-label-secondary, #94a3b8);
1603
+ }
1604
+ .dsh-toc-close-btn {
1605
+ background: none;
1606
+ border: none;
1607
+ color: #94a3b8;
1608
+ cursor: pointer;
1609
+ }
1610
+ .dsh-preview-toc-list {
1611
+ flex: 1;
1612
+ overflow-y: auto;
1613
+ padding: 8px 4px;
1614
+ }
1615
+ .dsh-toc-item {
1616
+ display: flex;
1617
+ align-items: center;
1618
+ gap: 6px;
1619
+ padding: 5px 8px;
1620
+ border-radius: 4px;
1621
+ font-size: 12px;
1622
+ color: var(--dsw-alias-label-secondary, #64748b);
1623
+ cursor: pointer;
1624
+ white-space: nowrap;
1625
+ overflow: hidden;
1626
+ text-overflow: ellipsis;
1627
+ }
1628
+ body[data-ds-dark-theme] .dsh-toc-item {
1629
+ color: var(--dsw-alias-label-secondary, #94a3b8);
1630
+ }
1631
+ .dsh-toc-item:hover {
1632
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1633
+ color: var(--dsw-static-deepseek-500, #4176e6);
1634
+ }
1635
+ body[data-ds-dark-theme] .dsh-toc-item:hover {
1636
+ background: var(--dsw-alias-bg-layer-3, #222938);
1637
+ }
1638
+ .dsh-toc-level-1 { padding-left: 8px; font-weight: 600; color: var(--dsw-alias-label-primary, #0f172a); }
1639
+ body[data-ds-dark-theme] .dsh-toc-level-1 { color: #f1f5f9; }
1640
+ .dsh-toc-level-2 { padding-left: 18px; }
1641
+ .dsh-toc-level-3 { padding-left: 28px; font-size: 11px; }
1642
+ .dsh-toc-level-4 { padding-left: 36px; font-size: 11px; }
1643
+ .dsh-toc-bullet { opacity: 0.5; }
1644
+
1645
+ /* Main Scroll Content */
1646
+ .dsh-preview-main-scroll {
1647
+ flex: 1;
1648
+ overflow-y: auto;
1649
+ padding: 24px 32px;
1650
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1651
+ }
1652
+ body[data-ds-dark-theme] .dsh-preview-main-scroll {
1653
+ background: var(--dsw-alias-bg-layer-1, #161922);
1654
+ }
1655
+
1656
+ /* Status Footer */
1657
+ .dsh-preview-footer {
1658
+ display: flex;
1659
+ align-items: center;
1660
+ justify-content: space-between;
1661
+ height: 28px;
1662
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
1663
+ border-top: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1664
+ padding: 0 14px;
1665
+ font-size: 11px;
1666
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
1667
+ }
1668
+ body[data-ds-dark-theme] .dsh-preview-footer {
1669
+ background: var(--dsw-alias-bg-layer-2, #161922);
1670
+ border-top-color: var(--dsw-alias-border-l2, #252d3d);
1671
+ color: var(--dsw-alias-label-tertiary, #64748b);
1672
+ }
1673
+ .dsh-footer-left {
1674
+ display: flex;
1675
+ align-items: center;
1676
+ gap: 8px;
1677
+ overflow: hidden;
1678
+ text-overflow: ellipsis;
1679
+ white-space: nowrap;
1680
+ flex: 1;
1681
+ }
1682
+ .dsh-footer-ext {
1683
+ background: var(--dsw-alias-bg-layer-3, #e2e8f0);
1684
+ color: var(--dsw-alias-label-secondary, #475569);
1685
+ padding: 1px 4px;
1686
+ border-radius: 3px;
1687
+ font-size: 10px;
1688
+ font-weight: 600;
1689
+ }
1690
+ body[data-ds-dark-theme] .dsh-footer-ext {
1691
+ background: var(--dsw-alias-bg-layer-3, #262e3d);
1692
+ color: #94a3b8;
1693
+ }
1694
+ .dsh-footer-path {
1695
+ overflow: hidden;
1696
+ text-overflow: ellipsis;
1697
+ }
1698
+ .dsh-footer-right {
1699
+ display: flex;
1700
+ gap: 12px;
1701
+ flex-shrink: 0;
1702
+ }
1703
+ .dsh-footer-stat {
1704
+ color: var(--dsw-alias-label-secondary, #64748b);
1705
+ }
1706
+ body[data-ds-dark-theme] .dsh-footer-stat {
1707
+ color: var(--dsw-alias-label-secondary, #94a3b8);
1708
+ }
1709
+
1710
+ /* Markdown Typography Styles */
1711
+ .dsh-markdown-body {
1712
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
1713
+ font-size: 14px;
1714
+ line-height: 1.72;
1715
+ color: var(--dsw-alias-label-primary, #1e293b);
1716
+ word-break: break-word;
1717
+ }
1718
+ body[data-ds-dark-theme] .dsh-markdown-body {
1719
+ color: var(--dsw-alias-label-primary, #e2e8f0);
1720
+ }
1721
+ .dsh-markdown-body h1,
1722
+ .dsh-markdown-body h2,
1723
+ .dsh-markdown-body h3,
1724
+ .dsh-markdown-body h4,
1725
+ .dsh-markdown-body h5,
1726
+ .dsh-markdown-body h6 {
1727
+ position: relative;
1728
+ color: var(--dsw-alias-label-primary, #0f172a);
1729
+ font-weight: 600;
1730
+ margin-top: 1.5em;
1731
+ margin-bottom: 0.6em;
1732
+ line-height: 1.35;
1733
+ }
1734
+ body[data-ds-dark-theme] .dsh-markdown-body h1,
1735
+ body[data-ds-dark-theme] .dsh-markdown-body h2,
1736
+ body[data-ds-dark-theme] .dsh-markdown-body h3,
1737
+ body[data-ds-dark-theme] .dsh-markdown-body h4,
1738
+ body[data-ds-dark-theme] .dsh-markdown-body h5,
1739
+ body[data-ds-dark-theme] .dsh-markdown-body h6 {
1740
+ color: #f8fafc;
1741
+ }
1742
+ .dsh-markdown-body h1 { font-size: 1.85em; border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0); padding-bottom: 0.3em; }
1743
+ .dsh-markdown-body h2 { font-size: 1.45em; border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0); padding-bottom: 0.25em; }
1744
+ body[data-ds-dark-theme] .dsh-markdown-body h1 { border-bottom-color: var(--dsw-alias-border-l2, #334155); }
1745
+ body[data-ds-dark-theme] .dsh-markdown-body h2 { border-bottom-color: var(--dsw-alias-border-l2, #273344); }
1746
+ .dsh-markdown-body h3 { font-size: 1.25em; }
1747
+ .dsh-markdown-body h4 { font-size: 1.1em; }
1748
+ .dsh-markdown-body h5 { font-size: 0.95em; }
1749
+ .dsh-markdown-body h6 { font-size: 0.88em; color: var(--dsw-alias-label-secondary, #64748b); }
1750
+ .dsh-md-anchor {
1751
+ position: absolute;
1752
+ left: -18px;
1753
+ opacity: 0;
1754
+ color: var(--dsw-static-deepseek-500, #4176e6);
1755
+ text-decoration: none;
1756
+ transition: opacity 0.15s;
1757
+ }
1758
+ .dsh-markdown-body h1:hover .dsh-md-anchor,
1759
+ .dsh-markdown-body h2:hover .dsh-md-anchor,
1760
+ .dsh-markdown-body h3:hover .dsh-md-anchor {
1761
+ opacity: 1;
1762
+ }
1763
+ .dsh-md-p {
1764
+ margin-bottom: 1em;
1765
+ }
1766
+ .dsh-md-hr {
1767
+ border: none;
1768
+ height: 1px;
1769
+ background: var(--dsw-alias-border-l2, #e2e8f0);
1770
+ margin: 2em 0;
1771
+ }
1772
+ body[data-ds-dark-theme] .dsh-md-hr {
1773
+ background: var(--dsw-alias-border-l2, #334155);
1774
+ }
1775
+ .dsh-md-blockquote {
1776
+ margin: 1.2em 0;
1777
+ padding: 8px 16px;
1778
+ border-left: 4px solid var(--dsw-static-deepseek-500, #4176e6);
1779
+ background: rgba(65, 118, 230, 0.05);
1780
+ border-radius: 0 6px 6px 0;
1781
+ color: var(--dsw-alias-label-secondary, #475569);
1782
+ }
1783
+ body[data-ds-dark-theme] .dsh-md-blockquote {
1784
+ color: var(--dsw-alias-label-secondary, #cbd5e1);
1785
+ background: rgba(65, 118, 230, 0.08);
1786
+ }
1787
+ .dsh-md-blockquote p { margin: 0.3em 0; }
1788
+
1789
+ /* Tables */
1790
+ .dsh-md-table-wrapper {
1791
+ width: 100%;
1792
+ overflow-x: auto;
1793
+ margin: 1.2em 0;
1794
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1795
+ border-radius: 8px;
1796
+ }
1797
+ body[data-ds-dark-theme] .dsh-md-table-wrapper {
1798
+ border-color: var(--dsw-alias-border-l2, #334155);
1799
+ }
1800
+ .dsh-md-table-wrapper table {
1801
+ width: 100%;
1802
+ border-collapse: collapse;
1803
+ font-size: 13px;
1804
+ }
1805
+ .dsh-md-table-wrapper th,
1806
+ .dsh-md-table-wrapper td {
1807
+ padding: 8px 14px;
1808
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1809
+ }
1810
+ body[data-ds-dark-theme] .dsh-md-table-wrapper th,
1811
+ body[data-ds-dark-theme] .dsh-md-table-wrapper td {
1812
+ border-color: var(--dsw-alias-border-l2, #273344);
1813
+ }
1814
+ .dsh-md-table-wrapper th {
1815
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
1816
+ font-weight: 600;
1817
+ color: var(--dsw-alias-label-primary, #0f172a);
1818
+ }
1819
+ body[data-ds-dark-theme] .dsh-md-table-wrapper th {
1820
+ background: var(--dsw-alias-bg-layer-2, #1a202c);
1821
+ color: #f1f5f9;
1822
+ }
1823
+ .dsh-md-table-wrapper tr:nth-child(even) {
1824
+ background: rgba(0, 0, 0, 0.015);
1825
+ }
1826
+ body[data-ds-dark-theme] .dsh-md-table-wrapper tr:nth-child(even) {
1827
+ background: rgba(255, 255, 255, 0.02);
1828
+ }
1829
+
1830
+ /* Lists & Tasks */
1831
+ .dsh-md-list {
1832
+ padding-left: 24px;
1833
+ margin: 0.8em 0 1.2em 0;
1834
+ }
1835
+ .dsh-md-list li { margin-bottom: 0.35em; }
1836
+ .dsh-md-task-list {
1837
+ list-style: none;
1838
+ padding-left: 0;
1839
+ }
1840
+ .dsh-md-task-item {
1841
+ display: flex;
1842
+ align-items: center;
1843
+ gap: 8px;
1844
+ margin-bottom: 0.4em;
1845
+ }
1846
+ .dsh-md-task-item input[type="checkbox"] {
1847
+ accent-color: var(--dsw-static-deepseek-500, #4176e6);
1848
+ cursor: pointer;
1849
+ }
1850
+
1851
+ /* Inline Code & Badges */
1852
+ .dsh-md-inline-code {
1853
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1854
+ color: var(--dsw-static-deepseek-500, #2563eb);
1855
+ padding: 2px 6px;
1856
+ border-radius: 4px;
1857
+ font-family: "Cascadia Code", Consolas, "Courier New", monospace;
1858
+ font-size: 0.88em;
1859
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1860
+ }
1861
+ body[data-ds-dark-theme] .dsh-md-inline-code {
1862
+ background: var(--dsw-alias-bg-layer-3, #1e2433);
1863
+ color: #93c5fd;
1864
+ border-color: rgba(147, 197, 253, 0.15);
1865
+ }
1866
+ .dsh-md-link {
1867
+ color: var(--dsw-static-deepseek-500, #2563eb);
1868
+ text-decoration: none;
1869
+ border-bottom: 1px solid rgba(37, 99, 235, 0.3);
1870
+ transition: border-color 0.15s;
1871
+ }
1872
+ body[data-ds-dark-theme] .dsh-md-link {
1873
+ color: var(--dsw-static-deepseek-500, #60a5fa);
1874
+ border-bottom-color: rgba(96, 165, 250, 0.3);
1875
+ }
1876
+ .dsh-md-link:hover {
1877
+ border-color: currentColor;
1878
+ }
1879
+
1880
+ /* Code Cards */
1881
+ .dsh-code-card {
1882
+ margin: 1.4em 0;
1883
+ background: var(--dsw-alias-bg-layer-3, #f8fafc);
1884
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1885
+ border-radius: 8px;
1886
+ overflow: hidden;
1887
+ }
1888
+ body[data-ds-dark-theme] .dsh-code-card {
1889
+ background: #0d1117;
1890
+ border-color: #30363d;
1891
+ }
1892
+ .dsh-code-card-header {
1893
+ display: flex;
1894
+ align-items: center;
1895
+ justify-content: space-between;
1896
+ padding: 6px 12px;
1897
+ background: rgba(0, 0, 0, 0.03);
1898
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1899
+ }
1900
+ body[data-ds-dark-theme] .dsh-code-card-header {
1901
+ background: rgba(255, 255, 255, 0.04);
1902
+ border-bottom-color: #21262d;
1903
+ }
1904
+ .dsh-code-lang-badge {
1905
+ font-family: monospace;
1906
+ font-size: 11px;
1907
+ color: var(--dsw-alias-label-secondary, #64748b);
1908
+ font-weight: 600;
1909
+ }
1910
+ body[data-ds-dark-theme] .dsh-code-lang-badge {
1911
+ color: #8b949e;
1912
+ }
1913
+ .dsh-code-copy-btn {
1914
+ background: transparent;
1915
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
1916
+ color: var(--dsw-alias-label-secondary, #475569);
1917
+ font-size: 11px;
1918
+ padding: 2px 8px;
1919
+ border-radius: 4px;
1920
+ cursor: pointer;
1921
+ transition: all 0.15s;
1922
+ }
1923
+ body[data-ds-dark-theme] .dsh-code-copy-btn {
1924
+ border-color: #30363d;
1925
+ color: #c9d1d9;
1926
+ }
1927
+ .dsh-code-copy-btn:hover {
1928
+ background: var(--dsw-alias-bg-layer-2, #e2e8f0);
1929
+ color: var(--dsw-static-deepseek-500, #2563eb);
1930
+ }
1931
+ body[data-ds-dark-theme] .dsh-code-copy-btn:hover {
1932
+ background: #21262d;
1933
+ color: #58a6ff;
1934
+ }
1935
+ .dsh-code-pre {
1936
+ margin: 0;
1937
+ padding: 14px 16px;
1938
+ overflow-x: auto;
1939
+ font-family: "Fira Code", "Cascadia Code", Consolas, monospace;
1940
+ font-size: 13px;
1941
+ line-height: 1.55;
1942
+ color: var(--dsw-alias-label-primary, #0f172a);
1943
+ }
1944
+ body[data-ds-dark-theme] .dsh-code-pre {
1945
+ color: #e6edf3;
1946
+ }
1947
+ .dsh-token-keyword { color: #d73a49; font-weight: 500; }
1948
+ body[data-ds-dark-theme] .dsh-token-keyword { color: #ff7b72; }
1949
+ .dsh-token-string { color: #032f62; }
1950
+ body[data-ds-dark-theme] .dsh-token-string { color: #a5d6ff; }
1951
+ .dsh-token-comment { color: #6a737d; font-style: italic; }
1952
+ body[data-ds-dark-theme] .dsh-token-comment { color: #8b949e; }
1953
+ .dsh-token-number { color: #005cc5; }
1954
+ body[data-ds-dark-theme] .dsh-token-number { color: #79c0ff; }
1955
+
1956
+ /* GitHub Alert Callouts */
1957
+ .dsh-alert {
1958
+ margin: 1.4em 0;
1959
+ padding: 12px 16px;
1960
+ border-left: 4px solid;
1961
+ border-radius: 0 8px 8px 0;
1962
+ }
1963
+ .dsh-alert-title {
1964
+ display: flex;
1965
+ align-items: center;
1966
+ gap: 6px;
1967
+ font-weight: 600;
1968
+ font-size: 13px;
1969
+ margin-bottom: 6px;
1970
+ }
1971
+ .dsh-alert-note { background: rgba(59, 130, 246, 0.08); border-color: #3b82f6; }
1972
+ .dsh-alert-note .dsh-alert-title { color: #2563eb; }
1973
+ body[data-ds-dark-theme] .dsh-alert-note .dsh-alert-title { color: #60a5fa; }
1974
+ .dsh-alert-tip { background: rgba(16, 185, 129, 0.08); border-color: #10b981; }
1975
+ .dsh-alert-tip .dsh-alert-title { color: #059669; }
1976
+ body[data-ds-dark-theme] .dsh-alert-tip .dsh-alert-title { color: #34d399; }
1977
+ .dsh-alert-important { background: rgba(168, 85, 247, 0.08); border-color: #a855f7; }
1978
+ .dsh-alert-important .dsh-alert-title { color: #7c3aed; }
1979
+ body[data-ds-dark-theme] .dsh-alert-important .dsh-alert-title { color: #c084fc; }
1980
+ .dsh-alert-warning { background: rgba(245, 158, 11, 0.08); border-color: #f59e0b; }
1981
+ .dsh-alert-warning .dsh-alert-title { color: #d97706; }
1982
+ body[data-ds-dark-theme] .dsh-alert-warning .dsh-alert-title { color: #fbbf24; }
1983
+ .dsh-alert-caution { background: rgba(239, 68, 68, 0.08); border-color: #ef4444; }
1984
+ .dsh-alert-caution .dsh-alert-title { color: #dc2626; }
1985
+ body[data-ds-dark-theme] .dsh-alert-caution .dsh-alert-title { color: #f87171; }
1986
+ .dsh-alert-body { font-size: 13px; color: var(--dsw-alias-label-secondary, #475569); }
1987
+ body[data-ds-dark-theme] .dsh-alert-body { color: #cbd5e1; }
1988
+
1989
+ /* Math Formulas */
1990
+ .dsh-math-block {
1991
+ margin: 1.2em 0;
1992
+ padding: 12px 16px;
1993
+ background: rgba(0, 0, 0, 0.02);
1994
+ border-radius: 6px;
1995
+ border: 1px dashed var(--dsw-alias-border-l2, #cbd5e1);
1996
+ text-align: center;
1997
+ font-family: "KaTeX_Math", "Times New Roman", serif;
1998
+ font-size: 1.1em;
1999
+ color: var(--dsw-alias-label-primary, #1e293b);
2000
+ }
2001
+ body[data-ds-dark-theme] .dsh-math-block {
2002
+ background: rgba(255, 255, 255, 0.03);
2003
+ border-color: var(--dsw-alias-border-l2, #334155);
2004
+ color: #cbd5e1;
2005
+ }
2006
+ .dsh-math-inline {
2007
+ font-family: "KaTeX_Math", "Times New Roman", serif;
2008
+ color: var(--dsw-static-deepseek-500, #2563eb);
2009
+ padding: 0 3px;
2010
+ }
2011
+ body[data-ds-dark-theme] .dsh-math-inline {
2012
+ color: #93c5fd;
2013
+ }
2014
+
2015
+ /* Source View Mode */
2016
+ .dsh-source-view {
2017
+ display: flex;
2018
+ background: var(--dsw-alias-bg-layer-3, #f8fafc);
2019
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2020
+ border-radius: 8px;
2021
+ overflow: hidden;
2022
+ font-family: "Fira Code", Consolas, monospace;
2023
+ font-size: 13px;
2024
+ line-height: 1.6;
2025
+ }
2026
+ body[data-ds-dark-theme] .dsh-source-view {
2027
+ background: var(--dsw-alias-bg-layer-3, #0d1017);
2028
+ border-color: var(--dsw-alias-border-l2, #252d3d);
2029
+ }
2030
+ .dsh-source-line-numbers {
2031
+ padding: 16px 8px;
2032
+ background: rgba(0, 0, 0, 0.02);
2033
+ border-right: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2034
+ color: #94a3b8;
2035
+ text-align: right;
2036
+ user-select: none;
2037
+ }
2038
+ body[data-ds-dark-theme] .dsh-source-line-numbers {
2039
+ background: rgba(255, 255, 255, 0.02);
2040
+ border-right-color: var(--dsw-alias-border-l2, #252d3d);
2041
+ color: #475569;
2042
+ }
2043
+ .dsh-source-pre {
2044
+ margin: 0;
2045
+ padding: 16px;
2046
+ flex: 1;
2047
+ overflow-x: auto;
2048
+ color: var(--dsw-alias-label-primary, #0f172a);
2049
+ }
2050
+ body[data-ds-dark-theme] .dsh-source-pre {
2051
+ color: #cbd5e1;
2052
+ }
2053
+
2054
+ /* Split View Mode */
2055
+ .dsh-split-view {
2056
+ display: flex;
2057
+ gap: 12px;
2058
+ height: 100%;
2059
+ }
2060
+ .dsh-split-pane {
2061
+ flex: 1;
2062
+ overflow-y: auto;
2063
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2064
+ border-radius: 8px;
2065
+ padding: 12px;
2066
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
2067
+ }
2068
+ body[data-ds-dark-theme] .dsh-split-pane {
2069
+ border-color: var(--dsw-alias-border-l2, #252d3d);
2070
+ background: var(--dsw-alias-bg-layer-1, #0d1017);
2071
+ }
2072
+ .dsh-split-pane-header {
2073
+ font-size: 11px;
2074
+ font-weight: 600;
2075
+ color: var(--dsw-alias-label-secondary, #64748b);
2076
+ margin-bottom: 8px;
2077
+ padding-bottom: 4px;
2078
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2079
+ }
2080
+ body[data-ds-dark-theme] .dsh-split-pane-header {
2081
+ color: #94a3b8;
2082
+ border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
2083
+ }
2084
+
2085
+ /* Loading & Error States */
2086
+ .dsh-preview-loading {
2087
+ display: flex;
2088
+ flex-direction: column;
2089
+ align-items: center;
2090
+ justify-content: center;
2091
+ padding: 60px 0;
2092
+ gap: 14px;
2093
+ color: var(--dsw-alias-label-secondary, #64748b);
2094
+ }
2095
+ .dsh-preview-spinner {
2096
+ width: 32px;
2097
+ height: 32px;
2098
+ border: 3px solid rgba(65, 118, 230, 0.2);
2099
+ border-top-color: var(--dsw-static-deepseek-500, #4176e6);
2100
+ border-radius: 50%;
2101
+ animation: dshSpinner 0.8s linear infinite;
2102
+ }
2103
+ @keyframes dshSpinner {
2104
+ to { transform: rotate(360deg); }
2105
+ }
2106
+
2107
+ .dsh-preview-error-card {
2108
+ padding: 32px 24px;
2109
+ background: rgba(239, 68, 68, 0.06);
2110
+ border: 1px solid rgba(239, 68, 68, 0.25);
2111
+ border-radius: 8px;
2112
+ text-align: center;
2113
+ margin: 30px auto;
2114
+ max-width: 480px;
2115
+ }
2116
+ .dsh-error-icon { font-size: 32px; margin-bottom: 10px; }
2117
+ .dsh-error-title { font-size: 16px; font-weight: 600; color: #ef4444; margin-bottom: 6px; }
2118
+ .dsh-error-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #64748b); margin-bottom: 16px; }
2119
+ body[data-ds-dark-theme] .dsh-error-desc { color: #cbd5e1; }
2120
+ .dsh-error-actions { display: flex; justify-content: center; gap: 10px; }
2121
+ .dsh-btn-retry {
2122
+ background: #ef4444; color: #fff; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px;
2123
+ }
2124
+ .dsh-btn-native {
2125
+ background: var(--dsw-alias-bg-layer-2, #f1f5f9); color: var(--dsw-alias-label-primary, #0f172a); border: 1px solid #cbd5e1; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px;
2126
+ }
2127
+ body[data-ds-dark-theme] .dsh-btn-native {
2128
+ background: var(--dsw-alias-bg-layer-2, #1e2433); color: #e2e8f0; border-color: #334155;
2129
+ }
2130
+
2131
+ /* Toast Popup */
2132
+ .dsh-preview-toast-container {
2133
+ position: fixed;
2134
+ bottom: 24px;
2135
+ left: 50%;
2136
+ transform: translateX(-50%);
2137
+ z-index: 10000;
2138
+ display: flex;
2139
+ flex-direction: column;
2140
+ gap: 8px;
2141
+ pointer-events: none;
2142
+ }
2143
+ .dsh-preview-toast {
2144
+ display: flex;
2145
+ align-items: center;
2146
+ gap: 8px;
2147
+ padding: 8px 16px;
2148
+ background: #1e293b;
2149
+ color: #f8fafc;
2150
+ border: 1px solid #334155;
2151
+ border-radius: 20px;
2152
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
2153
+ font-size: 12px;
2154
+ opacity: 0;
2155
+ transform: translateY(10px);
2156
+ transition: all 0.25s ease;
2157
+ }
2158
+ .dsh-preview-toast.dsh-preview-toast-show {
2159
+ opacity: 1;
2160
+ transform: translateY(0);
2161
+ }
2162
+ .dsh-preview-toast-success { border-color: #22c55e; }
2163
+ .dsh-preview-toast-success .dsh-preview-toast-icon { color: #22c55e; font-weight: bold; }
2164
+ .dsh-preview-toast-error { border-color: #ef4444; }
2165
+ .dsh-preview-toast-error .dsh-preview-toast-icon { color: #ef4444; font-weight: bold; }
2166
+ `;
2167
+ document.head.appendChild(style);
2168
+ }
2169
+
2170
+ // =========================================================================
2171
+ // Cordis Client Plugin Apply & Slot Injection
2172
+ // =========================================================================
2173
+ exports.inject = ['slots', 'locale', 'sessions', 'workspaces', 'connection', 'remote'];
2174
+
2175
+ exports.apply = function apply(ctx) {
2176
+ clientCtx = ctx;
2177
+ if (typeof window !== 'undefined') {
2178
+ window.__dsh_preview_ctx = ctx;
2179
+ window.__dsh_open_preview = openPreviewFile;
2180
+ }
2181
+
2182
+ injectStyles();
2183
+ setupFileInterceptors(ctx);
2184
+
2185
+ console.log('[Preview] Markdown Preview Client Plugin loaded successfully!');
2186
+
2187
+ // Mount into shell.overlay slot so it renders seamlessly across the app
2188
+ ctx.slots.inject(
2189
+ 'shell.overlay',
2190
+ () => ctx.slots.register({
2191
+ name: 'shell.overlay',
2192
+ id: 'preview-drawer',
2193
+ order: 20, // Render on top of normal overlays
2194
+ }, (props) => h(PreviewDrawerRoot, { ...props, ctx }))
2195
+ );
2196
+ };
2197
+
2198
+ return module.exports;
2199
+ }
2200
+ });