@cassius0924/dsh-usage-dashboard 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -363,17 +363,24 @@ var zh = {
363
363
  "export.jsonHint": "\u542B\u56FE\u8868\u4E0E\u6392\u884C",
364
364
  "cache.empty": "\u6682\u65E0\u7F13\u5B58\u547D\u4E2D\u6570\u636E\u3002",
365
365
  "cache.rate": "\u7F13\u5B58\u547D\u4E2D\u7387",
366
- "cache.saved": "\u5DF2\u8282\u7701\u8D39\u7528\uFF08\u4F30\u7B97\uFF09",
367
- "cache.allMiss": "\u82E5\u5168\u90E8\u672A\u547D\u4E2D",
366
+ "cache.saved": "\u8F83\u5168\u672A\u547D\u4E2D\u7701\u4E0B",
367
+ "cache.allMiss": "\u82E5\u5168\u672A\u547D\u4E2D",
368
368
  "cache.aria": "\u7F13\u5B58\u547D\u4E2D {hit}%\uFF0C\u672A\u547D\u4E2D {miss}%",
369
369
  "cache.hitLegend": "\u7F13\u5B58\u547D\u4E2D {tokens}",
370
370
  "cache.missLegend": "\u672A\u547D\u4E2D {tokens}\uFF08\u6309\u672A\u547D\u4E2D\u4EF7\u8BA1\u8D39\uFF09",
371
+ "cache.basis": "\u5B9E\u4ED8 \xA5{cost}\uFF1B\u82E5\u5168\u90E8\u672A\u547D\u4E2D\u7EA6 \xA5{miss}\uFF0C\u4E24\u8005\u5DEE\u989D\u5373\u547D\u4E2D\u7F13\u5B58\u7701\u4E0B\u7684\u8D39\u7528\u3002",
371
372
  "peak.empty": "\u6682\u65E0\u7528\u91CF\u3002",
372
373
  "peak.aria": "\u9AD8\u5CF0\u65F6\u6BB5 {peak}%\uFF0C\u95F2\u65F6 {offPeak}%",
373
374
  "peak.peakLegend": "\u9AD8\u5CF0 {share}% \xB7 {tokens} tokens \xB7 {calls}",
374
375
  "peak.offPeakLegend": "\u95F2\u65F6 {share}% \xB7 {tokens} tokens \xB7 {calls}",
375
- "peak.shift": "\u82E5\u9AD8\u5CF0\u7528\u91CF\u90FD\u632A\u5230\u95F2\u65F6",
376
- "peak.saving": "\u53EF\u7701 \xA5{amount}",
376
+ "peak.shift": "\u9AD8\u5CF0\u6539\u8D70\u95F2\u65F6\u53EF\u7701",
377
+ "peak.saving": "\xA5{amount}",
378
+ "peak.basis": "\u5168\u90E8\u6539\u8D70\u95F2\u65F6\u7EA6 \xA5{total}\uFF1B\u6B64\u5904\u6309\u5F53\u524D\u4EF7\u76EE\u4F30\u7B97\uFF0C\u4E0E\u300C\u6D88\u8017\u6982\u89C8 \xB7 \u7D2F\u8BA1\u300D\u7684\u5B9E\u9645\u82B1\u8D39\u53E3\u5F84\u4E0D\u540C\u3002",
379
+ "peak.clockTitle": "\u4ECA\u65E5\u4EF7\u683C\u65F6\u6BB5",
380
+ "peak.nowPeak": "\u73B0\u5728\u9AD8\u5CF0 \xB7 \u5168\u4EF7",
381
+ "peak.nowOff": "\u73B0\u5728\u95F2\u65F6 \xB7 \u534A\u4EF7",
382
+ "peak.clockAria": "24 \u5C0F\u65F6\u4EF7\u683C\u65F6\u6BB5\uFF0C{state}",
383
+ "peak.clockHint": "\u5DE5\u4F5C\u65E5 09\u201312\u300114\u201318 \u4E3A\u9AD8\u5CF0\uFF0C\u5176\u4F59\u65F6\u6BB5\u534A\u4EF7\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09",
377
384
  "vision.empty": "{window}\u6CA1\u6709\u56FE\u7247\u8F93\u5165\u3002",
378
385
  "vision.title": "\u56FE\u7247 / \u591A\u6A21\u6001\u7528\u91CF",
379
386
  "vision.images": "\u56FE\u7247\u5F20\u6570",
@@ -446,7 +453,7 @@ var zh = {
446
453
  "sync.first": "\u9996\u6B21\u540C\u6B65\u4E2D",
447
454
  "sync.syncing": "\u540C\u6B65\u4E2D \xB7 \u5F53\u524D\u6570\u636E{age}",
448
455
  "sync.fresh": "\u5DF2\u540C\u6B65 \xB7 {age}",
449
- "sync.cached": "\u7F13\u5B58\u6570\u636E \xB7 {age}",
456
+ "sync.cached": "\u672C\u5730\u7F13\u5B58 \xB7 {age}",
450
457
  "sync.fallback": "\u7F13\u5B58\u56DE\u9000 \xB7 {age}",
451
458
  "sync.error": "\u540C\u6B65\u5931\u8D25 \xB7 \u6682\u65E0\u53EF\u663E\u793A\u7684\u7528\u91CF",
452
459
  "status.neverSynced": "\u8FD8\u6CA1\u6709\u6210\u529F\u540C\u6B65\u8FC7\u7528\u91CF\u6570\u636E",
@@ -481,6 +488,7 @@ var zh = {
481
488
  "balance.granted": "\u8D60\u9001\u989D\u5EA6 {amount}",
482
489
  "balance.runway": "\u9884\u8BA1\u53EF\u7528",
483
490
  "balance.runwayBreakdownTitle": "\u67E5\u770B\u9884\u8BA1\u53EF\u7528\u5929\u6570\u8BF4\u660E",
491
+ "balance.topUpTitle": "\u4F59\u989D\u504F\u4F4E\uFF0C\u524D\u5F80 DeepSeek \u5F00\u653E\u5E73\u53F0\u5145\u503C\uFF08\u65B0\u6807\u7B7E\u9875\u6253\u5F00\uFF09",
484
492
  "balance.days": "{count} \u5929",
485
493
  "balance.overYear": "> 365 \u5929",
486
494
  "balance.noRunway": "\u8FD1 {days} \u5929\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u6CD5\u4F30\u7B97",
@@ -510,10 +518,23 @@ var zh = {
510
518
  "models.title": "\u6A21\u578B\u6210\u672C\u6392\u884C",
511
519
  "sessions.title": "\u4F1A\u8BDD\u6210\u672C\u6392\u884C",
512
520
  "links.title": "\u5FEB\u6377\u5165\u53E3",
521
+ "nav.label": "\u9875\u9762\u5927\u7EB2",
522
+ "nav.balance": "\u4F59\u989D",
523
+ "nav.session": "\u5F53\u524D\u4F1A\u8BDD",
524
+ "nav.overview": "\u6D88\u8017\u6982\u89C8",
525
+ "nav.trend": "\u7528\u91CF\u8D8B\u52BF",
526
+ "nav.heatmap": "\u7528\u91CF\u70ED\u529B\u56FE",
527
+ "nav.models": "\u6A21\u578B\u6210\u672C\u6392\u884C",
528
+ "nav.sessions": "\u4F1A\u8BDD\u6210\u672C\u6392\u884C",
529
+ "nav.peakcache": "\u9AD8\u5CF0 / \u7F13\u5B58",
530
+ "nav.settings": "\u8BBE\u7F6E",
513
531
  "links.usage": "\u67E5\u770B\u989D\u5EA6 / \u7528\u91CF",
514
532
  "links.apiKey": "\u751F\u6210 API Key",
515
533
  "links.status": "\u670D\u52A1\u72B6\u6001",
516
534
  "settings.title": "\u8BBE\u7F6E",
535
+ "settings.groupDisplay": "\u663E\u793A",
536
+ "settings.groupAlerts": "\u63D0\u9192",
537
+ "settings.groupLayout": "\u5E03\u5C40\u4E0E\u5165\u53E3",
517
538
  "settings.widget": "\u5728\u5176\u4ED6\u9875\u9762\u663E\u793A\u53F3\u4E0B\u89D2\u60AC\u6D6E\u989D\u5EA6\u7A97\u53E3",
518
539
  "settings.widgetHint": "\u989D\u5EA6\u9875\u81EA\u52A8\u9690\u85CF\uFF1B\u5176\u4ED6\u9875\u9762\u6309\u4E0B\u65B9\u9009\u62E9\u663E\u793A\u3002",
519
540
  "settings.widgetTabs": "\u5C55\u793A\u6D6E\u7A97\u7684\u9875\u9762",
@@ -634,17 +655,24 @@ var en = {
634
655
  "export.jsonHint": "Charts and rankings included",
635
656
  "cache.empty": "No cache data yet.",
636
657
  "cache.rate": "Cache hit rate",
637
- "cache.saved": "Estimated savings",
638
- "cache.allMiss": "Cost with no hits",
658
+ "cache.saved": "Saved vs. all-miss",
659
+ "cache.allMiss": "Cost if all miss",
639
660
  "cache.aria": "{hit}% cache hits, {miss}% misses",
640
661
  "cache.hitLegend": "Cache hits {tokens}",
641
662
  "cache.missLegend": "Misses {tokens} (charged at miss rate)",
663
+ "cache.basis": "You paid \xA5{cost}; with no cache hits it would be about \xA5{miss}. The difference is what caching saved.",
642
664
  "peak.empty": "No usage yet.",
643
665
  "peak.aria": "{peak}% peak, {offPeak}% off-peak",
644
666
  "peak.peakLegend": "Peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
645
667
  "peak.offPeakLegend": "Off-peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
646
- "peak.shift": "If all peak usage moved off-peak",
647
- "peak.saving": "Save \xA5{amount}",
668
+ "peak.shift": "Saving if peak runs off-peak",
669
+ "peak.saving": "\xA5{amount}",
670
+ "peak.basis": "All off-peak would cost about \xA5{total}, estimated at current prices \u2014 a different basis from \u201CSpend overview \xB7 All time\u201D.",
671
+ "peak.clockTitle": "Today's price windows",
672
+ "peak.nowPeak": "Peak now \xB7 full price",
673
+ "peak.nowOff": "Off-peak now \xB7 half price",
674
+ "peak.clockAria": "24-hour price schedule, {state}",
675
+ "peak.clockHint": "Peak 09\u201312 & 14\u201318 on weekdays; half price otherwise (Beijing time)",
648
676
  "vision.empty": "No image input in {window}.",
649
677
  "vision.title": "Image / multimodal usage",
650
678
  "vision.images": "Images",
@@ -717,7 +745,7 @@ var en = {
717
745
  "sync.first": "Syncing for the first time",
718
746
  "sync.syncing": "Syncing \xB7 current data {age}",
719
747
  "sync.fresh": "Synced \xB7 {age}",
720
- "sync.cached": "Cached data \xB7 {age}",
748
+ "sync.cached": "Local cache \xB7 {age}",
721
749
  "sync.fallback": "Using cache \xB7 {age}",
722
750
  "sync.error": "Sync failed \xB7 no usage to display",
723
751
  "status.neverSynced": "Usage has not synced successfully yet",
@@ -752,6 +780,7 @@ var en = {
752
780
  "balance.granted": "Granted {amount}",
753
781
  "balance.runway": "Estimated runway",
754
782
  "balance.runwayBreakdownTitle": "View estimated runway details",
783
+ "balance.topUpTitle": "Low balance \u2014 top up on the DeepSeek platform (opens a new tab)",
755
784
  "balance.days": "{count} days",
756
785
  "balance.overYear": "> 365 days",
757
786
  "balance.noRunway": "No usage in the last {days} days, so runway cannot be estimated",
@@ -781,10 +810,23 @@ var en = {
781
810
  "models.title": "Model cost ranking",
782
811
  "sessions.title": "Session cost ranking",
783
812
  "links.title": "Quick Links",
813
+ "nav.label": "On this page",
814
+ "nav.balance": "Balance",
815
+ "nav.session": "This session",
816
+ "nav.overview": "Spend overview",
817
+ "nav.trend": "Usage trend",
818
+ "nav.heatmap": "Usage heatmap",
819
+ "nav.models": "Model cost",
820
+ "nav.sessions": "Session cost",
821
+ "nav.peakcache": "Peak & cache",
822
+ "nav.settings": "Settings",
784
823
  "links.usage": "View balance / usage",
785
824
  "links.apiKey": "Create API key",
786
825
  "links.status": "Service status",
787
826
  "settings.title": "Settings",
827
+ "settings.groupDisplay": "Display",
828
+ "settings.groupAlerts": "Alerts",
829
+ "settings.groupLayout": "Layout & entry",
788
830
  "settings.widget": "Show the floating balance widget on other views",
789
831
  "settings.widgetHint": "Hidden on Usage; shown on the views selected below.",
790
832
  "settings.widgetTabs": "Show widget on",
@@ -837,6 +879,13 @@ var en = {
837
879
 
838
880
  // src/client/i18n.tsx
839
881
  var import_jsx_runtime2 = require("react/jsx-runtime");
882
+ var INTL_LOCALES = {
883
+ zh: "zh-CN",
884
+ en: "en-US"
885
+ };
886
+ function intlLocale(locale) {
887
+ return INTL_LOCALES[locale];
888
+ }
840
889
  var fallbackT = (key, params) => {
841
890
  const template = zh[key];
842
891
  if (params === void 0) return template;
@@ -961,6 +1010,7 @@ function ChartTip(props) {
961
1010
  var MODEL_COLORS = ["#4176e6", "#2da44e", "#e16f24", "#8250df", "#bf8700", "#cf222e", "#1f883d", "#0969da"];
962
1011
  function Bars(props) {
963
1012
  const { data, height = 120, labelEvery = 1, minWidth } = props;
1013
+ const { locale } = useI18n();
964
1014
  const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
965
1015
  const scrollRef = (0, import_react3.useRef)(null);
966
1016
  const initialFocus = lastPopulatedIndex(data.map((datum) => datum.value));
@@ -973,39 +1023,46 @@ function Bars(props) {
973
1023
  for (const datum of data) if (datum.value > max) max = datum.value;
974
1024
  const scale = max > 0 ? max : 1;
975
1025
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
976
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars-scroll", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px`, minWidth: minWidth === void 0 ? void 0 : `${minWidth}px` }, children: data.map((datum, i) => {
977
- const h = Math.max(1, Math.round(datum.value / scale * height));
978
- const text = datum.title ?? `${datum.label}: ${datum.value.toLocaleString()}`;
979
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
980
- "div",
981
- {
982
- className: `dq-bar-col${pinned === i ? " dq-bar-col--pinned" : ""}`,
983
- role: "img",
984
- "aria-label": text,
985
- "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
986
- tabIndex: i === roving.active ? 0 : -1,
987
- ref: (element) => {
988
- roving.refs.current[i] = element;
1026
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-plot", children: [
1027
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-yaxis", style: { height: `${height}px` }, "aria-hidden": "true", children: [
1028
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: max > 0 ? fmtCompact(max, locale) : "" }),
1029
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "0" })
1030
+ ] }),
1031
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars-scroll", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px`, minWidth: minWidth === void 0 ? void 0 : `${minWidth}px` }, children: data.map((datum, i) => {
1032
+ const h = Math.max(1, Math.round(datum.value / scale * height));
1033
+ const text = datum.title ?? `${datum.label}: ${datum.value.toLocaleString()}`;
1034
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1035
+ "div",
1036
+ {
1037
+ className: `dq-bar-col${pinned === i ? " dq-bar-col--pinned" : ""}`,
1038
+ role: "img",
1039
+ "aria-label": text,
1040
+ "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
1041
+ tabIndex: i === roving.active ? 0 : -1,
1042
+ ref: (element) => {
1043
+ roving.refs.current[i] = element;
1044
+ },
1045
+ onPointerMove: (e) => show(e, text),
1046
+ onPointerDown: onPointStart,
1047
+ onPointerUp: (e) => onPointEnd(e, i, text),
1048
+ onFocus: (e) => showFocused(e, text),
1049
+ onBlur: hide,
1050
+ onKeyDown: (e) => roving.onKeyDown(e, i),
1051
+ children: [
1052
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar", style: { height: `${h}px` } }),
1053
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: datum.label })
1054
+ ]
989
1055
  },
990
- onPointerMove: (e) => show(e, text),
991
- onPointerDown: onPointStart,
992
- onPointerUp: (e) => onPointEnd(e, i, text),
993
- onFocus: (e) => showFocused(e, text),
994
- onBlur: hide,
995
- onKeyDown: (e) => roving.onKeyDown(e, i),
996
- children: [
997
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar", style: { height: `${h}px` } }),
998
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: datum.label })
999
- ]
1000
- },
1001
- i
1002
- );
1003
- }) }) }),
1056
+ i
1057
+ );
1058
+ }) }) })
1059
+ ] }),
1004
1060
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChartTip, { tip })
1005
1061
  ] });
1006
1062
  }
1007
1063
  function GroupedBars(props) {
1008
1064
  const { series, height = 120, labelEvery = 1, minWidth } = props;
1065
+ const { locale } = useI18n();
1009
1066
  const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
1010
1067
  const scrollRef = (0, import_react3.useRef)(null);
1011
1068
  const count = series.length > 0 ? series[0].bars.length : 0;
@@ -1015,45 +1072,53 @@ function GroupedBars(props) {
1015
1072
  const scroll = scrollRef.current;
1016
1073
  if (scroll !== null && minWidth !== void 0) scroll.scrollLeft = scroll.scrollWidth;
1017
1074
  }, [count, minWidth]);
1018
- const scale = maxOrOne(stackedTotals(series, count));
1075
+ const totals = stackedTotals(series, count);
1076
+ const scale = maxOrOne(totals);
1077
+ const axisMax = totals.reduce((m, v) => Math.max(m, v), 0);
1019
1078
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
1020
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars-scroll", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px`, minWidth: minWidth === void 0 ? void 0 : `${minWidth}px` }, children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1021
- "div",
1022
- {
1023
- className: "dq-bar-col",
1024
- children: [
1025
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-group", children: series.map((s, seriesIndex) => {
1026
- const datum = s.bars[i];
1027
- const h = Math.max(1, Math.round(datum.value / scale * height));
1028
- const text = datum.title ?? `${s.key}: ${datum.value.toLocaleString()}`;
1029
- const pointIndex = i * series.length + seriesIndex;
1030
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1031
- "div",
1032
- {
1033
- className: `dq-bar${pinned === pointIndex ? " dq-bar--pinned" : ""}`,
1034
- style: { height: `${h}px`, background: s.color },
1035
- role: "img",
1036
- "aria-label": text,
1037
- "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
1038
- tabIndex: pointIndex === roving.active ? 0 : -1,
1039
- ref: (element) => {
1040
- roving.refs.current[pointIndex] = element;
1079
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-plot", children: [
1080
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-yaxis", style: { height: `${height}px` }, "aria-hidden": "true", children: [
1081
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: axisMax > 0 ? fmtCompact(axisMax, locale) : "" }),
1082
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "0" })
1083
+ ] }),
1084
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars-scroll", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px`, minWidth: minWidth === void 0 ? void 0 : `${minWidth}px` }, children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1085
+ "div",
1086
+ {
1087
+ className: "dq-bar-col",
1088
+ children: [
1089
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-group", children: series.map((s, seriesIndex) => {
1090
+ const datum = s.bars[i];
1091
+ const h = Math.max(1, Math.round(datum.value / scale * height));
1092
+ const text = datum.title ?? `${s.key}: ${datum.value.toLocaleString()}`;
1093
+ const pointIndex = i * series.length + seriesIndex;
1094
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1095
+ "div",
1096
+ {
1097
+ className: `dq-bar${pinned === pointIndex ? " dq-bar--pinned" : ""}`,
1098
+ style: { height: `${h}px`, background: s.color },
1099
+ role: "img",
1100
+ "aria-label": text,
1101
+ "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
1102
+ tabIndex: pointIndex === roving.active ? 0 : -1,
1103
+ ref: (element) => {
1104
+ roving.refs.current[pointIndex] = element;
1105
+ },
1106
+ onPointerMove: (e) => show(e, text),
1107
+ onPointerDown: onPointStart,
1108
+ onPointerUp: (e) => onPointEnd(e, pointIndex, text),
1109
+ onFocus: (e) => showFocused(e, text),
1110
+ onBlur: hide,
1111
+ onKeyDown: (e) => roving.onKeyDown(e, pointIndex)
1041
1112
  },
1042
- onPointerMove: (e) => show(e, text),
1043
- onPointerDown: onPointStart,
1044
- onPointerUp: (e) => onPointEnd(e, pointIndex, text),
1045
- onFocus: (e) => showFocused(e, text),
1046
- onBlur: hide,
1047
- onKeyDown: (e) => roving.onKeyDown(e, pointIndex)
1048
- },
1049
- s.key
1050
- );
1051
- }) }),
1052
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: series[0].bars[i].label })
1053
- ]
1054
- },
1055
- i
1056
- )) }) }),
1113
+ s.key
1114
+ );
1115
+ }) }),
1116
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: series[0].bars[i].label })
1117
+ ]
1118
+ },
1119
+ i
1120
+ )) }) })
1121
+ ] }),
1057
1122
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChartTip, { tip })
1058
1123
  ] });
1059
1124
  }
@@ -1102,7 +1167,7 @@ function Heatmap(props) {
1102
1167
  if (datum === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heat-cell dq-heat-cell--pad" }, d);
1103
1168
  const text = t("chart.tooltip", {
1104
1169
  head: datum.date,
1105
- tokens: datum.total.toLocaleString(locale === "en" ? "en-US" : "zh-CN"),
1170
+ tokens: datum.total.toLocaleString(intlLocale(locale)),
1106
1171
  cost: datum.cost.toFixed(2),
1107
1172
  calls: t("common.calls", { count: datum.calls })
1108
1173
  });
@@ -1395,6 +1460,14 @@ function viewOrderForIndex(entries, index, selfId = "balance", fallback = 20) {
1395
1460
  return previous >= next ? previous + 0.5 : (previous + next) / 2;
1396
1461
  }
1397
1462
 
1463
+ // src/client/top-up.ts
1464
+ var TOP_UP_URL = "https://platform.deepseek.com/top_up";
1465
+ var TOP_UP_FLOOR = 10;
1466
+ function needsTopUp(balance, warningLine) {
1467
+ if (!Number.isFinite(balance)) return false;
1468
+ return balance < TOP_UP_FLOOR || warningLine > 0 && balance < warningLine;
1469
+ }
1470
+
1398
1471
  // src/client/dashboard.tsx
1399
1472
  var import_jsx_runtime4 = require("react/jsx-runtime");
1400
1473
  var COMPOSER_FALLBACK_PX = 126;
@@ -1456,9 +1529,10 @@ function Delta(props) {
1456
1529
  const title = `${label} \xA5${fmt(previous)}`;
1457
1530
  if (percent === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta dq-delta--flat", title, children: t("delta.flat") });
1458
1531
  const up = percent > 0;
1532
+ const magnitude = Math.abs(percent) >= 1e3 ? "999+" : String(Math.abs(percent));
1459
1533
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: `dq-delta ${up ? "dq-delta--up" : "dq-delta--down"}`, title, children: [
1460
1534
  up ? "\u2191" : "\u2193",
1461
- Math.abs(percent),
1535
+ magnitude,
1462
1536
  "% ",
1463
1537
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta-label", children: label })
1464
1538
  ] });
@@ -1569,7 +1643,7 @@ function CoverageDiagnostics(props) {
1569
1643
  const { t, locale } = useI18n();
1570
1644
  const { coverage } = props;
1571
1645
  const hasGaps = coverage.failedSessions > 0 || coverage.skippedRecords > 0 || coverage.scannedSessions < coverage.listedSessions;
1572
- const coverageTime = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
1646
+ const coverageTime = new Intl.DateTimeFormat(intlLocale(locale), {
1573
1647
  timeZone: "Asia/Shanghai",
1574
1648
  year: "numeric",
1575
1649
  month: "numeric",
@@ -1616,6 +1690,137 @@ function CardTitle(props) {
1616
1690
  props.caption !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-card-caption", children: props.caption })
1617
1691
  ] });
1618
1692
  }
1693
+ function SectionNav(props) {
1694
+ const { rootRef, label, sections } = props;
1695
+ const [targets, setTargets] = (0, import_react4.useState)([]);
1696
+ const [active, setActive] = (0, import_react4.useState)(0);
1697
+ const [pos, setPos] = (0, import_react4.useState)({ right: 0, visible: false });
1698
+ const signature = sections.map((section) => `${section.key}:${section.label}`).join("|");
1699
+ (0, import_react4.useEffect)(() => {
1700
+ const root = rootRef.current;
1701
+ if (root === null) return;
1702
+ const nodes = Array.from(root.querySelectorAll(":scope > .dq-card, :scope > .dq-card-pair"));
1703
+ setTargets(nodes.map((el, i) => ({ el, label: sections[i]?.label ?? "" })).filter((item) => item.label !== ""));
1704
+ }, [rootRef, signature]);
1705
+ (0, import_react4.useEffect)(() => {
1706
+ const root = rootRef.current;
1707
+ if (root === null) return;
1708
+ const scrollport = root.closest("[data-conversation-scroll]");
1709
+ const measure = () => {
1710
+ const rect = root.getBoundingClientRect();
1711
+ setPos({ right: window.innerWidth - rect.left + 12, visible: window.innerWidth >= 1200 && rect.left >= 170 });
1712
+ };
1713
+ measure();
1714
+ const scroller = scrollport ?? window;
1715
+ scroller.addEventListener("scroll", measure, { passive: true });
1716
+ window.addEventListener("resize", measure);
1717
+ return () => {
1718
+ scroller.removeEventListener("scroll", measure);
1719
+ window.removeEventListener("resize", measure);
1720
+ };
1721
+ }, [rootRef]);
1722
+ (0, import_react4.useEffect)(() => {
1723
+ if (targets.length === 0) return;
1724
+ const root = rootRef.current;
1725
+ if (root === null) return;
1726
+ const scrollport = root.closest("[data-conversation-scroll]");
1727
+ const onScroll = () => {
1728
+ const base = scrollport === null ? 0 : scrollport.getBoundingClientRect().top;
1729
+ let current = 0;
1730
+ targets.forEach((target, i) => {
1731
+ if (target.el.getBoundingClientRect().top - base <= 120) current = i;
1732
+ });
1733
+ setActive(current);
1734
+ };
1735
+ onScroll();
1736
+ const scroller = scrollport ?? window;
1737
+ scroller.addEventListener("scroll", onScroll, { passive: true });
1738
+ return () => scroller.removeEventListener("scroll", onScroll);
1739
+ }, [targets, rootRef]);
1740
+ if (targets.length < 3) return null;
1741
+ const go = (el) => {
1742
+ const root = rootRef.current;
1743
+ const scrollport = root?.closest("[data-conversation-scroll]");
1744
+ const reduce = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1745
+ const behavior = reduce ? "auto" : "smooth";
1746
+ if (scrollport !== null) {
1747
+ const base = scrollport.getBoundingClientRect().top;
1748
+ scrollport.scrollTo({ top: scrollport.scrollTop + el.getBoundingClientRect().top - base - 12, behavior });
1749
+ } else {
1750
+ window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 12, behavior });
1751
+ }
1752
+ };
1753
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("nav", { className: `dq-sectionnav${pos.visible ? "" : " dq-sectionnav--hidden"}`, style: { right: `${pos.right}px` }, "aria-label": label, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ol", { children: targets.map((target, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1754
+ "button",
1755
+ {
1756
+ type: "button",
1757
+ className: `dq-sectionnav-dot${i === active ? " dq-sectionnav-dot--active" : ""}`,
1758
+ "aria-current": i === active ? "true" : void 0,
1759
+ onClick: () => go(target.el),
1760
+ children: [
1761
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-sectionnav-label", children: target.label }),
1762
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-sectionnav-pip", "aria-hidden": "true" })
1763
+ ]
1764
+ }
1765
+ ) }, `${target.label}-${String(i)}`)) }) });
1766
+ }
1767
+ function Collapsible(props) {
1768
+ const ref = (0, import_react4.useRef)(null);
1769
+ const animationRef = (0, import_react4.useRef)(null);
1770
+ const open = props.open;
1771
+ (0, import_react4.useLayoutEffect)(() => {
1772
+ const el = ref.current;
1773
+ if (el === null) return;
1774
+ animationRef.current?.cancel();
1775
+ animationRef.current = null;
1776
+ const reduce = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1777
+ const canAnimate = typeof el.animate === "function" && !reduce;
1778
+ const reset = () => {
1779
+ el.style.height = "";
1780
+ el.style.overflow = "";
1781
+ el.style.opacity = "";
1782
+ };
1783
+ if (open) {
1784
+ if (!canAnimate) {
1785
+ el.style.display = "";
1786
+ reset();
1787
+ return;
1788
+ }
1789
+ el.style.display = "block";
1790
+ el.style.overflow = "hidden";
1791
+ el.style.height = "0px";
1792
+ el.style.opacity = "0";
1793
+ const height2 = el.scrollHeight;
1794
+ const animation2 = el.animate(
1795
+ [{ height: "0px", opacity: 0 }, { height: `${height2}px`, opacity: 1 }],
1796
+ { duration: 300, easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
1797
+ );
1798
+ animationRef.current = animation2;
1799
+ animation2.addEventListener("finish", reset);
1800
+ return () => animation2.cancel();
1801
+ }
1802
+ if (el.style.display === "none") return;
1803
+ if (!canAnimate) {
1804
+ reset();
1805
+ el.style.display = "none";
1806
+ return;
1807
+ }
1808
+ const height = el.getBoundingClientRect().height || el.scrollHeight;
1809
+ el.style.overflow = "hidden";
1810
+ el.style.height = `${height}px`;
1811
+ const animation = el.animate(
1812
+ [{ height: `${height}px`, opacity: 1 }, { height: "0px", opacity: 0 }],
1813
+ { duration: 220, easing: "cubic-bezier(0.4, 0, 1, 1)" }
1814
+ );
1815
+ animationRef.current = animation;
1816
+ animation.addEventListener("finish", () => {
1817
+ reset();
1818
+ el.style.display = "none";
1819
+ });
1820
+ return () => animation.cancel();
1821
+ }, [open]);
1822
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref, children: props.children });
1823
+ }
1619
1824
  function Stat(props) {
1620
1825
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1621
1826
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: props.label }),
@@ -1711,7 +1916,7 @@ function SessionUsageCard(props) {
1711
1916
  const { t, locale } = useI18n();
1712
1917
  const { data } = props;
1713
1918
  if (data.calls === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessionUsage.empty") });
1714
- const rangeTime = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
1919
+ const rangeTime = new Intl.DateTimeFormat(intlLocale(locale), {
1715
1920
  timeZone: "Asia/Shanghai",
1716
1921
  year: "numeric",
1717
1922
  month: "numeric",
@@ -1821,7 +2026,8 @@ function CacheCard(props) {
1821
2026
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--miss" }),
1822
2027
  t("cache.missLegend", { tokens: fmtCompact(totals.input, locale) })
1823
2028
  ] })
1824
- ] })
2029
+ ] }),
2030
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: t("cache.basis", { cost: fmt(totals.cost), miss: fmt(wouldHaveCost) }) })
1825
2031
  ] });
1826
2032
  }
1827
2033
  function VisionCard(props) {
@@ -1864,11 +2070,44 @@ function VisionCard(props) {
1864
2070
  function PeakCard(props) {
1865
2071
  const { t, locale } = useI18n();
1866
2072
  const { split, pricing } = props;
2073
+ const [nowMs, setNowMs] = (0, import_react4.useState)(() => Date.now());
2074
+ (0, import_react4.useEffect)(() => {
2075
+ const id = setInterval(() => setNowMs(Date.now()), 6e4);
2076
+ return () => clearInterval(id);
2077
+ }, []);
1867
2078
  const total = split.peak.total + split.offPeak.total;
1868
2079
  if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") });
1869
2080
  const peakShare = split.peak.total / total;
1870
2081
  const shiftSaving = split.peakEraCost - split.offPeakEraCost;
2082
+ const beijing = new Date(nowMs + 8 * 36e5);
2083
+ const hour = beijing.getUTCHours();
2084
+ const minute = beijing.getUTCMinutes();
2085
+ const weekday = beijing.getUTCDay();
2086
+ const weekend = weekday === 0 || weekday === 6;
2087
+ const windows = pricing.peakWindows.map((w) => w.match(/(\d{1,2}):\d{2}\D+(\d{1,2}):\d{2}/)).filter((m) => m !== null).map((m) => [Number(m[1]), Number(m[2])]);
2088
+ const isPeakNow = !weekend && windows.some(([from, to]) => hour >= from && hour < to);
2089
+ const nowPercent = (hour + minute / 60) / 24 * 100;
2090
+ const clockCells = Array.from({ length: 24 }, (_, h) => !weekend && windows.some(([from, to]) => h >= from && h < to));
2091
+ const nowState = isPeakNow ? t("peak.nowPeak") : t("peak.nowOff");
1871
2092
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak", children: [
2093
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-clock", children: [
2094
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-clock-head", children: [
2095
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-clock-title", children: t("peak.clockTitle") }),
2096
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-clock-status ${isPeakNow ? "dq-clock-status--peak" : "dq-clock-status--off"}`, children: nowState })
2097
+ ] }),
2098
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-clock-track", role: "img", "aria-label": t("peak.clockAria", { state: nowState }), children: [
2099
+ clockCells.map((peak, h) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-clock-cell${peak ? " dq-clock-cell--peak" : ""}` }, h)),
2100
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-clock-now", style: { left: `${nowPercent}%` }, "aria-hidden": "true" })
2101
+ ] }),
2102
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-clock-scale", "aria-hidden": "true", children: [
2103
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "0" }),
2104
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "6" }),
2105
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "12" }),
2106
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "18" }),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "24" })
2108
+ ] }),
2109
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-clock-hint", children: t("peak.clockHint") })
2110
+ ] }),
1872
2111
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1873
2112
  "div",
1874
2113
  {
@@ -1898,12 +2137,9 @@ function PeakCard(props) {
1898
2137
  ] }),
1899
2138
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-figures", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1900
2139
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.shift") }),
1901
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1902
- "\xA5",
1903
- fmt(split.offPeakEraCost),
1904
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
1905
- ] })
1906
- ] }) })
2140
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value dq-stat-value--ok", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
2141
+ ] }) }),
2142
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-peak-foot", children: t("peak.basis", { total: fmt(split.offPeakEraCost) }) })
1907
2143
  ] });
1908
2144
  }
1909
2145
  function agoText(ms, t) {
@@ -2143,6 +2379,7 @@ function BalanceDashboard(props) {
2143
2379
  const [usageUpdatedAt, setUsageUpdatedAt] = (0, import_react4.useState)(cachedUsageAt);
2144
2380
  const [syncState, setSyncState] = (0, import_react4.useState)(cachedUsageAt === null ? "syncing" : "cached");
2145
2381
  const [freshnessNow, setFreshnessNow] = (0, import_react4.useState)(Date.now());
2382
+ const [settingsOpen, setSettingsOpen] = (0, import_react4.useState)(false);
2146
2383
  const hadDataRef = (0, import_react4.useRef)(cachedBalance !== null || cachedUsage !== null);
2147
2384
  const [widgetOn, setWidgetOn] = (0, import_react4.useState)(widgetVisibleStore.get());
2148
2385
  const [widgetTabIds, setWidgetTabIds] = (0, import_react4.useState)(widgetTabIdsStore.get());
@@ -2401,6 +2638,7 @@ function BalanceDashboard(props) {
2401
2638
  const avgDailyCost = recentDays.length > 0 ? recentDays.reduce((sum, d) => sum + d.cost, 0) / recentDays.length : 0;
2402
2639
  const balanceValue = primary !== null ? Number(primary.total) : Number.NaN;
2403
2640
  const balanceLow = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
2641
+ const topUpPrompt = needsTopUp(balanceValue, lowBalance);
2404
2642
  const daysLeft = avgDailyCost > 0 && Number.isFinite(balanceValue) ? balanceValue / avgDailyCost : null;
2405
2643
  const daysLeftText = daysLeft === null ? "\u2014" : daysLeft >= 365 ? t("balance.overYear") : t("balance.days", { count: daysLeft < 10 ? daysLeft.toFixed(1) : Math.round(daysLeft) });
2406
2644
  const runwayTitle = daysLeft === null ? t("balance.noRunway", { days: recentDays.length || 7 }) : t("balance.runwayTitle", { days: recentDays.length, cost: fmt(avgDailyCost) });
@@ -2508,7 +2746,18 @@ function BalanceDashboard(props) {
2508
2746
  // column, then narrows in place — no flash of the old width).
2509
2747
  ...composerWidth !== null ? { "--dq-composer-w": `${composerWidth}px` } : {}
2510
2748
  };
2749
+ const navSections = [
2750
+ { key: "balance", label: t("nav.balance") },
2751
+ ...props.sessionId !== void 0 && props.sessionId !== "" ? [{ key: "session", label: t("nav.session") }] : [],
2752
+ { key: "overview", label: t("nav.overview") },
2753
+ { key: "trend", label: t("nav.trend") },
2754
+ { key: "heatmap", label: t("nav.heatmap") },
2755
+ { key: "models", label: t("nav.models") },
2756
+ { key: "sessions", label: t("nav.sessions") },
2757
+ { key: "peakcache", label: t("nav.peakcache") }
2758
+ ];
2511
2759
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance", ref: rootRef, style: composerSafeAreaStyle, children: [
2760
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SectionNav, { rootRef, label: t("nav.label"), sections: navSections }),
2512
2761
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-row", children: [
2513
2762
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-messages", children: [
2514
2763
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -2517,7 +2766,7 @@ function BalanceDashboard(props) {
2517
2766
  className: `dq-sync dq-sync--${syncState}`,
2518
2767
  role: "status",
2519
2768
  "aria-live": "polite",
2520
- title: usageUpdatedAt === null ? t("status.neverSynced") : t("status.usageTime", { time: new Date(usageUpdatedAt).toLocaleString(locale === "en" ? "en-US" : "zh-CN", { timeZone: "Asia/Shanghai" }) }),
2769
+ title: usageUpdatedAt === null ? t("status.neverSynced") : t("status.usageTime", { time: new Date(usageUpdatedAt).toLocaleString(intlLocale(locale), { timeZone: "Asia/Shanghai" }) }),
2521
2770
  children: [
2522
2771
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-sync-dot", "aria-hidden": "true" }),
2523
2772
  syncStatusText(syncState, usageUpdatedAt, freshnessNow, t)
@@ -2527,23 +2776,39 @@ function BalanceDashboard(props) {
2527
2776
  noticeText !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-warn", children: noticeText }),
2528
2777
  errorText !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-error", children: errorText })
2529
2778
  ] }),
2530
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2531
- "button",
2532
- {
2533
- type: "button",
2534
- className: "dq-refresh-btn",
2535
- title: t("status.forceRefresh"),
2536
- disabled: refreshing,
2537
- "aria-busy": refreshing,
2538
- onClick: () => {
2539
- void load(true);
2540
- },
2541
- children: [
2542
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-refresh-icon${refreshing ? " dq-refresh-icon--spin" : ""}`, children: "\u21BB" }),
2543
- refreshing ? t("status.refreshing") : t("status.refresh")
2544
- ]
2545
- }
2546
- )
2779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-actions", children: [
2780
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2781
+ "button",
2782
+ {
2783
+ type: "button",
2784
+ className: "dq-settings-btn",
2785
+ "aria-expanded": settingsOpen,
2786
+ "aria-controls": "dq-settings-panel",
2787
+ onClick: () => setSettingsOpen((open) => !open),
2788
+ children: [
2789
+ t("settings.title"),
2790
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-settings-caret${settingsOpen ? " dq-settings-caret--open" : ""}`, "aria-hidden": "true", children: "\u25BE" })
2791
+ ]
2792
+ }
2793
+ ),
2794
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2795
+ "button",
2796
+ {
2797
+ type: "button",
2798
+ className: "dq-refresh-btn",
2799
+ title: t("status.forceRefresh"),
2800
+ disabled: refreshing,
2801
+ "aria-busy": refreshing,
2802
+ onClick: () => {
2803
+ void load(true);
2804
+ },
2805
+ children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-refresh-icon${refreshing ? " dq-refresh-icon--spin" : ""}`, children: "\u21BB" }),
2807
+ refreshing ? t("status.refreshing") : t("status.refresh")
2808
+ ]
2809
+ }
2810
+ )
2811
+ ] })
2547
2812
  ] }),
2548
2813
  balanceLow && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-alert", role: "status", children: [
2549
2814
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-alert-icon", "aria-hidden": "true", children: "!" }),
@@ -2551,7 +2816,7 @@ function BalanceDashboard(props) {
2551
2816
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: t("alert.lowTitle", { balance: fmt(balanceValue), currency: primary?.currency, threshold: fmt(lowBalance) }) }),
2552
2817
  daysLeft !== null && t("alert.runway", { days: daysLeftText }),
2553
2818
  t("common.period"),
2554
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { className: "dq-alert-link", href: "https://platform.deepseek.com/top_up", target: "_blank", rel: "noreferrer", children: t("alert.topUp") })
2819
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { className: "dq-alert-link", href: TOP_UP_URL, target: "_blank", rel: "noreferrer", children: t("alert.topUp") })
2555
2820
  ] })
2556
2821
  ] }),
2557
2822
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--hero", children: [
@@ -2592,33 +2857,177 @@ function BalanceDashboard(props) {
2592
2857
  ] }),
2593
2858
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2594
2859
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("balance.runway") }),
2595
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2596
- "details",
2597
- {
2598
- className: `dq-stat-value dq-runway${daysLeft !== null && daysLeft < 3 ? " dq-stat-value--bad" : ""}`,
2599
- open: runwayHovering || runwayManuallyOpened,
2600
- onPointerEnter: () => setRunwayHovering(true),
2601
- onPointerLeave: () => setRunwayHovering(false),
2602
- children: [
2603
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2604
- "summary",
2605
- {
2606
- title: t("balance.runwayBreakdownTitle"),
2607
- onClick: (e) => {
2608
- e.preventDefault();
2609
- setRunwayManuallyOpened((v) => !v);
2610
- },
2611
- children: daysLeftText
2612
- }
2613
- ),
2614
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-runway-detail", children: runwayTitle })
2615
- ]
2616
- }
2617
- )
2860
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-runway-line", children: [
2861
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2862
+ "details",
2863
+ {
2864
+ className: `dq-stat-value dq-runway${daysLeft !== null && daysLeft < 3 ? " dq-stat-value--bad" : ""}`,
2865
+ open: runwayHovering || runwayManuallyOpened,
2866
+ onPointerEnter: () => setRunwayHovering(true),
2867
+ onPointerLeave: () => setRunwayHovering(false),
2868
+ children: [
2869
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2870
+ "summary",
2871
+ {
2872
+ title: t("balance.runwayBreakdownTitle"),
2873
+ onClick: (e) => {
2874
+ e.preventDefault();
2875
+ setRunwayManuallyOpened((v) => !v);
2876
+ },
2877
+ children: daysLeftText
2878
+ }
2879
+ ),
2880
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-runway-detail", children: runwayTitle })
2881
+ ]
2882
+ }
2883
+ ),
2884
+ topUpPrompt && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("a", { className: "dq-topup", href: TOP_UP_URL, target: "_blank", rel: "noreferrer", title: t("balance.topUpTitle"), children: [
2885
+ t("alert.topUp"),
2886
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-topup-arrow", "aria-hidden": "true", children: "\u2197" })
2887
+ ] })
2888
+ ] })
2618
2889
  ] }),
2619
2890
  balance?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("common.status"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value dq-stat-value--bad", children: t("common.unavailable") }) })
2620
2891
  ] }) : loadingBalance ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BalanceSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("balance.empty") })
2621
2892
  ] }),
2893
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Collapsible, { open: settingsOpen, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-settings-panel", id: "dq-settings-panel", role: "region", "aria-label": t("settings.title"), children: [
2894
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardTitle, { children: t("settings.title") }),
2895
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-group", children: [
2896
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-group-title", children: t("settings.groupDisplay") }),
2897
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-toggle", children: [
2898
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: widgetOn, onChange: toggle }),
2899
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.widget") })
2900
+ ] }),
2901
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-toggle-hint", children: t("settings.widgetHint") }),
2902
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting dq-setting--tabs", children: [
2903
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-label", id: "dq-widget-tabs-label", children: t("settings.widgetTabs") }),
2904
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control dq-widget-tabs", role: "group", "aria-labelledby": "dq-widget-tabs-label", children: [
2905
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-widget-tab-list", children: widgetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-widget-tab", children: [
2906
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2907
+ "input",
2908
+ {
2909
+ type: "checkbox",
2910
+ checked: widgetTabIds === null || widgetTabIds.includes(tab.id),
2911
+ onChange: () => toggleWidgetTab(tab.id)
2912
+ }
2913
+ ),
2914
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: tab.label })
2915
+ ] }, tab.id)) }),
2916
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: widgetTabs.length === 0 ? t("settings.widgetTabsEmpty") : t("settings.widgetTabsHint") })
2917
+ ] })
2918
+ ] })
2919
+ ] }),
2920
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-group", children: [
2921
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-group-title", children: t("settings.groupAlerts") }),
2922
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2923
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: t("settings.lowBalance") }),
2924
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2925
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2926
+ "input",
2927
+ {
2928
+ id: "dq-low-balance",
2929
+ className: "dq-number",
2930
+ type: "number",
2931
+ min: 0,
2932
+ step: 1,
2933
+ value: lowBalance,
2934
+ onChange: (e) => changeLowBalance(Math.max(0, Number(e.target.value) || 0))
2935
+ }
2936
+ ),
2937
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: lowBalance > 0 ? t("settings.lowBalanceOn", { amount: fmt(lowBalance), currency: primary?.currency ?? "CNY" }) : t("settings.lowBalanceOff") })
2938
+ ] })
2939
+ ] }),
2940
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2941
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-monthly-budget", children: t("settings.monthlyBudget") }),
2942
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2943
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2944
+ "input",
2945
+ {
2946
+ ref: budgetInputRef,
2947
+ id: "dq-monthly-budget",
2948
+ className: "dq-number",
2949
+ type: "number",
2950
+ min: 0,
2951
+ step: 10,
2952
+ value: monthlyBudget,
2953
+ "aria-describedby": "dq-monthly-budget-hint",
2954
+ onChange: (e) => changeMonthlyBudget(Math.max(0, Number(e.target.value) || 0))
2955
+ }
2956
+ ),
2957
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: "dq-monthly-budget-hint", className: "dq-setting-hint", children: monthlyBudget > 0 ? t("settings.monthlyBudgetOn", { amount: fmt(monthlyBudget) }) : t("settings.monthlyBudgetOff") })
2958
+ ] })
2959
+ ] })
2960
+ ] }),
2961
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-group", children: [
2962
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-group-title", children: t("settings.groupLayout") }),
2963
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2964
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-sidebar-entry", children: t("settings.sidebarEntry") }),
2965
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2966
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2967
+ "select",
2968
+ {
2969
+ id: "dq-sidebar-entry",
2970
+ className: "dq-select",
2971
+ value: sidebarEntry,
2972
+ onChange: (e) => changeSidebarEntry(e.target.value),
2973
+ children: [
2974
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "off", children: t("settings.sidebarEntryOff") }),
2975
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "top", children: t("settings.sidebarEntryTop") }),
2976
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "bottom", children: t("settings.sidebarEntryBottom") })
2977
+ ]
2978
+ }
2979
+ ),
2980
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: t("settings.sidebarEntryHint") })
2981
+ ] })
2982
+ ] }),
2983
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2984
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-label", id: "dq-tab-order-label", children: t("settings.tabPosition") }),
2985
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2986
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2987
+ "div",
2988
+ {
2989
+ className: "dq-tab-order",
2990
+ role: "group",
2991
+ "aria-labelledby": "dq-tab-order-label",
2992
+ "aria-describedby": "dq-tab-order-hint",
2993
+ onDragOver: onOrderDragOver,
2994
+ onDrop: onOrderDrop,
2995
+ children: viewTabs.map((tab, index) => {
2996
+ const current = tab.id === "balance";
2997
+ const dropTarget = tabDragging && !current && tabDropTarget === index;
2998
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2999
+ "span",
3000
+ {
3001
+ className: `dq-tab-chip${current ? " dq-tab-chip--current" : ""}${current && tabDragging ? " dq-tab-chip--dragging" : ""}${dropTarget ? " dq-tab-chip--drop-target" : ""}`,
3002
+ draggable: current,
3003
+ tabIndex: current ? 0 : void 0,
3004
+ role: current ? "button" : void 0,
3005
+ "aria-label": current ? t("settings.tabDragAria") : void 0,
3006
+ onDragStart: current ? onQuotaTabDragStart : void 0,
3007
+ onDragEnd: current ? endTabDrag : void 0,
3008
+ onDragOver: (event) => onChipDragOver(event, index),
3009
+ onDrop: (event) => onChipDrop(event, index),
3010
+ onKeyDown: current ? onQuotaTabKeyDown : void 0,
3011
+ children: tab.label
3012
+ },
3013
+ tab.id
3014
+ );
3015
+ })
3016
+ }
3017
+ ),
3018
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: "dq-tab-order-hint", className: "dq-setting-hint", children: t("settings.tabPositionHint") })
3019
+ ] })
3020
+ ] })
3021
+ ] }),
3022
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-group", children: [
3023
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-group-title", children: t("links.title") }),
3024
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-links", children: [
3025
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://platform.deepseek.com/usage", children: t("links.usage") }),
3026
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://platform.deepseek.com/api_keys", children: t("links.apiKey") }),
3027
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://status.deepseek.com", children: t("links.status") })
3028
+ ] })
3029
+ ] })
3030
+ ] }) }),
2622
3031
  props.sessionId !== void 0 && props.sessionId !== "" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2623
3032
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardTitle, { caption: t("common.estimated"), children: t("sessionUsage.title") }),
2624
3033
  sessionUsage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageCard, { data: sessionUsage }) : loadingSessionUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageSkeleton, {}) : sessionUsageError !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty dq-empty--error", children: localizeApiError(sessionUsageError, t, "error.query") }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessionUsage.empty") })
@@ -2645,7 +3054,10 @@ function BalanceDashboard(props) {
2645
3054
  {
2646
3055
  spent: usage.summary.month.cost,
2647
3056
  budget: monthlyBudget,
2648
- onConfigure: () => budgetInputRef.current?.focus()
3057
+ onConfigure: () => {
3058
+ setSettingsOpen(true);
3059
+ window.setTimeout(() => budgetInputRef.current?.focus(), 80);
3060
+ }
2649
3061
  }
2650
3062
  ),
2651
3063
  usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PricingNote, { pricing: usage.pricing })
@@ -2773,134 +3185,7 @@ function BalanceDashboard(props) {
2773
3185
  ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("cache.empty") })
2774
3186
  ] })
2775
3187
  ] }),
2776
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2777
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardTitle, { children: t("settings.title") }),
2778
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-links-title", children: t("links.title") }),
2779
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-links", children: [
2780
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://platform.deepseek.com/usage", children: t("links.usage") }),
2781
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://platform.deepseek.com/api_keys", children: t("links.apiKey") }),
2782
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://status.deepseek.com", children: t("links.status") })
2783
- ] }),
2784
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-toggle", children: [
2785
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: widgetOn, onChange: toggle }),
2786
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.widget") })
2787
- ] }),
2788
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-toggle-hint", children: t("settings.widgetHint") }),
2789
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting dq-setting--tabs", children: [
2790
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-label", id: "dq-widget-tabs-label", children: t("settings.widgetTabs") }),
2791
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control dq-widget-tabs", role: "group", "aria-labelledby": "dq-widget-tabs-label", children: [
2792
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-widget-tab-list", children: widgetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-widget-tab", children: [
2793
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2794
- "input",
2795
- {
2796
- type: "checkbox",
2797
- checked: widgetTabIds === null || widgetTabIds.includes(tab.id),
2798
- onChange: () => toggleWidgetTab(tab.id)
2799
- }
2800
- ),
2801
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: tab.label })
2802
- ] }, tab.id)) }),
2803
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: widgetTabs.length === 0 ? t("settings.widgetTabsEmpty") : t("settings.widgetTabsHint") })
2804
- ] })
2805
- ] }),
2806
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2807
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: t("settings.lowBalance") }),
2808
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2809
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2810
- "input",
2811
- {
2812
- id: "dq-low-balance",
2813
- className: "dq-number",
2814
- type: "number",
2815
- min: 0,
2816
- step: 1,
2817
- value: lowBalance,
2818
- onChange: (e) => changeLowBalance(Math.max(0, Number(e.target.value) || 0))
2819
- }
2820
- ),
2821
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: lowBalance > 0 ? t("settings.lowBalanceOn", { amount: fmt(lowBalance), currency: primary?.currency ?? "CNY" }) : t("settings.lowBalanceOff") })
2822
- ] })
2823
- ] }),
2824
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2825
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-monthly-budget", children: t("settings.monthlyBudget") }),
2826
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2827
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2828
- "input",
2829
- {
2830
- ref: budgetInputRef,
2831
- id: "dq-monthly-budget",
2832
- className: "dq-number",
2833
- type: "number",
2834
- min: 0,
2835
- step: 10,
2836
- value: monthlyBudget,
2837
- "aria-describedby": "dq-monthly-budget-hint",
2838
- onChange: (e) => changeMonthlyBudget(Math.max(0, Number(e.target.value) || 0))
2839
- }
2840
- ),
2841
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: "dq-monthly-budget-hint", className: "dq-setting-hint", children: monthlyBudget > 0 ? t("settings.monthlyBudgetOn", { amount: fmt(monthlyBudget) }) : t("settings.monthlyBudgetOff") })
2842
- ] })
2843
- ] }),
2844
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2845
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-sidebar-entry", children: t("settings.sidebarEntry") }),
2846
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2847
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2848
- "select",
2849
- {
2850
- id: "dq-sidebar-entry",
2851
- className: "dq-select",
2852
- value: sidebarEntry,
2853
- onChange: (e) => changeSidebarEntry(e.target.value),
2854
- children: [
2855
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "off", children: t("settings.sidebarEntryOff") }),
2856
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "top", children: t("settings.sidebarEntryTop") }),
2857
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "bottom", children: t("settings.sidebarEntryBottom") })
2858
- ]
2859
- }
2860
- ),
2861
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: t("settings.sidebarEntryHint") })
2862
- ] })
2863
- ] }),
2864
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2865
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-label", id: "dq-tab-order-label", children: t("settings.tabPosition") }),
2866
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2867
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2868
- "div",
2869
- {
2870
- className: "dq-tab-order",
2871
- role: "group",
2872
- "aria-labelledby": "dq-tab-order-label",
2873
- "aria-describedby": "dq-tab-order-hint",
2874
- onDragOver: onOrderDragOver,
2875
- onDrop: onOrderDrop,
2876
- children: viewTabs.map((tab, index) => {
2877
- const current = tab.id === "balance";
2878
- const dropTarget = tabDragging && !current && tabDropTarget === index;
2879
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2880
- "span",
2881
- {
2882
- className: `dq-tab-chip${current ? " dq-tab-chip--current" : ""}${current && tabDragging ? " dq-tab-chip--dragging" : ""}${dropTarget ? " dq-tab-chip--drop-target" : ""}`,
2883
- draggable: current,
2884
- tabIndex: current ? 0 : void 0,
2885
- role: current ? "button" : void 0,
2886
- "aria-label": current ? t("settings.tabDragAria") : void 0,
2887
- onDragStart: current ? onQuotaTabDragStart : void 0,
2888
- onDragEnd: current ? endTabDrag : void 0,
2889
- onDragOver: (event) => onChipDragOver(event, index),
2890
- onDrop: (event) => onChipDrop(event, index),
2891
- onKeyDown: current ? onQuotaTabKeyDown : void 0,
2892
- children: tab.label
2893
- },
2894
- tab.id
2895
- );
2896
- })
2897
- }
2898
- ),
2899
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: "dq-tab-order-hint", className: "dq-setting-hint", children: t("settings.tabPositionHint") })
2900
- ] })
2901
- ] })
2902
- ] }),
2903
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.8.1" })
3188
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.9.0" })
2904
3189
  ] });
2905
3190
  }
2906
3191
 
@@ -3350,6 +3635,17 @@ var css = `
3350
3635
  .dq-remaining-breakdown{display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:20;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.15);white-space:nowrap;font-size:12px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
3351
3636
  .dq-remaining[open] .dq-remaining-breakdown{display:flex}
3352
3637
  .dq-remaining-granted{color:var(--dsw-alias-label-secondary,#59636e);font-weight:400;font-size:12px}
3638
+ /* Low-balance top-up shortcut beside the runway value. It is an <a>, not a
3639
+ <button>, because it leaves the app; the \u2197 glyph states that. Colours come
3640
+ from the host's primary-button tokens (the same pair DSH's own filled
3641
+ buttons use) so the page keeps exactly one filled call to action instead of
3642
+ reading as another filter chip. Height stays on the runway text line so the
3643
+ hero row does not grow. */
3644
+ .dq-runway-line{display:flex;align-items:center;flex-wrap:wrap;gap:4px 10px}
3645
+ .dq-topup{display:inline-flex;align-items:center;gap:2px;height:26px;padding:0 10px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-button-primary-fill,#0969da);color:var(--dsw-alias-label-primary-foreground,#fff);font-size:12px;font-weight:600;line-height:1;text-decoration:none;white-space:nowrap;transition:background .15s ease}
3646
+ .dq-topup:hover{background:var(--dsw-alias-button-primary-hover,#0757ba)}
3647
+ .dq-topup:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
3648
+ .dq-topup-arrow{font-size:10px;line-height:1;transform:translateY(-1px)}
3353
3649
  /* narrow screens: .dq-balance-grid wraps its 3 stats onto separate lines, so the absolutely-positioned
3354
3650
  .dq-remaining-breakdown/.dq-runway-detail popovers can float down over whichever stat now sits in the
3355
3651
  next row (e.g. "\u4F59\u989D\u660E\u7EC6" open covering "\u9884\u8BA1\u53EF\u7528"'s clickable summary) \u2014 force each stat onto its own
@@ -3403,7 +3699,11 @@ var css = `
3403
3699
  .dq-pricing-table{border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums;min-width:100%}
3404
3700
  .dq-pricing-table th{text-align:left;font-weight:500;color:var(--dsw-alias-label-secondary,#59636e);padding:4px 12px 4px 0;white-space:nowrap;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
3405
3701
  .dq-pricing-table td{padding:4px 12px 4px 0;color:var(--dsw-alias-label-primary,#1f2328);white-space:nowrap;vertical-align:top}
3406
- .dq-pricing-table th:nth-child(n+3),.dq-pricing-table td:nth-child(n+3){text-align:right}
3702
+ /* Align by counting from the end: a peak row starts with the rowSpan'd model
3703
+ cell (5 cells) while its off-peak pair starts at \u65F6\u6BB5 (4 cells), so
3704
+ nth-child(n+3) left the off-peak cache-hit price stranded under \u65F6\u6BB5. The
3705
+ last three cells are the three price columns in both row shapes. */
3706
+ .dq-pricing-table th:nth-last-child(-n+3),.dq-pricing-table td:nth-last-child(-n+3){text-align:right}
3407
3707
  .dq-pricing-table th:last-child,.dq-pricing-table td:last-child{padding-right:0}
3408
3708
  .dq-pricing-table tbody td:first-child{font-weight:500}
3409
3709
  .dq-pricing-table tbody tr+tr td{border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.05))}
@@ -3435,8 +3735,23 @@ var css = `
3435
3735
  .dq-legend-swatch--peak{background:var(--dsw-alias-state-warning-primary,#bf8700)}
3436
3736
  .dq-legend-swatch--offpeak{background:var(--dsw-alias-state-success-primary,#2da44e);opacity:.85}
3437
3737
  .dq-peak-figures{display:flex;flex-wrap:wrap;gap:12px 32px;margin-top:16px}
3438
- .dq-peak-delta{font-size:12px;font-weight:500;margin-left:8px;color:var(--dq-text-warn)}
3439
- .dq-peak-delta--save{color:var(--dq-text-success)}
3738
+ .dq-peak-foot{margin:12px 0 0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e);text-wrap:pretty}
3739
+ .dq-clock{margin-bottom:16px}
3740
+ .dq-clock-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}
3741
+ .dq-clock-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e)}
3742
+ .dq-clock-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;padding:2px 8px;border-radius:999px}
3743
+ .dq-clock-status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}
3744
+ .dq-clock-status--peak{color:var(--dq-text-warn);background:rgba(191,135,0,.12)}
3745
+ .dq-clock-status--off{color:var(--dq-text-success);background:rgba(45,164,78,.12)}
3746
+ .dq-clock-track{position:relative;display:flex;gap:1px;height:10px}
3747
+ .dq-clock-cell{flex:1 1 0;height:100%;background:var(--dsw-alias-state-success-primary,#2da44e);opacity:.55}
3748
+ .dq-clock-cell--peak{background:var(--dsw-alias-state-warning-primary,#bf8700);opacity:.92}
3749
+ .dq-clock-cell:first-child{border-radius:999px 0 0 999px}
3750
+ .dq-clock-cell:last-child{border-radius:0 999px 999px 0}
3751
+ .dq-clock-now{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--dsw-alias-label-primary,#1f2328);border-radius:2px;transform:translateX(-1px)}
3752
+ .dq-clock-now::before{content:'';position:absolute;top:-4px;left:50%;transform:translateX(-50%);width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-label-primary,#1f2328)}
3753
+ .dq-clock-scale{display:flex;justify-content:space-between;margin-top:4px;font-size:10px;color:var(--dsw-alias-label-tertiary,#59636e);font-variant-numeric:tabular-nums}
3754
+ .dq-clock-hint{margin:6px 0 0;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary,#59636e)}
3440
3755
  .dq-cache-figures{display:flex;flex-wrap:wrap;gap:12px 32px;margin-bottom:12px}
3441
3756
  @media (max-width:620px){.dq-cache-figures{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px}.dq-cache-figures .dq-stat{min-width:0}}
3442
3757
  .dq-muted-value{color:var(--dsw-alias-label-secondary,#59636e)!important;font-weight:500}
@@ -3502,12 +3817,29 @@ var css = `
3502
3817
  .dq-setting-label{width:128px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,#1f2328);padding-top:6px;flex:none}
3503
3818
  .dq-setting-control{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
3504
3819
  .dq-setting-hint{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);line-height:1.5}
3820
+ .dq-setting-group{margin-top:28px}
3821
+ .dq-setting-group:first-of-type{margin-top:0}
3822
+ .dq-setting-group-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:0 0 10px;letter-spacing:.02em}
3823
+ .dq-setting-group .dq-toggle{margin-top:0}
3824
+ .dq-setting-group .dq-setting{margin-top:16px}
3505
3825
  /* Page footer: the package name + version, injected at build time from
3506
3826
  package.json (see build.mjs) so it can never drift from the release. */
3507
3827
  .dq-footer{margin:16px 0 0;font-size:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#59636e)}
3828
+ .dq-sectionnav{position:fixed;top:50%;transform:translateY(-50%);z-index:20;width:max-content;pointer-events:none}
3829
+ .dq-sectionnav--hidden{display:none}
3830
+ .dq-sectionnav ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;pointer-events:auto}
3831
+ .dq-sectionnav li{display:flex;justify-content:flex-end}
3832
+ .dq-sectionnav-dot{display:inline-flex;align-items:center;justify-content:flex-end;gap:8px;padding:4px 2px;border:0;background:transparent;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-secondary,#59636e);font-size:12px;line-height:1.2;white-space:nowrap;transition:color .15s ease,background .15s ease}
3833
+ .dq-sectionnav-dot:hover,.dq-sectionnav-dot:focus-visible{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.10))}
3834
+ .dq-sectionnav-dot:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px}
3835
+ .dq-sectionnav-label{font-size:12px;color:inherit}
3836
+ .dq-sectionnav-pip{flex:none;width:9px;height:9px;border-radius:50%;background:var(--dsw-alias-border-l2,rgba(0,0,0,.22));transition:background .15s ease,transform .15s ease}
3837
+ .dq-sectionnav-dot:hover .dq-sectionnav-pip{background:var(--dsw-alias-label-secondary,#59636e)}
3838
+ .dq-sectionnav-dot--active{color:var(--dsw-alias-label-primary,#1f2328);font-weight:600}
3839
+ .dq-sectionnav-dot--active .dq-sectionnav-pip{background:var(--dsw-alias-state-business-primary,#0969da);transform:scale(1.25)}
3840
+ @media (max-width:1199px){.dq-sectionnav{display:none}}
3508
3841
  .dq-number{width:82px;padding:5px 8px;font-size:13px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
3509
3842
  .dq-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;border-color:transparent}
3510
- .dq-links-title{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 12px}
3511
3843
  .dq-links{display:flex;flex-wrap:wrap;gap:8px}
3512
3844
  .dq-link{display:inline-flex;align-items:center;gap:4px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);text-decoration:none;font-size:13px;font-weight:500;cursor:pointer}
3513
3845
  .dq-link::after{content:"\u2197";font-size:12px;color:var(--dsw-alias-label-secondary,#59636e)}
@@ -3574,7 +3906,10 @@ var css = `
3574
3906
  .dq-chart-switch-btn:hover{color:var(--dsw-alias-label-primary,#1f2328)}
3575
3907
  .dq-chart-switch-btn--on{background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1f2328);box-shadow:0 1px 3px rgba(0,0,0,.14)}
3576
3908
  .dq-chart-wrap{position:relative}
3577
- .dq-bars-scroll{overflow-x:auto;overflow-y:hidden;padding-bottom:2px}
3909
+ .dq-plot{display:flex;align-items:flex-end;gap:8px}
3910
+ .dq-yaxis{flex:none;width:46px;display:flex;flex-direction:column;justify-content:space-between;margin-bottom:18px;font-size:11px;line-height:1;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;text-align:right;pointer-events:none}
3911
+ .dq-yaxis span{white-space:nowrap}
3912
+ .dq-bars-scroll{flex:1 1 auto;min-width:0;overflow-x:auto;overflow-y:hidden;padding-bottom:2px}
3578
3913
  .dq-tip{position:absolute;transform:translate(-50%,14px);z-index:40;pointer-events:none;background:var(--dsw-alias-tooltip-bg,#1f2328);color:var(--dsw-static-neutral-bluish-00,#fff);border-radius:8px;padding:7px 10px;font-size:12px;line-height:1.5;white-space:nowrap;box-shadow:0 6px 20px rgba(0,0,0,.28);font-variant-numeric:tabular-nums}
3579
3914
  .dq-tip--above{transform:translate(-50%,calc(-100% - 8px))}
3580
3915
  .dq-tip-head{font-weight:600;margin-bottom:2px}
@@ -3595,13 +3930,13 @@ var css = `
3595
3930
  .dq-heat-cell--pinned{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;position:relative;z-index:2}
3596
3931
  .dq-bar-label{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);text-align:center;margin-top:4px;overflow:visible;white-space:nowrap}
3597
3932
  .dq-heat{display:flex;flex-direction:column;gap:8px;max-width:100%}
3598
- .dq-heat-scroll{overflow-x:auto;padding-bottom:2px}
3599
- .dq-heatmap{display:flex;gap:3px}
3600
- .dq-heat-week{display:flex;flex:1 1 0;min-width:6px;flex-direction:column;gap:3px}
3601
- .dq-heat-cell{width:100%;aspect-ratio:1/1;border-radius:3px;flex:none}
3933
+ .dq-heat-scroll{overflow:hidden;padding-bottom:2px}
3934
+ .dq-heatmap{display:flex;gap:2px}
3935
+ .dq-heat-week{display:flex;flex:1 1 0;min-width:0;flex-direction:column;gap:2px}
3936
+ .dq-heat-cell{width:100%;aspect-ratio:1/1;border-radius:2px;flex:none;min-width:0}
3602
3937
  .dq-heat-cell--pad{background:transparent}
3603
- .dq-heat-months{display:flex;gap:3px;margin-top:4px}
3604
- .dq-heat-month{flex:1 1 0;min-width:6px;font-size:12px;line-height:15px;color:var(--dsw-alias-label-secondary,#59636e);white-space:nowrap;overflow:visible}
3938
+ .dq-heat-months{display:flex;gap:2px;margin-top:4px}
3939
+ .dq-heat-month{flex:1 1 0;min-width:0;font-size:12px;line-height:15px;color:var(--dsw-alias-label-secondary,#59636e);white-space:nowrap;overflow:visible}
3605
3940
  .dq-heat-key{width:12px;height:12px;border-radius:2px;flex:none}
3606
3941
  .dq-heat-scale{display:flex;align-items:center;gap:3px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);align-self:flex-end}
3607
3942
  .dq-heat-scale>span:first-child{margin-right:2px}
@@ -3635,6 +3970,20 @@ var css = `
3635
3970
  .dq-sync--error .dq-sync-dot{background:var(--dsw-alias-state-error-primary,#cf222e)}
3636
3971
  @keyframes dq-sync-pulse{50%{opacity:.35;transform:scale(.8)}}
3637
3972
  .dq-refresh-btn{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#59636e);border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer;transition:border-color .15s ease,color .15s ease}
3973
+ .dq-status-actions{display:flex;align-items:center;gap:8px;flex:none}
3974
+ .dq-settings-btn{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#59636e);border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer;transition:border-color .15s ease,color .15s ease}
3975
+ .dq-settings-btn:hover{border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
3976
+ .dq-settings-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
3977
+ .dq-settings-caret{font-size:10px;line-height:1;transition:transform .2s cubic-bezier(.16,1,.3,1)}
3978
+ .dq-settings-caret--open{transform:rotate(180deg)}
3979
+ .dq-settings-panel{box-sizing:border-box;min-width:0;max-width:100%;padding:20px;background:var(--dsw-alias-bg-layer-1,#ffffff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.10));border-radius:12px}
3980
+ /* Motion: eased (easeOutExpo) arrivals. Operate surface \u2014 fast, purposeful,
3981
+ and fully disabled under prefers-reduced-motion (see the global reduce block). */
3982
+ .dq-export-menu,.dq-model-menu{transform-origin:top right;animation:dq-pop .18s cubic-bezier(.16,1,.3,1)}
3983
+ @keyframes dq-pop{from{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:none}}
3984
+ .dq-alert{animation:dq-alert-in .28s cubic-bezier(.16,1,.3,1)}
3985
+ @keyframes dq-alert-in{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
3986
+ @media (max-width:620px){.dq-settings-panel{padding:16px}}
3638
3987
  .dq-refresh-btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
3639
3988
  .dq-refresh-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
3640
3989
  .dq-refresh-btn:disabled{cursor:default;opacity:.6}