diff --git a/docs/app-shell/api/define-i18n-labels.md b/docs/app-shell/api/define-i18n-labels.md
index 699de89..b1f1e47 100644
--- a/docs/app-shell/api/define-i18n-labels.md
+++ b/docs/app-shell/api/define-i18n-labels.md
@@ -138,4 +138,5 @@ labels.t("invalid"); // ❌ Type error
## Related
+- [Internationalization Guide](../guides/internationalization) - Complete i18n guide
- [defineModule](define-module) - Use labels in modules
diff --git a/docs/app-shell/api/guards/overview.md b/docs/app-shell/api/guards/overview.md
index 5645934..f18bdac 100644
--- a/docs/app-shell/api/guards/overview.md
+++ b/docs/app-shell/api/guards/overview.md
@@ -350,3 +350,4 @@ const checkPermission: Guard = async ({ context }) => {
- [hidden()](hidden) - Hide/deny access guard function
- [redirectTo()](redirect-to) - Redirect guard function
- [WithGuard Component](../../components/with-guard) - Component-level guards
+- [Guards & Permissions Guide](../../guides/guards-permissions) - Detailed tutorial
diff --git a/docs/app-shell/changelog.md b/docs/app-shell/changelog.md
index 9f8c8ec..86d0303 100644
--- a/docs/app-shell/changelog.md
+++ b/docs/app-shell/changelog.md
@@ -1,5 +1,74 @@
# @tailor-platform/app-shell
+## 0.33.0
+
+### Minor Changes
+
+- 6f5c23f: **Breaking:** `AsyncFetcherFn` now receives `string | null` instead of `string` as the `query` parameter.
+
+ The fetcher is called with `null` when the user has not typed anything (e.g. the dropdown was just opened or the input was cleared). Return initial/default items for `null`, or return an empty array to show nothing until the user starts typing.
+
+ `useAsync` also now returns an `onOpenChange` handler that triggers `fetcher(null)` on the first open, so `Combobox.Async` shows initial items immediately when the dropdown opens.
+
+ ```tsx
+ // Before
+ const fetcher = async (query: string, { signal }) => { ... };
+
+ // After
+ const fetcher = async (query: string | null, { signal }) => {
+ const res = await fetch(`/api/items?q=${query ?? ""}`, { signal });
+ return res.json();
+ };
+ ```
+
+- 7917328: Add `useOverrideBreadcrumb` hook for dynamically overriding breadcrumb titles from within page components. This is useful for displaying data-driven titles (e.g., record names) instead of static route-based titles.
+
+ With `defineResource`:
+
+ ```tsx
+ import { useOverrideBreadcrumb } from "@tailor-platform/app-shell";
+
+ defineResource({
+ path: ":id",
+ component: () => {
+ const { data } = useQuery(GET_ORDER, { variables: { id } });
+
+ // Update breadcrumb with the order name
+ useOverrideBreadcrumb(data?.order?.name);
+
+ return ;
+ },
+ });
+ ```
+
+ With file-based routing (`pages/orders/[id]/page.tsx`):
+
+ ```tsx
+ import { useOverrideBreadcrumb, useParams } from "@tailor-platform/app-shell";
+
+ const OrderDetailPage = () => {
+ const { id } = useParams();
+ const { data } = useQuery(GET_ORDER, { variables: { id } });
+
+ // Update breadcrumb with the order name
+ useOverrideBreadcrumb(data?.order?.name);
+
+ return
...
;
+ };
+
+ export default OrderDetailPage;
+ ```
+
+- 58f8024: Fix guards defined via `appShellPageProps` being silently ignored in file-based routing. Guards now correctly produce route loaders for both root and non-root pages.
+
+### Patch Changes
+
+- 1cad50d: Fix portal-based components (`Menu`, `Select`, `Combobox`, `Autocomplete`, `Tooltip`) rendering behind the sidebar by establishing a stacking context on each portal container.
+
+ Centralize all z-index values into CSS custom properties (`--z-sidebar`, `--z-sidebar-rail`, `--z-popup`, `--z-overlay`) defined in `globals.css`.
+
+- afec4f7: Updated graphql (^16.13.0 -> ^16.13.2)
+
## 0.32.0
### Minor Changes
diff --git a/docs/app-shell/components/app-shell.md b/docs/app-shell/components/app-shell.md
index 1ad61da..7bd7737 100644
--- a/docs/app-shell/components/app-shell.md
+++ b/docs/app-shell/components/app-shell.md
@@ -170,6 +170,8 @@ Settings appear in a dropdown menu in the sidebar header, accessible via the set
Supported locales: `en`, `ja`
+[Learn more about Internationalization →](../guides/internationalization)
+
### errorBoundary
- **Type:** `ErrorBoundaryComponent` (optional)
diff --git a/docs/app-shell/components/command-palette.md b/docs/app-shell/components/command-palette.md
index 5fe8aac..194bbc0 100644
--- a/docs/app-shell/components/command-palette.md
+++ b/docs/app-shell/components/command-palette.md
@@ -277,6 +277,7 @@ The CommandPalette follows these design principles:
- [Modules and Resources](../concepts/modules-and-resources) - Define routes that appear in CommandPalette
- [Routing and Navigation](../concepts/routing-navigation) - Navigation system
+- [Guards and Permissions](../guides/guards-permissions) - Control route visibility
## Troubleshooting
diff --git a/docs/app-shell/components/csv-importer.md b/docs/app-shell/components/csv-importer.md
new file mode 100644
index 0000000..0022991
--- /dev/null
+++ b/docs/app-shell/components/csv-importer.md
@@ -0,0 +1,212 @@
+---
+title: CsvImporter
+description: Guided multi-step CSV import flow with drag-and-drop upload, column mapping, validation, and inline error correction
+---
+
+# CsvImporter
+
+The `CsvImporter` component provides a guided, multi-step CSV import flow rendered inside a drawer. It handles drag-and-drop file upload, interactive column mapping, Standard Schema validation, inline cell editing, and async server-side validation.
+
+## Import
+
+```tsx
+import {
+ CsvImporter,
+ useCsvImporter,
+ csv,
+ type CsvSchema,
+ type CsvColumn,
+ type CsvImportEvent,
+ type CsvCellIssue,
+ type CsvCorrection,
+ type CsvColumnMapping,
+ type ParsedRow,
+} from "@tailor-platform/app-shell";
+```
+
+## Basic Usage
+
+Use the `useCsvImporter` hook to manage state, then render `` with the returned props.
+
+```tsx
+import { CsvImporter, useCsvImporter, csv } from "@tailor-platform/app-shell";
+import { Button } from "@tailor-platform/app-shell";
+
+function ProductImport() {
+ const { open, props } = useCsvImporter({
+ schema: {
+ columns: [
+ {
+ key: "name",
+ label: "Name",
+ required: true,
+ aliases: ["product_name"],
+ schema: csv.string({ min: 1 }),
+ },
+ {
+ key: "price",
+ label: "Price",
+ schema: csv.number({ min: 0 }),
+ },
+ {
+ key: "active",
+ label: "Active",
+ schema: csv.boolean(),
+ },
+ ],
+ },
+ onImport: (event) => {
+ console.log(event.summary);
+ },
+ });
+
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+## `useCsvImporter`
+
+The `useCsvImporter` hook manages the open/close state and returns both an `open` function and `props` to spread onto ``.
+
+### Options
+
+| Option | Type | Default | Description |
+| ------------- | -------------------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
+| `schema` | `CsvSchema` | — | Column definitions for the import (see [CsvSchema](#csvschema)) |
+| `onImport` | `(event: CsvImportEvent) => void \| Promise` | — | Called when the user confirms the import after resolving all errors |
+| `onValidate` | `(rows: ParsedRow[]) => Promise` | — | Optional async callback for server-side validation after schema checks pass |
+| `maxFileSize` | `number` | `10485760` (10 MB) | Maximum allowed file size in bytes |
+
+### Return Value
+
+| Property | Type | Description |
+| -------- | ------------------ | ----------------------------------------------------- |
+| `open` | `() => void` | Function to programmatically open the importer drawer |
+| `props` | `CsvImporterProps` | Props object to spread directly onto `` |
+
+## `CsvImporter` Props
+
+`CsvImporter` is intended to be used with props returned from `useCsvImporter`. Spread the `props` object directly onto the component.
+
+```tsx
+
+```
+
+## Schemas
+
+### `CsvSchema`
+
+```ts
+type CsvSchema = {
+ columns: CsvColumn[];
+};
+```
+
+### `CsvColumn`
+
+| Property | Type | Default | Description |
+| ------------- | ------------------ | ------- | --------------------------------------------------------------------------------------- |
+| `key` | `string` | — | Internal key; becomes the object key in parsed row data |
+| `label` | `string` | — | Display label shown in the mapping UI |
+| `description` | `string` | — | Optional hint shown in the mapping UI |
+| `required` | `boolean` | `false` | Whether this column must be mapped before proceeding |
+| `aliases` | `string[]` | — | Alternative CSV header names for automatic matching |
+| `schema` | `StandardSchemaV1` | — | Standard Schema validator for coercion and validation (see [csv helpers](#csv-helpers)) |
+
+## `csv` Helpers
+
+Built-in Standard Schema validators for common CSV column types. Each helper handles both coercion and validation in a single declaration.
+
+| Helper | Output type | Description |
+| ------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------- |
+| `csv.string(options?: { min?: number; max?: number })` | `string` | Pass-through string with optional length constraints |
+| `csv.number(options?: { min?: number; max?: number; integer?: boolean })` | `number` | Coerces raw CSV string to a number; rejects `NaN` |
+| `csv.boolean(options?: { truthy?: string[]; falsy?: string[] })` | `boolean` | Recognises `"true"/"1"/"yes"` and `"false"/"0"/"no"` (case-insensitive) |
+| `csv.date()` | `Date` | Coerces raw CSV string to a `Date`; rejects unparseable values |
+| `csv.enum(values: string[])` | `T extends string` | Validates the value is one of the allowed strings (case-sensitive) |
+
+```tsx
+// String with required check (min: 1)
+csv.string({ min: 1 });
+
+// Number with lower bound
+csv.number({ min: 0 });
+
+// Integer only
+csv.number({ integer: true });
+
+// Boolean with defaults: truthy = ["true","1","yes"], falsy = ["false","0","no"]
+csv.boolean();
+
+// Date
+csv.date();
+
+// Enum
+csv.enum(["active", "inactive", "pending"]);
+```
+
+## Server-side Validation
+
+Use `onValidate` to perform async checks (e.g. uniqueness constraints) after schema validation passes. Return an array of `CsvCellIssue` objects to mark cells with errors or warnings.
+
+```tsx
+const { open, props } = useCsvImporter({
+ schema: { columns: [...] },
+ onValidate: async (rows) => {
+ const issues = await checkUniqueness(rows);
+ return issues; // CsvCellIssue[]
+ },
+ onImport: (event) => { /* ... */ },
+});
+```
+
+### `CsvCellIssue`
+
+| Property | Type | Description |
+| ----------- | ---------------------- | ---------------------------------------------------- |
+| `rowIndex` | `number` | 0-based row index |
+| `columnKey` | `string` | The schema column key |
+| `level` | `"error" \| "warning"` | `"error"` blocks import; `"warning"` allows import |
+| `message` | `string` | Human-readable message displayed inline in the table |
+
+## Import Event
+
+The `onImport` callback receives a `CsvImportEvent` with the following shape:
+
+| Property | Type | Description |
+| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| `file` | `File` | The original file selected by the user |
+| `mappings` | `CsvColumnMapping[]` | The confirmed column mappings |
+| `corrections` | `CsvCorrection[]` | Corrections made by the user in the review step |
+| `issues` | `CsvCellIssue[]` | Any remaining issues (warnings only — errors are resolved before import) |
+| `summary` | `{ totalRows, validRows, correctedRows, skippedRows, warningRows }` | Summary statistics |
+
+## `buildRows`
+
+A utility to reconstruct the fully processed row data on the client side from a `CsvImportEvent`. Useful when you want typed row objects on the frontend instead of sending raw file data to a server.
+
+```tsx
+import { buildRows } from "@tailor-platform/app-shell";
+
+onImport: async (event) => {
+ const rows = await buildRows(event, schema);
+ // rows: Record[] — schema coercion and corrections applied
+ await saveToBackend(rows);
+},
+```
+
+> If you are sending the file, mappings, and corrections to a backend for processing, you do not need `buildRows`.
+
+## i18n
+
+`CsvImporter` includes built-in English and Japanese labels. No additional setup is required.
+
+## Related Components
+
+- [Sheet](sheet) — Slide-in panel (the drawer used internally by CsvImporter)
+- [Button](button) — Use as the trigger to open the importer
diff --git a/docs/app-shell/components/sidebar-group.md b/docs/app-shell/components/sidebar-group.md
index a1c2c2d..39ad856 100644
--- a/docs/app-shell/components/sidebar-group.md
+++ b/docs/app-shell/components/sidebar-group.md
@@ -455,6 +455,7 @@ Without `to`, only the chevron is clickable (for expand/collapse).
## Related Concepts
- [Sidebar Navigation](../concepts/sidebar-navigation) - Sidebar customization guide
+- [Internationalization](../guides/internationalization) - Multi-language support
## API Reference
diff --git a/docs/app-shell/components/with-guard.md b/docs/app-shell/components/with-guard.md
index 67f432a..dd64a05 100644
--- a/docs/app-shell/components/with-guard.md
+++ b/docs/app-shell/components/with-guard.md
@@ -424,6 +424,7 @@ const checkPermission: Guard = async ({ context }) => {
## Related Concepts
+- [Guards and Permissions](../guides/guards-permissions) - Comprehensive guard guide
- [Modules and Resources](../concepts/modules-and-resources) - Route-level guards
## API Reference
diff --git a/docs/app-shell/concepts/authentication.md b/docs/app-shell/concepts/authentication.md
index 8823b37..08a8331 100644
--- a/docs/app-shell/concepts/authentication.md
+++ b/docs/app-shell/concepts/authentication.md
@@ -49,7 +49,7 @@ The above code will:
- Show the `guardComponent` while loading or when unauthenticated
- Handle token management and session persistence automatically
-See the [useAuth](../api/use-auth) for more details.
+See the [API](api.md#authprovider) for more details.
## Authentication Hook
@@ -89,7 +89,7 @@ The `authState` object contains:
| `isAuthenticated` | `boolean` | Whether user is authenticated |
| `user` | `User \| null` | Current user object (null if not authenticated) |
-See the [useAuth API](../api/use-auth) for more details.
+See the [API](api.md#useauth) for more details.
## Extending User Type
@@ -244,7 +244,7 @@ function App() {
| `getAppUri()` | `() => string` | Returns the `appUri` used to create this client |
| `fetch` | `typeof fetch` | Authenticated fetch with built-in DPoP proof generation and token refresh |
-See the [useAuth API](../api/use-auth) for more details.
+See the [API](api.md#createauthclient) for more details.
## Integration with AppShell
diff --git a/docs/app-shell/concepts/modules-and-resources.md b/docs/app-shell/concepts/modules-and-resources.md
index 4cdb062..b8f0f59 100644
--- a/docs/app-shell/concepts/modules-and-resources.md
+++ b/docs/app-shell/concepts/modules-and-resources.md
@@ -107,7 +107,7 @@ The same applies to resources: a `defineResource()` call without a `component` m
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).
-> Read more about [client-side navigation](routing-navigation) in AppShell apps
+> Read more about [client-side navigation](routing-and-navigation) in AppShell apps
## Route Guards
@@ -117,7 +117,7 @@ Both modules and resources support `guards` - an array of functions that control
- Reusability: Share common guards across routes
- Semantic constraints: Clear `pass()`, `hidden()`, or `redirectTo()` results
-See the [Route Guards documentation](../api/guards/overview) in the API reference for full details.
+See the [Route Guards documentation](api.md#route-guards) in the API reference for full details.
### Guard Examples