Settings & Access
A unified five-tab workspace settings area — Profile, Site Settings, Permissions, Payment, and Mail — sharing one cover-hero scaffold with a two-column body and a sticky rail.
The Settings hub is the product-style settings area a real SaaS app would have — five routes that read as one surface, sharing one scaffold: a cover-image profile hero with the hub tabs built in, a two-column body, and a sticky right rail. Everything is presentational (local state + a "Save changes" toast, no persistence), ready to be wired to a real API.
Overview
| Route | Page | What it demos |
|---|---|---|
/settings/profile | ProfilePage | Personal info, social profiles, change-password form. |
/settings | SiteSettingsPage | Workspace identity + logo upload, regional selects, appearance, feature toggles, danger zone. |
/settings/permissions | PermissionsPage | Role cards, an editable role×permission matrix, team-member list, invite modal. |
/settings/payment | PaymentPage | Plan hero with billing-cycle toggle, usage tiles, realistic credit-card visuals, billing history. |
/settings/mail | MailSettingsPage | Provider segmented control, SMTP / API-key configuration, send-a-test-email panel. |
Every tab renders inside the shared SettingsLayout scaffold: a PageHeader with a
presentational "Save changes" action, the ProfileHeader banner (cover-image hero + avatar +
identity, with the SettingsNav tabs integrated along its bottom edge), then a two-column body
(lg:grid-cols-[1fr_20rem]) — the tab's Panels on the left and the shared, sticky
SettingsAside rail (plan widget + help card) on the right.
Presentational by design
Forms hold local useState (or uncontrolled defaultValues); "Save changes", invites, card-adds,
and test-sends fire success toasts and nothing is written to localStorage or an API. The one real
integration is the Appearance section of Site Settings, which drives the actual Light / Dark /
System theme through useTheme().
Not the Layout Settings page
/layout-settings (see Customizer & Layout Settings) edits the
shell configuration (sidebar modes, skins, transitions) and persists via LayoutContext. The
Settings hub is a demo of app/workspace settings screens. They share the src/components/settings/
folder, but ShellPreview.tsx, ResetConfirmModal.tsx, and the tabs/ subfolder belong to Layout
Settings — only SettingsLayout / SettingsNav / ProfileHeader / SettingsAside are the hub's shell.
Architecture & files
| File | Responsibility |
|---|---|
src/components/settings/SettingsLayout.tsx | Shared scaffold: PageHeader + size="xs" "Save changes" button (fires settings:savedToast), ProfileHeader, and the two-column grid with the sticky (lg:sticky lg:top-32) aside. |
src/components/settings/SettingsNav.tsx | The hub tabs — a horizontal pill row of five NavLinks (icon + nav:* label; the /settings link uses end so it isn't active on subpages). |
src/components/settings/ProfileHeader.tsx | Shared banner on every tab: a cover-image hero (default /photos/photo-2.jpg) with a dark scrim, change-cover + change-photo file inputs, ringed avatar with a bg-success presence dot, name + role chip + meta chips, three AnimatedNumber stats, and SettingsNav below. |
src/components/settings/SettingsAside.tsx | Shared right rail: a plan widget (Pro badge, seats/storage UsageBars, "Manage billing" → /settings/payment) and a help card (Documentation → /faq, Contact support → /apps/chat, Keyboard shortcuts → the open-shortcuts window event). |
src/pages/settings/ProfilePage.tsx | Personal information, social profiles, and password panels. |
src/pages/settings/SiteSettingsPage.tsx | General (logo upload ≤ 1 MB, workspace name / support email / tagline), Regional selects, Appearance (theme-mode tiles via useTheme), feature Switch rows, and the danger-zone panel + confirm Modal. |
src/pages/settings/PermissionsPage.tsx | Role overview cards, the permission matrix, the team-member list with per-member role Select, and the invite Modal. |
src/pages/settings/PaymentPage.tsx | Plan hero + billing summary, usage tiles, CardFace payment-method visuals, add-card Modal, and the billing-history table. |
src/pages/settings/MailSettingsPage.tsx | Provider segmented control, SMTP-vs-API config branch, sender identity, and the send-test panel. |
src/data/settings.ts | All demo data: TIMEZONES, DATE_FORMATS, FEATURE_TOGGLES, ROLES, PERMISSION_GROUPS + defaultMatrix(), MEMBERS, SAVED_CARDS, BILLING_HISTORY, MAIL_PROVIDERS, ENCRYPTION_OPTS. |
src/locales/en/settings.json | The settings i18n namespace; tab titles reuse nav:* keys. |
No persistence
There is no STORAGE_KEY and nothing registered in src/lib/appStorage.ts — the hub keeps no
persisted state. Avatar / cover / logo uploads become in-memory data URLs; edited fields, matrix
cells, member roles, and saved cards live in component useState and reset on navigation. Currency
amounts on the Payment page use the shared formatMoney(amount, currency, locale) from
src/data/invoice.ts, and the Regional currency Select reuses its CURRENCIES list. The Payment
billing summary derives its figures (days left, period progress, next payment date) from today's
date so the demo always looks current.
Usage
The hub is already wired in — the five leaves live under Settings → Settings in the sidebar, and
the routes are plain (non-lazy) entries in src/routes.tsx:
{path: '/settings/profile', element: <ProfilePage />},
{path: '/settings', element: <SiteSettingsPage />},
{path: '/settings/permissions', element: <PermissionsPage />},
{path: '/settings/payment', element: <PaymentPage />},
{path: '/settings/mail', element: <MailSettingsPage />},Each page is just SettingsLayout + Panels, so a new tab is a few lines:
import {useTranslation} from '@/platform/i18n'
import {Panel} from '@/components/ui'
import {SettingsLayout} from '@/components/settings/SettingsLayout'
export function NotificationsSettingsPage() {
const {t} = useTranslation('settings')
return (
<SettingsLayout title={t('nav:notificationSettings')}>
<Panel title={t('settings:notifTitle')} subtitle={t('settings:notifHint')}>
{/* form controls */}
</Panel>
</SettingsLayout>
)
}Create the page
Compose SettingsLayout + Panels (every Panel needs title and subtitle).
Register the route
Add an appRoutes entry in src/routes.tsx.
Add the nav item
Add the menu leaf under the Settings group in src/data/menu.ts + its nav: label.
Add the hub tab
Append the tab to ITEMS in src/components/settings/SettingsNav.tsx.
API / Props
SettingsLayoutProps
| Prop | Type | Description |
|---|---|---|
title | string | The PageHeader title (each page passes its nav:* label). |
children | ReactNode | The tab's content — Panels rendered in the main (left) column. |
The layout itself owns the "Save changes" action (a size="xs" Button that toasts
settings:savedToast), the banner, and the aside — pages provide only their panels.
SettingsNav tab registry
SettingsNav renders a fixed ITEMS array — {to, navKey, icon, end?} per tab:
to | navKey | Icon | Notes |
|---|---|---|---|
/settings/profile | nav:profile | UserRound | |
/settings | nav:siteSettings | Settings2 | end: true (exact-match only) |
/settings/permissions | nav:permissions | ShieldCheck | |
/settings/payment | nav:paymentSettings | CreditCard | |
/settings/mail | nav:mailSettings | Mail |
Demo data (src/data/settings.ts)
| Export | Type | Used by |
|---|---|---|
TIMEZONES / DATE_FORMATS | {value; label}[] | Site Settings → Regional Selects (labels are literal/technical). |
FEATURE_TOGGLES | FeatureToggle[] (id, labelKey, descKey, on) | Site Settings → Features Switch rows. |
ROLES | Role[] (id, labelKey, members, locked?) | Permissions — role cards, matrix columns, role selects (owner is locked). |
PERMISSION_GROUPS | PermissionGroup[] (id, labelKey, defaults) | Permissions — matrix rows; defaults lists role ids granted by default. |
defaultMatrix() | () => Record<string, Record<string, boolean>> | Builds matrix[groupId][roleId] from the groups' defaults. |
MEMBERS | Member[] | Permissions — team-member list. |
SAVED_CARDS | SavedCard[] | Payment — CardFace visuals. |
BILLING_HISTORY | InvoiceRow[] | Payment — billing-history table. |
MAIL_PROVIDERS | {id; labelKey?; label?}[] | Mail — segmented control (smtp uses an i18n key; SendGrid / Mailgun / Postmark are literal brand names). |
ENCRYPTION_OPTS | {value; label}[] | Mail — SMTP encryption Select (None / SSL / TLS). |
Per-page behaviors worth knowing
- Permissions matrix — one
Checkboxper group×role cell; the Owner column is locked (role.locked→disabled) with anaria-labelof "permission — role". The owner member's roleSelectis disabled and their remove button is hidden. - Payment
CardFace— a realistic card mockup: per-brand gradient face (BRAND_STYLE: Visa blue, Mastercard near-black, slate fallback), a drawn EMVCardChip, contactless glyph,brand.nameas the issuer line, masked number fromlast4, holder + expiry, and aBrandLogo. Non-primary cards get Make default and remove actions; the dashed add tile opens the add-cardModal. - Mail config branch —
provider === 'smtp'renders host / port / encryption / username / password; any other provider renders a single API-key field. From-name / from-email are shared below. - Site Settings danger zone — a
headerVariant="muted",border-danger/30Panelwhose delete action opens asize="sm"confirmModal(both buttons just close it — presentational).
Configuration & customization
Edit roles & permissions
Both are data-driven from src/data/settings.ts. A new role is a Role entry (its column,
role-select option, and matrix cells all derive automatically); a new permission is a
PermissionGroup with the role ids that get it by default:
// src/data/settings.ts
export const PERMISSION_GROUPS: PermissionGroup[] = [
// …
{id: 'manageApiKeys', labelKey: 'settings:permManageApiKeys', defaults: ['owner', 'admin']},
]// src/locales/en/settings.json
{"permManageApiKeys": "Manage API keys"}The matrix row, checkboxes, and default checks appear automatically (via defaultMatrix()).
Wire a form to a real API
import {api} from '@/lib/api'
const onSave = async () => {
await api.put('/workspace/settings', {name, tagline, email})
toast({title: t('settings:savedToast'), tone: 'success'})
}A note on colors
Everything themable uses semantic tokens so all tabs re-skin and dark-mode automatically. The
only deliberate exceptions are decorative branded graphics: the CardFace gradients / gold EMV
chip and the Mastercard circles (#eb001b / #f79e1b) are intentionally fixed, and the cover-hero
scrim/chips use white/black overlays because they sit on a photograph.
Best practices
- Keep pages as
Panelcompositions. Every content section is aPanelwithtitle+subtitle; page files stay wiring-only. - Route theme changes through
useTheme. The Appearance tiles callsetModeon the shared theme store — never shadow it with local state. - Data in
src/data/settings.ts, copy in thesettingsnamespace. Proper nouns (SendGrid, Visa), technical values (ports, timezone labels), and card numbers stay literal. - If you add persistence, register the key in
APP_LOCAL_KEYS(src/lib/appStorage.ts). - Reuse the shared money formatter.
formatMoneyfromsrc/data/invoice.tsis the repo's onlyIntl.NumberFormatcurrency helper.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
The Site Settings tab lights up on every /settings/* page | Its NavLink needs end: true — /settings is a path prefix of all the others. |
| Edits vanish when switching tabs | By design — state is local per page and nothing persists. |
| "Save changes" doesn't save anything | It is presentational: a success toast only. |
| Owner checkboxes / role select / remove can't be changed | Deliberate — the owner role is locked in ROLES and the owner member is guarded in PermissionsPage. |
| The right rail scrolls away / overlaps the sticky page header | The aside is lg:sticky lg:top-32 — tuned to clear the app header + slim PageHeader. Adjust the offset if header heights change. |
| Avatar / cover / logo upload does nothing | Files over the size cap are silently ignored (cover ≤ 2 MB, avatar/logo ≤ 1 MB) and only image/* is accepted. |
| Card faces don't re-skin | Intentional — BRAND_STYLE gradients are fixed decorative brand graphics, not tokens. |
FAQ
How is this different from /layout-settings? The Settings hub demos application settings and
is presentational. Layout Settings edits the real shell configuration and persists it via
LayoutContext.
Does anything here persist? Only the theme mode changed from the Appearance section (through
useTheme, localStorage('theme')). Everything else resets on navigation.
Where do the Payment page's dates and totals come from? Computed from today's date at render and
from the cycle toggle ($29 monthly / $24 annual per seat × 12 seats), so the demo never looks stale.
Can members actually be invited / removed? Removals and role changes update local state; the
invite modal validates nothing and just toasts settings:invitedToast.
Why is the profile banner shown on every tab? It doubles as the hub's frame — the tabs are integrated along its bottom strip, so the five pages read as one settings area.
Related
Was this page helpful?
