@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.
- package/dist/components/ai/PromptInput/PromptInput.js +1 -1
- package/dist/components/ai/PromptInput/VoiceRecorder.js +1 -1
- package/dist/components/ai/PromptSuggestions/PromptSuggestion.theme.js +1 -1
- package/dist/components/charts/FunnelChart/FunnelChart.js +2 -2
- package/dist/components/core/Badge/Badge.js +3 -3
- package/dist/components/core/Badge/Badge.theme.js +5 -5
- package/dist/components/core/Badge/Badge.types.d.ts +1 -1
- package/dist/components/core/Button/Button.ripple.js +1 -1
- package/dist/components/core/Button/Button.theme.d.ts +1 -1
- package/dist/components/core/Button/Button.theme.js +3 -3
- package/dist/components/core/Button/Button.types.d.ts +1 -1
- package/dist/components/core/Calendar/CalendarMini/CalendarMini.js +2 -2
- package/dist/components/core/Drawer/DrawerHeader.js +1 -1
- package/dist/components/core/Link/Link.js +1 -1
- package/dist/components/core/Modal/ModalHeader.js +1 -1
- package/dist/components/core/NavItem/NavItem.js +100 -52
- package/dist/components/core/NavItem/NavItem.theme.js +6 -3
- package/dist/components/core/NavItem/NavItem.types.d.ts +44 -30
- package/dist/components/core/NavItem/index.d.ts +1 -1
- package/dist/components/core/Pill/Pill.theme.js +1 -1
- package/dist/components/core/Sidebar/Sidebar.d.ts +11 -1
- package/dist/components/core/Sidebar/Sidebar.js +49 -16
- package/dist/components/core/Sidebar/Sidebar.theme.js +9 -7
- package/dist/components/core/Sidebar/Sidebar.types.d.ts +57 -0
- package/dist/components/core/Sidebar/SidebarContent.js +3 -1
- package/dist/components/core/Sidebar/SidebarContext.d.ts +14 -0
- package/dist/components/core/Sidebar/SidebarContext.js +63 -11
- package/dist/components/core/Sidebar/SidebarFooter.js +3 -1
- package/dist/components/core/Sidebar/SidebarHeader.js +6 -2
- package/dist/components/core/Sidebar/SidebarMobileTrigger.d.ts +3 -0
- package/dist/components/core/Sidebar/SidebarMobileTrigger.js +28 -0
- package/dist/components/core/Sidebar/SidebarNav.js +3 -2
- package/dist/components/core/Sidebar/SidebarNavGroup.js +4 -2
- package/dist/components/core/Sidebar/SidebarToggle.js +1 -1
- package/dist/components/core/Sidebar/SidebarUserProfile.d.ts +3 -0
- package/dist/components/core/Sidebar/SidebarUserProfile.js +35 -0
- package/dist/components/core/Sidebar/index.d.ts +3 -1
- package/dist/components/core/Tabs/Tabs.theme.d.ts +1 -1
- package/dist/components/core/Tabs/Tabs.theme.js +3 -3
- package/dist/components/core/index.js +2 -0
- package/dist/components/data-display/GanttChart/GanttToolbar.js +1 -1
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +10 -10
- package/dist/index.js +2 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/global.scss +1 -0
- 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("
|
|
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: "
|
|
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
|
+
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: "
|
|
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: "
|
|
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
|
-
|
|
94
|
-
...(_e = badgeTheme.variantColors) === null || _e === void 0 ? void 0 : _e.
|
|
95
|
-
...(_f = customTheme.variantColors) === null || _f === void 0 ? void 0 : _f.
|
|
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
|
-
|
|
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
|
-
//
|
|
49
|
-
|
|
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-
|
|
67
|
-
sm: "text-sm px-1.5 py-0 gap-1 font-
|
|
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" | "
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
70
|
-
|
|
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" | "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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,
|
|
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
|
-
//
|
|
58
|
+
// Build end content
|
|
22
59
|
let endContent = propEndContent;
|
|
23
60
|
if (!endContent && badge) {
|
|
24
|
-
endContent = (jsx(Badge, { color: "neutral", variant: "
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
|
|
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
|
|
90
|
+
if (hasSubmenu) {
|
|
59
91
|
e.preventDefault();
|
|
60
|
-
|
|
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
|
-
//
|
|
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],
|
|
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(
|
|
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 ?
|
|
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 && (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
className: cn(
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
116
|
-
}
|
|
160
|
+
return node;
|
|
161
|
+
};
|
|
117
162
|
// Show tooltip in collapsed sidebar mode
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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';
|
|
@@ -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
|
+
};
|