@tollbooth-dpyc/web 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -3
- package/dist/client.d.ts +5 -1
- package/dist/client.js +15 -2
- package/dist/errorReport.d.ts +5 -0
- package/dist/errorReport.js +14 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/networkError.d.ts +29 -0
- package/dist/networkError.js +61 -0
- package/dist/react/CouponsPanel.d.ts +52 -0
- package/dist/react/CouponsPanel.js +98 -0
- package/dist/react/DebugPanel.d.ts +6 -0
- package/dist/react/DebugPanel.js +33 -6
- package/dist/react/ErrorBoundary.d.ts +55 -0
- package/dist/react/ErrorBoundary.js +49 -0
- package/dist/react/PagedTable.d.ts +64 -0
- package/dist/react/PagedTable.js +28 -0
- package/dist/react/TableFilter.d.ts +93 -0
- package/dist/react/TableFilter.js +68 -0
- package/dist/react/ThemeToggle.d.ts +22 -0
- package/dist/react/ThemeToggle.js +11 -0
- package/dist/react/WalletCard.d.ts +3 -0
- package/dist/react/WalletCard.js +19 -53
- package/dist/react/WalletPage.d.ts +70 -0
- package/dist/react/WalletPage.js +104 -0
- package/dist/react/debugSpacer.d.ts +15 -0
- package/dist/react/debugSpacer.js +21 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +8 -0
- package/dist/react/useTheme.d.ts +7 -0
- package/dist/react/useTheme.js +44 -0
- package/dist/react/useTopUp.d.ts +30 -0
- package/dist/react/useTopUp.js +74 -0
- package/dist/standardTools.d.ts +110 -7
- package/dist/standardTools.js +12 -0
- package/dist/table.d.ts +52 -0
- package/dist/table.js +95 -0
- package/dist/theme.d.ts +40 -0
- package/dist/theme.js +76 -0
- package/dist/wallet.d.ts +85 -0
- package/dist/wallet.js +153 -0
- package/package.json +1 -1
- package/theme.css +22 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The room the DebugPanel keeps for itself.
|
|
3
|
+
*
|
|
4
|
+
* The panel is fixed to the bottom of the viewport, so on its own it would sit
|
|
5
|
+
* over whatever the page puts last — on a phone, a loading quote or a Save
|
|
6
|
+
* button. Beside it the panel renders an in-flow spacer as tall as the panel is
|
|
7
|
+
* right now (the collapsed tab, or the open log), plus the iOS home-indicator
|
|
8
|
+
* inset, so the page can always scroll its last line clear of the bar.
|
|
9
|
+
*/
|
|
10
|
+
/** The collapsed tab: one `min-h-10` (2.5rem) button. Used until measured. */
|
|
11
|
+
export declare const DEBUG_BAR_PX = 40;
|
|
12
|
+
/** Whole CSS pixels for a measured height; the collapsed bar when unmeasured. */
|
|
13
|
+
export declare function debugSpacerPx(measured: number | null | undefined): number;
|
|
14
|
+
/** The spacer's CSS height: the panel's height plus the safe-area inset. */
|
|
15
|
+
export declare function debugSpacerHeight(measured: number | null | undefined): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The room the DebugPanel keeps for itself.
|
|
3
|
+
*
|
|
4
|
+
* The panel is fixed to the bottom of the viewport, so on its own it would sit
|
|
5
|
+
* over whatever the page puts last — on a phone, a loading quote or a Save
|
|
6
|
+
* button. Beside it the panel renders an in-flow spacer as tall as the panel is
|
|
7
|
+
* right now (the collapsed tab, or the open log), plus the iOS home-indicator
|
|
8
|
+
* inset, so the page can always scroll its last line clear of the bar.
|
|
9
|
+
*/
|
|
10
|
+
/** The collapsed tab: one `min-h-10` (2.5rem) button. Used until measured. */
|
|
11
|
+
export const DEBUG_BAR_PX = 40;
|
|
12
|
+
/** Whole CSS pixels for a measured height; the collapsed bar when unmeasured. */
|
|
13
|
+
export function debugSpacerPx(measured) {
|
|
14
|
+
if (typeof measured !== "number" || !Number.isFinite(measured) || measured <= 0)
|
|
15
|
+
return DEBUG_BAR_PX;
|
|
16
|
+
return Math.ceil(measured);
|
|
17
|
+
}
|
|
18
|
+
/** The spacer's CSS height: the panel's height plus the safe-area inset. */
|
|
19
|
+
export function debugSpacerHeight(measured) {
|
|
20
|
+
return `calc(${debugSpacerPx(measured)}px + env(safe-area-inset-bottom, 0px))`;
|
|
21
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
export { default as Avatar } from "./Avatar.tsx";
|
|
2
2
|
export { default as AvatarPicker } from "./AvatarPicker.tsx";
|
|
3
|
+
export { default as CouponsPanel, type CouponsPanelProps, type CouponsPanelClassNames } from "./CouponsPanel.tsx";
|
|
3
4
|
export { default as DebugPanel, type DebugPanelProps } from "./DebugPanel.tsx";
|
|
5
|
+
export { default as ErrorBoundary, type ErrorBoundaryProps, type ErrorBoundaryClassNames, type ErrorFallbackProps, } from "./ErrorBoundary.tsx";
|
|
4
6
|
export { default as NpubGate } from "./NpubGate.tsx";
|
|
5
7
|
export { default as NostrProfilePanel } from "./NostrProfilePanel.tsx";
|
|
8
|
+
export { PageControls, SortHeader, TableShell, type PageControlsProps, type PageControlsClassNames, type SortHeaderProps, type SortHeaderClassNames, type TableShellClassNames, } from "./PagedTable.tsx";
|
|
6
9
|
export { default as QuoteScroller, type QuoteScrollerProps, type QuoteScrollerClassNames } from "./QuoteScroller.tsx";
|
|
7
10
|
export { default as SessionKeyClaim } from "./SessionKeyClaim.tsx";
|
|
11
|
+
export { default as TableFilter, type DateFieldOption, type FilterQuestion, type TableFilterProps, type TableFilterClassNames, type TableFilterDates, type TableFilterQuestions, type TableFilterSearch, } from "./TableFilter.tsx";
|
|
12
|
+
export { default as ThemeToggle, type ThemeToggleProps, type ThemeToggleClassNames } from "./ThemeToggle.tsx";
|
|
8
13
|
export { default as WalletCard } from "./WalletCard.tsx";
|
|
14
|
+
export { default as WalletPage, type WalletPageProps, type WalletPageClassNames } from "./WalletPage.tsx";
|
|
9
15
|
export { useSession, type Session } from "./useSession.ts";
|
|
10
16
|
export { useDebugLog } from "./useDebugLog.ts";
|
|
17
|
+
export { useTheme } from "./useTheme.ts";
|
|
18
|
+
export { useTopUp, type TopUp, type UseTopUpOptions } from "./useTopUp.ts";
|
package/dist/react/index.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
export { default as Avatar } from "./Avatar.js";
|
|
2
2
|
export { default as AvatarPicker } from "./AvatarPicker.js";
|
|
3
|
+
export { default as CouponsPanel } from "./CouponsPanel.js";
|
|
3
4
|
export { default as DebugPanel } from "./DebugPanel.js";
|
|
5
|
+
export { default as ErrorBoundary, } from "./ErrorBoundary.js";
|
|
4
6
|
export { default as NpubGate } from "./NpubGate.js";
|
|
5
7
|
export { default as NostrProfilePanel } from "./NostrProfilePanel.js";
|
|
8
|
+
export { PageControls, SortHeader, TableShell, } from "./PagedTable.js";
|
|
6
9
|
export { default as QuoteScroller } from "./QuoteScroller.js";
|
|
7
10
|
export { default as SessionKeyClaim } from "./SessionKeyClaim.js";
|
|
11
|
+
export { default as TableFilter, } from "./TableFilter.js";
|
|
12
|
+
export { default as ThemeToggle } from "./ThemeToggle.js";
|
|
8
13
|
export { default as WalletCard } from "./WalletCard.js";
|
|
14
|
+
export { default as WalletPage } from "./WalletPage.js";
|
|
9
15
|
export { useSession } from "./useSession.js";
|
|
10
16
|
export { useDebugLog } from "./useDebugLog.js";
|
|
17
|
+
export { useTheme } from "./useTheme.js";
|
|
18
|
+
export { useTopUp } from "./useTopUp.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme pick as React state: persisted under `<prefix>:theme`, applied to
|
|
3
|
+
* <html>, followed across tabs, and — in "system" — following the OS live.
|
|
4
|
+
* Call `bootstrapTheme()` in main.tsx too, so the first frame is right.
|
|
5
|
+
*/
|
|
6
|
+
import { type Theme } from "../theme.ts";
|
|
7
|
+
export declare function useTheme(fallback?: Theme): [Theme, (next: Theme) => void];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme pick as React state: persisted under `<prefix>:theme`, applied to
|
|
3
|
+
* <html>, followed across tabs, and — in "system" — following the OS live.
|
|
4
|
+
* Call `bootstrapTheme()` in main.tsx too, so the first frame is right.
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useState } from "react";
|
|
7
|
+
import { applyTheme, readTheme, themeStorageKey, writeTheme } from "../theme.js";
|
|
8
|
+
export function useTheme(fallback = "dark") {
|
|
9
|
+
const [theme, setTheme] = useState(() => readTheme(fallback));
|
|
10
|
+
// Another tab changed it.
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
let key;
|
|
13
|
+
try {
|
|
14
|
+
key = themeStorageKey();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const onStorage = (e) => {
|
|
20
|
+
if (e.key !== key)
|
|
21
|
+
return;
|
|
22
|
+
const next = readTheme(fallback);
|
|
23
|
+
setTheme(next);
|
|
24
|
+
applyTheme(next);
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener("storage", onStorage);
|
|
27
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
28
|
+
}, [fallback]);
|
|
29
|
+
// In "system", the OS changed its mind.
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (theme !== "system" || !window.matchMedia)
|
|
32
|
+
return;
|
|
33
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
34
|
+
const onChange = () => applyTheme("system");
|
|
35
|
+
mq.addEventListener("change", onChange);
|
|
36
|
+
return () => mq.removeEventListener("change", onChange);
|
|
37
|
+
}, [theme]);
|
|
38
|
+
const update = useCallback((next) => {
|
|
39
|
+
writeTheme(next);
|
|
40
|
+
applyTheme(next);
|
|
41
|
+
setTheme(next);
|
|
42
|
+
}, []);
|
|
43
|
+
return [theme, update];
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Lightning top-up as a hook: purchase_credits → an invoice → check_payment
|
|
3
|
+
* until it settles, expires or the patron gives up.
|
|
4
|
+
*
|
|
5
|
+
* The state machine is `topUpReducer` (framework-free, tested). This adds the
|
|
6
|
+
* calls and the polling: while an invoice is out and the tab is visible,
|
|
7
|
+
* check_payment runs every `pollMs` — it is free and safe to repeat — up to
|
|
8
|
+
* `maxChecks` times, after which the patron's own "check" still works.
|
|
9
|
+
*/
|
|
10
|
+
import { type TopUpState } from "../wallet.ts";
|
|
11
|
+
export interface UseTopUpOptions {
|
|
12
|
+
/** How often to check an open invoice, in ms. 0 turns polling off. Default 5000. */
|
|
13
|
+
pollMs?: number;
|
|
14
|
+
/** Stop polling after this many checks. Default 60 (five minutes at the default rate). */
|
|
15
|
+
maxChecks?: number;
|
|
16
|
+
/** Called once when an invoice settles, with the sats credited — refresh a balance here. */
|
|
17
|
+
onSettled?: (credited: number) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface TopUp {
|
|
20
|
+
state: TopUpState;
|
|
21
|
+
/** Ask the service for an invoice of `sats`. Ignored while one is being made or is out. */
|
|
22
|
+
create: (sats: number) => void;
|
|
23
|
+
/** Check the open invoice now. */
|
|
24
|
+
check: () => void;
|
|
25
|
+
/** Drop the open invoice (it simply expires at BTCPay). */
|
|
26
|
+
cancel: () => void;
|
|
27
|
+
/** Back to idle after a settlement or failure. */
|
|
28
|
+
reset: () => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function useTopUp({ pollMs, maxChecks, onSettled }?: UseTopUpOptions): TopUp;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Lightning top-up as a hook: purchase_credits → an invoice → check_payment
|
|
3
|
+
* until it settles, expires or the patron gives up.
|
|
4
|
+
*
|
|
5
|
+
* The state machine is `topUpReducer` (framework-free, tested). This adds the
|
|
6
|
+
* calls and the polling: while an invoice is out and the tab is visible,
|
|
7
|
+
* check_payment runs every `pollMs` — it is free and safe to repeat — up to
|
|
8
|
+
* `maxChecks` times, after which the patron's own "check" still works.
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
11
|
+
import { checkPayment, purchaseCredits } from "../standardTools.js";
|
|
12
|
+
import { shouldPoll, TOP_UP_IDLE, topUpReducer } from "../wallet.js";
|
|
13
|
+
function messageOf(e) {
|
|
14
|
+
return e instanceof Error ? e.message : String(e);
|
|
15
|
+
}
|
|
16
|
+
function tabHidden() {
|
|
17
|
+
return typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
18
|
+
}
|
|
19
|
+
export function useTopUp({ pollMs = 5000, maxChecks = 60, onSettled } = {}) {
|
|
20
|
+
const [state, dispatch] = useReducer(topUpReducer, TOP_UP_IDLE);
|
|
21
|
+
const latest = useRef(state);
|
|
22
|
+
latest.current = state;
|
|
23
|
+
const settledRef = useRef(onSettled);
|
|
24
|
+
settledRef.current = onSettled;
|
|
25
|
+
// Bumped when the tab comes back, so a poll skipped while hidden is re-armed.
|
|
26
|
+
const [wake, setWake] = useState(0);
|
|
27
|
+
const create = useCallback((sats) => {
|
|
28
|
+
const s = latest.current;
|
|
29
|
+
if (s.phase === "creating" || s.phase === "awaiting")
|
|
30
|
+
return;
|
|
31
|
+
dispatch({ type: "create", sats });
|
|
32
|
+
purchaseCredits(sats).then((result) => dispatch({ type: "created", result }), (e) => dispatch({ type: "error", message: messageOf(e) }));
|
|
33
|
+
}, []);
|
|
34
|
+
const check = useCallback(() => {
|
|
35
|
+
const s = latest.current;
|
|
36
|
+
if (s.phase !== "awaiting" || s.checking)
|
|
37
|
+
return;
|
|
38
|
+
const id = s.invoice.id;
|
|
39
|
+
dispatch({ type: "check" });
|
|
40
|
+
checkPayment(id).then((result) => {
|
|
41
|
+
// A late answer for an invoice the patron has since cancelled is dropped.
|
|
42
|
+
const now = latest.current;
|
|
43
|
+
if (now.phase === "awaiting" && now.invoice.id === id)
|
|
44
|
+
dispatch({ type: "checked", result });
|
|
45
|
+
}, (e) => dispatch({ type: "error", message: messageOf(e) }));
|
|
46
|
+
}, []);
|
|
47
|
+
const cancel = useCallback(() => dispatch({ type: "cancel" }), []);
|
|
48
|
+
const reset = useCallback(() => dispatch({ type: "reset" }), []);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!(pollMs > 0) || !shouldPoll(state, maxChecks))
|
|
51
|
+
return;
|
|
52
|
+
const t = setTimeout(() => {
|
|
53
|
+
if (tabHidden())
|
|
54
|
+
return; // re-armed by the visibility listener below
|
|
55
|
+
check();
|
|
56
|
+
}, pollMs);
|
|
57
|
+
return () => clearTimeout(t);
|
|
58
|
+
}, [state, pollMs, maxChecks, check, wake]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (typeof document === "undefined")
|
|
61
|
+
return;
|
|
62
|
+
const onVisible = () => {
|
|
63
|
+
if (!tabHidden())
|
|
64
|
+
setWake((n) => n + 1);
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
67
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
68
|
+
}, []);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (state.phase === "settled")
|
|
71
|
+
settledRef.current?.(state.credited);
|
|
72
|
+
}, [state]);
|
|
73
|
+
return { state, create, check, cancel, reset };
|
|
74
|
+
}
|
package/dist/standardTools.d.ts
CHANGED
|
@@ -45,13 +45,36 @@ export interface ProofStatusResult {
|
|
|
45
45
|
error_code?: string;
|
|
46
46
|
}
|
|
47
47
|
export declare function checkProofStatus(patronNpub: string, dpopToken: string): Promise<ProofStatusResult>;
|
|
48
|
+
/** One credit tranche: a purchase (or grant) and what is left of it. */
|
|
49
|
+
export interface CreditTranche {
|
|
50
|
+
id: string;
|
|
51
|
+
amount_sats: number;
|
|
52
|
+
remaining_sats: number;
|
|
53
|
+
expires_at: string | null;
|
|
54
|
+
created_at: string | null;
|
|
55
|
+
}
|
|
48
56
|
export interface CheckBalanceResult {
|
|
49
57
|
success?: boolean;
|
|
50
58
|
balance_api_sats?: number;
|
|
51
59
|
total_deposited_api_sats?: number;
|
|
52
60
|
total_consumed_api_sats?: number;
|
|
61
|
+
total_expired_api_sats?: number;
|
|
62
|
+
pending_invoices?: number;
|
|
63
|
+
pending_invoice_ids?: string[];
|
|
64
|
+
last_deposit_at?: string | null;
|
|
53
65
|
next_expiration_iso?: string;
|
|
54
66
|
expiring_within_24h_sats?: number;
|
|
67
|
+
active_tranches?: number;
|
|
68
|
+
tranches?: CreditTranche[];
|
|
69
|
+
expired_tranches?: CreditTranche[];
|
|
70
|
+
seed_balance_granted?: boolean;
|
|
71
|
+
/** The ledger could not be read: the figures may be stale or zero. */
|
|
72
|
+
vault_unavailable?: boolean;
|
|
73
|
+
warning?: string;
|
|
74
|
+
today_usage?: Record<string, {
|
|
75
|
+
calls: number;
|
|
76
|
+
api_sats: number;
|
|
77
|
+
}>;
|
|
55
78
|
error?: string;
|
|
56
79
|
error_code?: string;
|
|
57
80
|
}
|
|
@@ -93,19 +116,99 @@ export interface CheckPaymentResult {
|
|
|
93
116
|
error_code?: string;
|
|
94
117
|
}
|
|
95
118
|
export declare function checkPayment(invoiceId: string): Promise<CheckPaymentResult>;
|
|
96
|
-
export interface
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
export interface StatementInvoice {
|
|
120
|
+
invoice_id: string;
|
|
121
|
+
status: string;
|
|
122
|
+
amount_sats: number;
|
|
123
|
+
api_sats_credited: number;
|
|
124
|
+
multiplier?: number;
|
|
125
|
+
created_at: string | null;
|
|
126
|
+
settled_at?: string;
|
|
127
|
+
}
|
|
128
|
+
export interface StatementTranche {
|
|
129
|
+
granted_at: string | null;
|
|
130
|
+
original_sats: number;
|
|
131
|
+
remaining_sats: number;
|
|
132
|
+
invoice_id: string | null;
|
|
133
|
+
expires_at?: string;
|
|
134
|
+
}
|
|
135
|
+
export interface StatementToolUsage {
|
|
136
|
+
tool: string;
|
|
137
|
+
calls: number;
|
|
138
|
+
api_sats: number;
|
|
139
|
+
}
|
|
140
|
+
export interface StatementDay {
|
|
141
|
+
date: string;
|
|
142
|
+
total_calls: number;
|
|
143
|
+
total_api_sats: number;
|
|
144
|
+
tools: Record<string, {
|
|
103
145
|
calls: number;
|
|
104
146
|
api_sats: number;
|
|
105
147
|
}>;
|
|
148
|
+
}
|
|
149
|
+
/** The wheel's `account_statement`: history, tranches and usage, newest first. */
|
|
150
|
+
export interface AccountStatementResult {
|
|
151
|
+
success?: boolean;
|
|
152
|
+
generated_at?: string;
|
|
153
|
+
statement_period_days?: number;
|
|
154
|
+
account_summary?: {
|
|
155
|
+
balance_api_sats: number;
|
|
156
|
+
total_deposited_api_sats: number;
|
|
157
|
+
total_consumed_api_sats: number;
|
|
158
|
+
total_expired_api_sats: number;
|
|
159
|
+
};
|
|
160
|
+
purchase_history?: StatementInvoice[];
|
|
161
|
+
active_tranches?: StatementTranche[];
|
|
162
|
+
tool_usage_all_time?: StatementToolUsage[];
|
|
163
|
+
daily_usage?: StatementDay[];
|
|
106
164
|
error?: string;
|
|
165
|
+
error_code?: string;
|
|
107
166
|
}
|
|
108
167
|
export declare function getAccountStatement(days?: number): Promise<AccountStatementResult>;
|
|
168
|
+
export interface PatronCoupon {
|
|
169
|
+
coupon_id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
discount_percent: number;
|
|
172
|
+
valid_from: string;
|
|
173
|
+
valid_until: string;
|
|
174
|
+
uses_per_patron: number | null;
|
|
175
|
+
use_count: number;
|
|
176
|
+
uses_remaining: number | null;
|
|
177
|
+
total_uses: number | null;
|
|
178
|
+
total_remaining: number | null;
|
|
179
|
+
/** active | window_closed | window_not_started | patron_limit | total_limit */
|
|
180
|
+
status: string;
|
|
181
|
+
}
|
|
182
|
+
export interface ListMyCouponsResult {
|
|
183
|
+
success?: boolean;
|
|
184
|
+
count?: number;
|
|
185
|
+
coupons?: PatronCoupon[];
|
|
186
|
+
error?: string;
|
|
187
|
+
error_code?: string;
|
|
188
|
+
}
|
|
189
|
+
export interface RedeemCouponResult {
|
|
190
|
+
success?: boolean;
|
|
191
|
+
coupon_id?: string;
|
|
192
|
+
name?: string;
|
|
193
|
+
discount_percent?: number;
|
|
194
|
+
valid_until?: string;
|
|
195
|
+
uses_remaining?: number | null;
|
|
196
|
+
uses_per_patron?: number | null;
|
|
197
|
+
error?: string;
|
|
198
|
+
error_code?: string;
|
|
199
|
+
}
|
|
200
|
+
export interface ForgetCouponResult {
|
|
201
|
+
success?: boolean;
|
|
202
|
+
coupon_id?: string;
|
|
203
|
+
error?: string;
|
|
204
|
+
error_code?: string;
|
|
205
|
+
}
|
|
206
|
+
/** The coupons this patron has redeemed here. Free. */
|
|
207
|
+
export declare function listMyCoupons(): Promise<ListMyCouponsResult>;
|
|
208
|
+
/** Redeem an operator's code once; the discount then applies on its own. Free. */
|
|
209
|
+
export declare function redeemCoupon(code: string): Promise<RedeemCouponResult>;
|
|
210
|
+
/** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
|
|
211
|
+
export declare function forgetCoupon(couponId: string): Promise<ForgetCouponResult>;
|
|
109
212
|
export interface Kind0 {
|
|
110
213
|
name?: string;
|
|
111
214
|
display_name?: string;
|
package/dist/standardTools.js
CHANGED
|
@@ -56,6 +56,18 @@ export function checkPayment(invoiceId) {
|
|
|
56
56
|
export function getAccountStatement(days = 30) {
|
|
57
57
|
return callTool("account_statement", { days });
|
|
58
58
|
}
|
|
59
|
+
/** The coupons this patron has redeemed here. Free. */
|
|
60
|
+
export function listMyCoupons() {
|
|
61
|
+
return callTool("list_my_coupons", {});
|
|
62
|
+
}
|
|
63
|
+
/** Redeem an operator's code once; the discount then applies on its own. Free. */
|
|
64
|
+
export function redeemCoupon(code) {
|
|
65
|
+
return callTool("redeem_coupon", { code });
|
|
66
|
+
}
|
|
67
|
+
/** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
|
|
68
|
+
export function forgetCoupon(couponId) {
|
|
69
|
+
return callTool("forget_coupon", { coupon_id: couponId });
|
|
70
|
+
}
|
|
59
71
|
export function getNostrProfile(npub) {
|
|
60
72
|
return callTool("get_nostr_profile", { npub });
|
|
61
73
|
}
|
package/dist/table.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paging, sorting and filtering arithmetic for a table, without a framework.
|
|
3
|
+
*
|
|
4
|
+
* Pages are zero-based. A table may be paged by the server (it sends one page
|
|
5
|
+
* and a total) or in the browser (`pageRows` over the whole list); the
|
|
6
|
+
* arithmetic is the same either way.
|
|
7
|
+
*/
|
|
8
|
+
export type SortDir = "asc" | "desc";
|
|
9
|
+
/** The last page's index for `total` rows. Zero when there are none. */
|
|
10
|
+
export declare function lastPage(total: number, pageSize: number): number;
|
|
11
|
+
/** A page index that exists: whole, and between 0 and the last page. */
|
|
12
|
+
export declare function clampPage(page: number, total: number, pageSize: number): number;
|
|
13
|
+
export declare function pageCount(total: number, pageSize: number): number;
|
|
14
|
+
/** One page of an in-memory list. */
|
|
15
|
+
export declare function pageRows<T>(rows: readonly T[], page: number, pageSize: number): T[];
|
|
16
|
+
/**
|
|
17
|
+
* The sort after a header is tapped: the active column flips direction; a new
|
|
18
|
+
* column starts at `initial` (descending by default — newest, largest first).
|
|
19
|
+
*/
|
|
20
|
+
export declare function nextSort(col: string, activeCol: string, dir: SortDir, initial?: SortDir): {
|
|
21
|
+
col: string;
|
|
22
|
+
dir: SortDir;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A case-insensitive test for `pattern`. It is read as a regular expression
|
|
26
|
+
* when it is one, and as plain text when it is not, so a stray "(" in a search
|
|
27
|
+
* box finds the bracket rather than throwing. An empty pattern matches all.
|
|
28
|
+
*/
|
|
29
|
+
export declare function searchMatcher(pattern: string): (text: string) => boolean;
|
|
30
|
+
/** The calendar day ("YYYY-MM-DD") of an ISO stamp or date, or null. */
|
|
31
|
+
export declare function dayOf(value: string | null | undefined): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Whether `value` falls in the inclusive range [from, to], compared by day.
|
|
34
|
+
* An empty bound is open. A row with no date is outside any range that has
|
|
35
|
+
* a bound, and inside one that has none.
|
|
36
|
+
*/
|
|
37
|
+
export declare function inDateRange(value: string | null | undefined, from: string, to: string): boolean;
|
|
38
|
+
export interface RowFilter {
|
|
39
|
+
search: string;
|
|
40
|
+
dateFrom: string;
|
|
41
|
+
dateTo: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const NO_FILTER: RowFilter;
|
|
44
|
+
export declare function filterActive(f: Partial<RowFilter>, summary?: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Filter rows in the browser, for a table the server does not filter.
|
|
47
|
+
* `text` gives what the search reads; `date` the stamp the range reads.
|
|
48
|
+
*/
|
|
49
|
+
export declare function filterRows<T>(rows: readonly T[], f: Partial<RowFilter>, by: {
|
|
50
|
+
text: (row: T) => string;
|
|
51
|
+
date?: (row: T) => string | null | undefined;
|
|
52
|
+
}): T[];
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paging, sorting and filtering arithmetic for a table, without a framework.
|
|
3
|
+
*
|
|
4
|
+
* Pages are zero-based. A table may be paged by the server (it sends one page
|
|
5
|
+
* and a total) or in the browser (`pageRows` over the whole list); the
|
|
6
|
+
* arithmetic is the same either way.
|
|
7
|
+
*/
|
|
8
|
+
/** The last page's index for `total` rows. Zero when there are none. */
|
|
9
|
+
export function lastPage(total, pageSize) {
|
|
10
|
+
if (!(pageSize > 0) || !(total > 0))
|
|
11
|
+
return 0;
|
|
12
|
+
return Math.max(0, Math.ceil(total / pageSize) - 1);
|
|
13
|
+
}
|
|
14
|
+
/** A page index that exists: whole, and between 0 and the last page. */
|
|
15
|
+
export function clampPage(page, total, pageSize) {
|
|
16
|
+
const p = Number.isFinite(page) ? Math.trunc(page) : 0;
|
|
17
|
+
return Math.min(Math.max(0, p), lastPage(total, pageSize));
|
|
18
|
+
}
|
|
19
|
+
export function pageCount(total, pageSize) {
|
|
20
|
+
return lastPage(total, pageSize) + 1;
|
|
21
|
+
}
|
|
22
|
+
/** One page of an in-memory list. */
|
|
23
|
+
export function pageRows(rows, page, pageSize) {
|
|
24
|
+
if (!(pageSize > 0))
|
|
25
|
+
return [...rows];
|
|
26
|
+
const p = clampPage(page, rows.length, pageSize);
|
|
27
|
+
return rows.slice(p * pageSize, (p + 1) * pageSize);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The sort after a header is tapped: the active column flips direction; a new
|
|
31
|
+
* column starts at `initial` (descending by default — newest, largest first).
|
|
32
|
+
*/
|
|
33
|
+
export function nextSort(col, activeCol, dir, initial = "desc") {
|
|
34
|
+
if (col === activeCol)
|
|
35
|
+
return { col, dir: dir === "desc" ? "asc" : "desc" };
|
|
36
|
+
return { col, dir: initial };
|
|
37
|
+
}
|
|
38
|
+
// ── Filtering ──────────────────────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* A case-insensitive test for `pattern`. It is read as a regular expression
|
|
41
|
+
* when it is one, and as plain text when it is not, so a stray "(" in a search
|
|
42
|
+
* box finds the bracket rather than throwing. An empty pattern matches all.
|
|
43
|
+
*/
|
|
44
|
+
export function searchMatcher(pattern) {
|
|
45
|
+
const p = pattern.trim();
|
|
46
|
+
if (!p)
|
|
47
|
+
return () => true;
|
|
48
|
+
try {
|
|
49
|
+
const re = new RegExp(p, "i");
|
|
50
|
+
return (text) => re.test(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
const needle = p.toLowerCase();
|
|
54
|
+
return (text) => text.toLowerCase().includes(needle);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The calendar day ("YYYY-MM-DD") of an ISO stamp or date, or null. */
|
|
58
|
+
export function dayOf(value) {
|
|
59
|
+
if (!value)
|
|
60
|
+
return null;
|
|
61
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(value);
|
|
62
|
+
return m ? m[1] : null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether `value` falls in the inclusive range [from, to], compared by day.
|
|
66
|
+
* An empty bound is open. A row with no date is outside any range that has
|
|
67
|
+
* a bound, and inside one that has none.
|
|
68
|
+
*/
|
|
69
|
+
export function inDateRange(value, from, to) {
|
|
70
|
+
if (!from && !to)
|
|
71
|
+
return true;
|
|
72
|
+
const day = dayOf(value);
|
|
73
|
+
if (!day)
|
|
74
|
+
return false;
|
|
75
|
+
if (from && day < from)
|
|
76
|
+
return false;
|
|
77
|
+
if (to && day > to)
|
|
78
|
+
return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
export const NO_FILTER = { search: "", dateFrom: "", dateTo: "" };
|
|
82
|
+
export function filterActive(f, summary = "") {
|
|
83
|
+
return !!(f.search?.trim() || f.dateFrom || f.dateTo || summary);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Filter rows in the browser, for a table the server does not filter.
|
|
87
|
+
* `text` gives what the search reads; `date` the stamp the range reads.
|
|
88
|
+
*/
|
|
89
|
+
export function filterRows(rows, f, by) {
|
|
90
|
+
const match = searchMatcher(f.search ?? "");
|
|
91
|
+
const from = f.dateFrom ?? "";
|
|
92
|
+
const to = f.dateTo ?? "";
|
|
93
|
+
const ranged = !!(from || to) && !!by.date;
|
|
94
|
+
return rows.filter((r) => match(by.text(r)) && (!ranged || inDateRange(by.date(r), from, to)));
|
|
95
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light, dark or the system's choice — kept under `<prefix>:theme` so a
|
|
3
|
+
* returning patron keeps their pick, and applied to <html> before first paint.
|
|
4
|
+
*
|
|
5
|
+
* Mechanics only; the colours are the site's. Applying a theme sets all three
|
|
6
|
+
* hooks a stylesheet might key on, so every fleet site's CSS keeps working:
|
|
7
|
+
* - the `dark` class (Tailwind `darkMode: "class"`, `@custom-variant dark`),
|
|
8
|
+
* - `data-theme="light" | "dark"` (the package's theme.css and token sheets),
|
|
9
|
+
* - `color-scheme` (native form controls and scrollbars).
|
|
10
|
+
* "system" resolves to the OS preference and follows it live.
|
|
11
|
+
*/
|
|
12
|
+
export type Theme = "dark" | "light" | "system";
|
|
13
|
+
export type EffectiveTheme = "dark" | "light";
|
|
14
|
+
export declare const THEMES: readonly Theme[];
|
|
15
|
+
export declare function isTheme(v: unknown): v is Theme;
|
|
16
|
+
/** The stored pick, or `fallback` when nothing (or nonsense) is stored. */
|
|
17
|
+
export declare function parseTheme(raw: string | null | undefined, fallback?: Theme): Theme;
|
|
18
|
+
export declare function themeStorageKey(): string;
|
|
19
|
+
export declare function readTheme(fallback?: Theme): Theme;
|
|
20
|
+
export declare function writeTheme(theme: Theme): void;
|
|
21
|
+
export declare function systemPrefersDark(): boolean;
|
|
22
|
+
export declare function resolveTheme(theme: Theme, prefersDark?: boolean): EffectiveTheme;
|
|
23
|
+
/** The part of <html> a theme touches — narrow, so it is testable without a DOM. */
|
|
24
|
+
export interface ThemeTarget {
|
|
25
|
+
classList: {
|
|
26
|
+
toggle(token: string, force?: boolean): unknown;
|
|
27
|
+
};
|
|
28
|
+
setAttribute(name: string, value: string): void;
|
|
29
|
+
style: {
|
|
30
|
+
colorScheme: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function applyTheme(theme: Theme, target?: ThemeTarget, prefersDark?: boolean): EffectiveTheme;
|
|
34
|
+
/**
|
|
35
|
+
* Apply the stored theme. Call once in main.tsx, after `configureTollbooth`
|
|
36
|
+
* and before rendering, so the first frame has the right palette.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bootstrapTheme(fallback?: Theme): Theme;
|
|
39
|
+
/** The pick after `current`, for a single chip that cycles. */
|
|
40
|
+
export declare function nextTheme(current: Theme, order?: readonly Theme[]): Theme;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light, dark or the system's choice — kept under `<prefix>:theme` so a
|
|
3
|
+
* returning patron keeps their pick, and applied to <html> before first paint.
|
|
4
|
+
*
|
|
5
|
+
* Mechanics only; the colours are the site's. Applying a theme sets all three
|
|
6
|
+
* hooks a stylesheet might key on, so every fleet site's CSS keeps working:
|
|
7
|
+
* - the `dark` class (Tailwind `darkMode: "class"`, `@custom-variant dark`),
|
|
8
|
+
* - `data-theme="light" | "dark"` (the package's theme.css and token sheets),
|
|
9
|
+
* - `color-scheme` (native form controls and scrollbars).
|
|
10
|
+
* "system" resolves to the OS preference and follows it live.
|
|
11
|
+
*/
|
|
12
|
+
import { tollboothConfig } from "./config.js";
|
|
13
|
+
export const THEMES = ["dark", "light", "system"];
|
|
14
|
+
export function isTheme(v) {
|
|
15
|
+
return v === "dark" || v === "light" || v === "system";
|
|
16
|
+
}
|
|
17
|
+
/** The stored pick, or `fallback` when nothing (or nonsense) is stored. */
|
|
18
|
+
export function parseTheme(raw, fallback = "dark") {
|
|
19
|
+
return isTheme(raw) ? raw : fallback;
|
|
20
|
+
}
|
|
21
|
+
export function themeStorageKey() {
|
|
22
|
+
return `${tollboothConfig().storagePrefix}:theme`;
|
|
23
|
+
}
|
|
24
|
+
export function readTheme(fallback = "dark") {
|
|
25
|
+
try {
|
|
26
|
+
return parseTheme(globalThis.localStorage?.getItem(themeStorageKey()), fallback);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return fallback; // blocked site data, or not configured yet
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function writeTheme(theme) {
|
|
33
|
+
try {
|
|
34
|
+
globalThis.localStorage?.setItem(themeStorageKey(), theme);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* the choice still applies for this page */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function systemPrefersDark() {
|
|
41
|
+
try {
|
|
42
|
+
return !!globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function resolveTheme(theme, prefersDark = systemPrefersDark()) {
|
|
49
|
+
if (theme === "system")
|
|
50
|
+
return prefersDark ? "dark" : "light";
|
|
51
|
+
return theme;
|
|
52
|
+
}
|
|
53
|
+
export function applyTheme(theme, target, prefersDark) {
|
|
54
|
+
const effective = resolveTheme(theme, prefersDark);
|
|
55
|
+
const el = target ?? globalThis.document?.documentElement;
|
|
56
|
+
if (el) {
|
|
57
|
+
el.classList.toggle("dark", effective === "dark");
|
|
58
|
+
el.setAttribute("data-theme", effective);
|
|
59
|
+
el.style.colorScheme = effective;
|
|
60
|
+
}
|
|
61
|
+
return effective;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Apply the stored theme. Call once in main.tsx, after `configureTollbooth`
|
|
65
|
+
* and before rendering, so the first frame has the right palette.
|
|
66
|
+
*/
|
|
67
|
+
export function bootstrapTheme(fallback = "dark") {
|
|
68
|
+
const theme = readTheme(fallback);
|
|
69
|
+
applyTheme(theme);
|
|
70
|
+
return theme;
|
|
71
|
+
}
|
|
72
|
+
/** The pick after `current`, for a single chip that cycles. */
|
|
73
|
+
export function nextTheme(current, order = THEMES) {
|
|
74
|
+
const i = order.indexOf(current);
|
|
75
|
+
return order[(i + 1) % order.length] ?? order[0];
|
|
76
|
+
}
|