Skip to content

Releases: tailor-platform/app-shell

@tailor-platform/app-shell@0.33.0

03 Apr 02:32
f6188a6

Choose a tag to compare

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.

    // 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:

    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 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)

@tailor-platform/app-shell@0.32.0

31 Mar 01:40
03b7902

Choose a tag to compare

Minor Changes

  • 3c1e939: Add ActivityCard APIs 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 an indicator render a default timeline node. The indicator prop accepts any ReactNode (avatars, icons, etc.).

  • 3c1e939: Add Avatar (Base UI): Avatar.Root, Avatar.Image, and Avatar.Fallback with size variants (sm, default, lg) and exported avatarVariants and AvatarProps. ActivityCard uses this shared avatar; export ActivityCardItem from 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 xs size variant to Button component — 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 Card compound component (Card.Root, Card.Header, Card.Content) as a general-purpose container with consistent styling. Existing card-style components (DescriptionCard, MetricCard, ActionPanel) now use Card internally.

    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, and Field components for building validated forms.

    Form

    A form element with consolidated error handling and validation. Supports onFormSubmit for type-safe parsed form values, and onSubmit for native FormEvent access. External errors (e.g. API responses) can be fed via the errors prop and are automatically routed to matching Field.Error components.

    <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>

    Fieldset

    A 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>

    Field

    A 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.Root creates 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 inside Field.Root automatically connect to this context — inheriting label association (htmlFor), aria-describedby, disabled state, and validation state (invalid, dirty, touched).

    Field.Control is a styled <input> that shares its base styles with the Input component. 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.Control can 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.Validity

    Field.Validity exposes the field's ValidityState via 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.Root accepts isTouched, isDirty, invalid, and error props that align with RHF's fieldState shape, so you can spread fieldState directly. Use Form's onSubmit prop to connect RHF's handleSubmit.

    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 MetricCard component 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 a component (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: dark to the .dark selector in theme CSS so that native form controls (e.g. <select>) render correctly in dark mode on Windows.
  • ae37125: Fix breadcrumbTitle not being propagated in file-based routing. The breadcrumbTitle set in AppShellPageProps.meta is now correctly reflected in breadcrumbs, matching the behavior of the defineResource API.
  • 49e2e3a: Fix DefaultSidebar not applying active style when basePath is 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-0 to SidebarInset so that nested content areas can scroll independently instead of causing the entire page to scroll.

@tailor-platform/app-shell@0.31.1

24 Mar 05:22
0ac0811

Choose a tag to compare

Patch Changes

@tailor-platform/app-shell-vite-plugin@0.2.0

23 Mar 08:27
9e016d7

Choose a tag to compare

Minor Changes

  • 01a2249: Add entrypoint option to appShellRoutes() 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

19 Mar 08:12
f500b83

Choose a tag to compare

Minor Changes

  • e7a1177: Add Menu, Select, Combobox, and Autocomplete components.

    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 via Menu.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 items and get a fully assembled select out of the box. Also supports async data fetching via Select.Async.

    <Select
      items={["Apple", "Banana", "Cherry"]}
      placeholder="Pick a fruit"
      onValueChange={(value) => console.log(value)}
    />

    Combobox

    Searchable combobox with single/multi selection. Pass items and get a fully assembled combobox with built-in filtering. Supports async data fetching via Combobox.Async and user-created items via onCreateItem prop.

    <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 .Parts

    Select, Combobox, and Autocomplete each expose a .Parts namespace 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

@tailor-platform/app-shell@0.30.0

17 Mar 02:09
0668737

Choose a tag to compare

Minor Changes

  • a8c5dcf: Export primitive UI components (Button, Input, Table, Dialog, Sheet, Tooltip) and update @base-ui/react to 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, and Layout.Column now accept an optional style prop 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

16 Mar 07:00
5710c1d

Choose a tag to compare

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 via defineModule()/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.tsx from 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 return pass(), the route falls back to a 404.

  • 565ae70: Add Layout.Header sub-component and area prop to Layout.Column.

    Motivation

    The current Layout component requires a columns prop that must exactly match the number of Layout.Column children, 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.Column children, making the columns prop 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 responsibilitiesLayout handles both page heading (title/actions) and column arrangement. Separating these into Layout.Header and Layout.Column makes each concern independently composable — for example, placing tabs between the header and columns.

    What's New

    • Layout.Header: Compose inside Layout for title, actions, and extra content (e.g. tabs) above columns.
    • Layout.Column area prop: Declare column roles (left, main, right) for area-based width templates, enabling layouts like left sidebar + main that were previously not possible.
    • columns and gap props are deprecated: Column count is auto-detected from children; use className for gap.
    • No runtime breaking changes — existing code continues to work as-is. Note: columns is now optional, so code that directly reads LayoutProps["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.Column Retained area?: "left" | "main" | "right", className?, children?
    Layout.Header New title?, actions?: ReactNode[], children?: ReactNode

    Children 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)).
    • area prop — When any column specifies area, all columns switch to area-based widths. Columns without area default to 1fr.
    • 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 area prop)

    Area Width
    left Fixed 320px
    main flex-1
    right Fixed 280px

    Responsive behavior is the same: 2 columns break at 1024px, 3+ columns break at 1280px.

@tailor-platform/app-shell@0.28.0

10 Mar 08:44
aca5b99

Choose a tag to compare

Minor Changes

  • 4cb5295: Replaced getAuthHeadersForQuery with auth-public-client 0.5.0's built-in fetch() method.

    Breaking change: EnhancedAuthClient.getAuthHeadersForQuery() has been removed. Use authClient.fetch instead, 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

09 Mar 06:24
4b72d72

Choose a tag to compare

Patch Changes

@tailor-platform/app-shell@0.27.2

04 Mar 04:30
5ebd7fd

Choose a tag to compare

Patch Changes

  • b01718f: Updated graphql (^16.12.0 -> ^16.13.0)
  • ed5f14f: Fixed incorrect types path in package.json exports. The "." entry was pointing to ./dist/index.d.ts which does not exist. Updated to ./dist/app-shell.d.ts to match the actual build output.
  • d0dc61b: Reorganized README and documentation structure for public-facing clarity. Added docs/quickstart.md, created NPM-facing packages/core/README.md with motivation and feature highlights, and ensured each documentation layer (root README, package READMEs, docs/) serves a distinct purpose.