@hyddenlabs/hydn-ui 0.3.16 → 0.3.18

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.
Files changed (24) hide show
  1. package/dist/components/feedback/error-page/error-400.js +1 -1
  2. package/dist/components/feedback/error-page/error-401.js +1 -1
  3. package/dist/components/feedback/error-page/error-403.js +1 -1
  4. package/dist/components/feedback/error-page/error-404.js +1 -1
  5. package/dist/components/forms/button/button-with-icon.js +1 -1
  6. package/dist/components/forms/button/icon-button.js +1 -1
  7. package/dist/components/layout/left-nav-layout/left-nav-item.d.ts +6 -3
  8. package/dist/components/layout/left-nav-layout/left-nav-item.d.ts.map +1 -1
  9. package/dist/components/layout/left-nav-layout/left-nav-item.js +38 -17
  10. package/dist/components/layout/left-nav-layout/left-nav-item.js.map +1 -1
  11. package/dist/components/layout/left-nav-layout/left-nav-layout.d.ts +27 -21
  12. package/dist/components/layout/left-nav-layout/left-nav-layout.d.ts.map +1 -1
  13. package/dist/components/layout/left-nav-layout/left-nav-layout.js +140 -94
  14. package/dist/components/layout/left-nav-layout/left-nav-layout.js.map +1 -1
  15. package/dist/components/layout/left-nav-layout/left-nav-section.d.ts.map +1 -1
  16. package/dist/components/layout/left-nav-layout/left-nav-section.js +10 -10
  17. package/dist/components/layout/left-nav-layout/left-nav-section.js.map +1 -1
  18. package/dist/components/navigation/dropdown/dropdown.js +1 -1
  19. package/dist/components/navigation/navbar/navbar.js +2 -2
  20. package/dist/components/navigation/navbar/navbar.js.map +1 -1
  21. package/dist/style.css +1 -1
  22. package/dist/theme/tokens.js +2 -2
  23. package/dist/theme/tokens.js.map +1 -1
  24. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Icon } from "../../system/icon/icon.js";
3
2
  import Button from "../../forms/button/button.js";
4
3
  import Text from "../../typography/text/text.js";
5
4
  import Heading from "../../typography/heading/heading.js";
6
5
  import Stack from "../../layout/stack/stack.js";
6
+ import { Icon } from "../../system/icon/icon.js";
7
7
  function Error400({
8
8
  title = "Bad Request",
9
9
  description = "We couldn't process your request. Please check your input and try again.",
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Icon } from "../../system/icon/icon.js";
3
2
  import Button from "../../forms/button/button.js";
4
3
  import Text from "../../typography/text/text.js";
5
4
  import Heading from "../../typography/heading/heading.js";
6
5
  import Stack from "../../layout/stack/stack.js";
6
+ import { Icon } from "../../system/icon/icon.js";
7
7
  function Error401({
8
8
  title = "Unauthorized",
9
9
  description = "You need to sign in to access this page. Please log in with your credentials.",
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Icon } from "../../system/icon/icon.js";
3
2
  import Button from "../../forms/button/button.js";
4
3
  import Text from "../../typography/text/text.js";
5
4
  import Heading from "../../typography/heading/heading.js";
6
5
  import Stack from "../../layout/stack/stack.js";
6
+ import { Icon } from "../../system/icon/icon.js";
7
7
  function Error403({
8
8
  title = "Access Forbidden",
9
9
  description = "You don't have permission to access this resource. If you believe this is a mistake, please contact support.",
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Icon } from "../../system/icon/icon.js";
3
2
  import Button from "../../forms/button/button.js";
4
3
  import Text from "../../typography/text/text.js";
5
4
  import Heading from "../../typography/heading/heading.js";
6
5
  import Stack from "../../layout/stack/stack.js";
6
+ import { Icon } from "../../system/icon/icon.js";
7
7
  function Error404({
8
8
  title = "Page Not Found",
9
9
  description = "The page you're looking for doesn't exist or has been moved. Please check the URL or return to the homepage.",
@@ -1,7 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Icon } from "../../system/icon/icon.js";
4
3
  import Button from "./button.js";
4
+ import { Icon } from "../../system/icon/icon.js";
5
5
  const ButtonWithIcon = React.forwardRef(
6
6
  ({
7
7
  icon,
@@ -1,7 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useCallback, useEffect } from "react";
3
- import { Icon } from "../../system/icon/icon.js";
4
3
  import Button from "./button.js";
4
+ import { Icon } from "../../system/icon/icon.js";
5
5
  const IconButton = React.forwardRef(
6
6
  ({
7
7
  icon,
@@ -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
- /** Link label text */
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,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
+ {"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 block w-full py-4 transition-colors ${!disableActiveState && (isActive || active) ? "bg-[#0C0E14] text-[#FBFBFF]" : "text-[#FBFBFF]/80 hover:text-[#FBFBFF] hover:bg-[#2e3148]"} ${className}`;
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
- !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
- ] })
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
- return element;
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 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;"}
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
- /** 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;
31
+ /** Optional header text for the navigation sidebar */
32
+ header?: string;
27
33
  };
28
34
  /**
29
- * LeftNavLayout - Fixed-width left navigation sidebar layout
35
+ * LeftNavLayout - Full-featured responsive left navigation layout
30
36
  *
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
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
- * logo={<img src="/logo.png" alt="Logo" className="h-8" />}
47
+ * collapsed={isCollapsed}
48
+ * onCollapsedChange={setIsCollapsed}
42
49
  * nav={
43
50
  * <>
44
- * <LeftNavItem icon="home" href="/">Home</LeftNavItem>
45
- * <LeftNavItem icon="dashboard" href="/dashboard">Dashboard</LeftNavItem>
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, hideOnMobile, embedded, mainContentRef, logo, bottomContent, topbar }: Readonly<LeftNavLayoutProps>): import("react/jsx-runtime").JSX.Element;
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,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
+ {"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 { Icon } from "../../system/icon/icon.js";
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 = "w-26",
14
+ navWidth = "16rem",
15
+ navWidthCollapsed = "4.5rem",
12
16
  hideOnMobile = false,
17
+ autoCollapseOnMobile = true,
13
18
  embedded = false,
14
- mainContentRef,
15
- logo,
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 [showScrollDown, setShowScrollDown] = useState(false);
21
- const [showScrollUp, setShowScrollUp] = useState(false);
22
- const closeMobileMenu = () => setMobileMenuOpen(false);
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 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";
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__ */ jsxs(
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-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}`,
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
- 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
- ]
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__ */ 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
- ] })
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";