@growth-operations/ui-shared 1.17.2 → 1.18.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -9,16 +9,27 @@ import { resolveCreditMeter } from "../lib/creditDisplay";
9
9
  // `credit_meter` block (CONTRACT.md). Renders nothing when there is no credit
10
10
  // data to show.
11
11
  //
12
- // All branch math (number precedence, threshold_state → tag/alert, the
13
- // pool+bank split) lives in ../lib/creditDisplay so it's unit-testable — this
14
- // component is pure rendering.
12
+ // All branch math (number precedence, threshold_state → tag/alert/bar
13
+ // variants, the pool+bank split) lives in ../lib/creditDisplay so it's
14
+ // unit-testable — this component is pure rendering.
15
15
  //
16
- // Two-segment bank display: accounts with a top-up bank (topup billing model)
17
- // get the combined marker bar — `${used} of ${granted}` over the combined
18
- // pool+bank denominator, so after a period reset with bank left it reads e.g.
19
- // "0 of 1,700" while the bank segment shows what's left — plus the
20
- // pool/bank breakdown lines. Accounts with NO bank (top_up_bank_remaining 0
21
- // AND never a top-up) render exactly the legacy single-segment bar.
16
+ // Stacked-bars bank display: an account with top-up bank credits left (topup
17
+ // billing model) gets TWO bars, one per bucket, and NO combined headline (the
18
+ // combined pool+bank denominator read as one big confusing allowance):
19
+ // 1. Monthly plan credits — a DEPLETION meter (value = pool used of the
20
+ // period grant), color-coded by the account's depletion state: success
21
+ // when healthy, warning at/below the low threshold, danger when
22
+ // depleted (view.barVariant).
23
+ // 2. Additional credits — a LEVEL gauge (value = bank remaining of the
24
+ // bank's granted total), always success: the bank is cumulative and
25
+ // never expires, so a used-of-granted bar would drift to
26
+ // permanently-near-full over multiple purchases; the remaining level
27
+ // refills on purchase and drains on spend.
28
+ // Hosts whose backend predates top_up_bank_granted degrade gracefully: the
29
+ // bank renders as a text-only line (no bar, never "of 0"). An account with
30
+ // NO bank credits left (never topped up, or the bank is spent) renders
31
+ // exactly the legacy view — the "{remaining} of {granted} credits left"
32
+ // headline plus the single color-coded bar.
22
33
  //
23
34
  // Props:
24
35
  // entitlement — the /v1/home entitlement union (uses the credits arm).
@@ -27,7 +38,7 @@ export function CreditMeter({ entitlement, creditMeter }) {
27
38
  const view = resolveCreditMeter(entitlement, creditMeter);
28
39
  if (!view) return null;
29
40
 
30
- const { granted, used, remaining, bank, tagVariant, tagLabel, alert } = view;
41
+ const { granted, used, remaining, bank, tagVariant, tagLabel, barVariant, alert } = view;
31
42
  const grantDaysLeft = view.grantExpiresAt ? daysUntil(view.grantExpiresAt) : null;
32
43
 
33
44
  return (
@@ -38,42 +49,64 @@ export function CreditMeter({ entitlement, creditMeter }) {
38
49
  <StatusTag variant={tagVariant}>{tagLabel}</StatusTag>
39
50
  </Flex>
40
51
 
41
- {/* PRIMARY signal — large and obvious. */}
42
- <Text format={{ fontWeight: "bold", fontSize: "lg" }}>
43
- {remaining} of {granted} credits left
44
- </Text>
45
-
46
52
  {bank ? (
47
53
  <>
48
- {/* Current-position marker over the COMBINED pool+bank
49
- denominator (used = granted - remaining). The HubSpot
50
- ProgressBar is single-value, so the two segments are the
51
- labeled breakdown below; this bar is the position within the
52
- whole. */}
53
- <ProgressBar
54
- title={`${used} of ${granted} used`}
55
- value={used}
56
- maxValue={granted > 0 ? granted : 1}
57
- showPercentage={true}
58
- />
54
+ {/* Pool — depletion meter over the period grant. */}
59
55
  <Flex direction="column" gap="extra-small">
60
56
  <Text>
61
57
  Monthly plan credits: {bank.poolRemaining} of {bank.poolGrant}{" "}
62
58
  left
63
59
  </Text>
64
- <Text>Additional credits: {bank.bankRemaining} left</Text>
65
- <Text format={{ fontStyle: "italic" }}>
66
- Additional credits don't expire.
67
- </Text>
60
+ <ProgressBar
61
+ title={`${bank.poolUsed} of ${bank.poolGrant} used`}
62
+ value={bank.poolUsed}
63
+ maxValue={bank.poolGrant > 0 ? bank.poolGrant : 1}
64
+ showPercentage={true}
65
+ variant={barVariant}
66
+ />
68
67
  </Flex>
68
+ {bank.bankGranted > 0 ? (
69
+ // Bank — level gauge over the granted total (refills on
70
+ // purchase, drains on spend; never expires).
71
+ <Flex direction="column" gap="extra-small">
72
+ <Text>
73
+ Additional credits (never expire): {bank.bankRemaining} of{" "}
74
+ {bank.bankGranted} left
75
+ </Text>
76
+ <ProgressBar
77
+ title={`${bank.bankRemaining} of ${bank.bankGranted} left`}
78
+ value={bank.bankRemaining}
79
+ maxValue={bank.bankGranted > 0 ? bank.bankGranted : 1}
80
+ showPercentage={true}
81
+ variant="success"
82
+ />
83
+ </Flex>
84
+ ) : (
85
+ // Graceful degradation: the host's backend predates
86
+ // top_up_bank_granted, so the bank total is unknown — text
87
+ // only, no bar (never divide by zero, never "of 0").
88
+ <Flex direction="column" gap="extra-small">
89
+ <Text>Additional credits: {bank.bankRemaining} left</Text>
90
+ <Text format={{ fontStyle: "italic" }}>
91
+ Additional credits don't expire.
92
+ </Text>
93
+ </Flex>
94
+ )}
69
95
  </>
70
96
  ) : (
71
- <ProgressBar
72
- title={`${used} used`}
73
- value={used}
74
- maxValue={granted > 0 ? granted : 1}
75
- showPercentage={true}
76
- />
97
+ <>
98
+ {/* PRIMARY signal — large and obvious. */}
99
+ <Text format={{ fontWeight: "bold", fontSize: "lg" }}>
100
+ {remaining} of {granted} credits left
101
+ </Text>
102
+ <ProgressBar
103
+ title={`${used} used`}
104
+ value={used}
105
+ maxValue={granted > 0 ? granted : 1}
106
+ showPercentage={true}
107
+ variant={barVariant}
108
+ />
109
+ </>
77
110
  )}
78
111
 
79
112
  {/* Free-grant countdown: "100 free credits — N days left". */}
@@ -26,9 +26,10 @@
26
26
  // incident where a paying, overage account was shown a blocking
27
27
  // "Depleted"/"out of credits" panel).
28
28
 
29
- // True when the account has (or ever had) a top-up bank: the two-segment
30
- // meter renders only then — a zero bank with NO top-up ever (last_top_up_at
31
- // null) renders exactly the legacy single-segment bar.
29
+ // True when the account has (or ever had) a top-up bank. Note the CreditMeter
30
+ // bank section renders only while the bank HAS credits left (bankRemaining >
31
+ // 0 — see resolveCreditMeter); this predicate stays the looser "ever had one"
32
+ // for consumers that render history/summary surfaces.
32
33
  export function hasTopUpBank(ent) {
33
34
  if (!ent) return false;
34
35
  return (ent.top_up_bank_remaining ?? 0) > 0 || ent.last_top_up_at != null;
@@ -38,12 +39,25 @@ export function hasTopUpBank(ent) {
38
39
  // Returns null when there is no credit data to show (component renders
39
40
  // nothing). Otherwise:
40
41
  // granted/used/remaining — combined (topup) or raw (legacy) display numbers
41
- // bank — { bankRemaining, poolGrant, poolRemaining, poolUsed }
42
- // when the account has a bank, else null
42
+ // bank — { bankRemaining, bankGranted, poolGrant,
43
+ // poolRemaining, poolUsed } when the account has bank
44
+ // credits LEFT (bankRemaining > 0), else null — a
45
+ // zero bank renders the plain single-pool bar (with
46
+ // bankRemaining 0 the combined numbers ARE the pool
47
+ // numbers). bankGranted is the bank's lifetime
48
+ // granted total (the bank bar's denominator); 0 on
49
+ // backends that predate top_up_bank_granted — the
50
+ // component degrades to a text-only bank line.
43
51
  // thresholdState — the arm's threshold_state, or null on a backend
44
52
  // that predates it
45
53
  // depleted/overIncluded/low — mutually-exclusive urgency flags
46
54
  // tagVariant/tagLabel — StatusTag rendering
55
+ // barVariant — ProgressBar sentiment for the pool/single bar:
56
+ // the tag's depletion mapping minus "info"
57
+ // (ProgressBar only takes success/warning/danger;
58
+ // top_up_pending is calm/positive, so success).
59
+ // The bank bar is always "success" (a never-expiring
60
+ // balance has no negative direction).
47
61
  // alert — { variant, title, message } or null
48
62
  // grantExpiresAt — pass-through for the free-grant countdown
49
63
  export function resolveCreditMeter(entitlement, creditMeter) {
@@ -82,13 +96,23 @@ export function resolveCreditMeter(entitlement, creditMeter) {
82
96
  // Bank split. Drawdown is monthly-first, so of the combined `remaining` the
83
97
  // bank portion is bankRemaining and the rest is pool remaining; the pool
84
98
  // segment's full size is the combined denominator minus the bank.
99
+ //
100
+ // The bank section renders ONLY while the bank has credits left
101
+ // (bankRemaining > 0): a zero bank — never topped up, or fully burned —
102
+ // renders the plain single-pool bar, and with bankRemaining 0 the combined
103
+ // numbers ARE the pool numbers (granted == poolGrant, remaining ==
104
+ // poolRemaining), so nothing is lost.
85
105
  let bank = null;
86
- if (hasTopUpBank(ent)) {
87
- const bankRemaining = Math.max(0, ent?.top_up_bank_remaining ?? 0);
106
+ const bankRemaining = Math.max(0, ent?.top_up_bank_remaining ?? 0);
107
+ if (bankRemaining > 0) {
88
108
  const poolGrant = Math.max(granted - bankRemaining, 0);
89
109
  const poolRemaining = Math.min(Math.max(remaining - bankRemaining, 0), poolGrant);
90
110
  bank = {
91
111
  bankRemaining,
112
+ // The bank's lifetime granted total — the bank bar's denominator. 0 on
113
+ // backends that predate top_up_bank_granted (the component degrades to
114
+ // a text-only bank line; never divide by zero, never "of 0").
115
+ bankGranted: Math.max(0, ent?.top_up_bank_granted ?? 0),
92
116
  poolGrant,
93
117
  poolRemaining,
94
118
  poolUsed: poolGrant - poolRemaining,
@@ -120,6 +144,13 @@ export function resolveCreditMeter(entitlement, creditMeter) {
120
144
  tagLabel = "Healthy";
121
145
  }
122
146
 
147
+ // ProgressBar sentiment for the pool bar (and the no-bank single bar):
148
+ // color-coded by depletion — success when healthy, warning at/below the
149
+ // low threshold (and for the other warning-level states), danger when
150
+ // depleted/top-up-failed. ProgressBar has no "info" variant, so the calm
151
+ // top_up_pending state renders success.
152
+ const barVariant = tagVariant === "info" ? "success" : tagVariant;
153
+
123
154
  // The inline meter alert mirrors the banner's messaging at the point of
124
155
  // glance. Legacy branches keep their pre-top-up copy byte-for-byte.
125
156
  let alert = null;
@@ -171,6 +202,7 @@ export function resolveCreditMeter(entitlement, creditMeter) {
171
202
  low,
172
203
  tagVariant,
173
204
  tagLabel,
205
+ barVariant,
174
206
  alert,
175
207
  grantExpiresAt: ent?.grant_expires_at ?? null,
176
208
  };