|
| 1 | +--- |
| 2 | +paths: |
| 3 | + - "apps/sim/app/workspace/*/settings/**" |
| 4 | + - "apps/sim/ee/**/components/**" |
| 5 | +--- |
| 6 | + |
| 7 | +# Settings Pages |
| 8 | + |
| 9 | +Every settings page renders through the shared **`SettingsPanel`** primitive |
| 10 | +(`@/app/workspace/[workspaceId]/settings/components/settings-panel`). It owns the |
| 11 | +page chrome so pages never hand-roll it: a fixed header bar (right-aligned |
| 12 | +actions), a scroll region, and a centered `max-w-[48rem]` content column led by a |
| 13 | +**title + description that come from navigation metadata**. Pages render only |
| 14 | +their body. |
| 15 | + |
| 16 | +Do NOT hand-roll any of these in a settings page — they are the panel's job: |
| 17 | + |
| 18 | +- `<div className='flex h-full flex-col bg-[var(--bg)]'>` shell |
| 19 | +- the header bar (`flex flex-shrink-0 … px-[16px] pt-[8.5px] pb-[8.5px]`) |
| 20 | +- the scroll container (`min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]`) |
| 21 | +- the content column (`mx-auto … max-w-[48rem] … gap-7`) |
| 22 | +- a title block (`<h1 className='font-medium text-[var(--text-body)] text-lg'>` + `<p className='text-[var(--text-muted)] text-md'>`) |
| 23 | +- the page-level search input |
| 24 | + |
| 25 | +## Canonical page shape |
| 26 | + |
| 27 | +```tsx |
| 28 | +import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' |
| 29 | + |
| 30 | +return ( |
| 31 | + <SettingsPanel |
| 32 | + actions={ |
| 33 | + <Chip leftIcon={Plus} variant='primary' onClick={onCreate}> |
| 34 | + Create |
| 35 | + </Chip> |
| 36 | + } |
| 37 | + search={{ value: searchTerm, onChange: setSearchTerm, placeholder: 'Search …' }} |
| 38 | + > |
| 39 | + {/* body only — sections, lists, forms */} |
| 40 | + </SettingsPanel> |
| 41 | +) |
| 42 | +``` |
| 43 | + |
| 44 | +When the page has modal/dialog siblings, wrap them with the panel in a fragment: |
| 45 | + |
| 46 | +```tsx |
| 47 | +return ( |
| 48 | + <> |
| 49 | + <SettingsPanel actions={…}>{body}</SettingsPanel> |
| 50 | + <SomeModal … /> |
| 51 | + </> |
| 52 | +) |
| 53 | +``` |
| 54 | + |
| 55 | +## `SettingsPanel` props |
| 56 | + |
| 57 | +- `actions?: ReactNode` — right-aligned header chips. Wrap multiple in a fragment; |
| 58 | + the slot reserves the 30px chip height even when empty, so vertical rhythm is |
| 59 | + identical across pages. Conditional actions are fine: `actions={canManage && <Chip…/>}`. |
| 60 | +- `search?: { value; onChange: (value: string) => void; placeholder?; disabled? }` — |
| 61 | + renders the canonical search field directly below the title. Pass `setSearchTerm` |
| 62 | + straight to `onChange`. Use this for a standalone search; if search shares a row |
| 63 | + with other controls (sort, filters, a date picker), render that whole row in |
| 64 | + `children` instead and omit the prop. |
| 65 | +- `title?` / `description?` — overrides for the nav-driven defaults. **Only** for a |
| 66 | + detail sub-view that needs a different heading; normal pages never pass these. |
| 67 | +- `scrollContainerRef?: React.Ref<HTMLDivElement>` — forwards a ref to the scroll |
| 68 | + region (e.g. programmatic scroll-to-bottom). |
| 69 | +- `contentClassName?` — layout/spacing only; reach for it rarely. Prefer the |
| 70 | + default `gap-7`. |
| 71 | + |
| 72 | +## Title + description live in navigation metadata |
| 73 | + |
| 74 | +`apps/sim/app/workspace/[workspaceId]/settings/navigation.ts` is the single source |
| 75 | +of truth. Every `NavigationItem` carries a one-line `description`; `SettingsPanel` |
| 76 | +resolves both via `getSettingsSectionMeta(section)` and the |
| 77 | +`SettingsSectionProvider` the settings shell wraps around the active section. |
| 78 | + |
| 79 | +Adding a new settings page: |
| 80 | + |
| 81 | +1. Add the `SettingsSection` id + a `NavigationItem` (with `label` **and** |
| 82 | + `description`) in `navigation.ts`. Keep descriptions verb-first, one line, |
| 83 | + ~40–55 chars, in the product voice (see `.claude/rules/constitution.md`). |
| 84 | +2. Render the component inside the shell's `effectiveSection` switch in |
| 85 | + `settings/[section]/settings.tsx`. |
| 86 | +3. Build the component body inside `<SettingsPanel>` — no shell, no title block. |
| 87 | + |
| 88 | +## Other shared settings primitives (do not re-roll these) |
| 89 | + |
| 90 | +- **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical |
| 91 | + muted status message. `variant='fill'` (default) centers in the available |
| 92 | + height (empty list, or a not-entitled/loading gate); `variant='inline'` sits in |
| 93 | + flow (a search "no results"). Never hand-roll |
| 94 | + `<div className='flex h-full items-center justify-center text-[var(--text-muted)] text-sm'>` |
| 95 | + or `<div className='py-4 text-center …'>`. It owns the `--text-muted` + `text-sm` |
| 96 | + tokens, so it also keeps these messages consistent across pages. |
| 97 | +- **`RowActionsMenu`** (`…/components/row-actions-menu`) — the trailing `...` |
| 98 | + actions menu for a list row. Pass `label` (aria-label) and |
| 99 | + `actions: RowAction[]` (`{ label, onSelect, destructive?, disabled? }`); the |
| 100 | + component renders the canonical flush `...` trigger + `DropdownMenuContent`. |
| 101 | + Conditional items become array spreads: `...(canManage ? [{…}] : [])`. Never |
| 102 | + hand-roll the `<DropdownMenu>` + `<MoreHorizontal>` trigger per page. |
| 103 | + |
| 104 | +## Detail sub-views (the one exception) |
| 105 | + |
| 106 | +A drill-down view reached from a list row (selected MCP server, workflow MCP |
| 107 | +server, credential set, permission group) keeps its **own** chrome because it |
| 108 | +needs a left-aligned back button (`<Chip leftIcon={ArrowLeft}>`), which the panel |
| 109 | +header (right-actions only) does not model. Leave those returns as hand-rolled |
| 110 | +shells; only the list/main view uses `SettingsPanel`. Gate/early-return states |
| 111 | +(not-entitled, loading, upgrade prompts) also stay as-is. |
| 112 | + |
| 113 | +## Audit checklist |
| 114 | + |
| 115 | +A settings page is design-system-clean when: |
| 116 | + |
| 117 | +- [ ] Its main return is a `<SettingsPanel>` (or `<>…<SettingsPanel>…</>` with modal siblings) — no hand-rolled shell/header/scroll/column. |
| 118 | +- [ ] It renders **no** hand-rolled `<h1>`/description title block — the title comes from nav metadata. |
| 119 | +- [ ] Header chips are in `actions`; a standalone search is in the `search` prop. |
| 120 | +- [ ] Its `NavigationItem` has an accurate, consistent-length `description`. |
| 121 | +- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional). |
| 122 | +- [ ] No business logic, handlers, or conditional rendering changed by the migration. |
| 123 | +- [ ] `tsc`, `biome`, and the page's tests pass. |
0 commit comments