filter styles and variant

This commit is contained in:
Saxon Fletcher committed 2025-12-08 13:05:25 +10:00
1 parent 5dcc1e6999
commit 6137785fc0
7 files changed
+90 -54

No files matched your search

@@ -64,8 +64,8 @@ export type HeaderProps = {
const DefaultHeader = () => {
return (
<div className="flex items-center gap-2 flex-1">
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0 flex items-center gap-2 flex-1 h-full">
<div className="flex-1 min-w-0 h-full">
<FilterPopover />
</div>
<div className="flex items-center gap-2 shrink-0">
@@ -202,7 +202,7 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
return (
<div>
<div className="flex h-10 items-center justify-between bg-dash-sidebar dark:bg-surface-100 px-1.5 py-1.5 gap-2 overflow-x-auto ">
<div className="flex h-10 items-stretch justify-between bg-dash-sidebar dark:bg-surface-100 px-1.5 py-0 gap-2 overflow-x-auto ">
{customHeader ? (
customHeader
) : snap.selectedRows.size > 0 ? (
@@ -139,7 +139,10 @@ export const FilterPopover = ({ portal = true }: FilterPopoverProps) => {
// Create filter properties from table columns
const filterProperties: FilterProperty[] = useMemo(() => {
const columns = snap.table?.columns ?? []
const defaultOperators = FilterOperatorOptions.map((op) => ({ value: op.value, label: op.label }))
const defaultOperators = FilterOperatorOptions.map((op) => ({
value: op.value,
label: op.label,
}))
const stringOperators = [
{ value: '~~*', label: 'ilike operator' },
...defaultOperators.filter((op) => op.value !== '~~*'),
@@ -267,6 +270,7 @@ export const FilterPopover = ({ portal = true }: FilterPopoverProps) => {
onFreeformTextChange={setFreeformText}
actions={actions}
isLoading={isGenerating}
variant="pill"
className="bg-transparent border-0"
/>
)
@@ -274,7 +274,7 @@ const FunctionsList = () => {
disabled={!canCreateFunctions}
onClick={() => createFunction()}
className="flex-grow"
size="small"
size="tiny"
tooltip={{
content: {
side: 'bottom',
@@ -288,7 +288,7 @@ const FunctionsList = () => {
</ButtonTooltip>
<ButtonTooltip
type="default"
size="small"
size="tiny"
disabled={!canCreateFunctions}
className="px-1 pointer-events-auto"
icon={<AiIconAnimation size={16} />}
@@ -3,7 +3,7 @@
import { motion } from 'framer-motion'
import { Search } from 'lucide-react'
import { cn } from 'ui'
import { FilterBarRoot, useFilterBar } from './FilterBarContext'
import { FilterBarRoot, useFilterBar, type FilterBarVariant } from './FilterBarContext'
import { FilterGroup } from './FilterGroup'
import { FilterBarAction, FilterGroup as FilterGroupType, FilterProperty } from './types'
@@ -17,22 +17,30 @@ export type FilterBarProps = {
isLoading?: boolean
className?: string
supportsOperators?: boolean
variant?: FilterBarVariant
}
function FilterBarContent({ className }: { className?: string }) {
const { filters, error, optionsError, isLoading } = useFilterBar()
const { filters, error, optionsError, isLoading, variant } = useFilterBar()
return (
<div className="w-full space-y-2 relative">
<div
className={cn(
'relative flex items-center gap-2 w-full border rounded-md h-[34px] bg-foreground/[.026] cursor-text p-0 px-2 overflow-auto',
'relative flex items-stretch gap-0 w-full border rounded-md h-full bg-foreground/[.026] cursor-text p-0 pr-2 overflow-auto',
className
)}
>
<Search className="text-foreground-muted w-4 h-4 sticky left-0 shrink-0" />
<div
className={cn(
'flex items-center shrink-0 px-2 bg-surface-200',
variant === 'pill' ? 'bg-transparent border-r-0' : 'border-r'
)}
>
<Search className="text-foreground-muted w-4 h-4 sticky" />
</div>
<motion.div
className="flex-1 flex flex-wrap items-center gap-1 h-full"
className="flex-1 flex flex-wrap items-stretch gap-0"
animate={{ opacity: isLoading ? 0.5 : 1 }}
transition={{
duration: 1,
@@ -88,6 +96,7 @@ export function FilterBar({
isLoading,
className,
supportsOperators = false,
variant = 'default',
}: FilterBarProps) {
return (
<FilterBarRoot
@@ -99,6 +108,7 @@ export function FilterBar({
actions={actions}
isLoading={isLoading}
supportsOperators={supportsOperators}
variant={variant}
>
<FilterBarContent className={className} />
</FilterBarRoot>
@@ -1,18 +1,18 @@
'use client'
import React, { createContext, useContext, useRef, useCallback } from 'react'
import { FilterBarAction, FilterProperty, FilterGroup } from './types'
import React, { createContext, useCallback, useContext, useRef } from 'react'
import { ActiveInput, useFilterBarState, useOptionsCache } from './hooks'
import { useKeyboardNavigation } from './useKeyboardNavigation'
import { useCommandHandling } from './useCommandHandling'
import { MenuItem } from './menuItems'
import { FilterBarAction, FilterGroup, FilterProperty } from './types'
import { useCommandHandling } from './useCommandHandling'
import { useKeyboardNavigation } from './useKeyboardNavigation'
import {
findConditionByPath,
isAsyncOptionsFunction,
removeFromGroup,
updateNestedValue,
updateNestedOperator,
updateNestedLogicalOperator,
updateNestedOperator,
updateNestedValue,
} from './utils'
export type FilterBarContextValue = {
@@ -49,6 +49,7 @@ export type FilterBarContextValue = {
// Config
supportsOperators: boolean
variant: FilterBarVariant
actions?: FilterBarAction[]
// Refs
@@ -75,8 +76,11 @@ export type FilterBarRootProps = {
actions?: FilterBarAction[]
isLoading?: boolean
supportsOperators?: boolean
variant?: FilterBarVariant
}
export type FilterBarVariant = 'default' | 'pill'
export function FilterBarRoot({
children,
filterProperties,
@@ -87,6 +91,7 @@ export function FilterBarRoot({
actions,
isLoading: externalLoading,
supportsOperators = false,
variant = 'default',
}: FilterBarRootProps) {
const rootRef = useRef<HTMLDivElement>(null)
@@ -100,12 +105,8 @@ export function FilterBarRoot({
setIsCommandMenuVisible,
} = useFilterBarState()
const {
loadingOptions,
propertyOptionsCache,
loadPropertyOptions,
optionsError,
} = useOptionsCache()
const { loadingOptions, propertyOptionsCache, loadPropertyOptions, optionsError } =
useOptionsCache()
const handleInputChange = useCallback(
(path: number[], value: string) => {
@@ -178,7 +179,14 @@ export function FilterBarRoot({
}
}
},
[filters, filterProperties, loadPropertyOptions, setActiveInput, setIsCommandMenuVisible, hideTimeoutRef]
[
filters,
filterProperties,
loadPropertyOptions,
setActiveInput,
setIsCommandMenuVisible,
hideTimeoutRef,
]
)
const handleOperatorFocus = useCallback(
@@ -284,6 +292,7 @@ export function FilterBarRoot({
// Config
supportsOperators,
variant,
actions,
// Refs
@@ -292,7 +301,7 @@ export function FilterBarRoot({
return (
<FilterBarContext.Provider value={contextValue}>
<div ref={rootRef} data-filterbar-root>
<div ref={rootRef} data-filterbar-root className="h-full min-h-[26px] flex items-stretch">
{children}
</div>
</FilterBarContext.Provider>
@@ -4,6 +4,7 @@ import { X } from 'lucide-react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
Button,
cn,
Input_Shadcn_,
Popover_Shadcn_,
PopoverAnchor_Shadcn_,
@@ -44,6 +45,7 @@ export function FilterCondition({
handleRemoveCondition,
handleSelectMenuItem,
setActiveInput,
variant,
} = useFilterBar()
const operatorRef = useRef<HTMLInputElement>(null)
@@ -190,17 +192,20 @@ export function FilterCondition({
return (
<div
ref={wrapperRef}
className="flex items-center rounded px-2 h-6 bg-muted border group shrink-0"
className={cn(
'flex items-stretch px-0 bg-muted group shrink-0',
variant === 'pill' ? 'rounded border' : 'border-r'
)}
>
<span
className="text-xs mr-1 cursor-pointer shrink-0 whitespace-nowrap text-foreground-light"
className="text-xs pl-2 pr-1 cursor-pointer shrink-0 whitespace-nowrap text-foreground-light h-full flex items-center"
onClick={() => handleLabelClick(path)}
>
{property.label}
</span>
<Popover_Shadcn_ open={isOperatorActive && !isLoading && operatorItems.length > 0}>
<PopoverAnchor_Shadcn_ asChild>
<div className="relative inline-block mr-1">
<div className="relative inline-block">
<Input_Shadcn_
ref={operatorRef}
type="text"
@@ -209,11 +214,11 @@ export function FilterCondition({
onFocus={() => handleOperatorFocus(path)}
onBlur={handleOperatorBlur}
onKeyDown={handleOperatorKeyDown}
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 text-foreground-light w-full absolute left-0 top-0"
className="h-full border-none bg-transparent py-0 px-1 text-center text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-foreground-light w-full absolute left-0 top-0"
disabled={isLoading}
aria-label={`Operator for ${property.label}`}
/>
<span className="invisible whitespace-pre text-xs block h-6">
<span className="invisible whitespace-pre text-xs block px-1 shrink-0 px-1">
{condition.operator || ' '}
</span>
</div>
@@ -242,7 +247,7 @@ export function FilterCondition({
</Popover_Shadcn_>
<Popover_Shadcn_ open={isActive && !isLoading && (showValueCustom || valueItems.length > 0)}>
<PopoverAnchor_Shadcn_ asChild>
<div className="relative inline-block mr-1 max-w-[150px]">
<div className="relative inline-block max-w-[150px]">
<Input_Shadcn_
ref={valueRef}
type="text"
@@ -251,11 +256,11 @@ export function FilterCondition({
onFocus={() => handleInputFocus(path)}
onBlur={handleValueBlur}
onKeyDown={handleValueKeyDown}
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 w-full absolute left-0 top-0"
className="h-full border-none bg-transparent py-0 px-1 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0"
disabled={isLoading}
aria-label={`Value for ${property.label}`}
/>
<span className="invisible whitespace-pre text-xs block h-6">
<span className="invisible whitespace-pre text-xs block px-1">
{(condition.value ?? '').toString() || ' '}
</span>
</div>
@@ -306,13 +311,13 @@ export function FilterCondition({
size="tiny"
icon={
<X
strokeWidth={1.5}
size={12}
className="group-hover:text-foreground text-foreground-light"
strokeWidth={1}
size={10}
className="group-hover:text-foreground text-foreground-lighter"
/>
}
onClick={onRemove}
className="group hover:text-foreground !hover:bg-surface-600 p-0"
className="group hover:text-foreground !hover:bg-surface-600 rounded-none px-1 h-auto py-0 px-1"
aria-label={`Remove ${property.label} filter`}
/>
</div>
@@ -1,7 +1,13 @@
'use client'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Input_Shadcn_, Popover_Shadcn_, PopoverAnchor_Shadcn_, PopoverContent_Shadcn_ } from 'ui'
import {
cn,
Input_Shadcn_,
Popover_Shadcn_,
PopoverAnchor_Shadcn_,
PopoverContent_Shadcn_,
} from 'ui'
import { DefaultCommandList } from './DefaultCommandList'
import { useFilterBar } from './FilterBarContext'
import { FilterCondition } from './FilterCondition'
@@ -23,6 +29,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
isLoading,
supportsOperators,
actions,
variant,
handleInputBlur,
handleGroupFreeformFocus,
handleGroupFreeformChange,
@@ -83,14 +90,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
actions,
supportsOperators,
}),
[
filterProperties,
isActive,
freeformText,
localFreeformValue,
actions,
supportsOperators,
]
[filterProperties, isActive, freeformText, localFreeformValue, actions, supportsOperators]
)
// Only the root group should expand to fill available space
@@ -115,13 +115,19 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
return (
<div
ref={wrapperRef}
className={`flex items-center gap-1 rounded shrink-0 ${
className={`flex items-stretch gap-0 shrink-0 ${
path.length > 0
? "before:content-['('] before:text-foreground-muted after:content-[')'] after:text-foreground-muted"
: ''
} ${isRootGroup ? 'flex-1 min-w-0' : ''}`}
} ${isRootGroup ? 'flex-1 min-w-0' : ''} ${variant === 'pill' ? 'py-2' : ''}`}
>
<div className={`flex items-center gap-1 ${isRootGroup ? 'flex-1 min-w-0' : ''}`}>
<div
className={cn(
'flex items-stretch',
isRootGroup ? 'flex-1 min-w-0' : '',
variant === 'pill' ? 'gap-1' : 'gap-0'
)}
>
{group.conditions.map((condition, index) => {
const currentPath = [...path, index]
@@ -165,8 +171,10 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
onFocus={() => handleGroupFreeformFocus(path)}
onBlur={handleFreeformBlur}
onKeyDown={handleFreeformKeyDown}
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 w-full flex-1 min-w-0"
placeholder={group.conditions.length === 0 ? 'Search or filter...' : '+'}
className="border-none bg-transparent text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full flex-1 h-auto min-w-0 px-2 py-0"
placeholder={
group.conditions.length === 0 ? 'Search or filter...' : '+ Search or filter...'
}
disabled={isLoading}
/>
) : (
@@ -179,11 +187,11 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
onFocus={() => handleGroupFreeformFocus(path)}
onBlur={handleFreeformBlur}
onKeyDown={handleFreeformKeyDown}
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 w-full absolute left-0 top-0"
placeholder="+"
className="h-full border-none bg-transparent py-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0 px-2"
placeholder="+ Add filter"
disabled={isLoading}
/>
<span className="invisible whitespace-pre text-xs block h-6">
<span className="invisible whitespace-pre text-xs block">
{(isActive ? freeformText : localFreeformValue) || '+'}
</span>
</div>