diff --git a/.claude/skills/create-dashboard/SKILL.md b/.claude/skills/create-dashboard/SKILL.md index 78471be..e05bd7f 100644 --- a/.claude/skills/create-dashboard/SKILL.md +++ b/.claude/skills/create-dashboard/SKILL.md @@ -175,6 +175,8 @@ filters: **Filter types:** `select`, `date-range`, `date`, `number`, `text` +**Searchable selects:** both single and multiple `select` filters show a searchable dropdown, so you can type to find an option quickly when the list is long. + **Date range presets:** `today`, `yesterday`, `last_7_days`, `last_30_days`, `last_90_days`, `this_month`, `last_month`, `this_quarter`, `this_year`, `year_to_date`, `all_time`. If `options.presets` is omitted, a default set is shown. Users can always pick "Custom range" for arbitrary dates. --- diff --git a/cmd/skill_templates/create-dashboard/SKILL.md b/cmd/skill_templates/create-dashboard/SKILL.md index 4bcf394..6237036 100644 --- a/cmd/skill_templates/create-dashboard/SKILL.md +++ b/cmd/skill_templates/create-dashboard/SKILL.md @@ -117,6 +117,8 @@ Supported filter types: Date range presets include `today`, `yesterday`, `last_7_days`, `last_30_days`, `last_90_days`, `this_month`, `last_month`, `this_quarter`, `this_year`, `year_to_date`, and `all_time`. +Both single and multiple `select` filters show a searchable dropdown, so you can type to find an option quickly when the list is long. + Select filters support `multiple: true` for multi-select. The value is a list — render with `join` in Jinja and guard the empty case: ```sql diff --git a/frontend/src/themes/bruin/FilterBar.tsx b/frontend/src/themes/bruin/FilterBar.tsx index 00eeb41..ac48abc 100644 --- a/frontend/src/themes/bruin/FilterBar.tsx +++ b/frontend/src/themes/bruin/FilterBar.tsx @@ -197,22 +197,7 @@ function FilterControl({ if (filter.multiple) { return ; } - return ( -
- - -
- ); + return ; case "date-range": return ; @@ -438,6 +423,50 @@ interface Option { value: string; } +function SingleSelectFilter({ + filter, + value, + onChange, + label, +}: { + filter: Filter; + value: unknown; + onChange: (value: unknown) => void; + label: string; +}) { + const options: Option[] = (filter.options?.values ?? []).map((v) => ({ label: v, value: v })); + const current = value ?? filter.default; + const selected = current != null && current !== "" + ? options.find((o) => o.value === String(current)) ?? null + : null; + const inputId = `select-${filter.name}`; + + return ( +
+ + + inputId={inputId} + aria-label={label} + options={options} + value={selected} + onChange={(opt) => onChange(opt ? opt.value : "")} + placeholder="Select..." + isSearchable + menuPortalTarget={popoverPortalTarget() as HTMLElement} + menuPosition="fixed" + unstyled + classNames={selectClassNames} + styles={selectStyles} + /> +
+ ); +} + function MultiSelectFilter({ filter, value, @@ -467,7 +496,7 @@ function MultiSelectFilter({ inputId={inputId} aria-label={label} isMulti - isSearchable={false} + isSearchable options={options} value={selected} onChange={(opts) => onChange(opts.map((o) => o.value))} @@ -488,6 +517,7 @@ function MultiSelectFilter({ function CompactValueContainer({ children, ...props }: ValueContainerProps) { const count = props.getValue().length; + const isTyping = Boolean(props.selectProps.inputValue); if (count === 0) { return {children}; } @@ -495,9 +525,12 @@ function CompactValueContainer({ children, ...props }: ValueContainerProps - - {count === 1 ? props.getValue()[0].label : `${count} selected`} - + {/* Hide the "N selected" summary while the user is typing a search. */} + {!isTyping && ( + + {count === 1 ? props.getValue()[0].label : `${count} selected`} + + )} {input} ); @@ -513,6 +546,7 @@ const selectClassNames = { }`, valueContainer: () => "overflow-hidden", placeholder: () => "text-[var(--dac-text-muted)]", + singleValue: () => "text-[var(--dac-text-primary)] truncate", input: () => "text-[var(--dac-text-primary)]", indicatorsContainer: () => "text-[var(--dac-text-muted)]", indicatorSeparator: () => "hidden", @@ -555,8 +589,6 @@ const selectStyles = { ...base, margin: 0, padding: 0, - height: 0, - width: 0, }), indicatorsContainer: (base: Record) => ({ ...base,