@burdenoff/microfe-adaptercloud 2026.706.2 → 2026.706.3

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 (40) hide show
  1. package/dist/adaptercloud/AdapterCloudRoot.js +52 -44
  2. package/dist/adaptercloud/AdapterCloudRoot.js.map +1 -1
  3. package/dist/adaptercloud/AdapterCloudRoutes.js +18 -14
  4. package/dist/adaptercloud/AdapterCloudRoutes.js.map +1 -1
  5. package/dist/adaptercloud/components/ConfirmDialog.js.map +1 -1
  6. package/dist/adaptercloud/components/viz/BarChart.js +32 -0
  7. package/dist/adaptercloud/components/viz/BarChart.js.map +1 -0
  8. package/dist/adaptercloud/components/viz/Sparkline.js +50 -0
  9. package/dist/adaptercloud/components/viz/Sparkline.js.map +1 -0
  10. package/dist/adaptercloud/components/viz/StackedBar.js +1 -0
  11. package/dist/adaptercloud/components/viz/TopologyGraph.js +96 -0
  12. package/dist/adaptercloud/components/viz/TopologyGraph.js.map +1 -0
  13. package/dist/adaptercloud/components/viz/index.js +4 -0
  14. package/dist/adaptercloud/hooks/useAdapterCloudApi.js +22 -4
  15. package/dist/adaptercloud/hooks/useAdapterCloudApi.js.map +1 -1
  16. package/dist/adaptercloud/pages/OverviewPage.js.map +1 -1
  17. package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js +176 -123
  18. package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js.map +1 -1
  19. package/dist/adaptercloud/pages/adapters/AdapterFormPage.js +261 -179
  20. package/dist/adaptercloud/pages/adapters/AdapterFormPage.js.map +1 -1
  21. package/dist/adaptercloud/pages/adapters/AdaptersListPage.js.map +1 -1
  22. package/dist/adaptercloud/pages/connections/ConnectionsListPage.js +158 -94
  23. package/dist/adaptercloud/pages/connections/ConnectionsListPage.js.map +1 -1
  24. package/dist/adaptercloud/pages/costs/CostsPage.js +160 -111
  25. package/dist/adaptercloud/pages/costs/CostsPage.js.map +1 -1
  26. package/dist/adaptercloud/pages/drift/DriftListPage.js +163 -84
  27. package/dist/adaptercloud/pages/drift/DriftListPage.js.map +1 -1
  28. package/dist/adaptercloud/pages/policies/PoliciesListPage.js +177 -96
  29. package/dist/adaptercloud/pages/policies/PoliciesListPage.js.map +1 -1
  30. package/dist/adaptercloud/pages/resources/ResourceDetailPage.js +145 -131
  31. package/dist/adaptercloud/pages/resources/ResourceDetailPage.js.map +1 -1
  32. package/dist/adaptercloud/pages/resources/ResourcesListPage.js +10 -1
  33. package/dist/adaptercloud/pages/resources/ResourcesListPage.js.map +1 -1
  34. package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js +226 -165
  35. package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js.map +1 -1
  36. package/dist/adaptercloud/pages/topology/TopologyPage.js +397 -0
  37. package/dist/adaptercloud/pages/topology/TopologyPage.js.map +1 -0
  38. package/dist/adaptercloud/pages/violations/ViolationsListPage.js +184 -131
  39. package/dist/adaptercloud/pages/violations/ViolationsListPage.js.map +1 -1
  40. package/package.json +1 -1
@@ -5,44 +5,65 @@ import { formatMoney as r } from "../../utils/domain.js";
5
5
  import { DataState as i } from "../../components/DataState.js";
6
6
  import { useCostRecords as a, useCostSummary as o } from "../../hooks/useAdapterCloudApi.js";
7
7
  import { formatDate as s } from "../../utils/formatters.js";
8
- import { useCallback as c, useMemo as l, useState as u } from "react";
9
- import { useI18n as d } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
10
- import { DollarSign as f, Receipt as p, Search as m, X as h } from "lucide-react";
11
- import { jsx as g, jsxs as _ } from "react/jsx-runtime";
12
- import { EmphasisPanel as v, GlassCard as y, ResponsiveTable as b } from "@burdenoff/fe-libs/ui";
8
+ import { BarChart as c } from "../../components/viz/BarChart.js";
9
+ import { Sparkline as l } from "../../components/viz/Sparkline.js";
10
+ import "../../components/viz/index.js";
11
+ import { useCallback as u, useMemo as d, useState as f } from "react";
12
+ import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
13
+ import { DollarSign as m, Receipt as h, Search as g, X as _ } from "lucide-react";
14
+ import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
15
+ import { EmphasisPanel as x, GlassCard as S, ResponsiveTable as C } from "@burdenoff/fe-libs/ui";
13
16
  //#region src/adaptercloud/pages/costs/CostsPage.tsx
14
- function x() {
15
- let { navigateTo: x } = e(), { t: S } = d(), C = c((e, t) => {
16
- let n = S(e);
17
+ function w() {
18
+ let { navigateTo: w } = e(), { t: T } = p(), E = u((e, t) => {
19
+ let n = T(e);
17
20
  return n === e ? t : n;
18
- }, [S]), [w, T] = u(""), [E, D] = u(""), O = o(), k = a({ service: E || void 0 }), A = O.data?.currency ?? "USD", j = l(() => [...O.data?.lines ?? []].sort((e, t) => t.total - e.total), [O.data]), M = c((e) => {
19
- e.preventDefault(), D(w.trim());
20
- }, [w]), N = c(() => {
21
- T(""), D("");
22
- }, []), P = [
21
+ }, [T]), [D, O] = f(""), [k, A] = f(""), j = o(), M = a({ service: k || void 0 }), N = j.data?.currency ?? "USD", P = d(() => [...j.data?.lines ?? []].sort((e, t) => t.total - e.total), [j.data]), F = d(() => P.map((e) => ({
22
+ key: e.service,
23
+ label: e.service,
24
+ value: e.total,
25
+ display: r(e.total, N)
26
+ })), [P, N]), I = d(() => {
27
+ let e = /* @__PURE__ */ new Map();
28
+ for (let t of M.data ?? []) {
29
+ let n = new Date(t.periodStart);
30
+ if (Number.isNaN(n.getTime())) continue;
31
+ let r = `${n.getUTCFullYear()}-${String(n.getUTCMonth() + 1).padStart(2, "0")}`;
32
+ e.set(r, (e.get(r) ?? 0) + t.amount);
33
+ }
34
+ let t = [...e.entries()].sort(([e], [t]) => e.localeCompare(t)).map(([, e]) => e), n = t[t.length - 1] ?? 0, r = t[t.length - 2] ?? 0;
35
+ return {
36
+ points: t,
37
+ deltaPct: r > 0 ? Math.round((n - r) / r * 100) : null
38
+ };
39
+ }, [M.data]), L = u((e) => {
40
+ e.preventDefault(), A(D.trim());
41
+ }, [D]), R = u(() => {
42
+ O(""), A("");
43
+ }, []), z = [
23
44
  {
24
45
  key: "service",
25
- header: C("adaptercloud.costs.service", "Service"),
46
+ header: E("adaptercloud.costs.service", "Service"),
26
47
  priority: "primary",
27
- cell: (e) => /* @__PURE__ */ g("span", {
48
+ cell: (e) => /* @__PURE__ */ y("span", {
28
49
  className: "font-medium text-text-primary",
29
50
  children: e.service
30
51
  })
31
52
  },
32
53
  {
33
54
  key: "amount",
34
- header: C("adaptercloud.costs.amount", "Amount"),
55
+ header: E("adaptercloud.costs.amount", "Amount"),
35
56
  priority: "primary",
36
- cell: (e) => /* @__PURE__ */ g("span", {
57
+ cell: (e) => /* @__PURE__ */ y("span", {
37
58
  className: "tabular-nums text-text-primary",
38
59
  children: r(e.amount, e.currency)
39
60
  })
40
61
  },
41
62
  {
42
63
  key: "period",
43
- header: C("adaptercloud.costs.period", "Period"),
64
+ header: E("adaptercloud.costs.period", "Period"),
44
65
  priority: "secondary",
45
- cell: (e) => /* @__PURE__ */ _("span", {
66
+ cell: (e) => /* @__PURE__ */ b("span", {
46
67
  className: "text-sm text-text-secondary",
47
68
  children: [
48
69
  s(e.periodStart, {
@@ -59,136 +80,164 @@ function x() {
59
80
  })
60
81
  },
61
82
  {
62
- key: "scope",
63
- header: C("adaptercloud.costs.scope", "Scope"),
83
+ key: "resource",
84
+ header: E("adaptercloud.costs.resource", "Resource"),
85
+ priority: "secondary",
86
+ cell: (e) => e.resource?.name ? /* @__PURE__ */ y("button", {
87
+ type: "button",
88
+ onClick: () => e.resourceId && w(`/resources/${e.resourceId}`),
89
+ className: "max-w-[12rem] truncate text-left text-sm text-accent-blue hover:underline",
90
+ children: e.resource.name
91
+ }) : e.resourceId ? /* @__PURE__ */ b("span", {
92
+ className: "font-mono text-xs text-text-secondary",
93
+ children: [e.resourceId.slice(0, 10), "…"]
94
+ }) : /* @__PURE__ */ y("span", {
95
+ className: "text-text-tertiary",
96
+ children: "—"
97
+ })
98
+ },
99
+ {
100
+ key: "connection",
101
+ header: E("adaptercloud.costs.connection", "Connection"),
64
102
  priority: "hidden-on-mobile",
65
- cell: (e) => /* @__PURE__ */ g("span", {
66
- className: "text-xs text-text-secondary",
67
- children: e.resourceId ? C("adaptercloud.costs.resourceScope", "Resource") : e.connectionId ? C("adaptercloud.costs.connectionScope", "Connection") : C("adaptercloud.costs.workspaceScope", "Workspace")
103
+ cell: (e) => e.connection?.name ? /* @__PURE__ */ y("span", {
104
+ className: "max-w-[12rem] truncate text-sm text-text-primary",
105
+ children: e.connection.name
106
+ }) : /* @__PURE__ */ y("span", {
107
+ className: "text-text-tertiary",
108
+ children: "—"
68
109
  })
69
110
  }
70
111
  ];
71
- return /* @__PURE__ */ _(t, {
72
- title: C("adaptercloud.costs.title", "FinOps"),
73
- description: C("adaptercloud.costs.subtitle", "Spend attributed to the same resources you operate — cost and topology share one model."),
74
- children: [/* @__PURE__ */ _(i, {
75
- isLoading: O.isLoading,
76
- isError: O.isError,
77
- error: O.error,
78
- onRetry: () => void O.refetch(),
79
- children: [/* @__PURE__ */ _(v, {
112
+ return /* @__PURE__ */ b(t, {
113
+ title: E("adaptercloud.costs.title", "FinOps"),
114
+ description: E("adaptercloud.costs.subtitle", "Spend attributed to the same resources you operate — cost and topology share one model."),
115
+ children: [/* @__PURE__ */ b(i, {
116
+ isLoading: j.isLoading,
117
+ isError: j.isError,
118
+ error: j.error,
119
+ onRetry: () => void j.refetch(),
120
+ children: [/* @__PURE__ */ b(x, {
80
121
  className: "mb-8",
81
- children: [/* @__PURE__ */ g("div", {
122
+ children: [/* @__PURE__ */ y("div", {
82
123
  className: "mb-4 flex items-center justify-between",
83
- children: /* @__PURE__ */ g("h2", {
124
+ children: /* @__PURE__ */ y("h2", {
84
125
  className: "text-lg font-semibold text-text-primary",
85
- children: C("adaptercloud.costs.summary", "Cost Summary")
126
+ children: E("adaptercloud.costs.summary", "Cost Summary")
86
127
  })
87
- }), /* @__PURE__ */ _("div", {
128
+ }), /* @__PURE__ */ b("div", {
88
129
  className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
89
- children: [/* @__PURE__ */ g(n, {
130
+ children: [/* @__PURE__ */ y(n, {
90
131
  featured: !0,
91
- icon: /* @__PURE__ */ g(f, { className: "h-5 w-5" }),
92
- value: O.data ? r(O.data.total, A) : "—",
93
- label: C("adaptercloud.costs.totalSpend", "Total Spend")
94
- }), j.slice(0, 3).map((e) => /* @__PURE__ */ g(n, {
95
- icon: /* @__PURE__ */ g(p, { className: "h-5 w-5" }),
96
- value: r(e.total, A),
132
+ icon: /* @__PURE__ */ y(m, { className: "h-5 w-5" }),
133
+ value: j.data ? r(j.data.total, N) : "—",
134
+ label: E("adaptercloud.costs.totalSpend", "Total Spend")
135
+ }), P.slice(0, 3).map((e) => /* @__PURE__ */ y(n, {
136
+ icon: /* @__PURE__ */ y(h, { className: "h-5 w-5" }),
137
+ value: r(e.total, N),
97
138
  label: e.service
98
139
  }, e.service))]
99
140
  })]
100
- }), j.length > 0 && /* @__PURE__ */ _("section", {
101
- className: "mb-8",
102
- children: [/* @__PURE__ */ g("h2", {
103
- className: "mb-4 text-lg font-semibold text-text-primary",
104
- children: C("adaptercloud.costs.byService", "Spend by Service")
105
- }), /* @__PURE__ */ g(y, {
106
- className: "p-5",
107
- children: /* @__PURE__ */ g("ul", {
108
- className: "space-y-3",
109
- children: j.map((e) => {
110
- let t = O.data?.total ? Math.round(e.total / O.data.total * 100) : 0;
111
- return /* @__PURE__ */ _("li", { children: [/* @__PURE__ */ _("div", {
112
- className: "mb-1 flex items-center justify-between text-sm",
113
- children: [/* @__PURE__ */ g("span", {
114
- className: "truncate text-text-primary",
115
- children: e.service
116
- }), /* @__PURE__ */ _("span", {
117
- className: "tabular-nums text-text-secondary",
118
- children: [
119
- r(e.total, A),
120
- " · ",
121
- t,
122
- "%"
123
- ]
124
- })]
125
- }), /* @__PURE__ */ g("div", {
126
- className: "h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
127
- children: /* @__PURE__ */ g("div", {
128
- className: "h-full rounded-full bg-action-primary-bg",
129
- style: { width: `${Math.max(t, 2)}%` }
130
- })
131
- })] }, e.service);
141
+ }), P.length > 0 && /* @__PURE__ */ b("div", {
142
+ className: "mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3",
143
+ children: [/* @__PURE__ */ b("section", {
144
+ className: "lg:col-span-2",
145
+ children: [/* @__PURE__ */ y("h2", {
146
+ className: "mb-4 text-lg font-semibold text-text-primary",
147
+ children: E("adaptercloud.costs.byService", "Spend by Service")
148
+ }), /* @__PURE__ */ y(S, {
149
+ className: "p-5",
150
+ children: /* @__PURE__ */ y(c, {
151
+ data: F,
152
+ labelWidthClassName: "w-24 sm:w-32"
132
153
  })
154
+ })]
155
+ }), /* @__PURE__ */ b("section", { children: [/* @__PURE__ */ y("h2", {
156
+ className: "mb-4 text-lg font-semibold text-text-primary",
157
+ children: E("adaptercloud.costs.trend", "Monthly Trend")
158
+ }), /* @__PURE__ */ y(S, {
159
+ className: "flex h-[calc(100%-2.75rem)] flex-col justify-between p-5",
160
+ children: I.points.length >= 2 ? /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ b("div", {
161
+ className: "flex items-baseline gap-2",
162
+ children: [/* @__PURE__ */ y("span", {
163
+ className: "text-2xl font-bold tabular-nums text-text-primary",
164
+ children: r(I.points[I.points.length - 1] ?? 0, N)
165
+ }), I.deltaPct !== null && /* @__PURE__ */ b("span", {
166
+ className: `text-xs font-semibold tabular-nums ${I.deltaPct > 0 ? "text-status-error-text" : "text-status-success-text"}`,
167
+ children: [
168
+ I.deltaPct > 0 ? "▲" : "▼",
169
+ " ",
170
+ Math.abs(I.deltaPct),
171
+ "%",
172
+ " ",
173
+ E("adaptercloud.costs.vsPrev", "vs prev")
174
+ ]
175
+ })]
176
+ }), /* @__PURE__ */ y(l, {
177
+ points: I.points,
178
+ className: "mt-4 h-16 w-full text-accent-blue"
179
+ })] }) : /* @__PURE__ */ y("p", {
180
+ className: "py-6 text-center text-sm text-text-secondary",
181
+ children: E("adaptercloud.costs.trendEmpty", "Ingest at least two months of cost to see a trend.")
133
182
  })
134
- })]
183
+ })] })]
135
184
  })]
136
- }), /* @__PURE__ */ _("section", { children: [/* @__PURE__ */ _("div", {
185
+ }), /* @__PURE__ */ b("section", { children: [/* @__PURE__ */ b("div", {
137
186
  className: "mb-4 flex flex-wrap items-center justify-between gap-3",
138
- children: [/* @__PURE__ */ g("h2", {
187
+ children: [/* @__PURE__ */ y("h2", {
139
188
  className: "text-lg font-semibold text-text-primary",
140
- children: C("adaptercloud.costs.records", "Cost Records")
141
- }), /* @__PURE__ */ _("form", {
142
- onSubmit: M,
189
+ children: E("adaptercloud.costs.records", "Cost Records")
190
+ }), /* @__PURE__ */ b("form", {
191
+ onSubmit: L,
143
192
  role: "search",
144
193
  className: "flex w-full items-center gap-2 sm:w-auto",
145
- children: [/* @__PURE__ */ _("div", {
194
+ children: [/* @__PURE__ */ b("div", {
146
195
  className: "relative w-full sm:w-56",
147
- children: [/* @__PURE__ */ g("input", {
196
+ children: [/* @__PURE__ */ y("input", {
148
197
  type: "text",
149
- value: w,
150
- onChange: (e) => T(e.target.value),
151
- "aria-label": C("adaptercloud.costs.filterService", "Filter by service"),
152
- placeholder: C("adaptercloud.costs.filterServicePlaceholder", "Filter by service…"),
198
+ value: D,
199
+ onChange: (e) => O(e.target.value),
200
+ "aria-label": E("adaptercloud.costs.filterService", "Filter by service"),
201
+ placeholder: E("adaptercloud.costs.filterServicePlaceholder", "Filter by service…"),
153
202
  className: "w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-3 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
154
- }), E && /* @__PURE__ */ g("button", {
203
+ }), k && /* @__PURE__ */ y("button", {
155
204
  type: "button",
156
- onClick: N,
157
- "aria-label": C("adaptercloud.costs.clearServiceFilter", "Clear service filter"),
205
+ onClick: R,
206
+ "aria-label": E("adaptercloud.costs.clearServiceFilter", "Clear service filter"),
158
207
  className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]",
159
- children: /* @__PURE__ */ g(h, { className: "h-4 w-4" })
208
+ children: /* @__PURE__ */ y(_, { className: "h-4 w-4" })
160
209
  })]
161
- }), /* @__PURE__ */ g("button", {
210
+ }), /* @__PURE__ */ y("button", {
162
211
  type: "submit",
163
- "aria-label": C("adaptercloud.costs.applyServiceFilter", "Apply service filter"),
212
+ "aria-label": E("adaptercloud.costs.applyServiceFilter", "Apply service filter"),
164
213
  className: "inline-flex shrink-0 items-center justify-center rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-text-primary transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]",
165
- children: /* @__PURE__ */ g(m, { className: "h-4 w-4" })
214
+ children: /* @__PURE__ */ y(g, { className: "h-4 w-4" })
166
215
  })]
167
216
  })]
168
- }), /* @__PURE__ */ g(i, {
169
- isLoading: k.isLoading,
170
- isError: k.isError,
171
- isEmpty: (k.data ?? []).length === 0,
172
- error: k.error,
173
- onRetry: () => void k.refetch(),
174
- emptyTitle: C("adaptercloud.costs.emptyTitle", "No cost records"),
175
- emptyDescription: C("adaptercloud.costs.emptyDescription", "Run a cost-ingest sync against a connection to populate the ledger."),
176
- emptyIcon: /* @__PURE__ */ g(p, { className: "h-10 w-10" }),
177
- emptyAction: /* @__PURE__ */ g("button", {
217
+ }), /* @__PURE__ */ y(i, {
218
+ isLoading: M.isLoading,
219
+ isError: M.isError,
220
+ isEmpty: (M.data ?? []).length === 0,
221
+ error: M.error,
222
+ onRetry: () => void M.refetch(),
223
+ emptyTitle: E("adaptercloud.costs.emptyTitle", "No cost records"),
224
+ emptyDescription: E("adaptercloud.costs.emptyDescription", "Run a cost-ingest sync against a connection to populate the ledger."),
225
+ emptyIcon: /* @__PURE__ */ y(h, { className: "h-10 w-10" }),
226
+ emptyAction: /* @__PURE__ */ y("button", {
178
227
  type: "button",
179
- onClick: () => x("/connections"),
228
+ onClick: () => w("/connections"),
180
229
  className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90",
181
- children: C("adaptercloud.costs.goToConnections", "Go to connections")
230
+ children: E("adaptercloud.costs.goToConnections", "Go to connections")
182
231
  }),
183
- children: /* @__PURE__ */ g(b, {
184
- data: k.data ?? [],
185
- columns: P,
232
+ children: /* @__PURE__ */ y(C, {
233
+ data: M.data ?? [],
234
+ columns: z,
186
235
  rowKey: (e) => e.id
187
236
  })
188
237
  })] })]
189
238
  });
190
239
  }
191
240
  //#endregion
192
- export { x as CostsPage };
241
+ export { w as CostsPage };
193
242
 
194
243
  //# sourceMappingURL=CostsPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CostsPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/costs/CostsPage.tsx"],"sourcesContent":["/**\n * Costs (FinOps) Page\n * @module adaptercloud/pages/costs\n *\n * Workspace FinOps: the cost summary by service (`adaCostSummary`) plus the raw\n * cost-record ledger (`adaListCostRecords`), optionally filtered by service.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DollarSign, Search, Receipt, X } from 'lucide-react';\nimport {\n EmphasisPanel,\n GlassCard,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\nexport function CostsPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [serviceInput, setServiceInput] = useState('');\n const [appliedService, setAppliedService] = useState('');\n\n const summary = useCostSummary();\n const records = useCostRecords({ service: appliedService || undefined });\n\n const currency = summary.data?.currency ?? 'USD';\n\n const sortedLines = useMemo(\n () => [...(summary.data?.lines ?? [])].sort((a, b) => b.total - a.total),\n [summary.data]\n );\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const clearService = useCallback(() => {\n setServiceInput('');\n setAppliedService('');\n }, []);\n\n const recordColumns: ResponsiveTableColumn<AdaCostRecord>[] = [\n {\n key: 'service',\n header: t('adaptercloud.costs.service', 'Service'),\n priority: 'primary',\n cell: (r) => <span className=\"font-medium text-text-primary\">{r.service}</span>,\n },\n {\n key: 'amount',\n header: t('adaptercloud.costs.amount', 'Amount'),\n priority: 'primary',\n cell: (r) => (\n <span className=\"tabular-nums text-text-primary\">{formatMoney(r.amount, r.currency)}</span>\n ),\n },\n {\n key: 'period',\n header: t('adaptercloud.costs.period', 'Period'),\n priority: 'secondary',\n cell: (r) => (\n <span className=\"text-sm text-text-secondary\">\n {formatDate(r.periodStart, { hour: undefined, minute: undefined })} –{' '}\n {formatDate(r.periodEnd, { hour: undefined, minute: undefined })}\n </span>\n ),\n },\n {\n key: 'scope',\n header: t('adaptercloud.costs.scope', 'Scope'),\n priority: 'hidden-on-mobile',\n cell: (r) => (\n <span className=\"text-xs text-text-secondary\">\n {r.resourceId\n ? t('adaptercloud.costs.resourceScope', 'Resource')\n : r.connectionId\n ? t('adaptercloud.costs.connectionScope', 'Connection')\n : t('adaptercloud.costs.workspaceScope', 'Workspace')}\n </span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={t('adaptercloud.costs.title', 'FinOps')}\n description={t(\n 'adaptercloud.costs.subtitle',\n 'Spend attributed to the same resources you operate — cost and topology share one model.'\n )}\n >\n <DataState\n isLoading={summary.isLoading}\n isError={summary.isError}\n error={summary.error}\n onRetry={() => void summary.refetch()}\n >\n {/* Total + by-service */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.summary', 'Cost Summary')}\n </h2>\n </div>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<DollarSign className=\"h-5 w-5\" />}\n value={summary.data ? formatMoney(summary.data.total, currency) : '—'}\n label={t('adaptercloud.costs.totalSpend', 'Total Spend')}\n />\n {sortedLines.slice(0, 3).map((line) => (\n <MetricsCard\n key={line.service}\n icon={<Receipt className=\"h-5 w-5\" />}\n value={formatMoney(line.total, currency)}\n label={line.service}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Full by-service breakdown */}\n {sortedLines.length > 0 && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.byService', 'Spend by Service')}\n </h2>\n <GlassCard className=\"p-5\">\n <ul className=\"space-y-3\">\n {sortedLines.map((line) => {\n const pct = summary.data?.total\n ? Math.round((line.total / summary.data.total) * 100)\n : 0;\n return (\n <li key={line.service}>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"truncate text-text-primary\">{line.service}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {formatMoney(line.total, currency)} · {pct}%\n </span>\n </div>\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(pct, 2)}%` }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n </section>\n )}\n </DataState>\n\n {/* Cost records ledger */}\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.records', 'Cost Records')}\n </h2>\n <form onSubmit={onSearch} role=\"search\" className=\"flex w-full items-center gap-2 sm:w-auto\">\n <div className=\"relative w-full sm:w-56\">\n <input\n type=\"text\"\n value={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n aria-label={t('adaptercloud.costs.filterService', 'Filter by service')}\n placeholder={t('adaptercloud.costs.filterServicePlaceholder', 'Filter by service…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-3 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {appliedService && (\n <button\n type=\"button\"\n onClick={clearService}\n aria-label={t('adaptercloud.costs.clearServiceFilter', 'Clear service filter')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <button\n type=\"submit\"\n aria-label={t('adaptercloud.costs.applyServiceFilter', 'Apply service filter')}\n className=\"inline-flex shrink-0 items-center justify-center rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-text-primary transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <Search className=\"h-4 w-4\" />\n </button>\n </form>\n </div>\n\n <DataState\n isLoading={records.isLoading}\n isError={records.isError}\n isEmpty={(records.data ?? []).length === 0}\n error={records.error}\n onRetry={() => void records.refetch()}\n emptyTitle={t('adaptercloud.costs.emptyTitle', 'No cost records')}\n emptyDescription={t(\n 'adaptercloud.costs.emptyDescription',\n 'Run a cost-ingest sync against a connection to populate the ledger.'\n )}\n emptyIcon={<Receipt className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.costs.goToConnections', 'Go to connections')}\n </button>\n }\n >\n <ResponsiveTable data={records.data ?? []} columns={recordColumns} rowKey={(r) => r.id} />\n </DataState>\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CostsPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/costs/CostsPage.tsx"],"sourcesContent":["/**\n * Costs (FinOps) Page\n * @module adaptercloud/pages/costs\n *\n * Workspace FinOps: the cost summary by service (`adaCostSummary`) plus the raw\n * cost-record ledger (`adaListCostRecords`), optionally filtered by service.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DollarSign, Search, Receipt, X } from 'lucide-react';\nimport {\n EmphasisPanel,\n GlassCard,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { DataState } from '../../components/DataState';\nimport { BarChart, Sparkline } from '../../components/viz';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\nexport function CostsPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [serviceInput, setServiceInput] = useState('');\n const [appliedService, setAppliedService] = useState('');\n\n const summary = useCostSummary();\n const records = useCostRecords({ service: appliedService || undefined });\n\n const currency = summary.data?.currency ?? 'USD';\n\n const sortedLines = useMemo(\n () => [...(summary.data?.lines ?? [])].sort((a, b) => b.total - a.total),\n [summary.data]\n );\n\n // Cost-by-service bars for the BarChart (service · magnitude · $).\n const serviceBars = useMemo(\n () =>\n sortedLines.map((line) => ({\n key: line.service,\n label: line.service,\n value: line.total,\n display: formatMoney(line.total, currency),\n })),\n [sortedLines, currency]\n );\n\n // Monthly cost trend, derived from the raw ledger (periodStart → YYYY-MM buckets).\n const trend = useMemo(() => {\n const buckets = new Map<string, number>();\n for (const r of records.data ?? []) {\n const d = new Date(r.periodStart);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n buckets.set(key, (buckets.get(key) ?? 0) + r.amount);\n }\n const series = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));\n const points = series.map(([, v]) => v);\n const last = points[points.length - 1] ?? 0;\n const prev = points[points.length - 2] ?? 0;\n const deltaPct = prev > 0 ? Math.round(((last - prev) / prev) * 100) : null;\n return { points, deltaPct };\n }, [records.data]);\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const clearService = useCallback(() => {\n setServiceInput('');\n setAppliedService('');\n }, []);\n\n const recordColumns: ResponsiveTableColumn<AdaCostRecord>[] = [\n {\n key: 'service',\n header: t('adaptercloud.costs.service', 'Service'),\n priority: 'primary',\n cell: (r) => <span className=\"font-medium text-text-primary\">{r.service}</span>,\n },\n {\n key: 'amount',\n header: t('adaptercloud.costs.amount', 'Amount'),\n priority: 'primary',\n cell: (r) => (\n <span className=\"tabular-nums text-text-primary\">{formatMoney(r.amount, r.currency)}</span>\n ),\n },\n {\n key: 'period',\n header: t('adaptercloud.costs.period', 'Period'),\n priority: 'secondary',\n cell: (r) => (\n <span className=\"text-sm text-text-secondary\">\n {formatDate(r.periodStart, { hour: undefined, minute: undefined })} –{' '}\n {formatDate(r.periodEnd, { hour: undefined, minute: undefined })}\n </span>\n ),\n },\n {\n key: 'resource',\n header: t('adaptercloud.costs.resource', 'Resource'),\n priority: 'secondary',\n cell: (r) =>\n r.resource?.name ? (\n <button\n type=\"button\"\n onClick={() => r.resourceId && navigateTo(`/resources/${r.resourceId}`)}\n className=\"max-w-[12rem] truncate text-left text-sm text-accent-blue hover:underline\"\n >\n {r.resource.name}\n </button>\n ) : r.resourceId ? (\n <span className=\"font-mono text-xs text-text-secondary\">{r.resourceId.slice(0, 10)}…</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'connection',\n header: t('adaptercloud.costs.connection', 'Connection'),\n priority: 'hidden-on-mobile',\n cell: (r) =>\n r.connection?.name ? (\n <span className=\"max-w-[12rem] truncate text-sm text-text-primary\">{r.connection.name}</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={t('adaptercloud.costs.title', 'FinOps')}\n description={t(\n 'adaptercloud.costs.subtitle',\n 'Spend attributed to the same resources you operate — cost and topology share one model.'\n )}\n >\n <DataState\n isLoading={summary.isLoading}\n isError={summary.isError}\n error={summary.error}\n onRetry={() => void summary.refetch()}\n >\n {/* Total + by-service */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.summary', 'Cost Summary')}\n </h2>\n </div>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<DollarSign className=\"h-5 w-5\" />}\n value={summary.data ? formatMoney(summary.data.total, currency) : '—'}\n label={t('adaptercloud.costs.totalSpend', 'Total Spend')}\n />\n {sortedLines.slice(0, 3).map((line) => (\n <MetricsCard\n key={line.service}\n icon={<Receipt className=\"h-5 w-5\" />}\n value={formatMoney(line.total, currency)}\n label={line.service}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Full by-service breakdown + monthly trend */}\n {sortedLines.length > 0 && (\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <section className=\"lg:col-span-2\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.byService', 'Spend by Service')}\n </h2>\n <GlassCard className=\"p-5\">\n <BarChart data={serviceBars} labelWidthClassName=\"w-24 sm:w-32\" />\n </GlassCard>\n </section>\n\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.trend', 'Monthly Trend')}\n </h2>\n <GlassCard className=\"flex h-[calc(100%-2.75rem)] flex-col justify-between p-5\">\n {trend.points.length >= 2 ? (\n <>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatMoney(trend.points[trend.points.length - 1] ?? 0, currency)}\n </span>\n {trend.deltaPct !== null && (\n <span\n className={`text-xs font-semibold tabular-nums ${\n trend.deltaPct > 0 ? 'text-status-error-text' : 'text-status-success-text'\n }`}\n >\n {trend.deltaPct > 0 ? '▲' : '▼'} {Math.abs(trend.deltaPct)}%{' '}\n {t('adaptercloud.costs.vsPrev', 'vs prev')}\n </span>\n )}\n </div>\n <Sparkline points={trend.points} className=\"mt-4 h-16 w-full text-accent-blue\" />\n </>\n ) : (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('adaptercloud.costs.trendEmpty', 'Ingest at least two months of cost to see a trend.')}\n </p>\n )}\n </GlassCard>\n </section>\n </div>\n )}\n </DataState>\n\n {/* Cost records ledger */}\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.records', 'Cost Records')}\n </h2>\n <form\n onSubmit={onSearch}\n role=\"search\"\n className=\"flex w-full items-center gap-2 sm:w-auto\"\n >\n <div className=\"relative w-full sm:w-56\">\n <input\n type=\"text\"\n value={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n aria-label={t('adaptercloud.costs.filterService', 'Filter by service')}\n placeholder={t('adaptercloud.costs.filterServicePlaceholder', 'Filter by service…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-3 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {appliedService && (\n <button\n type=\"button\"\n onClick={clearService}\n aria-label={t('adaptercloud.costs.clearServiceFilter', 'Clear service filter')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <button\n type=\"submit\"\n aria-label={t('adaptercloud.costs.applyServiceFilter', 'Apply service filter')}\n className=\"inline-flex shrink-0 items-center justify-center rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-text-primary transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <Search className=\"h-4 w-4\" />\n </button>\n </form>\n </div>\n\n <DataState\n isLoading={records.isLoading}\n isError={records.isError}\n isEmpty={(records.data ?? []).length === 0}\n error={records.error}\n onRetry={() => void records.refetch()}\n emptyTitle={t('adaptercloud.costs.emptyTitle', 'No cost records')}\n emptyDescription={t(\n 'adaptercloud.costs.emptyDescription',\n 'Run a cost-ingest sync against a connection to populate the ledger.'\n )}\n emptyIcon={<Receipt className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.costs.goToConnections', 'Go to connections')}\n </button>\n }\n >\n <ResponsiveTable data={records.data ?? []} columns={recordColumns} rowKey={(r) => r.id} />\n </DataState>\n </section>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA2BA,SAAgB,IAAY;CAC1B,IAAM,EAAE,kBAAe,GAAiB,EAClC,EAAE,GAAG,MAAc,GAAS,EAC5B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAU,CACZ,EAEK,CAAC,GAAc,KAAmB,EAAS,GAAG,EAC9C,CAAC,GAAgB,KAAqB,EAAS,GAAG,EAElD,IAAU,GAAgB,EAC1B,IAAU,EAAe,EAAE,SAAS,KAAkB,KAAA,GAAW,CAAC,EAElE,IAAW,EAAQ,MAAM,YAAY,OAErC,IAAc,QACZ,CAAC,GAAI,EAAQ,MAAM,SAAS,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EACxE,CAAC,EAAQ,KAAK,CACf,EAGK,IAAc,QAEhB,EAAY,KAAK,OAAU;EACzB,KAAK,EAAK;EACV,OAAO,EAAK;EACZ,OAAO,EAAK;EACZ,SAAS,EAAY,EAAK,OAAO,EAAS;EAC3C,EAAE,EACL,CAAC,GAAa,EAAS,CACxB,EAGK,IAAQ,QAAc;EAC1B,IAAM,oBAAU,IAAI,KAAqB;AACzC,OAAK,IAAM,KAAK,EAAQ,QAAQ,EAAE,EAAE;GAClC,IAAM,IAAI,IAAI,KAAK,EAAE,YAAY;AACjC,OAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAAE;GAC/B,IAAM,IAAM,GAAG,EAAE,gBAAgB,CAAC,GAAG,OAAO,EAAE,aAAa,GAAG,EAAE,CAAC,SAAS,GAAG,IAAI;AACjF,KAAQ,IAAI,IAAM,EAAQ,IAAI,EAAI,IAAI,KAAK,EAAE,OAAO;;EAGtD,IAAM,IADS,CAAC,GAAG,EAAQ,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC,CACtD,KAAK,GAAG,OAAO,EAAE,EACjC,IAAO,EAAO,EAAO,SAAS,MAAM,GACpC,IAAO,EAAO,EAAO,SAAS,MAAM;AAE1C,SAAO;GAAE;GAAQ,UADA,IAAO,IAAI,KAAK,OAAQ,IAAO,KAAQ,IAAQ,IAAI,GAAG;GAC5C;IAC1B,CAAC,EAAQ,KAAK,CAAC,EAEZ,IAAW,GACd,MAAuB;AAEtB,EADA,EAAE,gBAAgB,EAClB,EAAkB,EAAa,MAAM,CAAC;IAExC,CAAC,EAAa,CACf,EAEK,IAAe,QAAkB;AAErC,EADA,EAAgB,GAAG,EACnB,EAAkB,GAAG;IACpB,EAAE,CAAC,EAEA,IAAwD;EAC5D;GACE,KAAK;GACL,QAAQ,EAAE,8BAA8B,UAAU;GAClD,UAAU;GACV,OAAO,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAiC,EAAE;IAAe,CAAA;GAChF;EACD;GACE,KAAK;GACL,QAAQ,EAAE,6BAA6B,SAAS;GAChD,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAkC,EAAY,EAAE,QAAQ,EAAE,SAAS;IAAQ,CAAA;GAE9F;EACD;GACE,KAAK;GACL,QAAQ,EAAE,6BAA6B,SAAS;GAChD,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACG,EAAW,EAAE,aAAa;MAAE,MAAM,KAAA;MAAW,QAAQ,KAAA;MAAW,CAAC;KAAC;KAAG;KACrE,EAAW,EAAE,WAAW;MAAE,MAAM,KAAA;MAAW,QAAQ,KAAA;MAAW,CAAC;KAC3D;;GAEV;EACD;GACE,KAAK;GACL,QAAQ,EAAE,+BAA+B,WAAW;GACpD,UAAU;GACV,OAAO,MACL,EAAE,UAAU,OACV,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAE,cAAc,EAAW,cAAc,EAAE,aAAa;IACvE,WAAU;cAET,EAAE,SAAS;IACL,CAAA,GACP,EAAE,aACJ,kBAAC,QAAD;IAAM,WAAU;cAAhB,CAAyD,EAAE,WAAW,MAAM,GAAG,GAAG,EAAC,IAAQ;QAE3F,kBAAC,QAAD;IAAM,WAAU;cAAqB;IAAQ,CAAA;GAElD;EACD;GACE,KAAK;GACL,QAAQ,EAAE,iCAAiC,aAAa;GACxD,UAAU;GACV,OAAO,MACL,EAAE,YAAY,OACZ,kBAAC,QAAD;IAAM,WAAU;cAAoD,EAAE,WAAW;IAAY,CAAA,GAE7F,kBAAC,QAAD;IAAM,WAAU;cAAqB;IAAQ,CAAA;GAElD;EACF;AAED,QACE,kBAAC,GAAD;EACE,OAAO,EAAE,4BAA4B,SAAS;EAC9C,aAAa,EACX,+BACA,0FACD;YALH,CAOE,kBAAC,GAAD;GACE,WAAW,EAAQ;GACnB,SAAS,EAAQ;GACjB,OAAO,EAAQ;GACf,eAAe,KAAK,EAAQ,SAAS;aAJvC,CAOE,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,8BAA8B,eAAe;MAC7C,CAAA;KACD,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,UAAA;MACA,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;MACxC,OAAO,EAAQ,OAAO,EAAY,EAAQ,KAAK,OAAO,EAAS,GAAG;MAClE,OAAO,EAAE,iCAAiC,cAAc;MACxD,CAAA,EACD,EAAY,MAAM,GAAG,EAAE,CAAC,KAAK,MAC5B,kBAAC,GAAD;MAEE,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,OAAO,EAAY,EAAK,OAAO,EAAS;MACxC,OAAO,EAAK;MACZ,EAJK,EAAK,QAIV,CACF,CACE;OACQ;OAGf,EAAY,SAAS,KACpB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,WAAD;KAAS,WAAU;eAAnB,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,gCAAgC,mBAAmB;MACnD,CAAA,EACL,kBAAC,GAAD;MAAW,WAAU;gBACnB,kBAAC,GAAD;OAAU,MAAM;OAAa,qBAAoB;OAAiB,CAAA;MACxD,CAAA,CACJ;QAEV,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,4BAA4B,gBAAgB;KAC5C,CAAA,EACL,kBAAC,GAAD;KAAW,WAAU;eAClB,EAAM,OAAO,UAAU,IACtB,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAY,EAAM,OAAO,EAAM,OAAO,SAAS,MAAM,GAAG,EAAS;OAC7D,CAAA,EACN,EAAM,aAAa,QAClB,kBAAC,QAAD;OACE,WAAW,sCACT,EAAM,WAAW,IAAI,2BAA2B;iBAFpD;QAKG,EAAM,WAAW,IAAI,MAAM;QAAI;QAAE,KAAK,IAAI,EAAM,SAAS;QAAC;QAAE;QAC5D,EAAE,6BAA6B,UAAU;QACrC;SAEL;SACN,kBAAC,GAAD;MAAW,QAAQ,EAAM;MAAQ,WAAU;MAAsC,CAAA,CAChF,EAAA,CAAA,GAEH,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,iCAAiC,qDAAqD;MACvF,CAAA;KAEI,CAAA,CACJ,EAAA,CAAA,CACN;MAEE;MAGZ,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAE,8BAA8B,eAAe;IAC7C,CAAA,EACL,kBAAC,QAAD;IACE,UAAU;IACV,MAAK;IACL,WAAU;cAHZ,CAKE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,SAAD;MACE,MAAK;MACL,OAAO;MACP,WAAW,MAAM,EAAgB,EAAE,OAAO,MAAM;MAChD,cAAY,EAAE,oCAAoC,oBAAoB;MACtE,aAAa,EAAE,+CAA+C,qBAAqB;MACnF,WAAU;MACV,CAAA,EACD,KACC,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,cAAY,EAAE,yCAAyC,uBAAuB;MAC9E,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA,CAEP;QACN,kBAAC,UAAD;KACE,MAAK;KACL,cAAY,EAAE,yCAAyC,uBAAuB;KAC9E,WAAU;eAEV,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;KACvB,CAAA,CACJ;MACH;MAEN,kBAAC,GAAD;GACE,WAAW,EAAQ;GACnB,SAAS,EAAQ;GACjB,UAAU,EAAQ,QAAQ,EAAE,EAAE,WAAW;GACzC,OAAO,EAAQ;GACf,eAAe,KAAK,EAAQ,SAAS;GACrC,YAAY,EAAE,iCAAiC,kBAAkB;GACjE,kBAAkB,EAChB,uCACA,sEACD;GACD,WAAW,kBAAC,GAAD,EAAS,WAAU,aAAc,CAAA;GAC5C,aACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,eAAe;IACzC,WAAU;cAET,EAAE,sCAAsC,oBAAoB;IACtD,CAAA;aAGX,kBAAC,GAAD;IAAiB,MAAM,EAAQ,QAAQ,EAAE;IAAE,SAAS;IAAe,SAAS,MAAM,EAAE;IAAM,CAAA;GAChF,CAAA,CACJ,EAAA,CAAA,CACC"}