@castandcrew/spotlight-mcp 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/index.json +3 -3
- package/package.json +1 -1
package/data/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.1.0",
|
|
3
|
-
"generated": "2026-07-29T22:
|
|
3
|
+
"generated": "2026-07-29T22:44:29.302Z",
|
|
4
4
|
"components": [],
|
|
5
5
|
"patterns": [
|
|
6
6
|
{
|
|
@@ -556,8 +556,8 @@
|
|
|
556
556
|
"id": "filter-toolbar-pattern",
|
|
557
557
|
"title": "Filter Toolbar Pattern",
|
|
558
558
|
"category": "recipe",
|
|
559
|
-
"description": "The standard filter surface for tables and lists in Spotlight.
|
|
560
|
-
"content": "# Filter Toolbar Pattern\n\nThe standard filter surface for tables and lists in Spotlight. No `FilterToolbar` wrapper\ncomponent exists yet — teams compose the pattern from existing Spotlight pieces arranged with flex\nCSS. This recipe defines the layout rules, component choices, and behavioral specifications so\nthe composed result is consistent across products.\n\nA flex-column container with two rows: a **Bar row** (Search + Filters slot) and a **Chip Row**\n(applied filter chips + overflow trigger + Reset button). Source: SPT-229 v0.4.0, SPT-225 v1.2.0,\nSPT-262 v1.0.0. Design spec: Patterns — Timeless V2.0.0, node 2029:6499.\n\n---\n\n## Anatomy\n\nThe Filter Toolbar is a two-row container positioned above a data table. The top Bar row holds the\nSearch field and a Filters slot; the bottom Chip Row holds applied-filter chips and controls. The\nChip Row renders only when at least one filter is applied.\n\n### Anatomy callouts\n\n| # | Element | Description |\n|---|---------|-------------|\n| 1 | **Toolbar Container** | Outer 48px strip |\n| 2 | **Filters** | Right-aligned slot |\n| 3 | **Search Field** | Always first control |\n| 4 | **Controls — Max Chip Count, Reset Filters** | Right-aligned buttons |\n| 5 | **Applied Filters Chips** | Left-aligned |\n\n### Structural specifications\n\n| Property | Value | Note |\n|----------|-------|------|\n| Height | 48px | Controls sit at 32px. 8px top + bottom padding. |\n| Padding — all sides | 8px | Consistent horizontal and vertical. Follows nested border radius rule. |\n| Border radius — contained | 18px | Derived: control border radius (10px) + padding (8px) = 18px. |\n| Border radius — full-width | 0px | Toolbar runs flush with page margins. Nested radius rule does not apply. |\n| Background — default | None (transparent) | Toolbar blends with page surface. Spacing and control styling define the toolbar area. |\n| Border — default | None | Default variant. See Toolbar Variants section below. |\n| Width | 100% | Always equals width of the content container it controls. |\n| Spacing below toolbar | 16px | Gap between toolbar bottom edge and table header top edge. Maps to `--space-md`. |\n\n**Nested border radius rule.** Padding is derived from the relationship between the toolbar\ncontainer's border radius and the controls' border radius: outer radius = control radius (10px) +\npadding (8px) = 18px for the contained variant.\n\n### Two-row structure\n\n| Row | Contents | Layout |\n|-----|----------|--------|\n| **Bar** (top) | **Search** (flex-1, always left) · **Filters slot** (right — composition slot for filter controls) | `flex · gap: 16px · items-center · full-width` |\n| **Chip Row** (bottom) | RemovableChips · \"+N more\" expand button · Reset Filters (far right). **Rendered only when ≥1 filter active.** | `flex · flex-nowrap · gap: 8px · items-center · px: 8px · h: 28px` — single row, no wrapping |\n\nThe Filters slot is a composition slot — consuming teams place filter controls here in the fixed\norder defined below. The slot does not prescribe which controls appear; it provides the container.\n\n### Zone structure\n\n| Zone | Position | Contents | Layout |\n|------|----------|----------|--------|\n| A — Filter Controls | Left | Search, Date Range, Status, Entity dropdowns | flex-start · 16px gap between controls |\n| B — Actions | Right | Filters trigger (condensed mode) · Reset All (conditional) · Primary action (conditional) | flex-end · Filters trigger leftmost in Zone B · Reset All left of primary action |\n\n**Filters trigger is right-aligned.** In condensed mode the Filters button moves to Zone B (right side of the bar), not Zone A. This keeps the left side clean for Search and any inline controls that remain visible at the current breakpoint.\n\n### Control alignment and sizing\n\nAll filter controls in the bar row must be vertically aligned at their midpoint. Two rules enforce this:\n\n**Rule — no floating labels on toolbar controls.** Filter controls must not render a visible label above the input. A floating label shifts the control's bounding box downward and breaks alignment with adjacent controls that have none (see screenshot: \"Offer Status\" label above the dropdown misaligns it with the search field). Use `placeholder` for the visible hint and `aria-label` (or `aria-labelledby` pointing to a visually-hidden `<label>`) for accessibility instead.\n\n```tsx\n{/* Correct — no visible label, accessible via aria-label */}\n<SelectInputField\n aria-label=\"Offer Status\"\n placeholder=\"Offer Status\"\n size=\"md\"\n /* ... */\n/>\n\n{/* Incorrect — floating label pushes control down, misaligns row */}\n<SelectInputField\n label=\"Offer Status\"\n size=\"md\"\n /* ... */\n/>\n```\n\n**Rule — all toolbar controls use `md` size.** Every input in the bar row (TextField, SelectInputField, MultiSelectInput, AutocompleteInput, DatePicker) must use `size=\"md\"`. Do not mix `sm` and `md` in the same toolbar.\n\n| Control | Required size |\n|---------|--------------|\n| `TextField` (search) | `md` |\n| `SelectInputField` | `md` |\n| `MultiSelectInput` | `md` |\n| `AutocompleteInput` | `md` |\n| `DatePicker` | `md` |\n\n---\n\n### Design-spec dimensions (implement manually until component ships)\n\nThe design spec defines these dimensions as future component props. Until `FilterToolbar` ships,\nimplement each of these as explicit CSS or conditional rendering in the consuming screen.\n\n| Dimension | Spec value | How to implement today |\n|-----------|-----------|----------------------|\n| Variant | `default` (transparent) · `contained` (secondary surface) · `float` (transparent, white page) | Apply background color and border-radius to the wrapper div per variant |\n| Density | `standard` (16px gap) · `condensed` (10px gap) · `drawer-trigger` | Control the `gap` value on the Filters flex row |\n| Show search | Toggle `TextField` render | Conditional render of `TextField` |\n| Max chip count | Threshold before \"+N more\" spillover | Slice the chip array; render `ToggleButton \"+N more\"` for overflow items |\n| Reset visibility | Show only when ≥1 filter deviates from page default | Conditional render of ghost `Button` |\n| Reset display | `label` (\"Reset Filters\") for standard; `icon` (⊗ ≥24px) for condensed | Swap between `Button` with text and icon-only `Button` with `aria-label` |\n\n**Spillover behavior.** When chips exceed the max count threshold, the \"N+ more\" trigger reveals\nhidden filters in a Popover — on hover (transient, dismisses on mouse-out) and on click (pinned,\npersists until dismissed). Use a `ToggleButton` + `Popover` + `MaxChipSpillOverList` +\n`SpillOverItem` composition.\n\n### Spacing tokens\n\n```css\n/* Confirmed in style.css */\n--table-filter-margin-bottom: var(--space-xl); /* gap below filter bar */\n--table-search-min-width-lg: 250px; /* desktop search field */\n--table-search-min-width-md: 200px; /* tablet ≤768px */\n--table-search-min-width-sm: 150px; /* mobile ≤480px */\n\n/* Proposed additions for FilterToolbar component */\n--filter-toolbar-padding: 8px;\n--filter-toolbar-gap: 16px; /* gap between search and slot in Bar row */\n--filter-toolbar-gap-condensed: 10px; /* condensed modifier gap */\n--filter-toolbar-row-gap: 16px; /* gap between Bar row and Chip Row */\n--filter-toolbar-chip-row-height: 28px; /* Chip Row height */\n--filter-toolbar-margin-bottom: 16px; /* gap to table header — --space-md */\n--filter-toolbar-drawer-width: var(--drawer-size-width-sm); /* 350px */\n```\n\n---\n\n## Best Practices\n\n> Always place Search first in the Filters slot — it is required on every toolbar instance. Place the filters that serve your screen's most critical workflows immediately after Search, and suppress any slot that has no corresponding workflow. Never reorder the remaining slots. Keep the chip-row controls lean: show Reset All only when one or more filters deviate from the screen's default state, and only include a Primary Action button on the page header when it directly relates to the table content below.\n> — Patterns — Timeless V2.0.0\n\n### Rules at a glance\n\n| Rule | Detail |\n|------|--------|\n| Search is always first | Required on every toolbar instance, regardless of screen type. Never omit. |\n| Chip Row is always present when filters are active | The chip row MUST appear below the toolbar when ≥1 filter is active. Never suppress chips when filters are applied. |\n| Reset All shows on the page | The \"Reset filters\" button must be visible in the chip row on the page — not hidden behind a drawer or panel. Show it when ≥1 filter deviates from the page's default state. Hide it when filters are already at default. |\n| One Primary Action maximum | The primary action button (e.g. \"+ Create Offer\") belongs in the page header, not the filter toolbar, and only when it directly relates to the table content below. |\n| No submit button on the toolbar | All Zone A controls (inline filter bar) apply immediately on change — no Apply button needed. |\n| Never add a filter slot without a workflow | Omit any control that doesn't map to a primary user workflow on this screen. |\n\n---\n\n## Component configuration props\n\nThe API surface for configuring toolbar behavior and visibility. These correspond to the props that the future `FilterToolbar` component will expose; implement them as conditional rendering until the component ships.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `density` | Enum: `standard` · `condensed` · `drawer-trigger` | Controls gap between filter controls. `standard` = 16px gap; `condensed` = 10px gap; `drawer-trigger` = controls move into drawer |\n| `showSearch` | Boolean | Toggles the Search field in the Bar row. Almost always `true`. |\n| `maxChipCount` | Enum / Number | Max chips shown in the Chip Row before the \"+N more\" spillover trigger appears. Overflow chips surface in a Popover on hover (transient) or click (pinned). |\n| `allowReset` | Boolean | Shows the Reset Filters button in the Chip Row when ≥1 filter deviates from the page's default state (SPT-241). Hidden when filters are already at default. |\n| `resetDisplay` | Enum: `text` · `icon` | `text` = \"Reset filters\" ghost button (standard). `icon` = icon-only ⊗ (≥24px touch target) with `aria-label=\"Reset filters\"` + tooltip — condensed bar only. |\n| children (Filters slot) | ReactNode | Accepts any filter control combination in fixed slot order. The slot does not prescribe which controls appear; it provides the flex container. |\n\n---\n\n## Toolbar variants\n\nThree variants control background and border treatment. The default variant is standard for most\nscreens.\n\n| Variant | Background | Use when |\n|---------|-----------|----------|\n| `default` | None (transparent) | Standard — works on any page surface. Spacing and control styling define the area. |\n| `contained` | Secondary surface token | Toolbar needs to read as a distinct component separate from the page and table. |\n| `float` | None (transparent) | Toolbar fully blends into a white page. Spacing alone does all separation work. |\n\n---\n\n## Control ordering — Filters slot (fixed order)\n\nBefore placing controls, identify the primary workflows your screen supports — what are users most\nfrequently trying to find or narrow down? Let that answer drive which filters you include and how\nprominent they are.\n\n**How to use:**\n1. Identify your screen's primary workflows — what are users most frequently trying to find or narrow down?\n2. Map each workflow to the filter slot below. Workflows that drive the most filtering decisions should appear leftmost in Zone A.\n3. Omit any slot that doesn't apply to your screen's data. Never reorder the slots that remain.\n\nTeams may omit controls not relevant to their screen's data, but **may not reorder** controls\nthat are present.\n\n| Slot | Control | Spotlight component | Include when… |\n|------|---------|---------------------|---------------|\n| 1 — Always first | Search | `TextField` (mode: search) | Always. Every toolbar needs a search field regardless of screen type. |\n| 2 | Date Range (From / To) | `DatePicker` × 2 | Date is a primary workflow on this screen — users routinely narrow results by time period (e.g. pay periods, deadlines, week ending). |\n| 3 | Status | `SelectInputField` | Status is a primary workflow — users regularly need to isolate records by approval state, progress, or lifecycle stage. |\n| 4+ | Entity dropdowns (Employee, Project, Dept.) | `SelectInputField` or `AutocompleteInput` | Filtering by person, project, or department is a primary workflow. Admin and manager views only — suppress on single-entity employee views. |\n| Zone A — Left | Reset All | `Button` (variant: ghost or link) | Show only when 1 or more filters deviate from the screen's default state. Hidden when all filters are at their default. |\n| Zone B — Rightmost | Primary action (e.g. \"+ Create Offer\") | `Button` (variant: primary) | Conditional. Appears only when the action is directly related to the content in the table below. |\n\n---\n\n## Component mapping — filter control → Spotlight component\n\n**Deprecation notice.** `Select` is deprecated. All filter dropdowns must use `SelectInputField`.\n\n| Filter type | Component | Size | Key rule |\n|-------------|-----------|------|---------|\n| Text search | `TextField` (mode: search) | **md** | Search mode · Start adornment: search icon · End adornment: clear X · 300ms debounce · `aria-label` required (no visible label) |\n| Single select, <20 items | `SelectInputField` | **md** | Single-select mode · `placeholder` + `aria-label` — no `label` prop · List scannable without search |\n| Multi select, <20 items | `MultiSelectInput` | **md** | OR logic within filter · `placeholder` + `aria-label` — no `label` prop · Select All sticky at top |\n| Searchable entity, 20+ items | `AutocompleteInput` | **md** | Async search for 100+ items · `placeholder` + `aria-label` — no `label` prop · Single select · Legacy Autocomplete deprecated |\n| Multi searchable entity, 20+ items | `MultiAutocompleteInput` | **md** | OR logic · `placeholder` + `aria-label` — no `label` prop · Selections shown as dismissible chips inside trigger field |\n| Date — single | `DatePicker` | **md** | Calendar popup · MM/DD/YYYY · 280–320px popup width · `aria-label` required |\n| Date range | `DatePicker` × 2 | **md** | From/To side-by-side · 105px each · From constrains To minimum · Paired \"Clear dates\" link resets both · `aria-label` on each |\n| Date presets (Today, This Week…) | `ToggleButtonGroup` | Verify in Storybook | Mutually exclusive · Preset auto-populates date range · Include Custom fallback |\n| Boolean toggle (e.g. Read/Unread) | `Switch` | sm: 34×20px · md: 44×24px | Label describes active state · Default off = unfiltered · Immediate update |\n| Mutually exclusive options (2–5) | `ToggleButtonGroup` | Verify in Storybook | All options visible simultaneously · Toolbar use only |\n| Quick filter, combinable (3–6 values) | `Chip` / `ChipGroup` | Verify in Storybook | OR logic within selection · No dropdown · Status is primary candidate |\n| Grouped / hierarchical | No component available | — | Unvalidated use case. Do not implement until research confirms need. |\n\n---\n\n## Chip Row elements\n\nThe chip row is a **single, non-wrapping row**. Chips never wrap to a second line; overflow is\nhandled by the \"+N more\" expand button.\n\n| Element | Component | Condition |\n|---------|-----------|-----------|\n| RemovableChips | `Chip` (dismissible) | One per active filter, up to the visible threshold. Label format: \"FilterName: Value ✕\". |\n| \"+N more\" expand button | `Button` (variant: ghost or link) | Shown when applied chips exceed the visible threshold. Clicking **expands the chip area in-place** to reveal all applied chips — it does not open a popover. When expanded, button changes to \"Show less\" (or collapses on second click). |\n| Reset Filters | `Button` (variant: ghost) · `margin-left: auto` · **far right** | Visible when ≥1 filter deviates from page default. Always pinned to the right edge of the chip row regardless of how many chips are showing. Returns to page default state (not zero). |\n\n**\"+N more\" expand behavior.** The chip row starts collapsed (single line). Clicking \"+N more\"\nexpands the row container to `flex-wrap`, revealing all chips. Clicking \"Show less\" (or the same\nbutton toggled) collapses back to single-line. Reset Filters stays visible and right-aligned in\nboth states.\n\n**Reset Filters positioning.** Reset Filters uses `margin-left: auto` to push itself to the far\nright of the chip row at all times — collapsed, expanded, and at every chip count.\n\n---\n\n## Reset behavior\n\n| Mechanism | What it clears | Confirmation |\n|-----------|---------------|--------------|\n| Chip ✕ button | That single filter only | None — immediate |\n| Reset All / Clear all | All active filters — returns to page default state | None — non-destructive |\n\n**Reset All rule.** Reset All returns to the page's default state — not to \"no filters.\" If a\ntable's default shows the current pay period, clearing returns to that default, not to all-time\nunfiltered. Reset All is hidden when no filters deviate from the default state.\n\n---\n\n## Surface variants — Filter Bar vs Condensed Filter Bar\n\n### Filter Bar (1–4 filter controls)\n\nControls sit inline in the Filters slot. Each control applies immediately on change — no Apply\nbutton. Gap between controls: 16px (standard).\n\n### Condensed Filter Bar (5+ filter controls)\n\nTriggered automatically by the component when it receives 5+ controls. Search stays in the Bar\nrow; all filter controls move into the Condensed Filter Drawer piece. A **Filters trigger** button\n(toggle + funnel icon + count badge) replaces the Filters slot.\n\n| Condensed element | Spec |\n|------------------|------|\n| Filters trigger | `ToggleButton` + funnel icon + applied-count badge (shows applied, never draft) |\n| Filter controls | All move into the Condensed Filter Drawer piece (sm · 350px) |\n| Apply model | Batch Apply / Cancel inside the drawer |\n| Chip Row height | 34px (vs 28px standard) |\n| MaxChipCount | Badge-only (32×17px) — no text label |\n| Reset filters | Icon-only ⊗ (≥24px WCAG target) + `aria-label=\"Reset filters\"` + tooltip |\n\n**Auto-adapt.** The component counts controls it receives (Search = 0; Date Range = 1; each other\ncontrol = 1) and switches layout automatically. No screen-level surface selection needed.\n\n---\n\n## Condensed Filter Drawer (piece)\n\nA distinct component piece owned by the Condensed Filter Toolbar (not an instance of the\nstandalone Filter Drawer SPT-223). Shares SPT-223's visual language but has its own header,\nfooter, and behavior.\n\n| Property | Value |\n|----------|-------|\n| Width | sm · 350px (`--drawer-size-width-sm`) |\n| Header | \"Filters\" + ✕ close |\n| Footer | Reset Filters (ghost) · Cancel (outlined) · Apply (solid) |\n| Apply model | Batch — data refreshes once on Apply |\n| Scrim tap / ✕ | Close + **preserve draft state** (reopening restores draft) |\n| Cancel | Discard draft · close |\n| Badge updates | Only on Apply — draft selections never increment the badge |\n\n**Interaction loop:**\n\n| Trigger | Behavior |\n|---------|----------|\n| Click Filters button | Drawer opens · controls pre-populated with applied state |\n| Make selections | Draft state — controls update visually; filters not applied yet |\n| Apply | Drawer closes · chip row updates · results refresh · badge = applied count |\n| Cancel | Drawer closes · draft discarded · chip row and results unchanged |\n| Scrim tap or ✕ | Drawer closes · draft **preserved** · results unchanged |\n| Reset Filters (drawer footer) | All controls cleared · drawer closes · chip row empties · Undo snackbar |\n| Remove chip (chip row) | That filter cleared immediately · Undo snackbar · drawer not required to be open |\n\n---\n\n## Responsive behavior\n\nThree states are defined. The toolbar collapses progressively — entity dropdowns move to the\nFilters drawer at tablet, and all filter controls collapse at mobile.\n\n| Spotlight breakpoint | Viewport | Search | Date Range | Status | Entity dropdowns |\n|---------------------|---------|--------|-----------|--------|-----------------|\n| Desktop + Laptop (≥1025px) | ≥1025px | ✓ Bar | ✓ Inline | ✓ Inline | ✓ Inline |\n| Tablet landscape (769–1024px) | 769–1024px | ✓ Bar | ✓ Inline | ✓ Inline | ✓ Inline (960px accommodates) |\n| Tablet portrait (601–768px) | 601–768px | ✓ Bar | ✓ Inline | ✓ Inline | → Drawer (sm) |\n| Phone (≤600px) | ≤600px | ✓ Bar | → Drawer (xsm) | → Drawer (xsm) | → Drawer (xsm) |\n\nWhen filter controls move to a Drawer at smaller breakpoints, the Drawer uses an explicit Apply\nbecause multiple filters may change simultaneously.\n\nThe Condensed Filter Bar uses its three size variants (`xs` / `sm` / `md`) for responsive behavior\nrather than this breakpoint table.\n\n---\n\n## Motion\n\nAll animations follow Spotlight Motion Guidance. Designers pick an intent — the system resolves\nduration and easing.\n\n| Animation | Classification | Intent | Duration | Feel | Motion | Reduced motion |\n|-----------|---------------|--------|----------|------|--------|---------------|\n| Filters button badge count update | Connect | State | md · 200ms | Productive | Background + border + text color transition simultaneously | Preserved — color transitions are motion-safe |\n| Applied filter chips row appear | Reveal | Enter | lg · 300ms | Expressive | Fade + translateY(-8px) | Instant appear |\n| Applied filter chips row dismiss (Reset All) | Reveal | Exit | md · 200ms | Productive | Fade out | Instant disappear |\n| Individual chip dismiss (✕) | Reveal | Exit | md · 200ms | Productive | Fade out | Instant disappear |\n| Reset All button appear | Reveal | Enter | lg · 300ms | Expressive | Fade in | Instant appear |\n| Reset All button disappear | Reveal | Exit | md · 200ms | Productive | Fade out | Instant disappear |\n| Drawer open | — | Enter | lg · 300ms | — | Slide from right + scrim fade | Instant appear · scrim still fades |\n| Drawer close | — | Exit | md · 200ms | — | Reverse slide + scrim fade | Instant disappear |\n\n**Motion principle — asymmetric timing.** Enter animations (lg · 300ms) are intentionally slower\nthan exit animations (md · 200ms). Users need a moment to orient to new content; they do not\nneed to watch it leave. This applies to the drawer, chip row, and Reset All button throughout\nthe toolbar.\n\n---\n\n## Composing the Filter Toolbar from Spotlight pieces\n\nNo `FilterToolbar` wrapper component exists. Compose the pattern from these existing Spotlight\ncomponents using the CSS structure below.\n\n### HTML/CSS structure\n\n```tsx\n{/* Outer wrapper — flex-column, padding 8px, gap 16px between rows */}\n<div className={styles.filterToolbar}>\n\n {/* Bar row — flex, gap 16px, items-center */}\n <div className={styles.filterBar}>\n {/* Zone A — left, flex-start, 16px gap */}\n <div className={styles.filterControls}>\n <TextField mode=\"search\" /* ... */ /> {/* Slot 1 — always first */}\n <DatePicker /* From */ /> {/* Slot 2 — date range start */}\n <DatePicker /* To */ /> {/* Slot 2 — date range end */}\n <SelectInputField /* Status */ /> {/* Slot 3 */}\n {/* Slot 4+ entity dropdowns if applicable */}\n </div>\n\n {/* Zone B — right, flex-end */}\n <div className={styles.filterActions}>\n {showReset && <Button variant=\"ghost\">Reset Filters</Button>}\n {primaryAction}\n </div>\n </div>\n\n {/* Chip Row — renders only when ≥1 filter active */}\n {hasActiveFilters && (\n <div className={`${styles.chipRow} ${isExpanded ? styles.chipRowExpanded : ''}`}>\n {/* Visible chips — all chips when expanded, capped at threshold when collapsed */}\n {(isExpanded ? allChips : visibleChips).map(chip => (\n <Chip dismissible key={chip.id}>{chip.label}</Chip>\n ))}\n\n {/* \"+N more\" / \"Show less\" expand toggle — only when overflow exists */}\n {overflowCount > 0 && (\n <Button variant=\"ghost\" onClick={() => setIsExpanded(e => !e)}>\n {isExpanded ? 'Show less' : `+${overflowCount} more`}\n </Button>\n )}\n\n {/* Reset Filters — always far right via margin-left: auto */}\n {showReset && (\n <Button variant=\"ghost\" className={styles.resetFilters}>Reset Filters</Button>\n )}\n </div>\n )}\n</div>\n```\n\n### CSS values\n\n```css\n.filterToolbar {\n display: flex;\n flex-direction: column;\n gap: 16px; /* between Bar row and Chip Row */\n padding: 8px; /* all sides */\n width: 100%;\n margin-bottom: 16px; /* gap to table header — var(--space-md) */\n}\n\n/* contained variant */\n.filterToolbar--contained {\n background: var(--color-surface-secondary);\n border-radius: 18px; /* control radius (10px) + padding (8px) */\n}\n\n.filterBar {\n display: flex;\n align-items: center;\n gap: 16px;\n width: 100%;\n}\n\n.filterControls {\n display: flex;\n align-items: center;\n gap: 16px; /* 10px for condensed density */\n flex: 1;\n}\n\n.filterActions {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n}\n\n.chipRow {\n display: flex;\n align-items: center;\n flex-wrap: nowrap; /* single row — no wrapping by default */\n gap: 8px;\n min-height: 28px; /* 34px for condensed variant */\n padding: 0 8px;\n overflow: hidden;\n}\n\n/* Expanded state — reveals all chips */\n.chipRowExpanded {\n flex-wrap: wrap;\n overflow: visible;\n}\n\n/* Reset Filters always pinned to far right */\n.resetFilters {\n margin-left: auto;\n flex-shrink: 0;\n}\n```\n\n### Spotlight components used\n\n| Element | Spotlight component |\n|---------|-------------------|\n| Search field | `TextField` (mode: search, start adornment: search icon, end adornment: clear X) |\n| Single-select dropdown | `SelectInputField` |\n| Multi-select dropdown | `MultiSelectInput` |\n| Date picker | `DatePicker` (×2 for date range) |\n| Applied filter chip | `Chip` (dismissible) |\n| Chip container | `ChipGroup` (flex-wrap) |\n| Overflow trigger | `ToggleButton` + `Popover` + `MaxChipSpillOverList` + `SpillOverItem` |\n| Reset Filters | `Button` (variant: ghost) — standard; icon-only `Button` with aria-label — condensed |\n| Primary action | `Button` (variant: primary) — conditional |\n\n**DateRangePicker gap.** No single composed From/To component exists. Use two `DatePicker`\ninstances side-by-side with a paired \"Clear dates\" link. Will be replaced by `DateRangePicker`\n(FILT-12) when available.\n\n---\n\n## Component gaps\n\n| Gap | Status | Impact |\n|-----|--------|--------|\n| `FilterToolbar` composition component | Not yet built — compose manually per recipe | Teams build own layout using Spotlight pieces |\n| `DateRangePicker` | Missing (FILT-12) | Use 2× `DatePicker` interim |\n| `MultiAutocompleteInput` | New — verify in Storybook | Multi-select entity filters |\n| `Badge xs` size | Missing | Count badge for condensed bar uses `sm` (24px) as fallback |\n\n---\n\n## Related patterns\n\n| Pattern | Purpose |\n|---------|---------|\n| `filter-selection-guide` | Which surface to use (Filter Bar vs Condensed vs Drawer) |\n| `filter-behavior-rules` | AND/OR logic, debounce, server/client, sort/pagination rules |\n| `filter-drawer-pattern` | Standalone Filter Drawer recipe |\n| `filter-communicating-applied-guide` | Applied chips, count badge, results count, zero results |\n| `filter-persistence-guide` | Session, URL param, and saved-view persistence |\n\n---\n\n*Pattern ID: `filter-toolbar-pattern` · Source: SPT-229 v0.4.0, SPT-225 v1.2.0, SPT-262 v1.0.0 · Design spec: Patterns — Timeless V2.0.0*\n",
|
|
559
|
+
"description": "The standard filter surface for tables, card grids, and lists in Spotlight. Composed from a **Bar row** (Search field + inline filter controls or Filters trigger button) and a **Chip Row** (applied filter chips, \"+N more\" overflow button, and Reset Filters). The Chip Row renders only when at least o",
|
|
560
|
+
"content": "# Filter Toolbar Pattern\n\nThe standard filter surface for tables, card grids, and lists in Spotlight. Composed from a\n**Bar row** (Search field + inline filter controls or Filters trigger button) and a **Chip Row**\n(applied filter chips, \"+N more\" overflow button, and Reset Filters). The Chip Row renders only\nwhen at least one filter is active. All toolbar controls use `size=\"md\"` and must not render a\nvisible label — use `placeholder` and `aria-label` so every input sits on the same baseline.\nThe Filters trigger button is right-aligned (Zone B). \"+N more\" shows a transient hover popover\n(dismisses on mouse-out) and expands the chip row in-place on click.\n\nNo `FilterToolbar` wrapper component exists yet — teams compose the pattern from Spotlight pieces\n(`TextField`, `SelectInputField`, `MultiSelectInput`, `AutocompleteInput`, `DatePicker`, `Chip`,\n`ChipGroup`, `Button`, `ToggleButton`, `Popover`) arranged with flex CSS. Source: SPT-229 v0.4.0,\nSPT-225 v1.2.0, SPT-262 v1.0.0. Design spec: Patterns — Timeless V2.0.0, node 2029:6499.\n\n---\n\n## Anatomy\n\nThe Filter Toolbar is a two-row container positioned above a data table. The top Bar row holds the\nSearch field and a Filters slot; the bottom Chip Row holds applied-filter chips and controls. The\nChip Row renders only when at least one filter is applied.\n\n### Anatomy callouts\n\n| # | Element | Description |\n|---|---------|-------------|\n| 1 | **Toolbar Container** | Outer 48px strip |\n| 2 | **Filters** | Right-aligned slot |\n| 3 | **Search Field** | Always first control |\n| 4 | **Controls — Max Chip Count, Reset Filters** | Right-aligned buttons |\n| 5 | **Applied Filters Chips** | Left-aligned |\n\n### Structural specifications\n\n| Property | Value | Note |\n|----------|-------|------|\n| Height | 48px | Controls sit at 32px. 8px top + bottom padding. |\n| Padding — all sides | 8px | Consistent horizontal and vertical. Follows nested border radius rule. |\n| Border radius — contained | 18px | Derived: control border radius (10px) + padding (8px) = 18px. |\n| Border radius — full-width | 0px | Toolbar runs flush with page margins. Nested radius rule does not apply. |\n| Background — default | None (transparent) | Toolbar blends with page surface. Spacing and control styling define the toolbar area. |\n| Border — default | None | Default variant. See Toolbar Variants section below. |\n| Width | 100% | Always equals width of the content container it controls. |\n| Spacing below toolbar | 16px | Gap between toolbar bottom edge and table header top edge. Maps to `--space-md`. |\n\n**Nested border radius rule.** Padding is derived from the relationship between the toolbar\ncontainer's border radius and the controls' border radius: outer radius = control radius (10px) +\npadding (8px) = 18px for the contained variant.\n\n### Two-row structure\n\n| Row | Contents | Layout |\n|-----|----------|--------|\n| **Bar** (top) | **Search** (flex-1, always left) · **Filters slot** (right — composition slot for filter controls) | `flex · gap: 16px · items-center · full-width` |\n| **Chip Row** (bottom) | RemovableChips · \"+N more\" expand button · Reset Filters (far right). **Rendered only when ≥1 filter active.** | `flex · flex-nowrap · gap: 8px · items-center · px: 8px · h: 28px` — single row, no wrapping |\n\nThe Filters slot is a composition slot — consuming teams place filter controls here in the fixed\norder defined below. The slot does not prescribe which controls appear; it provides the container.\n\n### Zone structure\n\n| Zone | Position | Contents | Layout |\n|------|----------|----------|--------|\n| A — Filter Controls | Left | Search, Date Range, Status, Entity dropdowns | flex-start · 16px gap between controls |\n| B — Actions | Right | Filters trigger (condensed mode) · Reset All (conditional) · Primary action (conditional) | flex-end · Filters trigger leftmost in Zone B · Reset All left of primary action |\n\n**Filters trigger is right-aligned.** In condensed mode the Filters button moves to Zone B (right side of the bar), not Zone A. This keeps the left side clean for Search and any inline controls that remain visible at the current breakpoint.\n\n### Control alignment and sizing\n\nAll filter controls in the bar row must be vertically aligned at their midpoint. Two rules enforce this:\n\n**Rule — no floating labels on toolbar controls.** Filter controls must not render a visible label above the input. A floating label shifts the control's bounding box downward and breaks alignment with adjacent controls that have none (see screenshot: \"Offer Status\" label above the dropdown misaligns it with the search field). Use `placeholder` for the visible hint and `aria-label` (or `aria-labelledby` pointing to a visually-hidden `<label>`) for accessibility instead.\n\n```tsx\n{/* Correct — no visible label, accessible via aria-label */}\n<SelectInputField\n aria-label=\"Offer Status\"\n placeholder=\"Offer Status\"\n size=\"md\"\n /* ... */\n/>\n\n{/* Incorrect — floating label pushes control down, misaligns row */}\n<SelectInputField\n label=\"Offer Status\"\n size=\"md\"\n /* ... */\n/>\n```\n\n**Rule — all toolbar controls use `md` size.** Every input in the bar row (TextField, SelectInputField, MultiSelectInput, AutocompleteInput, DatePicker) must use `size=\"md\"`. Do not mix `sm` and `md` in the same toolbar.\n\n| Control | Required size |\n|---------|--------------|\n| `TextField` (search) | `md` |\n| `SelectInputField` | `md` |\n| `MultiSelectInput` | `md` |\n| `AutocompleteInput` | `md` |\n| `DatePicker` | `md` |\n\n---\n\n### Design-spec dimensions (implement manually until component ships)\n\nThe design spec defines these dimensions as future component props. Until `FilterToolbar` ships,\nimplement each of these as explicit CSS or conditional rendering in the consuming screen.\n\n| Dimension | Spec value | How to implement today |\n|-----------|-----------|----------------------|\n| Variant | `default` (transparent) · `contained` (secondary surface) · `float` (transparent, white page) | Apply background color and border-radius to the wrapper div per variant |\n| Density | `standard` (16px gap) · `condensed` (10px gap) · `drawer-trigger` | Control the `gap` value on the Filters flex row |\n| Show search | Toggle `TextField` render | Conditional render of `TextField` |\n| Max chip count | Threshold before \"+N more\" spillover | Slice the chip array; render `ToggleButton \"+N more\"` for overflow items |\n| Reset visibility | Show only when ≥1 filter deviates from page default | Conditional render of ghost `Button` |\n| Reset display | `label` (\"Reset Filters\") for standard; `icon` (⊗ ≥24px) for condensed | Swap between `Button` with text and icon-only `Button` with `aria-label` |\n\n**Spillover behavior.** When chips exceed the max count threshold, the \"N+ more\" trigger reveals\nhidden filters in a Popover — on hover (transient, dismisses on mouse-out) and on click (pinned,\npersists until dismissed). Use a `ToggleButton` + `Popover` + `MaxChipSpillOverList` +\n`SpillOverItem` composition.\n\n### Spacing tokens\n\n```css\n/* Confirmed in style.css */\n--table-filter-margin-bottom: var(--space-xl); /* gap below filter bar */\n--table-search-min-width-lg: 250px; /* desktop search field */\n--table-search-min-width-md: 200px; /* tablet ≤768px */\n--table-search-min-width-sm: 150px; /* mobile ≤480px */\n\n/* Proposed additions for FilterToolbar component */\n--filter-toolbar-padding: 8px;\n--filter-toolbar-gap: 16px; /* gap between search and slot in Bar row */\n--filter-toolbar-gap-condensed: 10px; /* condensed modifier gap */\n--filter-toolbar-row-gap: 16px; /* gap between Bar row and Chip Row */\n--filter-toolbar-chip-row-height: 28px; /* Chip Row height */\n--filter-toolbar-margin-bottom: 16px; /* gap to table header — --space-md */\n--filter-toolbar-drawer-width: var(--drawer-size-width-sm); /* 350px */\n```\n\n---\n\n## Best Practices\n\n> Always place Search first in the Filters slot — it is required on every toolbar instance. Place the filters that serve your screen's most critical workflows immediately after Search, and suppress any slot that has no corresponding workflow. Never reorder the remaining slots. Keep the chip-row controls lean: show Reset All only when one or more filters deviate from the screen's default state, and only include a Primary Action button on the page header when it directly relates to the table content below.\n> — Patterns — Timeless V2.0.0\n\n### Rules at a glance\n\n| Rule | Detail |\n|------|--------|\n| Search is always first | Required on every toolbar instance, regardless of screen type. Never omit. |\n| Chip Row is always present when filters are active | The chip row MUST appear below the toolbar when ≥1 filter is active. Never suppress chips when filters are applied. |\n| Reset All shows on the page | The \"Reset filters\" button must be visible in the chip row on the page — not hidden behind a drawer or panel. Show it when ≥1 filter deviates from the page's default state. Hide it when filters are already at default. |\n| One Primary Action maximum | The primary action button (e.g. \"+ Create Offer\") belongs in the page header, not the filter toolbar, and only when it directly relates to the table content below. |\n| No submit button on the toolbar | All Zone A controls (inline filter bar) apply immediately on change — no Apply button needed. |\n| Never add a filter slot without a workflow | Omit any control that doesn't map to a primary user workflow on this screen. |\n\n---\n\n## Component configuration props\n\nThe API surface for configuring toolbar behavior and visibility. These correspond to the props that the future `FilterToolbar` component will expose; implement them as conditional rendering until the component ships.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `density` | Enum: `standard` · `condensed` · `drawer-trigger` | Controls gap between filter controls. `standard` = 16px gap; `condensed` = 10px gap; `drawer-trigger` = controls move into drawer |\n| `showSearch` | Boolean | Toggles the Search field in the Bar row. Almost always `true`. |\n| `maxChipCount` | Enum / Number | Max chips shown in the Chip Row before the \"+N more\" spillover trigger appears. Overflow chips surface in a Popover on hover (transient) or click (pinned). |\n| `allowReset` | Boolean | Shows the Reset Filters button in the Chip Row when ≥1 filter deviates from the page's default state (SPT-241). Hidden when filters are already at default. |\n| `resetDisplay` | Enum: `text` · `icon` | `text` = \"Reset filters\" ghost button (standard). `icon` = icon-only ⊗ (≥24px touch target) with `aria-label=\"Reset filters\"` + tooltip — condensed bar only. |\n| children (Filters slot) | ReactNode | Accepts any filter control combination in fixed slot order. The slot does not prescribe which controls appear; it provides the flex container. |\n\n---\n\n## Toolbar variants\n\nThree variants control background and border treatment. The default variant is standard for most\nscreens.\n\n| Variant | Background | Use when |\n|---------|-----------|----------|\n| `default` | None (transparent) | Standard — works on any page surface. Spacing and control styling define the area. |\n| `contained` | Secondary surface token | Toolbar needs to read as a distinct component separate from the page and table. |\n| `float` | None (transparent) | Toolbar fully blends into a white page. Spacing alone does all separation work. |\n\n---\n\n## Control ordering — Filters slot (fixed order)\n\nBefore placing controls, identify the primary workflows your screen supports — what are users most\nfrequently trying to find or narrow down? Let that answer drive which filters you include and how\nprominent they are.\n\n**How to use:**\n1. Identify your screen's primary workflows — what are users most frequently trying to find or narrow down?\n2. Map each workflow to the filter slot below. Workflows that drive the most filtering decisions should appear leftmost in Zone A.\n3. Omit any slot that doesn't apply to your screen's data. Never reorder the slots that remain.\n\nTeams may omit controls not relevant to their screen's data, but **may not reorder** controls\nthat are present.\n\n| Slot | Control | Spotlight component | Include when… |\n|------|---------|---------------------|---------------|\n| 1 — Always first | Search | `TextField` (mode: search) | Always. Every toolbar needs a search field regardless of screen type. |\n| 2 | Date Range (From / To) | `DatePicker` × 2 | Date is a primary workflow on this screen — users routinely narrow results by time period (e.g. pay periods, deadlines, week ending). |\n| 3 | Status | `SelectInputField` | Status is a primary workflow — users regularly need to isolate records by approval state, progress, or lifecycle stage. |\n| 4+ | Entity dropdowns (Employee, Project, Dept.) | `SelectInputField` or `AutocompleteInput` | Filtering by person, project, or department is a primary workflow. Admin and manager views only — suppress on single-entity employee views. |\n| Zone A — Left | Reset All | `Button` (variant: ghost or link) | Show only when 1 or more filters deviate from the screen's default state. Hidden when all filters are at their default. |\n| Zone B — Rightmost | Primary action (e.g. \"+ Create Offer\") | `Button` (variant: primary) | Conditional. Appears only when the action is directly related to the content in the table below. |\n\n---\n\n## Component mapping — filter control → Spotlight component\n\n**Deprecation notice.** `Select` is deprecated. All filter dropdowns must use `SelectInputField`.\n\n| Filter type | Component | Size | Key rule |\n|-------------|-----------|------|---------|\n| Text search | `TextField` (mode: search) | **md** | Search mode · Start adornment: search icon · End adornment: clear X · 300ms debounce · `aria-label` required (no visible label) |\n| Single select, <20 items | `SelectInputField` | **md** | Single-select mode · `placeholder` + `aria-label` — no `label` prop · List scannable without search |\n| Multi select, <20 items | `MultiSelectInput` | **md** | OR logic within filter · `placeholder` + `aria-label` — no `label` prop · Select All sticky at top |\n| Searchable entity, 20+ items | `AutocompleteInput` | **md** | Async search for 100+ items · `placeholder` + `aria-label` — no `label` prop · Single select · Legacy Autocomplete deprecated |\n| Multi searchable entity, 20+ items | `MultiAutocompleteInput` | **md** | OR logic · `placeholder` + `aria-label` — no `label` prop · Selections shown as dismissible chips inside trigger field |\n| Date — single | `DatePicker` | **md** | Calendar popup · MM/DD/YYYY · 280–320px popup width · `aria-label` required |\n| Date range | `DatePicker` × 2 | **md** | From/To side-by-side · 105px each · From constrains To minimum · Paired \"Clear dates\" link resets both · `aria-label` on each |\n| Date presets (Today, This Week…) | `ToggleButtonGroup` | Verify in Storybook | Mutually exclusive · Preset auto-populates date range · Include Custom fallback |\n| Boolean toggle (e.g. Read/Unread) | `Switch` | sm: 34×20px · md: 44×24px | Label describes active state · Default off = unfiltered · Immediate update |\n| Mutually exclusive options (2–5) | `ToggleButtonGroup` | Verify in Storybook | All options visible simultaneously · Toolbar use only |\n| Quick filter, combinable (3–6 values) | `Chip` / `ChipGroup` | Verify in Storybook | OR logic within selection · No dropdown · Status is primary candidate |\n| Grouped / hierarchical | No component available | — | Unvalidated use case. Do not implement until research confirms need. |\n\n---\n\n## Chip Row elements\n\nThe chip row is a **single, non-wrapping row**. Chips never wrap to a second line; overflow is\nhandled by the \"+N more\" expand button.\n\n| Element | Component | Condition |\n|---------|-----------|-----------|\n| RemovableChips | `Chip` (dismissible) | One per active filter, up to the visible threshold. Label format: \"FilterName: Value ✕\". |\n| \"+N more\" overflow button | `ToggleButton` + `Popover` (hover) / expand toggle (click) | Shown when applied chips exceed the visible threshold. Two distinct interactions — see below. |\n| Reset Filters | `Button` (variant: ghost) · `margin-left: auto` · **far right** | Visible when ≥1 filter deviates from page default. Always pinned to the right edge of the chip row regardless of how many chips are showing. Returns to page default state (not zero). |\n\n**\"+N more\" — dual interaction model.**\n\n| Trigger | Behavior |\n|---------|----------|\n| **Hover** | Opens a transient `Popover` listing all hidden chips. Dismisses automatically on mouse-out. Provides a quick preview without committing to expanded state. |\n| **Click** | Expands the chip row **in-place** — toggles the row to `flex-wrap` so all chips are visible. Button label changes to \"Show less\". Clicking again collapses back to single-line. This is a persistent, pinned state — does not dismiss on mouse-out. |\n\nThe hover popover and click-expand are independent states. A user can hover to preview and then\nclick to pin — the two interactions do not interfere.\n\n**Reset Filters positioning.** Reset Filters uses `margin-left: auto` to push itself to the far\nright of the chip row at all times — collapsed, expanded, and at every chip count.\n\n---\n\n## Reset behavior\n\n| Mechanism | What it clears | Confirmation |\n|-----------|---------------|--------------|\n| Chip ✕ button | That single filter only | None — immediate |\n| Reset All / Clear all | All active filters — returns to page default state | None — non-destructive |\n\n**Reset All rule.** Reset All returns to the page's default state — not to \"no filters.\" If a\ntable's default shows the current pay period, clearing returns to that default, not to all-time\nunfiltered. Reset All is hidden when no filters deviate from the default state.\n\n---\n\n## Surface variants — Filter Bar vs Condensed Filter Bar\n\n### Filter Bar (1–4 filter controls)\n\nControls sit inline in the Filters slot. Each control applies immediately on change — no Apply\nbutton. Gap between controls: 16px (standard).\n\n### Condensed Filter Bar (5+ filter controls)\n\nTriggered automatically by the component when it receives 5+ controls. Search stays in the Bar\nrow; all filter controls move into the Condensed Filter Drawer piece. A **Filters trigger** button\n(toggle + funnel icon + count badge) replaces the Filters slot.\n\n| Condensed element | Spec |\n|------------------|------|\n| Filters trigger | `ToggleButton` + funnel icon + applied-count badge (shows applied, never draft) |\n| Filter controls | All move into the Condensed Filter Drawer piece (sm · 350px) |\n| Apply model | Batch Apply / Cancel inside the drawer |\n| Chip Row height | 34px (vs 28px standard) |\n| MaxChipCount | Badge-only (32×17px) — no text label |\n| Reset filters | Icon-only ⊗ (≥24px WCAG target) + `aria-label=\"Reset filters\"` + tooltip |\n\n**Auto-adapt.** The component counts controls it receives (Search = 0; Date Range = 1; each other\ncontrol = 1) and switches layout automatically. No screen-level surface selection needed.\n\n---\n\n## Condensed Filter Drawer (piece)\n\nA distinct component piece owned by the Condensed Filter Toolbar (not an instance of the\nstandalone Filter Drawer SPT-223). Shares SPT-223's visual language but has its own header,\nfooter, and behavior.\n\n| Property | Value |\n|----------|-------|\n| Width | sm · 350px (`--drawer-size-width-sm`) |\n| Header | \"Filters\" + ✕ close |\n| Footer | Reset Filters (ghost) · Cancel (outlined) · Apply (solid) |\n| Apply model | Batch — data refreshes once on Apply |\n| Scrim tap / ✕ | Close + **preserve draft state** (reopening restores draft) |\n| Cancel | Discard draft · close |\n| Badge updates | Only on Apply — draft selections never increment the badge |\n\n**Interaction loop:**\n\n| Trigger | Behavior |\n|---------|----------|\n| Click Filters button | Drawer opens · controls pre-populated with applied state |\n| Make selections | Draft state — controls update visually; filters not applied yet |\n| Apply | Drawer closes · chip row updates · results refresh · badge = applied count |\n| Cancel | Drawer closes · draft discarded · chip row and results unchanged |\n| Scrim tap or ✕ | Drawer closes · draft **preserved** · results unchanged |\n| Reset Filters (drawer footer) | All controls cleared · drawer closes · chip row empties · Undo snackbar |\n| Remove chip (chip row) | That filter cleared immediately · Undo snackbar · drawer not required to be open |\n\n---\n\n## Responsive behavior\n\nThree states are defined. The toolbar collapses progressively — entity dropdowns move to the\nFilters drawer at tablet, and all filter controls collapse at mobile.\n\n| Spotlight breakpoint | Viewport | Search | Date Range | Status | Entity dropdowns |\n|---------------------|---------|--------|-----------|--------|-----------------|\n| Desktop + Laptop (≥1025px) | ≥1025px | ✓ Bar | ✓ Inline | ✓ Inline | ✓ Inline |\n| Tablet landscape (769–1024px) | 769–1024px | ✓ Bar | ✓ Inline | ✓ Inline | ✓ Inline (960px accommodates) |\n| Tablet portrait (601–768px) | 601–768px | ✓ Bar | ✓ Inline | ✓ Inline | → Drawer (sm) |\n| Phone (≤600px) | ≤600px | ✓ Bar | → Drawer (xsm) | → Drawer (xsm) | → Drawer (xsm) |\n\nWhen filter controls move to a Drawer at smaller breakpoints, the Drawer uses an explicit Apply\nbecause multiple filters may change simultaneously.\n\nThe Condensed Filter Bar uses its three size variants (`xs` / `sm` / `md`) for responsive behavior\nrather than this breakpoint table.\n\n---\n\n## Motion\n\nAll animations follow Spotlight Motion Guidance. Designers pick an intent — the system resolves\nduration and easing.\n\n| Animation | Classification | Intent | Duration | Feel | Motion | Reduced motion |\n|-----------|---------------|--------|----------|------|--------|---------------|\n| Filters button badge count update | Connect | State | md · 200ms | Productive | Background + border + text color transition simultaneously | Preserved — color transitions are motion-safe |\n| Applied filter chips row appear | Reveal | Enter | lg · 300ms | Expressive | Fade + translateY(-8px) | Instant appear |\n| Applied filter chips row dismiss (Reset All) | Reveal | Exit | md · 200ms | Productive | Fade out | Instant disappear |\n| Individual chip dismiss (✕) | Reveal | Exit | md · 200ms | Productive | Fade out | Instant disappear |\n| Reset All button appear | Reveal | Enter | lg · 300ms | Expressive | Fade in | Instant appear |\n| Reset All button disappear | Reveal | Exit | md · 200ms | Productive | Fade out | Instant disappear |\n| Drawer open | — | Enter | lg · 300ms | — | Slide from right + scrim fade | Instant appear · scrim still fades |\n| Drawer close | — | Exit | md · 200ms | — | Reverse slide + scrim fade | Instant disappear |\n\n**Motion principle — asymmetric timing.** Enter animations (lg · 300ms) are intentionally slower\nthan exit animations (md · 200ms). Users need a moment to orient to new content; they do not\nneed to watch it leave. This applies to the drawer, chip row, and Reset All button throughout\nthe toolbar.\n\n---\n\n## Composing the Filter Toolbar from Spotlight pieces\n\nNo `FilterToolbar` wrapper component exists. Compose the pattern from these existing Spotlight\ncomponents using the CSS structure below.\n\n### HTML/CSS structure\n\n```tsx\n{/* Outer wrapper — flex-column, padding 8px, gap 16px between rows */}\n<div className={styles.filterToolbar}>\n\n {/* Bar row — flex, gap 16px, items-center */}\n <div className={styles.filterBar}>\n {/* Zone A — left, flex-start, 16px gap */}\n <div className={styles.filterControls}>\n <TextField mode=\"search\" /* ... */ /> {/* Slot 1 — always first */}\n <DatePicker /* From */ /> {/* Slot 2 — date range start */}\n <DatePicker /* To */ /> {/* Slot 2 — date range end */}\n <SelectInputField /* Status */ /> {/* Slot 3 */}\n {/* Slot 4+ entity dropdowns if applicable */}\n </div>\n\n {/* Zone B — right, flex-end */}\n <div className={styles.filterActions}>\n {showReset && <Button variant=\"ghost\">Reset Filters</Button>}\n {primaryAction}\n </div>\n </div>\n\n {/* Chip Row — renders only when ≥1 filter active */}\n {hasActiveFilters && (\n <div className={`${styles.chipRow} ${isExpanded ? styles.chipRowExpanded : ''}`}>\n {/* Visible chips — all chips when expanded, capped at threshold when collapsed */}\n {(isExpanded ? allChips : visibleChips).map(chip => (\n <Chip dismissible key={chip.id}>{chip.label}</Chip>\n ))}\n\n {/* \"+N more\" / \"Show less\" expand toggle — only when overflow exists */}\n {overflowCount > 0 && (\n <Button variant=\"ghost\" onClick={() => setIsExpanded(e => !e)}>\n {isExpanded ? 'Show less' : `+${overflowCount} more`}\n </Button>\n )}\n\n {/* Reset Filters — always far right via margin-left: auto */}\n {showReset && (\n <Button variant=\"ghost\" className={styles.resetFilters}>Reset Filters</Button>\n )}\n </div>\n )}\n</div>\n```\n\n### CSS values\n\n```css\n.filterToolbar {\n display: flex;\n flex-direction: column;\n gap: 16px; /* between Bar row and Chip Row */\n padding: 8px; /* all sides */\n width: 100%;\n margin-bottom: 16px; /* gap to table header — var(--space-md) */\n}\n\n/* contained variant */\n.filterToolbar--contained {\n background: var(--color-surface-secondary);\n border-radius: 18px; /* control radius (10px) + padding (8px) */\n}\n\n.filterBar {\n display: flex;\n align-items: center;\n gap: 16px;\n width: 100%;\n}\n\n.filterControls {\n display: flex;\n align-items: center;\n gap: 16px; /* 10px for condensed density */\n flex: 1;\n}\n\n.filterActions {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n}\n\n.chipRow {\n display: flex;\n align-items: center;\n flex-wrap: nowrap; /* single row — no wrapping by default */\n gap: 8px;\n min-height: 28px; /* 34px for condensed variant */\n padding: 0 8px;\n overflow: hidden;\n}\n\n/* Expanded state — reveals all chips */\n.chipRowExpanded {\n flex-wrap: wrap;\n overflow: visible;\n}\n\n/* Reset Filters always pinned to far right */\n.resetFilters {\n margin-left: auto;\n flex-shrink: 0;\n}\n```\n\n### Spotlight components used\n\n| Element | Spotlight component |\n|---------|-------------------|\n| Search field | `TextField` (mode: search, start adornment: search icon, end adornment: clear X) |\n| Single-select dropdown | `SelectInputField` |\n| Multi-select dropdown | `MultiSelectInput` |\n| Date picker | `DatePicker` (×2 for date range) |\n| Applied filter chip | `Chip` (dismissible) |\n| Chip container | `ChipGroup` (flex-wrap) |\n| Overflow trigger | `ToggleButton` + `Popover` + `MaxChipSpillOverList` + `SpillOverItem` |\n| Reset Filters | `Button` (variant: ghost) — standard; icon-only `Button` with aria-label — condensed |\n| Primary action | `Button` (variant: primary) — conditional |\n\n**DateRangePicker gap.** No single composed From/To component exists. Use two `DatePicker`\ninstances side-by-side with a paired \"Clear dates\" link. Will be replaced by `DateRangePicker`\n(FILT-12) when available.\n\n---\n\n## Component gaps\n\n| Gap | Status | Impact |\n|-----|--------|--------|\n| `FilterToolbar` composition component | Not yet built — compose manually per recipe | Teams build own layout using Spotlight pieces |\n| `DateRangePicker` | Missing (FILT-12) | Use 2× `DatePicker` interim |\n| `MultiAutocompleteInput` | New — verify in Storybook | Multi-select entity filters |\n| `Badge xs` size | Missing | Count badge for condensed bar uses `sm` (24px) as fallback |\n\n---\n\n## Related patterns\n\n| Pattern | Purpose |\n|---------|---------|\n| `filter-selection-guide` | Which surface to use (Filter Bar vs Condensed vs Drawer) |\n| `filter-behavior-rules` | AND/OR logic, debounce, server/client, sort/pagination rules |\n| `filter-drawer-pattern` | Standalone Filter Drawer recipe |\n| `filter-communicating-applied-guide` | Applied chips, count badge, results count, zero results |\n| `filter-persistence-guide` | Session, URL param, and saved-view persistence |\n\n---\n\n*Pattern ID: `filter-toolbar-pattern` · Source: SPT-229 v0.4.0, SPT-225 v1.2.0, SPT-262 v1.0.0 · Design spec: Patterns — Timeless V2.0.0*\n",
|
|
561
561
|
"components": [],
|
|
562
562
|
"sourcePath": "content/recipes/filter-toolbar-pattern.md"
|
|
563
563
|
},
|