UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

273 lines (272 loc) • 9.7 kB
"use client"; import Icon from "../../Icon/Icon.mjs"; import { Tooltip } from "../../base-ui/Tooltip/Tooltip.mjs"; import ScrollShadow from "../../ScrollShadow/ScrollShadow.mjs"; import { collectNavItems, findActiveBranch, resolveActiveHref } from "./active.mjs"; import { useConsoleShellState } from "../ConsoleShell/context.mjs"; import { useLocalStorage, usePrefersReducedMotion } from "../hooks.mjs"; import { styles } from "./style.mjs"; import { useCallback, useEffect, useId, useRef, useState } from "react"; import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime"; import { ChevronDown, Play } from "lucide-react"; //#region src/dashboard/ConsoleNav/ConsoleNav.tsx const PANEL_TRANSITION_MS = 160; const RAIL_TRANSITION_MS = 200; /** Older builds stored the closed group keys as an array. */ function readOverrides(value) { if (Array.isArray(value)) return Object.fromEntries(value.map((key) => [String(key), false])); return value && typeof value === "object" ? value : {}; } function scrollActiveIntoView(container, behavior = "auto") { const link = container.querySelector("[aria-current=\"page\"]") ?? [...container.querySelectorAll("button[data-active=\"true\"]")].at(-1); if (!link) return; const containerRect = container.getBoundingClientRect(); const linkRect = link.getBoundingClientRect(); if (linkRect.top >= containerRect.top && linkRect.bottom <= containerRect.bottom) return; container.scrollBy({ behavior, top: linkRect.top - containerRect.top - (containerRect.height - linkRect.height) / 2 }); } function renderAnchor(context, linkProps, onFollow) { if (context.renderLink) return context.renderLink(linkProps); return /* @__PURE__ */ jsx("a", { "aria-current": linkProps["aria-current"], "aria-label": linkProps["aria-label"], className: linkProps.className, "data-active": linkProps["data-active"], "data-collapsed": linkProps["data-collapsed"], "data-indent": linkProps["data-indent"], href: linkProps.href, rel: linkProps.external ? "noreferrer" : void 0, target: linkProps.external ? "_blank" : void 0, title: linkProps.title, onClick: (event) => { if (context.onNavigate && !linkProps.external) event.preventDefault(); onFollow(); }, children: linkProps.children }); } function NavLink({ active, collapsed, context, href, icon, indent = 0, item, name, overlay = false }) { const shell = useConsoleShellState(); const label = item?.label ?? name; const follow = () => { context.onNavigate?.(href); shell?.closeNavigation(); }; const link = renderAnchor(context, { "aria-current": active ? "page" : void 0, "aria-label": collapsed ? name : void 0, "children": overlay ? null : /* @__PURE__ */ jsxs(Fragment$1, { children: [ icon ? /* @__PURE__ */ jsx(Icon, { icon, size: 18 }) : null, /* @__PURE__ */ jsx("span", { className: styles.itemLabel, children: label }), item?.badge ? /* @__PURE__ */ jsx("span", { className: styles.badge, children: item.badge }) : null, item?.badge ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: styles.dot }) : null ] }), "className": overlay ? styles.railLink : styles.item, "data-active": active, "data-collapsed": collapsed, "data-indent": indent, "external": item?.external, href, "onClick": follow, "title": collapsed ? void 0 : label }, follow); if (!icon && !overlay) return link; return /* @__PURE__ */ jsx(Tooltip, { disabled: !collapsed, placement: "right", title: name, children: link }); } function Fold({ children, folded }) { return /* @__PURE__ */ jsx("div", { "aria-hidden": folded || void 0, className: styles.fold, "data-folded": folded, inert: folded || void 0, children: /* @__PURE__ */ jsx("div", { children }) }); } function ConsoleNav({ collapsed: collapsedProp, defaultExpanded = "all", groups = [], items = [], label = "Primary", onNavigate, pathname, renderLink, storageKey }) { const id = useId(); const navRef = useRef(null); const shell = useConsoleShellState(); const collapsed = collapsedProp ?? shell?.collapsed ?? false; const reducedMotion = usePrefersReducedMotion(); const [storedOverrides, setStoredOverrides] = useLocalStorage(storageKey, {}); const overrides = readOverrides(storedOverrides); const context = { onNavigate, renderLink }; const activeHref = resolveActiveHref(pathname, [...items, ...collectNavItems(groups)]); const activeBranch = findActiveBranch(pathname, groups, activeHref); const [trackedPathname, setTrackedPathname] = useState(pathname); if (trackedPathname !== pathname) { setTrackedPathname(pathname); if (activeBranch.some((key) => overrides[key] === false)) { const next = { ...overrides }; for (const key of activeBranch) delete next[key]; setStoredOverrides(next); } } const isExpanded = (group) => overrides[group.key] ?? (activeBranch.includes(group.key) || (group.defaultExpanded ?? defaultExpanded === "all")); const toggleGroup = useCallback((key, expanded) => { setStoredOverrides((current) => ({ ...readOverrides(current), [key]: !expanded })); }, [setStoredOverrides]); useEffect(() => { const container = navRef.current; if (!container) return; scrollActiveIntoView(container); if (reducedMotion) return; const timer = window.setTimeout(() => scrollActiveIntoView(container), PANEL_TRANSITION_MS); return () => window.clearTimeout(timer); }, [ activeHref, pathname, reducedMotion ]); useEffect(() => { const container = navRef.current; if (!container) return; const timer = window.setTimeout(() => scrollActiveIntoView(container, reducedMotion ? "auto" : "smooth"), reducedMotion ? 0 : RAIL_TRANSITION_MS); return () => window.clearTimeout(timer); }, [collapsed, reducedMotion]); const renderItems = (list, indent, groupLabel, foldable) => list.map((item) => { const link = /* @__PURE__ */ jsx(NavLink, { active: item.href === activeHref, collapsed, context, href: item.href, icon: item.icon, indent: item.icon ? Math.max(0, indent - 1) : indent, item, name: groupLabel ? `${groupLabel} / ${item.label}` : item.label }, item.href); if (item.icon || !foldable) return link; return /* @__PURE__ */ jsx(Fold, { folded: collapsed, children: link }, item.href); }); const renderGroup = (group, level, indent, underRailIcon) => { const expanded = isExpanded(group); const panelId = `${id}-${group.key}`; const childIndent = indent + (group.icon ? 1 : 0); const active = activeBranch.includes(group.key); const railIcon = level === 0 && Boolean(group.icon); const onRail = collapsed && !underRailIcon; const open = onRail ? !railIcon : expanded; const railHref = railIcon ? group.href ?? collectNavItems([group])[0]?.href : void 0; const railEmpty = !railIcon && !collectNavItems([group]).some((item) => item.icon); const header = /* @__PURE__ */ jsxs("button", { "aria-controls": panelId, "aria-expanded": open, "aria-hidden": onRail && railIcon || void 0, className: styles.groupHeader, "data-active": active, "data-icon": Boolean(group.icon), "data-indent": indent, "data-level": level, tabIndex: onRail && railIcon ? -1 : void 0, type: "button", onClick: () => toggleGroup(group.key, expanded), children: [ group.icon ? /* @__PURE__ */ jsx(Icon, { icon: group.icon, size: 16 }) : null, /* @__PURE__ */ jsx("span", { className: styles.itemLabel, "data-label": "", children: group.label }), level === 0 ? /* @__PURE__ */ jsx(Icon, { className: styles.chevron, "data-expanded": expanded, icon: ChevronDown, size: 14 }) : /* @__PURE__ */ jsx("span", { className: styles.indicator, "data-expanded": expanded, children: /* @__PURE__ */ jsx(Play, { fill: "currentColor", size: 7, strokeWidth: 1 }) }) ] }); let head = header; if (railIcon) head = /* @__PURE__ */ jsxs("div", { className: styles.groupHead, children: [header, onRail && railHref ? /* @__PURE__ */ jsx(NavLink, { collapsed: true, overlay: true, active: activeBranch[0] === group.key, context, href: railHref, name: group.label }) : null] }); else if (!underRailIcon) head = /* @__PURE__ */ jsx(Fold, { folded: onRail, children: header }); return /* @__PURE__ */ jsxs("div", { className: styles.group, "data-icon": Boolean(group.icon), "data-level": level, "data-rail-empty": railEmpty, children: [head, /* @__PURE__ */ jsx("div", { "aria-hidden": !open, className: styles.groupPanel, "data-expanded": open, "data-instant": reducedMotion, id: panelId, inert: open ? void 0 : true, children: /* @__PURE__ */ jsxs("div", { className: styles.groupItems, children: [renderItems(group.items ?? [], childIndent, group.label, !(underRailIcon || railIcon)), (group.groups ?? []).map((child) => renderGroup(child, level + 1, childIndent, underRailIcon || railIcon))] }) })] }, group.key); }; return /* @__PURE__ */ jsxs(ScrollShadow, { "aria-label": label, as: "nav", className: styles.nav, "data-collapsed": collapsed, orientation: "vertical", ref: navRef, size: 4, children: [items.length > 0 ? /* @__PURE__ */ jsx("div", { className: styles.topItems, "data-divided": groups.length > 0, "data-rail-empty": !items.some((item) => item.icon), children: renderItems(items, 0, void 0, true) }) : null, groups.map((group) => renderGroup(group, 0, 0, false))] }); } ConsoleNav.displayName = "ConsoleNav"; //#endregion export { ConsoleNav as default }; //# sourceMappingURL=ConsoleNav.mjs.map