lucent-ui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { RECIPE as ActionBar } from '../src/manifest/recipes/action-bar.recipe.j
5
5
  import { RECIPE as FormLayout } from '../src/manifest/recipes/form-layout.recipe.js';
6
6
  import { RECIPE as EmptyStateCard } from '../src/manifest/recipes/empty-state-card.recipe.js';
7
7
  import { RECIPE as CollapsibleCard } from '../src/manifest/recipes/collapsible-card.recipe.js';
8
+ import { RECIPE as SearchFilterBar } from '../src/manifest/recipes/search-filter-bar.recipe.js';
8
9
  export const ALL_RECIPES = [
9
10
  ProfileCard,
10
11
  SettingsPanel,
@@ -13,4 +14,5 @@ export const ALL_RECIPES = [
13
14
  FormLayout,
14
15
  EmptyStateCard,
15
16
  CollapsibleCard,
17
+ SearchFilterBar,
16
18
  ];
@@ -5,3 +5,4 @@ export { RECIPE as ActionBar } from './action-bar.recipe.js';
5
5
  export { RECIPE as FormLayout } from './form-layout.recipe.js';
6
6
  export { RECIPE as EmptyStateCard } from './empty-state-card.recipe.js';
7
7
  export { RECIPE as CollapsibleCard } from './collapsible-card.recipe.js';
8
+ export { RECIPE as SearchFilterBar } from './search-filter-bar.recipe.js';
@@ -0,0 +1,197 @@
1
+ export const RECIPE = {
2
+ id: 'search-filter-bar',
3
+ name: 'Search / Filter Bar',
4
+ description: 'Compact toolbar of secondary filter buttons with Menu popovers, collapsible search, multi-select tags, date range picker, sort control, and view toggle. Designed to sit above a DataTable or list.',
5
+ category: 'dashboard',
6
+ components: ['button', 'menu', 'chip', 'checkbox', 'date-range-picker', 'segmented-control', 'input', 'row'],
7
+ structure: `
8
+ Row gap="2" align="center" wrap
9
+ ├── Button (secondary, icon-only, search) ← expands to Input on click
10
+ ├── Menu ← single-select filter
11
+ │ ├── trigger: Button (secondary, chevron, "Availability")
12
+ │ └── MenuItem[] (selected marks active)
13
+ ├── Menu ← multi-select status filter
14
+ │ ├── trigger: Button (secondary, chevron, "Status" + Chip count)
15
+ │ └── MenuItem[] (controlled open, stays open on toggle)
16
+ ├── Menu ← multi-select tags with chips
17
+ │ ├── trigger: Button (secondary, chevron, "Tags" + Chip count)
18
+ │ └── MenuItem[] > Row > Checkbox + Chip (swatch)
19
+ ├── DateRangePicker ← trigger prop renders Button
20
+ │ └── trigger: Button (secondary, chevron, "Date range")
21
+ ├── Button (ghost, "Clear all") ← conditional, shown when filters active
22
+ ├── flex spacer ← pushes sort/view to right
23
+ └── Row gap="2" align="center" ← sort + view toggle
24
+ ├── Menu
25
+ │ ├── trigger: Button (secondary, chevron, leftIcon=sort, "Newest first")
26
+ │ └── MenuItem[] (selected marks active sort)
27
+ └── SegmentedControl (sm, grid/list icons)
28
+ `.trim(),
29
+ code: `const [searchOpen, setSearchOpen] = useState(false);
30
+ const [statuses, setStatuses] = useState<Set<string>>(new Set());
31
+ const [statusOpen, setStatusOpen] = useState(false);
32
+ const [tags, setTags] = useState<Set<string>>(new Set());
33
+ const [tagsOpen, setTagsOpen] = useState(false);
34
+ const [dateRange, setDateRange] = useState<DateRange | undefined>();
35
+
36
+ const toggleStatus = (s: string) => setStatuses(prev => {
37
+ if (s === 'all') return new Set();
38
+ const next = new Set(prev);
39
+ if (next.has(s)) next.delete(s); else next.add(s);
40
+ return next;
41
+ });
42
+ const toggleTag = (t: string) => setTags(prev => {
43
+ const next = new Set(prev);
44
+ if (next.has(t)) next.delete(t); else next.add(t);
45
+ return next;
46
+ });
47
+ const hasFilters = statuses.size > 0 || tags.size > 0 || dateRange !== undefined;
48
+ const clearAll = () => { setStatuses(new Set()); setTags(new Set()); setDateRange(undefined); };
49
+
50
+ <Row gap="2" align="center" wrap>
51
+ {/* Collapsible search */}
52
+ {searchOpen ? (
53
+ <Input placeholder="Search…" size="sm" style={{ width: 260 }} autoFocus
54
+ onBlur={(e) => { if (!e.target.value) setSearchOpen(false); }} />
55
+ ) : (
56
+ <Button variant="secondary" size="sm" aria-label="Search"
57
+ leftIcon={<SearchIcon />} />
58
+ )}
59
+
60
+ {/* Single-select filter */}
61
+ <Menu trigger={<Button variant="secondary" size="sm" chevron>Availability</Button>} size="sm">
62
+ <MenuItem selected onSelect={() => {}}>All</MenuItem>
63
+ <MenuItem onSelect={() => {}}>Available</MenuItem>
64
+ <MenuItem onSelect={() => {}}>On notice</MenuItem>
65
+ <MenuItem onSelect={() => {}}>Unavailable</MenuItem>
66
+ </Menu>
67
+
68
+ {/* Multi-select status */}
69
+ <Menu trigger={
70
+ <Button variant="secondary" size="sm" chevron>
71
+ Status{statuses.size > 0 && <Chip variant="accent" size="sm">{statuses.size}</Chip>}
72
+ </Button>
73
+ } size="sm" open={statusOpen} onOpenChange={setStatusOpen}>
74
+ <MenuItem selected={statuses.size === 0}
75
+ onSelect={() => { toggleStatus('all'); setStatusOpen(true); }}>All</MenuItem>
76
+ <MenuSeparator />
77
+ <MenuItem selected={statuses.has('active')}
78
+ onSelect={() => { toggleStatus('active'); setStatusOpen(true); }}>Active</MenuItem>
79
+ <MenuItem selected={statuses.has('archived')}
80
+ onSelect={() => { toggleStatus('archived'); setStatusOpen(true); }}>Archived</MenuItem>
81
+ <MenuItem selected={statuses.has('on-hold')}
82
+ onSelect={() => { toggleStatus('on-hold'); setStatusOpen(true); }}>On hold</MenuItem>
83
+ </Menu>
84
+
85
+ {/* Multi-select tags with checkbox + chip */}
86
+ <Menu trigger={
87
+ <Button variant="secondary" size="sm" chevron>
88
+ Tags{tags.size > 0 && <Chip variant="accent" size="sm">{tags.size}</Chip>}
89
+ </Button>
90
+ } size="sm" open={tagsOpen} onOpenChange={setTagsOpen}>
91
+ <MenuItem onSelect={() => { toggleTag('data-science'); setTagsOpen(true); }}>
92
+ <Row gap="2" align="center">
93
+ <Checkbox checked={tags.has('data-science')} onChange={() => {}} />
94
+ <Chip size="sm" swatch="#6366f1">Data Science</Chip>
95
+ </Row>
96
+ </MenuItem>
97
+ <MenuItem onSelect={() => { toggleTag('devops'); setTagsOpen(true); }}>
98
+ <Row gap="2" align="center">
99
+ <Checkbox checked={tags.has('devops')} onChange={() => {}} />
100
+ <Chip size="sm" swatch="#10b981">DevOps</Chip>
101
+ </Row>
102
+ </MenuItem>
103
+ </Menu>
104
+
105
+ {/* Date range with button trigger */}
106
+ <DateRangePicker placeholder="Date range" size="sm"
107
+ value={dateRange} onChange={setDateRange}
108
+ trigger={<Button variant="secondary" size="sm" chevron>
109
+ {dateRange ? formatRange(dateRange) : 'Date range'}
110
+ </Button>} />
111
+
112
+ {/* Clear all (conditional) */}
113
+ {hasFilters && <Button variant="ghost" size="sm" onClick={clearAll}>Clear all</Button>}
114
+
115
+ {/* Spacer */}
116
+ <div style={{ flex: 1 }} />
117
+
118
+ {/* Sort + view toggle */}
119
+ <Row gap="2" align="center">
120
+ <Menu trigger={
121
+ <Button variant="secondary" size="sm" chevron leftIcon={<SortIcon />}>
122
+ Newest first
123
+ </Button>
124
+ } size="sm">
125
+ <MenuItem selected onSelect={() => {}}>Newest first</MenuItem>
126
+ <MenuItem onSelect={() => {}}>Oldest first</MenuItem>
127
+ <MenuItem onSelect={() => {}}>Name A–Z</MenuItem>
128
+ </Menu>
129
+ <SegmentedControl size="sm" defaultValue="grid" options={[
130
+ { value: 'grid', label: <GridIcon /> },
131
+ { value: 'list', label: <ListIcon /> },
132
+ ]} />
133
+ </Row>
134
+ </Row>`,
135
+ variants: [
136
+ {
137
+ title: 'Minimal — search + sort only',
138
+ code: `<Row gap="2" align="center">
139
+ <Button variant="secondary" size="sm" aria-label="Search" leftIcon={<SearchIcon />} />
140
+ <div style={{ flex: 1 }} />
141
+ <Menu trigger={
142
+ <Button variant="secondary" size="sm" chevron leftIcon={<SortIcon />}>
143
+ Newest first
144
+ </Button>
145
+ } size="sm">
146
+ <MenuItem selected onSelect={() => {}}>Newest first</MenuItem>
147
+ <MenuItem onSelect={() => {}}>Oldest first</MenuItem>
148
+ <MenuItem onSelect={() => {}}>Name A–Z</MenuItem>
149
+ </Menu>
150
+ </Row>`,
151
+ },
152
+ {
153
+ title: 'Pipeline — multi-select filters only',
154
+ code: `<Row gap="2" align="center" wrap>
155
+ <Menu trigger={
156
+ <Button variant="secondary" size="sm" chevron>
157
+ Opportunity{selectedOpp.size > 0 && <Chip variant="accent" size="sm">{selectedOpp.size}</Chip>}
158
+ </Button>
159
+ } size="sm" open={oppOpen} onOpenChange={setOppOpen}>
160
+ <MenuItem selected={selectedOpp.size === 0}
161
+ onSelect={() => { clearOpp(); setOppOpen(true); }}>All</MenuItem>
162
+ <MenuSeparator />
163
+ {opportunities.map(o => (
164
+ <MenuItem key={o} selected={selectedOpp.has(o)}
165
+ onSelect={() => { toggleOpp(o); setOppOpen(true); }}>{o}</MenuItem>
166
+ ))}
167
+ </Menu>
168
+ <Menu trigger={
169
+ <Button variant="secondary" size="sm" chevron>
170
+ Stage{selectedStage.size > 0 && <Chip variant="accent" size="sm">{selectedStage.size}</Chip>}
171
+ </Button>
172
+ } size="sm" open={stageOpen} onOpenChange={setStageOpen}>
173
+ <MenuItem selected={selectedStage.size === 0}
174
+ onSelect={() => { clearStage(); setStageOpen(true); }}>All</MenuItem>
175
+ <MenuSeparator />
176
+ {stages.map(s => (
177
+ <MenuItem key={s} selected={selectedStage.has(s)}
178
+ onSelect={() => { toggleStage(s); setStageOpen(true); }}>{s}</MenuItem>
179
+ ))}
180
+ </Menu>
181
+ {hasFilters && <Button variant="ghost" size="sm" onClick={clearAll}>Clear all</Button>}
182
+ </Row>`,
183
+ },
184
+ ],
185
+ designNotes: 'All filters use secondary buttons with chevron to create a uniform, compact toolbar. ' +
186
+ 'No visible input fields or select dropdowns — everything opens as a popover from the button trigger. ' +
187
+ 'Search starts as a square icon-only button (leftIcon with no children) and expands to an Input on click, ' +
188
+ 'collapsing back when blurred empty. Multi-select menus use controlled open state (open + onOpenChange) ' +
189
+ 'and re-set open=true in onSelect handlers so the menu stays open while toggling items. ' +
190
+ 'Active filter count is shown as an accent Chip inside the button label, not parenthesized text. ' +
191
+ 'Tags menu items combine Checkbox + Chip (with swatch colors) in a Row for rich multi-select visuals. ' +
192
+ 'DateRangePicker uses the trigger prop to render a Button instead of its default input-style trigger. ' +
193
+ 'A ghost "Clear all" button appears conditionally when any filter is active. ' +
194
+ 'A flex spacer pushes sort and view controls to the right edge. ' +
195
+ 'Sort button uses leftIcon for the sort icon. ' +
196
+ 'SegmentedControl with icon-only options (grid/list SVGs via label: ReactNode) provides view toggling.',
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",