@aiquants/menu-bar 1.17.9 → 1.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.
@@ -1,3 +1,5 @@
1
+ import ExternalLink from "lucide-react/dist/esm/icons/external-link.js"
2
+ import User from "lucide-react/dist/esm/icons/user.js"
1
3
  import type { UserProfile } from "../types/user"
2
4
 
3
5
  const DEFAULT_USER_ICON =
@@ -48,32 +50,46 @@ export const SessionInfo = (props: SessionInfoProps): React.JSX.Element => {
48
50
  const { user, LinkComponent = DefaultLink } = props
49
51
 
50
52
  return (
51
- <div className="max-h-20 max-w-52">
52
- <div className="flex grow flex-col">
53
- {user ? (
54
- <>
55
- <div className="flex flex-row">
56
- <div className="aiquants-session-photo m-1 h-14 w-14 p-0.5">
57
- <img className="object-contain" src={user.photo || DEFAULT_USER_ICON} aria-label="profile photo" />
53
+ <div className="w-full">
54
+ {user ? (
55
+ <div className="aiquants-session-card group relative flex flex-col gap-1.5 rounded-lg border border-white/20 bg-white/10 p-2 shadow-sm transition-all duration-200 hover:border-white/30 hover:bg-white/15 dark:border-slate-700/60 dark:bg-slate-800/60 dark:hover:border-slate-600/80 dark:hover:bg-slate-800/90">
56
+ {/* アバター & ユーザー名 */}
57
+ <div className="flex min-w-0 items-center gap-2">
58
+ <div className="relative shrink-0">
59
+ <div className="h-7 w-7 overflow-hidden rounded-full border border-white/40 shadow-xs dark:border-slate-500">
60
+ <img className="h-full w-full rounded-full object-cover" src={user.photo || DEFAULT_USER_ICON} alt={user.displayName || "user profile"} />
58
61
  </div>
59
- <div className="mx-1 flex flex-col">
60
- <LinkComponent className="aiquants-session-link my-1 rounded-lg px-2 text-center text-xs" to="/">
61
- プロフィールを編集
62
- </LinkComponent>
63
- <LinkComponent className="aiquants-session-link rounded-lg px-2 text-center text-xs" to="/">
64
- プロフィール
65
- </LinkComponent>
66
- <LinkComponent className="aiquants-session-link my-1 rounded-lg px-2 text-center text-xs" to="https://myaccount.google.com/">
67
- Google Account
68
- </LinkComponent>
62
+ <span className="absolute -right-0.5 -bottom-0.5 h-2 w-2 rounded-full border border-slate-900 bg-emerald-400" title="Online" />
63
+ </div>
64
+ <div className="min-w-0 flex-1 leading-tight">
65
+ <div className="truncate font-semibold text-[11px] text-white tracking-tight dark:text-slate-100" title={user.displayName}>
66
+ {user.displayName}
69
67
  </div>
68
+ <div className="mt-0.5 text-[9px] text-white/70 tracking-tight dark:text-slate-400">Google Account</div>
70
69
  </div>
71
- <div className="aiquants-session-info mb-1 min-h-[1em] w-full rounded-r-lg px-2 py-0.5 text-xs">{user.displayName}</div>
72
- </>
73
- ) : (
74
- <div className="aiquants-session-info mb-1 min-h-[1em] w-9/12 rounded-r-lg py-0.5 pl-1 text-xs">ログインはこちら</div>
75
- )}
76
- </div>
70
+ </div>
71
+
72
+ {/* アクションボタン */}
73
+ <div className="grid grid-cols-2 gap-1 border-white/15 border-t pt-1 dark:border-slate-700/50">
74
+ <LinkComponent
75
+ className="flex items-center justify-center gap-1 rounded border border-white/15 bg-white/10 px-1.5 py-0.5 font-medium text-[10px] text-white transition-all duration-150 hover:bg-white/25 active:scale-95 dark:border-slate-600/50 dark:bg-slate-700/50 dark:text-slate-200 dark:hover:bg-slate-600/80"
76
+ to="/">
77
+ <User size={10} className="shrink-0 opacity-80" />
78
+ <span className="truncate">プロフィール</span>
79
+ </LinkComponent>
80
+ <LinkComponent
81
+ className="flex items-center justify-center gap-1 rounded border border-white/15 bg-white/10 px-1.5 py-0.5 font-medium text-[10px] text-white transition-all duration-150 hover:bg-white/25 active:scale-95 dark:border-slate-600/50 dark:bg-slate-700/50 dark:text-slate-200 dark:hover:bg-slate-600/80"
82
+ to="https://myaccount.google.com/">
83
+ <ExternalLink size={10} className="shrink-0 opacity-80" />
84
+ <span className="truncate">Google</span>
85
+ </LinkComponent>
86
+ </div>
87
+ </div>
88
+ ) : (
89
+ <div className="aiquants-session-card flex items-center justify-between rounded-lg border border-white/20 bg-white/10 p-2 shadow-xs dark:border-slate-700/60 dark:bg-slate-800/60">
90
+ <span className="font-medium text-white/90 text-xs dark:text-slate-200">ログインしてください</span>
91
+ </div>
92
+ )}
77
93
  </div>
78
94
  )
79
95
  }
@@ -4,6 +4,7 @@
4
4
  * @description このモジュールは、メニューバーのコンテンツを提供します。
5
5
  */
6
6
 
7
+ import LogOut from "lucide-react/dist/esm/icons/log-out.js"
7
8
  import Pin from "lucide-react/dist/esm/icons/pin.js"
8
9
  import PinOff from "lucide-react/dist/esm/icons/pin-off.js"
9
10
  import type { Theme } from "../types"
@@ -13,15 +14,6 @@ import { type IMenu, MenuList, type MenuNavigationPayload } from "./MenuList"
13
14
  * @interface IMenuContentProps
14
15
  * @description Props for the MenuContent component.
15
16
  * @description MenuContent コンポーネントの Props。
16
- * @property {IMenu[]} menuList - The list of menu items. メニュー項目のリスト。
17
- * @property {React.ReactNode} [menuHeader] - The header content of the menu. メニューのヘッダーコンテンツ。
18
- * @property {boolean} open - Whether the menu is open. メニューが開いているかどうか。
19
- * @property {(payload: MenuNavigationPayload) => void} onMenuSelected - Function to handle menu item selection. メニュー項目選択を処理する関数。
20
- * @property {() => void} onClickSignout - Function to handle sign out. サインアウトを処理する関数。
21
- * @property {Theme} theme - The current theme.
22
- * @property {() => void} toggleTheme - Function to toggle the theme.
23
- * @property {boolean} isPinned - Whether the menu is pinned open. メニューがピン留めされているかどうか。
24
- * @property {() => void} onTogglePin - Function to toggle pin state. ピン留め状態をトグルする関数。
25
17
  */
26
18
  interface IMenuContentProps {
27
19
  menuList: IMenu[]
@@ -47,36 +39,37 @@ export const MenuContent = (props: IMenuContentProps): React.JSX.Element => {
47
39
  const { menuList, menuHeader, open, onMenuSelected, onClickSignout, theme, toggleTheme, isPinned, onTogglePin, pinnedSubmenuIds, onToggleSubmenuPin } = props
48
40
 
49
41
  return (
50
- <div className={`aiquants-menu-content fixed top-0 right-0 z-2147483642 flex min-h-dvh w-[250px] select-none flex-col transition-transform duration-500 ${open ? "aiquants-menu-translate-x-0" : "aiquants-menu-translate-x-250"}`}>
51
- <div className="flex h-[100px] flex-col">{menuHeader}</div>
52
- <div className="flex grow flex-col">
42
+ <div className={`aiquants-menu-content fixed top-0 right-0 z-2147483642 flex min-h-dvh w-[240px] select-none flex-col transition-transform duration-500 ${open ? "aiquants-menu-translate-x-0" : "aiquants-menu-translate-x-250"}`}>
43
+ <div className="flex shrink-0 flex-col justify-center border-white/15 border-b px-2.5 py-2 dark:border-slate-700/60">{menuHeader}</div>
44
+ <div className="flex grow flex-col overflow-y-auto">
53
45
  <MenuList menuList={menuList} onMenuSelected={onMenuSelected} pinnedSubmenuIds={pinnedSubmenuIds} onToggleSubmenuPin={onToggleSubmenuPin} />
54
46
  </div>
55
- <div className="px-4 pt-2 pb-4">
56
- <div className="flex items-center justify-between gap-2">
47
+ <div className="border-white/15 border-t px-2.5 py-1.5 dark:border-slate-700/60">
48
+ <div className="flex items-center justify-between gap-1.5">
57
49
  <button
58
50
  type="button"
59
51
  onClick={onTogglePin}
60
52
  data-aqmb-pin-button
61
- className={`flex items-center justify-center rounded-md px-3 py-2 text-white text-xs shadow-sm transition-colors duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-300/60 ${isPinned ? "bg-amber-500/90 hover:bg-amber-600" : "border border-white/15 bg-white/10 hover:bg-white/20"}`}
53
+ className={`flex h-7 items-center justify-center rounded-md px-2 text-[11px] text-white shadow-2xs transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-300/60 ${isPinned ? "bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500" : "border border-white/20 bg-white/10 hover:bg-white/20 dark:border-slate-600/60 dark:bg-slate-700/60 dark:hover:bg-slate-600"}`}
62
54
  title={isPinned ? "ピン留め解除" : "ピン留め"}
63
55
  aria-label={isPinned ? "ピン留め解除" : "ピン留め"}>
64
- {isPinned ? <Pin size={14} /> : <PinOff size={14} />}
56
+ {isPinned ? <Pin size={12} /> : <PinOff size={12} />}
65
57
  </button>
66
58
  <button
67
59
  type="button"
68
60
  onClick={toggleTheme}
69
61
  data-aqmb-theme-button
70
- className="group relative flex items-center justify-center rounded-md border border-white/15 bg-white/10 px-3 py-2 text-white text-xs shadow-sm backdrop-blur-sm transition-all duration-300 hover:border-white/25 hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 focus-visible:ring-offset-0 dark:border-gray-600/40 dark:bg-gray-800/80 dark:hover:border-gray-500/60 dark:hover:bg-gray-700"
62
+ className="group relative flex h-7 items-center justify-center rounded-md border border-white/20 bg-white/10 px-2 text-[11px] text-white shadow-2xs backdrop-blur-xs transition-all duration-200 hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 dark:border-slate-600/60 dark:bg-slate-700/60 dark:hover:bg-slate-600"
71
63
  title={theme === "dark" ? "ライトモードに切り替え" : "ダークモードに切り替え"}
72
64
  aria-label={theme === "dark" ? "ライトモードに切り替え" : "ダークモードに切り替え"}>
73
- <span className="text-sm transition-transform duration-300 group-hover:scale-110">{theme === "dark" ? "🌞" : "🌙"}</span>
65
+ <span className="text-xs transition-transform duration-200 group-hover:scale-110">{theme === "dark" ? "🌞" : "🌙"}</span>
74
66
  </button>
75
67
  <button
76
68
  type="button"
77
69
  data-aqmb-signout-button
78
- className="aiquants-signout-button flex flex-1 items-center justify-center whitespace-nowrap rounded-md bg-red-500/90 px-3 py-2 font-medium text-white text-xs shadow-sm transition-colors duration-300 hover:bg-red-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-300/60"
70
+ className="aiquants-signout-button flex h-7 flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md bg-red-500/90 px-2.5 font-medium text-[11px] text-white shadow-2xs transition-colors duration-200 hover:bg-red-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-300/60 dark:bg-red-600/90 dark:hover:bg-red-500"
79
71
  onClick={onClickSignout}>
72
+ <LogOut size={11} className="shrink-0" />
80
73
  <span>サインアウト</span>
81
74
  </button>
82
75
  </div>
@@ -107,7 +107,7 @@ const StyledLinkText = ({ mark, children }: { mark?: string; children: ReactNode
107
107
  }
108
108
  return (
109
109
  <div className="flex flex-nowrap items-center">
110
- <Icon className="h-4 w-4" />
110
+ <Icon className="h-3.5 w-3.5 shrink-0" />
111
111
  <div className="aiquants-menu-link mx-1 hover:underline">{children}</div>
112
112
  </div>
113
113
  )
@@ -322,7 +322,7 @@ export const MenuList = (props: IMenuListProps): React.JSX.Element => {
322
322
  {menuList.map((item) => (
323
323
  <div key={item.id}>
324
324
  {item.href ? (
325
- <div className="aiquants-menu-list-item relative mb-1 ml-1 h-full rounded-l-2xl p-2 align-middle text-sm before:-mt-1 before:mr-2 before:inline-block before:h-4 before:w-4 before:rounded-full before:align-middle before:content-['']">
325
+ <div className="aiquants-menu-list-item relative mb-0.5 ml-1 flex items-center rounded-l-xl px-2.5 py-1 text-xs before:mr-2 before:inline-block before:h-2 before:w-2 before:shrink-0 before:rounded-full before:content-[''] sm:text-sm">
326
326
  {regexp.test(item.href) ? (
327
327
  <a
328
328
  href={item.href}
@@ -351,7 +351,7 @@ export const MenuList = (props: IMenuListProps): React.JSX.Element => {
351
351
  <details className="aiquants-menu-accordion-details" open={item.id === openMenuId || pinnedSubmenuIds.includes(item.id)}>
352
352
  {/** biome-ignore lint/a11y/noStaticElementInteractions: summary toggles submenu visibility */}
353
353
  <summary
354
- className="aiquants-menu-list-item aiquants-menu-accordion-summary relative mb-1 ml-1 h-full list-none rounded-l-2xl p-2 text-sm outline-none before:mr-2 before:inline-block before:h-4 before:w-4 before:rounded-full before:content-['']"
354
+ className="aiquants-menu-list-item aiquants-menu-accordion-summary relative mb-0.5 ml-1 list-none rounded-l-xl px-2.5 py-1 text-xs outline-none before:mr-2 before:inline-block before:h-2 before:w-2 before:shrink-0 before:rounded-full before:content-[''] sm:text-sm"
355
355
  onClick={(e) => {
356
356
  e.preventDefault()
357
357
  handleToggle(item.id)
@@ -374,12 +374,12 @@ export const MenuList = (props: IMenuListProps): React.JSX.Element => {
374
374
  }}
375
375
  className={`aiquants-submenu-pin-button ${pinnedSubmenuIds.includes(item.id) ? "pinned" : ""}`}
376
376
  title={pinnedSubmenuIds.includes(item.id) ? "ピン留め解除" : "ピン留め"}>
377
- {pinnedSubmenuIds.includes(item.id) ? <Pin size={14} /> : <PinOff size={14} />}
377
+ {pinnedSubmenuIds.includes(item.id) ? <Pin size={13} /> : <PinOff size={13} />}
378
378
  </button>
379
379
  </summary>
380
- <div className="aiquants-menu-submenu pointer-events-auto my-1 mr-1 ml-4 rounded p-2 shadow-md">
380
+ <div className="aiquants-menu-submenu pointer-events-auto my-0.5 mr-1 ml-3.5 rounded-lg px-2 py-1 shadow-sm">
381
381
  {item.submenu.map((subitem) => (
382
- <div key={subitem.subid} className="aiquants-menu-accordion-container block w-full whitespace-nowrap text-sm">
382
+ <div key={subitem.subid} className="aiquants-menu-accordion-container block w-full whitespace-nowrap py-0.5 text-xs">
383
383
  <SubMenuItem item={subitem} parentMenu={item} onMenuSelected={onMenuSelected} />
384
384
  </div>
385
385
  ))}
@@ -107,7 +107,7 @@ export const MenuSwitch = (props: IMenuSwitchProps): React.JSX.Element => {
107
107
  }, [open, styleOpen, styleClose])
108
108
 
109
109
  return (
110
- <button type="button" data-aqmb-menu-switch className="aiquants-menu-switch fixed z-2147483647 inline-block h-[27px] w-[27px] cursor-pointer align-middle outline-hidden" onClick={handleToggle} onFocus={handleFocus} onMouseDown={handleMouseDown} tabIndex={-1}>
110
+ <button type="button" data-aqmb-menu-switch className="aiquants-menu-switch inline-block h-[27px] w-[27px] cursor-pointer align-middle outline-hidden" onClick={handleToggle} onFocus={handleFocus} onMouseDown={handleMouseDown} tabIndex={-1}>
111
111
  <span className="absolute top-0 left-0 box-border inline-block h-[3px] w-full select-none" ref={refSpan1} />
112
112
  <span className="absolute top-1.5 left-0 box-border inline-block h-[3px] w-full select-none" ref={refSpan2} />
113
113
  <span className="absolute top-3 left-0 box-border inline-block h-[3px] w-full select-none" ref={refSpan3} />
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @module events/pinEvent
3
+ * @description Event handling and subscription helpers for menu pin state.
4
+ * @description メニューピン留め状態のイベント処理および購読ヘルパー。
5
+ */
6
+
7
+ import type { MenuPinChangeDetail, MenuPinCustomEvent } from "../types"
8
+
9
+ /**
10
+ * Canonical Window CustomEvent name for menu pin state changes.
11
+ * メニューのピン留め状態変更を通知する正準 CustomEvent 名。
12
+ */
13
+ export const MENU_PIN_CHANGE_EVENT = "aiquants-menu-pin-change"
14
+
15
+ /**
16
+ * Cached snapshot object reference to prevent unnecessary re-renders in useSyncExternalStore.
17
+ * 不要な再レンダリングを防止するためのキャッシュスナップショットオブジェクト参照。
18
+ */
19
+ let cachedSnapshot: MenuPinChangeDetail = { pinned: false, width: 0 }
20
+
21
+ /**
22
+ * Reads initial pin state safely from document CSS variable or localStorage.
23
+ * document の CSS 変数または localStorage から初期のピン留め状態を安全に読み取ります。
24
+ *
25
+ * @param defaultPinnedWidth - Default width in pixels when pinned (default: 240).
26
+ * @returns Current MenuPinChangeDetail object.
27
+ */
28
+ export const getInitialPinState = (defaultPinnedWidth = 240): MenuPinChangeDetail => {
29
+ if (typeof window === "undefined" || typeof document === "undefined") {
30
+ return { pinned: false, width: 0 }
31
+ }
32
+
33
+ try {
34
+ const cssVal = getComputedStyle(document.documentElement).getPropertyValue("--aiquants-menu-pinned-width").trim()
35
+
36
+ if (cssVal) {
37
+ const numericWidth = Number.parseInt(cssVal, 10)
38
+ if (!Number.isNaN(numericWidth)) {
39
+ const pinned = numericWidth > 0
40
+ const width = pinned ? numericWidth || defaultPinnedWidth : 0
41
+ if (cachedSnapshot.pinned === pinned && cachedSnapshot.width === width) {
42
+ return cachedSnapshot
43
+ }
44
+ cachedSnapshot = { pinned, width }
45
+ return cachedSnapshot
46
+ }
47
+ }
48
+
49
+ const saved = localStorage.getItem("aiquants-menu-pinned")
50
+ const pinned = saved === "true"
51
+ const width = pinned ? defaultPinnedWidth : 0
52
+ if (cachedSnapshot.pinned === pinned && cachedSnapshot.width === width) {
53
+ return cachedSnapshot
54
+ }
55
+ cachedSnapshot = { pinned, width }
56
+ return cachedSnapshot
57
+ } catch {
58
+ return { pinned: false, width: 0 }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Dispatches a CustomEvent on window with menu pin state details.
64
+ * window に対してメニューピン留め状態変更カスタムイベントを発行します。
65
+ *
66
+ * @param pinned - Whether the menu is pinned.
67
+ * @param width - The pinned width in pixels.
68
+ */
69
+ export const dispatchMenuPinChange = (pinned: boolean, width: number): void => {
70
+ if (typeof window === "undefined") return
71
+
72
+ const detail: MenuPinChangeDetail = { pinned, width }
73
+ if (cachedSnapshot.pinned !== pinned || cachedSnapshot.width !== width) {
74
+ cachedSnapshot = detail
75
+ }
76
+
77
+ const event: MenuPinCustomEvent = new CustomEvent(MENU_PIN_CHANGE_EVENT, {
78
+ detail,
79
+ bubbles: true,
80
+ cancelable: false,
81
+ })
82
+ window.dispatchEvent(event)
83
+ }
84
+
85
+ /**
86
+ * Options for subscribing to menu pin state changes.
87
+ * メニューピン留め状態購読時のオプション。
88
+ */
89
+ export interface SubscribeMenuPinOptions {
90
+ /** Whether to immediately invoke the listener with the current state upon subscription (default: true). 購読開始時に即座に現在の状態を通知するかどうか。 */
91
+ immediate?: boolean
92
+ /** Default width when pinned (default: 240). ピン留め時のデフォルト幅。 */
93
+ defaultPinnedWidth?: number
94
+ }
95
+
96
+ /**
97
+ * Subscribes a listener to menu pin state changes on window.
98
+ * Returns an unsubscription cleanup function.
99
+ * window 上のメニューピン留め状態変更イベントを購読します。
100
+ * クリーンアップ関数を返します。
101
+ *
102
+ * @param listener - Callback invoked on pin state changes.
103
+ * @param options - Subscription options (e.g. immediate notification).
104
+ * @returns Cleanup function to unsubscribe.
105
+ */
106
+ export const subscribeMenuPinChange = (listener: (detail: MenuPinChangeDetail) => void, options?: SubscribeMenuPinOptions): (() => void) => {
107
+ if (typeof window === "undefined") {
108
+ return () => {}
109
+ }
110
+
111
+ const { immediate = true, defaultPinnedWidth = 240 } = options ?? {}
112
+
113
+ if (immediate) {
114
+ const currentState = getInitialPinState(defaultPinnedWidth)
115
+ listener(currentState)
116
+ }
117
+
118
+ const handler = (e: Event) => {
119
+ const customEvent = e as MenuPinCustomEvent
120
+ if (customEvent.detail) {
121
+ listener(customEvent.detail)
122
+ }
123
+ }
124
+
125
+ window.addEventListener(MENU_PIN_CHANGE_EVENT, handler, { passive: true })
126
+ return () => {
127
+ window.removeEventListener(MENU_PIN_CHANGE_EVENT, handler)
128
+ }
129
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @module hooks/useMenuPinState
3
+ * @description Custom React hook to subscribe to menu pin state changes using useSyncExternalStore.
4
+ * @description useSyncExternalStore を用いてメニューのピン留め状態および幅の変化をシームレスに購読するカスタム React フック。
5
+ */
6
+
7
+ import { useCallback, useSyncExternalStore } from "react"
8
+ import { getInitialPinState, subscribeMenuPinChange } from "../events/pinEvent"
9
+ import type { MenuPinChangeDetail } from "../types"
10
+
11
+ /**
12
+ * Options for the useMenuPinState hook.
13
+ * useMenuPinState フックのオプション。
14
+ */
15
+ export interface UseMenuPinStateOptions {
16
+ /** Initial pinned state for server-side rendering (default: false). SSR 時の初期ピン留め状態。 */
17
+ initialPinned?: boolean
18
+ /** Default width when pinned in pixels (default: 240). デフォルトのピン留め幅。 */
19
+ defaultPinnedWidth?: number
20
+ }
21
+
22
+ /**
23
+ * React hook for consuming components to seamlessly subscribe to menu pin state changes.
24
+ * Hydration-safe, re-render optimized via React 18 useSyncExternalStore.
25
+ * 利用側 React コンポーネントがメニューのピン留め状態および幅をシームレスに購読するためのカスタムフック。
26
+ *
27
+ * @param options - Optional initial pin state and width configuration.
28
+ * @returns Current MenuPinChangeDetail ({ pinned: boolean, width: number }).
29
+ */
30
+ export const useMenuPinState = (options?: UseMenuPinStateOptions): MenuPinChangeDetail => {
31
+ const defaultPinnedWidth = options?.defaultPinnedWidth ?? 240
32
+ const initialPinned = options?.initialPinned ?? false
33
+
34
+ const subscribe = useCallback(
35
+ (onStoreChange: () => void) => {
36
+ return subscribeMenuPinChange(
37
+ () => {
38
+ onStoreChange()
39
+ },
40
+ { immediate: false, defaultPinnedWidth },
41
+ )
42
+ },
43
+ [defaultPinnedWidth],
44
+ )
45
+
46
+ const getSnapshot = useCallback(() => {
47
+ return getInitialPinState(defaultPinnedWidth)
48
+ }, [defaultPinnedWidth])
49
+
50
+ const getServerSnapshot = useCallback((): MenuPinChangeDetail => {
51
+ return {
52
+ pinned: initialPinned,
53
+ width: initialPinned ? defaultPinnedWidth : 0,
54
+ }
55
+ }, [initialPinned, defaultPinnedWidth])
56
+
57
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
58
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,29 @@
1
- export type { LinkProps, SessionInfoProps } from "./components/SessionInfo"
1
+ /**
2
+ * @module index
3
+ * @description Entry point for @aiquants/menu-bar package exports.
4
+ * @description @aiquants/menu-bar パッケージのエクスポートエントリーポイント。
5
+ */
6
+
7
+ /** @deprecated Use MenuBarLinkProps instead to avoid namespace collisions. */
8
+ export type { LinkProps as MenuBarLinkProps, LinkProps, SessionInfoProps as MenuBarSessionInfoProps, SessionInfoProps } from "./components/SessionInfo"
2
9
  export { DefaultLink, SessionInfo } from "./components/SessionInfo"
3
- export type { IMenu, ISubmenu, MenuNavigationPayload } from "./contents/MenuList"
10
+ /** @deprecated Use IMenuBarMenu instead to avoid namespace collisions. */
11
+ /** @deprecated Use IMenuBarSubmenu instead to avoid namespace collisions. */
12
+ export type { IMenu as IMenuBarMenu, IMenu, ISubmenu as IMenuBarSubmenu, ISubmenu, MenuNavigationPayload } from "./contents/MenuList"
13
+ export type { SubscribeMenuPinOptions } from "./events/pinEvent"
14
+ export {
15
+ dispatchMenuPinChange,
16
+ getInitialPinState,
17
+ MENU_PIN_CHANGE_EVENT,
18
+ subscribeMenuPinChange,
19
+ } from "./events/pinEvent"
20
+
21
+ export type { UseMenuPinStateOptions } from "./hooks/useMenuPinState"
22
+ export { useMenuPinState } from "./hooks/useMenuPinState"
23
+
24
+ export type { IMenuBarProps } from "./MenuBar"
4
25
  export { MenuBar } from "./MenuBar"
5
- export type { Theme } from "./types"
6
- export type { UserProfile } from "./types/user"
26
+ /** @deprecated Use MenuBarTheme instead to avoid namespace collisions. */
27
+ export type { MenuPinChangeDetail, MenuPinCustomEvent, Theme as MenuBarTheme, Theme } from "./types"
28
+ /** @deprecated Use MenuBarUserProfile instead to avoid namespace collisions. */
29
+ export type { UserProfile as MenuBarUserProfile, UserProfile } from "./types/user"
@@ -3,10 +3,19 @@
3
3
 
4
4
  :root {
5
5
  --aiquants-menu-pinned-width: 0px;
6
+
7
+ /* 中立なメニュースイッチのカスタムプロパティ・デフォルト値 */
8
+ --aqmb-switch-position: fixed;
9
+ --aqmb-switch-top: calc(var(--spacing, 0.25rem) * 2);
10
+ --aqmb-switch-right: calc(var(--spacing, 0.25rem) * 2);
11
+ --aqmb-switch-left: auto;
12
+ --aqmb-switch-bottom: auto;
13
+ --aqmb-switch-z-index: 2147483647;
6
14
  }
7
15
 
8
16
  /* メニューバーコンテナ内でのみTailwindスタイルを適用 */
9
17
  .aiquants-menu-bar {
18
+ position: relative;
10
19
  /* 基本的なフォントスタイル */
11
20
  font-size: 1rem;
12
21
  line-height: 1.5;
@@ -118,29 +127,9 @@
118
127
  height: 0;
119
128
  }
120
129
 
121
- /* ダークモード時のSessionInfo */
122
- .aiquants-menu-bar.dark .aiquants-session-photo {
123
- background-color: rgb(39 39 42); /* zinc-800 */
124
- }
125
-
126
- .aiquants-menu-bar.dark .aiquants-session-link,
127
- .aiquants-menu-bar.dark .aiquants-session-info {
128
- background-color: rgb(51 65 85); /* slate-700 */
129
- color: rgb(226 232 240); /* slate-200 */
130
- }
131
-
132
- /* ライトモード時のSessionInfo */
133
- .aiquants-menu-bar.light .aiquants-session-photo,
134
- .aiquants-menu-bar .aiquants-session-photo {
135
- background-color: rgb(250 250 250); /* zinc-50 */
136
- }
137
-
138
- .aiquants-menu-bar.light .aiquants-session-link,
139
- .aiquants-menu-bar .aiquants-session-link,
140
- .aiquants-menu-bar.light .aiquants-session-info,
141
- .aiquants-menu-bar .aiquants-session-info {
142
- background-color: rgb(220 252 231); /* green-100 */
143
- color: rgb(0 0 0); /* black */
130
+ /* SessionInfo グラスカード用基本スタイル */
131
+ .aiquants-menu-bar .aiquants-session-card {
132
+ backdrop-filter: blur(8px);
144
133
  }
145
134
 
146
135
  /* アコーディオンメニューの詳細部分のスタイル */
@@ -171,8 +160,8 @@
171
160
  display: inline-flex;
172
161
  align-items: center;
173
162
  justify-content: center;
174
- padding: 0.25rem;
175
- border-radius: 0.375rem;
163
+ padding: 0.15rem;
164
+ border-radius: 0.25rem;
176
165
  transition: all 0.3s;
177
166
  }
178
167
 
@@ -226,13 +215,17 @@
226
215
  }
227
216
 
228
217
  .aiquants-menu-switch {
229
- top: calc(var(--spacing) * 2);
230
- right: calc(var(--spacing) * 2);
218
+ position: var(--aqmb-switch-position, fixed);
219
+ top: var(--aqmb-switch-top, calc(var(--spacing, 0.25rem) * 2));
220
+ right: var(--aqmb-switch-right, calc(var(--spacing, 0.25rem) * 2));
221
+ left: var(--aqmb-switch-left, auto);
222
+ bottom: var(--aqmb-switch-bottom, auto);
223
+ z-index: var(--aqmb-switch-z-index, 2147483647);
231
224
  }
232
225
 
233
226
  @media (width >= 40rem) {
234
227
  .aiquants-menu-switch {
235
- top: calc(var(--spacing) * 3);
236
- right: calc(var(--spacing) * 3);
228
+ top: var(--aqmb-switch-top, calc(var(--spacing, 0.25rem) * 3));
229
+ right: var(--aqmb-switch-right, calc(var(--spacing, 0.25rem) * 3));
237
230
  }
238
231
  }
package/src/types.ts CHANGED
@@ -1 +1,24 @@
1
+ /**
2
+ * @module types
3
+ * @description Common type definitions for @aiquants/menu-bar.
4
+ * @description @aiquants/menu-bar の共通型定義。
5
+ */
6
+
1
7
  export type Theme = "light" | "dark"
8
+
9
+ /**
10
+ * Menu pin change event payload detail interface.
11
+ * メニューピン留め状態変更イベントの詳細ペイロード。
12
+ */
13
+ export interface MenuPinChangeDetail {
14
+ /** Whether the menu is currently pinned open. メニューが現在ピン留めされているかどうか。 */
15
+ pinned: boolean
16
+ /** Pinned width in pixels (e.g. 240 or 0). ピン留め時の幅 (ピクセル単位)。 */
17
+ width: number
18
+ }
19
+
20
+ /**
21
+ * Custom Event type for menu pin change events.
22
+ * メニューピン留め状態変更カスタムイベントの型。
23
+ */
24
+ export type MenuPinCustomEvent = CustomEvent<MenuPinChangeDetail>