@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/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 heatmap of a
6
- * SELECTABLE CALENDAR YEAR (Jan 1 – Dec 31; columns = weeks, rows = Mon..Sun,
7
- * all seven weekday labels on the left) plus today / this-month / all-time
8
- * totals and a ‹year› year selector next to the stats. It also registers a
9
- * settings card into the official `settings.plugin.item` seat
10
- * (设置 → 插件 → 插件配置) with a display switch and a green/blue palette
11
- * choice, persisted through the `token-heatmap` settings namespace (settings
12
- * scope).
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"` — and is positioned visually BELOW the
16
- * composer input card via a flex `order` on the dock wrapper (the dock's DOM
17
- * site sits above the bar, but the composer stack is a flex column and the
18
- * framework's true below-card seat `composer.dock` is disabled on the hero
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
- ".thm_dock{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width,780px);margin:0 auto;padding:0 16px 2px}",
42
- ".thm_card{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);border-radius:12px;padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px}",
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
- ".thm_yearNav{align-items:center;gap:2px;display:inline-flex}",
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
- ".thm_legend{align-items:center;gap:3px;margin-left:auto;font-size:10px;line-height:14px;display:inline-flex;color:var(--dsw-alias-label-tertiary)}",
64
- ".thm_legendCell{box-sizing:border-box;width:9px;height:9px;border-radius:2px}",
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
- ".thms_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}",
69
- ".thms_card:hover{border-color:var(--dsw-alias-label-dimmed)}",
70
- ".thms_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
71
- ".thms_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
72
- ".thms_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
73
- ".thms_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
74
- ".thms_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
75
- ".thms_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
76
- ".thms_chevronOpen{transform:rotate(180deg)}",
77
- ".thms_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
78
- ".thms_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}",
79
- ".thms_loading{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0}",
80
- ".thms_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5;margin:0}",
81
- ".thms_retryBtn{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;text-decoration:underline}",
82
- ".thms_field{display:flex;flex-direction:column;gap:4px}",
83
- ".thms_fieldLabel{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
84
- ".thms_fieldHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0}",
85
- ".thms_switchRow{display:flex;align-items:center;gap:10px}",
86
- ".thms_switch{position:relative;width:36px;height:20px;flex:none;border-radius:999px;border:0;padding:0;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover-solid);transition:background .15s}",
87
- ".thms_switch:disabled{opacity:.5;cursor:default}",
88
- ".thms_switchOn{background:var(--dsw-alias-state-business-primary)}",
89
- ".thms_thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}",
90
- ".thms_switchOn .thms_thumb{transform:translateX(16px)}",
91
- ".thms_swatches{display:flex;flex-wrap:wrap;gap:8px}",
92
- ".thms_swatch{appearance:none;font:inherit;cursor:pointer;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:8px;padding:6px 10px;display:flex}",
93
- ".thms_swatch:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
94
- ".thms_swatch:disabled{opacity:.5;cursor:default}",
95
- ".thms_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}",
96
- ".thms_swatchCells{display:flex;gap:2px}",
97
- ".thms_swatchCell{width:10px;height:10px;border-radius:2px}",
98
- ".thms_swatchLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}",
99
- ".thms_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}",
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
- yearNav: "thm_yearNav",
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
- settingsCard: "thms_card",
140
- settingsCardOpen: "thms_cardOpen",
141
- settingsHeader: "thms_header",
142
- settingsHeadText: "thms_headText",
143
- settingsName: "thms_name",
144
- settingsDesc: "thms_desc",
145
- settingsChevron: "thms_chevron",
146
- settingsChevronOpen: "thms_chevronOpen",
147
- settingsPending: "thms_pending",
148
- settingsBody: "thms_body",
149
- settingsLoading: "thms_loading",
150
- settingsError: "thms_error",
151
- settingsRetry: "thms_retryBtn",
152
- settingsField: "thms_field",
153
- settingsFieldLabel: "thms_fieldLabel",
154
- settingsFieldHint: "thms_fieldHint",
155
- settingsSwitchRow: "thms_switchRow",
156
- settingsSwitch: "thms_switch",
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
- settingsSave: "保存",
206
- settingsSaving: "保存中…",
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
- settingsSave: "Save",
246
- settingsSaving: "Saving…",
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
- /** Display date "8月13日" / "Aug 13". */
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 names = uiDict().monthName;
312
- return `${names[month - 1]} ${day}日`;
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 (enabled + colorScheme), persisted through the
336
- * Host `token-heatmap` settings namespace (settings.yaml) via the bound
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 (enabled / green), so the UI never
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 = { enabled: true, colorScheme: "green" };
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, enabled, colorScheme }` derived from the scope.
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
- enabled: v.enabled !== false,
371
- colorScheme: sanitizeScheme(v.colorScheme)
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"`); a flex `order` on the wrapper places
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
- // Year-selector bounds: the current year at most (no future usage),
557
- // and at least the earliest year present in the data so the selector
558
- // cannot wander into guaranteed-empty territory.
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 = monthKey(now);
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 card; unknown schemes fall back to green.
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.yearNav,
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: year <= minYear,
631
- onClick: () => setYear(year - 1),
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", { className: S.yearLabel, children: String(year) }),
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: year >= maxYear,
640
- onClick: () => setYear(year + 1),
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 TokenHeatmapSettingsCard
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
- * The plugin settings card, rendered inside the official
731
- * 设置 → 插件 → 插件配置 tab (the `settings.plugin.item` seat).
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
- * The card edits the `token-heatmap` settings namespace through the
734
- * bound settings scope (the tab only dispatches it while the Host
735
- * serves that namespace) and stages a draft until the user saves,
736
- * mirroring the official card chrome (disclosure header, unsaved
737
- * badge, discard/save footer). The legacy loopback config endpoint
738
- * remains as a back-compat API, but the card no longer uses it.
739
- * @param props - `t` bound by the slot runtime locale seat.
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 TokenHeatmapSettingsCard({ t }) {
742
- const dict = t !== void 0 ? t : (key) => zh[key] ?? key;
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 config = react.useSyncExternalStore(configStore.subscribe, configStore.getSnapshot);
749
- // Seed the staged form from the confirmed scope snapshot once (a
750
- // later scope change is the card's own save confirmation; external
751
- // edits land in loaded via the explicit sync in save()).
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
- if (config.status !== "ready" || loaded !== null) return;
754
- setLoaded({ enabled: config.enabled, colorScheme: config.colorScheme });
755
- setDraft({ enabled: config.enabled, colorScheme: config.colorScheme });
756
- }, [config, loaded]);
757
-
758
- const dirty = loaded !== null && draft !== null && (draft.enabled !== loaded.enabled || draft.colorScheme !== loaded.colorScheme);
759
- const save = react.useCallback(async () => {
760
- if (draft === null || saving) return;
761
- setSaving(true);
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 configStore.set({ enabled: draft.enabled, colorScheme: draft.colorScheme });
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 toggleEnabled = () => {
783
- if (draft !== null) setDraft({ ...draft, enabled: !draft.enabled });
1072
+ const chooseScheme = (value) => {
1073
+ if (value !== scheme) commit({ colorScheme: value });
784
1074
  };
785
- const chooseScheme = (scheme) => {
786
- if (draft !== null) setDraft({ ...draft, colorScheme: scheme });
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
- let bodyContent;
791
- if (config.status === "unavailable") {
792
- bodyContent = react_jsx_runtime.jsxs("p", {
793
- className: S.settingsError,
794
- children: [
795
- dict("settingsLoadFailed"),
796
- " ",
797
- react_jsx_runtime.jsx("button", {
798
- type: "button",
799
- className: S.settingsRetry,
800
- onClick: () => configStore.reload(),
801
- children: dict("settingsRetry")
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.jsx("button", {
819
- type: "button",
820
- role: "switch",
821
- "aria-checked": draft.enabled,
822
- "aria-label": dict("settingsEnabled"),
823
- disabled: saving,
824
- className: draft.enabled ? `${S.settingsSwitch} ${S.settingsSwitchOn}` : S.settingsSwitch,
825
- onClick: toggleEnabled,
826
- children: react_jsx_runtime.jsx("span", { className: S.settingsThumb })
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", { className: S.settingsFieldLabel, children: dict("settingsEnabled") })
1110
+ react_jsx_runtime.jsx("span", { children: title })
829
1111
  ]
830
1112
  })
831
- }),
832
- react_jsx_runtime.jsx("p", {
833
- className: S.settingsFieldHint,
834
- children: dict("settingsEnabledHint")
835
- }),
836
- react_jsx_runtime.jsxs("div", {
837
- className: S.settingsField,
838
- children: [
839
- react_jsx_runtime.jsx("span", { className: S.settingsFieldLabel, children: dict("settingsScheme") }),
840
- react_jsx_runtime.jsx("div", {
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: `${S.settingsBtn} ${S.settingsDiscard}`,
879
- disabled: !dirty || saving,
880
- onClick: discard,
881
- children: dict("settingsDiscard")
882
- }),
883
- react_jsx_runtime.jsx("button", {
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: `${S.settingsBtn} ${S.settingsSave}`,
886
- disabled: !dirty || saving,
887
- onClick: save,
888
- children: dict(saving ? "settingsSaving" : "settingsSave")
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("span", {
907
- className: S.settingsHeadText,
1168
+ react_jsx_runtime.jsxs("div", {
1169
+ className: S.footLegend,
908
1170
  children: [
909
- react_jsx_runtime.jsx("span", { className: S.settingsName, children: title }),
910
- react_jsx_runtime.jsx("span", { className: S.settingsDesc, children: dict("settingsDescription") })
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
- dirty ? react_jsx_runtime.jsx("span", {
914
- className: S.settingsPending,
915
- children: dict("settingsUnsaved")
916
- }) : null,
917
- react_jsx_runtime.jsx("svg", {
918
- width: 14,
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 this plugin's card edits — must match the
943
- * namespace the Host registers (lib/index.js). Spelled here rather
944
- * than imported: a client package must not depend on a Host package.
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, the input-dock entry
952
- * (heatmap below the composer card on the new-session screen) and the
953
- * plugin settings card (设置 → 插件 → 插件配置), all backed by the
954
- * `token-heatmap` settings namespace through a bound settings scope.
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`; `settings.plugin.item` is a KEYED slot,
958
- * so its registration must carry `key` (the settings namespace the card
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.TokenHeatmapSettingsCard = TokenHeatmapSettingsCard;
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;