designComponents

Search the design system…

Search the design system…

layouts

Settings screens

Five drop-in screens covering the universal SaaS /settings stack — Profile, Security, Organisations, API Tokens, Webhooks. Headless: feed data and callbacks, ship. Same authoring pattern as <LoginPage>/<SignUpPage> in @8maverik8/auth.

Each screen is the canonical recipe for its surface, composed from the primitives in /components/settings-shell. ProfileScreen owns avatar+name+email+update with an optional 'Delete account' section. SecurityScreen owns five rows (2FA, passkeys, recent activity, sessions, linked accounts) and four drill-down sub-views with `← Back to security` headers. OrganisationsScreen owns the table + create dialog. ApiTokensScreen owns the create form + one-time-secret reveal + list. WebhooksScreen owns the table + create dialog with a multi-select trigger picker. Every screen is data-agnostic — the consumer feeds data and callbacks. Diagonal logic (WebAuthn ceremonies, OTP verification, OAuth redirect URLs, S3 uploads) lives in those callbacks; the screens stay UI-only.

Install

Import from the workspace package:

import { ProfileScreen, SecurityScreen, OrganisationsScreen, ApiTokensScreen, WebhooksScreen, PasswordSection, TwoFactorSection, PasskeysSection, SessionsSection, RecentActivitySection, LinkedAccountsSection, DeleteAccountSection, Passkey, Session, ActivityEvent, LinkedAccount, LinkableProvider, Organisation, ApiToken, Webhook, ProfileScreenValue } from "@8maverik8/design";

Examples

ProfileScreen — avatar + name + email + delete

Avatar uploader (try clicking 'Upload avatar' — switches to a sample image), editable name, disabled email, Update button, and a destructive 'Delete account' row at the bottom with email-typed confirmation dialog.

Profile

Edit your personal details.

OT

Delete account

Delete your account and all its contents. This action is irreversible and will cancel any active subscription.

import { ProfileScreen, type ProfileScreenValue } from "@8maverik8/design";

const [profile, setProfile] = useState<ProfileScreenValue>({
  fullName: "Oleg Tkachev",
  email: "oleg@oapps.io",
  avatarUrl: null,
});

<ProfileScreen
  value={profile}
  onUpdate={async (next) => { await api.updateProfile(next); setProfile(next); }}
  onUploadAvatar={async (file) => {
    const { url } = await uploadToS3(file);
    return { url };
  }}
  onDelete={async () => {
    await api.deleteAccount();
    window.location.href = "/sign-in";
  }}
/>

SecurityScreen — 5 sections + 4 sub-views

The full Security flow. Click 'Enable 2FA' for the OTP dialog (token `123456` works in the demo), 'Manage passkeys' / 'Manage sessions' / 'View activity' / 'Manage linked accounts' to drill into sub-views with built-in '← Back to security' navigation. Every state lives in the consumer; the screen owns only the active-sub-view toggle.

Security

Manage your password, two-factor authentication, and recent activity.

Password

Change the password you use to sign in with email.

Two factor authentication

Add an authenticator app as a secondary authentication method. Required for signing documents in some products.

Passkeys

Allows authenticating using biometrics, password managers, hardware keys, etc.

Recent activity

View all recent security activity related to your account.

Active sessions

View and manage all active sessions for your account.

Linked accounts

View and manage all login methods linked to your account.

import { SecurityScreen } from "@8maverik8/design";

<SecurityScreen
  password={{
    hasPassword: user.hasPassword,
    onChangePassword: async ({ currentPassword, newPassword }) => {
      const r = await authClient.changePassword({ currentPassword, newPassword });
      return r.error ? { ok: false, error: r.error.message } : { ok: true };
    },
  }}
  twoFactor={{
    enabled: user.twoFa,
    onStartEnable: async () => {
      const { qrSvg, secret } = await authClient.totp.start();
      return { qrCode: qrSvg, manualCode: secret };
    },
    onConfirmEnable: async (otp) => {
      const r = await authClient.totp.verify(otp);
      return r.ok ? { ok: true } : { ok: false, error: r.error };
    },
    onDisable: async () => authClient.totp.disable(),
  }}
  passkeys={{
    items: passkeys,
    onAdd: async (name) => {
      const r = await authClient.passkey.add({ name });
      return r.ok ? { ok: true } : { ok: false, error: r.error };
    },
    onRename: (id, name) => authClient.passkey.rename(id, name),
    onRemove: (id) => authClient.passkey.remove(id),
  }}
  recentActivity={{
    fetchPage: (page, pageSize) => api.getSecurityActivity(page, pageSize),
  }}
  sessions={{
    items: sessions,
    onRevoke: (id) => api.revokeSession(id),
    onRevokeAllOthers: () => api.revokeAllSessions({ exceptCurrent: true }),
  }}
  linkedAccounts={{
    accounts: linked,
    linkableProviders: [{ id: "google", label: "Google", icon: <GoogleIcon /> }],
    onLink: (providerId) => window.location.assign(api.oauth(providerId)),
    onUnlink: (id) => api.unlinkAccount(id),
  }}
/>

OrganisationsScreen — table + create dialog

Table of orgs the user belongs to with a single 'Create organisation' action in the header. Built-in dialog asks for a name; the consumer decides whether to add a plan-selection step in front of the default flow.

Organisations

Manage all organisations you are currently associated with.

OrganisationRoleCreated
O

OAPPS

oapps

OwnerMay 21, 2026
A

Archi

archi-prod

AdminJun 03, 2026
<OrganisationsScreen
  organisations={orgs}
  onCreate={async ({ name }) => {
    const r = await api.createOrg({ name });
    if (!r.ok) return { ok: false, error: r.error };
    setOrgs((prev) => [...prev, r.org]);
    return { ok: true };
  }}
/>

ApiTokensScreen — create + one-time-secret reveal

Form at the top creates a token. On success, the consumer returns the plaintext secret — it's shown ONCE in a dismissible banner with a Copy button (try it). Below: existing tokens with destructive Revoke. The 'Never expire' switch toggles the expiration picker.

API Tokens

Create and manage API tokens. See our documentation for usage.

Never expire

Your tokens

CI deploy

Created May 02, 2026 · expires in 30d

<ApiTokensScreen
  tokens={tokens}
  onCreate={async ({ name, expiration }) => {
    const r = await api.createToken({ name, expiration });
    return r.ok
      ? { ok: true, secret: r.secret }
      : { ok: false, error: r.error };
  }}
  onRevoke={(id) => api.revokeToken(id)}
  docsUrl="https://docs.cip.io/api"
/>

WebhooksScreen — table + create dialog with multi-select triggers

Table of webhooks with status badge + per-row dropdown (Logs / Edit / Delete). The create dialog has a multi-select trigger picker, a show/hide secret input, and an enabled toggle in the URL row. Pass `availableTriggers` from your product.

Webhooks

Create webhooks to receive event notifications at your URL.

WebhookStatusListening toCreated

wh_01H8AB

https://example.com/webhook

Enabled2 eventsApr 02, 2026
<WebhooksScreen
  webhooks={hooks}
  availableTriggers={[
    "document.created",
    "document.sent",
    "document.signed",
    "audience.synced",
  ]}
  onCreate={async (input) => {
    const r = await api.createWebhook(input);
    return r.ok ? { ok: true } : { ok: false, error: r.error };
  }}
  onDelete={(id) => api.deleteWebhook(id)}
  onEdit={(hook) => openEditDialog(hook)}
  onViewLogs={(id) => router.push(`/settings/webhooks/${id}/logs`)}
/>

Variants

SecurityScreen — section presence

  • all sections(default)Pass twoFactor + passkeys + recentActivity + sessions + linkedAccounts — the canonical /settings/security layout used by the Cip / Archi / Supertest reference implementations.
  • subsetOmit any `*` prop block to hide that row entirely. Useful for products that haven't shipped a feature yet — invisible is better than placeholder.

OrganisationsScreen.onCreate

  • default dialog(default)Pass `onCreate` and the built-in name-only dialog renders. Resolves with `{ ok: true }` or `{ ok: false, error }`.
  • custom flowOmit `onCreate` to hide the create button entirely. Or open your own multi-step flow (plan selection + billing + name) from a button rendered ABOVE the screen — the screen still owns the table.

Anatomy

  • ProfileScreenAvatar uploader + Full name (editable) + Email (disabled, change via verify flow) + Update button. Below: optional DeleteAccountSection with email-typed confirm. Props: `value`, `onUpdate`, `onUploadAvatar?`, `onDelete?`.
  • SecurityScreenComposition of Password + TwoFactor + Passkeys + RecentActivity + Sessions + LinkedAccounts. Each is optional — omit a `*` prop block and the row disappears. Sub-views are managed internally: clicking 'Manage X' flips local state, the user gets a built-in '← Back to security' link. Sub-section types Passkey / Session / ActivityEvent / LinkedAccount / LinkableProvider are re-exported.
  • PasswordSectionSecurity > Password row. Card + dialog (current / new / confirm) with min-12 + match validation. Card-only (no sub-view), same shape as TwoFactorSection. When `hasPassword` is false the current-password field is hidden and the button reads 'Set password' (OAuth / passkey-only accounts). Outline button — changing a password is a state change, not a destructive action.
  • OrganisationsScreenHeader with optional 'Create organisation' button (built-in name-only dialog), then the orgs table. Pass a custom `renderRowAction` to surface per-row controls (Open / Leave / Settings).
  • ApiTokensScreenTop: create-token form (name + expiration with 'Never expire' switch). On create, the consumer returns the plaintext secret which the UI surfaces ONCE in a dismissible banner with a Copy button. Below: list of existing tokens with destructive Revoke action.
  • WebhooksScreenTable of webhooks (URL + status badge + trigger count + per-row dropdown) + create dialog with URL + multi-select trigger picker + secret (show/hide) + enabled toggle. `availableTriggers` is product-specific.
  • Reusable sections (composition primitive)When a product needs a Security-shaped page but only some rows (e.g. an internal admin tool with passkeys + sessions, no 2FA), drop in the individual sections instead of the whole screen: TwoFactorSection, PasskeysSection, SessionsSection, RecentActivitySection, LinkedAccountsSection, DeleteAccountSection. Each takes `view: 'card' | 'sub-view'` and toggles between the section-card row and its sub-view internally.

Guidelines

  • Use these screens for every Settings surface across OAPPS products (Archi, Cip, Supertest, future tools). They are the canonical recipes — same role as <LoginPage>/<SignUpPage> in @8maverik8/auth.
  • Treat callbacks as the integration surface. WebAuthn ceremonies, OTP verification, OAuth redirects, S3 signed-URL uploads — all of that lives in the consumer's callback. The screen stays UI-only.
  • Compose individual <PasskeysSection>, <SessionsSection>, etc. when you need a Security-shaped page but only some rows. Each section supports `view='card' | 'sub-view'`.
  • Render screens INSIDE <SettingsShell> + <SettingsSidebar>. The shell handles centering and chrome; the screen handles the content column.
  • Wrap screens in another <SettingsPageHeader>.Each screen owns its own h2 header. Double-headers are a clear sign the screen was nested incorrectly.
  • Re-implement a section the design system already ships (custom passkey table, custom create-token form, custom org table).Every consumer that's hand-rolled one has shipped a regression — wrong border radius, inconsistent padding, destructive variant on a non-destructive action. The whole point of the kit is that agents can't drift.
  • Use `variant='destructive'` for navigate-deeper actions like 'Sign out everywhere else', 'Manage sessions', 'View activity'.Destructive red is for irreversible actions only (Disable 2FA, Delete account, Revoke token, Unlink last account). Misusing it desensitises users — the next real destructive button reads as just another button.
  • Render the global app sidebar / top nav around the Settings flow.Settings is focused-mode (same as Auth). In Next.js: place under `app/(settings)/settings/` outside the `(app)` group that holds global chrome.