Design SystemPortal

LI.FI Portal

The partner-console primitives that compose portal.html — usage meters, rate badges, metric cards, wallet groups, fee tables, onboarding checklists, and identity avatars. These are page-local tier-3 .portal-* primitives defined in portal.css, documented here so the surface stays scannable; each is a promotion candidate once a second consumer appears. Press ⌘K to search the whole system.

Shell & identity
Shell & identity
The rail shell plus the cross-surface identity atoms it hosts (org switcher, account chip, integration rows, the detail entity header). The Portal rail (.portal-rail) composes the shared .app-rail shell — a fixed glass .panel — and layers in the design-system .brand-lockup, .search-trigger, .dropdown, and .side-nav primitives; no new chrome.

Rail panel .portal-rail

The fixed left rail. Composes the shared .app-rail shell (a glass .panel) and stacks head · search · org · nav · footer. Fully rounded — no minimize handle, so all four corners carry --panel-radius.

The mock below is the rail's actual root-screen markup from portal.html with one surgical change — position: fixedposition: relative on the rail (so it lives inside this viewport instead of floating over the page) and JS hooks (data-view / data-drill / id) stripped. Every visible part is a real composed primitive — Brand lockup, Search trigger, Dropdown, Side nav, Avatar (initial).

Page chrome
Page chrome
The two header primitives that sit at the top of every Portal canvas — .portal-page-head on top-level pages (Dashboard · Analytics · Members · …) and .portal-entity-head on secondary pages drilled into from a parent (Integration detail · Analytics children). Both are composable slots; the variants below show each slot omission so a designer can read which slots are required vs optional at a glance. Pick by tier — top-level pages → page header; secondary pages drilled into from a parent → entity header.

Page header .portal-page-head

Top-level page header — required title + optional sub-paragraph + optional right-aligned actions. Sits atop every top-level Portal view.

Canonical · all slots

Welcome back, Vilen

Here's what's moving across your integrations.

Title only

Members

Title + sub

Status

All systems operational — historical uptime and recent incidents below.

Title + actions

API keys

Entity header .portal-entity-head

Secondary-page header — eyebrow · avatar · name + status chip · integrator-string · actions. Avatar + title required; the rest are optional.

Canonical · all slots

Integrations

oneinteg10

Active
integrator-string · oneinteg10

No eyebrow · top-level entity

oneinteg10

Active
integrator-string · oneinteg10

No actions · read-only entity

Integrations

oneinteg10

Archived
integrator-string · oneinteg10

Minimal · avatar + title

oneinteg10

Integrations
Integrations
Primitives distinctive to the integrations-list view — the plan-usage meter that sits under the page title, and the empty-state illustration for a brand-new organisation with no integrations yet.

Empty-state illustration .portal-empty__art

Decorative overlapping-cards + checkmark + add-FAB illustration on a dotted grid, anchoring the "create your first integration" empty state.

Pairs with a .portal-empty__title heading, a .portal-empty__desc line, an actions row, and a 3-up .portal-steps grid below it.

Usage meter .portal-usage

Pill showing N / M plan usage with a mini progress bar; --limit flips it to a danger tone at capacity.

Within plan · .portal-usage

6 / 10 integrations

At capacity · .portal-usage--limit

2 / 2 integrations
New integration
New integration
The reusable compositions behind the New-integration wizard (integration-wizard.js) — a 4-step modal built end-to-end on design-system primitives, opened from the integrations view via [data-action="open-integration-modal"]. Documented here: the expandable wallet-family row, the in-field character counter, and the wizard flow itself.

Network row .network-row

Expandable wallet-family row: avatar, name, a Required/Optional chip, and an Add / Added / Adding action over a grid-rows drawer.

The wallets step adds one fee-collection wallet per network family. Each family is a .network-row that moves through three states, driven by data-state: empty (an Add affordance), added (the saved address + a remove kebab), and adding (an accent rim + an open drawer holding the add-wallet form). The drawer is a pure-CSS grid-rows 0fr → 1fr accordion — no JS height math.

Empty · data-state="empty"

Bitcoin Optional
BTC native

Added · data-state="added"

EVM Required
0x742d…f44e
Added

Adding · open drawer · data-state="adding"

SVM Required
Solana · enter a Solana address
Adding
Valid Solana address

Fees are sent to this address automatically. You can change it later in Fee Management.

In-field counter .input-counter

Wraps a maxlength input so a live count sits inside its right edge, turning danger at the cap. Distinct from the label-row .field-counter.

The wizard's name field overlays the live character count inside the input's right edge — the field reserves right padding so typed text never runs under it. The count is JS-bound (the consumer rewrites the text and toggles --over at the cap); the input's maxlength is the real constraint.

Under the cap · muted count

At the cap · .input-counter__count--over

Integration wizard .iwiz

The New-integration 4-step modal flow: stepper, per-step head and body, and a uniform footer, composed from design-system primitives.

The wizard is a JS-driven modal (integration-wizard.js) — renderStep() rewrites a [data-integration-flow] shell per step. The snapshot below is one assembled step (String) showing the four composed layers: the stepper, the head, the step body (here list-card radios + an alert), and the footer. The live 4-step flow runs in the Portal — open portal.html and click New integration.

Integration detail
Integration detail
The compositions that build the integration-detail view — Overview, Wallets, and Fees tabs. Each sits inside a .panel.portal-panel and composes design-system primitives (.list--cards, .data-table, .chip, .btn-*, .alert) with the Portal-specific pieces below.

Fee table & summary .portal-fee-chain · .portal-fee-bar · .portal-fees-summary

The Fees tab — a 3-column summary (total / lifetime / rates) above a per-chain collectable table with mini relative-balance bars.

Summary · .portal-fees-summary

Total collectable
$11,518,010↑ 12.4%
Across 6 chains · last 30 days
Lifetime collected
$48.21M
Since Mar 12, 2024
LI.FI fees
Default30 BPS · 0.3%
Stablecoin10 BPS · 0.1%
Same-chain5 BPS · 0.05%

Per-chain table · .portal-fee-chain + .portal-fee-bar

ChainCollectableTransactionsLast collected
Optimism2.4081 ETH $7,000,000 56,204 12h ago
Arbitrum1.0312 ETH $3,000,000 24,891 5h ago
Polygon128.42 POL $92,410 1,208 3d ago

Bar width is each chain's balance as a fraction of the largest balance — a relative-magnitude cue, not a percentage of total.

Metric card .portal-metric

Overview metric + action card: an icon-label, a large value, and a footer button or note pinned to the bottom of the panel.

Collectable fees
$1,284.32
RPM ceiling
100
Health
99.98%
Last 30d uptime

Bar list .portal-barlist

Ranked magnitude breakdown — label + value on top, a proportional fill bar below. Drives Top chains, fee splits, and Top routes.

Single chain · base row

Ethereum$18.4M
Base$11.5M
Arbitrum$10.6M

Route pair · .portal-barlist__row--route · --sm track

EthereumArbitrum$6K
BaseOptimism$3K

Onboarding checklist .portal-checklist

Vertical step list with done / todo check markers and an inline resume link, paired with a progress bar.

  • Create integration
  • Add a fee wallet
  • Generate first API key
  • Send a test transaction Resume →
  • Invite a teammate

Wallet group .portal-wallet-group

Fee-receiver group: a default-address row with per-chain override rows nested beneath a connector line, plus a not-configured empty state.

Default + per-chain overrides · .portal-wallet-overrides

EVM Networks 1 default · 1 override

Default address used across all EVM chains, plus per-chain overrides.

  • Default EVM DEFAULT · Ethereum
    0x2e8a346Ba1E0c7f9913282AFB2705CD00754E478
    Collected$842.10
  • Base override · Base
    0x4DFbc42E1e86Fb747A69a3Dc75BA8d6bD0cC7818
    Collected$157.56

Not configured · .portal-wallet-empty

Bitcoin 0 of 1

UTXO chains. Not configured for this integration yet.

No Bitcoin address on fileBTC fees will accumulate on platform until an address is added.
Withdraw fees
Withdraw fees
The fee-withdrawal modal flow (withdraw-fees.js) — pick a chain's collectable tokens, sign, land on a receipt or a revert state. Opened via [data-action="open-withdraw-modal"] from the Detail Fees table and the Fee Management list; a confirmed signature settles into LifiPartnerOrgs.recordWithdrawal, so every collectable surface drains live behind the overlay. The "Withdraw all fees" button runs the same chassis as a multi-chain batch — one signature per chain through a per-chain checklist (data-withdraw-all on the trigger); on Fee Management the batch runs org-wide — every integration × chain pair, grouped by integration (data-withdraw-all-org).

Withdraw fees .wfee

The fee-withdrawal modal flow: select a chain's collectable tokens, sign, land on a receipt or a revert state — all DS primitives.

A JS-driven modal (withdraw-fees.js) — renderStep() rewrites a [data-withdraw-flow] shell per state: select → signing → success | error. The snapshots below show the two full compositions plus the signing / error fragments; the live flow runs in the Portal — open portal.html and hit any Withdraw button on the Fees table (hold on the CTA to demo the failure path). Amounts derive from the live price oracle; the figures below are one captured run.

Select · amount hero · select-all · token rows

The select-all checkbox sits in its :indeterminate dash state (a partial pick); the hero value dims to faint ink when nothing is selected.

Success · receipt

Receipt rows are a .detail-list--divided in a card frame; the explorer link resolves per chain (Arbiscan, Etherscan, Basescan…).

Signing banner · revert detail

Waiting for wallet confirmation
Sign the transaction in MetaMask · 0x7C3B…c4B6

Error · Insufficient_gas

execution reverted: gas estimation failed at block 218682389

Signing seats a .spinner inside the alert's .spot-icon disc — progress + the mandatory shape signal in one. The revert detail is the tier-2 .error-detail — the alert family’s code-bearing sibling (same tone slots and surface recipe, no spot-icon by design).

Batch withdraw .wfee__legs

The multi-chain batch behind "Withdraw all fees" — one signature per chain, through a checklist that doubles as the running receipt.

The same .wfee chassis run as a sequential per-chain wizard: bselect → brun → bsummary. The progress model is a checklist, not a stepper — the steps ARE the chains (avatar + amount + status per row), and the select state's checkboxes become progress marks at the same 20 px geometry. Two scopes share the chassis: integration (the Detail Fees card — one integration's chains) and org-wide (Fee Management — every integration × chain pair, grouped by integration). Live in the Portal: Detail → Fees or Fee ManagementWithdraw all fees (hold to demo a failed leg). The snapshots below are one captured run.

Run · paused on a failed leg · Retry / Skip / Stop

A failed leg PAUSES the batch (it never aborts silently): Retry re-signs the same chain, Skip moves on, Stop ends it. While a leg signs, its mark holds a .spinner and the row carries the selected tint so the eye tracks the signature down the list.

Summary · partial · per-leg recap

The summary disc grades the outcome — success when every leg landed, warn for a partial run, danger when nothing was withdrawn. Done legs carry per-chain explorer icon-links; the recap is the same checklist in its final state. Contact support appears only when a leg failed.

Org-wide · Fee Management · grouped by integration

The org-wide scope (Fee Management, data-withdraw-all-org) flattens every integration × chain pair into one queue: rows stay chain legs, quiet .wfee__leg-group labels (xs monogram + name) mark each integration cluster, and the header pill names the org instead of an integration. Legs are keyed slug|chain — the same chain can appear once per integration.