Releases: tailor-platform/app-shell
@tailor-platform/app-shell@0.33.0
Minor Changes
-
6f5c23f: Breaking:
AsyncFetcherFnnow receivesstring | nullinstead ofstringas thequeryparameter.The fetcher is called with
nullwhen the user has not typed anything (e.g. the dropdown was just opened or the input was cleared). Return initial/default items fornull, or return an empty array to show nothing until the user starts typing.useAsyncalso now returns anonOpenChangehandler that triggersfetcher(null)on the first open, soCombobox.Asyncshows initial items immediately when the dropdown opens.// 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
useOverrideBreadcrumbhook 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: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):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
appShellPagePropsbeing 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 inglobals.css.
@tailor-platform/app-shell@0.32.0
Minor Changes
-
3c1e939: Add
ActivityCardAPIs for both simple and advanced use cases:- Standalone API (
<ActivityCard />) for quick timeline rendering with sensible defaults - Compound API (
ActivityCard.Root/.Items/.Item) for fully custom item rendering (icons, links, buttons, badges, mixed item kinds)
Standalone API
import { ActivityCard } from "@tailor-platform/app-shell"; <ActivityCard items={items} title="Updates" maxVisible={6} overflowLabel="more" groupBy="day" />;
Compound API
import { ActivityCard, Badge } from "@tailor-platform/app-shell"; import type { ActivityCardBaseItem } from "@tailor-platform/app-shell"; interface MyItem extends ActivityCardBaseItem { kind: "approval" | "update"; label?: string; message?: string; } <ActivityCard.Root items={items} title="Updates" groupBy="day"> <ActivityCard.Items<MyItem>> {(item) => item.kind === "approval" ? ( <ActivityCard.Item indicator={<ApprovedIcon />}> <p>{item.label}</p> <Badge variant="default">Complete</Badge> </ActivityCard.Item> ) : ( <ActivityCard.Item> <p>{item.message}</p> <a href="#">View changes</a> </ActivityCard.Item> ) } </ActivityCard.Items> </ActivityCard.Root>;
Each item must satisfy
ActivityCardBaseItem(id+timestamp). Items without anindicatorrender a default timeline node. Theindicatorprop accepts anyReactNode(avatars, icons, etc.). - Standalone API (
-
3c1e939: Add
Avatar(Base UI):Avatar.Root,Avatar.Image, andAvatar.Fallbackwithsizevariants (sm,default,lg) and exportedavatarVariantsandAvatarProps.ActivityCarduses this shared avatar; exportActivityCardItemfrom the package root for typed activity lists.import { Avatar } from "@tailor-platform/app-shell"; export function UserAvatar() { return ( <Avatar.Root> <Avatar.Image src="/user.png" alt="" /> <Avatar.Fallback>AB</Avatar.Fallback> </Avatar.Root> ); }
-
693d8aa: Add
xssize variant toButtoncomponent — a compact extra-small button (h-7,text-xs) for inline actions within cards and tight layouts.<Button size="xs" variant="outline"> Resubmit </Button>
-
497be49: Add
Cardcompound component (Card.Root,Card.Header,Card.Content) as a general-purpose container with consistent styling. Existing card-style components (DescriptionCard,MetricCard,ActionPanel) now useCardinternally.import { Card } from "@tailor-platform/app-shell"; <Card.Root> <Card.Header title="Order Details" description="Summary of order #1234" /> <Card.Content> <p>Content goes here</p> </Card.Content> </Card.Root>;
-
ea4b256: Add
Form,Fieldset, andFieldcomponents for building validated forms.FormA form element with consolidated error handling and validation. Supports
onFormSubmitfor type-safe parsed form values, andonSubmitfor nativeFormEventaccess. External errors (e.g. API responses) can be fed via theerrorsprop and are automatically routed to matchingField.Errorcomponents.<Form onFormSubmit={(values) => save(values)}> <Field.Root name="email"> <Field.Label>Email</Field.Label> <Field.Control type="email" required /> <Field.Error match="typeMismatch">Enter a valid email.</Field.Error> </Field.Root> <button type="submit">Save</button> </Form>
FieldsetA compound component (
Fieldset.Root,Fieldset.Legend) for grouping related fields with a shared legend for accessible form sectioning.<Fieldset.Root> <Fieldset.Legend>Billing details</Fieldset.Legend> <Field.Root name="company"> <Field.Label>Company</Field.Label> <Field.Control /> </Field.Root> </Fieldset.Root>
FieldA compound component (
Field.Root,Field.Label,Field.Control,Field.Description,Field.Error,Field.Validity) that groups all parts of a form field and manages its validation state.Field.Rootcreates a Field context boundary. All child sub-components (Field.Label,Field.Control,Field.Description,Field.Error,Field.Validity) and any Base UI-backed AppShell component (e.g. Select, Combobox, Autocomplete) placed insideField.Rootautomatically connect to this context — inheriting label association (htmlFor),aria-describedby, disabled state, and validation state (invalid,dirty,touched).Field.Controlis a styled<input>that shares its base styles with theInputcomponent. It can be omitted when using another AppShell input component (e.g. Select, Combobox, Autocomplete) as a sibling — those components register themselves with the Field context automatically. It also supports native HTML constraint attributes (required,type="email",pattern, etc.) for built-in validation.<Field.Root name="email"> <Field.Label>Email</Field.Label> <Field.Control type="email" required /> <Field.Description>We'll never share your email.</Field.Description> <Field.Error match="typeMismatch">Please enter a valid email.</Field.Error> </Field.Root>
Using another AppShell component as the control
Field.Controlcan be omitted when using a Base UI-backed AppShell component (e.g. Select, Combobox). The component registers itself with the Field context automatically, inheriting label association and validation state.<Field.Root name="country"> <Field.Label>Country</Field.Label> <Select> <Select.Trigger> <Select.Value placeholder="Select a country" /> </Select.Trigger> <Select.Popup> <Select.Item value="jp">Japan</Select.Item> <Select.Item value="us">United States</Select.Item> </Select.Popup> </Select> <Field.Error>Please select a country.</Field.Error> </Field.Root>
Custom rendering with
Field.ValidityField.Validityexposes the field'sValidityStatevia a render callback, allowing fully custom validation UI.<Field.Root name="password"> <Field.Label>Password</Field.Label> <Field.Control type="password" required minLength={8} /> <Field.Validity> {(state) => ( <ul> <li>{state.validity.valueMissing ? "❌" : "✅"} Required</li> <li>{state.validity.tooShort ? "❌" : "✅"} At least 8 characters</li> </ul> )} </Field.Validity> </Field.Root>
React Hook Form + Zod integration
Field.RootacceptsisTouched,isDirty,invalid, anderrorprops that align with RHF'sfieldStateshape, so you can spreadfieldStatedirectly. UseForm'sonSubmitprop to connect RHF'shandleSubmit.import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; const schema = z.object({ email: z.string().email() }); function MyForm() { const { control, handleSubmit } = useForm({ resolver: zodResolver(schema), }); return ( <Form onSubmit={handleSubmit((data) => save(data))}> <Controller name="email" control={control} render={({ field, fieldState }) => ( <Field.Root {...fieldState}> <Field.Label>Email</Field.Label> <Field.Control {...field} /> <Field.Error>{fieldState.error?.message}</Field.Error> </Field.Root> )} /> <button type="submit">Save</button> </Form> ); }
The Field context co-exists with RHF — it only drives accessibility wiring (
htmlFor,aria-describedby) and visual state (data-invalid,data-dirty,data-touched) without interfering with RHF's value management or validation lifecycle. -
524d49a: Add
MetricCardcomponent for dashboard KPI summaries (title, value, optional trend and description).import { MetricCard } from "@tailor-platform/app-shell"; <MetricCard title="Net total payment" value="$1,500.00" trend={{ direction: "up", value: "+5%" }} description="vs last month" />;
-
135d5a9: Render module-level breadcrumb segments as non-clickable text when the module has no component.
Modules defined without acomponent(group-only modules) now display their breadcrumb as plain text instead of a link, preventing navigation to non-existent pages.
Patch Changes
- 6dec130: Add
color-scheme: darkto the.darkselector in theme CSS so that native form controls (e.g.<select>) render correctly in dark mode on Windows. - ae37125: Fix
breadcrumbTitlenot being propagated in file-based routing. ThebreadcrumbTitleset inAppShellPageProps.metais now correctly reflected in breadcrumbs, matching the behavior of thedefineResourceAPI. - 49e2e3a: Fix
DefaultSidebarnot applying active style whenbasePathis not specified. The sidebar now correctly normalizes URLs before comparing with the current pathname, ensuring the active highlight appears on the correct menu item. - 10c0cd3: Add
min-h-0toSidebarInsetso that nested content areas can scroll independently instead of causing the entire page to scroll.
@tailor-platform/app-shell@0.31.1
Patch Changes
- e81fbd7: Updated react-router (^7.4.0 -> ^7.13.1)
- Updated dependencies [01a2249]
- @tailor-platform/app-shell-vite-plugin@0.2.0
@tailor-platform/app-shell-vite-plugin@0.2.0
Minor Changes
-
01a2249: Add
entrypointoption toappShellRoutes()plugin. When specified, only imports from the entrypoint file are intercepted and replaced with the pages-injected AppShell, eliminating circular module dependencies entirely. It is recommended to set this option to avoid potential TDZ errors caused by circular imports in page components.appShellRoutes({ entrypoint: "src/App.tsx", });
@tailor-platform/app-shell@0.31.0
Minor Changes
-
e7a1177: Add
Menu,Select,Combobox, andAutocompletecomponents.New components
import { Menu, Select, Combobox, Autocomplete, } from "@tailor-platform/app-shell";
Menu
Dropdown menu with compound component API (
Menu.Root,Menu.Trigger,Menu.Content,Menu.Item, etc.). Supports checkbox/radio items, grouped items, separators, and nested sub-menus viaMenu.SubmenuRoot/Menu.SubmenuTrigger.<Menu.Root> <Menu.Trigger>Open menu</Menu.Trigger> <Menu.Content> <Menu.Item>Edit</Menu.Item> <Menu.Item>Duplicate</Menu.Item> <Menu.Separator /> <Menu.Item>Delete</Menu.Item> </Menu.Content> </Menu.Root>
Select
Single or multi-select dropdown. Pass
itemsand get a fully assembled select out of the box. Also supports async data fetching viaSelect.Async.<Select items={["Apple", "Banana", "Cherry"]} placeholder="Pick a fruit" onValueChange={(value) => console.log(value)} />
Combobox
Searchable combobox with single/multi selection. Pass
itemsand get a fully assembled combobox with built-in filtering. Supports async data fetching viaCombobox.Asyncand user-created items viaonCreateItemprop.<Combobox items={["Apple", "Banana", "Cherry"]} placeholder="Search fruits..." onValueChange={(value) => console.log(value)} />
Autocomplete
Text input with a suggestion list. The value is the raw input string, not a discrete item selection. Also supports async suggestions via
Autocomplete.Async.<Autocomplete items={["Apple", "Banana", "Cherry"]} placeholder="Type a fruit..." onValueChange={(value) => console.log(value)} />
Low-level primitives via
.PartsSelect,Combobox, andAutocompleteeach expose a.Partsnamespace containing the styled low-level sub-components (e.g.Root,Input,Content,Item,List, etc.) and hooks (useFilter,useAsync,useCreatable) for building fully custom compositions when the ready-made component doesn't fit your needs.const { Root, Trigger, Content, Item } = Select.Parts;
Patch Changes
- 8e11a5e: Updated lucide-react (^0.562.0 -> ^0.577.0)
@tailor-platform/app-shell@0.30.0
Minor Changes
-
a8c5dcf: Export primitive UI components (
Button,Input,Table,Dialog,Sheet,Tooltip) and update@base-ui/reactto v1.3.0.New components
import { Button, Input, Table, Dialog, Sheet, Tooltip, } from "@tailor-platform/app-shell";
Button
Styled button with variant (
default,outline,destructive, etc.) and size options.<Button variant="outline" size="sm"> Click me </Button>
Input
Styled text input with consistent theming.
<Input placeholder="Enter your name" />
Dialog
Modal dialog with compound component API (
Dialog.Root,Dialog.Content, etc.).<Dialog.Root> <Dialog.Trigger render={<Button />}>Open</Dialog.Trigger> <Dialog.Content> <Dialog.Title>Confirm</Dialog.Title> <Dialog.Description>Are you sure?</Dialog.Description> <Dialog.Footer> <Dialog.Close render={<Button variant="outline" />}> Cancel </Dialog.Close> <Button>Confirm</Button> </Dialog.Footer> </Dialog.Content> </Dialog.Root>
Sheet
Slide-in panel backed by Drawer with native swipe-to-dismiss gesture support.
<Sheet.Root side="right"> <Sheet.Trigger render={<Button />}>Open</Sheet.Trigger> <Sheet.Content> <Sheet.Title>Settings</Sheet.Title> </Sheet.Content> </Sheet.Root>
Tooltip
Hover/focus tooltip with configurable placement and delay via
Tooltip.Provider.<Tooltip.Root> <Tooltip.Trigger render={<Button />}>Hover me</Tooltip.Trigger> <Tooltip.Content>Help text</Tooltip.Content> </Tooltip.Root>
Table
Semantic HTML table with pre-styled header, body, and footer sub-components.
<Table.Root> <Table.Header> <Table.Row> <Table.Head>Name</Table.Head> <Table.Head>Email</Table.Head> <Table.Head>Role</Table.Head> </Table.Row> </Table.Header> <Table.Body> <Table.Row> <Table.Cell>Alice</Table.Cell> <Table.Cell>alice@example.com</Table.Cell> <Table.Cell>Admin</Table.Cell> </Table.Row> </Table.Body> </Table.Root>
Other changes
DescriptionCard,Layout, andLayout.Columnnow accept an optionalstyleprop for inline styles.- Fixed Dialog and Sheet overlay flashing on close animation.
- Fixed missing
astw:prefixes on sidebar utility classes that caused mobile sidebar UI bugs.
@tailor-platform/app-shell@0.29.0
Minor Changes
-
fc59c8a: Add ActionPanel component for ERP-style action lists.
ActionPanel — Card with a title and vertical list of actions (icon + label). Each row is a button triggered via
onClick. The panel uses full width of its parent by default.Example
import { ActionPanel } from "@tailor-platform/app-shell"; <ActionPanel title="Actions" actions={[ { key: "create", label: "Create new invoice", icon: <ReceiptIcon />, onClick: () => openCreateModal(), }, { key: "docs", label: "View documentation", icon: <DocIcon />, onClick: () => window.open("/docs", "_blank", "noopener,noreferrer"), }, ]} />;
-
3e36ece: Allow modules and resources without a component for path-only definitions
Modules and resources can now be defined without a
component, both viadefineModule()/defineResource()and file-based routing. This is useful when a directory exists solely to group child routes under a shared path prefix.Accessing a component-less path directly returns a 404 response, while child routes remain accessible as normal.
// Module without component — groups child resources under a shared path defineModule({ path: "admin", meta: { title: "Admin" }, resources: [ defineResource({ path: "users", component: () => <Users /> }), defineResource({ path: "roles", component: () => <Roles /> }), ], }); // /admin → 404 // /admin/users → renders Users // /admin/roles → renders Roles // Resource without component — groups sub-resources under a namespace defineResource({ path: "namespace", subResources: [ defineResource({ path: "page-a", component: () => <div>Page A</div> }), ], }); // /namespace → 404 // /namespace/page-a → renders Page A
For file-based routing, simply omit
page.tsxfrom a directory:pages/ admin/ users/ page.tsx ← /admin/users renders this roles/ page.tsx ← /admin/roles renders this (no page.tsx for /admin itself → 404)Guards on component-less routes now execute correctly. Previously, guard loaders were silently ignored when no component was present. Now, guards such as
redirectTo()will fire as expected, and if all guards returnpass(), the route falls back to a 404. -
565ae70: Add
Layout.Headersub-component andareaprop toLayout.Column.Motivation
The current
Layoutcomponent requires acolumnsprop that must exactly match the number ofLayout.Columnchildren, and embeds header concerns (title,actions) directly into the layout component. This coupling creates several issues:- Redundant declaration — The column count can be inferred from the number of
Layout.Columnchildren, making thecolumnsprop unnecessary boilerplate. - Limited column placement — The 2-column template always treats the 1st column as main (flex) and the 2nd as a fixed sidebar. There is no way to express a "left sidebar + main content" layout.
- Mixed responsibilities —
Layouthandles both page heading (title/actions) and column arrangement. Separating these intoLayout.HeaderandLayout.Columnmakes each concern independently composable — for example, placing tabs between the header and columns.
What's New
Layout.Header: Compose insideLayoutfor title, actions, and extra content (e.g. tabs) above columns.Layout.Columnareaprop: Declare column roles (left,main,right) for area-based width templates, enabling layouts like left sidebar + main that were previously not possible.columnsandgapprops are deprecated: Column count is auto-detected from children; useclassNamefor gap.- No runtime breaking changes — existing code continues to work as-is. Note:
columnsis now optional, so code that directly readsLayoutProps["columns"]as a non-optional type will need adjustment.
Usage
// With Layout.Header (title, actions, and extra content such as tabs) <Layout className="gap-6"> <Layout.Header title="Edit" actions={[<Button key="save">Save</Button>]}> <Tabs>...</Tabs> {/* Renders below title/actions, above Columns */} </Layout.Header> <Layout.Column>Main content</Layout.Column> <Layout.Column>Side panel</Layout.Column> </Layout> // With area prop (Left + Main) <Layout> <Layout.Column area="left">Side nav</Layout.Column> <Layout.Column area="main">Main content</Layout.Column> </Layout> // With area prop (3 columns) <Layout> <Layout.Column area="left">Left panel</Layout.Column> <Layout.Column area="main">Main content</Layout.Column> <Layout.Column area="right">Right panel</Layout.Column> </Layout> // Existing API still works <Layout title="Edit" actions={[<Button key="save">Save</Button>]} columns={2}> <Layout.Column>Main content</Layout.Column> <Layout.Column>Side panel</Layout.Column> </Layout>
Sub-components
Component Status Props Layout.ColumnRetained area?: "left" | "main" | "right",className?,children?Layout.HeaderNew title?,actions?: ReactNode[],children?: ReactNodeChildren Rules
Layout.Header— At most one. If multiple are provided, only the first is rendered.Layout.Column— 1–3 columns use position-based or area-based width templates. 4+ columns use equal-width (repeat(N, 1fr)).areaprop — When any column specifiesarea, all columns switch to area-based widths. Columns withoutareadefault to1fr.- Anything else — Silently filtered out (not rendered).
Width Templates
Position-based (no
area, unchanged from current API)Column Count Responsive Behavior Width Distribution 1 Always stacked vertically Full width 2 < 1024px: stacked / ≥ 1024px: side-by-side 1st flex-1, 2nd fixed 280px 3 < 1280px: stacked / ≥ 1280px: side-by-side 1st fixed 320px, 2nd flex-1, 3rd fixed 280px 4+ < 1280px: stacked / ≥ 1280px: side-by-side All columns equal width ( repeat(N, 1fr))Area-based (with
areaprop)Area Width leftFixed 320px mainflex-1 rightFixed 280px Responsive behavior is the same: 2 columns break at 1024px, 3+ columns break at 1280px.
- Redundant declaration — The column count can be inferred from the number of
@tailor-platform/app-shell@0.28.0
Minor Changes
-
4cb5295: Replaced
getAuthHeadersForQuerywithauth-public-client0.5.0's built-infetch()method.Breaking change:
EnhancedAuthClient.getAuthHeadersForQuery()has been removed. UseauthClient.fetchinstead, which transparently handles DPoP proof generation and token refresh.Migration:
const urqlClient = createClient({ url: `${authClient.getAppUri()}/query`, - fetchOptions: async () => { - const headers = await authClient.getAuthHeadersForQuery(); - return { headers }; - }, + fetch: authClient.fetch, });
@tailor-platform/app-shell@0.27.3
Patch Changes
- 10cb588: Remove horizontal padding in Layout component
- 24f8eb1: Updated es-toolkit (^1.44.0 -> ^1.45.1)
@tailor-platform/app-shell@0.27.2
Patch Changes
- b01718f: Updated graphql (^16.12.0 -> ^16.13.0)
- ed5f14f: Fixed incorrect
typespath inpackage.jsonexports. The"."entry was pointing to./dist/index.d.tswhich does not exist. Updated to./dist/app-shell.d.tsto match the actual build output. - d0dc61b: Reorganized README and documentation structure for public-facing clarity. Added
docs/quickstart.md, created NPM-facingpackages/core/README.mdwith motivation and feature highlights, and ensured each documentation layer (root README, package READMEs, docs/) serves a distinct purpose.