@marcoschwartz/lite-ui 0.25.2 → 0.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,8 +102,10 @@ export default function Page() {
102
102
  - **DateTimePicker** - Combined date and time picker
103
103
 
104
104
  ### Layout & Navigation
105
+ - **AppShell** - Complete app layout with header, navbar, aside, and footer
105
106
  - **Navbar** - Responsive navigation bar
106
107
  - **Sidebar** - Collapsible sidebar with mobile support
108
+ - **SidebarNav** - Collapsible navigation with icon-only mode and tooltips
107
109
  - **Drawer** - Slide-out drawer panel
108
110
  - **Modal** - Accessible modal dialog
109
111
  - **Tabs** - Tabbed navigation component
@@ -313,6 +315,77 @@ export function ConfirmDialog() {
313
315
  }
314
316
  ```
315
317
 
318
+ ### Collapsible Sidebar Navigation
319
+
320
+ ```tsx
321
+ "use client";
322
+
323
+ import { useState } from 'react';
324
+ import {
325
+ AppShell,
326
+ SidebarNav,
327
+ SidebarNavItem,
328
+ SidebarNavSection,
329
+ SidebarNavDivider,
330
+ SidebarNavCollapseToggle,
331
+ HomeIcon,
332
+ UserIcon,
333
+ SettingsIcon,
334
+ } from '@marcoschwartz/lite-ui';
335
+
336
+ export function DashboardLayout({ children }) {
337
+ const [activeItem, setActiveItem] = useState('dashboard');
338
+ const [navbarWidth, setNavbarWidth] = useState<number | undefined>();
339
+
340
+ return (
341
+ <AppShell
342
+ title="My App"
343
+ navbar={{
344
+ content: (
345
+ <SidebarNav
346
+ collapsible
347
+ onCollapseChange={(_, width) => setNavbarWidth(width)}
348
+ >
349
+ <SidebarNavSection grow>
350
+ <SidebarNavItem
351
+ icon={<HomeIcon size="sm" />}
352
+ label="Dashboard"
353
+ active={activeItem === 'dashboard'}
354
+ onClick={() => setActiveItem('dashboard')}
355
+ />
356
+ <SidebarNavItem
357
+ icon={<UserIcon size="sm" />}
358
+ label="Users"
359
+ active={activeItem === 'users'}
360
+ onClick={() => setActiveItem('users')}
361
+ />
362
+ <SidebarNavItem
363
+ icon={<SettingsIcon size="sm" />}
364
+ label="Settings"
365
+ active={activeItem === 'settings'}
366
+ onClick={() => setActiveItem('settings')}
367
+ />
368
+ </SidebarNavSection>
369
+ <SidebarNavDivider />
370
+ <SidebarNavCollapseToggle />
371
+ </SidebarNav>
372
+ ),
373
+ width: navbarWidth, // Dynamic width from SidebarNav
374
+ }}
375
+ >
376
+ {children}
377
+ </AppShell>
378
+ );
379
+ }
380
+ ```
381
+
382
+ **SidebarNav Features:**
383
+ - **Collapsible mode** - Toggle between full and icon-only view
384
+ - **Tooltips** - Show labels on hover when collapsed
385
+ - **Persistent state** - Collapse state saved to localStorage
386
+ - **Smooth transitions** - Animated width changes
387
+ - **Mantine-compatible API** - Use `leftSection` as alias for `icon`
388
+
316
389
  ## 🛠️ Development
317
390
 
318
391
  ### Running the Demo
package/dist/index.d.mts CHANGED
@@ -68,9 +68,13 @@ interface SidebarNavProps {
68
68
  collapsible?: boolean;
69
69
  defaultCollapsed?: boolean;
70
70
  collapsed?: boolean;
71
- onCollapseChange?: (collapsed: boolean) => void;
71
+ onCollapseChange?: (collapsed: boolean, width: number) => void;
72
72
  storageKey?: string;
73
73
  className?: string;
74
+ /** Width in pixels when expanded (default: 256) */
75
+ expandedWidth?: number;
76
+ /** Width in pixels when collapsed (default: 64) */
77
+ collapsedWidth?: number;
74
78
  }
75
79
  interface SidebarNavItemProps {
76
80
  /** Icon element (alias: leftSection) */
@@ -155,7 +159,7 @@ interface AppShellHeaderConfig {
155
159
  }
156
160
  interface AppShellNavbarConfig {
157
161
  content: React.ReactNode;
158
- width?: 'sm' | 'md' | 'lg';
162
+ width?: 'sm' | 'md' | 'lg' | number;
159
163
  breakpoint?: 'sm' | 'md' | 'lg' | 'xl';
160
164
  variant?: 'solid' | 'glass' | 'transparent';
161
165
  collapsed?: {
@@ -166,7 +170,7 @@ interface AppShellNavbarConfig {
166
170
  }
167
171
  interface AppShellAsideConfig {
168
172
  content: React.ReactNode;
169
- width?: 'sm' | 'md' | 'lg';
173
+ width?: 'sm' | 'md' | 'lg' | number;
170
174
  breakpoint?: 'sm' | 'md' | 'lg' | 'xl';
171
175
  variant?: 'solid' | 'glass' | 'transparent';
172
176
  collapsed?: {
package/dist/index.d.ts CHANGED
@@ -68,9 +68,13 @@ interface SidebarNavProps {
68
68
  collapsible?: boolean;
69
69
  defaultCollapsed?: boolean;
70
70
  collapsed?: boolean;
71
- onCollapseChange?: (collapsed: boolean) => void;
71
+ onCollapseChange?: (collapsed: boolean, width: number) => void;
72
72
  storageKey?: string;
73
73
  className?: string;
74
+ /** Width in pixels when expanded (default: 256) */
75
+ expandedWidth?: number;
76
+ /** Width in pixels when collapsed (default: 64) */
77
+ collapsedWidth?: number;
74
78
  }
75
79
  interface SidebarNavItemProps {
76
80
  /** Icon element (alias: leftSection) */
@@ -155,7 +159,7 @@ interface AppShellHeaderConfig {
155
159
  }
156
160
  interface AppShellNavbarConfig {
157
161
  content: React.ReactNode;
158
- width?: 'sm' | 'md' | 'lg';
162
+ width?: 'sm' | 'md' | 'lg' | number;
159
163
  breakpoint?: 'sm' | 'md' | 'lg' | 'xl';
160
164
  variant?: 'solid' | 'glass' | 'transparent';
161
165
  collapsed?: {
@@ -166,7 +170,7 @@ interface AppShellNavbarConfig {
166
170
  }
167
171
  interface AppShellAsideConfig {
168
172
  content: React.ReactNode;
169
- width?: 'sm' | 'md' | 'lg';
173
+ width?: 'sm' | 'md' | 'lg' | number;
170
174
  breakpoint?: 'sm' | 'md' | 'lg' | 'xl';
171
175
  variant?: 'solid' | 'glass' | 'transparent';
172
176
  collapsed?: {
package/dist/index.js CHANGED
@@ -779,33 +779,44 @@ var SidebarNav = ({
779
779
  collapsed: controlledCollapsed,
780
780
  onCollapseChange,
781
781
  storageKey = "sidebar-nav-collapsed",
782
- className = ""
782
+ className = "",
783
+ expandedWidth = 256,
784
+ collapsedWidth = 64
783
785
  }) => {
784
786
  const [internalCollapsed, setInternalCollapsed] = (0, import_react4.useState)(defaultCollapsed);
785
787
  const [mounted, setMounted] = (0, import_react4.useState)(false);
786
788
  const isControlled = controlledCollapsed !== void 0;
787
789
  const collapsed = isControlled ? controlledCollapsed : internalCollapsed;
790
+ const currentWidth = collapsible && collapsed ? collapsedWidth : expandedWidth;
788
791
  (0, import_react4.useEffect)(() => {
789
792
  setMounted(true);
790
793
  if (!isControlled && collapsible && typeof window !== "undefined") {
791
794
  const stored = localStorage.getItem(storageKey);
792
795
  if (stored !== null) {
793
- setInternalCollapsed(stored === "true");
796
+ const isCollapsed = stored === "true";
797
+ setInternalCollapsed(isCollapsed);
798
+ onCollapseChange?.(isCollapsed, isCollapsed ? collapsedWidth : expandedWidth);
794
799
  }
795
800
  }
796
801
  }, [isControlled, collapsible, storageKey]);
802
+ (0, import_react4.useEffect)(() => {
803
+ if (mounted && collapsible) {
804
+ onCollapseChange?.(collapsed, currentWidth);
805
+ }
806
+ }, [mounted]);
797
807
  const setCollapsed = (0, import_react4.useCallback)((value) => {
798
808
  if (!collapsible) return;
809
+ const newWidth = value ? collapsedWidth : expandedWidth;
799
810
  if (isControlled) {
800
- onCollapseChange?.(value);
811
+ onCollapseChange?.(value, newWidth);
801
812
  } else {
802
813
  setInternalCollapsed(value);
803
814
  if (typeof window !== "undefined") {
804
815
  localStorage.setItem(storageKey, String(value));
805
816
  }
817
+ onCollapseChange?.(value, newWidth);
806
818
  }
807
- onCollapseChange?.(value);
808
- }, [collapsible, isControlled, onCollapseChange, storageKey]);
819
+ }, [collapsible, isControlled, onCollapseChange, storageKey, collapsedWidth, expandedWidth]);
809
820
  const toggleCollapsed = (0, import_react4.useCallback)(() => {
810
821
  setCollapsed(!collapsed);
811
822
  }, [collapsed, setCollapsed]);
@@ -814,16 +825,15 @@ var SidebarNav = ({
814
825
  setCollapsed,
815
826
  toggleCollapsed
816
827
  };
817
- const widthClass = collapsible && collapsed ? "w-16" : "w-64";
818
828
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SidebarNavContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
819
829
  "nav",
820
830
  {
821
831
  className: `
822
- flex flex-col h-full
823
- ${widthClass}
832
+ flex flex-col h-full overflow-x-hidden overflow-y-auto
824
833
  transition-[width] duration-200 ease-in-out
825
834
  ${className}
826
835
  `,
836
+ style: { width: currentWidth },
827
837
  "data-collapsed": collapsed,
828
838
  children
829
839
  }
@@ -834,7 +844,7 @@ var SidebarNavSection = ({
834
844
  grow = false,
835
845
  className = ""
836
846
  }) => {
837
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `${grow ? "flex-1 overflow-y-auto" : ""} ${className}`, children });
847
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `p-3 space-y-1 ${grow ? "flex-1 overflow-y-auto overflow-x-hidden" : ""} ${className}`, children });
838
848
  };
839
849
  var SidebarNavItem = ({
840
850
  icon,
@@ -859,8 +869,8 @@ var SidebarNavItem = ({
859
869
  flex items-center gap-3 w-full px-3 py-2.5 rounded-lg
860
870
  text-sm font-medium
861
871
  transition-colors duration-150
862
- ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:bg-[hsl(var(--accent))]"}
863
- ${active ? "bg-[hsl(var(--accent))] text-[hsl(var(--accent-foreground))]" : "text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"}
872
+ ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
873
+ ${active ? "bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]" : "text-[hsl(var(--muted-foreground))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]"}
864
874
  ${collapsed ? "justify-center px-0" : ""}
865
875
  ${className}
866
876
  `;
@@ -1648,6 +1658,14 @@ var widthClasses2 = {
1648
1658
  lg: "w-80"
1649
1659
  // 320px
1650
1660
  };
1661
+ var getWidthClass = (width) => {
1662
+ if (typeof width === "number") return "";
1663
+ return widthClasses2[width || "md"];
1664
+ };
1665
+ var getWidthStyle = (width) => {
1666
+ if (typeof width === "number") return { width, transition: "width 200ms ease-in-out" };
1667
+ return {};
1668
+ };
1651
1669
  var paddingClasses = {
1652
1670
  none: "p-0",
1653
1671
  sm: "p-2",
@@ -1790,10 +1808,11 @@ var AppShell = ({
1790
1808
  navbar && !navbarCollapsedDesktop && /* @__PURE__ */ (0, import_jsx_runtime79.jsxs)(
1791
1809
  "aside",
1792
1810
  {
1793
- className: `${navbarDesktopShowClass} ${widthClasses2[navbarWidth]} ${getVariantClasses(navbarVariant, navbarWithBorder)} ${navbarWithBorder ? "border-r" : ""} flex-col shrink-0`,
1811
+ className: `${navbarDesktopShowClass} ${getWidthClass(navbarWidth)} ${getVariantClasses(navbarVariant, navbarWithBorder)} ${navbarWithBorder ? "border-r" : ""} flex-col shrink-0`,
1812
+ style: getWidthStyle(navbarWidth),
1794
1813
  children: [
1795
1814
  (logo || title) && /* @__PURE__ */ (0, import_jsx_runtime79.jsx)("div", { className: `${heightClasses[headerHeight]} px-4 flex items-center border-b border-[hsl(var(--border))] shrink-0`, children: logo || /* @__PURE__ */ (0, import_jsx_runtime79.jsx)("span", { className: "text-xl font-bold text-[hsl(var(--foreground))]", children: title }) }),
1796
- /* @__PURE__ */ (0, import_jsx_runtime79.jsx)("nav", { className: "flex-1 overflow-y-auto", children: navbar.content })
1815
+ /* @__PURE__ */ (0, import_jsx_runtime79.jsx)("nav", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: navbar.content })
1797
1816
  ]
1798
1817
  }
1799
1818
  ),
@@ -1837,7 +1856,8 @@ var AppShell = ({
1837
1856
  aside && !asideCollapsedDesktop && /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(
1838
1857
  "aside",
1839
1858
  {
1840
- className: `${asideDesktopShowClass} ${widthClasses2[asideWidth]} ${getVariantClasses(asideVariant, asideWithBorder)} ${asideWithBorder ? "border-l" : ""} flex-col shrink-0 overflow-y-auto`,
1859
+ className: `${asideDesktopShowClass} ${getWidthClass(asideWidth)} ${getVariantClasses(asideVariant, asideWithBorder)} ${asideWithBorder ? "border-l" : ""} flex-col shrink-0 overflow-y-auto overflow-x-hidden`,
1860
+ style: getWidthStyle(asideWidth),
1841
1861
  children: aside.content
1842
1862
  }
1843
1863
  ),
@@ -1919,7 +1939,8 @@ var AppShell = ({
1919
1939
  navbar && !navbarCollapsedDesktop && /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(
1920
1940
  "nav",
1921
1941
  {
1922
- className: `${navbarDesktopShowClass} ${widthClasses2[navbarWidth]} ${getVariantClasses(navbarVariant, navbarWithBorder)} ${navbarWithBorder ? "border-r" : ""} flex-col shrink-0 overflow-y-auto`,
1942
+ className: `${navbarDesktopShowClass} ${getWidthClass(navbarWidth)} ${getVariantClasses(navbarVariant, navbarWithBorder)} ${navbarWithBorder ? "border-r" : ""} flex-col shrink-0 overflow-y-auto overflow-x-hidden`,
1943
+ style: getWidthStyle(navbarWidth),
1923
1944
  children: navbar.content
1924
1945
  }
1925
1946
  ),
@@ -1927,7 +1948,8 @@ var AppShell = ({
1927
1948
  aside && !asideCollapsedDesktop && /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(
1928
1949
  "aside",
1929
1950
  {
1930
- className: `${asideDesktopShowClass} ${widthClasses2[asideWidth]} ${getVariantClasses(asideVariant, asideWithBorder)} ${asideWithBorder ? "border-l" : ""} flex-col shrink-0 overflow-y-auto`,
1951
+ className: `${asideDesktopShowClass} ${getWidthClass(asideWidth)} ${getVariantClasses(asideVariant, asideWithBorder)} ${asideWithBorder ? "border-l" : ""} flex-col shrink-0 overflow-y-auto overflow-x-hidden`,
1952
+ style: getWidthStyle(asideWidth),
1931
1953
  children: aside.content
1932
1954
  }
1933
1955
  )