@tollbooth-dpyc/web 1.2.0 → 1.3.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.
- package/README.md +11 -1
- package/dist/client.d.ts +0 -8
- package/dist/client.js +0 -2
- package/dist/react/CouponsPanel.d.ts +11 -2
- package/dist/react/CouponsPanel.js +6 -5
- package/dist/react/PagedTable.d.ts +7 -2
- package/dist/react/PagedTable.js +8 -5
- package/dist/react/TableFilter.d.ts +10 -2
- package/dist/react/TableFilter.js +58 -9
- package/dist/react/WalletCard.d.ts +15 -3
- package/dist/react/WalletCard.js +10 -5
- package/dist/react/WalletPage.d.ts +11 -3
- package/dist/react/WalletPage.js +19 -9
- package/dist/react/index.d.ts +1 -1
- package/dist/react/panelPlacement.d.ts +26 -0
- package/dist/react/panelPlacement.js +44 -0
- package/dist/standardTools.d.ts +27 -22
- package/dist/standardTools.js +48 -53
- package/dist/table.d.ts +10 -0
- package/dist/table.js +23 -0
- package/dist/wallet.d.ts +18 -0
- package/dist/wallet.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ bootstrapTheme(); // main.tsx, after configureTollbooth: <prefix>:the
|
|
|
87
87
|
formatDateTime={(iso) => formatInZone(iso, zone)}
|
|
88
88
|
before={<FundingStatus />}
|
|
89
89
|
coupons={{ classNames: { root: "card", chip: "chip" } }}
|
|
90
|
-
classNames={{ section: "card p-5", figure: "text-3xl", chip: "chip", chipActive: "chip-on" }}
|
|
90
|
+
classNames={{ section: "card p-5", figure: "text-3xl", chip: "chip", chipActive: "chip-on", primary: "chip-accent" }}
|
|
91
91
|
/>
|
|
92
92
|
|
|
93
93
|
<ThemeToggle themes={["dark", "light", "system"]} classNames={{ chip: "chip", active: "chip-on" }} />
|
|
@@ -98,8 +98,18 @@ bootstrapTheme(); // main.tsx, after configureTollbooth: <prefix>:the
|
|
|
98
98
|
onClear={clearAll}
|
|
99
99
|
classNames={{ root: "flex gap-2", input: "field", chip: "chip" }}
|
|
100
100
|
/>
|
|
101
|
+
|
|
102
|
+
<SortHeader as="div" label="Date" col="date" activeCol={col} dir={dir} onSort={sortBy} /> // a div grid
|
|
101
103
|
```
|
|
102
104
|
|
|
105
|
+
A preset amount in `WalletPage` / `WalletCard` only fills the amount; the
|
|
106
|
+
invoice is made by the explicit "Create invoice". `classNames.primary` styles
|
|
107
|
+
the action that moves a top-up forward (Redeem in `CouponsPanel`); every other
|
|
108
|
+
action stays a chip. `TableFilter`'s panel slides itself back on screen on a
|
|
109
|
+
phone; `clearPlacement="panel"` puts Clear inside it. Every standard-tool
|
|
110
|
+
wrapper takes the same optional `CallOptions` as `callTool`, last
|
|
111
|
+
(`listCanonicalIdentities({ bestEffort: true })`).
|
|
112
|
+
|
|
103
113
|
`useTopUp` is the wallet's top-up on its own (purchase_credits → invoice →
|
|
104
114
|
check_payment, polled while the tab is visible) for a site drawing its own.
|
|
105
115
|
|
package/dist/client.d.ts
CHANGED
|
@@ -28,14 +28,6 @@ export interface CallOptions {
|
|
|
28
28
|
* patron out, whatever it gets back.
|
|
29
29
|
*/
|
|
30
30
|
bestEffort?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* A background read — a badge, a poll — that must not send the patron to
|
|
33
|
-
* the sign-in gate. A proof bounce still throws `ProofRequiredError`, but
|
|
34
|
-
* the session is left as it is and `onProofExpired` is not told; the next
|
|
35
|
-
* call the patron makes themselves meets the lapse and re-arms sign-in.
|
|
36
|
-
* Opt-in: by default a bounce signs the patron out and re-arms the gate.
|
|
37
|
-
*/
|
|
38
|
-
quietProof?: boolean;
|
|
39
31
|
timeoutMs?: number;
|
|
40
32
|
}
|
|
41
33
|
export interface ToolAnswer<T> {
|
package/dist/client.js
CHANGED
|
@@ -166,8 +166,6 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
|
|
|
166
166
|
if (!opts.bestEffort) {
|
|
167
167
|
const bounced = proofBounceMessage(payload);
|
|
168
168
|
if (bounced !== null) {
|
|
169
|
-
if (opts.quietProof)
|
|
170
|
-
throw new ProofRequiredError(bounced);
|
|
171
169
|
// The token just refused is the same one the recent-login shortcut
|
|
172
170
|
// would replay, so it goes too, or a returning patron re-bounces.
|
|
173
171
|
const npub = getStoredNpub();
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Mechanics only — the calls (list_my_coupons, redeem_coupon, forget_coupon),
|
|
6
6
|
* the states and the words. Every visual choice is the site's, through
|
|
7
7
|
* `classNames`; with none, the panel is plain markup inheriting from the page.
|
|
8
|
-
* Actions (Redeem, Remove) are chips: `classNames.chip
|
|
8
|
+
* Actions (Redeem, Remove) are chips: `classNames.chip`; Redeem, the primary
|
|
9
|
+
* action, takes `classNames.primary` instead when a site gives it its accent.
|
|
9
10
|
*/
|
|
10
11
|
import { type ReactNode } from "react";
|
|
11
12
|
import { type PatronCoupon } from "../standardTools.ts";
|
|
@@ -18,6 +19,10 @@ export interface CouponsPanelClassNames {
|
|
|
18
19
|
input?: string;
|
|
19
20
|
/** Every action: Redeem, Remove. */
|
|
20
21
|
chip?: string;
|
|
22
|
+
/** Redeem, in place of `chip`, for a site that gives it its accent. Default: `chip`. */
|
|
23
|
+
primary?: string;
|
|
24
|
+
/** The sub-labels `formHeading` and `listHeading`. */
|
|
25
|
+
subheading?: string;
|
|
21
26
|
/** Added to the message after a redeem, with `ok` or `error`. */
|
|
22
27
|
message?: string;
|
|
23
28
|
ok?: string;
|
|
@@ -38,6 +43,10 @@ export interface CouponsPanelProps {
|
|
|
38
43
|
intro?: ReactNode;
|
|
39
44
|
/** Shown when the patron holds no coupons. */
|
|
40
45
|
empty?: ReactNode;
|
|
46
|
+
/** A sub-label above the code box, e.g. "Redeem a code". Default none. */
|
|
47
|
+
formHeading?: ReactNode;
|
|
48
|
+
/** A sub-label above the held coupons, e.g. "Active". Shown only when there are some. Default none. */
|
|
49
|
+
listHeading?: ReactNode;
|
|
41
50
|
placeholder?: string;
|
|
42
51
|
/** Content of the Redeem chip. */
|
|
43
52
|
redeemLabel?: ReactNode;
|
|
@@ -49,4 +58,4 @@ export interface CouponsPanelProps {
|
|
|
49
58
|
/** Draw a coupon's row yourself; `forget` removes it (after confirmForget). */
|
|
50
59
|
renderCoupon?: (coupon: PatronCoupon, forget: () => void) => ReactNode;
|
|
51
60
|
}
|
|
52
|
-
export default function CouponsPanel({ heading, intro, empty, placeholder, redeemLabel, forgetLabel, confirmForget, classNames: c, renderCoupon, }: CouponsPanelProps): import("react").JSX.Element;
|
|
61
|
+
export default function CouponsPanel({ heading, intro, empty, formHeading, listHeading, placeholder, redeemLabel, forgetLabel, confirmForget, classNames: c, renderCoupon, }: CouponsPanelProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* The patron's coupons: redeem an operator's code once, see what is held, and
|
|
4
4
|
* take one off the list.
|
|
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* Mechanics only — the calls (list_my_coupons, redeem_coupon, forget_coupon),
|
|
7
7
|
* the states and the words. Every visual choice is the site's, through
|
|
8
8
|
* `classNames`; with none, the panel is plain markup inheriting from the page.
|
|
9
|
-
* Actions (Redeem, Remove) are chips: `classNames.chip
|
|
9
|
+
* Actions (Redeem, Remove) are chips: `classNames.chip`; Redeem, the primary
|
|
10
|
+
* action, takes `classNames.primary` instead when a site gives it its accent.
|
|
10
11
|
*/
|
|
11
12
|
import { useCallback, useEffect, useState } from "react";
|
|
12
13
|
import { forgetCoupon, listMyCoupons, redeemCoupon } from "../standardTools.js";
|
|
@@ -21,7 +22,7 @@ function cx(...parts) {
|
|
|
21
22
|
const s = parts.filter(Boolean).join(" ");
|
|
22
23
|
return s || undefined;
|
|
23
24
|
}
|
|
24
|
-
export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_INTRO, empty = DEFAULT_EMPTY, placeholder = "Coupon code", redeemLabel = "Redeem", forgetLabel = "Remove", confirmForget = defaultConfirm, classNames: c = {}, renderCoupon, }) {
|
|
25
|
+
export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_INTRO, empty = DEFAULT_EMPTY, formHeading, listHeading, placeholder = "Coupon code", redeemLabel = "Redeem", forgetLabel = "Remove", confirmForget = defaultConfirm, classNames: c = {}, renderCoupon, }) {
|
|
25
26
|
const [coupons, setCoupons] = useState([]);
|
|
26
27
|
const [loading, setLoading] = useState(true);
|
|
27
28
|
const [loadError, setLoadError] = useState(null);
|
|
@@ -86,10 +87,10 @@ export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_I
|
|
|
86
87
|
setMsg({ ok: false, text: e.message });
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
|
-
return (_jsxs("section", { className: c.root, children: [heading && _jsx("div", { className: c.heading, children: heading }), intro && _jsx("p", { className: c.intro, children: intro }), _jsxs("div", { className: c.form, children: [_jsx("input", { type: "text", value: code, onChange: (e) => setCode(e.target.value.toUpperCase()), onKeyDown: (e) => {
|
|
90
|
+
return (_jsxs("section", { className: c.root, children: [heading && _jsx("div", { className: c.heading, children: heading }), intro && _jsx("p", { className: c.intro, children: intro }), formHeading && _jsx("div", { className: c.subheading, children: formHeading }), _jsxs("div", { className: c.form, children: [_jsx("input", { type: "text", value: code, onChange: (e) => setCode(e.target.value.toUpperCase()), onKeyDown: (e) => {
|
|
90
91
|
if (e.key === "Enter")
|
|
91
92
|
void redeem();
|
|
92
|
-
}, placeholder: placeholder, "aria-label": "Coupon code", autoCapitalize: "characters", autoCorrect: "off", spellCheck: false, disabled: redeeming, className: c.input }), _jsx("button", { type: "button", onClick: () => void redeem(), disabled: redeeming || !code.trim(), className: c.chip, children: redeeming ? "Redeeming…" : redeemLabel })] }), msg && (_jsx("p", { role: "status", className: cx(c.message, msg.ok ? c.ok : c.error), children: msg.text })), loading ? (_jsx("p", { className: c.loading, children: "Loading\u2026" })) : loadError ? (_jsx("p", { className: cx(c.message, c.error), children: loadError })) : coupons.length === 0 ? (empty && _jsx("p", { className: c.empty, children: empty })) : (_jsx("ul", { className: c.list, children: coupons.map((coupon) => renderCoupon ? (_jsx("li", { className: c.row, children: renderCoupon(coupon, () => void forget(coupon)) }, coupon.coupon_id)) : (_jsx(CouponRow, { coupon: coupon, c: c, forgetLabel: forgetLabel, onForget: () => void forget(coupon) }, coupon.coupon_id))) }))] }));
|
|
93
|
+
}, placeholder: placeholder, "aria-label": "Coupon code", autoCapitalize: "characters", autoCorrect: "off", spellCheck: false, disabled: redeeming, className: c.input }), _jsx("button", { type: "button", onClick: () => void redeem(), disabled: redeeming || !code.trim(), className: c.primary ?? c.chip, children: redeeming ? "Redeeming…" : redeemLabel })] }), msg && (_jsx("p", { role: "status", className: cx(c.message, msg.ok ? c.ok : c.error), children: msg.text })), loading ? (_jsx("p", { className: c.loading, children: "Loading\u2026" })) : loadError ? (_jsx("p", { className: cx(c.message, c.error), children: loadError })) : coupons.length === 0 ? (empty && _jsx("p", { className: c.empty, children: empty })) : (_jsxs(_Fragment, { children: [listHeading && _jsx("div", { className: c.subheading, children: listHeading }), _jsx("ul", { className: c.list, children: coupons.map((coupon) => renderCoupon ? (_jsx("li", { className: c.row, children: renderCoupon(coupon, () => void forget(coupon)) }, coupon.coupon_id)) : (_jsx(CouponRow, { coupon: coupon, c: c, forgetLabel: forgetLabel, onForget: () => void forget(coupon) }, coupon.coupon_id))) })] }))] }));
|
|
93
94
|
}
|
|
94
95
|
function CouponRow({ coupon, c, forgetLabel, onForget, }) {
|
|
95
96
|
const active = coupon.status === "active";
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
11
|
import { type SortDir } from "../table.ts";
|
|
12
12
|
export interface SortHeaderClassNames {
|
|
13
|
-
/** The <th
|
|
13
|
+
/** The header cell: the <th>, or the <div> when `as="div"`. */
|
|
14
14
|
cell?: string;
|
|
15
15
|
/** The button inside a sortable header. */
|
|
16
16
|
button?: string;
|
|
@@ -28,10 +28,15 @@ export interface SortHeaderProps {
|
|
|
28
28
|
onSort: (col: string, dir: SortDir) => void;
|
|
29
29
|
/** The direction a newly chosen column starts in. Default "desc". */
|
|
30
30
|
initialDir?: SortDir;
|
|
31
|
+
/**
|
|
32
|
+
* The element: a <th> in a table (default), or a <div role="columnheader">
|
|
33
|
+
* for a grid of divs — the sort, the button and aria-sort are the same.
|
|
34
|
+
*/
|
|
35
|
+
as?: "th" | "div";
|
|
31
36
|
classNames?: SortHeaderClassNames;
|
|
32
37
|
}
|
|
33
38
|
/** A header cell. Tapping the sorted column flips it; another column starts at `initialDir`. */
|
|
34
|
-
export declare function SortHeader({ label, col, activeCol, dir, onSort, initialDir, classNames: c }: SortHeaderProps): import("react").JSX.Element;
|
|
39
|
+
export declare function SortHeader({ label, col, activeCol, dir, onSort, initialDir, as, classNames: c, }: SortHeaderProps): import("react").JSX.Element;
|
|
35
40
|
export interface PageControlsClassNames {
|
|
36
41
|
root?: string;
|
|
37
42
|
/** Every control. */
|
package/dist/react/PagedTable.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { clampPage, lastPage, nextSort } from "../table.js";
|
|
2
|
+
import { ariaSort, clampPage, lastPage, nextSort } from "../table.js";
|
|
3
3
|
function cx(...parts) {
|
|
4
4
|
const s = parts.filter(Boolean).join(" ");
|
|
5
5
|
return s || undefined;
|
|
6
6
|
}
|
|
7
7
|
/** A header cell. Tapping the sorted column flips it; another column starts at `initialDir`. */
|
|
8
|
-
export function SortHeader({ label, col, activeCol, dir, onSort, initialDir = "desc", classNames: c = {} }) {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export function SortHeader({ label, col, activeCol, dir, onSort, initialDir = "desc", as = "th", classNames: c = {}, }) {
|
|
9
|
+
const Cell = as;
|
|
10
|
+
const role = as === "div" ? "columnheader" : undefined;
|
|
11
|
+
if (!col) {
|
|
12
|
+
return (_jsx(Cell, { role: role, className: c.cell, children: label }));
|
|
13
|
+
}
|
|
11
14
|
const active = col === activeCol;
|
|
12
|
-
return (_jsx(
|
|
15
|
+
return (_jsx(Cell, { role: role, className: c.cell, "aria-sort": ariaSort(col, activeCol, dir), children: _jsxs("button", { type: "button", onClick: () => {
|
|
13
16
|
const next = nextSort(col, activeCol, dir, initialDir);
|
|
14
17
|
onSort(next.col, next.dir);
|
|
15
18
|
}, className: cx(c.button, active && c.active), children: [label, active && (_jsx("span", { "aria-hidden": true, className: c.arrow, children: dir === "desc" ? " ▾" : " ▴" }))] }) }));
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* the query (server-side, or `filterRows` in the browser), so paging reflects
|
|
11
11
|
* the filtered set. The search is applied on Enter or its chip, never on each
|
|
12
12
|
* keystroke. Mechanics only: every visual choice is the site's, through
|
|
13
|
-
* `classNames`; the actions are chips.
|
|
13
|
+
* `classNames`; the actions are chips. The panel is kept on screen: while open,
|
|
14
|
+
* it is slid sideways (`reclampPanel`) so no part of it hangs past either edge
|
|
15
|
+
* of a phone, however its mark grows or the window changes.
|
|
14
16
|
*/
|
|
15
17
|
import { type ReactNode } from "react";
|
|
16
18
|
export interface DateFieldOption {
|
|
@@ -86,8 +88,14 @@ export interface TableFilterProps<F extends object> {
|
|
|
86
88
|
questions?: TableFilterQuestions<F>;
|
|
87
89
|
/** Turn everything off. */
|
|
88
90
|
onClear: () => void;
|
|
91
|
+
/**
|
|
92
|
+
* Where the Clear chip goes: beside the other controls (default), or inside
|
|
93
|
+
* the questions panel. "panel" needs a `questions` group; without one it is
|
|
94
|
+
* beside.
|
|
95
|
+
*/
|
|
96
|
+
clearPlacement?: "beside" | "panel";
|
|
89
97
|
searchIcon?: ReactNode;
|
|
90
98
|
clearLabel?: ReactNode;
|
|
91
99
|
classNames?: TableFilterClassNames;
|
|
92
100
|
}
|
|
93
|
-
export default function TableFilter<F extends object>({ search, dates, questions, onClear, searchIcon, clearLabel, classNames: c, }: TableFilterProps<F>): import("react").JSX.Element;
|
|
101
|
+
export default function TableFilter<F extends object>({ search, dates, questions, onClear, clearPlacement, searchIcon, clearLabel, classNames: c, }: TableFilterProps<F>): import("react").JSX.Element;
|
|
@@ -11,18 +11,25 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* the query (server-side, or `filterRows` in the browser), so paging reflects
|
|
12
12
|
* the filtered set. The search is applied on Enter or its chip, never on each
|
|
13
13
|
* keystroke. Mechanics only: every visual choice is the site's, through
|
|
14
|
-
* `classNames`; the actions are chips.
|
|
14
|
+
* `classNames`; the actions are chips. The panel is kept on screen: while open,
|
|
15
|
+
* it is slid sideways (`reclampPanel`) so no part of it hangs past either edge
|
|
16
|
+
* of a phone, however its mark grows or the window changes.
|
|
15
17
|
*/
|
|
16
|
-
import { useEffect, useRef, useState } from "react";
|
|
18
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
17
19
|
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
18
20
|
import { filterActive } from "../table.js";
|
|
21
|
+
import { oncePerFrame, reclampPanel } from "./panelPlacement.js";
|
|
22
|
+
/** The panel keeps this many px from each edge of the viewport. */
|
|
23
|
+
const EDGE = 8;
|
|
19
24
|
function cx(...parts) {
|
|
20
25
|
const s = parts.filter(Boolean).join(" ");
|
|
21
26
|
return s || undefined;
|
|
22
27
|
}
|
|
23
|
-
export default function TableFilter({ search, dates, questions, onClear, searchIcon = _jsx(Search, { size: "1em", "aria-hidden": true }), clearLabel = (_jsxs(_Fragment, { children: [_jsx(X, { size: "1em", "aria-hidden": true }), " Clear"] })), classNames: c = {}, }) {
|
|
28
|
+
export default function TableFilter({ search, dates, questions, onClear, clearPlacement = "beside", searchIcon = _jsx(Search, { size: "1em", "aria-hidden": true }), clearLabel = (_jsxs(_Fragment, { children: [_jsx(X, { size: "1em", "aria-hidden": true }), " Clear"] })), classNames: c = {}, }) {
|
|
24
29
|
const active = filterActive({ search: search?.value, dateFrom: dates?.from, dateTo: dates?.to }, questions?.summary ?? "");
|
|
25
|
-
|
|
30
|
+
const clear = active ? (_jsx("button", { type: "button", onClick: onClear, className: c.chip, children: clearLabel })) : null;
|
|
31
|
+
const inPanel = clearPlacement === "panel" && !!questions;
|
|
32
|
+
return (_jsxs("div", { className: c.root, children: [search && _jsx(SearchBox, { search: search, icon: searchIcon, c: c }), dates && _jsx(DateRange, { dates: dates, c: c }), questions && _jsx(QuestionPanel, { q: questions, clear: inPanel ? clear : null, c: c }), !inPanel && clear] }));
|
|
26
33
|
}
|
|
27
34
|
function SearchBox({ search, icon, c }) {
|
|
28
35
|
// Edited freely, applied on Enter or the chip; follows the applied value
|
|
@@ -37,9 +44,51 @@ function SearchBox({ search, icon, c }) {
|
|
|
37
44
|
function DateRange({ dates, c }) {
|
|
38
45
|
return (_jsxs("div", { className: c.dates, children: [dates.fields && dates.fields.length > 0 && dates.onField && (_jsx("select", { value: dates.field, onChange: (e) => dates.onField(e.target.value), title: "Which date the range filters", "aria-label": "Date to filter", className: c.select, children: dates.fields.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })), _jsx("input", { type: "date", value: dates.from, max: dates.to || undefined, onChange: (e) => dates.onFrom(e.target.value), title: "From (inclusive)", "aria-label": "From date", className: c.date }), _jsx("span", { className: c.separator, "aria-hidden": true, children: "\u2013" }), _jsx("input", { type: "date", value: dates.to, min: dates.from || undefined, onChange: (e) => dates.onTo(e.target.value), title: "To (inclusive)", "aria-label": "To date", className: c.date })] }));
|
|
39
46
|
}
|
|
40
|
-
function QuestionPanel({ q, c }) {
|
|
47
|
+
function QuestionPanel({ q, clear, c, }) {
|
|
41
48
|
const [open, setOpen] = useState(false);
|
|
42
49
|
const box = useRef(null);
|
|
50
|
+
const panel = useRef(null);
|
|
51
|
+
const mark = useRef(null);
|
|
52
|
+
// Kept on screen, and kept there while open: the mark widens as questions
|
|
53
|
+
// are answered, the Clear chip joins the panel, the window turns or scrolls —
|
|
54
|
+
// each moves the panel, so each re-places it (at most once a frame). Slid by
|
|
55
|
+
// the `translate` property (a site's own `transform` is left alone); only a
|
|
56
|
+
// panel wider than the viewport gets a max-width.
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
const el = panel.current;
|
|
59
|
+
if (!open || !el)
|
|
60
|
+
return;
|
|
61
|
+
let dx = 0;
|
|
62
|
+
const place = () => {
|
|
63
|
+
const room = document.documentElement.clientWidth;
|
|
64
|
+
el.style.maxWidth = "";
|
|
65
|
+
if (el.getBoundingClientRect().width > room - 2 * EDGE)
|
|
66
|
+
el.style.maxWidth = `${room - 2 * EDGE}px`;
|
|
67
|
+
const r = el.getBoundingClientRect();
|
|
68
|
+
const next = reclampPanel(dx, r.left, r.width, room, EDGE);
|
|
69
|
+
if (next !== dx) {
|
|
70
|
+
dx = next;
|
|
71
|
+
el.style.translate = dx ? `${dx}px 0` : "";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
place();
|
|
75
|
+
const frame = oncePerFrame(place);
|
|
76
|
+
const sized = new ResizeObserver(frame.schedule);
|
|
77
|
+
sized.observe(el);
|
|
78
|
+
for (const around of [mark.current, box.current])
|
|
79
|
+
if (around)
|
|
80
|
+
sized.observe(around);
|
|
81
|
+
window.addEventListener("resize", frame.schedule);
|
|
82
|
+
window.addEventListener("orientationchange", frame.schedule);
|
|
83
|
+
window.addEventListener("scroll", frame.schedule, { capture: true, passive: true });
|
|
84
|
+
return () => {
|
|
85
|
+
frame.cancel();
|
|
86
|
+
sized.disconnect();
|
|
87
|
+
window.removeEventListener("resize", frame.schedule);
|
|
88
|
+
window.removeEventListener("orientationchange", frame.schedule);
|
|
89
|
+
window.removeEventListener("scroll", frame.schedule, { capture: true });
|
|
90
|
+
};
|
|
91
|
+
}, [open]);
|
|
43
92
|
// Dismissed by a tap outside or Escape.
|
|
44
93
|
useEffect(() => {
|
|
45
94
|
if (!open)
|
|
@@ -61,8 +110,8 @@ function QuestionPanel({ q, c }) {
|
|
|
61
110
|
}, [open]);
|
|
62
111
|
const on = q.summary !== "";
|
|
63
112
|
const set = (key, v) => q.onChange({ ...q.value, [key]: v });
|
|
64
|
-
return (_jsxs("div", { className: c.questions, ref: box, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, title: on ? `Filtered: ${q.summary}` : "Filter", "aria-label": on ? `Filtered: ${q.summary}` : "Filter", className: cx(c.chip, on && c.chipActive), children: [q.icon ?? _jsx(SlidersHorizontal, { size: "1em", "aria-hidden": true }), on && _jsx("span", { className: c.summary, children: q.summary })] }), open && (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
return (_jsxs("div", { className: c.questions, ref: box, children: [_jsxs("button", { ref: mark, type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, title: on ? `Filtered: ${q.summary}` : "Filter", "aria-label": on ? `Filtered: ${q.summary}` : "Filter", className: cx(c.chip, on && c.chipActive), children: [q.icon ?? _jsx(SlidersHorizontal, { size: "1em", "aria-hidden": true }), on && _jsx("span", { className: c.summary, children: q.summary })] }), open && (_jsxs("div", { className: c.panel, ref: panel, role: "group", "aria-label": "Filter", children: [q.questions.map((question) => {
|
|
114
|
+
const id = `tf-${String(question.key)}`;
|
|
115
|
+
return question.kind === "toggle" ? (_jsxs("label", { className: c.question, children: [_jsx("input", { type: "checkbox", checked: !!q.value[question.key], onChange: (e) => set(question.key, e.target.checked), className: c.checkbox }), " ", question.label] }, id)) : (_jsxs("div", { className: c.question, children: [_jsx("label", { htmlFor: id, children: question.label }), " ", _jsx("input", { id: id, inputMode: "numeric", value: String(q.value[question.key] ?? ""), onChange: (e) => set(question.key, e.target.value), className: c.number }), " ", _jsx("span", { className: c.unit, children: question.unit })] }, id));
|
|
116
|
+
}), clear] }))] }));
|
|
68
117
|
}
|
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
* Every figure is the service's answer or a dash. A balance nobody has read is
|
|
5
5
|
* unknown, and unknown is not zero: showing 0 to someone with funds would send
|
|
6
6
|
* them to buy credits they already own. The card offers round numbers to pay
|
|
7
|
-
* in and never guesses what anything costs.
|
|
7
|
+
* in and never guesses what anything costs. Tapping one only chooses it; the
|
|
8
|
+
* invoice is made by "Create invoice".
|
|
8
9
|
*
|
|
9
10
|
* The compact card. `WalletPage` is the full account view; both run the same
|
|
10
11
|
* top-up (`useTopUp`), which checks an open invoice on its own until it settles.
|
|
11
12
|
*/
|
|
12
|
-
export
|
|
13
|
+
export interface WalletCardClassNames {
|
|
14
|
+
/** Each preset amount, and the invoice's check and cancel. Default: the package's outlined chip. */
|
|
15
|
+
chip?: string;
|
|
16
|
+
/** Added to the chosen preset. Default: an accent ring. */
|
|
17
|
+
chipActive?: string;
|
|
18
|
+
/** The action that moves a top-up forward — "Create invoice", then "Pay". Default: the accent button. */
|
|
19
|
+
primary?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WalletCardProps {
|
|
22
|
+
/** Preset amounts, in sats. A tap fills the amount; it does not invoice. */
|
|
13
23
|
topUps?: number[];
|
|
14
|
-
|
|
24
|
+
classNames?: WalletCardClassNames;
|
|
25
|
+
}
|
|
26
|
+
export default function WalletCard({ topUps, classNames }: WalletCardProps): import("react").JSX.Element;
|
package/dist/react/WalletCard.js
CHANGED
|
@@ -5,20 +5,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* Every figure is the service's answer or a dash. A balance nobody has read is
|
|
6
6
|
* unknown, and unknown is not zero: showing 0 to someone with funds would send
|
|
7
7
|
* them to buy credits they already own. The card offers round numbers to pay
|
|
8
|
-
* in and never guesses what anything costs.
|
|
8
|
+
* in and never guesses what anything costs. Tapping one only chooses it; the
|
|
9
|
+
* invoice is made by "Create invoice".
|
|
9
10
|
*
|
|
10
11
|
* The compact card. `WalletPage` is the full account view; both run the same
|
|
11
12
|
* top-up (`useTopUp`), which checks an open invoice on its own until it settles.
|
|
12
13
|
*/
|
|
13
|
-
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
import { useCallback, useEffect, useReducer, useState } from "react";
|
|
14
15
|
import { RefreshCw, Zap } from "lucide-react";
|
|
15
16
|
import { tollboothConfig } from "../config.js";
|
|
16
17
|
import { checkBalance } from "../standardTools.js";
|
|
17
|
-
import { formatSats } from "../wallet.js";
|
|
18
|
+
import { amountReducer, formatSats, parseSats, presetChosen } from "../wallet.js";
|
|
18
19
|
import { card, ghost, iconButton, muted, primary } from "./ui.js";
|
|
19
20
|
import { useTopUp } from "./useTopUp.js";
|
|
20
21
|
const DEFAULT_TOP_UPS = [1_000, 5_000, 20_000];
|
|
21
|
-
|
|
22
|
+
const cx = (...parts) => parts.filter(Boolean).join(" ");
|
|
23
|
+
export default function WalletCard({ topUps = DEFAULT_TOP_UPS, classNames = {} }) {
|
|
24
|
+
const c = { chip: ghost, chipActive: "ring-2 ring-[var(--tb-accent)]", primary, ...classNames };
|
|
22
25
|
const { appName } = tollboothConfig();
|
|
23
26
|
const [balance, setBalance] = useState(null);
|
|
24
27
|
const [reachable, setReachable] = useState(true);
|
|
@@ -33,6 +36,8 @@ export default function WalletCard({ topUps = DEFAULT_TOP_UPS }) {
|
|
|
33
36
|
}, []);
|
|
34
37
|
useEffect(load, [load]);
|
|
35
38
|
const { state, create, check, cancel } = useTopUp({ onSettled: load });
|
|
39
|
+
const [amount, pick] = useReducer(amountReducer, "");
|
|
40
|
+
const sats = parseSats(amount);
|
|
36
41
|
const busy = state.phase === "creating" || (state.phase === "awaiting" && state.checking);
|
|
37
42
|
const invoice = state.phase === "awaiting" ? state.invoice : null;
|
|
38
43
|
const msg = state.phase === "settled"
|
|
@@ -44,5 +49,5 @@ export default function WalletCard({ topUps = DEFAULT_TOP_UPS }) {
|
|
|
44
49
|
: state.phase === "idle"
|
|
45
50
|
? (state.message ?? "")
|
|
46
51
|
: "";
|
|
47
|
-
return (_jsxs("section", { className: `${card} p-4`, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: `text-sm ${muted}`, children: "Balance" }), _jsx("button", { type: "button", onClick: load, title: "Refresh", "aria-label": "Refresh balance", className: iconButton, children: _jsx(RefreshCw, { size: 14 }) })] }), _jsxs("div", { className: "mt-1 flex items-baseline gap-2", children: [_jsx("span", { className: "text-2xl font-semibold tabular-nums", children: formatSats(reachable ? balance : null) }), _jsx("span", { className: `text-sm ${muted}`, children: "sats" })] }), !reachable && _jsxs("p", { className: `mt-2 text-xs ${muted}`, children: [appName, " did not answer, so no balance is shown."] }), !invoice ? (
|
|
52
|
+
return (_jsxs("section", { className: `${card} p-4`, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: `text-sm ${muted}`, children: "Balance" }), _jsx("button", { type: "button", onClick: load, title: "Refresh", "aria-label": "Refresh balance", className: iconButton, children: _jsx(RefreshCw, { size: 14 }) })] }), _jsxs("div", { className: "mt-1 flex items-baseline gap-2", children: [_jsx("span", { className: "text-2xl font-semibold tabular-nums", children: formatSats(reachable ? balance : null) }), _jsx("span", { className: `text-sm ${muted}`, children: "sats" })] }), !reachable && _jsxs("p", { className: `mt-2 text-xs ${muted}`, children: [appName, " did not answer, so no balance is shown."] }), !invoice ? (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsx("div", { className: "flex gap-2", children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: busy, "aria-pressed": presetChosen(amount, n), onClick: () => pick({ type: "preset", sats: n }), className: cx(c.chip, presetChosen(amount, n) && c.chipActive), children: ["+", n.toLocaleString("en-US")] }, n))) }), _jsx("button", { type: "button", disabled: busy || !sats, onClick: () => sats && create(sats), className: c.primary, children: "Create invoice" })] })) : (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsxs("a", { href: invoice.checkoutLink ?? (invoice.bolt11 ? `lightning:${invoice.bolt11}` : "#"), target: "_blank", rel: "noreferrer", className: cx(c.primary, "flex items-center justify-center gap-2"), children: [_jsx(Zap, { size: 16 }), " Pay ", invoice.sats.toLocaleString("en-US"), " sats"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", disabled: busy, onClick: check, className: c.chip, children: "I've paid \u2014 check" }), _jsx("button", { type: "button", onClick: cancel, className: cx(c.chip, "w-auto px-4"), children: "Cancel" })] })] })), msg && _jsx("p", { className: `mt-3 text-xs ${muted}`, children: msg })] }));
|
|
48
53
|
}
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* purchase_credits → check_payment, account_statement, the coupon tools),
|
|
7
7
|
* holds the states and writes the words; every visual choice belongs to the
|
|
8
8
|
* site, through `classNames` — with none it is plain markup that inherits from
|
|
9
|
-
* the page. Actions are chips (`classNames.chip`)
|
|
9
|
+
* the page. Actions are chips (`classNames.chip`); the one that moves a top-up
|
|
10
|
+
* forward may keep an accent (`classNames.primary`). A preset amount only fills
|
|
11
|
+
* the amount — the invoice is made by "Create invoice". Anything only one site has
|
|
10
12
|
* (a funding-health panel, an operator view) goes in `before` or `children`.
|
|
11
13
|
*
|
|
12
14
|
* A balance nobody has read is a dash, never 0: showing 0 to someone with
|
|
@@ -32,7 +34,13 @@ export interface WalletPageClassNames {
|
|
|
32
34
|
error?: string;
|
|
33
35
|
/** Every action. */
|
|
34
36
|
chip?: string;
|
|
35
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The action that moves a top-up forward — "Create invoice", then "Open
|
|
39
|
+
* checkout" — in place of `chip`, for a site that gives it its accent.
|
|
40
|
+
* Default: `chip`.
|
|
41
|
+
*/
|
|
42
|
+
primary?: string;
|
|
43
|
+
/** Added to a chip that is the current choice (the chosen preset). */
|
|
36
44
|
chipActive?: string;
|
|
37
45
|
/** A row of chips. */
|
|
38
46
|
chips?: string;
|
|
@@ -48,7 +56,7 @@ export interface WalletPageClassNames {
|
|
|
48
56
|
export interface WalletPageProps {
|
|
49
57
|
/** The page heading. Default "Wallet"; null for none. */
|
|
50
58
|
heading?: ReactNode;
|
|
51
|
-
/** Preset amounts, in sats, offered as chips. */
|
|
59
|
+
/** Preset amounts, in sats, offered as chips. A tap fills the amount; it does not invoice. */
|
|
52
60
|
topUps?: number[];
|
|
53
61
|
/** Offer a box for any amount. Default true. */
|
|
54
62
|
customAmount?: boolean;
|
package/dist/react/WalletPage.js
CHANGED
|
@@ -7,15 +7,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
7
7
|
* purchase_credits → check_payment, account_statement, the coupon tools),
|
|
8
8
|
* holds the states and writes the words; every visual choice belongs to the
|
|
9
9
|
* site, through `classNames` — with none it is plain markup that inherits from
|
|
10
|
-
* the page. Actions are chips (`classNames.chip`)
|
|
10
|
+
* the page. Actions are chips (`classNames.chip`); the one that moves a top-up
|
|
11
|
+
* forward may keep an accent (`classNames.primary`). A preset amount only fills
|
|
12
|
+
* the amount — the invoice is made by "Create invoice". Anything only one site has
|
|
11
13
|
* (a funding-health panel, an operator view) goes in `before` or `children`.
|
|
12
14
|
*
|
|
13
15
|
* A balance nobody has read is a dash, never 0: showing 0 to someone with
|
|
14
16
|
* funds sends them to buy credits they already own.
|
|
15
17
|
*/
|
|
16
|
-
import { useCallback, useEffect, useState } from "react";
|
|
18
|
+
import { useCallback, useEffect, useReducer, useState } from "react";
|
|
17
19
|
import { checkBalance, getAccountStatement, } from "../standardTools.js";
|
|
18
|
-
import { formatSats, parseSats } from "../wallet.js";
|
|
20
|
+
import { amountReducer, formatSats, parseSats, presetChosen } from "../wallet.js";
|
|
19
21
|
import CouponsPanel, {} from "./CouponsPanel.js";
|
|
20
22
|
import { useTopUp } from "./useTopUp.js";
|
|
21
23
|
const DEFAULT_TOP_UPS = [1_000, 5_000, 25_000];
|
|
@@ -55,10 +57,15 @@ function Balance({ bal, error, refresh, formatDateTime, c, }) {
|
|
|
55
57
|
}
|
|
56
58
|
function TopUpSection({ topUps, customAmount, poll, onSettled, c, }) {
|
|
57
59
|
const { state, create, check, cancel, reset } = useTopUp({ ...poll, onSettled });
|
|
58
|
-
const [
|
|
60
|
+
const [amount, pick] = useReducer(amountReducer, "");
|
|
59
61
|
const [copied, setCopied] = useState(false);
|
|
60
|
-
const
|
|
62
|
+
const sats = parseSats(amount);
|
|
61
63
|
const creating = state.phase === "creating";
|
|
64
|
+
const primary = c.primary ?? c.chip;
|
|
65
|
+
function confirm() {
|
|
66
|
+
if (sats && !creating)
|
|
67
|
+
create(sats);
|
|
68
|
+
}
|
|
62
69
|
async function copy(text) {
|
|
63
70
|
try {
|
|
64
71
|
await navigator.clipboard.writeText(text);
|
|
@@ -69,10 +76,13 @@ function TopUpSection({ topUps, customAmount, poll, onSettled, c, }) {
|
|
|
69
76
|
/* the string is on screen and selectable */
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
return (_jsxs("section", { className: c.section, children: [_jsx("div", { className: c.sectionTitle, children: "Top up with Lightning" }), (state.phase === "idle" || state.phase === "creating") && (_jsxs(_Fragment, { children: [_jsx("div", { className: c.chips, children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: creating, onClick: () =>
|
|
73
|
-
if (e.key === "Enter"
|
|
74
|
-
|
|
75
|
-
}, placeholder: "Any amount", "aria-label": "Amount in sats", className: c.input }), _jsx("button", { type: "button", disabled: creating || !
|
|
79
|
+
return (_jsxs("section", { className: c.section, children: [_jsx("div", { className: c.sectionTitle, children: "Top up with Lightning" }), (state.phase === "idle" || state.phase === "creating") && (_jsxs(_Fragment, { children: [_jsx("div", { className: c.chips, children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: creating, "aria-pressed": presetChosen(amount, n), onClick: () => pick({ type: "preset", sats: n }), className: cx(c.chip, presetChosen(amount, n) && c.chipActive), children: [n.toLocaleString("en-US"), " sats"] }, n))) }), _jsxs("div", { className: c.chips, children: [customAmount && (_jsx("input", { inputMode: "numeric", value: amount, disabled: creating, onChange: (e) => pick({ type: "typed", text: e.target.value }), onKeyDown: (e) => {
|
|
80
|
+
if (e.key === "Enter")
|
|
81
|
+
confirm();
|
|
82
|
+
}, placeholder: "Any amount", "aria-label": "Amount in sats", className: c.input })), _jsx("button", { type: "button", disabled: creating || !sats, onClick: confirm, className: primary, children: "Create invoice" })] }), creating && _jsx("p", { className: c.status, children: "Making a Lightning invoice\u2026" }), state.phase === "idle" && state.message && _jsx("p", { className: c.error, children: state.message })] })), state.phase === "awaiting" && (_jsxs("div", { className: c.invoice, children: [_jsxs("p", { className: c.status, children: ["Invoice for ", formatSats(state.invoice.sats), " sats"] }), state.invoice.bolt11 && _jsx("div", { className: c.bolt11, children: state.invoice.bolt11 }), _jsxs("div", { className: c.chips, children: [state.invoice.checkoutLink && (_jsx("a", { href: state.invoice.checkoutLink, target: "_blank", rel: "noreferrer", className: primary, children: "Open checkout" })), state.invoice.bolt11 && (_jsx("button", { type: "button", onClick: () => void copy(state.invoice.bolt11), className: c.chip, children: copied ? "Copied" : "Copy invoice" })), _jsx("button", { type: "button", disabled: state.checking, onClick: check, className: c.chip, children: state.checking ? "Checking…" : "I've paid — check" }), _jsx("button", { type: "button", onClick: cancel, className: c.chip, children: "Cancel" })] }), _jsx("p", { className: c.status, "aria-live": "polite", children: state.status ?? "Waiting for payment." })] })), state.phase === "settled" && (_jsxs(_Fragment, { children: [_jsxs("p", { className: c.status, "aria-live": "polite", children: ["Paid. ", formatSats(state.credited), " sats added."] }), _jsx("div", { className: c.chips, children: _jsx("button", { type: "button", onClick: () => {
|
|
83
|
+
pick({ type: "clear" });
|
|
84
|
+
reset();
|
|
85
|
+
}, className: c.chip, children: "Done" }) })] })), state.phase === "failed" && (_jsxs(_Fragment, { children: [_jsx("p", { className: c.error, children: state.message }), _jsx("div", { className: c.chips, children: _jsx("button", { type: "button", onClick: reset, className: c.chip, children: "Try again" }) })] }))] }));
|
|
76
86
|
}
|
|
77
87
|
function Tranches({ tranches, formatDate, c, }) {
|
|
78
88
|
return (_jsxs("section", { className: c.section, children: [_jsx("div", { className: c.sectionTitle, children: "Credit tranches" }), _jsx("ul", { className: c.list, children: tranches.map((t) => (_jsxs("li", { className: c.row, children: [_jsxs("span", { children: [formatSats(t.remaining_sats), " / ", formatSats(t.amount_sats), " sats"] }), " ", _jsx("span", { children: t.expires_at ? `expires ${formatDate(t.expires_at)}` : "no expiry" })] }, t.id))) })] }));
|
package/dist/react/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { default as QuoteScroller, type QuoteScrollerProps, type QuoteScrollerCl
|
|
|
10
10
|
export { default as SessionKeyClaim } from "./SessionKeyClaim.tsx";
|
|
11
11
|
export { default as TableFilter, type DateFieldOption, type FilterQuestion, type TableFilterProps, type TableFilterClassNames, type TableFilterDates, type TableFilterQuestions, type TableFilterSearch, } from "./TableFilter.tsx";
|
|
12
12
|
export { default as ThemeToggle, type ThemeToggleProps, type ThemeToggleClassNames } from "./ThemeToggle.tsx";
|
|
13
|
-
export { default as WalletCard } from "./WalletCard.tsx";
|
|
13
|
+
export { default as WalletCard, type WalletCardProps, type WalletCardClassNames } from "./WalletCard.tsx";
|
|
14
14
|
export { default as WalletPage, type WalletPageProps, type WalletPageClassNames } from "./WalletPage.tsx";
|
|
15
15
|
export { useSession, type Session } from "./useSession.ts";
|
|
16
16
|
export { useDebugLog } from "./useDebugLog.ts";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping TableFilter's questions panel on screen while it is open.
|
|
3
|
+
*
|
|
4
|
+
* The panel hangs from the filter mark, so anything that moves the mark moves
|
|
5
|
+
* the panel: ticking a question widens the mark (its summary), the Clear chip
|
|
6
|
+
* appears inside the panel, the window turns or scrolls. So the panel is
|
|
7
|
+
* re-placed on every one of those, not only when it opens.
|
|
8
|
+
*
|
|
9
|
+
* It is slid by the CSS `translate` property, which `getBoundingClientRect`
|
|
10
|
+
* already includes. Taking the slide in force back off gives where the site's
|
|
11
|
+
* CSS alone puts the panel, and the new slide is worked out from there — no
|
|
12
|
+
* need to clear the slide and force a second layout just to measure.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The slide (px) that keeps the panel `margin` px inside both edges.
|
|
16
|
+
* `measuredLeft` is where it is drawn now, with `currentDx` already applied.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reclampPanel(currentDx: number, measuredLeft: number, width: number, viewportWidth: number, margin?: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* `run` at most once per animation frame, however many times the returned
|
|
21
|
+
* function is called before that frame. `cancel` drops a pending run.
|
|
22
|
+
*/
|
|
23
|
+
export declare function oncePerFrame(run: () => void, request?: (cb: () => void) => number, release?: (id: number) => void): {
|
|
24
|
+
schedule: () => void;
|
|
25
|
+
cancel: () => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping TableFilter's questions panel on screen while it is open.
|
|
3
|
+
*
|
|
4
|
+
* The panel hangs from the filter mark, so anything that moves the mark moves
|
|
5
|
+
* the panel: ticking a question widens the mark (its summary), the Clear chip
|
|
6
|
+
* appears inside the panel, the window turns or scrolls. So the panel is
|
|
7
|
+
* re-placed on every one of those, not only when it opens.
|
|
8
|
+
*
|
|
9
|
+
* It is slid by the CSS `translate` property, which `getBoundingClientRect`
|
|
10
|
+
* already includes. Taking the slide in force back off gives where the site's
|
|
11
|
+
* CSS alone puts the panel, and the new slide is worked out from there — no
|
|
12
|
+
* need to clear the slide and force a second layout just to measure.
|
|
13
|
+
*/
|
|
14
|
+
import { nudgeIntoView } from "../table.js";
|
|
15
|
+
/**
|
|
16
|
+
* The slide (px) that keeps the panel `margin` px inside both edges.
|
|
17
|
+
* `measuredLeft` is where it is drawn now, with `currentDx` already applied.
|
|
18
|
+
*/
|
|
19
|
+
export function reclampPanel(currentDx, measuredLeft, width, viewportWidth, margin = 8) {
|
|
20
|
+
return nudgeIntoView(measuredLeft - currentDx, width, viewportWidth, margin);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `run` at most once per animation frame, however many times the returned
|
|
24
|
+
* function is called before that frame. `cancel` drops a pending run.
|
|
25
|
+
*/
|
|
26
|
+
export function oncePerFrame(run, request = (cb) => requestAnimationFrame(cb), release = (id) => cancelAnimationFrame(id)) {
|
|
27
|
+
let pending = null;
|
|
28
|
+
return {
|
|
29
|
+
schedule() {
|
|
30
|
+
if (pending !== null)
|
|
31
|
+
return;
|
|
32
|
+
pending = request(() => {
|
|
33
|
+
pending = null;
|
|
34
|
+
run();
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
cancel() {
|
|
38
|
+
if (pending === null)
|
|
39
|
+
return;
|
|
40
|
+
release(pending);
|
|
41
|
+
pending = null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/standardTools.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typed wrappers for the tools every operator gets from
|
|
3
3
|
* `register_standard_tools` in the tollbooth-dpyc wheel.
|
|
4
|
+
*
|
|
5
|
+
* Every wrapper takes the same optional `CallOptions` as `callTool`, last.
|
|
6
|
+
* A wrapper that is best-effort by default stays so unless the caller says
|
|
7
|
+
* otherwise; what the caller passes wins.
|
|
4
8
|
*/
|
|
9
|
+
import { type CallOptions } from "./client.ts";
|
|
5
10
|
/** What a patron must supply beyond the npub proof, per the wheel's `patron_auth_block`. */
|
|
6
11
|
export interface PatronAuth {
|
|
7
12
|
mode: "oauth" | "secure_courier" | "none";
|
|
@@ -42,7 +47,7 @@ export interface ServiceStatus {
|
|
|
42
47
|
lifecycle?: string;
|
|
43
48
|
message?: string;
|
|
44
49
|
}
|
|
45
|
-
export declare function serviceStatus(): Promise<ServiceStatus>;
|
|
50
|
+
export declare function serviceStatus(opts?: CallOptions): Promise<ServiceStatus>;
|
|
46
51
|
export interface NpubProofResult {
|
|
47
52
|
success?: boolean;
|
|
48
53
|
proven_npub?: string;
|
|
@@ -62,12 +67,12 @@ export interface NpubProofResult {
|
|
|
62
67
|
* in the DM is the one on their own screen; an impostor firing the same tool
|
|
63
68
|
* from elsewhere cannot make the patron's open tab show the attacker's code.
|
|
64
69
|
*/
|
|
65
|
-
export declare function requestNpubProof(patronNpub: string, verifyAt?: string, reason?: string): Promise<NpubProofResult>;
|
|
70
|
+
export declare function requestNpubProof(patronNpub: string, verifyAt?: string, reason?: string, opts?: CallOptions): Promise<NpubProofResult>;
|
|
66
71
|
/**
|
|
67
72
|
* Step 2: drain DMs for the signed reply to step 1. Call only after the human
|
|
68
73
|
* says they replied — it is destructive, so never poll it.
|
|
69
74
|
*/
|
|
70
|
-
export declare function receiveNpubProof(patronNpub: string, dpopToken: string): Promise<NpubProofResult>;
|
|
75
|
+
export declare function receiveNpubProof(patronNpub: string, dpopToken: string, opts?: CallOptions): Promise<NpubProofResult>;
|
|
71
76
|
export interface ProofStatusResult {
|
|
72
77
|
success?: boolean;
|
|
73
78
|
status?: "valid" | "expired" | "unknown" | string;
|
|
@@ -75,7 +80,7 @@ export interface ProofStatusResult {
|
|
|
75
80
|
error?: string;
|
|
76
81
|
error_code?: string;
|
|
77
82
|
}
|
|
78
|
-
export declare function checkProofStatus(patronNpub: string, dpopToken: string): Promise<ProofStatusResult>;
|
|
83
|
+
export declare function checkProofStatus(patronNpub: string, dpopToken: string, opts?: CallOptions): Promise<ProofStatusResult>;
|
|
79
84
|
/** One credit tranche: a purchase (or grant) and what is left of it. */
|
|
80
85
|
export interface CreditTranche {
|
|
81
86
|
id: string;
|
|
@@ -116,7 +121,7 @@ export interface CheckBalanceResult {
|
|
|
116
121
|
error?: string;
|
|
117
122
|
error_code?: string;
|
|
118
123
|
}
|
|
119
|
-
export declare function checkBalance(): Promise<CheckBalanceResult>;
|
|
124
|
+
export declare function checkBalance(opts?: CallOptions): Promise<CheckBalanceResult>;
|
|
120
125
|
export interface ConstraintEffect {
|
|
121
126
|
type: string;
|
|
122
127
|
message?: string;
|
|
@@ -148,7 +153,7 @@ export interface CheckPriceResult {
|
|
|
148
153
|
* them (ad valorem, or a multiplier on a categorical argument). Null when
|
|
149
154
|
* unreadable: show the answer without a price rather than invent one.
|
|
150
155
|
*/
|
|
151
|
-
export declare function checkPrice(toolId: string, toolKwargs?: Record<string, unknown
|
|
156
|
+
export declare function checkPrice(toolId: string, toolKwargs?: Record<string, unknown>, opts?: CallOptions): Promise<number | null>;
|
|
152
157
|
export interface PurchaseCreditsResult {
|
|
153
158
|
success?: boolean;
|
|
154
159
|
invoice_id?: string;
|
|
@@ -160,7 +165,7 @@ export interface PurchaseCreditsResult {
|
|
|
160
165
|
error?: string;
|
|
161
166
|
error_code?: string;
|
|
162
167
|
}
|
|
163
|
-
export declare function purchaseCredits(sats: number): Promise<PurchaseCreditsResult>;
|
|
168
|
+
export declare function purchaseCredits(sats: number, opts?: CallOptions): Promise<PurchaseCreditsResult>;
|
|
164
169
|
export interface CheckPaymentResult {
|
|
165
170
|
success?: boolean;
|
|
166
171
|
status?: "New" | "Processing" | "Settled" | "Expired" | "Invalid" | string;
|
|
@@ -175,7 +180,7 @@ export interface CheckPaymentResult {
|
|
|
175
180
|
error?: string;
|
|
176
181
|
error_code?: string;
|
|
177
182
|
}
|
|
178
|
-
export declare function checkPayment(invoiceId: string): Promise<CheckPaymentResult>;
|
|
183
|
+
export declare function checkPayment(invoiceId: string, opts?: CallOptions): Promise<CheckPaymentResult>;
|
|
179
184
|
export interface StatementInvoice {
|
|
180
185
|
invoice_id: string;
|
|
181
186
|
status: string;
|
|
@@ -224,7 +229,7 @@ export interface AccountStatementResult {
|
|
|
224
229
|
error?: string;
|
|
225
230
|
error_code?: string;
|
|
226
231
|
}
|
|
227
|
-
export declare function getAccountStatement(days?: number): Promise<AccountStatementResult>;
|
|
232
|
+
export declare function getAccountStatement(days?: number, opts?: CallOptions): Promise<AccountStatementResult>;
|
|
228
233
|
export interface PatronCoupon {
|
|
229
234
|
coupon_id: string;
|
|
230
235
|
name: string;
|
|
@@ -264,11 +269,11 @@ export interface ForgetCouponResult {
|
|
|
264
269
|
error_code?: string;
|
|
265
270
|
}
|
|
266
271
|
/** The coupons this patron has redeemed here. Free. */
|
|
267
|
-
export declare function listMyCoupons(): Promise<ListMyCouponsResult>;
|
|
272
|
+
export declare function listMyCoupons(opts?: CallOptions): Promise<ListMyCouponsResult>;
|
|
268
273
|
/** Redeem an operator's code once; the discount then applies on its own. Free. */
|
|
269
|
-
export declare function redeemCoupon(code: string): Promise<RedeemCouponResult>;
|
|
274
|
+
export declare function redeemCoupon(code: string, opts?: CallOptions): Promise<RedeemCouponResult>;
|
|
270
275
|
/** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
|
|
271
|
-
export declare function forgetCoupon(couponId: string): Promise<ForgetCouponResult>;
|
|
276
|
+
export declare function forgetCoupon(couponId: string, opts?: CallOptions): Promise<ForgetCouponResult>;
|
|
272
277
|
export interface Kind0 {
|
|
273
278
|
name?: string;
|
|
274
279
|
display_name?: string;
|
|
@@ -285,7 +290,7 @@ export interface GetNostrProfileResult {
|
|
|
285
290
|
profile?: Kind0;
|
|
286
291
|
error?: string;
|
|
287
292
|
}
|
|
288
|
-
export declare function getNostrProfile(npub: string): Promise<GetNostrProfileResult>;
|
|
293
|
+
export declare function getNostrProfile(npub: string, opts?: CallOptions): Promise<GetNostrProfileResult>;
|
|
289
294
|
export interface PublishNostrProfileResult {
|
|
290
295
|
success: boolean;
|
|
291
296
|
ok?: number;
|
|
@@ -294,7 +299,7 @@ export interface PublishNostrProfileResult {
|
|
|
294
299
|
error?: string;
|
|
295
300
|
}
|
|
296
301
|
/** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
|
|
297
|
-
export declare function publishNostrProfile(npub: string, signedEvent: string): Promise<PublishNostrProfileResult>;
|
|
302
|
+
export declare function publishNostrProfile(npub: string, signedEvent: string, opts?: CallOptions): Promise<PublishNostrProfileResult>;
|
|
298
303
|
/** Every lifecycle `session_status` reports. */
|
|
299
304
|
export type SessionLifecycle = "ready" | "warming_up" | "misconfigured" | "quota_exceeded" | "not_registered" | "no_identity";
|
|
300
305
|
/** The patron's stored upstream OAuth token, when the operator has a provider. */
|
|
@@ -325,7 +330,7 @@ export interface SessionStatusResult {
|
|
|
325
330
|
* Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
|
|
326
331
|
* patron's upstream OAuth token expiry, so a page can refresh ahead of time.
|
|
327
332
|
*/
|
|
328
|
-
export declare function sessionStatus(patronNpub?: string): Promise<SessionStatusResult>;
|
|
333
|
+
export declare function sessionStatus(patronNpub?: string, opts?: CallOptions): Promise<SessionStatusResult>;
|
|
329
334
|
export interface OnboardingItem {
|
|
330
335
|
field: string;
|
|
331
336
|
/** "identity" | "authority" | "secret" */
|
|
@@ -353,12 +358,12 @@ export interface OperatorOnboardingStatus {
|
|
|
353
358
|
error?: string;
|
|
354
359
|
error_code?: string;
|
|
355
360
|
}
|
|
356
|
-
export declare function getOperatorOnboardingStatus(): Promise<OperatorOnboardingStatus>;
|
|
361
|
+
export declare function getOperatorOnboardingStatus(opts?: CallOptions): Promise<OperatorOnboardingStatus>;
|
|
357
362
|
/**
|
|
358
363
|
* The operator's own balance at its Authority — what certifies patron top-ups.
|
|
359
364
|
* The Authority's ledger, in the same shape as a patron's `check_balance`.
|
|
360
365
|
*/
|
|
361
|
-
export declare function checkAuthorityBalance(): Promise<CheckBalanceResult>;
|
|
366
|
+
export declare function checkAuthorityBalance(opts?: CallOptions): Promise<CheckBalanceResult>;
|
|
362
367
|
export interface PricingStep {
|
|
363
368
|
id: string;
|
|
364
369
|
type: string;
|
|
@@ -395,7 +400,7 @@ export interface PricingModelResult {
|
|
|
395
400
|
error?: string;
|
|
396
401
|
}
|
|
397
402
|
/** The operator's active pricing model. Free, no proof. */
|
|
398
|
-
export declare function getPricingModel(): Promise<PricingModelResult>;
|
|
403
|
+
export declare function getPricingModel(opts?: CallOptions): Promise<PricingModelResult>;
|
|
399
404
|
export interface CanonicalIdentity {
|
|
400
405
|
tool_id: string;
|
|
401
406
|
mcp_name: string;
|
|
@@ -419,7 +424,7 @@ export interface CanonicalIdentitiesResult {
|
|
|
419
424
|
}>;
|
|
420
425
|
error?: string;
|
|
421
426
|
}
|
|
422
|
-
export declare function listCanonicalIdentities(): Promise<CanonicalIdentitiesResult>;
|
|
427
|
+
export declare function listCanonicalIdentities(opts?: CallOptions): Promise<CanonicalIdentitiesResult>;
|
|
423
428
|
export interface PatronCredentialFieldsResult {
|
|
424
429
|
success?: boolean;
|
|
425
430
|
fields?: string[];
|
|
@@ -436,8 +441,8 @@ export interface PatronCredentialWriteResult {
|
|
|
436
441
|
error_code?: string;
|
|
437
442
|
}
|
|
438
443
|
/** The names of the patron's stored credential fields — never their values. */
|
|
439
|
-
export declare function getPatronCredentialFields(): Promise<PatronCredentialFieldsResult>;
|
|
444
|
+
export declare function getPatronCredentialFields(opts?: CallOptions): Promise<PatronCredentialFieldsResult>;
|
|
440
445
|
/** Set one credential field, leaving the others as they are. */
|
|
441
|
-
export declare function updatePatronCredential(field: string, value: string): Promise<PatronCredentialWriteResult>;
|
|
446
|
+
export declare function updatePatronCredential(field: string, value: string, opts?: CallOptions): Promise<PatronCredentialWriteResult>;
|
|
442
447
|
/** Remove one credential field, leaving the others as they are. */
|
|
443
|
-
export declare function deletePatronCredential(field: string): Promise<PatronCredentialWriteResult>;
|
|
448
|
+
export declare function deletePatronCredential(field: string, opts?: CallOptions): Promise<PatronCredentialWriteResult>;
|
package/dist/standardTools.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typed wrappers for the tools every operator gets from
|
|
3
3
|
* `register_standard_tools` in the tollbooth-dpyc wheel.
|
|
4
|
+
*
|
|
5
|
+
* Every wrapper takes the same optional `CallOptions` as `callTool`, last.
|
|
6
|
+
* A wrapper that is best-effort by default stays so unless the caller says
|
|
7
|
+
* otherwise; what the caller passes wins.
|
|
4
8
|
*/
|
|
5
9
|
import { callTool } from "./client.js";
|
|
6
|
-
export function serviceStatus() {
|
|
7
|
-
return callTool("service_status", {}, { bestEffort: true });
|
|
10
|
+
export function serviceStatus(opts) {
|
|
11
|
+
return callTool("service_status", {}, { bestEffort: true, ...opts });
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
14
|
* Step 1 of DM sign-in: a Secure Courier challenge DM to the npub. Free.
|
|
@@ -14,28 +18,25 @@ export function serviceStatus() {
|
|
|
14
18
|
* in the DM is the one on their own screen; an impostor firing the same tool
|
|
15
19
|
* from elsewhere cannot make the patron's open tab show the attacker's code.
|
|
16
20
|
*/
|
|
17
|
-
export function requestNpubProof(patronNpub, verifyAt, reason) {
|
|
21
|
+
export function requestNpubProof(patronNpub, verifyAt, reason, opts) {
|
|
18
22
|
return callTool("request_npub_proof", {
|
|
19
23
|
patron_npub: patronNpub,
|
|
20
24
|
...(verifyAt ? { verify_at: verifyAt } : {}),
|
|
21
25
|
...(reason ? { reason } : {}),
|
|
22
|
-
});
|
|
26
|
+
}, opts);
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Step 2: drain DMs for the signed reply to step 1. Call only after the human
|
|
26
30
|
* says they replied — it is destructive, so never poll it.
|
|
27
31
|
*/
|
|
28
|
-
export function receiveNpubProof(patronNpub, dpopToken) {
|
|
29
|
-
return callTool("receive_npub_proof", {
|
|
30
|
-
patron_npub: patronNpub,
|
|
31
|
-
dpop_token: dpopToken,
|
|
32
|
-
});
|
|
32
|
+
export function receiveNpubProof(patronNpub, dpopToken, opts) {
|
|
33
|
+
return callTool("receive_npub_proof", { patron_npub: patronNpub, dpop_token: dpopToken }, opts);
|
|
33
34
|
}
|
|
34
|
-
export function checkProofStatus(patronNpub, dpopToken) {
|
|
35
|
-
return callTool("check_proof_status", { patron_npub: patronNpub, dpop_token: dpopToken }, { bestEffort: true });
|
|
35
|
+
export function checkProofStatus(patronNpub, dpopToken, opts) {
|
|
36
|
+
return callTool("check_proof_status", { patron_npub: patronNpub, dpop_token: dpopToken }, { bestEffort: true, ...opts });
|
|
36
37
|
}
|
|
37
|
-
export function checkBalance() {
|
|
38
|
-
return callTool("check_balance", {});
|
|
38
|
+
export function checkBalance(opts) {
|
|
39
|
+
return callTool("check_balance", {}, opts);
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
42
|
* The fare for one tool in api_sats, as the live pricing model resolves it,
|
|
@@ -44,79 +45,73 @@ export function checkBalance() {
|
|
|
44
45
|
* them (ad valorem, or a multiplier on a categorical argument). Null when
|
|
45
46
|
* unreadable: show the answer without a price rather than invent one.
|
|
46
47
|
*/
|
|
47
|
-
export async function checkPrice(toolId, toolKwargs) {
|
|
48
|
-
const r = await callTool("check_price", {
|
|
49
|
-
tool_id: toolId,
|
|
50
|
-
...(toolKwargs ? { tool_kwargs: JSON.stringify(toolKwargs) } : {}),
|
|
51
|
-
});
|
|
48
|
+
export async function checkPrice(toolId, toolKwargs, opts) {
|
|
49
|
+
const r = await callTool("check_price", { tool_id: toolId, ...(toolKwargs ? { tool_kwargs: JSON.stringify(toolKwargs) } : {}) }, opts);
|
|
52
50
|
const v = r.effective_cost_api_sats ?? r.base_cost_api_sats;
|
|
53
51
|
return typeof v === "number" ? v : null;
|
|
54
52
|
}
|
|
55
|
-
export function purchaseCredits(sats) {
|
|
56
|
-
return callTool("purchase_credits", { amount_sats: sats });
|
|
53
|
+
export function purchaseCredits(sats, opts) {
|
|
54
|
+
return callTool("purchase_credits", { amount_sats: sats }, opts);
|
|
57
55
|
}
|
|
58
|
-
export function checkPayment(invoiceId) {
|
|
59
|
-
return callTool("check_payment", { invoice_id: invoiceId });
|
|
56
|
+
export function checkPayment(invoiceId, opts) {
|
|
57
|
+
return callTool("check_payment", { invoice_id: invoiceId }, opts);
|
|
60
58
|
}
|
|
61
|
-
export function getAccountStatement(days = 30) {
|
|
62
|
-
return callTool("account_statement", { days });
|
|
59
|
+
export function getAccountStatement(days = 30, opts) {
|
|
60
|
+
return callTool("account_statement", { days }, opts);
|
|
63
61
|
}
|
|
64
62
|
/** The coupons this patron has redeemed here. Free. */
|
|
65
|
-
export function listMyCoupons() {
|
|
66
|
-
return callTool("list_my_coupons", {});
|
|
63
|
+
export function listMyCoupons(opts) {
|
|
64
|
+
return callTool("list_my_coupons", {}, opts);
|
|
67
65
|
}
|
|
68
66
|
/** Redeem an operator's code once; the discount then applies on its own. Free. */
|
|
69
|
-
export function redeemCoupon(code) {
|
|
70
|
-
return callTool("redeem_coupon", { code });
|
|
67
|
+
export function redeemCoupon(code, opts) {
|
|
68
|
+
return callTool("redeem_coupon", { code }, opts);
|
|
71
69
|
}
|
|
72
70
|
/** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
|
|
73
|
-
export function forgetCoupon(couponId) {
|
|
74
|
-
return callTool("forget_coupon", { coupon_id: couponId });
|
|
71
|
+
export function forgetCoupon(couponId, opts) {
|
|
72
|
+
return callTool("forget_coupon", { coupon_id: couponId }, opts);
|
|
75
73
|
}
|
|
76
|
-
export function getNostrProfile(npub) {
|
|
77
|
-
return callTool("get_nostr_profile", { npub });
|
|
74
|
+
export function getNostrProfile(npub, opts) {
|
|
75
|
+
return callTool("get_nostr_profile", { npub }, opts);
|
|
78
76
|
}
|
|
79
77
|
/** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
|
|
80
|
-
export function publishNostrProfile(npub, signedEvent) {
|
|
81
|
-
return callTool("publish_nostr_profile", {
|
|
82
|
-
npub,
|
|
83
|
-
signed_event: signedEvent,
|
|
84
|
-
});
|
|
78
|
+
export function publishNostrProfile(npub, signedEvent, opts) {
|
|
79
|
+
return callTool("publish_nostr_profile", { npub, signed_event: signedEvent }, opts);
|
|
85
80
|
}
|
|
86
81
|
/**
|
|
87
82
|
* Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
|
|
88
83
|
* patron's upstream OAuth token expiry, so a page can refresh ahead of time.
|
|
89
84
|
*/
|
|
90
|
-
export function sessionStatus(patronNpub) {
|
|
91
|
-
return callTool("session_status", patronNpub ? { patron_npub: patronNpub } : {}, { bestEffort: true });
|
|
85
|
+
export function sessionStatus(patronNpub, opts) {
|
|
86
|
+
return callTool("session_status", patronNpub ? { patron_npub: patronNpub } : {}, { bestEffort: true, ...opts });
|
|
92
87
|
}
|
|
93
|
-
export function getOperatorOnboardingStatus() {
|
|
94
|
-
return callTool("get_operator_onboarding_status", {}, { bestEffort: true });
|
|
88
|
+
export function getOperatorOnboardingStatus(opts) {
|
|
89
|
+
return callTool("get_operator_onboarding_status", {}, { bestEffort: true, ...opts });
|
|
95
90
|
}
|
|
96
91
|
/**
|
|
97
92
|
* The operator's own balance at its Authority — what certifies patron top-ups.
|
|
98
93
|
* The Authority's ledger, in the same shape as a patron's `check_balance`.
|
|
99
94
|
*/
|
|
100
|
-
export function checkAuthorityBalance() {
|
|
101
|
-
return callTool("check_authority_balance", {}, { bestEffort: true });
|
|
95
|
+
export function checkAuthorityBalance(opts) {
|
|
96
|
+
return callTool("check_authority_balance", {}, { bestEffort: true, ...opts });
|
|
102
97
|
}
|
|
103
98
|
/** The operator's active pricing model. Free, no proof. */
|
|
104
|
-
export function getPricingModel() {
|
|
105
|
-
return callTool("get_pricing_model", {});
|
|
99
|
+
export function getPricingModel(opts) {
|
|
100
|
+
return callTool("get_pricing_model", {}, opts);
|
|
106
101
|
}
|
|
107
|
-
export function listCanonicalIdentities() {
|
|
108
|
-
return callTool("list_canonical_identities", {});
|
|
102
|
+
export function listCanonicalIdentities(opts) {
|
|
103
|
+
return callTool("list_canonical_identities", {}, opts);
|
|
109
104
|
}
|
|
110
105
|
/** The names of the patron's stored credential fields — never their values. */
|
|
111
|
-
export function getPatronCredentialFields() {
|
|
112
|
-
return callTool("get_patron_credential_fields", {});
|
|
106
|
+
export function getPatronCredentialFields(opts) {
|
|
107
|
+
return callTool("get_patron_credential_fields", {}, opts);
|
|
113
108
|
}
|
|
114
109
|
/** Set one credential field, leaving the others as they are. */
|
|
115
|
-
export function updatePatronCredential(field, value) {
|
|
110
|
+
export function updatePatronCredential(field, value, opts) {
|
|
116
111
|
// `field` first: the log's scrubber keys on it to hide `value`.
|
|
117
|
-
return callTool("update_patron_credential", { field, value });
|
|
112
|
+
return callTool("update_patron_credential", { field, value }, opts);
|
|
118
113
|
}
|
|
119
114
|
/** Remove one credential field, leaving the others as they are. */
|
|
120
|
-
export function deletePatronCredential(field) {
|
|
121
|
-
return callTool("delete_patron_credential", { field });
|
|
115
|
+
export function deletePatronCredential(field, opts) {
|
|
116
|
+
return callTool("delete_patron_credential", { field }, opts);
|
|
122
117
|
}
|
package/dist/table.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare function nextSort(col: string, activeCol: string, dir: SortDir, i
|
|
|
21
21
|
col: string;
|
|
22
22
|
dir: SortDir;
|
|
23
23
|
};
|
|
24
|
+
/** The `aria-sort` of a column header: set on the sorted column only. */
|
|
25
|
+
export declare function ariaSort(col: string, activeCol: string, dir: SortDir): "ascending" | "descending" | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* How far to slide a popover sideways so it stays inside the viewport: the
|
|
28
|
+
* horizontal offset (px, negative is left) to add to where it would sit.
|
|
29
|
+
* `left` and `width` are its box as laid out before any slide. It keeps
|
|
30
|
+
* `margin` px from each edge; one wider than the room left is pinned to the
|
|
31
|
+
* left margin (give it a max-width to fit). Zero when it already fits.
|
|
32
|
+
*/
|
|
33
|
+
export declare function nudgeIntoView(left: number, width: number, viewportWidth: number, margin?: number): number;
|
|
24
34
|
/**
|
|
25
35
|
* A case-insensitive test for `pattern`. It is read as a regular expression
|
|
26
36
|
* when it is one, and as plain text when it is not, so a stray "(" in a search
|
package/dist/table.js
CHANGED
|
@@ -35,6 +35,29 @@ export function nextSort(col, activeCol, dir, initial = "desc") {
|
|
|
35
35
|
return { col, dir: dir === "desc" ? "asc" : "desc" };
|
|
36
36
|
return { col, dir: initial };
|
|
37
37
|
}
|
|
38
|
+
/** The `aria-sort` of a column header: set on the sorted column only. */
|
|
39
|
+
export function ariaSort(col, activeCol, dir) {
|
|
40
|
+
if (col !== activeCol)
|
|
41
|
+
return undefined;
|
|
42
|
+
return dir === "desc" ? "descending" : "ascending";
|
|
43
|
+
}
|
|
44
|
+
// ── A popover kept on screen ───────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* How far to slide a popover sideways so it stays inside the viewport: the
|
|
47
|
+
* horizontal offset (px, negative is left) to add to where it would sit.
|
|
48
|
+
* `left` and `width` are its box as laid out before any slide. It keeps
|
|
49
|
+
* `margin` px from each edge; one wider than the room left is pinned to the
|
|
50
|
+
* left margin (give it a max-width to fit). Zero when it already fits.
|
|
51
|
+
*/
|
|
52
|
+
export function nudgeIntoView(left, width, viewportWidth, margin = 8) {
|
|
53
|
+
const minLeft = margin;
|
|
54
|
+
const maxLeft = viewportWidth - margin - width;
|
|
55
|
+
if (maxLeft < minLeft || left < minLeft)
|
|
56
|
+
return minLeft - left;
|
|
57
|
+
if (left > maxLeft)
|
|
58
|
+
return maxLeft - left;
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
38
61
|
// ── Filtering ──────────────────────────────────────────────────────────────
|
|
39
62
|
/**
|
|
40
63
|
* A case-insensitive test for `pattern`. It is read as a regular expression
|
package/dist/wallet.d.ts
CHANGED
|
@@ -66,6 +66,24 @@ export declare function paymentPhase(status: string | undefined | null): Payment
|
|
|
66
66
|
export declare function invoiceFrom(result: PurchaseCreditsResult, asked: number): Invoice | null;
|
|
67
67
|
/** A whole number of sats worth invoicing, from what the patron typed; else null. */
|
|
68
68
|
export declare function parseSats(input: string | number): number | null;
|
|
69
|
+
/**
|
|
70
|
+
* The amount box. Tapping a preset only fills it; an invoice is made only by
|
|
71
|
+
* the explicit "Create invoice" action, which reads `parseSats(text)`. A tap is
|
|
72
|
+
* a choice, not a purchase — a patron who taps the wrong chip changes it
|
|
73
|
+
* before anything is asked of the service.
|
|
74
|
+
*/
|
|
75
|
+
export type AmountEvent = {
|
|
76
|
+
type: "preset";
|
|
77
|
+
sats: number;
|
|
78
|
+
} | {
|
|
79
|
+
type: "typed";
|
|
80
|
+
text: string;
|
|
81
|
+
} | {
|
|
82
|
+
type: "clear";
|
|
83
|
+
};
|
|
84
|
+
export declare function amountReducer(text: string, event: AmountEvent): string;
|
|
85
|
+
/** Whether preset `sats` is what the amount box holds — the chip to mark as chosen. */
|
|
86
|
+
export declare function presetChosen(text: string, sats: number): boolean;
|
|
69
87
|
export declare function topUpReducer(state: TopUpState, event: TopUpEvent): TopUpState;
|
|
70
88
|
/** Whether a poll should run now: an invoice is out, nothing is in flight, and the budget is not spent. */
|
|
71
89
|
export declare function shouldPoll(state: TopUpState, maxChecks: number): boolean;
|
package/dist/wallet.js
CHANGED
|
@@ -38,6 +38,20 @@ export function parseSats(input) {
|
|
|
38
38
|
const n = typeof input === "number" ? input : Number(String(input).replace(/[\s,_]/g, ""));
|
|
39
39
|
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
40
40
|
}
|
|
41
|
+
export function amountReducer(text, event) {
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case "preset":
|
|
44
|
+
return parseSats(event.sats) === null ? text : String(event.sats);
|
|
45
|
+
case "typed":
|
|
46
|
+
return event.text.replace(/[^\d]/g, "");
|
|
47
|
+
case "clear":
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Whether preset `sats` is what the amount box holds — the chip to mark as chosen. */
|
|
52
|
+
export function presetChosen(text, sats) {
|
|
53
|
+
return parseSats(text) === sats;
|
|
54
|
+
}
|
|
41
55
|
export function topUpReducer(state, event) {
|
|
42
56
|
switch (event.type) {
|
|
43
57
|
case "create":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Lonnie VanZandt",
|