@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,445 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: WithGuard
|
|
3
|
+
description: Conditionally render UI elements based on guard evaluation with support for async permissions checks
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/with-guard.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# WithGuard
|
|
9
|
+
|
|
10
|
+
`WithGuard` is a component for conditionally rendering UI elements based on guard evaluation. It uses the same guard system as route-level guards, making permission logic reusable throughout your application.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
|
|
22
|
+
|
|
23
|
+
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());
|
|
24
|
+
|
|
25
|
+
function AdminPanel() {
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<h1>Dashboard</h1>
|
|
29
|
+
|
|
30
|
+
<WithGuard guards={[isAdmin]}>
|
|
31
|
+
<Button>Delete All Users</Button>
|
|
32
|
+
</WithGuard>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If the user is not an admin, the button won't render.
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
_Generated from the type surface — first-party props only._
|
|
43
|
+
|
|
44
|
+
### WithGuardProps
|
|
45
|
+
|
|
46
|
+
| Prop | Type | Default | Description |
|
|
47
|
+
| ----------- | ----------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| `children` | `ReactNode` | - | Content to render when all guards pass. |
|
|
49
|
+
| `fallback?` | `ReactNode` | `null (renders nothing)` | Content to render when any guard returns hidden. |
|
|
50
|
+
| `guards` | `Guard[]` | - | Guards to evaluate. All guards must pass for children to render. Uses the same Guard type as route guards for full reusability. |
|
|
51
|
+
| `loading?` | `ReactNode` | `null (renders nothing)` | Content to render while guards are being evaluated (for async guards). |
|
|
52
|
+
|
|
53
|
+
## Exports
|
|
54
|
+
|
|
55
|
+
- `WithGuard` — const
|
|
56
|
+
- `WithGuardProps` — type
|
|
57
|
+
|
|
58
|
+
## Guard Functions
|
|
59
|
+
|
|
60
|
+
WithGuard uses the same `Guard` type as route guards:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
type Guard = (ctx: GuardContext) => GuardResult | Promise<GuardResult>;
|
|
64
|
+
|
|
65
|
+
type GuardResult =
|
|
66
|
+
| { type: "pass" } // Allow rendering
|
|
67
|
+
| { type: "hidden" }; // Hide content (render fallback)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note:** Unlike route guards, `redirectTo()` is **not supported** in WithGuard. Use `hidden()` with a fallback that handles navigation if needed.
|
|
71
|
+
|
|
72
|
+
### Creating Guards
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { type Guard, pass, hidden } from "@tailor-platform/app-shell";
|
|
76
|
+
|
|
77
|
+
// Simple guard
|
|
78
|
+
const requireAuth: Guard = ({ context }) => {
|
|
79
|
+
return context.currentUser ? pass() : hidden();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Async guard with API call
|
|
83
|
+
const hasPermission: Guard = async ({ context }) => {
|
|
84
|
+
const canAccess = await fetch("/api/permissions/admin")
|
|
85
|
+
.then((r) => r.json())
|
|
86
|
+
.then((data) => data.allowed);
|
|
87
|
+
|
|
88
|
+
return canAccess ? pass() : hidden();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Parameterized guard (curried function)
|
|
92
|
+
const isOwner =
|
|
93
|
+
(resourceId: string): Guard =>
|
|
94
|
+
({ context }) => {
|
|
95
|
+
return context.currentUser?.id === resourceId ? pass() : hidden();
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Examples
|
|
100
|
+
|
|
101
|
+
### Simple Role Check
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());
|
|
105
|
+
|
|
106
|
+
<WithGuard guards={[isAdmin]}>
|
|
107
|
+
<Button>Admin Settings</Button>
|
|
108
|
+
</WithGuard>;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### With Fallback
|
|
112
|
+
|
|
113
|
+
Show alternative content when guard fails:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<WithGuard guards={[isAdmin]} fallback={<p>You need admin access to view this.</p>}>
|
|
117
|
+
<AdminPanel />
|
|
118
|
+
</WithGuard>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### With Loading State
|
|
122
|
+
|
|
123
|
+
Show loading spinner while checking permissions:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const checkPermission: Guard = async ({ context }) => {
|
|
127
|
+
const allowed = await api.checkPermission("admin");
|
|
128
|
+
return allowed ? pass() : hidden();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
<WithGuard guards={[checkPermission]} loading={<Spinner />} fallback={<UpgradePrompt />}>
|
|
132
|
+
<PremiumFeature />
|
|
133
|
+
</WithGuard>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Multiple Guards
|
|
137
|
+
|
|
138
|
+
All guards must pass for content to render:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());
|
|
142
|
+
|
|
143
|
+
const hasFeatureFlag = ({ context }) => (context.featureFlags?.betaFeatures ? pass() : hidden());
|
|
144
|
+
|
|
145
|
+
<WithGuard guards={[isAdmin, hasFeatureFlag]}>
|
|
146
|
+
<BetaAdminFeature />
|
|
147
|
+
</WithGuard>;
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Parameterized Guards
|
|
151
|
+
|
|
152
|
+
Use curried functions to create reusable parameterized guards:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
const isOwner =
|
|
156
|
+
(resourceId: string): Guard =>
|
|
157
|
+
({ context }) => {
|
|
158
|
+
return context.currentUser?.id === resourceId ? pass() : hidden();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function ResourceActions({ resourceId }) {
|
|
162
|
+
return (
|
|
163
|
+
<WithGuard guards={[isOwner(resourceId)]}>
|
|
164
|
+
<Button>Edit</Button>
|
|
165
|
+
<Button>Delete</Button>
|
|
166
|
+
</WithGuard>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### In Sidebar Navigation
|
|
172
|
+
|
|
173
|
+
Conditionally show navigation items:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { DefaultSidebar, SidebarItem, WithGuard } from "@tailor-platform/app-shell";
|
|
177
|
+
|
|
178
|
+
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());
|
|
179
|
+
|
|
180
|
+
<DefaultSidebar>
|
|
181
|
+
<SidebarItem to="/dashboard" />
|
|
182
|
+
<SidebarItem to="/products" />
|
|
183
|
+
|
|
184
|
+
<WithGuard guards={[isAdmin]}>
|
|
185
|
+
<SidebarItem to="/admin" />
|
|
186
|
+
<SidebarItem to="/settings" />
|
|
187
|
+
</WithGuard>
|
|
188
|
+
</DefaultSidebar>;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Feature Flags
|
|
192
|
+
|
|
193
|
+
Toggle features based on flags:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
const hasBetaFeature = ({ context }) => (context.featureFlags?.newDashboard ? pass() : hidden());
|
|
197
|
+
|
|
198
|
+
<WithGuard guards={[hasBetaFeature]} fallback={<OldDashboard />}>
|
|
199
|
+
<NewDashboard />
|
|
200
|
+
</WithGuard>;
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Subscription Tiers
|
|
204
|
+
|
|
205
|
+
Show features based on user's subscription:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
const isPremium = ({ context }) =>
|
|
209
|
+
context.currentUser?.subscription === "premium" ? pass() : hidden();
|
|
210
|
+
|
|
211
|
+
<WithGuard
|
|
212
|
+
guards={[isPremium]}
|
|
213
|
+
fallback={
|
|
214
|
+
<Card>
|
|
215
|
+
<h3>Premium Feature</h3>
|
|
216
|
+
<p>Upgrade to access advanced analytics.</p>
|
|
217
|
+
<Button>Upgrade Now</Button>
|
|
218
|
+
</Card>
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
<AdvancedAnalytics />
|
|
222
|
+
</WithGuard>;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Dynamic Permissions
|
|
226
|
+
|
|
227
|
+
Check permissions from API:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
const canDeleteOrder =
|
|
231
|
+
(orderId: string): Guard =>
|
|
232
|
+
async ({ context }) => {
|
|
233
|
+
const response = await fetch(`/api/orders/${orderId}/permissions`);
|
|
234
|
+
const { canDelete } = await response.json();
|
|
235
|
+
return canDelete ? pass() : hidden();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
function OrderActions({ orderId }) {
|
|
239
|
+
return (
|
|
240
|
+
<div>
|
|
241
|
+
<Button>View</Button>
|
|
242
|
+
<Button>Edit</Button>
|
|
243
|
+
<WithGuard guards={[canDeleteOrder(orderId)]} loading={<Spinner size="sm" />}>
|
|
244
|
+
<Button variant="destructive">Delete</Button>
|
|
245
|
+
</WithGuard>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Combining with Route Guards
|
|
252
|
+
|
|
253
|
+
Reuse the same guards for routes and UI:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// guards.ts
|
|
257
|
+
export const requireAuth: Guard = ({ context }) => {
|
|
258
|
+
return context.currentUser ? pass() : hidden();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const isAdmin: Guard = ({ context }) => {
|
|
262
|
+
return context.currentUser?.role === "admin" ? pass() : hidden();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// routes.tsx
|
|
266
|
+
const adminModule = defineModule({
|
|
267
|
+
path: "admin",
|
|
268
|
+
component: AdminPage,
|
|
269
|
+
guards: [requireAuth, isAdmin], // Route-level
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// component.tsx
|
|
273
|
+
<WithGuard guards={[requireAuth, isAdmin]}>
|
|
274
|
+
{" "}
|
|
275
|
+
{/* UI-level */}
|
|
276
|
+
<AdminSettings />
|
|
277
|
+
</WithGuard>;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Guard Evaluation
|
|
281
|
+
|
|
282
|
+
Guards are evaluated in order and stop at the first non-pass result:
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const guards = [guardA, guardB, guardC];
|
|
286
|
+
|
|
287
|
+
// If guardA returns hidden(), guardB and guardC are NOT evaluated
|
|
288
|
+
<WithGuard guards={guards}>
|
|
289
|
+
<Content />
|
|
290
|
+
</WithGuard>;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Performance
|
|
294
|
+
|
|
295
|
+
### Caching
|
|
296
|
+
|
|
297
|
+
WithGuard caches guard results based on `contextData` reference:
|
|
298
|
+
|
|
299
|
+
- Guards re-evaluate when `contextData` changes
|
|
300
|
+
- Guards do NOT re-evaluate when props change (except `guards` array)
|
|
301
|
+
|
|
302
|
+
### Suspense Integration
|
|
303
|
+
|
|
304
|
+
Async guards use React Suspense:
|
|
305
|
+
|
|
306
|
+
- Shows `loading` prop while evaluating
|
|
307
|
+
- Renders `children` or `fallback` when complete
|
|
308
|
+
- Automatically suspends during evaluation
|
|
309
|
+
|
|
310
|
+
## Context Data
|
|
311
|
+
|
|
312
|
+
Guards receive the same context as route guards:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Define context type
|
|
316
|
+
declare module "@tailor-platform/app-shell" {
|
|
317
|
+
interface AppShellRegister {
|
|
318
|
+
contextData: {
|
|
319
|
+
currentUser: User | null;
|
|
320
|
+
featureFlags: FeatureFlags;
|
|
321
|
+
permissions: string[];
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Use in guards
|
|
327
|
+
const hasPermission =
|
|
328
|
+
(perm: string): Guard =>
|
|
329
|
+
({ context }) => {
|
|
330
|
+
return context.permissions.includes(perm) ? pass() : hidden();
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Comparison: WithGuard vs Route Guards
|
|
335
|
+
|
|
336
|
+
| Feature | WithGuard | Route Guards |
|
|
337
|
+
| --------------------------- | ------------------------ | --------------------------- |
|
|
338
|
+
| **Purpose** | Conditional UI rendering | Route access control |
|
|
339
|
+
| **Location** | Inside components | Module/resource definitions |
|
|
340
|
+
| **Supports `pass()`** | ✅ Yes | ✅ Yes |
|
|
341
|
+
| **Supports `hidden()`** | ✅ Yes | ✅ Yes |
|
|
342
|
+
| **Supports `redirectTo()`** | ❌ No | ✅ Yes |
|
|
343
|
+
| **Async guards** | ✅ Yes | ✅ Yes |
|
|
344
|
+
| **Reusable** | ✅ Yes | ✅ Yes |
|
|
345
|
+
|
|
346
|
+
## Best Practices
|
|
347
|
+
|
|
348
|
+
### Do:
|
|
349
|
+
|
|
350
|
+
- ✅ Reuse guards between routes and UI
|
|
351
|
+
- ✅ Use descriptive guard names (`isAdmin`, `canEdit`)
|
|
352
|
+
- ✅ Provide fallback for better UX
|
|
353
|
+
- ✅ Show loading state for async guards
|
|
354
|
+
- ✅ Cache expensive permission checks
|
|
355
|
+
|
|
356
|
+
### Don't:
|
|
357
|
+
|
|
358
|
+
- ❌ Use `redirectTo()` in WithGuard (not supported)
|
|
359
|
+
- ❌ Make guards too complex (extract to functions)
|
|
360
|
+
- ❌ Forget to handle loading states
|
|
361
|
+
- ❌ Skip fallback when hiding important features
|
|
362
|
+
- ❌ Duplicate permission logic (reuse guards)
|
|
363
|
+
|
|
364
|
+
## Accessibility
|
|
365
|
+
|
|
366
|
+
- Content is fully removed from DOM when hidden (not just visually)
|
|
367
|
+
- Screen readers won't announce hidden content
|
|
368
|
+
- Loading states should be announced to screen readers
|
|
369
|
+
- Fallback content should be accessible
|
|
370
|
+
|
|
371
|
+
## TypeScript
|
|
372
|
+
|
|
373
|
+
Full type safety with TypeScript:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { type Guard, type WithGuardProps } from "@tailor-platform/app-shell";
|
|
377
|
+
|
|
378
|
+
// Type-safe guard
|
|
379
|
+
const isAdmin: Guard = ({ context }) => {
|
|
380
|
+
// context is fully typed
|
|
381
|
+
return context.currentUser?.role === "admin" ? pass() : hidden();
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Type-safe props
|
|
385
|
+
const guardProps: WithGuardProps = {
|
|
386
|
+
guards: [isAdmin],
|
|
387
|
+
children: <AdminPanel />,
|
|
388
|
+
fallback: <AccessDenied />,
|
|
389
|
+
};
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Troubleshooting
|
|
393
|
+
|
|
394
|
+
### Guard not re-evaluating
|
|
395
|
+
|
|
396
|
+
**Problem:** Guard doesn't update when data changes
|
|
397
|
+
|
|
398
|
+
**Solution:** Ensure `contextData` reference changes:
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
// ❌ Bad - mutating object
|
|
402
|
+
context.currentUser.role = "admin";
|
|
403
|
+
|
|
404
|
+
// ✅ Good - new reference
|
|
405
|
+
setContextData({
|
|
406
|
+
...contextData,
|
|
407
|
+
currentUser: { ...currentUser, role: "admin" },
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Infinite loading
|
|
412
|
+
|
|
413
|
+
**Problem:** Loading state never resolves
|
|
414
|
+
|
|
415
|
+
**Solution:** Ensure async guards return a result:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
// ❌ Bad - no return
|
|
419
|
+
const checkPermission: Guard = async ({ context }) => {
|
|
420
|
+
await fetch("/api/check");
|
|
421
|
+
// Missing return!
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// ✅ Good - returns result
|
|
425
|
+
const checkPermission: Guard = async ({ context }) => {
|
|
426
|
+
const allowed = await fetch("/api/check").then((r) => r.json());
|
|
427
|
+
return allowed ? pass() : hidden();
|
|
428
|
+
};
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Related Components
|
|
432
|
+
|
|
433
|
+
- [AppShell](./app-shell.md) - Context provider for guards
|
|
434
|
+
- [SidebarItem](./sidebar-item.md) - Use WithGuard in navigation
|
|
435
|
+
|
|
436
|
+
## Related Concepts
|
|
437
|
+
|
|
438
|
+
- [Guards and Permissions](../api/guards/overview.md) - Comprehensive guard guide
|
|
439
|
+
- [Modules and Resources](../concepts/modules-and-resources.md) - Route-level guards
|
|
440
|
+
|
|
441
|
+
## API Reference
|
|
442
|
+
|
|
443
|
+
- [pass()](../api/guards/pass.md) - Allow access guard
|
|
444
|
+
- [hidden()](../api/guards/hidden.md) - Hide content guard
|
|
445
|
+
- [Guards Overview](../api/guards/overview.md) - Complete guard reference
|