Skip to content

feat(data-table): redesign Filters — add-filter panel + segmented chips#395

Merged
IzumiSy merged 28 commits into
mainfrom
feat/data-table-filter-panel-variant
Jul 24, 2026
Merged

feat(data-table): redesign Filters — add-filter panel + segmented chips#395
IzumiSy merged 28 commits into
mainfrom
feat/data-table-filter-panel-variant

Conversation

@itsprade

@itsprade itsprade commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Redesigns DataTable.Filters for a faster, more direct filtering experience. This replaces the old popover-with-nested-selects add flow with a single add-filter panel and turns each active filter into a segmented chip.

Supersedes #385. That draft explored a nested-menu add flow (variant A). After trying both, we're shipping the panel (variant B) as the single design and this PR drops the variant-A code.

Claude 2026-07-20 17 46 12

What changed

  • Add-filter panel — one popover laid out in up to three columns: field ▸ condition ▸ value. The condition column appears for fields with more than one operator (number, date/time, string); single-operator fields (enum, uuid) go straight to the value. Values are drafted and committed with an Apply button, and the panel stays open (fixed height, pinned in place) so several filters can be added in a row without it jumping around.
  • Segmented filter chips — each active filter renders as field │ operator │ value │ ✕. The operator segment opens a searchable dropdown to switch the condition; the value segment reopens the type-specific editor; removes the filter.
  • Friendlier operator labels (is, is not, is between, is any of, is none of, …), multi-select enum values summarized as "N items" (e.g. "2 statuses"), compact locale-formatted date ranges, and a single consistent primary-colored checkbox across every filter surface.
  • Date filters use app-shell's own date components — single-date operators (exact date / after / before) render the inline Calendar; the is between range renders two DatePicker From / To fields.

Date range — temporary From/To

The is between date range currently uses two DatePicker From / To fields as a stopgap. Once #388 (DateRangePicker + RangeCalendar) merges, this will be swapped for a single range calendar. The filter value shape ({ min, max }) is unchanged, so that swap is UI-only.

Notes

  • Public API surface is unchanged — <DataTable.Filters /> takes no new props; all changes are internal to the toolbar plus new i18n label keys (en + ja).
  • Incidental shared-component changes made while exploring variant A (Menu anchor-tracking, PositionProps, FilterConfig.chooseOperator) have been reverted to keep this PR focused.
  • Docs (docs/components/data-table.md) and the vite example page are updated for the new flow.

Verification

  • type-check clean · lint 0 warnings / 0 errors · test 1376 passed · build ✓ · oxfmt --check clean
  • Please try the DataTable + Filters page in the vite example to review the interaction.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR redesigns the public DataTable.Filters experience in @tailor-platform/app-shell by replacing the old “add filter” flow with a 3-column add-filter panel and rendering active filters as segmented chips (field / operator / value / remove), while updating i18n labels, docs, and the Vite example to match.

Changes:

  • Replaces the previous add-filter popover with a single pinned AddFilterPanel (field ▸ condition ▸ value) and adds a searchable operator picker in the chip operator segment.
  • Updates chip/value editors (date uses inline Calendar; date ranges use From/To DatePicker stopgap) and adds new label keys + test rewrites for the new interaction model.
  • Updates theme transparency rules, Vite example copy/comments, and DataTable docs; adds a changeset.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/core/src/components/data-table/toolbar.tsx Implements the new add-filter panel + segmented chips, operator search, updated value formatting, and shared checkbox UI.
packages/core/src/components/data-table/toolbar.test.tsx Updates/extends tests to drive the new segmented chip structure and add-filter panel behavior.
packages/core/src/components/data-table/i18n.ts Adds new placeholder labels and updates operator wording (notably in en).
packages/core/src/assets/themes/cream.css Extends “transparent chrome” styling to the new filter chip surface.
packages/core/src/assets/themes/bloom.css Extends “transparent chrome” styling to the new filter chip surface.
examples/vite-app/src/pages/data-table/page.tsx Updates example comments/copy for the new date filter behavior (Calendar vs From/To DatePickers).
docs/components/data-table.md Updates docs to describe the add-filter panel and segmented chips; revises date filter documentation accordingly.
.changeset/data-table-filter-redesign.md Adds a minor changeset describing the redesign and behavior changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +177 to +185
const [operator, setOperator] = useState<FilterOperator>(
config ? DEFAULT_OPERATOR[config.type] : "eq",
);

const selectField = (name: string) => {
setFieldName(name);
const col = columns.find((c) => c.filter.field === name);
if (col) setOperator(DEFAULT_OPERATOR[col.filter.type]);
};
Comment on lines +374 to +379
control.addFilter(
field,
operator,
toAddFilterSubmittedValue(type, operator, text),
type === "string" ? { caseSensitive: false } : undefined,
);
Comment on lines +603 to +620
} else if (nextOp === "between") {
const v = filter.value;
if (config.type === "number") {
const n = typeof v === "number" ? v : Number(v);
control.addFilter(config.field, nextOp, { min: n, max: n });
} else {
const s = v == null ? "" : String(v);
control.addFilter(config.field, nextOp, { min: s, max: s });
}
} else {
const range = (filter.value ?? {}) as { min?: unknown; max?: unknown };
const lower = range.min ?? range.max ?? "";
control.addFilter(
config.field,
nextOp,
config.type === "number" ? Number(lower) : String(lower),
);
}
Comment on lines +1689 to +1692
// Summarize multiple selections as "2 statuses" rather than listing them.
if (labels.length > 1 && label) {
return `${labels.length} ${pluralizeNoun(label)}`;
}
Comment on lines +792 to +794
{items.length === 0 && (
<div className="astw:px-2 astw:py-1.5 astw:text-sm astw:text-muted-foreground">—</div>
)}

@interacsean interacsean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Ideally when the data is invalid, the Apply button would be disabled:
Image

Currently it is enabled but clicking it does nothing

Related: min value can be >= max and nothing complains

It's a little odd to be able to:

  • Add a filter on "Amount" (e.g.)
  • Click "Add filter"
  • Then try to add another filter on "Amount"
  • Doing this updates the existing Amount filter - even though we clicked the "Add filter" button

It may not be obvious it will update the existing filter – the blue dot helps

3a.
For the Time filter, "Between" uses native time inputs, but any of the other operators just use a basic text input

3b.
For DateTime filter, it's just text inputs; I appreciate we're waiting on a DateTimeField / ..Picker but just flagging this is not a good UX and any application already using a DateTime filter may regress. End-customers are unlikely to be able to reliably enter a RFC timestamp

@itsprade
itsprade requested review from IzumiSy and interacsean July 21, 2026 14:20
itsprade and others added 22 commits July 22, 2026 13:10
… chips

- Replace the add-filter popover (nested selects) with a Menu flyout: all
  filterable fields visible on open; number/date/time fields get a condition
  step (field ▸ condition ▸ value), others go straight to value.
- Segmented filter chips: field │ operator │ value │ ✕, with a searchable
  operator dropdown on the operator segment.
- Add `FilterConfig.chooseOperator` (defaults true for number/date/datetime/time,
  false otherwise) to opt a field in/out of the condition step.
- Friendlier operator labels (is / is not / is between / is any of …),
  enum multi-select summarized as "N items", compact date ranges.
- Extract shared `FilterCheckbox` + `EnumOptionList` for a consistent
  primary-colored checkbox across all filter surfaces.
- `Menu.Content` gains `position.trackAnchor` to pin the menu when opening it
  shifts the trigger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second add-filter UI for A/B testing alongside the nested-menu variant:
a single popover with three columns — field ▸ condition ▸ value. Live-commit
(enum toggles, text/number on Enter, date on pick); reuses the shared value
editors and helpers. Gated behind `DataTable.Filters` `addFilterVariant`
("menu" default | "panel"). Example page gets an A/B toggle to compare.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ned, 3 cols for all multi-op fields

Feedback from testing variant B:
- Add an explicit Apply button (draft state per type; panel stays open).
- Fixed popup height so switching field/condition never resizes/jumps.
- disableAnchorTracking so the panel stays put when adding a chip shifts the trigger.
- Show the condition column for every field with >1 operator (e.g. string), not just number/date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er jump)

The popover DatePicker (and an inline Calendar) opened/re-rendered inside the
panel popover, causing it to jump and even dismiss on interaction. Use the
typed segmented DateField instead — no nested popover, stable panel. Reverts
the panel height back to the compact fixed size.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r button

The chip used border-input + flat bg-background, so in dark mode it read flatter
than the outline "Add filter" button. Align its border/background tokens with
the outline Button (border-border / dark:border-input, dark:bg-input/30) so the
pill matches the add-filter container in both themes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ariant A

Consolidate the two-variant filter experiment into the single panel design
(option B) and drop the nested-menu variant A code.

- Remove variant-A pieces from toolbar.tsx (AddFilterMenu, field/operator
  submenus, submenu editors) — DataTableFilters now always renders the
  3-column add-filter panel (field ▸ condition ▸ value, Apply) + segmented chips
- Revert incidental shared-component changes made for variant A: Menu.trackAnchor,
  PositionProps.trackAnchor, and FilterConfig.chooseOperator
- Date filters: single = inline Calendar, range = From/To DatePicker (stopgap
  until the date-range calendar lands)
- Update tests for the panel-only flow
- Rewrite the Filters/FilterConfig docs and refresh the vite example page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add panel now seeds a field's operator/value/caseSensitive from an active
  filter, so re-opening a filtered field and hitting Apply preserves it instead
  of resetting to the default (which could overwrite or remove the filter)
- Localize the enum count summary via a `filterEnumCount` i18n label (en plural,
  ja counter) — no more "2 ステータスs"
- Guard the chip's operator switch against NaN/Infinity when seeding numbers
- Localize the operator-search empty state (`filterOperatorNoResults`) and render
  it as <output> for assistive tech
- Add regression tests for the panel seeding + case-sensitivity preservation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rror, layout

- Reversed "between" range shows an inline error ("Max must be greater than or
  equal to Min") beneath the inputs and keeps the commit disabled
- Add a per-field Clear (icon-only, tooltip) beside Apply/Update and a sticky
  "Clear all filters" at the bottom of the field column (shown only when active)
- Right-align the Add filter trigger (funnel icon) so it stays put as chips are
  added; panel right-aligns to it with a fixed width so it never jumps between
  2- and 3-column fields and fits the inline calendar (now centered)
- Soften the Min/Max label cells (bg-muted) so they aren't near-black in dark mode

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chip polish

- Datetime filter now uses a date DatePicker + native time box (combined into a
  local "YYYY-MM-DDTHH:mm:ss") instead of a raw ISO text field — single and
  between. Stopgap until a dedicated DateTime picker component lands (swaps 1:1).
- Relax the datetime validator to accept the local no-zone value; format the
  datetime chip value as a locale date + time to match the column renderer.
- Truncate long chip values (uuid / long strings) so they don't stretch the toolbar.
- Expand the vite demo to one column per supported filter type (adds uuid,
  boolean, datetime, time) so every editor/operator path is exercisable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… time

Match the single-date experience: the panel's single-datetime editor now shows
the inline Calendar up front with a labelled "Choose time" picker beneath it,
instead of the compact segmented date field. The chip editor and the "between"
range keep the compact date-picker + time box so they stay short.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rom/To tabs

Replace the two stacked From/To fields in the panel's date and datetime range
editors with a single inline calendar and a From/To tab bar on top. Picking a
date edits the active tab; each tab shows its current value. datetime tabs also
carry the "Choose time" picker. Reuses the single-value inline editors; the chip
editor keeps the compact From/To fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r fits

Bump the fixed panel height (h-96 → h-[32rem]) so the tallest editor — the
datetime "between" (From/To tabs + inline calendar + "Choose time") — shows the
time picker without clipping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
32rem left a large empty gap below the datetime range's time picker; 28rem fits
the tallest editor snugly without clipping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The panel kept its selected-field state across open/close, so it reopened on
whatever field was last picked. Reset to the first field each time it opens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Panel field/condition rows: selection is the solid accent (+ bold), hover is a
  subtle muted tint, applied exclusively so they never look identical (previously
  both used bg-accent).
- Demo: add preset quick-filter tabs (All / Draft / Sent / Overdue) to the left of
  the toolbar, wired to the status filter — a prototype of a common ERP pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With the preset tabs present, keep them on their own row and let the active
filter chips (+ Add filter) flow onto the row below, instead of crowding them
all onto one line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bar layouts

`slot` ("all" default | "chips" | "add") renders the active chips and the Add
filter trigger separately, so consumers can place them on different rows — e.g.
the trigger in a header row (beside view tabs) with chips on the row below.

- Demo: preset tabs + Add filter on the top row, chips on their own row below
  (only shown when filters are active — no empty row otherwise).
- Tests + docs for the new prop; changeset refreshed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tabs)

Refactor the demo into a reusable InvoiceTable + StatusTabs and show three
independent tables trialing where the Add filter button sits relative to the
preset tabs: (1) tabs left / Add filter far right (current), (2) Add filter far
left, (3) Add filter right after the tabs. Chips still land on the row below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…trigger

`addIconOnly` renders the Add filter trigger as just the funnel icon; the
"Add filter" label is dropped from view but kept as the button's aria-label.

- Demo: add a 4th example (Add filter far left, icon only).
- Test + docs for the new prop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settle on the icon-only Add filter on the far left (label as aria-label) and
drop the other placement experiments. Show two examples — with preset tabs and
without — both with chips on the row below. Bump the icon-only funnel to 14px
(size-3.5) so it has proper presence in the button; stroke stays the default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… toolbar

DataTable.Toolbar is flex-col, which stretches its children — so a standalone
`<DataTable.Filters slot="add" />` rendered the Add filter button full-width.
Wrap the trigger in a shrink-to-content box so it keeps its natural size wherever
it's placed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a TODO noting the time / datetime / datetime-range filter editors are
stopgaps to be replaced 1:1 with dedicated app-shell pickers (TimePicker,
DateTimePicker, Date/DateTimeRangePicker) once available — the ISO value shapes
already match, so each swap is UI-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@itsprade
itsprade force-pushed the feat/data-table-filter-panel-variant branch from 00bca5f to 30242b9 Compare July 22, 2026 07:41
…r-panel-variant

# Conflicts:
#	docs/components/data-table.md
Comment thread .claude/launch.json Outdated
Comment thread packages/core/src/components/data-table/i18n.ts Outdated
itsprade and others added 2 commits July 23, 2026 14:18
…lify enum count

- Remove the local Claude Code preview config (.claude/launch.json) from the repo
  and gitignore .claude/ (matches the existing .cursor/ ignore).
- Drop the English pluralization helper; the multi-select enum chip summary now
  uses a simple "(s)" suffix ("2 Status(s)") instead of owning plural rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@IzumiSy

IzumiSy commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@copilot fix conflict

…r-panel-variant

# Conflicts:
#	packages/core/src/components/data-table/toolbar.tsx

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

Copilot AI commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@copilot fix conflict

Resolved in 764ebea. The one conflict was in toolbar.tsxorigin/main had merged the new shared Checkbox component (#401) which also touched the same file. Resolutions:

  • Kept all feature-branch imports (Filter as FilterIcon, Check, Search) and dropped the old @base-ui/react/checkbox direct import since the shared Checkbox was already imported from @/components/checkbox.
  • Kept the feature-branch DateTimeFilterInput function and the 3-column AddFilterPanel implementation; discarded the old AddFilter popover that main was still shipping.
  • Replaced the hand-rolled FilterCheckbox helper (which used Checkbox.Root/Checkbox.Indicator directly) with the shared Checkbox in EnumOptionList and the case-sensitive toggle, aligning with main's consolidation.

type-check clean · lint 0 warnings/errors · 1424 tests passed.

Copilot AI requested a review from IzumiSy July 24, 2026 05:14
@IzumiSy
IzumiSy merged commit b32f003 into main Jul 24, 2026
5 checks passed
@IzumiSy
IzumiSy deleted the feat/data-table-filter-panel-variant branch July 24, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants