@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,301 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Authentication
|
|
3
|
+
description: Set up OAuth2/OIDC authentication with Tailor Platform's Auth service using the AuthProvider component
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/concepts/authentication.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Authentication
|
|
9
|
+
|
|
10
|
+
AppShell provides built-in OAuth2/OIDC authentication through the `AuthProvider` component, which integrates seamlessly with Tailor Platform's Auth service. The provider supports any IdP configured in your Tailor Platform application (built-in IdP, Google, Okta, Auth0, etc.).
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
First, create an auth client, then wrap your app with `AuthProvider`:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import {
|
|
18
|
+
createAuthClient,
|
|
19
|
+
AuthProvider,
|
|
20
|
+
AppShell,
|
|
21
|
+
SidebarLayout,
|
|
22
|
+
} from "@tailor-platform/app-shell";
|
|
23
|
+
|
|
24
|
+
const authClient = createAuthClient({
|
|
25
|
+
clientId: CLIENT_ID,
|
|
26
|
+
appUri: TAILOR_PLATFORM_URL,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const App = () => (
|
|
30
|
+
<AuthProvider client={authClient} autoLogin={true} guardComponent={() => <LoadingScreen />}>
|
|
31
|
+
<AppShell modules={modules}>
|
|
32
|
+
<SidebarLayout />
|
|
33
|
+
</AppShell>
|
|
34
|
+
</AuthProvider>
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Find the above values in Tailor Console:
|
|
39
|
+
|
|
40
|
+
- **Tailor Platform URL** (`appUri`): Your application's base URL
|
|
41
|
+
- Obtained from the Application Overview screen in Tailor Platform
|
|
42
|
+
- Use the domain portion of the 'Accessing the API endpoint of this application' setting
|
|
43
|
+
- Example: `"https://xyz.erp.dev"` (no `/query` suffix needed)
|
|
44
|
+
|
|
45
|
+
- **Client ID** (`clientId`): Authentication client identifier
|
|
46
|
+
- Found in Application > Auth screen in your Tailor Platform console
|
|
47
|
+
|
|
48
|
+
The above code will:
|
|
49
|
+
|
|
50
|
+
- Automatically redirect unauthenticated users to the login page (if `autoLogin` is true)
|
|
51
|
+
- Show the `guardComponent` while loading or when unauthenticated
|
|
52
|
+
- Handle token management and session persistence automatically
|
|
53
|
+
|
|
54
|
+
## Authentication Hook
|
|
55
|
+
|
|
56
|
+
Use the `useAuth` hook to access authentication state and methods:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { useAuth } from "@tailor-platform/app-shell";
|
|
60
|
+
|
|
61
|
+
const UserProfile = () => {
|
|
62
|
+
const { isReady, isAuthenticated, login, logout } = useAuth();
|
|
63
|
+
|
|
64
|
+
if (!isReady) {
|
|
65
|
+
return <div>Loading...</div>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!isAuthenticated) {
|
|
69
|
+
return <button onClick={() => void login()}>Sign In</button>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return <button onClick={logout}>Sign Out</button>;
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Return Value
|
|
77
|
+
|
|
78
|
+
| Property | Type | Description |
|
|
79
|
+
| ----------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
80
|
+
| `isAuthenticated` | `boolean` | Whether the user is currently authenticated |
|
|
81
|
+
| `isReady` | `boolean` | Whether the initial authentication check has completed |
|
|
82
|
+
| `error` | `string \| null` | Error message if authentication failed |
|
|
83
|
+
| `login` | `(options?: { returnTo?: string }) => Promise<void>` | Initiates the login/redirect flow and returns to the current page by default |
|
|
84
|
+
| `logout` | `() => Promise<void>` | Clears tokens and ends the session |
|
|
85
|
+
| `checkAuthStatus` | `() => Promise<AuthState>` | Re-checks auth status (always makes a network request) |
|
|
86
|
+
|
|
87
|
+
### Return to the current page
|
|
88
|
+
|
|
89
|
+
`autoLogin` records the current path, query string, and hash before redirecting, then returns there after a successful callback. Manual `login()` calls do the same by default. Pass `returnTo` to choose another same-origin destination:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
function SignInButton() {
|
|
93
|
+
const { login } = useAuth();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<button onClick={() => void login({ returnTo: "/orders/42?tab=activity#history" })}>
|
|
97
|
+
Sign in
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Suspense-Compatible Hook
|
|
104
|
+
|
|
105
|
+
Use `useAuthSuspense` when you want React Suspense to handle the loading state:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { Suspense } from "react";
|
|
109
|
+
import { useAuthSuspense } from "@tailor-platform/app-shell";
|
|
110
|
+
|
|
111
|
+
function App() {
|
|
112
|
+
return (
|
|
113
|
+
<AuthProvider client={authClient}>
|
|
114
|
+
<Suspense fallback={<div>Loading authentication...</div>}>
|
|
115
|
+
<ProtectedContent />
|
|
116
|
+
</Suspense>
|
|
117
|
+
</AuthProvider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ProtectedContent() {
|
|
122
|
+
// isReady is guaranteed to be true here — Suspense handles the loading state
|
|
123
|
+
const { isAuthenticated, login, logout } = useAuthSuspense();
|
|
124
|
+
|
|
125
|
+
if (!isAuthenticated) {
|
|
126
|
+
return <button onClick={() => void login()}>Log In</button>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return <button onClick={logout}>Log Out</button>;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Auth Client
|
|
134
|
+
|
|
135
|
+
Use `createAuthClient` to create an authentication client that you pass to `AuthProvider`. The client handles token management and provides an authenticated `fetch` method for use with GraphQL clients.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { createAuthClient } from "@tailor-platform/app-shell";
|
|
139
|
+
|
|
140
|
+
const authClient = createAuthClient({
|
|
141
|
+
clientId: "your-client-id",
|
|
142
|
+
appUri: "https://xyz.erp.dev",
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Using `authClient.fetch` with a GraphQL Client
|
|
147
|
+
|
|
148
|
+
Pass `authClient.fetch` directly to your GraphQL client (e.g., urql). It transparently handles DPoP proof generation and token refresh on every request. If the server rejects the grant outright, it ends the session rather than replaying a dead token — see [Session expiry](#session-expiry):
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { createAuthClient, AuthProvider } from "@tailor-platform/app-shell";
|
|
152
|
+
import { createClient, Provider } from "urql";
|
|
153
|
+
|
|
154
|
+
const authClient = createAuthClient({
|
|
155
|
+
clientId: "your-client-id",
|
|
156
|
+
appUri: "https://xyz.erp.dev",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const urqlClient = createClient({
|
|
160
|
+
url: `${authClient.getAppUri()}/query`,
|
|
161
|
+
fetch: authClient.fetch,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function App() {
|
|
165
|
+
return (
|
|
166
|
+
<AuthProvider client={authClient} autoLogin={true}>
|
|
167
|
+
<Provider value={urqlClient}>
|
|
168
|
+
<YourAppComponents />
|
|
169
|
+
</Provider>
|
|
170
|
+
</AuthProvider>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Using `createAIGatewayClient` with AI Gateway
|
|
176
|
+
|
|
177
|
+
`createAIGatewayClient` reuses the same authenticated fetch path as the rest of AppShell. Requests go through `authClient.fetch`, so DPoP proof generation and token refresh stay in the auth layer.
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { createAuthClient, createAIGatewayClient, useAIChat } from "@tailor-platform/app-shell";
|
|
181
|
+
|
|
182
|
+
const authClient = createAuthClient({
|
|
183
|
+
clientId: "your-client-id",
|
|
184
|
+
appUri: "https://xyz.erp.dev",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const aiClient = createAIGatewayClient({
|
|
188
|
+
gatewayUri: "https://your-ai-gateway.example.com",
|
|
189
|
+
authClient,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function ChatScreen() {
|
|
193
|
+
const { messages, sendMessage, status, stop } = useAIChat({
|
|
194
|
+
client: aiClient,
|
|
195
|
+
model: "gpt-5-mini",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div>
|
|
200
|
+
{messages.map((message) => (
|
|
201
|
+
<div key={message.id}>
|
|
202
|
+
{message.role}: {message.content}
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
|
|
206
|
+
<button onClick={() => void sendMessage("Hello")}>Send</button>
|
|
207
|
+
<button onClick={stop} disabled={status !== "submitted" && status !== "streaming"}>
|
|
208
|
+
Stop
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `AuthClientConfig`
|
|
216
|
+
|
|
217
|
+
| Property | Type | Required | Description |
|
|
218
|
+
| ------------- | -------- | -------- | ---------------------------------------------------------- |
|
|
219
|
+
| `clientId` | `string` | Yes | OAuth2 client ID from Tailor Platform console |
|
|
220
|
+
| `appUri` | `string` | Yes | Your Tailor Platform application URL |
|
|
221
|
+
| `redirectUri` | `string` | No | OAuth2 redirect URI (defaults to `window.location.origin`) |
|
|
222
|
+
|
|
223
|
+
### `EnhancedAuthClient` Methods
|
|
224
|
+
|
|
225
|
+
| Method / Property | Type | Description |
|
|
226
|
+
| ----------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
227
|
+
| `getAppUri()` | `() => string` | Returns the `appUri` used to create this client |
|
|
228
|
+
| `fetch` | `typeof fetch` | Authenticated fetch with built-in DPoP proof generation, token refresh, and session teardown on a rejected grant |
|
|
229
|
+
|
|
230
|
+
## `AuthProvider` Props
|
|
231
|
+
|
|
232
|
+
| Prop | Type | Required | Description |
|
|
233
|
+
| ---------------- | ----------------------- | -------- | ----------------------------------------------------- |
|
|
234
|
+
| `client` | `EnhancedAuthClient` | Yes | Auth client created with `createAuthClient` |
|
|
235
|
+
| `autoLogin` | `boolean` | No | Automatically redirect unauthenticated users to login |
|
|
236
|
+
| `guardComponent` | `() => React.ReactNode` | No | Rendered while loading or when not authenticated |
|
|
237
|
+
|
|
238
|
+
## When the callback fails
|
|
239
|
+
|
|
240
|
+
A sign-in that comes back from the authorization server unsuccessfully — the user declined, the client is not permitted, the exchange failed — leaves `isAuthenticated` false with the reason on `useAuth().error`. The callback parameters are cleaned out of the URL on every outcome (since `auth-public-client` 0.6.1), preserving unrelated query parameters, the hash, and the router's history state.
|
|
241
|
+
|
|
242
|
+
With `autoLogin`, a failure that looks recoverable is retried once automatically. Beyond that, and for any refusal the authorization server issues explicitly, AppShell stops: sending the user straight back would ask the same question, get the same answer, and loop.
|
|
243
|
+
|
|
244
|
+
That makes the guard the place where a failed sign-in becomes visible. A guard that only ever renders a spinner will spin indefinitely in this case, so render the error and offer a way out:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
const AuthGate = () => {
|
|
248
|
+
const { isReady, error, login } = useAuth();
|
|
249
|
+
|
|
250
|
+
if (error) {
|
|
251
|
+
return (
|
|
252
|
+
<div>
|
|
253
|
+
<p>Sign-in failed: {error}</p>
|
|
254
|
+
<button onClick={() => void login()}>Try again</button>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return isReady ? <LoginPrompt /> : <LoadingScreen />;
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Integration with AppShell
|
|
264
|
+
|
|
265
|
+
The authentication provider works seamlessly with AppShell's data layer, automatically handling:
|
|
266
|
+
|
|
267
|
+
- OAuth2 token management
|
|
268
|
+
- Authenticated fetch with DPoP proof generation
|
|
269
|
+
- Session persistence and token refresh
|
|
270
|
+
- Session teardown when the server rejects the grant
|
|
271
|
+
- Automatic redirects for protected routes (via `autoLogin`)
|
|
272
|
+
|
|
273
|
+
OAuth callback parameters (`code`, `state`) are automatically cleaned from the URL after a successful login — no dedicated callback page is needed.
|
|
274
|
+
|
|
275
|
+
## Session expiry
|
|
276
|
+
|
|
277
|
+
Access tokens are refreshed transparently, so a session normally outlives individual token lifetimes without the app doing anything.
|
|
278
|
+
|
|
279
|
+
When a refresh cannot succeed — the refresh token has expired, been revoked, or is otherwise rejected by the token endpoint — the auth client ends the session instead of retrying indefinitely. It clears stored tokens, sets `isAuthenticated` to `false`, and emits `logout` followed by `auth_state_changed`.
|
|
280
|
+
|
|
281
|
+
What the user sees follows from the props you already pass:
|
|
282
|
+
|
|
283
|
+
- With `autoLogin`, `AuthProvider` redirects to sign-in.
|
|
284
|
+
- With `guardComponent`, the guard renders in place of your app.
|
|
285
|
+
- With neither, `useAuth().isAuthenticated` flips to `false` and your own UI decides.
|
|
286
|
+
|
|
287
|
+
Not every failed refresh ends the session. A request timeout, a network failure, and a plain 5xx from the token endpoint all leave it intact, so a dropped connection does not sign users out.
|
|
288
|
+
|
|
289
|
+
The line is drawn on the shape of the response, not on how transient the underlying cause is. Any rejection the token endpoint returns as a 4xx carrying an `error` code ends the session — the sole exception is `use_dpop_nonce`. A server that reports overload as `400 {"error":"server_error"}` or `400 {"error":"temporarily_unavailable"}` will therefore sign users out, as will a 5xx that carries a `WWW-Authenticate` header. If you operate the token endpoint, prefer a bare 5xx for conditions you want treated as retryable.
|
|
290
|
+
|
|
291
|
+
To react to teardown yourself — clearing app-level caches, for instance — subscribe to the client's events:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
return authClient.addEventListener((event) => {
|
|
296
|
+
if (event.type === "logout") {
|
|
297
|
+
clearAppCaches();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}, [authClient]);
|
|
301
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Custom Components
|
|
3
|
+
description: When AppShell lacks a component — compose first, and if you must build one, conform to the token system
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/concepts/custom-components.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Custom Components
|
|
9
|
+
|
|
10
|
+
Most ERP screens compose entirely from AppShell primitives. When you hit a gap, work through this decision tree before building anything:
|
|
11
|
+
|
|
12
|
+
### Decision tree
|
|
13
|
+
|
|
14
|
+
1. **Can you compose existing AppShell primitives?** A "card with a metric and a trend arrow" is `MetricCard`, or `Card` plus a lucide icon — not a new component. Compose first, and check the [component docs](../components/) for what already exists before concluding there is a gap.
|
|
15
|
+
2. **If not, build it locally** under `src/components/<name>/`, conforming to the rules below.
|
|
16
|
+
|
|
17
|
+
> If a local custom proves reusable across several apps, it is a candidate to contribute upstream into AppShell — a separate, contributor workflow (see `CONTRIBUTING.md` / the `add-component` skill), not something to do from a consuming app.
|
|
18
|
+
|
|
19
|
+
### Conformance rules (non-negotiable for any custom component)
|
|
20
|
+
|
|
21
|
+
- **Tokens only.** No hex literals, no magic px values, no hand-rolled shadows. Every visual property maps to a token from the [Styling & Theming](./styling-theming.md) catalog — and only to a token that actually exists there.
|
|
22
|
+
- **Base UI data-attribute pattern for state.** Expose `data-*` attributes that reflect internal state; never style off React props alone. A custom toggle exposes `data-checked`; a custom step indicator exposes `data-active`, `data-completed`, etc.
|
|
23
|
+
- **Compose AppShell primitives inside.** If the custom needs a button, use `Button` — not raw `<button>`. Same for `Input`, `Badge`, `Card`. The custom's job is composition, not reinvention.
|
|
24
|
+
- **Document with a `README.md`** in the component folder listing: purpose, props, tokens used, and a brief justification for why this can't be composed from existing AppShell primitives.
|
|
25
|
+
- **Match accessibility behavior** of the closest AppShell equivalent — focus management, ARIA roles/attributes, keyboard interactions. Reach for Base UI primitives if the behavior is non-trivial.
|
|
26
|
+
|
|
27
|
+
Example skeleton for a local custom component:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// src/components/StepIndicator/index.tsx
|
|
31
|
+
import { Check } from "lucide-react";
|
|
32
|
+
|
|
33
|
+
type Props = { steps: string[]; current: number };
|
|
34
|
+
|
|
35
|
+
export function StepIndicator({ steps, current }: Props) {
|
|
36
|
+
return (
|
|
37
|
+
<ol className="flex gap-2">
|
|
38
|
+
{steps.map((label, i) => (
|
|
39
|
+
<li
|
|
40
|
+
key={label}
|
|
41
|
+
data-active={i === current ? "" : undefined}
|
|
42
|
+
data-completed={i < current ? "" : undefined}
|
|
43
|
+
className="
|
|
44
|
+
flex items-center gap-2 rounded-md px-3 py-2
|
|
45
|
+
bg-muted text-sm text-muted-foreground
|
|
46
|
+
data-[active]:bg-primary data-[active]:text-primary-foreground
|
|
47
|
+
data-[completed]:text-status-completed
|
|
48
|
+
"
|
|
49
|
+
>
|
|
50
|
+
{i < current && <Check className="size-4" />}
|
|
51
|
+
{label}
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ol>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Notice: every value is a real token, `data-*` attributes carry state, no hex literals, and the icon comes from `lucide-react` rather than a nonexistent `Icon` component.
|