@phala/pay 0.8.2 → 0.8.3
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 +41 -3
- package/THIRD_PARTY_NOTICES +25 -0
- package/dist/chains.d.ts +2 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -2
- package/dist/chains.js.map +1 -1
- package/dist/checkout.js +2 -2
- package/dist/checkout.js.map +1 -1
- package/dist/deposit-address.js +2 -2
- package/dist/deposit-address.js.map +1 -1
- package/dist/icon-names.d.ts +7 -0
- package/dist/icon-names.d.ts.map +1 -0
- package/dist/icon-names.js +15 -0
- package/dist/icon-names.js.map +1 -0
- package/dist/icon-svg.d.ts +6 -0
- package/dist/icon-svg.d.ts.map +1 -0
- package/dist/icon-svg.js +13 -0
- package/dist/icon-svg.js.map +1 -0
- package/dist/icons.d.ts +5 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +27 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Checkout.d.ts.map +1 -1
- package/dist/react/Checkout.js +24 -22
- package/dist/react/Checkout.js.map +1 -1
- package/dist/react/DepositAddress.d.ts.map +1 -1
- package/dist/react/DepositAddress.js +6 -6
- package/dist/react/DepositAddress.js.map +1 -1
- package/dist/react/Icons.d.ts +17 -0
- package/dist/react/Icons.d.ts.map +1 -0
- package/dist/react/Icons.js +18 -0
- package/dist/react/Icons.js.map +1 -0
- package/dist/react/icon-paths.d.ts +13 -0
- package/dist/react/icon-paths.d.ts.map +1 -0
- package/dist/react/icon-paths.js +14 -0
- package/dist/react/icon-paths.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/request.d.ts +2 -0
- package/dist/request.d.ts.map +1 -1
- package/dist/request.js +7 -0
- package/dist/request.js.map +1 -1
- package/package.json +9 -4
- package/src/chains.ts +12 -2
- package/src/checkout.ts +2 -2
- package/src/deposit-address.ts +2 -2
- package/src/generated/openapi.d.ts +9482 -0
- package/src/icon-names.ts +20 -0
- package/src/icon-svg.ts +13 -0
- package/src/icons.ts +30 -0
- package/src/index.ts +1 -0
- package/src/react/Checkout.tsx +42 -36
- package/src/react/DepositAddress.tsx +10 -9
- package/src/react/Icons.tsx +43 -0
- package/src/react/icon-paths.tsx +13 -0
- package/src/react/index.ts +1 -0
- package/src/request.ts +7 -0
- package/src/styles.css +22 -18
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ICON_SVG } from "./icon-svg.js";
|
|
2
|
+
|
|
3
|
+
export type IconName = keyof typeof ICON_SVG;
|
|
4
|
+
|
|
5
|
+
/** Case-insensitive token symbol; unrecognized symbols use a neutral monogram. */
|
|
6
|
+
export function assetIconName(asset: string): IconName | undefined {
|
|
7
|
+
switch (asset.trim().toLowerCase()) {
|
|
8
|
+
case "usdc": return "usdc";
|
|
9
|
+
case "usdt": return "usdt";
|
|
10
|
+
case "pha": return "pha";
|
|
11
|
+
case "eth": return "eth";
|
|
12
|
+
default: return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** First character for a neutral fallback, including an empty-symbol placeholder. */
|
|
17
|
+
export function iconMonogram(label: string): string {
|
|
18
|
+
return Array.from(label.trim())[0]?.toUpperCase() ?? "?";
|
|
19
|
+
}
|
|
20
|
+
|
package/src/icon-svg.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Generated from @web3icons/core@4.0.57 by scripts/generate-icons.mjs.
|
|
2
|
+
* SPDX-License-Identifier: MIT; Copyright (c) 2024 0xa3k5.
|
|
3
|
+
* See THIRD_PARTY_NOTICES for the full license. Do not edit.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ICON_SVG: Record<"usdc" | "usdt" | "pha" | "eth" | "ethereum" | "base", string> = {
|
|
7
|
+
usdc: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#0B53BF\" d=\"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18\"/><path fill=\"#fff\" d=\"M13.62 5.45v1.159a5.64 5.64 0 0 1 4.005 5.394 5.64 5.64 0 0 1-4.005 5.394v1.16a6.74 6.74 0 0 0 5.13-6.554 6.74 6.74 0 0 0-5.13-6.553m-7.245 6.553a5.64 5.64 0 0 1 4.005-5.394V5.45a6.74 6.74 0 0 0-5.13 6.553 6.74 6.74 0 0 0 5.13 6.553v-1.159a5.63 5.63 0 0 1-4.005-5.394\"/><path fill=\"#fff\" d=\"M14.419 13.258c0-2.301-3.606-1.356-3.606-2.627 0-.456.366-.748 1.063-.748.833 0 1.12.405 1.21.95h1.147c-.102-1.024-.69-1.67-1.67-1.863v-.904h-1.125v.872c-1.075.137-1.75.762-1.75 1.693 0 2.312 3.611 1.445 3.611 2.694 0 .472-.455.787-1.226.787-1.007 0-1.339-.444-1.462-1.057H9.49c.073 1.122.764 1.823 1.947 1.999v.886h1.125v-.875c1.153-.149 1.856-.82 1.856-1.807\"/></svg>",
|
|
8
|
+
usdt: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#009393\" d=\"m12 19.2-9-8.88L6.433 4.8h11.134L21 10.32zm.9-8.1v-1.098c1.62.08 3.132.396 3.6.805-.544.477-2.493.824-4.5.824s-3.956-.347-4.5-.824c.463-.41 1.98-.72 3.6-.81V11.1zm-5.4-.297v.661c.463.41 1.975.72 3.6.81V14.7h1.8v-2.43c1.62-.081 3.136-.396 3.6-.806v-1.318c-.464-.41-1.98-.725-3.6-.81V8.4h2.7V7.05H8.4V8.4h2.7v.936c-1.625.085-3.137.4-3.6.81z\"/></svg>",
|
|
9
|
+
pha: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"#252525\"/><path fill=\"#D1FF52\" fill-rule=\"evenodd\" d=\"M3 3h14.143v3.857H21V12h-3.857V6.857H6.857V12H3zm3.857 12.857h10.286V12H6.857zm0 0V21H3v-5.143z\" clip-rule=\"evenodd\"/></svg>",
|
|
10
|
+
eth: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#8FFCF3\" d=\"M12 3v6.65l5.625 2.516z\"/><path fill=\"#CABCF8\" d=\"m12 3-5.625 9.166L12 9.651z\"/><path fill=\"#CBA7F5\" d=\"M12 16.477v4.522l5.625-7.784z\"/><path fill=\"#74A0F3\" d=\"M12 21v-4.523l-5.625-3.262z\"/><path fill=\"#CBA7F5\" d=\"m12 15.43 5.625-3.263L12 9.65z\"/><path fill=\"#74A0F3\" d=\"M6.375 12.167 12 15.429V9.651z\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"m12 15.429-5.625-3.263L12 3l5.625 9.166zM6.749 11.9l5.16-8.41v6.115zm-.077.23 5.238-2.327v5.364zm5.418-2.327v5.364l5.233-3.038zm0-.198 5.16 2.295-5.16-8.41z\" clip-rule=\"evenodd\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"M12 16.406 6.375 13.21 12 21l5.625-7.79zm-4.995-2.633 4.905 2.79v4.005zm5.085 2.79v4.005l4.905-6.795z\" clip-rule=\"evenodd\"/></svg>",
|
|
11
|
+
ethereum: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#8FFCF3\" d=\"M12 3v6.651l5.625 2.516z\"/><path fill=\"#CABCF8\" d=\"m12 3-5.625 9.166L12 9.653z\"/><path fill=\"#CBA7F5\" d=\"M12 16.478V21l5.625-7.784z\"/><path fill=\"#74A0F3\" d=\"M12 21v-4.522l-5.625-3.262z\"/><path fill=\"#CBA7F5\" d=\"m12 15.43 5.625-3.263L12 9.652z\"/><path fill=\"#74A0F3\" d=\"M6.375 12.167 12 15.43V9.652z\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"m12 15.43-5.625-3.263L12 3l5.624 9.166zm-5.252-3.528 5.161-8.41v6.114zm-.077.229 5.238-2.327v5.364zm5.418-2.327v5.364l5.234-3.037zm0-.198 5.161 2.296-5.161-8.41z\" clip-rule=\"evenodd\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"m12 16.406-5.625-3.195L12 21l5.624-7.79zm-4.995-2.633 4.904 2.79v4.005zm5.084 2.79v4.005l4.905-6.795z\" clip-rule=\"evenodd\"/></svg>",
|
|
12
|
+
base: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#00F\" d=\"M3 4.706c0-.585 0-.877.11-1.101.106-.215.28-.39.496-.495C3.83 3 4.122 3 4.706 3h14.588c.585 0 .876 0 1.101.11.215.105.389.28.494.495.111.225.111.517.111 1.101v14.588c0 .585 0 .876-.11 1.101-.106.215-.28.389-.495.494-.225.111-.517.111-1.101.111H4.706c-.585 0-.876 0-1.101-.11a1.08 1.08 0 0 1-.494-.495C3 20.17 3 19.878 3 19.294z\"/></svg>",
|
|
13
|
+
};
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { networkName, networkIconName } from "./chains.js";
|
|
2
|
+
import { ICON_SVG } from "./icon-svg.js";
|
|
3
|
+
|
|
4
|
+
import { assetIconName, iconMonogram } from "./icon-names.js";
|
|
5
|
+
|
|
6
|
+
function fallback(label: string): string {
|
|
7
|
+
const letter = iconMonogram(label).replace(/[&<>"']/g, (character) => {
|
|
8
|
+
switch (character) {
|
|
9
|
+
case "&": return "&";
|
|
10
|
+
case "<": return "<";
|
|
11
|
+
case ">": return ">";
|
|
12
|
+
case '"': return """;
|
|
13
|
+
default: return "'";
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor" opacity="0.12"/><text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-size="12" fill="currentColor">${letter}</text></svg>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Decorative inline SVG markup; testnets reuse their mainnet family artwork. */
|
|
20
|
+
export function networkIcon(chainId: number): string {
|
|
21
|
+
const name = networkIconName(chainId);
|
|
22
|
+
const svg = name === undefined ? fallback(networkName(chainId)) : ICON_SVG[name];
|
|
23
|
+
return svg;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Decorative inline SVG markup; keep the asset name visible beside it. */
|
|
27
|
+
export function assetIcon(asset: string): string {
|
|
28
|
+
const name = assetIconName(asset);
|
|
29
|
+
return name === undefined ? fallback(asset) : ICON_SVG[name];
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
package/src/react/Checkout.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type Wallet,
|
|
23
23
|
} from "../wallet.js";
|
|
24
24
|
import type { Appearance } from "./appearance.js";
|
|
25
|
+
import { AssetIcon, NetworkIcon } from "./Icons.js";
|
|
25
26
|
import { Field } from "./Field.js";
|
|
26
27
|
import { QrCode } from "./QrCode.js";
|
|
27
28
|
import { useCheckout } from "./useCheckout.js";
|
|
@@ -65,9 +66,9 @@ export interface CheckoutProps {
|
|
|
65
66
|
type Method = "wallet" | "qr" | "manual";
|
|
66
67
|
|
|
67
68
|
const METHODS: { id: Method; label: string }[] = [
|
|
68
|
-
{ id: "wallet", label: "
|
|
69
|
+
{ id: "wallet", label: "Wallet" },
|
|
69
70
|
{ id: "qr", label: "QR code" },
|
|
70
|
-
{ id: "manual", label: "
|
|
71
|
+
{ id: "manual", label: "Manual" },
|
|
71
72
|
];
|
|
72
73
|
|
|
73
74
|
/** A checkout for one quote: pay from a browser wallet, by QR code, or manually, with live status. */
|
|
@@ -132,10 +133,12 @@ export function Checkout({
|
|
|
132
133
|
{quote !== null && (
|
|
133
134
|
<>
|
|
134
135
|
<p className="pp-amount">
|
|
136
|
+
<AssetIcon asset={quote.asset} size={20} />{" "}
|
|
135
137
|
{formatTokenAmount(quote)} {quote.asset.toUpperCase()}
|
|
136
138
|
</p>
|
|
137
139
|
<p className="pp-subtitle">
|
|
138
|
-
{formatAmount(quote)} top-up · {
|
|
140
|
+
{formatAmount(quote)} top-up · <NetworkIcon chainId={quote.chain_id} size={16} />{" "}
|
|
141
|
+
{networkName(quote.chain_id)}
|
|
139
142
|
{!quote.livemode && <span className="pp-badge"> · Test mode</span>}
|
|
140
143
|
</p>
|
|
141
144
|
</>
|
|
@@ -147,7 +150,6 @@ export function Checkout({
|
|
|
147
150
|
quote={quote}
|
|
148
151
|
buttonText={buttonText}
|
|
149
152
|
walletClient={walletClient}
|
|
150
|
-
now={now}
|
|
151
153
|
onSent={(hash) => {
|
|
152
154
|
setTxHash(hash);
|
|
153
155
|
refresh();
|
|
@@ -204,17 +206,17 @@ function statusMessage(
|
|
|
204
206
|
case "seen":
|
|
205
207
|
return seenMessage(quote);
|
|
206
208
|
case "confirming":
|
|
207
|
-
return "
|
|
209
|
+
return "Confirmed on chain, crediting…";
|
|
208
210
|
case "credited":
|
|
209
211
|
return creditedMessage(quote);
|
|
210
212
|
case "rejected":
|
|
211
213
|
return "This payment cannot be credited. Contact support with your transaction.";
|
|
212
214
|
case "reversed":
|
|
213
|
-
return "
|
|
215
|
+
return "Payment reversed: a chain reorganization replaced it and its credit was removed. Check your wallet, then start a new top-up.";
|
|
214
216
|
case "expired":
|
|
215
|
-
return "
|
|
217
|
+
return "Quote expired. Do not send funds; start a new top-up.";
|
|
216
218
|
case "canceled":
|
|
217
|
-
return "
|
|
219
|
+
return "Quote canceled. Do not send funds.";
|
|
218
220
|
case "error":
|
|
219
221
|
return code === "address_mismatch"
|
|
220
222
|
? "This payment address could not be verified. Do not send funds; contact support."
|
|
@@ -224,12 +226,12 @@ function statusMessage(
|
|
|
224
226
|
|
|
225
227
|
function seenMessage(quote: ClientQuote | null): string {
|
|
226
228
|
if (quote === null) {
|
|
227
|
-
return "
|
|
229
|
+
return "Received, crediting after confirmation";
|
|
228
230
|
}
|
|
229
231
|
const wait = formatWait(quote.typical_credit_seconds);
|
|
230
232
|
return quote.confirmations === null
|
|
231
|
-
? `
|
|
232
|
-
: `
|
|
233
|
+
? `Received, crediting in ${wait}`
|
|
234
|
+
: `Received, ${quote.confirmations} confirmation${quote.confirmations === 1 ? "" : "s"}. Crediting in ${wait}`;
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
/** What was credited and, when the payment was valued at the market price, the quote it missed. */
|
|
@@ -242,7 +244,7 @@ function creditedMessage(quote: ClientQuote | null): string {
|
|
|
242
244
|
return `Payment credited: ${credited} of ${formatAmount(quote)}`;
|
|
243
245
|
}
|
|
244
246
|
if (quote.amount_credited > quote.amount) {
|
|
245
|
-
return `Payment credited: ${credited}
|
|
247
|
+
return `Payment credited: ${credited} (${formatAmount(quote)} quoted)`;
|
|
246
248
|
}
|
|
247
249
|
return `Payment credited: ${credited}`;
|
|
248
250
|
}
|
|
@@ -265,20 +267,24 @@ function Transaction({ hash, chainId }: { hash: Hash; chainId: number }) {
|
|
|
265
267
|
|
|
266
268
|
function PaymentOptions({
|
|
267
269
|
quote,
|
|
268
|
-
now,
|
|
269
270
|
buttonText,
|
|
270
271
|
walletClient,
|
|
271
272
|
onSent,
|
|
272
273
|
onWalletError,
|
|
273
274
|
}: {
|
|
274
275
|
quote: ClientQuote;
|
|
275
|
-
now: number;
|
|
276
276
|
buttonText: string;
|
|
277
277
|
walletClient: WalletClient | undefined;
|
|
278
278
|
onSent: (hash: Hash) => void;
|
|
279
279
|
onWalletError: CheckoutProps["onWalletError"];
|
|
280
280
|
}) {
|
|
281
|
-
const [
|
|
281
|
+
const [selectedMethod, setMethod] = useState<Method | null>(null);
|
|
282
|
+
const [discovered, setDiscovered] = useState<Wallet[]>([]);
|
|
283
|
+
useEffect(
|
|
284
|
+
() => (walletClient === undefined ? watchWallets(setDiscovered) : undefined),
|
|
285
|
+
[walletClient],
|
|
286
|
+
);
|
|
287
|
+
const method = selectedMethod ?? (walletClient !== undefined || discovered.length > 0 ? "wallet" : "qr");
|
|
282
288
|
const id = useId();
|
|
283
289
|
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
284
290
|
const amount = `${formatTokenAmount(quote)} ${quote.asset.toUpperCase()}`;
|
|
@@ -307,9 +313,7 @@ function PaymentOptions({
|
|
|
307
313
|
<>
|
|
308
314
|
<p className="pp-notice">
|
|
309
315
|
Send <strong>exactly {amount}</strong> on {networkName(quote.chain_id)} in one transfer
|
|
310
|
-
before the timer ends.
|
|
311
|
-
market price instead of this quote. Exchanges may deduct a withdrawal fee, so the amount
|
|
312
|
-
that arrives must be exact.
|
|
316
|
+
before the timer ends. Otherwise it’s credited at the market rate.
|
|
313
317
|
</p>
|
|
314
318
|
<div className="pp-tabs" role="tablist" aria-label="Payment method" onKeyDown={onKeyDown}>
|
|
315
319
|
{METHODS.map((m, index) => (
|
|
@@ -322,6 +326,7 @@ function PaymentOptions({
|
|
|
322
326
|
role="tab"
|
|
323
327
|
className="pp-tab"
|
|
324
328
|
id={`${id}-tab-${m.id}`}
|
|
329
|
+
aria-label={m.id === "wallet" ? "Browser wallet" : m.id === "manual" ? "Manual transfer" : m.label}
|
|
325
330
|
aria-selected={method === m.id}
|
|
326
331
|
aria-controls={`${id}-panel-${m.id}`}
|
|
327
332
|
tabIndex={method === m.id ? 0 : -1}
|
|
@@ -342,6 +347,7 @@ function PaymentOptions({
|
|
|
342
347
|
quote={quote}
|
|
343
348
|
buttonText={buttonText}
|
|
344
349
|
walletClient={walletClient}
|
|
350
|
+
discovered={discovered}
|
|
345
351
|
onSent={onSent}
|
|
346
352
|
onWalletError={onWalletError}
|
|
347
353
|
/>
|
|
@@ -350,12 +356,11 @@ function PaymentOptions({
|
|
|
350
356
|
<div className="pp-qr-panel">
|
|
351
357
|
<QrCode value={quote.payment_uri} label={`Payment request for ${amount}`} />
|
|
352
358
|
<p className="pp-message">
|
|
353
|
-
Scan with
|
|
354
|
-
{networkName(quote.chain_id)} before you confirm.
|
|
359
|
+
Scan with your wallet app and check it shows {amount} on {networkName(quote.chain_id)}.
|
|
355
360
|
</p>
|
|
356
361
|
</div>
|
|
357
362
|
)}
|
|
358
|
-
{method === "manual" && <ManualPanel quote={quote}
|
|
363
|
+
{method === "manual" && <ManualPanel quote={quote} />}
|
|
359
364
|
</div>
|
|
360
365
|
</>
|
|
361
366
|
);
|
|
@@ -370,21 +375,18 @@ function WalletPanel({
|
|
|
370
375
|
quote,
|
|
371
376
|
buttonText,
|
|
372
377
|
walletClient,
|
|
378
|
+
discovered,
|
|
373
379
|
onSent,
|
|
374
380
|
onWalletError,
|
|
375
381
|
}: {
|
|
376
382
|
quote: ClientQuote;
|
|
383
|
+
discovered: Wallet[];
|
|
377
384
|
buttonText: string;
|
|
378
385
|
walletClient: WalletClient | undefined;
|
|
379
386
|
onSent: (hash: Hash) => void;
|
|
380
387
|
onWalletError: CheckoutProps["onWalletError"];
|
|
381
388
|
}) {
|
|
382
|
-
const [discovered, setDiscovered] = useState<Wallet[]>([]);
|
|
383
389
|
const [step, setStep] = useState<WalletStep>({ kind: "idle" });
|
|
384
|
-
useEffect(
|
|
385
|
-
() => (walletClient === undefined ? watchWallets(setDiscovered) : undefined),
|
|
386
|
-
[walletClient],
|
|
387
|
-
);
|
|
388
390
|
const wallets: { id: string; name: string; icon: string; wallet: WalletClient | EthereumProvider }[] =
|
|
389
391
|
walletClient === undefined
|
|
390
392
|
? discovered.map(({ info: { uuid, name, icon }, provider }) => ({ id: uuid, name, icon, wallet: provider }))
|
|
@@ -409,8 +411,7 @@ function WalletPanel({
|
|
|
409
411
|
if (wallets.length === 0) {
|
|
410
412
|
return (
|
|
411
413
|
<p className="pp-message">
|
|
412
|
-
|
|
413
|
-
manually.
|
|
414
|
+
Install a browser wallet to pay here.
|
|
414
415
|
</p>
|
|
415
416
|
);
|
|
416
417
|
}
|
|
@@ -442,16 +443,21 @@ function WalletPanel({
|
|
|
442
443
|
);
|
|
443
444
|
}
|
|
444
445
|
|
|
445
|
-
function ManualPanel({ quote
|
|
446
|
+
function ManualPanel({ quote }: { quote: ClientQuote }) {
|
|
446
447
|
const token = quoteTransfer(quote).token;
|
|
447
448
|
return (
|
|
448
|
-
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
449
|
+
<>
|
|
450
|
+
<dl className="pp-fields">
|
|
451
|
+
<Field label="Network" value={`${networkName(quote.chain_id)} (chain ID ${quote.chain_id})`} />
|
|
452
|
+
<Field label={`Token (${quote.asset.toUpperCase()}) contract`} value={token} copy />
|
|
453
|
+
<Field label="Send to address" value={quote.address} copy />
|
|
454
|
+
<Field label="Exact amount" value={formatTokenAmount(quote)} copy={tokenAmount(quote)} />
|
|
455
|
+
</dl>
|
|
456
|
+
<p className="pp-message">
|
|
457
|
+
Only {quote.asset.toUpperCase()} on {networkName(quote.chain_id)} is credited. Exchange
|
|
458
|
+
withdrawal fees must not reduce the amount received.
|
|
459
|
+
</p>
|
|
460
|
+
</>
|
|
455
461
|
);
|
|
456
462
|
}
|
|
457
463
|
|
|
@@ -12,6 +12,7 @@ import { CheckoutError, pollDelay } from "../checkout.js";
|
|
|
12
12
|
import { formatWait } from "../format.js";
|
|
13
13
|
import { depositAddressTransfer, type DepositAddressDetails } from "../payment.js";
|
|
14
14
|
import type { Appearance } from "./appearance.js";
|
|
15
|
+
import { AssetIcon, NetworkIcon } from "./Icons.js";
|
|
15
16
|
import { Field } from "./Field.js";
|
|
16
17
|
import { QrCode } from "./QrCode.js";
|
|
17
18
|
|
|
@@ -72,7 +73,6 @@ export function DepositAddress({
|
|
|
72
73
|
const { token: contract, to } = depositAddressTransfer(network, token);
|
|
73
74
|
const symbol = token.asset.toUpperCase();
|
|
74
75
|
const name = networkName(network.chain_id);
|
|
75
|
-
const tokens = [...new Set(networks.flatMap((each) => each.assets.map((a) => a.asset.toUpperCase())))];
|
|
76
76
|
return (
|
|
77
77
|
<div
|
|
78
78
|
className={className === undefined ? "pp-root" : `pp-root ${className}`}
|
|
@@ -80,8 +80,8 @@ export function DepositAddress({
|
|
|
80
80
|
>
|
|
81
81
|
<p className="pp-subtitle">
|
|
82
82
|
{depositAddress.address === null
|
|
83
|
-
? "
|
|
84
|
-
: "One address for
|
|
83
|
+
? "Reusable deposit address; varies by network"
|
|
84
|
+
: "One reusable address for supported tokens and networks"}
|
|
85
85
|
</p>
|
|
86
86
|
{networks.length > 1 && (
|
|
87
87
|
<fieldset className="pp-choices">
|
|
@@ -94,6 +94,7 @@ export function DepositAddress({
|
|
|
94
94
|
checked={each.chain_id === network.chain_id}
|
|
95
95
|
onChange={() => setSelectedChain(each.chain_id)}
|
|
96
96
|
/>
|
|
97
|
+
<NetworkIcon chainId={each.chain_id} size={16} />{" "}
|
|
97
98
|
{networkName(each.chain_id)}
|
|
98
99
|
</label>
|
|
99
100
|
))}
|
|
@@ -110,6 +111,7 @@ export function DepositAddress({
|
|
|
110
111
|
checked={each.asset === token.asset}
|
|
111
112
|
onChange={() => setSelectedAsset(each.asset)}
|
|
112
113
|
/>
|
|
114
|
+
<AssetIcon asset={each.asset} size={16} />{" "}
|
|
113
115
|
{each.asset.toUpperCase()}
|
|
114
116
|
</label>
|
|
115
117
|
))}
|
|
@@ -133,9 +135,8 @@ export function DepositAddress({
|
|
|
133
135
|
</ul>
|
|
134
136
|
)}
|
|
135
137
|
<p className="pp-message">
|
|
136
|
-
Send
|
|
137
|
-
|
|
138
|
-
credited. You can reuse this address.
|
|
138
|
+
Send {symbol} on {name}. Unsupported tokens or networks are not credited.{" "}
|
|
139
|
+
{creditMessage(networks.map((each) => each.chain_id), view)}
|
|
139
140
|
</p>
|
|
140
141
|
</div>
|
|
141
142
|
);
|
|
@@ -205,7 +206,7 @@ function creditMessage(chainIds: number[], view: ClientDepositAddress | null): s
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
if (names.size === 0) {
|
|
208
|
-
return "Any amount is credited at the market rate
|
|
209
|
+
return "Any amount is credited at the market rate after confirmation.";
|
|
209
210
|
}
|
|
210
211
|
const list = new Intl.ListFormat("en", { type: "conjunction" });
|
|
211
212
|
const [only, ...others] = names.keys();
|
|
@@ -213,7 +214,7 @@ function creditMessage(chainIds: number[], view: ClientDepositAddress | null): s
|
|
|
213
214
|
only !== undefined && others.length === 0
|
|
214
215
|
? formatWait(only)
|
|
215
216
|
: list.format([...names].map(([seconds, on]) => `${formatWait(seconds)} on ${list.format(on)}`));
|
|
216
|
-
return `Any amount is credited at the market rate
|
|
217
|
+
return `Any amount is credited at the market rate on arrival, usually in ${usually}.`;
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
function paymentMessage(payment: DepositAddressPayment): string {
|
|
@@ -234,6 +235,6 @@ function paymentMessage(payment: DepositAddressPayment): string {
|
|
|
234
235
|
case "rejected":
|
|
235
236
|
return `${amount} ${on} cannot be credited. Contact support with your transaction.`;
|
|
236
237
|
case "reversed":
|
|
237
|
-
return `${amount} ${on} was reversed:
|
|
238
|
+
return `${amount} ${on} was reversed: a chain reorganization replaced it and its credit was removed.`;
|
|
238
239
|
}
|
|
239
240
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { networkName, networkIconName } from "../chains.js";
|
|
4
|
+
import { assetIconName, iconMonogram, type IconName } from "../icon-names.js";
|
|
5
|
+
import { ICON_PATHS } from "./icon-paths.js";
|
|
6
|
+
|
|
7
|
+
export interface IconProps {
|
|
8
|
+
/** Width and height in pixels; default 18. No inline styles are injected. */
|
|
9
|
+
size?: number;
|
|
10
|
+
/** Default true next to visible text. Set false when the icon is used alone. */
|
|
11
|
+
decorative?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface NetworkIconProps extends IconProps { chainId: number }
|
|
14
|
+
export interface AssetIconProps extends IconProps { asset: string }
|
|
15
|
+
|
|
16
|
+
function Icon({ name, label, size = 18, decorative = true }: IconProps & {
|
|
17
|
+
name: IconName | undefined;
|
|
18
|
+
label: string;
|
|
19
|
+
}) {
|
|
20
|
+
const pixels = Number.isFinite(size) && size > 0 ? size : 18;
|
|
21
|
+
return (
|
|
22
|
+
<span className="pp-icon-label" role={decorative ? undefined : "img"}
|
|
23
|
+
aria-label={decorative ? undefined : label || "Unknown asset"}>
|
|
24
|
+
<svg className="pp-icon" xmlns="http://www.w3.org/2000/svg" width={pixels} height={pixels}
|
|
25
|
+
viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
|
|
26
|
+
{name === undefined ? <>
|
|
27
|
+
<circle cx="12" cy="12" r="11" fill="currentColor" opacity="0.12" />
|
|
28
|
+
<text x="12" y="16" textAnchor="middle" fontFamily="sans-serif" fontSize="12" fill="currentColor">{iconMonogram(label)}</text>
|
|
29
|
+
</> : ICON_PATHS[name]}
|
|
30
|
+
</svg>
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Branded network icon; testnets reuse their mainnet family artwork. */
|
|
36
|
+
export function NetworkIcon({ chainId, ...props }: NetworkIconProps) {
|
|
37
|
+
return <Icon {...props} name={networkIconName(chainId)} label={networkName(chainId)} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Branded token icon, or a neutral first-letter monogram for unknown symbols. */
|
|
41
|
+
export function AssetIcon({ asset, ...props }: AssetIconProps) {
|
|
42
|
+
return <Icon {...props} name={assetIconName(asset)} label={asset.trim().toUpperCase()} />;
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Generated from @web3icons/core@4.0.57 by scripts/generate-icons.mjs.
|
|
2
|
+
* SPDX-License-Identifier: MIT; Copyright (c) 2024 0xa3k5.
|
|
3
|
+
* See THIRD_PARTY_NOTICES for the full license. Do not edit.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ICON_PATHS = {
|
|
7
|
+
usdc: <><path fill="#0B53BF" d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18"/><path fill="#fff" d="M13.62 5.45v1.159a5.64 5.64 0 0 1 4.005 5.394 5.64 5.64 0 0 1-4.005 5.394v1.16a6.74 6.74 0 0 0 5.13-6.554 6.74 6.74 0 0 0-5.13-6.553m-7.245 6.553a5.64 5.64 0 0 1 4.005-5.394V5.45a6.74 6.74 0 0 0-5.13 6.553 6.74 6.74 0 0 0 5.13 6.553v-1.159a5.63 5.63 0 0 1-4.005-5.394"/><path fill="#fff" d="M14.419 13.258c0-2.301-3.606-1.356-3.606-2.627 0-.456.366-.748 1.063-.748.833 0 1.12.405 1.21.95h1.147c-.102-1.024-.69-1.67-1.67-1.863v-.904h-1.125v.872c-1.075.137-1.75.762-1.75 1.693 0 2.312 3.611 1.445 3.611 2.694 0 .472-.455.787-1.226.787-1.007 0-1.339-.444-1.462-1.057H9.49c.073 1.122.764 1.823 1.947 1.999v.886h1.125v-.875c1.153-.149 1.856-.82 1.856-1.807"/></>,
|
|
8
|
+
usdt: <><path fill="#009393" d="m12 19.2-9-8.88L6.433 4.8h11.134L21 10.32zm.9-8.1v-1.098c1.62.08 3.132.396 3.6.805-.544.477-2.493.824-4.5.824s-3.956-.347-4.5-.824c.463-.41 1.98-.72 3.6-.81V11.1zm-5.4-.297v.661c.463.41 1.975.72 3.6.81V14.7h1.8v-2.43c1.62-.081 3.136-.396 3.6-.806v-1.318c-.464-.41-1.98-.725-3.6-.81V8.4h2.7V7.05H8.4V8.4h2.7v.936c-1.625.085-3.137.4-3.6.81z"/></>,
|
|
9
|
+
pha: <><circle cx="12" cy="12" r="12" fill="#252525"/><path fill="#D1FF52" fillRule="evenodd" d="M3 3h14.143v3.857H21V12h-3.857V6.857H6.857V12H3zm3.857 12.857h10.286V12H6.857zm0 0V21H3v-5.143z" clipRule="evenodd"/></>,
|
|
10
|
+
eth: <><path fill="#8FFCF3" d="M12 3v6.65l5.625 2.516z"/><path fill="#CABCF8" d="m12 3-5.625 9.166L12 9.651z"/><path fill="#CBA7F5" d="M12 16.477v4.522l5.625-7.784z"/><path fill="#74A0F3" d="M12 21v-4.523l-5.625-3.262z"/><path fill="#CBA7F5" d="m12 15.43 5.625-3.263L12 9.65z"/><path fill="#74A0F3" d="M6.375 12.167 12 15.429V9.651z"/><path fill="#202699" fillRule="evenodd" d="m12 15.429-5.625-3.263L12 3l5.625 9.166zM6.749 11.9l5.16-8.41v6.115zm-.077.23 5.238-2.327v5.364zm5.418-2.327v5.364l5.233-3.038zm0-.198 5.16 2.295-5.16-8.41z" clipRule="evenodd"/><path fill="#202699" fillRule="evenodd" d="M12 16.406 6.375 13.21 12 21l5.625-7.79zm-4.995-2.633 4.905 2.79v4.005zm5.085 2.79v4.005l4.905-6.795z" clipRule="evenodd"/></>,
|
|
11
|
+
ethereum: <><path fill="#8FFCF3" d="M12 3v6.651l5.625 2.516z"/><path fill="#CABCF8" d="m12 3-5.625 9.166L12 9.653z"/><path fill="#CBA7F5" d="M12 16.478V21l5.625-7.784z"/><path fill="#74A0F3" d="M12 21v-4.522l-5.625-3.262z"/><path fill="#CBA7F5" d="m12 15.43 5.625-3.263L12 9.652z"/><path fill="#74A0F3" d="M6.375 12.167 12 15.43V9.652z"/><path fill="#202699" fillRule="evenodd" d="m12 15.43-5.625-3.263L12 3l5.624 9.166zm-5.252-3.528 5.161-8.41v6.114zm-.077.229 5.238-2.327v5.364zm5.418-2.327v5.364l5.234-3.037zm0-.198 5.161 2.296-5.161-8.41z" clipRule="evenodd"/><path fill="#202699" fillRule="evenodd" d="m12 16.406-5.625-3.195L12 21l5.624-7.79zm-4.995-2.633 4.904 2.79v4.005zm5.084 2.79v4.005l4.905-6.795z" clipRule="evenodd"/></>,
|
|
12
|
+
base: <><path fill="#00F" d="M3 4.706c0-.585 0-.877.11-1.101.106-.215.28-.39.496-.495C3.83 3 4.122 3 4.706 3h14.588c.585 0 .876 0 1.101.11.215.105.389.28.494.495.111.225.111.517.111 1.101v14.588c0 .585 0 .876-.11 1.101-.106.215-.28.389-.495.494-.225.111-.517.111-1.101.111H4.706c-.585 0-.876 0-1.101-.11a1.08 1.08 0 0 1-.494-.495C3 20.17 3 19.878 3 19.294z"/></>,
|
|
13
|
+
};
|
package/src/react/index.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { Checkout, type CheckoutProps } from "./Checkout.js";
|
|
|
4
4
|
export { DepositAddress, type DepositAddressProps } from "./DepositAddress.js";
|
|
5
5
|
export { useCheckout, type UseCheckoutOptions, type UseCheckoutResult } from "./useCheckout.js";
|
|
6
6
|
export type { Appearance } from "./appearance.js";
|
|
7
|
+
export { NetworkIcon, AssetIcon, type IconProps, type NetworkIconProps, type AssetIconProps } from "./Icons.js";
|
package/src/request.ts
CHANGED
|
@@ -6,3 +6,10 @@ export function requestSignal(options: {
|
|
|
6
6
|
const deadline = AbortSignal.timeout(options.requestTimeout ?? 10_000);
|
|
7
7
|
return options.signal === undefined ? deadline : AbortSignal.any([options.signal, deadline]);
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/** `apiBase` without trailing slashes, in linear time (a `/\/+$/` regex is quadratic). */
|
|
11
|
+
export function trimTrailingSlashes(apiBase: string): string {
|
|
12
|
+
let end = apiBase.length;
|
|
13
|
+
while (end > 0 && apiBase[end - 1] === "/") end -= 1;
|
|
14
|
+
return apiBase.slice(0, end);
|
|
15
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
--pp-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
11
11
|
--pp-border-radius: 8px;
|
|
12
12
|
box-sizing: border-box;
|
|
13
|
-
max-width: 440px;
|
|
14
|
-
padding: 20px;
|
|
15
|
-
border: 1px solid var(--pp-color-border);
|
|
13
|
+
max-width: var(--pp-root-max-width, 440px);
|
|
14
|
+
padding: var(--pp-root-padding, 20px);
|
|
15
|
+
border: var(--pp-root-border, 1px solid var(--pp-color-border));
|
|
16
16
|
border-radius: var(--pp-border-radius);
|
|
17
|
-
background: var(--pp-color-background);
|
|
17
|
+
background: var(--pp-root-background, var(--pp-color-background));
|
|
18
18
|
color: var(--pp-color-text);
|
|
19
19
|
font-family: var(--pp-font-family);
|
|
20
20
|
font-size: 14px;
|
|
@@ -31,46 +31,50 @@
|
|
|
31
31
|
}
|
|
32
32
|
.pp-root *, .pp-root *::before, .pp-root *::after { box-sizing: inherit; }
|
|
33
33
|
.pp-amount { margin: 0; overflow-wrap: anywhere; font-size: 22px; font-weight: 600; }
|
|
34
|
-
.pp-subtitle { margin: 2px 0
|
|
34
|
+
.pp-subtitle { margin: 2px 0 12px; color: var(--pp-color-text-secondary); }
|
|
35
35
|
.pp-badge { font-weight: 600; }
|
|
36
36
|
.pp-payments { list-style: none; margin: 12px 0 0; padding: 0; }
|
|
37
37
|
.pp-payments li { margin: 4px 0; }
|
|
38
38
|
.pp-status { display: flex; gap: 8px; align-items: baseline; justify-content: space-between;
|
|
39
|
-
margin-bottom:
|
|
39
|
+
margin-bottom: 10px; padding: 8px 10px; border-radius: var(--pp-border-radius);
|
|
40
40
|
border: 1px solid var(--pp-color-border); }
|
|
41
41
|
.pp-status[data-tone="success"] { border-color: var(--pp-color-success); color: var(--pp-color-success); }
|
|
42
42
|
.pp-status[data-tone="danger"] { border-color: var(--pp-color-danger); color: var(--pp-color-danger); }
|
|
43
43
|
.pp-countdown { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
44
|
-
.pp-notice { margin: 0 0
|
|
44
|
+
.pp-notice { margin: 0 0 12px; color: var(--pp-color-text-secondary); font-size: 13px; }
|
|
45
45
|
.pp-notice strong { color: var(--pp-color-text); }
|
|
46
|
-
.pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom:
|
|
46
|
+
.pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom: 12px; }
|
|
47
47
|
.pp-tab { appearance: none; border: 0; border-bottom: 2px solid transparent; background: none;
|
|
48
|
-
color: var(--pp-color-text-secondary); font: inherit; padding: 8px 10px; cursor: pointer; }
|
|
48
|
+
color: var(--pp-color-text-secondary); font: inherit; min-height: 44px; white-space: nowrap; padding: 8px 10px; cursor: pointer; }
|
|
49
49
|
.pp-tab[aria-selected="true"] { color: var(--pp-color-text); border-bottom-color: var(--pp-color-primary); font-weight: 600; }
|
|
50
50
|
.pp-root button:focus-visible, .pp-root a:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
|
|
51
51
|
.pp-wallets { display: grid; gap: 8px; }
|
|
52
52
|
.pp-button { display: flex; gap: 10px; align-items: center; justify-content: center; width: 100%;
|
|
53
53
|
appearance: none; border: 1px solid var(--pp-color-primary); border-radius: var(--pp-border-radius);
|
|
54
|
-
background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; padding: 10px 14px; cursor: pointer; }
|
|
54
|
+
background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; min-height: 44px; padding: 10px 14px; cursor: pointer; }
|
|
55
55
|
.pp-button:disabled { opacity: 0.6; cursor: progress; }
|
|
56
56
|
.pp-button img { width: 20px; height: 20px; }
|
|
57
57
|
.pp-wallet-name { font-weight: 400; opacity: 0.85; }
|
|
58
|
-
.pp-message { margin:
|
|
58
|
+
.pp-message { margin: 8px 0 0; color: var(--pp-color-text-secondary); }
|
|
59
|
+
.pp-message:empty { display: none; }
|
|
59
60
|
.pp-message[data-tone="danger"] { color: var(--pp-color-danger); }
|
|
60
|
-
.pp-qr-panel { display: grid; justify-items: center; gap:
|
|
61
|
+
.pp-qr-panel { display: grid; justify-items: center; gap: 8px; text-align: center; }
|
|
61
62
|
.pp-qr { border-radius: 4px; }
|
|
62
|
-
.pp-fields { display: grid; gap:
|
|
63
|
+
.pp-fields { display: grid; gap: 8px; margin: 0; }
|
|
63
64
|
.pp-field dt { color: var(--pp-color-text-secondary); font-size: 12px; }
|
|
64
65
|
.pp-field dd { display: flex; gap: 8px; align-items: center; margin: 2px 0 0; }
|
|
65
66
|
.pp-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
|
|
66
|
-
.pp-copy { flex: none; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;
|
|
67
|
-
background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; padding: 3px 8px; cursor: pointer; }
|
|
67
|
+
.pp-copy { flex: none; margin-inline-start: auto; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;
|
|
68
|
+
background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; min-height: 44px; min-width: 44px; padding: 3px 8px; cursor: pointer; }
|
|
68
69
|
.pp-tx { margin: 0 0 12px; font-size: 13px; }
|
|
69
70
|
.pp-tx a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
70
71
|
.pp-tx .pp-value { overflow-wrap: anywhere; }
|
|
71
72
|
|
|
72
|
-
.pp-choices { display: flex; gap:
|
|
73
|
+
.pp-choices { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin: 0 0 12px; }
|
|
73
74
|
.pp-choices legend { padding: 0; margin-bottom: 6px; font-weight: 600; }
|
|
74
|
-
.pp-choices label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
|
|
75
|
-
.pp-choices input { accent-color: var(--pp-color-primary); }
|
|
75
|
+
.pp-choices label { display: flex; align-items: center; gap: 4px; min-height: 44px; cursor: pointer; }
|
|
76
|
+
.pp-choices input { margin: 0; accent-color: var(--pp-color-primary); }
|
|
76
77
|
.pp-choices input:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
|
|
78
|
+
|
|
79
|
+
.pp-icon-label { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; white-space: nowrap; }
|
|
80
|
+
.pp-icon { display: inline-block; flex: none; vertical-align: middle; }
|