|
| 1 | +--- |
| 2 | +paths: |
| 3 | + - "apps/sim/app/**/*.tsx" |
| 4 | + - "apps/sim/ee/**/*.tsx" |
| 5 | + - "apps/sim/components/**/*.tsx" |
| 6 | +--- |
| 7 | + |
| 8 | +# List & Menu Ordering |
| 9 | + |
| 10 | +**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 the sidebar, in a toolbar, in a column-header row. When the second presentation reorders that set, the user re-reads it from scratch every time. |
| 11 | + |
| 12 | +This is not a style preference. Order is the cheapest affordance a list has, and the only one that costs nothing to get right. |
| 13 | + |
| 14 | +## The rule |
| 15 | + |
| 16 | +Before writing a list of items, find where the user sees those same items *first*. That surface owns the order; your list mirrors it. |
| 17 | + |
| 18 | +| The list | Mirrors | |
| 19 | +| --- | --- | |
| 20 | +| Resource menus (`+` attach, `@` mention, resource-tab `+`) | the workspace **sidebar**, top-down | |
| 21 | +| A row / root **context menu** | that surface's **toolbar**, left-to-right → top-to-bottom | |
| 22 | +| Settings tab strip, recently-deleted tabs | the **settings nav**, top-down | |
| 23 | +| A "New …" menu | the order those things appear once created | |
| 24 | + |
| 25 | +Left-to-right becomes top-to-bottom. A toolbar reading `Filter · Sort · Export · Delete` becomes a menu reading Filter, Sort, Export, Delete — never alphabetized, never grouped by implementation, never "destructive last" unless the toolbar already puts it last. |
| 26 | + |
| 27 | +Platform-only entries (desktop **Browser** and **Terminal**) trail the shared set rather than interleaving, so the common prefix is identical on every platform. |
| 28 | + |
| 29 | +## Encode the order once |
| 30 | + |
| 31 | +An order duplicated across surfaces is an order that will drift. Export **one** constant and sort by it — do not hand-maintain a matching literal per menu. |
| 32 | + |
| 33 | +```ts |
| 34 | +/** Top-down order for every menu listing resource families, mirroring the sidebar. */ |
| 35 | +export const RESOURCE_MENU_ORDER: readonly MothershipResourceType[] = [ |
| 36 | + 'integration', 'task', 'table', 'file', 'filefolder', |
| 37 | + 'knowledgebase', 'log', 'workflow', 'folder', 'browser', 'terminal', 'generic', |
| 38 | +] |
| 39 | + |
| 40 | +export function byResourceMenuOrder<T extends { type: MothershipResourceType }>(a: T, b: T) { |
| 41 | + return RESOURCE_MENU_ORDER.indexOf(a.type) - RESOURCE_MENU_ORDER.indexOf(b.type) |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +Canonical instance: `app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx`, consumed by `useAvailableResources` and `ResourceMenuSections`. |
| 46 | + |
| 47 | +## Render kinds in one pass, not one phase per kind |
| 48 | + |
| 49 | +The most common way a canonical order gets silently defeated: emitting all items of one *kind* and then all of another. Every submenu-backed family lands above every flat family regardless of what the order constant says. |
| 50 | + |
| 51 | +```tsx |
| 52 | +// ✗ Bad — two phases; the trees always pin to the top |
| 53 | +<ResourceTreeSections sections={treeSections} /> |
| 54 | +{groups.filter((g) => !FOLDERED.has(g.type)).map(renderFlat)} |
| 55 | + |
| 56 | +// ✓ Good — one ordered pass; each entry picks its own rendering |
| 57 | +{entries.sort(byResourceMenuOrder).map((entry) => |
| 58 | + sectionByType.has(entry.type) ? renderTree(entry) : renderFlat(entry) |
| 59 | +)} |
| 60 | +``` |
| 61 | + |
| 62 | +The same trap appears as "render the pinned ones, then the rest", "render enabled, then disabled", and "render the groups, then the loose items". |
| 63 | + |
| 64 | +## When order may diverge |
| 65 | + |
| 66 | +Only for reasons the user can perceive: |
| 67 | + |
| 68 | +- **Search/filter results** rank by match quality — the whole point is that ranking beats position. |
| 69 | +- **User-controlled ordering** (drag-to-reorder, manual `sortOrder`) wins over any canonical order. |
| 70 | +- **Recency lists** ("Recent chats") order by time, which *is* the order the user reads them elsewhere. |
| 71 | + |
| 72 | +"Grouped by which hook provides it", "alphabetical because it was easy", and "that's the order the array was built in" are not reasons. |
| 73 | + |
| 74 | +## Reviewing |
| 75 | + |
| 76 | +When a diff adds or edits a list of items, ask: where does the user see this set already, and does this match? If the answer is a different file with a different order, the diff needs a shared constant, not a second literal. |
0 commit comments