@terreno/agent-plugin 0.0.0-stage → 57.21.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.
- package/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +2124 -0
- package/LICENSE +21 -0
- package/README.md +57 -2
- package/agents/pre-commit.md +65 -0
- package/agents/ui-verifier.md +93 -0
- package/package.json +32 -4
- package/references/async-review-bots.md +76 -0
- package/references/documentation-contract.md +61 -0
- package/references/execution-state.schema.json +77 -0
- package/references/github-attention-contract.md +165 -0
- package/references/independent-review.md +31 -0
- package/references/lifecycle-contract.md +211 -0
- package/references/loop-engineering.md +125 -0
- package/references/pick-roast-loop.md +112 -0
- package/references/pr-deployments.md +69 -0
- package/references/product-ci.md +164 -0
- package/references/push-ownership.md +91 -0
- package/references/stage-result.schema.json +82 -0
- package/references/subagent-briefing.md +43 -0
- package/references/testing-plan.md +163 -0
- package/references/user-guide.md +171 -0
- package/skills/1-grow/SKILL.md +145 -0
- package/skills/1-grow/references/grilling.md +243 -0
- package/skills/2-pick/SKILL.md +170 -0
- package/skills/2-pick/references/mocking.md +34 -0
- package/skills/2-pick/references/testing.md +43 -0
- package/skills/3-roast/SKILL.md +168 -0
- package/skills/4-brew/SKILL.md +201 -0
- package/skills/5-taste/SKILL.md +311 -0
- package/skills/ai-prompt-governance/SKILL.md +100 -0
- package/skills/attach-pr-media/SKILL.md +89 -0
- package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
- package/skills/backend-test-env/SKILL.md +64 -0
- package/skills/backfill-regression/SKILL.md +101 -0
- package/skills/backfill-user-guide/SKILL.md +96 -0
- package/skills/bootstrap-mcp-server/SKILL.md +244 -0
- package/skills/building-admin-interfaces/SKILL.md +83 -0
- package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
- package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
- package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
- package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
- package/skills/building-terreno-apps/SKILL.md +280 -0
- package/skills/building-terreno-apps/agents/openai.yaml +4 -0
- package/skills/building-terreno-apps/references/navigation.md +102 -0
- package/skills/building-terreno-apps/references/route-structure.md +91 -0
- package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
- package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
- package/skills/deploy-gcp/SKILL.md +73 -0
- package/skills/deploy-gcp/references/troubleshooting.md +29 -0
- package/skills/generate-release-announcements/SKILL.md +145 -0
- package/skills/generate-sdk/SKILL.md +156 -0
- package/skills/mongoose-schema-safety/SKILL.md +164 -0
- package/skills/pick-roast-loop/SKILL.md +152 -0
- package/skills/planning-loop/SKILL.md +161 -0
- package/skills/prd/SKILL.md +138 -0
- package/skills/set-up-social-login/SKILL.md +76 -0
- package/skills/store-assets/SKILL.md +221 -0
- package/skills/store-assets/examples/screenshots.yaml +31 -0
- package/skills/store-assets/examples/storeAssets.config.json +44 -0
- package/skills/store-assets/scripts/banner.ts +165 -0
- package/skills/store-assets/scripts/config.ts +255 -0
- package/skills/store-assets/scripts/devClient.ts +635 -0
- package/skills/store-assets/scripts/devices.ts +286 -0
- package/skills/store-assets/scripts/icon.ts +81 -0
- package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
- package/skills/store-assets/scripts/maestro/login.yaml +27 -0
- package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
- package/skills/store-assets/scripts/screenshots.ts +848 -0
- package/skills/taste-sweep/SKILL.md +172 -0
- package/skills/terreno-backend-api/SKILL.md +355 -0
- package/skills/terreno-backend-api/agents/openai.yaml +4 -0
- package/skills/terreno-backend-api/references/custom-routes.md +132 -0
- package/skills/terreno-backend-api/references/model-router.md +116 -0
- package/skills/terreno-backend-api/references/server-setup.md +116 -0
- package/skills/terreno-backend-tests/SKILL.md +176 -0
- package/skills/terreno-data-fetching/SKILL.md +276 -0
- package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
- package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
- package/skills/terreno-data-fetching/references/realtime.md +45 -0
- package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
- package/skills/terreno-frontend-tests/SKILL.md +190 -0
- package/skills/terreno-mobile-tests/SKILL.md +127 -0
- package/skills/terreno-test-setup/SKILL.md +81 -0
- package/skills/terreno-test-setup/references/backend.md +131 -0
- package/skills/terreno-test-setup/references/frontend.md +169 -0
- package/skills/terreno-test-setup/references/mobile.md +134 -0
- package/skills/terreno-ui/SKILL.md +252 -0
- package/skills/terreno-ui/agents/openai.yaml +4 -0
- package/skills/terreno-ui/references/data-display.md +85 -0
- package/skills/terreno-ui/references/feedback.md +122 -0
- package/skills/terreno-ui/references/forms.md +119 -0
- package/skills/terreno-ui/references/layout.md +124 -0
- package/skills/terreno-ui/references/theming.md +87 -0
- package/skills/update-docs/SKILL.md +82 -0
- package/skills/upgrading-terreno/SKILL.md +86 -0
- package/skills/upgrading-terreno/references/ordering.md +40 -0
- package/skills/verify-ui-changes/SKILL.md +605 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Patterns and tools
|
|
2
|
+
|
|
3
|
+
## Widget registry (`AdminProvider.widgets`)
|
|
4
|
+
|
|
5
|
+
Three buckets. Host entries override built-ins with the same ID.
|
|
6
|
+
|
|
7
|
+
| Bucket | IDs come from | Built-ins include |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `home` | `AdminApp.home.slots` | `modelStats` / `modelsGrid`, `scriptRunner`, `versionConfig`, `recentActivity`, `feature-flags-overrides` |
|
|
10
|
+
| `screens` | `customScreens[].name` | `comms`, `documents`, `ai-requests`, `version-config` |
|
|
11
|
+
| `fields` | `admin.fieldOverrides.widget` | `textarea`, `markdown`, consent widgets |
|
|
12
|
+
|
|
13
|
+
Unknown IDs render `MissingWidget` instead of crashing. Register host widgets once
|
|
14
|
+
on `AdminProvider`; do not pass the deprecated `customScreens` prop on
|
|
15
|
+
`AdminModelList`.
|
|
16
|
+
|
|
17
|
+
Plugin backend IDs arrive via `adminContribution()`. First-party React lives in
|
|
18
|
+
`@terreno/admin-frontend`. Third-party plugins ship their own frontend package;
|
|
19
|
+
the host spreads it into `widgets`. See
|
|
20
|
+
[Admin plugin frontend widgets](../../../../../docs/explanation/admin-plugin-frontend.md).
|
|
21
|
+
|
|
22
|
+
## Model admin metadata that affects UI
|
|
23
|
+
|
|
24
|
+
Set these on `modelRouter({admin: ...})` rather than cloning tables:
|
|
25
|
+
|
|
26
|
+
- `listFields`, `searchFields`, `sortableFields`, `filters`
|
|
27
|
+
- `fieldsets`, `readonlyFields`, `hiddenFields`
|
|
28
|
+
- `group`, `recordTitleField`, `pageSize`
|
|
29
|
+
- `actions` / `bulkPatchAllowlist` for changelist bulk work
|
|
30
|
+
|
|
31
|
+
Reference: [admin-config](../../../../../docs/reference/admin-config.md).
|
|
32
|
+
|
|
33
|
+
## Platform tools
|
|
34
|
+
|
|
35
|
+
| Tool | Config flag | Typical route |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Scripts | `platformTools.scripts` | `{routeBase}/__scripts` |
|
|
38
|
+
| Roles | `platformTools.roles` | `rolesPath` (embedded `/roles` or `/admin/roles`) |
|
|
39
|
+
| Version | `platformTools.version` | `versionConfigPath` |
|
|
40
|
+
| Configuration | `platformTools.configuration` | `configurationPath` |
|
|
41
|
+
| Audit Log / Feature Flags | model `read` | `/{model.name}` via Platform section |
|
|
42
|
+
|
|
43
|
+
## Data and MCP
|
|
44
|
+
|
|
45
|
+
- Admin String-`_id` collection CRUD: windowed syncdb with `adminBroadcast`.
|
|
46
|
+
- Framework admin RPC: host-bound `adminRequest`; do not add `injectEndpoints`.
|
|
47
|
+
- ObjectId/API-only compatibility CRUD: `useAdminApi` until Terreno 58.
|
|
48
|
+
- After backend route changes: `cd example-frontend && bun run sdk` (non-synced
|
|
49
|
+
only). Do not generate RTK CRUD for synced product collections.
|
|
50
|
+
- MCP: `terreno_install_admin` scaffolds wiring; still follow this skill for
|
|
51
|
+
screens and nav. `terreno_search_docs` / `terreno_get_component_docs` for APIs.
|
|
52
|
+
|
|
53
|
+
## Copy-paste custom screen
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import {AdminScreenPage} from "@terreno/admin-frontend";
|
|
57
|
+
import type {AdminScreenWidgetProps} from "@terreno/admin-frontend";
|
|
58
|
+
import {Box, Text} from "@terreno/ui";
|
|
59
|
+
import React from "react";
|
|
60
|
+
|
|
61
|
+
export const ExampleOpsScreen: React.FC<AdminScreenWidgetProps> = ({routeBase}) => {
|
|
62
|
+
return (
|
|
63
|
+
<AdminScreenPage backHref={routeBase} color="transparent" padding={0} title="Ops">
|
|
64
|
+
<Box padding={4}>
|
|
65
|
+
<Text>Operator content</Text>
|
|
66
|
+
</Box>
|
|
67
|
+
</AdminScreenPage>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Match `name: "example-ops"` on `AdminApp.customScreens` and
|
|
73
|
+
`widgets={{screens: {"example-ops": ExampleOpsScreen}}}`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Screens and routes
|
|
2
|
+
|
|
3
|
+
Canonical steps: [Build admin screens](../../../../../docs/how-to/build-admin-screens.md).
|
|
4
|
+
|
|
5
|
+
## Embedded host (`example-frontend/app/admin`)
|
|
6
|
+
|
|
7
|
+
Wrap **once** in `_layout.tsx`: `AdminProvider` then `AdminShellLayout` then `Stack`.
|
|
8
|
+
Child routes must not wrap another shell.
|
|
9
|
+
|
|
10
|
+
| File | Component |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.tsx` | `AdminHome` |
|
|
13
|
+
| `[model]/index.tsx` | `AdminScreenRouter` with `name={model}` |
|
|
14
|
+
| `[model]/create.tsx` | `AdminModelForm` `mode="create"` |
|
|
15
|
+
| `[model]/[id].tsx` | `AdminModelForm` |
|
|
16
|
+
| Extra folders (`comms/`, `consent-forms/`, `configuration.tsx`) | Only when generic CRUD is not enough |
|
|
17
|
+
|
|
18
|
+
Create is **`create`**, not `new`.
|
|
19
|
+
|
|
20
|
+
## Standalone host (`admin-spa/app`)
|
|
21
|
+
|
|
22
|
+
Root `_layout.tsx` is providers + `AdminGate` only. **Each screen** wraps
|
|
23
|
+
`AdminShellLayout` because there is no shared admin stack layout. `routeBase=""`.
|
|
24
|
+
`apiBase` comes from `app-config.json` (`adminApiBasePath`, default `/admin`).
|
|
25
|
+
|
|
26
|
+
## `AdminScreenRouter`
|
|
27
|
+
|
|
28
|
+
Resolves one URL segment:
|
|
29
|
+
|
|
30
|
+
1. `__scripts` → `AdminScriptList`
|
|
31
|
+
2. Config model `name` → `AdminModelTable`
|
|
32
|
+
3. `widgets.screens[name]` → custom screen widget
|
|
33
|
+
4. Config `customScreens` without a widget → missing-widget placeholder
|
|
34
|
+
5. Else not-found
|
|
35
|
+
|
|
36
|
+
Model **names** (Mongoose model name, or a unique suffix like `Food-archived-foods`)
|
|
37
|
+
are the URL segment, not `routePath` (`/admin/users`).
|
|
38
|
+
|
|
39
|
+
## Dedicated routes vs widgets
|
|
40
|
+
|
|
41
|
+
Prefer a `widgets.screens` component so `[model]/index` can render it. Add a
|
|
42
|
+
dedicated Expo file when:
|
|
43
|
+
|
|
44
|
+
- The path has more segments than `/admin/:name` (comms message `/comms/:id`).
|
|
45
|
+
- The screen must not share the generic create/edit form (consent editor).
|
|
46
|
+
- The host is admin-spa and you want a static export path (optional; the
|
|
47
|
+
`[model]` fallback already covers single-segment custom screens).
|
|
48
|
+
|
|
49
|
+
Expo file routes win over `[model]` when the folder name is literal
|
|
50
|
+
(`comms/index.tsx` beats `[model]/index.tsx` for `/admin/comms`).
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: building-terreno-apps
|
|
3
|
+
description: >-
|
|
4
|
+
Complete guide for building Terreno full-stack Expo apps with @terreno/ui,
|
|
5
|
+
Expo Router, and @terreno/rtk. Covers layout, forms, navigation, theming,
|
|
6
|
+
screen patterns, and when to use Expo Go vs custom builds.
|
|
7
|
+
---
|
|
8
|
+
# Building Terreno Apps
|
|
9
|
+
|
|
10
|
+
Use this skill when building or refactoring screens in a Terreno Expo app (`example-frontend`, `admin-spa`, or a consumer app using `@terreno/ui` + `@terreno/rtk`).
|
|
11
|
+
|
|
12
|
+
**Related skills:** `terreno-ui` (component selection), `terreno-data-fetching` (API hooks), `verify-ui-changes` (mandatory UI verification), `generate-sdk` (after backend changes), `building-admin-interfaces` (admin shell, custom screens, sidebar).
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
1. Read `docs/tutorials/`, `docs/explanation/`, and app-structure how-to pages before changing screens or navigation.
|
|
17
|
+
2. Implement against that design.
|
|
18
|
+
3. Update those pages in the same slice with `update-docs`.
|
|
19
|
+
4. Ship without matching docs is a failed slice.
|
|
20
|
+
|
|
21
|
+
## References
|
|
22
|
+
|
|
23
|
+
Consult these resources as needed:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
references/
|
|
27
|
+
route-structure.md Expo Router conventions adapted for Terreno apps
|
|
28
|
+
screen-patterns.md List, detail, form, and admin screen templates
|
|
29
|
+
navigation.md Stacks, tabs, modals, and auth-gated routing
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Admin CRUD, sidebar, custom screens, and `apiBase`/`routeBase`: use skill
|
|
33
|
+
`building-admin-interfaces` and `docs/how-to/build-admin-screens.md`.
|
|
34
|
+
|
|
35
|
+
## Running the App
|
|
36
|
+
|
|
37
|
+
**CRITICAL: Always try Expo Go first before creating custom builds.**
|
|
38
|
+
|
|
39
|
+
1. Start with `bun run frontend:web` (or `bun run start` in the app package) and test in Expo Go or the web dev server.
|
|
40
|
+
2. For full-stack features, start the backend: `bun run backend:dev` with MongoDB and auth env vars (see `AGENTS.md`).
|
|
41
|
+
3. Only create custom builds (`npx expo run:ios/android`, `eas build`) when you need local Expo modules, Apple targets, third-party native modules not in Expo Go, or native config that cannot be expressed in `app.json`.
|
|
42
|
+
|
|
43
|
+
## Provider Stack
|
|
44
|
+
|
|
45
|
+
Every Terreno app root layout should wrap children in this order (outer → inner):
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import {Provider} from "react-redux";
|
|
49
|
+
import {PersistGate} from "redux-persist/integration/react";
|
|
50
|
+
import {TerrenoProvider} from "@terreno/ui";
|
|
51
|
+
// Import from your app's store bootstrap module (see example-frontend/store/index.ts)
|
|
52
|
+
import store, {persistor} from "../store/index.ts";
|
|
53
|
+
|
|
54
|
+
const RootLayout: React.FC = () => {
|
|
55
|
+
return (
|
|
56
|
+
<Provider store={store}>
|
|
57
|
+
<PersistGate loading={null} persistor={persistor}>
|
|
58
|
+
<TerrenoProvider>
|
|
59
|
+
<Stack />
|
|
60
|
+
</TerrenoProvider>
|
|
61
|
+
</PersistGate>
|
|
62
|
+
</Provider>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
See `example-frontend/app/_layout.tsx` for the full pattern including auth routing, sockets, and feature flags.
|
|
68
|
+
|
|
69
|
+
## Code Style
|
|
70
|
+
|
|
71
|
+
- Use **functional components** with `React.FC` and explicit return types.
|
|
72
|
+
- Import hooks directly: `import {useEffect, useCallback} from "react"` — not `React.useEffect`.
|
|
73
|
+
- Wrap event handlers in `useCallback`. Add a comment above every `useEffect`.
|
|
74
|
+
- Use **Luxon** for all date/time operations — never `Date` or dayjs.
|
|
75
|
+
- Use **camelCase** directories (e.g. `components/todoList/`).
|
|
76
|
+
- Use **kebab-case** for route file names when the Expo skill convention applies (e.g. `comment-card.tsx`).
|
|
77
|
+
- **No barrel imports** — import concrete module files, not directory `index` re-exports. Cross-package `@terreno/*` roots are allowed.
|
|
78
|
+
- Never co-locate components, types, or utilities in the `app/` directory.
|
|
79
|
+
|
|
80
|
+
## Layout — Use @terreno/ui, Not Raw Primitives
|
|
81
|
+
|
|
82
|
+
| Instead of | Use |
|
|
83
|
+
|------------|-----|
|
|
84
|
+
| `View` | `Box` |
|
|
85
|
+
| React Native `Text` | `Text` or `Heading` from `@terreno/ui` |
|
|
86
|
+
| `ScrollView` as page wrapper | `Page` with `scroll={true}` |
|
|
87
|
+
| `StyleSheet.create` | Theme props (`padding`, `gap`, `color`) or inline styles |
|
|
88
|
+
| `SafeAreaView` | `Page` handles safe areas; or `contentInsetAdjustmentBehavior` on lists |
|
|
89
|
+
| Inline hex colors | Theme surface/text colors via props (`color="primary"`) |
|
|
90
|
+
|
|
91
|
+
### Page layout
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import {Box, Button, Heading, Page, Text} from "@terreno/ui";
|
|
95
|
+
|
|
96
|
+
const TodosScreen: React.FC = () => {
|
|
97
|
+
return (
|
|
98
|
+
<Page title="Todos" scroll loading={isLoading}>
|
|
99
|
+
<Box gap={3} padding={4}>
|
|
100
|
+
<Heading size="lg">My Todos</Heading>
|
|
101
|
+
<Text color="secondaryLight">Track your tasks</Text>
|
|
102
|
+
<Button text="Add Todo" onClick={handleAdd} variant="primary" />
|
|
103
|
+
</Box>
|
|
104
|
+
</Page>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Box layout
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<Box direction="row" padding={4} gap={2} alignItems="center">
|
|
113
|
+
<Text flex="grow">Content</Text>
|
|
114
|
+
<Button text="Action" onClick={handleAction} />
|
|
115
|
+
</Box>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use the spacing scale via props (`padding={4}` → 16px). Prefer `gap` over margin.
|
|
119
|
+
|
|
120
|
+
## Data on Screens
|
|
121
|
+
|
|
122
|
+
Never call `fetch` or `axios` in screen components. Use generated RTK Query hooks:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import {useGetTodosQuery, usePostTodosMutation} from "@/store/sdk";
|
|
126
|
+
|
|
127
|
+
const {data, isLoading, error, refetch} = useGetTodosQuery({});
|
|
128
|
+
const [createTodo, {isLoading: isCreating}] = usePostTodosMutation();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
See `terreno-data-fetching` for auth, caching, and error handling.
|
|
132
|
+
|
|
133
|
+
## Forms
|
|
134
|
+
|
|
135
|
+
Use `@terreno/ui` form fields with local state and RTK mutations:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import {Button, TextField} from "@terreno/ui";
|
|
139
|
+
|
|
140
|
+
const [title, setTitle] = useState<string>("");
|
|
141
|
+
const [titleError, setTitleError] = useState<string | undefined>();
|
|
142
|
+
|
|
143
|
+
const handleSubmit = useCallback(async (): Promise<void> => {
|
|
144
|
+
if (!title.trim()) {
|
|
145
|
+
setTitleError("Title is required");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await createTodo({title: title.trim(), completed: false}).unwrap();
|
|
150
|
+
setTitle("");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error("Failed to create todo", err);
|
|
153
|
+
}
|
|
154
|
+
}, [createTodo, title]);
|
|
155
|
+
|
|
156
|
+
<TextField
|
|
157
|
+
title="Title"
|
|
158
|
+
value={title}
|
|
159
|
+
onChange={setTitle}
|
|
160
|
+
errorText={titleError}
|
|
161
|
+
/>
|
|
162
|
+
<Button text="Create" onClick={handleSubmit} loading={isCreating} />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Navigation
|
|
166
|
+
|
|
167
|
+
- Use Expo Router file-based routing in `app/`.
|
|
168
|
+
- Gate routes on auth state in root `_layout.tsx` (redirect to `/login` when logged out).
|
|
169
|
+
- Use `Stack` from `expo-router` for stack navigation; set titles via `Stack.Screen options`.
|
|
170
|
+
- For admin screens, use `AdminShellLayout` from `@terreno/admin-frontend`.
|
|
171
|
+
|
|
172
|
+
See `./references/navigation.md` and `./references/route-structure.md`.
|
|
173
|
+
|
|
174
|
+
## Loading, Error, and Empty States
|
|
175
|
+
|
|
176
|
+
Every data-driven screen must handle all three:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
if (isLoading) {
|
|
180
|
+
return <Page title="Todos"><Spinner /></Page>;
|
|
181
|
+
}
|
|
182
|
+
if (error) {
|
|
183
|
+
return (
|
|
184
|
+
<Page title="Todos">
|
|
185
|
+
<Text color="error">Failed to load todos</Text>
|
|
186
|
+
<Button text="Retry" onClick={refetch} />
|
|
187
|
+
</Page>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (!data?.data?.length) {
|
|
191
|
+
return (
|
|
192
|
+
<Page title="Todos">
|
|
193
|
+
<Text>No todos yet</Text>
|
|
194
|
+
<Button text="Create your first todo" onClick={handleAdd} />
|
|
195
|
+
</Page>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Theming
|
|
201
|
+
|
|
202
|
+
- Wrap the app in `TerrenoProvider` (done in root layout).
|
|
203
|
+
- Use theme props on components: `color="primary"`, `border="default"`, `rounding="md"`.
|
|
204
|
+
- Access theme programmatically with `useTheme()` when needed.
|
|
205
|
+
- Do not hardcode hex colors when a theme token exists.
|
|
206
|
+
|
|
207
|
+
## Verification
|
|
208
|
+
|
|
209
|
+
Any UI change **must** follow the `verify-ui-changes` skill before opening or updating a PR. Launch the correct app, log in with seeded credentials, exercise the feature, and attach screenshots/videos to the PR.
|
|
210
|
+
|
|
211
|
+
## Decision Tree
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
Building or changing a screen?
|
|
215
|
+
|-- Need layout/structure?
|
|
216
|
+
| \-- Page for full screens, Box for sections, Card for grouped content
|
|
217
|
+
|
|
|
218
|
+
|-- Need data from API?
|
|
219
|
+
| \-- terreno-data-fetching (RTK Query hooks, never raw fetch)
|
|
220
|
+
|
|
|
221
|
+
|-- Need a form?
|
|
222
|
+
| \-- @terreno/ui TextField/SelectField/DateTimeField + local state + mutation
|
|
223
|
+
|
|
|
224
|
+
|-- Need navigation?
|
|
225
|
+
| \-- references/navigation.md (Expo Router + auth gating)
|
|
226
|
+
|
|
|
227
|
+
|-- Need admin CRUD, sidebar, or a custom operator screen?
|
|
228
|
+
| \-- building-admin-interfaces + AdminShellLayout + AdminScreenRouter
|
|
229
|
+
|
|
|
230
|
+
|-- Backend API doesn't exist yet?
|
|
231
|
+
| \-- terreno-backend-api (modelRouter on Express, then generate-sdk)
|
|
232
|
+
|
|
|
233
|
+
\-- Done coding?
|
|
234
|
+
\-- verify-ui-changes (mandatory)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Common Mistakes
|
|
238
|
+
|
|
239
|
+
**Wrong: Raw View/Text**
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
<View style={{padding: 16}}>
|
|
243
|
+
<Text style={{fontSize: 18}}>Hello</Text>
|
|
244
|
+
</View>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Right: @terreno/ui with theme props**
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
<Box padding={4}>
|
|
251
|
+
<Heading size="lg">Hello</Heading>
|
|
252
|
+
</Box>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Wrong: fetch in a screen**
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
const [todos, setTodos] = useState([]);
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
fetch("/todos").then((r) => r.json()).then(setTodos);
|
|
261
|
+
}, []);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Right: Generated RTK Query hook**
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
const {data} = useGetTodosQuery({});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Wrong: Editing openApiSdk.ts by hand**
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
// Manually adding a hook to store/openApiSdk.ts
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Right: Regenerate SDK after backend changes**
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
cd example-frontend && bun run sdk
|
|
280
|
+
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Building Terreno Apps"
|
|
3
|
+
short_description: "Build Terreno Expo apps with @terreno/ui layout, RTK Query data, Expo Router navigation, and mandatory UI verification"
|
|
4
|
+
default_prompt: "Use $building-terreno-apps to build or refactor a Terreno screen with @terreno/ui components, generated RTK Query hooks, and Expo Router navigation."
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Navigation — Terreno Apps
|
|
2
|
+
|
|
3
|
+
Terreno apps use Expo Router. Use Expo Router's public documentation for low-level APIs
|
|
4
|
+
such as `Link`, `Stack.Screen`, modals, and form sheets. This reference covers
|
|
5
|
+
Terreno-specific navigation patterns.
|
|
6
|
+
|
|
7
|
+
## Auth flow
|
|
8
|
+
|
|
9
|
+
1. Root `_layout.tsx` checks `useSelectCurrentUserId()` from `@terreno/rtk`.
|
|
10
|
+
2. Unauthenticated users redirect to `/login`.
|
|
11
|
+
3. Login screen calls `useEmailLoginMutation()` (from generated SDK via `generateAuthSlice`).
|
|
12
|
+
4. On success, tokens are stored automatically and `setUserId` is dispatched.
|
|
13
|
+
5. Root layout redirects to `/(tabs)` or the intended deep link.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import {useEmailLoginMutation} from "@/store/sdk";
|
|
17
|
+
import {Button, TextField, Page, Box} from "@terreno/ui";
|
|
18
|
+
|
|
19
|
+
const LoginScreen: React.FC = () => {
|
|
20
|
+
const [email, setEmail] = useState<string>("");
|
|
21
|
+
const [password, setPassword] = useState<string>("");
|
|
22
|
+
const [login, {isLoading, error}] = useEmailLoginMutation();
|
|
23
|
+
|
|
24
|
+
const handleLogin = useCallback(async (): Promise<void> => {
|
|
25
|
+
try {
|
|
26
|
+
await login({email, password}).unwrap();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error("Login failed", err);
|
|
29
|
+
}
|
|
30
|
+
}, [email, login, password]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Page title="Login" scroll>
|
|
34
|
+
<Box gap={3} padding={4}>
|
|
35
|
+
<TextField title="Email" value={email} onChange={setEmail} type="email" />
|
|
36
|
+
<TextField title="Password" value={password} onChange={setPassword} type="password" />
|
|
37
|
+
<Button text="Sign in" onClick={handleLogin} loading={isLoading} fullWidth />
|
|
38
|
+
</Box>
|
|
39
|
+
</Page>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Tabs
|
|
45
|
+
|
|
46
|
+
Use Expo Router tab groups. Set tab icons via `@expo/vector-icons` or FontAwesome through `@terreno/ui` `Icon`:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import {Tabs} from "expo-router";
|
|
50
|
+
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
|
51
|
+
|
|
52
|
+
const TabLayout: React.FC = () => {
|
|
53
|
+
return (
|
|
54
|
+
<Tabs screenOptions={{headerShown: false}}>
|
|
55
|
+
<Tabs.Screen
|
|
56
|
+
name="index"
|
|
57
|
+
options={{
|
|
58
|
+
title: "Todos",
|
|
59
|
+
tabBarIcon: ({color}) => <FontAwesome name="list" size={24} color={color} />,
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
<Tabs.Screen
|
|
63
|
+
name="profile"
|
|
64
|
+
options={{
|
|
65
|
+
title: "Profile",
|
|
66
|
+
tabBarIcon: ({color}) => <FontAwesome name="user" size={24} color={color} />,
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
</Tabs>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Modals
|
|
75
|
+
|
|
76
|
+
Use Expo Router modal presentation for simple overlays:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// app/_layout.tsx
|
|
80
|
+
<Stack>
|
|
81
|
+
<Stack.Screen name="(tabs)" options={{headerShown: false}} />
|
|
82
|
+
<Stack.Screen name="modal" options={{presentation: "modal"}} />
|
|
83
|
+
</Stack>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For confirmation dialogs and action sheets, prefer `@terreno/ui` `Modal` or `ActionSheet` — they render correctly on web and native.
|
|
87
|
+
|
|
88
|
+
## Deep linking
|
|
89
|
+
|
|
90
|
+
Configure `scheme` in `app.json`. Terreno RTK resolves API base URLs from `EXPO_PUBLIC_API_URL` or `expo.extra.BASE_URL` — keep deep link schemes separate from API URLs.
|
|
91
|
+
|
|
92
|
+
## Programmatic navigation
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import {useRouter} from "expo-router";
|
|
96
|
+
|
|
97
|
+
const router = useRouter();
|
|
98
|
+
router.push(`/admin/${modelName}`);
|
|
99
|
+
router.back();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `Link` from `expo-router` for declarative navigation when possible.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Route Structure — Terreno Apps
|
|
2
|
+
|
|
3
|
+
Terreno apps use Expo Router with the same file conventions as standard Expo apps. Use
|
|
4
|
+
Expo Router's public documentation for dynamic routes, groups, and query parameters.
|
|
5
|
+
|
|
6
|
+
## Terreno-specific conventions
|
|
7
|
+
|
|
8
|
+
### Directory layout
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
app/
|
|
12
|
+
_layout.tsx # Provider stack, auth gating, TerrenoProvider
|
|
13
|
+
login.tsx # Auth screen
|
|
14
|
+
(tabs)/
|
|
15
|
+
_layout.tsx # Tab navigator
|
|
16
|
+
index.tsx # Main screen (e.g. todos list)
|
|
17
|
+
profile.tsx
|
|
18
|
+
mail/
|
|
19
|
+
_layout.tsx # ListDetailLayout: list pane + Stack; unstable_settings.initialRouteName = "index"
|
|
20
|
+
index.tsx # ListDetailIndex: the list on a phone, the empty state on a tablet
|
|
21
|
+
[id].tsx # Detail screen; the open item lives in the URL
|
|
22
|
+
admin/
|
|
23
|
+
_layout.tsx # AdminShellLayout wrapper
|
|
24
|
+
index.tsx # AdminHome
|
|
25
|
+
[model]/
|
|
26
|
+
index.tsx # AdminScreenRouter (table or custom screen)
|
|
27
|
+
create.tsx # AdminModelForm (create)
|
|
28
|
+
[id].tsx # AdminModelForm (edit)
|
|
29
|
+
components/ # Screen components (NOT in app/)
|
|
30
|
+
hooks/ # Custom hooks
|
|
31
|
+
store/
|
|
32
|
+
index.ts # Redux store + generateAuthSlice
|
|
33
|
+
sdk.ts # Hand-maintained SDK extensions
|
|
34
|
+
openApiSdk.ts # Generated — never edit manually
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Rules
|
|
38
|
+
|
|
39
|
+
- Routes belong in `app/`; everything else lives outside.
|
|
40
|
+
- Never co-locate `components/`, `utils/`, or `types/` inside `app/`.
|
|
41
|
+
- Ensure a route matches `/` so the app is never blank.
|
|
42
|
+
- Use `(group)` routes to organize without affecting URLs.
|
|
43
|
+
- Use `_layout.tsx` for stacks and provider wrappers.
|
|
44
|
+
- Build list/detail screens (mail, inbox, records) as a route group with `ListDetailLayout` as its
|
|
45
|
+
`_layout.tsx`, `ListDetailIndex` as `index.tsx`, and the detail as `[id].tsx`. Phones push the
|
|
46
|
+
detail; tablets show the list and detail side by side.
|
|
47
|
+
|
|
48
|
+
### Auth-gated routing
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import {Redirect, Stack, useSegments} from "expo-router";
|
|
52
|
+
import {useSelectCurrentUserId} from "@terreno/rtk";
|
|
53
|
+
|
|
54
|
+
const RootLayoutNav: React.FC = () => {
|
|
55
|
+
const userId = useSelectCurrentUserId();
|
|
56
|
+
const segments = useSegments();
|
|
57
|
+
|
|
58
|
+
if (!userId && segments[0] !== "login") {
|
|
59
|
+
return <Redirect href="/login" />;
|
|
60
|
+
}
|
|
61
|
+
if (userId && segments[0] === "login") {
|
|
62
|
+
return <Redirect href="/(tabs)" />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return <Stack screenOptions={{headerShown: false}} />;
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Admin routes
|
|
70
|
+
|
|
71
|
+
Wrap admin routes once in `app/admin/_layout.tsx`:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import {AdminShellLayout} from "@terreno/admin-frontend";
|
|
75
|
+
import {Stack} from "expo-router";
|
|
76
|
+
import {terrenoApi} from "@/store/sdk";
|
|
77
|
+
|
|
78
|
+
export default function AdminLayout(): React.ReactElement {
|
|
79
|
+
return (
|
|
80
|
+
<AdminShellLayout
|
|
81
|
+
api={terrenoApi}
|
|
82
|
+
apiBase="/admin"
|
|
83
|
+
configurationPath="/admin/configuration"
|
|
84
|
+
routeBase="/admin"
|
|
85
|
+
versionConfigPath="/version-config"
|
|
86
|
+
>
|
|
87
|
+
<Stack screenOptions={{headerShown: false, contentStyle: {flex: 1}}} />
|
|
88
|
+
</AdminShellLayout>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|