From 3939c87c61848b0bb08544d1db72d0158e9810c0 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 19 Aug 2026 15:38:32 +1000 Subject: [PATCH 1/4] docs(data-table): propose reworked Toolbar layout API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents a rewritten `DataTable.Toolbar` surface as though it ships today, so the team can review the API by reading it rather than by reading a diff: - `showFilters` / `showColumnSettings` for default placement, with `DataTable.Filters` / `DataTable.ColumnSettings` as the placement escape hatches, under one rule: boolean prop = default, sub-component = custom. - `DataTable.ToolbarRow` for horizontal rows and multi-row toolbars, with an `endSection` for right-aligned content, plus `DataTable.Separator`. - `columnSettings` deprecated in favour of `showColumnSettings`. - Wrapping bare children in an implicit row — the flex-col to flex-row default flip — is deferred to the next major and called out inline. Open questions are kept in the doc for review and come out before any code lands. No implementation yet. Refs: tailor-inc/platform-planning#1699 Refs: tailor-professional-service/knowledge#350 --- docs/components/data-table-toolbar.md | 179 ++++++++++++++++++++++++++ docs/components/data-table.md | 32 ++--- 2 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 docs/components/data-table-toolbar.md diff --git a/docs/components/data-table-toolbar.md b/docs/components/data-table-toolbar.md new file mode 100644 index 00000000..dab0aa0f --- /dev/null +++ b/docs/components/data-table-toolbar.md @@ -0,0 +1,179 @@ +--- +title: DataTable Toolbar +description: Toolbar layout for DataTable — built-in filter and column-settings controls, multi-row layouts, aligned sections, and placement escape hatches +--- + +# DataTable Toolbar + +`DataTable.Toolbar` is the strip above the table that holds filters, search, and actions. It handles two jobs: + +- **Built-in controls.** Boolean props put the filter and column-settings controls in their conventional positions, so the common toolbar is one line of JSX. +- **Custom content.** Rows and sections lay out your own components — search boxes, view switchers, export buttons — without fighting the toolbar's internal styles. + +The rule that decides which to reach for: + +> **Boolean prop = default placement. Sub-component = custom placement.** Never both for the same control. + +## Import + +```tsx +import { DataTable } from "@tailor-platform/app-shell"; +``` + +## Basic usage + +Pass `showFilters` and `showColumnSettings` and the toolbar assembles itself — the **Add filter** trigger and chips on the left, the **Columns** control pinned right: + +```tsx + + + + +``` + +Add your own content as children. It sits alongside the built-in controls: + +```tsx + + + + + + +``` + +## Rows + +`DataTable.ToolbarRow` lays its children out horizontally with a gap sized for comfortable tap targets. Use it when the toolbar needs more than one line: + +```tsx + + + + + + + + + +``` + +Rows stack vertically in the order given. Each row is independent — a row whose contents wrap (filter chips, for example) grows without pushing the other rows out of alignment. + +### Aligning content to the right + +A row's `endSection` renders at its right-hand edge, with your children filling from the left: + +```tsx + + }> + + + + +``` + +### Separators + +`DataTable.Separator` draws a short vertical rule in the border colour, for visually grouping controls within a row: + +```tsx + + + + + +``` + +## Custom placement + +When a built-in control needs to go somewhere the boolean props don't put it, drop the boolean and place the sub-component yourself. These are the toolbar's escape hatches — reach for them only when default placement doesn't fit, because you take on positioning in exchange. + +| Control | Default placement | Escape hatch | +| --------------- | -------------------- | -------------------------- | +| Filters | `showFilters` | `DataTable.Filters` | +| Column settings | `showColumnSettings` | `DataTable.ColumnSettings` | + +```tsx +// Column settings next to the search box instead of pinned right. + + + + + + + +``` + +Setting the boolean **and** placing the sub-component renders the control twice. In development the toolbar warns and renders only the sub-component. + +`DataTable.Filters` can be split further with its `slot` prop — see [DataTable → `DataTable.Filters` Props](./data-table.md#datatablefilters-props). + +## Props + +### `DataTable.Toolbar` Props + +| Prop | Type | Default | Description | +| -------------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------- | +| `children` | `ReactNode` | — | Toolbar content. Use `DataTable.ToolbarRow` for multi-row layouts. | +| `showFilters` | `boolean` | `false` | Render the **Add filter** trigger and active chips in their default position. Requires `control`. | +| `showColumnSettings` | `boolean` | `false` | Render the **Columns** control (show/hide + reorder + pin) anchored top-right. Persists per-user via `tableId`. | +| `columnSettings` | `boolean` | `false` | **Deprecated** — renamed to `showColumnSettings`. See [Deprecations](#deprecations). | +| `className` | `string` | — | Additional CSS class for the toolbar container. | + +### `DataTable.ToolbarRow` Props + +| Prop | Type | Default | Description | +| ------------ | ----------- | ------- | ----------------------------------------------------- | +| `children` | `ReactNode` | — | Row content, laid out horizontally from the left. | +| `endSection` | `ReactNode` | — | Content aligned to the row's right-hand edge. | +| `gap` | `number` | `2` | Space between children, on the theme's spacing scale. | +| `className` | `string` | — | Additional CSS class for the row. | + +### `DataTable.ColumnSettings` Props + +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | ------------------------------------- | +| `className` | `string` | — | Additional CSS class for the control. | + +### `DataTable.Separator` Props + +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | ---------------------------------- | +| `className` | `string` | — | Additional CSS class for the rule. | + +## Layout defaults + +`DataTable.ToolbarRow` is the only element that sets a direction: it is a horizontal flex row with `gap` spacing and vertically centred items. + +Children passed directly to `DataTable.Toolbar` — without a `ToolbarRow` — currently stack vertically and stretch to full width. This is why `DataTable.Filters` renders on its own line in the split-slot recipe: + +```tsx + +
+ + +
+ +
+``` + +> **Changing in the next major.** Bare children will be wrapped in a single implicit `ToolbarRow`, making them horizontal by default. Toolbars that rely on stacking should wrap each line in its own `DataTable.ToolbarRow` — that is forward-compatible and can be done today. + +## Deprecations + +| Deprecated | Replacement | Removed | +| ---------------- | -------------------- | ---------- | +| `columnSettings` | `showColumnSettings` | Next major | + +`columnSettings` still works and is equivalent to `showColumnSettings`. Passing it logs a one-time development warning. The rename aligns the two built-in controls (`showFilters` / `showColumnSettings`) so the prop name matches its sub-component counterpart. + +## Open questions + +_This section is for review and will be removed before the API ships._ + +1. **`DataTable.Filters` vs. `DataTable.ColumnFilters`.** The proposal named the filters escape hatch `ColumnFilters`, for symmetry with `ColumnSettings`. `DataTable.Filters` already exists and is the escape hatch today, so this doc keeps that name. Renaming buys symmetry at the cost of a deprecation cycle on a component shipped in 1.10.0 — worth it or not? +2. **Gap props.** Only `gap` on `ToolbarRow` is documented here. `rowGap` / `colGap` were proposed too. The underlying reason `className` can't already do this is that `cn()` is bare `twMerge(clsx(...))` with no `astw:` prefix configured, so prefixed utilities never resolve conflicts. Configuring the prefix fixes overrides for **every** component; adding gap props fixes one. Do both, or just the prefix? +3. **`endSection` prop vs. nested sub-component.** The prop matches house style (`DescriptionCard.headerAction`, `Layout.Header.actions`, `Sheet.Header.action`) and is documented here. A `DataTable.ToolbarSection align="start" | "end"` sub-component would take its own `className`, compose conditionally, and allow more than one node without a fragment. Toolbar end sections are usually groups rather than single nodes, which argues for the sub-component. +4. **`DataTable.Separator` vs. the `Separator` primitive.** `packages/core/src/components/separator.tsx` already implements this with an `orientation` prop, but is not exported from `index.ts`. Options: export `Separator` and drop `DataTable.Separator`, or keep the namespaced one as a preset over it. Note the primitive's vertical variant is `h-full w-px`, which collapses in a centred row without an explicit height. +5. **Implicit-row timing.** The default flip is deferred to the next major here. In this repo, 7 of 8 `DataTable.Toolbar` usages pass a single child and are unaffected; the exception is the split-slot recipe above, which wants stacking. A scan of consumer repos would size the real blast radius before committing. diff --git a/docs/components/data-table.md b/docs/components/data-table.md index 1b34e237..def6d29f 100644 --- a/docs/components/data-table.md +++ b/docs/components/data-table.md @@ -144,14 +144,17 @@ function JournalsPage() { `DataTable` is a namespace object. All sub-components read state from `DataTable.Root` via context. -| Sub-component | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `DataTable.Root` | Context provider. Wraps all other sub-components. Required. | -| `DataTable.Table` | Renders the `` with headers and body. Required. | -| `DataTable.Toolbar` | Container for toolbar content (e.g. filters). Optional. Pass `columnSettings` to render the built-in "Columns" control (show/hide + reorder + pin) at the top-right. See props below. | -| `DataTable.Filters` | Add-filter panel + active filter chips, auto-generated from column filter configs. Requires `control` from `useCollectionVariables`. | -| `DataTable.Footer` | Footer container for pagination and other footer content. Optional. | -| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`. | +| Sub-component | Description | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DataTable.Root` | Context provider. Wraps all other sub-components. Required. | +| `DataTable.Table` | Renders the `
` with headers and body. Required. | +| `DataTable.Toolbar` | Container for toolbar content. Optional. Pass `showFilters` / `showColumnSettings` for the built-in controls. See [DataTable Toolbar](./data-table-toolbar.md). | +| `DataTable.ToolbarRow` | A horizontal row inside the toolbar, with an optional right-aligned `endSection`. Use several for multi-row toolbars. | +| `DataTable.ColumnSettings` | The "Columns" control (show/hide + reorder + pin) as a placeable sub-component, for when `showColumnSettings` puts it in the wrong place. | +| `DataTable.Separator` | Short vertical rule for grouping controls inside a `DataTable.ToolbarRow`. | +| `DataTable.Filters` | Add-filter panel + active filter chips, auto-generated from column filter configs. Requires `control` from `useCollectionVariables`. | +| `DataTable.Footer` | Footer container for pagination and other footer content. Optional. | +| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`. | ### `DataTable.Root` Props @@ -163,11 +166,8 @@ function JournalsPage() { ### `DataTable.Toolbar` Props -| Prop | Type | Default | Description | -| ---------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `children` | `ReactNode` | — | Toolbar content (e.g. `DataTable.Filters`), laid out on the left. | -| `columnSettings` | `boolean` | `false` | Render the built-in "Columns" control (show/hide + reorder + pin) anchored to the top-right. Persists per-user when `useDataTable` has a `tableId`. | -| `className` | `string` | — | Additional CSS class for the toolbar container. | +The toolbar has its own page — see [DataTable Toolbar](./data-table-toolbar.md) for props, multi-row +layouts, aligned sections, and the placement escape hatches. ### `DataTable.Filters` Props @@ -214,7 +214,7 @@ Row selection is enabled by providing `onSelectionChange` to `useDataTable`. The ## Column pinning, visibility & ordering - **Pin** a column with `pin: "left" | "right"`. Pinned columns stay visible during horizontal scroll; the selection and expand columns auto-pin left and the row-actions column auto-pins right. A subtle shadow appears at the frozen edge once the table is scrolled under it. Sticky offsets are measured from the rendered layout, so a `width` isn't required — but setting `width` on pinned columns is recommended so their size stays stable as content changes. -- **Column settings.** Pass `columnSettings` to `DataTable.Toolbar` to render a built-in "Columns" control — a popover to show/hide columns, reorder them (drag), and change pinning by dragging a column between the **Fixed left**, **Scrollable**, and **Fixed right** zones. It's a toolbar prop (not a composed sub-component) because the control always sits in the same top-right position. +- **Column settings.** Pass `showColumnSettings` to `DataTable.Toolbar` to render a built-in "Columns" control — a popover to show/hide columns, reorder them (drag), and change pinning by dragging a column between the **Fixed left**, **Scrollable**, and **Fixed right** zones. It sits top-right by default; place `DataTable.ColumnSettings` yourself to put it elsewhere. See [DataTable Toolbar](./data-table-toolbar.md). - **Persistence.** Pass a stable, **unique** `tableId` to persist each user's column layout (visibility, order, pinning) to `localStorage` (key `as:data-table:v1:`). This is a per-user preference — it is deliberately **not** stored in the URL like filters/sort/pagination, so it survives reloads and isn't reset by shared/filtered links. Omit `tableId` for in-memory-only layout (state simply isn't persisted). Two tables mounted with the same `tableId` share one storage key and overwrite each other — use a unique id per table (e.g. `:`); a dev-mode warning fires on duplicates. ```tsx @@ -225,7 +225,7 @@ const table = useDataTable({ }); - + ; ``` @@ -347,7 +347,7 @@ A column definition passed to `useDataTable`. `Column` is a discriminated | `render` | `(row: TRow) => ReactNode` | Renders the cell content. Optional — overrides the built-in `type` renderer when set. | | `id` | `string` | Stable identifier for column visibility and React key. Falls back to `label` when omitted. | | `width` | `number` | Fixed column width in pixels. Optional. | -| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via the toolbar's `columnSettings` control). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection and expand columns auto-pin left and the row-actions column auto-pins right. | +| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via the toolbar's column-settings control). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection and expand columns auto-pin left and the row-actions column auto-pins right. | | `align` | `"left" \| "right"` | Horizontal alignment. Defaults to `"right"` for `type: "number"` and `type: "money"`; `"left"` otherwise. Pass `"left"` to opt a numeric column out. | | `truncate` | `boolean` | Truncate overflowing text with an ellipsis. Wires up an app-shell `` automatically when the resolved cell value is a string or number — resolved via `accessor` first, then `row[col.id]` as a fallback — so hovering the cell reveals the full value. With `inferColumns`, no explicit `accessor` is needed because `id` is pinned to the field name. Requires another column to anchor the row width (`width` on a neighbor, or a fixed-size column like selection / row actions). | | `accessor` | _(narrowed per `type`)_ | Extracts the raw value. The return type is narrowed per `type` branch — returning an array is a compile error on all typed columns except `badge`, and returning a plain object is a compile error on all typed columns. Untyped columns (`type` omitted) retain `unknown`. `null` and `undefined` are always allowed. | From fe8f90af86d51f1ffc4e5cbb77436cde06481888 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 19 Aug 2026 16:18:10 +1000 Subject: [PATCH 2/4] docs(data-table): address review on the Toolbar proposal - State that toolbar children are left-aligned. - Broaden ToolbarRow's stated purpose to cover single-row horizontal layout, not just multi-row toolbars. - Point `showColumnSettings` at the `columnSettings` deprecation inline. - Add `row` / `col` on `DataTable.Toolbar`: `row` wraps children in a single ToolbarRow today, `col` is the no-op-today opt-out that survives the flip when row becomes the default. Both give a forward-compatible migration. - Document the `className` contract: classes land on a wrapper carrying no app-shell classes, so they never compete with `astw:` internals or depend on stylesheet order. Internal layout stays prop-driven, since a wrapper can't reach inside. - Rework the styling open question around that contract. Refs: tailor-inc/platform-planning#1699 --- docs/components/data-table-toolbar.md | 94 ++++++++++++++++++++------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/docs/components/data-table-toolbar.md b/docs/components/data-table-toolbar.md index dab0aa0f..cc543c97 100644 --- a/docs/components/data-table-toolbar.md +++ b/docs/components/data-table-toolbar.md @@ -31,7 +31,7 @@ Pass `showFilters` and `showColumnSettings` and the toolbar assembles itself — ``` -Add your own content as children. It sits alongside the built-in controls: +Add your own content as children. It is left-aligned, and sits alongside the built-in controls: ```tsx @@ -44,7 +44,7 @@ Add your own content as children. It sits alongside the built-in controls: ## Rows -`DataTable.ToolbarRow` lays its children out horizontally with a gap sized for comfortable tap targets. Use it when the toolbar needs more than one line: +`DataTable.ToolbarRow` lays its children out horizontally with a gap sized for comfortable tap targets. Use it to place children side by side in a single row with sensible spacing, or to give the toolbar more than one line: ```tsx @@ -113,40 +113,42 @@ Setting the boolean **and** placing the sub-component renders the control twice. ### `DataTable.Toolbar` Props -| Prop | Type | Default | Description | -| -------------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------- | -| `children` | `ReactNode` | — | Toolbar content. Use `DataTable.ToolbarRow` for multi-row layouts. | -| `showFilters` | `boolean` | `false` | Render the **Add filter** trigger and active chips in their default position. Requires `control`. | -| `showColumnSettings` | `boolean` | `false` | Render the **Columns** control (show/hide + reorder + pin) anchored top-right. Persists per-user via `tableId`. | -| `columnSettings` | `boolean` | `false` | **Deprecated** — renamed to `showColumnSettings`. See [Deprecations](#deprecations). | -| `className` | `string` | — | Additional CSS class for the toolbar container. | +| Prop | Type | Default | Description | +| -------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `children` | `ReactNode` | — | Toolbar content, left-aligned. Use `DataTable.ToolbarRow` for explicit rows. | +| `showFilters` | `boolean` | `false` | Render the **Add filter** trigger and active chips in their default position. Requires `control`. | +| `showColumnSettings` | `boolean` | `false` | Render the **Columns** control (show/hide + reorder + pin) anchored top-right. Persists per-user via `tableId`. Replaces `columnSettings` — see [Deprecations](#deprecations). | +| `row` | `boolean` | `false` | Wrap all children in a single `DataTable.ToolbarRow`, laying them out horizontally. See [Layout defaults](#layout-defaults). | +| `col` | `boolean` | `false` | Stack children vertically. This is the current default, so passing it changes nothing today — it is the forward-compatible way to keep stacking once `row` becomes the default. | +| `columnSettings` | `boolean` | `false` | **Deprecated** — renamed to `showColumnSettings`. See [Deprecations](#deprecations). | +| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | ### `DataTable.ToolbarRow` Props -| Prop | Type | Default | Description | -| ------------ | ----------- | ------- | ----------------------------------------------------- | -| `children` | `ReactNode` | — | Row content, laid out horizontally from the left. | -| `endSection` | `ReactNode` | — | Content aligned to the row's right-hand edge. | -| `gap` | `number` | `2` | Space between children, on the theme's spacing scale. | -| `className` | `string` | — | Additional CSS class for the row. | +| Prop | Type | Default | Description | +| ------------ | ----------- | ------- | ---------------------------------------------------------------------------- | +| `children` | `ReactNode` | — | Row content, laid out horizontally from the left. | +| `endSection` | `ReactNode` | — | Content aligned to the row's right-hand edge. | +| `gap` | `number` | `2` | Space between children, on the theme's spacing scale. | +| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | ### `DataTable.ColumnSettings` Props -| Prop | Type | Default | Description | -| ----------- | -------- | ------- | ------------------------------------- | -| `className` | `string` | — | Additional CSS class for the control. | +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | ---------------------------------------------------------------------------- | +| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | ### `DataTable.Separator` Props -| Prop | Type | Default | Description | -| ----------- | -------- | ------- | ---------------------------------- | -| `className` | `string` | — | Additional CSS class for the rule. | +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | ---------------------------------------------------------------------------- | +| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | ## Layout defaults `DataTable.ToolbarRow` is the only element that sets a direction: it is a horizontal flex row with `gap` spacing and vertically centred items. -Children passed directly to `DataTable.Toolbar` — without a `ToolbarRow` — currently stack vertically and stretch to full width. This is why `DataTable.Filters` renders on its own line in the split-slot recipe: +Children passed directly to `DataTable.Toolbar` — without a `ToolbarRow` — stack vertically and stretch to full width. This is why `DataTable.Filters` renders on its own line in the split-slot recipe: ```tsx @@ -158,7 +160,51 @@ Children passed directly to `DataTable.Toolbar` — without a `ToolbarRow` — c ``` -> **Changing in the next major.** Bare children will be wrapped in a single implicit `ToolbarRow`, making them horizontal by default. Toolbars that rely on stacking should wrap each line in its own `DataTable.ToolbarRow` — that is forward-compatible and can be done today. +### `row` and `col` + +For the common case — a handful of controls side by side — `row` wraps every child in a single `ToolbarRow` for you, so you don't have to nest one by hand: + +```tsx +// These two are equivalent. + + + + + + + + + + + +``` + +`col` is the opposite instruction: stack the children. It matches today's default, so adding it changes nothing right now. + +`row` is ignored when any child is already a `DataTable.ToolbarRow` — wrapping rows in a row would lay them side by side — and passing both `row` and `col` is a mistake; both cases log a development warning, and `row` takes precedence over `col`. + +> **Changing in the next major.** Bare children will be wrapped in an implicit `ToolbarRow`, making `row` the default. Two forward-compatible moves you can make today: +> +> - **Want horizontal?** Pass `row` now, and drop it after the major. +> - **Relying on stacking?** Pass `col` now. It is a no-op today and preserves your layout through the flip. + +## Styling + +`className` is applied to an **outer wrapper element that carries no app-shell classes of its own**. Your classes therefore never compete with the component's internal styles, and never depend on stylesheet import order to win. + +The trade-off is that `className` styles the box the component sits in, not the component's internals. Use it for the outside — margin, width, background, borders: + +```tsx + +``` + +Internal layout is controlled by props, not classes, because a wrapper cannot reach inside: + +| To change | Use | +| ----------------------------------- | -------------------------------------- | +| Direction of the toolbar's children | `row` / `col` on `DataTable.Toolbar` | +| Space between a row's children | `gap` on `DataTable.ToolbarRow` | +| Right-alignment of a group | `endSection` on `DataTable.ToolbarRow` | ## Deprecations @@ -173,7 +219,7 @@ Children passed directly to `DataTable.Toolbar` — without a `ToolbarRow` — c _This section is for review and will be removed before the API ships._ 1. **`DataTable.Filters` vs. `DataTable.ColumnFilters`.** The proposal named the filters escape hatch `ColumnFilters`, for symmetry with `ColumnSettings`. `DataTable.Filters` already exists and is the escape hatch today, so this doc keeps that name. Renaming buys symmetry at the cost of a deprecation cycle on a component shipped in 1.10.0 — worth it or not? -2. **Gap props.** Only `gap` on `ToolbarRow` is documented here. `rowGap` / `colGap` were proposed too. The underlying reason `className` can't already do this is that `cn()` is bare `twMerge(clsx(...))` with no `astw:` prefix configured, so prefixed utilities never resolve conflicts. Configuring the prefix fixes overrides for **every** component; adding gap props fixes one. Do both, or just the prefix? +2. **The `className` wrapper contract.** Applying `className` to a bare wrapper (see [Styling](#styling)) avoids conflicts with `astw:`-prefixed internals entirely, which configuring a `twMerge` prefix does not — a consumer writing unprefixed `gap-4` still collides with `astw:gap-2`, and source order decides. The costs: an extra DOM node per component, and no consumer control of internals, which is what makes `row` / `col` / `gap` first-class props rather than classes. Should this contract apply to every app-shell component or only the toolbar surface? And does `gap` want `rowGap` / `colGap` siblings, or is one knob enough? 3. **`endSection` prop vs. nested sub-component.** The prop matches house style (`DescriptionCard.headerAction`, `Layout.Header.actions`, `Sheet.Header.action`) and is documented here. A `DataTable.ToolbarSection align="start" | "end"` sub-component would take its own `className`, compose conditionally, and allow more than one node without a fragment. Toolbar end sections are usually groups rather than single nodes, which argues for the sub-component. 4. **`DataTable.Separator` vs. the `Separator` primitive.** `packages/core/src/components/separator.tsx` already implements this with an `orientation` prop, but is not exported from `index.ts`. Options: export `Separator` and drop `DataTable.Separator`, or keep the namespaced one as a preset over it. Note the primitive's vertical variant is `h-full w-px`, which collapses in a centred row without an explicit height. 5. **Implicit-row timing.** The default flip is deferred to the next major here. In this repo, 7 of 8 `DataTable.Toolbar` usages pass a single child and are unaffected; the exception is the split-slot recipe above, which wants stacking. A scan of consumer repos would size the real blast radius before committing. From 5485f8ceeb546a252cb7d977decf6a4f7ffe5f74 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 19 Aug 2026 16:29:13 +1000 Subject: [PATCH 3/4] docs(data-table): tighten row/col rules and the className reach - Say explicitly that `row` is skipped when any child is a ToolbarRow, including the all-children-are-rows case (the ordinary multi-row toolbar). - Make `row` / `col` mutually exclusive in the type via a discriminated union, with a runtime warning as the backstop for spreads and JS callers. - Spell out which CSS a wrapper can and cannot reach: box and inherited properties yes, the inner element's flex-direction / gap / align-items no. - Note that `data-slot` stays on the inner elements, so CSS already written against [data-slot="data-table-toolbar"] keeps matching. Refs: tailor-inc/platform-planning#1699 --- docs/components/data-table-toolbar.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/components/data-table-toolbar.md b/docs/components/data-table-toolbar.md index cc543c97..c716bf0e 100644 --- a/docs/components/data-table-toolbar.md +++ b/docs/components/data-table-toolbar.md @@ -181,7 +181,22 @@ For the common case — a handful of controls side by side — `row` wraps every `col` is the opposite instruction: stack the children. It matches today's default, so adding it changes nothing right now. -`row` is ignored when any child is already a `DataTable.ToolbarRow` — wrapping rows in a row would lay them side by side — and passing both `row` and `col` is a mistake; both cases log a development warning, and `row` takes precedence over `col`. +`row` is ignored when **any** child is already a `DataTable.ToolbarRow` — including when every child is one, which is the ordinary multi-row toolbar. Wrapping rows in a row would lay them side by side rather than stacked, so the wrap is skipped and a development warning is logged. + +`row` and `col` are mutually exclusive in the type, so passing both is a compile error: + +```ts +type ToolbarDirectionProps = { row?: boolean; col?: never } | { row?: never; col?: boolean }; +``` + +```tsx + // ok + // ok + // ok — a boolean expression is still one arm + // Type error +``` + +The component also warns at runtime if both arrive anyway — via a spread, or from JavaScript — and treats `row` as the winner. > **Changing in the next major.** Bare children will be wrapped in an implicit `ToolbarRow`, making `row` the default. Two forward-compatible moves you can make today: > @@ -198,7 +213,7 @@ The trade-off is that `className` styles the box the component sits in, not the ``` -Internal layout is controlled by props, not classes, because a wrapper cannot reach inside: +`flex-direction`, `gap`, and `align-items` on the wrapper cannot change how children are arranged — those belong to the inner element that actually contains them. A wrapper can set the component's own box (`margin`, `width`, `position`, `overflow`, `background`, `border`) and anything inherited (`color`, `font-*`). Internal layout is controlled by props instead: | To change | Use | | ----------------------------------- | -------------------------------------- | @@ -206,6 +221,9 @@ Internal layout is controlled by props, not classes, because a wrapper cannot re | Space between a row's children | `gap` on `DataTable.ToolbarRow` | | Right-alignment of a group | `endSection` on `DataTable.ToolbarRow` | +`data-slot` attributes stay on the inner elements rather than the wrapper, so CSS already written +against `[data-slot="data-table-toolbar"]` keeps matching the element it always matched. + ## Deprecations | Deprecated | Replacement | Removed | From fd0a838ddc0a79e28c661de830a8f72b9e01482d Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 19 Aug 2026 16:41:49 +1000 Subject: [PATCH 4/4] docs(data-table): direction enum, and scope the className wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the `row` / `col` booleans with `direction?: "row" | "col"`. One enum is mutually exclusive by construction — no `row col` to diagnose, and no union whose "no overload matches" error says nothing about the actual mistake. Scope the className wrapper by job rather than applying it everywhere: - Placed components (`Filters`, `ColumnSettings`, `Separator`) get a wrapper. They are dropped into a layout the consumer wrote, so positioning and sizing is the need, and a bare wrapper serves it deterministically. It also makes `Separator className="h-6"` work, since the rule itself is `h-full`. - Containers (`Toolbar`, `ToolbarRow`) merge onto the container. The consumer already owns the element these sit in, so a wrapper adds a DOM node without adding capability. - Toolbar's own padding / border-bottom stay honestly documented as unreliable to override, rather than papered over. Also correct the reasoning in the open question: configuring a twMerge prefix cannot fix cross-boundary conflicts. `cn()` dedupes within one class string, so an unprefixed `gap-4` and `astw:gap-2` are unrelated classes that both survive and are resolved by source order either way. Refs: tailor-inc/platform-planning#1699 --- docs/components/data-table-toolbar.md | 101 ++++++++++++++------------ 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/docs/components/data-table-toolbar.md b/docs/components/data-table-toolbar.md index c716bf0e..e835f0fc 100644 --- a/docs/components/data-table-toolbar.md +++ b/docs/components/data-table-toolbar.md @@ -113,36 +113,35 @@ Setting the boolean **and** placing the sub-component renders the control twice. ### `DataTable.Toolbar` Props -| Prop | Type | Default | Description | -| -------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `children` | `ReactNode` | — | Toolbar content, left-aligned. Use `DataTable.ToolbarRow` for explicit rows. | -| `showFilters` | `boolean` | `false` | Render the **Add filter** trigger and active chips in their default position. Requires `control`. | -| `showColumnSettings` | `boolean` | `false` | Render the **Columns** control (show/hide + reorder + pin) anchored top-right. Persists per-user via `tableId`. Replaces `columnSettings` — see [Deprecations](#deprecations). | -| `row` | `boolean` | `false` | Wrap all children in a single `DataTable.ToolbarRow`, laying them out horizontally. See [Layout defaults](#layout-defaults). | -| `col` | `boolean` | `false` | Stack children vertically. This is the current default, so passing it changes nothing today — it is the forward-compatible way to keep stacking once `row` becomes the default. | -| `columnSettings` | `boolean` | `false` | **Deprecated** — renamed to `showColumnSettings`. See [Deprecations](#deprecations). | -| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | +| Prop | Type | Default | Description | +| -------------------- | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `children` | `ReactNode` | — | Toolbar content, left-aligned. Use `DataTable.ToolbarRow` for explicit rows. | +| `showFilters` | `boolean` | `false` | Render the **Add filter** trigger and active chips in their default position. Requires `control`. | +| `showColumnSettings` | `boolean` | `false` | Render the **Columns** control (show/hide + reorder + pin) anchored top-right. Persists per-user via `tableId`. Replaces `columnSettings` — see [Deprecations](#deprecations). | +| `direction` | `"row" \| "col"` | `"col"` | How children are laid out. `"row"` wraps them in a single `DataTable.ToolbarRow`; `"col"` stacks them. See [Layout defaults](#layout-defaults). | +| `columnSettings` | `boolean` | `false` | **Deprecated** — renamed to `showColumnSettings`. See [Deprecations](#deprecations). | +| `className` | `string` | — | Additional CSS class, merged onto the toolbar container. See [Styling](#styling). | ### `DataTable.ToolbarRow` Props -| Prop | Type | Default | Description | -| ------------ | ----------- | ------- | ---------------------------------------------------------------------------- | -| `children` | `ReactNode` | — | Row content, laid out horizontally from the left. | -| `endSection` | `ReactNode` | — | Content aligned to the row's right-hand edge. | -| `gap` | `number` | `2` | Space between children, on the theme's spacing scale. | -| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | +| Prop | Type | Default | Description | +| ------------ | ----------- | ------- | ------------------------------------------------------------------- | +| `children` | `ReactNode` | — | Row content, laid out horizontally from the left. | +| `endSection` | `ReactNode` | — | Content aligned to the row's right-hand edge. | +| `gap` | `number` | `2` | Space between children, on the theme's spacing scale. | +| `className` | `string` | — | Additional CSS class, merged onto the row. See [Styling](#styling). | ### `DataTable.ColumnSettings` Props -| Prop | Type | Default | Description | -| ----------- | -------- | ------- | ---------------------------------------------------------------------------- | -| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | --------------------------------------------------------------------------------------- | +| `className` | `string` | — | Additional CSS class, applied to a wrapper around the control. See [Styling](#styling). | ### `DataTable.Separator` Props -| Prop | Type | Default | Description | -| ----------- | -------- | ------- | ---------------------------------------------------------------------------- | -| `className` | `string` | — | Additional CSS class. Applied to an outer wrapper — see [Styling](#styling). | +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------- | +| `className` | `string` | — | Additional CSS class, applied to a wrapper around the rule — this is also how you set its height. See [Styling](#styling). | ## Layout defaults @@ -160,13 +159,13 @@ Children passed directly to `DataTable.Toolbar` — without a `ToolbarRow` — s ``` -### `row` and `col` +### `direction` -For the common case — a handful of controls side by side — `row` wraps every child in a single `ToolbarRow` for you, so you don't have to nest one by hand: +For the common case — a handful of controls side by side — `direction="row"` wraps every child in a single `ToolbarRow` for you, so you don't have to nest one by hand: ```tsx // These two are equivalent. - + @@ -179,50 +178,56 @@ For the common case — a handful of controls side by side — `row` wraps every ``` -`col` is the opposite instruction: stack the children. It matches today's default, so adding it changes nothing right now. +`direction="col"` is the opposite instruction: stack the children. It matches today's default, so setting it explicitly changes nothing right now. -`row` is ignored when **any** child is already a `DataTable.ToolbarRow` — including when every child is one, which is the ordinary multi-row toolbar. Wrapping rows in a row would lay them side by side rather than stacked, so the wrap is skipped and a development warning is logged. +`direction="row"` is ignored when **any** child is already a `DataTable.ToolbarRow` — including when every child is one, which is the ordinary multi-row toolbar. Wrapping rows in a row would lay them side by side rather than stacked, so the wrap is skipped and a development warning is logged. -`row` and `col` are mutually exclusive in the type, so passing both is a compile error: +> **Changing in the next major.** `direction` will default to `"row"`, wrapping bare children in an implicit `ToolbarRow`. Two forward-compatible moves you can make today: +> +> - **Want horizontal?** Set `direction="row"` now, and drop it after the major. +> - **Relying on stacking?** Set `direction="col"` now. It is a no-op today and preserves your layout through the flip. -```ts -type ToolbarDirectionProps = { row?: boolean; col?: never } | { row?: never; col?: boolean }; -``` +## Styling + +`className` behaves differently depending on whether you are **placing** a component or **composing into** one, because those two jobs need different things. + +### Placed components — `Filters`, `ColumnSettings`, `Separator` + +These are dropped into a layout you wrote, so what you need is to position and size them. Their `className` is applied to a **wrapper element that carries no app-shell classes**, so your classes never compete with the component's internals and never depend on stylesheet order to win: ```tsx - // ok - // ok - // ok — a boolean expression is still one arm - // Type error + + + + + ``` -The component also warns at runtime if both arrive anyway — via a spread, or from JavaScript — and treats `row` as the winner. - -> **Changing in the next major.** Bare children will be wrapped in an implicit `ToolbarRow`, making `row` the default. Two forward-compatible moves you can make today: -> -> - **Want horizontal?** Pass `row` now, and drop it after the major. -> - **Relying on stacking?** Pass `col` now. It is a no-op today and preserves your layout through the flip. +The wrapper is what makes `DataTable.Separator className="h-6"` work at all: the rule itself is `h-full`, so it takes the height of whatever box it is given. -## Styling +### Containers — `Toolbar`, `ToolbarRow` -`className` is applied to an **outer wrapper element that carries no app-shell classes of its own**. Your classes therefore never compete with the component's internal styles, and never depend on stylesheet import order to win. +These hold **your** children, and you already own the element they sit in, so a wrapper would add a DOM node without adding capability. Their `className` is merged onto the container itself. -The trade-off is that `className` styles the box the component sits in, not the component's internals. Use it for the outside — margin, width, background, borders: +That means it can set anything the container does not already set — `margin`, `max-width`, `background`, `position`: ```tsx ``` -`flex-direction`, `gap`, and `align-items` on the wrapper cannot change how children are arranged — those belong to the inner element that actually contains them. A wrapper can set the component's own box (`margin`, `width`, `position`, `overflow`, `background`, `border`) and anything inherited (`color`, `font-*`). Internal layout is controlled by props instead: +`DataTable.Toolbar` sets `padding`, `border-bottom` and `gap` on that element, so classes touching those three are unreliable: your class and app-shell's are both single-class selectors on the same property, so the winner depends on CSS source order rather than on which one you wrote. If you need to change them, say so on the ticket rather than reaching for `!important` — the fix is a prop, not a class. + +### Internal layout is always a prop + +No `className` — merged or wrappered — can change how a component arranges its own children. `flex-direction`, `gap` and `align-items` belong to the element that directly contains those children, and that element is never the one you styled: | To change | Use | | ----------------------------------- | -------------------------------------- | -| Direction of the toolbar's children | `row` / `col` on `DataTable.Toolbar` | +| Direction of the toolbar's children | `direction` on `DataTable.Toolbar` | | Space between a row's children | `gap` on `DataTable.ToolbarRow` | | Right-alignment of a group | `endSection` on `DataTable.ToolbarRow` | -`data-slot` attributes stay on the inner elements rather than the wrapper, so CSS already written -against `[data-slot="data-table-toolbar"]` keeps matching the element it always matched. +`data-slot` attributes stay on the inner elements rather than on any wrapper, so CSS already written against `[data-slot="data-table-toolbar"]` keeps matching the element it always matched. ## Deprecations @@ -237,7 +242,7 @@ against `[data-slot="data-table-toolbar"]` keeps matching the element it always _This section is for review and will be removed before the API ships._ 1. **`DataTable.Filters` vs. `DataTable.ColumnFilters`.** The proposal named the filters escape hatch `ColumnFilters`, for symmetry with `ColumnSettings`. `DataTable.Filters` already exists and is the escape hatch today, so this doc keeps that name. Renaming buys symmetry at the cost of a deprecation cycle on a component shipped in 1.10.0 — worth it or not? -2. **The `className` wrapper contract.** Applying `className` to a bare wrapper (see [Styling](#styling)) avoids conflicts with `astw:`-prefixed internals entirely, which configuring a `twMerge` prefix does not — a consumer writing unprefixed `gap-4` still collides with `astw:gap-2`, and source order decides. The costs: an extra DOM node per component, and no consumer control of internals, which is what makes `row` / `col` / `gap` first-class props rather than classes. Should this contract apply to every app-shell component or only the toolbar surface? And does `gap` want `rowGap` / `colGap` siblings, or is one knob enough? +2. **Where the wrapper applies.** This doc wrappers the placed sub-components and merges on the two containers (see [Styling](#styling)). Worth confirming, along with two consequences. Configuring a `twMerge` prefix does **not** fix cross-boundary conflicts: `cn()` dedupes within one class string, so a consumer's unprefixed `gap-4` and app-shell's `astw:gap-2` are two unrelated classes that both survive and are resolved by source order, whichever way the prefix is configured. And `DataTable.Toolbar`'s own `padding` / `border-bottom` stay unreliable to override under either model — if there is real demand, they want props. Does `gap` also want `rowGap` / `colGap` siblings? 3. **`endSection` prop vs. nested sub-component.** The prop matches house style (`DescriptionCard.headerAction`, `Layout.Header.actions`, `Sheet.Header.action`) and is documented here. A `DataTable.ToolbarSection align="start" | "end"` sub-component would take its own `className`, compose conditionally, and allow more than one node without a fragment. Toolbar end sections are usually groups rather than single nodes, which argues for the sub-component. 4. **`DataTable.Separator` vs. the `Separator` primitive.** `packages/core/src/components/separator.tsx` already implements this with an `orientation` prop, but is not exported from `index.ts`. Options: export `Separator` and drop `DataTable.Separator`, or keep the namespaced one as a preset over it. Note the primitive's vertical variant is `h-full w-px`, which collapses in a centred row without an explicit height. 5. **Implicit-row timing.** The default flip is deferred to the next major here. In this repo, 7 of 8 `DataTable.Toolbar` usages pass a single child and are unaffected; the exception is the split-slot recipe above, which wants stacking. A scan of consumer repos would size the real blast radius before committing.