termbeam 1.2.10 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,23 @@
1
+ <div align="center">
2
+
1
3
  # TermBeam
2
4
 
3
5
  **Beam your terminal to any device.**
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/termbeam.svg)](https://www.npmjs.com/package/termbeam)
8
+ [![npm downloads](https://img.shields.io/npm/dm/termbeam.svg)](https://www.npmjs.com/package/termbeam)
6
9
  [![CI](https://github.com/dorlugasigal/TermBeam/actions/workflows/ci.yml/badge.svg)](https://github.com/dorlugasigal/TermBeam/actions/workflows/ci.yml)
7
10
  [![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/dorlugasigal/TermBeam/coverage-data/endpoint.json)](https://github.com/dorlugasigal/TermBeam/actions/workflows/ci.yml)
11
+ [![Node.js](https://img.shields.io/node/v/termbeam.svg)](https://nodejs.org/)
8
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
13
 
14
+ </div>
15
+
10
16
  TermBeam lets you access your terminal from a phone, tablet, or any browser — no SSH, no port forwarding, no config files. Run one command and scan the QR code.
11
17
 
12
18
  I built this because I kept needing to run quick commands on my dev machine while away from my desk, and SSH on a phone is painful. TermBeam gives you a real terminal with a touch-friendly UI that actually works on small screens.
13
19
 
14
- [Full documentation](https://dorlugasigal.github.io/TermBeam/)
20
+ [Full documentation](https://dorlugasigal.github.io/TermBeam/) · [Website](https://termbeam.pages.dev)
15
21
 
16
22
  https://github.com/user-attachments/assets/9dd4f3d7-f017-4314-9b3a-f6a5688e3671
17
23
 
@@ -106,6 +112,11 @@ termbeam [shell] [args...] # start with a specific shell (default: auto-d
106
112
  termbeam --port 8080 # custom port (default: 3456)
107
113
  termbeam --host 0.0.0.0 # allow LAN access (default: 127.0.0.1)
108
114
  termbeam --lan # shortcut for --host 0.0.0.0
115
+ termbeam service install # interactive PM2 service setup wizard
116
+ termbeam service uninstall # stop & remove PM2 service
117
+ termbeam service status # show PM2 service status
118
+ termbeam service logs # tail PM2 service logs
119
+ termbeam service restart # restart PM2 service
109
120
  ```
110
121
 
111
122
  | Flag | Description | Default |
@@ -121,6 +132,16 @@ termbeam --lan # shortcut for --host 0.0.0.0
121
132
  | `--host <addr>` | Bind address | `127.0.0.1` |
122
133
  | `--lan` | Bind to all interfaces (LAN access) | Off |
123
134
  | `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
135
+ | `-h, --help` | Show help | — |
136
+ | `-v, --version` | Show version | — |
137
+
138
+ | Subcommand | Description |
139
+ | ------------------- | ----------------------------- |
140
+ | `service install` | Interactive PM2 service setup |
141
+ | `service uninstall` | Stop & remove from PM2 |
142
+ | `service status` | Show PM2 service status |
143
+ | `service logs` | Tail PM2 service logs |
144
+ | `service restart` | Restart PM2 service |
124
145
 
125
146
  Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LOG_LEVEL`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
126
147
 
@@ -128,7 +149,7 @@ Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LO
128
149
 
129
150
  TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. By default, the server binds to `127.0.0.1` (localhost only). Use `--lan` or `--host 0.0.0.0` to allow LAN access, or `--no-tunnel` to disable the tunnel.
130
151
 
131
- Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. The QR code on startup embeds a share token for password-free login — the token is reusable within its 5-minute validity window, which handles tunnel proxy retries and link preview services. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header.
152
+ Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. Each QR code contains a single-use share token (5-minute expiry) for password-free login. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header.
132
153
 
133
154
  For the full threat model, safe usage guidance, and a quick safety checklist, see [SECURITY.md](SECURITY.md). For detailed security feature documentation, see the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/).
134
155
 
@@ -136,6 +157,10 @@ For the full threat model, safe usage guidance, and a quick safety checklist, se
136
157
 
137
158
  Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
138
159
 
160
+ ## Changelog
161
+
162
+ See [CHANGELOG.md](CHANGELOG.md) for version history.
163
+
139
164
  ## License
140
165
 
141
166
  [MIT](LICENSE)
package/bin/termbeam.js CHANGED
@@ -1,2 +1,27 @@
1
1
  #!/usr/bin/env node
2
- require('../src/server.js');
2
+
3
+ // Dispatch subcommands before loading the server
4
+ const subcommand = (process.argv[2] || '').toLowerCase();
5
+ if (subcommand === 'service') {
6
+ const { run } = require('../src/service');
7
+ run(process.argv.slice(3)).catch((err) => {
8
+ console.error(err.message);
9
+ process.exit(1);
10
+ });
11
+ } else {
12
+ const { createTermBeamServer } = require('../src/server.js');
13
+ const instance = createTermBeamServer();
14
+
15
+ process.on('SIGINT', () => {
16
+ console.log('\n[termbeam] Shutting down...');
17
+ instance.shutdown();
18
+ setTimeout(() => process.exit(0), 500).unref();
19
+ });
20
+ process.on('SIGTERM', () => {
21
+ console.log('\n[termbeam] Shutting down...');
22
+ instance.shutdown();
23
+ setTimeout(() => process.exit(0), 500).unref();
24
+ });
25
+
26
+ instance.start();
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.2.10",
3
+ "version": "1.4.0",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "start": "node bin/termbeam.js",
11
11
  "dev": "node bin/termbeam.js --generate-password",
12
12
  "test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')&&f!=='devtunnel-install.test.js').map(f=>'test/'+f)],{stdio:'inherit'})\"",
13
- "test:coverage": "c8 --exclude=src/tunnel.js --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')&&f!=='devtunnel-install.test.js').map(f=>'test/'+f)],{stdio:'inherit'})\"",
13
+ "test:coverage": "c8 --exclude=src/tunnel.js --exclude=src/devtunnel-install.js --exclude=test --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')&&f!=='devtunnel-install.test.js').map(f=>'test/'+f)],{stdio:'inherit'})\"",
14
14
  "prepare": "husky",
15
15
  "format": "prettier --write .",
16
16
  "lint": "node --check src/*.js bin/*.js",
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "author": "Dor Lugasi <dorlugasigal@gmail.com>",
40
40
  "license": "MIT",
41
- "homepage": "https://github.com/dorlugasigal/TermBeam",
41
+ "homepage": "https://termbeam.pages.dev",
42
42
  "repository": {
43
43
  "type": "git",
44
44
  "url": "https://github.com/dorlugasigal/TermBeam.git"
package/public/index.html CHANGED
@@ -55,6 +55,196 @@
55
55
  --shadow: rgba(0, 0, 0, 0.06);
56
56
  --overlay-bg: rgba(0, 0, 0, 0.4);
57
57
  }
58
+ [data-theme='monokai'] {
59
+ --bg: #272822;
60
+ --surface: #1e1f1c;
61
+ --border: #49483e;
62
+ --border-subtle: #5c5c4f;
63
+ --text: #f8f8f2;
64
+ --text-secondary: #a59f85;
65
+ --text-dim: #75715e;
66
+ --text-muted: #5a5854;
67
+ --accent: #a6e22e;
68
+ --accent-hover: #b8f53c;
69
+ --accent-active: #8acc16;
70
+ --danger: #f92672;
71
+ --danger-hover: #e0155d;
72
+ --success: #a6e22e;
73
+ --info: #a59f85;
74
+ --shadow: rgba(0, 0, 0, 0.3);
75
+ --overlay-bg: rgba(0, 0, 0, 0.75);
76
+ }
77
+ [data-theme='solarized-dark'] {
78
+ --bg: #002b36;
79
+ --surface: #073642;
80
+ --border: #586e75;
81
+ --border-subtle: #657b83;
82
+ --text: #839496;
83
+ --text-secondary: #657b83;
84
+ --text-dim: #586e75;
85
+ --text-muted: #4a5a62;
86
+ --accent: #268bd2;
87
+ --accent-hover: #379ce3;
88
+ --accent-active: #1a7abf;
89
+ --danger: #dc322f;
90
+ --danger-hover: #c8221f;
91
+ --success: #859900;
92
+ --info: #657b83;
93
+ --shadow: rgba(0, 0, 0, 0.25);
94
+ --overlay-bg: rgba(0, 0, 0, 0.75);
95
+ }
96
+ [data-theme='solarized-light'] {
97
+ --bg: #fdf6e3;
98
+ --surface: #eee8d5;
99
+ --border: #93a1a1;
100
+ --border-subtle: #839496;
101
+ --text: #657b83;
102
+ --text-secondary: #93a1a1;
103
+ --text-dim: #a0a0a0;
104
+ --text-muted: #b0b0b0;
105
+ --accent: #268bd2;
106
+ --accent-hover: #379ce3;
107
+ --accent-active: #1a7abf;
108
+ --danger: #dc322f;
109
+ --danger-hover: #c8221f;
110
+ --success: #859900;
111
+ --info: #93a1a1;
112
+ --shadow: rgba(0, 0, 0, 0.08);
113
+ --overlay-bg: rgba(0, 0, 0, 0.4);
114
+ }
115
+ [data-theme='nord'] {
116
+ --bg: #2e3440;
117
+ --surface: #3b4252;
118
+ --border: #434c5e;
119
+ --border-subtle: #4c566a;
120
+ --text: #d8dee9;
121
+ --text-secondary: #b0bac9;
122
+ --text-dim: #7b88a1;
123
+ --text-muted: #5c6a85;
124
+ --accent: #88c0d0;
125
+ --accent-hover: #9fd4e4;
126
+ --accent-active: #6aafbf;
127
+ --danger: #bf616a;
128
+ --danger-hover: #a84d57;
129
+ --success: #a3be8c;
130
+ --info: #b0bac9;
131
+ --shadow: rgba(0, 0, 0, 0.2);
132
+ --overlay-bg: rgba(0, 0, 0, 0.75);
133
+ }
134
+ [data-theme='dracula'] {
135
+ --bg: #282a36;
136
+ --surface: #343746;
137
+ --border: #44475a;
138
+ --border-subtle: #525568;
139
+ --text: #f8f8f2;
140
+ --text-secondary: #c1c4d2;
141
+ --text-dim: #8e92a4;
142
+ --text-muted: #6272a4;
143
+ --accent: #bd93f9;
144
+ --accent-hover: #d0b0ff;
145
+ --accent-active: #a77de7;
146
+ --danger: #ff5555;
147
+ --danger-hover: #e03d3d;
148
+ --success: #50fa7b;
149
+ --info: #c1c4d2;
150
+ --shadow: rgba(0, 0, 0, 0.25);
151
+ --overlay-bg: rgba(0, 0, 0, 0.75);
152
+ }
153
+ [data-theme='github-dark'] {
154
+ --bg: #0d1117;
155
+ --surface: #161b22;
156
+ --border: #30363d;
157
+ --border-subtle: #3d444d;
158
+ --text: #c9d1d9;
159
+ --text-secondary: #8b949e;
160
+ --text-dim: #6e7681;
161
+ --text-muted: #484f58;
162
+ --accent: #58a6ff;
163
+ --accent-hover: #79b8ff;
164
+ --accent-active: #388bfd;
165
+ --danger: #f85149;
166
+ --danger-hover: #da3633;
167
+ --success: #3fb950;
168
+ --info: #8b949e;
169
+ --shadow: rgba(0, 0, 0, 0.3);
170
+ --overlay-bg: rgba(0, 0, 0, 0.75);
171
+ }
172
+ [data-theme='one-dark'] {
173
+ --bg: #282c34;
174
+ --surface: #21252b;
175
+ --border: #3e4452;
176
+ --border-subtle: #4b5263;
177
+ --text: #abb2bf;
178
+ --text-secondary: #7f848e;
179
+ --text-dim: #5c6370;
180
+ --text-muted: #4b5263;
181
+ --accent: #61afef;
182
+ --accent-hover: #7dc0ff;
183
+ --accent-active: #4d9ede;
184
+ --danger: #e06c75;
185
+ --danger-hover: #c95c67;
186
+ --success: #98c379;
187
+ --info: #7f848e;
188
+ --shadow: rgba(0, 0, 0, 0.25);
189
+ --overlay-bg: rgba(0, 0, 0, 0.75);
190
+ }
191
+ [data-theme='catppuccin'] {
192
+ --bg: #1e1e2e;
193
+ --surface: #313244;
194
+ --border: #45475a;
195
+ --border-subtle: #585b70;
196
+ --text: #cdd6f4;
197
+ --text-secondary: #a6adc8;
198
+ --text-dim: #7f849c;
199
+ --text-muted: #585b70;
200
+ --accent: #89b4fa;
201
+ --accent-hover: #b4d0ff;
202
+ --accent-active: #5c9de3;
203
+ --danger: #f38ba8;
204
+ --danger-hover: #eb7c9d;
205
+ --success: #a6e3a1;
206
+ --info: #a6adc8;
207
+ --shadow: rgba(0, 0, 0, 0.2);
208
+ --overlay-bg: rgba(0, 0, 0, 0.75);
209
+ }
210
+ [data-theme='gruvbox'] {
211
+ --bg: #282828;
212
+ --surface: #3c3836;
213
+ --border: #504945;
214
+ --border-subtle: #665c54;
215
+ --text: #ebdbb2;
216
+ --text-secondary: #d5c4a1;
217
+ --text-dim: #a89984;
218
+ --text-muted: #7c6f64;
219
+ --accent: #83a598;
220
+ --accent-hover: #9dbfb4;
221
+ --accent-active: #6a8f8a;
222
+ --danger: #fb4934;
223
+ --danger-hover: #e33826;
224
+ --success: #b8bb26;
225
+ --info: #d5c4a1;
226
+ --shadow: rgba(0, 0, 0, 0.25);
227
+ --overlay-bg: rgba(0, 0, 0, 0.75);
228
+ }
229
+ [data-theme='night-owl'] {
230
+ --bg: #011627;
231
+ --surface: #0d2a45;
232
+ --border: #1d3b53;
233
+ --border-subtle: #264863;
234
+ --text: #d6deeb;
235
+ --text-secondary: #8badc1;
236
+ --text-dim: #5f7e97;
237
+ --text-muted: #3f5f7d;
238
+ --accent: #7fdbca;
239
+ --accent-hover: #9ff0e0;
240
+ --accent-active: #62c5b5;
241
+ --danger: #ef5350;
242
+ --danger-hover: #d83130;
243
+ --success: #addb67;
244
+ --info: #8badc1;
245
+ --shadow: rgba(0, 0, 0, 0.3);
246
+ --overlay-bg: rgba(0, 0, 0, 0.75);
247
+ }
58
248
  * {
59
249
  margin: 0;
60
250
  padding: 0;
@@ -116,10 +306,12 @@
116
306
  border-color: var(--border-subtle);
117
307
  background: var(--border);
118
308
  }
119
- .theme-toggle {
309
+ .theme-wrap {
120
310
  position: absolute;
121
311
  top: 16px;
122
312
  right: 16px;
313
+ }
314
+ .theme-toggle {
123
315
  background: none;
124
316
  border: 1px solid var(--border);
125
317
  color: var(--text-dim);
@@ -142,6 +334,47 @@
142
334
  border-color: var(--border-subtle);
143
335
  background: var(--border);
144
336
  }
337
+ .theme-picker {
338
+ display: none;
339
+ position: absolute;
340
+ top: calc(100% + 4px);
341
+ right: 0;
342
+ background: var(--surface);
343
+ border: 1px solid var(--border);
344
+ border-radius: 8px;
345
+ min-width: 160px;
346
+ padding: 4px 0;
347
+ z-index: 200;
348
+ box-shadow: 0 4px 12px var(--shadow);
349
+ }
350
+ .theme-picker.open {
351
+ display: block;
352
+ }
353
+ .theme-option {
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 8px;
357
+ padding: 7px 12px;
358
+ cursor: pointer;
359
+ font-size: 13px;
360
+ color: var(--text);
361
+ transition: background 0.1s;
362
+ white-space: nowrap;
363
+ }
364
+ .theme-option:hover {
365
+ background: var(--border);
366
+ }
367
+ .theme-option.active {
368
+ color: var(--accent);
369
+ }
370
+ .theme-swatch {
371
+ width: 14px;
372
+ height: 14px;
373
+ border-radius: 50%;
374
+ display: inline-block;
375
+ flex-shrink: 0;
376
+ border: 1px solid rgba(128, 128, 128, 0.3);
377
+ }
145
378
 
146
379
  .sessions-list {
147
380
  padding: 16px;
@@ -691,28 +924,66 @@
691
924
  <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
692
925
  </svg>
693
926
  </button>
694
- <button class="theme-toggle" id="theme-toggle" title="Toggle theme">
695
- <svg
696
- width="16"
697
- height="16"
698
- viewBox="0 0 24 24"
699
- fill="none"
700
- stroke="currentColor"
701
- stroke-width="2"
702
- stroke-linecap="round"
703
- stroke-linejoin="round"
704
- >
705
- <circle cx="12" cy="12" r="5" />
706
- <line x1="12" y1="1" x2="12" y2="3" />
707
- <line x1="12" y1="21" x2="12" y2="23" />
708
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
709
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
710
- <line x1="1" y1="12" x2="3" y2="12" />
711
- <line x1="21" y1="12" x2="23" y2="12" />
712
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
713
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
714
- </svg>
715
- </button>
927
+ <div class="theme-wrap" id="theme-wrap">
928
+ <button class="theme-toggle" id="theme-toggle" title="Switch theme">
929
+ <svg
930
+ width="16"
931
+ height="16"
932
+ viewBox="0 0 24 24"
933
+ fill="none"
934
+ stroke="currentColor"
935
+ stroke-width="2"
936
+ stroke-linecap="round"
937
+ stroke-linejoin="round"
938
+ >
939
+ <circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
940
+ <circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
941
+ <circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
942
+ <circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
943
+ <path
944
+ d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
945
+ />
946
+ </svg>
947
+ </button>
948
+ <div class="theme-picker" id="theme-picker">
949
+ <div class="theme-option" data-theme-option="dark">
950
+ <span class="theme-swatch" style="background: #1e1e1e"></span>Dark
951
+ </div>
952
+ <div class="theme-option" data-theme-option="light">
953
+ <span class="theme-swatch" style="background: #ffffff"></span>Light
954
+ </div>
955
+ <div class="theme-option" data-theme-option="monokai">
956
+ <span class="theme-swatch" style="background: #272822"></span>Monokai
957
+ </div>
958
+ <div class="theme-option" data-theme-option="solarized-dark">
959
+ <span class="theme-swatch" style="background: #002b36"></span>Solarized Dark
960
+ </div>
961
+ <div class="theme-option" data-theme-option="solarized-light">
962
+ <span class="theme-swatch" style="background: #fdf6e3"></span>Solarized Light
963
+ </div>
964
+ <div class="theme-option" data-theme-option="nord">
965
+ <span class="theme-swatch" style="background: #2e3440"></span>Nord
966
+ </div>
967
+ <div class="theme-option" data-theme-option="dracula">
968
+ <span class="theme-swatch" style="background: #282a36"></span>Dracula
969
+ </div>
970
+ <div class="theme-option" data-theme-option="github-dark">
971
+ <span class="theme-swatch" style="background: #0d1117"></span>GitHub Dark
972
+ </div>
973
+ <div class="theme-option" data-theme-option="one-dark">
974
+ <span class="theme-swatch" style="background: #282c34"></span>One Dark
975
+ </div>
976
+ <div class="theme-option" data-theme-option="catppuccin">
977
+ <span class="theme-swatch" style="background: #1e1e2e"></span>Catppuccin
978
+ </div>
979
+ <div class="theme-option" data-theme-option="gruvbox">
980
+ <span class="theme-swatch" style="background: #282828"></span>Gruvbox
981
+ </div>
982
+ <div class="theme-option" data-theme-option="night-owl">
983
+ <span class="theme-swatch" style="background: #011627"></span>Night Owl
984
+ </div>
985
+ </div>
986
+ </div>
716
987
  </div>
717
988
 
718
989
  <div class="sessions-list" id="sessions-list"></div>
@@ -852,24 +1123,46 @@
852
1123
 
853
1124
  <script>
854
1125
  // Theme
1126
+ const THEMES = [
1127
+ { id: 'dark', name: 'Dark', bg: '#1e1e1e' },
1128
+ { id: 'light', name: 'Light', bg: '#f3f3f3' },
1129
+ { id: 'monokai', name: 'Monokai', bg: '#272822' },
1130
+ { id: 'solarized-dark', name: 'Solarized Dark', bg: '#002b36' },
1131
+ { id: 'solarized-light', name: 'Solarized Light', bg: '#fdf6e3' },
1132
+ { id: 'nord', name: 'Nord', bg: '#2e3440' },
1133
+ { id: 'dracula', name: 'Dracula', bg: '#282a36' },
1134
+ { id: 'github-dark', name: 'GitHub Dark', bg: '#0d1117' },
1135
+ { id: 'one-dark', name: 'One Dark', bg: '#282c34' },
1136
+ { id: 'catppuccin', name: 'Catppuccin', bg: '#1e1e2e' },
1137
+ { id: 'gruvbox', name: 'Gruvbox', bg: '#282828' },
1138
+ { id: 'night-owl', name: 'Night Owl', bg: '#011627' },
1139
+ ];
855
1140
  function getTheme() {
856
1141
  return localStorage.getItem('termbeam-theme') || 'dark';
857
1142
  }
858
1143
  function applyTheme(theme) {
859
1144
  document.documentElement.setAttribute('data-theme', theme);
860
- document.querySelector('meta[name="theme-color"]').content =
861
- theme === 'light' ? '#f3f3f3' : '#1e1e1e';
862
- const btn = document.getElementById('theme-toggle');
863
- if (btn)
864
- btn.innerHTML =
865
- theme === 'light'
866
- ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
867
- : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
1145
+ const t = THEMES.find((x) => x.id === theme) || THEMES[0];
1146
+ document.querySelector('meta[name="theme-color"]').content = t.bg;
868
1147
  localStorage.setItem('termbeam-theme', theme);
1148
+ document.querySelectorAll('.theme-option').forEach((el) => {
1149
+ el.classList.toggle('active', el.dataset.themeOption === theme);
1150
+ });
869
1151
  }
870
1152
  applyTheme(getTheme());
871
- document.getElementById('theme-toggle').addEventListener('click', () => {
872
- applyTheme(getTheme() === 'light' ? 'dark' : 'light');
1153
+ document.getElementById('theme-toggle').addEventListener('click', (e) => {
1154
+ e.stopPropagation();
1155
+ document.getElementById('theme-picker').classList.toggle('open');
1156
+ });
1157
+ document.addEventListener('click', () => {
1158
+ document.getElementById('theme-picker').classList.remove('open');
1159
+ });
1160
+ document.querySelectorAll('.theme-option').forEach((el) => {
1161
+ el.addEventListener('click', (e) => {
1162
+ e.stopPropagation();
1163
+ applyTheme(el.dataset.themeOption);
1164
+ document.getElementById('theme-picker').classList.remove('open');
1165
+ });
873
1166
  });
874
1167
 
875
1168
  const listEl = document.getElementById('sessions-list');