Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-pugs-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bc-forge/react': minor
---

Add four accessible UI-kit primitives: `Pagination` with page-window truncation and a page-size selector (#1165), `MetricCard`/`MetricStatBox` with trend colours and a loading skeleton (#1166), `ConfirmationDialog` with a checkbox-or-keyword gate for destructive actions (#1167), and `EmptyState` for zero-result tables and search results (#1168). All four are inline-styled from the existing theme tokens, fully keyboard operable, and exported from the package root.
9 changes: 9 additions & 0 deletions .github/vulnerability-exceptions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
# Each exception must include an advisory ID and expiry date.

exceptions:
# Example format (commented out):
# - cve: CVE-2024-12345
# advisory: GHSA-xxxx-xxxx-xxxx
# package: package-name@version
# severity: critical
# reason: Brief explanation of why this cannot be fixed immediately
# expires: 2024-12-31
# approved-by: maintainer-name
# approved-date: 2024-01-15
- cve: CVE-2026-106445
advisory: GHSA-p8wg-vrv2-v86f
package: handlebars@4.7.9
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion react/playwright.visual.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ export default defineConfig({
animations: 'disabled',
caret: 'hide',
scale: 'css',
maxDiffPixels: 0,
// `threshold: 0` keeps every pixel strict — any real colour shift in a
// single pixel still fails. `maxDiffPixels` only absorbs the handful of
// stray pixels Chromium re-rasterises between runs: anti-aliasing on
// `border-radius` corners is not bit-reproducible on the pinned Playwright
// image, and it drifts with no code change at all (regenerating the
// baselines moved `products-mintform--ready` by 48px and
// `products-vaultdepositwidget--ready` by 2px). The budget is roughly 3x
// that observed noise floor, still ~2 orders of magnitude below a real
// regression: a moved or recoloured element moves thousands of pixels.
maxDiffPixels: 150,
threshold: 0,
},
},
Expand Down
122 changes: 100 additions & 22 deletions react/src/components/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,38 +49,116 @@ provided the badge becomes a keyboard-focusable interactive control
<Badge variant="warning" onClick={() => alert('clicked')}>Dismiss</Badge>
```

## CopyToClipboard (#1159)
## Pagination

Previous/next controls, a windowed list of page buttons, and a page-size
selector. `getPageItems` is exported on its own for callers that render their own
controls: it returns page numbers ascending, with `null` marking a run that
collapsed into an ellipsis.

| Prop | Type | Default | Description |
| ---------------------- | ----------------------------------- | ------------------ | ----------------------------------------------------------------------- |
| `page` | `number` | — (required) | Current 1-based page; clamped into `[1, pageCount]` before rendering. |
| `pageCount` | `number` | — (required) | Total pages; anything below 1 is treated as a single page. |
| `pageSize` | `number` | `10` | Rows per page, reported back through `onPageChange`. |
| `pageSizeOptions` | `readonly number[]` | `[10, 25, 50, 100]` | Sizes offered by the selector; the active one is always added. |
| `showPageSizeSelector` | `boolean` | `false` | Hide the selector for fixed-size listings. |
| `siblingCount` | `number` | `1` | Pages kept on each side of the current one. |
| `disabled` | `boolean` | `false` | Disables every control, e.g. while a page loads. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Sizing preset. |
| `onPageChange` | `(page: number, pageSize: number) => void` | — | Fired with an always in-range page; changing the size reports page `1`. |

Previous is disabled on the first page, Next on the last. The `nav` carries an
`aria-label` (`label`), the active button `aria-current="page"`, and every
control has an accessible name.

Interactive button for copying Stellar public keys, contract addresses, and transaction hashes with animated transient 'Copied!' tooltip confirmation.
```tsx
<Pagination page={page} pageCount={totalPages} pageSize={25} onPageChange={setPage} />
```

## MetricCard

A single figure with a trend badge. `MetricStatBox` is the compact form — it
takes `label` instead of `title` and defaults to the `sm` size.

| Prop | Type | Default | Description |
| -------------- | ------------------------------------------------- | -------------------- | -------------------------------------------------------------------- |
| `title` | `string` | — (required) | Heading of the metric. |
| `value` | `string \| number \| bigint \| null \| undefined` | — (required) | The metric; `bigint` is stringified, never coerced through `Number`. |
| `delta` | `number \| null` | — | Percentage change; the sign picks success / danger / neutral. |
| `deltaLabel` | `string` | — | Caption inside the badge, e.g. `'vs last week'`. |
| `subtitle` | `React.ReactNode` | — | Secondary line under the value. |
| `loading` | `boolean` | `false` | Renders the skeleton instead of the value. |
| `loadingLabel` | `string` | `'Loading…'` | Announced through a `role="status"` while the skeleton shows. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Sizing preset. |

The skeleton sets `aria-busy="true"` and keeps the value node out of both the
visual and accessibility trees.

```tsx
<MetricCard title="Total supply" value={supply} delta={2.4} deltaLabel="vs last week" />
<MetricCard title="TVL" value={tvl} loading />
```

| Prop | Type | Default | Description |
| ---------------- | ---------------------------------------- | ----------------------- | -------------------------------------------------------------- |
| `text` | `string` | — (required) | Text to copy to clipboard. |
| `label` | `string` | `'Copy to clipboard'` | Button label and accessible text. |
| `successMessage` | `string` | `'Copied!'` | Transient feedback message. |
| `timeout` | `number` | `2000` | Duration in ms to display success feedback. |
| `onCopy` | `(text: string) => void` | — | Callback fired when copy succeeds. |
| `onError` | `(error: Error) => void` | — | Callback fired when copy fails. |
## ConfirmationDialog

Guards a destructive action (burn, revoke role). Confirm stays disabled until
the acknowledgement gate is satisfied, so the dialog cannot be dismissed into a
write by a stray click. It is self-contained: its own portal, backdrop, `Escape`
handling and focus trap.

| Prop | Type | Default | Description |
| -------------------- | --------------------------------- | -------------------------------- | ----------------------------------------------------------------- |
| `open` | `boolean` | — (required) | Nothing renders while false. |
| `onClose` | `() => void` | — (required) | Called by Cancel, the backdrop and `Escape`. |
| `onConfirm` | `() => void` | — (required) | Called by Confirm once the gate is satisfied. |
| `title` | `string` | — (required) | Heading, wired via `aria-labelledby`. |
| `description` | `React.ReactNode` | — | Body copy, wired via `aria-describedby`. |
| `children` | `React.ReactNode` | — | Extra content between the description and the gate. |
| `variant` | `'danger' \| 'warning'` | `'danger'` | Tone of the dialog and the confirming button. |
| `confirmationMode` | `'checkbox' \| 'keyword' \| 'none'` | `'checkbox'` | Which gate guards Confirm. |
| `confirmationKeyword`| `string` | `'CONFIRM'` | Word to type in `'keyword'` mode; case- and trim-insensitive. |
| `confirmationLabel` | `string` | `'I understand this cannot be undone.'` | Text of the confirmation checkbox. |
| `busy` | `boolean` | `false` | In-flight write: locks both buttons and unbinds `Escape`. |

Focus moves onto the panel (never straight onto the destructive button), `Tab`
cycles within the dialog, and the previously focused element is refocused on
close. The gate resets between openings because the body unmounts with `open`.

```tsx
<CopyToClipboard text="GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" />
<CopyToClipboard text={txHash} label="Copy Hash" successMessage="Hash Copied!" />
<ConfirmationDialog
open={pending}
onClose={cancel}
onConfirm={burn}
title="Burn 1,000,000 BCC?"
confirmationMode="keyword"
confirmationKeyword="BURN"
busy={submitting}
/>
```

## ConnectionStatusBar (#1183)
## EmptyState

Status bar displaying connected wallet name, network pill, and real-time Soroban RPC ping latency indicator (green < 500ms, yellow 500–1500ms, red > 1500ms or offline).
The zero-result state for a table, a listing or a search. Renders a built-in
inline SVG by default; pass `icon={null}` for none, or any node for your own.

| Prop | Type | Default | Description |
| ---------------- | ---------------------------------------- | ----------------------- | -------------------------------------------------------------- |
| `rpcUrl` | `string` | — | Optional RPC URL to ping. |
| `pingRpc` | `() => Promise<number>` | — | Optional custom ping handler returning latency in ms. |
| `pingIntervalMs` | `number` | `10000` | Interval in ms between latency checks (0 to disable). |
| `onLatencyChange`| `(latency: number \| null) => void` | — | Callback fired when latency updates. |
| Prop | Type | Default | Description |
| ------------------ | ----------------------------------- | -------- | --------------------------------------------------------------- |
| `title` | `string` | — | Short headline, e.g. `'No transactions yet'`. |
| `icon` | `React.ReactNode` | built-in | Illustration; `null` renders none. |
| `description` | `React.ReactNode` | — | One or two sentences on what to do next. |
| `action` | `EmptyStateAction \| React.ReactNode` | — | Primary next step; an object renders the button for you. |
| `secondaryAction` | `EmptyStateAction \| React.ReactNode` | — | Lower-emphasis alternative. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Sizing preset. |

```tsx
<ConnectionStatusBar />
<ConnectionStatusBar pingIntervalMs={15000} />
<EmptyState
icon={<WalletIcon />}
title="No transactions yet"
description="Deposits and rewards will appear here once the vault is funded."
action={{ label: 'Deposit', onClick: openDeposit }}
secondaryAction={{ label: 'Learn more', onClick: openDocs }}
/>
```

## Product components (#950)
Expand Down
Loading
Loading