@hyddenlabs/hydn-ui 0.3.16 → 0.3.19
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/components/layout/left-nav-layout/left-nav-item.d.ts +6 -3
- package/dist/components/layout/left-nav-layout/left-nav-item.d.ts.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-item.js +38 -17
- package/dist/components/layout/left-nav-layout/left-nav-item.js.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-layout.d.ts +27 -21
- package/dist/components/layout/left-nav-layout/left-nav-layout.d.ts.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-layout.js +140 -94
- package/dist/components/layout/left-nav-layout/left-nav-layout.js.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-section.d.ts.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-section.js +10 -10
- package/dist/components/layout/left-nav-layout/left-nav-section.js.map +1 -1
- package/dist/components/navigation/navbar/navbar.js +2 -2
- package/dist/components/navigation/navbar/navbar.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/theme/tokens.js +2 -2
- package/dist/theme/tokens.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ReactNode, MouseEvent } from 'react';
|
|
2
|
+
import { Size } from '../../../theme/size-tokens';
|
|
2
3
|
export type LeftNavItemProps = {
|
|
3
4
|
/** Icon identifier string to display (uses Icon component) */
|
|
4
5
|
icon?: string;
|
|
5
|
-
/**
|
|
6
|
+
/** Icon size from unified size system */
|
|
7
|
+
iconSize?: Size;
|
|
8
|
+
/** Link label text (visible when expanded, hidden when collapsed) */
|
|
6
9
|
children: ReactNode;
|
|
7
10
|
/** Whether this item is currently active (highlighted state) */
|
|
8
11
|
active?: boolean;
|
|
@@ -12,7 +15,7 @@ export type LeftNavItemProps = {
|
|
|
12
15
|
badge?: ReactNode;
|
|
13
16
|
/** Destination URL for the navigation link (required for link mode) */
|
|
14
17
|
href?: string;
|
|
15
|
-
/** Accessible label override */
|
|
18
|
+
/** Accessible label override (used for tooltip in collapsed mode) */
|
|
16
19
|
title?: string;
|
|
17
20
|
/** Prevent actual navigation for demo/showcase mode (deprecated: use onClick instead) */
|
|
18
21
|
preventNavigation?: boolean;
|
|
@@ -45,7 +48,7 @@ export type LeftNavItemProps = {
|
|
|
45
48
|
* </LeftNavItem>
|
|
46
49
|
* ```
|
|
47
50
|
*/
|
|
48
|
-
declare function LeftNavItem({ icon, children, active, className, badge, href, title, preventNavigation, onClick, disableActiveState }: Readonly<LeftNavItemProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
51
|
+
declare function LeftNavItem({ icon, iconSize, children, active, className, badge, href, title, preventNavigation, onClick, disableActiveState }: Readonly<LeftNavItemProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
49
52
|
declare namespace LeftNavItem {
|
|
50
53
|
var displayName: string;
|
|
51
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-item.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"left-nav-item.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAA6D,MAAM,OAAO,CAAC;AAIzG,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE3C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,qEAAqE;IACrE,QAAQ,EAAE,SAAS,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uEAAuE;IACvE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC7E,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,iBAAS,WAAW,CAAC,EACnB,IAAI,EACJ,QAAe,EACf,QAAQ,EACR,MAAc,EACd,SAAc,EACd,KAAK,EACL,IAAI,EACJ,KAAK,EACL,iBAAyB,EACzB,OAAO,EACP,kBAA0B,EAC3B,EAAE,QAAQ,CAAC,gBAAgB,CAAC,kDA0H5B;kBAtIQ,WAAW;;;AA0IpB,eAAe,WAAW,CAAC"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from "../../system/icon/icon.js";
|
|
3
|
-
import { useRef, isValidElement, cloneElement } from "react";
|
|
3
|
+
import { useRef, useState, useEffect, isValidElement, cloneElement } from "react";
|
|
4
4
|
import { NavLink } from "react-router-dom";
|
|
5
|
+
import Tooltip from "../../feedback/tooltip/tooltip.js";
|
|
5
6
|
function LeftNavItem({
|
|
6
7
|
icon,
|
|
8
|
+
iconSize = "sm",
|
|
7
9
|
children,
|
|
8
10
|
active = false,
|
|
9
11
|
className = "",
|
|
@@ -15,25 +17,40 @@ function LeftNavItem({
|
|
|
15
17
|
disableActiveState = false
|
|
16
18
|
}) {
|
|
17
19
|
const navRef = useRef(null);
|
|
20
|
+
const [isCollapsed, setIsCollapsed] = useState(() => {
|
|
21
|
+
if (typeof window !== "undefined") {
|
|
22
|
+
const navElement = document.querySelector("nav[data-collapsed]");
|
|
23
|
+
return navElement?.getAttribute("data-collapsed") === "true";
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const checkCollapsed = () => {
|
|
29
|
+
const navElement2 = navRef.current?.closest("nav");
|
|
30
|
+
if (navElement2) {
|
|
31
|
+
setIsCollapsed(navElement2.getAttribute("data-collapsed") === "true");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
checkCollapsed();
|
|
35
|
+
const navElement = navRef.current?.closest("nav");
|
|
36
|
+
if (navElement) {
|
|
37
|
+
const observer = new MutationObserver(checkCollapsed);
|
|
38
|
+
observer.observe(navElement, {
|
|
39
|
+
attributes: true,
|
|
40
|
+
attributeFilter: ["data-collapsed"]
|
|
41
|
+
});
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}, []);
|
|
18
46
|
const itemTitle = title || (typeof children === "string" ? children : void 0);
|
|
19
47
|
const isButtonMode = !!onClick;
|
|
20
48
|
const isLinkMode = !isButtonMode && !!href;
|
|
21
|
-
const getBaseClasses = (isActive) => `group relative
|
|
49
|
+
const getBaseClasses = (isActive) => `group relative flex items-center gap-1 px-2 py-1.5 rounded-lg mx-2 [nav[data-collapsed='true']_&]:mx-2 [nav[data-collapsed='true']_&]:px-0 [nav[data-collapsed='true']_&]:w-12 [nav[data-collapsed='true']_&]:justify-center text-sm font-medium transition-colors duration-200 ${!disableActiveState && (isActive || active) ? "bg-primary text-primary-foreground shadow-sm" : "text-foreground hover:bg-muted-hover hover:text-foreground"} ${className}`;
|
|
22
50
|
const renderContent = (isActive) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
className: "absolute left-0 top-0 bottom-0 w-0.75",
|
|
27
|
-
style: {
|
|
28
|
-
background: "radial-gradient(70.52% 85.16% at 0% 53.3%, #B44F8C 28.85%, #3E4BA5 100%)"
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
),
|
|
32
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-1.5 px-3", children: [
|
|
33
|
-
icon && /* @__PURE__ */ jsx(Icon, { name: icon, size: "md" }),
|
|
34
|
-
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wide leading-none text-center select-none", children }),
|
|
35
|
-
badge && /* @__PURE__ */ jsx("span", { children: isValidElement(badge) && !disableActiveState && (isActive || active) ? cloneElement(badge, { inverted: true }) : badge })
|
|
36
|
-
] })
|
|
51
|
+
icon && /* @__PURE__ */ jsx(Icon, { name: icon, size: iconSize }),
|
|
52
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate [nav[data-collapsed='true']_&]:hidden", children }),
|
|
53
|
+
badge && /* @__PURE__ */ jsx("span", { className: "shrink-0 [nav[data-collapsed='true']_&]:hidden", children: isValidElement(badge) && !disableActiveState && (isActive || active) ? cloneElement(badge, { inverted: true }) : badge })
|
|
37
54
|
] });
|
|
38
55
|
const buttonElement = isButtonMode ? (
|
|
39
56
|
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
|
@@ -72,7 +89,11 @@ function LeftNavItem({
|
|
|
72
89
|
}
|
|
73
90
|
) : null;
|
|
74
91
|
const element = isButtonMode ? buttonElement : navLink;
|
|
75
|
-
|
|
92
|
+
if (!isCollapsed) {
|
|
93
|
+
return element;
|
|
94
|
+
}
|
|
95
|
+
const tooltipContent = typeof children === "string" ? children : itemTitle || "Menu Item";
|
|
96
|
+
return /* @__PURE__ */ jsx(Tooltip, { content: tooltipContent, position: "right", variant: "neutral", className: "w-fit", children: element });
|
|
76
97
|
}
|
|
77
98
|
LeftNavItem.displayName = "LeftNavItem";
|
|
78
99
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-item.js","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"sourcesContent":["import Icon from '../../system/icon/icon';\nimport { ReactNode, MouseEvent, useRef, cloneElement, isValidElement } from 'react';\nimport { NavLink } from 'react-router-dom';\n\nexport type LeftNavItemProps = {\n /** Icon identifier string to display (uses Icon component) */\n icon?: string;\n /** Link label text */\n children: ReactNode;\n /** Whether this item is currently active (highlighted state) */\n active?: boolean;\n /** Additional CSS classes applied to the navigation link */\n className?: string;\n /** Badge or count component to display on the right side */\n badge?: ReactNode;\n /** Destination URL for the navigation link (required for link mode) */\n href?: string;\n /** Accessible label override */\n title?: string;\n /** Prevent actual navigation for demo/showcase mode (deprecated: use onClick instead) */\n preventNavigation?: boolean;\n /** Click handler - when provided, renders as button instead of link */\n onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Disable active state highlighting (useful for non-navigation items like header buttons) */\n disableActiveState?: boolean;\n};\n\n/**\n * LeftNavItem - Navigation link or button item for LeftNavLayout\n *\n * Features:\n * - Polymorphic: renders as link (with href) or button (with onClick)\n * - Icon support with proper alignment\n * - Optional active state styling\n * - Tooltip on hover in collapsed mode (using Tooltip component with Portal)\n * - Badge/count support\n * - Accessible keyboard navigation\n *\n * @example\n * ```tsx\n * // Link mode\n * <LeftNavItem icon=\"home\" href=\"/\" active>\n * Home\n * </LeftNavItem>\n *\n * // Button mode\n * <LeftNavItem icon=\"settings\" onClick={() => console.log('clicked')} disableActiveState>\n * Settings\n * </LeftNavItem>\n * ```\n */\nfunction LeftNavItem({\n icon,\n children,\n active = false,\n className = '',\n badge,\n href,\n title,\n preventNavigation = false,\n onClick,\n disableActiveState = false\n}: Readonly<LeftNavItemProps>) {\n const navRef = useRef<HTMLAnchorElement | HTMLButtonElement>(null);\n\n // Use children as fallback title for accessibility\n const itemTitle = title || (typeof children === 'string' ? children : undefined);\n\n // Determine mode: button (onClick present) or link (href present)\n const isButtonMode = !!onClick;\n const isLinkMode = !isButtonMode && !!href;\n\n // Shared base classes for both button and link modes\n const getBaseClasses = (isActive: boolean) =>\n `group relative
|
|
1
|
+
{"version":3,"file":"left-nav-item.js","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"sourcesContent":["import Icon from '../../system/icon/icon';\nimport { ReactNode, MouseEvent, useRef, useState, useEffect, cloneElement, isValidElement } from 'react';\nimport { NavLink } from 'react-router-dom';\n\nimport Tooltip from '../../feedback/tooltip/tooltip';\nimport { Size } from '@/theme/size-tokens';\n\nexport type LeftNavItemProps = {\n /** Icon identifier string to display (uses Icon component) */\n icon?: string;\n /** Icon size from unified size system */\n iconSize?: Size;\n /** Link label text (visible when expanded, hidden when collapsed) */\n children: ReactNode;\n /** Whether this item is currently active (highlighted state) */\n active?: boolean;\n /** Additional CSS classes applied to the navigation link */\n className?: string;\n /** Badge or count component to display on the right side */\n badge?: ReactNode;\n /** Destination URL for the navigation link (required for link mode) */\n href?: string;\n /** Accessible label override (used for tooltip in collapsed mode) */\n title?: string;\n /** Prevent actual navigation for demo/showcase mode (deprecated: use onClick instead) */\n preventNavigation?: boolean;\n /** Click handler - when provided, renders as button instead of link */\n onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Disable active state highlighting (useful for non-navigation items like header buttons) */\n disableActiveState?: boolean;\n};\n\n/**\n * LeftNavItem - Navigation link or button item for LeftNavLayout\n *\n * Features:\n * - Polymorphic: renders as link (with href) or button (with onClick)\n * - Icon support with proper alignment\n * - Optional active state styling\n * - Tooltip on hover in collapsed mode (using Tooltip component with Portal)\n * - Badge/count support\n * - Accessible keyboard navigation\n *\n * @example\n * ```tsx\n * // Link mode\n * <LeftNavItem icon=\"home\" href=\"/\" active>\n * Home\n * </LeftNavItem>\n *\n * // Button mode\n * <LeftNavItem icon=\"settings\" onClick={() => console.log('clicked')} disableActiveState>\n * Settings\n * </LeftNavItem>\n * ```\n */\nfunction LeftNavItem({\n icon,\n iconSize = 'sm',\n children,\n active = false,\n className = '',\n badge,\n href,\n title,\n preventNavigation = false,\n onClick,\n disableActiveState = false\n}: Readonly<LeftNavItemProps>) {\n const navRef = useRef<HTMLAnchorElement | HTMLButtonElement>(null);\n const [isCollapsed, setIsCollapsed] = useState(() => {\n // Try to get initial collapsed state from nav element if it exists\n if (typeof window !== 'undefined') {\n const navElement = document.querySelector('nav[data-collapsed]');\n return navElement?.getAttribute('data-collapsed') === 'true';\n }\n return false;\n });\n\n // Check if nav is collapsed by looking at parent nav element\n useEffect(() => {\n const checkCollapsed = () => {\n const navElement = navRef.current?.closest('nav');\n if (navElement) {\n setIsCollapsed(navElement.getAttribute('data-collapsed') === 'true');\n }\n };\n\n checkCollapsed();\n\n // Create observer to watch for attribute changes\n const navElement = navRef.current?.closest('nav');\n if (navElement) {\n const observer = new MutationObserver(checkCollapsed);\n observer.observe(navElement, {\n attributes: true,\n attributeFilter: ['data-collapsed']\n });\n return () => observer.disconnect();\n }\n\n return undefined;\n }, []);\n\n // Use children as fallback title for accessibility\n const itemTitle = title || (typeof children === 'string' ? children : undefined);\n\n // Determine mode: button (onClick present) or link (href present)\n const isButtonMode = !!onClick;\n const isLinkMode = !isButtonMode && !!href;\n\n // Shared base classes for both button and link modes\n const getBaseClasses = (isActive: boolean) =>\n `group relative flex items-center gap-1 px-2 py-1.5 rounded-lg mx-2 [nav[data-collapsed='true']_&]:mx-2 [nav[data-collapsed='true']_&]:px-0 [nav[data-collapsed='true']_&]:w-12 [nav[data-collapsed='true']_&]:justify-center text-sm font-medium transition-colors duration-200 ${\n !disableActiveState && (isActive || active)\n ? 'bg-primary text-primary-foreground shadow-sm'\n : 'text-foreground hover:bg-muted-hover hover:text-foreground'\n } ${className}`;\n\n // Shared content renderer\n const renderContent = (isActive: boolean) => (\n <>\n {icon && <Icon name={icon} size={iconSize} />}\n\n <span className=\"flex-1 truncate [nav[data-collapsed='true']_&]:hidden\">{children}</span>\n\n {badge && (\n <span className=\"shrink-0 [nav[data-collapsed='true']_&]:hidden\">\n {/* Auto-inject inverted prop when nav item is active */}\n {isValidElement(badge) && !disableActiveState && (isActive || active)\n ? cloneElement(badge, { inverted: true } as { inverted: boolean })\n : badge}\n </span>\n )}\n </>\n );\n\n // Button mode rendering\n const buttonElement = isButtonMode ? (\n // eslint-disable-next-line jsx-a11y/anchor-is-valid\n <a\n ref={navRef as React.RefObject<HTMLAnchorElement>}\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n onClick?.(e as unknown as MouseEvent<HTMLAnchorElement>);\n }}\n className={getBaseClasses(active)}\n aria-current={!disableActiveState && active ? 'page' : undefined}\n aria-label={itemTitle}\n role=\"button\"\n >\n {renderContent(active)}\n </a>\n ) : null;\n\n // Link mode rendering\n const navLink = isLinkMode ? (\n <NavLink\n ref={navRef as React.RefObject<HTMLAnchorElement>}\n to={href!}\n onClick={(e: MouseEvent<HTMLAnchorElement>) => {\n if (preventNavigation) {\n e.preventDefault();\n }\n }}\n className={({ isActive }) => getBaseClasses(isActive)}\n aria-current={!disableActiveState && active ? 'page' : undefined}\n aria-label={itemTitle}\n end\n >\n {({ isActive }) => renderContent(isActive)}\n </NavLink>\n ) : null;\n\n const element = isButtonMode ? buttonElement : navLink;\n\n // Only show tooltip when collapsed\n if (!isCollapsed) {\n return element;\n }\n\n // Get tooltip content\n const tooltipContent = typeof children === 'string' ? children : itemTitle || 'Menu Item';\n\n return (\n <Tooltip content={tooltipContent} position=\"right\" variant=\"neutral\" className=\"w-fit\">\n {element}\n </Tooltip>\n );\n}\n\nLeftNavItem.displayName = 'LeftNavItem';\n\nexport default LeftNavItem;\n"],"names":["navElement"],"mappings":"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"}
|
|
@@ -4,56 +4,62 @@ export type LeftNavLayoutProps = {
|
|
|
4
4
|
nav: ReactNode;
|
|
5
5
|
/** Main content area */
|
|
6
6
|
children: ReactNode;
|
|
7
|
+
/** Whether sidebar is collapsed (icon-only mode) */
|
|
8
|
+
collapsed?: boolean;
|
|
9
|
+
/** Callback when collapse state changes */
|
|
10
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
11
|
+
/** Whether to show collapse toggle button */
|
|
12
|
+
showToggle?: boolean;
|
|
7
13
|
/** Custom className for the layout container */
|
|
8
14
|
className?: string;
|
|
9
15
|
/** Custom className for the nav sidebar */
|
|
10
16
|
navClassName?: string;
|
|
11
17
|
/** Custom className for the content area */
|
|
12
18
|
contentClassName?: string;
|
|
13
|
-
/** Width of the sidebar */
|
|
19
|
+
/** Width of the sidebar when expanded */
|
|
14
20
|
navWidth?: string;
|
|
21
|
+
/** Width of the sidebar when collapsed */
|
|
22
|
+
navWidthCollapsed?: string;
|
|
15
23
|
/** Hide navigation on mobile (slides in as overlay when toggled) */
|
|
16
24
|
hideOnMobile?: boolean;
|
|
25
|
+
/** Auto-collapse to icon-only mode on mobile screens */
|
|
26
|
+
autoCollapseOnMobile?: boolean;
|
|
17
27
|
/** Embedded demo mode (no fixed positioning, constrained height) */
|
|
18
28
|
embedded?: boolean;
|
|
19
29
|
/** Ref for the main content scrollable element */
|
|
20
30
|
mainContentRef?: React.RefObject<HTMLDivElement>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
/** Content pinned to the bottom of the sidebar (e.g. profile, logout actions) */
|
|
24
|
-
bottomContent?: ReactNode;
|
|
25
|
-
/** Optional bar rendered above the main content area, to the right of the sidebar */
|
|
26
|
-
topbar?: ReactNode;
|
|
31
|
+
/** Optional header text for the navigation sidebar */
|
|
32
|
+
header?: string;
|
|
27
33
|
};
|
|
28
34
|
/**
|
|
29
|
-
* LeftNavLayout -
|
|
35
|
+
* LeftNavLayout - Full-featured responsive left navigation layout
|
|
30
36
|
*
|
|
31
|
-
* Provides a sidebar navigation with:
|
|
32
|
-
* -
|
|
33
|
-
* -
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
37
|
+
* Provides a robust sidebar navigation with:
|
|
38
|
+
* - Full and collapsed (icon-only) states
|
|
39
|
+
* - Smooth transitions without janky spacing
|
|
40
|
+
* - Proper scroll handling
|
|
41
|
+
* - Responsive mobile behavior
|
|
42
|
+
* - Accessible keyboard navigation
|
|
37
43
|
*
|
|
38
44
|
* @example
|
|
39
45
|
* ```tsx
|
|
40
46
|
* <LeftNavLayout
|
|
41
|
-
*
|
|
47
|
+
* collapsed={isCollapsed}
|
|
48
|
+
* onCollapsedChange={setIsCollapsed}
|
|
42
49
|
* nav={
|
|
43
50
|
* <>
|
|
44
|
-
* <
|
|
45
|
-
*
|
|
51
|
+
* <LeftNavSection>
|
|
52
|
+
* <LeftNavItem icon={<HomeIcon />} href="/" active>Home</LeftNavItem>
|
|
53
|
+
* <LeftNavItem icon={<DashboardIcon />} href="/dashboard">Dashboard</LeftNavItem>
|
|
54
|
+
* </LeftNavSection>
|
|
46
55
|
* </>
|
|
47
56
|
* }
|
|
48
|
-
* bottomContent={
|
|
49
|
-
* <LeftNavItem icon="logout" onClick={logout}>Logout</LeftNavItem>
|
|
50
|
-
* }
|
|
51
57
|
* >
|
|
52
58
|
* <main>Your content here</main>
|
|
53
59
|
* </LeftNavLayout>
|
|
54
60
|
* ```
|
|
55
61
|
*/
|
|
56
|
-
declare function LeftNavLayout({ nav, children, className, navClassName, contentClassName, navWidth,
|
|
62
|
+
declare function LeftNavLayout({ nav, children, collapsed: controlledCollapsed, onCollapsedChange, showToggle, className, navClassName, contentClassName, navWidth, navWidthCollapsed, hideOnMobile, autoCollapseOnMobile, embedded, header, mainContentRef }: Readonly<LeftNavLayoutProps>): import("react/jsx-runtime").JSX.Element;
|
|
57
63
|
declare namespace LeftNavLayout {
|
|
58
64
|
var displayName: string;
|
|
59
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-layout.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-layout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"left-nav-layout.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-layout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgD,MAAM,OAAO,CAAC;AAIhF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iCAAiC;IACjC,GAAG,EAAE,SAAS,CAAC;IACf,wBAAwB;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2CAA2C;IAC3C,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,6CAA6C;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IACjD,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,iBAAS,aAAa,CAAC,EACrB,GAAG,EACH,QAAQ,EACR,SAAS,EAAE,mBAAmB,EAC9B,iBAAiB,EACjB,UAAiB,EACjB,SAAc,EACd,YAAiB,EACjB,gBAAqB,EACrB,QAAkB,EAClB,iBAA4B,EAC5B,YAAoB,EACpB,oBAA2B,EAC3B,QAAgB,EAChB,MAAqB,EACrB,cAAc,EACf,EAAE,QAAQ,CAAC,kBAAkB,CAAC,2CAwN9B;kBAxOQ,aAAa;;;AA4OtB,eAAe,aAAa,CAAC"}
|
|
@@ -1,63 +1,123 @@
|
|
|
1
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef, useEffect } from "react";
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useLayoutEffect, useEffect } from "react";
|
|
3
3
|
import useScrollReset from "../../../hooks/useScrollReset.js";
|
|
4
|
-
import
|
|
4
|
+
import LeftNavItem from "./left-nav-item.js";
|
|
5
5
|
function LeftNavLayout({
|
|
6
6
|
nav,
|
|
7
7
|
children,
|
|
8
|
+
collapsed: controlledCollapsed,
|
|
9
|
+
onCollapsedChange,
|
|
10
|
+
showToggle = true,
|
|
8
11
|
className = "",
|
|
9
12
|
navClassName = "",
|
|
10
13
|
contentClassName = "",
|
|
11
|
-
navWidth = "
|
|
14
|
+
navWidth = "16rem",
|
|
15
|
+
navWidthCollapsed = "4.5rem",
|
|
12
16
|
hideOnMobile = false,
|
|
17
|
+
autoCollapseOnMobile = true,
|
|
13
18
|
embedded = false,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
bottomContent,
|
|
17
|
-
topbar
|
|
19
|
+
header = "Navigation",
|
|
20
|
+
mainContentRef
|
|
18
21
|
}) {
|
|
22
|
+
const STORAGE_KEY_DESKTOP = "left-nav-desktop-collapsed";
|
|
23
|
+
const STORAGE_KEY_MOBILE = "left-nav-mobile-collapsed";
|
|
24
|
+
const [internalCollapsed, setInternalCollapsed] = useState(() => {
|
|
25
|
+
if (typeof window === "undefined") return false;
|
|
26
|
+
try {
|
|
27
|
+
const saved = localStorage.getItem(STORAGE_KEY_DESKTOP);
|
|
28
|
+
return saved !== null ? saved === "true" : false;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.warn("Failed to read desktop collapsed state from localStorage:", error);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
19
34
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(!hideOnMobile);
|
|
20
|
-
const [
|
|
21
|
-
const [
|
|
22
|
-
|
|
35
|
+
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024);
|
|
36
|
+
const [mobileCollapsedOverride, setMobileCollapsedOverride] = useState(() => {
|
|
37
|
+
if (typeof window === "undefined") return null;
|
|
38
|
+
try {
|
|
39
|
+
const saved = localStorage.getItem(STORAGE_KEY_MOBILE);
|
|
40
|
+
return saved !== null ? saved === "true" : null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn("Failed to read mobile collapsed state from localStorage:", error);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const desktopCollapsedRef = useRef(void 0);
|
|
23
47
|
const navRef = useRef(null);
|
|
24
48
|
const scrollPosRef = useRef(0);
|
|
25
49
|
const internalContentRef = useRef(null);
|
|
26
50
|
const contentRef = mainContentRef || internalContentRef;
|
|
51
|
+
useLayoutEffect(() => {
|
|
52
|
+
const checkMobile = () => {
|
|
53
|
+
const newIsMobile = window.innerWidth < 1024;
|
|
54
|
+
if (newIsMobile !== isMobile) {
|
|
55
|
+
const wasDesktop = !isMobile;
|
|
56
|
+
const isNowDesktop = !newIsMobile;
|
|
57
|
+
setIsMobile(newIsMobile);
|
|
58
|
+
if (wasDesktop && autoCollapseOnMobile) {
|
|
59
|
+
const currentCollapsed = controlledCollapsed ?? internalCollapsed;
|
|
60
|
+
desktopCollapsedRef.current = currentCollapsed;
|
|
61
|
+
}
|
|
62
|
+
if (isNowDesktop && autoCollapseOnMobile && desktopCollapsedRef.current !== void 0) {
|
|
63
|
+
if (controlledCollapsed === void 0) {
|
|
64
|
+
setInternalCollapsed(desktopCollapsedRef.current);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!isNowDesktop && autoCollapseOnMobile) {
|
|
68
|
+
try {
|
|
69
|
+
const savedMobile = localStorage.getItem(STORAGE_KEY_MOBILE);
|
|
70
|
+
setMobileCollapsedOverride(savedMobile !== null ? savedMobile === "true" : null);
|
|
71
|
+
} catch {
|
|
72
|
+
setMobileCollapsedOverride(null);
|
|
73
|
+
}
|
|
74
|
+
} else if (isNowDesktop) {
|
|
75
|
+
setMobileCollapsedOverride(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
checkMobile();
|
|
80
|
+
window.addEventListener("resize", checkMobile);
|
|
81
|
+
return () => window.removeEventListener("resize", checkMobile);
|
|
82
|
+
}, [isMobile, autoCollapseOnMobile, controlledCollapsed, internalCollapsed]);
|
|
83
|
+
const collapsed = controlledCollapsed ?? internalCollapsed;
|
|
84
|
+
const setCollapsed = (value) => {
|
|
85
|
+
if (onCollapsedChange) {
|
|
86
|
+
onCollapsedChange(value);
|
|
87
|
+
} else {
|
|
88
|
+
setInternalCollapsed(value);
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem(STORAGE_KEY_DESKTOP, String(value));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn("Failed to save desktop collapsed state to localStorage:", error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const effectiveCollapsed = autoCollapseOnMobile && isMobile ? mobileCollapsedOverride !== null ? mobileCollapsedOverride : true : collapsed;
|
|
97
|
+
const toggleCollapsed = () => setCollapsed(!collapsed);
|
|
98
|
+
const closeMobileMenu = () => setMobileMenuOpen(false);
|
|
99
|
+
const handleToggleClick = () => {
|
|
100
|
+
if (hideOnMobile && isMobile) {
|
|
101
|
+
closeMobileMenu();
|
|
102
|
+
} else if (autoCollapseOnMobile && isMobile) {
|
|
103
|
+
const newMobileState = !effectiveCollapsed;
|
|
104
|
+
setMobileCollapsedOverride(newMobileState);
|
|
105
|
+
try {
|
|
106
|
+
localStorage.setItem(STORAGE_KEY_MOBILE, String(newMobileState));
|
|
107
|
+
} catch {
|
|
108
|
+
console.warn("Failed to save mobile collapsed state to localStorage:");
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
toggleCollapsed();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
27
114
|
useEffect(() => {
|
|
28
115
|
if (navRef.current) {
|
|
29
116
|
navRef.current.scrollTop = scrollPosRef.current;
|
|
30
117
|
}
|
|
31
118
|
}, [children]);
|
|
32
119
|
useScrollReset([children], contentRef);
|
|
33
|
-
const
|
|
34
|
-
const el = navRef.current;
|
|
35
|
-
if (!el) return;
|
|
36
|
-
setShowScrollDown(el.scrollHeight > el.clientHeight && el.scrollTop < el.scrollHeight - el.clientHeight - 4);
|
|
37
|
-
setShowScrollUp(el.scrollTop > 50);
|
|
38
|
-
};
|
|
39
|
-
const scrollDown = () => {
|
|
40
|
-
const el = navRef.current;
|
|
41
|
-
if (!el) return;
|
|
42
|
-
el.scrollBy({ top: el.clientHeight * 0.75, behavior: "smooth" });
|
|
43
|
-
};
|
|
44
|
-
const scrollUp = () => {
|
|
45
|
-
const el = navRef.current;
|
|
46
|
-
if (!el) return;
|
|
47
|
-
el.scrollBy({ top: -el.clientHeight * 0.75, behavior: "smooth" });
|
|
48
|
-
};
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
updateScrollDirection();
|
|
51
|
-
const el = navRef.current;
|
|
52
|
-
if (!el) return;
|
|
53
|
-
const ro = new ResizeObserver(updateScrollDirection);
|
|
54
|
-
ro.observe(el);
|
|
55
|
-
return () => ro.disconnect();
|
|
56
|
-
}, [nav]);
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
updateScrollDirection();
|
|
59
|
-
}, []);
|
|
60
|
-
const containerClasses = embedded ? "flex bg-background border border-border rounded-lg overflow-hidden" : "fixed inset-0 flex overflow-hidden bg-background";
|
|
120
|
+
const containerClasses = embedded ? "flex bg-background border border-border rounded-lg overflow-hidden" : "flex h-[calc(100vh-3.5rem)] md:h-[calc(100vh-4rem)] bg-background";
|
|
61
121
|
return /* @__PURE__ */ jsxs("div", { className: `${containerClasses} ${className}`, children: [
|
|
62
122
|
!embedded && hideOnMobile && mobileMenuOpen && /* @__PURE__ */ jsx(
|
|
63
123
|
"div",
|
|
@@ -67,66 +127,52 @@ function LeftNavLayout({
|
|
|
67
127
|
"aria-hidden": "true"
|
|
68
128
|
}
|
|
69
129
|
),
|
|
70
|
-
/* @__PURE__ */
|
|
130
|
+
/* @__PURE__ */ jsx(
|
|
71
131
|
"aside",
|
|
72
132
|
{
|
|
73
|
-
className: `${embedded ? "relative flex flex-col h-full" : hideOnMobile ? "fixed lg:relative top-0 left-0 z-40 lg:z-10 h-
|
|
133
|
+
className: `${embedded ? "relative flex flex-col h-full" : hideOnMobile ? "fixed lg:relative top-14 md:top-16 lg:top-0 left-0 z-40 lg:z-10 h-[calc(100vh-3.5rem)] md:h-[calc(100vh-4rem)] lg:h-full" : "relative h-full"} flex flex-col shrink-0 bg-background border-r border-border transition-all duration-300 ease-in-out ${!embedded && hideOnMobile && !mobileMenuOpen ? "-translate-x-full lg:translate-x-0" : "translate-x-0"} ${effectiveCollapsed ? "w-18" : "w-64"} ${navClassName}`,
|
|
134
|
+
style: !effectiveCollapsed && navWidth !== "16rem" || effectiveCollapsed && navWidthCollapsed !== "4.5rem" ? {
|
|
135
|
+
width: effectiveCollapsed ? navWidthCollapsed : navWidth
|
|
136
|
+
} : void 0,
|
|
74
137
|
"aria-label": "Main navigation",
|
|
75
|
-
children:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"button",
|
|
104
|
-
{
|
|
105
|
-
onClick: scrollUp,
|
|
106
|
-
className: "group absolute top-0 left-0 right-0 h-16 flex items-start justify-center pt-2 cursor-pointer z-10",
|
|
107
|
-
"aria-label": "Scroll up",
|
|
108
|
-
children: [
|
|
109
|
-
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-linear-to-b from-[#24273D] via-[#24273D]/90 to-transparent" }),
|
|
110
|
-
/* @__PURE__ */ jsx("div", { className: "relative transition-transform duration-150 group-hover:-translate-y-0.5 pr-2.5", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-up", size: "md", color: "neutral" }) })
|
|
111
|
-
]
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
] }),
|
|
115
|
-
bottomContent && /* @__PURE__ */ jsx("div", { className: "border-t border-[#2e3148] shrink-0", children: bottomContent })
|
|
116
|
-
]
|
|
138
|
+
children: /* @__PURE__ */ jsxs(
|
|
139
|
+
"nav",
|
|
140
|
+
{
|
|
141
|
+
ref: navRef,
|
|
142
|
+
className: "flex-1 overflow-y-auto overflow-x-hidden scrollbar-thin",
|
|
143
|
+
"data-collapsed": effectiveCollapsed,
|
|
144
|
+
onScroll: (e) => {
|
|
145
|
+
scrollPosRef.current = e.currentTarget.scrollTop;
|
|
146
|
+
},
|
|
147
|
+
children: [
|
|
148
|
+
showToggle && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "py-2", children: [
|
|
149
|
+
/* @__PURE__ */ jsx(
|
|
150
|
+
LeftNavItem,
|
|
151
|
+
{
|
|
152
|
+
icon: effectiveCollapsed ? "layout-sidebar-left-expand" : "layout-sidebar-left-collapse",
|
|
153
|
+
iconSize: "md",
|
|
154
|
+
onClick: handleToggleClick,
|
|
155
|
+
disableActiveState: true,
|
|
156
|
+
title: effectiveCollapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
157
|
+
children: header
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsx("div", { className: "h-px bg-border mx-3 mt-1.5", "aria-hidden": "true" })
|
|
161
|
+
] }) }),
|
|
162
|
+
nav
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
)
|
|
117
166
|
}
|
|
118
167
|
),
|
|
119
|
-
/* @__PURE__ */
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
)
|
|
129
|
-
] })
|
|
168
|
+
/* @__PURE__ */ jsx(
|
|
169
|
+
"main",
|
|
170
|
+
{
|
|
171
|
+
ref: contentRef,
|
|
172
|
+
className: `flex-1 overflow-y-auto ${embedded ? "h-60" : ""} ${contentClassName}`,
|
|
173
|
+
children
|
|
174
|
+
}
|
|
175
|
+
)
|
|
130
176
|
] });
|
|
131
177
|
}
|
|
132
178
|
LeftNavLayout.displayName = "LeftNavLayout";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-layout.js","sources":["../../../../src/components/layout/left-nav-layout/left-nav-layout.tsx"],"sourcesContent":["import { ReactNode, useState, useRef, useEffect } from 'react';\nimport useScrollReset from '@/hooks/useScrollReset';\nimport { Icon } from '../../system/icon/icon';\n\nexport type LeftNavLayoutProps = {\n /** Navigation sidebar content */\n nav: ReactNode;\n /** Main content area */\n children: ReactNode;\n /** Custom className for the layout container */\n className?: string;\n /** Custom className for the nav sidebar */\n navClassName?: string;\n /** Custom className for the content area */\n contentClassName?: string;\n /** Width of the sidebar */\n navWidth?: string;\n /** Hide navigation on mobile (slides in as overlay when toggled) */\n hideOnMobile?: boolean;\n /** Embedded demo mode (no fixed positioning, constrained height) */\n embedded?: boolean;\n /** Ref for the main content scrollable element */\n mainContentRef?: React.RefObject<HTMLDivElement>;\n /** Logo or brand content rendered in the sidebar header */\n logo?: ReactNode;\n /** Content pinned to the bottom of the sidebar (e.g. profile, logout actions) */\n bottomContent?: ReactNode;\n /** Optional bar rendered above the main content area, to the right of the sidebar */\n topbar?: ReactNode;\n};\n\n/**\n * LeftNavLayout - Fixed-width left navigation sidebar layout\n *\n * Provides a sidebar navigation with:\n * - Fixed width (always visible, no collapse)\n * - Logo/brand header slot\n * - Scrollable nav area\n * - Pinned bottom content slot\n * - Optional mobile overlay mode\n *\n * @example\n * ```tsx\n * <LeftNavLayout\n * logo={<img src=\"/logo.png\" alt=\"Logo\" className=\"h-8\" />}\n * nav={\n * <>\n * <LeftNavItem icon=\"home\" href=\"/\">Home</LeftNavItem>\n * <LeftNavItem icon=\"dashboard\" href=\"/dashboard\">Dashboard</LeftNavItem>\n * </>\n * }\n * bottomContent={\n * <LeftNavItem icon=\"logout\" onClick={logout}>Logout</LeftNavItem>\n * }\n * >\n * <main>Your content here</main>\n * </LeftNavLayout>\n * ```\n */\nfunction LeftNavLayout({\n nav,\n children,\n className = '',\n navClassName = '',\n contentClassName = '',\n navWidth = 'w-26',\n hideOnMobile = false,\n embedded = false,\n mainContentRef,\n logo,\n bottomContent,\n topbar\n}: Readonly<LeftNavLayoutProps>) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(!hideOnMobile);\n const [showScrollDown, setShowScrollDown] = useState(false);\n const [showScrollUp, setShowScrollUp] = useState(false);\n\n const closeMobileMenu = () => setMobileMenuOpen(false);\n\n const navRef = useRef<HTMLDivElement | null>(null);\n const scrollPosRef = useRef<number>(0);\n const internalContentRef = useRef<HTMLElement | null>(null);\n const contentRef = mainContentRef || internalContentRef;\n\n // Restore nav scroll position after children change (route switch) & reset scroll\n useEffect(() => {\n if (navRef.current) {\n navRef.current.scrollTop = scrollPosRef.current;\n }\n }, [children]);\n\n // Robust scroll reset for main content area on children change\n // Pass the ref object (not .current) to avoid reading ref during render\n useScrollReset([children], contentRef as React.RefObject<HTMLElement | null>);\n\n const updateScrollDirection = () => {\n const el = navRef.current;\n if (!el) return;\n setShowScrollDown(el.scrollHeight > el.clientHeight && el.scrollTop < el.scrollHeight - el.clientHeight - 4);\n setShowScrollUp(el.scrollTop > 50);\n };\n\n const scrollDown = () => {\n const el = navRef.current;\n if (!el) return;\n el.scrollBy({ top: el.clientHeight * 0.75, behavior: 'smooth' });\n };\n\n const scrollUp = () => {\n const el = navRef.current;\n if (!el) return;\n el.scrollBy({ top: -el.clientHeight * 0.75, behavior: 'smooth' });\n };\n\n useEffect(() => {\n updateScrollDirection();\n const el = navRef.current;\n if (!el) return;\n const ro = new ResizeObserver(updateScrollDirection);\n ro.observe(el);\n return () => ro.disconnect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [nav]);\n\n useEffect(() => {\n updateScrollDirection();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const containerClasses = embedded\n ? 'flex bg-background border border-border rounded-lg overflow-hidden'\n : 'fixed inset-0 flex overflow-hidden bg-background';\n\n return (\n <div className={`${containerClasses} ${className}`}>\n {/* Mobile Backdrop Overlay - Only shows on mobile when menu is open */}\n {!embedded && hideOnMobile && mobileMenuOpen && (\n <div\n className=\"fixed inset-0 bg-black/50 z-30 lg:hidden transition-opacity duration-300\"\n onClick={closeMobileMenu}\n aria-hidden=\"true\"\n />\n )}\n\n {/* Sidebar Navigation */}\n <aside\n className={`${\n embedded\n ? 'relative flex flex-col h-full'\n : hideOnMobile\n ? 'fixed lg:relative top-0 left-0 z-40 lg:z-10 h-screen lg:h-full'\n : 'relative h-full'\n } flex flex-col shrink-0 bg-[#24273D] ${\n !embedded && hideOnMobile && !mobileMenuOpen ? '-translate-x-full lg:translate-x-0' : 'translate-x-0'\n } ${navWidth} ${navClassName}`}\n aria-label=\"Main navigation\"\n >\n {/* Logo / Brand Header */}\n {logo && (\n <div className=\"h-16 bg-[#151823] flex items-center justify-center border-b border-[#2e3148] shadow-sm shrink-0 px-4\">\n {logo}\n </div>\n )}\n\n {/* Nav Content - Scrollable */}\n <div className=\"relative flex-1 min-h-0 flex flex-col\">\n <nav\n ref={navRef}\n className=\"flex-1 overflow-y-auto divide-y divide-muted-foreground scrollbar-dark\"\n onScroll={(e) => {\n scrollPosRef.current = (e.currentTarget as HTMLDivElement).scrollTop;\n updateScrollDirection();\n }}\n >\n {nav}\n </nav>\n\n {/* Scroll-down indicator */}\n {showScrollDown && (\n <button\n onClick={scrollDown}\n className=\"group absolute bottom-0 left-0 right-0 h-16 flex items-end justify-center pb-2 cursor-pointer z-10\"\n aria-label=\"Scroll down\"\n >\n <div className=\"absolute inset-0 bg-linear-to-t from-[#24273D] via-[#24273D]/90 to-transparent\" />\n <div className=\"relative transition-transform duration-150 group-hover:translate-y-0.5 pr-2.5\">\n <Icon name=\"chevron-down\" size=\"md\" color=\"neutral\" />\n </div>\n </button>\n )}\n {showScrollUp && (\n <button\n onClick={scrollUp}\n className=\"group absolute top-0 left-0 right-0 h-16 flex items-start justify-center pt-2 cursor-pointer z-10\"\n aria-label=\"Scroll up\"\n >\n <div className=\"absolute inset-0 bg-linear-to-b from-[#24273D] via-[#24273D]/90 to-transparent\" />\n <div className=\"relative transition-transform duration-150 group-hover:-translate-y-0.5 pr-2.5\">\n <Icon name=\"chevron-up\" size=\"md\" color=\"neutral\" />\n </div>\n </button>\n )}\n </div>\n\n {/* Bottom Pinned Section */}\n {bottomContent && <div className=\"border-t border-[#2e3148] shrink-0\">{bottomContent}</div>}\n </aside>\n\n {/* Right side: topbar + main content */}\n <div className=\"flex-1 flex flex-col overflow-hidden\">\n {topbar && <div className=\"shrink-0\">{topbar}</div>}\n\n {/* Main Content Area */}\n <main\n ref={contentRef as React.RefObject<HTMLDivElement>}\n className={`flex-1 overflow-y-auto ${embedded ? 'h-60' : ''} ${contentClassName}`}\n >\n {children}\n </main>\n </div>\n </div>\n );\n}\n\nLeftNavLayout.displayName = 'LeftNavLayout';\n\nexport default LeftNavLayout;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"left-nav-layout.js","sources":["../../../../src/components/layout/left-nav-layout/left-nav-layout.tsx"],"sourcesContent":["import { ReactNode, useState, useRef, useEffect, useLayoutEffect } from 'react';\nimport useScrollReset from '@/hooks/useScrollReset';\nimport LeftNavItem from './left-nav-item';\n\nexport type LeftNavLayoutProps = {\n /** Navigation sidebar content */\n nav: ReactNode;\n /** Main content area */\n children: ReactNode;\n /** Whether sidebar is collapsed (icon-only mode) */\n collapsed?: boolean;\n /** Callback when collapse state changes */\n onCollapsedChange?: (collapsed: boolean) => void;\n /** Whether to show collapse toggle button */\n showToggle?: boolean;\n /** Custom className for the layout container */\n className?: string;\n /** Custom className for the nav sidebar */\n navClassName?: string;\n /** Custom className for the content area */\n contentClassName?: string;\n /** Width of the sidebar when expanded */\n navWidth?: string;\n /** Width of the sidebar when collapsed */\n navWidthCollapsed?: string;\n /** Hide navigation on mobile (slides in as overlay when toggled) */\n hideOnMobile?: boolean;\n /** Auto-collapse to icon-only mode on mobile screens */\n autoCollapseOnMobile?: boolean;\n /** Embedded demo mode (no fixed positioning, constrained height) */\n embedded?: boolean;\n /** Ref for the main content scrollable element */\n mainContentRef?: React.RefObject<HTMLDivElement>;\n /** Optional header text for the navigation sidebar */\n header?: string;\n};\n\n/**\n * LeftNavLayout - Full-featured responsive left navigation layout\n *\n * Provides a robust sidebar navigation with:\n * - Full and collapsed (icon-only) states\n * - Smooth transitions without janky spacing\n * - Proper scroll handling\n * - Responsive mobile behavior\n * - Accessible keyboard navigation\n *\n * @example\n * ```tsx\n * <LeftNavLayout\n * collapsed={isCollapsed}\n * onCollapsedChange={setIsCollapsed}\n * nav={\n * <>\n * <LeftNavSection>\n * <LeftNavItem icon={<HomeIcon />} href=\"/\" active>Home</LeftNavItem>\n * <LeftNavItem icon={<DashboardIcon />} href=\"/dashboard\">Dashboard</LeftNavItem>\n * </LeftNavSection>\n * </>\n * }\n * >\n * <main>Your content here</main>\n * </LeftNavLayout>\n * ```\n */\nfunction LeftNavLayout({\n nav,\n children,\n collapsed: controlledCollapsed,\n onCollapsedChange,\n showToggle = true,\n className = '',\n navClassName = '',\n contentClassName = '',\n navWidth = '16rem',\n navWidthCollapsed = '4.5rem',\n hideOnMobile = false,\n autoCollapseOnMobile = true,\n embedded = false,\n header = 'Navigation',\n mainContentRef\n}: Readonly<LeftNavLayoutProps>) {\n const STORAGE_KEY_DESKTOP = 'left-nav-desktop-collapsed';\n const STORAGE_KEY_MOBILE = 'left-nav-mobile-collapsed';\n\n // Initialize desktop collapsed state from localStorage (defaults to false = expanded)\n const [internalCollapsed, setInternalCollapsed] = useState(() => {\n if (typeof window === 'undefined') return false;\n try {\n const saved = localStorage.getItem(STORAGE_KEY_DESKTOP);\n return saved !== null ? saved === 'true' : false;\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('Failed to read desktop collapsed state from localStorage:', error);\n return false;\n }\n });\n\n const [mobileMenuOpen, setMobileMenuOpen] = useState(!hideOnMobile);\n const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024);\n\n // Initialize mobile collapsed state from localStorage (defaults to true = collapsed)\n const [mobileCollapsedOverride, setMobileCollapsedOverride] = useState<boolean | null>(() => {\n if (typeof window === 'undefined') return null;\n try {\n const saved = localStorage.getItem(STORAGE_KEY_MOBILE);\n // If user has never toggled, return null to use default (collapsed)\n // If they have toggled, use their saved preference\n return saved !== null ? saved === 'true' : null;\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('Failed to read mobile collapsed state from localStorage:', error);\n return null;\n }\n });\n\n const desktopCollapsedRef = useRef<boolean | undefined>(undefined);\n\n const navRef = useRef<HTMLDivElement | null>(null);\n const scrollPosRef = useRef<number>(0);\n const internalContentRef = useRef<HTMLElement | null>(null);\n const contentRef = mainContentRef || internalContentRef;\n\n // Detect mobile screen size\n // Use useLayoutEffect to run synchronously before paint, preventing layout shift\n useLayoutEffect(() => {\n const checkMobile = () => {\n const newIsMobile = window.innerWidth < 1024; // lg breakpoint\n if (newIsMobile !== isMobile) {\n const wasDesktop = !isMobile;\n const isNowDesktop = !newIsMobile;\n\n setIsMobile(newIsMobile);\n\n // When switching from desktop to mobile, save the desktop state\n if (wasDesktop && autoCollapseOnMobile) {\n const currentCollapsed = controlledCollapsed ?? internalCollapsed;\n desktopCollapsedRef.current = currentCollapsed;\n }\n\n // When switching from mobile to desktop, restore the desktop state\n if (isNowDesktop && autoCollapseOnMobile && desktopCollapsedRef.current !== undefined) {\n if (controlledCollapsed === undefined) {\n // Only restore if using uncontrolled state\n setInternalCollapsed(desktopCollapsedRef.current);\n }\n }\n\n // When switching to mobile, restore mobile state from localStorage\n if (!isNowDesktop && autoCollapseOnMobile) {\n try {\n const savedMobile = localStorage.getItem(STORAGE_KEY_MOBILE);\n setMobileCollapsedOverride(savedMobile !== null ? savedMobile === 'true' : null);\n } catch {\n setMobileCollapsedOverride(null);\n }\n } else if (isNowDesktop) {\n // When switching to desktop, clear mobile override (not needed on desktop)\n setMobileCollapsedOverride(null);\n }\n }\n };\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n return () => window.removeEventListener('resize', checkMobile);\n }, [isMobile, autoCollapseOnMobile, controlledCollapsed, internalCollapsed]);\n\n // Use controlled or uncontrolled collapsed state\n const collapsed = controlledCollapsed ?? internalCollapsed;\n const setCollapsed = (value: boolean) => {\n if (onCollapsedChange) {\n onCollapsedChange(value);\n } else {\n setInternalCollapsed(value);\n // Persist desktop state to localStorage\n try {\n localStorage.setItem(STORAGE_KEY_DESKTOP, String(value));\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('Failed to save desktop collapsed state to localStorage:', error);\n }\n }\n };\n\n // Determine effective collapsed state\n // On mobile with autoCollapseOnMobile: use override if set, otherwise default to collapsed\n // On desktop or without autoCollapseOnMobile: use normal collapsed state\n const effectiveCollapsed =\n autoCollapseOnMobile && isMobile ? (mobileCollapsedOverride !== null ? mobileCollapsedOverride : true) : collapsed;\n\n const toggleCollapsed = () => setCollapsed(!collapsed);\n const closeMobileMenu = () => setMobileMenuOpen(false);\n\n // On mobile with hideOnMobile, toggle closes the overlay; on desktop, it collapses\n // On mobile with autoCollapseOnMobile, toggle the mobile override state\n const handleToggleClick = () => {\n if (hideOnMobile && isMobile) {\n // Hide on mobile mode - close the overlay\n closeMobileMenu();\n } else if (autoCollapseOnMobile && isMobile) {\n // Auto-collapse mode - toggle the mobile override\n const newMobileState = !effectiveCollapsed;\n setMobileCollapsedOverride(newMobileState);\n // Persist mobile state to localStorage\n try {\n localStorage.setItem(STORAGE_KEY_MOBILE, String(newMobileState));\n } catch {\n // eslint-disable-next-line no-console\n console.warn('Failed to save mobile collapsed state to localStorage:');\n }\n } else {\n // Desktop or normal mode - toggle regular collapsed state\n toggleCollapsed();\n }\n };\n\n // Restore nav scroll position after children change (route switch) & reset scroll\n useEffect(() => {\n if (navRef.current) {\n navRef.current.scrollTop = scrollPosRef.current;\n }\n }, [children]);\n\n // Robust scroll reset for main content area on children change\n // Pass the ref object (not .current) to avoid reading ref during render\n useScrollReset([children], contentRef as React.RefObject<HTMLElement | null>);\n\n const containerClasses = embedded\n ? 'flex bg-background border border-border rounded-lg overflow-hidden'\n : 'flex h-[calc(100vh-3.5rem)] md:h-[calc(100vh-4rem)] bg-background';\n\n return (\n <div className={`${containerClasses} ${className}`}>\n {/* Mobile Backdrop Overlay - Only shows on mobile when menu is open */}\n {!embedded && hideOnMobile && mobileMenuOpen && (\n <div\n className=\"fixed inset-0 bg-black/50 z-30 lg:hidden transition-opacity duration-300\"\n onClick={closeMobileMenu}\n aria-hidden=\"true\"\n />\n )}\n\n {/* Sidebar Navigation */}\n <aside\n className={`${embedded ? 'relative flex flex-col h-full' : hideOnMobile ? 'fixed lg:relative top-14 md:top-16 lg:top-0 left-0 z-40 lg:z-10 h-[calc(100vh-3.5rem)] md:h-[calc(100vh-4rem)] lg:h-full' : 'relative h-full'} flex flex-col shrink-0 bg-background border-r border-border transition-all duration-300 ease-in-out ${!embedded && hideOnMobile && !mobileMenuOpen ? '-translate-x-full lg:translate-x-0' : 'translate-x-0'} ${effectiveCollapsed ? 'w-18' : 'w-64'} ${navClassName}`}\n style={\n (!effectiveCollapsed && navWidth !== '16rem') || (effectiveCollapsed && navWidthCollapsed !== '4.5rem')\n ? ({\n width: effectiveCollapsed ? navWidthCollapsed : navWidth\n } as React.CSSProperties)\n : undefined\n }\n aria-label=\"Main navigation\"\n >\n {/* Nav Header with Toggle */}\n\n {/* Nav Content - Scrollable */}\n <nav\n ref={navRef}\n className=\"flex-1 overflow-y-auto overflow-x-hidden scrollbar-thin\"\n data-collapsed={effectiveCollapsed}\n onScroll={(e) => {\n scrollPosRef.current = (e.currentTarget as HTMLDivElement).scrollTop;\n }}\n >\n {showToggle && (\n <>\n <div className=\"py-2\">\n <LeftNavItem\n icon={effectiveCollapsed ? 'layout-sidebar-left-expand' : 'layout-sidebar-left-collapse'}\n iconSize=\"md\"\n onClick={handleToggleClick}\n disableActiveState\n title={effectiveCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}\n >\n {header}\n </LeftNavItem>\n\n {/* Divider - Subtle separator between sections */}\n <div className=\"h-px bg-border mx-3 mt-1.5\" aria-hidden=\"true\" />\n </div>\n </>\n )}\n {nav}\n </nav>\n </aside>\n\n {/* Main Content Area */}\n <main\n ref={contentRef as React.RefObject<HTMLDivElement>}\n className={`flex-1 overflow-y-auto ${embedded ? 'h-60' : ''} ${contentClassName}`}\n >\n {children}\n </main>\n </div>\n );\n}\n\nLeftNavLayout.displayName = 'LeftNavLayout';\n\nexport default LeftNavLayout;\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-section.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-section.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAA+B,MAAM,OAAO,CAAC;AAG/D,MAAM,MAAM,mBAAmB,GAAG;IAChC,yDAAyD;IACzD,QAAQ,EAAE,SAAS,CAAC;IACpB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sFAAsF;IACtF,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,iBAAS,cAAc,CAAC,EACtB,QAAQ,EACR,KAAK,EACL,SAAc,EACd,WAAmB,EACnB,gBAAwB,EACxB,EAAE,EACH,EAAE,QAAQ,CAAC,mBAAmB,CAAC,
|
|
1
|
+
{"version":3,"file":"left-nav-section.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-section.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAA+B,MAAM,OAAO,CAAC;AAG/D,MAAM,MAAM,mBAAmB,GAAG;IAChC,yDAAyD;IACzD,QAAQ,EAAE,SAAS,CAAC;IACpB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sFAAsF;IACtF,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,iBAAS,cAAc,CAAC,EACtB,QAAQ,EACR,KAAK,EACL,SAAc,EACd,WAAmB,EACnB,gBAAwB,EACxB,EAAE,EACH,EAAE,QAAQ,CAAC,mBAAmB,CAAC,2CAqI/B;kBA5IQ,cAAc;;;AAgJvB,eAAe,cAAc,CAAC"}
|