dsh-ds-balance 1.0.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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/README_en.md +81 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/adapters/console-logger.d.ts +25 -0
  6. package/lib/adapters/console-logger.js +35 -0
  7. package/lib/adapters/domain-core-store.d.ts +104 -0
  8. package/lib/adapters/domain-core-store.js +184 -0
  9. package/lib/adapters/http-deepseek-client.d.ts +36 -0
  10. package/lib/adapters/http-deepseek-client.js +101 -0
  11. package/lib/adapters/memory-metrics.d.ts +30 -0
  12. package/lib/adapters/memory-metrics.js +67 -0
  13. package/lib/adapters/salt-file.d.ts +32 -0
  14. package/lib/adapters/salt-file.js +40 -0
  15. package/lib/client.js +2120 -0
  16. package/lib/config.d.ts +127 -0
  17. package/lib/config.js +112 -0
  18. package/lib/domain/balance.d.ts +70 -0
  19. package/lib/domain/balance.js +8 -0
  20. package/lib/domain/errors.d.ts +84 -0
  21. package/lib/domain/errors.js +136 -0
  22. package/lib/domain/money.d.ts +39 -0
  23. package/lib/domain/money.js +74 -0
  24. package/lib/domain/normalize.d.ts +38 -0
  25. package/lib/domain/normalize.js +120 -0
  26. package/lib/domain/select.d.ts +26 -0
  27. package/lib/domain/select.js +51 -0
  28. package/lib/domain/severity.d.ts +34 -0
  29. package/lib/domain/severity.js +44 -0
  30. package/lib/http/handlers.d.ts +47 -0
  31. package/lib/http/handlers.js +261 -0
  32. package/lib/http/routes.d.ts +63 -0
  33. package/lib/http/routes.js +61 -0
  34. package/lib/http/wire.d.ts +82 -0
  35. package/lib/http/wire.js +68 -0
  36. package/lib/index.d.ts +36 -0
  37. package/lib/index.js +210 -0
  38. package/lib/ports/clock.d.ts +12 -0
  39. package/lib/ports/clock.js +6 -0
  40. package/lib/ports/core-store.d.ts +28 -0
  41. package/lib/ports/core-store.js +6 -0
  42. package/lib/ports/credentials.d.ts +30 -0
  43. package/lib/ports/credentials.js +9 -0
  44. package/lib/ports/deepseek-client.d.ts +47 -0
  45. package/lib/ports/deepseek-client.js +7 -0
  46. package/lib/ports/logger.d.ts +12 -0
  47. package/lib/ports/logger.js +6 -0
  48. package/lib/ports/metrics.d.ts +36 -0
  49. package/lib/ports/metrics.js +11 -0
  50. package/lib/services/account-tag.d.ts +24 -0
  51. package/lib/services/account-tag.js +29 -0
  52. package/lib/services/balance-service.d.ts +121 -0
  53. package/lib/services/balance-service.js +220 -0
  54. package/lib/services/config-service.d.ts +52 -0
  55. package/lib/services/config-service.js +51 -0
  56. package/lib/services/key-resolver.d.ts +42 -0
  57. package/lib/services/key-resolver.js +62 -0
  58. package/lib/services/scheduler.d.ts +93 -0
  59. package/lib/services/scheduler.js +142 -0
  60. package/lib/types/client/api-types.d.ts +54 -0
  61. package/lib/types/client/data.d.ts +103 -0
  62. package/lib/types/client/index.d.ts +18 -0
  63. package/lib/types/client/locales.d.ts +98 -0
  64. package/lib/types/client/mock/index.d.ts +36 -0
  65. package/lib/types/client/mock/scenarios.d.ts +51 -0
  66. package/lib/types/client/model.d.ts +93 -0
  67. package/lib/types/client/settings/BalanceSettingsCard.d.ts +22 -0
  68. package/lib/types/client/settings/fields.d.ts +191 -0
  69. package/lib/types/client/settings/use-config-form.d.ts +212 -0
  70. package/lib/types/client/settings/use-credential-state.d.ts +36 -0
  71. package/lib/types/client/sidebar/BalancePopover.d.ts +46 -0
  72. package/lib/types/client/sidebar/PercentRing.d.ts +36 -0
  73. package/lib/types/client/sidebar/SidebarBalance.d.ts +44 -0
  74. package/lib/version.d.ts +12 -0
  75. package/lib/version.js +12 -0
  76. package/package.json +112 -0
package/lib/client.js ADDED
@@ -0,0 +1,2120 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-ds-balance",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ "use strict";
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/client/index.tsx
26
+ var client_exports = {};
27
+ __export(client_exports, {
28
+ SETTINGS_NAMESPACE: () => SETTINGS_NAMESPACE,
29
+ apply: () => apply,
30
+ inject: () => inject
31
+ });
32
+ module.exports = __toCommonJS(client_exports);
33
+ var import_react6 = require("react");
34
+
35
+ // src/client/settings/BalanceSettingsCard.tsx
36
+ var import_react4 = require("react");
37
+
38
+ // node_modules/clsx/dist/clsx.mjs
39
+ function r(e) {
40
+ var t, f, n = "";
41
+ if ("string" == typeof e || "number" == typeof e) n += e;
42
+ else if ("object" == typeof e) if (Array.isArray(e)) {
43
+ var o = e.length;
44
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
45
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
46
+ return n;
47
+ }
48
+ function clsx() {
49
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
50
+ return n;
51
+ }
52
+ var clsx_default = clsx;
53
+
54
+ // src/client/settings/BalanceSettingsCard.tsx
55
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
56
+
57
+ // src/client/locales.ts
58
+ var NS = "ds-balance";
59
+ var zh = {
60
+ "sidebar.label": "DeepSeek \u4F59\u989D",
61
+ "sidebar.aria.balance": "DeepSeek \u4F59\u989D",
62
+ "sidebar.aria.ring": "DeepSeek \u4F59\u989D\u72B6\u6001",
63
+ "sidebar.aria.refresh": "\u5237\u65B0\u4F59\u989D",
64
+ "sidebar.aria.mismatch": "\u663E\u793A\u7684\u5E01\u79CD\u4E0E\u4F60\u9009\u62E9\u7684\u4E0D\u4E00\u81F4",
65
+ "popover.title": "\u8D26\u6237\u4F59\u989D",
66
+ "popover.total": "\u4F59\u989D",
67
+ "popover.granted": "\u8D60\u9001",
68
+ "popover.toppedUp": "\u5145\u503C",
69
+ "popover.updated": "{value}\u524D",
70
+ "popover.updated.justNow": "\u521A\u521A\u66F4\u65B0",
71
+ "popover.refreshing": "\u5237\u65B0\u4E2D",
72
+ "popover.cooldown": "{value} \u79D2\u540E\u53EF\u518D\u6B21\u5237\u65B0",
73
+ "popover.mismatch": "\u672A\u627E\u5230 {wanted} \u4F59\u989D\uFF0C\u5F53\u524D\u663E\u793A {shown}",
74
+ "popover.action.useShown": "\u6539\u7528 {shown}",
75
+ "popover.action.openSettings": "\u53BB\u8BBE\u7F6E",
76
+ "state.unavailable": "\u8D26\u6237\u4E0D\u53EF\u7528",
77
+ "state.stale": "\u6570\u636E\u5DF2\u8FC7\u671F",
78
+ "state.error": "\u8BFB\u53D6\u5931\u8D25",
79
+ "state.empty": "\u5C1A\u672A\u914D\u7F6E",
80
+ "state.noKey": "\u5C1A\u672A\u586B\u5199 API Key",
81
+ "state.noBalance": "\u6682\u65E0\u4F59\u989D",
82
+ "settings.title": "DeepSeek \u4F59\u989D",
83
+ "settings.description": "\u914D\u7F6E\u8D26\u6237\u8FDE\u63A5\u3001\u5237\u65B0\u8282\u594F\u4E0E\u9884\u8B66\u9608\u503C\u3002",
84
+ "settings.group.connection": "\u8FDE\u63A5",
85
+ "settings.group.refresh": "\u5237\u65B0",
86
+ "settings.group.display": "\u5C55\u793A",
87
+ "settings.group.thresholds": "\u9608\u503C",
88
+ "settings.field.apiKey": "API \u5BC6\u94A5",
89
+ "settings.field.apiKeyRef": "API \u5BC6\u94A5\u5F15\u7528\u540D",
90
+ "settings.field.baseUrl": "API \u5730\u5740",
91
+ "settings.field.serverRefreshSeconds": "\u670D\u52A1\u7AEF\u5237\u65B0\u9891\u7387\uFF08\u79D2\uFF09",
92
+ "settings.field.clientPollSeconds": "\u5BA2\u6237\u7AEF\u8F6E\u8BE2\u9891\u7387\uFF08\u79D2\uFF09",
93
+ "settings.field.manualRefreshCooldownSeconds": "\u624B\u52A8\u5237\u65B0\u51B7\u5374\uFF08\u79D2\uFF09",
94
+ "settings.field.displayCurrency": "\u663E\u793A\u5E01\u79CD",
95
+ "settings.field.cnyWarn": "CNY \u9884\u8B66",
96
+ "settings.field.cnyCritical": "CNY \u544A\u6025",
97
+ "settings.field.usdWarn": "USD \u9884\u8B66",
98
+ "settings.field.usdCritical": "USD \u544A\u6025",
99
+ "settings.hint.apiKey": "\u7559\u7A7A\u5219\u6539\u7528\u5F15\u7528\u540D\u4ECE\u51ED\u636E\u5B58\u50A8\u8BFB\u53D6\u3002",
100
+ "settings.hint.apiKeyRef": "\u51ED\u636E\u5B58\u50A8\u91CC\u7684\u6761\u76EE\u540D\uFF1B\u975E\u7A7A\u65F6\u4F18\u5148\u4E8E API Key\u3002",
101
+ "settings.hint.baseUrl": "\u5B98\u65B9\u7AEF\u70B9\uFF1B\u4F7F\u7528\u4EE3\u7406\u6216\u955C\u50CF\u65F6\u4FEE\u6539\u3002",
102
+ "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",
103
+ "settings.hint.number": "\u8BF7\u586B\u6570\u5B57\uFF1B\u7559\u7A7A\u8868\u793A\u4F7F\u7528\u9ED8\u8BA4\u503C\u3002",
104
+ "settings.hint.threshold": "\u540E\u7AEF\u5224\u5B9A\uFF1B\u754C\u9762\u989C\u8272\u8DDF\u968F\u8FD4\u56DE\u7684\u4E25\u91CD\u5EA6\u3002",
105
+ "settings.hint.thresholdPair": "\u544A\u6025\u503C\u5FC5\u987B\u4F4E\u4E8E\u9884\u8B66\u503C",
106
+ "settings.hint.serverRefreshSeconds": "\u8BBF\u95EE\u5B98\u65B9\u63A5\u53E3\u7684\u95F4\u9694\u3002",
107
+ "settings.hint.clientPollSeconds": "\u5237\u65B0\u663E\u793A\u7684\u95F4\u9694\uFF1B\u53EA\u8BFB\u672C\u5730\u7F13\u5B58\uFF0C\u4E0D\u8BBF\u95EE\u4E0A\u6E38\u3002",
108
+ "settings.hint.manualRefreshCooldownSeconds": "\u4E24\u6B21\u624B\u52A8\u5237\u65B0\u4E4B\u95F4\u7684\u6700\u77ED\u95F4\u9694\u3002",
109
+ "settings.currency.auto": "\u81EA\u52A8\uFF08\u8DDF\u968F\u8D26\u6237\uFF09",
110
+ // 凭据只读时的**状态徽章**文案。此前这里放的是输入框占位符,逐字抄官方 settings.models
111
+ // 的 keyEnvLocked;现在输入框留空、状态走标签行右侧的徽章,措辞随之改短。
112
+ "settings.credential.envLocked": "\u5DF2\u7531\u542F\u52A8\u73AF\u5883\u63D0\u4F9B",
113
+ // 只读凭据行下方的说明,按状态四选一。其中「已配置」那条逐字用官方 webSearchApiKeyHint。
114
+ "settings.hint.credential.env": "\u542F\u52A8\u73AF\u5883\u63D0\u4F9B\uFF0C\u53EA\u8BFB\u3002",
115
+ "settings.hint.credential.configured": "\u4E0D\u5199\u5165\u8BBE\u7F6E\u6587\u4EF6\u3002\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u5F53\u524D\u5BC6\u94A5\u3002",
116
+ "settings.hint.credential.notConfigured": "\u5C1A\u672A\u914D\u7F6E\u3002",
117
+ "settings.hint.credential.overridden": "\u4F7F\u7528\u81EA\u5B9A\u4E49\u8BBE\u7F6E\u91CC\u7684\u503C\uFF0C\u5FFD\u7565\u7EE7\u627F\u3002",
118
+ "settings.group.customized": "\u81EA\u5B9A\u4E49\u8BBE\u7F6E",
119
+ "settings.test": "\u6D4B\u8BD5\u8FDE\u63A5",
120
+ "settings.testing": "\u6D4B\u8BD5\u4E2D\u2026",
121
+ "settings.test.ok": "\u8FDE\u63A5\u6B63\u5E38\u3002",
122
+ "settings.test.fail": "\u8FDE\u63A5\u5931\u8D25\uFF1A{message}",
123
+ "settings.save": "\u4FDD\u5B58",
124
+ "settings.saving": "\u4FDD\u5B58\u4E2D\u2026",
125
+ "settings.discard": "\u653E\u5F03\u4FEE\u6539",
126
+ "settings.failed": "\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002",
127
+ "settings.readOnly": "\u5F53\u524D\u914D\u7F6E\u53EA\u8BFB\uFF0C\u65E0\u6CD5\u4FDD\u5B58\u4FEE\u6539\u3002",
128
+ "settings.unsaved": "\u672A\u4FDD\u5B58",
129
+ "settings.invalid": "\u8BF7\u68C0\u67E5\u6807\u7EA2\u7684\u5B57\u6BB5\u3002",
130
+ "settings.configured": "\u5DF2\u914D\u7F6E",
131
+ "settings.notConfigured": "\u672A\u914D\u7F6E",
132
+ "settings.overridden": "\u5DF2\u8986\u76D6",
133
+ "settings.reset": "\u91CD\u7F6E",
134
+ "settings.invalidNumber": "\u8BF7\u586B\u6570\u5B57\uFF1B\u7559\u7A7A\u8868\u793A\u4F7F\u7528\u9ED8\u8BA4\u503C\u3002",
135
+ "settings.hint.refreshAdvanced": "\u4E0D\u5E38\u6539\uFF1A\u9ED8\u8BA4\u503C\u9002\u7528\u4E8E\u5927\u591A\u6570\u60C5\u51B5\u3002",
136
+ "settings.currencyMismatch": "\u5F53\u524D\u8D26\u6237\u65E0 {wanted} \u4F59\u989D\uFF0C\u5B9E\u9645\u663E\u793A {shown}",
137
+ "dev.title": "\u5F00\u53D1\u573A\u666F",
138
+ "dev.hint": "\u4EC5\u7528\u4E8E UI \u5F00\u53D1\uFF1B\u4E0D\u5F71\u54CD\u771F\u5B9E\u914D\u7F6E\u3002"
139
+ };
140
+ var en = {
141
+ "sidebar.label": "DeepSeek balance",
142
+ "sidebar.aria.balance": "DeepSeek balance",
143
+ "sidebar.aria.ring": "DeepSeek balance status",
144
+ "sidebar.aria.refresh": "Refresh balance",
145
+ "sidebar.aria.mismatch": "Shown currency differs from your selection",
146
+ "popover.title": "Account balance",
147
+ "popover.total": "Balance",
148
+ "popover.granted": "Granted",
149
+ "popover.toppedUp": "Topped up",
150
+ "popover.updated": "{value} ago",
151
+ "popover.updated.justNow": "Updated just now",
152
+ "popover.refreshing": "Refreshing",
153
+ "popover.cooldown": "Refresh available in {value}s",
154
+ "popover.mismatch": "No {wanted} balance found; showing {shown}",
155
+ "popover.action.useShown": "Use {shown}",
156
+ "popover.action.openSettings": "Open settings",
157
+ "state.unavailable": "Account unavailable",
158
+ "state.stale": "Data is stale",
159
+ "state.error": "Failed to load",
160
+ "state.empty": "Not configured",
161
+ "state.noKey": "API key missing",
162
+ "state.noBalance": "No balance",
163
+ "settings.title": "DeepSeek balance",
164
+ "settings.description": "Connection, refresh cadence, and warning thresholds.",
165
+ "settings.group.connection": "Connection",
166
+ "settings.group.refresh": "Refresh",
167
+ "settings.group.display": "Display",
168
+ "settings.group.thresholds": "Thresholds",
169
+ "settings.field.apiKey": "API key",
170
+ "settings.field.apiKeyRef": "API key reference",
171
+ "settings.field.baseUrl": "Base URL",
172
+ "settings.field.serverRefreshSeconds": "Server refresh (seconds)",
173
+ "settings.field.clientPollSeconds": "Client poll (seconds)",
174
+ "settings.field.manualRefreshCooldownSeconds": "Manual refresh cooldown (seconds)",
175
+ "settings.field.displayCurrency": "Display currency",
176
+ "settings.field.cnyWarn": "CNY warning",
177
+ "settings.field.cnyCritical": "CNY critical",
178
+ "settings.field.usdWarn": "USD warning",
179
+ "settings.field.usdCritical": "USD critical",
180
+ "settings.hint.apiKey": "Leave empty to resolve from the credential reference instead.",
181
+ "settings.hint.apiKeyRef": "Credential store entry name; takes precedence over the API key.",
182
+ "settings.hint.baseUrl": "Official endpoint; change it for a proxy or mirror.",
183
+ "settings.hint.displayCurrency": "Falls back to a currency the account actually holds.",
184
+ "settings.hint.number": "Enter a number; leave empty to use the default.",
185
+ "settings.hint.threshold": "Decided on the server; the UI colour follows the returned severity.",
186
+ "settings.hint.thresholdPair": "Critical must be lower than warning",
187
+ "settings.hint.serverRefreshSeconds": "How often the official API is called.",
188
+ "settings.hint.clientPollSeconds": "How often the display refreshes; local cache only.",
189
+ "settings.hint.manualRefreshCooldownSeconds": "Minimum gap between two manual refreshes.",
190
+ "settings.currency.auto": "Automatic (follow account)",
191
+ "settings.credential.envLocked": "Provided by launch environment",
192
+ "settings.hint.credential.env": "Provided by the launch environment; read-only.",
193
+ "settings.hint.credential.configured": "Never written to the settings file. Leave empty to keep the current key.",
194
+ "settings.hint.credential.notConfigured": "Not configured yet.",
195
+ "settings.hint.credential.overridden": "Uses the value in Customised settings; the inherited one is ignored.",
196
+ "settings.group.customized": "Customized settings",
197
+ "settings.test": "Test connection",
198
+ "settings.testing": "Testing\u2026",
199
+ "settings.test.ok": "Connection looks good.",
200
+ "settings.test.fail": "Connection failed: {message}",
201
+ "settings.save": "Save",
202
+ "settings.saving": "Saving\u2026",
203
+ "settings.discard": "Discard",
204
+ "settings.failed": "Saving failed. Try again.",
205
+ "settings.readOnly": "This configuration is read-only.",
206
+ "settings.unsaved": "Unsaved",
207
+ "settings.invalid": "Check the highlighted fields.",
208
+ "settings.configured": "Configured",
209
+ "settings.notConfigured": "Not configured",
210
+ "settings.overridden": "Overridden",
211
+ "settings.reset": "Reset",
212
+ "settings.invalidNumber": "Enter a number; leave empty to use the default.",
213
+ "settings.hint.refreshAdvanced": "Rarely changed: the defaults suit most setups.",
214
+ "settings.currencyMismatch": "The account has no {wanted} balance; showing {shown}",
215
+ "dev.title": "Dev scenario",
216
+ "dev.hint": "UI development only; does not affect real configuration."
217
+ };
218
+ function interpolate(template, params) {
219
+ return template.replace(/\{(\w+)\}/g, (whole, name) => Object.prototype.hasOwnProperty.call(params, name) ? params[name] : whole);
220
+ }
221
+
222
+ // src/client/settings/fields.tsx
223
+ var import_react = require("react");
224
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
225
+
226
+ // src/client/settings/fields.module.css
227
+ var fields_default = {
228
+ group: "fields_group",
229
+ groupLast: "fields_groupLast",
230
+ groupNote: "fields_groupNote",
231
+ fields: "fields_fields",
232
+ details: "fields_details",
233
+ detailsSummary: "fields_detailsSummary",
234
+ detailsBody: "fields_detailsBody",
235
+ field: "fields_field",
236
+ row: "fields_row",
237
+ rowText: "fields_rowText",
238
+ rowTitleLine: "fields_rowTitleLine",
239
+ rowTitle: "fields_rowTitle",
240
+ head: "fields_head",
241
+ label: "fields_label",
242
+ badges: "fields_badges",
243
+ reset: "fields_reset",
244
+ input: "fields_input",
245
+ inputInvalid: "fields_inputInvalid",
246
+ inputWithAction: "fields_inputWithAction",
247
+ reveal: "fields_reveal",
248
+ revealOn: "fields_revealOn",
249
+ selectorAnchor: "fields_selectorAnchor",
250
+ selector: "fields_selector",
251
+ selectorChevron: "fields_selectorChevron",
252
+ hint: "fields_hint",
253
+ pairNote: "fields_pairNote",
254
+ invalid: "fields_invalid",
255
+ actions: "fields_actions",
256
+ actionButton: "fields_actionButton",
257
+ resultOk: "fields_resultOk",
258
+ resultFail: "fields_resultFail"
259
+ };
260
+
261
+ // src/client/settings/fields.tsx
262
+ var import_jsx_runtime = require("react/jsx-runtime");
263
+ function FieldGroup(props) {
264
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
265
+ import_dsh_client_ui_primitives.DisclosureRow,
266
+ {
267
+ className: clsx_default(fields_default.group, props.last === true && fields_default.groupLast),
268
+ icon: props.icon,
269
+ title: props.title,
270
+ open: props.open,
271
+ expandable: true,
272
+ expandOnRowClick: true,
273
+ onToggle: props.onToggle,
274
+ children: [
275
+ props.note === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: fields_default.groupNote, children: props.note }),
276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fields_default.fields, children: props.children })
277
+ ]
278
+ }
279
+ );
280
+ }
281
+ function FieldBadges(props) {
282
+ const status = props.status ?? null;
283
+ if (status === null && !props.pending && !props.resettable) return null;
284
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: fields_default.badges, children: [
285
+ status === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: status.tone, children: status.label }),
286
+ props.pending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "neutral", children: props.pendingLabel }) : null,
287
+ props.resettable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
288
+ "button",
289
+ {
290
+ type: "button",
291
+ className: fields_default.reset,
292
+ disabled: props.disabled,
293
+ onClick: props.onReset,
294
+ children: props.resetLabel
295
+ }
296
+ ) : null
297
+ ] });
298
+ }
299
+ function FieldFrame(props) {
300
+ const note = props.invalid ? props.invalidNote ?? props.hint : props.hint;
301
+ const hasNote = note !== void 0 && note !== null && note !== "";
302
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: fields_default.field, children: [
303
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: fields_default.head, children: [
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: fields_default.label, htmlFor: props.id, children: props.label }),
305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
306
+ FieldBadges,
307
+ {
308
+ status: props.status ?? null,
309
+ pending: props.pending,
310
+ resettable: props.resettable,
311
+ pendingLabel: props.pendingLabel,
312
+ resetLabel: props.resetLabel,
313
+ disabled: props.disabled,
314
+ onReset: props.onReset
315
+ }
316
+ )
317
+ ] }),
318
+ props.children,
319
+ hasNote ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: props.invalid ? fields_default.invalid : fields_default.hint, children: note }) : null
320
+ ] });
321
+ }
322
+ function TextControl(props) {
323
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
324
+ "input",
325
+ {
326
+ id: props.id,
327
+ className: props.invalid ? fields_default.inputInvalid : fields_default.input,
328
+ type: "text",
329
+ ...props.numeric ? { inputMode: "numeric" } : {},
330
+ ...props.invalid ? { "aria-invalid": true } : {},
331
+ value: props.text,
332
+ disabled: props.disabled,
333
+ onBlur: props.onBlur,
334
+ onChange: (event) => {
335
+ props.onEdit(event.target.value);
336
+ }
337
+ }
338
+ );
339
+ }
340
+ function ReadOnlyControl(props) {
341
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
342
+ "input",
343
+ {
344
+ id: props.id,
345
+ className: fields_default.input,
346
+ type: "text",
347
+ value: "",
348
+ readOnly: true,
349
+ disabled: true
350
+ }
351
+ );
352
+ }
353
+ function DetailsGroup(props) {
354
+ const [open, setOpen] = (0, import_react.useState)(props.defaultOpen === true);
355
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
356
+ "details",
357
+ {
358
+ className: fields_default.details,
359
+ open,
360
+ onToggle: (event) => {
361
+ setOpen(event.currentTarget.open);
362
+ },
363
+ children: [
364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { className: fields_default.detailsSummary, children: props.title }),
365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fields_default.detailsBody, children: props.children })
366
+ ]
367
+ }
368
+ );
369
+ }
370
+ function SecretControl(props) {
371
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: fields_default.inputWithAction, children: [
372
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
373
+ "input",
374
+ {
375
+ id: props.id,
376
+ className: props.invalid ? fields_default.inputInvalid : fields_default.input,
377
+ type: props.revealed ? "text" : "password",
378
+ autoComplete: "off",
379
+ spellCheck: false,
380
+ ...props.invalid ? { "aria-invalid": true } : {},
381
+ value: props.text,
382
+ disabled: props.disabled,
383
+ onChange: (event) => {
384
+ props.onEdit(event.target.value);
385
+ }
386
+ }
387
+ ),
388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
389
+ "button",
390
+ {
391
+ type: "button",
392
+ className: clsx_default(fields_default.reveal, props.revealed && fields_default.revealOn),
393
+ "aria-label": props.revealLabel,
394
+ "aria-pressed": props.revealed,
395
+ title: props.revealLabel,
396
+ disabled: props.disabled,
397
+ onClick: props.onToggleReveal,
398
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconInspectOutline12, { size: 14 })
399
+ }
400
+ )
401
+ ] });
402
+ }
403
+ function SelectorControl(props) {
404
+ const [open, setOpen] = (0, import_react.useState)(false);
405
+ const selected = props.options.find((option) => option.id === props.selectedId);
406
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
407
+ import_dsh_client_ui_primitives.Menu,
408
+ {
409
+ open,
410
+ onClose: () => {
411
+ setOpen(false);
412
+ },
413
+ items: props.options.map((option) => ({ id: option.id, label: option.label })),
414
+ selectedId: props.selectedId,
415
+ onSelect: (id) => {
416
+ props.onSelect(id);
417
+ setOpen(false);
418
+ },
419
+ align: "end",
420
+ portal: true,
421
+ className: fields_default.selectorAnchor,
422
+ anchor: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
423
+ "button",
424
+ {
425
+ id: props.id,
426
+ type: "button",
427
+ className: fields_default.selector,
428
+ "aria-label": props.label,
429
+ "aria-haspopup": "menu",
430
+ "aria-expanded": open,
431
+ disabled: props.disabled,
432
+ onClick: () => {
433
+ setOpen((value) => !value);
434
+ },
435
+ children: [
436
+ selected?.label ?? "",
437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconChevronDownOutline14, { className: fields_default.selectorChevron })
438
+ ]
439
+ }
440
+ )
441
+ }
442
+ );
443
+ }
444
+ function ActionRow(props) {
445
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fields_default.actions, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
447
+ "button",
448
+ {
449
+ type: "button",
450
+ className: fields_default.actionButton,
451
+ disabled: props.disabled,
452
+ onClick: props.onClick,
453
+ children: props.label
454
+ }
455
+ ) }),
456
+ props.result === null ? null : (
457
+ // 成功只是通报,用 polite 的 status;失败需要用户处理,按 06 §6.7 的「行内错误」用 alert。
458
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
459
+ "p",
460
+ {
461
+ className: props.result.ok ? fields_default.resultOk : fields_default.resultFail,
462
+ role: props.result.ok ? "status" : "alert",
463
+ children: props.result.text
464
+ }
465
+ )
466
+ )
467
+ ] });
468
+ }
469
+
470
+ // src/client/settings/use-config-form.ts
471
+ var import_react2 = require("react");
472
+ function isTextSpec(spec) {
473
+ return spec.kind === "text" || spec.kind === "secret" || spec.kind === "number";
474
+ }
475
+ function textField(field, kind = "text") {
476
+ return {
477
+ field,
478
+ kind,
479
+ format: (value) => typeof value === "string" ? value : "",
480
+ parse: (text) => {
481
+ const trimmed = text.trim();
482
+ return trimmed === "" ? { kind: "clear" } : { kind: "set", value: trimmed };
483
+ }
484
+ };
485
+ }
486
+ function numberField(field) {
487
+ return {
488
+ field,
489
+ kind: "number",
490
+ format: (value) => typeof value === "number" && Number.isFinite(value) ? String(value) : "",
491
+ parse: (text) => {
492
+ const trimmed = text.trim();
493
+ if (trimmed === "") return { kind: "clear" };
494
+ const parsed = Number(trimmed);
495
+ return Number.isFinite(parsed) ? { kind: "set", value: parsed } : void 0;
496
+ }
497
+ };
498
+ }
499
+ function selectField(field) {
500
+ return { field, kind: "select" };
501
+ }
502
+ var AUTO_CURRENCY = "auto";
503
+ var KNOWN_CURRENCIES = ["CNY", "USD"];
504
+ var THRESHOLD_PAIRS = [
505
+ { currency: "CNY", warn: "cnyWarn", critical: "cnyCritical", defaultWarn: 10, defaultCritical: 5 },
506
+ { currency: "USD", warn: "usdWarn", critical: "usdCritical", defaultWarn: 2, defaultCritical: 1 }
507
+ ];
508
+ function pairNumber(state, fallback) {
509
+ const text = state.text.trim();
510
+ if (text === "") return fallback;
511
+ const parsed = Number(text);
512
+ return Number.isFinite(parsed) ? parsed : null;
513
+ }
514
+ function thresholdsOk(pair, warn, critical) {
515
+ const w = pairNumber(warn, pair.defaultWarn);
516
+ const c = pairNumber(critical, pair.defaultCritical);
517
+ if (w === null || c === null) return true;
518
+ return c < w;
519
+ }
520
+ function currencyCodes(...currents) {
521
+ const codes = new Set(KNOWN_CURRENCIES);
522
+ for (const current of currents) {
523
+ if (typeof current !== "string") continue;
524
+ const trimmed = current.trim().toUpperCase();
525
+ if (trimmed !== "" && trimmed !== AUTO_CURRENCY.toUpperCase()) codes.add(trimmed);
526
+ }
527
+ return [...codes];
528
+ }
529
+ var CONFIG_FIELDS = [
530
+ textField("apiKey", "secret"),
531
+ textField("apiKeyRef"),
532
+ textField("baseUrl"),
533
+ numberField("serverRefreshSeconds"),
534
+ numberField("clientPollSeconds"),
535
+ numberField("manualRefreshCooldownSeconds"),
536
+ selectField("displayCurrency"),
537
+ numberField("cnyWarn"),
538
+ numberField("cnyCritical"),
539
+ numberField("usdWarn"),
540
+ numberField("usdCritical")
541
+ ];
542
+ var SPEC_BY_FIELD = new Map(
543
+ CONFIG_FIELDS.map((spec) => [spec.field, spec])
544
+ );
545
+ var TEST_LATENCY_MS = 800;
546
+ function probeFailure(baseUrl, apiKey, apiKeyRef) {
547
+ const url = baseUrl.trim();
548
+ if (!/^https?:\/\//i.test(url)) return "base URL must start with http:// or https://";
549
+ if (apiKey.trim() === "" && apiKeyRef.trim() === "") return "no API key or credential reference configured";
550
+ return null;
551
+ }
552
+ function asRecord(input) {
553
+ return typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
554
+ }
555
+ function shallowEqual(a, b) {
556
+ const keys = Object.keys(a);
557
+ if (keys.length !== Object.keys(b).length) return false;
558
+ for (const key of keys) {
559
+ if (!Object.hasOwn(b, key) || a[key] !== b[key]) return false;
560
+ }
561
+ return true;
562
+ }
563
+ async function settle(outcome) {
564
+ if (outcome === null || typeof outcome !== "object") return;
565
+ if (typeof outcome.then !== "function") return;
566
+ await outcome;
567
+ }
568
+ function landedWrite(snapshot, field, write) {
569
+ if (write.kind === "clear") return !Object.hasOwn(snapshot.user, field);
570
+ return Object.hasOwn(snapshot.user, field) && snapshot.user[field] === write.value;
571
+ }
572
+ function planWrites(snapshot, staged) {
573
+ const plan = [];
574
+ for (const [field, edit] of staged) {
575
+ const spec = SPEC_BY_FIELD.get(field);
576
+ if (spec === void 0) continue;
577
+ const effective = snapshot.value[field];
578
+ const overridden = Object.hasOwn(snapshot.user, field);
579
+ if (!isTextSpec(spec)) {
580
+ if (edit.kind === "clear") {
581
+ if (overridden) plan.push({ field, write: { kind: "clear" } });
582
+ continue;
583
+ }
584
+ if (edit.kind === "value" && edit.value !== effective) {
585
+ plan.push({ field, write: { kind: "set", value: edit.value } });
586
+ }
587
+ continue;
588
+ }
589
+ if (edit.kind === "clear") {
590
+ if (overridden) plan.push({ field, write: { kind: "clear" } });
591
+ continue;
592
+ }
593
+ if (edit.kind !== "text") continue;
594
+ if (edit.text === spec.format(effective)) continue;
595
+ plan.push({ field, write: spec.parse(edit.text) });
596
+ }
597
+ return plan;
598
+ }
599
+ function orderPairWrites(writes, current) {
600
+ const ordered = [...writes];
601
+ for (const pair of THRESHOLD_PAIRS) {
602
+ const warnAt = ordered.findIndex((item) => item.field === pair.warn);
603
+ const criticalAt = ordered.findIndex((item) => item.field === pair.critical);
604
+ if (warnAt === -1 || criticalAt === -1) continue;
605
+ if (criticalAt < warnAt) continue;
606
+ const write = ordered[warnAt].write;
607
+ if (write === void 0) continue;
608
+ const next = write.kind === "clear" ? pair.defaultWarn : Number(write.value);
609
+ if (!Number.isFinite(next)) continue;
610
+ const criticalNow = current[pair.critical];
611
+ if (typeof criticalNow === "number" && Number.isFinite(criticalNow) && next > criticalNow) continue;
612
+ const [moved] = ordered.splice(warnAt, 1);
613
+ ordered.splice(criticalAt, 0, moved);
614
+ }
615
+ return ordered;
616
+ }
617
+ function fieldStateOf(snapshot, spec, edit) {
618
+ const effective = snapshot.value[spec.field];
619
+ const stored = Object.hasOwn(snapshot.user, spec.field);
620
+ if (edit === void 0) {
621
+ return isTextSpec(spec) ? { text: spec.format(effective), value: void 0, effective, stored, overridden: stored, dirty: false, invalid: false } : { text: "", value: effective, effective, stored, overridden: stored, dirty: false, invalid: false };
622
+ }
623
+ if (edit.kind === "clear") {
624
+ return isTextSpec(spec) ? { text: "", value: void 0, effective, stored, overridden: false, dirty: stored, invalid: false } : { text: "", value: effective, effective, stored, overridden: false, dirty: stored, invalid: false };
625
+ }
626
+ if (!isTextSpec(spec)) {
627
+ return edit.kind === "value" ? { text: "", value: edit.value, effective, stored, overridden: true, dirty: edit.value !== effective, invalid: false } : { text: "", value: effective, effective, stored, overridden: stored, dirty: false, invalid: false };
628
+ }
629
+ if (edit.kind !== "text") {
630
+ return { text: "", value: void 0, effective, stored, overridden: stored, dirty: false, invalid: false };
631
+ }
632
+ const write = spec.parse(edit.text);
633
+ const dirty = write === void 0 ? true : write.kind === "clear" ? stored : edit.text !== spec.format(effective);
634
+ return {
635
+ text: edit.text,
636
+ value: void 0,
637
+ effective,
638
+ stored,
639
+ overridden: write !== void 0 && write.kind === "set",
640
+ dirty,
641
+ invalid: write === void 0
642
+ };
643
+ }
644
+ function fieldOf(snapshot, name, edit) {
645
+ const spec = SPEC_BY_FIELD.get(name);
646
+ if (spec === void 0) {
647
+ return {
648
+ text: "",
649
+ value: void 0,
650
+ effective: void 0,
651
+ stored: false,
652
+ overridden: false,
653
+ dirty: false,
654
+ invalid: false
655
+ };
656
+ }
657
+ return fieldStateOf(snapshot, spec, edit);
658
+ }
659
+ function useConfigForm(scope) {
660
+ const cacheRef = (0, import_react2.useRef)(null);
661
+ const readSnapshot = (0, import_react2.useCallback)(() => {
662
+ const raw = scope.getSnapshot();
663
+ const value = asRecord(raw.value);
664
+ const user = asRecord(raw.user);
665
+ const writable = raw.writable !== false;
666
+ const cache = cacheRef.current;
667
+ if (cache !== null && cache.writable === writable && (cache.value === value || shallowEqual(cache.value, value)) && (cache.user === user || shallowEqual(cache.user, user))) {
668
+ return cache.snapshot;
669
+ }
670
+ const snapshot2 = { value, user, writable };
671
+ cacheRef.current = { value, user, writable, snapshot: snapshot2 };
672
+ return snapshot2;
673
+ }, [scope]);
674
+ const subscribe = (0, import_react2.useCallback)(
675
+ (listener) => scope.subscribe(listener),
676
+ [scope]
677
+ );
678
+ const snapshot = (0, import_react2.useSyncExternalStore)(subscribe, readSnapshot);
679
+ const [staged, setStaged] = (0, import_react2.useState)(() => /* @__PURE__ */ new Map());
680
+ const [saving, setSaving] = (0, import_react2.useState)(false);
681
+ const [failed, setFailed] = (0, import_react2.useState)(false);
682
+ const [blurred, setBlurred] = (0, import_react2.useState)(() => /* @__PURE__ */ new Set());
683
+ const savingRef = (0, import_react2.useRef)(false);
684
+ const [test, setTest] = (0, import_react2.useState)({ running: false, ok: null, message: "" });
685
+ const testTimerRef = (0, import_react2.useRef)(null);
686
+ const testRunningRef = (0, import_react2.useRef)(false);
687
+ (0, import_react2.useEffect)(() => () => {
688
+ if (testTimerRef.current !== null) clearTimeout(testTimerRef.current);
689
+ testTimerRef.current = null;
690
+ testRunningRef.current = false;
691
+ }, []);
692
+ const plan = (0, import_react2.useMemo)(() => planWrites(snapshot, staged), [snapshot, staged]);
693
+ const pairsInvalid = (0, import_react2.useMemo)(
694
+ () => THRESHOLD_PAIRS.some((pair) => !thresholdsOk(
695
+ pair,
696
+ fieldOf(snapshot, pair.warn, staged.get(pair.warn)),
697
+ fieldOf(snapshot, pair.critical, staged.get(pair.critical))
698
+ )),
699
+ [snapshot, staged]
700
+ );
701
+ const invalid = plan.some((item) => item.write === void 0) || pairsInvalid;
702
+ const stage = (0, import_react2.useCallback)((field2, edit2) => {
703
+ setStaged((current) => {
704
+ const next = new Map(current);
705
+ if (edit2 === null) next.delete(field2);
706
+ else next.set(field2, edit2);
707
+ return next;
708
+ });
709
+ setFailed(false);
710
+ if (testTimerRef.current !== null) {
711
+ clearTimeout(testTimerRef.current);
712
+ testTimerRef.current = null;
713
+ }
714
+ testRunningRef.current = false;
715
+ setTest((current) => current.running || current.ok !== null ? { running: false, ok: null, message: "" } : current);
716
+ }, []);
717
+ const edit = (0, import_react2.useCallback)((field2, text) => {
718
+ stage(field2, { kind: "text", text });
719
+ }, [stage]);
720
+ const setValue = (0, import_react2.useCallback)((field2, value) => {
721
+ stage(field2, { kind: "value", value });
722
+ }, [stage]);
723
+ const resetField = (0, import_react2.useCallback)((field2) => {
724
+ stage(field2, { kind: "clear" });
725
+ }, [stage]);
726
+ const discard = (0, import_react2.useCallback)(() => {
727
+ setStaged((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
728
+ setFailed(false);
729
+ if (testTimerRef.current !== null) {
730
+ clearTimeout(testTimerRef.current);
731
+ testTimerRef.current = null;
732
+ }
733
+ testRunningRef.current = false;
734
+ setTest((current) => current.running || current.ok !== null ? { running: false, ok: null, message: "" } : current);
735
+ }, []);
736
+ const save = (0, import_react2.useCallback)(async () => {
737
+ if (savingRef.current) return;
738
+ const current = readSnapshot();
739
+ const writes = orderPairWrites(planWrites(current, staged), current.value);
740
+ if (writes.length === 0) return;
741
+ if (writes.some((item) => item.write === void 0)) return;
742
+ savingRef.current = true;
743
+ setSaving(true);
744
+ setFailed(false);
745
+ let landed = true;
746
+ for (const item of writes) {
747
+ const write = item.write;
748
+ if (write === void 0) {
749
+ landed = false;
750
+ break;
751
+ }
752
+ try {
753
+ const outcome = write.kind === "clear" ? scope.unset(item.field) : scope.set(item.field, write.value);
754
+ await settle(outcome);
755
+ } catch {
756
+ landed = false;
757
+ break;
758
+ }
759
+ if (!landedWrite(readSnapshot(), item.field, write)) {
760
+ landed = false;
761
+ break;
762
+ }
763
+ }
764
+ savingRef.current = false;
765
+ setSaving(false);
766
+ if (landed) setStaged(/* @__PURE__ */ new Map());
767
+ setFailed(!landed);
768
+ }, [readSnapshot, scope, staged]);
769
+ const runTest = (0, import_react2.useCallback)(() => {
770
+ if (testRunningRef.current) return;
771
+ const current = readSnapshot();
772
+ const draftOf = (field2) => {
773
+ const spec = SPEC_BY_FIELD.get(field2);
774
+ const edit2 = staged.get(field2);
775
+ if (edit2 !== void 0 && edit2.kind === "text") return edit2.text;
776
+ return spec !== void 0 && isTextSpec(spec) ? spec.format(current.value[field2]) : "";
777
+ };
778
+ const failure = probeFailure(draftOf("baseUrl"), draftOf("apiKey"), draftOf("apiKeyRef"));
779
+ testRunningRef.current = true;
780
+ setTest({ running: true, ok: null, message: "" });
781
+ testTimerRef.current = setTimeout(() => {
782
+ testTimerRef.current = null;
783
+ testRunningRef.current = false;
784
+ setTest(failure === null ? { running: false, ok: true, message: "" } : { running: false, ok: false, message: failure });
785
+ }, TEST_LATENCY_MS);
786
+ }, [readSnapshot, staged]);
787
+ const field = (0, import_react2.useCallback)(
788
+ (name) => fieldOf(snapshot, name, staged.get(name)),
789
+ [snapshot, staged]
790
+ );
791
+ const thresholdPairOk = (0, import_react2.useCallback)(
792
+ (currency) => {
793
+ const pair = THRESHOLD_PAIRS.find((item) => item.currency === currency);
794
+ if (pair === void 0) return true;
795
+ return thresholdsOk(
796
+ pair,
797
+ fieldOf(snapshot, pair.warn, staged.get(pair.warn)),
798
+ fieldOf(snapshot, pair.critical, staged.get(pair.critical))
799
+ );
800
+ },
801
+ [snapshot, staged]
802
+ );
803
+ const touch = (0, import_react2.useCallback)((name) => {
804
+ setBlurred((current) => current.has(name) ? current : new Set(current).add(name));
805
+ }, []);
806
+ const touched = (0, import_react2.useCallback)((name) => blurred.has(name), [blurred]);
807
+ return {
808
+ state: {
809
+ writable: snapshot.writable,
810
+ dirty: plan.length > 0,
811
+ invalid,
812
+ saving,
813
+ failed
814
+ },
815
+ field,
816
+ edit,
817
+ setValue,
818
+ resetField,
819
+ discard,
820
+ save,
821
+ thresholdPairOk,
822
+ touched,
823
+ touch,
824
+ test,
825
+ runTest
826
+ };
827
+ }
828
+
829
+ // src/client/settings/use-credential-state.ts
830
+ var import_react3 = require("react");
831
+
832
+ // src/client/data.ts
833
+ var BALANCE_PATH = "/api/v1/balance";
834
+ var REFRESH_PATH = "/api/v1/balance/refresh";
835
+ var CONFIG_PATH = "/api/v1/config";
836
+ var UNREACHABLE_CODE = "PLUGIN_UNREACHABLE";
837
+ function pendingView() {
838
+ return {
839
+ requestId: "",
840
+ schemaVersion: 1,
841
+ state: "empty",
842
+ stale: false,
843
+ fetchedAt: 0,
844
+ ageMs: 0,
845
+ isAvailable: false,
846
+ accountTag8: "",
847
+ balances: [],
848
+ selected: null,
849
+ severity: "unknown",
850
+ thresholds: {},
851
+ todayUsage: null,
852
+ error: null
853
+ };
854
+ }
855
+ function unreachableView(message) {
856
+ return {
857
+ ...pendingView(),
858
+ state: "error",
859
+ error: { code: UNREACHABLE_CODE, message }
860
+ };
861
+ }
862
+ async function readJson(response, path) {
863
+ if (!response.ok) throw new Error(`${path} responded ${response.status}`);
864
+ try {
865
+ return await response.json();
866
+ } catch {
867
+ throw new Error(`${path} did not return JSON`);
868
+ }
869
+ }
870
+ async function requestBalance(options) {
871
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
872
+ const url = `${BALANCE_PATH}?currency=${encodeURIComponent(options.currency)}`;
873
+ const response = await fetchImpl(url, {
874
+ headers: { accept: "application/json" },
875
+ ...options.signal === void 0 ? {} : { signal: options.signal }
876
+ });
877
+ return await readJson(response, BALANCE_PATH);
878
+ }
879
+ function readCredential(value) {
880
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
881
+ const record = value;
882
+ if (typeof record.ref !== "string" || record.ref === "") return null;
883
+ return {
884
+ ref: record.ref,
885
+ configured: record.configured === true,
886
+ source: typeof record.source === "string" ? record.source : null,
887
+ writable: record.writable === true
888
+ };
889
+ }
890
+ async function requestConfig(options = {}) {
891
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
892
+ const response = await fetchImpl(CONFIG_PATH, { headers: { accept: "application/json" } });
893
+ const body = await readJson(response, CONFIG_PATH);
894
+ return { ...body, credential: readCredential(body?.credential) };
895
+ }
896
+ async function requestRefresh(options = {}) {
897
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
898
+ const response = await fetchImpl(REFRESH_PATH, {
899
+ method: "POST",
900
+ headers: { "content-type": "application/json", accept: "application/json" },
901
+ body: JSON.stringify({ reason: options.reason ?? "manual" })
902
+ });
903
+ return await readJson(response, REFRESH_PATH);
904
+ }
905
+
906
+ // src/client/settings/use-credential-state.ts
907
+ function credentialViewOf(credential, overridden) {
908
+ if (overridden) return "overridden";
909
+ if (credential?.writable !== true) return "env";
910
+ return credential.configured ? "configured" : "notConfigured";
911
+ }
912
+ function useCredentialState(ref) {
913
+ const [state, setState] = (0, import_react3.useState)(null);
914
+ (0, import_react3.useEffect)(() => {
915
+ let cancelled = false;
916
+ void requestConfig().then((body) => {
917
+ if (!cancelled) setState(body?.credential ?? null);
918
+ }).catch(() => {
919
+ if (!cancelled) setState(null);
920
+ });
921
+ return () => {
922
+ cancelled = true;
923
+ };
924
+ }, [ref]);
925
+ return state;
926
+ }
927
+
928
+ // src/client/settings/BalanceSettingsCard.module.css
929
+ var BalanceSettingsCard_default = {
930
+ card: "BalanceSettingsCard_card",
931
+ cardOpen: "BalanceSettingsCard_cardOpen",
932
+ header: "BalanceSettingsCard_header",
933
+ headText: "BalanceSettingsCard_headText",
934
+ name: "BalanceSettingsCard_name",
935
+ description: "BalanceSettingsCard_description",
936
+ chevronWrap: "BalanceSettingsCard_chevronWrap",
937
+ chevron: "BalanceSettingsCard_chevron",
938
+ chevronOpen: "BalanceSettingsCard_chevronOpen",
939
+ pending: "BalanceSettingsCard_pending",
940
+ body: "BalanceSettingsCard_body",
941
+ readOnly: "BalanceSettingsCard_readOnly",
942
+ footer: "BalanceSettingsCard_footer",
943
+ failed: "BalanceSettingsCard_failed",
944
+ notice: "BalanceSettingsCard_notice",
945
+ discard: "BalanceSettingsCard_discard",
946
+ save: "BalanceSettingsCard_save"
947
+ };
948
+
949
+ // src/client/settings/BalanceSettingsCard.tsx
950
+ var import_jsx_runtime2 = require("react/jsx-runtime");
951
+ var DEFAULT_API_KEY_REF = "DEEPSEEK_API_KEY";
952
+ var CREDENTIAL_BADGE = {
953
+ env: "settings.credential.envLocked",
954
+ configured: "settings.configured",
955
+ notConfigured: "settings.notConfigured",
956
+ overridden: "settings.overridden"
957
+ };
958
+ var CREDENTIAL_HINT = {
959
+ env: "settings.hint.credential.env",
960
+ configured: "settings.hint.credential.configured",
961
+ notConfigured: "settings.hint.credential.notConfigured",
962
+ overridden: "settings.hint.credential.overridden"
963
+ };
964
+ var DEFAULT_GROUP_OPEN = {
965
+ connection: false,
966
+ display: false,
967
+ thresholds: false,
968
+ refresh: false
969
+ };
970
+ var GROUP_FIELDS = {
971
+ connection: ["apiKey", "apiKeyRef", "baseUrl"],
972
+ display: ["displayCurrency"],
973
+ thresholds: ["cnyWarn", "cnyCritical", "usdWarn", "usdCritical"],
974
+ refresh: ["serverRefreshSeconds", "clientPollSeconds", "manualRefreshCooldownSeconds"]
975
+ };
976
+ var FIELD_IDS = {
977
+ apiKey: "ds-balance-api-key",
978
+ apiKeyRef: "ds-balance-api-key-ref",
979
+ apiKeyState: "ds-balance-api-key-state",
980
+ baseUrl: "ds-balance-base-url",
981
+ serverRefreshSeconds: "ds-balance-server-refresh-seconds",
982
+ clientPollSeconds: "ds-balance-client-poll-seconds",
983
+ manualRefreshCooldownSeconds: "ds-balance-manual-refresh-cooldown-seconds",
984
+ displayCurrency: "ds-balance-display-currency",
985
+ cnyWarn: "ds-balance-cny-warn",
986
+ cnyCritical: "ds-balance-cny-critical",
987
+ usdWarn: "ds-balance-usd-warn",
988
+ usdCritical: "ds-balance-usd-critical"
989
+ };
990
+ function isFilled(value) {
991
+ if (value === void 0 || value === null) return false;
992
+ return typeof value !== "string" || value.trim() !== "";
993
+ }
994
+ function BalanceSettingsCard({ t, scope }) {
995
+ const form = useConfigForm(scope);
996
+ const [open, setOpen] = (0, import_react4.useState)(false);
997
+ const [revealed, setRevealed] = (0, import_react4.useState)(false);
998
+ const [groupOpen, setGroupOpen] = (0, import_react4.useState)(DEFAULT_GROUP_OPEN);
999
+ const saveStarted = (0, import_react4.useRef)(false);
1000
+ const toggleGroup = (key) => {
1001
+ setGroupOpen((current) => ({ ...current, [key]: !current[key] }));
1002
+ };
1003
+ const groupOpenNow = (key) => groupOpen[key] || GROUP_FIELDS[key].some((name) => form.field(name).invalid) || key === "thresholds" && THRESHOLD_PAIRS.some((pair) => !form.thresholdPairOk(pair.currency));
1004
+ const { writable, dirty, invalid, saving, failed } = form.state;
1005
+ const disabled = !writable || saving;
1006
+ (0, import_react4.useEffect)(() => {
1007
+ if (saving) {
1008
+ saveStarted.current = true;
1009
+ return;
1010
+ }
1011
+ if (!saveStarted.current) return;
1012
+ saveStarted.current = false;
1013
+ if (!dirty && !failed) setOpen(false);
1014
+ }, [dirty, failed, saving]);
1015
+ const credentialStatus = (name) => {
1016
+ const state = form.field(name);
1017
+ if (state.stored) return { label: t("settings.overridden"), tone: "neutral" };
1018
+ if (isFilled(state.effective)) return { label: t("settings.configured"), tone: "neutral" };
1019
+ return { label: t("settings.notConfigured"), tone: "quiet" };
1020
+ };
1021
+ const textRow = (name, labelKey, hintKey, withStatus) => {
1022
+ const state = form.field(name);
1023
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1024
+ FieldFrame,
1025
+ {
1026
+ id: FIELD_IDS[name] ?? name,
1027
+ label: t(labelKey),
1028
+ status: withStatus ? credentialStatus(name) : null,
1029
+ pending: state.dirty,
1030
+ resettable: state.overridden || state.dirty,
1031
+ pendingLabel: t("settings.unsaved"),
1032
+ resetLabel: t("settings.reset"),
1033
+ invalid: state.invalid,
1034
+ hint: t(hintKey),
1035
+ disabled,
1036
+ onReset: () => {
1037
+ form.resetField(name);
1038
+ },
1039
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1040
+ TextControl,
1041
+ {
1042
+ id: FIELD_IDS[name] ?? name,
1043
+ text: state.text,
1044
+ numeric: false,
1045
+ invalid: state.invalid,
1046
+ disabled,
1047
+ onEdit: (text) => {
1048
+ form.edit(name, text);
1049
+ }
1050
+ }
1051
+ )
1052
+ }
1053
+ );
1054
+ };
1055
+ const numberRow = (name, labelKey, hintKey, note) => {
1056
+ const state = form.field(name);
1057
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1058
+ FieldFrame,
1059
+ {
1060
+ id: FIELD_IDS[name] ?? name,
1061
+ label: t(labelKey),
1062
+ pending: state.dirty,
1063
+ resettable: state.overridden || state.dirty,
1064
+ pendingLabel: t("settings.unsaved"),
1065
+ resetLabel: t("settings.reset"),
1066
+ invalid: state.invalid,
1067
+ invalidNote: t("settings.invalidNumber"),
1068
+ hint: hintKey === void 0 ? void 0 : t(hintKey),
1069
+ disabled,
1070
+ onReset: () => {
1071
+ form.resetField(name);
1072
+ },
1073
+ children: [
1074
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1075
+ TextControl,
1076
+ {
1077
+ id: FIELD_IDS[name] ?? name,
1078
+ text: state.text,
1079
+ numeric: true,
1080
+ invalid: state.invalid,
1081
+ disabled,
1082
+ onBlur: () => {
1083
+ form.touch(name);
1084
+ },
1085
+ onEdit: (text) => {
1086
+ form.edit(name, text);
1087
+ }
1088
+ }
1089
+ ),
1090
+ note ?? null
1091
+ ]
1092
+ }
1093
+ );
1094
+ };
1095
+ const pairNote = (currency2) => {
1096
+ const pair = THRESHOLD_PAIRS.find((item) => item.currency === currency2);
1097
+ if (pair === void 0 || form.thresholdPairOk(currency2)) return null;
1098
+ const drafting = form.field(pair.warn).dirty || form.field(pair.critical).dirty;
1099
+ if (drafting && !form.touched(pair.warn) && !form.touched(pair.critical)) return null;
1100
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: fields_default.pairNote, role: "alert", children: t("settings.hint.thresholdPair") });
1101
+ };
1102
+ const apiKey = form.field("apiKey");
1103
+ const apiKeyRef = form.field("apiKeyRef");
1104
+ const effectiveRef = typeof apiKeyRef.effective === "string" && apiKeyRef.effective !== "" ? apiKeyRef.effective : DEFAULT_API_KEY_REF;
1105
+ const credential = useCredentialState(effectiveRef);
1106
+ const credentialView = credentialViewOf(credential, apiKey.stored || apiKeyRef.stored);
1107
+ const currency = form.field("displayCurrency");
1108
+ const currencyId = typeof currency.value === "string" && currency.value !== "" ? currency.value : AUTO_CURRENCY;
1109
+ const currencyOptions = [
1110
+ { id: AUTO_CURRENCY, label: t("settings.currency.auto") },
1111
+ ...currencyCodes(currency.value, currency.effective).map((code) => ({ id: code, label: code }))
1112
+ ];
1113
+ const currencyRow = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: fields_default.row, children: [
1114
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: fields_default.rowText, children: [
1115
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: fields_default.rowTitleLine, children: [
1116
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: fields_default.rowTitle, children: t("settings.field.displayCurrency") }),
1117
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1118
+ FieldBadges,
1119
+ {
1120
+ pending: currency.dirty,
1121
+ resettable: currency.dirty,
1122
+ pendingLabel: t("settings.unsaved"),
1123
+ resetLabel: t("settings.reset"),
1124
+ disabled,
1125
+ onReset: () => {
1126
+ form.resetField("displayCurrency");
1127
+ }
1128
+ }
1129
+ )
1130
+ ] }),
1131
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: fields_default.hint, children: t("settings.hint.displayCurrency") })
1132
+ ] }),
1133
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1134
+ SelectorControl,
1135
+ {
1136
+ id: FIELD_IDS.displayCurrency ?? "displayCurrency",
1137
+ label: t("settings.field.displayCurrency"),
1138
+ options: currencyOptions,
1139
+ selectedId: currencyId,
1140
+ disabled,
1141
+ onSelect: (id) => {
1142
+ form.setValue("displayCurrency", id);
1143
+ }
1144
+ }
1145
+ )
1146
+ ] });
1147
+ const testResult = form.test.ok === null ? null : form.test.ok ? { ok: true, text: t("settings.test.ok") } : { ok: false, text: interpolate(t("settings.test.fail"), { message: form.test.message }) };
1148
+ const footerNote = failed ? { text: t("settings.failed"), className: BalanceSettingsCard_default.failed } : invalid ? { text: t("settings.invalid"), className: BalanceSettingsCard_default.notice } : null;
1149
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: clsx_default(BalanceSettingsCard_default.card, open && BalanceSettingsCard_default.cardOpen), children: [
1150
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1151
+ "button",
1152
+ {
1153
+ type: "button",
1154
+ className: BalanceSettingsCard_default.header,
1155
+ "aria-expanded": open,
1156
+ onClick: () => {
1157
+ setOpen(!open);
1158
+ },
1159
+ children: [
1160
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: BalanceSettingsCard_default.headText, children: [
1161
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: BalanceSettingsCard_default.name, children: t("settings.title") }),
1162
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: BalanceSettingsCard_default.description, children: t("settings.description") })
1163
+ ] }),
1164
+ dirty ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tag, { tone: "neutral", className: BalanceSettingsCard_default.pending, children: t("settings.unsaved") }) : null,
1165
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: BalanceSettingsCard_default.chevronWrap, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.IconChevronDownOutline14, { className: clsx_default(BalanceSettingsCard_default.chevron, open && BalanceSettingsCard_default.chevronOpen) }) })
1166
+ ]
1167
+ }
1168
+ ),
1169
+ open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: BalanceSettingsCard_default.body, children: [
1170
+ writable ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: BalanceSettingsCard_default.readOnly, role: "status", children: t("settings.readOnly") }),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1172
+ FieldGroup,
1173
+ {
1174
+ icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.IconApiOutline14, { size: 14 }),
1175
+ title: t("settings.group.connection"),
1176
+ open: groupOpenNow("connection"),
1177
+ onToggle: () => {
1178
+ toggleGroup("connection");
1179
+ },
1180
+ children: [
1181
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1182
+ FieldFrame,
1183
+ {
1184
+ id: FIELD_IDS.apiKeyState ?? "apiKeyState",
1185
+ label: t("settings.field.apiKey"),
1186
+ status: { label: t(CREDENTIAL_BADGE[credentialView]), tone: credentialView === "notConfigured" ? "quiet" : "neutral" },
1187
+ pending: false,
1188
+ resettable: false,
1189
+ pendingLabel: t("settings.unsaved"),
1190
+ resetLabel: t("settings.reset"),
1191
+ invalid: false,
1192
+ hint: t(CREDENTIAL_HINT[credentialView]),
1193
+ disabled: true,
1194
+ onReset: () => {
1195
+ },
1196
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1197
+ ReadOnlyControl,
1198
+ {
1199
+ id: FIELD_IDS.apiKeyState ?? "apiKeyState"
1200
+ }
1201
+ )
1202
+ }
1203
+ ),
1204
+ textRow("baseUrl", "settings.field.baseUrl", "settings.hint.baseUrl", false),
1205
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: fields_default.field, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1206
+ ActionRow,
1207
+ {
1208
+ label: form.test.running ? t("settings.testing") : t("settings.test"),
1209
+ disabled: disabled || form.test.running,
1210
+ result: testResult,
1211
+ onClick: form.runTest
1212
+ }
1213
+ ) }, "test"),
1214
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(DetailsGroup, { title: t("settings.group.customized"), children: [
1215
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1216
+ FieldFrame,
1217
+ {
1218
+ id: FIELD_IDS.apiKey ?? "apiKey",
1219
+ label: t("settings.field.apiKey"),
1220
+ status: credentialStatus("apiKey"),
1221
+ pending: apiKey.dirty,
1222
+ resettable: apiKey.overridden || apiKey.dirty,
1223
+ pendingLabel: t("settings.unsaved"),
1224
+ resetLabel: t("settings.reset"),
1225
+ invalid: false,
1226
+ hint: t("settings.hint.apiKey"),
1227
+ disabled,
1228
+ onReset: () => {
1229
+ form.resetField("apiKey");
1230
+ },
1231
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1232
+ SecretControl,
1233
+ {
1234
+ id: FIELD_IDS.apiKey ?? "apiKey",
1235
+ text: apiKey.text,
1236
+ invalid: false,
1237
+ disabled,
1238
+ revealed,
1239
+ revealLabel: t("settings.field.apiKey"),
1240
+ onToggleReveal: () => {
1241
+ setRevealed(!revealed);
1242
+ },
1243
+ onEdit: (text) => {
1244
+ form.edit("apiKey", text);
1245
+ }
1246
+ }
1247
+ )
1248
+ }
1249
+ ),
1250
+ textRow("apiKeyRef", "settings.field.apiKeyRef", "settings.hint.apiKeyRef", true)
1251
+ ] })
1252
+ ]
1253
+ }
1254
+ ),
1255
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1256
+ FieldGroup,
1257
+ {
1258
+ icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.IconGlobeOutline14, { size: 14 }),
1259
+ title: t("settings.group.display"),
1260
+ open: groupOpenNow("display"),
1261
+ onToggle: () => {
1262
+ toggleGroup("display");
1263
+ },
1264
+ children: currencyRow
1265
+ }
1266
+ ),
1267
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1268
+ FieldGroup,
1269
+ {
1270
+ icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.IconWarningOutline16, { size: 14 }),
1271
+ title: t("settings.group.thresholds"),
1272
+ note: t("settings.hint.threshold"),
1273
+ open: groupOpenNow("thresholds"),
1274
+ onToggle: () => {
1275
+ toggleGroup("thresholds");
1276
+ },
1277
+ children: [
1278
+ numberRow("cnyWarn", "settings.field.cnyWarn"),
1279
+ numberRow("cnyCritical", "settings.field.cnyCritical", void 0, pairNote("CNY")),
1280
+ numberRow("usdWarn", "settings.field.usdWarn"),
1281
+ numberRow("usdCritical", "settings.field.usdCritical", void 0, pairNote("USD"))
1282
+ ]
1283
+ }
1284
+ ),
1285
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1286
+ FieldGroup,
1287
+ {
1288
+ icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.IconRefreshOutline14, { size: 14 }),
1289
+ title: t("settings.group.refresh"),
1290
+ note: t("settings.hint.refreshAdvanced"),
1291
+ open: groupOpenNow("refresh"),
1292
+ onToggle: () => {
1293
+ toggleGroup("refresh");
1294
+ },
1295
+ last: true,
1296
+ children: [
1297
+ numberRow("serverRefreshSeconds", "settings.field.serverRefreshSeconds", "settings.hint.serverRefreshSeconds"),
1298
+ numberRow("clientPollSeconds", "settings.field.clientPollSeconds", "settings.hint.clientPollSeconds"),
1299
+ numberRow("manualRefreshCooldownSeconds", "settings.field.manualRefreshCooldownSeconds", "settings.hint.manualRefreshCooldownSeconds")
1300
+ ]
1301
+ }
1302
+ ),
1303
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: BalanceSettingsCard_default.footer, children: [
1304
+ footerNote === null ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: footerNote.className, role: "status", children: footerNote.text }),
1305
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1306
+ "button",
1307
+ {
1308
+ type: "button",
1309
+ className: BalanceSettingsCard_default.discard,
1310
+ disabled: !dirty || saving,
1311
+ onClick: form.discard,
1312
+ children: t("settings.discard")
1313
+ }
1314
+ ),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1316
+ "button",
1317
+ {
1318
+ type: "button",
1319
+ className: BalanceSettingsCard_default.save,
1320
+ disabled: !dirty || invalid || saving || !writable,
1321
+ onClick: () => {
1322
+ void form.save();
1323
+ },
1324
+ children: saving ? t("settings.saving") : t("settings.save")
1325
+ }
1326
+ )
1327
+ ] })
1328
+ ] }) : null
1329
+ ] });
1330
+ }
1331
+
1332
+ // src/client/sidebar/SidebarBalance.tsx
1333
+ var import_react5 = require("react");
1334
+ var import_react_dom = require("react-dom");
1335
+ var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
1336
+
1337
+ // src/client/model.ts
1338
+ function dotStateOf(severity) {
1339
+ switch (severity) {
1340
+ case "ok":
1341
+ return "done";
1342
+ case "warn":
1343
+ return "warning";
1344
+ case "critical":
1345
+ return "error";
1346
+ case "unavailable":
1347
+ return "error";
1348
+ case "unknown":
1349
+ return "idle";
1350
+ }
1351
+ }
1352
+ function ringSpecOf(severity) {
1353
+ return {
1354
+ state: dotStateOf(severity),
1355
+ marker: severity === "unavailable" ? "cross" : null
1356
+ };
1357
+ }
1358
+ var DECIMAL_SCALE = 100000000n;
1359
+ var ARC_STEPS = 10000n;
1360
+ var DECIMAL_PATTERN = /^(-?)(\d+)(?:\.(\d*))?$/;
1361
+ var QUALITATIVE_ARC = {
1362
+ ok: 1,
1363
+ warn: 0.75,
1364
+ critical: 0.25,
1365
+ unavailable: 1,
1366
+ unknown: 0
1367
+ };
1368
+ function scaledOf(value) {
1369
+ const match = DECIMAL_PATTERN.exec(value.trim());
1370
+ if (match === null) return null;
1371
+ const [, sign, whole, fraction = ""] = match;
1372
+ const magnitude = BigInt(whole) * DECIMAL_SCALE + BigInt((fraction + "00000000").slice(0, 8));
1373
+ return sign === "-" ? -magnitude : magnitude;
1374
+ }
1375
+ function ringRatioOf(total, warnThreshold, severity) {
1376
+ const balance = total === null ? null : scaledOf(total);
1377
+ const limit = warnThreshold === void 0 || !Number.isFinite(warnThreshold) ? null : scaledOf(String(warnThreshold));
1378
+ if (balance === null || limit === null || limit <= 0n) return QUALITATIVE_ARC[severity];
1379
+ if (balance <= 0n) return 0;
1380
+ if (balance >= limit) return 1;
1381
+ return Number(balance * ARC_STEPS / limit) / Number(ARC_STEPS);
1382
+ }
1383
+ function currencySymbol(currency) {
1384
+ switch (currency.toUpperCase()) {
1385
+ case "CNY":
1386
+ return "\xA5";
1387
+ case "USD":
1388
+ return "$";
1389
+ case "EUR":
1390
+ return "\u20AC";
1391
+ default:
1392
+ return "";
1393
+ }
1394
+ }
1395
+ function formatAmount(value) {
1396
+ const trimmed = value.trim();
1397
+ const match = /^(-?)(\d+)(?:\.(\d*))?$/.exec(trimmed);
1398
+ if (match === null) return trimmed;
1399
+ const [, sign, whole, fraction = ""] = match;
1400
+ const cents = (fraction + "00").slice(0, 2);
1401
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1402
+ return cents === "00" ? `${sign}${grouped}` : `${sign}${grouped}.${cents}`;
1403
+ }
1404
+ function formatMoney(amount, currency) {
1405
+ return `${currencySymbol(currency)}${formatAmount(amount)}`;
1406
+ }
1407
+ function selectionOf(response, preference) {
1408
+ const auto = preference === "auto" || preference === "";
1409
+ const selected = response.selected;
1410
+ if (selected === null) {
1411
+ return { shown: null, matchesPreference: auto, auto, empty: true };
1412
+ }
1413
+ const wanted = selected.currency.toUpperCase();
1414
+ const shown = response.balances.find((item) => item.currency.toUpperCase() === wanted) ?? null;
1415
+ return {
1416
+ shown,
1417
+ matchesPreference: auto || wanted === preference.toUpperCase(),
1418
+ auto,
1419
+ empty: shown === null
1420
+ };
1421
+ }
1422
+ function ageBucket(ageMs) {
1423
+ if (!Number.isFinite(ageMs) || ageMs < 0) return { bucket: "unknown", value: 0 };
1424
+ const seconds = Math.floor(ageMs / 1e3);
1425
+ if (seconds < 5) return { bucket: "just-now", value: seconds };
1426
+ if (seconds < 60) return { bucket: "seconds", value: seconds };
1427
+ const minutes = Math.floor(seconds / 60);
1428
+ if (minutes < 60) return { bucket: "minutes", value: minutes };
1429
+ const hours = Math.floor(minutes / 60);
1430
+ if (hours < 24) return { bucket: "hours", value: hours };
1431
+ return { bucket: "days", value: Math.floor(hours / 24) };
1432
+ }
1433
+
1434
+ // src/client/mock/scenarios.ts
1435
+ var T0 = 176e10;
1436
+ var cny = (total, granted, toppedUp) => ({
1437
+ currency: "CNY",
1438
+ total,
1439
+ granted,
1440
+ toppedUp
1441
+ });
1442
+ function make(patch) {
1443
+ return {
1444
+ requestId: "req_mock",
1445
+ schemaVersion: 1,
1446
+ state: "ok",
1447
+ stale: false,
1448
+ fetchedAt: T0,
1449
+ ageMs: 12e3,
1450
+ isAvailable: true,
1451
+ accountTag8: "a1b2c3d4",
1452
+ balances: [cny("110.00000000", "10.00000000", "100.00000000")],
1453
+ selected: { currency: "CNY", total: "110.00000000" },
1454
+ severity: "ok",
1455
+ thresholds: { CNY: { warn: "10.00000000", critical: "5.00000000" } },
1456
+ todayUsage: null,
1457
+ error: null,
1458
+ ...patch
1459
+ };
1460
+ }
1461
+ var scenarios = {
1462
+ /** 正常。 */
1463
+ ok: make({}),
1464
+ /** 余额偏低但可用。 */
1465
+ warn: make({ severity: "warn", balances: [cny("8.00000000", "0.00000000", "8.00000000")], selected: { currency: "CNY", total: "8.00000000" } }),
1466
+ /** 余额告急。 */
1467
+ critical: make({ severity: "critical", balances: [cny("3.00000000", "0.00000000", "3.00000000")], selected: { currency: "CNY", total: "3.00000000" } }),
1468
+ /** 账户不可用(余额耗尽 / 欠费)。 */
1469
+ unavailable: make({
1470
+ severity: "unavailable",
1471
+ isAvailable: false,
1472
+ balances: [cny("0.00000000", "0.00000000", "0.00000000")],
1473
+ selected: { currency: "CNY", total: "0.00000000" }
1474
+ }),
1475
+ /** 有旧值但本次刷新失败。 */
1476
+ stale: make({ state: "stale", stale: true, severity: "unknown", ageMs: 36e5, fetchedAt: T0 - 36e5, error: { code: "NETWORK", message: "fetch failed" } }),
1477
+ /** 无值失败。 */
1478
+ error: make({
1479
+ state: "error",
1480
+ stale: false,
1481
+ severity: "unknown",
1482
+ isAvailable: false,
1483
+ balances: [],
1484
+ selected: null,
1485
+ ageMs: 0,
1486
+ error: { code: "HTTP_500", message: "upstream returned 500" }
1487
+ }),
1488
+ /** 未配置。 */
1489
+ empty: make({ state: "empty", severity: "unknown", isAvailable: false, balances: [], selected: null, ageMs: 0 }),
1490
+ /** 未配置且带 NO_KEY 错误。 */
1491
+ noKey: make({ state: "empty", severity: "unknown", isAvailable: false, balances: [], selected: null, ageMs: 0, error: { code: "NO_KEY", message: "no API key configured" } }),
1492
+ /** 多币种。`selected` 显式写出,不靠 `make()` 的默认值兜。 */
1493
+ multiCurrency: make({
1494
+ balances: [cny("110.00000000", "10.00000000", "100.00000000"), { currency: "USD", total: "20.00000000", granted: "0.00000000", toppedUp: "20.00000000" }],
1495
+ selected: { currency: "CNY", total: "110.00000000" },
1496
+ thresholds: { CNY: { warn: "10.00000000", critical: "5.00000000" }, USD: { warn: "2.00000000", critical: "1.00000000" } }
1497
+ }),
1498
+ /**
1499
+ * 选定了账户里没有的币种(USD),账户只有 CNY。
1500
+ *
1501
+ * 不匹配由「`selected.currency` 与设置里的显示币种不等」判定 —— 所以场景本身
1502
+ * 只需要保证 `selected` 是账户里真实存在的那个币种,剩下的交给设置值。
1503
+ */
1504
+ currencyMismatch: make({
1505
+ balances: [cny("110.00000000", "10.00000000", "100.00000000")],
1506
+ selected: { currency: "CNY", total: "110.00000000" }
1507
+ }),
1508
+ /** 账户完全没有余额。 */
1509
+ noBalanceAtAll: make({
1510
+ severity: "unknown",
1511
+ isAvailable: true,
1512
+ balances: [],
1513
+ selected: null
1514
+ }),
1515
+ /** 今日用量缺失(第一版 UI 不消费,用于契约回归)。 */
1516
+ usageMissing: make({ todayUsage: null }),
1517
+ /** 今日用量需复核(第一版 UI 不消费,用于契约回归)。 */
1518
+ usageNeedsReview: make({
1519
+ todayUsage: {
1520
+ value: "3.21000000",
1521
+ currency: "CNY",
1522
+ source: "projection",
1523
+ confidence: "low",
1524
+ needsReview: true,
1525
+ range: ["1.50000000", "6.40000000"]
1526
+ }
1527
+ })
1528
+ };
1529
+ var scenarioKeys = Object.keys(scenarios);
1530
+ var defaultScenario = "ok";
1531
+ function isScenarioKey(value) {
1532
+ return Object.prototype.hasOwnProperty.call(scenarios, value);
1533
+ }
1534
+
1535
+ // src/client/mock/index.ts
1536
+ var SCENARIO_PARAM = "dsb";
1537
+ var LIVE_PARAM_VALUE = "live";
1538
+ var STORAGE_KEY = "ds-balance:scenario";
1539
+ function readStored() {
1540
+ try {
1541
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
1542
+ return raw !== null && raw !== void 0 && isScenarioKey(raw) ? raw : null;
1543
+ } catch {
1544
+ return null;
1545
+ }
1546
+ }
1547
+ function writeStored(key) {
1548
+ try {
1549
+ globalThis.localStorage?.setItem(STORAGE_KEY, key);
1550
+ } catch {
1551
+ }
1552
+ }
1553
+ function clearStored() {
1554
+ try {
1555
+ globalThis.localStorage?.removeItem(STORAGE_KEY);
1556
+ } catch {
1557
+ }
1558
+ }
1559
+ function readParam(name) {
1560
+ try {
1561
+ return new URLSearchParams(globalThis.location?.search ?? "").get(name);
1562
+ } catch {
1563
+ return null;
1564
+ }
1565
+ }
1566
+ function resolveScenario() {
1567
+ const fromUrl = readParam(SCENARIO_PARAM);
1568
+ if (fromUrl === LIVE_PARAM_VALUE) {
1569
+ clearStored();
1570
+ return null;
1571
+ }
1572
+ if (fromUrl !== null && isScenarioKey(fromUrl)) {
1573
+ writeStored(fromUrl);
1574
+ return fromUrl;
1575
+ }
1576
+ return readStored();
1577
+ }
1578
+ function currentScenario() {
1579
+ return resolveScenario() ?? defaultScenario;
1580
+ }
1581
+ function subscribeScenario(listener) {
1582
+ listeners.add(listener);
1583
+ listener(currentScenario());
1584
+ return () => {
1585
+ listeners.delete(listener);
1586
+ };
1587
+ }
1588
+ var listeners = /* @__PURE__ */ new Set();
1589
+ function currentBalance() {
1590
+ return scenarios[currentScenario()];
1591
+ }
1592
+
1593
+ // src/client/sidebar/BalancePopover.tsx
1594
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
1595
+
1596
+ // src/client/sidebar/BalancePopover.module.css
1597
+ var BalancePopover_default = {
1598
+ panel: "BalancePopover_panel",
1599
+ title: "BalancePopover_title",
1600
+ titleLabel: "BalancePopover_titleLabel",
1601
+ titleRule: "BalancePopover_titleRule",
1602
+ details: "BalancePopover_details",
1603
+ notice: "BalancePopover_notice",
1604
+ noticeText: "BalancePopover_noticeText",
1605
+ noticeActions: "BalancePopover_noticeActions",
1606
+ footer: "BalancePopover_footer",
1607
+ updated: "BalancePopover_updated",
1608
+ status: "BalancePopover_status",
1609
+ refresh: "BalancePopover_refresh",
1610
+ "balance-refresh-spin": "BalancePopover_balance-refresh-spin"
1611
+ };
1612
+
1613
+ // src/client/sidebar/BalancePopover.tsx
1614
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1615
+ var BALANCE_PLACEHOLDER = "--";
1616
+ var AGE_UNIT = {
1617
+ seconds: "second",
1618
+ minutes: "minute",
1619
+ hours: "hour",
1620
+ days: "day"
1621
+ };
1622
+ function unitLocale(t) {
1623
+ return /[\u3400-\u9fff]/.test(t("popover.updated.justNow")) ? "zh-CN" : "en-US";
1624
+ }
1625
+ function ageValueText(bucket, value, locale) {
1626
+ try {
1627
+ return new Intl.NumberFormat(locale, {
1628
+ style: "unit",
1629
+ unit: AGE_UNIT[bucket],
1630
+ unitDisplay: "long"
1631
+ }).format(value);
1632
+ } catch {
1633
+ return String(value);
1634
+ }
1635
+ }
1636
+ function BalancePopover(props) {
1637
+ const {
1638
+ t,
1639
+ selection,
1640
+ displayCurrency,
1641
+ fetchedAt,
1642
+ now,
1643
+ refreshing,
1644
+ cooldownSeconds,
1645
+ panelRef,
1646
+ style,
1647
+ onRefresh,
1648
+ onUseShown,
1649
+ onOpenSettings
1650
+ } = props;
1651
+ const shown = selection.shown;
1652
+ const totalText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.total, shown.currency);
1653
+ const grantedText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.granted, shown.currency);
1654
+ const toppedUpText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.toppedUp, shown.currency);
1655
+ const age = ageBucket(now - fetchedAt);
1656
+ const updatedText = age.bucket === "just-now" || age.bucket === "unknown" ? t("popover.updated.justNow") : interpolate(t("popover.updated"), {
1657
+ value: ageValueText(age.bucket, age.value, unitLocale(t))
1658
+ });
1659
+ const cooling = cooldownSeconds > 0;
1660
+ let statusText = "";
1661
+ if (refreshing) statusText = t("popover.refreshing");
1662
+ else if (cooling) statusText = interpolate(t("popover.cooldown"), { value: String(cooldownSeconds) });
1663
+ const refreshLabel = t("sidebar.aria.refresh");
1664
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1665
+ "section",
1666
+ {
1667
+ ref: panelRef,
1668
+ className: BalancePopover_default.panel,
1669
+ style,
1670
+ role: "dialog",
1671
+ "aria-label": t("popover.title"),
1672
+ children: [
1673
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: BalancePopover_default.title, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: BalancePopover_default.titleLabel, children: [
1674
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.FishLogo, { size: 14 }),
1675
+ t("popover.title")
1676
+ ] }) }),
1677
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: BalancePopover_default.titleRule, "aria-hidden": true }),
1678
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("dl", { className: BalancePopover_default.details, children: [
1679
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: t("popover.total") }),
1680
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: totalText }),
1681
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: t("popover.granted") }),
1682
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: grantedText }),
1683
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: t("popover.toppedUp") }),
1684
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: toppedUpText })
1685
+ ] }),
1686
+ shown !== null && !selection.matchesPreference ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.notice, children: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: BalancePopover_default.noticeText, children: interpolate(t("popover.mismatch"), { wanted: displayCurrency, shown: shown.currency }) }),
1688
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.noticeActions, children: [
1689
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Button, { size: "sm", variant: "outline", onClick: onUseShown, children: interpolate(t("popover.action.useShown"), { shown: shown.currency }) }),
1690
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Button, { size: "sm", variant: "outline", onClick: onOpenSettings, children: t("popover.action.openSettings") })
1691
+ ] })
1692
+ ] }) : null,
1693
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.footer, children: [
1694
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: BalancePopover_default.updated, children: updatedText }),
1695
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: BalancePopover_default.status, role: "status", children: statusText }),
1696
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label: refreshLabel, side: "top", delayMs: 500, disabled: refreshing || cooling, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1697
+ "button",
1698
+ {
1699
+ type: "button",
1700
+ className: BalancePopover_default.refresh,
1701
+ "aria-label": refreshLabel,
1702
+ disabled: refreshing || cooling,
1703
+ "data-refreshing": refreshing || void 0,
1704
+ "data-cooling": cooling || void 0,
1705
+ onClick: onRefresh,
1706
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.IconRefreshOutline16, { size: 16 })
1707
+ }
1708
+ ) })
1709
+ ] })
1710
+ ]
1711
+ }
1712
+ );
1713
+ }
1714
+
1715
+ // src/client/sidebar/PercentRing.module.css
1716
+ var PercentRing_default = {
1717
+ ring: "PercentRing_ring",
1718
+ track: "PercentRing_track",
1719
+ fill: "PercentRing_fill",
1720
+ cross: "PercentRing_cross"
1721
+ };
1722
+
1723
+ // src/client/sidebar/PercentRing.tsx
1724
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1725
+ var VIEW = 14;
1726
+ var STROKE = 2;
1727
+ var RADIUS = (VIEW - STROKE) / 2;
1728
+ var CENTER = VIEW / 2;
1729
+ var CIRCUMFERENCE = 2 * Math.PI * RADIUS;
1730
+ var CROSS_ARM = 1.8;
1731
+ function round3(value) {
1732
+ return Math.round(value * 1e3) / 1e3;
1733
+ }
1734
+ function PercentRing({ state, marker = null, ratio = 1, size = 18, title }) {
1735
+ const clamped = Number.isFinite(ratio) ? Math.min(1, Math.max(0, ratio)) : 1;
1736
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1737
+ "svg",
1738
+ {
1739
+ className: PercentRing_default.ring,
1740
+ "data-state": state,
1741
+ viewBox: `0 0 ${VIEW} ${VIEW}`,
1742
+ width: size,
1743
+ height: size,
1744
+ "aria-hidden": "true",
1745
+ focusable: "false",
1746
+ children: [
1747
+ title === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("title", { children: title }),
1748
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { className: PercentRing_default.track, cx: CENTER, cy: CENTER, r: RADIUS }),
1749
+ clamped === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1750
+ "circle",
1751
+ {
1752
+ className: PercentRing_default.fill,
1753
+ cx: CENTER,
1754
+ cy: CENTER,
1755
+ r: RADIUS,
1756
+ strokeDasharray: `${round3(CIRCUMFERENCE * clamped)} ${round3(CIRCUMFERENCE)}`,
1757
+ transform: `rotate(-90 ${CENTER} ${CENTER})`
1758
+ }
1759
+ ),
1760
+ marker === "cross" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: PercentRing_default.cross, children: [
1761
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1762
+ "line",
1763
+ {
1764
+ x1: CENTER - CROSS_ARM,
1765
+ y1: CENTER - CROSS_ARM,
1766
+ x2: CENTER + CROSS_ARM,
1767
+ y2: CENTER + CROSS_ARM
1768
+ }
1769
+ ),
1770
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1771
+ "line",
1772
+ {
1773
+ x1: CENTER + CROSS_ARM,
1774
+ y1: CENTER - CROSS_ARM,
1775
+ x2: CENTER - CROSS_ARM,
1776
+ y2: CENTER + CROSS_ARM
1777
+ }
1778
+ )
1779
+ ] }) : null
1780
+ ]
1781
+ }
1782
+ );
1783
+ }
1784
+
1785
+ // src/client/sidebar/SidebarBalance.module.css
1786
+ var SidebarBalance_default = {
1787
+ root: "SidebarBalance_root",
1788
+ trigger: "SidebarBalance_trigger",
1789
+ icon: "SidebarBalance_icon",
1790
+ label: "SidebarBalance_label",
1791
+ marker: "SidebarBalance_marker"
1792
+ };
1793
+
1794
+ // src/client/sidebar/SidebarBalance.tsx
1795
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1796
+ var REFRESH_SIMULATION_MS = 600;
1797
+ var PANEL_GAP = 8;
1798
+ var PANEL_MARGIN = 12;
1799
+ var MEASURE_STYLE = { visibility: "hidden", left: 0, top: 0 };
1800
+ var MS_PER_SECOND = 1e3;
1801
+ var TICK_MS = MS_PER_SECOND;
1802
+ function stateLabelKey(response, selection) {
1803
+ if (response.state === "error") return response.error?.code === "NO_KEY" ? "state.noKey" : "state.error";
1804
+ if (response.state === "empty") return response.error?.code === "NO_KEY" ? "state.noKey" : "state.empty";
1805
+ if (response.state === "stale") return "state.stale";
1806
+ if (!response.isAvailable) return "state.unavailable";
1807
+ if (selection.shown === null) return "state.noBalance";
1808
+ return null;
1809
+ }
1810
+ function ringSpecFor(severity) {
1811
+ const spec = ringSpecOf(severity);
1812
+ return { state: spec.state === "ongoing" ? "idle" : spec.state, marker: spec.marker };
1813
+ }
1814
+ function SidebarBalance({ wide, t, config }) {
1815
+ const [mock, setMock] = (0, import_react5.useState)(() => resolveScenario() !== null);
1816
+ const [response, setResponse] = (0, import_react5.useState)(() => resolveScenario() === null ? pendingView() : currentBalance());
1817
+ const loadedRef = (0, import_react5.useRef)(false);
1818
+ const [localCurrency, setLocalCurrency] = (0, import_react5.useState)(null);
1819
+ const [localFetchedAt, setLocalFetchedAt] = (0, import_react5.useState)(null);
1820
+ const [open, setOpen] = (0, import_react5.useState)(false);
1821
+ const [refreshing, setRefreshing] = (0, import_react5.useState)(false);
1822
+ const [cooldownUntil, setCooldownUntil] = (0, import_react5.useState)(0);
1823
+ const [now, setNow] = (0, import_react5.useState)(() => Date.now());
1824
+ const rootRef = (0, import_react5.useRef)(null);
1825
+ const panelRef = (0, import_react5.useRef)(null);
1826
+ const refreshTimer = (0, import_react5.useRef)(void 0);
1827
+ const lastScenario = (0, import_react5.useRef)(null);
1828
+ const mountedAt = (0, import_react5.useRef)(Date.now());
1829
+ const preference = localCurrency ?? config.displayCurrency;
1830
+ const selection = (0, import_react5.useMemo)(() => selectionOf(response, preference), [response, preference]);
1831
+ const fetchedAt = localFetchedAt ?? mountedAt.current - response.ageMs;
1832
+ (0, import_react5.useEffect)(() => subscribeScenario((key) => {
1833
+ const active = resolveScenario() !== null;
1834
+ setMock(active);
1835
+ if (!active || lastScenario.current === key) return;
1836
+ lastScenario.current = key;
1837
+ setResponse(currentBalance());
1838
+ setLocalFetchedAt(null);
1839
+ setRefreshing(false);
1840
+ setCooldownUntil(0);
1841
+ setOpen(false);
1842
+ }), []);
1843
+ (0, import_react5.useEffect)(() => {
1844
+ if (mock) return;
1845
+ let cancelled = false;
1846
+ const load = async () => {
1847
+ try {
1848
+ const next = await requestBalance({ currency: preference });
1849
+ if (cancelled) return;
1850
+ loadedRef.current = true;
1851
+ setResponse(next);
1852
+ setLocalFetchedAt(null);
1853
+ } catch (error) {
1854
+ if (cancelled || loadedRef.current) return;
1855
+ setResponse(unreachableView(error instanceof Error ? error.message : String(error)));
1856
+ }
1857
+ };
1858
+ void load();
1859
+ const id = window.setInterval(() => {
1860
+ void load();
1861
+ }, Math.max(5, config.clientPollSeconds) * MS_PER_SECOND);
1862
+ return () => {
1863
+ cancelled = true;
1864
+ window.clearInterval(id);
1865
+ };
1866
+ }, [mock, preference, config.clientPollSeconds, config.configSignature]);
1867
+ (0, import_react5.useEffect)(() => () => {
1868
+ if (refreshTimer.current !== void 0) window.clearTimeout(refreshTimer.current);
1869
+ }, []);
1870
+ const closeNow = (0, import_react5.useCallback)(() => {
1871
+ setOpen(false);
1872
+ }, []);
1873
+ const toggleOpen = (0, import_react5.useCallback)(() => {
1874
+ setOpen((value) => !value);
1875
+ }, []);
1876
+ const pos = (0, import_dsh_client_ui_primitives4.useAnchoredPosition)({
1877
+ open,
1878
+ anchorRef: rootRef,
1879
+ panelRef,
1880
+ side: "top",
1881
+ gap: PANEL_GAP,
1882
+ margin: PANEL_MARGIN
1883
+ });
1884
+ (0, import_react5.useEffect)(() => {
1885
+ if (!open) return;
1886
+ setNow(Date.now());
1887
+ const id = window.setInterval(() => {
1888
+ setNow(Date.now());
1889
+ }, TICK_MS);
1890
+ return () => {
1891
+ window.clearInterval(id);
1892
+ };
1893
+ }, [open]);
1894
+ (0, import_react5.useEffect)(() => {
1895
+ if (!open) return;
1896
+ const onKeyDown = (event) => {
1897
+ if (event.key === "Escape") closeNow();
1898
+ };
1899
+ document.addEventListener("keydown", onKeyDown);
1900
+ return () => {
1901
+ document.removeEventListener("keydown", onKeyDown);
1902
+ };
1903
+ }, [open, closeNow]);
1904
+ (0, import_dsh_client_ui_primitives4.useDismissOnOutsidePointer)(rootRef, open, setOpen, panelRef);
1905
+ const handleRefresh = (0, import_react5.useCallback)(() => {
1906
+ if (refreshing) return;
1907
+ if (Date.now() < cooldownUntil) return;
1908
+ setRefreshing(true);
1909
+ const finish = () => {
1910
+ setLocalFetchedAt(Date.now());
1911
+ setRefreshing(false);
1912
+ setCooldownUntil(Date.now() + Math.max(0, config.manualRefreshCooldownSeconds) * MS_PER_SECOND);
1913
+ };
1914
+ if (mock) {
1915
+ refreshTimer.current = window.setTimeout(() => {
1916
+ refreshTimer.current = void 0;
1917
+ finish();
1918
+ }, REFRESH_SIMULATION_MS);
1919
+ return;
1920
+ }
1921
+ void (async () => {
1922
+ try {
1923
+ await requestRefresh({ reason: "manual" });
1924
+ const next = await requestBalance({ currency: preference });
1925
+ loadedRef.current = true;
1926
+ setResponse(next);
1927
+ } catch (error) {
1928
+ if (!loadedRef.current) setResponse(unreachableView(error instanceof Error ? error.message : String(error)));
1929
+ } finally {
1930
+ finish();
1931
+ }
1932
+ })();
1933
+ }, [cooldownUntil, config.manualRefreshCooldownSeconds, mock, preference, refreshing]);
1934
+ const handleUseShown = (0, import_react5.useCallback)(() => {
1935
+ const current = selection.shown;
1936
+ if (current === null) return;
1937
+ setLocalCurrency(current.currency);
1938
+ }, [selection]);
1939
+ const handleOpenSettings = (0, import_react5.useCallback)(() => {
1940
+ }, []);
1941
+ const shown = selection.shown;
1942
+ const ring = ringSpecFor(response.severity);
1943
+ const ringRatio = ringRatioOf(
1944
+ shown === null ? null : shown.total,
1945
+ shown === null ? void 0 : config.warnThresholdOf(shown.currency),
1946
+ response.severity
1947
+ );
1948
+ const stateKey = stateLabelKey(response, selection);
1949
+ const stateText = stateKey === null ? null : t(stateKey);
1950
+ let markerLabel = null;
1951
+ let markerHint = "";
1952
+ if (shown === null) {
1953
+ markerLabel = t(stateKey ?? "state.noBalance");
1954
+ markerHint = markerLabel;
1955
+ } else if (!selection.matchesPreference) {
1956
+ markerLabel = t("sidebar.aria.mismatch");
1957
+ markerHint = interpolate(t("popover.mismatch"), { wanted: preference, shown: shown.currency });
1958
+ }
1959
+ const ariaParts = [wide ? t("sidebar.label") : t("sidebar.aria.ring")];
1960
+ if (shown !== null) ariaParts.push(formatMoney(shown.total, shown.currency));
1961
+ if (stateText !== null && stateText !== markerLabel) ariaParts.push(stateText);
1962
+ if (markerLabel !== null) ariaParts.push(markerLabel);
1963
+ const ariaLabel = ariaParts.join(" ");
1964
+ const cooldownSeconds = Math.max(0, Math.ceil((cooldownUntil - now) / MS_PER_SECOND));
1965
+ const ringTitle = !wide && stateText !== null ? `${t("sidebar.aria.ring")} ${stateText}` : void 0;
1966
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1967
+ "div",
1968
+ {
1969
+ ref: rootRef,
1970
+ className: SidebarBalance_default.root,
1971
+ "data-mode": wide ? "wide" : "rail",
1972
+ tabIndex: -1,
1973
+ children: [
1974
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1975
+ "button",
1976
+ {
1977
+ type: "button",
1978
+ className: SidebarBalance_default.trigger,
1979
+ "aria-label": ariaLabel,
1980
+ "aria-expanded": open,
1981
+ onClick: toggleOpen,
1982
+ children: wide ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1983
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.icon, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PercentRing, { state: ring.state, marker: ring.marker, ratio: ringRatio, size: 16 }) }),
1984
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.label, children: t("sidebar.label") }),
1985
+ markerLabel === null ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.Tooltip, { label: markerHint, side: "bottom", delayMs: 500, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.marker, role: "img", "aria-label": markerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.IconWarningOutline16, { size: 12 }) }) })
1986
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PercentRing, { state: ring.state, marker: ring.marker, ratio: ringRatio, size: 18, title: ringTitle })
1987
+ }
1988
+ ),
1989
+ open ? (0, import_react_dom.createPortal)(
1990
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1991
+ BalancePopover,
1992
+ {
1993
+ t,
1994
+ selection,
1995
+ displayCurrency: preference,
1996
+ fetchedAt,
1997
+ now,
1998
+ refreshing,
1999
+ cooldownSeconds,
2000
+ panelRef,
2001
+ style: pos ?? MEASURE_STYLE,
2002
+ onRefresh: handleRefresh,
2003
+ onUseShown: handleUseShown,
2004
+ onOpenSettings: handleOpenSettings
2005
+ }
2006
+ ),
2007
+ document.body
2008
+ ) : null
2009
+ ]
2010
+ }
2011
+ );
2012
+ }
2013
+
2014
+ // src/client/index.tsx
2015
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2016
+ var SETTINGS_NAMESPACE = "ds-balance";
2017
+ var DEFAULT_CONFIG = {
2018
+ displayCurrency: "auto",
2019
+ manualRefreshCooldownSeconds: 30,
2020
+ clientPollSeconds: 30
2021
+ };
2022
+ var inject = ["slots", "locale", "settingsScope"];
2023
+ function asRecord2(value) {
2024
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2025
+ }
2026
+ function adaptScope(raw) {
2027
+ const scope = raw;
2028
+ return {
2029
+ getSnapshot: () => {
2030
+ const snapshot = typeof scope.getSnapshot === "function" ? scope.getSnapshot() : void 0;
2031
+ return {
2032
+ value: asRecord2(snapshot?.value),
2033
+ user: asRecord2(snapshot?.user),
2034
+ writable: typeof snapshot?.writable === "boolean" ? snapshot.writable : true
2035
+ };
2036
+ },
2037
+ subscribe: (listener) => {
2038
+ const dispose = typeof scope.subscribe === "function" ? scope.subscribe(listener) : void 0;
2039
+ return typeof dispose === "function" ? dispose : () => {
2040
+ };
2041
+ },
2042
+ set: (field, value) => scope.set?.(field, value),
2043
+ unset: (field) => scope.unset?.(field)
2044
+ };
2045
+ }
2046
+ function useScopeValue(scope) {
2047
+ const [value, setValue] = (0, import_react6.useState)(() => scope.getSnapshot().value);
2048
+ (0, import_react6.useEffect)(() => {
2049
+ setValue(scope.getSnapshot().value);
2050
+ return scope.subscribe(() => {
2051
+ setValue(scope.getSnapshot().value);
2052
+ });
2053
+ }, [scope]);
2054
+ return value;
2055
+ }
2056
+ function readDisplayCurrency(value) {
2057
+ const raw = value.displayCurrency;
2058
+ return typeof raw === "string" && raw.length > 0 ? raw : DEFAULT_CONFIG.displayCurrency;
2059
+ }
2060
+ function readCooldown(value) {
2061
+ const raw = Number(value.manualRefreshCooldownSeconds);
2062
+ return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_CONFIG.manualRefreshCooldownSeconds;
2063
+ }
2064
+ function readPollSeconds(value) {
2065
+ const raw = Number(value.clientPollSeconds);
2066
+ return Number.isFinite(raw) && raw >= 5 ? raw : DEFAULT_CONFIG.clientPollSeconds;
2067
+ }
2068
+ function readWarnThreshold(value, currency) {
2069
+ const raw = Number(value[currency.toLowerCase() + "Warn"]);
2070
+ return Number.isFinite(raw) ? raw : void 0;
2071
+ }
2072
+ function readSignature(value) {
2073
+ const parts = Object.keys(value).filter((key) => key !== "apiKey").sort().map((key) => key + "=" + String(value[key]));
2074
+ parts.push("apiKeySet=" + String(value.apiKey !== void 0 && value.apiKey !== ""));
2075
+ return parts.join("");
2076
+ }
2077
+ function SidebarSeatComponent(props) {
2078
+ const value = useScopeValue(props.scope);
2079
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2080
+ SidebarBalance,
2081
+ {
2082
+ wide: props.seat.wide,
2083
+ t: props.seat.t,
2084
+ config: {
2085
+ displayCurrency: readDisplayCurrency(value),
2086
+ manualRefreshCooldownSeconds: readCooldown(value),
2087
+ clientPollSeconds: readPollSeconds(value),
2088
+ configSignature: readSignature(value),
2089
+ warnThresholdOf: (currency) => readWarnThreshold(value, currency)
2090
+ }
2091
+ }
2092
+ );
2093
+ }
2094
+ function SettingsSeatComponent(props) {
2095
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BalanceSettingsCard, { t: props.seat.t, scope: props.scope });
2096
+ }
2097
+ function apply(ctx) {
2098
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ds-balance: dictionaries");
2099
+ const scope = adaptScope(ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE }));
2100
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register(
2101
+ { name: "settings.plugin.item", key: SETTINGS_NAMESPACE, locale: NS },
2102
+ (seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SettingsSeatComponent, { seat, scope })
2103
+ ));
2104
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
2105
+ { name: "sidebar.footer.action", id: SETTINGS_NAMESPACE, order: 0, locale: NS },
2106
+ (seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SidebarSeatComponent, { seat, scope })
2107
+ ));
2108
+ }
2109
+ 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_detailsSummary {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n list-style: none;\n}\n.fields_detailsSummary::-webkit-details-marker {\n display: none;\n}\n.fields_detailsSummary::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor;\n transform: translate(-0.5px, -1px) rotate(-45deg);\n transition: transform 120ms ease;\n}\n.fields_details[open] > .fields_detailsSummary::before {\n transform: translate(-0.5px, -1px) rotate(45deg);\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_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n opacity: 0.6;\n cursor: default;\n}\n.fields_inputInvalid {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_inputInvalid:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputInvalid:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\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_actionButton {\n appearance: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.fields_actionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.fields_actionButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.fields_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\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_card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.BalanceSettingsCard_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.BalanceSettingsCard_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.BalanceSettingsCard_headText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.BalanceSettingsCard_name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.BalanceSettingsCard_description {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_chevronWrap {\n flex: none;\n display: inline-flex;\n align-items: center;\n}\n.BalanceSettingsCard_chevron {\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.BalanceSettingsCard_chevronOpen {\n transform: rotate(180deg);\n}\n.BalanceSettingsCard_pending {\n flex: none;\n}\n.BalanceSettingsCard_body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding-bottom: 0;\n}\n.BalanceSettingsCard_readOnly {\n margin: 12px 0 0;\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 justify-content: flex-end;\n gap: 8px;\n padding: 12px 0;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\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.BalanceSettingsCard_discard,\n.BalanceSettingsCard_save {\n appearance: none;\n border: 0.5px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.BalanceSettingsCard_discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.BalanceSettingsCard_discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.BalanceSettingsCard_discard:disabled,\n.BalanceSettingsCard_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalanceSettingsCard_discard:focus-visible,\n.BalanceSettingsCard_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\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 background: var(--dsw-specific-menu);\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_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}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\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_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n}\n.PercentRing_cross {\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\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: 50%;\n corner-shape: round;\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}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
2110
+ var __dsbTagId = "dsh-ds-balance/client.css";
2111
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + __dsbTagId + '"]') === null) {
2112
+ var __dsbTag = document.createElement('style');
2113
+ __dsbTag.dataset.plugin = "dsh-ds-balance";
2114
+ __dsbTag.dataset.pluginCss = __dsbTagId;
2115
+ __dsbTag.textContent = __dsbCss;
2116
+ document.head.appendChild(__dsbTag);
2117
+ }
2118
+ return module.exports;
2119
+ }
2120
+ });