Skip to content

Commit 786c854

Browse files
authored
improvement(settings): consolidate resource UI onto shared primitives (#6202)
* improvement(settings): consolidate resource UI onto shared primitives Sandboxes, MCP tools, and MCP servers each modeled their list rows and detail headers differently. Collapse them — and the surfaces they drifted from — onto one set of primitives. SettingsResourceRow now owns the row entirely: tile, title/subtitle tokens, padding and bleed, hover band, hit area, focus ring, and the one navigation chevron. Adds onClick/href (a stretched overlay, so interactive trailing controls keep their clicks), navigable, iconVariant='custom', and a badge slot for decoration that must not swallow row clicks. Rows that open a detail page get a chevron and a whole-row click; flat records keep the "..." menu. Delete moves to the detail header as a destructive chip behind a confirm modal — sandboxes previously deleted with no confirmation at all. Also folds in SettingsField (new), SettingsEmptyState tone='error', RESOURCE_LIST_STACK/GRID, RESOURCE_TILE_PLAIN, and a shared MemberAvatar; deletes DetailIconTile (byte-identical to ResourceTile); and standardizes on the emcn ArrowRight, which is a visibly different glyph from lucide's. * fix(settings): correct row bleed, delete-confirm binding, and avatar seeding Follow-up from review of the consolidation. The sandbox delete confirmation was boolean-only state. Browser Back unmounts the modal without closing it, so opening another sandbox re-opened it already confirmed — against the wrong sandbox. Reset it where the draft guard already handles the same history hazard. RESOURCE_LIST_GRID kept an 8px column gap after the bleed moved onto the row, so neighbouring cards overlapped by exactly the gutter and the right-hand card's stretched hit area won it: clicking between two cards opened the wrong one. Budget the gap for the bleed instead. Also: restore the `group` the template-icon hover outlines depend on; add a `flush` row for headings and overflow boxes; seed MemberAvatar identically on every surface; keep the MCP transport label visible in all row states; give the Delete chip a stable id so it doesn't remount mid-delete; and stop an empty subtitle rendering a phantom line. Docs: widen the rule's path globs to the surfaces it now governs, drop the `text-[14px]` example that contradicted the token rule, and add a Mode C for migrating rows onto the primitive. * revert(settings): keep the dense member roster avatar Consolidating the Teammates/Organization avatar onto the credential member row's was a redesign, not a deduplication. The two encode different things: the roster is a dense list keyed on email where the avatar is a 14px neutral marker, while a member management row carries a name, an email, and a role control and earns a 36px hashed avatar. Merging them made those rows ~70% taller, gave every workspace in a permission group a colour-hashed "avatar" seeded on its name, and cut the Add Members picker from ~7 visible rows to ~5 inside its fixed-height box. Restores both avatars and the containers that owned their bleed, and records in the rule why they stay separate. Keeps only the unrelated fix in that area: the picker row was the one settings row rounded at `sm`. * fix(settings): wire flush, drop the lone skeleton, close review gaps Final review round. Two fixes the previous commit claimed but did not land: `CredentialDetailHeading` never passed `flush`, so every credential detail heading wore list-row padding, and the empty-subtitle guard was never applied. Both were scripted replacements that silently no-op'd. RESOURCE_LIST_GRID also dropped to one column 32px earlier than the grid it replaced — `auto-fit` measures tracks, not margin boxes, so widening the gap for the rows' bleed moved the breakpoint. Track minimum now budgets for it. Removes the BYOK skeleton rather than maintaining a second copy of the row: it was the only skeleton in settings, and it had already desynced from the row it imitates. Its peers render nothing while loading. Also: unify the glyph-tile treatment across MCP/sandboxes/workflow-MCP with custom tools; move decoration out of `trailing` in verified-domains and recently-deleted; convert the last hand-rolled row and empty states in workflow-MCP, api-keys, copilot and group-detail; give copilot's delete the same `...` affordance as api-keys; announce the row description via aria-describedby, which the stretched overlay had silenced; and let SettingsField render its own value so callers stop restating type tokens. Docs: correct claims that predate this PR — `aside` does not exist, the navigation source of truth is under components/, beforeunload mounts in the layouts, `getSettingsSectionMeta` takes two args — and stop asserting a literal-pixel grep returns zero when display type legitimately uses it.
1 parent 2ccda18 commit 786c854

58 files changed

Lines changed: 1392 additions & 1225 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.

.claude/rules/sim-settings-pages.md

Lines changed: 132 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
---
22
paths:
33
- "apps/sim/app/workspace/*/settings/**"
4+
- "apps/sim/app/workspace/*/{integrations,skills,upgrade}/**"
5+
- "apps/sim/app/workspace/*/components/{resource-tile,credential-detail}/**"
6+
- "apps/sim/components/{settings,permissions}/**"
47
- "apps/sim/ee/**/components/**"
58
---
69

@@ -20,7 +23,7 @@ Do NOT hand-roll any of these in a settings page — they are owned by the layou
2023
shell (fed through `SettingsPanel`):
2124

2225
- `<div className='flex h-full flex-col bg-[var(--bg)]'>` shell
23-
- the header bar (`flex flex-shrink-0 … px-[16px] pt-[8.5px] pb-[8.5px]`)
26+
- the header bar — compose `PAGE_HEADER_BAR` (`@/components/page-header-bar`); never rewrite its padding
2427
- the scroll container (`min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]`)
2528
- the content column (`mx-auto … max-w-[48rem] … gap-7`)
2629
- a title block (`<h1 className='font-medium text-[var(--text-body)] text-lg'>` + `<p className='text-[var(--text-muted)] text-md'>`)
@@ -55,21 +58,23 @@ return (
5558
## `SettingsPanel` props
5659

5760
- `actions?: SettingsAction[]` — right-aligned header chips, **data only**:
58-
`{ text, icon?, variant?: 'primary'|'destructive', active?, onSelect, disabled?, tooltip? }`.
61+
`{ id?, text, textTone?: 'error', icon?, variant?: 'primary'|'destructive', active?,
62+
onSelect, onPrefetch?, disabled?, tooltip? }`.
5963
The shell renders each as a `Chip` — never pass JSX, a `<div>`, or `className`
6064
(the locked contract: it's structurally impossible to vibe-code a padding
6165
change). Multiple/conditional actions are a plain array
6266
(`[...(canManage ? [{…}] : []), …]`). Labels are **sentence case** (`Add override`,
6367
not `Add Override`). A disabled action that needs to explain itself sets
64-
`tooltip` (the shell renders the hover tooltip, disabled chip included) — never
65-
hand-roll a tooltip-wrapped chip in `aside`. Save/Discard pairs come from the
66-
`saveDiscardActions()` helper (spread it into `actions`). Only a widget that
67-
genuinely cannot be a chip (e.g. one needing hover-prefetch) goes in `aside`.
68+
`tooltip` (the shell renders the hover tooltip, disabled chip included). An action
69+
that wants to warm a route on hover sets `onPrefetch`; the shell wires it. A label
70+
that flips while pending (`Delete``Deleting...`) sets a stable `id`, or the chip
71+
remounts mid-action. Save/Discard pairs come from the `saveDiscardActions()`
72+
helper (spread it into `actions`).
6873
- `back?: SettingsBackAction` (`{ text, icon?, onSelect }`) — left-aligned back
6974
chip for a **detail sub-view** (e.g. a selected MCP server, a permission group,
7075
a retention policy). Detail sub-views render through `SettingsPanel` like list
7176
pages — they do NOT hand-roll their own shell.
72-
- `aside?: ReactNode`escape hatch for the rare non-chip header widget. Keep it rare.
77+
- `docsLink?: string`renders the header's `Docs` `ChipLink`.
7378
- `search?: { value; onChange: (value: string) => void; placeholder?; disabled? }`
7479
renders the canonical search field directly below the title. Pass `setSearchTerm`
7580
straight to `onChange`. Use this for a standalone search; if search shares a row
@@ -82,15 +87,15 @@ return (
8287

8388
## Title + description live in navigation metadata
8489

85-
`apps/sim/app/workspace/[workspaceId]/settings/navigation.ts` is the single source
86-
of truth. Every `NavigationItem` carries a one-line `description`; `SettingsPanel`
87-
resolves both via `getSettingsSectionMeta(section)` and the
90+
`apps/sim/components/settings/navigation.ts` is the single source of truth (the
91+
`settings/navigation.ts` in the route tree is only a re-export shim). Every `NavigationItem` carries a one-line `description`; `SettingsPanel`
92+
resolves both via `getSettingsSectionMeta(plane, section)` and the
8893
`SettingsSectionProvider` the settings shell wraps around the active section.
8994

9095
Adding a new settings page:
9196

92-
1. Add the `SettingsSection` id + a `NavigationItem` (with `label` **and**
93-
`description`) in `navigation.ts`. Keep descriptions verb-first, one line,
97+
1. Add the section id to the `UnifiedSettingsSection` union + a `NavigationItem`
98+
(with `label` **and** `description`) in `components/settings/navigation.ts`. Keep descriptions verb-first, one line,
9499
~40–55 chars, in the product voice (see `.claude/rules/constitution.md`).
95100
2. Render the component inside the shell's `effectiveSection` switch in
96101
`settings/[section]/settings.tsx`.
@@ -107,19 +112,14 @@ token (if the pixel value matches one exactly) or a sign the page never migrated
107112
grep `text-\[1[0-8]px\]` under `apps/sim/app/workspace/*/settings/**` and
108113
`apps/sim/ee/**` to find stragglers.
109114

110-
For a two-line list row (title/value on top, a muted subtitle below — a name +
111-
email, a tool name + description, a server name + status), the established
112-
pairing is:
115+
Watch `text-xs`: it is 11px here, so a "caption" written as `text-xs` is a pixel
116+
short. See `sim-styling.md` for the full scale.
113117

114-
- **Title / row value**: `text-[var(--text-body)] text-sm`
115-
- **Subtitle / muted description**: `text-[var(--text-muted)] text-caption`
116-
117-
This is not a stylistic guess — it is the tokenized form of the literal-pixel
118-
pairing (`text-[14px] text-[var(--text-body)]` / `text-[12px]
119-
text-[var(--text-muted)]`) already used for this exact row shape across
120-
`member-list.tsx`, `api-keys.tsx`, `mcp.tsx`, `billing.tsx`,
121-
`workflow-mcp-servers.tsx`, and others — keep new rows consistent with it rather
122-
than inventing a new size pairing.
118+
The two-line list row (title over a muted subtitle — a name + email, a tool name
119+
+ description, a server name + status) is **not something you build**: it is
120+
`SettingsResourceRow`, which owns the pairing
121+
(`text-[var(--text-body)] text-sm` over `text-[var(--text-muted)] text-caption`).
122+
See "The resource row" below.
123123

124124
For a toggle row (a `Switch` with a title and optional description), use the emcn
125125
`Label` component for the title — never a hand-rolled `<span>` — paired with
@@ -145,21 +145,114 @@ independently-defined tokens (not interchangeable — they resolve to different
145145
colors) and both see legitimate use across settings pages; this rule only pins
146146
down the **row title/subtitle** shape above, not every text element on every page.
147147

148+
## The resource row
149+
150+
**`SettingsResourceRow`** (`…/components/settings-resource-row`) is *the* list row
151+
for every settings resource — and for skills, integrations, and the `ee/` surfaces
152+
too. It owns the tile, the title/subtitle tokens, the row padding and bleed
153+
(`-mx-2 … rounded-lg p-2`), the hit area, the focus ring, the navigation chevron,
154+
and — on activatable rows only — the hover band. Never hand-roll any of it, and never wrap the row in your own
155+
`<button>` or `<Link>` — that is what `onClick`/`href` are for.
156+
157+
```tsx
158+
<div className={RESOURCE_LIST_STACK}>
159+
{items.map((item) => (
160+
<SettingsResourceRow
161+
key={item.id}
162+
icon={<Wrench className='text-[var(--text-icon)]' />}
163+
iconFilled
164+
title={item.name}
165+
description={item.summary}
166+
onClick={() => open(item.id)} // or href={`…/${item.id}`}
167+
clickLabel={`Open ${item.name}`}
168+
navigable
169+
/>
170+
))}
171+
</div>
172+
```
173+
174+
- `icon?` + `iconVariant``tile` (default, the 36px bordered tile), `plain` (a
175+
bare 14px glyph), `custom` (you supply the whole tile, e.g. the brand-tinted
176+
`IntegrationTile`). Omit `icon` entirely for resources with no identity glyph
177+
(an API key, a permission group). `iconFilled` uses the skills/tools fill;
178+
`iconFill` lets an uploaded image reach the tile edge.
179+
- `onClick` / `href` — makes the **whole row** activatable via a stretched
180+
overlay. Prefer `href` when the destination is a route, so the row keeps
181+
prefetch, middle-click, and open-in-new-tab. Always pass `clickLabel` with
182+
either — the overlay holds no text, so it is the control's *only* accessible
183+
name. The prop is optional in the type (nothing enforces it), so omitting it
184+
ships a nameless button rather than failing the build.
185+
- `navigable` — appends the one canonical chevron. Set it on rows that open a
186+
detail page; leave it off when `onClick` acts in place (revealing a folder).
187+
Never import an arrow yourself: `lucide-react` and `@sim/emcn/icons` ship
188+
visibly different glyphs, and the row already picked one. A sanctioned bespoke
189+
row (below) draws it with `RESOURCE_ROW_ARROW_CLASSES` from the same module.
190+
- `trailing` vs `badge``trailing` is for **interactive** controls (a `Chip`, a
191+
`RowActionsMenu`) and sits above the hit area. `badge` is for **decoration** (a
192+
status tag) and is click-through. Putting a badge in `trailing` turns the row's
193+
right edge into a dead zone.
194+
- `RESOURCE_LIST_STACK` / `RESOURCE_LIST_GRID` — the single-column and two-up
195+
containers. A `SettingsResourceRow` carries its own `-mx-2` bleed and padding,
196+
so a container holding one only sets rhythm: never add a second `-mx-2` (they
197+
stack into a 16px bleed) and never a different gap. A list of hand-rolled rows
198+
is the opposite — there the container owns the bleed. A row inside a
199+
fixed-height `overflow-y-auto` box, or a heading that must line up with the
200+
section labels under it, passes `flush` to drop the bleed and padding.
201+
`RESOURCE_LIST_GRID` budgets its column gap for the bleed (24px of track gap
202+
minus 16px of bleed = an 8px gutter); narrowing that gap makes neighbouring
203+
rows — and their stretched hit areas — overlap, so a click in the gutter opens
204+
the wrong card.
205+
206+
**Three-dots vs. chevron** is not a taste call:
207+
208+
- Opens a **detail page**`navigable` + a whole-row click, and no `Delete` in
209+
the row (it lives in the detail header).
210+
- **No detail page** → a `RowActionsMenu` in `trailing`, and no `navigable`
211+
chevron. The row may still take an `onClick` for an in-place action — a folder
212+
mount reveals itself in Finder and also carries a `...` menu — but a row must
213+
never offer both a chevron and a menu.
214+
148215
## Other shared settings primitives (do not re-roll these)
149216

217+
- **`SettingsSection`** (`…/components/settings-section/settings-section` — this
218+
directory has no barrel) — muted label, hairline
219+
divider, body. Also carries `headerAccessory` and `action` slots. Never
220+
re-derive the label/divider chrome; `sim-styling.md` owns those tokens.
221+
- **`SettingsField`** (`…/components/settings-field`) — a read-only label/value
222+
pair in a detail body: muted caption over the value. Pair it with
223+
`SETTINGS_FIELD_VALUE_CLASSES` for the value text.
150224
- **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical
151-
muted status message. `variant='fill'` (default) centers in the available
152-
height (empty list, or a not-entitled/loading gate); `variant='inline'` sits in
153-
flow (a search "no results"). Never hand-roll
154-
`<div className='flex h-full items-center justify-center text-[var(--text-muted)] text-sm'>`
155-
or `<div className='py-4 text-center …'>`. It owns the `--text-muted` + `text-sm`
156-
tokens, so it also keeps these messages consistent across pages.
225+
muted status message, for empty lists, "no results", loading gates, **and
226+
failed loads** (`tone='error'`). `variant='fill'` (default) centers in the
227+
available height; `variant='inline'` sits in flow. Never hand-roll
228+
`<div className='flex h-full items-center justify-center …'>` or
229+
`<div className='py-4 text-center …'>`.
157230
- **`RowActionsMenu`** (`…/components/row-actions-menu`) — the trailing `...`
158231
actions menu for a list row. Pass `label` (aria-label) and
159232
`actions: RowAction[]` (`{ label, onSelect, destructive?, disabled? }`); the
160233
component renders the canonical flush `...` trigger + `DropdownMenuContent`.
161234
Conditional items become array spreads: `...(canManage ? [{…}] : [])`. Never
162235
hand-roll the `<DropdownMenu>` + `<MoreHorizontal>` trigger per page.
236+
- **`RESOURCE_TILE_BASE`** + one of `RESOURCE_TILE_FILL` / `RESOURCE_TILE_PLAIN`
237+
(`app/workspace/[workspaceId]/components/resource-tile` — note: *not* under
238+
`settings/`, unlike the other `…/` paths on this page) — the 36px tile chrome, for
239+
any tile the row does not draw itself: a detail heading, or a caller-supplied
240+
`iconVariant='custom'` tile. `ResourceTile` wraps the filled pairing. Use
241+
`RESOURCE_TILE_FILL` for a glyph, `RESOURCE_TILE_PLAIN` for a brand logo or favicon.
242+
243+
244+
**Member avatars are deliberately two components, not one.** `member-list.tsx`
245+
renders a 14px neutral marker for the dense Teammates/Organization roster, where
246+
the email is the primary content; `components/permissions/member-row.tsx` renders
247+
a 36px `getUserColor`-hashed avatar for member *management* rows that carry a name,
248+
an email, and a role control. Same shape, different job — do not merge them.
249+
250+
## Deleting a resource
251+
252+
Delete lives in the **detail header**, as
253+
`{ text: 'Delete', variant: 'destructive', onSelect: … }` behind a
254+
`ChipConfirmModal` — never `textTone: 'error'`, never a bare `Chip`, and never
255+
unconfirmed. A list row does not carry Delete when the resource has a detail page.
163256

164257
## Save / Discard + unsaved-changes guard
165258

@@ -208,8 +301,9 @@ shells. Reach for it before hand-rolling a `Chip`.
208301
(from `@/app/workspace/[workspaceId]/components/credential-detail`). The
209302
in-view header **Discard** chip (via `SaveDiscardActions onDiscard`) is a
210303
*reset to original* — distinct from the back-confirm's discard, which leaves.
211-
- **`useSettingsBeforeUnload`** is mounted **once** in the settings shell
212-
(`settings/[section]/settings.tsx`) — never add a per-page `beforeunload`.
304+
- **`useSettingsBeforeUnload`** is mounted by the settings shells
305+
(`settings/layout.tsx` and `components/settings/standalone-settings-shell.tsx`) —
306+
never add a per-page `beforeunload`.
213307
- **Dirty *computation* stays local** (shapes differ: field-compare vs
214308
normalize+stringify) — only how dirty is *consumed* is shared. Derive it (a
215309
`const`/`useMemo`), never store it in `useState`.
@@ -244,6 +338,11 @@ A settings page is design-system-clean when:
244338
- [ ] Its `NavigationItem` has an accurate, consistent-length `description`.
245339
- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
246340
- [ ] 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.
247-
- [ ] No business logic, handlers, or conditional rendering changed by the migration.
341+
- [ ] No business logic, handlers, or conditional rendering changed by the migration — except where the shared primitive makes a gate structural (a permission gate becomes `onClick={can ? … : undefined}` + `navigable={can}`, which renders a plain non-interactive row).
248342
- [ ] No literal `text-[Npx]` classes — named scale tokens only (see "Text-scale tokens" above).
343+
- [ ] Every **resource** list row (a thing with an identity — a tool, a server, a key, a credential) is a `SettingsResourceRow` in a `RESOURCE_LIST_STACK`/`RESOURCE_LIST_GRID` — no wrapper `<button>`/`<Link>`, no hand-passed arrow, no re-derived title/subtitle spans. Rows with a genuinely different shape stay bespoke, and draw their own arrow with `RESOURCE_ROW_ARROW_CLASSES`: multi-line bodies (inbox tasks), tabular columns (billing invoices, credit usage), grids (secrets), and the member rows (see the avatar note above).
344+
- [ ] Rows that open a detail page use `navigable` + `clickLabel`; flat records use `RowActionsMenu`. Not both.
345+
- [ ] Decorative trailing content is in `badge`, not `trailing`.
346+
- [ ] Labeled sections use `SettingsSection`; read-only fields use `SettingsField`; empty/loading/error use `SettingsEmptyState`.
347+
- [ ] Delete is a `destructive` header action behind a `ChipConfirmModal`.
249348
- [ ] `tsc`, `biome`, and the page's tests pass.

.claude/rules/sim-styling.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ paths:
88

99
## Tailwind
1010

11-
1. **No inline styles** - Use Tailwind classes
11+
1. **No inline styles** - Use Tailwind classes. Exception: a genuinely dynamic
12+
value (a hashed avatar colour, a brand tile background) that cannot be a class.
1213
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
13-
3. **Exact values** - `text-[14px]`, `h-[26px]`
14+
3. **Exact values over approximations** - `h-[26px]`, not `h-6`. But **type size is
15+
always a named token** (`text-sm`, `text-caption`) — never `text-[14px]`, which
16+
sets font-size only and inherits a different line-height. See
17+
`sim-settings-pages.md` for the scale.
1418
4. **Transitions** - `transition-colors` for interactive states
1519

1620
## Conditional Classes

.claude/skills/add-settings-page/SKILL.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ skill is the procedure.
1212

1313
Key paths:
1414
- 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`
15+
- Nav metadata (titles + descriptions): `apps/sim/components/settings/navigation.ts`
1616
- Section switch + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
1717
- Pages: `apps/sim/app/workspace/[workspaceId]/settings/components/<name>/<name>.tsx` and EE pages under `apps/sim/ee/<feature>/components/`
1818

@@ -53,7 +53,9 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa
5353
3. Find literal pixel text sizes (should be 0 — see "Text-scale tokens" in
5454
`.claude/rules/sim-settings-pages.md` for the token map and the row
5555
title/subtitle pairing convention):
56-
`git grep -n "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
56+
`git grep -nE "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'` — should
57+
be 0. Display type above the scale (`text-[40px]` hero headings, the `text-[8px]`
58+
member-avatar initial) is deliberate and out of scope.
5759
4. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
5860
accurate `description` of consistent length with its peers.
5961
- Editable pages: confirm Save/Discard go through `SaveDiscardActions` and
@@ -77,3 +79,31 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa
7779
8. **Verify the whole sweep:** `tsc --noEmit`, `biome check` on every touched
7880
file, and run the affected pages' tests. Diff each file against the base and
7981
confirm the change is purely structural before shipping.
82+
83+
## Mode C — Migrate list rows to `SettingsResourceRow`
84+
85+
Read "The resource row" in `.claude/rules/sim-settings-pages.md` first — it is the
86+
contract. Then, per page:
87+
88+
1. Find hand-rolled rows:
89+
`git grep -n "truncate text-\[var(--text-body)\] text-sm" -- 'apps/sim/app/workspace/' 'apps/sim/ee/'`
90+
Every match outside `settings-resource-row.tsx` is either a row to migrate or a
91+
genuinely different shape (multi-line body, tabular columns, a grid) that stays
92+
bespoke — decide which, and say so.
93+
2. Replace the row *and* its wrapper: a `<button>`/`<Link>` around the row becomes
94+
`onClick`/`href` on the row itself. Wrapping the row is what the primitive
95+
exists to stop — it is also invalid HTML once `trailing` holds a control.
96+
3. Sort the trailing content: interactive → `trailing`, decorative → `badge`.
97+
Getting this backwards makes the row's right edge a dead zone.
98+
4. Add `navigable` only if the row opens a detail page, and `clickLabel` always.
99+
5. Drop the container's `-mx-2` — the row now owns the bleed. Use
100+
`RESOURCE_LIST_STACK` / `RESOURCE_LIST_GRID`; do not hand-write the gap.
101+
6. Unlike Mode B, this migration **may** change conditional rendering: a
102+
`<button disabled={!can}>` becomes `onClick={can ? … : undefined}` +
103+
`navigable={can}`, which renders a plain non-interactive row. Verify the gated
104+
state has no clickable affordance left.
105+
7. Check what the old row rendered *beside* the title (a badge, a timestamp, a
106+
transport label). The row's title truncates as one unit, so anything folded
107+
into it can be ellipsised away — move it to `description` or `badge`.
108+
8. Verify: `tsc --noEmit`, `biome check`, the page's tests, and a diff read of
109+
every converted block for lost props, conditions, and `key` placement.

0 commit comments

Comments
 (0)