@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.
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +811 -131
- package/dist/app-shell.js +7155 -5797
- package/dist/{appshell-DLVBFo5J.js → appshell-CahhZy7v.js} +656 -524
- package/dist/semantic-contrast.test.ts +154 -0
- package/dist/style-entry.d.ts +147 -92
- package/dist/testing.d.ts +152 -95
- package/dist/testing.js +1 -1
- package/dist/theme-bridge.test.ts +101 -1
- package/dist/theme.bridge.css +91 -0
- package/dist/theme.css +6 -1
- package/dist/themes/_template.css +7 -1
- package/dist/themes/bloom.css +0 -6
- package/dist/themes/cream.css +0 -6
- package/dist/themes/default.css +389 -69
- package/dist/vite-plugin.d.ts +147 -92
- package/package.json +8 -8
- package/skills/app-shell-patterns/SKILL.md +108 -19
- package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
- package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
- package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
- package/skills/app-shell-patterns/references/api/define-module.md +247 -0
- package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
- package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
- package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
- package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
- package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
- package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
- package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
- package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
- package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +132 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +150 -0
- package/skills/app-shell-patterns/references/api/use-app-shell.md +150 -0
- package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
- package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
- package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
- package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
- package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
- package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
- package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
- package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
- package/skills/app-shell-patterns/references/api/use-toast.md +104 -0
- package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
- package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
- package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
- package/skills/app-shell-patterns/references/components/alert.md +202 -0
- package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
- package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
- package/skills/app-shell-patterns/references/components/attachment.md +138 -0
- package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
- package/skills/app-shell-patterns/references/components/avatar.md +80 -0
- package/skills/app-shell-patterns/references/components/badge.md +309 -0
- package/skills/app-shell-patterns/references/components/button.md +164 -0
- package/skills/app-shell-patterns/references/components/card.md +181 -0
- package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
- package/skills/app-shell-patterns/references/components/combobox.md +318 -0
- package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
- package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
- package/skills/app-shell-patterns/references/components/data-table.md +1168 -0
- package/skills/app-shell-patterns/references/components/date-picker.md +259 -0
- package/skills/app-shell-patterns/references/components/default-header.md +75 -0
- package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
- package/skills/app-shell-patterns/references/components/description-card.md +569 -0
- package/skills/app-shell-patterns/references/components/dialog.md +164 -0
- package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
- package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
- package/skills/app-shell-patterns/references/components/form.md +378 -0
- package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
- package/skills/app-shell-patterns/references/components/grid.md +184 -0
- package/skills/app-shell-patterns/references/components/input.md +83 -0
- package/skills/app-shell-patterns/references/components/layout.md +539 -0
- package/skills/app-shell-patterns/references/components/menu.md +218 -0
- package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
- package/skills/app-shell-patterns/references/components/select.md +283 -0
- package/skills/app-shell-patterns/references/components/sheet.md +237 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +472 -0
- package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
- package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
- package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
- package/skills/app-shell-patterns/references/components/spinner.md +81 -0
- package/skills/app-shell-patterns/references/components/table.md +188 -0
- package/skills/app-shell-patterns/references/components/tabs.md +158 -0
- package/skills/app-shell-patterns/references/components/textarea.md +122 -0
- package/skills/app-shell-patterns/references/components/timeline.md +317 -0
- package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
- package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
- package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
- package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
- package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
- package/skills/app-shell-patterns/references/concepts/file-based-routing.md +356 -0
- package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +31 -18
- package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +425 -0
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +420 -0
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +785 -0
- package/skills/app-shell-patterns/references/migrations.md +55 -0
- package/skills/app-shell-patterns/references/pages/{detail.md → document-detail.md} +10 -33
- package/skills/app-shell-patterns/references/patterns/form-composer.md +43 -45
- package/skills/app-shell-patterns/references/patterns/form-modal.md +37 -92
- package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
- package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
- package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +136 -139
- package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +13 -88
- package/skills/app-shell-patterns/references/fundamental/components.md +0 -1009
- 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`
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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`
|
|
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
|