webtun 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -225
- package/package.json +1 -1
- package/public/commands.js +0 -1
- package/public/favicon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +1 -0
- package/public/index.html +863 -253
- package/server.js +1 -1
package/public/index.html
CHANGED
|
@@ -4,41 +4,48 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
6
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
7
8
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
9
|
<meta name="theme-color" content="#1a1b26">
|
|
9
10
|
<title>WebTun</title>
|
|
11
|
+
<link rel="icon" href="/favicon.png" type="image/png">
|
|
10
12
|
<link rel="manifest" href="/manifest.json">
|
|
11
13
|
<link rel="apple-touch-icon" href="/icon-192.png">
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12
17
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
13
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
14
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
|
|
16
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
17
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.7.0/lib/addon-unicode11.min.js"></script>
|
|
18
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.
|
|
19
|
-
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
|
20
|
-
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
|
18
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
19
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
20
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
|
|
21
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
22
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.7.0/lib/addon-unicode11.min.js"></script>
|
|
23
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
|
|
24
|
+
<script defer src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
|
25
|
+
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
|
21
26
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.css">
|
|
22
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.js"></script>
|
|
23
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/edit/matchbrackets.min.js"></script>
|
|
24
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/javascript/javascript.min.js"></script>
|
|
25
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/python/python.min.js"></script>
|
|
26
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/htmlmixed/htmlmixed.min.js"></script>
|
|
27
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/xml/xml.min.js"></script>
|
|
28
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/css/css.min.js"></script>
|
|
29
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/clike/clike.min.js"></script>
|
|
30
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/shell/shell.min.js"></script>
|
|
31
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/markdown/markdown.min.js"></script>
|
|
32
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/yaml/yaml.min.js"></script>
|
|
33
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/sql/sql.min.js"></script>
|
|
34
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/go/go.min.js"></script>
|
|
35
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/
|
|
36
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/
|
|
27
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.js"></script>
|
|
28
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/edit/matchbrackets.min.js"></script>
|
|
29
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/javascript/javascript.min.js"></script>
|
|
30
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/python/python.min.js"></script>
|
|
31
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/htmlmixed/htmlmixed.min.js"></script>
|
|
32
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/xml/xml.min.js"></script>
|
|
33
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/css/css.min.js"></script>
|
|
34
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/clike/clike.min.js"></script>
|
|
35
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/shell/shell.min.js"></script>
|
|
36
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/markdown/markdown.min.js"></script>
|
|
37
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/yaml/yaml.min.js"></script>
|
|
38
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/sql/sql.min.js"></script>
|
|
39
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/go/go.min.js"></script>
|
|
40
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/mode/simple.min.js"></script>
|
|
41
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/rust/rust.min.js"></script>
|
|
42
|
+
<script defer src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/properties/properties.min.js"></script>
|
|
37
43
|
<style>
|
|
38
44
|
/* ═══════════════════════════════════════════════════════
|
|
39
45
|
CSS VARIABLES / THEMES
|
|
40
46
|
═══════════════════════════════════════════════════════ */
|
|
41
47
|
:root {
|
|
48
|
+
color-scheme: dark;
|
|
42
49
|
--bg: #1a1b26;
|
|
43
50
|
--bg2: #16161e;
|
|
44
51
|
--bg3: #24283b;
|
|
@@ -46,6 +53,7 @@
|
|
|
46
53
|
--border: #3b3f52;
|
|
47
54
|
--fg: #c0caf5;
|
|
48
55
|
--fg2: #787c99;
|
|
56
|
+
--fg3: #565f89;
|
|
49
57
|
--accent: #7aa2f7;
|
|
50
58
|
--accent2: #bb9af7;
|
|
51
59
|
--green: #9ece6a;
|
|
@@ -71,43 +79,65 @@
|
|
|
71
79
|
--btn-sm: 26px;
|
|
72
80
|
--btn-md: 32px;
|
|
73
81
|
--btn-lg: 36px;
|
|
82
|
+
--z-header: 10;
|
|
83
|
+
--z-drawer: 15;
|
|
84
|
+
--z-popover: 25;
|
|
85
|
+
--z-sidebar: 30;
|
|
86
|
+
--z-drop: 50;
|
|
87
|
+
--z-overlay: 400;
|
|
88
|
+
--z-toast: 450;
|
|
89
|
+
--z-menu: 500;
|
|
74
90
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
75
91
|
--transition-fast: 0.15s;
|
|
76
92
|
--transition-normal: 0.2s;
|
|
77
93
|
--transition-slow: 0.3s;
|
|
78
|
-
--font: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
|
|
94
|
+
--font: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
|
|
95
|
+
--font-ui: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
96
|
+
}
|
|
97
|
+
[data-theme="tokyonight"] {
|
|
98
|
+
color-scheme: dark;
|
|
99
|
+
--bg: #1a1b26; --bg2: #16161e; --bg3: #24283b; --bg4: #2a2d3e;
|
|
100
|
+
--border: #3b3f52; --fg: #c0caf5; --fg2: #787c99; --fg3: #565f89;
|
|
101
|
+
--accent: #7aa2f7; --accent2: #bb9af7; --green: #9ece6a;
|
|
102
|
+
--red: #f7768e; --yellow: #e0af68; --cyan: #7dcfff;
|
|
103
|
+
--shadow-color: rgba(0,0,0,0.6);
|
|
79
104
|
}
|
|
80
105
|
[data-theme="light"] {
|
|
106
|
+
color-scheme: light;
|
|
81
107
|
--bg: #f9f9fb; --bg2: #eeeef2; --bg3: #e2e2e8; --bg4: #d5d5de;
|
|
82
|
-
--border: #c8c8d4; --fg: #
|
|
83
|
-
--accent: #4060d0; --accent2: #8050c0; --green: #
|
|
84
|
-
--red: #c03050; --yellow: #
|
|
108
|
+
--border: #c8c8d4; --fg: #1f1f30; --fg2: #2b2b45; --fg3: #5d5d78;
|
|
109
|
+
--accent: #4060d0; --accent2: #8050c0; --green: #2f7d1f;
|
|
110
|
+
--red: #c03050; --yellow: #9a6a00; --cyan: #1e7590;
|
|
85
111
|
--shadow-color: rgba(0,0,0,0.12);
|
|
86
112
|
}
|
|
87
113
|
[data-theme="solarized"] {
|
|
114
|
+
color-scheme: dark;
|
|
88
115
|
--bg: #002b36; --bg2: #073642; --bg3: #083f4d; --bg4: #0a4a5a;
|
|
89
|
-
--border: #1a5a6a; --fg: #93a1a1; --fg2: #586e75;
|
|
116
|
+
--border: #1a5a6a; --fg: #93a1a1; --fg2: #586e75; --fg3: #486168;
|
|
90
117
|
--accent: #268bd2; --accent2: #6c71c4; --green: #859900;
|
|
91
118
|
--red: #dc322f; --yellow: #b58900; --cyan: #2aa198;
|
|
92
119
|
--shadow-color: rgba(0,0,0,0.6);
|
|
93
120
|
}
|
|
94
121
|
[data-theme="gruvbox"] {
|
|
122
|
+
color-scheme: dark;
|
|
95
123
|
--bg: #282828; --bg2: #1d2021; --bg3: #32302f; --bg4: #3c3836;
|
|
96
|
-
--border: #504945; --fg: #ebdbb2; --fg2: #928374;
|
|
124
|
+
--border: #504945; --fg: #ebdbb2; --fg2: #928374; --fg3: #7c6f64;
|
|
97
125
|
--accent: #83a598; --accent2: #d3869b; --green: #b8bb26;
|
|
98
126
|
--red: #fb4934; --yellow: #fabd2f; --cyan: #8ec07c;
|
|
99
127
|
--shadow-color: rgba(0,0,0,0.6);
|
|
100
128
|
}
|
|
101
129
|
[data-theme="dracula"] {
|
|
130
|
+
color-scheme: dark;
|
|
102
131
|
--bg: #282a36; --bg2: #1e1f29; --bg3: #2f3240; --bg4: #373a50;
|
|
103
|
-
--border: #44475a; --fg: #f8f8f2; --fg2: #6272a4;
|
|
132
|
+
--border: #44475a; --fg: #f8f8f2; --fg2: #6272a4; --fg3: #4f5b84;
|
|
104
133
|
--accent: #bd93f9; --accent2: #ff79c6; --green: #50fa7b;
|
|
105
134
|
--red: #ff5555; --yellow: #f1fa8c; --cyan: #8be9fd;
|
|
106
135
|
--shadow-color: rgba(0,0,0,0.6);
|
|
107
136
|
}
|
|
108
137
|
[data-theme="monokai"] {
|
|
138
|
+
color-scheme: dark;
|
|
109
139
|
--bg: #272822; --bg2: #1e1f1c; --bg3: #31322b; --bg4: #3a3c32;
|
|
110
|
-
--border: #49483e; --fg: #f8f8f2; --fg2: #75715e;
|
|
140
|
+
--border: #49483e; --fg: #f8f8f2; --fg2: #75715e; --fg3: #5d5a4e;
|
|
111
141
|
--accent: #a6e22e; --accent2: #ae81ff; --green: #a6e22e;
|
|
112
142
|
--red: #f92672; --yellow: #e6db74; --cyan: #66d9e8;
|
|
113
143
|
--shadow-color: rgba(0,0,0,0.6);
|
|
@@ -117,10 +147,10 @@
|
|
|
117
147
|
RESET & BASE
|
|
118
148
|
═══════════════════════════════════════════════════════ */
|
|
119
149
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
120
|
-
html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--fg); font-family: var(--font); font-size: 13px; padding-bottom: env(safe-area-inset-bottom);
|
|
150
|
+
html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--fg); font-family: var(--font-ui); font-size: 13px; padding-bottom: env(safe-area-inset-bottom); touch-action: manipulation; -webkit-overflow-scrolling: touch; -webkit-tap-highlight-color: transparent; }
|
|
121
151
|
|
|
122
152
|
button { background: none; border: none; cursor: pointer; color: inherit; font-family: inherit; }
|
|
123
|
-
input, select, textarea { font-family: var(--font); background: var(--bg3); border: 1px solid var(--border); color: var(--fg); border-radius: var(--radius); padding: 6px 10px; outline: none; font-size: 16px; }
|
|
153
|
+
input, select, textarea { font-family: var(--font-ui); background: var(--bg3); border: 1px solid var(--border); color: var(--fg); border-radius: var(--radius); padding: 6px 10px; outline: none; font-size: 16px; }
|
|
124
154
|
@media (pointer: coarse) { input, select, textarea { font-size: 16px; } }
|
|
125
155
|
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
|
126
156
|
a { color: var(--accent); text-decoration: none; }
|
|
@@ -138,17 +168,38 @@ a { color: var(--accent); text-decoration: none; }
|
|
|
138
168
|
#new-tab-btn { width: 36px; height: 36px; }
|
|
139
169
|
.icon-btn { width: 40px; height: 40px; }
|
|
140
170
|
.file-action-btn { width: 36px; height: 36px; }
|
|
171
|
+
.search-btn { width: 32px; height: 32px; }
|
|
172
|
+
.search-btn#search-case-btn { width: 28px; height: 28px; line-height: 28px; }
|
|
141
173
|
}
|
|
142
174
|
svg { display: block; }
|
|
143
175
|
|
|
176
|
+
/* Code/path surfaces stay monospace while the chrome uses the UI font */
|
|
177
|
+
.file-name,
|
|
178
|
+
#path-input,
|
|
179
|
+
#file-breadcrumb,
|
|
180
|
+
#editor-filename,
|
|
181
|
+
.tunnel-url,
|
|
182
|
+
.cmd-lib-item .cmd-lib-cmd,
|
|
183
|
+
.cmd-hist-item .cmd-hist-cmd { font-family: var(--font); }
|
|
184
|
+
|
|
185
|
+
/* Skip link for keyboard/screen-reader users */
|
|
186
|
+
.skip-link {
|
|
187
|
+
position: fixed; top: -40px; left: 8px; z-index: 9999;
|
|
188
|
+
background: var(--accent); color: var(--bg); padding: 6px 14px;
|
|
189
|
+
border-radius: 0 0 var(--radius) var(--radius); font-size: 12px;
|
|
190
|
+
font-family: var(--font-ui); font-weight: 600;
|
|
191
|
+
transition: top var(--transition-fast) ease;
|
|
192
|
+
}
|
|
193
|
+
.skip-link:focus { top: 0; }
|
|
194
|
+
|
|
144
195
|
/* ═══════════════════════════════════════════════════════
|
|
145
196
|
LAYOUT
|
|
146
197
|
═══════════════════════════════════════════════════════ */
|
|
147
198
|
#app { display: flex; flex-direction: column; height: 100dvh; }
|
|
148
|
-
#header { display: flex; align-items: center; height: var(--header-h); background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 8px; padding-top: env(safe-area-inset-top, 0px); gap: 6px; flex-shrink: 0; z-index:
|
|
199
|
+
#header { display: flex; align-items: center; height: var(--header-h); background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 8px; padding-top: env(safe-area-inset-top, 0px); gap: 6px; flex-shrink: 0; z-index: var(--z-header); position: relative; }
|
|
149
200
|
|
|
150
201
|
/* Upload progress bar in header */
|
|
151
|
-
#upload-progress { position: absolute; bottom: 0; left: 0; right: 0; height: 0; background: var(--bg3); z-index:
|
|
202
|
+
#upload-progress { position: absolute; bottom: 0; left: 0; right: 0; height: 0; background: var(--bg3); z-index: calc(var(--z-header) + 1); overflow: visible; transition: height var(--transition-normal) ease; }
|
|
152
203
|
#upload-progress.active { height: 3px; }
|
|
153
204
|
#upload-progress-bar { height: 100%; width: 0%; background: var(--accent); transition: width var(--transition-slow) ease; border-radius: 0 2px 2px 0; }
|
|
154
205
|
#upload-progress.success #upload-progress-bar { background: var(--green); }
|
|
@@ -160,13 +211,26 @@ svg { display: block; }
|
|
|
160
211
|
#sidebar.dragging { transition: none; }
|
|
161
212
|
#sidebar.hidden { width: 0 !important; }
|
|
162
213
|
#content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; min-height: 0; position: relative; }
|
|
163
|
-
#tab-bar { display: flex; align-items: center; height: var(--tab-h); background: var(--bg2); border-bottom: 1px solid var(--border);
|
|
214
|
+
#tab-bar { display: flex; align-items: center; height: var(--tab-h); background: var(--bg2); border-bottom: 1px solid var(--border); flex-shrink: 0; gap: 2px; padding: 0 4px; }
|
|
215
|
+
#tab-scroll { display: flex; align-items: center; gap: 2px; height: 100%; flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; mask-image: linear-gradient(to right, black calc(100% - 20px), transparent); -webkit-mask-image: linear-gradient(to right, black calc(100% - 20px), transparent); scrollbar-width: none; }
|
|
216
|
+
#tab-scroll::-webkit-scrollbar { height: 3px; }
|
|
164
217
|
#tab-bar.drag-over, #new-tab-btn.drag-over { background: var(--accent, #00c853); opacity: 0.15; }
|
|
165
|
-
#tab-bar::-webkit-scrollbar { height: 3px; }
|
|
166
218
|
#terminals { flex: 1; position: relative; overflow: hidden; }
|
|
167
219
|
.term-wrapper { position: absolute; inset: 0; padding: 4px; display: none; touch-action: pan-y; -webkit-overflow-scrolling: touch; overscroll-behavior: none; }
|
|
168
220
|
.term-wrapper.active { display: flex; }
|
|
169
221
|
.xterm { flex: 1; }
|
|
222
|
+
.term-loading {
|
|
223
|
+
position: absolute; inset: 0; z-index: 3;
|
|
224
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
|
|
225
|
+
background: var(--bg); color: var(--fg2); font-size: 12px;
|
|
226
|
+
font-family: var(--font-ui); pointer-events: none;
|
|
227
|
+
}
|
|
228
|
+
.term-loading .tl-spinner {
|
|
229
|
+
width: 22px; height: 22px; border-radius: 50%;
|
|
230
|
+
border: 2px solid var(--bg4); border-top-color: var(--accent);
|
|
231
|
+
animation: reconnectSpin 0.8s linear infinite;
|
|
232
|
+
}
|
|
233
|
+
.term-loading.hidden { display: none; }
|
|
170
234
|
.xterm-viewport { border-radius: 0; }
|
|
171
235
|
.term-bell-flash { animation: bellFlash 0.15s ease-out; }
|
|
172
236
|
@keyframes bellFlash { 0% { filter: brightness(1.8); } 100% { filter: brightness(1); } }
|
|
@@ -252,7 +316,7 @@ svg { display: block; }
|
|
|
252
316
|
cursor: row-resize;
|
|
253
317
|
flex-shrink: 0;
|
|
254
318
|
position: relative;
|
|
255
|
-
z-index:
|
|
319
|
+
z-index: var(--z-header);
|
|
256
320
|
background: transparent;
|
|
257
321
|
transition: background var(--transition-fast);
|
|
258
322
|
}
|
|
@@ -272,7 +336,7 @@ svg { display: block; }
|
|
|
272
336
|
display: flex; flex-direction: column;
|
|
273
337
|
transform: translateX(100%);
|
|
274
338
|
transition: var(--transition-normal);
|
|
275
|
-
z-index:
|
|
339
|
+
z-index: var(--z-drawer);
|
|
276
340
|
}
|
|
277
341
|
#settings-panel.open {
|
|
278
342
|
transform: translateX(0);
|
|
@@ -305,17 +369,27 @@ svg { display: block; }
|
|
|
305
369
|
.keep-awake input { display: none; }
|
|
306
370
|
.keep-awake-box { width: 18px; height: 18px; border: 1.5px solid var(--border); border-radius: 4px; position: relative; transition: var(--transition-normal) ease; background: var(--bg2); }
|
|
307
371
|
.keep-awake:hover .keep-awake-box { border-color: var(--fg2); }
|
|
308
|
-
.keep-awake-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--fg); position: absolute; top: 4px; left: 3px; transition: var(--transition-normal) ease; }
|
|
372
|
+
.keep-awake-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--fg); position: absolute; top: 4px; left: 3px; transition: left var(--transition-normal) ease; }
|
|
309
373
|
.keep-awake input:checked + .keep-awake-box { border-color: var(--accent); background: var(--accent); }
|
|
310
|
-
.keep-awake input:checked + .keep-awake-box .keep-awake-dot { background: var(--bg); left: calc(100% - 11px);
|
|
311
|
-
|
|
312
|
-
@keyframes keepawake-bounce {
|
|
313
|
-
0%, 100% { left: 3px; }
|
|
314
|
-
50% { left: calc(100% - 11px); }
|
|
315
|
-
}
|
|
374
|
+
.keep-awake input:checked + .keep-awake-box .keep-awake-dot { background: var(--bg); left: calc(100% - 11px); }
|
|
316
375
|
|
|
317
376
|
#hostname-badge { font-size: 11px; color: var(--fg2); padding: 2px 8px; background: var(--bg3); border-radius: 999px; }
|
|
377
|
+
#more-btn { display: none; }
|
|
378
|
+
#more-menu { position: absolute; top: calc(100% + 4px); right: env(safe-area-inset-right, 0px); background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 8px 32px var(--shadow-color); padding: 4px; min-width: 190px; display: none; flex-direction: column; gap: 2px; z-index: var(--z-popover); }
|
|
379
|
+
#more-menu.open { display: flex; }
|
|
380
|
+
.mm-item { display: flex; align-items: center; gap: 9px; padding: 9px 10px; font-size: 12px; font-family: var(--font); border-radius: var(--radius-sm); color: var(--fg); text-align: left; width: 100%; cursor: pointer; }
|
|
381
|
+
.mm-item:hover, .mm-item:focus-visible { background: var(--bg4); color: var(--fg); outline: none; }
|
|
382
|
+
.tunnel-row { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
|
383
|
+
.tunnel-label { font-size: 10px; color: var(--fg2); margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
384
|
+
.tunnel-inner { display: flex; align-items: center; gap: 5px; min-width: 0; }
|
|
385
|
+
.tunnel-url { font-size: 11px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
386
|
+
.tunnel-status, .tunnel-target { font-size: 9px; padding: 1px 5px; border-radius: 4px; flex-shrink: 0; display: inline-flex; align-items: center; gap: 2px; }
|
|
387
|
+
.tunnel-btn { width: 22px; height: 22px; flex-shrink: 0; }
|
|
388
|
+
.tunnel-btn.stop { color: var(--red); }
|
|
318
389
|
#conn-status { transition: background var(--transition-slow); }
|
|
390
|
+
.reconnect-spinner { display: inline-flex; vertical-align: -1px; margin-right: 4px; }
|
|
391
|
+
.reconnect-spinner svg { animation: reconnectSpin 1s linear infinite; }
|
|
392
|
+
@keyframes reconnectSpin { to { transform: rotate(360deg); } }
|
|
319
393
|
|
|
320
394
|
/* ═══════════════════════════════════════════════════════
|
|
321
395
|
TABS
|
|
@@ -338,11 +412,24 @@ svg { display: block; }
|
|
|
338
412
|
═══════════════════════════════════════════════════════ */
|
|
339
413
|
#sidebar-header { display: flex; align-items: center; padding: 6px 8px; gap: 4px; border-bottom: 1px solid var(--border); flex-shrink: 0; flex-wrap: wrap; min-height: 40px; height: auto; }
|
|
340
414
|
#sidebar-header input { flex: 1 1 60px; min-width: 40px; height: 26px; font-size: 12px; }
|
|
341
|
-
#file-list-wrap { flex: 1; overflow-y: auto; min-height: 0; touch-action: pan-y; -webkit-overflow-scrolling: touch; }
|
|
415
|
+
#file-list-wrap { flex: 1; overflow-y: auto; min-height: 0; touch-action: pan-y; -webkit-overflow-scrolling: touch; position: relative; }
|
|
416
|
+
#ptr-indicator { position: absolute; top: -40px; left: 0; right: 0; height: 36px; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 11px; color: var(--fg2); opacity: 0; pointer-events: none; }
|
|
417
|
+
#ptr-indicator .ptr-arrow { display: inline-flex; }
|
|
418
|
+
#ptr-indicator .ptr-arrow svg { transition: transform 0.2s ease; }
|
|
419
|
+
#ptr-indicator.release .ptr-arrow svg { transform: rotate(180deg); }
|
|
420
|
+
#ptr-indicator.release .ptr-text { color: var(--accent); }
|
|
421
|
+
/* Loading state: dim content + indeterminate progress bar */
|
|
422
|
+
#file-list-wrap.loading #file-list { opacity: 0.45; transition: opacity var(--transition-fast); }
|
|
423
|
+
#file-list-wrap.loading::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--bg3); z-index: 5; }
|
|
424
|
+
#file-list-wrap.loading::after { content: ''; position: absolute; top: 0; left: -30%; width: 30%; height: 2px; background: var(--accent); border-radius: 0 2px 2px 0; z-index: 6; animation: fileListProgress 0.9s ease-in-out infinite; }
|
|
425
|
+
@keyframes fileListProgress { from { left: -30%; } to { left: 100%; } }
|
|
342
426
|
.file-list-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 20px; color: var(--fg2); font-size: 13px; text-align: center; gap: 8px; }
|
|
343
427
|
.file-list-empty svg { opacity: 0.4; }
|
|
344
|
-
|
|
345
|
-
|
|
428
|
+
.empty-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
|
|
429
|
+
.empty-actions .btn { font-size: 12px; padding: 4px 12px; }
|
|
430
|
+
#file-breadcrumb { padding: 8px 10px; font-size: 12px; color: var(--fg2); border-bottom: 1px solid var(--border); white-space: nowrap; overflow-x: auto; line-height: 1.8; flex-shrink: 0; scrollbar-width: none; }
|
|
431
|
+
#file-breadcrumb::-webkit-scrollbar { display: none; }
|
|
432
|
+
#file-breadcrumb a { transition: background var(--transition-fast); border-radius: var(--radius-sm); padding: 4px 6px !important; display: inline-block; }
|
|
346
433
|
#file-breadcrumb a:hover { background: var(--bg3); }
|
|
347
434
|
@media (pointer: coarse) {
|
|
348
435
|
#file-breadcrumb { padding: 10px 12px; font-size: 13px; }
|
|
@@ -351,6 +438,9 @@ svg { display: block; }
|
|
|
351
438
|
.file-item { display: flex; align-items: center; gap: 8px; padding: 5px 10px; cursor: pointer; border-bottom: 1px solid transparent; font-size: 12px; position: relative; user-select: none; }
|
|
352
439
|
.file-item:hover { background: var(--bg3); }
|
|
353
440
|
.file-item.selected { background: var(--bg4); }
|
|
441
|
+
.file-item.focused { background: var(--bg3); outline: 1px solid var(--accent); outline-offset: -1px; }
|
|
442
|
+
.file-item .file-ellipsis { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: var(--radius-sm); color: var(--fg2); opacity: 0.45; flex-shrink: 0; }
|
|
443
|
+
.file-item:hover .file-ellipsis { opacity: 1; }
|
|
354
444
|
.file-icon { font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; }
|
|
355
445
|
.file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; user-select: text; -webkit-user-select: text; }
|
|
356
446
|
.file-size { font-size: 10px; color: var(--fg2); flex-shrink: 0; user-select: text; -webkit-user-select: text; }
|
|
@@ -375,7 +465,7 @@ svg { display: block; }
|
|
|
375
465
|
/* Sidebar resize handle */
|
|
376
466
|
.sidebar-resize-handle {
|
|
377
467
|
position: absolute; right: 0; top: 0; bottom: 0;
|
|
378
|
-
width: 5px; cursor: col-resize; z-index:
|
|
468
|
+
width: 5px; cursor: col-resize; z-index: var(--z-header);
|
|
379
469
|
background: transparent; transition: background var(--transition-fast); flex-shrink: 0;
|
|
380
470
|
}
|
|
381
471
|
.sidebar-resize-handle:hover,
|
|
@@ -429,9 +519,10 @@ svg { display: block; }
|
|
|
429
519
|
/* ═══════════════════════════════════════════════════════
|
|
430
520
|
SEARCH BAR
|
|
431
521
|
═══════════════════════════════════════════════════════ */
|
|
432
|
-
#search-bar { position: absolute; top: 8px; right: 12px; z-index:
|
|
522
|
+
#search-bar { position: absolute; top: 8px; right: 12px; z-index: var(--z-popover); background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; display: none; align-items: center; gap: 6px; box-shadow: 0 4px 16px var(--shadow-color); }
|
|
433
523
|
#search-bar.open { display: flex; }
|
|
434
524
|
#search-input { width: 200px; height: 26px; }
|
|
525
|
+
@media (max-width: 480px) { #search-bar { left: 8px; right: 8px; } #search-input { flex: 1; width: auto; min-width: 0; } }
|
|
435
526
|
.search-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 4px; color: var(--fg2); }
|
|
436
527
|
.search-btn:hover { background: var(--bg4); color: var(--fg); }
|
|
437
528
|
.search-btn#search-case-btn { font-size: 11px; font-weight: bold; width: 22px; height: 22px; line-height: 22px; }
|
|
@@ -441,7 +532,7 @@ svg { display: block; }
|
|
|
441
532
|
/* ═══════════════════════════════════════════════════════
|
|
442
533
|
OVERLAY
|
|
443
534
|
═══════════════════════════════════════════════════════ */
|
|
444
|
-
.overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index:
|
|
535
|
+
.overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: var(--z-overlay); justify-content: center; align-items: center; backdrop-filter: blur(4px); will-change: backdrop-filter; }
|
|
445
536
|
@media (prefers-reduced-transparency: reduce) { .overlay { backdrop-filter: none; } }
|
|
446
537
|
.overlay.open { display: flex; }
|
|
447
538
|
.modal { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; min-width: 320px; max-width: 90vw; max-height: 90vh; overflow-y: auto; box-shadow: 0 8px 40px var(--shadow-color); }
|
|
@@ -453,6 +544,11 @@ svg { display: block; }
|
|
|
453
544
|
.modal-row label { display: block; font-size: 11px; color: var(--fg2); margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
454
545
|
.modal-row input, .modal-row select { width: 100%; }
|
|
455
546
|
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
|
|
547
|
+
.shortcuts-table { width: 100%; border-collapse: collapse; }
|
|
548
|
+
.shortcuts-table td { padding: 6px 0; font-size: 12.5px; color: var(--fg); border-bottom: 1px solid var(--border); }
|
|
549
|
+
.shortcuts-table td:first-child { color: var(--fg2); white-space: nowrap; padding-right: 16px; width: 1%; }
|
|
550
|
+
.shortcuts-table tr:last-child td { border-bottom: none; }
|
|
551
|
+
kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-size: 11px; color: var(--fg); background: var(--bg1); border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 4px; }
|
|
456
552
|
.btn { height: 34px; padding: 0 16px; border-radius: var(--radius); font-size: 13px; font-family: var(--font); cursor: pointer; transition: all var(--transition-fast); border: 1px solid transparent; }
|
|
457
553
|
#conflict-modal.modal-sm { width: 380px; }
|
|
458
554
|
#conflict-heading { font-size: 15px; margin-bottom: 8px; color: var(--fg); }
|
|
@@ -462,11 +558,22 @@ svg { display: block; }
|
|
|
462
558
|
#conflict-actions { flex-wrap: wrap; gap: 6px; }
|
|
463
559
|
#conflict-replace, #conflict-merge, #conflict-keep-both, #conflict-skip, #conflict-cancel { font-size: 12px; height: var(--btn-md); }
|
|
464
560
|
.btn-primary { background: var(--accent); color: var(--bg); }
|
|
465
|
-
.btn-primary:hover { filter: brightness(1.
|
|
561
|
+
.btn-primary:hover { filter: brightness(1.06); }
|
|
466
562
|
.btn-ghost { background: var(--bg3); color: var(--fg); border-color: var(--border); }
|
|
467
563
|
.btn-ghost:hover { background: var(--bg4); }
|
|
468
564
|
.btn-danger { background: var(--red); color: white; }
|
|
469
|
-
.btn-danger:hover { filter: brightness(1.
|
|
565
|
+
.btn-danger:hover { filter: brightness(1.06); }
|
|
566
|
+
.btn:not(.btn-primary):not(.btn-ghost):not(.btn-danger):hover { background: var(--bg4); }
|
|
567
|
+
.btn:active { filter: brightness(0.95); }
|
|
568
|
+
.btn.loading { opacity: 0.65; pointer-events: none; position: relative; color: transparent; }
|
|
569
|
+
.btn.loading::after {
|
|
570
|
+
content: ''; position: absolute; inset: 0; margin: auto;
|
|
571
|
+
width: 14px; height: 14px; border-radius: 50%;
|
|
572
|
+
border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
|
|
573
|
+
border-top-color: currentColor; animation: reconnectSpin 0.7s linear infinite;
|
|
574
|
+
}
|
|
575
|
+
.field-error { display: none; color: var(--red); font-size: 11px; margin-top: 5px; line-height: 1.4; }
|
|
576
|
+
.field-error.show { display: block; }
|
|
470
577
|
|
|
471
578
|
/* ═══════════════════════════════════════════════════════
|
|
472
579
|
PIN SCREEN
|
|
@@ -485,7 +592,10 @@ svg { display: block; }
|
|
|
485
592
|
EDITOR
|
|
486
593
|
═══════════════════════════════════════════════════════ */
|
|
487
594
|
#editor-header { display: flex; align-items: center; gap: 8px; padding: 0 12px; height: var(--header-h); border-bottom: 1px solid var(--border); flex-shrink: 0; background: var(--bg2); }
|
|
488
|
-
#editor-filename { flex: 1;
|
|
595
|
+
#editor-filename-wrap { flex: 1; display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
596
|
+
#editor-filename { color: var(--fg); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
597
|
+
.editor-dirty-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--yellow); flex-shrink: 0; display: none; }
|
|
598
|
+
#editor-view.editor-dirty .editor-dirty-dot { display: inline-block; }
|
|
489
599
|
#editor-status { font-size: 11px; color: var(--fg2); }
|
|
490
600
|
#editor-area { flex: 1; overflow: hidden; position: relative; }
|
|
491
601
|
#editor-preview { width: 100%; height: 100%; overflow-y: auto; padding: 12px 24px; font-size: 14px; line-height: 1.7; color: var(--fg); display: none; box-sizing: border-box; }
|
|
@@ -550,7 +660,7 @@ svg { display: block; }
|
|
|
550
660
|
/* ═══════════════════════════════════════════════════════
|
|
551
661
|
CONTEXT MENU
|
|
552
662
|
═══════════════════════════════════════════════════════ */
|
|
553
|
-
#ctx-menu { position: fixed; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 4px 20px var(--shadow-color); z-index:
|
|
663
|
+
#ctx-menu { position: fixed; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 4px 20px var(--shadow-color); z-index: var(--z-menu); display: none; min-width: 160px; overflow: hidden; max-height: calc(100vh - 16px); overflow-y: auto; }
|
|
554
664
|
@media (max-width: 360px) { #ctx-menu { min-width: 140px; max-width: calc(100vw - 16px); font-size: 11px; } }
|
|
555
665
|
#ctx-menu.open { display: block; }
|
|
556
666
|
.ctx-item { padding: 8px 14px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
|
@@ -566,11 +676,16 @@ svg { display: block; }
|
|
|
566
676
|
/* ═══════════════════════════════════════════════════════
|
|
567
677
|
TOAST
|
|
568
678
|
═══════════════════════════════════════════════════════ */
|
|
569
|
-
#toast-container { position: fixed; bottom: calc(var(--mobilekey-h) + 12px + env(safe-area-inset-bottom, 0px)); right: 16px; left: 16px; z-index:
|
|
679
|
+
#toast-container { position: fixed; bottom: calc(var(--mobilekey-h) + 12px + env(safe-area-inset-bottom, 0px)); right: 16px; left: 16px; z-index: var(--z-toast); display: flex; flex-direction: column; gap: 8px; align-items: flex-end; transition: bottom var(--transition-normal); }
|
|
570
680
|
#toast-container.keys-hidden { bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
|
|
571
681
|
#toast-container.banner-visible { bottom: calc(var(--mobilekey-h) + 60px + env(safe-area-inset-bottom, 0px)); }
|
|
572
682
|
#toast-container.keys-hidden.banner-visible { bottom: 60px; }
|
|
573
|
-
.toast { padding: 10px 16px; background: var(--bg4); border: 1px solid var(--border); border-radius: var(--radius); font-size: 12px; color: var(--fg); box-shadow: 0 4px 16px var(--shadow-color); animation: slideIn 0.2s ease; max-width: min(400px, calc(100vw - 32px)); }
|
|
683
|
+
.toast { padding: 10px 16px; background: var(--bg4); border: 1px solid var(--border); border-radius: var(--radius); font-size: 12px; color: var(--fg); box-shadow: 0 4px 16px var(--shadow-color); animation: slideIn 0.2s ease; max-width: min(400px, calc(100vw - 32px)); display: flex; align-items: center; gap: 8px; }
|
|
684
|
+
.toast svg { flex-shrink: 0; color: var(--fg2); }
|
|
685
|
+
.toast.success svg { color: var(--green); }
|
|
686
|
+
.toast.error svg { color: var(--red); }
|
|
687
|
+
.toast.warning svg { color: var(--yellow); }
|
|
688
|
+
.toast.info svg { color: var(--accent); }
|
|
574
689
|
.toast.success { border-left: 3px solid var(--green); }
|
|
575
690
|
.toast.error { border-left: 3px solid var(--red); }
|
|
576
691
|
.toast.warning { border-left: 3px solid var(--yellow); }
|
|
@@ -580,7 +695,7 @@ svg { display: block; }
|
|
|
580
695
|
/* ═══════════════════════════════════════════════════════
|
|
581
696
|
DRAG & DROP
|
|
582
697
|
═══════════════════════════════════════════════════════ */
|
|
583
|
-
#drop-overlay { position: fixed; inset: 0; background: color-mix(in srgb, var(--accent) 15%, transparent); border: 3px dashed var(--accent); z-index:
|
|
698
|
+
#drop-overlay { position: fixed; inset: 0; background: color-mix(in srgb, var(--accent) 15%, transparent); border: 3px dashed var(--accent); z-index: var(--z-drop); display: none; align-items: center; justify-content: center; font-size: 24px; color: var(--accent); }
|
|
584
699
|
#drop-overlay.active { display: flex; }
|
|
585
700
|
@media (hover: none) and (pointer: coarse) { #drop-overlay { display: none !important; } }
|
|
586
701
|
|
|
@@ -646,7 +761,7 @@ svg { display: block; }
|
|
|
646
761
|
COMMAND LIBRARY BUBBLE
|
|
647
762
|
═══════════════════════════════════════════════════════ */
|
|
648
763
|
#cmd-lib-panel {
|
|
649
|
-
position: absolute; top: 42px; right: 12px; z-index:
|
|
764
|
+
position: absolute; top: 42px; right: 12px; z-index: var(--z-popover);
|
|
650
765
|
width: 380px; max-height: 480px;
|
|
651
766
|
background: var(--bg2); border: 1px solid var(--border);
|
|
652
767
|
border-radius: var(--radius-lg);
|
|
@@ -818,12 +933,14 @@ svg { display: block; }
|
|
|
818
933
|
@media (max-width: 768px) {
|
|
819
934
|
#mobile-keys { display: flex; }
|
|
820
935
|
.sidebar-resize-handle { display: none; }
|
|
821
|
-
#sidebar { position: absolute; left: env(safe-area-inset-left, 0px); top: var(--header-h); bottom: 0; z-index:
|
|
936
|
+
#sidebar { position: absolute; left: env(safe-area-inset-left, 0px); top: var(--header-h); bottom: 0; z-index: var(--z-sidebar); width: var(--sidebar-w); transform: translateX(-100%); transition: var(--transition-normal); }
|
|
822
937
|
#sidebar.mobile-open { transform: translateX(0); }
|
|
823
938
|
#sidebar.hidden { transform: translateX(-100%); width: var(--sidebar-w); }
|
|
824
|
-
#settings-panel { position: fixed; right: env(safe-area-inset-right, 0px); top: var(--header-h); bottom: 0; z-index:
|
|
939
|
+
#settings-panel { position: fixed; right: env(safe-area-inset-right, 0px); top: var(--header-h); bottom: 0; z-index: var(--z-sidebar); width: var(--settings-w); border-left: 1px solid var(--border); transform: translateX(100%); transition: var(--transition-normal); }
|
|
825
940
|
#settings-panel.open { transform: translateX(0); }
|
|
826
|
-
#hostname-badge { display:
|
|
941
|
+
#hostname-badge { display: inline-flex; max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
942
|
+
#more-btn { display: flex; }
|
|
943
|
+
#cmd-lib-toggle, #system-stats-btn, #tilesBtn { display: none; }
|
|
827
944
|
#search-input { width: 200px; }
|
|
828
945
|
.modal { min-width: unset; width: 95vw; }
|
|
829
946
|
.sys-grid { grid-template-columns: 1fr; }
|
|
@@ -856,10 +973,17 @@ svg { display: block; }
|
|
|
856
973
|
</style>
|
|
857
974
|
</head>
|
|
858
975
|
<body data-theme="light">
|
|
976
|
+
<a class="skip-link" href="#content">Skip to content</a>
|
|
859
977
|
<script>
|
|
978
|
+
// Safe localStorage wrapper — degrades gracefully when Tracking Prevention blocks storage
|
|
979
|
+
const safeStorage = {
|
|
980
|
+
getItem(k) { try { return localStorage.getItem(k); } catch(e) { return null; } },
|
|
981
|
+
setItem(k, v) { try { localStorage.setItem(k, v); } catch(e) {} },
|
|
982
|
+
removeItem(k) { try { localStorage.removeItem(k); } catch(e) {} }
|
|
983
|
+
};
|
|
860
984
|
(function() {
|
|
861
985
|
try {
|
|
862
|
-
const settings = JSON.parse(
|
|
986
|
+
const settings = JSON.parse(safeStorage.getItem('wt-settings')) || {};
|
|
863
987
|
let theme = settings.theme || 'light';
|
|
864
988
|
if (theme === 'system') {
|
|
865
989
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dracula' : 'light';
|
|
@@ -872,7 +996,7 @@ svg { display: block; }
|
|
|
872
996
|
<!-- PIN Screen -->
|
|
873
997
|
<div id="pin-screen">
|
|
874
998
|
<div id="pin-box">
|
|
875
|
-
<h1 style="display:flex;align-items:center;justify-content:center;gap:6px"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
999
|
+
<h1 style="display:flex;align-items:center;justify-content:center;gap:6px"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="6" y1="8" x2="10" y2="8"/><line x1="6" y1="12" x2="10" y2="12"/><line x1="6" y1="16" x2="8" y2="16"/><line x1="12" y1="8" x2="18" y2="8"/><line x1="12" y1="12" x2="18" y2="12"/><line x1="12" y1="16" x2="16" y2="16"/><line x1="10" y1="16" x2="10" y2="16"/></svg>WebTun</h1>
|
|
876
1000
|
<p>Enter PIN to continue</p>
|
|
877
1001
|
<input id="pin-input" type="password" placeholder="••••" autocomplete="off" maxlength="32" inputmode="numeric" aria-label="PIN code">
|
|
878
1002
|
<div id="pin-error" role="alert"></div>
|
|
@@ -885,13 +1009,13 @@ svg { display: block; }
|
|
|
885
1009
|
<!-- Main App -->
|
|
886
1010
|
<div id="app" style="display:none">
|
|
887
1011
|
<!-- Header -->
|
|
888
|
-
<div id="header">
|
|
889
|
-
<span id="conn-status" style="width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0" title="Connected"></span>
|
|
890
|
-
<span id="logo" style="display:flex;align-items:center;gap:4px"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
1012
|
+
<div id="header" role="banner">
|
|
1013
|
+
<span id="conn-status" role="status" aria-label="Connected" style="width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0" title="Connected"></span>
|
|
1014
|
+
<span id="logo" style="display:flex;align-items:center;gap:4px"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="6" y1="8" x2="10" y2="8"/><line x1="6" y1="12" x2="10" y2="12"/><line x1="12" y1="8" x2="18" y2="8"/><line x1="12" y1="12" x2="18" y2="12"/></svg>WebTun</span>
|
|
891
1015
|
<span id="hostname-badge">—</span>
|
|
892
1016
|
<div class="header-spacer"></div>
|
|
893
1017
|
<label id="keep-awake-toggle" class="keep-awake" title="Keep Screen Awake">
|
|
894
|
-
<input type="checkbox" id="keep-awake-cb" onchange="toggleKeepAwake(this.checked)">
|
|
1018
|
+
<input type="checkbox" id="keep-awake-cb" aria-label="Keep screen awake" onchange="toggleKeepAwake(this.checked)">
|
|
895
1019
|
<span class="keep-awake-box">
|
|
896
1020
|
<span class="keep-awake-dot"></span>
|
|
897
1021
|
</span>
|
|
@@ -911,40 +1035,54 @@ svg { display: block; }
|
|
|
911
1035
|
<button class="icon-btn" id="tilesBtn" aria-label="Tiles Mode" title="Tiles Mode" onclick="toggleTiles()">
|
|
912
1036
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
913
1037
|
</button>
|
|
1038
|
+
<button class="icon-btn" id="more-btn" aria-label="More options" title="More options" onclick="toggleMoreMenu()">
|
|
1039
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="12" cy="5" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="12" cy="19" r="1.7"/></svg>
|
|
1040
|
+
</button>
|
|
914
1041
|
<button class="icon-btn" aria-label="Settings" title="Settings" onclick="openSettings()">
|
|
915
1042
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
916
1043
|
</button>
|
|
1044
|
+
<div id="more-menu" role="menu">
|
|
1045
|
+
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();openSystemStats()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="4" rx="1"/><rect x="2" y="10" width="20" height="4" rx="1"/><rect x="2" y="18" width="20" height="4" rx="1"/><circle cx="6" cy="4" r="1" fill="currentColor"/><circle cx="6" cy="12" r="1" fill="currentColor"/><circle cx="6" cy="20" r="1" fill="currentColor"/></svg> System Stats</button>
|
|
1046
|
+
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();toggleTiles()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg> Tiles Mode</button>
|
|
1047
|
+
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();toggleCmdLib()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg> Command Library</button>
|
|
1048
|
+
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();toggleSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> Search</button>
|
|
1049
|
+
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();openShortcuts()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 8h6M12 8v8"/><path d="m7 12 10-4"/><path d="m7 12 10 4"/></svg> Keyboard Shortcuts</button>
|
|
1050
|
+
</div>
|
|
917
1051
|
<div id="upload-progress"><div id="upload-progress-bar"></div><span id="upload-progress-text"></span></div>
|
|
918
1052
|
</div>
|
|
919
|
-
<div id="reconnect-banner" style="display:none;padding:4px 12px;font-size:11px;background:var(--accent);color:#fff;text-align:center;flex-shrink:0"
|
|
1053
|
+
<div id="reconnect-banner" style="display:none;padding:4px 12px;font-size:11px;background:var(--accent);color:#fff;text-align:center;flex-shrink:0"><span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span> Reconnecting…</div>
|
|
920
1054
|
|
|
921
1055
|
<!-- Main -->
|
|
922
1056
|
<div id="main">
|
|
923
1057
|
<!-- Sidebar -->
|
|
924
|
-
<div id="sidebar">
|
|
1058
|
+
<div id="sidebar" role="navigation" aria-label="File explorer">
|
|
925
1059
|
<div id="sidebar-header">
|
|
926
1060
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Back" title="Back" onclick="navigateBack()" id="back-btn">
|
|
927
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1061
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
|
928
1062
|
</button>
|
|
929
1063
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Forward" title="Forward" onclick="navigateForward()" id="fwd-btn">
|
|
930
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1064
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
931
1065
|
</button>
|
|
932
1066
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Go to terminal directory" title="Go to terminal directory" onclick="goToTerminalDir()" id="term-dir-btn">
|
|
933
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1067
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
|
934
1068
|
</button>
|
|
935
1069
|
<input id="path-input" type="text" placeholder="Path…" aria-label="File path" onkeydown="if(event.key==='Enter')navigateTo(this.value)">
|
|
936
1070
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Go up" title="Go up" onclick="navigateUp()">
|
|
937
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1071
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
|
|
938
1072
|
</button>
|
|
939
1073
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Refresh" title="Refresh" onclick="refreshFiles()">
|
|
940
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1074
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
941
1075
|
</button>
|
|
942
1076
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Toggle select mode" title="Toggle select mode" onclick="toggleSelectMode()">
|
|
943
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1077
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><polyline points="9 12 11 14 15 10"/></svg>
|
|
944
1078
|
</button>
|
|
945
1079
|
</div>
|
|
946
1080
|
<div id="file-breadcrumb">/</div>
|
|
947
1081
|
<div id="file-list-wrap">
|
|
1082
|
+
<div id="ptr-indicator" aria-hidden="true">
|
|
1083
|
+
<span class="ptr-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
1084
|
+
<span class="ptr-text">Pull to refresh</span>
|
|
1085
|
+
</div>
|
|
948
1086
|
<div id="file-list"></div>
|
|
949
1087
|
</div>
|
|
950
1088
|
<div id="select-actions" style="display:none;padding:8px 10px;border-top:1px solid var(--border);background:var(--bg2);gap:4px;flex-wrap:wrap">
|
|
@@ -982,10 +1120,11 @@ svg { display: block; }
|
|
|
982
1120
|
</div>
|
|
983
1121
|
|
|
984
1122
|
<!-- Content -->
|
|
985
|
-
<div id="content">
|
|
1123
|
+
<div id="content" role="main">
|
|
986
1124
|
<div id="tab-bar">
|
|
987
|
-
<
|
|
988
|
-
|
|
1125
|
+
<div id="tab-scroll"></div>
|
|
1126
|
+
<button id="new-tab-btn" onclick="newTab()" title="New terminal" aria-label="New terminal">
|
|
1127
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
989
1128
|
</button>
|
|
990
1129
|
</div>
|
|
991
1130
|
<div id="editor-split-area">
|
|
@@ -994,16 +1133,16 @@ svg { display: block; }
|
|
|
994
1133
|
<input id="search-input" type="text" placeholder="Search…" oninput="doSearch()" onkeydown="searchKeydown(event)">
|
|
995
1134
|
<span id="search-results"></span>
|
|
996
1135
|
<button class="search-btn" title="Prev (Shift+Enter)" onclick="searchPrev()">
|
|
997
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1136
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
|
|
998
1137
|
</button>
|
|
999
1138
|
<button class="search-btn" title="Next (Enter)" onclick="searchNext()">
|
|
1000
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1139
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1001
1140
|
</button>
|
|
1002
1141
|
<button class="search-btn" id="search-case-btn" title="Match Case" onclick="toggleSearchCase()">
|
|
1003
1142
|
Aa
|
|
1004
1143
|
</button>
|
|
1005
1144
|
<button class="search-btn" title="Close (Esc)" onclick="closeSearch()">
|
|
1006
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1145
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
1007
1146
|
</button>
|
|
1008
1147
|
</div>
|
|
1009
1148
|
</div>
|
|
@@ -1016,7 +1155,7 @@ svg { display: block; }
|
|
|
1016
1155
|
Command Library
|
|
1017
1156
|
</h3>
|
|
1018
1157
|
<button class="icon-btn" style="width:24px;height:24px" onclick="toggleCmdLib()" title="Close">
|
|
1019
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1158
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
1020
1159
|
</button>
|
|
1021
1160
|
</div>
|
|
1022
1161
|
<div class="cmd-lib-tabs">
|
|
@@ -1048,7 +1187,7 @@ svg { display: block; }
|
|
|
1048
1187
|
<button class="icon-btn" aria-label="Close editor" onclick="closeEditor()" title="Close editor">
|
|
1049
1188
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
1050
1189
|
</button>
|
|
1051
|
-
<span id="editor-filename">—</span>
|
|
1190
|
+
<span id="editor-filename-wrap"><span class="editor-dirty-dot" id="editor-dirty-dot" aria-label="Unsaved changes"></span><span id="editor-filename">—</span></span>
|
|
1052
1191
|
<span id="editor-status"></span>
|
|
1053
1192
|
<button class="btn" id="md-preview-toggle" style="height:26px;padding:0 12px;font-size:12px;display:none" onclick="toggleMdPreview()">Preview</button>
|
|
1054
1193
|
<button class="icon-btn" id="preview-refresh-btn" aria-label="Refresh preview" title="Refresh preview (Ctrl+Shift+R)" onclick="refreshPreview()" style="display:none">
|
|
@@ -1057,27 +1196,27 @@ svg { display: block; }
|
|
|
1057
1196
|
<button class="icon-btn active" id="editor-split-toggle" aria-label="Toggle split orientation" onclick="toggleEditorSplit()" title="Switch to horizontal split">
|
|
1058
1197
|
<svg id="split-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
|
|
1059
1198
|
</button>
|
|
1060
|
-
<button class="btn btn-primary" style="height:26px;padding:0 12px;font-size:12px" onclick="saveFile()">Save</button>
|
|
1199
|
+
<button class="btn btn-primary" id="editor-save-btn" style="height:26px;padding:0 12px;font-size:12px" onclick="saveFile()">Save</button>
|
|
1061
1200
|
</div>
|
|
1062
1201
|
<div id="editor-area">
|
|
1063
1202
|
<textarea id="editor-textarea" spellcheck="false"></textarea>
|
|
1064
|
-
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-same-origin"></iframe></div>
|
|
1203
|
+
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-same-origin allow-scripts"></iframe></div>
|
|
1065
1204
|
</div>
|
|
1066
1205
|
</div>
|
|
1067
1206
|
</div>
|
|
1068
1207
|
|
|
1069
1208
|
<!-- Mobile key bar -->
|
|
1070
1209
|
<div id="mobile-keys">
|
|
1071
|
-
<button class="mkey sel-toggle" onclick="toggleTermSelect()" title="Toggle selection mode" id="sel-toggle-btn"
|
|
1210
|
+
<button class="mkey sel-toggle" onclick="toggleTermSelect()" title="Toggle selection mode" id="sel-toggle-btn" aria-label="Toggle selection mode"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="5" width="14" height="14" rx="2"/><line x1="5" y1="12" x2="19" y2="12"/><line x1="12" y1="5" x2="12" y2="19"/></svg></button>
|
|
1072
1211
|
<span class="mkey-sep"></span>
|
|
1073
1212
|
<button class="mkey" onclick="sendKey('\x1b')" title="Escape">ESC</button>
|
|
1074
1213
|
<span class="mkey-sep"></span>
|
|
1075
1214
|
<button class="mkey" onclick="sendKey('\t')" title="Tab">TAB</button>
|
|
1076
1215
|
<span class="mkey-sep"></span>
|
|
1077
|
-
<button class="mkey" onclick="sendKey('\x1b[A')" title="Arrow Up"
|
|
1078
|
-
<button class="mkey" onclick="sendKey('\x1b[B')" title="Arrow Down"
|
|
1079
|
-
<button class="mkey" onclick="sendKey('\x1b[D')" title="Arrow Left"
|
|
1080
|
-
<button class="mkey" onclick="sendKey('\x1b[C')" title="Arrow Right"
|
|
1216
|
+
<button class="mkey" onclick="sendKey('\x1b[A')" title="Arrow Up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg></button>
|
|
1217
|
+
<button class="mkey" onclick="sendKey('\x1b[B')" title="Arrow Down"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></button>
|
|
1218
|
+
<button class="mkey" onclick="sendKey('\x1b[D')" title="Arrow Left"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></button>
|
|
1219
|
+
<button class="mkey" onclick="sendKey('\x1b[C')" title="Arrow Right"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>
|
|
1081
1220
|
<span class="mkey-sep"></span>
|
|
1082
1221
|
<button class="mkey" onclick="sendKey('\x01')" title="Ctrl+A (Home)">^A</button>
|
|
1083
1222
|
<button class="mkey" onclick="sendKey('\x03')" title="Ctrl+C (Interrupt)">^C</button>
|
|
@@ -1086,7 +1225,7 @@ svg { display: block; }
|
|
|
1086
1225
|
<button class="mkey" onclick="sendKey('\x0c')" title="Ctrl+L (Clear)">^L</button>
|
|
1087
1226
|
<span class="mkey-sep"></span>
|
|
1088
1227
|
<button class="mkey wide" onclick="sendKey('\x1b[3~')" title="Delete">DEL</button>
|
|
1089
|
-
<button class="mkey" onclick="sendKey('\x7f')" title="Backspace"
|
|
1228
|
+
<button class="mkey" onclick="sendKey('\x7f')" title="Backspace"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 5H9l-7 7 7 7h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1Z"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="12" y1="9" x2="18" y2="15"/></svg></button>
|
|
1090
1229
|
<span class="mkey-sep"></span>
|
|
1091
1230
|
<button class="mkey" onclick="sendKey('\x1b[H')" title="Home">Home</button>
|
|
1092
1231
|
<button class="mkey" onclick="sendKey('\x1b[F')" title="End">End</button>
|
|
@@ -1096,7 +1235,7 @@ svg { display: block; }
|
|
|
1096
1235
|
<button class="mkey" onclick="sendKey('\x1bOQ')" title="F2">F2</button>
|
|
1097
1236
|
<button class="mkey" onclick="sendKey('\x1b[15~')" title="F5">F5</button>
|
|
1098
1237
|
<span class="mkey-sep"></span>
|
|
1099
|
-
<button class="mkey" onclick="toggleShift()" title="Toggle Shift (latches for next key)" id="shift-toggle-btn"
|
|
1238
|
+
<button class="mkey" onclick="toggleShift()" title="Toggle Shift (latches for next key)" id="shift-toggle-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6h6"/><path d="M12 18V9"/><path d="m6 12 6-6 6 6"/></svg></button>
|
|
1100
1239
|
<button class="mkey" onclick="toggleAlt()" title="Toggle Alt (latches for next key)" id="alt-toggle-btn">Alt</button>
|
|
1101
1240
|
<span class="mkey-sep"></span>
|
|
1102
1241
|
<button class="mkey" onclick="toggleCtrlRow()" title="Toggle Ctrl keys" id="ctrl-toggle-btn">Ctrl</button>
|
|
@@ -1123,8 +1262,8 @@ svg { display: block; }
|
|
|
1123
1262
|
<button class="mkey" onclick="selectAllTerm()" title="Select all terminal content">Select All</button>
|
|
1124
1263
|
<button class="mkey" onclick="selectTermLine()" title="Select current visible line">Line</button>
|
|
1125
1264
|
<span class="mkey-sep"></span>
|
|
1126
|
-
<button class="mkey" onclick="termScrollUp()" title="Scroll up"
|
|
1127
|
-
<button class="mkey" onclick="termScrollDn()" title="Scroll down"
|
|
1265
|
+
<button class="mkey" onclick="termScrollUp()" title="Scroll up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg></button>
|
|
1266
|
+
<button class="mkey" onclick="termScrollDn()" title="Scroll down"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></button>
|
|
1128
1267
|
<button class="mkey" id="sel-scroll-btn" onclick="toggleScrollMode()" title="Toggle scroll mode">Scroll</button>
|
|
1129
1268
|
<span class="mkey-sep"></span>
|
|
1130
1269
|
<button class="mkey sel-done" onclick="toggleTermSelect()" title="Exit selection mode">Done</button>
|
|
@@ -1143,7 +1282,7 @@ svg { display: block; }
|
|
|
1143
1282
|
<div class="settings-section">
|
|
1144
1283
|
<h3>Appearance</h3>
|
|
1145
1284
|
<div class="setting-row">
|
|
1146
|
-
<label>Theme</label>
|
|
1285
|
+
<label for="s-theme">Theme</label>
|
|
1147
1286
|
<select id="s-theme" onchange="applyTheme(this.value)">
|
|
1148
1287
|
<option value="system">System</option>
|
|
1149
1288
|
<option value="light">Light</option>
|
|
@@ -1155,21 +1294,21 @@ svg { display: block; }
|
|
|
1155
1294
|
</select>
|
|
1156
1295
|
</div>
|
|
1157
1296
|
<div class="setting-row">
|
|
1158
|
-
<label>Font Size</label>
|
|
1297
|
+
<label for="s-fontsize">Font Size</label>
|
|
1159
1298
|
<input type="number" id="s-fontsize" min="8" max="32" value="14" onchange="applyFontSize(+this.value)">
|
|
1160
1299
|
</div>
|
|
1161
1300
|
<div class="setting-row">
|
|
1162
|
-
<label>Font Family</label>
|
|
1301
|
+
<label for="s-font">Font Family</label>
|
|
1163
1302
|
<select id="s-font" onchange="applyFont(this.value)">
|
|
1303
|
+
<option value="'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, monospace">JetBrains Mono</option>
|
|
1164
1304
|
<option value="'SF Mono', 'Fira Code', Consolas, monospace">System Mono</option>
|
|
1165
1305
|
<option value="'Fira Code', monospace">Fira Code</option>
|
|
1166
1306
|
<option value="'Cascadia Code', monospace">Cascadia Code</option>
|
|
1167
|
-
<option value="'JetBrains Mono', monospace">JetBrains Mono</option>
|
|
1168
1307
|
<option value="monospace">Monospace</option>
|
|
1169
1308
|
</select>
|
|
1170
1309
|
</div>
|
|
1171
1310
|
<div class="setting-row">
|
|
1172
|
-
<label>Cursor Style</label>
|
|
1311
|
+
<label for="s-cursor">Cursor Style</label>
|
|
1173
1312
|
<select id="s-cursor" onchange="applyCursor(this.value)">
|
|
1174
1313
|
<option value="block">Block</option>
|
|
1175
1314
|
<option value="underline">Underline</option>
|
|
@@ -1177,53 +1316,54 @@ svg { display: block; }
|
|
|
1177
1316
|
</select>
|
|
1178
1317
|
</div>
|
|
1179
1318
|
<div class="setting-row">
|
|
1180
|
-
<label>Cursor Blink</label>
|
|
1181
|
-
<div class="toggle" id="s-blink" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('blink')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('blink')}"></div>
|
|
1319
|
+
<label for="s-blink-cb">Cursor Blink</label>
|
|
1320
|
+
<div class="toggle" id="s-blink" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('blink')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('blink')}"><input type="checkbox" id="s-blink-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1182
1321
|
</div>
|
|
1183
1322
|
</div>
|
|
1184
1323
|
<div class="settings-section">
|
|
1185
1324
|
<h3>Terminal</h3>
|
|
1186
1325
|
<div class="setting-row">
|
|
1187
|
-
<label>Scrollback Lines</label>
|
|
1326
|
+
<label for="s-scrollback">Scrollback Lines</label>
|
|
1188
1327
|
<input type="number" id="s-scrollback" min="100" max="100000" value="5000" onchange="applyScrollback(+this.value)">
|
|
1189
1328
|
</div>
|
|
1190
1329
|
<div class="setting-row">
|
|
1191
|
-
<label>Bell Sound</label>
|
|
1192
|
-
<div class="toggle" id="s-bell" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('bell')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('bell')}"></div>
|
|
1330
|
+
<label for="s-bell-cb">Bell Sound</label>
|
|
1331
|
+
<div class="toggle" id="s-bell" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('bell')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('bell')}"><input type="checkbox" id="s-bell-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1193
1332
|
</div>
|
|
1194
1333
|
<div class="setting-row">
|
|
1195
|
-
<label>Right-Click Menu</label>
|
|
1196
|
-
<div class="toggle on" id="s-termRightClick" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('termRightClick')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('termRightClick')}"></div>
|
|
1334
|
+
<label for="s-termRightClick-cb">Right-Click Menu</label>
|
|
1335
|
+
<div class="toggle on" id="s-termRightClick" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('termRightClick')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('termRightClick')}"><input type="checkbox" id="s-termRightClick-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1197
1336
|
</div>
|
|
1198
1337
|
</div>
|
|
1199
1338
|
<div class="settings-section">
|
|
1200
1339
|
<h3>Interface</h3>
|
|
1201
1340
|
<div class="setting-row">
|
|
1202
|
-
<label>Show Mobile Keys</label>
|
|
1203
|
-
<div class="toggle" id="s-mobilekeys" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('mobilekeys')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('mobilekeys')}"></div>
|
|
1341
|
+
<label for="s-mobilekeys-cb">Show Mobile Keys</label>
|
|
1342
|
+
<div class="toggle" id="s-mobilekeys" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('mobilekeys')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('mobilekeys')}"><input type="checkbox" id="s-mobilekeys-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1204
1343
|
</div>
|
|
1205
1344
|
<div class="setting-row">
|
|
1206
|
-
<label>Confirm on Close</label>
|
|
1207
|
-
<div class="toggle on" id="s-confirmclose" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('confirmclose')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('confirmclose')}"></div>
|
|
1345
|
+
<label for="s-confirmclose-cb">Confirm on Close</label>
|
|
1346
|
+
<div class="toggle on" id="s-confirmclose" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('confirmclose')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('confirmclose')}"><input type="checkbox" id="s-confirmclose-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1208
1347
|
</div>
|
|
1209
1348
|
<div class="setting-row">
|
|
1210
|
-
<label>Data Saver</label>
|
|
1211
|
-
<div class="toggle" id="s-datasaver" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('datasaver')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('datasaver')}"></div>
|
|
1349
|
+
<label for="s-datasaver-cb">Data Saver</label>
|
|
1350
|
+
<div class="toggle" id="s-datasaver" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('datasaver')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('datasaver')}"><input type="checkbox" id="s-datasaver-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1212
1351
|
</div>
|
|
1213
1352
|
<div class="setting-row electron-only" style="display:none">
|
|
1214
|
-
<label>Start on Login</label>
|
|
1215
|
-
<div class="toggle" id="s-autostart" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('autostart')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('autostart')}"></div>
|
|
1353
|
+
<label for="s-autostart-cb">Start on Login</label>
|
|
1354
|
+
<div class="toggle" id="s-autostart" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('autostart')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('autostart')}"><input type="checkbox" id="s-autostart-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1216
1355
|
</div>
|
|
1217
1356
|
</div>
|
|
1218
1357
|
<div class="settings-section">
|
|
1219
1358
|
<h3>Tunnel</h3>
|
|
1220
1359
|
<div class="setting-row" style="flex-wrap:wrap;gap:6px">
|
|
1221
|
-
<label style="flex:0 0 auto">Local URL</label>
|
|
1360
|
+
<label for="tunnel-url" style="flex:0 0 auto">Local URL</label>
|
|
1222
1361
|
<div style="display:flex;flex:1;gap:6px;min-width:0">
|
|
1223
|
-
<input type="text" id="tunnel-url" value="http://localhost:3000" placeholder="http://localhost:PORT" style="flex:1;min-width:0">
|
|
1224
|
-
<button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:12px;flex-shrink:0" onclick="createTunnel()">Create</button>
|
|
1362
|
+
<input type="text" id="tunnel-url" value="http://localhost:3000" placeholder="http://localhost:PORT" style="flex:1;min-width:0" oninput="clearFieldError('tunnel-field-error')">
|
|
1363
|
+
<button class="btn btn-primary" id="tunnel-create-btn" style="height:28px;padding:0 12px;font-size:12px;flex-shrink:0" onclick="createTunnel()">Create</button>
|
|
1225
1364
|
</div>
|
|
1226
1365
|
</div>
|
|
1366
|
+
<div class="field-error" id="tunnel-field-error" role="alert"></div>
|
|
1227
1367
|
<div id="tunnel-list"></div>
|
|
1228
1368
|
<div id="tunnel-status" style="font-size:11px;color:var(--fg2);min-height:16px;margin-top:2px"></div>
|
|
1229
1369
|
</div>
|
|
@@ -1245,6 +1385,9 @@ svg { display: block; }
|
|
|
1245
1385
|
</div>
|
|
1246
1386
|
</div>
|
|
1247
1387
|
</div>
|
|
1388
|
+
<div class="settings-section" style="margin-top:12px;border-top:1px solid var(--border);padding-top:16px">
|
|
1389
|
+
<button class="btn btn-ghost" id="reset-settings-btn" onclick="resetSettings()" style="width:100%">Reset settings</button>
|
|
1390
|
+
</div>
|
|
1248
1391
|
</div>
|
|
1249
1392
|
</div>
|
|
1250
1393
|
</div>
|
|
@@ -1275,7 +1418,7 @@ svg { display: block; }
|
|
|
1275
1418
|
</div>
|
|
1276
1419
|
|
|
1277
1420
|
<!-- Terminal context menu -->
|
|
1278
|
-
<div id="term-ctx-menu" role="menu" style="position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 20px var(--shadow-color);z-index:
|
|
1421
|
+
<div id="term-ctx-menu" role="menu" style="position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 20px var(--shadow-color);z-index:var(--z-menu);display:none;min-width:180px;overflow:hidden">
|
|
1279
1422
|
<!-- Clipboard -->
|
|
1280
1423
|
<div class="ctx-item" id="term-ctx-copy" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy</div>
|
|
1281
1424
|
<div class="ctx-item" id="term-ctx-paste" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="14" y="2" width="8" height="8" rx="2"/><path d="M16 14H8M8 18h8"/><path d="M5 4h4"/><rect x="2" y="6" width="16" height="16" rx="2"/></svg> Paste</div>
|
|
@@ -1291,7 +1434,7 @@ svg { display: block; }
|
|
|
1291
1434
|
<!-- More Options -->
|
|
1292
1435
|
<div class="ctx-sep"></div>
|
|
1293
1436
|
<div class="ctx-submenu-wrap">
|
|
1294
|
-
<div class="ctx-item" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg> More Options<span class="ctx-submenu-arrow"
|
|
1437
|
+
<div class="ctx-item" role="menuitem" aria-haspopup="menu" aria-expanded="false"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg> More Options<span class="ctx-submenu-arrow"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></span></div>
|
|
1295
1438
|
<div class="ctx-submenu-items">
|
|
1296
1439
|
<div class="ctx-item" id="term-ctx-find" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Find in Terminal</div>
|
|
1297
1440
|
<div class="ctx-item" id="term-ctx-zoom-in" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg> Zoom In</div>
|
|
@@ -1309,7 +1452,7 @@ svg { display: block; }
|
|
|
1309
1452
|
</div>
|
|
1310
1453
|
|
|
1311
1454
|
<!-- Fuzzy Finder Modal -->
|
|
1312
|
-
<div class="overlay" id="finder-overlay">
|
|
1455
|
+
<div class="overlay" id="finder-overlay" role="dialog" aria-modal="true" aria-label="Search files">
|
|
1313
1456
|
<div class="modal modal-md" style="padding:12px">
|
|
1314
1457
|
<input id="finder-input" type="text" placeholder="Search files and folders…" autocomplete="off" oninput="doFinderSearch()" onkeydown="finderKeydown(event)">
|
|
1315
1458
|
<div id="finder-results"></div>
|
|
@@ -1317,6 +1460,31 @@ svg { display: block; }
|
|
|
1317
1460
|
</div>
|
|
1318
1461
|
</div>
|
|
1319
1462
|
|
|
1463
|
+
<!-- Keyboard Shortcuts Modal -->
|
|
1464
|
+
<div class="overlay" id="shortcuts-overlay">
|
|
1465
|
+
<div class="modal modal-md">
|
|
1466
|
+
<h2>Keyboard Shortcuts</h2>
|
|
1467
|
+
<div class="shortcuts-table-wrap">
|
|
1468
|
+
<table class="shortcuts-table">
|
|
1469
|
+
<tbody>
|
|
1470
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>P</kbd></td><td>File finder</td></tr>
|
|
1471
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>T</kbd></td><td>New terminal tab</td></tr>
|
|
1472
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>W</kbd></td><td>Close tab</td></tr>
|
|
1473
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>B</kbd></td><td>Toggle file explorer</td></tr>
|
|
1474
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>F</kbd></td><td>Search in terminal</td></tr>
|
|
1475
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>→</kbd>/<kbd>←</kbd></td><td>Cycle tabs</td></tr>
|
|
1476
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>S</kbd></td><td>Save editor file</td></tr>
|
|
1477
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R</kbd></td><td>Refresh preview</td></tr>
|
|
1478
|
+
<tr><td><kbd>Esc</kbd></td><td>Close menus / dialogs</td></tr>
|
|
1479
|
+
</tbody>
|
|
1480
|
+
</table>
|
|
1481
|
+
</div>
|
|
1482
|
+
<div class="modal-actions">
|
|
1483
|
+
<button class="btn btn-primary" onclick="closeOverlay('shortcuts-overlay')">Close</button>
|
|
1484
|
+
</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
</div>
|
|
1487
|
+
|
|
1320
1488
|
<!-- System Stats Modal -->
|
|
1321
1489
|
<div class="overlay" id="sys-overlay">
|
|
1322
1490
|
<div class="modal">
|
|
@@ -1373,10 +1541,11 @@ svg { display: block; }
|
|
|
1373
1541
|
<div class="modal-row">
|
|
1374
1542
|
<label for="rename-input">New Name</label>
|
|
1375
1543
|
<input type="text" id="rename-input" onkeydown="if(event.key==='Enter')confirmRename()">
|
|
1544
|
+
<div class="field-error" id="rename-error" role="alert"></div>
|
|
1376
1545
|
</div>
|
|
1377
1546
|
<div class="modal-actions">
|
|
1378
1547
|
<button class="btn btn-ghost" onclick="closeOverlay('rename-overlay')">Cancel</button>
|
|
1379
|
-
<button class="btn btn-primary" onclick="confirmRename()">Rename</button>
|
|
1548
|
+
<button class="btn btn-primary" id="rename-ok-btn" onclick="confirmRename()">Rename</button>
|
|
1380
1549
|
</div>
|
|
1381
1550
|
</div>
|
|
1382
1551
|
</div>
|
|
@@ -1388,10 +1557,11 @@ svg { display: block; }
|
|
|
1388
1557
|
<div class="modal-row">
|
|
1389
1558
|
<label for="newfolder-input">Folder Name</label>
|
|
1390
1559
|
<input type="text" id="newfolder-input" onkeydown="if(event.key==='Enter')confirmNewFolder()">
|
|
1560
|
+
<div class="field-error" id="newfolder-error" role="alert"></div>
|
|
1391
1561
|
</div>
|
|
1392
1562
|
<div class="modal-actions">
|
|
1393
1563
|
<button class="btn btn-ghost" onclick="closeOverlay('newfolder-overlay')">Cancel</button>
|
|
1394
|
-
<button class="btn btn-primary" onclick="confirmNewFolder()">Create</button>
|
|
1564
|
+
<button class="btn btn-primary" id="newfolder-ok-btn" onclick="confirmNewFolder()">Create</button>
|
|
1395
1565
|
</div>
|
|
1396
1566
|
</div>
|
|
1397
1567
|
</div>
|
|
@@ -1403,10 +1573,11 @@ svg { display: block; }
|
|
|
1403
1573
|
<div class="modal-row">
|
|
1404
1574
|
<label for="newfile-input">File Name</label>
|
|
1405
1575
|
<input type="text" id="newfile-input" onkeydown="if(event.key==='Enter')confirmNewFile()">
|
|
1576
|
+
<div class="field-error" id="newfile-error" role="alert"></div>
|
|
1406
1577
|
</div>
|
|
1407
1578
|
<div class="modal-actions">
|
|
1408
1579
|
<button class="btn btn-ghost" onclick="closeOverlay('newfile-overlay')">Cancel</button>
|
|
1409
|
-
<button class="btn btn-primary" onclick="confirmNewFile()">Create</button>
|
|
1580
|
+
<button class="btn btn-primary" id="newfile-ok-btn" onclick="confirmNewFile()">Create</button>
|
|
1410
1581
|
</div>
|
|
1411
1582
|
</div>
|
|
1412
1583
|
</div>
|
|
@@ -1427,13 +1598,25 @@ svg { display: block; }
|
|
|
1427
1598
|
</div>
|
|
1428
1599
|
</div>
|
|
1429
1600
|
|
|
1601
|
+
<!-- Confirm Dialog -->
|
|
1602
|
+
<div class="overlay" id="confirm-overlay">
|
|
1603
|
+
<div class="modal modal-sm">
|
|
1604
|
+
<h2 id="confirm-title">Confirm</h2>
|
|
1605
|
+
<p id="confirm-msg" style="font-size:13px;color:var(--fg2);line-height:1.6;white-space:pre-wrap;word-break:break-word"></p>
|
|
1606
|
+
<div class="modal-actions">
|
|
1607
|
+
<button class="btn btn-ghost" id="confirm-cancel-btn">Cancel</button>
|
|
1608
|
+
<button class="btn btn-primary" id="confirm-ok-btn">OK</button>
|
|
1609
|
+
</div>
|
|
1610
|
+
</div>
|
|
1611
|
+
</div>
|
|
1612
|
+
|
|
1430
1613
|
<!-- Image Viewer -->
|
|
1431
|
-
<div class="overlay" id="image-viewer">
|
|
1614
|
+
<div class="overlay" id="image-viewer" role="dialog" aria-modal="true" aria-label="Image preview">
|
|
1432
1615
|
<div class="modal">
|
|
1433
1616
|
<div class="iv-header">
|
|
1434
1617
|
<span class="iv-name" id="iv-name"></span>
|
|
1435
1618
|
<button class="iv-close" onclick="closeImageViewer()" aria-label="Close image viewer">
|
|
1436
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
1619
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
1437
1620
|
</button>
|
|
1438
1621
|
</div>
|
|
1439
1622
|
<div id="iv-loading" class="iv-loading">Loading…</div>
|
|
@@ -1448,7 +1631,7 @@ svg { display: block; }
|
|
|
1448
1631
|
<input type="file" id="upload-input" multiple style="display:none" onchange="handleUploadInput()">
|
|
1449
1632
|
|
|
1450
1633
|
<!-- PWA Install Banner -->
|
|
1451
|
-
<div id="install-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:
|
|
1634
|
+
<div id="install-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:var(--z-menu);background:var(--bg3);border-top:1px solid var(--border);padding:12px 16px;padding-bottom:calc(12px + env(safe-area-inset-bottom));align-items:center;gap:10px;box-shadow:0 -4px 16px rgba(0,0,0,0.3)">
|
|
1452
1635
|
<span class="install-msg" style="flex:1;font-size:13px">Install WebTun for the best experience</span>
|
|
1453
1636
|
<button class="btn btn-ghost" style="height:32px;padding:0 12px;font-size:12px" onclick="dismissInstall()">Not now</button>
|
|
1454
1637
|
<button class="btn btn-primary" style="height:32px;padding:0 12px;font-size:12px" onclick="installPWA()">Install</button>
|
|
@@ -1473,8 +1656,8 @@ let renamePath = '';
|
|
|
1473
1656
|
let editorPath = '';
|
|
1474
1657
|
let editorOriginalContent = '';
|
|
1475
1658
|
let hasTmux = false;
|
|
1476
|
-
let settings = {
|
|
1477
|
-
theme: 'light', fontSize: 14, font: "'SF Mono', 'Fira Code', Consolas, monospace",
|
|
1659
|
+
let settings = {
|
|
1660
|
+
theme: 'light', fontSize: 14, font: "'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, monospace",
|
|
1478
1661
|
cursor: 'block', blink: true, scrollback: 5000, bell: false,
|
|
1479
1662
|
mobilekeys: false, confirmclose: true, datasaver: false, autostart: false, keepAwake: false, termRightClick: true
|
|
1480
1663
|
};
|
|
@@ -1510,12 +1693,12 @@ function preventDoubleTap(el, fn, delay = 1000) {
|
|
|
1510
1693
|
|
|
1511
1694
|
function saveTabState() {
|
|
1512
1695
|
const state = tabs.map(t => ({ id: t.id, sessionId: t.sessionId, title: t.title }));
|
|
1513
|
-
try {
|
|
1696
|
+
try { safeStorage.setItem('wt-tabs', JSON.stringify(state)); } catch(e) { console.warn('Failed to save tabs:', e); }
|
|
1514
1697
|
}
|
|
1515
1698
|
|
|
1516
1699
|
function loadTabState() {
|
|
1517
1700
|
try {
|
|
1518
|
-
const val = JSON.parse(
|
|
1701
|
+
const val = JSON.parse(safeStorage.getItem('wt-tabs'));
|
|
1519
1702
|
return Array.isArray(val) ? val : [];
|
|
1520
1703
|
} catch(e) { console.warn('loadTabState error:', e); return []; }
|
|
1521
1704
|
}
|
|
@@ -1610,14 +1793,33 @@ async function unlockApp() {
|
|
|
1610
1793
|
restoreTunnels();
|
|
1611
1794
|
setupSwipeGestures();
|
|
1612
1795
|
setupFileListTouch();
|
|
1796
|
+
setupFileListKeyboard();
|
|
1797
|
+
setupCtxMenuKeyboard();
|
|
1798
|
+
setupMoreMenuKeyboard();
|
|
1613
1799
|
restoreSidebarWidth();
|
|
1614
1800
|
setupSidebarResize();
|
|
1615
1801
|
setupEditorResize();
|
|
1616
1802
|
window.addEventListener('resize', updateSidebarNarrowClass);
|
|
1617
1803
|
window._cleanups.push(() => window.removeEventListener('resize', updateSidebarNarrowClass));
|
|
1618
|
-
document.getElementById('tab-
|
|
1804
|
+
document.getElementById('tab-scroll').setAttribute('role', 'tablist');
|
|
1619
1805
|
document.getElementById('file-list-wrap').setAttribute('role', 'listbox');
|
|
1620
1806
|
|
|
1807
|
+
// Arrow-key navigation between tabs when focus is in the tab strip
|
|
1808
|
+
document.getElementById('tab-scroll').addEventListener('keydown', e => {
|
|
1809
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
1810
|
+
e.preventDefault();
|
|
1811
|
+
cycleTab(e.key === 'ArrowRight' ? 1 : -1);
|
|
1812
|
+
const t = getActiveTab();
|
|
1813
|
+
if (t?.el) t.el.focus();
|
|
1814
|
+
} else if (e.key === 'Home') {
|
|
1815
|
+
e.preventDefault();
|
|
1816
|
+
if (tabs.length) { activateTab(tabs[0].id); tabs[0].el.focus(); }
|
|
1817
|
+
} else if (e.key === 'End') {
|
|
1818
|
+
e.preventDefault();
|
|
1819
|
+
if (tabs.length) { activateTab(tabs[tabs.length - 1].id); tabs[tabs.length - 1].el.focus(); }
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1621
1823
|
// Listen for system theme changes
|
|
1622
1824
|
const systemThemeMql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1623
1825
|
const onSystemThemeChange = () => { if (settings.theme === 'system') applyTheme('system'); };
|
|
@@ -1717,7 +1919,7 @@ function setupTabDragDrop(tabEl, id) {
|
|
|
1717
1919
|
const [moved] = tabs.splice(fromIdx, 1);
|
|
1718
1920
|
const targetIdx = tabs.findIndex(t => t.id === id);
|
|
1719
1921
|
tabs.splice(targetIdx, 0, moved);
|
|
1720
|
-
const bar = document.getElementById('tab-
|
|
1922
|
+
const bar = document.getElementById('tab-scroll');
|
|
1721
1923
|
if (moved.el && moved.el.parentNode === bar) bar.insertBefore(moved.el, tabEl);
|
|
1722
1924
|
saveTabState();
|
|
1723
1925
|
};
|
|
@@ -1738,6 +1940,8 @@ function setupTabDragDrop(tabEl, id) {
|
|
|
1738
1940
|
function setupTabInlineRename(tabTitleSpan, tab) {
|
|
1739
1941
|
const startRename = (span) => {
|
|
1740
1942
|
const input = document.createElement('input');
|
|
1943
|
+
input.id = 'tab-rename-input';
|
|
1944
|
+
input.name = 'tab-rename';
|
|
1741
1945
|
input.type = 'text';
|
|
1742
1946
|
input.value = tab.title;
|
|
1743
1947
|
input.style.cssText = `width:${Math.max(60, tab.title.length * 9)}px;height:22px;font-size:12px;padding:0 6px`;
|
|
@@ -1841,7 +2045,7 @@ function createTabButton(tab) {
|
|
|
1841
2045
|
setupTabInlineRename(titleSpan, tab);
|
|
1842
2046
|
setupTabSwipeGesture(tabEl, id);
|
|
1843
2047
|
|
|
1844
|
-
document.getElementById('tab-
|
|
2048
|
+
document.getElementById('tab-scroll').appendChild(tabEl);
|
|
1845
2049
|
tab.el = tabEl;
|
|
1846
2050
|
}
|
|
1847
2051
|
|
|
@@ -1849,10 +2053,19 @@ function createTerminalWrapper(tab) {
|
|
|
1849
2053
|
const wrapper = document.createElement('div');
|
|
1850
2054
|
wrapper.className = 'term-wrapper';
|
|
1851
2055
|
wrapper.dataset.id = tab.id;
|
|
2056
|
+
const loading = document.createElement('div');
|
|
2057
|
+
loading.className = 'term-loading';
|
|
2058
|
+
loading.innerHTML = '<span class="tl-spinner"></span><span>Connecting…</span>';
|
|
2059
|
+
wrapper.appendChild(loading);
|
|
2060
|
+
tab.loadingEl = loading;
|
|
1852
2061
|
document.getElementById('terminals').appendChild(wrapper);
|
|
1853
2062
|
tab.wrapper = wrapper;
|
|
1854
2063
|
}
|
|
1855
2064
|
|
|
2065
|
+
function hideTermLoading(tab) {
|
|
2066
|
+
if (tab && tab.loadingEl) tab.loadingEl.classList.add('hidden');
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1856
2069
|
function newTab(title, sessionId, dir) {
|
|
1857
2070
|
const id = ++tabCounter;
|
|
1858
2071
|
const sid = sessionId || (hasTmux ? uuid() : null);
|
|
@@ -1912,7 +2125,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1912
2125
|
const ch = data.charCodeAt(i);
|
|
1913
2126
|
if (ch === 13 || ch === 10) { // Enter
|
|
1914
2127
|
try {
|
|
1915
|
-
// Prefer stored paste text
|
|
2128
|
+
// Prefer stored paste text
|
|
1916
2129
|
let text = tab._lastPasteText;
|
|
1917
2130
|
tab._lastPasteText = null;
|
|
1918
2131
|
if (text) {
|
|
@@ -1921,30 +2134,13 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1921
2134
|
text = lines[lines.length - 1] || lines[lines.length - 2] || text;
|
|
1922
2135
|
text = text.trim();
|
|
1923
2136
|
} else {
|
|
1924
|
-
//
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
// Scan upward to find the prompt line (contains $, #, or %)
|
|
1929
|
-
for (let r = cursorRow; r >= Math.max(0, cursorRow - 20); r--) {
|
|
1930
|
-
const row = buf.getLine(r);
|
|
1931
|
-
if (!row) break;
|
|
1932
|
-
const rowText = row.translateToString(false);
|
|
1933
|
-
// Prompt detection: line ends with $, #, or % followed by space
|
|
1934
|
-
if (/\s*[#$%]\s*$/.test(rowText)) {
|
|
1935
|
-
const promptEnd = rowText.search(/[#$%]\s*$/);
|
|
1936
|
-
if (promptEnd >= 0) {
|
|
1937
|
-
collected = rowText.substring(promptEnd + 1) + collected;
|
|
1938
|
-
}
|
|
1939
|
-
break;
|
|
1940
|
-
}
|
|
1941
|
-
collected = rowText + collected;
|
|
1942
|
-
}
|
|
1943
|
-
text = collected.trim();
|
|
1944
|
-
// Strip any trailing prompt artifacts from multi-row
|
|
1945
|
-
text = text.replace(/\s*[#$%]\s*$/, '').trim();
|
|
2137
|
+
// Use the tracked input buffer (more reliable than buffer scanning)
|
|
2138
|
+
text = (tab._currentInput || '').trim();
|
|
2139
|
+
// Strip any leaked VT/ANSI parameter junk that bypassed the escape parser
|
|
2140
|
+
text = text.replace(/^[>;\d\s]+(?=[a-zA-Z/\\~\-.])/, '').replace(/^[>;\d\s]+$/, '').trim();
|
|
1946
2141
|
}
|
|
1947
2142
|
if (text) addToCmdHist(text);
|
|
2143
|
+
tab._currentInput = ''; // Clear after history save
|
|
1948
2144
|
} catch {}
|
|
1949
2145
|
}
|
|
1950
2146
|
}
|
|
@@ -1952,6 +2148,27 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1952
2148
|
const buf = new Uint8Array(1 + enc.length);
|
|
1953
2149
|
buf[0] = 0x00; buf.set(enc, 1);
|
|
1954
2150
|
ws.send(buf.buffer);
|
|
2151
|
+
|
|
2152
|
+
// Update the keystroke buffer — skip all escape sequences and control characters
|
|
2153
|
+
for (let i = 0; i < data.length; i++) {
|
|
2154
|
+
const ch = data.charCodeAt(i);
|
|
2155
|
+
if (ch === 13 || ch === 10) { // Enter (handled above)
|
|
2156
|
+
continue;
|
|
2157
|
+
} else if (ch === 127 || ch === 8) { // Backspace / Delete
|
|
2158
|
+
if (tab._currentInput) tab._currentInput = tab._currentInput.slice(0, -1);
|
|
2159
|
+
} else if (ch === 27) { // ESC — skip entire escape sequence (CSI, SS3, OSC, DCS, etc.)
|
|
2160
|
+
i++; // skip first char after ESC (usually '[', 'O', ']', 'P')
|
|
2161
|
+
// Skip until we find a final char or run out of data
|
|
2162
|
+
while (i < data.length) {
|
|
2163
|
+
const c = data.charCodeAt(i);
|
|
2164
|
+
if (c >= 0x40 && c <= 0x7E) { i++; break; } // final char — sequence done
|
|
2165
|
+
i++;
|
|
2166
|
+
}
|
|
2167
|
+
} else if (ch >= 32 && ch < 0x7F) { // Printable ASCII only
|
|
2168
|
+
tab._currentInput = (tab._currentInput || '') + data[i];
|
|
2169
|
+
}
|
|
2170
|
+
// All other control chars (0x00-0x1F except 0x0D/0x0A) are silently dropped
|
|
2171
|
+
}
|
|
1955
2172
|
};
|
|
1956
2173
|
const sendResize = (cols, rows) => {
|
|
1957
2174
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -1963,10 +2180,11 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1963
2180
|
ws.onopen = () => {
|
|
1964
2181
|
tab.reconnectDelay = 1000;
|
|
1965
2182
|
tab.reconnectAttempts = 0;
|
|
2183
|
+
hideTermLoading(tab);
|
|
1966
2184
|
updateConnStatus(true);
|
|
1967
2185
|
const banner = document.getElementById('reconnect-banner');
|
|
1968
2186
|
if (banner) {
|
|
1969
|
-
banner.innerHTML = '
|
|
2187
|
+
banner.innerHTML = '<span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span> Reconnecting…';
|
|
1970
2188
|
banner.style.display = 'none';
|
|
1971
2189
|
}
|
|
1972
2190
|
if (isReconnect) {
|
|
@@ -2063,7 +2281,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
2063
2281
|
if (tab.reconnectAttempts >= 3) {
|
|
2064
2282
|
const banner = document.getElementById('reconnect-banner');
|
|
2065
2283
|
if (banner) {
|
|
2066
|
-
banner.innerHTML = '
|
|
2284
|
+
banner.innerHTML = 'Connection lost. <button class="btn btn-primary" data-action="manual-reconnect" style="height:26px;padding:0 12px;font-size:11px;margin-left:8px">Reconnect</button>';
|
|
2067
2285
|
banner.style.display = 'block';
|
|
2068
2286
|
}
|
|
2069
2287
|
} else {
|
|
@@ -2087,7 +2305,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
2087
2305
|
function manualReconnect() {
|
|
2088
2306
|
const banner = document.getElementById('reconnect-banner');
|
|
2089
2307
|
if (banner) {
|
|
2090
|
-
banner.innerHTML = '
|
|
2308
|
+
banner.innerHTML = '<span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span> Reconnecting…';
|
|
2091
2309
|
banner.style.display = 'block';
|
|
2092
2310
|
}
|
|
2093
2311
|
tabs.forEach(tab => {
|
|
@@ -2100,15 +2318,19 @@ function manualReconnect() {
|
|
|
2100
2318
|
});
|
|
2101
2319
|
}
|
|
2102
2320
|
|
|
2103
|
-
function closeTab(e, id) {
|
|
2321
|
+
async function closeTab(e, id) {
|
|
2104
2322
|
e.stopPropagation();
|
|
2105
|
-
if (settings.confirmclose) {
|
|
2323
|
+
if (settings.confirmclose) {
|
|
2324
|
+
const ok = await confirmDialog({ title: 'Close terminal', message: tabs.length === 1 ? 'Close last terminal?' : 'Close terminal?', okText: 'Close', cancelText: 'Cancel' });
|
|
2325
|
+
if (!ok) return;
|
|
2326
|
+
}
|
|
2106
2327
|
|
|
2107
2328
|
// Check for unsaved editor changes
|
|
2108
2329
|
const editorOpen = document.getElementById('editor-view').classList.contains('open');
|
|
2109
2330
|
const currentContent = editor ? editor.getValue() : '';
|
|
2110
2331
|
if (editorOpen && currentContent !== editorOriginalContent) {
|
|
2111
|
-
|
|
2332
|
+
const ok = await confirmDialog({ title: 'Unsaved changes', message: 'You have unsaved changes. Close anyway?', okText: 'Close', cancelText: 'Cancel', danger: true });
|
|
2333
|
+
if (!ok) return;
|
|
2112
2334
|
}
|
|
2113
2335
|
|
|
2114
2336
|
const tab = tabs.find(t => t.id === id);
|
|
@@ -2149,11 +2371,7 @@ function initTerminal(tab) {
|
|
|
2149
2371
|
convertEol: false,
|
|
2150
2372
|
bellStyle: settings.bell ? 'sound' : 'none',
|
|
2151
2373
|
smoothScrollDuration: 80,
|
|
2152
|
-
selectionTheme:
|
|
2153
|
-
extension: true,
|
|
2154
|
-
foreground: '#1a1b26',
|
|
2155
|
-
background: '#7aa2f744'
|
|
2156
|
-
}
|
|
2374
|
+
selectionTheme: getXtermSelectionTheme()
|
|
2157
2375
|
};
|
|
2158
2376
|
|
|
2159
2377
|
const term = new Terminal(cfg);
|
|
@@ -2185,6 +2403,10 @@ function initTerminal(tab) {
|
|
|
2185
2403
|
tab.searchAddon = searchAddon;
|
|
2186
2404
|
tab.closed = false;
|
|
2187
2405
|
tab.textarea = term.textarea || term.element?.querySelector('.xterm-textarea, textarea');
|
|
2406
|
+
if (tab.textarea && !tab.textarea.id) {
|
|
2407
|
+
tab.textarea.id = 'xterm-helper-' + tab.id;
|
|
2408
|
+
tab.textarea.name = 'terminal-input';
|
|
2409
|
+
}
|
|
2188
2410
|
|
|
2189
2411
|
// Intercept paste events to use bracketed paste mode (required for TUI apps like vim, nano, mc)
|
|
2190
2412
|
if (tab.textarea) {
|
|
@@ -2274,6 +2496,7 @@ function initTerminal(tab) {
|
|
|
2274
2496
|
top = Math.max(0, Math.min(top, vh - mh));
|
|
2275
2497
|
menu.style.left = left + 'px';
|
|
2276
2498
|
menu.style.top = top + 'px';
|
|
2499
|
+
menu.querySelector('.ctx-item')?.focus();
|
|
2277
2500
|
});
|
|
2278
2501
|
|
|
2279
2502
|
// Touch-to-mouse translation for TUI apps (mobile)
|
|
@@ -2370,9 +2593,9 @@ function initTerminal(tab) {
|
|
|
2370
2593
|
if (tab.fitAddon) tab.fitAddon.fit();
|
|
2371
2594
|
if (!window._pinchSaveTimer) {
|
|
2372
2595
|
window._pinchSaveTimer = setTimeout(() => {
|
|
2373
|
-
const persisted = JSON.parse(
|
|
2596
|
+
const persisted = JSON.parse(safeStorage.getItem('wt-settings') || '{}');
|
|
2374
2597
|
persisted.fontSize = tab.term.options.fontSize;
|
|
2375
|
-
|
|
2598
|
+
safeStorage.setItem('wt-settings', JSON.stringify(persisted));
|
|
2376
2599
|
window._pinchSaveTimer = null;
|
|
2377
2600
|
}, 500);
|
|
2378
2601
|
}
|
|
@@ -2504,7 +2727,7 @@ function layoutTiles() {
|
|
|
2504
2727
|
const tthClose = document.createElement('button');
|
|
2505
2728
|
tthClose.className = 'tth-close';
|
|
2506
2729
|
tthClose.setAttribute('aria-label', 'Close tab');
|
|
2507
|
-
tthClose.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2
|
|
2730
|
+
tthClose.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
2508
2731
|
hdr.appendChild(tthClose);
|
|
2509
2732
|
hdr.addEventListener('click', e => {
|
|
2510
2733
|
if (e.target.closest('.tth-close')) return;
|
|
@@ -2538,10 +2761,32 @@ function getXtermTheme() {
|
|
|
2538
2761
|
};
|
|
2539
2762
|
}
|
|
2540
2763
|
|
|
2764
|
+
function getXtermSelectionTheme() {
|
|
2765
|
+
const s = getComputedStyle(document.documentElement);
|
|
2766
|
+
const g = v => s.getPropertyValue(v).trim();
|
|
2767
|
+
return {
|
|
2768
|
+
extension: true,
|
|
2769
|
+
foreground: g('--bg'),
|
|
2770
|
+
background: g('--accent') + '44'
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2541
2774
|
// ═══════════════════════════════════════════════════════
|
|
2542
2775
|
// FILE EXPLORER
|
|
2543
2776
|
// ═══════════════════════════════════════════════════════
|
|
2544
2777
|
let loadFilesAbortController = null;
|
|
2778
|
+
let fileListLoadingTimer = null;
|
|
2779
|
+
function setFileListLoading(on) {
|
|
2780
|
+
const wrap = document.getElementById('file-list-wrap');
|
|
2781
|
+
if (!wrap) return;
|
|
2782
|
+
clearTimeout(fileListLoadingTimer);
|
|
2783
|
+
if (on) {
|
|
2784
|
+
fileListLoadingTimer = setTimeout(() => wrap.classList.add('loading'), 150);
|
|
2785
|
+
} else {
|
|
2786
|
+
fileListLoadingTimer = null;
|
|
2787
|
+
wrap.classList.remove('loading');
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2545
2790
|
|
|
2546
2791
|
async function loadFiles(dir) {
|
|
2547
2792
|
if (loadFilesAbortController) loadFilesAbortController.abort();
|
|
@@ -2563,11 +2808,13 @@ async function loadFiles(dir) {
|
|
|
2563
2808
|
|
|
2564
2809
|
const list = document.getElementById('file-list');
|
|
2565
2810
|
|
|
2811
|
+
setFileListLoading(true);
|
|
2566
2812
|
let data;
|
|
2567
2813
|
try {
|
|
2568
2814
|
data = await api(`/api/files?path=${encodeURIComponent(dir)}`, { signal: loadFilesAbortController.signal });
|
|
2569
2815
|
} catch (e) {
|
|
2570
|
-
if (e.name === 'AbortError') return;
|
|
2816
|
+
if (e.name === 'AbortError') { setFileListLoading(false); return; }
|
|
2817
|
+
setFileListLoading(false);
|
|
2571
2818
|
const errorDiv = document.createElement('div');
|
|
2572
2819
|
errorDiv.style.cssText = 'padding:16px;text-align:center;';
|
|
2573
2820
|
errorDiv.innerHTML = '<div style="color:var(--red);margin-bottom:8px">Failed to load directory</div>' +
|
|
@@ -2579,6 +2826,7 @@ async function loadFiles(dir) {
|
|
|
2579
2826
|
}
|
|
2580
2827
|
|
|
2581
2828
|
if (!data.files) {
|
|
2829
|
+
setFileListLoading(false);
|
|
2582
2830
|
if (dir !== homeDir) loadFiles(homeDir);
|
|
2583
2831
|
return;
|
|
2584
2832
|
}
|
|
@@ -2663,16 +2911,25 @@ async function loadFiles(dir) {
|
|
|
2663
2911
|
if (list.children.length === 0) {
|
|
2664
2912
|
const empty = document.createElement('div');
|
|
2665
2913
|
empty.className = 'file-list-empty';
|
|
2666
|
-
empty.innerHTML = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
2914
|
+
empty.innerHTML = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg><span>Empty directory</span>' +
|
|
2915
|
+
'<div class="empty-actions">' +
|
|
2916
|
+
'<button class="btn btn-ghost" onclick="openOverlay(\'newfile-overlay\');document.getElementById(\'newfile-input\').focus()">New file</button>' +
|
|
2917
|
+
'<button class="btn btn-ghost" onclick="openOverlay(\'newfolder-overlay\');document.getElementById(\'newfolder-input\').focus()">New folder</button>' +
|
|
2918
|
+
'<button class="btn btn-ghost" onclick="uploadFiles()">Upload</button>' +
|
|
2919
|
+
'</div>';
|
|
2667
2920
|
list.appendChild(empty);
|
|
2668
2921
|
}
|
|
2669
2922
|
|
|
2670
2923
|
updateBackBtn();
|
|
2924
|
+
setFileListLoading(false);
|
|
2671
2925
|
}
|
|
2672
2926
|
|
|
2927
|
+
let fileItemCounter = 0;
|
|
2928
|
+
|
|
2673
2929
|
function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
2674
2930
|
const div = document.createElement('div');
|
|
2675
2931
|
div.className = 'file-item';
|
|
2932
|
+
div.id = 'file-item-' + (++fileItemCounter);
|
|
2676
2933
|
div.setAttribute('role', 'option');
|
|
2677
2934
|
div.setAttribute('aria-selected', 'false');
|
|
2678
2935
|
div.dataset.path = fullPath;
|
|
@@ -2682,6 +2939,7 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2682
2939
|
<span class="file-icon">${icon}</span>
|
|
2683
2940
|
<span class="file-name ${isDir ? 'file-dir' : ''}" title="${escHtml(fullPath)}">${escHtml(name)}</span>
|
|
2684
2941
|
${size ? `<span class="file-size">${escHtml(size)}</span>` : ''}
|
|
2942
|
+
<span class="file-ellipsis" role="button" tabindex="0" aria-label="More actions for ${escHtml(name)}" title="More actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></span>
|
|
2685
2943
|
`;
|
|
2686
2944
|
div.addEventListener('click', e => {
|
|
2687
2945
|
if (e.target.closest('.file-select-check')) {
|
|
@@ -2689,6 +2947,13 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2689
2947
|
if (selectedFiles.length > 0) document.getElementById('select-actions').style.display = 'flex';
|
|
2690
2948
|
return;
|
|
2691
2949
|
}
|
|
2950
|
+
if (e.target.closest('.file-ellipsis')) {
|
|
2951
|
+
e.stopPropagation();
|
|
2952
|
+
const rect = div.querySelector('.file-ellipsis').getBoundingClientRect();
|
|
2953
|
+
const m = fullPath.match(/\.([^.]+)$/);
|
|
2954
|
+
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path: fullPath, name, isDir, ext: m ? '.' + m[1].toLowerCase() : '' });
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2692
2957
|
if (selectMode) {
|
|
2693
2958
|
toggleFileSelection(fullPath, div);
|
|
2694
2959
|
return;
|
|
@@ -2697,9 +2962,58 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2697
2962
|
else if (isImageFile(fullPath)) openImageViewer(fullPath);
|
|
2698
2963
|
else openFileEditor(fullPath);
|
|
2699
2964
|
});
|
|
2965
|
+
const ell = div.querySelector('.file-ellipsis');
|
|
2966
|
+
if (ell) {
|
|
2967
|
+
ell.addEventListener('keydown', e => {
|
|
2968
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
2969
|
+
e.preventDefault();
|
|
2970
|
+
e.stopPropagation();
|
|
2971
|
+
const rect = ell.getBoundingClientRect();
|
|
2972
|
+
const m = fullPath.match(/\.([^.]+)$/);
|
|
2973
|
+
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path: fullPath, name, isDir, ext: m ? '.' + m[1].toLowerCase() : '' });
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2700
2977
|
return div;
|
|
2701
2978
|
}
|
|
2702
2979
|
|
|
2980
|
+
function setupFileListKeyboard() {
|
|
2981
|
+
const list = document.getElementById('file-list');
|
|
2982
|
+
if (!list || list.dataset._kbSetup) return;
|
|
2983
|
+
list.dataset._kbSetup = '1';
|
|
2984
|
+
list.setAttribute('tabindex', '0');
|
|
2985
|
+
list.addEventListener('keydown', e => {
|
|
2986
|
+
const items = [...list.querySelectorAll('.file-item')];
|
|
2987
|
+
if (!items.length) return;
|
|
2988
|
+
let idx = items.findIndex(el => el.classList.contains('focused'));
|
|
2989
|
+
const focusIdx = i => {
|
|
2990
|
+
if (i < 0 || i >= items.length) return;
|
|
2991
|
+
items.forEach(el => el.classList.remove('focused'));
|
|
2992
|
+
items[i].classList.add('focused');
|
|
2993
|
+
if (items[i].id) list.setAttribute('aria-activedescendant', items[i].id);
|
|
2994
|
+
items[i].scrollIntoView({ block: 'nearest' });
|
|
2995
|
+
};
|
|
2996
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); focusIdx(idx < 0 ? 0 : Math.min(idx + 1, items.length - 1)); }
|
|
2997
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); focusIdx(idx < 0 ? items.length - 1 : Math.max(0, idx - 1)); }
|
|
2998
|
+
else if (e.key === 'Home') { e.preventDefault(); focusIdx(0); }
|
|
2999
|
+
else if (e.key === 'End') { e.preventDefault(); focusIdx(items.length - 1); }
|
|
3000
|
+
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) items[idx].click(); }
|
|
3001
|
+
else if (e.key === 'Backspace' || e.key === 'ArrowLeft') { e.preventDefault(); navigateUp(); }
|
|
3002
|
+
else if ((e.key === 'a' || e.key === 'A') && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (selectMode) selectAllFiles(); }
|
|
3003
|
+
else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
3004
|
+
const prefix = (list._typeAhead || '') + e.key.toLowerCase();
|
|
3005
|
+
list._typeAhead = prefix;
|
|
3006
|
+
clearTimeout(list._typeAheadTimer);
|
|
3007
|
+
list._typeAheadTimer = setTimeout(() => { list._typeAhead = ''; }, 500);
|
|
3008
|
+
const found = items.findIndex(el => {
|
|
3009
|
+
const nm = el.querySelector('.file-name');
|
|
3010
|
+
return nm && nm.textContent.toLowerCase().startsWith(prefix);
|
|
3011
|
+
});
|
|
3012
|
+
if (found >= 0) focusIdx(found);
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
|
|
2703
3017
|
function refreshFiles() {
|
|
2704
3018
|
if (currentPath) loadFiles(currentPath);
|
|
2705
3019
|
}
|
|
@@ -2973,8 +3287,77 @@ document.querySelector('#term-ctx-menu .ctx-submenu-wrap > .ctx-item').addEventL
|
|
|
2973
3287
|
e.stopPropagation();
|
|
2974
3288
|
const wrap = e.currentTarget.closest('.ctx-submenu-wrap');
|
|
2975
3289
|
wrap.classList.toggle('open');
|
|
3290
|
+
e.currentTarget.setAttribute('aria-expanded', wrap.classList.contains('open'));
|
|
2976
3291
|
});
|
|
2977
3292
|
|
|
3293
|
+
// ═══════════════════════════════════════════════════════
|
|
3294
|
+
// CONTEXT MENU KEYBOARD NAVIGATION
|
|
3295
|
+
// ═══════════════════════════════════════════════════════
|
|
3296
|
+
function setupCtxMenuKeyboard() {
|
|
3297
|
+
['ctx-menu', 'term-ctx-menu'].forEach(id => {
|
|
3298
|
+
const menu = document.getElementById(id);
|
|
3299
|
+
if (!menu) return;
|
|
3300
|
+
menu.querySelectorAll('.ctx-item').forEach(el => {
|
|
3301
|
+
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1');
|
|
3302
|
+
});
|
|
3303
|
+
const openSubmenu = wrap => {
|
|
3304
|
+
wrap.classList.add('open');
|
|
3305
|
+
const toggle = wrap.querySelector(':scope > .ctx-item');
|
|
3306
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'true');
|
|
3307
|
+
const first = wrap.querySelector('.ctx-submenu-items .ctx-item');
|
|
3308
|
+
if (first) first.focus();
|
|
3309
|
+
};
|
|
3310
|
+
const closeSubmenu = wrap => {
|
|
3311
|
+
wrap.classList.remove('open');
|
|
3312
|
+
const toggle = wrap.querySelector(':scope > .ctx-item');
|
|
3313
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
|
3314
|
+
if (toggle) toggle.focus();
|
|
3315
|
+
};
|
|
3316
|
+
const visibleItems = () => [...menu.querySelectorAll('.ctx-item')].filter(el => {
|
|
3317
|
+
const subWrap = el.closest('.ctx-submenu-items');
|
|
3318
|
+
if (subWrap && !subWrap.parentElement.classList.contains('open')) return false;
|
|
3319
|
+
return el.offsetParent !== null;
|
|
3320
|
+
});
|
|
3321
|
+
menu.addEventListener('keydown', e => {
|
|
3322
|
+
const items = visibleItems();
|
|
3323
|
+
if (!items.length) return;
|
|
3324
|
+
let idx = items.indexOf(document.activeElement);
|
|
3325
|
+
const activeItem = document.activeElement;
|
|
3326
|
+
switch (e.key) {
|
|
3327
|
+
case 'Escape': {
|
|
3328
|
+
const wrap = activeItem && activeItem.closest('.ctx-submenu-wrap');
|
|
3329
|
+
if (wrap && wrap.classList.contains('open')) { closeSubmenu(wrap); e.preventDefault(); return; }
|
|
3330
|
+
if (id === 'ctx-menu') document.getElementById('ctx-menu').classList.remove('open');
|
|
3331
|
+
else hideTermCtxMenu();
|
|
3332
|
+
e.preventDefault();
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
case 'ArrowDown': idx = (idx + 1) % items.length; break;
|
|
3336
|
+
case 'ArrowUp': idx = (idx - 1 + items.length) % items.length; break;
|
|
3337
|
+
case 'Home': idx = 0; break;
|
|
3338
|
+
case 'End': idx = items.length - 1; break;
|
|
3339
|
+
case 'ArrowRight': {
|
|
3340
|
+
const wrap = activeItem && activeItem.closest('.ctx-submenu-wrap');
|
|
3341
|
+
if (wrap && !wrap.classList.contains('open')) { e.preventDefault(); openSubmenu(wrap); }
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
case 'ArrowLeft': {
|
|
3345
|
+
const itemsWrap = activeItem && activeItem.closest('.ctx-submenu-items');
|
|
3346
|
+
if (itemsWrap && itemsWrap.parentElement) { e.preventDefault(); closeSubmenu(itemsWrap.parentElement); }
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
case 'Enter':
|
|
3350
|
+
case ' ':
|
|
3351
|
+
if (activeItem && activeItem.classList.contains('ctx-item')) { e.preventDefault(); activeItem.click(); }
|
|
3352
|
+
return;
|
|
3353
|
+
default: return;
|
|
3354
|
+
}
|
|
3355
|
+
e.preventDefault();
|
|
3356
|
+
if (idx >= 0 && items[idx]) items[idx].focus();
|
|
3357
|
+
});
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
|
|
2978
3361
|
function showCtxMenu(e, file) {
|
|
2979
3362
|
e.preventDefault();
|
|
2980
3363
|
ctxTarget = file;
|
|
@@ -3002,6 +3385,8 @@ function showCtxMenu(e, file) {
|
|
|
3002
3385
|
menu.style.top = top + 'px';
|
|
3003
3386
|
menu.classList.add('open');
|
|
3004
3387
|
menu.style.display = '';
|
|
3388
|
+
const firstItem = menu.querySelector('.ctx-item');
|
|
3389
|
+
if (firstItem) firstItem.focus();
|
|
3005
3390
|
}
|
|
3006
3391
|
|
|
3007
3392
|
document.addEventListener('click', () => {
|
|
@@ -3164,19 +3549,23 @@ function startRename(path, name) {
|
|
|
3164
3549
|
|
|
3165
3550
|
async function confirmRename() {
|
|
3166
3551
|
const newName = document.getElementById('rename-input').value.trim();
|
|
3167
|
-
if (!newName) return;
|
|
3552
|
+
if (!newName) { showFieldError('rename-error', 'Enter a new name'); return; }
|
|
3553
|
+
clearFieldError('rename-error');
|
|
3554
|
+
setBtnBusy(document.getElementById('rename-ok-btn'), true);
|
|
3168
3555
|
const r = await api('/api/files/rename', {
|
|
3169
3556
|
method: 'POST',
|
|
3170
3557
|
headers: { 'Content-Type': 'application/json' },
|
|
3171
3558
|
body: JSON.stringify({ oldPath: renamePath, newName })
|
|
3172
3559
|
});
|
|
3560
|
+
setBtnBusy(document.getElementById('rename-ok-btn'), false);
|
|
3173
3561
|
if (r.success) { toast('Renamed', 'success'); refreshFiles(); }
|
|
3174
|
-
else
|
|
3562
|
+
else showFieldError('rename-error', r.error || 'Rename failed');
|
|
3175
3563
|
closeOverlay('rename-overlay');
|
|
3176
3564
|
}
|
|
3177
3565
|
|
|
3178
3566
|
async function deleteFile(file) {
|
|
3179
|
-
|
|
3567
|
+
const ok = await confirmDialog({ title: 'Delete', message: `Delete "${file.name}"?${file.isDir ? '\n\nThis will delete the entire directory.' : ''}`, okText: 'Delete', danger: true });
|
|
3568
|
+
if (!ok) return;
|
|
3180
3569
|
const r = await api(`/api/files?path=${encodeURIComponent(file.path)}`, { method: 'DELETE' });
|
|
3181
3570
|
if (r.success) { toast('Deleted', 'success'); refreshFiles(); }
|
|
3182
3571
|
else toast(r.error, 'error');
|
|
@@ -3220,6 +3609,8 @@ function toggleSelectMode() {
|
|
|
3220
3609
|
selectedFiles = [];
|
|
3221
3610
|
document.getElementById('select-actions').style.display = selectMode ? 'flex' : 'none';
|
|
3222
3611
|
document.getElementById('sidebar').dataset.selectMode = selectMode ? 'true' : '';
|
|
3612
|
+
const flw = document.getElementById('file-list-wrap');
|
|
3613
|
+
if (flw) flw.setAttribute('aria-multiselectable', String(selectMode));
|
|
3223
3614
|
document.getElementById('select-count').textContent = '0 selected';
|
|
3224
3615
|
document.querySelector('[onclick="toggleSelectMode()"]')?.classList.toggle('active', selectMode);
|
|
3225
3616
|
refreshFiles();
|
|
@@ -3283,7 +3674,8 @@ function clearSelection() {
|
|
|
3283
3674
|
async function deleteSelected() {
|
|
3284
3675
|
if (!selectedFiles.length) return;
|
|
3285
3676
|
const msg = `Delete ${selectedFiles.length} item${selectedFiles.length > 1 ? 's' : ''}?`;
|
|
3286
|
-
|
|
3677
|
+
const ok = await confirmDialog({ title: 'Delete', message: msg, okText: 'Delete', danger: true });
|
|
3678
|
+
if (!ok) return;
|
|
3287
3679
|
const results = await Promise.allSettled(
|
|
3288
3680
|
[...selectedFiles].map(p =>
|
|
3289
3681
|
api(`/api/files?path=${encodeURIComponent(p)}`, { method: 'DELETE' })
|
|
@@ -3324,15 +3716,17 @@ function newFolder() {
|
|
|
3324
3716
|
|
|
3325
3717
|
async function confirmNewFolder() {
|
|
3326
3718
|
const name = document.getElementById('newfolder-input').value.trim();
|
|
3327
|
-
if (!name) return;
|
|
3719
|
+
if (!name) { showFieldError('newfolder-error', 'Enter a folder name'); return; }
|
|
3720
|
+
clearFieldError('newfolder-error');
|
|
3721
|
+
setBtnBusy(document.getElementById('newfolder-ok-btn'), true);
|
|
3328
3722
|
const r = await api('/api/files/mkdir', {
|
|
3329
3723
|
method: 'POST',
|
|
3330
3724
|
headers: { 'Content-Type': 'application/json' },
|
|
3331
3725
|
body: JSON.stringify({ path: joinPath(currentPath, name) })
|
|
3332
3726
|
});
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3727
|
+
setBtnBusy(document.getElementById('newfolder-ok-btn'), false);
|
|
3728
|
+
if (r.success) { toast('Folder created', 'success'); refreshFiles(); closeOverlay('newfolder-overlay'); }
|
|
3729
|
+
else showFieldError('newfolder-error', r.error || 'Could not create folder');
|
|
3336
3730
|
}
|
|
3337
3731
|
|
|
3338
3732
|
function newFile() {
|
|
@@ -3343,15 +3737,18 @@ function newFile() {
|
|
|
3343
3737
|
|
|
3344
3738
|
async function confirmNewFile() {
|
|
3345
3739
|
const name = document.getElementById('newfile-input').value.trim();
|
|
3346
|
-
if (!name) return;
|
|
3740
|
+
if (!name) { showFieldError('newfile-error', 'Enter a file name'); return; }
|
|
3741
|
+
clearFieldError('newfile-error');
|
|
3742
|
+
setBtnBusy(document.getElementById('newfile-ok-btn'), true);
|
|
3347
3743
|
const filePath = joinPath(currentPath, name);
|
|
3348
3744
|
const r = await api('/api/files/touch', {
|
|
3349
3745
|
method: 'POST',
|
|
3350
3746
|
headers: { 'Content-Type': 'application/json' },
|
|
3351
3747
|
body: JSON.stringify({ path: filePath })
|
|
3352
3748
|
});
|
|
3749
|
+
setBtnBusy(document.getElementById('newfile-ok-btn'), false);
|
|
3353
3750
|
if (r.success) { refreshFiles(); closeOverlay('newfile-overlay'); openFileEditor(filePath); }
|
|
3354
|
-
else
|
|
3751
|
+
else showFieldError('newfile-error', r.error || 'Could not create file');
|
|
3355
3752
|
}
|
|
3356
3753
|
|
|
3357
3754
|
// Upload
|
|
@@ -3454,11 +3851,11 @@ async function uploadFileList(items) {
|
|
|
3454
3851
|
|
|
3455
3852
|
// Final result
|
|
3456
3853
|
if (failedItems.length === 0) {
|
|
3457
|
-
setUploadProgress(100, 'success',
|
|
3854
|
+
setUploadProgress(100, 'success', `${completed}/${total} uploaded`);
|
|
3458
3855
|
} else if (completed > 0) {
|
|
3459
|
-
setUploadProgress(100, 'error',
|
|
3856
|
+
setUploadProgress(100, 'error', `${completed} ok, ${failedItems.length} failed`);
|
|
3460
3857
|
} else {
|
|
3461
|
-
setUploadProgress(100, 'error',
|
|
3858
|
+
setUploadProgress(100, 'error', `All ${total} failed`);
|
|
3462
3859
|
}
|
|
3463
3860
|
|
|
3464
3861
|
clearUploadProgress(2000);
|
|
@@ -3493,9 +3890,17 @@ function initCodeMirror() {
|
|
|
3493
3890
|
},
|
|
3494
3891
|
}
|
|
3495
3892
|
});
|
|
3893
|
+
editor.on('change', () => updateEditorDirty());
|
|
3496
3894
|
return editor;
|
|
3497
3895
|
}
|
|
3498
3896
|
|
|
3897
|
+
function updateEditorDirty() {
|
|
3898
|
+
const el = document.getElementById('editor-view');
|
|
3899
|
+
if (!el) return;
|
|
3900
|
+
const currentContent = editor ? editor.getValue() : document.getElementById('editor-textarea').value;
|
|
3901
|
+
el.classList.toggle('editor-dirty', currentContent !== editorOriginalContent);
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3499
3904
|
function getCMmode(fileName) {
|
|
3500
3905
|
const m = fileName.match(/\.([^.]+)$/);
|
|
3501
3906
|
if (!m) return null;
|
|
@@ -3536,7 +3941,8 @@ async function openFileEditor(path) {
|
|
|
3536
3941
|
const r = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
|
|
3537
3942
|
if (r.error) { toast(r.error, 'error'); return; }
|
|
3538
3943
|
if (r.length > MAX_EDITOR_SIZE) {
|
|
3539
|
-
|
|
3944
|
+
const ok = await confirmDialog({ title: 'Large file', message: `File is ${(r.length / 1024 / 1024).toFixed(1)}MB. Open anyway?`, okText: 'Open', danger: true });
|
|
3945
|
+
if (!ok) return;
|
|
3540
3946
|
}
|
|
3541
3947
|
editorPath = path;
|
|
3542
3948
|
editorOriginalContent = r.content;
|
|
@@ -3550,6 +3956,9 @@ async function openFileEditor(path) {
|
|
|
3550
3956
|
cm.setOption('mode', getCMmode(fileName) || 'text/plain');
|
|
3551
3957
|
document.querySelector('.CodeMirror').style.display = '';
|
|
3552
3958
|
cm.refresh();
|
|
3959
|
+
const ta = document.getElementById('editor-textarea');
|
|
3960
|
+
ta.oninput = () => updateEditorDirty();
|
|
3961
|
+
updateEditorDirty();
|
|
3553
3962
|
|
|
3554
3963
|
const preview = document.getElementById('editor-preview');
|
|
3555
3964
|
const toggleBtn = document.getElementById('md-preview-toggle');
|
|
@@ -3565,7 +3974,7 @@ async function openFileEditor(path) {
|
|
|
3565
3974
|
toggleBtn.style.display = 'none';
|
|
3566
3975
|
if (refreshBtn) refreshBtn.style.display = 'none';
|
|
3567
3976
|
}
|
|
3568
|
-
document.getElementById('editor-filename').textContent = fileName + ' ' + path;
|
|
3977
|
+
document.getElementById('editor-filename').textContent = fileName + ' / ' + path;
|
|
3569
3978
|
document.getElementById('editor-status').textContent = '';
|
|
3570
3979
|
|
|
3571
3980
|
// Desktop: split layout; Mobile: overlay (handled by CSS)
|
|
@@ -3576,7 +3985,7 @@ async function openFileEditor(path) {
|
|
|
3576
3985
|
const splitArea = document.getElementById('editor-split-area');
|
|
3577
3986
|
const orientToggle = document.getElementById('editor-split-toggle');
|
|
3578
3987
|
try {
|
|
3579
|
-
const orient =
|
|
3988
|
+
const orient = safeStorage.getItem('wt-editor-split-orientation');
|
|
3580
3989
|
const isHoriz = orient === 'horizontal';
|
|
3581
3990
|
if (splitArea) splitArea.classList.toggle('horizontal', isHoriz);
|
|
3582
3991
|
if (orientToggle) {
|
|
@@ -3600,7 +4009,7 @@ async function openFileEditor(path) {
|
|
|
3600
4009
|
try {
|
|
3601
4010
|
const isHoriz = splitArea && splitArea.classList.contains('horizontal');
|
|
3602
4011
|
const key = isHoriz ? 'wt-editor-width' : 'wt-editor-height';
|
|
3603
|
-
const saved =
|
|
4012
|
+
const saved = safeStorage.getItem(key);
|
|
3604
4013
|
if (saved) {
|
|
3605
4014
|
document.documentElement.style.setProperty('--editor-size', saved + 'px');
|
|
3606
4015
|
}
|
|
@@ -3617,25 +4026,29 @@ async function openFileEditor(path) {
|
|
|
3617
4026
|
|
|
3618
4027
|
async function saveFile() {
|
|
3619
4028
|
if (!editorPath) { toast('No file open', 'error'); return; }
|
|
4029
|
+
setBtnBusy(document.getElementById('editor-save-btn'), true);
|
|
3620
4030
|
const content = editor ? editor.getValue() : document.getElementById('editor-textarea').value;
|
|
3621
4031
|
const r = await api('/api/files/write', {
|
|
3622
4032
|
method: 'POST',
|
|
3623
4033
|
headers: { 'Content-Type': 'application/json' },
|
|
3624
4034
|
body: JSON.stringify({ path: editorPath, content })
|
|
3625
4035
|
});
|
|
4036
|
+
setBtnBusy(document.getElementById('editor-save-btn'), false);
|
|
3626
4037
|
if (r.success) {
|
|
3627
4038
|
editorOriginalContent = content;
|
|
3628
|
-
|
|
4039
|
+
updateEditorDirty();
|
|
4040
|
+
document.getElementById('editor-status').textContent = 'Saved';
|
|
3629
4041
|
toast('Saved', 'success');
|
|
3630
4042
|
if (mdPreviewActive) refreshPreview();
|
|
3631
4043
|
setTimeout(() => document.getElementById('editor-status').textContent = '', 2000);
|
|
3632
4044
|
} else toast(r.error, 'error');
|
|
3633
4045
|
}
|
|
3634
4046
|
|
|
3635
|
-
function closeEditor() {
|
|
4047
|
+
async function closeEditor() {
|
|
3636
4048
|
const currentContent = editor ? editor.getValue() : document.getElementById('editor-textarea').value;
|
|
3637
4049
|
if (currentContent !== editorOriginalContent) {
|
|
3638
|
-
|
|
4050
|
+
const ok = await confirmDialog({ title: 'Unsaved changes', message: 'You have unsaved changes. Close anyway?', okText: 'Discard', cancelText: 'Keep Editing', danger: true });
|
|
4051
|
+
if (!ok) return;
|
|
3639
4052
|
}
|
|
3640
4053
|
clearPreviewLiveReload();
|
|
3641
4054
|
document.getElementById('editor-view').classList.remove('open');
|
|
@@ -3933,7 +4346,7 @@ function searchKeydown(e) {
|
|
|
3933
4346
|
// ═══════════════════════════════════════════════════════
|
|
3934
4347
|
function loadSettings() {
|
|
3935
4348
|
try {
|
|
3936
|
-
const s = JSON.parse(
|
|
4349
|
+
const s = JSON.parse(safeStorage.getItem('wt-settings'));
|
|
3937
4350
|
if (s && typeof s === 'object' && !Array.isArray(s)) {
|
|
3938
4351
|
if (typeof s.theme === 'string' && ['system','tokyonight','light','solarized','gruvbox','dracula','monokai'].includes(s.theme)) settings.theme = s.theme;
|
|
3939
4352
|
if (typeof s.fontSize === 'number' && s.fontSize >= 8 && s.fontSize <= 32) settings.fontSize = s.fontSize;
|
|
@@ -3965,7 +4378,34 @@ function loadSettings() {
|
|
|
3965
4378
|
}
|
|
3966
4379
|
}
|
|
3967
4380
|
|
|
3968
|
-
function saveSettings() {
|
|
4381
|
+
function saveSettings() { safeStorage.setItem('wt-settings', JSON.stringify(settings)); }
|
|
4382
|
+
|
|
4383
|
+
const DEFAULT_SETTINGS = {
|
|
4384
|
+
theme: 'light', fontSize: 14, font: "'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, monospace",
|
|
4385
|
+
cursor: 'block', blink: true, scrollback: 5000, bell: false,
|
|
4386
|
+
mobilekeys: false, confirmclose: true, datasaver: false, autostart: false, keepAwake: false, termRightClick: true
|
|
4387
|
+
};
|
|
4388
|
+
|
|
4389
|
+
async function resetSettings() {
|
|
4390
|
+
const ok = await confirmDialog({ title: 'Reset settings', message: 'Reset all settings to defaults?', okText: 'Reset', danger: true });
|
|
4391
|
+
if (!ok) return;
|
|
4392
|
+
settings = { ...DEFAULT_SETTINGS };
|
|
4393
|
+
saveSettings();
|
|
4394
|
+
document.getElementById('s-theme').value = settings.theme;
|
|
4395
|
+
document.getElementById('s-fontsize').value = settings.fontSize;
|
|
4396
|
+
document.getElementById('s-scrollback').value = settings.scrollback;
|
|
4397
|
+
document.getElementById('s-font').value = settings.font;
|
|
4398
|
+
document.getElementById('s-cursor').value = settings.cursor;
|
|
4399
|
+
syncToggle('blink'); syncToggle('bell'); syncToggle('mobilekeys'); syncToggle('confirmclose'); syncToggle('datasaver'); syncToggle('termRightClick');
|
|
4400
|
+
applyTheme(settings.theme);
|
|
4401
|
+
applyFontSize(settings.fontSize);
|
|
4402
|
+
applyScrollback(settings.scrollback);
|
|
4403
|
+
if (settings.font) applyFont(settings.font);
|
|
4404
|
+
applyCursor(settings.cursor);
|
|
4405
|
+
const mk = document.getElementById('mobile-keys');
|
|
4406
|
+
if (mk) mk.style.display = settings.mobilekeys && window.innerWidth <= 768 ? 'flex' : 'none';
|
|
4407
|
+
toast('Settings reset', 'success');
|
|
4408
|
+
}
|
|
3969
4409
|
|
|
3970
4410
|
function saveSetting(k, v) { settings[k] = v; saveSettings(); }
|
|
3971
4411
|
|
|
@@ -4052,50 +4492,57 @@ function renderTunnels() {
|
|
|
4052
4492
|
list.style.display = 'block';
|
|
4053
4493
|
tunnelList.forEach(t => {
|
|
4054
4494
|
const row = document.createElement('div');
|
|
4055
|
-
row.
|
|
4495
|
+
row.className = 'tunnel-row';
|
|
4056
4496
|
const label = document.createElement('div');
|
|
4057
|
-
label.
|
|
4497
|
+
label.className = 'tunnel-label';
|
|
4058
4498
|
label.textContent = t.localUrl;
|
|
4059
4499
|
row.appendChild(label);
|
|
4060
4500
|
const inner = document.createElement('div');
|
|
4061
|
-
inner.
|
|
4501
|
+
inner.className = 'tunnel-inner';
|
|
4062
4502
|
const urlSpan = document.createElement('span');
|
|
4063
|
-
urlSpan.
|
|
4503
|
+
urlSpan.className = 'tunnel-url';
|
|
4064
4504
|
urlSpan.textContent = t.tunnelUrl;
|
|
4065
4505
|
urlSpan.title = t.tunnelUrl;
|
|
4066
4506
|
inner.appendChild(urlSpan);
|
|
4067
4507
|
const status = document.createElement('span');
|
|
4068
|
-
status.
|
|
4508
|
+
status.className = 'tunnel-status';
|
|
4509
|
+
status.style.background = t.alive ? 'var(--green)' : 'var(--yellow)';
|
|
4510
|
+
status.style.color = 'var(--bg)';
|
|
4069
4511
|
status.textContent = t.alive ? 'Live' : 'Orphan';
|
|
4070
4512
|
status.title = t.alive ? 'cloudflared is running' : 'cloudflared process not found';
|
|
4071
4513
|
inner.appendChild(status);
|
|
4072
4514
|
if (t.alive && t.targetAlive !== undefined) {
|
|
4073
4515
|
const targetStatus = document.createElement('span');
|
|
4074
4516
|
const ok = t.targetAlive;
|
|
4075
|
-
targetStatus.
|
|
4076
|
-
targetStatus.
|
|
4517
|
+
targetStatus.className = 'tunnel-target';
|
|
4518
|
+
targetStatus.style.background = ok ? 'var(--green)' : 'var(--red)';
|
|
4519
|
+
targetStatus.style.color = 'var(--bg)';
|
|
4520
|
+
targetStatus.style.opacity = '0.8';
|
|
4521
|
+
targetStatus.appendChild(mkSvg('9','9','0 0 24 24', ok
|
|
4522
|
+
? '<polyline points="20 6 9 17 4 12"/>'
|
|
4523
|
+
: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>'));
|
|
4077
4524
|
targetStatus.title = ok ? `Target ${t.localUrl} is responding` : `Target ${t.localUrl} is not responding`;
|
|
4078
4525
|
inner.appendChild(targetStatus);
|
|
4079
4526
|
}
|
|
4080
4527
|
const copyBtn = document.createElement('button');
|
|
4081
|
-
copyBtn.className = 'icon-btn';
|
|
4082
|
-
copyBtn.style.cssText = 'width:22px;height:22px;flex-shrink:0';
|
|
4528
|
+
copyBtn.className = 'icon-btn tunnel-btn';
|
|
4083
4529
|
copyBtn.title = 'Copy';
|
|
4084
|
-
copyBtn.
|
|
4530
|
+
copyBtn.setAttribute('aria-label', 'Copy tunnel URL');
|
|
4531
|
+
copyBtn.appendChild(mkSvg('12','12','0 0 24 24','<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'));
|
|
4085
4532
|
copyBtn.addEventListener('click', () => copyText(t.tunnelUrl));
|
|
4086
4533
|
inner.appendChild(copyBtn);
|
|
4087
4534
|
const openBtn = document.createElement('button');
|
|
4088
|
-
openBtn.className = 'icon-btn';
|
|
4089
|
-
openBtn.style.cssText = 'width:22px;height:22px;flex-shrink:0';
|
|
4535
|
+
openBtn.className = 'icon-btn tunnel-btn';
|
|
4090
4536
|
openBtn.title = 'Open in new tab';
|
|
4091
|
-
openBtn.
|
|
4537
|
+
openBtn.setAttribute('aria-label', 'Open tunnel in new tab');
|
|
4538
|
+
openBtn.appendChild(mkSvg('12','12','0 0 24 24','<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>'));
|
|
4092
4539
|
openBtn.addEventListener('click', () => window.open(t.tunnelUrl, '_blank'));
|
|
4093
4540
|
inner.appendChild(openBtn);
|
|
4094
4541
|
const stopBtn = document.createElement('button');
|
|
4095
|
-
stopBtn.className = 'icon-btn';
|
|
4096
|
-
stopBtn.style.cssText = 'width:22px;height:22px;flex-shrink:0;color:var(--red)';
|
|
4542
|
+
stopBtn.className = 'icon-btn tunnel-btn stop';
|
|
4097
4543
|
stopBtn.title = 'Stop';
|
|
4098
|
-
stopBtn.
|
|
4544
|
+
stopBtn.setAttribute('aria-label', 'Stop tunnel');
|
|
4545
|
+
stopBtn.appendChild(mkSvg('12','12','0 0 24 24','<rect x="6" y="6" width="12" height="12" rx="1"/>'));
|
|
4099
4546
|
stopBtn.addEventListener('click', () => stopTunnelById(t.id));
|
|
4100
4547
|
inner.appendChild(stopBtn);
|
|
4101
4548
|
row.appendChild(inner);
|
|
@@ -4104,22 +4551,24 @@ function renderTunnels() {
|
|
|
4104
4551
|
}
|
|
4105
4552
|
|
|
4106
4553
|
async function createTunnel() {
|
|
4107
|
-
const btn = document.
|
|
4554
|
+
const btn = document.getElementById('tunnel-create-btn');
|
|
4108
4555
|
if (btn && btn.dataset.busy === 'true') return;
|
|
4109
|
-
if (btn) btn.dataset.busy = 'true';
|
|
4110
4556
|
const url = document.getElementById('tunnel-url').value.trim();
|
|
4111
|
-
if (!url) {
|
|
4557
|
+
if (!url) { showFieldError('tunnel-field-error', 'Enter a local URL'); return; }
|
|
4558
|
+
clearFieldError('tunnel-field-error');
|
|
4559
|
+
setBtnBusy(btn, true);
|
|
4112
4560
|
document.getElementById('tunnel-status').textContent = 'Creating tunnel…';
|
|
4113
4561
|
const r = await api('/api/tunnel', {
|
|
4114
4562
|
method: 'POST',
|
|
4115
4563
|
headers: { 'Content-Type': 'application/json' },
|
|
4116
4564
|
body: JSON.stringify({ url })
|
|
4117
4565
|
});
|
|
4566
|
+
setBtnBusy(btn, false);
|
|
4118
4567
|
if (r.success) {
|
|
4119
4568
|
tunnelList.push({ id: r.id, tunnelUrl: r.url, localUrl: url, alive: true, createdAt: Date.now() });
|
|
4120
4569
|
renderTunnels();
|
|
4121
4570
|
if (r.warning) {
|
|
4122
|
-
document.getElementById('tunnel-status').textContent =
|
|
4571
|
+
document.getElementById('tunnel-status').textContent = r.warning;
|
|
4123
4572
|
toast(r.warning, 'warning');
|
|
4124
4573
|
} else {
|
|
4125
4574
|
document.getElementById('tunnel-status').textContent = '';
|
|
@@ -4127,9 +4576,8 @@ async function createTunnel() {
|
|
|
4127
4576
|
}
|
|
4128
4577
|
} else {
|
|
4129
4578
|
document.getElementById('tunnel-status').textContent = '';
|
|
4130
|
-
|
|
4579
|
+
showFieldError('tunnel-field-error', r.error || 'Failed to create tunnel');
|
|
4131
4580
|
}
|
|
4132
|
-
if (btn) setTimeout(() => btn.dataset.busy = 'false', 2000);
|
|
4133
4581
|
}
|
|
4134
4582
|
|
|
4135
4583
|
async function stopTunnelById(id) {
|
|
@@ -4167,7 +4615,7 @@ function applyTheme(theme, save = true) {
|
|
|
4167
4615
|
document.documentElement.dataset.theme = resolved;
|
|
4168
4616
|
settings.theme = theme;
|
|
4169
4617
|
document.getElementById('s-theme').value = theme;
|
|
4170
|
-
tabs.forEach(t => { if (t.term) { t.term.options.theme = getXtermTheme(); } });
|
|
4618
|
+
tabs.forEach(t => { if (t.term) { t.term.options.theme = getXtermTheme(); t.term.options.selectionTheme = getXtermSelectionTheme(); } });
|
|
4171
4619
|
const themeColor = getComputedStyle(document.body).getPropertyValue('--bg2').trim();
|
|
4172
4620
|
document.querySelector('meta[name="theme-color"]').content = themeColor;
|
|
4173
4621
|
if (save) saveSettings();
|
|
@@ -4222,6 +4670,64 @@ function toggleSidebar() {
|
|
|
4222
4670
|
}
|
|
4223
4671
|
}
|
|
4224
4672
|
|
|
4673
|
+
let moreMenuOpen = false;
|
|
4674
|
+
function closeMoreMenu() {
|
|
4675
|
+
const menu = document.getElementById('more-menu');
|
|
4676
|
+
const btn = document.getElementById('more-btn');
|
|
4677
|
+
if (menu) menu.classList.remove('open');
|
|
4678
|
+
if (btn) btn.classList.remove('active');
|
|
4679
|
+
document.removeEventListener('click', closeMoreMenuOnClickOutside, true);
|
|
4680
|
+
moreMenuOpen = false;
|
|
4681
|
+
}
|
|
4682
|
+
function closeMoreMenuOnClickOutside(e) {
|
|
4683
|
+
const menu = document.getElementById('more-menu');
|
|
4684
|
+
const btn = document.getElementById('more-btn');
|
|
4685
|
+
if (menu && menu.contains(e.target)) return;
|
|
4686
|
+
if (btn && btn.contains(e.target)) return;
|
|
4687
|
+
closeMoreMenu();
|
|
4688
|
+
}
|
|
4689
|
+
function toggleMoreMenu() {
|
|
4690
|
+
if (moreMenuOpen) { closeMoreMenu(); return; }
|
|
4691
|
+
moreMenuOpen = true;
|
|
4692
|
+
const menu = document.getElementById('more-menu');
|
|
4693
|
+
const btn = document.getElementById('more-btn');
|
|
4694
|
+
if (menu) menu.classList.add('open');
|
|
4695
|
+
if (btn) btn.classList.add('active');
|
|
4696
|
+
setTimeout(() => {
|
|
4697
|
+
document.addEventListener('click', closeMoreMenuOnClickOutside, true);
|
|
4698
|
+
const first = menu && menu.querySelector('.mm-item');
|
|
4699
|
+
if (first) first.focus();
|
|
4700
|
+
}, 0);
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
function setupMoreMenuKeyboard() {
|
|
4704
|
+
const menu = document.getElementById('more-menu');
|
|
4705
|
+
if (!menu || menu.dataset._kbSetup) return;
|
|
4706
|
+
menu.dataset._kbSetup = '1';
|
|
4707
|
+
menu.querySelectorAll('.mm-item').forEach(el => { if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1'); });
|
|
4708
|
+
menu.addEventListener('keydown', e => {
|
|
4709
|
+
const items = [...menu.querySelectorAll('.mm-item')].filter(el => el.offsetParent !== null);
|
|
4710
|
+
if (!items.length) return;
|
|
4711
|
+
let idx = items.indexOf(document.activeElement);
|
|
4712
|
+
switch (e.key) {
|
|
4713
|
+
case 'Escape': e.preventDefault(); closeMoreMenu(); return;
|
|
4714
|
+
case 'ArrowDown': idx = (idx + 1) % items.length; break;
|
|
4715
|
+
case 'ArrowUp': idx = (idx - 1 + items.length) % items.length; break;
|
|
4716
|
+
case 'Home': idx = 0; break;
|
|
4717
|
+
case 'End': idx = items.length - 1; break;
|
|
4718
|
+
case 'Tab': {
|
|
4719
|
+
const last = document.activeElement === items[items.length - 1];
|
|
4720
|
+
if (last && !e.shiftKey) { e.preventDefault(); closeMoreMenu(); return; }
|
|
4721
|
+
if (document.activeElement === items[0] && e.shiftKey) { e.preventDefault(); closeMoreMenu(); return; }
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
default: return;
|
|
4725
|
+
}
|
|
4726
|
+
e.preventDefault();
|
|
4727
|
+
if (idx >= 0 && items[idx]) items[idx].focus();
|
|
4728
|
+
});
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4225
4731
|
function updateSidebarNarrowClass() {
|
|
4226
4732
|
const sb = document.getElementById('sidebar');
|
|
4227
4733
|
if (!sb) return;
|
|
@@ -4265,7 +4771,7 @@ function setupSidebarResize() {
|
|
|
4265
4771
|
document.removeEventListener('mouseup', stopResize);
|
|
4266
4772
|
document.removeEventListener('touchmove', doResize);
|
|
4267
4773
|
document.removeEventListener('touchend', stopResize);
|
|
4268
|
-
try {
|
|
4774
|
+
try { safeStorage.setItem('wt-sidebar-width', String(sidebar.offsetWidth)); } catch(e) { console.warn(e); }
|
|
4269
4775
|
}
|
|
4270
4776
|
handle.addEventListener('mousedown', startResize);
|
|
4271
4777
|
handle.addEventListener('touchstart', startResize, { passive: false });
|
|
@@ -4274,7 +4780,7 @@ function setupSidebarResize() {
|
|
|
4274
4780
|
function restoreSidebarWidth() {
|
|
4275
4781
|
if (window.innerWidth <= 768) return;
|
|
4276
4782
|
try {
|
|
4277
|
-
const savedW =
|
|
4783
|
+
const savedW = safeStorage.getItem('wt-sidebar-width');
|
|
4278
4784
|
if (savedW) {
|
|
4279
4785
|
const w = Math.max(150, Math.min(600, parseInt(savedW)));
|
|
4280
4786
|
if (w) {
|
|
@@ -4352,7 +4858,7 @@ function setupEditorResize() {
|
|
|
4352
4858
|
const v = document.documentElement.style.getPropertyValue('--editor-size');
|
|
4353
4859
|
if (v) {
|
|
4354
4860
|
const key = isHorizontal() ? 'wt-editor-width' : 'wt-editor-height';
|
|
4355
|
-
|
|
4861
|
+
safeStorage.setItem(key, v.replace('px', ''));
|
|
4356
4862
|
}
|
|
4357
4863
|
} catch(e) { console.warn(e); }
|
|
4358
4864
|
}
|
|
@@ -4391,7 +4897,7 @@ function toggleEditorSplit() {
|
|
|
4391
4897
|
// Restore saved size for this orientation
|
|
4392
4898
|
try {
|
|
4393
4899
|
const key = isHoriz ? 'wt-editor-width' : 'wt-editor-height';
|
|
4394
|
-
const saved =
|
|
4900
|
+
const saved = safeStorage.getItem(key);
|
|
4395
4901
|
if (saved) {
|
|
4396
4902
|
document.documentElement.style.setProperty('--editor-size', saved + 'px');
|
|
4397
4903
|
}
|
|
@@ -4403,7 +4909,7 @@ function toggleEditorSplit() {
|
|
|
4403
4909
|
});
|
|
4404
4910
|
|
|
4405
4911
|
// Persist orientation
|
|
4406
|
-
try {
|
|
4912
|
+
try { safeStorage.setItem('wt-editor-split-orientation', isHoriz ? 'horizontal' : 'vertical'); } catch(e) { console.warn(e); }
|
|
4407
4913
|
}
|
|
4408
4914
|
|
|
4409
4915
|
// ═══════════════════════════════════════════════════════
|
|
@@ -4455,9 +4961,8 @@ function moveTabToEnd(fromId) {
|
|
|
4455
4961
|
if (fromIdx === -1) return;
|
|
4456
4962
|
const [moved] = tabs.splice(fromIdx, 1);
|
|
4457
4963
|
tabs.push(moved);
|
|
4458
|
-
const bar = document.getElementById('tab-
|
|
4459
|
-
|
|
4460
|
-
if (moved.el && moved.el.parentNode === bar) bar.insertBefore(moved.el, newTabBtn);
|
|
4964
|
+
const bar = document.getElementById('tab-scroll');
|
|
4965
|
+
if (moved.el && moved.el.parentNode === bar) bar.appendChild(moved.el);
|
|
4461
4966
|
saveTabState();
|
|
4462
4967
|
}
|
|
4463
4968
|
|
|
@@ -4502,7 +5007,7 @@ function setupKeyboardShortcuts() {
|
|
|
4502
5007
|
if (e.key === 'Escape') {
|
|
4503
5008
|
const termMenu = document.getElementById('term-ctx-menu');
|
|
4504
5009
|
if (termMenu && termMenu.style.display !== 'none') {
|
|
4505
|
-
|
|
5010
|
+
hideTermCtxMenu();
|
|
4506
5011
|
return;
|
|
4507
5012
|
}
|
|
4508
5013
|
if (document.getElementById('ctx-menu').classList.contains('open')) {
|
|
@@ -4519,6 +5024,7 @@ function setupKeyboardShortcuts() {
|
|
|
4519
5024
|
else if (id === 'newfolder-overlay' || id === 'newfile-overlay' || id === 'rename-overlay') closeOverlay(id);
|
|
4520
5025
|
else if (id === 'image-viewer') closeImageViewer();
|
|
4521
5026
|
else if (id === 'conflict-overlay') closeOverlay(id);
|
|
5027
|
+
else closeOverlay(id);
|
|
4522
5028
|
});
|
|
4523
5029
|
}
|
|
4524
5030
|
});
|
|
@@ -4679,6 +5185,8 @@ function pasteToTerminal() {
|
|
|
4679
5185
|
function fallbackPaste(callback) {
|
|
4680
5186
|
if (!_pasteInput) {
|
|
4681
5187
|
_pasteInput = document.createElement('textarea');
|
|
5188
|
+
_pasteInput.id = 'fallback-paste-area';
|
|
5189
|
+
_pasteInput.name = 'paste-input';
|
|
4682
5190
|
_pasteInput.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;z-index:-1';
|
|
4683
5191
|
document.body.appendChild(_pasteInput);
|
|
4684
5192
|
_pasteInput.addEventListener('paste', () => {
|
|
@@ -4897,10 +5405,23 @@ function setupDragDrop() {
|
|
|
4897
5405
|
// ═══════════════════════════════════════════════════════
|
|
4898
5406
|
let lastFocusedElement = null;
|
|
4899
5407
|
let _focusTrapHandler = null;
|
|
5408
|
+
function openShortcuts() {
|
|
5409
|
+
openOverlay('shortcuts-overlay');
|
|
5410
|
+
document.getElementById('shortcuts-overlay').querySelector('.btn').focus();
|
|
5411
|
+
}
|
|
4900
5412
|
function openOverlay(id) {
|
|
4901
5413
|
lastFocusedElement = document.activeElement;
|
|
4902
5414
|
const overlay = document.getElementById(id);
|
|
4903
5415
|
overlay.classList.add('open');
|
|
5416
|
+
overlay.setAttribute('role', 'dialog');
|
|
5417
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
5418
|
+
const titleEl = overlay.querySelector('h2');
|
|
5419
|
+
if (titleEl) {
|
|
5420
|
+
if (!titleEl.id) titleEl.id = id + '-title';
|
|
5421
|
+
overlay.setAttribute('aria-labelledby', titleEl.id);
|
|
5422
|
+
} else if (overlay.getAttribute('aria-label')) {
|
|
5423
|
+
overlay.removeAttribute('aria-labelledby');
|
|
5424
|
+
}
|
|
4904
5425
|
const modal = overlay.querySelector('.modal, input, button');
|
|
4905
5426
|
if (modal) setTimeout(() => modal.focus(), 50);
|
|
4906
5427
|
installFocusTrap(overlay);
|
|
@@ -4912,7 +5433,7 @@ function closeOverlay(id) {
|
|
|
4912
5433
|
overlay.classList.remove('open');
|
|
4913
5434
|
removeFocusTrap();
|
|
4914
5435
|
if (lastFocusedElement) {
|
|
4915
|
-
setTimeout(() => lastFocusedElement
|
|
5436
|
+
setTimeout(() => lastFocusedElement?.focus(), 50);
|
|
4916
5437
|
lastFocusedElement = null;
|
|
4917
5438
|
}
|
|
4918
5439
|
}
|
|
@@ -4939,24 +5460,89 @@ function removeFocusTrap() {
|
|
|
4939
5460
|
}
|
|
4940
5461
|
}
|
|
4941
5462
|
|
|
5463
|
+
let _confirmState = null;
|
|
5464
|
+
function confirmDialog({ title = 'Confirm', message = '', okText = 'OK', cancelText = 'Cancel', danger = false } = {}) {
|
|
5465
|
+
return new Promise(resolve => {
|
|
5466
|
+
const ov = document.getElementById('confirm-overlay');
|
|
5467
|
+
if (!ov || !ov.parentNode) { resolve(false); return; }
|
|
5468
|
+
const okBtn = document.getElementById('confirm-ok-btn');
|
|
5469
|
+
const cancelBtn = document.getElementById('confirm-cancel-btn');
|
|
5470
|
+
const titleEl = document.getElementById('confirm-title');
|
|
5471
|
+
const msgEl = document.getElementById('confirm-msg');
|
|
5472
|
+
titleEl.textContent = title;
|
|
5473
|
+
msgEl.textContent = message;
|
|
5474
|
+
ov.setAttribute('role', 'dialog');
|
|
5475
|
+
ov.setAttribute('aria-modal', 'true');
|
|
5476
|
+
ov.setAttribute('aria-labelledby', 'confirm-title');
|
|
5477
|
+
okBtn.textContent = okText;
|
|
5478
|
+
cancelBtn.textContent = cancelText;
|
|
5479
|
+
okBtn.classList.toggle('btn-danger', !!danger);
|
|
5480
|
+
let settled = false;
|
|
5481
|
+
const cleanup = () => {
|
|
5482
|
+
if (settled) return;
|
|
5483
|
+
settled = true;
|
|
5484
|
+
_confirmState = null;
|
|
5485
|
+
ov.classList.remove('open');
|
|
5486
|
+
removeFocusTrap();
|
|
5487
|
+
if (lastFocusedElement) { setTimeout(() => lastFocusedElement?.focus(), 50); lastFocusedElement = null; }
|
|
5488
|
+
};
|
|
5489
|
+
const finish = val => { const cb = _confirmState && _confirmState.resolve; cleanup(); if (cb) cb(val); };
|
|
5490
|
+
const onKey = e => {
|
|
5491
|
+
if (e.key === 'Escape') { e.stopPropagation(); finish(false); }
|
|
5492
|
+
else if (e.key === 'Enter' && !(e.target instanceof HTMLButtonElement)) { e.preventDefault(); finish(true); }
|
|
5493
|
+
};
|
|
5494
|
+
_confirmState = { resolve, cleanup };
|
|
5495
|
+
lastFocusedElement = document.activeElement;
|
|
5496
|
+
okBtn.onclick = () => finish(true);
|
|
5497
|
+
cancelBtn.onclick = () => finish(false);
|
|
5498
|
+
ov.addEventListener('keydown', onKey);
|
|
5499
|
+
ov.classList.add('open');
|
|
5500
|
+
installFocusTrap(ov);
|
|
5501
|
+
setTimeout(() => okBtn.focus(), 50);
|
|
5502
|
+
});
|
|
5503
|
+
}
|
|
5504
|
+
|
|
4942
5505
|
document.querySelectorAll('.overlay').forEach(o => {
|
|
4943
5506
|
o.addEventListener('click', e => {
|
|
4944
5507
|
if (e.target !== o) return;
|
|
4945
5508
|
if (o.id === 'sys-overlay') { closeSystemStats(); return; }
|
|
4946
5509
|
if (o.id === 'finder-overlay') { clearTimeout(finderTimer); }
|
|
5510
|
+
if (o.id === 'confirm-overlay') return; // dismiss handled by confirmDialog only
|
|
4947
5511
|
closeOverlay(o.id);
|
|
4948
5512
|
});
|
|
4949
5513
|
});
|
|
4950
5514
|
|
|
5515
|
+
function setBtnBusy(btn, busy) {
|
|
5516
|
+
if (!btn) return;
|
|
5517
|
+
if (busy) { btn.dataset.busy = 'true'; btn.classList.add('loading'); btn.disabled = true; }
|
|
5518
|
+
else { btn.dataset.busy = 'false'; btn.classList.remove('loading'); btn.disabled = false; }
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
function showFieldError(id, msg) {
|
|
5522
|
+
const el = document.getElementById(id);
|
|
5523
|
+
if (el) { el.textContent = msg || ''; el.classList.toggle('show', !!msg); }
|
|
5524
|
+
}
|
|
5525
|
+
function clearFieldError(id) { showFieldError(id, ''); }
|
|
5526
|
+
|
|
5527
|
+
const TOAST_ICONS = {
|
|
5528
|
+
success: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>',
|
|
5529
|
+
error: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
|
5530
|
+
warning: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
5531
|
+
info: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
|
|
5532
|
+
};
|
|
5533
|
+
|
|
4951
5534
|
function toast(msg, type = 'info') {
|
|
4952
5535
|
const el = document.createElement('div');
|
|
4953
5536
|
el.className = `toast ${type}`;
|
|
4954
|
-
el.
|
|
5537
|
+
el.innerHTML = (TOAST_ICONS[type] || '') + '<span></span>';
|
|
5538
|
+
el.querySelector('span').textContent = msg;
|
|
4955
5539
|
const mk = document.getElementById('mobile-keys');
|
|
4956
5540
|
if (mk && mk.style.display !== 'none' && window.getComputedStyle(mk).display !== 'none') {
|
|
4957
5541
|
el.style.marginBottom = 'var(--mobilekey-h)';
|
|
4958
5542
|
}
|
|
4959
|
-
document.getElementById('toast-container')
|
|
5543
|
+
const container = document.getElementById('toast-container');
|
|
5544
|
+
container.appendChild(el);
|
|
5545
|
+
while (container.children.length > 4) container.firstChild.remove();
|
|
4960
5546
|
setTimeout(() => el.remove(), 3000);
|
|
4961
5547
|
return el;
|
|
4962
5548
|
}
|
|
@@ -4966,6 +5552,7 @@ function updateConnStatus(connected) {
|
|
|
4966
5552
|
if (dot) {
|
|
4967
5553
|
dot.style.background = connected ? 'var(--green)' : 'var(--red)';
|
|
4968
5554
|
dot.title = connected ? 'Connected' : 'Disconnected';
|
|
5555
|
+
dot.setAttribute('aria-label', connected ? 'Connected' : 'Disconnected');
|
|
4969
5556
|
}
|
|
4970
5557
|
}
|
|
4971
5558
|
|
|
@@ -5007,11 +5594,14 @@ function setupSwipeGestures() {
|
|
|
5007
5594
|
let pullStartY = 0;
|
|
5008
5595
|
let pullOffset = 0;
|
|
5009
5596
|
const fileListWrap = document.getElementById('file-list-wrap');
|
|
5010
|
-
|
|
5597
|
+
const ptr = document.getElementById('ptr-indicator');
|
|
5598
|
+
if (fileListWrap && ptr) {
|
|
5011
5599
|
fileListWrap.addEventListener('touchstart', e => {
|
|
5012
5600
|
if (fileListWrap.scrollTop <= 0) {
|
|
5013
5601
|
pullStartY = e.touches[0].clientY;
|
|
5014
5602
|
pullOffset = 0;
|
|
5603
|
+
ptr.style.opacity = '0';
|
|
5604
|
+
ptr.classList.remove('release');
|
|
5015
5605
|
}
|
|
5016
5606
|
}, { passive: true });
|
|
5017
5607
|
fileListWrap.addEventListener('touchmove', e => {
|
|
@@ -5019,16 +5609,21 @@ function setupSwipeGestures() {
|
|
|
5019
5609
|
const dy = e.touches[0].clientY - pullStartY;
|
|
5020
5610
|
if (dy > 0 && fileListWrap.scrollTop <= 0) {
|
|
5021
5611
|
pullOffset = dy;
|
|
5022
|
-
|
|
5612
|
+
const t = Math.min(dy * 0.4, 60);
|
|
5613
|
+
fileListWrap.style.transform = `translateY(${t}px)`;
|
|
5023
5614
|
fileListWrap.style.transition = 'none';
|
|
5615
|
+
ptr.style.opacity = Math.min(1, dy / 40).toFixed(2);
|
|
5616
|
+
ptr.classList.toggle('release', pullOffset > 60);
|
|
5024
5617
|
}
|
|
5025
5618
|
}, { passive: true });
|
|
5026
5619
|
fileListWrap.addEventListener('touchend', () => {
|
|
5027
5620
|
if (pullOffset > 60) {
|
|
5621
|
+
ptr.classList.remove('release');
|
|
5028
5622
|
refreshFiles();
|
|
5029
5623
|
}
|
|
5030
5624
|
fileListWrap.style.transition = 'transform 0.3s ease';
|
|
5031
5625
|
fileListWrap.style.transform = '';
|
|
5626
|
+
ptr.style.opacity = '0';
|
|
5032
5627
|
pullStartY = 0;
|
|
5033
5628
|
pullOffset = 0;
|
|
5034
5629
|
setTimeout(() => { fileListWrap.style.transition = ''; }, 300);
|
|
@@ -5092,9 +5687,9 @@ function showPinScreen() {
|
|
|
5092
5687
|
// BOOKMARKS
|
|
5093
5688
|
// ═══════════════════════════════════════════════════════
|
|
5094
5689
|
function getBookmarks() {
|
|
5095
|
-
try { return JSON.parse(
|
|
5690
|
+
try { return JSON.parse(safeStorage.getItem('wt-bookmarks')) || []; } catch(e) { console.warn('Failed to parse bookmarks:', e); return []; }
|
|
5096
5691
|
}
|
|
5097
|
-
function saveBookmarks(bm) {
|
|
5692
|
+
function saveBookmarks(bm) { safeStorage.setItem('wt-bookmarks', JSON.stringify(bm)); renderBookmarks(); }
|
|
5098
5693
|
|
|
5099
5694
|
function renderBookmarks() {
|
|
5100
5695
|
const list = document.getElementById('bookmarks-list');
|
|
@@ -5120,7 +5715,9 @@ function renderBookmarks() {
|
|
|
5120
5715
|
div.appendChild(bmPath);
|
|
5121
5716
|
const rmBtn = document.createElement('span');
|
|
5122
5717
|
rmBtn.className = 'bm-remove';
|
|
5123
|
-
rmBtn.
|
|
5718
|
+
rmBtn.setAttribute('role', 'button');
|
|
5719
|
+
rmBtn.setAttribute('aria-label', 'Remove bookmark');
|
|
5720
|
+
rmBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
5124
5721
|
rmBtn.title = 'Remove';
|
|
5125
5722
|
rmBtn.addEventListener('click', e => {
|
|
5126
5723
|
e.stopPropagation();
|
|
@@ -5143,11 +5740,11 @@ function bookmarkCurrentDir() {
|
|
|
5143
5740
|
toast('Bookmarked', 'success');
|
|
5144
5741
|
}
|
|
5145
5742
|
|
|
5146
|
-
let bookmarksOpen = (() => { try { const v =
|
|
5743
|
+
let bookmarksOpen = (() => { try { const v = safeStorage.getItem('wt-bm-open'); return v !== null ? v === 'true' : true; } catch(e) { console.warn('Failed to read bookmarksOpen:', e); return true; } })();
|
|
5147
5744
|
function toggleBookmarks() {
|
|
5148
5745
|
bookmarksOpen = !bookmarksOpen;
|
|
5149
5746
|
document.getElementById('bookmarks-list').classList.toggle('open', bookmarksOpen);
|
|
5150
|
-
try {
|
|
5747
|
+
try { safeStorage.setItem('wt-bm-open', bookmarksOpen); } catch(e) { console.warn(e); }
|
|
5151
5748
|
}
|
|
5152
5749
|
|
|
5153
5750
|
// ═══════════════════════════════════════════════════════
|
|
@@ -5220,10 +5817,10 @@ function finderKeydown(e) {
|
|
|
5220
5817
|
const DEFAULT_CMDS = window.DEFAULT_CMDS || [];
|
|
5221
5818
|
|
|
5222
5819
|
function getCmdLib() {
|
|
5223
|
-
try { return JSON.parse(
|
|
5820
|
+
try { return JSON.parse(safeStorage.getItem('wt-cmdlib')) || []; } catch(e) { return []; }
|
|
5224
5821
|
}
|
|
5225
5822
|
function saveCmdLib(cmds) {
|
|
5226
|
-
|
|
5823
|
+
safeStorage.setItem('wt-cmdlib', JSON.stringify(cmds));
|
|
5227
5824
|
}
|
|
5228
5825
|
|
|
5229
5826
|
function toggleCmdLib() {
|
|
@@ -5293,7 +5890,9 @@ function renderCmdLib(filter) {
|
|
|
5293
5890
|
cmd.textContent = c.cmd;
|
|
5294
5891
|
const runBtn = document.createElement('button');
|
|
5295
5892
|
runBtn.className = 'cmd-lib-run';
|
|
5296
|
-
|
|
5893
|
+
runBtn.title = 'Run in terminal';
|
|
5894
|
+
runBtn.setAttribute('aria-label', 'Run in terminal');
|
|
5895
|
+
runBtn.setAttribute('aria-label', 'Run in terminal');
|
|
5297
5896
|
runBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6,3 20,12 6,21"/></svg>';
|
|
5298
5897
|
runBtn.addEventListener('click', e => {
|
|
5299
5898
|
e.stopPropagation();
|
|
@@ -5307,7 +5906,8 @@ function renderCmdLib(filter) {
|
|
|
5307
5906
|
const delBtn = document.createElement('button');
|
|
5308
5907
|
delBtn.className = 'cmd-lib-del';
|
|
5309
5908
|
delBtn.title = 'Delete';
|
|
5310
|
-
delBtn.
|
|
5909
|
+
delBtn.setAttribute('aria-label', 'Delete command');
|
|
5910
|
+
delBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
5311
5911
|
delBtn.addEventListener('click', e => {
|
|
5312
5912
|
e.stopPropagation();
|
|
5313
5913
|
deleteCustomCmd(c.name, c.cmd);
|
|
@@ -5319,7 +5919,7 @@ function renderCmdLib(filter) {
|
|
|
5319
5919
|
const tab = getActiveTab();
|
|
5320
5920
|
if (!tab?.term) return;
|
|
5321
5921
|
tab.term.focus();
|
|
5322
|
-
|
|
5922
|
+
tab.term.paste(c.cmd);
|
|
5323
5923
|
toggleCmdLib();
|
|
5324
5924
|
});
|
|
5325
5925
|
section.appendChild(item);
|
|
@@ -5383,10 +5983,10 @@ let cmdHistMax = 50;
|
|
|
5383
5983
|
let _cmdHistCache = [];
|
|
5384
5984
|
|
|
5385
5985
|
function histHeaders() {
|
|
5386
|
-
return { 'Content-Type': 'application/json', 'x-pin-token':
|
|
5986
|
+
return { 'Content-Type': 'application/json', 'x-pin-token': authToken || '' };
|
|
5387
5987
|
}
|
|
5388
5988
|
function histHeadersDelete() {
|
|
5389
|
-
return { 'x-pin-token':
|
|
5989
|
+
return { 'x-pin-token': authToken || '' };
|
|
5390
5990
|
}
|
|
5391
5991
|
async function getCmdHist() {
|
|
5392
5992
|
try {
|
|
@@ -5406,10 +6006,15 @@ function loadHistMax() {
|
|
|
5406
6006
|
}
|
|
5407
6007
|
async function addToCmdHist(cmd) {
|
|
5408
6008
|
if (!cmd || !cmd.trim()) return;
|
|
5409
|
-
// Strip ANSI
|
|
5410
|
-
const clean = cmd
|
|
5411
|
-
.replace(/\x1b
|
|
5412
|
-
.replace(
|
|
6009
|
+
// Strip all ANSI/VT escape sequences and control characters
|
|
6010
|
+
const clean = cmd
|
|
6011
|
+
.replace(/\x1b[^a-zA-Z0-9]*[a-zA-Z0-9~]/g, '') // CSI: ESC [ ... final
|
|
6012
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC: ESC ] ... BEL/ST
|
|
6013
|
+
.replace(/\x1b[OPP][^\x40-\x7e]*[\x40-\x7e]/g, '') // SS3/DCS: ESC O/P ... final
|
|
6014
|
+
.replace(/\x1b./g, '') // Any other ESC sequence
|
|
6015
|
+
.replace(/[\x00-\x1f\x7f]/g, '') // All remaining control chars
|
|
6016
|
+
.replace(/^[>;\d\s]+(?=[a-zA-Z/\\~\-.])/, '') // VT params before a real command char
|
|
6017
|
+
.replace(/^[>;\d\s]+$/, '') // Pure VT params, no command at all
|
|
5413
6018
|
.trim();
|
|
5414
6019
|
if (!clean) return;
|
|
5415
6020
|
try {
|
|
@@ -5510,7 +6115,8 @@ async function renderCmdHist() {
|
|
|
5510
6115
|
const delBtn = document.createElement('button');
|
|
5511
6116
|
delBtn.className = 'cmd-hist-del';
|
|
5512
6117
|
delBtn.title = 'Remove';
|
|
5513
|
-
delBtn.
|
|
6118
|
+
delBtn.setAttribute('aria-label', 'Remove from history');
|
|
6119
|
+
delBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
5514
6120
|
delBtn.addEventListener('click', e => { e.stopPropagation(); removeCmdHistItem(realIdx); });
|
|
5515
6121
|
item.appendChild(delBtn);
|
|
5516
6122
|
|
|
@@ -5518,7 +6124,7 @@ async function renderCmdHist() {
|
|
|
5518
6124
|
const tab = getActiveTab();
|
|
5519
6125
|
if (!tab?.term) return;
|
|
5520
6126
|
tab.term.focus();
|
|
5521
|
-
|
|
6127
|
+
tab.term.paste(h.cmd);
|
|
5522
6128
|
toggleCmdLib();
|
|
5523
6129
|
});
|
|
5524
6130
|
list.appendChild(item);
|
|
@@ -5627,7 +6233,7 @@ async function refreshSystemStats(showLoading) {
|
|
|
5627
6233
|
// ═══════════════════════════════════════════════════════
|
|
5628
6234
|
let deferredPrompt = null;
|
|
5629
6235
|
let installBannerDismissed = false;
|
|
5630
|
-
try { installBannerDismissed =
|
|
6236
|
+
try { installBannerDismissed = safeStorage.getItem('wt-install-dismissed') === 'true'; } catch(e) { console.warn(e); }
|
|
5631
6237
|
|
|
5632
6238
|
window.addEventListener('beforeinstallprompt', e => {
|
|
5633
6239
|
e.preventDefault();
|
|
@@ -5651,6 +6257,10 @@ function showInstallBanner(isIOS) {
|
|
|
5651
6257
|
el.querySelector('.btn-primary').style.display = 'none';
|
|
5652
6258
|
el.querySelector('.btn-ghost').textContent = 'Dismiss';
|
|
5653
6259
|
}
|
|
6260
|
+
const mk = document.getElementById('mobile-keys');
|
|
6261
|
+
if (mk && window.getComputedStyle(mk).display !== 'none') {
|
|
6262
|
+
el.style.bottom = 'var(--mobilekey-h)';
|
|
6263
|
+
}
|
|
5654
6264
|
el.style.display = 'flex';
|
|
5655
6265
|
document.getElementById('toast-container').classList.add('banner-visible');
|
|
5656
6266
|
}
|
|
@@ -5666,7 +6276,7 @@ function installPWA() {
|
|
|
5666
6276
|
function dismissInstall() {
|
|
5667
6277
|
document.getElementById('install-banner').style.display = 'none';
|
|
5668
6278
|
document.getElementById('toast-container').classList.remove('banner-visible');
|
|
5669
|
-
try {
|
|
6279
|
+
try { safeStorage.setItem('wt-install-dismissed', 'true'); } catch(e) { console.warn(e); }
|
|
5670
6280
|
installBannerDismissed = true;
|
|
5671
6281
|
}
|
|
5672
6282
|
|