@linxin666/dsh-usage 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -54,9 +54,9 @@ window.__ModuleLoader__.load({
54
54
  "usage.refresh": "刷新",
55
55
  "usage.refreshing": "刷新中…",
56
56
  "usage.updated": "更新于 {time}",
57
- "usage.loading": "正在加载用量数据…",
57
+ "usage.loading": "正在读取用量…",
58
58
  "usage.error": "加载失败:{error}",
59
- "usage.disabled": "插件已停用:勾选下方「启用插件」即可恢复统计。",
59
+ "usage.disabled": "已停用,打开开关继续统计。",
60
60
  "usage.current": "当前",
61
61
  "usage.today": "今日用量",
62
62
  "usage.today.cost": "今日消费(估算)",
@@ -80,28 +80,26 @@ window.__ModuleLoader__.load({
80
80
  "usage.tokens.cacheRead": "缓存读",
81
81
  "usage.tokens.cacheWrite": "缓存写",
82
82
  "usage.trend": "近 30 天",
83
- "usage.noData": "暂无用量数据(统计自插件启用起)",
83
+ "usage.noData": "还没有用量,插件启用后开始统计。",
84
84
  "usage.balance": "余额",
85
- "usage.balance.unsupported": "暂不支持余额查询",
85
+ "usage.balance.unsupported": "不支持查询余额",
86
86
  "usage.balance.noCredential": "未配置凭据",
87
- "usage.balance.noneConfigured": "没有已配置的提供方",
88
- "usage.oauth": "OAuth 凭据,不做余额查询",
87
+ "usage.balance.noneConfigured": "还没有配置提供方",
88
+ "usage.oauth": "OAuth 凭据,不查余额",
89
89
  "usage.plan.reset": "{date} 重置",
90
- "usage.plan.noPlan": "未检测到套餐数据",
91
- "usage.plan.noneConfigured": "没有已配置的套餐类 provider(如 Kimi、GLM、OpenCode Go、MiniMax、Codex 订阅)",
90
+ "usage.plan.noPlan": "没有读到套餐数据",
91
+ "usage.plan.noneConfigured": "没有配置套餐类提供方,例如 Kimi、GLM、OpenCode Go、MiniMax、Codex 订阅。",
92
92
  "usage.plan.windows.5h": "5 小时",
93
93
  "usage.plan.windows.week": "每周",
94
94
  "usage.plan.windows.month": "每月",
95
95
  "usage.provider.error": "查询失败:{error}",
96
96
  "usage.errorListSeparator": ";",
97
97
  "usage.config.title": "设置",
98
- "usage.config.enabled": "启用插件",
99
98
  "usage.config.pollIntervalSec": "轮询间隔(秒)",
100
- "usage.config.spendCurrency": "消费显示货币",
101
- "usage.config.cnyPerUsd": "汇率(1 美元兑人民币)",
102
- "usage.config.saveFailed": "保存失败:设置未被接受。",
99
+ "usage.config.spendCurrency": "货币",
100
+ "usage.config.cnyPerUsd": "汇率",
103
101
  "usage.bank.title": "鲸元券",
104
- "usage.bank.hint": "所有模型每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。",
102
+ "usage.bank.hint": "每用掉 100 万 tokens 铸造 1 鲸元,可以保存或分享这张票券。",
105
103
  "usage.bank.noUsage": "暂无用量数据",
106
104
  "usage.bank.minted": "累计铸造 {minted} 鲸元({tokens} tokens)",
107
105
  "usage.bank.spend.observed": "官方余额实测花费 {cost}(自 {since} 起)",
@@ -115,7 +113,20 @@ window.__ModuleLoader__.load({
115
113
  "usage.foot.noData": "今日暂无用量",
116
114
  "usage.foot.open": "打开使用统计设置",
117
115
  "usage.foot.collapse": "收起用量卡片",
118
- "usage.foot.expand": "展开用量卡片"
116
+ "usage.foot.expand": "展开用量卡片",
117
+ "usage.description": "各提供方的用量、余额与套餐额度。",
118
+ "usage.config.pollIntervalHint": "多久查一次余额与套餐。",
119
+ "usage.config.spendCurrencyHint": "消费金额用这种货币显示。",
120
+ "usage.config.cnyPerUsdHint": "1 美元折合多少人民币。",
121
+ "settings.collapse": "收起",
122
+ "settings.expand": "展开",
123
+ "settings.notExposed": "这个版本的 DSH 没有开放使用统计的设置。",
124
+ "settings.readOnly": "设置只读。",
125
+ "settings.saveFailed": "没有保存成功。",
126
+ "settings.inherit": "默认",
127
+ "settings.overridden": "已修改",
128
+ "settings.reset": "恢复默认",
129
+ "settings.invalidNumber": "填一个数字,留空用默认值。"
119
130
  };
120
131
  /** English mirror; every zh key present. */
121
132
  const en = {
@@ -126,9 +137,9 @@ window.__ModuleLoader__.load({
126
137
  "usage.refresh": "Refresh",
127
138
  "usage.refreshing": "Refreshing…",
128
139
  "usage.updated": "Updated {time}",
129
- "usage.loading": "Loading usage data…",
140
+ "usage.loading": "Reading usage…",
130
141
  "usage.error": "Failed to load: {error}",
131
- "usage.disabled": "Plugin disabled. Tick \"Enable plugin\" below to resume statistics.",
142
+ "usage.disabled": "Turned off; flip the switch to resume.",
132
143
  "usage.current": "Current",
133
144
  "usage.today": "Today",
134
145
  "usage.today.cost": "Today spend (estimated)",
@@ -152,28 +163,26 @@ window.__ModuleLoader__.load({
152
163
  "usage.tokens.cacheRead": "Cache read",
153
164
  "usage.tokens.cacheWrite": "Cache write",
154
165
  "usage.trend": "Last 30 days",
155
- "usage.noData": "No usage data yet (counting starts when the plugin is enabled)",
166
+ "usage.noData": "No usage yet; counting starts once the plugin is on.",
156
167
  "usage.balance": "Balance",
157
- "usage.balance.unsupported": "Balance query not supported",
168
+ "usage.balance.unsupported": "No balance query",
158
169
  "usage.balance.noCredential": "No credential configured",
159
170
  "usage.balance.noneConfigured": "No providers configured",
160
171
  "usage.oauth": "OAuth credential, no balance query",
161
172
  "usage.plan.reset": "resets {date}",
162
- "usage.plan.noPlan": "No plan data detected",
163
- "usage.plan.noneConfigured": "No plan-capable provider configured (such as Kimi, GLM, OpenCode Go, MiniMax, Codex subscription)",
173
+ "usage.plan.noPlan": "No plan data",
174
+ "usage.plan.noneConfigured": "No plan provider configured, such as Kimi, GLM, OpenCode Go, MiniMax or a Codex subscription.",
164
175
  "usage.plan.windows.5h": "5 hours",
165
176
  "usage.plan.windows.week": "Weekly",
166
177
  "usage.plan.windows.month": "Monthly",
167
178
  "usage.provider.error": "Query failed: {error}",
168
179
  "usage.errorListSeparator": "; ",
169
180
  "usage.config.title": "Settings",
170
- "usage.config.enabled": "Enable plugin",
171
181
  "usage.config.pollIntervalSec": "Poll interval (seconds)",
172
- "usage.config.spendCurrency": "Spend display currency",
173
- "usage.config.cnyPerUsd": "Exchange rate (CNY per USD)",
174
- "usage.config.saveFailed": "Save failed: the deployment did not accept the setting.",
182
+ "usage.config.spendCurrency": "Currency",
183
+ "usage.config.cnyPerUsd": "Exchange rate",
175
184
  "usage.bank.title": "Whale-yuan voucher",
176
- "usage.bank.hint": "Every 1,000,000 tokens spent across all models mint one whale yuan; save or share the note.",
185
+ "usage.bank.hint": "Every million tokens mints one whale yuan; save or share the note.",
177
186
  "usage.bank.noUsage": "No usage data yet",
178
187
  "usage.bank.minted": "Minted {minted} whale yuan ({tokens} tokens)",
179
188
  "usage.bank.spend.observed": "Spent {cost} observed on the official balance (watching since {since})",
@@ -187,7 +196,20 @@ window.__ModuleLoader__.load({
187
196
  "usage.foot.noData": "No usage yet today",
188
197
  "usage.foot.open": "Open usage statistics settings",
189
198
  "usage.foot.collapse": "Collapse the usage card",
190
- "usage.foot.expand": "Expand the usage card"
199
+ "usage.foot.expand": "Expand the usage card",
200
+ "usage.description": "Usage, balances and plan quotas for each provider.",
201
+ "usage.config.pollIntervalHint": "How often balances and plans are read.",
202
+ "usage.config.spendCurrencyHint": "Spend figures show in this currency.",
203
+ "usage.config.cnyPerUsdHint": "Yuan per US dollar.",
204
+ "settings.collapse": "Collapse",
205
+ "settings.expand": "Expand",
206
+ "settings.notExposed": "This DSH version does not open the usage settings.",
207
+ "settings.readOnly": "Settings are read-only.",
208
+ "settings.saveFailed": "Not saved.",
209
+ "settings.inherit": "Default",
210
+ "settings.overridden": "Changed",
211
+ "settings.reset": "Reset",
212
+ "settings.invalidNumber": "Enter a number, or leave it empty for the default."
191
213
  };
192
214
  /**
193
215
  * Active dictionary, picked by the document language at call time. The
@@ -205,13 +227,13 @@ window.__ModuleLoader__.load({
205
227
  }
206
228
  //#endregion
207
229
  //#region \0dsh-css:packages/dsh-usage/src/client/usage.module.css.mjs
208
- const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_dayPicker{align-items:center;gap:8px;display:flex}.cvtkAW_dayPickerLabel{opacity:.6;font-size:11px}.cvtkAW_daySelect{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;font-variant-numeric:tabular-nums;background:0 0;border-radius:6px;padding:3px 8px;font-size:12px}.cvtkAW_daySelect:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_daySelect option,.cvtkAW_settingItem select option{background-color:var(--dsw-alias-bg-multi-select,Canvas);color:var(--dsw-alias-label-primary,CanvasText)}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{flex-direction:column;gap:12px;display:flex}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_voucherPreview canvas{border-radius:8px;width:100%;height:auto;display:block}.cvtkAW_buttonRow{gap:8px;display:flex}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footCard{box-sizing:border-box;background:color-mix(in srgb, currentColor 4%, transparent);width:100%;color:inherit;border:none;border-radius:12px;margin:2px 0 4px;transition:background-color .12s;position:relative}.cvtkAW_footCard:hover{background:var(--dsw-alias-interactive-bg-hover)}.cvtkAW_footCardCollapsed{background:0 0}.cvtkAW_footMain{appearance:none;box-sizing:border-box;width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:12px;flex-direction:column;gap:4px;padding:8px;display:flex}.cvtkAW_footMain:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footToggle{appearance:none;width:20px;height:20px;color:inherit;cursor:pointer;opacity:.55;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background-color .12s;display:inline-flex;position:absolute;top:6px;right:6px}.cvtkAW_footCard:hover .cvtkAW_footToggle{opacity:.8}.cvtkAW_footToggle:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}.cvtkAW_footToggle:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footToggle svg{display:block}.cvtkAW_footHead{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.cvtkAW_footTitle{opacity:.65;white-space:nowrap;align-items:center;gap:6px;min-width:0;font-size:12px;display:inline-flex}.cvtkAW_footTitle svg{flex:none;display:block}.cvtkAW_footValue{font-variant-numeric:tabular-nums;white-space:nowrap;margin-right:18px;font-size:15px;font-weight:600}.cvtkAW_footLine{opacity:.7;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.cvtkAW_footMeta{opacity:.5;font-variant-numeric:tabular-nums;font-size:10px}.cvtkAW_footCardCollapsed .cvtkAW_footMain{min-height:36px;padding:7px 8px}.cvtkAW_footStrip{white-space:nowrap;align-items:center;gap:6px;padding-right:20px;font-size:12px;display:flex;overflow:hidden}.cvtkAW_footStrip svg{opacity:.65;flex:none;display:block}.cvtkAW_footStripProvider{text-overflow:ellipsis;flex:none;max-width:38%;font-weight:500;overflow:hidden}.cvtkAW_footStripLabel{opacity:.65;text-overflow:ellipsis;overflow:hidden}.cvtkAW_footStripValue{font-variant-numeric:tabular-nums;margin-left:auto;font-weight:600}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_footCard,[data-sidebar-collapsed] .cvtkAW_footCard{display:none}.cvtkAW_peakBadge,.cvtkAW_peakLine{font-variant-numeric:tabular-nums;white-space:nowrap;align-items:center;gap:4px;font-size:11px;display:inline-flex}.cvtkAW_peakBadge{flex:none}.cvtkAW_peakLine{gap:6px}.cvtkAW_peakBadge[data-peak=on],.cvtkAW_peakLine[data-peak=on]{color:var(--dsw-alias-state-error-primary,#b42318)}.cvtkAW_peakBadge[data-peak=off],.cvtkAW_peakLine[data-peak=off]{color:var(--dsw-alias-state-success-primary,#067647)}.cvtkAW_peakDot{background:currentColor;border-radius:50%;flex:none;width:6px;height:6px}.cvtkAW_footPeak{white-space:nowrap;align-items:center;gap:5px;font-size:10px;display:flex;overflow:hidden}.cvtkAW_footPeakClock{opacity:.7;font-variant-numeric:tabular-nums}";
209
- const tagId = "@linxin666/dsh-usage/packages/dsh-usage/src/client/usage.module.css";
210
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
230
+ const css$1 = ".cvtkAW_section{min-width:0;color:inherit;flex-direction:column;gap:16px;display:flex;container:cvtkAW_usage-section/inline-size}.cvtkAW_header{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 16px;display:flex}.cvtkAW_headerActions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_sectionList{margin:0;padding:0;list-style:none}.cvtkAW_tabs{align-self:stretch;display:flex}.cvtkAW_tabs>.cvtkAW_tab{text-align:center;flex:1 1 0;min-width:max-content}.cvtkAW_settingsGroup{flex-direction:column;margin-top:16px;display:flex}.cvtkAW_groupTitle{opacity:.6;margin:0 0 2px;font-size:12px;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_dayPicker{align-items:center;gap:8px;display:flex}.cvtkAW_dayPickerLabel{opacity:.6;font-size:11px}.cvtkAW_daySelect{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;font-variant-numeric:tabular-nums;background:0 0;border-radius:6px;padding:3px 8px;font-size:12px}.cvtkAW_daySelect:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_daySelect option{background-color:var(--dsw-alias-bg-multi-select,Canvas);color:var(--dsw-alias-label-primary,CanvasText)}.cvtkAW_statRow{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;display:grid}.cvtkAW_stat{background:color-mix(in srgb, currentColor 4%, transparent);border-radius:8px;flex-direction:column;gap:3px;min-width:0;padding:9px 10px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 20px;display:grid}.cvtkAW_usageSummaryGrid{grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:16px;display:grid}.cvtkAW_trendAxis{grid-column:1/-1}@container cvtkAW_usage-section (width<=680px){.cvtkAW_usageSummaryGrid,.cvtkAW_chart{grid-template-columns:minmax(0,1fr)}.cvtkAW_statRow{grid-template-columns:repeat(2,minmax(0,1fr))}}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_voucherPreview canvas{border-radius:8px;width:100%;height:auto;display:block}.cvtkAW_buttonRow{gap:8px;display:flex}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_footCard{box-sizing:border-box;background:color-mix(in srgb, currentColor 4%, transparent);width:100%;color:inherit;border:none;border-radius:12px;margin:2px 0 4px;transition:background-color .12s;position:relative}.cvtkAW_footCard:hover{background:var(--dsw-alias-interactive-bg-hover)}.cvtkAW_footCardCollapsed{background:0 0}.cvtkAW_footMain{appearance:none;box-sizing:border-box;width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:12px;flex-direction:column;gap:4px;padding:8px;display:flex}.cvtkAW_footMain:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footToggle{appearance:none;width:20px;height:20px;color:inherit;cursor:pointer;opacity:.55;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background-color .12s;display:inline-flex;position:absolute;top:6px;right:6px}.cvtkAW_footCard:hover .cvtkAW_footToggle{opacity:.8}.cvtkAW_footToggle:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}.cvtkAW_footToggle:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footToggle svg{display:block}.cvtkAW_footHead{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.cvtkAW_footTitle{opacity:.65;white-space:nowrap;align-items:center;gap:6px;min-width:0;font-size:12px;display:inline-flex}.cvtkAW_footTitle svg{flex:none;display:block}.cvtkAW_footValue{font-variant-numeric:tabular-nums;white-space:nowrap;margin-right:18px;font-size:15px;font-weight:600}.cvtkAW_footLine{opacity:.7;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.cvtkAW_footMeta{opacity:.5;font-variant-numeric:tabular-nums;font-size:10px}.cvtkAW_footCardCollapsed .cvtkAW_footMain{min-height:36px;padding:7px 8px}.cvtkAW_footStrip{white-space:nowrap;align-items:center;gap:6px;padding-right:20px;font-size:12px;display:flex;overflow:hidden}.cvtkAW_footStrip svg{opacity:.65;flex:none;display:block}.cvtkAW_footStripProvider{text-overflow:ellipsis;flex:none;max-width:38%;font-weight:500;overflow:hidden}.cvtkAW_footStripLabel{opacity:.65;text-overflow:ellipsis;overflow:hidden}.cvtkAW_footStripValue{font-variant-numeric:tabular-nums;margin-left:auto;font-weight:600}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_footCard,[data-sidebar-collapsed] .cvtkAW_footCard{display:none}.cvtkAW_peakBadge,.cvtkAW_peakLine{font-variant-numeric:tabular-nums;white-space:nowrap;align-items:center;gap:4px;font-size:11px;display:inline-flex}.cvtkAW_peakBadge{flex:none}.cvtkAW_peakLine{gap:6px}.cvtkAW_peakBadge[data-peak=on],.cvtkAW_peakLine[data-peak=on]{color:var(--dsw-alias-state-error-primary,#b42318)}.cvtkAW_peakBadge[data-peak=off],.cvtkAW_peakLine[data-peak=off]{color:var(--dsw-alias-state-success-primary,#067647)}.cvtkAW_peakDot{background:currentColor;border-radius:50%;flex:none;width:6px;height:6px}.cvtkAW_footPeak{white-space:nowrap;align-items:center;gap:5px;font-size:10px;display:flex;overflow:hidden}.cvtkAW_footPeakClock{opacity:.7;font-variant-numeric:tabular-nums}";
231
+ const tagId$1 = "@linxin666/dsh-usage/packages/dsh-usage/src/client/usage.module.css";
232
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
211
233
  const tag = document.createElement("style");
212
234
  tag.dataset.plugin = "@linxin666/dsh-usage";
213
- tag.dataset.pluginCss = tagId;
214
- tag.textContent = css;
235
+ tag.dataset.pluginCss = tagId$1;
236
+ tag.textContent = css$1;
215
237
  document.head.appendChild(tag);
216
238
  }
217
239
  var usage_module_css_default = {
@@ -253,7 +275,9 @@ window.__ModuleLoader__.load({
253
275
  "footTitle": "cvtkAW_footTitle",
254
276
  "footToggle": "cvtkAW_footToggle",
255
277
  "footValue": "cvtkAW_footValue",
278
+ "groupTitle": "cvtkAW_groupTitle",
256
279
  "header": "cvtkAW_header",
280
+ "headerActions": "cvtkAW_headerActions",
257
281
  "muted": "cvtkAW_muted",
258
282
  "peakBadge": "cvtkAW_peakBadge",
259
283
  "peakDot": "cvtkAW_peakDot",
@@ -268,21 +292,87 @@ window.__ModuleLoader__.load({
268
292
  "refreshBtn": "cvtkAW_refreshBtn",
269
293
  "resetLine": "cvtkAW_resetLine",
270
294
  "section": "cvtkAW_section",
271
- "settingItem": "cvtkAW_settingItem",
272
- "settingsGrid": "cvtkAW_settingsGrid",
295
+ "sectionList": "cvtkAW_sectionList",
296
+ "settingsGroup": "cvtkAW_settingsGroup",
273
297
  "stat": "cvtkAW_stat",
274
298
  "statLabel": "cvtkAW_statLabel",
275
299
  "statRow": "cvtkAW_statRow",
276
300
  "statValue": "cvtkAW_statValue",
277
301
  "tab": "cvtkAW_tab",
278
- "tabActive": "cvtkAW_tabActive",
279
302
  "tabs": "cvtkAW_tabs",
280
303
  "trendAxis": "cvtkAW_trendAxis",
304
+ "usage-section": "cvtkAW_usage-section",
305
+ "usageSummaryGrid": "cvtkAW_usageSummaryGrid",
281
306
  "voucherPreview": "cvtkAW_voucherPreview",
282
307
  "windowLabel": "cvtkAW_windowLabel",
283
308
  "windowRow": "cvtkAW_windowRow"
284
309
  };
285
310
  //#endregion
311
+ //#region \0dsh-css:packages/dsh-usage/src/client/settings-card.module.css.mjs
312
+ const css = ".Fys0GG_card{--settings-card-radius:12px;--settings-card-header-pad:16px;--settings-card-header-gap:13px;--settings-card-title-size:15px;--settings-card-description-size:13px;--settings-card-body-inset:16px;--settings-card-motion:.18s;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:var(--settings-card-radius);list-style:none;transition:border-color .16s,background .16s}.Fys0GG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Fys0GG_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Fys0GG_cardNested{--settings-card-radius:10px;--settings-card-header-pad:10px 12px;--settings-card-header-gap:10px;--settings-card-title-size:13px;--settings-card-description-size:12px;--settings-card-body-inset:12px}.Fys0GG_cardNested .Fys0GG_description{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.Fys0GG_header{appearance:none;box-sizing:border-box;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;border-radius:var(--settings-card-radius);align-items:center;gap:var(--settings-card-header-gap);padding:var(--settings-card-header-pad);background:0 0;border:0;display:flex}.Fys0GG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Fys0GG_headerStatic{box-sizing:border-box;border-radius:var(--settings-card-radius);align-items:center;gap:var(--settings-card-header-gap);width:100%;padding:var(--settings-card-header-pad);display:flex}.Fys0GG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Fys0GG_mark{background:var(--dsw-alias-bg-module-platform);width:34px;height:34px;color:var(--dsw-alias-label-secondary);border-radius:10px;flex:none;place-items:center;transition:color .16s;display:grid}.Fys0GG_header:hover .Fys0GG_mark,.Fys0GG_cardOpen .Fys0GG_mark{color:var(--dsw-alias-label-primary)}.Fys0GG_name{color:var(--dsw-alias-label-primary);font-size:var(--settings-card-title-size);font-weight:600;line-height:1.4}.Fys0GG_description{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.Fys0GG_headerAction{flex:none;align-items:center;display:inline-flex}.Fys0GG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;width:16px;height:16px;transition:transform .16s,color .16s}.Fys0GG_header:hover .Fys0GG_chevron{color:var(--dsw-alias-label-secondary)}.Fys0GG_chevronOpen{transform:rotate(180deg)}.Fys0GG_disclosure{opacity:0;transition:grid-template-rows var(--settings-card-motion) ease, opacity var(--settings-card-motion) ease;grid-template-rows:0fr;display:grid}.Fys0GG_disclosure[data-state=collapsed],.Fys0GG_disclosure[data-state=entering],.Fys0GG_disclosure[data-state=closing]{overflow:hidden}.Fys0GG_disclosure[data-state=entering],.Fys0GG_disclosure[data-state=shown]{opacity:1;grid-template-rows:1fr}.Fys0GG_disclosure>.Fys0GG_body{min-height:0}.Fys0GG_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 var(--settings-card-body-inset);padding-bottom:8px;container:Fys0GG_settings-card/inline-size}.Fys0GG_readOnly{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:12px;line-height:1.5}.Fys0GG_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Fys0GG_failed{color:var(--dsw-alias-state-error-primary,#b42318);overflow-wrap:anywhere;margin:8px 0 4px;font-size:12px;line-height:18px}.Fys0GG_field{grid-template-columns:minmax(0,1fr) fit-content(50%);grid-template-areas:\"Fys0GG_head Fys0GG_control\"\"Fys0GG_hint Fys0GG_control\";align-items:center;gap:4px 24px;padding:16px 0;display:grid}.Fys0GG_field>.Fys0GG_head{grid-area:Fys0GG_head}.Fys0GG_field>:is(.Fys0GG_hint,.Fys0GG_invalid){grid-area:Fys0GG_hint}.Fys0GG_field>:not(.Fys0GG_head,.Fys0GG_hint,.Fys0GG_invalid){grid-area:Fys0GG_control;justify-self:end;max-width:100%}.Fys0GG_field+.Fys0GG_field{border-top:1px solid var(--dsw-alias-border-l2)}.Fys0GG_head{align-items:center;gap:8px;display:flex}.Fys0GG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:14px;font-weight:400;line-height:22px}.Fys0GG_badges{align-items:center;gap:8px;display:inline-flex}.Fys0GG_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Fys0GG_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Fys0GG_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Fys0GG_reset:disabled{cursor:default}.Fys0GG_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Fys0GG_segmentGroup{background:color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent);border-radius:8px;flex-wrap:wrap;align-items:center;gap:2px;padding:2px;display:inline-flex;position:relative}.Fys0GG_segment{appearance:none;color:var(--dsw-alias-label-secondary);font:inherit;white-space:nowrap;cursor:pointer;background:0 0;border:0;border-radius:7px;margin:0;padding:3px 12px;font-size:13px;font-weight:500;line-height:18px;transition:background-color .12s,color .12s}.Fys0GG_segment:hover:not(:disabled):not([data-active]){color:var(--dsw-alias-label-primary)}.Fys0GG_segmentGroup:not([data-pill])>.Fys0GG_segment[data-active],.Fys0GG_segmentGroup[data-pill]:before{background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-layer-3));box-shadow:0 1px 2px #14141314}body[data-ds-dark-theme] .Fys0GG_segmentGroup:not([data-pill])>.Fys0GG_segment[data-active],body[data-ds-dark-theme] .Fys0GG_segmentGroup[data-pill]:before{box-shadow:none;background:#ffffff24}.Fys0GG_segment[data-active]{color:var(--dsw-alias-label-primary)}.Fys0GG_segmentGroup[data-pill]:before{content:\"\";width:var(--segment-pill-w,0px);height:var(--segment-pill-h,0px);transform:translate(var(--segment-pill-x,0px), var(--segment-pill-y,0px));pointer-events:none;border-radius:7px;transition:transform .24s cubic-bezier(.22,.61,.36,1),width .24s cubic-bezier(.22,.61,.36,1),height .24s cubic-bezier(.22,.61,.36,1);position:absolute;top:0;left:0}.Fys0GG_segmentGroup>*{z-index:1;position:relative}.Fys0GG_segment:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Fys0GG_segment:disabled{cursor:not-allowed;opacity:.45}.Fys0GG_segmentGroup[data-pill]:has(>.Fys0GG_segment:disabled):before{opacity:.45}.Fys0GG_input,.Fys0GG_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Fys0GG_input:focus-visible,.Fys0GG_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Fys0GG_input:disabled,.Fys0GG_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Fys0GG_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary,#b42318);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Fys0GG_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary,#b42318);outline-offset:1px;border-color:var(--dsw-alias-state-error-primary,#b42318)}.Fys0GG_field>:is(.Fys0GG_input,.Fys0GG_inputInvalid,.Fys0GG_select,.Fys0GG_selectWrap,.Fys0GG_rangeAnchor){box-sizing:border-box;width:220px}.Fys0GG_rangeAnchor>:is(.Fys0GG_input,.Fys0GG_inputInvalid){box-sizing:border-box;width:100%}@container Fys0GG_settings-card (width<=440px){.Fys0GG_field{grid-template-columns:minmax(0,1fr);grid-template-areas:\"Fys0GG_head\"\"Fys0GG_hint\"\"Fys0GG_control\";row-gap:8px}.Fys0GG_field>:not(.Fys0GG_head,.Fys0GG_hint,.Fys0GG_invalid){justify-self:start}.Fys0GG_field>:is(.Fys0GG_input,.Fys0GG_inputInvalid,.Fys0GG_select,.Fys0GG_selectWrap,.Fys0GG_rangeAnchor){justify-self:stretch;width:auto}}.Fys0GG_selectWrap{position:relative}.Fys0GG_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Fys0GG_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Fys0GG_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Fys0GG_selectChevronOpen{transform:rotate(180deg)}.Fys0GG_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Fys0GG_selectPopupOpen{opacity:1;transform:none}.Fys0GG_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Fys0GG_rangeAnchor{display:inline-flex;position:relative}.Fys0GG_rangeBubble{z-index:40;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);width:220px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;visibility:hidden;border-radius:10px;padding:10px 12px 6px;transition:opacity .12s,transform .12s,visibility 0s linear .12s;position:absolute;right:0}.Fys0GG_rangeBubble[data-side=above]{bottom:calc(100% + 6px);transform:translateY(4px)}.Fys0GG_rangeBubble[data-side=below]{top:calc(100% + 6px);transform:translateY(-4px)}.Fys0GG_rangeBubble[data-open]{opacity:1;visibility:visible;transition:opacity .12s,transform .12s,visibility;transform:none}.Fys0GG_rangeSlider{appearance:none;cursor:pointer;background:0 0;width:100%;height:18px;margin:0;display:block}.Fys0GG_rangeSlider::-webkit-slider-runnable-track{background:linear-gradient(to right, var(--dsw-alias-brand-primary) var(--range-fill,0%), color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent) var(--range-fill,0%));border-radius:999px;height:4px}.Fys0GG_rangeSlider::-moz-range-track{background:color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent);border-radius:999px;height:4px}.Fys0GG_rangeSlider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:999px;height:4px}.Fys0GG_rangeSlider::-webkit-slider-thumb{appearance:none;background:#fff;border:0;border-radius:50%;width:14px;height:14px;margin-top:-5px;transition:transform .12s;box-shadow:0 0 0 1px #0000001f,0 1px 3px #00000040}.Fys0GG_rangeSlider::-moz-range-thumb{background:#fff;border:0;border-radius:50%;width:14px;height:14px;box-shadow:0 0 0 1px #0000001f,0 1px 3px #00000040}.Fys0GG_rangeSlider:active::-webkit-slider-thumb{transform:scale(1.15)}.Fys0GG_rangeSlider:focus-visible{outline:none}.Fys0GG_rangeSlider:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary)}.Fys0GG_rangeSlider:disabled{cursor:not-allowed}.Fys0GG_rangeEnds{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;justify-content:space-between;margin-top:2px;font-size:11px;line-height:16px;display:flex}.Fys0GG_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Fys0GG_selectOption:hover,.Fys0GG_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Fys0GG_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent);font-weight:500}.Fys0GG_invalid{color:var(--dsw-alias-state-error-primary,#b42318);margin:0;font-size:12px;line-height:18px}.Fys0GG_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}.Fys0GG_toggle{appearance:none;cursor:pointer;font:inherit;background:0 0;border:0;border-radius:999px;align-items:center;margin:0;padding:0;display:inline-flex}.Fys0GG_toggleTrack{background:var(--dsw-alias-label-dimmed);border-radius:999px;flex:none;width:36px;height:20px;transition:background-color .18s;position:relative}.Fys0GG_toggleThumb{--toggle-travel:16px;--toggle-overshoot:1px;background:var(--dsw-alias-bg-layer-3);border-radius:50%;width:16px;height:16px;transition:translate .18s;position:absolute;top:2px;left:2px;translate:0;box-shadow:0 1px 2px #1414132e}.Fys0GG_toggle[aria-checked=true] .Fys0GG_toggleTrack{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-label-primary))}.Fys0GG_toggle[aria-checked=true] .Fys0GG_toggleThumb{translate:var(--toggle-travel) 0;background:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-bg-layer-3))}.Fys0GG_toggle[data-moved][aria-checked=true] .Fys0GG_toggleThumb{animation:.35s cubic-bezier(.34,1.45,.64,1) both Fys0GG_toggle-on}.Fys0GG_toggle[data-moved][aria-checked=false] .Fys0GG_toggleThumb{animation:.35s cubic-bezier(.34,1.45,.64,1) both Fys0GG_toggle-off}@keyframes Fys0GG_toggle-on{0%{translate:0}55%{translate:calc(var(--toggle-travel) + var(--toggle-overshoot)) 0}80%,to{translate:var(--toggle-travel) 0}}@keyframes Fys0GG_toggle-off{0%{translate:var(--toggle-travel) 0}55%{translate:calc(0px - var(--toggle-overshoot)) 0}80%,to{translate:0}}.Fys0GG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Fys0GG_toggle:disabled{opacity:.45;cursor:not-allowed}@media (prefers-reduced-motion:reduce){.Fys0GG_card,.Fys0GG_header,.Fys0GG_mark,.Fys0GG_chevron,.Fys0GG_chevronOpen,.Fys0GG_rangeBubble,.Fys0GG_selectChevron,.Fys0GG_selectChevronOpen,.Fys0GG_selectPopup,.Fys0GG_toggleTrack,.Fys0GG_toggleThumb{transition:none}.Fys0GG_toggle[data-moved] .Fys0GG_toggleThumb{animation:none}.Fys0GG_disclosure{transition-property:opacity}}";
313
+ const tagId = "@linxin666/dsh-usage/packages/dsh-usage/src/client/settings-card.module.css";
314
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
315
+ const tag = document.createElement("style");
316
+ tag.dataset.plugin = "@linxin666/dsh-usage";
317
+ tag.dataset.pluginCss = tagId;
318
+ tag.textContent = css;
319
+ document.head.appendChild(tag);
320
+ }
321
+ var settings_card_module_css_default = {
322
+ "badge": "Fys0GG_badge",
323
+ "badges": "Fys0GG_badges",
324
+ "body": "Fys0GG_body",
325
+ "card": "Fys0GG_card",
326
+ "cardNested": "Fys0GG_cardNested",
327
+ "cardOpen": "Fys0GG_cardOpen",
328
+ "chevron": "Fys0GG_chevron",
329
+ "chevronOpen": "Fys0GG_chevronOpen",
330
+ "control": "Fys0GG_control",
331
+ "description": "Fys0GG_description",
332
+ "disclosure": "Fys0GG_disclosure",
333
+ "failed": "Fys0GG_failed",
334
+ "field": "Fys0GG_field",
335
+ "head": "Fys0GG_head",
336
+ "headText": "Fys0GG_headText",
337
+ "header": "Fys0GG_header",
338
+ "headerAction": "Fys0GG_headerAction",
339
+ "headerStatic": "Fys0GG_headerStatic",
340
+ "hint": "Fys0GG_hint",
341
+ "input": "Fys0GG_input",
342
+ "inputInvalid": "Fys0GG_inputInvalid",
343
+ "invalid": "Fys0GG_invalid",
344
+ "label": "Fys0GG_label",
345
+ "mark": "Fys0GG_mark",
346
+ "name": "Fys0GG_name",
347
+ "notExposed": "Fys0GG_notExposed",
348
+ "rangeAnchor": "Fys0GG_rangeAnchor",
349
+ "rangeBubble": "Fys0GG_rangeBubble",
350
+ "rangeEnds": "Fys0GG_rangeEnds",
351
+ "rangeSlider": "Fys0GG_rangeSlider",
352
+ "readOnly": "Fys0GG_readOnly",
353
+ "reset": "Fys0GG_reset",
354
+ "segment": "Fys0GG_segment",
355
+ "segmentGroup": "Fys0GG_segmentGroup",
356
+ "select": "Fys0GG_select",
357
+ "selectButton": "Fys0GG_selectButton",
358
+ "selectChevron": "Fys0GG_selectChevron",
359
+ "selectChevronOpen": "Fys0GG_selectChevronOpen",
360
+ "selectLabel": "Fys0GG_selectLabel",
361
+ "selectOption": "Fys0GG_selectOption",
362
+ "selectOptionActive": "Fys0GG_selectOptionActive",
363
+ "selectOptionSelected": "Fys0GG_selectOptionSelected",
364
+ "selectPopup": "Fys0GG_selectPopup",
365
+ "selectPopupClose": "Fys0GG_selectPopupClose",
366
+ "selectPopupOpen": "Fys0GG_selectPopupOpen",
367
+ "selectWrap": "Fys0GG_selectWrap",
368
+ "settings-card": "Fys0GG_settings-card",
369
+ "toggle": "Fys0GG_toggle",
370
+ "toggle-off": "Fys0GG_toggle-off",
371
+ "toggle-on": "Fys0GG_toggle-on",
372
+ "toggleThumb": "Fys0GG_toggleThumb",
373
+ "toggleTrack": "Fys0GG_toggleTrack"
374
+ };
375
+ //#endregion
286
376
  //#region src/client/PeakBadge.tsx
287
377
  /** HH:MM of one instant; an unparsable instant drops the time. */
288
378
  function formatClock(ms) {
@@ -311,6 +401,1303 @@ window.__ModuleLoader__.load({
311
401
  });
312
402
  }
313
403
  //#endregion
404
+ //#region src/client/SegmentGroup.tsx
405
+ /**
406
+ * The family's segmented control: a quiet track whose options share one
407
+ * highlight that slides to the pressed option, instead of each option fading
408
+ * its own background out and the next one in. The settings fields' small
409
+ * choices and the Web Extensions TAB strip both render through it, so the two
410
+ * read as one control.
411
+ *
412
+ * The caller builds the option buttons and marks the pressed one with
413
+ * `data-active`; the group measures that button after every render and writes
414
+ * its box as four custom properties, and the stylesheet draws the highlight
415
+ * there (`.segmentGroup[data-pill]::before`). Attribute and first placement
416
+ * land in the same task, so the highlight appears in place and only later
417
+ * changes slide. A ResizeObserver re-measures when the group or an option
418
+ * changes size: a longer locale, a font arriving, a container growing, a
419
+ * strip wrapping onto a second line.
420
+ */
421
+ /** The attribute marking a group whose highlight has been placed. */
422
+ const PILL_ATTR = "data-pill";
423
+ /** The pressed option, relative to the group. */
424
+ const ACTIVE_SELECTOR = ":scope > [data-active]";
425
+ const PILL_PROPERTIES = [
426
+ "--segment-pill-x",
427
+ "--segment-pill-y",
428
+ "--segment-pill-w",
429
+ "--segment-pill-h"
430
+ ];
431
+ /**
432
+ * Write the highlight's box from the pressed option's box, or take the
433
+ * highlight off when nothing is pressed or the group has no layout yet.
434
+ * The two boxes are read in viewport pixels and turned back into the group's
435
+ * own CSS pixels, so a dialog that opens through a scale transform still
436
+ * lands the highlight on its option.
437
+ */
438
+ function placePill(group) {
439
+ const item = group.querySelector(ACTIVE_SELECTOR);
440
+ const box = group.getBoundingClientRect();
441
+ if (item === null || group.offsetWidth === 0 || box.width === 0) {
442
+ group.removeAttribute(PILL_ATTR);
443
+ for (const property of PILL_PROPERTIES) group.style.removeProperty(property);
444
+ return;
445
+ }
446
+ const scale = box.width / group.offsetWidth;
447
+ const itemBox = item.getBoundingClientRect();
448
+ [
449
+ (itemBox.left - box.left) / scale - group.clientLeft,
450
+ (itemBox.top - box.top) / scale - group.clientTop,
451
+ itemBox.width / scale,
452
+ itemBox.height / scale
453
+ ].forEach((value, index) => {
454
+ group.style.setProperty(PILL_PROPERTIES[index], `${Math.round(value * 100) / 100}px`);
455
+ });
456
+ if (!group.hasAttribute(PILL_ATTR)) group.setAttribute(PILL_ATTR, "");
457
+ }
458
+ /**
459
+ * Render a segmented track around the caller's option buttons.
460
+ * @param props - the track's attributes (role, accessible name, key handler) and its options.
461
+ * @returns the track.
462
+ */
463
+ function SegmentGroup(props) {
464
+ const { children, className, ...rest } = props;
465
+ const group = (0, react.useRef)(null);
466
+ const count = react.Children.count(children);
467
+ (0, react.useLayoutEffect)(() => {
468
+ if (group.current !== null) placePill(group.current);
469
+ });
470
+ (0, react.useLayoutEffect)(() => {
471
+ const element = group.current;
472
+ if (element === null || typeof ResizeObserver === "undefined") return void 0;
473
+ const observer = new ResizeObserver(() => {
474
+ placePill(element);
475
+ });
476
+ observer.observe(element);
477
+ for (const item of Array.from(element.children)) observer.observe(item);
478
+ return () => {
479
+ observer.disconnect();
480
+ };
481
+ }, [count]);
482
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
483
+ ...rest,
484
+ ref: group,
485
+ className: className === void 0 ? settings_card_module_css_default.segmentGroup : `${settings_card_module_css_default.segmentGroup} ${className}`,
486
+ children
487
+ });
488
+ }
489
+ //#endregion
490
+ //#region src/client/RangeBubble.tsx
491
+ /**
492
+ * Coarse adjustment for a bounded number: resting the pointer on the control
493
+ * (or moving focus into it) raises a small bubble holding a slider across the
494
+ * field's whole range, so a large change is one drag instead of many presses
495
+ * or a retyped value. The control itself stays the precise entry.
496
+ *
497
+ * The bubble belongs to the anchor's subtree, so moving from the control onto
498
+ * the bubble never counts as leaving. It opens after a short rest and closes a
499
+ * moment after the pointer and focus have both gone, so crossing the control
500
+ * on the way elsewhere does not flash it. While a drag is in progress the
501
+ * bubble stays put even if the pointer strays off it.
502
+ *
503
+ * Dragging reports each value through `onInput`, so the field shows it live;
504
+ * releasing the thumb, or a key press on it, reports `onCommit` once. The
505
+ * bubble opens on the side of the control that has room inside the nearest
506
+ * scrolling ancestor, so a row near the top of a settings page does not hang
507
+ * its bubble under the page edge.
508
+ */
509
+ /** Rest on the control before the bubble opens. */
510
+ const OPEN_DELAY_MS = 220;
511
+ /** Grace after pointer and focus have left before the bubble closes. */
512
+ const CLOSE_DELAY_MS = 200;
513
+ /** Room the bubble needs between the control and the scroller's edge. */
514
+ const BUBBLE_ROOM_PX = 64;
515
+ /** The nearest ancestor that scrolls vertically, or the viewport's root. */
516
+ function scroller(node) {
517
+ for (let parent = node.parentElement; parent !== null; parent = parent.parentElement) {
518
+ const overflow = getComputedStyle(parent).overflowY;
519
+ if (overflow === "auto" || overflow === "scroll") return parent;
520
+ }
521
+ return node.ownerDocument.documentElement;
522
+ }
523
+ /**
524
+ * Render the control with its coarse-adjustment bubble.
525
+ * @param props - the range, the value and the two report callbacks.
526
+ * @returns the anchor holding the control and the bubble.
527
+ */
528
+ function RangeBubble(props) {
529
+ const { min, max, step, disabled = false } = props;
530
+ const [open, setOpen] = (0, react.useState)(false);
531
+ const [side, setSide] = (0, react.useState)("above");
532
+ const [live, setLive] = (0, react.useState)(null);
533
+ const anchorRef = (0, react.useRef)(null);
534
+ const timer = (0, react.useRef)(void 0);
535
+ const hovered = (0, react.useRef)(false);
536
+ const focused = (0, react.useRef)(false);
537
+ const dragging = (0, react.useRef)(false);
538
+ (0, react.useEffect)(() => () => {
539
+ clearTimeout(timer.current);
540
+ }, []);
541
+ (0, react.useEffect)(() => {
542
+ if (disabled) {
543
+ clearTimeout(timer.current);
544
+ setOpen(false);
545
+ }
546
+ }, [disabled]);
547
+ const show = () => {
548
+ const anchor = anchorRef.current;
549
+ if (anchor !== null) {
550
+ const room = anchor.getBoundingClientRect().top - scroller(anchor).getBoundingClientRect().top;
551
+ setSide(room >= BUBBLE_ROOM_PX ? "above" : "below");
552
+ }
553
+ setOpen(true);
554
+ };
555
+ const schedule = () => {
556
+ clearTimeout(timer.current);
557
+ if (disabled) return;
558
+ if (hovered.current || focused.current || dragging.current) {
559
+ if (!open) timer.current = setTimeout(show, focused.current ? 0 : OPEN_DELAY_MS);
560
+ return;
561
+ }
562
+ timer.current = setTimeout(() => {
563
+ setOpen(false);
564
+ }, CLOSE_DELAY_MS);
565
+ };
566
+ const commit = (value) => {
567
+ dragging.current = false;
568
+ setLive(null);
569
+ props.onCommit(value);
570
+ schedule();
571
+ };
572
+ const onChange = (event) => {
573
+ const value = Number(event.target.value);
574
+ setLive(value);
575
+ props.onInput(value);
576
+ };
577
+ const onKeyUp = (event) => {
578
+ if (live !== null) commit(live);
579
+ if (event.key === "Escape") {
580
+ clearTimeout(timer.current);
581
+ setOpen(false);
582
+ }
583
+ };
584
+ const shown = live ?? Math.min(max, Math.max(min, props.value));
585
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
586
+ ref: anchorRef,
587
+ className: settings_card_module_css_default.rangeAnchor,
588
+ onPointerEnter: () => {
589
+ hovered.current = true;
590
+ schedule();
591
+ },
592
+ onPointerLeave: () => {
593
+ hovered.current = false;
594
+ schedule();
595
+ },
596
+ onFocus: () => {
597
+ focused.current = true;
598
+ schedule();
599
+ },
600
+ onBlur: (event) => {
601
+ if (event.currentTarget.contains(event.relatedTarget)) return;
602
+ focused.current = false;
603
+ if (live !== null) commit(live);
604
+ else schedule();
605
+ },
606
+ children: [props.children, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
607
+ className: settings_card_module_css_default.rangeBubble,
608
+ "data-open": open ? "" : void 0,
609
+ "data-side": side,
610
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
611
+ type: "range",
612
+ className: settings_card_module_css_default.rangeSlider,
613
+ min,
614
+ max,
615
+ step,
616
+ value: shown,
617
+ "aria-label": props.label,
618
+ tabIndex: open ? 0 : -1,
619
+ disabled,
620
+ style: { "--range-fill": `${(shown - min) / (max - min) * 100}%` },
621
+ onPointerDown: () => {
622
+ dragging.current = true;
623
+ },
624
+ onPointerUp: (event) => {
625
+ commit(Number(event.currentTarget.value));
626
+ },
627
+ onPointerCancel: (event) => {
628
+ commit(Number(event.currentTarget.value));
629
+ },
630
+ onChange,
631
+ onKeyUp
632
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
633
+ className: settings_card_module_css_default.rangeEnds,
634
+ "aria-hidden": "true",
635
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: min }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: max })]
636
+ })]
637
+ })]
638
+ });
639
+ }
640
+ //#endregion
641
+ //#region src/client/PluginSettingsCard.tsx
642
+ /**
643
+ * Family-shared chrome for plugin settings cards: a disclosure header naming
644
+ * the plugin and what its settings govern, the controls inside, and the line
645
+ * that reports a refused write. A pick is written the moment it is made and a
646
+ * typed value after a pause, so the card has no save button and nothing waits
647
+ * on the reader. Renders nothing while the namespace is unavailable — a
648
+ * deployment that does not compose the owning plugin should show no trace of
649
+ * it. Inlined into each consumer's client bundle; mirrors the official
650
+ * ui-plugin-config PluginCard in a self-contained slice.
651
+ *
652
+ * Every field inside the body is one settings row: the label and its hint on
653
+ * the left, the control on the right, a hairline between two rows; a narrow
654
+ * card stacks the control under the text (settings-card.module.css).
655
+ */
656
+ /**
657
+ * What a text field inside a card reaches without its plugin wiring it: write
658
+ * the pending draft now (Enter, leaving the field) or put it back (Escape).
659
+ * Picks need neither; they are written the moment they are made.
660
+ */
661
+ const CardApply = (0, react.createContext)(null);
662
+ /** Length of the body's disclosure motion; it matches `--settings-card-motion` on the card root. */
663
+ const BODY_MOTION_MS = 180;
664
+ /**
665
+ * Render one plugin settings card.
666
+ * @param props - the plugin's copy keys, its form state, and its controls.
667
+ * @returns the card, or nothing while the namespace is still loading.
668
+ */
669
+ function PluginSettingsCard(props) {
670
+ const [open, setOpen] = (0, react.useState)(props.defaultOpen ?? true);
671
+ const { state, alwaysOpen } = props;
672
+ const expanded = alwaysOpen === true || open;
673
+ const [bodyPhase, setBodyPhase] = (0, react.useState)(expanded ? "shown" : "hidden");
674
+ const bodyRef = (0, react.useRef)(null);
675
+ (0, react.useLayoutEffect)(() => {
676
+ if (expanded) {
677
+ if (bodyPhase === "hidden") setBodyPhase("collapsed");
678
+ else if (bodyPhase === "collapsed") {
679
+ const node = bodyRef.current;
680
+ if (node !== null) node.offsetHeight;
681
+ setBodyPhase("entering");
682
+ } else if (bodyPhase === "closing") setBodyPhase("entering");
683
+ return;
684
+ }
685
+ if (bodyPhase === "collapsed") setBodyPhase("hidden");
686
+ else if (bodyPhase === "entering" || bodyPhase === "shown") setBodyPhase("closing");
687
+ }, [expanded, bodyPhase]);
688
+ (0, react.useEffect)(() => {
689
+ if (bodyPhase !== "entering" && bodyPhase !== "closing") return;
690
+ const timer = setTimeout(() => {
691
+ setBodyPhase(bodyPhase === "entering" ? "shown" : "hidden");
692
+ }, BODY_MOTION_MS);
693
+ return () => {
694
+ clearTimeout(timer);
695
+ };
696
+ }, [bodyPhase]);
697
+ if (!state.available) return null;
698
+ const title = props.t(props.titleKey);
699
+ const description = props.t(props.descriptionKey);
700
+ const descriptionTitle = props.descriptionTitleKey === void 0 ? description : props.t(props.descriptionTitleKey);
701
+ const baseClass = expanded ? `${settings_card_module_css_default.cardOpen} ${settings_card_module_css_default.card}` : settings_card_module_css_default.card;
702
+ const cardClass = props.density === "nested" ? `${baseClass} ${settings_card_module_css_default.cardNested}` : baseClass;
703
+ /** The body region both legs share; a hidden phase renders nothing at all. */
704
+ const bodyRegion = (content) => bodyPhase === "hidden" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
705
+ className: settings_card_module_css_default.disclosure,
706
+ "data-state": bodyPhase,
707
+ ref: bodyRef,
708
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
709
+ className: settings_card_module_css_default.body,
710
+ children: content
711
+ })
712
+ });
713
+ const header = alwaysOpen === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
714
+ className: settings_card_module_css_default.headerStatic,
715
+ children: [
716
+ props.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
717
+ className: settings_card_module_css_default.mark,
718
+ "aria-hidden": "true",
719
+ children: props.icon
720
+ }) : null,
721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
722
+ className: settings_card_module_css_default.headText,
723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
724
+ className: settings_card_module_css_default.name,
725
+ id: props.titleId,
726
+ title,
727
+ children: title
728
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
729
+ className: settings_card_module_css_default.description,
730
+ title: descriptionTitle,
731
+ children: props.descriptionNode ?? description
732
+ })]
733
+ }),
734
+ props.headerAction !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
735
+ className: settings_card_module_css_default.headerAction,
736
+ children: props.headerAction
737
+ }) : null
738
+ ]
739
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
740
+ type: "button",
741
+ className: settings_card_module_css_default.header,
742
+ "aria-expanded": open,
743
+ "aria-label": `${props.t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
744
+ onClick: () => {
745
+ setOpen(!open);
746
+ },
747
+ children: [
748
+ props.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
749
+ className: settings_card_module_css_default.mark,
750
+ "aria-hidden": "true",
751
+ children: props.icon
752
+ }) : null,
753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
754
+ className: settings_card_module_css_default.headText,
755
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
756
+ className: settings_card_module_css_default.name,
757
+ id: props.titleId,
758
+ title,
759
+ children: title
760
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
761
+ className: settings_card_module_css_default.description,
762
+ title: descriptionTitle,
763
+ children: props.descriptionNode ?? description
764
+ })]
765
+ }),
766
+ props.headerAction !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
767
+ className: settings_card_module_css_default.headerAction,
768
+ children: props.headerAction
769
+ }) : null,
770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
771
+ width: "14",
772
+ height: "14",
773
+ viewBox: "0 0 14 14",
774
+ fill: "none",
775
+ xmlns: "http://www.w3.org/2000/svg",
776
+ className: open ? `${settings_card_module_css_default.chevron} ${settings_card_module_css_default.chevronOpen}` : settings_card_module_css_default.chevron,
777
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
778
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
779
+ fill: "currentColor"
780
+ })
781
+ })
782
+ ]
783
+ });
784
+ if (!state.exposed && props.renderChildrenWhenNotExposed !== true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
785
+ className: cardClass,
786
+ children: [header, bodyRegion(props.hideNotExposedNotice !== true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
787
+ className: settings_card_module_css_default.notExposed,
788
+ role: "status",
789
+ children: props.t("settings.notExposed")
790
+ }) : null)]
791
+ });
792
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
793
+ className: cardClass,
794
+ children: [header, props.headerOnly !== true ? bodyRegion(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
795
+ !state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
796
+ className: settings_card_module_css_default.readOnly,
797
+ role: "status",
798
+ children: props.t("settings.readOnly")
799
+ }) : null,
800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardApply.Provider, {
801
+ value: {
802
+ commit: props.onSave,
803
+ revert: props.onDiscard
804
+ },
805
+ children: props.children
806
+ }),
807
+ props.hideFooter !== true && state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
808
+ className: settings_card_module_css_default.failed,
809
+ role: "status",
810
+ children: [props.t("settings.saveFailed"), state.failedReason ? " - " + state.failedReason : ""]
811
+ }) : null
812
+ ] })) : null]
813
+ });
814
+ }
815
+ /**
816
+ * A typed value field. Its draft is written after a pause in typing, at once on
817
+ * Enter or when focus leaves, and Escape puts the stored value back. `numeric`
818
+ * only hints the keypad: which drafts a field accepts is decided by its spec.
819
+ * A field given a `range` also offers the coarse slider bubble: dragging fills
820
+ * the draft live and releasing writes it.
821
+ */
822
+ function ValueField(props) {
823
+ const apply = (0, react.useContext)(CardApply);
824
+ const onKeyDown = (event) => {
825
+ if (apply === null || event.key !== "Enter" && event.key !== "Escape") return;
826
+ event.preventDefault();
827
+ if (event.key === "Enter") apply.commit();
828
+ else apply.revert();
829
+ event.currentTarget.blur();
830
+ };
831
+ const input = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
832
+ id: props.id,
833
+ className: props.invalid ? settings_card_module_css_default.inputInvalid : settings_card_module_css_default.input,
834
+ type: "text",
835
+ ...props.numeric === true ? { inputMode: "numeric" } : {},
836
+ ...props.invalid ? { "aria-invalid": true } : {},
837
+ value: props.text,
838
+ placeholder: props.placeholder ?? "",
839
+ disabled: props.disabled,
840
+ onChange: (event) => {
841
+ props.onEdit(event.target.value);
842
+ },
843
+ onKeyDown,
844
+ onBlur: () => {
845
+ apply?.commit();
846
+ }
847
+ });
848
+ const { range } = props;
849
+ const typed = Number(props.text);
850
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
851
+ className: settings_card_module_css_default.field,
852
+ children: [
853
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
854
+ className: settings_card_module_css_default.head,
855
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
856
+ className: settings_card_module_css_default.label,
857
+ htmlFor: props.id,
858
+ children: props.label
859
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
860
+ className: settings_card_module_css_default.badges,
861
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
862
+ className: settings_card_module_css_default.badge,
863
+ children: props.overriddenLabel
864
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
865
+ type: "button",
866
+ className: settings_card_module_css_default.reset,
867
+ disabled: props.disabled,
868
+ onClick: props.onReset,
869
+ children: props.resetLabel
870
+ })]
871
+ }) : null]
872
+ }),
873
+ range === void 0 ? input : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeBubble, {
874
+ min: range.min,
875
+ max: range.max,
876
+ step: range.step,
877
+ value: props.text.trim() === "" || Number.isNaN(typed) ? range.min : typed,
878
+ label: `${props.label} ${range.min}–${range.max}`,
879
+ disabled: props.disabled,
880
+ onInput: (value) => {
881
+ props.onEdit(String(value));
882
+ },
883
+ onCommit: (value) => {
884
+ props.onEdit(String(value));
885
+ apply?.commit();
886
+ },
887
+ children: input
888
+ }),
889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
890
+ className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
891
+ title: props.hintTitle,
892
+ children: props.invalid ? props.invalidLabel : props.hint
893
+ })
894
+ ]
895
+ });
896
+ }
897
+ const NON_SKIN_BODY_MARKERS = /* @__PURE__ */ new Set(["dshSkinCenter", "dshSidebarCollapsed"]);
898
+ function isSkinActive() {
899
+ return Object.keys(document.body.dataset).some((key) => key.startsWith("dsh") && !NON_SKIN_BODY_MARKERS.has(key));
900
+ }
901
+ const SELECT_CLOSE_MS = 100;
902
+ /**
903
+ * The shared dual-mode select control. While an appearance skin is active it
904
+ * renders the legacy native `<select>` untouched, so element-level skin
905
+ * selectors keep working; under the default appearance it renders a
906
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
907
+ * Cards reach it through BooleanField/ChoiceField; editors outside a card
908
+ * (the side-card prefs) bind it directly through onEdit.
909
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
910
+ */
911
+ function SelectField(props) {
912
+ const { id, options, value } = props;
913
+ const [open, setOpen] = (0, react.useState)(false);
914
+ const [closing, setClosing] = (0, react.useState)(false);
915
+ const [phase, setPhase] = (0, react.useState)("initial");
916
+ const [activeIndex, setActiveIndex] = (0, react.useState)(0);
917
+ const closeTimer = (0, react.useRef)(void 0);
918
+ const wrapRef = (0, react.useRef)(null);
919
+ const popupRef = (0, react.useRef)(null);
920
+ const currentIndex = () => {
921
+ const index = options.findIndex((option) => option.value === value);
922
+ return index >= 0 ? index : 0;
923
+ };
924
+ const close = (0, react.useCallback)(() => {
925
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
926
+ setClosing(true);
927
+ closeTimer.current = setTimeout(() => {
928
+ setClosing(false);
929
+ setOpen(false);
930
+ }, SELECT_CLOSE_MS);
931
+ }, []);
932
+ const openPopup = () => {
933
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
934
+ setActiveIndex(currentIndex());
935
+ setPhase("initial");
936
+ setClosing(false);
937
+ setOpen(true);
938
+ };
939
+ const commit = (index) => {
940
+ const option = options[index];
941
+ if (option) props.onEdit(option.value);
942
+ close();
943
+ };
944
+ const onTriggerClick = () => {
945
+ if (props.disabled) return;
946
+ if (open && !closing) close();
947
+ else openPopup();
948
+ };
949
+ const onKeyDown = (event) => {
950
+ if (props.disabled) return;
951
+ const count = options.length;
952
+ switch (event.key) {
953
+ case "ArrowDown":
954
+ case "ArrowUp":
955
+ case "Enter":
956
+ case " ":
957
+ event.preventDefault();
958
+ if (!open) openPopup();
959
+ else if (!closing) if (event.key === "ArrowDown") setActiveIndex((index) => (index + 1) % count);
960
+ else if (event.key === "ArrowUp") setActiveIndex((index) => (index - 1 + count) % count);
961
+ else commit(activeIndex);
962
+ break;
963
+ case "Escape":
964
+ if (open) {
965
+ event.preventDefault();
966
+ event.stopPropagation();
967
+ close();
968
+ }
969
+ break;
970
+ case "Tab":
971
+ if (open) close();
972
+ break;
973
+ }
974
+ };
975
+ (0, react.useEffect)(() => () => {
976
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
977
+ }, []);
978
+ (0, react.useLayoutEffect)(() => {
979
+ if (open && !closing && phase === "initial") {
980
+ popupRef.current?.offsetHeight;
981
+ setPhase("open");
982
+ }
983
+ }, [
984
+ open,
985
+ closing,
986
+ phase
987
+ ]);
988
+ (0, react.useEffect)(() => {
989
+ if (!open) return;
990
+ const onPointerDown = (event) => {
991
+ const target = event.target;
992
+ if (target instanceof Node && !wrapRef.current?.contains(target)) close();
993
+ };
994
+ document.addEventListener("pointerdown", onPointerDown);
995
+ return () => document.removeEventListener("pointerdown", onPointerDown);
996
+ }, [open, close]);
997
+ (0, react.useEffect)(() => {
998
+ if (props.disabled && open) close();
999
+ }, [
1000
+ props.disabled,
1001
+ open,
1002
+ close
1003
+ ]);
1004
+ if (isSkinActive()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1005
+ id,
1006
+ className: settings_card_module_css_default.select,
1007
+ value,
1008
+ disabled: props.disabled,
1009
+ onChange: (event) => {
1010
+ props.onEdit(event.target.value);
1011
+ },
1012
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1013
+ value: option.value,
1014
+ children: option.label
1015
+ }, option.value))
1016
+ });
1017
+ const label = options.find((option) => option.value === value)?.label ?? "";
1018
+ const popupClass = closing ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupClose}` : phase === "open" ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupOpen}` : settings_card_module_css_default.selectPopup;
1019
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1020
+ className: settings_card_module_css_default.selectWrap,
1021
+ ref: wrapRef,
1022
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1023
+ type: "button",
1024
+ id,
1025
+ className: `${settings_card_module_css_default.select} ${settings_card_module_css_default.selectButton}`,
1026
+ disabled: props.disabled,
1027
+ "aria-haspopup": "listbox",
1028
+ "aria-expanded": open,
1029
+ "aria-activedescendant": open ? `${id}-o${activeIndex}` : void 0,
1030
+ "aria-invalid": props.invalid || void 0,
1031
+ onClick: onTriggerClick,
1032
+ onKeyDown,
1033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1034
+ className: settings_card_module_css_default.selectLabel,
1035
+ children: label
1036
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1037
+ width: "14",
1038
+ height: "14",
1039
+ viewBox: "0 0 14 14",
1040
+ fill: "none",
1041
+ xmlns: "http://www.w3.org/2000/svg",
1042
+ className: open ? `${settings_card_module_css_default.selectChevron} ${settings_card_module_css_default.selectChevronOpen}` : settings_card_module_css_default.selectChevron,
1043
+ "aria-hidden": "true",
1044
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1045
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
1046
+ fill: "currentColor"
1047
+ })
1048
+ })]
1049
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1050
+ className: popupClass,
1051
+ role: "listbox",
1052
+ ref: popupRef,
1053
+ children: options.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1054
+ id: `${id}-o${index}`,
1055
+ role: "option",
1056
+ "aria-selected": option.value === value,
1057
+ className: `${settings_card_module_css_default.selectOption}${option.value === value ? ` ${settings_card_module_css_default.selectOptionSelected}` : ""}${index === activeIndex && !closing ? ` ${settings_card_module_css_default.selectOptionActive}` : ""}`,
1058
+ onClick: () => {
1059
+ commit(index);
1060
+ },
1061
+ children: option.label
1062
+ }, option.value))
1063
+ }) : null]
1064
+ });
1065
+ }
1066
+ /**
1067
+ * A visible segmented group for a small set of mutually exclusive values.
1068
+ * Every option stays on screen, so inherit/on/off and short enum fields take
1069
+ * one click without opening a popup. The pressed option carries the group's
1070
+ * sliding highlight and `aria-pressed`; the group takes the field label as its
1071
+ * accessible name.
1072
+ */
1073
+ function SegmentField(props) {
1074
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SegmentGroup, {
1075
+ id: props.id,
1076
+ role: "group",
1077
+ "aria-label": props.label,
1078
+ "aria-invalid": props.invalid || void 0,
1079
+ children: props.options.map((option, index) => {
1080
+ const active = option.value === props.value;
1081
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1082
+ type: "button",
1083
+ className: settings_card_module_css_default.segment,
1084
+ "aria-pressed": active,
1085
+ disabled: props.disabled,
1086
+ "data-active": active ? "" : void 0,
1087
+ "data-segment-index": index,
1088
+ onClick: () => {
1089
+ if (!active) props.onEdit(option.value);
1090
+ },
1091
+ children: option.label
1092
+ }, option.value);
1093
+ })
1094
+ });
1095
+ }
1096
+ /** An enumerated field, selecting or segmenting its small choice set. */
1097
+ function ChoiceField(props) {
1098
+ const segmented = props.presentation === "segment";
1099
+ const options = [{
1100
+ value: "",
1101
+ label: props.inheritLabel
1102
+ }, ...props.choices];
1103
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1104
+ className: settings_card_module_css_default.field,
1105
+ children: [
1106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1107
+ className: settings_card_module_css_default.head,
1108
+ children: [segmented ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1109
+ className: settings_card_module_css_default.label,
1110
+ children: props.label
1111
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1112
+ className: settings_card_module_css_default.label,
1113
+ htmlFor: props.id,
1114
+ children: props.label
1115
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1116
+ className: settings_card_module_css_default.badges,
1117
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1118
+ className: settings_card_module_css_default.badge,
1119
+ children: props.overriddenLabel
1120
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1121
+ type: "button",
1122
+ className: settings_card_module_css_default.reset,
1123
+ disabled: props.disabled,
1124
+ onClick: props.onReset,
1125
+ children: props.resetLabel
1126
+ })]
1127
+ }) : null]
1128
+ }),
1129
+ segmented ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SegmentField, {
1130
+ id: props.id,
1131
+ label: props.label,
1132
+ options,
1133
+ value: props.text,
1134
+ disabled: props.disabled,
1135
+ invalid: props.invalid,
1136
+ onEdit: props.onEdit
1137
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1138
+ id: props.id,
1139
+ options,
1140
+ value: props.text,
1141
+ disabled: props.disabled,
1142
+ invalid: props.invalid,
1143
+ onEdit: props.onEdit
1144
+ }),
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1146
+ className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
1147
+ title: props.hintTitle,
1148
+ children: props.invalid ? props.invalidLabel : props.hint
1149
+ })
1150
+ ]
1151
+ });
1152
+ }
1153
+ /**
1154
+ * A boolean toggle drawn as a switch track, for a setting that is only on or
1155
+ * off and applies on flip. It is a real button with
1156
+ * `role="switch"` and `aria-checked`, so keyboard and assistive activation
1157
+ * arrive as a click and the state is announced instead of inferred from a
1158
+ * checkbox tick.
1159
+ *
1160
+ * The thumb bounces across only once the reader has pressed this switch: the
1161
+ * press writes the mark that arms the bounce, so a page opening, a TAB
1162
+ * switching back or a value arriving from elsewhere draws every thumb at rest.
1163
+ */
1164
+ function ToggleSwitch(props) {
1165
+ const [moved, setMoved] = (0, react.useState)(false);
1166
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1167
+ type: "button",
1168
+ className: settings_card_module_css_default.toggle,
1169
+ role: "switch",
1170
+ "aria-checked": props.checked,
1171
+ "aria-labelledby": props.labelledBy,
1172
+ "aria-label": props.labelledBy === void 0 ? props.ariaLabel : void 0,
1173
+ disabled: props.disabled,
1174
+ "data-moved": moved ? "" : void 0,
1175
+ onClick: () => {
1176
+ setMoved(true);
1177
+ props.onChange(!props.checked);
1178
+ },
1179
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1180
+ className: settings_card_module_css_default.toggleTrack,
1181
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: settings_card_module_css_default.toggleThumb })
1182
+ })
1183
+ });
1184
+ }
1185
+ /** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints stays unwritten. */
1186
+ function numberField(field, constraints = {}) {
1187
+ const { integer = false, min } = constraints;
1188
+ return {
1189
+ field,
1190
+ entry: "type",
1191
+ format: (value) => typeof value === "number" ? String(value) : "",
1192
+ parse: (text) => {
1193
+ const trimmed = text.trim();
1194
+ if (trimmed === "") return { kind: "clear" };
1195
+ const parsed = Number(trimmed);
1196
+ if (!Number.isFinite(parsed)) return void 0;
1197
+ if (integer && !Number.isInteger(parsed)) return void 0;
1198
+ if (min !== void 0 && parsed < min) return void 0;
1199
+ return {
1200
+ kind: "set",
1201
+ value: parsed
1202
+ };
1203
+ }
1204
+ };
1205
+ }
1206
+ /** A boolean field, edited through true/false draft text. */
1207
+ function booleanField(field) {
1208
+ return {
1209
+ field,
1210
+ entry: "pick",
1211
+ format: (value) => typeof value === "boolean" ? String(value) : "",
1212
+ parse: (text) => {
1213
+ const trimmed = text.trim();
1214
+ if (trimmed === "") return { kind: "clear" };
1215
+ if (trimmed === "true") return {
1216
+ kind: "set",
1217
+ value: true
1218
+ };
1219
+ if (trimmed === "false") return {
1220
+ kind: "set",
1221
+ value: false
1222
+ };
1223
+ }
1224
+ };
1225
+ }
1226
+ /** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
1227
+ function choiceField(field, choices) {
1228
+ return {
1229
+ field,
1230
+ entry: "pick",
1231
+ format: (value) => typeof value === "string" && choices.includes(value) ? value : "",
1232
+ parse: (text) => {
1233
+ if (text === "") return { kind: "clear" };
1234
+ return choices.includes(text) ? {
1235
+ kind: "set",
1236
+ value: text
1237
+ } : void 0;
1238
+ }
1239
+ };
1240
+ }
1241
+ /**
1242
+ * Applies one card's edits to one settings namespace as the reader makes them.
1243
+ *
1244
+ * The Host is the only authority on whether a value was accepted — its
1245
+ * validators own the constraints no schema can express — so the outcome is
1246
+ * read back from the section rather than predicted here. A write that did not
1247
+ * land puts each pick back to the stored value, the way a switch that could not
1248
+ * flip springs back, and keeps typed text so the reader can correct it instead
1249
+ * of retyping.
1250
+ */
1251
+ var CardForm = class {
1252
+ scope;
1253
+ specs;
1254
+ staged = /* @__PURE__ */ new Map();
1255
+ listeners = /* @__PURE__ */ new Set();
1256
+ /** The form subscription installed in the constructor; released by dispose(). */
1257
+ disposeForm;
1258
+ disposed = false;
1259
+ /** The pending write of typed text, waiting for typing to pause. */
1260
+ typePause;
1261
+ saving = false;
1262
+ /** A save asked for while one was in flight; it runs once the first settles (#1754). */
1263
+ saveQueued = false;
1264
+ failed = false;
1265
+ failedReason;
1266
+ /** @param scope - the bound configuration form for this card's namespace. */
1267
+ constructor(scope, specs) {
1268
+ this.scope = scope;
1269
+ this.specs = new Map(specs.map((spec) => [spec.field, spec]));
1270
+ this.disposeForm = scope.subscribe(() => {
1271
+ this.publish();
1272
+ });
1273
+ }
1274
+ /**
1275
+ * Release the form subscription and every bound store listener. The card
1276
+ * must call this on teardown; later calls are no-ops.
1277
+ */
1278
+ dispose() {
1279
+ if (this.disposed) return;
1280
+ this.disposed = true;
1281
+ clearTimeout(this.typePause);
1282
+ this.disposeForm();
1283
+ this.listeners.clear();
1284
+ }
1285
+ /** Publish a projection of this form, rebuilt whenever the form or a draft changes. */
1286
+ bind(project) {
1287
+ const store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(project());
1288
+ this.listeners.add(() => {
1289
+ store.set(project());
1290
+ });
1291
+ return store;
1292
+ }
1293
+ /** Read the card-level state: what the Host serves, and what is still waiting to be written. */
1294
+ shell() {
1295
+ const snapshot = this.scope.getSnapshot();
1296
+ const plan = this.plan();
1297
+ return {
1298
+ available: snapshot.status !== "loading",
1299
+ exposed: snapshot.status === "ready",
1300
+ writable: snapshot.writable,
1301
+ dirty: plan.length > 0,
1302
+ invalid: plan.some((item) => item.judge === void 0),
1303
+ saving: this.saving,
1304
+ failed: this.failed,
1305
+ ...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
1306
+ };
1307
+ }
1308
+ /** Read one field's state from the effective section and its staged draft. */
1309
+ field(field) {
1310
+ const spec = this.specOf(field);
1311
+ const staged = this.staged.get(field);
1312
+ if (staged === void 0) return {
1313
+ text: spec.format(this.sectionValue(field)),
1314
+ overridden: this.stored(field),
1315
+ invalid: false
1316
+ };
1317
+ const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
1318
+ return {
1319
+ text: staged.text,
1320
+ overridden: write?.kind === "set",
1321
+ invalid: write === void 0
1322
+ };
1323
+ }
1324
+ /** The actions the card's slot registration injects. */
1325
+ actions() {
1326
+ return {
1327
+ edit: (field, text) => {
1328
+ this.stage(field, {
1329
+ text,
1330
+ clear: false
1331
+ });
1332
+ },
1333
+ resetField: (field) => {
1334
+ this.stage(field, {
1335
+ text: this.specOf(field).format(this.baseValue(field)),
1336
+ clear: true
1337
+ });
1338
+ },
1339
+ save: () => {
1340
+ this.applyNow();
1341
+ },
1342
+ discard: () => {
1343
+ clearTimeout(this.typePause);
1344
+ if (this.staged.size === 0 && !this.failed) return;
1345
+ this.staged.clear();
1346
+ this.failed = false;
1347
+ this.failedReason = void 0;
1348
+ this.publish();
1349
+ }
1350
+ };
1351
+ }
1352
+ /**
1353
+ * Run one write, and re-run it once if another was asked for while this one
1354
+ * was still in flight.
1355
+ *
1356
+ * A write is a Host round trip that also drives the profile reconcile, and
1357
+ * the Host runs it inside one exclusive transaction. Answering an edit that
1358
+ * arrives mid-flight by returning immediately dropped it with no explanation
1359
+ * (#1754). Serializing instead means an edit made while another write is
1360
+ * still settling is written against the settled state.
1361
+ * @returns settlement after the mutation and the read-back.
1362
+ */
1363
+ async requestSave() {
1364
+ if (this.saving) {
1365
+ this.saveQueued = true;
1366
+ return;
1367
+ }
1368
+ await this.save();
1369
+ if (!this.saveQueued) return;
1370
+ this.saveQueued = false;
1371
+ await this.requestSave();
1372
+ }
1373
+ /**
1374
+ * Write every valid draft in one atomic form mutation, then re-seed from
1375
+ * what the Host accepted. A draft the field does not accept stays on screen,
1376
+ * unwritten, without holding the others back.
1377
+ *
1378
+ * The batch rides one mutate, so cross-field validate hooks (baseURL+model)
1379
+ * judge it as a unit: the Host either applies every write or refuses the
1380
+ * batch. The form contract answers a refusal or a skipped write with `false`
1381
+ * (it recovers with a fresh Host view instead of throwing), so the outcome is
1382
+ * judged twice: the answer itself, and then the settled snapshot read back
1383
+ * one planned write at a time. One missed write fails the whole batch. A
1384
+ * transport that rejects instead (the dsh-web bridge controller on a dead
1385
+ * connection) reports through the same failure path with its rejection
1386
+ * message. A batch that did not land puts its picks and clears back to the
1387
+ * stored value and keeps its typed text, so the reader can correct it.
1388
+ * @returns settlement after the mutation and the read-back.
1389
+ */
1390
+ async save() {
1391
+ const valid = this.plan().filter((item) => item.judge !== void 0);
1392
+ if (valid.length === 0 || this.saving) return;
1393
+ const pending = /* @__PURE__ */ new Map();
1394
+ for (const item of valid) pending.set(item.field, this.staged.get(item.field));
1395
+ this.saving = true;
1396
+ this.failed = false;
1397
+ this.failedReason = void 0;
1398
+ this.publish();
1399
+ const ops = valid.map((item) => item.op.op === "set" ? {
1400
+ op: "set",
1401
+ path: [item.field],
1402
+ value: item.op.value
1403
+ } : {
1404
+ op: "unset",
1405
+ path: [item.field]
1406
+ });
1407
+ let failedReason;
1408
+ let accepted = false;
1409
+ try {
1410
+ accepted = await this.scope.mutate(ops);
1411
+ } catch (error) {
1412
+ failedReason = error instanceof Error ? error.message : String(error);
1413
+ }
1414
+ const landed = accepted && failedReason === void 0 && valid.every((item) => item.judge());
1415
+ for (const [field, before] of pending) {
1416
+ if (this.staged.get(field) !== before) continue;
1417
+ if (landed || before === void 0 || before.clear || this.specOf(field).entry === "pick") this.staged.delete(field);
1418
+ }
1419
+ this.saving = false;
1420
+ this.failed = !landed;
1421
+ this.failedReason = failedReason;
1422
+ this.publish();
1423
+ }
1424
+ /**
1425
+ * Every staged edit a write would carry. An entry whose draft is not a value
1426
+ * its field accepts carries no write: the form is still dirty, and the draft
1427
+ * stays on screen rather than being dropped. A staged edit that matches the
1428
+ * effective section is not a write at all.
1429
+ * @returns the planned writes, in the order the fields were staged.
1430
+ */
1431
+ plan() {
1432
+ const plan = [];
1433
+ for (const [field, staged] of this.staged) {
1434
+ const spec = this.specOf(field);
1435
+ if (staged.clear) {
1436
+ if (this.stored(field)) plan.push({
1437
+ field,
1438
+ op: {
1439
+ field,
1440
+ op: "unset"
1441
+ },
1442
+ judge: () => this.landedUnset(field)
1443
+ });
1444
+ continue;
1445
+ }
1446
+ if (staged.text === spec.format(this.sectionValue(field))) continue;
1447
+ const write = spec.parse(staged.text);
1448
+ if (write === void 0) plan.push({
1449
+ field,
1450
+ op: {
1451
+ field,
1452
+ op: "unset"
1453
+ },
1454
+ judge: void 0
1455
+ });
1456
+ else if (write.kind === "clear") plan.push({
1457
+ field,
1458
+ op: {
1459
+ field,
1460
+ op: "unset"
1461
+ },
1462
+ judge: () => this.landedUnset(field)
1463
+ });
1464
+ else plan.push({
1465
+ field,
1466
+ op: {
1467
+ field,
1468
+ op: "set",
1469
+ value: write.value
1470
+ },
1471
+ judge: () => this.landedSet(field, write.value)
1472
+ });
1473
+ }
1474
+ return plan;
1475
+ }
1476
+ /**
1477
+ * Read-back judgment for a planned set: the user layer must hold the
1478
+ * intended value once the mutation has settled.
1479
+ */
1480
+ landedSet(field, value) {
1481
+ if (this.specOf(field).secret) return true;
1482
+ return this.userLayer()?.[field] === value;
1483
+ }
1484
+ /**
1485
+ * Read-back judgment for a planned unset: the field must be gone from the
1486
+ * user layer once the mutation has settled.
1487
+ */
1488
+ landedUnset(field) {
1489
+ return !this.stored(field);
1490
+ }
1491
+ /**
1492
+ * Take one edit and schedule its write: a pick or a clear is written now,
1493
+ * typed text once typing has rested for TYPE_PAUSE_MS.
1494
+ */
1495
+ stage(field, edit) {
1496
+ this.staged.set(field, edit);
1497
+ this.failed = false;
1498
+ this.failedReason = void 0;
1499
+ this.publish();
1500
+ if (edit.clear || this.specOf(field).entry === "pick") {
1501
+ this.applyNow();
1502
+ return;
1503
+ }
1504
+ clearTimeout(this.typePause);
1505
+ this.typePause = setTimeout(() => {
1506
+ this.applyNow();
1507
+ }, 600);
1508
+ }
1509
+ /** Write every pending draft now, taking the place of a waiting pause. */
1510
+ applyNow() {
1511
+ clearTimeout(this.typePause);
1512
+ this.typePause = void 0;
1513
+ if (this.disposed) return;
1514
+ this.requestSave();
1515
+ }
1516
+ specOf(field) {
1517
+ const spec = this.specs.get(field);
1518
+ if (spec === void 0) throw new Error(`settings card has no field ${field}`);
1519
+ return spec;
1520
+ }
1521
+ snapshotOf() {
1522
+ return this.scope.getSnapshot();
1523
+ }
1524
+ sectionValue(field) {
1525
+ return this.snapshotOf().value?.[field];
1526
+ }
1527
+ baseValue(field) {
1528
+ return this.snapshotOf().base?.[field];
1529
+ }
1530
+ userLayer() {
1531
+ return this.snapshotOf().user;
1532
+ }
1533
+ stored(field) {
1534
+ const user = this.userLayer();
1535
+ return user !== void 0 && Object.hasOwn(user, field);
1536
+ }
1537
+ publish() {
1538
+ for (const listener of this.listeners) listener();
1539
+ }
1540
+ };
1541
+ //#endregion
1542
+ //#region src/client/usage-settings-card.ts
1543
+ /** The display currencies the spend figures offer. */
1544
+ const SPEND_CURRENCIES = ["CNY", "USD"];
1545
+ /** Bounds of the poll interval, mirroring the host schema; the slider walks them in steps of 10 s. */
1546
+ const POLL_INTERVAL_RANGE = {
1547
+ min: 30,
1548
+ max: 3600,
1549
+ step: 10
1550
+ };
1551
+ /** Bridges the usage namespace onto the family card form for the plugin's lifetime. */
1552
+ var UsageSettingsCardController = class {
1553
+ form;
1554
+ store;
1555
+ /** @param scope - the bound configuration form of the usage namespace. */
1556
+ constructor(scope) {
1557
+ this.form = new CardForm(scope, [
1558
+ booleanField("enabled"),
1559
+ numberField("pollIntervalSec", {
1560
+ integer: true,
1561
+ min: POLL_INTERVAL_RANGE.min
1562
+ }),
1563
+ choiceField("spendCurrency", SPEND_CURRENCIES),
1564
+ numberField("cnyPerUsd", { min: .01 })
1565
+ ]);
1566
+ this.store = this.form.bind(() => ({
1567
+ ...this.form.shell(),
1568
+ enabled: this.form.field("enabled"),
1569
+ pollIntervalSec: this.form.field("pollIntervalSec"),
1570
+ spendCurrency: this.form.field("spendCurrency"),
1571
+ cnyPerUsd: this.form.field("cnyPerUsd")
1572
+ }));
1573
+ }
1574
+ /** The face the section's slot entry injects. */
1575
+ inject() {
1576
+ return {
1577
+ store: this.store,
1578
+ ...this.form.actions()
1579
+ };
1580
+ }
1581
+ /** Release the form subscription; the plugin disposer calls this on teardown. */
1582
+ dispose() {
1583
+ this.form.dispose();
1584
+ }
1585
+ };
1586
+ //#endregion
1587
+ //#region src/client/UsagePage.tsx
1588
+ /**
1589
+ * The page's card: the plugin switch in the header, the statistics and the
1590
+ * display rows in the body. A switched-off plugin keeps only the header, whose
1591
+ * line says how to bring it back; a refused write keeps the body, so the
1592
+ * failure line under the rows stays readable.
1593
+ */
1594
+ function UsagePage(props) {
1595
+ const { card, face, enabled } = props;
1596
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1597
+ className: usage_module_css_default.sectionList,
1598
+ "data-dsh-plugin": "usage",
1599
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
1600
+ t,
1601
+ titleKey: "usage.title",
1602
+ titleId: "settings-usage-title",
1603
+ descriptionKey: enabled ? "usage.description" : "usage.disabled",
1604
+ headerAction: card.exposed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleSwitch, {
1605
+ labelledBy: "settings-usage-title",
1606
+ checked: enabled,
1607
+ disabled: !card.writable,
1608
+ onChange: (next) => {
1609
+ face.edit("enabled", String(next));
1610
+ }
1611
+ }) : void 0,
1612
+ headerOnly: !enabled && !card.failed,
1613
+ state: card,
1614
+ alwaysOpen: true,
1615
+ renderChildrenWhenNotExposed: true,
1616
+ hideNotExposedNotice: true,
1617
+ onSave: face.save,
1618
+ onDiscard: face.discard,
1619
+ children: [props.children, card.exposed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRows, {
1620
+ card,
1621
+ face
1622
+ }) : null]
1623
+ })
1624
+ });
1625
+ }
1626
+ /**
1627
+ * The display rows under the statistics. Each control writes through the card
1628
+ * form: the currency pick at once, a typed number after a pause or on Enter.
1629
+ * The rate only matters while spend shows in dollars, so it appears then.
1630
+ */
1631
+ function SettingsRows(props) {
1632
+ const { card, face } = props;
1633
+ const fieldProps = {
1634
+ overriddenLabel: t("settings.overridden"),
1635
+ resetLabel: t("settings.reset"),
1636
+ invalidLabel: t("settings.invalidNumber"),
1637
+ disabled: !card.writable
1638
+ };
1639
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1640
+ className: usage_module_css_default.settingsGroup,
1641
+ "aria-labelledby": "settings-usage-display-title",
1642
+ "data-dsh-part": "settings-rows",
1643
+ children: [
1644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1645
+ className: usage_module_css_default.groupTitle,
1646
+ id: "settings-usage-display-title",
1647
+ children: t("usage.config.title")
1648
+ }),
1649
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1650
+ id: "settings-usage-poll-interval",
1651
+ label: t("usage.config.pollIntervalSec"),
1652
+ hint: t("usage.config.pollIntervalHint"),
1653
+ numeric: true,
1654
+ range: POLL_INTERVAL_RANGE,
1655
+ ...fieldProps,
1656
+ ...card.pollIntervalSec,
1657
+ onEdit: (text) => {
1658
+ face.edit("pollIntervalSec", text);
1659
+ },
1660
+ onReset: () => {
1661
+ face.resetField("pollIntervalSec");
1662
+ }
1663
+ }),
1664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceField, {
1665
+ id: "settings-usage-spend-currency",
1666
+ label: t("usage.config.spendCurrency"),
1667
+ hint: t("usage.config.spendCurrencyHint"),
1668
+ inheritLabel: t("settings.inherit"),
1669
+ presentation: "segment",
1670
+ choices: SPEND_CURRENCIES.map((currency) => ({
1671
+ value: currency,
1672
+ label: currency
1673
+ })),
1674
+ ...fieldProps,
1675
+ ...card.spendCurrency,
1676
+ onEdit: (text) => {
1677
+ face.edit("spendCurrency", text);
1678
+ },
1679
+ onReset: () => {
1680
+ face.resetField("spendCurrency");
1681
+ }
1682
+ }),
1683
+ card.spendCurrency.text === "USD" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1684
+ id: "settings-usage-cny-per-usd",
1685
+ label: t("usage.config.cnyPerUsd"),
1686
+ hint: t("usage.config.cnyPerUsdHint"),
1687
+ numeric: true,
1688
+ ...fieldProps,
1689
+ ...card.cnyPerUsd,
1690
+ onEdit: (text) => {
1691
+ face.edit("cnyPerUsd", text);
1692
+ },
1693
+ onReset: () => {
1694
+ face.resetField("cnyPerUsd");
1695
+ }
1696
+ }) : null
1697
+ ]
1698
+ });
1699
+ }
1700
+ //#endregion
314
1701
  //#region src/core/adapters.ts
315
1702
  /** Parse a string/number into a finite number, else undefined. */
316
1703
  function toNum(value) {
@@ -1254,12 +2641,13 @@ window.__ModuleLoader__.load({
1254
2641
  //#endregion
1255
2642
  //#region src/client/UsageSectionCard.tsx
1256
2643
  /**
1257
- * The usage statistics settings section: three tabs (用量: today's usage,
1258
- * balances, trend; 个人套餐: per-provider plan quota windows; Token 银行:
1259
- * the whale-yuan voucher minted from every provider's ledger usage)
1260
- * plus a compact settings row. Data comes from the host's loopback-fenced
1261
- * /api/dsh-usage/overview document; polling runs only while the section is
1262
- * mounted and the tab is visible.
2644
+ * The usage statistics settings page: one family settings card whose header
2645
+ * carries the plugin switch, whose body holds three tabs (用量: today's
2646
+ * usage, balances, trend; 个人套餐: per-provider plan quota windows; Token
2647
+ * 银行: the whale-yuan voucher minted from every provider's ledger usage)
2648
+ * and, under them, the display rows. Data comes from the host's
2649
+ * loopback-fenced /api/dsh-usage/overview document; polling runs only while
2650
+ * the section is mounted and the tab is visible.
1263
2651
  * @module @linxin666/dsh-usage/client/UsageSectionCard
1264
2652
  */
1265
2653
  /** Poll cadence while the section is open. */
@@ -1300,9 +2688,11 @@ window.__ModuleLoader__.load({
1300
2688
  const { totals } = props;
1301
2689
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1302
2690
  className: usage_module_css_default.statRow,
2691
+ "data-dsh-layout": "usage-totals",
1303
2692
  children: [
1304
2693
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1305
2694
  className: usage_module_css_default.stat,
2695
+ "data-dsh-stat": "total",
1306
2696
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1307
2697
  className: usage_module_css_default.statValue,
1308
2698
  children: formatTokens(totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens)
@@ -1313,6 +2703,7 @@ window.__ModuleLoader__.load({
1313
2703
  }),
1314
2704
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1315
2705
  className: usage_module_css_default.stat,
2706
+ "data-dsh-stat": "input",
1316
2707
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1317
2708
  className: usage_module_css_default.statValue,
1318
2709
  children: formatTokens(totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens)
@@ -1323,6 +2714,7 @@ window.__ModuleLoader__.load({
1323
2714
  }),
1324
2715
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1325
2716
  className: usage_module_css_default.stat,
2717
+ "data-dsh-stat": "output",
1326
2718
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1327
2719
  className: usage_module_css_default.statValue,
1328
2720
  children: formatTokens(totals.outputTokens)
@@ -1333,6 +2725,7 @@ window.__ModuleLoader__.load({
1333
2725
  }),
1334
2726
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1335
2727
  className: usage_module_css_default.stat,
2728
+ "data-dsh-stat": "cache-read",
1336
2729
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1337
2730
  className: usage_module_css_default.statValue,
1338
2731
  children: formatTokens(totals.cacheReadTokens)
@@ -1343,6 +2736,7 @@ window.__ModuleLoader__.load({
1343
2736
  }),
1344
2737
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
2738
  className: usage_module_css_default.stat,
2739
+ "data-dsh-stat": "cache-write",
1346
2740
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1347
2741
  className: usage_module_css_default.statValue,
1348
2742
  children: formatTokens(totals.cacheWriteTokens)
@@ -1353,6 +2747,7 @@ window.__ModuleLoader__.load({
1353
2747
  }),
1354
2748
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1355
2749
  className: usage_module_css_default.stat,
2750
+ "data-dsh-stat": "calls",
1356
2751
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1357
2752
  className: usage_module_css_default.statValue,
1358
2753
  children: formatTokens(totals.calls)
@@ -1478,17 +2873,14 @@ window.__ModuleLoader__.load({
1478
2873
  }
1479
2874
  /** The section component; the slot merges the face into these props. */
1480
2875
  function UsageSectionCard(props) {
1481
- const { store, poll, refresh, loadDay, settings } = props;
2876
+ const { store, poll, refresh, loadDay, settingsCard } = props;
1482
2877
  const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
1483
- const settingsSnapshot = settings.getSnapshot();
1484
- const settingsValue = settingsSnapshot.value ?? {};
2878
+ const card = (0, react.useSyncExternalStore)(settingsCard.store.subscribe, settingsCard.store.getSnapshot);
1485
2879
  const [tab, setTab] = (0, react.useState)("usage");
1486
2880
  const [usageDay, setUsageDay] = (0, react.useState)(void 0);
1487
2881
  const [bankDay, setBankDay] = (0, react.useState)(void 0);
1488
2882
  const [refreshing, setRefreshing] = (0, react.useState)(false);
1489
- const [, bumpSettings] = (0, react.useState)(0);
1490
- (0, react.useEffect)(() => settings.subscribe(() => bumpSettings((count) => count + 1)), [settings]);
1491
- const enabled = settingsValue.enabled ?? true;
2883
+ const enabled = card.enabled.text !== "false";
1492
2884
  (0, react.useEffect)(() => {
1493
2885
  if (!enabled) return void 0;
1494
2886
  poll();
@@ -1537,219 +2929,201 @@ window.__ModuleLoader__.load({
1537
2929
  window.setTimeout(() => setRefreshing(false), 3e3);
1538
2930
  }
1539
2931
  };
1540
- if (!enabled || ui.status === "error" || snapshot === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1541
- className: usage_module_css_default.section,
1542
- "data-dsh-plugin": "usage",
1543
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2932
+ if (!enabled || ui.status === "error" || snapshot === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePage, {
2933
+ card,
2934
+ face: settingsCard,
2935
+ enabled,
2936
+ children: enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1544
2937
  className: usage_module_css_default.muted,
1545
2938
  "data-dsh-part": "status-line",
1546
- children: !enabled ? t("usage.disabled") : ui.status === "error" ? t("usage.error", { error: ui.error ?? "" }) : t("usage.loading")
1547
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
1548
- settings,
1549
- snapshot: settingsSnapshot.status === "ready" ? settingsSnapshot : void 0,
1550
- value: settingsValue
1551
- })]
2939
+ children: ui.status === "error" ? t("usage.error", { error: ui.error ?? "" }) : t("usage.loading")
2940
+ }) : null
1552
2941
  });
1553
2942
  const current = snapshot.current;
1554
2943
  const currentProvider = snapshot.providers.find((provider) => provider.provider === current.provider);
1555
2944
  const deepseekPeriod = deepseekPeriodAt(Date.now());
1556
2945
  const deepseekVisible = isToday && (current.provider !== void 0 && isDeepSeekProviderRoute(current.provider) || snapshot.usage.today.providers.some((row) => isDeepSeekProviderRoute(row.provider)));
1557
2946
  const planProviders = snapshot.providers.filter((provider) => isConfigured(provider) && (provider.planSupported === true || provider.planSupported === void 0 && provider.plan !== void 0));
1558
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1559
- className: usage_module_css_default.section,
1560
- "data-dsh-plugin": "usage",
1561
- children: [
1562
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1563
- className: usage_module_css_default.header,
1564
- "data-dsh-part": "header",
1565
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1566
- className: usage_module_css_default.currentProvider,
1567
- children: currentProvider !== void 0 ? `${currentProvider.displayName}${current.model !== void 0 && current.model !== "" ? " · " + current.model : ""}` : t("usage.noData")
1568
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1569
- style: {
1570
- display: "flex",
1571
- gap: 8,
1572
- alignItems: "center"
1573
- },
2947
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePage, {
2948
+ card,
2949
+ face: settingsCard,
2950
+ enabled,
2951
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2952
+ className: usage_module_css_default.section,
2953
+ children: [
2954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2955
+ className: usage_module_css_default.header,
2956
+ "data-dsh-part": "header",
1574
2957
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1575
- className: usage_module_css_default.muted,
1576
- children: t("usage.updated", { time: formatTime(snapshot.updatedAt) })
1577
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1578
- type: "button",
1579
- className: usage_module_css_default.refreshBtn,
1580
- onClick: onRefresh,
1581
- disabled: refreshing,
1582
- children: refreshing ? t("usage.refreshing") : t("usage.refresh")
2958
+ className: usage_module_css_default.currentProvider,
2959
+ children: currentProvider !== void 0 ? `${currentProvider.displayName}${current.model !== void 0 && current.model !== "" ? " · " + current.model : ""}` : null
2960
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2961
+ className: usage_module_css_default.headerActions,
2962
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2963
+ className: usage_module_css_default.muted,
2964
+ children: t("usage.updated", { time: formatTime(snapshot.updatedAt) })
2965
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2966
+ type: "button",
2967
+ className: usage_module_css_default.refreshBtn,
2968
+ onClick: onRefresh,
2969
+ disabled: refreshing,
2970
+ children: refreshing ? t("usage.refreshing") : t("usage.refresh")
2971
+ })]
1583
2972
  })]
1584
- })]
1585
- }),
1586
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1587
- className: usage_module_css_default.tabs,
1588
- role: "tablist",
1589
- "data-dsh-part": "tabs",
1590
- children: [
1591
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1592
- type: "button",
1593
- role: "tab",
1594
- "aria-selected": tab === "usage",
1595
- className: tab === "usage" ? `${usage_module_css_default.tab} ${usage_module_css_default.tabActive}` : usage_module_css_default.tab,
1596
- onClick: () => setTab("usage"),
1597
- children: t("usage.tab.usage")
1598
- }),
1599
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1600
- type: "button",
1601
- role: "tab",
1602
- "aria-selected": tab === "plans",
1603
- className: tab === "plans" ? `${usage_module_css_default.tab} ${usage_module_css_default.tabActive}` : usage_module_css_default.tab,
1604
- onClick: () => setTab("plans"),
1605
- children: t("usage.tab.plans")
1606
- }),
1607
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2973
+ }),
2974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SegmentGroup, {
2975
+ className: usage_module_css_default.tabs,
2976
+ role: "tablist",
2977
+ "aria-label": t("usage.title"),
2978
+ "data-dsh-part": "tabs",
2979
+ children: USAGE_TABS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1608
2980
  type: "button",
1609
2981
  role: "tab",
1610
- "aria-selected": tab === "bank",
1611
- className: tab === "bank" ? `${usage_module_css_default.tab} ${usage_module_css_default.tabActive}` : usage_module_css_default.tab,
1612
- onClick: () => setTab("bank"),
1613
- children: t("usage.tab.bank")
1614
- })
1615
- ]
1616
- }),
1617
- tab === "usage" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1618
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1619
- className: usage_module_css_default.card,
1620
- "data-dsh-part": "today-card",
1621
- children: [
1622
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1623
- className: usage_module_css_default.cardTitle,
1624
- children: isToday ? t("usage.today") : t("usage.day.title", { date: selectedDay ?? "" })
1625
- }),
1626
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DayPicker, {
1627
- part: "usage",
1628
- value: selectedDay ?? "",
1629
- options: dayOptions,
1630
- today: todayDate ?? "",
1631
- onSelect: (date) => {
1632
- setUsageDay(date === todayDate ? void 0 : date);
1633
- }
1634
- }),
1635
- deepseekVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PeakBadge, {
1636
- peak: deepseekPeriod.peak,
1637
- boundaryMs: deepseekPeriod.boundaryMs,
1638
- variant: "full"
1639
- }),
1640
- usageDayLoad?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1641
- className: usage_module_css_default.errorLine,
1642
- children: t("usage.day.error", { error: usageDayLoad.error ?? "" })
1643
- }) : day === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1644
- className: usage_module_css_default.muted,
1645
- children: t("usage.day.loading")
1646
- }) : day.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1647
- className: usage_module_css_default.muted,
1648
- children: isToday ? t("usage.noData") : t("usage.day.empty")
1649
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TotalsRow, { totals: day.totals }),
1650
- (day?.totals.cost ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1651
- className: usage_module_css_default.providerRow,
1652
- "data-dsh-part": "today-cost",
1653
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1654
- className: usage_module_css_default.providerName,
1655
- children: isToday ? t("usage.today.cost") : t("usage.day.cost")
1656
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1657
- className: usage_module_css_default.providerTokens,
1658
- children: formatSpend(day?.totals.cost ?? 0, snapshot.display)
1659
- })]
1660
- }),
1661
- (day?.providers.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1662
- "data-dsh-part": "provider-list",
1663
- children: (day?.providers ?? []).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2982
+ "aria-selected": tab === entry,
2983
+ className: `${settings_card_module_css_default.segment} ${usage_module_css_default.tab}`,
2984
+ "data-active": tab === entry ? "" : void 0,
2985
+ onClick: () => setTab(entry),
2986
+ children: t(`usage.tab.${entry}`)
2987
+ }, entry))
2988
+ }),
2989
+ tab === "usage" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2990
+ className: usage_module_css_default.usageSummaryGrid,
2991
+ "data-dsh-layout": "summary-grid",
2992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2993
+ className: usage_module_css_default.card,
2994
+ "data-dsh-part": "today-card",
2995
+ children: [
2996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2997
+ className: usage_module_css_default.cardTitle,
2998
+ children: isToday ? t("usage.today") : t("usage.day.title", { date: selectedDay ?? "" })
2999
+ }),
3000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DayPicker, {
3001
+ part: "usage",
3002
+ value: selectedDay ?? "",
3003
+ options: dayOptions,
3004
+ today: todayDate ?? "",
3005
+ onSelect: (date) => {
3006
+ setUsageDay(date === todayDate ? void 0 : date);
3007
+ }
3008
+ }),
3009
+ deepseekVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PeakBadge, {
3010
+ peak: deepseekPeriod.peak,
3011
+ boundaryMs: deepseekPeriod.boundaryMs,
3012
+ variant: "full"
3013
+ }),
3014
+ usageDayLoad?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3015
+ className: usage_module_css_default.errorLine,
3016
+ children: t("usage.day.error", { error: usageDayLoad.error ?? "" })
3017
+ }) : day === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3018
+ className: usage_module_css_default.muted,
3019
+ children: t("usage.day.loading")
3020
+ }) : day.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3021
+ className: usage_module_css_default.muted,
3022
+ children: isToday ? t("usage.noData") : t("usage.day.empty")
3023
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TotalsRow, { totals: day.totals }),
3024
+ (day?.totals.cost ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1664
3025
  className: usage_module_css_default.providerRow,
1665
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3026
+ "data-dsh-part": "today-cost",
3027
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1666
3028
  className: usage_module_css_default.providerName,
1667
- children: [snapshot.providers.find((provider) => provider.provider === row.provider)?.displayName ?? row.provider, current.provider === row.provider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1668
- className: usage_module_css_default.currentBadge,
1669
- children: t("usage.current")
1670
- })]
1671
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3029
+ children: isToday ? t("usage.today.cost") : t("usage.day.cost")
3030
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1672
3031
  className: usage_module_css_default.providerTokens,
1673
- children: [
1674
- formatTokens(row.totals.inputTokens + row.totals.cacheReadTokens + row.totals.cacheWriteTokens + row.totals.outputTokens),
1675
- " · ",
1676
- t("usage.calls", { n: row.totals.calls }),
1677
- row.totals.cost > 0 ? ` · ${formatSpend(row.totals.cost, snapshot.display)}` : ""
1678
- ]
3032
+ children: formatSpend(day?.totals.cost ?? 0, snapshot.display)
1679
3033
  })]
1680
- }, row.provider))
1681
- })
1682
- ]
1683
- }),
1684
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1685
- className: usage_module_css_default.card,
1686
- "data-dsh-part": "balance-card",
1687
- children: [
1688
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1689
- className: usage_module_css_default.cardTitle,
1690
- children: t("usage.balance")
1691
- }),
1692
- (() => {
1693
- const configured = snapshot.providers.filter(isConfigured);
1694
- const rows = configured.filter((provider) => provider.balanceSupported === true || provider.balanceSupported === false || provider.balanceSupported === void 0 && provider.supported);
1695
- if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1696
- className: usage_module_css_default.muted,
1697
- children: configured.length === 0 ? t("usage.balance.noneConfigured") : t("usage.balance.unsupported")
1698
- });
1699
- return rows.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderRow, {
1700
- provider,
1701
- current: current.provider
1702
- }, provider.provider));
1703
- })(),
1704
- snapshot.providers.some((provider) => isConfigured(provider) && provider.error !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1705
- className: usage_module_css_default.errorLine,
1706
- children: snapshot.providers.filter((provider) => isConfigured(provider) && provider.error !== void 0).map((provider) => `${provider.displayName}: ${t("usage.provider.error", { error: provider.error ?? "" })}`).join(t("usage.errorListSeparator"))
1707
- })
1708
- ]
1709
- }),
1710
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeCard, {
3034
+ }),
3035
+ (day?.providers.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3036
+ "data-dsh-part": "provider-list",
3037
+ children: (day?.providers ?? []).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3038
+ className: usage_module_css_default.providerRow,
3039
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3040
+ className: usage_module_css_default.providerName,
3041
+ children: [snapshot.providers.find((provider) => provider.provider === row.provider)?.displayName ?? row.provider, current.provider === row.provider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3042
+ className: usage_module_css_default.currentBadge,
3043
+ children: t("usage.current")
3044
+ })]
3045
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3046
+ className: usage_module_css_default.providerTokens,
3047
+ children: [
3048
+ formatTokens(row.totals.inputTokens + row.totals.cacheReadTokens + row.totals.cacheWriteTokens + row.totals.outputTokens),
3049
+ " · ",
3050
+ t("usage.calls", { n: row.totals.calls }),
3051
+ row.totals.cost > 0 ? ` · ${formatSpend(row.totals.cost, snapshot.display)}` : ""
3052
+ ]
3053
+ })]
3054
+ }, row.provider))
3055
+ })
3056
+ ]
3057
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3058
+ className: usage_module_css_default.card,
3059
+ "data-dsh-part": "balance-card",
3060
+ children: [
3061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3062
+ className: usage_module_css_default.cardTitle,
3063
+ children: t("usage.balance")
3064
+ }),
3065
+ (() => {
3066
+ const configured = snapshot.providers.filter(isConfigured);
3067
+ const rows = configured.filter((provider) => provider.balanceSupported === true || provider.balanceSupported === false || provider.balanceSupported === void 0 && provider.supported);
3068
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3069
+ className: usage_module_css_default.muted,
3070
+ children: configured.length === 0 ? t("usage.balance.noneConfigured") : t("usage.balance.unsupported")
3071
+ });
3072
+ return rows.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderRow, {
3073
+ provider,
3074
+ current: current.provider
3075
+ }, provider.provider));
3076
+ })(),
3077
+ snapshot.providers.some((provider) => isConfigured(provider) && provider.error !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3078
+ className: usage_module_css_default.errorLine,
3079
+ children: snapshot.providers.filter((provider) => isConfigured(provider) && provider.error !== void 0).map((provider) => `${provider.displayName}: ${t("usage.provider.error", { error: provider.error ?? "" })}`).join(t("usage.errorListSeparator"))
3080
+ })
3081
+ ]
3082
+ })]
3083
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeCard, {
1711
3084
  range: snapshot.usage.range,
1712
3085
  providers: snapshot.providers,
1713
3086
  currentProvider: current.provider
1714
- }),
1715
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
1716
- settings,
1717
- snapshot: settingsSnapshot.status === "ready" ? settingsSnapshot : void 0,
1718
- value: settingsValue
1719
- })
1720
- ] }),
1721
- tab === "plans" && (planProviders.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1722
- className: usage_module_css_default.card,
1723
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1724
- className: usage_module_css_default.muted,
1725
- children: t("usage.plan.noneConfigured")
1726
- })
1727
- }) : planProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanCard, {
1728
- provider,
1729
- current: current.provider
1730
- }, provider.provider))),
1731
- tab === "bank" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DayPicker, {
1732
- part: "bank",
1733
- value: bankSelection ?? BANK_ALL_DAYS,
1734
- options: dayOptions,
1735
- today: todayDate ?? "",
1736
- leading: {
1737
- value: BANK_ALL_DAYS,
1738
- label: t("usage.bank.windowAll")
1739
- },
1740
- onSelect: (date) => {
1741
- setBankDay(date === BANK_ALL_DAYS ? void 0 : date);
1742
- }
1743
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoucherCard, {
1744
- window: bankWindow,
1745
- observedSpend: bankSelection === void 0 ? snapshot.usage.observedSpend : void 0,
1746
- loading: bankSelection !== void 0 && bankDayLoad?.status === "loading",
1747
- display: snapshot.display,
1748
- ...bankDayLoad?.status === "error" ? { error: bankDayLoad.error ?? "" } : {}
1749
- })] })
1750
- ]
3087
+ })] }),
3088
+ tab === "plans" && (planProviders.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3089
+ className: usage_module_css_default.card,
3090
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3091
+ className: usage_module_css_default.muted,
3092
+ children: t("usage.plan.noneConfigured")
3093
+ })
3094
+ }) : planProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanCard, {
3095
+ provider,
3096
+ current: current.provider
3097
+ }, provider.provider))),
3098
+ tab === "bank" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DayPicker, {
3099
+ part: "bank",
3100
+ value: bankSelection ?? BANK_ALL_DAYS,
3101
+ options: dayOptions,
3102
+ today: todayDate ?? "",
3103
+ leading: {
3104
+ value: BANK_ALL_DAYS,
3105
+ label: t("usage.bank.windowAll")
3106
+ },
3107
+ onSelect: (date) => {
3108
+ setBankDay(date === BANK_ALL_DAYS ? void 0 : date);
3109
+ }
3110
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoucherCard, {
3111
+ window: bankWindow,
3112
+ observedSpend: bankSelection === void 0 ? snapshot.usage.observedSpend : void 0,
3113
+ loading: bankSelection !== void 0 && bankDayLoad?.status === "loading",
3114
+ display: snapshot.display,
3115
+ ...bankDayLoad?.status === "error" ? { error: bankDayLoad.error ?? "" } : {}
3116
+ })] })
3117
+ ]
3118
+ })
1751
3119
  });
1752
3120
  }
3121
+ /** The statistics tabs, in their order on the page. */
3122
+ const USAGE_TABS = [
3123
+ "usage",
3124
+ "plans",
3125
+ "bank"
3126
+ ];
1753
3127
  /** How many model sub-bars render under one provider bar. */
1754
3128
  const CHART_MODEL_CAP = 3;
1755
3129
  /**
@@ -1776,6 +3150,7 @@ window.__ModuleLoader__.load({
1776
3150
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1777
3151
  className: usage_module_css_default.chart,
1778
3152
  "data-dsh-part": "usage-chart",
3153
+ "data-dsh-layout": "trend-grid",
1779
3154
  children: [range.providers.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartProviderRow, {
1780
3155
  row,
1781
3156
  name: nameOf(row.provider),
@@ -1794,6 +3169,7 @@ window.__ModuleLoader__.load({
1794
3169
  const maxModel = Math.max(1, ...row.models.slice(0, CHART_MODEL_CAP).map((model) => totalOf(model.totals)));
1795
3170
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1796
3171
  className: usage_module_css_default.chartProvider,
3172
+ "data-dsh-layout": "trend-provider",
1797
3173
  children: [
1798
3174
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1799
3175
  className: usage_module_css_default.chartHead,
@@ -2045,109 +3421,6 @@ window.__ModuleLoader__.load({
2045
3421
  ]
2046
3422
  });
2047
3423
  }
2048
- /**
2049
- * The compact settings row. Both controls write through the shared form the
2050
- * moment the user changes them, and the Host answers each write with a
2051
- * boolean: a refused (or transport-failed) write is surfaced as a failed save,
2052
- * because a value that did not land must never read as applied.
2053
- */
2054
- function SettingsRow(props) {
2055
- const { settings, snapshot, value } = props;
2056
- const disabled = snapshot === void 0 || !snapshot.writable;
2057
- const [failure, setFailure] = (0, react.useState)(void 0);
2058
- const write = (field, next) => {
2059
- setFailure(void 0);
2060
- let answer;
2061
- try {
2062
- answer = settings.set(field, next);
2063
- } catch (error) {
2064
- setFailure(error instanceof Error ? error.message : String(error));
2065
- return;
2066
- }
2067
- Promise.resolve(answer).then((accepted) => {
2068
- if (!accepted) setFailure("");
2069
- }, (error) => {
2070
- setFailure(error instanceof Error ? error.message : String(error));
2071
- });
2072
- };
2073
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2074
- className: usage_module_css_default.card,
2075
- "data-dsh-part": "settings-row",
2076
- children: [
2077
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2078
- className: usage_module_css_default.cardTitle,
2079
- children: t("usage.config.title")
2080
- }),
2081
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2082
- className: usage_module_css_default.settingsGrid,
2083
- children: [
2084
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2085
- className: usage_module_css_default.settingItem,
2086
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2087
- type: "checkbox",
2088
- checked: value.enabled ?? true,
2089
- disabled,
2090
- onChange: (event) => {
2091
- write("enabled", event.target.checked);
2092
- }
2093
- }), t("usage.config.enabled")]
2094
- }),
2095
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2096
- className: usage_module_css_default.settingItem,
2097
- children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2098
- type: "number",
2099
- min: 30,
2100
- max: 3600,
2101
- value: typeof value.pollIntervalSec === "number" ? value.pollIntervalSec : 60,
2102
- disabled,
2103
- onChange: (event) => {
2104
- const parsed = Number(event.target.value);
2105
- if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) write("pollIntervalSec", Math.round(parsed));
2106
- }
2107
- })]
2108
- }),
2109
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2110
- className: usage_module_css_default.settingItem,
2111
- children: [t("usage.config.spendCurrency"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2112
- value: value.spendCurrency === "USD" ? "USD" : "CNY",
2113
- disabled,
2114
- onChange: (event) => {
2115
- write("spendCurrency", event.target.value === "USD" ? "USD" : "CNY");
2116
- },
2117
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2118
- value: "CNY",
2119
- children: "CNY"
2120
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2121
- value: "USD",
2122
- children: "USD"
2123
- })]
2124
- })]
2125
- }),
2126
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2127
- className: usage_module_css_default.settingItem,
2128
- children: [t("usage.config.cnyPerUsd"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2129
- type: "number",
2130
- min: .01,
2131
- max: 1e3,
2132
- step: .01,
2133
- value: typeof value.cnyPerUsd === "number" ? value.cnyPerUsd : 7.2,
2134
- disabled,
2135
- onChange: (event) => {
2136
- const parsed = Number(event.target.value);
2137
- if (Number.isFinite(parsed) && parsed >= .01 && parsed <= 1e3) write("cnyPerUsd", Math.round(parsed * 100) / 100);
2138
- }
2139
- })]
2140
- })
2141
- ]
2142
- }),
2143
- failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2144
- className: usage_module_css_default.errorLine,
2145
- role: "status",
2146
- children: [t("usage.config.saveFailed"), failure === "" ? "" : " - " + failure]
2147
- })
2148
- ]
2149
- });
2150
- }
2151
3424
  //#endregion
2152
3425
  //#region src/client/body-mutations.ts
2153
3426
  /** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
@@ -2739,6 +4012,140 @@ window.__ModuleLoader__.load({
2739
4012
  };
2740
4013
  }
2741
4014
  //#endregion
4015
+ //#region src/client/web-extension-page.ts
4016
+ const SETTINGS_SECTION = "settings.section";
4017
+ const WEB_EXTENSION_SLOT = "web-ui.extension";
4018
+ function serviceOf(ctx) {
4019
+ if (typeof ctx.get !== "function") return void 0;
4020
+ try {
4021
+ const candidate = ctx.get("webUiExtensions");
4022
+ return candidate !== null && typeof candidate === "object" && typeof candidate.sectionId === "string" && typeof candidate.registerPage === "function" ? candidate : void 0;
4023
+ } catch {
4024
+ return;
4025
+ }
4026
+ }
4027
+ function warn(seat, error) {
4028
+ try {
4029
+ console.warn(`[dsh-web-settings] page "${seat}" could not be mounted`, error);
4030
+ } catch {}
4031
+ }
4032
+ /**
4033
+ * Keep exactly one reachable page contribution while the parent loads or
4034
+ * unloads. The parent service owns integrated mounting; only the fallback uses
4035
+ * this plugin's own slot registry. Slot disposal is synchronous, so the latch
4036
+ * prevents a move from recursively registering a duplicate.
4037
+ */
4038
+ function installWebExtensionPage(ctx, page) {
4039
+ let dispose;
4040
+ let current;
4041
+ let reconciling = false;
4042
+ let disposed = false;
4043
+ const present = (seat) => {
4044
+ if (typeof ctx.slots.entries !== "function") return current === seat;
4045
+ const slot = seat === "integrated" ? WEB_EXTENSION_SLOT : SETTINGS_SECTION;
4046
+ return ctx.slots.entries(slot).some((entry) => entry.component === page.component && (seat === "integrated" ? entry.options.key : entry.options.id) === page.key);
4047
+ };
4048
+ const registerSeat = (seat) => {
4049
+ if (seat === "integrated") {
4050
+ const service = serviceOf(ctx);
4051
+ if (service === void 0) throw new Error("webUiExtensions service is unavailable");
4052
+ return service.registerPage(page);
4053
+ }
4054
+ return ctx.slots.register({
4055
+ name: SETTINGS_SECTION,
4056
+ id: page.key,
4057
+ order: page.order,
4058
+ label: page.label,
4059
+ locale: page.locale,
4060
+ inject: page.inject,
4061
+ ...page.children === void 0 ? {} : { children: page.children }
4062
+ }, page.component);
4063
+ };
4064
+ const reconcile = () => {
4065
+ if (disposed || reconciling) return;
4066
+ const target = serviceOf(ctx) === void 0 ? "legacy" : "integrated";
4067
+ if (current === target && present(target)) return;
4068
+ const slot = target === "integrated" ? WEB_EXTENSION_SLOT : SETTINGS_SECTION;
4069
+ if (typeof ctx.slots.spec === "function" && ctx.slots.spec(slot) === void 0) return;
4070
+ reconciling = true;
4071
+ const previous = current;
4072
+ const disposePrevious = dispose;
4073
+ current = void 0;
4074
+ dispose = void 0;
4075
+ disposePrevious?.();
4076
+ try {
4077
+ dispose = registerSeat(target);
4078
+ current = target;
4079
+ } catch (error) {
4080
+ warn(target, error);
4081
+ if (previous !== void 0 && previous !== target) try {
4082
+ dispose = registerSeat(previous);
4083
+ current = previous;
4084
+ } catch (restoreError) {
4085
+ warn(previous, restoreError);
4086
+ }
4087
+ } finally {
4088
+ reconciling = false;
4089
+ }
4090
+ };
4091
+ if (typeof ctx.on === "function") try {
4092
+ ctx.on("slots/changed", reconcile);
4093
+ } catch {}
4094
+ reconcile();
4095
+ return () => {
4096
+ if (disposed) return;
4097
+ disposed = true;
4098
+ dispose?.();
4099
+ dispose = void 0;
4100
+ current = void 0;
4101
+ };
4102
+ }
4103
+ //#endregion
4104
+ //#region src/client/section-integration.ts
4105
+ /**
4106
+ * Optional integration with the family Web Extensions parent.
4107
+ *
4108
+ * The parent service owns page mounting; this module contributes one page
4109
+ * contract and keeps the original first-level section as the standalone
4110
+ * fallback. Deep links still resolve the parent service first.
4111
+ */
4112
+ const USAGE_SECTION_ID = "dsh-usage";
4113
+ /** Read the optional service without assuming a particular cordis test double. */
4114
+ function getWebUiExtensions(ctx) {
4115
+ if (typeof ctx.get !== "function") return void 0;
4116
+ try {
4117
+ const service = ctx.get("webUiExtensions");
4118
+ return service?.sectionId === "web-ui-plugins" && typeof service.label === "function" && typeof service.open === "function" ? service : void 0;
4119
+ } catch {
4120
+ return;
4121
+ }
4122
+ }
4123
+ /** Register the one usage page; disposal is owned by the calling plugin fiber. */
4124
+ function installUsageSection(ctx, registration) {
4125
+ return installWebExtensionPage(ctx, {
4126
+ key: USAGE_SECTION_ID,
4127
+ order: registration.order,
4128
+ locale: registration.locale,
4129
+ label: registration.label,
4130
+ inject: registration.inject,
4131
+ component: registration.component
4132
+ });
4133
+ }
4134
+ /**
4135
+ * Open usage through the family service first. If it declines, fall back to the
4136
+ * native settings shell on the family section (or on the original usage row
4137
+ * when the service is absent), preserving the matching localized nav label.
4138
+ */
4139
+ function openIntegratedUsageSettings(ctx) {
4140
+ const service = getWebUiExtensions(ctx);
4141
+ if (service?.open("dsh-usage") === true) return;
4142
+ openUsageSettings({
4143
+ id: service?.sectionId ?? "dsh-usage",
4144
+ label: service === void 0 ? () => t("usage.title") : () => service.label(),
4145
+ store: settingsShellStore(ctx.slots)
4146
+ });
4147
+ }
4148
+ //#endregion
2742
4149
  //#region src/client/settings-entry-form.ts
2743
4150
  /** The snapshot a form reports before the Host has answered with this entry. */
2744
4151
  function pendingSnapshot() {
@@ -2857,8 +4264,6 @@ window.__ModuleLoader__.load({
2857
4264
  ];
2858
4265
  /** First-level nav position: directly below the Workshop section (order 150). */
2859
4266
  const SECTION_ORDER = 151;
2860
- /** The section's nav row id — what the shell's store selects when the card opens. */
2861
- const USAGE_SECTION_ID = "dsh-usage";
2862
4267
  /** Required services. */
2863
4268
  const inject = [
2864
4269
  "slots",
@@ -2914,23 +4319,24 @@ window.__ModuleLoader__.load({
2914
4319
  });
2915
4320
  };
2916
4321
  const loadDay = (date) => usageApi.day(date);
4322
+ const settingsCard = new UsageSettingsCardController(settingsForm);
4323
+ ctx.effect(() => () => {
4324
+ settingsCard.dispose();
4325
+ }, "dsh-usage: settings card");
4326
+ const settingsCardFace = settingsCard.inject();
2917
4327
  const face = () => ({
2918
4328
  store,
2919
4329
  poll,
2920
4330
  refresh,
2921
4331
  loadDay,
2922
- settings: settingsForm
4332
+ settingsCard: settingsCardFace
2923
4333
  });
2924
4334
  const disposeFootCard = mountUsageFootCard({
2925
4335
  store,
2926
4336
  poll,
2927
4337
  settings: settingsForm,
2928
4338
  onOpen: () => {
2929
- openUsageSettings({
2930
- id: USAGE_SECTION_ID,
2931
- label: () => t("usage.title"),
2932
- store: settingsShellStore(ctx.slots)
2933
- });
4339
+ openIntegratedUsageSettings(ctx);
2934
4340
  },
2935
4341
  locale: ctx.locale
2936
4342
  });
@@ -2939,23 +4345,13 @@ window.__ModuleLoader__.load({
2939
4345
  disposeFootCard();
2940
4346
  } catch {}
2941
4347
  }, "dsh-usage: sidebar foot card");
2942
- ctx.slots.inject("settings.section", () => {
2943
- try {
2944
- const unregister = ctx.slots.register({
2945
- name: "settings.section",
2946
- id: USAGE_SECTION_ID,
2947
- order: SECTION_ORDER,
2948
- label: () => ctx.locale.bind(NS)("usage.title"),
2949
- locale: NS,
2950
- inject: face
2951
- }, UsageSectionCard);
2952
- return () => {
2953
- unregister();
2954
- };
2955
- } catch {
2956
- return () => {};
2957
- }
2958
- });
4348
+ ctx.effect(() => installUsageSection(ctx, {
4349
+ order: SECTION_ORDER,
4350
+ label: () => ctx.locale.bind(NS)("usage.title"),
4351
+ locale: NS,
4352
+ inject: face,
4353
+ component: UsageSectionCard
4354
+ }), "dsh-usage: settings page");
2959
4355
  }
2960
4356
  //#endregion
2961
4357
  exports.apply = apply;