@linxin666/dsh-usage 0.4.9 → 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/README.i18n.yaml +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +1762 -366
- package/lib/client.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +251 -0
- package/lib/types/client/RangeBubble.d.ts +46 -0
- package/lib/types/client/SegmentGroup.d.ts +28 -0
- package/lib/types/client/UsagePage.d.ts +19 -0
- package/lib/types/client/UsageSectionCard.d.ts +11 -18
- package/lib/types/client/locales.d.ts +13 -2
- package/lib/types/client/section-integration.d.ts +47 -0
- package/lib/types/client/settings-form.d.ts +235 -0
- package/lib/types/client/usage-settings-card.d.ts +48 -0
- package/lib/types/client/web-extension-page.d.ts +50 -0
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +817 -0
- package/src/client/RangeBubble.tsx +164 -0
- package/src/client/SegmentGroup.tsx +93 -0
- package/src/client/UsagePage.tsx +108 -0
- package/src/client/UsageSectionCard.tsx +173 -270
- package/src/client/index.ts +19 -30
- package/src/client/locales.ts +46 -24
- package/src/client/section-integration.ts +88 -0
- package/src/client/settings-card.module.css +821 -0
- package/src/client/settings-form.ts +510 -0
- package/src/client/usage-settings-card.ts +72 -0
- package/src/client/usage.module.css +74 -82
- package/src/client/web-extension-page.ts +151 -0
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": "
|
|
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": "
|
|
102
|
-
"usage.config.saveFailed": "保存失败:设置未被接受。",
|
|
99
|
+
"usage.config.spendCurrency": "货币",
|
|
100
|
+
"usage.config.cnyPerUsd": "汇率",
|
|
103
101
|
"usage.bank.title": "鲸元券",
|
|
104
|
-
"usage.bank.hint": "
|
|
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": "
|
|
140
|
+
"usage.loading": "Reading usage…",
|
|
130
141
|
"usage.error": "Failed to load: {error}",
|
|
131
|
-
"usage.disabled": "
|
|
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
|
|
166
|
+
"usage.noData": "No usage yet; counting starts once the plugin is on.",
|
|
156
167
|
"usage.balance": "Balance",
|
|
157
|
-
"usage.balance.unsupported": "
|
|
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
|
|
163
|
-
"usage.plan.noneConfigured": "No plan
|
|
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": "
|
|
173
|
-
"usage.config.cnyPerUsd": "Exchange rate
|
|
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
|
|
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:
|
|
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
|
-
"
|
|
272
|
-
"
|
|
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
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
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,
|
|
2876
|
+
const { store, poll, refresh, loadDay, settingsCard } = props;
|
|
1482
2877
|
const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
|
|
1483
|
-
const
|
|
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
|
|
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.
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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:
|
|
1547
|
-
})
|
|
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.
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
className: usage_module_css_default.
|
|
1567
|
-
|
|
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.
|
|
1576
|
-
children:
|
|
1577
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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 ===
|
|
1611
|
-
className:
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
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:
|
|
1668
|
-
|
|
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
|
-
},
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
provider
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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)(
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
4332
|
+
settingsCard: settingsCardFace
|
|
2923
4333
|
});
|
|
2924
4334
|
const disposeFootCard = mountUsageFootCard({
|
|
2925
4335
|
store,
|
|
2926
4336
|
poll,
|
|
2927
4337
|
settings: settingsForm,
|
|
2928
4338
|
onOpen: () => {
|
|
2929
|
-
|
|
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.
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
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;
|