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.
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.
| Organisation | Role | Created |
|---|---|---|
O OAPPS oapps | Owner | May 21, 2026 |
A Archi archi-prod | Admin | Jun 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.
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.
| Webhook | Status | Listening to | Created | |
|---|---|---|---|---|
wh_01H8AB https://example.com/webhook | Enabled | 2 events | Apr 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.