@burdenoff/microfe-subscriberbot 2026.1030.2 → 2026.1030.4

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.
Files changed (33) hide show
  1. package/dist/subscriberbot/SubscriberBotRoutes.d.ts.map +1 -1
  2. package/dist/subscriberbot/SubscriberBotRoutes.js +12 -8
  3. package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
  4. package/dist/subscriberbot/components/savings/SavingsCards.d.ts +11 -0
  5. package/dist/subscriberbot/components/savings/SavingsCards.d.ts.map +1 -0
  6. package/dist/subscriberbot/components/savings/SavingsCards.js +448 -0
  7. package/dist/subscriberbot/components/savings/SavingsCards.js.map +1 -0
  8. package/dist/subscriberbot/components/savings/index.d.ts +8 -0
  9. package/dist/subscriberbot/components/savings/index.d.ts.map +1 -0
  10. package/dist/subscriberbot/components/savings/index.js +2 -0
  11. package/dist/subscriberbot/components/savings/savingsModel.d.ts +53 -0
  12. package/dist/subscriberbot/components/savings/savingsModel.d.ts.map +1 -0
  13. package/dist/subscriberbot/components/savings/savingsModel.js +105 -0
  14. package/dist/subscriberbot/components/savings/savingsModel.js.map +1 -0
  15. package/dist/subscriberbot/hooks/index.d.ts +1 -1
  16. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  17. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +11 -2
  18. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  19. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +48 -26
  20. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  21. package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
  22. package/dist/subscriberbot/pages/InsightsPage.js +121 -110
  23. package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
  24. package/dist/subscriberbot/pages/SavingsPage.d.ts +4 -0
  25. package/dist/subscriberbot/pages/SavingsPage.d.ts.map +1 -0
  26. package/dist/subscriberbot/pages/SavingsPage.js +205 -0
  27. package/dist/subscriberbot/pages/SavingsPage.js.map +1 -0
  28. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  29. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +318 -312
  30. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  31. package/dist/subscriberbot/types/index.d.ts +61 -1
  32. package/dist/subscriberbot/types/index.d.ts.map +1 -1
  33. package/package.json +1 -1
@@ -0,0 +1,205 @@
1
+ import { useSubscriberBot as e } from "../context/SubscriberBotContext.js";
2
+ import { PageLayout as t } from "../components/PageLayout.js";
3
+ import { formatCurrency as n } from "../utils/formatters.js";
4
+ import { EmptyState as r } from "../components/EmptyState.js";
5
+ import { useTr as i } from "../hooks/useTr.js";
6
+ import { LoadingView as a, Section as o } from "../components/StateViews.js";
7
+ import "../components/index.js";
8
+ import { useDecisions as s, useSavingsOverview as c } from "../hooks/useSubscriberBotApi.js";
9
+ import { AiDisclaimer as l, DecisionsUnavailableBanner as u } from "../components/decisions/DecisionBanners.js";
10
+ import "../components/decisions/index.js";
11
+ import { SAVINGS_SECTIONS as d, groupDecisions as f, headlineAmounts as p, joinAmounts as m, perMonth as h, savingsByCurrency as g } from "../components/savings/savingsModel.js";
12
+ import { BundleCard as _, SavingsCard as v } from "../components/savings/SavingsCards.js";
13
+ import "../components/savings/index.js";
14
+ import { useMemo as y, useState as b } from "react";
15
+ import { Bot as x, Combine as S, PiggyBank as C, RefreshCw as w } from "lucide-react";
16
+ import { Fragment as T, jsx as E, jsxs as D } from "react/jsx-runtime";
17
+ import { GlassCard as O } from "@burdenoff/fe-libs/ui";
18
+ //#region src/subscriberbot/pages/SavingsPage.tsx
19
+ var k = { status: "PROPOSED" }, A = () => {
20
+ let A = i(), { navigateTo: j } = e(), M = c(), N = s(k), [P, F] = b(null), I = y(() => f(N.data?.items ?? []), [N.data]), L = M.data?.bundles ?? [], R = M.data?.totals ?? [], z = m(p(R), n), B = m(R.map((e) => ({
21
+ currency: e.currency,
22
+ amount: h(e.annualSavings)
23
+ })), n), V = m(R.map((e) => ({
24
+ currency: e.currency,
25
+ amount: e.fromDecisions
26
+ })), n), H = m(R.map((e) => ({
27
+ currency: e.currency,
28
+ amount: e.fromBundles
29
+ })), n), U = Object.values(I).reduce((e, t) => e + t.length, 0), W = M.isLoading || N.isLoading, G = M.isError || N.isError, K = M.error ?? N.error, q = (e) => {
30
+ let t = n(e.estimatedSavings, e.currency);
31
+ F(e.failed.length > 0 ? A("savings.bundle.partial", "Kept \"{{keep}}\" and cancelled {{count}} subscription(s); {{failed}} could not be cancelled and are unchanged.", {
32
+ keep: e.kept.label,
33
+ count: e.cancelled.length,
34
+ failed: e.failed.length
35
+ }) : A("savings.bundle.done", "Kept \"{{keep}}\" and cancelled {{count}} subscription(s). Estimated saving: {{amount}} a year.", {
36
+ keep: e.kept.label,
37
+ count: e.cancelled.length,
38
+ amount: t
39
+ }));
40
+ }, J = () => {
41
+ M.refetch(), N.refetch();
42
+ };
43
+ return /* @__PURE__ */ D(t, {
44
+ title: A("savings.title", "Optimize your spend"),
45
+ description: A("savings.description", "What you could save this year and the specific changes that get you there. Every change asks you to confirm first."),
46
+ actions: /* @__PURE__ */ D("div", {
47
+ className: "flex flex-wrap gap-2",
48
+ children: [/* @__PURE__ */ D("button", {
49
+ type: "button",
50
+ onClick: J,
51
+ className: "inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken",
52
+ children: [
53
+ /* @__PURE__ */ E(w, {
54
+ className: "h-4 w-4",
55
+ "aria-hidden": !0
56
+ }),
57
+ " ",
58
+ A("savings.refresh", "Refresh")
59
+ ]
60
+ }), /* @__PURE__ */ D("button", {
61
+ type: "button",
62
+ onClick: () => j("/agents"),
63
+ className: "inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken",
64
+ children: [
65
+ /* @__PURE__ */ E(x, {
66
+ className: "h-4 w-4",
67
+ "aria-hidden": !0
68
+ }),
69
+ " ",
70
+ A("savings.runAgents", "Run agents")
71
+ ]
72
+ })]
73
+ }),
74
+ children: [/* @__PURE__ */ E(l, {}), W ? /* @__PURE__ */ E(a, { label: A("savings.loading", "Finding savings…") }) : G ? /* @__PURE__ */ E("div", {
75
+ className: "mt-6",
76
+ children: /* @__PURE__ */ E(u, {
77
+ message: K instanceof Error ? K.message : void 0,
78
+ onRetry: J
79
+ })
80
+ }) : /* @__PURE__ */ D(T, { children: [
81
+ /* @__PURE__ */ D(O, {
82
+ treatment: "glass",
83
+ glow: !0,
84
+ className: "mt-6 flex flex-col gap-4 p-6 sm:flex-row sm:items-end sm:justify-between",
85
+ "data-testid": "savings-headline",
86
+ children: [/* @__PURE__ */ D("div", {
87
+ className: "min-w-0",
88
+ children: [
89
+ /* @__PURE__ */ D("p", {
90
+ className: "flex items-center gap-2 text-sm font-medium text-text-secondary",
91
+ children: [/* @__PURE__ */ E(C, {
92
+ className: "h-4 w-4",
93
+ "aria-hidden": !0
94
+ }), A("savings.headline.label", "Estimated savings this year")]
95
+ }),
96
+ /* @__PURE__ */ E("p", {
97
+ className: "mt-1 break-words text-3xl font-bold tabular-nums text-status-success-text sm:text-4xl",
98
+ "data-testid": "savings-headline-amount",
99
+ children: z ?? n(0, "USD")
100
+ }),
101
+ B && /* @__PURE__ */ E("p", {
102
+ className: "mt-1 text-sm tabular-nums text-text-secondary",
103
+ children: A("savings.headline.perMonth", "About {{amount}} a month", { amount: B })
104
+ })
105
+ ]
106
+ }), /* @__PURE__ */ D("dl", {
107
+ className: "grid shrink-0 grid-cols-3 gap-4 text-sm",
108
+ children: [
109
+ /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("dt", {
110
+ className: "text-xs text-text-secondary",
111
+ children: A("savings.headline.actions", "Actions")
112
+ }), /* @__PURE__ */ E("dd", {
113
+ className: "font-semibold tabular-nums text-text-primary",
114
+ children: U
115
+ })] }),
116
+ /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("dt", {
117
+ className: "text-xs text-text-secondary",
118
+ children: A("savings.headline.fromDecisions", "From recommendations")
119
+ }), /* @__PURE__ */ E("dd", {
120
+ className: "font-semibold tabular-nums text-text-primary",
121
+ children: V ?? "—"
122
+ })] }),
123
+ /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("dt", {
124
+ className: "text-xs text-text-secondary",
125
+ children: A("savings.headline.fromBundles", "From bundles")
126
+ }), /* @__PURE__ */ E("dd", {
127
+ className: "font-semibold tabular-nums text-text-primary",
128
+ children: H ?? "—"
129
+ })] })
130
+ ]
131
+ })]
132
+ }),
133
+ M.data?.truncated && /* @__PURE__ */ E("p", {
134
+ className: "mt-2 text-xs text-text-secondary",
135
+ children: A("savings.truncated", "Your portfolio is large, so this total covers the newest recommendations and subscriptions only.")
136
+ }),
137
+ N.data?.pageInfo.hasMore && /* @__PURE__ */ E("p", {
138
+ className: "mt-2 text-xs text-text-secondary",
139
+ children: A("savings.moreDecisions", "Showing the newest {{count}} recommendations. The full queue is on the AI Decisions page.", { count: N.data.items.length })
140
+ }),
141
+ P && /* @__PURE__ */ E("div", {
142
+ role: "status",
143
+ className: "mt-4 rounded-lg border border-status-success-text/30 bg-status-success-bg-subtle px-4 py-2 text-sm text-status-success-text",
144
+ "data-testid": "savings-notice",
145
+ children: P
146
+ }),
147
+ U === 0 && L.length === 0 ? /* @__PURE__ */ E("div", {
148
+ className: "mt-6",
149
+ children: /* @__PURE__ */ E(r, {
150
+ illustration: "empty-data",
151
+ title: A("savings.empty.title", "No savings found right now"),
152
+ description: A("savings.empty.description", "Run your cost, renewal, vendor and discovery agents to look for savings. Running an agent never changes a subscription on its own."),
153
+ action: /* @__PURE__ */ E("button", {
154
+ type: "button",
155
+ onClick: () => j("/agents"),
156
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90",
157
+ children: A("savings.empty.action", "Go to agents")
158
+ })
159
+ })
160
+ }) : /* @__PURE__ */ D("div", {
161
+ className: "mt-6",
162
+ children: [d.map((e) => {
163
+ let t = I[e.id];
164
+ if (t.length === 0) return null;
165
+ let r = m(g(t), n);
166
+ return /* @__PURE__ */ E(o, {
167
+ title: `${A(e.key, e.fallback)} (${t.length})`,
168
+ description: A(e.descriptionKey, e.descriptionFallback),
169
+ action: r ? /* @__PURE__ */ E("span", {
170
+ className: "text-sm font-semibold tabular-nums text-status-success-text",
171
+ children: A("savings.sectionTotal", "{{amount}} a year", { amount: r })
172
+ }) : void 0,
173
+ children: /* @__PURE__ */ E("div", {
174
+ className: "grid grid-cols-1 gap-3 lg:grid-cols-2",
175
+ "data-testid": `savings-section-${e.id}`,
176
+ children: t.map((e) => /* @__PURE__ */ E(v, { decision: e }, e.id))
177
+ })
178
+ }, e.id);
179
+ }), /* @__PURE__ */ E(o, {
180
+ title: A("savings.bundles.title", "Bundle recommendations"),
181
+ description: A("savings.bundles.description", "Subscriptions that overlap and could be merged onto a single plan."),
182
+ children: L.length === 0 ? /* @__PURE__ */ D("p", {
183
+ className: "flex items-center gap-2 rounded-lg border border-border-subtle px-4 py-3 text-sm text-text-secondary",
184
+ "data-testid": "bundles-empty",
185
+ children: [/* @__PURE__ */ E(S, {
186
+ className: "h-4 w-4",
187
+ "aria-hidden": !0
188
+ }), A("savings.bundles.empty", "No overlapping subscriptions found. Duplicate accounts with one provider, or several streaming, AI, news, telecom or education services, show up here.")]
189
+ }) : /* @__PURE__ */ E("div", {
190
+ className: "grid grid-cols-1 gap-3 lg:grid-cols-2",
191
+ "data-testid": "savings-bundles",
192
+ children: L.map((e) => /* @__PURE__ */ E(_, {
193
+ bundle: e,
194
+ onConsolidated: q
195
+ }, e.id))
196
+ })
197
+ })]
198
+ })
199
+ ] })]
200
+ });
201
+ };
202
+ //#endregion
203
+ export { A as SavingsPage };
204
+
205
+ //# sourceMappingURL=SavingsPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SavingsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/SavingsPage.tsx"],"sourcesContent":["/**\n * Cost optimization dashboard (BOFF-4001)\n * @module subscriberbot/pages\n *\n * `/subscriberbot/savings` — \"how much can I save this year, and what should I\n * do about it\". The headline yearly saving and the bundle suggestions come\n * from `subSavingsOverview`; the action cards are the open (PROPOSED)\n * decisions from `subListDecisions`, grouped into Cancel unused, Downgrade\n * plan, Switch to annual and Other. Apply / Dismiss run `subApplyDecision` /\n * `subDismissDecision`; a bundle is consolidated with\n * `subConsolidateRelationships` or hidden with `subDismissBundle`. Nothing on\n * this page is placeholder data.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Bot, Combine, PiggyBank, RefreshCw } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState, LoadingView, Section } from '../components';\nimport { AiDisclaimer, DecisionsUnavailableBanner } from '../components/decisions';\nimport {\n BundleCard,\n SAVINGS_SECTIONS,\n SavingsCard,\n groupDecisions,\n headlineAmounts,\n joinAmounts,\n perMonth,\n savingsByCurrency,\n} from '../components/savings';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport { useDecisions, useSavingsOverview } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatCurrency } from '../utils/formatters';\nimport type { SubConsolidationResult, SubDecisionFilter } from '../types';\n\nconst OPEN_FILTER: SubDecisionFilter = { status: 'PROPOSED' };\n\nexport const SavingsPage: FC = () => {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const overview = useSavingsOverview();\n const decisions = useDecisions(OPEN_FILTER);\n const [notice, setNotice] = useState<string | null>(null);\n\n const grouped = useMemo(() => groupDecisions(decisions.data?.items ?? []), [decisions.data]);\n const bundles = overview.data?.bundles ?? [];\n const totals = overview.data?.totals ?? [];\n const headline = joinAmounts(headlineAmounts(totals), formatCurrency);\n const monthly = joinAmounts(\n totals.map((t) => ({ currency: t.currency, amount: perMonth(t.annualSavings) })),\n formatCurrency\n );\n const fromDecisions = joinAmounts(\n totals.map((t) => ({ currency: t.currency, amount: t.fromDecisions })),\n formatCurrency\n );\n const fromBundles = joinAmounts(\n totals.map((t) => ({ currency: t.currency, amount: t.fromBundles })),\n formatCurrency\n );\n const openCount = Object.values(grouped).reduce((n, list) => n + list.length, 0);\n const isLoading = overview.isLoading || decisions.isLoading;\n const isError = overview.isError || decisions.isError;\n const error = overview.error ?? decisions.error;\n\n const onConsolidated = (result: SubConsolidationResult) => {\n const saved = formatCurrency(result.estimatedSavings, result.currency);\n setNotice(\n result.failed.length > 0\n ? tr(\n 'savings.bundle.partial',\n 'Kept \"{{keep}}\" and cancelled {{count}} subscription(s); {{failed}} could not be cancelled and are unchanged.',\n {\n keep: result.kept.label,\n count: result.cancelled.length,\n failed: result.failed.length,\n }\n )\n : tr(\n 'savings.bundle.done',\n 'Kept \"{{keep}}\" and cancelled {{count}} subscription(s). Estimated saving: {{amount}} a year.',\n { keep: result.kept.label, count: result.cancelled.length, amount: saved }\n )\n );\n };\n\n const refresh = () => {\n void overview.refetch();\n void decisions.refetch();\n };\n\n return (\n <PageLayout\n title={tr('savings.title', 'Optimize your spend')}\n description={tr(\n 'savings.description',\n 'What you could save this year and the specific changes that get you there. Every change asks you to confirm first.'\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={refresh}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <RefreshCw className=\"h-4 w-4\" aria-hidden /> {tr('savings.refresh', 'Refresh')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Bot className=\"h-4 w-4\" aria-hidden /> {tr('savings.runAgents', 'Run agents')}\n </button>\n </div>\n }\n >\n <AiDisclaimer />\n\n {isLoading ? (\n <LoadingView label={tr('savings.loading', 'Finding savings…')} />\n ) : isError ? (\n <div className=\"mt-6\">\n <DecisionsUnavailableBanner\n message={error instanceof Error ? error.message : undefined}\n onRetry={refresh}\n />\n </div>\n ) : (\n <>\n <GlassCard\n treatment=\"glass\"\n glow\n className=\"mt-6 flex flex-col gap-4 p-6 sm:flex-row sm:items-end sm:justify-between\"\n data-testid=\"savings-headline\"\n >\n <div className=\"min-w-0\">\n <p className=\"flex items-center gap-2 text-sm font-medium text-text-secondary\">\n <PiggyBank className=\"h-4 w-4\" aria-hidden />\n {tr('savings.headline.label', 'Estimated savings this year')}\n </p>\n <p\n className=\"mt-1 break-words text-3xl font-bold tabular-nums text-status-success-text sm:text-4xl\"\n data-testid=\"savings-headline-amount\"\n >\n {headline ?? formatCurrency(0, 'USD')}\n </p>\n {monthly && (\n <p className=\"mt-1 text-sm tabular-nums text-text-secondary\">\n {tr('savings.headline.perMonth', 'About {{amount}} a month', { amount: monthly })}\n </p>\n )}\n </div>\n <dl className=\"grid shrink-0 grid-cols-3 gap-4 text-sm\">\n <div>\n <dt className=\"text-xs text-text-secondary\">\n {tr('savings.headline.actions', 'Actions')}\n </dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">{openCount}</dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-secondary\">\n {tr('savings.headline.fromDecisions', 'From recommendations')}\n </dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">\n {fromDecisions ?? '—'}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-secondary\">\n {tr('savings.headline.fromBundles', 'From bundles')}\n </dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">\n {fromBundles ?? '—'}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {overview.data?.truncated && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n {tr(\n 'savings.truncated',\n 'Your portfolio is large, so this total covers the newest recommendations and subscriptions only.'\n )}\n </p>\n )}\n {decisions.data?.pageInfo.hasMore && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n {tr(\n 'savings.moreDecisions',\n 'Showing the newest {{count}} recommendations. The full queue is on the AI Decisions page.',\n { count: decisions.data.items.length }\n )}\n </p>\n )}\n\n {notice && (\n <div\n role=\"status\"\n className=\"mt-4 rounded-lg border border-status-success-text/30 bg-status-success-bg-subtle px-4 py-2 text-sm text-status-success-text\"\n data-testid=\"savings-notice\"\n >\n {notice}\n </div>\n )}\n\n {openCount === 0 && bundles.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n title={tr('savings.empty.title', 'No savings found right now')}\n description={tr(\n 'savings.empty.description',\n 'Run your cost, renewal, vendor and discovery agents to look for savings. Running an agent never changes a subscription on its own.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('savings.empty.action', 'Go to agents')}\n </button>\n }\n />\n </div>\n ) : (\n <div className=\"mt-6\">\n {SAVINGS_SECTIONS.map((section) => {\n const items = grouped[section.id];\n if (items.length === 0) return null;\n const sum = joinAmounts(savingsByCurrency(items), formatCurrency);\n return (\n <Section\n key={section.id}\n title={`${tr(section.key, section.fallback)} (${items.length})`}\n description={tr(section.descriptionKey, section.descriptionFallback)}\n action={\n sum ? (\n <span className=\"text-sm font-semibold tabular-nums text-status-success-text\">\n {tr('savings.sectionTotal', '{{amount}} a year', { amount: sum })}\n </span>\n ) : undefined\n }\n >\n <div\n className=\"grid grid-cols-1 gap-3 lg:grid-cols-2\"\n data-testid={`savings-section-${section.id}`}\n >\n {items.map((d) => (\n <SavingsCard key={d.id} decision={d} />\n ))}\n </div>\n </Section>\n );\n })}\n\n <Section\n title={tr('savings.bundles.title', 'Bundle recommendations')}\n description={tr(\n 'savings.bundles.description',\n 'Subscriptions that overlap and could be merged onto a single plan.'\n )}\n >\n {bundles.length === 0 ? (\n <p\n className=\"flex items-center gap-2 rounded-lg border border-border-subtle px-4 py-3 text-sm text-text-secondary\"\n data-testid=\"bundles-empty\"\n >\n <Combine className=\"h-4 w-4\" aria-hidden />\n {tr(\n 'savings.bundles.empty',\n 'No overlapping subscriptions found. Duplicate accounts with one provider, or several streaming, AI, news, telecom or education services, show up here.'\n )}\n </p>\n ) : (\n <div\n className=\"grid grid-cols-1 gap-3 lg:grid-cols-2\"\n data-testid=\"savings-bundles\"\n >\n {bundles.map((b) => (\n <BundleCard key={b.id} bundle={b} onConsolidated={onConsolidated} />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RelationshipDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/relationships/RelationshipDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA21BH,wBAAgB,sBAAsB,gCA+iBrC"}
1
+ {"version":3,"file":"RelationshipDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/relationships/RelationshipDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA61BH,wBAAgB,sBAAsB,gCAsjBrC"}