@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
@@ -0,0 +1,143 @@
1
+ # Responsive design
2
+
3
+ Terreno apps run on desktop web and native mobile. Responsive behavior follows the workflow, the available viewport, and how important that workflow is on that surface.
4
+
5
+ The goal is not to make every screen work everywhere. The goal is the best experience on the devices and workflows people actually use. Design specs name the supported breakpoints and the support level for each feature. Implement and test only those behaviors.
6
+
7
+ Breakpoints are viewport width in points, not a device model and not a physical screen resolution. Browser chrome, a resized window, split screen, and display scaling all change the available width.
8
+
9
+ ## Surfaces
10
+
11
+ Primary devices define the design target. Support for other devices comes from the breakpoint system.
12
+
13
+ | Surface | Who it is for | Where it runs | Design target | Role |
14
+ | --- | --- | --- | --- | --- |
15
+ | Desktop web | People doing data-heavy work | Web, validated in Chrome | 13" laptop at `xl` (≥ 1280pt), such as a MacBook Air 13" | Primary experience. Full functionality, including complex review and multi-panel workflows. |
16
+ | Primary mobile | People whose main experience is the phone or tablet | iOS and Android | Large phone / small tablet at native `lg` (600–1023pt), such as a Samsung A11 | Full supported mobile workflows. |
17
+ | Reduced mobile | People who need emergency or lightweight access | iOS and Android | Common phones | A reduced set of workflows. Not a replacement for desktop. |
18
+
19
+ Other browsers are outside the desktop support contract. Do not create a layout for an individual phone or tablet model.
20
+
21
+ ## Support levels
22
+
23
+ Every feature names one of these levels at each breakpoint it claims.
24
+
25
+ | Level | Definition | Example |
26
+ | --- | --- | --- |
27
+ | Fully supported | Designed and tested at this breakpoint. Intended workflows work. | A filter with search, groups, saved filters, and bulk actions on desktop `lg` and `xl`. |
28
+ | Supported with adaptation | Still fully usable. Layout or interaction is simpler so it fits. | The same filter on a tighter width becomes one category at a time, or moves advanced options behind an "Advanced filters" action. |
29
+ | Functionally available | Essential workflows can be finished. Complexity is intentionally omitted. | On reduced mobile, the filter keeps search and one or two high-value filters. |
30
+ | Not supported | Not available, not optimized, or not guaranteed. | The advanced filter below the minimum supported breakpoint. The app may still open. Layout and functionality are not guaranteed. |
31
+
32
+ ### What a feature spec must define
33
+
34
+ | Item | Required? |
35
+ | --- | --- |
36
+ | Supported surfaces (desktop web, primary mobile, reduced mobile) | Yes |
37
+ | Supported breakpoints (`sm`, `md`, `lg`, `xl`) | Yes |
38
+ | Support level at each of those breakpoints | Yes |
39
+ | Behavior when a breakpoint is not supported | Yes |
40
+ | Layout changes at supported breakpoints | When the layout changes |
41
+
42
+ ## Breakpoints
43
+
44
+ Tokens are platform-specific. `lg` is not the same width on web and native. The tables and the constants `WEB_BREAKPOINT_MIN_WIDTH` and `NATIVE_BREAKPOINT_MIN_WIDTH` are the same contract. API details: [Media query helpers](../reference/ui.md#media-query-helpers).
45
+
46
+ ### Desktop web
47
+
48
+ The desktop web app is optimized for desktop. The minimum supported desktop breakpoint is `lg`.
49
+
50
+ | Category | Token | Viewport | Meaning |
51
+ | --- | --- | --- | --- |
52
+ | Primary desktop | `xl` | ≥ 1280pt | Full desktop layouts, multi-panel workflows, dense data. |
53
+ | Smaller desktop | `lg` | 1024–1279pt | Minimum supported desktop. Collapse secondary content, adjust panels, or simplify controls. Keep core workflows. |
54
+ | Below supported desktop | below `lg` | < 1024pt | Not a supported desktop experience. The app may stay reachable. Layouts are not guaranteed. |
55
+
56
+ On web, `sm` (320) and `md` (375) still classify widths below 1024 so a layout can stay reachable.
57
+
58
+ Test `xl` fully, including complex layouts and multi-panel interactions. Test `lg` to confirm critical workflows stay usable when space shrinks. Do not do breakpoint-specific testing below `lg`. Degrade gracefully there when it is cheap.
59
+
60
+ ### Native mobile
61
+
62
+ | Category | Token | Viewport | Meaning |
63
+ | --- | --- | --- | --- |
64
+ | Extra large | `xl` | ≥ 1024pt | Not a supported mobile layout. Use desktop web where it exists. |
65
+ | Large mobile / tablet | `lg` | 600–1023pt | Primary tablet experience. Optimize supported mobile workflows here. |
66
+ | Standard mobile | `md` | 375–599pt | Common phone. Supported workflows stay usable. |
67
+ | Small mobile | `sm` | 320–374pt | Small phone. Critical workflows stay functional. Extra scrolling or a simpler layout is expected. |
68
+ | Below supported mobile | below `sm` | < 320pt | Not supported. The app may stay reachable. Layouts are not guaranteed. |
69
+
70
+ Test native `lg`, `md`, and `sm` unless the spec says otherwise. Do not do breakpoint-specific testing below `sm`.
71
+
72
+ A larger mobile screen does not automatically get the desktop layout. Use a breakpoint only when the layout actually changes.
73
+
74
+ `isSupportedDesktopWidth({width})` is the desktop floor: web `lg` and native `xl`, both 1024pt. Below that, do not ship a desktop multi-panel layout.
75
+
76
+ ## Adapting a layout
77
+
78
+ ### Desktop
79
+
80
+ Support a primary laptop display, a resized browser window, split screen, and a smaller window that is still inside `lg` or `xl`. Keep the workflow as width drops inside that range.
81
+
82
+ When width drops:
83
+
84
+ - Collapse secondary panels.
85
+ - Allow horizontal scrolling for a dense table.
86
+ - Move secondary actions into an overflow menu.
87
+ - Reduce spacing before removing content.
88
+
89
+ Do not shrink content until it is unreadable, compress a layout into a strip, or hide a critical workflow with no other way to reach it.
90
+
91
+ ### Native
92
+
93
+ Mobile layouts favor finishing the task over matching desktop feature parity.
94
+
95
+ Reduced mobile, when a desktop workflow is too complex:
96
+
97
+ - Simplify the workflow.
98
+ - Reduce the available actions.
99
+ - Keep the high-value, time-sensitive tasks.
100
+ - Send people to desktop when the workflow needs it.
101
+
102
+ Do not recreate a desktop layout on mobile, support every desktop workflow, show a dense table or a multi-panel layout, or require a lot of horizontal scrolling.
103
+
104
+ Primary mobile should stay clear, readable, touch-friendly, and focused on the supported workflows.
105
+
106
+ ## Checklist
107
+
108
+ Before building a new experience, answer these:
109
+
110
+ 1. Is this desktop-only, mobile-only, or both? Is the workflow required on reduced mobile, or only on primary mobile?
111
+ 2. What is the minimum supported width? When space decreases, does content collapse, stack, scroll, or hide?
112
+ 3. What is the primary goal, which information is essential, and which actions are secondary?
113
+ 4. Does this need a breakpoint, or a mobile variant of a component? Does it match an existing responsive pattern?
114
+
115
+ ## Framework component audit
116
+
117
+ Audited against this policy. Pixel cutoffs that were not a breakpoint token were moved onto `isSupportedDesktopWidth`.
118
+
119
+ | Component | Result |
120
+ | --- | --- |
121
+ | `Box` responsive props (`sm` / `md` / `lg` / `xl`) | Compliant. One shared dimension listener. The largest matching token wins. |
122
+ | `DashboardGrid` | Compliant. Column counts follow `sm`, `md`, and `lg` (`xl` uses the `lg` count). |
123
+ | `SplitPage` (web) | Compliant. Side-by-side layout follows `isNarrowViewport()` unless the caller passes `narrowBelowWidth`. |
124
+ | `SplitPage` (native) | Compliant. Stacked list/detail at every native width, including tablet `lg`. Native tablets do not inherit the desktop split. |
125
+ | `Modal` | Compliant. Action sheet on native, centered dialog on web. |
126
+ | `DataTable` | Partial. Web keeps inline filters. Native collapses filters into one sheet. Horizontal scroll stays on for wide tables, including native, which is more scrolling than reduced mobile should require. |
127
+ | `Text` / `Heading` | Compliant. Separate web and native type scales. Type does not shrink again inside `sm`. |
128
+ | `Card` | Fixed. Container and display variants used a 768pt cutoff. They now follow the desktop floor. |
129
+ | `MarkdownEditor` | Fixed. Side-by-side edit/preview used a 768pt cutoff. It now follows the desktop floor. |
130
+ | `AdminShell` | Fixed. The sidebar became a hamburger drawer below 768pt. It now does that below the desktop floor. |
131
+ | `AdminFilterDrawer` | Fixed. The bottom sheet used the same 768pt cutoff. It now follows the desktop floor. `ADMIN_FILTER_MOBILE_BREAKPOINT` is 1024. |
132
+ | `AdminHome` | Fixed. Main and sidebar stay side by side only at or above the desktop floor, and only when the dashboard is not embedded. Below the floor they stack. |
133
+ | Admin SPA shell | Compliant. It wraps `AdminShell`, so it uses the same floor. |
134
+ | Example app screens | Compliant. `maxWidth={800}` is a content measure. Toolbars that sit in a row also wrap. No second pixel cutoff. |
135
+ | Sync debugger | Fixed. The event list and JSON inspector stack below the desktop floor. The screen still uses raw `View` and `Text` on purpose. |
136
+ | Demo headers | Fixed. Back-button visibility and header margin followed `isNarrowViewport()` once at module load. They now follow the desktop floor and update when the window resizes. |
137
+ | Demo stories (Tooltip, Multiselect, Accordion) | Fixed. Width switches followed a one-time `isNarrowViewport()` read. They now follow the desktop floor and update on resize. |
138
+ | Admin action rows | Partial. Some rows (`ConsentHistory`, consent editors) stay in one row without `wrap`. They are not a second breakpoint. |
139
+ | `DateTimeField` | Partial. Inline controls switch to an action sheet when the parent is narrower than 395pt, and also when `isNarrowViewport()` is true. 395pt is the field's own minimum, not a viewport token. |
140
+ | `Image` (`fullWidth`) | Gap. Width is read from `Dimensions` once at module load, so it does not track resize or rotation. |
141
+ | `SideDrawer` | Partial. Width is 40% on web and 95% on native, by platform rather than by breakpoint token. |
142
+
143
+ Do not add a new raw pixel cutoff. Use `Box` breakpoint props, `useResponsiveBreakpoint`, or `isSupportedDesktopWidth`.
@@ -1342,6 +1342,17 @@ One easy-to-use harness inside `@terreno/ai` that runs long-lived agentic work d
1342
1342
 
1343
1343
  ---
1344
1344
 
1345
+ ## list-detail-layout
1346
+
1347
+ **Title:** `ListDetailLayout: a route-driven list/detail layout for phone and tablet`
1348
+
1349
+ **Labels:** `area:ui`, `type:feature`
1350
+ **Project fields:** Area=`ui`, Target=`Next`, Impact=`Feature`, IP=`list-detail-layout`, Status=`Planned`
1351
+
1352
+ - **Implementation plan:** [list-detail-layout.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/list-detail-layout.md)
1353
+
1354
+ ---
1355
+
1345
1356
  # Shipped, umbrella, and declined IPs
1346
1357
 
1347
1358
  Tracking issues created for IPs that previously lacked a `**Roadmap issue:**` header.
@@ -43,6 +43,11 @@ const routerDescription = describeModelForRouter(Todo, {
43
43
 
44
44
  System paths (`_id`, `__v`, `created`, `updated`, `deleted`) are marked with `system: true`.
45
45
 
46
+ `required` is true only when the field is required on every document. A conditional
47
+ `required` (a function, such as "required when `provisional` is true") leaves it false, so
48
+ OpenAPI and generated SDKs type the field as optional; Mongoose still enforces the condition
49
+ on save.
50
+
46
51
  ## Views (not canonical)
47
52
 
48
53
  | Surface | Function | Notes |
@@ -27,15 +27,18 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
27
27
  - [Durable background jobs](background-jobs.md) — `@terreno/jobs`: enqueue, workers, schedules, Cloud Tasks / Vercel Queues
28
28
  - [Create a Mongoose model](create-a-model.md) — Define models with proper conventions
29
29
  - [Enable the framework audit log](audit-log.md) — `AuditApp`, per-router `audit`, append-only events
30
+ - [Sign consent on a user's behalf](sign-consent-on-behalf.md) — Provisional consent signatures: RBAC grant, signer screen, status checks, void, print
30
31
  - [Seed a database](seed-a-database.md) — Idempotent sync, dry runs, and guarded reset-and-reseed
31
32
  - [Run MongoDB migrations](run-mongodb-migrations.md) — Versioned `up`/`down` files, `terreno-migrate`, production gate
32
33
  - [Add GitHub OAuth authentication](add-github-oauth.md) — Enable GitHub login for your API
33
34
  - [Configure Better Auth](configure-better-auth.md) — Set up Better Auth with social OAuth (Google, GitHub, Apple)
35
+ - [Set up social login](set-up-social-login.md) — Register Google, GitHub, and Apple once on the stable deploy, proxy previews through it, and move stable onto one domain
34
36
  - [Password reset and email verification](password-reset.md) — JWT routes, comms templates, Better Auth hooks
37
+ - [Recover a locked-out user](recover-locked-out-user.md) — Platform and org-admin recovery: 2FA, passkeys, providers, sessions, suspend
35
38
  - [Add WebSocket integration](websocket-integration.md) — Set up real-time Socket.io connections
36
39
  - [Expose Model Context Protocol tools](expose-mcp-tools.md) — Turn `modelRouter` models into MCP tools an LLM can call
37
- - [Set up Terreno MCP](set-up-terreno-mcp.md) — Configure hosted docs/codegen and local runtime tools
38
- - [Debug with MCP](debug-with-mcp.md) — Diagnose backend, browser, Metro, app, and client-state failures
40
+ - [Set up Terreno MCP](set-up-terreno-mcp.md) — Connect Cursor or Claude Code to the hosted docs/codegen server and the local runtime server
41
+ - [Debug with MCP](debug-with-mcp.md) — Follow a failure from symptom to verified fix with the local MCP tools
39
42
  - [Connect an MCP client with a service token](connect-mcp-service-token.md) — Mint a personal `mcp_` key for Perplexity and other remote clients
40
43
  - [Enable MCP OAuth for a JWT app](enable-mcp-oauth.md) — Let Claude.ai and ChatGPT connect to `/mcp` through OAuth 2.1 with your own consent screen
41
44
  - [Install agent skills](install-agent-skills.md) — `npx skills`, the Cursor plugin, the Codex plugin, or the Claude Code plugin
@@ -48,7 +51,9 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
48
51
  - [Call external APIs](call-external-apis.md) — Authenticated HTTP client, retries, and error normalization for third-party integrations
49
52
  - [Upgrade banner](upgrade-banner.md) — Soft warning and hard-block app update UX
50
53
  - [Upgrade Terreno](upgrade-terreno.md) — Lockstep `@terreno/*` bumps, note range, order, rollback
54
+ - [Automate Terreno upgrades](automate-terreno-upgrades.md) — Hourly workflow that launches one upgrade agent when a week-old release is out and no upgrade is open
51
55
  - [Track upstream Expo](track-upstream-expo.md) — Morning probe for SDK betas; `release-X.Y.Z` loop log + native fingerprint deps
56
+ - [Build a list/detail screen](build-list-detail-screens.md) — `ListDetailLayout` route group: list on a phone, list + detail panes on a tablet, selection in the URL
52
57
  - [Show loading skeletons](show-loading-skeletons.md) — `loading`, `loadingComponent`, and `loadingAnimation` on Box, Card, charts, and Page; `withLoading` for custom components
53
58
  - [Compose charts in a dashboard grid](charts-and-dashboards.md) — `LineChart` / `BarChart` / `AreaChart` / `DonutChart` inside `Card`s in `DashboardGrid` (ops-dashboard scorecards and spans: [dashboard-chart-parity](../implementationPlans/dashboard-chart-parity.md))
54
59
  - [Compare rendered chart snapshots](compare-chart-rendered-snapshots.md) — PNG goldens for the chart visual gallery, not JSON snapshots
@@ -38,7 +38,10 @@ modelRouter("/todos", Todo, {
38
38
  | PATCH `/:id/:field/:itemId` | `updated` | `arrayUpdate` | Changed fields; `recordId` set |
39
39
  | DELETE `/:id/:field/:itemId` | `updated` | `arrayRemove` | Changed fields; `recordId` set |
40
40
 
41
- `source` is `modelRouter` for these writes. Secrets (`password`, `hash`, `salt`, `token`,
41
+ `source` is `modelRouter` for these writes. `ConsentApp` also writes `modelRouter` events for
42
+ provisional consent actions: `created`/`create` for a provisional signature, `updated`/`update` for a void, and
43
+ `viewed`/`read` when staff read another user's consent statuses. `read` and `viewed` mark
44
+ audited reads; no other surface writes them. Secrets (`password`, `hash`, `salt`, `token`,
42
45
  `secret`, `refreshToken`) are omitted at every object and array depth, including compound
43
46
  keys such as `tokenHash`. Extra `redact` names merge with that list as exact field names.
44
47
 
@@ -51,6 +54,22 @@ Admin diffs also omit each model's `hiddenFields` and `excludeFields`.
51
54
 
52
55
  RBAC mutations fan into the same collection when `createAccess({auditSink: persistRbacAuditToAuditEvent})` is set. `source` is `rbac`.
53
56
 
57
+ ### 4. Account security actions
58
+
59
+ `AccountSecurityApp` writes the same collection for account recovery. It refuses to start
60
+ unless `AuditApp` is registered (or you pass `audit: false`).
61
+
62
+ | `source` | Written by | `modelName` / `recordId` | `after` |
63
+ | --- | --- | --- | --- |
64
+ | `admin` | AdminApp POST/PATCH/DELETE | Model / document id | Changed fields |
65
+ | `modelRouter` | `modelRouter` with `audit` | Model / document id | Changed fields |
66
+ | `rbac` | `createAccess({auditSink: persistRbacAuditToAuditEvent})` | RBAC target | Role change |
67
+ | `security` | `AccountSecurityApp` recovery actions (2FA reset and later actions) | `User` / target user id | `{securityAction, organizationId?}` |
68
+
69
+ A recovery call that changes nothing (resetting 2FA that is already off) writes no row. List
70
+ one user's security history with `GET /audit-events?source=security&recordId=<userId>`. Routes
71
+ and action names: [Account security reference](../reference/account-security.md).
72
+
54
73
  `organizationId` is copied onto the event when `req.organization` is set (`id` or `_id`), otherwise from the request org AsyncLocalStorage, then `X-Organization-Id`, then the mutated document's `organizationId` string. Socket admin-window mutates attach `organizationId` on the payload so non-tenant models still land in the selected organization's audit log. The field is omitted when none of those exist. Admin list/read for `AuditEvent` is not organization-scoped, so unscoped rows stay visible when no organization is selected.
55
74
 
56
75
  `actorId` comes from `req.user.id` (or `_id`). Only a 24-hex Mongo ObjectId is stored;
@@ -60,7 +79,7 @@ shape still writes the event with `actorId` omitted and a warning logged.
60
79
  Update and delete snapshot `before` only when `audit` is on. If that `toJSON` throws, the
61
80
  mutation still completes and the event is recorded without a `before` snapshot.
62
81
 
63
- ### 4. Fire-and-forget persist
82
+ ### 5. Fire-and-forget persist
64
83
 
65
84
  HTTP handlers do not await Mongo. Diffs still run before the response so `before` / `after` are captured. Failures log; the mutation stays 2xx.
66
85
 
@@ -80,6 +99,6 @@ Authenticated malformed queue payloads are acknowledged without persistence. The
80
99
  become valid on retry, and returning a non-2xx response would make Cloud Tasks retry the
81
100
  same poison task indefinitely.
82
101
 
83
- ### 5. Retention
102
+ ### 6. Retention
84
103
 
85
104
  Omit `retentionDays` or set `0` to keep events forever (no TTL index). `new AuditApp({retentionDays: 90})` creates a Mongo TTL index on `created` with `expireAfterSeconds = 90 * 86400`, replacing the default `{created: 1}` field index so Mongo does not reject a duplicate key pattern. Mongo expires documents in the background; lowering or removing TTL later requires dropping that index yourself (`db.auditevents.dropIndex(...)`) — Mongoose will not remove it.
@@ -0,0 +1,169 @@
1
+ # How to automate Terreno upgrades with a scheduled agent
2
+
3
+ Run a GitHub Actions job every working hour that checks whether your app is behind the latest Terreno release. When it is, and no upgrade is already in progress, the job starts one cloud agent. The agent runs the [`upgrading-terreno`](upgrade-terreno.md) skill and opens a pull request. The job never edits the tree and cannot merge. A person reviews the pull request.
4
+
5
+ ```text
6
+ cron tick ──► in working hours? ──► behind the latest GitHub release?
7
+ │
8
+ ▼
9
+ release ≥ 7 days old? ──► no open upgrade PR? ──► no upgrade agent running or in cooldown?
10
+ │
11
+ ▼
12
+ npm integrity for every installed package ──► launch one agent ──► agent opens PR
13
+ ```
14
+
15
+ This page is an example. Copy the workflow and adapt the script to your repository. The Terreno repository does not run it.
16
+
17
+ ## Why the job only decides
18
+
19
+ A GitHub runner usually has no MongoDB, no running backend, and no browser. The upgrade skill needs all three: it starts the upgraded backend, regenerates the typed client from `/openapi.json`, and runs the app. So the job decides and enqueues. The agent runs in a cloud environment that already has your app's services, such as a Cursor Cloud Agent environment.
20
+
21
+ ## Example workflow
22
+
23
+ `.github/workflows/upgrade-terreno.yml`:
24
+
25
+ ```yaml
26
+ name: Upgrade Terreno
27
+
28
+ on:
29
+ schedule:
30
+ # 13:00–22:00 UTC covers 9:00am–5:59pm America/New_York in both EST and EDT.
31
+ # The script drops ticks outside the local window.
32
+ - cron: "0 13-22 * * 1-5"
33
+ workflow_dispatch:
34
+ inputs:
35
+ version:
36
+ description: "Target @terreno/* version (empty = latest GitHub release)"
37
+ required: false
38
+ type: string
39
+ dry_run:
40
+ description: "Decide only — do not launch an upgrade agent"
41
+ required: false
42
+ default: false
43
+ type: boolean
44
+
45
+ permissions:
46
+ contents: read
47
+ pull-requests: read
48
+
49
+ # A second tick waits instead of racing the pull-request and active-agent checks.
50
+ concurrency:
51
+ group: upgrade-terreno
52
+ cancel-in-progress: false
53
+
54
+ jobs:
55
+ upgrade-terreno:
56
+ runs-on: ubuntu-latest
57
+ timeout-minutes: 15
58
+ steps:
59
+ - uses: actions/checkout@v6
60
+ with:
61
+ persist-credentials: false
62
+
63
+ - uses: oven-sh/setup-bun@v2
64
+ with:
65
+ bun-version: "1.4.4"
66
+
67
+ - run: bun install --frozen-lockfile
68
+
69
+ - name: Launch Terreno upgrade when one is not already open
70
+ env:
71
+ CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
72
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73
+ TERRENO_UPGRADE_CLOUD_ENVIRONMENT: ${{ vars.TERRENO_UPGRADE_CLOUD_ENVIRONMENT }}
74
+ TERRENO_UPGRADE_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
75
+ TERRENO_UPGRADE_VERSION: ${{ inputs.version }}
76
+ run: bun .github/scripts/upgrade-terreno.ts
77
+ ```
78
+
79
+ The job needs read access to contents and pull requests only. The agent pushes with its own credentials.
80
+
81
+ ## What the script decides
82
+
83
+ `.github/scripts/upgrade-terreno.ts` checks these in order and stops at the first one that says no. Order matters: the cheap checks run first, and no agent API call happens until every gate passes.
84
+
85
+ | Order | Check | Skip when |
86
+ | --- | --- | --- |
87
+ | 1 | Working hours | A scheduled tick lands outside weekdays 9:00am–5:59pm in your team's timezone. Use Luxon with a named zone so daylight saving does not move the window. Manual runs are not gated. |
88
+ | 2 | Installed versions | No `@terreno/*` pin is found. This is an error, not a skip. Read every manifest that pins Terreno. With a Bun catalog, the versions live in the root `package.json` under `workspaces.catalog` (and `workspaces.catalogs.<name>`); workspace packages say `catalog:`. |
89
+ | 3 | Target version | The lowest installed pin already equals the target, or any pin is newer than it. Never downgrade. Resolving the target fails the job when `@terreno/api` at that version is not on npm yet; see below. |
90
+ | 4 | Release age | A scheduled run targets a release under 7 days old. A manual run is the human gate and is not held. |
91
+ | 5 | Open pull request | Any open pull request is an upgrade: its title names a Terreno upgrade, its branch contains `upgrade-terreno`, or its body has the `<!-- terreno-upgrade -->` marker. |
92
+ | 6 | Running agent | An upgrade agent (name starts with `Upgrade Terreno`) is still running. |
93
+ | 7 | Cooldown | An agent for the same target version was created in the last 24 hours, even one that finished or failed. Without this, a broken upgrade is retried every hour. |
94
+ | 8 | npm integrity | Any installed package has no published `dist.integrity` for the target. |
95
+
96
+ Take the target from the latest stable [TerrenoLabs/terreno GitHub release](https://github.com/TerrenoLabs/terreno/releases), not the npm `latest` dist-tag. Reject drafts, prereleases, and tags that are not semver. Validate a manual `version` input as semver before using it.
97
+
98
+ Write the decision to `$GITHUB_STEP_SUMMARY` so each run says why it skipped:
99
+
100
+ ```text
101
+ ## Terreno upgrade
102
+
103
+ - Decision: skipped (open-pull-request)
104
+ - Detail: Open Terreno upgrade pull request #261 (Upgrade Terreno to 57.19.2).
105
+ - From: 57.16.2
106
+ - To: 57.19.2
107
+ ```
108
+
109
+ The job fails, rather than skips, in two cases:
110
+
111
+ - A launch would happen and `CURSOR_API_KEY` is missing. A skip with no key is still green.
112
+ - The latest GitHub release exists but `@terreno/api` at that version is not on npm. This check runs while the target is resolved, before the release-age and open-pull-request gates, so every in-hours run is red until publishing finishes. That points at a stuck release. If you would rather skip quietly, return a skip from that check instead of throwing.
113
+
114
+ ## The agent prompt
115
+
116
+ The skill stops for confirmation unless the invoking task names both versions and authorizes the bump. The prompt does that, so the agent prints its plan into the transcript and continues:
117
+
118
+ ```text
119
+ Upgrade Terreno from 57.16.2 to 57.19.2.
120
+
121
+ This unattended task names fromVersion 57.16.2 and toVersion 57.19.2 and authorizes the
122
+ lockstep bump. Follow the upgrading-terreno skill. Print the plan into the transcript, then
123
+ perform the upgrade. Do not wait for confirmation.
124
+
125
+ Check out the latest origin/master and create a feature branch before editing. Do not commit
126
+ to master.
127
+
128
+ Bump every already-installed @terreno/* dependency to 57.19.2. Do not add @terreno packages
129
+ that are not already installed.
130
+
131
+ Installed pins:
132
+ package.json workspaces.catalog @terreno/api@57.16.2
133
+ package.json workspaces.catalog @terreno/ui@57.16.2
134
+
135
+ The target is the TerrenoLabs/terreno GitHub release 57.19.2, not the npm latest dist-tag.
136
+ Install these exact versions. npm dist.integrity:
137
+ @terreno/api@57.19.2 sha512-…
138
+ @terreno/ui@57.19.2 sha512-…
139
+
140
+ Open exactly one pull request against master. Put <!-- terreno-upgrade --> in its body.
141
+ ```
142
+
143
+ Add the repo-specific steps the skill cannot know, such as which apps run `bun run sdk`. Install the skill in the app first ([install agent skills](install-agent-skills.md)); otherwise the agent has nothing to follow.
144
+
145
+ Name the agent `Upgrade Terreno to <version>` so checks 6 and 7 can find it.
146
+
147
+ ## Test the script
148
+
149
+ Inject the I/O (manifest reads, GitHub, npm, the agent API, and the clock) as dependencies and test the decision function with fakes. Cover at least:
150
+
151
+ - A scheduled tick at 8:59am and 6:00pm local time in both winter and summer
152
+ - Already current, and a pin newer than the target
153
+ - A 6-day-old release on a schedule, and the same release on a manual run
154
+ - Each open pull request signal on its own
155
+ - A finished agent for the same version inside and outside the cooldown
156
+ - A package with no npm integrity
157
+ - A dry run that reports `launch` without calling the agent API
158
+
159
+ ## Try it
160
+
161
+ Run the workflow manually with `dry_run` checked. The summary shows the decision without launching anything. Then run it locally:
162
+
163
+ ```bash
164
+ GITHUB_EVENT_NAME=workflow_dispatch \
165
+ GITHUB_REPOSITORY=<owner>/<repo> \
166
+ GITHUB_TOKEN=$(gh auth token) \
167
+ TERRENO_UPGRADE_DRY_RUN=true \
168
+ bun .github/scripts/upgrade-terreno.ts
169
+ ```
@@ -0,0 +1,160 @@
1
+ # Build a list/detail screen
2
+
3
+ Put the list and its detail route in one route group, and make `ListDetailLayout` the group's
4
+ layout. On a phone the list pushes the detail. On a tablet or a wide browser the list stays in a
5
+ left pane and the detail opens beside it. The URL always names the open item.
6
+
7
+ ```text
8
+ app/mail/
9
+ _layout.tsx ListDetailLayout
10
+ index.tsx ListDetailIndex
11
+ [id].tsx the detail screen
12
+ ```
13
+
14
+ ```tsx
15
+ // app/mail/_layout.tsx
16
+ import {ListDetailLayout, Text} from "@terreno/ui";
17
+ import type React from "react";
18
+
19
+ import {MailList} from "@/components/MailList";
20
+
21
+ // A deep link to /mail/3 on a phone still has the list to go back to.
22
+ export const unstable_settings = {initialRouteName: "index"};
23
+
24
+ const MailLayout: React.FC = () => (
25
+ <ListDetailLayout
26
+ detailHref={(id) => `/mail/${id}`}
27
+ emptyDetail={<Text>Select a message</Text>}
28
+ indexHref="/mail"
29
+ list={<MailList />}
30
+ screenOptions={{title: "Inbox"}}
31
+ />
32
+ );
33
+ export default MailLayout;
34
+ ```
35
+
36
+ ```tsx
37
+ // app/mail/index.tsx
38
+ import {ListDetailIndex} from "@terreno/ui";
39
+ import type React from "react";
40
+
41
+ const MailIndex: React.FC = () => <ListDetailIndex />;
42
+ export default MailIndex;
43
+ ```
44
+
45
+ ```tsx
46
+ // app/mail/[id].tsx
47
+ import {Page, Text} from "@terreno/ui";
48
+ import {useLocalSearchParams} from "expo-router";
49
+ import type React from "react";
50
+
51
+ const MailMessage: React.FC = () => {
52
+ const {id} = useLocalSearchParams<{id: string}>();
53
+ return (
54
+ <Page>
55
+ <Text>Message {id}</Text>
56
+ </Page>
57
+ );
58
+ };
59
+ export default MailMessage;
60
+ ```
61
+
62
+ ## Select rows from the list
63
+
64
+ Read the open item and open others with `useListDetail()`. Mark the open row as selected so
65
+ screen readers announce it. On web a clickable `Box` row sends this as `aria-current="true"`.
66
+
67
+ ```tsx
68
+ import {Box, Text, useListDetail} from "@terreno/ui";
69
+ import type React from "react";
70
+
71
+ export const MailRow: React.FC<{id: string; subject: string}> = ({id, subject}) => {
72
+ const {select, selectedId} = useListDetail();
73
+ const isSelected = id === selectedId;
74
+ return (
75
+ <Box
76
+ accessibilityHint="Opens the message"
77
+ accessibilityLabel={subject}
78
+ accessibilityState={{selected: isSelected}}
79
+ color={isSelected ? "primary" : "base"}
80
+ onClick={() => select(id)}
81
+ padding={3}
82
+ testID={`mail-row-${id}`}
83
+ >
84
+ <Text color={isSelected ? "inverted" : "primary"}>{subject}</Text>
85
+ </Box>
86
+ );
87
+ };
88
+ ```
89
+
90
+ | Call | Stacked (phone) | Split (tablet, wide web) |
91
+ | --- | --- | --- |
92
+ | `select(id)`, nothing open | Pushes `detailHref(id)` | Pushes `detailHref(id)` |
93
+ | `select(id)`, another item open | Pushes `detailHref(id)` | Replaces the open detail |
94
+ | `select(selectedId)` | Pushes again | Does nothing |
95
+ | `clear()` | `router.dismissTo(indexHref)`: pops back to the list inside this group | Same |
96
+ | `clear()`, nothing open | Does nothing | Does nothing |
97
+
98
+ Because split mode replaces the open detail, the stack is never deeper than the list plus one
99
+ detail, so back returns to the list after a rotation too. When split, the header's back button
100
+ (`list-detail-leave`) leaves the whole group instead, since the list is already on screen.
101
+
102
+ When another screen opens a detail directly, push it with `withAnchor` so the list sits below it
103
+ and back lands on the list instead of leaving the group:
104
+
105
+ ```tsx
106
+ router.push("/mail/3", {withAnchor: true});
107
+ <Link href="/mail/3" withAnchor>Open message</Link>
108
+ ```
109
+
110
+ `selectedId` is the `paramName` param (default `id`) of the route directly below `indexHref`
111
+ whose segment is `detailRoute` (default `[id]`). An `id` from a parent dynamic segment, such as
112
+ `/orgs/[id]/mail`, never counts.
113
+
114
+ ## Publish the detail URL
115
+
116
+ A static web export (`web.output: "static"`) writes an HTML file only for paths
117
+ `generateStaticParams` returns. Without it, opening `/mail/3` directly is a 404 on a host that
118
+ does not rewrite unknown paths.
119
+
120
+ ```tsx
121
+ // app/mail/[id].tsx
122
+ export const generateStaticParams = (): {id: string}[] =>
123
+ messages.map((message) => ({id: message.id}));
124
+ ```
125
+
126
+ Ids that are not known at build time need the host fallback from
127
+ [Build for web](build-for-web.md): Netlify `_redirects` with `/* /index.html 200`, or the Vercel
128
+ rewrite in [Deploy to Vercel](deploy-to-vercel.md). The demo inbox exports one page per fixture
129
+ message at `/demo/list-detail/:id` and `/demo/sidebar-navigation/mail/:id`. The Netlify demo
130
+ deploy also writes that fallback, so an id outside the fixtures still opens the missing-message
131
+ screen.
132
+
133
+ ## Keep list state across the breakpoint
134
+
135
+ The open item survives a rotation or a browser resize. The `list` element does not: it moves
136
+ between the index screen and the left pane, so it remounts. Keep filters, search text, and other
137
+ list state in the route (`router.setParams`) or a store.
138
+
139
+ ## Nest it inside a sidebar
140
+
141
+ `ListDetailLayout` measures its own width, so it splits correctly inside `SidebarNavigation` or
142
+ an admin rail. Make the group a screen of the sidebar navigator:
143
+
144
+ ```text
145
+ app/workspace/
146
+ _layout.tsx SidebarNavigation with <SidebarNavigation.Screen name="mail" />
147
+ mail/_layout.tsx ListDetailLayout with indexHref="/workspace/mail"
148
+ mail/index.tsx
149
+ mail/[id].tsx
150
+ ```
151
+
152
+ ## Choose SplitPage instead
153
+
154
+ Use `SplitPage` when the list and detail do not have their own routes, such as a picker inside a
155
+ modal. `SplitPage` keeps the selection in component state, so it has no URL, deep links, or back
156
+ navigation for the open item.
157
+
158
+ ## Reference
159
+
160
+ Props, defaults, and testIDs: [`ListDetailLayout`](../reference/ui.md#listdetaillayout).
@@ -106,7 +106,9 @@ when the package imports them. Coverage-script unit tests run in the cheap
106
106
  Playwright runs five shards after `e2e-prepare` (`auth`, `app`, `admin-core`,
107
107
  `admin-table`, `syncdb`) instead of one container per spec file. Each shard
108
108
  first checks the [affected gate](#e2e-affected-gate) and halts when no changed
109
- file can reach it. Repository
109
+ file can reach it. The cross-site bearer specs run beside them in `e2e-cross-site`,
110
+ which builds its own web bundle and backend (`playwright.config.ts` with
111
+ `E2E_CROSS_SITE_BEARER=true`), has no affected gate, and is not a required check. Repository
110
112
  policy checks share one `repo-policies` job so eight small checkouts do not
111
113
  sit in the concurrency queue. `repo-policies` uses Node 22.14 because Knip's
112
114
  oxc-parser throws `ERR_REQUIRE_ESM` on the shared 22.11 executor.
@@ -316,6 +318,7 @@ the same commands through the parameterized `packages-ci` job, gated by
316
318
  | Admin SPA Build and E2E | `admin-spa-ci` |
317
319
  | Lint, compile, and coverage (matrix package) | `packages-ci` (`admin-backend`, `admin-frontend`, `api-health`, `feature-flags`, `test`) |
318
320
  | E2E · `<shard>` | `e2e` (matrix `shard`: `auth`, `app`, `admin-core`, `admin-table`, `syncdb`) |
321
+ | _(cross-site bearer specs, not required)_ | `e2e-cross-site` (after `e2e-prepare`; also the explicit-linking pass) |
319
322
  | E2E Load · syncdb-loadlab | `e2e-load` (trigger-gated, see below) |
320
323
  | Admin SPA Backend Integration E2E | `admin-spa-integration` |
321
324
  | _(e2e compile+export once)_ | `e2e-prepare` |
@@ -368,10 +371,17 @@ To teach the agent from what reviewers actually say, run
368
371
  <file>` of pasted comments): it opens one PR editing `docs/approval-rubric.md` and the
369
372
  path rules from the review comments since the last pass.
370
373
 
371
- Rollout: `approveLevels` is `[]`, so every PR gets a `COMMENT` review that says the
372
- risk level and why, and nothing approves. After a dozen PRs, compare the levels with
373
- how humans reviewed them, tune the path rules and rubric, then set
374
- `"approveLevels": ["low"]` in a one-line PR. The prompts and answers behind each
374
+ Levels and human approvals (defined in `docs/approval-rubric.md`):
375
+
376
+ | Level | Human approvals | Examples |
377
+ | --- | --- | --- |
378
+ | low | None once automated review and security checks are green | docs, copy, internal-only features, additive changes behind a flag |
379
+ | medium | One person | ordinary product changes, and sensitive areas such as caching, app updates, and AI system prompts |
380
+ | high | One person | security-related code: auth, permissions, secrets, crypto, PHI handling |
381
+
382
+ `approveLevels` is `["low"]`, so the App's approval satisfies master's one-review rule
383
+ for low PRs. Medium and high PRs get a `COMMENT` review that says the risk level and
384
+ why, and a human approves. The prompts and answers behind each
375
385
  verdict are stored as the `brewery-approve` job artifact. Full reference:
376
386
  [brewery `docs/approve.md`](https://github.com/joshgachnang/brewery/blob/master/docs/approve.md).
377
387