generated from coulomb/repo-seed
Dashboard: reusable MultiSelect dropdown component for workstreams filters
Adds src/components/multiselect.js — a compact dropdown multi-select that is Observable-compatible (exposes .value, dispatches bubbling input events) so it works with view(), Inputs.form, and Generators.input without modification. Component behaviour: - Closed state: pill button showing "Label: All" (muted) or active selection (1-2 items shown by name, 3+ shown as "N of M"); blue border when active - Open state: dropdown with per-item checkboxes + "Clear selection" link (only visible when something is selected); closes on outside click / Escape - Styles injected once into document.head (STYLE_ID guard prevents duplicates) - Uses CSS custom properties for light/dark mode compatibility Workstreams page update: - Domain and Status filters now use MultiSelect instead of Inputs.checkbox - Filter bar layout reduced to a tight inline row (0.5rem gap) - Owner text filter restyled to match trigger button height - No changes to filter logic or downstream cells (filters.domain / .status are still string[] with empty = show all) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,21 +56,22 @@ display(html`<div class="live-bar">
|
||||
```
|
||||
|
||||
```js
|
||||
import {MultiSelect} from "./components/multiselect.js";
|
||||
|
||||
// Static options — no dependency on `data`, so selections survive polls
|
||||
const DOMAINS = ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"];
|
||||
const STATUSES = ["active", "blocked", "completed", "archived"];
|
||||
|
||||
const filters = view(Inputs.form(
|
||||
{
|
||||
domain: Inputs.checkbox(DOMAINS, {label: "Domain"}),
|
||||
status: Inputs.checkbox(STATUSES, {label: "Status"}),
|
||||
owner: Inputs.text({label: "Owner contains", placeholder: "filter by owner…"}),
|
||||
domain: MultiSelect(DOMAINS, {label: "Domain", placeholder: "All domains"}),
|
||||
status: MultiSelect(STATUSES, {label: "Status", placeholder: "All statuses"}),
|
||||
owner: Inputs.text({placeholder: "Owner…", style: "width:120px"}),
|
||||
},
|
||||
{
|
||||
template: ({domain, status, owner}) => html`<div class="filter-bar">
|
||||
<div class="filter-group">${domain}</div>
|
||||
<div class="filter-group">${status}</div>
|
||||
<div class="filter-group filter-group-text">${owner}</div>
|
||||
${domain}${status}
|
||||
<div class="filter-owner">${owner}</div>
|
||||
</div>`,
|
||||
}
|
||||
));
|
||||
@@ -146,9 +147,9 @@ if (wsWithDeps.length === 0) {
|
||||
<style>
|
||||
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
|
||||
.dim { color: gray; font-style: italic; }
|
||||
.filter-bar { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-start; padding: 0.75rem 1rem; background: var(--theme-background-alt); border-radius: 8px; margin-bottom: 1rem; }
|
||||
.filter-group { min-width: 140px; }
|
||||
.filter-group-text { align-self: flex-end; }
|
||||
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
|
||||
.filter-owner { display: flex; align-items: center; }
|
||||
.filter-owner input { height: 30px; font-size: 0.85rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--theme-foreground-faint, #ccc); background: var(--theme-background, #fff); font-family: inherit; color: inherit; }
|
||||
.dep-grid { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.dep-card { border: 1px solid #e0e0e0; border-radius: 6px; padding: 0.75rem 1rem; background: var(--theme-background-alt, #fafafa); }
|
||||
.dep-title { font-weight: 600; margin-bottom: 0.25rem; }
|
||||
|
||||
Reference in New Issue
Block a user