Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/app-shell/api/define-i18n-labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,5 @@ labels.t("invalid"); // ❌ Type error

## Related

- [Internationalization Guide](../guides/internationalization) - Complete i18n guide
- [defineModule](define-module) - Use labels in modules
1 change: 1 addition & 0 deletions docs/app-shell/api/guards/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
69 changes: 69 additions & 0 deletions docs/app-shell/changelog.md
Original file line number Diff line number Diff line change
@@ -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 <OrderDetail />;
},
});
```

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 <div>...</div>;
};

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
Expand Down
2 changes: 2 additions & 0 deletions docs/app-shell/components/app-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/app-shell/components/command-palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
212 changes: 212 additions & 0 deletions docs/app-shell/components/csv-importer.md
Original file line number Diff line number Diff line change
@@ -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 `<CsvImporter>` 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 (
<>
<Button onClick={open}>Import CSV</Button>
<CsvImporter {...props} />
</>
);
}
```

## `useCsvImporter`

The `useCsvImporter` hook manages the open/close state and returns both an `open` function and `props` to spread onto `<CsvImporter>`.

### Options

| Option | Type | Default | Description |
| ------------- | -------------------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
| `schema` | `CsvSchema` | — | Column definitions for the import (see [CsvSchema](#csvschema)) |
| `onImport` | `(event: CsvImportEvent) => void \| Promise<void>` | — | Called when the user confirms the import after resolving all errors |
| `onValidate` | `(rows: ParsedRow[]) => Promise<CsvCellIssue[]>` | — | 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>` |

## `CsvImporter` Props

`CsvImporter` is intended to be used with props returned from `useCsvImporter`. Spread the `props` object directly onto the component.

```tsx
<CsvImporter {...props} />
```

## 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<string, unknown>[] — 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
1 change: 1 addition & 0 deletions docs/app-shell/components/sidebar-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/app-shell/components/with-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/app-shell/concepts/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/app-shell/concepts/modules-and-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down