@franco_roura/module-01 0.0.0-stage → 1.0.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/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +64 -2
- package/dist/charts.d.ts +3 -0
- package/dist/charts.js +3 -0
- package/dist/components/alert-dialog.d.ts +19 -0
- package/dist/components/alert-dialog.js +101 -0
- package/dist/components/alert-dialog.js.map +1 -0
- package/dist/components/amount.d.ts +24 -0
- package/dist/components/amount.js +27 -0
- package/dist/components/amount.js.map +1 -0
- package/dist/components/appearance-pickers.d.ts +9 -0
- package/dist/components/appearance-pickers.js +65 -0
- package/dist/components/appearance-pickers.js.map +1 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.js +28 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/budget-meter.d.ts +18 -0
- package/dist/components/budget-meter.js +38 -0
- package/dist/components/budget-meter.js.map +1 -0
- package/dist/components/button.d.ts +13 -0
- package/dist/components/button.js +54 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/calendar.d.ts +7 -0
- package/dist/components/calendar.js +52 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/category-icon.d.ts +33 -0
- package/dist/components/category-icon.js +49 -0
- package/dist/components/category-icon.js.map +1 -0
- package/dist/components/chart.d.ts +44 -0
- package/dist/components/chart.js +147 -0
- package/dist/components/chart.js.map +1 -0
- package/dist/components/charts.d.ts +52 -0
- package/dist/components/charts.js +482 -0
- package/dist/components/charts.js.map +1 -0
- package/dist/components/checkbox.d.ts +5 -0
- package/dist/components/checkbox.js +21 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/combobox.d.ts +32 -0
- package/dist/components/combobox.js +100 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/command.d.ts +19 -0
- package/dist/components/command.js +86 -0
- package/dist/components/command.js.map +1 -0
- package/dist/components/cta-banner.d.ts +14 -0
- package/dist/components/cta-banner.js +39 -0
- package/dist/components/cta-banner.js.map +1 -0
- package/dist/components/date-picker.d.ts +19 -0
- package/dist/components/date-picker.js +107 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dialog.d.ts +18 -0
- package/dist/components/dialog.js +99 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +26 -0
- package/dist/components/dropdown-menu.js +123 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/field.d.ts +30 -0
- package/dist/components/field.js +53 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/footer.d.ts +18 -0
- package/dist/components/footer.js +40 -0
- package/dist/components/footer.js.map +1 -0
- package/dist/components/fx-converter.d.ts +22 -0
- package/dist/components/fx-converter.js +91 -0
- package/dist/components/fx-converter.js.map +1 -0
- package/dist/components/hero.d.ts +28 -0
- package/dist/components/hero.js +83 -0
- package/dist/components/hero.js.map +1 -0
- package/dist/components/hud.d.ts +41 -0
- package/dist/components/hud.js +108 -0
- package/dist/components/hud.js.map +1 -0
- package/dist/components/input.d.ts +4 -0
- package/dist/components/input.js +14 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.js +15 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/media-card.d.ts +29 -0
- package/dist/components/media-card.js +80 -0
- package/dist/components/media-card.js.map +1 -0
- package/dist/components/money-input.d.ts +10 -0
- package/dist/components/money-input.js +30 -0
- package/dist/components/money-input.js.map +1 -0
- package/dist/components/multi-combobox.d.ts +27 -0
- package/dist/components/multi-combobox.js +101 -0
- package/dist/components/multi-combobox.js.map +1 -0
- package/dist/components/nav-bar.d.ts +21 -0
- package/dist/components/nav-bar.js +44 -0
- package/dist/components/nav-bar.js.map +1 -0
- package/dist/components/panel.d.ts +27 -0
- package/dist/components/panel.js +49 -0
- package/dist/components/panel.js.map +1 -0
- package/dist/components/popover.d.ts +11 -0
- package/dist/components/popover.js +56 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/select.d.ts +16 -0
- package/dist/components/select.js +128 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/separator.d.ts +5 -0
- package/dist/components/separator.js +17 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +15 -0
- package/dist/components/sheet.js +87 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/skeleton.d.ts +3 -0
- package/dist/components/skeleton.js +13 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/sonner.d.ts +5 -0
- package/dist/components/sonner.js +44 -0
- package/dist/components/sonner.js.map +1 -0
- package/dist/components/spec-readout.d.ts +11 -0
- package/dist/components/spec-readout.js +25 -0
- package/dist/components/spec-readout.js.map +1 -0
- package/dist/components/split-flap.d.ts +12 -0
- package/dist/components/split-flap.js +77 -0
- package/dist/components/split-flap.js.map +1 -0
- package/dist/components/tabs.d.ts +12 -0
- package/dist/components/tabs.js +47 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/tag.d.ts +16 -0
- package/dist/components/tag.js +26 -0
- package/dist/components/tag.js.map +1 -0
- package/dist/components/terminal.d.ts +10 -0
- package/dist/components/terminal.js +20 -0
- package/dist/components/terminal.js.map +1 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.js +13 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/timeline-entry.d.ts +26 -0
- package/dist/components/timeline-entry.js +63 -0
- package/dist/components/timeline-entry.js.map +1 -0
- package/dist/components/toggle-group.d.ts +10 -0
- package/dist/components/toggle-group.js +49 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/toggle.d.ts +10 -0
- package/dist/components/toggle.js +37 -0
- package/dist/components/toggle.js.map +1 -0
- package/dist/components/tooltip.d.ts +8 -0
- package/dist/components/tooltip.js +36 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/finance.d.ts +10 -0
- package/dist/finance.js +10 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/lib/category-icons.d.ts +80 -0
- package/dist/lib/category-icons.js +102 -0
- package/dist/lib/category-icons.js.map +1 -0
- package/dist/lib/fault.d.ts +7 -0
- package/dist/lib/fault.js +28 -0
- package/dist/lib/fault.js.map +1 -0
- package/dist/lib/form-flow.d.ts +15 -0
- package/dist/lib/form-flow.js +50 -0
- package/dist/lib/form-flow.js.map +1 -0
- package/dist/lib/fx.d.ts +39 -0
- package/dist/lib/fx.js +98 -0
- package/dist/lib/fx.js.map +1 -0
- package/dist/lib/money.d.ts +19 -0
- package/dist/lib/money.js +55 -0
- package/dist/lib/money.js.map +1 -0
- package/dist/lib/use-media-query.d.ts +3 -0
- package/dist/lib/use-media-query.js +15 -0
- package/dist/lib/use-media-query.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +26 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/site.d.ts +10 -0
- package/dist/site.js +10 -0
- package/dist/styles.css +2 -0
- package/dist/theme.css +391 -0
- package/dist/tokens.css +181 -0
- package/guidelines/AlertDialog.md +9 -0
- package/guidelines/Amount.md +10 -0
- package/guidelines/AppearancePickers.md +8 -0
- package/guidelines/Badge.md +11 -0
- package/guidelines/BudgetMeter.md +11 -0
- package/guidelines/Button.md +16 -0
- package/guidelines/Calendar.md +8 -0
- package/guidelines/CategoryIcon.md +10 -0
- package/guidelines/Charts.md +12 -0
- package/guidelines/Checkbox.md +8 -0
- package/guidelines/Combobox.md +18 -0
- package/guidelines/Command.md +8 -0
- package/guidelines/Crosshair.md +7 -0
- package/guidelines/CtaBanner.md +8 -0
- package/guidelines/DatePicker.md +11 -0
- package/guidelines/Dialog.md +9 -0
- package/guidelines/DropdownMenu.md +9 -0
- package/guidelines/Field.md +22 -0
- package/guidelines/Footer.md +7 -0
- package/guidelines/FxConverter.md +12 -0
- package/guidelines/GlassPanel.md +8 -0
- package/guidelines/Grill.md +7 -0
- package/guidelines/Hero.md +9 -0
- package/guidelines/HudLabel.md +8 -0
- package/guidelines/Input.md +9 -0
- package/guidelines/Label.md +7 -0
- package/guidelines/MediaCard.md +8 -0
- package/guidelines/MoneyInput.md +10 -0
- package/guidelines/MultiCombobox.md +11 -0
- package/guidelines/NavBar.md +10 -0
- package/guidelines/Panel.md +10 -0
- package/guidelines/Popover.md +8 -0
- package/guidelines/README.md +100 -0
- package/guidelines/SectionHeader.md +9 -0
- package/guidelines/Select.md +9 -0
- package/guidelines/Separator.md +8 -0
- package/guidelines/Sheet.md +9 -0
- package/guidelines/Skeleton.md +8 -0
- package/guidelines/SpecReadout.md +7 -0
- package/guidelines/SplitFlap.md +9 -0
- package/guidelines/StatusLed.md +8 -0
- package/guidelines/Tabs.md +9 -0
- package/guidelines/Tag.md +9 -0
- package/guidelines/Terminal.md +7 -0
- package/guidelines/Textarea.md +8 -0
- package/guidelines/TickRuler.md +8 -0
- package/guidelines/TimelineEntry.md +8 -0
- package/guidelines/Toaster.md +9 -0
- package/guidelines/Toggle.md +9 -0
- package/guidelines/Tooltip.md +8 -0
- package/package.json +103 -4
- package/tokens/tokens.json +739 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { categoryColorVar } from "../lib/category-icons.js";
|
|
3
|
+
import { formatMoney } from "../lib/money.js";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { CategoryIcon } from "./category-icon.js";
|
|
6
|
+
import { ChartContainer, ChartTooltip } from "./chart.js";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, ReferenceLine, XAxis, YAxis } from "recharts";
|
|
10
|
+
// src/components/charts.tsx
|
|
11
|
+
const toNumber = (minor, units) => Number(minor) / 10 ** units;
|
|
12
|
+
const axisProps = {
|
|
13
|
+
tickLine: false,
|
|
14
|
+
axisLine: false,
|
|
15
|
+
tick: {
|
|
16
|
+
fill: "var(--color-ink-soft)",
|
|
17
|
+
fontSize: 11,
|
|
18
|
+
fontFamily: "var(--font-mono)"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const compactFormat = new Intl.NumberFormat("en-US", {
|
|
22
|
+
notation: "compact",
|
|
23
|
+
maximumSignificantDigits: 4
|
|
24
|
+
});
|
|
25
|
+
function compact(n) {
|
|
26
|
+
return compactFormat.format(n);
|
|
27
|
+
}
|
|
28
|
+
function TooltipCard({ title, rows }) {
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: "min-w-44 rounded-md border border-seam bg-shell px-3 py-2 shadow-float",
|
|
31
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
32
|
+
className: "hud-sm mb-1.5 text-ink-soft",
|
|
33
|
+
children: title
|
|
34
|
+
}), rows.map((r) => /* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className: "flex items-center justify-between gap-4 text-body-sm",
|
|
36
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
37
|
+
className: "flex items-center gap-1.5 text-ink-soft",
|
|
38
|
+
children: [r.swatch && /* @__PURE__ */ jsx("span", {
|
|
39
|
+
className: "size-2 rounded-full",
|
|
40
|
+
style: { background: r.swatch }
|
|
41
|
+
}), r.label]
|
|
42
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
43
|
+
className: "tabular text-ink",
|
|
44
|
+
children: r.value
|
|
45
|
+
})]
|
|
46
|
+
}, r.label))]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** One series over time: the balance line with a soft area and a crosshair tooltip. */
|
|
50
|
+
function BalanceChart({ points, money, className }) {
|
|
51
|
+
const data = points.map((p) => ({
|
|
52
|
+
...p,
|
|
53
|
+
value: toNumber(p.balance, money.minorUnits)
|
|
54
|
+
}));
|
|
55
|
+
return /* @__PURE__ */ jsx(ChartContainer, {
|
|
56
|
+
config: { value: {
|
|
57
|
+
label: "Balance",
|
|
58
|
+
color: "var(--color-chart-line)"
|
|
59
|
+
} },
|
|
60
|
+
className: cn("aspect-auto h-56 w-full", className),
|
|
61
|
+
children: /* @__PURE__ */ jsxs(AreaChart, {
|
|
62
|
+
data,
|
|
63
|
+
margin: {
|
|
64
|
+
left: 4,
|
|
65
|
+
right: 20,
|
|
66
|
+
top: 8,
|
|
67
|
+
bottom: 0
|
|
68
|
+
},
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
71
|
+
vertical: false,
|
|
72
|
+
stroke: "var(--color-seam)",
|
|
73
|
+
strokeDasharray: "2 4"
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
76
|
+
dataKey: "label",
|
|
77
|
+
...axisProps,
|
|
78
|
+
minTickGap: 24
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
81
|
+
...axisProps,
|
|
82
|
+
width: 48,
|
|
83
|
+
tickFormatter: compact,
|
|
84
|
+
domain: ["auto", "auto"]
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ jsx(ChartTooltip, {
|
|
87
|
+
cursor: {
|
|
88
|
+
stroke: "var(--color-hud-mark)",
|
|
89
|
+
strokeWidth: 1
|
|
90
|
+
},
|
|
91
|
+
content: ({ active, payload }) => {
|
|
92
|
+
const p = active && payload?.[0]?.payload;
|
|
93
|
+
if (!p) return null;
|
|
94
|
+
return /* @__PURE__ */ jsx(TooltipCard, {
|
|
95
|
+
title: p.tooltipLabel,
|
|
96
|
+
rows: [{
|
|
97
|
+
label: "Balance",
|
|
98
|
+
value: formatMoney(p.balance, money.currency, money.minorUnits, { sign: "always" })
|
|
99
|
+
}]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ jsx(Area, {
|
|
104
|
+
dataKey: "value",
|
|
105
|
+
type: "linear",
|
|
106
|
+
stroke: "var(--color-chart-line)",
|
|
107
|
+
strokeWidth: 2,
|
|
108
|
+
fill: "var(--color-signal-tint)",
|
|
109
|
+
fillOpacity: .6,
|
|
110
|
+
dot: false,
|
|
111
|
+
activeDot: {
|
|
112
|
+
r: 4,
|
|
113
|
+
fill: "var(--color-chart-line)",
|
|
114
|
+
stroke: "var(--color-shell)",
|
|
115
|
+
strokeWidth: 2
|
|
116
|
+
},
|
|
117
|
+
isAnimationActive: false
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Income above the baseline, expenses below it, per bucket. With `only`, a single series (no
|
|
125
|
+
* legend: the panel title names it).
|
|
126
|
+
*/
|
|
127
|
+
function FlowChart({ points, money, only, className }) {
|
|
128
|
+
const showIncome = only !== "expense";
|
|
129
|
+
const showExpense = only !== "income";
|
|
130
|
+
const data = points.map((p) => ({
|
|
131
|
+
...p,
|
|
132
|
+
incomeValue: toNumber(p.income, money.minorUnits),
|
|
133
|
+
expenseValue: -toNumber(p.expense, money.minorUnits)
|
|
134
|
+
}));
|
|
135
|
+
const config = {
|
|
136
|
+
incomeValue: {
|
|
137
|
+
label: "Income",
|
|
138
|
+
color: "var(--color-chart-income)"
|
|
139
|
+
},
|
|
140
|
+
expenseValue: {
|
|
141
|
+
label: "Expenses",
|
|
142
|
+
color: "var(--color-chart-expense)"
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
146
|
+
className: cn("flex flex-col gap-2", className),
|
|
147
|
+
children: [/* @__PURE__ */ jsx(ChartContainer, {
|
|
148
|
+
config,
|
|
149
|
+
className: "aspect-auto h-56 w-full",
|
|
150
|
+
children: /* @__PURE__ */ jsxs(BarChart, {
|
|
151
|
+
data,
|
|
152
|
+
stackOffset: "sign",
|
|
153
|
+
margin: {
|
|
154
|
+
left: 4,
|
|
155
|
+
right: 20,
|
|
156
|
+
top: 8,
|
|
157
|
+
bottom: 0
|
|
158
|
+
},
|
|
159
|
+
barCategoryGap: "20%",
|
|
160
|
+
children: [
|
|
161
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
162
|
+
vertical: false,
|
|
163
|
+
stroke: "var(--color-seam)",
|
|
164
|
+
strokeDasharray: "2 4"
|
|
165
|
+
}),
|
|
166
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
167
|
+
dataKey: "label",
|
|
168
|
+
...axisProps,
|
|
169
|
+
minTickGap: 24
|
|
170
|
+
}),
|
|
171
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
172
|
+
...axisProps,
|
|
173
|
+
width: 48,
|
|
174
|
+
tickFormatter: (v) => compact(Math.abs(v))
|
|
175
|
+
}),
|
|
176
|
+
/* @__PURE__ */ jsx(ReferenceLine, {
|
|
177
|
+
y: 0,
|
|
178
|
+
stroke: "var(--color-hud-mark)"
|
|
179
|
+
}),
|
|
180
|
+
/* @__PURE__ */ jsx(ChartTooltip, {
|
|
181
|
+
cursor: {
|
|
182
|
+
fill: "var(--color-shell-recess)",
|
|
183
|
+
opacity: .6
|
|
184
|
+
},
|
|
185
|
+
content: ({ active, payload }) => {
|
|
186
|
+
const p = active && payload?.[0]?.payload;
|
|
187
|
+
if (!p) return null;
|
|
188
|
+
return /* @__PURE__ */ jsx(TooltipCard, {
|
|
189
|
+
title: p.tooltipLabel,
|
|
190
|
+
rows: [...showIncome ? [{
|
|
191
|
+
label: "Income",
|
|
192
|
+
swatch: "var(--color-chart-income)",
|
|
193
|
+
value: formatMoney(p.income, money.currency, money.minorUnits, { sign: "always" })
|
|
194
|
+
}] : [], ...showExpense ? [{
|
|
195
|
+
label: "Expenses",
|
|
196
|
+
swatch: "var(--color-chart-expense)",
|
|
197
|
+
value: formatMoney(0n - p.expense, money.currency, money.minorUnits)
|
|
198
|
+
}] : []]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}),
|
|
202
|
+
showIncome && /* @__PURE__ */ jsx(Bar, {
|
|
203
|
+
dataKey: "incomeValue",
|
|
204
|
+
stackId: "flow",
|
|
205
|
+
fill: "var(--color-chart-income)",
|
|
206
|
+
radius: [
|
|
207
|
+
4,
|
|
208
|
+
4,
|
|
209
|
+
0,
|
|
210
|
+
0
|
|
211
|
+
],
|
|
212
|
+
isAnimationActive: false
|
|
213
|
+
}),
|
|
214
|
+
showExpense && /* @__PURE__ */ jsx(Bar, {
|
|
215
|
+
dataKey: "expenseValue",
|
|
216
|
+
stackId: "flow",
|
|
217
|
+
fill: "var(--color-chart-expense)",
|
|
218
|
+
radius: [
|
|
219
|
+
0,
|
|
220
|
+
0,
|
|
221
|
+
4,
|
|
222
|
+
4
|
|
223
|
+
],
|
|
224
|
+
isAnimationActive: false
|
|
225
|
+
})
|
|
226
|
+
]
|
|
227
|
+
})
|
|
228
|
+
}), !only && /* @__PURE__ */ jsx("div", {
|
|
229
|
+
className: "flex gap-4 pl-14",
|
|
230
|
+
children: ["incomeValue", "expenseValue"].map((k) => /* @__PURE__ */ jsxs("span", {
|
|
231
|
+
className: "hud-sm flex items-center gap-1.5 text-ink-soft",
|
|
232
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
233
|
+
className: "size-2 rounded-xs",
|
|
234
|
+
style: { background: config[k].color }
|
|
235
|
+
}), config[k].label]
|
|
236
|
+
}, k))
|
|
237
|
+
})]
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const callout = {
|
|
241
|
+
leader: 12,
|
|
242
|
+
icon: 28,
|
|
243
|
+
dot: 12,
|
|
244
|
+
gap: 5,
|
|
245
|
+
charWidth: 7.8,
|
|
246
|
+
textHeight: 12,
|
|
247
|
+
minShare: 50n
|
|
248
|
+
};
|
|
249
|
+
const donutMaxRadius = 112;
|
|
250
|
+
const donutMinRadius = 56;
|
|
251
|
+
const TAU = 2 * Math.PI;
|
|
252
|
+
/** Angles run clockwise from 12 o'clock, like the donut's slices. */
|
|
253
|
+
function polar(cx, cy, r, angle) {
|
|
254
|
+
return {
|
|
255
|
+
x: cx + r * Math.sin(angle),
|
|
256
|
+
y: cy - r * Math.cos(angle)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** Pushes sorted angles apart, the least it can, until neighbours are at least `min` apart. */
|
|
260
|
+
function spreadAngles(angles, min) {
|
|
261
|
+
const p = [...angles];
|
|
262
|
+
const n = p.length;
|
|
263
|
+
if (n < 2) return p;
|
|
264
|
+
const step = Math.min(min, TAU / n);
|
|
265
|
+
for (let round = 0; round < 200; round++) {
|
|
266
|
+
let moved = false;
|
|
267
|
+
for (let i = 0; i < n; i++) {
|
|
268
|
+
const j = (i + 1) % n;
|
|
269
|
+
const a = p[i] ?? 0;
|
|
270
|
+
const b = (p[j] ?? 0) + (j === 0 ? TAU : 0);
|
|
271
|
+
if (b - a < step - 1e-6) {
|
|
272
|
+
const push = (step - (b - a)) / 2;
|
|
273
|
+
p[i] = a - push;
|
|
274
|
+
p[j] = (p[j] ?? 0) + push;
|
|
275
|
+
moved = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!moved) break;
|
|
279
|
+
}
|
|
280
|
+
return p;
|
|
281
|
+
}
|
|
282
|
+
function useWidth() {
|
|
283
|
+
const ref = useRef(null);
|
|
284
|
+
const [width, setWidth] = useState(0);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
const el = ref.current;
|
|
287
|
+
if (!el) return;
|
|
288
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
289
|
+
if (entry) setWidth(Math.floor(entry.contentRect.width));
|
|
290
|
+
});
|
|
291
|
+
observer.observe(el);
|
|
292
|
+
return () => observer.disconnect();
|
|
293
|
+
}, []);
|
|
294
|
+
return [ref, width];
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Shares of a total, clockwise from 12 o'clock. Up to 7 slices plus "Other". Each slice of 2% or
|
|
298
|
+
* more gets a callout outside the ring (its icon and share), so it reads without the legend; the
|
|
299
|
+
* caller still shows the ranked list beside it.
|
|
300
|
+
*/
|
|
301
|
+
function DonutChart({ slices, money, centerLabel, className }) {
|
|
302
|
+
const [ref, width] = useWidth();
|
|
303
|
+
const sorted = [...slices].sort((a, b) => b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0);
|
|
304
|
+
const withIcons = slices.some((s) => s.icon);
|
|
305
|
+
const top = sorted.slice(0, 7);
|
|
306
|
+
const rest = sorted.slice(7).reduce((s, x) => s + x.amount, 0n);
|
|
307
|
+
if (rest > 0n) top.push({
|
|
308
|
+
key: "other",
|
|
309
|
+
label: "Other",
|
|
310
|
+
color: "cat-slate",
|
|
311
|
+
amount: rest,
|
|
312
|
+
icon: withIcons ? "ellipsis" : void 0
|
|
313
|
+
});
|
|
314
|
+
const total = sorted.reduce((s, x) => s + x.amount, 0n);
|
|
315
|
+
const data = top.map((s) => ({
|
|
316
|
+
...s,
|
|
317
|
+
value: toNumber(s.amount, money.minorUnits)
|
|
318
|
+
}));
|
|
319
|
+
const sum = data.reduce((s, d) => s + d.value, 0);
|
|
320
|
+
let acc = 0;
|
|
321
|
+
const labelled = data.map((d) => {
|
|
322
|
+
const from = sum > 0 ? acc / sum * TAU : 0;
|
|
323
|
+
acc += d.value;
|
|
324
|
+
const share = total > 0n ? Number(d.amount * 1000n / total) / 10 : 0;
|
|
325
|
+
return {
|
|
326
|
+
...d,
|
|
327
|
+
from,
|
|
328
|
+
to: sum > 0 ? acc / sum * TAU : 0,
|
|
329
|
+
share: `${share.toFixed(1)}%`
|
|
330
|
+
};
|
|
331
|
+
}).filter((a) => a.amount * callout.minShare >= total && total > 0n);
|
|
332
|
+
const marker = withIcons ? callout.icon : callout.dot;
|
|
333
|
+
const textWidth = Math.max(0, ...labelled.map((a) => a.share.length)) * callout.charWidth;
|
|
334
|
+
const withShares = callout.leader + marker + callout.gap + textWidth;
|
|
335
|
+
const showShares = width === 0 || width / 2 - withShares >= donutMinRadius;
|
|
336
|
+
const marginX = showShares ? withShares : callout.leader + marker;
|
|
337
|
+
const marginY = callout.leader + marker + (showShares ? callout.gap + callout.textHeight : 0);
|
|
338
|
+
const radius = width === 0 ? donutMaxRadius : Math.max(32, Math.min(donutMaxRadius, width / 2 - marginX));
|
|
339
|
+
const height = Math.round(2 * (radius + marginY));
|
|
340
|
+
const cx = width / 2;
|
|
341
|
+
const cy = height / 2;
|
|
342
|
+
const inner = radius * .62;
|
|
343
|
+
const orbit = radius + callout.leader + marker / 2;
|
|
344
|
+
const clearance = Math.max(marker, showShares ? textWidth : 0) + 6;
|
|
345
|
+
const angles = spreadAngles(labelled.map((a) => (a.from + a.to) / 2), 2 * Math.asin(Math.min(1, clearance / 2 / orbit)));
|
|
346
|
+
const callouts = labelled.map((a, i) => {
|
|
347
|
+
const angle = angles[i] ?? 0;
|
|
348
|
+
const pad = Math.min(.05, (a.to - a.from) / 4);
|
|
349
|
+
const start = polar(cx, cy, radius + 3, Math.min(a.to - pad, Math.max(a.from + pad, angle)));
|
|
350
|
+
const center = polar(cx, cy, orbit, angle);
|
|
351
|
+
const dx = start.x - center.x;
|
|
352
|
+
const dy = start.y - center.y;
|
|
353
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
354
|
+
const end = {
|
|
355
|
+
x: center.x + dx / len * (marker / 2 + 1),
|
|
356
|
+
y: center.y + dy / len * (marker / 2 + 1)
|
|
357
|
+
};
|
|
358
|
+
const text = polar(cx, cy, orbit + marker / 2 + callout.gap, angle);
|
|
359
|
+
const sx = Math.sin(angle);
|
|
360
|
+
const sy = -Math.cos(angle);
|
|
361
|
+
const tx = sx > .38 ? "0" : sx < -.38 ? "-100%" : "-50%";
|
|
362
|
+
const ty = sy > .38 ? "0" : sy < -.38 ? "-100%" : "-50%";
|
|
363
|
+
return {
|
|
364
|
+
...a,
|
|
365
|
+
start,
|
|
366
|
+
end,
|
|
367
|
+
center,
|
|
368
|
+
text,
|
|
369
|
+
align: `translate(${tx}, ${ty})`
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
const totalText = formatMoney(total, money.currency, money.minorUnits, { code: false });
|
|
373
|
+
const totalSize = Math.max(10, Math.min(15, Math.floor(inner * 2 * .84 / (totalText.length * .62))));
|
|
374
|
+
return /* @__PURE__ */ jsx("div", {
|
|
375
|
+
ref,
|
|
376
|
+
className: cn("relative w-full [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden", className),
|
|
377
|
+
style: { height },
|
|
378
|
+
children: width > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
379
|
+
"aria-hidden": true,
|
|
380
|
+
className: "pointer-events-none absolute inset-0",
|
|
381
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
382
|
+
"aria-hidden": true,
|
|
383
|
+
width,
|
|
384
|
+
height,
|
|
385
|
+
className: "absolute inset-0",
|
|
386
|
+
children: [
|
|
387
|
+
callouts.map((c) => /* @__PURE__ */ jsx("line", {
|
|
388
|
+
x1: c.start.x,
|
|
389
|
+
y1: c.start.y,
|
|
390
|
+
x2: c.end.x,
|
|
391
|
+
y2: c.end.y,
|
|
392
|
+
stroke: categoryColorVar(c.color),
|
|
393
|
+
strokeWidth: 1.5,
|
|
394
|
+
strokeLinecap: "round"
|
|
395
|
+
}, c.key)),
|
|
396
|
+
/* @__PURE__ */ jsx("text", {
|
|
397
|
+
x: cx,
|
|
398
|
+
y: cy - 6,
|
|
399
|
+
textAnchor: "middle",
|
|
400
|
+
className: "hud-sm",
|
|
401
|
+
fill: "var(--color-ink-soft)",
|
|
402
|
+
children: centerLabel
|
|
403
|
+
}),
|
|
404
|
+
/* @__PURE__ */ jsx("text", {
|
|
405
|
+
x: cx,
|
|
406
|
+
y: cy + 14,
|
|
407
|
+
textAnchor: "middle",
|
|
408
|
+
className: "tabular",
|
|
409
|
+
fontSize: totalSize,
|
|
410
|
+
fontWeight: 700,
|
|
411
|
+
fill: "var(--color-ink)",
|
|
412
|
+
children: totalText
|
|
413
|
+
})
|
|
414
|
+
]
|
|
415
|
+
}), callouts.map((c) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
416
|
+
className: "absolute -translate-x-1/2 -translate-y-1/2",
|
|
417
|
+
style: {
|
|
418
|
+
left: c.center.x,
|
|
419
|
+
top: c.center.y
|
|
420
|
+
},
|
|
421
|
+
children: withIcons ? /* @__PURE__ */ jsx(CategoryIcon, {
|
|
422
|
+
icon: c.icon ?? "circle-help",
|
|
423
|
+
color: c.color,
|
|
424
|
+
size: "sm",
|
|
425
|
+
className: "flex"
|
|
426
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
427
|
+
className: "block size-3 rounded-full",
|
|
428
|
+
style: { background: categoryColorVar(c.color) }
|
|
429
|
+
})
|
|
430
|
+
}), showShares && /* @__PURE__ */ jsx("span", {
|
|
431
|
+
className: "hud-sm absolute whitespace-nowrap text-ink",
|
|
432
|
+
style: {
|
|
433
|
+
left: c.text.x,
|
|
434
|
+
top: c.text.y,
|
|
435
|
+
transform: c.align
|
|
436
|
+
},
|
|
437
|
+
children: c.share
|
|
438
|
+
})] }, c.key))]
|
|
439
|
+
}), /* @__PURE__ */ jsxs(PieChart, {
|
|
440
|
+
width,
|
|
441
|
+
height,
|
|
442
|
+
margin: {
|
|
443
|
+
top: 0,
|
|
444
|
+
right: 0,
|
|
445
|
+
bottom: 0,
|
|
446
|
+
left: 0
|
|
447
|
+
},
|
|
448
|
+
children: [/* @__PURE__ */ jsx(ChartTooltip, { content: ({ active, payload }) => {
|
|
449
|
+
const p = active && payload?.[0]?.payload;
|
|
450
|
+
if (!p) return null;
|
|
451
|
+
const share = total > 0n ? Number(p.amount * 1000n / total) / 10 : 0;
|
|
452
|
+
return /* @__PURE__ */ jsx(TooltipCard, {
|
|
453
|
+
title: p.label,
|
|
454
|
+
rows: [{
|
|
455
|
+
label: "Amount",
|
|
456
|
+
value: formatMoney(p.amount, money.currency, money.minorUnits)
|
|
457
|
+
}, {
|
|
458
|
+
label: "Share",
|
|
459
|
+
value: `${share.toFixed(1)}%`
|
|
460
|
+
}]
|
|
461
|
+
});
|
|
462
|
+
} }), /* @__PURE__ */ jsx(Pie, {
|
|
463
|
+
data,
|
|
464
|
+
dataKey: "value",
|
|
465
|
+
nameKey: "label",
|
|
466
|
+
cx,
|
|
467
|
+
cy,
|
|
468
|
+
innerRadius: inner,
|
|
469
|
+
outerRadius: radius,
|
|
470
|
+
startAngle: 90,
|
|
471
|
+
endAngle: -270,
|
|
472
|
+
stroke: data.length > 1 ? "var(--color-shell)" : "none",
|
|
473
|
+
strokeWidth: 2,
|
|
474
|
+
isAnimationActive: false,
|
|
475
|
+
children: data.map((s) => /* @__PURE__ */ jsx(Cell, { fill: categoryColorVar(s.color) }, s.key))
|
|
476
|
+
})]
|
|
477
|
+
})] })
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
export { BalanceChart, DonutChart, FlowChart };
|
|
481
|
+
|
|
482
|
+
//# sourceMappingURL=charts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"charts.js","names":[],"sources":["../../src/components/charts.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoryColorVar } from \"../lib/category-icons\";\nimport { formatMoney } from \"../lib/money\";\nimport { cn } from \"../lib/utils\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Cell,\n Pie,\n PieChart,\n ReferenceLine,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { CategoryIcon } from \"./category-icon\";\nimport { type ChartConfig, ChartContainer, ChartTooltip } from \"./chart\";\n\n// Module/01 charts. Marks are thin, axes and grid recede (seam / hud-mark), text uses ink tokens,\n// and series colors come from tokens: chart-line (single series), chart-income / chart-expense\n// (paired with position above/below the baseline), category palette for categories.\n\ntype Money = { currency: string; minorUnits: number };\n\nconst toNumber = (minor: bigint, units: number) => Number(minor) / 10 ** units;\n\nconst axisProps = {\n tickLine: false,\n axisLine: false,\n tick: { fill: \"var(--color-ink-soft)\", fontSize: 11, fontFamily: \"var(--font-mono)\" },\n} as const;\n\n// Enough digits that neighbouring ticks never read the same (11.38K, 11.42K).\nconst compactFormat = new Intl.NumberFormat(\"en-US\", {\n notation: \"compact\",\n maximumSignificantDigits: 4,\n});\n\nfunction compact(n: number) {\n return compactFormat.format(n);\n}\n\nfunction TooltipCard({\n title,\n rows,\n}: {\n title: string;\n rows: { label: string; value: string; swatch?: string }[];\n}) {\n return (\n <div className=\"min-w-44 rounded-md border border-seam bg-shell px-3 py-2 shadow-float\">\n <div className=\"hud-sm mb-1.5 text-ink-soft\">{title}</div>\n {rows.map((r) => (\n <div key={r.label} className=\"flex items-center justify-between gap-4 text-body-sm\">\n <span className=\"flex items-center gap-1.5 text-ink-soft\">\n {r.swatch && <span className=\"size-2 rounded-full\" style={{ background: r.swatch }} />}\n {r.label}\n </span>\n <span className=\"tabular text-ink\">{r.value}</span>\n </div>\n ))}\n </div>\n );\n}\n\nexport type BalancePoint = { label: string; tooltipLabel: string; balance: bigint };\n\n/** One series over time: the balance line with a soft area and a crosshair tooltip. */\nexport function BalanceChart({\n points,\n money,\n className,\n}: {\n points: BalancePoint[];\n money: Money;\n className?: string;\n}) {\n const data = points.map((p) => ({ ...p, value: toNumber(p.balance, money.minorUnits) }));\n const config = {\n value: { label: \"Balance\", color: \"var(--color-chart-line)\" },\n } satisfies ChartConfig;\n return (\n <ChartContainer config={config} className={cn(\"aspect-auto h-56 w-full\", className)}>\n <AreaChart data={data} margin={{ left: 4, right: 20, top: 8, bottom: 0 }}>\n <CartesianGrid vertical={false} stroke=\"var(--color-seam)\" strokeDasharray=\"2 4\" />\n <XAxis dataKey=\"label\" {...axisProps} minTickGap={24} />\n <YAxis {...axisProps} width={48} tickFormatter={compact} domain={[\"auto\", \"auto\"]} />\n <ChartTooltip\n cursor={{ stroke: \"var(--color-hud-mark)\", strokeWidth: 1 }}\n content={({ active, payload }) => {\n const p = active && payload?.[0]?.payload;\n if (!p) return null;\n return (\n <TooltipCard\n title={p.tooltipLabel}\n rows={[\n {\n label: \"Balance\",\n value: formatMoney(p.balance, money.currency, money.minorUnits, {\n sign: \"always\",\n }),\n },\n ]}\n />\n );\n }}\n />\n <Area\n dataKey=\"value\"\n type=\"linear\"\n stroke=\"var(--color-chart-line)\"\n strokeWidth={2}\n fill=\"var(--color-signal-tint)\"\n fillOpacity={0.6}\n dot={false}\n activeDot={{\n r: 4,\n fill: \"var(--color-chart-line)\",\n stroke: \"var(--color-shell)\",\n strokeWidth: 2,\n }}\n isAnimationActive={false}\n />\n </AreaChart>\n </ChartContainer>\n );\n}\n\nexport type FlowPoint = { label: string; tooltipLabel: string; income: bigint; expense: bigint };\n\n/**\n * Income above the baseline, expenses below it, per bucket. With `only`, a single series (no\n * legend: the panel title names it).\n */\nexport function FlowChart({\n points,\n money,\n only,\n className,\n}: {\n points: FlowPoint[];\n money: Money;\n only?: \"income\" | \"expense\";\n className?: string;\n}) {\n const showIncome = only !== \"expense\";\n const showExpense = only !== \"income\";\n const data = points.map((p) => ({\n ...p,\n incomeValue: toNumber(p.income, money.minorUnits),\n expenseValue: -toNumber(p.expense, money.minorUnits),\n }));\n const config = {\n incomeValue: { label: \"Income\", color: \"var(--color-chart-income)\" },\n expenseValue: { label: \"Expenses\", color: \"var(--color-chart-expense)\" },\n } satisfies ChartConfig;\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <ChartContainer config={config} className=\"aspect-auto h-56 w-full\">\n <BarChart\n data={data}\n stackOffset=\"sign\"\n margin={{ left: 4, right: 20, top: 8, bottom: 0 }}\n barCategoryGap=\"20%\"\n >\n <CartesianGrid vertical={false} stroke=\"var(--color-seam)\" strokeDasharray=\"2 4\" />\n <XAxis dataKey=\"label\" {...axisProps} minTickGap={24} />\n <YAxis {...axisProps} width={48} tickFormatter={(v: number) => compact(Math.abs(v))} />\n <ReferenceLine y={0} stroke=\"var(--color-hud-mark)\" />\n <ChartTooltip\n cursor={{ fill: \"var(--color-shell-recess)\", opacity: 0.6 }}\n content={({ active, payload }) => {\n const p = active && payload?.[0]?.payload;\n if (!p) return null;\n return (\n <TooltipCard\n title={p.tooltipLabel}\n rows={[\n ...(showIncome\n ? [\n {\n label: \"Income\",\n swatch: \"var(--color-chart-income)\",\n value: formatMoney(p.income, money.currency, money.minorUnits, {\n sign: \"always\",\n }),\n },\n ]\n : []),\n ...(showExpense\n ? [\n {\n label: \"Expenses\",\n swatch: \"var(--color-chart-expense)\",\n value: formatMoney(\n 0n - (p.expense as bigint),\n money.currency,\n money.minorUnits,\n ),\n },\n ]\n : []),\n ]}\n />\n );\n }}\n />\n {showIncome && (\n <Bar\n dataKey=\"incomeValue\"\n stackId=\"flow\"\n fill=\"var(--color-chart-income)\"\n radius={[4, 4, 0, 0]}\n isAnimationActive={false}\n />\n )}\n {showExpense && (\n <Bar\n dataKey=\"expenseValue\"\n stackId=\"flow\"\n fill=\"var(--color-chart-expense)\"\n radius={[0, 0, 4, 4]}\n isAnimationActive={false}\n />\n )}\n </BarChart>\n </ChartContainer>\n {!only && (\n <div className=\"flex gap-4 pl-14\">\n {([\"incomeValue\", \"expenseValue\"] as const).map((k) => (\n <span key={k} className=\"hud-sm flex items-center gap-1.5 text-ink-soft\">\n <span className=\"size-2 rounded-xs\" style={{ background: config[k].color }} />\n {config[k].label}\n </span>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport type DonutSlice = {\n key: string;\n label: string;\n color: string;\n amount: bigint;\n /** Category icon for the slice's callout. Charts without icons get color dots. */\n icon?: string;\n};\n\n// Donut callouts, in px: a leader from the ring to a marker (the category icon, or a dot), then\n// the share past the marker. Slices under 2% get none; the tooltip and the list still name them.\nconst callout = {\n leader: 12,\n icon: 28, // CategoryIcon \"sm\"\n dot: 12,\n gap: 5,\n charWidth: 7.8, // hud-sm: 10px mono plus tracking\n textHeight: 12,\n minShare: 50n, // amount * 50 >= total\n};\nconst donutMaxRadius = 112;\n// Below this the shares drop out first, so the total still fits in the hole.\nconst donutMinRadius = 56;\nconst TAU = 2 * Math.PI;\n\n/** Angles run clockwise from 12 o'clock, like the donut's slices. */\nfunction polar(cx: number, cy: number, r: number, angle: number) {\n return { x: cx + r * Math.sin(angle), y: cy - r * Math.cos(angle) };\n}\n\n/** Pushes sorted angles apart, the least it can, until neighbours are at least `min` apart. */\nfunction spreadAngles(angles: number[], min: number): number[] {\n const p = [...angles];\n const n = p.length;\n if (n < 2) return p;\n const step = Math.min(min, TAU / n);\n for (let round = 0; round < 200; round++) {\n let moved = false;\n for (let i = 0; i < n; i++) {\n const j = (i + 1) % n;\n const a = p[i] ?? 0;\n const b = (p[j] ?? 0) + (j === 0 ? TAU : 0);\n if (b - a < step - 1e-6) {\n const push = (step - (b - a)) / 2;\n p[i] = a - push;\n p[j] = (p[j] ?? 0) + push;\n moved = true;\n }\n }\n if (!moved) break;\n }\n return p;\n}\n\nfunction useWidth() {\n const ref = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState(0);\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setWidth(Math.floor(entry.contentRect.width));\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n return [ref, width] as const;\n}\n\n/**\n * Shares of a total, clockwise from 12 o'clock. Up to 7 slices plus \"Other\". Each slice of 2% or\n * more gets a callout outside the ring (its icon and share), so it reads without the legend; the\n * caller still shows the ranked list beside it.\n */\nexport function DonutChart({\n slices,\n money,\n centerLabel,\n className,\n}: {\n slices: DonutSlice[];\n money: Money;\n centerLabel: string;\n className?: string;\n}) {\n const [ref, width] = useWidth();\n const sorted = [...slices].sort((a, b) =>\n b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0,\n );\n const withIcons = slices.some((s) => s.icon);\n const top = sorted.slice(0, 7);\n const rest = sorted.slice(7).reduce((s, x) => s + x.amount, 0n);\n if (rest > 0n) {\n top.push({\n key: \"other\",\n label: \"Other\",\n color: \"cat-slate\",\n amount: rest,\n icon: withIcons ? \"ellipsis\" : undefined,\n });\n }\n const total = sorted.reduce((s, x) => s + x.amount, 0n);\n const data = top.map((s) => ({ ...s, value: toNumber(s.amount, money.minorUnits) }));\n\n const sum = data.reduce((s, d) => s + d.value, 0);\n let acc = 0;\n const arcs = data.map((d) => {\n const from = sum > 0 ? (acc / sum) * TAU : 0;\n acc += d.value;\n const share = total > 0n ? Number((d.amount * 1000n) / total) / 10 : 0;\n return { ...d, from, to: sum > 0 ? (acc / sum) * TAU : 0, share: `${share.toFixed(1)}%` };\n });\n const labelled = arcs.filter((a) => a.amount * callout.minShare >= total && total > 0n);\n\n // The ring gets the room the callouts leave, up to its maximum size.\n const marker = withIcons ? callout.icon : callout.dot;\n const textWidth = Math.max(0, ...labelled.map((a) => a.share.length)) * callout.charWidth;\n const withShares = callout.leader + marker + callout.gap + textWidth;\n const showShares = width === 0 || width / 2 - withShares >= donutMinRadius;\n const marginX = showShares ? withShares : callout.leader + marker;\n const marginY = callout.leader + marker + (showShares ? callout.gap + callout.textHeight : 0);\n const radius =\n width === 0 ? donutMaxRadius : Math.max(32, Math.min(donutMaxRadius, width / 2 - marginX));\n const height = Math.round(2 * (radius + marginY));\n const cx = width / 2;\n const cy = height / 2;\n const inner = radius * 0.62;\n\n const orbit = radius + callout.leader + marker / 2;\n const clearance = Math.max(marker, showShares ? textWidth : 0) + 6;\n const angles = spreadAngles(\n labelled.map((a) => (a.from + a.to) / 2),\n 2 * Math.asin(Math.min(1, clearance / 2 / orbit)),\n );\n const callouts = labelled.map((a, i) => {\n const angle = angles[i] ?? 0;\n // The leader leaves the ring inside its own slice, as close to the marker as it can.\n const pad = Math.min(0.05, (a.to - a.from) / 4);\n const start = polar(cx, cy, radius + 3, Math.min(a.to - pad, Math.max(a.from + pad, angle)));\n const center = polar(cx, cy, orbit, angle);\n const dx = start.x - center.x;\n const dy = start.y - center.y;\n const len = Math.hypot(dx, dy) || 1;\n const end = {\n x: center.x + (dx / len) * (marker / 2 + 1),\n y: center.y + (dy / len) * (marker / 2 + 1),\n };\n // The share sits past the marker, aligned away from the ring.\n const text = polar(cx, cy, orbit + marker / 2 + callout.gap, angle);\n const sx = Math.sin(angle);\n const sy = -Math.cos(angle);\n const tx = sx > 0.38 ? \"0\" : sx < -0.38 ? \"-100%\" : \"-50%\";\n const ty = sy > 0.38 ? \"0\" : sy < -0.38 ? \"-100%\" : \"-50%\";\n return { ...a, start, end, center, text, align: `translate(${tx}, ${ty})` };\n });\n\n const totalText = formatMoney(total, money.currency, money.minorUnits, { code: false });\n // Shrink the total until it fits the hole (mono digits are about 0.6em wide).\n const totalSize = Math.max(\n 10,\n Math.min(15, Math.floor((inner * 2 * 0.84) / (totalText.length * 0.62))),\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n \"relative w-full [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden\",\n className,\n )}\n style={{ height }}\n >\n {width > 0 && (\n <>\n <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n <svg aria-hidden width={width} height={height} className=\"absolute inset-0\">\n {callouts.map((c) => (\n <line\n key={c.key}\n x1={c.start.x}\n y1={c.start.y}\n x2={c.end.x}\n y2={c.end.y}\n stroke={categoryColorVar(c.color)}\n strokeWidth={1.5}\n strokeLinecap=\"round\"\n />\n ))}\n <text\n x={cx}\n y={cy - 6}\n textAnchor=\"middle\"\n className=\"hud-sm\"\n fill=\"var(--color-ink-soft)\"\n >\n {centerLabel}\n </text>\n <text\n x={cx}\n y={cy + 14}\n textAnchor=\"middle\"\n className=\"tabular\"\n fontSize={totalSize}\n fontWeight={700}\n fill=\"var(--color-ink)\"\n >\n {totalText}\n </text>\n </svg>\n {callouts.map((c) => (\n <div key={c.key}>\n <span\n className=\"absolute -translate-x-1/2 -translate-y-1/2\"\n style={{ left: c.center.x, top: c.center.y }}\n >\n {withIcons ? (\n <CategoryIcon\n icon={c.icon ?? \"circle-help\"}\n color={c.color}\n size=\"sm\"\n className=\"flex\"\n />\n ) : (\n <span\n className=\"block size-3 rounded-full\"\n style={{ background: categoryColorVar(c.color) }}\n />\n )}\n </span>\n {showShares && (\n <span\n className=\"hud-sm absolute whitespace-nowrap text-ink\"\n style={{ left: c.text.x, top: c.text.y, transform: c.align }}\n >\n {c.share}\n </span>\n )}\n </div>\n ))}\n </div>\n <PieChart width={width} height={height} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>\n <ChartTooltip\n content={({ active, payload }) => {\n const p = active && payload?.[0]?.payload;\n if (!p) return null;\n const share = total > 0n ? Number((p.amount * 1000n) / total) / 10 : 0;\n return (\n <TooltipCard\n title={p.label}\n rows={[\n {\n label: \"Amount\",\n value: formatMoney(p.amount, money.currency, money.minorUnits),\n },\n { label: \"Share\", value: `${share.toFixed(1)}%` },\n ]}\n />\n );\n }}\n />\n <Pie\n data={data}\n dataKey=\"value\"\n nameKey=\"label\"\n cx={cx}\n cy={cy}\n innerRadius={inner}\n outerRadius={radius}\n startAngle={90}\n endAngle={-270}\n stroke={data.length > 1 ? \"var(--color-shell)\" : \"none\"}\n strokeWidth={2}\n isAnimationActive={false}\n >\n {data.map((s) => (\n <Cell key={s.key} fill={categoryColorVar(s.color)} />\n ))}\n </Pie>\n </PieChart>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { Checkbox } from "radix-ui";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { CheckIcon } from "lucide-react";
|
|
6
|
+
// src/components/checkbox.tsx
|
|
7
|
+
function Checkbox$1({ className, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ jsx(Checkbox.Root, {
|
|
9
|
+
"data-slot": "checkbox",
|
|
10
|
+
className: cn("peer size-4 shrink-0 rounded-[4px] border border-input shadow-press transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", className),
|
|
11
|
+
...props,
|
|
12
|
+
children: /* @__PURE__ */ jsx(Checkbox.Indicator, {
|
|
13
|
+
"data-slot": "checkbox-indicator",
|
|
14
|
+
className: "grid place-content-center text-current transition-none",
|
|
15
|
+
children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" })
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export { Checkbox$1 as Checkbox };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=checkbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkbox.js","names":["Checkbox","CheckboxPrimitive"],"sources":["../../src/components/checkbox.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../lib/utils\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\n\nfunction Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer size-4 shrink-0 rounded-[4px] border border-input shadow-press transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n className,\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"grid place-content-center text-current transition-none\"\n >\n <CheckIcon className=\"size-3.5\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\n}\n\nexport { Checkbox };\n"],"mappings":";;;;;;AAOA,SAASA,WAAS,EAAE,WAAW,GAAG,SAA8D;CAC9F,OACE,oBAACC,SAAkB,MAAnB;EACE,aAAU;EACV,WAAW,GACT,sZACA,SACF;EACA,GAAI;EAEJ,UAAA,oBAACA,SAAkB,WAAnB;GACE,aAAU;GACV,WAAU;GAEV,UAAA,oBAAC,WAAD,EAAW,WAAU,WAAY,CAAA;EACN,CAAA;CACP,CAAA;AAE5B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ComboboxGroup<T> = {
|
|
2
|
+
heading?: string;
|
|
3
|
+
options: T[];
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Searchable single-choice picker. Part of the form flow: typing on the focused trigger opens it
|
|
7
|
+
* with that text as the search, and picking an option moves focus to the next field.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Combobox<T>({ id, value, onChange, options, groups, getKey, getSearchText, renderOption, renderValue, placeholder, searchPlaceholder, emptyText, footer, invalid, disabled, className, contentClassName }: {
|
|
10
|
+
id?: string;
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (key: string) => void;
|
|
13
|
+
/** Flat options; or pass `groups` for headed sections. */
|
|
14
|
+
options?: T[];
|
|
15
|
+
groups?: ComboboxGroup<T>[];
|
|
16
|
+
getKey: (option: T) => string;
|
|
17
|
+
/** Text the search matches against (defaults to the key). */
|
|
18
|
+
getSearchText?: (option: T) => string;
|
|
19
|
+
renderOption: (option: T) => React.ReactNode;
|
|
20
|
+
/** The selected option on the trigger (defaults to renderOption). */
|
|
21
|
+
renderValue?: (option: T) => React.ReactNode;
|
|
22
|
+
placeholder?: React.ReactNode;
|
|
23
|
+
searchPlaceholder?: string;
|
|
24
|
+
emptyText?: React.ReactNode;
|
|
25
|
+
/** Shown under the list, e.g. a link to manage the options. */
|
|
26
|
+
footer?: React.ReactNode;
|
|
27
|
+
invalid?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
contentClassName?: string;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
//# sourceMappingURL=combobox.d.ts.map
|