lucent-ui 0.23.0 → 0.25.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.
File without changes
File without changes
File without changes
@@ -86,7 +86,9 @@ export const COMPONENT_MANIFEST = {
86
86
  name: 'style',
87
87
  type: 'object',
88
88
  required: false,
89
- description: 'Inline style overrides. Applied after computed token styles.',
89
+ description: 'Inline style overrides. Applied after computed token styles, so any property you set here wins. ' +
90
+ 'This is the official escape hatch for one-off styling outside the token system — e.g. a custom ' +
91
+ 'color via style={{ color: "var(--my-green)" }}. See docs/style-escape-hatch.md.',
90
92
  },
91
93
  ],
92
94
  usageExamples: [
@@ -96,6 +98,10 @@ export const COMPONENT_MANIFEST = {
96
98
  { title: 'Inline code', code: `<Text as="code" family="mono" size="sm">const x = 1;</Text>` },
97
99
  { title: 'Truncated', code: `<Text truncate style={{ maxWidth: 200 }}>Very long text that will be clipped</Text>` },
98
100
  { title: 'Status color', code: `<Text color="danger" size="xs">This field is required</Text>` },
101
+ {
102
+ title: 'Custom color via style escape hatch',
103
+ code: `<Text style={{ color: 'var(--chart-series-a)' }}>Revenue</Text>`,
104
+ },
99
105
  ],
100
106
  compositionGraph: [],
101
107
  accessibility: {
@@ -102,7 +102,7 @@ export const COMPONENT_MANIFEST = {
102
102
  name: 'radius',
103
103
  type: 'enum',
104
104
  required: false,
105
- default: 'md',
105
+ default: 'lg',
106
106
  description: 'Border radius of the card.',
107
107
  enumValues: ['none', 'sm', 'md', 'lg'],
108
108
  },
@@ -149,8 +149,10 @@ export const COMPONENT_MANIFEST = {
149
149
  name: 'status',
150
150
  type: 'enum',
151
151
  required: false,
152
- description: 'Adds a 3px colored accent bar on the left edge of the card. Uses the corresponding status ' +
153
- 'token (successDefault, warningDefault, dangerDefault, infoDefault). Works with all variants.',
152
+ description: 'Adds a 3px colored inset box-shadow on the left edge of the card. Rendered as an inset shadow ' +
153
+ '(same technique as NavMenu inverse highlight) so it naturally follows the card\'s border-radius. ' +
154
+ 'Uses the corresponding status token (successDefault, warningDefault, dangerDefault, infoDefault). ' +
155
+ 'Works with all variants.',
154
156
  enumValues: ['success', 'warning', 'danger', 'info'],
155
157
  },
156
158
  {
@@ -161,6 +163,15 @@ export const COMPONENT_MANIFEST = {
161
163
  'where cards act as radio/checkbox options. Pairs with onClick for toggle behavior. ' +
162
164
  'Sets aria-pressed on interactive cards. Disabled takes precedence — ring is hidden when disabled.',
163
165
  },
166
+ {
167
+ name: 'hoverable',
168
+ type: 'boolean',
169
+ required: false,
170
+ description: 'Enables hover lift (translateY -1px) and neutral glow shadow without making the card a button or link. ' +
171
+ 'Use when the card contains its own interactive content (e.g. a Collapsible trigger) and the whole card ' +
172
+ 'surface should hint at interactivity. Interactive cards (onClick/href) get accent-colored hover glow; ' +
173
+ 'hoverable-only cards get a neutral glow (12% text-primary).',
174
+ },
164
175
  {
165
176
  name: 'media',
166
177
  type: 'ReactNode',
@@ -259,6 +270,24 @@ export const COMPONENT_MANIFEST = {
259
270
  >
260
271
  <Text weight="semibold">Article title</Text>
261
272
  <Text color="secondary" size="sm">A card with a full-bleed hero image.</Text>
273
+ </Card>`,
274
+ },
275
+ {
276
+ title: 'CollapsibleCard recipe — outline',
277
+ code: `<Card variant="outline" padding="none" hoverable>
278
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Filters</Text>} defaultOpen>
279
+ <Text size="sm" color="secondary">Card + Collapsible composed together.</Text>
280
+ </Collapsible>
281
+ </Card>`,
282
+ },
283
+ {
284
+ title: 'CollapsibleCard recipe — combo (filled + elevated)',
285
+ code: `<Card variant="filled" padding="none" hoverable>
286
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Details</Text>} padded={false}>
287
+ <Card variant="elevated" padding="sm">
288
+ <Text size="sm" color="secondary">Two-tone: flat trigger on filled surface, content in elevated body.</Text>
289
+ </Card>
290
+ </Collapsible>
262
291
  </Card>`,
263
292
  },
264
293
  ],
@@ -4,11 +4,17 @@ export const COMPONENT_MANIFEST = {
4
4
  tier: 'molecule',
5
5
  domain: 'neutral',
6
6
  specVersion: '0.1',
7
- description: 'Animated expand/collapse container with a built-in chevron trigger and smooth height transition.',
7
+ description: 'Animated expand/collapse container with a built-in chevron trigger, smooth height transition, ' +
8
+ 'and CSS-driven hover feedback.',
8
9
  designIntent: 'Collapsible hides secondary content behind a trigger button to reduce visual noise. ' +
9
- 'Height is animated by snapshotting scrollHeight before closing and transitioning to 0, ' +
10
- 'then removing the fixed height after the open transition completes so content can reflow freely. ' +
10
+ 'Height animation uses direct DOM manipulation via useLayoutEffect to avoid React batching issues: ' +
11
+ 'on expand, scrollHeight is snapshotted and transitioned to, then the fixed height is cleared for reflow; ' +
12
+ 'on collapse, the current height is flushed to the DOM, a reflow is forced, then height is set to 0. ' +
13
+ 'Content fades in/out with opacity + translateY(-4px) at 80ms, while height transitions at 180ms ' +
14
+ 'using the easing-default token. ' +
11
15
  'A built-in chevron rotates 180° on open, giving clear directional affordance. ' +
16
+ 'Hover feedback uses a CSS rule via data-lucent-collapsible-trigger (same pattern as NavMenu): ' +
17
+ '5% text-primary tint on the trigger background, chevron darkens to text-primary. ' +
12
18
  'The `trigger` prop accepts only label content — the chevron and button chrome are owned by the component ' +
13
19
  'so callers cannot accidentally break the expand/collapse contract. ' +
14
20
  'The component supports controlled (open + onOpenChange) and uncontrolled (defaultOpen) modes.',
@@ -44,6 +50,22 @@ export const COMPONENT_MANIFEST = {
44
50
  required: false,
45
51
  description: 'Callback fired with the new open boolean when the trigger is clicked.',
46
52
  },
53
+ {
54
+ name: 'disabled',
55
+ type: 'boolean',
56
+ required: false,
57
+ default: 'false',
58
+ description: 'Disables the trigger button. Reduces opacity, sets cursor to not-allowed, and prevents toggling.',
59
+ },
60
+ {
61
+ name: 'padded',
62
+ type: 'boolean',
63
+ required: false,
64
+ default: 'true',
65
+ description: 'When true (default), applies built-in content padding (space-2 top, space-4 sides, space-3 bottom). ' +
66
+ 'Set to false when children provide their own padding — e.g. when nesting a Card inside the Collapsible ' +
67
+ 'for the CollapsibleCard combo recipe.',
68
+ },
47
69
  {
48
70
  name: 'style',
49
71
  type: 'object',
@@ -72,6 +94,24 @@ export const COMPONENT_MANIFEST = {
72
94
  <Text size="sm" color="secondary">This section starts expanded.</Text>
73
95
  </Collapsible>`,
74
96
  },
97
+ {
98
+ title: 'CollapsibleCard recipe',
99
+ code: `<Card variant="outline" padding="none" hoverable>
100
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Filters</Text>} defaultOpen>
101
+ <Text size="sm" color="secondary">Card + Collapsible composed together.</Text>
102
+ </Collapsible>
103
+ </Card>`,
104
+ },
105
+ {
106
+ title: 'CollapsibleCard combo recipe (padded={false})',
107
+ code: `<Card variant="filled" padding="none" hoverable>
108
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Details</Text>} padded={false}>
109
+ <Card variant="elevated" padding="sm">
110
+ <Text size="sm" color="secondary">Nested elevated card inside a filled card.</Text>
111
+ </Card>
112
+ </Collapsible>
113
+ </Card>`,
114
+ },
75
115
  ],
76
116
  compositionGraph: [],
77
117
  accessibility: {
@@ -0,0 +1,256 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'nav-menu',
3
+ name: 'NavMenu',
4
+ tier: 'molecule',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'Multi-level navigation menu supporting vertical (sidebar) and horizontal (top bar) orientations with a sliding highlight pill, CSS-driven hover states, collapsible section groups, and expandable tree items.',
8
+ designIntent:
9
+ // ── Composition pattern (most important for correct usage) ──
10
+ 'CRITICAL COMPOSITION PATTERN — There are two different nesting mechanisms that serve different purposes:\n\n' +
11
+ '1. NavMenu.Sub (tree nesting): Place <NavMenu.Sub> as a DIRECT CHILD of <NavMenu.Item> to make that item ' +
12
+ 'expandable with nested children. NavMenu.Sub is a bare fragment — it accepts ONLY children, no other props ' +
13
+ '(no label, no icon). The parent Item auto-detects the Sub child and renders itself as a collapsible toggle ' +
14
+ 'with expand/collapse behavior. The label, icon, and badge props all go on the parent Item, not on Sub.\n' +
15
+ 'Example: <NavMenu.Item icon={<Gear />}>Settings<NavMenu.Sub><NavMenu.Item>General</NavMenu.Item></NavMenu.Sub></NavMenu.Item>\n\n' +
16
+ '2. NavMenu.Group (section grouping): Wraps multiple sibling items under an uppercase section header. ' +
17
+ 'Groups are for visual organization — they do NOT create parent-child navigation relationships. ' +
18
+ 'A Group with label="Admin" containing Users and Roles items means "these items are in the Admin section", ' +
19
+ 'NOT "Admin is a parent page with Users and Roles as sub-pages". Groups have their own independent collapse.\n' +
20
+ 'Example: <NavMenu.Group label="Admin"><NavMenu.Item>Users</NavMenu.Item><NavMenu.Item>Roles</NavMenu.Item></NavMenu.Group>\n\n' +
21
+ 'WHEN TO USE WHICH:\n' +
22
+ '- Use NavMenu.Sub when an item IS a parent page with child pages (tree hierarchy, e.g. Settings → General, Team, Billing)\n' +
23
+ '- Use NavMenu.Group when items BELONG to a section but are all peers (flat grouping, e.g. "Admin" section containing Users, Roles)\n' +
24
+ '- They can be combined: a Group can contain Items that themselves have Sub children\n\n' +
25
+ // ── Highlight system ──
26
+ 'SLIDING HIGHLIGHT: A single pill follows the active item, driven from the root via DOM measurement. ' +
27
+ 'The root queries for data-active / data-active-parent attributes and positions an always-in-DOM pill ' +
28
+ 'using requestAnimationFrame. MutationObserver + ResizeObserver auto-trigger re-measurement; ' +
29
+ 'aria-hidden ancestry detects collapsed items. Three highlight states: ' +
30
+ '(1) child active — full accent pill, ' +
31
+ '(2) collapsed with active child — lighter pill (12% accent tint) on the parent button, ' +
32
+ '(3) self-active parent — full accent pill on the parent itself (isActive on parent, no active children). ' +
33
+ 'Hover uses a CSS rule on [data-lucent-navitem] with :not() exclusions, rendering a 5% translucent tint. ' +
34
+ 'Inverse mode uses surface background with accent right-border (inset -3px) and elevation shadow. ' +
35
+ 'The hasIcons prop controls left-padding alignment globally for items, group headers, and sub-menu children.',
36
+ props: [
37
+ {
38
+ name: 'orientation',
39
+ type: 'enum',
40
+ required: false,
41
+ default: '"vertical"',
42
+ description: 'Layout direction. Vertical for sidebars, horizontal for top navigation bars.',
43
+ enumValues: ['vertical', 'horizontal'],
44
+ },
45
+ {
46
+ name: 'inverse',
47
+ type: 'boolean',
48
+ required: false,
49
+ default: 'false',
50
+ description: 'Uses surface background with accent right-border and elevation shadow instead of accent fill for the active highlight pill.',
51
+ },
52
+ {
53
+ name: 'size',
54
+ type: 'enum',
55
+ required: false,
56
+ default: '"md"',
57
+ description: 'Size variant controlling font size, padding, gap, and icon width.',
58
+ enumValues: ['sm', 'md', 'lg'],
59
+ },
60
+ {
61
+ name: 'hasIcons',
62
+ type: 'boolean',
63
+ required: false,
64
+ default: 'false',
65
+ description: 'Whether items use icons. Tightens left padding on items and group headers so text aligns with icon positions. ' +
66
+ 'Sub-menu children automatically inherit parent icon awareness via context for consistent text alignment.',
67
+ },
68
+ {
69
+ name: 'aria-label',
70
+ type: 'string',
71
+ required: false,
72
+ default: '"Navigation"',
73
+ description: 'Accessible label for the root <nav> element.',
74
+ },
75
+ {
76
+ name: 'style',
77
+ type: 'object',
78
+ required: false,
79
+ description: 'Inline style overrides for the root <nav> element.',
80
+ },
81
+ ],
82
+ usageExamples: [
83
+ {
84
+ title: 'Expandable parent item with NavMenu.Sub (tree nesting)',
85
+ code: `<NavMenu orientation="vertical">
86
+ <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
87
+ <NavMenu.Item>
88
+ Settings
89
+ <NavMenu.Sub>
90
+ <NavMenu.Item href="/settings/general">General</NavMenu.Item>
91
+ <NavMenu.Item href="/settings/team">Team</NavMenu.Item>
92
+ <NavMenu.Item href="/settings/billing">Billing</NavMenu.Item>
93
+ </NavMenu.Sub>
94
+ </NavMenu.Item>
95
+ <NavMenu.Item href="/help">Help</NavMenu.Item>
96
+ </NavMenu>`,
97
+ description: 'NavMenu.Sub goes INSIDE a NavMenu.Item to make it expandable. The parent Item ("Settings") becomes a ' +
98
+ 'toggle button — clicking it expands/collapses the child items. NavMenu.Sub is a bare fragment that only ' +
99
+ 'accepts children. The label "Settings" and any icon/badge props go on the parent Item, never on Sub.',
100
+ },
101
+ {
102
+ title: 'Section grouping with NavMenu.Group (flat organization)',
103
+ code: `<NavMenu orientation="vertical">
104
+ <NavMenu.Group label="Main">
105
+ <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
106
+ <NavMenu.Item href="/analytics">Analytics</NavMenu.Item>
107
+ </NavMenu.Group>
108
+ <NavMenu.Separator />
109
+ <NavMenu.Group label="Admin" defaultOpen={false}>
110
+ <NavMenu.Item href="/users">Users</NavMenu.Item>
111
+ <NavMenu.Item href="/roles">Roles</NavMenu.Item>
112
+ </NavMenu.Group>
113
+ </NavMenu>`,
114
+ description: 'NavMenu.Group wraps peer items under an uppercase section header. It does NOT create a parent-child ' +
115
+ 'navigation relationship — items inside a Group are all at the same level. Groups have their own ' +
116
+ 'independent collapse via the label header toggle. Use Group for sections, use Sub for tree nesting.',
117
+ },
118
+ {
119
+ title: 'Combined: Groups containing items with Sub children',
120
+ code: `<NavMenu orientation="vertical" hasIcons>
121
+ <NavMenu.Group label="Workspace">
122
+ <NavMenu.Item icon={<FolderIcon />} href="/projects">Projects</NavMenu.Item>
123
+ <NavMenu.Item icon={<GearIcon />}>
124
+ Settings
125
+ <NavMenu.Sub>
126
+ <NavMenu.Item href="/settings/general" isActive>General</NavMenu.Item>
127
+ <NavMenu.Item href="/settings/team">Team</NavMenu.Item>
128
+ </NavMenu.Sub>
129
+ </NavMenu.Item>
130
+ </NavMenu.Group>
131
+ <NavMenu.Separator />
132
+ <NavMenu.Group label="Admin">
133
+ <NavMenu.Item icon={<UserIcon />} href="/users">Users</NavMenu.Item>
134
+ </NavMenu.Group>
135
+ </NavMenu>`,
136
+ description: 'Groups and Sub can be combined. Here the "Workspace" Group contains a "Settings" Item with Sub children. ' +
137
+ 'The Group provides section organization; the Sub provides tree hierarchy within an item.',
138
+ },
139
+ {
140
+ title: 'Self-active parent (section-level page)',
141
+ code: `<NavMenu orientation="vertical" hasIcons>
142
+ <NavMenu.Item isActive icon={<TextIcon />}>
143
+ Text & Labels
144
+ <NavMenu.Sub>
145
+ <NavMenu.Item href="/text">Text</NavMenu.Item>
146
+ <NavMenu.Item href="/badge">Badge</NavMenu.Item>
147
+ <NavMenu.Item href="/chip">Chip</NavMenu.Item>
148
+ </NavMenu.Sub>
149
+ </NavMenu.Item>
150
+ <NavMenu.Item icon={<InputIcon />}>
151
+ Input Fields
152
+ <NavMenu.Sub>
153
+ <NavMenu.Item href="/input" isActive>Input</NavMenu.Item>
154
+ <NavMenu.Item href="/select">Select</NavMenu.Item>
155
+ </NavMenu.Sub>
156
+ </NavMenu.Item>
157
+ </NavMenu>`,
158
+ description: 'Setting isActive on a parent Item (with Sub) while no child has isActive highlights the parent itself ' +
159
+ 'with the full accent pill. The sub-nav expands but no child is individually selected — the parent ' +
160
+ 'represents "all items in this section". Clicking a specific child moves the highlight to that child.',
161
+ },
162
+ {
163
+ title: 'Inverse mode',
164
+ code: `<NavMenu orientation="vertical" inverse>
165
+ <NavMenu.Group label="Main">
166
+ <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
167
+ <NavMenu.Item href="/analytics">Analytics</NavMenu.Item>
168
+ </NavMenu.Group>
169
+ </NavMenu>`,
170
+ description: 'Inverse highlight: surface background with accent right-border and elevation shadow. Text stays text-primary.',
171
+ },
172
+ {
173
+ title: 'Horizontal top navigation with dropdown',
174
+ code: `<NavMenu orientation="horizontal">
175
+ <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
176
+ <NavMenu.Item href="/projects">Projects</NavMenu.Item>
177
+ <NavMenu.Item>
178
+ Settings
179
+ <NavMenu.Sub>
180
+ <NavMenu.Item href="/settings/general">General</NavMenu.Item>
181
+ <NavMenu.Item href="/settings/team">Team</NavMenu.Item>
182
+ </NavMenu.Sub>
183
+ </NavMenu.Item>
184
+ </NavMenu>`,
185
+ description: 'In horizontal orientation, Items with Sub children render as dropdown menus instead of inline expand. ' +
186
+ 'The Sub pattern is the same — only the visual rendering changes based on orientation.',
187
+ },
188
+ {
189
+ title: 'Badges, disabled items, and polymorphic rendering',
190
+ code: `<NavMenu orientation="vertical" hasIcons>
191
+ <NavMenu.Item href="/inbox" icon={<InboxIcon />} badge={<Chip size="sm" variant="accent">3</Chip>}>Inbox</NavMenu.Item>
192
+ <NavMenu.Item as={Link} href="/settings" icon={<GearIcon />} isActive>Settings</NavMenu.Item>
193
+ <NavMenu.Item href="/archive" icon={<ArchiveIcon />} disabled>Archived</NavMenu.Item>
194
+ </NavMenu>`,
195
+ description: 'Items support badge (right-aligned content), disabled state, and polymorphic rendering via the "as" prop ' +
196
+ '(e.g. React Router Link). Disabled items use not-allowed cursor and muted text.',
197
+ },
198
+ ],
199
+ compositionGraph: [
200
+ {
201
+ componentId: 'nav-menu-item',
202
+ componentName: 'NavMenu.Item',
203
+ role: 'Navigation link or expandable parent. When it contains a NavMenu.Sub child, it automatically becomes ' +
204
+ 'a collapsible toggle with expand/collapse behavior — the label, icon, and badge go on the Item. ' +
205
+ 'Props: children, href, isActive, icon, badge, disabled, onClick, as, style.',
206
+ required: true,
207
+ },
208
+ {
209
+ componentId: 'nav-menu-sub',
210
+ componentName: 'NavMenu.Sub',
211
+ role: 'Bare fragment placed as a DIRECT CHILD of NavMenu.Item to make that item expandable with nested children. ' +
212
+ 'Accepts ONLY children — no label, icon, or other props. The parent Item detects it and switches to ' +
213
+ 'parent-toggle rendering. In vertical orientation, children expand inline below the parent; ' +
214
+ 'in horizontal orientation, children render as a dropdown. ' +
215
+ 'Do NOT confuse with NavMenu.Group — Sub creates tree hierarchy, Group creates flat section organization.',
216
+ required: false,
217
+ },
218
+ {
219
+ componentId: 'nav-menu-group',
220
+ componentName: 'NavMenu.Group',
221
+ role: 'Section grouping that wraps peer items under an uppercase header label. Does NOT create parent-child ' +
222
+ 'navigation relationships — items inside are all at the same level. Has its own independent collapse. ' +
223
+ 'Props: children, label, defaultOpen, open, onOpenChange, collapsible, style. ' +
224
+ 'Do NOT confuse with NavMenu.Sub — Group is for flat section headers, Sub is for tree nesting inside an Item.',
225
+ required: false,
226
+ },
227
+ {
228
+ componentId: 'nav-menu-separator',
229
+ componentName: 'NavMenu.Separator',
230
+ role: 'Visual divider between sections. Horizontal line in vertical mode, vertical line in horizontal mode.',
231
+ required: false,
232
+ },
233
+ ],
234
+ accessibility: {
235
+ role: 'navigation',
236
+ ariaAttributes: [
237
+ 'aria-label on root <nav>',
238
+ 'aria-expanded on parent items (Items with Sub children)',
239
+ 'aria-current="page" on active leaf items',
240
+ 'aria-disabled on disabled items',
241
+ 'aria-hidden on collapsed sub-menu content and the highlight pill',
242
+ 'role="separator" on dividers',
243
+ ],
244
+ keyboardInteractions: [
245
+ 'Enter / Space — toggle parent item expand/collapse',
246
+ 'ArrowRight (vertical) / ArrowDown (horizontal) — expand parent item',
247
+ 'ArrowLeft (vertical) / ArrowUp (horizontal) — collapse parent item',
248
+ 'Escape — close open sub-menu',
249
+ ],
250
+ notes: 'Parent items (Items containing NavMenu.Sub) use aria-expanded to communicate open/closed state. ' +
251
+ 'Active leaf items use aria-current="page". Disabled items use aria-disabled with no click handler. ' +
252
+ 'Collapsed sections are marked aria-hidden="true", which the sliding highlight uses to detect ' +
253
+ 'visibility — items inside aria-hidden containers are skipped in favor of the parent fallback. ' +
254
+ 'Separators use role="separator". The highlight pill is aria-hidden.',
255
+ },
256
+ };
@@ -4,12 +4,14 @@ export const COMPONENT_MANIFEST = {
4
4
  tier: 'molecule',
5
5
  domain: 'neutral',
6
6
  specVersion: '0.1',
7
- description: 'A search field with a built-in magnifier icon, clear button, and an optional results dropdown.',
7
+ description: 'A search field with a built-in magnifier icon, clear button, and an optional results dropdown. Use mode="filter" for a simple filter input with a funnel icon and no dropdown.',
8
8
  designIntent: 'SearchInput is intentionally dumb about filtering — the consumer passes already-filtered results ' +
9
9
  'so the component stays stateless and flexible. The clear button appears only when the input has a ' +
10
10
  'value, keeping the right side clean at rest. The results dropdown is rendered absolutely below the ' +
11
11
  'input and closes after a 150ms delay on blur to allow result clicks to register before focus is lost. ' +
12
- 'Spinner replaces the clear button during loading to communicate async state without layout shift.',
12
+ 'Spinner replaces the clear button during loading to communicate async state without layout shift. ' +
13
+ 'Use mode="filter" when you only need a text input with a funnel icon and clear button — no dropdown, ' +
14
+ 'no results array, just a clean filter field.',
13
15
  props: [
14
16
  {
15
17
  name: 'value',
@@ -23,6 +25,14 @@ export const COMPONENT_MANIFEST = {
23
25
  required: true,
24
26
  description: 'Called with the new string value whenever the input changes.',
25
27
  },
28
+ {
29
+ name: 'mode',
30
+ type: 'enum',
31
+ required: false,
32
+ default: 'search',
33
+ description: 'Controls variant. "filter" swaps the magnifier for a funnel icon and disables the results dropdown.',
34
+ enumValues: ['search', 'filter'],
35
+ },
26
36
  {
27
37
  name: 'size',
28
38
  type: 'enum',
@@ -53,8 +63,8 @@ export const COMPONENT_MANIFEST = {
53
63
  name: 'placeholder',
54
64
  type: 'string',
55
65
  required: false,
56
- default: '"Search…"',
57
- description: 'Placeholder text for the input.',
66
+ default: '"Search…" (or "Filter…" in filter mode)',
67
+ description: 'Placeholder text for the input. Defaults to "Search…" in search mode and "Filter…" in filter mode.',
58
68
  },
59
69
  {
60
70
  name: 'results',
@@ -109,6 +119,10 @@ const [results, setResults] = useState([]);
109
119
  onResultSelect={(r) => console.log(r)}
110
120
  />`,
111
121
  },
122
+ {
123
+ title: 'Filter mode (no dropdown)',
124
+ code: `<SearchInput mode="filter" value={filter} onChange={setFilter} placeholder="Filter items…" />`,
125
+ },
112
126
  {
113
127
  title: 'Loading state',
114
128
  code: `<SearchInput value={query} onChange={setQuery} isLoading={isFetching} results={[]} />`,
@@ -0,0 +1,28 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { validateManifest } from './validate.js';
3
+ // Auto-discover all component manifests
4
+ const manifestModules = import.meta.glob('../components/**/*.manifest.ts', { eager: true });
5
+ const manifests = Object.entries(manifestModules).map(([path, mod]) => {
6
+ const m = mod;
7
+ const manifest = m['COMPONENT_MANIFEST'];
8
+ return { path, manifest };
9
+ });
10
+ describe('Component manifests', () => {
11
+ test('at least one manifest was discovered', () => {
12
+ expect(manifests.length).toBeGreaterThan(0);
13
+ });
14
+ for (const { path, manifest } of manifests) {
15
+ const label = path.replace('../components/', '').replace('.manifest.ts', '');
16
+ test(`${label} — exports COMPONENT_MANIFEST`, () => {
17
+ expect(manifest).toBeDefined();
18
+ });
19
+ test(`${label} — passes schema validation`, () => {
20
+ const result = validateManifest(manifest);
21
+ if (!result.valid) {
22
+ const messages = result.errors.map(e => ` ${e.field}: ${e.message}`).join('\n');
23
+ throw new Error(`Invalid manifest:\n${messages}`);
24
+ }
25
+ expect(result.valid).toBe(true);
26
+ });
27
+ }
28
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -25,6 +25,18 @@
25
25
  "dist-server",
26
26
  "dist-cli"
27
27
  ],
28
+ "scripts": {
29
+ "dev": "vite --config vite.dev.config.ts",
30
+ "build": "vite build",
31
+ "build:server": "tsc -p server/tsconfig.json",
32
+ "build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "prepublishOnly": "tsc --noEmit && pnpm build && pnpm build:server && pnpm build:cli",
36
+ "changeset": "changeset",
37
+ "version-packages": "changeset version",
38
+ "release": "pnpm prepublishOnly && changeset publish"
39
+ },
28
40
  "keywords": [
29
41
  "react",
30
42
  "component-library",
@@ -39,6 +51,7 @@
39
51
  },
40
52
  "author": "Rozina Szogyenyi",
41
53
  "license": "MIT",
54
+ "packageManager": "pnpm@10.30.3",
42
55
  "peerDependencies": {
43
56
  "react": "^18.0.0 || ^19.0.0",
44
57
  "react-dom": "^18.0.0 || ^19.0.0"
@@ -60,16 +73,5 @@
60
73
  "dependencies": {
61
74
  "@modelcontextprotocol/sdk": "^1.27.1",
62
75
  "zod": "^4.3.6"
63
- },
64
- "scripts": {
65
- "dev": "vite --config vite.dev.config.ts",
66
- "build": "vite build",
67
- "build:server": "tsc -p server/tsconfig.json",
68
- "build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
69
- "test": "vitest run",
70
- "test:watch": "vitest",
71
- "changeset": "changeset",
72
- "version-packages": "changeset version",
73
- "release": "pnpm prepublishOnly && changeset publish"
74
76
  }
75
- }
77
+ }