webtun 1.5.1 → 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 +24 -0
- package/package.json +1 -1
- package/public/commands.js +0 -1
- package/public/index.html +765 -181
- package/server.js +1 -1
package/public/index.html
CHANGED
|
@@ -11,37 +11,41 @@
|
|
|
11
11
|
<link rel="icon" href="/favicon.png" type="image/png">
|
|
12
12
|
<link rel="manifest" href="/manifest.json">
|
|
13
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">
|
|
14
17
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
16
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
17
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
|
|
18
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
19
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.7.0/lib/addon-unicode11.min.js"></script>
|
|
20
|
-
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.
|
|
21
|
-
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
|
22
|
-
<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>
|
|
23
26
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.css">
|
|
24
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.js"></script>
|
|
25
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/edit/matchbrackets.min.js"></script>
|
|
26
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/javascript/javascript.min.js"></script>
|
|
27
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/python/python.min.js"></script>
|
|
28
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/htmlmixed/htmlmixed.min.js"></script>
|
|
29
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/xml/xml.min.js"></script>
|
|
30
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/css/css.min.js"></script>
|
|
31
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/clike/clike.min.js"></script>
|
|
32
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/shell/shell.min.js"></script>
|
|
33
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/markdown/markdown.min.js"></script>
|
|
34
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/yaml/yaml.min.js"></script>
|
|
35
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/sql/sql.min.js"></script>
|
|
36
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/go/go.min.js"></script>
|
|
37
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/mode/simple.min.js"></script>
|
|
38
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/rust/rust.min.js"></script>
|
|
39
|
-
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/properties/properties.min.js"></script>
|
|
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>
|
|
40
43
|
<style>
|
|
41
44
|
/* ═══════════════════════════════════════════════════════
|
|
42
45
|
CSS VARIABLES / THEMES
|
|
43
46
|
═══════════════════════════════════════════════════════ */
|
|
44
47
|
:root {
|
|
48
|
+
color-scheme: dark;
|
|
45
49
|
--bg: #1a1b26;
|
|
46
50
|
--bg2: #16161e;
|
|
47
51
|
--bg3: #24283b;
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
--border: #3b3f52;
|
|
50
54
|
--fg: #c0caf5;
|
|
51
55
|
--fg2: #787c99;
|
|
56
|
+
--fg3: #565f89;
|
|
52
57
|
--accent: #7aa2f7;
|
|
53
58
|
--accent2: #bb9af7;
|
|
54
59
|
--green: #9ece6a;
|
|
@@ -74,43 +79,65 @@
|
|
|
74
79
|
--btn-sm: 26px;
|
|
75
80
|
--btn-md: 32px;
|
|
76
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;
|
|
77
90
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
78
91
|
--transition-fast: 0.15s;
|
|
79
92
|
--transition-normal: 0.2s;
|
|
80
93
|
--transition-slow: 0.3s;
|
|
81
|
-
--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);
|
|
82
104
|
}
|
|
83
105
|
[data-theme="light"] {
|
|
106
|
+
color-scheme: light;
|
|
84
107
|
--bg: #f9f9fb; --bg2: #eeeef2; --bg3: #e2e2e8; --bg4: #d5d5de;
|
|
85
|
-
--border: #c8c8d4; --fg: #
|
|
86
|
-
--accent: #4060d0; --accent2: #8050c0; --green: #
|
|
87
|
-
--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;
|
|
88
111
|
--shadow-color: rgba(0,0,0,0.12);
|
|
89
112
|
}
|
|
90
113
|
[data-theme="solarized"] {
|
|
114
|
+
color-scheme: dark;
|
|
91
115
|
--bg: #002b36; --bg2: #073642; --bg3: #083f4d; --bg4: #0a4a5a;
|
|
92
|
-
--border: #1a5a6a; --fg: #93a1a1; --fg2: #586e75;
|
|
116
|
+
--border: #1a5a6a; --fg: #93a1a1; --fg2: #586e75; --fg3: #486168;
|
|
93
117
|
--accent: #268bd2; --accent2: #6c71c4; --green: #859900;
|
|
94
118
|
--red: #dc322f; --yellow: #b58900; --cyan: #2aa198;
|
|
95
119
|
--shadow-color: rgba(0,0,0,0.6);
|
|
96
120
|
}
|
|
97
121
|
[data-theme="gruvbox"] {
|
|
122
|
+
color-scheme: dark;
|
|
98
123
|
--bg: #282828; --bg2: #1d2021; --bg3: #32302f; --bg4: #3c3836;
|
|
99
|
-
--border: #504945; --fg: #ebdbb2; --fg2: #928374;
|
|
124
|
+
--border: #504945; --fg: #ebdbb2; --fg2: #928374; --fg3: #7c6f64;
|
|
100
125
|
--accent: #83a598; --accent2: #d3869b; --green: #b8bb26;
|
|
101
126
|
--red: #fb4934; --yellow: #fabd2f; --cyan: #8ec07c;
|
|
102
127
|
--shadow-color: rgba(0,0,0,0.6);
|
|
103
128
|
}
|
|
104
129
|
[data-theme="dracula"] {
|
|
130
|
+
color-scheme: dark;
|
|
105
131
|
--bg: #282a36; --bg2: #1e1f29; --bg3: #2f3240; --bg4: #373a50;
|
|
106
|
-
--border: #44475a; --fg: #f8f8f2; --fg2: #6272a4;
|
|
132
|
+
--border: #44475a; --fg: #f8f8f2; --fg2: #6272a4; --fg3: #4f5b84;
|
|
107
133
|
--accent: #bd93f9; --accent2: #ff79c6; --green: #50fa7b;
|
|
108
134
|
--red: #ff5555; --yellow: #f1fa8c; --cyan: #8be9fd;
|
|
109
135
|
--shadow-color: rgba(0,0,0,0.6);
|
|
110
136
|
}
|
|
111
137
|
[data-theme="monokai"] {
|
|
138
|
+
color-scheme: dark;
|
|
112
139
|
--bg: #272822; --bg2: #1e1f1c; --bg3: #31322b; --bg4: #3a3c32;
|
|
113
|
-
--border: #49483e; --fg: #f8f8f2; --fg2: #75715e;
|
|
140
|
+
--border: #49483e; --fg: #f8f8f2; --fg2: #75715e; --fg3: #5d5a4e;
|
|
114
141
|
--accent: #a6e22e; --accent2: #ae81ff; --green: #a6e22e;
|
|
115
142
|
--red: #f92672; --yellow: #e6db74; --cyan: #66d9e8;
|
|
116
143
|
--shadow-color: rgba(0,0,0,0.6);
|
|
@@ -120,10 +147,10 @@
|
|
|
120
147
|
RESET & BASE
|
|
121
148
|
═══════════════════════════════════════════════════════ */
|
|
122
149
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
123
|
-
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; }
|
|
124
151
|
|
|
125
152
|
button { background: none; border: none; cursor: pointer; color: inherit; font-family: inherit; }
|
|
126
|
-
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; }
|
|
127
154
|
@media (pointer: coarse) { input, select, textarea { font-size: 16px; } }
|
|
128
155
|
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
|
129
156
|
a { color: var(--accent); text-decoration: none; }
|
|
@@ -141,17 +168,38 @@ a { color: var(--accent); text-decoration: none; }
|
|
|
141
168
|
#new-tab-btn { width: 36px; height: 36px; }
|
|
142
169
|
.icon-btn { width: 40px; height: 40px; }
|
|
143
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; }
|
|
144
173
|
}
|
|
145
174
|
svg { display: block; }
|
|
146
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
|
+
|
|
147
195
|
/* ═══════════════════════════════════════════════════════
|
|
148
196
|
LAYOUT
|
|
149
197
|
═══════════════════════════════════════════════════════ */
|
|
150
198
|
#app { display: flex; flex-direction: column; height: 100dvh; }
|
|
151
|
-
#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; }
|
|
152
200
|
|
|
153
201
|
/* Upload progress bar in header */
|
|
154
|
-
#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; }
|
|
155
203
|
#upload-progress.active { height: 3px; }
|
|
156
204
|
#upload-progress-bar { height: 100%; width: 0%; background: var(--accent); transition: width var(--transition-slow) ease; border-radius: 0 2px 2px 0; }
|
|
157
205
|
#upload-progress.success #upload-progress-bar { background: var(--green); }
|
|
@@ -163,13 +211,26 @@ svg { display: block; }
|
|
|
163
211
|
#sidebar.dragging { transition: none; }
|
|
164
212
|
#sidebar.hidden { width: 0 !important; }
|
|
165
213
|
#content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; min-height: 0; position: relative; }
|
|
166
|
-
#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; }
|
|
167
217
|
#tab-bar.drag-over, #new-tab-btn.drag-over { background: var(--accent, #00c853); opacity: 0.15; }
|
|
168
|
-
#tab-bar::-webkit-scrollbar { height: 3px; }
|
|
169
218
|
#terminals { flex: 1; position: relative; overflow: hidden; }
|
|
170
219
|
.term-wrapper { position: absolute; inset: 0; padding: 4px; display: none; touch-action: pan-y; -webkit-overflow-scrolling: touch; overscroll-behavior: none; }
|
|
171
220
|
.term-wrapper.active { display: flex; }
|
|
172
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; }
|
|
173
234
|
.xterm-viewport { border-radius: 0; }
|
|
174
235
|
.term-bell-flash { animation: bellFlash 0.15s ease-out; }
|
|
175
236
|
@keyframes bellFlash { 0% { filter: brightness(1.8); } 100% { filter: brightness(1); } }
|
|
@@ -255,7 +316,7 @@ svg { display: block; }
|
|
|
255
316
|
cursor: row-resize;
|
|
256
317
|
flex-shrink: 0;
|
|
257
318
|
position: relative;
|
|
258
|
-
z-index:
|
|
319
|
+
z-index: var(--z-header);
|
|
259
320
|
background: transparent;
|
|
260
321
|
transition: background var(--transition-fast);
|
|
261
322
|
}
|
|
@@ -275,7 +336,7 @@ svg { display: block; }
|
|
|
275
336
|
display: flex; flex-direction: column;
|
|
276
337
|
transform: translateX(100%);
|
|
277
338
|
transition: var(--transition-normal);
|
|
278
|
-
z-index:
|
|
339
|
+
z-index: var(--z-drawer);
|
|
279
340
|
}
|
|
280
341
|
#settings-panel.open {
|
|
281
342
|
transform: translateX(0);
|
|
@@ -308,17 +369,27 @@ svg { display: block; }
|
|
|
308
369
|
.keep-awake input { display: none; }
|
|
309
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); }
|
|
310
371
|
.keep-awake:hover .keep-awake-box { border-color: var(--fg2); }
|
|
311
|
-
.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; }
|
|
312
373
|
.keep-awake input:checked + .keep-awake-box { border-color: var(--accent); background: var(--accent); }
|
|
313
|
-
.keep-awake input:checked + .keep-awake-box .keep-awake-dot { background: var(--bg); left: calc(100% - 11px);
|
|
314
|
-
|
|
315
|
-
@keyframes keepawake-bounce {
|
|
316
|
-
0%, 100% { left: 3px; }
|
|
317
|
-
50% { left: calc(100% - 11px); }
|
|
318
|
-
}
|
|
374
|
+
.keep-awake input:checked + .keep-awake-box .keep-awake-dot { background: var(--bg); left: calc(100% - 11px); }
|
|
319
375
|
|
|
320
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); }
|
|
321
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); } }
|
|
322
393
|
|
|
323
394
|
/* ═══════════════════════════════════════════════════════
|
|
324
395
|
TABS
|
|
@@ -341,11 +412,24 @@ svg { display: block; }
|
|
|
341
412
|
═══════════════════════════════════════════════════════ */
|
|
342
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; }
|
|
343
414
|
#sidebar-header input { flex: 1 1 60px; min-width: 40px; height: 26px; font-size: 12px; }
|
|
344
|
-
#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%; } }
|
|
345
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; }
|
|
346
427
|
.file-list-empty svg { opacity: 0.4; }
|
|
347
|
-
|
|
348
|
-
|
|
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; }
|
|
349
433
|
#file-breadcrumb a:hover { background: var(--bg3); }
|
|
350
434
|
@media (pointer: coarse) {
|
|
351
435
|
#file-breadcrumb { padding: 10px 12px; font-size: 13px; }
|
|
@@ -354,6 +438,9 @@ svg { display: block; }
|
|
|
354
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; }
|
|
355
439
|
.file-item:hover { background: var(--bg3); }
|
|
356
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; }
|
|
357
444
|
.file-icon { font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; }
|
|
358
445
|
.file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; user-select: text; -webkit-user-select: text; }
|
|
359
446
|
.file-size { font-size: 10px; color: var(--fg2); flex-shrink: 0; user-select: text; -webkit-user-select: text; }
|
|
@@ -378,7 +465,7 @@ svg { display: block; }
|
|
|
378
465
|
/* Sidebar resize handle */
|
|
379
466
|
.sidebar-resize-handle {
|
|
380
467
|
position: absolute; right: 0; top: 0; bottom: 0;
|
|
381
|
-
width: 5px; cursor: col-resize; z-index:
|
|
468
|
+
width: 5px; cursor: col-resize; z-index: var(--z-header);
|
|
382
469
|
background: transparent; transition: background var(--transition-fast); flex-shrink: 0;
|
|
383
470
|
}
|
|
384
471
|
.sidebar-resize-handle:hover,
|
|
@@ -432,9 +519,10 @@ svg { display: block; }
|
|
|
432
519
|
/* ═══════════════════════════════════════════════════════
|
|
433
520
|
SEARCH BAR
|
|
434
521
|
═══════════════════════════════════════════════════════ */
|
|
435
|
-
#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); }
|
|
436
523
|
#search-bar.open { display: flex; }
|
|
437
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; } }
|
|
438
526
|
.search-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 4px; color: var(--fg2); }
|
|
439
527
|
.search-btn:hover { background: var(--bg4); color: var(--fg); }
|
|
440
528
|
.search-btn#search-case-btn { font-size: 11px; font-weight: bold; width: 22px; height: 22px; line-height: 22px; }
|
|
@@ -444,7 +532,7 @@ svg { display: block; }
|
|
|
444
532
|
/* ═══════════════════════════════════════════════════════
|
|
445
533
|
OVERLAY
|
|
446
534
|
═══════════════════════════════════════════════════════ */
|
|
447
|
-
.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; }
|
|
448
536
|
@media (prefers-reduced-transparency: reduce) { .overlay { backdrop-filter: none; } }
|
|
449
537
|
.overlay.open { display: flex; }
|
|
450
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); }
|
|
@@ -456,6 +544,11 @@ svg { display: block; }
|
|
|
456
544
|
.modal-row label { display: block; font-size: 11px; color: var(--fg2); margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
457
545
|
.modal-row input, .modal-row select { width: 100%; }
|
|
458
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; }
|
|
459
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; }
|
|
460
553
|
#conflict-modal.modal-sm { width: 380px; }
|
|
461
554
|
#conflict-heading { font-size: 15px; margin-bottom: 8px; color: var(--fg); }
|
|
@@ -465,11 +558,22 @@ svg { display: block; }
|
|
|
465
558
|
#conflict-actions { flex-wrap: wrap; gap: 6px; }
|
|
466
559
|
#conflict-replace, #conflict-merge, #conflict-keep-both, #conflict-skip, #conflict-cancel { font-size: 12px; height: var(--btn-md); }
|
|
467
560
|
.btn-primary { background: var(--accent); color: var(--bg); }
|
|
468
|
-
.btn-primary:hover { filter: brightness(1.
|
|
561
|
+
.btn-primary:hover { filter: brightness(1.06); }
|
|
469
562
|
.btn-ghost { background: var(--bg3); color: var(--fg); border-color: var(--border); }
|
|
470
563
|
.btn-ghost:hover { background: var(--bg4); }
|
|
471
564
|
.btn-danger { background: var(--red); color: white; }
|
|
472
|
-
.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; }
|
|
473
577
|
|
|
474
578
|
/* ═══════════════════════════════════════════════════════
|
|
475
579
|
PIN SCREEN
|
|
@@ -488,7 +592,10 @@ svg { display: block; }
|
|
|
488
592
|
EDITOR
|
|
489
593
|
═══════════════════════════════════════════════════════ */
|
|
490
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); }
|
|
491
|
-
#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; }
|
|
492
599
|
#editor-status { font-size: 11px; color: var(--fg2); }
|
|
493
600
|
#editor-area { flex: 1; overflow: hidden; position: relative; }
|
|
494
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; }
|
|
@@ -553,7 +660,7 @@ svg { display: block; }
|
|
|
553
660
|
/* ═══════════════════════════════════════════════════════
|
|
554
661
|
CONTEXT MENU
|
|
555
662
|
═══════════════════════════════════════════════════════ */
|
|
556
|
-
#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; }
|
|
557
664
|
@media (max-width: 360px) { #ctx-menu { min-width: 140px; max-width: calc(100vw - 16px); font-size: 11px; } }
|
|
558
665
|
#ctx-menu.open { display: block; }
|
|
559
666
|
.ctx-item { padding: 8px 14px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
|
@@ -569,11 +676,16 @@ svg { display: block; }
|
|
|
569
676
|
/* ═══════════════════════════════════════════════════════
|
|
570
677
|
TOAST
|
|
571
678
|
═══════════════════════════════════════════════════════ */
|
|
572
|
-
#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); }
|
|
573
680
|
#toast-container.keys-hidden { bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
|
|
574
681
|
#toast-container.banner-visible { bottom: calc(var(--mobilekey-h) + 60px + env(safe-area-inset-bottom, 0px)); }
|
|
575
682
|
#toast-container.keys-hidden.banner-visible { bottom: 60px; }
|
|
576
|
-
.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); }
|
|
577
689
|
.toast.success { border-left: 3px solid var(--green); }
|
|
578
690
|
.toast.error { border-left: 3px solid var(--red); }
|
|
579
691
|
.toast.warning { border-left: 3px solid var(--yellow); }
|
|
@@ -583,7 +695,7 @@ svg { display: block; }
|
|
|
583
695
|
/* ═══════════════════════════════════════════════════════
|
|
584
696
|
DRAG & DROP
|
|
585
697
|
═══════════════════════════════════════════════════════ */
|
|
586
|
-
#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); }
|
|
587
699
|
#drop-overlay.active { display: flex; }
|
|
588
700
|
@media (hover: none) and (pointer: coarse) { #drop-overlay { display: none !important; } }
|
|
589
701
|
|
|
@@ -649,7 +761,7 @@ svg { display: block; }
|
|
|
649
761
|
COMMAND LIBRARY BUBBLE
|
|
650
762
|
═══════════════════════════════════════════════════════ */
|
|
651
763
|
#cmd-lib-panel {
|
|
652
|
-
position: absolute; top: 42px; right: 12px; z-index:
|
|
764
|
+
position: absolute; top: 42px; right: 12px; z-index: var(--z-popover);
|
|
653
765
|
width: 380px; max-height: 480px;
|
|
654
766
|
background: var(--bg2); border: 1px solid var(--border);
|
|
655
767
|
border-radius: var(--radius-lg);
|
|
@@ -821,12 +933,14 @@ svg { display: block; }
|
|
|
821
933
|
@media (max-width: 768px) {
|
|
822
934
|
#mobile-keys { display: flex; }
|
|
823
935
|
.sidebar-resize-handle { display: none; }
|
|
824
|
-
#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); }
|
|
825
937
|
#sidebar.mobile-open { transform: translateX(0); }
|
|
826
938
|
#sidebar.hidden { transform: translateX(-100%); width: var(--sidebar-w); }
|
|
827
|
-
#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); }
|
|
828
940
|
#settings-panel.open { transform: translateX(0); }
|
|
829
|
-
#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; }
|
|
830
944
|
#search-input { width: 200px; }
|
|
831
945
|
.modal { min-width: unset; width: 95vw; }
|
|
832
946
|
.sys-grid { grid-template-columns: 1fr; }
|
|
@@ -859,6 +973,7 @@ svg { display: block; }
|
|
|
859
973
|
</style>
|
|
860
974
|
</head>
|
|
861
975
|
<body data-theme="light">
|
|
976
|
+
<a class="skip-link" href="#content">Skip to content</a>
|
|
862
977
|
<script>
|
|
863
978
|
// Safe localStorage wrapper — degrades gracefully when Tracking Prevention blocks storage
|
|
864
979
|
const safeStorage = {
|
|
@@ -881,7 +996,7 @@ svg { display: block; }
|
|
|
881
996
|
<!-- PIN Screen -->
|
|
882
997
|
<div id="pin-screen">
|
|
883
998
|
<div id="pin-box">
|
|
884
|
-
<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>
|
|
885
1000
|
<p>Enter PIN to continue</p>
|
|
886
1001
|
<input id="pin-input" type="password" placeholder="••••" autocomplete="off" maxlength="32" inputmode="numeric" aria-label="PIN code">
|
|
887
1002
|
<div id="pin-error" role="alert"></div>
|
|
@@ -894,13 +1009,13 @@ svg { display: block; }
|
|
|
894
1009
|
<!-- Main App -->
|
|
895
1010
|
<div id="app" style="display:none">
|
|
896
1011
|
<!-- Header -->
|
|
897
|
-
<div id="header">
|
|
898
|
-
<span id="conn-status" style="width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0" title="Connected"></span>
|
|
899
|
-
<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>
|
|
900
1015
|
<span id="hostname-badge">—</span>
|
|
901
1016
|
<div class="header-spacer"></div>
|
|
902
1017
|
<label id="keep-awake-toggle" class="keep-awake" title="Keep Screen Awake">
|
|
903
|
-
<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)">
|
|
904
1019
|
<span class="keep-awake-box">
|
|
905
1020
|
<span class="keep-awake-dot"></span>
|
|
906
1021
|
</span>
|
|
@@ -920,40 +1035,54 @@ svg { display: block; }
|
|
|
920
1035
|
<button class="icon-btn" id="tilesBtn" aria-label="Tiles Mode" title="Tiles Mode" onclick="toggleTiles()">
|
|
921
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>
|
|
922
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>
|
|
923
1041
|
<button class="icon-btn" aria-label="Settings" title="Settings" onclick="openSettings()">
|
|
924
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>
|
|
925
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>
|
|
926
1051
|
<div id="upload-progress"><div id="upload-progress-bar"></div><span id="upload-progress-text"></span></div>
|
|
927
1052
|
</div>
|
|
928
|
-
<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>
|
|
929
1054
|
|
|
930
1055
|
<!-- Main -->
|
|
931
1056
|
<div id="main">
|
|
932
1057
|
<!-- Sidebar -->
|
|
933
|
-
<div id="sidebar">
|
|
1058
|
+
<div id="sidebar" role="navigation" aria-label="File explorer">
|
|
934
1059
|
<div id="sidebar-header">
|
|
935
1060
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Back" title="Back" onclick="navigateBack()" id="back-btn">
|
|
936
|
-
<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>
|
|
937
1062
|
</button>
|
|
938
1063
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Forward" title="Forward" onclick="navigateForward()" id="fwd-btn">
|
|
939
|
-
<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>
|
|
940
1065
|
</button>
|
|
941
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">
|
|
942
|
-
<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>
|
|
943
1068
|
</button>
|
|
944
1069
|
<input id="path-input" type="text" placeholder="Path…" aria-label="File path" onkeydown="if(event.key==='Enter')navigateTo(this.value)">
|
|
945
1070
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Go up" title="Go up" onclick="navigateUp()">
|
|
946
|
-
<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>
|
|
947
1072
|
</button>
|
|
948
1073
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Refresh" title="Refresh" onclick="refreshFiles()">
|
|
949
|
-
<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>
|
|
950
1075
|
</button>
|
|
951
1076
|
<button class="icon-btn" style="width:24px;height:24px" aria-label="Toggle select mode" title="Toggle select mode" onclick="toggleSelectMode()">
|
|
952
|
-
<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>
|
|
953
1078
|
</button>
|
|
954
1079
|
</div>
|
|
955
1080
|
<div id="file-breadcrumb">/</div>
|
|
956
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>
|
|
957
1086
|
<div id="file-list"></div>
|
|
958
1087
|
</div>
|
|
959
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">
|
|
@@ -991,10 +1120,11 @@ svg { display: block; }
|
|
|
991
1120
|
</div>
|
|
992
1121
|
|
|
993
1122
|
<!-- Content -->
|
|
994
|
-
<div id="content">
|
|
1123
|
+
<div id="content" role="main">
|
|
995
1124
|
<div id="tab-bar">
|
|
996
|
-
<
|
|
997
|
-
|
|
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>
|
|
998
1128
|
</button>
|
|
999
1129
|
</div>
|
|
1000
1130
|
<div id="editor-split-area">
|
|
@@ -1003,16 +1133,16 @@ svg { display: block; }
|
|
|
1003
1133
|
<input id="search-input" type="text" placeholder="Search…" oninput="doSearch()" onkeydown="searchKeydown(event)">
|
|
1004
1134
|
<span id="search-results"></span>
|
|
1005
1135
|
<button class="search-btn" title="Prev (Shift+Enter)" onclick="searchPrev()">
|
|
1006
|
-
<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>
|
|
1007
1137
|
</button>
|
|
1008
1138
|
<button class="search-btn" title="Next (Enter)" onclick="searchNext()">
|
|
1009
|
-
<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>
|
|
1010
1140
|
</button>
|
|
1011
1141
|
<button class="search-btn" id="search-case-btn" title="Match Case" onclick="toggleSearchCase()">
|
|
1012
1142
|
Aa
|
|
1013
1143
|
</button>
|
|
1014
1144
|
<button class="search-btn" title="Close (Esc)" onclick="closeSearch()">
|
|
1015
|
-
<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>
|
|
1016
1146
|
</button>
|
|
1017
1147
|
</div>
|
|
1018
1148
|
</div>
|
|
@@ -1025,7 +1155,7 @@ svg { display: block; }
|
|
|
1025
1155
|
Command Library
|
|
1026
1156
|
</h3>
|
|
1027
1157
|
<button class="icon-btn" style="width:24px;height:24px" onclick="toggleCmdLib()" title="Close">
|
|
1028
|
-
<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>
|
|
1029
1159
|
</button>
|
|
1030
1160
|
</div>
|
|
1031
1161
|
<div class="cmd-lib-tabs">
|
|
@@ -1057,7 +1187,7 @@ svg { display: block; }
|
|
|
1057
1187
|
<button class="icon-btn" aria-label="Close editor" onclick="closeEditor()" title="Close editor">
|
|
1058
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>
|
|
1059
1189
|
</button>
|
|
1060
|
-
<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>
|
|
1061
1191
|
<span id="editor-status"></span>
|
|
1062
1192
|
<button class="btn" id="md-preview-toggle" style="height:26px;padding:0 12px;font-size:12px;display:none" onclick="toggleMdPreview()">Preview</button>
|
|
1063
1193
|
<button class="icon-btn" id="preview-refresh-btn" aria-label="Refresh preview" title="Refresh preview (Ctrl+Shift+R)" onclick="refreshPreview()" style="display:none">
|
|
@@ -1066,7 +1196,7 @@ svg { display: block; }
|
|
|
1066
1196
|
<button class="icon-btn active" id="editor-split-toggle" aria-label="Toggle split orientation" onclick="toggleEditorSplit()" title="Switch to horizontal split">
|
|
1067
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>
|
|
1068
1198
|
</button>
|
|
1069
|
-
<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>
|
|
1070
1200
|
</div>
|
|
1071
1201
|
<div id="editor-area">
|
|
1072
1202
|
<textarea id="editor-textarea" spellcheck="false"></textarea>
|
|
@@ -1077,16 +1207,16 @@ svg { display: block; }
|
|
|
1077
1207
|
|
|
1078
1208
|
<!-- Mobile key bar -->
|
|
1079
1209
|
<div id="mobile-keys">
|
|
1080
|
-
<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>
|
|
1081
1211
|
<span class="mkey-sep"></span>
|
|
1082
1212
|
<button class="mkey" onclick="sendKey('\x1b')" title="Escape">ESC</button>
|
|
1083
1213
|
<span class="mkey-sep"></span>
|
|
1084
1214
|
<button class="mkey" onclick="sendKey('\t')" title="Tab">TAB</button>
|
|
1085
1215
|
<span class="mkey-sep"></span>
|
|
1086
|
-
<button class="mkey" onclick="sendKey('\x1b[A')" title="Arrow Up"
|
|
1087
|
-
<button class="mkey" onclick="sendKey('\x1b[B')" title="Arrow Down"
|
|
1088
|
-
<button class="mkey" onclick="sendKey('\x1b[D')" title="Arrow Left"
|
|
1089
|
-
<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>
|
|
1090
1220
|
<span class="mkey-sep"></span>
|
|
1091
1221
|
<button class="mkey" onclick="sendKey('\x01')" title="Ctrl+A (Home)">^A</button>
|
|
1092
1222
|
<button class="mkey" onclick="sendKey('\x03')" title="Ctrl+C (Interrupt)">^C</button>
|
|
@@ -1095,7 +1225,7 @@ svg { display: block; }
|
|
|
1095
1225
|
<button class="mkey" onclick="sendKey('\x0c')" title="Ctrl+L (Clear)">^L</button>
|
|
1096
1226
|
<span class="mkey-sep"></span>
|
|
1097
1227
|
<button class="mkey wide" onclick="sendKey('\x1b[3~')" title="Delete">DEL</button>
|
|
1098
|
-
<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>
|
|
1099
1229
|
<span class="mkey-sep"></span>
|
|
1100
1230
|
<button class="mkey" onclick="sendKey('\x1b[H')" title="Home">Home</button>
|
|
1101
1231
|
<button class="mkey" onclick="sendKey('\x1b[F')" title="End">End</button>
|
|
@@ -1105,7 +1235,7 @@ svg { display: block; }
|
|
|
1105
1235
|
<button class="mkey" onclick="sendKey('\x1bOQ')" title="F2">F2</button>
|
|
1106
1236
|
<button class="mkey" onclick="sendKey('\x1b[15~')" title="F5">F5</button>
|
|
1107
1237
|
<span class="mkey-sep"></span>
|
|
1108
|
-
<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>
|
|
1109
1239
|
<button class="mkey" onclick="toggleAlt()" title="Toggle Alt (latches for next key)" id="alt-toggle-btn">Alt</button>
|
|
1110
1240
|
<span class="mkey-sep"></span>
|
|
1111
1241
|
<button class="mkey" onclick="toggleCtrlRow()" title="Toggle Ctrl keys" id="ctrl-toggle-btn">Ctrl</button>
|
|
@@ -1132,8 +1262,8 @@ svg { display: block; }
|
|
|
1132
1262
|
<button class="mkey" onclick="selectAllTerm()" title="Select all terminal content">Select All</button>
|
|
1133
1263
|
<button class="mkey" onclick="selectTermLine()" title="Select current visible line">Line</button>
|
|
1134
1264
|
<span class="mkey-sep"></span>
|
|
1135
|
-
<button class="mkey" onclick="termScrollUp()" title="Scroll up"
|
|
1136
|
-
<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>
|
|
1137
1267
|
<button class="mkey" id="sel-scroll-btn" onclick="toggleScrollMode()" title="Toggle scroll mode">Scroll</button>
|
|
1138
1268
|
<span class="mkey-sep"></span>
|
|
1139
1269
|
<button class="mkey sel-done" onclick="toggleTermSelect()" title="Exit selection mode">Done</button>
|
|
@@ -1170,10 +1300,10 @@ svg { display: block; }
|
|
|
1170
1300
|
<div class="setting-row">
|
|
1171
1301
|
<label for="s-font">Font Family</label>
|
|
1172
1302
|
<select id="s-font" onchange="applyFont(this.value)">
|
|
1303
|
+
<option value="'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, monospace">JetBrains Mono</option>
|
|
1173
1304
|
<option value="'SF Mono', 'Fira Code', Consolas, monospace">System Mono</option>
|
|
1174
1305
|
<option value="'Fira Code', monospace">Fira Code</option>
|
|
1175
1306
|
<option value="'Cascadia Code', monospace">Cascadia Code</option>
|
|
1176
|
-
<option value="'JetBrains Mono', monospace">JetBrains Mono</option>
|
|
1177
1307
|
<option value="monospace">Monospace</option>
|
|
1178
1308
|
</select>
|
|
1179
1309
|
</div>
|
|
@@ -1229,10 +1359,11 @@ svg { display: block; }
|
|
|
1229
1359
|
<div class="setting-row" style="flex-wrap:wrap;gap:6px">
|
|
1230
1360
|
<label for="tunnel-url" style="flex:0 0 auto">Local URL</label>
|
|
1231
1361
|
<div style="display:flex;flex:1;gap:6px;min-width:0">
|
|
1232
|
-
<input type="text" id="tunnel-url" value="http://localhost:3000" placeholder="http://localhost:PORT" style="flex:1;min-width:0">
|
|
1233
|
-
<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>
|
|
1234
1364
|
</div>
|
|
1235
1365
|
</div>
|
|
1366
|
+
<div class="field-error" id="tunnel-field-error" role="alert"></div>
|
|
1236
1367
|
<div id="tunnel-list"></div>
|
|
1237
1368
|
<div id="tunnel-status" style="font-size:11px;color:var(--fg2);min-height:16px;margin-top:2px"></div>
|
|
1238
1369
|
</div>
|
|
@@ -1254,6 +1385,9 @@ svg { display: block; }
|
|
|
1254
1385
|
</div>
|
|
1255
1386
|
</div>
|
|
1256
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>
|
|
1257
1391
|
</div>
|
|
1258
1392
|
</div>
|
|
1259
1393
|
</div>
|
|
@@ -1284,7 +1418,7 @@ svg { display: block; }
|
|
|
1284
1418
|
</div>
|
|
1285
1419
|
|
|
1286
1420
|
<!-- Terminal context menu -->
|
|
1287
|
-
<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">
|
|
1288
1422
|
<!-- Clipboard -->
|
|
1289
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>
|
|
1290
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>
|
|
@@ -1300,7 +1434,7 @@ svg { display: block; }
|
|
|
1300
1434
|
<!-- More Options -->
|
|
1301
1435
|
<div class="ctx-sep"></div>
|
|
1302
1436
|
<div class="ctx-submenu-wrap">
|
|
1303
|
-
<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>
|
|
1304
1438
|
<div class="ctx-submenu-items">
|
|
1305
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>
|
|
1306
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>
|
|
@@ -1318,7 +1452,7 @@ svg { display: block; }
|
|
|
1318
1452
|
</div>
|
|
1319
1453
|
|
|
1320
1454
|
<!-- Fuzzy Finder Modal -->
|
|
1321
|
-
<div class="overlay" id="finder-overlay">
|
|
1455
|
+
<div class="overlay" id="finder-overlay" role="dialog" aria-modal="true" aria-label="Search files">
|
|
1322
1456
|
<div class="modal modal-md" style="padding:12px">
|
|
1323
1457
|
<input id="finder-input" type="text" placeholder="Search files and folders…" autocomplete="off" oninput="doFinderSearch()" onkeydown="finderKeydown(event)">
|
|
1324
1458
|
<div id="finder-results"></div>
|
|
@@ -1326,6 +1460,31 @@ svg { display: block; }
|
|
|
1326
1460
|
</div>
|
|
1327
1461
|
</div>
|
|
1328
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
|
+
|
|
1329
1488
|
<!-- System Stats Modal -->
|
|
1330
1489
|
<div class="overlay" id="sys-overlay">
|
|
1331
1490
|
<div class="modal">
|
|
@@ -1382,10 +1541,11 @@ svg { display: block; }
|
|
|
1382
1541
|
<div class="modal-row">
|
|
1383
1542
|
<label for="rename-input">New Name</label>
|
|
1384
1543
|
<input type="text" id="rename-input" onkeydown="if(event.key==='Enter')confirmRename()">
|
|
1544
|
+
<div class="field-error" id="rename-error" role="alert"></div>
|
|
1385
1545
|
</div>
|
|
1386
1546
|
<div class="modal-actions">
|
|
1387
1547
|
<button class="btn btn-ghost" onclick="closeOverlay('rename-overlay')">Cancel</button>
|
|
1388
|
-
<button class="btn btn-primary" onclick="confirmRename()">Rename</button>
|
|
1548
|
+
<button class="btn btn-primary" id="rename-ok-btn" onclick="confirmRename()">Rename</button>
|
|
1389
1549
|
</div>
|
|
1390
1550
|
</div>
|
|
1391
1551
|
</div>
|
|
@@ -1397,10 +1557,11 @@ svg { display: block; }
|
|
|
1397
1557
|
<div class="modal-row">
|
|
1398
1558
|
<label for="newfolder-input">Folder Name</label>
|
|
1399
1559
|
<input type="text" id="newfolder-input" onkeydown="if(event.key==='Enter')confirmNewFolder()">
|
|
1560
|
+
<div class="field-error" id="newfolder-error" role="alert"></div>
|
|
1400
1561
|
</div>
|
|
1401
1562
|
<div class="modal-actions">
|
|
1402
1563
|
<button class="btn btn-ghost" onclick="closeOverlay('newfolder-overlay')">Cancel</button>
|
|
1403
|
-
<button class="btn btn-primary" onclick="confirmNewFolder()">Create</button>
|
|
1564
|
+
<button class="btn btn-primary" id="newfolder-ok-btn" onclick="confirmNewFolder()">Create</button>
|
|
1404
1565
|
</div>
|
|
1405
1566
|
</div>
|
|
1406
1567
|
</div>
|
|
@@ -1412,10 +1573,11 @@ svg { display: block; }
|
|
|
1412
1573
|
<div class="modal-row">
|
|
1413
1574
|
<label for="newfile-input">File Name</label>
|
|
1414
1575
|
<input type="text" id="newfile-input" onkeydown="if(event.key==='Enter')confirmNewFile()">
|
|
1576
|
+
<div class="field-error" id="newfile-error" role="alert"></div>
|
|
1415
1577
|
</div>
|
|
1416
1578
|
<div class="modal-actions">
|
|
1417
1579
|
<button class="btn btn-ghost" onclick="closeOverlay('newfile-overlay')">Cancel</button>
|
|
1418
|
-
<button class="btn btn-primary" onclick="confirmNewFile()">Create</button>
|
|
1580
|
+
<button class="btn btn-primary" id="newfile-ok-btn" onclick="confirmNewFile()">Create</button>
|
|
1419
1581
|
</div>
|
|
1420
1582
|
</div>
|
|
1421
1583
|
</div>
|
|
@@ -1436,13 +1598,25 @@ svg { display: block; }
|
|
|
1436
1598
|
</div>
|
|
1437
1599
|
</div>
|
|
1438
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
|
+
|
|
1439
1613
|
<!-- Image Viewer -->
|
|
1440
|
-
<div class="overlay" id="image-viewer">
|
|
1614
|
+
<div class="overlay" id="image-viewer" role="dialog" aria-modal="true" aria-label="Image preview">
|
|
1441
1615
|
<div class="modal">
|
|
1442
1616
|
<div class="iv-header">
|
|
1443
1617
|
<span class="iv-name" id="iv-name"></span>
|
|
1444
1618
|
<button class="iv-close" onclick="closeImageViewer()" aria-label="Close image viewer">
|
|
1445
|
-
<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>
|
|
1446
1620
|
</button>
|
|
1447
1621
|
</div>
|
|
1448
1622
|
<div id="iv-loading" class="iv-loading">Loading…</div>
|
|
@@ -1457,7 +1631,7 @@ svg { display: block; }
|
|
|
1457
1631
|
<input type="file" id="upload-input" multiple style="display:none" onchange="handleUploadInput()">
|
|
1458
1632
|
|
|
1459
1633
|
<!-- PWA Install Banner -->
|
|
1460
|
-
<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)">
|
|
1461
1635
|
<span class="install-msg" style="flex:1;font-size:13px">Install WebTun for the best experience</span>
|
|
1462
1636
|
<button class="btn btn-ghost" style="height:32px;padding:0 12px;font-size:12px" onclick="dismissInstall()">Not now</button>
|
|
1463
1637
|
<button class="btn btn-primary" style="height:32px;padding:0 12px;font-size:12px" onclick="installPWA()">Install</button>
|
|
@@ -1482,8 +1656,8 @@ let renamePath = '';
|
|
|
1482
1656
|
let editorPath = '';
|
|
1483
1657
|
let editorOriginalContent = '';
|
|
1484
1658
|
let hasTmux = false;
|
|
1485
|
-
let settings = {
|
|
1486
|
-
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",
|
|
1487
1661
|
cursor: 'block', blink: true, scrollback: 5000, bell: false,
|
|
1488
1662
|
mobilekeys: false, confirmclose: true, datasaver: false, autostart: false, keepAwake: false, termRightClick: true
|
|
1489
1663
|
};
|
|
@@ -1619,14 +1793,33 @@ async function unlockApp() {
|
|
|
1619
1793
|
restoreTunnels();
|
|
1620
1794
|
setupSwipeGestures();
|
|
1621
1795
|
setupFileListTouch();
|
|
1796
|
+
setupFileListKeyboard();
|
|
1797
|
+
setupCtxMenuKeyboard();
|
|
1798
|
+
setupMoreMenuKeyboard();
|
|
1622
1799
|
restoreSidebarWidth();
|
|
1623
1800
|
setupSidebarResize();
|
|
1624
1801
|
setupEditorResize();
|
|
1625
1802
|
window.addEventListener('resize', updateSidebarNarrowClass);
|
|
1626
1803
|
window._cleanups.push(() => window.removeEventListener('resize', updateSidebarNarrowClass));
|
|
1627
|
-
document.getElementById('tab-
|
|
1804
|
+
document.getElementById('tab-scroll').setAttribute('role', 'tablist');
|
|
1628
1805
|
document.getElementById('file-list-wrap').setAttribute('role', 'listbox');
|
|
1629
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
|
+
|
|
1630
1823
|
// Listen for system theme changes
|
|
1631
1824
|
const systemThemeMql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1632
1825
|
const onSystemThemeChange = () => { if (settings.theme === 'system') applyTheme('system'); };
|
|
@@ -1726,7 +1919,7 @@ function setupTabDragDrop(tabEl, id) {
|
|
|
1726
1919
|
const [moved] = tabs.splice(fromIdx, 1);
|
|
1727
1920
|
const targetIdx = tabs.findIndex(t => t.id === id);
|
|
1728
1921
|
tabs.splice(targetIdx, 0, moved);
|
|
1729
|
-
const bar = document.getElementById('tab-
|
|
1922
|
+
const bar = document.getElementById('tab-scroll');
|
|
1730
1923
|
if (moved.el && moved.el.parentNode === bar) bar.insertBefore(moved.el, tabEl);
|
|
1731
1924
|
saveTabState();
|
|
1732
1925
|
};
|
|
@@ -1852,7 +2045,7 @@ function createTabButton(tab) {
|
|
|
1852
2045
|
setupTabInlineRename(titleSpan, tab);
|
|
1853
2046
|
setupTabSwipeGesture(tabEl, id);
|
|
1854
2047
|
|
|
1855
|
-
document.getElementById('tab-
|
|
2048
|
+
document.getElementById('tab-scroll').appendChild(tabEl);
|
|
1856
2049
|
tab.el = tabEl;
|
|
1857
2050
|
}
|
|
1858
2051
|
|
|
@@ -1860,10 +2053,19 @@ function createTerminalWrapper(tab) {
|
|
|
1860
2053
|
const wrapper = document.createElement('div');
|
|
1861
2054
|
wrapper.className = 'term-wrapper';
|
|
1862
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;
|
|
1863
2061
|
document.getElementById('terminals').appendChild(wrapper);
|
|
1864
2062
|
tab.wrapper = wrapper;
|
|
1865
2063
|
}
|
|
1866
2064
|
|
|
2065
|
+
function hideTermLoading(tab) {
|
|
2066
|
+
if (tab && tab.loadingEl) tab.loadingEl.classList.add('hidden');
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1867
2069
|
function newTab(title, sessionId, dir) {
|
|
1868
2070
|
const id = ++tabCounter;
|
|
1869
2071
|
const sid = sessionId || (hasTmux ? uuid() : null);
|
|
@@ -1978,10 +2180,11 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1978
2180
|
ws.onopen = () => {
|
|
1979
2181
|
tab.reconnectDelay = 1000;
|
|
1980
2182
|
tab.reconnectAttempts = 0;
|
|
2183
|
+
hideTermLoading(tab);
|
|
1981
2184
|
updateConnStatus(true);
|
|
1982
2185
|
const banner = document.getElementById('reconnect-banner');
|
|
1983
2186
|
if (banner) {
|
|
1984
|
-
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…';
|
|
1985
2188
|
banner.style.display = 'none';
|
|
1986
2189
|
}
|
|
1987
2190
|
if (isReconnect) {
|
|
@@ -2078,7 +2281,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
2078
2281
|
if (tab.reconnectAttempts >= 3) {
|
|
2079
2282
|
const banner = document.getElementById('reconnect-banner');
|
|
2080
2283
|
if (banner) {
|
|
2081
|
-
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>';
|
|
2082
2285
|
banner.style.display = 'block';
|
|
2083
2286
|
}
|
|
2084
2287
|
} else {
|
|
@@ -2102,7 +2305,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
2102
2305
|
function manualReconnect() {
|
|
2103
2306
|
const banner = document.getElementById('reconnect-banner');
|
|
2104
2307
|
if (banner) {
|
|
2105
|
-
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…';
|
|
2106
2309
|
banner.style.display = 'block';
|
|
2107
2310
|
}
|
|
2108
2311
|
tabs.forEach(tab => {
|
|
@@ -2115,15 +2318,19 @@ function manualReconnect() {
|
|
|
2115
2318
|
});
|
|
2116
2319
|
}
|
|
2117
2320
|
|
|
2118
|
-
function closeTab(e, id) {
|
|
2321
|
+
async function closeTab(e, id) {
|
|
2119
2322
|
e.stopPropagation();
|
|
2120
|
-
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
|
+
}
|
|
2121
2327
|
|
|
2122
2328
|
// Check for unsaved editor changes
|
|
2123
2329
|
const editorOpen = document.getElementById('editor-view').classList.contains('open');
|
|
2124
2330
|
const currentContent = editor ? editor.getValue() : '';
|
|
2125
2331
|
if (editorOpen && currentContent !== editorOriginalContent) {
|
|
2126
|
-
|
|
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;
|
|
2127
2334
|
}
|
|
2128
2335
|
|
|
2129
2336
|
const tab = tabs.find(t => t.id === id);
|
|
@@ -2164,11 +2371,7 @@ function initTerminal(tab) {
|
|
|
2164
2371
|
convertEol: false,
|
|
2165
2372
|
bellStyle: settings.bell ? 'sound' : 'none',
|
|
2166
2373
|
smoothScrollDuration: 80,
|
|
2167
|
-
selectionTheme:
|
|
2168
|
-
extension: true,
|
|
2169
|
-
foreground: '#1a1b26',
|
|
2170
|
-
background: '#7aa2f744'
|
|
2171
|
-
}
|
|
2374
|
+
selectionTheme: getXtermSelectionTheme()
|
|
2172
2375
|
};
|
|
2173
2376
|
|
|
2174
2377
|
const term = new Terminal(cfg);
|
|
@@ -2293,6 +2496,7 @@ function initTerminal(tab) {
|
|
|
2293
2496
|
top = Math.max(0, Math.min(top, vh - mh));
|
|
2294
2497
|
menu.style.left = left + 'px';
|
|
2295
2498
|
menu.style.top = top + 'px';
|
|
2499
|
+
menu.querySelector('.ctx-item')?.focus();
|
|
2296
2500
|
});
|
|
2297
2501
|
|
|
2298
2502
|
// Touch-to-mouse translation for TUI apps (mobile)
|
|
@@ -2523,7 +2727,7 @@ function layoutTiles() {
|
|
|
2523
2727
|
const tthClose = document.createElement('button');
|
|
2524
2728
|
tthClose.className = 'tth-close';
|
|
2525
2729
|
tthClose.setAttribute('aria-label', 'Close tab');
|
|
2526
|
-
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>';
|
|
2527
2731
|
hdr.appendChild(tthClose);
|
|
2528
2732
|
hdr.addEventListener('click', e => {
|
|
2529
2733
|
if (e.target.closest('.tth-close')) return;
|
|
@@ -2557,10 +2761,32 @@ function getXtermTheme() {
|
|
|
2557
2761
|
};
|
|
2558
2762
|
}
|
|
2559
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
|
+
|
|
2560
2774
|
// ═══════════════════════════════════════════════════════
|
|
2561
2775
|
// FILE EXPLORER
|
|
2562
2776
|
// ═══════════════════════════════════════════════════════
|
|
2563
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
|
+
}
|
|
2564
2790
|
|
|
2565
2791
|
async function loadFiles(dir) {
|
|
2566
2792
|
if (loadFilesAbortController) loadFilesAbortController.abort();
|
|
@@ -2582,11 +2808,13 @@ async function loadFiles(dir) {
|
|
|
2582
2808
|
|
|
2583
2809
|
const list = document.getElementById('file-list');
|
|
2584
2810
|
|
|
2811
|
+
setFileListLoading(true);
|
|
2585
2812
|
let data;
|
|
2586
2813
|
try {
|
|
2587
2814
|
data = await api(`/api/files?path=${encodeURIComponent(dir)}`, { signal: loadFilesAbortController.signal });
|
|
2588
2815
|
} catch (e) {
|
|
2589
|
-
if (e.name === 'AbortError') return;
|
|
2816
|
+
if (e.name === 'AbortError') { setFileListLoading(false); return; }
|
|
2817
|
+
setFileListLoading(false);
|
|
2590
2818
|
const errorDiv = document.createElement('div');
|
|
2591
2819
|
errorDiv.style.cssText = 'padding:16px;text-align:center;';
|
|
2592
2820
|
errorDiv.innerHTML = '<div style="color:var(--red);margin-bottom:8px">Failed to load directory</div>' +
|
|
@@ -2598,6 +2826,7 @@ async function loadFiles(dir) {
|
|
|
2598
2826
|
}
|
|
2599
2827
|
|
|
2600
2828
|
if (!data.files) {
|
|
2829
|
+
setFileListLoading(false);
|
|
2601
2830
|
if (dir !== homeDir) loadFiles(homeDir);
|
|
2602
2831
|
return;
|
|
2603
2832
|
}
|
|
@@ -2682,16 +2911,25 @@ async function loadFiles(dir) {
|
|
|
2682
2911
|
if (list.children.length === 0) {
|
|
2683
2912
|
const empty = document.createElement('div');
|
|
2684
2913
|
empty.className = 'file-list-empty';
|
|
2685
|
-
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>';
|
|
2686
2920
|
list.appendChild(empty);
|
|
2687
2921
|
}
|
|
2688
2922
|
|
|
2689
2923
|
updateBackBtn();
|
|
2924
|
+
setFileListLoading(false);
|
|
2690
2925
|
}
|
|
2691
2926
|
|
|
2927
|
+
let fileItemCounter = 0;
|
|
2928
|
+
|
|
2692
2929
|
function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
2693
2930
|
const div = document.createElement('div');
|
|
2694
2931
|
div.className = 'file-item';
|
|
2932
|
+
div.id = 'file-item-' + (++fileItemCounter);
|
|
2695
2933
|
div.setAttribute('role', 'option');
|
|
2696
2934
|
div.setAttribute('aria-selected', 'false');
|
|
2697
2935
|
div.dataset.path = fullPath;
|
|
@@ -2701,6 +2939,7 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2701
2939
|
<span class="file-icon">${icon}</span>
|
|
2702
2940
|
<span class="file-name ${isDir ? 'file-dir' : ''}" title="${escHtml(fullPath)}">${escHtml(name)}</span>
|
|
2703
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>
|
|
2704
2943
|
`;
|
|
2705
2944
|
div.addEventListener('click', e => {
|
|
2706
2945
|
if (e.target.closest('.file-select-check')) {
|
|
@@ -2708,6 +2947,13 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2708
2947
|
if (selectedFiles.length > 0) document.getElementById('select-actions').style.display = 'flex';
|
|
2709
2948
|
return;
|
|
2710
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
|
+
}
|
|
2711
2957
|
if (selectMode) {
|
|
2712
2958
|
toggleFileSelection(fullPath, div);
|
|
2713
2959
|
return;
|
|
@@ -2716,9 +2962,58 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2716
2962
|
else if (isImageFile(fullPath)) openImageViewer(fullPath);
|
|
2717
2963
|
else openFileEditor(fullPath);
|
|
2718
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
|
+
}
|
|
2719
2977
|
return div;
|
|
2720
2978
|
}
|
|
2721
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
|
+
|
|
2722
3017
|
function refreshFiles() {
|
|
2723
3018
|
if (currentPath) loadFiles(currentPath);
|
|
2724
3019
|
}
|
|
@@ -2992,8 +3287,77 @@ document.querySelector('#term-ctx-menu .ctx-submenu-wrap > .ctx-item').addEventL
|
|
|
2992
3287
|
e.stopPropagation();
|
|
2993
3288
|
const wrap = e.currentTarget.closest('.ctx-submenu-wrap');
|
|
2994
3289
|
wrap.classList.toggle('open');
|
|
3290
|
+
e.currentTarget.setAttribute('aria-expanded', wrap.classList.contains('open'));
|
|
2995
3291
|
});
|
|
2996
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
|
+
|
|
2997
3361
|
function showCtxMenu(e, file) {
|
|
2998
3362
|
e.preventDefault();
|
|
2999
3363
|
ctxTarget = file;
|
|
@@ -3021,6 +3385,8 @@ function showCtxMenu(e, file) {
|
|
|
3021
3385
|
menu.style.top = top + 'px';
|
|
3022
3386
|
menu.classList.add('open');
|
|
3023
3387
|
menu.style.display = '';
|
|
3388
|
+
const firstItem = menu.querySelector('.ctx-item');
|
|
3389
|
+
if (firstItem) firstItem.focus();
|
|
3024
3390
|
}
|
|
3025
3391
|
|
|
3026
3392
|
document.addEventListener('click', () => {
|
|
@@ -3183,19 +3549,23 @@ function startRename(path, name) {
|
|
|
3183
3549
|
|
|
3184
3550
|
async function confirmRename() {
|
|
3185
3551
|
const newName = document.getElementById('rename-input').value.trim();
|
|
3186
|
-
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);
|
|
3187
3555
|
const r = await api('/api/files/rename', {
|
|
3188
3556
|
method: 'POST',
|
|
3189
3557
|
headers: { 'Content-Type': 'application/json' },
|
|
3190
3558
|
body: JSON.stringify({ oldPath: renamePath, newName })
|
|
3191
3559
|
});
|
|
3560
|
+
setBtnBusy(document.getElementById('rename-ok-btn'), false);
|
|
3192
3561
|
if (r.success) { toast('Renamed', 'success'); refreshFiles(); }
|
|
3193
|
-
else
|
|
3562
|
+
else showFieldError('rename-error', r.error || 'Rename failed');
|
|
3194
3563
|
closeOverlay('rename-overlay');
|
|
3195
3564
|
}
|
|
3196
3565
|
|
|
3197
3566
|
async function deleteFile(file) {
|
|
3198
|
-
|
|
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;
|
|
3199
3569
|
const r = await api(`/api/files?path=${encodeURIComponent(file.path)}`, { method: 'DELETE' });
|
|
3200
3570
|
if (r.success) { toast('Deleted', 'success'); refreshFiles(); }
|
|
3201
3571
|
else toast(r.error, 'error');
|
|
@@ -3239,6 +3609,8 @@ function toggleSelectMode() {
|
|
|
3239
3609
|
selectedFiles = [];
|
|
3240
3610
|
document.getElementById('select-actions').style.display = selectMode ? 'flex' : 'none';
|
|
3241
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));
|
|
3242
3614
|
document.getElementById('select-count').textContent = '0 selected';
|
|
3243
3615
|
document.querySelector('[onclick="toggleSelectMode()"]')?.classList.toggle('active', selectMode);
|
|
3244
3616
|
refreshFiles();
|
|
@@ -3302,7 +3674,8 @@ function clearSelection() {
|
|
|
3302
3674
|
async function deleteSelected() {
|
|
3303
3675
|
if (!selectedFiles.length) return;
|
|
3304
3676
|
const msg = `Delete ${selectedFiles.length} item${selectedFiles.length > 1 ? 's' : ''}?`;
|
|
3305
|
-
|
|
3677
|
+
const ok = await confirmDialog({ title: 'Delete', message: msg, okText: 'Delete', danger: true });
|
|
3678
|
+
if (!ok) return;
|
|
3306
3679
|
const results = await Promise.allSettled(
|
|
3307
3680
|
[...selectedFiles].map(p =>
|
|
3308
3681
|
api(`/api/files?path=${encodeURIComponent(p)}`, { method: 'DELETE' })
|
|
@@ -3343,15 +3716,17 @@ function newFolder() {
|
|
|
3343
3716
|
|
|
3344
3717
|
async function confirmNewFolder() {
|
|
3345
3718
|
const name = document.getElementById('newfolder-input').value.trim();
|
|
3346
|
-
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);
|
|
3347
3722
|
const r = await api('/api/files/mkdir', {
|
|
3348
3723
|
method: 'POST',
|
|
3349
3724
|
headers: { 'Content-Type': 'application/json' },
|
|
3350
3725
|
body: JSON.stringify({ path: joinPath(currentPath, name) })
|
|
3351
3726
|
});
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
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');
|
|
3355
3730
|
}
|
|
3356
3731
|
|
|
3357
3732
|
function newFile() {
|
|
@@ -3362,15 +3737,18 @@ function newFile() {
|
|
|
3362
3737
|
|
|
3363
3738
|
async function confirmNewFile() {
|
|
3364
3739
|
const name = document.getElementById('newfile-input').value.trim();
|
|
3365
|
-
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);
|
|
3366
3743
|
const filePath = joinPath(currentPath, name);
|
|
3367
3744
|
const r = await api('/api/files/touch', {
|
|
3368
3745
|
method: 'POST',
|
|
3369
3746
|
headers: { 'Content-Type': 'application/json' },
|
|
3370
3747
|
body: JSON.stringify({ path: filePath })
|
|
3371
3748
|
});
|
|
3749
|
+
setBtnBusy(document.getElementById('newfile-ok-btn'), false);
|
|
3372
3750
|
if (r.success) { refreshFiles(); closeOverlay('newfile-overlay'); openFileEditor(filePath); }
|
|
3373
|
-
else
|
|
3751
|
+
else showFieldError('newfile-error', r.error || 'Could not create file');
|
|
3374
3752
|
}
|
|
3375
3753
|
|
|
3376
3754
|
// Upload
|
|
@@ -3473,11 +3851,11 @@ async function uploadFileList(items) {
|
|
|
3473
3851
|
|
|
3474
3852
|
// Final result
|
|
3475
3853
|
if (failedItems.length === 0) {
|
|
3476
|
-
setUploadProgress(100, 'success',
|
|
3854
|
+
setUploadProgress(100, 'success', `${completed}/${total} uploaded`);
|
|
3477
3855
|
} else if (completed > 0) {
|
|
3478
|
-
setUploadProgress(100, 'error',
|
|
3856
|
+
setUploadProgress(100, 'error', `${completed} ok, ${failedItems.length} failed`);
|
|
3479
3857
|
} else {
|
|
3480
|
-
setUploadProgress(100, 'error',
|
|
3858
|
+
setUploadProgress(100, 'error', `All ${total} failed`);
|
|
3481
3859
|
}
|
|
3482
3860
|
|
|
3483
3861
|
clearUploadProgress(2000);
|
|
@@ -3512,9 +3890,17 @@ function initCodeMirror() {
|
|
|
3512
3890
|
},
|
|
3513
3891
|
}
|
|
3514
3892
|
});
|
|
3893
|
+
editor.on('change', () => updateEditorDirty());
|
|
3515
3894
|
return editor;
|
|
3516
3895
|
}
|
|
3517
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
|
+
|
|
3518
3904
|
function getCMmode(fileName) {
|
|
3519
3905
|
const m = fileName.match(/\.([^.]+)$/);
|
|
3520
3906
|
if (!m) return null;
|
|
@@ -3555,7 +3941,8 @@ async function openFileEditor(path) {
|
|
|
3555
3941
|
const r = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
|
|
3556
3942
|
if (r.error) { toast(r.error, 'error'); return; }
|
|
3557
3943
|
if (r.length > MAX_EDITOR_SIZE) {
|
|
3558
|
-
|
|
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;
|
|
3559
3946
|
}
|
|
3560
3947
|
editorPath = path;
|
|
3561
3948
|
editorOriginalContent = r.content;
|
|
@@ -3569,6 +3956,9 @@ async function openFileEditor(path) {
|
|
|
3569
3956
|
cm.setOption('mode', getCMmode(fileName) || 'text/plain');
|
|
3570
3957
|
document.querySelector('.CodeMirror').style.display = '';
|
|
3571
3958
|
cm.refresh();
|
|
3959
|
+
const ta = document.getElementById('editor-textarea');
|
|
3960
|
+
ta.oninput = () => updateEditorDirty();
|
|
3961
|
+
updateEditorDirty();
|
|
3572
3962
|
|
|
3573
3963
|
const preview = document.getElementById('editor-preview');
|
|
3574
3964
|
const toggleBtn = document.getElementById('md-preview-toggle');
|
|
@@ -3584,7 +3974,7 @@ async function openFileEditor(path) {
|
|
|
3584
3974
|
toggleBtn.style.display = 'none';
|
|
3585
3975
|
if (refreshBtn) refreshBtn.style.display = 'none';
|
|
3586
3976
|
}
|
|
3587
|
-
document.getElementById('editor-filename').textContent = fileName + ' ' + path;
|
|
3977
|
+
document.getElementById('editor-filename').textContent = fileName + ' / ' + path;
|
|
3588
3978
|
document.getElementById('editor-status').textContent = '';
|
|
3589
3979
|
|
|
3590
3980
|
// Desktop: split layout; Mobile: overlay (handled by CSS)
|
|
@@ -3636,25 +4026,29 @@ async function openFileEditor(path) {
|
|
|
3636
4026
|
|
|
3637
4027
|
async function saveFile() {
|
|
3638
4028
|
if (!editorPath) { toast('No file open', 'error'); return; }
|
|
4029
|
+
setBtnBusy(document.getElementById('editor-save-btn'), true);
|
|
3639
4030
|
const content = editor ? editor.getValue() : document.getElementById('editor-textarea').value;
|
|
3640
4031
|
const r = await api('/api/files/write', {
|
|
3641
4032
|
method: 'POST',
|
|
3642
4033
|
headers: { 'Content-Type': 'application/json' },
|
|
3643
4034
|
body: JSON.stringify({ path: editorPath, content })
|
|
3644
4035
|
});
|
|
4036
|
+
setBtnBusy(document.getElementById('editor-save-btn'), false);
|
|
3645
4037
|
if (r.success) {
|
|
3646
4038
|
editorOriginalContent = content;
|
|
3647
|
-
|
|
4039
|
+
updateEditorDirty();
|
|
4040
|
+
document.getElementById('editor-status').textContent = 'Saved';
|
|
3648
4041
|
toast('Saved', 'success');
|
|
3649
4042
|
if (mdPreviewActive) refreshPreview();
|
|
3650
4043
|
setTimeout(() => document.getElementById('editor-status').textContent = '', 2000);
|
|
3651
4044
|
} else toast(r.error, 'error');
|
|
3652
4045
|
}
|
|
3653
4046
|
|
|
3654
|
-
function closeEditor() {
|
|
4047
|
+
async function closeEditor() {
|
|
3655
4048
|
const currentContent = editor ? editor.getValue() : document.getElementById('editor-textarea').value;
|
|
3656
4049
|
if (currentContent !== editorOriginalContent) {
|
|
3657
|
-
|
|
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;
|
|
3658
4052
|
}
|
|
3659
4053
|
clearPreviewLiveReload();
|
|
3660
4054
|
document.getElementById('editor-view').classList.remove('open');
|
|
@@ -3986,6 +4380,33 @@ function loadSettings() {
|
|
|
3986
4380
|
|
|
3987
4381
|
function saveSettings() { safeStorage.setItem('wt-settings', JSON.stringify(settings)); }
|
|
3988
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
|
+
}
|
|
4409
|
+
|
|
3989
4410
|
function saveSetting(k, v) { settings[k] = v; saveSettings(); }
|
|
3990
4411
|
|
|
3991
4412
|
function syncToggle(k) {
|
|
@@ -4071,50 +4492,57 @@ function renderTunnels() {
|
|
|
4071
4492
|
list.style.display = 'block';
|
|
4072
4493
|
tunnelList.forEach(t => {
|
|
4073
4494
|
const row = document.createElement('div');
|
|
4074
|
-
row.
|
|
4495
|
+
row.className = 'tunnel-row';
|
|
4075
4496
|
const label = document.createElement('div');
|
|
4076
|
-
label.
|
|
4497
|
+
label.className = 'tunnel-label';
|
|
4077
4498
|
label.textContent = t.localUrl;
|
|
4078
4499
|
row.appendChild(label);
|
|
4079
4500
|
const inner = document.createElement('div');
|
|
4080
|
-
inner.
|
|
4501
|
+
inner.className = 'tunnel-inner';
|
|
4081
4502
|
const urlSpan = document.createElement('span');
|
|
4082
|
-
urlSpan.
|
|
4503
|
+
urlSpan.className = 'tunnel-url';
|
|
4083
4504
|
urlSpan.textContent = t.tunnelUrl;
|
|
4084
4505
|
urlSpan.title = t.tunnelUrl;
|
|
4085
4506
|
inner.appendChild(urlSpan);
|
|
4086
4507
|
const status = document.createElement('span');
|
|
4087
|
-
status.
|
|
4508
|
+
status.className = 'tunnel-status';
|
|
4509
|
+
status.style.background = t.alive ? 'var(--green)' : 'var(--yellow)';
|
|
4510
|
+
status.style.color = 'var(--bg)';
|
|
4088
4511
|
status.textContent = t.alive ? 'Live' : 'Orphan';
|
|
4089
4512
|
status.title = t.alive ? 'cloudflared is running' : 'cloudflared process not found';
|
|
4090
4513
|
inner.appendChild(status);
|
|
4091
4514
|
if (t.alive && t.targetAlive !== undefined) {
|
|
4092
4515
|
const targetStatus = document.createElement('span');
|
|
4093
4516
|
const ok = t.targetAlive;
|
|
4094
|
-
targetStatus.
|
|
4095
|
-
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"/>'));
|
|
4096
4524
|
targetStatus.title = ok ? `Target ${t.localUrl} is responding` : `Target ${t.localUrl} is not responding`;
|
|
4097
4525
|
inner.appendChild(targetStatus);
|
|
4098
4526
|
}
|
|
4099
4527
|
const copyBtn = document.createElement('button');
|
|
4100
|
-
copyBtn.className = 'icon-btn';
|
|
4101
|
-
copyBtn.style.cssText = 'width:22px;height:22px;flex-shrink:0';
|
|
4528
|
+
copyBtn.className = 'icon-btn tunnel-btn';
|
|
4102
4529
|
copyBtn.title = 'Copy';
|
|
4103
|
-
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"/>'));
|
|
4104
4532
|
copyBtn.addEventListener('click', () => copyText(t.tunnelUrl));
|
|
4105
4533
|
inner.appendChild(copyBtn);
|
|
4106
4534
|
const openBtn = document.createElement('button');
|
|
4107
|
-
openBtn.className = 'icon-btn';
|
|
4108
|
-
openBtn.style.cssText = 'width:22px;height:22px;flex-shrink:0';
|
|
4535
|
+
openBtn.className = 'icon-btn tunnel-btn';
|
|
4109
4536
|
openBtn.title = 'Open in new tab';
|
|
4110
|
-
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"/>'));
|
|
4111
4539
|
openBtn.addEventListener('click', () => window.open(t.tunnelUrl, '_blank'));
|
|
4112
4540
|
inner.appendChild(openBtn);
|
|
4113
4541
|
const stopBtn = document.createElement('button');
|
|
4114
|
-
stopBtn.className = 'icon-btn';
|
|
4115
|
-
stopBtn.style.cssText = 'width:22px;height:22px;flex-shrink:0;color:var(--red)';
|
|
4542
|
+
stopBtn.className = 'icon-btn tunnel-btn stop';
|
|
4116
4543
|
stopBtn.title = 'Stop';
|
|
4117
|
-
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"/>'));
|
|
4118
4546
|
stopBtn.addEventListener('click', () => stopTunnelById(t.id));
|
|
4119
4547
|
inner.appendChild(stopBtn);
|
|
4120
4548
|
row.appendChild(inner);
|
|
@@ -4123,22 +4551,24 @@ function renderTunnels() {
|
|
|
4123
4551
|
}
|
|
4124
4552
|
|
|
4125
4553
|
async function createTunnel() {
|
|
4126
|
-
const btn = document.
|
|
4554
|
+
const btn = document.getElementById('tunnel-create-btn');
|
|
4127
4555
|
if (btn && btn.dataset.busy === 'true') return;
|
|
4128
|
-
if (btn) btn.dataset.busy = 'true';
|
|
4129
4556
|
const url = document.getElementById('tunnel-url').value.trim();
|
|
4130
|
-
if (!url) {
|
|
4557
|
+
if (!url) { showFieldError('tunnel-field-error', 'Enter a local URL'); return; }
|
|
4558
|
+
clearFieldError('tunnel-field-error');
|
|
4559
|
+
setBtnBusy(btn, true);
|
|
4131
4560
|
document.getElementById('tunnel-status').textContent = 'Creating tunnel…';
|
|
4132
4561
|
const r = await api('/api/tunnel', {
|
|
4133
4562
|
method: 'POST',
|
|
4134
4563
|
headers: { 'Content-Type': 'application/json' },
|
|
4135
4564
|
body: JSON.stringify({ url })
|
|
4136
4565
|
});
|
|
4566
|
+
setBtnBusy(btn, false);
|
|
4137
4567
|
if (r.success) {
|
|
4138
4568
|
tunnelList.push({ id: r.id, tunnelUrl: r.url, localUrl: url, alive: true, createdAt: Date.now() });
|
|
4139
4569
|
renderTunnels();
|
|
4140
4570
|
if (r.warning) {
|
|
4141
|
-
document.getElementById('tunnel-status').textContent =
|
|
4571
|
+
document.getElementById('tunnel-status').textContent = r.warning;
|
|
4142
4572
|
toast(r.warning, 'warning');
|
|
4143
4573
|
} else {
|
|
4144
4574
|
document.getElementById('tunnel-status').textContent = '';
|
|
@@ -4146,9 +4576,8 @@ async function createTunnel() {
|
|
|
4146
4576
|
}
|
|
4147
4577
|
} else {
|
|
4148
4578
|
document.getElementById('tunnel-status').textContent = '';
|
|
4149
|
-
|
|
4579
|
+
showFieldError('tunnel-field-error', r.error || 'Failed to create tunnel');
|
|
4150
4580
|
}
|
|
4151
|
-
if (btn) setTimeout(() => btn.dataset.busy = 'false', 2000);
|
|
4152
4581
|
}
|
|
4153
4582
|
|
|
4154
4583
|
async function stopTunnelById(id) {
|
|
@@ -4186,7 +4615,7 @@ function applyTheme(theme, save = true) {
|
|
|
4186
4615
|
document.documentElement.dataset.theme = resolved;
|
|
4187
4616
|
settings.theme = theme;
|
|
4188
4617
|
document.getElementById('s-theme').value = theme;
|
|
4189
|
-
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(); } });
|
|
4190
4619
|
const themeColor = getComputedStyle(document.body).getPropertyValue('--bg2').trim();
|
|
4191
4620
|
document.querySelector('meta[name="theme-color"]').content = themeColor;
|
|
4192
4621
|
if (save) saveSettings();
|
|
@@ -4241,6 +4670,64 @@ function toggleSidebar() {
|
|
|
4241
4670
|
}
|
|
4242
4671
|
}
|
|
4243
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
|
+
|
|
4244
4731
|
function updateSidebarNarrowClass() {
|
|
4245
4732
|
const sb = document.getElementById('sidebar');
|
|
4246
4733
|
if (!sb) return;
|
|
@@ -4474,9 +4961,8 @@ function moveTabToEnd(fromId) {
|
|
|
4474
4961
|
if (fromIdx === -1) return;
|
|
4475
4962
|
const [moved] = tabs.splice(fromIdx, 1);
|
|
4476
4963
|
tabs.push(moved);
|
|
4477
|
-
const bar = document.getElementById('tab-
|
|
4478
|
-
|
|
4479
|
-
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);
|
|
4480
4966
|
saveTabState();
|
|
4481
4967
|
}
|
|
4482
4968
|
|
|
@@ -4521,7 +5007,7 @@ function setupKeyboardShortcuts() {
|
|
|
4521
5007
|
if (e.key === 'Escape') {
|
|
4522
5008
|
const termMenu = document.getElementById('term-ctx-menu');
|
|
4523
5009
|
if (termMenu && termMenu.style.display !== 'none') {
|
|
4524
|
-
|
|
5010
|
+
hideTermCtxMenu();
|
|
4525
5011
|
return;
|
|
4526
5012
|
}
|
|
4527
5013
|
if (document.getElementById('ctx-menu').classList.contains('open')) {
|
|
@@ -4538,6 +5024,7 @@ function setupKeyboardShortcuts() {
|
|
|
4538
5024
|
else if (id === 'newfolder-overlay' || id === 'newfile-overlay' || id === 'rename-overlay') closeOverlay(id);
|
|
4539
5025
|
else if (id === 'image-viewer') closeImageViewer();
|
|
4540
5026
|
else if (id === 'conflict-overlay') closeOverlay(id);
|
|
5027
|
+
else closeOverlay(id);
|
|
4541
5028
|
});
|
|
4542
5029
|
}
|
|
4543
5030
|
});
|
|
@@ -4918,10 +5405,23 @@ function setupDragDrop() {
|
|
|
4918
5405
|
// ═══════════════════════════════════════════════════════
|
|
4919
5406
|
let lastFocusedElement = null;
|
|
4920
5407
|
let _focusTrapHandler = null;
|
|
5408
|
+
function openShortcuts() {
|
|
5409
|
+
openOverlay('shortcuts-overlay');
|
|
5410
|
+
document.getElementById('shortcuts-overlay').querySelector('.btn').focus();
|
|
5411
|
+
}
|
|
4921
5412
|
function openOverlay(id) {
|
|
4922
5413
|
lastFocusedElement = document.activeElement;
|
|
4923
5414
|
const overlay = document.getElementById(id);
|
|
4924
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
|
+
}
|
|
4925
5425
|
const modal = overlay.querySelector('.modal, input, button');
|
|
4926
5426
|
if (modal) setTimeout(() => modal.focus(), 50);
|
|
4927
5427
|
installFocusTrap(overlay);
|
|
@@ -4960,24 +5460,89 @@ function removeFocusTrap() {
|
|
|
4960
5460
|
}
|
|
4961
5461
|
}
|
|
4962
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
|
+
|
|
4963
5505
|
document.querySelectorAll('.overlay').forEach(o => {
|
|
4964
5506
|
o.addEventListener('click', e => {
|
|
4965
5507
|
if (e.target !== o) return;
|
|
4966
5508
|
if (o.id === 'sys-overlay') { closeSystemStats(); return; }
|
|
4967
5509
|
if (o.id === 'finder-overlay') { clearTimeout(finderTimer); }
|
|
5510
|
+
if (o.id === 'confirm-overlay') return; // dismiss handled by confirmDialog only
|
|
4968
5511
|
closeOverlay(o.id);
|
|
4969
5512
|
});
|
|
4970
5513
|
});
|
|
4971
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
|
+
|
|
4972
5534
|
function toast(msg, type = 'info') {
|
|
4973
5535
|
const el = document.createElement('div');
|
|
4974
5536
|
el.className = `toast ${type}`;
|
|
4975
|
-
el.
|
|
5537
|
+
el.innerHTML = (TOAST_ICONS[type] || '') + '<span></span>';
|
|
5538
|
+
el.querySelector('span').textContent = msg;
|
|
4976
5539
|
const mk = document.getElementById('mobile-keys');
|
|
4977
5540
|
if (mk && mk.style.display !== 'none' && window.getComputedStyle(mk).display !== 'none') {
|
|
4978
5541
|
el.style.marginBottom = 'var(--mobilekey-h)';
|
|
4979
5542
|
}
|
|
4980
|
-
document.getElementById('toast-container')
|
|
5543
|
+
const container = document.getElementById('toast-container');
|
|
5544
|
+
container.appendChild(el);
|
|
5545
|
+
while (container.children.length > 4) container.firstChild.remove();
|
|
4981
5546
|
setTimeout(() => el.remove(), 3000);
|
|
4982
5547
|
return el;
|
|
4983
5548
|
}
|
|
@@ -4987,6 +5552,7 @@ function updateConnStatus(connected) {
|
|
|
4987
5552
|
if (dot) {
|
|
4988
5553
|
dot.style.background = connected ? 'var(--green)' : 'var(--red)';
|
|
4989
5554
|
dot.title = connected ? 'Connected' : 'Disconnected';
|
|
5555
|
+
dot.setAttribute('aria-label', connected ? 'Connected' : 'Disconnected');
|
|
4990
5556
|
}
|
|
4991
5557
|
}
|
|
4992
5558
|
|
|
@@ -5028,11 +5594,14 @@ function setupSwipeGestures() {
|
|
|
5028
5594
|
let pullStartY = 0;
|
|
5029
5595
|
let pullOffset = 0;
|
|
5030
5596
|
const fileListWrap = document.getElementById('file-list-wrap');
|
|
5031
|
-
|
|
5597
|
+
const ptr = document.getElementById('ptr-indicator');
|
|
5598
|
+
if (fileListWrap && ptr) {
|
|
5032
5599
|
fileListWrap.addEventListener('touchstart', e => {
|
|
5033
5600
|
if (fileListWrap.scrollTop <= 0) {
|
|
5034
5601
|
pullStartY = e.touches[0].clientY;
|
|
5035
5602
|
pullOffset = 0;
|
|
5603
|
+
ptr.style.opacity = '0';
|
|
5604
|
+
ptr.classList.remove('release');
|
|
5036
5605
|
}
|
|
5037
5606
|
}, { passive: true });
|
|
5038
5607
|
fileListWrap.addEventListener('touchmove', e => {
|
|
@@ -5040,16 +5609,21 @@ function setupSwipeGestures() {
|
|
|
5040
5609
|
const dy = e.touches[0].clientY - pullStartY;
|
|
5041
5610
|
if (dy > 0 && fileListWrap.scrollTop <= 0) {
|
|
5042
5611
|
pullOffset = dy;
|
|
5043
|
-
|
|
5612
|
+
const t = Math.min(dy * 0.4, 60);
|
|
5613
|
+
fileListWrap.style.transform = `translateY(${t}px)`;
|
|
5044
5614
|
fileListWrap.style.transition = 'none';
|
|
5615
|
+
ptr.style.opacity = Math.min(1, dy / 40).toFixed(2);
|
|
5616
|
+
ptr.classList.toggle('release', pullOffset > 60);
|
|
5045
5617
|
}
|
|
5046
5618
|
}, { passive: true });
|
|
5047
5619
|
fileListWrap.addEventListener('touchend', () => {
|
|
5048
5620
|
if (pullOffset > 60) {
|
|
5621
|
+
ptr.classList.remove('release');
|
|
5049
5622
|
refreshFiles();
|
|
5050
5623
|
}
|
|
5051
5624
|
fileListWrap.style.transition = 'transform 0.3s ease';
|
|
5052
5625
|
fileListWrap.style.transform = '';
|
|
5626
|
+
ptr.style.opacity = '0';
|
|
5053
5627
|
pullStartY = 0;
|
|
5054
5628
|
pullOffset = 0;
|
|
5055
5629
|
setTimeout(() => { fileListWrap.style.transition = ''; }, 300);
|
|
@@ -5141,7 +5715,9 @@ function renderBookmarks() {
|
|
|
5141
5715
|
div.appendChild(bmPath);
|
|
5142
5716
|
const rmBtn = document.createElement('span');
|
|
5143
5717
|
rmBtn.className = 'bm-remove';
|
|
5144
|
-
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>';
|
|
5145
5721
|
rmBtn.title = 'Remove';
|
|
5146
5722
|
rmBtn.addEventListener('click', e => {
|
|
5147
5723
|
e.stopPropagation();
|
|
@@ -5314,7 +5890,9 @@ function renderCmdLib(filter) {
|
|
|
5314
5890
|
cmd.textContent = c.cmd;
|
|
5315
5891
|
const runBtn = document.createElement('button');
|
|
5316
5892
|
runBtn.className = 'cmd-lib-run';
|
|
5317
|
-
|
|
5893
|
+
runBtn.title = 'Run in terminal';
|
|
5894
|
+
runBtn.setAttribute('aria-label', 'Run in terminal');
|
|
5895
|
+
runBtn.setAttribute('aria-label', 'Run in terminal');
|
|
5318
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>';
|
|
5319
5897
|
runBtn.addEventListener('click', e => {
|
|
5320
5898
|
e.stopPropagation();
|
|
@@ -5328,7 +5906,8 @@ function renderCmdLib(filter) {
|
|
|
5328
5906
|
const delBtn = document.createElement('button');
|
|
5329
5907
|
delBtn.className = 'cmd-lib-del';
|
|
5330
5908
|
delBtn.title = 'Delete';
|
|
5331
|
-
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>';
|
|
5332
5911
|
delBtn.addEventListener('click', e => {
|
|
5333
5912
|
e.stopPropagation();
|
|
5334
5913
|
deleteCustomCmd(c.name, c.cmd);
|
|
@@ -5340,7 +5919,7 @@ function renderCmdLib(filter) {
|
|
|
5340
5919
|
const tab = getActiveTab();
|
|
5341
5920
|
if (!tab?.term) return;
|
|
5342
5921
|
tab.term.focus();
|
|
5343
|
-
|
|
5922
|
+
tab.term.paste(c.cmd);
|
|
5344
5923
|
toggleCmdLib();
|
|
5345
5924
|
});
|
|
5346
5925
|
section.appendChild(item);
|
|
@@ -5536,7 +6115,8 @@ async function renderCmdHist() {
|
|
|
5536
6115
|
const delBtn = document.createElement('button');
|
|
5537
6116
|
delBtn.className = 'cmd-hist-del';
|
|
5538
6117
|
delBtn.title = 'Remove';
|
|
5539
|
-
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>';
|
|
5540
6120
|
delBtn.addEventListener('click', e => { e.stopPropagation(); removeCmdHistItem(realIdx); });
|
|
5541
6121
|
item.appendChild(delBtn);
|
|
5542
6122
|
|
|
@@ -5544,7 +6124,7 @@ async function renderCmdHist() {
|
|
|
5544
6124
|
const tab = getActiveTab();
|
|
5545
6125
|
if (!tab?.term) return;
|
|
5546
6126
|
tab.term.focus();
|
|
5547
|
-
|
|
6127
|
+
tab.term.paste(h.cmd);
|
|
5548
6128
|
toggleCmdLib();
|
|
5549
6129
|
});
|
|
5550
6130
|
list.appendChild(item);
|
|
@@ -5677,6 +6257,10 @@ function showInstallBanner(isIOS) {
|
|
|
5677
6257
|
el.querySelector('.btn-primary').style.display = 'none';
|
|
5678
6258
|
el.querySelector('.btn-ghost').textContent = 'Dismiss';
|
|
5679
6259
|
}
|
|
6260
|
+
const mk = document.getElementById('mobile-keys');
|
|
6261
|
+
if (mk && window.getComputedStyle(mk).display !== 'none') {
|
|
6262
|
+
el.style.bottom = 'var(--mobilekey-h)';
|
|
6263
|
+
}
|
|
5680
6264
|
el.style.display = 'flex';
|
|
5681
6265
|
document.getElementById('toast-container').classList.add('banner-visible');
|
|
5682
6266
|
}
|