@copilotz/billing 0.0.0-stage → 1.0.0-rc.35

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -2
  3. package/contract.d.ts +7 -0
  4. package/contract.js +7 -0
  5. package/credits/actions/check.d.ts +5 -0
  6. package/credits/actions/check.js +97 -0
  7. package/credits/actions/manage.d.ts +9 -0
  8. package/credits/actions/manage.js +266 -0
  9. package/credits/actions/state.d.ts +2 -0
  10. package/credits/actions/state.js +172 -0
  11. package/credits/collections/credit.d.ts +110 -0
  12. package/credits/collections/credit.js +31 -0
  13. package/credits/collections/payment.d.ts +139 -0
  14. package/credits/collections/payment.js +32 -0
  15. package/credits/collections/plan.d.ts +428 -0
  16. package/credits/collections/plan.js +97 -0
  17. package/credits/collections/room.d.ts +85 -0
  18. package/credits/collections/room.js +24 -0
  19. package/credits/contract.d.ts +19 -0
  20. package/credits/contract.js +12 -0
  21. package/credits/mod.d.ts +38 -0
  22. package/credits/mod.js +72 -0
  23. package/credits/shared/apply.d.ts +66 -0
  24. package/credits/shared/apply.js +106 -0
  25. package/credits/shared/settings.d.ts +70 -0
  26. package/credits/shared/settings.js +28 -0
  27. package/credits/shared/standing.d.ts +71 -0
  28. package/credits/shared/standing.js +235 -0
  29. package/credits/shared/walk.d.ts +50 -0
  30. package/credits/shared/walk.js +127 -0
  31. package/credits/views/web/add.d.ts +25 -0
  32. package/credits/views/web/add.js +92 -0
  33. package/credits/views/web/budget.d.ts +16 -0
  34. package/credits/views/web/budget.js +62 -0
  35. package/credits/views/web/composer.d.ts +13 -0
  36. package/credits/views/web/composer.js +65 -0
  37. package/credits/views/web/history.d.ts +12 -0
  38. package/credits/views/web/history.js +55 -0
  39. package/credits/views/web/numbers.d.ts +256 -0
  40. package/credits/views/web/numbers.js +291 -0
  41. package/credits/views/web/org.d.ts +31 -0
  42. package/credits/views/web/org.js +238 -0
  43. package/credits/views/web/plan.d.ts +28 -0
  44. package/credits/views/web/plan.js +84 -0
  45. package/credits/views/web/staff.d.ts +20 -0
  46. package/credits/views/web/staff.js +265 -0
  47. package/credits/views/web/style.d.ts +9 -0
  48. package/credits/views/web/style.js +190 -0
  49. package/credits/views/web/types.d.ts +17 -0
  50. package/credits/views/web/types.js +32 -0
  51. package/credits/web.d.ts +12 -0
  52. package/credits/web.js +33 -0
  53. package/mod.d.ts +26 -0
  54. package/mod.js +25 -0
  55. package/package.json +61 -4
  56. package/stripe/actions/checkout.d.ts +4 -0
  57. package/stripe/actions/checkout.js +188 -0
  58. package/stripe/actions/fact.d.ts +1 -0
  59. package/stripe/actions/fact.js +39 -0
  60. package/stripe/mod.d.ts +30 -0
  61. package/stripe/mod.js +52 -0
  62. package/stripe/routes/webhook.d.ts +16 -0
  63. package/stripe/routes/webhook.js +56 -0
  64. package/stripe/shared/api.d.ts +14 -0
  65. package/stripe/shared/api.js +42 -0
  66. package/stripe/shared/events.d.ts +42 -0
  67. package/stripe/shared/events.js +226 -0
  68. package/stripe/shared/settings.d.ts +39 -0
  69. package/stripe/shared/settings.js +10 -0
  70. package/web.d.ts +13 -0
  71. package/web.js +13 -0
@@ -0,0 +1,265 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * What platform staff do with an org's billing, in the staff colour and marked
4
+ * as theirs: where it stands now (its plan, how it pays, any waiver, who waived
5
+ * it and why), and doing something about it: setting its plan or a contract of
6
+ * its own, giving or taking credits, waiving its usage or seats (and lifting
7
+ * that), recording a payment that came in by hand. Each in a dialog that says
8
+ * exactly what will happen, on a button naming it: all of it is on the org's
9
+ * record, and most of it can only be corrected afterwards, not undone.
10
+ */
11
+ import { useEffect, useState } from "react";
12
+ import { Badge, Button, Checkbox, Dialog, Field, Icon, Input, Notice, Select, Tabs, Textarea, } from "copilotz/web";
13
+ import { nameOf } from "copilotz/plugins/access/contract";
14
+ import { amountOf, dayShown, dollars, includes, money, seatsSaid, slug, startOfDay, } from "./numbers.js";
15
+ import { useAct } from "./types.js";
16
+ export function StaffPanel({ state, org, call, onChanged }) {
17
+ const [doing, setDoing] = useState();
18
+ const [lifting, setLifting] = useState();
19
+ const [people, setPeople] = useState([]);
20
+ useEffect(() => {
21
+ call("org.people", {}).then((found) => setPeople(found.people), () => { });
22
+ }, []);
23
+ const who = (id) => {
24
+ if (!id)
25
+ return "someone";
26
+ const found = people.find((p) => p.person === id);
27
+ return found?.name || found?.email || nameOf(id);
28
+ };
29
+ const plan = state.plan;
30
+ const name = org?.name ?? "this org";
31
+ const waived = plan?.waived;
32
+ const close = () => {
33
+ setDoing(undefined);
34
+ setLifting(undefined);
35
+ };
36
+ const shared = { state, org: name, call, close, onChanged };
37
+ return (_jsxs("section", { className: "billing-staff", "aria-labelledby": "billing-staff", children: [_jsxs("header", { className: "billing-staff-head", children: [_jsxs(Badge, { tone: "warning", children: [_jsx(Icon, { name: "shield", size: 12 }), "Staff only"] }), _jsxs("span", { id: "billing-staff", children: ["Setting ", name, "'s billing by hand. What you do here is on its record."] })] }), _jsxs("div", { className: "billing-staff-now", children: [_jsxs("dl", { className: "billing-facts", children: [_jsxs("div", { children: [_jsx("dt", { children: "Plan" }), _jsxs("dd", { children: [plan ? plan.terms.title : "None started yet", plan && plan.name.toLowerCase() !== plan.terms.title.toLowerCase()
38
+ ? _jsxs("small", { children: ["\u00B7 ", plan.name] })
39
+ : null] })] }), plan
40
+ ? (_jsxs("div", { children: [_jsx("dt", { children: "Paid through" }), _jsxs("dd", { children: [plan.channel ?? "manual", plan.status ? _jsxs("small", { children: ["\u00B7 ", plan.status] }) : null, plan.customer ? _jsxs("small", { children: ["\u00B7 ", plan.customer] }) : null] })] }))
41
+ : null, plan?.fixedSeats !== undefined
42
+ ? (_jsxs("div", { children: [_jsx("dt", { children: "Fixed seats" }), _jsx("dd", { children: seatsSaid(plan.fixedSeats) })] }))
43
+ : null, _jsxs("div", { children: [_jsx("dt", { children: "Balance" }), _jsx("dd", { children: money(state.balance ?? 0) })] })] }), _jsx(WaiverLine, { what: "usage", waiver: waived?.usage, who: who, onLift: () => setLifting("usage") }), _jsx(WaiverLine, { what: "seats", waiver: waived?.seats, who: who, onLift: () => setLifting("seats") })] }), _jsxs("div", { className: "billing-staff-actions", children: [_jsx(Button, { icon: "pencil", onClick: () => setDoing("plan"), children: "Set plan" }), _jsx(Button, { icon: "plus", onClick: () => setDoing("grant"), children: "Give or take credits" }), _jsx(Button, { icon: "pause", onClick: () => setDoing("waive"), children: "Waive" }), _jsx(Button, { icon: "note", onClick: () => setDoing("pay"), children: "Record a payment" })] }), _jsx(PlanDialog, { open: doing === "plan", ...shared }), _jsx(GrantDialog, { open: doing === "grant", ...shared }), _jsx(WaiveDialog, { open: doing === "waive", people: people, ...shared }), _jsx(PayDialog, { open: doing === "pay", ...shared }), _jsx(LiftDialog, { what: lifting, ...shared })] }));
44
+ }
45
+ /** A waiver in force: what, who and why, until when, and lifting it. */
46
+ function WaiverLine({ what, waiver, who, onLift }) {
47
+ if (!waiver)
48
+ return null;
49
+ return (_jsxs("div", { className: "billing-waiver", children: [_jsxs("div", { children: [_jsx("b", { children: what === "usage" ? "Usage waived" : "Seats waived" }), _jsxs("p", { children: ["\u201C", waiver.reason, "\u201D, by ", who(waiver.by), ", since ", dayShown(waiver.from), waiver.until ? `, until ${dayShown(waiver.until)}` : ", until lifted", waiver.people?.length ? `, for ${waiver.people.map((id) => who(id)).join(", ")}` : ""] })] }), _jsx(Button, { size: "sm", onClick: onLift, children: "Lift" })] }));
50
+ }
51
+ /** A dialog's footer, its button naming what it does. */
52
+ const Footer = ({ close, busy, ready, onGo, label, tone }) => (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: close, disabled: busy, children: "Cancel" }), _jsx(Button, { variant: tone ?? "primary", loading: busy, disabled: !ready, onClick: onGo, children: label })] }));
53
+ const termsLine = (terms) => includes(terms).join(" · ") || "No credits of its own";
54
+ function PlanDialog({ open, state, org, call, close, onChanged }) {
55
+ const plans = state.plans ?? {};
56
+ const names = Object.keys(plans);
57
+ const [kind, setKind] = useState("listed");
58
+ const [name, setName] = useState("");
59
+ const [title, setTitle] = useState("");
60
+ const [monthly, setMonthly] = useState("");
61
+ const [seats, setSeats] = useState("");
62
+ const { busy, error, run, clear } = useAct();
63
+ useEffect(() => {
64
+ if (!open)
65
+ return;
66
+ setKind("listed");
67
+ setName(names.find((n) => n !== state.plan?.name) ?? names[0] ?? "");
68
+ setTitle("");
69
+ setMonthly("");
70
+ setSeats("");
71
+ clear();
72
+ }, [open]);
73
+ const credits = amountOf(monthly);
74
+ const fixed = seats.trim() ? Number(seats) : undefined;
75
+ const seatsWrong = fixed !== undefined && (!Number.isInteger(fixed) || fixed < 0);
76
+ const listed = kind === "listed" ? plans[name] : undefined;
77
+ const ready = kind === "listed"
78
+ ? !!listed
79
+ : !!title.trim() && (!monthly.trim() || credits !== undefined) && !seatsWrong;
80
+ const hadFixed = state.plan?.fixedSeats !== undefined;
81
+ const go = (event) => {
82
+ event?.preventDefault();
83
+ if (!ready || busy)
84
+ return;
85
+ void run(async () => {
86
+ await call("billing.setPlan", kind === "listed" ? { name, ...(hadFixed ? { seats: null } : {}) } : {
87
+ name: slug(title) || "contract",
88
+ terms: {
89
+ title: title.trim(),
90
+ ...(credits ? { credits: { monthly: credits } } : {}),
91
+ },
92
+ ...(fixed !== undefined ? { seats: fixed } : hadFixed ? { seats: null } : {}),
93
+ });
94
+ await onChanged();
95
+ close();
96
+ });
97
+ };
98
+ const to = kind === "listed" ? listed?.title : title.trim();
99
+ return (_jsx(Dialog, { open: open, onClose: close, title: "Set plan", description: state.plan ? `${org} is on ${state.plan.terms.title} now.` : undefined, footer: _jsx(Footer, { close: close, busy: busy, ready: ready, onGo: () => go(), label: to ? `Move to ${to}` : "Move" }), children: _jsxs("form", { className: "billing-dialog", onSubmit: go, children: [_jsx(Tabs, { label: "A plan, or a contract", value: kind, onChange: setKind, tabs: [{ value: "listed", label: "A plan" }, { value: "contract", label: "A contract" }] }), kind === "listed"
100
+ ? (_jsx(Field, { label: "Plan", hint: listed ? termsLine(listed) : undefined, children: (control) => (_jsx(Select, { ...control, value: name, onChange: (event) => setName(event.target.value), children: names.map((n) => (_jsxs("option", { value: n, children: [plans[n].title, plans[n].for ? ` · ${plans[n].for} orgs only` : "", n === state.plan?.name ? " (now)" : ""] }, n))) })) }))
101
+ : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: "As the org's owners and admins will see it.", children: (control) => (_jsx(Input, { ...control, value: title, maxLength: 80, placeholder: "Acme contract", onChange: (event) => setTitle(event.target.value) })) }), _jsxs("div", { className: "billing-two", children: [_jsx(Field, { label: "Credits a month", error: monthly.trim() && credits === undefined
102
+ ? "That isn't an amount."
103
+ : undefined, children: (control) => (_jsx(Input, { ...control, inputMode: "decimal", placeholder: "0", className: "billing-money-input", value: monthly, onChange: (event) => setMonthly(event.target.value) })) }), _jsx(Field, { label: "Fixed seats", error: seatsWrong ? "A whole number." : undefined, hint: seatsWrong ? undefined : "Empty: count its people.", children: (control) => (_jsx(Input, { ...control, inputMode: "numeric", placeholder: "Its people", value: seats, onChange: (event) => setSeats(event.target.value) })) })] })] })), _jsxs("p", { className: "billing-text", children: ["It takes effect now, and its period starts today. What ", org, " ", "has stays until it lapses; the new plan's credits land next time it's checked.", hadFixed && (kind === "listed" || fixed === undefined)
104
+ ? " Its seats go back to counting its people."
105
+ : ""] }), error ? _jsx(Notice, { tone: "destructive", role: "alert", children: error }) : null] }) }));
106
+ }
107
+ function GrantDialog({ open, org, call, close, onChanged }) {
108
+ const [way, setWay] = useState("give");
109
+ const [typed, setTyped] = useState("");
110
+ const [reason, setReason] = useState("");
111
+ const [expires, setExpires] = useState("");
112
+ const { busy, error, run, clear } = useAct();
113
+ useEffect(() => {
114
+ if (!open)
115
+ return;
116
+ setWay("give");
117
+ setTyped("");
118
+ setReason("");
119
+ setExpires("");
120
+ clear();
121
+ }, [open]);
122
+ const amount = amountOf(typed);
123
+ const ready = !!amount && !!reason.trim();
124
+ const go = (event) => {
125
+ event?.preventDefault();
126
+ if (!ready || busy)
127
+ return;
128
+ void run(async () => {
129
+ await call("billing.grant", {
130
+ amount: way === "give" ? amount : -amount,
131
+ reason: reason.trim(),
132
+ ...(way === "give" && expires ? { expires } : {}),
133
+ });
134
+ await onChanged();
135
+ close();
136
+ });
137
+ };
138
+ return (_jsx(Dialog, { open: open, onClose: close, title: "Give or take credits", footer: _jsx(Footer, { close: close, busy: busy, ready: ready, onGo: () => go(), tone: way === "take" ? "destructive" : undefined, label: amount
139
+ ? `${way === "give" ? "Give" : "Take"} ${money(amount)}`
140
+ : way === "give"
141
+ ? "Give"
142
+ : "Take" }), children: _jsxs("form", { className: "billing-dialog", onSubmit: go, children: [_jsx(Tabs, { label: "Give or take", value: way, onChange: setWay, tabs: [{ value: "give", label: "Give" }, { value: "take", label: "Take away" }] }), _jsxs("div", { className: "billing-two", children: [_jsx(Field, { label: "Amount", error: typed.trim() && !amount ? "That isn't an amount." : undefined, children: (control) => (_jsx(Input, { ...control, inputMode: "decimal", autoFocus: true, placeholder: "0", className: "billing-money-input", value: typed, onChange: (event) => setTyped(event.target.value) })) }), way === "give"
143
+ ? (_jsx(Field, { label: "Lapses on", hint: "Empty: they don't lapse.", children: (control) => (_jsx(Input, { ...control, type: "date", value: expires, onChange: (event) => setExpires(event.target.value) })) }))
144
+ : null] }), _jsx(Field, { label: "Why", hint: `${org}'s owners and admins see this in their history.`, children: (control) => (_jsx(Textarea, { ...control, rows: 2, value: reason, placeholder: "Welcome credits for the pilot", onChange: (event) => setReason(event.target.value) })) }), _jsxs("p", { className: "billing-text", children: [way === "give"
145
+ ? `${org} can use these right away. Any debt is covered first.`
146
+ : `These come off ${org}'s balance now. If that takes it to nothing, its work stops.`, " ", "It stays in the history: to correct it, give or take again."] }), error ? _jsx(Notice, { tone: "destructive", role: "alert", children: error }) : null] }) }));
147
+ }
148
+ function WaiveDialog({ open, org, call, close, onChanged, people }) {
149
+ const [what, setWhat] = useState("usage");
150
+ const [reason, setReason] = useState("");
151
+ const [until, setUntil] = useState("");
152
+ const [whose, setWhose] = useState([]);
153
+ const { busy, error, run, clear } = useAct();
154
+ useEffect(() => {
155
+ if (!open)
156
+ return;
157
+ setWhat("usage");
158
+ setReason("");
159
+ setUntil("");
160
+ setWhose([]);
161
+ clear();
162
+ }, [open]);
163
+ const ready = !!reason.trim();
164
+ const go = (event) => {
165
+ event?.preventDefault();
166
+ if (!ready || busy)
167
+ return;
168
+ void run(async () => {
169
+ await call("billing.waive", {
170
+ what,
171
+ reason: reason.trim(),
172
+ ...(until ? { until: startOfDay(until) } : {}),
173
+ ...(what === "seats" && whose.length ? { people: whose } : {}),
174
+ });
175
+ await onChanged();
176
+ close();
177
+ });
178
+ };
179
+ return (_jsx(Dialog, { open: open, onClose: close, title: "Waive", footer: _jsx(Footer, { close: close, busy: busy, ready: ready, onGo: () => go(), label: what === "usage" ? "Waive usage" : "Waive seats" }), children: _jsxs("form", { className: "billing-dialog", onSubmit: go, children: [_jsx(Tabs, { label: "What to waive", value: what, onChange: setWhat, tabs: [{ value: "usage", label: "Usage" }, { value: "seats", label: "Seats" }] }), _jsx(Field, { label: "Why", children: (control) => (_jsx(Input, { ...control, autoFocus: true, value: reason, placeholder: "Design partner", onChange: (event) => setReason(event.target.value) })) }), _jsx(Field, { label: "Ends on", hint: "Empty: until you lift it.", children: (control) => (_jsx(Input, { ...control, type: "date", value: until, onChange: (event) => setUntil(event.target.value) })) }), what === "seats" && people.length
180
+ ? (_jsxs("fieldset", { className: "billing-people", children: [_jsx("legend", { children: "Whose (none ticked: every seat)" }), people.map((p) => (_jsx(Checkbox, { label: p.name || p.email || nameOf(p.person), checked: whose.includes(p.person), onChange: (event) => setWhose(event.target.checked
181
+ ? [...whose, p.person]
182
+ : whose.filter((id) => id !== p.person)) }, p.person)))] }))
183
+ : null, _jsx("p", { className: "billing-text", children: what === "usage"
184
+ ? `${org}'s usage is still recorded but isn't charged, and when this ends none of it counts against its credits. Its owners and admins see that usage isn't charged, not why.`
185
+ : `Those seats aren't paid for, from the next time its seats are counted.` }), error ? _jsx(Notice, { tone: "destructive", role: "alert", children: error }) : null] }) }));
186
+ }
187
+ function LiftDialog({ what, org, call, close, onChanged, state }) {
188
+ const { busy, error, run } = useAct();
189
+ const go = () => void run(async () => {
190
+ await call("billing.waive", { what: what, lift: true });
191
+ await onChanged();
192
+ close();
193
+ });
194
+ const noCredits = (state.balance ?? 0) <= 0;
195
+ return (_jsxs(Dialog, { open: !!what, onClose: close, size: "sm", title: what === "usage" ? "Start charging usage again?" : "Stop waiving seats?", footer: _jsx(Footer, { close: close, busy: busy, ready: true, onGo: go, label: "Lift the waiver" }), children: [_jsx("p", { className: "billing-text", children: what === "usage"
196
+ ? `From now on, ${org}'s usage comes off its credits. What it used while waived doesn't count.${noCredits ? " It has no credits now, so its work stops." : ""}`
197
+ : `${org}'s seats are all paid for again, from the next time they're counted.` }), error ? _jsx(Notice, { tone: "destructive", role: "alert", children: error }) : null] }));
198
+ }
199
+ const PURPOSES = [
200
+ { value: "credits", label: "Credits" },
201
+ { value: "seats", label: "Seats" },
202
+ { value: "contract", label: "Contract" },
203
+ { value: "refund", label: "Refund" },
204
+ ];
205
+ function PayDialog({ open, org, call, close, onChanged }) {
206
+ const [ref, setRef] = useState("");
207
+ const [amount, setAmount] = useState("");
208
+ const [currency, setCurrency] = useState("usd");
209
+ const [purpose, setPurpose] = useState("credits");
210
+ const [credits, setCredits] = useState("");
211
+ const [rate, setRate] = useState("");
212
+ const [fee, setFee] = useState("");
213
+ const [note, setNote] = useState("");
214
+ const { busy, error, run, clear } = useAct();
215
+ useEffect(() => {
216
+ if (!open)
217
+ return;
218
+ setRef("");
219
+ setAmount("");
220
+ setCurrency("usd");
221
+ setPurpose("credits");
222
+ setCredits("");
223
+ setRate("");
224
+ setFee("");
225
+ setNote("");
226
+ clear();
227
+ }, [open]);
228
+ const paid = amountOf(amount);
229
+ const bought = amountOf(credits);
230
+ const rated = amountOf(rate);
231
+ const kept = amountOf(fee);
232
+ const usd = currency.trim().toLowerCase() === "usd";
233
+ const codeWrong = !/^[a-zA-Z]{3}$/.test(currency.trim());
234
+ const bad = (text, parsed) => !!text.trim() && parsed === undefined;
235
+ const refund = purpose === "refund";
236
+ const ready = !!ref.trim() && !!paid && !codeWrong && !bad(credits, bought) &&
237
+ !bad(rate, rated) &&
238
+ !bad(fee, kept) && (usd || !!rated);
239
+ const go = (event) => {
240
+ event?.preventDefault();
241
+ if (!ready || busy)
242
+ return;
243
+ void run(async () => {
244
+ await call("billing.pay", {
245
+ ref: ref.trim(),
246
+ amount: refund ? -paid : paid,
247
+ currency: currency.trim().toLowerCase(),
248
+ purpose,
249
+ ...(bought ? { credits: refund ? -bought : bought } : {}),
250
+ ...(rated && !usd ? { rate: rated } : {}),
251
+ ...(kept ? { fee: kept } : {}),
252
+ ...(note.trim() ? { note: note.trim() } : {}),
253
+ });
254
+ await onChanged();
255
+ close();
256
+ });
257
+ };
258
+ const adds = bought
259
+ ? refund ? `takes ${money(bought)} of credits back` : `adds ${money(bought)} of credits`
260
+ : "changes no credits";
261
+ return (_jsx(Dialog, { open: open, onClose: close, title: refund ? "Record a refund" : "Record a payment", description: `Money that came in by hand, for ${org}.`, footer: _jsx(Footer, { close: close, busy: busy, ready: ready, onGo: () => go(), label: refund ? "Record the refund" : "Record the payment" }), children: _jsxs("form", { className: "billing-dialog", onSubmit: go, children: [_jsxs("div", { className: "billing-two", children: [_jsx(Field, { label: "Reference", hint: "An invoice or transfer number. Once each.", children: (control) => (_jsx(Input, { ...control, autoFocus: true, value: ref, placeholder: "INV-1042", spellCheck: false, onChange: (event) => setRef(event.target.value) })) }), _jsx(Field, { label: "What for", children: (control) => (_jsx(Select, { ...control, value: purpose, onChange: (event) => setPurpose(event.target.value), children: PURPOSES.map((p) => _jsx("option", { value: p.value, children: p.label }, p.value)) })) }), _jsx(Field, { label: refund ? "Amount refunded" : "Amount paid", error: bad(amount, paid) ? "That isn't an amount." : undefined, children: (control) => (_jsx(Input, { ...control, inputMode: "decimal", placeholder: "0", value: amount, onChange: (event) => setAmount(event.target.value) })) }), _jsx(Field, { label: "Currency", error: currency.trim() && codeWrong ? "Three letters: usd, brl…" : undefined, children: (control) => (_jsx(Input, { ...control, value: currency, maxLength: 3, spellCheck: false, onChange: (event) => setCurrency(event.target.value) })) }), _jsx(Field, { label: refund ? "Credits to take back" : "Credits it buys", error: bad(credits, bought) ? "That isn't an amount." : undefined, children: (control) => (_jsx(Input, { ...control, inputMode: "decimal", placeholder: "0", className: "billing-money-input", value: credits, onChange: (event) => setCredits(event.target.value) })) }), _jsx(Field, { label: "What the channel kept", error: bad(fee, kept) ? "That isn't an amount." : undefined, children: (control) => (_jsx(Input, { ...control, inputMode: "decimal", placeholder: "0", value: fee, onChange: (event) => setFee(event.target.value) })) }), usd ? null : (_jsx(Field, { label: "USD per unit", error: bad(rate, rated) ? "That isn't an amount." : undefined, hint: "Needed, as it isn't in USD.", children: (control) => (_jsx(Input, { ...control, inputMode: "decimal", placeholder: "0.18", value: rate, onChange: (event) => setRate(event.target.value) })) }))] }), _jsx(Field, { label: "Note", hint: "Its owners and admins see this.", children: (control) => (_jsx(Input, { ...control, value: note, onChange: (event) => setNote(event.target.value) })) }), _jsxs("p", { className: "billing-text", children: [paid
262
+ ? `This keeps ${dollarsIn(paid, currency, usd)} for ${org} and ${adds}.`
263
+ : `This keeps the payment for ${org}.`, " It can't be edited after: a refund corrects it."] }), error ? _jsx(Notice, { tone: "destructive", role: "alert", children: error }) : null] }) }));
264
+ }
265
+ const dollarsIn = (amount, currency, usd) => usd ? dollars(amount) : `${amount} ${currency.trim().toUpperCase()}`;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Billing's layout on the web, on the theme's variables: the balance first (the
3
+ * big number, and what it's made of as one bar and a list), pairs of cards that
4
+ * stack where there's no room, the amounts to choose from, the budget's meter,
5
+ * the history's rows, staff's panel in the warning colour as the shell marks
6
+ * staff, and the note above where you type. Only layout and those marks: the
7
+ * pieces are the toolkit's.
8
+ */
9
+ export declare const STYLE = "\n.billing { container: billing / inline-size; display: grid; gap: 16px; min-width: 0; }\n.billing-label { margin: 0; color: var(--muted-foreground); font-family: inherit; font-size: 12.5px; font-weight: 600; }\n.billing-text { margin: 0; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.5; }\n.billing-small { display: flex; align-items: flex-start; gap: 6px; margin: 0; color: var(--muted-foreground);\n font-size: 12.5px; line-height: 1.45; }\n.billing-small .copilotz-icon { flex: none; margin-top: 2px; }\n\n/* An org with nothing to worry about */\n.billing-calm { display: flex; align-items: flex-start; gap: 12px; padding: 18px 20px; }\n.billing-calm h3 { margin: 0; font-family: var(--font-display); font-size: 17px; font-weight: 650; letter-spacing: -.01em; }\n.billing-calm p { margin: 3px 0 0; color: var(--secondary-foreground); font-size: 14px; line-height: 1.5; }\n.billing-calm-icon { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--radius-lg);\n background: var(--success-soft); color: var(--success-text); }\n\n/* The balance */\n.billing-balance { display: grid; gap: 16px; padding: 20px 22px; }\n.billing-standing { margin: -4px -6px 2px; }\n.billing-balance-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 16px 36px; align-items: start; }\n.billing-balance-main { display: grid; gap: 2px; min-width: 0; }\n.billing-amount { margin: 2px 0 0; font-family: var(--font-display); font-size: clamp(34px, 8.5cqi, 50px); font-weight: 680;\n letter-spacing: -.03em; line-height: 1.08; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }\n.billing-debt { margin: 6px 0 0; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.45; }\n.billing-worth { margin: 8px 0 0; color: var(--muted-foreground); font-size: 12.5px; line-height: 1.45; }\n.billing-buckets { display: grid; gap: 10px; min-width: 0; }\n.billing-buckets h4 { font-size: 12.5px; }\n.billing-stack { display: flex; gap: 2px; height: 8px; border-radius: var(--radius-sm); overflow: hidden; }\n.billing-stack > span { flex-basis: 0; min-width: 3px; }\n[data-colour=\"1\"] { background: var(--chart-1, var(--primary)); }\n[data-colour=\"2\"] { background: var(--chart-2, var(--primary)); }\n[data-colour=\"3\"] { background: var(--chart-3, var(--success)); }\n[data-colour=\"4\"] { background: var(--chart-4, var(--warning)); }\n[data-colour=\"5\"] { background: var(--chart-5, var(--primary)); }\n.billing-bucket-list { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }\n.billing-bucket-list li { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: baseline; gap: 10px;\n padding: 5px 0; }\n.billing-bucket-list li + li { border-top: 1px solid var(--border); }\n.billing-bucket-list i { align-self: center; width: 10px; height: 10px; border-radius: var(--radius-sm); }\n.billing-bucket-name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; min-width: 0; font-size: 14px; font-weight: 580; }\n.billing-bucket-name small { color: var(--muted-foreground); font-size: 12.5px; font-weight: 450; }\n.billing-bucket-left { display: inline-flex; flex-wrap: wrap; align-items: baseline; justify-content: flex-end; gap: 0 6px;\n font-size: 14px; font-weight: 620; font-variant-numeric: tabular-nums; text-align: right; }\n.billing-bucket-left small { color: var(--muted-foreground); font-size: 12px; font-weight: 450; }\n\n/* A payment being confirmed */\n.billing-paying-line { display: flex; align-items: flex-start; gap: 10px; }\n.billing-paying-line > :first-child { flex: none; margin-top: 2px; }\n.billing-paying b { font-weight: 650; }\n\n/* Cards, in pairs where there's room */\n.billing-pair { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: 16px; }\n.billing-pair[data-single] { grid-template-columns: minmax(0, 1fr); }\n.billing-pair-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n.billing-card { display: grid; align-content: start; gap: 12px; min-width: 0; padding: 18px 20px; scroll-margin: 24px; }\n.billing-card-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 12px; min-height: 22px; }\n.billing-card-head h3 { margin: 0; font-size: 15px; font-weight: 650; }\n.billing-card-end { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 2px; }\n.billing-card-end .billing-small { flex: 1 1 140px; }\n.billing-facts { display: grid; gap: 8px; margin: 0; }\n.billing-facts > div { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 2px 10px; align-items: baseline; }\n.billing-facts dt { color: var(--muted-foreground); font-size: 13px; }\n.billing-facts dd { margin: 0; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }\n.billing-facts small { margin-left: 5px; color: var(--muted-foreground); font-size: 12.5px; }\n\n/* Adding credits */\n.billing-add { display: grid; gap: 12px; }\n.billing-choices { display: flex; flex-wrap: wrap; gap: 6px; }\n.billing-choice { position: relative; flex: 1 1 54px; display: grid; place-items: center; min-height: 46px; padding: 0 4px; border: 1px solid var(--input);\n border-radius: var(--radius-lg); background: var(--card); color: var(--foreground); cursor: pointer; font-size: 15px; font-weight: 620;\n font-variant-numeric: tabular-nums; transition: background-color .12s, border-color .12s, box-shadow .12s; }\n.billing-choice:hover { background: var(--muted); }\n.billing-choice[data-on] { border-color: var(--ring); background: var(--primary-soft); color: var(--primary-text);\n outline: 1px solid var(--ring); outline-offset: -2px; }\n.billing-choice input { position: absolute; inset: 0; margin: 0; appearance: none; border-radius: inherit; cursor: inherit; outline: none; }\n.billing-choice input:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }\n.billing-choice:has(input:disabled) { opacity: .6; cursor: default; }\n.billing-money-input::before { content: \"$\"; color: var(--muted-foreground); font-size: 14.5px; }\n.billing-after { margin: 0; min-height: 1.45em; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.45; }\n.billing-after b { color: var(--foreground); font-weight: 650; font-variant-numeric: tabular-nums; }\n.billing-go { width: 100%; }\n.billing-pair[data-single] .billing-redeem { max-width: 480px; }\n.billing-redeem { display: grid; gap: 10px; justify-items: start; }\n.billing-redeem > * { width: 100%; }\n.billing-redeem > .billing-go-small { width: auto; }\n.billing-code-input input { font-family: var(--font-mono); font-size: 13.5px; letter-spacing: .01em; }\n\n/* The plan */\n.billing-plan-name { margin: -6px 0 0; font-family: var(--font-display); font-size: 22px; font-weight: 660; letter-spacing: -.015em; line-height: 1.2; }\n.billing-plan-credits { display: grid; gap: 6px; padding: 10px 12px 11px; border-radius: var(--radius-lg); background: var(--muted); }\n.billing-plan-left { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.4; font-variant-numeric: tabular-nums; }\n.billing-for-teams { padding-top: 10px; border-top: 1px solid var(--border); }\n.billing-card-head + .billing-for-teams { padding-top: 0; border-top: 0; }\n.billing-offers { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 0; padding: 0; list-style: none; }\n.billing-offer { display: grid; grid-template-rows: 1fr auto; gap: 12px; min-width: 0; padding: 14px 16px;\n border: 1px solid var(--border); border-radius: var(--radius-lg); }\n.billing-offer-text { display: grid; align-content: start; gap: 1px; min-width: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.45; }\n.billing-offer-text b { color: var(--foreground); font-family: var(--font-display); font-size: 17px; font-weight: 650; letter-spacing: -.01em; }\n.billing-offer-total { margin-bottom: 4px; color: var(--foreground); font-size: 14.5px; font-weight: 620; font-variant-numeric: tabular-nums; }\n\n/* The month's budget */\n.billing-spent { margin: -4px 0 0; color: var(--muted-foreground); font-size: 15px; font-variant-numeric: tabular-nums; }\n.billing-spent b { color: var(--foreground); font-family: var(--font-display); font-size: 22px; font-weight: 660; letter-spacing: -.015em; }\n.billing-meter { height: 8px; border-radius: var(--radius-sm); overflow: hidden;\n background: color-mix(in oklab, var(--chart-1, var(--primary)) 14%, transparent); }\n.billing-meter > span { display: block; height: 100%; border-radius: var(--radius-sm); background: var(--chart-1, var(--primary));\n transition: width .3s ease-out; }\n.billing-meter[data-tone=near] { background: color-mix(in oklab, var(--warning) 20%, transparent); }\n.billing-meter[data-tone=near] > span { background: var(--warning); }\n.billing-meter[data-tone=at] { background: color-mix(in oklab, var(--warning) 20%, transparent); }\n.billing-meter[data-tone=at] > span { background: var(--warning); border-radius: 0; }\n\n/* Dialogs */\n.billing-dialog { display: grid; gap: 14px; min-width: 0; }\n.billing-dialog > .copilotz-tabs { justify-self: start; }\n.billing-two { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; align-items: start; }\n.billing-rules { display: grid; gap: 6px; margin: 0; padding: 12px 14px; border-radius: var(--radius-lg); background: var(--muted);\n list-style: none; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.4; }\n.billing-rules li { display: flex; align-items: flex-start; gap: 8px; }\n.billing-rules .copilotz-icon { flex: none; margin-top: 2px; color: var(--muted-foreground); }\n.billing-people { display: grid; gap: 6px; margin: 0; padding: 0; border: 0; min-width: 0; }\n.billing-people legend { margin-bottom: 6px; padding: 0; font-size: 13.5px; font-weight: 600; }\n\n/* History */\n.billing-history { gap: 6px; }\n.billing-history-head { margin-bottom: 6px; }\n.billing-rows { display: grid; margin: 0 -4px; padding: 0; list-style: none; }\n.billing-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 16px; padding: 9px 4px; }\n.billing-row + .billing-row { border-top: 1px solid var(--border); }\n.billing-row-text { display: grid; gap: 1px; min-width: 0; font-size: 14px; line-height: 1.4; }\n.billing-row-text b { font-weight: 600; }\n.billing-row-note { color: var(--muted-foreground); font-size: 13px; overflow-wrap: anywhere; }\n.billing-row-end { display: grid; justify-items: end; gap: 1px; line-height: 1.4; }\n.billing-row-end small { color: var(--muted-foreground); font-size: 12.5px; white-space: nowrap; }\n.billing-row-staff { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 5px; margin-top: 1px; font-size: 11.5px;\n color: color-mix(in oklab, var(--warning-text) 80%, var(--muted-foreground)); overflow-wrap: anywhere; }\n.billing-row-amount { font-size: 14.5px; font-weight: 640; font-variant-numeric: tabular-nums; white-space: nowrap; }\n.billing-row-amount[data-way=in] { color: var(--success-text); }\n.billing-row-amount[data-way=out] { color: var(--secondary-foreground); }\n.billing-none { padding: 22px 12px 16px; }\n.billing-none .copilotz-empty-icon { width: 44px; height: 44px; margin-bottom: 4px; }\n.billing-none .copilotz-empty-title { font-size: 16px; }\n.billing-none .copilotz-empty-text { font-size: 13.5px; }\n.billing-more { justify-self: start; margin: 4px 0 -6px -8px; }\n\n/* Staff only */\n.billing-staff { display: grid; gap: 12px; padding: 12px 14px 14px; border: 1px solid var(--warning-border);\n border-radius: var(--radius-lg); background: var(--warning-soft); }\n.billing-staff-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; color: var(--secondary-foreground);\n font-size: 12.5px; line-height: 1.4; }\n.billing-staff-head .copilotz-badge { gap: 4px; }\n.billing-staff-now { display: grid; gap: 10px; padding: 12px 14px; border: 1px solid var(--warning-border);\n border-radius: var(--radius-md); background: var(--card); }\n.billing-waiver { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 10px;\n border-top: 1px solid var(--border); font-size: 14px; }\n.billing-waiver b { font-weight: 620; }\n.billing-waiver p { margin: 1px 0 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }\n.billing-waiver .copilotz-button { flex: none; }\n.billing-staff-actions { display: flex; flex-wrap: wrap; gap: 8px; }\n\n/* Above where you type */\n.billing-note { align-items: center; padding: 6px 8px 6px 12px; font-size: 13.5px; }\n.billing-note > .copilotz-icon { margin-top: 0; }\n.billing-note .copilotz-notice-action { margin: 0; }\n.billing-note .copilotz-notice-text b { color: var(--foreground); font-weight: 620; }\n\n@container billing (max-width: 720px) {\n .billing-pair, .billing-pair-even { grid-template-columns: minmax(0, 1fr); }\n .billing-pair[data-budget-first] > :first-child { order: 1; }\n .billing-balance-grid { grid-template-columns: minmax(0, 1fr); }\n}\n@container billing (max-width: 520px) {\n .billing-balance, .billing-card { padding: 16px; }\n .billing-standing { margin: -2px -2px 0; }\n .billing-wrap { flex-wrap: wrap; }\n .billing-wrap .copilotz-notice-body { flex: 1 1 calc(100% - 30px); }\n .billing-wrap .copilotz-notice-action { flex: 1 1 100%; margin: 2px 0 0; }\n .billing-wrap .copilotz-notice-action > .copilotz-button { flex: 1; }\n .billing-two { grid-template-columns: minmax(0, 1fr); }\n .billing-staff-actions > .copilotz-button { flex: 1 1 calc(50% - 4px); padding: 0 8px; }\n .billing-waiver { align-items: flex-start; }\n}\n";
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Billing's layout on the web, on the theme's variables: the balance first (the
3
+ * big number, and what it's made of as one bar and a list), pairs of cards that
4
+ * stack where there's no room, the amounts to choose from, the budget's meter,
5
+ * the history's rows, staff's panel in the warning colour as the shell marks
6
+ * staff, and the note above where you type. Only layout and those marks: the
7
+ * pieces are the toolkit's.
8
+ */
9
+ export const STYLE = `
10
+ .billing { container: billing / inline-size; display: grid; gap: 16px; min-width: 0; }
11
+ .billing-label { margin: 0; color: var(--muted-foreground); font-family: inherit; font-size: 12.5px; font-weight: 600; }
12
+ .billing-text { margin: 0; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.5; }
13
+ .billing-small { display: flex; align-items: flex-start; gap: 6px; margin: 0; color: var(--muted-foreground);
14
+ font-size: 12.5px; line-height: 1.45; }
15
+ .billing-small .copilotz-icon { flex: none; margin-top: 2px; }
16
+
17
+ /* An org with nothing to worry about */
18
+ .billing-calm { display: flex; align-items: flex-start; gap: 12px; padding: 18px 20px; }
19
+ .billing-calm h3 { margin: 0; font-family: var(--font-display); font-size: 17px; font-weight: 650; letter-spacing: -.01em; }
20
+ .billing-calm p { margin: 3px 0 0; color: var(--secondary-foreground); font-size: 14px; line-height: 1.5; }
21
+ .billing-calm-icon { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--radius-lg);
22
+ background: var(--success-soft); color: var(--success-text); }
23
+
24
+ /* The balance */
25
+ .billing-balance { display: grid; gap: 16px; padding: 20px 22px; }
26
+ .billing-standing { margin: -4px -6px 2px; }
27
+ .billing-balance-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 16px 36px; align-items: start; }
28
+ .billing-balance-main { display: grid; gap: 2px; min-width: 0; }
29
+ .billing-amount { margin: 2px 0 0; font-family: var(--font-display); font-size: clamp(34px, 8.5cqi, 50px); font-weight: 680;
30
+ letter-spacing: -.03em; line-height: 1.08; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
31
+ .billing-debt { margin: 6px 0 0; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.45; }
32
+ .billing-worth { margin: 8px 0 0; color: var(--muted-foreground); font-size: 12.5px; line-height: 1.45; }
33
+ .billing-buckets { display: grid; gap: 10px; min-width: 0; }
34
+ .billing-buckets h4 { font-size: 12.5px; }
35
+ .billing-stack { display: flex; gap: 2px; height: 8px; border-radius: var(--radius-sm); overflow: hidden; }
36
+ .billing-stack > span { flex-basis: 0; min-width: 3px; }
37
+ [data-colour="1"] { background: var(--chart-1, var(--primary)); }
38
+ [data-colour="2"] { background: var(--chart-2, var(--primary)); }
39
+ [data-colour="3"] { background: var(--chart-3, var(--success)); }
40
+ [data-colour="4"] { background: var(--chart-4, var(--warning)); }
41
+ [data-colour="5"] { background: var(--chart-5, var(--primary)); }
42
+ .billing-bucket-list { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
43
+ .billing-bucket-list li { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: baseline; gap: 10px;
44
+ padding: 5px 0; }
45
+ .billing-bucket-list li + li { border-top: 1px solid var(--border); }
46
+ .billing-bucket-list i { align-self: center; width: 10px; height: 10px; border-radius: var(--radius-sm); }
47
+ .billing-bucket-name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; min-width: 0; font-size: 14px; font-weight: 580; }
48
+ .billing-bucket-name small { color: var(--muted-foreground); font-size: 12.5px; font-weight: 450; }
49
+ .billing-bucket-left { display: inline-flex; flex-wrap: wrap; align-items: baseline; justify-content: flex-end; gap: 0 6px;
50
+ font-size: 14px; font-weight: 620; font-variant-numeric: tabular-nums; text-align: right; }
51
+ .billing-bucket-left small { color: var(--muted-foreground); font-size: 12px; font-weight: 450; }
52
+
53
+ /* A payment being confirmed */
54
+ .billing-paying-line { display: flex; align-items: flex-start; gap: 10px; }
55
+ .billing-paying-line > :first-child { flex: none; margin-top: 2px; }
56
+ .billing-paying b { font-weight: 650; }
57
+
58
+ /* Cards, in pairs where there's room */
59
+ .billing-pair { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: 16px; }
60
+ .billing-pair[data-single] { grid-template-columns: minmax(0, 1fr); }
61
+ .billing-pair-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
62
+ .billing-card { display: grid; align-content: start; gap: 12px; min-width: 0; padding: 18px 20px; scroll-margin: 24px; }
63
+ .billing-card-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 12px; min-height: 22px; }
64
+ .billing-card-head h3 { margin: 0; font-size: 15px; font-weight: 650; }
65
+ .billing-card-end { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 2px; }
66
+ .billing-card-end .billing-small { flex: 1 1 140px; }
67
+ .billing-facts { display: grid; gap: 8px; margin: 0; }
68
+ .billing-facts > div { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 2px 10px; align-items: baseline; }
69
+ .billing-facts dt { color: var(--muted-foreground); font-size: 13px; }
70
+ .billing-facts dd { margin: 0; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
71
+ .billing-facts small { margin-left: 5px; color: var(--muted-foreground); font-size: 12.5px; }
72
+
73
+ /* Adding credits */
74
+ .billing-add { display: grid; gap: 12px; }
75
+ .billing-choices { display: flex; flex-wrap: wrap; gap: 6px; }
76
+ .billing-choice { position: relative; flex: 1 1 54px; display: grid; place-items: center; min-height: 46px; padding: 0 4px; border: 1px solid var(--input);
77
+ border-radius: var(--radius-lg); background: var(--card); color: var(--foreground); cursor: pointer; font-size: 15px; font-weight: 620;
78
+ font-variant-numeric: tabular-nums; transition: background-color .12s, border-color .12s, box-shadow .12s; }
79
+ .billing-choice:hover { background: var(--muted); }
80
+ .billing-choice[data-on] { border-color: var(--ring); background: var(--primary-soft); color: var(--primary-text);
81
+ outline: 1px solid var(--ring); outline-offset: -2px; }
82
+ .billing-choice input { position: absolute; inset: 0; margin: 0; appearance: none; border-radius: inherit; cursor: inherit; outline: none; }
83
+ .billing-choice input:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
84
+ .billing-choice:has(input:disabled) { opacity: .6; cursor: default; }
85
+ .billing-money-input::before { content: "$"; color: var(--muted-foreground); font-size: 14.5px; }
86
+ .billing-after { margin: 0; min-height: 1.45em; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.45; }
87
+ .billing-after b { color: var(--foreground); font-weight: 650; font-variant-numeric: tabular-nums; }
88
+ .billing-go { width: 100%; }
89
+ .billing-pair[data-single] .billing-redeem { max-width: 480px; }
90
+ .billing-redeem { display: grid; gap: 10px; justify-items: start; }
91
+ .billing-redeem > * { width: 100%; }
92
+ .billing-redeem > .billing-go-small { width: auto; }
93
+ .billing-code-input input { font-family: var(--font-mono); font-size: 13.5px; letter-spacing: .01em; }
94
+
95
+ /* The plan */
96
+ .billing-plan-name { margin: -6px 0 0; font-family: var(--font-display); font-size: 22px; font-weight: 660; letter-spacing: -.015em; line-height: 1.2; }
97
+ .billing-plan-credits { display: grid; gap: 6px; padding: 10px 12px 11px; border-radius: var(--radius-lg); background: var(--muted); }
98
+ .billing-plan-left { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.4; font-variant-numeric: tabular-nums; }
99
+ .billing-for-teams { padding-top: 10px; border-top: 1px solid var(--border); }
100
+ .billing-card-head + .billing-for-teams { padding-top: 0; border-top: 0; }
101
+ .billing-offers { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 0; padding: 0; list-style: none; }
102
+ .billing-offer { display: grid; grid-template-rows: 1fr auto; gap: 12px; min-width: 0; padding: 14px 16px;
103
+ border: 1px solid var(--border); border-radius: var(--radius-lg); }
104
+ .billing-offer-text { display: grid; align-content: start; gap: 1px; min-width: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.45; }
105
+ .billing-offer-text b { color: var(--foreground); font-family: var(--font-display); font-size: 17px; font-weight: 650; letter-spacing: -.01em; }
106
+ .billing-offer-total { margin-bottom: 4px; color: var(--foreground); font-size: 14.5px; font-weight: 620; font-variant-numeric: tabular-nums; }
107
+
108
+ /* The month's budget */
109
+ .billing-spent { margin: -4px 0 0; color: var(--muted-foreground); font-size: 15px; font-variant-numeric: tabular-nums; }
110
+ .billing-spent b { color: var(--foreground); font-family: var(--font-display); font-size: 22px; font-weight: 660; letter-spacing: -.015em; }
111
+ .billing-meter { height: 8px; border-radius: var(--radius-sm); overflow: hidden;
112
+ background: color-mix(in oklab, var(--chart-1, var(--primary)) 14%, transparent); }
113
+ .billing-meter > span { display: block; height: 100%; border-radius: var(--radius-sm); background: var(--chart-1, var(--primary));
114
+ transition: width .3s ease-out; }
115
+ .billing-meter[data-tone=near] { background: color-mix(in oklab, var(--warning) 20%, transparent); }
116
+ .billing-meter[data-tone=near] > span { background: var(--warning); }
117
+ .billing-meter[data-tone=at] { background: color-mix(in oklab, var(--warning) 20%, transparent); }
118
+ .billing-meter[data-tone=at] > span { background: var(--warning); border-radius: 0; }
119
+
120
+ /* Dialogs */
121
+ .billing-dialog { display: grid; gap: 14px; min-width: 0; }
122
+ .billing-dialog > .copilotz-tabs { justify-self: start; }
123
+ .billing-two { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; align-items: start; }
124
+ .billing-rules { display: grid; gap: 6px; margin: 0; padding: 12px 14px; border-radius: var(--radius-lg); background: var(--muted);
125
+ list-style: none; color: var(--secondary-foreground); font-size: 13.5px; line-height: 1.4; }
126
+ .billing-rules li { display: flex; align-items: flex-start; gap: 8px; }
127
+ .billing-rules .copilotz-icon { flex: none; margin-top: 2px; color: var(--muted-foreground); }
128
+ .billing-people { display: grid; gap: 6px; margin: 0; padding: 0; border: 0; min-width: 0; }
129
+ .billing-people legend { margin-bottom: 6px; padding: 0; font-size: 13.5px; font-weight: 600; }
130
+
131
+ /* History */
132
+ .billing-history { gap: 6px; }
133
+ .billing-history-head { margin-bottom: 6px; }
134
+ .billing-rows { display: grid; margin: 0 -4px; padding: 0; list-style: none; }
135
+ .billing-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 16px; padding: 9px 4px; }
136
+ .billing-row + .billing-row { border-top: 1px solid var(--border); }
137
+ .billing-row-text { display: grid; gap: 1px; min-width: 0; font-size: 14px; line-height: 1.4; }
138
+ .billing-row-text b { font-weight: 600; }
139
+ .billing-row-note { color: var(--muted-foreground); font-size: 13px; overflow-wrap: anywhere; }
140
+ .billing-row-end { display: grid; justify-items: end; gap: 1px; line-height: 1.4; }
141
+ .billing-row-end small { color: var(--muted-foreground); font-size: 12.5px; white-space: nowrap; }
142
+ .billing-row-staff { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 5px; margin-top: 1px; font-size: 11.5px;
143
+ color: color-mix(in oklab, var(--warning-text) 80%, var(--muted-foreground)); overflow-wrap: anywhere; }
144
+ .billing-row-amount { font-size: 14.5px; font-weight: 640; font-variant-numeric: tabular-nums; white-space: nowrap; }
145
+ .billing-row-amount[data-way=in] { color: var(--success-text); }
146
+ .billing-row-amount[data-way=out] { color: var(--secondary-foreground); }
147
+ .billing-none { padding: 22px 12px 16px; }
148
+ .billing-none .copilotz-empty-icon { width: 44px; height: 44px; margin-bottom: 4px; }
149
+ .billing-none .copilotz-empty-title { font-size: 16px; }
150
+ .billing-none .copilotz-empty-text { font-size: 13.5px; }
151
+ .billing-more { justify-self: start; margin: 4px 0 -6px -8px; }
152
+
153
+ /* Staff only */
154
+ .billing-staff { display: grid; gap: 12px; padding: 12px 14px 14px; border: 1px solid var(--warning-border);
155
+ border-radius: var(--radius-lg); background: var(--warning-soft); }
156
+ .billing-staff-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; color: var(--secondary-foreground);
157
+ font-size: 12.5px; line-height: 1.4; }
158
+ .billing-staff-head .copilotz-badge { gap: 4px; }
159
+ .billing-staff-now { display: grid; gap: 10px; padding: 12px 14px; border: 1px solid var(--warning-border);
160
+ border-radius: var(--radius-md); background: var(--card); }
161
+ .billing-waiver { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 10px;
162
+ border-top: 1px solid var(--border); font-size: 14px; }
163
+ .billing-waiver b { font-weight: 620; }
164
+ .billing-waiver p { margin: 1px 0 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
165
+ .billing-waiver .copilotz-button { flex: none; }
166
+ .billing-staff-actions { display: flex; flex-wrap: wrap; gap: 8px; }
167
+
168
+ /* Above where you type */
169
+ .billing-note { align-items: center; padding: 6px 8px 6px 12px; font-size: 13.5px; }
170
+ .billing-note > .copilotz-icon { margin-top: 0; }
171
+ .billing-note .copilotz-notice-action { margin: 0; }
172
+ .billing-note .copilotz-notice-text b { color: var(--foreground); font-weight: 620; }
173
+
174
+ @container billing (max-width: 720px) {
175
+ .billing-pair, .billing-pair-even { grid-template-columns: minmax(0, 1fr); }
176
+ .billing-pair[data-budget-first] > :first-child { order: 1; }
177
+ .billing-balance-grid { grid-template-columns: minmax(0, 1fr); }
178
+ }
179
+ @container billing (max-width: 520px) {
180
+ .billing-balance, .billing-card { padding: 16px; }
181
+ .billing-standing { margin: -2px -2px 0; }
182
+ .billing-wrap { flex-wrap: wrap; }
183
+ .billing-wrap .copilotz-notice-body { flex: 1 1 calc(100% - 30px); }
184
+ .billing-wrap .copilotz-notice-action { flex: 1 1 100%; margin: 2px 0 0; }
185
+ .billing-wrap .copilotz-notice-action > .copilotz-button { flex: 1; }
186
+ .billing-two { grid-template-columns: minmax(0, 1fr); }
187
+ .billing-staff-actions > .copilotz-button { flex: 1 1 calc(50% - 4px); padding: 0 8px; }
188
+ .billing-waiver { align-items: flex-start; }
189
+ }
190
+ `;
@@ -0,0 +1,17 @@
1
+ export type Call = <R = unknown>(action: string, input?: unknown) => Promise<R>;
2
+ export type Org = {
3
+ id: string;
4
+ name: string;
5
+ kind: string;
6
+ };
7
+ /** Where what's there before paying is kept, to tell what's new on coming back. */
8
+ export declare const LEFT = "billing-left-for-payment";
9
+ /** Something being done: busy while it goes, why if it failed, kept until they try again. */
10
+ export declare function useAct(): {
11
+ busy: boolean;
12
+ error: string | undefined;
13
+ clear: () => void;
14
+ /** Runs it; true when it went. `stay`: still busy after, as the page is leaving. */
15
+ run: (work: () => Promise<unknown>, stay?: boolean) => Promise<boolean>;
16
+ release: () => void;
17
+ };
@@ -0,0 +1,32 @@
1
+ /** What Billing's pieces pass each other: how they call the app, and busy-then-why on acting. */
2
+ import { useState } from "react";
3
+ import { message } from "copilotz/web";
4
+ /** Where what's there before paying is kept, to tell what's new on coming back. */
5
+ export const LEFT = "billing-left-for-payment";
6
+ /** Something being done: busy while it goes, why if it failed, kept until they try again. */
7
+ export function useAct() {
8
+ const [busy, setBusy] = useState(false);
9
+ const [error, setError] = useState();
10
+ return {
11
+ busy,
12
+ error,
13
+ clear: () => setError(undefined),
14
+ /** Runs it; true when it went. `stay`: still busy after, as the page is leaving. */
15
+ run: async (work, stay = false) => {
16
+ setBusy(true);
17
+ setError(undefined);
18
+ try {
19
+ await work();
20
+ if (!stay)
21
+ setBusy(false);
22
+ return true;
23
+ }
24
+ catch (e) {
25
+ setError(message(e));
26
+ setBusy(false);
27
+ return false;
28
+ }
29
+ },
30
+ release: () => setBusy(false),
31
+ };
32
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Credits' web front end: the org page's Billing section (the web shell's
3
+ * `orgSettings`): what the org has and how it'll be spent, adding credits and
4
+ * redeeming a gift code, its plan, its month's budget and its history; for
5
+ * platform staff, setting all of that by hand. And a short note above where
6
+ * you type (chat's `composerNotes`), so people know before they send that the
7
+ * org is out, or running low. Add it beside the web shell, where the browser
8
+ * draws.
9
+ */
10
+ import { type Plugin } from "copilotz";
11
+ export declare const creditsWeb: Plugin;
12
+ export default creditsWeb;
package/credits/web.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Credits' web front end: the org page's Billing section (the web shell's
3
+ * `orgSettings`): what the org has and how it'll be spent, adding credits and
4
+ * redeeming a gift code, its plan, its month's budget and its history; for
5
+ * platform staff, setting all of that by hand. And a short note above where
6
+ * you type (chat's `composerNotes`), so people know before they send that the
7
+ * org is out, or running low. Add it beside the web shell, where the browser
8
+ * draws.
9
+ */
10
+ import { definePlugin } from "copilotz";
11
+ import { composerNoteView } from "./views/web/composer.js";
12
+ import { orgBillingView } from "./views/web/org.js";
13
+ export const creditsWeb = definePlugin({
14
+ name: "credits-web",
15
+ requires: ["web-shell"],
16
+ views: {
17
+ "billing.org": orgBillingView,
18
+ "billing.composer": composerNoteView,
19
+ },
20
+ resources: {
21
+ orgSettings: {
22
+ billing: {
23
+ title: "Billing",
24
+ view: "billing.org",
25
+ icon: "star",
26
+ order: 25,
27
+ roles: ["owner", "admin"],
28
+ },
29
+ },
30
+ composerNotes: { billing: { view: "billing.composer" } },
31
+ },
32
+ });
33
+ export default creditsWeb;
package/mod.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Billing (`@copilotz/billing`): what an org has to spend, and paying for it,
3
+ * built on Copilotz's usage ledger (`usage`, in `copilotz/plugins/billing`),
4
+ * whose prices it counts.
5
+ *
6
+ * - `credits`: an org's plan, credits and payments, its balance and budget,
7
+ * and the check that stops it at its limit (the `spending` resource
8
+ * whatever spends reads); staff change plans and give credits by hand.
9
+ * - `stripe`: one way an org pays, by Stripe's Checkout and portal; what was
10
+ * paid lands by a signed webhook (`stripeWebhook`, in the runtime of the
11
+ * app's one address: the directory's, with many orgs).
12
+ *
13
+ * ```ts
14
+ * import { usage } from "copilotz/plugins/billing";
15
+ * import { credits, stripe, stripeWebhook } from "@copilotz/billing";
16
+ *
17
+ * plugins: [models, chat, usage, credits, stripe, stripeWebhook, …]
18
+ * ```
19
+ *
20
+ * Their front end is `@copilotz/billing/web` (`creditsWeb`: the org page's
21
+ * Billing, the note above where you type); their shapes,
22
+ * `@copilotz/billing/contract`.
23
+ */
24
+ export * from "./credits/mod.js";
25
+ export { API_VERSION, EVENTS, stripe, stripeWebhook } from "./stripe/mod.js";
26
+ export type { Seller, StripeSettings } from "./stripe/mod.js";