cicy-desktop 2.1.77 → 2.1.78

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 (44) hide show
  1. package/README.md +21 -18
  2. package/build/icon.icns +0 -0
  3. package/build/icon.ico +0 -0
  4. package/build/icon.png +0 -0
  5. package/build/icon.svg +8 -18
  6. package/build/icons/icon-1024.png +0 -0
  7. package/build/icons/icon-128.png +0 -0
  8. package/build/icons/icon-16.png +0 -0
  9. package/build/icons/icon-24.png +0 -0
  10. package/build/icons/icon-256.png +0 -0
  11. package/build/icons/icon-32.png +0 -0
  12. package/build/icons/icon-48.png +0 -0
  13. package/build/icons/icon-512.png +0 -0
  14. package/build/icons/icon-64.png +0 -0
  15. package/build/icons/icon-96.png +0 -0
  16. package/build/icons/trayTemplate-16.png +0 -0
  17. package/build/icons/trayTemplate-16@2x.png +0 -0
  18. package/build/icons/trayTemplate-22.png +0 -0
  19. package/build/icons/trayTemplate-22@2x.png +0 -0
  20. package/build/icons/trayTemplate-32.png +0 -0
  21. package/build/icons/trayTemplate-32@2x.png +0 -0
  22. package/build/trayTemplate.png +0 -0
  23. package/build/trayTemplate.svg +11 -12
  24. package/build/trayTemplate@2x.png +0 -0
  25. package/package.json +6 -6
  26. package/src/backends/auth-loopback.js +17 -6
  27. package/src/backends/homepage-react/assets/index-DE9m6JTn.css +1 -0
  28. package/src/backends/homepage-react/assets/index-DLYMzgf5.js +365 -0
  29. package/src/backends/homepage-react/favicon-256.png +0 -0
  30. package/src/backends/homepage-react/favicon.svg +12 -0
  31. package/src/backends/homepage-react/index.html +4 -2
  32. package/src/backends/local-teams.js +53 -1
  33. package/src/backends/login-success.html +96 -0
  34. package/src/cloud/cloud-client.js +239 -0
  35. package/src/main.js +62 -1
  36. package/src/utils/brand-host-electron.js +134 -0
  37. package/src/utils/window-utils.js +62 -14
  38. package/workers/render/index.html +2 -0
  39. package/workers/render/public/favicon-256.png +0 -0
  40. package/workers/render/public/favicon.svg +12 -0
  41. package/workers/render/src/App.css +127 -31
  42. package/workers/render/src/App.jsx +170 -24
  43. package/src/backends/homepage-react/assets/index-CPH-S8uU.css +0 -1
  44. package/src/backends/homepage-react/assets/index-DuWX0iug.js +0 -365
@@ -0,0 +1,134 @@
1
+ // Rebrand the HOST (unpackaged) Electron bundle so cicy-desktop presents as
2
+ // "CiCy Desktop" at the OS level — menu-bar app name, Cmd-Tab app switcher,
3
+ // Finder, and dock — instead of the stock "Electron".
4
+ //
5
+ // Why this is needed: cicy-desktop runs UNPACKAGED (npx / npm i -g — the
6
+ // mac/win/linux default, 主人令). An unpackaged app borrows the shared
7
+ // node_modules/electron bundle for its OS identity, so the menu-bar name and
8
+ // the app-switcher icon come from that bundle's Info.plist + electron.icns,
9
+ // NOT from app code. `app.dock.setIcon()` (see tray.js) only repaints the dock
10
+ // at runtime; it cannot touch the menu-bar name or the Cmd-Tab icon. The only
11
+ // way to fully rebrand an unpackaged app is to patch the host bundle itself.
12
+ //
13
+ // We do it idempotently at startup: set CFBundleName/CFBundleDisplayName and
14
+ // swap Resources/electron.icns for our build/icon.icns (backing up the stock
15
+ // one once). macOS reads the bundle name at launch, so the first time we change
16
+ // the name we relaunch ONCE to apply it — that path is self-terminating because
17
+ // the next run sees the name already branded and does nothing.
18
+ //
19
+ // macOS-only for now. Windows (rcedit electron.exe icon + FileDescription) is a
20
+ // follow-up: the .exe is locked while running, so it needs a copy-swap or an
21
+ // install-time step rather than a startup patch.
22
+
23
+ const { app } = require("electron");
24
+ const path = require("path");
25
+ const fs = require("fs");
26
+ const cp = require("child_process");
27
+ const log = require("electron-log");
28
+
29
+ const BRAND = "CiCy Desktop";
30
+
31
+ // execPath = .../Electron.app/Contents/MacOS/Electron → return .../Contents
32
+ function macBundleContents() {
33
+ const macOSDir = path.dirname(process.execPath); // .../Contents/MacOS
34
+ const contents = path.dirname(macOSDir); // .../Contents
35
+ return path.basename(contents) === "Contents" ? contents : null;
36
+ }
37
+
38
+ function plistGet(plist, key) {
39
+ try {
40
+ return cp
41
+ .execFileSync("/usr/libexec/PlistBuddy", ["-c", `Print :${key}`, plist], { encoding: "utf8" })
42
+ .trim();
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function plistSet(plist, key, val) {
48
+ // PlistBuddy takes everything after the key as the value, spaces included,
49
+ // so "Set :CFBundleName CiCy Desktop" sets the value to "CiCy Desktop".
50
+ try {
51
+ cp.execFileSync("/usr/libexec/PlistBuddy", ["-c", `Set :${key} ${val}`, plist]);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function ourIcns() {
59
+ const p = path.join(__dirname, "..", "..", "build", "icon.icns");
60
+ return fs.existsSync(p) ? p : null;
61
+ }
62
+
63
+ // Patch name + icon. Returns true if the bundle NAME changed (caller relaunches).
64
+ function brandMac() {
65
+ const contents = macBundleContents();
66
+ if (!contents) return false;
67
+ const plist = path.join(contents, "Info.plist");
68
+ if (!fs.existsSync(plist)) return false;
69
+ const icnsTarget = path.join(contents, "Resources", "electron.icns");
70
+ const src = ourIcns();
71
+
72
+ let nameChanged = false;
73
+
74
+ // 1) Menu-bar / switcher / Finder name. Read at launch, so a change needs a relaunch.
75
+ if (plistGet(plist, "CFBundleName") !== BRAND) {
76
+ plistSet(plist, "CFBundleName", BRAND);
77
+ plistSet(plist, "CFBundleDisplayName", BRAND);
78
+ nameChanged = true;
79
+ }
80
+
81
+ // 2) App-switcher / Finder / base dock icon. Compare by size as a cheap check;
82
+ // LaunchServices refresh below makes it visible without a relaunch.
83
+ if (src) {
84
+ let needIcon = true;
85
+ try {
86
+ needIcon =
87
+ !fs.existsSync(icnsTarget) || fs.statSync(icnsTarget).size !== fs.statSync(src).size;
88
+ } catch {}
89
+ if (needIcon) {
90
+ try {
91
+ const bak = icnsTarget + ".electron-orig";
92
+ if (fs.existsSync(icnsTarget) && !fs.existsSync(bak)) fs.copyFileSync(icnsTarget, bak);
93
+ fs.copyFileSync(src, icnsTarget);
94
+ } catch (e) {
95
+ log.warn(`[Brand] icns swap failed: ${e.message}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ // Nudge LaunchServices / Finder / Dock to drop the cached icon + name.
101
+ try {
102
+ const bundle = path.dirname(contents); // .../Electron.app
103
+ cp.execFileSync("/usr/bin/touch", [bundle]);
104
+ cp.execFile(
105
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister",
106
+ ["-f", bundle],
107
+ () => {}
108
+ );
109
+ } catch {}
110
+
111
+ log.info(`[Brand] host Electron bundle → ${BRAND} (nameChanged=${nameChanged})`);
112
+ return nameChanged;
113
+ }
114
+
115
+ let __done = false;
116
+ function brandHostElectron() {
117
+ if (__done) return;
118
+ __done = true;
119
+ try {
120
+ if (process.platform !== "darwin") return; // win32 rcedit path: TODO
121
+ const nameChanged = brandMac();
122
+ if (nameChanged) {
123
+ // The new menu-bar name only shows after a relaunch. Self-terminating:
124
+ // the relaunched run sees the name already branded → no second relaunch.
125
+ log.info("[Brand] relaunching once so the menu-bar name applies…");
126
+ app.relaunch({ args: process.argv.slice(1) });
127
+ app.exit(0);
128
+ }
129
+ } catch (e) {
130
+ log.warn(`[Brand] failed: ${e.message}`);
131
+ }
132
+ }
133
+
134
+ module.exports = { brandHostElectron };
@@ -14,12 +14,33 @@ if (app) {
14
14
  }
15
15
 
16
16
  function setupWindowHandlers(win) {
17
- // Hook window.open to use createWindow with proper webPreferences (webviewTag etc)
17
+ // Hook window.open. App/internal popups (about:blank or trusted/local hosts
18
+ // e.g. a popped-out ttyd terminal from CiCy Code) must open as REAL Electron
19
+ // windows WITH webviewTag, or any <webview> inside them (terminal / artifact
20
+ // guest) collapses to 0x0. Returning action:"deny" (the old behavior) routed
21
+ // every popup through a dialog; an *allowed* window.open window inherits
22
+ // DEFAULT webPreferences (webviewTag=false) unless we override it here.
23
+ // External links (other websites) keep the open-in-browser/app dialog.
18
24
  win.webContents.setWindowOpenHandler(({ url }) => {
19
25
  log.info(`[WindowOpen] Intercepted: ${url}`);
20
- if (url && url !== "about:blank") {
21
- showOpenLinkDialog(win, url);
26
+ if (!url || url === "about:blank" || isTrustedUrl(url)) {
27
+ return {
28
+ action: "allow",
29
+ overrideBrowserWindowOptions: {
30
+ autoHideMenuBar: true,
31
+ icon: require("./app-icon").appIconPath(),
32
+ webPreferences: {
33
+ webviewTag: true, // embedded <webview> (ttyd/artifact) must render
34
+ nodeIntegration: isTrustedUrl(url),
35
+ contextIsolation: !isTrustedUrl(url),
36
+ preload: path.join(__dirname, "../backends/webview-preload.js"),
37
+ webSecurity: false,
38
+ enableClipboard: true,
39
+ },
40
+ },
41
+ };
22
42
  }
43
+ showOpenLinkDialog(win, url);
23
44
  return { action: "deny" };
24
45
  });
25
46
  if (!win.webContents.debugger.isAttached()) {
@@ -29,16 +50,13 @@ function setupWindowHandlers(win) {
29
50
  // 初始化窗口监控(在 dom-ready 之前调用)
30
51
  initWindowMonitoring(win);
31
52
 
32
- // 🔥 确保窗口可以正常关闭 + 添加日志
33
- // Close hide. The user can re-open from the tray icon or topbar menu.
34
- // Only actually destroy the window when the app is quitting (set by the
35
- // tray "Quit" item / "before-quit" handler in main.js).
36
- win.on("close", (event) => {
37
- log.info(`[Window ${win.id}] Close event triggered: ${win.getTitle()}`);
38
- if (!app.isQuitting) {
39
- event.preventDefault();
40
- win.hide();
41
- }
53
+ // Non-homepage windows (team / backend windows) close DIRECTLY — a close
54
+ // actually destroys the window, it does NOT hide it (主人令). Only the
55
+ // homepage is a persistent window; everything created here is disposable and
56
+ // re-openable from the homepage. (Previously these preventDefault()+hide()'d,
57
+ // so "closed" windows lingered hidden forever.)
58
+ win.on("close", () => {
59
+ log.info(`[Window ${win.id}] Close → destroy: ${win.getTitle()}`);
42
60
  });
43
61
 
44
62
  // 🔥 全局下载处理 - 自动保存到 ~/Downloads/electron/
@@ -291,7 +309,6 @@ function createWindow(options = {}, accountIdx = 0, forceNew = false) {
291
309
  // the way for backend pages.
292
310
  autoHideMenuBar: true,
293
311
  webPreferences: {
294
- webviewTag: true,
295
312
  offscreen: false, // 确保不是离屏渲染
296
313
  nodeIntegration: isTrustedUrl(url),
297
314
  contextIsolation: !isTrustedUrl(url),
@@ -307,6 +324,10 @@ function createWindow(options = {}, accountIdx = 0, forceNew = false) {
307
324
  // 允许 webview 访问剪贴板
308
325
  webSecurity: false, // 在开发环境中可以考虑禁用,生产环境需要谨慎
309
326
  ...webPreferences,
327
+ // webviewTag MUST stay on for embedded <webview> (ttyd terminal, artifact
328
+ // guest) to render — a 0x0/blank <webview> is the classic symptom of it
329
+ // being off. Forced LAST so a caller's webPreferences can never disable it.
330
+ webviewTag: true,
310
331
  },
311
332
  });
312
333
 
@@ -412,6 +433,33 @@ if (app) {
412
433
  app.on("browser-window-created", (event, win) => {
413
434
  setupWindowHandlers(win);
414
435
  });
436
+
437
+ // <webview> guests (Team Helper drawer, artifact, an embedded CiCy Code SPA)
438
+ // get their OWN webContents — setupWindowHandlers only runs for BrowserWindows,
439
+ // so a window.open from INSIDE a <webview> would otherwise create a window with
440
+ // default webPreferences (webviewTag=false) → any nested <webview> there is 0x0.
441
+ // Give every guest a handler that opens its popups WITH webviewTag.
442
+ app.on("web-contents-created", (_e, contents) => {
443
+ try {
444
+ if (contents.getType && contents.getType() === "webview") {
445
+ contents.setWindowOpenHandler(({ url }) => ({
446
+ action: "allow",
447
+ overrideBrowserWindowOptions: {
448
+ autoHideMenuBar: true,
449
+ webPreferences: {
450
+ webviewTag: true,
451
+ contextIsolation: !isTrustedUrl(url),
452
+ nodeIntegration: isTrustedUrl(url),
453
+ webSecurity: false,
454
+ enableClipboard: true,
455
+ },
456
+ },
457
+ }));
458
+ }
459
+ } catch (e) {
460
+ log.warn(`[web-contents-created] guest open-handler failed: ${e.message}`);
461
+ }
462
+ });
415
463
  }
416
464
 
417
465
  function showOpenLinkDialog(parentWin, url) {
@@ -3,6 +3,8 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <link rel="icon" type="image/png" sizes="256x256" href="/favicon-256.png" />
6
8
  <title>CiCy Desktop</title>
7
9
  </head>
8
10
  <body>
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
2
+ <defs>
3
+ <linearGradient id="cicyMark" x1="16" y1="12" x2="80" y2="84" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#60A5FA"/>
5
+ <stop offset="0.55" stop-color="#2563EB"/>
6
+ <stop offset="1" stop-color="#1E3A8A"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <path d="M48 11L39.5 33.3L16 29.5L31 48L16 66.5L39.5 62.7L48 85L56.5 62.7L80 66.5L65 48L80 29.5L56.5 33.3Z"
10
+ fill="url(#cicyMark)" stroke="url(#cicyMark)" stroke-width="8"
11
+ stroke-linejoin="round" stroke-linecap="round"/>
12
+ </svg>
@@ -1,3 +1,19 @@
1
+ /* ── Palette (restrained: one accent + neutrals + sparse semantics) ──
2
+ accent = brand blue (primary actions, version/update/local accents)
3
+ brand gradient (blue→violet) = logo + primary CTA ONLY
4
+ ok/danger = sparse semantic (live dot / destructive); amber retired. */
5
+ :root {
6
+ --accent: #5b8df7;
7
+ --accent-soft: rgba(91,141,247,.16);
8
+ --accent-line: rgba(91,141,247,.32);
9
+ --accent-text: #a5c4ff;
10
+ --ok: #4ade80;
11
+ --danger: #f7a3a3;
12
+ --text: #e5e7eb;
13
+ --text-dim: #9da7b3;
14
+ --text-mute: #6b7280;
15
+ --line: rgba(255,255,255,.08);
16
+ }
1
17
  * { box-sizing: border-box; }
2
18
  html, body, #root { margin: 0; height: 100%; }
3
19
  body {
@@ -132,10 +148,12 @@ body {
132
148
  border-bottom: 1px solid rgba(255,255,255,.05);
133
149
  backdrop-filter: blur(14px);
134
150
  }
135
- /* Reserve left gutter for the macOS hiddenInset traffic-light buttons
136
- (red/yellow/green) so brand-mini isn't covered. Removed in fullscreen
137
- where the buttons hide. */
138
- [data-platform="darwin"][data-fullscreen="0"] .topbar { padding-left: 84px; }
151
+ /* macOS hiddenInset traffic-light buttons sit top-left. Instead of squeezing
152
+ the brand to their RIGHT (84px gutter), drop the whole header row BELOW them
153
+ logo/title and the user chip live on their own line under the red/yellow/
154
+ green dots. Cleaner, and the brand isn't crammed beside the controls.
155
+ Removed in fullscreen where the buttons hide. */
156
+ [data-platform="darwin"][data-fullscreen="0"] .topbar { padding-top: 34px; }
139
157
  .brand-mini { display: inline-flex; align-items: center; gap: 10px; }
140
158
  .brand-mini .brand-name { font-size: 14px; }
141
159
 
@@ -144,10 +162,10 @@ body {
144
162
  display: inline-flex; align-items: center; gap: 10px;
145
163
  }
146
164
  .welcome {
147
- font-size: 12px; color: #34d399;
165
+ font-size: 12px; color: var(--text-dim);
148
166
  padding: 4px 10px; border-radius: 999px;
149
- background: rgba(16,185,129,.1);
150
- border: 1px solid rgba(16,185,129,.25);
167
+ background: rgba(255,255,255,.05);
168
+ border: 1px solid var(--line);
151
169
  animation: fadein 200ms ease;
152
170
  }
153
171
  @keyframes fadein { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
@@ -159,6 +177,82 @@ body {
159
177
  }
160
178
  .user-name { font-size: 13px; color: #c7cdd6; }
161
179
 
180
+ /* user-chip dropdown (我的钱包 / 我的订单 / HTTPS tip / 退出) */
181
+ .user-chip { position: relative; }
182
+ .user-chip__trigger {
183
+ -webkit-app-region: no-drag;
184
+ display: inline-flex; align-items: center; gap: 8px;
185
+ border: 1px solid transparent; background: transparent;
186
+ border-radius: 999px; padding: 3px 8px 3px 3px; cursor: pointer;
187
+ transition: .12s;
188
+ }
189
+ .user-chip__trigger:hover,
190
+ .user-chip__trigger.is-open {
191
+ background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.12);
192
+ }
193
+ .user-chip__caret { font-size: 10px; color: #8a93a3; transition: transform .12s; }
194
+ .user-chip__trigger.is-open .user-chip__caret { transform: rotate(180deg); }
195
+ .user-chip__menu {
196
+ position: absolute; top: 38px; right: 0; z-index: 30;
197
+ min-width: 200px; padding: 6px;
198
+ background: #1b2027; border: 1px solid rgba(255,255,255,.12);
199
+ border-radius: 12px; box-shadow: 0 12px 34px rgba(0,0,0,.5);
200
+ display: flex; flex-direction: column; gap: 2px;
201
+ animation: fadein 140ms ease;
202
+ }
203
+ .user-chip__menu-item {
204
+ text-align: left; width: 100%;
205
+ border: none; background: transparent; color: #d1d5db;
206
+ border-radius: 8px; padding: 9px 11px; font-size: 13px;
207
+ cursor: pointer; transition: .12s;
208
+ }
209
+ .user-chip__menu-item:hover { background: rgba(255,255,255,.07); color: #fff; }
210
+ .user-chip__menu-item.is-danger { color: #f7a3a3; }
211
+ .user-chip__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
212
+ .user-chip__menu-sep { height: 1px; margin: 4px 2px; background: rgba(255,255,255,.08); }
213
+ /* HTTPS audit tip, rendered as a flat menu row with an on/off switch */
214
+ .user-chip__mitm-row {
215
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
216
+ cursor: default;
217
+ }
218
+ .user-chip__mitm-row:hover { background: transparent; }
219
+ .user-chip__mitm-label { font-size: 13px; color: var(--text); }
220
+ /* mini toggle switch (开/关) */
221
+ .mini-switch {
222
+ -webkit-app-region: no-drag;
223
+ position: relative; flex: 0 0 auto;
224
+ width: 36px; height: 20px; padding: 0;
225
+ border-radius: 999px; border: 1px solid var(--line);
226
+ background: rgba(255,255,255,.08); cursor: pointer;
227
+ transition: background .16s ease, border-color .16s ease;
228
+ }
229
+ .mini-switch.is-on { background: var(--accent); border-color: var(--accent); }
230
+ .mini-switch.is-busy { opacity: .6; cursor: default; }
231
+ .mini-switch:disabled { cursor: default; }
232
+ .mini-switch__knob {
233
+ position: absolute; top: 1px; left: 1px;
234
+ width: 16px; height: 16px; border-radius: 50%;
235
+ background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.45);
236
+ transition: transform .16s ease;
237
+ }
238
+ .mini-switch.is-on .mini-switch__knob { transform: translateX(16px); }
239
+ .mini-switch.is-busy .mini-switch__knob { animation: spin 1s linear infinite; }
240
+ .user-chip__mitm-note { margin: 0 4px; padding: 2px 4px 6px; font-size: 11px; color: var(--text-mute); }
241
+ .user-chip__mitm-err {
242
+ margin: 2px 4px 6px; padding: 5px 8px; font-size: 11px;
243
+ color: var(--danger); background: rgba(239,68,68,.1); border-radius: 6px; line-height: 1.4;
244
+ }
245
+
246
+ /* cloud team card: 账单 entry (top-right, beside trial badge) */
247
+ .bcard__top-right { display: inline-flex; align-items: center; gap: 6px; }
248
+ .bcard__billing-btn {
249
+ -webkit-app-region: no-drag;
250
+ border: 1px solid var(--line); background: transparent; color: var(--text-dim);
251
+ border-radius: 7px; padding: 3px 9px; font-size: 11px; cursor: pointer;
252
+ transition: color .12s, border-color .12s, background .12s;
253
+ }
254
+ .bcard__billing-btn:hover { color: var(--accent-text); border-color: var(--accent-line); background: var(--accent-soft); }
255
+
162
256
  .glow--app {
163
257
  inset: -10% -10% auto -10%;
164
258
  height: 50vh;
@@ -276,9 +370,9 @@ body {
276
370
  border: 1px solid rgba(52,211,153,.3);
277
371
  }
278
372
  .bcard__badge--trial {
279
- background: rgba(251,191,36,.15);
280
- color: #fcd34d;
281
- border: 1px solid rgba(251,191,36,.3);
373
+ background: rgba(255,255,255,.06);
374
+ color: var(--text-dim);
375
+ border: 1px solid var(--line);
282
376
  font-size: 9.5px;
283
377
  letter-spacing: 0.3px;
284
378
  padding: 2px 7px;
@@ -309,10 +403,10 @@ body {
309
403
  opacity: .8;
310
404
  }
311
405
  .bcard__cta--helper {
312
- background: linear-gradient(90deg, #5b8df7, #a78bfa);
313
- box-shadow: 0 6px 18px -4px rgba(167,139,250,.45);
406
+ background: rgba(255,255,255,.05);
407
+ box-shadow: none;
314
408
  }
315
- .bcard__cta--helper:hover { filter: brightness(1.1); }
409
+ .bcard__cta--helper:hover { background: var(--accent-soft); }
316
410
 
317
411
  /* ── Helper drawer top bar (close button) ── */
318
412
  .helper-aside__top {
@@ -563,8 +657,8 @@ body {
563
657
  display: inline-flex; align-items: center;
564
658
  padding: 2px 8px;
565
659
  border-radius: 999px;
566
- background: rgba(251,191,36,.12);
567
- color: #fbbf24;
660
+ background: var(--accent-soft);
661
+ color: var(--accent-text);
568
662
  font-size: 10px; font-weight: 700;
569
663
  text-transform: uppercase; letter-spacing: 0.5px;
570
664
  }
@@ -604,27 +698,29 @@ body {
604
698
  border-radius: 999px;
605
699
  }
606
700
 
701
+ /* One calm, uniform 打开 button for EVERY card type — no loud blue/amber/purple
702
+ fills or colored shadows (主人: 打开 别用不同颜色太醒目的 btn). Quiet neutral
703
+ surface that warms to the single accent on hover. */
607
704
  .bcard__cta {
608
705
  position: relative; z-index: 1;
609
706
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
610
707
  width: 100%; height: 38px;
611
- border: 0; border-radius: 9px;
612
- background: var(--brand); color: #fff;
708
+ border: 1px solid var(--line); border-radius: 9px;
709
+ background: rgba(255,255,255,.05); color: var(--text);
613
710
  font-size: 13px; font-weight: 600;
614
711
  letter-spacing: 0.1px;
615
712
  cursor: pointer;
616
- transition: transform 80ms ease, background 160ms ease, box-shadow 160ms ease, filter 160ms ease;
617
- box-shadow: 0 4px 12px -2px rgba(91,141,247,.45);
618
- }
619
- .bcard--cloud .bcard__cta {
620
- background: var(--accent-cloud);
621
- box-shadow: 0 4px 12px -2px rgba(245,158,11,.45);
713
+ transition: transform 80ms ease, background 140ms ease, border-color 140ms ease, color 140ms ease;
714
+ box-shadow: none;
622
715
  }
716
+ /* card-type variants inherit the same quiet style (overrides removed) */
717
+ .bcard--cloud .bcard__cta,
623
718
  .bcard--custom .bcard__cta {
624
- background: var(--accent-custom);
625
- box-shadow: 0 4px 12px -2px rgba(139,92,246,.45);
719
+ background: rgba(255,255,255,.05); box-shadow: none;
720
+ }
721
+ .bcard__cta:hover {
722
+ background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-text);
626
723
  }
627
- .bcard__cta:hover { filter: brightness(1.08); }
628
724
  .bcard__cta:active { transform: translateY(1px); }
629
725
  .bcard__cta:disabled {
630
726
  background: rgba(255,255,255,.03);
@@ -676,8 +772,8 @@ body {
676
772
  cursor: pointer; transition: .12s;
677
773
  }
678
774
  .bcard__menu-item:hover { background: rgba(255,255,255,.07); color: #fff; }
679
- .bcard__menu-item.is-accent { color: #fbbf24; font-weight: 600; }
680
- .bcard__menu-item.is-accent:hover { background: rgba(245,158,11,.15); color: #fcd34d; }
775
+ .bcard__menu-item.is-accent { color: var(--accent-text); font-weight: 600; }
776
+ .bcard__menu-item.is-accent:hover { background: var(--accent-soft); color: #c7dbff; }
681
777
  .bcard__menu-item.is-danger { color: #f7a3a3; }
682
778
  .bcard__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
683
779
  /* ---- Windows Docker 一键安装/启动卡 (DockerSetup) ---- */
@@ -723,9 +819,9 @@ body {
723
819
  }
724
820
  /* "新版 vX.Y.Z" chip on the card face */
725
821
  .bcard__chip--new {
726
- color: #fbbf24;
727
- background: rgba(245,158,11,.12);
728
- border-color: rgba(245,158,11,.3);
822
+ color: var(--accent-text);
823
+ background: var(--accent-soft);
824
+ border-color: var(--accent-line);
729
825
  }
730
826
  .bcard__opmsg {
731
827
  display: flex; align-items: center; gap: 6px;