lucent-ui 0.40.0 → 0.41.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/LucentProvider-DluNb5H9.cjs +109 -0
- package/dist/{LucentProvider-Bm39MMvv.js → LucentProvider-lGSitrJV.js} +1019 -1001
- package/dist/devtools.cjs +1 -1
- package/dist/devtools.js +1 -1
- package/dist/index.cjs +47 -42
- package/dist/index.d.ts +33 -4
- package/dist/index.js +2252 -1998
- package/dist-server/server/pattern-registry.js +6 -0
- package/dist-server/server/recipe-registry.js +18 -0
- package/dist-server/src/components/molecules/PageLayout/PageLayout.manifest.js +65 -8
- package/dist-server/src/manifest/patterns/index.js +2 -0
- package/dist-server/src/manifest/patterns/multi-step-wizard.pattern.js +180 -0
- package/dist-server/src/manifest/patterns/search-filter-panel.pattern.js +188 -0
- package/dist-server/src/manifest/patterns/tab-page.pattern.js +152 -0
- package/dist-server/src/manifest/recipes/action-bar.recipe.js +91 -0
- package/dist-server/src/manifest/recipes/collapsible-card.recipe.js +100 -0
- package/dist-server/src/manifest/recipes/empty-state-card.recipe.js +72 -0
- package/dist-server/src/manifest/recipes/form-layout.recipe.js +98 -0
- package/dist-server/src/manifest/recipes/index.js +8 -0
- package/dist-server/src/manifest/recipes/profile-card.recipe.js +101 -0
- package/dist-server/src/manifest/recipes/search-filter-bar.recipe.js +122 -0
- package/dist-server/src/manifest/recipes/settings-panel.recipe.js +167 -0
- package/dist-server/src/manifest/recipes/stats-row.recipe.js +106 -0
- package/dist-server/src/manifest/validate.test.js +28 -0
- package/package.json +1 -1
- package/dist/LucentProvider-CzEDW5SL.cjs +0 -109
|
@@ -12,6 +12,9 @@ import { PATTERN as ConfirmationDialog } from '../src/manifest/patterns/confirma
|
|
|
12
12
|
import { PATTERN as BulkActionBar } from '../src/manifest/patterns/bulk-action-bar.pattern.js';
|
|
13
13
|
import { PATTERN as ActivityFeed } from '../src/manifest/patterns/activity-feed.pattern.js';
|
|
14
14
|
import { PATTERN as MetricsDashboard } from '../src/manifest/patterns/metrics-dashboard.pattern.js';
|
|
15
|
+
import { PATTERN as TabPage } from '../src/manifest/patterns/tab-page.pattern.js';
|
|
16
|
+
import { PATTERN as SearchFilterPanel } from '../src/manifest/patterns/search-filter-panel.pattern.js';
|
|
17
|
+
import { PATTERN as MultiStepWizard } from '../src/manifest/patterns/multi-step-wizard.pattern.js';
|
|
15
18
|
export const ALL_PATTERNS = [
|
|
16
19
|
ProfileCard,
|
|
17
20
|
SettingsPanel,
|
|
@@ -27,4 +30,7 @@ export const ALL_PATTERNS = [
|
|
|
27
30
|
BulkActionBar,
|
|
28
31
|
ActivityFeed,
|
|
29
32
|
MetricsDashboard,
|
|
33
|
+
TabPage,
|
|
34
|
+
SearchFilterPanel,
|
|
35
|
+
MultiStepWizard,
|
|
30
36
|
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RECIPE as ProfileCard } from '../src/manifest/recipes/profile-card.recipe.js';
|
|
2
|
+
import { RECIPE as SettingsPanel } from '../src/manifest/recipes/settings-panel.recipe.js';
|
|
3
|
+
import { RECIPE as StatsRow } from '../src/manifest/recipes/stats-row.recipe.js';
|
|
4
|
+
import { RECIPE as ActionBar } from '../src/manifest/recipes/action-bar.recipe.js';
|
|
5
|
+
import { RECIPE as FormLayout } from '../src/manifest/recipes/form-layout.recipe.js';
|
|
6
|
+
import { RECIPE as EmptyStateCard } from '../src/manifest/recipes/empty-state-card.recipe.js';
|
|
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';
|
|
9
|
+
export const ALL_RECIPES = [
|
|
10
|
+
ProfileCard,
|
|
11
|
+
SettingsPanel,
|
|
12
|
+
StatsRow,
|
|
13
|
+
ActionBar,
|
|
14
|
+
FormLayout,
|
|
15
|
+
EmptyStateCard,
|
|
16
|
+
CollapsibleCard,
|
|
17
|
+
SearchFilterBar,
|
|
18
|
+
];
|
|
@@ -4,15 +4,22 @@ export const COMPONENT_MANIFEST = {
|
|
|
4
4
|
tier: 'molecule',
|
|
5
5
|
domain: 'neutral',
|
|
6
6
|
specVersion: '0.1',
|
|
7
|
-
description: 'Full-viewport shell layout with optional header, left sidebar, right panel, and footer slots arranged in a flex column/row structure.',
|
|
7
|
+
description: 'Full-viewport shell layout with optional header, left sidebar (with header/footer slots), right panel, and footer slots arranged in a flex column/row structure.',
|
|
8
8
|
designIntent: 'PageLayout owns the outermost chrome of an application page. Chrome regions (header, sidebar, footer) ' +
|
|
9
9
|
'default to the navigation token so the main content card feels elevated against the chrome — especially ' +
|
|
10
10
|
'noticeable with tinted navigation values. The body row is a flex row containing ' +
|
|
11
11
|
'an optional left sidebar, a bordered main content card, and an optional right panel — all as structural ' +
|
|
12
12
|
'siblings so they share the same vertical space. The header and footer sit outside the body row as ' +
|
|
13
|
-
'flex children of the outer column, ensuring they span the full width.
|
|
14
|
-
'
|
|
15
|
-
'
|
|
13
|
+
'flex children of the outer column, ensuring they span the full width. ' +
|
|
14
|
+
'The left sidebar is a flex column with three zones: sidebarHeader (pinned top — logo/branding), ' +
|
|
15
|
+
'sidebar (scrollable middle — navigation), and sidebarFooter (pinned bottom — account/help/settings). ' +
|
|
16
|
+
'All three zones are optional and render independently. ' +
|
|
17
|
+
'sidebarCollapsedWidth controls the collapsed width (default 0 for full collapse, set to e.g. 56 for icon-only rail). ' +
|
|
18
|
+
'On narrow viewports (below sidebarDrawerBreakpoint, default 768px), sidebarMode "auto" automatically ' +
|
|
19
|
+
'switches the sidebar to a slide-in drawer overlay with a hamburger toggle in the header, ' +
|
|
20
|
+
'keeping the main content full-width. The drawer renders via portal with backdrop, ' +
|
|
21
|
+
'closes on backdrop click or Escape, and locks body scroll while open. ' +
|
|
22
|
+
'The main card automatically drops its right margin when a right panel is present so no manual mainStyle override is needed. ' +
|
|
16
23
|
'The footer is intentionally narrow (default 28px) and should be used sparingly — suited to ' +
|
|
17
24
|
'status bars, connection indicators, keyboard shortcut hints, or contextual action strips, ' +
|
|
18
25
|
'in the style of an editor status bar. It is not meant as a general-purpose page footer.',
|
|
@@ -40,7 +47,19 @@ export const COMPONENT_MANIFEST = {
|
|
|
40
47
|
name: 'sidebar',
|
|
41
48
|
type: 'ReactNode',
|
|
42
49
|
required: false,
|
|
43
|
-
description: '
|
|
50
|
+
description: 'Scrollable content in the middle zone of the left sidebar (typically navigation).',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'sidebarHeader',
|
|
54
|
+
type: 'ReactNode',
|
|
55
|
+
required: false,
|
|
56
|
+
description: 'Pinned content at the top of the sidebar — logo, app name, branding. Does not scroll.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'sidebarFooter',
|
|
60
|
+
type: 'ReactNode',
|
|
61
|
+
required: false,
|
|
62
|
+
description: 'Pinned content at the bottom of the sidebar — account switcher, help, secondary actions. Does not scroll.',
|
|
44
63
|
},
|
|
45
64
|
{
|
|
46
65
|
name: 'sidebarWidth',
|
|
@@ -49,12 +68,47 @@ export const COMPONENT_MANIFEST = {
|
|
|
49
68
|
default: '240',
|
|
50
69
|
description: 'Left sidebar width in px (number) or any CSS value (string). Default: 240.',
|
|
51
70
|
},
|
|
71
|
+
{
|
|
72
|
+
name: 'sidebarCollapsedWidth',
|
|
73
|
+
type: 'string',
|
|
74
|
+
required: false,
|
|
75
|
+
default: '0',
|
|
76
|
+
description: 'Width the sidebar collapses to — 0 for full collapse (default), 56 for icon-only rail.',
|
|
77
|
+
},
|
|
52
78
|
{
|
|
53
79
|
name: 'sidebarCollapsed',
|
|
54
80
|
type: 'boolean',
|
|
55
81
|
required: false,
|
|
56
82
|
default: 'false',
|
|
57
|
-
description: 'When true, collapses the left sidebar to
|
|
83
|
+
description: 'When true, collapses the left sidebar to sidebarCollapsedWidth with a transition.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'sidebarMode',
|
|
87
|
+
type: 'string',
|
|
88
|
+
required: false,
|
|
89
|
+
default: '"auto"',
|
|
90
|
+
description: 'Sidebar display mode. "inline" = always inline, "drawer" = always drawer overlay, ' +
|
|
91
|
+
'"auto" = switches to drawer below sidebarDrawerBreakpoint. Default: "auto".',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'sidebarDrawerBreakpoint',
|
|
95
|
+
type: 'number',
|
|
96
|
+
required: false,
|
|
97
|
+
default: '768',
|
|
98
|
+
description: 'Viewport width (px) below which sidebar switches to drawer mode. Only used when sidebarMode="auto". Default: 768.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'sidebarDrawerOpen',
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
required: false,
|
|
104
|
+
default: 'false',
|
|
105
|
+
description: 'Controlled open state when sidebar is in drawer mode.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'onSidebarDrawerOpenChange',
|
|
109
|
+
type: '(open: boolean) => void',
|
|
110
|
+
required: false,
|
|
111
|
+
description: 'Called when the drawer open state changes (backdrop click, escape key).',
|
|
58
112
|
},
|
|
59
113
|
{
|
|
60
114
|
name: 'rightSidebar',
|
|
@@ -137,10 +191,13 @@ export const COMPONENT_MANIFEST = {
|
|
|
137
191
|
</PageLayout>`,
|
|
138
192
|
},
|
|
139
193
|
{
|
|
140
|
-
title: 'Collapsible sidebar',
|
|
194
|
+
title: 'Collapsible sidebar with logo and account footer',
|
|
141
195
|
code: `<PageLayout
|
|
142
|
-
|
|
196
|
+
sidebarHeader={<Logo collapsed={isCollapsed} />}
|
|
197
|
+
sidebar={<NavMenu orientation="vertical" hasIcons>...</NavMenu>}
|
|
198
|
+
sidebarFooter={<AccountMenu collapsed={isCollapsed} />}
|
|
143
199
|
sidebarCollapsed={isCollapsed}
|
|
200
|
+
sidebarCollapsedWidth={56}
|
|
144
201
|
>
|
|
145
202
|
<div>Page content</div>
|
|
146
203
|
</PageLayout>`,
|
|
@@ -10,3 +10,5 @@ export { PATTERN as PricingTable } from './pricing-table.pattern.js';
|
|
|
10
10
|
export { PATTERN as NotificationFeed } from './notification-feed.pattern.js';
|
|
11
11
|
export { PATTERN as OnboardingFlow } from './onboarding-flow.pattern.js';
|
|
12
12
|
export { PATTERN as DashboardHeader } from './dashboard-header.pattern.js';
|
|
13
|
+
export { PATTERN as TabPage } from './tab-page.pattern.js';
|
|
14
|
+
export { PATTERN as SearchFilterPanel } from './search-filter-panel.pattern.js';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
export const PATTERN = {
|
|
2
|
+
id: 'multi-step-wizard',
|
|
3
|
+
name: 'Multi-Step Wizard',
|
|
4
|
+
description: 'Stepped form with progress indicator and back/next navigation. For onboarding flows, checkout, or any sequential data collection.',
|
|
5
|
+
category: 'form',
|
|
6
|
+
components: ['card', 'text', 'progress', 'form-field', 'input', 'select', 'button', 'stack', 'row', 'divider'],
|
|
7
|
+
structure: `
|
|
8
|
+
Card (elevated, padding="lg")
|
|
9
|
+
└── Stack gap="5"
|
|
10
|
+
├── Stack gap="2" ← progress header
|
|
11
|
+
│ ├── Text (xs, secondary) ← "Step 2 of 4"
|
|
12
|
+
│ └── Progress (sm) ← progress bar
|
|
13
|
+
├── Stack gap="1" ← step title area
|
|
14
|
+
│ ├── Text (lg, semibold) ← step title
|
|
15
|
+
│ └── Text (sm, secondary) ← step description
|
|
16
|
+
├── Divider
|
|
17
|
+
├── Stack gap="4" ← step content
|
|
18
|
+
│ └── FormField[] ← fields for current step
|
|
19
|
+
└── Row gap="3" justify="between" ← navigation
|
|
20
|
+
├── Button (outline) ← "Back"
|
|
21
|
+
└── Button (primary) ← "Continue"
|
|
22
|
+
`.trim(),
|
|
23
|
+
code: `const [step, setStep] = useState(0);
|
|
24
|
+
|
|
25
|
+
const STEPS = [
|
|
26
|
+
{ title: 'Shipping address', description: 'Where should we deliver your order?' },
|
|
27
|
+
{ title: 'Payment method', description: 'How would you like to pay?' },
|
|
28
|
+
{ title: 'Order preferences', description: 'Customise your delivery.' },
|
|
29
|
+
{ title: 'Review & confirm', description: 'Double-check everything before placing your order.' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const current = STEPS[step];
|
|
33
|
+
|
|
34
|
+
<Card variant="elevated" padding="lg" style={{ width: 480 }}>
|
|
35
|
+
<Stack gap="5">
|
|
36
|
+
<Stack gap="2">
|
|
37
|
+
<Text size="xs" color="secondary">Step {step + 1} of {STEPS.length}</Text>
|
|
38
|
+
<Progress value={((step + 1) / STEPS.length) * 100} size="sm" />
|
|
39
|
+
</Stack>
|
|
40
|
+
<Stack gap="1">
|
|
41
|
+
<Text as="h2" size="lg" weight="semibold">{current.title}</Text>
|
|
42
|
+
<Text size="sm" color="secondary">{current.description}</Text>
|
|
43
|
+
</Stack>
|
|
44
|
+
<Divider />
|
|
45
|
+
{step === 0 && (
|
|
46
|
+
<Stack gap="4">
|
|
47
|
+
<FormField label="Full name" htmlFor="wiz-name" required>
|
|
48
|
+
<Input id="wiz-name" placeholder="Jane Doe" />
|
|
49
|
+
</FormField>
|
|
50
|
+
<FormField label="Street address" htmlFor="wiz-addr" required>
|
|
51
|
+
<Input id="wiz-addr" placeholder="123 Main St" />
|
|
52
|
+
</FormField>
|
|
53
|
+
<Row gap="4">
|
|
54
|
+
<FormField label="City" htmlFor="wiz-city" required style={{ flex: 2 }}>
|
|
55
|
+
<Input id="wiz-city" placeholder="San Francisco" />
|
|
56
|
+
</FormField>
|
|
57
|
+
<FormField label="State" htmlFor="wiz-state" required style={{ flex: 1 }}>
|
|
58
|
+
<Input id="wiz-state" placeholder="CA" />
|
|
59
|
+
</FormField>
|
|
60
|
+
<FormField label="ZIP" htmlFor="wiz-zip" required style={{ flex: 1 }}>
|
|
61
|
+
<Input id="wiz-zip" placeholder="94103" />
|
|
62
|
+
</FormField>
|
|
63
|
+
</Row>
|
|
64
|
+
</Stack>
|
|
65
|
+
)}
|
|
66
|
+
{step === 1 && (
|
|
67
|
+
<Stack gap="4">
|
|
68
|
+
<FormField label="Card number" htmlFor="wiz-card" required>
|
|
69
|
+
<Input id="wiz-card" placeholder="4242 4242 4242 4242" />
|
|
70
|
+
</FormField>
|
|
71
|
+
<Row gap="4">
|
|
72
|
+
<FormField label="Expiry" htmlFor="wiz-exp" required style={{ flex: 1 }}>
|
|
73
|
+
<Input id="wiz-exp" placeholder="MM / YY" />
|
|
74
|
+
</FormField>
|
|
75
|
+
<FormField label="CVV" htmlFor="wiz-cvv" required style={{ flex: 1 }}>
|
|
76
|
+
<Input id="wiz-cvv" placeholder="123" />
|
|
77
|
+
</FormField>
|
|
78
|
+
</Row>
|
|
79
|
+
</Stack>
|
|
80
|
+
)}
|
|
81
|
+
{step === 2 && (
|
|
82
|
+
<Stack gap="4">
|
|
83
|
+
<FormField label="Delivery speed" htmlFor="wiz-speed">
|
|
84
|
+
<Select
|
|
85
|
+
id="wiz-speed"
|
|
86
|
+
defaultValue="standard"
|
|
87
|
+
options={[
|
|
88
|
+
{ value: 'standard', label: 'Standard (5-7 days)' },
|
|
89
|
+
{ value: 'express', label: 'Express (2-3 days)' },
|
|
90
|
+
{ value: 'overnight', label: 'Overnight' },
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
</FormField>
|
|
94
|
+
<FormField label="Gift message (optional)" htmlFor="wiz-gift">
|
|
95
|
+
<Input id="wiz-gift" placeholder="Happy birthday!" />
|
|
96
|
+
</FormField>
|
|
97
|
+
</Stack>
|
|
98
|
+
)}
|
|
99
|
+
{step === 3 && (
|
|
100
|
+
<Stack gap="3">
|
|
101
|
+
<Row justify="between">
|
|
102
|
+
<Text size="sm" color="secondary">Ship to</Text>
|
|
103
|
+
<Text size="sm">Jane Doe, 123 Main St, SF 94103</Text>
|
|
104
|
+
</Row>
|
|
105
|
+
<Row justify="between">
|
|
106
|
+
<Text size="sm" color="secondary">Payment</Text>
|
|
107
|
+
<Text size="sm">Visa ending 4242</Text>
|
|
108
|
+
</Row>
|
|
109
|
+
<Row justify="between">
|
|
110
|
+
<Text size="sm" color="secondary">Delivery</Text>
|
|
111
|
+
<Text size="sm">Standard (5-7 days)</Text>
|
|
112
|
+
</Row>
|
|
113
|
+
<Divider />
|
|
114
|
+
<Row justify="between">
|
|
115
|
+
<Text size="sm" weight="semibold">Total</Text>
|
|
116
|
+
<Text size="sm" weight="semibold">$49.99</Text>
|
|
117
|
+
</Row>
|
|
118
|
+
</Stack>
|
|
119
|
+
)}
|
|
120
|
+
<Row gap="3" justify="between">
|
|
121
|
+
<Button variant="outline" onClick={() => setStep(s => s - 1)} disabled={step === 0}>
|
|
122
|
+
Back
|
|
123
|
+
</Button>
|
|
124
|
+
<Button variant="primary" onClick={() => setStep(s => Math.min(s + 1, STEPS.length - 1))}>
|
|
125
|
+
{step === STEPS.length - 1 ? 'Place order' : 'Continue'}
|
|
126
|
+
</Button>
|
|
127
|
+
</Row>
|
|
128
|
+
</Stack>
|
|
129
|
+
</Card>`,
|
|
130
|
+
variants: [
|
|
131
|
+
{
|
|
132
|
+
title: 'Final step with summary & confirm',
|
|
133
|
+
code: `const [step, setStep] = useState(2);
|
|
134
|
+
const STEPS = ['Details', 'Preferences', 'Confirm'];
|
|
135
|
+
|
|
136
|
+
<Card variant="elevated" padding="lg" style={{ width: 440 }}>
|
|
137
|
+
<Stack gap="5">
|
|
138
|
+
<Stack gap="2">
|
|
139
|
+
<Text size="xs" color="secondary">Step {step + 1} of {STEPS.length}</Text>
|
|
140
|
+
<Progress value={((step + 1) / STEPS.length) * 100} size="sm" />
|
|
141
|
+
</Stack>
|
|
142
|
+
<Stack gap="1">
|
|
143
|
+
<Text as="h2" size="lg" weight="semibold">Confirm your details</Text>
|
|
144
|
+
<Text size="sm" color="secondary">Review everything before submitting.</Text>
|
|
145
|
+
</Stack>
|
|
146
|
+
<Divider />
|
|
147
|
+
<Stack gap="3">
|
|
148
|
+
<Row justify="between">
|
|
149
|
+
<Text size="sm" color="secondary">Name</Text>
|
|
150
|
+
<Text size="sm">Jane Doe</Text>
|
|
151
|
+
</Row>
|
|
152
|
+
<Row justify="between">
|
|
153
|
+
<Text size="sm" color="secondary">Email</Text>
|
|
154
|
+
<Text size="sm">jane@example.com</Text>
|
|
155
|
+
</Row>
|
|
156
|
+
<Row justify="between">
|
|
157
|
+
<Text size="sm" color="secondary">Plan</Text>
|
|
158
|
+
<Text size="sm">Pro — $19/mo</Text>
|
|
159
|
+
</Row>
|
|
160
|
+
</Stack>
|
|
161
|
+
<Row gap="3" justify="between">
|
|
162
|
+
<Button variant="outline" onClick={() => setStep(s => s - 1)}>Back</Button>
|
|
163
|
+
<Button variant="primary">Confirm</Button>
|
|
164
|
+
</Row>
|
|
165
|
+
</Stack>
|
|
166
|
+
</Card>`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
designNotes: 'The progress header (step counter + Progress bar) sits at the top so the user always ' +
|
|
170
|
+
'knows where they are. gap="5" between major sections gives clear breathing room without ' +
|
|
171
|
+
'feeling sparse. The step title and description use gap="1" to pair tightly as a unit. ' +
|
|
172
|
+
'A Divider below the title separates navigation chrome from step content. Step content ' +
|
|
173
|
+
'uses gap="4" for field spacing, matching form-layout conventions. Side-by-side fields ' +
|
|
174
|
+
'(city/state/zip) use Row with flex ratios so they share width proportionally. The ' +
|
|
175
|
+
'navigation row uses justify="between" with outline Back and primary Continue — the ' +
|
|
176
|
+
'outline variant signals "go back" without competing with the primary action. Back is ' +
|
|
177
|
+
'disabled on step 0 rather than hidden, so the layout stays stable across steps. On the ' +
|
|
178
|
+
'final step the button label changes to a concrete action ("Place order", "Confirm") to ' +
|
|
179
|
+
'signal finality.',
|
|
180
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export const PATTERN = {
|
|
2
|
+
id: 'search-filter-panel',
|
|
3
|
+
name: 'Search & Filter Panel',
|
|
4
|
+
description: 'Advanced filter panel inside a Collapsible disclosure — expands to reveal search, multiple filter groups (select, multi-select, date range), active filter chips, and apply/clear actions. More full-featured than Search / Filter Bar.',
|
|
5
|
+
category: 'form',
|
|
6
|
+
components: [
|
|
7
|
+
'card',
|
|
8
|
+
'collapsible',
|
|
9
|
+
'search-input',
|
|
10
|
+
'select',
|
|
11
|
+
'multi-select',
|
|
12
|
+
'date-range-picker',
|
|
13
|
+
'chip',
|
|
14
|
+
'button',
|
|
15
|
+
'row',
|
|
16
|
+
'stack',
|
|
17
|
+
'divider',
|
|
18
|
+
],
|
|
19
|
+
structure: `
|
|
20
|
+
Card
|
|
21
|
+
└── Collapsible (trigger="Filters" + active count badge)
|
|
22
|
+
└── Stack gap="4"
|
|
23
|
+
├── SearchInput (full width)
|
|
24
|
+
├── Row gap="3" wrap
|
|
25
|
+
│ ├── Select ← category filter
|
|
26
|
+
│ ├── MultiSelect ← tags filter
|
|
27
|
+
│ ├── DateRangePicker ← date range filter
|
|
28
|
+
│ └── Select ← sort by
|
|
29
|
+
├── Row gap="2" wrap ← active filter chips
|
|
30
|
+
│ └── Chip[] (onDismiss) ← closable chips
|
|
31
|
+
├── Divider
|
|
32
|
+
└── Row gap="3" justify="end"
|
|
33
|
+
├── Button (ghost) ← "Clear all"
|
|
34
|
+
└── Button (primary) ← "Apply filters"
|
|
35
|
+
`.trim(),
|
|
36
|
+
code: `const [search, setSearch] = useState('');
|
|
37
|
+
const [category, setCategory] = useState('');
|
|
38
|
+
const [tags, setTags] = useState<string[]>([]);
|
|
39
|
+
const [dateRange, setDateRange] = useState<DateRange | undefined>();
|
|
40
|
+
const [sortBy, setSortBy] = useState('newest');
|
|
41
|
+
|
|
42
|
+
type ActiveFilter = { key: string; label: string; clear: () => void };
|
|
43
|
+
|
|
44
|
+
const activeFilters: ActiveFilter[] = [
|
|
45
|
+
...(category ? [{ key: 'category', label: \`Category: \${category}\`, clear: () => setCategory('') }] : []),
|
|
46
|
+
...tags.map(t => ({ key: \`tag-\${t}\`, label: t, clear: () => setTags(prev => prev.filter(v => v !== t)) })),
|
|
47
|
+
...(dateRange ? [{ key: 'date', label: \`\${dateRange.start} – \${dateRange.end}\`, clear: () => setDateRange(undefined) }] : []),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const clearAll = () => {
|
|
51
|
+
setSearch('');
|
|
52
|
+
setCategory('');
|
|
53
|
+
setTags([]);
|
|
54
|
+
setDateRange(undefined);
|
|
55
|
+
setSortBy('newest');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
<Card>
|
|
59
|
+
<Collapsible
|
|
60
|
+
trigger={
|
|
61
|
+
<Row gap="2" align="center">
|
|
62
|
+
<Text as="span" weight="medium">Filters</Text>
|
|
63
|
+
{activeFilters.length > 0 && (
|
|
64
|
+
<Badge variant="accent">{activeFilters.length}</Badge>
|
|
65
|
+
)}
|
|
66
|
+
</Row>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<Stack gap="4">
|
|
70
|
+
<SearchInput
|
|
71
|
+
placeholder="Search items…"
|
|
72
|
+
value={search}
|
|
73
|
+
onChange={setSearch}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<Row gap="3" wrap>
|
|
77
|
+
<Select
|
|
78
|
+
label="Category"
|
|
79
|
+
value={category}
|
|
80
|
+
onChange={setCategory}
|
|
81
|
+
options={[
|
|
82
|
+
{ value: 'design', label: 'Design' },
|
|
83
|
+
{ value: 'engineering', label: 'Engineering' },
|
|
84
|
+
{ value: 'marketing', label: 'Marketing' },
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
<MultiSelect
|
|
88
|
+
label="Tags"
|
|
89
|
+
value={tags}
|
|
90
|
+
onChange={setTags}
|
|
91
|
+
options={[
|
|
92
|
+
{ value: 'react', label: 'React' },
|
|
93
|
+
{ value: 'typescript', label: 'TypeScript' },
|
|
94
|
+
{ value: 'figma', label: 'Figma' },
|
|
95
|
+
{ value: 'node', label: 'Node.js' },
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
<DateRangePicker
|
|
99
|
+
label="Date range"
|
|
100
|
+
value={dateRange}
|
|
101
|
+
onChange={setDateRange}
|
|
102
|
+
/>
|
|
103
|
+
<Select
|
|
104
|
+
label="Sort by"
|
|
105
|
+
value={sortBy}
|
|
106
|
+
onChange={setSortBy}
|
|
107
|
+
options={[
|
|
108
|
+
{ value: 'newest', label: 'Newest first' },
|
|
109
|
+
{ value: 'oldest', label: 'Oldest first' },
|
|
110
|
+
{ value: 'name', label: 'Name A–Z' },
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
</Row>
|
|
114
|
+
|
|
115
|
+
{activeFilters.length > 0 && (
|
|
116
|
+
<Row gap="2" wrap>
|
|
117
|
+
{activeFilters.map(f => (
|
|
118
|
+
<Chip key={f.key} size="sm" onDismiss={f.clear}>
|
|
119
|
+
{f.label}
|
|
120
|
+
</Chip>
|
|
121
|
+
))}
|
|
122
|
+
</Row>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
<Divider />
|
|
126
|
+
|
|
127
|
+
<Row gap="3" justify="end">
|
|
128
|
+
<Button variant="ghost" onClick={clearAll}>Clear all</Button>
|
|
129
|
+
<Button variant="primary" onClick={() => onApply({ search, category, tags, dateRange, sortBy })}>
|
|
130
|
+
Apply filters
|
|
131
|
+
</Button>
|
|
132
|
+
</Row>
|
|
133
|
+
</Stack>
|
|
134
|
+
</Collapsible>
|
|
135
|
+
</Card>`,
|
|
136
|
+
variants: [
|
|
137
|
+
{
|
|
138
|
+
title: 'Vertical stacked — sidebar / drawer layout',
|
|
139
|
+
code: `<Card padding="lg" style={{ width: 320 }}>
|
|
140
|
+
<Stack gap="4">
|
|
141
|
+
<Text size="lg" weight="semibold">Filters</Text>
|
|
142
|
+
|
|
143
|
+
<SearchInput placeholder="Search…" value={search} onChange={setSearch} />
|
|
144
|
+
|
|
145
|
+
<Stack gap="3">
|
|
146
|
+
<Select label="Category" value={category} onChange={setCategory} options={categoryOptions} />
|
|
147
|
+
<MultiSelect label="Tags" value={tags} onChange={setTags} options={tagOptions} />
|
|
148
|
+
<DateRangePicker label="Date range" value={dateRange} onChange={setDateRange} />
|
|
149
|
+
<Select label="Sort by" value={sortBy} onChange={setSortBy} options={sortOptions} />
|
|
150
|
+
</Stack>
|
|
151
|
+
|
|
152
|
+
{activeFilters.length > 0 && (
|
|
153
|
+
<>
|
|
154
|
+
<Divider />
|
|
155
|
+
<Stack gap="2">
|
|
156
|
+
<Row gap="2" align="center" justify="between">
|
|
157
|
+
<Text size="sm" weight="semibold">Active filters</Text>
|
|
158
|
+
<Button variant="ghost" size="xs" onClick={clearAll}>Clear all</Button>
|
|
159
|
+
</Row>
|
|
160
|
+
<Row gap="2" wrap>
|
|
161
|
+
{activeFilters.map(f => (
|
|
162
|
+
<Chip key={f.key} size="sm" onDismiss={f.clear}>{f.label}</Chip>
|
|
163
|
+
))}
|
|
164
|
+
</Row>
|
|
165
|
+
</Stack>
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<Divider />
|
|
170
|
+
|
|
171
|
+
<Button variant="primary" onClick={() => onApply({ search, category, tags, dateRange, sortBy })}>
|
|
172
|
+
Apply filters
|
|
173
|
+
</Button>
|
|
174
|
+
</Stack>
|
|
175
|
+
</Card>`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
designNotes: 'The panel lives inside a Collapsible disclosure so it stays collapsed by default and ' +
|
|
179
|
+
'doesn\'t dominate the page. The trigger shows a "Filters" label plus an active-count Badge ' +
|
|
180
|
+
'so users know filters are applied even when the panel is closed. When expanded, SearchInput ' +
|
|
181
|
+
'spans full width for prominence, filter controls sit in a wrapping Row, and active filter ' +
|
|
182
|
+
'chips appear conditionally with onDismiss to remove individual filters. The Divider separates ' +
|
|
183
|
+
'filter controls from the action row. "Clear all" uses ghost variant to de-emphasize it next ' +
|
|
184
|
+
'to the primary "Apply filters" button, both right-aligned via justify="end". The vertical ' +
|
|
185
|
+
'variant uses a Stack for all filters (ideal for sidebars/drawers at ~320px) with its own ' +
|
|
186
|
+
'"Active filters" label and clear button — no Collapsible needed since the panel is already ' +
|
|
187
|
+
'in a constrained space.',
|
|
188
|
+
};
|