Settings Page (add / audit)
Settings page chrome (header bar, scroll region, content column, nav-driven
title + description) is owned by the settings/[section]/layout.tsx shell. Each
page renders through SettingsPanel, which registers the page's header data
(actions, search, back) with that shell and renders only the body. The full
convention lives in .claude/rules/sim-settings-pages.md — read it first; this
skill is the procedure.
Key paths:
- Chrome shell:
apps/sim/app/workspace/[workspaceId]/settings/[section]/layout.tsx (SettingsHeaderShell)
SettingsPanel registrar: apps/sim/components/settings/settings-panel.tsx
- Nav metadata (titles + descriptions):
apps/sim/components/settings/navigation.ts
- Section switch + provider:
apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
- Pages:
apps/sim/app/workspace/[workspaceId]/settings/components/<name>/<name>.tsx and EE pages under apps/sim/ee/<feature>/components/
Mode A — Add a new settings page
- Navigation. In
navigation.ts: add the id to the SettingsSection union,
then a NavigationItem with label AND a one-line description (verb-first,
~40–55 chars, product voice per .claude/rules/constitution.md). Place it in
the right section group and set any gating flags (requiresHosted,
requiresEnterprise, etc.).
- Wire the switch. Add the component to the
effectiveSection render switch
in settings/[section]/settings.tsx (lazy dynamic(...) like its siblings).
- Build the body inside
SettingsPanel per the rule's canonical page shape:
actions, search, children, modal siblings in a fragment.
- If the page has editable state, wire the shared save/discard stack — put
SaveDiscardActions (dirty-gated Discard+Save chips) in actions, and call
useSettingsUnsavedGuard({ isDirty }) before any early-return gate.
Detail sub-views additionally route the back chip through
guard.guardBack(closeFn) and render the shared UnsavedChangesModal. Never
hand-roll a Save button, a beforeunload, or an "Unsaved changes" modal —
they're centralized. See the "Save / Discard + unsaved-changes guard" section
in .claude/rules/sim-settings-pages.md.
- Verify:
cd apps/sim && bun run type-check; bunx biome check --write <file>.
Mode B — Audit existing settings pages
For each page component, confirm the checklist in .claude/rules/sim-settings-pages.md:
- Find hand-rolled shells that should be
SettingsPanel:
git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'
— every match should be either settings-panel.tsx, a detail sub-view
(has a <Chip leftIcon={ArrowLeft}> back button), or an entitlement/loading
gate early-return. Anything else is a page that still needs migrating.
- Find hand-rolled title blocks (should be 0 outside detail views):
git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/' 'apps/sim/ee/'
- Find literal pixel text sizes (should be 0 — see "Text-scale tokens" in
.claude/rules/sim-settings-pages.md for the token map and the row
title/subtitle pairing convention):
git grep -nE "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/' — should
be 0. Display type above the scale (text-[40px] hero headings, the text-[8px]
member-avatar initial) is deliberate and out of scope.
- Confirm each page imports
SettingsPanel and that its NavigationItem has an
accurate description of consistent length with its peers.
- Editable pages: confirm Save/Discard go through
SaveDiscardActions and
dirty is wired via useSettingsUnsavedGuard (called before early-return
gates) — flag any hand-rolled Save button, beforeunload, or unsaved modal.
git grep -n "beforeunload" -- 'apps/sim/**/settings/' 'apps/sim/ee/'
should only hit the centralized use-settings-before-unload.ts.
- When migrating a page, change ONLY the structural shell→
SettingsPanel swap:
move header chips to actions, the standalone search to search, delete the
<h1> title block, replace the three closing </div> (column/scroll/shell)
with </SettingsPanel>, and keep modal siblings in a <> fragment. Do NOT
touch handlers, state, queries, conditional rendering, or detail/gate returns.
Drop per-page gap-*/pt-* on the content column in favor of the panel default.
- When fixing literal pixel text sizes, replace ONLY the size class with its
exact-pixel-equivalent named token (e.g.
text-[12px] → text-caption,
never a different size) — this must render pixel-identical, not restyle the
page. Leave color tokens (--text-primary vs --text-body, etc.) untouched
unless they're also being changed for an unrelated, deliberate reason.
- Remove now-unused imports (
ChipInput/Search) ONLY after grepping that
they are not still used elsewhere in the file (e.g. by a detail view).
- Verify the whole sweep:
bun run type-check, biome check on every touched
file, and run the affected pages' tests. Diff each file against the base and
confirm the change is purely structural before shipping.
Mode C — Migrate list rows to SettingsResourceRow
Read "The resource row" in .claude/rules/sim-settings-pages.md first — it is the
contract. Then, per page:
- Find hand-rolled rows:
git grep -n "truncate text-\[var(--text-body)\] text-sm" -- 'apps/sim/app/workspace/' 'apps/sim/ee/'
Every match outside settings-resource-row.tsx is either a row to migrate or a
genuinely different shape (multi-line body, tabular columns, a grid) that stays
bespoke — decide which, and say so.
- Replace the row and its wrapper: a
<button>/<Link> around the row becomes
onClick/href on the row itself. Wrapping the row is what the primitive
exists to stop — it is also invalid HTML once trailing holds a control.
- Sort the trailing content: interactive →
trailing, decorative → badge.
Getting this backwards makes the row's right edge a dead zone.
- Add
navigable only if the row opens a detail page, and clickLabel always.
- Drop the container's
-mx-2 — the row now owns the bleed. Use
RESOURCE_LIST_STACK / RESOURCE_LIST_GRID; do not hand-write the gap.
- Unlike Mode B, this migration may change conditional rendering: a
<button disabled={!can}> becomes onClick={can ? … : undefined} +
navigable={can}, which renders a plain non-interactive row. Verify the gated
state has no clickable affordance left.
- Check what the old row rendered beside the title (a badge, a timestamp, a
transport label). The row's title truncates as one unit, so anything folded
into it can be ellipsised away — move it to
description or badge.
- Verify:
bun run type-check, biome check, the page's tests, and a diff read of
every converted block for lost props, conditions, and key placement.
1---2name: add-settings-page3description: Add a new Sim settings page, or audit existing settings pages for design-system compliance with the shared SettingsPanel layout. Use when creating a settings tab, or when asked to check/clean up settings pages so they match the design system (consistent title, header, search, spacing).4---5
6# Settings Page (add / audit)
7
8Settings page chrome (header bar, scroll region, content column, nav-driven
9title + description) is owned by the `settings/[section]/layout.tsx` shell. Each
10page renders through **`SettingsPanel`**, which registers the page's header data
11(actions, search, back) with that shell and renders only the body. The full
12convention lives in `.claude/rules/sim-settings-pages.md` — read it first; this
13skill is the procedure.
14
15Key paths:
16- Chrome shell: `apps/sim/app/workspace/[workspaceId]/settings/[section]/layout.tsx` (`SettingsHeaderShell`)
17- `SettingsPanel` registrar: `apps/sim/components/settings/settings-panel.tsx`
18- Nav metadata (titles + descriptions): `apps/sim/components/settings/navigation.ts`
19- Section switch + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
20- Pages: `apps/sim/app/workspace/[workspaceId]/settings/components/<name>/<name>.tsx` and EE pages under `apps/sim/ee/<feature>/components/`
21
22## Mode A — Add a new settings page
23
241. **Navigation.** In `navigation.ts`: add the id to the `SettingsSection` union,
25 then a `NavigationItem` with `label` AND a one-line `description` (verb-first,
26 ~40–55 chars, product voice per `.claude/rules/constitution.md`). Place it in
27 the right `section` group and set any gating flags (`requiresHosted`,
28 `requiresEnterprise`, etc.).
292. **Wire the switch.** Add the component to the `effectiveSection` render switch
30 in `settings/[section]/settings.tsx` (lazy `dynamic(...)` like its siblings).
313. **Build the body inside `SettingsPanel`** per the rule's canonical page shape:
32 `actions`, `search`, `children`, modal siblings in a fragment.
334. **If the page has editable state**, wire the shared save/discard stack — put
34 `SaveDiscardActions` (dirty-gated Discard+Save chips) in `actions`, and call
35 `useSettingsUnsavedGuard({ isDirty })` **before any early-return gate**.
36 Detail sub-views additionally route the back chip through
37 `guard.guardBack(closeFn)` and render the shared `UnsavedChangesModal`. Never
38 hand-roll a Save button, a `beforeunload`, or an "Unsaved changes" modal —
39 they're centralized. See the "Save / Discard + unsaved-changes guard" section
40 in `.claude/rules/sim-settings-pages.md`.
415. **Verify:** `cd apps/sim && bun run type-check`; `bunx biome check --write <file>`.
42
43## Mode B — Audit existing settings pages
44
45For each page component, confirm the checklist in `.claude/rules/sim-settings-pages.md`:
46
471. Find hand-rolled shells that should be `SettingsPanel`:
48 `git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
49 — every match should be either `settings-panel.tsx`, a **detail sub-view**
50 (has a `<Chip leftIcon={ArrowLeft}>` back button), or an entitlement/loading
51 **gate** early-return. Anything else is a page that still needs migrating.
522. Find hand-rolled title blocks (should be 0 outside detail views):
53 `git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
543. Find literal pixel text sizes (should be 0 — see "Text-scale tokens" in
55 `.claude/rules/sim-settings-pages.md` for the token map and the row
56 title/subtitle pairing convention):
57 `git grep -nE "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'` — should
58 be 0. Display type above the scale (`text-[40px]` hero headings, the `text-[8px]`
59 member-avatar initial) is deliberate and out of scope.
604. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
61 accurate `description` of consistent length with its peers.
62 - Editable pages: confirm Save/Discard go through `SaveDiscardActions` and
63 dirty is wired via `useSettingsUnsavedGuard` (called before early-return
64 gates) — flag any hand-rolled Save button, `beforeunload`, or unsaved modal.
65 `git grep -n "beforeunload" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
66 should only hit the centralized `use-settings-before-unload.ts`.
675. When migrating a page, change ONLY the structural shell→`SettingsPanel` swap:
68 move header chips to `actions`, the standalone search to `search`, delete the
69 `<h1>` title block, replace the three closing `</div>` (column/scroll/shell)
70 with `</SettingsPanel>`, and keep modal siblings in a `<>` fragment. Do NOT
71 touch handlers, state, queries, conditional rendering, or detail/gate returns.
72 Drop per-page `gap-*`/`pt-*` on the content column in favor of the panel default.
736. When fixing literal pixel text sizes, replace ONLY the size class with its
74 exact-pixel-equivalent named token (e.g. `text-[12px]` → `text-caption`,
75 never a different size) — this must render pixel-identical, not restyle the
76 page. Leave color tokens (`--text-primary` vs `--text-body`, etc.) untouched
77 unless they're also being changed for an unrelated, deliberate reason.
787. Remove now-unused imports (`ChipInput`/`Search`) ONLY after grepping that
79 they are not still used elsewhere in the file (e.g. by a detail view).
808. **Verify the whole sweep:** `bun run type-check`, `biome check` on every touched
81 file, and run the affected pages' tests. Diff each file against the base and
82 confirm the change is purely structural before shipping.
83
84## Mode C — Migrate list rows to `SettingsResourceRow`
85
86Read "The resource row" in `.claude/rules/sim-settings-pages.md` first — it is the
87contract. Then, per page:
88
891. Find hand-rolled rows:
90 `git grep -n "truncate text-\[var(--text-body)\] text-sm" -- 'apps/sim/app/workspace/' 'apps/sim/ee/'`
91 Every match outside `settings-resource-row.tsx` is either a row to migrate or a
92 genuinely different shape (multi-line body, tabular columns, a grid) that stays
93 bespoke — decide which, and say so.
942. Replace the row *and* its wrapper: a `<button>`/`<Link>` around the row becomes
95 `onClick`/`href` on the row itself. Wrapping the row is what the primitive
96 exists to stop — it is also invalid HTML once `trailing` holds a control.
973. Sort the trailing content: interactive → `trailing`, decorative → `badge`.
98 Getting this backwards makes the row's right edge a dead zone.
994. Add `navigable` only if the row opens a detail page, and `clickLabel` always.
1005. Drop the container's `-mx-2` — the row now owns the bleed. Use
101 `RESOURCE_LIST_STACK` / `RESOURCE_LIST_GRID`; do not hand-write the gap.
1026. Unlike Mode B, this migration **may** change conditional rendering: a
103 `<button disabled={!can}>` becomes `onClick={can ? … : undefined}` +
104 `navigable={can}`, which renders a plain non-interactive row. Verify the gated
105 state has no clickable affordance left.
1067. Check what the old row rendered *beside* the title (a badge, a timestamp, a
107 transport label). The row's title truncates as one unit, so anything folded
108 into it can be ellipsised away — move it to `description` or `badge`.
1098. Verify: `bun run type-check`, `biome check`, the page's tests, and a diff read of
110 every converted block for lost props, conditions, and `key` placement.