Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .claude/rules/sim-settings-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,55 @@ Adding a new settings page:
`settings/[section]/settings.tsx`.
3. Build the component body inside `<SettingsPanel>` — no shell, no title block.

## Text-scale tokens (no literal pixel sizes)

Settings pages never use a literal `text-[Npx]` class — always the named Tailwind
scale token from `apps/sim/tailwind.config.ts`'s `fontSize` extension (`text-micro`
10px, `text-xs` 11px, `text-caption` 12px, `text-small` 13px, `text-sm` 14px
[Tailwind default, unmodified], `text-base` 15px, `text-md` 16px, `text-lg` 18px
[Tailwind default]). A literal size is either a straight rename to the equivalent
token (if the pixel value matches one exactly) or a sign the page never migrated —
grep `text-\[1[0-8]px\]` under `apps/sim/app/workspace/*/settings/**` and
`apps/sim/ee/**` to find stragglers.

For a two-line list row (title/value on top, a muted subtitle below — a name +
email, a tool name + description, a server name + status), the established
pairing is:

- **Title / row value**: `text-[var(--text-body)] text-sm`
- **Subtitle / muted description**: `text-[var(--text-muted)] text-caption`

This is not a stylistic guess — it is the tokenized form of the literal-pixel
pairing (`text-[14px] text-[var(--text-body)]` / `text-[12px]
text-[var(--text-muted)]`) already used for this exact row shape across
`member-list.tsx`, `api-keys.tsx`, `mcp.tsx`, `billing.tsx`, `credential-sets.tsx`,
`workflow-mcp-servers.tsx`, and others — keep new rows consistent with it rather
than inventing a new size pairing.

For a toggle row (a `Switch` with a title and optional description), use the emcn
`Label` component for the title — never a hand-rolled `<span>` — paired with
`Switch`'s `id`/`Label`'s `htmlFor`:

```tsx
<div className='flex items-center justify-between'>
<div className='flex flex-col gap-1'>
<Label htmlFor='my-toggle'>Enable thing</Label>
<p className='text-[var(--text-muted)] text-caption'>One-line description.</p>
</div>
<Switch id='my-toggle' checked={enabled} onCheckedChange={onToggle} />
</div>
```

`Label`'s own default styling (`font-medium text-[var(--text-primary)]
text-small`) already matches the established title treatment — do not add a
`className` overriding its size/color unless the row genuinely needs something
different.

`--text-primary`/`--text-secondary` and `--text-body`/`--text-muted` are both real,
independently-defined tokens (not interchangeable — they resolve to different
colors) and both see legitimate use across settings pages; this rule only pins
down the **row title/subtitle** shape above, not every text element on every page.

## Other shared settings primitives (do not re-roll these)

- **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical
Expand Down Expand Up @@ -175,4 +224,5 @@ A settings page is design-system-clean when:
- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
- [ ] If it has editable state: Save/Discard go through `SaveDiscardActions`, dirty is wired via `useSettingsUnsavedGuard` (called before any early-return gate), and there is **no** hand-rolled Save button / `beforeunload` / "Unsaved changes" modal.
- [ ] No business logic, handlers, or conditional rendering changed by the migration.
- [ ] No literal `text-[Npx]` classes — named scale tokens only (see "Text-scale tokens" above).
- [ ] `tsc`, `biome`, and the page's tests pass.
17 changes: 13 additions & 4 deletions .claude/skills/add-settings-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,30 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa
**gate** early-return. Anything else is a page that still needs migrating.
2. 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/'`
3. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
3. 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 -n "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
4. 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`.
4. When migrating a page, change ONLY the structural shell→`SettingsPanel` swap:
5. 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.
5. Remove now-unused imports (`ChipInput`/`Search`) ONLY after grepping that
6. 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.
7. 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).
6. **Verify the whole sweep:** `tsc --noEmit`, `biome check` on every touched
8. **Verify the whole sweep:** `tsc --noEmit`, `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.
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ interface UsageLogRowProps {
function UsageLogRow({ log }: UsageLogRowProps) {
return (
<div className='flex items-center gap-2.5 rounded-lg p-2 text-left'>
<span className='w-[150px] flex-shrink-0 text-[12px] text-[var(--text-muted)]'>
<span className='w-[150px] flex-shrink-0 text-[var(--text-muted)] text-caption'>
{formatDateTime(new Date(log.createdAt))}
</span>
<span className='min-w-0 flex-1 truncate text-[14px] text-[var(--text-body)]'>
<span className='min-w-0 flex-1 truncate text-[var(--text-body)] text-sm'>
{rowLabel(log)}
</span>
<span className='flex-shrink-0 text-[12px] text-[var(--text-muted)] tabular-nums'>
<span className='flex-shrink-0 text-[var(--text-muted)] text-caption tabular-nums'>
{formatApportionedCreditCost(log.creditCost, log.dollarCost)}
</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export function Admin() {
unbanUser.error ||
impersonateUser.error ||
impersonationGuardError) && (
<p className='text-[13px] text-[var(--text-error)]'>
<p className='text-[var(--text-error)] text-small'>
{impersonationGuardError ||
(setUserRole.error || banUser.error || unbanUser.error || impersonateUser.error)
?.message ||
Expand Down Expand Up @@ -304,7 +304,7 @@ export function Admin() {
<>
<Button
variant='active'
className='h-[28px] px-2 text-[12px]'
className='h-[28px] px-2 text-caption'
onClick={() => handleImpersonate(u.id)}
disabled={pendingUserIds.has(u.id)}
>
Expand All @@ -317,7 +317,7 @@ export function Admin() {
</Button>
<Button
variant='active'
className='h-[28px] px-2 text-[12px]'
className='h-[28px] px-2 text-caption'
onClick={() => {
setUserRole.reset()
setUserRole.mutate({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,14 @@ export function ApiKeys() {
<div key={key.id} className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-1.5'>
<span className='max-w-[280px] truncate text-[14px] text-[var(--text-body)]'>
<span className='max-w-[280px] truncate text-[var(--text-body)] text-sm'>
{key.name}
</span>
<span className='text-[var(--text-secondary)] text-sm'>
(last used: {formatLastUsed(key.lastUsed).toLowerCase()})
</span>
</div>
<p className='truncate text-[12px] text-[var(--text-muted)]'>
<p className='truncate text-[var(--text-muted)] text-caption'>
{key.displayKey || key.key}
</p>
</div>
Expand All @@ -212,14 +212,14 @@ export function ApiKeys() {
<div key={key.id} className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-1.5'>
<span className='max-w-[280px] truncate text-[14px] text-[var(--text-body)]'>
<span className='max-w-[280px] truncate text-[var(--text-body)] text-sm'>
{key.name}
</span>
<span className='text-[var(--text-secondary)] text-sm'>
(last used: {formatLastUsed(key.lastUsed).toLowerCase()})
</span>
</div>
<p className='truncate text-[12px] text-[var(--text-muted)]'>
<p className='truncate text-[var(--text-muted)] text-caption'>
{key.displayKey || key.key}
</p>
</div>
Expand Down Expand Up @@ -247,14 +247,14 @@ export function ApiKeys() {
<div className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-1.5'>
<span className='max-w-[280px] truncate text-[14px] text-[var(--text-body)]'>
<span className='max-w-[280px] truncate text-[var(--text-body)] text-sm'>
{key.name}
</span>
<span className='text-[var(--text-secondary)] text-sm'>
(last used: {formatLastUsed(key.lastUsed).toLowerCase()})
</span>
</div>
<p className='truncate text-[12px] text-[var(--text-muted)]'>
<p className='truncate text-[var(--text-muted)] text-caption'>
{key.displayKey || key.key}
</p>
</div>
Expand Down Expand Up @@ -295,9 +295,7 @@ export function ApiKeys() {
<SettingsSection label='Permissions'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<span className='text-[14px] text-[var(--text-body)]'>
Allow personal API keys
</span>
<span className='text-[var(--text-body)] text-sm'>Allow personal API keys</span>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,8 @@ export function Billing() {
</div>
</div>
<div className='flex min-w-0 flex-col'>
<span className='truncate text-[14px] text-[var(--text-body)]'>{planName} plan</span>
<span className='truncate text-[12px] text-[var(--text-muted)]'>{priceText}</span>
<span className='truncate text-[var(--text-body)] text-sm'>{planName} plan</span>
<span className='truncate text-[var(--text-muted)] text-caption'>{priceText}</span>
</div>
</div>
{!subscription.isEnterprise &&
Expand Down Expand Up @@ -594,13 +594,13 @@ export function Billing() {
'flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors'
const rowContent = (
<>
<span className='min-w-0 flex-1 truncate text-[14px] text-[var(--text-body)]'>
<span className='min-w-0 flex-1 truncate text-[var(--text-body)] text-sm'>
{invoice.date}
</span>
<Badge variant={invoice.badge.variant} size='sm'>
{invoice.badge.label}
</Badge>
<span className='flex-shrink-0 text-[12px] text-[var(--text-muted)]'>
<span className='flex-shrink-0 text-[var(--text-muted)] text-caption'>
{invoice.amount}
</span>
<ArrowRight className='size-4 flex-shrink-0 text-[var(--text-icon)]' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export function CreditUsageSection({ workspaceId }: CreditUsageSectionProps) {
<SettingsSection label='Credit usage'>
<div className='flex items-center justify-between px-2'>
<div className='flex flex-col justify-center gap-[1px]'>
<span className='text-[14px] text-[var(--text-body)] tabular-nums'>
<span className='text-[var(--text-body)] text-sm tabular-nums'>
{isPending || isError ? '—' : formatCreditsLabel(totalCredits ?? 0)}
</span>
<span className='text-[12px] text-[var(--text-muted)]'>Last 30 days</span>
<span className='text-[var(--text-muted)] text-caption'>Last 30 days</span>
</div>
<ChipLink href={`/workspace/${workspaceId}/settings/billing/credit-usage`}>
View usage logs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
const keyCount = getProviderKeys(provider.id).length
return (
<div className='flex flex-shrink-0 items-center gap-2'>
<span className='text-[12px] text-[var(--text-muted)]'>
<span className='text-[var(--text-muted)] text-caption'>
{keyCount} {keyCount === 1 ? 'key' : 'keys'}
</span>
<Chip onClick={() => setManagingProviderId(provider.id)}>Manage</Chip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ export function BYOKProviderKeysModal({
{keys.map((key) => (
<div key={key.id} className='flex items-center justify-between gap-2.5'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<span className='truncate text-[14px] text-[var(--text-body)]'>
<span className='truncate text-[var(--text-body)] text-sm'>
{key.name ?? 'Unnamed key'}
</span>
<span className='truncate font-mono text-[12px] text-[var(--text-muted)]'>
<span className='truncate font-mono text-[var(--text-muted)] text-caption'>
{key.maskedKey}
</span>
</div>
Expand All @@ -65,7 +65,7 @@ export function BYOKProviderKeysModal({
))}
</div>
{atCapacity && (
<p className='px-2 text-[12px] text-[var(--text-muted)]'>
<p className='px-2 text-[var(--text-muted)] text-caption'>
Key limit reached ({maxKeys} keys per provider).
</p>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,14 @@ export function Copilot() {
<div key={key.id} className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-1.5'>
<span className='max-w-[280px] truncate text-[14px] text-[var(--text-body)]'>
<span className='max-w-[280px] truncate text-[var(--text-body)] text-sm'>
{key.name || 'Unnamed Key'}
</span>
<span className='text-[var(--text-secondary)] text-sm'>
(last used: {formatLastUsed(key.lastUsed).toLowerCase()})
</span>
</div>
<p className='truncate text-[12px] text-[var(--text-muted)]'>{key.displayKey}</p>
<p className='truncate text-[var(--text-muted)] text-caption'>{key.displayKey}</p>
</div>
<Chip
className='flex-shrink-0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export function CredentialSets() {

<div className='min-w-0'>
<div className='flex items-center gap-2'>
<span className='truncate font-medium text-[14px] text-[var(--text-primary)]'>
<span className='truncate font-medium text-[var(--text-primary)] text-sm'>
{name}
</span>
{member.credentials.length === 0 && (
Expand Down Expand Up @@ -551,7 +551,7 @@ export function CredentialSets() {

<div className='min-w-0'>
<div className='flex items-center gap-2'>
<span className='truncate font-medium text-[14px] text-[var(--text-primary)]'>
<span className='truncate font-medium text-[var(--text-primary)] text-sm'>
{emailPrefix}
</span>
<Badge variant='gray-secondary' size='sm'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ export function CustomTools() {
{filteredTools.map((tool) => (
<div key={tool.id} className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<span className='truncate text-[14px] text-[var(--text-body)]'>
<span className='truncate text-[var(--text-body)] text-sm'>
{tool.title || 'Unnamed Tool'}
</span>
{tool.schema?.function?.description && (
<p className='truncate text-[12px] text-[var(--text-muted)]'>
<p className='truncate text-[var(--text-muted)] text-caption'>
{tool.schema.function.description}
</p>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ChipModalField,
ChipModalFooter,
ChipModalHeader,
Label,
Switch,
} from '@sim/emcn'
import { createLogger } from '@sim/logger'
Expand Down Expand Up @@ -62,12 +63,13 @@ export function InboxEnableToggle() {
<>
<div className='flex items-center justify-between'>
<div className='flex flex-col gap-1'>
<span className='text-[13px] text-[var(--text-primary)]'>Enable email inbox</span>
<span className='text-[12px] text-[var(--text-muted)]'>
<Label htmlFor='inbox-enabled'>Enable email inbox</Label>
<p className='text-[var(--text-muted)] text-caption'>
Allow this workspace to receive tasks via email
</span>
</p>
</div>
<Switch
id='inbox-enabled'
checked={config?.enabled ?? false}
onCheckedChange={handleToggle}
disabled={toggleInbox.isPending}
Expand Down
Loading
Loading