@firedrill-tools/stripe 0.1.1

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 (59) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +433 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/api-unavailable.scenario.json +11 -0
  5. package/firedrill/baseline.scenario.json +5 -0
  6. package/firedrill/conformance.suite.json +17 -0
  7. package/firedrill/rate-limited.scenario.json +11 -0
  8. package/firedrill/refund-committed-lost.scenario.json +11 -0
  9. package/firedrill/stripe-api-unavailable.drill.json +463 -0
  10. package/firedrill/stripe-denied.drill.json +74 -0
  11. package/firedrill/stripe-large-pages.drill.json +153 -0
  12. package/firedrill/stripe-live-mode.drill.json +136 -0
  13. package/firedrill/stripe-mcp-aliases.drill.json +406 -0
  14. package/firedrill/stripe-no-permissions.drill.json +1143 -0
  15. package/firedrill/stripe-rate-limited.drill.json +616 -0
  16. package/firedrill/stripe-refund-committed-lost.drill.json +139 -0
  17. package/firedrill/stripe-rest-flow.drill.json +1401 -0
  18. package/firedrill/stripe-restricted-key.drill.json +171 -0
  19. package/firedrill/tools/stripe/app/assets/ATTRIBUTION.md +36 -0
  20. package/firedrill/tools/stripe/app/assets/fonts/OFL.txt +93 -0
  21. package/firedrill/tools/stripe/app/assets/stripe-s.svg +1 -0
  22. package/firedrill/tools/stripe/app/assets/stripe.svg +1 -0
  23. package/firedrill/tools/stripe/app/site/app.js +456 -0
  24. package/firedrill/tools/stripe/app/site/assets/fonts/inter-latin.woff2 +0 -0
  25. package/firedrill/tools/stripe/app/site/assets/stripe-s.svg +1 -0
  26. package/firedrill/tools/stripe/app/site/assets/stripe.svg +1 -0
  27. package/firedrill/tools/stripe/app/site/icons.js +90 -0
  28. package/firedrill/tools/stripe/app/site/index.html +137 -0
  29. package/firedrill/tools/stripe/app/site/pages-billing.js +902 -0
  30. package/firedrill/tools/stripe/app/site/pages-catalog.js +314 -0
  31. package/firedrill/tools/stripe/app/site/pages-customers.js +416 -0
  32. package/firedrill/tools/stripe/app/site/pages-home.js +373 -0
  33. package/firedrill/tools/stripe/app/site/pages-payments.js +502 -0
  34. package/firedrill/tools/stripe/app/site/store.js +99 -0
  35. package/firedrill/tools/stripe/app/site/styles.css +2512 -0
  36. package/firedrill/tools/stripe/app/site/ui.js +767 -0
  37. package/firedrill/tools/stripe/app/site/widgets.js +707 -0
  38. package/firedrill/tools/stripe/behavior.mjs +148 -0
  39. package/firedrill/tools/stripe/lib/cards.mjs +53 -0
  40. package/firedrill/tools/stripe/lib/form.mjs +204 -0
  41. package/firedrill/tools/stripe/lib/ids.mjs +85 -0
  42. package/firedrill/tools/stripe/lib/money.mjs +35 -0
  43. package/firedrill/tools/stripe/lib/objects.mjs +229 -0
  44. package/firedrill/tools/stripe/lib/periods.mjs +41 -0
  45. package/firedrill/tools/stripe/lib/size.mjs +55 -0
  46. package/firedrill/tools/stripe/lib/state.mjs +230 -0
  47. package/firedrill/tools/stripe/lib/validate.mjs +184 -0
  48. package/firedrill/tools/stripe/lib/wire.mjs +98 -0
  49. package/firedrill/tools/stripe/ops/billing.mjs +914 -0
  50. package/firedrill/tools/stripe/ops/catalog.mjs +203 -0
  51. package/firedrill/tools/stripe/ops/customers.mjs +241 -0
  52. package/firedrill/tools/stripe/ops/dashboard.mjs +29 -0
  53. package/firedrill/tools/stripe/ops/payments.mjs +608 -0
  54. package/firedrill/tools/stripe/stripe.tool.json +28833 -0
  55. package/firedrill/world.json +8527 -0
  56. package/firedrill.json +5 -0
  57. package/package.json +64 -0
  58. package/starter.json +7999 -0
  59. package/test/conformance.mjs +1133 -0
@@ -0,0 +1,456 @@
1
+ // Stripe Tool browser app: shell, routing, search and the global Create menu. Every screen calls the Tool's own
2
+ // operations through /_firedrill/client.js; nothing rendered here is authoritative — records are re-read after
3
+ // each write and whenever the world revision moves.
4
+ import { hydrateIcons, icon } from "./icons.js";
5
+ import "./pages-payments.js";
6
+ import "./pages-customers.js";
7
+ import "./pages-catalog.js";
8
+ import "./pages-billing.js";
9
+ import "./pages-home.js";
10
+ import { openCustomerForm } from "./pages-customers.js";
11
+ import { openProductForm } from "./pages-catalog.js";
12
+ import { PREFIX_PAGES, app, canRead, canWrite, indexAll, invalidateCache, loadContext, navigate, pageHost, parseRoute } from "./store.js";
13
+ import { $, $$, ToolError, call, describe, el, isPending, money, openMenu, readPreference, toast, watchWorld, writePreference } from "./ui.js";
14
+ import { alert, deniedPanel, notSimulated } from "./widgets.js";
15
+
16
+ const DETAIL_PAGES = { payments: "payment", customers: "customer", products: "product", invoices: "invoice", subscriptions: "subscription" };
17
+ const NEW_PAGES = { payments: "payment-new", invoices: "invoice-new", subscriptions: "subscription-new" };
18
+ const NAV_ROUTE_FOR = { payment: "payments", "payment-new": "payments", customer: "customers", product: "products", invoice: "invoices", "invoice-new": "invoices", subscription: "subscriptions", "subscription-new": "subscriptions", settings: undefined };
19
+
20
+ // ---------------------------------------------------------------------------------------------
21
+ // Router
22
+ // ---------------------------------------------------------------------------------------------
23
+
24
+ function resolve(route) {
25
+ if (route.id === "new" && NEW_PAGES[route.page]) return NEW_PAGES[route.page];
26
+ if (route.id && DETAIL_PAGES[route.page]) return DETAIL_PAGES[route.page];
27
+ return route.page;
28
+ }
29
+
30
+ let renderToken = 0;
31
+ async function render() {
32
+ const host = pageHost();
33
+ const route = parseRoute(location.hash);
34
+ const pageName = resolve(route);
35
+ const page = app.pages[pageName];
36
+ const token = (renderToken += 1);
37
+ host.dispatchEvent(new CustomEvent("page-leave"));
38
+ app.route = { ...route, name: pageName };
39
+ const navName = NAV_ROUTE_FOR[pageName] === undefined && pageName in NAV_ROUTE_FOR ? undefined : NAV_ROUTE_FOR[pageName] ?? pageName;
40
+ recordVisit(navName);
41
+ renderShortcuts();
42
+ markNav(navName);
43
+ closeNav();
44
+ host.scrollTop = 0;
45
+ if (!page) {
46
+ host.replaceChildren(el("div", { class: "page-inner" }, [alert(`There is no page at ${location.hash || "#/"}.`, "neutral", { title: "Not found" })]));
47
+ return;
48
+ }
49
+ try {
50
+ await page(host, route);
51
+ } catch (error) {
52
+ if (token !== renderToken) return;
53
+ host.replaceChildren(el("div", { class: "page-inner" }, [error instanceof ToolError && (error.denied || error.is("PERMISSION_DENIED")) ? deniedPanel(error, error.details?.group) : alert(describe(error), "danger", { title: "Could not render this page" })]));
54
+ }
55
+ }
56
+
57
+ // Shortcuts: pinned pages first, then recently visited ones (per-viewer preference, never record data).
58
+ const SHORTCUT_PAGES = {
59
+ payments: { label: "Payments", icon: "payments", href: "#/payments" },
60
+ invoices: { label: "Invoices", icon: "invoices", href: "#/invoices" },
61
+ subscriptions: { label: "Subscriptions", icon: "subscriptions", href: "#/subscriptions" },
62
+ refunds: { label: "Refunds", icon: "refunds", href: "#/refunds" },
63
+ developers: { label: "Developers", icon: "developers", href: "#/developers" },
64
+ settings: { label: "Settings", icon: "gear", href: "#/settings" },
65
+ };
66
+ function readList(name, fallback) {
67
+ try {
68
+ const value = JSON.parse(readPreference(name, JSON.stringify(fallback)));
69
+ return Array.isArray(value) ? value.filter((entry) => Object.hasOwn(SHORTCUT_PAGES, entry)) : fallback;
70
+ } catch {
71
+ return fallback;
72
+ }
73
+ }
74
+ function recordVisit(navName) {
75
+ if (!navName || !Object.hasOwn(SHORTCUT_PAGES, navName)) return;
76
+ const recent = [navName, ...readList("recent", []).filter((entry) => entry !== navName)].slice(0, 4);
77
+ writePreference("recent", JSON.stringify(recent));
78
+ }
79
+ function renderShortcuts() {
80
+ const pinned = readList("pinned", ["payments", "invoices", "subscriptions"]);
81
+ const recent = readList("recent", []).filter((entry) => !pinned.includes(entry));
82
+ const list = $("#shortcuts-list");
83
+ list.replaceChildren(
84
+ ...[...pinned.map((entry) => [entry, true]), ...recent.map((entry) => [entry, false])].slice(0, 6).map(([entry, isPinned]) => {
85
+ const page = SHORTCUT_PAGES[entry];
86
+ const anchor = el("a", { class: "sidenav-item", attrs: { href: page.href, "data-route": entry } }, [icon(page.icon, "sidenav-icon"), el("span", { text: page.label })]);
87
+ const pin = el("button", { class: `shortcut-pin btn-reset ${isPinned ? "is-pinned" : ""}`.trim(), title: isPinned ? "Unpin" : "Pin", attrs: { type: "button", "aria-label": `${isPinned ? "Unpin" : "Pin"} ${page.label}`, "aria-pressed": String(isPinned) } }, icon("pin"));
88
+ pin.addEventListener("click", (event) => {
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ const current = readList("pinned", ["payments", "invoices", "subscriptions"]);
92
+ writePreference("pinned", JSON.stringify(isPinned ? current.filter((item) => item !== entry) : [...current, entry]));
93
+ renderShortcuts();
94
+ markNav(currentNav);
95
+ });
96
+ anchor.append(pin);
97
+ return el("li", {}, anchor);
98
+ }),
99
+ );
100
+ }
101
+ let currentNav;
102
+
103
+ function markNav(routeName) {
104
+ currentNav = routeName;
105
+ for (const link of $$("[data-route]")) {
106
+ const current = link.dataset.route === routeName;
107
+ if (current) link.setAttribute("aria-current", "page");
108
+ else link.removeAttribute("aria-current");
109
+ }
110
+ for (const group of $$(".sidenav-group")) {
111
+ const hasCurrent = group.querySelector(".sidenav-subitem[aria-current]") !== null;
112
+ if (hasCurrent) expandGroup(group, true);
113
+ }
114
+ }
115
+
116
+ function expandGroup(group, open) {
117
+ const toggle = group.querySelector(".sidenav-toggle");
118
+ const list = group.querySelector(".sidenav-sublist");
119
+ toggle.setAttribute("aria-expanded", String(open));
120
+ list.hidden = !open;
121
+ }
122
+
123
+ function closeNav() {
124
+ $("#app").classList.remove("nav-open");
125
+ $("#sidebar-scrim").hidden = true;
126
+ $("#menu-toggle").setAttribute("aria-expanded", "false");
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------------------------
130
+ // Shell
131
+ // ---------------------------------------------------------------------------------------------
132
+
133
+ function wireShell() {
134
+ for (const control of $$("[data-not-simulated]")) {
135
+ const feature = control.dataset.notSimulated;
136
+ control.title = `${feature} (not simulated)`;
137
+ control.addEventListener("click", () => notSimulated(feature));
138
+ }
139
+ $("#sandbox-switch").addEventListener("click", () => notSimulated("Live account", "This Tool only simulates a sandbox. There is no live account behind it, and no real card network is ever reached."));
140
+ const apps = $("#topbar-apps");
141
+ apps.addEventListener("click", () => notSimulated("Stripe Apps", "Installed apps and the App Marketplace are not simulated."));
142
+ for (const group of $$(".sidenav-group")) {
143
+ const toggle = group.querySelector(".sidenav-toggle");
144
+ toggle.addEventListener("click", () => expandGroup(group, toggle.getAttribute("aria-expanded") !== "true"));
145
+ }
146
+ $("#menu-toggle").addEventListener("click", () => {
147
+ const open = !$("#app").classList.contains("nav-open");
148
+ $("#app").classList.toggle("nav-open", open);
149
+ $("#sidebar-scrim").hidden = !open;
150
+ $("#menu-toggle").setAttribute("aria-expanded", String(open));
151
+ });
152
+ $("#sidebar-scrim").addEventListener("click", closeNav);
153
+
154
+ const collapseButton = $("#sidebar-collapse");
155
+ const applyCollapsed = (collapsed) => {
156
+ $("#app").classList.toggle("sidebar-collapsed", collapsed);
157
+ collapseButton.setAttribute("aria-expanded", String(!collapsed));
158
+ collapseButton.setAttribute("aria-label", collapsed ? "Expand sidebar" : "Collapse sidebar");
159
+ collapseButton.title = collapsed ? "Expand sidebar" : "Collapse sidebar";
160
+ collapseButton.replaceChildren(icon(collapsed ? "expand" : "collapse", "sidenav-icon"), el("span", { class: "sidebar-collapse-label", text: "Collapse" }));
161
+ };
162
+ applyCollapsed(readPreference("sidebar-collapsed", "0") === "1");
163
+ collapseButton.addEventListener("click", () => {
164
+ const collapsed = !$("#app").classList.contains("sidebar-collapsed");
165
+ writePreference("sidebar-collapsed", collapsed ? "1" : "0");
166
+ applyCollapsed(collapsed);
167
+ });
168
+
169
+ $("#topbar-search").addEventListener("click", () => {
170
+ $("#app").classList.add("nav-open");
171
+ $("#sidebar-scrim").hidden = false;
172
+ $("#menu-toggle").setAttribute("aria-expanded", "true");
173
+ $("#search").focus();
174
+ });
175
+ $("#search-form").addEventListener("click", () => {
176
+ if ($("#app").classList.contains("sidebar-collapsed")) {
177
+ writePreference("sidebar-collapsed", "0");
178
+ applyCollapsed(false);
179
+ $("#search").focus();
180
+ }
181
+ });
182
+
183
+ const switcher = $("#account-switcher");
184
+ switcher.addEventListener("click", () => {
185
+ const account = app.context?.account;
186
+ openMenu(switcher, [
187
+ { header: account?.business_name ?? "Account" },
188
+ { label: "Settings", icon: "gear", onSelect: () => navigate("#/settings") },
189
+ { label: "Developers", icon: "developers", onSelect: () => navigate("#/developers") },
190
+ "divider",
191
+ { label: "Switch to sandbox", icon: "flask", description: "Not simulated", onSelect: () => notSimulated("Switch to sandbox", "This Tool serves exactly one sandbox account; there are no other sandboxes to switch to.") },
192
+ { label: "Manage sandboxes", icon: "apps", description: "Not simulated", onSelect: () => notSimulated("Manage sandboxes") },
193
+ { label: "Create account", icon: "plus", description: "Not simulated", onSelect: () => notSimulated("Create account") },
194
+ "divider",
195
+ { label: `Actor: ${app.connection?.actorId ?? "…"}`, icon: "customers", disabled: true, description: "Change actors from Firedrill, not from the app" },
196
+ ], { width: 260 });
197
+ });
198
+
199
+ const create = $("#topbar-create");
200
+ create.addEventListener("click", () => {
201
+ openMenu(create, [
202
+ { label: "Payment", icon: "payments", disabled: !canWrite("payment_intents"), onSelect: () => navigate("#/payments/new") },
203
+ { label: "Customer", icon: "customers", disabled: !canWrite("customers"), onSelect: () => openCustomerForm() },
204
+ { label: "Invoice", icon: "invoices", disabled: !canWrite("invoices"), onSelect: () => navigate("#/invoices/new") },
205
+ { label: "Subscription", icon: "subscriptions", disabled: !canWrite("subscriptions"), onSelect: () => navigate("#/subscriptions/new") },
206
+ { label: "Product", icon: "catalog", disabled: !canWrite("products"), onSelect: () => openProductForm() },
207
+ "divider",
208
+ { label: "Payment link", icon: "external", description: "Not simulated", onSelect: () => notSimulated("Payment Links") },
209
+ { label: "Coupon", icon: "receipt", description: "Not simulated", onSelect: () => notSimulated("Coupons") },
210
+ { label: "Quote", icon: "note", description: "Not simulated", onSelect: () => notSimulated("Quotes") },
211
+ ], { align: "end", width: 240 });
212
+ });
213
+
214
+ const help = $("#topbar-help");
215
+ const openHelp = () =>
216
+ openMenu(help, [
217
+ { header: "Keyboard shortcuts" },
218
+ { label: "Search", shortcut: "/", disabled: true },
219
+ { label: "Go to Home", shortcut: "g h", disabled: true },
220
+ { label: "Go to Payments", shortcut: "g p", disabled: true },
221
+ { label: "Go to Customers", shortcut: "g c", disabled: true },
222
+ { label: "Go to Invoices", shortcut: "g i", disabled: true },
223
+ { label: "Go to Subscriptions", shortcut: "g s", disabled: true },
224
+ { label: "Show keyboard shortcuts", shortcut: "?", disabled: true },
225
+ { label: "Close menus and dialogs", shortcut: "Esc", disabled: true },
226
+ "divider",
227
+ { label: "Documentation", icon: "external", description: "Not simulated", onSelect: () => notSimulated("Documentation", "This offline app links nowhere; see the Tool README for what is simulated.") },
228
+ { label: "Contact support", icon: "help", description: "Not simulated", onSelect: () => notSimulated("Support") },
229
+ { label: "Developers", icon: "developers", onSelect: () => navigate("#/developers") },
230
+ ], { align: "end", width: 280 });
231
+ help.addEventListener("click", openHelp);
232
+
233
+ const notifications = $("#topbar-notifications");
234
+ notifications.addEventListener("click", () => openMenu(notifications, el("div", { class: "menu-rich" }, [el("div", { class: "menu-header", text: "Notifications" }), el("div", { class: "muted", text: "No notifications. This synthetic account does not generate alerts or e-mail." })]), { align: "end", width: 300 }));
235
+
236
+ const settings = $("#topbar-settings");
237
+ settings.addEventListener("click", () =>
238
+ openMenu(settings, [
239
+ { header: "Settings" },
240
+ { label: "Business details", icon: "gear", onSelect: () => navigate("#/settings") },
241
+ { label: "Developers", icon: "developers", onSelect: () => navigate("#/developers") },
242
+ "divider",
243
+ { label: "Personal details", icon: "customers", description: "Not simulated", onSelect: () => notSimulated("Personal details") },
244
+ { label: "Team and security", icon: "customers", description: "Not simulated", onSelect: () => notSimulated("Team and security") },
245
+ { label: "Branding", icon: "sparkle", description: "Not simulated", onSelect: () => notSimulated("Branding") },
246
+ { label: "Billing settings", icon: "invoices", description: "Not simulated", onSelect: () => notSimulated("Billing settings", "Invoice and subscription behaviour of this Tool is fixed; see the README.") },
247
+ ], { align: "end", width: 260 }),
248
+ );
249
+
250
+ document.addEventListener("keydown", (event) => {
251
+ const editable = event.target.closest("input, textarea, select, [contenteditable]");
252
+ if (event.key === "/" && !editable && !event.metaKey && !event.ctrlKey) {
253
+ event.preventDefault();
254
+ $("#search").focus();
255
+ return;
256
+ }
257
+ if (editable) return;
258
+ if (event.key === "?" && !event.metaKey && !event.ctrlKey) {
259
+ event.preventDefault();
260
+ openHelp();
261
+ return;
262
+ }
263
+ if (event.key === "g") {
264
+ pendingChord = true;
265
+ clearTimeout(chordTimer);
266
+ chordTimer = setTimeout(() => {
267
+ pendingChord = false;
268
+ }, 900);
269
+ return;
270
+ }
271
+ if (pendingChord) {
272
+ pendingChord = false;
273
+ const target = { h: "#/", p: "#/payments", c: "#/customers", i: "#/invoices", s: "#/subscriptions", b: "#/balances", t: "#/transactions", d: "#/developers" }[event.key];
274
+ if (target) navigate(target);
275
+ }
276
+ });
277
+ }
278
+ let pendingChord = false;
279
+ let chordTimer;
280
+
281
+ // ---------------------------------------------------------------------------------------------
282
+ // Search: ids jump straight to the object; text searches customers, payments and products (bounded index)
283
+ // ---------------------------------------------------------------------------------------------
284
+
285
+ function wireSearch() {
286
+ const form = $("#search-form");
287
+ const inputElement = $("#search");
288
+ const results = $("#search-results");
289
+ let timer;
290
+ let activeIndex = -1;
291
+ const hide = () => {
292
+ results.hidden = true;
293
+ inputElement.setAttribute("aria-expanded", "false");
294
+ activeIndex = -1;
295
+ };
296
+ const show = (nodes) => {
297
+ results.replaceChildren(...nodes);
298
+ results.hidden = false;
299
+ inputElement.setAttribute("aria-expanded", "true");
300
+ };
301
+ const hit = (iconName, main, sub, href) => {
302
+ const element = el("button", { class: "search-hit", attrs: { type: "button", role: "option" } }, [icon(iconName), el("span", { class: "search-hit-main", text: main }), sub ? el("span", { class: "search-hit-sub", text: sub }) : null]);
303
+ element.addEventListener("mousedown", (event) => event.preventDefault());
304
+ element.addEventListener("click", () => {
305
+ hide();
306
+ inputElement.value = "";
307
+ navigate(href);
308
+ });
309
+ return element;
310
+ };
311
+ async function search(query) {
312
+ const trimmed = query.trim();
313
+ if (!trimmed) {
314
+ hide();
315
+ return;
316
+ }
317
+ const idMatch = /^([a-z]+)_[0-9A-Za-z]+$/.exec(trimmed);
318
+ if (idMatch && PREFIX_PAGES[idMatch[1]]) {
319
+ const page = PREFIX_PAGES[idMatch[1]];
320
+ const nodes = [el("div", { class: "search-group", text: "Jump to" })];
321
+ if (page === "charges") nodes.push(hit("payments", trimmed, "Open the payment for this charge", `#/charges/${trimmed}`));
322
+ else if (page === "refunds") nodes.push(hit("arrow-return", trimmed, "Refunds are listed with their payment", "#/refunds"));
323
+ else if (page === "prices" || page === "payment_methods" || page === "invoice_items") nodes.push(el("div", { class: "search-note", text: `${trimmed} is a ${page.replace("_", " ").replace(/s$/, "")} — open its product, customer or invoice instead.` }));
324
+ else nodes.push(hit(page === "payments" ? "payments" : page === "customers" ? "customers" : page === "invoices" ? "invoices" : page === "subscriptions" ? "subscriptions" : "catalog", trimmed, `Open ${page.replace(/s$/, "")}`, `#/${page}/${trimmed}`));
325
+ show(nodes);
326
+ return;
327
+ }
328
+ show([el("div", { class: "search-note" }, [icon("spinner", "spin"), el("span", { text: " Searching…" })])]);
329
+ const lower = trimmed.toLowerCase();
330
+ const nodes = [];
331
+ // Each index is bounded (store.indexAll); a group that stopped at its bound is named in a note, never hidden.
332
+ const bounded = [];
333
+ const scan = async (operation, args, noun) => {
334
+ const index = await indexAll(operation, args);
335
+ if (!index.complete) bounded.push(`${index.items.length.toLocaleString("en-US")} ${noun}`);
336
+ return index.items;
337
+ };
338
+ try {
339
+ if (canRead("customers")) {
340
+ const customers = (await scan("customers.list", {}, "customers")).filter((customer) => [customer.name, customer.email, customer.description].some((value) => value && String(value).toLowerCase().includes(lower))).slice(0, 5);
341
+ if (customers.length > 0) nodes.push(el("div", { class: "search-group", text: "Customers" }), ...customers.map((customer) => hit("customers", customer.name || customer.email || customer.id, customer.email, `#/customers/${customer.id}`)));
342
+ }
343
+ if (canRead("payment_intents")) {
344
+ const intents = (await scan("payment_intents.list", { expand: ["data.latest_charge", "data.customer"] }, "payments")).filter((intent) => [intent.description, intent.id, intent.metadata?.order_id].some((value) => value && String(value).toLowerCase().includes(lower))).slice(0, 5);
345
+ if (intents.length > 0) nodes.push(el("div", { class: "search-group", text: "Payments" }), ...intents.map((intent) => hit("payments", `${money(intent.amount, intent.currency)} ${intent.description ?? ""}`.trim(), intent.id, `#/payments/${intent.id}`)));
346
+ }
347
+ if (canRead("products")) {
348
+ const products = (await scan("products.list", {}, "products")).filter((product) => [product.name, product.description].some((value) => value && String(value).toLowerCase().includes(lower))).slice(0, 5);
349
+ if (products.length > 0) nodes.push(el("div", { class: "search-group", text: "Products" }), ...products.map((product) => hit("catalog", product.name, product.active ? undefined : "Archived", `#/products/${product.id}`)));
350
+ }
351
+ if (canRead("invoices")) {
352
+ const invoices = (await scan("invoices.list", {}, "invoices")).filter((invoice) => [invoice.number, invoice.customer_name, invoice.customer_email].some((value) => value && String(value).toLowerCase().includes(lower))).slice(0, 5);
353
+ if (invoices.length > 0) nodes.push(el("div", { class: "search-group", text: "Invoices" }), ...invoices.map((invoice) => hit("invoices", `${invoice.number ?? "Draft"} · ${money(invoice.total, invoice.currency)}`, invoice.customer_name ?? undefined, `#/invoices/${invoice.id}`)));
354
+ }
355
+ } catch (error) {
356
+ nodes.push(el("div", { class: "search-note", text: describe(error) }));
357
+ }
358
+ if (nodes.length === 0) nodes.push(el("div", { class: "search-note", text: `No ${bounded.length > 0 ? "loaded " : ""}customers, payments, products or invoices match “${trimmed}”.` }));
359
+ if (bounded.length > 0) nodes.push(el("div", { class: "search-note", attrs: { "data-bound": "true" }, text: `Searched only the newest ${bounded.join(", ")}; older records are not included. Paste an id to open any record.` }));
360
+ nodes.push(el("div", { class: "search-note", text: "Search matches names, e-mails, descriptions and invoice numbers of this account; paste an id (pi_…, cus_…, in_…) to jump to it." }));
361
+ show(nodes);
362
+ }
363
+ inputElement.addEventListener("input", () => {
364
+ clearTimeout(timer);
365
+ timer = setTimeout(() => void search(inputElement.value), 180);
366
+ });
367
+ inputElement.addEventListener("focus", () => {
368
+ if (inputElement.value.trim()) void search(inputElement.value);
369
+ });
370
+ inputElement.addEventListener("blur", () => setTimeout(hide, 120));
371
+ inputElement.addEventListener("keydown", (event) => {
372
+ const hits = $$(".search-hit", results);
373
+ if (event.key === "Escape") {
374
+ inputElement.value = "";
375
+ hide();
376
+ inputElement.blur();
377
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
378
+ if (hits.length === 0) return;
379
+ event.preventDefault();
380
+ activeIndex = event.key === "ArrowDown" ? (activeIndex + 1) % hits.length : (activeIndex - 1 + hits.length) % hits.length;
381
+ hits.forEach((element, index) => element.setAttribute("aria-selected", String(index === activeIndex)));
382
+ } else if (event.key === "Enter") {
383
+ event.preventDefault();
384
+ if (activeIndex >= 0 && hits[activeIndex]) hits[activeIndex].click();
385
+ else if (hits.length > 0) hits[0].click();
386
+ }
387
+ });
388
+ form.addEventListener("submit", (event) => event.preventDefault());
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------------------------
392
+ // Charges / refunds by id: resolve to the owning payment
393
+ // ---------------------------------------------------------------------------------------------
394
+
395
+ app.pages.charges = async (host, route) => {
396
+ const charge = await call("charges.retrieve", { charge: route.id });
397
+ if (charge.payment_intent) navigate(`#/payments/${charge.payment_intent}`);
398
+ else host.replaceChildren(el("div", { class: "page-inner" }, [alert(`Charge ${charge.id} is not linked to a PaymentIntent.`, "neutral")]));
399
+ };
400
+
401
+ // ---------------------------------------------------------------------------------------------
402
+ // Boot
403
+ // ---------------------------------------------------------------------------------------------
404
+
405
+ async function boot() {
406
+ hydrateIcons();
407
+ wireShell();
408
+ wireSearch();
409
+ const initial = $("#page");
410
+ try {
411
+ await loadContext();
412
+ } catch (error) {
413
+ initial.replaceChildren(el("div", { class: "page-inner" }, [error instanceof ToolError && error.denied ? deniedPanel(error) : alert(describe(error), "danger", { title: "Could not connect to the synthetic account", actions: [el("button", { class: "btn btn-secondary", text: "Retry", attrs: { type: "button" }, on: { click: () => location.reload() } })] })]));
414
+ $("#account-name").textContent = "Stripe";
415
+ return;
416
+ }
417
+ renderAccount();
418
+ window.addEventListener("hashchange", () => void render());
419
+ await render();
420
+ watchWorld(
421
+ async (first) => {
422
+ if (first) return;
423
+ invalidateCache();
424
+ try {
425
+ await loadContext();
426
+ renderAccount();
427
+ } catch {
428
+ /* the page render reports it */
429
+ }
430
+ await render();
431
+ },
432
+ () => app.editing === 0 && !isPending() && document.querySelector("dialog[open]") === null,
433
+ (context) => {
434
+ app.connection = context;
435
+ const label = context.title ? `${context.title}` : "Stripe";
436
+ if (!app.context) $("#account-name").textContent = label;
437
+ },
438
+ );
439
+ }
440
+
441
+ function renderAccount() {
442
+ const account = app.context?.account;
443
+ const name = account?.business_name ?? "Stripe";
444
+ $("#account-name").textContent = name;
445
+ $("#account-avatar").textContent = name
446
+ .split(/\s+/)
447
+ .filter(Boolean)
448
+ .slice(0, 2)
449
+ .map((part) => part.charAt(0).toUpperCase())
450
+ .join("");
451
+ $("#sandbox-banner").hidden = Boolean(app.context?.livemode);
452
+ $("#sandbox-label").textContent = ` · ${name}`;
453
+ }
454
+
455
+ void boot().catch((error) => toast(describe(error), { error: true }));
456
+
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="currentColor" d="M13.706 9.663c0-1.394 1.162-1.931 3.025-1.931c2.713 0 6.156.831 8.869 2.294V1.633C22.644.452 19.694.002 16.737.002c-7.231 0-12.05 3.775-12.05 10.087c0 9.869 13.55 8.269 13.55 12.525c0 1.65-1.431 2.181-3.419 2.181c-2.95 0-6.763-1.219-9.756-2.844v8.031a24.75 24.75 0 0 0 9.75 2.025c7.413 0 12.519-3.188 12.519-9.6c0-10.637-13.625-8.731-13.625-12.744"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 214"><path fill="#635bff" d="M512 110.08c0-36.409-17.636-65.138-51.342-65.138c-33.85 0-54.33 28.73-54.33 64.854c0 42.808 24.179 64.426 58.88 64.426c16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823c-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658m-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756c8.675 0 17.92 6.685 17.92 22.756zm-90.31-51.627c-13.939 0-22.899 6.542-27.876 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537l.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96c25.458 0 48.64-20.48 48.64-65.564c-.142-41.245-23.609-63.716-48.498-63.716m-8.534 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968c12.942 0 21.902 14.506 21.902 33.137c0 19.058-8.818 33.28-21.902 33.28M241.493 36.551l35.698-7.68V0l-35.698 7.538zm0 10.809h35.698v124.444h-35.698zm-38.257 10.524L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524m-71.112-41.386l-34.702 7.395l-.142 113.92c0 21.05 15.787 36.551 36.836 36.551c11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022zM35.982 83.484c0-5.546 4.551-7.68 12.09-7.68c10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92c0 6.541-5.688 8.675-13.653 8.675c-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106c29.582 0 49.92-14.648 49.92-40.106c-.142-42.382-54.329-34.845-54.329-50.774"/></svg>
@@ -0,0 +1,90 @@
1
+ // Monochrome 16 px interface glyphs drawn for this package in the solid, rounded style of the Stripe Dashboard's
2
+ // icon set. Every glyph is an original path; no third-party icon font or brand mark is embedded here.
3
+
4
+ const PATHS = {
5
+ home: "M8 1.5a1 1 0 0 1 .64.23l5.5 4.58A1 1 0 0 1 14.5 7.08V13a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1-.5-.5v-3.5h-3V14a.5.5 0 0 1-.5.5H3A1.5 1.5 0 0 1 1.5 13V7.08a1 1 0 0 1 .36-.77l5.5-4.58A1 1 0 0 1 8 1.5Z",
6
+ balances: "M8 1.25 14.6 4.6a.6.6 0 0 1-.27 1.14H13.5V11h.5a1 1 0 0 1 1 1v1.5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V12a1 1 0 0 1 1-1h.5V5.75H1.67A.6.6 0 0 1 1.4 4.6L8 1.25ZM4.5 5.75V11h1.75V5.75H4.5Zm3.25 0V11h.5V5.75h-.5Zm2 0V11h1.75V5.75H9.75Z",
7
+ transactions: "M9.9 1.7a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1-1.06 1.06L11.5 4.36V8.5a.75.75 0 0 1-1.5 0V4.36l-1.64 1.65A.75.75 0 1 1 7.3 4.95L9.9 1.7ZM6.1 14.3a.75.75 0 0 1-1.06 0L1.8 11.05a.75.75 0 0 1 1.06-1.06l1.64 1.65V7.5a.75.75 0 0 1 1.5 0v4.14l1.64-1.65a.75.75 0 1 1 1.06 1.06L6.1 14.3Z",
8
+ customers: "M8 1.5a3.25 3.25 0 1 1 0 6.5 3.25 3.25 0 0 1 0-6.5ZM3.2 14.5a.7.7 0 0 1-.7-.75C2.7 10.9 4.87 9 8 9s5.3 1.9 5.5 4.75a.7.7 0 0 1-.7.75H3.2Z",
9
+ catalog: "M2.5 2A1.5 1.5 0 0 0 1 3.5v9A1.5 1.5 0 0 0 2.5 14h11a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 13.5 2h-11Zm.5 3.5h10V6H3v-.5Zm0 3h4V9H3v-.5Z",
10
+ payments: "M1 4.5A1.5 1.5 0 0 1 2.5 3h11A1.5 1.5 0 0 1 15 4.5V6H1V4.5ZM1 7.5h14v4A1.5 1.5 0 0 1 13.5 13h-11A1.5 1.5 0 0 1 1 11.5v-4Zm2.25 2.25a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z",
11
+ invoices: "M3.5 1A1.5 1.5 0 0 0 2 2.5v11.72a.75.75 0 0 0 1.1.66L5 13.9l1.9.98a.75.75 0 0 0 .7 0l1.9-.98 1.9.98a.75.75 0 0 0 .7 0l1.9-.98.8.4A.75.75 0 0 0 14 13.66V2.5A1.5 1.5 0 0 0 12.5 1h-9Zm1.25 3.5h6.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5Zm0 3h6.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5Zm0 3h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1 0-1.5Z",
12
+ subscriptions: "M8 2.5a5.5 5.5 0 0 0-4.72 2.67.75.75 0 1 1-1.28-.78A7 7 0 0 1 14.9 7.2a.75.75 0 0 1-1.48.25A5.5 5.5 0 0 0 8 2.5Zm-6.15 5.8a.75.75 0 0 1 .73.55A5.5 5.5 0 0 0 12.72 10.8a.75.75 0 1 1 1.28.78A7 7 0 0 1 1.1 8.8a.75.75 0 0 1 .75-.5ZM10.5 3.25a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0V4h-2.25a.75.75 0 0 1-.75-.75ZM1 9.75A.75.75 0 0 1 1.75 9h3a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0v-2.25H1.75A.75.75 0 0 1 1 9.75Z",
13
+ refunds: "M6.03 2.22a.75.75 0 0 1 0 1.06L4.06 5.25H9.5a4.75 4.75 0 0 1 0 9.5H5a.75.75 0 0 1 0-1.5h4.5a3.25 3.25 0 0 0 0-6.5H4.06l1.97 1.97a.75.75 0 1 1-1.06 1.06L1.72 6.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z",
14
+ developers: "M5.53 3.47a.75.75 0 0 1 0 1.06L2.06 8l3.47 3.47a.75.75 0 1 1-1.06 1.06l-4-4a.75.75 0 0 1 0-1.06l4-4a.75.75 0 0 1 1.06 0Zm4.94 0a.75.75 0 0 1 1.06 0l4 4a.75.75 0 0 1 0 1.06l-4 4a.75.75 0 1 1-1.06-1.06L13.94 8l-3.47-3.47a.75.75 0 0 1 0-1.06Z",
15
+ search: "M6.75 1.5a5.25 5.25 0 1 1-3.3 9.33l-.02.02-2.2 2.2a.75.75 0 0 1-1.06-1.06l2.2-2.2.02-.02A5.25 5.25 0 0 1 6.75 1.5Zm0 1.5a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5Z",
16
+ plus: "M8 2.25a.75.75 0 0 1 .75.75v4.25H13a.75.75 0 0 1 0 1.5H8.75V13a.75.75 0 0 1-1.5 0V8.75H3a.75.75 0 0 1 0-1.5h4.25V3A.75.75 0 0 1 8 2.25Z",
17
+ "chevron-down": "M3.47 5.97a.75.75 0 0 1 1.06 0L8 9.44l3.47-3.47a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06Z",
18
+ "chevron-up": "M12.53 10.03a.75.75 0 0 1-1.06 0L8 6.56l-3.47 3.47a.75.75 0 0 1-1.06-1.06l4-4a.75.75 0 0 1 1.06 0l4 4a.75.75 0 0 1 0 1.06Z",
19
+ "chevron-right": "M5.97 3.47a.75.75 0 0 1 1.06 0l4 4a.75.75 0 0 1 0 1.06l-4 4a.75.75 0 0 1-1.06-1.06L9.44 8 5.97 4.53a.75.75 0 0 1 0-1.06Z",
20
+ "chevron-left": "M10.03 3.47a.75.75 0 0 1 0 1.06L6.56 8l3.47 3.47a.75.75 0 1 1-1.06 1.06l-4-4a.75.75 0 0 1 0-1.06l4-4a.75.75 0 0 1 1.06 0Z",
21
+ "arrow-left": "M7.53 2.47a.75.75 0 0 1 0 1.06L4.06 7h9.19a.75.75 0 0 1 0 1.5H4.06l3.47 3.47a.75.75 0 0 1-1.06 1.06l-4.75-4.75a.75.75 0 0 1 0-1.06l4.75-4.75a.75.75 0 0 1 1.06 0Z",
22
+ close: "M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z",
23
+ check: "M13.78 4.22a.75.75 0 0 1 0 1.06l-7 7a.75.75 0 0 1-1.06 0l-3.5-3.5a.75.75 0 1 1 1.06-1.06L6.25 10.7l6.47-6.47a.75.75 0 0 1 1.06 0Z",
24
+ "check-circle": "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm3.28 4.72a.75.75 0 0 0-1.06 0L7 8.94 5.78 7.72a.75.75 0 0 0-1.06 1.06l1.75 1.75a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0 0-1.06Z",
25
+ "x-circle": "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1ZM5.47 5.47a.75.75 0 0 0 0 1.06L6.94 8 5.47 9.47a.75.75 0 1 0 1.06 1.06L8 9.06l1.47 1.47a.75.75 0 1 0 1.06-1.06L9.06 8l1.47-1.47a.75.75 0 1 0-1.06-1.06L8 6.94 6.53 5.47a.75.75 0 0 0-1.06 0Z",
26
+ clock: "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm0 3a.75.75 0 0 0-.75.75v3.5c0 .2.08.39.22.53l2 2a.75.75 0 1 0 1.06-1.06L8.75 7.94V4.75A.75.75 0 0 0 8 4Z",
27
+ "minus-circle": "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1ZM4.75 7.25a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z",
28
+ "arrow-return": "M6.03 3.22a.75.75 0 0 1 0 1.06L4.56 5.75H10a3.75 3.75 0 0 1 0 7.5H5.5a.75.75 0 0 1 0-1.5H10a2.25 2.25 0 0 0 0-4.5H4.56l1.47 1.47a.75.75 0 1 1-1.06 1.06l-2.75-2.75a.75.75 0 0 1 0-1.06l2.75-2.75a.75.75 0 0 1 1.06 0Z",
29
+ "alert-circle": "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm0 3.25a.75.75 0 0 0-.75.75v3a.75.75 0 0 0 1.5 0V5A.75.75 0 0 0 8 4.25ZM8 10a.9.9 0 1 0 0 1.8A.9.9 0 0 0 8 10Z",
30
+ "alert-triangle": "M7.13 1.9a1 1 0 0 1 1.74 0l6 10.6a1 1 0 0 1-.87 1.5H2a1 1 0 0 1-.87-1.5l6-10.6ZM8 5.25a.75.75 0 0 0-.75.75v3a.75.75 0 0 0 1.5 0V6A.75.75 0 0 0 8 5.25ZM8 11a.9.9 0 1 0 0 1.8A.9.9 0 0 0 8 11Z",
31
+ info: "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm0 6a.75.75 0 0 0-.75.75v3.5a.75.75 0 0 0 1.5 0v-3.5A.75.75 0 0 0 8 7Zm0-2.8a.9.9 0 1 0 0 1.8.9.9 0 0 0 0-1.8Z",
32
+ more: "M3 6.75a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm5 0a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm5 0a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z",
33
+ copy: "M6 1.5A1.5 1.5 0 0 0 4.5 3v.5H3A1.5 1.5 0 0 0 1.5 5v8A1.5 1.5 0 0 0 3 14.5h7a1.5 1.5 0 0 0 1.5-1.5v-.5H13a1.5 1.5 0 0 0 1.5-1.5V3A1.5 1.5 0 0 0 13 1.5H6ZM11.5 11H13V3H6v.5h4A1.5 1.5 0 0 1 11.5 5v6Z",
34
+ external: "M9.75 1.5a.75.75 0 0 0 0 1.5h2.19L7.22 7.72a.75.75 0 1 0 1.06 1.06L13 4.06v2.19a.75.75 0 0 0 1.5 0V2.25a.75.75 0 0 0-.75-.75h-4ZM3 3.5A1.5 1.5 0 0 0 1.5 5v8A1.5 1.5 0 0 0 3 14.5h8a1.5 1.5 0 0 0 1.5-1.5V9.75a.75.75 0 0 0-1.5 0V13H3V5h3.25a.75.75 0 0 0 0-1.5H3Z",
35
+ edit: "M11.47 1.53a1.5 1.5 0 0 1 2.12 0l.88.88a1.5 1.5 0 0 1 0 2.12l-8 8a1.5 1.5 0 0 1-.7.4l-3.1.77a.75.75 0 0 1-.9-.9l.77-3.1a1.5 1.5 0 0 1 .4-.7l8.53-8.53Zm.6 2.03L10.9 4.72l.9.9 1.17-1.16-.9-.9Z",
36
+ trash: "M6 1.5a1 1 0 0 0-1 1V3H2.75a.75.75 0 0 0 0 1.5h.5l.6 8.4A1.5 1.5 0 0 0 5.35 14.5h5.3a1.5 1.5 0 0 0 1.5-1.4l.6-8.4h.5a.75.75 0 0 0 0-1.5H11v-.5a1 1 0 0 0-1-1H6Zm.75 5a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4A.75.75 0 0 1 6.75 6.5Zm2.5 0a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75Z",
37
+ filter: "M2.25 3A.75.75 0 0 1 3 2.25h10a.75.75 0 0 1 .58 1.22L9.5 8.52v4.23a.75.75 0 0 1-.43.68l-2 1a.75.75 0 0 1-1.07-.68V8.52L2.42 3.47A.75.75 0 0 1 2.25 3Z",
38
+ calendar: "M4.75 1a.75.75 0 0 1 .75.75V2.5h5V1.75a.75.75 0 0 1 1.5 0V2.5H13A1.5 1.5 0 0 1 14.5 4v9a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 13V4A1.5 1.5 0 0 1 3 2.5h1V1.75A.75.75 0 0 1 4.75 1ZM3 6.5V13h10V6.5H3Z",
39
+ bell: "M8 1.5a4.5 4.5 0 0 0-4.5 4.5v2.6l-1.2 2.4a.75.75 0 0 0 .67 1.09h10.06a.75.75 0 0 0 .67-1.09L12.5 8.6V6A4.5 4.5 0 0 0 8 1.5Zm-1.9 11.6a2 2 0 0 0 3.8 0H6.1Z",
40
+ gear: "M6.9 1.25a.75.75 0 0 0-.73.6l-.26 1.3a5.4 5.4 0 0 0-1.1.64l-1.27-.44a.75.75 0 0 0-.9.33l-1.1 1.9a.75.75 0 0 0 .17.95l1.01.86a5.6 5.6 0 0 0 0 1.27l-1 .86a.75.75 0 0 0-.18.95l1.1 1.9a.75.75 0 0 0 .9.33l1.27-.44c.34.26.7.47 1.1.64l.26 1.3a.75.75 0 0 0 .73.6h2.2a.75.75 0 0 0 .73-.6l.26-1.3c.4-.17.76-.38 1.1-.64l1.27.44a.75.75 0 0 0 .9-.33l1.1-1.9a.75.75 0 0 0-.17-.95l-1.01-.86a5.6 5.6 0 0 0 0-1.27l1-.86a.75.75 0 0 0 .18-.95l-1.1-1.9a.75.75 0 0 0-.9-.33l-1.27.44a5.4 5.4 0 0 0-1.1-.64l-.26-1.3a.75.75 0 0 0-.73-.6H6.9ZM8 10.25a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5Z",
41
+ help: "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm0 3.25c-1.3 0-2.25.85-2.25 2a.75.75 0 0 0 1.5 0c0-.22.25-.5.75-.5s.75.28.75.5c0 .2-.1.32-.5.6-.5.36-1 .82-1 1.65v.25a.75.75 0 0 0 1.5 0V8.5c0-.1.03-.16.4-.42.44-.32 1.1-.8 1.1-1.83 0-1.15-.95-2-2.25-2ZM8 10.5a.9.9 0 1 0 0 1.8.9.9 0 0 0 0-1.8Z",
42
+ collapse: "M2.5 2A1.5 1.5 0 0 0 1 3.5v9A1.5 1.5 0 0 0 2.5 14h11a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 13.5 2h-11Zm3 1.5h8v9h-8v-9Zm5.28 2.97a.75.75 0 0 1 0 1.06L9.81 8.5l.97.97a.75.75 0 1 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06l1.5-1.5a.75.75 0 0 1 1.06 0Z",
43
+ expand: "M2.5 2A1.5 1.5 0 0 0 1 3.5v9A1.5 1.5 0 0 0 2.5 14h11a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 13.5 2h-11Zm3 1.5h8v9h-8v-9Zm2.72 2.97a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1 0 1.06l-1.5 1.5a.75.75 0 1 1-1.06-1.06l.97-.97-.97-.97a.75.75 0 0 1 0-1.06Z",
44
+ check: "M13.78 4.22a.75.75 0 0 1 0 1.06l-6.5 6.5a.75.75 0 0 1-1.06 0l-3.25-3.25a.75.75 0 1 1 1.06-1.06l2.72 2.72 5.97-5.97a.75.75 0 0 1 1.06 0Z",
45
+ menu: "M2 4.25a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.25ZM2 8a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 8Zm.75 3a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5H2.75Z",
46
+ card: "M1 4.5A1.5 1.5 0 0 1 2.5 3h11A1.5 1.5 0 0 1 15 4.5V6H1V4.5ZM1 7.5h14v4A1.5 1.5 0 0 1 13.5 13h-11A1.5 1.5 0 0 1 1 11.5v-4Z",
47
+ "arrow-up-right": "M5.25 3a.75.75 0 0 0 0 1.5h5.19l-7.72 7.72a.75.75 0 1 0 1.06 1.06L11.5 5.56v5.19a.75.75 0 0 0 1.5 0v-7a.75.75 0 0 0-.75-.75h-7Z",
48
+ dot: "M8 5.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z",
49
+ sparkle: "M8 1.5l1.4 3.6 3.6 1.4-3.6 1.4L8 11.5 6.6 7.9 3 6.5l3.6-1.4L8 1.5Zm4.5 8 .7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7.7-1.8Z",
50
+ receipt: "M4 1.5A1.5 1.5 0 0 0 2.5 3v11.1a.6.6 0 0 0 .9.52l1.6-.92 1.6.92a.6.6 0 0 0 .6 0L8.8 13.7l1.6.92a.6.6 0 0 0 .6 0l1.6-.92 1.6.92a.6.6 0 0 0 .9-.52V3A1.5 1.5 0 0 0 13.6 1.5H4Zm1.25 3.75h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1 0-1.5Zm0 3h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1 0-1.5Z",
51
+ "plus-circle": "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm0 3.25a.75.75 0 0 0-.75.75v2.25H5a.75.75 0 0 0 0 1.5h2.25V11a.75.75 0 0 0 1.5 0V8.75H11a.75.75 0 0 0 0-1.5H8.75V5A.75.75 0 0 0 8 4.25Z",
52
+ "x-circle-outline": "M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1Zm0 1.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM5.97 5.97a.75.75 0 0 1 1.06 0L8 6.94l.97-.97a.75.75 0 1 1 1.06 1.06L9.06 8l.97.97a.75.75 0 1 1-1.06 1.06L8 9.06l-.97.97a.75.75 0 0 1-1.06-1.06L6.94 8l-.97-.97a.75.75 0 0 1 0-1.06Z",
53
+ download: "M8 1.5a.75.75 0 0 1 .75.75v6.69l2.22-2.22a.75.75 0 1 1 1.06 1.06l-3.5 3.5a.75.75 0 0 1-1.06 0l-3.5-3.5a.75.75 0 1 1 1.06-1.06l2.22 2.22V2.25A.75.75 0 0 1 8 1.5ZM2.25 11a.75.75 0 0 1 .75.75V13h10v-1.25a.75.75 0 0 1 1.5 0V13a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 13v-1.25a.75.75 0 0 1 .75-.75Z",
54
+ columns: "M2.5 2A1.5 1.5 0 0 0 1 3.5v9A1.5 1.5 0 0 0 2.5 14h11a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 13.5 2h-11Zm0 1.5h3v9h-3v-9Zm4.5 0h2v9H7v-9Zm3.5 0h3v9h-3v-9Z",
55
+ connect: "M4.5 1.5a3 3 0 0 1 2.9 2.25h1.2a3 3 0 1 1 0 1.5H7.4A3 3 0 0 1 5.25 7.4v1.2a3 3 0 1 1-1.5 0V7.4A3 3 0 0 1 4.5 1.5Zm0 8.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm7-8a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm0 7a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z",
56
+ reporting: "M2.25 1.5a.75.75 0 0 1 .75.75V13h10.75a.75.75 0 0 1 0 1.5H2.5a1 1 0 0 1-1-1V2.25a.75.75 0 0 1 .75-.75ZM13.28 4.22a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06 0L7 7.06 5.28 8.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0L9.25 7.19l2.97-2.97a.75.75 0 0 1 1.06 0Z",
57
+ apps: "M2.5 1.5h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm8 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm-8 8h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm8 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Z",
58
+ pin: "M9.97 1.22a.75.75 0 0 1 1.06 0l3.75 3.75a.75.75 0 0 1-.3 1.24l-2.1.7-2.02 2.02.4 2.4a.75.75 0 0 1-.21.66l-1 1a.75.75 0 0 1-1.06 0L6.25 10.8l-3.72 3.73a.75.75 0 0 1-1.06-1.06L5.19 9.75 3 7.56a.75.75 0 0 1 0-1.06l1-1a.75.75 0 0 1 .66-.21l2.4.4 2.02-2.02.7-2.1a.75.75 0 0 1 .19-.35Z",
59
+ note: "M3 1.5A1.5 1.5 0 0 0 1.5 3v10A1.5 1.5 0 0 0 3 14.5h6.5a.75.75 0 0 0 .53-.22l4.25-4.25a.75.75 0 0 0 .22-.53V3A1.5 1.5 0 0 0 13 1.5H3Zm2 3.5h6a.75.75 0 0 1 0 1.5H5A.75.75 0 0 1 5 5Zm0 3h3.5a.75.75 0 0 1 0 1.5H5A.75.75 0 0 1 5 8Z",
60
+ flask: "M5.75 1.5h4.5a.75.75 0 0 1 0 1.5H10v3.3l3.9 6.3A1.5 1.5 0 0 1 12.62 15H3.38a1.5 1.5 0 0 1-1.28-2.4L6 6.3V3h-.25a.75.75 0 0 1 0-1.5ZM7.5 3v3.5a.75.75 0 0 1-.11.4L5.6 9.75h4.8L8.6 6.9a.75.75 0 0 1-.1-.4V3h-1Z",
61
+ sparkline: "M1.5 8a.75.75 0 0 1 .75-.75h1.6l1.5-4.2a.75.75 0 0 1 1.42.03l2.3 7.66 1.3-3.2a.75.75 0 0 1 .7-.49h2.68a.75.75 0 0 1 0 1.5h-2.18l-1.88 4.64a.75.75 0 0 1-1.42-.07L6.02 5.6l-.99 2.9a.75.75 0 0 1-.71.5H2.25A.75.75 0 0 1 1.5 8Z",
62
+ spinner: "M8 1.5a6.5 6.5 0 1 0 6.5 6.5.75.75 0 0 1 1.5 0A8 8 0 1 1 8 0a.75.75 0 0 1 0 1.5Z",
63
+ };
64
+
65
+ const SVG = "http://www.w3.org/2000/svg";
66
+
67
+ /** Create one glyph as an inline SVG element (16 × 16 viewBox, `currentColor`). */
68
+ export function icon(name, className = "") {
69
+ const svg = document.createElementNS(SVG, "svg");
70
+ svg.setAttribute("viewBox", "0 0 16 16");
71
+ svg.setAttribute("aria-hidden", "true");
72
+ svg.setAttribute("focusable", "false");
73
+ svg.classList.add("icon");
74
+ if (className) for (const item of className.split(/\s+/)) if (item) svg.classList.add(item);
75
+ const path = document.createElementNS(SVG, "path");
76
+ path.setAttribute("d", PATHS[name] ?? PATHS["alert-circle"]);
77
+ path.setAttribute("fill", "currentColor");
78
+ path.setAttribute("fill-rule", "evenodd");
79
+ svg.append(path);
80
+ return svg;
81
+ }
82
+
83
+ /** Replace every `<span data-icon="name">` placeholder in `root` with the matching glyph. */
84
+ export function hydrateIcons(root = document) {
85
+ for (const placeholder of root.querySelectorAll("[data-icon]")) {
86
+ placeholder.replaceWith(icon(placeholder.dataset.icon, placeholder.className));
87
+ }
88
+ }
89
+
90
+ export const ICON_NAMES = Object.freeze(Object.keys(PATHS));