finality-ux-sampler 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ATTRIBUTION.md ADDED
@@ -0,0 +1,10 @@
1
+ # Attribution
2
+
3
+ When you reuse the sampler's interface copy or visual design, include a credit
4
+ in your app's credits, documentation, or another place suitable for your use:
5
+
6
+ > Interface copy and design adapted from [Finality UX Kit Free Sampler](https://finality-ux-kit.pages.dev/from-code/), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Changes: describe your changes here.
7
+
8
+ If you use the work without changes, replace the last sentence with “Used
9
+ without changes.” Preserve the MIT notice when redistributing the component
10
+ code. No visible product logo or tracking code is required.
package/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Finality UX Kit Free Sampler
2
+
3
+ Component implementation, JavaScript/TypeScript interfaces and integration
4
+ examples: MIT. See LICENSE-CODE.txt.
5
+
6
+ Visual design and interface copy, including the text in dist/states.json and
7
+ dist/states.mjs: Creative Commons Attribution 4.0 International (CC BY 4.0).
8
+ See LICENSE-DESIGN.txt and ATTRIBUTION.md.
9
+
10
+ The package combines these works; it does not offer a choice between licenses.
11
+ These free-sampler licenses do not license the separate paid Finality Core Kit.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Finality UX Kit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ Finality UX Kit Free Sampler design and interface copy
2
+ Copyright (c) 2026 Finality UX Kit
3
+ Creative Commons Attribution 4.0 International (CC BY 4.0)
4
+
5
+ You may share and adapt the free sampler's design and interface copy for any
6
+ purpose provided you give appropriate credit, link to the license, and indicate
7
+ changes. This notice is a summary; the full license controls.
8
+
9
+ License: https://creativecommons.org/licenses/by/4.0/
10
+ Full legal text: https://creativecommons.org/licenses/by/4.0/legalcode
11
+ Creator: Finality UX Kit
12
+ Source: https://finality-ux-kit.pages.dev/from-code/
13
+
14
+ This license covers the visual design and interface copy in this free package.
15
+ It does not grant rights to third-party names or trademarks or to the separately
16
+ sold Finality Core Kit. See ATTRIBUTION.md for an example credit.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Finality UX Kit — Free Sampler
2
+
3
+ One React/Tailwind card and eight editable wallet and transaction copy examples.
4
+ Use it to prototype connection, network, signature, approval, pending, finalized,
5
+ and reverted states with explicit recovery instructions.
6
+
7
+ The package renders the state you supply. It does not connect a wallet, send
8
+ transactions, verify payments, or advance a flow automatically. Base, amounts,
9
+ confirmation counts, transaction IDs and receipt times in the catalog are
10
+ illustrative: replace them with data verified by your application. Transition
11
+ entries describe examples; `external:` targets require your application's logic.
12
+ A signed message alone does not prove that a transaction was submitted.
13
+
14
+ For the free SVG import board, copy CSV and JSON, [download the full free
15
+ sampler](https://finality-ux-kit.pages.dev/starter/?source=code). The separate
16
+ [Finality Core Kit](https://finality-ux-kit.pages.dev/from-code/) adds an
17
+ eleven-state transfer flow, native Figma components and a typed React controller.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install finality-ux-sampler react
23
+ ```
24
+
25
+ React 18.2 or 19 and Tailwind CSS 3.4 or 4 are supported. Runtime exports are
26
+ compiled ESM; your application does not need to transpile JSX inside this
27
+ package. CommonJS `require()` is not the supported interface. TypeScript
28
+ declarations cover the root and `finality-ux-sampler/states` exports.
29
+
30
+ ## Include Tailwind styles
31
+
32
+ The package ships utility classes, not a stylesheet. Tailwind normally excludes
33
+ `node_modules`, so add the package explicitly to your existing Tailwind setup.
34
+
35
+ For Tailwind 4, in **`src/app.css`**:
36
+
37
+ ```css
38
+ @import "tailwindcss";
39
+ @source "../node_modules/finality-ux-sampler/src";
40
+ ```
41
+
42
+ The `@source` path is relative to that CSS file. Adjust it if your stylesheet
43
+ lives elsewhere.
44
+
45
+ For Tailwind 3.4, add this entry to **`tailwind.config.js`** in your project root:
46
+
47
+ ```js
48
+ export default {
49
+ content: [
50
+ "./src/**/*.{js,ts,jsx,tsx}",
51
+ "./node_modules/finality-ux-sampler/src/**/*.jsx",
52
+ ],
53
+ theme: { extend: {} },
54
+ plugins: [],
55
+ };
56
+ ```
57
+
58
+ Keep your app's existing content paths, theme and plugins when merging this
59
+ example. The card uses fixed utility names, so all five tones can be detected.
60
+
61
+ ## Render a state
62
+
63
+ ```jsx
64
+ import { FinalityStateCard, finalityStates } from "finality-ux-sampler";
65
+
66
+ export function PendingCard({ transaction, onViewActivity, onCopyId }) {
67
+ const example = finalityStates.find(s => s.id === "transaction.pending");
68
+ const state = {
69
+ ...example,
70
+ title: "Your transfer is processing",
71
+ status: "Submitted",
72
+ body: `Transfer ${transaction.id} was submitted. Check its existing result.`,
73
+ };
74
+
75
+ return (
76
+ <FinalityStateCard
77
+ state={state}
78
+ onPrimaryAction={onViewActivity}
79
+ onSecondaryAction={() => onCopyId(transaction.id)}
80
+ />
81
+ );
82
+ }
83
+ ```
84
+
85
+ Pass callbacks for the actions your app supports. A missing callback disables
86
+ its button. `disabled={true}` disables both actions while your own operation is
87
+ pending. Callbacks receive the displayed state; the host owns network requests,
88
+ timeouts, duplicate-submission prevention, error handling and focus after a
89
+ wallet dialog. In a Next.js App Router app, render the interactive card from a
90
+ client component with `"use client"`.
91
+
92
+ ## Catalog and types
93
+
94
+ ```ts
95
+ import { finalityCatalog, finalityStates } from "finality-ux-sampler/states";
96
+ import type { FinalityState, FinalityStateId } from "finality-ux-sampler/states";
97
+ ```
98
+
99
+ The plain JSON export is `finality-ux-sampler/states.json`; import it using the
100
+ JSON-module syntax supported by your runtime or bundler.
101
+
102
+ | State ID | Example |
103
+ | --- | --- |
104
+ | `wallet.disconnected` | Choose a wallet |
105
+ | `network.unsupported` | Review the expected network |
106
+ | `signature.requested` | Review a signature |
107
+ | `signature.rejected` | Recover from rejection |
108
+ | `approval.requested` | Review a spending cap |
109
+ | `transaction.pending` | Check an existing transaction |
110
+ | `transaction.finalized` | Review a verified receipt |
111
+ | `transaction.reverted` | Review a revert before retrying |
112
+
113
+ The sampler does not include an unknown-result card. Treat a timeout as an
114
+ unknown outcome in your host flow, preserve the original request, and check it
115
+ before allowing a retry that could send money again.
116
+
117
+ ## License and support
118
+
119
+ Code is MIT; design and interface copy are CC BY 4.0. See `LICENSE` and
120
+ `ATTRIBUTION.md` for the file boundaries and a ready-to-use credit. The separate
121
+ paid Core Kit has its own license. No analytics or tracking is included here.
122
+
123
+ [Support](https://finality-ux-kit.pages.dev/support/).
@@ -0,0 +1,99 @@
1
+ import React from "react";
2
+ const toneClassNames = Object.freeze({
3
+ neutral: {
4
+ shell: "border-slate-700 bg-slate-900",
5
+ badge: "bg-slate-800 text-slate-200",
6
+ icon: "bg-slate-800 text-slate-200"
7
+ },
8
+ attention: {
9
+ shell: "border-sky-900 bg-slate-900",
10
+ badge: "bg-sky-950 text-sky-200",
11
+ icon: "bg-sky-950 text-sky-200"
12
+ },
13
+ warning: {
14
+ shell: "border-amber-900 bg-slate-900",
15
+ badge: "bg-amber-950 text-amber-200",
16
+ icon: "bg-amber-950 text-amber-200"
17
+ },
18
+ danger: {
19
+ shell: "border-rose-900 bg-slate-900",
20
+ badge: "bg-rose-950 text-rose-200",
21
+ icon: "bg-rose-950 text-rose-200"
22
+ },
23
+ success: {
24
+ shell: "border-emerald-900 bg-slate-900",
25
+ badge: "bg-emerald-950 text-emerald-200",
26
+ icon: "bg-emerald-950 text-emerald-200"
27
+ }
28
+ });
29
+ function cx(...values) {
30
+ return values.filter(Boolean).join(" ");
31
+ }
32
+ function FinalityStateCard({
33
+ state,
34
+ onPrimaryAction,
35
+ onSecondaryAction,
36
+ disabled = false
37
+ }) {
38
+ if (!state || !toneClassNames[state.tone]) {
39
+ throw new Error("FinalityStateCard requires a valid Finality state.");
40
+ }
41
+ const tone = toneClassNames[state.tone];
42
+ const live = state.live === "off" ? void 0 : state.live;
43
+ return /* @__PURE__ */ React.createElement(
44
+ "article",
45
+ {
46
+ className: cx(
47
+ "grid min-h-[30rem] grid-rows-[auto_auto_1fr_auto] rounded-[1.5rem] border p-6 text-slate-50 shadow-2xl shadow-black/20",
48
+ tone.shell
49
+ ),
50
+ "data-state-id": state.id,
51
+ "data-state-version": "1.1.1"
52
+ },
53
+ /* @__PURE__ */ React.createElement("header", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-mono text-xs uppercase tracking-[0.16em] text-slate-400" }, state.eyebrow), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold leading-tight" }, state.title)), /* @__PURE__ */ React.createElement(
54
+ "span",
55
+ {
56
+ "aria-hidden": "true",
57
+ className: cx(
58
+ "grid size-11 shrink-0 place-items-center rounded-2xl font-mono text-sm uppercase",
59
+ tone.icon
60
+ )
61
+ },
62
+ String(state.order).padStart(2, "0")
63
+ )),
64
+ /* @__PURE__ */ React.createElement(
65
+ "p",
66
+ {
67
+ "aria-live": live,
68
+ className: cx(
69
+ "mt-6 w-fit rounded-full px-3 py-1 font-mono text-xs font-semibold uppercase tracking-[0.12em]",
70
+ tone.badge
71
+ )
72
+ },
73
+ state.status
74
+ ),
75
+ /* @__PURE__ */ React.createElement("div", { className: "py-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-base leading-7 text-slate-300" }, state.body), /* @__PURE__ */ React.createElement("dl", { className: "mt-6 space-y-4 border-t border-slate-800 pt-5 text-sm" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "font-semibold text-slate-100" }, "Safe recovery"), /* @__PURE__ */ React.createElement("dd", { className: "mt-1 leading-6 text-slate-400" }, state.recovery)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "font-semibold text-slate-100" }, "Safety note"), /* @__PURE__ */ React.createElement("dd", { className: "mt-1 leading-6 text-slate-400" }, state.safetyNote)))),
76
+ /* @__PURE__ */ React.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React.createElement(
77
+ "button",
78
+ {
79
+ type: "button",
80
+ disabled: disabled || typeof onPrimaryAction !== "function",
81
+ onClick: () => onPrimaryAction?.(state),
82
+ className: "min-h-12 rounded-xl bg-lime-300 px-4 py-3 font-semibold text-slate-950 outline-none transition hover:bg-lime-200 focus-visible:ring-4 focus-visible:ring-lime-200/40 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
83
+ },
84
+ state.primaryAction
85
+ ), /* @__PURE__ */ React.createElement(
86
+ "button",
87
+ {
88
+ type: "button",
89
+ disabled: disabled || typeof onSecondaryAction !== "function",
90
+ onClick: () => onSecondaryAction?.(state),
91
+ className: "min-h-12 rounded-xl border border-slate-700 px-4 py-3 font-semibold text-slate-100 outline-none transition hover:border-slate-500 hover:bg-slate-800 focus-visible:ring-4 focus-visible:ring-lime-200/30 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
92
+ },
93
+ state.secondaryAction
94
+ ))
95
+ );
96
+ }
97
+ export {
98
+ FinalityStateCard
99
+ };
@@ -0,0 +1,10 @@
1
+ import type { ComponentType } from "react";
2
+ import type { FinalityState } from "./states.js";
3
+ export * from "./states.js";
4
+ export interface FinalityStateCardProps {
5
+ state: FinalityState;
6
+ onPrimaryAction?: (state: FinalityState) => void;
7
+ onSecondaryAction?: (state: FinalityState) => void;
8
+ disabled?: boolean;
9
+ }
10
+ export const FinalityStateCard: ComponentType<FinalityStateCardProps>;
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export { FinalityStateCard } from "./FinalityStateCard.mjs";
2
+ export { finalityCatalog, finalityStates } from "./states.mjs";
@@ -0,0 +1,27 @@
1
+ export type FinalityStateId = "wallet.disconnected" | "network.unsupported" | "signature.requested" | "signature.rejected" | "approval.requested" | "transaction.pending" | "transaction.finalized" | "transaction.reverted";
2
+ export type FinalityTone = "neutral" | "attention" | "warning" | "danger" | "success";
3
+ export interface FinalityTransition { event: string; label: string; target: string; }
4
+ export interface FinalityState {
5
+ order: number;
6
+ id: FinalityStateId;
7
+ family: string;
8
+ tone: FinalityTone;
9
+ icon: string;
10
+ eyebrow: string;
11
+ title: string;
12
+ body: string;
13
+ status: string;
14
+ primaryAction: string;
15
+ secondaryAction: string;
16
+ recovery: string;
17
+ safetyNote: string;
18
+ live: "off" | "polite";
19
+ figmaVariant: string;
20
+ reactVariant: string;
21
+ transitions: FinalityTransition[];
22
+ }
23
+ export const finalityStates: FinalityState[];
24
+ export const finalityCatalog: {
25
+ catalog: { id: string; version: string; title: string; updatedAt: string; language: string; stateCount: number; licenses: { design: string; code: string } };
26
+ states: FinalityState[];
27
+ };
@@ -0,0 +1,246 @@
1
+ {
2
+ "catalog": {
3
+ "id": "finality-ux-sampler",
4
+ "version": "1.1.1",
5
+ "title": "Finality UX Kit — Free Sampler",
6
+ "updatedAt": "2026-08-21",
7
+ "language": "en",
8
+ "stateCount": 8,
9
+ "licenses": {
10
+ "design": "CC BY 4.0",
11
+ "code": "MIT"
12
+ }
13
+ },
14
+ "states": [
15
+ {
16
+ "order": 1,
17
+ "id": "wallet.disconnected",
18
+ "family": "wallet",
19
+ "tone": "neutral",
20
+ "icon": "wallet",
21
+ "eyebrow": "Before the request",
22
+ "title": "Connect a wallet you trust",
23
+ "body": "No wallet is connected. Choose one to review this action; connecting does not approve or submit anything.",
24
+ "status": "Not connected",
25
+ "primaryAction": "Choose wallet",
26
+ "secondaryAction": "Why connect?",
27
+ "recovery": "Check the site address before selecting a wallet.",
28
+ "safetyNote": "Never enter a seed phrase on this screen.",
29
+ "live": "off",
30
+ "figmaVariant": "State=Disconnected,Tone=Neutral",
31
+ "reactVariant": "disconnected",
32
+ "transitions": [
33
+ {
34
+ "event": "wallet_connected",
35
+ "label": "Wallet connected",
36
+ "target": "external:next-request"
37
+ }
38
+ ]
39
+ },
40
+ {
41
+ "order": 2,
42
+ "id": "network.unsupported",
43
+ "family": "network",
44
+ "tone": "warning",
45
+ "icon": "network",
46
+ "eyebrow": "Network check",
47
+ "title": "Switch to Base to continue",
48
+ "body": "This action is prepared for Base, but your wallet is connected to Ethereum. Review the network change in your wallet.",
49
+ "status": "Wrong network",
50
+ "primaryAction": "Switch network",
51
+ "secondaryAction": "Cancel action",
52
+ "recovery": "If Base is unexpected, cancel and verify the original link.",
53
+ "safetyNote": "A network switch does not authorize a transaction.",
54
+ "live": "polite",
55
+ "figmaVariant": "State=Unsupported network,Tone=Warning",
56
+ "reactVariant": "unsupported-network",
57
+ "transitions": [
58
+ {
59
+ "event": "network_changed",
60
+ "label": "Expected network selected",
61
+ "target": "external:retry-intent"
62
+ },
63
+ {
64
+ "event": "cancelled",
65
+ "label": "Action cancelled",
66
+ "target": "external:idle"
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "order": 3,
72
+ "id": "signature.requested",
73
+ "family": "signature",
74
+ "tone": "attention",
75
+ "icon": "signature",
76
+ "eyebrow": "Wallet action",
77
+ "title": "Review the signature request",
78
+ "body": "Your wallet has the final request. Match the site, action, and displayed details before you sign.",
79
+ "status": "Waiting for signature",
80
+ "primaryAction": "Open wallet",
81
+ "secondaryAction": "Cancel request",
82
+ "recovery": "If the wallet details differ from this screen, reject the request.",
83
+ "safetyNote": "A signature can authorize actions even when no network fee is shown.",
84
+ "live": "polite",
85
+ "figmaVariant": "State=Signature requested,Tone=Attention",
86
+ "reactVariant": "signature-requested",
87
+ "transitions": [
88
+ {
89
+ "event": "signature_approved",
90
+ "label": "Signature approved",
91
+ "target": "transaction.pending"
92
+ },
93
+ {
94
+ "event": "signature_rejected",
95
+ "label": "Signature rejected",
96
+ "target": "signature.rejected"
97
+ }
98
+ ]
99
+ },
100
+ {
101
+ "order": 4,
102
+ "id": "signature.rejected",
103
+ "family": "signature",
104
+ "tone": "neutral",
105
+ "icon": "close",
106
+ "eyebrow": "Request ended",
107
+ "title": "You rejected the request",
108
+ "body": "Nothing was signed or submitted. You can close this action or review the details and start again.",
109
+ "status": "Not submitted",
110
+ "primaryAction": "Review and retry",
111
+ "secondaryAction": "Close",
112
+ "recovery": "A fresh request should show the same site, action, and amount.",
113
+ "safetyNote": "Repeated prompts can be a warning sign; pause if you did not retry.",
114
+ "live": "polite",
115
+ "figmaVariant": "State=Signature rejected,Tone=Neutral",
116
+ "reactVariant": "signature-rejected",
117
+ "transitions": [
118
+ {
119
+ "event": "retry",
120
+ "label": "Fresh request created",
121
+ "target": "signature.requested"
122
+ },
123
+ {
124
+ "event": "dismissed",
125
+ "label": "Flow closed",
126
+ "target": "external:idle"
127
+ }
128
+ ]
129
+ },
130
+ {
131
+ "order": 5,
132
+ "id": "approval.requested",
133
+ "family": "approval",
134
+ "tone": "warning",
135
+ "icon": "shield",
136
+ "eyebrow": "Permission request",
137
+ "title": "Review the 250 USDC spending cap",
138
+ "body": "This permission lets the app use up to 250 USDC. It is not the transfer itself and remains active until used or revoked.",
139
+ "status": "Approval required",
140
+ "primaryAction": "Review permission",
141
+ "secondaryAction": "Use a lower cap",
142
+ "recovery": "Prefer the smallest cap and duration that complete the intended action.",
143
+ "safetyNote": "Unlimited approvals increase the impact of a compromised app.",
144
+ "live": "polite",
145
+ "figmaVariant": "State=Approval requested,Tone=Warning",
146
+ "reactVariant": "approval-requested",
147
+ "transitions": [
148
+ {
149
+ "event": "approval_signed",
150
+ "label": "Approval submitted",
151
+ "target": "transaction.pending"
152
+ },
153
+ {
154
+ "event": "approval_rejected",
155
+ "label": "Approval rejected",
156
+ "target": "signature.rejected"
157
+ }
158
+ ]
159
+ },
160
+ {
161
+ "order": 6,
162
+ "id": "transaction.pending",
163
+ "family": "transaction",
164
+ "tone": "attention",
165
+ "icon": "clock",
166
+ "eyebrow": "Submitted",
167
+ "title": "Transaction is confirming",
168
+ "body": "Transaction 0x8D…91A was submitted. One confirmation is recorded; two are required before this app treats it as final.",
169
+ "status": "1 of 2 confirmations",
170
+ "primaryAction": "View activity",
171
+ "secondaryAction": "Copy transaction ID",
172
+ "recovery": "Do not submit the same action again while its final status is unknown.",
173
+ "safetyNote": "Leaving this page does not cancel a submitted transaction.",
174
+ "live": "polite",
175
+ "figmaVariant": "State=Transaction pending,Tone=Attention",
176
+ "reactVariant": "transaction-pending",
177
+ "transitions": [
178
+ {
179
+ "event": "finalized",
180
+ "label": "Required finality reached",
181
+ "target": "transaction.finalized"
182
+ },
183
+ {
184
+ "event": "reverted",
185
+ "label": "Execution reverted",
186
+ "target": "transaction.reverted"
187
+ },
188
+ {
189
+ "event": "timeout",
190
+ "label": "Status remains unknown",
191
+ "target": "external:transaction-unknown"
192
+ }
193
+ ]
194
+ },
195
+ {
196
+ "order": 7,
197
+ "id": "transaction.finalized",
198
+ "family": "transaction",
199
+ "tone": "success",
200
+ "icon": "check",
201
+ "eyebrow": "Complete",
202
+ "title": "Transfer reached finality",
203
+ "body": "The confirmed transfer completed at 14:32 UTC. The receipt includes the network, amount, destination, and transaction ID.",
204
+ "status": "Finalized",
205
+ "primaryAction": "View receipt",
206
+ "secondaryAction": "Copy transaction ID",
207
+ "recovery": "Keep the receipt if this action is part of a later support request.",
208
+ "safetyNote": "Verify the destination and amount in the receipt, not only the success label.",
209
+ "live": "polite",
210
+ "figmaVariant": "State=Transaction finalized,Tone=Success",
211
+ "reactVariant": "transaction-finalized",
212
+ "transitions": [
213
+ {
214
+ "event": "receipt_opened",
215
+ "label": "Receipt opened",
216
+ "target": "external:block-explorer"
217
+ }
218
+ ]
219
+ },
220
+ {
221
+ "order": 8,
222
+ "id": "transaction.reverted",
223
+ "family": "transaction",
224
+ "tone": "danger",
225
+ "icon": "error",
226
+ "eyebrow": "Did not complete",
227
+ "title": "Transaction reverted",
228
+ "body": "The requested state change did not complete. The network may still charge a fee for the failed execution.",
229
+ "status": "Reverted onchain",
230
+ "primaryAction": "Review failure",
231
+ "secondaryAction": "View transaction",
232
+ "recovery": "Fix the stated cause before creating a new transaction.",
233
+ "safetyNote": "Do not promise that all funds or fees are unchanged after a revert.",
234
+ "live": "polite",
235
+ "figmaVariant": "State=Transaction reverted,Tone=Danger",
236
+ "reactVariant": "transaction-reverted",
237
+ "transitions": [
238
+ {
239
+ "event": "cause_resolved",
240
+ "label": "Failure cause resolved",
241
+ "target": "external:rebuild-transaction"
242
+ }
243
+ ]
244
+ }
245
+ ]
246
+ }
@@ -0,0 +1,247 @@
1
+ export const finalityCatalog = {
2
+ "catalog": {
3
+ "id": "finality-ux-sampler",
4
+ "version": "1.1.1",
5
+ "title": "Finality UX Kit — Free Sampler",
6
+ "updatedAt": "2026-08-21",
7
+ "language": "en",
8
+ "stateCount": 8,
9
+ "licenses": {
10
+ "design": "CC BY 4.0",
11
+ "code": "MIT"
12
+ }
13
+ },
14
+ "states": [
15
+ {
16
+ "order": 1,
17
+ "id": "wallet.disconnected",
18
+ "family": "wallet",
19
+ "tone": "neutral",
20
+ "icon": "wallet",
21
+ "eyebrow": "Before the request",
22
+ "title": "Connect a wallet you trust",
23
+ "body": "No wallet is connected. Choose one to review this action; connecting does not approve or submit anything.",
24
+ "status": "Not connected",
25
+ "primaryAction": "Choose wallet",
26
+ "secondaryAction": "Why connect?",
27
+ "recovery": "Check the site address before selecting a wallet.",
28
+ "safetyNote": "Never enter a seed phrase on this screen.",
29
+ "live": "off",
30
+ "figmaVariant": "State=Disconnected,Tone=Neutral",
31
+ "reactVariant": "disconnected",
32
+ "transitions": [
33
+ {
34
+ "event": "wallet_connected",
35
+ "label": "Wallet connected",
36
+ "target": "external:next-request"
37
+ }
38
+ ]
39
+ },
40
+ {
41
+ "order": 2,
42
+ "id": "network.unsupported",
43
+ "family": "network",
44
+ "tone": "warning",
45
+ "icon": "network",
46
+ "eyebrow": "Network check",
47
+ "title": "Switch to Base to continue",
48
+ "body": "This action is prepared for Base, but your wallet is connected to Ethereum. Review the network change in your wallet.",
49
+ "status": "Wrong network",
50
+ "primaryAction": "Switch network",
51
+ "secondaryAction": "Cancel action",
52
+ "recovery": "If Base is unexpected, cancel and verify the original link.",
53
+ "safetyNote": "A network switch does not authorize a transaction.",
54
+ "live": "polite",
55
+ "figmaVariant": "State=Unsupported network,Tone=Warning",
56
+ "reactVariant": "unsupported-network",
57
+ "transitions": [
58
+ {
59
+ "event": "network_changed",
60
+ "label": "Expected network selected",
61
+ "target": "external:retry-intent"
62
+ },
63
+ {
64
+ "event": "cancelled",
65
+ "label": "Action cancelled",
66
+ "target": "external:idle"
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "order": 3,
72
+ "id": "signature.requested",
73
+ "family": "signature",
74
+ "tone": "attention",
75
+ "icon": "signature",
76
+ "eyebrow": "Wallet action",
77
+ "title": "Review the signature request",
78
+ "body": "Your wallet has the final request. Match the site, action, and displayed details before you sign.",
79
+ "status": "Waiting for signature",
80
+ "primaryAction": "Open wallet",
81
+ "secondaryAction": "Cancel request",
82
+ "recovery": "If the wallet details differ from this screen, reject the request.",
83
+ "safetyNote": "A signature can authorize actions even when no network fee is shown.",
84
+ "live": "polite",
85
+ "figmaVariant": "State=Signature requested,Tone=Attention",
86
+ "reactVariant": "signature-requested",
87
+ "transitions": [
88
+ {
89
+ "event": "signature_approved",
90
+ "label": "Signature approved",
91
+ "target": "transaction.pending"
92
+ },
93
+ {
94
+ "event": "signature_rejected",
95
+ "label": "Signature rejected",
96
+ "target": "signature.rejected"
97
+ }
98
+ ]
99
+ },
100
+ {
101
+ "order": 4,
102
+ "id": "signature.rejected",
103
+ "family": "signature",
104
+ "tone": "neutral",
105
+ "icon": "close",
106
+ "eyebrow": "Request ended",
107
+ "title": "You rejected the request",
108
+ "body": "Nothing was signed or submitted. You can close this action or review the details and start again.",
109
+ "status": "Not submitted",
110
+ "primaryAction": "Review and retry",
111
+ "secondaryAction": "Close",
112
+ "recovery": "A fresh request should show the same site, action, and amount.",
113
+ "safetyNote": "Repeated prompts can be a warning sign; pause if you did not retry.",
114
+ "live": "polite",
115
+ "figmaVariant": "State=Signature rejected,Tone=Neutral",
116
+ "reactVariant": "signature-rejected",
117
+ "transitions": [
118
+ {
119
+ "event": "retry",
120
+ "label": "Fresh request created",
121
+ "target": "signature.requested"
122
+ },
123
+ {
124
+ "event": "dismissed",
125
+ "label": "Flow closed",
126
+ "target": "external:idle"
127
+ }
128
+ ]
129
+ },
130
+ {
131
+ "order": 5,
132
+ "id": "approval.requested",
133
+ "family": "approval",
134
+ "tone": "warning",
135
+ "icon": "shield",
136
+ "eyebrow": "Permission request",
137
+ "title": "Review the 250 USDC spending cap",
138
+ "body": "This permission lets the app use up to 250 USDC. It is not the transfer itself and remains active until used or revoked.",
139
+ "status": "Approval required",
140
+ "primaryAction": "Review permission",
141
+ "secondaryAction": "Use a lower cap",
142
+ "recovery": "Prefer the smallest cap and duration that complete the intended action.",
143
+ "safetyNote": "Unlimited approvals increase the impact of a compromised app.",
144
+ "live": "polite",
145
+ "figmaVariant": "State=Approval requested,Tone=Warning",
146
+ "reactVariant": "approval-requested",
147
+ "transitions": [
148
+ {
149
+ "event": "approval_signed",
150
+ "label": "Approval submitted",
151
+ "target": "transaction.pending"
152
+ },
153
+ {
154
+ "event": "approval_rejected",
155
+ "label": "Approval rejected",
156
+ "target": "signature.rejected"
157
+ }
158
+ ]
159
+ },
160
+ {
161
+ "order": 6,
162
+ "id": "transaction.pending",
163
+ "family": "transaction",
164
+ "tone": "attention",
165
+ "icon": "clock",
166
+ "eyebrow": "Submitted",
167
+ "title": "Transaction is confirming",
168
+ "body": "Transaction 0x8D…91A was submitted. One confirmation is recorded; two are required before this app treats it as final.",
169
+ "status": "1 of 2 confirmations",
170
+ "primaryAction": "View activity",
171
+ "secondaryAction": "Copy transaction ID",
172
+ "recovery": "Do not submit the same action again while its final status is unknown.",
173
+ "safetyNote": "Leaving this page does not cancel a submitted transaction.",
174
+ "live": "polite",
175
+ "figmaVariant": "State=Transaction pending,Tone=Attention",
176
+ "reactVariant": "transaction-pending",
177
+ "transitions": [
178
+ {
179
+ "event": "finalized",
180
+ "label": "Required finality reached",
181
+ "target": "transaction.finalized"
182
+ },
183
+ {
184
+ "event": "reverted",
185
+ "label": "Execution reverted",
186
+ "target": "transaction.reverted"
187
+ },
188
+ {
189
+ "event": "timeout",
190
+ "label": "Status remains unknown",
191
+ "target": "external:transaction-unknown"
192
+ }
193
+ ]
194
+ },
195
+ {
196
+ "order": 7,
197
+ "id": "transaction.finalized",
198
+ "family": "transaction",
199
+ "tone": "success",
200
+ "icon": "check",
201
+ "eyebrow": "Complete",
202
+ "title": "Transfer reached finality",
203
+ "body": "The confirmed transfer completed at 14:32 UTC. The receipt includes the network, amount, destination, and transaction ID.",
204
+ "status": "Finalized",
205
+ "primaryAction": "View receipt",
206
+ "secondaryAction": "Copy transaction ID",
207
+ "recovery": "Keep the receipt if this action is part of a later support request.",
208
+ "safetyNote": "Verify the destination and amount in the receipt, not only the success label.",
209
+ "live": "polite",
210
+ "figmaVariant": "State=Transaction finalized,Tone=Success",
211
+ "reactVariant": "transaction-finalized",
212
+ "transitions": [
213
+ {
214
+ "event": "receipt_opened",
215
+ "label": "Receipt opened",
216
+ "target": "external:block-explorer"
217
+ }
218
+ ]
219
+ },
220
+ {
221
+ "order": 8,
222
+ "id": "transaction.reverted",
223
+ "family": "transaction",
224
+ "tone": "danger",
225
+ "icon": "error",
226
+ "eyebrow": "Did not complete",
227
+ "title": "Transaction reverted",
228
+ "body": "The requested state change did not complete. The network may still charge a fee for the failed execution.",
229
+ "status": "Reverted onchain",
230
+ "primaryAction": "Review failure",
231
+ "secondaryAction": "View transaction",
232
+ "recovery": "Fix the stated cause before creating a new transaction.",
233
+ "safetyNote": "Do not promise that all funds or fees are unchanged after a revert.",
234
+ "live": "polite",
235
+ "figmaVariant": "State=Transaction reverted,Tone=Danger",
236
+ "reactVariant": "transaction-reverted",
237
+ "transitions": [
238
+ {
239
+ "event": "cause_resolved",
240
+ "label": "Failure cause resolved",
241
+ "target": "external:rebuild-transaction"
242
+ }
243
+ ]
244
+ }
245
+ ]
246
+ };
247
+ export const finalityStates = finalityCatalog.states;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "finality-ux-sampler",
3
+ "version": "1.1.1",
4
+ "description": "Free React/Tailwind state card with eight wallet and transaction copy examples.",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "./states": {
15
+ "types": "./dist/states.d.ts",
16
+ "import": "./dist/states.mjs",
17
+ "default": "./dist/states.mjs"
18
+ },
19
+ "./states.json": "./dist/states.json",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md",
26
+ "LICENSE",
27
+ "LICENSE-CODE.txt",
28
+ "LICENSE-DESIGN.txt",
29
+ "ATTRIBUTION.md"
30
+ ],
31
+ "sideEffects": false,
32
+ "peerDependencies": {
33
+ "react": "^18.2.0 || ^19.0.0"
34
+ },
35
+ "keywords": [
36
+ "web3",
37
+ "wallet",
38
+ "transaction",
39
+ "ux",
40
+ "react",
41
+ "tailwind",
42
+ "figma",
43
+ "microcopy"
44
+ ],
45
+ "license": "(MIT AND CC-BY-4.0)",
46
+ "homepage": "https://finality-ux-kit.pages.dev/from-code/",
47
+ "bugs": {
48
+ "url": "https://finality-ux-kit.pages.dev/support/"
49
+ },
50
+ "author": "Finality UX Kit",
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ }
55
+ }
@@ -0,0 +1,121 @@
1
+ import React from "react";
2
+
3
+ const toneClassNames = Object.freeze({
4
+ neutral: {
5
+ shell: "border-slate-700 bg-slate-900",
6
+ badge: "bg-slate-800 text-slate-200",
7
+ icon: "bg-slate-800 text-slate-200"
8
+ },
9
+ attention: {
10
+ shell: "border-sky-900 bg-slate-900",
11
+ badge: "bg-sky-950 text-sky-200",
12
+ icon: "bg-sky-950 text-sky-200"
13
+ },
14
+ warning: {
15
+ shell: "border-amber-900 bg-slate-900",
16
+ badge: "bg-amber-950 text-amber-200",
17
+ icon: "bg-amber-950 text-amber-200"
18
+ },
19
+ danger: {
20
+ shell: "border-rose-900 bg-slate-900",
21
+ badge: "bg-rose-950 text-rose-200",
22
+ icon: "bg-rose-950 text-rose-200"
23
+ },
24
+ success: {
25
+ shell: "border-emerald-900 bg-slate-900",
26
+ badge: "bg-emerald-950 text-emerald-200",
27
+ icon: "bg-emerald-950 text-emerald-200"
28
+ }
29
+ });
30
+
31
+ function cx(...values) {
32
+ return values.filter(Boolean).join(" ");
33
+ }
34
+
35
+ export function FinalityStateCard({
36
+ state,
37
+ onPrimaryAction,
38
+ onSecondaryAction,
39
+ disabled = false
40
+ }) {
41
+ if (!state || !toneClassNames[state.tone]) {
42
+ throw new Error("FinalityStateCard requires a valid Finality state.");
43
+ }
44
+
45
+ const tone = toneClassNames[state.tone];
46
+ const live = state.live === "off" ? undefined : state.live;
47
+
48
+ return (
49
+ <article
50
+ className={cx(
51
+ "grid min-h-[30rem] grid-rows-[auto_auto_1fr_auto] rounded-[1.5rem] border p-6 text-slate-50 shadow-2xl shadow-black/20",
52
+ tone.shell
53
+ )}
54
+ data-state-id={state.id}
55
+ data-state-version="1.1.1"
56
+ >
57
+ <header className="flex items-start justify-between gap-4">
58
+ <div>
59
+ <p className="font-mono text-xs uppercase tracking-[0.16em] text-slate-400">
60
+ {state.eyebrow}
61
+ </p>
62
+ <h2 className="mt-3 text-2xl font-semibold leading-tight">
63
+ {state.title}
64
+ </h2>
65
+ </div>
66
+ <span
67
+ aria-hidden="true"
68
+ className={cx(
69
+ "grid size-11 shrink-0 place-items-center rounded-2xl font-mono text-sm uppercase",
70
+ tone.icon
71
+ )}
72
+ >
73
+ {String(state.order).padStart(2, "0")}
74
+ </span>
75
+ </header>
76
+
77
+ <p
78
+ aria-live={live}
79
+ className={cx(
80
+ "mt-6 w-fit rounded-full px-3 py-1 font-mono text-xs font-semibold uppercase tracking-[0.12em]",
81
+ tone.badge
82
+ )}
83
+ >
84
+ {state.status}
85
+ </p>
86
+
87
+ <div className="py-6">
88
+ <p className="text-base leading-7 text-slate-300">{state.body}</p>
89
+ <dl className="mt-6 space-y-4 border-t border-slate-800 pt-5 text-sm">
90
+ <div>
91
+ <dt className="font-semibold text-slate-100">Safe recovery</dt>
92
+ <dd className="mt-1 leading-6 text-slate-400">{state.recovery}</dd>
93
+ </div>
94
+ <div>
95
+ <dt className="font-semibold text-slate-100">Safety note</dt>
96
+ <dd className="mt-1 leading-6 text-slate-400">{state.safetyNote}</dd>
97
+ </div>
98
+ </dl>
99
+ </div>
100
+
101
+ <div className="grid gap-3">
102
+ <button
103
+ type="button"
104
+ disabled={disabled || typeof onPrimaryAction !== "function"}
105
+ onClick={() => onPrimaryAction?.(state)}
106
+ className="min-h-12 rounded-xl bg-lime-300 px-4 py-3 font-semibold text-slate-950 outline-none transition hover:bg-lime-200 focus-visible:ring-4 focus-visible:ring-lime-200/40 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
107
+ >
108
+ {state.primaryAction}
109
+ </button>
110
+ <button
111
+ type="button"
112
+ disabled={disabled || typeof onSecondaryAction !== "function"}
113
+ onClick={() => onSecondaryAction?.(state)}
114
+ className="min-h-12 rounded-xl border border-slate-700 px-4 py-3 font-semibold text-slate-100 outline-none transition hover:border-slate-500 hover:bg-slate-800 focus-visible:ring-4 focus-visible:ring-lime-200/30 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
115
+ >
116
+ {state.secondaryAction}
117
+ </button>
118
+ </div>
119
+ </article>
120
+ );
121
+ }