Settings Hub
A unified, five-tab workspace settings area — Profile, Site Settings, Permissions, Payment, and Mail — sharing one scaffold: a cover-image profile hero with the hub tabs built in, a two-column body, and a sticky right rail.
Overview
The Settings hub is the product-style settings area a real SaaS app would have — five routes that read as one surface:
| 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.
This hub is deliberately presentational. 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 (documented in
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 the settings:savedToast toast), ProfileHeader, and the two-column grid with the sticky (lg:sticky lg:top-32) aside. |
src/components/settings/SettingsNav.tsx | The hub tabs — a bare horizontal pill row of five NavLinks (icon + nav:* label, overflow-x-auto; the /settings link uses end so it isn't active on subpages). Its card frame comes from ProfileHeader. |
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 (data-URL, size-capped), ringed avatar with a bg-success presence dot, name + role chip + meta chips, three AnimatedNumber stats, and SettingsNav on the surface strip 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 → dispatches the open-shortcuts window event). |
src/pages/settings/ProfilePage.tsx | Personal information, social profiles, and password panels (Update password → toast). |
src/pages/settings/SiteSettingsPage.tsx | General (logo upload ≤ 1 MB, workspace name / support email / tagline), Regional selects, Appearance (theme-mode tiles via useTheme + "More options" → /layout-settings), 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 (+ their types). |
src/locales/en/settings.json | The settings i18n namespace (registered in src/i18n/languages.ts + en/manifest.json); tab titles reuse nav:* keys. |
Presentational by design
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 hub adds no duplicate formatters. 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
(src/data/menu.ts), and each is a plain route:
{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>
)
}Register it in three places (the standard real-page recipe, see
Architecture & Routing) plus the hub tab row:
a route for it, a menu leaf in src/data/menu.ts, the nav: label, and an
entry in ITEMS in src/components/settings/SettingsNav.tsx.
API / Props
SettingsLayoutProps (SettingsLayout.tsx)
| 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[] (id, name, email, avatar?, role) | Permissions — team-member list. |
SAVED_CARDS | SavedCard[] (id, brand, last4, exp, primary?) | Payment — CardFace visuals. |
BILLING_HISTORY | InvoiceRow[] (id, date, amount, statusKey) | 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) and each cell carries anaria-labelof "permission — role". The owner member's roleSelectis disabled and their remove button is hidden (invisible). - 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(VISA wordmark / Mastercard interlocking circles). 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 the branch. - Site Settings danger zone — a
headerVariant="muted",border-danger/30Panelwhose delete action opens asize="sm"confirmModal(both buttons just close it — presentational).
Configuration & customization
Add a hub tab
- Create the page (compose
SettingsLayout+Panels — everyPanelneedstitleandsubtitle). - Register the route.
- Add the menu leaf under the Settings group in
src/data/menu.ts+ itsnav:label. - Append the tab to
ITEMSinsrc/components/settings/SettingsNav.tsx.
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. Add the settings:role* / settings:perm* labels (role cards also
read a <labelKey>Desc key for the description line).
Feature toggles, providers, cards, history
Same pattern — edit FEATURE_TOGGLES, MAIL_PROVIDERS, SAVED_CARDS, or BILLING_HISTORY and the
UI follows. Card brands map to a gradient in BRAND_STYLE (PaymentPage.tsx); unknown brands fall
back to the slate face.
Identity & imagery
The banner identity (name "Alex Morgan", role chip, location / company / joined chips, the three
stats) is literal demo content in ProfileHeader.tsx (STATS + inline JSX); the default cover is
/photos/photo-2.jpg and the default avatar /avatars/avatar-1.png (self-hosted, like all avatars).
The plan widget's "Pro / $29/mo" and usage figures are literal in SettingsAside.tsx.
A note on colors
Everything themable uses semantic tokens (bg-surface, border-border, bg-primary,
text-muted-foreground, danger tints for the danger zone) 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 —
a credit card doesn't change color with the app skin — and the cover-hero scrim/chips use white/black
overlays because they sit on a photograph.
Examples
Add a permission group
// 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
Replace the toast in your page (or in SettingsLayout's Save action) with a call through the shared
HTTP client:
import {api} from '@/lib/api'
const onSave = async () => {
await api.put('/workspace/settings', {name, tagline, email})
toast({title: t('settings:savedToast'), tone: 'success'})
}Best practices
- Keep pages as
Panelcompositions. Every content section is aPanelwithtitle+subtitle(the project-wide convention — see Panel); page files stay wiring-only. - Route theme changes through
useTheme. The Appearance tiles callsetModeon the shared theme store — never shadow it with local state, or the header toggle / customizer drift out of sync. - Data in
src/data/settings.ts, copy in thesettingsnamespace. Rows/roles/cards are data entries; user-facing labels are i18n keys resolved at render. Proper nouns (SendGrid, Visa), technical values (ports, timezone labels), and card numbers stay literal. - If you add persistence, register the key. The hub currently persists nothing; the moment you add
a
localStoragekey, add it toAPP_LOCAL_KEYSinsrc/lib/appStorage.tsso "Reset to defaults" clears it. - Reuse the shared money formatter.
formatMoneyfromsrc/data/invoice.tsis the repo's onlyIntl.NumberFormatcurrency helper — don't inline another.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
The Site Settings tab lights up on every /settings/* page | Its NavLink needs end: true (it has it in ITEMS) — /settings is a path prefix of all the others. |
| Edits vanish when switching tabs | By design — state is local per page and nothing persists. Wire the forms to your API for real behavior. |
| "Save changes" doesn't save anything | It is presentational: a success toast only. See the API example above. |
| 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. Remove the guards if your model differs. |
| 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 you change header heights. |
| 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
(profile, workspace, roles, billing, email) and is presentational. Layout Settings edits the real
shell configuration and persists it via LayoutContext. See
Customizer & Layout Settings.
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
(billing-period progress, days left, next payment via Intl.DateTimeFormat in the active language)
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 (so the
list responds live); the invite modal validates nothing and just toasts settings:invitedToast.
Why is the profile banner shown on every tab, not just Profile? It doubles as the hub's frame — the tabs are integrated along its bottom strip, so the five pages read as one settings area.
Notes for designers & content editors
- All hub copy — panel titles/hints, labels, toasts, role and permission names — lives in
src/locales/<lng>/settings.json; tab labels come from thenavnamespace. Edit copy there, never in the components. (English + Japanese ship at parity.) - Demo identity ("Alex Morgan", "Northwind Studio", emails, member names, invoice ids/dates) is
literal demo content in
src/data/settings.tsand the page/ProfileHeaderJSX — safe to replace wholesale for a preview. - Imagery: the cover photo is
/photos/photo-2.jpg, avatars come from/avatars/avatar-N.png— swap the files or the paths; uploads only preview in-memory. - Colors come from semantic tokens and re-skin/dark-mode automatically, except the deliberately fixed credit-card faces and the photo-overlay whites in the banner.
Related
Customizer & Layout Settings
the other settings surface (shell config); shares the src/components/settings/ folder
Panel
the portlet every section uses (title + subtitle mandatory)
Forms
Field, Input, Select, Checkbox, Switch, Textarea
Overlays & Disclosure
the Modals (danger zone, invite, add-card)
Core & Feedback
Avatar, Badge, Button, Toast
Layout System
PageHeader, sticky offsets, avatars
Design Tokens & Dark Mode
the semantic tokens used throughout
i18n
namespaces, adding keys, the translate tool
Architecture & Routing
route registration and the add-a-page recipe
Was this page helpful?
