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
* 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.
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 …'>`.
157
230
-**`RowActionsMenu`** (`…/components/row-actions-menu`) — the trailing `...`
158
231
actions menu for a list row. Pass `label` (aria-label) and
159
232
`actions: RowAction[]` (`{ label, onSelect, destructive?, disabled? }`); the
160
233
component renders the canonical flush `...` trigger + `DropdownMenuContent`.
161
234
Conditional items become array spreads: `...(canManage ? [{…}] : [])`. Never
162
235
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.
163
256
164
257
## Save / Discard + unsaved-changes guard
165
258
@@ -208,8 +301,9 @@ shells. Reach for it before hand-rolling a `Chip`.
208
301
(from `@/app/workspace/[workspaceId]/components/credential-detail`). The
209
302
in-view header **Discard** chip (via `SaveDiscardActions onDiscard`) is a
210
303
*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`.
213
307
-**Dirty *computation* stays local** (shapes differ: field-compare vs
214
308
normalize+stringify) — only how dirty is *consumed* is shared. Derive it (a
215
309
`const`/`useMemo`), never store it in `useState`.
@@ -244,6 +338,11 @@ A settings page is design-system-clean when:
244
338
-[ ] Its `NavigationItem` has an accurate, consistent-length `description`.
245
339
-[ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
246
340
-[ ] 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).
248
342
-[ ] 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`.
0 commit comments