|
| 1 | +--- |
| 2 | +description: One pattern per job across the organization views (Settings, People, Integrations, Links, Home) |
| 3 | +paths: |
| 4 | + - "apps/sim/platform/embedded/app/(shell)/{settings,people,integrations,links,home}/**" |
| 5 | + - "apps/sim/platform/embedded/lib/**" |
| 6 | + - "apps/sim/platform/shared/**" |
| 7 | + - "apps/sim/platform/design-system/components/{status,modal,picker,settings-section,search-field,activity-row}/**" |
| 8 | + - "apps/sim/app/o/*/**" |
| 9 | + - "apps/sim/app/o/*/settings/**" |
| 10 | +--- |
| 11 | + |
| 12 | +# Organization Views |
| 13 | + |
| 14 | +The organization views live in `apps/sim/platform/embedded/app/(shell)/` and take everything shared from |
| 15 | +`@/platform/design-system/components`. Each job below has one pattern. A view that differs needs a |
| 16 | +reason written where it differs; "it was already like that" is not one. |
| 17 | + |
| 18 | +| Job | Use | Never | |
| 19 | +| --- | --- | --- | |
| 20 | +| First read of a view | `QueryBoundary` (`query`, `failure`, `loadingLabel`, optional `empty`, `loading`), which renders `LoadingRows`, then `FailureNotice`, then `empty`, then `children(data)` | Inline `isPending ? … : error ? …`, a raw `<p>Loading…</p>`, a loading `InlineNotice` | |
| 21 | +| Loading | `LoadingRows` by default. A placeholder shaped like its content (`WidgetSkeleton`, usage charts) only where rows would misrepresent it | A second generic skeleton | |
| 22 | +| Failed read | `FailureNotice`, labelled Retry. `intent='warning'` where the read partly answered | A hand-built retry button or `InlineNotice` + `Button` | |
| 23 | +| Failed background refresh | `FailureNotice` outside the boundary, over the content it keeps | Replacing content already on screen | |
| 24 | +| Empty | `EmptyState`, with no create button when the page already has one. A picker's no-match line inside a dialog stays `Muted`, since a block would take over the dialog | `Muted` or a raw `<p>` anywhere else | |
| 25 | +| Confirm | `ConfirmDialog` (`target`, `title`, `confirmLabel`, `pendingLabel`, `onConfirm`, `safe` when nothing is lost). Removing a person, revoking an invite and any destructive row action always ask first | `Modal variant='confirm'`, `window.confirm`, an unconfirmed destructive action | |
| 26 | +| Save | `SaveActions`, end-aligned in the page flow under the fields it commits: Discard while dirty, then Save. Save is visible and disabled while clean | Save in a header, Save before Discard, a Cancel beside Discard | |
| 27 | +| Editing a server value | `useServerDraft`. Adopts a newer value while clean; while dirty it keeps the edits and sets `conflict`. Show `DraftConflictNotice`, and block Save until the viewer chooses | Seeding `useState` from a query once | |
| 28 | +| Autosave vs Save | One or the other per page. A page with `SaveActions` holds every field in one draft and writes it in one request. List actions with their own dialog, such as adding or removing a member, are not field edits | A page mixing both | |
| 29 | +| Create | A primary compact button in `SectionToolbar`, or in the page header on a page with one | Create in both the toolbar and the empty state | |
| 30 | +| Search over a section's rows | `SectionSearch subject='members'` | `SearchField` with its own copy, unless the search covers more than the subject's name | |
| 31 | +| Row | `ActivityRow` for a row that opens something, `SettingsRow` for a row holding a control | A hand-built row | |
| 32 | +| List of rows | `RowList` (`dividers` for a list read as one block). `ActivityRow density='comfortable'` in settings | `calc(-1 * var(--ui-row-inset))` around a list; styling a row's height from outside | |
| 33 | +| Row actions | `ActionMenu` with icon `I108`, destructive item last with a separator | The same action in the row and its menu | |
| 34 | +| Section heading | `SettingsSection title` (h2), `DetailHeader` for a sub-view's way back | A hand-written heading | |
| 35 | +| Toolbar | `SectionToolbar grow={<SectionSearch />}` with its actions as children | A hand-built flex row | |
| 36 | + |
| 37 | +## Failure feedback |
| 38 | + |
| 39 | +- Form validation is inline, on the field (`error`), once a save has been attempted. |
| 40 | +- A dialog's own action failing stays in the dialog: `Modal` shows a rejected `onConfirm`'s message and keeps the draft. |
| 41 | +- Any other action failing is a toast. Nothing fails silently. |
| 42 | + |
| 43 | +## Page gates |
| 44 | + |
| 45 | +Every page under `app/o/[organizationId]` calls |
| 46 | +`requireOrganizationViewer(organizationId, callbackUrl)` from `@/app/o/[organizationId]/viewer`. It |
| 47 | +redirects a signed-out viewer to sign-in and back, 404s a viewer with no access, and returns |
| 48 | +`{ userId, principal, surface }`. Never write `{ kind: 'session', ... }` in a page. |
| 49 | + |
| 50 | +## Dialogs |
| 51 | + |
| 52 | +`Modal` holds itself open while `onConfirm`'s promise or its `pending` prop runs. Escape, the backdrop |
| 53 | +and Close do nothing until it settles, including when the browser closes the `<dialog>` itself: once |
| 54 | +user activation is spent, `cancel` cannot be prevented, so `OverlayDialog` reopens from `close`. |
| 55 | + |
| 56 | +## Files |
| 57 | + |
| 58 | +- A section with more than one job splits by job: `group-detail.tsx` is the shell, `group-general-tab.tsx` |
| 59 | + and `group-catalog-tabs.tsx` its bodies, `use-group-draft.ts` its write. |
| 60 | +- Merge logic worth a test is a pure module next to the view, such as `secrets-draft.ts`. |
| 61 | +- `(shell)/_components/section-parts/` holds what settings and the secrets page share: `SectionStack`, `FieldStack`, `SectionToolbar`, |
| 62 | + `DetailHeader`, `HeadingRow`, `Muted`, `SectionSkeleton`, `DraftConflictNotice`. `useServerDraft` is in `_components/`, since Integrations uses it too. |
| 63 | + |
| 64 | +## Non-UI code lives once |
| 65 | + |
| 66 | +A view's data and logic belong in `apps/sim/platform/embedded/lib/data` when only these views use them, and in `apps/sim/platform/shared` when the zone has the same thing. A view reads hooks through that layer; a real adapter owns the mutation, its toasts and any mapping, and is never a re-export. Before adding a file to `embedded/`, look for its counterpart under `apps/sim/platform/lib` and follow "Converging a domain" in `apps/sim/platform/AGENTS.md`. `check:platform-convergence` fails a new file here or a new path both trees hold. |
| 67 | + |
| 68 | +## Legacy workspace settings |
| 69 | + |
| 70 | +`apps/sim/app/workspace/*/settings/**` keeps `SettingsPanel` from `@/components/settings/settings-panel`, |
| 71 | +which owns chrome through `settings/[section]/layout.tsx`. The `add-settings-page` skill describes it. |
| 72 | +New organization work never imports it. |
0 commit comments