@terreno/mcp 57.19.2 → 57.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +32 -269
  3. package/dist/bootstrap.d.ts +12 -0
  4. package/dist/bootstrap.d.ts.map +1 -1
  5. package/dist/bootstrap.js +77 -122
  6. package/dist/bootstrap.js.map +1 -1
  7. package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
  8. package/dist/docs/resources/overview.md +4 -2
  9. package/dist/docs/resources/patterns.md +237 -176
  10. package/dist/docs/resources/ui.md +48 -17
  11. package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  12. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  13. package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  14. package/dist/docs/upgrades/57.20.0.md +238 -0
  15. package/dist/docs/versioned/next/approval-rubric.md +30 -12
  16. package/dist/docs/versioned/next/explanation/README.md +5 -0
  17. package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
  18. package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
  19. package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  20. package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  21. package/dist/docs/versioned/next/explanation/authentication.md +45 -1
  22. package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
  23. package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
  24. package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
  25. package/dist/docs/versioned/next/explanation/organizations.md +37 -0
  26. package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
  27. package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
  28. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  29. package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
  30. package/dist/docs/versioned/next/how-to/README.md +7 -2
  31. package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
  32. package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  33. package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  34. package/dist/docs/versioned/next/how-to/circleci.md +15 -5
  35. package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  36. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  37. package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  38. package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  39. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  40. package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
  41. package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  42. package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  43. package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  44. package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  45. package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  46. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  47. package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  48. package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  49. package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  50. package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  51. package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  52. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  53. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  54. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  55. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  56. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  57. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  58. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  59. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  60. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  61. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  62. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  63. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  64. package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  65. package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  66. package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  67. package/dist/docs/versioned/next/reference/README.md +1 -0
  68. package/dist/docs/versioned/next/reference/account-security.md +700 -0
  69. package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
  70. package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
  71. package/dist/docs/versioned/next/reference/api.md +461 -35
  72. package/dist/docs/versioned/next/reference/cli.md +1 -1
  73. package/dist/docs/versioned/next/reference/comms.md +8 -0
  74. package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
  75. package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
  76. package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
  77. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  78. package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
  79. package/dist/docs/versioned/next/reference/syncdb.md +8 -1
  80. package/dist/docs/versioned/next/reference/ui.md +1271 -27
  81. package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  82. package/dist/docs/versioned/next/tutorials/README.md +4 -0
  83. package/dist/httpApp.d.ts +14 -0
  84. package/dist/httpApp.d.ts.map +1 -0
  85. package/dist/httpApp.js +99 -0
  86. package/dist/httpApp.js.map +1 -0
  87. package/dist/index.js +8 -90
  88. package/dist/index.js.map +1 -1
  89. package/dist/local/localTools.d.ts.map +1 -1
  90. package/dist/local/localTools.js +44 -14
  91. package/dist/local/localTools.js.map +1 -1
  92. package/dist/local/metro/metroDevSession.d.ts +18 -0
  93. package/dist/local/metro/metroDevSession.d.ts.map +1 -1
  94. package/dist/local/metro/metroDevSession.js +42 -3
  95. package/dist/local/metro/metroDevSession.js.map +1 -1
  96. package/dist/local/tools/databaseQuery.d.ts.map +1 -1
  97. package/dist/local/tools/databaseQuery.js +3 -13
  98. package/dist/local/tools/databaseQuery.js.map +1 -1
  99. package/dist/local/tools/databaseSchema.d.ts.map +1 -1
  100. package/dist/local/tools/databaseSchema.js +3 -13
  101. package/dist/local/tools/databaseSchema.js.map +1 -1
  102. package/dist/local/tools/localMongo.d.ts +10 -0
  103. package/dist/local/tools/localMongo.d.ts.map +1 -0
  104. package/dist/local/tools/localMongo.js +30 -0
  105. package/dist/local/tools/localMongo.js.map +1 -0
  106. package/dist/local/tools/runtime.d.ts +18 -2
  107. package/dist/local/tools/runtime.d.ts.map +1 -1
  108. package/dist/local/tools/runtime.js +75 -20
  109. package/dist/local/tools/runtime.js.map +1 -1
  110. package/dist/local/tools/syncDb.d.ts +11 -0
  111. package/dist/local/tools/syncDb.d.ts.map +1 -1
  112. package/dist/local/tools/syncDb.js +15 -3
  113. package/dist/local/tools/syncDb.js.map +1 -1
  114. package/dist/rateLimit.d.ts +35 -0
  115. package/dist/rateLimit.d.ts.map +1 -0
  116. package/dist/rateLimit.js +112 -0
  117. package/dist/rateLimit.js.map +1 -0
  118. package/dist/tools.d.ts.map +1 -1
  119. package/dist/tools.js +42 -60
  120. package/dist/tools.js.map +1 -1
  121. package/package.json +4 -3
  122. package/src/docs/guidelines/ui/guidelines/core.md +1 -1
  123. package/src/docs/resources/overview.md +4 -2
  124. package/src/docs/resources/patterns.md +237 -176
  125. package/src/docs/resources/ui.md +48 -17
  126. package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  127. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  128. package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  129. package/src/docs/upgrades/57.20.0.md +238 -0
  130. package/src/docs/versioned/next/approval-rubric.md +30 -12
  131. package/src/docs/versioned/next/explanation/README.md +5 -0
  132. package/src/docs/versioned/next/explanation/account-linking.md +169 -0
  133. package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
  134. package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  135. package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  136. package/src/docs/versioned/next/explanation/authentication.md +45 -1
  137. package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
  138. package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
  139. package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
  140. package/src/docs/versioned/next/explanation/organizations.md +37 -0
  141. package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
  142. package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
  143. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  144. package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
  145. package/src/docs/versioned/next/how-to/README.md +7 -2
  146. package/src/docs/versioned/next/how-to/audit-log.md +22 -3
  147. package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  148. package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  149. package/src/docs/versioned/next/how-to/circleci.md +15 -5
  150. package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  151. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  152. package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  153. package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  154. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  155. package/src/docs/versioned/next/how-to/password-reset.md +1 -1
  156. package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  157. package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  158. package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  159. package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  160. package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  161. package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  162. package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  163. package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  164. package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  165. package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  166. package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  167. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  168. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  169. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  170. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  171. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  172. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  173. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  174. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  175. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  176. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  177. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  178. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  179. package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  180. package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  181. package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  182. package/src/docs/versioned/next/reference/README.md +1 -0
  183. package/src/docs/versioned/next/reference/account-security.md +700 -0
  184. package/src/docs/versioned/next/reference/admin-backend.md +3 -1
  185. package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
  186. package/src/docs/versioned/next/reference/api.md +461 -35
  187. package/src/docs/versioned/next/reference/cli.md +1 -1
  188. package/src/docs/versioned/next/reference/comms.md +8 -0
  189. package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
  190. package/src/docs/versioned/next/reference/environment-variables.md +9 -3
  191. package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
  192. package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  193. package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
  194. package/src/docs/versioned/next/reference/syncdb.md +8 -1
  195. package/src/docs/versioned/next/reference/ui.md +1271 -27
  196. package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  197. package/src/docs/versioned/next/tutorials/README.md +4 -0
@@ -79,6 +79,14 @@ consentResponseSchema.index({userId: 1, consentFormId: 1});
79
79
  addDefaultPlugins(consentResponseSchema);
80
80
  ```
81
81
 
82
+ **Follow-up: provisional signing.** Staff can now record consent on a user's behalf. A
83
+ provisional `ConsentResponse` sets `provisional`, `provisionalNote`, and
84
+ `provisionalSignedBy`; it counts as consent but leaves the form in the user's pending list
85
+ until they answer it themselves, and it can be voided (`voidedAt`, `voidedBy`,
86
+ `voidReason`) without being deleted. That work is planned in
87
+ [`docs/plans/2026-10-23-provisional-consent.md`](../plans/2026-10-23-provisional-consent.md)
88
+ and explained in [`docs/explanation/consent-forms.md`](../explanation/consent-forms.md).
89
+
82
90
  ## **APIs**
83
91
 
84
92
  ### ConsentApp Plugin Routes
@@ -0,0 +1,297 @@
1
+ # ListDetailLayout: a route-driven list/detail layout for phone and tablet
2
+
3
+ **Status:** Approved
4
+ **Roadmap:** Area=`ui`, Target=`Next`, Impact=`Feature`
5
+ **Branch:** list-detail-layout
6
+ **Owner:** Josh Gachnang
7
+ **Created:** 2026-10-20
8
+
9
+ `@terreno/ui` ships `SplitPage` for list/detail screens. It keeps the selected item in
10
+ local state, so a selected item has no URL. Deep links and browser or Android back
11
+ don't work for it. The native build has no tablet layout at all; its first line is a
12
+ TODO. This adds `ListDetailLayout`, which is used from an Expo Router `_layout.tsx`.
13
+ On a phone it is a scrolling list that pushes a detail screen. On a tablet or wide
14
+ browser the list stays in a left pane and the detail opens on the right. The URL always
15
+ names the open item. App developers building mail-, inbox- or records-style screens get
16
+ one layout that behaves correctly on every device. SplitPage stays as it is, but its
17
+ docs point to the new layout as the preferred choice.
18
+
19
+ ## The idea
20
+
21
+ A route group owns the list/detail pair, so the router (not component state) holds the
22
+ selection:
23
+
24
+ ```tsx
25
+ // app/mail/_layout.tsx
26
+ export const unstable_settings = {initialRouteName: "index"};
27
+ const MailLayout: React.FC = () => (
28
+ <ListDetailLayout
29
+ detailHref={(id) => `/mail/${id}`}
30
+ indexHref="/mail"
31
+ emptyDetail={<Text>Select a message</Text>}
32
+ list={<MailList />}
33
+ />
34
+ );
35
+ // app/mail/index.tsx: the list on a phone, the empty state on a tablet
36
+ const MailIndex: React.FC = () => <ListDetailIndex />;
37
+ // app/mail/[id].tsx: an ordinary route screen
38
+ // MailList rows: const {selectedId, select} = useListDetail();
39
+ ```
40
+
41
+ `ListDetailLayout` always renders one Expo Router `<Stack>`, inside a detail-pane
42
+ wrapper that is present at every width. When the layout's own measured width is at
43
+ least `splitAtWidth` (default 768), it also renders the `list` as a sibling pane to the
44
+ left. The width is measured with `onLayout`; before the first layout it falls back to
45
+ the window width. Measuring the container means the layout still decides correctly
46
+ inside a sidebar or admin rail. The Stack's position in the tree never changes, so it
47
+ never remounts, and rotating a tablet or resizing a browser keeps the open item. On a
48
+ phone you get native push animations and swipe-back.
49
+
50
+ Rejected alternatives:
51
+
52
+ - A custom `Navigator` like `SidebarNavigation`: it has no native push animation or
53
+ iOS swipe-back on phones.
54
+ - Swapping `<Stack>` and `<Slot>` at the breakpoint: it remounts the navigator and
55
+ drops deep-linked routes (`example-frontend/app/_layout.tsx:285-290`).
56
+ - Expo Router's `unstable-split-view`: iOS only. Elsewhere it is a plain Slot, and it
57
+ can't sit inside another navigator.
58
+
59
+ ## The plan
60
+
61
+ | # | Task | Depends on | Lands in | Proves it |
62
+ | --- | --- | --- | --- | --- |
63
+ | T1 | Layout switch: list pane beside a never-remounted Stack | none | ui | bun tests: panes at 375/1024 px, Stack mount count 1 |
64
+ | T2 | Selection lives in the route: `useListDetail` select/clear/selectedId, plus a how-to | T1 | ui, docs | bun tests on router calls and params |
65
+ | T3 | Crossing the breakpoint keeps the open item | T2 | ui | bun tests on selectedId and the narrow detail |
66
+ | T4 | Demo mail example, standalone and inside SidebarNavigation | T2 | demo | registration + coverage tests, screenshot script |
67
+ | T5 | Example-frontend todo browser: Playwright phone/tablet + Maestro flow | T3 | example-frontend | Playwright spec at 390 and 1024 px; Maestro flow |
68
+ | T6 | Guidance: skills, rules, guidelines, changelog prefer ListDetailLayout | T2 | plugins, rules | `skills:check`, `rules:check`, `check:changelog` |
69
+ | T7 | Native proof on iOS and Android phone + tablet (demo build), best effort | T4 | .maestro | native Maestro flow on the available devices (D4) |
70
+
71
+ Tracer: T1 cuts through the new layout seam, from the measured width to the rendered
72
+ panes.
73
+ Out of scope: a built-in third (folders) pane; a non-router controlled variant; native
74
+ iOS UISplitViewController; changes to SplitPage behaviour (see Expansions).
75
+ Open risks:
76
+ - Native proof is best effort (D4). A device with no simulator available is reported as skipped, not proven.
77
+
78
+ ## Contract
79
+
80
+ Props live in `ui/src/Common.ts` next to `SplitPageProps`.
81
+
82
+ | Export | Shape | Notes |
83
+ | --- | --- | --- |
84
+ | `ListDetailLayout` | `React.FC<ListDetailLayoutProps>` | Used as a route group's `_layout.tsx` default export |
85
+ | `ListDetailLayoutProps.list` | `ReactElement` | Rendered in the left pane when split; by `ListDetailIndex` when not |
86
+ | `ListDetailLayoutProps.detailHref` | `(id: string) => Href` | Where `select(id)` navigates |
87
+ | `ListDetailLayoutProps.indexHref` | `string` | The group's own path (e.g. `/mail`); `clear()` target and the scope for `selectedId` |
88
+ | `ListDetailLayoutProps.emptyDetail` | `ReactElement?` | Right pane when split and nothing is selected |
89
+ | `ListDetailLayoutProps.splitAtWidth` | `number?`, default 768 | Split when the layout's measured width is ≥ this |
90
+ | `ListDetailLayoutProps.listWidth` | `number?`, default 360 | Left pane width when split |
91
+ | `ListDetailLayoutProps.detailRoute` | `string?`, default `"[id]"` | The child route name that holds the detail |
92
+ | `ListDetailLayoutProps.paramName` | `string?`, default `"id"` | Route param holding the item id |
93
+ | `ListDetailLayoutProps.screenOptions`, `children` | passthrough to `<Stack>` | `Stack.Screen` children and options |
94
+ | `ListDetailIndex` | `React.FC` | The group's `index.tsx`: `list` when not split, `emptyDetail` when split |
95
+ | `useListDetail()` | `{isSplit, selectedId?, select(id), clear()}` | Throws outside a `ListDetailLayout` |
96
+
97
+ `selectedId` comes from `useGlobalSearchParams()[paramName]`. It counts only while
98
+ `usePathname()` is exactly one segment below `indexHref` and `useSegments()` ends with
99
+ `detailRoute`. That way an `id` param from a parent dynamic segment or another route
100
+ never leaks in.
101
+
102
+ testIDs (fixed, for app e2e): `list-detail-layout`, `list-detail-list-pane`,
103
+ `list-detail-detail-pane`, `list-detail-empty`.
104
+
105
+ ## Tasks
106
+
107
+ - [x] **T1** — Render the list pane beside a never-remounted Stack on wide layouts
108
+ - Depends on: none
109
+ - Files: ui/src/ListDetailLayout.tsx, ui/src/isolated/ListDetailLayout.isolated.tsx, ui/src/Common.ts, ui/src/index.tsx, ui/src/SplitPage.tsx, ui/src/SplitPage.native.tsx, scripts/check-demo-coverage.ts, docs/reference/ui.md
110
+ - Delivers: `ListDetailLayout`, `ListDetailIndex`, `useListDetail().isSplit`, and the props in the Contract table except selection. `ListDetailLayout` and `ListDetailIndex` get temporary `DEMO_COVERAGE_ALLOWLIST` entries ("story lands in T4") so `check:demo-coverage` stays green.
111
+ - Criteria:
112
+ - `list-detail-detail-pane` wraps the `<Stack>` at every width.
113
+ - At a measured width of 1024, `list-detail-list-pane` (width `listWidth`, default 360) renders as a sibling before it, and `isSplit` is true.
114
+ - At 375 there is no list pane, and `isSplit` is false.
115
+ - Width comes from the root's `onLayout`, falling back to `useWindowDimensions` before the first layout event. A 1024 window with a 600 px layout event is not split.
116
+ - `splitAtWidth={1200}` at 1024 is not split. At exactly 768 with the default, it is split.
117
+ - Re-rendering 1024 → 375 → 1024 mounts the `<Stack>` exactly once (a mount-counting Stack mock).
118
+ - `ListDetailIndex` renders `list` when not split, and `emptyDetail` (wrapped in `list-detail-empty`) when split.
119
+ - `screenOptions` and `Stack.Screen` children reach the Stack.
120
+ - `useListDetail()` outside the layout throws a descriptive error.
121
+ - The left pane has a theme border divider. Colors come from theme tokens (no hex).
122
+ - Docs:
123
+ - `docs/reference/ui.md` gets a `### ListDetailLayout` section (a props table and the `_layout`/`index`/`[id]` example).
124
+ - The `### SplitPage` section and the `SplitPage` JSDoc (`ui/src/SplitPage.tsx`) say `ListDetailLayout` is preferred for new screens.
125
+ - In `ui/src/SplitPage.native.tsx`, the stale tablet TODO on line 1 is replaced by the same note.
126
+ - SplitPage keeps working and is not marked `@deprecated`.
127
+ - Verification: `cd ui && bun test ./src/isolated/ListDetailLayout.isolated.tsx` passes, using the mocked `useWindowDimensions` and `expo-router` from `ui/src/bunSetup.ts` and firing `layout` events. `bun run compile` and `bun run lint` pass.
128
+
129
+ - [x] **T2** — Keep the selected item in the route with useListDetail
130
+ - Depends on: T1
131
+ - Files: ui/src/ListDetailLayout.tsx, ui/src/isolated/ListDetailLayout.isolated.tsx, ui/src/Common.ts, ui/src/Box.tsx, ui/src/Box.test.tsx, docs/reference/ui.md, docs/how-to/build-list-detail-screens.md, docs/how-to/README.md
132
+ - Added in Pick: `Box` `accessibilityState.selected`, sent as `aria-current="true"` on a clickable row (ARIA forbids `aria-selected` on a button) and as `aria-selected` on selectable roles (`tab`, `option`, `row`, ...), so list rows can report selection; T4/T5 rows rely on it.
133
+ - Criteria:
134
+ - With `useSegments` mocked to end in `[id]` and `useGlobalSearchParams` returning `{id: "3"}`, `selectedId` is `"3"`.
135
+ - With segments ending in `index`, `selectedId` is `undefined` even if the params contain `id`.
136
+ - With `indexHref="/orgs/9/mail"`, a pathname of `/orgs/9/mail` and params `{id: "9"}` (a parent dynamic segment), `selectedId` is `undefined`.
137
+ - When not split, `select(id)` calls `router.push(detailHref(id))`.
138
+ - When split with nothing open, `select(id)` pushes.
139
+ - When split with a detail open, `select(id)` calls `router.replace(detailHref(id))`, so the stack is never deeper than index + one detail (D6).
140
+ - When split, `select(selectedId)` is a no-op.
141
+ - `clear()` calls `router.dismissTo(indexHref)` when an item is open, so it never leaves the group, and does nothing when none is open. (Changed in Pick: `router.back()` asks the root navigator and can leave the group.)
142
+ - When split, Stack screens get `headerBackVisible: false` and `animation: "none"`. (Added in T4: also a `headerLeft`, because expo-router's web header ignores `headerBackVisible`. Changed after T5's Roast: that `headerLeft` is a `list-detail-leave` back button that leaves the group through the parent navigator, or nothing when the parent cannot go back, so a tablet is never stuck inside the group.) The caller's `screenOptions` win where they set the same key.
143
+ - Docs:
144
+ - The `ui.md` section documents `useListDetail` and the history rule.
145
+ - New `docs/how-to/build-list-detail-screens.md`, linked from `docs/how-to/README.md`, covers:
146
+ - the route group (`_layout`/`index`/`[id]`)
147
+ - `unstable_settings.initialRouteName` for deep links on phones
148
+ - selecting rows with `accessibilityState.selected`
149
+ - nesting inside `SidebarNavigation`
150
+ - when to use SplitPage instead (non-route list/detail)
151
+ - Verification: `bun test ./src/isolated/ListDetailLayout.isolated.tsx` asserts the router mock calls and screen options at 375 and 1024 px.
152
+
153
+ - [x] **T3** — Keep the open item when crossing the breakpoint
154
+ - Depends on: T2
155
+ - Files: ui/src/ListDetailLayout.tsx, ui/src/isolated/ListDetailLayout.isolated.tsx, docs/reference/ui.md
156
+ - Criteria:
157
+ - With a detail open (segments `[id]`, `id: "3"`), layout events 1024 → 375 → 1024 leave `selectedId` `"3"` throughout.
158
+ - At 375 with segments ending in `[id]`, there is no list pane and the detail route stays mounted. The mocked Stack renders no routes; T5's Playwright resize check proves the rendered detail.
159
+ - Changed in Pick: `ListDetailIndex` keeps rendering `list` at 375 while the detail is open, because it is the index screen under the detail and swipe-back must reveal the list.
160
+ - No router call is made during a resize.
161
+ - Docs: the `ui.md` section notes that rotation and resize keep the open item, and that the list pane remounts (its scroll position resets; X5).
162
+ - Verification: `bun test ./src/isolated/ListDetailLayout.isolated.tsx`.
163
+
164
+ - [x] **T4** — Add the demo mail example, standalone and inside SidebarNavigation
165
+ - Depends on: T2
166
+ - Files: demo/app/demo/_layout.tsx, demo/app/demo/list-detail/_layout.tsx, demo/app/demo/list-detail/index.tsx, demo/app/demo/list-detail/[id].tsx, demo/components/MailList.tsx, demo/components/MailMessage.tsx, demo/components/mailFixtures.ts, demo/app/demo/sidebar-navigation/_layout.tsx, demo/app/demo/sidebar-navigation/mail/_layout.tsx, demo/app/demo/sidebar-navigation/mail/index.tsx, demo/app/demo/sidebar-navigation/mail/[id].tsx, demo/story-config/ListDetailLayout.config.tsx, demo/demoConfig.tsx, demo/p0Stories.registration.test.ts, scripts/check-demo-coverage.ts, scripts/captureListDetail.ts
167
+ - Added in Pick: demo/stories/ListDetailLayout.stories.tsx (route launcher), demo/components/MailLayout.tsx (shared by both mounts), package.json `ui:list-detail:capture` + knip.jsonc entry, and ui/src/ListDetailLayout.tsx `headerLeft: () => null` in split mode (the web header ignored `headerBackVisible` and still drew a back arrow).
168
+ - Delivers:
169
+ - An inbox of about 30 static messages (sender, subject, preview, a date via Luxon) in shared `MailList` / `MailMessage` components.
170
+ - Rows highlight the selected message and set `accessibilityState={{selected: true}}`.
171
+ - The same group is mounted twice: standalone at `/demo/list-detail`, and at `/demo/sidebar-navigation/mail` as a new "Mail" sidebar item, proving the composition D1 relies on.
172
+ - A `ListDetailLayoutConfiguration` story (`component: ListDetailLayout`) links to both routes, like `SidebarNavigationExpoRouterConfiguration`.
173
+ - T1's temporary allowlist entry for `ListDetailLayout` is removed. `ListDetailIndex` keeps an allowlist entry with the reason "Covered by the ListDetailLayout story".
174
+ - `demo/app/demo/_layout.tsx` registers `list-detail` with `headerShown: false`, as it already does for `sidebar-navigation`, so phones don't show a double header.
175
+ - Criteria:
176
+ - `demo/p0Stories.registration.test.ts` asserts `ListDetailLayoutConfiguration` is registered.
177
+ - From the repo root, `bun run scripts/check-demo-coverage.ts` reports nothing missing for `ListDetailLayout` or `ListDetailIndex`.
178
+ - Root `scripts/captureListDetail.ts` uses `@playwright/test`'s `chromium` (like `scripts/charts/compareRenderedSnapshots.ts`) against the running demo web server. It captures screenshots to `/opt/cursor/artifacts/list-detail-demo-*.png` and exits non-zero when any expected testID is missing:
179
+ - `/demo/list-detail` at 390×844: the list, no list pane.
180
+ - `/demo/list-detail/3` at 390×844: only message 3.
181
+ - `/demo/list-detail/3` at 1024×768: both panes, row 3 selected.
182
+ - `/demo/sidebar-navigation/mail/3` at 1280×800: both panes beside the sidebar.
183
+ - Verification:
184
+ - `cd demo && bun test --only-failures p0Stories.registration.test.ts`
185
+ - `bun run scripts/check-demo-coverage.ts` from the repo root
186
+ - `cd demo && bun run web`, then `bun run ui:list-detail:capture` from the repo root
187
+ - `bun run check:knip` passes
188
+ - Behaviour against a real router and data is proven in T5; this script is the visual evidence.
189
+
190
+ - [x] **T5** — Browse todos in a list/detail layout in example-frontend
191
+ - Depends on: T3
192
+ - Files: example-frontend/app/todo-browser/_layout.tsx, example-frontend/app/todo-browser/index.tsx, example-frontend/app/todo-browser/[id].tsx, example-frontend/components/TodoBrowserList.tsx, example-frontend/components/SyncTodosScreen.tsx, example-frontend/app/_layout.tsx, example-frontend/e2e/todo-browser.spec.ts, .maestro/flows/todo-browser.yaml, .maestro/flows/config.yaml
193
+ - Delivers:
194
+ - `/todo-browser` lists the user's todos with syncdb `useEntityIds`, and shows one todo's title, status and created date with `useTodo`.
195
+ - The detail has a complete toggle.
196
+ - It is linked from the todos screen (`todos-browse-button`) and registered in the root Stack as `todo-browser`. The root Stack already sets `headerShown: false`, and the group's own Stack supplies the header.
197
+ - testIDs: `todo-browser-item-{id}` (with `aria-current="true"` when selected), `todo-browser-detail-{id}`, `todo-browser-detail-toggle`.
198
+ - Criteria (Playwright, `loginAs` in `beforeEach`, seeded todos):
199
+ - Phone (390×844): tapping an item shows `todo-browser-detail-{id}` with no `list-detail-list-pane`. Browser back returns to the list.
200
+ - Tablet (1024×768):
201
+ - `list-detail-list-pane` and `list-detail-empty` are visible.
202
+ - Tapping item A shows its detail, the list stays visible, the URL is `/todo-browser/{A}`, and item A has `aria-current="true"`.
203
+ - Tapping item B replaces the detail and moves `aria-current` to B.
204
+ - Browser back then returns to `/todo-browser`.
205
+ - Deep link: `/todo-browser/{id}` at 1024×768 shows the list with that row `aria-current="true"`, plus the detail.
206
+ - Resize: with a detail open at 1024×768, `setViewportSize(390×844)` keeps `todo-browser-detail-{id}` visible.
207
+ - Keyboard: at 1024×768, Tab from the last list row moves focus into `list-detail-detail-pane`.
208
+ - The Maestro web flow `.maestro/flows/todo-browser.yaml` passes under `bun run maestro:test`:
209
+ - It is listed in `.maestro/flows/config.yaml` after `create-todo`.
210
+ - It reuses the session from `login.yaml`, so it has no login of its own.
211
+ - Steps: open the browser, tap a todo, assert the detail, go back, assert the list.
212
+ - Verification:
213
+ - `cd example-frontend && bunx playwright test e2e/todo-browser.spec.ts`
214
+ - `bun run maestro:test`
215
+ - A terreno:verify-ui-changes run saves screenshots to `/opt/cursor/artifacts/`. Native devices are T7.
216
+ - Added in Pick: docs/explanation/example-coverage.md gets a List/detail row. The Maestro web flow could not run in this sandbox (the existing `login.yaml` flow also fails here: headless Chromium under gVisor); CI's `run-maestro` job runs it.
217
+
218
+ - [ ] **T7** — Prove the layout on native phones and tablets
219
+ - Depends on: T4
220
+ - Status: follow-up after this PR (D4: best effort). No simulator was available, so the flow, the script, and the native runs are not in the PR. The demo marks iOS and Android `inProgress` until this task passes.
221
+ - Files: .maestro/flows/native/list-detail.yaml, package.json, docs/how-to/build-list-detail-screens.md
222
+ - Delivers:
223
+ - A native Maestro flow (`appId: com.terreno.demo`) over the demo mail example, which needs no backend.
224
+ - A root script `maestro:native:list-detail` to run it.
225
+ - Criteria, each recorded with a screenshot in `/opt/cursor/artifacts/`:
226
+ - **iPhone simulator:** opening a message pushes it with no list pane, and an edge swipe-back returns to the list.
227
+ - **iPad simulator (≥ 768pt), landscape:** list and message side by side.
228
+ - **iPad, rotated to portrait and back with a message open:** the message stays open.
229
+ - **Android phone emulator:** opening a message pushes it, and hardware back (`pressKey: back`) returns to the list.
230
+ - **Android tablet emulator (≥ 768dp):** both panes, and rotating with a message open keeps it.
231
+ - Docs: the how-to's verification section names the flow and the four devices.
232
+ - Verification:
233
+ - `bun run maestro:native:list-detail` on each device, using a demo dev build (`demo` `ios` / `android` scripts).
234
+ - The Android emulator comes from the cloud toolchain (`install-cloud-dev-tools.sh`).
235
+ - Best effort (D4): run on every device that is available. The stage result lists each device as passed, failed, or skipped (no device). A skipped device does not block the PR, and the PR body names it. A device that ran and failed does block.
236
+
237
+ - [x] **T6** — Point framework guidance at ListDetailLayout
238
+ - Depends on: T2
239
+ - Files: plugins/terreno-planning/skills/terreno-ui/SKILL.md, plugins/terreno-planning/skills/terreno-ui/references/layout.md, plugins/terreno-planning/skills/building-terreno-apps/references/route-structure.md, .rulesync/rules/00-root.md, .rulesync/rules/01-claudecode-root.md, .rulesync/rules/ui/00-ui.md, ui/.ai/guidelines/core.md, mcp-server/src/docs/resources/ui.md, changelog/unreleased/list-detail-layout.md. Regenerated, not hand-edited: skills/, plugins/terreno-claude/, the rulesync outputs (AGENTS.md, CLAUDE*.md, .claude/, .cursor/, .devin/, .github/), mcp-server/src/docs/guidelines/.
240
+ - Criteria:
241
+ - In the terreno-ui skill, the line 153 row `| Two-column layout | \`SplitPage\` |` recommends `ListDetailLayout` for route-backed list/detail, and SplitPage for non-route uses.
242
+ - `route-structure.md` shows the `_layout`/`index`/`[id]` group.
243
+ - `- **Layout**: Box, Page, SplitPage, Card` (`.rulesync/rules/00-root.md:325`, `01-claudecode-root.md:222`, and the same line in `ui/.ai/guidelines/core.md:5`) gains `ListDetailLayout`. So does the `### Layout` list at `.rulesync/rules/ui/00-ui.md:81`.
244
+ - The changelog entry is `category: Added`.
245
+ - Added in Pick: root `AGENTS.md` and the per-package `AGENTS.md` / `.claude/CLAUDE.local.md` / `.cursor` / `.github` copies are hand-maintained (no generator writes them), so the same Layout line was updated in each.
246
+ - Verification:
247
+ - Run `bun run skills:sync`, `bun run rules`, and `cd mcp-server && bun run sync-package-guidelines`. Then `bun run skills:check` and `bun run rules:check` pass.
248
+ - `bun run check:changelog` passes.
249
+ - `grep -rn ListDetailLayout plugins/terreno-claude/skills/terreno-ui skills/terreno-ui` matches.
250
+
251
+ ## Assumptions
252
+
253
+ - `@terreno/ui` already depends on `expo-router` (`ui/package.json`), and `SidebarNavigation` is an expo-router precedent, so no new dependency or subpath export is needed.
254
+ - Tablets are detected by width only, as elsewhere in ui (there is no `Platform.isPad` use).
255
+ - Under the default, iPads at least 768pt wide in portrait split.
256
+ - iPad mini portrait (744pt) and narrow iPad Split View get the phone layout.
257
+ - Apps set `unstable_settings = {initialRouteName: "index"}` in the group layout so that a deep link on a phone has the list to go back to. The how-to and both examples do this.
258
+ - The demo static export lists each fixture id from `generateStaticParams` on both `[id]` routes. The Netlify demo deploy also rewrites unknown paths to `index.html`. Without those, `/demo/list-detail/2` and `/demo/sidebar-navigation/mail/2` 404 on the static host.
259
+ - The list pane remounts when crossing the breakpoint, so its scroll position resets. The open item does not.
260
+ - No backend, model, or SDK change: T5 reuses existing synced todos. No PHI is involved.
261
+
262
+ Supporting skills: terreno:terreno-ui, terreno:building-terreno-apps,
263
+ terreno:terreno-data-fetching (T5), terreno:update-docs, terreno:verify-ui-changes,
264
+ accessibility-testing.
265
+
266
+ ## Open questions (recommendation assumed)
267
+
268
+ None.
269
+
270
+ ## Expansions (follow up later)
271
+
272
+ | ID | Idea | Why it came up | Rough size | Depends on |
273
+ | --- | --- | --- | --- | --- |
274
+ | X1 | Built-in third pane (folders / nav \| list \| detail) | "side bar" in the request; you chose two panes | M | T3 |
275
+ | X2 | Controlled, no-router variant (`selectedId`/`onSelect`) for modals and tabs | You chose router-only | S | T1 |
276
+ | X3 | Use `expo-router/unstable-split-view` on iOS for a native UISplitViewController | Exists in expo-router 57, iOS only | M | T3 |
277
+ | X4 | Migrate SplitPage's demo stories, or deprecate it, once apps have moved | You chose to keep both | S | T6 |
278
+ | X5 | Keep list scroll position across the breakpoint | List pane remounts on resize | S | T3 |
279
+ | X6 | Tablet/phone Playwright projects for the whole example-frontend suite | Today there is only one 1280×900 viewport (`example-frontend/playwright.config.ts:91`) | S | T5 |
280
+
281
+ ## Decisions
282
+
283
+ | ID | Question asked | Answer | What it changes |
284
+ | --- | --- | --- | --- |
285
+ | D1 | How many panes should the tablet layout have? | Two panes (list \| detail) | No built-in nav column; T4 proves it composes with `SidebarNavigation`; a third pane is X1 |
286
+ | D2 (durable) | Should the selected item live only in the URL, or also be usable without routes? | Router layout only | The selection is always a route; no controlled variant (X2) |
287
+ | D3 | What should happen to SplitPage? | Keep both, but note in SplitPage that the new component is the preferred system | T1 adds a "prefer ListDetailLayout" note to SplitPage's JSDoc and docs; there is no `@deprecated` tag and no removal |
288
+ | D4 | Must native phone + tablet proof (iOS and Android) pass before the PR, even if it waits on a human for a simulator? | Best effort | T7 runs on the available devices and reports skips; skipped devices don't block the PR |
289
+ | D5 | At what width should it split? | 768 (layout width, overridable with `splitAtWidth`) | As planned. iPad mini portrait and 600–767dp Android tablets get the phone layout |
290
+ | D6 | In split mode, should picking a second item add a history entry? (approved with the plan) | No, replace it | As planned in T2/T5 |
291
+ | D7 | Name? (approved with the plan) | `ListDetailLayout` / `ListDetailIndex` / `useListDetail` | As planned |
292
+ | D8 | Which example app proves it end to end? (approved with the plan) | example-frontend todos plus a static demo inbox | As planned in T4/T5 |
293
+
294
+ ## Sign-off
295
+
296
+ Status: approved 2026-10-20 (chat)
297
+ Cut: 2 rounds, 20 findings: 19 fixed, 1 moved to questions or expansions (native proof → T7, settled as D4), 0 rebutted
@@ -0,0 +1,211 @@
1
+ # Login screen redesign — build handoff
2
+
3
+ **Repo:** `TerrenoLabs/terreno`, branch `auth-experience` (PR #1613)
4
+ **Design canvas:** https://claude.ai/artifact/4AExCVaPYePwyRym9twPsn (12 boards; static HTML copies are in `boards/` next to this file)
5
+ **Scope:** visual and layout changes to the login flow in `@terreno/ui` and `@terreno/rtk`. No API, auth-logic or server changes.
6
+
7
+ Build everything with existing `@terreno/ui` primitives (`Box`, `Text`, `Heading`, `Button`, `TextField`, `Banner`, `Avatar`, `Icon`, `IconButton`, `Badge`, `Spinner`) and theme roles. Never hard-code hex values; the boards use raw hex only because they are static mockups. The role each color maps to is listed under "Tokens" below.
8
+
9
+ ---
10
+
11
+ ## 1. What is wrong today (current `ui/src/login/LoginScreen.tsx`)
12
+
13
+ 1. Up to three section dividers ("Or sign in another way", "Or log in with email", "Or continue with") on one screen.
14
+ 2. The passkey button is stranded at the very bottom, below "Sign up".
15
+ 3. The `error` text renders under the password field even when the failure came from a social button at the top ("Auth cancelled").
16
+ 4. Each remembered account is its own bordered card with a grey "Forget" pill button and a third "Uses Apple" line.
17
+ 5. `LastUsedBadge` floats right-aligned *above* the button it labels.
18
+ 6. Social buttons are three different fills (black, white, near-black).
19
+ 7. "Forgot password?" is a `muted` pill button and "Need an account? Sign Up" is a full-width outline button, so secondary actions look like primary ones.
20
+
21
+ ---
22
+
23
+ ## 2. Target layout
24
+
25
+ Mobile-first, centered column, `maxWidth={400}`, screen padding `spacing4` horizontally. Vertical rhythm: `gap={6}` (24px) between blocks, `gap={3}` (12px) inside a button stack, `gap={4}` (16px) between form fields.
26
+
27
+ Order, top to bottom (each block only renders when it applies):
28
+
29
+ 1. **Logo** (`logo` prop, unchanged).
30
+ 2. **Heading block** — `Heading size="xl"` + one line of `Text color="secondaryLight"` subtitle.
31
+ - With continue-as accounts: "Welcome back" / "Pick up where you left off."
32
+ - Without: "Sign in" / "Choose how you want to sign in."
33
+ - Add a `subtitle?: string` prop; keep `title` overridable. Default titles above replace "Welcome Back".
34
+ 3. **Feedback banner** — the single place for `notice` *and* `error` (see §4). Never under a field, except field-level errors (§4.3).
35
+ 4. **Continue-as list** (§3.1), if there are accounts.
36
+ 5. **Methods**:
37
+ - With accounts: collapsed behind one `Button variant="muted" fullWidth text="Sign in another way"`. Pressing it expands, in place, a divider "or sign in another way" followed by the method stack and the email form. (Local `useState`; reset to collapsed on unmount.)
38
+ - Without accounts: always expanded.
39
+ 6. **Method stack** — every social provider, generic provider and passkey as identical full-width outline pills (§3.2), in this order: last-used method first, then branded providers in their given order, then generic providers, then passkey.
40
+ 7. **One divider** — "or use your email" (only when there is a form *and* at least one method above it).
41
+ 8. **Email form** — fields, then the primary submit. "Forgot password?" is a text link on the password field's label row (§3.3).
42
+ 9. **Footer** — pushed to the bottom: `Text`: "New here? " + link "Create an account" (`onSignUpPress`). Not a button.
43
+
44
+ There is never more than **one visible divider** above the form (two total in the expanded returning state: one before methods, one before email).
45
+
46
+ ---
47
+
48
+ ## 3. Components
49
+
50
+ ### 3.1 Continue-as list (rewrite `LoginAccountCard.tsx`, add a list wrapper)
51
+
52
+ - Section header row: `Heading size="sm"` "Continue as" on the left; text link "Edit" on the right (only when `onForget` is set and at least one remembered account exists).
53
+ - One container: `border="default"`, `rounding="lg"` (16px), `overflow hidden`. Rows separated by a 1px `border-hover` top border, no per-row borders.
54
+ - Row (whole row is the `Pressable`, min height 68, padding `spacing3`/`spacing4`):
55
+ - `Avatar size="sm"` (40px) or the `circle-user` icon fallback.
56
+ - Line 1: name, `Text bold`, truncate; if this account's method equals `lastUsedMethod`, an inline "Last used" pill right after the name (§3.4).
57
+ - Line 2 (`size="sm" color="secondaryLight"`): provider icon (12px, brands set: `apple`/`google`/`github`; `key` for passkey; `envelope` for email) + "`{Provider} · {masked email}`". For a session row: green presence dot + "Signed in · {email}".
58
+ - Trailing `chevron-right` icon, `color="secondaryLight"`.
59
+ - Remove the "Uses Apple" line and the "Forget" button from the row.
60
+ - **Edit mode** (new local state, header link toggles "Edit" ↔ "Done"):
61
+ - Header text becomes "Accounts on this device".
62
+ - Rows stop being pressable; chevron is replaced by a `Button variant="ghost"` with `text="Remove"` in error text color, 44px touch height, `accessibilityLabel="Remove {name}, {Provider}, from this device"`. Calls `onForget`.
63
+ - Session rows show `Text size="sm" color="secondaryLight"` "Can't remove" instead (sessions were never forgettable — keep that rule).
64
+ - Under the list: `Text size="sm" color="secondaryLight"`: "Removing an account only forgets it on this device. It doesn't sign you out anywhere else or delete the account."
65
+ - If the last remembered account is removed, exit edit mode automatically.
66
+ - Keep testIDs: `${testID}-continue-${index}` on each row; `${testID}-forget-${index}` moves to the Remove button. Add `${testID}-accounts-edit`.
67
+
68
+ ### 3.2 Method buttons (`OAuthButtons` / `SocialLoginButton`)
69
+
70
+ - Use `SocialLoginButton variant="outline"` for every provider (Apple, Google, GitHub) — it already exists. Do not use the black/`#24292e` fills on the login screen.
71
+ - Icons: monochrome, `text.primary`, 18px, pinned left at 20px inside the pill; label centered. Height 48, `radius3xl`.
72
+ - Passkey: same style, `iconName="key"`, "Sign in with a passkey". It lives in the method stack, never below the form.
73
+ - Generic providers: same style; use their icon if provided, else none.
74
+ - `OAuthButtons` currently draws its own divider (`dividerPosition`/`dividerText`). On the login screen pass `dividerPosition="none"`; `LoginScreen` owns the single divider.
75
+
76
+ ### 3.3 Email form
77
+
78
+ - `TextField` titles: "Email", "Password". Email placeholder "you@example.com"; password has no placeholder.
79
+ - "Forgot password?" → text link (`Text`/`Link` in `text.link`, bold, 14px) right-aligned on the same row as the Password label. If `TextField` cannot take a label accessory, render the label row yourself above a title-less `TextField` and keep `accessibilityLabel`.
80
+ - Submit: `Button variant="primary" fullWidth` text **"Sign in"** (was "Log In"). `loading` shows the spinner inside the button with text "Signing in"; all fields and other buttons `disabled`; wrap the screen with `aria-busy`.
81
+ - Keep the `FormContainer`, Enter-moves-focus and autofill behaviour from this PR untouched.
82
+
83
+ ### 3.4 "Last used" pill (replace `LastUsedBadge.tsx`)
84
+
85
+ - Inline pill, not a stacked badge: `Text size="sm" bold`, `color="secondaryDark"`, background `surface.secondaryExtraLight`, 1px border `surface.secondaryLight`, `radius3xl`, padding 2×8.
86
+ - On a method button: absolutely positioned on the right inside the pill, and the button itself gets `surface.secondaryExtraLight` fill with a 2px `surface.secondaryLight` border. The button also moves to the top of the stack (existing behaviour).
87
+ - On an account row: inline after the name.
88
+ - Email last used: pill on the "Email" label row instead of above the fields.
89
+ - Keep the testIDs `${testID}-last-used-${method}` and the `withLastUsedLabel` accessibility label.
90
+
91
+ ### 3.5 Continue as an email account (password-only step)
92
+
93
+ When a remembered/ session account with `method === "email"` and a usable email is tapped (today `useLoginController` just sets `initialValues`):
94
+
95
+ - Replace the whole screen body with:
96
+ - Top-left back link: `chevron-left` + "All sign-in options" (returns to the list, clears the prefill).
97
+ - `Heading` "Enter your password".
98
+ - Identity chip: `Box` with `surface.secondaryExtraLight` fill, 1px `surface.secondaryLight` border, `rounding="lg"`: avatar, name (bold), email (`sm`, secondaryLight), trailing "Not you?" link (same action as back).
99
+ - Hidden email input with `autocomplete="username"` so password managers still pair correctly; visible password field autofocused; "Forgot password?" on its label row; "Sign in" primary button.
100
+ - Footer unchanged.
101
+ - Implement as a `step` in `LoginScreen` (e.g. a new optional prop `continuingAccount?: LoginAccountOption` + `onCancelContinue?: () => void`), set by `useLoginController` instead of only `initialValues`. Fall back to today's info notice when the account has no usable email.
102
+
103
+ ---
104
+
105
+ ## 4. Feedback
106
+
107
+ ### 4.1 Placement
108
+ `notice` and `error` both render as a banner directly under the heading block, above accounts and methods. Remove the `<Text color="error">` under the fields. Only one banner at a time; `notice` wins if both are set.
109
+
110
+ ### 4.2 Banner styles (extend `NoticeBox`, or use `Banner` if it can take a title and an action)
111
+
112
+ | Tone | Fill | Border | Icon (left, 20px) | Text |
113
+ |---|---|---|---|---|
114
+ | info | `surface.secondaryExtraLight` | `surface.secondaryLight` | `circle-info`, `text.secondaryDark` | `text.secondaryDark` |
115
+ | warning | `surface.warningLight` | `border.warning` | `triangle-exclamation`, `text.warning` | `text.primary` |
116
+ | error | `surface.errorLight` | `border.error` | `circle-exclamation`, `text.error` | `text.primary` |
117
+
118
+ - `rounding="lg"`, padding `spacing3`/`spacing4`, 16px text.
119
+ - Optional bold title line (add `title?: string` to `LoginNotice`).
120
+ - Optional action: small outline pill button under the text (`actionLabel`/`onAction`, already in the type).
121
+ - Info banners are dismissible (`IconButton` "xmark", `accessibilityLabel="Dismiss"`); warnings/errors are not.
122
+ - `role="status"` for info, `role="alert"` for warning/error.
123
+
124
+ ### 4.3 Copy and which tone (edit `rtk/src/auth/loginErrors.ts` and `useLoginController.ts`)
125
+
126
+ | Situation | Tone | Title | Text | Action |
127
+ |---|---|---|---|---|
128
+ | Social sign-in cancelled by the user (today: "Auth cancelled") | info | — | "{Provider} sign-in was cancelled. Tap an account to try again." (no accounts: "…Try again or choose another way.") | — |
129
+ | `account_not_linked`, mode `explicit` | warning | "You already have an account" | "That email is already used with {originalMethod}. Sign in with {originalMethod}, then link {Provider} from Security." | "Sign in with {originalMethod}" (focuses that method / email field) |
130
+ | `account_not_linked`, mode `never` | warning | "You already have an account" | "That email already has an account. Sign in with {originalMethod} instead." | same |
131
+ | `account_not_linked`, `verified-email` | warning | "Verify your email first" | keep current text | — |
132
+ | Verify email / linking disabled / sign-up closed | warning | short noun title | keep current text | existing |
133
+ | Provider not found / generic failure | error | — | keep current text | — |
134
+ | Wrong email or password | **field error**, not a banner | — | "That password doesn't match. Try again or reset it." under the password field; field gets a 2px `border.error`, `aria-invalid`, `aria-describedby` | — |
135
+ | Continue with email account but no usable email | info | — | keep current "Enter your email and password to continue as {who}." | — |
136
+
137
+ The cancelled case needs a distinct failure code from the social flow so it isn't treated as an error; check how "Auth cancelled" reaches `getLoginFeedback` today and map it to the info notice.
138
+
139
+ ---
140
+
141
+ ## 5. Two-factor challenge (`ui/src/login/TwoFactorChallengeScreen.tsx`)
142
+
143
+ - Top-left back link `chevron-left` "Back" (`onCancel`).
144
+ - 56px badge: `rounding="lg"`, `surface.secondaryExtraLight`, `shield-halved` icon (TOTP) / `lock` icon (backup) in `text.secondaryDark`.
145
+ - `Heading` "Enter your code" (TOTP) / "Use a backup code" (backup). Default `title` changes from "Two-factor authentication".
146
+ - Subtitle: "Open your authenticator app and enter the 6-digit code for {masked email}." (add optional `accountLabel` prop; omit "for …" without it). Backup: "Enter one of the backup codes you saved when you turned on two-factor sign-in. Each code works once."
147
+ - `OtpField`: six boxes 48×56, 8px gaps, `radiusMd`, centered bold 24px digits; the active box gets the focus ring. Error: all boxes 2px `border.error` + message below with icon: "That code didn't work. Codes change every 30 seconds, so enter the one your app shows now." Keep single tab stop, paste and `one-time-code` autofill.
148
+ - Backup field: monospace, letter-spacing 2px, placeholder "xxxxx-xxxxx", helper "Dashes and spaces are optional."
149
+ - Trust device: checkbox + "Trust this device", helper line `sm` secondaryLight "We won't ask for a code here for {trustDeviceDays} days." Checked by default for TOTP. Only rendered with `trustDeviceDays`.
150
+ - Primary "Verify" button; below it a centered text link toggling "Use a backup code instead" / "Use your authenticator app"; bottom of screen a grey text link "Sign in a different way" (`cancelText` default changes from "Use another method").
151
+
152
+ ---
153
+
154
+ ## 6. Tokens used (light / dark come from the theme automatically)
155
+
156
+ | Role | Use |
157
+ |---|---|
158
+ | `surface.base` | screen, rows, outline buttons |
159
+ | `surface.baseAlternate` | "Sign in another way" muted button; disabled field fill |
160
+ | `surface.primary` | primary "Sign in"/"Verify", focus border |
161
+ | `surface.secondaryExtraLight` / `surface.secondaryLight` | last-used pill & highlighted button, identity chip, info banner, 2FA badge |
162
+ | `surface.warningLight` + `border.warning` | warning banner |
163
+ | `surface.errorLight` + `border.error`, `text.error` | error banner, field errors |
164
+ | `text.primary`, `text.secondaryLight`, `text.secondaryDark`, `text.link` | body, captions, pill/info text, links |
165
+ | `text.extraLight` | placeholders only |
166
+ | `border.default` | inputs, outline buttons, account list |
167
+ | `border.hover` | row separators |
168
+ | `border.focus` | 3px focus ring |
169
+ | `status.active` | session dot |
170
+
171
+ Type: `Heading` (Titillium Web) for titles: `xl` 32px page title, `sm` section titles. `Text` (Nunito) 16px body, 12px (`sm`) metadata. Primary buttons follow the library's 19px bold for contrast on `surface.primary`. Radii: inputs `radiusMd` 4px, lists/banners/chips `radiusLg` 16px, buttons and pills `radius3xl`. No shadows.
172
+
173
+ ---
174
+
175
+ ## 7. Implementation plan
176
+
177
+ 1. `ui/src/login/loginTypes.ts`: add `subtitle`, `LoginNotice.title`, `LoginNotice.tone: "info" | "warning" | "error"`, `continuingAccount`, `onCancelContinue`; add `accountLabel` to the 2FA props. All optional; keep backwards compatibility.
178
+ 2. `LastUsedBadge.tsx` → inline `LastUsedPill`.
179
+ 3. `LoginAccountCard.tsx` → row component; new `LoginAccountList.tsx` with header + edit mode.
180
+ 4. `LoginScreen.tsx`: new block order (§2), single banner (§4.1), collapse/expand with accounts, one divider, link-style forgot/sign-up, "Sign in" copy, continuing-account step (§3.5). Pass `dividerPosition="none"` and `variant="outline"` to `OAuthButtons`.
181
+ 5. `NoticeBox`: tones, title, action, dismiss (§4.2).
182
+ 6. `TwoFactorChallengeScreen.tsx` + `OtpField`: §5.
183
+ 7. `rtk/src/auth/loginErrors.ts`, `useLoginController.ts`, `TerrenoLoginScreen.tsx`: copy table §4.3, cancelled-as-info, field-level wrong password, set `continuingAccount`.
184
+ 8. Update tests and snapshots: `LoginScreen.test.tsx`, `TwoFactorChallengeScreen.test.tsx`, `loginErrors` tests, rtk `TerrenoLoginScreen` tests, and Playwright specs `login`, `login-form`, `two-factor` in `example-frontend`. About 33 test references use the old strings ("Log In", "Forget", "Or sign in another way", etc.). Keep testIDs stable wherever listed above so most selectors still work; only update text assertions.
185
+ 9. Update the `LoginScreen` and `TwoFactorChallengeScreen` story configs in `ui/demo/story-config/` with one story per state in §8.
186
+ 10. `bun run compile`, lint, the ui/rtk test suites, and the Playwright login shards must pass. Changelog entry under ui and rtk.
187
+
188
+ ---
189
+
190
+ ## 8. Acceptance: one story / screenshot per state
191
+
192
+ Match each against its board in `boards/` (and the canvas):
193
+
194
+ | # | State | Board |
195
+ |---|---|---|
196
+ | 1 | Returning, accounts, methods collapsed | `Main.html` |
197
+ | 2 | First visit, last used provider on top | `FirstVisit.html` |
198
+ | 3 | Returning, "Sign in another way" expanded | `AnotherWay.html` |
199
+ | 4 | Edit accounts | `ManageAccounts.html` |
200
+ | 5 | Continue as email account | `ContinueEmail.html` |
201
+ | 6 | Signing in (loading) | `SigningIn.html` |
202
+ | 7 | Social sign-in cancelled (info) | `Cancelled.html` |
203
+ | 8 | Account not linked (warning + action) | `NotLinked.html` |
204
+ | 9 | Wrong password (field error) | `WrongPassword.html` |
205
+ | 10 | 2FA authenticator code | `TwoFactor.html` |
206
+ | 11 | 2FA wrong code | `TwoFactorError.html` |
207
+ | 12 | 2FA backup code | `BackupCode.html` |
208
+
209
+ Also check every state in dark mode, at 320px width, and with only email+password enabled (no providers: no dividers at all) and with no form (providers only: no "or use your email" divider).
210
+
211
+ Out of scope: sign-up screen, account security screen, admin panels. "LOGO" in the boards is the app's own `logo` prop.