everything-dev 1.42.1 → 1.42.2
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/dist/cli/prompts.cjs +2 -1
- package/dist/cli/prompts.cjs.map +1 -1
- package/dist/cli/prompts.mjs +2 -1
- package/dist/cli/prompts.mjs.map +1 -1
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/api-and-auth/SKILL.md +355 -0
- package/skills/api-and-auth/references/generated-types.md +14 -0
- package/skills/api-and-auth/references/middleware.md +38 -0
- package/skills/cli-reference/SKILL.md +249 -0
- package/skills/plugin-development/SKILL.md +478 -0
- package/skills/ui-integration/SKILL.md +383 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-integration
|
|
3
|
+
description: Route creation, API client usage, auth client, SSR hydration, sidebar system, and the @/app module surface. Use when adding new UI routes, fetching data from the API, implementing auth flows, or customizing sidebar navigation.
|
|
4
|
+
metadata:
|
|
5
|
+
sources: "ui/src/app.ts,ui/src/lib/api.ts,ui/src/lib/auth.ts,ui/src/router.tsx,ui/src/router.server.tsx,ui/src/hydrate.tsx,ui/src/routes/__root.tsx,ui/src/routes/_layout.tsx,ui/src/routes/_layout/_authenticated.tsx,ui/src/lib/plugin-sidebar.gen.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# UI Integration
|
|
9
|
+
|
|
10
|
+
## File-based Routing
|
|
11
|
+
|
|
12
|
+
Routes are defined as files in `ui/src/routes/`. TanStack Router auto-generates the route tree.
|
|
13
|
+
|
|
14
|
+
### Route File Convention
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ui/src/routes/
|
|
18
|
+
├── __root.tsx # Root layout (HTML shell, head, scripts)
|
|
19
|
+
├── index.tsx # /
|
|
20
|
+
├── _layout.tsx # Shell layout (sidebar, header, footer)
|
|
21
|
+
├── _layout/
|
|
22
|
+
│ ├── index.tsx # / (inside shell)
|
|
23
|
+
│ ├── _authenticated.tsx # Auth guard layout (redirects to /login)
|
|
24
|
+
│ └── _authenticated/
|
|
25
|
+
│ ├── index.tsx # / (authenticated)
|
|
26
|
+
│ ├── settings.tsx # /settings
|
|
27
|
+
│ └── your-plugin/
|
|
28
|
+
│ └── index.tsx # /your-plugin
|
|
29
|
+
├── login.tsx # /login
|
|
30
|
+
└── about.tsx # /about
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Files starting with `_` are **layout** routes (parent components with `<Outlet />`)
|
|
34
|
+
- Files starting with `_` followed by a path segment are **nested layouts**
|
|
35
|
+
- Regular files become path segments (e.g., `settings.tsx` → `/settings`)
|
|
36
|
+
- Directories create nested paths (e.g., `_authenticated/settings.tsx` → `/settings` inside the auth guard)
|
|
37
|
+
|
|
38
|
+
### Basic Route
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
42
|
+
|
|
43
|
+
export const Route = createFileRoute("/about")({
|
|
44
|
+
component: AboutPage,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function AboutPage() {
|
|
48
|
+
return <div>About</div>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Route with Loader
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
56
|
+
import { useApiClient } from "@/app";
|
|
57
|
+
|
|
58
|
+
export const Route = createFileRoute("/projects/$id")({
|
|
59
|
+
loader: async ({ context, params }) => {
|
|
60
|
+
const { apiClient } = context;
|
|
61
|
+
const project = await apiClient.projects.getProject({ id: params.id });
|
|
62
|
+
return { project };
|
|
63
|
+
},
|
|
64
|
+
component: ProjectPage,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function ProjectPage() {
|
|
68
|
+
const { project } = Route.useLoaderData();
|
|
69
|
+
return <div>{project.name}</div>;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Route with Head/Sidebar Metadata
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
export const Route = createFileRoute("/_layout/settings")({
|
|
77
|
+
component: SettingsPage,
|
|
78
|
+
head: () => ({
|
|
79
|
+
meta: [{ title: "Settings" }],
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## The `@/app` Module Surface
|
|
85
|
+
|
|
86
|
+
`ui/src/app.ts` exports everything UI route code needs:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// Runtime config helpers
|
|
90
|
+
import { getRuntimeConfig, getAccount, getAppName, getActiveRuntime, getRepository, getCspNonce } from "@/app";
|
|
91
|
+
|
|
92
|
+
// API client
|
|
93
|
+
import { createApiClient, useApiClient, useOrpc, type ApiClient } from "@/app";
|
|
94
|
+
|
|
95
|
+
// Auth client
|
|
96
|
+
import { createAuthClient, useAuthClient, sessionQueryOptions, useRelayHistory, type AuthClient, type SessionData } from "@/app";
|
|
97
|
+
|
|
98
|
+
// Types
|
|
99
|
+
import type { ClientRuntimeConfig, RouterContext, CreateRouterOptions, RenderOptions } from "@/app";
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Runtime Helpers
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const config = getRuntimeConfig(); // Full window.__RUNTIME_CONFIG__
|
|
106
|
+
const account = getAccount(config); // NEAR account
|
|
107
|
+
const appName = getAppName(config); // Title or account
|
|
108
|
+
const activeRuntime = getActiveRuntime(config); // { accountId, gatewayId, title }
|
|
109
|
+
const repo = getRepository(config); // Repository URL
|
|
110
|
+
const nonce = getCspNonce(); // CSP nonce for inline scripts
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
These are SSR-safe — they accept an optional `RuntimeConfigInput` to work with config from loader data.
|
|
114
|
+
|
|
115
|
+
## API Client
|
|
116
|
+
|
|
117
|
+
### Creation
|
|
118
|
+
|
|
119
|
+
The client is created once in `hydrate.tsx` and stored in the Router context:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const apiClient = createApiClient({
|
|
123
|
+
hostUrl: runtimeConfig.hostUrl,
|
|
124
|
+
rpcBase: runtimeConfig.rpcBase, // "/api/rpc"
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It uses `RPCLink` with `credentials: "include"` and a global error interceptor that shows a toast on network failures.
|
|
129
|
+
|
|
130
|
+
### In Route Components
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { useApiClient, useOrpc } from "@/app";
|
|
134
|
+
|
|
135
|
+
function Component() {
|
|
136
|
+
// Direct usage
|
|
137
|
+
const apiClient = useApiClient();
|
|
138
|
+
const { data } = await apiClient.registry.listRegistryApps({ limit: 24 });
|
|
139
|
+
|
|
140
|
+
// With TanStack Query utils (caching, refetch, mutations)
|
|
141
|
+
const orpc = useOrpc();
|
|
142
|
+
const { data, isLoading } = orpc.registry.listRegistryApps.useQuery({ limit: 24 });
|
|
143
|
+
const mutation = orpc.registry.listRegistryApps.useMutation();
|
|
144
|
+
|
|
145
|
+
// In route loaders (from context, no hooks):
|
|
146
|
+
const { apiClient } = context;
|
|
147
|
+
const result = await apiClient.ping();
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Typed API Calls
|
|
152
|
+
|
|
153
|
+
Every procedure from every plugin is available on `apiClient` with full TypeScript types:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
apiClient.ping() // → { status, timestamp }
|
|
157
|
+
apiClient.registry.listRegistryApps({ limit: 24 }) // → { apps, meta }
|
|
158
|
+
apiClient.projects.getProject({ id: "proj_123" }) // → Project
|
|
159
|
+
apiClient.authHealth() // → { status, emailConfigured, ... }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The types come from the auto-generated `api-types.gen.ts`, which merges every plugin's contract into a single `ApiContract` type.
|
|
163
|
+
|
|
164
|
+
### Error Handling
|
|
165
|
+
|
|
166
|
+
Network/fetch errors are automatically caught by the `RPCLink` interceptor and shown as a toast:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
"Unable to connect to API" — The API is currently unavailable.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Procedure-level errors (like `UNAUTHORIZED`, `NOT_FOUND`) are thrown as `ORPCError` instances and should be caught inline:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
try {
|
|
176
|
+
await apiClient.authHealth();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof ORPCError) {
|
|
179
|
+
// Handle specific error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Auth Client
|
|
185
|
+
|
|
186
|
+
### Creation
|
|
187
|
+
|
|
188
|
+
The auth client is also created once in `hydrate.tsx`:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const authClient = createAuthClient(runtimeConfig);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Configured with Better-Auth plugins: SIWN (NEAR), passkey, organization, admin, API key, anonymous, phone.
|
|
195
|
+
|
|
196
|
+
### In Route Components
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { useAuthClient, sessionQueryOptions } from "@/app";
|
|
200
|
+
|
|
201
|
+
function LoginPage() {
|
|
202
|
+
const authClient = useAuthClient();
|
|
203
|
+
|
|
204
|
+
// Sign in with email
|
|
205
|
+
await authClient.signIn.email({ email, password });
|
|
206
|
+
|
|
207
|
+
// Sign in with NEAR (SIWN)
|
|
208
|
+
await authClient.signIn.siwn({ networkId: "mainnet" });
|
|
209
|
+
|
|
210
|
+
// Sign out
|
|
211
|
+
await authClient.signOut();
|
|
212
|
+
|
|
213
|
+
// Organization switching
|
|
214
|
+
await authClient.organization.setActive({ organizationId: "org_123" });
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Session Query Pattern
|
|
219
|
+
|
|
220
|
+
Use `sessionQueryOptions()` for standardized session fetching with TanStack Query:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// In a route loader:
|
|
224
|
+
const session = await queryClient.ensureQueryData(
|
|
225
|
+
sessionQueryOptions(authClient, context.session),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// In a component:
|
|
229
|
+
const { data: session } = useQuery(sessionQueryOptions(authClient));
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Returns `SessionData` with `user`, `session`, and typed auth context.
|
|
233
|
+
|
|
234
|
+
## Auth Route Guard
|
|
235
|
+
|
|
236
|
+
The `_authenticated.tsx` layout protects routes that require a session:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
export const Route = createFileRoute("/_layout/_authenticated")({
|
|
240
|
+
beforeLoad: async ({ context, location }) => {
|
|
241
|
+
const { queryClient, authClient } = context;
|
|
242
|
+
const session = await queryClient.ensureQueryData(
|
|
243
|
+
sessionQueryOptions(authClient, context.session),
|
|
244
|
+
);
|
|
245
|
+
if (!session?.user) {
|
|
246
|
+
throw redirect({
|
|
247
|
+
to: "/login",
|
|
248
|
+
search: { redirect: location.href },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (session.user.banned) {
|
|
252
|
+
throw redirect({ to: "/login", hash: "banned" });
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
auth: {
|
|
256
|
+
isAuthenticated: true,
|
|
257
|
+
user: session.user,
|
|
258
|
+
session: session.session,
|
|
259
|
+
activeOrganizationId: session.session?.activeOrganizationId || null,
|
|
260
|
+
isAnonymous: session.user.isAnonymous || false,
|
|
261
|
+
isAdmin: session.user.role === "admin",
|
|
262
|
+
isBanned: session.user.banned || false,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
component: AuthenticatedLayout,
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Nest routes under `_layout/_authenticated/` to inherit this guard. Unauthenticated users are redirected to `/login?redirect=<current-path>`.
|
|
271
|
+
|
|
272
|
+
## Sidebar System
|
|
273
|
+
|
|
274
|
+
Sidebar items are auto-generated by `bos types gen` into `ui/src/lib/plugin-sidebar.gen.ts` from `bos.config.json`.
|
|
275
|
+
|
|
276
|
+
### Plugin sidebar config:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"plugins": {
|
|
281
|
+
"your-plugin": {
|
|
282
|
+
"sidebar": [
|
|
283
|
+
{ "to": "/your-plugin", "label": "Your Plugin", "icon": "Activity", "roleRequired": "member" }
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Role filtering in `_layout.tsx`:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { pluginSidebarItems, type SidebarItem, type SidebarRole } from "@/lib/plugin-sidebar.gen";
|
|
294
|
+
|
|
295
|
+
function filterSidebarByRole(items: SidebarItem[], userRole: SidebarRole): SidebarItem[] {
|
|
296
|
+
return items.filter((item) => {
|
|
297
|
+
if (item.roleRequired === "anon") return true;
|
|
298
|
+
if (item.roleRequired === "member" && userRole !== "anon") return true;
|
|
299
|
+
if (item.roleRequired === "admin" && userRole === "admin") return true;
|
|
300
|
+
return false;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const userRole = isAdmin ? "admin" : isAuthenticated ? "member" : "anon";
|
|
305
|
+
const visibleItems = filterSidebarByRole(pluginSidebarItems, userRole);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Available icons: any `lucide-react` icon name as a string.
|
|
309
|
+
|
|
310
|
+
## SSR Architecture
|
|
311
|
+
|
|
312
|
+
### Client Bootstrap (`hydrate.tsx`)
|
|
313
|
+
|
|
314
|
+
1. Reads `window.__RUNTIME_CONFIG__`
|
|
315
|
+
2. Creates `QueryClient`, `ApiClient`, `AuthClient`
|
|
316
|
+
3. Creates TanStack Router with browser history
|
|
317
|
+
4. Hydrates React DOM into the HTML shell
|
|
318
|
+
|
|
319
|
+
### Server-Side (`router.server.tsx`)
|
|
320
|
+
|
|
321
|
+
1. Creates request-scoped router with memory history
|
|
322
|
+
2. Creates per-request `apiClient` and `authClient`
|
|
323
|
+
3. Renders to stream via TanStack Router SSR's `createRequestHandler`
|
|
324
|
+
4. Host calls `loadRouterModule()` to dynamically load the SSR bundle
|
|
325
|
+
|
|
326
|
+
### Route Loading for SSR
|
|
327
|
+
|
|
328
|
+
Loaders run on both server and client. Use `loader` for data needed at render time, `beforeLoad` for auth checks and redirects:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
export const Route = createFileRoute("/projects")({
|
|
332
|
+
beforeLoad: async ({ context }) => {
|
|
333
|
+
// Runs on server and client — good for auth
|
|
334
|
+
},
|
|
335
|
+
loader: async ({ context }) => {
|
|
336
|
+
// Runs on server and client — good for data fetching
|
|
337
|
+
const { apiClient } = context;
|
|
338
|
+
return await apiClient.projects.listProjects();
|
|
339
|
+
},
|
|
340
|
+
component: ProjectsPage,
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The `RouterContext` type:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
interface RouterContext extends BaseRouterContextWithApi<ApiClient, SessionData> {
|
|
348
|
+
apiClient: ApiClient;
|
|
349
|
+
authClient: AuthClient;
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Component Patterns
|
|
354
|
+
|
|
355
|
+
### Semantic Tailwind Classes
|
|
356
|
+
|
|
357
|
+
Use theme-aware classes — never hardcoded colors:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
<div className="bg-background text-foreground"> // ✅ correct
|
|
361
|
+
<div className="bg-blue-600 text-white"> // ❌ wrong
|
|
362
|
+
<div className="text-muted-foreground"> // ✅ muted text
|
|
363
|
+
<div className="bg-card border border-border"> // ✅ card surface
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### SSR-Safe Rendering
|
|
367
|
+
|
|
368
|
+
For values that only exist on the client:
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
import { useClientValue } from "@/hooks";
|
|
372
|
+
|
|
373
|
+
function Component() {
|
|
374
|
+
const appName = useClientValue(() => getAppName(), "app");
|
|
375
|
+
return <h1>{appName}</h1>;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Component Location
|
|
380
|
+
|
|
381
|
+
- Shared UI components: `ui/src/components/ui/` — semantic, reusable primitives
|
|
382
|
+
- Feature components: colocated with the route that uses them
|
|
383
|
+
- Exports from `ui/src/components/index.ts` for shared components
|