Skip to content

Commit 9d3a090

Browse files
feat(billing): sidenav compute gauge redesign — button-shape above sign-out row (#4140)
Replace BillingComputeGaugeComponent (decorative card, hover-expand) with BillingNavComputeGaugeComponent: v-list-item button-shape matching other nav items. Gauge bar replaces FA icon in prepend slot. Collapsed (rail): bar only, no text. Expanded: % progress + reset date (e.g. "62% / resets Sat May 17"). Click → navigates to /users?tab=subscriptions (locked, no tooltip/popover). Gate: hidden when not logged in, meterMode false, or usageMeter null. 17 unit tests for the new component + 4 new navigation tests.
1 parent bf6b9c8 commit 9d3a090

4 files changed

Lines changed: 456 additions & 4 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<!--
2+
BillingNavComputeGaugeComponent
3+
================================
4+
Compute usage mini-gauge for the navigation sidenav.
5+
Renders as a v-list-item (button-shape, same as other nav items) ABOVE the
6+
user/sign-out row. Only visible when meterMode is active.
7+
8+
Collapsed (rail) state : gauge bar only — no text label.
9+
Expanded state : gauge bar (prepend) + "62% · resets Sat May 17" text.
10+
11+
Click → navigates to /users?tab=subscriptions (locked per T5 spec).
12+
NO inline tooltip / popover.
13+
14+
Gate: hidden when not logged in, meterMode false, or usageMeter null.
15+
16+
PROPS:
17+
- rail (Boolean, default false): mirrors navigation drawer rail prop.
18+
When true, only the gauge bar is shown — title/subtitle hidden.
19+
20+
USAGE:
21+
<BillingNavComputeGaugeComponent :rail="isRail" />
22+
-->
23+
<template>
24+
<v-list-item
25+
v-if="show"
26+
class="billing-nav-gauge px-2"
27+
:to="'/users?tab=subscriptions'"
28+
:aria-label="`Compute usage: ${pct}%`"
29+
>
30+
<template #prepend>
31+
<div class="billing-nav-gauge__bar-wrap">
32+
<v-progress-linear
33+
:model-value="pct"
34+
:color="thresholdColor"
35+
rounded
36+
height="4"
37+
bg-color="surface-variant"
38+
/>
39+
</div>
40+
</template>
41+
<template v-if="!rail">
42+
<v-list-item-title class="text-body-small font-weight-medium">
43+
{{ pct }}%
44+
</v-list-item-title>
45+
<v-list-item-subtitle v-if="resetLabel" class="text-caption text-medium-emphasis">
46+
{{ resetLabel }}
47+
</v-list-item-subtitle>
48+
</template>
49+
</v-list-item>
50+
</template>
51+
52+
<script>
53+
/**
54+
* Module dependencies.
55+
*/
56+
import { useBillingStore } from '../stores/billing.store.js';
57+
import { useAuthStore } from '../../auth/stores/auth.store.js';
58+
59+
/**
60+
* Component definition.
61+
*/
62+
export default {
63+
name: 'BillingNavComputeGaugeComponent',
64+
65+
props: {
66+
/**
67+
* @desc Mirror of the navigation drawer's rail prop.
68+
* When true, only the gauge bar is shown — no text labels.
69+
*/
70+
rail: {
71+
type: Boolean,
72+
default: false,
73+
},
74+
},
75+
76+
setup() {
77+
const billingStore = useBillingStore();
78+
const authStore = useAuthStore();
79+
return { billingStore, authStore };
80+
},
81+
82+
computed: {
83+
/**
84+
* @desc Render only when logged in, meterMode is active, and usage data is available.
85+
* @returns {boolean}
86+
*/
87+
show() {
88+
if (!this.authStore.isLoggedIn) return false;
89+
if (!this.authStore.serverConfig?.billing?.meterMode) return false;
90+
return Boolean(this.billingStore.usageMeter);
91+
},
92+
93+
/**
94+
* @desc Proxy to usageMeter for clean template access.
95+
* @returns {Object|null}
96+
*/
97+
usageMeter() {
98+
return this.billingStore.usageMeter;
99+
},
100+
101+
/**
102+
* @desc Percentage of weekly quota consumed, clamped [0, 100].
103+
* @returns {number}
104+
*/
105+
pct() {
106+
const { meterUsed = 0, meterQuota = 0 } = this.usageMeter ?? {};
107+
if (meterQuota === 0) return 0;
108+
return Math.max(0, Math.min(100, Math.round((meterUsed / meterQuota) * 100)));
109+
},
110+
111+
/**
112+
* @desc Vuetify color token based on usage threshold.
113+
* @returns {string}
114+
*/
115+
thresholdColor() {
116+
if (this.pct >= 100) return 'error';
117+
if (this.pct >= 80) return 'warning';
118+
return 'success';
119+
},
120+
121+
/**
122+
* @desc Human-readable reset date label (short weekday + month + day format).
123+
* Example: "resets Sat May 17"
124+
* @returns {string|null}
125+
*/
126+
resetLabel() {
127+
const resetAt = this.usageMeter?.weekResetAt;
128+
if (!resetAt) return null;
129+
try {
130+
const d = new Date(resetAt);
131+
const formatted = d.toLocaleDateString(undefined, {
132+
weekday: 'short',
133+
month: 'short',
134+
day: 'numeric',
135+
});
136+
return `resets ${formatted}`;
137+
} catch {
138+
return null;
139+
}
140+
},
141+
},
142+
};
143+
</script>
144+
145+
<style scoped>
146+
.billing-nav-gauge__bar-wrap {
147+
width: 24px;
148+
display: flex;
149+
align-items: center;
150+
}
151+
152+
.billing-nav-gauge__bar-wrap :deep(.v-progress-linear) {
153+
width: 24px;
154+
min-width: 24px;
155+
}
156+
</style>

0 commit comments

Comments
 (0)