lucent-ui 0.16.0 → 0.18.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.
@@ -0,0 +1,272 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'menu',
3
+ name: 'Menu',
4
+ tier: 'molecule',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A dropdown menu triggered by clicking a host element. Renders in a portal to avoid overflow clipping, ' +
8
+ 'supports placement with auto-flip, keyboard navigation (arrow keys, Enter, Escape), and outside-click dismissal.',
9
+ designIntent: 'Menu provides a contextual action list that appears from a trigger element. It is a foundational ' +
10
+ 'overlay primitive — Select dropdowns, notification feeds, and context menus can all be composed from ' +
11
+ 'this building block.\n\n' +
12
+ '## Compound component API\n' +
13
+ 'Menu uses a compound component pattern (`Menu`, `MenuItem`, `MenuSeparator`, `MenuGroup`) rather than ' +
14
+ 'a flat data array. Menu items have divergent structures — actions, separators, groups, icons, shortcut ' +
15
+ 'hints, danger state — that compose naturally as JSX children rather than discriminated union objects.\n\n' +
16
+ '## Sizing\n' +
17
+ 'The `size` prop (`sm` | `md` | `lg`) flows from the root `Menu` through context to all sub-components. ' +
18
+ 'Font sizes are aligned with Button: sm → `font-size-sm`, md → `font-size-md`, lg → `font-size-lg`. ' +
19
+ 'Item padding and gap use `space-2` for sm/md and `space-3` for lg, matching MultiSelect dropdown spacing. ' +
20
+ 'Group labels stay one step smaller than item text. Checkmark icons scale from 12px to 16px.\n\n' +
21
+ '## Placement & auto-flip\n' +
22
+ 'The `placement` prop sets the preferred position (default `bottom-start`). When the popover would ' +
23
+ 'overflow the viewport, it automatically flips to the opposite side. Horizontal alignment (`-start`, ' +
24
+ '`-end`, or centered) is preserved during the flip. Position is computed with `getBoundingClientRect` ' +
25
+ 'on mount and rendered via `position: fixed` in a portal.\n\n' +
26
+ '## Portal rendering\n' +
27
+ 'The popover is portaled to `document.body` via `createPortal`. This prevents overflow clipping from ' +
28
+ 'parent containers with `overflow: hidden`. The trigger wrapper stays inline in the DOM tree so it ' +
29
+ 'participates in layout normally.\n\n' +
30
+ '## Keyboard navigation\n' +
31
+ 'Follows WAI-ARIA Menu Button pattern. Arrow keys cycle through enabled items (wrapping). Enter/Space ' +
32
+ 'selects the active item and closes the menu. Escape closes without selection. Tab closes and lets ' +
33
+ 'focus move naturally. Home/End jump to first/last enabled item.\n\n' +
34
+ '## Dismissal\n' +
35
+ 'Menus close on outside click (mousedown, deferred via `requestAnimationFrame` to avoid catching the ' +
36
+ 'opening click), Escape, Tab, and scroll (armed after 50ms to skip mount-triggered scroll events). ' +
37
+ 'After close, focus returns to the trigger element.\n\n' +
38
+ '## Selected state\n' +
39
+ 'MenuItem accepts a `selected` prop that renders a trailing accent-colored checkmark. The selected item ' +
40
+ 'gets a `color-mix(in srgb, accent-default 12%, surface-overlay)` background with `shadow-sm` elevation, ' +
41
+ 'making it visually stronger than the hover state (`surface-secondary`). Uses `role="menuitemcheckbox"` ' +
42
+ 'with `aria-checked` for accessibility.\n\n' +
43
+ '## Animation\n' +
44
+ 'Both entrance and exit use a subtle scale + fade (`scale(0.97) ↔ 1`, `opacity 0 ↔ 1`) over 120ms. ' +
45
+ 'Entrance uses `easing-decelerate`, exit uses `easing-default`. `transform-origin` is set based on the ' +
46
+ 'actual placement (after auto-flip). The portal stays mounted during the exit animation via a `visible` ' +
47
+ 'state with `pointerEvents: none` to prevent interaction while fading out.',
48
+ props: [
49
+ {
50
+ name: 'trigger',
51
+ type: 'ReactNode',
52
+ required: true,
53
+ description: 'The element that toggles the menu on click. Typically a Button with a chevron. ' +
54
+ 'Wrapped in a <span> that receives click, keyboard, and ARIA attributes.',
55
+ },
56
+ {
57
+ name: 'children',
58
+ type: 'ReactNode',
59
+ required: true,
60
+ description: 'MenuItem, MenuSeparator, and/or MenuGroup elements.',
61
+ },
62
+ {
63
+ name: 'size',
64
+ type: 'enum',
65
+ required: false,
66
+ default: 'md',
67
+ description: 'Size of the menu panel. Controls item padding, gap, font size, and checkmark icon size. ' +
68
+ 'Font sizes match Button at each tier: sm → font-size-sm, md → font-size-md, lg → font-size-lg.',
69
+ enumValues: ['sm', 'md', 'lg'],
70
+ },
71
+ {
72
+ name: 'placement',
73
+ type: 'enum',
74
+ required: false,
75
+ default: 'bottom-start',
76
+ description: 'Preferred placement relative to the trigger. Auto-flips when near viewport edges.',
77
+ enumValues: ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'right'],
78
+ },
79
+ {
80
+ name: 'open',
81
+ type: 'boolean',
82
+ required: false,
83
+ description: 'Controlled open state. When provided, the menu becomes controlled.',
84
+ },
85
+ {
86
+ name: 'onOpenChange',
87
+ type: 'function',
88
+ required: false,
89
+ description: 'Callback fired when the menu opens or closes. Receives the new open state.',
90
+ },
91
+ {
92
+ name: 'style',
93
+ type: 'object',
94
+ required: false,
95
+ description: 'Inline style overrides for the popover panel.',
96
+ },
97
+ // MenuItem props
98
+ {
99
+ name: 'MenuItem.onSelect',
100
+ type: 'function',
101
+ required: true,
102
+ description: 'Fires when the item is selected via click or Enter/Space.',
103
+ },
104
+ {
105
+ name: 'MenuItem.children',
106
+ type: 'ReactNode',
107
+ required: true,
108
+ description: 'Label content. Rendered via the Text atom at the size determined by the parent Menu.',
109
+ },
110
+ {
111
+ name: 'MenuItem.icon',
112
+ type: 'ReactNode',
113
+ required: false,
114
+ description: 'Leading icon rendered before the label.',
115
+ },
116
+ {
117
+ name: 'MenuItem.shortcut',
118
+ type: 'string',
119
+ required: false,
120
+ description: 'Trailing shortcut hint (e.g. "Cmd+D"). Displayed in secondary text.',
121
+ },
122
+ {
123
+ name: 'MenuItem.disabled',
124
+ type: 'boolean',
125
+ required: false,
126
+ default: 'false',
127
+ description: 'Disables selection, grays out the item, and skips it during keyboard navigation.',
128
+ },
129
+ {
130
+ name: 'MenuItem.danger',
131
+ type: 'boolean',
132
+ required: false,
133
+ default: 'false',
134
+ description: 'Renders the item in danger color for destructive actions.',
135
+ },
136
+ {
137
+ name: 'MenuItem.selected',
138
+ type: 'boolean',
139
+ required: false,
140
+ default: 'false',
141
+ description: 'Marks the item as currently selected. Shows a trailing checkmark in accent color, ' +
142
+ 'accent-tinted background via color-mix, and shadow-sm elevation. Visually stronger than hover.',
143
+ },
144
+ {
145
+ name: 'MenuItem.style',
146
+ type: 'object',
147
+ required: false,
148
+ description: 'Inline style overrides for the item.',
149
+ },
150
+ // MenuSeparator props
151
+ {
152
+ name: 'MenuSeparator.style',
153
+ type: 'object',
154
+ required: false,
155
+ description: 'Inline style overrides for the separator line.',
156
+ },
157
+ // MenuGroup props
158
+ {
159
+ name: 'MenuGroup.label',
160
+ type: 'string',
161
+ required: true,
162
+ description: 'Label shown above the group. Rendered one font step smaller than item text.',
163
+ },
164
+ {
165
+ name: 'MenuGroup.children',
166
+ type: 'ReactNode',
167
+ required: true,
168
+ description: 'MenuItem elements within the group.',
169
+ },
170
+ {
171
+ name: 'MenuGroup.style',
172
+ type: 'object',
173
+ required: false,
174
+ description: 'Inline style overrides for the group wrapper.',
175
+ },
176
+ ],
177
+ usageExamples: [
178
+ {
179
+ title: 'Basic menu',
180
+ code: `<Menu trigger={<Button chevron>Actions</Button>}>
181
+ <MenuItem onSelect={() => console.log('edit')}>Edit</MenuItem>
182
+ <MenuItem onSelect={() => console.log('duplicate')}>Duplicate</MenuItem>
183
+ <MenuSeparator />
184
+ <MenuItem onSelect={() => console.log('delete')} danger>Delete</MenuItem>
185
+ </Menu>`,
186
+ },
187
+ {
188
+ title: 'With icons and shortcuts',
189
+ code: `<Menu trigger={<Button variant="outline" chevron>Options</Button>}>
190
+ <MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
191
+ <MenuItem icon={<CopyIcon />} shortcut="⌘D" onSelect={duplicate}>Duplicate</MenuItem>
192
+ <MenuSeparator />
193
+ <MenuItem icon={<TrashIcon />} onSelect={remove} danger>Delete</MenuItem>
194
+ </Menu>`,
195
+ },
196
+ {
197
+ title: 'With groups',
198
+ code: `<Menu trigger={<Button chevron>File</Button>} placement="bottom-start">
199
+ <MenuGroup label="Document">
200
+ <MenuItem onSelect={newDoc}>New</MenuItem>
201
+ <MenuItem onSelect={openDoc}>Open</MenuItem>
202
+ </MenuGroup>
203
+ <MenuSeparator />
204
+ <MenuGroup label="Export">
205
+ <MenuItem onSelect={exportPdf}>PDF</MenuItem>
206
+ <MenuItem onSelect={exportCsv}>CSV</MenuItem>
207
+ </MenuGroup>
208
+ </Menu>`,
209
+ },
210
+ {
211
+ title: 'Selected items (e.g. sort order)',
212
+ code: `const [sort, setSort] = useState('name');
213
+ <Menu trigger={<Button variant="outline" chevron>Sort by</Button>}>
214
+ <MenuItem selected={sort === 'name'} onSelect={() => setSort('name')}>Name</MenuItem>
215
+ <MenuItem selected={sort === 'date'} onSelect={() => setSort('date')}>Date modified</MenuItem>
216
+ <MenuItem selected={sort === 'size'} onSelect={() => setSort('size')}>Size</MenuItem>
217
+ </Menu>`,
218
+ },
219
+ {
220
+ title: 'Size variants',
221
+ code: `<Menu size="sm" trigger={<Button size="sm" chevron>Small</Button>}>
222
+ <MenuItem onSelect={() => {}}>Option A</MenuItem>
223
+ <MenuItem onSelect={() => {}}>Option B</MenuItem>
224
+ </Menu>
225
+
226
+ <Menu size="lg" trigger={<Button size="lg" chevron>Large</Button>}>
227
+ <MenuItem onSelect={() => {}}>Option A</MenuItem>
228
+ <MenuItem onSelect={() => {}}>Option B</MenuItem>
229
+ </Menu>`,
230
+ },
231
+ {
232
+ title: 'Controlled open state',
233
+ code: `const [open, setOpen] = useState(false);
234
+ <Menu trigger={<Button>Menu</Button>} open={open} onOpenChange={setOpen}>
235
+ <MenuItem onSelect={() => {}}>Option A</MenuItem>
236
+ <MenuItem onSelect={() => {}}>Option B</MenuItem>
237
+ </Menu>`,
238
+ },
239
+ ],
240
+ compositionGraph: [
241
+ { componentId: 'text', componentName: 'Text', role: 'Item labels and group headers', required: true },
242
+ ],
243
+ accessibility: {
244
+ role: 'menu',
245
+ ariaAttributes: [
246
+ 'role="menu" on the popover panel',
247
+ 'role="menuitemcheckbox" with aria-checked on each MenuItem',
248
+ 'role="separator" on MenuSeparator',
249
+ 'role="group" with aria-label on MenuGroup',
250
+ 'aria-haspopup="menu" on the trigger wrapper',
251
+ 'aria-expanded on the trigger wrapper',
252
+ 'aria-controls linking trigger to popover via id',
253
+ 'aria-disabled on disabled MenuItems',
254
+ ],
255
+ keyboardInteractions: [
256
+ 'Enter/Space on trigger — toggle menu',
257
+ 'ArrowDown on trigger — open menu, focus first item',
258
+ 'ArrowUp on trigger — open menu, focus last item',
259
+ 'ArrowDown — focus next enabled item (wraps)',
260
+ 'ArrowUp — focus previous enabled item (wraps)',
261
+ 'Enter/Space — select focused item, close menu',
262
+ 'Escape — close menu, return focus to trigger',
263
+ 'Tab — close menu, let focus move naturally',
264
+ 'Home — focus first enabled item',
265
+ 'End — focus last enabled item',
266
+ ],
267
+ notes: 'Focus returns to the trigger element after the menu is dismissed via Escape or selection. ' +
268
+ 'Disabled items are skipped during keyboard navigation and have aria-disabled. ' +
269
+ 'Selected items use role="menuitemcheckbox" with aria-checked for screen reader announcement. ' +
270
+ 'The popover is portaled to document.body but remains semantically linked to the trigger via aria-controls.',
271
+ },
272
+ };
@@ -8,24 +8,38 @@ export const ButtonManifest = {
8
8
  designIntent: 'Buttons communicate available actions. Variant conveys hierarchy: use "primary" for the ' +
9
9
  'single most important action in a view, "secondary" for supporting actions, "ghost" for ' +
10
10
  'low-emphasis actions in dense UIs, "outline" for bordered buttons with no fill, and "danger" exclusively for destructive or irreversible ' +
11
- 'operations. Size should match surrounding content density prefer "md" as the default, ' +
12
- '"sm" for toolbars or tables, and "xs" for compact UIs like customizer panels.',
11
+ 'operations. Use "danger-ghost" for low-emphasis destructive actions (red text, no fill) and ' +
12
+ '"danger-outline" for bordered destructive buttons. Size should match surrounding content density — prefer "md" as the default, ' +
13
+ '"sm" for toolbars or tables, "xs" for compact UIs like customizer panels, and "2xs" for ' +
14
+ 'ultra-dense inline controls (~22px height) such as table-inline actions or toolbar icon triggers.',
13
15
  props: [
14
16
  {
15
17
  name: 'variant',
16
18
  type: 'enum',
17
19
  required: false,
18
20
  default: 'primary',
19
- description: 'Visual style conveying action hierarchy.',
20
- enumValues: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
21
+ description: 'Visual style conveying action hierarchy. ' +
22
+ '"primary" filled accent for the single most important action. ' +
23
+ '"secondary" — subtle accent-tinted fill for supporting actions. ' +
24
+ '"outline" — bordered with no fill, for neutral secondary actions. ' +
25
+ '"ghost" — transparent with no border, for low-emphasis or inline actions. ' +
26
+ '"danger" — filled red for irreversible destructive actions (e.g. "Delete account"). ' +
27
+ '"danger-outline" — red border + red text for destructive actions that need visual weight without a filled background. ' +
28
+ '"danger-ghost" — red text only, for low-emphasis destructive actions (e.g. "Remove" in a list row).',
29
+ enumValues: ['primary', 'secondary', 'outline', 'ghost', 'danger', 'danger-outline', 'danger-ghost'],
21
30
  },
22
31
  {
23
32
  name: 'size',
24
33
  type: 'enum',
25
34
  required: false,
26
35
  default: 'md',
27
- description: 'Controls height and padding.',
28
- enumValues: ['xs', 'sm', 'md', 'lg'],
36
+ description: 'Controls height and padding. ' +
37
+ '"lg" (48px) — hero sections, onboarding flows. ' +
38
+ '"md" (42px) — default for most forms and dialogs. ' +
39
+ '"sm" (34px) — toolbars, table headers, card actions. ' +
40
+ '"xs" (26px) — compact UIs like customizer panels, inline controls. ' +
41
+ '"2xs" (22px) — ultra-dense inline icon triggers, table-row actions, dashboard toolbar buttons.',
42
+ enumValues: ['2xs', 'xs', 'sm', 'md', 'lg'],
29
43
  },
30
44
  {
31
45
  name: 'children',
@@ -135,6 +149,18 @@ export const ButtonManifest = {
135
149
  title: 'Dropdown trigger',
136
150
  code: `<Button variant="outline" chevron>Options</Button>`,
137
151
  },
152
+ {
153
+ title: 'Bordered destructive action',
154
+ code: `<Button variant="danger-outline" onClick={handleRevoke}>Revoke access</Button>`,
155
+ },
156
+ {
157
+ title: 'Low-emphasis destructive action',
158
+ code: `<Button variant="danger-ghost" onClick={handleRemove}>Remove</Button>`,
159
+ },
160
+ {
161
+ title: 'Dense inline action',
162
+ code: `<Button variant="ghost" size="2xs" leftIcon={<RefreshIcon />}>Retry</Button>`,
163
+ },
138
164
  ],
139
165
  compositionGraph: [],
140
166
  accessibility: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -47,7 +47,7 @@
47
47
  "homepage": "https://lucentui.dev",
48
48
  "repository": {
49
49
  "type": "git",
50
- "url": "https://github.com/rozinashopify/lucent-ui"
50
+ "url": "https://github.com/rozina-hudson/lucent-ui"
51
51
  },
52
52
  "author": "Rozina Szogyenyi",
53
53
  "license": "MIT",