@tailor-platform/app-shell 1.15.0 → 1.17.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 (109) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +811 -131
  3. package/dist/app-shell.js +7155 -5797
  4. package/dist/{appshell-DLVBFo5J.js → appshell-CahhZy7v.js} +656 -524
  5. package/dist/semantic-contrast.test.ts +154 -0
  6. package/dist/style-entry.d.ts +147 -92
  7. package/dist/testing.d.ts +152 -95
  8. package/dist/testing.js +1 -1
  9. package/dist/theme-bridge.test.ts +101 -1
  10. package/dist/theme.bridge.css +91 -0
  11. package/dist/theme.css +6 -1
  12. package/dist/themes/_template.css +7 -1
  13. package/dist/themes/bloom.css +0 -6
  14. package/dist/themes/cream.css +0 -6
  15. package/dist/themes/default.css +389 -69
  16. package/dist/vite-plugin.d.ts +147 -92
  17. package/package.json +8 -8
  18. package/skills/app-shell-patterns/SKILL.md +108 -19
  19. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
  20. package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
  21. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
  22. package/skills/app-shell-patterns/references/api/define-module.md +247 -0
  23. package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
  24. package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
  25. package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
  26. package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
  27. package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
  28. package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
  29. package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
  30. package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
  31. package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
  32. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +132 -0
  33. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +150 -0
  34. package/skills/app-shell-patterns/references/api/use-app-shell.md +150 -0
  35. package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
  36. package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
  37. package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
  38. package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
  39. package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
  40. package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
  41. package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
  42. package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
  43. package/skills/app-shell-patterns/references/api/use-toast.md +104 -0
  44. package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
  45. package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
  46. package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
  47. package/skills/app-shell-patterns/references/components/alert.md +202 -0
  48. package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
  49. package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
  50. package/skills/app-shell-patterns/references/components/attachment.md +138 -0
  51. package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
  52. package/skills/app-shell-patterns/references/components/avatar.md +80 -0
  53. package/skills/app-shell-patterns/references/components/badge.md +309 -0
  54. package/skills/app-shell-patterns/references/components/button.md +164 -0
  55. package/skills/app-shell-patterns/references/components/card.md +181 -0
  56. package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
  57. package/skills/app-shell-patterns/references/components/combobox.md +318 -0
  58. package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
  59. package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
  60. package/skills/app-shell-patterns/references/components/data-table.md +1168 -0
  61. package/skills/app-shell-patterns/references/components/date-picker.md +259 -0
  62. package/skills/app-shell-patterns/references/components/default-header.md +75 -0
  63. package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
  64. package/skills/app-shell-patterns/references/components/description-card.md +569 -0
  65. package/skills/app-shell-patterns/references/components/dialog.md +164 -0
  66. package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
  67. package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
  68. package/skills/app-shell-patterns/references/components/form.md +378 -0
  69. package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
  70. package/skills/app-shell-patterns/references/components/grid.md +184 -0
  71. package/skills/app-shell-patterns/references/components/input.md +83 -0
  72. package/skills/app-shell-patterns/references/components/layout.md +539 -0
  73. package/skills/app-shell-patterns/references/components/menu.md +218 -0
  74. package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
  75. package/skills/app-shell-patterns/references/components/select.md +283 -0
  76. package/skills/app-shell-patterns/references/components/sheet.md +237 -0
  77. package/skills/app-shell-patterns/references/components/sidebar-group.md +472 -0
  78. package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
  79. package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
  80. package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
  81. package/skills/app-shell-patterns/references/components/spinner.md +81 -0
  82. package/skills/app-shell-patterns/references/components/table.md +188 -0
  83. package/skills/app-shell-patterns/references/components/tabs.md +158 -0
  84. package/skills/app-shell-patterns/references/components/textarea.md +122 -0
  85. package/skills/app-shell-patterns/references/components/timeline.md +317 -0
  86. package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
  87. package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
  88. package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
  89. package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
  90. package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
  91. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +356 -0
  92. package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +31 -18
  93. package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
  94. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +425 -0
  95. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +420 -0
  96. package/skills/app-shell-patterns/references/concepts/styling-theming.md +785 -0
  97. package/skills/app-shell-patterns/references/migrations.md +55 -0
  98. package/skills/app-shell-patterns/references/pages/{detail.md → document-detail.md} +10 -33
  99. package/skills/app-shell-patterns/references/patterns/form-composer.md +43 -45
  100. package/skills/app-shell-patterns/references/patterns/form-modal.md +37 -92
  101. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
  102. package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
  103. package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
  104. package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
  105. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +136 -139
  106. package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
  107. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +13 -88
  108. package/skills/app-shell-patterns/references/fundamental/components.md +0 -1009
  109. package/skills/app-shell-patterns/references/fundamental/design-system.md +0 -440
@@ -0,0 +1,356 @@
1
+ ---
2
+ title: File-Based Routing
3
+ description: Define pages via directory structure using the Vite plugin instead of explicit module configuration
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/concepts/file-based-routing.docs.outline.md — do not edit by hand. -->
7
+
8
+ # File-Based Routing
9
+
10
+ File-based routing allows you to define pages by simply placing components in a directory structure, eliminating the need for explicit `defineModule()` and `defineResource()` calls.
11
+
12
+ ## Overview
13
+
14
+ Instead of manually assembling module/resource hierarchies, you define pages as files in a `pages/` directory. The path is automatically derived from the directory structure.
15
+
16
+ ```
17
+ src/pages/
18
+ ├── page.tsx # / (root path)
19
+ ├── purchasing/
20
+ │ ├── page.tsx # /purchasing
21
+ │ └── orders/
22
+ │ ├── page.tsx # /purchasing/orders
23
+ │ └── [id]/
24
+ │ └── page.tsx # /purchasing/orders/:id
25
+ └── (admin)/ # Grouping (not included in path)
26
+ └── settings/
27
+ └── page.tsx # /settings
28
+ ```
29
+
30
+ Directories **without** a `page.tsx` still contribute a path segment — navigating to that path returns a 404 while their children remain accessible:
31
+
32
+ ```
33
+ src/pages/
34
+ ├── admin/
35
+ │ ├── users/
36
+ │ │ └── page.tsx # /admin/users → renders page
37
+ │ └── roles/
38
+ │ └── page.tsx # /admin/roles → renders page
39
+ │ # no page.tsx for /admin → 404
40
+ ```
41
+
42
+ Guards defined on a `page.tsx` apply to that exact path. Directories without `page.tsx` do not support guards — add guards to the individual child pages instead.
43
+
44
+ ## Setup
45
+
46
+ ### 1. Configure Vite Plugin
47
+
48
+ ```typescript
49
+ // vite.config.ts
50
+ import { defineConfig } from "vite";
51
+ import react from "@vitejs/plugin-react";
52
+ import { appShellRoutes } from "@tailor-platform/app-shell/vite-plugin";
53
+
54
+ export default defineConfig({
55
+ plugins: [
56
+ react(),
57
+ appShellRoutes({
58
+ entrypoint: "src/App.tsx", // recommended
59
+ }),
60
+ ],
61
+ });
62
+ ```
63
+
64
+ #### Plugin Options
65
+
66
+ The canonical TypeScript import for the Vite plugin and its option types is the
67
+ `@tailor-platform/app-shell/vite-plugin` subpath:
68
+
69
+ ```ts
70
+ import {
71
+ appShellRoutes,
72
+ type AppShellRoutesPluginOptions,
73
+ type TypedRoutesOptions,
74
+ } from "@tailor-platform/app-shell/vite-plugin";
75
+ ```
76
+
77
+ | Option | Type | Default | Description |
78
+ | --------------------- | ------------------------------- | ------------- | ------------------------------------------------------------------------------------ |
79
+ | `pagesDir` | `string` | `"src/pages"` | Directory containing page files |
80
+ | `logLevel` | `"info" \| "debug" \| "off"` | `"info"` | Log level for plugin output |
81
+ | `entrypoint` | `string` | `undefined` | File that renders the AppShell component (relative to project root). See note below. |
82
+ | `generateTypedRoutes` | `boolean \| { output: string }` | `false` | Generate a TypeScript file with type-safe route definitions |
83
+
84
+ > **`entrypoint` (recommended):** When specified, only imports from this file are intercepted and replaced with the pages-injected AppShell. All other files — including page components — import directly from the real package. Without this option, all user-code imports of `@tailor-platform/app-shell` are intercepted, which can cause TDZ (Temporal Dead Zone) errors in page components that also import from the package.
85
+
86
+ ### 2. Use AppShell (No Configuration Needed)
87
+
88
+ ```tsx
89
+ // App.tsx
90
+ import { AppShell, SidebarLayout, DefaultSidebar } from "@tailor-platform/app-shell";
91
+
92
+ const App = () => {
93
+ return (
94
+ <AppShell title="My App">
95
+ <SidebarLayout sidebar={<DefaultSidebar />} />
96
+ </AppShell>
97
+ );
98
+ };
99
+ ```
100
+
101
+ Pages are automatically discovered and injected—no `modules` prop required!
102
+
103
+ ## Page Components
104
+
105
+ ### Minimal Example
106
+
107
+ The simplest page is just a default-exported component:
108
+
109
+ ```tsx
110
+ // src/pages/about/page.tsx
111
+ export default () => <div>About</div>;
112
+ ```
113
+
114
+ ### Full Example
115
+
116
+ Use `appShellPageProps` static field to configure page metadata and guards:
117
+
118
+ ```tsx
119
+ // src/pages/dashboard/page.tsx
120
+ import type { AppShellPageProps } from "@tailor-platform/app-shell";
121
+ import { authGuard } from "../guards";
122
+ import { DashboardIcon } from "../icons";
123
+
124
+ const DashboardPage = () => {
125
+ return <div>Dashboard Content</div>;
126
+ };
127
+
128
+ DashboardPage.appShellPageProps = {
129
+ meta: {
130
+ title: "Dashboard",
131
+ icon: <DashboardIcon />,
132
+ },
133
+ guards: [authGuard],
134
+ } satisfies AppShellPageProps;
135
+
136
+ export default DashboardPage;
137
+ ```
138
+
139
+ ### AppShellPageProps Type
140
+
141
+ ```typescript
142
+ type AppShellPageProps = {
143
+ meta?: {
144
+ title?: LocalizedString;
145
+ icon?: ReactNode;
146
+ breadcrumbTitle?: string | ((segment: string) => LocalizedString);
147
+ };
148
+ guards?: Guard[];
149
+ };
150
+ ```
151
+
152
+ For a localized breadcrumb, use `meta: { breadcrumbTitle: labels.t("orders") }`, just like `meta.title`.
153
+ AppShell recognizes translation functions created by `labels.t()` and resolves them with the current locale.
154
+ For a dynamic label, use `breadcrumbTitle: (segment) => labels.t("order", { id: segment })`.
155
+ Other callbacks receive the URL segment; AppShell resolves any returned `LocalizedString` with the current locale.
156
+ The existing `() => labels.t("orders")` form remains supported. Wrap handwritten locale functions in an outer callback rather than passing them directly.
157
+
158
+ ## Path Conventions
159
+
160
+ | Directory Name | Converts To | Description |
161
+ | -------------- | ----------- | ---------------------------------------------- |
162
+ | `orders` | `orders` | Static segment |
163
+ | `[id]` | `:id` | Dynamic parameter |
164
+ | `[...slug]` | `*slug` | Catch-all — matches zero or more path segments |
165
+ | `(group)` | (excluded) | Grouping only (not in path) |
166
+ | `_lib` | (ignored) | Not routed (for shared logic) |
167
+
168
+ ### Examples
169
+
170
+ ```
171
+ src/pages/
172
+ ├── users/
173
+ │ ├── page.tsx # /users
174
+ │ └── [userId]/
175
+ │ └── page.tsx # /users/:userId
176
+ ├── (marketing)/
177
+ │ ├── campaigns/
178
+ │ │ └── page.tsx # /campaigns (not /marketing/campaigns)
179
+ │ └── analytics/
180
+ │ └── page.tsx # /analytics
181
+ └── _utils/
182
+ └── helpers.ts # Not routed (shared utilities)
183
+ ```
184
+
185
+ ## Guards
186
+
187
+ Guards are **not** automatically inherited from parent pages. Each page must explicitly define its own guards:
188
+
189
+ ```tsx
190
+ // /dashboard/page.tsx
191
+ DashboardPage.appShellPageProps = {
192
+ guards: [authGuard],
193
+ } satisfies AppShellPageProps;
194
+
195
+ // /dashboard/admin/page.tsx — must include authGuard explicitly
196
+ AdminPage.appShellPageProps = {
197
+ guards: [authGuard, adminGuard],
198
+ } satisfies AppShellPageProps;
199
+ ```
200
+
201
+ To share common guards across pages, compose them from a shared module:
202
+
203
+ ```tsx
204
+ // src/guards.ts
205
+ export const requireAuth = [authGuard];
206
+ export const requireAdmin = [authGuard, adminGuard];
207
+
208
+ // src/pages/dashboard/orders/page.tsx
209
+ import { requireAuth } from "@/guards";
210
+
211
+ OrdersPage.appShellPageProps = {
212
+ guards: [...requireAuth],
213
+ } satisfies AppShellPageProps;
214
+ ```
215
+
216
+ ## Typed Routes
217
+
218
+ Enable `generateTypedRoutes` in the Vite plugin to generate type-safe route helpers:
219
+
220
+ ```typescript
221
+ // vite.config.ts
222
+ appShellRoutes({
223
+ generateTypedRoutes: true,
224
+ });
225
+ ```
226
+
227
+ This generates `src/routes.generated.ts` with a `paths` helper:
228
+
229
+ ```tsx
230
+ import { paths } from './routes.generated';
231
+ import { Link } from '@tailor-platform/app-shell';
232
+
233
+ // Static routes
234
+ <Link to={paths.for("/dashboard")}>Dashboard</Link>
235
+
236
+ // Dynamic routes - params are type-checked
237
+ <Link to={paths.for("/dashboard/orders/:id", { id: orderId })}>Order</Link>
238
+
239
+ // TypeScript catches errors:
240
+ paths.for("/dashboard/orders/:id"); // Error: missing 'id'
241
+ paths.for("/invalid"); // Error: route doesn't exist
242
+ ```
243
+
244
+ ## Comparison with Legacy API
245
+
246
+ ### Before: Explicit Hierarchy Assembly
247
+
248
+ ```tsx
249
+ const orderDetailResource = defineResource({ path: ":id", component: OrderDetail });
250
+ const ordersResource = defineResource({
251
+ path: "orders",
252
+ component: OrdersList,
253
+ subResources: [orderDetailResource],
254
+ });
255
+ const purchasingModule = defineModule({
256
+ path: "purchasing",
257
+ resources: [ordersResource],
258
+ });
259
+
260
+ <AppShell modules={[purchasingModule]} />;
261
+ ```
262
+
263
+ ### After: File-Based Pages
264
+
265
+ ```tsx
266
+ // src/pages/purchasing/orders/[id]/page.tsx
267
+ const OrderDetailPage = () => <div>Order Detail</div>;
268
+ OrderDetailPage.appShellPageProps = {
269
+ meta: { title: "Order Detail" },
270
+ } satisfies AppShellPageProps;
271
+ export default OrderDetailPage;
272
+ ```
273
+
274
+ ```tsx
275
+ // App.tsx - No configuration needed
276
+ <AppShell title="My App">
277
+ <SidebarLayout sidebar={<DefaultSidebar />} />
278
+ </AppShell>
279
+ ```
280
+
281
+ ### Concept Mapping
282
+
283
+ | Legacy API | File-Based | Notes |
284
+ | ----------------------- | ------------------------------- | ----------------------------- |
285
+ | `Module` | — | First-level directory |
286
+ | `Resource` | — | Directory structure |
287
+ | `defineModule()` | — | Not needed |
288
+ | `defineResource()` | — | Not needed |
289
+ | `path` property | Directory name | Auto-derived |
290
+ | `component` property | `page.tsx` default export | File convention |
291
+ | `meta` property | `Page.appShellPageProps.meta` | Static field |
292
+ | `guards` property | `Page.appShellPageProps.guards` | Static field (no inheritance) |
293
+ | `subResources` property | Subdirectories | Auto-derived |
294
+
295
+ ## Compatibility
296
+
297
+ File-based pages and explicit `modules` prop are **mutually exclusive**.
298
+
299
+ ### Usage Patterns
300
+
301
+ ```tsx
302
+ // ✅ Pattern 1: File-based pages with plugin (recommended)
303
+ // vite.config.ts has plugin configured
304
+ <AppShell title="My App">
305
+ <SidebarLayout sidebar={<DefaultSidebar />} />
306
+ </AppShell>
307
+
308
+ // ✅ Pattern 2: Explicit modules without plugin (legacy)
309
+ <AppShell modules={[myModule]} title="My App">
310
+ <SidebarLayout sidebar={<DefaultSidebar />} />
311
+ </AppShell>
312
+
313
+ // ✅ Pattern 3: Plugin enabled + modules prop (modules takes precedence)
314
+ // Even with plugin enabled, modules prop is used when provided
315
+ <AppShell modules={[myModule]} title="My App">
316
+ <SidebarLayout sidebar={<DefaultSidebar />} />
317
+ </AppShell>
318
+
319
+ // ❌ Pattern 4: No plugin + no modules (runtime error)
320
+ <AppShell title="My App">
321
+ {/* → Runtime error: No routes configured */}
322
+ </AppShell>
323
+ ```
324
+
325
+ ## Migration Guide
326
+
327
+ To migrate from `defineModule`/`defineResource` to file-based routing:
328
+
329
+ 1. **Add Vite Plugin**
330
+
331
+ ```typescript
332
+ // vite.config.ts
333
+ import { appShellRoutes } from "@tailor-platform/app-shell/vite-plugin";
334
+
335
+ export default defineConfig({
336
+ plugins: [react(), appShellRoutes({ entrypoint: "src/App.tsx" })],
337
+ });
338
+ ```
339
+
340
+ 2. **Create pages directory structure**
341
+ - Map each module to a top-level directory
342
+ - Map each resource to a subdirectory with `page.tsx`
343
+ - Use `[param]` for dynamic segments
344
+
345
+ 3. **Move component and metadata**
346
+
347
+ ```tsx
348
+ // Before: defineResource({ path: "orders", component: Orders, meta: {...} })
349
+
350
+ // After: src/pages/orders/page.tsx
351
+ const OrdersPage = () => <Orders />;
352
+ OrdersPage.appShellPageProps = { meta: {...} };
353
+ export default OrdersPage;
354
+ ```
355
+
356
+ 4. **Remove `modules` prop** from `<AppShell>` once all pages are migrated
@@ -1,3 +1,10 @@
1
+ ---
2
+ title: GraphQL Conventions
3
+ description: Conventions for data access in AppShell apps — urql setup, fragment collocation, the connection pattern, and mutation cache invalidation
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/concepts/graphql.docs.outline.md — do not edit by hand. -->
7
+
1
8
  # GraphQL Conventions
2
9
 
3
10
  Type-safe GraphQL via `gql.tada`. The frontend never hand-writes types — generated bindings come from `pnpm generate` (step 2 of the workflow) reading the deployed backend schema.
@@ -12,7 +19,7 @@ import { graphql, type FragmentOf, readFragment } from "@/graphql";
12
19
 
13
20
  `@/graphql` is generated. Don't import `gql` from `urql` or `gql.tada` directly — go through the barrel so types stay coherent.
14
21
 
15
- The urql client is wired in `App.tsx` (see [project-setup.md](project-setup.md)). Pages don't construct their own client.
22
+ The urql client is wired in your app's root (`App.tsx`); pages don't construct their own client.
16
23
 
17
24
  ## Built-in operations vs custom resolvers
18
25
 
@@ -106,7 +113,7 @@ For `pnpm gql-tada:check` to pass, always destructure with optional chaining and
106
113
 
107
114
  ### Wiring to `DataTable` (sort, filters, pagination)
108
115
 
109
- For **standard list pages**, do not hand-roll only `first` / `after`. Compose AppShell **`DataTable`** + **`useCollectionVariables`** + **`useDataTable`**: derive query variables from `variables.pagination`, `variables.query`, `variables.order`, map `edges` / `pageInfo` / `total` into `useDataTable`’s `data`. See **`patterns/list/dense-scan.md`** and **`components.md` → DataTable**.
116
+ For **standard list pages**, do not hand-roll only `first` / `after`. Compose AppShell **`DataTable`** + **`useCollectionVariables`** + **`useDataTable`**: derive query variables from `variables.pagination`, `variables.query`, `variables.order`, map `edges` / `pageInfo` / `total` into `useDataTable`’s `data`. See [`list-dense-scan`](../patterns/list-dense-scan.md) and [`DataTable`](../components/data-table.md).
110
117
 
111
118
  When `@tailor-platform/sdk-plugin-app-shell` codegen is enabled, pass generated **`tableMetadata`** into **`useCollectionVariables`** so `variables.query` / `variables.order` are typed against your Tailor GraphQL inputs (upstream **data-table.md** Typed query variables).
112
119
 
@@ -146,22 +153,28 @@ function CreateOrderButton() {
146
153
 
147
154
  ### `additionalTypenames`
148
155
 
149
- When a mutation creates/updates/deletes an entity that another query already has cached, urql won't know to refetch unless you pass `additionalTypenames`. Use it on **both** the query and the mutation when they share a typename:
156
+ When a mutation creates/updates/deletes an entity that another query already has cached, urql won't know to refetch unless you pass `additionalTypenames`. Use it on **both** the query and the mutation when they share a typename.
157
+
158
+ Define the query's static `context` at module scope, outside the component, so its reference stays stable between renders. `useQuery` lists `context` as a hook dependency, so a fresh object on each render re-creates the query subscription and can cause query re-execution loops. The mutation's second argument is passed per call and is not a hook dependency, so it can stay inline.
150
159
 
151
160
  ```tsx
152
- // Query side — declares it cares about the Order typename
153
- const [{ data }] = useQuery({
154
- query: OrdersQuery,
155
- variables: { first: 25 },
156
- context: { additionalTypenames: ["Order"] },
157
- });
158
-
159
- // Mutation side — declares it produces Order writes
160
- const [, createOrder] = useMutation(CreateOrderMutation);
161
- await createOrder(
162
- { input },
163
- { additionalTypenames: ["Order"] }, // triggers refetch of any query subscribed to 'Order'
164
- );
161
+ const ordersQueryContext = { additionalTypenames: ["Order"] };
162
+
163
+ function useOrders() {
164
+ // Query side — declares it cares about the Order typename
165
+ const [{ data }] = useQuery({
166
+ query: OrdersQuery,
167
+ variables: { first: 25 },
168
+ context: ordersQueryContext,
169
+ });
170
+
171
+ // Mutation side — declares it produces Order writes
172
+ const [, executeCreateOrder] = useMutation(CreateOrderMutation);
173
+ const createOrder = (input: CreateOrderInput) =>
174
+ executeCreateOrder({ input }, { additionalTypenames: ["Order"] });
175
+
176
+ return { data, createOrder };
177
+ }
165
178
  ```
166
179
 
167
180
  Skip `additionalTypenames` only when the mutation's return type already includes the affected entity _and_ every cached query for it touched the same `id` — urql will then refresh by id. When in doubt, add it.
@@ -191,7 +204,7 @@ Every page component:
191
204
  };
192
205
  ```
193
206
 
194
- 3. Uses `appShellPageProps.guards` for permission gates. See [project-setup.md](project-setup.md).
207
+ 3. Uses `appShellPageProps.guards` for permission gates.
195
208
 
196
209
  ## Quick reference
197
210
 
@@ -202,4 +215,4 @@ Every page component:
202
215
  | Why isn't my list refreshing after a mutation? | Add `additionalTypenames: ['<Type>']` on both query and mutation |
203
216
  | Connection vs array? | Always connection (`edges { node }`) for built-in `<entities>` queries; check the schema for custom resolvers |
204
217
  | Pagination / sort / toolbar filters together? | `useCollectionVariables` + `DataTable`; see § **Wiring to `DataTable`** above |
205
- | Where's the urql client configured? | `App.tsx` — see `project-setup.md` |
218
+ | Where's the urql client configured? | `App.tsx` (your app's root) |
@@ -0,0 +1,229 @@
1
+ ---
2
+ title: Modules and Resources
3
+ description: Learn how to structure your AppShell application using modules and resources for automatic routing, navigation, and breadcrumbs
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/concepts/modules-and-resources.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Modules and Resources
9
+
10
+ AppShell renders sidebar navigation, breadcrumbs, and handles routing based on a structured configuration of modules and resources (and sub-resources, sub-sub-resources, and so on).
11
+
12
+ Modules and Resources both share core interface. Of interest:
13
+
14
+ - `path: string` - the path segment
15
+ - `component: (props: ResourceComponentProps) => ReactNode` - the component to render when the router navigates to that module/resource (optional — omitting it causes the path to return a 404 while child routes remain accessible)
16
+ - `guards?: Guard[]` - optional array of guard functions to control access based on permissions or feature flags
17
+
18
+ A trivial `modules` example:
19
+
20
+ ```tsx
21
+ import { defineModule, defineResource, pass, hidden } from "@tailor-platform/app-shell";
22
+
23
+ const appShellPropModule = [
24
+ defineModule({
25
+ path: "purchasing",
26
+ component: PurchasingLandingPage,
27
+ resources: [
28
+ defineResource({
29
+ path: "orders",
30
+ component: OrdersPage,
31
+ subResources: [
32
+ defineResource({
33
+ path: ":id",
34
+ component: OrderDetailPage,
35
+ }),
36
+ ],
37
+ }),
38
+ defineResource({
39
+ path: "invoices",
40
+ component: PurchaseInvoicesPage,
41
+ // ...
42
+ }),
43
+ ],
44
+ }),
45
+ defineModule({
46
+ path: "sales",
47
+ component: SalesLandingPage,
48
+ resources: [
49
+ defineResource({
50
+ path: "invoices",
51
+ component: SalesInvoicesPage,
52
+ // ...
53
+ }),
54
+ // ...
55
+ ],
56
+ }),
57
+ // Module without component - /dashboard returns 404, child routes remain accessible
58
+ defineModule({
59
+ path: "dashboard",
60
+ resources: [
61
+ defineResource({
62
+ path: "overview",
63
+ component: DashboardOverview,
64
+ }),
65
+ defineResource({
66
+ path: "analytics",
67
+ component: DashboardAnalytics,
68
+ }),
69
+ ],
70
+ }),
71
+ // Module with guards for access control
72
+ defineModule({
73
+ path: "reports",
74
+ component: ReportsPage,
75
+ resources: [reportsListResource],
76
+ guards: [
77
+ async ({ context, signal }) => {
78
+ const ok = await fetch("/api/me/permissions?scope=reports", {
79
+ signal,
80
+ }).then((r) => r.ok);
81
+ return ok ? pass() : hidden();
82
+ },
83
+ ],
84
+ }),
85
+ ];
86
+ ```
87
+
88
+ Produces the following navigation menu:
89
+
90
+ ```
91
+ › Purchasing
92
+ - Orders
93
+ - Invoices
94
+ › Sales
95
+ - Invoices
96
+ ...
97
+ › Reports (only visible if user has reports permission)
98
+ ```
99
+
100
+ Modules show as top-level menu items and resources are the sub-menu items. Sub-resources do not show up on the side navigation menu, but will be available via other navigation and show in breadcrumbs.
101
+
102
+ In the example above, clicking the 'Orders' menu item above will take you to `/{basePath}/purchasing/orders` and render the OrdersPage page. If you browse, either by direct request or via client-side navigation to `/{basePath}/purchasing/orders/1234`, AppShell will render OrderDetailPage with 'id' available via useParams
103
+
104
+ Providing a `component` for a Module is optional. If omitted, navigating directly to the module path returns a 404 response while its child resources remain fully accessible. This is useful when a path segment exists solely to group child routes under a shared URL prefix with no landing page of its own.
105
+
106
+ When a module has no `component`, its breadcrumb segment is rendered as plain non-clickable text (instead of a link), preventing navigation to a page that does not exist.
107
+
108
+ The same applies to resources: a `defineResource()` call without a `component` makes the resource path itself return 404 while its `subResources` remain accessible.
109
+
110
+ Guards on component-less modules and resources execute correctly. For example, a `redirectTo()` guard will fire as expected. If all guards return `pass()`, the route falls back to a 404 (since there is no component to render).
111
+
112
+ > Read more about [client-side navigation](./routing-navigation.md) in AppShell apps
113
+
114
+ ## Route Guards
115
+
116
+ Both modules and resources support `guards` - an array of functions that control access based on permissions, feature flags, or any other criteria. Guards are executed in order and provide:
117
+
118
+ - Composability: Combine multiple guards for complex access logic
119
+ - Reusability: Share common guards across routes
120
+ - Semantic constraints: Clear `pass()`, `hidden()`, or `redirectTo()` results
121
+
122
+ See the [Guards documentation](../api/guards/overview.md) for full details.
123
+
124
+ ### Guard Examples
125
+
126
+ **Permission-based access:**
127
+
128
+ ```tsx
129
+ import { defineModule, pass, hidden } from "@tailor-platform/app-shell";
130
+
131
+ const reportsModule = defineModule({
132
+ path: "reports",
133
+ component: ReportsPage,
134
+ resources: [reportsListResource],
135
+ guards: [
136
+ async ({ context }) => {
137
+ const ok = await fetch("/api/me/permissions?scope=reports").then((r) => r.ok);
138
+ return ok ? pass() : hidden();
139
+ },
140
+ ],
141
+ });
142
+ ```
143
+
144
+ **Feature flag based:**
145
+
146
+ ```tsx
147
+ import { pass, hidden } from "@tailor-platform/app-shell";
148
+
149
+ const betaFeaturesModule = defineModule({
150
+ path: "beta",
151
+ component: BetaFeaturesPage,
152
+ resources: [newFeatureResource],
153
+ guards: [
154
+ async ({ context }) => {
155
+ const enabled = await checkFeatureFlag("beta-features");
156
+ return enabled ? pass() : hidden();
157
+ },
158
+ ],
159
+ });
160
+ ```
161
+
162
+ **Tenant tier based:**
163
+
164
+ ```tsx
165
+ import { pass, hidden } from "@tailor-platform/app-shell";
166
+
167
+ const billingModule = defineModule({
168
+ path: "billing",
169
+ component: BillingPage,
170
+ resources: [billingResources],
171
+ guards: [
172
+ async ({ context }) => {
173
+ const plan = await getCurrentTenantPlan();
174
+ return plan === "enterprise" ? pass() : hidden();
175
+ },
176
+ ],
177
+ });
178
+ ```
179
+
180
+ **Resource-level guards:**
181
+
182
+ ```tsx
183
+ import { pass, hidden } from "@tailor-platform/app-shell";
184
+
185
+ defineResource({
186
+ path: "admin-settings",
187
+ component: AdminSettingsPage,
188
+ guards: [
189
+ async ({ context }) => {
190
+ const user = await getCurrentUser();
191
+ return user.role === "admin" ? pass() : hidden();
192
+ },
193
+ ],
194
+ });
195
+ ```
196
+
197
+ **Reusable guards:**
198
+
199
+ ```tsx
200
+ import { type Guard, pass, hidden, redirectTo } from "@tailor-platform/app-shell";
201
+
202
+ // Define reusable guards
203
+ const requireAuth: Guard = ({ context }) => {
204
+ if (!context.currentUser) {
205
+ return redirectTo("/login");
206
+ }
207
+ return pass();
208
+ };
209
+
210
+ const requireAdmin: Guard = ({ context }) => {
211
+ if (context.currentUser?.role !== "admin") {
212
+ return hidden();
213
+ }
214
+ return pass();
215
+ };
216
+
217
+ // Use in multiple resources
218
+ defineResource({
219
+ path: "admin/users",
220
+ component: AdminUsersPage,
221
+ guards: [requireAuth, requireAdmin],
222
+ });
223
+ ```
224
+
225
+ When a module or resource is hidden via guards, it will:
226
+
227
+ - Not appear in navigation menus
228
+ - Not be accessible via direct URL navigation
229
+ - Not appear in CommandPalette search results