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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ All of these live under [`./docs/`](./docs/).
| Shared error taxonomy | [`ERROR_TAXONOMY.md`](./docs/ERROR_TAXONOMY.md) |
| Escrow storage migration guide | [`ESCROW_MIGRATION.md`](./docs/ESCROW_MIGRATION.md) |
| Shared test fixtures | [`FIXTURE_FRAMEWORK.md`](./docs/FIXTURE_FRAMEWORK.md) |
| Status severity mapping | [`STATUS_SEVERITY_MAPPING.md`](./docs/STATUS_SEVERITY_MAPPING.md) |
| Contributor guide | [`CONTRIBUTOR_GUIDE.md`](./docs/CONTRIBUTOR_GUIDE.md) |
| Maintainer guide | [`MAINTAINER_GUIDE.md`](./docs/MAINTAINER_GUIDE.md) |
| Issue writing guide | [`ISSUE_WRITING_GUIDE.md`](./docs/ISSUE_WRITING_GUIDE.md) |
Expand Down
90 changes: 26 additions & 64 deletions apps/web/components/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import clsx from "clsx";
import type { AnchorTransactionStatus, MilestoneStatus, TransactionReceiptStatus } from "@anchorkit/types";
import { receiptStatusBadge } from "@anchorkit/stellar-kit";
import { anchorStatusBadge } from "@anchorkit/anchor-utils";
import {
badgeClasses,
alertClasses,
getAnchorSeverity,
getMilestoneSeverity,
getReceiptSeverity,
getAccountSeverity,
} from "@anchorkit/stellar-kit";

/**
* Renders an anchor transaction status badge using the shared
* {@link anchorStatusBadge} mapping from `@anchorkit/anchor-utils`.
* {@link getAnchorSeverity} mapping from `@anchorkit/stellar-kit`.
*
* The colour scheme is derived from the badge's `tone` so there is a single
* source of truth for status labelling and styling.
*/
export function AnchorStatusBadge({ status }: { status: AnchorTransactionStatus }) {
const { label, tone } = anchorStatusBadge(status);

const toneStyles: Record<string, string> = {
neutral: "bg-ink-100 text-ink-700 border-ink-200 dark:bg-ink-900 dark:text-ink-300 dark:border-ink-800",
amber: "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-900",
blue: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900",
green: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-300 dark:border-green-900",
red: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-300 dark:border-red-900",
};
const { label, tone } = getAnchorSeverity(status);

return (
<span
className={clsx(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-mono-xs font-medium",
toneStyles[tone] ?? toneStyles.blue,
badgeClasses(tone),
)}
>
<span className="mr-1.5 inline-block h-1.5 w-1.5 rounded-full bg-current opacity-80" />
Expand All @@ -35,79 +33,49 @@ export function AnchorStatusBadge({ status }: { status: AnchorTransactionStatus
}

export function MilestoneStatusBadge({ status }: { status: MilestoneStatus }) {
const styles: Record<MilestoneStatus, string> = {
draft: "bg-ink-100 text-ink-700 border-ink-200 dark:bg-ink-900 dark:text-ink-300 dark:border-ink-800",
active: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900",
evidence_submitted: "bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/40 dark:text-cyan-300 dark:border-cyan-900",
approved: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-300 dark:border-green-900",
disputed: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-300 dark:border-red-900",
ready_for_release: "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900",
released: "bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950/60 dark:text-emerald-200 dark:border-emerald-900",
};
const label: Record<MilestoneStatus, string> = {
draft: "Draft",
active: "Active",
evidence_submitted: "Evidence Submitted",
approved: "Approved",
disputed: "Disputed",
ready_for_release: "Ready for Release",
released: "Released",
};
const { label, tone } = getMilestoneSeverity(status);

return (
<span
className={clsx(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-mono-xs font-medium",
styles[status]
badgeClasses(tone),
)}
>
{label[status]}
{label}
</span>
);
}

const RECEIPT_BADGE_STYLES: Record<
ReturnType<typeof receiptStatusBadge>["tone"],
string
> = {
green: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-300 dark:border-green-900",
blue: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900",
red: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-300 dark:border-red-900",
amber: "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-900",
neutral: "bg-ink-100 text-ink-700 border-ink-200 dark:bg-ink-900 dark:text-ink-300 dark:border-ink-800",
};

export function TransactionReceiptBadge({ status }: { status: TransactionReceiptStatus }) {
const badge = receiptStatusBadge(status);
const { label, tone } = getReceiptSeverity(status);

return (
<span
className={clsx(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-mono-xs font-medium",
RECEIPT_BADGE_STYLES[badge.tone]
badgeClasses(tone),
)}
>
<span className="mr-1.5 inline-block h-1.5 w-1.5 rounded-full bg-current opacity-80" />
{badge.label}
{label}
</span>
);
}

export function AccountStatusBadge({ status }: { status: "funded" | "unfunded" | "unknown" | "error" | "checking" }) {
const map = {
funded: { label: "Funded", cls: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-300 dark:border-green-900" },
unfunded: { label: "Unfunded", cls: "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-900" },
unknown: { label: "Unknown", cls: "bg-ink-100 text-ink-700 border-ink-200 dark:bg-ink-900 dark:text-ink-300 dark:border-ink-800" },
error: { label: "Error", cls: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-300 dark:border-red-900" },
checking: { label: "Checking…", cls: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900" },
} as const;
const s = map[status];
const severity = getAccountSeverity(status === "checking" ? "unknown" : status);
const label = status === "checking" ? "Checking…" : severity.label;
const tone = severity.tone;

return (
<span
className={clsx(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-mono-xs font-medium",
s.cls
badgeClasses(tone),
)}
>
{s.label}
{label}
</span>
);
}
Expand All @@ -121,14 +89,8 @@ export function Alert({
title?: string;
children: React.ReactNode;
}) {
const map = {
info: "border-stellar-200 bg-stellar-50 text-stellar-800 dark:border-stellar-900 dark:bg-stellar-950/40 dark:text-stellar-200",
warning: "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200",
error: "border-red-200 bg-red-50 text-red-800 dark:border-red-900 dark:bg-red-950/30 dark:text-red-200",
success: "border-anchor-100 bg-anchor-50 text-anchor-700 dark:border-anchor-900 dark:bg-anchor-950/30 dark:text-anchor-200",
} as const;
return (
<div className={clsx("rounded-lg border px-4 py-3 text-sm", map[tone])}>
<div className={clsx("rounded-lg border px-4 py-3 text-sm", alertClasses(tone))}>
{title && <p className="mb-1 font-semibold">{title}</p>}
<div>{children}</div>
</div>
Expand Down
157 changes: 157 additions & 0 deletions docs/STATUS_SEVERITY_MAPPING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Status Severity Mapping

Single source of truth for mapping domain-specific statuses to canonical severity levels, badge tones, headlines, detail messages, recommended actions, and documentation links.

## Overview

Every status in AnchorKit maps to exactly one `StatusSeverity` entry with:

- **Level**: `info` | `success` | `warning` | `blocked` | `error` | `unknown`
- **Tone**: `neutral` | `amber` | `blue` | `green` | `red` (for badge styling)
- **Label**: Short display text for badges
- **Headline**: User-facing headline for alerts
- **Detail**: User-facing detail message
- **Action**: Recommended next action (optional)
- **DocLink**: Path to relevant documentation (optional)

## Severity Levels

| Level | When to Use |
|-------|-------------|
| `info` | Neutral status updates, in-progress states |
| `success` | Completed, confirmed, funded, ready states |
| `warning` | Non-blocking issues, needs attention but not blocking |
| `blocked` | Cannot proceed until action is taken |
| `error` | Failed, disputed, critical issues |
| `unknown` | Status cannot be determined |

## Usage

```typescript
import {
getReceiptSeverity,
getAnchorSeverity,
getReadinessSeverity,
getAccountSeverity,
getMilestoneSeverity,
getStatusSeverity, // unified lookup
badgeClasses,
alertClasses,
} from "@anchorkit/stellar-kit";
```

### Individual Lookup Functions

```typescript
const severity = getReceiptSeverity("confirmed");
// { level: "success", label: "Confirmed", tone: "green", ... }

const badgeClasses = badgeClasses(severity.tone, "default");
// "bg-green-50 text-green-700 border-green-200 ..."
```

### Unified Lookup

```typescript
const severity = getStatusSeverity("receipt", "confirmed");
// Returns StatusSeverity or null if domain/status unknown
```

### Badge Components

```tsx
import { badgeClasses, getReceiptSeverity } from "@anchorkit/stellar-kit";

function StatusBadge({ status }) {
const { label, tone } = getReceiptSeverity(status);
return (
<span className={badgeClasses(tone)}>
{label}
</span>
);
}
```

### Alert Components

```tsx
import { alertClasses } from "@anchorkit/stellar-kit";

function StatusAlert({ level, title, children }) {
return (
<div className={alertClasses(level)}>
<h3>{title}</h3>
{children}
</div>
);
}
```

## Domain Mappings

### Receipt Statuses

| Status | Level | Tone | Action |
|--------|-------|------|--------|
| `confirmed` | success | green | none |
| `pending` | info | blue | wait |
| `failed` | error | red | retry |
| `rejected` | warning | amber | check_explorer |
| `unknown` | unknown | neutral | check_explorer |

### Anchor Statuses

| Status | Level | Tone | Action |
|--------|-------|------|--------|
| `pending_user` | warning | amber | review_details |
| `pending_anchor` | info | blue | wait |
| `pending_stellar` | info | blue | wait |
| `completed` | success | green | none |
| `failed` | error | red | retry |
| `refunded` | warning | amber | check_explorer |

### Readiness States

| State | Level | Tone | Action |
|-------|-------|------|--------|
| `ready` | success | green | none |
| `warnings` | warning | amber | review_details |
| `unsafe-network` | blocked | red | enable_mainnet |
| `blocked` | blocked | red | review_details |

### Account Statuses

| Status | Level | Tone | Action |
|--------|-------|------|--------|
| `funded` | success | green | none |
| `unfunded` | warning | amber | fund_account |
| `unknown` | unknown | neutral | retry |
| `error` | error | red | contact_support |

### Milestone Statuses

| Status | Level | Tone | Action |
|--------|-------|------|--------|
| `draft` | info | neutral | none |
| `active` | info | blue | none |
| `evidence_submitted` | info | blue | wait |
| `approved` | success | green | none |
| `disputed` | error | red | contact_support |
| `ready_for_release` | success | green | none |
| `released` | success | green | none |

## Adding New Statuses

1. Add the status type to `packages/types/src/index.ts`
2. Add severity mapping to `packages/stellar-kit/src/severity.ts`
3. Add test fixtures to `packages/stellar-kit/test/fixtures/severity.ts`
4. Update tests in `packages/stellar-kit/test/severity.test.ts`
5. Update this documentation

## Design Principles

1. **Single source of truth**: One mapping per status, consumed by all UI components
2. **Exhaustive coverage**: Every domain status must have a mapping
3. **Consistent semantics**: Same tone always means the same thing
4. **Actionable guidance**: Every status should suggest a user action
5. **Documentation links**: Link to relevant docs when available
1 change: 1 addition & 0 deletions packages/stellar-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export * from "./receipt";
export * from "./balances";
export * from "./diagnostics";
export * from "./assetRegistry";
export * from "./severity";
export type { StellarKeypair } from "@anchorkit/types";
Loading