Releases: tailor-platform/app-shell
@tailor-platform/app-shell@1.2.0
Minor Changes
-
8a09b26: Add
align: "left" | "right"toDataTableColumn. When set to"right", the header label, body cell, and loading-skeleton bar are right-aligned together — eliminating the inline<span className="text-right">wrappers callers were adding insiderenderfor numeric columns (Amount, Score, Total).alignis auto-defaulted to"right"fortype: "number"andtype: "money"so the common case Just Works without extra config. Other types default to"left". Pass"left"explicitly to opt a numeric column out.// Auto-aligned right — no `align` needed column({ label: "Total", type: "money", accessor: (row) => row.total, typeOptions: { currency: "USD" }, }); // Explicit alignment for a custom-render column column({ label: "Amount", align: "right", render: (row) => formatMoney(row.amount), });
-
b644bdb: Add
truncate: booleantoDataTableColumn. When set, the cell content is truncated with an ellipsis on overflow, and an app-shell<Tooltip>is auto-wired to reveal the full value on hover when the cell value is a stringifiable primitive. The tooltip resolves through the same precedence rule the built-intyperenderers use —accessorfirst, thenrow[col.id]— soinferColumnsconsumers get the tooltip for free without an explicit accessor. Pairtruncatewithwidthon neighboring columns to anchor row width, since truncate cells usemax-w-0to stay shrinkable.column({ label: "Description", render: (row) => row.description, accessor: (row) => row.description, truncate: true, }); // Or with `inferColumns`, no explicit `accessor` needed — the inferred // column pins `id` to the field name so the tooltip resolves automatically: column({ ...infer("description"), truncate: true });
inferColumnsnow also pinsidto the metadata field name (previously omitted). This makes the cell renderer'srow[col.id]fallback resolve cleanly and stabilizes the React key / column-visibility identifier across re-renders. -
4c89923: Add
defaultOpenandcollapsibleprops toSidebarLayoutfor controlling sidebar behavior.// Sidebar closed by default on desktop <SidebarLayout defaultOpen={false} /> // Non-collapsible sidebar (always visible, toggle buttons hidden) <SidebarLayout collapsible={false} />
-
c4fbfa2: Add
typeandtypeOptionstoDataTableColumnfor built-in cell rendering. Settypetotext,number,money,date,badge, orlinkto skip writing arenderfunction for the common cases.renderstays required for untyped columns and becomes an optional override whentypeis set.Column<TRow>is a discriminated union ontype, so wrong-shape options are a compile error rather than silently ignored at runtime — andtype: "link"requirestypeOptions.href.column({ label: "Total", accessor: (row) => row.total, type: "money", typeOptions: { currency: "USD" }, }); column({ label: "Status", accessor: (row) => row.status, type: "badge", typeOptions: { badgeVariantMap: { active: "success", draft: "neutral" }, badgeLabelMap: { active: "Active", draft: "Draft" }, }, });
Patch Changes
-
c4fbfa2: Narrow
Column.accessor's return type per built-intypeso the typed cell renderers reject values they can't display. Returning an array or a plain object from atext/number/money/date/badge/linkaccessor is now a compile error instead of silently rendering[object Object]or a stringified list.nullandundefinedare still allowed and continue to render the—placeholder. Columns without atyperetain the looseunknownreturn type — they pair withrenderto draw whatever shape they like.column({ label: "Tags", type: "text", // ^ compile error: text accessor cannot return an array. accessor: (row) => row.tags, });
-
eecff8e: Add
subtle-success,subtle-warning, andsubtle-errorbadge variants for low-emphasis status labels.<Badge variant="subtle-success">Matched</Badge> <Badge variant="subtle-warning">Needs Attention</Badge> <Badge variant="subtle-error">Needs Review</Badge>
@tailor-platform/app-shell@1.1.1
Patch Changes
- d4d15f5: Add
DescriptionCardbadge field support formeta.sentenceCaseBadges = falseso apps can opt out of the default sentence-case badge labels.
@tailor-platform/app-shell@1.1.0
Minor Changes
-
9654369: Fix root page (
/) showing"/"as its title inSidebarItemand breadcrumb.The root page is now treated as a first-class page (module) so that title, icon, and guards are resolved consistently.
DefaultSidebarandCommandPalettenow include the root page when it is defined. When no title is set, the fallback is localized"Home"/"ホーム". -
39a8521: Change
useDataTable()to use single-column sorting by default and add asortoption for configuring sorting behavior.Before:
const table = useDataTable({ columns, data, control, });
After:
const table = useDataTable({ columns, data, control, sort: { multiple: true }, });
Use
sort: falseto disable sorting entirely. -
c2a50b9: Add row count and selection info to
DataTable.Pagination.- When
totalis provided: shows"X row(s)" - When rows are selected with
total: shows"Y of X row(s) selected" - When rows are selected without
total: shows"Y row(s) selected"
- When
-
642aa1e: Add case-sensitivity control for string filters in
DataTable.Filters. String filters are now case-insensitive by default (using Tailor Platform'sregexoperator with(?i)prefix). A "Case sensitive" checkbox allows users to opt into exact-case matching.The
Filtertype andCollectionControl.addFilternow accept an optionalcaseSensitiveproperty to control this behavior programmatically. -
a3d0170: Add "between" filter mode to
DataTable.Filtersfor numeric and date/time columns, allowing users to filter by a range with min and max bounds.
Patch Changes
-
2a860d9: Fix DataTable filter types for
datetimeandtimefields. Previously these were incorrectly mapped to thedatefilter type, causing wrong input formats. Each temporal type now uses its proper HTML input type (datetime-local,date,time) and format handling. -
125aee2: Fix a
Select.Asyncbug where reopening the dropdown after the first async load could leave the popup invisible while the page stayed scroll-locked.This could happen after options were fetched once, the dropdown was closed, and then opened again. The fix cancels in-flight requests on close and avoids the Base UI modal and anchored alignment paths that were leaving the async popup in that broken reopen state.
-
681333f: Remove
next-themesdependency by using the internalThemeProvidercontext for the Sonner toast theming. -
826bd97: Updated @base-ui/react (^1.3.0 -> ^1.4.1)
@tailor-platform/app-shell@1.0.2
@tailor-platform/app-shell@1.0.1
Patch Changes
-
598bc90: Fix cursor-based pagination in
DataTable.- Fix "Previous" button not working correctly when the GraphQL server returns unreliable
hasPreviousPage(common withfirst + afterqueries per the Relay spec) - Fix navigating back after jumping to the last page incorrectly returning to page 1
- Fix "Next" button remaining enabled past the last page when
totalis known
- Fix "Previous" button not working correctly when the GraphQL server returns unreliable
-
4aaf5ab: Fix "Go to Last Page" pagination alignment. When total items aren't evenly divisible by page size,
goToLastPagenow requestslast: total % pageSizeinstead oflast: pageSize, so the last page boundaries match forward pagination.
@tailor-platform/app-shell-vite-plugin@0.2.2
@tailor-platform/app-shell@1.0.0
Major Changes
-
3f31e8a: Add
DataTablecompound component. Also introduces@tailor-platform/app-shell-sdk-plugin— a companion SDK plugin that generatestableMetadatafrom TailorDB type definitions for use withcreateColumnHelper.DataTable
- Sortable columns (click header to cycle Asc → Desc → off)
- Filter chips with per-type editors (string, number, date, enum, boolean, uuid)
- Cursor-based pagination with optional total-aware First/Last navigation
- Per-row action menu (kebab menu)
- Multi-row checkbox selection (current page only)
- Loading, error, and empty states
- Metadata-driven column inference via
createColumnHelperandinferColumns
Example with urql (Relay Cursor Connection GraphQL API)
import { gql, useQuery } from "urql"; import { DataTable, useDataTable, useCollectionVariables, createColumnHelper, } from "@tailor-platform/app-shell"; const LIST_JOURNALS = gql` query ListJournals( $after: String $before: String $first: Int $last: Int $order: [JournalOrderInput] $query: JournalQueryInput ) { journals( after: $after before: $before first: $first last: $last order: $order query: $query ) { edges { node { id contents authorID } } pageInfo { endCursor hasNextPage hasPreviousPage startCursor } total } } `; const { column } = createColumnHelper<{ id: string; contents: string; authorID: string; }>(); const columns = [ column({ field: "id", label: "ID", type: "uuid" }), column({ field: "authorID", label: "Author", type: "string" }), column({ field: "contents", label: "Contents", type: "string" }), ]; function JournalsPage() { // variables: { query, order, pagination } — maps directly to GraphQL variables. // control: holds filter/sort/pagination state and methods (addFilter, setSort, nextPage, …). // Passing it to useDataTable wires UI interactions (column clicks, filter chips, // pagination buttons) to state updates, which re-derive variables and re-run the query. const { variables, control } = useCollectionVariables({ params: { pageSize: 20 }, }); // pagination holds { first, after? } (forward) or { last, before? } (backward). const [result] = useQuery({ query: LIST_JOURNALS, variables: { first: variables.pagination.first, after: variables.pagination.after, last: variables.pagination.last, before: variables.pagination.before, query: variables.query, order: variables.order, }, }); const table = useDataTable({ columns, data: result.data ? { rows: result.data.journals.edges.map((e) => e.node), pageInfo: result.data.journals.pageInfo, total: result.data.journals.total, } : undefined, loading: result.fetching, control, }); // DataTable.Root + DataTable.Table are the only required sub-components. // DataTable.Toolbar / DataTable.Filters / DataTable.Pagination are opt-in sensible defaults. // If they don't fit, use useDataTableContext() to build your own sub-components — // it exposes the full DataTable state (rows, columns, sort, pagination, selection, etc.) // from the nearest DataTable.Root. return ( <DataTable.Root value={table}> <DataTable.Toolbar> <DataTable.Filters /> </DataTable.Toolbar> <DataTable.Table /> <DataTable.Footer> <DataTable.Pagination pageSizeOptions={[10, 20, 50]} /> </DataTable.Footer> </DataTable.Root> ); }
useCollectionVariablesis intentionally decoupled from DataTable and any other UI component. The hook owns only the query state and exposes plainvariables— how those variables are rendered is entirely up to the consumer. This means future collection-based views such as Kanban boards can adopt the same hook without modification, and any custom component you build can use a GraphQL cursor-based API as its backend with minimal wiring.sdk-plugin (
@tailor-platform/app-shell-sdk-plugin)tableMetadatais what bridges your TailorDB schema to the DataTable. It tellsinferColumnshow to render and filter each field — for example, which fields get a date picker, which get an enum dropdown (and with what options), and which are numeric. Without it, you would need to declare all of this manually per column.The metadata is generated at SDK code-gen time from your TailorDB type definitions. Register the plugin in
tailor.config.tsand runtailor-sdk generate:import { definePlugins } from "@tailor-platform/sdk"; import { appShellPlugin } from "@tailor-platform/app-shell-sdk-plugin"; export const plugins = definePlugins( appShellPlugin({ dataTable: { metadataOutputPath: "src/generated/app-shell-datatable.generated.ts", }, }) );
The generated file exports
tableMetadata,tableNames, andTableName. PasstableMetadatatoinferColumnsto get type-safe column definitions with filter editors automatically configured:import { tableMetadata } from "@/generated/app-shell-datatable.generated"; import { createColumnHelper } from "@tailor-platform/app-shell"; const { column, inferColumns } = createColumnHelper<Order>(); const infer = inferColumns(tableMetadata.order); const columns = [ column(infer("title")), // string column → text filter column(infer("status")), // enum column → dropdown filter with generated values column(infer("createdAt")), // datetime column → date picker filter ];
Typed query variables with
tableMetadataWhen using typed GraphQL documents (e.g.
TypedDocumentNodefrom@graphql-typed-document-node/coreor codegen-generated types), urql and other GraphQL clients enforce strict types on thevariablesobject passed touseQuery. In that case, passingtableMetadatatouseCollectionVariablesis required — it is what narrowsvariables.queryandvariables.orderfromunknownto the precise types expected by the generated document.Without
tableMetadata,variables.queryis typed asRecord<string, Record<string, unknown>> | undefined, which will not satisfy the stricter generated variable types and will cause a TypeScript error at theuseQuerycall site.Use
sdk-pluginto generatetableMetadataand pass it touseCollectionVariables:const { variables, control } = useCollectionVariables({ tableMetadata: tableMetadata.order, // required for typed documents params: { pageSize: 20 }, }); // variables.query is now BuildQueryVariables<typeof tableMetadata.order> // variables.order is now { field: OrderableFieldName; direction: "Asc" | "Desc" }[] // Both satisfy the types generated by GraphQL codegen. const [result] = useQuery({ query: LIST_ORDERS, // TypedDocumentNode — variables are fully type-checked variables: { ...variables.pagination, query: variables.query, order: variables.order, }, });
@tailor-platform/app-shell-sdk-plugin@0.1.0
0.1.0
Minor Changes
-
3f31e8a: Initial release of
@tailor-platform/app-shell-sdk-plugin.A companion SDK plugin that generates
tableMetadatafrom TailorDB type definitions for use with@tailor-platform/app-shell'sDataTablecomponent andcreateColumnHelper.Register the plugin in
tailor.config.ts:import { definePlugins } from "@tailor-platform/sdk"; import { appShellPlugin } from "@tailor-platform/app-shell-sdk-plugin"; export const plugins = definePlugins( appShellPlugin({ dataTable: { metadataOutputPath: "src/generated/app-shell-datatable.generated.ts", }, }) );
Then run
tailor-sdk generateto produce the metadata file, and passtableMetadatatoinferColumns:import { tableMetadata } from "@/generated/app-shell-datatable.generated"; import { createColumnHelper } from "@tailor-platform/app-shell"; const { column, inferColumns } = createColumnHelper<Order>(); const infer = inferColumns(tableMetadata.order); const columns = [ column(infer("title")), // string column → text filter column(infer("status")), // enum column → dropdown filter with generated values column(infer("createdAt")), // datetime column → date picker filter ];
@tailor-platform/app-shell@0.36.0
Minor Changes
-
e2a6f81: Add
Attachmentcomponent anduseAttachmenthook for ERP attachment workflows with drag-and-drop upload, image/file previews, and per-itemDownload/Deleteactions.Use
useAttachmentto manage upload/delete state locally and flush operations to your backend on submit viaapplyChanges. Spread the returnedpropsdirectly onto<Attachment />.import { Attachment, useAttachment } from "@tailor-platform/app-shell"; import type { AttachmentOperation } from "@tailor-platform/app-shell"; const { props, applyChanges } = useAttachment({ initialItems: existingAttachments, accept: "image/*,.pdf", }); async function handleSubmit() { // The component is agnostic to backend shape — run all operations in parallel. await applyChanges((operations) => Promise.all( operations.map((op) => { if (op.type === "upload") return uploadToServer(op.file); if (op.type === "delete") return deleteFromServer(op.item.id); }) ) ); } <Attachment {...props} uploadLabel="Upload" onDownload={handleDownload} />;
@tailor-platform/app-shell@0.35.1
Patch Changes
-
3b11ca4: Fix CommandPalette and DefaultSidebar not showing top-level pages that have no child pages.
When using file-based routing with a flat page structure (e.g.
pages/dashboard/page.tsxwith no sub-pages), those pages were silently excluded from the CommandPalette and the DefaultSidebar auto-generation. They now appear correctly as navigable entries. -
0d8d87e: Add top-padding in Card component"
-
8c0eed5: Adjust
Tablecell padding so the first column uses 24px left inset and the last column uses 24px right inset (middle columns unchanged). UpdateCard.Headertitles totext-lgto align withDescriptionCard,ActivityCard, andActionPanel. -
e841014: Enable richColors in toast