@hyddenlabs/hydn-ui 0.3.15 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,8 @@
1
1
  import { ReactNode, MouseEvent } from 'react';
2
- import { Size } from '../../../theme/size-tokens';
3
2
  export type LeftNavItemProps = {
4
3
  /** Icon identifier string to display (uses Icon component) */
5
4
  icon?: string;
6
- /** Icon size from unified size system */
7
- iconSize?: Size;
8
- /** Link label text (visible when expanded, hidden when collapsed) */
5
+ /** Link label text */
9
6
  children: ReactNode;
10
7
  /** Whether this item is currently active (highlighted state) */
11
8
  active?: boolean;
@@ -15,7 +12,7 @@ export type LeftNavItemProps = {
15
12
  badge?: ReactNode;
16
13
  /** Destination URL for the navigation link (required for link mode) */
17
14
  href?: string;
18
- /** Accessible label override (used for tooltip in collapsed mode) */
15
+ /** Accessible label override */
19
16
  title?: string;
20
17
  /** Prevent actual navigation for demo/showcase mode (deprecated: use onClick instead) */
21
18
  preventNavigation?: boolean;
@@ -48,7 +45,7 @@ export type LeftNavItemProps = {
48
45
  * </LeftNavItem>
49
46
  * ```
50
47
  */
51
- declare function LeftNavItem({ icon, iconSize, children, active, className, badge, href, title, preventNavigation, onClick, disableActiveState }: Readonly<LeftNavItemProps>): import("react/jsx-runtime").JSX.Element | null;
48
+ declare function LeftNavItem({ icon, children, active, className, badge, href, title, preventNavigation, onClick, disableActiveState }: Readonly<LeftNavItemProps>): import("react/jsx-runtime").JSX.Element | null;
52
49
  declare namespace LeftNavItem {
53
50
  var displayName: string;
54
51
  }
@@ -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,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
+ {"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,EAAwC,MAAM,OAAO,CAAC;AAGpF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,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,gCAAgC;IAChC,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,QAAQ,EACR,MAAc,EACd,SAAc,EACd,KAAK,EACL,IAAI,EACJ,KAAK,EACL,iBAAyB,EACzB,OAAO,EACP,kBAA0B,EAC3B,EAAE,QAAQ,CAAC,gBAAgB,CAAC,kDAuF5B;kBAlGQ,WAAW;;;AAsGpB,eAAe,WAAW,CAAC"}
@@ -1,11 +1,9 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { Icon } from "../../system/icon/icon.js";
3
- import { useRef, useState, useEffect, isValidElement, cloneElement } from "react";
3
+ import { useRef, isValidElement, cloneElement } from "react";
4
4
  import { NavLink } from "react-router-dom";
5
- import Tooltip from "../../feedback/tooltip/tooltip.js";
6
5
  function LeftNavItem({
7
6
  icon,
8
- iconSize = "sm",
9
7
  children,
10
8
  active = false,
11
9
  className = "",
@@ -17,40 +15,25 @@ function LeftNavItem({
17
15
  disableActiveState = false
18
16
  }) {
19
17
  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
- }, []);
46
18
  const itemTitle = title || (typeof children === "string" ? children : void 0);
47
19
  const isButtonMode = !!onClick;
48
20
  const isLinkMode = !isButtonMode && !!href;
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}`;
21
+ const getBaseClasses = (isActive) => `group relative block w-full py-4 transition-colors ${!disableActiveState && (isActive || active) ? "bg-[#0C0E14] text-[#FBFBFF]" : "text-[#FBFBFF]/80 hover:text-[#FBFBFF] hover:bg-[#2e3148]"} ${className}`;
50
22
  const renderContent = (isActive) => /* @__PURE__ */ jsxs(Fragment, { children: [
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 })
23
+ !disableActiveState && (isActive || active) && /* @__PURE__ */ jsx(
24
+ "div",
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
+ ] })
54
37
  ] });
55
38
  const buttonElement = isButtonMode ? (
56
39
  // eslint-disable-next-line jsx-a11y/anchor-is-valid
@@ -89,11 +72,7 @@ function LeftNavItem({
89
72
  }
90
73
  ) : null;
91
74
  const element = isButtonMode ? buttonElement : navLink;
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 });
75
+ return element;
97
76
  }
98
77
  LeftNavItem.displayName = "LeftNavItem";
99
78
  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, 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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"}
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 block w-full py-4 transition-colors ${\n !disableActiveState && (isActive || active)\n ? 'bg-[#0C0E14] text-[#FBFBFF]'\n : 'text-[#FBFBFF]/80 hover:text-[#FBFBFF] hover:bg-[#2e3148]'\n } ${className}`;\n\n // Shared content renderer\n const renderContent = (isActive: boolean) => (\n <>\n {/* Gradient left-border active indicator */}\n {!disableActiveState && (isActive || active) && (\n <div\n className=\"absolute left-0 top-0 bottom-0 w-0.75\"\n style={{\n background: 'radial-gradient(70.52% 85.16% at 0% 53.3%, #B44F8C 28.85%, #3E4BA5 100%)'\n }}\n />\n )}\n <div className=\"flex flex-col items-center justify-center gap-1.5 px-3\">\n {icon && <Icon name={icon} size=\"md\" />}\n <span className=\"text-[10px] font-semibold uppercase tracking-wide leading-none text-center select-none\">\n {children}\n </span>\n {badge && (\n <span>\n {isValidElement(badge) && !disableActiveState && (isActive || active)\n ? cloneElement(badge, { inverted: true } as { inverted: boolean })\n : badge}\n </span>\n )}\n </div>\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 return element;\n}\n\nLeftNavItem.displayName = 'LeftNavItem';\n\nexport default LeftNavItem;\n"],"names":[],"mappings":";;;;AAmDA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA,qBAAqB;AACvB,GAA+B;AAC7B,QAAM,SAAS,OAA8C,IAAI;AAGjE,QAAM,YAAY,UAAU,OAAO,aAAa,WAAW,WAAW;AAGtE,QAAM,eAAe,CAAC,CAAC;AACvB,QAAM,aAAa,CAAC,gBAAgB,CAAC,CAAC;AAGtC,QAAM,iBAAiB,CAAC,aACtB,sDACE,CAAC,uBAAuB,YAAY,UAChC,gCACA,2DACN,IAAI,SAAS;AAGf,QAAM,gBAAgB,CAAC,aACrB,qBAAA,UAAA,EAEG,UAAA;AAAA,IAAA,CAAC,uBAAuB,YAAY,WACnC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO;AAAA,UACL,YAAY;AAAA,QAAA;AAAA,MACd;AAAA,IAAA;AAAA,IAGJ,qBAAC,OAAA,EAAI,WAAU,0DACZ,UAAA;AAAA,MAAA,QAAQ,oBAAC,MAAA,EAAK,MAAM,MAAM,MAAK,MAAK;AAAA,MACrC,oBAAC,QAAA,EAAK,WAAU,0FACb,SAAA,CACH;AAAA,MACC,SACC,oBAAC,QAAA,EACE,UAAA,eAAe,KAAK,KAAK,CAAC,uBAAuB,YAAY,UAC1D,aAAa,OAAO,EAAE,UAAU,KAAA,CAA+B,IAC/D,MAAA,CACN;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAIF,QAAM,gBAAgB;AAAA;AAAA,IAEpB;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS,CAAC,MAAM;AACd,YAAE,eAAA;AACF,oBAAU,CAA6C;AAAA,QACzD;AAAA,QACA,WAAW,eAAe,MAAM;AAAA,QAChC,gBAAc,CAAC,sBAAsB,SAAS,SAAS;AAAA,QACvD,cAAY;AAAA,QACZ,MAAK;AAAA,QAEJ,wBAAc,MAAM;AAAA,MAAA;AAAA,IAAA;AAAA,MAErB;AAGJ,QAAM,UAAU,aACd;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,IAAI;AAAA,MACJ,SAAS,CAAC,MAAqC;AAC7C,YAAI,mBAAmB;AACrB,YAAE,eAAA;AAAA,QACJ;AAAA,MACF;AAAA,MACA,WAAW,CAAC,EAAE,eAAe,eAAe,QAAQ;AAAA,MACpD,gBAAc,CAAC,sBAAsB,SAAS,SAAS;AAAA,MACvD,cAAY;AAAA,MACZ,KAAG;AAAA,MAEF,UAAA,CAAC,EAAE,SAAA,MAAe,cAAc,QAAQ;AAAA,IAAA;AAAA,EAAA,IAEzC;AAEJ,QAAM,UAAU,eAAe,gBAAgB;AAE/C,SAAO;AACT;AAEA,YAAY,cAAc;"}
@@ -4,62 +4,56 @@ 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;
13
7
  /** Custom className for the layout container */
14
8
  className?: string;
15
9
  /** Custom className for the nav sidebar */
16
10
  navClassName?: string;
17
11
  /** Custom className for the content area */
18
12
  contentClassName?: string;
19
- /** Width of the sidebar when expanded */
13
+ /** Width of the sidebar */
20
14
  navWidth?: string;
21
- /** Width of the sidebar when collapsed */
22
- navWidthCollapsed?: string;
23
15
  /** Hide navigation on mobile (slides in as overlay when toggled) */
24
16
  hideOnMobile?: boolean;
25
- /** Auto-collapse to icon-only mode on mobile screens */
26
- autoCollapseOnMobile?: boolean;
27
17
  /** Embedded demo mode (no fixed positioning, constrained height) */
28
18
  embedded?: boolean;
29
19
  /** Ref for the main content scrollable element */
30
20
  mainContentRef?: React.RefObject<HTMLDivElement>;
31
- /** Optional header text for the navigation sidebar */
32
- header?: string;
21
+ /** Logo or brand content rendered in the sidebar header */
22
+ logo?: ReactNode;
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;
33
27
  };
34
28
  /**
35
- * LeftNavLayout - Full-featured responsive left navigation layout
29
+ * LeftNavLayout - Fixed-width left navigation sidebar layout
36
30
  *
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
31
+ * Provides a sidebar navigation with:
32
+ * - Fixed width (always visible, no collapse)
33
+ * - Logo/brand header slot
34
+ * - Scrollable nav area
35
+ * - Pinned bottom content slot
36
+ * - Optional mobile overlay mode
43
37
  *
44
38
  * @example
45
39
  * ```tsx
46
40
  * <LeftNavLayout
47
- * collapsed={isCollapsed}
48
- * onCollapsedChange={setIsCollapsed}
41
+ * logo={<img src="/logo.png" alt="Logo" className="h-8" />}
49
42
  * nav={
50
43
  * <>
51
- * <LeftNavSection>
52
- * <LeftNavItem icon={<HomeIcon />} href="/" active>Home</LeftNavItem>
53
- * <LeftNavItem icon={<DashboardIcon />} href="/dashboard">Dashboard</LeftNavItem>
54
- * </LeftNavSection>
44
+ * <LeftNavItem icon="home" href="/">Home</LeftNavItem>
45
+ * <LeftNavItem icon="dashboard" href="/dashboard">Dashboard</LeftNavItem>
55
46
  * </>
56
47
  * }
48
+ * bottomContent={
49
+ * <LeftNavItem icon="logout" onClick={logout}>Logout</LeftNavItem>
50
+ * }
57
51
  * >
58
52
  * <main>Your content here</main>
59
53
  * </LeftNavLayout>
60
54
  * ```
61
55
  */
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;
56
+ declare function LeftNavLayout({ nav, children, className, navClassName, contentClassName, navWidth, hideOnMobile, embedded, mainContentRef, logo, bottomContent, topbar }: Readonly<LeftNavLayoutProps>): import("react/jsx-runtime").JSX.Element;
63
57
  declare namespace LeftNavLayout {
64
58
  var displayName: string;
65
59
  }
@@ -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,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
+ {"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,EAA+B,MAAM,OAAO,CAAC;AAI/D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iCAAiC;IACjC,GAAG,EAAE,SAAS,CAAC;IACf,wBAAwB;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IACjD,2DAA2D;IAC3D,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,iFAAiF;IACjF,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,qFAAqF;IACrF,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,iBAAS,aAAa,CAAC,EACrB,GAAG,EACH,QAAQ,EACR,SAAc,EACd,YAAiB,EACjB,gBAAqB,EACrB,QAAiB,EACjB,YAAoB,EACpB,QAAgB,EAChB,cAAc,EACd,IAAI,EACJ,aAAa,EACb,MAAM,EACP,EAAE,QAAQ,CAAC,kBAAkB,CAAC,2CAsJ9B;kBAnKQ,aAAa;;;AAuKtB,eAAe,aAAa,CAAC"}
@@ -1,123 +1,63 @@
1
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import { useState, useRef, useLayoutEffect, useEffect } from "react";
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect } from "react";
3
3
  import useScrollReset from "../../../hooks/useScrollReset.js";
4
- import LeftNavItem from "./left-nav-item.js";
4
+ import { Icon } from "../../system/icon/icon.js";
5
5
  function LeftNavLayout({
6
6
  nav,
7
7
  children,
8
- collapsed: controlledCollapsed,
9
- onCollapsedChange,
10
- showToggle = true,
11
8
  className = "",
12
9
  navClassName = "",
13
10
  contentClassName = "",
14
- navWidth = "16rem",
15
- navWidthCollapsed = "4.5rem",
11
+ navWidth = "w-26",
16
12
  hideOnMobile = false,
17
- autoCollapseOnMobile = true,
18
13
  embedded = false,
19
- header = "Navigation",
20
- mainContentRef
14
+ mainContentRef,
15
+ logo,
16
+ bottomContent,
17
+ topbar
21
18
  }) {
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
- });
34
19
  const [mobileMenuOpen, setMobileMenuOpen] = useState(!hideOnMobile);
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);
20
+ const [showScrollDown, setShowScrollDown] = useState(false);
21
+ const [showScrollUp, setShowScrollUp] = useState(false);
22
+ const closeMobileMenu = () => setMobileMenuOpen(false);
47
23
  const navRef = useRef(null);
48
24
  const scrollPosRef = useRef(0);
49
25
  const internalContentRef = useRef(null);
50
26
  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
- };
114
27
  useEffect(() => {
115
28
  if (navRef.current) {
116
29
  navRef.current.scrollTop = scrollPosRef.current;
117
30
  }
118
31
  }, [children]);
119
32
  useScrollReset([children], contentRef);
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";
33
+ const updateScrollDirection = () => {
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";
121
61
  return /* @__PURE__ */ jsxs("div", { className: `${containerClasses} ${className}`, children: [
122
62
  !embedded && hideOnMobile && mobileMenuOpen && /* @__PURE__ */ jsx(
123
63
  "div",
@@ -127,52 +67,66 @@ function LeftNavLayout({
127
67
  "aria-hidden": "true"
128
68
  }
129
69
  ),
130
- /* @__PURE__ */ jsx(
70
+ /* @__PURE__ */ jsxs(
131
71
  "aside",
132
72
  {
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,
73
+ className: `${embedded ? "relative flex flex-col h-full" : hideOnMobile ? "fixed lg:relative top-0 left-0 z-40 lg:z-10 h-screen lg:h-full" : "relative h-full"} flex flex-col shrink-0 bg-[#24273D] ${!embedded && hideOnMobile && !mobileMenuOpen ? "-translate-x-full lg:translate-x-0" : "translate-x-0"} ${navWidth} ${navClassName}`,
137
74
  "aria-label": "Main navigation",
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
- )
75
+ children: [
76
+ logo && /* @__PURE__ */ jsx("div", { className: "h-16 bg-[#151823] flex items-center justify-center border-b border-[#2e3148] shadow-sm shrink-0 px-4", children: logo }),
77
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-h-0 flex flex-col", children: [
78
+ /* @__PURE__ */ jsx(
79
+ "nav",
80
+ {
81
+ ref: navRef,
82
+ className: "flex-1 overflow-y-auto divide-y divide-muted-foreground scrollbar-dark",
83
+ onScroll: (e) => {
84
+ scrollPosRef.current = e.currentTarget.scrollTop;
85
+ updateScrollDirection();
86
+ },
87
+ children: nav
88
+ }
89
+ ),
90
+ showScrollDown && /* @__PURE__ */ jsxs(
91
+ "button",
92
+ {
93
+ onClick: scrollDown,
94
+ className: "group absolute bottom-0 left-0 right-0 h-16 flex items-end justify-center pb-2 cursor-pointer z-10",
95
+ "aria-label": "Scroll down",
96
+ children: [
97
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-linear-to-t from-[#24273D] via-[#24273D]/90 to-transparent" }),
98
+ /* @__PURE__ */ jsx("div", { className: "relative transition-transform duration-150 group-hover:translate-y-0.5 pr-2.5", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-down", size: "md", color: "neutral" }) })
99
+ ]
100
+ }
101
+ ),
102
+ showScrollUp && /* @__PURE__ */ jsxs(
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
+ ]
166
117
  }
167
118
  ),
168
- /* @__PURE__ */ jsx(
169
- "main",
170
- {
171
- ref: contentRef,
172
- className: `flex-1 overflow-y-auto ${embedded ? "h-60" : ""} ${contentClassName}`,
173
- children
174
- }
175
- )
119
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
120
+ topbar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: topbar }),
121
+ /* @__PURE__ */ jsx(
122
+ "main",
123
+ {
124
+ ref: contentRef,
125
+ className: `flex-1 overflow-y-auto ${embedded ? "h-60" : ""} ${contentClassName}`,
126
+ children
127
+ }
128
+ )
129
+ ] })
176
130
  ] });
177
131
  }
178
132
  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, 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
+ {"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 +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,2CAqI/B;kBA5IQ,cAAc;;;AAgJvB,eAAe,cAAc,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,2CAmI/B;kBA1IQ,cAAc;;;AA8IvB,eAAe,cAAc,CAAC"}