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