+
+
+ Connectors
+ {connectorFilter.length > 0 && (
+
+ )}
+
+
setConnectorFilter(value === 'all' ? [] : [value])}
+ align='start'
+ fullWidth
+ flush
/>
-
-
Content
-
{contentDisplayLabel}
- }
- showAllOption
- allOptionLabel='All'
- size='sm'
- className='h-[32px] w-full rounded-md'
+
+
+ Content
+ {contentFilter.length > 0 && (
+
+ )}
+
+
setContentFilter(value === 'all' ? [] : [value])}
+ align='start'
+ fullWidth
+ flush
/>
{memberOptions.length > 0 && (
-
-
Owner
-
+
+ Owner
+ {ownerFilter.length > 0 && (
+
+ )}
+
+ {ownerDisplayLabel}
- }
+ values={ownerFilter}
+ onChange={setOwnerFilter}
+ allLabel='All'
searchable
searchPlaceholder='Search members...'
- showAllOption
- allOptionLabel='All'
- size='sm'
- className='h-[32px] w-full rounded-md'
+ align='start'
+ fullWidth
+ flush
/>
)}
- {hasActiveFilters && (
-
- )}
),
- [
- connectorFilter,
- contentFilter,
- ownerFilter,
- memberOptions,
- connectorDisplayLabel,
- contentDisplayLabel,
- ownerDisplayLabel,
- hasActiveFilters,
- ]
+ [connectorFilter, contentFilter, ownerFilter, memberOptions]
)
const filterTags: FilterTag[] = useMemo(() => {
diff --git a/apps/sim/components/emcn/components/calendar/calendar.tsx b/apps/sim/components/emcn/components/calendar/calendar.tsx
new file mode 100644
index 0000000000..9153d35682
--- /dev/null
+++ b/apps/sim/components/emcn/components/calendar/calendar.tsx
@@ -0,0 +1,207 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+import { ChevronLeft, ChevronRight } from 'lucide-react'
+import { cn } from '@/lib/core/utils/cn'
+
+const MONTHS = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+] as const
+
+const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const
+
+function getDaysInMonth(year: number, month: number): number {
+ return new Date(year, month + 1, 0).getDate()
+}
+
+function getFirstDayOfMonth(year: number, month: number): number {
+ return new Date(year, month, 1).getDay()
+}
+
+function pad2(value: number): string {
+ return value.toString().padStart(2, '0')
+}
+
+/**
+ * Serializes a calendar cell to the `YYYY-MM-DD` wire format used across the
+ * date filters. Built from local Y/M/D parts so there is no UTC offset shift.
+ */
+function toDateString(year: number, month: number, day: number): string {
+ return `${year}-${pad2(month + 1)}-${pad2(day)}`
+}
+
+/**
+ * Parses a `YYYY-MM-DD` string (or `Date`) into a local `Date`. `YYYY-MM-DD`
+ * is parsed as local time to avoid the off-by-one day that `new Date('2026-05-08')`
+ * (UTC midnight) produces in negative-offset timezones.
+ */
+export function parseDateValue(value: string | Date | undefined): Date | null {
+ if (!value) return null
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
+ const [year, month, day] = value.split('-').map(Number)
+ return new Date(year, month - 1, day)
+ }
+ const parsed = new Date(value)
+ return Number.isNaN(parsed.getTime()) ? null : parsed
+}
+
+/**
+ * Human-readable label for a date value (e.g. `May 8, 2026`). Returns an empty
+ * string when there is no parseable date — callers fall back to a placeholder.
+ */
+export function formatDateLabel(value: string | Date | undefined): string {
+ const date = parseDateValue(value)
+ if (!date) return ''
+ return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
+}
+
+function isSameDay(a: Date, b: Date): boolean {
+ return (
+ a.getFullYear() === b.getFullYear() &&
+ a.getMonth() === b.getMonth() &&
+ a.getDate() === b.getDate()
+ )
+}
+
+export interface CalendarProps {
+ /** Selected date as a `YYYY-MM-DD` string or `Date`. */
+ value?: string | Date
+ /** Called with the picked date in `YYYY-MM-DD` format. */
+ onChange?: (value: string) => void
+ /** Forwarded to the root grid container. */
+ className?: string
+}
+
+/**
+ * Single-month date grid aligned with the chip family — `rounded-lg` day cells,
+ * `--surface-active` hover, and a `primary`-chip fill on the selected day. Pair
+ * it with {@link ChipDatePicker} for a chip trigger, or embed it directly.
+ *
+ * @example
+ *
+ */
+export function Calendar({ value, onChange, className }: CalendarProps) {
+ const selected = useMemo(() => parseDateValue(value), [value])
+ const today = useMemo(() => new Date(), [])
+
+ const [view, setView] = useState(() => {
+ const base = selected ?? today
+ return { month: base.getMonth(), year: base.getFullYear() }
+ })
+
+ const cells = useMemo<(number | null)[]>(() => {
+ const leading = getFirstDayOfMonth(view.year, view.month)
+ const total = getDaysInMonth(view.year, view.month)
+ const result: (number | null)[] = []
+ for (let i = 0; i < leading; i++) result.push(null)
+ for (let day = 1; day <= total; day++) result.push(day)
+ return result
+ }, [view])
+
+ const goToPrevMonth = () =>
+ setView((prev) =>
+ prev.month === 0
+ ? { month: 11, year: prev.year - 1 }
+ : { month: prev.month - 1, year: prev.year }
+ )
+
+ const goToNextMonth = () =>
+ setView((prev) =>
+ prev.month === 11
+ ? { month: 0, year: prev.year + 1 }
+ : { month: prev.month + 1, year: prev.year }
+ )
+
+ const selectDay = (day: number) => onChange?.(toDateString(view.year, view.month, day))
+
+ const goToToday = () => {
+ setView({ month: today.getMonth(), year: today.getFullYear() })
+ onChange?.(toDateString(today.getFullYear(), today.getMonth(), today.getDate()))
+ }
+
+ return (
+
+
+
+
+ {MONTHS[view.month]} {view.year}
+
+
+
+
+
+ {WEEKDAYS.map((weekday) => (
+
+ {weekday}
+
+ ))}
+
+
+
+ {cells.map((day, index) => {
+ if (day === null) return
+
+ const cellDate = new Date(view.year, view.month, day)
+ const isSelected = selected ? isSameDay(cellDate, selected) : false
+ const isToday = isSameDay(cellDate, today)
+
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/apps/sim/components/emcn/components/chip-date-picker/chip-date-picker.tsx b/apps/sim/components/emcn/components/chip-date-picker/chip-date-picker.tsx
new file mode 100644
index 0000000000..cc2f2e7883
--- /dev/null
+++ b/apps/sim/components/emcn/components/chip-date-picker/chip-date-picker.tsx
@@ -0,0 +1,116 @@
+'use client'
+
+import { forwardRef, useState } from 'react'
+import * as PopoverPrimitive from '@radix-ui/react-popover'
+import { Calendar, formatDateLabel } from '@/components/emcn/components/calendar/calendar'
+import { chipVariants } from '@/components/emcn/components/chip/chip'
+import { ChevronDown } from '@/components/emcn/icons'
+import { cn } from '@/lib/core/utils/cn'
+
+/**
+ * Mirrors {@link ChipDropdown}'s flat-surface treatment: a 1px border makes the
+ * `filled` chip read as an interactive form control rather than a static pill.
+ */
+const TRIGGER_BORDER_CLASS = 'border border-[var(--border-1)]'
+
+const POPOVER_ANIMATION_CLASSES =
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'
+
+export interface ChipDatePickerProps {
+ /** Selected date as a `YYYY-MM-DD` string. */
+ value?: string
+ /** Called with the picked date in `YYYY-MM-DD` format. */
+ onChange?: (value: string) => void
+ /** Shown in the trigger when no date is selected. */
+ placeholder?: string
+ /** Aligns the calendar popover relative to the trigger. */
+ align?: 'start' | 'center' | 'end'
+ /** Disables the trigger. */
+ disabled?: boolean
+ /** Stretch the trigger to fill its container (mirrors `Chip`'s `fullWidth`). */
+ fullWidth?: boolean
+ /** Removes the default `mx-0.5` cluster margin (mirrors `Chip`'s `flush`). */
+ flush?: boolean
+ /** Forwarded class for the trigger button. */
+ className?: string
+}
+
+/**
+ * Date counterpart to {@link ChipDropdown} — a chip-styled trigger that opens a
+ * {@link Calendar} in a popover. The trigger reuses `chipVariants` (filled +
+ * border) and the owned chevron for visual parity with the other chip controls.
+ *
+ * @example
+ *
+ */
+const ChipDatePicker = forwardRef
(function ChipDatePicker(
+ {
+ value,
+ onChange,
+ placeholder = 'Select date',
+ align = 'start',
+ disabled,
+ fullWidth,
+ flush,
+ className,
+ },
+ ref
+) {
+ const [open, setOpen] = useState(false)
+ const label = formatDateLabel(value)
+
+ return (
+
+
+
+
+
+
+ {
+ onChange?.(next)
+ setOpen(false)
+ }}
+ />
+
+
+
+ )
+})
+
+ChipDatePicker.displayName = 'ChipDatePicker'
+
+export { ChipDatePicker }
diff --git a/apps/sim/components/emcn/components/chip-dropdown/chip-dropdown.tsx b/apps/sim/components/emcn/components/chip-dropdown/chip-dropdown.tsx
index 5c1d2cb94e..e3e02519af 100644
--- a/apps/sim/components/emcn/components/chip-dropdown/chip-dropdown.tsx
+++ b/apps/sim/components/emcn/components/chip-dropdown/chip-dropdown.tsx
@@ -112,6 +112,7 @@ const ChipDropdown = forwardRef(function C
variant = 'filled',
active,
fullWidth,
+ flush,
},
ref
) {
@@ -149,7 +150,7 @@ const ChipDropdown = forwardRef(function C
type='button'
disabled={disabled}
className={cn(
- chipVariants({ variant, active, fullWidth }),
+ chipVariants({ variant, active, fullWidth, flush }),
hasTriggerBorder && TRIGGER_BORDER_CLASS,
className
)}
diff --git a/apps/sim/components/emcn/components/chip-multi-select/chip-multi-select.tsx b/apps/sim/components/emcn/components/chip-multi-select/chip-multi-select.tsx
new file mode 100644
index 0000000000..abcc13b9e5
--- /dev/null
+++ b/apps/sim/components/emcn/components/chip-multi-select/chip-multi-select.tsx
@@ -0,0 +1,207 @@
+'use client'
+
+import { type ComponentType, forwardRef, type ReactNode, useMemo, useState } from 'react'
+import type { VariantProps } from 'class-variance-authority'
+import { chipVariants } from '@/components/emcn/components/chip/chip'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSearchInput,
+ DropdownMenuTrigger,
+} from '@/components/emcn/components/dropdown-menu/dropdown-menu'
+import { Check, ChevronDown } from '@/components/emcn/icons'
+import { cn } from '@/lib/core/utils/cn'
+
+type ChipIcon = ComponentType<{ className?: string }>
+
+/**
+ * Single option rendered inside a {@link ChipMultiSelect}.
+ */
+interface ChipMultiSelectOption {
+ /** Stable identifier toggled in/out of the selected `values`. */
+ value: string
+ /** Visual label rendered in the menu and (when only one is picked) the trigger. */
+ label: string
+ /** Optional leading icon component rendered inside the menu item. */
+ icon?: ChipIcon
+ /** Pre-rendered leading element (e.g. an avatar) — takes precedence over `icon`. */
+ iconElement?: ReactNode
+}
+
+interface ChipMultiSelectProps
+ extends Pick, 'fullWidth' | 'flush'> {
+ /** Currently selected values. Empty array reads as "all" / no filter. */
+ values: string[]
+ /** Called with the next selected values when an option is toggled. */
+ onChange: (values: string[]) => void
+ /** Options to render in the menu. */
+ options: ReadonlyArray
+ /** Label shown in the trigger and as the reset row when nothing is selected. */
+ allLabel?: string
+ /** Renders a search field at the top of the menu that filters options by label. */
+ searchable?: boolean
+ /** Placeholder for the search field. */
+ searchPlaceholder?: string
+ /** Aligns the menu relative to the trigger. */
+ align?: 'start' | 'center' | 'end'
+ /**
+ * Whether the menu width matches the trigger's (default `true`). Set `false`
+ * to let the menu size to its widest item instead.
+ */
+ matchTriggerWidth?: boolean
+ /** Forwarded to the inner `DropdownMenuContent`. */
+ contentClassName?: string
+ /** Disables the trigger. */
+ disabled?: boolean
+ /** Forwarded class for the trigger button. */
+ className?: string
+}
+
+/**
+ * Mirrors {@link ChipDropdown}'s flat-surface treatment — a 1px border makes
+ * the `filled` chip read as an interactive control rather than a static pill.
+ */
+const TRIGGER_BORDER_CLASS = 'border border-[var(--border-1)]'
+
+/**
+ * Multi-select counterpart to {@link ChipDropdown} — a chip trigger that opens a
+ * menu of toggleable options with a leading "all" reset row and an optional
+ * search field. The menu stays open across selections; a trailing check marks
+ * each active option, matching `ChipDropdown`'s single-select affordance.
+ *
+ * @example
+ *
+ */
+const ChipMultiSelect = forwardRef(
+ function ChipMultiSelect(
+ {
+ values,
+ onChange,
+ options,
+ allLabel = 'All',
+ searchable = false,
+ searchPlaceholder = 'Search...',
+ align = 'start',
+ matchTriggerWidth = true,
+ contentClassName,
+ disabled,
+ fullWidth,
+ flush,
+ className,
+ },
+ ref
+ ) {
+ const [open, setOpen] = useState(false)
+ const [search, setSearch] = useState('')
+
+ const displayLabel = useMemo(() => {
+ if (values.length === 0) return allLabel
+ if (values.length === 1) return options.find((o) => o.value === values[0])?.label ?? allLabel
+ return `${values.length} selected`
+ }, [values, options, allLabel])
+
+ const filteredOptions = useMemo(() => {
+ const query = search.trim().toLowerCase()
+ if (!searchable || !query) return options
+ return options.filter((option) => option.label.toLowerCase().includes(query))
+ }, [options, searchable, search])
+
+ const toggle = (value: string) => {
+ onChange(values.includes(value) ? values.filter((v) => v !== value) : [...values, value])
+ }
+
+ return (
+ {
+ setOpen(next)
+ if (!next) setSearch('')
+ }}
+ >
+
+
+
+ event.preventDefault() : undefined}
+ className={cn(
+ matchTriggerWidth && 'w-[var(--radix-dropdown-menu-trigger-width)] max-w-none',
+ contentClassName
+ )}
+ >
+ {searchable && (
+ setSearch(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Escape') setOpen(false)
+ }}
+ />
+ )}
+ {
+ event.preventDefault()
+ onChange([])
+ }}
+ >
+ {allLabel}
+ {values.length === 0 ? : null}
+
+ {filteredOptions.map((option) => {
+ const isSelected = values.includes(option.value)
+ const OptionIcon = option.icon
+ return (
+ {
+ event.preventDefault()
+ toggle(option.value)
+ }}
+ >
+ {option.iconElement ?? (OptionIcon ? : null)}
+ {option.label}
+ {isSelected ? : null}
+
+ )
+ })}
+
+
+ )
+ }
+)
+
+ChipMultiSelect.displayName = 'ChipMultiSelect'
+
+export { ChipMultiSelect }
+export type { ChipMultiSelectOption, ChipMultiSelectProps }
diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts
index cd0a68541e..2851c2580e 100644
--- a/apps/sim/components/emcn/components/index.ts
+++ b/apps/sim/components/emcn/components/index.ts
@@ -15,6 +15,7 @@ export {
buttonGroupItemVariants,
buttonGroupVariants,
} from './button-group/button-group'
+export { Calendar, type CalendarProps, formatDateLabel, parseDateValue } from './calendar/calendar'
export { Callout, calloutVariants } from './callout/callout'
export {
Checkbox,
@@ -22,6 +23,7 @@ export {
checkboxVariants,
} from './checkbox/checkbox'
export { Chip, ChipLink, type ChipLinkProps, type ChipProps, chipVariants } from './chip/chip'
+export { ChipDatePicker, type ChipDatePickerProps } from './chip-date-picker/chip-date-picker'
export {
ChipDropdown,
type ChipDropdownOption,
@@ -40,6 +42,11 @@ export {
type ChipModalHeaderProps,
type ChipModalProps,
} from './chip-modal/chip-modal'
+export {
+ ChipMultiSelect,
+ type ChipMultiSelectOption,
+ type ChipMultiSelectProps,
+} from './chip-multi-select/chip-multi-select'
export {
CODE_LINE_HEIGHT_PX,
Code,
diff --git a/apps/sim/components/emcn/components/input/input.tsx b/apps/sim/components/emcn/components/input/input.tsx
index b216d67bb5..bdf162f3d6 100644
--- a/apps/sim/components/emcn/components/input/input.tsx
+++ b/apps/sim/components/emcn/components/input/input.tsx
@@ -31,6 +31,12 @@ const inputVariants = cva(
variants: {
variant: {
default: '',
+ /**
+ * Aligns the input with the chip family — a 30px `rounded-lg` filled
+ * surface that sits flush next to {@link ChipDropdown} / {@link ChipDatePicker}
+ * in form rows (e.g. tag-filter values).
+ */
+ chip: 'h-[30px] rounded-lg text-[var(--text-body)] focus-visible:border-[var(--border-focus)] dark:bg-[var(--surface-4)]',
},
},
defaultVariants: {