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