@flikk/ui 1.0.0-beta.13 → 1.0.0-beta.14

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 (47) hide show
  1. package/dist/components/ai/PromptInput/PromptInput.js +1 -1
  2. package/dist/components/ai/PromptInput/VoiceRecorder.js +1 -1
  3. package/dist/components/ai/PromptSuggestions/PromptSuggestion.theme.js +1 -1
  4. package/dist/components/charts/FunnelChart/FunnelChart.js +2 -2
  5. package/dist/components/core/Badge/Badge.js +3 -3
  6. package/dist/components/core/Badge/Badge.theme.js +5 -5
  7. package/dist/components/core/Badge/Badge.types.d.ts +1 -1
  8. package/dist/components/core/Button/Button.ripple.js +1 -1
  9. package/dist/components/core/Button/Button.theme.d.ts +1 -1
  10. package/dist/components/core/Button/Button.theme.js +3 -3
  11. package/dist/components/core/Button/Button.types.d.ts +1 -1
  12. package/dist/components/core/Calendar/CalendarMini/CalendarMini.js +2 -2
  13. package/dist/components/core/Drawer/DrawerHeader.js +1 -1
  14. package/dist/components/core/Link/Link.js +1 -1
  15. package/dist/components/core/Modal/ModalHeader.js +1 -1
  16. package/dist/components/core/NavItem/NavItem.js +100 -52
  17. package/dist/components/core/NavItem/NavItem.theme.js +6 -3
  18. package/dist/components/core/NavItem/NavItem.types.d.ts +44 -30
  19. package/dist/components/core/NavItem/index.d.ts +1 -1
  20. package/dist/components/core/Pill/Pill.theme.js +1 -1
  21. package/dist/components/core/Sidebar/Sidebar.d.ts +11 -1
  22. package/dist/components/core/Sidebar/Sidebar.js +49 -16
  23. package/dist/components/core/Sidebar/Sidebar.theme.js +9 -7
  24. package/dist/components/core/Sidebar/Sidebar.types.d.ts +57 -0
  25. package/dist/components/core/Sidebar/SidebarContent.js +3 -1
  26. package/dist/components/core/Sidebar/SidebarContext.d.ts +14 -0
  27. package/dist/components/core/Sidebar/SidebarContext.js +63 -11
  28. package/dist/components/core/Sidebar/SidebarFooter.js +3 -1
  29. package/dist/components/core/Sidebar/SidebarHeader.js +6 -2
  30. package/dist/components/core/Sidebar/SidebarMobileTrigger.d.ts +3 -0
  31. package/dist/components/core/Sidebar/SidebarMobileTrigger.js +28 -0
  32. package/dist/components/core/Sidebar/SidebarNav.js +3 -2
  33. package/dist/components/core/Sidebar/SidebarNavGroup.js +4 -2
  34. package/dist/components/core/Sidebar/SidebarToggle.js +1 -1
  35. package/dist/components/core/Sidebar/SidebarUserProfile.d.ts +3 -0
  36. package/dist/components/core/Sidebar/SidebarUserProfile.js +35 -0
  37. package/dist/components/core/Sidebar/index.d.ts +3 -1
  38. package/dist/components/core/Tabs/Tabs.theme.d.ts +1 -1
  39. package/dist/components/core/Tabs/Tabs.theme.js +3 -3
  40. package/dist/components/core/index.js +2 -0
  41. package/dist/components/data-display/GanttChart/GanttToolbar.js +1 -1
  42. package/dist/components/forms/RichTextEditor/RichTextEditor.js +10 -10
  43. package/dist/index.js +2 -0
  44. package/dist/styles.css +1 -1
  45. package/package.json +1 -1
  46. package/src/global.scss +1 -0
  47. package/src/styles/theme.css +48 -48
@@ -512,7 +512,7 @@ className, textareaClassName, footerClassName, theme = {},
512
512
  return (jsx(Button, { iconOnly: true, className: cn(mergedTheme.submitButtonStyle, extraClassName), size: buttonSize, variant: buttonVariant, color: buttonColor, onClick: handleSubmit, disabled: !canSubmit, state: isLoading ? "loading" : "default", "aria-label": submitButtonLabel, title: submitShortcut ? `Submit (${submitShortcut})` : undefined, showReflection: buttonVariant === "filled" ? false : undefined, children: jsx(AnimatePresence, { mode: "wait", initial: false, children: jsx(motion.span, { variants: iconSwapVariants, initial: "initial", animate: "animate", exit: "exit", className: "flex items-center justify-center", children: submitButtonIcon }, "submit") }) }));
513
513
  };
514
514
  // ========== Render ==========
515
- return (jsxs("div", { className: cn("min-w-xl mx-auto", className), ...props, children: [jsx("input", { ref: fileInputRef, type: "file", accept: acceptedFileTypes.join(","), multiple: true, onChange: (e) => {
515
+ return (jsxs("div", { className: cn("w-full mx-auto", className), ...props, children: [jsx("input", { ref: fileInputRef, type: "file", accept: acceptedFileTypes.join(","), multiple: true, onChange: (e) => {
516
516
  if (e.target.files)
517
517
  processFiles(e.target.files);
518
518
  e.target.value = "";
@@ -170,7 +170,7 @@ const VoiceRecorder = ({ onComplete, onCancel, maxDuration, className, }) => {
170
170
  setRecordingState("recording");
171
171
  }
172
172
  }, [recordingState, clearTimer, startTimer]);
173
- return (jsxs("div", { className: cn("flex items-center justify-between w-full", className), children: [jsx(Button, { variant: "ghost", color: "neutral", size: "sm", className: "rounded-full", onClick: handleCancel, "aria-label": "Cancel recording", children: "Cancel" }), jsxs("div", { className: "flex items-center gap-2.5", children: [jsx(motion.div, { className: "size-2.5 rounded-full bg-[var(--color-danger)]", animate: recordingState === "recording"
173
+ return (jsxs("div", { className: cn("flex items-center justify-between w-full", className), children: [jsx(Button, { variant: "soft", color: "neutral", size: "sm", className: "rounded-full", onClick: handleCancel, "aria-label": "Cancel recording", children: "Cancel" }), jsxs("div", { className: "flex items-center gap-2.5", children: [jsx(motion.div, { className: "size-2.5 rounded-full bg-[var(--color-danger)]", animate: recordingState === "recording"
174
174
  ? { opacity: [1, 0.3, 1] }
175
175
  : { opacity: 0.5 }, transition: recordingState === "recording"
176
176
  ? { duration: 1.2, repeat: Infinity, ease: "easeInOut" }
@@ -1,5 +1,5 @@
1
1
  const promptSuggestionTheme = {
2
- baseStyle: "inline-flex items-center gap-2 py-2 px-3.5 rounded-full " +
2
+ baseStyle: "inline-flex items-center gap-2 py-1 px-3 rounded-full " +
3
3
  "bg-white dark:bg-[var(--color-background)] " +
4
4
  "border border-[var(--color-border)] " +
5
5
  "text-base font-medium text-[var(--color-text-primary)] " +
@@ -430,7 +430,7 @@ const FunnelChart = ({ data, height = 400, width = 600, showPercentages = true,
430
430
  animate: "visible",
431
431
  custom: index,
432
432
  }
433
- : {}), children: jsxs(Badge, { color: isGoodConversion ? "success" : "warning", variant: "ghost", size: "sm", pill: true, children: ["\u2193 ", stage.conversionRate.toFixed(1), "%"] }) }, `conversion-${stage.name}`));
433
+ : {}), children: jsxs(Badge, { color: isGoodConversion ? "success" : "warning", variant: "soft", size: "sm", pill: true, children: ["\u2193 ", stage.conversionRate.toFixed(1), "%"] }) }, `conversion-${stage.name}`));
434
434
  }), orientation === "vertical" &&
435
435
  showConversionRate &&
436
436
  stages.map((stage, index) => {
@@ -448,7 +448,7 @@ const FunnelChart = ({ data, height = 400, width = 600, showPercentages = true,
448
448
  animate: "visible",
449
449
  custom: index,
450
450
  }
451
- : {}), children: jsxs(Badge, { color: isGoodConversion ? "success" : "warning", variant: "ghost", size: "sm", pill: true, children: [stage.conversionRate.toFixed(0), "%"] }) }, `conversion-${stage.name}`));
451
+ : {}), children: jsxs(Badge, { color: isGoodConversion ? "success" : "warning", variant: "soft", size: "sm", pill: true, children: [stage.conversionRate.toFixed(0), "%"] }) }, `conversion-${stage.name}`));
452
452
  }), showSummary && summaryPosition !== "none" && stages.length > 1 && (jsxs(motion.div, { className: cn("absolute bg-[var(--color-background)] rounded-lg shadow-sm border border-[var(--color-border)] px-3 py-2 pointer-events-none", getSummaryPositionClasses(summaryPosition)), ...(animate
453
453
  ? {
454
454
  variants: summaryVariants,
@@ -90,9 +90,9 @@ const Badge = React__default.memo(React__default.forwardRef(({ children, variant
90
90
  ...(_c = badgeTheme.variantColors) === null || _c === void 0 ? void 0 : _c.outline,
91
91
  ...(_d = customTheme.variantColors) === null || _d === void 0 ? void 0 : _d.outline,
92
92
  },
93
- ghost: {
94
- ...(_e = badgeTheme.variantColors) === null || _e === void 0 ? void 0 : _e.ghost,
95
- ...(_f = customTheme.variantColors) === null || _f === void 0 ? void 0 : _f.ghost,
93
+ soft: {
94
+ ...(_e = badgeTheme.variantColors) === null || _e === void 0 ? void 0 : _e.soft,
95
+ ...(_f = customTheme.variantColors) === null || _f === void 0 ? void 0 : _f.soft,
96
96
  },
97
97
  },
98
98
  sizes: {
@@ -11,7 +11,7 @@ const badgeTheme = {
11
11
  variants: {
12
12
  filled: "",
13
13
  outline: "bg-transparent border",
14
- ghost: "ring-transparent border-0",
14
+ soft: "ring-transparent border-0",
15
15
  },
16
16
  // Color styles organized PER VARIANT
17
17
  // This structure enables reliable dark: variants and className overrides
@@ -45,8 +45,8 @@ const badgeTheme = {
45
45
  danger: "text-[var(--color-danger)] border-[var(--color-border)] " +
46
46
  "dark:text-[var(--color-danger-400)]",
47
47
  },
48
- // Ghost variant - subtle background tints
49
- ghost: {
48
+ // Soft variant - subtle background tints
49
+ soft: {
50
50
  neutral: "bg-[var(--color-neutral-200)] text-[var(--color-text-primary)] " +
51
51
  "dark:bg-[var(--color-neutral-900)] ",
52
52
  primary: "bg-[var(--color-primary-100)] text-[var(--color-primary-700)] " +
@@ -63,8 +63,8 @@ const badgeTheme = {
63
63
  },
64
64
  // Flat size structure (no nested objects)
65
65
  sizes: {
66
- xs: "text-xs px-1 py-0 gap-1 font-medium",
67
- sm: "text-sm px-1.5 py-0 gap-1 font-medium",
66
+ xs: "text-xs px-1 py-0 gap-1 font-semibold",
67
+ sm: "text-sm px-1.5 py-0 gap-1 font-semibold",
68
68
  md: "text-sm px-2 py-0.25 gap-1 font-semibold",
69
69
  },
70
70
  // Border radius per size (using CSS variable)
@@ -1,6 +1,6 @@
1
1
  import React, { ReactNode, MouseEvent } from "react";
2
2
  import { SemanticColor } from "../../../utils/colorUtils";
3
- export type BadgeVariant = "filled" | "outline" | "ghost";
3
+ export type BadgeVariant = "filled" | "outline" | "soft";
4
4
  export type BadgeColor = SemanticColor;
5
5
  export type BadgeSize = "xs" | "sm" | "md";
6
6
  /**
@@ -120,7 +120,7 @@ function getRippleColorClass(variant, color) {
120
120
  if (variant === "filled") {
121
121
  return "bg-white/30";
122
122
  }
123
- // For outline/ghost buttons, use color-matched ripple
123
+ // For outline/soft buttons, use color-matched ripple
124
124
  switch (color) {
125
125
  case "primary":
126
126
  return "bg-[var(--color-primary)]/20";
@@ -9,7 +9,7 @@ export interface ButtonTheme {
9
9
  variantColors: {
10
10
  filled: Record<ButtonColor, string>;
11
11
  outline: Record<ButtonColor, string>;
12
- ghost: Record<ButtonColor, string>;
12
+ soft: Record<ButtonColor, string>;
13
13
  link: Record<ButtonColor, string>;
14
14
  };
15
15
  sizes: Record<ButtonSize, {
@@ -8,7 +8,7 @@ const buttonTheme = {
8
8
  variantStyles: {
9
9
  filled: "border hover:brightness-[0.9] dark:hover:brightness-[1.15]",
10
10
  outline: "bg-transparent border",
11
- ghost: "border-transparent",
11
+ soft: "border-transparent",
12
12
  link: "bg-transparent border-transparent",
13
13
  },
14
14
  // Color styles organized BY VARIANT for reliable dark: prefixes
@@ -66,8 +66,8 @@ const buttonTheme = {
66
66
  "dark:text-[var(--color-danger-300)] dark:border-[var(--color-danger-400)] " +
67
67
  "dark:hover:bg-[var(--color-danger-900)]/30",
68
68
  },
69
- // Ghost variant - subtle background tints
70
- ghost: {
69
+ // Soft variant - subtle background tints
70
+ soft: {
71
71
  neutral: "text-[var(--color-text-primary)] bg-[var(--color-neutral-100)] " +
72
72
  "hover:bg-[var(--color-neutral-200)] " +
73
73
  "focus-visible:outline-[var(--color-text-secondary)] " +
@@ -51,6 +51,6 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
51
51
  darkMode?: boolean;
52
52
  }
53
53
  export type ButtonColor = SemanticColor;
54
- export type ButtonVariant = "filled" | "outline" | "ghost" | "link";
54
+ export type ButtonVariant = "filled" | "outline" | "soft" | "link";
55
55
  export type ButtonSize = "sm" | "md" | "lg";
56
56
  export type ButtonState = "default" | "disabled" | "error" | "loading";
@@ -195,7 +195,7 @@ const CalendarMini = React__default.forwardRef(({ selectedDate, currentMonth, on
195
195
  };
196
196
  const MonthRow = enableAnimations ? motion.div : "div";
197
197
  const DateItem = enableAnimations ? motion.button : "button";
198
- return (jsxs("div", { ref: ref, className: cn(mergedTheme.baseStyle, className), role: "region", "aria-label": "Mini calendar", ...rest, children: [jsx(Button, { iconOnly: true, color: "neutral", variant: "ghost", className: "!flex-shrink !flex-grow-0 !w-fit self-stretch !h-auto !min-h-0 px-0.5", onClick: handlePrevMonth, "aria-label": "Previous month", children: jsx(ChevronLeftIcon, { className: "size-5 text-[var(--color-text-primary)]" }) }), jsxs("div", { className: "flex flex-col gap-1 relative", children: [jsxs("div", { className: "flex items-center gap-1", children: [jsx("div", { className: "flex flex-col relative overflow-hidden flex-1", style: {
198
+ return (jsxs("div", { ref: ref, className: cn(mergedTheme.baseStyle, className), role: "region", "aria-label": "Mini calendar", ...rest, children: [jsx(Button, { iconOnly: true, color: "neutral", variant: "soft", className: "!flex-shrink !flex-grow-0 !w-fit self-stretch !h-auto !min-h-0 px-0.5", onClick: handlePrevMonth, "aria-label": "Previous month", children: jsx(ChevronLeftIcon, { className: "size-5 text-[var(--color-text-primary)]" }) }), jsxs("div", { className: "flex flex-col gap-1 relative", children: [jsxs("div", { className: "flex items-center gap-1", children: [jsx("div", { className: "flex flex-col relative overflow-hidden flex-1", style: {
199
199
  maskImage: "linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)",
200
200
  WebkitMaskImage: "linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)",
201
201
  }, children: jsx("div", { ref: monthScrollRef, className: cn(mergedTheme.monthRowStyle), role: "tablist", "aria-label": "Month selection", children: jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: direction, children: jsx(MonthRow, { custom: direction, ...(enableAnimations && {
@@ -243,7 +243,7 @@ const CalendarMini = React__default.forwardRef(({ selectedDate, currentMonth, on
243
243
  }), "aria-selected": isSelected, "aria-current": isToday ? "date" : undefined, "aria-disabled": disabled, type: "button", ...(enableAnimations && {
244
244
  whileTap: !disabled ? { scale: 0.95 } : undefined,
245
245
  }), children: [jsx("div", { className: "text-base font-medium opacity-60", children: formatDayName(date) }), jsx("time", { dateTime: date.toISOString(), className: cn("font-semibold"), children: formatDateNumber(date) })] }, date.toISOString()));
246
- }) }, `${startDate.getTime()}-dates`) }) }) })] }), jsx(Button, { iconOnly: true, color: "neutral", variant: "ghost", className: "!flex-shrink !flex-grow-0 !w-fit self-stretch !h-auto !min-h-0 px-0.5", onClick: handleNextMonth, "aria-label": "Next month", children: jsx(ChevronRightIcon, { className: "size-5 text-[var(--color-text-primary)]" }) })] }));
246
+ }) }, `${startDate.getTime()}-dates`) }) }) })] }), jsx(Button, { iconOnly: true, color: "neutral", variant: "soft", className: "!flex-shrink !flex-grow-0 !w-fit self-stretch !h-auto !min-h-0 px-0.5", onClick: handleNextMonth, "aria-label": "Next month", children: jsx(ChevronRightIcon, { className: "size-5 text-[var(--color-text-primary)]" }) })] }));
247
247
  });
248
248
  CalendarMini.displayName = "CalendarMini";
249
249
 
@@ -8,7 +8,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
8
8
  const DrawerHeader = ({ children, icon, showCloseButton, className, ...props }) => {
9
9
  const { onClose, showCloseButton: contextShowCloseButton, theme, } = useDrawerContext();
10
10
  const shouldShowCloseButton = showCloseButton !== null && showCloseButton !== void 0 ? showCloseButton : contextShowCloseButton;
11
- return (jsxs("div", { className: cn(theme.header, className), ...props, children: [jsxs("div", { className: cn(theme.headerContent), children: [icon && (jsx("div", { className: "flex-shrink-0 text-[var(--color-text-secondary)]", children: icon })), jsx("div", { className: "flex-1 min-w-0", children: children })] }), shouldShowCloseButton && (jsx(Button, { color: "neutral", variant: "ghost", iconOnly: true, size: "sm", onClick: onClose, "aria-label": "Close drawer", className: "rounded-full shrink-0", children: jsx(XMarkIcon, { className: "size-5" }) }))] }));
11
+ return (jsxs("div", { className: cn(theme.header, className), ...props, children: [jsxs("div", { className: cn(theme.headerContent), children: [icon && (jsx("div", { className: "flex-shrink-0 text-[var(--color-text-secondary)]", children: icon })), jsx("div", { className: "flex-1 min-w-0", children: children })] }), shouldShowCloseButton && (jsx(Button, { color: "neutral", variant: "soft", iconOnly: true, size: "sm", onClick: onClose, "aria-label": "Close drawer", className: "rounded-full shrink-0", children: jsx(XMarkIcon, { className: "size-5" }) }))] }));
12
12
  };
13
13
  DrawerHeader.displayName = "Drawer.Header";
14
14
 
@@ -33,7 +33,7 @@ const Link = React__default.forwardRef(({ children, className = "", size = "inhe
33
33
  }, [shouldReduceMotion]);
34
34
  return (jsx(motion.a, { ref: ref, href: disabled ? undefined : href, className: cn(buttonTheme.baseStyle, themeClasses.colorClasses, themeClasses.variantClasses, themeClasses.sizeClasses, themeClasses.stateClasses,
35
35
  // Special case for link variant to have an underline
36
- "underline underline-offset-4 rounded-none", className), "data-color": color, "data-variant": resolvedVariant, "data-size": size, "aria-disabled": disabled || undefined, ...(disabled ? {} : animationProps), ...props, children: children }));
36
+ "underline underline-offset-4 rounded-none font-semibold", className), "data-color": color, "data-variant": resolvedVariant, "data-size": size, "aria-disabled": disabled || undefined, ...(disabled ? {} : animationProps), ...props, children: children }));
37
37
  });
38
38
  Link.displayName = "Link";
39
39
 
@@ -8,7 +8,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
8
8
  const ModalHeader = ({ children, icon, showCloseButton, className, ...props }) => {
9
9
  const { onClose, showCloseButton: contextShowCloseButton, theme, isBodyScrollable, } = useModalContext();
10
10
  const shouldShowCloseButton = showCloseButton !== null && showCloseButton !== void 0 ? showCloseButton : contextShowCloseButton;
11
- return (jsxs("div", { className: cn(theme.header, isBodyScrollable && "border-[var(--color-border)] relative", className), ...props, children: [jsxs("div", { className: cn(theme.headerContent), children: [icon && (jsx("div", { className: "flex-shrink-0 text-[var(--color-text-secondary)]", children: icon })), jsx("div", { className: "flex-1", children: children })] }), shouldShowCloseButton && (jsx("div", { className: "", children: jsx(Button, { color: "neutral", variant: "ghost", iconOnly: true, size: "sm", onClick: onClose, "aria-label": "Close modal", className: "rounded-full", children: jsx(XMarkIcon, { className: "size-5" }) }) }))] }));
11
+ return (jsxs("div", { className: cn(theme.header, isBodyScrollable && "border-[var(--color-border)] relative", className), ...props, children: [jsxs("div", { className: cn(theme.headerContent), children: [icon && (jsx("div", { className: "flex-shrink-0 text-[var(--color-text-secondary)]", children: icon })), jsx("div", { className: "flex-1", children: children })] }), shouldShowCloseButton && (jsx("div", { className: "", children: jsx(Button, { color: "neutral", variant: "soft", iconOnly: true, size: "sm", onClick: onClose, "aria-label": "Close modal", className: "rounded-full", children: jsx(XMarkIcon, { className: "size-5" }) }) }))] }));
12
12
  };
13
13
  ModalHeader.displayName = "Modal.Header";
14
14
 
@@ -1,53 +1,85 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
2
- import React__default, { useMemo } from 'react';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import React__default, { useId, useState, useCallback, useMemo } from 'react';
3
3
  import { cn } from '../../../utils/cn.js';
4
4
  import { navItemTheme } from './NavItem.theme.js';
5
- import { useSidebarContext } from '../Sidebar/SidebarContext.js';
5
+ import { useSidebarContext, useAccordionNavContext } from '../Sidebar/SidebarContext.js';
6
6
  import { Tooltip } from '../Tooltip/Tooltip.js';
7
7
  import '../Tooltip/Tooltip.animations.js';
8
8
  import { Badge } from '../Badge/Badge.js';
9
9
  import { ChevronDownIcon } from '@heroicons/react/20/solid';
10
+ import { AnimatePresence, motion } from 'motion/react';
10
11
 
11
12
  const NavItem = React__default.forwardRef(({ children, startContent: propStartContent, endContent: propEndContent, icon, // Legacy
12
13
  badge, // Legacy
13
- description, shortcut, href, onClick, active = false, disabled = false, isDisabled: propIsDisabled, isDanger = false, hasSubmenu = false, expanded = false, onToggleExpanded, level = 0, showTooltipWhenCollapsed = true, className, theme: customTheme = {}, ...props }, ref) => {
14
+ notification, description, shortcut, href, onClick, active = false, disabled = false, isDisabled: propIsDisabled, isDanger = false, hasSubmenu: propHasSubmenu = false, expanded: controlledExpanded, onToggleExpanded, items, defaultExpanded = false, level = 0, showTooltipWhenCollapsed = true, className, theme: customTheme = {}, ...props }, ref) => {
14
15
  var _a, _b;
15
16
  const sidebarContext = useSidebarContext();
17
+ const accordionNav = useAccordionNavContext();
18
+ const stableId = useId();
16
19
  const collapsed = (sidebarContext === null || sidebarContext === void 0 ? void 0 : sidebarContext.collapsed) || false;
17
20
  const isInSidebar = !!sidebarContext;
21
+ const isMobile = (sidebarContext === null || sidebarContext === void 0 ? void 0 : sidebarContext.isMobile) || false;
22
+ // Self-managed submenu state
23
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
24
+ // Determine if this item has a submenu
25
+ const hasSubmenu = propHasSubmenu || (items && items.length > 0);
26
+ // Determine expanded state: controlled vs accordion vs internal
27
+ const isAccordionManaged = !!accordionNav;
28
+ const isControlled = controlledExpanded !== undefined;
29
+ let isExpanded;
30
+ if (isControlled) {
31
+ isExpanded = controlledExpanded;
32
+ }
33
+ else if (isAccordionManaged && hasSubmenu) {
34
+ isExpanded = accordionNav.openId === stableId;
35
+ }
36
+ else {
37
+ isExpanded = internalExpanded;
38
+ }
39
+ const handleToggleExpanded = useCallback(() => {
40
+ if (isControlled) {
41
+ onToggleExpanded === null || onToggleExpanded === void 0 ? void 0 : onToggleExpanded();
42
+ }
43
+ else if (isAccordionManaged) {
44
+ if (isExpanded) {
45
+ accordionNav.unregister(stableId);
46
+ }
47
+ else {
48
+ accordionNav.register(stableId);
49
+ }
50
+ }
51
+ else {
52
+ setInternalExpanded(prev => !prev);
53
+ }
54
+ }, [isControlled, isAccordionManaged, isExpanded, onToggleExpanded, accordionNav, stableId]);
18
55
  // Map legacy props
19
56
  const startContent = propStartContent || icon;
20
57
  const actualIsDisabled = propIsDisabled || disabled;
21
- // Construct end content
58
+ // Build end content
22
59
  let endContent = propEndContent;
23
60
  if (!endContent && badge) {
24
- endContent = (jsx(Badge, { color: "neutral", variant: "ghost", size: "sm", children: badge }));
25
- }
26
- if (!endContent && hasSubmenu) {
27
- endContent = (jsx(ChevronDownIcon, { className: cn("size-4 transition-transform duration-300", expanded && "rotate-180") }));
61
+ endContent = (jsx(Badge, { color: "neutral", variant: "soft", size: "sm", children: badge }));
28
62
  }
63
+ // Notification element
64
+ const notificationElement = useMemo(() => {
65
+ if (notification === undefined || notification === false)
66
+ return null;
67
+ if (notification === true) {
68
+ return (jsx("span", { className: cn(navItemTheme.notificationDot, customTheme.notificationDot) }));
69
+ }
70
+ if (typeof notification === "number" && notification > 0) {
71
+ return (jsx("span", { className: cn(navItemTheme.notificationBadge, customTheme.notificationBadge), children: notification > 99 ? "99+" : notification }));
72
+ }
73
+ return null;
74
+ }, [notification, customTheme.notificationDot, customTheme.notificationBadge]);
29
75
  const theme = useMemo(() => ({
30
76
  ...navItemTheme,
31
77
  ...customTheme,
32
- states: {
33
- ...navItemTheme.states,
34
- ...customTheme.states,
35
- },
36
- iconStates: {
37
- ...navItemTheme.iconStates,
38
- ...customTheme.iconStates,
39
- },
40
- levels: {
41
- ...navItemTheme.levels,
42
- ...customTheme.levels,
43
- },
44
- collapsed: {
45
- ...navItemTheme.collapsed,
46
- ...customTheme.collapsed,
47
- },
48
- danger: customTheme.danger ||
49
- navItemTheme.danger ||
50
- "text-[var(--color-danger)] hover:bg-[var(--color-danger-50)]",
78
+ states: { ...navItemTheme.states, ...customTheme.states },
79
+ iconStates: { ...navItemTheme.iconStates, ...customTheme.iconStates },
80
+ levels: { ...navItemTheme.levels, ...customTheme.levels },
81
+ collapsed: { ...navItemTheme.collapsed, ...customTheme.collapsed },
82
+ danger: customTheme.danger || navItemTheme.danger || "text-[var(--color-danger)] hover:bg-[var(--color-danger-50)]",
51
83
  }), [customTheme]);
52
84
  // Handle click events
53
85
  const handleClick = (e) => {
@@ -55,19 +87,27 @@ description, shortcut, href, onClick, active = false, disabled = false, isDisabl
55
87
  e.preventDefault();
56
88
  return;
57
89
  }
58
- if (hasSubmenu && onToggleExpanded) {
90
+ if (hasSubmenu) {
59
91
  e.preventDefault();
60
- onToggleExpanded();
92
+ handleToggleExpanded();
61
93
  return;
62
94
  }
63
95
  if (onClick) {
64
96
  e.preventDefault();
65
97
  onClick();
98
+ // Close mobile sidebar after clicking a nav item
99
+ if (isMobile && sidebarContext) {
100
+ sidebarContext.setMobileOpen(false);
101
+ }
66
102
  return;
67
103
  }
68
- // If href is provided, let the default navigation happen
104
+ // Close mobile sidebar after navigation
105
+ if (href && isMobile && sidebarContext) {
106
+ sidebarContext.setMobileOpen(false);
107
+ }
69
108
  };
70
109
  const isActive = active;
110
+ const isCollapsedView = isInSidebar && collapsed && !isMobile;
71
111
  // Get state classes
72
112
  const getStateClasses = () => {
73
113
  if (actualIsDisabled)
@@ -78,8 +118,10 @@ description, shortcut, href, onClick, active = false, disabled = false, isDisabl
78
118
  };
79
119
  // Danger overrides
80
120
  const dangerClasses = isDanger ? theme.danger : "";
121
+ // Active indicator
122
+ const activeIndicatorClasses = isActive && level === 0 ? theme.activeIndicator : "";
81
123
  // Build container classes
82
- const containerClasses = cn(theme.baseStyle, getStateClasses(), theme.levels[level], isInSidebar && collapsed && ((_a = theme.collapsed) === null || _a === void 0 ? void 0 : _a.container), dangerClasses, className);
124
+ const containerClasses = cn(theme.baseStyle, getStateClasses(), theme.levels[level], isCollapsedView && ((_a = theme.collapsed) === null || _a === void 0 ? void 0 : _a.container), dangerClasses, activeIndicatorClasses, className);
83
125
  // Get icon state classes
84
126
  const getIconStateClasses = () => {
85
127
  var _a, _b, _c;
@@ -90,35 +132,41 @@ description, shortcut, href, onClick, active = false, disabled = false, isDisabl
90
132
  return (_c = theme.iconStates) === null || _c === void 0 ? void 0 : _c.default;
91
133
  };
92
134
  // Build icon classes
93
- const iconClasses = cn(isInSidebar && collapsed ? (_b = theme.collapsed) === null || _b === void 0 ? void 0 : _b.icon : theme.icon, getIconStateClasses());
135
+ const iconClasses = cn(isCollapsedView ? (_b = theme.collapsed) === null || _b === void 0 ? void 0 : _b.icon : theme.icon, getIconStateClasses());
94
136
  const textClasses = cn(theme.text);
137
+ // Chevron for submenu
138
+ const chevron = hasSubmenu && !isCollapsedView ? (jsx(ChevronDownIcon, { className: cn("size-4 transition-transform duration-200 text-[var(--color-text-muted)]", isExpanded && "rotate-180") })) : null;
95
139
  // Content component
96
- const content = (jsxs("div", { ref: ref, className: containerClasses, role: href ? "link" : "button", tabIndex: actualIsDisabled ? -1 : 0, "aria-disabled": actualIsDisabled, "aria-expanded": hasSubmenu ? expanded : undefined, "data-active": isActive, "data-disabled": actualIsDisabled, "data-level": level, onClick: handleClick, onKeyDown: (e) => {
140
+ const content = (jsxs("div", { ref: ref, className: containerClasses, role: href ? "link" : "button", tabIndex: actualIsDisabled ? -1 : 0, "aria-disabled": actualIsDisabled, "aria-expanded": hasSubmenu ? isExpanded : undefined, "data-active": isActive, "data-disabled": actualIsDisabled, "data-level": level, onClick: handleClick, onKeyDown: (e) => {
97
141
  if (e.key === "Enter" || e.key === " ") {
98
142
  e.preventDefault();
99
143
  handleClick(e);
100
144
  }
101
- }, ...props, children: [startContent && (jsx("div", { className: cn("flex items-center justify-center flex-shrink-0"
102
- // Icon size is handled by icon classes
103
- ), children: React__default.isValidElement(startContent)
104
- ? React__default.cloneElement(startContent, {
105
- className: cn(iconClasses, startContent.props.className),
106
- })
107
- : startContent })), (!isInSidebar || !collapsed) && (jsxs("div", { className: "flex-1 min-w-0", children: [jsxs("div", { className: "flex items-center justify-between gap-2", children: [jsx("span", { className: cn("truncate", textClasses), children: children }), shortcut && (jsx("kbd", { className: "hidden group-hover:inline-block text-sm text-[var(--color-text-muted)] font-sans border border-[var(--color-border)] rounded px-1.5 py-0.5", children: shortcut }))] }), description && (jsx("div", { className: "text-sm text-[var(--color-text-muted)] mt-0.5 truncate", children: description }))] })), endContent && (!isInSidebar || !collapsed) && (jsx("div", { className: "flex items-center justify-center flex-shrink-0", children: endContent })), isInSidebar && collapsed && (jsx("span", { className: "sr-only", children: children }))] }));
145
+ }, ...props, children: [startContent && (jsxs("div", { className: "flex items-center justify-center flex-shrink-0 relative", children: [React__default.isValidElement(startContent)
146
+ ? React__default.cloneElement(startContent, {
147
+ className: cn(iconClasses, startContent.props.className),
148
+ })
149
+ : startContent, isCollapsedView && notificationElement && (jsx("span", { className: "absolute -top-1 -right-1", children: notificationElement }))] })), !isCollapsedView && (jsxs("div", { className: "flex-1 min-w-0", children: [jsxs("div", { className: "flex items-center justify-between gap-2", children: [jsx("span", { className: cn("truncate", textClasses), children: children }), shortcut && (jsx("kbd", { className: "hidden group-hover:inline-block text-sm text-[var(--color-text-muted)] font-sans border border-[var(--color-border)] rounded px-1.5 py-0.5", children: shortcut }))] }), description && (jsx("div", { className: "text-sm text-[var(--color-text-muted)] mt-0.5 truncate", children: description }))] })), !isCollapsedView && (jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [notificationElement, endContent, chevron] })), isCollapsedView && (jsx("span", { className: "sr-only", children: children }))] }));
150
+ // Self-contained submenu
151
+ const submenu = hasSubmenu && items && items.length > 0 && !isCollapsedView ? (jsx(AnimatePresence, { initial: false, children: isExpanded && (jsx(motion.div, { initial: "collapsed", animate: "open", exit: "collapsed", variants: {
152
+ open: { opacity: 1, height: "auto" },
153
+ collapsed: { opacity: 0, height: 0 },
154
+ }, transition: { duration: 0.2, ease: "easeInOut" }, className: "overflow-hidden", children: items.map((item, idx) => (jsx(NavItem, { level: Math.min((level + 1), 2), startContent: item.startContent, active: item.active, isDisabled: item.isDisabled, badge: item.badge, notification: item.notification, href: item.href, onClick: item.onClick, children: item.children }, idx))) })) })) : null;
108
155
  // Wrap with link if href is provided
109
- if (href && !actualIsDisabled) {
110
- const LinkContent = (jsx("a", { href: href, className: "block group", children: content }));
111
- // Show tooltip in collapsed sidebar mode
112
- if (isInSidebar && collapsed && showTooltipWhenCollapsed) {
113
- return (jsx(Tooltip, { content: children, placement: "right", children: LinkContent }));
156
+ const renderWrapped = (node) => {
157
+ if (href && !actualIsDisabled) {
158
+ return (jsx("a", { href: href, className: "block group", children: node }));
114
159
  }
115
- return LinkContent;
116
- }
160
+ return node;
161
+ };
117
162
  // Show tooltip in collapsed sidebar mode
118
- if (isInSidebar && collapsed && showTooltipWhenCollapsed) {
119
- return (jsx(Tooltip, { content: children, placement: "right", children: content }));
120
- }
121
- return content;
163
+ const renderWithTooltip = (node) => {
164
+ if (isCollapsedView && showTooltipWhenCollapsed) {
165
+ return (jsx(Tooltip, { content: children, placement: "right", children: node }));
166
+ }
167
+ return node;
168
+ };
169
+ return (jsxs(Fragment, { children: [renderWithTooltip(renderWrapped(content)), submenu] }));
122
170
  });
123
171
  NavItem.displayName = "NavItem";
124
172
 
@@ -1,12 +1,12 @@
1
1
  const navItemTheme = {
2
- baseStyle: "relative flex items-center gap-2 px-2 py-1.5 text-base font-medium cursor-pointer select-none transition-all duration-300 w-full rounded-[var(--nav-item-radius)] outline-none group",
2
+ baseStyle: "relative flex items-center gap-2 px-2 py-1.5 text-base font-medium cursor-pointer select-none transition-all duration-150 w-full rounded-[var(--nav-item-radius)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] group",
3
3
  icon: "size-4 text-[var(--color-text-secondary)]",
4
4
  text: "flex-1 min-w-0 truncate",
5
5
  states: {
6
- default: "text-[var(--color-text-primary)] hover:bg-neutral-100",
6
+ default: "text-[var(--color-text-primary)] hover:bg-[var(--color-background-secondary)]",
7
7
  active: "bg-[var(--color-primary)]/10 text-[var(--color-primary)] font-semibold",
8
8
  disabled: "opacity-60 cursor-not-allowed pointer-events-none text-[var(--color-text-placeholder)]",
9
- hover: "hover:bg-neutral-100",
9
+ hover: "hover:bg-[var(--color-background-secondary)]",
10
10
  },
11
11
  iconStates: {
12
12
  default: "text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]",
@@ -22,6 +22,9 @@ const navItemTheme = {
22
22
  container: "!gap-0 justify-center p-2 aspect-square",
23
23
  icon: "size-5",
24
24
  },
25
+ activeIndicator: "before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-5 before:w-[3px] before:rounded-full before:bg-[var(--color-primary)]",
26
+ notificationDot: "size-2 rounded-full bg-[var(--color-danger)]",
27
+ notificationBadge: "min-w-[18px] h-[18px] px-1 text-[10px] font-semibold rounded-full bg-[var(--color-danger)] text-[var(--color-danger-contrast)] flex items-center justify-center leading-none",
25
28
  };
26
29
 
27
30
  export { navItemTheme };
@@ -1,4 +1,22 @@
1
1
  import { ReactNode } from "react";
2
+ export interface NavItemSubmenuItem {
3
+ /** Label text */
4
+ children: ReactNode;
5
+ /** Link href */
6
+ href?: string;
7
+ /** Click handler */
8
+ onClick?: () => void;
9
+ /** Whether this sub-item is active */
10
+ active?: boolean;
11
+ /** Whether this sub-item is disabled */
12
+ isDisabled?: boolean;
13
+ /** Badge text/number on the sub-item */
14
+ badge?: string | number;
15
+ /** Notification count/dot on the sub-item */
16
+ notification?: number | boolean;
17
+ /** Start content (icon) for the sub-item */
18
+ startContent?: ReactNode;
19
+ }
2
20
  export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
3
21
  /**
4
22
  * Content of the navigation item
@@ -30,6 +48,10 @@ export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
30
48
  * @deprecated Use endContent instead
31
49
  */
32
50
  badge?: string | number;
51
+ /**
52
+ * Notification indicator. Pass a number for a count badge, or true for a dot.
53
+ */
54
+ notification?: number | boolean;
33
55
  /**
34
56
  * Link href (for navigation)
35
57
  */
@@ -58,19 +80,31 @@ export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
58
80
  */
59
81
  isDanger?: boolean;
60
82
  /**
61
- * Whether the item has a submenu
83
+ * Whether the item has a submenu (manual mode).
84
+ * If `items` is provided, this is set automatically.
62
85
  * @default false
63
86
  */
64
87
  hasSubmenu?: boolean;
65
88
  /**
66
- * Whether the submenu is expanded (for multi-level navigation)
89
+ * Whether the submenu is expanded (controlled mode for manual submenus).
67
90
  * @default false
68
91
  */
69
92
  expanded?: boolean;
70
93
  /**
71
- * Callback when submenu is toggled
94
+ * Callback when submenu is toggled (manual mode)
72
95
  */
73
96
  onToggleExpanded?: () => void;
97
+ /**
98
+ * Self-contained submenu items. When provided, NavItem manages its own
99
+ * expand/collapse state internally. Children items automatically get
100
+ * level={parentLevel + 1}.
101
+ */
102
+ items?: NavItemSubmenuItem[];
103
+ /**
104
+ * Whether the self-contained submenu is expanded by default
105
+ * @default false
106
+ */
107
+ defaultExpanded?: boolean;
74
108
  /**
75
109
  * Nesting level for indentation (0, 1, 2)
76
110
  * @default 0
@@ -111,6 +145,10 @@ export interface NavItemTheme {
111
145
  container: string;
112
146
  icon: string;
113
147
  };
148
+ activeIndicator?: string;
149
+ notificationDot?: string;
150
+ notificationBadge?: string;
151
+ danger?: string;
114
152
  }
115
153
  export interface NavItemThemeOverrides {
116
154
  baseStyle?: string;
@@ -137,32 +175,8 @@ export interface NavItemThemeOverrides {
137
175
  container: string;
138
176
  icon: string;
139
177
  }>;
140
- danger?: string;
141
- }
142
- export interface NavItemTheme {
143
- baseStyle: string;
144
- icon: string;
145
- text: string;
146
- states: {
147
- default: string;
148
- active: string;
149
- disabled: string;
150
- hover: string;
151
- };
152
- iconStates?: {
153
- default?: string;
154
- active?: string;
155
- disabled?: string;
156
- hover?: string;
157
- };
158
- levels: {
159
- 0: string;
160
- 1: string;
161
- 2: string;
162
- };
163
- collapsed: {
164
- container: string;
165
- icon: string;
166
- };
178
+ activeIndicator?: string;
179
+ notificationDot?: string;
180
+ notificationBadge?: string;
167
181
  danger?: string;
168
182
  }
@@ -1,2 +1,2 @@
1
1
  export { NavItem } from './NavItem';
2
- export type { NavItemProps, NavItemTheme, NavItemThemeOverrides } from './NavItem.types';
2
+ export type { NavItemProps, NavItemSubmenuItem, NavItemTheme, NavItemThemeOverrides } from './NavItem.types';
@@ -28,7 +28,7 @@ const pillTheme = {
28
28
  */
29
29
  sizes: {
30
30
  sm: "text-sm px-2 py-0.5 gap-0.5",
31
- md: "text-base px-2.5 py-1 gap-1",
31
+ md: "text-base px-2 py-0.5 gap-1",
32
32
  lg: "text-base px-3 py-1 gap-1.5",
33
33
  },
34
34
  /**
@@ -1,3 +1,13 @@
1
1
  import React from "react";
2
2
  import { SidebarProps } from "./Sidebar.types";
3
- export declare const Sidebar: React.ForwardRefExoticComponent<SidebarProps & React.RefAttributes<HTMLDivElement>>;
3
+ export declare const Sidebar: React.ForwardRefExoticComponent<SidebarProps & React.RefAttributes<HTMLDivElement>> & {
4
+ Header: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarHeaderProps & React.RefAttributes<HTMLDivElement>>;
5
+ Content: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarContentProps & React.RefAttributes<HTMLDivElement>>;
6
+ Footer: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarFooterProps & React.RefAttributes<HTMLDivElement>>;
7
+ Nav: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarNavProps & React.RefAttributes<HTMLDivElement>>;
8
+ NavGroup: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarNavGroupProps & React.RefAttributes<HTMLDivElement>>;
9
+ Submenu: ({ children, isOpen, className, }: import("./SidebarSubmenu").SidebarSubmenuProps) => import("react/jsx-runtime").JSX.Element;
10
+ Toggle: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarToggleProps & React.RefAttributes<HTMLButtonElement>>;
11
+ MobileTrigger: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarMobileTriggerProps & React.RefAttributes<HTMLButtonElement>>;
12
+ UserProfile: React.ForwardRefExoticComponent<import("./Sidebar.types").SidebarUserProfileProps & React.RefAttributes<HTMLDivElement>>;
13
+ };