dsh-ds-balance 2.1.2 → 2.2.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 (48) hide show
  1. package/README.md +78 -19
  2. package/README_en.md +81 -19
  3. package/lib/adapters/domain-core-store.d.ts +2 -2
  4. package/lib/adapters/domain-core-store.js +3 -1
  5. package/lib/adapters/host-account.d.ts +66 -0
  6. package/lib/adapters/host-account.js +68 -0
  7. package/lib/client.js +473 -114
  8. package/lib/domain/account.d.ts +40 -0
  9. package/lib/domain/account.js +74 -0
  10. package/lib/domain/balance.d.ts +20 -1
  11. package/lib/domain/normalize.d.ts +3 -2
  12. package/lib/domain/normalize.js +3 -2
  13. package/lib/domain/severity.d.ts +8 -2
  14. package/lib/domain/severity.js +11 -4
  15. package/lib/domain/situation.d.ts +104 -0
  16. package/lib/domain/situation.js +93 -0
  17. package/lib/http/handlers.d.ts +1 -1
  18. package/lib/http/handlers.js +29 -5
  19. package/lib/http/wire.d.ts +11 -1
  20. package/lib/http/wire.js +2 -0
  21. package/lib/index.js +122 -6
  22. package/lib/ports/account.d.ts +27 -0
  23. package/lib/ports/account.js +9 -0
  24. package/lib/services/balance-service.d.ts +66 -85
  25. package/lib/services/balance-service.js +132 -181
  26. package/lib/services/balance-source.d.ts +174 -0
  27. package/lib/services/balance-source.js +294 -0
  28. package/lib/services/scheduler.d.ts +31 -2
  29. package/lib/services/scheduler.js +29 -2
  30. package/lib/services/source-readers.d.ts +26 -0
  31. package/lib/services/source-readers.js +73 -0
  32. package/lib/services/source-selector.d.ts +50 -0
  33. package/lib/services/source-selector.js +64 -0
  34. package/lib/types/client/api-types.d.ts +16 -0
  35. package/lib/types/client/data.d.ts +12 -3
  36. package/lib/types/client/index.d.ts +6 -0
  37. package/lib/types/client/locales.d.ts +10 -18
  38. package/lib/types/client/mock/index.d.ts +12 -11
  39. package/lib/types/client/mock/scenarios.d.ts +13 -5
  40. package/lib/types/client/model.d.ts +49 -25
  41. package/lib/types/client/route-hint.d.ts +61 -0
  42. package/lib/types/client/sidebar/BalancePopover.d.ts +7 -0
  43. package/lib/types/client/sidebar/PercentRing.d.ts +13 -5
  44. package/lib/types/client/sidebar/SidebarBalance.d.ts +9 -1
  45. package/lib/types/client/situation.d.ts +164 -0
  46. package/lib/version.d.ts +1 -1
  47. package/lib/version.js +1 -1
  48. package/package.json +25 -23
package/lib/client.js CHANGED
@@ -42,11 +42,14 @@ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primi
42
42
  var NS = "ds-balance";
43
43
  var zh = {
44
44
  "sidebar.label": "DeepSeek \u4F59\u989D",
45
- "sidebar.aria.balance": "DeepSeek \u4F59\u989D",
46
45
  "sidebar.aria.ring": "DeepSeek \u4F59\u989D\u72B6\u6001",
47
46
  "sidebar.aria.refresh": "\u5237\u65B0\u4F59\u989D",
48
47
  "sidebar.aria.mismatch": "\u663E\u793A\u7684\u5E01\u79CD\u4E0E\u4F60\u9009\u62E9\u7684\u4E0D\u4E00\u81F4",
49
48
  "popover.title": "\u8D26\u6237\u4F59\u989D",
49
+ // 来源标签:跟着数据来源走。**括号属于词典** —— 中文全角、英文半角带前置空格,
50
+ // 代码里只做拼接,绝不写死括号字符。
51
+ "popover.source.key": "\uFF08API Key\uFF09",
52
+ "popover.source.account": "\uFF08\u8D26\u53F7\u767B\u5F55\uFF09",
50
53
  "popover.total": "\u4F59\u989D",
51
54
  "popover.granted": "\u8D60\u9001",
52
55
  "popover.toppedUp": "\u5145\u503C",
@@ -60,12 +63,33 @@ var zh = {
60
63
  // 措辞就跟着落点分档:一句含糊的话盖不住两种落点。见 BalancePopover 的 PluginsAction。
61
64
  "popover.action.openPlugins": "\u6253\u5F00\u63D2\u4EF6\u9875",
62
65
  "popover.action.openPluginConfig": "\u6253\u5F00\u63D2\u4EF6\u914D\u7F6E\u9875",
63
- "state.unavailable": "\u8D26\u6237\u4E0D\u53EF\u7528",
64
- "state.stale": "\u6570\u636E\u5DF2\u8FC7\u671F",
65
- "state.error": "\u8BFB\u53D6\u5931\u8D25",
66
- "state.empty": "\u5C1A\u672A\u914D\u7F6E",
67
- "state.noKey": "\u5C1A\u672A\u586B\u5199 API Key",
68
- "state.noBalance": "\u6682\u65E0\u4F59\u989D",
66
+ // 处境文案:**一个处境一句**,环 / 悬停 / 浮层都从这里取。
67
+ // 键名前缀是 situation 而不是 state:后者是契约字段名,而「处境」是界面概念
68
+ // (一个处境可能吃掉多个 state + severity + error 的组合)。
69
+ //
70
+ // **措辞是给用户看的,不是给开发者看的**。这七句会出现在两处,都必须能**单独读懂**:
71
+ // - **悬停气泡**(收起态与展开态同一份,见 `client/situation.ts` 的 `hoverTextOf`):
72
+ // 数字**可信**时气泡显示金额本身(只有 `ok` / `low` / `critical` 三个处境,
73
+ // 它们的 `textKey` 是 `null`);其余八个处境显示下面这些句子 —— 包括
74
+ // `stale` 与 `account-unavailable`,它们**手里有数字也显文案**;
75
+ // - 浮层与读屏的 `aria-label`。
76
+ //
77
+ // 所以每一句都要能单独回答「**这一格现在怎么了**」:
78
+ //
79
+ // - 不写「插件内部错误」:用户不知道也不关心是哪个组件坏了,他要的是「现在拿不到」。
80
+ // 改「服务异常」—— 说的是同一件事,但主语是这个功能,不是我们的代码。
81
+ // - `unavailable` 刻意**不带「暂时」**。它接住的是 `fetch-failed` 那一整片错误码,
82
+ // 里面既有等一等就好的(网络 / 超时 / 429 / 5xx),也有**等不好的**:
83
+ // `UPSTREAM_401`(key 失效)与 `UPSTREAM_402`(欠费)。对后者说「暂时」是假话,
84
+ // 而且是有害的假话 —— 用户该去改凭据、去充值,不是在原地等。
85
+ // 宁可少说一层时间性,也不能说反。
86
+ "situation.checking": "\u6B63\u5728\u83B7\u53D6",
87
+ "situation.unavailable": "\u8BFB\u4E0D\u5230\u4F59\u989D",
88
+ "situation.internalError": "\u670D\u52A1\u5F02\u5E38",
89
+ "situation.noCredential": "\u5C1A\u672A\u914D\u7F6E\u51ED\u636E",
90
+ "situation.accountUnavailable": "\u8D26\u6237\u4E0D\u53EF\u7528",
91
+ "situation.stale": "\u6570\u636E\u5DF2\u8FC7\u671F",
92
+ "situation.emptyWallet": "\u6682\u65E0\u4F59\u989D",
69
93
  "settings.group.connection": "\u8FDE\u63A5",
70
94
  "settings.group.refresh": "\u5237\u65B0",
71
95
  "settings.group.display": "\u5C55\u793A",
@@ -81,15 +105,13 @@ var zh = {
81
105
  "settings.field.cnyCritical": "CNY \u544A\u6025",
82
106
  "settings.field.usdWarn": "USD \u9884\u8B66",
83
107
  "settings.field.usdCritical": "USD \u544A\u6025",
84
- "settings.hint.apiKey": "\u7559\u7A7A\u5219\u6539\u7528\u5F15\u7528\u540D\u4ECE\u51ED\u636E\u5B58\u50A8\u8BFB\u53D6\u3002",
85
108
  "settings.hint.apiKeyRef": "\u51ED\u636E\u5B58\u50A8\u91CC\u7684\u6761\u76EE\u540D\uFF1B\u975E\u7A7A\u65F6\u4F18\u5148\u4E8E API Key\u3002",
86
109
  "settings.hint.baseUrl": "\u7559\u7A7A\u5219\u4F7F\u7528\u63D0\u4F9B\u65B9\u9ED8\u8BA4\u5730\u5740\u3002",
87
110
  "settings.hint.displayCurrency": "\u6240\u9009\u5E01\u79CD\u4E0D\u5B58\u5728\u65F6\uFF0C\u56DE\u843D\u5230\u8D26\u6237\u5B9E\u9645\u6301\u6709\u7684\u5E01\u79CD\u3002",
88
- "settings.hint.number": "\u8BF7\u586B\u6570\u5B57\uFF1B\u7559\u7A7A\u8868\u793A\u4F7F\u7528\u9ED8\u8BA4\u503C\u3002",
89
111
  "settings.hint.threshold": "\u540E\u7AEF\u5224\u5B9A\uFF1B\u754C\u9762\u989C\u8272\u8DDF\u968F\u8FD4\u56DE\u7684\u4E25\u91CD\u5EA6\u3002",
90
112
  "settings.hint.thresholdPair": "\u544A\u6025\u503C\u5FC5\u987B\u4F4E\u4E8E\u9884\u8B66\u503C",
91
113
  "settings.hint.serverRefreshSeconds": "\u8BBF\u95EE\u5B98\u65B9\u63A5\u53E3\u7684\u95F4\u9694\u3002",
92
- "settings.hint.clientPollSeconds": "\u5237\u65B0\u663E\u793A\u7684\u95F4\u9694\uFF1B\u53EA\u8BFB\u672C\u5730\u7F13\u5B58\uFF0C\u4E0D\u8BBF\u95EE\u4E0A\u6E38\u3002",
114
+ "settings.hint.clientPollSeconds": "\u5237\u65B0\u663E\u793A\u7684\u95F4\u9694\uFF1B\u6B63\u5E38\u65F6\u53EA\u8BFB\u7F13\u5B58\uFF0C\u8BFB\u4E0D\u5230\u65F6\u624D\u8BBF\u95EE\u4E0A\u6E38\u3002",
93
115
  "settings.hint.manualRefreshCooldownSeconds": "\u4E24\u6B21\u624B\u52A8\u5237\u65B0\u4E4B\u95F4\u7684\u6700\u77ED\u95F4\u9694\u3002",
94
116
  "settings.currency.auto": "\u81EA\u52A8\uFF08\u8DDF\u968F\u8D26\u6237\uFF09",
95
117
  // 凭据行的**状态徽章**:两条逐字用官方措辞 —— 取自 ui-settings-plugins 的
@@ -115,21 +137,24 @@ var zh = {
115
137
  "settings.overridden": "\u5DF2\u8986\u76D6",
116
138
  "settings.reset": "\u91CD\u7F6E",
117
139
  "settings.invalidNumber": "\u8BF7\u586B\u6570\u5B57\uFF1B\u7559\u7A7A\u8868\u793A\u4F7F\u7528\u9ED8\u8BA4\u503C\u3002",
118
- "settings.hint.refreshAdvanced": "\u4E0D\u5E38\u6539\uFF1A\u9ED8\u8BA4\u503C\u9002\u7528\u4E8E\u5927\u591A\u6570\u60C5\u51B5\u3002",
119
- "settings.currencyMismatch": "\u5F53\u524D\u8D26\u6237\u65E0 {wanted} \u4F59\u989D\uFF0C\u5B9E\u9645\u663E\u793A {shown}",
140
+ "settings.hint.refreshAdvanced": "\u4E0D\u5E38\u6539\uFF1A\u9ED8\u8BA4\u503C\u9002\u7528\u4E8E\u5927\u591A\u6570\u60C5\u51B5\u3002"
120
141
  // 这里曾有一条 'settings.summary'(该行 Configure 子页的说明行)。回退到
121
142
  // plugins.bundle.config 之后没有任何渲染路径会问 summary 档,于是连同它一起删了 ——
122
143
  // 详见 src/client/index.tsx 的 SettingsSeat。
123
- "dev.title": "\u5F00\u53D1\u573A\u666F",
124
- "dev.hint": "\u4EC5\u7528\u4E8E UI \u5F00\u53D1\uFF1B\u4E0D\u5F71\u54CD\u771F\u5B9E\u914D\u7F6E\u3002"
144
+ // 本轮又删掉四条**孤儿键**(词典里有、全仓没人引用):`sidebar.aria.balance`
145
+ // (与 sidebar.label 逐字相同,rail 的 aria 走 aria.ring)、`settings.hint.apiKey`
146
+ // (那一行用的是 settings.hint.credential)、`settings.hint.number`
147
+ // (真正用的是 settings.invalidNumber,两者文案原本就重复)、
148
+ // `settings.currencyMismatch`(出到屏幕的是 popover.mismatch)。
125
149
  };
126
150
  var en = {
127
151
  "sidebar.label": "DeepSeek balance",
128
- "sidebar.aria.balance": "DeepSeek balance",
129
152
  "sidebar.aria.ring": "DeepSeek balance status",
130
153
  "sidebar.aria.refresh": "Refresh balance",
131
154
  "sidebar.aria.mismatch": "Shown currency differs from your selection",
132
155
  "popover.title": "Account balance",
156
+ "popover.source.key": " (API key)",
157
+ "popover.source.account": " (account sign-in)",
133
158
  "popover.total": "Balance",
134
159
  "popover.granted": "Granted",
135
160
  "popover.toppedUp": "Topped up",
@@ -141,12 +166,16 @@ var en = {
141
166
  "popover.action.useShown": "Use {shown}",
142
167
  "popover.action.openPlugins": "Open the Plugins page",
143
168
  "popover.action.openPluginConfig": "Open the plugin configuration page",
144
- "state.unavailable": "Account unavailable",
145
- "state.stale": "Data is stale",
146
- "state.error": "Failed to load",
147
- "state.empty": "Not configured",
148
- "state.noKey": "API key missing",
149
- "state.noBalance": "No balance",
169
+ // 与 zh 逐条对应,理由写在 zh 那一段。
170
+ // `unavailable` 同样**不带 Temporarily**:它接住 401(key 失效)/ 402(欠费)这类
171
+ // 「等不好」的错误,说「暂时」会把人劝去干等。
172
+ "situation.checking": "Loading",
173
+ "situation.unavailable": "Balance unavailable",
174
+ "situation.internalError": "Service error",
175
+ "situation.noCredential": "No credential configured",
176
+ "situation.accountUnavailable": "Account unavailable",
177
+ "situation.stale": "Data is stale",
178
+ "situation.emptyWallet": "No balance",
150
179
  "settings.group.connection": "Connection",
151
180
  "settings.group.refresh": "Refresh",
152
181
  "settings.group.display": "Display",
@@ -162,15 +191,13 @@ var en = {
162
191
  "settings.field.cnyCritical": "CNY critical",
163
192
  "settings.field.usdWarn": "USD warning",
164
193
  "settings.field.usdCritical": "USD critical",
165
- "settings.hint.apiKey": "Leave empty to resolve from the credential reference instead.",
166
194
  "settings.hint.apiKeyRef": "Credential store entry name; takes precedence over the API key.",
167
195
  "settings.hint.baseUrl": "Leave blank to use the provider default.",
168
196
  "settings.hint.displayCurrency": "Falls back to a currency the account actually holds.",
169
- "settings.hint.number": "Enter a number; leave empty to use the default.",
170
197
  "settings.hint.threshold": "Decided on the server; the UI colour follows the returned severity.",
171
198
  "settings.hint.thresholdPair": "Critical must be lower than warning",
172
199
  "settings.hint.serverRefreshSeconds": "How often the official API is called.",
173
- "settings.hint.clientPollSeconds": "How often the display refreshes; local cache only.",
200
+ "settings.hint.clientPollSeconds": "How often the display refreshes; cache-only while healthy.",
174
201
  "settings.hint.manualRefreshCooldownSeconds": "Minimum gap between two manual refreshes.",
175
202
  "settings.currency.auto": "Automatic (follow account)",
176
203
  "settings.credential.configured": "A key is configured.",
@@ -192,10 +219,8 @@ var en = {
192
219
  "settings.overridden": "Overridden",
193
220
  "settings.reset": "Reset",
194
221
  "settings.invalidNumber": "Enter a number; leave empty to use the default.",
195
- "settings.hint.refreshAdvanced": "Rarely changed: the defaults suit most setups.",
196
- "settings.currencyMismatch": "The account has no {wanted} balance; showing {shown}",
197
- "dev.title": "Dev scenario",
198
- "dev.hint": "UI development only; does not affect real configuration."
222
+ "settings.hint.refreshAdvanced": "Rarely changed: the defaults suit most setups."
223
+ // 与 zh 同批删掉四条孤儿键,以及 `dev.*`(见 zh 那段末尾的说明)。
199
224
  };
200
225
  function interpolate(template, params) {
201
226
  return template.replace(
@@ -411,16 +436,7 @@ function SelectorControl(props) {
411
436
  }
412
437
  function ActionRow(props) {
413
438
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
414
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fields_default.actions, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
415
- import_dsh_client_ui_primitives.Button,
416
- {
417
- type: "button",
418
- variant: "outline",
419
- disabled: props.disabled,
420
- onClick: props.onClick,
421
- children: props.label
422
- }
423
- ) }),
439
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fields_default.actions, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { type: "button", variant: "outline", disabled: props.disabled, onClick: props.onClick, children: props.label }) }),
424
440
  props.result === null ? null : (
425
441
  // 成功只是通报,用 polite 的 status;失败需要用户处理,按 06 §6.7 的「行内错误」用 alert。
426
442
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -862,6 +878,9 @@ function pendingView() {
862
878
  return {
863
879
  requestId: "",
864
880
  schemaVersion: 1,
881
+ // 占位视图没有取数路可言;处境为 checking / offline 时界面都不渲染来源标签。
882
+ source: "deepseek-http",
883
+ situation: "checking",
865
884
  state: "empty",
866
885
  stale: false,
867
886
  fetchedAt: 0,
@@ -879,6 +898,7 @@ function pendingView() {
879
898
  function unreachableView(message) {
880
899
  return {
881
900
  ...pendingView(),
901
+ situation: "offline",
882
902
  state: "error",
883
903
  error: { code: UNREACHABLE_CODE, message }
884
904
  };
@@ -893,7 +913,11 @@ async function readJson(response, path) {
893
913
  }
894
914
  async function requestBalance(options) {
895
915
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
896
- const url = `${BALANCE_PATH}?currency=${encodeURIComponent(options.currency)}`;
916
+ const query = new URLSearchParams({ currency: options.currency });
917
+ if (options.provider !== void 0 && options.provider !== "") {
918
+ query.set("provider", options.provider);
919
+ }
920
+ const url = `${BALANCE_PATH}?${query.toString()}`;
897
921
  const response = await fetchImpl(url, {
898
922
  headers: { accept: "application/json" },
899
923
  ...options.signal === void 0 ? {} : { signal: options.signal }
@@ -922,7 +946,10 @@ async function requestRefresh(options = {}) {
922
946
  const response = await fetchImpl(REFRESH_PATH, {
923
947
  method: "POST",
924
948
  headers: { "content-type": "application/json", accept: "application/json" },
925
- body: JSON.stringify({ reason: options.reason ?? "manual" })
949
+ body: JSON.stringify({
950
+ reason: options.reason ?? "manual",
951
+ ...options.provider === void 0 || options.provider === "" ? {} : { provider: options.provider }
952
+ })
926
953
  });
927
954
  return await readJson(response, REFRESH_PATH);
928
955
  }
@@ -1292,12 +1319,6 @@ function dotStateOf(severity) {
1292
1319
  return "idle";
1293
1320
  }
1294
1321
  }
1295
- function ringSpecOf(severity) {
1296
- return {
1297
- state: dotStateOf(severity),
1298
- marker: severity === "unavailable" ? "cross" : null
1299
- };
1300
- }
1301
1322
  var DECIMAL_SCALE = 100000000n;
1302
1323
  var ARC_STEPS = 10000n;
1303
1324
  var DECIMAL_PATTERN = /^(-?)(\d+)(?:\.(\d*))?$/;
@@ -1377,6 +1398,166 @@ function currentAgeMs(seenAt, seenAgeMs, now) {
1377
1398
  const base = Number.isFinite(seenAgeMs) ? Math.max(0, seenAgeMs) : 0;
1378
1399
  return Math.max(0, base + (now - seenAt));
1379
1400
  }
1401
+ var MS_PER_SECOND = 1e3;
1402
+ function sourceLabelKeyOf(source) {
1403
+ if (source === "deepseek-http") return "popover.source.key";
1404
+ if (source === "deepseek-account") return "popover.source.account";
1405
+ return null;
1406
+ }
1407
+ function cooldownUntilOf(now, configuredSeconds, rejectedRemainingMs) {
1408
+ const span = rejectedRemainingMs === null ? Math.max(0, configuredSeconds) * MS_PER_SECOND : Math.max(0, rejectedRemainingMs);
1409
+ return now + span;
1410
+ }
1411
+ function cooldownSecondsOf(until, now) {
1412
+ return Math.max(0, Math.ceil((until - now) / MS_PER_SECOND));
1413
+ }
1414
+
1415
+ // src/client/situation.ts
1416
+ var SITUATIONS = [
1417
+ "checking",
1418
+ "offline",
1419
+ "internal-error",
1420
+ "no-credential",
1421
+ "fetch-failed",
1422
+ "account-unavailable",
1423
+ "stale",
1424
+ "empty-wallet",
1425
+ "ok",
1426
+ "low",
1427
+ "critical"
1428
+ ];
1429
+ var SHAPES = {
1430
+ // —— 客户端本地事实(宿主不可能知道这两件事)——
1431
+ checking: {
1432
+ family: "pending",
1433
+ ring: "ongoing",
1434
+ marker: null,
1435
+ arc: "none",
1436
+ textKey: "situation.checking",
1437
+ severityColoured: false
1438
+ },
1439
+ offline: {
1440
+ family: "unreadable",
1441
+ ring: "error",
1442
+ marker: "cross",
1443
+ arc: "none",
1444
+ textKey: "situation.unavailable",
1445
+ severityColoured: false
1446
+ },
1447
+ // —— 宿主判定 ——
1448
+ "internal-error": {
1449
+ family: "unreadable",
1450
+ ring: "error",
1451
+ marker: "cross",
1452
+ arc: "none",
1453
+ textKey: "situation.internalError",
1454
+ severityColoured: false
1455
+ },
1456
+ "no-credential": {
1457
+ family: "needs-credential",
1458
+ ring: "idle",
1459
+ marker: "plus",
1460
+ arc: "none",
1461
+ textKey: "situation.noCredential",
1462
+ severityColoured: false
1463
+ },
1464
+ "fetch-failed": {
1465
+ family: "unreadable",
1466
+ ring: "error",
1467
+ marker: "cross",
1468
+ arc: "none",
1469
+ textKey: "situation.unavailable",
1470
+ severityColoured: false
1471
+ },
1472
+ "account-unavailable": {
1473
+ family: "unreadable",
1474
+ // **颜色定死为红,不吃 severity。**
1475
+ // 它是「账户停用」—— 这个事实本身就决定了红,与余额多少无关;
1476
+ // 而上游对欠费账户可能给空的余额列表(selected=null → severity=unknown → 灰环),
1477
+ // 那会把「停用」画成「没信息」。定死红环是**结构保证**,不依赖另一处的判定顺序。
1478
+ ring: "error",
1479
+ // **叉:与「余额告急」分开的唯一办法。**
1480
+ // 从前它靠「红环无叉」与 critical 区分,但余额恰好为 0 的 critical 也是红环无叉无弧 ——
1481
+ // 两者在屏幕上逐像素相同(见 test/render-matrix.test.ts 的「两枚红环」一组)。
1482
+ // 叉的语义从「读不到」扩到「读不到**或用不了**」:两者同属「这里没有可用的数字」。
1483
+ marker: "cross",
1484
+ arc: "none",
1485
+ textKey: "situation.accountUnavailable",
1486
+ severityColoured: false
1487
+ },
1488
+ stale: {
1489
+ family: "gauge",
1490
+ ring: "done",
1491
+ marker: null,
1492
+ arc: "gauge",
1493
+ textKey: "situation.stale",
1494
+ severityColoured: true
1495
+ },
1496
+ "empty-wallet": {
1497
+ family: "empty",
1498
+ ring: "idle",
1499
+ marker: null,
1500
+ arc: "none",
1501
+ textKey: "situation.emptyWallet",
1502
+ severityColoured: false
1503
+ },
1504
+ ok: {
1505
+ family: "gauge",
1506
+ ring: "done",
1507
+ marker: null,
1508
+ arc: "gauge",
1509
+ textKey: null,
1510
+ severityColoured: true
1511
+ },
1512
+ low: {
1513
+ family: "gauge",
1514
+ ring: "warning",
1515
+ marker: null,
1516
+ arc: "gauge",
1517
+ textKey: null,
1518
+ severityColoured: true
1519
+ },
1520
+ critical: {
1521
+ family: "gauge",
1522
+ ring: "error",
1523
+ marker: null,
1524
+ arc: "gauge",
1525
+ textKey: null,
1526
+ severityColoured: true
1527
+ }
1528
+ };
1529
+ function presentationOf(situation, severity) {
1530
+ const shape = SHAPES[situation];
1531
+ if (!shape.severityColoured) return shape;
1532
+ return { ...shape, ring: dotStateOf(severity) };
1533
+ }
1534
+ function hoverTextOf(shown, stateText) {
1535
+ if (stateText !== null) return stateText;
1536
+ return shown === null ? null : formatMoney(shown.total, shown.currency);
1537
+ }
1538
+ function isSituation(value) {
1539
+ return typeof value === "string" && SITUATIONS.includes(value);
1540
+ }
1541
+ function legacySituationOf(response) {
1542
+ const hasSnapshot = response.fetchedAt > 0 || response.selected !== null;
1543
+ const code = response.error?.code;
1544
+ if (!hasSnapshot) {
1545
+ if (code === "NO_KEY") return "no-credential";
1546
+ if (code === "PLUGIN_UNREACHABLE") return "offline";
1547
+ return response.state === "empty" && code === void 0 ? "checking" : "fetch-failed";
1548
+ }
1549
+ if (!response.isAvailable) return "account-unavailable";
1550
+ if (response.selected === null)
1551
+ return response.state === "stale" ? "fetch-failed" : "empty-wallet";
1552
+ if (response.state === "stale") return "stale";
1553
+ if (response.severity === "critical") return "critical";
1554
+ if (response.severity === "warn") return "low";
1555
+ if (response.severity === "unavailable" || response.severity === "unknown") return "fetch-failed";
1556
+ return "ok";
1557
+ }
1558
+ function situationOfResponse(response) {
1559
+ return isSituation(response.situation) ? response.situation : legacySituationOf(response);
1560
+ }
1380
1561
 
1381
1562
  // src/client/mock/scenarios.ts
1382
1563
  var T0 = 176e10;
@@ -1390,6 +1571,8 @@ function make(patch) {
1390
1571
  return {
1391
1572
  requestId: "req_mock",
1392
1573
  schemaVersion: 1,
1574
+ source: "deepseek-http",
1575
+ situation: "ok",
1393
1576
  state: "ok",
1394
1577
  stale: false,
1395
1578
  fetchedAt: T0,
@@ -1408,27 +1591,33 @@ function make(patch) {
1408
1591
  var scenarios = {
1409
1592
  /** 正常。 */
1410
1593
  ok: make({}),
1594
+ /** 正常,但数字来自账号登录那条路(用来眼看「来源标签」)。 */
1595
+ account: make({ source: "deepseek-account" }),
1411
1596
  /** 余额偏低但可用。 */
1412
1597
  warn: make({
1598
+ situation: "low",
1413
1599
  severity: "warn",
1414
1600
  balances: [cny("8.00000000", "0.00000000", "8.00000000")],
1415
1601
  selected: { currency: "CNY", total: "8.00000000" }
1416
1602
  }),
1417
1603
  /** 余额告急。 */
1418
1604
  critical: make({
1605
+ situation: "critical",
1419
1606
  severity: "critical",
1420
1607
  balances: [cny("3.00000000", "0.00000000", "3.00000000")],
1421
1608
  selected: { currency: "CNY", total: "3.00000000" }
1422
1609
  }),
1423
- /** 账户不可用(余额耗尽 / 欠费)。 */
1610
+ /** 账户不可用(余额耗尽 / 欠费)。读到了,但上游说这个账户停用。 */
1424
1611
  unavailable: make({
1612
+ situation: "account-unavailable",
1425
1613
  severity: "unavailable",
1426
1614
  isAvailable: false,
1427
1615
  balances: [cny("0.00000000", "0.00000000", "0.00000000")],
1428
1616
  selected: { currency: "CNY", total: "0.00000000" }
1429
1617
  }),
1430
- /** 有旧值但本次刷新失败。 */
1618
+ /** 有旧值但本次刷新失败:数字照常显示,文案说它旧。 */
1431
1619
  stale: make({
1620
+ situation: "stale",
1432
1621
  state: "stale",
1433
1622
  stale: true,
1434
1623
  severity: "unknown",
@@ -1436,8 +1625,9 @@ var scenarios = {
1436
1625
  fetchedAt: T0 - 36e5,
1437
1626
  error: { code: "NO_NETWORK", message: "fetch failed" }
1438
1627
  }),
1439
- /** 无值失败。 */
1628
+ /** 无值失败:接入了却读不到(没有旧快照)。环是叉。 */
1440
1629
  error: make({
1630
+ situation: "fetch-failed",
1441
1631
  state: "error",
1442
1632
  stale: false,
1443
1633
  severity: "unknown",
@@ -1447,8 +1637,33 @@ var scenarios = {
1447
1637
  ageMs: 0,
1448
1638
  error: { code: "UPSTREAM_5XX", message: "upstream returned 500" }
1449
1639
  }),
1450
- /** 未配置。 */
1640
+ /** 插件自己的端点不可达(旧契约里没有这个信号,靠客户端本地合成)。 */
1641
+ offline: make({
1642
+ situation: "offline",
1643
+ state: "error",
1644
+ stale: false,
1645
+ severity: "unknown",
1646
+ isAvailable: false,
1647
+ balances: [],
1648
+ selected: null,
1649
+ ageMs: 0,
1650
+ error: { code: UNREACHABLE_CODE, message: "balance endpoint responded 502" }
1651
+ }),
1652
+ /** 插件自己出错了(装配错误之类):与「上游坏了」分开说。 */
1653
+ internalError: make({
1654
+ situation: "internal-error",
1655
+ state: "error",
1656
+ stale: false,
1657
+ severity: "unknown",
1658
+ isAvailable: false,
1659
+ balances: [],
1660
+ selected: null,
1661
+ ageMs: 0,
1662
+ error: { code: "STORAGE_ERROR", message: "no ledger for source deepseek-account" }
1663
+ }),
1664
+ /** 未配置:尚无任何解释。 */
1451
1665
  empty: make({
1666
+ situation: "empty-wallet",
1452
1667
  state: "empty",
1453
1668
  severity: "unknown",
1454
1669
  isAvailable: false,
@@ -1456,8 +1671,9 @@ var scenarios = {
1456
1671
  selected: null,
1457
1672
  ageMs: 0
1458
1673
  }),
1459
- /** 未配置且带 NO_KEY 错误。 */
1674
+ /** 未配置且带 NO_KEY 错误 —— 需要用户去配置的那一族。 */
1460
1675
  noKey: make({
1676
+ situation: "no-credential",
1461
1677
  state: "empty",
1462
1678
  severity: "unknown",
1463
1679
  isAvailable: false,
@@ -1466,6 +1682,16 @@ var scenarios = {
1466
1682
  ageMs: 0,
1467
1683
  error: { code: "NO_KEY", message: "no API key configured" }
1468
1684
  }),
1685
+ /** 首帧:还没问到后端。 */
1686
+ checking: make({
1687
+ situation: "checking",
1688
+ state: "empty",
1689
+ severity: "unknown",
1690
+ isAvailable: false,
1691
+ balances: [],
1692
+ selected: null,
1693
+ ageMs: 0
1694
+ }),
1469
1695
  /** 多币种。`selected` 显式写出,不靠 `make()` 的默认值兜。 */
1470
1696
  multiCurrency: make({
1471
1697
  balances: [
@@ -1490,6 +1716,7 @@ var scenarios = {
1490
1716
  }),
1491
1717
  /** 账户完全没有余额。 */
1492
1718
  noBalanceAtAll: make({
1719
+ situation: "empty-wallet",
1493
1720
  severity: "unknown",
1494
1721
  isAvailable: true,
1495
1722
  balances: [],
@@ -1627,7 +1854,9 @@ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primi
1627
1854
  var BalancePopover_default = {
1628
1855
  panel: "BalancePopover_panel",
1629
1856
  title: "BalancePopover_title",
1857
+ titleMain: "BalancePopover_titleMain",
1630
1858
  titleLabel: "BalancePopover_titleLabel",
1859
+ sourceLabel: "BalancePopover_sourceLabel",
1631
1860
  iconButton: "BalancePopover_iconButton",
1632
1861
  titleRule: "BalancePopover_titleRule",
1633
1862
  details: "BalancePopover_details",
@@ -1670,6 +1899,7 @@ function BalancePopover(props) {
1670
1899
  selection,
1671
1900
  displayCurrency,
1672
1901
  ageMs,
1902
+ sourceLabel,
1673
1903
  refreshing,
1674
1904
  cooldownSeconds,
1675
1905
  configSlotWarning,
@@ -1707,19 +1937,22 @@ function BalancePopover(props) {
1707
1937
  "aria-label": t("popover.title"),
1708
1938
  children: [
1709
1939
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.title, children: [
1710
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1711
- "a",
1712
- {
1713
- className: BalancePopover_default.titleLabel,
1714
- href: "https://platform.deepseek.com/usage",
1715
- target: "_blank",
1716
- rel: "noopener noreferrer",
1717
- children: [
1718
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.FishLogo, { size: 14 }),
1719
- t("popover.title")
1720
- ]
1721
- }
1722
- ),
1940
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: BalancePopover_default.titleMain, children: [
1941
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1942
+ "a",
1943
+ {
1944
+ className: BalancePopover_default.titleLabel,
1945
+ href: "https://platform.deepseek.com/usage",
1946
+ target: "_blank",
1947
+ rel: "noopener noreferrer",
1948
+ children: [
1949
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.FishLogo, { size: 14 }),
1950
+ t("popover.title")
1951
+ ]
1952
+ }
1953
+ ),
1954
+ sourceLabel === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: BalancePopover_default.sourceLabel, children: sourceLabel })
1955
+ ] }),
1723
1956
  pluginsAction === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label: pluginsLabel, side: "top", delayMs: 500, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1724
1957
  "button",
1725
1958
  {
@@ -1781,7 +2014,9 @@ var PercentRing_default = {
1781
2014
  ring: "PercentRing_ring",
1782
2015
  track: "PercentRing_track",
1783
2016
  fill: "PercentRing_fill",
1784
- cross: "PercentRing_cross"
2017
+ marker: "PercentRing_marker",
2018
+ spin: "PercentRing_spin",
2019
+ "dsb-ring-spin": "PercentRing_dsb-ring-spin"
1785
2020
  };
1786
2021
 
1787
2022
  // src/client/sidebar/PercentRing.tsx
@@ -1792,10 +2027,13 @@ var RING_INSET = 1;
1792
2027
  var RADIUS = VIEW / 2 - RING_INSET - STROKE / 2;
1793
2028
  var CENTER = VIEW / 2;
1794
2029
  var CIRCUMFERENCE = 2 * Math.PI * RADIUS;
1795
- var CROSS_ARM = (2 * RADIUS - STROKE) / 2 / Math.SQRT2;
2030
+ var INNER_RADIUS = RADIUS - STROKE / 2;
2031
+ var MARK_ARM = INNER_RADIUS / 2 / Math.SQRT2;
1796
2032
  function round3(value) {
1797
2033
  return Math.round(value * 1e3) / 1e3;
1798
2034
  }
2035
+ var SPIN_ARC_RATIO = 12 / (2 * Math.PI * 9.5);
2036
+ var SPIN_ARC = round3(CIRCUMFERENCE * SPIN_ARC_RATIO);
1799
2037
  function PercentRing({
1800
2038
  state,
1801
2039
  marker = null,
@@ -1817,37 +2055,55 @@ function PercentRing({
1817
2055
  children: [
1818
2056
  title === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("title", { children: title }),
1819
2057
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { className: PercentRing_default.track, cx: CENTER, cy: CENTER, r: RADIUS }),
1820
- clamped === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2058
+ state === "ongoing" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("g", { className: PercentRing_default.spin, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1821
2059
  "circle",
1822
2060
  {
1823
2061
  className: PercentRing_default.fill,
1824
2062
  cx: CENTER,
1825
2063
  cy: CENTER,
1826
2064
  r: RADIUS,
1827
- strokeDasharray: `${round3(CIRCUMFERENCE * clamped)} ${round3(CIRCUMFERENCE)}`,
2065
+ strokeDasharray: `${SPIN_ARC} ${round3(CIRCUMFERENCE)}`,
1828
2066
  transform: `rotate(-90 ${CENTER} ${CENTER})`
1829
2067
  }
2068
+ ) }) : (
2069
+ /* dash 取弧长、gap 取整周长,于是只画出一条弧;rotate 让接缝落在 12 点而不是 3 点
2070
+ (中心跟着 VIEW 走,16 格时是 rotate(-90 8 8))。
2071
+ 比例为 0 时整条弧不画:dash 长度为 0 配上圆头线帽会在 12 点留下一个点。 */
2072
+ clamped === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2073
+ "circle",
2074
+ {
2075
+ className: PercentRing_default.fill,
2076
+ cx: CENTER,
2077
+ cy: CENTER,
2078
+ r: RADIUS,
2079
+ strokeDasharray: `${round3(CIRCUMFERENCE * clamped)} ${round3(CIRCUMFERENCE)}`,
2080
+ transform: `rotate(-90 ${CENTER} ${CENTER})`
2081
+ }
2082
+ )
1830
2083
  ),
1831
- marker === "cross" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: PercentRing_default.cross, children: [
2084
+ marker === null ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("g", { className: PercentRing_default.marker, children: marker === "cross" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1832
2085
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1833
2086
  "line",
1834
2087
  {
1835
- x1: CENTER - CROSS_ARM,
1836
- y1: CENTER - CROSS_ARM,
1837
- x2: CENTER + CROSS_ARM,
1838
- y2: CENTER + CROSS_ARM
2088
+ x1: CENTER - MARK_ARM,
2089
+ y1: CENTER - MARK_ARM,
2090
+ x2: CENTER + MARK_ARM,
2091
+ y2: CENTER + MARK_ARM
1839
2092
  }
1840
2093
  ),
1841
2094
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1842
2095
  "line",
1843
2096
  {
1844
- x1: CENTER + CROSS_ARM,
1845
- y1: CENTER - CROSS_ARM,
1846
- x2: CENTER - CROSS_ARM,
1847
- y2: CENTER + CROSS_ARM
2097
+ x1: CENTER + MARK_ARM,
2098
+ y1: CENTER - MARK_ARM,
2099
+ x2: CENTER - MARK_ARM,
2100
+ y2: CENTER + MARK_ARM
1848
2101
  }
1849
2102
  )
1850
- ] }) : null
2103
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2104
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: CENTER, y1: CENTER - MARK_ARM, x2: CENTER, y2: CENTER + MARK_ARM }),
2105
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: CENTER - MARK_ARM, y1: CENTER, x2: CENTER + MARK_ARM, y2: CENTER })
2106
+ ] }) })
1851
2107
  ]
1852
2108
  }
1853
2109
  );
@@ -1868,8 +2124,8 @@ var REFRESH_SIMULATION_MS = 600;
1868
2124
  var PANEL_GAP = 8;
1869
2125
  var PANEL_MARGIN = 12;
1870
2126
  var MEASURE_STYLE = { visibility: "hidden", left: 0, top: 0 };
1871
- var MS_PER_SECOND = 1e3;
1872
- var TICK_MS = MS_PER_SECOND;
2127
+ var MS_PER_SECOND2 = 1e3;
2128
+ var TICK_MS = MS_PER_SECOND2;
1873
2129
  function useConfigSlotState(probe) {
1874
2130
  const [state, setState] = (0, import_react5.useState)(() => probe.getSnapshot());
1875
2131
  (0, import_react5.useEffect)(() => {
@@ -1894,19 +2150,22 @@ function usePluginsNavigation(navigation) {
1894
2150
  }, [navigation]);
1895
2151
  return action;
1896
2152
  }
1897
- function stateLabelKey(response, selection) {
1898
- if (response.state === "error")
1899
- return response.error?.code === "NO_KEY" ? "state.noKey" : "state.error";
1900
- if (response.state === "empty")
1901
- return response.error?.code === "NO_KEY" ? "state.noKey" : "state.empty";
1902
- if (response.state === "stale") return "state.stale";
1903
- if (!response.isAvailable) return "state.unavailable";
1904
- if (selection.shown === null) return "state.noBalance";
1905
- return null;
2153
+ function ringSpecFor(presentation) {
2154
+ return { state: presentation.ring, marker: presentation.marker };
1906
2155
  }
1907
- function ringSpecFor(severity) {
1908
- const spec = ringSpecOf(severity);
1909
- return { state: spec.state === "ongoing" ? "idle" : spec.state, marker: spec.marker };
2156
+ function useRouteProvider(hint) {
2157
+ const [provider, setProvider] = (0, import_react5.useState)(() => hint?.getSnapshot());
2158
+ (0, import_react5.useEffect)(() => {
2159
+ if (hint === void 0) {
2160
+ setProvider(void 0);
2161
+ return;
2162
+ }
2163
+ setProvider(hint.getSnapshot());
2164
+ return hint.subscribe(() => {
2165
+ setProvider(hint.getSnapshot());
2166
+ });
2167
+ }, [hint]);
2168
+ return provider;
1910
2169
  }
1911
2170
  function SidebarBalance({
1912
2171
  wide,
@@ -1914,7 +2173,8 @@ function SidebarBalance({
1914
2173
  config,
1915
2174
  configSlotProbe,
1916
2175
  onSelectCurrency,
1917
- pluginsNavigation
2176
+ pluginsNavigation,
2177
+ routeHint
1918
2178
  }) {
1919
2179
  const [view, setView] = (0, import_react5.useState)(() => {
1920
2180
  const response2 = resolveScenario() === null ? pendingView() : currentBalance();
@@ -1927,6 +2187,7 @@ function SidebarBalance({
1927
2187
  const [refreshing, setRefreshing] = (0, import_react5.useState)(false);
1928
2188
  const [cooldownUntil, setCooldownUntil] = (0, import_react5.useState)(0);
1929
2189
  const [now, setNow] = (0, import_react5.useState)(() => Date.now());
2190
+ const provider = useRouteProvider(routeHint);
1930
2191
  const rootRef = (0, import_react5.useRef)(null);
1931
2192
  const panelRef = (0, import_react5.useRef)(null);
1932
2193
  const refreshTimer = (0, import_react5.useRef)(void 0);
@@ -1961,7 +2222,7 @@ function SidebarBalance({
1961
2222
  let cancelled = false;
1962
2223
  const load = async () => {
1963
2224
  try {
1964
- const next = await requestBalance({ currency: preference });
2225
+ const next = await requestBalance({ currency: preference, provider });
1965
2226
  if (cancelled) return;
1966
2227
  loadedRef.current = true;
1967
2228
  acceptBalance(next);
@@ -1975,7 +2236,7 @@ function SidebarBalance({
1975
2236
  () => {
1976
2237
  void load();
1977
2238
  },
1978
- Math.max(5, config.clientPollSeconds) * MS_PER_SECOND
2239
+ Math.max(5, config.clientPollSeconds) * MS_PER_SECOND2
1979
2240
  );
1980
2241
  return () => {
1981
2242
  cancelled = true;
@@ -1984,6 +2245,7 @@ function SidebarBalance({
1984
2245
  }, [
1985
2246
  mock,
1986
2247
  preference,
2248
+ provider,
1987
2249
  config.clientPollSeconds,
1988
2250
  config.configSignature,
1989
2251
  acceptBalance,
@@ -2009,9 +2271,12 @@ function SidebarBalance({
2009
2271
  gap: PANEL_GAP,
2010
2272
  margin: PANEL_MARGIN
2011
2273
  });
2012
- (0, import_react5.useEffect)(() => {
2274
+ (0, import_react5.useLayoutEffect)(() => {
2013
2275
  if (!open) return;
2014
2276
  setNow(Date.now());
2277
+ }, [open]);
2278
+ (0, import_react5.useEffect)(() => {
2279
+ if (!open) return;
2015
2280
  const id = window.setInterval(() => {
2016
2281
  setNow(Date.now());
2017
2282
  }, TICK_MS);
@@ -2019,6 +2284,17 @@ function SidebarBalance({
2019
2284
  window.clearInterval(id);
2020
2285
  };
2021
2286
  }, [open]);
2287
+ (0, import_react5.useEffect)(() => {
2288
+ if (cooldownUntil === 0) return;
2289
+ const remaining = cooldownUntil - Date.now();
2290
+ if (remaining <= 0) return;
2291
+ const id = window.setTimeout(() => {
2292
+ setNow(Date.now());
2293
+ }, remaining);
2294
+ return () => {
2295
+ window.clearTimeout(id);
2296
+ };
2297
+ }, [cooldownUntil]);
2022
2298
  (0, import_react5.useEffect)(() => {
2023
2299
  if (!open) return;
2024
2300
  const onKeyDown = (event) => {
@@ -2034,30 +2310,34 @@ function SidebarBalance({
2034
2310
  if (refreshing) return;
2035
2311
  if (Date.now() < cooldownUntil) return;
2036
2312
  setRefreshing(true);
2037
- const finish = () => {
2313
+ const finish = (rejectedRemainingMs) => {
2314
+ const at = Date.now();
2038
2315
  setRefreshing(false);
2039
2316
  setCooldownUntil(
2040
- Date.now() + Math.max(0, config.manualRefreshCooldownSeconds) * MS_PER_SECOND
2317
+ cooldownUntilOf(at, config.manualRefreshCooldownSeconds, rejectedRemainingMs)
2041
2318
  );
2319
+ setNow(at);
2042
2320
  };
2043
2321
  if (mock) {
2044
2322
  refreshTimer.current = window.setTimeout(() => {
2045
2323
  refreshTimer.current = void 0;
2046
2324
  markFresh();
2047
- finish();
2325
+ finish(null);
2048
2326
  }, REFRESH_SIMULATION_MS);
2049
2327
  return;
2050
2328
  }
2051
2329
  void (async () => {
2330
+ let rejectedRemainingMs = null;
2052
2331
  try {
2053
- await requestRefresh({ reason: "manual" });
2054
- const next = await requestBalance({ currency: preference });
2332
+ const result = await requestRefresh({ reason: "manual", provider });
2333
+ if (!result.triggered) rejectedRemainingMs = Math.max(0, result.cooldownMs);
2334
+ const next = await requestBalance({ currency: preference, provider });
2055
2335
  loadedRef.current = true;
2056
2336
  acceptBalance(next);
2057
2337
  } catch (error) {
2058
2338
  if (!loadedRef.current) showUnreachable(error);
2059
2339
  } finally {
2060
- finish();
2340
+ finish(rejectedRemainingMs);
2061
2341
  }
2062
2342
  })();
2063
2343
  }, [
@@ -2065,6 +2345,7 @@ function SidebarBalance({
2065
2345
  config.manualRefreshCooldownSeconds,
2066
2346
  mock,
2067
2347
  preference,
2348
+ provider,
2068
2349
  refreshing,
2069
2350
  acceptBalance,
2070
2351
  showUnreachable,
@@ -2088,18 +2369,20 @@ function SidebarBalance({
2088
2369
  );
2089
2370
  const configSlotState = useConfigSlotState(configSlotProbe);
2090
2371
  const shown = selection.shown;
2091
- const ring = ringSpecFor(response.severity);
2092
- const ringRatio = ringRatioOf(
2372
+ const situation = situationOfResponse(response);
2373
+ const presentation = presentationOf(situation, response.severity);
2374
+ const ring = ringSpecFor(presentation);
2375
+ const ringRatio = presentation.arc === "gauge" ? ringRatioOf(
2093
2376
  shown === null ? null : shown.total,
2094
2377
  shown === null ? void 0 : config.warnThresholdOf(shown.currency),
2095
2378
  response.severity
2096
- );
2097
- const stateKey = stateLabelKey(response, selection);
2379
+ ) : 0;
2380
+ const stateKey = presentation.textKey;
2098
2381
  const stateText = stateKey === null ? null : t(stateKey);
2099
2382
  let markerLabel = null;
2100
2383
  let markerHint = "";
2101
2384
  if (shown === null) {
2102
- markerLabel = t(stateKey ?? "state.noBalance");
2385
+ markerLabel = t(stateKey ?? "situation.emptyWallet");
2103
2386
  markerHint = markerLabel;
2104
2387
  } else if (!selection.matchesPreference) {
2105
2388
  markerLabel = t("sidebar.aria.mismatch");
@@ -2110,10 +2393,11 @@ function SidebarBalance({
2110
2393
  if (stateText !== null && stateText !== markerLabel) ariaParts.push(stateText);
2111
2394
  if (markerLabel !== null) ariaParts.push(markerLabel);
2112
2395
  const ariaLabel = ariaParts.join(" ");
2113
- const cooldownSeconds = Math.max(0, Math.ceil((cooldownUntil - now) / MS_PER_SECOND));
2396
+ const cooldownSeconds = cooldownSecondsOf(cooldownUntil, now);
2397
+ const sourceKey = shown === null ? null : sourceLabelKeyOf(response.source);
2398
+ const sourceLabel = sourceKey === null ? null : t(sourceKey);
2114
2399
  const ageMs = currentAgeMs(view.seenAt, view.seenAgeMs, now);
2115
- const ringTitle = !wide && stateText !== null ? `${t("sidebar.aria.ring")} ${stateText}` : void 0;
2116
- const hoverLabel = shown === null ? stateText : formatMoney(shown.total, shown.currency);
2400
+ const hoverText = hoverTextOf(shown, stateText);
2117
2401
  const trigger = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2118
2402
  "button",
2119
2403
  {
@@ -2133,13 +2417,13 @@ function SidebarBalance({
2133
2417
  marker: ring.marker,
2134
2418
  ratio: ringRatio,
2135
2419
  size: 18,
2136
- title: ringTitle
2420
+ title: hoverText ?? void 0
2137
2421
  }
2138
2422
  )
2139
2423
  }
2140
2424
  );
2141
2425
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { ref: rootRef, className: SidebarBalance_default.root, "data-mode": wide ? "wide" : "rail", tabIndex: -1, children: [
2142
- wide && hoverLabel !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.Tooltip, { label: hoverLabel, side: "right", delayMs: 500, disabled: open, children: trigger }) : trigger,
2426
+ wide && hoverText !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.Tooltip, { label: hoverText, side: "right", delayMs: 500, disabled: open, children: trigger }) : trigger,
2143
2427
  open ? (0, import_react_dom.createPortal)(
2144
2428
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2145
2429
  BalancePopover,
@@ -2148,6 +2432,7 @@ function SidebarBalance({
2148
2432
  selection,
2149
2433
  displayCurrency: preference,
2150
2434
  ageMs,
2435
+ sourceLabel,
2151
2436
  refreshing,
2152
2437
  cooldownSeconds,
2153
2438
  configSlotWarning: configSlotState === "missing" ? CONFIG_SLOT_WARNING : null,
@@ -2164,6 +2449,66 @@ function SidebarBalance({
2164
2449
  ] });
2165
2450
  }
2166
2451
 
2452
+ // src/client/route-hint.ts
2453
+ function safely(read) {
2454
+ try {
2455
+ return read();
2456
+ } catch {
2457
+ return void 0;
2458
+ }
2459
+ }
2460
+ function projectionOf(ctx) {
2461
+ return safely(() => {
2462
+ const current = ctx.uiSession?.adapter?.current?.getSnapshot?.();
2463
+ const key = current?.key;
2464
+ if (key === void 0) return void 0;
2465
+ return ctx.sessions?.binding?.(key)?.session?.projections?.faceOf?.("modelSelection");
2466
+ });
2467
+ }
2468
+ function createRouteHint(services) {
2469
+ const listeners2 = /* @__PURE__ */ new Set();
2470
+ let stopCurrent;
2471
+ let stopProjection;
2472
+ const notify = () => {
2473
+ for (const listener of [...listeners2]) listener();
2474
+ };
2475
+ const bindProjection = () => {
2476
+ stopProjection?.();
2477
+ stopProjection = safely(() => projectionOf(services())?.subscribe?.(notify));
2478
+ };
2479
+ const start = () => {
2480
+ stopCurrent = safely(
2481
+ () => services().uiSession?.adapter?.current?.subscribe?.(() => {
2482
+ bindProjection();
2483
+ notify();
2484
+ })
2485
+ );
2486
+ bindProjection();
2487
+ };
2488
+ const stop = () => {
2489
+ stopCurrent?.();
2490
+ stopProjection?.();
2491
+ stopCurrent = void 0;
2492
+ stopProjection = void 0;
2493
+ };
2494
+ return {
2495
+ getSnapshot() {
2496
+ const value = safely(
2497
+ () => projectionOf(services())?.getSnapshot?.()?.next?.provider
2498
+ );
2499
+ return typeof value === "string" && value !== "" ? value : void 0;
2500
+ },
2501
+ subscribe(listener) {
2502
+ listeners2.add(listener);
2503
+ if (listeners2.size === 1) start();
2504
+ return () => {
2505
+ listeners2.delete(listener);
2506
+ if (listeners2.size === 0) stop();
2507
+ };
2508
+ }
2509
+ };
2510
+ }
2511
+
2167
2512
  // src/client/index.tsx
2168
2513
  var import_jsx_runtime6 = require("react/jsx-runtime");
2169
2514
  var ENTRY_ID = "dsh-ds-balance";
@@ -2233,6 +2578,7 @@ function SidebarSeatComponent(props) {
2233
2578
  t: translateOf(props.seat.t),
2234
2579
  configSlotProbe: props.configSlotProbe,
2235
2580
  pluginsNavigation: props.pluginsNavigation,
2581
+ routeHint: props.routeHint,
2236
2582
  onSelectCurrency: selectCurrency,
2237
2583
  config: {
2238
2584
  displayCurrency: readDisplayCurrency(value),
@@ -2312,6 +2658,18 @@ function apply(ctx) {
2312
2658
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ds-balance: dictionaries");
2313
2659
  const configSlotProbe = createConfigSlotProbe();
2314
2660
  ctx.effect(() => () => configSlotProbe.dispose(), "ds-balance: config slot probe");
2661
+ const routeServices = {};
2662
+ for (const name of ["uiSession", "sessions"]) {
2663
+ ctx.inject([name], (serviceCtx) => {
2664
+ serviceCtx.effect(() => {
2665
+ routeServices[name] = serviceCtx[name];
2666
+ return () => {
2667
+ delete routeServices[name];
2668
+ };
2669
+ }, `ds-balance: route hint (${name})`);
2670
+ });
2671
+ }
2672
+ const routeHint = createRouteHint(() => routeServices);
2315
2673
  const pluginsNavigation = createPluginsNavigation();
2316
2674
  ctx.inject(["pluginNavigation"], (navCtx) => {
2317
2675
  const face = navCtx["pluginNavigation"];
@@ -2357,14 +2715,15 @@ function apply(ctx) {
2357
2715
  seat,
2358
2716
  form,
2359
2717
  configSlotProbe,
2360
- pluginsNavigation
2718
+ pluginsNavigation,
2719
+ routeHint
2361
2720
  }
2362
2721
  )
2363
2722
  )
2364
2723
  );
2365
2724
  });
2366
2725
  }
2367
- var __dsbCss = "/* src/client/settings/fields.module.css */\n.fields_group {\n padding: 12px 0 0;\n}\n.fields_groupLast {\n padding-bottom: 12px;\n}\n.fields_groupNote {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_fields {\n display: flex;\n flex-direction: column;\n}\n.fields_fields > * + * {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.fields_details {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-top: 12px;\n padding-top: 12px;\n}\n.fields_detailsBody {\n padding-top: 10px;\n}\n.fields_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.fields_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n}\n.fields_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n.fields_rowTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_rowTitle {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_badges {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.fields_reset {\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.fields_reset:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reset:disabled {\n cursor: default;\n}\n.fields_inputStatic,\n.fields_inputStatic:hover {\n pointer-events: none;\n}\n.fields_inputStatic input,\n.fields_inputStatic input:hover,\n.fields_inputStatic input:focus,\n.fields_inputStatic input:focus-visible,\n.fields_inputStatic input:active {\n border-color: var(--dsw-alias-border-l4);\n background: var(--dsw-alias-bg-layer-3);\n outline: none;\n box-shadow: none;\n cursor: default;\n}\n.fields_inputInvalid input {\n border: 1px solid var(--dsw-alias-state-error-primary);\n}\n.fields_inputInvalid input:focus-visible {\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputWithAction {\n position: relative;\n display: flex;\n align-items: center;\n}\n.fields_inputWithAction .fields_input,\n.fields_inputWithAction .fields_inputInvalid {\n padding-right: 38px;\n}\n.fields_reveal {\n position: absolute;\n right: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.fields_reveal:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.fields_revealOn {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reveal:disabled {\n cursor: default;\n opacity: 0.4;\n}\n.fields_selectorAnchor {\n flex: none;\n}\n.fields_selector {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n flex: none;\n max-width: 100%;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n background: var(--dsw-alias-bg-module-platform);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n.fields_selector:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.fields_selector:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_selectorChevron {\n flex: none;\n}\n.fields_hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_pairNote {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n.fields_pairNote::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-radius: 999px;\n corner-shape: round;\n background: currentColor;\n}\n.fields_invalid {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.fields_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_resultOk {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary);\n}\n.fields_resultFail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* src/client/settings/BalanceSettingsCard.module.css */\n.BalanceSettingsCard_form {\n display: flex;\n flex-direction: column;\n}\n.BalanceSettingsCard_readOnly {\n margin: 0 0 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 16px;\n}\n.BalanceSettingsCard_failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.BalanceSettingsCard_notice {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\n isolation: isolate;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: 12px;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n.BalancePopover_panel::before {\n position: absolute;\n z-index: -1;\n inset: 0;\n border-radius: inherit;\n background: var(--dsw-specific-menu);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n content: \"\";\n pointer-events: none;\n}\n.BalancePopover_title {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n.BalancePopover_titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n cursor: pointer;\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.BalancePopover_titleLabel:hover,\n.BalancePopover_titleLabel:visited {\n color: inherit;\n}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\n}\n.BalancePopover_iconButton {\n position: relative;\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n corner-shape: round;\n padding: 0;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_iconButton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.BalancePopover_iconButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.BalancePopover_titleRule {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalancePopover_details {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_details dt,\n.BalancePopover_details dd {\n min-width: 0;\n margin: 0;\n}\n.BalancePopover_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.BalancePopover_notice {\n margin-top: 12px;\n}\n.BalancePopover_noticeText {\n margin: 0 0 8px;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_noticeActions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.BalancePopover_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n}\n.BalancePopover_updated {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_status {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_refresh {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.BalancePopover_refresh:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_refresh:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.BalancePopover_refresh:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalancePopover_refresh[data-cooling] {\n color: var(--dsw-alias-label-caption);\n}\n.BalancePopover_refresh[data-refreshing] svg {\n animation: BalancePopover_balance-refresh-spin 900ms linear infinite;\n}\n@keyframes BalancePopover_balance-refresh-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .BalancePopover_refresh[data-refreshing] svg {\n animation-duration: 2400ms;\n }\n}\n\n/* src/client/sidebar/PercentRing.module.css */\n.PercentRing_ring {\n display: block;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.PercentRing_ring[data-state=done] {\n color: var(--dsw-alias-state-success-primary);\n}\n.PercentRing_ring[data-state=warning] {\n color: var(--dsw-alias-state-warn-primary);\n}\n.PercentRing_ring[data-state=error] {\n color: var(--dsw-alias-state-error-primary);\n}\n.PercentRing_ring[data-state=idle] {\n color: var(--dsw-alias-state-idle-primary);\n}\n.PercentRing_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 1;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 1;\n stroke-linecap: round;\n}\n.PercentRing_cross {\n fill: none;\n stroke: currentColor;\n stroke-width: 1;\n stroke-linecap: round;\n}\n\n/* src/client/sidebar/SidebarBalance.module.css */\n.SidebarBalance_root {\n position: relative;\n display: flex;\n min-width: 0;\n}\n.SidebarBalance_root[data-mode=wide] {\n flex: none;\n width: calc(100% + 4px);\n margin: 4px -2px 0;\n}\n.SidebarBalance_root[data-mode=rail] {\n flex: none;\n margin: 8px 0 0;\n}\n.SidebarBalance_trigger {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n overflow: hidden;\n}\n.SidebarBalance_trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.SidebarBalance_trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.SidebarBalance_root[data-mode=wide] .SidebarBalance_trigger {\n flex: none;\n gap: 8px;\n width: 100%;\n height: 42px;\n padding: 0 10px 0 8px;\n border-radius: 12px;\n}\n.SidebarBalance_root[data-mode=rail] .SidebarBalance_trigger {\n flex: none;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 12px;\n}\n.SidebarBalance_icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.SidebarBalance_label {\n flex: 0 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.SidebarBalance_marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-tertiary);\n position: relative;\n top: 1.63px;\n}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
2726
+ var __dsbCss = "/* src/client/settings/fields.module.css */\n.fields_group {\n padding: 12px 0 0;\n}\n.fields_groupLast {\n padding-bottom: 12px;\n}\n.fields_groupNote {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_fields {\n display: flex;\n flex-direction: column;\n}\n.fields_fields > * + * {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.fields_details {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-top: 12px;\n padding-top: 12px;\n}\n.fields_detailsBody {\n padding-top: 10px;\n}\n.fields_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.fields_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n}\n.fields_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n.fields_rowTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_rowTitle {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_badges {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.fields_reset {\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.fields_reset:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reset:disabled {\n cursor: default;\n}\n.fields_inputStatic,\n.fields_inputStatic:hover {\n pointer-events: none;\n}\n.fields_inputStatic input,\n.fields_inputStatic input:hover,\n.fields_inputStatic input:focus,\n.fields_inputStatic input:focus-visible,\n.fields_inputStatic input:active {\n border-color: var(--dsw-alias-border-l4);\n background: var(--dsw-alias-bg-layer-3);\n outline: none;\n box-shadow: none;\n cursor: default;\n}\n.fields_inputInvalid input {\n border: 1px solid var(--dsw-alias-state-error-primary);\n}\n.fields_inputInvalid input:focus-visible {\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputWithAction {\n position: relative;\n display: flex;\n align-items: center;\n}\n.fields_inputWithAction .fields_input,\n.fields_inputWithAction .fields_inputInvalid {\n padding-right: 38px;\n}\n.fields_reveal {\n position: absolute;\n right: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 50%;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.fields_reveal:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.fields_revealOn {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reveal:disabled {\n cursor: default;\n opacity: 0.4;\n}\n.fields_selectorAnchor {\n flex: none;\n}\n.fields_selector {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n flex: none;\n max-width: 100%;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-module-platform);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n.fields_selector:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.fields_selector:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_selectorChevron {\n flex: none;\n}\n.fields_hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_pairNote {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n.fields_pairNote::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n corner-shape: round;\n background: currentColor;\n}\n.fields_invalid {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.fields_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_resultOk {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary);\n}\n.fields_resultFail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* src/client/settings/BalanceSettingsCard.module.css */\n.BalanceSettingsCard_form {\n display: flex;\n flex-direction: column;\n}\n.BalanceSettingsCard_readOnly {\n margin: 0 0 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 16px;\n}\n.BalanceSettingsCard_failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.BalanceSettingsCard_notice {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\n isolation: isolate;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n.BalancePopover_panel::before {\n position: absolute;\n z-index: -1;\n inset: 0;\n border-radius: inherit;\n background: var(--dsw-specific-menu);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n content: \"\";\n pointer-events: none;\n}\n.BalancePopover_title {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n.BalancePopover_titleMain {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n.BalancePopover_titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n cursor: pointer;\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.BalancePopover_titleLabel:hover,\n.BalancePopover_titleLabel:visited {\n color: inherit;\n}\n.BalancePopover_sourceLabel {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-weight: 400;\n}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\n}\n.BalancePopover_iconButton {\n position: relative;\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n margin-block: -5px;\n border: none;\n border-radius: var(--dsw-radius-sm);\n padding: 0;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_iconButton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.BalancePopover_iconButton:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color);\n outline-offset: -2px;\n}\n.BalancePopover_titleRule {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalancePopover_details {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_details dt,\n.BalancePopover_details dd {\n min-width: 0;\n margin: 0;\n}\n.BalancePopover_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.BalancePopover_notice {\n margin-top: 12px;\n}\n.BalancePopover_noticeText {\n margin: 0 0 8px;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_noticeActions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.BalancePopover_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n}\n.BalancePopover_updated {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_status {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_refresh {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.BalancePopover_refresh:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_refresh:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color);\n outline-offset: -2px;\n}\n.BalancePopover_refresh:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalancePopover_refresh[data-cooling] {\n color: var(--dsw-alias-label-caption);\n}\n.BalancePopover_refresh[data-refreshing] svg {\n animation: BalancePopover_balance-refresh-spin 900ms linear infinite;\n}\n@keyframes BalancePopover_balance-refresh-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .BalancePopover_refresh[data-refreshing] svg {\n animation-duration: 2400ms;\n }\n}\n\n/* src/client/sidebar/PercentRing.module.css */\n.PercentRing_ring {\n display: block;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.PercentRing_ring[data-state=done] {\n color: var(--dsw-alias-state-success-primary);\n}\n.PercentRing_ring[data-state=warning] {\n color: var(--dsw-alias-state-warn-primary);\n}\n.PercentRing_ring[data-state=error] {\n color: var(--dsw-alias-state-error-primary);\n}\n.PercentRing_ring[data-state=idle] {\n color: var(--dsw-alias-state-idle-primary);\n}\n.PercentRing_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 1;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 1;\n stroke-linecap: round;\n}\n.PercentRing_marker {\n fill: none;\n stroke: currentColor;\n stroke-width: 1;\n stroke-linecap: round;\n}\n.PercentRing_spin {\n transform-origin: center;\n animation: PercentRing_dsb-ring-spin 1.5s linear infinite;\n}\n@keyframes PercentRing_dsb-ring-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .PercentRing_spin {\n animation: none;\n }\n}\n\n/* src/client/sidebar/SidebarBalance.module.css */\n.SidebarBalance_root {\n position: relative;\n display: flex;\n min-width: 0;\n}\n.SidebarBalance_root[data-mode=wide] {\n flex: none;\n width: calc(100% + 4px);\n margin: 4px -2px 0;\n}\n.SidebarBalance_root[data-mode=rail] {\n flex: none;\n margin: 8px 0 0;\n}\n.SidebarBalance_trigger {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n overflow: hidden;\n}\n.SidebarBalance_trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.SidebarBalance_trigger:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color);\n outline-offset: -2px;\n}\n.SidebarBalance_root[data-mode=wide] .SidebarBalance_trigger {\n flex: none;\n gap: 8px;\n width: 100%;\n height: 42px;\n padding: 0 10px 0 8px;\n border-radius: var(--dsw-radius-md);\n}\n.SidebarBalance_root[data-mode=rail] .SidebarBalance_trigger {\n flex: none;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: var(--dsw-radius-md);\n}\n.SidebarBalance_icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.SidebarBalance_label {\n flex: 0 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.SidebarBalance_marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-tertiary);\n position: relative;\n top: 1.63px;\n}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
2368
2727
  var __dsbTagId = "dsh-ds-balance/client.css";
2369
2728
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + __dsbTagId + '"]') === null) {
2370
2729
  var __dsbTag = document.createElement('style');