diff --git a/src/components/FilterBuilder.tsx b/src/components/FilterBuilder.tsx
index 0b018b8a..6760d1bf 100644
--- a/src/components/FilterBuilder.tsx
+++ b/src/components/FilterBuilder.tsx
@@ -1,14 +1,11 @@
-import { Plus, X, CalendarBlank, WarningCircle } from '@phosphor-icons/react'
-import { format } from 'date-fns'
+import { Plus, X, WarningCircle } from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
-import { Calendar } from '@/components/ui/calendar'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { cn } from '@/lib/utils'
import type { FilterCondition, FilterOp, FilterGroup, FilterNode } from '../types'
-import { parseDateFilterInput } from '../utils/filterDates'
import { FilterFieldCombobox } from './FilterFieldCombobox'
+import { DateValueInput } from './filter-builder/DateValueInput'
const OPERATORS: { value: FilterOp; label: string }[] = [
{ value: 'equals', label: 'equals' },
@@ -81,43 +78,6 @@ function OperatorSelect({ value, onChange }: {
)
}
-function DateValueInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
- const selected = value ? parseDateFilterInput(value) ?? undefined : undefined
- return (
-
-
onChange(e.target.value)}
- data-testid="date-value-input"
- />
-
-
-
-
-
- onChange(day ? format(day, 'yyyy-MM-dd') : '')}
- />
-
-
-
- )
-}
-
function TextValueInput({ value, onChange, regexEnabled, regexSupported, invalidRegex, onToggleRegex }: {
value: string
onChange: (v: string) => void
diff --git a/src/components/filter-builder/DateValueInput.test.tsx b/src/components/filter-builder/DateValueInput.test.tsx
new file mode 100644
index 00000000..ac29b9a9
--- /dev/null
+++ b/src/components/filter-builder/DateValueInput.test.tsx
@@ -0,0 +1,59 @@
+import { useState } from 'react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { act, fireEvent, render, screen } from '@testing-library/react'
+import { DateValueInput } from './DateValueInput'
+
+describe('DateValueInput', () => {
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ function renderControlledDateValueInput(initialValue = '') {
+ function ControlledDateValueInput() {
+ const [value, setValue] = useState(initialValue)
+ return
+ }
+
+ return render()
+ }
+
+ it('shows a debounced resolved-date preview while focused', async () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2026-04-08T12:00:00Z'))
+
+ renderControlledDateValueInput()
+
+ const input = screen.getByTestId('date-value-input')
+ fireEvent.focus(input)
+ fireEvent.change(input, { target: { value: '10 days ago' } })
+
+ expect(screen.queryByTestId('date-value-preview')).not.toBeInTheDocument()
+
+ await act(async () => {
+ vi.advanceTimersByTime(250)
+ })
+
+ expect(screen.getByTestId('date-value-preview')).toHaveTextContent('Resolves to March 29, 2026')
+ })
+
+ it('shows a neutral hint for unrecognized input and hides the preview on blur', async () => {
+ vi.useFakeTimers()
+
+ renderControlledDateValueInput()
+
+ const input = screen.getByTestId('date-value-input')
+ fireEvent.focus(input)
+ fireEvent.change(input, { target: { value: 'eventually maybe' } })
+
+ await act(async () => {
+ vi.advanceTimersByTime(250)
+ })
+
+ expect(screen.getByTestId('date-value-preview-unrecognized')).toHaveTextContent('Not recognized')
+
+ fireEvent.blur(input)
+
+ expect(screen.queryByTestId('date-value-preview')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('date-value-preview-unrecognized')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/components/filter-builder/DateValueInput.tsx b/src/components/filter-builder/DateValueInput.tsx
new file mode 100644
index 00000000..e8ea7f5f
--- /dev/null
+++ b/src/components/filter-builder/DateValueInput.tsx
@@ -0,0 +1,78 @@
+import { useEffect, useState } from 'react'
+import { CalendarBlank } from '@phosphor-icons/react'
+import { format } from 'date-fns'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Calendar } from '@/components/ui/calendar'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { parseDateFilterInput } from '@/utils/filterDates'
+
+const DATE_PREVIEW_DEBOUNCE_MS = 250
+
+export function DateValueInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const selected = value ? parseDateFilterInput(value) ?? undefined : undefined
+ const [showPreview, setShowPreview] = useState(false)
+ const [debouncedValue, setDebouncedValue] = useState(value)
+
+ useEffect(() => {
+ const timeoutId = window.setTimeout(() => setDebouncedValue(value), DATE_PREVIEW_DEBOUNCE_MS)
+ return () => window.clearTimeout(timeoutId)
+ }, [value])
+
+ const previewValue = showPreview ? debouncedValue.trim() : ''
+ const resolvedPreview = previewValue ? parseDateFilterInput(previewValue) : null
+ const previewLabel = resolvedPreview
+ ? format(resolvedPreview, 'MMMM d, yyyy')
+ : previewValue
+ ? 'Not recognized'
+ : null
+
+ return (
+
+
+
{
+ setShowPreview(true)
+ onChange(e.target.value)
+ }}
+ onFocus={() => setShowPreview(true)}
+ onBlur={() => setShowPreview(false)}
+ data-testid="date-value-input"
+ />
+
+
+
+
+
+ onChange(day ? format(day, 'yyyy-MM-dd') : '')}
+ />
+
+
+
+ {previewLabel && (
+
+ {resolvedPreview ? `Resolves to ${previewLabel}` : previewLabel}
+
+ )}
+
+ )
+}
diff --git a/src/utils/filterDates.test.ts b/src/utils/filterDates.test.ts
index 000df304..d46732b9 100644
--- a/src/utils/filterDates.test.ts
+++ b/src/utils/filterDates.test.ts
@@ -18,6 +18,12 @@ describe('filterDates', () => {
expect(parsed && format(parsed, 'yyyy-MM-dd')).toBe('2026-03-28')
})
+ it('parses numeric relative year phrases', () => {
+ const reference = new Date('2026-04-08T12:00:00Z')
+ const parsed = parseDateFilterInput('10 years ago', reference)
+ expect(parsed && format(parsed, 'yyyy-MM-dd')).toBe('2016-04-08')
+ })
+
it('parses word-based relative week phrases', () => {
const reference = new Date('2026-04-07T12:00:00Z')
const parsed = parseDateFilterInput('one week ago', reference)