@open-mercato/ui 0.6.2-develop.3372.1.de9a169973 → 0.6.2-develop.3380.1.bb14509041
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/ai/AiIcon.js +1 -1
- package/dist/ai/AiIcon.js.map +2 -2
- package/dist/backend/AppShell.js +223 -153
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +101 -79
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/notifications/NotificationCountBadge.js +1 -1
- package/dist/backend/notifications/NotificationCountBadge.js.map +2 -2
- package/dist/backend/notifications/NotificationItem.js +67 -50
- package/dist/backend/notifications/NotificationItem.js.map +2 -2
- package/dist/backend/notifications/NotificationPanel.js +108 -85
- package/dist/backend/notifications/NotificationPanel.js.map +2 -2
- package/dist/primitives/breadcrumb.js +161 -0
- package/dist/primitives/breadcrumb.js.map +7 -0
- package/dist/primitives/sheet.js +114 -0
- package/dist/primitives/sheet.js.map +7 -0
- package/package.json +3 -3
- package/src/ai/AiIcon.tsx +1 -1
- package/src/backend/AppShell.tsx +151 -49
- package/src/backend/ProfileDropdown.tsx +115 -88
- package/src/backend/__tests__/AppShell.test.tsx +8 -0
- package/src/backend/notifications/NotificationCountBadge.tsx +1 -1
- package/src/backend/notifications/NotificationItem.tsx +97 -104
- package/src/backend/notifications/NotificationPanel.tsx +126 -102
- package/src/primitives/__tests__/breadcrumb.test.tsx +298 -0
- package/src/primitives/__tests__/sheet.test.tsx +152 -0
- package/src/primitives/breadcrumb.tsx +185 -0
- package/src/primitives/sheet.tsx +165 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { User, LogOut, Bell, Moon, Sun, Globe, Key, Check } from "lucide-react";
|
|
5
|
+
import { User, LogOut, Bell, Moon, Sun, Globe, Key, Check, ChevronRight } from "lucide-react";
|
|
6
6
|
import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { locales } from "@open-mercato/shared/lib/i18n/config";
|
|
8
8
|
import { useTheme } from "@open-mercato/ui/theme";
|
|
9
|
-
import {
|
|
9
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
10
10
|
import { IconButton } from "../primitives/icon-button.js";
|
|
11
|
+
import { Switch } from "../primitives/switch.js";
|
|
11
12
|
import { useInjectedMenuItems } from "./injection/useInjectedMenuItems.js";
|
|
12
13
|
import { mergeMenuItems } from "./injection/mergeMenuItems.js";
|
|
13
14
|
import { resolveInjectedIcon } from "./injection/resolveInjectedIcon.js";
|
|
@@ -78,7 +79,12 @@ function ProfileDropdown({
|
|
|
78
79
|
} catch {
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
|
-
const menuItemClass =
|
|
82
|
+
const menuItemClass = cn(
|
|
83
|
+
"group flex h-9 w-full items-center gap-3 rounded-md px-2.5 text-sm text-foreground",
|
|
84
|
+
"transition-colors hover:bg-muted/60 focus:outline-none focus-visible:bg-muted/60",
|
|
85
|
+
"cursor-pointer"
|
|
86
|
+
);
|
|
87
|
+
const menuIconClass = "size-4 shrink-0 text-muted-foreground group-hover:text-foreground";
|
|
82
88
|
const resolveMenuLabel = React.useCallback(
|
|
83
89
|
(item) => {
|
|
84
90
|
if (item.labelKey && item.label) return t(item.labelKey, item.label);
|
|
@@ -113,8 +119,12 @@ function ProfileDropdown({
|
|
|
113
119
|
(item) => {
|
|
114
120
|
const label = resolveMenuLabel(item);
|
|
115
121
|
const icon = resolveInjectedIcon(item.icon);
|
|
122
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
123
|
+
/* @__PURE__ */ jsx("span", { className: menuIconClass, children: icon }),
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: label })
|
|
125
|
+
] });
|
|
116
126
|
if (item.href) {
|
|
117
|
-
return /* @__PURE__ */
|
|
127
|
+
return /* @__PURE__ */ jsx(
|
|
118
128
|
Link,
|
|
119
129
|
{
|
|
120
130
|
href: item.href,
|
|
@@ -122,36 +132,28 @@ function ProfileDropdown({
|
|
|
122
132
|
role: "menuitem",
|
|
123
133
|
"data-menu-item-id": item.id,
|
|
124
134
|
onClick: () => setOpen(false),
|
|
125
|
-
children:
|
|
126
|
-
icon,
|
|
127
|
-
/* @__PURE__ */ jsx("span", { children: label })
|
|
128
|
-
]
|
|
135
|
+
children: inner
|
|
129
136
|
},
|
|
130
137
|
item.id
|
|
131
138
|
);
|
|
132
139
|
}
|
|
133
|
-
return /* @__PURE__ */
|
|
134
|
-
|
|
140
|
+
return /* @__PURE__ */ jsx(
|
|
141
|
+
"button",
|
|
135
142
|
{
|
|
136
143
|
type: "button",
|
|
137
|
-
|
|
138
|
-
size: "sm",
|
|
139
|
-
className: "w-full justify-start",
|
|
144
|
+
className: menuItemClass,
|
|
140
145
|
role: "menuitem",
|
|
141
146
|
"data-menu-item-id": item.id,
|
|
142
147
|
onClick: () => {
|
|
143
148
|
item.onClick?.();
|
|
144
149
|
setOpen(false);
|
|
145
150
|
},
|
|
146
|
-
children:
|
|
147
|
-
icon,
|
|
148
|
-
/* @__PURE__ */ jsx("span", { children: label })
|
|
149
|
-
]
|
|
151
|
+
children: inner
|
|
150
152
|
},
|
|
151
153
|
item.id
|
|
152
154
|
);
|
|
153
155
|
},
|
|
154
|
-
[menuItemClass, resolveMenuLabel]
|
|
156
|
+
[menuItemClass, menuIconClass, resolveMenuLabel]
|
|
155
157
|
);
|
|
156
158
|
const renderBuiltInItem = React.useCallback(
|
|
157
159
|
(id) => {
|
|
@@ -164,8 +166,8 @@ function ProfileDropdown({
|
|
|
164
166
|
role: "menuitem",
|
|
165
167
|
onClick: () => setOpen(false),
|
|
166
168
|
children: [
|
|
167
|
-
/* @__PURE__ */ jsx(Key, { className:
|
|
168
|
-
/* @__PURE__ */ jsx("span", { children: t("ui.profileMenu.changePassword", "Change Password") })
|
|
169
|
+
/* @__PURE__ */ jsx(Key, { className: menuIconClass }),
|
|
170
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: t("ui.profileMenu.changePassword", "Change Password") })
|
|
169
171
|
]
|
|
170
172
|
},
|
|
171
173
|
id
|
|
@@ -180,66 +182,86 @@ function ProfileDropdown({
|
|
|
180
182
|
role: "menuitem",
|
|
181
183
|
onClick: () => setOpen(false),
|
|
182
184
|
children: [
|
|
183
|
-
/* @__PURE__ */ jsx(Bell, { className:
|
|
184
|
-
/* @__PURE__ */ jsx("span", { children: t("ui.profileMenu.notifications", "Notification Preferences") })
|
|
185
|
+
/* @__PURE__ */ jsx(Bell, { className: menuIconClass }),
|
|
186
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: t("ui.profileMenu.notifications", "Notification Preferences") })
|
|
185
187
|
]
|
|
186
188
|
},
|
|
187
189
|
id
|
|
188
190
|
);
|
|
189
191
|
}
|
|
190
192
|
if (id === "theme-toggle") {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
if (!mounted) return null;
|
|
194
|
+
return /* @__PURE__ */ jsxs(
|
|
195
|
+
"div",
|
|
193
196
|
{
|
|
194
|
-
|
|
195
|
-
variant: "ghost",
|
|
196
|
-
size: "sm",
|
|
197
|
-
className: "w-full justify-between",
|
|
197
|
+
className: cn(menuItemClass, "justify-between"),
|
|
198
198
|
role: "menuitem",
|
|
199
199
|
onClick: handleThemeToggle,
|
|
200
|
+
onKeyDown: (e) => {
|
|
201
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
handleThemeToggle();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
tabIndex: 0,
|
|
200
207
|
children: [
|
|
201
|
-
/* @__PURE__ */ jsxs("span", { className: "
|
|
202
|
-
isDark ? /* @__PURE__ */ jsx(Moon, { className:
|
|
203
|
-
/* @__PURE__ */ jsx("span", { children: t("ui.profileMenu.theme", "
|
|
208
|
+
/* @__PURE__ */ jsxs("span", { className: "flex flex-1 items-center gap-3", children: [
|
|
209
|
+
isDark ? /* @__PURE__ */ jsx(Moon, { className: menuIconClass }) : /* @__PURE__ */ jsx(Sun, { className: menuIconClass }),
|
|
210
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: t("ui.profileMenu.theme", "Theme") })
|
|
204
211
|
] }),
|
|
205
|
-
/* @__PURE__ */ jsx(
|
|
212
|
+
/* @__PURE__ */ jsx(
|
|
213
|
+
Switch,
|
|
214
|
+
{
|
|
215
|
+
checked: isDark,
|
|
216
|
+
onCheckedChange: handleThemeToggle,
|
|
217
|
+
"aria-label": t("ui.profileMenu.theme", "Theme"),
|
|
218
|
+
onClick: (e) => e.stopPropagation()
|
|
219
|
+
}
|
|
220
|
+
)
|
|
206
221
|
]
|
|
207
222
|
},
|
|
208
223
|
id
|
|
209
|
-
)
|
|
224
|
+
);
|
|
210
225
|
}
|
|
211
226
|
if (id === "language") {
|
|
212
|
-
return /* @__PURE__ */ jsxs("div", { className: "
|
|
227
|
+
return /* @__PURE__ */ jsxs("div", { className: "contents", children: [
|
|
213
228
|
/* @__PURE__ */ jsxs(
|
|
214
|
-
|
|
229
|
+
"button",
|
|
215
230
|
{
|
|
216
231
|
type: "button",
|
|
217
|
-
|
|
218
|
-
size: "sm",
|
|
219
|
-
className: "w-full justify-between",
|
|
232
|
+
className: menuItemClass,
|
|
220
233
|
role: "menuitem",
|
|
221
234
|
onClick: () => setLanguageOpen(!languageOpen),
|
|
222
235
|
"aria-expanded": languageOpen,
|
|
223
236
|
children: [
|
|
224
|
-
/* @__PURE__ */
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
237
|
+
/* @__PURE__ */ jsx(Globe, { className: menuIconClass }),
|
|
238
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: t("ui.profileMenu.language", "Language") }),
|
|
239
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: localeLabels[currentLocale] }),
|
|
240
|
+
/* @__PURE__ */ jsx(
|
|
241
|
+
ChevronRight,
|
|
242
|
+
{
|
|
243
|
+
className: cn(
|
|
244
|
+
"ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform",
|
|
245
|
+
languageOpen && "rotate-90"
|
|
246
|
+
),
|
|
247
|
+
"aria-hidden": "true"
|
|
248
|
+
}
|
|
249
|
+
)
|
|
229
250
|
]
|
|
230
251
|
}
|
|
231
252
|
),
|
|
232
|
-
languageOpen && /* @__PURE__ */ jsx("div", { className: "
|
|
233
|
-
|
|
253
|
+
languageOpen && /* @__PURE__ */ jsx("div", { className: "ml-7 mr-1 flex flex-col gap-0.5 border-l pl-2 py-1", children: locales.map((locale) => /* @__PURE__ */ jsxs(
|
|
254
|
+
"button",
|
|
234
255
|
{
|
|
235
256
|
type: "button",
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
257
|
+
className: cn(
|
|
258
|
+
"flex h-8 w-full items-center justify-between gap-2 rounded-md px-2 text-sm transition-colors hover:bg-muted/60 focus:outline-none focus-visible:bg-muted/60",
|
|
259
|
+
locale === currentLocale ? "font-medium text-foreground" : "text-muted-foreground"
|
|
260
|
+
),
|
|
239
261
|
onClick: () => handleLocaleChange(locale),
|
|
240
262
|
children: [
|
|
241
|
-
/* @__PURE__ */ jsx("span", { children: localeLabels[locale] }),
|
|
242
|
-
locale === currentLocale && /* @__PURE__ */ jsx(Check, { className: "size-3.5" })
|
|
263
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: localeLabels[locale] }),
|
|
264
|
+
locale === currentLocale && /* @__PURE__ */ jsx(Check, { className: "size-3.5 shrink-0 text-accent-indigo", "aria-hidden": "true" })
|
|
243
265
|
]
|
|
244
266
|
},
|
|
245
267
|
locale
|
|
@@ -248,16 +270,14 @@ function ProfileDropdown({
|
|
|
248
270
|
}
|
|
249
271
|
if (id === "sign-out") {
|
|
250
272
|
return /* @__PURE__ */ jsx("form", { action: "/api/auth/logout", method: "POST", children: /* @__PURE__ */ jsxs(
|
|
251
|
-
|
|
273
|
+
"button",
|
|
252
274
|
{
|
|
253
|
-
variant: "ghost",
|
|
254
|
-
size: "sm",
|
|
255
|
-
className: "w-full justify-start",
|
|
256
275
|
type: "submit",
|
|
276
|
+
className: menuItemClass,
|
|
257
277
|
role: "menuitem",
|
|
258
278
|
children: [
|
|
259
|
-
/* @__PURE__ */ jsx(LogOut, { className:
|
|
260
|
-
/* @__PURE__ */ jsx("span", { children: t("ui.userMenu.logout", "Sign Out") })
|
|
279
|
+
/* @__PURE__ */ jsx(LogOut, { className: menuIconClass }),
|
|
280
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-left", children: t("ui.userMenu.logout", "Sign Out") })
|
|
261
281
|
]
|
|
262
282
|
}
|
|
263
283
|
) }, id);
|
|
@@ -271,6 +291,7 @@ function ProfileDropdown({
|
|
|
271
291
|
isDark,
|
|
272
292
|
languageOpen,
|
|
273
293
|
menuItemClass,
|
|
294
|
+
menuIconClass,
|
|
274
295
|
mounted,
|
|
275
296
|
notificationsHref,
|
|
276
297
|
t
|
|
@@ -295,33 +316,34 @@ function ProfileDropdown({
|
|
|
295
316
|
"div",
|
|
296
317
|
{
|
|
297
318
|
ref: menuRef,
|
|
298
|
-
className: "absolute right-0 top-full mt-
|
|
319
|
+
className: "absolute right-0 top-full z-popover mt-2 w-64 overflow-hidden rounded-lg border bg-popover p-0 shadow-lg",
|
|
299
320
|
role: "menu",
|
|
300
321
|
"data-testid": "profile-dropdown",
|
|
301
322
|
children: [
|
|
302
|
-
(displayName || email) && /* @__PURE__ */ jsxs("div", { className: "
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
displayName
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
" ",
|
|
311
|
-
email
|
|
323
|
+
(displayName || email) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 border-b bg-muted/30 px-3 py-3", children: [
|
|
324
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-indigo/10 text-accent-indigo", children: /* @__PURE__ */ jsx(User, { className: "size-4", "aria-hidden": "true" }) }),
|
|
325
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
326
|
+
displayName ? /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium leading-5 text-foreground", children: displayName }) : null,
|
|
327
|
+
email ? /* @__PURE__ */ jsx("span", { className: cn(
|
|
328
|
+
"truncate text-xs leading-4",
|
|
329
|
+
displayName ? "text-muted-foreground" : "text-foreground font-medium"
|
|
330
|
+
), children: email }) : null,
|
|
331
|
+
!displayName && email ? /* @__PURE__ */ jsx("span", { className: "truncate text-overline uppercase tracking-wider text-muted-foreground/80", children: t("ui.userMenu.loggedInAs", "Logged in") }) : null
|
|
312
332
|
] })
|
|
313
333
|
] }),
|
|
314
|
-
|
|
315
|
-
item
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
334
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col p-1.5", children: [
|
|
335
|
+
mergedMenuItems.map((item) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
336
|
+
item.separator ? /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-border", "aria-hidden": "true" }) : null,
|
|
337
|
+
item.source === "injected" ? item.href || item.onClick || item.label || item.labelKey ? renderInjectedItem(item) : null : renderBuiltInItem(item.id)
|
|
338
|
+
] }, item.id)),
|
|
339
|
+
/* @__PURE__ */ jsx(
|
|
340
|
+
InjectionSpot,
|
|
341
|
+
{
|
|
342
|
+
spotId: BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID,
|
|
343
|
+
context: injectionContext
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
] })
|
|
325
347
|
]
|
|
326
348
|
}
|
|
327
349
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/ProfileDropdown.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { User, LogOut, Bell, Moon, Sun, Globe, Key, Check } from 'lucide-react'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport { useTheme } from '@open-mercato/ui/theme'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { useInjectedMenuItems } from './injection/useInjectedMenuItems'\nimport { mergeMenuItems, type MergedMenuItem } from './injection/mergeMenuItems'\nimport { resolveInjectedIcon } from './injection/resolveInjectedIcon'\nimport { InjectionSpot } from './injection/InjectionSpot'\nimport { BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID } from './injection/spotIds'\n\nexport type ProfileDropdownProps = {\n email?: string\n displayName?: string\n changePasswordHref?: string\n notificationsHref?: string\n}\n\nconst localeLabels: Record<Locale, string> = {\n en: 'English',\n de: 'Deutsch',\n es: 'Espa\u00F1ol',\n pl: 'Polski',\n}\n\nexport function ProfileDropdown({\n email,\n displayName,\n changePasswordHref = '/backend/profile/change-password',\n notificationsHref,\n}: ProfileDropdownProps) {\n const t = useT()\n const currentLocale = useLocale()\n const { resolvedTheme, setTheme } = useTheme()\n const [open, setOpen] = React.useState(false)\n const [languageOpen, setLanguageOpen] = React.useState(false)\n const [mounted, setMounted] = React.useState(false)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const menuRef = React.useRef<HTMLDivElement>(null)\n const { items: injectedItems } = useInjectedMenuItems('menu:topbar:profile-dropdown')\n\n React.useEffect(() => {\n setMounted(true)\n }, [])\n\n const isDark = resolvedTheme === 'dark'\n\n // Close on click outside\n React.useEffect(() => {\n if (!open) return\n function handleClick(event: MouseEvent) {\n if (\n menuRef.current &&\n !menuRef.current.contains(event.target as Node) &&\n buttonRef.current &&\n !buttonRef.current.contains(event.target as Node)\n ) {\n setOpen(false)\n setLanguageOpen(false)\n }\n }\n document.addEventListener('mousedown', handleClick)\n return () => document.removeEventListener('mousedown', handleClick)\n }, [open])\n\n // Close on Escape\n React.useEffect(() => {\n if (!open) return\n function handleKeyDown(event: KeyboardEvent) {\n if (event.key === 'Escape') {\n if (languageOpen) {\n setLanguageOpen(false)\n } else {\n setOpen(false)\n buttonRef.current?.focus()\n }\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, languageOpen])\n\n const handleThemeToggle = () => {\n setTheme(isDark ? 'light' : 'dark')\n }\n\n const handleLocaleChange = async (locale: Locale) => {\n try {\n await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n window.location.reload()\n } catch {}\n }\n\n const menuItemClass =\n 'w-full text-left text-sm cursor-pointer px-3 py-2 rounded hover:bg-accent inline-flex items-center gap-2.5 outline-none focus-visible:ring-1 focus-visible:ring-ring'\n\n const resolveMenuLabel = React.useCallback(\n (item: Pick<MergedMenuItem, 'id' | 'label' | 'labelKey'>): string => {\n if (item.labelKey && item.label) return t(item.labelKey, item.label)\n if (item.labelKey) return t(item.labelKey, item.id)\n if (item.label && item.label.includes('.')) return t(item.label, item.id)\n return item.label ?? item.id\n },\n [t],\n )\n\n const builtInMenuItems = React.useMemo(\n () => {\n const items: Array<{ id: string; separator?: boolean }> = [{ id: 'change-password' }]\n if (notificationsHref) items.push({ id: 'notifications' })\n items.push({ id: 'theme-toggle', separator: true }, { id: 'language' }, { id: 'sign-out', separator: true })\n return items\n },\n [notificationsHref],\n )\n\n const mergedMenuItems = React.useMemo(\n () => mergeMenuItems(builtInMenuItems, injectedItems),\n [builtInMenuItems, injectedItems],\n )\n const injectionContext = React.useMemo(\n () => ({\n email,\n displayName,\n locale: currentLocale,\n }),\n [currentLocale, displayName, email],\n )\n\n const renderInjectedItem = React.useCallback(\n (item: MergedMenuItem) => {\n const label = resolveMenuLabel(item)\n const icon = resolveInjectedIcon(item.icon)\n if (item.href) {\n return (\n <Link\n key={item.id}\n href={item.href}\n className={menuItemClass}\n role=\"menuitem\"\n data-menu-item-id={item.id}\n onClick={() => setOpen(false)}\n >\n {icon}\n <span>{label}</span>\n </Link>\n )\n }\n return (\n <Button\n key={item.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n role=\"menuitem\"\n data-menu-item-id={item.id}\n onClick={() => {\n item.onClick?.()\n setOpen(false)\n }}\n >\n {icon}\n <span>{label}</span>\n </Button>\n )\n },\n [menuItemClass, resolveMenuLabel],\n )\n\n const renderBuiltInItem = React.useCallback(\n (id: string) => {\n if (id === 'change-password') {\n return (\n <Link\n key={id}\n href={changePasswordHref}\n className={menuItemClass}\n role=\"menuitem\"\n onClick={() => setOpen(false)}\n >\n <Key className=\"size-4\" />\n <span>{t('ui.profileMenu.changePassword', 'Change Password')}</span>\n </Link>\n )\n }\n\n if (id === 'notifications' && notificationsHref) {\n return (\n <Link\n key={id}\n href={notificationsHref}\n className={menuItemClass}\n role=\"menuitem\"\n onClick={() => setOpen(false)}\n >\n <Bell className=\"size-4\" />\n <span>{t('ui.profileMenu.notifications', 'Notification Preferences')}</span>\n </Link>\n )\n }\n\n if (id === 'theme-toggle') {\n return mounted ? (\n <Button\n key={id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-between\"\n role=\"menuitem\"\n onClick={handleThemeToggle}\n >\n <span className=\"inline-flex items-center gap-2.5\">\n {isDark ? <Moon className=\"size-4\" /> : <Sun className=\"size-4\" />}\n <span>{t('ui.profileMenu.theme', 'Dark Mode')}</span>\n </span>\n <div className={`w-8 h-4 rounded-full transition-colors ${isDark ? 'bg-primary' : 'bg-muted'} relative`}>\n <div className={`absolute top-0.5 w-3 h-3 rounded-full bg-background shadow transition-transform ${isDark ? 'translate-x-4' : 'translate-x-0.5'}`} />\n </div>\n </Button>\n ) : null\n }\n\n if (id === 'language') {\n return (\n <div key={id} className=\"relative\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-between\"\n role=\"menuitem\"\n onClick={() => setLanguageOpen(!languageOpen)}\n aria-expanded={languageOpen}\n >\n <span className=\"inline-flex items-center gap-2.5\">\n <Globe className=\"size-4\" />\n <span>{t('ui.profileMenu.language', 'Language')}</span>\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {localeLabels[currentLocale]}\n </span>\n </Button>\n {languageOpen && (\n <div className=\"mt-1 ml-6 space-y-0.5 border-l pl-2\">\n {locales.map((locale) => (\n <Button\n key={locale}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={`w-full justify-start gap-2 ${locale === currentLocale ? 'text-primary font-medium' : ''}`}\n onClick={() => handleLocaleChange(locale)}\n >\n <span>{localeLabels[locale]}</span>\n {locale === currentLocale && <Check className=\"size-3.5\" />}\n </Button>\n ))}\n </div>\n )}\n </div>\n )\n }\n\n if (id === 'sign-out') {\n return (\n <form key={id} action=\"/api/auth/logout\" method=\"POST\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n type=\"submit\"\n role=\"menuitem\"\n >\n <LogOut className=\"size-4\" />\n <span>{t('ui.userMenu.logout', 'Sign Out')}</span>\n </Button>\n </form>\n )\n }\n\n return null\n },\n [\n changePasswordHref,\n currentLocale,\n handleThemeToggle,\n isDark,\n languageOpen,\n menuItemClass,\n mounted,\n notificationsHref,\n t,\n ],\n )\n\n return (\n <div className=\"relative\">\n <IconButton\n ref={buttonRef}\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setOpen(!open)}\n aria-expanded={open}\n aria-haspopup=\"menu\"\n data-testid=\"profile-dropdown-trigger\"\n title={email || t('ui.userMenu.userFallback', 'User')}\n >\n <User className=\"size-4\" />\n </IconButton>\n\n {open && (\n <div\n ref={menuRef}\n className=\"absolute right-0 top-full mt-1 w-56 rounded-md border bg-background p-1 shadow-lg z-dropdown\"\n role=\"menu\"\n data-testid=\"profile-dropdown\"\n >\n {/* User info header */}\n {(displayName || email) && (\n <div className=\"px-3 py-2.5 border-b mb-1\">\n {displayName && (\n <div className=\"font-medium text-sm flex items-center gap-2\">\n <User className=\"size-4\" />\n {displayName}\n </div>\n )}\n {displayName && email && (\n <div className=\"text-xs text-muted-foreground mt-0.5 ml-6\">{email}</div>\n )}\n {!displayName && email && (\n <div className=\"text-xs text-muted-foreground\">\n {t('ui.userMenu.loggedInAs', 'Logged in as:')} {email}\n </div>\n )}\n </div>\n )}\n\n {mergedMenuItems.map((item) => (\n <React.Fragment key={item.id}>\n {item.separator ? <div className=\"my-1 border-t\" /> : null}\n {item.source === 'injected'\n ? (item.href || item.onClick || item.label || item.labelKey ? renderInjectedItem(item) : null)\n : renderBuiltInItem(item.id)}\n </React.Fragment>\n ))}\n <InjectionSpot\n spotId={BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { User, LogOut, Bell, Moon, Sun, Globe, Key, Check, ChevronRight } from 'lucide-react'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport { useTheme } from '@open-mercato/ui/theme'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { IconButton } from '../primitives/icon-button'\nimport { Switch } from '../primitives/switch'\nimport { useInjectedMenuItems } from './injection/useInjectedMenuItems'\nimport { mergeMenuItems, type MergedMenuItem } from './injection/mergeMenuItems'\nimport { resolveInjectedIcon } from './injection/resolveInjectedIcon'\nimport { InjectionSpot } from './injection/InjectionSpot'\nimport { BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID } from './injection/spotIds'\n\nexport type ProfileDropdownProps = {\n email?: string\n displayName?: string\n changePasswordHref?: string\n notificationsHref?: string\n}\n\nconst localeLabels: Record<Locale, string> = {\n en: 'English',\n de: 'Deutsch',\n es: 'Espa\u00F1ol',\n pl: 'Polski',\n}\n\nexport function ProfileDropdown({\n email,\n displayName,\n changePasswordHref = '/backend/profile/change-password',\n notificationsHref,\n}: ProfileDropdownProps) {\n const t = useT()\n const currentLocale = useLocale()\n const { resolvedTheme, setTheme } = useTheme()\n const [open, setOpen] = React.useState(false)\n const [languageOpen, setLanguageOpen] = React.useState(false)\n const [mounted, setMounted] = React.useState(false)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const menuRef = React.useRef<HTMLDivElement>(null)\n const { items: injectedItems } = useInjectedMenuItems('menu:topbar:profile-dropdown')\n\n React.useEffect(() => {\n setMounted(true)\n }, [])\n\n const isDark = resolvedTheme === 'dark'\n\n // Close on click outside\n React.useEffect(() => {\n if (!open) return\n function handleClick(event: MouseEvent) {\n if (\n menuRef.current &&\n !menuRef.current.contains(event.target as Node) &&\n buttonRef.current &&\n !buttonRef.current.contains(event.target as Node)\n ) {\n setOpen(false)\n setLanguageOpen(false)\n }\n }\n document.addEventListener('mousedown', handleClick)\n return () => document.removeEventListener('mousedown', handleClick)\n }, [open])\n\n // Close on Escape\n React.useEffect(() => {\n if (!open) return\n function handleKeyDown(event: KeyboardEvent) {\n if (event.key === 'Escape') {\n if (languageOpen) {\n setLanguageOpen(false)\n } else {\n setOpen(false)\n buttonRef.current?.focus()\n }\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, languageOpen])\n\n const handleThemeToggle = () => {\n setTheme(isDark ? 'light' : 'dark')\n }\n\n const handleLocaleChange = async (locale: Locale) => {\n try {\n await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n window.location.reload()\n } catch {}\n }\n\n // Unified row class \u2014 every menu item uses this for perfectly aligned layout.\n // h-9 keeps all rows the same height regardless of whether they have a trailing element.\n const menuItemClass = cn(\n 'group flex h-9 w-full items-center gap-3 rounded-md px-2.5 text-sm text-foreground',\n 'transition-colors hover:bg-muted/60 focus:outline-none focus-visible:bg-muted/60',\n 'cursor-pointer',\n )\n const menuIconClass = 'size-4 shrink-0 text-muted-foreground group-hover:text-foreground'\n\n const resolveMenuLabel = React.useCallback(\n (item: Pick<MergedMenuItem, 'id' | 'label' | 'labelKey'>): string => {\n if (item.labelKey && item.label) return t(item.labelKey, item.label)\n if (item.labelKey) return t(item.labelKey, item.id)\n if (item.label && item.label.includes('.')) return t(item.label, item.id)\n return item.label ?? item.id\n },\n [t],\n )\n\n const builtInMenuItems = React.useMemo(\n () => {\n const items: Array<{ id: string; separator?: boolean }> = [{ id: 'change-password' }]\n if (notificationsHref) items.push({ id: 'notifications' })\n items.push({ id: 'theme-toggle', separator: true }, { id: 'language' }, { id: 'sign-out', separator: true })\n return items\n },\n [notificationsHref],\n )\n\n const mergedMenuItems = React.useMemo(\n () => mergeMenuItems(builtInMenuItems, injectedItems),\n [builtInMenuItems, injectedItems],\n )\n const injectionContext = React.useMemo(\n () => ({\n email,\n displayName,\n locale: currentLocale,\n }),\n [currentLocale, displayName, email],\n )\n\n const renderInjectedItem = React.useCallback(\n (item: MergedMenuItem) => {\n const label = resolveMenuLabel(item)\n const icon = resolveInjectedIcon(item.icon)\n const inner = (\n <>\n <span className={menuIconClass}>{icon}</span>\n <span className=\"flex-1 truncate\">{label}</span>\n </>\n )\n if (item.href) {\n return (\n <Link\n key={item.id}\n href={item.href}\n className={menuItemClass}\n role=\"menuitem\"\n data-menu-item-id={item.id}\n onClick={() => setOpen(false)}\n >\n {inner}\n </Link>\n )\n }\n return (\n <button\n key={item.id}\n type=\"button\"\n className={menuItemClass}\n role=\"menuitem\"\n data-menu-item-id={item.id}\n onClick={() => {\n item.onClick?.()\n setOpen(false)\n }}\n >\n {inner}\n </button>\n )\n },\n [menuItemClass, menuIconClass, resolveMenuLabel],\n )\n\n const renderBuiltInItem = React.useCallback(\n (id: string) => {\n if (id === 'change-password') {\n return (\n <Link\n key={id}\n href={changePasswordHref}\n className={menuItemClass}\n role=\"menuitem\"\n onClick={() => setOpen(false)}\n >\n <Key className={menuIconClass} />\n <span className=\"flex-1 truncate\">{t('ui.profileMenu.changePassword', 'Change Password')}</span>\n </Link>\n )\n }\n\n if (id === 'notifications' && notificationsHref) {\n return (\n <Link\n key={id}\n href={notificationsHref}\n className={menuItemClass}\n role=\"menuitem\"\n onClick={() => setOpen(false)}\n >\n <Bell className={menuIconClass} />\n <span className=\"flex-1 truncate\">{t('ui.profileMenu.notifications', 'Notification Preferences')}</span>\n </Link>\n )\n }\n\n if (id === 'theme-toggle') {\n if (!mounted) return null\n return (\n <div\n key={id}\n className={cn(menuItemClass, 'justify-between')}\n role=\"menuitem\"\n onClick={handleThemeToggle}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleThemeToggle()\n }\n }}\n tabIndex={0}\n >\n <span className=\"flex flex-1 items-center gap-3\">\n {isDark ? <Moon className={menuIconClass} /> : <Sun className={menuIconClass} />}\n <span className=\"flex-1 truncate\">{t('ui.profileMenu.theme', 'Theme')}</span>\n </span>\n <Switch\n checked={isDark}\n onCheckedChange={handleThemeToggle}\n aria-label={t('ui.profileMenu.theme', 'Theme')}\n onClick={(e) => e.stopPropagation()}\n />\n </div>\n )\n }\n\n if (id === 'language') {\n return (\n <div key={id} className=\"contents\">\n <button\n type=\"button\"\n className={menuItemClass}\n role=\"menuitem\"\n onClick={() => setLanguageOpen(!languageOpen)}\n aria-expanded={languageOpen}\n >\n <Globe className={menuIconClass} />\n <span className=\"truncate\">{t('ui.profileMenu.language', 'Language')}</span>\n <span className=\"text-xs text-muted-foreground\">{localeLabels[currentLocale]}</span>\n <ChevronRight\n className={cn(\n 'ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform',\n languageOpen && 'rotate-90',\n )}\n aria-hidden=\"true\"\n />\n </button>\n {languageOpen && (\n <div className=\"ml-7 mr-1 flex flex-col gap-0.5 border-l pl-2 py-1\">\n {locales.map((locale) => (\n <button\n key={locale}\n type=\"button\"\n className={cn(\n 'flex h-8 w-full items-center justify-between gap-2 rounded-md px-2 text-sm transition-colors hover:bg-muted/60 focus:outline-none focus-visible:bg-muted/60',\n locale === currentLocale ? 'font-medium text-foreground' : 'text-muted-foreground',\n )}\n onClick={() => handleLocaleChange(locale)}\n >\n <span className=\"truncate\">{localeLabels[locale]}</span>\n {locale === currentLocale && <Check className=\"size-3.5 shrink-0 text-accent-indigo\" aria-hidden=\"true\" />}\n </button>\n ))}\n </div>\n )}\n </div>\n )\n }\n\n if (id === 'sign-out') {\n return (\n <form key={id} action=\"/api/auth/logout\" method=\"POST\">\n <button\n type=\"submit\"\n className={menuItemClass}\n role=\"menuitem\"\n >\n <LogOut className={menuIconClass} />\n <span className=\"flex-1 truncate text-left\">{t('ui.userMenu.logout', 'Sign Out')}</span>\n </button>\n </form>\n )\n }\n\n return null\n },\n [\n changePasswordHref,\n currentLocale,\n handleThemeToggle,\n isDark,\n languageOpen,\n menuItemClass,\n menuIconClass,\n mounted,\n notificationsHref,\n t,\n ],\n )\n\n return (\n <div className=\"relative\">\n <IconButton\n ref={buttonRef}\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setOpen(!open)}\n aria-expanded={open}\n aria-haspopup=\"menu\"\n data-testid=\"profile-dropdown-trigger\"\n title={email || t('ui.userMenu.userFallback', 'User')}\n >\n <User className=\"size-4\" />\n </IconButton>\n\n {open && (\n <div\n ref={menuRef}\n className=\"absolute right-0 top-full z-popover mt-2 w-64 overflow-hidden rounded-lg border bg-popover p-0 shadow-lg\"\n role=\"menu\"\n data-testid=\"profile-dropdown\"\n >\n {/* User info header */}\n {(displayName || email) && (\n <div className=\"flex items-center gap-3 border-b bg-muted/30 px-3 py-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-indigo/10 text-accent-indigo\">\n <User className=\"size-4\" aria-hidden=\"true\" />\n </div>\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {displayName ? (\n <span className=\"truncate text-sm font-medium leading-5 text-foreground\">{displayName}</span>\n ) : null}\n {email ? (\n <span className={cn(\n 'truncate text-xs leading-4',\n displayName ? 'text-muted-foreground' : 'text-foreground font-medium',\n )}>\n {email}\n </span>\n ) : null}\n {!displayName && email ? (\n <span className=\"truncate text-overline uppercase tracking-wider text-muted-foreground/80\">\n {t('ui.userMenu.loggedInAs', 'Logged in')}\n </span>\n ) : null}\n </div>\n </div>\n )}\n\n <div className=\"flex flex-col p-1.5\">\n {mergedMenuItems.map((item) => (\n <React.Fragment key={item.id}>\n {item.separator ? <div className=\"my-1 h-px bg-border\" aria-hidden=\"true\" /> : null}\n {item.source === 'injected'\n ? (item.href || item.onClick || item.label || item.labelKey ? renderInjectedItem(item) : null)\n : renderBuiltInItem(item.id)}\n </React.Fragment>\n ))}\n <InjectionSpot\n spotId={BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n </div>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
function NotificationCountBadge({ count }) {
|
|
3
3
|
if (count <= 0) return null;
|
|
4
|
-
return /* @__PURE__ */ jsx("span", { className: "absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-
|
|
4
|
+
return /* @__PURE__ */ jsx("span", { className: "absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-accent-indigo px-1 text-overline font-medium text-accent-indigo-foreground ring-2 ring-background", children: count > 99 ? "99+" : count });
|
|
5
5
|
}
|
|
6
6
|
export {
|
|
7
7
|
NotificationCountBadge
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/notifications/NotificationCountBadge.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\n\nexport type NotificationCountBadgeProps = {\n count: number\n}\n\nexport function NotificationCountBadge({ count }: NotificationCountBadgeProps) {\n if (count <= 0) return null\n return (\n <span className=\"absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-
|
|
5
|
-
"mappings": "AASI;AAHG,SAAS,uBAAuB,EAAE,MAAM,GAAgC;AAC7E,MAAI,SAAS,EAAG,QAAO;AACvB,SACE,oBAAC,UAAK,WAAU,
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\n\nexport type NotificationCountBadgeProps = {\n count: number\n}\n\nexport function NotificationCountBadge({ count }: NotificationCountBadgeProps) {\n if (count <= 0) return null\n return (\n <span className=\"absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-accent-indigo px-1 text-overline font-medium text-accent-indigo-foreground ring-2 ring-background\">\n {count > 99 ? '99+' : count}\n </span>\n )\n}\n"],
|
|
5
|
+
"mappings": "AASI;AAHG,SAAS,uBAAuB,EAAE,MAAM,GAAgC;AAC7E,MAAI,SAAS,EAAG,QAAO;AACvB,SACE,oBAAC,UAAK,WAAU,2LACb,kBAAQ,KAAK,QAAQ,OACxB;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|