lucent-ui 0.24.0 → 0.25.1

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,13 +163,23 @@ 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',
167
178
  required: false,
168
179
  description: 'Full-bleed content rendered at the top of the card (before header). No padding is applied. ' +
169
- 'Use for hero images, illustrations, or any edge-to-edge top content. The card\'s overflow:hidden ' +
170
- 'clips media to the border-radius.',
180
+ 'Use for hero images, illustrations, or any edge-to-edge top content. The media slot self-clips ' +
181
+ 'to the card\'s top border-radius. Cards without media default to overflow:visible so nested child ' +
182
+ 'shadows (e.g. an elevated Card inside a Collapsible) are never cut off.',
171
183
  },
172
184
  ],
173
185
  usageExamples: [
@@ -259,6 +271,24 @@ export const COMPONENT_MANIFEST = {
259
271
  >
260
272
  <Text weight="semibold">Article title</Text>
261
273
  <Text color="secondary" size="sm">A card with a full-bleed hero image.</Text>
274
+ </Card>`,
275
+ },
276
+ {
277
+ title: 'CollapsibleCard recipe — outline',
278
+ code: `<Card variant="outline" hoverable>
279
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Filters</Text>} defaultOpen>
280
+ <Text size="sm" color="secondary">Card + Collapsible composed together. Collapsible auto-bleeds to card edges.</Text>
281
+ </Collapsible>
282
+ </Card>`,
283
+ },
284
+ {
285
+ title: 'CollapsibleCard recipe — combo (filled + elevated)',
286
+ code: `<Card variant="filled" hoverable>
287
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Details</Text>} padded={false}>
288
+ <Card variant="elevated" padding="sm" style={{ margin: 'var(--lucent-space-1) var(--lucent-space-2) var(--lucent-space-2)' }}>
289
+ <Text size="sm" color="secondary">Two-tone: flat trigger on filled surface, content in elevated body.</Text>
290
+ </Card>
291
+ </Collapsible>
262
292
  </Card>`,
263
293
  },
264
294
  ],
@@ -4,14 +4,26 @@ 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. The animated content wrapper uses overflow:hidden only during the ' +
15
+ 'height transition and switches to overflow:visible once open, so nested child shadows (e.g. an ' +
16
+ 'elevated Card in the combo recipe) are never clipped in the resting state.\n\n' +
11
17
  'A built-in chevron rotates 180° on open, giving clear directional affordance. ' +
18
+ 'Hover feedback uses a CSS rule via data-lucent-collapsible-trigger (same pattern as NavMenu): ' +
19
+ '5% text-primary tint on the trigger background, chevron darkens to text-primary. ' +
12
20
  'The `trigger` prop accepts only label content — the chevron and button chrome are owned by the component ' +
13
21
  'so callers cannot accidentally break the expand/collapse contract. ' +
14
- 'The component supports controlled (open + onOpenChange) and uncontrolled (defaultOpen) modes.',
22
+ 'The component supports controlled (open + onOpenChange) and uncontrolled (defaultOpen) modes.\n\n' +
23
+ '**Card-aware auto-bleed** — when placed inside a Card, Collapsible consumes CardPaddingContext and ' +
24
+ 'applies negative margins to cancel the Card body\'s padding, so the trigger spans the full card width ' +
25
+ 'and only the Collapsible\'s own padding applies. This means `<Card hoverable><Collapsible>` just works — ' +
26
+ 'no `padding="none"` on Card required.',
15
27
  props: [
16
28
  {
17
29
  name: 'trigger',
@@ -44,6 +56,22 @@ export const COMPONENT_MANIFEST = {
44
56
  required: false,
45
57
  description: 'Callback fired with the new open boolean when the trigger is clicked.',
46
58
  },
59
+ {
60
+ name: 'disabled',
61
+ type: 'boolean',
62
+ required: false,
63
+ default: 'false',
64
+ description: 'Disables the trigger button. Reduces opacity, sets cursor to not-allowed, and prevents toggling.',
65
+ },
66
+ {
67
+ name: 'padded',
68
+ type: 'boolean',
69
+ required: false,
70
+ default: 'true',
71
+ description: 'When true (default), applies built-in content padding (space-2 top, space-4 sides, space-3 bottom). ' +
72
+ 'Set to false when children provide their own padding — e.g. when nesting a Card inside the Collapsible ' +
73
+ 'for the CollapsibleCard combo recipe.',
74
+ },
47
75
  {
48
76
  name: 'style',
49
77
  type: 'object',
@@ -72,6 +100,24 @@ export const COMPONENT_MANIFEST = {
72
100
  <Text size="sm" color="secondary">This section starts expanded.</Text>
73
101
  </Collapsible>`,
74
102
  },
103
+ {
104
+ title: 'CollapsibleCard recipe (auto-bleed)',
105
+ code: `<Card variant="outline" hoverable>
106
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Filters</Text>} defaultOpen>
107
+ <Text size="sm" color="secondary">Card + Collapsible composed together. No padding="none" needed.</Text>
108
+ </Collapsible>
109
+ </Card>`,
110
+ },
111
+ {
112
+ title: 'CollapsibleCard combo recipe (padded={false})',
113
+ code: `<Card variant="filled" hoverable>
114
+ <Collapsible trigger={<Text as="span" weight="semibold" size="sm">Details</Text>} padded={false}>
115
+ <Card variant="elevated" padding="sm" style={{ margin: 'var(--lucent-space-1) var(--lucent-space-2) var(--lucent-space-2)' }}>
116
+ <Text size="sm" color="secondary">Nested elevated card inside a filled card.</Text>
117
+ </Card>
118
+ </Collapsible>
119
+ </Card>`,
120
+ },
75
121
  ],
76
122
  compositionGraph: [],
77
123
  accessibility: {
@@ -4,28 +4,35 @@ export const COMPONENT_MANIFEST = {
4
4
  tier: 'molecule',
5
5
  domain: 'neutral',
6
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 groups, and nested sub-menus.',
8
- designIntent: 'NavMenu provides hierarchical navigation for sidebar and top-bar layouts. ' +
9
- 'A single sliding highlight pill follows the active item, driven entirely from the root via DOM measurement — ' +
10
- 'the root queries for data-active / data-active-parent attributes and positions an absolutely-placed pill ' +
11
- 'using requestAnimationFrame. MutationObserver and ResizeObserver auto-trigger re-measurement; ' +
12
- 'aria-hidden ancestry is used to detect collapsed items (not offsetHeight), eliminating all timeout-based coordination. ' +
13
- 'Hover states use a CSS rule on [data-lucent-navitem] with :not() exclusions for active, parent-active, hint, and disabled states, ' +
14
- 'rendering a 5% translucent text-primary tint that never conflicts with the accent pill. ' +
15
- 'In vertical orientation, parent items expand inline with smooth height animation (same pattern as Collapsible) ' +
16
- 'and children are indented by depth level. When a parent with an active child is collapsed, the pill slides ' +
17
- 'to the parent button with a lighter visual style (12% accent tint or surface-secondary in inverse mode) ' +
18
- 'so text remains readable without on-accent color. When re-expanded, the pill slides back to the child. ' +
19
- 'Parent items support "self-active" mode: setting isActive on a parent with no active children ' +
20
- 'highlights the parent with the full accent pill, useful for section-level pages that represent all children. ' +
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. ' +
21
34
  'Inverse mode uses surface background with accent right-border (inset -3px) and elevation shadow. ' +
22
- 'The hasIcons prop controls left-padding alignment globally: when true, items use tighter padding (space-2) ' +
23
- 'and group headers align with icon start; sub-menu children inherit parentHasIcon via context so their ' +
24
- 'text aligns with the parent label text regardless of whether they have icons themselves. ' +
25
- 'In horizontal orientation, parent items show dropdown sub-menus on hover/click with enter/exit animation ' +
26
- 'and viewport collision detection. Groups are flattened in horizontal mode. ' +
27
- 'Three sizes (sm/md/lg) scale font, padding, gap, and icon width via a token map. ' +
28
- 'The compound API (NavMenu.Item, NavMenu.Group, NavMenu.Sub, NavMenu.Separator) keeps the tree declarative.',
35
+ 'The hasIcons prop controls left-padding alignment globally for items, group headers, and sub-menu children.',
29
36
  props: [
30
37
  {
31
38
  name: 'orientation',
@@ -74,60 +81,26 @@ export const COMPONENT_MANIFEST = {
74
81
  ],
75
82
  usageExamples: [
76
83
  {
77
- title: 'Vertical sidebar with icons',
78
- code: `<NavMenu orientation="vertical" hasIcons>
79
- <NavMenu.Item icon={<DashIcon />} href="/dashboard" isActive>Dashboard</NavMenu.Item>
80
- <NavMenu.Group label="Workspace">
81
- <NavMenu.Item icon={<FolderIcon />} href="/projects">Projects</NavMenu.Item>
82
- <NavMenu.Item icon={<GearIcon />}>
83
- Settings
84
- <NavMenu.Sub>
85
- <NavMenu.Item href="/settings/general" isActive>General</NavMenu.Item>
86
- <NavMenu.Item href="/settings/team">Team</NavMenu.Item>
87
- </NavMenu.Sub>
88
- </NavMenu.Item>
89
- </NavMenu.Group>
90
- <NavMenu.Separator />
91
- <NavMenu.Item icon={<HelpIcon />} href="/help">Help</NavMenu.Item>
92
- </NavMenu>`,
93
- description: 'Sidebar with icons, groups, and nested sub-menu. hasIcons tightens padding so group headers align with icons ' +
94
- 'and sub-menu children align with parent text.',
95
- },
96
- {
97
- title: 'Vertical sidebar without icons',
84
+ title: 'Expandable parent item with NavMenu.Sub (tree nesting)',
98
85
  code: `<NavMenu orientation="vertical">
99
86
  <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
100
- <NavMenu.Group label="Workspace">
101
- <NavMenu.Item href="/projects">Projects</NavMenu.Item>
102
- <NavMenu.Item>
103
- Settings
104
- <NavMenu.Sub>
105
- <NavMenu.Item href="/settings/general">General</NavMenu.Item>
106
- <NavMenu.Item href="/settings/team">Team</NavMenu.Item>
107
- </NavMenu.Sub>
108
- </NavMenu.Item>
109
- </NavMenu.Group>
110
- </NavMenu>`,
111
- description: 'Without hasIcons, items and group headers use standard padding (space-4) for comfortable text-only layout.',
112
- },
113
- {
114
- title: 'Horizontal top navigation',
115
- code: `<NavMenu orientation="horizontal">
116
- <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
117
- <NavMenu.Item href="/projects">Projects</NavMenu.Item>
118
87
  <NavMenu.Item>
119
88
  Settings
120
89
  <NavMenu.Sub>
121
90
  <NavMenu.Item href="/settings/general">General</NavMenu.Item>
122
91
  <NavMenu.Item href="/settings/team">Team</NavMenu.Item>
92
+ <NavMenu.Item href="/settings/billing">Billing</NavMenu.Item>
123
93
  </NavMenu.Sub>
124
94
  </NavMenu.Item>
95
+ <NavMenu.Item href="/help">Help</NavMenu.Item>
125
96
  </NavMenu>`,
126
- description: 'Top bar layout. Parent items show dropdown sub-menus on hover/click with viewport collision detection.',
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.',
127
100
  },
128
101
  {
129
- title: 'Inverse mode with section groups',
130
- code: `<NavMenu orientation="vertical" inverse>
102
+ title: 'Section grouping with NavMenu.Group (flat organization)',
103
+ code: `<NavMenu orientation="vertical">
131
104
  <NavMenu.Group label="Main">
132
105
  <NavMenu.Item href="/dashboard" isActive>Dashboard</NavMenu.Item>
133
106
  <NavMenu.Item href="/analytics">Analytics</NavMenu.Item>
@@ -138,7 +111,30 @@ export const COMPONENT_MANIFEST = {
138
111
  <NavMenu.Item href="/roles">Roles</NavMenu.Item>
139
112
  </NavMenu.Group>
140
113
  </NavMenu>`,
141
- description: 'Inverse highlight: surface background with accent right-border and elevation shadow. Text stays text-primary.',
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.',
142
138
  },
143
139
  {
144
140
  title: 'Self-active parent (section-level page)',
@@ -159,16 +155,26 @@ export const COMPONENT_MANIFEST = {
159
155
  </NavMenu.Sub>
160
156
  </NavMenu.Item>
161
157
  </NavMenu>`,
162
- description: 'Setting isActive on a parent with no active children gives it the full accent highlight. ' +
163
- 'The sub-nav expands but no child is individually selected — the parent represents all items. ' +
164
- 'Clicking a specific child moves the highlight to that child.',
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.',
165
171
  },
166
172
  {
167
- title: 'Compact sidebar (sm size)',
168
- code: `<NavMenu orientation="vertical" size="sm" hasIcons>
169
- <NavMenu.Item href="/dash" icon={<DashIcon />} isActive>Dashboard</NavMenu.Item>
170
- <NavMenu.Item href="/projects" icon={<FolderIcon />}>Projects</NavMenu.Item>
171
- <NavMenu.Item icon={<GearIcon />}>
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>
172
178
  Settings
173
179
  <NavMenu.Sub>
174
180
  <NavMenu.Item href="/settings/general">General</NavMenu.Item>
@@ -176,43 +182,46 @@ export const COMPONENT_MANIFEST = {
176
182
  </NavMenu.Sub>
177
183
  </NavMenu.Item>
178
184
  </NavMenu>`,
179
- description: 'Small size variant for compact sidebar layouts with tighter padding and smaller font.',
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.',
180
187
  },
181
188
  {
182
- title: 'Badges and disabled items',
189
+ title: 'Badges, disabled items, and polymorphic rendering',
183
190
  code: `<NavMenu orientation="vertical" hasIcons>
184
191
  <NavMenu.Item href="/inbox" icon={<InboxIcon />} badge={<Chip size="sm" variant="accent">3</Chip>}>Inbox</NavMenu.Item>
185
- <NavMenu.Item href="/settings" icon={<GearIcon />} isActive>Settings</NavMenu.Item>
192
+ <NavMenu.Item as={Link} href="/settings" icon={<GearIcon />} isActive>Settings</NavMenu.Item>
186
193
  <NavMenu.Item href="/archive" icon={<ArchiveIcon />} disabled>Archived</NavMenu.Item>
187
194
  </NavMenu>`,
188
- description: 'Items with badge counts and disabled state. Disabled items use not-allowed cursor and muted text.',
189
- },
190
- {
191
- title: 'Polymorphic items with React Router',
192
- code: `<NavMenu orientation="vertical">
193
- <NavMenu.Item as={Link} href="/dashboard" isActive>Dashboard</NavMenu.Item>
194
- <NavMenu.Item as={Link} href="/projects">Projects</NavMenu.Item>
195
- </NavMenu>`,
196
- description: 'Using the "as" prop to render items as React Router Link components instead of anchor tags.',
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
197
  },
198
198
  ],
199
199
  compositionGraph: [
200
200
  {
201
201
  componentId: 'nav-menu-item',
202
202
  componentName: 'NavMenu.Item',
203
- role: 'Individual navigation link or parent toggle. Sets data-active when self-active, data-active-parent when collapsed with an active child. Uses data-lucent-navitem for CSS hover targeting.',
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.',
204
206
  required: true,
205
207
  },
206
208
  {
207
209
  componentId: 'nav-menu-sub',
208
210
  componentName: 'NavMenu.Sub',
209
- role: 'Marker wrapper for nested sub-menu children inside a parent NavMenu.Item.',
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.',
210
216
  required: false,
211
217
  },
212
218
  {
213
219
  componentId: 'nav-menu-group',
214
220
  componentName: 'NavMenu.Group',
215
- role: 'Section grouping with optional uppercase label header and independent collapse. Header left padding responds to hasIcons context.',
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.',
216
225
  required: false,
217
226
  },
218
227
  {
@@ -226,7 +235,7 @@ export const COMPONENT_MANIFEST = {
226
235
  role: 'navigation',
227
236
  ariaAttributes: [
228
237
  'aria-label on root <nav>',
229
- 'aria-expanded on parent items with sub-menus',
238
+ 'aria-expanded on parent items (Items with Sub children)',
230
239
  'aria-current="page" on active leaf items',
231
240
  'aria-disabled on disabled items',
232
241
  'aria-hidden on collapsed sub-menu content and the highlight pill',
@@ -238,7 +247,7 @@ export const COMPONENT_MANIFEST = {
238
247
  'ArrowLeft (vertical) / ArrowUp (horizontal) — collapse parent item',
239
248
  'Escape — close open sub-menu',
240
249
  ],
241
- notes: 'Parent items use aria-expanded to communicate open/closed state. ' +
250
+ notes: 'Parent items (Items containing NavMenu.Sub) use aria-expanded to communicate open/closed state. ' +
242
251
  'Active leaf items use aria-current="page". Disabled items use aria-disabled with no click handler. ' +
243
252
  'Collapsed sections are marked aria-hidden="true", which the sliding highlight uses to detect ' +
244
253
  'visibility — items inside aria-hidden containers are skipped in favor of the parent fallback. ' +
@@ -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.24.0",
3
+ "version": "0.25.1",
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
+ }