@kidli1412/dsh-token-heatmap 0.1.6 → 0.4.2
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.md +37 -12
- package/docs//345/215/241/347/211/207/350/256/276/347/275/256/351/235/242/346/235/277.jpg +0 -0
- package/docs//345/271/264/350/247/206/345/233/276.jpg +0 -0
- package/docs//346/234/210/350/247/206/345/233/276.jpg +0 -0
- package/docs//351/242/204/350/247/210-/346/226/260/347/211/210/344/274/232/350/257/235/351/241/265.jpg +0 -0
- package/lib/client.js +614 -363
- package/lib/config.js +17 -4
- package/lib/index.js +131 -35
- package/lib/usage.js +6 -2
- package/package.json +2 -2
- package/docs//347/203/255/345/212/233/345/233/276.jpg +0 -0
package/lib/client.js
CHANGED
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
* dsh-token-heatmap — browser half.
|
|
3
3
|
*
|
|
4
4
|
* Hand-written `__ModuleLoader__` bundle (no build step): registers into the
|
|
5
|
-
* conversation `input.dock` list slot and renders a GitHub-style
|
|
6
|
-
*
|
|
7
|
-
* all seven weekday labels on the left)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* conversation `input.dock` list slot and renders a GitHub-style token-usage
|
|
6
|
+
* card on the new-session (hero) screen — a calendar-year grid (Jan 1 – Dec 31;
|
|
7
|
+
* columns = weeks, rows = Mon..Sun, all seven weekday labels on the left) or a
|
|
8
|
+
* single-month calendar, switchable from the header, plus today / this-month /
|
|
9
|
+
* all-time totals and a ‹period› stepper. The card configures ITSELF through a
|
|
10
|
+
* ⚙ panel (palette + default view) persisted in the `token-heatmap` settings
|
|
11
|
+
* namespace (settings scope); it deliberately does not register into the
|
|
12
|
+
* official `settings.plugin.item` seat (设置 → 插件 → 插件配置), so all of its
|
|
13
|
+
* settings live on the card.
|
|
13
14
|
*
|
|
14
|
-
* The card is shown ONLY on the new-session (hero) screen — gated on
|
|
15
|
-
* `session.composerPhase === "blank"`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* framework's true below-card seat
|
|
19
|
-
* screen).
|
|
15
|
+
* The card is shown ONLY on the new-session (hero) screen — gated on the
|
|
16
|
+
* boolean `session.blank`, with `composerPhase === "blank"` as the pre-0.1.2
|
|
17
|
+
* fallback — and is positioned visually BELOW the composer input card via a
|
|
18
|
+
* flex `order` on the dock wrapper (the dock's DOM site sits above the bar, but
|
|
19
|
+
* the composer stack is a flex column and the framework's true below-card seat
|
|
20
|
+
* `composer.dock` is disabled on the hero screen). There is no display switch:
|
|
21
|
+
* the hero screen always renders it.
|
|
20
22
|
*
|
|
21
23
|
* Cell colors use ABSOLUTE token thresholds (not window-relative ranks), so
|
|
22
24
|
* a huge day is always dark regardless of its neighbours:
|
|
@@ -35,21 +37,34 @@ window.__ModuleLoader__.load({
|
|
|
35
37
|
|
|
36
38
|
let react = require("react");
|
|
37
39
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
40
|
+
// Only for the settings panel, which is PORTALED to document.body so the
|
|
41
|
+
// card's own box can never clip it (the pattern the stat pills use).
|
|
42
|
+
let reactDom = require("react-dom");
|
|
38
43
|
|
|
39
44
|
//#region css
|
|
40
45
|
const css = [
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
// Surface matched to the composer card above it (measured computed
|
|
47
|
+
// styles): same 22px radius, no hard border — the edge is the same
|
|
48
|
+
// hairline + soft drop the composer uses — and the same 16px inner
|
|
49
|
+
// inset, so the card's text lines up with the composer's placeholder.
|
|
50
|
+
".thm_dock{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width,780px);margin:0 auto;padding:0;display:flex;flex-direction:column;gap:8px}",
|
|
51
|
+
".thm_card{box-sizing:border-box;background:var(--dsw-alias-bg-base);border:none;border-radius:22px;box-shadow:0 0 0 .5px rgba(0,0,0,.1),0 4px 16px rgba(0,0,0,.03),0 0 24px rgba(0,0,0,.03);padding:14px 16px 16px;display:flex;flex-direction:column;gap:8px}",
|
|
43
52
|
".thm_head{flex-wrap:wrap;align-items:baseline;gap:4px 12px;display:flex}",
|
|
44
53
|
".thm_title{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;line-height:18px}",
|
|
45
54
|
".thm_stat{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
46
55
|
".thm_statLabel{margin-right:4px}",
|
|
47
56
|
".thm_statValue{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
48
|
-
".
|
|
57
|
+
".thm_nav{align-items:center;gap:2px;display:inline-flex}",
|
|
49
58
|
".thm_yearBtn{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:4px;padding:0 5px;font-size:13px;line-height:18px}",
|
|
50
59
|
".thm_yearBtn:hover:not(:disabled){color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
51
60
|
".thm_yearBtn:disabled{opacity:.35;cursor:default}",
|
|
52
61
|
".thm_yearLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;min-width:36px;text-align:center;font-variant-numeric:tabular-nums}",
|
|
62
|
+
".thm_monthLabelNav{min-width:62px}",
|
|
63
|
+
".thm_viewNav{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;align-items:center;display:inline-flex;overflow:hidden}",
|
|
64
|
+
".thm_viewBtn{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;padding:1px 10px;font:inherit;font-size:11px;line-height:18px}",
|
|
65
|
+
".thm_viewBtn:hover:not(:disabled){color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
66
|
+
".thm_viewBtn[data-active=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}",
|
|
67
|
+
".thm_viewBtn:disabled{opacity:.4;cursor:default}",
|
|
53
68
|
".thm_refresh{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:6px;margin-left:auto;padding:2px 6px;font:inherit;font-size:11px;line-height:16px}",
|
|
54
69
|
".thm_refresh:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
55
70
|
".thm_refresh:disabled{opacity:.5;cursor:default}",
|
|
@@ -60,49 +75,54 @@ window.__ModuleLoader__.load({
|
|
|
60
75
|
".thm_row{display:flex;gap:2px}",
|
|
61
76
|
".thm_rowLabel{color:var(--dsw-alias-label-caption);width:16px;flex:none;font-size:9px;line-height:12px;text-align:center}",
|
|
62
77
|
".thm_cell{box-sizing:border-box;width:10px;height:10px;border-radius:2px;flex:none}",
|
|
63
|
-
|
|
64
|
-
".
|
|
78
|
+
// The calendar spans the card's full content width, so the weekday
|
|
79
|
+
// rail (一) starts exactly under the "Token 用量" title.
|
|
80
|
+
".thm_monthCal{display:flex;flex-direction:column;gap:3px}",
|
|
81
|
+
".thm_weekHead{display:flex;gap:4px}",
|
|
82
|
+
".thm_weekHeadCell{color:var(--dsw-alias-label-tertiary);flex:1;min-width:0;font-size:9px;line-height:12px;text-align:center}",
|
|
83
|
+
".thm_week{display:flex;gap:4px}",
|
|
84
|
+
".thm_day{box-sizing:border-box;flex:1;min-width:0;height:38px;border-radius:5px;flex-direction:column;align-items:center;justify-content:center;gap:1px;display:flex}",
|
|
85
|
+
".thm_day[data-weekend=true]:not([data-empty=true]){background-color:var(--dsw-alias-interactive-bg-hover)}",
|
|
86
|
+
".thm_dayNum{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:13px;font-variant-numeric:tabular-nums}",
|
|
87
|
+
".thm_dayTokens{color:var(--dsw-alias-label-tertiary);font-size:8px;line-height:10px;font-variant-numeric:tabular-nums}",
|
|
88
|
+
".thm_day[data-level=\"2\"] .thm_dayNum,.thm_day[data-level=\"3\"] .thm_dayNum,.thm_day[data-level=\"4\"] .thm_dayNum{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.28)}",
|
|
89
|
+
".thm_day[data-level=\"2\"] .thm_dayTokens,.thm_day[data-level=\"3\"] .thm_dayTokens,.thm_day[data-level=\"4\"] .thm_dayTokens{color:rgba(255,255,255,.82)}",
|
|
90
|
+
".thm_legend{align-items:center;justify-content:flex-end;gap:3px;font-size:10px;line-height:14px;display:flex;color:var(--dsw-alias-label-tertiary)}", ".thm_legendCell{box-sizing:border-box;width:9px;height:9px;border-radius:2px}",
|
|
65
91
|
".thm_error{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger);border-radius:8px;padding:7px 8px;font-size:11px;line-height:16px;display:flex;justify-content:space-between;align-items:center;gap:8px}",
|
|
66
92
|
".thm_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0}",
|
|
67
93
|
".thm_loading{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;padding:6px 2px}",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
".
|
|
71
|
-
".
|
|
72
|
-
".
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
".
|
|
78
|
-
".
|
|
79
|
-
".
|
|
80
|
-
".
|
|
81
|
-
".
|
|
82
|
-
".
|
|
83
|
-
".
|
|
84
|
-
".
|
|
85
|
-
".
|
|
86
|
-
".
|
|
87
|
-
".
|
|
88
|
-
".
|
|
89
|
-
".
|
|
90
|
-
".
|
|
91
|
-
".
|
|
92
|
-
".
|
|
93
|
-
".
|
|
94
|
-
".
|
|
95
|
-
".
|
|
96
|
-
".
|
|
97
|
-
".
|
|
98
|
-
".
|
|
99
|
-
".
|
|
100
|
-
".thms_footerError{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}",
|
|
101
|
-
".thms_btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}",
|
|
102
|
-
".thms_btn:disabled{opacity:.4;cursor:default}",
|
|
103
|
-
".thms_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}",
|
|
104
|
-
".thms_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}",
|
|
105
|
-
".thms_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}"
|
|
94
|
+
// The ⚙ at the row's right end opens a floating pill panel — the
|
|
95
|
+
// card configures itself; nothing registers into 设置 → 插件 → 插件配置.
|
|
96
|
+
".thm_gear{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:6px;align-items:center;padding:2px 4px;display:inline-flex}",
|
|
97
|
+
".thm_gear:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
98
|
+
".thm_gear[data-active=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
99
|
+
// The settings panel mirrors DSH's own pill dialogs: portaled to
|
|
100
|
+
// document.body, position:fixed (so the card can never clip it),
|
|
101
|
+
// measure-then-place above the ⚙ and clamped to the viewport, with
|
|
102
|
+
// the shipped dialog surface + elevation tokens.
|
|
103
|
+
".thm_panel{position:fixed;z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu,#fff);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:400px;max-width:calc(100vw - 24px);box-shadow:var(--dsw-elevation-prominent,0 8px 32px rgba(0,0,0,.16));color:var(--dsw-alias-label-secondary);cursor:default;border:0;border-radius:12px;padding:16px;font-size:12px;line-height:18px;text-align:left;font-weight:400}",
|
|
104
|
+
".thm_panelHead{color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:center;gap:16px;margin-bottom:8px;font-weight:500;display:flex}",
|
|
105
|
+
".thm_panelTitleLabel{align-items:center;gap:6px;display:inline-flex}",
|
|
106
|
+
".thm_panelTitleLabel svg{flex:none;width:14px;height:14px}",
|
|
107
|
+
".thm_panelRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:12px}",
|
|
108
|
+
".thm_group{flex-direction:column;gap:5px;display:flex}",
|
|
109
|
+
".thm_group+.thm_group{margin-top:14px}",
|
|
110
|
+
".thm_groupLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
111
|
+
".thm_swatches{flex-wrap:wrap;gap:6px;display:flex}",
|
|
112
|
+
".thm_swatch{cursor:pointer;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:6px;padding:3px 8px;font:inherit;display:flex}",
|
|
113
|
+
".thm_swatch:hover{border-color:var(--dsw-alias-label-dimmed)}",
|
|
114
|
+
".thm_swatch[data-active=true]{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary) inset}",
|
|
115
|
+
".thm_swatchCells{display:flex;gap:2px}",
|
|
116
|
+
".thm_swatchCell{width:9px;height:9px;border-radius:2px}",
|
|
117
|
+
".thm_swatchLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}",
|
|
118
|
+
".thm_seg{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;align-items:center;display:inline-flex;overflow:hidden;width:max-content}",
|
|
119
|
+
".thm_segBtn{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;padding:1px 10px;font:inherit;font-size:11px;line-height:18px}",
|
|
120
|
+
".thm_segBtn:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
121
|
+
".thm_segBtn[data-active=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}",
|
|
122
|
+
".thm_panelFoot{border-top:.5px solid var(--dsw-alias-border-l2);margin-top:14px;padding-top:10px}",
|
|
123
|
+
".thm_footLegend{align-items:center;gap:3px;font-size:10px;line-height:14px;display:flex;color:var(--dsw-alias-label-tertiary)}",
|
|
124
|
+
".thm_footRule{color:var(--dsw-alias-label-caption);margin-top:6px;font-size:10px;line-height:14px}",
|
|
125
|
+
".thm_footError{color:var(--dsw-alias-state-error-primary);margin-top:6px;font-size:10px;line-height:14px}"
|
|
106
126
|
].join("");
|
|
107
127
|
const tagId = "@kidli1412/dsh-token-heatmap/TokenHeatmap.module.css";
|
|
108
128
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
@@ -120,9 +140,12 @@ window.__ModuleLoader__.load({
|
|
|
120
140
|
stat: "thm_stat",
|
|
121
141
|
statLabel: "thm_statLabel",
|
|
122
142
|
statValue: "thm_statValue",
|
|
123
|
-
|
|
143
|
+
nav: "thm_nav",
|
|
124
144
|
yearBtn: "thm_yearBtn",
|
|
125
145
|
yearLabel: "thm_yearLabel",
|
|
146
|
+
monthLabelNav: "thm_monthLabelNav",
|
|
147
|
+
viewNav: "thm_viewNav",
|
|
148
|
+
viewBtn: "thm_viewBtn",
|
|
126
149
|
refresh: "thm_refresh",
|
|
127
150
|
gridWrap: "thm_gridWrap",
|
|
128
151
|
grid: "thm_grid",
|
|
@@ -131,41 +154,36 @@ window.__ModuleLoader__.load({
|
|
|
131
154
|
row: "thm_row",
|
|
132
155
|
rowLabel: "thm_rowLabel",
|
|
133
156
|
cell: "thm_cell",
|
|
157
|
+
monthCal: "thm_monthCal",
|
|
158
|
+
weekHead: "thm_weekHead",
|
|
159
|
+
weekHeadCell: "thm_weekHeadCell",
|
|
160
|
+
week: "thm_week",
|
|
161
|
+
day: "thm_day",
|
|
162
|
+
dayNum: "thm_dayNum",
|
|
163
|
+
dayTokens: "thm_dayTokens",
|
|
134
164
|
legend: "thm_legend",
|
|
135
165
|
legendCell: "thm_legendCell",
|
|
136
166
|
error: "thm_error",
|
|
137
167
|
retry: "thm_retry",
|
|
138
168
|
loading: "thm_loading",
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
settingsSwitchOn: "thms_switchOn",
|
|
158
|
-
settingsThumb: "thms_thumb",
|
|
159
|
-
settingsSwatches: "thms_swatches",
|
|
160
|
-
settingsSwatch: "thms_swatch",
|
|
161
|
-
settingsSwatchCells: "thms_swatchCells",
|
|
162
|
-
settingsSwatchCell: "thms_swatchCell",
|
|
163
|
-
settingsSwatchLabel: "thms_swatchLabel",
|
|
164
|
-
settingsFooter: "thms_footer",
|
|
165
|
-
settingsFooterError: "thms_footerError",
|
|
166
|
-
settingsBtn: "thms_btn",
|
|
167
|
-
settingsDiscard: "thms_discard",
|
|
168
|
-
settingsSave: "thms_save"
|
|
169
|
+
gear: "thm_gear",
|
|
170
|
+
settingsPanel: "thm_panel",
|
|
171
|
+
panelHead: "thm_panelHead",
|
|
172
|
+
panelTitleLabel: "thm_panelTitleLabel",
|
|
173
|
+
panelRule: "thm_panelRule",
|
|
174
|
+
panelFoot: "thm_panelFoot",
|
|
175
|
+
footLegend: "thm_footLegend",
|
|
176
|
+
footRule: "thm_footRule",
|
|
177
|
+
footError: "thm_footError",
|
|
178
|
+
settingsGroup: "thm_group",
|
|
179
|
+
settingsGroupLabel: "thm_groupLabel",
|
|
180
|
+
settingsSwatches: "thm_swatches",
|
|
181
|
+
settingsSwatch: "thm_swatch",
|
|
182
|
+
settingsSwatchCells: "thm_swatchCells",
|
|
183
|
+
settingsSwatchCell: "thm_swatchCell",
|
|
184
|
+
settingsSwatchLabel: "thm_swatchLabel",
|
|
185
|
+
seg: "thm_seg",
|
|
186
|
+
segBtn: "thm_segBtn"
|
|
169
187
|
};
|
|
170
188
|
//#endregion
|
|
171
189
|
|
|
@@ -184,31 +202,32 @@ window.__ModuleLoader__.load({
|
|
|
184
202
|
total: "累计",
|
|
185
203
|
prevYear: "上一年",
|
|
186
204
|
nextYear: "下一年",
|
|
205
|
+
prevMonth: "上一月",
|
|
206
|
+
nextMonth: "下一月",
|
|
207
|
+
viewYear: "年",
|
|
208
|
+
viewMonth: "月",
|
|
209
|
+
viewLabel: "视图",
|
|
187
210
|
less: "少",
|
|
188
211
|
more: "多",
|
|
189
212
|
cellTokens: "{date} · {tokens} tokens",
|
|
190
213
|
weekdayLabel: ["一", "二", "三", "四", "五", "六", "日"],
|
|
191
214
|
monthName: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
|
|
215
|
+
dayLabel: (monthName, day) => `${monthName}${day}日`,
|
|
216
|
+
monthYear: (year, monthName) => `${year}年${monthName}`,
|
|
192
217
|
levelLegend: ["0", "<1M", "1M–10M", "10M–100M", "≥100M"],
|
|
193
218
|
settingsTitle: "Token 用量热力图",
|
|
194
|
-
settingsDescription: "新会话页面的 Token 用量热力图显示与配色。",
|
|
195
|
-
settingsEnabled: "显示热力图",
|
|
196
|
-
settingsEnabledHint: "关闭后,新会话输入框下方的 Token 用量热力图不再显示。",
|
|
197
219
|
settingsScheme: "配色方案",
|
|
198
220
|
settingsSchemeHint: "热力图格子的颜色主题:绿色为经典 GitHub 风格,其余为新增配色。",
|
|
221
|
+
settingsDefaultView: "默认视图",
|
|
222
|
+
settingsDefaultViewHint: "新会话页面首次打开时显示的视图(标题行右端可随时切换)。",
|
|
199
223
|
settingsGreen: "绿色",
|
|
200
224
|
settingsBlue: "蓝色",
|
|
201
225
|
settingsOrange: "橙色",
|
|
202
226
|
settingsRed: "红色",
|
|
203
227
|
settingsPurple: "紫色",
|
|
204
228
|
settingsTeal: "青色",
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
settingsDiscard: "放弃修改",
|
|
208
|
-
settingsUnsaved: "未保存",
|
|
209
|
-
settingsSaveFailed: "保存失败,修改已保留,请重试。",
|
|
210
|
-
settingsLoadFailed: "配置加载失败。",
|
|
211
|
-
settingsRetry: "重试",
|
|
229
|
+
settingsThresholdNote: "颜色按每天 token 数的绝对阈值分档(悬停图例看各档范围)。",
|
|
230
|
+
settingsSaveFailed: "保存失败,已回到服务端的值,请重试。",
|
|
212
231
|
settingsExpand: "展开设置",
|
|
213
232
|
settingsCollapse: "收起设置"
|
|
214
233
|
};
|
|
@@ -224,31 +243,32 @@ window.__ModuleLoader__.load({
|
|
|
224
243
|
total: "All time",
|
|
225
244
|
prevYear: "Previous year",
|
|
226
245
|
nextYear: "Next year",
|
|
246
|
+
prevMonth: "Previous month",
|
|
247
|
+
nextMonth: "Next month",
|
|
248
|
+
viewYear: "Year",
|
|
249
|
+
viewMonth: "Month",
|
|
250
|
+
viewLabel: "View",
|
|
227
251
|
less: "Less",
|
|
228
252
|
more: "More",
|
|
229
253
|
cellTokens: "{date} · {tokens} tokens",
|
|
230
254
|
weekdayLabel: ["M", "T", "W", "T", "F", "S", "S"],
|
|
231
255
|
monthName: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
|
256
|
+
dayLabel: (monthName, day) => `${monthName} ${day}`,
|
|
257
|
+
monthYear: (year, monthName) => `${monthName} ${year}`,
|
|
232
258
|
levelLegend: ["0", "<1M", "1M–10M", "10M–100M", "≥100M"],
|
|
233
259
|
settingsTitle: "Token usage heatmap",
|
|
234
|
-
settingsDescription: "Display and colors of the token heatmap on the new-session screen.",
|
|
235
|
-
settingsEnabled: "Show heatmap",
|
|
236
|
-
settingsEnabledHint: "When off, the token heatmap below the composer is hidden.",
|
|
237
260
|
settingsScheme: "Color scheme",
|
|
238
261
|
settingsSchemeHint: "Cell palette: green is the classic GitHub style; the rest are new themes.",
|
|
262
|
+
settingsDefaultView: "Default view",
|
|
263
|
+
settingsDefaultViewHint: "The view the new-session screen opens with (switchable anytime at the row's right end).",
|
|
239
264
|
settingsGreen: "Green",
|
|
240
265
|
settingsBlue: "Blue",
|
|
241
266
|
settingsOrange: "Orange",
|
|
242
267
|
settingsRed: "Red",
|
|
243
268
|
settingsPurple: "Purple",
|
|
244
269
|
settingsTeal: "Teal",
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
settingsDiscard: "Discard",
|
|
248
|
-
settingsUnsaved: "Unsaved",
|
|
249
|
-
settingsSaveFailed: "Save failed; your changes were kept.",
|
|
250
|
-
settingsLoadFailed: "Failed to load configuration.",
|
|
251
|
-
settingsRetry: "Retry",
|
|
270
|
+
settingsThresholdNote: "Colors follow absolute per-day token thresholds (hover the legend for each range).",
|
|
271
|
+
settingsSaveFailed: "Save failed; the server value was restored.",
|
|
252
272
|
settingsExpand: "Show settings",
|
|
253
273
|
settingsCollapse: "Hide settings"
|
|
254
274
|
};
|
|
@@ -293,6 +313,19 @@ window.__ModuleLoader__.load({
|
|
|
293
313
|
return key.slice(0, 7);
|
|
294
314
|
}
|
|
295
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Step a `YYYY-MM` key by whole months ("2026-01" -1 → "2025-12"),
|
|
318
|
+
* returning the input unchanged when it is not a month key.
|
|
319
|
+
* @param key - `YYYY-MM` month key.
|
|
320
|
+
* @param delta - signed month count.
|
|
321
|
+
*/
|
|
322
|
+
function shiftMonthKey(key, delta) {
|
|
323
|
+
const [year, month] = key.split("-").map(Number);
|
|
324
|
+
if (!Number.isFinite(year) || !Number.isFinite(month)) return key;
|
|
325
|
+
const date = new Date(year, month - 1 + delta, 1);
|
|
326
|
+
return monthKey(date.getTime());
|
|
327
|
+
}
|
|
328
|
+
|
|
296
329
|
/**
|
|
297
330
|
* Pick the display dictionary for locale-shaped helpers (month names,
|
|
298
331
|
* weekday labels, date formatting). The locale plugin resolves the UI
|
|
@@ -305,13 +338,31 @@ window.__ModuleLoader__.load({
|
|
|
305
338
|
return candidates.some((tag) => tag.toLowerCase().startsWith("zh")) ? zh : en;
|
|
306
339
|
}
|
|
307
340
|
|
|
308
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Display date "8月13日" / "Aug 13" — the whole shape comes from the
|
|
343
|
+
* dictionary, so an English UI never renders a Chinese 日 and the
|
|
344
|
+
* Chinese one never grows a space before it.
|
|
345
|
+
*/
|
|
309
346
|
function dateLabel(key) {
|
|
310
347
|
const [year, month, day] = key.split("-").map(Number);
|
|
311
|
-
const
|
|
312
|
-
return
|
|
348
|
+
const dict = uiDict();
|
|
349
|
+
return dict.dayLabel(dict.monthName[month - 1], day);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Month-key navigation label, ordered per locale: "2026年8月" in Chinese
|
|
354
|
+
* (year first), "Aug 2026" in English (month first) — each dictionary
|
|
355
|
+
* owns its ordering rather than both sharing a Chinese-shaped template.
|
|
356
|
+
*/
|
|
357
|
+
function monthLabelFull(key) {
|
|
358
|
+
const [year, month] = key.split("-").map(Number);
|
|
359
|
+
const dict = uiDict();
|
|
360
|
+
return dict.monthYear(year, dict.monthName[month - 1]);
|
|
313
361
|
}
|
|
314
362
|
|
|
363
|
+
/** Heatmap view modes: the calendar-year grid or a single-month calendar. */
|
|
364
|
+
const VIEW_MODES = ["year", "month"];
|
|
365
|
+
|
|
315
366
|
/** Per-request staleness guard: only the most recent start may `isCurrent()`. */
|
|
316
367
|
function createLoader() {
|
|
317
368
|
let current = 0;
|
|
@@ -332,25 +383,32 @@ window.__ModuleLoader__.load({
|
|
|
332
383
|
|
|
333
384
|
//#region config store (settings-scope backed)
|
|
334
385
|
/**
|
|
335
|
-
* Display preferences (
|
|
336
|
-
* Host `token-heatmap` settings namespace (settings.yaml) via the
|
|
337
|
-
* settings scope — NOT the legacy loopback config endpoint. The
|
|
386
|
+
* Display preferences (colorScheme + defaultView), persisted through
|
|
387
|
+
* the Host `token-heatmap` settings namespace (settings.yaml) via the
|
|
388
|
+
* bound settings scope — NOT the legacy loopback config endpoint. The
|
|
338
389
|
* heatmap and the settings card read the store through
|
|
339
390
|
* useSyncExternalStore; while the scope is loading or unavailable the
|
|
340
|
-
* store falls back to the defaults (
|
|
341
|
-
* blocks on the settings transport.
|
|
391
|
+
* store falls back to the defaults (green / year view), so the UI never
|
|
392
|
+
* blocks on the settings transport. The 0.1.x display switch is gone:
|
|
393
|
+
* its `enabled` key is simply no longer read (see the settings card).
|
|
342
394
|
*/
|
|
343
|
-
const CONFIG_DEFAULTS = {
|
|
395
|
+
const CONFIG_DEFAULTS = { colorScheme: "green", defaultView: "year" };
|
|
344
396
|
/** Coerce a scheme to a non-blank string, else the default. */
|
|
345
397
|
function sanitizeScheme(value) {
|
|
346
398
|
if (typeof value !== "string") return CONFIG_DEFAULTS.colorScheme;
|
|
347
399
|
const scheme = value.trim();
|
|
348
400
|
return scheme.length > 0 && scheme.length <= 32 ? scheme : CONFIG_DEFAULTS.colorScheme;
|
|
349
401
|
}
|
|
402
|
+
/** Coerce a view mode to a known mode, else the default (year). */
|
|
403
|
+
function sanitizeView(value) {
|
|
404
|
+
if (typeof value !== "string") return CONFIG_DEFAULTS.defaultView;
|
|
405
|
+
const view = value.trim();
|
|
406
|
+
return VIEW_MODES.includes(view) ? view : CONFIG_DEFAULTS.defaultView;
|
|
407
|
+
}
|
|
350
408
|
/**
|
|
351
409
|
* Reactive adapter over one bound settings scope: a
|
|
352
410
|
* useSyncExternalStore-compatible store whose snapshot is
|
|
353
|
-
* `{ status,
|
|
411
|
+
* `{ status, colorScheme, defaultView }` derived from the scope.
|
|
354
412
|
* `set(patch)` writes the touched fields through scope.set (async; the
|
|
355
413
|
* scope fences revisions and reloads on failure), and the snapshot
|
|
356
414
|
* only changes after the Host confirms. `reload()` re-reads from the
|
|
@@ -367,8 +425,8 @@ window.__ModuleLoader__.load({
|
|
|
367
425
|
const v = s.value;
|
|
368
426
|
return {
|
|
369
427
|
status: s.status,
|
|
370
|
-
|
|
371
|
-
|
|
428
|
+
colorScheme: sanitizeScheme(v.colorScheme),
|
|
429
|
+
defaultView: sanitizeView(v.defaultView)
|
|
372
430
|
};
|
|
373
431
|
};
|
|
374
432
|
const notify = () => {
|
|
@@ -389,12 +447,12 @@ window.__ModuleLoader__.load({
|
|
|
389
447
|
};
|
|
390
448
|
},
|
|
391
449
|
set: async (patch) => {
|
|
392
|
-
if (Object.hasOwn(patch, "enabled")) {
|
|
393
|
-
await scope.set("enabled", patch.enabled === true);
|
|
394
|
-
}
|
|
395
450
|
if (Object.hasOwn(patch, "colorScheme")) {
|
|
396
451
|
await scope.set("colorScheme", sanitizeScheme(patch.colorScheme));
|
|
397
452
|
}
|
|
453
|
+
if (Object.hasOwn(patch, "defaultView")) {
|
|
454
|
+
await scope.set("defaultView", sanitizeView(patch.defaultView));
|
|
455
|
+
}
|
|
398
456
|
},
|
|
399
457
|
reload: () => scope.load(),
|
|
400
458
|
dispose: () => unsubscribe()
|
|
@@ -490,13 +548,56 @@ window.__ModuleLoader__.load({
|
|
|
490
548
|
}
|
|
491
549
|
return { columns, monthStarts };
|
|
492
550
|
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Build the calendar grid for ONE MONTH (the 年/月 view switch's month
|
|
554
|
+
* view): Monday-first weeks, exactly as many week rows as the month
|
|
555
|
+
* spans (5 or 6), cells outside the month null. Days after today are
|
|
556
|
+
* empty level-0 cells (today itself is always rendered with its real
|
|
557
|
+
* value), and every cell carries `day` (day of month) so the calendar
|
|
558
|
+
* can print numbers on top of the same absolute-threshold heat colors
|
|
559
|
+
* the year view uses.
|
|
560
|
+
* @param dayMap - date key → tokens.
|
|
561
|
+
* @param now - reference timestamp.
|
|
562
|
+
* @param month - `YYYY-MM` to render; defaults to the month of `now`.
|
|
563
|
+
* @returns `{ month, weeks, weekdays }` where each week is
|
|
564
|
+
* `Array<{key, tokens, level, day} | null>` of 7 cells.
|
|
565
|
+
*/
|
|
566
|
+
function buildMonthGrid(dayMap, now, month) {
|
|
567
|
+
const nowDate = new Date(now);
|
|
568
|
+
const todayKey = dayKey(nowDate.getTime());
|
|
569
|
+
const target = month ?? monthKey(nowDate.getTime());
|
|
570
|
+
const [year, monthNumber] = target.split("-").map(Number);
|
|
571
|
+
const monthStart = new Date(year, monthNumber - 1, 1);
|
|
572
|
+
const monthEnd = new Date(year, monthNumber, 0);
|
|
573
|
+
const startKey = dayKey(monthStart.getTime());
|
|
574
|
+
const endKey = dayKey(monthEnd.getTime());
|
|
575
|
+
const cursor = new Date(year, monthNumber - 1, 1 - ((monthStart.getDay() + 6) % 7));
|
|
576
|
+
const weeks = [];
|
|
577
|
+
while (cursor.getTime() <= monthEnd.getTime()) {
|
|
578
|
+
const cells = [];
|
|
579
|
+
for (let d = 0; d < 7; d += 1) {
|
|
580
|
+
const key = dayKey(cursor.getTime());
|
|
581
|
+
if (key < startKey || key > endKey) {
|
|
582
|
+
cells.push(null);
|
|
583
|
+
} else {
|
|
584
|
+
const future = key > todayKey;
|
|
585
|
+
const tokens = future ? 0 : (dayMap.get(key) ?? 0);
|
|
586
|
+
cells.push({ key, tokens, level: levelOf(tokens), day: cursor.getDate() });
|
|
587
|
+
}
|
|
588
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
589
|
+
}
|
|
590
|
+
weeks.push(cells);
|
|
591
|
+
}
|
|
592
|
+
return { month: target, weeks, weekdays: 7 };
|
|
593
|
+
}
|
|
493
594
|
//#endregion
|
|
494
595
|
|
|
495
596
|
//#region TokenHeatmap
|
|
496
597
|
/**
|
|
497
598
|
* The heatmap card. Shown only on the new-session hero screen
|
|
498
|
-
* (`composerPhase === "blank"`
|
|
499
|
-
* it below the composer input card.
|
|
599
|
+
* (`session.blank`, or `composerPhase === "blank"` on older hosts); a
|
|
600
|
+
* flex `order` on the wrapper places it below the composer input card.
|
|
500
601
|
* @param props - `session`/`input` from the InputZone owner share, `t`
|
|
501
602
|
* bound by the slot runtime locale seat.
|
|
502
603
|
*/
|
|
@@ -507,6 +608,38 @@ window.__ModuleLoader__.load({
|
|
|
507
608
|
const [error, setError] = react.useState(null);
|
|
508
609
|
const [loading, setLoading] = react.useState(false);
|
|
509
610
|
const [year, setYear] = react.useState(() => new Date().getFullYear());
|
|
611
|
+
// Active view mode, seeded from the persisted default (the 年/月
|
|
612
|
+
// switch at the row's right end; a manual switch wins for this
|
|
613
|
+
// mount).
|
|
614
|
+
const [view, setView] = react.useState(() => sanitizeView(configStore?.getSnapshot().defaultView));
|
|
615
|
+
// Month-view cursor (`YYYY-MM`); initialised from the current month
|
|
616
|
+
// and re-anchored when it would fall outside the navigable bounds.
|
|
617
|
+
const [monthCursor, setMonthCursor] = react.useState(() => monthKey(Date.now()));
|
|
618
|
+
// The ⚙ at the row's right end opens the floating settings panel;
|
|
619
|
+
// the card configures itself here instead of living in
|
|
620
|
+
// 设置 → 插件 → 插件配置. The panel is PORTALED to document.body and
|
|
621
|
+
// anchored to this button, so the open state lives here with it.
|
|
622
|
+
const gearRef = react.useRef(null);
|
|
623
|
+
const [settingsOpen, setSettingsOpen] = react.useState(false);
|
|
624
|
+
react.useEffect(() => {
|
|
625
|
+
if (!settingsOpen) return;
|
|
626
|
+
const onKeyDown = (event) => {
|
|
627
|
+
if (event.key === "Escape") setSettingsOpen(false);
|
|
628
|
+
};
|
|
629
|
+
const onPointerDown = (event) => {
|
|
630
|
+
const gear = gearRef.current;
|
|
631
|
+
if (gear !== null && gear.contains(event.target)) return;
|
|
632
|
+
const panel = document.querySelector(`.${S.settingsPanel}`);
|
|
633
|
+
if (panel !== null && panel.contains(event.target)) return;
|
|
634
|
+
setSettingsOpen(false);
|
|
635
|
+
};
|
|
636
|
+
document.addEventListener("keydown", onKeyDown);
|
|
637
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
638
|
+
return () => {
|
|
639
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
640
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
641
|
+
};
|
|
642
|
+
}, [settingsOpen]);
|
|
510
643
|
const loaderRef = react.useRef(null);
|
|
511
644
|
if (loaderRef.current === null) loaderRef.current = createLoader();
|
|
512
645
|
const load = react.useCallback(async () => {
|
|
@@ -545,17 +678,18 @@ window.__ModuleLoader__.load({
|
|
|
545
678
|
// New-session hero only. DSH 0.1.2+ (rc.1) exposes the state as
|
|
546
679
|
// the boolean `session.blank` (the `composerPhase` string is
|
|
547
680
|
// gone); older harnesses keep `composerPhase === "blank"`.
|
|
548
|
-
// Conversed sessions hide the card.
|
|
681
|
+
// Conversed sessions hide the card. There is no display switch any
|
|
682
|
+
// more: the card is always shown on the hero screen, and the
|
|
683
|
+
// settings card only carries presentation choices.
|
|
549
684
|
if (session === void 0 || session === null || input === void 0 || input === null) return null;
|
|
550
685
|
const heroBlank = typeof session.blank === "boolean" ? session.blank === true : session.composerPhase === "blank";
|
|
551
686
|
if (!heroBlank) return null;
|
|
552
|
-
// Master switch from the settings card (设置 → 插件 → 插件配置).
|
|
553
|
-
if (config.enabled === false) return null;
|
|
554
687
|
|
|
555
688
|
const now = Date.now();
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
689
|
+
// Navigation bounds, derived from the data itself: at most the
|
|
690
|
+
// current year/month (future usage cannot exist) and at least the
|
|
691
|
+
// earliest one present, so the ‹ › selectors never wander into
|
|
692
|
+
// guaranteed-empty territory.
|
|
559
693
|
const nowYear = new Date(now).getFullYear();
|
|
560
694
|
let minYear = nowYear;
|
|
561
695
|
for (const day of data?.days ?? []) {
|
|
@@ -563,21 +697,47 @@ window.__ModuleLoader__.load({
|
|
|
563
697
|
if (y < minYear) minYear = y;
|
|
564
698
|
}
|
|
565
699
|
const maxYear = nowYear;
|
|
700
|
+
const currentMonthKey = monthKey(now);
|
|
701
|
+
let minMonth = currentMonthKey;
|
|
702
|
+
for (const day of data?.days ?? []) {
|
|
703
|
+
const key = monthOfDateKey(day.date);
|
|
704
|
+
if (key < minMonth) minMonth = key;
|
|
705
|
+
}
|
|
706
|
+
const maxMonth = currentMonthKey;
|
|
566
707
|
const todayTokens = data?.days?.find((day) => day.date === dayKey(now))?.tokens ?? 0;
|
|
567
|
-
const monthPrefix =
|
|
708
|
+
const monthPrefix = currentMonthKey;
|
|
568
709
|
let monthTokens = 0;
|
|
569
710
|
for (const day of data?.days ?? []) {
|
|
570
711
|
if (day.date.startsWith(monthPrefix)) monthTokens += day.tokens;
|
|
571
712
|
}
|
|
572
713
|
const totalTokens = data?.total ?? 0;
|
|
714
|
+
// Keep the last known cursor, but never outside the bounds (a
|
|
715
|
+
// brand-new store of only old months must not open on next month).
|
|
716
|
+
const activeMonth = monthCursor < minMonth
|
|
717
|
+
? minMonth
|
|
718
|
+
: monthCursor > maxMonth ? maxMonth : monthCursor;
|
|
573
719
|
|
|
574
720
|
const dayMap = new Map();
|
|
575
721
|
for (const day of data?.days ?? []) dayMap.set(day.date, day.tokens);
|
|
576
722
|
const grid = buildGrid(dayMap, now, year);
|
|
723
|
+
const monthGrid = buildMonthGrid(dayMap, now, activeMonth);
|
|
577
724
|
const levelLegend = uiDict().levelLegend;
|
|
578
|
-
// Palette from the settings
|
|
725
|
+
// Palette from the in-card settings panel; unknown schemes fall back
|
|
726
|
+
// to green.
|
|
579
727
|
const palette = COLOR_SCHEMES[config.colorScheme] ?? COLOR_SCHEMES.green;
|
|
580
728
|
|
|
729
|
+
// ‹ › step the active view: years in year view, months in month view.
|
|
730
|
+
const stepBack = () => {
|
|
731
|
+
if (view === "month") setMonthCursor(shiftMonthKey(activeMonth, -1));
|
|
732
|
+
else setYear(year - 1);
|
|
733
|
+
};
|
|
734
|
+
const stepForward = () => {
|
|
735
|
+
if (view === "month") setMonthCursor(shiftMonthKey(activeMonth, 1));
|
|
736
|
+
else setYear(year + 1);
|
|
737
|
+
};
|
|
738
|
+
const backDisabled = view === "month" ? activeMonth <= minMonth : year <= minYear;
|
|
739
|
+
const forwardDisabled = view === "month" ? activeMonth >= maxMonth : year >= maxYear;
|
|
740
|
+
|
|
581
741
|
const stats = [
|
|
582
742
|
{ label: dict("today"), value: todayTokens },
|
|
583
743
|
{ label: dict("month"), value: monthTokens },
|
|
@@ -622,37 +782,123 @@ window.__ModuleLoader__.load({
|
|
|
622
782
|
]
|
|
623
783
|
}, stat.label)),
|
|
624
784
|
react_jsx_runtime.jsxs("span", {
|
|
625
|
-
className: S.
|
|
785
|
+
className: S.nav,
|
|
626
786
|
children: [
|
|
627
787
|
react_jsx_runtime.jsx("button", {
|
|
628
788
|
type: "button",
|
|
629
789
|
className: S.yearBtn,
|
|
630
|
-
disabled:
|
|
631
|
-
onClick:
|
|
632
|
-
"aria-label": dict("prevYear"),
|
|
790
|
+
disabled: backDisabled,
|
|
791
|
+
onClick: stepBack,
|
|
792
|
+
"aria-label": dict(view === "month" ? "prevMonth" : "prevYear"),
|
|
633
793
|
children: "\u2039"
|
|
634
794
|
}),
|
|
635
|
-
react_jsx_runtime.jsx("span", {
|
|
795
|
+
view === "month" ? react_jsx_runtime.jsx("span", {
|
|
796
|
+
className: `${S.yearLabel} ${S.monthLabelNav}`,
|
|
797
|
+
children: monthLabelFull(activeMonth)
|
|
798
|
+
}) : react_jsx_runtime.jsx("span", { className: S.yearLabel, children: String(year) }),
|
|
636
799
|
react_jsx_runtime.jsx("button", {
|
|
637
800
|
type: "button",
|
|
638
801
|
className: S.yearBtn,
|
|
639
|
-
disabled:
|
|
640
|
-
onClick:
|
|
641
|
-
"aria-label": dict("nextYear"),
|
|
802
|
+
disabled: forwardDisabled,
|
|
803
|
+
onClick: stepForward,
|
|
804
|
+
"aria-label": dict(view === "month" ? "nextMonth" : "nextYear"),
|
|
642
805
|
children: "\u203a"
|
|
643
806
|
})
|
|
644
807
|
]
|
|
645
808
|
}),
|
|
809
|
+
// The view switch sits next to the ‹ › stepper it pairs
|
|
810
|
+
// with; the ⚙ closes the row on the right.
|
|
811
|
+
react_jsx_runtime.jsx("span", {
|
|
812
|
+
className: S.viewNav,
|
|
813
|
+
role: "group",
|
|
814
|
+
"aria-label": dict("viewLabel"),
|
|
815
|
+
children: VIEW_MODES.map((mode) => react_jsx_runtime.jsx("button", {
|
|
816
|
+
type: "button",
|
|
817
|
+
className: S.viewBtn,
|
|
818
|
+
"data-active": view === mode,
|
|
819
|
+
"aria-pressed": view === mode,
|
|
820
|
+
onClick: () => setView(mode),
|
|
821
|
+
children: dict(mode === "month" ? "viewMonth" : "viewYear")
|
|
822
|
+
}, mode))
|
|
823
|
+
}),
|
|
646
824
|
react_jsx_runtime.jsx("button", {
|
|
647
825
|
type: "button",
|
|
648
826
|
className: S.refresh,
|
|
649
827
|
disabled: loading,
|
|
650
828
|
onClick: load,
|
|
651
829
|
children: dict("refresh")
|
|
830
|
+
}),
|
|
831
|
+
// The ⚙ owns the row's right end; the ‹ › stepper
|
|
832
|
+
// and the view switch sit left of it, the stepper
|
|
833
|
+
// next to the stats it moves through. This button is
|
|
834
|
+
// also the settings panel's anchor.
|
|
835
|
+
react_jsx_runtime.jsx("button", {
|
|
836
|
+
type: "button",
|
|
837
|
+
ref: gearRef,
|
|
838
|
+
className: S.gear,
|
|
839
|
+
"data-active": settingsOpen,
|
|
840
|
+
"aria-haspopup": "dialog",
|
|
841
|
+
"aria-expanded": settingsOpen,
|
|
842
|
+
"aria-label": dict(settingsOpen ? "settingsCollapse" : "settingsExpand"),
|
|
843
|
+
title: dict(settingsOpen ? "settingsCollapse" : "settingsExpand"),
|
|
844
|
+
onClick: () => setSettingsOpen(!settingsOpen),
|
|
845
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
846
|
+
width: 14,
|
|
847
|
+
height: 14,
|
|
848
|
+
viewBox: "0 0 24 24",
|
|
849
|
+
fill: "none",
|
|
850
|
+
stroke: "currentColor",
|
|
851
|
+
"aria-hidden": "true",
|
|
852
|
+
children: react_jsx_runtime.jsx("path", {
|
|
853
|
+
d: "M10.9 2.6h2.2l.4 2.5 1.9.8 2-1.5 1.6 1.6-1.5 2 .8 1.9 2.5.4v2.2l-2.5.4-.8 1.9 1.5 2-1.6 1.6-2-1.5-1.9.8-.4 2.5h-2.2l-.4-2.5-1.9-.8-2 1.5-1.6-1.6 1.5-2-.8-1.9-2.5-.4v-2.2l2.5-.4.8-1.9-1.5-2 1.6-1.6 2 1.5 1.9-.8.4-2.5Z",
|
|
854
|
+
"stroke-width": 1.5,
|
|
855
|
+
"stroke-linejoin": "round",
|
|
856
|
+
"stroke-linecap": "round"
|
|
857
|
+
})
|
|
858
|
+
})
|
|
652
859
|
})
|
|
653
860
|
]
|
|
654
861
|
}),
|
|
655
|
-
react_jsx_runtime.jsx("div", {
|
|
862
|
+
view === "month" ? react_jsx_runtime.jsx("div", {
|
|
863
|
+
className: S.gridWrap,
|
|
864
|
+
children: react_jsx_runtime.jsxs("div", {
|
|
865
|
+
className: S.monthCal,
|
|
866
|
+
children: [
|
|
867
|
+
react_jsx_runtime.jsx("div", {
|
|
868
|
+
className: S.weekHead,
|
|
869
|
+
children: uiDict().weekdayLabel.map((label, index) => react_jsx_runtime.jsx("span", {
|
|
870
|
+
className: S.weekHeadCell,
|
|
871
|
+
children: label
|
|
872
|
+
}, `weekhead-${index}`))
|
|
873
|
+
}),
|
|
874
|
+
...monthGrid.weeks.map((week, weekIndex) => react_jsx_runtime.jsx("div", {
|
|
875
|
+
className: S.week,
|
|
876
|
+
children: week.map((cell, dayIndex) => {
|
|
877
|
+
if (cell === null) {
|
|
878
|
+
return react_jsx_runtime.jsx("span", {
|
|
879
|
+
className: S.day,
|
|
880
|
+
"data-empty": "true"
|
|
881
|
+
}, `empty-${weekIndex}-${dayIndex}`);
|
|
882
|
+
}
|
|
883
|
+
return react_jsx_runtime.jsxs("span", {
|
|
884
|
+
className: S.day,
|
|
885
|
+
"data-level": String(cell.level),
|
|
886
|
+
"data-weekend": dayIndex >= 5 ? "true" : "false",
|
|
887
|
+
style: { background: palette[cell.level] },
|
|
888
|
+
title: translate("cellTokens", { date: dateLabel(cell.key), tokens: fmt(cell.tokens) }),
|
|
889
|
+
children: [
|
|
890
|
+
react_jsx_runtime.jsx("span", { className: S.dayNum, children: String(cell.day) }),
|
|
891
|
+
cell.tokens > 0 ? react_jsx_runtime.jsx("span", {
|
|
892
|
+
className: S.dayTokens,
|
|
893
|
+
children: fmtCompact(cell.tokens)
|
|
894
|
+
}) : null
|
|
895
|
+
]
|
|
896
|
+
}, cell.key);
|
|
897
|
+
})
|
|
898
|
+
}, `week-${weekIndex}`))
|
|
899
|
+
]
|
|
900
|
+
})
|
|
901
|
+
}) : react_jsx_runtime.jsx("div", {
|
|
656
902
|
className: S.gridWrap,
|
|
657
903
|
children: react_jsx_runtime.jsxs("div", {
|
|
658
904
|
className: S.grid,
|
|
@@ -691,28 +937,32 @@ window.__ModuleLoader__.load({
|
|
|
691
937
|
})
|
|
692
938
|
]
|
|
693
939
|
}, `row-${row}`);
|
|
694
|
-
}),
|
|
695
|
-
react_jsx_runtime.jsx("div", {
|
|
696
|
-
className: S.row,
|
|
697
|
-
children: [
|
|
698
|
-
react_jsx_runtime.jsx("span", { className: S.rowLabel, children: "" }),
|
|
699
|
-
react_jsx_runtime.jsx("span", {
|
|
700
|
-
className: S.legend,
|
|
701
|
-
children: [
|
|
702
|
-
dict("less"),
|
|
703
|
-
...palette.map((color, level) => react_jsx_runtime.jsx("span", {
|
|
704
|
-
className: S.legendCell,
|
|
705
|
-
style: { background: color },
|
|
706
|
-
title: levelLegend[level] ?? ""
|
|
707
|
-
}, `lvl-${level}`)),
|
|
708
|
-
dict("more")
|
|
709
|
-
]
|
|
710
|
-
})
|
|
711
|
-
]
|
|
712
940
|
})
|
|
713
941
|
]
|
|
714
942
|
})
|
|
715
|
-
})
|
|
943
|
+
}),
|
|
944
|
+
react_jsx_runtime.jsx("div", {
|
|
945
|
+
className: S.legend,
|
|
946
|
+
children: [
|
|
947
|
+
dict("less"),
|
|
948
|
+
...palette.map((color, level) => react_jsx_runtime.jsx("span", {
|
|
949
|
+
className: S.legendCell,
|
|
950
|
+
style: { background: color },
|
|
951
|
+
title: levelLegend[level] ?? ""
|
|
952
|
+
}, `lvl-${level}`)),
|
|
953
|
+
dict("more")
|
|
954
|
+
]
|
|
955
|
+
}),
|
|
956
|
+
settingsOpen ? react_jsx_runtime.jsx(TokenHeatmapInlineSettings, {
|
|
957
|
+
dict,
|
|
958
|
+
config,
|
|
959
|
+
store: configStore,
|
|
960
|
+
palette,
|
|
961
|
+
translate,
|
|
962
|
+
legend: levelLegend,
|
|
963
|
+
anchor: gearRef.current,
|
|
964
|
+
onClose: () => setSettingsOpen(false)
|
|
965
|
+
}) : null
|
|
716
966
|
]
|
|
717
967
|
});
|
|
718
968
|
}
|
|
@@ -725,242 +975,245 @@ window.__ModuleLoader__.load({
|
|
|
725
975
|
}
|
|
726
976
|
//#endregion
|
|
727
977
|
|
|
728
|
-
//#region
|
|
978
|
+
//#region TokenHeatmapInlineSettings
|
|
979
|
+
/** Distance between the ⚙ and the panel's bottom edge (native dialog spec). */
|
|
980
|
+
const PANEL_GAP = 8;
|
|
981
|
+
/** Viewport margin the placement clamp keeps (native dialog spec). */
|
|
982
|
+
const PANEL_MARGIN = 12;
|
|
983
|
+
|
|
729
984
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
985
|
+
* Place the floating panel above its ⚙ trigger, clamped to the viewport —
|
|
986
|
+
* the same measure-then-place flow (and the same 8px gap / 12px margin)
|
|
987
|
+
* the built-in stat dialogs use. The panel is `position:fixed` and lives
|
|
988
|
+
* in `document.body`, so the card can never clip it.
|
|
989
|
+
* @param trigger - the ⚙ button the panel is anchored to.
|
|
990
|
+
* @param panel - the `position:fixed` panel element.
|
|
991
|
+
*/
|
|
992
|
+
function placePanel(trigger, panel) {
|
|
993
|
+
const anchor = trigger.getBoundingClientRect();
|
|
994
|
+
const width = panel.offsetWidth;
|
|
995
|
+
const height = panel.offsetHeight;
|
|
996
|
+
if (width === 0 || height === 0) return;
|
|
997
|
+
const viewportWidth = window.innerWidth;
|
|
998
|
+
const viewportHeight = window.innerHeight;
|
|
999
|
+
const left = Math.min(
|
|
1000
|
+
Math.max(PANEL_MARGIN, anchor.left + anchor.width / 2 - width / 2),
|
|
1001
|
+
Math.max(PANEL_MARGIN, viewportWidth - width - PANEL_MARGIN)
|
|
1002
|
+
);
|
|
1003
|
+
let top = anchor.top - height - PANEL_GAP;
|
|
1004
|
+
if (top < PANEL_MARGIN) top = Math.min(anchor.bottom + PANEL_GAP, viewportHeight - height - PANEL_MARGIN);
|
|
1005
|
+
panel.style.left = `${Math.round(left)}px`;
|
|
1006
|
+
panel.style.top = `${Math.round(Math.max(PANEL_MARGIN, top))}px`;
|
|
1007
|
+
panel.style.visibility = "visible";
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* The card's settings panel: 配色方案 and 默认视图, edited from the card
|
|
1012
|
+
* itself instead of inside 设置 → 插件 → 插件配置 (this plugin no longer
|
|
1013
|
+
* registers into the `settings.plugin.item` seat).
|
|
1014
|
+
*
|
|
1015
|
+
* It is PORTALED to `document.body` (the card's own box can never clip
|
|
1016
|
+
* it) and anchored to the ⚙ through `anchor`; the owner closes it on
|
|
1017
|
+
* outside pointerdown and Escape, so this component only draws.
|
|
732
1018
|
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
* @param props -
|
|
1019
|
+
* Every control applies immediately through the bound settings scope —
|
|
1020
|
+
* there is no draft/save step, so the component owns only a local save
|
|
1021
|
+
* error. The 0.1.x 显示热力图 switch is gone (the card is always shown on
|
|
1022
|
+
* the hero screen), so its stored `enabled` key is neither read nor
|
|
1023
|
+
* written. The legacy loopback config endpoint remains as a back-compat
|
|
1024
|
+
* API, but nothing in the UI uses it.
|
|
1025
|
+
* @param props.dict - bound translator.
|
|
1026
|
+
* @param props.config - current config store snapshot.
|
|
1027
|
+
* @param props.store - config store (set writes through the scope).
|
|
1028
|
+
* @param props.palette - active palette (level 0..4 → css color).
|
|
1029
|
+
* @param props.translate - interpolation helper for translated labels.
|
|
1030
|
+
* @param props.legend - level labels for the palette footnote.
|
|
1031
|
+
* @param props.anchor - the ⚙ button the panel is placed against.
|
|
1032
|
+
* @param props.onClose - collapse the panel.
|
|
740
1033
|
*/
|
|
741
|
-
function
|
|
742
|
-
const
|
|
743
|
-
const [open, setOpen] = react.useState(false);
|
|
744
|
-
const [loaded, setLoaded] = react.useState(null);
|
|
745
|
-
const [draft, setDraft] = react.useState(null);
|
|
746
|
-
const [saving, setSaving] = react.useState(false);
|
|
1034
|
+
function TokenHeatmapInlineSettings({ dict, config, store, palette, translate, legend, anchor, onClose }) {
|
|
1035
|
+
const panelRef = react.useRef(null);
|
|
747
1036
|
const [saveFailed, setSaveFailed] = react.useState(false);
|
|
748
|
-
const
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
|
|
1037
|
+
const schemes = Object.keys(COLOR_SCHEMES);
|
|
1038
|
+
// Optimistic local echo: the settings scope only reports the new value
|
|
1039
|
+
// after the Host confirms, and a control must not lag behind the click.
|
|
1040
|
+
const [pending, setPending] = react.useState({});
|
|
1041
|
+
const scheme = pending.colorScheme ?? config.colorScheme;
|
|
1042
|
+
const defaultView = pending.defaultView ?? config.defaultView;
|
|
1043
|
+
// Place before paint (and again when the palette reflows the panel):
|
|
1044
|
+
// the trigger's box is only meaningful once the card is laid out.
|
|
1045
|
+
react.useLayoutEffect(() => {
|
|
1046
|
+
const panel = panelRef.current;
|
|
1047
|
+
if (panel === null || anchor === null || anchor === void 0) return;
|
|
1048
|
+
placePanel(anchor, panel);
|
|
1049
|
+
});
|
|
752
1050
|
react.useEffect(() => {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1051
|
+
const replace = () => {
|
|
1052
|
+
const panel = panelRef.current;
|
|
1053
|
+
if (panel === null || anchor === null || anchor === void 0) return;
|
|
1054
|
+
placePanel(anchor, panel);
|
|
1055
|
+
};
|
|
1056
|
+
window.addEventListener("resize", replace);
|
|
1057
|
+
window.addEventListener("scroll", replace, true);
|
|
1058
|
+
return () => {
|
|
1059
|
+
window.removeEventListener("resize", replace);
|
|
1060
|
+
window.removeEventListener("scroll", replace, true);
|
|
1061
|
+
};
|
|
1062
|
+
}, [anchor]);
|
|
1063
|
+
const commit = async (patch) => {
|
|
1064
|
+
setPending((current) => ({ ...current, ...patch }));
|
|
762
1065
|
setSaveFailed(false);
|
|
763
1066
|
try {
|
|
764
|
-
await
|
|
765
|
-
// The scope confirms the write and refreshes its snapshot
|
|
766
|
-
// synchronously before settling; sync the staged form.
|
|
767
|
-
const confirmed = configStore.getSnapshot();
|
|
768
|
-
if (confirmed.status === "ready") {
|
|
769
|
-
setLoaded({ enabled: confirmed.enabled, colorScheme: confirmed.colorScheme });
|
|
770
|
-
setDraft({ enabled: confirmed.enabled, colorScheme: confirmed.colorScheme });
|
|
771
|
-
}
|
|
1067
|
+
await store.set(patch);
|
|
772
1068
|
} catch {
|
|
773
1069
|
setSaveFailed(true);
|
|
774
|
-
} finally {
|
|
775
|
-
setSaving(false);
|
|
776
1070
|
}
|
|
777
|
-
}, [draft, saving]);
|
|
778
|
-
const discard = () => {
|
|
779
|
-
if (loaded !== null) setDraft(loaded);
|
|
780
|
-
setSaveFailed(false);
|
|
781
1071
|
};
|
|
782
|
-
const
|
|
783
|
-
if (
|
|
1072
|
+
const chooseScheme = (value) => {
|
|
1073
|
+
if (value !== scheme) commit({ colorScheme: value });
|
|
784
1074
|
};
|
|
785
|
-
const
|
|
786
|
-
if (
|
|
1075
|
+
const chooseView = (value) => {
|
|
1076
|
+
if (value !== defaultView) commit({ defaultView: value });
|
|
787
1077
|
};
|
|
1078
|
+
const labelOf = (name) => dict(`settings${name[0].toUpperCase()}${name.slice(1)}`);
|
|
788
1079
|
const title = dict("settingsTitle");
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
})
|
|
803
|
-
]
|
|
804
|
-
});
|
|
805
|
-
} else if (loaded === null || draft === null) {
|
|
806
|
-
bodyContent = react_jsx_runtime.jsx("p", {
|
|
807
|
-
className: S.settingsLoading,
|
|
808
|
-
children: dict("loading")
|
|
809
|
-
});
|
|
810
|
-
} else {
|
|
811
|
-
bodyContent = react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
|
|
812
|
-
children: [
|
|
813
|
-
react_jsx_runtime.jsx("div", {
|
|
814
|
-
className: S.settingsField,
|
|
815
|
-
children: react_jsx_runtime.jsxs("div", {
|
|
816
|
-
className: S.settingsSwitchRow,
|
|
1080
|
+
const panel = react_jsx_runtime.jsxs("div", {
|
|
1081
|
+
ref: panelRef,
|
|
1082
|
+
className: S.settingsPanel,
|
|
1083
|
+
role: "dialog",
|
|
1084
|
+
"aria-label": title,
|
|
1085
|
+
// Nothing may paint at the viewport origin before placement runs.
|
|
1086
|
+
style: { visibility: "hidden" },
|
|
1087
|
+
children: [
|
|
1088
|
+
react_jsx_runtime.jsxs("div", {
|
|
1089
|
+
className: S.panelHead,
|
|
1090
|
+
children: [
|
|
1091
|
+
react_jsx_runtime.jsxs("span", {
|
|
1092
|
+
className: S.panelTitleLabel,
|
|
817
1093
|
children: [
|
|
818
|
-
react_jsx_runtime.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
"
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1094
|
+
react_jsx_runtime.jsxs("svg", {
|
|
1095
|
+
width: 14,
|
|
1096
|
+
height: 14,
|
|
1097
|
+
viewBox: "0 0 24 24",
|
|
1098
|
+
fill: "none",
|
|
1099
|
+
stroke: "currentColor",
|
|
1100
|
+
"stroke-width": 2,
|
|
1101
|
+
"stroke-linecap": "round",
|
|
1102
|
+
"stroke-linejoin": "round",
|
|
1103
|
+
"aria-hidden": "true",
|
|
1104
|
+
children: [
|
|
1105
|
+
react_jsx_runtime.jsx("circle", { cx: 12, cy: 12, r: 9 }),
|
|
1106
|
+
react_jsx_runtime.jsx("path", { d: "M12 11v5" }),
|
|
1107
|
+
react_jsx_runtime.jsx("path", { d: "M12 8h.01" })
|
|
1108
|
+
]
|
|
827
1109
|
}),
|
|
828
|
-
react_jsx_runtime.jsx("span", {
|
|
1110
|
+
react_jsx_runtime.jsx("span", { children: title })
|
|
829
1111
|
]
|
|
830
1112
|
})
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
className: S.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
react_jsx_runtime.
|
|
841
|
-
className: S.settingsSwatches,
|
|
842
|
-
children: Object.keys(COLOR_SCHEMES).map((scheme) => react_jsx_runtime.jsx("button", {
|
|
843
|
-
type: "button",
|
|
844
|
-
disabled: saving,
|
|
845
|
-
className: S.settingsSwatch,
|
|
846
|
-
"data-active": draft.colorScheme === scheme,
|
|
847
|
-
onClick: () => chooseScheme(scheme),
|
|
848
|
-
children: [
|
|
849
|
-
react_jsx_runtime.jsx("span", {
|
|
850
|
-
className: S.settingsSwatchCells,
|
|
851
|
-
children: COLOR_SCHEMES[scheme].slice(1).map((color, index) => react_jsx_runtime.jsx("span", {
|
|
852
|
-
className: S.settingsSwatchCell,
|
|
853
|
-
style: { background: color }
|
|
854
|
-
}, `cell-${index}`))
|
|
855
|
-
}),
|
|
856
|
-
react_jsx_runtime.jsx("span", {
|
|
857
|
-
className: S.settingsSwatchLabel,
|
|
858
|
-
children: dict(`settings${scheme[0].toUpperCase()}${scheme.slice(1)}`)
|
|
859
|
-
})
|
|
860
|
-
]
|
|
861
|
-
}, scheme))
|
|
862
|
-
})
|
|
863
|
-
]
|
|
864
|
-
}),
|
|
865
|
-
react_jsx_runtime.jsx("p", {
|
|
866
|
-
className: S.settingsFieldHint,
|
|
867
|
-
children: dict("settingsSchemeHint")
|
|
868
|
-
}),
|
|
869
|
-
react_jsx_runtime.jsxs("div", {
|
|
870
|
-
className: S.settingsFooter,
|
|
871
|
-
children: [
|
|
872
|
-
saveFailed ? react_jsx_runtime.jsx("p", {
|
|
873
|
-
className: S.settingsFooterError,
|
|
874
|
-
children: dict("settingsSaveFailed")
|
|
875
|
-
}) : null,
|
|
876
|
-
react_jsx_runtime.jsx("button", {
|
|
1113
|
+
]
|
|
1114
|
+
}),
|
|
1115
|
+
react_jsx_runtime.jsx("div", { className: S.panelRule, "aria-hidden": "true" }),
|
|
1116
|
+
react_jsx_runtime.jsxs("div", {
|
|
1117
|
+
className: S.settingsGroup,
|
|
1118
|
+
children: [
|
|
1119
|
+
react_jsx_runtime.jsx("span", { className: S.settingsGroupLabel, children: dict("settingsScheme") }),
|
|
1120
|
+
react_jsx_runtime.jsx("div", {
|
|
1121
|
+
className: S.settingsSwatches,
|
|
1122
|
+
children: schemes.map((name) => react_jsx_runtime.jsxs("button", {
|
|
877
1123
|
type: "button",
|
|
878
|
-
className:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1124
|
+
className: S.settingsSwatch,
|
|
1125
|
+
"data-active": scheme === name,
|
|
1126
|
+
"aria-pressed": scheme === name,
|
|
1127
|
+
title: labelOf(name),
|
|
1128
|
+
onClick: () => chooseScheme(name),
|
|
1129
|
+
children: [
|
|
1130
|
+
react_jsx_runtime.jsx("span", {
|
|
1131
|
+
className: S.settingsSwatchCells,
|
|
1132
|
+
children: COLOR_SCHEMES[name].slice(1).map((color, index) => react_jsx_runtime.jsx("span", {
|
|
1133
|
+
className: S.settingsSwatchCell,
|
|
1134
|
+
style: { background: color }
|
|
1135
|
+
}, `cell-${index}`))
|
|
1136
|
+
}),
|
|
1137
|
+
react_jsx_runtime.jsx("span", {
|
|
1138
|
+
className: S.settingsSwatchLabel,
|
|
1139
|
+
children: labelOf(name)
|
|
1140
|
+
})
|
|
1141
|
+
]
|
|
1142
|
+
}, name))
|
|
1143
|
+
})
|
|
1144
|
+
]
|
|
1145
|
+
}),
|
|
1146
|
+
react_jsx_runtime.jsxs("div", {
|
|
1147
|
+
className: S.settingsGroup,
|
|
1148
|
+
children: [
|
|
1149
|
+
react_jsx_runtime.jsx("span", { className: S.settingsGroupLabel, children: dict("settingsDefaultView") }),
|
|
1150
|
+
react_jsx_runtime.jsx("div", {
|
|
1151
|
+
className: S.seg,
|
|
1152
|
+
role: "group",
|
|
1153
|
+
"aria-label": dict("settingsDefaultView"),
|
|
1154
|
+
children: VIEW_MODES.map((mode) => react_jsx_runtime.jsx("button", {
|
|
884
1155
|
type: "button",
|
|
885
|
-
className:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
return react_jsx_runtime.jsxs("li", {
|
|
897
|
-
className: open ? `${S.settingsCard} ${S.settingsCardOpen}` : S.settingsCard,
|
|
898
|
-
children: [
|
|
899
|
-
react_jsx_runtime.jsxs("button", {
|
|
900
|
-
type: "button",
|
|
901
|
-
className: S.settingsHeader,
|
|
902
|
-
"aria-expanded": open,
|
|
903
|
-
"aria-label": `${dict(open ? "settingsCollapse" : "settingsExpand")}: ${title}`,
|
|
904
|
-
onClick: () => setOpen(!open),
|
|
1156
|
+
className: S.segBtn,
|
|
1157
|
+
"data-active": defaultView === mode,
|
|
1158
|
+
"aria-pressed": defaultView === mode,
|
|
1159
|
+
onClick: () => chooseView(mode),
|
|
1160
|
+
children: dict(mode === "month" ? "viewMonth" : "viewYear")
|
|
1161
|
+
}, mode))
|
|
1162
|
+
})
|
|
1163
|
+
]
|
|
1164
|
+
}),
|
|
1165
|
+
react_jsx_runtime.jsxs("div", {
|
|
1166
|
+
className: S.panelFoot,
|
|
905
1167
|
children: [
|
|
906
|
-
react_jsx_runtime.jsxs("
|
|
907
|
-
className: S.
|
|
1168
|
+
react_jsx_runtime.jsxs("div", {
|
|
1169
|
+
className: S.footLegend,
|
|
908
1170
|
children: [
|
|
909
|
-
|
|
910
|
-
|
|
1171
|
+
dict("less"),
|
|
1172
|
+
...palette.map((color, level) => react_jsx_runtime.jsx("span", {
|
|
1173
|
+
className: S.legendCell,
|
|
1174
|
+
style: { background: color },
|
|
1175
|
+
title: legend[level] ?? ""
|
|
1176
|
+
}, `foot-${level}`)),
|
|
1177
|
+
dict("more")
|
|
911
1178
|
]
|
|
912
1179
|
}),
|
|
913
|
-
|
|
914
|
-
className: S.
|
|
915
|
-
children: dict("
|
|
916
|
-
}) :
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
height: 14,
|
|
920
|
-
className: open ? `${S.settingsChevron} ${S.settingsChevronOpen}` : S.settingsChevron,
|
|
921
|
-
viewBox: "0 0 14 14",
|
|
922
|
-
fill: "none",
|
|
923
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
924
|
-
children: react_jsx_runtime.jsx("path", {
|
|
925
|
-
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",
|
|
926
|
-
fill: "currentColor"
|
|
927
|
-
})
|
|
1180
|
+
saveFailed ? react_jsx_runtime.jsx("div", {
|
|
1181
|
+
className: S.footError,
|
|
1182
|
+
children: dict("settingsSaveFailed")
|
|
1183
|
+
}) : react_jsx_runtime.jsx("div", {
|
|
1184
|
+
className: S.footRule,
|
|
1185
|
+
children: dict("settingsThresholdNote")
|
|
928
1186
|
})
|
|
929
1187
|
]
|
|
930
|
-
})
|
|
931
|
-
open ? react_jsx_runtime.jsx("div", {
|
|
932
|
-
className: S.settingsBody,
|
|
933
|
-
children: bodyContent
|
|
934
|
-
}) : null
|
|
1188
|
+
})
|
|
935
1189
|
]
|
|
936
1190
|
});
|
|
1191
|
+
return reactDom.createPortal(panel, document.body);
|
|
937
1192
|
}
|
|
938
1193
|
//#endregion
|
|
939
1194
|
|
|
940
1195
|
//#region plugin body
|
|
941
1196
|
/**
|
|
942
|
-
* Settings namespace
|
|
943
|
-
*
|
|
944
|
-
*
|
|
1197
|
+
* Settings namespace the card reads/writes — must match the namespace
|
|
1198
|
+
* the Host registers (lib/index.js). Spelled here rather than imported:
|
|
1199
|
+
* a client package must not depend on a Host package.
|
|
945
1200
|
*/
|
|
946
1201
|
const SETTINGS_NS = "token-heatmap";
|
|
947
1202
|
/** Services required by the client plugin body. */
|
|
948
1203
|
const inject = ["slots", "locale", "connection", "remote", "settingsScope"];
|
|
949
1204
|
|
|
950
1205
|
/**
|
|
951
|
-
* Client plugin body: register the dictionaries
|
|
952
|
-
* (heatmap below the composer card on the new-session screen)
|
|
953
|
-
*
|
|
954
|
-
*
|
|
1206
|
+
* Client plugin body: register the dictionaries and the input-dock entry
|
|
1207
|
+
* (the heatmap below the composer card on the new-session screen), backed
|
|
1208
|
+
* by the `token-heatmap` settings namespace through a bound settings
|
|
1209
|
+
* scope. The card configures itself through its own ⚙ panel, so this
|
|
1210
|
+
* plugin does NOT register into `settings.plugin.item` (the official
|
|
1211
|
+
* 设置 → 插件 → 插件配置 card seat) any more — the Host still serves the
|
|
1212
|
+
* namespace, it just has no configuration card to dispatch.
|
|
955
1213
|
*
|
|
956
1214
|
* Slot-kind contract: `conversation.input.dock` is a LIST slot, so its
|
|
957
|
-
* registration is keyed by `id
|
|
958
|
-
*
|
|
959
|
-
* edits) — registering with `id` instead throws
|
|
960
|
-
* `keyed slot "settings.plugin.item" requires options.key` and fails
|
|
961
|
-
* the whole plugin load. The card is only dispatched by the official
|
|
962
|
-
* 插件配置 tab when the Host actually SERVES that namespace, which is
|
|
963
|
-
* why this plugin registers it on the Host side too.
|
|
1215
|
+
* registration is keyed by `id` (a `settings.plugin.item` registration
|
|
1216
|
+
* would instead need `key`, the settings namespace the card edits).
|
|
964
1217
|
* @param ctx - client root context.
|
|
965
1218
|
*/
|
|
966
1219
|
function apply(ctx) {
|
|
@@ -977,21 +1230,19 @@ window.__ModuleLoader__.load({
|
|
|
977
1230
|
order: 10,
|
|
978
1231
|
locale: NS
|
|
979
1232
|
}, TokenHeatmap));
|
|
980
|
-
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
981
|
-
name: "settings.plugin.item",
|
|
982
|
-
key: SETTINGS_NS,
|
|
983
|
-
order: 30,
|
|
984
|
-
locale: NS
|
|
985
|
-
}, TokenHeatmapSettingsCard));
|
|
986
1233
|
}
|
|
987
1234
|
//#endregion
|
|
988
1235
|
|
|
989
1236
|
exports.apply = apply;
|
|
990
1237
|
exports.inject = inject;
|
|
991
1238
|
exports.TokenHeatmap = TokenHeatmap;
|
|
992
|
-
exports.
|
|
1239
|
+
exports.TokenHeatmapInlineSettings = TokenHeatmapInlineSettings;
|
|
993
1240
|
exports.buildGrid = buildGrid;
|
|
1241
|
+
exports.buildMonthGrid = buildMonthGrid;
|
|
994
1242
|
exports.levelOf = levelOf;
|
|
1243
|
+
exports.monthLabelFull = monthLabelFull;
|
|
1244
|
+
exports.dateLabel = dateLabel;
|
|
1245
|
+
exports.shiftMonthKey = shiftMonthKey;
|
|
995
1246
|
exports.CELL_COLORS = CELL_COLORS;
|
|
996
1247
|
exports.COLOR_SCHEMES = COLOR_SCHEMES;
|
|
997
1248
|
exports.createConfigStore = createConfigStore;
|