diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ac9821..a1aa5803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,75 @@ name: CI + 'on': push: branches: - main pull_request: null + +# Cancel in-progress runs for the same PR / branch when a new commit is pushed. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: '20' + jobs: + # ───────────────────────────────────────────────────────────────────── + # Quality Gate — lint, type-check, unit tests, build + # ───────────────────────────────────────────────────────────────────── quality-gate: name: Quality Gate runs-on: ubuntu-latest steps: - - name: "Quality Gate (paused \u2014 CI stabilization in progress)" - run: echo 'Quality Gate check intentionally stubbed while main-branch CI is being stabilized.' + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Lint + run: npm run lint + + - name: Type-check + run: npx tsc -b --noEmit + + - name: Test + run: npm test + + - name: Build + run: npm run build + + # ───────────────────────────────────────────────────────────────────── + # Lighthouse CI — performance, accessibility, best-practices audit + # ───────────────────────────────────────────────────────────────────── + lighthouse: + name: Lighthouse CI + needs: quality-gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Build + run: npm run build + + - name: Run Lighthouse CI + run: npx lhci autorun --config=./lighthouserc.json + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 25b511f3..c93ed73e 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ See the [docs/](docs/) directory for detailed project documentation, including: - [Security Checklist](docs/SECURITY_CHECKLIST_FRONTEND.md) — CSP, storage, third-party scripts, and dependency posture for contributors. - [API Client Policies](docs/API_CLIENT_POLICIES.md) — Interceptors, retry policy, and error taxonomy for the API client. - [Cookie-Secret Rotation Runbook](docs/COOKIE_SECRETS.md) — Rotation cadence, blast radius, and step-by-step procedure for backend session/CSRF cookie secrets. +- [Authentication Flows](docs/AUTH_FLOWS.md) — Sequence diagrams for login, logout, and session refresh flows. - [Hooks & Utilities Reference](docs/HOOKS.md) — Catalog of reusable hooks (`src/hooks/`) and helpers (`src/lib/`) with signatures and usage. - [Offline Strategy](docs/PWA.md) — What's cached, what's queued, and what happens on cache miss. - [Bundle Size Baseline](docs/BUNDLE.md) — Current production bundle sizes, per-route breakdowns, and profiling guide. diff --git a/docs/AUTH_FLOWS.md b/docs/AUTH_FLOWS.md new file mode 100644 index 00000000..fe9f3b8b Binary files /dev/null and b/docs/AUTH_FLOWS.md differ diff --git a/docs/README.md b/docs/README.md index cc55883b..2fea2a29 100644 --- a/docs/README.md +++ b/docs/README.md @@ -121,7 +121,12 @@ This directory contains comprehensive design specifications and implementation g - Connection state machine and UX contract for connection/network states - Usage guide and network mismatch handling -16. **[Security Checklist](./SECURITY_CHECKLIST_FRONTEND.md)** +16. [Authentication Flows](./AUTH_FLOWS.md) ⭐ NEW + - Sequence diagrams for login (connect wallet), logout (disconnect), and session refresh (re-authentication) + - Session timeout (inactivity logout) flow + - Key code paths and cross-references + +17. **[Security Checklist](./SECURITY_CHECKLIST_FRONTEND.md)** - CSP policy, browser storage rules, third-party script posture, and dependency audit workflow - Concrete review checklist for each security area **[Settings Auto-Save Indicator](./auto-save.md)** ⭐ NEW (closes #564) @@ -130,30 +135,30 @@ This directory contains comprehensive design specifications and implementation g - `` token-driven pill showing `Saving…` / `Saved just now` / "Couldn't save" with retry. - In-flight cancellation via `AbortController` so stale PATCHes can't overwrite newer state. -16. **[Widget Cache & Per-Widget Refresh](./widget-cache.md)** ⭐ NEW (closes #561) +18. **[Widget Cache & Per-Widget Refresh](./widget-cache.md)** ⭐ NEW (closes #561) - Shared in-app cache for dashboard widgets so a refresh button on one card only invalidates that card's key — others keep their state. - `useWidgetCache` hook + `` + token-driven styling. - Coverage includes mount, key isolation, error surfacing, and reduced-motion. -17. **[API Client Policies](./API_CLIENT_POLICIES.md)** ⭐ NEW +19. **[API Client Policies](./API_CLIENT_POLICIES.md)** ⭐ NEW - Interceptors, retry policy, and error taxonomy for the API client - `ApiError` structure and usage examples -18. **[Bundle Size Baseline](./BUNDLE.md)** ⭐ NEW +20. **[Bundle Size Baseline](./BUNDLE.md)** ⭐ NEW - Current production bundle size estimates and per-route breakdowns - Top 10 heaviest dependencies ranked by gzipped size - How to profile and compare bundle sizes with Vite, rollup-plugin-visualizer, and size-limit - Contributor guidelines for keeping the bundle lean -19. **[Telemetry & Analytics](./telemetry.md)** +21. **[Telemetry & Analytics](./telemetry.md)** - Privacy-first approach (no telemetry collected) - No PII handling or third-party analytics -20. **[Offline Strategy](./PWA.md)** +22. **[Offline Strategy](./PWA.md)** - What's cached (localStorage keys, in-memory widget cache), what's queued (pending transactions, auto-save retry), and what happens on cache miss - Offline-aware hooks (`useQuery`, `useWidgetCache`), offline banner, install prompt behaviour -21. **[First Bond Coach Marks](./uiux/onboarding-coachmarks-first-bond.md)** +23. **[First Bond Coach Marks](./uiux/onboarding-coachmarks-first-bond.md)** - First-run onboarding concept for creating a bond - Coach mark placement, copy, sequencing, and dismissal behavior - Accessibility, responsive behavior, and visual QA checklist diff --git a/docs/components.md b/docs/components.md deleted file mode 100644 index 4416fa88..00000000 --- a/docs/components.md +++ /dev/null @@ -1,681 +0,0 @@ -# Shared Components Catalog - -This catalog is the source-facing reference for shared UI under `src/components/`. It documents current TypeScript props, accessibility contracts, styling ownership, and the `--credence-*` design tokens each component consumes. Keep this page in sync whenever component props or CSS tokens change. - -Related focused docs: [button system](./button-system.md), [notifications](./notifications.md), [design tokens](./DESIGN_TOKENS.md), [dark mode](./dark-mode.md), [focus patterns](./focus-patterns.md), [UI states](./UI_STATES_GUIDE.md), [forms & inputs](./FORMS_AND_INPUTS.md), [TrustGauge quick reference](./TRUST_GAUGE_QUICK_REFERENCE.md), and [tier thresholds](./tier-thresholds.md). - -**Storybook**: Components that have stories are listed with their Storybook path and variant names. Run `npm run storybook` (defaults to port 6006) to browse and interact with them. Components without a Storybook entry have no story file yet. - -## Styling ownership snapshot - -| Component | Styling owner | Inline-style migration note | -| ----------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| BottomNav | `src/components/navigation/BottomNav.css` | None. | -| Progress | `src/components/Progress.css` | `color` prop for colour variants (`primary`, `success`, `warning`, `danger`). | -| Button | `src/components/Button.css` | None. | -| Badge | `src/components/Badge.css` | None. | -| Banner | `src/components/Banner.css` | None. | -| Toast / ToastProvider | `src/components/Toast.css` | None. | -| ConfirmDialog | `src/components/ConfirmDialog.css` | None. | -| AddressInput | `src/components/AddressInput.css` + `FormField.css` | None. | -| AmountInput | `src/components/AmountInput.css` | None. | -| TrustGauge | `src/components/TrustGauge.css` | Uses inline CSS custom properties for dynamic progress, marker, thumb, and legend-dot colors; keep scoped until migrated. | -| TierLadder | `src/components/TierLadder.css` + `Badge.css` | None. | -| ActivityTimeline | `src/components/ActivityTimeline.css` + EmptyState inline styles for empty fallback | Empty fallback inherits `EmptyState` inline styles; migrate with states components. | -| WindowedList | `src/components/WindowedList.tsx` | Lightweight helper for large lists; uses the shared threshold in `src/config/listing.ts` and preserves the existing list semantics. | -| FormField | `src/components/forms/FormField.css` | None. | -| forms/Input | `src/components/forms/Input.css` | None. | -| forms/Textarea | `src/components/forms/Input.css` | Shares Input.css. | -| controls/Select | `src/components/controls/controls.css` | None. | -| controls/Toggle | `src/components/controls/controls.css` | None. | -| states/EmptyState | Inline styles in `src/components/states/EmptyState.tsx` | Owns inline styles and should be migrated to CSS. | -| states/ErrorState | Inline styles in `src/components/states/ErrorState.tsx` | Owns inline styles and should be migrated to CSS. | -| states/LoadingSkeleton | Inline styles in `src/components/states/LoadingSkeleton.tsx` | Owns inline styles and should be migrated to CSS. | -| SessionTimeoutDialog | Inline styles in `src/components/SessionTimeoutDialog.tsx` | Uses `ConfirmDialog` primitive with internal warning styles. | -| ActionCard | Inline styles in `src/components/ActionCard.tsx` | Owns all inline styles; migrate to a CSS file when a module is added. | -| VirtualizedList | `src/components/VirtualizedList.tsx` | No dedicated CSS file; uses consumer-provided layout and spacing. | -| Disclaimer | `src/components/Disclaimer.css` | None. | -| ThemeToggle | `src/components/ThemeToggle.css` | None. | -| Kbd | `src/components/Kbd.css` | None. | -| KeyboardShortcutsDialog | `src/components/KeyboardShortcutsDialog.css` | None. | -| AttestationForm | Delegates to `AddressInput`, `Select`, `FormField`, `Button` | No dedicated CSS file; inherits from composing components. | -| CreateBondFlow | `src/components/CreateBondFlow.css` | None. | -| ErrorBoundary | Delegates to `states/ErrorState` | No dedicated CSS file. | -| RepoAvatar | `src/components/RepoAvatar.css` | None. | - -## Shared vocabularies - -### `BadgeVariant` - -Source: [`Badge.tsx`](../src/components/Badge.tsx) - -`'bronze' | 'silver' | 'gold' | 'platinum' | 'active' | 'locked' | 'slashed' | 'grace-period' | 'unknown'` - -Unknown runtime strings normalize to the `unknown` visual style while preserving the supplied string as a fallback label only when no known label exists. - -### `BannerSeverity` - -Source: [`Banner.tsx`](../src/components/Banner.tsx) - -`'info' | 'success' | 'warning' | 'critical'` - -`warning` and `critical` render urgent `role="alert"`; `info` and `success` render `role="status"`. - -### `ToastSeverity` - -Source: [`Toast.tsx`](../src/components/Toast.tsx) and [`ToastProvider.tsx`](../src/components/ToastProvider.tsx) - -`'info' | 'success' | 'warning' | 'danger'` - -Default auto-dismiss timeouts are 5s for `info` and `success`, 8s for `warning`, and persistent for `danger` unless settings override auto-dismiss. - -### `TIER_CONFIG` - -Source: [`TrustGauge.tsx`](../src/components/TrustGauge.tsx) - -| Tier | Range | Label | Tokens referenced by config | -| ---------- | -------- | -------- | --------------------------------------------------------------------------------------------------------- | -| `bronze` | 0-250 | Bronze | `--credence-color-bronze-border`, `--credence-color-bronze-surface`, `--credence-color-bronze-text` | -| `silver` | 250-500 | Silver | `--credence-color-silver-border`, `--credence-color-silver-surface`, `--credence-color-silver-text` | -| `gold` | 500-750 | Gold | `--credence-color-gold-border`, `--credence-color-gold-surface`, `--credence-color-gold-text` | -| `platinum` | 750-1000 | Platinum | `--credence-color-platinum-border`, `--credence-color-platinum-surface`, `--credence-color-platinum-text` | - -## WindowedList - -Source: [`src/components/WindowedList.tsx`](../src/components/WindowedList.tsx). - -| Prop | Type | Default | -| ----------------- | ---------------------------------------------- | ----------- | -| `items` | `readonly T[]` | Required | -| `itemHeight` | `number` | Required | -| `overscan` | `number` | `4` | -| `renderItem` | `(item: T, index: number) => ReactNode` | Required | -| `className` | `string` | `undefined` | -| `role` | `string` | `undefined` | -| `ariaLabel` | `string` | `undefined` | -| `emptyMessage` | `string` | `undefined` | -| `getItemKey` | `(item: T, index: number) => string \| number` | `undefined` | -| `containerHeight` | `number` | `320` | - -The component uses the shared `LONG_LIST_RENDER_THRESHOLD` from [`src/config/listing.ts`](../src/config/listing.ts) to switch into a windowed render path for large data sets. It preserves the existing DOM structure for shorter lists, uses the supplied `containerHeight` for the scroll viewport, and keeps the rendered content accessible for larger lists. - -## Button - -Source: [`src/components/Button.tsx`](../src/components/Button.tsx). Focused docs: [button system](./button-system.md). - -| Prop | Type | Default | -| ------------------- | ------------------------------------------------- | ---------------------------------------- | -| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger'` | `'primary'` | -| `isLoading` | `boolean` | `false` | -| `fullWidth` | `boolean` | `false` | -| `children` | `ReactNode` | Required | -| Native button props | `ButtonHTMLAttributes` | Forwarded; `type` defaults to `'button'` | - -Accessibility: renders a native ` -``` - -## PinWidgetButton - -Toggle button rendered on each dashboard widget card, allowing the user -to pin/unpin it to the top row. Pinned state persists in `localStorage` -under `credence:pinnedWidgets` (see `src/config/pinnedWidgets.ts`), capped -at `MAX_PINNED_WIDGETS`. - -**Props** - -- `slug: string` — widget identifier -- `isPinned: boolean` -- `onToggle: (slug: string) => void` - -**Styling**: uses `--spacing-xs`, `--radius-sm`, `--color-text-secondary`, -`--color-bg-hover`, `--color-focus-ring` design tokens. No hard-coded values. - -**Accessibility**: `aria-pressed` reflects pin state; `aria-label` announces -the pin/unpin action. - -## Badge - -Source: [`src/components/Badge.tsx`](../src/components/Badge.tsx). - -| Prop | Type | Default | -| ----------- | ------------------------ | ------------------- | -| `variant` | `BadgeVariant \| string` | Required | -| `label` | `string` | Known variant label | -| `className` | `string` | `''` | - -Accessibility: renders text in a ``; consumers should provide surrounding context when the badge alone is not descriptive. - -Tokens: tier/status color tokens, `--credence-font-size-xs`, `--credence-font-weight-semibold`, `--credence-radius-full`, `--credence-space-2`. - -```tsx - - -``` - -## Banner - -Source: [`src/components/Banner.tsx`](../src/components/Banner.tsx). Focused docs: [notifications](./notifications.md). - -| Prop | Type | Default | -| ---------------- | -------------------------------------------------------- | ------------------------ | -| `severity` | `BannerSeverity` | Required | -| `children` | `ReactNode` | Required | -| `title` | `string` | `undefined` | -| `dismissible` | `boolean` | `undefined` | -| `onDismiss` | `() => void` | `undefined` | -| `action` | `{ label: string; href?: string; onClick?: () => void }` | `undefined` | -| `returnFocusRef` | `React.RefObject` | `document.body` fallback | - -Accessibility: severity maps to `role="alert"` for warning/critical and `role="status"` for info/success. The root has an aria label such as "Warning banner". Dismiss buttons have `aria-label="Dismiss banner"`, support Escape while focused, and return focus to `returnFocusRef` or `document.body` after dismissal. Icons are aria-hidden. - -Tokens: motion duration/easing tokens in CSS; severity color styling is component-owned CSS values and should be reviewed during token migrations. - -```tsx - - Your bond evidence needs one more attestation. - -``` - -## VirtualizedList - -Source: [`src/components/VirtualizedList.tsx`](../src/components/VirtualizedList.tsx). - -| Prop | Type | Default | -| --------------------- | ---------------------------------------------- | ----------- | -| `items` | `T[]` | Required | -| `itemHeight` | `number` | `64` | -| `overscan` | `number` | `3` | -| `virtualizeThreshold` | `number` | `1000` | -| `getKey` | `(item: T, index: number) => string \| number` | Required | -| `renderItem` | `(item: T, index: number) => ReactNode` | Required | -| `height` | `number` | `320` | -| `emptyMessage` | `ReactNode` | `undefined` | - -The component renders only the visible window of a large list when the item count exceeds the configured threshold. It is used in the command launcher to keep search results responsive for very large result sets while preserving scroll behavior and keyboard access. - -## Toast and ToastProvider - -Sources: [`src/components/Toast.tsx`](../src/components/Toast.tsx), [`src/components/ToastProvider.tsx`](../src/components/ToastProvider.tsx). Focused docs: [notifications](./notifications.md). - -### Toast props - -| Prop | Type | Default | -| ----------- | ---------------------------------------------------------- | -------- | -| `toast` | `{ id: string; severity: ToastSeverity; message: string }` | Required | -| `onDismiss` | `(id: string) => void` | Required | - -### ToastProvider API - -| API | Type | Default | -| ---------------------------- | ---------------------------------------------------- | ------------- | -| `children` prop | `ReactNode` | Required | -| `useToast().addToast` | `(severity: ToastSeverity, message: string) => void` | Context value | -| `useToast().removeToast` | `(id: string) => void` | Context value | -| `useToast().removeAllToasts` | `() => void` | Context value | - -Accessibility: individual danger toasts use `role="alert"`; other severities use `role="status"`. Provider separates polite notifications into `aria-live="polite"` and danger notifications into `aria-live="assertive"` regions, each with a region label. Dismiss buttons have severity-specific accessible names. - -Tokens: `--credence-font-size-*`, `--credence-line-height-base`, motion duration/easing, `--credence-radius-md`, `--credence-shadow-toast`, spacing, `--credence-surface-card`, `--credence-text-primary`. - -```tsx -function SaveButton() { - const { addToast } = useToast() - return -} -``` - -## ConfirmDialog - -Source: [`src/components/ConfirmDialog.tsx`](../src/components/ConfirmDialog.tsx). Focused docs: [focus patterns](./focus-patterns.md). - -| Prop | Type | Default | -| ------------------- | -------------------------------- | --------------------------------- | -| `open` | `boolean` | Required | -| `title` | `string` | Required | -| `subtitle` | `string` | `undefined` | -| `breakdown` | `ConfirmDialogPenaltyBreakdown` | `undefined` | -| `description` | `React.ReactNode` | `undefined` | -| `children` | `React.ReactNode` | `undefined` | -| `onConfirm` | `() => void` | Required | -| `onCancel` | `() => void` | Required | -| `returnFocusRef` | `RefObject` | `undefined` | -| `confirmLabel` | `string` | `'Withdraw bond'` | -| `confirmInputLabel` | `React.ReactNode` | `undefined` | -| `confirmInputHint` | `React.ReactNode` | `undefined` | -| `variant` | `'danger' \| 'info'` | `'danger'` | -| `confirmPhrase` | `string` | `'CONFIRM'` | -| `confirmHint` | `string` | Wallet/funds irreversibility hint | - -`ConfirmDialogPenaltyBreakdown` is `{ bondAmount: string; penaltyAmount: string; penaltyPercent: number; resultingBalance: string }`. When `breakdown` is omitted, the `description` prop or `children` slot is rendered in its place. - -Accessibility: renders in a portal with `role="dialog"`, `aria-modal="true"`, generated `aria-labelledby`/`aria-describedby`, focus trap, initial focus on Cancel, Escape and backdrop cancellation, body scroll lock, and optional focus restoration. The confirm button is disabled until the user types the value of `confirmPhrase` (default: `CONFIRM`); assertive sr-only announcements describe state changes. - -Tokens: danger color tokens, font family/size/weight, line-height, motion, radius, spacing, surface, and text tokens. - -```tsx - -``` - -## AddressInput - -Source: [`src/components/AddressInput.tsx`](../src/components/AddressInput.tsx). - -| Prop | Type | Default | -| -------------------- | ---------------------------- | ------------------- | -| `id` | `string` | Required | -| `label` | `string` | `'Stellar Address'` | -| `value` | `string` | Required | -| `onChange` | `(value: string) => void` | Required | -| `onValidationChange` | `(isValid: boolean) => void` | `undefined` | -| `disabled` | `boolean` | `false` | -| `className` | `string` | `''` | - -Accessibility: composes `FormField`, so label, hint, and error IDs wire through `htmlFor`, `aria-describedby`, and `aria-invalid`. Paste and copy controls are native buttons with explicit aria labels and hidden SVGs. Validation requires a 56-character Stellar public key starting with `G`; invalid feedback is exposed by the FormField alert. - -Tokens: border, danger, primary, slate, success, focus, font, line-height, motion, radius, spacing, surface, and text tokens. - -```tsx - -``` - -Storybook: `Components/Forms/AddressInput` — **Default** · **Filled** · **Invalid** · **Disabled** · **Loading**. - -## AmountInput - -Source: [`src/components/AmountInput.tsx`](../src/components/AmountInput.tsx). Focused docs: [USDC amount input](./uiux/usdc-amount-input.md). - -| Prop | Type | Default | -| ------------------ | ----------------------------------------------------------------------------------- | ------------------ | -| `value` | `string` | Required | -| `onChange` | `(value: string) => void` | Required | -| `balance` | `number` | Required | -| `presets` | `number[]` | `[100, 500, 1000]` | -| `currencyLabel` | `string` | `'USDC'` | -| `error` | `string` | `undefined` | -| Native input props | `Omit, 'value' \| 'onChange' \| 'inputMode'>` | Forwarded | - -Accessibility: uses a native input with `inputMode="decimal"`, disables browser autocomplete, exposes invalid state when `error` or `aria-invalid="true"` is supplied, hides the currency adornment, and gives Max/preset buttons descriptive aria labels. Presets above balance and Max at zero balance are disabled. - -Tokens: border, danger-border, slate, focus, font, motion, radius, spacing, surface, and text tokens. - -```tsx - -``` - -Storybook: `Components/Forms/AmountInput` — **Default** · **Filled** · **OverBalance** · **Error** · **Disabled** · **Loading**. - -## TrustGauge - -Source: [`src/components/TrustGauge.tsx`](../src/components/TrustGauge.tsx). Focused docs: [TrustGauge quick reference](./TRUST_GAUGE_QUICK_REFERENCE.md), [accessibility report](./TRUST_GAUGE_ACCESSIBILITY_REPORT.md). - -| Prop | Type | Default | -| ----------- | ---------------------------------------------- | --------------- | -| `score` | `number` | Required | -| `tier` | `'bronze' \| 'silver' \| 'gold' \| 'platinum'` | Required | -| `className` | `string` | `''` | -| `id` | `string` | `'trust-gauge'` | - -Accessibility: includes visible heading/description, a `role="progressbar"` with `aria-valuenow`, `aria-valuemin="0"`, `aria-valuemax="1000"`, and an aria label summarizing score and tier. Decorative fills, thumb, and legend dots are presentational or aria-hidden. - -Tokens: tier color tokens, `--credence-color-primary`, slate, focus, font, line-height, motion, radius, and spacing tokens. Dynamic inline CSS custom properties set progress width, marker position, thumb position, and legend-dot color. - -```tsx - -``` - -## TierLadder - -Source: [`src/components/TierLadder.tsx`](../src/components/TierLadder.tsx). Focused docs: [tier thresholds](./tier-thresholds.md). - -| Prop | Type | Default | -| ------------- | --------- | ------- | -| `className` | `string` | `''` | -| `defaultOpen` | `boolean` | `false` | - -Accessibility: root section is labelled by an sr-only heading. Trigger is a native button with `aria-expanded` and `aria-controls`; the panel uses `hidden` when collapsed. Decorative rail and chevron are aria-hidden. Tier content is structured as an ordered list with nested headings and benefit lists. - -Tokens: border, tier color tokens, slate, focus, font, line-height, motion, radius, spacing, surface, and text tokens. - -```tsx - -``` - -## ActivityTimeline - -Source: [`src/components/ActivityTimeline.tsx`](../src/components/ActivityTimeline.tsx). Focused docs: [activity surface concept](./ACTIVITY_SURFACE_CONCEPT.md). - -| Prop | Type | Default | -| --------- | ---------------- | ---------------------- | -| `compact` | `boolean` | `false` | -| `items` | `ActivityItem[]` | Built-in sample events | - -`ActivityItem` is `{ id: string; timestamp: string; title: string; description: string; actor: string; statusLabel: string; tone: 'success' | 'warning' | 'info'; meta: string }`. - -Accessibility: renders a labelled section and a labelled timeline list. Decorative rails/nodes are aria-hidden. Empty data delegates to `EmptyState` with activity illustration and explanatory copy. - -Tokens: border, info/success/warning color tokens, primary, font, line-height, radius, spacing, surface, and text tokens. - -```tsx - -``` - -## FormField - -Source: [`src/components/forms/FormField.tsx`](../src/components/forms/FormField.tsx). - -| Prop | Type | Default | -| ------------- | -------------------- | ----------- | -| `id` | `string` | Required | -| `label` | `string` | Required | -| `hint` | `string` | `undefined` | -| `error` | `string` | `undefined` | -| `success` | `string` | `undefined` | -| `srOnlyLabel` | `boolean` | `false` | -| `required` | `boolean` | `false` | -| `className` | `string` | `undefined` | -| `children` | `React.ReactElement` | Required | - -Accessibility: - -- Renders `