@handled-ai/design-system 0.18.10 → 0.18.11

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ListFilter, Plus, Search } from \"lucide-react\"\n\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DataTableConditionFilter,\n type ConditionFieldDef,\n type ConditionFilterValue,\n} from \"./data-table-condition-filter\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n /** Filter behavior. Defaults to \"multi\" (checkbox multi-select). */\n type?: \"multi\" | \"single\" | \"boolean\"\n /**\n * Submenu search behavior. Defaults to the DataTableFilter\n * optionSearchThreshold prop. Use true to always show search or false to\n * hide it for a specific category.\n */\n searchable?: boolean | { threshold?: number }\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n /** Fields exposed in the unified condition-builder panel. */\n conditionFields?: ConditionFieldDef[]\n /** Active builder-managed field/operator/value conditions. */\n conditionFilters?: ConditionFilterValue[]\n /** Callback when builder-managed conditions are applied, removed, or cleared. */\n onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void\n /** Dropdown entry label for the condition-builder panel. Default: \"Add filter\". */\n conditionBuilderLabel?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n conditionFields = [],\n conditionFilters = [],\n onConditionFiltersChange,\n conditionBuilderLabel = \"Add filter\",\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n const [conditionBuilderOpen, setConditionBuilderOpen] = React.useState(false)\n const hasConditionBuilder = conditionFields.length > 0\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n return userCount + conditionFilters.length\n }, [selectedFilters, conditionFilters.length])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category?.options.find(\n (opt) => getOptionValue(opt) === value\n )\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold text-foreground\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const filterType = category.type ?? \"multi\"\n\n /* ── Boolean toggle ─────────────────────────────────── */\n if (filterType === \"boolean\") {\n const active = selectedFilters[category.id]?.includes(\"true\") ?? false\n return (\n <DropdownMenuItem\n key={category.id}\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, \"true\")\n }}\n >\n <category.icon className=\"mr-2 h-3.5 w-3.5\" />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuItem>\n )\n }\n\n /* ── Sub-menu (single / multi) ──────────────────────── */\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n const shouldShowSubmenuSearch = (() => {\n if (category.searchable === true) return true\n if (category.searchable === false) return false\n const threshold =\n typeof category.searchable === \"object\"\n ? (category.searchable.threshold ?? optionSearchThreshold)\n : optionSearchThreshold\n return category.options.length > threshold\n })()\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — shown for long lists or categories that opt in. */}\n {shouldShowSubmenuSearch && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : filterType === \"single\" ? (\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n\n {hasConditionBuilder ? (\n <div className=\"border-t border-border p-1\">\n <PopoverPrimitive.Root\n open={conditionBuilderOpen}\n onOpenChange={setConditionBuilderOpen}\n >\n <PopoverPrimitive.Trigger asChild>\n <DropdownMenuItem\n className=\"cursor-pointer py-1.5 text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n setConditionBuilderOpen(true)\n }}\n >\n <Plus className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {conditionBuilderLabel}\n {conditionFilters.length > 0 ? (\n <span className=\"ml-auto rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {conditionFilters.length}\n </span>\n ) : null}\n </DropdownMenuItem>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n side=\"right\"\n sideOffset={8}\n className=\"z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n onEscapeKeyDown={() => setConditionBuilderOpen(false)}\n onInteractOutside={(event) => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-slot=\"dropdown-menu-content\"]')) {\n event.preventDefault()\n }\n }}\n >\n <DataTableConditionFilter\n fields={conditionFields}\n conditions={conditionFilters}\n onConditionsChange={onConditionFiltersChange ?? (() => {})}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </div>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":"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","names":["_a","_b"]}
1
+ {"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ListFilter, Plus, Search } from \"lucide-react\"\n\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DataTableConditionFilter,\n type ConditionFieldDef,\n type ConditionFilterValue,\n} from \"./data-table-condition-filter\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n /** Filter behavior. Defaults to \"multi\" (checkbox multi-select). */\n type?: \"multi\" | \"single\" | \"boolean\"\n /**\n * Submenu search behavior. Defaults to the DataTableFilter\n * optionSearchThreshold prop. Use true to always show search or false to\n * hide it for a specific category.\n */\n searchable?: boolean | { threshold?: number }\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n /** Fields exposed in the unified condition-builder panel. */\n conditionFields?: ConditionFieldDef[]\n /** Active builder-managed field/operator/value conditions. */\n conditionFilters?: ConditionFilterValue[]\n /** Callback when builder-managed conditions are applied, removed, or cleared. */\n onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void\n /** Dropdown entry label for the condition-builder panel. Default: \"Add filter\". */\n conditionBuilderLabel?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n conditionFields = [],\n conditionFilters = [],\n onConditionFiltersChange,\n conditionBuilderLabel = \"Add filter\",\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n const [conditionBuilderOpen, setConditionBuilderOpen] = React.useState(false)\n const hasConditionBuilder = conditionFields.length > 0\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n return userCount + conditionFilters.length\n }, [selectedFilters, conditionFilters.length])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category?.options.find(\n (opt) => getOptionValue(opt) === value\n )\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const filterType = category.type ?? \"multi\"\n\n /* ── Boolean toggle ─────────────────────────────────── */\n if (filterType === \"boolean\") {\n const active = selectedFilters[category.id]?.includes(\"true\") ?? false\n return (\n <DropdownMenuItem\n key={category.id}\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, \"true\")\n }}\n >\n <category.icon className=\"mr-2 h-3.5 w-3.5\" />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuItem>\n )\n }\n\n /* ── Sub-menu (single / multi) ──────────────────────── */\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n const shouldShowSubmenuSearch = (() => {\n if (category.searchable === true) return true\n if (category.searchable === false) return false\n const threshold =\n typeof category.searchable === \"object\"\n ? (category.searchable.threshold ?? optionSearchThreshold)\n : optionSearchThreshold\n return category.options.length > threshold\n })()\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — shown for long lists or categories that opt in. */}\n {shouldShowSubmenuSearch && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : filterType === \"single\" ? (\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n\n {hasConditionBuilder ? (\n <div className=\"border-t border-border p-1\">\n <PopoverPrimitive.Root\n open={conditionBuilderOpen}\n onOpenChange={setConditionBuilderOpen}\n >\n <PopoverPrimitive.Trigger asChild>\n <DropdownMenuItem\n className=\"cursor-pointer py-1.5 text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n setConditionBuilderOpen(true)\n }}\n >\n <Plus className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {conditionBuilderLabel}\n {conditionFilters.length > 0 ? (\n <span className=\"ml-auto rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {conditionFilters.length}\n </span>\n ) : null}\n </DropdownMenuItem>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n side=\"right\"\n sideOffset={8}\n className=\"z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n onEscapeKeyDown={() => setConditionBuilderOpen(false)}\n onInteractOutside={(event) => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-slot=\"dropdown-menu-content\"]')) {\n event.preventDefault()\n }\n }}\n >\n <DataTableConditionFilter\n fields={conditionFields}\n conditions={conditionFilters}\n onConditionsChange={onConditionFiltersChange ?? (() => {})}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </div>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":"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","names":["_a","_b"]}
@@ -12,7 +12,7 @@ import { VariantProps } from 'class-variance-authority';
12
12
  */
13
13
  type PillStatus = "success" | "warning" | "error" | "neutral" | "info";
14
14
  declare const pillVariants: (props?: ({
15
- variant?: "error" | "default" | "secondary" | "destructive" | "outline" | "ghost" | "neutral" | "info" | "warning" | "success" | null | undefined;
15
+ variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "error" | "neutral" | "info" | "warning" | "success" | null | undefined;
16
16
  } & class_variance_authority_types.ClassProp) | undefined) => string;
17
17
  interface PillProps extends React.ComponentProps<"span">, VariantProps<typeof pillVariants> {
18
18
  }
@@ -5,7 +5,7 @@ import { Tabs as Tabs$1 } from 'radix-ui';
5
5
 
6
6
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof Tabs$1.Root>): React.JSX.Element;
7
7
  declare const tabsListVariants: (props?: ({
8
- variant?: "line" | "default" | null | undefined;
8
+ variant?: "default" | "line" | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
10
10
  declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof Tabs$1.List> & VariantProps<typeof tabsListVariants>): React.JSX.Element;
11
11
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof Tabs$1.Trigger>): React.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { ContactChannel, ContactItem, ContactList, ContactListProps } from './co
17
17
  export { ContextualQuickActionContextLabel, ContextualQuickActionContextLabelProps, ContextualQuickActionItem, ContextualQuickActionLauncher, ContextualQuickActionLauncherProps } from './components/contextual-quick-action-launcher.js';
18
18
  export { CheckInsCard, RecentlyCompletedCard, TopTasksCard, UpcomingMeetingsCard } from './components/dashboard-cards.js';
19
19
  export { DataRow, DataTable, DataTableProps } from './components/data-table.js';
20
- export { ConditionFieldDef, ConditionFilterValue, ConditionOperator, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators } from './components/data-table-condition-filter.js';
20
+ export { ConditionFieldDef, ConditionFieldOption, ConditionFilterValue, ConditionOperator, ConditionOptionObject, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators } from './components/data-table-condition-filter.js';
21
21
  export { DataTableDisplay, DataTableDisplayColumn } from './components/data-table-display.js';
22
22
  export { DataTableFilter, DataTableFilterCategory, DataTableFilterProps, FilterOption } from './components/data-table-filter.js';
23
23
  export { DataTableQuickViewValue, DataTableQuickViews } from './components/data-table-quick-views.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
 
3
3
  "name": "@handled-ai/design-system",
4
- "version": "0.18.10",
4
+ "version": "0.18.11",
5
5
  "description": "Handled UI component library (shadcn-style, New York)",
6
6
  "type": "module",
7
7
  "packageManager": "pnpm@9.12.0",
@@ -103,6 +103,23 @@ const dateField: ConditionFieldDef = {
103
103
  type: "date",
104
104
  };
105
105
 
106
+ const selectField: ConditionFieldDef = {
107
+ id: "stage",
108
+ label: "Stage",
109
+ type: "select",
110
+ options: [
111
+ { label: "Qualified", value: "qualified" },
112
+ { label: "Disqualified", value: "disqualified" },
113
+ ],
114
+ };
115
+
116
+ const multiSelectField: ConditionFieldDef = {
117
+ id: "industry",
118
+ label: "Industry",
119
+ type: "multi_select",
120
+ options: ["Finance", { label: "Healthcare", value: "healthcare" }, "Education"],
121
+ };
122
+
106
123
  const allFields: ConditionFieldDef[] = [
107
124
  textField,
108
125
  numberField,
@@ -110,6 +127,12 @@ const allFields: ConditionFieldDef[] = [
110
127
  dateField,
111
128
  ];
112
129
 
130
+ const optionFields: ConditionFieldDef[] = [
131
+ textField,
132
+ selectField,
133
+ multiSelectField,
134
+ ];
135
+
113
136
  describe("DataTableConditionFilter", () => {
114
137
  let onConditionsChange: ReturnType<typeof vi.fn<(conditions: ConditionFilterValue[]) => void>>;
115
138
 
@@ -117,6 +140,27 @@ describe("DataTableConditionFilter", () => {
117
140
  onConditionsChange = vi.fn<(conditions: ConditionFilterValue[]) => void>();
118
141
  });
119
142
 
143
+ const renderOptionFilter = () => {
144
+ const result = render(
145
+ <DataTableConditionFilter
146
+ fields={optionFields}
147
+ conditions={[]}
148
+ onConditionsChange={onConditionsChange}
149
+ />,
150
+ );
151
+
152
+ fireEvent.click(screen.getByText("Add filter"));
153
+ return result;
154
+ };
155
+
156
+ const chooseField = (container: HTMLElement, label: string) => {
157
+ const fieldOption = Array.from(container.querySelectorAll('[data-slot="select-item"]')).find(
158
+ (opt) => opt.textContent?.includes(label),
159
+ );
160
+ expect(fieldOption).not.toBeUndefined();
161
+ fireEvent.click(fieldOption!);
162
+ };
163
+
120
164
  it("renders a polished empty panel with Add filter, disabled Add filter group, and quiet Clear filters", () => {
121
165
  const { container } = render(
122
166
  <DataTableConditionFilter
@@ -151,7 +195,6 @@ describe("DataTableConditionFilter", () => {
151
195
  expect(onConditionsChange).toHaveBeenCalledWith([]);
152
196
  });
153
197
 
154
-
155
198
  it("preserves an in-progress draft when fields are recreated with the same definition", () => {
156
199
  const { rerender } = render(
157
200
  <DataTableConditionFilter
@@ -197,6 +240,94 @@ describe("DataTableConditionFilter", () => {
197
240
  expect(committed?.[0]).toMatchObject({ field: "name", operator: "eq", value: "Acme" });
198
241
  });
199
242
 
243
+ it("renders select options and commits an eq condition", () => {
244
+ const { container } = renderOptionFilter();
245
+ chooseField(container, "Stage");
246
+
247
+ expect(getOperators(selectField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
248
+ fireEvent.click(screen.getByText("Qualified"));
249
+ fireEvent.click(screen.getByText("Apply"));
250
+
251
+ const committed = onConditionsChange.mock.calls.at(-1)?.[0];
252
+ expect(committed).toHaveLength(1);
253
+ expect(committed?.[0]).toMatchObject({
254
+ field: "stage",
255
+ operator: "eq",
256
+ value: "qualified",
257
+ });
258
+ });
259
+
260
+ it("filters searchable select options before committing a selected value", () => {
261
+ const searchableOwnerField: ConditionFieldDef = {
262
+ id: "owner",
263
+ label: "Owner",
264
+ type: "select",
265
+ searchable: true,
266
+ options: [
267
+ { label: "Alice Adams", value: "alice@example.com" },
268
+ { label: "Bob Brown", value: "bob@example.com" },
269
+ ],
270
+ };
271
+
272
+ render(
273
+ <DataTableConditionFilter
274
+ fields={[searchableOwnerField]}
275
+ conditions={[]}
276
+ onConditionsChange={onConditionsChange}
277
+ />,
278
+ );
279
+
280
+ fireEvent.click(screen.getByText("Add filter"));
281
+ fireEvent.change(screen.getByPlaceholderText("Search options..."), {
282
+ target: { value: "bob" },
283
+ });
284
+
285
+ expect(screen.queryByText("Alice Adams")).toBeNull();
286
+ fireEvent.click(screen.getByText("Bob Brown"));
287
+ fireEvent.click(screen.getByText("Apply"));
288
+
289
+ const committed = onConditionsChange.mock.calls.at(-1)?.[0];
290
+ expect(committed).toHaveLength(1);
291
+ expect(committed?.[0]).toMatchObject({
292
+ field: "owner",
293
+ operator: "eq",
294
+ value: "bob@example.com",
295
+ });
296
+ });
297
+
298
+ it("renders multi-select checkboxes, toggles multiple options, and commits an in condition", () => {
299
+ const { container } = renderOptionFilter();
300
+ chooseField(container, "Industry");
301
+
302
+ expect(getOperators(multiSelectField)).toEqual(["in", "is_null", "is_not_null"]);
303
+
304
+ const financeCheckbox = screen.getByRole("checkbox", { name: "Finance" });
305
+ const healthcareCheckbox = screen.getByRole("checkbox", { name: "Healthcare" });
306
+ fireEvent.click(financeCheckbox);
307
+ fireEvent.click(healthcareCheckbox);
308
+
309
+ expect(financeCheckbox.getAttribute("aria-checked")).toBe("true");
310
+ expect(healthcareCheckbox.getAttribute("aria-checked")).toBe("true");
311
+
312
+ fireEvent.click(screen.getByText("Apply"));
313
+
314
+ const committed = onConditionsChange.mock.calls.at(-1)?.[0];
315
+ expect(committed).toHaveLength(1);
316
+ expect(committed?.[0]).toMatchObject({
317
+ field: "industry",
318
+ operator: "in",
319
+ value: ["Finance", "healthcare"],
320
+ });
321
+ });
322
+
323
+ it("omits incomplete multi-select conditions until at least one option is selected", () => {
324
+ const { container } = renderOptionFilter();
325
+ chooseField(container, "Industry");
326
+ fireEvent.click(screen.getByText("Apply"));
327
+
328
+ expect(onConditionsChange.mock.calls.at(-1)?.[0]).toEqual([]);
329
+ });
330
+
200
331
  it("Enter in the value field commits the current draft row", () => {
201
332
  render(
202
333
  <DataTableConditionFilter
@@ -380,9 +511,13 @@ describe("DataTableConditionFilter", () => {
380
511
  expect(screen.getAllByText("And")).toHaveLength(2);
381
512
  });
382
513
 
383
- it("operator options are scalar only and exclude in/contains", () => {
514
+ it("operator options respect the selected field definition", () => {
384
515
  expect(getOperators(textField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
385
- expect(getOperators({ ...textField, operators: ["eq", "in", "is_null"] })).toEqual(["eq", "is_null"]);
516
+ expect(getOperators({ ...textField, operators: ["eq", "in", "is_null"] })).toEqual([
517
+ "eq",
518
+ "in",
519
+ "is_null",
520
+ ]);
386
521
 
387
522
  const { container } = render(
388
523
  <DataTableConditionFilter
@@ -392,9 +527,10 @@ describe("DataTableConditionFilter", () => {
392
527
  />,
393
528
  );
394
529
 
395
- const optionTexts = Array.from(container.querySelectorAll('[data-slot="select-item"]'))
396
- .map((el) => el.textContent);
397
- expect(optionTexts).not.toContain("contains");
530
+ const optionTexts = Array.from(container.querySelectorAll('[data-slot="select-item"]')).map(
531
+ (el) => el.textContent,
532
+ );
533
+ expect(optionTexts).not.toContain("is any of");
398
534
  expect(optionTexts).toContain("is empty");
399
535
  });
400
536
 
@@ -413,7 +549,7 @@ describe("DataTableConditionFilter", () => {
413
549
  expect(optionTexts).toContain("≥");
414
550
  expect(optionTexts).toContain("<");
415
551
  expect(optionTexts).toContain("≤");
416
- expect(optionTexts).not.toContain("contains");
552
+ expect(optionTexts).not.toContain("is any of");
417
553
  });
418
554
 
419
555
  it("generateConditionId returns unique values", () => {
@@ -367,49 +367,4 @@ describe("DataTableFilter", () => {
367
367
 
368
368
  expect(screen.getByText("Advanced filters")).toBeDefined();
369
369
  });
370
-
371
- it("renders badge with bg-muted and text-foreground when activeCount > 0", () => {
372
- render(
373
- <DataTableFilter
374
- categories={[
375
- {
376
- id: "status",
377
- label: "Status",
378
- icon: ListFilter,
379
- options: ["Open", "Closed"],
380
- },
381
- ]}
382
- selectedFilters={{ status: ["Open"] }}
383
- onToggleFilter={() => {}}
384
- />
385
- );
386
-
387
- const badge = screen.getByText("1");
388
- expect(badge.tagName).toBe("SPAN");
389
- expect(badge.className).toContain("bg-muted");
390
- expect(badge.className).toContain("text-foreground");
391
- });
392
-
393
- it("does not render badge when activeCount is 0", () => {
394
- const { container } = render(
395
- <DataTableFilter
396
- categories={[
397
- {
398
- id: "status",
399
- label: "Status",
400
- icon: ListFilter,
401
- options: ["Open", "Closed"],
402
- },
403
- ]}
404
- selectedFilters={{}}
405
- onToggleFilter={() => {}}
406
- />
407
- );
408
-
409
- const button = container.querySelector("button");
410
- expect(button).not.toBeNull();
411
- // The button should contain "Filter" text but no badge span with a count
412
- const spans = button!.querySelectorAll("span.bg-muted");
413
- expect(spans.length).toBe(0);
414
- });
415
370
  });