diff --git a/examples/prototype/src/FilterBuilderDemo.tsx b/examples/prototype/src/FilterBuilderDemo.tsx
new file mode 100644
index 000000000..3d57d15be
--- /dev/null
+++ b/examples/prototype/src/FilterBuilderDemo.tsx
@@ -0,0 +1,317 @@
+import { SchemaRenderer } from '@object-ui/react';
+import '@object-ui/components';
+
+const filterBuilderSchema = {
+ type: 'div',
+ className: 'min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-8',
+ body: [
+ {
+ type: 'div',
+ className: 'max-w-5xl mx-auto space-y-8',
+ body: [
+ // Header
+ {
+ type: 'div',
+ className: 'space-y-2',
+ body: [
+ {
+ type: 'div',
+ className: 'text-3xl font-bold tracking-tight',
+ body: { type: 'text', content: 'Filter Builder Demo' }
+ },
+ {
+ type: 'div',
+ className: 'text-muted-foreground',
+ body: {
+ type: 'text',
+ content: 'Airtable-like filter component with advanced field types and operators'
+ }
+ }
+ ]
+ },
+
+ // Example 1: User Data Filtering with Date and Select
+ {
+ type: 'card',
+ className: 'shadow-lg',
+ body: [
+ {
+ type: 'div',
+ className: 'p-6 border-b',
+ body: [
+ {
+ type: 'div',
+ className: 'text-xl font-semibold',
+ body: { type: 'text', content: 'User Data Filters' }
+ },
+ {
+ type: 'div',
+ className: 'text-sm text-muted-foreground mt-1',
+ body: {
+ type: 'text',
+ content: 'Advanced filtering with date, select, and boolean fields'
+ }
+ }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'p-6',
+ body: {
+ type: 'filter-builder',
+ name: 'userFilters',
+ label: 'User Filters',
+ fields: [
+ { value: 'name', label: 'Name', type: 'text' },
+ { value: 'email', label: 'Email', type: 'text' },
+ { value: 'age', label: 'Age', type: 'number' },
+ {
+ value: 'status',
+ label: 'Status',
+ type: 'select',
+ options: [
+ { value: 'active', label: 'Active' },
+ { value: 'inactive', label: 'Inactive' },
+ { value: 'pending', label: 'Pending' }
+ ]
+ },
+ {
+ value: 'department',
+ label: 'Department',
+ type: 'select',
+ options: [
+ { value: 'engineering', label: 'Engineering' },
+ { value: 'sales', label: 'Sales' },
+ { value: 'marketing', label: 'Marketing' },
+ { value: 'support', label: 'Support' }
+ ]
+ },
+ { value: 'joinDate', label: 'Join Date', type: 'date' },
+ { value: 'isVerified', label: 'Is Verified', type: 'boolean' }
+ ],
+ value: {
+ id: 'root',
+ logic: 'and',
+ conditions: [
+ {
+ id: 'cond-1',
+ field: 'status',
+ operator: 'equals',
+ value: 'active'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ type: 'card',
+ className: 'shadow-lg',
+ body: [
+ {
+ type: 'div',
+ className: 'p-6 border-b',
+ body: [
+ {
+ type: 'div',
+ className: 'text-xl font-semibold',
+ body: { type: 'text', content: 'Product Filters' }
+ },
+ {
+ type: 'div',
+ className: 'text-sm text-muted-foreground mt-1',
+ body: {
+ type: 'text',
+ content: 'Filter products by name, price, category, and stock'
+ }
+ }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'p-6',
+ body: {
+ type: 'filter-builder',
+ name: 'productFilters',
+ label: 'Product Filters',
+ fields: [
+ { value: 'name', label: 'Product Name', type: 'text' },
+ { value: 'price', label: 'Price', type: 'number' },
+ { value: 'category', label: 'Category', type: 'text' },
+ { value: 'stock', label: 'Stock Quantity', type: 'number' },
+ { value: 'brand', label: 'Brand', type: 'text' },
+ { value: 'rating', label: 'Rating', type: 'number' }
+ ],
+ value: {
+ id: 'root',
+ logic: 'or',
+ conditions: [
+ {
+ id: 'cond-1',
+ field: 'price',
+ operator: 'lessThan',
+ value: '100'
+ },
+ {
+ id: 'cond-2',
+ field: 'category',
+ operator: 'equals',
+ value: 'Electronics'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+
+ // Example 3: Empty Filter Builder
+ {
+ type: 'card',
+ className: 'shadow-lg',
+ body: [
+ {
+ type: 'div',
+ className: 'p-6 border-b',
+ body: [
+ {
+ type: 'div',
+ className: 'text-xl font-semibold',
+ body: { type: 'text', content: 'Order Filters' }
+ },
+ {
+ type: 'div',
+ className: 'text-sm text-muted-foreground mt-1',
+ body: {
+ type: 'text',
+ content: 'Start with an empty filter - try adding conditions!'
+ }
+ }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'p-6',
+ body: {
+ type: 'filter-builder',
+ name: 'orderFilters',
+ label: 'Order Filters',
+ fields: [
+ { value: 'orderId', label: 'Order ID', type: 'text' },
+ { value: 'customer', label: 'Customer Name', type: 'text' },
+ { value: 'total', label: 'Total Amount', type: 'number' },
+ { value: 'status', label: 'Order Status', type: 'text' },
+ { value: 'date', label: 'Order Date', type: 'text' },
+ { value: 'shipped', label: 'Shipped', type: 'boolean' }
+ ],
+ value: {
+ id: 'root',
+ logic: 'and',
+ conditions: []
+ }
+ }
+ }
+ ]
+ },
+
+ // Features section
+ {
+ type: 'card',
+ className: 'shadow-lg bg-primary/5 border-primary/20',
+ body: {
+ type: 'div',
+ className: 'p-6',
+ body: [
+ {
+ type: 'div',
+ className: 'text-lg font-semibold mb-4',
+ body: { type: 'text', content: '✨ Features' }
+ },
+ {
+ type: 'div',
+ className: 'grid md:grid-cols-2 gap-4 text-sm',
+ body: [
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Dynamic add/remove filter conditions' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Field-type aware operators' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'AND/OR logic toggling' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Date & Select field support' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Clear all filters button' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Tailwind CSS styled' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Shadcn UI components' } }
+ ]
+ },
+ {
+ type: 'div',
+ className: 'flex items-start gap-2',
+ body: [
+ { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' },
+ { type: 'div', body: { type: 'text', content: 'Schema-driven configuration' } }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+};
+
+function FilterBuilderDemo() {
+ return (
+
+ );
+}
+
+export default FilterBuilderDemo;
diff --git a/examples/prototype/src/main.tsx b/examples/prototype/src/main.tsx
index bef5202a3..3ce6029b4 100644
--- a/examples/prototype/src/main.tsx
+++ b/examples/prototype/src/main.tsx
@@ -2,9 +2,16 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
+import FilterBuilderDemo from './FilterBuilderDemo.tsx'
+
+// Check if URL parameter specifies which demo to show
+const urlParams = new URLSearchParams(window.location.search);
+const demo = urlParams.get('demo');
+
+const DemoApp = demo === 'filter-builder' ? FilterBuilderDemo : App;
createRoot(document.getElementById('root')!).render(
-
+
,
)
diff --git a/packages/components/docs/FilterBuilder.md b/packages/components/docs/FilterBuilder.md
new file mode 100644
index 000000000..786aa3a8a
--- /dev/null
+++ b/packages/components/docs/FilterBuilder.md
@@ -0,0 +1,268 @@
+# Filter Builder Component
+
+An Airtable-like filter builder component for building complex query conditions in Object UI.
+
+## Overview
+
+The Filter Builder component provides a user-friendly interface for creating and managing filter conditions. It supports:
+
+- ✅ Dynamic add/remove filter conditions
+- ✅ Field selection from configurable list
+- ✅ Type-aware operators (text, number, boolean, date, select)
+- ✅ AND/OR logic toggling
+- ✅ Clear all filters button
+- ✅ Date picker support for date fields
+- ✅ Dropdown support for select fields
+- ✅ Schema-driven configuration
+- ✅ Tailwind CSS styled with Shadcn UI components
+
+## Usage
+
+### Basic Example
+
+```typescript
+{
+ type: 'filter-builder',
+ name: 'userFilters',
+ label: 'User Filters',
+ fields: [
+ { value: 'name', label: 'Name', type: 'text' },
+ { value: 'email', label: 'Email', type: 'text' },
+ { value: 'age', label: 'Age', type: 'number' },
+ { value: 'status', label: 'Status', type: 'text' }
+ ],
+ value: {
+ id: 'root',
+ logic: 'and',
+ conditions: [
+ {
+ id: 'cond-1',
+ field: 'status',
+ operator: 'equals',
+ value: 'active'
+ }
+ ]
+ }
+}
+```
+
+### With Select Fields
+
+```typescript
+{
+ type: 'filter-builder',
+ name: 'productFilters',
+ label: 'Product Filters',
+ fields: [
+ { value: 'name', label: 'Product Name', type: 'text' },
+ { value: 'price', label: 'Price', type: 'number' },
+ {
+ value: 'category',
+ label: 'Category',
+ type: 'select',
+ options: [
+ { value: 'electronics', label: 'Electronics' },
+ { value: 'clothing', label: 'Clothing' },
+ { value: 'food', label: 'Food' }
+ ]
+ },
+ { value: 'inStock', label: 'In Stock', type: 'boolean' }
+ ],
+ value: {
+ id: 'root',
+ logic: 'and',
+ conditions: []
+ }
+}
+```
+
+### With Date Fields
+
+```typescript
+{
+ type: 'filter-builder',
+ name: 'orderFilters',
+ label: 'Order Filters',
+ fields: [
+ { value: 'orderId', label: 'Order ID', type: 'text' },
+ { value: 'amount', label: 'Amount', type: 'number' },
+ { value: 'orderDate', label: 'Order Date', type: 'date' },
+ { value: 'shipped', label: 'Shipped', type: 'boolean' }
+ ],
+ showClearAll: true
+}
+```
+
+## Props
+
+### Schema Properties
+
+| Property | Type | Required | Description |
+|----------|------|----------|-------------|
+| `type` | `string` | ✅ | Must be `'filter-builder'` |
+| `name` | `string` | ✅ | Form field name for the filter value |
+| `label` | `string` | ❌ | Label displayed above the filter builder |
+| `fields` | `Field[]` | ✅ | Array of available fields for filtering |
+| `value` | `FilterGroup` | ❌ | Initial filter configuration |
+| `showClearAll` | `boolean` | ❌ | Show "Clear all" button (default: true) |
+
+### Field Type
+
+```typescript
+interface Field {
+ value: string; // Field identifier
+ label: string; // Display label
+ type?: string; // Field type: 'text' | 'number' | 'boolean' | 'date' | 'select'
+ options?: Array<{ value: string; label: string }> // For select fields
+}
+```
+
+### FilterGroup Type
+
+```typescript
+interface FilterGroup {
+ id: string; // Group identifier
+ logic: 'and' | 'or'; // Logic operator
+ conditions: FilterCondition[]; // Array of conditions
+}
+
+interface FilterCondition {
+ id: string; // Condition identifier
+ field: string; // Field value
+ operator: string; // Operator (see below)
+ value: string | number | boolean; // Filter value
+}
+```
+
+## Operators
+
+The available operators change based on the field type:
+
+### Text Fields
+- `equals` - Equals
+- `notEquals` - Does not equal
+- `contains` - Contains
+- `notContains` - Does not contain
+- `isEmpty` - Is empty
+- `isNotEmpty` - Is not empty
+
+### Number Fields
+- `equals` - Equals
+- `notEquals` - Does not equal
+- `greaterThan` - Greater than
+- `lessThan` - Less than
+- `greaterOrEqual` - Greater than or equal
+- `lessOrEqual` - Less than or equal
+- `isEmpty` - Is empty
+- `isNotEmpty` - Is not empty
+
+### Boolean Fields
+- `equals` - Equals
+- `notEquals` - Does not equal
+
+### Date Fields
+- `equals` - Equals
+- `notEquals` - Does not equal
+- `before` - Before
+- `after` - After
+- `between` - Between
+- `isEmpty` - Is empty
+- `isNotEmpty` - Is not empty
+
+### Select Fields
+- `equals` - Equals
+- `notEquals` - Does not equal
+- `in` - In
+- `notIn` - Not in
+- `isEmpty` - Is empty
+- `isNotEmpty` - Is not empty
+
+## Events
+
+The component emits change events when the filter configuration is modified:
+
+```typescript
+{
+ target: {
+ name: 'filters',
+ value: {
+ id: 'root',
+ logic: 'and',
+ conditions: [...]
+ }
+ }
+}
+```
+
+## Demo
+
+To see the filter builder in action:
+
+```bash
+pnpm --filter prototype dev
+# Visit http://localhost:5173/?demo=filter-builder
+```
+
+## Styling
+
+The component is fully styled with Tailwind CSS and follows the Object UI design system. All Shadcn UI components are used for consistent look and feel.
+
+You can customize the appearance using the `className` prop or by overriding Tailwind classes.
+
+## Integration
+
+The Filter Builder integrates seamlessly with Object UI's schema system and can be used in:
+
+- Forms
+- Data tables
+- Search interfaces
+- Admin panels
+- Dashboard filters
+
+## Example in Context
+
+```typescript
+const pageSchema = {
+ type: 'page',
+ title: 'User Management',
+ body: [
+ {
+ type: 'card',
+ body: [
+ {
+ type: 'filter-builder',
+ name: 'userFilters',
+ label: 'Filter Users',
+ fields: [
+ { value: 'name', label: 'Name', type: 'text' },
+ { value: 'email', label: 'Email', type: 'text' },
+ { value: 'age', label: 'Age', type: 'number' },
+ { value: 'department', label: 'Department', type: 'text' },
+ { value: 'active', label: 'Active', type: 'boolean' }
+ ]
+ }
+ ]
+ },
+ {
+ type: 'table',
+ // table configuration...
+ }
+ ]
+};
+```
+
+## Technical Details
+
+- Built with React 18+ hooks
+- Uses Radix UI primitives (Select, Popover)
+- Type-safe with TypeScript
+- Accessible keyboard navigation
+- Responsive design
+
+## Browser Support
+
+Works in all modern browsers that support:
+- ES6+
+- CSS Grid
+- Flexbox
+- crypto.randomUUID()
diff --git a/packages/components/metadata/FilterBuilder.component.yml b/packages/components/metadata/FilterBuilder.component.yml
new file mode 100644
index 000000000..5125c0cf7
--- /dev/null
+++ b/packages/components/metadata/FilterBuilder.component.yml
@@ -0,0 +1,39 @@
+name: FilterBuilder
+label: Filter Builder
+description: Airtable-like filter builder for creating complex query conditions
+category: complex
+version: 1.0.0
+framework: react
+
+props:
+ - name: label
+ type: string
+ description: Label text displayed above the filter builder
+ - name: name
+ type: string
+ required: true
+ description: Form field name for the filter value
+ - name: fields
+ type: array
+ required: true
+ description: Available fields for filtering
+ schema:
+ - value: string
+ - label: string
+ - type: string
+ - name: value
+ type: object
+ description: Current filter configuration
+ schema:
+ - id: string
+ - logic: enum[and, or]
+ - conditions: array
+
+events:
+ - name: onChange
+ payload: "{ name: string, value: FilterGroup }"
+
+features:
+ dynamic_conditions: true
+ multiple_operators: true
+ field_type_aware: true
diff --git a/packages/components/src/renderers/complex/filter-builder.tsx b/packages/components/src/renderers/complex/filter-builder.tsx
new file mode 100644
index 000000000..0518d2f84
--- /dev/null
+++ b/packages/components/src/renderers/complex/filter-builder.tsx
@@ -0,0 +1,67 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { FilterBuilder, type FilterGroup } from '@/ui/filter-builder';
+
+ComponentRegistry.register('filter-builder',
+ ({ schema, className, onChange, ...props }) => {
+ const handleChange = (value: FilterGroup) => {
+ if (onChange) {
+ onChange({
+ target: {
+ name: schema.name,
+ value: value,
+ },
+ });
+ }
+ };
+
+ return (
+
+ {schema.label && (
+
+ )}
+
+
+ );
+ },
+ {
+ label: 'Filter Builder',
+ inputs: [
+ { name: 'label', type: 'string', label: 'Label' },
+ { name: 'name', type: 'string', label: 'Name', required: true },
+ {
+ name: 'fields',
+ type: 'array',
+ label: 'Fields',
+ description: 'Array of { value: string, label: string, type?: string } objects',
+ required: true
+ },
+ {
+ name: 'value',
+ type: 'object',
+ label: 'Initial Value',
+ description: 'FilterGroup object with conditions'
+ }
+ ],
+ defaultProps: {
+ label: 'Filters',
+ name: 'filters',
+ fields: [
+ { value: 'name', label: 'Name', type: 'text' },
+ { value: 'email', label: 'Email', type: 'text' },
+ { value: 'age', label: 'Age', type: 'number' },
+ { value: 'status', label: 'Status', type: 'text' }
+ ],
+ value: {
+ id: 'root',
+ logic: 'and',
+ conditions: []
+ }
+ }
+ }
+);
diff --git a/packages/components/src/renderers/complex/index.ts b/packages/components/src/renderers/complex/index.ts
index 4957d296e..29d7b0fa2 100644
--- a/packages/components/src/renderers/complex/index.ts
+++ b/packages/components/src/renderers/complex/index.ts
@@ -1,4 +1,5 @@
import './carousel';
+import './filter-builder';
import './scroll-area';
import './resizable';
import './table';
diff --git a/packages/components/src/ui/filter-builder.tsx b/packages/components/src/ui/filter-builder.tsx
new file mode 100644
index 000000000..8c973c85f
--- /dev/null
+++ b/packages/components/src/ui/filter-builder.tsx
@@ -0,0 +1,359 @@
+"use client"
+
+import * as React from "react"
+import { X, Plus, Trash2 } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/ui/button"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/select"
+import { Input } from "@/ui/input"
+
+export interface FilterCondition {
+ id: string
+ field: string
+ operator: string
+ value: string | number | boolean
+}
+
+export interface FilterGroup {
+ id: string
+ logic: "and" | "or"
+ conditions: FilterCondition[]
+}
+
+export interface FilterBuilderProps {
+ fields?: Array<{
+ value: string
+ label: string
+ type?: string
+ options?: Array<{ value: string; label: string }> // For select fields
+ }>
+ value?: FilterGroup
+ onChange?: (value: FilterGroup) => void
+ className?: string
+ showClearAll?: boolean
+}
+
+const defaultOperators = [
+ { value: "equals", label: "Equals" },
+ { value: "notEquals", label: "Does not equal" },
+ { value: "contains", label: "Contains" },
+ { value: "notContains", label: "Does not contain" },
+ { value: "isEmpty", label: "Is empty" },
+ { value: "isNotEmpty", label: "Is not empty" },
+ { value: "greaterThan", label: "Greater than" },
+ { value: "lessThan", label: "Less than" },
+ { value: "greaterOrEqual", label: "Greater than or equal" },
+ { value: "lessOrEqual", label: "Less than or equal" },
+ { value: "before", label: "Before" },
+ { value: "after", label: "After" },
+ { value: "between", label: "Between" },
+ { value: "in", label: "In" },
+ { value: "notIn", label: "Not in" },
+]
+
+const textOperators = ["equals", "notEquals", "contains", "notContains", "isEmpty", "isNotEmpty"]
+const numberOperators = ["equals", "notEquals", "greaterThan", "lessThan", "greaterOrEqual", "lessOrEqual", "isEmpty", "isNotEmpty"]
+const booleanOperators = ["equals", "notEquals"]
+const dateOperators = ["equals", "notEquals", "before", "after", "between", "isEmpty", "isNotEmpty"]
+const selectOperators = ["equals", "notEquals", "in", "notIn", "isEmpty", "isNotEmpty"]
+
+function FilterBuilder({
+ fields = [],
+ value,
+ onChange,
+ className,
+ showClearAll = true,
+}: FilterBuilderProps) {
+ const [filterGroup, setFilterGroup] = React.useState(
+ value || {
+ id: "root",
+ logic: "and",
+ conditions: [],
+ }
+ )
+
+ React.useEffect(() => {
+ if (value && JSON.stringify(value) !== JSON.stringify(filterGroup)) {
+ setFilterGroup(value)
+ }
+ }, [value])
+
+ const handleChange = (newGroup: FilterGroup) => {
+ setFilterGroup(newGroup)
+ onChange?.(newGroup)
+ }
+
+ const addCondition = () => {
+ const newCondition: FilterCondition = {
+ id: crypto.randomUUID(),
+ field: fields[0]?.value || "",
+ operator: "equals",
+ value: "",
+ }
+ handleChange({
+ ...filterGroup,
+ conditions: [...filterGroup.conditions, newCondition],
+ })
+ }
+
+ const removeCondition = (conditionId: string) => {
+ handleChange({
+ ...filterGroup,
+ conditions: filterGroup.conditions.filter((c) => c.id !== conditionId),
+ })
+ }
+
+ const clearAllConditions = () => {
+ handleChange({
+ ...filterGroup,
+ conditions: [],
+ })
+ }
+
+ const updateCondition = (conditionId: string, updates: Partial) => {
+ handleChange({
+ ...filterGroup,
+ conditions: filterGroup.conditions.map((c) =>
+ c.id === conditionId ? { ...c, ...updates } : c
+ ),
+ })
+ }
+
+ const toggleLogic = () => {
+ handleChange({
+ ...filterGroup,
+ logic: filterGroup.logic === "and" ? "or" : "and",
+ })
+ }
+
+ const getOperatorsForField = (fieldValue: string) => {
+ const field = fields.find((f) => f.value === fieldValue)
+ const fieldType = field?.type || "text"
+
+ switch (fieldType) {
+ case "number":
+ return defaultOperators.filter((op) => numberOperators.includes(op.value))
+ case "boolean":
+ return defaultOperators.filter((op) => booleanOperators.includes(op.value))
+ case "date":
+ return defaultOperators.filter((op) => dateOperators.includes(op.value))
+ case "select":
+ return defaultOperators.filter((op) => selectOperators.includes(op.value))
+ case "text":
+ default:
+ return defaultOperators.filter((op) => textOperators.includes(op.value))
+ }
+ }
+
+ const needsValueInput = (operator: string) => {
+ return !["isEmpty", "isNotEmpty"].includes(operator)
+ }
+
+ const getInputType = (fieldValue: string) => {
+ const field = fields.find((f) => f.value === fieldValue)
+ const fieldType = field?.type || "text"
+
+ switch (fieldType) {
+ case "number":
+ return "number"
+ case "date":
+ return "date"
+ default:
+ return "text"
+ }
+ }
+
+ const renderValueInput = (condition: FilterCondition) => {
+ const field = fields.find((f) => f.value === condition.field)
+
+ // For select fields with options
+ if (field?.type === "select" && field.options) {
+ return (
+
+ )
+ }
+
+ // For boolean fields
+ if (field?.type === "boolean") {
+ return (
+
+ )
+ }
+
+ // Default input for text, number, date
+ const inputType = getInputType(condition.field)
+
+ // Format value based on field type
+ const formatValue = () => {
+ if (!condition.value) return ""
+ if (inputType === "date" && typeof condition.value === "string") {
+ // Ensure date is in YYYY-MM-DD format
+ return condition.value.split('T')[0]
+ }
+ return String(condition.value)
+ }
+
+ // Handle value change with proper type conversion
+ const handleValueChange = (newValue: string) => {
+ let convertedValue: string | number | boolean = newValue
+
+ if (field?.type === "number" && newValue !== "") {
+ convertedValue = parseFloat(newValue) || 0
+ } else if (field?.type === "date") {
+ convertedValue = newValue // Keep as ISO string
+ }
+
+ updateCondition(condition.id, { value: convertedValue })
+ }
+
+ return (
+ handleValueChange(e.target.value)}
+ />
+ )
+ }
+
+ return (
+
+
+
+ Where
+ {filterGroup.conditions.length > 1 && (
+
+ )}
+
+ {showClearAll && filterGroup.conditions.length > 0 && (
+
+ )}
+
+
+
+ {filterGroup.conditions.map((condition) => (
+
+
+
+
+
+
+
+
+
+
+ {needsValueInput(condition.operator) && (
+
+ {renderValueInput(condition)}
+
+ )}
+
+
+
+
+ ))}
+
+
+
+
+ )
+}
+
+FilterBuilder.displayName = "FilterBuilder"
+
+export { FilterBuilder }
diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts
index 7d71e7829..309d56584 100644
--- a/packages/components/src/ui/index.ts
+++ b/packages/components/src/ui/index.ts
@@ -20,6 +20,7 @@ export * from './drawer';
export * from './dropdown-menu';
export * from './empty';
export * from './field';
+export * from './filter-builder';
export * from './form';
export * from './hover-card';
export * from './input-group';