lucent-ui 0.29.0 → 0.31.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 +102 -45
- package/dist/index.d.ts +143 -0
- package/dist/index.js +2578 -1885
- package/dist-cli/cli/entry.js +0 -0
- package/dist-cli/cli/index.js +0 -0
- package/dist-server/server/index.js +29 -29
- package/dist-server/server/pattern-registry.js +18 -0
- package/dist-server/server/registry.js +8 -0
- package/dist-server/src/components/molecules/Card/Card.manifest.js +2 -2
- package/dist-server/src/components/molecules/Collapsible/Collapsible.manifest.js +4 -4
- 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/Stepper/Stepper.manifest.js +115 -0
- package/dist-server/src/components/molecules/Timeline/Timeline.manifest.js +23 -9
- package/dist-server/src/manifest/{recipes/action-bar.recipe.js → patterns/action-bar.pattern.js} +1 -1
- package/dist-server/src/manifest/{recipes/collapsible-card.recipe.js → patterns/collapsible-card.pattern.js} +1 -1
- package/dist-server/src/manifest/patterns/dashboard-header.pattern.js +98 -0
- package/dist-server/src/manifest/{recipes/empty-state-card.recipe.js → patterns/empty-state-card.pattern.js} +1 -1
- package/dist-server/src/manifest/{recipes/form-layout.recipe.js → patterns/form-layout.pattern.js} +1 -1
- package/dist-server/src/manifest/patterns/index.js +12 -0
- package/dist-server/src/manifest/patterns/notification-feed.pattern.js +91 -0
- package/dist-server/src/manifest/patterns/onboarding-flow.pattern.js +107 -0
- package/dist-server/src/manifest/patterns/pricing-table.pattern.js +108 -0
- package/dist-server/src/manifest/{recipes/profile-card.recipe.js → patterns/profile-card.pattern.js} +1 -1
- package/dist-server/src/manifest/patterns/search-filter-bar.pattern.js +122 -0
- package/dist-server/src/manifest/{recipes/settings-panel.recipe.js → patterns/settings-panel.pattern.js} +1 -1
- package/dist-server/src/manifest/{recipes/stats-row.recipe.js → patterns/stats-row.pattern.js} +1 -1
- package/package.json +13 -15
- package/dist-server/server/recipe-registry.js +0 -18
- package/dist-server/src/manifest/recipes/index.js +0 -8
- package/dist-server/src/manifest/recipes/search-filter-bar.recipe.js +0 -197
- package/dist-server/src/manifest/validate.test.js +0 -28
package/dist-cli/cli/entry.js
CHANGED
|
File without changes
|
package/dist-cli/cli/index.js
CHANGED
|
File without changes
|
|
@@ -3,7 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ALL_MANIFESTS } from './registry.js';
|
|
6
|
-
import {
|
|
6
|
+
import { ALL_PATTERNS } from './pattern-registry.js';
|
|
7
7
|
import { PALETTES, SHAPES, DENSITIES, SHADOWS, COMBINED, generatePresetConfig } from './presets.js';
|
|
8
8
|
// ─── Auth stub ───────────────────────────────────────────────────────────────
|
|
9
9
|
// LUCENT_API_KEY is reserved for the future paid tier.
|
|
@@ -38,11 +38,11 @@ function scoreManifest(m, query) {
|
|
|
38
38
|
}
|
|
39
39
|
return score;
|
|
40
40
|
}
|
|
41
|
-
function
|
|
41
|
+
function findPattern(nameOrId) {
|
|
42
42
|
const q = nameOrId.trim().toLowerCase();
|
|
43
|
-
return
|
|
43
|
+
return ALL_PATTERNS.find((r) => r.id.toLowerCase() === q || r.name.toLowerCase() === q);
|
|
44
44
|
}
|
|
45
|
-
function
|
|
45
|
+
function scorePattern(r, query) {
|
|
46
46
|
const q = query.toLowerCase();
|
|
47
47
|
let score = 0;
|
|
48
48
|
if (r.name.toLowerCase().includes(q))
|
|
@@ -110,7 +110,7 @@ server.tool('get_component_manifest', 'Returns the full manifest JSON for a Luce
|
|
|
110
110
|
};
|
|
111
111
|
});
|
|
112
112
|
// Tool: search_components
|
|
113
|
-
server.tool('search_components', 'Searches Lucent UI components and composition
|
|
113
|
+
server.tool('search_components', 'Searches Lucent UI components and composition patterns by description or concept. Returns matching components and patterns ranked by relevance.', { query: z.string().describe('Natural language or keyword query, e.g. "loading indicator", "form validation", or "profile card"') }, async ({ query }) => {
|
|
114
114
|
const componentResults = ALL_MANIFESTS
|
|
115
115
|
.map((m) => ({ manifest: m, score: scoreManifest(m, query) }))
|
|
116
116
|
.filter(({ score }) => score > 0)
|
|
@@ -122,41 +122,41 @@ server.tool('search_components', 'Searches Lucent UI components and composition
|
|
|
122
122
|
description: manifest.description,
|
|
123
123
|
score,
|
|
124
124
|
}));
|
|
125
|
-
const
|
|
126
|
-
.map((r) => ({
|
|
125
|
+
const patternResults = ALL_PATTERNS
|
|
126
|
+
.map((r) => ({ pattern: r, score: scorePattern(r, query) }))
|
|
127
127
|
.filter(({ score }) => score > 0)
|
|
128
128
|
.sort((a, b) => b.score - a.score)
|
|
129
|
-
.map(({
|
|
130
|
-
id:
|
|
131
|
-
name:
|
|
132
|
-
category:
|
|
133
|
-
description:
|
|
129
|
+
.map(({ pattern, score }) => ({
|
|
130
|
+
id: pattern.id,
|
|
131
|
+
name: pattern.name,
|
|
132
|
+
category: pattern.category,
|
|
133
|
+
description: pattern.description,
|
|
134
134
|
score,
|
|
135
135
|
}));
|
|
136
136
|
return {
|
|
137
137
|
content: [
|
|
138
138
|
{
|
|
139
139
|
type: 'text',
|
|
140
|
-
text: JSON.stringify({ query, components: componentResults,
|
|
140
|
+
text: JSON.stringify({ query, components: componentResults, patterns: patternResults }, null, 2),
|
|
141
141
|
},
|
|
142
142
|
],
|
|
143
143
|
};
|
|
144
144
|
});
|
|
145
|
-
// Tool:
|
|
146
|
-
server.tool('
|
|
147
|
-
name: z.string().optional().describe('
|
|
148
|
-
category: z.string().optional().describe('
|
|
145
|
+
// Tool: get_composition_pattern
|
|
146
|
+
server.tool('get_composition_pattern', 'Returns a full composition pattern with structure tree, working JSX code, variants, and design notes. Query by pattern name/id or by category to get all patterns in that category.', {
|
|
147
|
+
name: z.string().optional().describe('Pattern name or id, e.g. "Profile Card" or "settings-panel"'),
|
|
148
|
+
category: z.string().optional().describe('Pattern category: "card", "form", "nav", "dashboard", "settings", or "action"'),
|
|
149
149
|
}, async ({ name, category }) => {
|
|
150
150
|
if (name) {
|
|
151
|
-
const
|
|
152
|
-
if (!
|
|
151
|
+
const pattern = findPattern(name);
|
|
152
|
+
if (!pattern) {
|
|
153
153
|
return {
|
|
154
154
|
content: [
|
|
155
155
|
{
|
|
156
156
|
type: 'text',
|
|
157
157
|
text: JSON.stringify({
|
|
158
|
-
error: `
|
|
159
|
-
available:
|
|
158
|
+
error: `Pattern "${name}" not found.`,
|
|
159
|
+
available: ALL_PATTERNS.map((r) => ({ id: r.id, name: r.name, category: r.category })),
|
|
160
160
|
}),
|
|
161
161
|
},
|
|
162
162
|
],
|
|
@@ -167,22 +167,22 @@ server.tool('get_composition_recipe', 'Returns a full composition recipe with st
|
|
|
167
167
|
content: [
|
|
168
168
|
{
|
|
169
169
|
type: 'text',
|
|
170
|
-
text: JSON.stringify(
|
|
170
|
+
text: JSON.stringify(pattern, null, 2),
|
|
171
171
|
},
|
|
172
172
|
],
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
if (category) {
|
|
176
176
|
const cat = category.trim().toLowerCase();
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
177
|
+
const patterns = ALL_PATTERNS.filter((r) => r.category === cat);
|
|
178
|
+
if (patterns.length === 0) {
|
|
179
179
|
return {
|
|
180
180
|
content: [
|
|
181
181
|
{
|
|
182
182
|
type: 'text',
|
|
183
183
|
text: JSON.stringify({
|
|
184
|
-
error: `No
|
|
185
|
-
availableCategories: [...new Set(
|
|
184
|
+
error: `No patterns found in category "${category}".`,
|
|
185
|
+
availableCategories: [...new Set(ALL_PATTERNS.map((r) => r.category))],
|
|
186
186
|
}),
|
|
187
187
|
},
|
|
188
188
|
],
|
|
@@ -193,18 +193,18 @@ server.tool('get_composition_recipe', 'Returns a full composition recipe with st
|
|
|
193
193
|
content: [
|
|
194
194
|
{
|
|
195
195
|
type: 'text',
|
|
196
|
-
text: JSON.stringify({ category: cat,
|
|
196
|
+
text: JSON.stringify({ category: cat, patterns }, null, 2),
|
|
197
197
|
},
|
|
198
198
|
],
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
-
// No filter — return all
|
|
201
|
+
// No filter — return all patterns
|
|
202
202
|
return {
|
|
203
203
|
content: [
|
|
204
204
|
{
|
|
205
205
|
type: 'text',
|
|
206
206
|
text: JSON.stringify({
|
|
207
|
-
|
|
207
|
+
patterns: ALL_PATTERNS.map((r) => ({
|
|
208
208
|
id: r.id,
|
|
209
209
|
name: r.name,
|
|
210
210
|
category: r.category,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PATTERN as ProfileCard } from '../src/manifest/patterns/profile-card.pattern.js';
|
|
2
|
+
import { PATTERN as SettingsPanel } from '../src/manifest/patterns/settings-panel.pattern.js';
|
|
3
|
+
import { PATTERN as StatsRow } from '../src/manifest/patterns/stats-row.pattern.js';
|
|
4
|
+
import { PATTERN as ActionBar } from '../src/manifest/patterns/action-bar.pattern.js';
|
|
5
|
+
import { PATTERN as FormLayout } from '../src/manifest/patterns/form-layout.pattern.js';
|
|
6
|
+
import { PATTERN as EmptyStateCard } from '../src/manifest/patterns/empty-state-card.pattern.js';
|
|
7
|
+
import { PATTERN as CollapsibleCard } from '../src/manifest/patterns/collapsible-card.pattern.js';
|
|
8
|
+
import { PATTERN as SearchFilterBar } from '../src/manifest/patterns/search-filter-bar.pattern.js';
|
|
9
|
+
export const ALL_PATTERNS = [
|
|
10
|
+
ProfileCard,
|
|
11
|
+
SettingsPanel,
|
|
12
|
+
StatsRow,
|
|
13
|
+
ActionBar,
|
|
14
|
+
FormLayout,
|
|
15
|
+
EmptyStateCard,
|
|
16
|
+
CollapsibleCard,
|
|
17
|
+
SearchFilterBar,
|
|
18
|
+
];
|
|
@@ -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
|
];
|
|
@@ -274,7 +274,7 @@ export const COMPONENT_MANIFEST = {
|
|
|
274
274
|
</Card>`,
|
|
275
275
|
},
|
|
276
276
|
{
|
|
277
|
-
title: 'CollapsibleCard
|
|
277
|
+
title: 'CollapsibleCard pattern — outline',
|
|
278
278
|
code: `<Card variant="outline" hoverable>
|
|
279
279
|
<Collapsible trigger={<Text as="span" weight="semibold" size="sm">Filters</Text>} defaultOpen>
|
|
280
280
|
<Text size="sm" color="secondary">Card + Collapsible composed together. Collapsible auto-bleeds to card edges.</Text>
|
|
@@ -282,7 +282,7 @@ export const COMPONENT_MANIFEST = {
|
|
|
282
282
|
</Card>`,
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
|
-
title: 'CollapsibleCard
|
|
285
|
+
title: 'CollapsibleCard pattern — combo (filled + elevated)',
|
|
286
286
|
code: `<Card variant="filled" hoverable>
|
|
287
287
|
<Collapsible trigger={<Text as="span" weight="semibold" size="sm">Details</Text>} padded={false}>
|
|
288
288
|
<Card variant="elevated" padding="sm" style={{ margin: 'var(--lucent-space-1) var(--lucent-space-2) var(--lucent-space-2)' }}>
|
|
@@ -13,7 +13,7 @@ export const COMPONENT_MANIFEST = {
|
|
|
13
13
|
'Content fades in/out with opacity + translateY(-4px) at 80ms, while height transitions at 180ms ' +
|
|
14
14
|
'using the easing-default token. The animated content wrapper uses overflow:hidden only during the ' +
|
|
15
15
|
'height transition and switches to overflow:visible once open, so nested child shadows (e.g. an ' +
|
|
16
|
-
'elevated Card in the combo
|
|
16
|
+
'elevated Card in the combo pattern) are never clipped in the resting state.\n\n' +
|
|
17
17
|
'A built-in chevron rotates 180° on open, giving clear directional affordance. ' +
|
|
18
18
|
'Hover feedback uses a CSS rule via data-lucent-collapsible-trigger (same pattern as NavMenu): ' +
|
|
19
19
|
'5% text-primary tint on the trigger background, chevron darkens to text-primary. ' +
|
|
@@ -70,7 +70,7 @@ export const COMPONENT_MANIFEST = {
|
|
|
70
70
|
default: 'true',
|
|
71
71
|
description: 'When true (default), applies built-in content padding (space-2 top, space-4 sides, space-3 bottom). ' +
|
|
72
72
|
'Set to false when children provide their own padding — e.g. when nesting a Card inside the Collapsible ' +
|
|
73
|
-
'for the CollapsibleCard combo
|
|
73
|
+
'for the CollapsibleCard combo pattern.',
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
name: 'style',
|
|
@@ -101,7 +101,7 @@ export const COMPONENT_MANIFEST = {
|
|
|
101
101
|
</Collapsible>`,
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
|
-
title: 'CollapsibleCard
|
|
104
|
+
title: 'CollapsibleCard pattern (auto-bleed)',
|
|
105
105
|
code: `<Card variant="outline" hoverable>
|
|
106
106
|
<Collapsible trigger={<Text as="span" weight="semibold" size="sm">Filters</Text>} defaultOpen>
|
|
107
107
|
<Text size="sm" color="secondary">Card + Collapsible composed together. No padding="none" needed.</Text>
|
|
@@ -109,7 +109,7 @@ export const COMPONENT_MANIFEST = {
|
|
|
109
109
|
</Card>`,
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
|
-
title: 'CollapsibleCard combo
|
|
112
|
+
title: 'CollapsibleCard combo pattern (padded={false})',
|
|
113
113
|
code: `<Card variant="filled" hoverable>
|
|
114
114
|
<Collapsible trigger={<Text as="span" weight="semibold" size="sm">Details</Text>} padded={false}>
|
|
115
115
|
<Card variant="elevated" padding="sm" style={{ margin: 'var(--lucent-space-1) var(--lucent-space-2) var(--lucent-space-2)' }}>
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'stepper',
|
|
3
|
+
name: 'Stepper',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A step indicator for multi-step flows — onboarding, wizards, checkout. Supports horizontal and vertical orientations with animated transitions.',
|
|
8
|
+
designIntent: 'Use Stepper to visualise progress through a sequence of discrete steps. ' +
|
|
9
|
+
'Steps can be simple strings or objects with label, description, and custom icon. ' +
|
|
10
|
+
'Completed steps show an animated checkmark (spring scale 0→1.2→1), the current step ' +
|
|
11
|
+
'is highlighted with accent, and future steps are subdued. In horizontal mode the connector ' +
|
|
12
|
+
'track runs behind all circles as a continuous bar; the filled portion animates smoothly ' +
|
|
13
|
+
'between steps. First/last labels align left/right; middle labels center under their circles. ' +
|
|
14
|
+
'Enable numbered to show "STEP N" prefixes and showStatus for Completed/In Progress/Pending ' +
|
|
15
|
+
'Chip badges (success/accent/neutral variants). The vertical orientation suits sidebar layouts ' +
|
|
16
|
+
'and forms with longer descriptive steps. ' +
|
|
17
|
+
'Unlike Progress, Stepper is for discrete named stages, not continuous percentages.',
|
|
18
|
+
props: [
|
|
19
|
+
{
|
|
20
|
+
name: 'steps',
|
|
21
|
+
type: 'array',
|
|
22
|
+
required: true,
|
|
23
|
+
description: 'Step definitions. Each element is either a string (used as label) or an object ' +
|
|
24
|
+
'{ label: string, description?: string, icon?: ReactNode }. Custom icons override ' +
|
|
25
|
+
'the default number/checkmark in the circle.',
|
|
26
|
+
},
|
|
27
|
+
{ name: 'current', type: 'number', required: true, description: 'Zero-based index of the active step. Steps before this index show as completed; steps after show as pending.' },
|
|
28
|
+
{ name: 'size', type: 'enum', required: false, default: 'md', description: 'Controls circle diameter (sm=24, md=32, lg=40), checkmark size, connector thickness, and label font size.', enumValues: ['sm', 'md', 'lg'] },
|
|
29
|
+
{ name: 'orientation', type: 'enum', required: false, default: 'horizontal', description: 'Layout direction. Horizontal shows circles in a row with a connector track behind them; vertical stacks them with a connector column on the left.', enumValues: ['horizontal', 'vertical'] },
|
|
30
|
+
{ name: 'numbered', type: 'boolean', required: false, default: 'false', description: 'Show uppercase "STEP N" prefix above each step label.' },
|
|
31
|
+
{ name: 'showStatus', type: 'boolean', required: false, default: 'false', description: 'Show a Chip badge below each label — "Completed" (success), "In Progress" (accent), or "Pending" (neutral).' },
|
|
32
|
+
{ name: 'style', type: 'object', required: false, description: 'Inline style overrides for the root container.' },
|
|
33
|
+
],
|
|
34
|
+
usageExamples: [
|
|
35
|
+
{
|
|
36
|
+
title: 'Basic horizontal',
|
|
37
|
+
code: `<Stepper steps={['Profile', 'Preferences', 'Confirm']} current={1} />`,
|
|
38
|
+
description: 'Minimal stepper — string labels, no extras.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: 'Numbered with status badges',
|
|
42
|
+
code: `<Stepper
|
|
43
|
+
steps={['Basic Details', 'Company Details', 'Subscription', 'Payment']}
|
|
44
|
+
current={2}
|
|
45
|
+
numbered
|
|
46
|
+
showStatus
|
|
47
|
+
/>`,
|
|
48
|
+
description: 'STEP N prefixes and Completed/In Progress/Pending chips.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: 'Large size',
|
|
52
|
+
code: `<Stepper steps={['Cart', 'Shipping', 'Payment']} current={2} size="lg" showStatus />`,
|
|
53
|
+
description: 'Larger circles and text for prominent placement.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: 'Vertical with descriptions',
|
|
57
|
+
code: `<Stepper
|
|
58
|
+
orientation="vertical"
|
|
59
|
+
numbered
|
|
60
|
+
showStatus
|
|
61
|
+
size="lg"
|
|
62
|
+
current={1}
|
|
63
|
+
steps={[
|
|
64
|
+
{ label: 'Basic Details', description: 'Name, email, and contact info' },
|
|
65
|
+
{ label: 'Company Details', description: 'Organization and team size' },
|
|
66
|
+
{ label: 'Subscription Plan', description: 'Choose your plan and billing' },
|
|
67
|
+
{ label: 'Payment Details', description: 'Card information and billing address' },
|
|
68
|
+
]}
|
|
69
|
+
/>`,
|
|
70
|
+
description: 'Vertical layout with step descriptions for sidebar or form flows.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Custom icons',
|
|
74
|
+
code: `<Stepper
|
|
75
|
+
current={1}
|
|
76
|
+
steps={[
|
|
77
|
+
{ label: 'Cart', icon: <CartIcon /> },
|
|
78
|
+
{ label: 'Shipping', icon: <TruckIcon /> },
|
|
79
|
+
{ label: 'Payment', icon: <CreditCardIcon /> },
|
|
80
|
+
]}
|
|
81
|
+
/>`,
|
|
82
|
+
description: 'Custom icon per step overrides the default number/checkmark.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: 'In onboarding flow',
|
|
86
|
+
code: `const [step, setStep] = useState(0);
|
|
87
|
+
|
|
88
|
+
<Card variant="elevated" padding="lg">
|
|
89
|
+
<Stack gap="6">
|
|
90
|
+
<Stepper steps={['Profile', 'Preferences', 'Confirm']} current={step} />
|
|
91
|
+
<Divider />
|
|
92
|
+
{/* Step content */}
|
|
93
|
+
<Row justify="between">
|
|
94
|
+
<Button variant="ghost" size="sm" onClick={() => setStep(s => s - 1)} disabled={step === 0}>Back</Button>
|
|
95
|
+
<Button variant="primary" size="sm" onClick={() => setStep(s => s + 1)}>Continue</Button>
|
|
96
|
+
</Row>
|
|
97
|
+
</Stack>
|
|
98
|
+
</Card>`,
|
|
99
|
+
description: 'Stepper paired with step content and navigation buttons inside a Card.',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
compositionGraph: [
|
|
103
|
+
{ componentId: 'chip', componentName: 'Chip', role: 'Status badge for each step (Completed/In Progress/Pending)', required: false },
|
|
104
|
+
],
|
|
105
|
+
accessibility: {
|
|
106
|
+
role: 'group',
|
|
107
|
+
ariaAttributes: ['aria-label', 'aria-current'],
|
|
108
|
+
keyboardInteractions: [],
|
|
109
|
+
notes: 'Root element has role="group" with aria-label="Progress steps". ' +
|
|
110
|
+
'The current step circle receives aria-current="step" so screen readers ' +
|
|
111
|
+
'can identify which step is active. Step labels are visible text, not aria-only. ' +
|
|
112
|
+
'The checkmark animation uses prefers-reduced-motion: the spring scale is purely ' +
|
|
113
|
+
'decorative and does not affect content comprehension.',
|
|
114
|
+
},
|
|
115
|
+
};
|