You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 66ab4f7
Browse filesBrowse the repository at this point in the historyBrowse files
docs(agents): consolidate guidance and add the local gate to CLAUDE.md
- CLAUDE.md: "How your work is checked" (the local gate and a rule-to-check
map), sharper comment rules (no narration, restated names, or change
history), drop filler, fix the type-check command description.
- Rules: one source for the text scale (sim-styling), use-client boundary
(sim-queries), testing principles (CLAUDE.md); fix an in-place sort in a
list-ordering example and an ESLint directive the repo does not use; drop
change-history narration and rotting line-number references.
Copy file name to clipboardExpand all lines: .claude/rules/emcn-components.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -21,7 +21,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
21
21
22
22
## Component catalogue
23
23
24
-
-**`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`. The old `mx-0.5` default and its `flush` opt-out are gone; do not reintroduce either, and never add a margin to a chip through `className`.
24
+
-**`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`, so never give a chip a margin, by default or through `className`.
25
25
-**`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
26
26
-**`ChipCopyInput`** — the canonical view-only field: a read-only `ChipInput` at full opacity with a trailing copy-to-clipboard button. View-only is a display mode, not a disabled state — reach for it (or `ChipModalField type='copy'`) over a `disabled` (greyed) input for values the user cannot edit.
27
27
-**`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default), `viewOnly` (read-only at full opacity with the default cursor — the multi-line counterpart of `ChipCopyInput`).
@@ -52,8 +52,8 @@ Declare keyboard intent on the action-owning primitive; never add document-level
52
52
-**Props over `className` overrides.** When a consumer needs to change chrome, expose a prop (`error`, `icon`, `endAdornment`, `inputClassName`); reaching for `className` to restyle chrome is the smell.
53
53
-**`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
54
54
-**Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
55
-
-**Delete legacy variants after migration** — don't leave dead paths (this paradigm removed `Input variant='chip'` and `ChipMultiSelect`).
56
-
-**Verify CSS vars exist.** An undefined var resolves to `currentColor` (caused a real black-border bug). Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
55
+
-**Delete legacy variants after migration** — leave no dead paths.
56
+
-**Verify CSS vars exist.** An undefined var resolves to `currentColor`, which draws black borders. Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
57
57
- Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example.
58
58
59
59
Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.
Every export of a `'use client'` module becomes a *client reference* on the server — server-evaluated code (RSC pages/layouts, `prefetch.ts`, route handlers, block definitions, triggers) can only *render* it as a component or pass it as a prop, never *call* it (doing so throws at runtime, e.g. `tableKeys.list is not a function`; `next build` does not catch it). Keep server-importable query primitives (key factories, fetchers, mappers, constants) in non-`'use client'`modules — see `.claude/rules/sim-queries.md`. Enforced by `scripts/check-client-boundary-imports.ts`.
13
+
Server-evaluated code (RSC pages/layouts, `prefetch.ts`, route handlers, block definitions, triggers) can render an export of a `'use client'` module but never call it; the call throws at runtime and `next build` does not catch it. Keep anything a server module calls (key factories, fetchers, mappers, constants) in a non-`'use client'`module. Detail and the escape hatch: `.claude/rules/sim-queries.md`. Enforced by `bun run check:client-boundary`.
When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):
34
34
35
-
-**Precompute a lookup `Map` once**, never `array.find(...)` per row. Build `const byId = useMemo(() => { const m = new Map<string, T>(); for (const x of items ?? []) m.set(x.id, x); return m }, [items])` and read `byId.get(id)` in the sort comparator, `.map(...)`, and cell builders. A `.find` inside a sort comparator is O(n²·log n) — the worst offender. Depend memos on the derived `Map`, not the raw array.
36
-
-**Sort a copy with `[...array].sort(cmp)`**, never `toSorted` in client code — see `sim-react-performance.md` → "Never mutate a shared array in place".
35
+
-**Precompute a lookup `Map` once** (`useMemo` over the collection) and read `byId.get(id)` in the sort comparator, `.map(...)`, and cell builders. A `.find` inside a sort comparator is O(n²·log n). Depend downstream memos on the derived `Map`, not the raw array. Map-building and copy-then-sort rules: `sim-react-performance.md`.
37
36
-**Partition in a single pass** — when splitting one collection into several (`fileIds`/`folderIds`), do one `for…of` pushing into each bucket and return `{ a, b }` from a single `useMemo`, not two memos that each `map→filter→map` the same source twice.
38
37
39
38
## react-doctor (`bunx react-doctor`) — apply the wins, skip the false positives
Copy file name to clipboardExpand all lines: .claude/rules/sim-list-ordering.md
+4-10Lines changed: 4 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,8 +10,6 @@ paths:
10
10
11
11
**A list orders itself the way the user already reads the same things somewhere else.** Dropdowns, context menus, tab strips, command palettes, and settings navs are all *second* presentations of a set the user has already seen — in a toolbar, in the settings nav, in a column-header row. When the second presentation reorders that set, the user re-reads it from scratch every time.
12
12
13
-
This is not a style preference. Order is the cheapest affordance a list has, and the only one that costs nothing to get right.
14
-
15
13
## The rule
16
14
17
15
Before writing a list of items, find where the user sees those same items *first*. That surface owns the order; your list mirrors it.
Copy file name to clipboardExpand all lines: .claude/rules/sim-styling.md
+8-8Lines changed: 8 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,8 +15,8 @@ paths:
15
15
2.**No duplicate dark classes** - Skip `dark:` when value matches light mode
16
16
3.**Exact values over approximations** - `h-[26px]`, not `h-6`. But **type size is
17
17
always a named token** (`text-sm`, `text-caption`) — never `text-[14px]`, which
18
-
sets font-size only and inherits a different line-height. See
19
-
`sim-settings-pages.md` for the scale.
18
+
sets font-size only and inherits a different line-height. The scale is under
19
+
"Text Scale" below.
20
20
4.**Transitions** - `transition-colors` for interactive states
21
21
22
22
## Conditional Classes
@@ -48,7 +48,7 @@ setWidth: (width) => {
48
48
49
49
## Text Scale
50
50
51
-
Custom font sizes (the `@theme` block in `apps/sim/app/_styles/globals.css`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px.`text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
51
+
The `@theme` block in `apps/sim/app/_styles/globals.css`: `text-micro`10px, `text-xs`11px, `text-caption`12px, `text-small`13px, `text-sm` 14px (Tailwind default), `text-base`15px,`text-md` 16px, `text-lg` 18px (Tailwind default). `text-xs` is 11px here, so a caption written as `text-xs` is a pixel short. Field titles use `text-small`; hints/errors use `text-caption`. A literal `text-[Npx]` is either a rename to the matching token or an unmigrated straggler.
52
52
53
53
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N` or `h-[Npx] w-[Npx]`.
54
54
@@ -70,7 +70,7 @@ A scroll region that can hide rows past an edge uses `useScrollEdges` with `scro
70
70
71
71
Three steps, Tailwind's stock scale, nothing else: **`font-normal` (400)**, **`font-medium` (500)**, **`font-semibold` (600)**. 400 is the document default, so body text, chip labels, sidebar items, and headings carry **no weight class at all** — they inherit. Reach for a class only to step *up* from body.
72
72
73
-
Never write an arbitrary weight (`font-[380]`, `font-[430]`, `font-[450]`, …), and never set `fontWeight` in an inline `style`. There was previously a CSS-variable weight scale (`--font-weight-base/medium/semibold`, remapping `font-medium` to 440/480) plus seven ad-hoc values clustered between 380 and 500; it was deleted because nothing read as hierarchical. Off-scale values are only acceptable where the design system genuinely cannot reach — react-email templates and the static `apps/sim/emails/broadcasts/*.html`, which email clients render without CSS variables.
73
+
Never write an arbitrary weight (`font-[380]`, `font-[450]`, …) or a `--font-weight-*` variable, and never set `fontWeight` in an inline `style`: values clustered between the three steps do not read as hierarchy. Off-scale values are acceptable only where the design system cannot reach — react-email templates and the static `apps/sim/emails/broadcasts/*.html`, which email clients render without CSS variables.
74
74
75
75
Headings inherit their weight. Tailwind preflight resets `h1`–`h6` to `font-weight: inherit`, so an `<h1>` is 400 unless you say otherwise — that is the intended look, not a bug to patch.
76
76
@@ -95,19 +95,19 @@ Draw a line with a real `border-*` utility. Never hand-roll one as `shadow-[inse
95
95
-**Errors** → `error` prop. Never `className={cn(err && 'border-[var(--text-error)]')}`.
96
96
-**Leading icon** → `icon` prop (rendered 14px in `--text-icon`).
-**`ChipModalField` controls take NO className.** Pass `title`/`value`/`onChange`/`error`/`hint`/`required`/`flush`. The field owns label, control, and error/hint rendering. See `app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx:170`.
-**`ChipModalField` controls take NO className.** Pass `title`/`value`/`onChange`/`error`/`hint`/`required`/`flush`. The field owns label, control, and error/hint rendering. See `app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx`.
100
100
101
101
### What className MAY carry
102
102
103
-
Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins. `truncate` is allowed only for the explicit overflow exceptions above, never as a general layout class. Example: `<ChipInput icon={Search} className='min-w-0 flex-1' .../>` (`app/workspace/[workspaceId]/integrations/integrations.tsx:257`). NEVER re-specify canonical chrome — the component already applies it.
103
+
Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins. `truncate` is allowed only for the explicit overflow exceptions above, never as a general layout class. Example: `<ChipInput icon={Search} className='min-w-0 flex-1' .../>` (`app/workspace/[workspaceId]/integrations/integrations.tsx`). NEVER re-specify canonical chrome — the component already applies it.
104
104
105
105
### Form / chip-modal layout rhythm
106
106
107
107
-**Field row** = `ChipModalField`: label↔control `gap-[9px]`, field gutter `px-2` (`px-0` when `flush`). Title = `Label` at `text-small` (13px), muted, normal weight; hint/error at `text-caption` (12px).
108
108
-**Modal body** (`ChipModalBody`): `gap-4` between fields, padding `px-2 pt-4 pb-4.5`.
-**Every body field MUST be a `ChipModalField`** — NEVER hand-roll a field row (raw `<div>` + hand-rolled `<p>`/`<label>` title + bare `ChipInput`/`ChipTextarea`). WHY: body `px-2` + field `px-2` = effective `px-4`, exactly matching the `px-4` header/footer. A hand-rolled row skips the field gutter, sits at `px-2`, and is visibly misaligned (this bug shipped in the scheduled-tasks "Create new scheduled task" modal). Inline errors go through the `error` prop, not a hand-rolled `<p>`.
110
+
-**Every body field MUST be a `ChipModalField`** — NEVER hand-roll a field row (raw `<div>` + hand-rolled `<p>`/`<label>` title + bare `ChipInput`/`ChipTextarea`). WHY: body `px-2` + field `px-2` = effective `px-4`, exactly matching the `px-4` header/footer. A hand-rolled row skips the field gutter, sits at `px-2`, and is visibly misaligned. Inline errors go through the `error` prop, not a hand-rolled `<p>`.
111
111
-**Uncovered controls** (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX) → `ChipModalField type='custom'` with a `title`. It still applies the `px-2` gutter and renders the canonical `Label`, so it stays aligned. Never drop such a control into a raw `<div>`, and never add a body-level wrapper `<div>` with a custom `gap-*` that fights `gap-4`.
112
112
-**Page section rhythm** (integrations/skills/settings): muted `text-small` label + `mt-[9px] mb-3 h-px bg-[var(--border)]` divider, sections stacked `gap-7`. Reuse `SettingsSection` (`app/workspace/[workspaceId]/settings/components/settings-section/settings-section.tsx`) rather than re-deriving it.
Copy file name to clipboardExpand all lines: .claude/rules/sim-url-state.md
+1-10Lines changed: 1 addition & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,16 +14,7 @@ URL query state is managed with [`nuqs`](https://nuqs.dev). The `NuqsAdapter` is
14
14
15
15
## Decision framework — where does this state live?
16
16
17
-
Pick exactly one home for each piece of state:
18
-
19
-
-**React Query** → server/remote data. Unchanged; see `.claude/rules/sim-queries.md`.
20
-
-**URL params (nuqs)** → client view-state worth putting in a link: active tab/panel, selected entity id, filters, search query, pagination, view mode (list/grid), an open "view" drawer/modal that represents a destination.
21
-
-**Zustand** → cross-component client state that must NOT be in the URL: high-frequency, large, ephemeral, or socket-synced (canvas pan/zoom, cursor, drag state, resize widths, unsaved buffers, live collaborative selection).
Put state in the URL **only** when it is *all* of: shareable, deep-linkable, bookmarkable, survives reload + back/forward — **and** is discrete, low-frequency, and small. If it fails any of those, it does not go in the URL.
25
-
26
-
### When to use what (decision table)
17
+
Pick exactly one home for each piece of state (table below). Put state in the URL **only** when it is *all* of: shareable, deep-linkable, bookmarkable, survives reload + back/forward — **and** is discrete, low-frequency, and small. If it fails any of those, it does not go in the URL.
0 commit comments