dsh-explorer 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +106 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +7 -0
- package/lib/client.js +2814 -0
- package/lib/index.js +747 -0
- package/package.json +92 -4
- package/src/client/ExplorerContextMenu.tsx +159 -0
- package/src/client/ExplorerHeader.tsx +75 -0
- package/src/client/ExplorerOptionsModal.tsx +243 -0
- package/src/client/ExplorerRow.tsx +240 -0
- package/src/client/ExplorerSearch.tsx +42 -0
- package/src/client/ExplorerView.tsx +1139 -0
- package/src/client/api.ts +121 -0
- package/src/client/conversation-draft.ts +635 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/drag-drop.ts +209 -0
- package/src/client/explorer.module.css +628 -0
- package/src/client/index.tsx +160 -0
- package/src/client/options.ts +59 -0
- package/src/client/vscode-theme.tsx +250 -0
- package/src/config.ts +11 -0
- package/src/fs-operations.ts +116 -0
- package/src/fs-tree.ts +155 -0
- package/src/fs-watch.ts +179 -0
- package/src/git-status.ts +161 -0
- package/src/index.ts +165 -0
- package/src/path-security.ts +22 -0
- package/src/session-path.ts +35 -0
- package/src/wire.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-explorer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native workspace file explorer for DeepSeek Harness with precision drag-and-drop chat insertion, motion collapse, git status badges, compact folders, and tab integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"dsh": {
|
|
22
|
+
"bundle": {
|
|
23
|
+
"patch": "./cordis.patch.yml"
|
|
24
|
+
},
|
|
25
|
+
"client": {
|
|
26
|
+
"inject": [
|
|
27
|
+
"@deepseek-ai/dsh-client-locale",
|
|
28
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
29
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
31
|
+
"@deepseek-ai/dsh-client-modules"
|
|
32
|
+
],
|
|
33
|
+
"platform": "web"
|
|
34
|
+
},
|
|
35
|
+
"manifestVersion": 1
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"lib/index.js",
|
|
39
|
+
"lib/client.js",
|
|
40
|
+
"src",
|
|
41
|
+
"cordis.patch.yml",
|
|
42
|
+
"icon.svg",
|
|
43
|
+
"README.md",
|
|
44
|
+
"package.json",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"dsh-plugin",
|
|
50
|
+
"deepseek-harness",
|
|
51
|
+
"file-explorer",
|
|
52
|
+
"workspace-explorer"
|
|
53
|
+
],
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/wwwroot/dsh-explorer.git"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"clsx": "^2.1.1",
|
|
60
|
+
"ws": "^8.18.0",
|
|
61
|
+
"yaml": "^2.7.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.1-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.2.1-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.1",
|
|
72
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
73
|
+
"react": "^18.2.0",
|
|
74
|
+
"react-dom": "^18.2.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@types/node": "^24.0.0",
|
|
83
|
+
"@types/react": "~18.3.1",
|
|
84
|
+
"@types/react-dom": "~18.3.1",
|
|
85
|
+
"@types/ws": "^8.5.10",
|
|
86
|
+
"esbuild": "^0.28.2",
|
|
87
|
+
"typescript": "^5.6.0"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "node scripts/build.mjs",
|
|
91
|
+
"typecheck": "tsc --noEmit"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
import css from './explorer.module.css'
|
|
3
|
+
import type { FsEntry } from './api.ts'
|
|
4
|
+
|
|
5
|
+
interface ContextMenuProps {
|
|
6
|
+
x: number
|
|
7
|
+
y: number
|
|
8
|
+
entry: FsEntry
|
|
9
|
+
relativePath: string
|
|
10
|
+
onClose: () => void
|
|
11
|
+
onOpenFile?: () => void
|
|
12
|
+
onInsertReference: () => void
|
|
13
|
+
onInsertSnippet: () => void
|
|
14
|
+
onCopyPath: () => void
|
|
15
|
+
onCopyRelativePath: () => void
|
|
16
|
+
onNewFile: () => void
|
|
17
|
+
onNewFolder: () => void
|
|
18
|
+
onRename: () => void
|
|
19
|
+
onDelete: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ExplorerContextMenu: React.FC<ContextMenuProps> = ({
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
entry,
|
|
26
|
+
relativePath,
|
|
27
|
+
onClose,
|
|
28
|
+
onOpenFile,
|
|
29
|
+
onInsertReference,
|
|
30
|
+
onInsertSnippet,
|
|
31
|
+
onCopyPath,
|
|
32
|
+
onCopyRelativePath,
|
|
33
|
+
onNewFile,
|
|
34
|
+
onNewFolder,
|
|
35
|
+
onRename,
|
|
36
|
+
onDelete,
|
|
37
|
+
}) => {
|
|
38
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handleOutsideClick = (e: MouseEvent) => {
|
|
42
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
43
|
+
onClose()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
window.addEventListener('mousedown', handleOutsideClick)
|
|
47
|
+
return () => window.removeEventListener('mousedown', handleOutsideClick)
|
|
48
|
+
}, [onClose])
|
|
49
|
+
|
|
50
|
+
// Adjust menu bounds if overflowing screen
|
|
51
|
+
const top = Math.min(y, window.innerHeight - 260)
|
|
52
|
+
const left = Math.min(x, window.innerWidth - 200)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div ref={menuRef} className={css.contextMenu} style={{ top, left }}>
|
|
56
|
+
{!entry.isDir && onOpenFile && (
|
|
57
|
+
<>
|
|
58
|
+
<div
|
|
59
|
+
className={css.menuItem}
|
|
60
|
+
onClick={() => {
|
|
61
|
+
onOpenFile()
|
|
62
|
+
onClose()
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<span>Open in New Tab</span>
|
|
66
|
+
<span className={css.menuShortcut}>Double-Click</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div className={css.menuDivider} />
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
<div
|
|
72
|
+
className={css.menuItem}
|
|
73
|
+
onClick={() => {
|
|
74
|
+
onInsertReference()
|
|
75
|
+
onClose()
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<span>Insert Mention to Chat</span>
|
|
79
|
+
<span className={css.menuShortcut}>@</span>
|
|
80
|
+
</div>
|
|
81
|
+
{!entry.isDir && (
|
|
82
|
+
<div
|
|
83
|
+
className={css.menuItem}
|
|
84
|
+
onClick={() => {
|
|
85
|
+
onInsertSnippet()
|
|
86
|
+
onClose()
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<span>Insert Code to Chat</span>
|
|
90
|
+
<span className={css.menuShortcut}>Alt+Drop</span>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
<div className={css.menuDivider} />
|
|
94
|
+
{entry.isDir && (
|
|
95
|
+
<>
|
|
96
|
+
<div
|
|
97
|
+
className={css.menuItem}
|
|
98
|
+
onClick={() => {
|
|
99
|
+
onNewFile()
|
|
100
|
+
onClose()
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<span>New File...</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div
|
|
106
|
+
className={css.menuItem}
|
|
107
|
+
onClick={() => {
|
|
108
|
+
onNewFolder()
|
|
109
|
+
onClose()
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<span>New Folder...</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className={css.menuDivider} />
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
<div
|
|
118
|
+
className={css.menuItem}
|
|
119
|
+
onClick={() => {
|
|
120
|
+
onCopyRelativePath()
|
|
121
|
+
onClose()
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<span>Copy Relative Path</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div
|
|
127
|
+
className={css.menuItem}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
onCopyPath()
|
|
130
|
+
onClose()
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<span>Copy Path</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div className={css.menuDivider} />
|
|
136
|
+
<div
|
|
137
|
+
className={css.menuItem}
|
|
138
|
+
onClick={() => {
|
|
139
|
+
onRename()
|
|
140
|
+
onClose()
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<span>Rename...</span>
|
|
144
|
+
<span className={css.menuShortcut}>F2</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div
|
|
147
|
+
className={css.menuItem}
|
|
148
|
+
onClick={() => {
|
|
149
|
+
onDelete()
|
|
150
|
+
onClose()
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<span style={{ color: '#f44336' }}>Delete</span>
|
|
154
|
+
<span className={css.menuShortcut}>Del</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import css from './explorer.module.css'
|
|
3
|
+
|
|
4
|
+
interface ExplorerHeaderProps {
|
|
5
|
+
title: string
|
|
6
|
+
searchOpen: boolean
|
|
7
|
+
optionsOpen: boolean
|
|
8
|
+
onToggleSearch: () => void
|
|
9
|
+
onToggleOptions: () => void
|
|
10
|
+
onNewFile: () => void
|
|
11
|
+
onNewFolder: () => void
|
|
12
|
+
onRefresh: () => void
|
|
13
|
+
onCollapseAll: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ExplorerHeader: React.FC<ExplorerHeaderProps> = ({
|
|
17
|
+
title,
|
|
18
|
+
searchOpen,
|
|
19
|
+
optionsOpen,
|
|
20
|
+
onToggleSearch,
|
|
21
|
+
onToggleOptions,
|
|
22
|
+
onNewFile,
|
|
23
|
+
onNewFolder,
|
|
24
|
+
onRefresh,
|
|
25
|
+
onCollapseAll,
|
|
26
|
+
}) => {
|
|
27
|
+
return (
|
|
28
|
+
<div className={css.header}>
|
|
29
|
+
<span className={css.headerTitle} title={title}>
|
|
30
|
+
{title}
|
|
31
|
+
</span>
|
|
32
|
+
<div className={css.headerActions}>
|
|
33
|
+
<button
|
|
34
|
+
className={`${css.iconBtn} ${searchOpen ? css.iconBtnActive : ''}`}
|
|
35
|
+
title="Filter files"
|
|
36
|
+
onClick={onToggleSearch}
|
|
37
|
+
>
|
|
38
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
39
|
+
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
<button className={css.iconBtn} title="New File" onClick={onNewFile}>
|
|
43
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
44
|
+
<path d="M9 1H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6L9 1zm0 1.5L12.5 6H9V2.5zM8 8.5V11h1V8.5h2.5v-1H9V5H8v2.5H5.5v1H8z"/>
|
|
45
|
+
</svg>
|
|
46
|
+
</button>
|
|
47
|
+
<button className={css.iconBtn} title="New Folder" onClick={onNewFolder}>
|
|
48
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
49
|
+
<path d="M7 2l2 2h5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h5zm2.5 6.5H8V6H7v2.5H4.5v1H7V12h1V9.5h2.5v-1z"/>
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
<button className={css.iconBtn} title="Refresh" onClick={onRefresh}>
|
|
53
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
54
|
+
<path d="M13.65 2.35A7.958 7.958 0 0 0 8 0a8 8 0 1 0 8 8h-2a6 6 0 1 1-1.76-4.24l-2.24 2.24H16V0l-2.35 2.35z"/>
|
|
55
|
+
</svg>
|
|
56
|
+
</button>
|
|
57
|
+
<button className={css.iconBtn} title="Collapse All Folders" onClick={onCollapseAll}>
|
|
58
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
59
|
+
<path d="M1 3.5l1.5-1.5L8 7.5 13.5 2 15 3.5 8 10.5 1 3.5zM1 9.5l1.5-1.5L8 13.5 13.5 8 15 9.5 8 16.5 1 9.5z"/>
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
<button
|
|
63
|
+
className={`${css.iconBtn} ${optionsOpen ? css.iconBtnActive : ''}`}
|
|
64
|
+
title="Explorer Options"
|
|
65
|
+
onClick={onToggleOptions}
|
|
66
|
+
>
|
|
67
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
68
|
+
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
import css from './explorer.module.css'
|
|
3
|
+
import type { ExplorerOptions, ExplorerDensity, SingleClickAction } from './options.ts'
|
|
4
|
+
|
|
5
|
+
interface ExplorerOptionsModalProps {
|
|
6
|
+
options: ExplorerOptions
|
|
7
|
+
onChange: (updated: Partial<ExplorerOptions>) => void
|
|
8
|
+
onReset: () => void
|
|
9
|
+
onClose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
13
|
+
options,
|
|
14
|
+
onChange,
|
|
15
|
+
onReset,
|
|
16
|
+
onClose,
|
|
17
|
+
}) => {
|
|
18
|
+
const popoverRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
|
|
20
|
+
// Close on Click Outside & Escape key
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
23
|
+
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
|
|
24
|
+
onClose()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === 'Escape') {
|
|
29
|
+
onClose()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.addEventListener('mousedown', handleMouseDown, true)
|
|
34
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
37
|
+
document.removeEventListener('keydown', handleKeyDown, true)
|
|
38
|
+
}
|
|
39
|
+
}, [onClose])
|
|
40
|
+
|
|
41
|
+
const handleDensityChange = (density: ExplorerDensity) => {
|
|
42
|
+
onChange({ density })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handleScaleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
46
|
+
const scale = parseFloat(e.target.value)
|
|
47
|
+
if (!Number.isNaN(scale)) {
|
|
48
|
+
onChange({ scale: Math.round(scale * 100) / 100 })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleActionChange = (action: SingleClickAction) => {
|
|
53
|
+
onChange({ singleClickAction: action })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div ref={popoverRef} className={css.optionsPopover}>
|
|
58
|
+
{/* Header */}
|
|
59
|
+
<div className={css.optionsHeader}>
|
|
60
|
+
<div className={css.optionsHeaderTitle}>
|
|
61
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor" style={{ marginRight: '6px' }}>
|
|
62
|
+
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z" />
|
|
63
|
+
</svg>
|
|
64
|
+
EXPLORER OPTIONS
|
|
65
|
+
</div>
|
|
66
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
67
|
+
<button className={css.optionsIconBtn} title="Reset to defaults" onClick={onReset}>
|
|
68
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
69
|
+
<path d="M13.65 2.35A7.958 7.958 0 0 0 8 0a8 8 0 1 0 8 8h-2a6 6 0 1 1-1.76-4.24l-2.24 2.24H16V0l-2.35 2.35z" />
|
|
70
|
+
</svg>
|
|
71
|
+
</button>
|
|
72
|
+
<button className={css.optionsIconBtn} title="Close" onClick={onClose}>
|
|
73
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
74
|
+
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
|
75
|
+
</svg>
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Body */}
|
|
81
|
+
<div className={css.optionsBody}>
|
|
82
|
+
{/* Section 1: Density & Scale */}
|
|
83
|
+
<div className={css.optionsSection}>
|
|
84
|
+
<div className={css.optionsSectionTitle}>Scale & Density</div>
|
|
85
|
+
|
|
86
|
+
{/* Density preset buttons */}
|
|
87
|
+
<div className={css.segmentGroup}>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className={`${css.segmentBtn} ${options.density === 'compact' ? css.segmentBtnActive : ''}`}
|
|
91
|
+
onClick={() => handleDensityChange('compact')}
|
|
92
|
+
>
|
|
93
|
+
Compact
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className={`${css.segmentBtn} ${options.density === 'normal' ? css.segmentBtnActive : ''}`}
|
|
98
|
+
onClick={() => handleDensityChange('normal')}
|
|
99
|
+
>
|
|
100
|
+
Normal
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className={`${css.segmentBtn} ${options.density === 'relaxed' ? css.segmentBtnActive : ''}`}
|
|
105
|
+
onClick={() => handleDensityChange('relaxed')}
|
|
106
|
+
>
|
|
107
|
+
Relaxed
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Scale zoom slider */}
|
|
112
|
+
<div className={css.optionsRow} style={{ marginTop: '6px' }}>
|
|
113
|
+
<div className={css.optionsLabelWrap}>
|
|
114
|
+
<span className={css.optionsLabel}>Zoom Scale</span>
|
|
115
|
+
<span className={css.optionsSubLabel}>Resize tree text and icons</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
118
|
+
<span className={css.optionsBadge}>{Math.round(options.scale * 100)}%</span>
|
|
119
|
+
{options.scale !== 1.0 && (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
className={css.optionsMiniBtn}
|
|
123
|
+
title="Reset scale to 100%"
|
|
124
|
+
onClick={() => onChange({ scale: 1.0 })}
|
|
125
|
+
>
|
|
126
|
+
1x
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<input
|
|
132
|
+
type="range"
|
|
133
|
+
min="0.75"
|
|
134
|
+
max="1.35"
|
|
135
|
+
step="0.05"
|
|
136
|
+
value={options.scale}
|
|
137
|
+
onChange={handleScaleChange}
|
|
138
|
+
className={css.optionsSlider}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Section 2: Motion & Animation */}
|
|
143
|
+
<div className={css.optionsSection}>
|
|
144
|
+
<div className={css.optionsSectionTitle}>Motion & Transitions</div>
|
|
145
|
+
<div className={css.optionsRow}>
|
|
146
|
+
<div className={css.optionsLabelWrap}>
|
|
147
|
+
<span className={css.optionsLabel}>Smooth Animations</span>
|
|
148
|
+
<span className={css.optionsSubLabel}>Fluid folder collapse & icon turns</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div
|
|
151
|
+
className={`${css.switch} ${options.enableMotion ? css.switchActive : ''}`}
|
|
152
|
+
onClick={() => onChange({ enableMotion: !options.enableMotion })}
|
|
153
|
+
role="switch"
|
|
154
|
+
aria-checked={options.enableMotion}
|
|
155
|
+
>
|
|
156
|
+
<div className={`${css.switchKnob} ${options.enableMotion ? css.switchKnobActive : ''}`} />
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Section 3: File Tree Display */}
|
|
162
|
+
<div className={css.optionsSection}>
|
|
163
|
+
<div className={css.optionsSectionTitle}>Workspace View</div>
|
|
164
|
+
|
|
165
|
+
{/* Show hidden files */}
|
|
166
|
+
<div className={css.optionsRow}>
|
|
167
|
+
<div className={css.optionsLabelWrap}>
|
|
168
|
+
<span className={css.optionsLabel}>Show Hidden Files</span>
|
|
169
|
+
<span className={css.optionsSubLabel}>Include dotfiles and config directories</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div
|
|
172
|
+
className={`${css.switch} ${options.showHiddenFiles ? css.switchActive : ''}`}
|
|
173
|
+
onClick={() => onChange({ showHiddenFiles: !options.showHiddenFiles })}
|
|
174
|
+
role="switch"
|
|
175
|
+
aria-checked={options.showHiddenFiles}
|
|
176
|
+
>
|
|
177
|
+
<div className={`${css.switchKnob} ${options.showHiddenFiles ? css.switchKnobActive : ''}`} />
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Git badges */}
|
|
182
|
+
<div className={css.optionsRow}>
|
|
183
|
+
<div className={css.optionsLabelWrap}>
|
|
184
|
+
<span className={css.optionsLabel}>Git Status Indicators</span>
|
|
185
|
+
<span className={css.optionsSubLabel}>Highlight modified & untracked files</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div
|
|
188
|
+
className={`${css.switch} ${options.showGitBadges ? css.switchActive : ''}`}
|
|
189
|
+
onClick={() => onChange({ showGitBadges: !options.showGitBadges })}
|
|
190
|
+
role="switch"
|
|
191
|
+
aria-checked={options.showGitBadges}
|
|
192
|
+
>
|
|
193
|
+
<div className={`${css.switchKnob} ${options.showGitBadges ? css.switchKnobActive : ''}`} />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Compact folders */}
|
|
198
|
+
<div className={css.optionsRow}>
|
|
199
|
+
<div className={css.optionsLabelWrap}>
|
|
200
|
+
<span className={css.optionsLabel}>Compact Folders</span>
|
|
201
|
+
<span className={css.optionsSubLabel}>Chain single-child directory paths</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div
|
|
204
|
+
className={`${css.switch} ${options.compactFolders ? css.switchActive : ''}`}
|
|
205
|
+
onClick={() => onChange({ compactFolders: !options.compactFolders })}
|
|
206
|
+
role="switch"
|
|
207
|
+
aria-checked={options.compactFolders}
|
|
208
|
+
>
|
|
209
|
+
<div className={`${css.switchKnob} ${options.compactFolders ? css.switchKnobActive : ''}`} />
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Section 4: Quick Workflow */}
|
|
215
|
+
<div className={css.optionsSection}>
|
|
216
|
+
<div className={css.optionsSectionTitle}>Interaction</div>
|
|
217
|
+
<div className={css.optionsRow}>
|
|
218
|
+
<div className={css.optionsLabelWrap}>
|
|
219
|
+
<span className={css.optionsLabel}>File Click Behavior</span>
|
|
220
|
+
<span className={css.optionsSubLabel}>Direct action when clicking a file</span>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div className={css.segmentGroup} style={{ marginTop: '4px' }}>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
className={`${css.segmentBtn} ${options.singleClickAction === 'select' ? css.segmentBtnActive : ''}`}
|
|
227
|
+
onClick={() => handleActionChange('select')}
|
|
228
|
+
>
|
|
229
|
+
Select / Focus
|
|
230
|
+
</button>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
className={`${css.segmentBtn} ${options.singleClickAction === 'mention' ? css.segmentBtnActive : ''}`}
|
|
234
|
+
onClick={() => handleActionChange('mention')}
|
|
235
|
+
>
|
|
236
|
+
Insert @Mention
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|