@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.
- package/LICENSE.md +21 -0
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +1203 -316
- package/dist/app-shell.js +4562 -3307
- package/dist/{appshell-CtS8Y3LW.js → appshell-B0wh5Lq0.js} +850 -549
- package/dist/style-entry.d.ts +118 -98
- package/dist/testing.d.ts +118 -98
- package/dist/testing.js +1 -1
- package/dist/themes/_template.css +1 -1
- package/dist/vite-plugin.d.ts +118 -98
- package/package.json +13 -13
- package/skills/app-shell-patterns/SKILL.md +108 -24
- 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 +126 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +146 -0
- package/skills/app-shell-patterns/references/api/use-app-shell.md +143 -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 +107 -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 +109 -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 +953 -0
- package/skills/app-shell-patterns/references/components/date-picker.md +248 -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/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 +466 -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/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 +350 -0
- package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +11 -4
- package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +387 -0
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +388 -0
- package/skills/app-shell-patterns/references/{fundamental/design-system.md → concepts/styling-theming.md} +260 -105
- package/skills/app-shell-patterns/references/migrations.md +34 -0
- package/skills/app-shell-patterns/references/pages/document-detail.md +1125 -0
- package/skills/app-shell-patterns/references/patterns/form-composer.md +44 -46
- package/skills/app-shell-patterns/references/patterns/form-modal.md +39 -100
- 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 +14 -28
- package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +11 -86
- package/skills/app-shell-patterns/references/fundamental/components.md +0 -981
- package/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +0 -201
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tooltip
|
|
3
|
+
description: Hover/focus tooltip with configurable placement and shared delay via Tooltip.Provider
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/tooltip.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Tooltip
|
|
9
|
+
|
|
10
|
+
The `Tooltip` component displays contextual information on hover or focus. It is backed by Base UI's Tooltip primitive.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Tooltip } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
<example-preview name="basic-usage"></example-preview>
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
function BasicUsage() {
|
|
24
|
+
return (
|
|
25
|
+
<Tooltip.Root>
|
|
26
|
+
<Tooltip.Trigger render={<Button variant="outline" />}>Hover me</Tooltip.Trigger>
|
|
27
|
+
<Tooltip.Content>Helpful information</Tooltip.Content>
|
|
28
|
+
</Tooltip.Root>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Sub-components
|
|
34
|
+
|
|
35
|
+
| Sub-component | Description |
|
|
36
|
+
| ------------------ | ----------------------------------------------------------- |
|
|
37
|
+
| `Tooltip.Provider` | Provides shared delay configuration for all nested tooltips |
|
|
38
|
+
| `Tooltip.Root` | Manages tooltip open/close state |
|
|
39
|
+
| `Tooltip.Trigger` | Element that shows the tooltip on hover or focus |
|
|
40
|
+
| `Tooltip.Content` | The tooltip popup containing the help text |
|
|
41
|
+
|
|
42
|
+
## Props
|
|
43
|
+
|
|
44
|
+
### Tooltip.Provider Props
|
|
45
|
+
|
|
46
|
+
Wrap a section of your UI (e.g. a toolbar) with `Tooltip.Provider` to share delay settings across multiple tooltips.
|
|
47
|
+
|
|
48
|
+
| Prop | Type | Default | Description |
|
|
49
|
+
| ------------ | ----------------- | ------- | --------------------------------------------------- |
|
|
50
|
+
| `delay` | `number` | `0` | Delay in ms before the tooltip opens (milliseconds) |
|
|
51
|
+
| `closeDelay` | `number` | - | Delay in ms before the tooltip closes |
|
|
52
|
+
| `timeout` | `number` | - | Timeout in ms after which the tooltip auto-closes |
|
|
53
|
+
| `children` | `React.ReactNode` | - | Nested tooltip components |
|
|
54
|
+
|
|
55
|
+
### Tooltip.Root Props
|
|
56
|
+
|
|
57
|
+
| Prop | Type | Default | Description |
|
|
58
|
+
| -------------- | ------------------------- | ------- | --------------------------------- |
|
|
59
|
+
| `open` | `boolean` | - | Controlled open state |
|
|
60
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
|
|
61
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
|
|
62
|
+
| `children` | `React.ReactNode` | - | Tooltip sub-components |
|
|
63
|
+
|
|
64
|
+
### Tooltip.Content Props
|
|
65
|
+
|
|
66
|
+
| Prop | Type | Default | Description |
|
|
67
|
+
| ------------ | ---------------------------------------- | ---------- | -------------------------------------------------------- |
|
|
68
|
+
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"top"` | Preferred placement of the tooltip |
|
|
69
|
+
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the placement axis |
|
|
70
|
+
| `sideOffset` | `number` | `5` | Distance in px from the trigger |
|
|
71
|
+
| `noArrow` | `boolean` | `false` | When `true`, suppresses the arrow indicator on the popup |
|
|
72
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
73
|
+
| `children` | `React.ReactNode` | - | Tooltip text or content |
|
|
74
|
+
|
|
75
|
+
## Placement
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Tooltip.Root>
|
|
79
|
+
<Tooltip.Trigger render={<Button />}>Trigger</Tooltip.Trigger>
|
|
80
|
+
<Tooltip.Content side="right" align="start">
|
|
81
|
+
Appears on the right
|
|
82
|
+
</Tooltip.Content>
|
|
83
|
+
</Tooltip.Root>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Shared Delay with Provider
|
|
87
|
+
|
|
88
|
+
Use `Tooltip.Provider` to avoid tooltips opening immediately in dense UIs like toolbars:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<Tooltip.Provider delay={300}>
|
|
92
|
+
<Toolbar>
|
|
93
|
+
<Tooltip.Root>
|
|
94
|
+
<Tooltip.Trigger render={<Button size="icon" />}>
|
|
95
|
+
<BoldIcon />
|
|
96
|
+
</Tooltip.Trigger>
|
|
97
|
+
<Tooltip.Content>Bold</Tooltip.Content>
|
|
98
|
+
</Tooltip.Root>
|
|
99
|
+
<Tooltip.Root>
|
|
100
|
+
<Tooltip.Trigger render={<Button size="icon" />}>
|
|
101
|
+
<ItalicIcon />
|
|
102
|
+
</Tooltip.Trigger>
|
|
103
|
+
<Tooltip.Content>Italic</Tooltip.Content>
|
|
104
|
+
</Tooltip.Root>
|
|
105
|
+
</Toolbar>
|
|
106
|
+
</Tooltip.Provider>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### Icon Button with Tooltip
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { PencilIcon } from "lucide-react";
|
|
115
|
+
|
|
116
|
+
<Tooltip.Root>
|
|
117
|
+
<Tooltip.Trigger render={<Button size="icon" variant="ghost" />}>
|
|
118
|
+
<PencilIcon />
|
|
119
|
+
<span className="sr-only">Edit</span>
|
|
120
|
+
</Tooltip.Trigger>
|
|
121
|
+
<Tooltip.Content>Edit order</Tooltip.Content>
|
|
122
|
+
</Tooltip.Root>;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Controlled Tooltip
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const [open, setOpen] = useState(false);
|
|
129
|
+
|
|
130
|
+
<Tooltip.Root open={open} onOpenChange={setOpen}>
|
|
131
|
+
<Tooltip.Trigger render={<Button />}>Info</Tooltip.Trigger>
|
|
132
|
+
<Tooltip.Content>Additional details here</Tooltip.Content>
|
|
133
|
+
</Tooltip.Root>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Accessibility
|
|
137
|
+
|
|
138
|
+
- Tooltip content is rendered in a portal
|
|
139
|
+
- Trigger receives focus and keyboard activation support
|
|
140
|
+
- `sr-only` text on icon-only triggers is recommended for screen readers
|
|
141
|
+
|
|
142
|
+
## Related Components
|
|
143
|
+
|
|
144
|
+
- [Button](./button.md) - Common trigger element for tooltips
|
|
@@ -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
|