lucent-ui 0.29.0 → 0.30.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.
- package/dist/index.cjs +64 -43
- package/dist/index.d.ts +109 -0
- package/dist/index.js +2235 -1848
- package/dist-server/server/registry.js +8 -0
- package/dist-server/src/components/molecules/FilterDateRange/FilterDateRange.manifest.js +46 -0
- package/dist-server/src/components/molecules/FilterMultiSelect/FilterMultiSelect.manifest.js +66 -0
- package/dist-server/src/components/molecules/FilterSearch/FilterSearch.manifest.js +37 -0
- package/dist-server/src/components/molecules/FilterSelect/FilterSelect.manifest.js +58 -0
- package/dist-server/src/components/molecules/Timeline/Timeline.manifest.js +23 -9
- package/dist-server/src/manifest/recipes/search-filter-bar.recipe.js +86 -161
- package/package.json +1 -1
|
@@ -22,6 +22,10 @@ import { COMPONENT_MANIFEST as EmptyState } from '../src/components/molecules/Em
|
|
|
22
22
|
import { COMPONENT_MANIFEST as FormField } from '../src/components/molecules/FormField/FormField.manifest.js';
|
|
23
23
|
import { COMPONENT_MANIFEST as SearchInput } from '../src/components/molecules/SearchInput/SearchInput.manifest.js';
|
|
24
24
|
import { COMPONENT_MANIFEST as Skeleton } from '../src/components/molecules/Skeleton/Skeleton.manifest.js';
|
|
25
|
+
import { COMPONENT_MANIFEST as FilterSearch } from '../src/components/molecules/FilterSearch/FilterSearch.manifest.js';
|
|
26
|
+
import { COMPONENT_MANIFEST as FilterSelect } from '../src/components/molecules/FilterSelect/FilterSelect.manifest.js';
|
|
27
|
+
import { COMPONENT_MANIFEST as FilterMultiSelect } from '../src/components/molecules/FilterMultiSelect/FilterMultiSelect.manifest.js';
|
|
28
|
+
import { COMPONENT_MANIFEST as FilterDateRange } from '../src/components/molecules/FilterDateRange/FilterDateRange.manifest.js';
|
|
25
29
|
export const ALL_MANIFESTS = [
|
|
26
30
|
// Atoms
|
|
27
31
|
Avatar,
|
|
@@ -47,4 +51,8 @@ export const ALL_MANIFESTS = [
|
|
|
47
51
|
FormField,
|
|
48
52
|
SearchInput,
|
|
49
53
|
Skeleton,
|
|
54
|
+
FilterSearch,
|
|
55
|
+
FilterSelect,
|
|
56
|
+
FilterMultiSelect,
|
|
57
|
+
FilterDateRange,
|
|
50
58
|
];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'filter-date-range',
|
|
3
|
+
name: 'FilterDateRange',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A compact date range filter button that opens a dual-calendar DateRangePicker popover. Shows the formatted range in the trigger when active.',
|
|
8
|
+
designIntent: 'FilterDateRange wraps DateRangePicker with a Button trigger instead of the default input-style trigger. ' +
|
|
9
|
+
'When no range is selected, the button shows the label prop. When a range is set, it shows "start → end" dates. ' +
|
|
10
|
+
'The outline variant starts as an outlined button and upgrades to secondary when a range is selected. ' +
|
|
11
|
+
'Use in filter bars alongside FilterSearch, FilterSelect, and FilterMultiSelect for date-based filtering.',
|
|
12
|
+
props: [
|
|
13
|
+
{ name: 'label', type: 'string', required: false, default: '"Date range"', description: 'Text shown on the trigger button when no range is selected.' },
|
|
14
|
+
{ name: 'value', type: 'object', required: false, description: 'Controlled DateRange value ({ start: Date, end: Date }).' },
|
|
15
|
+
{ name: 'defaultValue', type: 'object', required: false, description: 'Initial range for uncontrolled usage.' },
|
|
16
|
+
{ name: 'onChange', type: 'function', required: false, description: 'Called with the selected DateRange.' },
|
|
17
|
+
{ name: 'variant', type: 'enum', required: false, default: 'secondary', description: 'Button style. "outline" switches to "secondary" when a range is selected.', enumValues: ['secondary', 'outline'] },
|
|
18
|
+
{ name: 'size', type: 'enum', required: false, default: 'sm', description: 'Controls button height and calendar scale.', enumValues: ['sm', 'md', 'lg'] },
|
|
19
|
+
{ name: 'min', type: 'object', required: false, description: 'Earliest selectable date.' },
|
|
20
|
+
{ name: 'max', type: 'object', required: false, description: 'Latest selectable date.' },
|
|
21
|
+
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Disables the trigger button.' },
|
|
22
|
+
{ name: 'style', type: 'object', required: false, description: 'Inline style overrides.' },
|
|
23
|
+
],
|
|
24
|
+
usageExamples: [
|
|
25
|
+
{ title: 'Basic', code: `<FilterDateRange label="Date added" />` },
|
|
26
|
+
{
|
|
27
|
+
title: 'Controlled',
|
|
28
|
+
code: `const [range, setRange] = useState<DateRange | undefined>();
|
|
29
|
+
<FilterDateRange label="Date range" value={range} onChange={setRange} />`,
|
|
30
|
+
},
|
|
31
|
+
{ title: 'Outline variant', code: `<FilterDateRange label="Date range" variant="outline" />` },
|
|
32
|
+
{
|
|
33
|
+
title: 'With min/max',
|
|
34
|
+
code: `<FilterDateRange label="Period" min={new Date(2026, 0, 1)} max={new Date(2026, 11, 31)} />`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
compositionGraph: [
|
|
38
|
+
{ componentId: 'button', componentName: 'Button', role: 'Trigger button with chevron', required: true },
|
|
39
|
+
{ componentId: 'date-range-picker', componentName: 'DateRangePicker', role: 'Dual-calendar popover', required: true },
|
|
40
|
+
],
|
|
41
|
+
accessibility: {
|
|
42
|
+
role: 'group',
|
|
43
|
+
keyboardInteractions: ['Enter / Space to open calendar', 'Escape to close', 'Arrow keys to navigate calendar days'],
|
|
44
|
+
notes: 'Delegates to DateRangePicker for full calendar keyboard navigation and aria-haspopup="dialog".',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'filter-multi-select',
|
|
3
|
+
name: 'FilterMultiSelect',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A compact multi-select filter button with a dropdown panel of checkbox items, accent count badge, optional color swatches, and a "Clear all" footer.',
|
|
8
|
+
designIntent: 'FilterMultiSelect encapsulates the full multi-select filter pattern into a single component. ' +
|
|
9
|
+
'The trigger button shows the label plus an accent Chip with the selection count when items are active. ' +
|
|
10
|
+
'The dropdown stays open on toggle so users can check multiple items without reopening. ' +
|
|
11
|
+
'Each option renders a Checkbox for selection state; when a swatch color is provided, the label renders as a Chip with a colored dot. ' +
|
|
12
|
+
'An always-visible "Clear all" footer (disabled when nothing is selected) lets users reset without an external button. ' +
|
|
13
|
+
'The outline variant starts as an outlined button and upgrades to secondary when items are checked. ' +
|
|
14
|
+
'This replaces the verbose pattern of manually wiring Menu open state, keepOpen hacks, and count badges.',
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'label', type: 'string', required: true, description: 'Text shown on the trigger button.' },
|
|
17
|
+
{ name: 'options', type: 'array', required: true, description: 'Array of { value, label, swatch?, disabled? } option objects.' },
|
|
18
|
+
{ name: 'value', type: 'array', required: false, description: 'Controlled array of selected values.' },
|
|
19
|
+
{ name: 'defaultValue', type: 'array', required: false, default: '[]', description: 'Initial selected values for uncontrolled usage.' },
|
|
20
|
+
{ name: 'onChange', type: 'function', required: false, description: 'Called with the updated array of selected values on each change.' },
|
|
21
|
+
{ name: 'variant', type: 'enum', required: false, default: 'secondary', description: 'Button style. "outline" switches to "secondary" when items are selected.', enumValues: ['secondary', 'outline'] },
|
|
22
|
+
{ name: 'size', type: 'enum', required: false, default: 'sm', description: 'Controls button, checkbox, and chip size.', enumValues: ['sm', 'md', 'lg'] },
|
|
23
|
+
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Disables the trigger button.' },
|
|
24
|
+
{ name: 'icon', type: 'ReactNode', required: false, description: 'Icon rendered before the label via leftIcon on the trigger button.' },
|
|
25
|
+
{ name: 'style', type: 'object', required: false, description: 'Inline style overrides for the outer wrapper.' },
|
|
26
|
+
],
|
|
27
|
+
usageExamples: [
|
|
28
|
+
{
|
|
29
|
+
title: 'Tags with swatches',
|
|
30
|
+
code: `<FilterMultiSelect label="Tags" options={[
|
|
31
|
+
{ value: 'react', label: 'React', swatch: '#3b82f6' },
|
|
32
|
+
{ value: 'devops', label: 'DevOps', swatch: '#10b981' },
|
|
33
|
+
{ value: 'data', label: 'Data Science', swatch: '#6366f1' },
|
|
34
|
+
]} />`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: 'Plain labels',
|
|
38
|
+
code: `<FilterMultiSelect label="Status" options={[
|
|
39
|
+
{ value: 'active', label: 'Active' },
|
|
40
|
+
{ value: 'archived', label: 'Archived' },
|
|
41
|
+
{ value: 'on-hold', label: 'On hold' },
|
|
42
|
+
]} />`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Outline variant',
|
|
46
|
+
code: `<FilterMultiSelect label="Tags" variant="outline" options={tagOptions} />`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: 'Controlled',
|
|
50
|
+
code: `const [tags, setTags] = useState<string[]>([]);
|
|
51
|
+
<FilterMultiSelect label="Tags" value={tags} onChange={setTags} options={tagOptions} />`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
compositionGraph: [
|
|
55
|
+
{ componentId: 'button', componentName: 'Button', role: 'Trigger button with chevron and count badge', required: true },
|
|
56
|
+
{ componentId: 'chip', componentName: 'Chip', role: 'Count badge in trigger and swatch labels in dropdown', required: true },
|
|
57
|
+
{ componentId: 'checkbox', componentName: 'Checkbox', role: 'Selection indicator per option row', required: true },
|
|
58
|
+
{ componentId: 'text', componentName: 'Text', role: 'Plain option labels and Clear all footer', required: true },
|
|
59
|
+
],
|
|
60
|
+
accessibility: {
|
|
61
|
+
role: 'listbox',
|
|
62
|
+
ariaAttributes: ['aria-multiselectable', 'aria-selected', 'aria-disabled', 'aria-label'],
|
|
63
|
+
keyboardInteractions: ['↑↓ to navigate options', 'Enter / Space to toggle selection', 'Escape to close'],
|
|
64
|
+
notes: 'The dropdown panel has role="listbox" with aria-multiselectable. Each option has role="option" with aria-selected.',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'filter-search',
|
|
3
|
+
name: 'FilterSearch',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A collapsible search button that expands into a text input on click and collapses back when blurred empty.',
|
|
8
|
+
designIntent: 'FilterSearch saves toolbar space by rendering as a compact square icon button until the user needs it. ' +
|
|
9
|
+
'Clicking expands to a full Input with autofocus. Blurring with an empty value collapses it back. ' +
|
|
10
|
+
'If a value is present the input stays expanded so the user can see their active search. ' +
|
|
11
|
+
'Use in filter bars above DataTables and lists where search is secondary to structured filters.',
|
|
12
|
+
props: [
|
|
13
|
+
{ name: 'value', type: 'string', required: false, description: 'Controlled search value.' },
|
|
14
|
+
{ name: 'defaultValue', type: 'string', required: false, default: '""', description: 'Initial value for uncontrolled usage.' },
|
|
15
|
+
{ name: 'onChange', type: 'function', required: false, description: 'Called with the current input value on each keystroke.' },
|
|
16
|
+
{ name: 'placeholder', type: 'string', required: false, default: '"Search…"', description: 'Placeholder shown in the expanded input.' },
|
|
17
|
+
{ name: 'variant', type: 'enum', required: false, default: 'secondary', description: 'Button style when collapsed.', enumValues: ['secondary', 'outline'] },
|
|
18
|
+
{ name: 'size', type: 'enum', required: false, default: 'sm', description: 'Controls button and input height.', enumValues: ['sm', 'md', 'lg'] },
|
|
19
|
+
{ name: 'width', type: 'number', required: false, default: '260', description: 'Width of the expanded input in pixels.' },
|
|
20
|
+
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Disables the button and input.' },
|
|
21
|
+
{ name: 'style', type: 'object', required: false, description: 'Inline style overrides.' },
|
|
22
|
+
],
|
|
23
|
+
usageExamples: [
|
|
24
|
+
{ title: 'Basic', code: `<FilterSearch placeholder="Search candidates…" />` },
|
|
25
|
+
{ title: 'Controlled', code: `const [q, setQ] = useState('');\n<FilterSearch value={q} onChange={setQ} />` },
|
|
26
|
+
{ title: 'Outline variant', code: `<FilterSearch variant="outline" />` },
|
|
27
|
+
],
|
|
28
|
+
compositionGraph: [
|
|
29
|
+
{ componentId: 'button', componentName: 'Button', role: 'Collapsed icon trigger', required: true },
|
|
30
|
+
{ componentId: 'input', componentName: 'Input', role: 'Expanded text field', required: true },
|
|
31
|
+
],
|
|
32
|
+
accessibility: {
|
|
33
|
+
role: 'search',
|
|
34
|
+
keyboardInteractions: ['Enter / Space on icon button expands to input', 'Escape or blur with empty value collapses back'],
|
|
35
|
+
notes: 'The collapsed button has aria-label="Search". The expanded input receives autofocus.',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'filter-select',
|
|
3
|
+
name: 'FilterSelect',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A compact single-select filter button that opens a Menu dropdown. Shows the selected label in the trigger and a "Clear" footer when active.',
|
|
8
|
+
designIntent: 'FilterSelect wraps Button + Menu into a self-contained single-select filter. ' +
|
|
9
|
+
'The trigger shows the label prop when nothing is selected and switches to the selected option label when active. ' +
|
|
10
|
+
'A "Clear" footer appears when a value is selected so the user can reset without an external clear button. ' +
|
|
11
|
+
'The outline variant starts as an outlined button and upgrades to secondary when a value is picked, ' +
|
|
12
|
+
'giving a visual signal that the filter is active. ' +
|
|
13
|
+
'Use for sort controls, single-category filters, and any exclusive-choice filter in a toolbar.',
|
|
14
|
+
props: [
|
|
15
|
+
{ name: 'label', type: 'string', required: true, description: 'Text shown on the trigger button when no value is selected.' },
|
|
16
|
+
{ name: 'options', type: 'array', required: true, description: 'Array of { value, label, disabled? } option objects.' },
|
|
17
|
+
{ name: 'value', type: 'string', required: false, description: 'Controlled selected value.' },
|
|
18
|
+
{ name: 'defaultValue', type: 'string', required: false, description: 'Initial value for uncontrolled usage.' },
|
|
19
|
+
{ name: 'onChange', type: 'function', required: false, description: 'Called with the selected value, or undefined when cleared.' },
|
|
20
|
+
{ name: 'variant', type: 'enum', required: false, default: 'secondary', description: 'Button style. "outline" switches to "secondary" when a value is selected.', enumValues: ['secondary', 'outline'] },
|
|
21
|
+
{ name: 'size', type: 'enum', required: false, default: 'sm', description: 'Controls button and menu item size.', enumValues: ['sm', 'md', 'lg'] },
|
|
22
|
+
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Disables the trigger button.' },
|
|
23
|
+
{ name: 'icon', type: 'ReactNode', required: false, description: 'Icon rendered before the label via leftIcon on the trigger button.' },
|
|
24
|
+
{ name: 'style', type: 'object', required: false, description: 'Inline style overrides for the Menu wrapper.' },
|
|
25
|
+
],
|
|
26
|
+
usageExamples: [
|
|
27
|
+
{
|
|
28
|
+
title: 'Sort control',
|
|
29
|
+
code: `<FilterSelect label="Newest first" icon={<SortIcon />} options={[
|
|
30
|
+
{ value: 'newest', label: 'Newest first' },
|
|
31
|
+
{ value: 'oldest', label: 'Oldest first' },
|
|
32
|
+
{ value: 'name', label: 'Name A–Z' },
|
|
33
|
+
]} />`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Outline variant',
|
|
37
|
+
code: `<FilterSelect label="Availability" variant="outline" options={[
|
|
38
|
+
{ value: 'available', label: 'Available' },
|
|
39
|
+
{ value: 'unavailable', label: 'Unavailable' },
|
|
40
|
+
]} />`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'Controlled',
|
|
44
|
+
code: `const [sort, setSort] = useState<string | undefined>();
|
|
45
|
+
<FilterSelect label="Sort" value={sort} onChange={setSort} options={sortOptions} />`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
compositionGraph: [
|
|
49
|
+
{ componentId: 'button', componentName: 'Button', role: 'Trigger button with chevron', required: true },
|
|
50
|
+
{ componentId: 'menu', componentName: 'Menu', role: 'Dropdown popover', required: true },
|
|
51
|
+
{ componentId: 'text', componentName: 'Text', role: 'Clear footer label', required: true },
|
|
52
|
+
],
|
|
53
|
+
accessibility: {
|
|
54
|
+
role: 'listbox',
|
|
55
|
+
keyboardInteractions: ['Arrow keys to navigate', 'Enter / Space to select', 'Escape to close'],
|
|
56
|
+
notes: 'Delegates to Menu for full WAI-ARIA menu button keyboard navigation.',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -4,19 +4,20 @@ export const COMPONENT_MANIFEST = {
|
|
|
4
4
|
tier: 'molecule',
|
|
5
5
|
domain: 'neutral',
|
|
6
6
|
specVersion: '1.0',
|
|
7
|
-
description: 'A vertical
|
|
7
|
+
description: 'A vertical activity feed with filled status dots, inline timestamps, optional nested content blocks, and custom icons.',
|
|
8
8
|
designIntent: 'Timeline renders as a semantic <ol> with each event as a <li>, preserving document order for assistive technologies. ' +
|
|
9
|
-
'
|
|
10
|
-
'
|
|
9
|
+
'Dots are compact (20px) and filled with the status color, with white iconography for high contrast — this mirrors modern activity-feed patterns. ' +
|
|
10
|
+
'Title and date sit inline (date follows title) to keep the eye on a single reading line, rather than splitting attention across the row. ' +
|
|
11
|
+
'Each item supports an optional content slot for embedded blocks — comment cards, review notes, nested detail panels — placed below the title/description. ' +
|
|
12
|
+
'The connector line is thin (1.5px) and omitted on the last item. ' +
|
|
11
13
|
'Status colors follow the same semantic token set as Alert and Badge so danger/success/warning/info carry consistent meaning across the design system. ' +
|
|
12
|
-
'Default status (no explicit icon) renders a plain dot; success/danger/warning get built-in iconography inside the dot. ' +
|
|
13
14
|
'Custom icons slot in via the icon prop to handle domain-specific event types (e.g. a deploy icon, a payment icon).',
|
|
14
15
|
props: [
|
|
15
16
|
{
|
|
16
17
|
name: 'items',
|
|
17
18
|
type: 'array',
|
|
18
19
|
required: true,
|
|
19
|
-
description: 'Array of TimelineItem objects. Each has id, title, optional description, optional date string, optional status, and optional icon.',
|
|
20
|
+
description: 'Array of TimelineItem objects. Each has id, title, optional description, optional content (ReactNode for embedded blocks), optional date string, optional status, and optional icon.',
|
|
20
21
|
},
|
|
21
22
|
{
|
|
22
23
|
name: 'style',
|
|
@@ -38,18 +39,31 @@ export const COMPONENT_MANIFEST = {
|
|
|
38
39
|
/>`,
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
|
-
title: '
|
|
42
|
+
title: 'Activity feed with nested content',
|
|
42
43
|
code: `<Timeline
|
|
43
44
|
items={[
|
|
44
|
-
{ id: '
|
|
45
|
-
{
|
|
46
|
-
|
|
45
|
+
{ id: '1', title: 'Submitted for review', date: 'Mar 22', status: 'success' },
|
|
46
|
+
{
|
|
47
|
+
id: '2',
|
|
48
|
+
title: 'Changes requested',
|
|
49
|
+
date: 'Mar 25',
|
|
50
|
+
status: 'warning',
|
|
51
|
+
content: (
|
|
52
|
+
<Card variant="outline" padding="sm" radius="md">
|
|
53
|
+
<Text size="sm" weight="semibold">Oliver Brown</Text>
|
|
54
|
+
<Text size="sm" color="secondary">Please update the error handling.</Text>
|
|
55
|
+
</Card>
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
{ id: '3', title: 'Resubmitted', date: 'Mar 28', status: 'info' },
|
|
59
|
+
{ id: '4', title: 'Approved', date: 'Mar 29', status: 'success' },
|
|
47
60
|
]}
|
|
48
61
|
/>`,
|
|
49
62
|
},
|
|
50
63
|
],
|
|
51
64
|
compositionGraph: [
|
|
52
65
|
{ componentId: 'text', componentName: 'Text', role: 'Event title, description, and date label', required: true },
|
|
66
|
+
{ componentId: 'card', componentName: 'Card', role: 'Container for nested content blocks in the content slot', required: false },
|
|
53
67
|
],
|
|
54
68
|
accessibility: {
|
|
55
69
|
role: 'list',
|
|
@@ -1,197 +1,122 @@
|
|
|
1
1
|
export const RECIPE = {
|
|
2
2
|
id: 'search-filter-bar',
|
|
3
3
|
name: 'Search / Filter Bar',
|
|
4
|
-
description: 'Compact toolbar of
|
|
4
|
+
description: 'Compact toolbar of filter molecules — FilterSearch, FilterSelect, FilterMultiSelect, FilterDateRange — with sort control and view toggle. Designed to sit above a DataTable or list.',
|
|
5
5
|
category: 'dashboard',
|
|
6
|
-
components: ['
|
|
6
|
+
components: ['filter-search', 'filter-select', 'filter-multi-select', 'filter-date-range', 'segmented-control', 'row', 'text', 'button'],
|
|
7
7
|
structure: `
|
|
8
8
|
Row gap="2" align="center" wrap
|
|
9
|
-
├──
|
|
10
|
-
├──
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
├──
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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)
|
|
9
|
+
├── FilterSearch ← icon button, expands to Input
|
|
10
|
+
├── FilterSelect (label="Availability") ← single-select dropdown
|
|
11
|
+
├── FilterMultiSelect (label="Status") ← multi-select, stays open, count badge
|
|
12
|
+
├── FilterMultiSelect (label="Tags") ← multi-select with swatch chips
|
|
13
|
+
├── FilterDateRange (label="Date range") ← button trigger → calendar popover
|
|
14
|
+
├── Text + Button (ghost, "Clear all") ← conditional result count row
|
|
15
|
+
├── flex spacer
|
|
16
|
+
└── Row gap="2" align="center"
|
|
17
|
+
├── FilterSelect (icon=sort, "Newest") ← sort control
|
|
18
|
+
└── SegmentedControl (grid/list icons) ← view toggle
|
|
28
19
|
`.trim(),
|
|
29
|
-
code: `const [
|
|
30
|
-
const [statuses, setStatuses] = useState<
|
|
31
|
-
const [
|
|
32
|
-
const [tags, setTags] = useState<Set<string>>(new Set());
|
|
33
|
-
const [tagsOpen, setTagsOpen] = useState(false);
|
|
20
|
+
code: `const [search, setSearch] = useState('');
|
|
21
|
+
const [statuses, setStatuses] = useState<string[]>([]);
|
|
22
|
+
const [tags, setTags] = useState<string[]>([]);
|
|
34
23
|
const [dateRange, setDateRange] = useState<DateRange | undefined>();
|
|
35
24
|
|
|
36
|
-
const
|
|
37
|
-
|
|
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); };
|
|
25
|
+
const hasFilters = statuses.length > 0 || tags.length > 0 || dateRange !== undefined;
|
|
26
|
+
const clearAll = () => { setStatuses([]); setTags([]); setDateRange(undefined); setSearch(''); };
|
|
49
27
|
|
|
50
28
|
<Row gap="2" align="center" wrap>
|
|
51
|
-
|
|
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>
|
|
29
|
+
<FilterSearch placeholder="Search…" value={search} onChange={setSearch} />
|
|
67
30
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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>
|
|
31
|
+
<FilterSelect label="Availability" options={[
|
|
32
|
+
{ value: 'available', label: 'Available' },
|
|
33
|
+
{ value: 'notice', label: 'On notice' },
|
|
34
|
+
{ value: 'unavailable', label: 'Unavailable' },
|
|
35
|
+
]} />
|
|
84
36
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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>
|
|
37
|
+
<FilterMultiSelect label="Status" options={[
|
|
38
|
+
{ value: 'active', label: 'Active' },
|
|
39
|
+
{ value: 'archived', label: 'Archived' },
|
|
40
|
+
{ value: 'on-hold', label: 'On hold' },
|
|
41
|
+
]} value={statuses} onChange={setStatuses} />
|
|
104
42
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
</Button>} />
|
|
43
|
+
<FilterMultiSelect label="Tags" options={[
|
|
44
|
+
{ value: 'react', label: 'React', swatch: '#3b82f6' },
|
|
45
|
+
{ value: 'devops', label: 'DevOps', swatch: '#10b981' },
|
|
46
|
+
{ value: 'data', label: 'Data Science', swatch: '#6366f1' },
|
|
47
|
+
]} value={tags} onChange={setTags} />
|
|
111
48
|
|
|
112
|
-
|
|
113
|
-
{hasFilters && <Button variant="ghost" size="sm" onClick={clearAll}>Clear all</Button>}
|
|
49
|
+
<FilterDateRange label="Date range" value={dateRange} onChange={setDateRange} />
|
|
114
50
|
|
|
115
|
-
{/* Spacer */}
|
|
116
51
|
<div style={{ flex: 1 }} />
|
|
117
52
|
|
|
118
|
-
{/* Sort + view toggle */}
|
|
119
53
|
<Row gap="2" align="center">
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
<MenuItem selected onSelect={() => {}}>Newest first</MenuItem>
|
|
126
|
-
<MenuItem onSelect={() => {}}>Oldest first</MenuItem>
|
|
127
|
-
<MenuItem onSelect={() => {}}>Name A–Z</MenuItem>
|
|
128
|
-
</Menu>
|
|
54
|
+
<FilterSelect label="Newest first" icon={<SortIcon />} options={[
|
|
55
|
+
{ value: 'newest', label: 'Newest first' },
|
|
56
|
+
{ value: 'oldest', label: 'Oldest first' },
|
|
57
|
+
{ value: 'name', label: 'Name A–Z' },
|
|
58
|
+
]} />
|
|
129
59
|
<SegmentedControl size="sm" defaultValue="grid" options={[
|
|
130
60
|
{ value: 'grid', label: <GridIcon /> },
|
|
131
61
|
{ value: 'list', label: <ListIcon /> },
|
|
132
62
|
]} />
|
|
133
63
|
</Row>
|
|
134
|
-
</Row
|
|
64
|
+
</Row>
|
|
65
|
+
|
|
66
|
+
{/* Result count between filter bar and DataTable */}
|
|
67
|
+
{hasFilters && (
|
|
68
|
+
<Row gap="2" align="center">
|
|
69
|
+
<Text size="sm" color="secondary">
|
|
70
|
+
Showing {filteredRows.length} of {allRows.length} candidates
|
|
71
|
+
</Text>
|
|
72
|
+
<Button variant="ghost" size="xs" onClick={clearAll}>Clear all</Button>
|
|
73
|
+
</Row>
|
|
74
|
+
)}`,
|
|
135
75
|
variants: [
|
|
136
76
|
{
|
|
137
77
|
title: 'Minimal — search + sort only',
|
|
138
78
|
code: `<Row gap="2" align="center">
|
|
139
|
-
<
|
|
79
|
+
<FilterSearch placeholder="Search…" />
|
|
140
80
|
<div style={{ flex: 1 }} />
|
|
141
|
-
<
|
|
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>
|
|
81
|
+
<FilterSelect label="Newest first" icon={<SortIcon />} options={sortOptions} />
|
|
150
82
|
</Row>`,
|
|
151
83
|
},
|
|
152
84
|
{
|
|
153
|
-
title: '
|
|
154
|
-
code:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
))}
|
|
180
|
-
</Menu>
|
|
181
|
-
{hasFilters && <Button variant="ghost" size="sm" onClick={clearAll}>Clear all</Button>}
|
|
182
|
-
</Row>`,
|
|
85
|
+
title: 'With DataTable',
|
|
86
|
+
code: `const [search, setSearch] = useState('');
|
|
87
|
+
const [statuses, setStatuses] = useState<string[]>([]);
|
|
88
|
+
|
|
89
|
+
const filtered = candidates.filter(c => {
|
|
90
|
+
if (search && !c.name.toLowerCase().includes(search.toLowerCase())) return false;
|
|
91
|
+
if (statuses.length > 0 && !statuses.includes(c.status.toLowerCase())) return false;
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
<Stack gap="3">
|
|
96
|
+
<Row gap="2" align="center" wrap>
|
|
97
|
+
<FilterSearch placeholder="Search…" value={search} onChange={setSearch} />
|
|
98
|
+
<FilterMultiSelect label="Status" options={statusOptions}
|
|
99
|
+
value={statuses} onChange={setStatuses} />
|
|
100
|
+
</Row>
|
|
101
|
+
{statuses.length > 0 && (
|
|
102
|
+
<Row gap="2" align="center">
|
|
103
|
+
<Text size="sm" color="secondary">
|
|
104
|
+
Showing {filtered.length} of {candidates.length}
|
|
105
|
+
</Text>
|
|
106
|
+
<Button variant="ghost" size="xs" onClick={() => setStatuses([])}>Clear all</Button>
|
|
107
|
+
</Row>
|
|
108
|
+
)}
|
|
109
|
+
<DataTable columns={columns} rows={filtered} pageSize={5} />
|
|
110
|
+
</Stack>`,
|
|
183
111
|
},
|
|
184
112
|
],
|
|
185
|
-
designNotes: '
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
'
|
|
189
|
-
'
|
|
190
|
-
'
|
|
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. ' +
|
|
113
|
+
designNotes: 'Four dedicated filter molecules replace the previous boilerplate of manually wiring Menu open state, ' +
|
|
114
|
+
'keepOpen hacks, count badges, and clear footers. FilterSearch collapses to a square icon-only button ' +
|
|
115
|
+
'and expands on click. FilterSelect wraps Menu for single-select with a "Clear" footer. ' +
|
|
116
|
+
'FilterMultiSelect builds its own portal dropdown (not Menu) so items toggle without closing — ' +
|
|
117
|
+
'it renders Checkbox + optional Chip (with swatch) per option and an always-visible "Clear all" footer. ' +
|
|
118
|
+
'FilterDateRange wraps DateRangePicker with the trigger prop to render a secondary Button. ' +
|
|
194
119
|
'A flex spacer pushes sort and view controls to the right edge. ' +
|
|
195
|
-
'
|
|
196
|
-
'
|
|
120
|
+
'Between the filter bar and DataTable, a conditional result count row shows ' +
|
|
121
|
+
'"Showing X of Y" with a ghost "Clear all" button when filters are active.',
|
|
197
122
|
};
|