@xulongzhe/clawbench-desktop-linux-x64-payload 0.102.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.
Files changed (120) hide show
  1. package/package.json +10 -0
  2. package/payload.json +4 -0
  3. package/resources/app.asar +0 -0
  4. package/resources/app.asar.unpacked/node_modules/cpu-features/.eslintrc.js +5 -0
  5. package/resources/app.asar.unpacked/node_modules/cpu-features/LICENSE +19 -0
  6. package/resources/app.asar.unpacked/node_modules/cpu-features/build/Release/cpufeatures.node +0 -0
  7. package/resources/app.asar.unpacked/node_modules/cpu-features/build/deps/cpu_features/cpu_features.Makefile +6 -0
  8. package/resources/app.asar.unpacked/node_modules/cpu-features/buildcheck.gypi +19 -0
  9. package/resources/app.asar.unpacked/node_modules/cpu-features/buildcheck.js +32 -0
  10. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/.clang-format +4 -0
  11. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/.dockerignore +31 -0
  12. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +21 -0
  13. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +329 -0
  14. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +261 -0
  15. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +23 -0
  16. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/LICENSE +230 -0
  17. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/README.md +272 -0
  18. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/WORKSPACE +19 -0
  19. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +5 -0
  20. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +11 -0
  21. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +3 -0
  22. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +3 -0
  23. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/README.md +30 -0
  24. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +252 -0
  25. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +40 -0
  26. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +64 -0
  27. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +312 -0
  28. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +7 -0
  29. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +48 -0
  30. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +34 -0
  31. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +22 -0
  32. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +11 -0
  33. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +107 -0
  34. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +15 -0
  35. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +105 -0
  36. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +54 -0
  37. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +384 -0
  38. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +259 -0
  39. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +121 -0
  40. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +74 -0
  41. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +149 -0
  42. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +72 -0
  43. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +108 -0
  44. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +288 -0
  45. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +40 -0
  46. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +37 -0
  47. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +39 -0
  48. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +240 -0
  49. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +49 -0
  50. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +110 -0
  51. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +70 -0
  52. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +60 -0
  53. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +4 -0
  54. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +205 -0
  55. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +320 -0
  56. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +12 -0
  57. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +456 -0
  58. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +165 -0
  59. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +75 -0
  60. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +490 -0
  61. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +79 -0
  62. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/copy.inl +19 -0
  63. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +86 -0
  64. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +26 -0
  65. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/equals.inl +22 -0
  66. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +62 -0
  67. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +169 -0
  68. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +88 -0
  69. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +79 -0
  70. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +82 -0
  71. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +138 -0
  72. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +212 -0
  73. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +93 -0
  74. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +163 -0
  75. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +111 -0
  76. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +120 -0
  77. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +2055 -0
  78. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +68 -0
  79. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +58 -0
  80. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +57 -0
  81. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +58 -0
  82. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +132 -0
  83. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/string_view.c +192 -0
  84. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +461 -0
  85. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +114 -0
  86. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +61 -0
  87. package/resources/app.asar.unpacked/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +31 -0
  88. package/resources/app.asar.unpacked/node_modules/cpu-features/lib/index.js +5 -0
  89. package/resources/app.asar.unpacked/node_modules/cpu-features/package.json +28 -0
  90. package/resources/app.asar.unpacked/node_modules/ssh2/.eslintignore +4 -0
  91. package/resources/app.asar.unpacked/node_modules/ssh2/.eslintrc.js +5 -0
  92. package/resources/app.asar.unpacked/node_modules/ssh2/LICENSE +19 -0
  93. package/resources/app.asar.unpacked/node_modules/ssh2/SFTP.md +413 -0
  94. package/resources/app.asar.unpacked/node_modules/ssh2/install.js +27 -0
  95. package/resources/app.asar.unpacked/node_modules/ssh2/lib/Channel.js +295 -0
  96. package/resources/app.asar.unpacked/node_modules/ssh2/lib/agent.js +1123 -0
  97. package/resources/app.asar.unpacked/node_modules/ssh2/lib/client.js +2176 -0
  98. package/resources/app.asar.unpacked/node_modules/ssh2/lib/http-agents.js +84 -0
  99. package/resources/app.asar.unpacked/node_modules/ssh2/lib/index.js +44 -0
  100. package/resources/app.asar.unpacked/node_modules/ssh2/lib/keygen.js +582 -0
  101. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/Protocol.js +2136 -0
  102. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/SFTP.js +4052 -0
  103. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/constants.js +356 -0
  104. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node +0 -0
  105. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/crypto/poly1305.js +43 -0
  106. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/crypto.js +1602 -0
  107. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/handlers.js +16 -0
  108. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/handlers.misc.js +1285 -0
  109. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/kex.js +1908 -0
  110. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/keyParser.js +1484 -0
  111. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/node-fs-compat.js +115 -0
  112. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/utils.js +356 -0
  113. package/resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/zlib.js +255 -0
  114. package/resources/app.asar.unpacked/node_modules/ssh2/lib/server.js +1380 -0
  115. package/resources/app.asar.unpacked/node_modules/ssh2/lib/utils.js +336 -0
  116. package/resources/app.asar.unpacked/node_modules/ssh2/package.json +32 -0
  117. package/resources/app.asar.unpacked/node_modules/ssh2/util/build_pagent.bat +2 -0
  118. package/resources/app.asar.unpacked/node_modules/ssh2/util/pagent.c +88 -0
  119. package/resources/login.html +1777 -0
  120. package/resources/logo.png +0 -0
@@ -0,0 +1,1777 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>ClawBench</title>
7
+ <script>
8
+ // i18n — loaded before DOM to avoid flash of wrong-language text
9
+ var I18N = {
10
+ en: {
11
+ slogan: 'From Terminal to Desktop',
12
+ subtitle: 'AI Workstation for Desktop',
13
+ password_label: 'Password',
14
+ password_placeholder: 'Password',
15
+ connect: 'Connect',
16
+ protocol: 'Protocol',
17
+ server_address: 'Server Address',
18
+ add_connect: 'Add & Connect',
19
+ cancel: 'Cancel',
20
+ add_server: 'Add Server',
21
+ confirm: 'Confirm',
22
+ delete: 'Delete',
23
+ delete_server_title: 'Delete Server',
24
+ delete_server_msg: 'Delete this server?',
25
+ saved_servers: 'Saved Servers',
26
+ authenticating: 'Authenticating...',
27
+ enter_password: 'Please enter password',
28
+ enter_address: 'Please enter server address',
29
+ connection_failed: 'Connection failed: ',
30
+ connect_error: 'Unable to connect. Check address and network.',
31
+ wrong_password: 'Wrong password',
32
+ too_many_attempts: 'Too many attempts'
33
+ },
34
+ zh: {
35
+ slogan: '从终端到桌面',
36
+ subtitle: '为桌面端打造的 AI 工作台',
37
+ password_label: '登录密码',
38
+ password_placeholder: '密码',
39
+ connect: '连 接',
40
+ protocol: '协议',
41
+ server_address: '服务器地址',
42
+ add_connect: '添 加 并 连 接',
43
+ cancel: '取 消',
44
+ add_server: '添加服务器',
45
+ confirm: '确认',
46
+ delete: '删除',
47
+ delete_server_title: '删除服务器',
48
+ delete_server_msg: '删除此服务器?',
49
+ saved_servers: '已保存服务器',
50
+ authenticating: '认证中...',
51
+ enter_password: '请输入密码',
52
+ enter_address: '请输入服务器地址',
53
+ connection_failed: '连接失败: ',
54
+ connect_error: '无法连接到服务器,请检查地址和网络。',
55
+ wrong_password: '密码错误',
56
+ too_many_attempts: '尝试次数过多'
57
+ }
58
+ };
59
+ var LANG = 'en';
60
+ try { LANG = ClawBenchNative.getLanguage(); } catch(e) {}
61
+ if (!I18N[LANG]) LANG = 'en';
62
+ function t(key) { return I18N[LANG][key] || I18N['en'][key] || key; }
63
+ // Follow the main interface theme when available.
64
+ try {
65
+ // Must list every dark theme in web/src/utils/themeMeta.ts. A missing ID
66
+ // makes the page treat a dark theme as light (wrong data-theme-base, and
67
+ // unreadable text over the splash backdrop). Kept in sync by
68
+ // desktop/src/shared/theme.test.ts.
69
+ var DARK_IDS = ['ayu-dark','bluloco-dark','catppuccin-mocha','dark-plus','dracula','everforest-dark','github-dark','gruvbox-dark','high-contrast-dark','kanagawa','material-darker','monokai','night-owl','nord','one-dark-pro','rose-pine','solarized-dark','solarized-deep','tokyo-night','vitesse-dark'];
70
+ var LIGHT_IDS = ['alabaster','ayu-light','bluloco-light','catppuccin-latte','everforest-light','github-light','gruvbox-light','high-contrast-light','light-modern','light-plus','material-lighter','nord-light','one-light','quiet-light','solarized-light','vitesse-light'];
71
+ // Every colour variable is declared inside a [data-theme="<id>"] block, so
72
+ // an unrecognised value leaves them ALL undefined and the page renders with
73
+ // transparent inputs and buttons. This happens on a fresh install before
74
+ // any setTheme call, and on older builds that persisted a collapsed
75
+ // 'dark'/'light'. Fall back to a real ID rather than trusting the value.
76
+ var THEME = (ClawBenchNative.getTheme && ClawBenchNative.getTheme()) || '';
77
+ if (DARK_IDS.indexOf(THEME) < 0 && LIGHT_IDS.indexOf(THEME) < 0) {
78
+ // Keep the colour scheme the caller implied, so a light-mode user is not
79
+ // flipped to a dark page (and vice versa).
80
+ THEME = (THEME === 'light') ? 'github-light' : 'github-dark';
81
+ }
82
+ document.documentElement.setAttribute('data-theme', THEME);
83
+ var BASE = DARK_IDS.indexOf(THEME) >= 0 ? 'dark' : 'light';
84
+ document.documentElement.setAttribute('data-theme-base', BASE);
85
+ // Sync the resolved theme back to native. Without this, a first launch (or
86
+ // a launch where native holds no value) leaves the store empty, so the next
87
+ // launch cannot follow the main interface. Android does the same.
88
+ if (ClawBenchNative.setTheme) ClawBenchNative.setTheme(THEME);
89
+ } catch(e) {}
90
+ </script>
91
+ <style>
92
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
93
+
94
+ :root {
95
+ --server-indicator-size: 8px;
96
+ }
97
+
98
+ /* ── GitHub Light ─────────────────────────────────────────── */
99
+ [data-theme="github-light"] {
100
+ --bg-primary: #ffffff;
101
+ --bg-secondary: #f8f9fa;
102
+ --bg-tertiary: #e9ecef;
103
+ --text-primary: #212529;
104
+ --text-muted: #6c757d;
105
+ --text-hint: #6c757d;
106
+ --accent-color: #4a90d9;
107
+ --accent-hover: #3a7bc8;
108
+ --border-color: #dee2e6;
109
+ --color-red: #dc2626;
110
+ --accent-rgb: 74, 144, 217;
111
+ --grid-color: rgba(0, 0, 0, 0.06);
112
+ --gradient-color: rgba(74, 144, 217, 0.06);
113
+ }
114
+
115
+ /* ── GitHub Dark ──────────────────────────────────────────── */
116
+ [data-theme="github-dark"] {
117
+ --bg-primary: #0d1117;
118
+ --bg-secondary: #161b22;
119
+ --bg-tertiary: #21262d;
120
+ --text-primary: #c9d1d9;
121
+ --text-muted: #6e7681;
122
+ --text-hint: #6e7681;
123
+ --accent-color: #58a6ff;
124
+ --accent-hover: #79b8ff;
125
+ --border-color: #30363d;
126
+ --color-red: #f85149;
127
+ --accent-rgb: 88, 166, 255;
128
+ --grid-color: rgba(48, 54, 61, 0.3);
129
+ --gradient-color: rgba(88, 166, 255, 0.08);
130
+ }
131
+
132
+ /* ── One Dark Pro ─────────────────────────────────────────── */
133
+ [data-theme="one-dark-pro"] {
134
+ --bg-primary: #282c34;
135
+ --bg-secondary: #21252b;
136
+ --bg-tertiary: #2c313a;
137
+ --text-primary: #abb2bf;
138
+ --text-muted: #8c93a1;
139
+ --text-hint: #8c93a1;
140
+ --accent-color: #61afef;
141
+ --accent-hover: #82c4f4;
142
+ --border-color: #3e4451;
143
+ --color-red: #e06c75;
144
+ --accent-rgb: 97, 175, 239;
145
+ --grid-color: rgba(62, 68, 81, 0.3);
146
+ --gradient-color: rgba(97, 175, 239, 0.06);
147
+ }
148
+
149
+ /* ── Catppuccin Mocha ─────────────────────────────────────── */
150
+ [data-theme="catppuccin-mocha"] {
151
+ --bg-primary: #1e1e2e;
152
+ --bg-secondary: #181825;
153
+ --bg-tertiary: #313244;
154
+ --text-primary: #cdd6f4;
155
+ --text-muted: #6c7086;
156
+ --text-hint: #6c7086;
157
+ --accent-color: #89b4fa;
158
+ --accent-hover: #b4befe;
159
+ --border-color: #45475a;
160
+ --color-red: #f38ba8;
161
+ --accent-rgb: 137, 180, 250;
162
+ --grid-color: rgba(69, 71, 90, 0.3);
163
+ --gradient-color: rgba(137, 180, 250, 0.06);
164
+ }
165
+
166
+ /* ── Catppuccin Latte ─────────────────────────────────────── */
167
+ [data-theme="catppuccin-latte"] {
168
+ --bg-primary: #eff1f5;
169
+ --bg-secondary: #e6e9ef;
170
+ --bg-tertiary: #ccd0da;
171
+ --text-primary: #4c4f69;
172
+ --text-muted: #8c8fa1;
173
+ --text-hint: #8c8fa1;
174
+ --accent-color: #1e66f5;
175
+ --accent-hover: #2a7de6;
176
+ --border-color: #bcc0cc;
177
+ --color-red: #d20f39;
178
+ --accent-rgb: 30, 102, 245;
179
+ --grid-color: rgba(0, 0, 0, 0.06);
180
+ --gradient-color: rgba(30, 102, 245, 0.06);
181
+ }
182
+
183
+ /* ── Dracula ──────────────────────────────────────────────── */
184
+ [data-theme="dracula"] {
185
+ --bg-primary: #282a36;
186
+ --bg-secondary: #21222c;
187
+ --bg-tertiary: #343746;
188
+ --text-primary: #f8f8f2;
189
+ --text-muted: #6272a4;
190
+ --text-hint: #6272a4;
191
+ --accent-color: #bd93f9;
192
+ --accent-hover: #caa8fb;
193
+ --border-color: #44475a;
194
+ --color-red: #ff5555;
195
+ --accent-rgb: 189, 147, 249;
196
+ --grid-color: rgba(68, 71, 90, 0.3);
197
+ --gradient-color: rgba(189, 147, 249, 0.06);
198
+ }
199
+
200
+ /* ── Nord ─────────────────────────────────────────────────── */
201
+ [data-theme="nord"] {
202
+ --bg-primary: #171e27;
203
+ --bg-secondary: #202833;
204
+ --bg-tertiary: #2a3441;
205
+ --text-primary: #e6ecf4;
206
+ --text-muted: #7f8fa5;
207
+ --text-hint: #7f8fa5;
208
+ --accent-color: #6cb2f0;
209
+ --accent-hover: #82c3fa;
210
+ --border-color: #38434f;
211
+ --color-red: #e06c75;
212
+ --accent-rgb: 108, 178, 240;
213
+ --grid-color: rgba(56, 67, 79, 0.35);
214
+ --gradient-color: rgba(108, 178, 240, 0.08);
215
+ }
216
+
217
+ /* ── Tokyo Night ──────────────────────────────────────────── */
218
+ [data-theme="tokyo-night"] {
219
+ --bg-primary: #1a1b26;
220
+ --bg-secondary: #16161e;
221
+ --bg-tertiary: #292e42;
222
+ --text-primary: #c0caf5;
223
+ --text-muted: #7f87af;
224
+ --text-hint: #7f87af;
225
+ --accent-color: #7aa2f7;
226
+ --accent-hover: #89b4fa;
227
+ --border-color: #292e42;
228
+ --color-red: #f7768e;
229
+ --accent-rgb: 122, 162, 247;
230
+ --grid-color: rgba(41, 46, 66, 0.3);
231
+ --gradient-color: rgba(122, 162, 247, 0.06);
232
+ }
233
+
234
+ /* ── Solarized Dark ───────────────────────────────────────── */
235
+ [data-theme="solarized-dark"] {
236
+ --bg-primary: #002b36;
237
+ --bg-secondary: #0a3541;
238
+ --bg-tertiary: #12434f;
239
+ --text-primary: #a0b0b4;
240
+ --text-muted: #677f86;
241
+ --text-hint: #677f86;
242
+ --accent-color: #2e9fd8;
243
+ --accent-hover: #48b5ea;
244
+ --border-color: #1d4f5e;
245
+ --color-red: #dc4949;
246
+ --accent-rgb: 46, 159, 216;
247
+ --grid-color: rgba(29, 79, 94, 0.35);
248
+ --gradient-color: rgba(46, 159, 216, 0.08);
249
+ }
250
+
251
+ /* ── Solarized Deep ───────────────────────────────────────── */
252
+ [data-theme="solarized-deep"] {
253
+ --bg-primary: #0c141d;
254
+ --bg-secondary: #15212b;
255
+ --bg-tertiary: #1d2b37;
256
+ --text-primary: #dce5ec;
257
+ --text-muted: #7d8ea0;
258
+ --text-hint: #7d8ea0;
259
+ --accent-color: #3bb8e0;
260
+ --accent-hover: #4fc8f0;
261
+ --border-color: #2a3a48;
262
+ --color-red: #e06262;
263
+ --accent-rgb: 59, 184, 224;
264
+ --grid-color: rgba(42, 58, 72, 0.35);
265
+ --gradient-color: rgba(59, 184, 224, 0.08);
266
+ }
267
+
268
+ /* ── Solarized Light ──────────────────────────────────────── */
269
+ [data-theme="solarized-light"] {
270
+ --bg-primary: #fdf6e3;
271
+ --bg-secondary: #eee8d5;
272
+ --bg-tertiary: #e0dcc8;
273
+ --text-primary: #657b83;
274
+ --text-muted: #93a1a1;
275
+ --text-hint: #93a1a1;
276
+ --accent-color: #268bd2;
277
+ --accent-hover: #2aa198;
278
+ --border-color: #eee8d5;
279
+ --color-red: #dc322f;
280
+ --accent-rgb: 38, 139, 210;
281
+ --grid-color: rgba(0, 0, 0, 0.06);
282
+ --gradient-color: rgba(38, 139, 210, 0.06);
283
+ }
284
+
285
+ /* ── Gruvbox Dark ─────────────────────────────────────────── */
286
+ [data-theme="gruvbox-dark"] {
287
+ --bg-primary: #282828;
288
+ --bg-secondary: #1d2021;
289
+ --bg-tertiary: #3c3836;
290
+ --text-primary: #ebdbb2;
291
+ --text-muted: #9d9188;
292
+ --text-hint: #9d9188;
293
+ --accent-color: #fe8019;
294
+ --accent-hover: #fabd2f;
295
+ --border-color: #3c3836;
296
+ --color-red: #fb4934;
297
+ --accent-rgb: 254, 128, 25;
298
+ --grid-color: rgba(60, 56, 54, 0.4);
299
+ --gradient-color: rgba(254, 128, 25, 0.06);
300
+ }
301
+
302
+ /* ── Gruvbox Light ────────────────────────────────────────── */
303
+ [data-theme="gruvbox-light"] {
304
+ --bg-primary: #fbf1c7;
305
+ --bg-secondary: #f2e5bc;
306
+ --bg-tertiary: #ebdbb2;
307
+ --text-primary: #3c3836;
308
+ --text-muted: #928374;
309
+ --text-hint: #928374;
310
+ --accent-color: #af3a03;
311
+ --accent-hover: #d65d0e;
312
+ --border-color: #d5c4a1;
313
+ --color-red: #9d0006;
314
+ --accent-rgb: 175, 58, 3;
315
+ --grid-color: rgba(0, 0, 0, 0.06);
316
+ --gradient-color: rgba(175, 58, 3, 0.06);
317
+ }
318
+
319
+ /* ── High Contrast Dark ───────────────────────────────────── */
320
+ [data-theme="high-contrast-dark"] {
321
+ --bg-primary: #000000;
322
+ --bg-secondary: #0a0a0a;
323
+ --bg-tertiary: #1a1a1a;
324
+ --text-primary: #ffffff;
325
+ --text-muted: #a0a0a0;
326
+ --text-hint: #a0a0a0;
327
+ --accent-color: #00ccff;
328
+ --accent-hover: #33ddff;
329
+ --border-color: #555555;
330
+ --color-red: #ff3333;
331
+ --accent-rgb: 0, 204, 255;
332
+ --grid-color: rgba(85, 85, 85, 0.3);
333
+ --gradient-color: rgba(0, 204, 255, 0.06);
334
+ }
335
+
336
+ /* ── High Contrast Light ──────────────────────────────────── */
337
+ [data-theme="high-contrast-light"] {
338
+ --bg-primary: #ffffff;
339
+ --bg-secondary: #f5f5f5;
340
+ --bg-tertiary: #e0e0e0;
341
+ --text-primary: #000000;
342
+ --text-muted: #444444;
343
+ --text-hint: #444444;
344
+ --accent-color: #0055cc;
345
+ --accent-hover: #0044aa;
346
+ --border-color: #000000;
347
+ --color-red: #cc0000;
348
+ --accent-rgb: 0, 85, 204;
349
+ --grid-color: rgba(0, 0, 0, 0.1);
350
+ --gradient-color: rgba(0, 85, 204, 0.06);
351
+ }
352
+
353
+ /* ── Night Owl ──────────────────────────────────── */
354
+ [data-theme="night-owl"] {
355
+ --bg-primary: #011627;
356
+ --bg-secondary: #001122;
357
+ --bg-tertiary: #0b253a;
358
+ --text-primary: #d6deeb;
359
+ --text-muted: #5f7e97;
360
+ --text-hint: #5f7e97;
361
+ --accent-color: #82aaff;
362
+ --accent-hover: #addbff;
363
+ --border-color: #1b3b5b;
364
+ --color-red: #ef5350;
365
+ --accent-rgb: 130, 170, 255;
366
+ --grid-color: rgba(27, 59, 91, 0.3);
367
+ --gradient-color: rgba(130, 170, 255, 0.06);
368
+ }
369
+
370
+ /* ── Ayu Dark ──────────────────────────────────── */
371
+ [data-theme="ayu-dark"] {
372
+ --bg-primary: #0a0e14;
373
+ --bg-secondary: #0d1017;
374
+ --bg-tertiary: #131721;
375
+ --text-primary: #b3b1ad;
376
+ --text-muted: #626a73;
377
+ --text-hint: #626a73;
378
+ --accent-color: #e6b450;
379
+ --accent-hover: #f0c674;
380
+ --border-color: #1a1f29;
381
+ --color-red: #f07178;
382
+ --accent-rgb: 230, 180, 80;
383
+ --grid-color: rgba(26, 31, 41, 0.3);
384
+ --gradient-color: rgba(230, 180, 80, 0.06);
385
+ }
386
+
387
+ /* ── Vitesse Dark ──────────────────────────────────── */
388
+ [data-theme="vitesse-dark"] {
389
+ --bg-primary: #121212;
390
+ --bg-secondary: #181818;
391
+ --bg-tertiary: #222222;
392
+ --text-primary: #dbd7ca;
393
+ --text-muted: #758575;
394
+ --text-hint: #758575;
395
+ --accent-color: #4d9375;
396
+ --accent-hover: #6aa88a;
397
+ --border-color: #2e2e2e;
398
+ --color-red: #cb7676;
399
+ --accent-rgb: 77, 147, 117;
400
+ --grid-color: rgba(46, 46, 46, 0.3);
401
+ --gradient-color: rgba(77, 147, 117, 0.06);
402
+ }
403
+
404
+ [data-theme="rose-pine"] {
405
+ --bg-primary: #191724;
406
+ --bg-secondary: #1f1d2e;
407
+ --bg-tertiary: #26233a;
408
+ --text-primary: #e0def4;
409
+ --text-muted: #6e6a86;
410
+ --text-hint: #6e6a86;
411
+ --accent-color: #ebbcba;
412
+ --accent-hover: #f6c5c2;
413
+ --border-color: #413f5e;
414
+ --color-red: #eb6f92;
415
+ --accent-rgb: 235, 188, 186;
416
+ --grid-color: rgba(65, 63, 94, 0.3);
417
+ --gradient-color: rgba(235, 188, 186, 0.06);
418
+ }
419
+
420
+ [data-theme="everforest-dark"] {
421
+ --bg-primary: #1e2326;
422
+ --bg-secondary: #22282b;
423
+ --bg-tertiary: #2d353b;
424
+ --text-primary: #d3c6aa;
425
+ --text-muted: #859289;
426
+ --text-hint: #859289;
427
+ --accent-color: #a7c080;
428
+ --accent-hover: #b8c890;
429
+ --border-color: #3d484d;
430
+ --color-red: #e67e80;
431
+ --accent-rgb: 167, 192, 128;
432
+ --grid-color: rgba(61, 72, 77, 0.3);
433
+ --gradient-color: rgba(167, 192, 128, 0.06);
434
+ }
435
+
436
+ [data-theme="kanagawa"] {
437
+ --bg-primary: #1f1f28;
438
+ --bg-secondary: #16161d;
439
+ --bg-tertiary: #2a2a37;
440
+ --text-primary: #dcd7ba;
441
+ --text-muted: #727169;
442
+ --text-hint: #727169;
443
+ --accent-color: #7e9cd8;
444
+ --accent-hover: #9cabde;
445
+ --border-color: #363646;
446
+ --color-red: #c34043;
447
+ --accent-rgb: 126, 156, 216;
448
+ --grid-color: rgba(54, 54, 70, 0.3);
449
+ --gradient-color: rgba(126, 156, 216, 0.06);
450
+ }
451
+
452
+ [data-theme="one-light"] {
453
+ --bg-primary: #fafafa;
454
+ --bg-secondary: #f0f0f0;
455
+ --bg-tertiary: #e5e5e5;
456
+ --text-primary: #383a42;
457
+ --text-muted: #a0a1a7;
458
+ --text-hint: #a0a1a7;
459
+ --accent-color: #4078f2;
460
+ --accent-hover: #2c65d8;
461
+ --border-color: #e0e0e0;
462
+ --color-red: #e45649;
463
+ --accent-rgb: 64, 120, 242;
464
+ --grid-color: rgba(224, 224, 224, 0.4);
465
+ --gradient-color: rgba(64, 120, 242, 0.06);
466
+ }
467
+
468
+ [data-theme="everforest-light"] {
469
+ --bg-primary: #fdf6e3;
470
+ --bg-secondary: #f2e9d0;
471
+ --bg-tertiary: #e5dcc0;
472
+ --text-primary: #5c6a72;
473
+ --text-muted: #939f91;
474
+ --text-hint: #939f91;
475
+ --accent-color: #7a8478;
476
+ --accent-hover: #8b9885;
477
+ --border-color: #d8cdb1;
478
+ --color-red: #c65c5c;
479
+ --accent-rgb: 122, 132, 120;
480
+ --grid-color: rgba(216, 205, 177, 0.4);
481
+ --gradient-color: rgba(122, 132, 120, 0.06);
482
+ }
483
+
484
+ [data-theme="ayu-light"] {
485
+ --bg-primary: #fafafa;
486
+ --bg-secondary: #f3f3f3;
487
+ --bg-tertiary: #e8e8e8;
488
+ --text-primary: #5c6166;
489
+ --text-muted: #a0a5aa;
490
+ --text-hint: #a0a5aa;
491
+ --accent-color: #ff9940;
492
+ --accent-hover: #f2872a;
493
+ --border-color: #e3e3e3;
494
+ --color-red: #f51818;
495
+ --accent-rgb: 255, 153, 64;
496
+ --grid-color: rgba(227, 227, 227, 0.4);
497
+ --gradient-color: rgba(255, 153, 64, 0.06);
498
+ }
499
+
500
+ [data-theme="nord-light"] {
501
+ --bg-primary: #eceff4;
502
+ --bg-secondary: #e5e9f0;
503
+ --bg-tertiary: #d8dee9;
504
+ --text-primary: #2e3440;
505
+ --text-muted: #8a93a5;
506
+ --text-hint: #8a93a5;
507
+ --accent-color: #5e81ac;
508
+ --accent-hover: #6b95c0;
509
+ --border-color: #d5dbe5;
510
+ --color-red: #bf616a;
511
+ --accent-rgb: 94, 129, 172;
512
+ --grid-color: rgba(213, 219, 229, 0.4);
513
+ --gradient-color: rgba(94, 129, 172, 0.06);
514
+ }
515
+
516
+ [data-theme="light-modern"] {
517
+ --bg-primary: #fafafa;
518
+ --bg-secondary: #f3f3f3;
519
+ --bg-tertiary: #e9e9e9;
520
+ --text-primary: #1a1a1a;
521
+ --text-muted: #717175;
522
+ --text-hint: #717175;
523
+ --accent-color: #0b57d0;
524
+ --accent-hover: #0b4aa9;
525
+ --border-color: #d4d4d4;
526
+ --color-red: #d7002a;
527
+ --accent-rgb: 11, 87, 208;
528
+ --grid-color: rgba(212, 212, 212, 0.4);
529
+ --gradient-color: rgba(11, 87, 208, 0.06);
530
+ }
531
+
532
+ [data-theme="light-plus"] {
533
+ --bg-primary: #ffffff;
534
+ --bg-secondary: #f5f5f5;
535
+ --bg-tertiary: #e8e8e8;
536
+ --text-primary: #000000;
537
+ --text-muted: #6f6f6f;
538
+ --text-hint: #6f6f6f;
539
+ --accent-color: #0065bf;
540
+ --accent-hover: #00529c;
541
+ --border-color: #d9d9d9;
542
+ --color-red: #cd3131;
543
+ --accent-rgb: 0, 101, 191;
544
+ --grid-color: rgba(217, 217, 217, 0.4);
545
+ --gradient-color: rgba(0, 101, 191, 0.06);
546
+ }
547
+
548
+ [data-theme="quiet-light"] {
549
+ --bg-primary: #f5f5f5;
550
+ --bg-secondary: #ececec;
551
+ --bg-tertiary: #e0e0e0;
552
+ --text-primary: #333333;
553
+ --text-muted: #767676;
554
+ --text-hint: #767676;
555
+ --accent-color: #4a6f8b;
556
+ --accent-hover: #3d5e77;
557
+ --border-color: #c9c9c9;
558
+ --color-red: #a03131;
559
+ --accent-rgb: 74, 111, 139;
560
+ --grid-color: rgba(201, 201, 201, 0.4);
561
+ --gradient-color: rgba(74, 111, 139, 0.06);
562
+ }
563
+
564
+ [data-theme="vitesse-light"] {
565
+ --bg-primary: #ffffff;
566
+ --bg-secondary: #f6f6f4;
567
+ --bg-tertiary: #ebebe8;
568
+ --text-primary: #393a34;
569
+ --text-muted: #999999;
570
+ --text-hint: #999999;
571
+ --accent-color: #4d9375;
572
+ --accent-hover: #3f7a61;
573
+ --border-color: #d1d0cb;
574
+ --color-red: #e53935;
575
+ --accent-rgb: 77, 147, 117;
576
+ --grid-color: rgba(209, 208, 203, 0.4);
577
+ --gradient-color: rgba(77, 147, 117, 0.06);
578
+ }
579
+
580
+ [data-theme="bluloco-light"] {
581
+ --bg-primary: #f7f9fc;
582
+ --bg-secondary: #edf1f7;
583
+ --bg-tertiary: #e0e6ee;
584
+ --text-primary: #292d3e;
585
+ --text-muted: #718096;
586
+ --text-hint: #718096;
587
+ --accent-color: #2b7bda;
588
+ --accent-hover: #2265b4;
589
+ --border-color: #d4dae3;
590
+ --color-red: #dd4a70;
591
+ --accent-rgb: 43, 123, 218;
592
+ --grid-color: rgba(212, 218, 227, 0.4);
593
+ --gradient-color: rgba(43, 123, 218, 0.06);
594
+ }
595
+
596
+ [data-theme="material-lighter"] {
597
+ --bg-primary: #fafafa;
598
+ --bg-secondary: #f0f0f0;
599
+ --bg-tertiary: #e4e4e4;
600
+ --text-primary: #212121;
601
+ --text-muted: #9e9e9e;
602
+ --text-hint: #9e9e9e;
603
+ --accent-color: #1976d2;
604
+ --accent-hover: #1565c0;
605
+ --border-color: #e0e0e0;
606
+ --color-red: #c62828;
607
+ --accent-rgb: 25, 118, 210;
608
+ --grid-color: rgba(224, 224, 224, 0.4);
609
+ --gradient-color: rgba(25, 118, 210, 0.06);
610
+ }
611
+
612
+ [data-theme="alabaster"] {
613
+ --bg-primary: #f7f7f7;
614
+ --bg-secondary: #eeeeee;
615
+ --bg-tertiary: #e2e2e2;
616
+ --text-primary: #272727;
617
+ --text-muted: #777777;
618
+ --text-hint: #777777;
619
+ --accent-color: #0086b3;
620
+ --accent-hover: #006d91;
621
+ --border-color: #d9d9d9;
622
+ --color-red: #aa3731;
623
+ --accent-rgb: 0, 134, 179;
624
+ --grid-color: rgba(217, 217, 217, 0.4);
625
+ --gradient-color: rgba(0, 134, 179, 0.06);
626
+ }
627
+
628
+ [data-theme="dark-plus"] {
629
+ --bg-primary: #1e1e1e;
630
+ --bg-secondary: #252526;
631
+ --bg-tertiary: #2d2d2d;
632
+ --text-primary: #d4d4d4;
633
+ --text-muted: #8c8c8c;
634
+ --text-hint: #8c8c8c;
635
+ --accent-color: #0e639c;
636
+ --accent-hover: #1177bb;
637
+ --border-color: #3c3c3c;
638
+ --color-red: #f14c4c;
639
+ --accent-rgb: 55, 148, 255;
640
+ --grid-color: rgba(60, 60, 60, 0.3);
641
+ --gradient-color: rgba(55, 148, 255, 0.08);
642
+ }
643
+
644
+ [data-theme="monokai"] {
645
+ --bg-primary: #272822;
646
+ --bg-secondary: #2e2f29;
647
+ --bg-tertiary: #373832;
648
+ --text-primary: #f8f8f2;
649
+ --text-muted: #a6a68e;
650
+ --text-hint: #a6a68e;
651
+ --accent-color: #ae81ff;
652
+ --accent-hover: #c29aff;
653
+ --border-color: #49483e;
654
+ --color-red: #f92672;
655
+ --accent-rgb: 174, 129, 255;
656
+ --grid-color: rgba(73, 72, 62, 0.3);
657
+ --gradient-color: rgba(174, 129, 255, 0.08);
658
+ }
659
+
660
+ [data-theme="bluloco-dark"] {
661
+ --bg-primary: #1d212c;
662
+ --bg-secondary: #242936;
663
+ --bg-tertiary: #2e3440;
664
+ --text-primary: #e2e8f0;
665
+ --text-muted: #94a3b8;
666
+ --text-hint: #94a3b8;
667
+ --accent-color: #52a5ff;
668
+ --accent-hover: #6db4ff;
669
+ --border-color: #3a4151;
670
+ --color-red: #f075a0;
671
+ --accent-rgb: 82, 165, 255;
672
+ --grid-color: rgba(58, 65, 81, 0.3);
673
+ --gradient-color: rgba(82, 165, 255, 0.08);
674
+ }
675
+
676
+ [data-theme="material-darker"] {
677
+ --bg-primary: #212121;
678
+ --bg-secondary: #282828;
679
+ --bg-tertiary: #303030;
680
+ --text-primary: #eeffff;
681
+ --text-muted: #92a6a7;
682
+ --text-hint: #92a6a7;
683
+ --accent-color: #80cbc4;
684
+ --accent-hover: #97d6d0;
685
+ --border-color: #424242;
686
+ --color-red: #f07178;
687
+ --accent-rgb: 128, 203, 196;
688
+ --grid-color: rgba(66, 66, 66, 0.3);
689
+ --gradient-color: rgba(128, 203, 196, 0.08);
690
+ }
691
+
692
+ body {
693
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
694
+ background: var(--bg-primary);
695
+ color: var(--text-primary);
696
+ min-height: 100vh;
697
+ min-height: 100dvh;
698
+ display: flex;
699
+ align-items: center;
700
+ justify-content: center;
701
+ position: relative;
702
+ overflow: hidden;
703
+ -webkit-tap-highlight-color: transparent;
704
+ }
705
+
706
+ /* Decorative background — matches LoginView.vue */
707
+ .bg-gradient {
708
+ position: fixed;
709
+ inset: 0;
710
+ background:
711
+ radial-gradient(ellipse 60% 50% at 20% 20%, var(--gradient-color), transparent),
712
+ radial-gradient(ellipse 50% 60% at 80% 80%, var(--gradient-color), transparent);
713
+ pointer-events: none;
714
+ }
715
+
716
+ .bg-grid {
717
+ position: fixed;
718
+ inset: 0;
719
+ background-image:
720
+ linear-gradient(var(--grid-color) 1px, transparent 1px),
721
+ linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
722
+ background-size: 48px 48px;
723
+ -webkit-mask-image: radial-gradient(ellipse 70% 70% at center, black, transparent);
724
+ mask-image: radial-gradient(ellipse 70% 70% at center, black, transparent);
725
+ opacity: 0.4;
726
+ pointer-events: none;
727
+ }
728
+
729
+ .content {
730
+ position: relative;
731
+ z-index: 1;
732
+ display: flex;
733
+ flex-direction: column;
734
+ align-items: center;
735
+ width: 100%;
736
+ max-width: 380px;
737
+ padding: 0 24px;
738
+ gap: 36px;
739
+ }
740
+
741
+ /* Brand section */
742
+ .brand {
743
+ text-align: center;
744
+ display: flex;
745
+ flex-direction: column;
746
+ align-items: center;
747
+ }
748
+
749
+ .logo-wrapper {
750
+ position: relative;
751
+ width: 96px;
752
+ height: 96px;
753
+ margin-bottom: 20px;
754
+ }
755
+
756
+ .logo {
757
+ width: 96px;
758
+ height: 96px;
759
+ border-radius: 50%;
760
+ display: block;
761
+ position: relative;
762
+ z-index: 1;
763
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
764
+ object-fit: cover;
765
+ }
766
+
767
+ .logo-ring {
768
+ position: absolute;
769
+ inset: -4px;
770
+ border-radius: 50%;
771
+ border: 2px solid rgba(var(--accent-rgb), 0.3);
772
+ animation: ring-pulse 3s ease-in-out infinite;
773
+ }
774
+
775
+ @keyframes ring-pulse {
776
+ 0%, 100% { opacity: 0.4; transform: scale(1); }
777
+ 50% { opacity: 0.8; transform: scale(1.04); }
778
+ }
779
+
780
+ .title {
781
+ font-size: 26px;
782
+ font-weight: 700;
783
+ color: var(--text-primary);
784
+ letter-spacing: -0.02em;
785
+ margin-bottom: 8px;
786
+ }
787
+
788
+ .slogan {
789
+ font-size: 18px;
790
+ font-weight: 500;
791
+ color: var(--accent-color);
792
+ margin-bottom: 4px;
793
+ letter-spacing: 0.08em;
794
+ }
795
+
796
+ .subtitle {
797
+ font-size: 13px;
798
+ color: var(--text-muted);
799
+ }
800
+
801
+ /* Form card — matches LoginView.vue .login-form-card */
802
+ .form-card {
803
+ width: 100%;
804
+ background: var(--bg-secondary);
805
+ border: 1px solid var(--border-color);
806
+ border-radius: 14px;
807
+ padding: 28px 24px;
808
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
809
+ }
810
+
811
+ /* Server list */
812
+ .server-list-header {
813
+ font-size: 12px;
814
+ color: var(--text-muted);
815
+ margin-bottom: 6px;
816
+ letter-spacing: 0.02em;
817
+ font-weight: 500;
818
+ }
819
+
820
+ .server-list {
821
+ display: flex;
822
+ flex-direction: column;
823
+ gap: 4px;
824
+ margin-bottom: 0;
825
+ max-height: 160px;
826
+ overflow-y: auto;
827
+ scrollbar-width: thin;
828
+ }
829
+
830
+ .server-list-divider {
831
+ border: none;
832
+ border-top: 1px solid var(--border-color);
833
+ margin: 0 0 16px 0;
834
+ }
835
+
836
+ .server-item {
837
+ display: flex;
838
+ align-items: center;
839
+ justify-content: space-between;
840
+ padding: 8px 10px;
841
+ border-radius: 8px;
842
+ border: 1.5px solid var(--border-color);
843
+ cursor: pointer;
844
+ transition: background 0.1s, border-color 0.1s;
845
+ font-size: 13px;
846
+ background: transparent;
847
+ }
848
+
849
+ .server-item:hover {
850
+ background: var(--bg-tertiary);
851
+ border-color: #484f58;
852
+ }
853
+
854
+ .server-item:active {
855
+ background: var(--bg-tertiary);
856
+ border-color: #484f58;
857
+ }
858
+
859
+ .server-item.active {
860
+ background: rgba(var(--accent-rgb), 0.06);
861
+ border: 1.5px solid var(--accent-color);
862
+ color: var(--text-primary);
863
+ }
864
+
865
+ .server-item-info {
866
+ display: flex;
867
+ align-items: center;
868
+ gap: 6px;
869
+ min-width: 0;
870
+ flex: 1;
871
+ }
872
+
873
+ /* Radio-dot indicator for server selection */
874
+ .server-indicator {
875
+ flex-shrink: 0;
876
+ width: var(--server-indicator-size);
877
+ height: var(--server-indicator-size);
878
+ border-radius: 50%;
879
+ border: 1.5px solid var(--border-color);
880
+ background: transparent;
881
+ transition: background 0.15s, border-color 0.15s;
882
+ display: flex;
883
+ align-items: center;
884
+ justify-content: center;
885
+ }
886
+
887
+ .server-item.active .server-indicator {
888
+ border-color: var(--accent-color);
889
+ background: var(--accent-color);
890
+ }
891
+
892
+ .server-item-icon {
893
+ flex-shrink: 0;
894
+ width: 14px;
895
+ height: 14px;
896
+ color: var(--text-muted);
897
+ transition: color 0.15s;
898
+ }
899
+
900
+ .server-item.active .server-item-icon {
901
+ color: var(--accent-color);
902
+ }
903
+
904
+ .server-item-url {
905
+ overflow: hidden;
906
+ text-overflow: ellipsis;
907
+ white-space: nowrap;
908
+ font-weight: 500;
909
+ }
910
+
911
+ .server-delete-btn {
912
+ flex-shrink: 0;
913
+ border: none;
914
+ background: transparent;
915
+ color: var(--text-muted);
916
+ cursor: pointer;
917
+ padding: 2px;
918
+ border-radius: 4px;
919
+ display: flex;
920
+ align-items: center;
921
+ justify-content: center;
922
+ transition: background 0.1s, color 0.1s;
923
+ }
924
+
925
+ .server-delete-btn:hover {
926
+ background: rgba(220, 38, 38, 0.1);
927
+ color: var(--color-red);
928
+ }
929
+
930
+ .server-item.active .server-delete-btn {
931
+ color: var(--text-muted);
932
+ }
933
+
934
+ .server-item.active .server-delete-btn:hover {
935
+ color: var(--color-red);
936
+ background: rgba(220, 38, 38, 0.12);
937
+ }
938
+
939
+ .field-group {
940
+ margin-bottom: 16px;
941
+ }
942
+
943
+ .field-group:last-of-type {
944
+ margin-bottom: 0;
945
+ }
946
+
947
+ .field-label {
948
+ display: block;
949
+ font-size: 12px;
950
+ color: var(--text-muted);
951
+ margin-bottom: 6px;
952
+ }
953
+
954
+ /* Protocol radio */
955
+ .protocol-group {
956
+ display: flex;
957
+ gap: 20px;
958
+ margin-bottom: 16px;
959
+ }
960
+
961
+ .protocol-group label {
962
+ display: flex;
963
+ align-items: center;
964
+ gap: 6px;
965
+ font-size: 14px;
966
+ color: var(--text-primary);
967
+ cursor: pointer;
968
+ }
969
+
970
+ .protocol-group input[type="radio"] {
971
+ appearance: none;
972
+ -webkit-appearance: none;
973
+ width: 18px;
974
+ height: 18px;
975
+ border: 2px solid var(--border-color);
976
+ border-radius: 50%;
977
+ position: relative;
978
+ cursor: pointer;
979
+ transition: border-color 0.2s;
980
+ }
981
+
982
+ .protocol-group input[type="radio"]:checked {
983
+ border-color: var(--accent-color);
984
+ }
985
+
986
+ .protocol-group input[type="radio"]:checked::after {
987
+ content: '';
988
+ position: absolute;
989
+ top: 3px;
990
+ left: 3px;
991
+ width: 8px;
992
+ height: 8px;
993
+ border-radius: 50%;
994
+ background: var(--accent-color);
995
+ }
996
+
997
+ /* Host + Port row */
998
+ .host-row {
999
+ display: flex;
1000
+ align-items: center;
1001
+ gap: 4px;
1002
+ }
1003
+
1004
+ .host-row input:first-child {
1005
+ flex: 1;
1006
+ }
1007
+
1008
+ .host-row .colon {
1009
+ color: var(--text-muted);
1010
+ font-size: 18px;
1011
+ line-height: 44px;
1012
+ }
1013
+
1014
+ .host-row input:last-child {
1015
+ width: 90px;
1016
+ flex-shrink: 0;
1017
+ }
1018
+
1019
+ /* Input fields — matches LoginView.vue input style */
1020
+ .input-with-icon {
1021
+ position: relative;
1022
+ display: flex;
1023
+ align-items: center;
1024
+ }
1025
+
1026
+ .input-icon {
1027
+ position: absolute;
1028
+ left: 12px;
1029
+ width: 16px;
1030
+ height: 16px;
1031
+ color: var(--text-muted);
1032
+ pointer-events: none;
1033
+ flex-shrink: 0;
1034
+ }
1035
+
1036
+ input[type="text"],
1037
+ input[type="number"],
1038
+ input[type="password"],
1039
+ input[type="url"] {
1040
+ width: 100%;
1041
+ height: 44px;
1042
+ padding: 0 12px;
1043
+ border: 1px solid var(--border-color);
1044
+ border-radius: 8px;
1045
+ font-size: 15px;
1046
+ background: var(--bg-primary);
1047
+ color: var(--text-primary);
1048
+ outline: none;
1049
+ transition: border-color 0.2s, box-shadow 0.2s;
1050
+ }
1051
+
1052
+ .input-with-icon input {
1053
+ padding-left: 36px;
1054
+ }
1055
+
1056
+ input::placeholder {
1057
+ color: var(--text-hint);
1058
+ }
1059
+
1060
+ input:focus {
1061
+ border-color: var(--accent-color);
1062
+ box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
1063
+ }
1064
+
1065
+ /* Connect button — matches LoginView.vue .login-btn */
1066
+ .connect-btn {
1067
+ width: 100%;
1068
+ height: 46px;
1069
+ margin-top: 20px;
1070
+ border: none;
1071
+ border-radius: 10px;
1072
+ background: var(--accent-color);
1073
+ color: #fff;
1074
+ font-size: 15px;
1075
+ font-weight: 600;
1076
+ cursor: pointer;
1077
+ display: flex;
1078
+ align-items: center;
1079
+ justify-content: center;
1080
+ gap: 8px;
1081
+ transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
1082
+ -webkit-user-select: none;
1083
+ user-select: none;
1084
+ }
1085
+
1086
+ .connect-btn:hover:not(:disabled) {
1087
+ background: var(--accent-hover);
1088
+ box-shadow: 0 4px 14px rgba(var(--accent-rgb), 0.3);
1089
+ }
1090
+
1091
+ .connect-btn:active:not(:disabled) {
1092
+ transform: scale(0.98);
1093
+ }
1094
+
1095
+ .connect-btn:disabled {
1096
+ opacity: 0.6;
1097
+ cursor: default;
1098
+ }
1099
+
1100
+ /* Spinner shown inside the connect button. Its colours must contrast with
1101
+ the BUTTON background (the accent colour), not the page: using
1102
+ --text-primary put a dark grey spinner on the blue button (1.64:1 on
1103
+ github-dark, 1.21:1 on solarized-light) and it all but vanished.
1104
+ Android hardcodes white here for the same reason; white is the one value
1105
+ that works on every theme's accent. Values kept identical to Android's. */
1106
+ .spinner {
1107
+ width: 16px;
1108
+ height: 16px;
1109
+ border: 2px solid rgba(255, 255, 255, 0.3);
1110
+ border-top-color: #fff;
1111
+ border-radius: 50%;
1112
+ animation: spin 0.6s linear infinite;
1113
+ }
1114
+
1115
+ @keyframes spin {
1116
+ to { transform: rotate(360deg); }
1117
+ }
1118
+
1119
+ /* Cancel button */
1120
+ .cancel-btn {
1121
+ width: 100%;
1122
+ height: 40px;
1123
+ margin-top: 8px;
1124
+ border: 1px solid var(--border-color);
1125
+ border-radius: 10px;
1126
+ background: transparent;
1127
+ color: var(--text-muted);
1128
+ font-size: 14px;
1129
+ cursor: pointer;
1130
+ transition: background 0.15s;
1131
+ }
1132
+
1133
+ .cancel-btn:hover {
1134
+ background: var(--bg-tertiary);
1135
+ }
1136
+
1137
+ /* Error — matches LoginView.vue .error */
1138
+ .error {
1139
+ margin-top: 14px;
1140
+ padding: 10px 14px;
1141
+ border-radius: 8px;
1142
+ background: rgba(220, 38, 38, 0.08);
1143
+ border: 1px solid rgba(220, 38, 38, 0.2);
1144
+ color: var(--color-red);
1145
+ font-size: 13px;
1146
+ display: none;
1147
+ align-items: center;
1148
+ gap: 8px;
1149
+ }
1150
+
1151
+ .error.visible {
1152
+ display: flex;
1153
+ }
1154
+
1155
+ .error svg {
1156
+ flex-shrink: 0;
1157
+ }
1158
+
1159
+ /* Add server button — dashed border style */
1160
+ .add-server-btn {
1161
+ display: flex;
1162
+ align-items: center;
1163
+ justify-content: center;
1164
+ gap: 6px;
1165
+ width: 100%;
1166
+ padding: 10px;
1167
+ margin-top: 12px;
1168
+ border: 1px dashed var(--border-color);
1169
+ border-radius: 10px;
1170
+ background: transparent;
1171
+ color: rgba(var(--accent-rgb), 0.7);
1172
+ font-size: 13px;
1173
+ cursor: pointer;
1174
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
1175
+ }
1176
+
1177
+ .add-server-btn:hover {
1178
+ background: rgba(var(--accent-rgb), 0.06);
1179
+ color: var(--accent-color);
1180
+ border-color: var(--accent-color);
1181
+ }
1182
+
1183
+ /* Version footer */
1184
+ .version {
1185
+ margin-top: 8px;
1186
+ font-size: 11px;
1187
+ color: var(--text-hint);
1188
+ text-align: center;
1189
+ }
1190
+
1191
+ /* Confirm dialog — matches web DialogOverlay.vue */
1192
+ .dlg-overlay {
1193
+ position: fixed;
1194
+ inset: 0;
1195
+ background: rgba(0, 0, 0, 0.45);
1196
+ display: flex;
1197
+ align-items: center;
1198
+ justify-content: center;
1199
+ z-index: 3000;
1200
+ padding: 0 20px;
1201
+ opacity: 0;
1202
+ transition: opacity 0.2s ease;
1203
+ }
1204
+
1205
+ .dlg-overlay.visible {
1206
+ opacity: 1;
1207
+ }
1208
+
1209
+ .dlg-box {
1210
+ background: var(--bg-secondary);
1211
+ border-radius: 14px;
1212
+ padding: 18px 16px 14px;
1213
+ max-width: 320px;
1214
+ width: 100%;
1215
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1216
+ transform: scale(0.9);
1217
+ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
1218
+ }
1219
+
1220
+ .dlg-overlay.visible .dlg-box {
1221
+ transform: scale(1);
1222
+ }
1223
+
1224
+ .dlg-title {
1225
+ font-weight: 600;
1226
+ font-size: 14px;
1227
+ color: var(--text-primary);
1228
+ margin-bottom: 8px;
1229
+ }
1230
+
1231
+ .dlg-msg {
1232
+ font-size: 13px;
1233
+ color: var(--text-muted);
1234
+ line-height: 1.5;
1235
+ margin-bottom: 14px;
1236
+ white-space: pre-line;
1237
+ word-break: break-word;
1238
+ overflow-wrap: break-word;
1239
+ }
1240
+
1241
+ .dlg-actions {
1242
+ display: flex;
1243
+ gap: 8px;
1244
+ justify-content: flex-end;
1245
+ }
1246
+
1247
+ .dlg-btn {
1248
+ padding: 6px 16px;
1249
+ border-radius: 8px;
1250
+ font-size: 13px;
1251
+ font-weight: 500;
1252
+ border: none;
1253
+ cursor: pointer;
1254
+ -webkit-tap-highlight-color: transparent;
1255
+ }
1256
+
1257
+ .dlg-btn:active { opacity: 0.7; }
1258
+
1259
+ .dlg-cancel {
1260
+ background: var(--bg-tertiary);
1261
+ color: var(--text-muted);
1262
+ }
1263
+
1264
+ .dlg-ok {
1265
+ background: var(--color-red);
1266
+ color: #fff;
1267
+ }
1268
+
1269
+ /* Safe area padding for notch devices */
1270
+ @supports (padding: env(safe-area-inset-bottom)) {
1271
+ .content {
1272
+ padding-bottom: env(safe-area-inset-bottom);
1273
+ }
1274
+ }
1275
+ </style>
1276
+ </head>
1277
+ <body>
1278
+ <div class="bg-gradient"></div>
1279
+ <div class="bg-grid"></div>
1280
+
1281
+ <div class="content">
1282
+ <!-- Brand -->
1283
+ <div class="brand">
1284
+ <div class="logo-wrapper">
1285
+ <img class="logo" src="logo.png" alt="ClawBench">
1286
+ <div class="logo-ring"></div>
1287
+ </div>
1288
+ <h1 class="title">ClawBench</h1>
1289
+ <p class="slogan" data-i18n="slogan">从终端到桌面</p>
1290
+ <p class="subtitle" data-i18n="subtitle">为桌面端打造的 AI 工作台</p>
1291
+ </div>
1292
+
1293
+ <!-- Form -->
1294
+ <div class="form-card">
1295
+ <!-- Saved server list (shown when servers exist) -->
1296
+ <div id="serverListSection" style="display:none;">
1297
+ <div class="server-list-header" id="serverListHeader" data-i18n="saved_servers">已保存服务器</div>
1298
+ <div id="serverListContainer" class="server-list"></div>
1299
+ <hr class="server-list-divider" id="serverListDivider">
1300
+ </div>
1301
+
1302
+ <!-- Login form for existing / current server -->
1303
+ <form id="connectForm" onsubmit="return false;" style="display:none;">
1304
+ <div id="passwordGroup" class="field-group">
1305
+ <span class="field-label" data-i18n="password_label">登录密码</span>
1306
+ <div class="input-with-icon">
1307
+ <svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1308
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
1309
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
1310
+ </svg>
1311
+ <input type="password" id="password" data-i18n-placeholder="password_placeholder" placeholder="密码">
1312
+ </div>
1313
+ </div>
1314
+
1315
+ <button type="submit" class="connect-btn" id="connectBtn">
1316
+ <span id="btnText" data-i18n="connect">连 接</span>
1317
+ </button>
1318
+
1319
+ <div class="error" id="errorMsg">
1320
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
1321
+ <circle cx="12" cy="12" r="10"/>
1322
+ <line x1="15" y1="9" x2="9" y2="15"/>
1323
+ <line x1="9" y1="9" x2="15" y2="15"/>
1324
+ </svg>
1325
+ <span id="errorText"></span>
1326
+ </div>
1327
+ </form>
1328
+
1329
+ <!-- Add server form (hidden by default) -->
1330
+ <form id="addServerForm" onsubmit="return false;" style="display:none;">
1331
+ <!-- Protocol -->
1332
+ <div class="field-group">
1333
+ <span class="field-label" data-i18n="protocol">协议</span>
1334
+ <div class="protocol-group">
1335
+ <label>
1336
+ <input type="radio" name="addProtocol" value="https" checked>
1337
+ HTTPS
1338
+ </label>
1339
+ <label>
1340
+ <input type="radio" name="addProtocol" value="http">
1341
+ HTTP
1342
+ </label>
1343
+ </div>
1344
+ </div>
1345
+
1346
+ <!-- Host + Port -->
1347
+ <div class="field-group">
1348
+ <span class="field-label" data-i18n="server_address">服务器地址</span>
1349
+ <div class="host-row">
1350
+ <input type="text" id="addHost" placeholder="192.168.1.100" autocorrect="off" autocapitalize="none" spellcheck="false">
1351
+ <span class="colon">:</span>
1352
+ <input type="number" id="addPort" value="20000" inputmode="numeric">
1353
+ </div>
1354
+ </div>
1355
+
1356
+ <!-- Password -->
1357
+ <div class="field-group">
1358
+ <span class="field-label" data-i18n="password_label">登录密码</span>
1359
+ <div class="input-with-icon">
1360
+ <svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1361
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
1362
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
1363
+ </svg>
1364
+ <input type="password" id="addPassword" data-i18n-placeholder="password_placeholder" placeholder="密码">
1365
+ </div>
1366
+ </div>
1367
+
1368
+ <button type="submit" class="connect-btn" id="addConnectBtn">
1369
+ <span id="addBtnText" data-i18n="add_connect">添 加 并 连 接</span>
1370
+ </button>
1371
+ <button type="button" class="cancel-btn" id="cancelAddBtn" data-i18n="cancel">取 消</button>
1372
+
1373
+ <div class="error" id="addErrorMsg">
1374
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
1375
+ <circle cx="12" cy="12" r="10"/>
1376
+ <line x1="15" y1="9" x2="9" y2="15"/>
1377
+ <line x1="9" y1="9" x2="15" y2="15"/>
1378
+ </svg>
1379
+ <span id="addErrorText"></span>
1380
+ </div>
1381
+ </form>
1382
+
1383
+ <!-- Add server button (shown in main view when server list exists) -->
1384
+ <button type="button" class="add-server-btn" id="addServerBtn" style="display:none;">
1385
+ <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>
1386
+ <span data-i18n="add_server">添加服务器</span>
1387
+ </button>
1388
+ </div>
1389
+
1390
+ <div class="version" id="version"></div>
1391
+ </div>
1392
+
1393
+ <!-- Custom confirm dialog -->
1394
+ <div class="dlg-overlay" id="dlgOverlay" style="display:none;">
1395
+ <div class="dlg-box">
1396
+ <div class="dlg-title" id="dlgTitle"></div>
1397
+ <div class="dlg-msg" id="dlgMsg"></div>
1398
+ <div class="dlg-actions">
1399
+ <button class="dlg-btn dlg-cancel" id="dlgCancel" data-i18n="cancel">取消</button>
1400
+ <button class="dlg-btn dlg-ok" id="dlgOk" data-i18n="confirm">确认</button>
1401
+ </div>
1402
+ </div>
1403
+ </div>
1404
+
1405
+ <script>
1406
+ var connecting = false;
1407
+ var savedServers = [];
1408
+
1409
+ // SVG icons
1410
+ var SVG_SERVER = '<svg class="server-item-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>';
1411
+ var SVG_X = '<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>';
1412
+
1413
+ function applyI18n() {
1414
+ document.querySelectorAll('[data-i18n]').forEach(function(el) {
1415
+ el.textContent = t(el.getAttribute('data-i18n'));
1416
+ });
1417
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
1418
+ el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
1419
+ });
1420
+ }
1421
+
1422
+ window.addEventListener('DOMContentLoaded', function() {
1423
+ // Apply i18n to all elements with data-i18n / data-i18n-placeholder
1424
+ applyI18n();
1425
+
1426
+ // Load server list (single source of truth)
1427
+ loadServerList();
1428
+
1429
+ // Version. getAppVersion() goes through ipcRenderer.invoke, so it returns a
1430
+ // Promise — reading it synchronously renders "v[object Promise]".
1431
+ try {
1432
+ Promise.resolve(ClawBenchNative.getAppVersion()).then(function(ver) {
1433
+ if (ver) document.getElementById('version').textContent = 'v' + ver;
1434
+ }).catch(function() {});
1435
+ } catch(e) {}
1436
+
1437
+ // Event: connect to existing server
1438
+ document.getElementById('connectForm').addEventListener('submit', function(e) {
1439
+ e.preventDefault();
1440
+ if (connecting) return;
1441
+ hideError('errorMsg');
1442
+
1443
+ var passwordEl = document.getElementById('password');
1444
+ var passwordVisible = document.getElementById('passwordGroup').style.display !== 'none';
1445
+ var password = passwordVisible ? passwordEl.value : (connectForm.getAttribute('data-saved-password') || '');
1446
+
1447
+ if (!password) {
1448
+ showError('errorMsg', t('enter_password'));
1449
+ passwordEl.focus();
1450
+ return;
1451
+ }
1452
+
1453
+ // Current server URL is stored as data attribute on the form
1454
+ var url = document.getElementById('connectForm').getAttribute('data-url');
1455
+ if (!url) return;
1456
+
1457
+ setConnecting(true);
1458
+ try {
1459
+ // Update saved password in case it was changed.
1460
+ // Both calls go through ipcRenderer.invoke, so failures surface as
1461
+ // Promise rejections rather than throws. The main error path is the
1462
+ // native onConnectError() callback (the window navigates on success),
1463
+ // but a rejection here must still reset the button or the form would
1464
+ // stay stuck on "authenticating" if native never calls back.
1465
+ ClawBenchNative.saveServer(url, password);
1466
+ Promise.resolve(ClawBenchNative.connectToServer(url, password)).catch(function(err) {
1467
+ showError('errorMsg', t('connection_failed') + (err && err.message ? err.message : ''));
1468
+ setConnecting(false);
1469
+ });
1470
+ } catch(e) {
1471
+ showError('errorMsg', t('connection_failed') + e.message);
1472
+ setConnecting(false);
1473
+ }
1474
+ });
1475
+
1476
+ // Event: add and connect to new server
1477
+ document.getElementById('addServerForm').addEventListener('submit', function(e) {
1478
+ e.preventDefault();
1479
+ if (connecting) return;
1480
+ hideError('addErrorMsg');
1481
+
1482
+ var host = document.getElementById('addHost').value.trim();
1483
+ var port = document.getElementById('addPort').value.trim();
1484
+ var password = document.getElementById('addPassword').value;
1485
+ var protocol = document.querySelector('input[name="addProtocol"]:checked').value;
1486
+
1487
+ if (!host) {
1488
+ showError('addErrorMsg', t('enter_address'));
1489
+ document.getElementById('addHost').focus();
1490
+ return;
1491
+ }
1492
+ if (!port) {
1493
+ port = (protocol === 'https') ? '443' : '80';
1494
+ }
1495
+
1496
+ var url = protocol + '://' + host + ':' + port;
1497
+ if (url.endsWith('/')) url = url.substring(0, url.length - 1);
1498
+
1499
+ setConnecting(true, true);
1500
+ try {
1501
+ // Save to native list first. See the note in the connect handler above:
1502
+ // these are async, so the rejection must be handled explicitly.
1503
+ ClawBenchNative.saveServer(url, password);
1504
+ Promise.resolve(ClawBenchNative.connectToServer(url, password)).catch(function(err) {
1505
+ showError('addErrorMsg', t('connection_failed') + (err && err.message ? err.message : ''));
1506
+ setConnecting(false, true);
1507
+ });
1508
+ } catch(e) {
1509
+ showError('addErrorMsg', t('connection_failed') + e.message);
1510
+ setConnecting(false, true);
1511
+ }
1512
+ });
1513
+
1514
+ // Event: show add form
1515
+ document.getElementById('addServerBtn').addEventListener('click', function() {
1516
+ showAddForm();
1517
+ });
1518
+
1519
+ // Event: cancel add form
1520
+ document.getElementById('cancelAddBtn').addEventListener('click', function() {
1521
+ hideAddForm();
1522
+ });
1523
+ });
1524
+
1525
+ function loadServerList() {
1526
+ // getServerList() goes through ipcRenderer.invoke and returns a Promise.
1527
+ // Reading it synchronously gave JSON.parse() a Promise, which threw and left
1528
+ // savedServers empty — so a returning user never saw their saved servers and
1529
+ // was always dropped into the "add server" form.
1530
+ Promise.resolve(ClawBenchNative.getServerList()).then(function(json) {
1531
+ try {
1532
+ savedServers = JSON.parse(json);
1533
+ if (!Array.isArray(savedServers)) savedServers = [];
1534
+ } catch(e) {
1535
+ savedServers = [];
1536
+ }
1537
+ renderServerList();
1538
+ }).catch(function() {
1539
+ savedServers = [];
1540
+ renderServerList();
1541
+ });
1542
+ }
1543
+
1544
+ function renderServerList() {
1545
+ var container = document.getElementById('serverListContainer');
1546
+ var section = document.getElementById('serverListSection');
1547
+ var connectForm = document.getElementById('connectForm');
1548
+ var addBtn = document.getElementById('addServerBtn');
1549
+
1550
+ if (savedServers.length === 0) {
1551
+ // No saved servers — show the add form directly
1552
+ section.style.display = 'none';
1553
+ connectForm.style.display = 'none';
1554
+ addBtn.style.display = 'none';
1555
+ showAddForm();
1556
+ return;
1557
+ }
1558
+
1559
+ // Build server list HTML — default to first server in list
1560
+ var html = '';
1561
+ savedServers.forEach(function(srv, idx) {
1562
+ var isActive = (idx === 0);
1563
+ html += '<div class="server-item' + (isActive ? ' active' : '') + '" data-url="' + escHtml(srv.url) + '" onclick="selectServer(\'' + escJs(srv.url) + '\')">';
1564
+ html += ' <div class="server-item-info">';
1565
+ html += ' <span class="server-indicator"></span>';
1566
+ html += ' ' + SVG_SERVER;
1567
+ html += ' <span class="server-item-url">' + escHtml(formatHost(srv.url)) + '</span>';
1568
+ html += ' </div>';
1569
+ html += ' <button class="server-delete-btn" onclick="event.stopPropagation(); deleteServer(\'' + escJs(srv.url) + '\')" title="' + t('delete') + '">';
1570
+ html += ' ' + SVG_X;
1571
+ html += ' </button>';
1572
+ html += '</div>';
1573
+ });
1574
+
1575
+ container.innerHTML = html;
1576
+ section.style.display = 'block';
1577
+
1578
+ // Show connect form and add button
1579
+ connectForm.style.display = 'block';
1580
+ addBtn.style.display = 'flex';
1581
+
1582
+ // Select first server by default
1583
+ var selected = savedServers[0];
1584
+
1585
+ connectForm.setAttribute('data-url', selected.url);
1586
+ connectForm.setAttribute('data-saved-password', selected.password || '');
1587
+ // Hide password field if saved password exists; show if no password saved
1588
+ var passwordGroup = document.getElementById('passwordGroup');
1589
+ var passwordEl = document.getElementById('password');
1590
+ if (selected.password) {
1591
+ passwordGroup.style.display = 'none';
1592
+ passwordEl.value = '';
1593
+ } else {
1594
+ passwordGroup.style.display = 'block';
1595
+ passwordEl.value = '';
1596
+ passwordEl.focus();
1597
+ }
1598
+ }
1599
+
1600
+ function selectServer(url) {
1601
+ var srv = savedServers.find(function(s) { return s.url === url; });
1602
+ if (!srv) return;
1603
+
1604
+ // Update active state visually
1605
+ var items = document.querySelectorAll('.server-item');
1606
+ items.forEach(function(el) {
1607
+ if (el.getAttribute('data-url') === url) {
1608
+ el.classList.add('active');
1609
+ } else {
1610
+ el.classList.remove('active');
1611
+ }
1612
+ });
1613
+
1614
+ // Update connect form
1615
+ var connectForm = document.getElementById('connectForm');
1616
+ var passwordGroup = document.getElementById('passwordGroup');
1617
+ var passwordEl = document.getElementById('password');
1618
+ connectForm.setAttribute('data-url', url);
1619
+ connectForm.setAttribute('data-saved-password', srv.password || '');
1620
+ hideError('errorMsg');
1621
+ if (srv.password) {
1622
+ passwordGroup.style.display = 'none';
1623
+ passwordEl.value = '';
1624
+ } else {
1625
+ passwordGroup.style.display = 'block';
1626
+ passwordEl.value = '';
1627
+ passwordEl.focus();
1628
+ }
1629
+ }
1630
+
1631
+ function customConfirm(message, opts) {
1632
+ opts = opts || {};
1633
+ var title = opts.title || '';
1634
+ var confirmText = opts.confirmText || t('confirm');
1635
+ var cancelText = opts.cancelText || t('cancel');
1636
+
1637
+ return new Promise(function(resolve) {
1638
+ var overlay = document.getElementById('dlgOverlay');
1639
+ var titleEl = document.getElementById('dlgTitle');
1640
+ var msgEl = document.getElementById('dlgMsg');
1641
+ var okBtn = document.getElementById('dlgOk');
1642
+ var cancelBtn = document.getElementById('dlgCancel');
1643
+
1644
+ titleEl.textContent = title;
1645
+ titleEl.style.display = title ? 'block' : 'none';
1646
+ msgEl.textContent = message;
1647
+ okBtn.textContent = confirmText;
1648
+ cancelBtn.textContent = cancelText;
1649
+
1650
+ overlay.style.display = 'flex';
1651
+ // Force reflow before adding visible class for transition
1652
+ overlay.offsetHeight;
1653
+ overlay.classList.add('visible');
1654
+
1655
+ function cleanup(result) {
1656
+ overlay.classList.remove('visible');
1657
+ setTimeout(function() { overlay.style.display = 'none'; }, 200);
1658
+ okBtn.removeEventListener('click', onOk);
1659
+ cancelBtn.removeEventListener('click', onCancel);
1660
+ overlay.removeEventListener('click', onBackdrop);
1661
+ resolve(result);
1662
+ }
1663
+
1664
+ function onOk() { cleanup(true); }
1665
+ function onCancel() { cleanup(false); }
1666
+ function onBackdrop(e) { if (e.target === overlay) cleanup(false); }
1667
+
1668
+ okBtn.addEventListener('click', onOk);
1669
+ cancelBtn.addEventListener('click', onCancel);
1670
+ overlay.addEventListener('click', onBackdrop);
1671
+ });
1672
+ }
1673
+
1674
+ function deleteServer(url) {
1675
+ customConfirm(t('delete_server_msg'), { title: t('delete_server_title'), confirmText: t('delete') }).then(function(ok) {
1676
+ if (!ok) return;
1677
+ // removeServer is async; reload only after it settles, or the list would
1678
+ // be re-read before the deletion landed and still show the removed entry.
1679
+ Promise.resolve(ClawBenchNative.removeServer(url)).catch(function() {}).then(function() {
1680
+ loadServerList();
1681
+ });
1682
+ });
1683
+ }
1684
+
1685
+ function showAddForm() {
1686
+ document.getElementById('serverListSection').style.display = 'none';
1687
+ document.getElementById('connectForm').style.display = 'none';
1688
+ document.getElementById('addServerBtn').style.display = 'none';
1689
+ document.getElementById('addServerForm').style.display = 'block';
1690
+ }
1691
+
1692
+ function hideAddForm() {
1693
+ document.getElementById('addServerForm').style.display = 'none';
1694
+ document.getElementById('addErrorMsg').classList.remove('visible');
1695
+ renderServerList();
1696
+ }
1697
+
1698
+ function formatHost(url) {
1699
+ try {
1700
+ var u = new URL(url);
1701
+ return u.origin;
1702
+ } catch(e) {
1703
+ return url;
1704
+ }
1705
+ }
1706
+
1707
+ function escHtml(s) {
1708
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1709
+ }
1710
+
1711
+ function escJs(s) {
1712
+ return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"');
1713
+ }
1714
+
1715
+ function showError(id, msg) {
1716
+ var el = document.getElementById(id);
1717
+ var textId = id === 'errorMsg' ? 'errorText' : 'addErrorText';
1718
+ document.getElementById(textId).textContent = msg;
1719
+ el.classList.add('visible');
1720
+ }
1721
+
1722
+ function hideError(id) {
1723
+ document.getElementById(id).classList.remove('visible');
1724
+ }
1725
+
1726
+ function setConnecting(loading, isAddForm) {
1727
+ connecting = loading;
1728
+ var btnId = isAddForm ? 'addConnectBtn' : 'connectBtn';
1729
+ var btn = document.getElementById(btnId);
1730
+ if (loading) {
1731
+ btn.disabled = true;
1732
+ btn.innerHTML = '<div class="spinner"></div><span>' + t('authenticating') + '</span>';
1733
+ } else {
1734
+ btn.disabled = false;
1735
+ if (isAddForm) {
1736
+ btn.innerHTML = '<span id="addBtnText">' + t('add_connect') + '</span>';
1737
+ } else {
1738
+ btn.innerHTML = '<span id="btnText">' + t('connect') + '</span>';
1739
+ }
1740
+ }
1741
+ }
1742
+
1743
+ // Called by native code on connection failure.
1744
+ // msg is the Chromium load-error description (e.g. "ERR_CONNECTION_REFUSED")
1745
+ // delivered from the main process's did-fail-load handler; empty when the
1746
+ // failure came from the page's own calls.
1747
+ function onConnectError(msg) {
1748
+ setConnecting(false);
1749
+ setConnecting(false, true);
1750
+ var text = msg ? t('connection_failed') + msg : t('connect_error');
1751
+ var addForm = document.getElementById('addServerForm');
1752
+ if (addForm.style.display !== 'none') {
1753
+ showError('addErrorMsg', text);
1754
+ } else {
1755
+ showError('errorMsg', text);
1756
+ // Only show password field on auth errors (wrong password, too many attempts).
1757
+ // Network/server errors should NOT reveal the password input — the user can't
1758
+ // fix a connection issue by entering a different password.
1759
+ var passwordGroup = document.getElementById('passwordGroup');
1760
+ var passwordEl = document.getElementById('password');
1761
+ if (msg && (msg.indexOf('密码错误') !== -1 || msg.indexOf('尝试次数过多') !== -1 ||
1762
+ msg.indexOf(t('wrong_password')) !== -1 || msg.indexOf(t('too_many_attempts')) !== -1)) {
1763
+ passwordGroup.style.display = 'block';
1764
+ passwordEl.value = '';
1765
+ passwordEl.focus();
1766
+ }
1767
+ }
1768
+ }
1769
+
1770
+ // Called by native code on connection success
1771
+ function onConnectSuccess() {
1772
+ setConnecting(false);
1773
+ setConnecting(false, true);
1774
+ }
1775
+ </script>
1776
+ </body>
1777
+ </html>