@thanh-libs/menu 0.0.2 → 0.0.3

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.
Files changed (2) hide show
  1. package/README.md +612 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,612 @@
1
+ # @thanh-libs/menu
2
+
3
+ Persistent navigation menu for React — compound-component API with **inline collapsible** and **floating popover** sub-menus, keyboard navigation, typeahead search, customisable color schemes, and active-state indicators. Built with [Emotion](https://emotion.sh/) and [Floating UI](https://floating-ui.com/).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @thanh-libs/menu
9
+ ```
10
+
11
+ ### Peer dependencies
12
+
13
+ ```bash
14
+ npm install react react-dom @emotion/react @emotion/styled @floating-ui/react @thanh-libs/theme @thanh-libs/dialog
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Quick Start
20
+
21
+ ```tsx
22
+ import {
23
+ Menu, MenuItem, MenuLabel, MenuDivider,
24
+ MenuGroup, MenuSub, MenuSubTrigger, MenuSubContent,
25
+ } from '@thanh-libs/menu';
26
+
27
+ function Sidebar() {
28
+ const [active, setActive] = useState('home');
29
+
30
+ return (
31
+ <Menu style={{ width: 260 }}>
32
+ <MenuItem
33
+ icon={<HomeIcon />}
34
+ selected={active === 'home'}
35
+ onClick={() => setActive('home')}
36
+ >
37
+ Home
38
+ </MenuItem>
39
+
40
+ <MenuSub>
41
+ <MenuSubTrigger icon={<SettingsIcon />}>Settings</MenuSubTrigger>
42
+ <MenuSubContent>
43
+ <MenuItem
44
+ selected={active === 'general'}
45
+ onClick={() => setActive('general')}
46
+ >
47
+ General
48
+ </MenuItem>
49
+ <MenuItem
50
+ selected={active === 'security'}
51
+ onClick={() => setActive('security')}
52
+ >
53
+ Security
54
+ </MenuItem>
55
+ </MenuSubContent>
56
+ </MenuSub>
57
+
58
+ <MenuDivider />
59
+ <MenuItem danger>Logout</MenuItem>
60
+ </Menu>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Components
68
+
69
+ ### Menu
70
+
71
+ Root container. Provides context (mode, density, color scheme, …) to all descendants. Always rendered — this is a **persistent visible list**, not a dropdown/popover.
72
+
73
+ ```tsx
74
+ <Menu dense mode="inline" display="default" style={{ width: 260 }}>
75
+ {/* MenuItem, MenuSub, MenuDivider, MenuGroup, MenuLabel … */}
76
+ </Menu>
77
+ ```
78
+
79
+ #### Props
80
+
81
+ | Prop | Type | Default | Description |
82
+ |------|------|---------|-------------|
83
+ | `children` | `ReactNode` | **(required)** | Menu items and sub-components |
84
+ | `dense` | `boolean` | `false` | Compact mode — smaller padding and font size |
85
+ | `maxHeight` | `number \| string` | — | Fixed max height with overflow scroll |
86
+ | `mode` | `MenuSubMode` | `'inline'` | Default sub-menu mode for all nested `MenuSub` |
87
+ | `display` | `MenuDisplay` | `'default'` | `'icon'` shows only icons (mini-sidebar) |
88
+ | `trigger` | `MenuSubTriggerType` | `'hover'` | Default popover trigger type (`'hover'` or `'click'`) |
89
+ | `floatingSettings` | `MenuFloatingSettings` | — | Global Floating UI settings for popover sub-menus |
90
+ | `colorScheme` | `MenuColorScheme` | — | Custom color scheme (raw CSS color strings) |
91
+ | `activeIndicator` | `MenuActiveIndicator` | `'dot'` | Active indicator on selected items (see below) |
92
+ | `showDot` | `boolean` | `false` | Show inline dot bullets on child items inside `MenuSubContent` |
93
+
94
+ Also accepts all native `<div>` HTML attributes (`className`, `style`, `id`, `onKeyDown`, etc.).
95
+
96
+ #### Active Indicator
97
+
98
+ Controls the visual marker beside selected items:
99
+
100
+ | Value | Description |
101
+ |-------|-------------|
102
+ | `'dot'` (default) | Small filled circle beside the selected item |
103
+ | `'bar'` | Vertical bar beside the selected item |
104
+ | `false` | No indicator |
105
+ | `ReactNode` | Custom indicator element |
106
+
107
+ ```tsx
108
+ <Menu activeIndicator="bar">…</Menu>
109
+ <Menu activeIndicator={false}>…</Menu>
110
+ <Menu activeIndicator={<CustomIcon />}>…</Menu>
111
+ ```
112
+
113
+ ---
114
+
115
+ ### MenuItem
116
+
117
+ Clickable action or navigation item. Supports icon, shortcut, disabled, danger, and selected states.
118
+
119
+ ```tsx
120
+ <MenuItem
121
+ icon={<UserIcon />}
122
+ shortcut="⌘K"
123
+ selected
124
+ onClick={() => navigate('/users')}
125
+ >
126
+ Users
127
+ </MenuItem>
128
+ ```
129
+
130
+ #### Props
131
+
132
+ | Prop | Type | Default | Description |
133
+ |------|------|---------|-------------|
134
+ | `children` | `ReactNode` | **(required)** | Label content |
135
+ | `icon` | `ReactNode` | — | Leading icon (20×20 container, SVGs auto-fill) |
136
+ | `shortcut` | `ReactNode` | — | Trailing keyboard shortcut text |
137
+ | `disabled` | `boolean` | `false` | Disabled state — no click, muted color |
138
+ | `danger` | `boolean` | `false` | Destructive/danger styling (red) |
139
+ | `selected` | `boolean` | `false` | Selected/active state — bold text + background highlight |
140
+ | `onClick` | `() => void` | — | Click handler (blocked when disabled) |
141
+
142
+ Also accepts all native `<div>` HTML attributes except `onClick` (redefined as `() => void`).
143
+
144
+ **Auto-expand behaviour**: When `selected={true}` and the item is inside a `MenuSub`, the parent sub-menu (and all ancestors) automatically expand to reveal the active item on mount.
145
+
146
+ ---
147
+
148
+ ### MenuLabel
149
+
150
+ Non-interactive group heading text.
151
+
152
+ ```tsx
153
+ <MenuLabel>Navigation</MenuLabel>
154
+ ```
155
+
156
+ #### Props
157
+
158
+ | Prop | Type | Default | Description |
159
+ |------|------|---------|-------------|
160
+ | `children` | `ReactNode` | **(required)** | Label text |
161
+ | `className` | `string` | — | Additional CSS class |
162
+ | `style` | `CSSProperties` | — | Inline styles |
163
+ | `id` | `string` | — | Element ID |
164
+
165
+ ---
166
+
167
+ ### MenuDivider
168
+
169
+ Visual horizontal separator between menu items.
170
+
171
+ ```tsx
172
+ <MenuDivider />
173
+ ```
174
+
175
+ #### Props
176
+
177
+ | Prop | Type | Default | Description |
178
+ |------|------|---------|-------------|
179
+ | `className` | `string` | — | Additional CSS class |
180
+ | `style` | `CSSProperties` | — | Inline styles |
181
+
182
+ ---
183
+
184
+ ### MenuGroup
185
+
186
+ Semantic grouping of items with an optional label shorthand.
187
+
188
+ ```tsx
189
+ <MenuGroup label="Account">
190
+ <MenuItem>Profile</MenuItem>
191
+ <MenuItem>Settings</MenuItem>
192
+ </MenuGroup>
193
+ ```
194
+
195
+ #### Props
196
+
197
+ | Prop | Type | Default | Description |
198
+ |------|------|---------|-------------|
199
+ | `children` | `ReactNode` | **(required)** | Menu items inside the group |
200
+ | `label` | `ReactNode` | — | Optional group label (renders a `MenuLabel` internally) |
201
+ | `className` | `string` | — | Additional CSS class |
202
+
203
+ ---
204
+
205
+ ### MenuSub
206
+
207
+ Container for a collapsible or floating sub-menu. Wraps `MenuSubTrigger` + `MenuSubContent`.
208
+
209
+ Supports **two modes**:
210
+ - **`inline`** (default) — Collapsible accordion within the parent list
211
+ - **`popover`** — Floating panel that opens to the side via Floating UI
212
+
213
+ ```tsx
214
+ <MenuSub mode="inline" defaultOpen>
215
+ <MenuSubTrigger icon={<SettingsIcon />}>Settings</MenuSubTrigger>
216
+ <MenuSubContent>
217
+ <MenuItem>General</MenuItem>
218
+ <MenuItem>Security</MenuItem>
219
+ </MenuSubContent>
220
+ </MenuSub>
221
+ ```
222
+
223
+ #### Props
224
+
225
+ | Prop | Type | Default | Description |
226
+ |------|------|---------|-------------|
227
+ | `children` | `ReactNode` | **(required)** | Must contain `MenuSubTrigger` + `MenuSubContent` |
228
+ | `open` | `boolean` | — | Controlled open state |
229
+ | `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
230
+ | `onOpenChange` | `(open: boolean) => void` | — | Callback when open state changes |
231
+ | `mode` | `MenuSubMode` | inherited | Override parent sub-menu mode (`'inline'` or `'popover'`) |
232
+ | `trigger` | `MenuSubTriggerType` | inherited | Override popover trigger type (`'hover'` or `'click'`) |
233
+
234
+ **Controlled vs Uncontrolled**:
235
+ - Pass `open` + `onOpenChange` for controlled mode
236
+ - Pass `defaultOpen` for uncontrolled mode (internal state)
237
+
238
+ **Auto-expand**: When any descendant `MenuItem` has `selected={true}`, the sub-menu automatically opens and propagates upward through all ancestor `MenuSub` components.
239
+
240
+ **Soft-select**: When any descendant is selected, the `MenuSubTrigger` renders with a soft-selected background to indicate it contains the active item.
241
+
242
+ ---
243
+
244
+ ### MenuSubTrigger
245
+
246
+ The item that toggles a sub-menu open/closed. Renders a trailing arrow indicator.
247
+
248
+ ```tsx
249
+ <MenuSubTrigger icon={<FolderIcon />}>Projects</MenuSubTrigger>
250
+ ```
251
+
252
+ #### Props
253
+
254
+ | Prop | Type | Default | Description |
255
+ |------|------|---------|-------------|
256
+ | `children` | `ReactNode` | **(required)** | Label content |
257
+ | `icon` | `ReactNode` | — | Leading icon |
258
+ | `disabled` | `boolean` | `false` | Disabled state |
259
+
260
+ Also accepts all native `<div>` HTML attributes except `onClick`.
261
+
262
+ **Arrow indicators**:
263
+ - Inline mode: `▾` (closed) / `▴` (open)
264
+ - Popover mode: `▸` (always)
265
+
266
+ ---
267
+
268
+ ### MenuSubContent
269
+
270
+ Content container for sub-menu items. Automatically renders as inline (animated collapse) or floating popover based on the resolved mode.
271
+
272
+ ```tsx
273
+ <MenuSubContent>
274
+ <MenuItem>Child Item 1</MenuItem>
275
+ <MenuItem>Child Item 2</MenuItem>
276
+ </MenuSubContent>
277
+ ```
278
+
279
+ #### Props
280
+
281
+ | Prop | Type | Default | Description |
282
+ |------|------|---------|-------------|
283
+ | `children` | `ReactNode` | **(required)** | Sub-menu items |
284
+
285
+ Also accepts all native `<div>` HTML attributes.
286
+
287
+ ---
288
+
289
+ ## Types
290
+
291
+ ### MenuSubMode
292
+
293
+ ```ts
294
+ type MenuSubMode = 'inline' | 'popover';
295
+ ```
296
+
297
+ | Value | Description |
298
+ |-------|-------------|
299
+ | `'inline'` | Collapsible sub-menu rendered inline within parent |
300
+ | `'popover'` | Floating sub-menu using Floating UI positioning |
301
+
302
+ ### MenuDisplay
303
+
304
+ ```ts
305
+ type MenuDisplay = 'default' | 'icon';
306
+ ```
307
+
308
+ | Value | Description |
309
+ |-------|-------------|
310
+ | `'default'` | Full display — icons, labels, shortcuts visible |
311
+ | `'icon'` | Icon-only mode (mini sidebar) — labels and shortcuts hidden |
312
+
313
+ ### MenuSubTriggerType
314
+
315
+ ```ts
316
+ type MenuSubTriggerType = 'hover' | 'click';
317
+ ```
318
+
319
+ Controls how popover sub-menus are triggered. Only applies when `mode="popover"`.
320
+
321
+ ### MenuActiveIndicator
322
+
323
+ ```ts
324
+ type MenuActiveIndicator = 'dot' | 'bar' | false | ReactNode;
325
+ ```
326
+
327
+ ### MenuFloatingSettings
328
+
329
+ Global Floating UI configuration for all popover sub-menus.
330
+
331
+ ```ts
332
+ interface MenuFloatingSettings {
333
+ placement?: Placement; // default: 'right-start'
334
+ offset?: number; // gap between trigger and popover (px)
335
+ flip?: boolean; // flip placement if space is limited
336
+ shift?: boolean; // shift to stay in viewport
337
+ }
338
+ ```
339
+
340
+ ### MenuColorScheme
341
+
342
+ Full color customisation via raw CSS color strings. All properties are optional — only override what you need.
343
+
344
+ ```ts
345
+ interface MenuColorScheme {
346
+ // Container
347
+ background?: string;
348
+ color?: string;
349
+
350
+ // Hover state
351
+ hoverBg?: string;
352
+ hoverColor?: string;
353
+
354
+ // Active/selected item
355
+ activeBg?: string;
356
+ activeColor?: string;
357
+
358
+ // Soft-selected (parent trigger of active child)
359
+ softSelectedBg?: string;
360
+
361
+ // Danger item
362
+ dangerColor?: string;
363
+ dangerHoverBg?: string;
364
+
365
+ // Disabled text
366
+ disabledColor?: string;
367
+
368
+ // Secondary text (labels, shortcuts, arrows)
369
+ secondaryColor?: string;
370
+
371
+ // Divider line
372
+ dividerColor?: string;
373
+
374
+ // Focus ring
375
+ focusRingColor?: string;
376
+
377
+ // Popover sub-menu
378
+ popoverBg?: string;
379
+ popoverBorderColor?: string;
380
+
381
+ // Inline dot indicator
382
+ dotColor?: string;
383
+ dotActiveColor?: string;
384
+
385
+ // Child item hover (items inside SubContent)
386
+ childHoverColor?: string;
387
+ childHoverBg?: string;
388
+
389
+ // Active indicator icon color
390
+ activeIconColor?: string;
391
+ }
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Keyboard Navigation
397
+
398
+ Full keyboard support following [WAI-ARIA Menu Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu/):
399
+
400
+ | Key | Action |
401
+ |-----|--------|
402
+ | `↓` Arrow Down | Move focus to next visible item (wraps to first) |
403
+ | `↑` Arrow Up | Move focus to previous visible item (wraps to last) |
404
+ | `Home` | Move focus to first item |
405
+ | `End` | Move focus to last item |
406
+ | `Enter` / `Space` | Activate focused item or toggle sub-menu |
407
+ | `→` Arrow Right | Open inline sub-menu (on trigger) |
408
+ | `←` Arrow Left | Close current sub-menu, return focus to trigger |
409
+ | `Escape` | Close current sub-menu |
410
+ | **Typeahead** | Type characters to jump to matching item (500ms buffer) |
411
+
412
+ - Focus management uses roving tabindex — only the focused item has `tabIndex={0}`
413
+ - Collapsed sub-menu items are excluded from keyboard navigation
414
+ - Popover sub-menus have independent keyboard navigation within their floating panel
415
+
416
+ ---
417
+
418
+ ## Accessibility
419
+
420
+ | Feature | Implementation |
421
+ |---------|---------------|
422
+ | Roles | `menu` (container), `menuitem` (items), `separator` (dividers), `group` (groups) |
423
+ | `aria-disabled` | Set on disabled items |
424
+ | `aria-current="page"` | Set on selected items |
425
+ | `aria-haspopup="menu"` | Set on sub-menu triggers |
426
+ | `aria-expanded` | Set on sub-menu triggers (true/false) |
427
+ | `aria-labelledby` | Groups and sub-content reference their labels/triggers |
428
+ | Focus ring | Visible `box-shadow` outline on `:focus-visible` |
429
+
430
+ ---
431
+
432
+ ## Theming
433
+
434
+ ### Theme Provider
435
+
436
+ Wrap your app with `ThemeProvider` from `@thanh-libs/theme`. The menu reads `palette`, `spacing` from the theme context.
437
+
438
+ ```tsx
439
+ import { ThemeProvider } from '@thanh-libs/theme';
440
+ import { Menu, MenuItem } from '@thanh-libs/menu';
441
+
442
+ <ThemeProvider>
443
+ <Menu style={{ width: 260 }}>
444
+ <MenuItem>Home</MenuItem>
445
+ </Menu>
446
+ </ThemeProvider>
447
+ ```
448
+
449
+ ### Custom Color Scheme
450
+
451
+ For full control without modifying the theme, pass a `colorScheme` prop with raw CSS color strings:
452
+
453
+ ```tsx
454
+ <Menu
455
+ colorScheme={{
456
+ background: '#1e1e2e',
457
+ color: '#cdd6f4',
458
+ hoverBg: 'rgba(205,214,244,0.08)',
459
+ activeBg: 'rgba(137,180,250,0.2)',
460
+ activeColor: '#89b4fa',
461
+ softSelectedBg: 'rgba(137,180,250,0.08)',
462
+ secondaryColor: 'rgba(205,214,244,0.5)',
463
+ dividerColor: 'rgba(205,214,244,0.12)',
464
+ focusRingColor: '#89b4fa',
465
+ dangerColor: '#f38ba8',
466
+ dangerHoverBg: 'rgba(243,139,168,0.12)',
467
+ disabledColor: 'rgba(205,214,244,0.3)',
468
+ popoverBg: '#181825',
469
+ popoverBorderColor: 'rgba(205,214,244,0.1)',
470
+ }}
471
+ >
472
+
473
+ </Menu>
474
+ ```
475
+
476
+ ---
477
+
478
+ ## Usage Patterns
479
+
480
+ ### Sidebar with Grouped Items
481
+
482
+ ```tsx
483
+ <Menu style={{ width: 260 }}>
484
+ <MenuGroup label="Navigation">
485
+ <MenuItem icon={<DashboardIcon />} selected>Dashboard</MenuItem>
486
+ <MenuItem icon={<UsersIcon />}>Users</MenuItem>
487
+ <MenuItem icon={<ChartIcon />}>Analytics</MenuItem>
488
+ </MenuGroup>
489
+ <MenuDivider />
490
+ <MenuGroup label="Account">
491
+ <MenuItem icon={<ProfileIcon />}>Profile</MenuItem>
492
+ <MenuItem icon={<SettingsIcon />}>Settings</MenuItem>
493
+ <MenuItem icon={<LogoutIcon />} danger>Logout</MenuItem>
494
+ </MenuGroup>
495
+ </Menu>
496
+ ```
497
+
498
+ ### Icon-Only Mini Sidebar (with Popover Sub-menus)
499
+
500
+ ```tsx
501
+ <Menu display="icon" mode="popover" style={{ width: 'fit-content' }}>
502
+ <MenuItem icon={<HomeIcon />} selected>Home</MenuItem>
503
+ <MenuItem icon={<UserIcon />}>Profile</MenuItem>
504
+ <MenuSub>
505
+ <MenuSubTrigger icon={<SettingsIcon />}>Settings</MenuSubTrigger>
506
+ <MenuSubContent>
507
+ <MenuItem>General</MenuItem>
508
+ <MenuItem>Security</MenuItem>
509
+ </MenuSubContent>
510
+ </MenuSub>
511
+ </Menu>
512
+ ```
513
+
514
+ ### Nested Sub-menus (Multi-level)
515
+
516
+ ```tsx
517
+ <Menu style={{ width: 260 }}>
518
+ <MenuItem>Home</MenuItem>
519
+ <MenuSub>
520
+ <MenuSubTrigger>Projects</MenuSubTrigger>
521
+ <MenuSubContent>
522
+ <MenuItem>All Projects</MenuItem>
523
+ <MenuSub>
524
+ <MenuSubTrigger>By Team</MenuSubTrigger>
525
+ <MenuSubContent>
526
+ <MenuItem>Frontend</MenuItem>
527
+ <MenuItem>Backend</MenuItem>
528
+ <MenuItem>DevOps</MenuItem>
529
+ </MenuSubContent>
530
+ </MenuSub>
531
+ </MenuSubContent>
532
+ </MenuSub>
533
+ </Menu>
534
+ ```
535
+
536
+ ### Popover Sub-menus (Floating)
537
+
538
+ ```tsx
539
+ <Menu mode="popover" trigger="hover" floatingSettings={{ placement: 'right-start', offset: 4 }}>
540
+ <MenuItem>Dashboard</MenuItem>
541
+ <MenuSub>
542
+ <MenuSubTrigger>Analytics</MenuSubTrigger>
543
+ <MenuSubContent>
544
+ <MenuItem>Overview</MenuItem>
545
+ <MenuItem>Reports</MenuItem>
546
+ </MenuSubContent>
547
+ </MenuSub>
548
+ </Menu>
549
+ ```
550
+
551
+ ### Inline Dot Indicators
552
+
553
+ Show dot bullets beside child items inside expanded sub-menus:
554
+
555
+ ```tsx
556
+ <Menu showDot style={{ width: 280 }}>
557
+ <MenuItem icon={<HomeIcon />} selected>Dashboard</MenuItem>
558
+ <MenuSub defaultOpen>
559
+ <MenuSubTrigger icon={<ChartIcon />}>Analytics</MenuSubTrigger>
560
+ <MenuSubContent>
561
+ <MenuItem>Overview</MenuItem>
562
+ <MenuItem selected>Reports</MenuItem>
563
+ <MenuItem>Exports</MenuItem>
564
+ </MenuSubContent>
565
+ </MenuSub>
566
+ </Menu>
567
+ ```
568
+
569
+ ### Dense Mode
570
+
571
+ ```tsx
572
+ <Menu dense style={{ width: 220 }}>
573
+ <MenuItem>Home</MenuItem>
574
+ <MenuItem selected>Users</MenuItem>
575
+ <MenuItem>Analytics</MenuItem>
576
+ </Menu>
577
+ ```
578
+
579
+ ### Scrollable Menu
580
+
581
+ ```tsx
582
+ <Menu maxHeight={300} style={{ width: 260 }}>
583
+ {items.map((item) => (
584
+ <MenuItem key={item.id}>{item.label}</MenuItem>
585
+ ))}
586
+ </Menu>
587
+ ```
588
+
589
+ ---
590
+
591
+ ## Design Tokens
592
+
593
+ | Token | Value | Description |
594
+ |-------|-------|-------------|
595
+ | Font size (default) | `0.875rem` (14px) | Regular item text |
596
+ | Font size (dense) | `0.8125rem` (13px) | Dense mode item text |
597
+ | Font size (shortcut) | `0.75rem` (12px) | Keyboard shortcut text |
598
+ | Font size (label) | `0.75rem` (12px) | Group label text |
599
+ | Icon size | `20×20px` | Leading icon container |
600
+ | Border radius | `0.375rem` (6px) | Item border radius |
601
+ | Collapse duration | `250ms` | Inline sub-menu expand/collapse |
602
+ | Background transition | `150ms` | Hover/select background change |
603
+ | Typeahead timeout | `500ms` | Character buffer reset delay |
604
+ | Popover min width | `160px` | Minimum floating panel width |
605
+ | Popover z-index | `1300` | Floating panel z-index |
606
+ | Sub close delay | `150ms` | Popover close delay (hover mode) |
607
+
608
+ ---
609
+
610
+ ## License
611
+
612
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thanh-libs/menu",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",