Skip to content

Commit f568baf

Browse files
Merge remote-tracking branch 'origin/staging' into perf/dev-server-memory
# Conflicts: # apps/sim/blocks/registry.ts
2 parents 0dcaea8 + 365d8be commit f568baf

178 files changed

Lines changed: 18246 additions & 4885 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
name: add-settings-page
3+
description: 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+
8+
Sim settings pages all render through the shared **`SettingsPanel`** primitive,
9+
which owns the page chrome and renders a nav-driven title + description. The full
10+
convention lives in `.claude/rules/sim-settings-pages.md` — read it first; this
11+
skill is the procedure.
12+
13+
Key paths:
14+
- Layout primitive: `apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx`
15+
- Nav metadata (titles + descriptions): `apps/sim/app/workspace/[workspaceId]/settings/navigation.ts`
16+
- Section switch + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
17+
- Pages: `apps/sim/app/workspace/[workspaceId]/settings/components/<name>/<name>.tsx` and EE pages under `apps/sim/ee/<feature>/components/`
18+
19+
## Mode A — Add a new settings page
20+
21+
1. **Navigation.** In `navigation.ts`: add the id to the `SettingsSection` union,
22+
then a `NavigationItem` with `label` AND a one-line `description` (verb-first,
23+
~40–55 chars, product voice per `.claude/rules/constitution.md`). Place it in
24+
the right `section` group and set any gating flags (`requiresHosted`,
25+
`requiresEnterprise`, etc.).
26+
2. **Wire the switch.** Add the component to the `effectiveSection` render switch
27+
in `settings/[section]/settings.tsx` (lazy `dynamic(...)` like its siblings).
28+
3. **Build the body inside `SettingsPanel`.** Never hand-roll the shell, header
29+
bar, scroll region, content column, or title block. Put header buttons in
30+
`actions`, a standalone search in `search={{ value, onChange, placeholder }}`,
31+
and the page content as `children`. Modals go beside the panel inside a `<>`.
32+
4. **Verify:** `cd apps/sim && bunx tsc --noEmit`; `bunx biome check --write <file>`.
33+
34+
## Mode B — Audit existing settings pages
35+
36+
For each page component, confirm the checklist in `.claude/rules/sim-settings-pages.md`:
37+
38+
1. Find hand-rolled shells that should be `SettingsPanel`:
39+
`git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
40+
— every match should be either `settings-panel.tsx`, a **detail sub-view**
41+
(has a `<Chip leftIcon={ArrowLeft}>` back button), or an entitlement/loading
42+
**gate** early-return. Anything else is a page that still needs migrating.
43+
2. Find hand-rolled title blocks (should be 0 outside detail views):
44+
`git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
45+
3. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
46+
accurate `description` of consistent length with its peers.
47+
4. When migrating a page, change ONLY the structural shell→`SettingsPanel` swap:
48+
move header chips to `actions`, the standalone search to `search`, delete the
49+
`<h1>` title block, replace the three closing `</div>` (column/scroll/shell)
50+
with `</SettingsPanel>`, and keep modal siblings in a `<>` fragment. Do NOT
51+
touch handlers, state, queries, conditional rendering, or detail/gate returns.
52+
Drop per-page `gap-*`/`pt-*` on the content column in favor of the panel default.
53+
5. Remove now-unused imports (`ChipInput`/`Search`) ONLY after grepping that
54+
they are not still used elsewhere in the file (e.g. by a detail view).
55+
6. **Verify the whole sweep:** `tsc --noEmit`, `biome check` on every touched
56+
file, and run the affected pages' tests. Diff each file against the base and
57+
confirm the change is purely structural before shipping.

apps/docs/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ next-env.d.ts
3838
# Fumadocs
3939
/.source/
4040
.plans/
41+
42+
# fumadocs generates .source dirs anywhere a source.config sits
43+
**/.source/

apps/docs/app/[lang]/[[...slug]]/page.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,23 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
7575
}
7676
const isOpenAPI = '_openapi' in data && data._openapi != null
7777
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
78+
// Academy lessons are video-first: drop the "On this page" TOC and go full
79+
// width so the lesson hero/video gets the room (chapters live in-page instead).
80+
const isAcademy = slug?.[0] === 'academy'
7881

7982
const pageTreeRecord = source.pageTree as Record<string, Root>
8083
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
8184
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
82-
const neighbours = isApiReference
85+
// Academy and API Reference are self-contained sections; keep prev/next inside
86+
// the section instead of spilling into the main documentation tree. Match both
87+
// the section's pages (`/<slug>/...`) and its index (`/<slug>`).
88+
const sectionSlug = isApiReference ? 'api-reference' : isAcademy ? 'academy' : null
89+
const inSection = (url?: string) =>
90+
url != null && (url.includes(`/${sectionSlug}/`) || url.endsWith(`/${sectionSlug}`))
91+
const neighbours = sectionSlug
8392
? {
84-
previous: rawNeighbours?.previous?.url.includes('/api-reference/')
85-
? rawNeighbours.previous
86-
: undefined,
87-
next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined,
93+
previous: inSection(rawNeighbours?.previous?.url) ? rawNeighbours?.previous : undefined,
94+
next: inSection(rawNeighbours?.next?.url) ? rawNeighbours?.next : undefined,
8895
}
8996
: rawNeighbours
9097

@@ -197,18 +204,18 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
197204
/>
198205
<DocsPage
199206
toc={data.toc}
200-
full={data.full}
207+
full={data.full || isAcademy}
201208
breadcrumb={{
202209
enabled: false,
203210
}}
204211
tableOfContent={{
205212
style: 'clerk',
206-
enabled: true,
213+
enabled: !isAcademy,
207214
single: false,
208215
}}
209216
tableOfContentPopover={{
210217
style: 'clerk',
211-
enabled: true,
218+
enabled: !isAcademy,
212219
}}
213220
footer={{
214221
enabled: true,

apps/docs/app/[lang]/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { defineI18nUI } from 'fumadocs-ui/i18n'
33
import { DocsLayout } from 'fumadocs-ui/layouts/docs'
44
import { RootProvider } from 'fumadocs-ui/provider/next'
55
import { Geist_Mono, Inter } from 'next/font/google'
6+
import { AskAI } from '@/components/ai/ask-ai'
67
import {
78
SidebarFolder,
89
SidebarItem,
@@ -120,6 +121,7 @@ export default async function Layout({ children, params }: LayoutProps) {
120121
>
121122
{children}
122123
</DocsLayout>
124+
<AskAI locale={lang} />
123125
</RootProvider>
124126
</body>
125127
</html>

0 commit comments

Comments
 (0)