@rooode/dsh-plugin-preview 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -494,6 +494,97 @@
494
494
  }
495
495
  }
496
496
 
497
+ function renameOpenTabPaths(oldPath, newPath, isDir = false) {
498
+ if (!oldPath || !newPath) return;
499
+ let changed = false;
500
+ const oldNorm = oldPath.replace(/\\/g, '/').toLowerCase();
501
+ const newNorm = newPath.replace(/\\/g, '/');
502
+
503
+ globalTabs = globalTabs.map(tab => {
504
+ const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
505
+ if (!isDir) {
506
+ if (tabNorm === oldNorm) {
507
+ changed = true;
508
+ const newFileName = getFileName(newPath);
509
+ const newExt = getFileExtension(newPath);
510
+ const newCat = getFileTypeCategory(newPath);
511
+ return {
512
+ ...tab,
513
+ id: 'tab:' + newPath.toLowerCase(),
514
+ filePath: newPath,
515
+ title: newFileName,
516
+ extension: newExt,
517
+ category: newCat,
518
+ };
519
+ }
520
+ } else {
521
+ if (tabNorm === oldNorm || tabNorm.startsWith(oldNorm + '/')) {
522
+ changed = true;
523
+ const subPath = tab.filePath.replace(/\\/g, '/').slice(oldNorm.length);
524
+ const updatedFilePath = newNorm + subPath;
525
+ const newFileName = getFileName(updatedFilePath);
526
+ const newExt = getFileExtension(updatedFilePath);
527
+ const newCat = getFileTypeCategory(updatedFilePath);
528
+ return {
529
+ ...tab,
530
+ id: 'tab:' + updatedFilePath.toLowerCase(),
531
+ filePath: updatedFilePath,
532
+ title: newFileName,
533
+ extension: newExt,
534
+ category: newCat,
535
+ };
536
+ }
537
+ }
538
+ return tab;
539
+ });
540
+
541
+ if (changed) {
542
+ if (globalActiveTabId) {
543
+ const activeNorm = globalActiveTabId.toLowerCase();
544
+ const targetPrefix = 'tab:' + oldNorm;
545
+ if (activeNorm === targetPrefix) {
546
+ globalActiveTabId = 'tab:' + newPath.toLowerCase();
547
+ } else if (isDir && activeNorm.startsWith(targetPrefix + '/')) {
548
+ const sub = activeNorm.slice(targetPrefix.length);
549
+ globalActiveTabId = 'tab:' + newNorm.toLowerCase() + sub;
550
+ }
551
+ }
552
+ saveTabsToStorage();
553
+ notifyStateChange();
554
+ }
555
+ }
556
+
557
+ function removeOpenTabsForPath(targetPath, isDir = false) {
558
+ if (!targetPath) return;
559
+ let changed = false;
560
+ const delNorm = targetPath.replace(/\\/g, '/').toLowerCase();
561
+ const nextTabs = globalTabs.filter(tab => {
562
+ const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
563
+ if (!isDir) {
564
+ if (tabNorm === delNorm) {
565
+ changed = true;
566
+ return false;
567
+ }
568
+ } else {
569
+ if (tabNorm === delNorm || tabNorm.startsWith(delNorm + '/')) {
570
+ changed = true;
571
+ return false;
572
+ }
573
+ }
574
+ return true;
575
+ });
576
+
577
+ if (changed) {
578
+ globalTabs = nextTabs;
579
+ if (!globalTabs.some(t => t.id === globalActiveTabId)) {
580
+ globalActiveTabId = globalTabs.length > 0 ? globalTabs[0].id : null;
581
+ }
582
+ saveTabsToStorage();
583
+ notifyStateChange();
584
+ }
585
+ }
586
+
587
+
497
588
  // =========================================================================
498
589
  // Toast Notification Utility
499
590
  // =========================================================================
@@ -949,12 +1040,262 @@
949
1040
  }
950
1041
 
951
1042
  // =========================================================================
952
- // React Components: Workspace File Tree Explorer
1043
+ // React Components: Workspace File Tree Explorer (with DnD, Create & Move)
953
1044
  // =========================================================================
954
- function FileTreeNodeItem({ node, depth = 0, expandedPaths, onToggleExpand, activeFilePath, onOpenFile, searchQuery }) {
1045
+
1046
+ function isValidDropTarget(draggedItem, targetNode) {
1047
+ if (!draggedItem || !targetNode) return false;
1048
+ if (!targetNode.isDir) return false;
1049
+ const src = draggedItem.path.replace(/\\/g, '/').toLowerCase();
1050
+ const dest = targetNode.path.replace(/\\/g, '/').toLowerCase();
1051
+ // Cannot drop on itself
1052
+ if (src === dest) return false;
1053
+ // Cannot drop into current parent directory (already there)
1054
+ const lastSlash = src.lastIndexOf('/');
1055
+ const srcParent = lastSlash > 0 ? src.substring(0, lastSlash) : '';
1056
+ if (srcParent && srcParent === dest) return false;
1057
+ // If dragged is a dir, cannot drop into its own children or itself
1058
+ if (draggedItem.isDir) {
1059
+ if (dest.startsWith(src + '/')) return false;
1060
+ }
1061
+ return true;
1062
+ }
1063
+
1064
+ function InlineCreateInputNode({ depth = 0, type, onConfirm, onCancel }) {
1065
+ const [val, setVal] = useState('');
1066
+ const inputRef = useRef(null);
1067
+
1068
+ useEffect(() => {
1069
+ if (inputRef.current) {
1070
+ inputRef.current.focus();
1071
+ inputRef.current.select();
1072
+ }
1073
+ }, []);
1074
+
1075
+ const handleKeyDown = (e) => {
1076
+ if (e.key === 'Enter') {
1077
+ e.preventDefault();
1078
+ e.stopPropagation();
1079
+ if (val.trim()) onConfirm(val.trim());
1080
+ else onCancel();
1081
+ } else if (e.key === 'Escape') {
1082
+ e.preventDefault();
1083
+ e.stopPropagation();
1084
+ onCancel();
1085
+ }
1086
+ };
1087
+
1088
+ const handleBlur = () => {
1089
+ if (!val.trim()) onCancel();
1090
+ };
1091
+
1092
+ return h('div', {
1093
+ className: 'dsh-tree-inline-input-row',
1094
+ style: { paddingLeft: `${depth * 14 + 6}px` },
1095
+ onClick: (e) => e.stopPropagation(),
1096
+ },
1097
+ h('span', { className: 'dsh-tree-chevron-placeholder' }),
1098
+ h('span', {
1099
+ className: 'dsh-tree-icon',
1100
+ style: { color: type === 'dir' ? '#eab308' : '#38bdf8' },
1101
+ }, type === 'dir' ? '📁' : '📄'),
1102
+ h('input', {
1103
+ ref: inputRef,
1104
+ type: 'text',
1105
+ className: 'dsh-tree-inline-input',
1106
+ value: val,
1107
+ placeholder: type === 'dir' ? '新文件夹名称...' : '新文件名 (如 doc.md)...',
1108
+ onChange: (e) => setVal(e.target.value),
1109
+ onKeyDown: handleKeyDown,
1110
+ onBlur: handleBlur,
1111
+ })
1112
+ );
1113
+ }
1114
+
1115
+ function InlineRenameInputNode({ node, depth = 0, onConfirm, onCancel }) {
1116
+ const [val, setVal] = useState(node.name);
1117
+ const inputRef = useRef(null);
1118
+
1119
+ useEffect(() => {
1120
+ if (inputRef.current) {
1121
+ inputRef.current.focus();
1122
+ if (!node.isDir && node.name.includes('.')) {
1123
+ const lastDot = node.name.lastIndexOf('.');
1124
+ if (lastDot > 0) {
1125
+ inputRef.current.setSelectionRange(0, lastDot);
1126
+ return;
1127
+ }
1128
+ }
1129
+ inputRef.current.select();
1130
+ }
1131
+ }, [node]);
1132
+
1133
+ const handleKeyDown = (e) => {
1134
+ if (e.key === 'Enter') {
1135
+ e.preventDefault();
1136
+ e.stopPropagation();
1137
+ if (val.trim() && val.trim() !== node.name) {
1138
+ onConfirm(val.trim());
1139
+ } else {
1140
+ onCancel();
1141
+ }
1142
+ } else if (e.key === 'Escape') {
1143
+ e.preventDefault();
1144
+ e.stopPropagation();
1145
+ onCancel();
1146
+ }
1147
+ };
1148
+
1149
+ const handleBlur = () => {
1150
+ onCancel();
1151
+ };
1152
+
1153
+ const iconInfo = getFileIcon(node);
1154
+
1155
+ return h('div', {
1156
+ className: 'dsh-tree-inline-input-row',
1157
+ style: { paddingLeft: `${depth * 14 + 6}px` },
1158
+ onClick: (e) => e.stopPropagation(),
1159
+ },
1160
+ h('span', { className: 'dsh-tree-chevron-placeholder' }),
1161
+ h('span', {
1162
+ className: 'dsh-tree-icon',
1163
+ style: { color: node.isDir ? '#eab308' : iconInfo.color },
1164
+ }, node.isDir ? '📁' : iconInfo.icon),
1165
+ h('input', {
1166
+ ref: inputRef,
1167
+ type: 'text',
1168
+ className: 'dsh-tree-inline-input',
1169
+ value: val,
1170
+ onChange: (e) => setVal(e.target.value),
1171
+ onKeyDown: handleKeyDown,
1172
+ onBlur: handleBlur,
1173
+ })
1174
+ );
1175
+ }
1176
+
1177
+ function TreeContextMenu({ x, y, node, isRoot, clipboard, onAction, onClose }) {
1178
+ const menuRef = useRef(null);
1179
+ useEffect(() => {
1180
+ const handleDown = (e) => {
1181
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
1182
+ onClose();
1183
+ }
1184
+ };
1185
+ window.addEventListener('mousedown', handleDown, true);
1186
+ window.addEventListener('scroll', onClose, true);
1187
+ return () => {
1188
+ window.removeEventListener('mousedown', handleDown, true);
1189
+ window.removeEventListener('scroll', onClose, true);
1190
+ };
1191
+ }, [onClose]);
1192
+
1193
+ const style = {
1194
+ position: 'fixed',
1195
+ top: `${Math.min(Math.max(10, y), window.innerHeight - 300)}px`,
1196
+ left: `${Math.min(Math.max(10, x), window.innerWidth - 210)}px`,
1197
+ zIndex: 99999,
1198
+ };
1199
+
1200
+ const isDir = isRoot || (node && node.isDir);
1201
+
1202
+ return h('div', { ref: menuRef, className: 'dsh-tree-context-menu', style },
1203
+ isDir ? [
1204
+ h('div', { key: 'new-file', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-file', node); } },
1205
+ h('span', { className: 'dsh-menu-icon' }, '📄'),
1206
+ h('span', null, '新建文件')
1207
+ ),
1208
+ h('div', { key: 'new-dir', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-dir', node); } },
1209
+ h('span', { className: 'dsh-menu-icon' }, '📁'),
1210
+ h('span', null, '新建文件夹')
1211
+ ),
1212
+ h('div', { key: 'div-1', className: 'dsh-menu-divider' }),
1213
+ ] : null,
1214
+
1215
+ !isRoot && !isDir ? h('div', { key: 'preview', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('preview', node); } },
1216
+ h('span', { className: 'dsh-menu-icon' }, '📖'),
1217
+ h('span', null, '打开预览')
1218
+ ) : null,
1219
+
1220
+ !isRoot ? h('div', { key: 'cut', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('cut', node); } },
1221
+ h('span', { className: 'dsh-menu-icon' }, '✂️'),
1222
+ h('span', null, '剪切')
1223
+ ) : null,
1224
+
1225
+ isDir && clipboard && clipboard.action === 'cut' ? h('div', {
1226
+ key: 'paste',
1227
+ className: 'dsh-menu-item',
1228
+ onClick: () => { onClose(); onAction('paste', node); }
1229
+ },
1230
+ h('span', { className: 'dsh-menu-icon' }, '📋'),
1231
+ h('span', null, `粘贴 "${clipboard.name}" 到此处`)
1232
+ ) : null,
1233
+
1234
+ !isRoot ? h('div', { key: 'rename', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('rename', node); } },
1235
+ h('span', { className: 'dsh-menu-icon' }, '🏷️'),
1236
+ h('span', null, '重命名')
1237
+ ) : null,
1238
+
1239
+ !isRoot ? h('div', { key: 'div-2', className: 'dsh-menu-divider' }) : null,
1240
+
1241
+ !isRoot ? h('div', { key: 'reveal', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('reveal', node); } },
1242
+ h('span', { className: 'dsh-menu-icon' }, '📂'),
1243
+ h('span', null, '在文件管理器中定位')
1244
+ ) : null,
1245
+
1246
+ !isRoot && !isDir ? h('div', { key: 'native', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('native', node); } },
1247
+ h('span', { className: 'dsh-menu-icon' }, '🌐'),
1248
+ h('span', null, '系统默认应用打开')
1249
+ ) : null,
1250
+
1251
+ !isRoot ? [
1252
+ h('div', { key: 'div-3', className: 'dsh-menu-divider' }),
1253
+ h('div', { key: 'delete', className: 'dsh-menu-item dsh-menu-item-danger', onClick: () => { onClose(); onAction('delete', node); } },
1254
+ h('span', { className: 'dsh-menu-icon' }, '🗑️'),
1255
+ h('span', null, '删除')
1256
+ )
1257
+ ] : null,
1258
+
1259
+ isRoot ? [
1260
+ h('div', { key: 'refresh', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('refresh'); } },
1261
+ h('span', { className: 'dsh-menu-icon' }, '🔄'),
1262
+ h('span', null, '刷新目录树')
1263
+ )
1264
+ ] : null
1265
+ );
1266
+ }
1267
+
1268
+ function FileTreeNodeItem({
1269
+ node,
1270
+ depth = 0,
1271
+ expandedPaths,
1272
+ onToggleExpand,
1273
+ activeFilePath,
1274
+ onOpenFile,
1275
+ searchQuery,
1276
+ creatingItem,
1277
+ onConfirmCreate,
1278
+ onCancelCreate,
1279
+ renamingPath,
1280
+ onConfirmRename,
1281
+ onCancelRename,
1282
+ onContextMenu,
1283
+ draggedNode,
1284
+ dragOverNode,
1285
+ onDragStart,
1286
+ onDragEnd,
1287
+ onDragOver,
1288
+ onDragLeave,
1289
+ onDrop,
1290
+ clipboard,
1291
+ }) {
955
1292
  const isDir = node.isDir;
956
1293
  const isExpanded = expandedPaths.has(node.path);
957
1294
  const isActive = !isDir && activeFilePath && activeFilePath.toLowerCase() === node.path.toLowerCase();
1295
+ const isRenaming = renamingPath && renamingPath.toLowerCase() === node.path.toLowerCase();
1296
+ const isDragged = draggedNode && draggedNode.path && draggedNode.path.toLowerCase() === node.path.toLowerCase();
1297
+ const isDropTarget = isDir && dragOverNode && dragOverNode.path && dragOverNode.path.toLowerCase() === node.path.toLowerCase();
1298
+ const isCut = clipboard && clipboard.action === 'cut' && clipboard.path && clipboard.path.toLowerCase() === node.path.toLowerCase();
958
1299
  const iconInfo = getFileIcon(node);
959
1300
 
960
1301
  const indentStyle = { paddingLeft: `${depth * 14 + 6}px` };
@@ -968,16 +1309,70 @@
968
1309
  }
969
1310
  };
970
1311
 
1312
+ const handleContextMenu = (e) => {
1313
+ e.preventDefault();
1314
+ e.stopPropagation();
1315
+ onContextMenu(node, e);
1316
+ };
1317
+
1318
+ const handleDragStart = (e) => {
1319
+ e.stopPropagation();
1320
+ e.dataTransfer.setData('text/plain', node.path);
1321
+ e.dataTransfer.effectAllowed = 'move';
1322
+ onDragStart(node);
1323
+ };
1324
+
1325
+ const handleDragOver = (e) => {
1326
+ if (isDir && isValidDropTarget(draggedNode, node)) {
1327
+ e.preventDefault();
1328
+ e.stopPropagation();
1329
+ e.dataTransfer.dropEffect = 'move';
1330
+ onDragOver(node);
1331
+ }
1332
+ };
1333
+
1334
+ const handleDragLeave = (e) => {
1335
+ if (isDir) {
1336
+ e.stopPropagation();
1337
+ onDragLeave(node);
1338
+ }
1339
+ };
1340
+
1341
+ const handleDrop = (e) => {
1342
+ if (isDir && isValidDropTarget(draggedNode, node)) {
1343
+ e.preventDefault();
1344
+ e.stopPropagation();
1345
+ onDrop(draggedNode, node);
1346
+ }
1347
+ };
1348
+
1349
+ if (isRenaming) {
1350
+ return h(InlineRenameInputNode, {
1351
+ node,
1352
+ depth,
1353
+ onConfirm: (newName) => onConfirmRename(node, newName),
1354
+ onCancel: onCancelRename,
1355
+ });
1356
+ }
1357
+
971
1358
  return h(Fragment, null,
972
1359
  h('div', {
973
- className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''}`,
1360
+ className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''} ${isDropTarget ? 'drop-target' : ''} ${isDragged ? 'is-dragged' : ''} ${isCut ? 'is-cut' : ''}`,
974
1361
  style: indentStyle,
975
1362
  onClick: handleClick,
1363
+ onContextMenu: handleContextMenu,
1364
+ draggable: true,
1365
+ onDragStart: handleDragStart,
1366
+ onDragEnd: onDragEnd,
1367
+ onDragOver: handleDragOver,
1368
+ onDragLeave: handleDragLeave,
1369
+ onDrop: handleDrop,
976
1370
  title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
977
1371
  },
978
1372
  // Chevron indicator for directory
979
1373
  isDir ? h('span', {
980
1374
  className: `dsh-tree-chevron ${isExpanded ? 'expanded' : ''}`,
1375
+ onClick: (e) => { e.stopPropagation(); onToggleExpand(node.path); },
981
1376
  }, isExpanded ? '▼' : '▶') : h('span', { className: 'dsh-tree-chevron-placeholder' }),
982
1377
 
983
1378
  // File / Directory Icon
@@ -995,18 +1390,47 @@
995
1390
  )
996
1391
  ),
997
1392
 
998
- // Children if expanded
999
- isDir && isExpanded && node.children && node.children.length > 0 ? (
1000
- node.children.map(child => h(FileTreeNodeItem, {
1001
- key: child.path,
1002
- node: child,
1003
- depth: depth + 1,
1004
- expandedPaths,
1005
- onToggleExpand,
1006
- activeFilePath,
1007
- onOpenFile,
1008
- searchQuery,
1009
- }))
1393
+ // Children or Inline creation node if expanded
1394
+ isDir && isExpanded ? (
1395
+ h(Fragment, null,
1396
+ // If creating inside this directory, render inline creation node first
1397
+ creatingItem && creatingItem.parentPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === node.path.replace(/\\/g, '/').toLowerCase() ? (
1398
+ h(InlineCreateInputNode, {
1399
+ depth: depth + 1,
1400
+ type: creatingItem.type,
1401
+ onConfirm: onConfirmCreate,
1402
+ onCancel: onCancelCreate,
1403
+ })
1404
+ ) : null,
1405
+
1406
+ node.children && node.children.length > 0 ? (
1407
+ node.children.map(child => h(FileTreeNodeItem, {
1408
+ key: child.path,
1409
+ node: child,
1410
+ depth: depth + 1,
1411
+ expandedPaths,
1412
+ onToggleExpand,
1413
+ activeFilePath,
1414
+ onOpenFile,
1415
+ searchQuery,
1416
+ creatingItem,
1417
+ onConfirmCreate,
1418
+ onCancelCreate,
1419
+ renamingPath,
1420
+ onConfirmRename,
1421
+ onCancelRename,
1422
+ onContextMenu,
1423
+ draggedNode,
1424
+ dragOverNode,
1425
+ onDragStart,
1426
+ onDragEnd,
1427
+ onDragOver,
1428
+ onDragLeave,
1429
+ onDrop,
1430
+ clipboard,
1431
+ }))
1432
+ ) : null
1433
+ )
1010
1434
  ) : null
1011
1435
  );
1012
1436
  }
@@ -1060,6 +1484,15 @@
1060
1484
  const [isSelectorOpen, setIsSelectorOpen] = useState(false);
1061
1485
  const [customPathInput, setCustomPathInput] = useState('');
1062
1486
  const [validating, setValidating] = useState(false);
1487
+
1488
+ // File operations state
1489
+ const [creatingItem, setCreatingItem] = useState(null); // { parentPath: string, type: 'dir' | 'file' } | null
1490
+ const [renamingPath, setRenamingPath] = useState(null); // string | null
1491
+ const [contextMenu, setContextMenu] = useState(null); // { x: number, y: number, node: any, isRoot: boolean } | null
1492
+ const [draggedNode, setDraggedNode] = useState(null);
1493
+ const [dragOverNode, setDragOverNode] = useState(null);
1494
+ const [clipboard, setClipboard] = useState(null); // { action: 'cut', path: string, name: string, isDir: boolean } | null
1495
+
1063
1496
  const currentWs = getCurrentWorkspaceRoot();
1064
1497
 
1065
1498
  const loadTree = useCallback(async (force = false) => {
@@ -1164,6 +1597,168 @@
1164
1597
  showToast('已恢复根据当前会话自动识别工作空间', 'info');
1165
1598
  };
1166
1599
 
1600
+ // =========================================================================
1601
+ // File Operations Handlers
1602
+ // =========================================================================
1603
+ const handleStartCreate = (parentPath, type) => {
1604
+ const root = treeData?.workspaceRoot || currentWs;
1605
+ const target = parentPath || root;
1606
+ setCreatingItem({ parentPath: target, type });
1607
+ if (target) {
1608
+ setExpandedPaths(prev => new Set(prev).add(target));
1609
+ }
1610
+ };
1611
+
1612
+ const handleConfirmCreate = async (name) => {
1613
+ if (!creatingItem || !name) return;
1614
+ const isDir = creatingItem.type === 'dir';
1615
+ const endpoint = isDir ? '/api/preview/create-dir' : '/api/preview/create-file';
1616
+ try {
1617
+ const res = await fetch(endpoint, {
1618
+ method: 'POST',
1619
+ headers: { 'Content-Type': 'application/json' },
1620
+ body: JSON.stringify({
1621
+ parentPath: creatingItem.parentPath,
1622
+ name: name.trim(),
1623
+ }),
1624
+ });
1625
+ const json = await res.json();
1626
+ if (json.ok) {
1627
+ setCreatingItem(null);
1628
+ setExpandedPaths(prev => new Set(prev).add(creatingItem.parentPath));
1629
+ await loadTree(true);
1630
+ showToast(`已创建${isDir ? '文件夹' : '文件'}: ${json.name}`, 'success');
1631
+ if (!isDir && json.path) {
1632
+ openPreviewFile(json.path);
1633
+ }
1634
+ } else {
1635
+ showToast(json?.error?.message || `创建${isDir ? '文件夹' : '文件'}失败`, 'error');
1636
+ }
1637
+ } catch (err) {
1638
+ showToast(`创建失败: ${err.message}`, 'error');
1639
+ }
1640
+ };
1641
+
1642
+ const handleConfirmRename = async (node, newName) => {
1643
+ if (!node || !newName || newName.trim() === node.name) {
1644
+ setRenamingPath(null);
1645
+ return;
1646
+ }
1647
+ try {
1648
+ const res = await fetch('/api/preview/rename', {
1649
+ method: 'POST',
1650
+ headers: { 'Content-Type': 'application/json' },
1651
+ body: JSON.stringify({
1652
+ path: node.path,
1653
+ newName: newName.trim(),
1654
+ }),
1655
+ });
1656
+ const json = await res.json();
1657
+ if (json.ok) {
1658
+ setRenamingPath(null);
1659
+ renameOpenTabPaths(json.sourcePath, json.targetPath, node.isDir);
1660
+ await loadTree(true);
1661
+ showToast(`已重命名为: ${json.name}`, 'success');
1662
+ } else {
1663
+ showToast(json?.error?.message || '重命名失败', 'error');
1664
+ }
1665
+ } catch (err) {
1666
+ showToast(`重命名失败: ${err.message}`, 'error');
1667
+ }
1668
+ };
1669
+
1670
+ const handleMove = async (sourcePath, targetDirPath) => {
1671
+ if (!sourcePath || !targetDirPath) return;
1672
+ try {
1673
+ const res = await fetch('/api/preview/move', {
1674
+ method: 'POST',
1675
+ headers: { 'Content-Type': 'application/json' },
1676
+ body: JSON.stringify({
1677
+ sourcePath,
1678
+ targetPath: targetDirPath,
1679
+ }),
1680
+ });
1681
+ const json = await res.json();
1682
+ if (json.ok) {
1683
+ if (json.noop) {
1684
+ showToast('目标路径与源路径相同', 'info');
1685
+ return;
1686
+ }
1687
+ renameOpenTabPaths(json.sourcePath, json.targetPath, json.isDir);
1688
+ setExpandedPaths(prev => new Set(prev).add(targetDirPath));
1689
+ await loadTree(true);
1690
+ showToast(`已移动 ${json.name} 到 ${getFileName(targetDirPath)}`, 'success');
1691
+ } else {
1692
+ showToast(json?.error?.message || '移动文件失败', 'error');
1693
+ }
1694
+ } catch (err) {
1695
+ showToast(`移动失败: ${err.message}`, 'error');
1696
+ }
1697
+ };
1698
+
1699
+ const handleDelete = async (node) => {
1700
+ if (!node) return;
1701
+ const isDir = node.isDir;
1702
+ const msg = `确定要永久删除${isDir ? '文件夹' : '文件'} "${node.name}" 吗?此操作不可恢复。`;
1703
+ if (typeof window !== 'undefined' && !window.confirm(msg)) return;
1704
+
1705
+ try {
1706
+ const res = await fetch('/api/preview/delete', {
1707
+ method: 'POST',
1708
+ headers: { 'Content-Type': 'application/json' },
1709
+ body: JSON.stringify({ path: node.path }),
1710
+ });
1711
+ const json = await res.json();
1712
+ if (json.ok) {
1713
+ removeOpenTabsForPath(node.path, isDir);
1714
+ await loadTree(true);
1715
+ showToast(`已删除${isDir ? '文件夹' : '文件'}: ${node.name}`, 'info');
1716
+ } else {
1717
+ showToast(json?.error?.message || '删除失败', 'error');
1718
+ }
1719
+ } catch (err) {
1720
+ showToast(`删除失败: ${err.message}`, 'error');
1721
+ }
1722
+ };
1723
+
1724
+ const handleContextMenuAction = (action, node) => {
1725
+ const rootPath = treeData?.workspaceRoot || currentWs;
1726
+ if (action === 'new-file') {
1727
+ const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
1728
+ handleStartCreate(target, 'file');
1729
+ } else if (action === 'new-dir') {
1730
+ const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
1731
+ handleStartCreate(target, 'dir');
1732
+ } else if (action === 'preview') {
1733
+ if (node && !node.isDir) onOpenFile(node.path);
1734
+ } else if (action === 'cut') {
1735
+ if (node) {
1736
+ setClipboard({ action: 'cut', path: node.path, name: node.name, isDir: node.isDir });
1737
+ showToast(`已剪切: ${node.name}`, 'info');
1738
+ }
1739
+ } else if (action === 'paste') {
1740
+ if (clipboard && clipboard.action === 'cut') {
1741
+ const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
1742
+ handleMove(clipboard.path, target);
1743
+ setClipboard(null);
1744
+ }
1745
+ } else if (action === 'rename') {
1746
+ if (node) setRenamingPath(node.path);
1747
+ } else if (action === 'reveal') {
1748
+ if (node) triggerRevealInExplorer(node.path);
1749
+ } else if (action === 'native') {
1750
+ if (node) triggerOpenNative(node.path);
1751
+ } else if (action === 'delete') {
1752
+ if (node) handleDelete(node);
1753
+ } else if (action === 'refresh') {
1754
+ loadTree(true);
1755
+ }
1756
+ };
1757
+
1758
+ const rootPath = treeData?.workspaceRoot || currentWs;
1759
+ const isCreatingAtRoot = creatingItem && creatingItem.parentPath && rootPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
1760
+ const isRootDropTarget = dragOverNode && rootPath && dragOverNode.path && dragOverNode.path.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
1761
+
1167
1762
  return h('div', {
1168
1763
  className: 'dsh-tree-sidebar',
1169
1764
  style: { width: `${width}px` },
@@ -1186,6 +1781,18 @@
1186
1781
  onClick: () => loadTree(true),
1187
1782
  title: '刷新文件树',
1188
1783
  }, '🔄'),
1784
+ h('button', {
1785
+ type: 'button',
1786
+ className: 'dsh-tree-action-btn',
1787
+ onClick: () => handleStartCreate(rootPath, 'file'),
1788
+ title: '在根目录新建文件',
1789
+ }, '📄➕'),
1790
+ h('button', {
1791
+ type: 'button',
1792
+ className: 'dsh-tree-action-btn',
1793
+ onClick: () => handleStartCreate(rootPath, 'dir'),
1794
+ title: '在根目录新建文件夹',
1795
+ }, '📁➕'),
1189
1796
  h('button', {
1190
1797
  type: 'button',
1191
1798
  className: 'dsh-tree-action-btn',
@@ -1283,7 +1890,40 @@
1283
1890
  ),
1284
1891
 
1285
1892
  // Tree Content
1286
- h('div', { className: 'dsh-tree-node-list' },
1893
+ h('div', {
1894
+ className: `dsh-tree-node-list ${isRootDropTarget ? 'drop-target-root' : ''}`,
1895
+ onContextMenu: (e) => {
1896
+ e.preventDefault();
1897
+ setContextMenu({ x: e.clientX, y: e.clientY, node: null, isRoot: true });
1898
+ },
1899
+ onDragOver: (e) => {
1900
+ if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
1901
+ e.preventDefault();
1902
+ e.dataTransfer.dropEffect = 'move';
1903
+ setDragOverNode({ path: rootPath, isDir: true });
1904
+ }
1905
+ },
1906
+ onDragLeave: () => {
1907
+ if (dragOverNode && dragOverNode.path === rootPath) {
1908
+ setDragOverNode(null);
1909
+ }
1910
+ },
1911
+ onDrop: (e) => {
1912
+ e.preventDefault();
1913
+ setDragOverNode(null);
1914
+ if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
1915
+ handleMove(draggedNode.path, rootPath);
1916
+ }
1917
+ },
1918
+ },
1919
+ // If creating at root level, render inline create input at top
1920
+ isCreatingAtRoot ? h(InlineCreateInputNode, {
1921
+ depth: 0,
1922
+ type: creatingItem.type,
1923
+ onConfirm: handleConfirmCreate,
1924
+ onCancel: () => setCreatingItem(null),
1925
+ }) : null,
1926
+
1287
1927
  isLoading ? (
1288
1928
  h('div', { className: 'dsh-tree-loading' }, '正在扫描工作区文件...')
1289
1929
  ) : error ? (
@@ -1295,7 +1935,7 @@
1295
1935
  onClick: () => loadTree(true),
1296
1936
  }, '重试')
1297
1937
  )
1298
- ) : displayTree.length === 0 ? (
1938
+ ) : displayTree.length === 0 && !isCreatingAtRoot ? (
1299
1939
  h('div', { className: 'dsh-tree-empty' }, searchQuery ? '未匹配到相关文件' : '工作区暂无文件')
1300
1940
  ) : (
1301
1941
  displayTree.map(node => h(FileTreeNodeItem, {
@@ -1307,10 +1947,42 @@
1307
1947
  activeFilePath,
1308
1948
  onOpenFile,
1309
1949
  searchQuery,
1950
+ creatingItem,
1951
+ onConfirmCreate: handleConfirmCreate,
1952
+ onCancelCreate: () => setCreatingItem(null),
1953
+ renamingPath,
1954
+ onConfirmRename: handleConfirmRename,
1955
+ onCancelRename: () => setRenamingPath(null),
1956
+ onContextMenu: (targetNode, e) => {
1957
+ setContextMenu({ x: e.clientX, y: e.clientY, node: targetNode, isRoot: false });
1958
+ },
1959
+ draggedNode,
1960
+ dragOverNode,
1961
+ onDragStart: (targetNode) => setDraggedNode(targetNode),
1962
+ onDragEnd: () => { setDraggedNode(null); setDragOverNode(null); },
1963
+ onDragOver: (targetNode) => setDragOverNode(targetNode),
1964
+ onDragLeave: () => setDragOverNode(null),
1965
+ onDrop: (source, target) => {
1966
+ setDraggedNode(null);
1967
+ setDragOverNode(null);
1968
+ handleMove(source.path, target.path);
1969
+ },
1970
+ clipboard,
1310
1971
  }))
1311
1972
  )
1312
1973
  ),
1313
1974
 
1975
+ // Context Menu Overlay
1976
+ contextMenu ? h(TreeContextMenu, {
1977
+ x: contextMenu.x,
1978
+ y: contextMenu.y,
1979
+ node: contextMenu.node,
1980
+ isRoot: contextMenu.isRoot,
1981
+ clipboard,
1982
+ onAction: handleContextMenuAction,
1983
+ onClose: () => setContextMenu(null),
1984
+ }) : null,
1985
+
1314
1986
  // Sidebar Footer with file count stats
1315
1987
  treeData ? h('div', { className: 'dsh-tree-sidebar-footer' },
1316
1988
  h('span', null, `${treeData.totalDirs || 0} 目录`),
@@ -2242,22 +2914,62 @@
2242
2914
  }
2243
2915
 
2244
2916
  // =========================================================================
2245
- // React Component: CodeEditorView
2917
+ // React Component: CodeEditorView (Unified line-row architecture)
2246
2918
  // =========================================================================
2247
- function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
2248
- const lines = useMemo(() => (content ? content.split('\n') : []), [content]);
2919
+ function splitHighlightedHtmlIntoLines(html) {
2920
+ if (!html) return [];
2921
+ const rawLines = html.split('\n');
2922
+ const lines = [];
2923
+ const openTags = [];
2924
+
2925
+ for (let i = 0; i < rawLines.length; i++) {
2926
+ let line = rawLines[i];
2927
+ let prefix = '';
2928
+ if (openTags.length > 0) {
2929
+ prefix = openTags.map(t => '<' + t.name + (t.attrs ? ' ' + t.attrs : '') + '>').join('');
2930
+ }
2931
+
2932
+ const tagRegex = /<\/?([a-zA-Z0-9\-]+)([^>]*)>/g;
2933
+ let match;
2934
+ while ((match = tagRegex.exec(line)) !== null) {
2935
+ const isClosing = match[0].startsWith('</');
2936
+ const tagName = match[1];
2937
+ const attrs = match[2];
2938
+ if (isClosing) {
2939
+ const lastIdx = openTags.map(t => t.name).lastIndexOf(tagName);
2940
+ if (lastIdx !== -1) {
2941
+ openTags.splice(lastIdx, 1);
2942
+ }
2943
+ } else if (!match[0].endsWith('/>')) {
2944
+ openTags.push({ name: tagName, attrs: attrs.trim() });
2945
+ }
2946
+ }
2249
2947
 
2250
- const highlightedHtml = useMemo(() => {
2948
+ let suffix = '';
2949
+ if (openTags.length > 0) {
2950
+ suffix = openTags.slice().reverse().map(t => '</' + t.name + '>').join('');
2951
+ }
2952
+
2953
+ lines.push(prefix + line + suffix);
2954
+ }
2955
+
2956
+ return lines;
2957
+ }
2958
+
2959
+ function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
2960
+ const normalizedContent = useMemo(() => {
2251
2961
  if (!content) return '';
2962
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
2963
+ }, [content]);
2964
+
2965
+ const highlightedLines = useMemo(() => {
2966
+ if (!normalizedContent) return [];
2967
+ let html = '';
2252
2968
  if (!isHighlighted) {
2253
- let escaped = escapeHtml(content);
2254
- if (searchQuery.trim()) {
2255
- const q = escapeHtml(searchQuery.trim());
2256
- escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
2257
- }
2258
- return escaped;
2969
+ html = escapeHtml(normalizedContent);
2970
+ } else {
2971
+ html = highlightCode(normalizedContent, category);
2259
2972
  }
2260
- let html = highlightCode(content, category);
2261
2973
  if (searchQuery.trim()) {
2262
2974
  try {
2263
2975
  const q = escapeHtml(searchQuery.trim());
@@ -2265,27 +2977,34 @@
2265
2977
  html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
2266
2978
  } catch (e) {}
2267
2979
  }
2268
- return html;
2269
- }, [content, category, isHighlighted, searchQuery]);
2980
+ return splitHighlightedHtmlIntoLines(html);
2981
+ }, [normalizedContent, category, isHighlighted, searchQuery]);
2982
+
2983
+ const lineCount = highlightedLines.length;
2984
+ const digitCount = Math.max(2, String(lineCount).length);
2985
+ const gutterWidth = Math.max(24, digitCount * 7.5 + 12);
2270
2986
 
2271
2987
  return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
2272
- h('div', { className: 'dsh-code-gutter' },
2273
- lines.map((_, i) => h('div', {
2274
- key: i,
2275
- id: `line-gutter-${i + 1}`,
2276
- className: 'dsh-gutter-line',
2277
- onClick: () => onLineClick && onLineClick(i + 1),
2278
- title: `第 ${i + 1} 行 (点击定位)`
2279
- }, i + 1))
2280
- ),
2281
- h('div', { className: 'dsh-code-content' },
2282
- h('pre', { className: 'dsh-code-pre' },
2283
- h('code', {
2284
- className: `dsh-code-lang-${category}`,
2285
- dangerouslySetInnerHTML: { __html: highlightedHtml }
2988
+ highlightedLines.map((lineHtml, i) => {
2989
+ const lineNum = i + 1;
2990
+ return h('div', {
2991
+ key: lineNum,
2992
+ id: `line-row-${lineNum}`,
2993
+ className: 'dsh-code-line',
2994
+ },
2995
+ h('span', {
2996
+ id: `line-gutter-${lineNum}`,
2997
+ className: 'dsh-line-gutter',
2998
+ style: { minWidth: `${gutterWidth}px` },
2999
+ onClick: () => onLineClick && onLineClick(lineNum),
3000
+ title: `第 ${lineNum} 行 (点击定位)`,
3001
+ }, lineNum),
3002
+ h('span', {
3003
+ className: 'dsh-line-code',
3004
+ dangerouslySetInnerHTML: { __html: lineHtml ? lineHtml : ' ' },
2286
3005
  })
2287
- )
2288
- )
3006
+ );
3007
+ })
2289
3008
  );
2290
3009
  }
2291
3010
 
@@ -2387,7 +3106,9 @@
2387
3106
  const el = contentRef.current.querySelector(`#${item.id}`);
2388
3107
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
2389
3108
  } else {
2390
- const el = document.getElementById(`line-gutter-${item.line}`);
3109
+ const rowEl = document.getElementById(`line-row-${item.line}`);
3110
+ const gutterEl = document.getElementById(`line-gutter-${item.line}`);
3111
+ const el = rowEl || gutterEl;
2391
3112
  if (el) {
2392
3113
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2393
3114
  el.classList.add('dsh-line-highlight-flash');
@@ -4741,6 +5462,124 @@
4741
5462
  }
4742
5463
  .dsh-tree-footer-dot { font-size: 8px; }
4743
5464
 
5465
+ /* Drag & Drop & Cut Styles */
5466
+ .dsh-tree-node-row.drop-target {
5467
+ background: rgba(14, 165, 233, 0.22) !important;
5468
+ outline: 1px dashed #0284c7 !important;
5469
+ color: #0284c7 !important;
5470
+ }
5471
+ body[data-ds-dark-theme] .dsh-tree-node-row.drop-target {
5472
+ background: rgba(56, 189, 248, 0.25) !important;
5473
+ outline-color: #38bdf8 !important;
5474
+ color: #38bdf8 !important;
5475
+ }
5476
+ .dsh-tree-node-list.drop-target-root {
5477
+ background: rgba(14, 165, 233, 0.08) !important;
5478
+ outline: 2px dashed #0284c7 !important;
5479
+ outline-offset: -4px;
5480
+ }
5481
+ body[data-ds-dark-theme] .dsh-tree-node-list.drop-target-root {
5482
+ background: rgba(56, 189, 248, 0.1) !important;
5483
+ outline-color: #38bdf8 !important;
5484
+ }
5485
+ .dsh-tree-node-row.is-dragged {
5486
+ opacity: 0.45;
5487
+ filter: grayscale(0.6);
5488
+ }
5489
+ .dsh-tree-node-row.is-cut {
5490
+ opacity: 0.5;
5491
+ filter: grayscale(0.5);
5492
+ border: 1px dashed #94a3b8;
5493
+ }
5494
+
5495
+ /* Inline Creation & Rename Input */
5496
+ .dsh-tree-inline-input-row {
5497
+ display: flex;
5498
+ align-items: center;
5499
+ height: 24px;
5500
+ margin: 1px 4px;
5501
+ gap: 3px;
5502
+ box-sizing: border-box;
5503
+ }
5504
+ .dsh-tree-inline-input {
5505
+ flex: 1;
5506
+ height: 20px;
5507
+ border-radius: 3px;
5508
+ border: 1px solid #38bdf8;
5509
+ background: var(--dsw-alias-bg-base, #ffffff);
5510
+ color: inherit;
5511
+ font-size: 11px;
5512
+ padding: 0 4px;
5513
+ outline: none;
5514
+ box-sizing: border-box;
5515
+ box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.4);
5516
+ }
5517
+ body[data-ds-dark-theme] .dsh-tree-inline-input {
5518
+ background: #1e2330;
5519
+ color: #f1f5f9;
5520
+ border-color: #38bdf8;
5521
+ }
5522
+
5523
+ /* Tree Context Menu */
5524
+ .dsh-tree-context-menu {
5525
+ position: fixed;
5526
+ min-width: 170px;
5527
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
5528
+ border: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
5529
+ border-radius: 6px;
5530
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.15);
5531
+ padding: 4px;
5532
+ user-select: none;
5533
+ font-family: inherit;
5534
+ }
5535
+ body[data-ds-dark-theme] .dsh-tree-context-menu {
5536
+ background: #1a1e29;
5537
+ border-color: #2e384d;
5538
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
5539
+ }
5540
+ .dsh-menu-item {
5541
+ display: flex;
5542
+ align-items: center;
5543
+ gap: 8px;
5544
+ padding: 5px 8px;
5545
+ border-radius: 4px;
5546
+ font-size: 12px;
5547
+ cursor: pointer;
5548
+ color: var(--dsw-alias-label-primary, #1e293b);
5549
+ transition: background 0.1s;
5550
+ }
5551
+ body[data-ds-dark-theme] .dsh-menu-item {
5552
+ color: #e2e8f0;
5553
+ }
5554
+ .dsh-menu-item:hover {
5555
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.06));
5556
+ }
5557
+ body[data-ds-dark-theme] .dsh-menu-item:hover {
5558
+ background: rgba(255, 255, 255, 0.09);
5559
+ }
5560
+ .dsh-menu-item-danger {
5561
+ color: #ef4444 !important;
5562
+ }
5563
+ .dsh-menu-item-danger:hover {
5564
+ background: rgba(239, 68, 68, 0.12) !important;
5565
+ }
5566
+ .dsh-menu-icon {
5567
+ font-size: 13px;
5568
+ width: 16px;
5569
+ display: inline-flex;
5570
+ align-items: center;
5571
+ justify-content: center;
5572
+ flex-shrink: 0;
5573
+ }
5574
+ .dsh-menu-divider {
5575
+ height: 1px;
5576
+ background: var(--dsw-alias-border-l3, #e2e8f0);
5577
+ margin: 3px 4px;
5578
+ }
5579
+ body[data-ds-dark-theme] .dsh-menu-divider {
5580
+ background: #2a3140;
5581
+ }
5582
+
4744
5583
  /* Splitter between Tree and Preview */
4745
5584
  .dsh-tree-splitter {
4746
5585
  width: 4px;
@@ -4860,85 +5699,89 @@
4860
5699
  body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
4861
5700
  body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
4862
5701
 
4863
- /* Code Editor Viewer */
5702
+ /* Code Editor Viewer (with unified line row architecture for 100% perfect alignment) */
4864
5703
  .dsh-code-editor-view {
4865
5704
  display: flex;
5705
+ flex-direction: column;
5706
+ width: 100%;
4866
5707
  min-height: 100%;
4867
- font-family: "Cascadia Code", "Fira Code", Consolas, Menlo, monospace;
4868
- font-size: 13px;
5708
+ overflow: auto;
5709
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Cascadia Code", "Fira Code", monospace;
5710
+ font-size: 12px;
4869
5711
  line-height: 20px;
4870
5712
  background: var(--dsw-alias-bg-layer-1, #ffffff);
4871
5713
  color: var(--dsw-alias-label-primary, #1e293b);
5714
+ padding: 8px 0;
5715
+ box-sizing: border-box;
5716
+ user-select: text;
4872
5717
  }
4873
5718
  body[data-ds-dark-theme] .dsh-code-editor-view {
4874
- background: #181b24;
5719
+ background: #161922;
4875
5720
  color: #d4d4d4;
4876
5721
  }
4877
5722
 
4878
- .dsh-code-gutter {
4879
- width: 44px;
4880
- min-width: 44px;
4881
- padding: 12px 6px 12px 0;
5723
+ .dsh-code-line {
5724
+ display: flex;
5725
+ flex-direction: row;
5726
+ align-items: flex-start;
5727
+ min-height: 20px;
5728
+ line-height: 20px;
5729
+ width: 100%;
5730
+ box-sizing: border-box;
5731
+ transition: background 0.1s;
5732
+ }
5733
+ .dsh-code-line:hover {
5734
+ background: rgba(0, 0, 0, 0.035);
5735
+ }
5736
+ body[data-ds-dark-theme] .dsh-code-line:hover {
5737
+ background: rgba(255, 255, 255, 0.035);
5738
+ }
5739
+ .dsh-code-line.dsh-line-highlight-flash {
5740
+ background: rgba(14, 165, 233, 0.22) !important;
5741
+ }
5742
+
5743
+ .dsh-line-gutter {
5744
+ display: inline-block;
5745
+ box-sizing: border-box;
5746
+ padding: 0 8px 0 6px;
4882
5747
  text-align: right;
4883
5748
  user-select: none;
4884
- background: var(--dsw-alias-bg-layer-2, #f8fafc);
4885
- border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4886
5749
  color: var(--dsw-alias-label-tertiary, #94a3b8);
4887
- font-size: 12px;
5750
+ font-size: 11px;
4888
5751
  line-height: 20px;
4889
5752
  flex-shrink: 0;
5753
+ border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
5754
+ cursor: pointer;
5755
+ transition: color 0.1s;
4890
5756
  }
4891
- body[data-ds-dark-theme] .dsh-code-gutter {
4892
- background: #141720;
5757
+ body[data-ds-dark-theme] .dsh-line-gutter {
4893
5758
  border-right-color: #2a3140;
4894
- color: #64748b;
4895
- }
4896
-
4897
- .dsh-gutter-line {
4898
- height: 20px;
4899
- cursor: pointer;
4900
- padding-right: 8px;
5759
+ color: #55637a;
4901
5760
  }
4902
- .dsh-gutter-line:hover {
5761
+ .dsh-line-gutter:hover {
4903
5762
  color: var(--dsw-brand-primary, #0284c7);
4904
- font-weight: bold;
5763
+ font-weight: 600;
4905
5764
  }
4906
- .dsh-gutter-line.dsh-line-highlight-flash {
4907
- background: rgba(14, 165, 233, 0.25);
4908
- color: #0284c7;
5765
+ body[data-ds-dark-theme] .dsh-line-gutter:hover {
5766
+ color: #38bdf8;
4909
5767
  }
4910
5768
 
4911
- .dsh-code-content {
5769
+ .dsh-line-code {
4912
5770
  flex: 1;
4913
5771
  min-width: 0;
4914
- padding: 12px 16px;
4915
- overflow: auto;
4916
- }
4917
- .dsh-code-pre {
4918
- margin: 0;
5772
+ padding: 0 12px 0 10px;
5773
+ white-space: pre;
4919
5774
  font-family: inherit;
4920
5775
  font-size: inherit;
4921
5776
  line-height: 20px;
4922
5777
  tab-size: 2;
4923
- white-space: pre;
5778
+ box-sizing: border-box;
4924
5779
  }
4925
- .dsh-code-editor-view.word-wrap .dsh-code-pre {
5780
+ .dsh-code-editor-view.word-wrap .dsh-line-code {
4926
5781
  white-space: pre-wrap;
4927
5782
  word-break: normal;
4928
5783
  overflow-wrap: break-word;
4929
5784
  }
4930
- .dsh-code-editor-view {
4931
- display: flex;
4932
- min-height: 100%;
4933
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
4934
- font-size: 12.5px;
4935
- line-height: 20px;
4936
- background: var(--dsw-alias-bg-layer-1, #ffffff);
4937
- color: var(--dsw-alias-label-primary, #1e293b);
4938
- }
4939
- .dsh-code-pre code {
4940
- font-family: inherit;
4941
- }
4942
5785
 
4943
5786
  /* Interactive JSON Inspector */
4944
5787
  .dsh-json-inspector {