diff --git a/content/tools/uikit/architecture.mdx b/content/tools/uikit/architecture.mdx new file mode 100644 index 00000000..1f888c2a --- /dev/null +++ b/content/tools/uikit/architecture.mdx @@ -0,0 +1,220 @@ +--- +title: Architecture +--- + +OpenZeppelin UIKit is built as a layered stack of independently installable packages. This page explains how those layers fit together, how the capability-driven adapter model works, and how runtimes manage lifecycle across multiple ecosystems. + +## Package Layers + +The packages form a dependency chain where each layer builds on the ones below it. Lower layers are lighter and more generic; higher layers add React-specific and domain-specific behavior. + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 40}} }%% +flowchart TD + Storage(["7 · ui-storage"]) + Renderer(["6 · ui-renderer"]) + ReactPkg(["5 · ui-react"]) + Components(["4 · ui-components"]) + Styles(["3 · ui-styles"]) + Utils(["2 · ui-utils"]) + Types(["1 · ui-types"]) + + Storage --> Utils + Renderer --> Components + ReactPkg --> Components + Components --> Styles + Components --> Utils + Utils --> Types + + style Storage fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style Renderer fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style ReactPkg fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style Components fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Styles fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Utils fill:#fff3e0,stroke:#ff9800,color:#e65100 + style Types fill:#fff3e0,stroke:#ff9800,color:#e65100 +``` + +**Color key:** Application layers (5–7) · UI & design (3–4) · Foundation (1–2) + +| Layer | Package | Responsibility | +| --- | --- | --- | +| 1 | `@openzeppelin/ui-types` | TypeScript interfaces for capabilities, schemas, form models, networks, transactions, and execution config. No runtime code — pure type definitions. | +| 2 | `@openzeppelin/ui-utils` | Framework-agnostic helpers: `AppConfigService` for environment/config loading, structured logger, validation utilities, and routing helpers. | +| 3 | `@openzeppelin/ui-styles` | Tailwind CSS 4 theme tokens using OKLCH color space. Ships CSS variables and custom variants (dark mode). No JavaScript. | +| 4 | `@openzeppelin/ui-components` | React UI primitives (buttons, dialogs, cards, tabs) and blockchain-aware form fields (address, amount, bytes, enum, map). Built on Radix UI + shadcn/ui patterns. | +| 5 | `@openzeppelin/ui-react` | `RuntimeProvider` for managing `EcosystemRuntime` instances per network. `WalletStateProvider` for global wallet state. Derived hooks for cross-ecosystem wallet abstraction. | +| 6 | `@openzeppelin/ui-renderer` | `TransactionForm` for schema-driven transaction forms. `ContractStateWidget` for view function queries. `ExecutionConfigDisplay`, `AddressBookWidget`, address book components. | +| 7 | `@openzeppelin/ui-storage` | `EntityStorage` and `KeyValueStorage` base classes on Dexie.js/IndexedDB. Account alias plugin for address-to-name mapping. | + + +## Capabilities + +The UIKit type system defines 13 **capabilities** — small, focused interfaces that describe what an adapter can do. + +Capabilities are organized into three tiers based on their requirements: + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 30}} }%% +flowchart TD + T1["Tier 1 — Lightweight
Addressing · Explorer · NetworkCatalog · UiLabels"] + T2["Tier 2 — Network-Aware
ContractLoading · Schema · TypeMapping · Query"] + T3["Tier 3 — Stateful
Execution · Wallet · UiKit · Relayer · AccessControl"] + + T1 --"may import"--> T2 --"may import"--> T3 + + style T1 fill:#e8eaf6,stroke:#5c6bc0,color:#000 + style T2 fill:#e0f2f1,stroke:#26a69a,color:#000 + style T3 fill:#fff3e0,stroke:#ff9800,color:#000 +``` + +**Tier 1** requires no runtime context — safe to import anywhere. **Tier 2** needs a `networkConfig`. **Tier 3** additionally needs wallet state and participates in the `dispose()` lifecycle. Each higher tier may import from lower tiers, but never the reverse. + +| Capability | Tier | Purpose | +| --- | --- | --- | +| `Addressing` | 1 | Address validation, formatting, checksumming | +| `Explorer` | 1 | Block explorer URL generation | +| `NetworkCatalog` | 1 | Available network listing and metadata | +| `UiLabels` | 1 | Human-readable labels for ecosystem-specific terms | +| `ContractLoading` | 2 | Fetch and parse contract ABIs/IDLs | +| `Schema` | 2 | Transform contract definitions into form-renderable schemas | +| `TypeMapping` | 2 | Map blockchain types (e.g. `uint256`) to form field types | +| `Query` | 2 | Execute read-only contract calls (view functions) | +| `Execution` | 3 | Sign, broadcast, and track transactions | +| `Wallet` | 3 | Connect/disconnect wallets, account state, chain switching | +| `UiKit` | 3 | Ecosystem-specific React components and hooks | +| `Relayer` | 3 | Gas-sponsored transaction execution via relayers | +| `AccessControl` | 3 | Role-based access control queries and snapshots | + +For more background on the adapter pattern and how chain-specific integrations are structured, see [Building New Adapters](/ui-builder/building-adapters). + +### Capability Bundles + +Higher-level components request specific **bundles** of capabilities rather than the full set. For example, `TransactionForm` expects a `TransactionFormCapabilities` type — an intersection of the capabilities needed for form rendering, execution, and status tracking. + +This means you can pass a partial adapter that only implements what the component actually needs. + + +## Runtimes and Profiles + +### Ecosystem Runtimes + +An `EcosystemRuntime` is a live instance that bundles capabilities for a specific network. Capabilities created within the same runtime share runtime-scoped state (network config, wallet connection, caches) and are disposed together. + +Runtimes are created by ecosystem adapter packages: + +```tsx +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +const runtime = await ecosystemDefinition.createRuntime( + 'composer', // profile name + ethereumMainnetConfig // network config +); + +// Access capabilities from the runtime +const address = runtime.addressing.formatAddress('0x...'); +const schema = await runtime.schema.generateFormSchema(contractDef); +const txHash = await runtime.execution.signAndBroadcast(txData, execConfig); + +// Clean up when done +runtime.dispose(); +``` + +### Profiles + +Adapters support five standard profiles that define which capabilities are included: + +| Profile | Use Case | Tier 1 | Tier 2 | Tier 3 | +| --- | --- | --- | --- | --- | +| `declarative` | Address formatting, explorer links | ✓ | — | — | +| `viewer` | Read contract state, no wallet needed | ✓ | ✓ | — | +| `transactor` | Execute transactions, basic wallet | ✓ | ✓ | Execution, Wallet | +| `composer` | Full UI with form rendering | ✓ | ✓ | ✓ (most) | +| `operator` | Administrative tools, access control | ✓ | ✓ | ✓ (all) | + +Choose the lightest profile that fits your use case. A dashboard that only displays contract state can use `viewer`; a full transaction builder should use `composer` or `operator`. + +For more background on the adapter architecture, see [Building New Adapters](/ui-builder/building-adapters). + +### Runtime Lifecycle + +```mermaid +sequenceDiagram + participant App as Application + participant RP as RuntimeProvider + participant Adapter as Ecosystem Adapter + + App->>RP: Mount with resolveRuntime + App->>RP: setActiveNetworkId("ethereum-mainnet") + RP->>Adapter: createRuntime("composer", networkConfig) + Adapter-->>RP: EcosystemRuntime + + Note over RP: Cached by network ID + + App->>RP: setActiveNetworkId("polygon-mainnet") + RP->>Adapter: createRuntime("composer", polygonConfig) + Adapter-->>RP: New EcosystemRuntime + + Note over RP: Both runtimes cached + + App->>RP: Unmount + RP->>RP: dispose() all cached runtimes +``` + +`RuntimeProvider` maintains a **per-network-id registry** of runtimes. When a network is selected for the first time, the runtime is created asynchronously and cached. On unmount, all runtimes are disposed, releasing any wallet connections, subscriptions, or internal state. + +## Execution Strategies + +The execution system supports multiple ways to submit a transaction. The adapter selects the appropriate strategy based on the `ExecutionConfig`: + +```mermaid +flowchart LR + Form["TransactionForm"] --> Exec["ExecutionCapability"] + Exec --> Strategy{"Method?"} + Strategy -->|EOA| EOA["Direct wallet signing"] + Strategy -->|Relayer| Relay["Gas-sponsored via Relayer"] + EOA --> Chain["Blockchain"] + Relay --> Chain +``` + +Each ecosystem adapter defines which execution methods it supports. The EVM adapter supports both EOA and Relayer; other ecosystems may support only EOA. + +## How It Connects + +Putting it all together, here is how a typical application uses UIKit with an Ecosystem Adapter: + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 40}} }%% +flowchart TD + App(["Your React App"]) + RP(["RuntimeProvider"]) + WSP(["WalletStateProvider"]) + TF(["TransactionForm"]) + CSW(["ContractStateWidget"]) + AdapterEVM(["adapter-evm"]) + AdapterStellar(["adapter-stellar"]) + Ethereum(["Ethereum"]) + Stellar(["Stellar"]) + + App --> RP --> WSP + WSP --> TF & CSW + RP -.->|createRuntime| AdapterEVM & AdapterStellar + AdapterEVM --> Ethereum + AdapterStellar --> Stellar + + style App fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style RP fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style WSP fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style TF fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style CSW fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style AdapterEVM fill:#fff3e0,stroke:#ff9800,color:#e65100 + style AdapterStellar fill:#fff3e0,stroke:#ff9800,color:#e65100 + style Ethereum fill:#fce4ec,stroke:#e91e63,color:#880e4f + style Stellar fill:#fce4ec,stroke:#e91e63,color:#880e4f +``` + +## Next Steps + +- [Components](/tools/uikit/components) — Explore all available UI primitives and form fields +- [React Integration](/tools/uikit/react-integration) — Deep dive into providers, hooks, and wallet state +- [Building New Adapters](/ui-builder/building-adapters) — Background on the adapter pattern and ecosystem integrations diff --git a/content/tools/uikit/components.mdx b/content/tools/uikit/components.mdx new file mode 100644 index 00000000..f8044682 --- /dev/null +++ b/content/tools/uikit/components.mdx @@ -0,0 +1,267 @@ +--- +title: Components +--- + +OpenZeppelin UIKit ships two categories of components: **UI primitives** from `@openzeppelin/ui-components` and **renderer widgets** from `@openzeppelin/ui-renderer`. This page catalogs both and shows how to use them. + +## UI Primitives + +`@openzeppelin/ui-components` provides foundational React components built on [Radix UI](https://www.radix-ui.com/) and [shadcn/ui](https://ui.shadcn.com/) patterns. All components are styled with Tailwind CSS and support dark mode. + +### General Components + +| Component | Description | +| --- | --- | +| `Button` / `LoadingButton` | Action buttons with multiple variants (`default`, `destructive`, `outline`, `secondary`, `ghost`, `link`) | +| `Input` / `Textarea` | Text input primitives | +| `Label` | Accessible form labels | +| `Card` | Container with `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | +| `Dialog` | Modal dialogs with `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter` | +| `Alert` | Alert messages with `AlertTitle` and `AlertDescription` | +| `Checkbox` / `RadioGroup` | Selection inputs | +| `Select` | Dropdown select with `SelectTrigger`, `SelectContent`, `SelectItem` | +| `DropdownMenu` | Context menu with `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem` | +| `Popover` | Floating content anchored to a trigger element | +| `Progress` | Progress bar indicator | +| `Tabs` | Tab navigation with `TabsList`, `TabsTrigger`, `TabsContent` | +| `Tooltip` | Hover tooltips | +| `Accordion` | Collapsible content sections | +| `Banner` | Full-width notification banners | +| `Calendar` / `DateRangePicker` | Date selection and date-range picker components | +| `EmptyState` | Placeholder UI for empty lists or initial states | +| `ExternalLink` | Anchor that opens in a new tab with appropriate `rel` attributes | +| `Form` | Form wrapper integrating react-hook-form with accessible error messages | +| `Header` / `Footer` | Page-level header and footer layout components | +| `Sonner` | Toast notification system (powered by [sonner](https://sonner.emilkowal.dev/)) | +| `Sidebar` | Sidebar navigation layout | +| `Wizard` | Multi-step wizard flow | + +### Blockchain-Specific Components + +| Component | Description | +| --- | --- | +| `NetworkSelector` | Searchable network dropdown with optional multi-select mode | +| `NetworkIcon` | Network-specific icon (renders the correct logo for a given network ID) | +| `NetworkStatusBadge` | Colored badge showing network status (e.g. connected, syncing) | +| `EcosystemDropdown` | Ecosystem selection with chain icons | +| `EcosystemIcon` | Chain-specific icons (Ethereum, Stellar, Polkadot, etc.) | +| `AddressDisplay` | Formatted address with optional alias labels and edit controls | +| `ViewContractStateButton` | Quick-action button to open the contract state widget | +| `OverflowMenu` | Compact "..." dropdown for secondary actions | + +### Usage + +```tsx +import { Button, Card, CardContent, CardHeader, CardTitle } from '@openzeppelin/ui-components'; + +function ContractCard({ name, address }) { + return ( + + + {name} + + +

{address}

+ +
+
+ ); +} +``` + +## Form Fields + +The field components are designed for use with [react-hook-form](https://react-hook-form.com/). Each field handles its own validation, formatting, and blockchain-specific behavior. + +| Field | Input Type | Use Case | +| --- | --- | --- | +| `AddressField` | Blockchain address | Contract addresses, recipient addresses — validates format per ecosystem | +| `AmountField` | Token amounts | Token transfers — handles decimals and BigInt formatting | +| `BigIntField` | Large integers | Raw `uint256` values, timestamps, token IDs | +| `BytesField` | Hex byte strings | Calldata, hashes, signatures | +| `TextField` | Free-form text | String parameters, names, URIs | +| `UrlField` | URL | Endpoint URLs, metadata URIs — validates URL format | +| `PasswordField` | Masked text | API keys, secrets — input is hidden by default | +| `NumberField` | Numeric values | Counts, percentages, indices | +| `BooleanField` | True/false | Feature flags, approvals | +| `EnumField` | Enum selection | Contract enum parameters | +| `SelectField` | Dropdown | Predefined option lists | +| `SelectGroupedField` | Grouped dropdown | Categorized options | +| `RadioField` | Radio buttons | Small option sets | +| `TextAreaField` | Multi-line text | ABI input, JSON data | +| `CodeEditorField` | Code / JSON editor | Contract source code, ABI JSON, configuration files | +| `DateTimeField` | Date and time | Timestamps, scheduling, time-locked parameters | +| `FileUploadField` | File upload | Importing ABIs, uploading contract artifacts | +| `ArrayField` | Dynamic arrays | Array parameters (e.g. `address[]`, `uint256[]`) | +| `ArrayObjectField` | Array of structs | Repeated struct entries (e.g. batch transfers, multi-recipient calls) | +| `MapField` | Key-value pairs | Mapping-like structures | +| `ObjectField` | Nested objects | Struct/tuple parameters | + +### Field Example + +```tsx +import { useForm } from 'react-hook-form'; +import { AddressField, AmountField, Button } from '@openzeppelin/ui-components'; + +function TransferForm() { + const { control, handleSubmit } = useForm({ + defaultValues: { to: '', amount: '' }, + }); + + return ( +
+ + + + + ); +} +``` + +### Address Label & Suggestion Contexts + +UIKit provides context-driven address resolution that works automatically with `AddressField` and `AddressDisplay`: + +- **`AddressLabelProvider`** — When mounted, all `AddressDisplay` instances in the subtree auto-resolve human-readable labels (e.g. "Treasury Multisig" instead of `0x1234...abcd`). +- **`AddressSuggestionProvider`** — When mounted, all `AddressField` instances show autocomplete suggestions as the user types. +- **`useAddressLabel(address, networkId?)`** — Hook to resolve a label from the nearest provider. + +## Renderer Widgets + +`@openzeppelin/ui-renderer` provides high-level, ready-to-use widgets for common blockchain UI patterns. These compose the lower-level components with adapter capabilities. + +![TransactionForm and dynamic fields in the example app](/uikit/form-renderer.png) + +### TransactionForm + +The primary widget for rendering blockchain transaction forms. Accepts a declarative schema and an adapter capabilities bundle: + +```tsx +import { TransactionForm } from '@openzeppelin/ui-renderer'; +import type { RenderFormSchema, TransactionFormCapabilities } from '@openzeppelin/ui-types'; + +const schema: RenderFormSchema = { + id: 'approve-form', + title: 'Approve Spending', + fields: [ + { id: 'spender', name: 'spender', type: 'address', label: 'Spender' }, + { id: 'amount', name: 'amount', type: 'amount', label: 'Amount' }, + ], + layout: { columns: 1, spacing: 'normal', labelPosition: 'top' }, + submitButton: { text: 'Approve', loadingText: 'Approving...' }, +}; + +function ApprovePage({ adapter }: { adapter: TransactionFormCapabilities }) { + return ( + console.log('Approved:', result)} + /> + ); +} +``` + +**Key props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `schema` | `RenderFormSchema` | Form layout, fields, and submit button config | +| `contractSchema` | `ContractSchema` | ABI / function metadata for the target contract | +| `adapter` | `TransactionFormCapabilities` | Capability bundle from the active runtime | +| `executionConfig` | `ExecutionConfig` | EOA vs. relayer configuration | +| `onTransactionSuccess` | callback | Fires after successful transaction | +| `isWalletConnected` | `boolean` | Whether a wallet session is active | + +### DynamicFormField + +Renders individual form fields dynamically based on type configuration. Used internally by `TransactionForm` but available for custom form layouts: + +```tsx +import { DynamicFormField } from '@openzeppelin/ui-renderer'; + + +``` + +### ContractStateWidget + +
+
+ +![ContractStateWidget](/uikit/contract-state-widget.png) + +
+
+ +Displays read-only contract state by querying view functions. The widget automatically discovers all view functions from the contract schema and renders their return types. Supports auto-refresh to keep values current. + +Each row shows the function name, return type, and live result — no wallet connection required. Pass `query` and `schema` capabilities from the active runtime: + +```tsx +import { ContractStateWidget } from '@openzeppelin/ui-renderer'; + + +``` + +
+
+ +![AddressBookWidget](/uikit/address-book-widget.png) + +### AddressBookWidget + +A full-featured address book UI for managing saved addresses and their human-readable aliases. Supports CRUD operations, search, and import/export. + +`AddressBookWidget` works with the [`@openzeppelin/ui-storage`](/tools/uikit/storage) package — specifically the **account alias plugin** which persists address-to-name mappings in IndexedDB via Dexie.js. When mounted alongside `AddressLabelProvider` and `AddressSuggestionProvider`, saved aliases automatically appear in all `AddressDisplay` and `AddressField` components throughout the app. + +```tsx +import { AddressBookWidget } from '@openzeppelin/ui-renderer'; +import { createDexieDatabase, ALIAS_SCHEMA, useAddressBookWidgetProps } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); + +function AddressBook({ addressing }) { + const widgetProps = useAddressBookWidgetProps(db, { networkId: 'ethereum-mainnet' }); + return ; +} +``` + +See the [Storage](/tools/uikit/storage) page for setup and plugin configuration. + +### Other Renderer Components + +| Component | Description | +| --- | --- | +| `ContractActionBar` | Network status display with contract state toggle | +| `ExecutionConfigDisplay` | Configure execution method (EOA / Relayer) | +| `NetworkSettingsDialog` | Configure RPC endpoints and indexer URLs | +| `WalletConnectionWithSettings` | Wallet connection UI with integrated settings | +| `AliasEditPopover` | Inline popover for editing address aliases | + +## Next Steps + +- [React Integration](/tools/uikit/react-integration) — Connect components to wallet state and runtime capabilities +- [Theming & Styling](/tools/uikit/theming) — Customize the visual appearance +- [Getting Started](/tools/uikit/getting-started) — Step-by-step setup guide diff --git a/content/tools/uikit/getting-started.mdx b/content/tools/uikit/getting-started.mdx new file mode 100644 index 00000000..02e1729f --- /dev/null +++ b/content/tools/uikit/getting-started.mdx @@ -0,0 +1,218 @@ +--- +title: Getting Started +--- + +This guide walks you through installing OpenZeppelin UIKit, configuring styles, and rendering your first blockchain transaction form. + +## Prerequisites + +- **Node.js** >= 20.19.0 +- **React** 19 +- **Tailwind CSS** 4 +- A package manager: `pnpm` (recommended), `npm`, or `yarn` + +## Installation + +Install only the packages your application needs. The packages are designed to be incrementally adopted. + +### Minimal Setup (Types + Components) + +For projects that only need the component library and type system: + +```bash +pnpm add @openzeppelin/ui-types @openzeppelin/ui-utils @openzeppelin/ui-components @openzeppelin/ui-styles +``` + +### Full Setup (With Rendering + React Integration) + +For applications that need transaction form rendering and wallet integration: + +```bash +pnpm add @openzeppelin/ui-types @openzeppelin/ui-utils @openzeppelin/ui-styles \ + @openzeppelin/ui-components @openzeppelin/ui-react @openzeppelin/ui-renderer +``` + +### Optional Packages + +```bash +# IndexedDB persistence (address book, settings, etc.) +pnpm add @openzeppelin/ui-storage + +# Dev CLI for Tailwind wiring and local development +pnpm add -D @openzeppelin/ui-dev-cli +``` + +### Ecosystem Adapters + +You also need at least one ecosystem adapter package for the blockchain(s) your app supports. For background on how adapters are structured, see [Building New Adapters](/ui-builder/building-adapters): + +```bash +# EVM (Ethereum, Polygon, Arbitrum, etc.) +pnpm add @openzeppelin/adapter-evm + +# Stellar / Soroban +pnpm add @openzeppelin/adapter-stellar + +# Polkadot (EVM-compatible path) +pnpm add @openzeppelin/adapter-polkadot +``` + +## Step 1: Configure Tailwind CSS + +UIKit uses Tailwind CSS 4 for styling. Components ship class names but **not** compiled CSS — your application's Tailwind build must know where to find them. + +### Automated Setup (Recommended) + +The dev CLI handles Tailwind configuration automatically: + +```bash +pnpm add -D @openzeppelin/ui-dev-cli +pnpm exec oz-ui-dev tailwind doctor --project "$PWD" +pnpm exec oz-ui-dev tailwind fix --project "$PWD" +``` + +This generates an `oz-tailwind.generated.css` file with the correct `@source` directives for all installed OpenZeppelin packages. + +### Manual Setup + +If you prefer manual configuration, your entry CSS must register the OpenZeppelin package sources: + +```css +@layer base, components, utilities; + +@import 'tailwindcss' source(none); +@source "./"; +@source "../"; +@source "../node_modules/@openzeppelin/ui-components"; +@source "../node_modules/@openzeppelin/ui-react"; +@source "../node_modules/@openzeppelin/ui-renderer"; +@source "../node_modules/@openzeppelin/ui-styles"; +@source "../node_modules/@openzeppelin/ui-utils"; +@import '@openzeppelin/ui-styles/global.css'; +``` + + +A bare Tailwind import is not enough. Tailwind v4 must be told to scan the OpenZeppelin `node_modules` paths, or component classes will be missing from the final CSS. + + +## Step 2: Use Components + +UIKit components work with [react-hook-form](https://react-hook-form.com/) for form state management. Here is a simple form with an address field and a submit button: + +```tsx +import { useForm } from 'react-hook-form'; +import { Button, TextField, AddressField } from '@openzeppelin/ui-components'; + +function SimpleForm() { + const { control, handleSubmit } = useForm(); + + const onSubmit = (data) => { + console.log('Form data:', data); + }; + + return ( +
+ + + + + ); +} +``` + +## Step 3: Render a Transaction Form + +The renderer package provides a declarative way to build transaction forms from a schema. The form fields, layout, and submission are all driven by data. + +```tsx +import { TransactionForm } from '@openzeppelin/ui-renderer'; +import type { RenderFormSchema } from '@openzeppelin/ui-types'; + +const schema: RenderFormSchema = { + id: 'transfer-form', + title: 'Transfer Tokens', + fields: [ + { id: 'to', name: 'to', type: 'address', label: 'Recipient' }, + { id: 'amount', name: 'amount', type: 'amount', label: 'Amount' }, + ], + layout: { columns: 1, spacing: 'normal', labelPosition: 'top' }, + submitButton: { text: 'Transfer', loadingText: 'Transferring...' }, +}; + +function TransferPage({ adapter, contractSchema }) { + return ( + { + console.log('Transaction successful:', result); + }} + /> + ); +} +``` + +The `adapter` prop accepts a `TransactionFormCapabilities` object — a bundle of capabilities from your active [Ecosystem Runtime](/tools/uikit/architecture#runtimes). + +## Step 4: Wire Up React Providers + +For wallet integration and multi-network support, wrap your app with `RuntimeProvider` and `WalletStateProvider`: + +```tsx +import { RuntimeProvider, WalletStateProvider } from '@openzeppelin/ui-react'; +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +async function resolveRuntime(networkConfig) { + return ecosystemDefinition.createRuntime('composer', networkConfig); +} + +function App() { + return ( + + + + + + ); +} +``` + +Then access wallet state and runtime capabilities from any component: + +```tsx +import { useWalletState } from '@openzeppelin/ui-react'; + +function WalletInfo() { + const { activeNetworkConfig, activeRuntime, isRuntimeLoading } = useWalletState(); + + if (isRuntimeLoading || !activeRuntime) { + return

Loading...

; + } + + return

Connected to {activeNetworkConfig?.name}

; +} +``` + +For the full React integration guide, see [React Integration](/tools/uikit/react-integration). + +## Next Steps + +- [Architecture](/tools/uikit/architecture) — Understand the package layers, capability tiers, and runtime model +- [Components](/tools/uikit/components) — Explore UI primitives and blockchain-aware form fields +- [React Integration](/tools/uikit/react-integration) — Deep dive into providers, hooks, and wallet state +- [Theming & Styling](/tools/uikit/theming) — Customize tokens, colors, and dark mode +- [Building New Adapters](/ui-builder/building-adapters) — Background on adapter packages and ecosystem integrations diff --git a/content/tools/uikit/index.mdx b/content/tools/uikit/index.mdx new file mode 100644 index 00000000..9391cdb1 --- /dev/null +++ b/content/tools/uikit/index.mdx @@ -0,0 +1,120 @@ +--- +title: OpenZeppelin UIKit +--- + +A modular React component library for building blockchain transaction interfaces — chain-agnostic, capability-driven, and designed for multi-ecosystem applications. + + +**Source code**: OpenZeppelin UIKit is open-source. Browse the implementation, open issues, and contribute at [**github.com/OpenZeppelin/openzeppelin-ui**](https://github.com/OpenZeppelin/openzeppelin-ui). + + + + + + + + + + + +## What is OpenZeppelin UIKit? + +OpenZeppelin UIKit is a set of modular npm packages that provide everything needed to build rich blockchain UIs in React. Instead of a monolithic library, it ships as a **layered stack** — from low-level types and utilities up to high-level form renderers and wallet integration. + +Each layer is independently installable. Use only the pieces you need: the type system for a headless integration, the component library for a design system, or the full renderer for turnkey transaction forms. + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 40}} }%% +flowchart TD + App(["Your Application"]) + + Storage(["ui-storage"]) + Renderer(["ui-renderer"]) + ReactPkg(["ui-react"]) + Components(["ui-components"]) + Utils(["ui-utils"]) + Types(["ui-types"]) + + EVM(["adapter-evm"]) + Stellar(["adapter-stellar"]) + Polkadot(["adapter-polkadot"]) + + App --> Storage & Renderer & ReactPkg + Storage --> Utils + Renderer --> Components + ReactPkg --> Components + Components --> Utils + Utils --> Types + ReactPkg -.->|consumes| EVM & Stellar & Polkadot + + style App fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style Storage fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Renderer fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style ReactPkg fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Components fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Utils fill:#fff3e0,stroke:#ff9800,color:#e65100 + style Types fill:#fff3e0,stroke:#ff9800,color:#e65100 + style EVM fill:#fce4ec,stroke:#e91e63,color:#880e4f + style Stellar fill:#fce4ec,stroke:#e91e63,color:#880e4f + style Polkadot fill:#fce4ec,stroke:#e91e63,color:#880e4f +``` + +## Packages + +| Package | Description | Layer | +| --- | --- | --- | +| [`@openzeppelin/ui-types`](https://www.npmjs.com/package/@openzeppelin/ui-types) | Shared TypeScript type definitions — capabilities, schemas, form models | 1 | +| [`@openzeppelin/ui-utils`](https://www.npmjs.com/package/@openzeppelin/ui-utils) | Framework-agnostic utilities — config, logging, validation, routing | 2 | +| [`@openzeppelin/ui-styles`](https://www.npmjs.com/package/@openzeppelin/ui-styles) | Centralized Tailwind CSS 4 theme with OKLCH tokens and dark mode | 3 | +| [`@openzeppelin/ui-components`](https://www.npmjs.com/package/@openzeppelin/ui-components) | React UI primitives and blockchain-aware form fields (shadcn/ui based) | 4 | +| [`@openzeppelin/ui-react`](https://www.npmjs.com/package/@openzeppelin/ui-react) | React context providers, runtime management, and wallet hooks | 5 | +| [`@openzeppelin/ui-renderer`](https://www.npmjs.com/package/@openzeppelin/ui-renderer) | Transaction form rendering engine and contract state widgets | 6 | +| [`@openzeppelin/ui-storage`](https://www.npmjs.com/package/@openzeppelin/ui-storage) | IndexedDB storage abstraction with Dexie.js and address book plugin | 7 | + +## Key Design Principles + +**Chain-agnostic core.** UIKit packages never import chain-specific logic. Blockchain details are handled entirely by ecosystem adapter packages. For background on the adapter pattern, see [Building New Adapters](/ui-builder/building-adapters). + +**Capability-driven, not monolithic.** Instead of one large adapter interface, the system defines small, focused [capabilities](/tools/uikit/architecture#capabilities) (addressing, query, execution, wallet, etc.) organized into tiers. Components request only the capabilities they need. + +**Pay for what you use.** Install only the layers your app requires. A simple form builder can use just `ui-types` + `ui-components`. A full transaction dashboard can add `ui-renderer` + `ui-react` + `ui-storage`. + +**Multi-ecosystem ready.** A single app can support EVM, Stellar, Polkadot, and more simultaneously. The runtime system manages per-network adapter instances with proper lifecycle and disposal. + +## Ecosystem Adapter Integration + +UIKit connects to blockchains through ecosystem adapter packages — standalone packages that translate chain-specific operations into the shared capability model. For more background, see [Building New Adapters](/ui-builder/building-adapters). + +```mermaid +sequenceDiagram + participant App as Your App + participant React as UIKit React + participant Adapter as Ecosystem Adapter + participant Chain as Blockchain + + App->>React: RuntimeProvider + WalletStateProvider + React->>Adapter: createRuntime(profile, networkConfig) + Adapter-->>React: EcosystemRuntime (capabilities) + React-->>App: useWalletState() → activeRuntime + + App->>React: TransactionForm adapter={capabilities} + React->>Adapter: execution.signAndBroadcast(...) + Adapter->>Chain: Submit transaction + Chain-->>Adapter: Transaction hash + Adapter-->>React: Status updates + React-->>App: UI reflects tx status +``` + +For more background on how adapters work and how new ecosystem integrations are structured, see [Building New Adapters](/ui-builder/building-adapters). + +## Requirements + +- **Node.js** >= 20.19.0 +- **React** 19 +- **Tailwind CSS** 4 + +## Next Steps + +- [Getting Started](/tools/uikit/getting-started) — Install, configure, and render your first form +- [Architecture](/tools/uikit/architecture) — Deep dive into the capability model and runtime lifecycle +- [Building New Adapters](/ui-builder/building-adapters) — How chain-specific logic is decoupled from the UI diff --git a/content/tools/uikit/react-integration.mdx b/content/tools/uikit/react-integration.mdx new file mode 100644 index 00000000..6d1cf54c --- /dev/null +++ b/content/tools/uikit/react-integration.mdx @@ -0,0 +1,268 @@ +--- +title: React Integration +--- + +The `@openzeppelin/ui-react` package provides the runtime and wallet infrastructure that connects UIKit components to blockchain ecosystems. This page covers providers, hooks, wallet state management, and multi-ecosystem support. + +## Provider Setup + +Two providers form the foundation of a UIKit-powered React app: + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'wrappingWidth': 400}} }%% +flowchart TD + RP["RuntimeProvider
manages EcosystemRuntime instances"] + WSP["WalletStateProvider
active network · wallet state · hooks"] + App["Your Components"] + + RP --> WSP --> App + + classDef provider fill:#e0f2f1,stroke:#26a69a,color:#004d40,width:350px + classDef component fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + + class RP,WSP provider + class App component +``` + +### RuntimeProvider + +`RuntimeProvider` maintains a **registry of `EcosystemRuntime` instances** — one per network ID. It creates runtimes on demand, caches them, and disposes all of them on unmount. + +```tsx +import { RuntimeProvider } from '@openzeppelin/ui-react'; +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +async function resolveRuntime(networkConfig) { + return ecosystemDefinition.createRuntime('composer', networkConfig); +} + +function App() { + return ( + + {/* children */} + + ); +} +``` + +**Props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `resolveRuntime` | `(networkConfig) => Promise` | Factory function that creates a runtime for a given network | + +### WalletStateProvider + +`WalletStateProvider` builds on `RuntimeProvider` to manage: + +- The **active network** ID and config +- The **active runtime** and its loading state +- **Wallet facade hooks** from the active runtime's `UiKitCapability` +- The ecosystem's **React UI provider** component (e.g. wagmi's `WagmiProvider`) + +```tsx +import { RuntimeProvider, WalletStateProvider } from '@openzeppelin/ui-react'; + +function App() { + return ( + + + + + + ); +} +``` + +**Props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `initialNetworkId` | `string` | Network to activate on mount | +| `getNetworkConfigById` | `(id: string) => NetworkConfig` | Resolver for network configurations | +| `loadConfigModule` | `(runtime) => Promise` | Optional — load UI kit config modules for the runtime | + +## Hooks + +### useWalletState + +The primary hook for accessing global wallet and runtime state: + +```tsx +import { useWalletState } from '@openzeppelin/ui-react'; + +function Dashboard() { + const { + activeNetworkId, // Current network ID string + activeNetworkConfig, // Full NetworkConfig object + activeRuntime, // EcosystemRuntime instance (or null) + isRuntimeLoading, // True while runtime is being created + walletFacadeHooks, // Ecosystem-specific React hooks + setActiveNetworkId, // Switch networks + reconfigureActiveUiKit // Re-initialize UI kit config + } = useWalletState(); + + if (isRuntimeLoading || !activeRuntime) { + return

Loading runtime...

; + } + + return ( +
+

Network: {activeNetworkConfig?.name}

+

Ecosystem: {activeRuntime.ecosystem}

+
+ ); +} +``` + +### useRuntimeContext + +Low-level hook for direct access to the runtime registry: + +```tsx +import { useRuntimeContext } from '@openzeppelin/ui-react'; + +function AdvancedComponent() { + const { getRuntimeForNetwork } = useRuntimeContext(); + + const handleQuery = async () => { + const { runtime, isLoading } = getRuntimeForNetwork(polygonConfig); + if (isLoading || !runtime) return; + const result = await runtime.query.queryViewFunction(/* ... */); + }; +} +``` + +### Derived Wallet Hooks + +These hooks abstract wallet interactions across ecosystems, providing a consistent API regardless of whether the user is connected to EVM, Stellar, or any other chain: + +| Hook | Returns | +| --- | --- | +| `useDerivedAccountStatus()` | `{ isConnected, address, chainId }` | +| `useDerivedConnectStatus()` | `{ connect, isPending, error }` | +| `useDerivedDisconnect()` | `{ disconnect }` | +| `useDerivedSwitchChainStatus()` | `{ switchChain, isPending }` | +| `useDerivedChainInfo()` | `{ chainId, chains }` | +| `useWalletReconnectionHandler()` | Manages automatic wallet reconnection | + +These are built on top of the `walletFacadeHooks` from the active runtime's `UiKitCapability`, which wraps the ecosystem's native wallet library (e.g. `wagmi` for EVM, Stellar Wallets Kit for Stellar). + +## Wallet Components + +`@openzeppelin/ui-react` ships ready-to-use wallet UI components: + +| Component | Description | +| --- | --- | +| `WalletConnectionHeader` | Compact header bar with wallet status and connect/disconnect | +| `WalletConnectionUI` | Full wallet connection interface | +| `WalletConnectionWithSettings` | Wallet connection with integrated network/RPC settings | +| `NetworkSwitchManager` | Handles programmatic network switching with user confirmation | + +### NetworkSwitchManager + +Pass capabilities from the active runtime — not a monolithic adapter instance: + +```tsx +import { useState } from 'react'; +import { NetworkSwitchManager, useWalletState } from '@openzeppelin/ui-react'; + +function MyApp() { + const [targetNetwork, setTargetNetwork] = useState(null); + const { activeRuntime } = useWalletState(); + + const wallet = activeRuntime?.wallet; + const networkCatalog = activeRuntime?.networkCatalog; + + return ( + <> + {wallet && networkCatalog && targetNetwork && ( + setTargetNetwork(null)} + /> + )} + + ); +} +``` + +### Ecosystem Wallet Components + +Each adapter can provide its own wallet components via the `UiKitCapability`. These are accessed through the runtime: + +```tsx +const walletComponents = runtime.uiKit?.getEcosystemWalletComponents(); +// { ConnectButton, AccountDisplay, NetworkSwitcher } +``` + +For EVM, this integrates with [RainbowKit](https://www.rainbowkit.com/). For Stellar, it integrates with [Stellar Wallets Kit](https://stellarwalletskit.dev/). Each adapter maps its ecosystem's wallet library into the standardized component interface. + +## Multi-Ecosystem Apps + +A single app can support multiple blockchain ecosystems. The key is the `resolveRuntime` function, which determines which adapter to use based on the network config: + +```tsx +import { ecosystemDefinition as evmDef } from '@openzeppelin/adapter-evm'; +import { ecosystemDefinition as stellarDef } from '@openzeppelin/adapter-stellar'; + +async function resolveRuntime(networkConfig) { + switch (networkConfig.ecosystem) { + case 'evm': + return evmDef.createRuntime('composer', networkConfig); + case 'stellar': + return stellarDef.createRuntime('composer', networkConfig); + default: + throw new Error(`Unsupported ecosystem: ${networkConfig.ecosystem}`); + } +} + +function App() { + return ( + + + + + + ); +} +``` + +When the user switches networks, `WalletStateProvider` automatically resolves the correct runtime and updates the wallet facade hooks, UI provider, and wallet components for the new ecosystem. + +```mermaid +sequenceDiagram + participant User + participant WSP as WalletStateProvider + participant RP as RuntimeProvider + participant EVM as EVM Adapter + participant Stellar as Stellar Adapter + + User->>WSP: setActiveNetworkId("ethereum-mainnet") + WSP->>RP: getRuntimeForNetwork(ethConfig) + RP->>EVM: createRuntime("composer", ethConfig) + EVM-->>RP: EVM Runtime + RP-->>WSP: EVM Runtime + Note over WSP: Wallet hooks: wagmi / UI: RainbowKit + + User->>WSP: setActiveNetworkId("stellar-pubnet") + WSP->>RP: getRuntimeForNetwork(stellarConfig) + RP->>Stellar: createRuntime("composer", stellarConfig) + Stellar-->>RP: Stellar Runtime + RP-->>WSP: Stellar Runtime + Note over WSP: Wallet hooks: Stellar Wallets Kit +``` + +## Next Steps + +- [Components](/tools/uikit/components) — Browse all available components and form fields +- [Theming & Styling](/tools/uikit/theming) — Customize the visual design +- [Building New Adapters](/ui-builder/building-adapters) — Background on adapter packages and ecosystem integrations diff --git a/content/tools/uikit/storage.mdx b/content/tools/uikit/storage.mdx new file mode 100644 index 00000000..78da5e4d --- /dev/null +++ b/content/tools/uikit/storage.mdx @@ -0,0 +1,341 @@ +--- +title: Storage +--- + +`@openzeppelin/ui-storage` provides client-side persistence for UIKit applications. It is built on [Dexie.js](https://dexie.org/) (an IndexedDB wrapper) and ships a plugin system for extending storage with domain-specific functionality. + +## Installation + +```bash +pnpm add @openzeppelin/ui-storage +``` + +## Why Use the Storage Plugin? + +Browser applications commonly reach for `localStorage` to persist user data. While fine for a handful of string values, `localStorage` hits hard limits as your app grows: + +| Concern | `localStorage` | `@openzeppelin/ui-storage` (IndexedDB) | +| --- | --- | --- | +| **Storage quota** | ~5 MB per origin | Hundreds of MB — often limited only by available disk space | +| **Data model** | Flat key-value strings — every read/write requires `JSON.parse`/`JSON.stringify` | Structured object stores with typed records, indexes, and compound queries | +| **Performance** | Synchronous — blocks the main thread on every call | Asynchronous — all reads and writes are non-blocking | +| **Querying** | Full-scan only; no way to filter or sort without loading everything | Indexed lookups and range queries via Dexie.js | +| **Concurrent tabs** | No built-in synchronization; race conditions on simultaneous writes | Transactional; supports multi-tab coordination out of the box | +| **Schema evolution** | Manual — you must handle migrations yourself | Declarative versioned schemas with automatic upgrade migrations | + +Beyond these raw IndexedDB advantages, the storage plugin adds an **opinionated layer** designed for blockchain UIs: + +- **Typed base classes** — `EntityStorage` and `KeyValueStorage` give you CRUD, validation, quota handling, and timestamps without boilerplate. +- **Plugin system** — domain-specific plugins (like the built-in account alias plugin) drop into any Dexie database and integrate with UIKit providers automatically. +- **Reactive hooks** — `useLiveQuery` re-renders components when the underlying IndexedDB data changes, including changes from other browser tabs. +- **Quota-safe writes** — the `withQuotaHandling` wrapper catches `QuotaExceededError` and surfaces a typed error code so your UI can handle it gracefully instead of silently failing. + +### When Does It Make Sense? + +Use `@openzeppelin/ui-storage` when your application needs to persist **structured, queryable data** on the client — especially data that grows over time or must survive page reloads. Common examples include: + +- **Contract history and recent contracts** — the [Role Manager](https://github.com/OpenZeppelin/role-manager) persists recently accessed contracts per network in a `RecentContractsStorage` built on `EntityStorage`. Records are indexed by `[networkId+address]` for fast lookups and sorted by `lastAccessed` so the most recent entries always appear first — something that would require deserializing, sorting, and re-serializing an entire array in `localStorage`. + +- **UI configuration and form state** — the [UI Builder](https://github.com/OpenZeppelin/ui-builder) stores complete contract UI configurations (including large ABIs and compiled contract definitions) in a `ContractUIStorage` entity store. With records that can reach tens of megabytes, `localStorage`'s 5 MB limit would be a non-starter. The storage plugin also powers the builder's import/export and multi-tab auto-save features. + +- **User preferences and settings** — both projects use `KeyValueStorage` for simple typed preferences (theme, active network, page size) — a lightweight alternative that still benefits from async I/O and schema versioning. + +- **Address book and aliases** — the built-in account alias plugin persists address-to-name mappings in IndexedDB and wires them into UIKit's `AddressLabelProvider` and `AddressSuggestionProvider`. Every `AddressDisplay` and `AddressField` in the component tree resolves labels automatically without any per-component wiring. + +If your app only stores a single flag or token, `localStorage` is perfectly adequate. Reach for the storage plugin when you need **indexed queries, large payloads, multi-tab safety, or domain-specific persistence** that integrates with the rest of the UIKit ecosystem. + +## Core Abstractions + +The package exposes two base classes that your application can extend: + +| Class | Description | +| --- | --- | +| `EntityStorage` | Generic IndexedDB store for typed entities. Handles create, read, update, delete, and query. | +| `KeyValueStorage` | Simple key-value store backed by IndexedDB. Useful for persisting settings and preferences. | + +Both classes wrap a Dexie database instance and can be composed with plugins. + +## Account Alias Plugin + +
+
+ +![Account alias plugin](/uikit/storage-account-alias-plugin.png) + +
+
+ +The built-in **account alias plugin** persists address-to-name mappings. It powers the `AddressBookWidget` and integrates with `AddressLabelProvider` and `AddressSuggestionProvider` to resolve human-readable labels automatically across all `AddressDisplay` and `AddressField` components. + +
+
+ +### Setup + +Create a Dexie database instance with the alias schema, then use the provided hooks: + +```tsx +import { + createDexieDatabase, + ALIAS_SCHEMA, + useAliasLabelResolver, + useAliasSuggestionResolver, + useAddressBookWidgetProps, +} from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); +``` + +### Integration with Address Providers + +Mount the providers near the root of your app to activate automatic label resolution. The `useAliasLabelResolver` and `useAliasSuggestionResolver` hooks return props that can be spread directly into the respective providers: + +```tsx +import { + AddressLabelProvider, + AddressSuggestionProvider, +} from '@openzeppelin/ui-components'; +import { + createDexieDatabase, + ALIAS_SCHEMA, + useAliasLabelResolver, + useAliasSuggestionResolver, +} from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); + +function App() { + const labelResolver = useAliasLabelResolver(db); + const suggestionResolver = useAliasSuggestionResolver(db); + + return ( + + + + + + ); +} +``` + +Once mounted: + +- Every `AddressDisplay` in the subtree automatically shows the saved alias instead of the raw address. +- Every `AddressField` shows autocomplete suggestions as the user types. + +### AddressBookWidget + +Wire `AddressBookWidget` using the `useAddressBookWidgetProps` hook, which returns all the props the widget needs: + +```tsx +import { AddressBookWidget } from '@openzeppelin/ui-renderer'; +import { createDexieDatabase, ALIAS_SCHEMA, useAddressBookWidgetProps } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); + +function AddressBook({ addressing }) { + const widgetProps = useAddressBookWidgetProps(db, { networkId: 'ethereum-mainnet' }); + + return ( + + ); +} +``` + +See [Components — AddressBookWidget](/tools/uikit/components#addressbookwidget) for a screenshot. + +## Custom Entity Stores + +Extend `EntityStorage` to create typed stores for your own domain objects. The constructor takes a Dexie database instance and a table name: + +```tsx +import { EntityStorage, createDexieDatabase } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +interface SavedContract { + id: string; + address: string; + name: string; + networkId: string; + addedAt: number; +} + +const MY_SCHEMA = { contracts: '++id, address, networkId' }; +const db = createDexieDatabase(new Dexie('my-app'), MY_SCHEMA); + +class ContractStore extends EntityStorage { + constructor() { + super(db, 'contracts'); + } + + async findByNetwork(networkId: string): Promise { + return this.query((item) => item.networkId === networkId); + } +} + +const contractStore = new ContractStore(); +await contractStore.add({ id: '...', address: '0x...', name: 'My Token', networkId: 'ethereum-mainnet', addedAt: Date.now() }); +``` + +## Key-Value Store + +Use `KeyValueStorage` for simple settings and flags. Like `EntityStorage`, it takes a Dexie db instance and a table name: + +```tsx +import { KeyValueStorage, createDexieDatabase } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const MY_SCHEMA = { settings: 'key' }; +const db = createDexieDatabase(new Dexie('my-app'), MY_SCHEMA); + +class AppSettings extends KeyValueStorage { + constructor() { + super(db, 'settings'); + } +} + +const settings = new AppSettings(); +await settings.set('theme', 'dark'); +const theme = await settings.get('theme'); // 'dark' +``` + +## React Hook Factories + +The storage package ships a set of **factory functions** that turn any `EntityStorage` or `KeyValueStorage` into a fully reactive React hook — complete with live queries, CRUD wrappers, and file import/export. These are the recommended way to consume storage in React components. + +### `createRepositoryHook` + +The main factory. It composes the lower-level factories below into a single hook that provides everything a component needs: live data, loading state, CRUD operations, and optional file I/O. + +```tsx +import { createRepositoryHook, createDexieDatabase, EntityStorage } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; +import { toast } from 'sonner'; + +interface Bookmark { id: string; url: string; label: string; } + +const SCHEMA = { bookmarks: '++id, url' }; +const db = createDexieDatabase(new Dexie('my-app'), [{ version: 1, stores: SCHEMA }]); + +class BookmarkStore extends EntityStorage { + constructor() { super(db, 'bookmarks'); } + async exportJson() { return JSON.stringify(await this.getAll()); } + async importJson(json: string) { /* parse and bulk-insert */ } +} + +const bookmarkStore = new BookmarkStore(); + +const useBookmarks = createRepositoryHook({ + db, + tableName: 'bookmarks', + repo: bookmarkStore, + onError: (title, err) => toast.error(title), + fileIO: { + exportJson: () => bookmarkStore.exportJson(), + importJson: (json) => bookmarkStore.importJson(json), + filePrefix: 'bookmarks-backup', + }, +}); + +function BookmarkList() { + const { records, isLoading, save, remove, exportAsFile, importFromFile } = useBookmarks(); + + if (isLoading) return

Loading…

; + + return ( +
    + {records?.map((b) => ( +
  • + {b.label} +
  • + ))} +
+ ); +} +``` + +The hook returned by `createRepositoryHook` exposes: + +| Property | Type | Description | +| --- | --- | --- | +| `records` | `T[] \| undefined` | Live query result — `undefined` while loading | +| `isLoading` | `boolean` | `true` until the first query resolves | +| `save` | `(record) => Promise` | Insert a new record | +| `update` | `(id, partial) => Promise` | Patch an existing record | +| `remove` | `(id) => Promise` | Delete by ID | +| `clear` | `() => Promise` | Remove all records | +| `exportAsFile` | `(ids?) => Promise` | Download records as a timestamped JSON file (only when `fileIO` is configured) | +| `importFromFile` | `(file) => Promise` | Import from a JSON `File` (only when `fileIO` is configured) | + +### Lower-Level Factories + +`createRepositoryHook` is built from three smaller factories that can be used independently when you need finer-grained control: + +| Factory | Purpose | +| --- | --- | +| `createLiveQueryHook(db, tableName, query?)` | Returns a hook that re-renders whenever the underlying Dexie table changes. Powered by `useLiveQuery` from `dexie-react-hooks`. | +| `createCrudHook(repo, { onError? })` | Wraps a `CrudRepository` (anything with `save`, `update`, `delete`, `clear`) with unified error handling. | +| `createJsonFileIO({ exportJson, importJson }, { filePrefix, onError? })` | Produces `exportAsFile` / `importFromFile` functions that handle Blob creation, download triggers, file reading, and JSON validation. | + +#### `createLiveQueryHook` + +```tsx +import { createLiveQueryHook, createDexieDatabase } from '@openzeppelin/ui-storage'; + +const useContracts = createLiveQueryHook(db, 'contracts'); + +function ContractList() { + const contracts = useContracts(); // undefined while loading, then T[] + return
    {contracts?.map((c) =>
  • {c.name}
  • )}
; +} +``` + +Pass an optional `query` function for filtered or sorted results: + +```tsx +const useRecentContracts = createLiveQueryHook( + db, + 'contracts', + (table) => table.orderBy('addedAt').reverse().limit(10).toArray(), +); +``` + +#### `createCrudHook` + +```tsx +import { createCrudHook } from '@openzeppelin/ui-storage'; + +const useContractCrud = createCrudHook(contractStore, { + onError: (title, err) => console.error(title, err), +}); + +function AddButton() { + const { save } = useContractCrud(); + return ; +} +``` + +#### `createJsonFileIO` + +```tsx +import { createJsonFileIO } from '@openzeppelin/ui-storage'; + +const { exportAsFile, importFromFile } = createJsonFileIO( + { exportJson: () => store.exportJson(), importJson: (json) => store.importJson(json) }, + { filePrefix: 'my-data', onError: (title, err) => toast.error(title) }, +); + +// exportAsFile() triggers a browser download of "my-data-2026-04-09.json" +// importFromFile(file) reads a File, validates JSON, and calls importJson() +``` + +## Next Steps + +- [Components](/tools/uikit/components) — UI components that consume storage plugins +- [React Integration](/tools/uikit/react-integration) — Wire up providers and wallet state diff --git a/content/tools/uikit/theming.mdx b/content/tools/uikit/theming.mdx new file mode 100644 index 00000000..89e3c5be --- /dev/null +++ b/content/tools/uikit/theming.mdx @@ -0,0 +1,169 @@ +--- +title: Theming & Styling +--- + +OpenZeppelin UIKit uses **Tailwind CSS 4** with a centralized design token system. This page explains how styling works, how to customize the theme, and how to set up your application's CSS pipeline. + +## How Styling Works + +UIKit components use Tailwind CSS utility classes internally. The `@openzeppelin/ui-styles` package provides: + +- A **`global.css`** file with Tailwind v4 `@theme` definitions using OKLCH color tokens +- CSS custom properties for colors, radii, spacing, and animations +- A `@custom-variant dark` for dark mode support +- Chart and sidebar color tokens + +Components do **not** ship pre-compiled CSS. Your application runs Tailwind and produces the final stylesheet, which means: + +- You have full control over the CSS output +- Unused component styles are automatically tree-shaken +- You can extend or override any design token + +## Setting Up Styles + +### Automated Setup + +The dev CLI generates and maintains the Tailwind configuration: + +```bash +pnpm add -D @openzeppelin/ui-dev-cli +pnpm exec oz-ui-dev tailwind doctor --project "$PWD" +pnpm exec oz-ui-dev tailwind fix --project "$PWD" +``` + +This creates `oz-tailwind.generated.css` with `@source` directives that tell Tailwind where to find class names in OpenZeppelin packages. + +### Manual Setup + +Add these directives to your application's entry CSS file: + +```css +@layer base, components, utilities; + +@import 'tailwindcss' source(none); + +/* Your app sources */ +@source "./"; +@source "../"; + +/* OpenZeppelin UIKit sources */ +@source "../node_modules/@openzeppelin/ui-components"; +@source "../node_modules/@openzeppelin/ui-react"; +@source "../node_modules/@openzeppelin/ui-renderer"; +@source "../node_modules/@openzeppelin/ui-styles"; +@source "../node_modules/@openzeppelin/ui-utils"; + +/* OpenZeppelin theme tokens */ +@import '@openzeppelin/ui-styles/global.css'; +``` + + +If you also use Ecosystem Adapter packages, add their `@source` directives too — adapters may ship UI components (wallet dialogs, network selectors) that need Tailwind scanning. The `oz-ui-dev tailwind fix` command handles this automatically. + + +## Design Tokens + +The theme is defined in `@openzeppelin/ui-styles/global.css` using Tailwind v4's `@theme` directive with [OKLCH](https://oklch.com/) color values. OKLCH provides perceptually uniform colors across light and dark modes. + +### Color Tokens + +The theme defines semantic color tokens rather than raw color values: + +| Token | Purpose | +| --- | --- | +| `--background` / `--foreground` | Page background and default text | +| `--card` / `--card-foreground` | Card surfaces | +| `--primary` / `--primary-foreground` | Primary actions (buttons, links) | +| `--secondary` / `--secondary-foreground` | Secondary actions | +| `--muted` / `--muted-foreground` | Subdued elements | +| `--accent` / `--accent-foreground` | Highlighted elements | +| `--destructive` / `--destructive-foreground` | Destructive actions (delete, error) | +| `--border` | Border colors | +| `--input` | Input field borders | +| `--ring` | Focus ring color | + +### Layout Tokens + +| Token | Purpose | +| --- | --- | +| `--radius` | Base border radius (used with `rounded-*` utilities) | +| `--sidebar-*` | Sidebar-specific colors and dimensions | +| `--chart-1` through `--chart-5` | Chart/data visualization colors | + +## Dark Mode + +UIKit uses `next-themes` for dark mode support, with a `@custom-variant dark` in the theme CSS. The dark variant activates automatically based on the user's system preference or an explicit toggle. + +All color tokens have dark mode equivalents defined in the theme. Components automatically adjust when the variant is active. + +### Integrating with next-themes + +If your app uses `next-themes`, dark mode works out of the box: + +```tsx +import { ThemeProvider } from 'next-themes'; + +function App({ children }) { + return ( + + {children} + + ); +} +``` + +## Component Variants + +Button and other interactive components use [`class-variance-authority`](https://cva.style/) for variant management: + +```tsx +import { Button } from '@openzeppelin/ui-components'; + +// Variant options: default, destructive, outline, secondary, ghost, link + + + +// Size options: default, sm, lg, icon + + +``` + +## Customizing the Theme + +Since the theme is CSS custom properties, you can override any token in your app's CSS: + +```css +@import '@openzeppelin/ui-styles/global.css'; + +:root { + --primary: oklch(0.65 0.2 250); + --radius: 0.75rem; +} +``` + +This approach works because UIKit components reference the CSS variables, not hard-coded values. Your overrides take precedence and propagate to all components. + +## Utility Functions + +`@openzeppelin/ui-components` exports styling utilities used internally and available for your custom components: + +| Utility | Source | Purpose | +| --- | --- | --- | +| `cn(...classes)` | `tailwind-merge` + `clsx` | Merge Tailwind classes with conflict resolution | +| `buttonVariants` | `class-variance-authority` | Apply button variant styles to custom elements | + +```tsx +import { cn } from '@openzeppelin/ui-utils'; + +function CustomCard({ className, ...props }) { + return ( +
+ ); +} +``` + +## Next Steps + +- [Getting Started](/tools/uikit/getting-started) — Full setup walkthrough including Tailwind configuration +- [Components](/tools/uikit/components) — Browse all available UI components +- [Architecture](/tools/uikit/architecture) — Understand the package layer system diff --git a/public/uikit/address-book-widget.png b/public/uikit/address-book-widget.png new file mode 100644 index 00000000..d05c5bc6 Binary files /dev/null and b/public/uikit/address-book-widget.png differ diff --git a/public/uikit/contract-state-widget.png b/public/uikit/contract-state-widget.png new file mode 100644 index 00000000..5a6ba485 Binary files /dev/null and b/public/uikit/contract-state-widget.png differ diff --git a/public/uikit/example-app-overview.png b/public/uikit/example-app-overview.png new file mode 100644 index 00000000..aa9b8775 Binary files /dev/null and b/public/uikit/example-app-overview.png differ diff --git a/public/uikit/form-renderer.png b/public/uikit/form-renderer.png new file mode 100644 index 00000000..849055c1 Binary files /dev/null and b/public/uikit/form-renderer.png differ diff --git a/public/uikit/storage-account-alias-plugin.png b/public/uikit/storage-account-alias-plugin.png new file mode 100644 index 00000000..00b2aac4 Binary files /dev/null and b/public/uikit/storage-account-alias-plugin.png differ diff --git a/src/navigation/ethereum-evm.json b/src/navigation/ethereum-evm.json index 8ce1463b..09441417 100644 --- a/src/navigation/ethereum-evm.json +++ b/src/navigation/ethereum-evm.json @@ -1246,6 +1246,47 @@ "name": "Role Manager", "url": "/role-manager" }, + { + "type": "folder", + "name": "UIKit", + "index": { + "type": "page", + "name": "Overview", + "url": "/tools/uikit" + }, + "children": [ + { + "type": "page", + "name": "Getting Started", + "url": "/tools/uikit/getting-started" + }, + { + "type": "page", + "name": "Architecture", + "url": "/tools/uikit/architecture" + }, + { + "type": "page", + "name": "Components", + "url": "/tools/uikit/components" + }, + { + "type": "page", + "name": "React Integration", + "url": "/tools/uikit/react-integration" + }, + { + "type": "page", + "name": "Theming & Styling", + "url": "/tools/uikit/theming" + }, + { + "type": "page", + "name": "Storage", + "url": "/tools/uikit/storage" + } + ] + }, { "type": "folder", "name": "Defender",