@aiquants/menu-bar 1.18.3 → 1.19.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.
package/README.md CHANGED
@@ -4,13 +4,16 @@ A React menu bar component with authentication support and theming for modern we
4
4
 
5
5
  ## Key Features
6
6
 
7
- - **Menu Navigation**: Collapsible menu with smooth animations and hierarchical structure
7
+ - **Menu Navigation**: A drawer that slides in from the right, with top-level links and collapsible groups
8
+ - **Current Page Highlighting**: The entry the current route belongs to carries `aria-current="page"` and its group opens, with exact or prefix matching per entry
8
9
  - **Dark/Light Theme**: Comprehensive theming support with scoped CSS
9
10
  - **Session Management**: Built-in session information display with user profiles
10
- - **Responsive Design**: Mobile-friendly with overlay support
11
+ - **Pinning**: Keep the drawer open while navigating, with the pinned width exposed as a CSS variable and an event
12
+ - **Accessible**: Named switch with `aria-expanded`, an inert closed drawer, focus that returns to the switch, visible focus rings, reduced-motion and forced-colors support
13
+ - **Configurable Switch**: Position, size, layering and caption of the open/close switch set through CSS custom properties
11
14
  - **TypeScript Support**: Full TypeScript definitions included
12
15
  - **React Router Integration**: Built for React Router with generic link component support
13
- - **CSS Scoped**: All styles are scoped to prevent conflicts with host applications
16
+ - **Scoped Component CSS**: The hand-written rules are scoped to `.aiquants-*` classes and package-prefixed variables; the standalone build also carries Tailwind utilities and theme variables, which are global (see "Styling (CSS)")
14
17
 
15
18
  ## Installation
16
19
 
@@ -18,20 +21,30 @@ A React menu bar component with authentication support and theming for modern we
18
21
  npm install @aiquants/menu-bar
19
22
  ```
20
23
 
24
+ Peer dependencies: `react` and `react-dom` `^19.2.7`, and `react-router-dom` `^7.7.1`. The package's `engines` field asks for Node.js 18 or later.
25
+
21
26
  ## Exports
22
27
 
23
- This package exports the following components and types:
28
+ This package exports the following components, functions and types:
24
29
 
25
30
  ```tsx
26
31
  // Components
27
32
  export { MenuBar } from '@aiquants/menu-bar';
28
33
  export { SessionInfo, DefaultLink } from '@aiquants/menu-bar';
29
34
 
35
+ // Pin state (event and hook)
36
+ export { useMenuPinState, subscribeMenuPinChange, dispatchMenuPinChange, getInitialPinState, MENU_PIN_CHANGE_EVENT } from '@aiquants/menu-bar';
37
+
30
38
  // Types
31
- export type { IMenu, ISubmenu, MenuNavigationPayload } from '@aiquants/menu-bar';
39
+ export type { IMenuBarProps } from '@aiquants/menu-bar';
40
+ export type { IMenu, ISubmenu, MenuBarPathMatch, MenuNavigationPayload } from '@aiquants/menu-bar';
32
41
  export type { Theme } from '@aiquants/menu-bar';
33
42
  export type { SessionInfoProps, LinkProps } from '@aiquants/menu-bar';
34
43
  export type { UserProfile } from '@aiquants/menu-bar';
44
+ export type { MenuPinChangeDetail, MenuPinCustomEvent, SubscribeMenuPinOptions, UseMenuPinStateOptions } from '@aiquants/menu-bar';
45
+
46
+ // Collision-free aliases of the types above
47
+ export type { IMenuBarMenu, IMenuBarSubmenu, MenuBarTheme, MenuBarUserProfile, MenuBarLinkProps, MenuBarSessionInfoProps } from '@aiquants/menu-bar';
35
48
 
36
49
  // CSS — non-Tailwind hosts load the self-contained standalone build once (Tailwind hosts: see "Styling (CSS)")
37
50
  import '@aiquants/menu-bar/styles/menu-bar.standalone.css';
@@ -41,11 +54,11 @@ import '@aiquants/menu-bar/styles/menu-bar.standalone.css';
41
54
 
42
55
  ```tsx
43
56
  import React, { useState } from 'react';
44
- import { MenuBar } from '@aiquants/menu-bar';
57
+ import { MenuBar, type Theme } from '@aiquants/menu-bar';
45
58
  import '@aiquants/menu-bar/styles/menu-bar.standalone.css';
46
59
 
47
60
  function App() {
48
- const [theme, setTheme] = useState('light');
61
+ const [theme, setTheme] = useState<Theme>('light');
49
62
 
50
63
  const menuList = [
51
64
  {
@@ -87,6 +100,8 @@ function App() {
87
100
  }
88
101
  ```
89
102
 
103
+ `MenuBar` renders React Router links, so place it inside a router provider.
104
+
90
105
  ## Props
91
106
 
92
107
  ### MenuBar Props
@@ -98,9 +113,12 @@ function App() {
98
113
  | `toggleTheme` | `() => void` | Yes | Function to toggle between light and dark themes |
99
114
  | `onClickSignout` | `() => void` | Yes | Callback function for sign out action |
100
115
  | `menuHeader` | `React.ReactNode` | No | Optional header content for the menu |
101
- | `isMenuhide` | `boolean` | No | Whether to hide the menu bar entirely |
116
+ | `isMenuHide` | `boolean` | No | Whether to hide the menu bar entirely. Renders nothing while `true`, but the pin state is still reported: while pinned, `--aiquants-menu-pinned-width` and the pin event keep the pinned width (see "Pin State") |
117
+ | `isMenuhide` | `boolean` | No | Legacy spelling of `isMenuHide`; read only when `isMenuHide` is not given |
102
118
  | `onMenuNavigate` | `(payload: MenuNavigationPayload) => void` | No | Callback function triggered when a menu or submenu item is selected |
103
- | `onPinChange` | `(pinned: boolean) => void` | No | Callback function triggered when the menu bar's pin state changes |
119
+ | `isPinned` | `boolean` | No | Controlled pin state. When omitted, the menu bar keeps the pin state itself and stores it in `localStorage` |
120
+ | `pinnedWidth` | `number` | No | Width in pixels reported while pinned (default `240`) |
121
+ | `onPinChange` | `(pinned: boolean, widthPx: number) => void` | No | Called when the pin button toggles the pin state, and with `(false, 0)` whenever the switch, the backdrop or Escape closes the drawer (not when selecting an item closes it) |
104
122
  | `pinnedSubmenuIds` | `number[]` | No | Pinned submenu IDs (controlled) |
105
123
  | `initialPinnedSubmenuIds` | `number[]` | No | Initial pinned submenu IDs |
106
124
  | `onSubmenuPinChange` | `(pinnedIds: number[]) => void` | No | Callback function triggered when submenu pin states change |
@@ -114,6 +132,7 @@ interface IMenu {
114
132
  title: string;
115
133
  href?: string;
116
134
  submenu: ISubmenu[];
135
+ match?: MenuBarPathMatch; // ignored on a group without href
117
136
  }
118
137
  ```
119
138
 
@@ -126,9 +145,18 @@ interface ISubmenu {
126
145
  href: string;
127
146
  mark?: string;
128
147
  new?: boolean;
148
+ match?: MenuBarPathMatch;
129
149
  }
130
150
  ```
131
151
 
152
+ ### MenuBarPathMatch Type
153
+
154
+ ```tsx
155
+ type MenuBarPathMatch = 'exact' | 'prefix';
156
+ ```
157
+
158
+ How an entry's `href` is matched against the current path. See "Current Page Highlighting" below.
159
+
132
160
  ### MenuNavigationPayload Type
133
161
 
134
162
  ```tsx
@@ -167,17 +195,20 @@ interface LinkProps {
167
195
 
168
196
  The package ships **two CSS artifacts** — pick the one that matches your host.
169
197
 
170
- - **Tailwind v4 host** — import the components-only build into the `components` layer, and let your own Tailwind build generate the JSX utility classes by scanning the package source:
198
+ - **Tailwind v4 host** — import the components-only build into the `components` layer, and let your own Tailwind build generate the JSX utility classes by scanning the package's built JavaScript (the published package ships `dist`, not `src`; the class names stay intact in the bundle):
171
199
 
172
200
  ```css
173
201
  /* app tailwind.css */
174
202
  @import "@aiquants/menu-bar/styles/menu-bar.css" layer(components);
175
- @source "../node_modules/@aiquants/menu-bar/src/**/*.{ts,tsx}";
176
- /* monorepo: @source "../../../../packages/menu-bar/src/**/*.{ts,tsx}"; */
203
+ @source "../node_modules/@aiquants/menu-bar/dist/**/*.js";
204
+ /* consuming the package's sources from a workspace: point @source at its src/**/*.{ts,tsx} instead */
205
+ @custom-variant dark (&:where(.dark, .dark *));
177
206
  ```
178
207
 
179
- `menu-bar.css` (Artifact A) carries only the hand-written `.aiquants-*` component classes — no `:root` theme variables, no Tailwind utilities, no preflight — so it drops into `layer(components)` safely.
180
- The one `:root` rule it emits is menu-bar's own runtime variable `--aiquants-menu-pinned-width`, which the component sets inline at runtime (a runtime input, not a theme override).
208
+ The `@custom-variant` line makes the menu bar's `dark:` utilities follow the `theme` prop: the menu bar's root carries `.dark` while `theme` is `'dark'`. With Tailwind's default `dark` variant (`prefers-color-scheme`), they follow the OS setting instead. The variant applies to your whole build, so your own `dark:` utilities then key off a `.dark` ancestor as well.
209
+
210
+ `menu-bar.css` (Artifact A) carries only the hand-written `.aiquants-*` component classes and the package's own `:root` variables — no Tailwind theme variables, no Tailwind utilities, no preflight — so it drops into `layer(components)` safely.
211
+ Its `:root` rule declares menu-bar's runtime variable `--aiquants-menu-pinned-width` (which the component sets inline at runtime) and the defaults of the menu switch custom properties (`--aqmb-switch-*`, see below).
181
212
  Do **not** also `@import` the standalone build in a Tailwind host: it re-emits the same utilities your build produces, and two concatenated builds cannot reproduce the single-build canonical order (base → variants), so base/variant cascade winners flip.
182
213
 
183
214
  - **Non-Tailwind host** — load the single self-contained standalone build (via `<link>` or a plain `@import`); it bundles the component classes plus every JSX utility:
@@ -186,11 +217,53 @@ The package ships **two CSS artifacts** — pick the one that matches your host.
186
217
  @import "@aiquants/menu-bar/styles/menu-bar.standalone.css";
187
218
  ```
188
219
 
189
- Dark mode keys off the `.dark` class — add `.dark` to `<html>` to enable the dark styles.
220
+ The utilities in this build are global selectors (`.flex`, `.fixed`, `.hidden`, ...), and it declares the Tailwind theme variables it uses on `:root, :host`, so a host element with one of those class names picks up the rule.
221
+
222
+ Dark mode follows the `theme` prop: the menu bar's root carries `.dark` while `theme` is `'dark'`, and this build's `dark:` utilities apply to an element with the `.dark` class or inside one, so `<html>` needs no class. A `SessionInfo` rendered outside `MenuBar` takes its dark styles from a `.dark` ancestor of its own. If you also put `.dark` on `<html>`, keep it in step with `theme`: with `theme="light"` it turns on the `dark:` utilities inside the light menu bar while the light colours of the hand-written rules stay, mixing the two palettes.
223
+
224
+ In both setups the component classes sit in the `components` layer and the utilities in the later `utilities` layer, so a utility wins over a component rule for the same property.
225
+
226
+ ## Menu Switch Custom Properties
227
+
228
+ The open/close switch is a square box with four bars that turn into a cross while the drawer is open, over a "menu" / "close" caption. Its whole drawing lives in the package stylesheet, and it carries no utility classes, so every custom property below takes effect. The defaults are declared on `:root`; override them on `:root` (or any ancestor of the menu bar).
229
+
230
+ | Property | Default | Effect |
231
+ | --- | --- | --- |
232
+ | `--aqmb-switch-position` | `fixed` | `position` of the switch |
233
+ | `--aqmb-switch-top` | `6px` | `top` of the switch |
234
+ | `--aqmb-switch-right` | `8px` | `right` of the switch |
235
+ | `--aqmb-switch-left` | `auto` | `left` of the switch |
236
+ | `--aqmb-switch-bottom` | `auto` | `bottom` of the switch |
237
+ | `--aqmb-switch-z-index` | `calc(infinity)` | `z-index` of the switch. The default computes to the largest integer the browser supports (2147483647 in Chromium), which keeps the switch above the drawer (`z-index` 2147483642) and the backdrop (2147483640) |
238
+ | `--aqmb-switch-size` | `27px` | Side S of the switch's square box; every length of the drawing derives from it |
239
+ | `--aqmb-switch-caption-display` | `block` | `display` of the caption; `none` hides it |
240
+
241
+ The `z-index` default is `calc(infinity)` rather than a number because Lightning CSS, which Tailwind v4 builds pass stylesheets through, writes the numbers of custom-property values and `var()` fallbacks with 6 significant digits: `2147483647` would reach the browser as `2147480000`, below the drawer. An override in a stylesheet that also goes through Lightning CSS is rounded the same way when it has 7 or more significant digits. A small value such as `50` is kept as written, and stacks the switch below the drawer and the backdrop.
242
+
243
+ How the drawing follows S:
244
+
245
+ - The bars are S/9 thick, at 0, 2S/9, 4S/9 and 6S/9 of a stack 7S/9 high. While open, the outer bars become 32S/27 wide and 4S/27 thick and cross at ±45°; the inner bars fade out.
246
+ - The caption's top edge is S/2 + 0.5rem below the top of the stack.
247
+ - With the caption shown (the default) and S below 48px (at a 16px root font size), the column of bars and caption is taller than the box: it starts at the box's top and the caption hangs below the box.
248
+ - With `--aqmb-switch-caption-display: none`, the bars are centred in the box and nothing is drawn above or below it. The cross can still extend slightly past the box's right edge, as it does in the default drawing.
249
+ - The default `27px` gives 3px bars spaced 6px apart, a 32px × 4px cross, and a caption 21.5px below the top of the box.
250
+ - The switch draws in black, and in white on the dark theme and while the drawer is open. Its keyboard focus ring is a 2px outline in the same colour at a 2px offset, so it occupies 4px around the box.
251
+
252
+ Example — fit the switch into a 32px header bar (4px + 24px line box + 4px):
253
+
254
+ ```css
255
+ :root {
256
+ --aqmb-switch-size: 24px;
257
+ --aqmb-switch-caption-display: none;
258
+ --aqmb-switch-top: 4px;
259
+ }
260
+ ```
261
+
262
+ The box sits on the bar's 24px line box, and the focus ring (0 to 32px from the top) stays inside the bar. Adjust `--aqmb-switch-right` to align it horizontally.
190
263
 
191
264
  ## Theming
192
265
 
193
- The menu bar supports both light and dark themes. All styles are scoped with the `.aiquants-menu-bar` class to prevent conflicts with your application's styles.
266
+ The menu bar supports both light and dark themes. The hand-written styles are scoped with the `.aiquants-menu-bar` class; the utility classes and Tailwind theme variables of the standalone build are global (see "Styling (CSS)"). The root element carries the theme as a class: `.aiquants-menu-bar.light` or `.aiquants-menu-bar.dark`.
194
267
 
195
268
  ### Custom Styling
196
269
 
@@ -201,11 +274,11 @@ You can override the default styles by targeting the scoped classes:
201
274
  /* Custom menu bar styles */
202
275
  }
203
276
 
204
- .aiquants-menu-bar.theme-dark {
277
+ .aiquants-menu-bar.dark {
205
278
  /* Dark theme overrides */
206
279
  }
207
280
 
208
- .aiquants-menu-bar.theme-light {
281
+ .aiquants-menu-bar.light {
209
282
  /* Light theme overrides */
210
283
  }
211
284
  ```
@@ -214,10 +287,10 @@ You can override the default styles by targeting the scoped classes:
214
287
 
215
288
  ```tsx
216
289
  import React, { useState } from 'react';
217
- import { MenuBar } from '@aiquants/menu-bar';
290
+ import { MenuBar, type Theme } from '@aiquants/menu-bar';
218
291
 
219
292
  function App() {
220
- const [theme, setTheme] = useState('light');
293
+ const [theme, setTheme] = useState<Theme>('light');
221
294
 
222
295
  const menuList = [
223
296
  {
@@ -247,6 +320,89 @@ function App() {
247
320
  }
248
321
  ```
249
322
 
323
+ ## Current Page Highlighting
324
+
325
+ The menu finds the one entry the current route belongs to, marks its link with `aria-current="page"`, and opens its group. Each top-level item with an `href` and each submenu item can choose how its `href` is matched with `match`:
326
+
327
+ ```tsx
328
+ const menuList: IMenu[] = [
329
+ { id: 1, title: 'Home', href: '/', submenu: [] },
330
+ {
331
+ id: 2,
332
+ title: 'Admin',
333
+ submenu: [
334
+ // Also the current entry on /admin/console/search, /admin/console/users/42, ...
335
+ { subid: 1, title: 'Console', href: '/admin/console', match: 'prefix' },
336
+ { subid: 2, title: 'Audit log', href: '/admin/audit' },
337
+ ],
338
+ },
339
+ ];
340
+ ```
341
+
342
+ The rule:
343
+
344
+ - `'exact'` (the default) matches the path itself. `'prefix'` also matches every path below the `href` at a segment boundary: `/admin` matches `/admin/users`, not `/administration`. Any other value behaves as `'exact'`.
345
+ - The query, the hash and a trailing slash are ignored on both the `href` and the path.
346
+ - An `href` of `/` matches only the root, even with `'prefix'`. External URLs never match.
347
+ - When several entries match, the longest `href` wins, across groups too; an exact match therefore always beats a prefix match. On a tie, the first entry in menu order wins (a top-level item before its own submenu items).
348
+
349
+ The group of the matched entry opens once per route change, and only one group is open at a time (pinned groups always stay open). When the matched entry is a top-level link, no group opens, and the group that was open closes. A route with no matching entry leaves the open group as it is, and reloading the menu on the same route does not reopen a group the user closed.
350
+
351
+ ## Pin State
352
+
353
+ The pin button keeps the drawer open, without the backdrop, while the user navigates. When it mounts and whenever the pin state, `pinnedWidth` or the open state changes, the menu bar reports the state in two ways, and it resets both to "not pinned" when it unmounts:
354
+
355
+ - **CSS variable**: `--aiquants-menu-pinned-width` on `<html>` holds the pinned width (`pinnedWidth`, default `240`) in pixels while pinned, and `0px` otherwise.
356
+ - **Event**: a `CustomEvent` named by `MENU_PIN_CHANGE_EVENT` (`"aiquants-menu-pin-change"`) on `window`. Its `detail` is a `MenuPinChangeDetail`, `{ pinned, width, active? }`: `width` matches the CSS variable, and `active` tells whether the drawer is open or pinned.
357
+
358
+ These exports read the state:
359
+
360
+ | Export | Behaviour |
361
+ | --- | --- |
362
+ | `useMenuPinState(options?)` | Returns the current `MenuPinChangeDetail` and re-renders when it changes. Options: `initialPinned` (default `false`) and `defaultPinnedWidth` (default `240`). During server rendering it returns `{ pinned: initialPinned, width: initialPinned ? defaultPinnedWidth : 0 }`, without `active` |
363
+ | `subscribeMenuPinChange(listener, options?)` | Calls `listener` with the `detail` of every event and returns a function that unsubscribes. Options: `immediate` (default `true`: also calls `listener` once right away with `getInitialPinState(defaultPinnedWidth)`) and `defaultPinnedWidth` (default `240`) |
364
+ | `getInitialPinState(defaultPinnedWidth = 240)` | Reads the CSS variable: pinned when it is a positive number of pixels, which is then the width. When the variable is empty or not a number, it reads the `aiquants-menu-pinned` `localStorage` key instead (`"true"`: pinned at `defaultPinnedWidth`); the package stylesheet declares the variable as `0px`, so this happens only while neither the stylesheet nor a menu bar has set it. `active` is that of the last `dispatchMenuPinChange` call (`false` before the first) |
365
+ | `dispatchMenuPinChange(pinned, width, active = pinned)` | Dispatches the event on `window`. The menu bar calls it itself |
366
+
367
+ ```tsx
368
+ import type { ReactNode } from 'react';
369
+ import { useMenuPinState } from '@aiquants/menu-bar';
370
+
371
+ function Page({ children }: { children: ReactNode }) {
372
+ const { width } = useMenuPinState();
373
+ // The pinned drawer covers the right edge of the viewport
374
+ return <main style={{ paddingRight: width }}>{children}</main>;
375
+ }
376
+ ```
377
+
378
+ ## Accessibility
379
+
380
+ - **Switch**: a native `<button>` named "Open menu" / "Close menu" (`aria-label`) with `aria-expanded`; the visible caption is `aria-hidden`. Keyboard focus shows a 2px ring (`:focus-visible`).
381
+ - **Closed drawer is inert**: while closed, nothing in the drawer takes focus (by Tab or by script) or appears in the accessibility tree, so the switch is the menu bar's only Tab stop. A pinned drawer stays open and interactive. This relies on the HTML `inert` attribute.
382
+ - **Decorative backdrop**: the dimming layer behind the open drawer is an `aria-hidden` `div` that never takes focus. A pointer press on it closes the drawer without moving the focus; keyboard and assistive-technology users close the drawer with Escape (per `closeOnEscape`) or the switch.
383
+ - **Focus return**: when the drawer closes — by the switch, the backdrop, Escape or selecting an item while not pinned — with the focus inside it, the focus moves to the switch. A focus outside the drawer stays where it is.
384
+ - **Current page**: the matched entry's link carries `aria-current="page"`.
385
+ - **Groups**: group summaries toggle with Enter or Space and show a focus outline on keyboard focus. Every button has an accessible name.
386
+ - **Reduced motion**: the drawer's slide, the backdrop's fade and the switch's transitions run only under `prefers-reduced-motion: no-preference`.
387
+ - **Forced colors**: the switch's bars are drawn in `ButtonText`, so they stay visible when forced colors replace backgrounds.
388
+ - **Built-in labels**: the names and texts are fixed, and no prop localises them. The switch is named in English ("Open menu" / "Close menu", caption "menu" / "close"); the drawer's pin and theme buttons, the sign-out button and the group pin buttons are named in Japanese (for example "ピン留め", "ダークモードに切り替え", "サインアウト"), and `SessionInfo` shows Japanese texts such as "プロフィール" and "ログインしてください".
389
+
390
+ ## DOM Hooks
391
+
392
+ Select the menu bar's elements in scripts and tests by these attributes (or by role and accessible name). Class names — `.aiquants-*` and utilities — are styling and may change.
393
+
394
+ | Attribute | Element |
395
+ | --- | --- |
396
+ | `data-aqmb-menu-switch` | The open/close switch button |
397
+ | `data-aqmb-menu-content` | The drawer's root element |
398
+ | `data-aqmb-menu-overlay` | The backdrop |
399
+ | `data-aqmb-menu-list` | The scroll container of the menu list |
400
+ | `data-aqmb-pin-button` | The drawer's pin button |
401
+ | `data-aqmb-theme-button` | The theme toggle button |
402
+ | `data-aqmb-signout-button` | The sign-out button |
403
+ | `data-aqmb-submenu-pin-button` | The pin button of each group |
404
+ | `data-aqmb-session-card` | The `SessionInfo` card, with or without a `user` |
405
+
250
406
  ## Advanced Usage
251
407
 
252
408
  ### With Menu Header
@@ -280,7 +436,7 @@ import React from 'react';
280
436
  theme={theme}
281
437
  toggleTheme={toggleTheme}
282
438
  onClickSignout={handleSignout}
283
- isMenuhide={shouldHideMenu}
439
+ isMenuHide={shouldHideMenu}
284
440
  />
285
441
  ```
286
442
 
@@ -290,19 +446,21 @@ import React from 'react';
290
446
 
291
447
  The main menu bar component that includes:
292
448
 
293
- - Menu toggle button with hamburger icon
294
- - Company logo and navigation menu
295
- - Theme toggle functionality
296
- - Responsive overlay for mobile devices
297
- - Sign out functionality
449
+ - Open/close switch with four bars that turn into a cross
450
+ - Header slot (`menuHeader`) and navigation menu
451
+ - Pin, theme toggle and sign-out buttons
452
+ - Backdrop that closes the drawer when pressed
453
+ - Focus management and current page highlighting
298
454
 
299
455
  ### SessionInfo
300
456
 
301
457
  A standalone session information component that displays:
302
458
 
303
- - User avatar/profile picture
304
- - User display name
305
- - Logout functionality with customizable link component
459
+ - The user's avatar (a default icon when `photo` is empty), display name and a "Google Account" label
460
+ - Two links rendered through the customizable `LinkComponent` (default: `DefaultLink`, a plain `<a>`): a profile link to `/` and a link to `https://myaccount.google.com/`
461
+ - A "please sign in" card ("ログインしてください") when `user` is `null`
462
+
463
+ The card carries `data-aqmb-session-card` in both states. Sign-out is not part of `SessionInfo`: it is the drawer's sign-out button, which calls `onClickSignout`.
306
464
 
307
465
  ```tsx
308
466
  import { SessionInfo } from '@aiquants/menu-bar';
@@ -322,42 +480,56 @@ import { SessionInfo } from '@aiquants/menu-bar';
322
480
 
323
481
  - **Hierarchical Menu Structure**: Support for main menu items with submenus
324
482
  - **Dynamic Navigation**: Menu items can have direct links or submenu collections
325
- - **Visual Indicators**: Support for special marks (lock icon, etc.) and "new" badges
326
- - **External Links**: Option to open links in new tabs
483
+ - **Current Page**: Exact or prefix matching per entry, with `aria-current="page"`
484
+ - **Visual Indicators**: An icon per submenu `mark` (`report`, `slip`, `table`, `chart`, `download`, `lock`, `href`); any other value, or none, renders the text only
485
+ - **External Links**: `new: true` opens a submenu item in a new tab (`target="_blank"`, `rel="noopener noreferrer"`); it draws no badge
327
486
 
328
487
  ## CSS Classes
329
488
 
330
- All CSS classes are scoped with `.aiquants-` prefix:
489
+ Hand-written component classes use the `.aiquants-` prefix (the modifiers are the theme class `light` / `dark` on the root and `pinned` on a pinned group's pin button), for example:
331
490
 
332
- - `.aiquants-menu-bar` - Main container
333
- - `.aiquants-menu-switch` - Menu toggle button
334
- - `.aiquants-menu-content` - Menu content area
335
- - `.aiquants-session-info` - Session information
336
- - `.aiquants-overlay-shadow` - Mobile overlay
491
+ - `.aiquants-menu-bar` - Main container (with the theme class `light` or `dark`)
492
+ - `.aiquants-menu-switch` - Open/close switch (`.aiquants-menu-switch-bars`, `.aiquants-menu-switch-bar`, `.aiquants-menu-switch-caption`)
493
+ - `.aiquants-menu-content` - Drawer
494
+ - `.aiquants-menu-list-item` - Menu item and group summary
495
+ - `.aiquants-session-card` - Session information card
496
+
497
+ The menu list's scroll container also carries `hidden-scrollbar`, a class the package does not define, so a host rule of that name applies to it.
498
+
499
+ These classes are a styling contract, not DOM hooks — use the `data-aqmb-*` attributes above to select elements.
337
500
 
338
501
  ## Browser Support
339
502
 
340
- - Chrome/Edge 88+
341
- - Firefox 78+
342
- - Safari 14+
343
- - Node.js 18+
503
+ - Chrome/Edge 111+
504
+ - Firefox 128+
505
+ - Safari 16.4+
506
+
507
+ These are the versions Tailwind CSS v4, which builds both stylesheets, targets. The stylesheets also sit in cascade layers, which an older browser drops whole, and the closed drawer and the backdrop rely on the HTML `inert` attribute.
344
508
 
345
509
  ## Development
346
510
 
347
511
  ```bash
348
512
  # Install dependencies
349
- npm install
513
+ pnpm install
350
514
 
351
515
  # Build the package
352
- npm run build
516
+ pnpm run build
353
517
 
354
518
  # Run type checking
355
- npm run typecheck
519
+ pnpm run typecheck
520
+
521
+ # Run the unit tests (Vitest, jsdom)
522
+ pnpm run test
523
+
524
+ # Run the browser tests (Playwright, Chromium) against a local harness page
525
+ pnpm run test:e2e
356
526
 
357
527
  # Clean build files
358
- npm run clean
528
+ pnpm run clean
359
529
  ```
360
530
 
531
+ The browser tests start a Vite server for the harness on port `25178`; set `MENU_BAR_E2E_PORT` to use another port, and `MENU_BAR_E2E_REUSE_SERVER=1` to reuse a server that is already running. Install Chromium once with `pnpm exec playwright install chromium` if it is missing.
532
+
361
533
  ## License
362
534
 
363
535
  MIT License - see [LICENSE](LICENSE) file for details.
@@ -12,6 +12,7 @@ interface IMenuContentProps {
12
12
  onTogglePin(): void;
13
13
  pinnedSubmenuIds: number[];
14
14
  onToggleSubmenuPin(id: number): void;
15
+ ref: React.Ref<HTMLDivElement>;
15
16
  }
16
17
  export declare const MenuContent: (props: IMenuContentProps) => React.JSX.Element;
17
18
  export {};
@@ -1,15 +1,18 @@
1
+ export type MenuBarPathMatch = "exact" | "prefix";
1
2
  export interface ISubmenu {
2
3
  subid: number;
3
4
  title?: string;
4
5
  href: string;
5
6
  mark?: string;
6
7
  new?: boolean;
8
+ match?: MenuBarPathMatch;
7
9
  }
8
10
  export interface IMenu {
9
11
  id: number;
10
12
  title: string;
11
13
  href?: string;
12
14
  submenu: ISubmenu[];
15
+ match?: MenuBarPathMatch;
13
16
  }
14
17
  export type MenuNavigationPayload = {
15
18
  menu: Pick<IMenu, "id" | "title" | "href">;
@@ -1,6 +1,6 @@
1
1
  interface IOverlayProps {
2
2
  isOverlay: boolean;
3
- onOverlayClick?(): void;
3
+ onOverlayClick(): void;
4
4
  }
5
5
  export declare const OverlayShadow: (props: IOverlayProps) => React.JSX.Element;
6
6
  export {};
@@ -1,13 +1,8 @@
1
- import { Theme } from "../types";
2
- export interface IMenuSwitchProps {
3
- open?: boolean;
4
- isOpen?: boolean;
5
- onMenuOpen?: () => void;
6
- onMenuClose?: () => void;
7
- onToggle?: (nextOpen: boolean) => void;
8
- theme?: Theme;
9
- pinned?: boolean;
10
- isPinned?: boolean;
11
- className?: string;
12
- }
13
- export declare const MenuSwitch: (props: IMenuSwitchProps) => React.JSX.Element;
1
+ type MenuSwitchProps = {
2
+ isMenuOpen: boolean;
3
+ onMenuOpen(): void;
4
+ onMenuClose(): void;
5
+ ref: React.Ref<HTMLButtonElement>;
6
+ };
7
+ export declare const MenuSwitch: ({ isMenuOpen, onMenuOpen, onMenuClose, ref }: MenuSwitchProps) => React.JSX.Element;
8
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { LinkProps as MenuBarLinkProps, LinkProps, SessionInfoProps as MenuBarSessionInfoProps, SessionInfoProps } from "./components/SessionInfo";
2
2
  export { DefaultLink, SessionInfo } from "./components/SessionInfo";
3
- export type { IMenu as IMenuBarMenu, IMenu, ISubmenu as IMenuBarSubmenu, ISubmenu, MenuNavigationPayload } from "./contents/MenuList";
3
+ export type { IMenu as IMenuBarMenu, IMenu, ISubmenu as IMenuBarSubmenu, ISubmenu, MenuBarPathMatch, MenuNavigationPayload } from "./contents/MenuList";
4
4
  export type { SubscribeMenuPinOptions } from "./events/pinEvent";
5
5
  export { dispatchMenuPinChange, getInitialPinState, MENU_PIN_CHANGE_EVENT, subscribeMenuPinChange, } from "./events/pinEvent";
6
6
  export type { UseMenuPinStateOptions } from "./hooks/useMenuPinState";