返回 DeepSeek-Reasonix
UsageStatsPanel.css
根目录 / desktop / frontend / src / components / UsageStatsPanel.css
1 /* ── Usage statistics panel (UsageStatsPanel.tsx) ─────────────────────────
2 Charts are hand-drawn SVG driven entirely by theme variables, so both stock
3 themes and theme packs adapt automatically. The heatmap's 5 levels come from
4 --accent via color-mix; model colours are the fixed --chart-1..5 series
5 (rank order by token volume) plus the gray --chart-other step, with
6 light/dark variants defined in styles.css. */
7
8 .usage-stats {
9 display: flex;
10 flex-direction: column;
11 gap: 18px;
12 min-width: 0;
13 }
14
15 .usage-stats__toolbar {
16 display: flex;
17 flex-wrap: wrap;
18 align-items: center;
19 gap: 10px;
20 }
21
22 /* Each picker (time range / source) lives in its own framed group, matching
23 the .settings-subtabs pattern used for the model subtab tabs, with a
24 slightly elevated background so the group reads against the panel. */
25 .usage-stats__group {
26 display: inline-flex;
27 flex-wrap: wrap;
28 align-items: center;
29 gap: 2px;
30 padding: 3px;
31 border: 1px solid var(--border);
32 border-radius: 8px;
33 background: var(--bg-soft);
34 }
35
36 .usage-stats__group .provider-add-segmented__item {
37 min-height: 28px;
38 padding: 0 12px;
39 border-radius: 6px;
40 }
41
42 .usage-stats__custom {
43 display: inline-flex;
44 align-items: center;
45 gap: 6px;
46 }
47
48 .usage-stats__custom .mem-input {
49 width: 150px;
50 flex: 0 0 auto;
51 }
52
53 .usage-stats__custom-sep {
54 color: var(--fg-faint);
55 }
56
57 .usage-stats__refresh {
58 margin-left: auto;
59 flex: 0 0 auto;
60 min-height: 30px;
61 padding: 4px 12px;
62 border: 1px solid var(--border);
63 border-radius: 8px;
64 background: var(--bg);
65 color: var(--fg);
66 font: inherit;
67 font-size: var(--font-control-small);
68 cursor: pointer;
69 }
70
71 .usage-stats__refresh:hover:not(:disabled) {
72 border-color: var(--accent);
73 }
74
75 .usage-stats__loading {
76 padding: 24px;
77 text-align: center;
78 color: var(--fg-faint);
79 font-size: var(--font-control);
80 }
81
82 .usage-stats__empty {
83 padding: 24px;
84 text-align: center;
85 color: var(--fg-faint);
86 font-size: var(--font-control);
87 border: 1px dashed var(--border);
88 border-radius: 9px;
89 }
90
91 /* numeric cards (sections 2–3): the two long-valued cards (tokens, top model)
92 stretch wider than the four short numeric ones, which stay equal-width. On
93 narrow windows the grid drops to two rows of three so cards keep room. */
94 .usage-stats__cards {
95 display: grid;
96 grid-template-columns: 1.3fr repeat(4, 1fr) 1.3fr;
97 gap: 8px;
98 }
99
100 @media (max-width: 900px) {
101 .usage-stats__cards {
102 grid-template-columns: 1.3fr 1fr 1.3fr;
103 }
104 }
105
106 .usage-stats__card {
107 display: flex;
108 flex-direction: column;
109 gap: 6px;
110 padding: 10px 12px;
111 border: 1px solid var(--border);
112 border-radius: 9px;
113 background: var(--bg-elev);
114 min-width: 0;
115 }
116
117 .usage-stats__card-head {
118 display: inline-flex;
119 align-items: center;
120 gap: 6px;
121 min-width: 0;
122 }
123
124 .usage-stats__card-icon {
125 color: var(--accent);
126 flex: 0 0 auto;
127 }
128
129 .usage-stats__card-label {
130 font-size: var(--font-caption);
131 color: var(--fg-faint);
132 overflow: hidden;
133 text-overflow: ellipsis;
134 white-space: nowrap;
135 }
136
137 .usage-stats__card-value {
138 font-size: 22px;
139 line-height: 1.1;
140 font-weight: 600;
141 color: var(--fg);
142 white-space: nowrap; /* FitText keeps long values on one line */
143 overflow: hidden;
144 text-overflow: clip;
145 }
146
147 /* The "most used model" card holds a long model name, so it uses a smaller
148 value font and is allowed to wrap to a second line on narrow windows. */
149 .usage-stats__card-value--sm {
150 font-size: 14px;
151 line-height: 1.35;
152 padding-top: 3px;
153 font-variant-numeric: tabular-nums;
154 }
155
156 .usage-stats__card-value--wrap {
157 white-space: normal;
158 overflow-wrap: anywhere;
159 word-break: break-word;
160 overflow: visible;
161 }
162
163 /* sections + charts */
164 .usage-stats__section {
165 display: flex;
166 flex-direction: column;
167 gap: 10px;
168 min-width: 0;
169 }
170
171 .usage-stats__section-title {
172 margin: 0;
173 font-size: var(--font-control);
174 font-weight: 600;
175 color: var(--fg);
176 }
177
178 /* Title row: heatmap keeps its level legend on the right of the heading. */
179 .usage-stats__section-head {
180 display: flex;
181 align-items: center;
182 justify-content: space-between;
183 gap: 12px;
184 flex-wrap: wrap;
185 }
186
187 .usage-stats__section-note {
188 color: var(--muted);
189 font-size: var(--font-caption);
190 }
191
192 .usage-stats__heatmap-wrap,
193 .usage-stats__chart-wrap {
194 display: flex;
195 flex-direction: column;
196 gap: 8px;
197 min-width: 0;
198 padding-bottom: 4px;
199 position: relative; /* anchors the absolutely-positioned chart tooltip */
200 }
201
202 .usage-stats__heatmap {
203 display: block;
204 }
205
206 /* 5 heat levels: progressively stronger accent on the elevated background.
207 Levels apply to both SVG rects and the legend swatches. */
208 .usage-stats__heat-cell {
209 fill: color-mix(in srgb, var(--accent) 8%, var(--bg-elev));
210 }
211 .usage-stats__heat-cell--1 {
212 fill: color-mix(in srgb, var(--accent) 16%, var(--bg-elev));
213 }
214 .usage-stats__heat-cell--2 {
215 fill: color-mix(in srgb, var(--accent) 32%, var(--bg-elev));
216 }
217 .usage-stats__heat-cell--3 {
218 fill: color-mix(in srgb, var(--accent) 52%, var(--bg-elev));
219 }
220 .usage-stats__heat-cell--4 {
221 fill: color-mix(in srgb, var(--accent) 72%, var(--bg-elev));
222 }
223 .usage-stats__heat-cell--5 {
224 fill: var(--accent);
225 }
226
227 /* Legend swatches are HTML elements, where SVG `fill` has no effect — give
228 them the same 5-level accent backgrounds the svg cells use. Their size
229 follows the live cell size (inline style from the component). */
230 .usage-stats__heatmap-legend i.usage-stats__heat-cell--1 { background: color-mix(in srgb, var(--accent) 16%, var(--bg-elev)); }
231 .usage-stats__heatmap-legend i.usage-stats__heat-cell--2 { background: color-mix(in srgb, var(--accent) 32%, var(--bg-elev)); }
232 .usage-stats__heatmap-legend i.usage-stats__heat-cell--3 { background: color-mix(in srgb, var(--accent) 52%, var(--bg-elev)); }
233 .usage-stats__heatmap-legend i.usage-stats__heat-cell--4 { background: color-mix(in srgb, var(--accent) 72%, var(--bg-elev)); }
234 .usage-stats__heatmap-legend i.usage-stats__heat-cell--5 { background: var(--accent); }
235
236 .usage-stats__heatmap-legend {
237 display: inline-flex;
238 align-items: center;
239 gap: 5px;
240 font-size: var(--font-caption);
241 color: var(--fg-faint);
242 }
243
244 .usage-stats__heatmap-legend i {
245 display: inline-block;
246 width: 12px;
247 height: 12px;
248 border-radius: 3px;
249 }
250
251 .usage-stats__chart {
252 display: block;
253 width: 100%;
254 }
255
256 .usage-stats__grid {
257 stroke: var(--border);
258 stroke-width: 1;
259 }
260
261 .usage-stats__axis {
262 fill: var(--fg-faint);
263 font-size: 10px;
264 }
265
266 /* Invisible whole-column hover target — hovering anywhere on a bar shows the
267 day's full per-model breakdown, not just the segment under the cursor. */
268 .usage-stats__bar-hit {
269 fill: transparent;
270 cursor: default;
271 }
272
273 .usage-stats__bar--dim,
274 .usage-stats__donut-seg--dim {
275 opacity: 0.35;
276 }
277
278 .usage-stats__legend {
279 display: flex;
280 flex-wrap: wrap;
281 justify-content: center;
282 gap: 10px 16px;
283 font-size: var(--font-caption);
284 color: var(--fg-dim);
285 }
286
287 .usage-stats__legend-item {
288 display: inline-flex;
289 align-items: center;
290 gap: 5px;
291 cursor: default;
292 }
293
294 .usage-stats__legend-swatch {
295 display: inline-block;
296 width: 4px;
297 height: 14px;
298 border-radius: 3px;
299 flex: 0 0 auto;
300 }
301
302 /* cache hit-rate trend legend swatch: a small line, distinct from the model
303 colour blocks above (the line itself uses var(--fg) so it reads on both
304 themes; the dot's fill comes from var(--accent)). */
305 .usage-stats__legend-swatch--trend {
306 width: 14px;
307 height: 3px;
308 border-radius: 2px;
309 background: color-mix(in srgb, var(--accent) 80%, var(--fg));
310 }
311
312 /* per-model donut + list */
313 .usage-stats__models {
314 display: flex;
315 flex-wrap: wrap;
316 justify-content: center;
317 gap: 44px;
318 align-items: flex-start;
319 min-width: 0;
320 }
321
322 .usage-stats__donut-wrap {
323 position: relative;
324 flex: 0 0 auto;
325 }
326
327 .usage-stats__donut {
328 display: block;
329 }
330
331 .usage-stats__donut-track {
332 stroke: var(--bg-elev-2, var(--bg-elev));
333 }
334
335 .usage-stats__donut-seg {
336 transition: opacity 0.12s ease;
337 }
338
339 .usage-stats__donut-center {
340 fill: var(--fg);
341 font-size: 26px;
342 font-weight: 600;
343 }
344
345 .usage-stats__donut-label {
346 fill: var(--fg-faint);
347 font-size: 12px;
348 }
349
350 .usage-stats__model-list {
351 list-style: none;
352 margin: 0;
353 padding: 0;
354 flex: 1 1 280px;
355 min-width: 240px;
356 display: flex;
357 flex-direction: column;
358 gap: 0;
359 }
360
361 /* thin divider between rows so the detail list reads as separate entries */
362 .usage-stats__model-row + .usage-stats__model-row {
363 border-top: 1px solid var(--border-soft);
364 }
365
366 .usage-stats__model-row {
367 display: grid;
368 grid-template-columns: 8px minmax(0, 1fr) auto auto auto;
369 align-items: center;
370 gap: 8px;
371 padding: 8px 10px;
372 border-radius: 7px;
373 font-size: var(--font-control);
374 }
375
376 .usage-stats__model-row:hover {
377 background: var(--bg-elev);
378 }
379
380 /* "Other" aggregate row: the entire row is one native button that toggles the
381 detail list. */
382 .usage-stats__model-row--expandable {
383 width: 100%;
384 border: none;
385 background: transparent;
386 color: inherit;
387 font: inherit;
388 text-align: inherit;
389 cursor: pointer;
390 }
391
392 .usage-stats__model-item {
393 list-style: none;
394 }
395
396 .usage-stats__model-row + .usage-stats__model-item {
397 border-top: 1px solid var(--border-soft);
398 }
399
400 /* Decorative chevron inside the "Other" row button. */
401 .usage-stats__model-toggle {
402 display: inline-flex;
403 align-items: center;
404 justify-content: center;
405 padding: 0;
406 margin-right: 4px;
407 border: none;
408 background: none;
409 color: var(--fg-faint);
410 vertical-align: middle;
411 }
412
413 /* Expanded "Other" detail rows: indented, slightly recessed, caption size. */
414 .usage-stats__model-row--sub {
415 padding-left: 28px;
416 background: color-mix(in srgb, var(--bg-elev) 55%, transparent);
417 font-size: var(--font-caption);
418 }
419
420 /* "Other" details expand with an accordion: the wrapper is a grid whose
421 single track animates 0fr → 1fr, so the inner list slides open smoothly
422 instead of popping in. */
423 .usage-stats__model-other-wrap {
424 display: grid;
425 grid-template-rows: 0fr;
426 transition: grid-template-rows 0.25s ease;
427 }
428
429 .usage-stats__model-other-wrap--open {
430 grid-template-rows: 1fr;
431 }
432
433 .usage-stats__model-other-list {
434 overflow: hidden;
435 min-height: 0;
436 list-style: none;
437 margin: 0;
438 padding: 0;
439 }
440
441 .usage-stats__model-name {
442 color: var(--fg);
443 overflow: hidden;
444 text-overflow: ellipsis;
445 white-space: nowrap;
446 }
447
448 .usage-stats__model-provider {
449 color: var(--fg-faint);
450 font-size: var(--font-caption);
451 }
452
453 .usage-stats__model-tokens {
454 color: var(--fg);
455 font-variant-numeric: tabular-nums;
456 white-space: nowrap;
457 }
458
459 .usage-stats__model-pct {
460 color: var(--fg-faint);
461 font-variant-numeric: tabular-nums;
462 min-width: 48px;
463 text-align: right;
464 white-space: nowrap;
465 }
466
467 /* hover tooltip — anchored inside its chart container (position: relative on
468 the wrap) and positioned via transform so it can slide between columns. */
469 .usage-stats__tip {
470 position: absolute;
471 z-index: var(--z-popover);
472 left: 0;
473 top: 0;
474 padding: 8px 10px;
475 border: 1px solid var(--border);
476 border-radius: 8px;
477 background: var(--bg-elev-2, var(--bg));
478 color: var(--fg);
479 font-size: var(--font-caption);
480 box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
481 pointer-events: none;
482 white-space: nowrap;
483 display: flex;
484 flex-direction: column;
485 gap: 3px;
486 }
487
488 .usage-stats__tip--chart {
489 transition: transform 0.15s ease, opacity 0.12s ease;
490 }
491
492 /* Viewport-fixed tooltip (daily trend): coordinates are screen-space, clamped
493 to the settings content pane by the component. */
494 .usage-stats__tip--screen {
495 position: fixed;
496 }
497
498 .usage-stats__tip-title {
499 font-weight: 600;
500 color: var(--fg);
501 }
502
503 .usage-stats__tip-row {
504 display: inline-flex;
505 align-items: center;
506 gap: 5px;
507 }
508
509 /* Hover tooltip breakdown rows for the "Other" aggregate (trend + donut):
510 indented, muted, same gray swatch as the Other step. */
511 .usage-stats__tip-row--other {
512 padding-left: 8px;
513 color: var(--fg-faint);
514 }
515
516 .usage-stats__tip-breakdown {
517 border-top: 1px solid var(--border-soft);
518 margin-top: 4px;
519 padding-top: 4px;
520 }
521
522 .usage-stats__tip-more {
523 padding-left: 8px;
524 color: var(--fg-faint);
525 font-style: italic;
526 }
527
528 .usage-stats__foot {
529 font-size: var(--font-caption);
530 color: var(--fg-faint);
531 }
532
533 /* cache hit-rate trend curve: theme accent shifted slightly toward the
534 foreground so it stays distinct from the pure-accent top model bars; drawn
535 above the token bars. No point markers — the line alone reads the trend. */
536 .usage-stats__trend {
537 stroke: color-mix(in srgb, var(--accent) 80%, var(--fg));
538 stroke-width: 2;
539 opacity: 0.9;
540 pointer-events: none;
541 }
542
543 /* Right-hand axis labels for the hit-rate scale. */
544 .usage-stats__axis--rate {
545 font-size: 9px;
546 fill: var(--fg-faint);
547 }
548
549 /* Hovered column bars widen horizontally via scaleX around the bar's own
550 centre (transform-box: fill-box), so both edges grow symmetrically; height
551 never changes and the token value stays readable. */
552 .usage-stats__bar {
553 transform-box: fill-box;
554 transform-origin: center;
555 transition: transform 0.12s ease, opacity 0.12s ease;
556 }
557
558 /* Marker that lights up on the hit-rate curve for the hovered day. */
559 .usage-stats__trend-dot {
560 fill: color-mix(in srgb, var(--accent) 80%, var(--fg));
561 stroke: var(--bg-elev);
562 stroke-width: 2;
563 pointer-events: none;
564 }
565
565 lines CSS