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.
@@ -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