@tailor-platform/app-shell 1.14.0 → 1.16.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 (101) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/app-shell-core.css +1 -1
  3. package/dist/app-shell.d.ts +1203 -316
  4. package/dist/app-shell.js +4562 -3307
  5. package/dist/{appshell-CtS8Y3LW.js → appshell-B0wh5Lq0.js} +850 -549
  6. package/dist/style-entry.d.ts +118 -98
  7. package/dist/testing.d.ts +118 -98
  8. package/dist/testing.js +1 -1
  9. package/dist/themes/_template.css +1 -1
  10. package/dist/vite-plugin.d.ts +118 -98
  11. package/package.json +13 -13
  12. package/skills/app-shell-patterns/SKILL.md +108 -24
  13. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
  14. package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
  15. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
  16. package/skills/app-shell-patterns/references/api/define-module.md +247 -0
  17. package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
  18. package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
  19. package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
  20. package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
  21. package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
  22. package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
  23. package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
  24. package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
  25. package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
  26. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +126 -0
  27. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +146 -0
  28. package/skills/app-shell-patterns/references/api/use-app-shell.md +143 -0
  29. package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
  30. package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
  31. package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
  32. package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
  33. package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
  34. package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
  35. package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
  36. package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
  37. package/skills/app-shell-patterns/references/api/use-toast.md +107 -0
  38. package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
  39. package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
  40. package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
  41. package/skills/app-shell-patterns/references/components/alert.md +202 -0
  42. package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
  43. package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
  44. package/skills/app-shell-patterns/references/components/attachment.md +138 -0
  45. package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
  46. package/skills/app-shell-patterns/references/components/avatar.md +80 -0
  47. package/skills/app-shell-patterns/references/components/badge.md +309 -0
  48. package/skills/app-shell-patterns/references/components/button.md +164 -0
  49. package/skills/app-shell-patterns/references/components/card.md +109 -0
  50. package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
  51. package/skills/app-shell-patterns/references/components/combobox.md +318 -0
  52. package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
  53. package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
  54. package/skills/app-shell-patterns/references/components/data-table.md +953 -0
  55. package/skills/app-shell-patterns/references/components/date-picker.md +248 -0
  56. package/skills/app-shell-patterns/references/components/default-header.md +75 -0
  57. package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
  58. package/skills/app-shell-patterns/references/components/description-card.md +569 -0
  59. package/skills/app-shell-patterns/references/components/dialog.md +164 -0
  60. package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
  61. package/skills/app-shell-patterns/references/components/form.md +378 -0
  62. package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
  63. package/skills/app-shell-patterns/references/components/grid.md +184 -0
  64. package/skills/app-shell-patterns/references/components/input.md +83 -0
  65. package/skills/app-shell-patterns/references/components/layout.md +539 -0
  66. package/skills/app-shell-patterns/references/components/menu.md +218 -0
  67. package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
  68. package/skills/app-shell-patterns/references/components/select.md +283 -0
  69. package/skills/app-shell-patterns/references/components/sheet.md +237 -0
  70. package/skills/app-shell-patterns/references/components/sidebar-group.md +466 -0
  71. package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
  72. package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
  73. package/skills/app-shell-patterns/references/components/spinner.md +81 -0
  74. package/skills/app-shell-patterns/references/components/table.md +188 -0
  75. package/skills/app-shell-patterns/references/components/tabs.md +158 -0
  76. package/skills/app-shell-patterns/references/components/textarea.md +122 -0
  77. package/skills/app-shell-patterns/references/components/timeline.md +317 -0
  78. package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
  79. package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
  80. package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
  81. package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
  82. package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
  83. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +350 -0
  84. package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +11 -4
  85. package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
  86. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +387 -0
  87. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +388 -0
  88. package/skills/app-shell-patterns/references/{fundamental/design-system.md → concepts/styling-theming.md} +260 -105
  89. package/skills/app-shell-patterns/references/migrations.md +34 -0
  90. package/skills/app-shell-patterns/references/pages/document-detail.md +1125 -0
  91. package/skills/app-shell-patterns/references/patterns/form-composer.md +44 -46
  92. package/skills/app-shell-patterns/references/patterns/form-modal.md +39 -100
  93. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
  94. package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
  95. package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
  96. package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
  97. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +14 -28
  98. package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
  99. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +11 -86
  100. package/skills/app-shell-patterns/references/fundamental/components.md +0 -981
  101. package/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +0 -201
@@ -0,0 +1,350 @@
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) => string);
147
+ };
148
+ guards?: Guard[];
149
+ };
150
+ ```
151
+
152
+ ## Path Conventions
153
+
154
+ | Directory Name | Converts To | Description |
155
+ | -------------- | ----------- | ---------------------------------------------- |
156
+ | `orders` | `orders` | Static segment |
157
+ | `[id]` | `:id` | Dynamic parameter |
158
+ | `[...slug]` | `*slug` | Catch-all — matches zero or more path segments |
159
+ | `(group)` | (excluded) | Grouping only (not in path) |
160
+ | `_lib` | (ignored) | Not routed (for shared logic) |
161
+
162
+ ### Examples
163
+
164
+ ```
165
+ src/pages/
166
+ ├── users/
167
+ │ ├── page.tsx # /users
168
+ │ └── [userId]/
169
+ │ └── page.tsx # /users/:userId
170
+ ├── (marketing)/
171
+ │ ├── campaigns/
172
+ │ │ └── page.tsx # /campaigns (not /marketing/campaigns)
173
+ │ └── analytics/
174
+ │ └── page.tsx # /analytics
175
+ └── _utils/
176
+ └── helpers.ts # Not routed (shared utilities)
177
+ ```
178
+
179
+ ## Guards
180
+
181
+ Guards are **not** automatically inherited from parent pages. Each page must explicitly define its own guards:
182
+
183
+ ```tsx
184
+ // /dashboard/page.tsx
185
+ DashboardPage.appShellPageProps = {
186
+ guards: [authGuard],
187
+ } satisfies AppShellPageProps;
188
+
189
+ // /dashboard/admin/page.tsx — must include authGuard explicitly
190
+ AdminPage.appShellPageProps = {
191
+ guards: [authGuard, adminGuard],
192
+ } satisfies AppShellPageProps;
193
+ ```
194
+
195
+ To share common guards across pages, compose them from a shared module:
196
+
197
+ ```tsx
198
+ // src/guards.ts
199
+ export const requireAuth = [authGuard];
200
+ export const requireAdmin = [authGuard, adminGuard];
201
+
202
+ // src/pages/dashboard/orders/page.tsx
203
+ import { requireAuth } from "@/guards";
204
+
205
+ OrdersPage.appShellPageProps = {
206
+ guards: [...requireAuth],
207
+ } satisfies AppShellPageProps;
208
+ ```
209
+
210
+ ## Typed Routes
211
+
212
+ Enable `generateTypedRoutes` in the Vite plugin to generate type-safe route helpers:
213
+
214
+ ```typescript
215
+ // vite.config.ts
216
+ appShellRoutes({
217
+ generateTypedRoutes: true,
218
+ });
219
+ ```
220
+
221
+ This generates `src/routes.generated.ts` with a `paths` helper:
222
+
223
+ ```tsx
224
+ import { paths } from './routes.generated';
225
+ import { Link } from '@tailor-platform/app-shell';
226
+
227
+ // Static routes
228
+ <Link to={paths.for("/dashboard")}>Dashboard</Link>
229
+
230
+ // Dynamic routes - params are type-checked
231
+ <Link to={paths.for("/dashboard/orders/:id", { id: orderId })}>Order</Link>
232
+
233
+ // TypeScript catches errors:
234
+ paths.for("/dashboard/orders/:id"); // Error: missing 'id'
235
+ paths.for("/invalid"); // Error: route doesn't exist
236
+ ```
237
+
238
+ ## Comparison with Legacy API
239
+
240
+ ### Before: Explicit Hierarchy Assembly
241
+
242
+ ```tsx
243
+ const orderDetailResource = defineResource({ path: ":id", component: OrderDetail });
244
+ const ordersResource = defineResource({
245
+ path: "orders",
246
+ component: OrdersList,
247
+ subResources: [orderDetailResource],
248
+ });
249
+ const purchasingModule = defineModule({
250
+ path: "purchasing",
251
+ resources: [ordersResource],
252
+ });
253
+
254
+ <AppShell modules={[purchasingModule]} />;
255
+ ```
256
+
257
+ ### After: File-Based Pages
258
+
259
+ ```tsx
260
+ // src/pages/purchasing/orders/[id]/page.tsx
261
+ const OrderDetailPage = () => <div>Order Detail</div>;
262
+ OrderDetailPage.appShellPageProps = {
263
+ meta: { title: "Order Detail" },
264
+ } satisfies AppShellPageProps;
265
+ export default OrderDetailPage;
266
+ ```
267
+
268
+ ```tsx
269
+ // App.tsx - No configuration needed
270
+ <AppShell title="My App">
271
+ <SidebarLayout sidebar={<DefaultSidebar />} />
272
+ </AppShell>
273
+ ```
274
+
275
+ ### Concept Mapping
276
+
277
+ | Legacy API | File-Based | Notes |
278
+ | ----------------------- | ------------------------------- | ----------------------------- |
279
+ | `Module` | — | First-level directory |
280
+ | `Resource` | — | Directory structure |
281
+ | `defineModule()` | — | Not needed |
282
+ | `defineResource()` | — | Not needed |
283
+ | `path` property | Directory name | Auto-derived |
284
+ | `component` property | `page.tsx` default export | File convention |
285
+ | `meta` property | `Page.appShellPageProps.meta` | Static field |
286
+ | `guards` property | `Page.appShellPageProps.guards` | Static field (no inheritance) |
287
+ | `subResources` property | Subdirectories | Auto-derived |
288
+
289
+ ## Compatibility
290
+
291
+ File-based pages and explicit `modules` prop are **mutually exclusive**.
292
+
293
+ ### Usage Patterns
294
+
295
+ ```tsx
296
+ // ✅ Pattern 1: File-based pages with plugin (recommended)
297
+ // vite.config.ts has plugin configured
298
+ <AppShell title="My App">
299
+ <SidebarLayout sidebar={<DefaultSidebar />} />
300
+ </AppShell>
301
+
302
+ // ✅ Pattern 2: Explicit modules without plugin (legacy)
303
+ <AppShell modules={[myModule]} title="My App">
304
+ <SidebarLayout sidebar={<DefaultSidebar />} />
305
+ </AppShell>
306
+
307
+ // ✅ Pattern 3: Plugin enabled + modules prop (modules takes precedence)
308
+ // Even with plugin enabled, modules prop is used when provided
309
+ <AppShell modules={[myModule]} title="My App">
310
+ <SidebarLayout sidebar={<DefaultSidebar />} />
311
+ </AppShell>
312
+
313
+ // ❌ Pattern 4: No plugin + no modules (runtime error)
314
+ <AppShell title="My App">
315
+ {/* → Runtime error: No routes configured */}
316
+ </AppShell>
317
+ ```
318
+
319
+ ## Migration Guide
320
+
321
+ To migrate from `defineModule`/`defineResource` to file-based routing:
322
+
323
+ 1. **Add Vite Plugin**
324
+
325
+ ```typescript
326
+ // vite.config.ts
327
+ import { appShellRoutes } from "@tailor-platform/app-shell/vite-plugin";
328
+
329
+ export default defineConfig({
330
+ plugins: [react(), appShellRoutes({ entrypoint: "src/App.tsx" })],
331
+ });
332
+ ```
333
+
334
+ 2. **Create pages directory structure**
335
+ - Map each module to a top-level directory
336
+ - Map each resource to a subdirectory with `page.tsx`
337
+ - Use `[param]` for dynamic segments
338
+
339
+ 3. **Move component and metadata**
340
+
341
+ ```tsx
342
+ // Before: defineResource({ path: "orders", component: Orders, meta: {...} })
343
+
344
+ // After: src/pages/orders/page.tsx
345
+ const OrdersPage = () => <Orders />;
346
+ OrdersPage.appShellPageProps = { meta: {...} };
347
+ export default OrdersPage;
348
+ ```
349
+
350
+ 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
 
@@ -191,7 +198,7 @@ Every page component:
191
198
  };
192
199
  ```
193
200
 
194
- 3. Uses `appShellPageProps.guards` for permission gates. See [project-setup.md](project-setup.md).
201
+ 3. Uses `appShellPageProps.guards` for permission gates.
195
202
 
196
203
  ## Quick reference
197
204
 
@@ -202,4 +209,4 @@ Every page component:
202
209
  | Why isn't my list refreshing after a mutation? | Add `additionalTypenames: ['<Type>']` on both query and mutation |
203
210
  | Connection vs array? | Always connection (`edges { node }`) for built-in `<entities>` queries; check the schema for custom resolvers |
204
211
  | 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` |
212
+ | 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