@open-mercato/ui 0.6.3-develop.3753.1.29e9a20dde → 0.6.3-develop.3766.1.33102bfc91
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/dist/backend/AppShell.js +1 -1
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/portal/PortalShell.js +43 -15
- package/dist/portal/PortalShell.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +1 -1
- package/src/backend/__tests__/AppShell.test.tsx +86 -0
- package/src/portal/PortalShell.tsx +73 -35
- package/src/portal/__tests__/PortalShell.test.tsx +154 -0
package/dist/backend/AppShell.js
CHANGED
|
@@ -580,7 +580,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
|
|
|
580
580
|
] });
|
|
581
581
|
}
|
|
582
582
|
function renderSidebar(compact, hideHeader, forceMainOnly) {
|
|
583
|
-
if (!isChromeReady && isChromeLoading
|
|
583
|
+
if (!isChromeReady && isChromeLoading) {
|
|
584
584
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-full gap-3", "data-testid": "backend-chrome-loading", children: [
|
|
585
585
|
!hideHeader ? /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxs(
|
|
586
586
|
Link,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/AppShell.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport Link from 'next/link'\nimport Image from 'next/image'\nimport { ChevronDown, ChevronLeft, Home, PanelLeftClose, PanelLeftOpen, Search, X } from 'lucide-react'\nimport { Button } from '../primitives/button'\nimport {\n Breadcrumb as BreadcrumbNav,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from '../primitives/breadcrumb'\nimport { IconButton } from '../primitives/icon-button'\nimport { Input } from '../primitives/input'\nimport { SearchInput } from '../primitives/search-input'\nimport { Checkbox } from '../primitives/checkbox'\nimport { Separator } from '../primitives/separator'\nimport { FlashMessages } from './FlashMessages'\nimport { QueryProvider } from '../theme/QueryProvider'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { apiCall } from './utils/apiCall'\nimport { LastOperationBanner } from './operations/LastOperationBanner'\nimport { ProgressTopBar } from './progress/ProgressTopBar'\nimport { UpgradeActionBanner } from './upgrades/UpgradeActionBanner'\nimport { PartialIndexBanner } from './indexes/PartialIndexBanner'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { slugifySidebarId } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { cloneSidebarGroups } from './sidebar/customization-helpers'\nimport type { SectionNavGroup } from './section-page/types'\nimport { InjectionSpot } from './injection/InjectionSpot'\nimport type { InjectionMenuItem } from '@open-mercato/shared/modules/widgets/injection'\nimport { LEGACY_GLOBAL_MUTATION_INJECTION_SPOT_ID } from './injection/mutationEvents'\nimport { mergeMenuItems } from './injection/mergeMenuItems'\nimport { useInjectedMenuItems } from './injection/useInjectedMenuItems'\nimport { resolveInjectedIcon } from './injection/resolveInjectedIcon'\nimport { useEventBridge } from './injection/eventBridge'\nimport { StatusBadgeInjectionSpot } from './injection/StatusBadgeInjectionSpot'\nimport { UmesDevToolsPanel } from './devtools'\nimport { AiDockProvider } from '../ai/AiDock'\nimport { AiChatSessionsProvider } from '../ai/AiChatSessions'\nimport { AiAssistantLauncher } from '../ai/AiAssistantLauncher'\nimport { BackendChromeProvider, useBackendChrome } from './BackendChromeProvider'\nimport {\n BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID,\n BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID,\n BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID,\n BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,\n GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,\n GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,\n} from './injection/spotIds'\n\nexport type ShellLogo = {\n src: string\n alt?: string\n}\n\nexport type AppShellProps = {\n productName?: string\n logo?: ShellLogo\n email?: string\n groups: {\n id?: string\n name: string\n defaultName?: string\n items: {\n id?: string\n href: string\n title: string\n defaultTitle?: string\n icon?: React.ReactNode\n iconName?: string\n iconMarkup?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n children?: {\n id?: string\n href: string\n title: string\n defaultTitle?: string\n icon?: React.ReactNode\n iconName?: string\n iconMarkup?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n }[]\n }[]\n }[]\n children: React.ReactNode\n rightHeaderSlot?: React.ReactNode\n sidebarCollapsedDefault?: boolean\n currentTitle?: string\n breadcrumb?: Array<{ label: string; href?: string }>\n // Optional: full admin nav API to refresh sidebar client-side\n adminNavApi?: string\n version?: string\n settingsSectionTitle?: string\n settingsPathPrefixes?: string[]\n settingsSections?: SectionNavGroup[]\n profileSections?: SectionNavGroup[]\n profileSectionTitle?: string\n profilePathPrefixes?: string[]\n mobileSidebarSlot?: React.ReactNode\n}\n\ntype Breadcrumb = Array<{ label: string; href?: string }>\n\ntype SidebarGroup = AppShellProps['groups'][number]\ntype SidebarItem = SidebarGroup['items'][number]\n\nfunction convertInjectedMenuItemToSidebarItem(item: InjectionMenuItem, title: string): SidebarItem | null {\n if (!item.href) return null\n return {\n id: item.id,\n href: item.href,\n title,\n defaultTitle: title,\n icon: resolveInjectedIcon(item.icon) ?? undefined,\n iconName: item.icon,\n enabled: true,\n hidden: false,\n pageContext: 'main',\n }\n}\n\nfunction resolveInjectedMenuLabel(\n item: { id: string; label?: string; labelKey?: string },\n t: (key: string, fallback?: string) => string,\n): 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\nfunction mergeSidebarItemsWithInjected(\n items: SidebarItem[],\n injectedItems: InjectionMenuItem[],\n t: (key: string, fallback?: string) => string,\n): SidebarItem[] {\n if (injectedItems.length === 0) return items\n\n const builtInById = new Map<string, SidebarItem>()\n for (const item of items) {\n builtInById.set(item.id ?? item.href, item)\n }\n\n const merged = mergeMenuItems(\n items.map((item) => ({\n id: item.id ?? item.href,\n })),\n injectedItems,\n )\n\n const result: SidebarItem[] = []\n for (const entry of merged) {\n if (entry.source === 'built-in') {\n const original = builtInById.get(entry.id)\n if (original) result.push(original)\n continue\n }\n const translatedLabel = resolveInjectedMenuLabel(\n { id: entry.id, label: entry.label, labelKey: entry.labelKey },\n t,\n )\n const converted = convertInjectedMenuItemToSidebarItem(\n {\n id: entry.id,\n label: translatedLabel,\n icon: entry.icon,\n href: entry.href,\n },\n translatedLabel,\n )\n if (converted) result.push(converted)\n }\n\n return result\n}\n\nfunction mergeSidebarGroupsWithInjected(\n groups: SidebarGroup[],\n injectedItems: InjectionMenuItem[],\n t: (key: string, fallback?: string) => string,\n): SidebarGroup[] {\n if (injectedItems.length === 0) return groups\n\n const injectedByGroup = new Map<string, InjectionMenuItem[]>()\n const ungrouped: InjectionMenuItem[] = []\n\n for (const item of injectedItems) {\n if (item.groupId && item.groupId.trim().length > 0) {\n const groupItems = injectedByGroup.get(item.groupId) ?? []\n groupItems.push(item)\n injectedByGroup.set(item.groupId, groupItems)\n continue\n }\n ungrouped.push(item)\n }\n\n const nextGroups = groups.map((group, index) => {\n const groupId = group.id || resolveGroupKey(group)\n const groupInjected = [\n ...(injectedByGroup.get(groupId) ?? []),\n ...(index === 0 ? ungrouped : []),\n ]\n return {\n ...group,\n items: mergeSidebarItemsWithInjected(group.items, groupInjected, t),\n }\n })\n\n const existingIds = new Set(nextGroups.map((group) => group.id || resolveGroupKey(group)))\n for (const [groupId, items] of injectedByGroup.entries()) {\n if (existingIds.has(groupId)) continue\n const first = items[0]\n const label = first.groupLabelKey\n ? t(first.groupLabelKey, first.groupLabel ?? groupId)\n : (first.groupLabel ?? groupId)\n const groupItems = mergeSidebarItemsWithInjected([], items, t)\n if (groupItems.length === 0) continue\n nextGroups.push({\n id: groupId,\n name: label,\n defaultName: label,\n items: groupItems,\n })\n }\n\n return nextGroups\n}\n\nfunction mergeSectionGroupsWithInjected(\n sections: SectionNavGroup[],\n injectedItems: InjectionMenuItem[],\n t: (key: string, fallback?: string) => string,\n): SectionNavGroup[] {\n if (injectedItems.length === 0) return sections\n const byGroup = new Map<string, InjectionMenuItem[]>()\n for (const item of injectedItems) {\n const groupId = item.groupId && item.groupId.trim().length > 0 ? item.groupId : 'injected'\n const bucket = byGroup.get(groupId) ?? []\n bucket.push(item)\n byGroup.set(groupId, bucket)\n }\n\n const nextSections = sections.map((section) => {\n const sectionItems = byGroup.get(section.id) ?? []\n if (sectionItems.length === 0) return section\n const mergedItems = mergeMenuItems(\n section.items.map((item) => ({ id: item.id, item })),\n sectionItems,\n ).flatMap((item) => {\n if (item.source === 'built-in') {\n const original = section.items.find((entry) => entry.id === item.id)\n return original ? [original] : []\n }\n if (!item.href) return []\n const label = resolveInjectedMenuLabel(item, t)\n return [{\n id: item.id,\n label,\n href: item.href,\n icon: resolveInjectedIcon(item.icon) ?? undefined,\n }]\n })\n return {\n ...section,\n items: mergedItems,\n }\n })\n\n for (const [sectionId, sectionItems] of byGroup.entries()) {\n const exists = nextSections.some((section) => section.id === sectionId)\n if (exists) continue\n const first = sectionItems[0]\n const label = first.groupLabelKey\n ? t(first.groupLabelKey, first.groupLabel ?? sectionId)\n : (first.groupLabel ?? sectionId)\n const items = sectionItems.flatMap((item) => {\n if (!item.href) return []\n const itemLabel = resolveInjectedMenuLabel(item, t)\n return [{\n id: item.id,\n label: itemLabel,\n href: item.href,\n icon: resolveInjectedIcon(item.icon) ?? undefined,\n }]\n })\n if (items.length === 0) continue\n nextSections.push({ id: sectionId, label, items })\n }\n\n return nextSections\n}\n\nfunction resolveGroupKey(group: SidebarGroup): string {\n if (group.id && group.id.length) return group.id\n if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName)\n return slugifySidebarId(group.name)\n}\n\nfunction resolveItemKey(item: { id?: string; href: string }): string {\n const candidate = item.id?.trim()\n if (candidate && candidate.length > 0) return candidate\n return item.href\n}\n\nfunction SerializedIcon({ markup }: { markup: string }) {\n return <span aria-hidden=\"true\" dangerouslySetInnerHTML={{ __html: markup }} />\n}\n\nfunction renderIcon(\n icon: React.ReactNode | undefined,\n iconName: string | undefined,\n iconMarkup: string | undefined,\n fallback: React.ReactNode,\n) {\n if (icon) return icon\n if (iconName) {\n const resolved = resolveInjectedIcon(iconName)\n if (resolved) return resolved\n }\n if (iconMarkup) return <SerializedIcon markup={iconMarkup} />\n return fallback\n}\n\nconst HeaderContext = createContext<{\n setBreadcrumb: (b?: Breadcrumb) => void\n setTitle: (t?: string) => void\n} | null>(null)\n\nexport function ApplyBreadcrumb({ breadcrumb, title, titleKey }: { breadcrumb?: Array<{ label: string; href?: string; labelKey?: string }>; title?: string; titleKey?: string }) {\n const ctx = useContext(HeaderContext)\n const t = useT()\n const resolvedBreadcrumb = React.useMemo<Breadcrumb | undefined>(() => {\n if (!breadcrumb) return undefined\n return breadcrumb.map(({ label, labelKey, href }) => {\n const translated = labelKey ? t(labelKey) : undefined\n const finalLabel = translated && translated !== labelKey ? translated : label\n return {\n href,\n label: finalLabel,\n }\n })\n }, [breadcrumb, t])\n const resolvedTitle = React.useMemo(() => {\n if (!titleKey) return title\n const translated = t(titleKey)\n if (translated && translated !== titleKey) return translated\n return title\n }, [titleKey, title, t])\n React.useEffect(() => {\n ctx?.setBreadcrumb(resolvedBreadcrumb)\n if (resolvedTitle !== undefined) ctx?.setTitle(resolvedTitle)\n }, [ctx, resolvedBreadcrumb, resolvedTitle])\n return null\n}\n\nconst DefaultIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M8 6h13M8 12h13M8 18h13\"/>\n <path d=\"M3 6h.01M3 12h.01M3 18h.01\"/>\n </svg>\n)\n\n// DataTable icon used for dynamic custom entity records links\nconst DataTableIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\" ry=\"2\"/>\n <line x1=\"3\" y1=\"8\" x2=\"21\" y2=\"8\"/>\n <line x1=\"9\" y1=\"8\" x2=\"9\" y2=\"20\"/>\n <line x1=\"15\" y1=\"8\" x2=\"15\" y2=\"20\"/>\n </svg>\n)\n\nconst CustomizeIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.05.05a2 2 0 1 1-2.83 2.83l-.05-.05A1.65 1.65 0 0 0 15 19.4a1.65 1.65 0 0 0-1 .6 1.65 1.65 0 0 0-.33 1.82l-.05.05a2 2 0 1 1-2.83-2.83l.05-.05A1.65 1.65 0 0 0 9 15a1.65 1.65 0 0 0-1-.6 1.65 1.65 0 0 0-1.82.33l-.05.05a2 2 0 1 1-2.83-2.83l.05-.05A1.65 1.65 0 0 0 4.6 9 1.65 1.65 0 0 0 4 8a1.65 1.65 0 0 0-.6-1.82l-.05-.05a2 2 0 1 1 2.83-2.83l.05.05A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-.6 1.65 1.65 0 0 0 .33-1.82l.05-.05a2 2 0 1 1 2.83 2.83l-.05.05A1.65 1.65 0 0 0 15 9a1.65 1.65 0 0 0 1 .6 1.65 1.65 0 0 0 1.82-.33l.05-.05a2 2 0 1 1 2.83 2.83l-.05.05A1.65 1.65 0 0 0 19.4 15z\" />\n </svg>\n)\n\nconst BackArrowIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M19 12H5M12 19l-7-7 7-7\" />\n </svg>\n)\n\nfunction Chevron({ open }: { open: boolean }) {\n return (\n <svg className={`transition-transform ${open ? 'rotate-180' : ''}`} width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M6 9l6 6 6-6\"/></svg>\n )\n}\n\nexport function AppShell(props: AppShellProps) {\n return (\n <QueryProvider>\n <BackendChromeProvider adminNavApi={props.adminNavApi}>\n <AiChatSessionsProvider>\n <AiDockProvider>\n <AppShellBody {...props} />\n </AiDockProvider>\n </AiChatSessionsProvider>\n </BackendChromeProvider>\n </QueryProvider>\n )\n}\n\nfunction AppShellBody({ productName, logo, email, groups, rightHeaderSlot, children, sidebarCollapsedDefault = false, currentTitle, breadcrumb, version, settingsSectionTitle, settingsPathPrefixes = [], settingsSections, profileSections, profileSectionTitle, profilePathPrefixes = [], mobileSidebarSlot }: AppShellProps) {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const t = useT()\n const locale = useLocale()\n const { payload: chromePayload, isReady: isChromeReady, isLoading: isChromeLoading } = useBackendChrome()\n const resolvedGroups = React.useMemo(\n () => cloneSidebarGroups(chromePayload?.groups ?? groups),\n [chromePayload?.groups, groups],\n )\n const resolvedSettingsSections = chromePayload?.settingsSections ?? settingsSections\n const resolvedSettingsPathPrefixes = chromePayload?.settingsPathPrefixes ?? settingsPathPrefixes\n const resolvedProfileSections = chromePayload?.profileSections ?? profileSections\n const resolvedProfilePathPrefixes = chromePayload?.profilePathPrefixes ?? profilePathPrefixes\n const { items: mainSidebarInjectedMenuItems } = useInjectedMenuItems('menu:sidebar:main')\n const { items: settingsSidebarInjectedMenuItems } = useInjectedMenuItems('menu:sidebar:settings')\n const { items: profileSidebarInjectedMenuItems } = useInjectedMenuItems('menu:sidebar:profile')\n const { items: topbarInjectedMenuItems } = useInjectedMenuItems('menu:topbar:actions')\n useEventBridge() // SSE DOM Event Bridge \u2014 singleton SSE connection for real-time server events\n const resolvedProductName = productName ?? t('appShell.productName')\n const [mobileOpen, setMobileOpen] = React.useState(false)\n // When the mobile drawer opens on a settings/profile route, it follows the\n // section sidebar by default. Set to 'main' to force-show the main nav even\n // when the route is in a section context. Reset on close.\n const [mobileDrawerView, setMobileDrawerView] = React.useState<'auto' | 'main'>('auto')\n React.useEffect(() => {\n if (!mobileOpen) setMobileDrawerView('auto')\n }, [mobileOpen])\n // Initialize from server-provided prop only to avoid hydration flicker\n const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault)\n // Maintain internal nav state so we can augment it client-side\n const [navGroups, setNavGroups] = React.useState(resolvedGroups)\n const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>(() =>\n Object.fromEntries(resolvedGroups.map((g) => [resolveGroupKey(g), true])) as Record<string, boolean>\n )\n const [headerTitle, setHeaderTitle] = React.useState<string | undefined>(currentTitle)\n const [headerBreadcrumb, setHeaderBreadcrumb] = React.useState<Breadcrumb | undefined>(breadcrumb)\n const [navQuery, setNavQuery] = React.useState('')\n const navQueryNorm = navQuery.trim().toLowerCase()\n const navQueryActive = navQueryNorm.length > 0\n const matchesQuery = React.useCallback((label: string | undefined) => {\n if (!navQueryActive) return true\n if (!label) return false\n return label.toLowerCase().includes(navQueryNorm)\n }, [navQueryActive, navQueryNorm])\n const effectiveCollapsed = collapsed\n const expandedSidebarWidth = '240px'\n\n // Track scroll position of the desktop sidebar's inner scroll container so we can\n // flip the affordance chevron between down/up (and hide it entirely when content\n // fits without scrolling). The inner div is rendered deep in renderSidebar /\n // renderSectionSidebar \u2014 we tag it with `data-sidebar-scroll=\"true\"` and look it\n // up via the aside ref so we don't have to thread refs through the JSX tree.\n const sidebarAsideRef = React.useRef<HTMLElement>(null)\n const [sidebarScrollState, setSidebarScrollState] = React.useState<'down' | 'up' | 'none'>('down')\n const sidebarScrollIntentRef = React.useRef<'top' | 'bottom' | null>(null)\n\n // Click-to-scroll handler for the sidebar affordance chevron (#1803). Resolves the\n // scroll target lazily through the aside ref so we don't have to thread refs into\n // renderSidebar; respects `prefers-reduced-motion` by falling back to instant\n // scrolling when the user has opted out of smooth motion.\n const handleSidebarChevronScroll = React.useCallback((target: 'top' | 'bottom') => {\n const aside = sidebarAsideRef.current\n if (!aside) return\n const scrollTarget = aside.querySelector<HTMLElement>('[data-sidebar-scroll=\"true\"]')\n if (!scrollTarget) return\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n const behavior: ScrollBehavior = prefersReducedMotion ? 'auto' : 'smooth'\n const maxScrollTop = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight)\n if (maxScrollTop <= 1) {\n sidebarScrollIntentRef.current = null\n setSidebarScrollState('none')\n return\n }\n sidebarScrollIntentRef.current = target\n setSidebarScrollState(target === 'bottom' ? 'up' : 'down')\n scrollTarget.scrollTo({\n top: target === 'top' ? 0 : maxScrollTop,\n behavior,\n })\n }, [])\n React.useEffect(() => {\n const aside = sidebarAsideRef.current\n if (!aside) return\n const target = aside.querySelector<HTMLElement>('[data-sidebar-scroll=\"true\"]')\n if (!target) return\n const update = () => {\n const { scrollTop, scrollHeight, clientHeight } = target\n const canScroll = scrollHeight > clientHeight + 1\n if (!canScroll) {\n sidebarScrollIntentRef.current = null\n setSidebarScrollState('none')\n return\n }\n const maxScrollTop = Math.max(0, scrollHeight - clientHeight)\n const atTop = scrollTop <= 8\n const atBottom = scrollTop >= maxScrollTop - 8\n const scrollIntent = sidebarScrollIntentRef.current\n if (scrollIntent === 'bottom') {\n if (atBottom) sidebarScrollIntentRef.current = null\n setSidebarScrollState('up')\n return\n }\n if (scrollIntent === 'top') {\n if (atTop) sidebarScrollIntentRef.current = null\n setSidebarScrollState('down')\n return\n }\n setSidebarScrollState(atBottom ? 'up' : 'down')\n }\n update()\n target.addEventListener('scroll', update, { passive: true })\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(update) : null\n ro?.observe(target)\n return () => {\n target.removeEventListener('scroll', update)\n ro?.disconnect()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pathname, effectiveCollapsed])\n const injectionContext = React.useMemo(\n () => ({\n path: pathname ?? '',\n query: searchParams?.toString() ?? '',\n }),\n [pathname, searchParams],\n )\n\n const isOnSettingsPath = React.useMemo(() => {\n if (!pathname) return false\n if (pathname === '/backend/settings') return true\n return resolvedSettingsPathPrefixes.some((prefix) => pathname.startsWith(prefix))\n }, [pathname, resolvedSettingsPathPrefixes])\n\n const isOnProfilePath = React.useMemo(() => {\n if (!pathname) return false\n if (pathname === '/backend/profile') return true\n return resolvedProfilePathPrefixes.some((prefix) => pathname.startsWith(prefix))\n }, [pathname, resolvedProfilePathPrefixes])\n\n const sidebarMode: 'main' | 'settings' | 'profile' =\n isOnSettingsPath ? 'settings' :\n isOnProfilePath ? 'profile' :\n 'main'\n\n const mainNavGroupsWithInjected = React.useMemo(\n () => mergeSidebarGroupsWithInjected(navGroups, mainSidebarInjectedMenuItems, t),\n [mainSidebarInjectedMenuItems, navGroups, t],\n )\n\n // Lock body scroll when mobile drawer is open so touch scroll stays in the drawer\n React.useEffect(() => {\n if (!mobileOpen || typeof document === 'undefined') return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [mobileOpen])\n\n React.useEffect(() => {\n try {\n const savedOpen = typeof window !== 'undefined' ? localStorage.getItem('om:sidebarOpenGroups') : null\n if (!savedOpen) return\n const parsed = JSON.parse(savedOpen) as Record<string, boolean>\n setOpenGroups((prev) => {\n const next = { ...prev }\n for (const group of resolvedGroups) {\n const key = resolveGroupKey(group)\n if (key in parsed) next[key] = !!parsed[key]\n else if (group.name in parsed) next[key] = !!parsed[group.name]\n }\n return next\n })\n } catch {\n // ignore localStorage errors to avoid breaking hydration\n }\n }, [resolvedGroups])\n\n const toggleGroup = (groupId: string) => setOpenGroups((prev) => ({ ...prev, [groupId]: prev[groupId] === false }))\n\n const asideWidth = effectiveCollapsed ? '80px' : expandedSidebarWidth\n // Use min-h-svh so the border extends with tall content; no overflow so sticky bottom works\n const asideClassesBase = `border-r bg-background py-4`;\n\n // Persist collapse state to localStorage and cookie. Both writes can throw in\n // private/incognito mode (storage blocked) or when cookies are disabled \u2014\n // the persisted preference is purely a UX nice-to-have, never functional, so\n // swallow the failure and let the component fall back to the default state.\n React.useEffect(() => {\n try { localStorage.setItem('om:sidebarCollapsed', collapsed ? '1' : '0') } catch { /* localStorage blocked (private mode) \u2014 non-critical */ }\n try {\n document.cookie = `om_sidebar_collapsed=${collapsed ? '1' : '0'}; path=/; max-age=31536000; samesite=lax`\n } catch { /* cookies disabled \u2014 non-critical */ }\n }, [collapsed])\n\n // Two-level sidebar (Option B): when entering settings/profile mode, force the\n // main sidebar to collapsed (icons only) so the section sub-nav can sit beside\n // it; restore the user's previous expansion when returning to the main mode.\n // Initial ref is 'main' so direct mounts on /backend/settings also auto-collapse.\n const collapsedBeforeSectionRef = React.useRef<boolean | null>(null)\n const previousSidebarModeRef = React.useRef<'main' | 'settings' | 'profile'>('main')\n React.useEffect(() => {\n const previous = previousSidebarModeRef.current\n if (previous === 'main' && sidebarMode !== 'main') {\n collapsedBeforeSectionRef.current = collapsed\n if (!collapsed) setCollapsed(true)\n } else if (previous !== 'main' && sidebarMode === 'main' && collapsedBeforeSectionRef.current !== null) {\n const restoreTo = collapsedBeforeSectionRef.current\n collapsedBeforeSectionRef.current = null\n if (collapsed !== restoreTo) setCollapsed(restoreTo)\n }\n previousSidebarModeRef.current = sidebarMode\n }, [sidebarMode, collapsed])\n React.useEffect(() => {\n try { localStorage.setItem('om:sidebarOpenGroups', JSON.stringify(openGroups)) } catch { /* localStorage blocked (private mode) \u2014 non-critical */ }\n }, [openGroups])\n\n // Ensure current route's group is expanded on load\n React.useEffect(() => {\n const activeGroup = navGroups.find((g) => g.items.some((i) => pathname?.startsWith(i.href)))\n if (!activeGroup) return\n const key = resolveGroupKey(activeGroup)\n setOpenGroups((prev) => (prev[key] === false ? { ...prev, [key]: true } : prev))\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pathname, navGroups])\n // Keep header state in sync with props (server-side updates)\n React.useEffect(() => {\n setHeaderTitle(currentTitle)\n setHeaderBreadcrumb(breadcrumb)\n }, [currentTitle, breadcrumb])\n // Clear breadcrumb on client-side navigation so stale state doesn't persist;\n // the new page's ApplyBreadcrumb (if any) will set the correct values\n const prevPathname = React.useRef(pathname)\n React.useEffect(() => {\n if (pathname !== prevPathname.current) {\n prevPathname.current = pathname\n setHeaderTitle(undefined)\n setHeaderBreadcrumb(undefined)\n }\n }, [pathname])\n\n // Keep navGroups in sync when server-provided groups change\n React.useEffect(() => {\n setNavGroups(cloneSidebarGroups(resolvedGroups))\n }, [resolvedGroups])\n\n function renderSectionSidebar(\n sections: SectionNavGroup[],\n title: string,\n compact: boolean,\n hideHeader?: boolean,\n hideSearch?: boolean\n ) {\n const sortedSections = [...sections].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n const lastVisibleIndex = sortedSections.length - 1\n\n return (\n <div className=\"flex h-full flex-col gap-3\">\n {!hideHeader && (\n <div className=\"mb-2\">\n <Link\n href=\"/backend\"\n className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}\n aria-label={t('appShell.goToDashboard')}\n >\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className=\"rounded-full shrink-0\" />\n {!compact && <span className=\"truncate text-sm font-medium text-foreground\">{resolvedProductName}</span>}\n </Link>\n </div>\n )}\n {!compact && !hideSearch && (\n <SearchInput\n value={navQuery}\n onChange={setNavQuery}\n placeholder={t('appShell.searchNavPlaceholder', 'Search...')}\n aria-label={t('appShell.searchNavAria', 'Search navigation')}\n clearLabel={t('appShell.searchNavClear', 'Clear search')}\n className=\"mb-2\"\n />\n )}\n <div data-sidebar-scroll=\"true\" className={`flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? '-ml-2 pl-2' : '-ml-3 pl-3'}`}>\n <nav className=\"flex flex-col gap-2\">\n {sortedSections.map((section, sectionIndex) => {\n const sectionNavQueryActive = hideSearch ? false : navQueryActive\n const matchesItemQuery = (item: typeof section.items[number]): boolean => {\n if (!sectionNavQueryActive) return true\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n if (matchesQuery(label)) return true\n return Array.isArray(item.children) && item.children.some(matchesItemQuery)\n }\n const visibleItems = sectionNavQueryActive\n ? section.items.filter(matchesItemQuery)\n : section.items\n if (visibleItems.length === 0) return null\n const sortedItems = [...visibleItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n const sectionLabel = section.labelKey ? t(section.labelKey, section.label) : section.label\n const sectionKey = `settings:${section.id}`\n const open = openGroups[sectionKey] !== false\n const sortSectionItems = (items: typeof section.items = []) =>\n [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n const filterChildren = (children: typeof section.items | undefined) => {\n if (!children) return [] as typeof section.items\n if (!sectionNavQueryActive) return [...children]\n return children.filter(matchesItemQuery)\n }\n\n const renderSectionItem = (item: (typeof section.items)[number], depth = 0): React.ReactNode => {\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n const childItems = sortSectionItems(filterChildren(item.children))\n const isOnItemBranch = !!pathname && (\n pathname === item.href ||\n pathname.startsWith(`${item.href}/`)\n )\n const hasActiveChild = !!(pathname && childItems.some((child) => (\n pathname === child.href ||\n pathname.startsWith(`${child.href}/`)\n )))\n const showChildren = childItems.length > 0 && (isOnItemBranch || sectionNavQueryActive)\n const isActive = isOnItemBranch || hasActiveChild\n const base = compact ? 'w-10 h-10 justify-center' : 'w-full py-2 gap-2'\n const spacingStyle = !compact\n ? {\n paddingLeft: `${12 + depth * 16}px`,\n paddingRight: '12px',\n }\n : undefined\n\n return (\n <React.Fragment key={item.id}>\n <Link\n href={item.href}\n className={`relative text-sm font-medium rounded-lg inline-flex items-center ${base} ${\n isActive\n ? 'bg-muted text-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n style={spacingStyle}\n title={compact ? label : undefined}\n data-menu-item-id={item.id}\n onClick={() => setMobileOpen(false)}\n >\n {isActive && (\n <span aria-hidden className={`absolute ${compact ? 'left-[-20px]' : 'left-[-12px]'} top-2 w-1 h-5 rounded-r bg-foreground`} />\n )}\n <span className=\"flex items-center justify-center shrink-0\">\n {renderIcon(\n item.icon,\n item.iconName,\n item.iconMarkup,\n item.href.includes('/backend/entities/user/') && item.href.endsWith('/records') ? DataTableIcon : DefaultIcon,\n )}\n </span>\n {!compact && <span className=\"truncate\">{label}</span>}\n </Link>\n {showChildren ? childItems.map((child) => renderSectionItem(child, depth + 1)) : null}\n </React.Fragment>\n )\n }\n\n return (\n <div key={section.id}>\n {!compact && (\n <Button\n variant=\"muted\"\n onClick={() => toggleGroup(sectionKey)}\n className=\"w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1\"\n aria-expanded={open}\n >\n <span>{sectionLabel}</span>\n <Chevron open={open} />\n </Button>\n )}\n {(open || compact) && (\n <div className={`flex flex-col ${compact ? 'items-center' : ''} gap-1`}>\n {sortedItems.map((item) => renderSectionItem(item))}\n </div>\n )}\n {sectionIndex !== lastVisibleIndex && <div className={`my-2 border-t ${compact ? '-ml-2 -mr-3' : '-ml-3 -mr-4'}`} />}\n </div>\n )\n })}\n </nav>\n </div>\n </div>\n )\n }\n\n function renderSidebar(compact: boolean, hideHeader?: boolean, forceMainOnly?: boolean) {\n if (!isChromeReady && isChromeLoading && resolvedGroups.length === 0) {\n return (\n <div className=\"flex flex-col min-h-full gap-3\" data-testid=\"backend-chrome-loading\">\n {!hideHeader ? (\n <div className=\"mb-2\">\n <Link\n href=\"/backend\"\n className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}\n aria-label={t('appShell.goToDashboard')}\n >\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className=\"rounded-full shrink-0\" />\n {!compact && <span className=\"truncate text-sm font-medium text-foreground\">{resolvedProductName}</span>}\n </Link>\n </div>\n ) : null}\n <div className=\"flex flex-1 flex-col gap-3 pr-1\">\n <div className=\"space-y-3\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"space-y-2 pl-1\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"h-8 rounded bg-muted/50\" />\n </div>\n </div>\n <div className=\"space-y-3\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"space-y-2 pl-1\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"h-8 rounded bg-muted/50\" />\n </div>\n </div>\n </div>\n </div>\n )\n }\n\n if (!forceMainOnly && sidebarMode === 'settings' && resolvedSettingsSections && resolvedSettingsSections.length > 0) {\n const mergedSettingsSections = mergeSectionGroupsWithInjected(\n resolvedSettingsSections,\n settingsSidebarInjectedMenuItems,\n t,\n )\n return renderSectionSidebar(\n mergedSettingsSections,\n settingsSectionTitle ?? t('backend.nav.settings', 'Settings'),\n compact,\n hideHeader\n )\n }\n\n if (!forceMainOnly && sidebarMode === 'profile' && resolvedProfileSections && resolvedProfileSections.length > 0) {\n const mergedProfileSections = mergeSectionGroupsWithInjected(\n resolvedProfileSections,\n profileSidebarInjectedMenuItems,\n t,\n )\n return renderSectionSidebar(\n mergedProfileSections,\n profileSectionTitle ?? t('backend.nav.profile', 'Profile'),\n compact,\n hideHeader\n )\n }\n\n const isMobileVariant = !!hideHeader\n const shouldRenderSidebarInjectionSpots = !isMobileVariant\n\n return (\n <div className=\"flex h-full flex-col gap-3\">\n {!hideHeader && (\n <div className=\"mb-2\">\n <Link\n href=\"/backend\"\n className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}\n aria-label={t('appShell.goToDashboard')}\n >\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className=\"rounded-full shrink-0\" />\n {!compact && <span className=\"truncate text-sm font-medium text-foreground\">{resolvedProductName}</span>}\n </Link>\n </div>\n )}\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {!compact && (\n <SearchInput\n value={navQuery}\n onChange={setNavQuery}\n placeholder={t('appShell.searchNavPlaceholder', 'Search...')}\n aria-label={t('appShell.searchNavAria', 'Search navigation')}\n clearLabel={t('appShell.searchNavClear', 'Clear search')}\n className=\"mb-2\"\n />\n )}\n <div data-sidebar-scroll=\"true\" className={`flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? '-ml-2 pl-2' : '-ml-3 pl-3'}`}>\n {(() => {\n const isSettingsPath = (href: string) => {\n if (href === '/backend/settings') return true\n return resolvedSettingsPathPrefixes.some((prefix) => href.startsWith(prefix))\n }\n\n const isMainItem = (item: SidebarItem) => {\n if (item.pageContext && item.pageContext !== 'main') return false\n if (isSettingsPath(item.href)) return false\n return true\n }\n\n const mainGroups = mainNavGroupsWithInjected.map((g) => ({\n ...g,\n items: g.items.filter((item) => isMainItem(item) && item.hidden !== true),\n })).filter((g) => g.items.length > 0)\n\n const mainLastVisibleGroupIndex = (() => {\n for (let idx = mainGroups.length - 1; idx >= 0; idx -= 1) {\n if (mainGroups[idx].items.some((item) => item.hidden !== true)) return idx\n }\n return -1\n })()\n\n return (\n <>\n <nav className=\"flex flex-col gap-2\" data-testid=\"sidebar\">\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {mainGroups.map((g, gi) => {\n const groupId = resolveGroupKey(g)\n const open = navQueryActive ? true : openGroups[groupId] !== false\n const visibleItems = g.items.filter((item) => {\n if (item.hidden === true) return false\n if (!navQueryActive) return true\n if (matchesQuery(item.title)) return true\n const itemChildren = (item.children ?? []).filter((c) => c.hidden !== true)\n return itemChildren.some((c) => matchesQuery(c.title))\n })\n if (visibleItems.length === 0) return null\n return (\n <div key={groupId}>\n {!compact && (\n <Button\n variant=\"muted\"\n onClick={() => toggleGroup(groupId)}\n className=\"w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1\"\n aria-expanded={open}\n >\n <span>{g.name}</span>\n <Chevron open={open} />\n </Button>\n )}\n {(open || compact) && (\n <div className={`flex flex-col ${compact ? 'items-center' : ''} gap-1`}>\n {visibleItems.map((i) => {\n const allChildItems = (i.children ?? []).filter((child) => child.hidden !== true)\n const matchingChildItems = navQueryActive\n ? allChildItems.filter((c) => matchesQuery(c.title))\n : allChildItems\n const childItems = navQueryActive ? matchingChildItems : allChildItems\n const showChildren = navQueryActive\n ? matchingChildItems.length > 0\n : (!!pathname && allChildItems.length > 0 && pathname.startsWith(i.href))\n const hasActiveChild = !!(pathname && allChildItems.some((c) => pathname.startsWith(c.href)))\n const isParentActive = (pathname === i.href) || (!navQueryActive && showChildren && !hasActiveChild)\n const base = compact ? 'w-10 h-10 justify-center' : 'w-full px-3 py-2 gap-2'\n return (\n <React.Fragment key={i.href}>\n <Link\n href={i.href}\n className={`relative text-sm font-medium rounded-lg inline-flex items-center ${base} ${\n isParentActive ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted'\n } ${i.enabled === false ? 'pointer-events-none opacity-50' : ''}`}\n aria-disabled={i.enabled === false}\n title={compact ? i.title : undefined}\n data-menu-item-id={i.id ?? i.href}\n onClick={() => setMobileOpen(false)}\n >\n {isParentActive ? (\n <span aria-hidden className={`absolute ${compact ? 'left-[-20px]' : 'left-[-12px]'} top-2 w-1 h-5 rounded-r bg-foreground`} />\n ) : null}\n <span className=\"flex items-center justify-center shrink-0\">\n {renderIcon(\n i.icon,\n i.iconName,\n i.iconMarkup,\n DefaultIcon,\n )}\n </span>\n {!compact && <span>{i.title}</span>}\n </Link>\n {showChildren ? (\n <div className={`relative flex flex-col ${compact ? 'items-center' : ''} gap-1`}>\n {!compact && (\n <span aria-hidden className=\"pointer-events-none absolute left-1.5 top-1 bottom-1 w-px bg-border\" />\n )}\n {childItems.map((c) => {\n const childActive = pathname?.startsWith(c.href)\n const childBase = compact ? 'w-10 h-8 justify-center' : 'w-full pl-5 pr-3 py-2 gap-2'\n return (\n <Link\n key={c.href}\n href={c.href}\n className={`relative text-sm font-medium rounded-lg inline-flex items-center ${childBase} ${\n childActive ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted'\n } ${c.enabled === false ? 'pointer-events-none opacity-50' : ''}`}\n aria-disabled={c.enabled === false}\n title={compact ? c.title : undefined}\n data-menu-item-id={c.id ?? c.href}\n onClick={() => setMobileOpen(false)}\n >\n {childActive ? (\n <span aria-hidden className={`absolute ${compact ? 'left-[-20px]' : 'left-[-12px]'} top-2 w-1 h-5 rounded-r bg-foreground`} />\n ) : null}\n <span className=\"flex items-center justify-center shrink-0\">\n {renderIcon(\n c.icon,\n c.iconName,\n c.iconMarkup,\n c.href.includes('/backend/entities/user/') && c.href.endsWith('/records') ? DataTableIcon : DefaultIcon,\n )}\n </span>\n {!compact && <span>{c.title}</span>}\n </Link>\n )\n })}\n </div>\n ) : null}\n </React.Fragment>\n )\n })}\n </div>\n )}\n {gi !== mainLastVisibleGroupIndex && <div className={`my-2 border-t ${compact ? '-ml-2 -mr-3' : '-ml-3 -mr-4'}`} />}\n </div>\n )\n })}\n </nav>\n </>\n )\n })()}\n </div>\n <div className=\"sticky bottom-0 bg-background pb-1\">\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {shouldRenderSidebarInjectionSpots ? (\n <StatusBadgeInjectionSpot\n spotId={GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n </div>\n </div>\n )\n }\n\n function renderSectionAside() {\n let sections: SectionNavGroup[] | null = null\n let title = ''\n if (sidebarMode === 'settings' && resolvedSettingsSections && resolvedSettingsSections.length > 0) {\n sections = mergeSectionGroupsWithInjected(\n resolvedSettingsSections,\n settingsSidebarInjectedMenuItems,\n t,\n )\n title = settingsSectionTitle ?? t('backend.nav.settings', 'Settings')\n } else if (sidebarMode === 'profile' && resolvedProfileSections && resolvedProfileSections.length > 0) {\n sections = mergeSectionGroupsWithInjected(\n resolvedProfileSections,\n profileSidebarInjectedMenuItems,\n t,\n )\n title = profileSectionTitle ?? t('backend.nav.profile', 'Profile')\n }\n if (!sections) return null\n return (\n <div className=\"flex h-full flex-col gap-2\">\n <Link\n href=\"/backend\"\n className=\"inline-flex items-center gap-2 rounded-lg px-2 py-2 text-sm font-semibold text-foreground transition-colors hover:bg-muted\"\n data-testid=\"appshell-section-back-to-main\"\n aria-label={t('backend.nav.backToMain', 'Back to Main')}\n >\n <ChevronLeft className=\"size-4 shrink-0\" aria-hidden />\n <span className=\"truncate\">{title}</span>\n </Link>\n <div className=\"min-h-0 flex-1\">\n {renderSectionSidebar(sections, title, false, true, true)}\n </div>\n </div>\n )\n }\n\n const isSectionView =\n (sidebarMode === 'settings' && !!resolvedSettingsSections && resolvedSettingsSections.length > 0) ||\n (sidebarMode === 'profile' && !!resolvedProfileSections && resolvedProfileSections.length > 0)\n const gridColsClass = isSectionView\n ? (effectiveCollapsed ? 'lg:grid-cols-[80px_240px_1fr]' : 'lg:grid-cols-[240px_240px_1fr]')\n : (effectiveCollapsed ? 'lg:grid-cols-[80px_1fr]' : 'lg:grid-cols-[240px_1fr]')\n const headerCtxValue = React.useMemo(() => ({\n setBreadcrumb: setHeaderBreadcrumb,\n setTitle: setHeaderTitle,\n }), [])\n const renderedTopbarInjectedActions = React.useMemo(\n () =>\n topbarInjectedMenuItems.map((item) => {\n const label = resolveInjectedMenuLabel(item, t)\n if (item.href) {\n return (\n <Link\n key={item.id}\n href={item.href}\n className=\"inline-flex items-center rounded border px-2 py-1 text-xs hover:bg-accent hover:text-accent-foreground\"\n data-menu-item-id={item.id}\n >\n {label}\n </Link>\n )\n }\n return (\n <Button\n key={item.id}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n data-menu-item-id={item.id}\n onClick={() => item.onClick?.()}\n >\n {label}\n </Button>\n )\n }),\n [t, topbarInjectedMenuItems],\n )\n\n return (\n <HeaderContext.Provider value={headerCtxValue}>\n <div\n className={`relative min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}\n style={{ '--topbar-height': '61px' } as React.CSSProperties}\n >\n {/* Desktop sidebar collapse/expand toggle \u2014 sits on the divider line between\n sidebar and content, like Notion/Vercel. Hidden on mobile (hamburger in\n topbar handles the drawer). */}\n <button\n type=\"button\"\n onClick={() => setCollapsed((c) => !c)}\n aria-label={t('appShell.toggleSidebar')}\n className=\"hidden lg:flex fixed top-4 z-dropdown size-7 items-center justify-center rounded-md border bg-background text-muted-foreground shadow-sm transition-all hover:text-foreground hover:bg-muted focus:outline-none focus-visible:shadow-focus\"\n style={{ left: `calc(${asideWidth} - 14px)` }}\n >\n {effectiveCollapsed ? <PanelLeftOpen className=\"size-4\" /> : <PanelLeftClose className=\"size-4\" />}\n </button>\n {/* Desktop main sidebar */}\n <aside ref={sidebarAsideRef} className={`${asideClassesBase} ${effectiveCollapsed ? 'px-2' : 'px-3'} hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative transition-[width,padding] duration-200 ease-out`} style={{ width: asideWidth }}>\n {renderSidebar(effectiveCollapsed, false, isSectionView)}\n {/* Scroll affordance \u2014 gradient fade + clickable chevron that flips up when\n the user reaches the bottom and disappears when nothing is scrollable\n (#1803). Clicking the chevron scrolls the inner sidebar container to\n top/bottom (`prefers-reduced-motion: reduce` collapses to instant\n scrolling). The wrapper is `pointer-events-none` so the gradient fade\n doesn't block hover/click on the rendered nav items behind it; the\n IconButton restores `pointer-events-auto` so it stays interactive. */}\n {sidebarScrollState !== 'none' ? (\n <div\n className=\"pointer-events-none absolute inset-x-0 bottom-0 flex h-10 items-end justify-center bg-gradient-to-t from-background via-background/80 to-transparent pb-1.5\"\n >\n {/* The IconButton owns hover/focus affordance; the inner span owns the\n rotate transition so it doesn't fight with the animate-bounce\n keyframes (both target `transform`). */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"sidebar-scroll-chevron\"\n data-sidebar-scroll-chevron={sidebarScrollState}\n aria-label={\n sidebarScrollState === 'up'\n ? t('ui.sidebar.chevron.scrollTop', 'Scroll to top')\n : t('ui.sidebar.chevron.scrollBottom', 'Scroll to bottom')\n }\n className=\"pointer-events-auto text-muted-foreground/70 hover:text-foreground\"\n onClick={() => handleSidebarChevronScroll(sidebarScrollState === 'up' ? 'top' : 'bottom')}\n >\n <span\n className={`inline-flex transition-transform duration-300 ${sidebarScrollState === 'up' ? 'rotate-180' : ''}`}\n >\n <ChevronDown className=\"size-4 animate-bounce\" />\n </span>\n </IconButton>\n </div>\n ) : null}\n </aside>\n\n {/* Desktop section sidebar (Option B two-level) \u2014 sits beside the main sidebar\n when the user is on settings/profile routes. Mobile drawer keeps the\n original swap behavior to fit the narrow width. */}\n {isSectionView ? (\n <aside\n className={`${asideClassesBase} px-3 hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative`}\n style={{ width: '240px' }}\n data-testid=\"appshell-section-sidebar\"\n >\n {renderSectionAside()}\n {/* Static bottom fade \u2014 covers the native iOS scroll indicator and signals\n that the section list is scrollable. Same look as the main sidebar's\n affordance but without the chevron / scroll-state machinery. */}\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-x-0 bottom-0 h-10 bg-gradient-to-t from-background via-background/80 to-transparent\"\n />\n </aside>\n ) : null}\n\n <div className=\"flex min-h-svh flex-col min-w-0\">\n <header className=\"sticky top-0 z-sticky border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 px-3 sm:px-4 lg:px-6 py-3 flex items-center justify-between gap-2 sm:gap-3\">\n <div\n data-testid=\"backend-chrome-ready\"\n data-ready={isChromeReady ? 'true' : 'false'}\n className=\"hidden\"\n />\n <div className=\"flex items-center gap-2 min-w-0\">\n {/* Mobile menu button */}\n <IconButton variant=\"ghost\" size=\"sm\" className=\"lg:hidden\" aria-label={t('appShell.openMenu')} onClick={() => setMobileOpen(true)}>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M3 6h18M3 12h18M3 18h18\"/></svg>\n </IconButton>\n {/* Header breadcrumb: always starts with Dashboard */}\n {(() => {\n const dashboardLabel = t('dashboard.title')\n const root: Breadcrumb = [{ label: dashboardLabel, href: '/backend' }]\n let rest: Breadcrumb = []\n if (headerBreadcrumb && headerBreadcrumb.length) {\n const first = headerBreadcrumb[0]\n const dup = first && (first.href === '/backend' || first.label === dashboardLabel || first.label?.toLowerCase() === 'dashboard')\n rest = dup ? headerBreadcrumb.slice(1) : headerBreadcrumb\n } else if (headerTitle) {\n rest = [{ label: headerTitle }]\n }\n const items = [...root, ...rest]\n if (items.length === 0) return null\n const home = items[0]\n const current = items.length > 1 ? items[items.length - 1] : null\n const mid = items.slice(1, -1)\n const hasMid = mid.length > 0\n return (\n <BreadcrumbNav divider=\"arrow\" className=\"ml-2 lg:ml-3 text-sm\">\n <BreadcrumbList className=\"[&_[data-slot=breadcrumb-separator]_svg]:size-4\">\n <BreadcrumbItem>\n {home.href && current ? (\n <BreadcrumbLink asChild aria-label={home.label}>\n <Link href={home.href}>\n <Home className=\"size-4\" aria-hidden=\"true\" />\n </Link>\n </BreadcrumbLink>\n ) : (\n <BreadcrumbPage aria-label={home.label}>\n <Home className=\"size-4\" aria-hidden=\"true\" />\n </BreadcrumbPage>\n )}\n </BreadcrumbItem>\n {current ? (\n <>\n {hasMid ? (\n <>\n <BreadcrumbSeparator className=\"md:hidden\" />\n <BreadcrumbItem className=\"md:hidden\">\n <BreadcrumbEllipsis aria-label={t('appShell.breadcrumb.collapsed', { count: mid.length })} />\n </BreadcrumbItem>\n {mid.map((b, i) => (\n <React.Fragment key={`mid-${i}`}>\n <BreadcrumbSeparator className=\"hidden md:inline-flex\" />\n <BreadcrumbItem className=\"hidden md:inline-flex\">\n {b.href ? (\n <BreadcrumbLink asChild title={b.label}>\n <Link href={b.href}>{b.label}</Link>\n </BreadcrumbLink>\n ) : (\n <BreadcrumbLink title={b.label} aria-disabled=\"true\" tabIndex={-1}>\n {b.label}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </React.Fragment>\n ))}\n </>\n ) : null}\n <BreadcrumbSeparator />\n <BreadcrumbItem>\n <BreadcrumbPage title={current.label}>{current.label}</BreadcrumbPage>\n </BreadcrumbItem>\n </>\n ) : null}\n </BreadcrumbList>\n </BreadcrumbNav>\n )\n })()}\n </div>\n <div className=\"flex items-center gap-1.5 sm:gap-2 md:gap-3 text-sm shrink-0\">\n <StatusBadgeInjectionSpot\n spotId={GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n <InjectionSpot\n spotId={BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n {renderedTopbarInjectedActions}\n <AiAssistantLauncher variant=\"topbar\" />\n {rightHeaderSlot ? (\n rightHeaderSlot\n ) : (\n <span className=\"opacity-80\">{email || t('appShell.userFallback')}</span>\n )}\n </div>\n </header>\n <ProgressTopBar t={t} className=\"sticky top-0 z-sticky\" />\n <main className=\"flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl\">\n <InjectionSpot spotId={BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID} context={injectionContext} />\n <FlashMessages />\n <PartialIndexBanner />\n <UpgradeActionBanner />\n <LastOperationBanner />\n <InjectionSpot spotId={BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID} context={injectionContext} />\n <InjectionSpot\n spotId={LEGACY_GLOBAL_MUTATION_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n <div id=\"om-top-banners\" className=\"mb-3 space-y-2\" />\n {children}\n <InjectionSpot spotId={BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID} context={injectionContext} />\n </main>\n <footer className=\"border-t bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/80 px-4 py-3 flex flex-wrap items-center justify-end gap-4\">\n {version ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('appShell.version', { version })}\n </span>\n ) : null}\n <nav className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n <Link href=\"/terms\" className=\"transition hover:text-foreground\">\n {t('common.terms')}\n </Link>\n <Link href=\"/privacy\" className=\"transition hover:text-foreground\">\n {t('common.privacy')}\n </Link>\n </nav>\n </footer>\n </div>\n\n {/* Mobile drawer */}\n {mobileOpen && (\n <div className=\"lg:hidden fixed inset-0 z-modal\">\n <div className=\"absolute inset-0 bg-black/50 backdrop-blur-sm\" onClick={() => setMobileOpen(false)} aria-hidden=\"true\" />\n <aside className=\"absolute left-0 top-0 flex h-full w-[280px] max-w-[85vw] flex-col bg-background border-r shadow-lg overflow-hidden\">\n <div className=\"shrink-0 flex items-center justify-between gap-2 border-b px-4 py-3\">\n <Link href=\"/backend\" className=\"flex items-center gap-2 min-w-0 text-sm font-semibold\" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={28} height={28} className=\"rounded shrink-0\" />\n <span className=\"truncate\">{resolvedProductName}</span>\n </Link>\n <IconButton variant=\"ghost\" size=\"sm\" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n {mobileSidebarSlot && (\n <div className=\"shrink-0 border-b px-3 py-2\">\n {mobileSidebarSlot}\n </div>\n )}\n {sidebarMode !== 'main' ? (\n <div className=\"shrink-0 flex items-center gap-5 border-b px-4 pt-3 pb-0\" role=\"tablist\">\n {([\n { id: 'main' as const, label: t('backend.nav.main', 'Main') },\n {\n id: 'section' as const,\n label:\n sidebarMode === 'settings'\n ? settingsSectionTitle ?? t('backend.nav.settings', 'Settings')\n : profileSectionTitle ?? t('backend.nav.profile', 'Profile'),\n },\n ]).map((tab) => {\n const isActive =\n tab.id === 'main' ? mobileDrawerView === 'main' : mobileDrawerView === 'auto'\n const tabId = `mobile-drawer-tab-${tab.id}`\n return (\n <button\n key={tab.id}\n id={tabId}\n type=\"button\"\n role=\"tab\"\n aria-selected={isActive}\n aria-controls=\"mobile-drawer-tabpanel\"\n onClick={() => setMobileDrawerView(tab.id === 'main' ? 'main' : 'auto')}\n className=\"relative inline-flex items-center pb-2 text-sm font-medium leading-5 tracking-tight transition-colors focus:outline-none data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground\"\n data-active={isActive}\n >\n <span>{tab.label}</span>\n {isActive ? (\n <span\n className=\"absolute -bottom-px left-0 right-0 h-0.5 bg-foreground\"\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n )\n })}\n </div>\n ) : null}\n <div\n id=\"mobile-drawer-tabpanel\"\n role={sidebarMode !== 'main' ? 'tabpanel' : undefined}\n aria-labelledby={\n sidebarMode !== 'main'\n ? `mobile-drawer-tab-${mobileDrawerView === 'main' ? 'main' : 'section'}`\n : undefined\n }\n className=\"min-h-0 flex-1 overflow-y-auto overflow-x-hidden p-3\"\n >\n {/* Force expanded sidebar in mobile drawer, hide its header and collapse toggle */}\n {renderSidebar(false, true, mobileDrawerView === 'main')}\n </div>\n </aside>\n </div>\n )}\n </div>\n <UmesDevToolsPanel />\n </HeaderContext.Provider>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport Link from 'next/link'\nimport Image from 'next/image'\nimport { ChevronDown, ChevronLeft, Home, PanelLeftClose, PanelLeftOpen, Search, X } from 'lucide-react'\nimport { Button } from '../primitives/button'\nimport {\n Breadcrumb as BreadcrumbNav,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from '../primitives/breadcrumb'\nimport { IconButton } from '../primitives/icon-button'\nimport { Input } from '../primitives/input'\nimport { SearchInput } from '../primitives/search-input'\nimport { Checkbox } from '../primitives/checkbox'\nimport { Separator } from '../primitives/separator'\nimport { FlashMessages } from './FlashMessages'\nimport { QueryProvider } from '../theme/QueryProvider'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { apiCall } from './utils/apiCall'\nimport { LastOperationBanner } from './operations/LastOperationBanner'\nimport { ProgressTopBar } from './progress/ProgressTopBar'\nimport { UpgradeActionBanner } from './upgrades/UpgradeActionBanner'\nimport { PartialIndexBanner } from './indexes/PartialIndexBanner'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { slugifySidebarId } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { cloneSidebarGroups } from './sidebar/customization-helpers'\nimport type { SectionNavGroup } from './section-page/types'\nimport { InjectionSpot } from './injection/InjectionSpot'\nimport type { InjectionMenuItem } from '@open-mercato/shared/modules/widgets/injection'\nimport { LEGACY_GLOBAL_MUTATION_INJECTION_SPOT_ID } from './injection/mutationEvents'\nimport { mergeMenuItems } from './injection/mergeMenuItems'\nimport { useInjectedMenuItems } from './injection/useInjectedMenuItems'\nimport { resolveInjectedIcon } from './injection/resolveInjectedIcon'\nimport { useEventBridge } from './injection/eventBridge'\nimport { StatusBadgeInjectionSpot } from './injection/StatusBadgeInjectionSpot'\nimport { UmesDevToolsPanel } from './devtools'\nimport { AiDockProvider } from '../ai/AiDock'\nimport { AiChatSessionsProvider } from '../ai/AiChatSessions'\nimport { AiAssistantLauncher } from '../ai/AiAssistantLauncher'\nimport { BackendChromeProvider, useBackendChrome } from './BackendChromeProvider'\nimport {\n BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID,\n BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID,\n BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID,\n BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID,\n BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,\n GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,\n GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,\n} from './injection/spotIds'\n\nexport type ShellLogo = {\n src: string\n alt?: string\n}\n\nexport type AppShellProps = {\n productName?: string\n logo?: ShellLogo\n email?: string\n groups: {\n id?: string\n name: string\n defaultName?: string\n items: {\n id?: string\n href: string\n title: string\n defaultTitle?: string\n icon?: React.ReactNode\n iconName?: string\n iconMarkup?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n children?: {\n id?: string\n href: string\n title: string\n defaultTitle?: string\n icon?: React.ReactNode\n iconName?: string\n iconMarkup?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n }[]\n }[]\n }[]\n children: React.ReactNode\n rightHeaderSlot?: React.ReactNode\n sidebarCollapsedDefault?: boolean\n currentTitle?: string\n breadcrumb?: Array<{ label: string; href?: string }>\n // Optional: full admin nav API to refresh sidebar client-side\n adminNavApi?: string\n version?: string\n settingsSectionTitle?: string\n settingsPathPrefixes?: string[]\n settingsSections?: SectionNavGroup[]\n profileSections?: SectionNavGroup[]\n profileSectionTitle?: string\n profilePathPrefixes?: string[]\n mobileSidebarSlot?: React.ReactNode\n}\n\ntype Breadcrumb = Array<{ label: string; href?: string }>\n\ntype SidebarGroup = AppShellProps['groups'][number]\ntype SidebarItem = SidebarGroup['items'][number]\n\nfunction convertInjectedMenuItemToSidebarItem(item: InjectionMenuItem, title: string): SidebarItem | null {\n if (!item.href) return null\n return {\n id: item.id,\n href: item.href,\n title,\n defaultTitle: title,\n icon: resolveInjectedIcon(item.icon) ?? undefined,\n iconName: item.icon,\n enabled: true,\n hidden: false,\n pageContext: 'main',\n }\n}\n\nfunction resolveInjectedMenuLabel(\n item: { id: string; label?: string; labelKey?: string },\n t: (key: string, fallback?: string) => string,\n): 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\nfunction mergeSidebarItemsWithInjected(\n items: SidebarItem[],\n injectedItems: InjectionMenuItem[],\n t: (key: string, fallback?: string) => string,\n): SidebarItem[] {\n if (injectedItems.length === 0) return items\n\n const builtInById = new Map<string, SidebarItem>()\n for (const item of items) {\n builtInById.set(item.id ?? item.href, item)\n }\n\n const merged = mergeMenuItems(\n items.map((item) => ({\n id: item.id ?? item.href,\n })),\n injectedItems,\n )\n\n const result: SidebarItem[] = []\n for (const entry of merged) {\n if (entry.source === 'built-in') {\n const original = builtInById.get(entry.id)\n if (original) result.push(original)\n continue\n }\n const translatedLabel = resolveInjectedMenuLabel(\n { id: entry.id, label: entry.label, labelKey: entry.labelKey },\n t,\n )\n const converted = convertInjectedMenuItemToSidebarItem(\n {\n id: entry.id,\n label: translatedLabel,\n icon: entry.icon,\n href: entry.href,\n },\n translatedLabel,\n )\n if (converted) result.push(converted)\n }\n\n return result\n}\n\nfunction mergeSidebarGroupsWithInjected(\n groups: SidebarGroup[],\n injectedItems: InjectionMenuItem[],\n t: (key: string, fallback?: string) => string,\n): SidebarGroup[] {\n if (injectedItems.length === 0) return groups\n\n const injectedByGroup = new Map<string, InjectionMenuItem[]>()\n const ungrouped: InjectionMenuItem[] = []\n\n for (const item of injectedItems) {\n if (item.groupId && item.groupId.trim().length > 0) {\n const groupItems = injectedByGroup.get(item.groupId) ?? []\n groupItems.push(item)\n injectedByGroup.set(item.groupId, groupItems)\n continue\n }\n ungrouped.push(item)\n }\n\n const nextGroups = groups.map((group, index) => {\n const groupId = group.id || resolveGroupKey(group)\n const groupInjected = [\n ...(injectedByGroup.get(groupId) ?? []),\n ...(index === 0 ? ungrouped : []),\n ]\n return {\n ...group,\n items: mergeSidebarItemsWithInjected(group.items, groupInjected, t),\n }\n })\n\n const existingIds = new Set(nextGroups.map((group) => group.id || resolveGroupKey(group)))\n for (const [groupId, items] of injectedByGroup.entries()) {\n if (existingIds.has(groupId)) continue\n const first = items[0]\n const label = first.groupLabelKey\n ? t(first.groupLabelKey, first.groupLabel ?? groupId)\n : (first.groupLabel ?? groupId)\n const groupItems = mergeSidebarItemsWithInjected([], items, t)\n if (groupItems.length === 0) continue\n nextGroups.push({\n id: groupId,\n name: label,\n defaultName: label,\n items: groupItems,\n })\n }\n\n return nextGroups\n}\n\nfunction mergeSectionGroupsWithInjected(\n sections: SectionNavGroup[],\n injectedItems: InjectionMenuItem[],\n t: (key: string, fallback?: string) => string,\n): SectionNavGroup[] {\n if (injectedItems.length === 0) return sections\n const byGroup = new Map<string, InjectionMenuItem[]>()\n for (const item of injectedItems) {\n const groupId = item.groupId && item.groupId.trim().length > 0 ? item.groupId : 'injected'\n const bucket = byGroup.get(groupId) ?? []\n bucket.push(item)\n byGroup.set(groupId, bucket)\n }\n\n const nextSections = sections.map((section) => {\n const sectionItems = byGroup.get(section.id) ?? []\n if (sectionItems.length === 0) return section\n const mergedItems = mergeMenuItems(\n section.items.map((item) => ({ id: item.id, item })),\n sectionItems,\n ).flatMap((item) => {\n if (item.source === 'built-in') {\n const original = section.items.find((entry) => entry.id === item.id)\n return original ? [original] : []\n }\n if (!item.href) return []\n const label = resolveInjectedMenuLabel(item, t)\n return [{\n id: item.id,\n label,\n href: item.href,\n icon: resolveInjectedIcon(item.icon) ?? undefined,\n }]\n })\n return {\n ...section,\n items: mergedItems,\n }\n })\n\n for (const [sectionId, sectionItems] of byGroup.entries()) {\n const exists = nextSections.some((section) => section.id === sectionId)\n if (exists) continue\n const first = sectionItems[0]\n const label = first.groupLabelKey\n ? t(first.groupLabelKey, first.groupLabel ?? sectionId)\n : (first.groupLabel ?? sectionId)\n const items = sectionItems.flatMap((item) => {\n if (!item.href) return []\n const itemLabel = resolveInjectedMenuLabel(item, t)\n return [{\n id: item.id,\n label: itemLabel,\n href: item.href,\n icon: resolveInjectedIcon(item.icon) ?? undefined,\n }]\n })\n if (items.length === 0) continue\n nextSections.push({ id: sectionId, label, items })\n }\n\n return nextSections\n}\n\nfunction resolveGroupKey(group: SidebarGroup): string {\n if (group.id && group.id.length) return group.id\n if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName)\n return slugifySidebarId(group.name)\n}\n\nfunction resolveItemKey(item: { id?: string; href: string }): string {\n const candidate = item.id?.trim()\n if (candidate && candidate.length > 0) return candidate\n return item.href\n}\n\nfunction SerializedIcon({ markup }: { markup: string }) {\n return <span aria-hidden=\"true\" dangerouslySetInnerHTML={{ __html: markup }} />\n}\n\nfunction renderIcon(\n icon: React.ReactNode | undefined,\n iconName: string | undefined,\n iconMarkup: string | undefined,\n fallback: React.ReactNode,\n) {\n if (icon) return icon\n if (iconName) {\n const resolved = resolveInjectedIcon(iconName)\n if (resolved) return resolved\n }\n if (iconMarkup) return <SerializedIcon markup={iconMarkup} />\n return fallback\n}\n\nconst HeaderContext = createContext<{\n setBreadcrumb: (b?: Breadcrumb) => void\n setTitle: (t?: string) => void\n} | null>(null)\n\nexport function ApplyBreadcrumb({ breadcrumb, title, titleKey }: { breadcrumb?: Array<{ label: string; href?: string; labelKey?: string }>; title?: string; titleKey?: string }) {\n const ctx = useContext(HeaderContext)\n const t = useT()\n const resolvedBreadcrumb = React.useMemo<Breadcrumb | undefined>(() => {\n if (!breadcrumb) return undefined\n return breadcrumb.map(({ label, labelKey, href }) => {\n const translated = labelKey ? t(labelKey) : undefined\n const finalLabel = translated && translated !== labelKey ? translated : label\n return {\n href,\n label: finalLabel,\n }\n })\n }, [breadcrumb, t])\n const resolvedTitle = React.useMemo(() => {\n if (!titleKey) return title\n const translated = t(titleKey)\n if (translated && translated !== titleKey) return translated\n return title\n }, [titleKey, title, t])\n React.useEffect(() => {\n ctx?.setBreadcrumb(resolvedBreadcrumb)\n if (resolvedTitle !== undefined) ctx?.setTitle(resolvedTitle)\n }, [ctx, resolvedBreadcrumb, resolvedTitle])\n return null\n}\n\nconst DefaultIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M8 6h13M8 12h13M8 18h13\"/>\n <path d=\"M3 6h.01M3 12h.01M3 18h.01\"/>\n </svg>\n)\n\n// DataTable icon used for dynamic custom entity records links\nconst DataTableIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\" ry=\"2\"/>\n <line x1=\"3\" y1=\"8\" x2=\"21\" y2=\"8\"/>\n <line x1=\"9\" y1=\"8\" x2=\"9\" y2=\"20\"/>\n <line x1=\"15\" y1=\"8\" x2=\"15\" y2=\"20\"/>\n </svg>\n)\n\nconst CustomizeIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.05.05a2 2 0 1 1-2.83 2.83l-.05-.05A1.65 1.65 0 0 0 15 19.4a1.65 1.65 0 0 0-1 .6 1.65 1.65 0 0 0-.33 1.82l-.05.05a2 2 0 1 1-2.83-2.83l.05-.05A1.65 1.65 0 0 0 9 15a1.65 1.65 0 0 0-1-.6 1.65 1.65 0 0 0-1.82.33l-.05.05a2 2 0 1 1-2.83-2.83l.05-.05A1.65 1.65 0 0 0 4.6 9 1.65 1.65 0 0 0 4 8a1.65 1.65 0 0 0-.6-1.82l-.05-.05a2 2 0 1 1 2.83-2.83l.05.05A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-.6 1.65 1.65 0 0 0 .33-1.82l.05-.05a2 2 0 1 1 2.83 2.83l-.05.05A1.65 1.65 0 0 0 15 9a1.65 1.65 0 0 0 1 .6 1.65 1.65 0 0 0 1.82-.33l.05-.05a2 2 0 1 1 2.83 2.83l-.05.05A1.65 1.65 0 0 0 19.4 15z\" />\n </svg>\n)\n\nconst BackArrowIcon = (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M19 12H5M12 19l-7-7 7-7\" />\n </svg>\n)\n\nfunction Chevron({ open }: { open: boolean }) {\n return (\n <svg className={`transition-transform ${open ? 'rotate-180' : ''}`} width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M6 9l6 6 6-6\"/></svg>\n )\n}\n\nexport function AppShell(props: AppShellProps) {\n return (\n <QueryProvider>\n <BackendChromeProvider adminNavApi={props.adminNavApi}>\n <AiChatSessionsProvider>\n <AiDockProvider>\n <AppShellBody {...props} />\n </AiDockProvider>\n </AiChatSessionsProvider>\n </BackendChromeProvider>\n </QueryProvider>\n )\n}\n\nfunction AppShellBody({ productName, logo, email, groups, rightHeaderSlot, children, sidebarCollapsedDefault = false, currentTitle, breadcrumb, version, settingsSectionTitle, settingsPathPrefixes = [], settingsSections, profileSections, profileSectionTitle, profilePathPrefixes = [], mobileSidebarSlot }: AppShellProps) {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const t = useT()\n const locale = useLocale()\n const { payload: chromePayload, isReady: isChromeReady, isLoading: isChromeLoading } = useBackendChrome()\n const resolvedGroups = React.useMemo(\n () => cloneSidebarGroups(chromePayload?.groups ?? groups),\n [chromePayload?.groups, groups],\n )\n const resolvedSettingsSections = chromePayload?.settingsSections ?? settingsSections\n const resolvedSettingsPathPrefixes = chromePayload?.settingsPathPrefixes ?? settingsPathPrefixes\n const resolvedProfileSections = chromePayload?.profileSections ?? profileSections\n const resolvedProfilePathPrefixes = chromePayload?.profilePathPrefixes ?? profilePathPrefixes\n const { items: mainSidebarInjectedMenuItems } = useInjectedMenuItems('menu:sidebar:main')\n const { items: settingsSidebarInjectedMenuItems } = useInjectedMenuItems('menu:sidebar:settings')\n const { items: profileSidebarInjectedMenuItems } = useInjectedMenuItems('menu:sidebar:profile')\n const { items: topbarInjectedMenuItems } = useInjectedMenuItems('menu:topbar:actions')\n useEventBridge() // SSE DOM Event Bridge \u2014 singleton SSE connection for real-time server events\n const resolvedProductName = productName ?? t('appShell.productName')\n const [mobileOpen, setMobileOpen] = React.useState(false)\n // When the mobile drawer opens on a settings/profile route, it follows the\n // section sidebar by default. Set to 'main' to force-show the main nav even\n // when the route is in a section context. Reset on close.\n const [mobileDrawerView, setMobileDrawerView] = React.useState<'auto' | 'main'>('auto')\n React.useEffect(() => {\n if (!mobileOpen) setMobileDrawerView('auto')\n }, [mobileOpen])\n // Initialize from server-provided prop only to avoid hydration flicker\n const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault)\n // Maintain internal nav state so we can augment it client-side\n const [navGroups, setNavGroups] = React.useState(resolvedGroups)\n const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>(() =>\n Object.fromEntries(resolvedGroups.map((g) => [resolveGroupKey(g), true])) as Record<string, boolean>\n )\n const [headerTitle, setHeaderTitle] = React.useState<string | undefined>(currentTitle)\n const [headerBreadcrumb, setHeaderBreadcrumb] = React.useState<Breadcrumb | undefined>(breadcrumb)\n const [navQuery, setNavQuery] = React.useState('')\n const navQueryNorm = navQuery.trim().toLowerCase()\n const navQueryActive = navQueryNorm.length > 0\n const matchesQuery = React.useCallback((label: string | undefined) => {\n if (!navQueryActive) return true\n if (!label) return false\n return label.toLowerCase().includes(navQueryNorm)\n }, [navQueryActive, navQueryNorm])\n const effectiveCollapsed = collapsed\n const expandedSidebarWidth = '240px'\n\n // Track scroll position of the desktop sidebar's inner scroll container so we can\n // flip the affordance chevron between down/up (and hide it entirely when content\n // fits without scrolling). The inner div is rendered deep in renderSidebar /\n // renderSectionSidebar \u2014 we tag it with `data-sidebar-scroll=\"true\"` and look it\n // up via the aside ref so we don't have to thread refs through the JSX tree.\n const sidebarAsideRef = React.useRef<HTMLElement>(null)\n const [sidebarScrollState, setSidebarScrollState] = React.useState<'down' | 'up' | 'none'>('down')\n const sidebarScrollIntentRef = React.useRef<'top' | 'bottom' | null>(null)\n\n // Click-to-scroll handler for the sidebar affordance chevron (#1803). Resolves the\n // scroll target lazily through the aside ref so we don't have to thread refs into\n // renderSidebar; respects `prefers-reduced-motion` by falling back to instant\n // scrolling when the user has opted out of smooth motion.\n const handleSidebarChevronScroll = React.useCallback((target: 'top' | 'bottom') => {\n const aside = sidebarAsideRef.current\n if (!aside) return\n const scrollTarget = aside.querySelector<HTMLElement>('[data-sidebar-scroll=\"true\"]')\n if (!scrollTarget) return\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n const behavior: ScrollBehavior = prefersReducedMotion ? 'auto' : 'smooth'\n const maxScrollTop = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight)\n if (maxScrollTop <= 1) {\n sidebarScrollIntentRef.current = null\n setSidebarScrollState('none')\n return\n }\n sidebarScrollIntentRef.current = target\n setSidebarScrollState(target === 'bottom' ? 'up' : 'down')\n scrollTarget.scrollTo({\n top: target === 'top' ? 0 : maxScrollTop,\n behavior,\n })\n }, [])\n React.useEffect(() => {\n const aside = sidebarAsideRef.current\n if (!aside) return\n const target = aside.querySelector<HTMLElement>('[data-sidebar-scroll=\"true\"]')\n if (!target) return\n const update = () => {\n const { scrollTop, scrollHeight, clientHeight } = target\n const canScroll = scrollHeight > clientHeight + 1\n if (!canScroll) {\n sidebarScrollIntentRef.current = null\n setSidebarScrollState('none')\n return\n }\n const maxScrollTop = Math.max(0, scrollHeight - clientHeight)\n const atTop = scrollTop <= 8\n const atBottom = scrollTop >= maxScrollTop - 8\n const scrollIntent = sidebarScrollIntentRef.current\n if (scrollIntent === 'bottom') {\n if (atBottom) sidebarScrollIntentRef.current = null\n setSidebarScrollState('up')\n return\n }\n if (scrollIntent === 'top') {\n if (atTop) sidebarScrollIntentRef.current = null\n setSidebarScrollState('down')\n return\n }\n setSidebarScrollState(atBottom ? 'up' : 'down')\n }\n update()\n target.addEventListener('scroll', update, { passive: true })\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(update) : null\n ro?.observe(target)\n return () => {\n target.removeEventListener('scroll', update)\n ro?.disconnect()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pathname, effectiveCollapsed])\n const injectionContext = React.useMemo(\n () => ({\n path: pathname ?? '',\n query: searchParams?.toString() ?? '',\n }),\n [pathname, searchParams],\n )\n\n const isOnSettingsPath = React.useMemo(() => {\n if (!pathname) return false\n if (pathname === '/backend/settings') return true\n return resolvedSettingsPathPrefixes.some((prefix) => pathname.startsWith(prefix))\n }, [pathname, resolvedSettingsPathPrefixes])\n\n const isOnProfilePath = React.useMemo(() => {\n if (!pathname) return false\n if (pathname === '/backend/profile') return true\n return resolvedProfilePathPrefixes.some((prefix) => pathname.startsWith(prefix))\n }, [pathname, resolvedProfilePathPrefixes])\n\n const sidebarMode: 'main' | 'settings' | 'profile' =\n isOnSettingsPath ? 'settings' :\n isOnProfilePath ? 'profile' :\n 'main'\n\n const mainNavGroupsWithInjected = React.useMemo(\n () => mergeSidebarGroupsWithInjected(navGroups, mainSidebarInjectedMenuItems, t),\n [mainSidebarInjectedMenuItems, navGroups, t],\n )\n\n // Lock body scroll when mobile drawer is open so touch scroll stays in the drawer\n React.useEffect(() => {\n if (!mobileOpen || typeof document === 'undefined') return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [mobileOpen])\n\n React.useEffect(() => {\n try {\n const savedOpen = typeof window !== 'undefined' ? localStorage.getItem('om:sidebarOpenGroups') : null\n if (!savedOpen) return\n const parsed = JSON.parse(savedOpen) as Record<string, boolean>\n setOpenGroups((prev) => {\n const next = { ...prev }\n for (const group of resolvedGroups) {\n const key = resolveGroupKey(group)\n if (key in parsed) next[key] = !!parsed[key]\n else if (group.name in parsed) next[key] = !!parsed[group.name]\n }\n return next\n })\n } catch {\n // ignore localStorage errors to avoid breaking hydration\n }\n }, [resolvedGroups])\n\n const toggleGroup = (groupId: string) => setOpenGroups((prev) => ({ ...prev, [groupId]: prev[groupId] === false }))\n\n const asideWidth = effectiveCollapsed ? '80px' : expandedSidebarWidth\n // Use min-h-svh so the border extends with tall content; no overflow so sticky bottom works\n const asideClassesBase = `border-r bg-background py-4`;\n\n // Persist collapse state to localStorage and cookie. Both writes can throw in\n // private/incognito mode (storage blocked) or when cookies are disabled \u2014\n // the persisted preference is purely a UX nice-to-have, never functional, so\n // swallow the failure and let the component fall back to the default state.\n React.useEffect(() => {\n try { localStorage.setItem('om:sidebarCollapsed', collapsed ? '1' : '0') } catch { /* localStorage blocked (private mode) \u2014 non-critical */ }\n try {\n document.cookie = `om_sidebar_collapsed=${collapsed ? '1' : '0'}; path=/; max-age=31536000; samesite=lax`\n } catch { /* cookies disabled \u2014 non-critical */ }\n }, [collapsed])\n\n // Two-level sidebar (Option B): when entering settings/profile mode, force the\n // main sidebar to collapsed (icons only) so the section sub-nav can sit beside\n // it; restore the user's previous expansion when returning to the main mode.\n // Initial ref is 'main' so direct mounts on /backend/settings also auto-collapse.\n const collapsedBeforeSectionRef = React.useRef<boolean | null>(null)\n const previousSidebarModeRef = React.useRef<'main' | 'settings' | 'profile'>('main')\n React.useEffect(() => {\n const previous = previousSidebarModeRef.current\n if (previous === 'main' && sidebarMode !== 'main') {\n collapsedBeforeSectionRef.current = collapsed\n if (!collapsed) setCollapsed(true)\n } else if (previous !== 'main' && sidebarMode === 'main' && collapsedBeforeSectionRef.current !== null) {\n const restoreTo = collapsedBeforeSectionRef.current\n collapsedBeforeSectionRef.current = null\n if (collapsed !== restoreTo) setCollapsed(restoreTo)\n }\n previousSidebarModeRef.current = sidebarMode\n }, [sidebarMode, collapsed])\n React.useEffect(() => {\n try { localStorage.setItem('om:sidebarOpenGroups', JSON.stringify(openGroups)) } catch { /* localStorage blocked (private mode) \u2014 non-critical */ }\n }, [openGroups])\n\n // Ensure current route's group is expanded on load\n React.useEffect(() => {\n const activeGroup = navGroups.find((g) => g.items.some((i) => pathname?.startsWith(i.href)))\n if (!activeGroup) return\n const key = resolveGroupKey(activeGroup)\n setOpenGroups((prev) => (prev[key] === false ? { ...prev, [key]: true } : prev))\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pathname, navGroups])\n // Keep header state in sync with props (server-side updates)\n React.useEffect(() => {\n setHeaderTitle(currentTitle)\n setHeaderBreadcrumb(breadcrumb)\n }, [currentTitle, breadcrumb])\n // Clear breadcrumb on client-side navigation so stale state doesn't persist;\n // the new page's ApplyBreadcrumb (if any) will set the correct values\n const prevPathname = React.useRef(pathname)\n React.useEffect(() => {\n if (pathname !== prevPathname.current) {\n prevPathname.current = pathname\n setHeaderTitle(undefined)\n setHeaderBreadcrumb(undefined)\n }\n }, [pathname])\n\n // Keep navGroups in sync when server-provided groups change\n React.useEffect(() => {\n setNavGroups(cloneSidebarGroups(resolvedGroups))\n }, [resolvedGroups])\n\n function renderSectionSidebar(\n sections: SectionNavGroup[],\n title: string,\n compact: boolean,\n hideHeader?: boolean,\n hideSearch?: boolean\n ) {\n const sortedSections = [...sections].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n const lastVisibleIndex = sortedSections.length - 1\n\n return (\n <div className=\"flex h-full flex-col gap-3\">\n {!hideHeader && (\n <div className=\"mb-2\">\n <Link\n href=\"/backend\"\n className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}\n aria-label={t('appShell.goToDashboard')}\n >\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className=\"rounded-full shrink-0\" />\n {!compact && <span className=\"truncate text-sm font-medium text-foreground\">{resolvedProductName}</span>}\n </Link>\n </div>\n )}\n {!compact && !hideSearch && (\n <SearchInput\n value={navQuery}\n onChange={setNavQuery}\n placeholder={t('appShell.searchNavPlaceholder', 'Search...')}\n aria-label={t('appShell.searchNavAria', 'Search navigation')}\n clearLabel={t('appShell.searchNavClear', 'Clear search')}\n className=\"mb-2\"\n />\n )}\n <div data-sidebar-scroll=\"true\" className={`flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? '-ml-2 pl-2' : '-ml-3 pl-3'}`}>\n <nav className=\"flex flex-col gap-2\">\n {sortedSections.map((section, sectionIndex) => {\n const sectionNavQueryActive = hideSearch ? false : navQueryActive\n const matchesItemQuery = (item: typeof section.items[number]): boolean => {\n if (!sectionNavQueryActive) return true\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n if (matchesQuery(label)) return true\n return Array.isArray(item.children) && item.children.some(matchesItemQuery)\n }\n const visibleItems = sectionNavQueryActive\n ? section.items.filter(matchesItemQuery)\n : section.items\n if (visibleItems.length === 0) return null\n const sortedItems = [...visibleItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n const sectionLabel = section.labelKey ? t(section.labelKey, section.label) : section.label\n const sectionKey = `settings:${section.id}`\n const open = openGroups[sectionKey] !== false\n const sortSectionItems = (items: typeof section.items = []) =>\n [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n const filterChildren = (children: typeof section.items | undefined) => {\n if (!children) return [] as typeof section.items\n if (!sectionNavQueryActive) return [...children]\n return children.filter(matchesItemQuery)\n }\n\n const renderSectionItem = (item: (typeof section.items)[number], depth = 0): React.ReactNode => {\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n const childItems = sortSectionItems(filterChildren(item.children))\n const isOnItemBranch = !!pathname && (\n pathname === item.href ||\n pathname.startsWith(`${item.href}/`)\n )\n const hasActiveChild = !!(pathname && childItems.some((child) => (\n pathname === child.href ||\n pathname.startsWith(`${child.href}/`)\n )))\n const showChildren = childItems.length > 0 && (isOnItemBranch || sectionNavQueryActive)\n const isActive = isOnItemBranch || hasActiveChild\n const base = compact ? 'w-10 h-10 justify-center' : 'w-full py-2 gap-2'\n const spacingStyle = !compact\n ? {\n paddingLeft: `${12 + depth * 16}px`,\n paddingRight: '12px',\n }\n : undefined\n\n return (\n <React.Fragment key={item.id}>\n <Link\n href={item.href}\n className={`relative text-sm font-medium rounded-lg inline-flex items-center ${base} ${\n isActive\n ? 'bg-muted text-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n style={spacingStyle}\n title={compact ? label : undefined}\n data-menu-item-id={item.id}\n onClick={() => setMobileOpen(false)}\n >\n {isActive && (\n <span aria-hidden className={`absolute ${compact ? 'left-[-20px]' : 'left-[-12px]'} top-2 w-1 h-5 rounded-r bg-foreground`} />\n )}\n <span className=\"flex items-center justify-center shrink-0\">\n {renderIcon(\n item.icon,\n item.iconName,\n item.iconMarkup,\n item.href.includes('/backend/entities/user/') && item.href.endsWith('/records') ? DataTableIcon : DefaultIcon,\n )}\n </span>\n {!compact && <span className=\"truncate\">{label}</span>}\n </Link>\n {showChildren ? childItems.map((child) => renderSectionItem(child, depth + 1)) : null}\n </React.Fragment>\n )\n }\n\n return (\n <div key={section.id}>\n {!compact && (\n <Button\n variant=\"muted\"\n onClick={() => toggleGroup(sectionKey)}\n className=\"w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1\"\n aria-expanded={open}\n >\n <span>{sectionLabel}</span>\n <Chevron open={open} />\n </Button>\n )}\n {(open || compact) && (\n <div className={`flex flex-col ${compact ? 'items-center' : ''} gap-1`}>\n {sortedItems.map((item) => renderSectionItem(item))}\n </div>\n )}\n {sectionIndex !== lastVisibleIndex && <div className={`my-2 border-t ${compact ? '-ml-2 -mr-3' : '-ml-3 -mr-4'}`} />}\n </div>\n )\n })}\n </nav>\n </div>\n </div>\n )\n }\n\n function renderSidebar(compact: boolean, hideHeader?: boolean, forceMainOnly?: boolean) {\n if (!isChromeReady && isChromeLoading) {\n return (\n <div className=\"flex flex-col min-h-full gap-3\" data-testid=\"backend-chrome-loading\">\n {!hideHeader ? (\n <div className=\"mb-2\">\n <Link\n href=\"/backend\"\n className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}\n aria-label={t('appShell.goToDashboard')}\n >\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className=\"rounded-full shrink-0\" />\n {!compact && <span className=\"truncate text-sm font-medium text-foreground\">{resolvedProductName}</span>}\n </Link>\n </div>\n ) : null}\n <div className=\"flex flex-1 flex-col gap-3 pr-1\">\n <div className=\"space-y-3\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"space-y-2 pl-1\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"h-8 rounded bg-muted/50\" />\n </div>\n </div>\n <div className=\"space-y-3\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"space-y-2 pl-1\">\n <div className=\"h-8 rounded bg-muted/50\" />\n <div className=\"h-8 rounded bg-muted/50\" />\n </div>\n </div>\n </div>\n </div>\n )\n }\n\n if (!forceMainOnly && sidebarMode === 'settings' && resolvedSettingsSections && resolvedSettingsSections.length > 0) {\n const mergedSettingsSections = mergeSectionGroupsWithInjected(\n resolvedSettingsSections,\n settingsSidebarInjectedMenuItems,\n t,\n )\n return renderSectionSidebar(\n mergedSettingsSections,\n settingsSectionTitle ?? t('backend.nav.settings', 'Settings'),\n compact,\n hideHeader\n )\n }\n\n if (!forceMainOnly && sidebarMode === 'profile' && resolvedProfileSections && resolvedProfileSections.length > 0) {\n const mergedProfileSections = mergeSectionGroupsWithInjected(\n resolvedProfileSections,\n profileSidebarInjectedMenuItems,\n t,\n )\n return renderSectionSidebar(\n mergedProfileSections,\n profileSectionTitle ?? t('backend.nav.profile', 'Profile'),\n compact,\n hideHeader\n )\n }\n\n const isMobileVariant = !!hideHeader\n const shouldRenderSidebarInjectionSpots = !isMobileVariant\n\n return (\n <div className=\"flex h-full flex-col gap-3\">\n {!hideHeader && (\n <div className=\"mb-2\">\n <Link\n href=\"/backend\"\n className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}\n aria-label={t('appShell.goToDashboard')}\n >\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className=\"rounded-full shrink-0\" />\n {!compact && <span className=\"truncate text-sm font-medium text-foreground\">{resolvedProductName}</span>}\n </Link>\n </div>\n )}\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {!compact && (\n <SearchInput\n value={navQuery}\n onChange={setNavQuery}\n placeholder={t('appShell.searchNavPlaceholder', 'Search...')}\n aria-label={t('appShell.searchNavAria', 'Search navigation')}\n clearLabel={t('appShell.searchNavClear', 'Clear search')}\n className=\"mb-2\"\n />\n )}\n <div data-sidebar-scroll=\"true\" className={`flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? '-ml-2 pl-2' : '-ml-3 pl-3'}`}>\n {(() => {\n const isSettingsPath = (href: string) => {\n if (href === '/backend/settings') return true\n return resolvedSettingsPathPrefixes.some((prefix) => href.startsWith(prefix))\n }\n\n const isMainItem = (item: SidebarItem) => {\n if (item.pageContext && item.pageContext !== 'main') return false\n if (isSettingsPath(item.href)) return false\n return true\n }\n\n const mainGroups = mainNavGroupsWithInjected.map((g) => ({\n ...g,\n items: g.items.filter((item) => isMainItem(item) && item.hidden !== true),\n })).filter((g) => g.items.length > 0)\n\n const mainLastVisibleGroupIndex = (() => {\n for (let idx = mainGroups.length - 1; idx >= 0; idx -= 1) {\n if (mainGroups[idx].items.some((item) => item.hidden !== true)) return idx\n }\n return -1\n })()\n\n return (\n <>\n <nav className=\"flex flex-col gap-2\" data-testid=\"sidebar\">\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {mainGroups.map((g, gi) => {\n const groupId = resolveGroupKey(g)\n const open = navQueryActive ? true : openGroups[groupId] !== false\n const visibleItems = g.items.filter((item) => {\n if (item.hidden === true) return false\n if (!navQueryActive) return true\n if (matchesQuery(item.title)) return true\n const itemChildren = (item.children ?? []).filter((c) => c.hidden !== true)\n return itemChildren.some((c) => matchesQuery(c.title))\n })\n if (visibleItems.length === 0) return null\n return (\n <div key={groupId}>\n {!compact && (\n <Button\n variant=\"muted\"\n onClick={() => toggleGroup(groupId)}\n className=\"w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1\"\n aria-expanded={open}\n >\n <span>{g.name}</span>\n <Chevron open={open} />\n </Button>\n )}\n {(open || compact) && (\n <div className={`flex flex-col ${compact ? 'items-center' : ''} gap-1`}>\n {visibleItems.map((i) => {\n const allChildItems = (i.children ?? []).filter((child) => child.hidden !== true)\n const matchingChildItems = navQueryActive\n ? allChildItems.filter((c) => matchesQuery(c.title))\n : allChildItems\n const childItems = navQueryActive ? matchingChildItems : allChildItems\n const showChildren = navQueryActive\n ? matchingChildItems.length > 0\n : (!!pathname && allChildItems.length > 0 && pathname.startsWith(i.href))\n const hasActiveChild = !!(pathname && allChildItems.some((c) => pathname.startsWith(c.href)))\n const isParentActive = (pathname === i.href) || (!navQueryActive && showChildren && !hasActiveChild)\n const base = compact ? 'w-10 h-10 justify-center' : 'w-full px-3 py-2 gap-2'\n return (\n <React.Fragment key={i.href}>\n <Link\n href={i.href}\n className={`relative text-sm font-medium rounded-lg inline-flex items-center ${base} ${\n isParentActive ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted'\n } ${i.enabled === false ? 'pointer-events-none opacity-50' : ''}`}\n aria-disabled={i.enabled === false}\n title={compact ? i.title : undefined}\n data-menu-item-id={i.id ?? i.href}\n onClick={() => setMobileOpen(false)}\n >\n {isParentActive ? (\n <span aria-hidden className={`absolute ${compact ? 'left-[-20px]' : 'left-[-12px]'} top-2 w-1 h-5 rounded-r bg-foreground`} />\n ) : null}\n <span className=\"flex items-center justify-center shrink-0\">\n {renderIcon(\n i.icon,\n i.iconName,\n i.iconMarkup,\n DefaultIcon,\n )}\n </span>\n {!compact && <span>{i.title}</span>}\n </Link>\n {showChildren ? (\n <div className={`relative flex flex-col ${compact ? 'items-center' : ''} gap-1`}>\n {!compact && (\n <span aria-hidden className=\"pointer-events-none absolute left-1.5 top-1 bottom-1 w-px bg-border\" />\n )}\n {childItems.map((c) => {\n const childActive = pathname?.startsWith(c.href)\n const childBase = compact ? 'w-10 h-8 justify-center' : 'w-full pl-5 pr-3 py-2 gap-2'\n return (\n <Link\n key={c.href}\n href={c.href}\n className={`relative text-sm font-medium rounded-lg inline-flex items-center ${childBase} ${\n childActive ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted'\n } ${c.enabled === false ? 'pointer-events-none opacity-50' : ''}`}\n aria-disabled={c.enabled === false}\n title={compact ? c.title : undefined}\n data-menu-item-id={c.id ?? c.href}\n onClick={() => setMobileOpen(false)}\n >\n {childActive ? (\n <span aria-hidden className={`absolute ${compact ? 'left-[-20px]' : 'left-[-12px]'} top-2 w-1 h-5 rounded-r bg-foreground`} />\n ) : null}\n <span className=\"flex items-center justify-center shrink-0\">\n {renderIcon(\n c.icon,\n c.iconName,\n c.iconMarkup,\n c.href.includes('/backend/entities/user/') && c.href.endsWith('/records') ? DataTableIcon : DefaultIcon,\n )}\n </span>\n {!compact && <span>{c.title}</span>}\n </Link>\n )\n })}\n </div>\n ) : null}\n </React.Fragment>\n )\n })}\n </div>\n )}\n {gi !== mainLastVisibleGroupIndex && <div className={`my-2 border-t ${compact ? '-ml-2 -mr-3' : '-ml-3 -mr-4'}`} />}\n </div>\n )\n })}\n </nav>\n </>\n )\n })()}\n </div>\n <div className=\"sticky bottom-0 bg-background pb-1\">\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {shouldRenderSidebarInjectionSpots ? (\n <StatusBadgeInjectionSpot\n spotId={GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n {shouldRenderSidebarInjectionSpots ? (\n <InjectionSpot\n spotId={BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n ) : null}\n </div>\n </div>\n )\n }\n\n function renderSectionAside() {\n let sections: SectionNavGroup[] | null = null\n let title = ''\n if (sidebarMode === 'settings' && resolvedSettingsSections && resolvedSettingsSections.length > 0) {\n sections = mergeSectionGroupsWithInjected(\n resolvedSettingsSections,\n settingsSidebarInjectedMenuItems,\n t,\n )\n title = settingsSectionTitle ?? t('backend.nav.settings', 'Settings')\n } else if (sidebarMode === 'profile' && resolvedProfileSections && resolvedProfileSections.length > 0) {\n sections = mergeSectionGroupsWithInjected(\n resolvedProfileSections,\n profileSidebarInjectedMenuItems,\n t,\n )\n title = profileSectionTitle ?? t('backend.nav.profile', 'Profile')\n }\n if (!sections) return null\n return (\n <div className=\"flex h-full flex-col gap-2\">\n <Link\n href=\"/backend\"\n className=\"inline-flex items-center gap-2 rounded-lg px-2 py-2 text-sm font-semibold text-foreground transition-colors hover:bg-muted\"\n data-testid=\"appshell-section-back-to-main\"\n aria-label={t('backend.nav.backToMain', 'Back to Main')}\n >\n <ChevronLeft className=\"size-4 shrink-0\" aria-hidden />\n <span className=\"truncate\">{title}</span>\n </Link>\n <div className=\"min-h-0 flex-1\">\n {renderSectionSidebar(sections, title, false, true, true)}\n </div>\n </div>\n )\n }\n\n const isSectionView =\n (sidebarMode === 'settings' && !!resolvedSettingsSections && resolvedSettingsSections.length > 0) ||\n (sidebarMode === 'profile' && !!resolvedProfileSections && resolvedProfileSections.length > 0)\n const gridColsClass = isSectionView\n ? (effectiveCollapsed ? 'lg:grid-cols-[80px_240px_1fr]' : 'lg:grid-cols-[240px_240px_1fr]')\n : (effectiveCollapsed ? 'lg:grid-cols-[80px_1fr]' : 'lg:grid-cols-[240px_1fr]')\n const headerCtxValue = React.useMemo(() => ({\n setBreadcrumb: setHeaderBreadcrumb,\n setTitle: setHeaderTitle,\n }), [])\n const renderedTopbarInjectedActions = React.useMemo(\n () =>\n topbarInjectedMenuItems.map((item) => {\n const label = resolveInjectedMenuLabel(item, t)\n if (item.href) {\n return (\n <Link\n key={item.id}\n href={item.href}\n className=\"inline-flex items-center rounded border px-2 py-1 text-xs hover:bg-accent hover:text-accent-foreground\"\n data-menu-item-id={item.id}\n >\n {label}\n </Link>\n )\n }\n return (\n <Button\n key={item.id}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n data-menu-item-id={item.id}\n onClick={() => item.onClick?.()}\n >\n {label}\n </Button>\n )\n }),\n [t, topbarInjectedMenuItems],\n )\n\n return (\n <HeaderContext.Provider value={headerCtxValue}>\n <div\n className={`relative min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}\n style={{ '--topbar-height': '61px' } as React.CSSProperties}\n >\n {/* Desktop sidebar collapse/expand toggle \u2014 sits on the divider line between\n sidebar and content, like Notion/Vercel. Hidden on mobile (hamburger in\n topbar handles the drawer). */}\n <button\n type=\"button\"\n onClick={() => setCollapsed((c) => !c)}\n aria-label={t('appShell.toggleSidebar')}\n className=\"hidden lg:flex fixed top-4 z-dropdown size-7 items-center justify-center rounded-md border bg-background text-muted-foreground shadow-sm transition-all hover:text-foreground hover:bg-muted focus:outline-none focus-visible:shadow-focus\"\n style={{ left: `calc(${asideWidth} - 14px)` }}\n >\n {effectiveCollapsed ? <PanelLeftOpen className=\"size-4\" /> : <PanelLeftClose className=\"size-4\" />}\n </button>\n {/* Desktop main sidebar */}\n <aside ref={sidebarAsideRef} className={`${asideClassesBase} ${effectiveCollapsed ? 'px-2' : 'px-3'} hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative transition-[width,padding] duration-200 ease-out`} style={{ width: asideWidth }}>\n {renderSidebar(effectiveCollapsed, false, isSectionView)}\n {/* Scroll affordance \u2014 gradient fade + clickable chevron that flips up when\n the user reaches the bottom and disappears when nothing is scrollable\n (#1803). Clicking the chevron scrolls the inner sidebar container to\n top/bottom (`prefers-reduced-motion: reduce` collapses to instant\n scrolling). The wrapper is `pointer-events-none` so the gradient fade\n doesn't block hover/click on the rendered nav items behind it; the\n IconButton restores `pointer-events-auto` so it stays interactive. */}\n {sidebarScrollState !== 'none' ? (\n <div\n className=\"pointer-events-none absolute inset-x-0 bottom-0 flex h-10 items-end justify-center bg-gradient-to-t from-background via-background/80 to-transparent pb-1.5\"\n >\n {/* The IconButton owns hover/focus affordance; the inner span owns the\n rotate transition so it doesn't fight with the animate-bounce\n keyframes (both target `transform`). */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"sidebar-scroll-chevron\"\n data-sidebar-scroll-chevron={sidebarScrollState}\n aria-label={\n sidebarScrollState === 'up'\n ? t('ui.sidebar.chevron.scrollTop', 'Scroll to top')\n : t('ui.sidebar.chevron.scrollBottom', 'Scroll to bottom')\n }\n className=\"pointer-events-auto text-muted-foreground/70 hover:text-foreground\"\n onClick={() => handleSidebarChevronScroll(sidebarScrollState === 'up' ? 'top' : 'bottom')}\n >\n <span\n className={`inline-flex transition-transform duration-300 ${sidebarScrollState === 'up' ? 'rotate-180' : ''}`}\n >\n <ChevronDown className=\"size-4 animate-bounce\" />\n </span>\n </IconButton>\n </div>\n ) : null}\n </aside>\n\n {/* Desktop section sidebar (Option B two-level) \u2014 sits beside the main sidebar\n when the user is on settings/profile routes. Mobile drawer keeps the\n original swap behavior to fit the narrow width. */}\n {isSectionView ? (\n <aside\n className={`${asideClassesBase} px-3 hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative`}\n style={{ width: '240px' }}\n data-testid=\"appshell-section-sidebar\"\n >\n {renderSectionAside()}\n {/* Static bottom fade \u2014 covers the native iOS scroll indicator and signals\n that the section list is scrollable. Same look as the main sidebar's\n affordance but without the chevron / scroll-state machinery. */}\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-x-0 bottom-0 h-10 bg-gradient-to-t from-background via-background/80 to-transparent\"\n />\n </aside>\n ) : null}\n\n <div className=\"flex min-h-svh flex-col min-w-0\">\n <header className=\"sticky top-0 z-sticky border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 px-3 sm:px-4 lg:px-6 py-3 flex items-center justify-between gap-2 sm:gap-3\">\n <div\n data-testid=\"backend-chrome-ready\"\n data-ready={isChromeReady ? 'true' : 'false'}\n className=\"hidden\"\n />\n <div className=\"flex items-center gap-2 min-w-0\">\n {/* Mobile menu button */}\n <IconButton variant=\"ghost\" size=\"sm\" className=\"lg:hidden\" aria-label={t('appShell.openMenu')} onClick={() => setMobileOpen(true)}>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M3 6h18M3 12h18M3 18h18\"/></svg>\n </IconButton>\n {/* Header breadcrumb: always starts with Dashboard */}\n {(() => {\n const dashboardLabel = t('dashboard.title')\n const root: Breadcrumb = [{ label: dashboardLabel, href: '/backend' }]\n let rest: Breadcrumb = []\n if (headerBreadcrumb && headerBreadcrumb.length) {\n const first = headerBreadcrumb[0]\n const dup = first && (first.href === '/backend' || first.label === dashboardLabel || first.label?.toLowerCase() === 'dashboard')\n rest = dup ? headerBreadcrumb.slice(1) : headerBreadcrumb\n } else if (headerTitle) {\n rest = [{ label: headerTitle }]\n }\n const items = [...root, ...rest]\n if (items.length === 0) return null\n const home = items[0]\n const current = items.length > 1 ? items[items.length - 1] : null\n const mid = items.slice(1, -1)\n const hasMid = mid.length > 0\n return (\n <BreadcrumbNav divider=\"arrow\" className=\"ml-2 lg:ml-3 text-sm\">\n <BreadcrumbList className=\"[&_[data-slot=breadcrumb-separator]_svg]:size-4\">\n <BreadcrumbItem>\n {home.href && current ? (\n <BreadcrumbLink asChild aria-label={home.label}>\n <Link href={home.href}>\n <Home className=\"size-4\" aria-hidden=\"true\" />\n </Link>\n </BreadcrumbLink>\n ) : (\n <BreadcrumbPage aria-label={home.label}>\n <Home className=\"size-4\" aria-hidden=\"true\" />\n </BreadcrumbPage>\n )}\n </BreadcrumbItem>\n {current ? (\n <>\n {hasMid ? (\n <>\n <BreadcrumbSeparator className=\"md:hidden\" />\n <BreadcrumbItem className=\"md:hidden\">\n <BreadcrumbEllipsis aria-label={t('appShell.breadcrumb.collapsed', { count: mid.length })} />\n </BreadcrumbItem>\n {mid.map((b, i) => (\n <React.Fragment key={`mid-${i}`}>\n <BreadcrumbSeparator className=\"hidden md:inline-flex\" />\n <BreadcrumbItem className=\"hidden md:inline-flex\">\n {b.href ? (\n <BreadcrumbLink asChild title={b.label}>\n <Link href={b.href}>{b.label}</Link>\n </BreadcrumbLink>\n ) : (\n <BreadcrumbLink title={b.label} aria-disabled=\"true\" tabIndex={-1}>\n {b.label}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </React.Fragment>\n ))}\n </>\n ) : null}\n <BreadcrumbSeparator />\n <BreadcrumbItem>\n <BreadcrumbPage title={current.label}>{current.label}</BreadcrumbPage>\n </BreadcrumbItem>\n </>\n ) : null}\n </BreadcrumbList>\n </BreadcrumbNav>\n )\n })()}\n </div>\n <div className=\"flex items-center gap-1.5 sm:gap-2 md:gap-3 text-sm shrink-0\">\n <StatusBadgeInjectionSpot\n spotId={GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n <InjectionSpot\n spotId={BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n {renderedTopbarInjectedActions}\n <AiAssistantLauncher variant=\"topbar\" />\n {rightHeaderSlot ? (\n rightHeaderSlot\n ) : (\n <span className=\"opacity-80\">{email || t('appShell.userFallback')}</span>\n )}\n </div>\n </header>\n <ProgressTopBar t={t} className=\"sticky top-0 z-sticky\" />\n <main className=\"flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl\">\n <InjectionSpot spotId={BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID} context={injectionContext} />\n <FlashMessages />\n <PartialIndexBanner />\n <UpgradeActionBanner />\n <LastOperationBanner />\n <InjectionSpot spotId={BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID} context={injectionContext} />\n <InjectionSpot\n spotId={LEGACY_GLOBAL_MUTATION_INJECTION_SPOT_ID}\n context={injectionContext}\n />\n <div id=\"om-top-banners\" className=\"mb-3 space-y-2\" />\n {children}\n <InjectionSpot spotId={BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID} context={injectionContext} />\n </main>\n <footer className=\"border-t bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/80 px-4 py-3 flex flex-wrap items-center justify-end gap-4\">\n {version ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('appShell.version', { version })}\n </span>\n ) : null}\n <nav className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n <Link href=\"/terms\" className=\"transition hover:text-foreground\">\n {t('common.terms')}\n </Link>\n <Link href=\"/privacy\" className=\"transition hover:text-foreground\">\n {t('common.privacy')}\n </Link>\n </nav>\n </footer>\n </div>\n\n {/* Mobile drawer */}\n {mobileOpen && (\n <div className=\"lg:hidden fixed inset-0 z-modal\">\n <div className=\"absolute inset-0 bg-black/50 backdrop-blur-sm\" onClick={() => setMobileOpen(false)} aria-hidden=\"true\" />\n <aside className=\"absolute left-0 top-0 flex h-full w-[280px] max-w-[85vw] flex-col bg-background border-r shadow-lg overflow-hidden\">\n <div className=\"shrink-0 flex items-center justify-between gap-2 border-b px-4 py-3\">\n <Link href=\"/backend\" className=\"flex items-center gap-2 min-w-0 text-sm font-semibold\" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? resolvedProductName} width={28} height={28} className=\"rounded shrink-0\" />\n <span className=\"truncate\">{resolvedProductName}</span>\n </Link>\n <IconButton variant=\"ghost\" size=\"sm\" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n {mobileSidebarSlot && (\n <div className=\"shrink-0 border-b px-3 py-2\">\n {mobileSidebarSlot}\n </div>\n )}\n {sidebarMode !== 'main' ? (\n <div className=\"shrink-0 flex items-center gap-5 border-b px-4 pt-3 pb-0\" role=\"tablist\">\n {([\n { id: 'main' as const, label: t('backend.nav.main', 'Main') },\n {\n id: 'section' as const,\n label:\n sidebarMode === 'settings'\n ? settingsSectionTitle ?? t('backend.nav.settings', 'Settings')\n : profileSectionTitle ?? t('backend.nav.profile', 'Profile'),\n },\n ]).map((tab) => {\n const isActive =\n tab.id === 'main' ? mobileDrawerView === 'main' : mobileDrawerView === 'auto'\n const tabId = `mobile-drawer-tab-${tab.id}`\n return (\n <button\n key={tab.id}\n id={tabId}\n type=\"button\"\n role=\"tab\"\n aria-selected={isActive}\n aria-controls=\"mobile-drawer-tabpanel\"\n onClick={() => setMobileDrawerView(tab.id === 'main' ? 'main' : 'auto')}\n className=\"relative inline-flex items-center pb-2 text-sm font-medium leading-5 tracking-tight transition-colors focus:outline-none data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground\"\n data-active={isActive}\n >\n <span>{tab.label}</span>\n {isActive ? (\n <span\n className=\"absolute -bottom-px left-0 right-0 h-0.5 bg-foreground\"\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n )\n })}\n </div>\n ) : null}\n <div\n id=\"mobile-drawer-tabpanel\"\n role={sidebarMode !== 'main' ? 'tabpanel' : undefined}\n aria-labelledby={\n sidebarMode !== 'main'\n ? `mobile-drawer-tab-${mobileDrawerView === 'main' ? 'main' : 'section'}`\n : undefined\n }\n className=\"min-h-0 flex-1 overflow-y-auto overflow-x-hidden p-3\"\n >\n {/* Force expanded sidebar in mobile drawer, hide its header and collapse toggle */}\n {renderSidebar(false, true, mobileDrawerView === 'main')}\n </div>\n </aside>\n </div>\n )}\n </div>\n <UmesDevToolsPanel />\n </HeaderContext.Provider>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["children"]
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
4
4
|
import Image from "next/image";
|
|
5
5
|
import Link from "next/link";
|
|
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
|
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
8
|
import { Button } from "../primitives/button.js";
|
|
9
9
|
import { IconButton } from "../primitives/icon-button.js";
|
|
10
|
+
import { Skeleton } from "../primitives/skeleton.js";
|
|
10
11
|
import { usePortalInjectedMenuItems } from "./hooks/usePortalInjectedMenuItems.js";
|
|
11
12
|
import { usePortalEventBridge } from "./hooks/usePortalEventBridge.js";
|
|
12
13
|
import { mergeMenuItems } from "../backend/injection/mergeMenuItems.js";
|
|
@@ -65,6 +66,14 @@ function SidebarNavItem({
|
|
|
65
66
|
}
|
|
66
67
|
return null;
|
|
67
68
|
}
|
|
69
|
+
function SidebarNavSkeleton() {
|
|
70
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-3 py-1", "data-testid": "portal-nav-loading", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full rounded-lg" }),
|
|
72
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-11/12 rounded-lg" }),
|
|
73
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-5/6 rounded-lg" }),
|
|
74
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-10/12 rounded-lg" })
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
68
77
|
function UserAvatar({ name, className }) {
|
|
69
78
|
const initials = name ? name.split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase() : "?";
|
|
70
79
|
return /* @__PURE__ */ jsx("div", { className: `flex items-center justify-center rounded-full bg-foreground text-overline font-semibold text-background ${className ?? "size-8"}`, children: initials });
|
|
@@ -106,12 +115,15 @@ function PortalShell({
|
|
|
106
115
|
const headerTitle = orgName || t("portal.title", "Customer Portal");
|
|
107
116
|
const closeMobile = useCallback(() => setMobileOpen(false), []);
|
|
108
117
|
const [autoNavGroups, setAutoNavGroups] = useState([]);
|
|
118
|
+
const [isNavLoading, setIsNavLoading] = useState(authenticated);
|
|
109
119
|
useEffect(() => {
|
|
110
120
|
if (!authenticated) {
|
|
111
121
|
setAutoNavGroups([]);
|
|
122
|
+
setIsNavLoading(false);
|
|
112
123
|
return;
|
|
113
124
|
}
|
|
114
125
|
let cancelled = false;
|
|
126
|
+
setIsNavLoading(true);
|
|
115
127
|
const load = async () => {
|
|
116
128
|
try {
|
|
117
129
|
const { ok, result } = await apiCall(
|
|
@@ -121,6 +133,8 @@ function PortalShell({
|
|
|
121
133
|
setAutoNavGroups(Array.isArray(result.groups) ? result.groups : []);
|
|
122
134
|
} catch {
|
|
123
135
|
if (!cancelled) setAutoNavGroups([]);
|
|
136
|
+
} finally {
|
|
137
|
+
if (!cancelled) setIsNavLoading(false);
|
|
124
138
|
}
|
|
125
139
|
};
|
|
126
140
|
void load();
|
|
@@ -150,6 +164,9 @@ function PortalShell({
|
|
|
150
164
|
}));
|
|
151
165
|
return mergeMenuItems(builtIn, injectedAccountItems);
|
|
152
166
|
}, [authenticated, autoNavGroups, injectedAccountItems]);
|
|
167
|
+
const shouldRenderMainNav = isNavLoading || mergedNavItems.length > 0;
|
|
168
|
+
const shouldRenderAccountNav = mergedAccountItems.length > 0;
|
|
169
|
+
const shouldRenderNav = shouldRenderMainNav || shouldRenderAccountNav;
|
|
153
170
|
if (!authenticated) {
|
|
154
171
|
return /* @__PURE__ */ jsxs("div", { className: "flex min-h-svh flex-col bg-background", "data-portal-handle": PORTAL_SHELL_HANDLE, children: [
|
|
155
172
|
/* @__PURE__ */ jsx("header", { className: "sticky top-0 z-sticky border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80", "data-portal-handle": PORTAL_HEADER_HANDLE, children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex h-16 w-full max-w-screen-lg items-center justify-between px-6", children: [
|
|
@@ -177,19 +194,30 @@ function PortalShell({
|
|
|
177
194
|
/* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? "", width: 22, height: 22, className: "" }),
|
|
178
195
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold tracking-tight truncate", children: headerTitle })
|
|
179
196
|
] }) }),
|
|
180
|
-
/* @__PURE__ */
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
/* @__PURE__ */ jsx(
|
|
198
|
+
"div",
|
|
199
|
+
{
|
|
200
|
+
className: "hidden",
|
|
201
|
+
"data-testid": "portal-nav-ready",
|
|
202
|
+
"data-ready": isNavLoading ? "false" : "true",
|
|
203
|
+
"aria-hidden": "true"
|
|
204
|
+
}
|
|
205
|
+
),
|
|
206
|
+
shouldRenderNav ? /* @__PURE__ */ jsxs("nav", { "aria-label": "Portal navigation", className: "flex-1 overflow-y-auto px-3 py-5", children: [
|
|
207
|
+
shouldRenderMainNav ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
208
|
+
/* @__PURE__ */ jsx("p", { className: "mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50", children: t("portal.nav.home", "Portal") }),
|
|
209
|
+
isNavLoading ? /* @__PURE__ */ jsx(SidebarNavSkeleton, {}) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: mergedNavItems.map((item) => /* @__PURE__ */ jsx(
|
|
210
|
+
SidebarNavItem,
|
|
211
|
+
{
|
|
212
|
+
item,
|
|
213
|
+
active: !!item.href && pathname.startsWith(item.href),
|
|
214
|
+
t,
|
|
215
|
+
onClick: closeMobile
|
|
216
|
+
},
|
|
217
|
+
item.id
|
|
218
|
+
)) })
|
|
219
|
+
] }) : null,
|
|
220
|
+
shouldRenderAccountNav ? /* @__PURE__ */ jsxs("div", { className: shouldRenderMainNav ? "mt-8" : "", children: [
|
|
193
221
|
/* @__PURE__ */ jsx("p", { className: "mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50", children: t("portal.nav.account", "Account") }),
|
|
194
222
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: mergedAccountItems.map((item) => /* @__PURE__ */ jsx(
|
|
195
223
|
SidebarNavItem,
|
|
@@ -202,7 +230,7 @@ function PortalShell({
|
|
|
202
230
|
item.id
|
|
203
231
|
)) })
|
|
204
232
|
] }) : null
|
|
205
|
-
] }),
|
|
233
|
+
] }) : null,
|
|
206
234
|
/* @__PURE__ */ jsxs("div", { className: "border-t px-3 py-3", children: [
|
|
207
235
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 rounded-lg px-3 py-2", children: [
|
|
208
236
|
/* @__PURE__ */ jsx(UserAvatar, { name: userName, className: "size-8" }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/portal/PortalShell.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { type ReactNode, useEffect, useState, useCallback, useMemo, useContext } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { usePortalInjectedMenuItems } from './hooks/usePortalInjectedMenuItems'\nimport { usePortalEventBridge } from './hooks/usePortalEventBridge'\nimport { mergeMenuItems } from '../backend/injection/mergeMenuItems'\nimport type { MergedMenuItem } from '../backend/injection/mergeMenuItems'\nimport { PortalNotificationBell } from './components/PortalNotificationBell'\nimport { usePortalContext } from './PortalContext'\nimport { apiCall } from '../backend/utils/apiCall'\nimport type { PortalNavGroup } from './utils/nav'\n\n// Component replacement handle IDs (FROZEN once shipped)\nexport const PORTAL_SHELL_HANDLE = 'page:portal:layout'\nexport const PORTAL_HEADER_HANDLE = 'section:portal:header'\nexport const PORTAL_FOOTER_HANDLE = 'section:portal:footer'\nexport const PORTAL_SIDEBAR_HANDLE = 'section:portal:sidebar'\nexport const PORTAL_USER_MENU_HANDLE = 'section:portal:user-menu'\n\nexport type ShellLogo = {\n src: string\n alt?: string\n}\n\nexport type PortalShellProps = {\n children: ReactNode\n /** Override orgSlug (used on public pages without context) */\n orgSlug?: string\n /** Override organization name (used on public pages without context) */\n organizationName?: string\n /** Override the brand logo rendered in the header, footer, and sidebar. */\n logo?: ShellLogo\n /** Whether to show authenticated layout. Auto-detected from context when omitted. */\n authenticated?: boolean\n /** Logout handler. Auto-provided from context when omitted. */\n onLogout?: () => void\n enableEventBridge?: boolean\n /** Override user name. Auto-read from context when omitted. */\n userName?: string\n /** Override user email. Auto-read from context when omitted. */\n userEmail?: string\n}\n\nfunction PortalEventBridgeMount() {\n usePortalEventBridge()\n return null\n}\n\n/* ---- Inline SVG icons ---- */\n\nfunction MenuIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" /><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\" /><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\" />\n </svg>\n )\n}\n\nfunction XIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n )\n}\n\nfunction LogOutIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" /><polyline points=\"16 17 21 12 16 7\" /><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\n/* ---- Sidebar nav item ---- */\n\nfunction SidebarNavItem({\n item,\n active,\n t,\n onClick,\n}: {\n item: MergedMenuItem\n active: boolean\n t: (key: string, fallback?: string) => string\n onClick?: () => void\n}) {\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n if (!label) return null\n\n const cls = [\n 'flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors',\n active\n ? 'bg-foreground text-background'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n ].join(' ')\n\n if (item.href) {\n return (\n <Link href={item.href} className={cls} data-menu-item-id={item.id} onClick={onClick}>\n {label}\n </Link>\n )\n }\n if (item.onClick) {\n return (\n <button type=\"button\" className={cls} data-menu-item-id={item.id} onClick={() => { item.onClick?.(); onClick?.() }}>\n {label}\n </button>\n )\n }\n return null\n}\n\n/* ---- User initials avatar ---- */\n\nfunction UserAvatar({ name, className }: { name?: string; className?: string }) {\n const initials = name\n ? name.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase()\n : '?'\n return (\n <div className={`flex items-center justify-center rounded-full bg-foreground text-overline font-semibold text-background ${className ?? 'size-8'}`}>\n {initials}\n </div>\n )\n}\n\n/* ---- Try reading from PortalContext ---- */\n\nfunction useOptionalPortalContext() {\n try {\n return usePortalContext()\n } catch {\n return null\n }\n}\n\n/* ================================================================== */\n/* PortalShell */\n/* ================================================================== */\n\n/**\n * Portal layout shell.\n *\n * When a `PortalProvider` is mounted in a parent layout, PortalShell reads\n * auth/tenant state from context \u2014 no re-fetching on navigation. Props are\n * used as overrides or for public pages that don't have a context.\n */\nexport function PortalShell({\n children,\n orgSlug: orgSlugProp,\n organizationName: orgNameProp,\n logo,\n authenticated: authenticatedProp,\n onLogout: onLogoutProp,\n enableEventBridge = false,\n userName: userNameProp,\n userEmail: userEmailProp,\n}: PortalShellProps) {\n const t = useT()\n const pathname = usePathname()\n const [mobileOpen, setMobileOpen] = useState(false)\n\n // Read from context when available (persists across navigations)\n const portalCtx = useOptionalPortalContext()\n\n // Resolve values: context takes priority, props are fallback/override\n const orgSlug = portalCtx?.orgSlug ?? orgSlugProp\n const orgName = portalCtx?.tenant.organizationName ?? orgNameProp\n const user = portalCtx?.auth.user ?? null\n const authenticated = authenticatedProp ?? !!user\n const onLogout = onLogoutProp ?? portalCtx?.auth.logout\n const userName = userNameProp ?? user?.displayName\n const userEmail = userEmailProp ?? user?.email\n\n const { items: injectedMainItems } = usePortalInjectedMenuItems('menu:portal:sidebar:main')\n const { items: injectedAccountItems } = usePortalInjectedMenuItems('menu:portal:sidebar:account')\n\n const portalHome = orgSlug ? `/${orgSlug}/portal` : '/portal'\n const loginHref = orgSlug ? `/${orgSlug}/portal/login` : '/portal/login'\n const signupHref = orgSlug ? `/${orgSlug}/portal/signup` : '/portal/signup'\n // Always use the resolved organization name from the database.\n // Fall back to the generic portal title \u2014 never display the raw slug.\n const headerTitle = orgName || t('portal.title', 'Customer Portal')\n\n const closeMobile = useCallback(() => setMobileOpen(false), [])\n\n const [autoNavGroups, setAutoNavGroups] = useState<PortalNavGroup[]>([])\n useEffect(() => {\n if (!authenticated) {\n setAutoNavGroups([])\n return\n }\n let cancelled = false\n const load = async () => {\n try {\n const { ok, result } = await apiCall<{ ok: boolean; groups?: PortalNavGroup[] }>(\n '/api/customer_accounts/portal/nav',\n )\n if (cancelled || !ok || !result?.ok) return\n setAutoNavGroups(Array.isArray(result.groups) ? result.groups : [])\n } catch {\n if (!cancelled) setAutoNavGroups([])\n }\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [authenticated])\n\n const mergedNavItems = useMemo(() => {\n if (!authenticated) return []\n const discovered = autoNavGroups.find((g) => g.id === 'main')?.items ?? []\n const builtIn = discovered.map((item) => ({\n id: item.id,\n labelKey: item.labelKey,\n label: item.label,\n href: item.href,\n }))\n return mergeMenuItems(builtIn, injectedMainItems)\n }, [authenticated, autoNavGroups, injectedMainItems])\n\n const mergedAccountItems = useMemo(() => {\n if (!authenticated) return []\n const discovered = autoNavGroups.find((g) => g.id === 'account')?.items ?? []\n const builtIn = discovered.map((item) => ({\n id: item.id,\n labelKey: item.labelKey,\n label: item.label,\n href: item.href,\n }))\n return mergeMenuItems(builtIn, injectedAccountItems)\n }, [authenticated, autoNavGroups, injectedAccountItems])\n\n /* ---- PUBLIC LAYOUT ---- */\n if (!authenticated) {\n return (\n <div className=\"flex min-h-svh flex-col bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n <header className=\"sticky top-0 z-sticky border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"mx-auto flex h-16 w-full max-w-screen-lg items-center justify-between px-6\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? \"\"} width={28} height={28} className=\"\" priority />\n <span className=\"text-base font-semibold tracking-tight\">{headerTitle}</span>\n </Link>\n <nav aria-label=\"Primary\" className=\"flex items-center gap-1\">\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"text-sm\">\n <Link href={loginHref}>{t('portal.nav.login', 'Log In')}</Link>\n </Button>\n <Button asChild size=\"sm\" className=\"rounded-lg text-sm\">\n <Link href={signupHref}>{t('portal.nav.signup', 'Sign Up')}</Link>\n </Button>\n </nav>\n </div>\n </header>\n\n <main className=\"flex-1\">\n <div className=\"mx-auto flex w-full max-w-screen-lg flex-col gap-8 px-6 py-12 sm:py-20\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <div className=\"mx-auto flex w-full max-w-screen-lg items-center justify-between px-6 py-6\">\n <Link href={portalHome} className=\"flex items-center gap-2 text-muted-foreground transition hover:text-foreground\">\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? \"\"} width={20} height={20} className=\"\" />\n <span className=\"text-sm font-medium text-foreground\">{headerTitle}</span>\n </Link>\n <p className=\"text-xs text-muted-foreground/60\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </div>\n </footer>\n </div>\n )\n }\n\n /* ---- AUTHENTICATED LAYOUT ---- */\n\n const sidebarContent = (\n <div className=\"flex h-full flex-col\" data-portal-handle={PORTAL_SIDEBAR_HANDLE}>\n <div className=\"flex h-16 items-center gap-2.5 border-b px-5\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? \"\"} width={22} height={22} className=\"\" />\n <span className=\"text-sm font-semibold tracking-tight truncate\">{headerTitle}</span>\n </Link>\n </div>\n\n <nav aria-label=\"Portal navigation\" className=\"flex-1 overflow-y-auto px-3 py-5\">\n <p className=\"mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50\">\n {t('portal.nav.home', 'Portal')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedNavItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n\n {mergedAccountItems.length > 0 ? (\n <div className=\"mt-8\">\n <p className=\"mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50\">\n {t('portal.nav.account', 'Account')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedAccountItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n </div>\n ) : null}\n </nav>\n\n <div className=\"border-t px-3 py-3\">\n <div className=\"flex items-center gap-2.5 rounded-lg px-3 py-2\">\n <UserAvatar name={userName} className=\"size-8\" />\n <div className=\"min-w-0 flex-1\">\n {userName ? (\n <p className=\"truncate text-sm font-medium leading-tight\">{userName}</p>\n ) : (\n <div className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n )}\n {userEmail ? (\n <p className=\"truncate text-overline text-muted-foreground\">{userEmail}</p>\n ) : (\n <div className=\"mt-1 h-3 w-32 animate-pulse rounded bg-muted\" />\n )}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onLogout}\n className=\"mt-0.5 flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n data-portal-handle={PORTAL_USER_MENU_HANDLE}\n data-menu-item-id=\"portal-logout\"\n >\n <LogOutIcon className=\"size-4\" />\n {t('portal.nav.logout', 'Log Out')}\n </button>\n </div>\n </div>\n )\n\n return (\n <div className=\"flex min-h-svh bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n {enableEventBridge ? <PortalEventBridgeMount /> : null}\n\n <aside className=\"hidden w-[240px] shrink-0 border-r lg:block\">\n {sidebarContent}\n </aside>\n\n {mobileOpen ? (\n <div className=\"fixed inset-0 z-modal lg:hidden\">\n <div className=\"absolute inset-0 bg-black/20 backdrop-blur-sm\" onClick={closeMobile} />\n <aside className=\"relative z-10 h-full w-[280px] bg-background shadow-2xl\">\n <div className=\"absolute right-3 top-4 z-20\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={closeMobile} aria-label=\"Close menu\">\n <XIcon className=\"size-4\" />\n </IconButton>\n </div>\n {sidebarContent}\n </aside>\n </div>\n ) : null}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <header className=\"flex h-16 items-center justify-between border-b px-4 lg:px-8\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"flex items-center gap-3\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => setMobileOpen(true)} className=\"lg:hidden\" aria-label=\"Open menu\">\n <MenuIcon className=\"size-5\" />\n </IconButton>\n </div>\n <div className=\"flex items-center gap-3\">\n <PortalNotificationBell t={t} />\n </div>\n </header>\n\n <main className=\"flex-1 overflow-y-auto\">\n <div className=\"w-full px-4 py-6 lg:px-8 lg:py-8\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t px-4 py-4 lg:px-8\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <p className=\"text-overline text-muted-foreground/50\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </footer>\n </div>\n </div>\n )\n}\n\nexport default PortalShell\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { type ReactNode, useEffect, useState, useCallback, useMemo } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Skeleton } from '../primitives/skeleton'\nimport { usePortalInjectedMenuItems } from './hooks/usePortalInjectedMenuItems'\nimport { usePortalEventBridge } from './hooks/usePortalEventBridge'\nimport { mergeMenuItems } from '../backend/injection/mergeMenuItems'\nimport type { MergedMenuItem } from '../backend/injection/mergeMenuItems'\nimport { PortalNotificationBell } from './components/PortalNotificationBell'\nimport { usePortalContext } from './PortalContext'\nimport { apiCall } from '../backend/utils/apiCall'\nimport type { PortalNavGroup } from './utils/nav'\n\n// Component replacement handle IDs (FROZEN once shipped)\nexport const PORTAL_SHELL_HANDLE = 'page:portal:layout'\nexport const PORTAL_HEADER_HANDLE = 'section:portal:header'\nexport const PORTAL_FOOTER_HANDLE = 'section:portal:footer'\nexport const PORTAL_SIDEBAR_HANDLE = 'section:portal:sidebar'\nexport const PORTAL_USER_MENU_HANDLE = 'section:portal:user-menu'\n\nexport type ShellLogo = {\n src: string\n alt?: string\n}\n\nexport type PortalShellProps = {\n children: ReactNode\n /** Override orgSlug (used on public pages without context) */\n orgSlug?: string\n /** Override organization name (used on public pages without context) */\n organizationName?: string\n /** Override the brand logo rendered in the header, footer, and sidebar. */\n logo?: ShellLogo\n /** Whether to show authenticated layout. Auto-detected from context when omitted. */\n authenticated?: boolean\n /** Logout handler. Auto-provided from context when omitted. */\n onLogout?: () => void\n enableEventBridge?: boolean\n /** Override user name. Auto-read from context when omitted. */\n userName?: string\n /** Override user email. Auto-read from context when omitted. */\n userEmail?: string\n}\n\nfunction PortalEventBridgeMount() {\n usePortalEventBridge()\n return null\n}\n\n/* ---- Inline SVG icons ---- */\n\nfunction MenuIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" /><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\" /><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\" />\n </svg>\n )\n}\n\nfunction XIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n )\n}\n\nfunction LogOutIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" /><polyline points=\"16 17 21 12 16 7\" /><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\n/* ---- Sidebar nav item ---- */\n\nfunction SidebarNavItem({\n item,\n active,\n t,\n onClick,\n}: {\n item: MergedMenuItem\n active: boolean\n t: (key: string, fallback?: string) => string\n onClick?: () => void\n}) {\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n if (!label) return null\n\n const cls = [\n 'flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors',\n active\n ? 'bg-foreground text-background'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n ].join(' ')\n\n if (item.href) {\n return (\n <Link href={item.href} className={cls} data-menu-item-id={item.id} onClick={onClick}>\n {label}\n </Link>\n )\n }\n if (item.onClick) {\n return (\n <button type=\"button\" className={cls} data-menu-item-id={item.id} onClick={() => { item.onClick?.(); onClick?.() }}>\n {label}\n </button>\n )\n }\n return null\n}\n\nfunction SidebarNavSkeleton() {\n return (\n <div className=\"flex flex-col gap-2 px-3 py-1\" data-testid=\"portal-nav-loading\">\n <Skeleton className=\"h-9 w-full rounded-lg\" />\n <Skeleton className=\"h-9 w-11/12 rounded-lg\" />\n <Skeleton className=\"h-9 w-5/6 rounded-lg\" />\n <Skeleton className=\"h-9 w-10/12 rounded-lg\" />\n </div>\n )\n}\n\n/* ---- User initials avatar ---- */\n\nfunction UserAvatar({ name, className }: { name?: string; className?: string }) {\n const initials = name\n ? name.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase()\n : '?'\n return (\n <div className={`flex items-center justify-center rounded-full bg-foreground text-overline font-semibold text-background ${className ?? 'size-8'}`}>\n {initials}\n </div>\n )\n}\n\n/* ---- Try reading from PortalContext ---- */\n\nfunction useOptionalPortalContext() {\n try {\n return usePortalContext()\n } catch {\n return null\n }\n}\n\n/* ================================================================== */\n/* PortalShell */\n/* ================================================================== */\n\n/**\n * Portal layout shell.\n *\n * When a `PortalProvider` is mounted in a parent layout, PortalShell reads\n * auth/tenant state from context \u2014 no re-fetching on navigation. Props are\n * used as overrides or for public pages that don't have a context.\n */\nexport function PortalShell({\n children,\n orgSlug: orgSlugProp,\n organizationName: orgNameProp,\n logo,\n authenticated: authenticatedProp,\n onLogout: onLogoutProp,\n enableEventBridge = false,\n userName: userNameProp,\n userEmail: userEmailProp,\n}: PortalShellProps) {\n const t = useT()\n const pathname = usePathname()\n const [mobileOpen, setMobileOpen] = useState(false)\n\n // Read from context when available (persists across navigations)\n const portalCtx = useOptionalPortalContext()\n\n // Resolve values: context takes priority, props are fallback/override\n const orgSlug = portalCtx?.orgSlug ?? orgSlugProp\n const orgName = portalCtx?.tenant.organizationName ?? orgNameProp\n const user = portalCtx?.auth.user ?? null\n const authenticated = authenticatedProp ?? !!user\n const onLogout = onLogoutProp ?? portalCtx?.auth.logout\n const userName = userNameProp ?? user?.displayName\n const userEmail = userEmailProp ?? user?.email\n\n const { items: injectedMainItems } = usePortalInjectedMenuItems('menu:portal:sidebar:main')\n const { items: injectedAccountItems } = usePortalInjectedMenuItems('menu:portal:sidebar:account')\n\n const portalHome = orgSlug ? `/${orgSlug}/portal` : '/portal'\n const loginHref = orgSlug ? `/${orgSlug}/portal/login` : '/portal/login'\n const signupHref = orgSlug ? `/${orgSlug}/portal/signup` : '/portal/signup'\n // Always use the resolved organization name from the database.\n // Fall back to the generic portal title \u2014 never display the raw slug.\n const headerTitle = orgName || t('portal.title', 'Customer Portal')\n\n const closeMobile = useCallback(() => setMobileOpen(false), [])\n\n const [autoNavGroups, setAutoNavGroups] = useState<PortalNavGroup[]>([])\n const [isNavLoading, setIsNavLoading] = useState(authenticated)\n useEffect(() => {\n if (!authenticated) {\n setAutoNavGroups([])\n setIsNavLoading(false)\n return\n }\n let cancelled = false\n setIsNavLoading(true)\n const load = async () => {\n try {\n const { ok, result } = await apiCall<{ ok: boolean; groups?: PortalNavGroup[] }>(\n '/api/customer_accounts/portal/nav',\n )\n if (cancelled || !ok || !result?.ok) return\n setAutoNavGroups(Array.isArray(result.groups) ? result.groups : [])\n } catch {\n if (!cancelled) setAutoNavGroups([])\n } finally {\n if (!cancelled) setIsNavLoading(false)\n }\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [authenticated])\n\n const mergedNavItems = useMemo(() => {\n if (!authenticated) return []\n const discovered = autoNavGroups.find((g) => g.id === 'main')?.items ?? []\n const builtIn = discovered.map((item) => ({\n id: item.id,\n labelKey: item.labelKey,\n label: item.label,\n href: item.href,\n }))\n return mergeMenuItems(builtIn, injectedMainItems)\n }, [authenticated, autoNavGroups, injectedMainItems])\n\n const mergedAccountItems = useMemo(() => {\n if (!authenticated) return []\n const discovered = autoNavGroups.find((g) => g.id === 'account')?.items ?? []\n const builtIn = discovered.map((item) => ({\n id: item.id,\n labelKey: item.labelKey,\n label: item.label,\n href: item.href,\n }))\n return mergeMenuItems(builtIn, injectedAccountItems)\n }, [authenticated, autoNavGroups, injectedAccountItems])\n\n const shouldRenderMainNav = isNavLoading || mergedNavItems.length > 0\n const shouldRenderAccountNav = mergedAccountItems.length > 0\n const shouldRenderNav = shouldRenderMainNav || shouldRenderAccountNav\n\n /* ---- PUBLIC LAYOUT ---- */\n if (!authenticated) {\n return (\n <div className=\"flex min-h-svh flex-col bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n <header className=\"sticky top-0 z-sticky border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"mx-auto flex h-16 w-full max-w-screen-lg items-center justify-between px-6\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? \"\"} width={28} height={28} className=\"\" priority />\n <span className=\"text-base font-semibold tracking-tight\">{headerTitle}</span>\n </Link>\n <nav aria-label=\"Primary\" className=\"flex items-center gap-1\">\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"text-sm\">\n <Link href={loginHref}>{t('portal.nav.login', 'Log In')}</Link>\n </Button>\n <Button asChild size=\"sm\" className=\"rounded-lg text-sm\">\n <Link href={signupHref}>{t('portal.nav.signup', 'Sign Up')}</Link>\n </Button>\n </nav>\n </div>\n </header>\n\n <main className=\"flex-1\">\n <div className=\"mx-auto flex w-full max-w-screen-lg flex-col gap-8 px-6 py-12 sm:py-20\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <div className=\"mx-auto flex w-full max-w-screen-lg items-center justify-between px-6 py-6\">\n <Link href={portalHome} className=\"flex items-center gap-2 text-muted-foreground transition hover:text-foreground\">\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? \"\"} width={20} height={20} className=\"\" />\n <span className=\"text-sm font-medium text-foreground\">{headerTitle}</span>\n </Link>\n <p className=\"text-xs text-muted-foreground/60\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </div>\n </footer>\n </div>\n )\n }\n\n /* ---- AUTHENTICATED LAYOUT ---- */\n\n const sidebarContent = (\n <div className=\"flex h-full flex-col\" data-portal-handle={PORTAL_SIDEBAR_HANDLE}>\n <div className=\"flex h-16 items-center gap-2.5 border-b px-5\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src={logo?.src ?? \"/open-mercato.svg\"} alt={logo?.alt ?? \"\"} width={22} height={22} className=\"\" />\n <span className=\"text-sm font-semibold tracking-tight truncate\">{headerTitle}</span>\n </Link>\n </div>\n\n <div\n className=\"hidden\"\n data-testid=\"portal-nav-ready\"\n data-ready={isNavLoading ? 'false' : 'true'}\n aria-hidden=\"true\"\n />\n\n {shouldRenderNav ? (\n <nav aria-label=\"Portal navigation\" className=\"flex-1 overflow-y-auto px-3 py-5\">\n {shouldRenderMainNav ? (\n <>\n <p className=\"mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50\">\n {t('portal.nav.home', 'Portal')}\n </p>\n {isNavLoading ? (\n <SidebarNavSkeleton />\n ) : (\n <div className=\"flex flex-col gap-0.5\">\n {mergedNavItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n )}\n </>\n ) : null}\n\n {shouldRenderAccountNav ? (\n <div className={shouldRenderMainNav ? 'mt-8' : ''}>\n <p className=\"mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50\">\n {t('portal.nav.account', 'Account')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedAccountItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n </div>\n ) : null}\n </nav>\n ) : null}\n\n <div className=\"border-t px-3 py-3\">\n <div className=\"flex items-center gap-2.5 rounded-lg px-3 py-2\">\n <UserAvatar name={userName} className=\"size-8\" />\n <div className=\"min-w-0 flex-1\">\n {userName ? (\n <p className=\"truncate text-sm font-medium leading-tight\">{userName}</p>\n ) : (\n <div className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n )}\n {userEmail ? (\n <p className=\"truncate text-overline text-muted-foreground\">{userEmail}</p>\n ) : (\n <div className=\"mt-1 h-3 w-32 animate-pulse rounded bg-muted\" />\n )}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onLogout}\n className=\"mt-0.5 flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n data-portal-handle={PORTAL_USER_MENU_HANDLE}\n data-menu-item-id=\"portal-logout\"\n >\n <LogOutIcon className=\"size-4\" />\n {t('portal.nav.logout', 'Log Out')}\n </button>\n </div>\n </div>\n )\n\n return (\n <div className=\"flex min-h-svh bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n {enableEventBridge ? <PortalEventBridgeMount /> : null}\n\n <aside className=\"hidden w-[240px] shrink-0 border-r lg:block\">\n {sidebarContent}\n </aside>\n\n {mobileOpen ? (\n <div className=\"fixed inset-0 z-modal lg:hidden\">\n <div className=\"absolute inset-0 bg-black/20 backdrop-blur-sm\" onClick={closeMobile} />\n <aside className=\"relative z-10 h-full w-[280px] bg-background shadow-2xl\">\n <div className=\"absolute right-3 top-4 z-20\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={closeMobile} aria-label=\"Close menu\">\n <XIcon className=\"size-4\" />\n </IconButton>\n </div>\n {sidebarContent}\n </aside>\n </div>\n ) : null}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <header className=\"flex h-16 items-center justify-between border-b px-4 lg:px-8\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"flex items-center gap-3\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => setMobileOpen(true)} className=\"lg:hidden\" aria-label=\"Open menu\">\n <MenuIcon className=\"size-5\" />\n </IconButton>\n </div>\n <div className=\"flex items-center gap-3\">\n <PortalNotificationBell t={t} />\n </div>\n </header>\n\n <main className=\"flex-1 overflow-y-auto\">\n <div className=\"w-full px-4 py-6 lg:px-8 lg:py-8\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t px-4 py-4 lg:px-8\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <p className=\"text-overline text-muted-foreground/50\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </footer>\n </div>\n </div>\n )\n}\n\nexport default PortalShell\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3766.1.33102bfc91",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -141,13 +141,13 @@
|
|
|
141
141
|
"remark-gfm": "^4.0.1"
|
|
142
142
|
},
|
|
143
143
|
"peerDependencies": {
|
|
144
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
144
|
+
"@open-mercato/shared": "0.6.3-develop.3766.1.33102bfc91",
|
|
145
145
|
"react": ">=18.0.0",
|
|
146
146
|
"react-dom": ">=18.0.0",
|
|
147
147
|
"react-is": ">=18.0.0"
|
|
148
148
|
},
|
|
149
149
|
"devDependencies": {
|
|
150
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
150
|
+
"@open-mercato/shared": "0.6.3-develop.3766.1.33102bfc91",
|
|
151
151
|
"@testing-library/dom": "^10.4.1",
|
|
152
152
|
"@testing-library/jest-dom": "^6.9.1",
|
|
153
153
|
"@testing-library/react": "^16.3.1",
|
package/src/backend/AppShell.tsx
CHANGED
|
@@ -808,7 +808,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
|
|
|
808
808
|
}
|
|
809
809
|
|
|
810
810
|
function renderSidebar(compact: boolean, hideHeader?: boolean, forceMainOnly?: boolean) {
|
|
811
|
-
if (!isChromeReady && isChromeLoading
|
|
811
|
+
if (!isChromeReady && isChromeLoading) {
|
|
812
812
|
return (
|
|
813
813
|
<div className="flex flex-col min-h-full gap-3" data-testid="backend-chrome-loading">
|
|
814
814
|
{!hideHeader ? (
|
|
@@ -610,4 +610,90 @@ describe('AppShell', () => {
|
|
|
610
610
|
;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = previousOriginalFetch
|
|
611
611
|
}
|
|
612
612
|
})
|
|
613
|
+
|
|
614
|
+
// Regression: #1828 — skeleton must hide stale SSR groups until chrome resolves
|
|
615
|
+
it('shows skeleton (not stale SSR groups) while chrome API is loading', async () => {
|
|
616
|
+
const previousFetch = global.fetch
|
|
617
|
+
const previousWindowFetch = window.fetch
|
|
618
|
+
const previousOriginalFetch = (window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch
|
|
619
|
+
let resolveFetch: ((response: Response) => void) | null = null
|
|
620
|
+
const fetchPromise = new Promise<Response>((resolve) => {
|
|
621
|
+
resolveFetch = resolve
|
|
622
|
+
})
|
|
623
|
+
const fetchMock = jest.fn(async (input: RequestInfo | URL) => {
|
|
624
|
+
const url = typeof input === 'string'
|
|
625
|
+
? input
|
|
626
|
+
: input instanceof Request
|
|
627
|
+
? input.url
|
|
628
|
+
: input.toString()
|
|
629
|
+
if (url.includes('/api/auth/admin/nav-flicker-regression')) {
|
|
630
|
+
return fetchPromise
|
|
631
|
+
}
|
|
632
|
+
return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } })
|
|
633
|
+
}) as unknown as typeof fetch
|
|
634
|
+
global.fetch = fetchMock
|
|
635
|
+
window.fetch = fetchMock
|
|
636
|
+
;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = fetchMock
|
|
637
|
+
|
|
638
|
+
const staleGroups = [
|
|
639
|
+
{
|
|
640
|
+
id: 'core',
|
|
641
|
+
name: 'Stale Core',
|
|
642
|
+
items: [{ href: '/backend/stale-link', title: 'Stale Link' }],
|
|
643
|
+
},
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
renderWithProviders(
|
|
648
|
+
<AppShell
|
|
649
|
+
email="demo@example.com"
|
|
650
|
+
groups={staleGroups}
|
|
651
|
+
adminNavApi="/api/auth/admin/nav-flicker-regression"
|
|
652
|
+
>
|
|
653
|
+
<div>Hydrated content</div>
|
|
654
|
+
</AppShell>,
|
|
655
|
+
{ dict },
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
await waitFor(() => {
|
|
659
|
+
expect(screen.getAllByTestId('backend-chrome-loading').length).toBeGreaterThan(0)
|
|
660
|
+
})
|
|
661
|
+
expect(screen.queryByText('Stale Link')).toBeNull()
|
|
662
|
+
expect(screen.getByTestId('backend-chrome-ready')).toHaveAttribute('data-ready', 'false')
|
|
663
|
+
|
|
664
|
+
resolveFetch?.(new Response(JSON.stringify({
|
|
665
|
+
groups: [
|
|
666
|
+
{
|
|
667
|
+
id: 'core',
|
|
668
|
+
name: 'Core',
|
|
669
|
+
defaultName: 'Core',
|
|
670
|
+
items: [
|
|
671
|
+
{
|
|
672
|
+
href: '/backend/users',
|
|
673
|
+
title: 'Fresh Link',
|
|
674
|
+
defaultTitle: 'Fresh Link',
|
|
675
|
+
enabled: true,
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
settingsSections: [],
|
|
681
|
+
settingsPathPrefixes: [],
|
|
682
|
+
profileSections: [],
|
|
683
|
+
profilePathPrefixes: ['/backend/profile/'],
|
|
684
|
+
grantedFeatures: ['auth.*'],
|
|
685
|
+
roles: ['admin'],
|
|
686
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } }))
|
|
687
|
+
|
|
688
|
+
await waitFor(() => {
|
|
689
|
+
expect(screen.getByText('Fresh Link')).toBeInTheDocument()
|
|
690
|
+
})
|
|
691
|
+
expect(screen.queryByTestId('backend-chrome-loading')).toBeNull()
|
|
692
|
+
expect(screen.queryByText('Stale Link')).toBeNull()
|
|
693
|
+
} finally {
|
|
694
|
+
global.fetch = previousFetch
|
|
695
|
+
window.fetch = previousWindowFetch
|
|
696
|
+
;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = previousOriginalFetch
|
|
697
|
+
}
|
|
698
|
+
})
|
|
613
699
|
})
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
|
-
import { type ReactNode, useEffect, useState, useCallback, useMemo
|
|
2
|
+
import { type ReactNode, useEffect, useState, useCallback, useMemo } from 'react'
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { usePathname } from 'next/navigation'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { Button } from '../primitives/button'
|
|
8
8
|
import { IconButton } from '../primitives/icon-button'
|
|
9
|
+
import { Skeleton } from '../primitives/skeleton'
|
|
9
10
|
import { usePortalInjectedMenuItems } from './hooks/usePortalInjectedMenuItems'
|
|
10
11
|
import { usePortalEventBridge } from './hooks/usePortalEventBridge'
|
|
11
12
|
import { mergeMenuItems } from '../backend/injection/mergeMenuItems'
|
|
@@ -117,6 +118,17 @@ function SidebarNavItem({
|
|
|
117
118
|
return null
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function SidebarNavSkeleton() {
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex flex-col gap-2 px-3 py-1" data-testid="portal-nav-loading">
|
|
124
|
+
<Skeleton className="h-9 w-full rounded-lg" />
|
|
125
|
+
<Skeleton className="h-9 w-11/12 rounded-lg" />
|
|
126
|
+
<Skeleton className="h-9 w-5/6 rounded-lg" />
|
|
127
|
+
<Skeleton className="h-9 w-10/12 rounded-lg" />
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
/* ---- User initials avatar ---- */
|
|
121
133
|
|
|
122
134
|
function UserAvatar({ name, className }: { name?: string; className?: string }) {
|
|
@@ -191,12 +203,15 @@ export function PortalShell({
|
|
|
191
203
|
const closeMobile = useCallback(() => setMobileOpen(false), [])
|
|
192
204
|
|
|
193
205
|
const [autoNavGroups, setAutoNavGroups] = useState<PortalNavGroup[]>([])
|
|
206
|
+
const [isNavLoading, setIsNavLoading] = useState(authenticated)
|
|
194
207
|
useEffect(() => {
|
|
195
208
|
if (!authenticated) {
|
|
196
209
|
setAutoNavGroups([])
|
|
210
|
+
setIsNavLoading(false)
|
|
197
211
|
return
|
|
198
212
|
}
|
|
199
213
|
let cancelled = false
|
|
214
|
+
setIsNavLoading(true)
|
|
200
215
|
const load = async () => {
|
|
201
216
|
try {
|
|
202
217
|
const { ok, result } = await apiCall<{ ok: boolean; groups?: PortalNavGroup[] }>(
|
|
@@ -206,6 +221,8 @@ export function PortalShell({
|
|
|
206
221
|
setAutoNavGroups(Array.isArray(result.groups) ? result.groups : [])
|
|
207
222
|
} catch {
|
|
208
223
|
if (!cancelled) setAutoNavGroups([])
|
|
224
|
+
} finally {
|
|
225
|
+
if (!cancelled) setIsNavLoading(false)
|
|
209
226
|
}
|
|
210
227
|
}
|
|
211
228
|
void load()
|
|
@@ -238,6 +255,10 @@ export function PortalShell({
|
|
|
238
255
|
return mergeMenuItems(builtIn, injectedAccountItems)
|
|
239
256
|
}, [authenticated, autoNavGroups, injectedAccountItems])
|
|
240
257
|
|
|
258
|
+
const shouldRenderMainNav = isNavLoading || mergedNavItems.length > 0
|
|
259
|
+
const shouldRenderAccountNav = mergedAccountItems.length > 0
|
|
260
|
+
const shouldRenderNav = shouldRenderMainNav || shouldRenderAccountNav
|
|
261
|
+
|
|
241
262
|
/* ---- PUBLIC LAYOUT ---- */
|
|
242
263
|
if (!authenticated) {
|
|
243
264
|
return (
|
|
@@ -291,41 +312,58 @@ export function PortalShell({
|
|
|
291
312
|
</Link>
|
|
292
313
|
</div>
|
|
293
314
|
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
315
|
+
<div
|
|
316
|
+
className="hidden"
|
|
317
|
+
data-testid="portal-nav-ready"
|
|
318
|
+
data-ready={isNavLoading ? 'false' : 'true'}
|
|
319
|
+
aria-hidden="true"
|
|
320
|
+
/>
|
|
321
|
+
|
|
322
|
+
{shouldRenderNav ? (
|
|
323
|
+
<nav aria-label="Portal navigation" className="flex-1 overflow-y-auto px-3 py-5">
|
|
324
|
+
{shouldRenderMainNav ? (
|
|
325
|
+
<>
|
|
326
|
+
<p className="mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50">
|
|
327
|
+
{t('portal.nav.home', 'Portal')}
|
|
328
|
+
</p>
|
|
329
|
+
{isNavLoading ? (
|
|
330
|
+
<SidebarNavSkeleton />
|
|
331
|
+
) : (
|
|
332
|
+
<div className="flex flex-col gap-0.5">
|
|
333
|
+
{mergedNavItems.map((item) => (
|
|
334
|
+
<SidebarNavItem
|
|
335
|
+
key={item.id}
|
|
336
|
+
item={item}
|
|
337
|
+
active={!!item.href && pathname.startsWith(item.href)}
|
|
338
|
+
t={t}
|
|
339
|
+
onClick={closeMobile}
|
|
340
|
+
/>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
</>
|
|
345
|
+
) : null}
|
|
346
|
+
|
|
347
|
+
{shouldRenderAccountNav ? (
|
|
348
|
+
<div className={shouldRenderMainNav ? 'mt-8' : ''}>
|
|
349
|
+
<p className="mb-2 px-3 text-overline font-semibold uppercase tracking-widest text-muted-foreground/50">
|
|
350
|
+
{t('portal.nav.account', 'Account')}
|
|
351
|
+
</p>
|
|
352
|
+
<div className="flex flex-col gap-0.5">
|
|
353
|
+
{mergedAccountItems.map((item) => (
|
|
354
|
+
<SidebarNavItem
|
|
355
|
+
key={item.id}
|
|
356
|
+
item={item}
|
|
357
|
+
active={!!item.href && pathname.startsWith(item.href)}
|
|
358
|
+
t={t}
|
|
359
|
+
onClick={closeMobile}
|
|
360
|
+
/>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
325
363
|
</div>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
364
|
+
) : null}
|
|
365
|
+
</nav>
|
|
366
|
+
) : null}
|
|
329
367
|
|
|
330
368
|
<div className="border-t px-3 py-3">
|
|
331
369
|
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2">
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { act, render, screen, waitFor } from '@testing-library/react'
|
|
7
|
+
import { PortalShell } from '../PortalShell'
|
|
8
|
+
|
|
9
|
+
const apiCallMock = jest.fn()
|
|
10
|
+
|
|
11
|
+
jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
|
|
12
|
+
useT: () => (key: string, fallback?: string) => fallback ?? key,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
jest.mock('next/link', () => {
|
|
16
|
+
const React = require('react')
|
|
17
|
+
return React.forwardRef(({ children, href, ...rest }: any, ref: React.ForwardedRef<HTMLAnchorElement>) => (
|
|
18
|
+
<a href={typeof href === 'string' ? href : href?.toString?.()} ref={ref} {...rest}>
|
|
19
|
+
{children}
|
|
20
|
+
</a>
|
|
21
|
+
))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
jest.mock('next/image', () => (props: any) => <img alt={props.alt} {...props} />)
|
|
25
|
+
|
|
26
|
+
jest.mock('next/navigation', () => ({
|
|
27
|
+
usePathname: () => '/acme/portal/orders',
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
jest.mock('../../backend/utils/apiCall', () => ({
|
|
31
|
+
apiCall: (...args: unknown[]) => apiCallMock(...args),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
jest.mock('../hooks/usePortalInjectedMenuItems', () => ({
|
|
35
|
+
usePortalInjectedMenuItems: () => ({
|
|
36
|
+
items: [],
|
|
37
|
+
isLoading: false,
|
|
38
|
+
}),
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
jest.mock('../hooks/usePortalEventBridge', () => ({
|
|
42
|
+
usePortalEventBridge: jest.fn(),
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
jest.mock('../components/PortalNotificationBell', () => ({
|
|
46
|
+
PortalNotificationBell: () => null,
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
function createDeferred<T>() {
|
|
50
|
+
let resolve!: (value: T) => void
|
|
51
|
+
const promise = new Promise<T>((res) => {
|
|
52
|
+
resolve = res
|
|
53
|
+
})
|
|
54
|
+
return { promise, resolve }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
apiCallMock.mockReset()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('PortalShell', () => {
|
|
62
|
+
it('shows a loading skeleton until the portal nav payload arrives', async () => {
|
|
63
|
+
const deferred = createDeferred<{
|
|
64
|
+
ok: boolean
|
|
65
|
+
result: {
|
|
66
|
+
ok: boolean
|
|
67
|
+
groups: Array<{
|
|
68
|
+
id: string
|
|
69
|
+
items: Array<{ id: string; label: string; href: string }>
|
|
70
|
+
}>
|
|
71
|
+
}
|
|
72
|
+
}>()
|
|
73
|
+
|
|
74
|
+
apiCallMock.mockReturnValueOnce(deferred.promise)
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<PortalShell
|
|
78
|
+
authenticated
|
|
79
|
+
orgSlug="acme"
|
|
80
|
+
organizationName="Acme"
|
|
81
|
+
userName="Ada Lovelace"
|
|
82
|
+
userEmail="ada@example.com"
|
|
83
|
+
onLogout={jest.fn()}
|
|
84
|
+
>
|
|
85
|
+
<div>Portal content</div>
|
|
86
|
+
</PortalShell>,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(apiCallMock).toHaveBeenCalledWith('/api/customer_accounts/portal/nav')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
expect(screen.getByTestId('portal-nav-loading')).toBeInTheDocument()
|
|
94
|
+
expect(screen.queryByRole('link', { name: 'Orders' })).not.toBeInTheDocument()
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
deferred.resolve({
|
|
98
|
+
ok: true,
|
|
99
|
+
result: {
|
|
100
|
+
ok: true,
|
|
101
|
+
groups: [
|
|
102
|
+
{
|
|
103
|
+
id: 'main',
|
|
104
|
+
items: [
|
|
105
|
+
{
|
|
106
|
+
id: 'orders',
|
|
107
|
+
label: 'Orders',
|
|
108
|
+
href: '/acme/portal/orders',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
await deferred.promise
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.queryByTestId('portal-nav-loading')).not.toBeInTheDocument()
|
|
120
|
+
expect(screen.getByRole('link', { name: 'Orders' })).toHaveAttribute('href', '/acme/portal/orders')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('does not keep an empty nav section visible when the payload has no items', async () => {
|
|
125
|
+
apiCallMock.mockResolvedValueOnce({
|
|
126
|
+
ok: true,
|
|
127
|
+
result: {
|
|
128
|
+
ok: true,
|
|
129
|
+
groups: [],
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
render(
|
|
134
|
+
<PortalShell
|
|
135
|
+
authenticated
|
|
136
|
+
orgSlug="acme"
|
|
137
|
+
organizationName="Acme"
|
|
138
|
+
onLogout={jest.fn()}
|
|
139
|
+
>
|
|
140
|
+
<div>Portal content</div>
|
|
141
|
+
</PortalShell>,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
await waitFor(() => {
|
|
145
|
+
expect(apiCallMock).toHaveBeenCalledWith('/api/customer_accounts/portal/nav')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(screen.queryByTestId('portal-nav-loading')).not.toBeInTheDocument()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(screen.queryByRole('navigation', { name: 'Portal navigation' })).not.toBeInTheDocument()
|
|
153
|
+
})
|
|
154
|
+
})
|