@vamos-ui/react 0.2.0 → 0.3.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/dist/blocks/site/_kit-1.d.ts +1 -0
- package/dist/blocks/site/_kit-1.js +2 -0
- package/dist/blocks/site/cta.js +2 -1
- package/dist/blocks/site/faq.js +2 -1
- package/dist/blocks/site/feature-grid.js +2 -1
- package/dist/blocks/site/gallery-section.js +3 -2
- package/dist/blocks/site/hero.js +12 -6
- package/dist/blocks/site/logo-strip.d.ts +5 -3
- package/dist/blocks/site/logo-strip.js +8 -7
- package/dist/blocks/site/media-text.js +2 -1
- package/dist/blocks/site/pricing-table.js +2 -1
- package/dist/blocks/site/process.js +2 -1
- package/dist/blocks/site/site-shell.d.ts +11 -0
- package/dist/blocks/site/site-shell.js +43 -16
- package/dist/blocks/site/stats-band.d.ts +3 -1
- package/dist/blocks/site/stats-band.js +11 -7
- package/dist/blocks/site/testimonials.js +2 -1
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.js +2 -0
- package/dist/business/signer-status.d.ts +53 -0
- package/dist/business/signer-status.js +76 -0
- package/dist/charts/bar-chart.js +5 -1
- package/dist/charts/combo-chart.js +1 -1
- package/dist/components/_qr-core.d.ts +26 -0
- package/dist/components/_qr-core.js +376 -0
- package/dist/components/backup-codes.d.ts +42 -0
- package/dist/components/backup-codes.js +67 -0
- package/dist/components/breadcrumb.d.ts +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/command.d.ts +8 -2
- package/dist/components/command.js +7 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/pin-map.d.ts +58 -0
- package/dist/components/pin-map.js +167 -0
- package/dist/components/qr-code.d.ts +36 -0
- package/dist/components/qr-code.js +27 -0
- package/dist/components/record-detail-layout.d.ts +2 -2
- package/dist/components/record-detail-layout.js +1 -1
- package/dist/components/sidebar.js +8 -8
- package/dist/components/sla-timer.d.ts +75 -0
- package/dist/components/sla-timer.js +142 -0
- package/dist/components/slug-input.js +1 -1
- package/dist/components/tour.d.ts +60 -0
- package/dist/components/tour.js +139 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/public.d.ts +1 -0
- package/dist/lib/public.js +1 -0
- package/dist/locale-data/core/holidays.d.ts +1 -1
- package/dist/locale-data/core/holidays.js +1 -1
- package/dist/media/gallery.js +3 -1
- package/dist/motion/core.d.ts +32 -0
- package/dist/motion/core.js +77 -0
- package/dist/motion/count-up.d.ts +29 -0
- package/dist/motion/count-up.js +92 -0
- package/dist/motion/index.d.ts +11 -0
- package/dist/motion/index.js +13 -0
- package/dist/motion/motion-scope.d.ts +17 -0
- package/dist/motion/motion-scope.js +16 -0
- package/dist/motion/page-transition.d.ts +33 -0
- package/dist/motion/page-transition.js +61 -0
- package/dist/motion/parallax.d.ts +37 -0
- package/dist/motion/parallax.js +90 -0
- package/dist/motion/pointer.d.ts +38 -0
- package/dist/motion/pointer.js +117 -0
- package/dist/motion/reveal.d.ts +72 -0
- package/dist/motion/reveal.js +133 -0
- package/dist/motion/scroll-shrink.d.ts +18 -0
- package/dist/motion/scroll-shrink.js +47 -0
- package/dist/motion/skeleton-swap.d.ts +17 -0
- package/dist/motion/skeleton-swap.js +43 -0
- package/dist/motion/text-reveal.d.ts +30 -0
- package/dist/motion/text-reveal.js +29 -0
- package/dist/motion/ticker.d.ts +35 -0
- package/dist/motion/ticker.js +16 -0
- package/dist/sortable/sortable-tree.d.ts +4 -0
- package/dist/sortable/sortable-tree.js +16 -4
- package/dist/styles/blocks.css +3 -3
- package/dist/styles/motion.css +389 -0
- package/dist/styles.css +2 -1
- package/package.json +5 -1
package/dist/business/index.js
CHANGED
|
@@ -18,3 +18,5 @@ export { SetupChecklist, SetupChecklistLauncher, setupChecklistLabels, } from ".
|
|
|
18
18
|
// Permission matrix
|
|
19
19
|
export { PermissionMatrix, PermissionMatrixCell, PermissionDiff, PermissionCompare, permissionMatrixLabels, } from "./permission-matrix.js";
|
|
20
20
|
export { diffPermissions, applyDependencies, effectiveLevel, scopeRank, isGranted, setMany, setPermission, SCOPE_ORDER, } from "./permission-core.js";
|
|
21
|
+
// Signer status (e-signature progress)
|
|
22
|
+
export { SignerStatus, signerStatusLabels, } from "./signer-status.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type SignerState = "waiting" | "sent" | "viewed" | "signed" | "declined";
|
|
3
|
+
export interface Signer {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
/** "Client", "Account manager", "Witness". */
|
|
8
|
+
role?: string;
|
|
9
|
+
status: SignerState;
|
|
10
|
+
/** When the current status happened (ISO date-time). */
|
|
11
|
+
at?: string;
|
|
12
|
+
/** Decline reason or a note. */
|
|
13
|
+
note?: React.ReactNode;
|
|
14
|
+
/** Avatar image. */
|
|
15
|
+
avatarSrc?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const signerStatusLabels: {
|
|
18
|
+
title: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
declinedSummary: string;
|
|
21
|
+
complete: string;
|
|
22
|
+
waiting: string;
|
|
23
|
+
sent: string;
|
|
24
|
+
viewed: string;
|
|
25
|
+
signed: string;
|
|
26
|
+
declined: string;
|
|
27
|
+
remind: string;
|
|
28
|
+
remindName: string;
|
|
29
|
+
reminded: string;
|
|
30
|
+
order: string;
|
|
31
|
+
};
|
|
32
|
+
export type SignerStatusLabels = typeof signerStatusLabels;
|
|
33
|
+
export interface SignerStatusProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
34
|
+
signers: Signer[];
|
|
35
|
+
/** Signers sign in list order — numbers the rows and draws the chain. */
|
|
36
|
+
sequential?: boolean;
|
|
37
|
+
/** Heading. Default "Signatures"; `null` hides the header row. */
|
|
38
|
+
title?: React.ReactNode;
|
|
39
|
+
/** Shows "Remind" on sent / viewed rows. Resolve to show "Reminder sent". */
|
|
40
|
+
onRemind?: (signer: Signer) => void | Promise<void>;
|
|
41
|
+
/** Fixed reference time for relative dates (epoch ms) — tests, screenshots. */
|
|
42
|
+
now?: number;
|
|
43
|
+
labels?: Partial<SignerStatusLabels>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* E-signature progress for a quote, contract or offer letter: who signs, in which order,
|
|
47
|
+
* where each person is (sent, viewed, signed, declined) and when — with a reminder action.
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <SignerStatus sequential signers={contract.signers} onRemind={(s) => api.remind(s.id)} />
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare const SignerStatus: React.ForwardRefExoticComponent<SignerStatusProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { BellRing, Check, Clock3, Eye, Send, X } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { useLabels } from "../lib/labels.js";
|
|
7
|
+
import { useFormat } from "../lib/locale.js";
|
|
8
|
+
import { Avatar } from "../components/avatar.js";
|
|
9
|
+
import { Button } from "../components/button.js";
|
|
10
|
+
export const signerStatusLabels = {
|
|
11
|
+
title: "Signatures",
|
|
12
|
+
summary: "{signed} of {total} signed",
|
|
13
|
+
declinedSummary: "Declined by {name}",
|
|
14
|
+
complete: "Fully signed",
|
|
15
|
+
waiting: "Waiting",
|
|
16
|
+
sent: "Sent",
|
|
17
|
+
viewed: "Viewed",
|
|
18
|
+
signed: "Signed",
|
|
19
|
+
declined: "Declined",
|
|
20
|
+
remind: "Remind",
|
|
21
|
+
remindName: "Send a reminder to {name}",
|
|
22
|
+
reminded: "Reminder sent",
|
|
23
|
+
order: "Signs {position}",
|
|
24
|
+
};
|
|
25
|
+
const stateIcon = {
|
|
26
|
+
waiting: Clock3,
|
|
27
|
+
sent: Send,
|
|
28
|
+
viewed: Eye,
|
|
29
|
+
signed: Check,
|
|
30
|
+
declined: X,
|
|
31
|
+
};
|
|
32
|
+
const stateTone = {
|
|
33
|
+
waiting: "text-fg-subtle",
|
|
34
|
+
sent: "tone-info tone-soft",
|
|
35
|
+
viewed: "tone-warning tone-soft",
|
|
36
|
+
signed: "tone-success tone-soft",
|
|
37
|
+
declined: "tone-danger tone-soft",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* E-signature progress for a quote, contract or offer letter: who signs, in which order,
|
|
41
|
+
* where each person is (sent, viewed, signed, declined) and when — with a reminder action.
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <SignerStatus sequential signers={contract.signers} onRemind={(s) => api.remind(s.id)} />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const SignerStatus = React.forwardRef(function SignerStatus({ signers, sequential, title, onRemind, now, labels, className, ...props }, ref) {
|
|
48
|
+
const t = useLabels("signerStatus", signerStatusLabels, labels);
|
|
49
|
+
const f = useFormat();
|
|
50
|
+
const titleId = React.useId();
|
|
51
|
+
const [reminded, setReminded] = React.useState({});
|
|
52
|
+
const signed = signers.filter((s) => s.status === "signed").length;
|
|
53
|
+
const declined = signers.find((s) => s.status === "declined");
|
|
54
|
+
const total = signers.length;
|
|
55
|
+
const heading = title === undefined ? t("title") : title;
|
|
56
|
+
const summary = declined ? t("declinedSummary", { name: declined.name }) : signed === total && total > 0 ? t("complete") : t("summary", { signed: f.number(signed), total: f.number(total) });
|
|
57
|
+
const remind = async (s) => {
|
|
58
|
+
if (!onRemind)
|
|
59
|
+
return;
|
|
60
|
+
setReminded((r) => ({ ...r, [s.id]: "busy" }));
|
|
61
|
+
try {
|
|
62
|
+
await onRemind(s);
|
|
63
|
+
setReminded((r) => ({ ...r, [s.id]: "done" }));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
setReminded(({ [s.id]: _drop, ...rest }) => rest);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return (_jsxs("section", { ref: ref, "data-slot": "signer-status", "aria-labelledby": heading ? titleId : undefined, className: cn("grid gap-3", className), ...props, children: [heading ? (_jsxs("header", { className: "grid gap-2", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("h3", { id: titleId, className: "text-sm font-medium text-fg", children: heading }), _jsx("span", { className: cn("text-xs tabular", declined ? "text-danger" : signed === total ? "text-success" : "text-fg-muted"), children: summary })] }), _jsx("div", { "aria-hidden": "true", className: "flex gap-1", children: signers.map((s) => (_jsx("span", { className: cn("h-1 flex-1 rounded-full", s.status === "signed" ? "bg-success" : s.status === "declined" ? "bg-danger" : s.status === "viewed" ? "bg-warning" : s.status === "sent" ? "bg-info" : "bg-track") }, s.id))) })] })) : null, _jsx("ol", { className: "grid", children: signers.map((s, i) => {
|
|
70
|
+
const Icon = stateIcon[s.status];
|
|
71
|
+
const canRemind = onRemind && (s.status === "sent" || s.status === "viewed");
|
|
72
|
+
const r = reminded[s.id];
|
|
73
|
+
const when = s.at ? f.relative(s.at, now != null ? { now } : undefined) : null;
|
|
74
|
+
return (_jsxs("li", { "data-state": s.status, className: "relative flex items-start gap-3 py-2.5 not-last:border-b not-last:border-line", children: [_jsxs("span", { className: "relative shrink-0", children: [_jsx(Avatar, { name: s.name, src: s.avatarSrc, size: "sm" }), sequential ? (_jsx("span", { "aria-hidden": "true", className: "absolute -end-1 -bottom-1 grid size-4 place-items-center rounded-full bg-surface text-[10px] font-semibold text-fg-muted ring-1 ring-line tabular", children: i + 1 })) : null] }), _jsxs("div", { className: "grid min-w-0 flex-1 gap-0.5", children: [_jsxs("p", { className: "flex min-w-0 items-baseline gap-2 text-[13px]", children: [_jsx("span", { className: "truncate font-medium text-fg", children: s.name }), s.role ? _jsx("span", { className: "truncate text-xs text-fg-subtle", children: s.role }) : null, sequential ? _jsx("span", { className: "sr-only", children: t("order", { position: i + 1 }) }) : null] }), s.email ? _jsx("p", { className: "truncate text-xs text-fg-muted", children: s.email }) : null, s.note ? _jsx("div", { className: "mt-1 text-xs text-fg-muted", children: s.note }) : null] }), _jsxs("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [_jsxs("span", { className: cn("inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[11px] font-medium [&_svg]:size-3", stateTone[s.status]), children: [_jsx(Icon, { "aria-hidden": "true" }), t(s.status)] }), when ? (_jsx("time", { dateTime: s.at, title: f.dateTime(s.at), className: "text-[11px] text-fg-subtle tabular", suppressHydrationWarning: true, children: when })) : null, canRemind ? (r === "done" ? (_jsxs("span", { role: "status", className: "inline-flex items-center gap-1 text-[11px] text-fg-muted", children: [_jsx(Check, { "aria-hidden": "true", className: "size-3" }), t("reminded")] })) : (_jsxs(Button, { size: "xs", variant: "ghost", className: "-me-1.5", loading: r === "busy", "aria-label": t("remindName", { name: s.name }), onClick: () => remind(s), children: [_jsx(BellRing, { "aria-hidden": "true" }), t("remind")] }))) : null] })] }, s.id));
|
|
75
|
+
}) })] }));
|
|
76
|
+
});
|
package/dist/charts/bar-chart.js
CHANGED
|
@@ -26,7 +26,11 @@ function BarPlot({ series: keys, layout, stack, radius = 4, highlight, valueLabe
|
|
|
26
26
|
const [min] = extent(chart.data, visible.map((s) => s.key), stacked);
|
|
27
27
|
const valueWidth = useValueAxisWidth(visible.map((s) => s.key), { stacked, percent });
|
|
28
28
|
const categoryWidth = React.useMemo(() => (horizontal && chart.x ? Math.min(160, estimateAxisWidth(chart.data.map((r) => chart.formatX(r[chart.x])), 12)) : 0), [horizontal, chart]);
|
|
29
|
-
|
|
29
|
+
// A time series (dates on x) legitimately has many bars; the hint is for long category lists.
|
|
30
|
+
const timeAxis = (typeof chart.xFormat === "object" && chart.xFormat !== null && ("date" in chart.xFormat || "relative" in chart.xFormat)) ||
|
|
31
|
+
(chart.x != null && typeof chart.data[0]?.[chart.x] === "string" && /^\d{4}-\d{2}-\d{2}/.test(chart.data[0][chart.x])) ||
|
|
32
|
+
(chart.x != null && (chart.data[0]?.[chart.x] instanceof Date || typeof chart.data[0]?.[chart.x] === "number" && chart.data[0][chart.x] > 1e11));
|
|
33
|
+
if (process.env.NODE_ENV !== "production" && chart.data.length > 20 && !horizontal && !timeAxis) {
|
|
30
34
|
console.warn("[vamos-ui] BarChart: more than 20 categories — use layout=\"horizontal\" or group the tail into \"Other\".");
|
|
31
35
|
}
|
|
32
36
|
const lastKey = visible[visible.length - 1]?.key;
|
|
@@ -40,7 +40,7 @@ function ComboPlot({ bars, lines, areas = [], endAxisFormat, endAxis, stackBars,
|
|
|
40
40
|
const left = rtl ? "end" : "start";
|
|
41
41
|
const yId = (key, fallback) => axisOf(key, fallback);
|
|
42
42
|
const titleProps = (value, side) => value ? { value, angle: -90, position: side === "left" ? "insideLeft" : "insideRight", className: "vui-chart-axis-title", offset: 0, style: { textAnchor: "middle" } } : undefined;
|
|
43
|
-
return (_jsx(ChartFrame, { children: _jsxs(ComposedChart, { data: chart.data, margin: { top: 8, right: 4, bottom: 0, left: 4 }, barCategoryGap: "30%", children: [_jsx(ChartGrid, { yAxisId: "start" }), _jsx(ChartXAxis, {}), _jsx(ChartYAxis, { yAxisId: "start", orientation: left === "start" ? "left" : "right", domain: startDomain, ticks: ticksOf(startDomain), width: startWidth + (startAxisLabel ? 16 : 0), tickFormatter: (v) => chart.formatTick(v, startFormat), label: titleProps(startAxisLabel, left === "start" ? "left" : "right"), keys: startKeys }), _jsx(ChartYAxis, { yAxisId: "end", orientation: left === "start" ? "right" : "left", domain: endDomain, ticks: ticksOf(endDomain), hide: !endAxis || endKeys.length === 0, width: endWidth + (endAxisLabel ? 16 : 0), tickFormatter: (v) => chart.formatTick(v, endFormat), label: titleProps(endAxisLabel, left === "start" ? "right" : "left"), keys: endKeys }), _jsx(ChartTooltip, { cursor: { fill: "var(--hover)" } }), areas.map((key) => (_jsx(Area, { yAxisId: yId(key, "start"), dataKey: key, name: chart.config[key]?.label, hide: chart.isHidden(key), type: "monotone", stroke: seriesColor(key), strokeWidth: 1.5, fill: `color-mix(in oklch, ${seriesColor(key)} 16%, transparent)`, isAnimationActive: chart.animate }, key))), bars.map((key, i) => (_jsx(Bar, { yAxisId: yId(key, "start"), dataKey: key, name: chart.config[key]?.label, hide: chart.isHidden(key), stackId: stackBars ? "bars" : undefined, fill: seriesColor(key), radius: stackBars && i < bars.length - 1 ? 0 : [4, 4, 0, 0], maxBarSize: 40, isAnimationActive: chart.animate }, key))), lines.map((key) => {
|
|
43
|
+
return (_jsx(ChartFrame, { children: _jsxs(ComposedChart, { data: chart.data, stackOffset: startStacked && startDomain[0] < 0 ? "sign" : undefined, margin: { top: 8, right: 4, bottom: 0, left: 4 }, barCategoryGap: "30%", children: [_jsx(ChartGrid, { yAxisId: "start" }), _jsx(ChartXAxis, {}), _jsx(ChartYAxis, { yAxisId: "start", orientation: left === "start" ? "left" : "right", domain: startDomain, ticks: ticksOf(startDomain), width: startWidth + (startAxisLabel ? 16 : 0), tickFormatter: (v) => chart.formatTick(v, startFormat), label: titleProps(startAxisLabel, left === "start" ? "left" : "right"), keys: startKeys }), _jsx(ChartYAxis, { yAxisId: "end", orientation: left === "start" ? "right" : "left", domain: endDomain, ticks: ticksOf(endDomain), hide: !endAxis || endKeys.length === 0, width: endWidth + (endAxisLabel ? 16 : 0), tickFormatter: (v) => chart.formatTick(v, endFormat), label: titleProps(endAxisLabel, left === "start" ? "right" : "left"), keys: endKeys }), _jsx(ChartTooltip, { cursor: { fill: "var(--hover)" } }), areas.map((key) => (_jsx(Area, { yAxisId: yId(key, "start"), dataKey: key, name: chart.config[key]?.label, hide: chart.isHidden(key), type: "monotone", stroke: seriesColor(key), strokeWidth: 1.5, fill: `color-mix(in oklch, ${seriesColor(key)} 16%, transparent)`, isAnimationActive: chart.animate }, key))), bars.map((key, i) => (_jsx(Bar, { yAxisId: yId(key, "start"), dataKey: key, name: chart.config[key]?.label, hide: chart.isHidden(key), stackId: stackBars ? "bars" : undefined, fill: seriesColor(key), radius: stackBars && i < bars.length - 1 ? 0 : [4, 4, 0, 0], maxBarSize: 40, isAnimationActive: chart.animate }, key))), lines.map((key) => {
|
|
44
44
|
const s = chart.series.find((x) => x.key === key);
|
|
45
45
|
return (_jsx(Line, { yAxisId: yId(key, "end"), dataKey: key, name: s?.label, hide: chart.isHidden(key), type: "monotone", stroke: seriesColor(key), strokeWidth: 2, strokeDasharray: patternDash(s?.pattern), dot: { r: 2.5, fill: seriesColor(key), stroke: "var(--chart-tooltip)", strokeWidth: 1.5 }, activeDot: { r: 4, strokeWidth: 2, stroke: "var(--chart-tooltip)", fill: seriesColor(key) }, isAnimationActive: chart.animate }, key));
|
|
46
46
|
})] }) }));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type QrErrorCorrection = "L" | "M" | "Q" | "H";
|
|
2
|
+
export interface QrMatrix {
|
|
3
|
+
/** Modules per side (version × 4 + 17). */
|
|
4
|
+
size: number;
|
|
5
|
+
version: number;
|
|
6
|
+
mask: number;
|
|
7
|
+
errorCorrection: QrErrorCorrection;
|
|
8
|
+
/** `modules[y][x]` — true = dark. */
|
|
9
|
+
modules: boolean[][];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Encode text (UTF-8, byte mode) as a QR matrix.
|
|
13
|
+
* Throws when the text does not fit version 40 at the chosen level.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const qr = encodeQr("otpauth://totp/Northpoint:ada@example.com?secret=…", { errorCorrection: "M" });
|
|
17
|
+
* qr.size; // 41 (version 6)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function encodeQr(text: string, options?: {
|
|
21
|
+
errorCorrection?: QrErrorCorrection;
|
|
22
|
+
minVersion?: number;
|
|
23
|
+
mask?: number;
|
|
24
|
+
}): QrMatrix;
|
|
25
|
+
/** One SVG path (`d`) drawing every dark module, offset by `margin` modules. */
|
|
26
|
+
export declare function qrPath(qr: Pick<QrMatrix, "size" | "modules">, margin?: number): string;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// QR Code Model 2 encoder (ISO/IEC 18004), byte mode, versions 1–40, all four error
|
|
2
|
+
// correction levels, automatic mask choice. Pure and dependency-free so it runs in RSC.
|
|
3
|
+
// Follows the structure of the public-domain-style reference algorithm (Nayuki).
|
|
4
|
+
const ECL_INDEX = { L: 0, M: 1, Q: 2, H: 3 };
|
|
5
|
+
const ECL_FORMAT = { L: 1, M: 0, Q: 3, H: 2 };
|
|
6
|
+
// prettier-ignore
|
|
7
|
+
const ECC_CODEWORDS_PER_BLOCK = [
|
|
8
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
9
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
10
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
11
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
12
|
+
];
|
|
13
|
+
// prettier-ignore
|
|
14
|
+
const NUM_ERROR_CORRECTION_BLOCKS = [
|
|
15
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
16
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
17
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
18
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
|
|
19
|
+
];
|
|
20
|
+
const bit = (x, i) => ((x >>> i) & 1) !== 0;
|
|
21
|
+
function rawDataModules(ver) {
|
|
22
|
+
let result = (16 * ver + 128) * ver + 64;
|
|
23
|
+
if (ver >= 2) {
|
|
24
|
+
const numAlign = Math.floor(ver / 7) + 2;
|
|
25
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
26
|
+
if (ver >= 7)
|
|
27
|
+
result -= 36;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function dataCodewords(ver, ecl) {
|
|
32
|
+
const e = ECL_INDEX[ecl];
|
|
33
|
+
return Math.floor(rawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[e][ver] * NUM_ERROR_CORRECTION_BLOCKS[e][ver];
|
|
34
|
+
}
|
|
35
|
+
/* Reed–Solomon over GF(2^8), polynomial 0x11D. */
|
|
36
|
+
function gfMul(x, y) {
|
|
37
|
+
let z = 0;
|
|
38
|
+
for (let i = 7; i >= 0; i--) {
|
|
39
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
40
|
+
z ^= ((y >>> i) & 1) * x;
|
|
41
|
+
}
|
|
42
|
+
return z & 0xff;
|
|
43
|
+
}
|
|
44
|
+
function rsDivisor(degree) {
|
|
45
|
+
const result = new Array(degree - 1).fill(0);
|
|
46
|
+
result.push(1);
|
|
47
|
+
let root = 1;
|
|
48
|
+
for (let i = 0; i < degree; i++) {
|
|
49
|
+
for (let j = 0; j < result.length; j++) {
|
|
50
|
+
result[j] = gfMul(result[j], root);
|
|
51
|
+
if (j + 1 < result.length)
|
|
52
|
+
result[j] ^= result[j + 1];
|
|
53
|
+
}
|
|
54
|
+
root = gfMul(root, 0x02);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function rsRemainder(data, divisor) {
|
|
59
|
+
const result = divisor.map(() => 0);
|
|
60
|
+
for (const b of data) {
|
|
61
|
+
const factor = b ^ result.shift();
|
|
62
|
+
result.push(0);
|
|
63
|
+
divisor.forEach((coef, i) => (result[i] ^= gfMul(coef, factor)));
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
/** UTF-8 bytes without TextEncoder (works everywhere). */
|
|
68
|
+
function utf8(text) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const ch of text) {
|
|
71
|
+
const cp = ch.codePointAt(0);
|
|
72
|
+
if (cp < 0x80)
|
|
73
|
+
out.push(cp);
|
|
74
|
+
else if (cp < 0x800)
|
|
75
|
+
out.push(0xc0 | (cp >> 6), 0x80 | (cp & 63));
|
|
76
|
+
else if (cp < 0x10000)
|
|
77
|
+
out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
|
|
78
|
+
else
|
|
79
|
+
out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 63), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
class Grid {
|
|
84
|
+
version;
|
|
85
|
+
size;
|
|
86
|
+
modules;
|
|
87
|
+
fn;
|
|
88
|
+
constructor(version) {
|
|
89
|
+
this.version = version;
|
|
90
|
+
this.size = version * 4 + 17;
|
|
91
|
+
this.modules = Array.from({ length: this.size }, () => new Array(this.size).fill(false));
|
|
92
|
+
this.fn = Array.from({ length: this.size }, () => new Array(this.size).fill(false));
|
|
93
|
+
}
|
|
94
|
+
setFn(x, y, dark) {
|
|
95
|
+
this.modules[y][x] = dark;
|
|
96
|
+
this.fn[y][x] = true;
|
|
97
|
+
}
|
|
98
|
+
alignmentPositions() {
|
|
99
|
+
if (this.version === 1)
|
|
100
|
+
return [];
|
|
101
|
+
const numAlign = Math.floor(this.version / 7) + 2;
|
|
102
|
+
const step = Math.floor((this.version * 8 + numAlign * 3 + 5) / (numAlign * 4 - 4)) * 2;
|
|
103
|
+
const result = [6];
|
|
104
|
+
for (let pos = this.size - 7; result.length < numAlign; pos -= step)
|
|
105
|
+
result.splice(1, 0, pos);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
drawFunctionPatterns(ecl) {
|
|
109
|
+
const n = this.size;
|
|
110
|
+
for (let i = 0; i < n; i++) {
|
|
111
|
+
this.setFn(6, i, i % 2 === 0);
|
|
112
|
+
this.setFn(i, 6, i % 2 === 0);
|
|
113
|
+
}
|
|
114
|
+
this.finder(3, 3);
|
|
115
|
+
this.finder(n - 4, 3);
|
|
116
|
+
this.finder(3, n - 4);
|
|
117
|
+
const pos = this.alignmentPositions();
|
|
118
|
+
const k = pos.length;
|
|
119
|
+
for (let i = 0; i < k; i++)
|
|
120
|
+
for (let j = 0; j < k; j++) {
|
|
121
|
+
if ((i === 0 && j === 0) || (i === 0 && j === k - 1) || (i === k - 1 && j === 0))
|
|
122
|
+
continue;
|
|
123
|
+
for (let dy = -2; dy <= 2; dy++)
|
|
124
|
+
for (let dx = -2; dx <= 2; dx++)
|
|
125
|
+
this.setFn(pos[i] + dx, pos[j] + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
126
|
+
}
|
|
127
|
+
this.drawFormat(ecl, 0);
|
|
128
|
+
this.drawVersion();
|
|
129
|
+
}
|
|
130
|
+
finder(x, y) {
|
|
131
|
+
for (let dy = -4; dy <= 4; dy++)
|
|
132
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
133
|
+
const d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
134
|
+
const xx = x + dx;
|
|
135
|
+
const yy = y + dy;
|
|
136
|
+
if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
|
|
137
|
+
this.setFn(xx, yy, d !== 2 && d !== 4);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
drawFormat(ecl, mask) {
|
|
141
|
+
const data = (ECL_FORMAT[ecl] << 3) | mask;
|
|
142
|
+
let rem = data;
|
|
143
|
+
for (let i = 0; i < 10; i++)
|
|
144
|
+
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
145
|
+
const bits = ((data << 10) | rem) ^ 0x5412;
|
|
146
|
+
const n = this.size;
|
|
147
|
+
for (let i = 0; i <= 5; i++)
|
|
148
|
+
this.setFn(8, i, bit(bits, i));
|
|
149
|
+
this.setFn(8, 7, bit(bits, 6));
|
|
150
|
+
this.setFn(8, 8, bit(bits, 7));
|
|
151
|
+
this.setFn(7, 8, bit(bits, 8));
|
|
152
|
+
for (let i = 9; i < 15; i++)
|
|
153
|
+
this.setFn(14 - i, 8, bit(bits, i));
|
|
154
|
+
for (let i = 0; i < 8; i++)
|
|
155
|
+
this.setFn(n - 1 - i, 8, bit(bits, i));
|
|
156
|
+
for (let i = 8; i < 15; i++)
|
|
157
|
+
this.setFn(8, n - 15 + i, bit(bits, i));
|
|
158
|
+
this.setFn(8, n - 8, true);
|
|
159
|
+
}
|
|
160
|
+
drawVersion() {
|
|
161
|
+
if (this.version < 7)
|
|
162
|
+
return;
|
|
163
|
+
let rem = this.version;
|
|
164
|
+
for (let i = 0; i < 12; i++)
|
|
165
|
+
rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
|
|
166
|
+
const bits = (this.version << 12) | rem;
|
|
167
|
+
for (let i = 0; i < 18; i++) {
|
|
168
|
+
const b = bit(bits, i);
|
|
169
|
+
const a = this.size - 11 + (i % 3);
|
|
170
|
+
const c = Math.floor(i / 3);
|
|
171
|
+
this.setFn(a, c, b);
|
|
172
|
+
this.setFn(c, a, b);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
drawCodewords(data) {
|
|
176
|
+
let i = 0;
|
|
177
|
+
const n = this.size;
|
|
178
|
+
for (let right = n - 1; right >= 1; right -= 2) {
|
|
179
|
+
if (right === 6)
|
|
180
|
+
right = 5;
|
|
181
|
+
for (let vert = 0; vert < n; vert++)
|
|
182
|
+
for (let j = 0; j < 2; j++) {
|
|
183
|
+
const x = right - j;
|
|
184
|
+
const upward = ((right + 1) & 2) === 0;
|
|
185
|
+
const y = upward ? n - 1 - vert : vert;
|
|
186
|
+
if (!this.fn[y][x] && i < data.length * 8) {
|
|
187
|
+
this.modules[y][x] = bit(data[i >>> 3], 7 - (i & 7));
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
applyMask(mask) {
|
|
194
|
+
for (let y = 0; y < this.size; y++)
|
|
195
|
+
for (let x = 0; x < this.size; x++) {
|
|
196
|
+
let invert;
|
|
197
|
+
switch (mask) {
|
|
198
|
+
case 0:
|
|
199
|
+
invert = (x + y) % 2 === 0;
|
|
200
|
+
break;
|
|
201
|
+
case 1:
|
|
202
|
+
invert = y % 2 === 0;
|
|
203
|
+
break;
|
|
204
|
+
case 2:
|
|
205
|
+
invert = x % 3 === 0;
|
|
206
|
+
break;
|
|
207
|
+
case 3:
|
|
208
|
+
invert = (x + y) % 3 === 0;
|
|
209
|
+
break;
|
|
210
|
+
case 4:
|
|
211
|
+
invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
|
|
212
|
+
break;
|
|
213
|
+
case 5:
|
|
214
|
+
invert = ((x * y) % 2) + ((x * y) % 3) === 0;
|
|
215
|
+
break;
|
|
216
|
+
case 6:
|
|
217
|
+
invert = (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
|
|
218
|
+
break;
|
|
219
|
+
default: invert = (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
|
|
220
|
+
}
|
|
221
|
+
if (!this.fn[y][x] && invert)
|
|
222
|
+
this.modules[y][x] = !this.modules[y][x];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
penalty() {
|
|
226
|
+
const n = this.size;
|
|
227
|
+
const m = this.modules;
|
|
228
|
+
let score = 0;
|
|
229
|
+
const line = (get) => {
|
|
230
|
+
let run = 1;
|
|
231
|
+
let s = 0;
|
|
232
|
+
for (let i = 1; i <= n; i++) {
|
|
233
|
+
if (i < n && get(i) === get(i - 1))
|
|
234
|
+
run++;
|
|
235
|
+
else {
|
|
236
|
+
if (run >= 5)
|
|
237
|
+
s += 3 + (run - 5);
|
|
238
|
+
run = 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Finder-like 1:1:3:1:1 with four light modules on one side.
|
|
242
|
+
for (let i = 0; i + 10 < n + 4; i++) {
|
|
243
|
+
const at = (k) => (k < 0 || k >= n ? false : get(k));
|
|
244
|
+
const core = at(i) && !at(i + 1) && at(i + 2) && at(i + 3) && at(i + 4) && !at(i + 5) && at(i + 6);
|
|
245
|
+
if (!core)
|
|
246
|
+
continue;
|
|
247
|
+
const before = !at(i - 1) && !at(i - 2) && !at(i - 3) && !at(i - 4);
|
|
248
|
+
const after = !at(i + 7) && !at(i + 8) && !at(i + 9) && !at(i + 10);
|
|
249
|
+
if (before || after)
|
|
250
|
+
s += 40;
|
|
251
|
+
}
|
|
252
|
+
return s;
|
|
253
|
+
};
|
|
254
|
+
for (let y = 0; y < n; y++)
|
|
255
|
+
score += line((x) => m[y][x]);
|
|
256
|
+
for (let x = 0; x < n; x++)
|
|
257
|
+
score += line((y) => m[y][x]);
|
|
258
|
+
for (let y = 0; y < n - 1; y++)
|
|
259
|
+
for (let x = 0; x < n - 1; x++) {
|
|
260
|
+
const c = m[y][x];
|
|
261
|
+
if (c === m[y][x + 1] && c === m[y + 1][x] && c === m[y + 1][x + 1])
|
|
262
|
+
score += 3;
|
|
263
|
+
}
|
|
264
|
+
let dark = 0;
|
|
265
|
+
for (const row of m)
|
|
266
|
+
for (const c of row)
|
|
267
|
+
if (c)
|
|
268
|
+
dark++;
|
|
269
|
+
const total = n * n;
|
|
270
|
+
score += (Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1) * 10;
|
|
271
|
+
return score;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Encode text (UTF-8, byte mode) as a QR matrix.
|
|
276
|
+
* Throws when the text does not fit version 40 at the chosen level.
|
|
277
|
+
*
|
|
278
|
+
* ```ts
|
|
279
|
+
* const qr = encodeQr("otpauth://totp/Northpoint:ada@example.com?secret=…", { errorCorrection: "M" });
|
|
280
|
+
* qr.size; // 41 (version 6)
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
export function encodeQr(text, options = {}) {
|
|
284
|
+
const ecl = options.errorCorrection ?? "M";
|
|
285
|
+
const bytes = utf8(text);
|
|
286
|
+
let version = Math.max(1, Math.min(40, options.minVersion ?? 1));
|
|
287
|
+
let capacityBits = 0;
|
|
288
|
+
for (;; version++) {
|
|
289
|
+
if (version > 40)
|
|
290
|
+
throw new RangeError("QR: text is too long to encode");
|
|
291
|
+
const ccBits = version < 10 ? 8 : 16;
|
|
292
|
+
capacityBits = dataCodewords(version, ecl) * 8;
|
|
293
|
+
if (4 + ccBits + bytes.length * 8 <= capacityBits)
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
// Bit stream: mode (byte = 0100), length, data, terminator, padding.
|
|
297
|
+
const bits = [];
|
|
298
|
+
const push = (value, len) => {
|
|
299
|
+
for (let i = len - 1; i >= 0; i--)
|
|
300
|
+
bits.push((value >>> i) & 1);
|
|
301
|
+
};
|
|
302
|
+
push(0b0100, 4);
|
|
303
|
+
push(bytes.length, version < 10 ? 8 : 16);
|
|
304
|
+
for (const b of bytes)
|
|
305
|
+
push(b, 8);
|
|
306
|
+
push(0, Math.min(4, capacityBits - bits.length));
|
|
307
|
+
push(0, (8 - (bits.length % 8)) % 8);
|
|
308
|
+
for (let pad = 0xec; bits.length < capacityBits; pad ^= 0xec ^ 0x11)
|
|
309
|
+
push(pad, 8);
|
|
310
|
+
const data = [];
|
|
311
|
+
for (let i = 0; i < bits.length; i += 8)
|
|
312
|
+
data.push(bits.slice(i, i + 8).reduce((acc, b) => (acc << 1) | b, 0));
|
|
313
|
+
// Error correction + interleave.
|
|
314
|
+
const e = ECL_INDEX[ecl];
|
|
315
|
+
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[e][version];
|
|
316
|
+
const eccLen = ECC_CODEWORDS_PER_BLOCK[e][version];
|
|
317
|
+
const rawCodewords = Math.floor(rawDataModules(version) / 8);
|
|
318
|
+
const numShort = numBlocks - (rawCodewords % numBlocks);
|
|
319
|
+
const shortLen = Math.floor(rawCodewords / numBlocks);
|
|
320
|
+
const divisor = rsDivisor(eccLen);
|
|
321
|
+
const blocks = [];
|
|
322
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
323
|
+
const dat = data.slice(k, k + shortLen - eccLen + (i < numShort ? 0 : 1));
|
|
324
|
+
k += dat.length;
|
|
325
|
+
const ecc = rsRemainder(dat, divisor);
|
|
326
|
+
if (i < numShort)
|
|
327
|
+
dat.push(0);
|
|
328
|
+
blocks.push(dat.concat(ecc));
|
|
329
|
+
}
|
|
330
|
+
const codewords = [];
|
|
331
|
+
for (let i = 0; i < blocks[0].length; i++)
|
|
332
|
+
blocks.forEach((block, j) => {
|
|
333
|
+
if (i !== shortLen - eccLen || j >= numShort)
|
|
334
|
+
codewords.push(block[i]);
|
|
335
|
+
});
|
|
336
|
+
// Place, then pick the mask with the lowest penalty.
|
|
337
|
+
const grid = new Grid(version);
|
|
338
|
+
grid.drawFunctionPatterns(ecl);
|
|
339
|
+
grid.drawCodewords(codewords);
|
|
340
|
+
let mask = options.mask ?? -1;
|
|
341
|
+
if (mask < 0 || mask > 7) {
|
|
342
|
+
let best = Infinity;
|
|
343
|
+
for (let candidate = 0; candidate < 8; candidate++) {
|
|
344
|
+
grid.applyMask(candidate);
|
|
345
|
+
grid.drawFormat(ecl, candidate);
|
|
346
|
+
const p = grid.penalty();
|
|
347
|
+
if (p < best) {
|
|
348
|
+
best = p;
|
|
349
|
+
mask = candidate;
|
|
350
|
+
}
|
|
351
|
+
grid.applyMask(candidate); // undo (XOR)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
grid.applyMask(mask);
|
|
355
|
+
grid.drawFormat(ecl, mask);
|
|
356
|
+
return { size: grid.size, version, mask, errorCorrection: ecl, modules: grid.modules };
|
|
357
|
+
}
|
|
358
|
+
/** One SVG path (`d`) drawing every dark module, offset by `margin` modules. */
|
|
359
|
+
export function qrPath(qr, margin = 0) {
|
|
360
|
+
let d = "";
|
|
361
|
+
for (let y = 0; y < qr.size; y++) {
|
|
362
|
+
let x = 0;
|
|
363
|
+
while (x < qr.size) {
|
|
364
|
+
if (!qr.modules[y][x]) {
|
|
365
|
+
x++;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
let run = 1;
|
|
369
|
+
while (x + run < qr.size && qr.modules[y][x + run])
|
|
370
|
+
run++;
|
|
371
|
+
d += `M${x + margin} ${y + margin}h${run}v1h-${run}z`;
|
|
372
|
+
x += run;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return d;
|
|
376
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export declare const backupCodesLabels: {
|
|
3
|
+
title: string;
|
|
4
|
+
copy: string;
|
|
5
|
+
copied: string;
|
|
6
|
+
copiedAnnouncement: string;
|
|
7
|
+
download: string;
|
|
8
|
+
regenerate: string;
|
|
9
|
+
used: string;
|
|
10
|
+
remaining: string;
|
|
11
|
+
fileHeader: string;
|
|
12
|
+
};
|
|
13
|
+
export type BackupCodesLabels = typeof backupCodesLabels;
|
|
14
|
+
export interface BackupCodesProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title" | "onCopy"> {
|
|
15
|
+
/** One-time recovery codes, in display order. */
|
|
16
|
+
codes: string[];
|
|
17
|
+
/** Codes already spent — struck through and announced as used. */
|
|
18
|
+
used?: string[];
|
|
19
|
+
/** Heading above the grid. Default "Backup codes"; `null` hides it (label the region yourself). */
|
|
20
|
+
title?: React.ReactNode;
|
|
21
|
+
/** Name of the downloaded text file. Default `"backup-codes.txt"`. */
|
|
22
|
+
filename?: string;
|
|
23
|
+
/** Extra first line(s) of the downloaded file (account, service). */
|
|
24
|
+
fileNote?: string;
|
|
25
|
+
/** Called after the codes were copied. */
|
|
26
|
+
onCopy?: (text: string) => void;
|
|
27
|
+
/** Replace the default download (Blob + link) — e.g. to log it. */
|
|
28
|
+
onDownload?: (text: string) => void;
|
|
29
|
+
/** Shows a "New codes" button. */
|
|
30
|
+
onRegenerate?: () => void;
|
|
31
|
+
regenerating?: boolean;
|
|
32
|
+
labels?: Partial<BackupCodesLabels>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One-time recovery codes after two-factor enrolment: a numbered monospace grid with copy,
|
|
36
|
+
* download and regenerate. Used codes stay visible, struck through.
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <BackupCodes codes={codes} used={["4K7Q-PX2M"]} onRegenerate={regenerate} />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare const BackupCodes: React.ForwardRefExoticComponent<BackupCodesProps & React.RefAttributes<HTMLDivElement>>;
|