@rooode/dsh-plugin-preview 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/lib/client.js +3277 -1241
  2. package/lib/index.js +172 -1
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -23,8 +23,13 @@
23
23
  // =========================================================================
24
24
  const STORAGE_KEY_TABS = 'dsh:preview:tabs:v1';
25
25
  const STORAGE_KEY_WIDTH = 'dsh:preview:panel_width:v1';
26
- const DEFAULT_PANEL_WIDTH = 620;
26
+ const STORAGE_KEY_TREE_OPEN = 'dsh:preview:tree:open:v1';
27
+ const STORAGE_KEY_TREE_WIDTH = 'dsh:preview:tree:width:v1';
28
+ const DEFAULT_PANEL_WIDTH = 760;
27
29
  const MIN_PANEL_WIDTH = 380;
30
+ const DEFAULT_TREE_WIDTH = 240;
31
+ const MIN_TREE_WIDTH = 180;
32
+ const MAX_TREE_WIDTH = 450;
28
33
  const SUPPORTED_EXTENSIONS = new Set([
29
34
  '.md', '.markdown', '.mdown', '.mkdn', '.mdwn',
30
35
  '.txt', '.log', '.json', '.yaml', '.yml',
@@ -39,7 +44,9 @@
39
44
  let globalTabs = [];
40
45
  let globalActiveTabId = null;
41
46
  let globalIsPanelOpen = false;
47
+ let globalIsTreeOpen = true;
42
48
  let globalPanelWidth = DEFAULT_PANEL_WIDTH;
49
+ let globalTreeWidth = DEFAULT_TREE_WIDTH;
43
50
  let globalLastOpenTime = 0;
44
51
  const stateListeners = new Set();
45
52
 
@@ -53,6 +60,23 @@
53
60
  }
54
61
  } catch (e) {}
55
62
 
63
+ try {
64
+ const savedTreeOpen = localStorage.getItem(STORAGE_KEY_TREE_OPEN);
65
+ if (savedTreeOpen !== null) {
66
+ globalIsTreeOpen = savedTreeOpen === 'true';
67
+ }
68
+ } catch (e) {}
69
+
70
+ try {
71
+ const savedTreeWidth = localStorage.getItem(STORAGE_KEY_TREE_WIDTH);
72
+ if (savedTreeWidth) {
73
+ const parsed = parseInt(savedTreeWidth, 10);
74
+ if (!isNaN(parsed) && parsed >= MIN_TREE_WIDTH) {
75
+ globalTreeWidth = Math.min(parsed, MAX_TREE_WIDTH);
76
+ }
77
+ }
78
+ } catch (e) {}
79
+
56
80
  try {
57
81
  const savedTabs = localStorage.getItem(STORAGE_KEY_TABS);
58
82
  if (savedTabs) {
@@ -84,6 +108,22 @@
84
108
  notifyStateChange();
85
109
  }
86
110
 
111
+ function setIsTreeOpen(open) {
112
+ globalIsTreeOpen = open;
113
+ try {
114
+ localStorage.setItem(STORAGE_KEY_TREE_OPEN, String(open));
115
+ } catch (e) {}
116
+ notifyStateChange();
117
+ }
118
+
119
+ function setTreeWidth(width) {
120
+ globalTreeWidth = Math.max(MIN_TREE_WIDTH, Math.min(width, MAX_TREE_WIDTH));
121
+ try {
122
+ localStorage.setItem(STORAGE_KEY_TREE_WIDTH, String(globalTreeWidth));
123
+ } catch (e) {}
124
+ notifyStateChange();
125
+ }
126
+
87
127
  function setPanelOpen(open) {
88
128
  globalIsPanelOpen = open;
89
129
  if (open) {
@@ -112,6 +152,85 @@
112
152
  return SUPPORTED_EXTENSIONS.has(ext);
113
153
  }
114
154
 
155
+ function formatBytes(bytes) {
156
+ if (!bytes || bytes === 0) return '0 B';
157
+ const k = 1024;
158
+ const sizes = ['B', 'KB', 'MB', 'GB'];
159
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
160
+ return (bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 1) + ' ' + sizes[i];
161
+ }
162
+
163
+ function getFileIcon(node) {
164
+ if (node.isDir) {
165
+ return { icon: '📁', color: '#eab308' };
166
+ }
167
+ const ext = (node.extension || getFileExtension(node.name || node.path || '')).toLowerCase();
168
+ if (ext === '.md' || ext === '.markdown') return { icon: '📝', color: '#0284c7' };
169
+ if (ext === '.json') return { icon: '{ }', color: '#10b981' };
170
+ if (ext === '.ts' || ext === '.tsx') return { icon: 'TS', color: '#3b82f6' };
171
+ if (ext === '.js' || ext === '.jsx' || ext === '.mjs') return { icon: 'JS', color: '#eab308' };
172
+ if (ext === '.yaml' || ext === '.yml' || ext === '.toml' || ext === '.ini') return { icon: '⚙️', color: '#f97316' };
173
+ if (ext === '.html' || ext === '.htm') return { icon: '🌐', color: '#ef4444' };
174
+ if (ext === '.css' || ext === '.scss' || ext === '.less') return { icon: '#', color: '#06b6d4' };
175
+ if (ext === '.py') return { icon: '🐍', color: '#38bdf8' };
176
+ if (ext === '.sh' || ext === '.bash' || ext === '.ps1') return { icon: '⌨️', color: '#8b5cf6' };
177
+ if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) return { icon: '🖼️', color: '#ec4899' };
178
+ if (ext === '.log' || ext === '.txt') return { icon: '📄', color: '#64748b' };
179
+ return { icon: '📄', color: '#94a3b8' };
180
+ }
181
+
182
+
183
+ function getFileTypeCategory(filePath) {
184
+ if (!filePath) return 'text';
185
+ const ext = getFileExtension(filePath).toLowerCase();
186
+ if (['.md', '.markdown', '.mdown', '.mkdn', '.mdwn'].includes(ext)) return 'markdown';
187
+ if (['.json', '.jsonc', '.json5', '.geojson', '.lock'].includes(ext)) return 'json';
188
+ if (['.yaml', '.yml'].includes(ext)) return 'yaml';
189
+ if (['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'].includes(ext)) return 'javascript';
190
+ if (['.py', '.pyw'].includes(ext)) return 'python';
191
+ if (['.html', '.htm', '.svg', '.xml'].includes(ext)) return 'markup';
192
+ if (['.css', '.scss', '.less', '.sass'].includes(ext)) return 'css';
193
+ if (['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd'].includes(ext)) return 'shell';
194
+ if (['.sql'].includes(ext)) return 'sql';
195
+ if (['.toml', '.ini', '.env', '.properties', '.conf', '.cfg', '.gitignore', '.gitattributes', '.editorconfig'].includes(ext)) return 'config';
196
+ return 'text';
197
+ }
198
+
199
+ function getDefaultViewMode(category) {
200
+ if (category === 'markdown') return 'preview';
201
+ if (category === 'json') return 'json-tree';
202
+ if (category === 'yaml') return 'code';
203
+ return 'code';
204
+ }
205
+
206
+ function getCurrentWorkspaceRoot() {
207
+ if (clientCtx && clientCtx.workspaces) {
208
+ try {
209
+ if (clientCtx.workspaces.current?.directory) {
210
+ return clientCtx.workspaces.current.directory;
211
+ }
212
+ const snapshot = clientCtx.workspaces.list?.getSnapshot();
213
+ if (snapshot?.items?.length > 0) {
214
+ return snapshot.items[0].directory || snapshot.items[0].path || '';
215
+ }
216
+ } catch (e) {}
217
+ }
218
+ if (clientCtx && clientCtx.sessions) {
219
+ try {
220
+ const snapshot = clientCtx.sessions.list?.getSnapshot();
221
+ const currentSessionId = snapshot?.current;
222
+ if (currentSessionId && snapshot?.byId?.[currentSessionId]?.cwd) {
223
+ return snapshot.byId[currentSessionId].cwd;
224
+ }
225
+ } catch (e) {}
226
+ }
227
+ if (typeof location !== 'undefined' && location.hash) {
228
+ const m = location.hash.match(/workspace=([^&]+)/);
229
+ if (m) return decodeURIComponent(m[1]);
230
+ }
231
+ return '';
232
+ }
233
+
115
234
  function resolveAbsoluteFilePath(targetPath) {
116
235
  if (!targetPath) return '';
117
236
  let cleanPath = targetPath.trim();
@@ -126,24 +245,7 @@
126
245
  if (isWinAbs || isPosixAbs) {
127
246
  return cleanPath;
128
247
  }
129
- let cwd = '';
130
- if (clientCtx && clientCtx.sessions) {
131
- try {
132
- const snapshot = clientCtx.sessions.list.getSnapshot();
133
- const currentSessionId = snapshot.current;
134
- if (currentSessionId && snapshot.byId[currentSessionId]) {
135
- cwd = snapshot.byId[currentSessionId].cwd || '';
136
- }
137
- } catch (e) {}
138
- }
139
- if (!cwd && clientCtx && clientCtx.workspaces) {
140
- try {
141
- const wsList = clientCtx.workspaces.list.getSnapshot();
142
- if (wsList.items && wsList.items.length > 0) {
143
- cwd = wsList.items[0].directory || '';
144
- }
145
- } catch (e) {}
146
- }
248
+ const cwd = getCurrentWorkspaceRoot();
147
249
  if (cwd) {
148
250
  const isWinCwd = /^[a-zA-Z]:[\\/]/.test(cwd);
149
251
  const sep = isWinCwd ? '\\' : '/';
@@ -170,12 +272,15 @@
170
272
  globalActiveTabId = globalTabs[existingIndex].id;
171
273
  globalTabs[existingIndex].lastActiveAt = Date.now();
172
274
  } else {
275
+ const category = getFileTypeCategory(fullPath);
276
+ const defaultMode = getDefaultViewMode(category);
173
277
  const newTab = {
174
278
  id: tabId,
175
279
  filePath: fullPath,
176
280
  title: fileName,
177
281
  extension: ext,
178
- viewMode: options.viewMode || 'preview',
282
+ category: category,
283
+ viewMode: options.viewMode || defaultMode,
179
284
  pinned: false,
180
285
  createdAt: Date.now(),
181
286
  lastActiveAt: Date.now(),
@@ -201,7 +306,6 @@
201
306
  globalActiveTabId = nextTabs[nextIdx].id;
202
307
  } else {
203
308
  globalActiveTabId = null;
204
- globalIsPanelOpen = false;
205
309
  }
206
310
  }
207
311
  saveTabsToStorage();
@@ -211,7 +315,6 @@
211
315
  function closeAllTabs() {
212
316
  globalTabs = [];
213
317
  globalActiveTabId = null;
214
- globalIsPanelOpen = false;
215
318
  saveTabsToStorage();
216
319
  notifyStateChange();
217
320
  }
@@ -687,506 +790,1759 @@
687
790
  }
688
791
 
689
792
  // =========================================================================
690
- // React Components: MarkdownPreviewView
793
+ // React Components: Workspace File Tree Explorer
691
794
  // =========================================================================
692
- function MarkdownPreviewView({ tab, fileInfo, isLoading, error, onRefresh, onOpenNative, onReveal }) {
693
- const [viewMode, setViewMode] = useState(tab.viewMode || 'preview');
694
- const [isTocOpen, setIsTocOpen] = useState(false);
695
- const [searchQuery, setSearchQuery] = useState('');
696
- const [isSearchOpen, setIsSearchOpen] = useState(false);
697
- const contentRef = useRef(null);
698
- const sourceRef = useRef(null);
795
+ function FileTreeNodeItem({ node, depth = 0, expandedPaths, onToggleExpand, activeFilePath, onOpenFile, searchQuery }) {
796
+ const isDir = node.isDir;
797
+ const isExpanded = expandedPaths.has(node.path);
798
+ const isActive = !isDir && activeFilePath && activeFilePath.toLowerCase() === node.path.toLowerCase();
799
+ const iconInfo = getFileIcon(node);
800
+
801
+ const indentStyle = { paddingLeft: `${depth * 14 + 6}px` };
802
+
803
+ const handleClick = (e) => {
804
+ e.stopPropagation();
805
+ if (isDir) {
806
+ onToggleExpand(node.path);
807
+ } else {
808
+ onOpenFile(node.path);
809
+ }
810
+ };
699
811
 
700
- const content = fileInfo?.content || '';
701
- const tocHeadings = useMemo(() => extractTocHeadings(content), [content]);
812
+ return h(Fragment, null,
813
+ h('div', {
814
+ className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''}`,
815
+ style: indentStyle,
816
+ onClick: handleClick,
817
+ title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
818
+ },
819
+ // Chevron indicator for directory
820
+ isDir ? h('span', {
821
+ className: `dsh-tree-chevron ${isExpanded ? 'expanded' : ''}`,
822
+ }, isExpanded ? '▼' : '▶') : h('span', { className: 'dsh-tree-chevron-placeholder' }),
823
+
824
+ // File / Directory Icon
825
+ h('span', {
826
+ className: 'dsh-tree-icon',
827
+ style: { color: isDir ? (isExpanded ? '#ca8a04' : '#eab308') : iconInfo.color },
828
+ }, isDir ? (isExpanded ? '📂' : '📁') : iconInfo.icon),
829
+
830
+ // File / Directory Name
831
+ h('span', { className: 'dsh-tree-name' }, node.name),
832
+
833
+ // Size / Count badge
834
+ isDir && node.childCount !== undefined ? h('span', { className: 'dsh-tree-badge' }, node.childCount) : (
835
+ !isDir && node.size ? h('span', { className: 'dsh-tree-size-badge' }, formatBytes(node.size)) : null
836
+ )
837
+ ),
702
838
 
703
- const renderedHtml = useMemo(() => {
704
- if (!content) return '';
705
- let html = renderMarkdownToHtml(content);
706
- if (searchQuery.trim()) {
707
- try {
708
- const q = escapeHtml(searchQuery.trim());
709
- const regex = new RegExp(`(${q})`, 'gi');
710
- html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
711
- } catch (e) {}
839
+ // Children if expanded
840
+ isDir && isExpanded && node.children && node.children.length > 0 ? (
841
+ node.children.map(child => h(FileTreeNodeItem, {
842
+ key: child.path,
843
+ node: child,
844
+ depth: depth + 1,
845
+ expandedPaths,
846
+ onToggleExpand,
847
+ activeFilePath,
848
+ onOpenFile,
849
+ searchQuery,
850
+ }))
851
+ ) : null
852
+ );
853
+ }
854
+
855
+ function filterTreeNode(node, query) {
856
+ if (!query) return true;
857
+ const q = query.toLowerCase();
858
+ if (node.name.toLowerCase().includes(q)) return true;
859
+ if (node.isDir && node.children) {
860
+ return node.children.some(child => filterTreeNode(child, query));
861
+ }
862
+ return false;
863
+ }
864
+
865
+ function filterTree(tree, query) {
866
+ if (!query) return tree;
867
+ return tree.filter(node => filterTreeNode(node, query)).map(node => {
868
+ if (!node.isDir) return node;
869
+ return {
870
+ ...node,
871
+ children: filterTree(node.children || [], query),
872
+ };
873
+ });
874
+ }
875
+
876
+ function collectAllDirPaths(tree) {
877
+ const paths = [];
878
+ const traverse = (nodes) => {
879
+ for (const n of nodes) {
880
+ if (n.isDir) {
881
+ paths.push(n.path);
882
+ if (n.children) traverse(n.children);
883
+ }
712
884
  }
713
- return html;
714
- }, [content, searchQuery]);
885
+ };
886
+ traverse(tree);
887
+ return paths;
888
+ }
715
889
 
716
- const handleCopyAll = () => {
717
- if (!content) return;
718
- navigator.clipboard.writeText(content).then(() => {
719
- showToast('已复制文档全文到剪贴板', 'success');
720
- }).catch(() => {
721
- showToast('复制失败', 'error');
890
+ function WorkspaceTreeSidebar({
891
+ width,
892
+ activeFilePath,
893
+ onOpenFile,
894
+ onCloseSidebar,
895
+ }) {
896
+ const [treeData, setTreeData] = useState(null);
897
+ const [isLoading, setIsLoading] = useState(false);
898
+ const [error, setError] = useState(null);
899
+ const [searchQuery, setSearchQuery] = useState('');
900
+ const [expandedPaths, setExpandedPaths] = useState(new Set());
901
+ const currentWs = getCurrentWorkspaceRoot();
902
+
903
+ const loadTree = useCallback(async (force = false) => {
904
+ setIsLoading(true);
905
+ setError(null);
906
+ try {
907
+ const rootPath = getCurrentWorkspaceRoot();
908
+ const query = encodeURIComponent(rootPath);
909
+ const res = await fetch(`/api/preview/workspace-tree?path=${query}`);
910
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
911
+ const json = await res.json();
912
+ if (!json.ok) throw new Error(json?.error?.message || '读取工作区目录失败');
913
+ setTreeData(json);
914
+ // By default expand top 1 level directories
915
+ if (json.tree) {
916
+ const topDirs = json.tree.filter(n => n.isDir).map(n => n.path);
917
+ setExpandedPaths(new Set(topDirs));
918
+ }
919
+ } catch (err) {
920
+ setError(err.message || '加载工作空间文件树失败');
921
+ } finally {
922
+ setIsLoading(false);
923
+ }
924
+ }, []);
925
+
926
+ useEffect(() => {
927
+ loadTree();
928
+ }, [loadTree]);
929
+
930
+ const handleToggleExpand = (dirPath) => {
931
+ setExpandedPaths(prev => {
932
+ const next = new Set(prev);
933
+ if (next.has(dirPath)) next.delete(dirPath);
934
+ else next.add(dirPath);
935
+ return next;
722
936
  });
723
937
  };
724
938
 
725
- const handleTocClick = (headingId) => {
726
- if (!contentRef.current) return;
727
- const el = contentRef.current.querySelector(`#${headingId}`);
728
- if (el) {
729
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
939
+ const handleExpandAll = () => {
940
+ if (treeData?.tree) {
941
+ setExpandedPaths(new Set(collectAllDirPaths(treeData.tree)));
730
942
  }
731
943
  };
732
944
 
733
- const handleModeChange = (mode) => {
734
- setViewMode(mode);
735
- updateTabMode(tab.id, mode);
945
+ const handleCollapseAll = () => {
946
+ setExpandedPaths(new Set());
736
947
  };
737
948
 
738
- return h('div', { className: 'dsh-preview-content-container' },
739
- // Toolbar
740
- h('div', { className: 'dsh-preview-toolbar' },
741
- // Left: View mode toggles
742
- h('div', { className: 'dsh-preview-toolbar-left' },
743
- h('div', { className: 'dsh-preview-mode-group' },
744
- h('button', {
745
- type: 'button',
746
- className: `dsh-preview-mode-btn ${viewMode === 'preview' ? 'active' : ''}`,
747
- onClick: () => handleModeChange('preview'),
748
- title: '渲染预览模式 (Markdown Render)',
749
- }, '📖 预览'),
750
- h('button', {
751
- type: 'button',
752
- className: `dsh-preview-mode-btn ${viewMode === 'source' ? 'active' : ''}`,
753
- onClick: () => handleModeChange('source'),
754
- title: '源码模式 (Raw Source)',
755
- }, '📝 源码'),
756
- h('button', {
757
- type: 'button',
758
- className: `dsh-preview-mode-btn ${viewMode === 'split' ? 'active' : ''}`,
759
- onClick: () => handleModeChange('split'),
760
- title: '分栏对比模式 (Split View)',
761
- }, '🌓 分栏')
762
- ),
763
-
764
- tocHeadings.length > 0 ? h('button', {
765
- type: 'button',
766
- className: `dsh-preview-tool-btn ${isTocOpen ? 'active' : ''}`,
767
- onClick: () => setIsTocOpen(!isTocOpen),
768
- title: isTocOpen ? '隐藏目录大纲' : '显示目录大纲 (TOC)',
769
- }, `📑 大纲 (${tocHeadings.length})`) : null,
949
+ const displayTree = useMemo(() => {
950
+ if (!treeData?.tree) return [];
951
+ return filterTree(treeData.tree, searchQuery.trim());
952
+ }, [treeData, searchQuery]);
770
953
 
771
- h('button', {
772
- type: 'button',
773
- className: `dsh-preview-tool-btn ${isSearchOpen ? 'active' : ''}`,
774
- onClick: () => setIsSearchOpen(!isSearchOpen),
775
- title: '在文档中查找',
776
- }, '🔍 查找')
954
+ // If searching, auto expand matching folders
955
+ useEffect(() => {
956
+ if (searchQuery.trim() && treeData?.tree) {
957
+ const allMatchingDirs = collectAllDirPaths(filterTree(treeData.tree, searchQuery.trim()));
958
+ setExpandedPaths(new Set(allMatchingDirs));
959
+ }
960
+ }, [searchQuery, treeData]);
961
+
962
+ return h('div', {
963
+ className: 'dsh-tree-sidebar',
964
+ style: { width: `${width}px` },
965
+ },
966
+ // Sidebar Header
967
+ h('div', { className: 'dsh-tree-sidebar-header' },
968
+ h('div', { className: 'dsh-tree-sidebar-title', title: treeData?.workspaceRoot || currentWs },
969
+ h('span', { className: 'dsh-tree-header-icon' }, '📁'),
970
+ h('span', { className: 'dsh-tree-header-text' }, treeData?.workspaceName || getFileName(currentWs) || '工作区')
777
971
  ),
778
-
779
- // Right: Action buttons
780
- h('div', { className: 'dsh-preview-toolbar-right' },
972
+ h('div', { className: 'dsh-tree-header-actions' },
781
973
  h('button', {
782
974
  type: 'button',
783
- className: 'dsh-preview-tool-btn',
784
- onClick: handleCopyAll,
785
- title: '一键复制全文',
786
- }, '📋 复制'),
787
-
975
+ className: 'dsh-tree-action-btn',
976
+ onClick: () => loadTree(true),
977
+ title: '刷新文件树',
978
+ }, '🔄'),
788
979
  h('button', {
789
980
  type: 'button',
790
- className: 'dsh-preview-tool-btn',
791
- onClick: onRefresh,
792
- title: '从磁盘重新加载最新内容',
793
- }, '🔄 刷新'),
794
-
981
+ className: 'dsh-tree-action-btn',
982
+ onClick: handleExpandAll,
983
+ title: '全部展开',
984
+ }, '📂'),
795
985
  h('button', {
796
986
  type: 'button',
797
- className: 'dsh-preview-tool-btn dsh-preview-tool-btn-accent',
798
- onClick: () => onOpenNative(tab.filePath),
799
- title: '在系统本地默认编辑器中打开 (记事本/VSCode)',
800
- }, '🖥️ 本地打开'),
801
-
802
- h('button', {
987
+ className: 'dsh-tree-action-btn',
988
+ onClick: handleCollapseAll,
989
+ title: '全部折叠',
990
+ }, '📁'),
991
+ onCloseSidebar ? h('button', {
803
992
  type: 'button',
804
- className: 'dsh-preview-tool-btn',
805
- onClick: () => onReveal(tab.filePath),
806
- title: '在文件管理器中定位',
807
- }, '📁 定位')
993
+ className: 'dsh-tree-action-btn',
994
+ onClick: onCloseSidebar,
995
+ title: '收起目录树 (快捷键 Ctrl/Cmd+B)',
996
+ }, '') : null
808
997
  )
809
998
  ),
810
999
 
811
- // Search Bar (if open)
812
- isSearchOpen ? h('div', { className: 'dsh-preview-search-bar' },
813
- h('span', { className: 'dsh-search-icon' }, '🔍'),
814
- h('input', {
815
- type: 'text',
816
- placeholder: '输入关键词在文档中搜索...',
817
- value: searchQuery,
818
- onChange: e => setSearchQuery(e.target.value),
819
- className: 'dsh-preview-search-input',
820
- autoFocus: true,
821
- }),
822
- searchQuery ? h('button', {
823
- type: 'button',
824
- className: 'dsh-search-clear-btn',
825
- onClick: () => setSearchQuery(''),
826
- }, '✕') : null
827
- ) : null,
1000
+ // Search Input
1001
+ h('div', { className: 'dsh-tree-sidebar-search' },
1002
+ h('div', { className: 'dsh-tree-search-wrapper' },
1003
+ h('input', {
1004
+ type: 'text',
1005
+ className: 'dsh-tree-search-input',
1006
+ placeholder: '🔍 过滤文件 (如 .md, ts)...',
1007
+ value: searchQuery,
1008
+ onChange: (e) => setSearchQuery(e.target.value),
1009
+ }),
1010
+ searchQuery ? h('button', {
1011
+ type: 'button',
1012
+ className: 'dsh-tree-search-clear',
1013
+ onClick: () => setSearchQuery(''),
1014
+ title: '清空过滤条件',
1015
+ }, '✕') : null
1016
+ )
1017
+ ),
828
1018
 
829
- // Body area with optional TOC sidebar + Main content
830
- h('div', { className: 'dsh-preview-body-layout' },
831
- // TOC Sidebar
832
- isTocOpen && tocHeadings.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
833
- h('div', { className: 'dsh-preview-toc-header' },
834
- h('span', { style: { fontWeight: 600, fontSize: 12 } }, '目录大纲 (TOC)'),
1019
+ // Tree Content
1020
+ h('div', { className: 'dsh-tree-node-list' },
1021
+ isLoading ? (
1022
+ h('div', { className: 'dsh-tree-loading' }, '正在扫描工作区文件...')
1023
+ ) : error ? (
1024
+ h('div', { className: 'dsh-tree-error' },
1025
+ h('p', null, `⚠️ ${error}`),
835
1026
  h('button', {
836
1027
  type: 'button',
837
- className: 'dsh-toc-close-btn',
838
- onClick: () => setIsTocOpen(false),
839
- }, '')
840
- ),
841
- h('div', { className: 'dsh-preview-toc-list' },
842
- tocHeadings.map((hItem, idx) =>
843
- h('div', {
844
- key: idx,
845
- className: `dsh-toc-item dsh-toc-level-${hItem.level}`,
846
- onClick: () => handleTocClick(hItem.id),
847
- title: hItem.text,
848
- },
849
- h('span', { className: 'dsh-toc-bullet' }, '•'),
850
- h('span', { className: 'dsh-toc-text' }, hItem.text)
851
- )
852
- )
853
- )
854
- ) : null,
855
-
856
- // Main Viewer
857
- h('div', { className: 'dsh-preview-main-scroll' },
858
- isLoading ? h('div', { className: 'dsh-preview-loading' },
859
- h('div', { className: 'dsh-preview-spinner' }),
860
- h('span', null, '正在加载文档内容...')
861
- ) : (error || fileInfo?.error) ? h('div', { className: 'dsh-preview-error-card' },
862
- h('div', { className: 'dsh-error-icon' }, '⚠️'),
863
- h('div', { className: 'dsh-error-title' }, '读取文档失败'),
864
- h('div', { className: 'dsh-error-desc' }, error || fileInfo?.error),
865
- h('div', { className: 'dsh-error-actions' },
866
- h('button', { type: 'button', className: 'dsh-btn-retry', onClick: onRefresh }, '重试'),
867
- h('button', { type: 'button', className: 'dsh-btn-native', onClick: () => onOpenNative(tab.filePath) }, '在外部打开')
868
- )
869
- ) : viewMode === 'preview' ? (
870
- // 1. Preview Mode
871
- h('div', {
872
- ref: contentRef,
873
- className: 'dsh-markdown-body',
874
- dangerouslySetInnerHTML: { __html: renderedHtml },
875
- })
876
- ) : viewMode === 'source' ? (
877
- // 2. Source Code Mode
878
- h('div', { className: 'dsh-source-view' },
879
- h('div', { className: 'dsh-source-line-numbers' },
880
- (content.split('\n') || []).map((_, i) => h('div', { key: i }, i + 1))
881
- ),
882
- h('pre', { ref: sourceRef, className: 'dsh-source-pre' },
883
- h('code', null, content)
884
- )
885
- )
886
- ) : (
887
- // 3. Split Mode
888
- h('div', { className: 'dsh-split-view' },
889
- h('div', { className: 'dsh-split-pane dsh-split-source' },
890
- h('div', { className: 'dsh-split-pane-header' }, '📝 原始源码'),
891
- h('div', { className: 'dsh-source-view' },
892
- h('div', { className: 'dsh-source-line-numbers' },
893
- (content.split('\n') || []).map((_, i) => h('div', { key: i }, i + 1))
894
- ),
895
- h('pre', { className: 'dsh-source-pre' },
896
- h('code', null, content)
897
- )
898
- )
899
- ),
900
- h('div', { className: 'dsh-split-pane dsh-split-render' },
901
- h('div', { className: 'dsh-split-pane-header' }, '📖 渲染预览'),
902
- h('div', {
903
- ref: contentRef,
904
- className: 'dsh-markdown-body',
905
- dangerouslySetInnerHTML: { __html: renderedHtml },
906
- })
907
- )
908
- )
1028
+ className: 'dsh-tree-retry-btn',
1029
+ onClick: () => loadTree(true),
1030
+ }, '重试')
909
1031
  )
1032
+ ) : displayTree.length === 0 ? (
1033
+ h('div', { className: 'dsh-tree-empty' }, searchQuery ? '未匹配到相关文件' : '工作区暂无文件')
1034
+ ) : (
1035
+ displayTree.map(node => h(FileTreeNodeItem, {
1036
+ key: node.path,
1037
+ node,
1038
+ depth: 0,
1039
+ expandedPaths,
1040
+ onToggleExpand: handleToggleExpand,
1041
+ activeFilePath,
1042
+ onOpenFile,
1043
+ searchQuery,
1044
+ }))
910
1045
  )
911
1046
  ),
912
1047
 
913
- // Status Footer
914
- h('div', { className: 'dsh-preview-footer' },
915
- h('div', { className: 'dsh-footer-left', title: fileInfo?.path || tab.filePath },
916
- h('span', { className: 'dsh-footer-ext' }, (fileInfo?.extension || tab.extension || '.md').toUpperCase().replace('.', '')),
917
- h('span', { className: 'dsh-footer-path' }, fileInfo?.path || tab.filePath)
1048
+ // Sidebar Footer with file count stats
1049
+ treeData ? h('div', { className: 'dsh-tree-sidebar-footer' },
1050
+ h('span', null, `${treeData.totalDirs || 0} 目录`),
1051
+ h('span', { className: 'dsh-tree-footer-dot' }, ''),
1052
+ h('span', null, `${treeData.totalFiles || 0} 文件`)
1053
+ ) : null
1054
+ );
1055
+ }
1056
+
1057
+ function WorkspaceWelcomeView({ workspaceRoot, workspaceName, onOpenFile, onOpenTree }) {
1058
+ return h('div', { className: 'dsh-preview-welcome-container' },
1059
+ h('div', { className: 'dsh-preview-welcome-card' },
1060
+ h('div', { className: 'dsh-welcome-icon' }, '🗂️'),
1061
+ h('h3', { className: 'dsh-welcome-title' }, `工作空间: ${workspaceName || '当前项目'}`),
1062
+ h('p', { className: 'dsh-welcome-path' }, workspaceRoot || '本地项目目录'),
1063
+ h('p', { className: 'dsh-welcome-desc' },
1064
+ '在左侧目录树中选择并点击任意文件(Markdown 文档、代码文件、配置文件等),即可在此多标签预览、阅读与对照。'
918
1065
  ),
919
- h('div', { className: 'dsh-footer-right' },
920
- fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, `${(fileInfo.size / 1024).toFixed(1)} KB`) : null,
921
- fileInfo?.lineCount ? h('span', { className: 'dsh-footer-stat' }, `${fileInfo.lineCount} 行`) : null,
922
- fileInfo?.wordCount ? h('span', { className: 'dsh-footer-stat' }, `约 ${fileInfo.wordCount} 字`) : null
1066
+ h('div', { className: 'dsh-welcome-actions' },
1067
+ h('button', {
1068
+ type: 'button',
1069
+ className: 'dsh-welcome-btn primary',
1070
+ onClick: onOpenTree,
1071
+ }, '📁 浏览工作区文件树'),
1072
+ h('button', {
1073
+ type: 'button',
1074
+ className: 'dsh-welcome-btn',
1075
+ onClick: () => triggerRevealInExplorer(workspaceRoot || '.'),
1076
+ }, '🖥️ 在资源管理器中查看')
923
1077
  )
924
1078
  )
925
1079
  );
926
1080
  }
927
1081
 
928
1082
  // =========================================================================
929
- // React Components: Right Side Drawer / Shell Overlay Container
1083
+
930
1084
  // =========================================================================
931
- function PreviewDrawerRoot() {
932
- const [tabs, setTabs] = useState(globalTabs);
933
- const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
934
- const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
935
- const [width, setWidth] = useState(globalPanelWidth);
936
- const [fileData, setFileData] = useState({});
937
- const [loadingMap, setLoadingMap] = useState({});
938
- const [errorMap, setErrorMap] = useState({});
939
- const [isDragging, setIsDragging] = useState(false);
940
- const dragStartX = useRef(0);
941
- const dragStartWidth = useRef(0);
1085
+ // Multi-Language Syntax Highlighting & Tokenizers (Single-Pass Safe)
1086
+ // =========================================================================
1087
+ function highlightJSON(jsonStr) {
1088
+ if (!jsonStr) return '';
1089
+ const regex = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?|[{}\[\],:])/g;
1090
+ let lastIndex = 0;
1091
+ let out = '';
1092
+ let match;
1093
+
1094
+ while ((match = regex.exec(jsonStr)) !== null) {
1095
+ const raw = match[0];
1096
+ const index = match.index;
1097
+ if (index > lastIndex) {
1098
+ out += escapeHtml(jsonStr.slice(lastIndex, index));
1099
+ }
1100
+ lastIndex = regex.lastIndex;
1101
+
1102
+ if (raw.startsWith('"')) {
1103
+ if (raw.endsWith(':')) {
1104
+ const keyText = raw.slice(0, -1).trim();
1105
+ out += '<span class="tok-key">' + escapeHtml(keyText) + '</span>:';
1106
+ } else {
1107
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1108
+ }
1109
+ } else if (raw === 'true' || raw === 'false') {
1110
+ out += '<span class="tok-boolean">' + raw + '</span>';
1111
+ } else if (raw === 'null') {
1112
+ out += '<span class="tok-null">' + raw + '</span>';
1113
+ } else if (/^-?\d/.test(raw)) {
1114
+ out += '<span class="tok-number">' + raw + '</span>';
1115
+ } else if (/[{}\[\]]/.test(raw)) {
1116
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1117
+ } else if (/[:,]/.test(raw)) {
1118
+ out += '<span class="tok-punctuation">' + raw + '</span>';
1119
+ } else {
1120
+ out += escapeHtml(raw);
1121
+ }
1122
+ }
942
1123
 
943
- // Subscribe to global store changes
944
- useEffect(() => {
945
- const update = () => {
946
- setTabs([...globalTabs]);
947
- setActiveTabId(globalActiveTabId);
948
- setIsOpen(globalIsPanelOpen);
949
- setWidth(globalPanelWidth);
950
- };
951
- stateListeners.add(update);
952
- return () => stateListeners.delete(update);
953
- }, []);
954
-
955
- const activeTab = useMemo(() => {
956
- return tabs.find(t => t.id === activeTabId) || tabs[0] || null;
957
- }, [tabs, activeTabId]);
1124
+ if (lastIndex < jsonStr.length) {
1125
+ out += escapeHtml(jsonStr.slice(lastIndex));
1126
+ }
1127
+ return out;
1128
+ }
958
1129
 
959
- // Load file content when active tab changes
960
- const loadTabFile = useCallback(async (tab, force = false) => {
961
- if (!tab || !tab.filePath) return;
962
- const key = tab.filePath.toLowerCase();
963
- setLoadingMap(m => ({ ...m, [key]: true }));
964
- setErrorMap(m => ({ ...m, [key]: null }));
965
- try {
966
- const fileInfo = await fetchFileContent(tab.filePath, force);
967
- if (fileInfo?.error) {
968
- setErrorMap(m => ({ ...m, [key]: fileInfo.error }));
1130
+ function highlightYAML(yamlStr) {
1131
+ if (!yamlStr) return '';
1132
+ const lines = yamlStr.split('\n');
1133
+ return lines.map(line => {
1134
+ const commentIdx = line.indexOf('#');
1135
+ let mainPart = line;
1136
+ let commentPart = '';
1137
+ if (commentIdx !== -1) {
1138
+ mainPart = line.slice(0, commentIdx);
1139
+ commentPart = '<span class="tok-comment">' + escapeHtml(line.slice(commentIdx)) + '</span>';
1140
+ }
1141
+
1142
+ let highlightedMain = '';
1143
+ const kvMatch = mainPart.match(/^(\s*-\s+)?([a-zA-Z0-9_\-./@$]+)(\s*:\s*)(.*)$/);
1144
+ if (kvMatch) {
1145
+ const prefix = kvMatch[1] ? '<span class="tok-operator">' + escapeHtml(kvMatch[1]) + '</span>' : '';
1146
+ const key = '<span class="tok-key">' + escapeHtml(kvMatch[2]) + '</span>';
1147
+ const colon = '<span class="tok-punctuation">' + escapeHtml(kvMatch[3]) + '</span>';
1148
+ let val = kvMatch[4];
1149
+ const trimmedVal = val.trim();
1150
+ let valHtml = escapeHtml(val);
1151
+ if (/^(true|false|yes|no|on|off)$/i.test(trimmedVal)) {
1152
+ valHtml = '<span class="tok-boolean">' + escapeHtml(val) + '</span>';
1153
+ } else if (/^(null|~)$/i.test(trimmedVal)) {
1154
+ valHtml = '<span class="tok-null">' + escapeHtml(val) + '</span>';
1155
+ } else if (/^-?\d+(\.\d+)?$/.test(trimmedVal)) {
1156
+ valHtml = '<span class="tok-number">' + escapeHtml(val) + '</span>';
1157
+ } else if (/^["'].*["']$/.test(trimmedVal)) {
1158
+ valHtml = '<span class="tok-string">' + escapeHtml(val) + '</span>';
1159
+ } else if (trimmedVal.startsWith('&') || trimmedVal.startsWith('*')) {
1160
+ valHtml = '<span class="tok-type">' + escapeHtml(val) + '</span>';
1161
+ }
1162
+ highlightedMain = prefix + key + colon + valHtml;
1163
+ } else {
1164
+ const bulletMatch = mainPart.match(/^(\s*-\s+)(.*)$/);
1165
+ if (bulletMatch) {
1166
+ highlightedMain = '<span class="tok-operator">' + escapeHtml(bulletMatch[1]) + '</span>' + escapeHtml(bulletMatch[2]);
969
1167
  } else {
970
- setFileData(d => ({ ...d, [key]: fileInfo }));
1168
+ highlightedMain = escapeHtml(mainPart);
971
1169
  }
972
- } catch (e) {
973
- setErrorMap(m => ({ ...m, [key]: e.message }));
974
- } finally {
975
- setLoadingMap(m => ({ ...m, [key]: false }));
976
1170
  }
977
- }, []);
1171
+ return highlightedMain + commentPart;
1172
+ }).join('\n');
1173
+ }
978
1174
 
979
- useEffect(() => {
980
- if (activeTab) {
981
- loadTabFile(activeTab);
1175
+ function highlightJS(code) {
1176
+ if (!code) return '';
1177
+ const keywords = new Set([
1178
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
1179
+ 'switch', 'case', 'break', 'continue', 'default', 'new', 'delete', 'typeof', 'instanceof',
1180
+ 'void', 'yield', 'await', 'async', 'try', 'catch', 'finally', 'throw', 'class',
1181
+ 'extends', 'super', 'this', 'import', 'export', 'from', 'as', 'type', 'interface',
1182
+ 'enum', 'implements', 'declare', 'namespace', 'public', 'private', 'protected',
1183
+ 'readonly', 'static', 'abstract', 'get', 'set', 'of', 'in', 'debugger', 'constructor'
1184
+ ]);
1185
+ const types = new Set([
1186
+ 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Map', 'Set', 'WeakMap',
1187
+ 'WeakSet', 'JSON', 'Math', 'RegExp', 'Date', 'Error', 'Symbol', 'React', 'ReactDOM',
1188
+ 'null', 'undefined', 'true', 'false', 'any', 'unknown', 'never', 'void', 'string',
1189
+ 'number', 'boolean', 'bigint', 'Record', 'Partial', 'Required', 'Pick', 'Omit'
1190
+ ]);
1191
+
1192
+ const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*$|`(?:\[\s\S]|[^`\])*`|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?n?)\b|\b[a-zA-Z_$][a-zA-Z0-9_$]*\b|=>|===|!==|<=|>=|&&|\|\||\?\?|\?\.|[+\-*/%=!&|^~<>?:;,{}()\[\].])/gm;
1193
+
1194
+ let lastIndex = 0;
1195
+ let out = '';
1196
+ let match;
1197
+
1198
+ while ((match = tokenRegex.exec(code)) !== null) {
1199
+ const raw = match[0];
1200
+ const index = match.index;
1201
+ if (index > lastIndex) {
1202
+ out += escapeHtml(code.slice(lastIndex, index));
1203
+ }
1204
+ lastIndex = tokenRegex.lastIndex;
1205
+
1206
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1207
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1208
+ } else if (raw.startsWith('`') || raw.startsWith('"') || raw.startsWith("'")) {
1209
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1210
+ } else if (/^\d|^0x/.test(raw)) {
1211
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1212
+ } else if (/^[a-zA-Z_$]/.test(raw)) {
1213
+ if (keywords.has(raw)) {
1214
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1215
+ } else if (types.has(raw)) {
1216
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1217
+ } else {
1218
+ const remainder = code.slice(lastIndex);
1219
+ if (/^\s*\(/.test(remainder)) {
1220
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1221
+ } else {
1222
+ out += escapeHtml(raw);
1223
+ }
1224
+ }
1225
+ } else if (/^(=>|===|!==|<=|>=|&&|\|\||\?\?|\?\.|[+\-*/%=!&|^~<>])$/.test(raw)) {
1226
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1227
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1228
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1229
+ } else if (/^[,;:?]$/.test(raw)) {
1230
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1231
+ } else {
1232
+ out += escapeHtml(raw);
982
1233
  }
983
- }, [activeTab, loadTabFile]);
1234
+ }
984
1235
 
985
- // Drag Resize Handlers
986
- const onResizePointerDown = useCallback((e) => {
987
- e.preventDefault();
988
- e.currentTarget.setPointerCapture(e.pointerId);
989
- dragStartX.current = e.clientX;
990
- dragStartWidth.current = globalPanelWidth;
991
- setIsDragging(true);
992
- }, []);
1236
+ if (lastIndex < code.length) {
1237
+ out += escapeHtml(code.slice(lastIndex));
1238
+ }
1239
+ return out;
1240
+ }
993
1241
 
994
- const onResizePointerMove = useCallback((e) => {
995
- if (!isDragging) return;
996
- const dx = dragStartX.current - e.clientX; // drag left increases width
997
- const newWidth = dragStartWidth.current + dx;
998
- setPanelWidth(newWidth);
999
- }, [isDragging]);
1242
+ function highlightPython(code) {
1243
+ if (!code) return '';
1244
+ const pyKeywords = new Set([
1245
+ 'def', 'class', 'import', 'from', 'return', 'if', 'elif', 'else', 'for', 'while',
1246
+ 'try', 'except', 'finally', 'raise', 'with', 'as', 'lambda', 'pass', 'yield',
1247
+ 'async', 'await', 'global', 'nonlocal', 'assert', 'del', 'in', 'is', 'not', 'and', 'or'
1248
+ ]);
1249
+ const pyBuiltins = new Set([
1250
+ 'True', 'False', 'None', 'self', 'cls', 'print', 'len', 'range', 'dict', 'list',
1251
+ 'set', 'tuple', 'str', 'int', 'float', 'bool', 'type', 'isinstance', 'enumerate', 'zip'
1252
+ ]);
1253
+
1254
+ const tokenRegex = /("""[\s\S]*?"""|'''[\s\S]*?'''|#.*$|f?"(?:\\[\s\S]|[^"\\])*"|f?'(?:\\[\s\S]|[^'\\])*'|\b\d+(?:\.\d+)?\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>?:;,{}()\[\].])/gm;
1255
+
1256
+ let lastIndex = 0;
1257
+ let out = '';
1258
+ let match;
1259
+
1260
+ while ((match = tokenRegex.exec(code)) !== null) {
1261
+ const raw = match[0];
1262
+ const index = match.index;
1263
+ if (index > lastIndex) {
1264
+ out += escapeHtml(code.slice(lastIndex, index));
1265
+ }
1266
+ lastIndex = tokenRegex.lastIndex;
1267
+
1268
+ if (raw.startsWith('#')) {
1269
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1270
+ } else if (raw.startsWith('"""') || raw.startsWith("'''") || raw.startsWith('"') || raw.startsWith("'") || raw.startsWith('f"') || raw.startsWith("f'")) {
1271
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1272
+ } else if (/^\d/.test(raw)) {
1273
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1274
+ } else if (/^[a-zA-Z_]/.test(raw)) {
1275
+ if (pyKeywords.has(raw)) {
1276
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1277
+ } else if (pyBuiltins.has(raw)) {
1278
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1279
+ } else {
1280
+ const remainder = code.slice(lastIndex);
1281
+ if (/^\s*\(/.test(remainder)) {
1282
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1283
+ } else {
1284
+ out += escapeHtml(raw);
1285
+ }
1286
+ }
1287
+ } else if (/^(==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>])$/.test(raw)) {
1288
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1289
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1290
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1291
+ } else if (/^[,;:?]$/.test(raw)) {
1292
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1293
+ } else {
1294
+ out += escapeHtml(raw);
1295
+ }
1296
+ }
1000
1297
 
1001
- const onResizePointerUp = useCallback((e) => {
1002
- if (isDragging) {
1003
- setIsDragging(false);
1004
- try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
1298
+ if (lastIndex < code.length) {
1299
+ out += escapeHtml(code.slice(lastIndex));
1300
+ }
1301
+ return out;
1302
+ }
1303
+
1304
+ function highlightHTML(code) {
1305
+ if (!code) return '';
1306
+ const tokenRegex = /(<!--[\s\S]*?-->|<\/?[a-zA-Z0-9\-:]+|(?:[a-zA-Z0-9\-:]+)=("[^"]*"|'[^']*')|\/?>)/g;
1307
+ let lastIndex = 0;
1308
+ let out = '';
1309
+ let match;
1310
+
1311
+ while ((match = tokenRegex.exec(code)) !== null) {
1312
+ const raw = match[0];
1313
+ const index = match.index;
1314
+ if (index > lastIndex) {
1315
+ out += escapeHtml(code.slice(lastIndex, index));
1316
+ }
1317
+ lastIndex = tokenRegex.lastIndex;
1318
+
1319
+ if (raw.startsWith('<!--')) {
1320
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1321
+ } else if (raw.startsWith('<')) {
1322
+ out += '<span class="tok-bracket">&lt;</span><span class="tok-tag">' + escapeHtml(raw.slice(raw.startsWith('</') ? 2 : 1)) + '</span>';
1323
+ } else if (raw.endsWith('>')) {
1324
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1325
+ } else if (raw.includes('=')) {
1326
+ const eqIdx = raw.indexOf('=');
1327
+ const attrName = raw.slice(0, eqIdx);
1328
+ const attrVal = raw.slice(eqIdx + 1);
1329
+ out += '<span class="tok-attr">' + escapeHtml(attrName) + '</span>=<span class="tok-string">' + escapeHtml(attrVal) + '</span>';
1330
+ } else {
1331
+ out += escapeHtml(raw);
1005
1332
  }
1006
- }, [isDragging]);
1333
+ }
1007
1334
 
1008
- const drawerRef = useRef(null);
1335
+ if (lastIndex < code.length) {
1336
+ out += escapeHtml(code.slice(lastIndex));
1337
+ }
1338
+ return out;
1339
+ }
1009
1340
 
1010
- // Outside Click Listener: Click on the chat conversation / dialog box outside the drawer to auto close/hide it
1011
- useEffect(() => {
1012
- if (!isOpen) return;
1341
+ function highlightGenericCode(code, category) {
1342
+ if (!code) return '';
1343
+ if (category === 'sql') {
1344
+ const sqlKeywords = /\b(SELECT|FROM|WHERE|INSERT|INTO|UPDATE|DELETE|JOIN|LEFT|RIGHT|INNER|OUTER|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|AS|ON|AND|OR|NOT|NULL|IS|CREATE|TABLE|DROP|ALTER|ADD|INDEX|PRIMARY|KEY)\b/gi;
1345
+ return escapeHtml(code)
1346
+ .replace(sqlKeywords, '<span class="tok-keyword">$1</span>')
1347
+ .replace(/(--.*$)/gm, '<span class="tok-comment">$1</span>')
1348
+ .replace(/('[^']*')/g, '<span class="tok-string">$1</span>');
1349
+ }
1350
+ const lines = code.split('\n');
1351
+ return lines.map(line => {
1352
+ const commentIdx = line.indexOf('#');
1353
+ let mainPart = line;
1354
+ let commentPart = '';
1355
+ if (commentIdx !== -1) {
1356
+ mainPart = line.slice(0, commentIdx);
1357
+ commentPart = '<span class="tok-comment">' + escapeHtml(line.slice(commentIdx)) + '</span>';
1358
+ }
1359
+ let h = escapeHtml(mainPart);
1360
+ h = h.replace(/^([a-zA-Z0-9_\-.]+)(\s*=\s*)(.*)$/, (m, k, eq, v) => {
1361
+ return '<span class="tok-key">' + k + '</span>' + eq + '<span class="tok-string">' + v + '</span>';
1362
+ });
1363
+ h = h.replace(/(\$[a-zA-Z0-9_{}]+)/g, '<span class="tok-type">$1</span>');
1364
+ h = h.replace(/\b(if|then|else|elif|fi|for|in|do|done|case|esac|echo|cd|export|npm|pnpm|node|git|curl)\b/g, '<span class="tok-keyword">$1</span>');
1365
+ return h + commentPart;
1366
+ }).join('\n');
1367
+ }
1013
1368
 
1014
- const handleOutsidePointerDown = (e) => {
1015
- // 1. Ignore if just opened within 250ms
1016
- if (Date.now() - globalLastOpenTime < 250) return;
1017
- // 2. Ignore during dragging resize handle
1018
- if (isDragging) return;
1369
+ function highlightCode(rawCode, category) {
1370
+ if (!rawCode) return '';
1371
+ if (category === 'json') return highlightJSON(rawCode);
1372
+ if (category === 'yaml') return highlightYAML(rawCode);
1373
+ if (category === 'javascript') return highlightJS(rawCode);
1374
+ if (category === 'python') return highlightPython(rawCode);
1375
+ if (category === 'markup') return highlightHTML(rawCode);
1376
+ if (category === 'css') return highlightCSS(rawCode);
1377
+ if (category === 'shell' || category === 'config' || category === 'sql') return highlightGenericCode(rawCode, category);
1378
+ return escapeHtml(rawCode);
1379
+ }
1019
1380
 
1020
- const target = e.target;
1021
- if (!target) return;
1381
+ // =========================================================================
1382
+ // Lightweight YAML to JSON Recursive Parser
1383
+ // =========================================================================
1384
+ function parseYamlScalar(val) {
1385
+ if (!val) return '';
1386
+ const v = val.trim();
1387
+ if (/^(true|yes|on)$/i.test(v)) return true;
1388
+ if (/^(false|no|off)$/i.test(v)) return false;
1389
+ if (/^(null|~)$/i.test(v)) return null;
1390
+ if (/^-?\d+$/.test(v)) return parseInt(v, 10);
1391
+ if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
1392
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
1393
+ return v.slice(1, -1);
1394
+ }
1395
+ return v;
1396
+ }
1022
1397
 
1023
- // 3. If clicking inside the preview drawer, do not close
1024
- const drawerEl = drawerRef.current || document.querySelector('.dsh-preview-drawer-container');
1025
- if (drawerEl && (drawerEl === target || drawerEl.contains(target))) {
1026
- return;
1398
+ function parseYamlToJson(yamlStr) {
1399
+ if (!yamlStr) return { ok: true, data: {} };
1400
+ try {
1401
+ const rawLines = yamlStr.split('\n');
1402
+ const lines = [];
1403
+ for (const raw of rawLines) {
1404
+ const cIdx = raw.indexOf('#');
1405
+ const l = cIdx !== -1 ? raw.slice(0, cIdx) : raw;
1406
+ if (l.trim()) {
1407
+ lines.push({ indent: l.search(/\S/), text: l.trim() });
1027
1408
  }
1409
+ }
1410
+ if (lines.length === 0) return { ok: true, data: {} };
1028
1411
 
1029
- // 4. If clicking on the floating pill, do not close
1030
- const floatPill = document.querySelector('.dsh-preview-float-pill');
1031
- if (floatPill && (floatPill === target || floatPill.contains(target))) {
1412
+ let idx = 0;
1413
+ function parseBlock(currentIndent) {
1414
+ let isArr = false;
1415
+ if (idx < lines.length && lines[idx].text.startsWith('- ')) {
1416
+ isArr = true;
1417
+ }
1418
+ const container = isArr ? [] : {};
1419
+
1420
+ while (idx < lines.length) {
1421
+ const line = lines[idx];
1422
+ if (line.indent < currentIndent) break;
1423
+ if (line.indent > currentIndent) break;
1424
+
1425
+ idx++;
1426
+
1427
+ if (isArr) {
1428
+ if (line.text.startsWith('- ')) {
1429
+ const itemText = line.text.slice(2).trim();
1430
+ if (!itemText) {
1431
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1432
+ container.push(parseBlock(lines[idx].indent));
1433
+ } else {
1434
+ container.push(null);
1435
+ }
1436
+ } else if (itemText.includes(':')) {
1437
+ const colonPos = itemText.indexOf(':');
1438
+ const k = itemText.slice(0, colonPos).trim().replace(/^['"]|['"]$/g, '');
1439
+ const v = itemText.slice(colonPos + 1).trim();
1440
+ const obj = {};
1441
+ if (!v) {
1442
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1443
+ obj[k] = parseBlock(lines[idx].indent);
1444
+ } else {
1445
+ obj[k] = {};
1446
+ }
1447
+ } else {
1448
+ obj[k] = parseYamlScalar(v);
1449
+ }
1450
+ while (idx < lines.length && lines[idx].indent === line.indent + 2 && !lines[idx].text.startsWith('- ')) {
1451
+ const nextLine = lines[idx++];
1452
+ const cPos = nextLine.text.indexOf(':');
1453
+ if (cPos !== -1) {
1454
+ const nk = nextLine.text.slice(0, cPos).trim().replace(/^['"]|['"]$/g, '');
1455
+ const nv = nextLine.text.slice(cPos + 1).trim();
1456
+ if (!nv && idx < lines.length && lines[idx].indent > nextLine.indent) {
1457
+ obj[nk] = parseBlock(lines[idx].indent);
1458
+ } else {
1459
+ obj[nk] = parseYamlScalar(nv);
1460
+ }
1461
+ }
1462
+ }
1463
+ container.push(obj);
1464
+ } else {
1465
+ container.push(parseYamlScalar(itemText));
1466
+ }
1467
+ }
1468
+ } else {
1469
+ const colonPos = line.text.indexOf(':');
1470
+ if (colonPos !== -1) {
1471
+ const k = line.text.slice(0, colonPos).trim().replace(/^['"]|['"]$/g, '');
1472
+ const v = line.text.slice(colonPos + 1).trim();
1473
+ if (!v) {
1474
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1475
+ container[k] = parseBlock(lines[idx].indent);
1476
+ } else {
1477
+ container[k] = {};
1478
+ }
1479
+ } else {
1480
+ container[k] = parseYamlScalar(v);
1481
+ }
1482
+ }
1483
+ }
1484
+ }
1485
+ return container;
1486
+ }
1487
+
1488
+ const root = parseBlock(lines[0].indent);
1489
+ return { ok: true, data: root };
1490
+ } catch (err) {
1491
+ return { ok: false, error: err.message };
1492
+ }
1493
+ }
1494
+
1495
+ // =========================================================================
1496
+ // Code Symbol Outline Extractor
1497
+ // =========================================================================
1498
+ function extractCodeSymbols(content, category) {
1499
+ if (!content) return [];
1500
+ const lines = content.split('\n');
1501
+ const symbols = [];
1502
+
1503
+ if (category === 'javascript') {
1504
+ lines.forEach((line, idx) => {
1505
+ const lineNum = idx + 1;
1506
+ const trimmed = line.trim();
1507
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) return;
1508
+
1509
+ const classMatch = trimmed.match(/\bclass\s+([a-zA-Z0-9_$]+)/);
1510
+ if (classMatch) {
1511
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1032
1512
  return;
1033
1513
  }
1034
1514
 
1035
- // 5. If clicking on a file link or button that opens a file preview, do not close (let interceptor open it)
1036
- if (target.closest) {
1037
- const btn = target.closest('button');
1038
- if (btn) {
1039
- const candidate = btn.getAttribute('title') || btn.getAttribute('aria-label') || btn.textContent || '';
1040
- if (isPreviewableFile(candidate.trim())) return;
1041
- }
1042
- const anchor = target.closest('a');
1043
- if (anchor) {
1044
- const href = anchor.getAttribute('href') || '';
1045
- if (isPreviewableFile(href)) return;
1046
- }
1515
+ const funcMatch = trimmed.match(/\b(?:async\s+)?function\s*([a-zA-Z0-9_$]+)\s*\(/);
1516
+ if (funcMatch) {
1517
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${funcMatch[1]}()` });
1518
+ return;
1047
1519
  }
1048
1520
 
1049
- // 6. Outside click in dialog/conversation/background: Auto hide the preview drawer!
1050
- setPanelOpen(false);
1051
- };
1521
+ const arrowMatch = trimmed.match(/(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>/);
1522
+ if (arrowMatch) {
1523
+ symbols.push({ id: `line-${lineNum}`, name: arrowMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${arrowMatch[1]}()` });
1524
+ return;
1525
+ }
1052
1526
 
1053
- window.addEventListener('pointerdown', handleOutsidePointerDown, true);
1054
- return () => {
1055
- window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
1056
- };
1057
- }, [isOpen, isDragging]);
1527
+ const typeMatch = trimmed.match(/\b(?:interface|type|enum)\s+([a-zA-Z0-9_$]+)/);
1528
+ if (typeMatch) {
1529
+ symbols.push({ id: `line-${lineNum}`, name: typeMatch[1], kind: 'type', line: lineNum, text: `🏷️ ${typeMatch[1]}` });
1530
+ return;
1531
+ }
1058
1532
 
1059
- // Hotkey: Esc closes preview
1060
- useEffect(() => {
1061
- const onKeyDown = (e) => {
1062
- if (e.key === 'Escape' && globalIsPanelOpen) {
1063
- setPanelOpen(false);
1533
+ if (trimmed.startsWith('export default')) {
1534
+ symbols.push({ id: `line-${lineNum}`, name: 'default export', kind: 'export', line: lineNum, text: `📦 export default` });
1064
1535
  }
1065
- };
1066
- window.addEventListener('keydown', onKeyDown);
1067
- return () => window.removeEventListener('keydown', onKeyDown);
1068
- }, []);
1536
+ });
1537
+ } else if (category === 'python') {
1538
+ lines.forEach((line, idx) => {
1539
+ const lineNum = idx + 1;
1540
+ const classMatch = line.match(/^class\s+([a-zA-Z0-9_]+)/);
1541
+ if (classMatch) {
1542
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1543
+ return;
1544
+ }
1545
+ const funcMatch = line.match(/^(\s*)(?:async\s+)?def\s+([a-zA-Z0-9_]+)/);
1546
+ if (funcMatch) {
1547
+ const prefix = funcMatch[1].length > 0 ? ' ' : '';
1548
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[2], kind: 'function', line: lineNum, text: `${prefix}⚡ def ${funcMatch[2]}()` });
1549
+ }
1550
+ });
1551
+ } else if (category === 'yaml') {
1552
+ lines.forEach((line, idx) => {
1553
+ const lineNum = idx + 1;
1554
+ const topKeyMatch = line.match(/^([a-zA-Z0-9_\-./@$]+)\s*:/);
1555
+ if (topKeyMatch && !line.startsWith('#')) {
1556
+ symbols.push({ id: `line-${lineNum}`, name: topKeyMatch[1], kind: 'key', line: lineNum, text: `🔑 ${topKeyMatch[1]}` });
1557
+ }
1558
+ });
1559
+ } else if (category === 'json') {
1560
+ lines.forEach((line, idx) => {
1561
+ const lineNum = idx + 1;
1562
+ const keyMatch = line.match(/^\s{2}"([a-zA-Z0-9_\-./@$]+)"\s*:/);
1563
+ if (keyMatch) {
1564
+ symbols.push({ id: `line-${lineNum}`, name: keyMatch[1], kind: 'key', line: lineNum, text: `🔑 "${keyMatch[1]}"` });
1565
+ }
1566
+ });
1567
+ }
1568
+ return symbols;
1569
+ }
1069
1570
 
1070
- if (!isOpen && tabs.length === 0) return null;
1571
+ // =========================================================================
1572
+ // React Component: JSONTreeNode & JSONTreeInspector
1573
+ // =========================================================================
1574
+ function JSONTreeNode({ name, value, path, depth, isArrayItem, index, expandedKeys, onToggle, filterQuery, onCopy }) {
1575
+ const isObject = value !== null && typeof value === 'object';
1576
+ const isArray = Array.isArray(value);
1577
+ const isExpanded = expandedKeys.has(path);
1071
1578
 
1072
- const activeKey = activeTab?.filePath?.toLowerCase();
1073
- const currentFileInfo = activeKey ? fileData[activeKey] : null;
1074
- const currentLoading = activeKey ? !!loadingMap[activeKey] : false;
1075
- const currentError = activeKey ? errorMap[activeKey] : null;
1579
+ if (isObject) {
1580
+ const keys = Object.keys(value);
1581
+ const count = keys.length;
1582
+ const typeLabel = isArray ? `Array(${count})` : `Object{${count}}`;
1076
1583
 
1077
- return h(Fragment, null,
1078
- // Floating Mini Button to Reopen (when closed but tabs exist)
1079
- !isOpen && tabs.length > 0 ? h('div', {
1080
- className: 'dsh-preview-float-pill',
1081
- onClick: () => setPanelOpen(true),
1082
- title: `重新展开文档预览 (${tabs.length} 个标签)`,
1083
- },
1084
- h('span', { className: 'dsh-float-pill-icon' }, '📝'),
1085
- h('span', { className: 'dsh-float-pill-text' }, `预览: ${activeTab?.title || '文档'}`),
1086
- h('span', { className: 'dsh-float-pill-badge' }, tabs.length)
1087
- ) : null,
1584
+ if (filterQuery) {
1585
+ const matchSelf = (name && String(name).toLowerCase().includes(filterQuery)) || path.toLowerCase().includes(filterQuery);
1586
+ let matchChild = false;
1587
+ try { matchChild = JSON.stringify(value).toLowerCase().includes(filterQuery); } catch (e) {}
1588
+ if (!matchSelf && !matchChild) return null;
1589
+ }
1088
1590
 
1089
- // Right-Side Slide Drawer
1090
- isOpen ? h('div', {
1091
- ref: drawerRef,
1092
- className: `dsh-preview-drawer-container ${isDragging ? 'dragging' : ''}`,
1093
- style: { width: `${width}px` },
1094
- },
1095
- // Drag Handle on Left Border
1591
+ const previewSummary = isArray
1592
+ ? `[ ${count === 0 ? '' : '...'} ]`
1593
+ : `{ ${keys.slice(0, 3).map(k => `${k}: ...`).join(', ')}${count > 3 ? ', ...' : ''} }`;
1594
+
1595
+ return h('div', { className: 'dsh-json-node-group' },
1096
1596
  h('div', {
1097
- className: 'dsh-preview-drag-handle',
1098
- onPointerDown: onResizePointerDown,
1099
- onPointerMove: onResizePointerMove,
1100
- onPointerUp: onResizePointerUp,
1101
- title: '左右拖拽调整预览面板宽度',
1102
- }),
1597
+ className: 'dsh-json-row dsh-json-row-expandable',
1598
+ style: { paddingLeft: `${depth * 16 + 6}px` },
1599
+ onClick: () => onToggle(path),
1600
+ },
1601
+ h('span', { className: `dsh-json-chevron ${isExpanded ? 'expanded' : ''}` }, isExpanded ? '▼' : '▶'),
1602
+ name ? h('span', { className: 'dsh-json-key' }, isArrayItem ? `[${index}]` : `"${name}":`) : null,
1603
+ h('span', { className: 'dsh-json-type-badge' }, typeLabel),
1604
+ !isExpanded ? h('span', { className: 'dsh-json-preview' }, previewSummary) : null,
1605
+ h('span', {
1606
+ className: 'dsh-json-copy-btn',
1607
+ title: `复制字段路径: ${path.replace(/^root\.?/, '')}`,
1608
+ onClick: (e) => {
1609
+ e.stopPropagation();
1610
+ onCopy(path.replace(/^root\.?/, ''), '字段路径');
1611
+ }
1612
+ }, '📋')
1613
+ ),
1614
+ isExpanded ? h('div', { className: 'dsh-json-children' },
1615
+ keys.map((k, i) => h(JSONTreeNode, {
1616
+ key: k,
1617
+ name: isArray ? null : k,
1618
+ value: value[k],
1619
+ path: `${path}.${k}`,
1620
+ depth: depth + 1,
1621
+ isArrayItem: isArray,
1622
+ index: i,
1623
+ expandedKeys,
1624
+ onToggle,
1625
+ filterQuery,
1626
+ onCopy,
1627
+ }))
1628
+ ) : null
1629
+ );
1630
+ }
1103
1631
 
1104
- // Drawer Header with FileTabs
1105
- h('div', { className: 'dsh-preview-drawer-header' },
1106
- // Tabs Bar
1107
- h('div', { className: 'dsh-preview-tabs-row' },
1108
- tabs.map(tab => {
1109
- const isActive = tab.id === activeTabId;
1110
- const ext = (tab.extension || '').toLowerCase();
1111
- const icon = ext === '.json' ? '{ }' : (ext === '.ts' || ext === '.js' ? 'TS' : '📝');
1112
- return h('div', {
1113
- key: tab.id,
1114
- className: `dsh-preview-tab-item ${isActive ? 'active' : ''}`,
1115
- onClick: () => switchActiveTab(tab.id),
1116
- title: `${tab.title}\n${tab.filePath}`,
1117
- },
1118
- h('span', { className: 'dsh-tab-icon' }, icon),
1119
- h('span', { className: 'dsh-tab-title' }, tab.title),
1120
- h('button', {
1121
- type: 'button',
1122
- className: 'dsh-tab-close-btn',
1123
- onClick: (e) => closePreviewTab(tab.id, e),
1124
- title: '关闭此标签',
1125
- }, '✕')
1126
- );
1127
- })
1128
- ),
1632
+ // Primitive Value
1633
+ const valType = value === null ? 'null' : typeof value;
1634
+ let valDisplay = String(value);
1635
+ let valClass = `tok-${valType}`;
1636
+
1637
+ if (valType === 'string') {
1638
+ valDisplay = `"${value}"`;
1639
+ valClass = 'tok-string';
1640
+ } else if (valType === 'number') {
1641
+ valClass = 'tok-number';
1642
+ } else if (valType === 'boolean') {
1643
+ valClass = 'tok-boolean';
1644
+ } else if (valType === 'null') {
1645
+ valDisplay = 'null';
1646
+ valClass = 'tok-null';
1647
+ }
1129
1648
 
1130
- // Header Right Window Controls
1131
- h('div', { className: 'dsh-preview-window-controls' },
1132
- tabs.length > 1 ? h('button', {
1133
- type: 'button',
1134
- className: 'dsh-win-ctrl-btn',
1135
- onClick: closeAllTabs,
1136
- title: '关闭所有标签页',
1137
- }, '✕ 全部') : null,
1138
- h('button', {
1139
- type: 'button',
1140
- className: 'dsh-win-ctrl-btn dsh-win-ctrl-close',
1141
- onClick: () => setPanelOpen(false),
1142
- title: '收起预览面板 (Esc)',
1143
- }, '✕')
1144
- )
1145
- ),
1649
+ if (filterQuery) {
1650
+ const matchName = name && String(name).toLowerCase().includes(filterQuery);
1651
+ const matchVal = valDisplay.toLowerCase().includes(filterQuery);
1652
+ if (!matchName && !matchVal) return null;
1653
+ }
1146
1654
 
1147
- // Drawer Body
1148
- activeTab ? h(MarkdownPreviewView, {
1149
- tab: activeTab,
1150
- fileInfo: currentFileInfo,
1151
- isLoading: currentLoading,
1152
- error: currentError,
1153
- onRefresh: () => loadTabFile(activeTab, true),
1154
- onOpenNative: triggerOpenNative,
1155
- onReveal: triggerRevealInExplorer,
1156
- }) : h('div', { className: 'dsh-preview-empty-state' }, '暂无打开的文件')
1157
- ) : null
1655
+ const isUrl = typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
1656
+
1657
+ return h('div', {
1658
+ className: 'dsh-json-row dsh-json-row-leaf',
1659
+ style: { paddingLeft: `${depth * 16 + 22}px` },
1660
+ },
1661
+ name ? h('span', { className: 'dsh-json-key' }, isArrayItem ? `[${index}]:` : `"${name}":`) : null,
1662
+ isUrl ? h('a', {
1663
+ href: value,
1664
+ target: '_blank',
1665
+ rel: 'noopener noreferrer',
1666
+ className: 'dsh-json-link tok-string',
1667
+ onClick: (e) => e.stopPropagation(),
1668
+ }, valDisplay) : h('span', { className: valClass }, valDisplay),
1669
+ h('span', {
1670
+ className: 'dsh-json-copy-btn',
1671
+ title: '复制值',
1672
+ onClick: () => onCopy(String(value), '字段值')
1673
+ }, '📋')
1158
1674
  );
1159
1675
  }
1160
1676
 
1161
- // =========================================================================
1162
- // Global Link Click Listener & Workspace Interceptor
1163
- // =========================================================================
1164
- function setupFileInterceptors(ctx) {
1165
- // 1. Intercept ctx.workspaces.openPath
1166
- if (ctx && ctx.workspaces && typeof ctx.workspaces.openPath === 'function') {
1167
- const originalOpenPath = ctx.workspaces.openPath.bind(ctx.workspaces);
1168
- ctx.workspaces.openPath = async function(targetPath) {
1169
- if (isPreviewableFile(targetPath)) {
1170
- console.log('[Preview] Intercepted workspaces.openPath ->', targetPath);
1171
- openPreviewFile(targetPath);
1172
- return Promise.resolve();
1677
+ function JSONTreeInspector({ data, rawContent, onFormat, onMinify }) {
1678
+ const [expandedKeys, setExpandedKeys] = useState(new Set(['root', 'root.scripts', 'root.dependencies', 'root.devDependencies']));
1679
+ const [filterQuery, setFilterQuery] = useState('');
1680
+
1681
+ const toggleKey = (path) => {
1682
+ setExpandedKeys(prev => {
1683
+ const next = new Set(prev);
1684
+ if (next.has(path)) next.delete(path);
1685
+ else next.add(path);
1686
+ return next;
1687
+ });
1688
+ };
1689
+
1690
+ const expandAll = () => {
1691
+ const all = new Set();
1692
+ const traverse = (val, p) => {
1693
+ all.add(p);
1694
+ if (val && typeof val === 'object') {
1695
+ for (const k of Object.keys(val)) {
1696
+ traverse(val[k], p ? `${p}.${k}` : k);
1697
+ }
1173
1698
  }
1174
- return originalOpenPath(targetPath);
1175
1699
  };
1176
- console.log('[Preview] Successfully hooked ctx.workspaces.openPath');
1177
- }
1700
+ traverse(data, 'root');
1701
+ setExpandedKeys(all);
1702
+ };
1178
1703
 
1179
- // 2. Intercept DOM Clicks globally
1180
- if (typeof document !== 'undefined') {
1181
- document.addEventListener('click', (e) => {
1182
- const target = e.target;
1183
- if (!target || !target.closest) return;
1704
+ const collapseAll = () => {
1705
+ setExpandedKeys(new Set(['root']));
1706
+ };
1184
1707
 
1185
- // A. Click on ProducedFiles chip button or ToolRow summary button
1186
- const button = target.closest('button');
1187
- if (button) {
1188
- const title = button.getAttribute('title') || '';
1189
- const ariaLabel = button.getAttribute('aria-label') || '';
1708
+ const handleCopy = (text, label) => {
1709
+ navigator.clipboard.writeText(text).then(() => {
1710
+ showToast(`已复制 ${label || '内容'}`, 'success');
1711
+ });
1712
+ };
1713
+
1714
+ return h('div', { className: 'dsh-json-inspector' },
1715
+ h('div', { className: 'dsh-json-header' },
1716
+ h('div', { className: 'dsh-json-search-box' },
1717
+ h('span', { className: 'dsh-json-search-icon' }, '🔍'),
1718
+ h('input', {
1719
+ type: 'text',
1720
+ placeholder: '过滤属性名或值...',
1721
+ value: filterQuery,
1722
+ onChange: (e) => setFilterQuery(e.target.value),
1723
+ className: 'dsh-json-search-input',
1724
+ }),
1725
+ filterQuery ? h('button', {
1726
+ className: 'dsh-search-clear-btn',
1727
+ onClick: () => setFilterQuery(''),
1728
+ }, '✕') : null
1729
+ ),
1730
+ h('div', { className: 'dsh-json-actions' },
1731
+ h('button', { type: 'button', className: 'dsh-json-btn', onClick: expandAll, title: '展开所有层级' }, '📂 展开全部'),
1732
+ h('button', { type: 'button', className: 'dsh-json-btn', onClick: collapseAll, title: '折叠到根节点' }, '📁 折叠全部'),
1733
+ onFormat ? h('button', { type: 'button', className: 'dsh-json-btn', onClick: onFormat, title: '格式化并复制' }, '⚡ 格式化') : null,
1734
+ onMinify ? h('button', { type: 'button', className: 'dsh-json-btn', onClick: onMinify, title: '压缩并复制' }, '🗜️ 压缩') : null
1735
+ )
1736
+ ),
1737
+ h('div', { className: 'dsh-json-tree-body' },
1738
+ h(JSONTreeNode, {
1739
+ name: Array.isArray(data) ? 'Array' : 'Object',
1740
+ value: data,
1741
+ path: 'root',
1742
+ depth: 0,
1743
+ expandedKeys,
1744
+ onToggle: toggleKey,
1745
+ filterQuery: filterQuery.trim().toLowerCase(),
1746
+ onCopy: handleCopy,
1747
+ })
1748
+ )
1749
+ );
1750
+ }
1751
+
1752
+ // =========================================================================
1753
+ // React Component: CodeEditorView
1754
+ // =========================================================================
1755
+ function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
1756
+ const lines = useMemo(() => (content ? content.split('\n') : []), [content]);
1757
+
1758
+ const highlightedHtml = useMemo(() => {
1759
+ if (!content) return '';
1760
+ if (!isHighlighted) {
1761
+ let escaped = escapeHtml(content);
1762
+ if (searchQuery.trim()) {
1763
+ const q = escapeHtml(searchQuery.trim());
1764
+ escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
1765
+ }
1766
+ return escaped;
1767
+ }
1768
+ let html = highlightCode(content, category);
1769
+ if (searchQuery.trim()) {
1770
+ try {
1771
+ const q = escapeHtml(searchQuery.trim());
1772
+ const regex = new RegExp(`(${q})(?![^<]*>)`, 'gi');
1773
+ html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
1774
+ } catch (e) {}
1775
+ }
1776
+ return html;
1777
+ }, [content, category, isHighlighted, searchQuery]);
1778
+
1779
+ return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
1780
+ h('div', { className: 'dsh-code-gutter' },
1781
+ lines.map((_, i) => h('div', {
1782
+ key: i,
1783
+ id: `line-gutter-${i + 1}`,
1784
+ className: 'dsh-gutter-line',
1785
+ onClick: () => onLineClick && onLineClick(i + 1),
1786
+ title: `第 ${i + 1} 行 (点击定位)`
1787
+ }, i + 1))
1788
+ ),
1789
+ h('div', { className: 'dsh-code-content' },
1790
+ h('pre', { className: 'dsh-code-pre' },
1791
+ h('code', {
1792
+ className: `dsh-code-lang-${category}`,
1793
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1794
+ })
1795
+ )
1796
+ )
1797
+ );
1798
+ }
1799
+
1800
+ // React Components: MarkdownPreviewView
1801
+ // =========================================================================
1802
+ function MarkdownPreviewView({ tab, fileInfo, isLoading, error, isTreeOpen, onToggleTree, onRefresh, onOpenNative, onReveal }) {
1803
+ const category = getFileTypeCategory(tab.filePath);
1804
+ const defaultMode = getDefaultViewMode(category);
1805
+ const [viewMode, setViewMode] = useState(tab.viewMode || defaultMode);
1806
+
1807
+ useEffect(() => {
1808
+ const mode = tab.viewMode || getDefaultViewMode(getFileTypeCategory(tab.filePath));
1809
+ setViewMode(mode);
1810
+ }, [tab.id, tab.filePath, tab.viewMode]);
1811
+ const [isWordWrap, setIsWordWrap] = useState(false);
1812
+ const [isTocOpen, setIsTocOpen] = useState(false);
1813
+ const [searchQuery, setSearchQuery] = useState('');
1814
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
1815
+ const contentRef = useRef(null);
1816
+ const sourceRef = useRef(null);
1817
+
1818
+ const content = fileInfo?.content || '';
1819
+
1820
+ // Outline extraction: markdown headings for md, symbols for code/json/yaml
1821
+ const outlineItems = useMemo(() => {
1822
+ if (!content) return [];
1823
+ if (category === 'markdown') {
1824
+ return extractTocHeadings(content);
1825
+ }
1826
+ return extractCodeSymbols(content, category);
1827
+ }, [content, category]);
1828
+
1829
+ // Parse JSON / YAML structure for tree mode
1830
+ const parsedTreeData = useMemo(() => {
1831
+ if (!content) return { ok: false, error: '空文件' };
1832
+ if (category === 'json' || viewMode === 'json-tree') {
1833
+ if (category === 'yaml') {
1834
+ return parseYamlToJson(content);
1835
+ }
1836
+ try {
1837
+ const data = JSON.parse(content);
1838
+ return { ok: true, data };
1839
+ } catch (e) {
1840
+ return { ok: false, error: e.message };
1841
+ }
1842
+ }
1843
+ return { ok: false };
1844
+ }, [content, category, viewMode]);
1845
+
1846
+ const renderedMarkdownHtml = useMemo(() => {
1847
+ if (!content || category !== 'markdown') return '';
1848
+ let html = renderMarkdownToHtml(content);
1849
+ if (searchQuery.trim()) {
1850
+ try {
1851
+ const q = escapeHtml(searchQuery.trim());
1852
+ const regex = new RegExp(`(${q})`, 'gi');
1853
+ html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
1854
+ } catch (e) {}
1855
+ }
1856
+ return html;
1857
+ }, [content, category, searchQuery]);
1858
+
1859
+ const handleCopyAll = () => {
1860
+ if (!content) return;
1861
+ navigator.clipboard.writeText(content).then(() => {
1862
+ showToast('已复制全文到剪贴板', 'success');
1863
+ }).catch(() => {
1864
+ showToast('复制失败', 'error');
1865
+ });
1866
+ };
1867
+
1868
+ const handleFormatJSON = () => {
1869
+ try {
1870
+ const obj = JSON.parse(content);
1871
+ const formatted = JSON.stringify(obj, null, 2);
1872
+ navigator.clipboard.writeText(formatted).then(() => {
1873
+ showToast('已格式化 JSON 并复制到剪贴板', 'success');
1874
+ });
1875
+ } catch (e) {
1876
+ showToast('JSON 语法错误,无法格式化', 'error');
1877
+ }
1878
+ };
1879
+
1880
+ const handleMinifyJSON = () => {
1881
+ try {
1882
+ const obj = JSON.parse(content);
1883
+ const minified = JSON.stringify(obj);
1884
+ navigator.clipboard.writeText(minified).then(() => {
1885
+ showToast('已压缩为单行 JSON 并复制到剪贴板', 'success');
1886
+ });
1887
+ } catch (e) {
1888
+ showToast('JSON 语法错误,无法压缩', 'error');
1889
+ }
1890
+ };
1891
+
1892
+ const handleOutlineClick = (item) => {
1893
+ if (category === 'markdown') {
1894
+ if (!contentRef.current) return;
1895
+ const el = contentRef.current.querySelector(`#${item.id}`);
1896
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
1897
+ } else {
1898
+ const el = document.getElementById(`line-gutter-${item.line}`);
1899
+ if (el) {
1900
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1901
+ el.classList.add('dsh-line-highlight-flash');
1902
+ setTimeout(() => el.classList.remove('dsh-line-highlight-flash'), 1800);
1903
+ }
1904
+ }
1905
+ };
1906
+
1907
+ const handleModeChange = (mode) => {
1908
+ setViewMode(mode);
1909
+ updateTabMode(tab.id, mode);
1910
+ };
1911
+
1912
+ return h('div', { className: 'dsh-preview-content-container' },
1913
+ // Toolbar
1914
+ h('div', { className: 'dsh-preview-toolbar' },
1915
+ // Left: View mode toggles
1916
+ h('div', { className: 'dsh-preview-toolbar-left' },
1917
+ h('button', {
1918
+ type: 'button',
1919
+ className: `dsh-preview-tool-btn ${isTreeOpen ? 'active' : ''}`,
1920
+ onClick: onToggleTree,
1921
+ title: isTreeOpen ? '隐藏工作区目录树 (Ctrl/Cmd+B)' : '展开工作区目录树 (Ctrl/Cmd+B)',
1922
+ }, '🗂️ 目录树'),
1923
+
1924
+ // Adaptive View Mode buttons based on File Category
1925
+ category === 'markdown' ? (
1926
+ h('div', { className: 'dsh-preview-mode-group' },
1927
+ h('button', {
1928
+ type: 'button',
1929
+ className: `dsh-preview-mode-btn ${viewMode === 'preview' ? 'active' : ''}`,
1930
+ onClick: () => handleModeChange('preview'),
1931
+ title: 'Markdown 渲染预览 (Markdown Render)',
1932
+ }, '📖 预览'),
1933
+ h('button', {
1934
+ type: 'button',
1935
+ className: `dsh-preview-mode-btn ${viewMode === 'source' ? 'active' : ''}`,
1936
+ onClick: () => handleModeChange('source'),
1937
+ title: '源码高亮模式 (Source Code)',
1938
+ }, '📝 源码'),
1939
+ h('button', {
1940
+ type: 'button',
1941
+ className: `dsh-preview-mode-btn ${viewMode === 'split' ? 'active' : ''}`,
1942
+ onClick: () => handleModeChange('split'),
1943
+ title: '分栏对照模式 (Split View)',
1944
+ }, '🌓 分栏')
1945
+ )
1946
+ ) : category === 'json' ? (
1947
+ h('div', { className: 'dsh-preview-mode-group' },
1948
+ h('button', {
1949
+ type: 'button',
1950
+ className: `dsh-preview-mode-btn ${viewMode === 'json-tree' ? 'active' : ''}`,
1951
+ onClick: () => handleModeChange('json-tree'),
1952
+ title: '可视化结构树检查器 (JSON Tree)',
1953
+ }, '🌳 结构树'),
1954
+ h('button', {
1955
+ type: 'button',
1956
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
1957
+ onClick: () => handleModeChange('code'),
1958
+ title: '语法高亮代码 (Code View)',
1959
+ }, '📜 代码'),
1960
+ h('button', {
1961
+ type: 'button',
1962
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
1963
+ onClick: () => handleModeChange('raw'),
1964
+ title: '纯文本模式 (Raw Text)',
1965
+ }, '📝 纯文本')
1966
+ )
1967
+ ) : category === 'yaml' ? (
1968
+ h('div', { className: 'dsh-preview-mode-group' },
1969
+ h('button', {
1970
+ type: 'button',
1971
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
1972
+ onClick: () => handleModeChange('code'),
1973
+ title: 'YAML 语法高亮 (Syntax Highlighting)',
1974
+ }, '📜 高亮'),
1975
+ h('button', {
1976
+ type: 'button',
1977
+ className: `dsh-preview-mode-btn ${viewMode === 'json-tree' ? 'active' : ''}`,
1978
+ onClick: () => handleModeChange('json-tree'),
1979
+ title: '转为可视化结构树 (YAML as Tree)',
1980
+ }, '🌳 结构树'),
1981
+ h('button', {
1982
+ type: 'button',
1983
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
1984
+ onClick: () => handleModeChange('raw'),
1985
+ title: '纯文本模式 (Raw Text)',
1986
+ }, '📝 纯文本')
1987
+ )
1988
+ ) : (
1989
+ h('div', { className: 'dsh-preview-mode-group' },
1990
+ h('button', {
1991
+ type: 'button',
1992
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
1993
+ onClick: () => handleModeChange('code'),
1994
+ title: '语法高亮模式 (Syntax Highlighting)',
1995
+ }, '📜 高亮'),
1996
+ h('button', {
1997
+ type: 'button',
1998
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
1999
+ onClick: () => handleModeChange('raw'),
2000
+ title: '纯文本模式 (Raw Text)',
2001
+ }, '📝 纯文本')
2002
+ )
2003
+ ),
2004
+
2005
+ // Outline / TOC Button
2006
+ outlineItems.length > 0 ? h('button', {
2007
+ type: 'button',
2008
+ className: `dsh-preview-tool-btn ${isTocOpen ? 'active' : ''}`,
2009
+ onClick: () => setIsTocOpen(!isTocOpen),
2010
+ title: isTocOpen ? '隐藏大纲侧栏' : '显示大纲侧栏',
2011
+ }, category === 'markdown' ? `📑 大纲 (${outlineItems.length})` : `📑 符号 (${outlineItems.length})`) : null,
2012
+
2013
+ // Word Wrap Toggle
2014
+ h('button', {
2015
+ type: 'button',
2016
+ className: `dsh-preview-tool-btn ${isWordWrap ? 'active' : ''}`,
2017
+ onClick: () => setIsWordWrap(!isWordWrap),
2018
+ title: isWordWrap ? '切换为不自动换行 (水平滚动)' : '切换为自动换行',
2019
+ }, '↩️ 换行'),
2020
+
2021
+ // Search Button
2022
+ h('button', {
2023
+ type: 'button',
2024
+ className: `dsh-preview-tool-btn ${isSearchOpen ? 'active' : ''}`,
2025
+ onClick: () => setIsSearchOpen(!isSearchOpen),
2026
+ title: '在文件中搜索',
2027
+ }, '🔍 查找')
2028
+ ),
2029
+
2030
+ // Right: Action buttons
2031
+ h('div', { className: 'dsh-preview-toolbar-right' },
2032
+ category === 'json' ? h('button', {
2033
+ type: 'button',
2034
+ className: 'dsh-preview-tool-btn',
2035
+ onClick: handleFormatJSON,
2036
+ title: '格式化 JSON 并复制',
2037
+ }, '⚡ 格式化') : null,
2038
+
2039
+ category === 'json' ? h('button', {
2040
+ type: 'button',
2041
+ className: 'dsh-preview-tool-btn',
2042
+ onClick: handleMinifyJSON,
2043
+ title: '单行压缩 JSON 并复制',
2044
+ }, '🗜️ 压缩') : null,
2045
+
2046
+ h('button', {
2047
+ type: 'button',
2048
+ className: 'dsh-preview-tool-btn',
2049
+ onClick: handleCopyAll,
2050
+ title: '一键复制全文',
2051
+ }, '📋 复制'),
2052
+
2053
+ h('button', {
2054
+ type: 'button',
2055
+ className: 'dsh-preview-tool-btn',
2056
+ onClick: onRefresh,
2057
+ title: '从磁盘重新加载最新内容',
2058
+ }, '🔄 刷新'),
2059
+
2060
+ h('button', {
2061
+ type: 'button',
2062
+ className: 'dsh-preview-tool-btn dsh-preview-tool-btn-accent',
2063
+ onClick: () => onOpenNative(tab.filePath),
2064
+ title: '在系统本地默认编辑器中打开 (记事本/VSCode)',
2065
+ }, '🖥️ 本地打开'),
2066
+
2067
+ h('button', {
2068
+ type: 'button',
2069
+ className: 'dsh-preview-tool-btn',
2070
+ onClick: () => onReveal(tab.filePath),
2071
+ title: '在文件管理器中定位',
2072
+ }, '📁 定位')
2073
+ )
2074
+ ),
2075
+
2076
+ // Search Bar (if open)
2077
+ isSearchOpen ? h('div', { className: 'dsh-preview-search-bar' },
2078
+ h('span', { className: 'dsh-search-icon' }, '🔍'),
2079
+ h('input', {
2080
+ type: 'text',
2081
+ placeholder: '输入关键词在文档中搜索...',
2082
+ value: searchQuery,
2083
+ onChange: e => setSearchQuery(e.target.value),
2084
+ className: 'dsh-preview-search-input',
2085
+ autoFocus: true,
2086
+ }),
2087
+ searchQuery ? h('button', {
2088
+ type: 'button',
2089
+ className: 'dsh-search-clear-btn',
2090
+ onClick: () => setSearchQuery(''),
2091
+ }, '✕') : null
2092
+ ) : null,
2093
+
2094
+ // Body Layout
2095
+ h('div', { className: 'dsh-preview-body-layout' },
2096
+ // Outline / TOC Sidebar
2097
+ isTocOpen && outlineItems.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
2098
+ h('div', { className: 'dsh-preview-toc-header' },
2099
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, category === 'markdown' ? '目录大纲 (TOC)' : '符号与属性大纲'),
2100
+ h('button', {
2101
+ type: 'button',
2102
+ className: 'dsh-toc-close-btn',
2103
+ onClick: () => setIsTocOpen(false),
2104
+ }, '✕')
2105
+ ),
2106
+ h('div', { className: 'dsh-preview-toc-list' },
2107
+ outlineItems.map((item, idx) =>
2108
+ h('div', {
2109
+ key: idx,
2110
+ className: `dsh-toc-item ${item.level ? `dsh-toc-level-${item.level}` : 'dsh-symbol-item'}`,
2111
+ onClick: () => handleOutlineClick(item),
2112
+ title: item.text,
2113
+ },
2114
+ h('span', { className: 'dsh-toc-text' }, item.text)
2115
+ )
2116
+ )
2117
+ )
2118
+ ) : null,
2119
+
2120
+ // Main View Content
2121
+ h('div', { className: 'dsh-preview-main-scroll' },
2122
+ isLoading ? h('div', { className: 'dsh-preview-loading' },
2123
+ h('div', { className: 'dsh-preview-spinner' }),
2124
+ h('span', null, '正在加载文件内容...')
2125
+ ) : (error || fileInfo?.error) ? h('div', { className: 'dsh-preview-error-card' },
2126
+ h('div', { className: 'dsh-error-icon' }, '⚠️'),
2127
+ h('div', { className: 'dsh-error-title' }, '读取文件失败'),
2128
+ h('div', { className: 'dsh-error-desc' }, error || fileInfo?.error),
2129
+ h('div', { className: 'dsh-error-actions' },
2130
+ h('button', { type: 'button', className: 'dsh-btn-retry', onClick: onRefresh }, '重试'),
2131
+ h('button', { type: 'button', className: 'dsh-btn-native', onClick: () => onOpenNative(tab.filePath) }, '在外部打开')
2132
+ )
2133
+ ) : viewMode === 'json-tree' ? (
2134
+ // JSON / YAML Tree Inspector
2135
+ parsedTreeData.ok ? (
2136
+ h(JSONTreeInspector, {
2137
+ data: parsedTreeData.data,
2138
+ rawContent: content,
2139
+ onFormat: category === 'json' ? handleFormatJSON : null,
2140
+ onMinify: category === 'json' ? handleMinifyJSON : null,
2141
+ })
2142
+ ) : (
2143
+ h('div', { className: 'dsh-json-parse-error' },
2144
+ h('div', { style: { fontSize: 28, marginBottom: 8 } }, '⚠️'),
2145
+ h('div', { style: { fontWeight: 600, fontSize: 14, marginBottom: 4 } }, '结构解析失败'),
2146
+ h('div', { style: { fontSize: 12, color: '#ef4444', marginBottom: 12 } }, parsedTreeData.error || '无法按树状结构解析'),
2147
+ h('button', {
2148
+ type: 'button',
2149
+ className: 'dsh-btn-native',
2150
+ onClick: () => handleModeChange('code')
2151
+ }, '切换到代码高亮模式查看')
2152
+ )
2153
+ )
2154
+ ) : viewMode === 'preview' && category === 'markdown' ? (
2155
+ // Markdown Render View
2156
+ h('div', {
2157
+ ref: contentRef,
2158
+ className: 'dsh-markdown-body',
2159
+ dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
2160
+ })
2161
+ ) : viewMode === 'split' && category === 'markdown' ? (
2162
+ // Markdown Split View
2163
+ h('div', { className: 'dsh-split-view' },
2164
+ h('div', { className: 'dsh-split-pane dsh-split-source' },
2165
+ h('div', { className: 'dsh-split-pane-header' }, '📝 源码'),
2166
+ h(CodeEditorView, {
2167
+ content,
2168
+ category: 'markdown',
2169
+ isHighlighted: true,
2170
+ isWordWrap,
2171
+ searchQuery,
2172
+ onLineClick: () => {},
2173
+ })
2174
+ ),
2175
+ h('div', { className: 'dsh-split-pane dsh-split-render' },
2176
+ h('div', { className: 'dsh-split-pane-header' }, '📖 渲染预览'),
2177
+ h('div', {
2178
+ ref: contentRef,
2179
+ className: 'dsh-markdown-body',
2180
+ dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
2181
+ })
2182
+ )
2183
+ )
2184
+ ) : (
2185
+ // Code View (for code/json/yaml/source/raw modes)
2186
+ h(CodeEditorView, {
2187
+ content,
2188
+ category,
2189
+ isHighlighted: viewMode !== 'raw',
2190
+ isWordWrap,
2191
+ searchQuery,
2192
+ onLineClick: (lineNum) => {
2193
+ showToast(`已选定第 ${lineNum} 行`, 'info');
2194
+ },
2195
+ })
2196
+ )
2197
+ )
2198
+ ),
2199
+
2200
+ // Status Footer
2201
+ h('div', { className: 'dsh-preview-footer' },
2202
+ h('div', { className: 'dsh-footer-left', title: fileInfo?.path || tab.filePath },
2203
+ h('span', { className: 'dsh-footer-ext' }, (fileInfo?.extension || tab.extension || category).toUpperCase().replace('.', '')),
2204
+ h('span', { className: 'dsh-footer-path' }, fileInfo?.path || tab.filePath)
2205
+ ),
2206
+ h('div', { className: 'dsh-footer-right' },
2207
+ fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, formatBytes(fileInfo.size)) : null,
2208
+ fileInfo?.lineCount ? h('span', { className: 'dsh-footer-stat' }, `${fileInfo.lineCount} 行`) : null,
2209
+ fileInfo?.wordCount ? h('span', { className: 'dsh-footer-stat' }, `约 ${fileInfo.wordCount} 字`) : null
2210
+ )
2211
+ )
2212
+ );
2213
+ }
2214
+
2215
+ // =========================================================================
2216
+ // React Components: Right Side Drawer / Shell Overlay Container
2217
+ // =========================================================================
2218
+ function PreviewDrawerRoot() {
2219
+ const [tabs, setTabs] = useState(globalTabs);
2220
+ const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
2221
+ const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
2222
+ const [width, setWidth] = useState(globalPanelWidth);
2223
+ const [isTreeOpen, setLocalIsTreeOpen] = useState(globalIsTreeOpen);
2224
+ const [treeWidth, setLocalTreeWidth] = useState(globalTreeWidth);
2225
+ const [isTreeDragging, setIsTreeDragging] = useState(false);
2226
+ const treeDragStartX = useRef(0);
2227
+ const treeDragStartWidth = useRef(0);
2228
+ const [fileData, setFileData] = useState({});
2229
+ const [loadingMap, setLoadingMap] = useState({});
2230
+ const [errorMap, setErrorMap] = useState({});
2231
+ const [isDragging, setIsDragging] = useState(false);
2232
+ const dragStartX = useRef(0);
2233
+ const dragStartWidth = useRef(0);
2234
+
2235
+ // Subscribe to global store changes
2236
+ useEffect(() => {
2237
+ const update = () => {
2238
+ setTabs([...globalTabs]);
2239
+ setActiveTabId(globalActiveTabId);
2240
+ setIsOpen(globalIsPanelOpen);
2241
+ setWidth(globalPanelWidth);
2242
+ setLocalIsTreeOpen(globalIsTreeOpen);
2243
+ setLocalTreeWidth(globalTreeWidth);
2244
+ };
2245
+ stateListeners.add(update);
2246
+ return () => stateListeners.delete(update);
2247
+ }, []);
2248
+
2249
+ const activeTab = useMemo(() => {
2250
+ return tabs.find(t => t.id === activeTabId) || tabs[0] || null;
2251
+ }, [tabs, activeTabId]);
2252
+
2253
+ // Load file content when active tab changes
2254
+ const loadTabFile = useCallback(async (tab, force = false) => {
2255
+ if (!tab || !tab.filePath) return;
2256
+ const key = tab.filePath.toLowerCase();
2257
+ setLoadingMap(m => ({ ...m, [key]: true }));
2258
+ setErrorMap(m => ({ ...m, [key]: null }));
2259
+ try {
2260
+ const fileInfo = await fetchFileContent(tab.filePath, force);
2261
+ if (fileInfo?.error) {
2262
+ setErrorMap(m => ({ ...m, [key]: fileInfo.error }));
2263
+ } else {
2264
+ setFileData(d => ({ ...d, [key]: fileInfo }));
2265
+ }
2266
+ } catch (e) {
2267
+ setErrorMap(m => ({ ...m, [key]: e.message }));
2268
+ } finally {
2269
+ setLoadingMap(m => ({ ...m, [key]: false }));
2270
+ }
2271
+ }, []);
2272
+
2273
+ useEffect(() => {
2274
+ if (activeTab) {
2275
+ loadTabFile(activeTab);
2276
+ }
2277
+ }, [activeTab, loadTabFile]);
2278
+
2279
+ // Drag Resize Handlers (Panel Width)
2280
+ const onResizePointerDown = useCallback((e) => {
2281
+ e.preventDefault();
2282
+ e.currentTarget.setPointerCapture(e.pointerId);
2283
+ dragStartX.current = e.clientX;
2284
+ dragStartWidth.current = globalPanelWidth;
2285
+ setIsDragging(true);
2286
+ }, []);
2287
+
2288
+ const onResizePointerMove = useCallback((e) => {
2289
+ if (!isDragging) return;
2290
+ const dx = dragStartX.current - e.clientX; // drag left increases width
2291
+ const newWidth = dragStartWidth.current + dx;
2292
+ setPanelWidth(newWidth);
2293
+ }, [isDragging]);
2294
+
2295
+ const onResizePointerUp = useCallback((e) => {
2296
+ if (isDragging) {
2297
+ setIsDragging(false);
2298
+ try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
2299
+ }
2300
+ }, [isDragging]);
2301
+
2302
+ // Drag Resize Handlers (Tree Width)
2303
+ const onTreeResizePointerDown = useCallback((e) => {
2304
+ e.preventDefault();
2305
+ e.currentTarget.setPointerCapture(e.pointerId);
2306
+ treeDragStartX.current = e.clientX;
2307
+ treeDragStartWidth.current = globalTreeWidth;
2308
+ setIsTreeDragging(true);
2309
+ }, []);
2310
+
2311
+ const onTreeResizePointerMove = useCallback((e) => {
2312
+ if (!isTreeDragging) return;
2313
+ const dx = e.clientX - treeDragStartX.current;
2314
+ const newWidth = treeDragStartWidth.current + dx;
2315
+ setTreeWidth(newWidth);
2316
+ }, [isTreeDragging]);
2317
+
2318
+ const onTreeResizePointerUp = useCallback((e) => {
2319
+ if (isTreeDragging) {
2320
+ setIsTreeDragging(false);
2321
+ try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
2322
+ }
2323
+ }, [isTreeDragging]);
2324
+
2325
+ const drawerRef = useRef(null);
2326
+
2327
+ // Outside Click Listener: Click on the chat conversation / dialog box outside the drawer to auto close/hide it
2328
+ useEffect(() => {
2329
+ if (!isOpen) return;
2330
+
2331
+ const handleOutsidePointerDown = (e) => {
2332
+ // 1. Ignore if just opened within 250ms
2333
+ if (Date.now() - globalLastOpenTime < 250) return;
2334
+ // 2. Ignore during dragging resize handle
2335
+ if (isDragging || isTreeDragging) return;
2336
+
2337
+ const target = e.target;
2338
+ if (!target) return;
2339
+
2340
+ // 3. If clicking inside the preview drawer, do not close
2341
+ const drawerEl = drawerRef.current || document.querySelector('.dsh-preview-drawer-container');
2342
+ if (drawerEl && (drawerEl === target || drawerEl.contains(target))) {
2343
+ return;
2344
+ }
2345
+
2346
+ // 4. If clicking on the floating pill, do not close
2347
+ const floatPill = document.querySelector('.dsh-preview-float-pill');
2348
+ if (floatPill && (floatPill === target || floatPill.contains(target))) {
2349
+ return;
2350
+ }
2351
+
2352
+ // 5. If clicking on a file link or button that opens a file preview, do not close (let interceptor open it)
2353
+ if (target.closest) {
2354
+ const btn = target.closest('button');
2355
+ if (btn) {
2356
+ const candidate = btn.getAttribute('title') || btn.getAttribute('aria-label') || btn.textContent || '';
2357
+ if (isPreviewableFile(candidate.trim())) return;
2358
+ }
2359
+ const anchor = target.closest('a');
2360
+ if (anchor) {
2361
+ const href = anchor.getAttribute('href') || '';
2362
+ if (isPreviewableFile(href)) return;
2363
+ }
2364
+ }
2365
+
2366
+ // 6. Outside click in dialog/conversation/background: Auto hide the preview drawer!
2367
+ setPanelOpen(false);
2368
+ };
2369
+
2370
+ window.addEventListener('pointerdown', handleOutsidePointerDown, true);
2371
+ return () => {
2372
+ window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
2373
+ };
2374
+ }, [isOpen, isDragging, isTreeDragging]);
2375
+
2376
+ // Hotkey: Esc closes preview, Ctrl/Cmd+B toggles file tree
2377
+ useEffect(() => {
2378
+ const onKeyDown = (e) => {
2379
+ if (e.key === 'Escape' && globalIsPanelOpen) {
2380
+ setPanelOpen(false);
2381
+ }
2382
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'b' || e.key === 'B')) {
2383
+ e.preventDefault();
2384
+ setIsTreeOpen(!globalIsTreeOpen);
2385
+ }
2386
+ };
2387
+ window.addEventListener('keydown', onKeyDown);
2388
+ return () => window.removeEventListener('keydown', onKeyDown);
2389
+ }, []);
2390
+
2391
+ const activeKey = activeTab?.filePath?.toLowerCase();
2392
+ const currentFileInfo = activeKey ? fileData[activeKey] : null;
2393
+ const currentLoading = activeKey ? !!loadingMap[activeKey] : false;
2394
+ const currentError = activeKey ? errorMap[activeKey] : null;
2395
+
2396
+ return h(Fragment, null,
2397
+ // Floating Mini Button to Reopen (when closed)
2398
+ !isOpen ? h('div', {
2399
+ className: 'dsh-preview-float-pill',
2400
+ onClick: () => setPanelOpen(true),
2401
+ title: tabs.length > 0 ? `展开文档与工作区文件浏览 (${tabs.length} 个标签)` : '浏览工作空间文件树',
2402
+ },
2403
+ h('span', { className: 'dsh-float-pill-icon' }, '📁'),
2404
+ h('span', { className: 'dsh-float-pill-text' }, tabs.length > 0 ? `预览: ${activeTab?.title || '文档'}` : '工作区文件'),
2405
+ tabs.length > 0 ? h('span', { className: 'dsh-float-pill-badge' }, tabs.length) : null
2406
+ ) : null,
2407
+
2408
+ // Right-Side Slide Drawer
2409
+ isOpen ? h('div', {
2410
+ ref: drawerRef,
2411
+ className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''}`,
2412
+ style: { width: `${width}px` },
2413
+ },
2414
+ // Drag Handle on Left Border
2415
+ h('div', {
2416
+ className: 'dsh-preview-drag-handle',
2417
+ onPointerDown: onResizePointerDown,
2418
+ onPointerMove: onResizePointerMove,
2419
+ onPointerUp: onResizePointerUp,
2420
+ title: '左右拖拽调整预览面板宽度',
2421
+ }),
2422
+
2423
+ // Drawer Header with FileTabs
2424
+ h('div', { className: 'dsh-preview-drawer-header' },
2425
+ // Tabs Bar
2426
+ h('div', { className: 'dsh-preview-tabs-row' },
2427
+ tabs.length > 0 ? tabs.map(tab => {
2428
+ const isActive = tab.id === activeTabId;
2429
+ const ext = (tab.extension || '').toLowerCase();
2430
+ const icon = ext === '.json' ? '{ }' : (ext === '.ts' || ext === '.js' ? 'TS' : '📝');
2431
+ return h('div', {
2432
+ key: tab.id,
2433
+ className: `dsh-preview-tab-item ${isActive ? 'active' : ''}`,
2434
+ onClick: () => switchActiveTab(tab.id),
2435
+ title: `${tab.title}\n${tab.filePath}`,
2436
+ },
2437
+ h('span', { className: 'dsh-tab-icon' }, icon),
2438
+ h('span', { className: 'dsh-tab-title' }, tab.title),
2439
+ h('button', {
2440
+ type: 'button',
2441
+ className: 'dsh-tab-close-btn',
2442
+ onClick: (e) => closePreviewTab(tab.id, e),
2443
+ title: '关闭此标签',
2444
+ }, '✕')
2445
+ );
2446
+ }) : h('div', { className: 'dsh-preview-tab-item active' },
2447
+ h('span', { className: 'dsh-tab-icon' }, '📁'),
2448
+ h('span', { className: 'dsh-tab-title' }, '工作空间文件树')
2449
+ )
2450
+ ),
2451
+
2452
+ // Header Right Window Controls
2453
+ h('div', { className: 'dsh-preview-window-controls' },
2454
+ h('button', {
2455
+ type: 'button',
2456
+ className: `dsh-win-ctrl-btn ${isTreeOpen ? 'active' : ''}`,
2457
+ onClick: () => setIsTreeOpen(!globalIsTreeOpen),
2458
+ title: isTreeOpen ? '收起目录树 (Ctrl/Cmd+B)' : '展开目录树 (Ctrl/Cmd+B)',
2459
+ }, '🗂️ 目录树'),
2460
+ tabs.length > 1 ? h('button', {
2461
+ type: 'button',
2462
+ className: 'dsh-win-ctrl-btn',
2463
+ onClick: closeAllTabs,
2464
+ title: '关闭所有标签页',
2465
+ }, '✕ 全部') : null,
2466
+ h('button', {
2467
+ type: 'button',
2468
+ className: 'dsh-win-ctrl-btn dsh-win-ctrl-close',
2469
+ onClick: () => setPanelOpen(false),
2470
+ title: '收起预览面板 (Esc)',
2471
+ }, '✕')
2472
+ )
2473
+ ),
2474
+
2475
+ // Drawer Body (Split View between Workspace Tree & Preview View)
2476
+ h('div', { className: 'dsh-preview-drawer-body-split' },
2477
+ // Left: Workspace File Tree
2478
+ isTreeOpen ? h(WorkspaceTreeSidebar, {
2479
+ width: treeWidth,
2480
+ activeFilePath: activeTab?.filePath,
2481
+ onOpenFile: openPreviewFile,
2482
+ onCloseSidebar: () => setIsTreeOpen(false),
2483
+ }) : null,
2484
+
2485
+ // Resizer between Tree and Content
2486
+ isTreeOpen ? h('div', {
2487
+ className: `dsh-tree-splitter ${isTreeDragging ? 'dragging' : ''}`,
2488
+ onPointerDown: onTreeResizePointerDown,
2489
+ onPointerMove: onTreeResizePointerMove,
2490
+ onPointerUp: onTreeResizePointerUp,
2491
+ title: '拖拽调整目录树宽度',
2492
+ }) : null,
2493
+
2494
+ // Right: Content View or Welcome View
2495
+ activeTab ? h(MarkdownPreviewView, {
2496
+ key: activeTab.id,
2497
+ tab: activeTab,
2498
+ fileInfo: currentFileInfo,
2499
+ isLoading: currentLoading,
2500
+ error: currentError,
2501
+ isTreeOpen: isTreeOpen,
2502
+ onToggleTree: () => setIsTreeOpen(!globalIsTreeOpen),
2503
+ onRefresh: () => loadTabFile(activeTab, true),
2504
+ onOpenNative: triggerOpenNative,
2505
+ onReveal: triggerRevealInExplorer,
2506
+ }) : h(WorkspaceWelcomeView, {
2507
+ workspaceRoot: getCurrentWorkspaceRoot(),
2508
+ workspaceName: getFileName(getCurrentWorkspaceRoot()),
2509
+ onOpenFile: openPreviewFile,
2510
+ onOpenTree: () => setIsTreeOpen(true),
2511
+ })
2512
+ )
2513
+ ) : null
2514
+ );
2515
+ }
2516
+
2517
+ // =========================================================================
2518
+ // Global Link Click Listener & Workspace Interceptor
2519
+ // =========================================================================
2520
+ function setupFileInterceptors(ctx) {
2521
+ // 1. Intercept ctx.workspaces.openPath
2522
+ if (ctx && ctx.workspaces && typeof ctx.workspaces.openPath === 'function') {
2523
+ const originalOpenPath = ctx.workspaces.openPath.bind(ctx.workspaces);
2524
+ ctx.workspaces.openPath = async function(targetPath) {
2525
+ if (isPreviewableFile(targetPath)) {
2526
+ console.log('[Preview] Intercepted workspaces.openPath ->', targetPath);
2527
+ openPreviewFile(targetPath);
2528
+ return Promise.resolve();
2529
+ }
2530
+ return originalOpenPath(targetPath);
2531
+ };
2532
+ console.log('[Preview] Successfully hooked ctx.workspaces.openPath');
2533
+ }
2534
+
2535
+ // 2. Intercept DOM Clicks globally
2536
+ if (typeof document !== 'undefined') {
2537
+ document.addEventListener('click', (e) => {
2538
+ const target = e.target;
2539
+ if (!target || !target.closest) return;
2540
+
2541
+ // A. Click on ProducedFiles chip button or ToolRow summary button
2542
+ const button = target.closest('button');
2543
+ if (button) {
2544
+ const title = button.getAttribute('title') || '';
2545
+ const ariaLabel = button.getAttribute('aria-label') || '';
1190
2546
  const text = (button.textContent || '').trim();
1191
2547
 
1192
2548
  const candidate = title || ariaLabel || text;
@@ -1202,1023 +2558,1703 @@
1202
2558
  }
1203
2559
  }
1204
2560
 
1205
- // B. Click on <a> link pointing to .md or file://
1206
- const anchor = target.closest('a');
1207
- if (anchor) {
1208
- const href = anchor.getAttribute('href') || '';
1209
- if (href.startsWith('#') && !href.startsWith('#heading-')) return;
1210
- if (href.startsWith('file://') || isPreviewableFile(href)) {
1211
- e.preventDefault();
1212
- e.stopPropagation();
1213
- const fullPath = resolveAbsoluteFilePath(href);
1214
- console.log('[Preview] Intercepted link click ->', fullPath);
1215
- openPreviewFile(fullPath);
1216
- }
1217
- }
1218
- }, true); // Use capture phase to intercept early!
1219
- }
1220
- }
2561
+ // B. Click on <a> link pointing to .md or file://
2562
+ const anchor = target.closest('a');
2563
+ if (anchor) {
2564
+ const href = anchor.getAttribute('href') || '';
2565
+ if (href.startsWith('#') && !href.startsWith('#heading-')) return;
2566
+ if (href.startsWith('file://') || isPreviewableFile(href)) {
2567
+ e.preventDefault();
2568
+ e.stopPropagation();
2569
+ const fullPath = resolveAbsoluteFilePath(href);
2570
+ console.log('[Preview] Intercepted link click ->', fullPath);
2571
+ openPreviewFile(fullPath);
2572
+ }
2573
+ }
2574
+ }, true); // Use capture phase to intercept early!
2575
+ }
2576
+ }
2577
+
2578
+ // =========================================================================
2579
+ // Inject Custom Stylesheet
2580
+ // =========================================================================
2581
+ function injectStyles() {
2582
+ if (typeof document === 'undefined') return;
2583
+ if (document.getElementById('dsh-plugin-preview-styles')) return;
2584
+
2585
+ const style = document.createElement('style');
2586
+ style.id = 'dsh-plugin-preview-styles';
2587
+ style.textContent = `
2588
+ /* =====================================================================
2589
+ DeepSeek Harness Preview Plugin Styles (WorkBuddy FileTabs Inspired)
2590
+ ===================================================================== */
2591
+
2592
+ /* Floating Pill Button */
2593
+ .dsh-preview-float-pill {
2594
+ position: fixed;
2595
+ bottom: 84px;
2596
+ right: 20px;
2597
+ z-index: 9990;
2598
+ display: flex;
2599
+ align-items: center;
2600
+ gap: 8px;
2601
+ padding: 8px 14px;
2602
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
2603
+ color: var(--dsw-alias-label-primary, #0f172a);
2604
+ border: 1px solid var(--dsw-alias-border-l3, #cbd5e1);
2605
+ border-radius: 20px;
2606
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
2607
+ cursor: pointer;
2608
+ font-size: 13px;
2609
+ font-weight: 500;
2610
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
2611
+ user-select: none;
2612
+ }
2613
+ body[data-ds-dark-theme] .dsh-preview-float-pill {
2614
+ background: var(--dsw-alias-bg-layer-2, #1e222d);
2615
+ color: var(--dsw-alias-label-primary, #e2e8f0);
2616
+ border-color: var(--dsw-alias-border-l3, #334155);
2617
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
2618
+ }
2619
+ .dsh-preview-float-pill:hover {
2620
+ transform: translateY(-2px);
2621
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
2622
+ box-shadow: 0 10px 24px rgba(65, 118, 230, 0.25);
2623
+ }
2624
+ .dsh-float-pill-badge {
2625
+ background: var(--dsw-static-deepseek-500, #4176e6);
2626
+ color: #fff;
2627
+ font-size: 11px;
2628
+ padding: 1px 6px;
2629
+ border-radius: 10px;
2630
+ font-weight: 600;
2631
+ }
2632
+
2633
+ /* Right-Side Drawer Container */
2634
+ .dsh-preview-drawer-container {
2635
+ position: fixed;
2636
+ top: 0;
2637
+ right: 0;
2638
+ bottom: 0;
2639
+ height: 100vh;
2640
+ z-index: 9995;
2641
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
2642
+ color: var(--dsw-alias-label-primary, #0f172a);
2643
+ border-left: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
2644
+ box-shadow: -8px 0 32px rgba(0, 0, 0, 0.14);
2645
+ display: flex;
2646
+ flex-direction: column;
2647
+ box-sizing: border-box;
2648
+ overflow: hidden;
2649
+ animation: dshPreviewSlideIn 0.24s cubic-bezier(0.16, 1, 0.3, 1);
2650
+ }
2651
+ body[data-ds-dark-theme] .dsh-preview-drawer-container {
2652
+ background: var(--dsw-alias-bg-layer-1, #161922);
2653
+ color: var(--dsw-alias-label-primary, #e2e8f0);
2654
+ border-left-color: var(--dsw-alias-border-l3, #2d3748);
2655
+ box-shadow: -12px 0 40px rgba(0, 0, 0, 0.45);
2656
+ }
2657
+ .dsh-preview-drawer-container.dragging {
2658
+ user-select: none;
2659
+ transition: none;
2660
+ }
2661
+ @keyframes dshPreviewSlideIn {
2662
+ from { transform: translateX(100%); }
2663
+ to { transform: translateX(0); }
2664
+ }
2665
+
2666
+ /* Left Resize Handle */
2667
+ .dsh-preview-drag-handle {
2668
+ position: absolute;
2669
+ top: 0;
2670
+ left: -4px;
2671
+ bottom: 0;
2672
+ width: 8px;
2673
+ cursor: col-resize;
2674
+ z-index: 100;
2675
+ transition: background 0.15s;
2676
+ }
2677
+ .dsh-preview-drag-handle:hover,
2678
+ .dsh-preview-drawer-container.dragging .dsh-preview-drag-handle {
2679
+ background: var(--dsw-static-deepseek-500, #4176e6);
2680
+ }
2681
+
2682
+ /* Header & FileTabs */
2683
+ .dsh-preview-drawer-header {
2684
+ display: flex;
2685
+ align-items: center;
2686
+ justify-content: space-between;
2687
+ height: 38px;
2688
+ min-height: 38px;
2689
+ flex-shrink: 0;
2690
+ background: var(--dsw-alias-bg-layer-2, #f1f5f9);
2691
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2692
+ padding: 0 8px;
2693
+ overflow: hidden;
2694
+ box-sizing: border-box;
2695
+ }
2696
+ body[data-ds-dark-theme] .dsh-preview-drawer-header {
2697
+ background: var(--dsw-alias-bg-layer-2, #12151d);
2698
+ border-bottom-color: var(--dsw-alias-border-l2, #2d3748);
2699
+ }
2700
+ .dsh-preview-tabs-row {
2701
+ display: flex;
2702
+ align-items: flex-end;
2703
+ gap: 4px;
2704
+ overflow-x: auto;
2705
+ scrollbar-width: none;
2706
+ flex: 1;
2707
+ height: 100%;
2708
+ padding-top: 4px;
2709
+ }
2710
+ .dsh-preview-tabs-row::-webkit-scrollbar { display: none; }
2711
+
2712
+ .dsh-preview-tab-item {
2713
+ display: flex;
2714
+ align-items: center;
2715
+ gap: 6px;
2716
+ padding: 0 10px;
2717
+ height: 30px;
2718
+ border-radius: 6px 6px 0 0;
2719
+ font-size: 12px;
2720
+ color: var(--dsw-alias-label-secondary, #64748b);
2721
+ background: transparent;
2722
+ cursor: pointer;
2723
+ user-select: none;
2724
+ white-space: nowrap;
2725
+ max-width: 180px;
2726
+ transition: all 0.15s;
2727
+ border: 1px solid transparent;
2728
+ border-bottom: none;
2729
+ }
2730
+ .dsh-preview-tab-item:hover {
2731
+ background: rgba(0, 0, 0, 0.04);
2732
+ color: var(--dsw-alias-label-primary, #0f172a);
2733
+ }
2734
+ body[data-ds-dark-theme] .dsh-preview-tab-item:hover {
2735
+ background: rgba(255, 255, 255, 0.05);
2736
+ color: var(--dsw-alias-label-primary, #f1f5f9);
2737
+ }
2738
+ .dsh-preview-tab-item.active {
2739
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
2740
+ color: var(--dsw-static-deepseek-500, #4176e6);
2741
+ font-weight: 600;
2742
+ border-color: var(--dsw-alias-border-l2, #e2e8f0);
2743
+ border-bottom: 2px solid var(--dsw-static-deepseek-500, #4176e6);
2744
+ box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.03);
2745
+ }
2746
+ body[data-ds-dark-theme] .dsh-preview-tab-item.active {
2747
+ background: var(--dsw-alias-bg-layer-1, #161922);
2748
+ border-color: var(--dsw-alias-border-l2, #2d3748);
2749
+ border-bottom: 2px solid var(--dsw-static-deepseek-500, #4176e6);
2750
+ box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.2);
2751
+ }
2752
+ .dsh-tab-icon {
2753
+ font-size: 12px;
2754
+ }
2755
+ .dsh-tab-title {
2756
+ overflow: hidden;
2757
+ text-overflow: ellipsis;
2758
+ white-space: nowrap;
2759
+ }
2760
+ .dsh-tab-close-btn {
2761
+ background: none;
2762
+ border: none;
2763
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
2764
+ font-size: 11px;
2765
+ cursor: pointer;
2766
+ padding: 2px 4px;
2767
+ border-radius: 4px;
2768
+ line-height: 1;
2769
+ }
2770
+ .dsh-tab-close-btn:hover {
2771
+ background: rgba(239, 68, 68, 0.15);
2772
+ color: #ef4444;
2773
+ }
2774
+
2775
+ .dsh-preview-window-controls {
2776
+ display: flex;
2777
+ align-items: center;
2778
+ gap: 6px;
2779
+ padding-left: 8px;
2780
+ height: 100%;
2781
+ }
2782
+ .dsh-win-ctrl-btn {
2783
+ background: transparent;
2784
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
2785
+ color: var(--dsw-alias-label-secondary, #64748b);
2786
+ font-size: 11px;
2787
+ padding: 3px 8px;
2788
+ border-radius: 4px;
2789
+ cursor: pointer;
2790
+ transition: all 0.15s;
2791
+ }
2792
+ .dsh-win-ctrl-btn:hover {
2793
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
2794
+ color: var(--dsw-alias-label-primary, #0f172a);
2795
+ }
2796
+ body[data-ds-dark-theme] .dsh-win-ctrl-btn {
2797
+ border-color: var(--dsw-alias-border-l2, #334155);
2798
+ color: var(--dsw-alias-label-secondary, #94a3b8);
2799
+ }
2800
+ body[data-ds-dark-theme] .dsh-win-ctrl-btn:hover {
2801
+ background: var(--dsw-alias-bg-layer-3, #283042);
2802
+ color: var(--dsw-alias-label-primary, #f1f5f9);
2803
+ }
2804
+ .dsh-win-ctrl-close:hover {
2805
+ background: #ef4444 !important;
2806
+ border-color: #ef4444 !important;
2807
+ color: #fff !important;
2808
+ }
1221
2809
 
1222
- // =========================================================================
1223
- // Inject Custom Stylesheet
1224
- // =========================================================================
1225
- function injectStyles() {
1226
- if (typeof document === 'undefined') return;
1227
- if (document.getElementById('dsh-plugin-preview-styles')) return;
2810
+ /* Content Container */
2811
+ .dsh-preview-content-container {
2812
+ display: flex;
2813
+ flex-direction: column;
2814
+ flex: 1;
2815
+ min-height: 0;
2816
+ overflow: hidden;
2817
+ box-sizing: border-box;
2818
+ }
1228
2819
 
1229
- const style = document.createElement('style');
1230
- style.id = 'dsh-plugin-preview-styles';
1231
- style.textContent = `
1232
- /* =====================================================================
1233
- DeepSeek Harness Preview Plugin Styles (WorkBuddy FileTabs Inspired)
1234
- ===================================================================== */
1235
-
1236
- /* Floating Pill Button */
1237
- .dsh-preview-float-pill {
1238
- position: fixed;
1239
- bottom: 84px;
1240
- right: 20px;
1241
- z-index: 9990;
2820
+ /* Toolbar */
2821
+ .dsh-preview-toolbar {
1242
2822
  display: flex;
1243
2823
  align-items: center;
1244
- gap: 8px;
1245
- padding: 8px 14px;
2824
+ justify-content: space-between;
2825
+ padding: 6px 12px;
2826
+ height: 38px;
2827
+ min-height: 38px;
2828
+ flex-shrink: 0;
1246
2829
  background: var(--dsw-alias-bg-layer-2, #f8fafc);
2830
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2831
+ gap: 10px;
2832
+ box-sizing: border-box;
2833
+ }
2834
+ body[data-ds-dark-theme] .dsh-preview-toolbar {
2835
+ background: var(--dsw-alias-bg-layer-2, #161a24);
2836
+ border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
2837
+ }
2838
+ .dsh-preview-toolbar-left,
2839
+ .dsh-preview-toolbar-right {
2840
+ display: flex;
2841
+ align-items: center;
2842
+ gap: 6px;
2843
+ flex-shrink: 0;
2844
+ white-space: nowrap;
2845
+ }
2846
+
2847
+ .dsh-preview-mode-group {
2848
+ display: flex;
2849
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
2850
+ padding: 2px;
2851
+ border-radius: 6px;
2852
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2853
+ flex-shrink: 0;
2854
+ }
2855
+ body[data-ds-dark-theme] .dsh-preview-mode-group {
2856
+ background: var(--dsw-alias-bg-layer-3, #0d1017);
2857
+ border-color: var(--dsw-alias-border-l2, #252d3d);
2858
+ }
2859
+ .dsh-preview-mode-btn {
2860
+ background: transparent;
2861
+ border: none;
2862
+ color: var(--dsw-alias-label-secondary, #64748b);
2863
+ font-size: 11px;
2864
+ padding: 4px 9px;
2865
+ border-radius: 4px;
2866
+ cursor: pointer;
2867
+ transition: all 0.15s;
2868
+ white-space: nowrap;
2869
+ flex-shrink: 0;
2870
+ }
2871
+ body[data-ds-dark-theme] .dsh-preview-mode-btn {
2872
+ color: var(--dsw-alias-label-secondary, #94a3b8);
2873
+ }
2874
+ .dsh-preview-mode-btn.active {
2875
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
1247
2876
  color: var(--dsw-alias-label-primary, #0f172a);
1248
- border: 1px solid var(--dsw-alias-border-l3, #cbd5e1);
1249
- border-radius: 20px;
1250
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
2877
+ font-weight: 600;
2878
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
2879
+ }
2880
+ body[data-ds-dark-theme] .dsh-preview-mode-btn.active {
2881
+ background: var(--dsw-alias-bg-layer-2, #1e2433);
2882
+ color: var(--dsw-alias-label-primary, #f8fafc);
2883
+ box-shadow: 0 1px 4px rgba(0,0,0,0.25);
2884
+ }
2885
+ .dsh-preview-tool-btn {
2886
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
2887
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
2888
+ color: var(--dsw-alias-label-primary, #334155);
2889
+ font-size: 11px;
2890
+ padding: 4px 9px;
2891
+ border-radius: 5px;
1251
2892
  cursor: pointer;
1252
- font-size: 13px;
2893
+ transition: all 0.15s;
2894
+ display: flex;
2895
+ align-items: center;
2896
+ gap: 4px;
2897
+ white-space: nowrap;
2898
+ flex-shrink: 0;
2899
+ }
2900
+ body[data-ds-dark-theme] .dsh-preview-tool-btn {
2901
+ background: var(--dsw-alias-bg-layer-2, #1e2433);
2902
+ border-color: var(--dsw-alias-border-l2, #2d3748);
2903
+ color: var(--dsw-alias-label-secondary, #cbd5e1);
2904
+ }
2905
+ .dsh-preview-tool-btn:hover {
2906
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
2907
+ color: var(--dsw-static-deepseek-500, #4176e6);
2908
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
2909
+ }
2910
+ body[data-ds-dark-theme] .dsh-preview-tool-btn:hover {
2911
+ background: var(--dsw-alias-bg-layer-3, #293347);
2912
+ color: #fff;
2913
+ }
2914
+ .dsh-preview-tool-btn.active {
2915
+ background: rgba(65, 118, 230, 0.12);
2916
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
2917
+ color: var(--dsw-static-deepseek-500, #4176e6);
2918
+ font-weight: 600;
2919
+ }
2920
+ .dsh-preview-tool-btn-accent {
2921
+ background: rgba(65, 118, 230, 0.1);
2922
+ border-color: rgba(65, 118, 230, 0.35);
2923
+ color: var(--dsw-static-deepseek-500, #2563eb);
1253
2924
  font-weight: 500;
1254
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1255
- user-select: none;
1256
2925
  }
1257
- body[data-ds-dark-theme] .dsh-preview-float-pill {
1258
- background: var(--dsw-alias-bg-layer-2, #1e222d);
1259
- color: var(--dsw-alias-label-primary, #e2e8f0);
2926
+ body[data-ds-dark-theme] .dsh-preview-tool-btn-accent {
2927
+ background: rgba(65, 118, 230, 0.15);
2928
+ border-color: rgba(65, 118, 230, 0.4);
2929
+ color: #60a5fa;
2930
+ }
2931
+ .dsh-preview-tool-btn-accent:hover {
2932
+ background: var(--dsw-static-deepseek-500, #4176e6) !important;
2933
+ color: #fff !important;
2934
+ }
2935
+
2936
+ /* Search Bar */
2937
+ .dsh-preview-search-bar {
2938
+ display: flex;
2939
+ align-items: center;
2940
+ gap: 8px;
2941
+ padding: 6px 12px;
2942
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
2943
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2944
+ }
2945
+ body[data-ds-dark-theme] .dsh-preview-search-bar {
2946
+ background: var(--dsw-alias-bg-layer-3, #0d1017);
2947
+ border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
2948
+ }
2949
+ .dsh-preview-search-input {
2950
+ flex: 1;
2951
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
2952
+ border: 1px solid var(--dsw-alias-border-l3, #cbd5e1);
2953
+ color: var(--dsw-alias-label-primary, #0f172a);
2954
+ font-size: 12px;
2955
+ padding: 4px 10px;
2956
+ border-radius: 4px;
2957
+ outline: none;
2958
+ }
2959
+ body[data-ds-dark-theme] .dsh-preview-search-input {
2960
+ background: var(--dsw-alias-bg-layer-2, #1a1f2c);
1260
2961
  border-color: var(--dsw-alias-border-l3, #334155);
1261
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
2962
+ color: #f1f5f9;
1262
2963
  }
1263
- .dsh-preview-float-pill:hover {
1264
- transform: translateY(-2px);
2964
+ .dsh-preview-search-input:focus {
1265
2965
  border-color: var(--dsw-static-deepseek-500, #4176e6);
1266
- box-shadow: 0 10px 24px rgba(65, 118, 230, 0.25);
1267
2966
  }
1268
- .dsh-float-pill-badge {
1269
- background: var(--dsw-static-deepseek-500, #4176e6);
1270
- color: #fff;
1271
- font-size: 11px;
1272
- padding: 1px 6px;
1273
- border-radius: 10px;
1274
- font-weight: 600;
2967
+ .dsh-search-clear-btn {
2968
+ background: none;
2969
+ border: none;
2970
+ color: var(--dsw-alias-label-secondary, #64748b);
2971
+ cursor: pointer;
2972
+ }
2973
+ mark.dsh-search-highlight {
2974
+ background: #fde047;
2975
+ color: #000;
2976
+ padding: 1px 3px;
2977
+ border-radius: 2px;
1275
2978
  }
1276
2979
 
1277
- /* Right-Side Drawer Container */
1278
- .dsh-preview-drawer-container {
1279
- position: fixed;
1280
- top: 0;
1281
- right: 0;
1282
- bottom: 0;
1283
- height: 100vh;
1284
- z-index: 9995;
1285
- background: var(--dsw-alias-bg-layer-1, #ffffff);
1286
- color: var(--dsw-alias-label-primary, #0f172a);
1287
- border-left: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
1288
- box-shadow: -8px 0 32px rgba(0, 0, 0, 0.14);
2980
+ /* Body Layout & TOC */
2981
+ .dsh-preview-body-layout {
1289
2982
  display: flex;
1290
- flex-direction: column;
1291
- box-sizing: border-box;
2983
+ flex: 1;
1292
2984
  overflow: hidden;
1293
- animation: dshPreviewSlideIn 0.24s cubic-bezier(0.16, 1, 0.3, 1);
1294
- }
1295
- body[data-ds-dark-theme] .dsh-preview-drawer-container {
1296
- background: var(--dsw-alias-bg-layer-1, #161922);
1297
- color: var(--dsw-alias-label-primary, #e2e8f0);
1298
- border-left-color: var(--dsw-alias-border-l3, #2d3748);
1299
- box-shadow: -12px 0 40px rgba(0, 0, 0, 0.45);
1300
- }
1301
- .dsh-preview-drawer-container.dragging {
1302
- user-select: none;
1303
- transition: none;
2985
+ position: relative;
1304
2986
  }
1305
- @keyframes dshPreviewSlideIn {
1306
- from { transform: translateX(100%); }
1307
- to { transform: translateX(0); }
2987
+ .dsh-preview-toc-sidebar {
2988
+ width: 220px;
2989
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
2990
+ border-right: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2991
+ display: flex;
2992
+ flex-direction: column;
2993
+ flex-shrink: 0;
2994
+ animation: dshTocSlide 0.2s ease;
1308
2995
  }
1309
-
1310
- /* Left Resize Handle */
1311
- .dsh-preview-drag-handle {
1312
- position: absolute;
1313
- top: 0;
1314
- left: -4px;
1315
- bottom: 0;
1316
- width: 8px;
1317
- cursor: col-resize;
1318
- z-index: 100;
1319
- transition: background 0.15s;
2996
+ body[data-ds-dark-theme] .dsh-preview-toc-sidebar {
2997
+ background: var(--dsw-alias-bg-layer-2, #161922);
2998
+ border-right-color: var(--dsw-alias-border-l2, #262c3b);
1320
2999
  }
1321
- .dsh-preview-drag-handle:hover,
1322
- .dsh-preview-drawer-container.dragging .dsh-preview-drag-handle {
1323
- background: var(--dsw-static-deepseek-500, #4176e6);
3000
+ @keyframes dshTocSlide {
3001
+ from { width: 0; opacity: 0; }
3002
+ to { width: 220px; opacity: 1; }
1324
3003
  }
1325
-
1326
- /* Header & FileTabs */
1327
- .dsh-preview-drawer-header {
3004
+ .dsh-preview-toc-header {
1328
3005
  display: flex;
1329
3006
  align-items: center;
1330
3007
  justify-content: space-between;
1331
- height: 38px;
1332
- min-height: 38px;
1333
- flex-shrink: 0;
1334
- background: var(--dsw-alias-bg-layer-2, #f1f5f9);
3008
+ padding: 10px 12px;
1335
3009
  border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1336
- padding: 0 8px;
1337
- overflow: hidden;
1338
- box-sizing: border-box;
3010
+ color: var(--dsw-alias-label-secondary, #64748b);
1339
3011
  }
1340
- body[data-ds-dark-theme] .dsh-preview-drawer-header {
1341
- background: var(--dsw-alias-bg-layer-2, #12151d);
1342
- border-bottom-color: var(--dsw-alias-border-l2, #2d3748);
3012
+ body[data-ds-dark-theme] .dsh-preview-toc-header {
3013
+ border-bottom-color: var(--dsw-alias-border-l2, #262c3b);
3014
+ color: var(--dsw-alias-label-secondary, #94a3b8);
1343
3015
  }
1344
- .dsh-preview-tabs-row {
1345
- display: flex;
1346
- align-items: flex-end;
1347
- gap: 4px;
1348
- overflow-x: auto;
1349
- scrollbar-width: none;
3016
+ .dsh-toc-close-btn {
3017
+ background: none;
3018
+ border: none;
3019
+ color: #94a3b8;
3020
+ cursor: pointer;
3021
+ }
3022
+ .dsh-preview-toc-list {
1350
3023
  flex: 1;
1351
- height: 100%;
1352
- padding-top: 4px;
3024
+ overflow-y: auto;
3025
+ padding: 8px 4px;
1353
3026
  }
1354
- .dsh-preview-tabs-row::-webkit-scrollbar { display: none; }
1355
-
1356
- .dsh-preview-tab-item {
3027
+ .dsh-toc-item {
1357
3028
  display: flex;
1358
3029
  align-items: center;
1359
3030
  gap: 6px;
1360
- padding: 0 10px;
1361
- height: 30px;
1362
- border-radius: 6px 6px 0 0;
3031
+ padding: 5px 8px;
3032
+ border-radius: 4px;
1363
3033
  font-size: 12px;
1364
3034
  color: var(--dsw-alias-label-secondary, #64748b);
1365
- background: transparent;
1366
3035
  cursor: pointer;
1367
- user-select: none;
1368
3036
  white-space: nowrap;
1369
- max-width: 180px;
1370
- transition: all 0.15s;
1371
- border: 1px solid transparent;
1372
- border-bottom: none;
3037
+ overflow: hidden;
3038
+ text-overflow: ellipsis;
1373
3039
  }
1374
- .dsh-preview-tab-item:hover {
1375
- background: rgba(0, 0, 0, 0.04);
3040
+ body[data-ds-dark-theme] .dsh-toc-item {
3041
+ color: var(--dsw-alias-label-secondary, #94a3b8);
3042
+ }
3043
+ .dsh-toc-item:hover {
3044
+ background: var(--dsw-alias-bg-layer-3, #f1f5f9);
3045
+ color: var(--dsw-static-deepseek-500, #4176e6);
3046
+ }
3047
+ body[data-ds-dark-theme] .dsh-toc-item:hover {
3048
+ background: var(--dsw-alias-bg-layer-3, #222938);
3049
+ }
3050
+ .dsh-toc-level-1 { padding-left: 8px; font-weight: 600; color: var(--dsw-alias-label-primary, #0f172a); }
3051
+ body[data-ds-dark-theme] .dsh-toc-level-1 { color: #f1f5f9; }
3052
+ .dsh-toc-level-2 { padding-left: 18px; }
3053
+ .dsh-toc-level-3 { padding-left: 28px; font-size: 11px; }
3054
+ .dsh-toc-level-4 { padding-left: 36px; font-size: 11px; }
3055
+ .dsh-toc-bullet { opacity: 0.5; }
3056
+
3057
+ /* Main Scroll Content */
3058
+ .dsh-preview-main-scroll {
3059
+ flex: 1;
3060
+ overflow-y: auto;
3061
+ padding: 24px 32px;
3062
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
3063
+ }
3064
+ body[data-ds-dark-theme] .dsh-preview-main-scroll {
3065
+ background: var(--dsw-alias-bg-layer-1, #161922);
3066
+ }
3067
+
3068
+ /* Status Footer */
3069
+ .dsh-preview-footer {
3070
+ display: flex;
3071
+ align-items: center;
3072
+ justify-content: space-between;
3073
+ height: 28px;
3074
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
3075
+ border-top: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3076
+ padding: 0 14px;
3077
+ font-size: 11px;
3078
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
3079
+ }
3080
+ body[data-ds-dark-theme] .dsh-preview-footer {
3081
+ background: var(--dsw-alias-bg-layer-2, #161922);
3082
+ border-top-color: var(--dsw-alias-border-l2, #252d3d);
3083
+ color: var(--dsw-alias-label-tertiary, #64748b);
3084
+ }
3085
+ .dsh-footer-left {
3086
+ display: flex;
3087
+ align-items: center;
3088
+ gap: 8px;
3089
+ overflow: hidden;
3090
+ text-overflow: ellipsis;
3091
+ white-space: nowrap;
3092
+ flex: 1;
3093
+ }
3094
+ .dsh-footer-ext {
3095
+ background: var(--dsw-alias-bg-layer-3, #e2e8f0);
3096
+ color: var(--dsw-alias-label-secondary, #475569);
3097
+ padding: 1px 4px;
3098
+ border-radius: 3px;
3099
+ font-size: 10px;
3100
+ font-weight: 600;
3101
+ }
3102
+ body[data-ds-dark-theme] .dsh-footer-ext {
3103
+ background: var(--dsw-alias-bg-layer-3, #262e3d);
3104
+ color: #94a3b8;
3105
+ }
3106
+ .dsh-footer-path {
3107
+ overflow: hidden;
3108
+ text-overflow: ellipsis;
3109
+ }
3110
+ .dsh-footer-right {
3111
+ display: flex;
3112
+ gap: 12px;
3113
+ flex-shrink: 0;
3114
+ }
3115
+ .dsh-footer-stat {
3116
+ color: var(--dsw-alias-label-secondary, #64748b);
3117
+ }
3118
+ body[data-ds-dark-theme] .dsh-footer-stat {
3119
+ color: var(--dsw-alias-label-secondary, #94a3b8);
3120
+ }
3121
+
3122
+ /* Markdown Typography Styles */
3123
+ .dsh-markdown-body {
3124
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
3125
+ font-size: 14px;
3126
+ line-height: 1.72;
3127
+ color: var(--dsw-alias-label-primary, #1e293b);
3128
+ word-break: break-word;
3129
+ }
3130
+ body[data-ds-dark-theme] .dsh-markdown-body {
3131
+ color: var(--dsw-alias-label-primary, #e2e8f0);
3132
+ }
3133
+ .dsh-markdown-body h1,
3134
+ .dsh-markdown-body h2,
3135
+ .dsh-markdown-body h3,
3136
+ .dsh-markdown-body h4,
3137
+ .dsh-markdown-body h5,
3138
+ .dsh-markdown-body h6 {
3139
+ position: relative;
1376
3140
  color: var(--dsw-alias-label-primary, #0f172a);
3141
+ font-weight: 600;
3142
+ margin-top: 1.5em;
3143
+ margin-bottom: 0.6em;
3144
+ line-height: 1.35;
1377
3145
  }
1378
- body[data-ds-dark-theme] .dsh-preview-tab-item:hover {
1379
- background: rgba(255, 255, 255, 0.05);
1380
- color: var(--dsw-alias-label-primary, #f1f5f9);
3146
+ body[data-ds-dark-theme] .dsh-markdown-body h1,
3147
+ body[data-ds-dark-theme] .dsh-markdown-body h2,
3148
+ body[data-ds-dark-theme] .dsh-markdown-body h3,
3149
+ body[data-ds-dark-theme] .dsh-markdown-body h4,
3150
+ body[data-ds-dark-theme] .dsh-markdown-body h5,
3151
+ body[data-ds-dark-theme] .dsh-markdown-body h6 {
3152
+ color: #f8fafc;
1381
3153
  }
1382
- .dsh-preview-tab-item.active {
1383
- background: var(--dsw-alias-bg-layer-1, #ffffff);
3154
+ .dsh-markdown-body h1 { font-size: 1.85em; border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0); padding-bottom: 0.3em; }
3155
+ .dsh-markdown-body h2 { font-size: 1.45em; border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0); padding-bottom: 0.25em; }
3156
+ body[data-ds-dark-theme] .dsh-markdown-body h1 { border-bottom-color: var(--dsw-alias-border-l2, #334155); }
3157
+ body[data-ds-dark-theme] .dsh-markdown-body h2 { border-bottom-color: var(--dsw-alias-border-l2, #273344); }
3158
+ .dsh-markdown-body h3 { font-size: 1.25em; }
3159
+ .dsh-markdown-body h4 { font-size: 1.1em; }
3160
+ .dsh-markdown-body h5 { font-size: 0.95em; }
3161
+ .dsh-markdown-body h6 { font-size: 0.88em; color: var(--dsw-alias-label-secondary, #64748b); }
3162
+ .dsh-md-anchor {
3163
+ position: absolute;
3164
+ left: -18px;
3165
+ opacity: 0;
1384
3166
  color: var(--dsw-static-deepseek-500, #4176e6);
1385
- font-weight: 600;
1386
- border-color: var(--dsw-alias-border-l2, #e2e8f0);
1387
- border-bottom: 2px solid var(--dsw-static-deepseek-500, #4176e6);
1388
- box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.03);
1389
- }
1390
- body[data-ds-dark-theme] .dsh-preview-tab-item.active {
1391
- background: var(--dsw-alias-bg-layer-1, #161922);
1392
- border-color: var(--dsw-alias-border-l2, #2d3748);
1393
- border-bottom: 2px solid var(--dsw-static-deepseek-500, #4176e6);
1394
- box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.2);
3167
+ text-decoration: none;
3168
+ transition: opacity 0.15s;
1395
3169
  }
1396
- .dsh-tab-icon {
1397
- font-size: 12px;
3170
+ .dsh-markdown-body h1:hover .dsh-md-anchor,
3171
+ .dsh-markdown-body h2:hover .dsh-md-anchor,
3172
+ .dsh-markdown-body h3:hover .dsh-md-anchor {
3173
+ opacity: 1;
1398
3174
  }
1399
- .dsh-tab-title {
1400
- overflow: hidden;
1401
- text-overflow: ellipsis;
1402
- white-space: nowrap;
3175
+ .dsh-md-p {
3176
+ margin-bottom: 1em;
1403
3177
  }
1404
- .dsh-tab-close-btn {
1405
- background: none;
3178
+ .dsh-md-hr {
1406
3179
  border: none;
1407
- color: var(--dsw-alias-label-tertiary, #94a3b8);
1408
- font-size: 11px;
1409
- cursor: pointer;
1410
- padding: 2px 4px;
1411
- border-radius: 4px;
1412
- line-height: 1;
3180
+ height: 1px;
3181
+ background: var(--dsw-alias-border-l2, #e2e8f0);
3182
+ margin: 2em 0;
1413
3183
  }
1414
- .dsh-tab-close-btn:hover {
1415
- background: rgba(239, 68, 68, 0.15);
1416
- color: #ef4444;
3184
+ body[data-ds-dark-theme] .dsh-md-hr {
3185
+ background: var(--dsw-alias-border-l2, #334155);
1417
3186
  }
1418
-
1419
- .dsh-preview-window-controls {
1420
- display: flex;
1421
- align-items: center;
1422
- gap: 6px;
1423
- padding-left: 8px;
1424
- height: 100%;
3187
+ .dsh-md-blockquote {
3188
+ margin: 1.2em 0;
3189
+ padding: 8px 16px;
3190
+ border-left: 4px solid var(--dsw-static-deepseek-500, #4176e6);
3191
+ background: rgba(65, 118, 230, 0.05);
3192
+ border-radius: 0 6px 6px 0;
3193
+ color: var(--dsw-alias-label-secondary, #475569);
1425
3194
  }
1426
- .dsh-win-ctrl-btn {
1427
- background: transparent;
1428
- border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
1429
- color: var(--dsw-alias-label-secondary, #64748b);
1430
- font-size: 11px;
1431
- padding: 3px 8px;
1432
- border-radius: 4px;
1433
- cursor: pointer;
1434
- transition: all 0.15s;
3195
+ body[data-ds-dark-theme] .dsh-md-blockquote {
3196
+ color: var(--dsw-alias-label-secondary, #cbd5e1);
3197
+ background: rgba(65, 118, 230, 0.08);
1435
3198
  }
1436
- .dsh-win-ctrl-btn:hover {
1437
- background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1438
- color: var(--dsw-alias-label-primary, #0f172a);
3199
+ .dsh-md-blockquote p { margin: 0.3em 0; }
3200
+
3201
+ /* Tables */
3202
+ .dsh-md-table-wrapper {
3203
+ width: 100%;
3204
+ overflow-x: auto;
3205
+ margin: 1.2em 0;
3206
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3207
+ border-radius: 8px;
1439
3208
  }
1440
- body[data-ds-dark-theme] .dsh-win-ctrl-btn {
3209
+ body[data-ds-dark-theme] .dsh-md-table-wrapper {
1441
3210
  border-color: var(--dsw-alias-border-l2, #334155);
1442
- color: var(--dsw-alias-label-secondary, #94a3b8);
1443
3211
  }
1444
- body[data-ds-dark-theme] .dsh-win-ctrl-btn:hover {
1445
- background: var(--dsw-alias-bg-layer-3, #283042);
1446
- color: var(--dsw-alias-label-primary, #f1f5f9);
3212
+ .dsh-md-table-wrapper table {
3213
+ width: 100%;
3214
+ border-collapse: collapse;
3215
+ font-size: 13px;
1447
3216
  }
1448
- .dsh-win-ctrl-close:hover {
1449
- background: #ef4444 !important;
1450
- border-color: #ef4444 !important;
1451
- color: #fff !important;
3217
+ .dsh-md-table-wrapper th,
3218
+ .dsh-md-table-wrapper td {
3219
+ padding: 8px 14px;
3220
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1452
3221
  }
1453
-
1454
- /* Content Container */
1455
- .dsh-preview-content-container {
1456
- display: flex;
1457
- flex-direction: column;
1458
- flex: 1;
1459
- min-height: 0;
1460
- overflow: hidden;
1461
- box-sizing: border-box;
3222
+ body[data-ds-dark-theme] .dsh-md-table-wrapper th,
3223
+ body[data-ds-dark-theme] .dsh-md-table-wrapper td {
3224
+ border-color: var(--dsw-alias-border-l2, #273344);
1462
3225
  }
1463
-
1464
- /* Toolbar */
1465
- .dsh-preview-toolbar {
1466
- display: flex;
1467
- align-items: center;
1468
- justify-content: space-between;
1469
- padding: 6px 12px;
1470
- height: 38px;
1471
- min-height: 38px;
1472
- flex-shrink: 0;
3226
+ .dsh-md-table-wrapper th {
1473
3227
  background: var(--dsw-alias-bg-layer-2, #f8fafc);
1474
- border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1475
- gap: 10px;
1476
- box-sizing: border-box;
1477
- }
1478
- body[data-ds-dark-theme] .dsh-preview-toolbar {
1479
- background: var(--dsw-alias-bg-layer-2, #161a24);
1480
- border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
1481
- }
1482
- .dsh-preview-toolbar-left,
1483
- .dsh-preview-toolbar-right {
1484
- display: flex;
1485
- align-items: center;
1486
- gap: 6px;
1487
- flex-shrink: 0;
1488
- white-space: nowrap;
1489
- }
1490
-
1491
- .dsh-preview-mode-group {
1492
- display: flex;
1493
- background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1494
- padding: 2px;
1495
- border-radius: 6px;
1496
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1497
- flex-shrink: 0;
3228
+ font-weight: 600;
3229
+ color: var(--dsw-alias-label-primary, #0f172a);
1498
3230
  }
1499
- body[data-ds-dark-theme] .dsh-preview-mode-group {
1500
- background: var(--dsw-alias-bg-layer-3, #0d1017);
1501
- border-color: var(--dsw-alias-border-l2, #252d3d);
3231
+ body[data-ds-dark-theme] .dsh-md-table-wrapper th {
3232
+ background: var(--dsw-alias-bg-layer-2, #1a202c);
3233
+ color: #f1f5f9;
1502
3234
  }
1503
- .dsh-preview-mode-btn {
1504
- background: transparent;
1505
- border: none;
1506
- color: var(--dsw-alias-label-secondary, #64748b);
1507
- font-size: 11px;
1508
- padding: 4px 9px;
1509
- border-radius: 4px;
1510
- cursor: pointer;
1511
- transition: all 0.15s;
1512
- white-space: nowrap;
1513
- flex-shrink: 0;
3235
+ .dsh-md-table-wrapper tr:nth-child(even) {
3236
+ background: rgba(0, 0, 0, 0.015);
1514
3237
  }
1515
- body[data-ds-dark-theme] .dsh-preview-mode-btn {
1516
- color: var(--dsw-alias-label-secondary, #94a3b8);
3238
+ body[data-ds-dark-theme] .dsh-md-table-wrapper tr:nth-child(even) {
3239
+ background: rgba(255, 255, 255, 0.02);
1517
3240
  }
1518
- .dsh-preview-mode-btn.active {
1519
- background: var(--dsw-alias-bg-layer-1, #ffffff);
1520
- color: var(--dsw-alias-label-primary, #0f172a);
1521
- font-weight: 600;
1522
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
3241
+
3242
+ /* Lists & Tasks */
3243
+ .dsh-md-list {
3244
+ padding-left: 24px;
3245
+ margin: 0.8em 0 1.2em 0;
1523
3246
  }
1524
- body[data-ds-dark-theme] .dsh-preview-mode-btn.active {
1525
- background: var(--dsw-alias-bg-layer-2, #1e2433);
1526
- color: var(--dsw-alias-label-primary, #f8fafc);
1527
- box-shadow: 0 1px 4px rgba(0,0,0,0.25);
3247
+ .dsh-md-list li { margin-bottom: 0.35em; }
3248
+ .dsh-md-task-list {
3249
+ list-style: none;
3250
+ padding-left: 0;
1528
3251
  }
1529
- .dsh-preview-tool-btn {
1530
- background: var(--dsw-alias-bg-layer-1, #ffffff);
1531
- border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
1532
- color: var(--dsw-alias-label-primary, #334155);
1533
- font-size: 11px;
1534
- padding: 4px 9px;
1535
- border-radius: 5px;
1536
- cursor: pointer;
1537
- transition: all 0.15s;
3252
+ .dsh-md-task-item {
1538
3253
  display: flex;
1539
3254
  align-items: center;
1540
- gap: 4px;
1541
- white-space: nowrap;
1542
- flex-shrink: 0;
3255
+ gap: 8px;
3256
+ margin-bottom: 0.4em;
1543
3257
  }
1544
- body[data-ds-dark-theme] .dsh-preview-tool-btn {
1545
- background: var(--dsw-alias-bg-layer-2, #1e2433);
1546
- border-color: var(--dsw-alias-border-l2, #2d3748);
1547
- color: var(--dsw-alias-label-secondary, #cbd5e1);
3258
+ .dsh-md-task-item input[type="checkbox"] {
3259
+ accent-color: var(--dsw-static-deepseek-500, #4176e6);
3260
+ cursor: pointer;
1548
3261
  }
1549
- .dsh-preview-tool-btn:hover {
3262
+
3263
+ /* Inline Code & Badges */
3264
+ .dsh-md-inline-code {
1550
3265
  background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1551
- color: var(--dsw-static-deepseek-500, #4176e6);
1552
- border-color: var(--dsw-static-deepseek-500, #4176e6);
1553
- }
1554
- body[data-ds-dark-theme] .dsh-preview-tool-btn:hover {
1555
- background: var(--dsw-alias-bg-layer-3, #293347);
1556
- color: #fff;
3266
+ color: var(--dsw-static-deepseek-500, #2563eb);
3267
+ padding: 2px 6px;
3268
+ border-radius: 4px;
3269
+ font-family: "Cascadia Code", Consolas, "Courier New", monospace;
3270
+ font-size: 0.88em;
3271
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1557
3272
  }
1558
- .dsh-preview-tool-btn.active {
1559
- background: rgba(65, 118, 230, 0.12);
1560
- border-color: var(--dsw-static-deepseek-500, #4176e6);
1561
- color: var(--dsw-static-deepseek-500, #4176e6);
1562
- font-weight: 600;
3273
+ body[data-ds-dark-theme] .dsh-md-inline-code {
3274
+ background: var(--dsw-alias-bg-layer-3, #1e2433);
3275
+ color: #93c5fd;
3276
+ border-color: rgba(147, 197, 253, 0.15);
1563
3277
  }
1564
- .dsh-preview-tool-btn-accent {
1565
- background: rgba(65, 118, 230, 0.1);
1566
- border-color: rgba(65, 118, 230, 0.35);
3278
+ .dsh-md-link {
1567
3279
  color: var(--dsw-static-deepseek-500, #2563eb);
1568
- font-weight: 500;
3280
+ text-decoration: none;
3281
+ border-bottom: 1px solid rgba(37, 99, 235, 0.3);
3282
+ transition: border-color 0.15s;
1569
3283
  }
1570
- body[data-ds-dark-theme] .dsh-preview-tool-btn-accent {
1571
- background: rgba(65, 118, 230, 0.15);
1572
- border-color: rgba(65, 118, 230, 0.4);
1573
- color: #60a5fa;
3284
+ body[data-ds-dark-theme] .dsh-md-link {
3285
+ color: var(--dsw-static-deepseek-500, #60a5fa);
3286
+ border-bottom-color: rgba(96, 165, 250, 0.3);
1574
3287
  }
1575
- .dsh-preview-tool-btn-accent:hover {
1576
- background: var(--dsw-static-deepseek-500, #4176e6) !important;
1577
- color: #fff !important;
3288
+ .dsh-md-link:hover {
3289
+ border-color: currentColor;
1578
3290
  }
1579
3291
 
1580
- /* Search Bar */
1581
- .dsh-preview-search-bar {
3292
+ /* Code Cards */
3293
+ .dsh-code-card {
3294
+ margin: 1.4em 0;
3295
+ background: var(--dsw-alias-bg-layer-3, #f8fafc);
3296
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3297
+ border-radius: 8px;
3298
+ overflow: hidden;
3299
+ }
3300
+ body[data-ds-dark-theme] .dsh-code-card {
3301
+ background: #0d1117;
3302
+ border-color: #30363d;
3303
+ }
3304
+ .dsh-code-card-header {
1582
3305
  display: flex;
1583
3306
  align-items: center;
1584
- gap: 8px;
3307
+ justify-content: space-between;
1585
3308
  padding: 6px 12px;
1586
- background: var(--dsw-alias-bg-layer-3, #f1f5f9);
3309
+ background: rgba(0, 0, 0, 0.03);
1587
3310
  border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1588
3311
  }
1589
- body[data-ds-dark-theme] .dsh-preview-search-bar {
1590
- background: var(--dsw-alias-bg-layer-3, #0d1017);
1591
- border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
1592
- }
1593
- .dsh-preview-search-input {
1594
- flex: 1;
1595
- background: var(--dsw-alias-bg-layer-1, #ffffff);
1596
- border: 1px solid var(--dsw-alias-border-l3, #cbd5e1);
1597
- color: var(--dsw-alias-label-primary, #0f172a);
1598
- font-size: 12px;
1599
- padding: 4px 10px;
1600
- border-radius: 4px;
1601
- outline: none;
3312
+ body[data-ds-dark-theme] .dsh-code-card-header {
3313
+ background: rgba(255, 255, 255, 0.04);
3314
+ border-bottom-color: #21262d;
1602
3315
  }
1603
- body[data-ds-dark-theme] .dsh-preview-search-input {
1604
- background: var(--dsw-alias-bg-layer-2, #1a1f2c);
1605
- border-color: var(--dsw-alias-border-l3, #334155);
1606
- color: #f1f5f9;
3316
+ .dsh-code-lang-badge {
3317
+ font-family: monospace;
3318
+ font-size: 11px;
3319
+ color: var(--dsw-alias-label-secondary, #64748b);
3320
+ font-weight: 600;
1607
3321
  }
1608
- .dsh-preview-search-input:focus {
1609
- border-color: var(--dsw-static-deepseek-500, #4176e6);
3322
+ body[data-ds-dark-theme] .dsh-code-lang-badge {
3323
+ color: #8b949e;
1610
3324
  }
1611
- .dsh-search-clear-btn {
1612
- background: none;
1613
- border: none;
1614
- color: var(--dsw-alias-label-secondary, #64748b);
3325
+ .dsh-code-copy-btn {
3326
+ background: transparent;
3327
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
3328
+ color: var(--dsw-alias-label-secondary, #475569);
3329
+ font-size: 11px;
3330
+ padding: 2px 8px;
3331
+ border-radius: 4px;
1615
3332
  cursor: pointer;
3333
+ transition: all 0.15s;
1616
3334
  }
1617
- mark.dsh-search-highlight {
1618
- background: #fde047;
1619
- color: #000;
1620
- padding: 1px 3px;
1621
- border-radius: 2px;
3335
+ body[data-ds-dark-theme] .dsh-code-copy-btn {
3336
+ border-color: #30363d;
3337
+ color: #c9d1d9;
1622
3338
  }
1623
-
1624
- /* Body Layout & TOC */
1625
- .dsh-preview-body-layout {
1626
- display: flex;
1627
- flex: 1;
1628
- overflow: hidden;
1629
- position: relative;
3339
+ .dsh-code-copy-btn:hover {
3340
+ background: var(--dsw-alias-bg-layer-2, #e2e8f0);
3341
+ color: var(--dsw-static-deepseek-500, #2563eb);
1630
3342
  }
1631
- .dsh-preview-toc-sidebar {
1632
- width: 220px;
1633
- background: var(--dsw-alias-bg-layer-2, #f8fafc);
1634
- border-right: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1635
- display: flex;
1636
- flex-direction: column;
1637
- flex-shrink: 0;
1638
- animation: dshTocSlide 0.2s ease;
3343
+ body[data-ds-dark-theme] .dsh-code-copy-btn:hover {
3344
+ background: #21262d;
3345
+ color: #58a6ff;
1639
3346
  }
1640
- body[data-ds-dark-theme] .dsh-preview-toc-sidebar {
1641
- background: var(--dsw-alias-bg-layer-2, #161922);
1642
- border-right-color: var(--dsw-alias-border-l2, #262c3b);
3347
+ .dsh-code-pre {
3348
+ margin: 0;
3349
+ padding: 14px 16px;
3350
+ overflow-x: auto;
3351
+ font-family: "Fira Code", "Cascadia Code", Consolas, monospace;
3352
+ font-size: 13px;
3353
+ line-height: 1.55;
3354
+ color: var(--dsw-alias-label-primary, #0f172a);
1643
3355
  }
1644
- @keyframes dshTocSlide {
1645
- from { width: 0; opacity: 0; }
1646
- to { width: 220px; opacity: 1; }
3356
+ body[data-ds-dark-theme] .dsh-code-pre {
3357
+ color: #e6edf3;
1647
3358
  }
1648
- .dsh-preview-toc-header {
3359
+ .dsh-token-keyword { color: #d73a49; font-weight: 500; }
3360
+ body[data-ds-dark-theme] .dsh-token-keyword { color: #ff7b72; }
3361
+ .dsh-token-string { color: #032f62; }
3362
+ body[data-ds-dark-theme] .dsh-token-string { color: #a5d6ff; }
3363
+ .dsh-token-comment { color: #6a737d; font-style: italic; }
3364
+ body[data-ds-dark-theme] .dsh-token-comment { color: #8b949e; }
3365
+ .dsh-token-number { color: #005cc5; }
3366
+ body[data-ds-dark-theme] .dsh-token-number { color: #79c0ff; }
3367
+
3368
+ /* GitHub Alert Callouts */
3369
+ .dsh-alert {
3370
+ margin: 1.4em 0;
3371
+ padding: 12px 16px;
3372
+ border-left: 4px solid;
3373
+ border-radius: 0 8px 8px 0;
3374
+ }
3375
+ .dsh-alert-title {
1649
3376
  display: flex;
1650
3377
  align-items: center;
1651
- justify-content: space-between;
1652
- padding: 10px 12px;
1653
- border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1654
- color: var(--dsw-alias-label-secondary, #64748b);
3378
+ gap: 6px;
3379
+ font-weight: 600;
3380
+ font-size: 13px;
3381
+ margin-bottom: 6px;
1655
3382
  }
1656
- body[data-ds-dark-theme] .dsh-preview-toc-header {
1657
- border-bottom-color: var(--dsw-alias-border-l2, #262c3b);
1658
- color: var(--dsw-alias-label-secondary, #94a3b8);
3383
+ .dsh-alert-note { background: rgba(59, 130, 246, 0.08); border-color: #3b82f6; }
3384
+ .dsh-alert-note .dsh-alert-title { color: #2563eb; }
3385
+ body[data-ds-dark-theme] .dsh-alert-note .dsh-alert-title { color: #60a5fa; }
3386
+ .dsh-alert-tip { background: rgba(16, 185, 129, 0.08); border-color: #10b981; }
3387
+ .dsh-alert-tip .dsh-alert-title { color: #059669; }
3388
+ body[data-ds-dark-theme] .dsh-alert-tip .dsh-alert-title { color: #34d399; }
3389
+ .dsh-alert-important { background: rgba(168, 85, 247, 0.08); border-color: #a855f7; }
3390
+ .dsh-alert-important .dsh-alert-title { color: #7c3aed; }
3391
+ body[data-ds-dark-theme] .dsh-alert-important .dsh-alert-title { color: #c084fc; }
3392
+ .dsh-alert-warning { background: rgba(245, 158, 11, 0.08); border-color: #f59e0b; }
3393
+ .dsh-alert-warning .dsh-alert-title { color: #d97706; }
3394
+ body[data-ds-dark-theme] .dsh-alert-warning .dsh-alert-title { color: #fbbf24; }
3395
+ .dsh-alert-caution { background: rgba(239, 68, 68, 0.08); border-color: #ef4444; }
3396
+ .dsh-alert-caution .dsh-alert-title { color: #dc2626; }
3397
+ body[data-ds-dark-theme] .dsh-alert-caution .dsh-alert-title { color: #f87171; }
3398
+ .dsh-alert-body { font-size: 13px; color: var(--dsw-alias-label-secondary, #475569); }
3399
+ body[data-ds-dark-theme] .dsh-alert-body { color: #cbd5e1; }
3400
+
3401
+ /* Math Formulas */
3402
+ .dsh-math-block {
3403
+ margin: 1.2em 0;
3404
+ padding: 12px 16px;
3405
+ background: rgba(0, 0, 0, 0.02);
3406
+ border-radius: 6px;
3407
+ border: 1px dashed var(--dsw-alias-border-l2, #cbd5e1);
3408
+ text-align: center;
3409
+ font-family: "KaTeX_Math", "Times New Roman", serif;
3410
+ font-size: 1.1em;
3411
+ color: var(--dsw-alias-label-primary, #1e293b);
1659
3412
  }
1660
- .dsh-toc-close-btn {
1661
- background: none;
1662
- border: none;
1663
- color: #94a3b8;
1664
- cursor: pointer;
3413
+ body[data-ds-dark-theme] .dsh-math-block {
3414
+ background: rgba(255, 255, 255, 0.03);
3415
+ border-color: var(--dsw-alias-border-l2, #334155);
3416
+ color: #cbd5e1;
1665
3417
  }
1666
- .dsh-preview-toc-list {
1667
- flex: 1;
1668
- overflow-y: auto;
1669
- padding: 8px 4px;
3418
+ .dsh-math-inline {
3419
+ font-family: "KaTeX_Math", "Times New Roman", serif;
3420
+ color: var(--dsw-static-deepseek-500, #2563eb);
3421
+ padding: 0 3px;
1670
3422
  }
1671
- .dsh-toc-item {
3423
+ body[data-ds-dark-theme] .dsh-math-inline {
3424
+ color: #93c5fd;
3425
+ }
3426
+
3427
+ /* Source View Mode */
3428
+ .dsh-source-view {
1672
3429
  display: flex;
1673
- align-items: center;
1674
- gap: 6px;
1675
- padding: 5px 8px;
1676
- border-radius: 4px;
1677
- font-size: 12px;
1678
- color: var(--dsw-alias-label-secondary, #64748b);
1679
- cursor: pointer;
1680
- white-space: nowrap;
3430
+ background: var(--dsw-alias-bg-layer-3, #f8fafc);
3431
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3432
+ border-radius: 8px;
1681
3433
  overflow: hidden;
1682
- text-overflow: ellipsis;
3434
+ font-family: "Fira Code", Consolas, monospace;
3435
+ font-size: 13px;
3436
+ line-height: 1.6;
1683
3437
  }
1684
- body[data-ds-dark-theme] .dsh-toc-item {
1685
- color: var(--dsw-alias-label-secondary, #94a3b8);
3438
+ body[data-ds-dark-theme] .dsh-source-view {
3439
+ background: var(--dsw-alias-bg-layer-3, #0d1017);
3440
+ border-color: var(--dsw-alias-border-l2, #252d3d);
1686
3441
  }
1687
- .dsh-toc-item:hover {
1688
- background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1689
- color: var(--dsw-static-deepseek-500, #4176e6);
3442
+ .dsh-source-line-numbers {
3443
+ padding: 16px 8px;
3444
+ background: rgba(0, 0, 0, 0.02);
3445
+ border-right: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3446
+ color: #94a3b8;
3447
+ text-align: right;
3448
+ user-select: none;
1690
3449
  }
1691
- body[data-ds-dark-theme] .dsh-toc-item:hover {
1692
- background: var(--dsw-alias-bg-layer-3, #222938);
3450
+ body[data-ds-dark-theme] .dsh-source-line-numbers {
3451
+ background: rgba(255, 255, 255, 0.02);
3452
+ border-right-color: var(--dsw-alias-border-l2, #252d3d);
3453
+ color: #475569;
3454
+ }
3455
+ .dsh-source-pre {
3456
+ margin: 0;
3457
+ padding: 16px;
3458
+ flex: 1;
3459
+ overflow-x: auto;
3460
+ color: var(--dsw-alias-label-primary, #0f172a);
3461
+ }
3462
+ body[data-ds-dark-theme] .dsh-source-pre {
3463
+ color: #cbd5e1;
1693
3464
  }
1694
- .dsh-toc-level-1 { padding-left: 8px; font-weight: 600; color: var(--dsw-alias-label-primary, #0f172a); }
1695
- body[data-ds-dark-theme] .dsh-toc-level-1 { color: #f1f5f9; }
1696
- .dsh-toc-level-2 { padding-left: 18px; }
1697
- .dsh-toc-level-3 { padding-left: 28px; font-size: 11px; }
1698
- .dsh-toc-level-4 { padding-left: 36px; font-size: 11px; }
1699
- .dsh-toc-bullet { opacity: 0.5; }
1700
3465
 
1701
- /* Main Scroll Content */
1702
- .dsh-preview-main-scroll {
3466
+ /* Split View Mode */
3467
+ .dsh-split-view {
3468
+ display: flex;
3469
+ gap: 12px;
3470
+ height: 100%;
3471
+ }
3472
+ .dsh-split-pane {
1703
3473
  flex: 1;
1704
3474
  overflow-y: auto;
1705
- padding: 24px 32px;
3475
+ border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3476
+ border-radius: 8px;
3477
+ padding: 12px;
1706
3478
  background: var(--dsw-alias-bg-layer-1, #ffffff);
1707
3479
  }
1708
- body[data-ds-dark-theme] .dsh-preview-main-scroll {
1709
- background: var(--dsw-alias-bg-layer-1, #161922);
3480
+ body[data-ds-dark-theme] .dsh-split-pane {
3481
+ border-color: var(--dsw-alias-border-l2, #252d3d);
3482
+ background: var(--dsw-alias-bg-layer-1, #0d1017);
1710
3483
  }
1711
-
1712
- /* Status Footer */
1713
- .dsh-preview-footer {
1714
- display: flex;
1715
- align-items: center;
1716
- justify-content: space-between;
1717
- height: 28px;
1718
- background: var(--dsw-alias-bg-layer-2, #f8fafc);
1719
- border-top: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1720
- padding: 0 14px;
3484
+ .dsh-split-pane-header {
1721
3485
  font-size: 11px;
1722
- color: var(--dsw-alias-label-tertiary, #94a3b8);
3486
+ font-weight: 600;
3487
+ color: var(--dsw-alias-label-secondary, #64748b);
3488
+ margin-bottom: 8px;
3489
+ padding-bottom: 4px;
3490
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1723
3491
  }
1724
- body[data-ds-dark-theme] .dsh-preview-footer {
1725
- background: var(--dsw-alias-bg-layer-2, #161922);
1726
- border-top-color: var(--dsw-alias-border-l2, #252d3d);
1727
- color: var(--dsw-alias-label-tertiary, #64748b);
3492
+ body[data-ds-dark-theme] .dsh-split-pane-header {
3493
+ color: #94a3b8;
3494
+ border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
1728
3495
  }
1729
- .dsh-footer-left {
3496
+
3497
+ /* Loading & Error States */
3498
+ .dsh-preview-loading {
1730
3499
  display: flex;
3500
+ flex-direction: column;
1731
3501
  align-items: center;
1732
- gap: 8px;
1733
- overflow: hidden;
1734
- text-overflow: ellipsis;
1735
- white-space: nowrap;
1736
- flex: 1;
3502
+ justify-content: center;
3503
+ padding: 60px 0;
3504
+ gap: 14px;
3505
+ color: var(--dsw-alias-label-secondary, #64748b);
1737
3506
  }
1738
- .dsh-footer-ext {
1739
- background: var(--dsw-alias-bg-layer-3, #e2e8f0);
1740
- color: var(--dsw-alias-label-secondary, #475569);
1741
- padding: 1px 4px;
1742
- border-radius: 3px;
1743
- font-size: 10px;
1744
- font-weight: 600;
3507
+ .dsh-preview-spinner {
3508
+ width: 32px;
3509
+ height: 32px;
3510
+ border: 3px solid rgba(65, 118, 230, 0.2);
3511
+ border-top-color: var(--dsw-static-deepseek-500, #4176e6);
3512
+ border-radius: 50%;
3513
+ animation: dshSpinner 0.8s linear infinite;
1745
3514
  }
1746
- body[data-ds-dark-theme] .dsh-footer-ext {
1747
- background: var(--dsw-alias-bg-layer-3, #262e3d);
1748
- color: #94a3b8;
3515
+ @keyframes dshSpinner {
3516
+ to { transform: rotate(360deg); }
1749
3517
  }
1750
- .dsh-footer-path {
1751
- overflow: hidden;
1752
- text-overflow: ellipsis;
3518
+
3519
+ .dsh-preview-error-card {
3520
+ padding: 32px 24px;
3521
+ background: rgba(239, 68, 68, 0.06);
3522
+ border: 1px solid rgba(239, 68, 68, 0.25);
3523
+ border-radius: 8px;
3524
+ text-align: center;
3525
+ margin: 30px auto;
3526
+ max-width: 480px;
1753
3527
  }
1754
- .dsh-footer-right {
1755
- display: flex;
1756
- gap: 12px;
1757
- flex-shrink: 0;
3528
+ .dsh-error-icon { font-size: 32px; margin-bottom: 10px; }
3529
+ .dsh-error-title { font-size: 16px; font-weight: 600; color: #ef4444; margin-bottom: 6px; }
3530
+ .dsh-error-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #64748b); margin-bottom: 16px; }
3531
+ body[data-ds-dark-theme] .dsh-error-desc { color: #cbd5e1; }
3532
+ .dsh-error-actions { display: flex; justify-content: center; gap: 10px; }
3533
+ .dsh-btn-retry {
3534
+ background: #ef4444; color: #fff; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px;
1758
3535
  }
1759
- .dsh-footer-stat {
1760
- color: var(--dsw-alias-label-secondary, #64748b);
3536
+ .dsh-btn-native {
3537
+ 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;
1761
3538
  }
1762
- body[data-ds-dark-theme] .dsh-footer-stat {
1763
- color: var(--dsw-alias-label-secondary, #94a3b8);
3539
+ body[data-ds-dark-theme] .dsh-btn-native {
3540
+ background: var(--dsw-alias-bg-layer-2, #1e2433); color: #e2e8f0; border-color: #334155;
1764
3541
  }
1765
3542
 
1766
- /* Markdown Typography Styles */
1767
- .dsh-markdown-body {
1768
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
1769
- font-size: 14px;
1770
- line-height: 1.72;
1771
- color: var(--dsw-alias-label-primary, #1e293b);
1772
- word-break: break-word;
1773
- }
1774
- body[data-ds-dark-theme] .dsh-markdown-body {
1775
- color: var(--dsw-alias-label-primary, #e2e8f0);
1776
- }
1777
- .dsh-markdown-body h1,
1778
- .dsh-markdown-body h2,
1779
- .dsh-markdown-body h3,
1780
- .dsh-markdown-body h4,
1781
- .dsh-markdown-body h5,
1782
- .dsh-markdown-body h6 {
1783
- position: relative;
1784
- color: var(--dsw-alias-label-primary, #0f172a);
1785
- font-weight: 600;
1786
- margin-top: 1.5em;
1787
- margin-bottom: 0.6em;
1788
- line-height: 1.35;
3543
+ /* Toast Popup */
3544
+ .dsh-preview-toast-container {
3545
+ position: fixed;
3546
+ bottom: 24px;
3547
+ left: 50%;
3548
+ transform: translateX(-50%);
3549
+ z-index: 10000;
3550
+ display: flex;
3551
+ flex-direction: column;
3552
+ gap: 8px;
3553
+ pointer-events: none;
1789
3554
  }
1790
- body[data-ds-dark-theme] .dsh-markdown-body h1,
1791
- body[data-ds-dark-theme] .dsh-markdown-body h2,
1792
- body[data-ds-dark-theme] .dsh-markdown-body h3,
1793
- body[data-ds-dark-theme] .dsh-markdown-body h4,
1794
- body[data-ds-dark-theme] .dsh-markdown-body h5,
1795
- body[data-ds-dark-theme] .dsh-markdown-body h6 {
3555
+ .dsh-preview-toast {
3556
+ display: flex;
3557
+ align-items: center;
3558
+ gap: 8px;
3559
+ padding: 8px 16px;
3560
+ background: #1e293b;
1796
3561
  color: #f8fafc;
1797
- }
1798
- .dsh-markdown-body h1 { font-size: 1.85em; border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0); padding-bottom: 0.3em; }
1799
- .dsh-markdown-body h2 { font-size: 1.45em; border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0); padding-bottom: 0.25em; }
1800
- body[data-ds-dark-theme] .dsh-markdown-body h1 { border-bottom-color: var(--dsw-alias-border-l2, #334155); }
1801
- body[data-ds-dark-theme] .dsh-markdown-body h2 { border-bottom-color: var(--dsw-alias-border-l2, #273344); }
1802
- .dsh-markdown-body h3 { font-size: 1.25em; }
1803
- .dsh-markdown-body h4 { font-size: 1.1em; }
1804
- .dsh-markdown-body h5 { font-size: 0.95em; }
1805
- .dsh-markdown-body h6 { font-size: 0.88em; color: var(--dsw-alias-label-secondary, #64748b); }
1806
- .dsh-md-anchor {
1807
- position: absolute;
1808
- left: -18px;
3562
+ border: 1px solid #334155;
3563
+ border-radius: 20px;
3564
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
3565
+ font-size: 12px;
1809
3566
  opacity: 0;
1810
- color: var(--dsw-static-deepseek-500, #4176e6);
1811
- text-decoration: none;
1812
- transition: opacity 0.15s;
3567
+ transform: translateY(10px);
3568
+ transition: all 0.25s ease;
1813
3569
  }
1814
- .dsh-markdown-body h1:hover .dsh-md-anchor,
1815
- .dsh-markdown-body h2:hover .dsh-md-anchor,
1816
- .dsh-markdown-body h3:hover .dsh-md-anchor {
3570
+ .dsh-preview-toast.dsh-preview-toast-show {
1817
3571
  opacity: 1;
3572
+ transform: translateY(0);
1818
3573
  }
1819
- .dsh-md-p {
1820
- margin-bottom: 1em;
1821
- }
1822
- .dsh-md-hr {
1823
- border: none;
1824
- height: 1px;
1825
- background: var(--dsw-alias-border-l2, #e2e8f0);
1826
- margin: 2em 0;
1827
- }
1828
- body[data-ds-dark-theme] .dsh-md-hr {
1829
- background: var(--dsw-alias-border-l2, #334155);
3574
+ .dsh-preview-toast-success { border-color: #22c55e; }
3575
+ .dsh-preview-toast-success .dsh-preview-toast-icon { color: #22c55e; font-weight: bold; }
3576
+ .dsh-preview-toast-error { border-color: #ef4444; }
3577
+ .dsh-preview-toast-error .dsh-preview-toast-icon { color: #ef4444; font-weight: bold; }
3578
+
3579
+ /* Workspace Tree Sidebar & Split Layout */
3580
+ .dsh-preview-drawer-body-split {
3581
+ display: flex;
3582
+ flex-direction: row;
3583
+ flex: 1;
3584
+ min-height: 0;
3585
+ overflow: hidden;
3586
+ width: 100%;
1830
3587
  }
1831
- .dsh-md-blockquote {
1832
- margin: 1.2em 0;
1833
- padding: 8px 16px;
1834
- border-left: 4px solid var(--dsw-static-deepseek-500, #4176e6);
1835
- background: rgba(65, 118, 230, 0.05);
1836
- border-radius: 0 6px 6px 0;
1837
- color: var(--dsw-alias-label-secondary, #475569);
3588
+
3589
+ .dsh-tree-sidebar {
3590
+ display: flex;
3591
+ flex-direction: column;
3592
+ overflow: hidden;
3593
+ flex-shrink: 0;
3594
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
3595
+ border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
3596
+ height: 100%;
3597
+ user-select: none;
3598
+ box-sizing: border-box;
1838
3599
  }
1839
- body[data-ds-dark-theme] .dsh-md-blockquote {
1840
- color: var(--dsw-alias-label-secondary, #cbd5e1);
1841
- background: rgba(65, 118, 230, 0.08);
3600
+ body[data-ds-dark-theme] .dsh-tree-sidebar {
3601
+ background: var(--dsw-alias-bg-layer-2, #151821);
3602
+ border-right-color: var(--dsw-alias-border-l3, #2a3140);
1842
3603
  }
1843
- .dsh-md-blockquote p { margin: 0.3em 0; }
1844
3604
 
1845
- /* Tables */
1846
- .dsh-md-table-wrapper {
1847
- width: 100%;
1848
- overflow-x: auto;
1849
- margin: 1.2em 0;
1850
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1851
- border-radius: 8px;
1852
- }
1853
- body[data-ds-dark-theme] .dsh-md-table-wrapper {
1854
- border-color: var(--dsw-alias-border-l2, #334155);
3605
+ .dsh-tree-sidebar-header {
3606
+ height: 36px;
3607
+ min-height: 36px;
3608
+ display: flex;
3609
+ align-items: center;
3610
+ justify-content: space-between;
3611
+ padding: 0 8px;
3612
+ border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
3613
+ font-weight: 600;
3614
+ font-size: 12px;
3615
+ color: var(--dsw-alias-label-primary, #0f172a);
3616
+ background: rgba(0, 0, 0, 0.02);
3617
+ box-sizing: border-box;
1855
3618
  }
1856
- .dsh-md-table-wrapper table {
1857
- width: 100%;
1858
- border-collapse: collapse;
1859
- font-size: 13px;
3619
+ body[data-ds-dark-theme] .dsh-tree-sidebar-header {
3620
+ border-bottom-color: var(--dsw-alias-border-l3, #2a3140);
3621
+ background: rgba(255, 255, 255, 0.02);
3622
+ color: #e2e8f0;
1860
3623
  }
1861
- .dsh-md-table-wrapper th,
1862
- .dsh-md-table-wrapper td {
1863
- padding: 8px 14px;
1864
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3624
+
3625
+ .dsh-tree-sidebar-title {
3626
+ display: flex;
3627
+ align-items: center;
3628
+ gap: 5px;
3629
+ overflow: hidden;
3630
+ text-overflow: ellipsis;
3631
+ white-space: nowrap;
3632
+ font-size: 12px;
1865
3633
  }
1866
- body[data-ds-dark-theme] .dsh-md-table-wrapper th,
1867
- body[data-ds-dark-theme] .dsh-md-table-wrapper td {
1868
- border-color: var(--dsw-alias-border-l2, #273344);
3634
+ .dsh-tree-header-icon { font-size: 13px; }
3635
+ .dsh-tree-header-text {
3636
+ overflow: hidden;
3637
+ text-overflow: ellipsis;
3638
+ white-space: nowrap;
1869
3639
  }
1870
- .dsh-md-table-wrapper th {
1871
- background: var(--dsw-alias-bg-layer-2, #f8fafc);
1872
- font-weight: 600;
1873
- color: var(--dsw-alias-label-primary, #0f172a);
3640
+
3641
+ .dsh-tree-header-actions {
3642
+ display: flex;
3643
+ align-items: center;
3644
+ gap: 2px;
3645
+ flex-shrink: 0;
1874
3646
  }
1875
- body[data-ds-dark-theme] .dsh-md-table-wrapper th {
1876
- background: var(--dsw-alias-bg-layer-2, #1a202c);
1877
- color: #f1f5f9;
3647
+
3648
+ .dsh-tree-action-btn {
3649
+ width: 22px;
3650
+ height: 22px;
3651
+ border-radius: 4px;
3652
+ border: none;
3653
+ background: transparent;
3654
+ color: var(--dsw-alias-label-secondary, #64748b);
3655
+ cursor: pointer;
3656
+ display: inline-flex;
3657
+ align-items: center;
3658
+ justify-content: center;
3659
+ font-size: 11px;
3660
+ padding: 0;
3661
+ transition: all 0.1s;
1878
3662
  }
1879
- .dsh-md-table-wrapper tr:nth-child(even) {
1880
- background: rgba(0, 0, 0, 0.015);
3663
+ .dsh-tree-action-btn:hover {
3664
+ background: rgba(0, 0, 0, 0.06);
3665
+ color: var(--dsw-alias-label-primary, #0f172a);
1881
3666
  }
1882
- body[data-ds-dark-theme] .dsh-md-table-wrapper tr:nth-child(even) {
1883
- background: rgba(255, 255, 255, 0.02);
3667
+ body[data-ds-dark-theme] .dsh-tree-action-btn:hover {
3668
+ background: rgba(255, 255, 255, 0.08);
3669
+ color: #ffffff;
1884
3670
  }
1885
3671
 
1886
- /* Lists & Tasks */
1887
- .dsh-md-list {
1888
- padding-left: 24px;
1889
- margin: 0.8em 0 1.2em 0;
3672
+ .dsh-tree-sidebar-search {
3673
+ padding: 6px 8px;
3674
+ border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
3675
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
3676
+ box-sizing: border-box;
1890
3677
  }
1891
- .dsh-md-list li { margin-bottom: 0.35em; }
1892
- .dsh-md-task-list {
1893
- list-style: none;
1894
- padding-left: 0;
3678
+ body[data-ds-dark-theme] .dsh-tree-sidebar-search {
3679
+ border-bottom-color: var(--dsw-alias-border-l3, #2a3140);
3680
+ background: var(--dsw-alias-bg-layer-1, #161922);
1895
3681
  }
1896
- .dsh-md-task-item {
3682
+
3683
+ .dsh-tree-search-wrapper {
3684
+ position: relative;
1897
3685
  display: flex;
1898
3686
  align-items: center;
1899
- gap: 8px;
1900
- margin-bottom: 0.4em;
1901
- }
1902
- .dsh-md-task-item input[type="checkbox"] {
1903
- accent-color: var(--dsw-static-deepseek-500, #4176e6);
1904
- cursor: pointer;
1905
3687
  }
1906
3688
 
1907
- /* Inline Code & Badges */
1908
- .dsh-md-inline-code {
1909
- background: var(--dsw-alias-bg-layer-3, #f1f5f9);
1910
- color: var(--dsw-static-deepseek-500, #2563eb);
1911
- padding: 2px 6px;
3689
+ .dsh-tree-search-input {
3690
+ width: 100%;
3691
+ height: 24px;
1912
3692
  border-radius: 4px;
1913
- font-family: "Cascadia Code", Consolas, "Courier New", monospace;
1914
- font-size: 0.88em;
1915
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3693
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
3694
+ padding: 0 20px 0 6px;
3695
+ font-size: 11px;
3696
+ background: var(--dsw-alias-bg-base, #f8fafc);
3697
+ color: inherit;
3698
+ box-sizing: border-box;
3699
+ outline: none;
3700
+ transition: border-color 0.15s;
1916
3701
  }
1917
- body[data-ds-dark-theme] .dsh-md-inline-code {
1918
- background: var(--dsw-alias-bg-layer-3, #1e2433);
1919
- color: #93c5fd;
1920
- border-color: rgba(147, 197, 253, 0.15);
3702
+ .dsh-tree-search-input:focus {
3703
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
3704
+ background: #ffffff;
1921
3705
  }
1922
- .dsh-md-link {
1923
- color: var(--dsw-static-deepseek-500, #2563eb);
1924
- text-decoration: none;
1925
- border-bottom: 1px solid rgba(37, 99, 235, 0.3);
1926
- transition: border-color 0.15s;
3706
+ body[data-ds-dark-theme] .dsh-tree-search-input {
3707
+ background: #1e2330;
3708
+ border-color: #334155;
3709
+ color: #f1f5f9;
1927
3710
  }
1928
- body[data-ds-dark-theme] .dsh-md-link {
1929
- color: var(--dsw-static-deepseek-500, #60a5fa);
1930
- border-bottom-color: rgba(96, 165, 250, 0.3);
3711
+ body[data-ds-dark-theme] .dsh-tree-search-input:focus {
3712
+ background: #181b24;
3713
+ border-color: #60a5fa;
1931
3714
  }
1932
- .dsh-md-link:hover {
1933
- border-color: currentColor;
3715
+
3716
+ .dsh-tree-search-clear {
3717
+ position: absolute;
3718
+ right: 4px;
3719
+ width: 16px;
3720
+ height: 16px;
3721
+ border-radius: 50%;
3722
+ border: none;
3723
+ background: rgba(0, 0, 0, 0.1);
3724
+ color: inherit;
3725
+ font-size: 9px;
3726
+ display: flex;
3727
+ align-items: center;
3728
+ justify-content: center;
3729
+ cursor: pointer;
3730
+ padding: 0;
1934
3731
  }
1935
3732
 
1936
- /* Code Cards */
1937
- .dsh-code-card {
1938
- margin: 1.4em 0;
1939
- background: var(--dsw-alias-bg-layer-3, #f8fafc);
1940
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
1941
- border-radius: 8px;
1942
- overflow: hidden;
3733
+ .dsh-tree-node-list {
3734
+ flex: 1;
3735
+ min-height: 0;
3736
+ overflow-y: auto;
3737
+ overflow-x: hidden;
3738
+ padding: 4px 2px;
3739
+ box-sizing: border-box;
1943
3740
  }
1944
- body[data-ds-dark-theme] .dsh-code-card {
1945
- background: #0d1117;
1946
- border-color: #30363d;
3741
+ .dsh-tree-node-list::-webkit-scrollbar {
3742
+ width: 4px;
1947
3743
  }
1948
- .dsh-code-card-header {
3744
+ .dsh-tree-node-list::-webkit-scrollbar-thumb {
3745
+ background: rgba(0, 0, 0, 0.15);
3746
+ border-radius: 2px;
3747
+ }
3748
+
3749
+ .dsh-tree-node-row {
1949
3750
  display: flex;
1950
3751
  align-items: center;
1951
- justify-content: space-between;
1952
- padding: 6px 12px;
1953
- background: rgba(0, 0, 0, 0.03);
1954
- border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
3752
+ height: 24px;
3753
+ min-height: 24px;
3754
+ border-radius: 4px;
3755
+ margin: 1px 4px;
3756
+ cursor: pointer;
3757
+ font-size: 12px;
3758
+ color: var(--dsw-alias-label-secondary, #334155);
3759
+ gap: 3px;
3760
+ transition: background 0.1s ease, color 0.1s ease;
3761
+ overflow: hidden;
3762
+ box-sizing: border-box;
1955
3763
  }
1956
- body[data-ds-dark-theme] .dsh-code-card-header {
1957
- background: rgba(255, 255, 255, 0.04);
1958
- border-bottom-color: #21262d;
3764
+ body[data-ds-dark-theme] .dsh-tree-node-row {
3765
+ color: #94a3b8;
1959
3766
  }
1960
- .dsh-code-lang-badge {
1961
- font-family: monospace;
1962
- font-size: 11px;
1963
- color: var(--dsw-alias-label-secondary, #64748b);
3767
+
3768
+ .dsh-tree-node-row:hover {
3769
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
3770
+ color: var(--dsw-alias-label-primary, #0f172a);
3771
+ }
3772
+ body[data-ds-dark-theme] .dsh-tree-node-row:hover {
3773
+ background: rgba(255, 255, 255, 0.06);
3774
+ color: #f1f5f9;
3775
+ }
3776
+
3777
+ .dsh-tree-node-row.active {
3778
+ background: rgba(14, 165, 233, 0.14);
3779
+ color: #0284c7;
1964
3780
  font-weight: 600;
1965
3781
  }
1966
- body[data-ds-dark-theme] .dsh-code-lang-badge {
1967
- color: #8b949e;
3782
+ body[data-ds-dark-theme] .dsh-tree-node-row.active {
3783
+ background: rgba(56, 189, 248, 0.16);
3784
+ color: #38bdf8;
1968
3785
  }
1969
- .dsh-code-copy-btn {
1970
- background: transparent;
1971
- border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
1972
- color: var(--dsw-alias-label-secondary, #475569);
1973
- font-size: 11px;
1974
- padding: 2px 8px;
1975
- border-radius: 4px;
1976
- cursor: pointer;
1977
- transition: all 0.15s;
3786
+
3787
+ .dsh-tree-chevron {
3788
+ width: 14px;
3789
+ height: 14px;
3790
+ display: inline-flex;
3791
+ align-items: center;
3792
+ justify-content: center;
3793
+ font-size: 8px;
3794
+ color: #94a3b8;
3795
+ flex-shrink: 0;
3796
+ user-select: none;
1978
3797
  }
1979
- body[data-ds-dark-theme] .dsh-code-copy-btn {
1980
- border-color: #30363d;
1981
- color: #c9d1d9;
3798
+
3799
+ .dsh-tree-chevron-placeholder {
3800
+ width: 14px;
3801
+ height: 14px;
3802
+ display: inline-flex;
3803
+ flex-shrink: 0;
1982
3804
  }
1983
- .dsh-code-copy-btn:hover {
1984
- background: var(--dsw-alias-bg-layer-2, #e2e8f0);
1985
- color: var(--dsw-static-deepseek-500, #2563eb);
3805
+
3806
+ .dsh-tree-icon {
3807
+ width: 16px;
3808
+ height: 16px;
3809
+ display: inline-flex;
3810
+ align-items: center;
3811
+ justify-content: center;
3812
+ font-size: 12px;
3813
+ flex-shrink: 0;
3814
+ font-family: ui-monospace, monospace;
3815
+ font-weight: 700;
1986
3816
  }
1987
- body[data-ds-dark-theme] .dsh-code-copy-btn:hover {
1988
- background: #21262d;
1989
- color: #58a6ff;
3817
+
3818
+ .dsh-tree-name {
3819
+ flex: 1;
3820
+ overflow: hidden;
3821
+ text-overflow: ellipsis;
3822
+ white-space: nowrap;
3823
+ font-size: 12px;
3824
+ text-align: left;
1990
3825
  }
1991
- .dsh-code-pre {
1992
- margin: 0;
1993
- padding: 14px 16px;
1994
- overflow-x: auto;
1995
- font-family: "Fira Code", "Cascadia Code", Consolas, monospace;
1996
- font-size: 13px;
1997
- line-height: 1.55;
1998
- color: var(--dsw-alias-label-primary, #0f172a);
3826
+
3827
+ .dsh-tree-badge {
3828
+ font-size: 10px;
3829
+ color: #94a3b8;
3830
+ background: rgba(0, 0, 0, 0.05);
3831
+ padding: 0 4px;
3832
+ border-radius: 8px;
3833
+ margin-right: 4px;
3834
+ flex-shrink: 0;
1999
3835
  }
2000
- body[data-ds-dark-theme] .dsh-code-pre {
2001
- color: #e6edf3;
3836
+ body[data-ds-dark-theme] .dsh-tree-badge {
3837
+ background: rgba(255, 255, 255, 0.08);
3838
+ color: #64748b;
2002
3839
  }
2003
- .dsh-token-keyword { color: #d73a49; font-weight: 500; }
2004
- body[data-ds-dark-theme] .dsh-token-keyword { color: #ff7b72; }
2005
- .dsh-token-string { color: #032f62; }
2006
- body[data-ds-dark-theme] .dsh-token-string { color: #a5d6ff; }
2007
- .dsh-token-comment { color: #6a737d; font-style: italic; }
2008
- body[data-ds-dark-theme] .dsh-token-comment { color: #8b949e; }
2009
- .dsh-token-number { color: #005cc5; }
2010
- body[data-ds-dark-theme] .dsh-token-number { color: #79c0ff; }
2011
3840
 
2012
- /* GitHub Alert Callouts */
2013
- .dsh-alert {
2014
- margin: 1.4em 0;
2015
- padding: 12px 16px;
2016
- border-left: 4px solid;
2017
- border-radius: 0 8px 8px 0;
3841
+ .dsh-tree-size-badge {
3842
+ font-size: 10px;
3843
+ color: #94a3b8;
3844
+ margin-right: 4px;
3845
+ flex-shrink: 0;
2018
3846
  }
2019
- .dsh-alert-title {
3847
+
3848
+ .dsh-tree-loading, .dsh-tree-error, .dsh-tree-empty {
3849
+ padding: 24px 12px;
3850
+ text-align: center;
3851
+ font-size: 12px;
3852
+ color: #94a3b8;
3853
+ }
3854
+
3855
+ .dsh-tree-retry-btn {
3856
+ margin-top: 8px;
3857
+ padding: 2px 10px;
3858
+ border-radius: 4px;
3859
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
3860
+ background: transparent;
3861
+ color: inherit;
3862
+ cursor: pointer;
3863
+ }
3864
+
3865
+ .dsh-tree-sidebar-footer {
3866
+ height: 24px;
3867
+ min-height: 24px;
3868
+ border-top: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
2020
3869
  display: flex;
2021
3870
  align-items: center;
3871
+ justify-content: center;
2022
3872
  gap: 6px;
2023
- font-weight: 600;
2024
- font-size: 13px;
2025
- margin-bottom: 6px;
3873
+ font-size: 11px;
3874
+ color: #94a3b8;
3875
+ background: rgba(0, 0, 0, 0.015);
2026
3876
  }
2027
- .dsh-alert-note { background: rgba(59, 130, 246, 0.08); border-color: #3b82f6; }
2028
- .dsh-alert-note .dsh-alert-title { color: #2563eb; }
2029
- body[data-ds-dark-theme] .dsh-alert-note .dsh-alert-title { color: #60a5fa; }
2030
- .dsh-alert-tip { background: rgba(16, 185, 129, 0.08); border-color: #10b981; }
2031
- .dsh-alert-tip .dsh-alert-title { color: #059669; }
2032
- body[data-ds-dark-theme] .dsh-alert-tip .dsh-alert-title { color: #34d399; }
2033
- .dsh-alert-important { background: rgba(168, 85, 247, 0.08); border-color: #a855f7; }
2034
- .dsh-alert-important .dsh-alert-title { color: #7c3aed; }
2035
- body[data-ds-dark-theme] .dsh-alert-important .dsh-alert-title { color: #c084fc; }
2036
- .dsh-alert-warning { background: rgba(245, 158, 11, 0.08); border-color: #f59e0b; }
2037
- .dsh-alert-warning .dsh-alert-title { color: #d97706; }
2038
- body[data-ds-dark-theme] .dsh-alert-warning .dsh-alert-title { color: #fbbf24; }
2039
- .dsh-alert-caution { background: rgba(239, 68, 68, 0.08); border-color: #ef4444; }
2040
- .dsh-alert-caution .dsh-alert-title { color: #dc2626; }
2041
- body[data-ds-dark-theme] .dsh-alert-caution .dsh-alert-title { color: #f87171; }
2042
- .dsh-alert-body { font-size: 13px; color: var(--dsw-alias-label-secondary, #475569); }
2043
- body[data-ds-dark-theme] .dsh-alert-body { color: #cbd5e1; }
3877
+ body[data-ds-dark-theme] .dsh-tree-sidebar-footer {
3878
+ border-top-color: var(--dsw-alias-border-l3, #2a3140);
3879
+ }
3880
+ .dsh-tree-footer-dot { font-size: 8px; }
2044
3881
 
2045
- /* Math Formulas */
2046
- .dsh-math-block {
2047
- margin: 1.2em 0;
2048
- padding: 12px 16px;
2049
- background: rgba(0, 0, 0, 0.02);
2050
- border-radius: 6px;
2051
- border: 1px dashed var(--dsw-alias-border-l2, #cbd5e1);
3882
+ /* Splitter between Tree and Preview */
3883
+ .dsh-tree-splitter {
3884
+ width: 4px;
3885
+ cursor: col-resize;
3886
+ background: transparent;
3887
+ transition: background 0.15s;
3888
+ flex-shrink: 0;
3889
+ z-index: 10;
3890
+ }
3891
+ .dsh-tree-splitter:hover, .dsh-tree-splitter.dragging {
3892
+ background: var(--dsw-static-deepseek-500, #4176e6);
3893
+ }
3894
+
3895
+ /* Welcome Empty View */
3896
+ .dsh-preview-welcome-container {
3897
+ flex: 1;
3898
+ display: flex;
3899
+ align-items: center;
3900
+ justify-content: center;
3901
+ padding: 32px;
3902
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
3903
+ color: var(--dsw-alias-label-primary, #0f172a);
3904
+ overflow: auto;
3905
+ }
3906
+ body[data-ds-dark-theme] .dsh-preview-welcome-container {
3907
+ background: var(--dsw-alias-bg-layer-1, #161922);
3908
+ color: #e2e8f0;
3909
+ }
3910
+
3911
+ .dsh-preview-welcome-card {
3912
+ max-width: 420px;
2052
3913
  text-align: center;
2053
- font-family: "KaTeX_Math", "Times New Roman", serif;
2054
- font-size: 1.1em;
2055
- color: var(--dsw-alias-label-primary, #1e293b);
3914
+ display: flex;
3915
+ flex-direction: column;
3916
+ align-items: center;
3917
+ gap: 12px;
2056
3918
  }
2057
- body[data-ds-dark-theme] .dsh-math-block {
2058
- background: rgba(255, 255, 255, 0.03);
2059
- border-color: var(--dsw-alias-border-l2, #334155);
2060
- color: #cbd5e1;
3919
+ .dsh-welcome-icon { font-size: 40px; }
3920
+ .dsh-welcome-title { margin: 0; font-size: 16px; font-weight: 600; }
3921
+ .dsh-welcome-path {
3922
+ margin: 0;
3923
+ font-size: 11px;
3924
+ color: #64748b;
3925
+ background: rgba(0, 0, 0, 0.04);
3926
+ padding: 4px 8px;
3927
+ border-radius: 4px;
3928
+ word-break: break-all;
2061
3929
  }
2062
- .dsh-math-inline {
2063
- font-family: "KaTeX_Math", "Times New Roman", serif;
2064
- color: var(--dsw-static-deepseek-500, #2563eb);
2065
- padding: 0 3px;
3930
+ body[data-ds-dark-theme] .dsh-welcome-path {
3931
+ background: rgba(255, 255, 255, 0.06);
3932
+ color: #94a3b8;
2066
3933
  }
2067
- body[data-ds-dark-theme] .dsh-math-inline {
2068
- color: #93c5fd;
3934
+ .dsh-welcome-desc {
3935
+ margin: 0;
3936
+ font-size: 12px;
3937
+ color: var(--dsw-alias-label-secondary, #64748b);
3938
+ line-height: 1.6;
2069
3939
  }
2070
-
2071
- /* Source View Mode */
2072
- .dsh-source-view {
3940
+ .dsh-welcome-actions {
2073
3941
  display: flex;
2074
- background: var(--dsw-alias-bg-layer-3, #f8fafc);
2075
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2076
- border-radius: 8px;
2077
- overflow: hidden;
2078
- font-family: "Fira Code", Consolas, monospace;
3942
+ gap: 8px;
3943
+ margin-top: 8px;
3944
+ }
3945
+ .dsh-welcome-btn {
3946
+ padding: 6px 14px;
3947
+ border-radius: 6px;
3948
+ font-size: 12px;
3949
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
3950
+ background: transparent;
3951
+ color: inherit;
3952
+ cursor: pointer;
3953
+ transition: all 0.15s;
3954
+ }
3955
+ .dsh-welcome-btn:hover {
3956
+ background: rgba(0, 0, 0, 0.05);
3957
+ }
3958
+ .dsh-welcome-btn.primary {
3959
+ background: var(--dsw-static-deepseek-500, #4176e6);
3960
+ color: #ffffff;
3961
+ border-color: var(--dsw-static-deepseek-500, #4176e6);
3962
+ }
3963
+ .dsh-welcome-btn.primary:hover {
3964
+ opacity: 0.9;
3965
+ }
3966
+
3967
+ /* Syntax Highlighting Tokens (Light Theme) */
3968
+ .tok-keyword { color: #0000ff; font-weight: 600; }
3969
+ .tok-string { color: #a31515; }
3970
+ .tok-number { color: #098658; }
3971
+ .tok-boolean { color: #0000ff; font-weight: 600; }
3972
+ .tok-null { color: #708090; font-style: italic; }
3973
+ .tok-key { color: #001080; font-weight: 600; }
3974
+ .tok-comment { color: #008000; font-style: italic; }
3975
+ .tok-type { color: #267f99; }
3976
+ .tok-func { color: #795e26; }
3977
+ .tok-operator { color: #000000; font-weight: 500; }
3978
+ .tok-punctuation, .tok-bracket { color: #333333; }
3979
+ .tok-tag { color: #800000; font-weight: 600; }
3980
+ .tok-attr { color: #e50000; }
3981
+ .tok-prop { color: #001080; }
3982
+ .tok-selector { color: #800000; }
3983
+
3984
+ /* Syntax Highlighting Tokens (Dark Theme) */
3985
+ body[data-ds-dark-theme] .tok-keyword { color: #569cd6; }
3986
+ body[data-ds-dark-theme] .tok-string { color: #ce9178; }
3987
+ body[data-ds-dark-theme] .tok-number { color: #b5cea8; }
3988
+ body[data-ds-dark-theme] .tok-boolean { color: #569cd6; font-weight: 600; }
3989
+ body[data-ds-dark-theme] .tok-null { color: #808080; font-style: italic; }
3990
+ body[data-ds-dark-theme] .tok-key { color: #9cdcfe; font-weight: 600; }
3991
+ body[data-ds-dark-theme] .tok-comment { color: #6a9955; font-style: italic; }
3992
+ body[data-ds-dark-theme] .tok-type { color: #4ec9b0; }
3993
+ body[data-ds-dark-theme] .tok-func { color: #dcdcaa; }
3994
+ body[data-ds-dark-theme] .tok-operator { color: #d4d4d4; }
3995
+ body[data-ds-dark-theme] .tok-punctuation, body[data-ds-dark-theme] .tok-bracket { color: #d4d4d4; }
3996
+ body[data-ds-dark-theme] .tok-tag { color: #569cd6; }
3997
+ body[data-ds-dark-theme] .tok-attr { color: #9cdcfe; }
3998
+ body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
3999
+ body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
4000
+
4001
+ /* Code Editor Viewer */
4002
+ .dsh-code-editor-view {
4003
+ display: flex;
4004
+ min-height: 100%;
4005
+ font-family: "Cascadia Code", "Fira Code", Consolas, Menlo, monospace;
2079
4006
  font-size: 13px;
2080
- line-height: 1.6;
4007
+ line-height: 20px;
4008
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4009
+ color: var(--dsw-alias-label-primary, #1e293b);
2081
4010
  }
2082
- body[data-ds-dark-theme] .dsh-source-view {
2083
- background: var(--dsw-alias-bg-layer-3, #0d1017);
2084
- border-color: var(--dsw-alias-border-l2, #252d3d);
4011
+ body[data-ds-dark-theme] .dsh-code-editor-view {
4012
+ background: #181b24;
4013
+ color: #d4d4d4;
2085
4014
  }
2086
- .dsh-source-line-numbers {
2087
- padding: 16px 8px;
2088
- background: rgba(0, 0, 0, 0.02);
2089
- border-right: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2090
- color: #94a3b8;
4015
+
4016
+ .dsh-code-gutter {
4017
+ width: 44px;
4018
+ min-width: 44px;
4019
+ padding: 12px 6px 12px 0;
2091
4020
  text-align: right;
2092
4021
  user-select: none;
4022
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
4023
+ border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4024
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
4025
+ font-size: 12px;
4026
+ line-height: 20px;
4027
+ flex-shrink: 0;
2093
4028
  }
2094
- body[data-ds-dark-theme] .dsh-source-line-numbers {
2095
- background: rgba(255, 255, 255, 0.02);
2096
- border-right-color: var(--dsw-alias-border-l2, #252d3d);
2097
- color: #475569;
4029
+ body[data-ds-dark-theme] .dsh-code-gutter {
4030
+ background: #141720;
4031
+ border-right-color: #2a3140;
4032
+ color: #64748b;
2098
4033
  }
2099
- .dsh-source-pre {
2100
- margin: 0;
2101
- padding: 16px;
4034
+
4035
+ .dsh-gutter-line {
4036
+ height: 20px;
4037
+ cursor: pointer;
4038
+ padding-right: 8px;
4039
+ }
4040
+ .dsh-gutter-line:hover {
4041
+ color: var(--dsw-brand-primary, #0284c7);
4042
+ font-weight: bold;
4043
+ }
4044
+ .dsh-gutter-line.dsh-line-highlight-flash {
4045
+ background: rgba(14, 165, 233, 0.25);
4046
+ color: #0284c7;
4047
+ }
4048
+
4049
+ .dsh-code-content {
2102
4050
  flex: 1;
2103
- overflow-x: auto;
2104
- color: var(--dsw-alias-label-primary, #0f172a);
4051
+ min-width: 0;
4052
+ padding: 12px 16px;
4053
+ overflow: auto;
2105
4054
  }
2106
- body[data-ds-dark-theme] .dsh-source-pre {
2107
- color: #cbd5e1;
4055
+ .dsh-code-pre {
4056
+ margin: 0;
4057
+ font-family: inherit;
4058
+ font-size: inherit;
4059
+ line-height: 20px;
4060
+ tab-size: 2;
4061
+ white-space: pre;
4062
+ }
4063
+ .dsh-code-editor-view.word-wrap .dsh-code-pre {
4064
+ white-space: pre-wrap;
4065
+ word-break: normal;
4066
+ overflow-wrap: break-word;
4067
+ }
4068
+ .dsh-code-editor-view {
4069
+ display: flex;
4070
+ min-height: 100%;
4071
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
4072
+ font-size: 12.5px;
4073
+ line-height: 20px;
4074
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4075
+ color: var(--dsw-alias-label-primary, #1e293b);
4076
+ }
4077
+ .dsh-code-pre code {
4078
+ font-family: inherit;
2108
4079
  }
2109
4080
 
2110
- /* Split View Mode */
2111
- .dsh-split-view {
4081
+ /* Interactive JSON Inspector */
4082
+ .dsh-json-inspector {
2112
4083
  display: flex;
2113
- gap: 12px;
4084
+ flex-direction: column;
2114
4085
  height: 100%;
4086
+ overflow: hidden;
4087
+ font-family: "Cascadia Code", Consolas, monospace;
4088
+ font-size: 12px;
4089
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
2115
4090
  }
2116
- .dsh-split-pane {
4091
+ body[data-ds-dark-theme] .dsh-json-inspector {
4092
+ background: #181b24;
4093
+ color: #d4d4d4;
4094
+ }
4095
+
4096
+ .dsh-json-header {
4097
+ display: flex;
4098
+ align-items: center;
4099
+ justify-content: space-between;
4100
+ padding: 8px 12px;
4101
+ border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4102
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
4103
+ gap: 12px;
4104
+ flex-shrink: 0;
4105
+ }
4106
+ body[data-ds-dark-theme] .dsh-json-header {
4107
+ border-bottom-color: #2a3140;
4108
+ background: #141720;
4109
+ }
4110
+
4111
+ .dsh-json-search-box {
4112
+ position: relative;
4113
+ display: flex;
4114
+ align-items: center;
2117
4115
  flex: 1;
2118
- overflow-y: auto;
2119
- border: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
2120
- border-radius: 8px;
2121
- padding: 12px;
2122
- background: var(--dsw-alias-bg-layer-1, #ffffff);
4116
+ max-width: 280px;
2123
4117
  }
2124
- body[data-ds-dark-theme] .dsh-split-pane {
2125
- border-color: var(--dsw-alias-border-l2, #252d3d);
2126
- background: var(--dsw-alias-bg-layer-1, #0d1017);
4118
+ .dsh-json-search-icon {
4119
+ position: absolute;
4120
+ left: 8px;
4121
+ font-size: 11px;
4122
+ pointer-events: none;
2127
4123
  }
2128
- .dsh-split-pane-header {
4124
+ .dsh-json-search-input {
4125
+ width: 100%;
4126
+ height: 26px;
4127
+ border-radius: 6px;
4128
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4129
+ padding: 0 24px 0 26px;
4130
+ font-size: 12px;
4131
+ background: #ffffff;
4132
+ color: inherit;
4133
+ outline: none;
4134
+ }
4135
+ body[data-ds-dark-theme] .dsh-json-search-input {
4136
+ background: #1e2330;
4137
+ border-color: #334155;
4138
+ color: #f1f5f9;
4139
+ }
4140
+ .dsh-json-actions {
4141
+ display: flex;
4142
+ gap: 6px;
4143
+ }
4144
+ .dsh-json-btn {
4145
+ padding: 3px 8px;
4146
+ border-radius: 4px;
4147
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4148
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4149
+ color: var(--dsw-alias-label-secondary, #475569);
4150
+ cursor: pointer;
2129
4151
  font-size: 11px;
2130
- font-weight: 600;
2131
- color: var(--dsw-alias-label-secondary, #64748b);
2132
- margin-bottom: 8px;
2133
- padding-bottom: 4px;
2134
- border-bottom: 1px solid var(--dsw-alias-border-l2, #e2e8f0);
4152
+ transition: all 0.15s;
2135
4153
  }
2136
- body[data-ds-dark-theme] .dsh-split-pane-header {
4154
+ .dsh-json-btn:hover {
4155
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
4156
+ color: var(--dsw-alias-label-primary, #0f172a);
4157
+ }
4158
+ body[data-ds-dark-theme] .dsh-json-btn {
4159
+ background: #1e2330;
4160
+ border-color: #334155;
2137
4161
  color: #94a3b8;
2138
- border-bottom-color: var(--dsw-alias-border-l2, #252d3d);
4162
+ }
4163
+ body[data-ds-dark-theme] .dsh-json-btn:hover {
4164
+ background: #283042;
4165
+ color: #f1f5f9;
2139
4166
  }
2140
4167
 
2141
- /* Loading & Error States */
2142
- .dsh-preview-loading {
4168
+ .dsh-json-tree-body {
4169
+ flex: 1;
4170
+ overflow: auto;
4171
+ padding: 10px 12px;
4172
+ }
4173
+
4174
+ .dsh-json-row {
2143
4175
  display: flex;
2144
- flex-direction: column;
2145
4176
  align-items: center;
2146
- justify-content: center;
2147
- padding: 60px 0;
2148
- gap: 14px;
2149
- color: var(--dsw-alias-label-secondary, #64748b);
4177
+ min-height: 22px;
4178
+ line-height: 22px;
4179
+ gap: 6px;
4180
+ border-radius: 4px;
4181
+ padding-right: 8px;
4182
+ transition: background 0.1s;
2150
4183
  }
2151
- .dsh-preview-spinner {
2152
- width: 32px;
2153
- height: 32px;
2154
- border: 3px solid rgba(65, 118, 230, 0.2);
2155
- border-top-color: var(--dsw-static-deepseek-500, #4176e6);
2156
- border-radius: 50%;
2157
- animation: dshSpinner 0.8s linear infinite;
4184
+ .dsh-json-row:hover {
4185
+ background: rgba(0, 0, 0, 0.03);
2158
4186
  }
2159
- @keyframes dshSpinner {
2160
- to { transform: rotate(360deg); }
4187
+ body[data-ds-dark-theme] .dsh-json-row:hover {
4188
+ background: rgba(255, 255, 255, 0.04);
4189
+ }
4190
+ .dsh-json-row-expandable {
4191
+ cursor: pointer;
2161
4192
  }
2162
4193
 
2163
- .dsh-preview-error-card {
2164
- padding: 32px 24px;
2165
- background: rgba(239, 68, 68, 0.06);
2166
- border: 1px solid rgba(239, 68, 68, 0.25);
2167
- border-radius: 8px;
2168
- text-align: center;
2169
- margin: 30px auto;
2170
- max-width: 480px;
4194
+ .dsh-json-chevron {
4195
+ font-size: 9px;
4196
+ color: #94a3b8;
4197
+ width: 12px;
4198
+ height: 12px;
4199
+ display: inline-flex;
4200
+ align-items: center;
4201
+ justify-content: center;
4202
+ flex-shrink: 0;
4203
+ user-select: none;
2171
4204
  }
2172
- .dsh-error-icon { font-size: 32px; margin-bottom: 10px; }
2173
- .dsh-error-title { font-size: 16px; font-weight: 600; color: #ef4444; margin-bottom: 6px; }
2174
- .dsh-error-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #64748b); margin-bottom: 16px; }
2175
- body[data-ds-dark-theme] .dsh-error-desc { color: #cbd5e1; }
2176
- .dsh-error-actions { display: flex; justify-content: center; gap: 10px; }
2177
- .dsh-btn-retry {
2178
- background: #ef4444; color: #fff; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px;
4205
+ .dsh-json-key {
4206
+ color: #001080;
4207
+ font-weight: 600;
4208
+ flex-shrink: 0;
2179
4209
  }
2180
- .dsh-btn-native {
2181
- 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;
4210
+ body[data-ds-dark-theme] .dsh-json-key {
4211
+ color: #9cdcfe;
2182
4212
  }
2183
- body[data-ds-dark-theme] .dsh-btn-native {
2184
- background: var(--dsw-alias-bg-layer-2, #1e2433); color: #e2e8f0; border-color: #334155;
4213
+ .dsh-json-type-badge {
4214
+ font-size: 10px;
4215
+ color: #94a3b8;
4216
+ background: rgba(0, 0, 0, 0.05);
4217
+ padding: 0 5px;
4218
+ border-radius: 6px;
4219
+ user-select: none;
2185
4220
  }
2186
-
2187
- /* Toast Popup */
2188
- .dsh-preview-toast-container {
2189
- position: fixed;
2190
- bottom: 24px;
2191
- left: 50%;
2192
- transform: translateX(-50%);
2193
- z-index: 10000;
2194
- display: flex;
2195
- flex-direction: column;
2196
- gap: 8px;
2197
- pointer-events: none;
4221
+ body[data-ds-dark-theme] .dsh-json-type-badge {
4222
+ background: rgba(255, 255, 255, 0.08);
4223
+ color: #64748b;
2198
4224
  }
2199
- .dsh-preview-toast {
2200
- display: flex;
2201
- align-items: center;
2202
- gap: 8px;
2203
- padding: 8px 16px;
2204
- background: #1e293b;
2205
- color: #f8fafc;
2206
- border: 1px solid #334155;
2207
- border-radius: 20px;
2208
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
2209
- font-size: 12px;
4225
+ .dsh-json-preview {
4226
+ color: #64748b;
4227
+ font-style: italic;
4228
+ font-size: 11px;
4229
+ }
4230
+ body[data-ds-dark-theme] .dsh-json-preview {
4231
+ color: #94a3b8;
4232
+ }
4233
+ .dsh-json-copy-btn {
2210
4234
  opacity: 0;
2211
- transform: translateY(10px);
2212
- transition: all 0.25s ease;
4235
+ cursor: pointer;
4236
+ font-size: 10px;
4237
+ margin-left: auto;
4238
+ padding: 1px 4px;
4239
+ border-radius: 3px;
4240
+ transition: opacity 0.15s;
2213
4241
  }
2214
- .dsh-preview-toast.dsh-preview-toast-show {
4242
+ .dsh-json-row:hover .dsh-json-copy-btn {
4243
+ opacity: 0.75;
4244
+ }
4245
+ .dsh-json-copy-btn:hover {
2215
4246
  opacity: 1;
2216
- transform: translateY(0);
4247
+ background: rgba(0, 0, 0, 0.08);
4248
+ }
4249
+
4250
+ .dsh-json-parse-error {
4251
+ padding: 36px 16px;
4252
+ text-align: center;
4253
+ color: var(--dsw-alias-label-secondary, #64748b);
4254
+ }
4255
+ .dsh-symbol-item {
4256
+ padding-left: 10px;
2217
4257
  }
2218
- .dsh-preview-toast-success { border-color: #22c55e; }
2219
- .dsh-preview-toast-success .dsh-preview-toast-icon { color: #22c55e; font-weight: bold; }
2220
- .dsh-preview-toast-error { border-color: #ef4444; }
2221
- .dsh-preview-toast-error .dsh-preview-toast-icon { color: #ef4444; font-weight: bold; }
2222
4258
  `;
2223
4259
  document.head.appendChild(style);
2224
4260
  }