@kala-ui/react-app 0.1.0-beta.0 → 0.1.0-beta.10
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/CHANGELOG.md +83 -0
- package/README.md +9 -4
- package/dist/components/app-shell/app-shell.d.ts +8 -46
- package/dist/components/app-shell/app-shell.js +41 -42
- package/dist/components/app-shell/app-shell.types.d.ts +45 -0
- package/dist/components/app-shell/app-shell.types.js +2 -0
- package/dist/components/app-shell/index.d.ts +1 -0
- package/dist/components/charts/area-chart.d.ts +1 -1
- package/dist/components/charts/area-chart.js +4 -3
- package/dist/components/charts/bar-chart.d.ts +1 -1
- package/dist/components/charts/bar-chart.js +4 -3
- package/dist/components/charts/chart-skeleton.d.ts +0 -21
- package/dist/components/charts/chart-skeleton.js +2 -1
- package/dist/components/charts/chart-utils.d.ts +56 -0
- package/dist/components/charts/chart-utils.js +135 -0
- package/dist/components/charts/chart.d.ts +3 -1
- package/dist/components/charts/chart.js +14 -4
- package/dist/components/charts/chart.types.d.ts +3 -0
- package/dist/components/charts/donut-chart.d.ts +1 -1
- package/dist/components/charts/donut-chart.js +5 -4
- package/dist/components/charts/index.d.ts +1 -1
- package/dist/components/charts/index.js +1 -1
- package/dist/components/charts/line-chart.d.ts +1 -1
- package/dist/components/charts/line-chart.js +4 -3
- package/dist/components/charts/radial-bar-chart.d.ts +1 -1
- package/dist/components/charts/radial-bar-chart.js +4 -3
- package/dist/components/data-table/column-filters.js +1 -1
- package/dist/components/data-table/column-header-filter.js +1 -1
- package/dist/components/data-table/data-table-body-rows.js +1 -1
- package/dist/components/data-table/data-table-filter-chips.js +1 -1
- package/dist/components/data-table/data-table-header-row.js +1 -1
- package/dist/components/data-table/data-table-skeleton.js +1 -1
- package/dist/components/data-table/data-table-toolbar.js +1 -1
- package/dist/components/data-table/data-table.js +6 -4
- package/dist/components/data-table/data-table.types.d.ts +2 -0
- package/dist/components/data-table/index.d.ts +1 -1
- package/dist/components/data-table/index.js +1 -1
- package/dist/components/data-table/pagination-nav.js +1 -1
- package/dist/components/data-table/use-table-state.d.ts +29 -0
- package/dist/components/data-table/use-table-state.js +312 -0
- package/dist/components/dnd/dnd.d.ts +16 -98
- package/dist/components/dnd/dnd.js +36 -8
- package/dist/components/dnd/dnd.types.d.ts +92 -0
- package/dist/components/dnd/dnd.types.js +7 -0
- package/dist/components/dnd/index.d.ts +2 -1
- package/dist/components/dnd/index.js +1 -1
- package/dist/components/footer/footer.d.ts +2 -37
- package/dist/components/footer/footer.js +18 -2
- package/dist/components/footer/footer.types.d.ts +25 -0
- package/dist/components/footer/footer.types.js +2 -0
- package/dist/components/footer/index.d.ts +1 -1
- package/dist/components/header/header-skeleton.js +1 -1
- package/dist/components/header/header.js +6 -12
- package/dist/components/metric-card/index.d.ts +2 -1
- package/dist/components/metric-card/metric-card-skeleton.js +2 -1
- package/dist/components/metric-card/metric-card.d.ts +2 -14
- package/dist/components/metric-card/metric-card.js +33 -29
- package/dist/components/metric-card/metric-card.types.d.ts +3 -0
- package/dist/components/nav-link/index.d.ts +2 -1
- package/dist/components/nav-link/index.js +1 -1
- package/dist/components/nav-link/nav-link.d.ts +2 -25
- package/dist/components/nav-link/nav-link.js +43 -5
- package/dist/components/nav-link/nav-link.types.d.ts +23 -0
- package/dist/components/nav-link/nav-link.types.js +2 -0
- package/dist/components/navigation/index.d.ts +1 -1
- package/dist/components/navigation/navigation-skeleton.js +1 -1
- package/dist/components/navigation/navigation.d.ts +1 -24
- package/dist/components/navigation/navigation.js +9 -19
- package/dist/components/navigation/navigation.types.d.ts +15 -0
- package/dist/components/navigation/navigation.types.js +2 -0
- package/dist/components/session-card/index.d.ts +2 -1
- package/dist/components/session-card/index.js +1 -1
- package/dist/components/session-card/session-card-skeleton.d.ts +3 -1
- package/dist/components/session-card/session-card-skeleton.js +3 -2
- package/dist/components/session-card/session-card.d.ts +3 -26
- package/dist/components/session-card/session-card.js +23 -8
- package/dist/components/session-card/session-card.types.d.ts +26 -0
- package/dist/components/session-card/session-card.types.js +2 -0
- package/dist/components/sidebar/sidebar-skeleton.js +1 -1
- package/dist/components/sidebar/sidebar.js +6 -12
- package/dist/components/social-login-button/index.d.ts +3 -1
- package/dist/components/social-login-button/index.js +2 -1
- package/dist/components/social-login-button/social-login-button.d.ts +2 -20
- package/dist/components/social-login-button/social-login-button.js +9 -11
- package/dist/components/social-login-button/social-login-button.types.d.ts +28 -0
- package/dist/components/social-login-button/social-login-button.types.js +2 -0
- package/dist/components/social-login-button/social-login-buttons.d.ts +4 -0
- package/dist/components/social-login-button/social-login-buttons.js +18 -0
- package/dist/components/social-login-buttons/social-login-buttons.js +1 -1
- package/dist/components/sparkline-chart/sparkline-chart.js +1 -1
- package/dist/components/user-menu-dropdown/index.d.ts +2 -1
- package/dist/components/user-menu-dropdown/index.js +1 -1
- package/dist/components/user-menu-dropdown/user-menu-dropdown.d.ts +2 -31
- package/dist/components/user-menu-dropdown/user-menu-dropdown.js +14 -2
- package/dist/components/user-menu-dropdown/user-menu-dropdown.types.d.ts +16 -0
- package/dist/components/user-menu-dropdown/user-menu-dropdown.types.js +2 -0
- package/dist/config/app-shell.d.ts +13 -0
- package/dist/config/app-shell.js +20 -0
- package/dist/config/charts.d.ts +23 -0
- package/dist/config/charts.js +23 -0
- package/dist/config/footer.d.ts +16 -0
- package/dist/config/footer.js +16 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/metric-card.d.ts +36 -0
- package/dist/config/metric-card.js +44 -0
- package/dist/config/session-card.d.ts +32 -0
- package/dist/config/session-card.js +32 -0
- package/dist/config/social-login-button.d.ts +15 -0
- package/dist/config/social-login-button.js +15 -0
- package/dist/config/user-menu-dropdown.d.ts +10 -0
- package/dist/config/user-menu-dropdown.js +10 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/styles/app.css +18 -53
- package/package.json +24 -28
|
@@ -1,37 +1,2 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export
|
|
3
|
-
label: string;
|
|
4
|
-
href: string;
|
|
5
|
-
}
|
|
6
|
-
export interface FooterLinkSection {
|
|
7
|
-
title: string;
|
|
8
|
-
links: FooterLink[];
|
|
9
|
-
}
|
|
10
|
-
export interface SocialLink {
|
|
11
|
-
name: string;
|
|
12
|
-
href: string;
|
|
13
|
-
icon: React.ReactNode;
|
|
14
|
-
}
|
|
15
|
-
export interface FooterProps extends React.HTMLAttributes<HTMLElement>, React.RefAttributes<HTMLElement> {
|
|
16
|
-
/**
|
|
17
|
-
* Sections of links to display in columns
|
|
18
|
-
*/
|
|
19
|
-
linkSections?: FooterLinkSection[];
|
|
20
|
-
/**
|
|
21
|
-
* Social media links with icons
|
|
22
|
-
*/
|
|
23
|
-
socialLinks?: SocialLink[];
|
|
24
|
-
/**
|
|
25
|
-
* Copyright text
|
|
26
|
-
*/
|
|
27
|
-
copyright?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Additional content to display in footer (e.g., newsletter signup)
|
|
30
|
-
*/
|
|
31
|
-
children?: React.ReactNode;
|
|
32
|
-
/**
|
|
33
|
-
* Center align all content
|
|
34
|
-
*/
|
|
35
|
-
centered?: boolean;
|
|
36
|
-
}
|
|
37
|
-
export declare function Footer({ ref, className, linkSections, socialLinks, copyright, children, centered, ...props }: FooterProps): React.JSX.Element;
|
|
1
|
+
import type { FooterProps } from "./footer.types";
|
|
2
|
+
export declare function Footer({ ref, className, style, slotStyles: slotStylesRaw, linkSections, socialLinks, copyright, children, centered, ...props }: FooterProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useSlotStyles } from "@kala-ui/react/kala-provider";
|
|
4
|
+
import { applySlot } from "@kala-ui/react/lib/slot-styles";
|
|
3
5
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
4
|
-
|
|
6
|
+
import { footerStyles } from "../../config/footer.js";
|
|
7
|
+
export function Footer({ ref, className, style, slotStyles: slotStylesRaw, linkSections = [], socialLinks = [], copyright, children, centered = false, ...props }) {
|
|
8
|
+
const slotStyles = useSlotStyles("footer", slotStylesRaw);
|
|
5
9
|
const currentYear = new Date().getFullYear();
|
|
6
10
|
const copyrightText = copyright || `© ${currentYear} All rights reserved.`;
|
|
7
|
-
|
|
11
|
+
const root = applySlot(cn(footerStyles.root, className), slotStyles?.root);
|
|
12
|
+
const inner = applySlot(footerStyles.inner, slotStyles?.inner);
|
|
13
|
+
const centeredContent = applySlot(footerStyles.centeredContent, slotStyles?.centeredContent);
|
|
14
|
+
const grid = applySlot(footerStyles.grid, slotStyles?.grid);
|
|
15
|
+
const sectionTitle = applySlot(footerStyles.sectionTitle, slotStyles?.sectionTitle);
|
|
16
|
+
const sectionList = applySlot(footerStyles.sectionList, slotStyles?.sectionList);
|
|
17
|
+
const sectionLink = applySlot(footerStyles.sectionLink, slotStyles?.sectionLink);
|
|
18
|
+
const customContent = applySlot(footerStyles.customContent, slotStyles?.customContent);
|
|
19
|
+
const bottomRow = applySlot(footerStyles.bottomRow, slotStyles?.bottomRow);
|
|
20
|
+
const copyrightPart = applySlot(footerStyles.copyright, slotStyles?.copyright);
|
|
21
|
+
const socialList = applySlot(footerStyles.socialList, slotStyles?.socialList);
|
|
22
|
+
const socialLink = applySlot(footerStyles.socialLink, slotStyles?.socialLink);
|
|
23
|
+
return (_jsx("footer", { "data-kala-component": "footer", ref: ref, className: root.className, style: root.style, ...props, children: _jsxs("div", { className: inner.className, children: [centered && children && (_jsx("div", { className: centeredContent.className, children: children })), _jsxs("div", { className: grid.className, children: [linkSections.map((section) => (_jsxs("nav", { "aria-label": `${section.title} links`, children: [_jsx("h3", { className: sectionTitle.className, children: section.title }), _jsx("ul", { className: sectionList.className, children: section.links.map((link) => (_jsx("li", { children: _jsx("a", { href: link.href, className: sectionLink.className, children: link.label }) }, `${link.href}-${link.label}`))) })] }, section.title))), !centered && children && (_jsx("div", { className: customContent.className, children: children }))] }), _jsxs("div", { className: cn(bottomRow.className, !centered && footerStyles.bottomRowSide, centered && footerStyles.bottomRowCentered), children: [_jsx("div", { className: copyrightPart.className, children: copyrightText }), socialLinks.length > 0 && (_jsx("div", { className: socialList.className, children: socialLinks.map((social) => (_jsx("a", { href: social.href, target: "_blank", rel: "noopener noreferrer", className: socialLink.className, "aria-label": social.name, children: social.icon }, social.href))) }))] })] }) }));
|
|
8
24
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SlotStyles } from "@kala-ui/react/lib/slot-styles";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
export interface FooterLink {
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FooterLinkSection {
|
|
8
|
+
title: string;
|
|
9
|
+
links: FooterLink[];
|
|
10
|
+
}
|
|
11
|
+
export interface SocialLink {
|
|
12
|
+
name: string;
|
|
13
|
+
href: string;
|
|
14
|
+
icon: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export interface FooterProps extends React.HTMLAttributes<HTMLElement>, React.RefAttributes<HTMLElement> {
|
|
17
|
+
linkSections?: FooterLinkSection[];
|
|
18
|
+
socialLinks?: SocialLink[];
|
|
19
|
+
copyright?: string;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
centered?: boolean;
|
|
22
|
+
/** Per-part overrides: root, inner, centeredContent, grid, sectionTitle,
|
|
23
|
+
* sectionList, sectionLink, customContent, bottomRow, copyright, socialList, socialLink. */
|
|
24
|
+
slotStyles?: SlotStyles;
|
|
25
|
+
}
|
|
@@ -19,5 +19,5 @@ import { Skeleton } from "@kala-ui/react/skeleton";
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export function HeaderSkeleton({ showNavigation = true, navItemCount = 4, showSearch = false, showNotifications = false, showUserMenu = true, className, "data-testid": dataTestId, }) {
|
|
22
|
-
return (_jsxs("header", { "data-testid": dataTestId || "header-skeleton", className: cn("flex items-center justify-between p-4 border-b bg-background", className), children: [_jsx(Skeleton, { className: "h-8 w-32 flex-shrink-0" }), showNavigation && (_jsx("div", { className: "hidden md:flex gap-6 flex-1 mx-8", children: Array.from({ length: navItemCount }).map((_, i) => (_jsx(Skeleton, { className: "h-4 w-20" }, `nav-${i}`))) })), _jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [showSearch && (_jsx(Skeleton, { className: "hidden sm:block h-9 w-64 rounded-full" })), showNotifications && _jsx(Skeleton, { className: "h-9 w-9 rounded-full" }), showUserMenu && _jsx(Skeleton, { className: "h-9 w-9 rounded-full" }), _jsx(Skeleton, { className: "h-9 w-9 md:hidden" })] })] }));
|
|
22
|
+
return (_jsxs("header", { "data-kala-component": "header-skeleton", "data-testid": dataTestId || "header-skeleton", className: cn("flex items-center justify-between p-4 border-b bg-background", className), children: [_jsx(Skeleton, { className: "h-8 w-32 flex-shrink-0" }), showNavigation && (_jsx("div", { className: "hidden md:flex gap-6 flex-1 mx-8", children: Array.from({ length: navItemCount }).map((_, i) => (_jsx(Skeleton, { className: "h-4 w-20" }, `nav-${i}`))) })), _jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [showSearch && (_jsx(Skeleton, { className: "hidden sm:block h-9 w-64 rounded-full" })), showNotifications && _jsx(Skeleton, { className: "h-9 w-9 rounded-full" }), showUserMenu && _jsx(Skeleton, { className: "h-9 w-9 rounded-full" }), _jsx(Skeleton, { className: "h-9 w-9 md:hidden" })] })] }));
|
|
23
23
|
}
|
|
@@ -9,7 +9,7 @@ import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMe
|
|
|
9
9
|
import { Stack } from "@kala-ui/react/stack";
|
|
10
10
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@kala-ui/react/tabs";
|
|
11
11
|
import { Text } from "@kala-ui/react/text";
|
|
12
|
-
import { useFocusTrap, useMounted, useScrollLock } from "@kala-ui/react-hooks";
|
|
12
|
+
import { useFocusTrap, useMounted, useScrollLock, useWindowEvent, } from "@kala-ui/react-hooks";
|
|
13
13
|
import { Bell, ChevronDown, Menu, Settings, X } from "lucide-react";
|
|
14
14
|
import * as React from "react";
|
|
15
15
|
import { createPortal } from "react-dom";
|
|
@@ -22,16 +22,10 @@ export function Header({ ref, className, logo, navLinks = [], userMenu, userProf
|
|
|
22
22
|
}, [isMobileMenuOpen, setScrollLocked]);
|
|
23
23
|
// Fullscreen mobile menu: trap focus, close on Escape.
|
|
24
24
|
const mobileNavRef = useFocusTrap(!!isMobileMenuOpen);
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (event.key === "Escape")
|
|
30
|
-
onMobileMenuToggle?.();
|
|
31
|
-
};
|
|
32
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
33
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
34
|
-
}, [isMobileMenuOpen, onMobileMenuToggle]);
|
|
25
|
+
useWindowEvent("keydown", (event) => {
|
|
26
|
+
if (event.key === "Escape" && isMobileMenuOpen)
|
|
27
|
+
onMobileMenuToggle?.();
|
|
28
|
+
});
|
|
35
29
|
const mounted = useMounted();
|
|
36
30
|
// Track expanded mobile menu items
|
|
37
31
|
const [expandedItems, setExpandedItems] = React.useState(new Set());
|
|
@@ -47,7 +41,7 @@ export function Header({ ref, className, logo, navLinks = [], userMenu, userProf
|
|
|
47
41
|
return next;
|
|
48
42
|
});
|
|
49
43
|
};
|
|
50
|
-
return (_jsxs(Box, {
|
|
44
|
+
return (_jsxs(Box, { "data-kala-component": "header", as: "header", ref: ref, className: cn("w-full bg-popover border-b sticky top-0 z-20 kala-surface-popover", className), ...props, children: [_jsx(Box, { className: "container mx-auto px-3 sm:px-4 lg:px-6 xl:px-8", children: _jsxs(Flex, { align: "center", justify: "between", className: "h-14 sm:h-16 gap-2 sm:gap-4", children: [_jsx(Box, { className: "shrink-0 min-w-0 max-w-[120px] sm:max-w-none", children: logo }), _jsx(Flex, { align: "center", gap: variant === "default" ? 3 : 6, className: cn("hidden lg:flex", variant === "default" ? "flex-1" : ""), children: _jsx(Box, { className: cn("flex", variant === "default" ? "mx-auto" : ""), children: _jsxs(NavigationMenu, { children: [_jsx(NavigationMenuList, { children: navLinks.map((link) => (_jsx(NavigationMenuItem, { children: link.children ? (_jsxs(_Fragment, { children: [_jsx(NavigationMenuTrigger, { className: cn("text-sm lg:text-base", link.active && "text-primary"), children: link.label }), _jsx(NavigationMenuContent, { children: _jsx(Box, { className: "py-2", children: _jsx(List, { divided: false, className: cn("bg-transparent border-none", link.children.length <= 3
|
|
51
45
|
? "w-[220px]"
|
|
52
46
|
: "w-[440px] grid grid-cols-2"), children: link.children.map((child) => (_jsx(Box, { as: "li", children: _jsx(NavigationMenuLink, { asChild: true, children: _jsx(Box, { as: "a", href: child.href, className: cn("block px-4 py-2.5 text-sm font-normal no-underline outline-none transition-colors", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "focus-visible:bg-accent focus-visible:text-accent-foreground", child.active &&
|
|
53
47
|
"text-primary bg-primary/10"), children: child.label }) }) }, child.href))) }) }) })] })) : (_jsx(NavigationMenuLink, { href: link.href, className: cn("px-4 py-2 text-sm lg:text-base", link.active && "text-primary"), "aria-current": link.active ? "page" : undefined, children: link.label })) }, link.href))) }), _jsx(NavigationMenuViewport, {})] }) }) }), _jsxs(Flex, { align: "center", className: "gap-1.5 sm:gap-2 lg:gap-3 shrink-0", children: [searchBar && (_jsx(Box, { className: "hidden lg:block w-48 xl:w-64", children: searchBar })), languageSwitcher && (_jsx(Box, { className: "hidden lg:block", children: languageSwitcher })), themeSwitcher && (_jsx(Box, { className: "hidden lg:block", children: themeSwitcher })), notifications && (_jsx(Box, { className: "hidden lg:block", children: notifications })), userMenu && _jsx(Box, { className: "hidden lg:block", children: userMenu }), onMobileMenuToggle && (_jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-9 w-9 sm:h-10 sm:w-10", onClick: onMobileMenuToggle, "aria-label": "Toggle mobile menu", "aria-expanded": isMobileMenuOpen, children: isMobileMenuOpen ? (_jsx(X, { className: "w-5 h-5 sm:w-6 sm:h-6" })) : (_jsx(Menu, { className: "w-5 h-5 sm:w-6 sm:h-6" })) }))] })] }) }), isMobileMenuOpen &&
|
|
@@ -10,6 +10,7 @@ import { Card } from "@kala-ui/react/card";
|
|
|
10
10
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
11
11
|
import { Skeleton, SkeletonCircle } from "@kala-ui/react/skeleton";
|
|
12
12
|
import * as React from "react";
|
|
13
|
+
import { metricCardSkeletonStyles } from "../../config/metric-card.js";
|
|
13
14
|
/**
|
|
14
15
|
* Metric Card skeleton component with multiple variants
|
|
15
16
|
*
|
|
@@ -21,7 +22,7 @@ import * as React from "react";
|
|
|
21
22
|
* ```
|
|
22
23
|
*/
|
|
23
24
|
export function MetricCardSkeleton({ variant = "default", showIcon = false, showChange = true, className, "data-testid": dataTestId, }) {
|
|
24
|
-
return (_jsx(Card, { ref: React.createRef(), "data-testid": dataTestId || "metric-card-skeleton", className: cn(className), children: _jsxs("div", { className:
|
|
25
|
+
return (_jsx(Card, { "data-kala-component": "metric-card-skeleton", ref: React.createRef(), "data-testid": dataTestId || "metric-card-skeleton", className: cn(className), children: _jsxs("div", { className: metricCardSkeletonStyles.body, children: [_jsxs("div", { className: metricCardSkeletonStyles.head, children: [_jsx(Skeleton, { className: metricCardSkeletonStyles.title }), variant === "withIcon" || showIcon ? (_jsx(SkeletonCircle, { size: "1.5rem", className: metricCardSkeletonStyles.icon })) : null] }), _jsx("div", { className: metricCardSkeletonStyles.valueBlock, children: _jsx(Skeleton, { className: metricCardSkeletonStyles.value }) }), (variant === "default" || showChange) && (_jsx("div", { className: metricCardSkeletonStyles.meta, children: _jsx(Skeleton, { className: metricCardSkeletonStyles.metaLine }) }))] }) }));
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Compound component: MetricCard.Skeleton
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import type { MetricCardSkeletonConfig } from "@kala-ui/react/skeleton";
|
|
2
1
|
import type * as React from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
value: number | string;
|
|
6
|
-
icon?: React.ReactNode;
|
|
7
|
-
change?: number;
|
|
8
|
-
changeLabel?: string;
|
|
9
|
-
subtitle?: string;
|
|
10
|
-
color?: "primary" | "secondary" | "destructive" | "success" | "warning" | "info" | "muted";
|
|
11
|
-
isLoading?: boolean;
|
|
12
|
-
skeletonConfig?: MetricCardSkeletonConfig;
|
|
13
|
-
skeleton?: React.ReactNode;
|
|
14
|
-
}
|
|
15
|
-
declare function MetricCard({ ref, title, value, icon, change, changeLabel, subtitle, className, color, isLoading, skeletonConfig, skeleton, ...props }: MetricCardProps): React.JSX.Element;
|
|
2
|
+
import type { MetricCardProps } from "./metric-card.types";
|
|
3
|
+
declare function MetricCard({ ref, title, value, icon, change, changeLabel, subtitle, className, style, slotStyles: slotStylesRaw, color, isLoading, skeletonConfig, skeleton, ...props }: MetricCardProps): React.JSX.Element;
|
|
16
4
|
export { MetricCard };
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Card } from "@kala-ui/react/card";
|
|
4
|
+
import { useSlotStyles } from "@kala-ui/react/kala-provider";
|
|
5
|
+
import { applySlot } from "@kala-ui/react/lib/slot-styles";
|
|
4
6
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
7
|
+
import { metricCardChangeColors, metricCardColorStyles, metricCardStyles, } from "../../config/metric-card.js";
|
|
5
8
|
import { MetricCardSkeleton } from "./metric-card-skeleton.js";
|
|
6
|
-
function MetricCard({ ref, title, value, icon, change, changeLabel, subtitle, className, color = "muted", isLoading = false, skeletonConfig, skeleton, ...props }) {
|
|
9
|
+
function MetricCard({ ref, title, value, icon, change, changeLabel, subtitle, className, style, slotStyles: slotStylesRaw, color = "muted", isLoading = false, skeletonConfig, skeleton, ...props }) {
|
|
10
|
+
const slotStyles = useSlotStyles("metric-card", slotStylesRaw);
|
|
7
11
|
if (isLoading) {
|
|
8
12
|
if (skeleton) {
|
|
9
|
-
|
|
13
|
+
const loadingRoot = applySlot(cn(className), slotStyles?.root);
|
|
14
|
+
return (_jsx(Card, { "data-kala-component": "metric-card", ref: ref, className: loadingRoot.className, style: loadingRoot.style, ...props, children: skeleton }));
|
|
10
15
|
}
|
|
11
|
-
return _jsx(MetricCardSkeleton, { className: className, ...skeletonConfig });
|
|
16
|
+
return (_jsx(MetricCardSkeleton, { "data-kala-component": "metric-card", className: className, ...skeletonConfig }));
|
|
12
17
|
}
|
|
13
|
-
const getChangeColor = (changeValue) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return "text-muted-foreground";
|
|
19
|
-
};
|
|
18
|
+
const getChangeColor = (changeValue) => changeValue > 0
|
|
19
|
+
? metricCardChangeColors.up
|
|
20
|
+
: changeValue < 0
|
|
21
|
+
? metricCardChangeColors.down
|
|
22
|
+
: metricCardChangeColors.flat;
|
|
20
23
|
const getChangeIcon = (changeValue) => {
|
|
21
24
|
if (changeValue > 0)
|
|
22
25
|
return "↑";
|
|
@@ -31,25 +34,26 @@ function MetricCard({ ref, title, value, icon, change, changeLabel, subtitle, cl
|
|
|
31
34
|
: "No change";
|
|
32
35
|
return changeLabel || defaultLabel;
|
|
33
36
|
};
|
|
34
|
-
const getColorStyles = () => {
|
|
35
|
-
switch (color) {
|
|
36
|
-
case "primary":
|
|
37
|
-
return "bg-gradient-to-br from-primary to-primary/90 text-primary-foreground";
|
|
38
|
-
case "secondary":
|
|
39
|
-
return "bg-gradient-to-br from-secondary to-secondary/90 text-secondary-foreground";
|
|
40
|
-
case "destructive":
|
|
41
|
-
return "bg-gradient-to-br from-destructive to-destructive/90 text-destructive-foreground";
|
|
42
|
-
case "success":
|
|
43
|
-
return "bg-gradient-to-br from-success to-success/90 text-success-foreground";
|
|
44
|
-
case "warning":
|
|
45
|
-
return "bg-gradient-to-br from-warning to-warning/90 text-warning-foreground";
|
|
46
|
-
case "info":
|
|
47
|
-
return "bg-gradient-to-br from-info to-info/90 text-info-foreground";
|
|
48
|
-
default:
|
|
49
|
-
return "bg-card border text-card-foreground";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
37
|
const isColorful = color !== "muted" || className?.includes("bg-");
|
|
53
|
-
|
|
38
|
+
const root = applySlot(cn(metricCardColorStyles[color], className), slotStyles?.root);
|
|
39
|
+
const body = applySlot(metricCardStyles.body, slotStyles?.body);
|
|
40
|
+
const head = applySlot(metricCardStyles.head, slotStyles?.head);
|
|
41
|
+
const titleLabel = applySlot(metricCardStyles.title, slotStyles?.title);
|
|
42
|
+
const iconSlot = applySlot(metricCardStyles.icon, slotStyles?.icon);
|
|
43
|
+
const valueSlot = applySlot(metricCardStyles.value, slotStyles?.value);
|
|
44
|
+
const meta = applySlot(metricCardStyles.meta, slotStyles?.meta);
|
|
45
|
+
const changeSlot = applySlot(metricCardStyles.change, slotStyles?.change);
|
|
46
|
+
const subtitleSlot = applySlot(metricCardStyles.subtitle, slotStyles?.subtitle);
|
|
47
|
+
return (_jsx(Card, { "data-kala-component": "metric-card", ref: ref, className: root.className, style: root.style, ...props, children: _jsxs("div", { className: body.className, children: [_jsxs("div", { className: head.className, children: [_jsx("h6", { className: cn(titleLabel.className, isColorful
|
|
48
|
+
? metricCardStyles.titleOnColor
|
|
49
|
+
: metricCardStyles.titleMuted), children: title }), icon && (_jsx("div", { className: cn(iconSlot.className, isColorful
|
|
50
|
+
? metricCardStyles.iconOnColor
|
|
51
|
+
: metricCardStyles.iconMuted), "aria-hidden": "true", children: icon }))] }), _jsx("div", { className: metricCardStyles.valueBlock, children: _jsxs("div", { className: cn(valueSlot.className, isColorful
|
|
52
|
+
? metricCardStyles.valueOnColor
|
|
53
|
+
: metricCardStyles.valueMuted), children: [_jsxs("span", { className: "sr-only", children: [title, ":", " ", typeof value === "number" ? value.toLocaleString() : value] }), typeof value === "number" ? value.toLocaleString() : value] }) }), (change !== undefined || subtitle) && (_jsx("div", { className: meta.className, children: change !== undefined ? (_jsxs("div", { className: cn(changeSlot.className, isColorful
|
|
54
|
+
? metricCardStyles.changeOnColor
|
|
55
|
+
: getChangeColor(change)), children: [_jsxs("span", { className: "sr-only", children: ["Change: ", getChangeText(change)] }), _jsx("span", { className: "inline-block", "aria-hidden": "true", children: getChangeIcon(change) }), _jsx("span", { children: getChangeText(change) })] })) : subtitle ? (_jsx("div", { className: cn(subtitleSlot.className, isColorful
|
|
56
|
+
? metricCardStyles.subtitleOnColor
|
|
57
|
+
: metricCardStyles.subtitleMuted), children: subtitle })) : null }))] }) }));
|
|
54
58
|
}
|
|
55
59
|
export { MetricCard };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlotStyles } from "@kala-ui/react/lib/slot-styles";
|
|
1
2
|
import type { MetricCardSkeletonConfig } from "@kala-ui/react/skeleton";
|
|
2
3
|
import type * as React from "react";
|
|
3
4
|
export interface MetricCardProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
@@ -11,4 +12,6 @@ export interface MetricCardProps extends Omit<React.ComponentProps<"div">, "titl
|
|
|
11
12
|
isLoading?: boolean;
|
|
12
13
|
skeletonConfig?: MetricCardSkeletonConfig;
|
|
13
14
|
skeleton?: React.ReactNode;
|
|
15
|
+
/** Per-part overrides: root, body, head, title, icon, value, meta, change, subtitle. */
|
|
16
|
+
slotStyles?: SlotStyles;
|
|
14
17
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { NavLink } from "./nav-link";
|
|
2
|
+
export type { NavLinkProps } from "./nav-link.types";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export
|
|
2
|
+
export { NavLink } from "./nav-link.js";
|
|
@@ -1,26 +1,3 @@
|
|
|
1
1
|
import type * as React from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
label: React.ReactNode;
|
|
5
|
-
/** Link description */
|
|
6
|
-
description?: React.ReactNode;
|
|
7
|
-
/** Left icon */
|
|
8
|
-
icon?: React.ReactNode;
|
|
9
|
-
/** Right section (e.g. badge, icon) */
|
|
10
|
-
rightSection?: React.ReactNode;
|
|
11
|
-
/** Active state */
|
|
12
|
-
active?: boolean;
|
|
13
|
-
/** Collapsed/Expanded state for nested items */
|
|
14
|
-
defaultOpen?: boolean;
|
|
15
|
-
/** Controlled opened state */
|
|
16
|
-
open?: boolean;
|
|
17
|
-
/** Callback for opened state change */
|
|
18
|
-
onOpenChange?: (open: boolean) => void;
|
|
19
|
-
/** Nested links */
|
|
20
|
-
children?: React.ReactNode;
|
|
21
|
-
/** Disable right section rotation when opened */
|
|
22
|
-
disableRightSectionRotation?: boolean;
|
|
23
|
-
/** Indentation for nested items */
|
|
24
|
-
indent?: boolean;
|
|
25
|
-
}
|
|
26
|
-
export declare function NavLink({ ref, className, label, description, icon, rightSection, active, defaultOpen, open: openProp, onOpenChange, children, disableRightSectionRotation, indent, onClick, ...props }: NavLinkProps): React.JSX.Element;
|
|
2
|
+
import type { NavLinkProps } from "./nav-link.types";
|
|
3
|
+
export declare function NavLink({ className, label, description, icon, rightSection, active, href, defaultOpen, open: openProp, onOpenChange, children, disableRightSectionRotation, indent, onClick, type, disabled, ...props }: NavLinkProps): React.JSX.Element;
|
|
@@ -3,12 +3,53 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
4
4
|
import { useUncontrolled } from "@kala-ui/react-hooks";
|
|
5
5
|
import { ChevronRight } from "lucide-react";
|
|
6
|
-
|
|
6
|
+
/** True for plain left-clicks only — modifier and non-left clicks stay native
|
|
7
|
+
* so open-in-new-tab and download-as-link keep working. */
|
|
8
|
+
function isPlainLeftClick(e) {
|
|
9
|
+
return e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;
|
|
10
|
+
}
|
|
11
|
+
/** Props shared by both arms with element-specific handlers widened to a
|
|
12
|
+
* common HTMLElement base, so the same rest-object spreads onto <button>
|
|
13
|
+
* and <a> without fighting their event-target variance. */
|
|
14
|
+
const widenHandlers = (props) => {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [key, value] of Object.entries(props)) {
|
|
17
|
+
out[key] =
|
|
18
|
+
typeof value === "function" && (key.startsWith("on") || key === "ref")
|
|
19
|
+
? value
|
|
20
|
+
: value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
/** Label/description/icon/right-section row shared by the anchor and
|
|
25
|
+
* button arms. */
|
|
26
|
+
function NavLinkContent({ label, description, icon, rightSection, children, disableRightSectionRotation, isOpened, }) {
|
|
27
|
+
return (_jsxs(_Fragment, { children: [icon && _jsx("span", { className: "flex items-center justify-center", children: icon }), _jsxs("div", { className: "flex flex-1 flex-col items-start overflow-hidden", children: [_jsx("span", { className: "truncate", children: label }), description && (_jsx("span", { className: "truncate text-xs text-muted-foreground font-normal", children: description }))] }), (rightSection || children) && (_jsx("span", { className: cn("flex items-center justify-center text-muted-foreground transition-transform duration-200", children && !disableRightSectionRotation && isOpened && "rotate-90"), children: rightSection || (children && _jsx(ChevronRight, { className: "h-4 w-4" })) }))] }));
|
|
28
|
+
}
|
|
29
|
+
/** Base classes for both arms; the active variant swaps fg/bg. */
|
|
30
|
+
function navLinkBaseClassName(active, className) {
|
|
31
|
+
return cn("flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", active ? "bg-accent text-accent-foreground" : "text-muted-foreground", className);
|
|
32
|
+
}
|
|
33
|
+
export function NavLink({ className, label, description, icon, rightSection, active, href, defaultOpen = false, open: openProp, onOpenChange, children, disableRightSectionRotation = false, indent = false, onClick, type, disabled, ...props }) {
|
|
7
34
|
const [isOpened, handleOpenChange] = useUncontrolled({
|
|
8
35
|
value: openProp,
|
|
9
36
|
defaultValue: defaultOpen,
|
|
10
37
|
onChange: onOpenChange,
|
|
11
38
|
});
|
|
39
|
+
const content = (_jsx(NavLinkContent, { label: label, description: description, icon: icon, rightSection: rightSection, children: children, disableRightSectionRotation: disableRightSectionRotation, isOpened: isOpened }));
|
|
40
|
+
const baseClassName = navLinkBaseClassName(active, className);
|
|
41
|
+
if (href) {
|
|
42
|
+
// Anchor mode: button-only props (type/disabled) are invalid on <a> —
|
|
43
|
+
// disabled degrades to aria-disabled + pointer-events styling. Plain
|
|
44
|
+
// left-clicks are the SPA router's to handle; every other click
|
|
45
|
+
// (modifiers, middle) stays native so open-in-new-tab keeps working.
|
|
46
|
+
const handleAnchorClick = (e) => {
|
|
47
|
+
if (isPlainLeftClick(e))
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
onClick?.(e);
|
|
50
|
+
};
|
|
51
|
+
return (_jsx("a", { "data-kala-component": "nav-link", href: href, "aria-current": active ? "page" : undefined, "aria-disabled": disabled || undefined, className: cn(baseClassName, disabled && "pointer-events-none opacity-50"), onClick: handleAnchorClick, ...widenHandlers(props), children: content }));
|
|
52
|
+
}
|
|
12
53
|
const handleToggle = (e) => {
|
|
13
54
|
if (children) {
|
|
14
55
|
e.preventDefault();
|
|
@@ -16,8 +57,5 @@ export function NavLink({ ref, className, label, description, icon, rightSection
|
|
|
16
57
|
}
|
|
17
58
|
onClick?.(e);
|
|
18
59
|
};
|
|
19
|
-
return (_jsxs(_Fragment, { children: [
|
|
20
|
-
!disableRightSectionRotation &&
|
|
21
|
-
isOpened &&
|
|
22
|
-
"rotate-90"), children: rightSection || (children && _jsx(ChevronRight, { className: "h-4 w-4" })) }))] }), children && isOpened && (_jsx("div", { className: cn("flex flex-col gap-1", indent && "pl-4"), children: children }))] }));
|
|
60
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { "data-kala-component": "nav-link", type: type ?? "button", disabled: disabled, className: baseClassName, onClick: handleToggle, ...widenHandlers(props), children: content }), children && isOpened && (_jsx("div", { className: cn("flex flex-col gap-1", indent && "pl-4"), children: children }))] }));
|
|
23
61
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export interface NavLinkProps extends Omit<React.ComponentProps<"button">, "onChange" | "ref"> {
|
|
3
|
+
label: React.ReactNode;
|
|
4
|
+
description?: React.ReactNode;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
rightSection?: React.ReactNode;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Renders the NavLink as a real anchor (<a href>) instead of a button.
|
|
10
|
+
* Use for top-level navigation targets: the href keeps middle-click,
|
|
11
|
+
* clone-tab and keyboard semantics native. A plain left-click is
|
|
12
|
+
* preventDefaulted so SPA routers can route client-side; modifier clicks
|
|
13
|
+
* are left to the browser. Nested-collapse behavior (children chevron
|
|
14
|
+
* toggle) only applies in button mode.
|
|
15
|
+
*/
|
|
16
|
+
href?: string;
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
open?: boolean;
|
|
19
|
+
onOpenChange?: (open: boolean) => void;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
disableRightSectionRotation?: boolean;
|
|
22
|
+
indent?: boolean;
|
|
23
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { NavigationLink, NavigationProps } from "./navigation";
|
|
1
|
+
export type { NavigationLink, NavigationProps } from "./navigation.types";
|
|
2
2
|
export { Navigation } from "./navigation";
|
|
3
3
|
export type { NavigationSkeletonConfig, NavigationSkeletonProps, } from "./navigation-skeleton";
|
|
4
4
|
export { NavigationSkeleton } from "./navigation-skeleton";
|
|
@@ -19,5 +19,5 @@ import { Skeleton } from "@kala-ui/react/skeleton";
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export function NavigationSkeleton({ orientation = "horizontal", itemCount = 5, showIcons = false, showBadges = false, nested = false, className, "data-testid": dataTestId, }) {
|
|
22
|
-
return (_jsxs("nav", { "data-testid": dataTestId || "navigation-skeleton", className: cn("flex", orientation === "vertical" ? "flex-col gap-2" : "flex-row gap-6", className), "aria-label": "Loading navigation", "aria-busy": "true", children: [Array.from({ length: itemCount }).map((_, i) => (_jsxs("div", { className: cn("flex items-center", orientation === "vertical" ? "gap-3 px-3 py-2" : "gap-2"), children: [showIcons && (_jsx(Skeleton, { className: cn("flex-shrink-0", orientation === "vertical" ? "h-5 w-5" : "h-4 w-4") })), _jsx(Skeleton, { className: cn("h-4", orientation === "vertical" ? "w-24 flex-1" : "w-20") }), showBadges && (_jsx(Skeleton, { className: "h-4 w-4 rounded-full ml-auto flex-shrink-0" }))] }, `nav-${i}`))), nested && orientation === "vertical" && (_jsx("div", { className: "ml-8 space-y-2", children: Array.from({ length: 2 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [showIcons && _jsx(Skeleton, { className: "h-4 w-4 flex-shrink-0" }), _jsx(Skeleton, { className: "h-4 w-20" })] }, `nested-${i}`))) }))] }));
|
|
22
|
+
return (_jsxs("nav", { "data-kala-component": "navigation-skeleton", "data-testid": dataTestId || "navigation-skeleton", className: cn("flex", orientation === "vertical" ? "flex-col gap-2" : "flex-row gap-6", className), "aria-label": "Loading navigation", "aria-busy": "true", children: [Array.from({ length: itemCount }).map((_, i) => (_jsxs("div", { className: cn("flex items-center", orientation === "vertical" ? "gap-3 px-3 py-2" : "gap-2"), children: [showIcons && (_jsx(Skeleton, { className: cn("flex-shrink-0", orientation === "vertical" ? "h-5 w-5" : "h-4 w-4") })), _jsx(Skeleton, { className: cn("h-4", orientation === "vertical" ? "w-24 flex-1" : "w-20") }), showBadges && (_jsx(Skeleton, { className: "h-4 w-4 rounded-full ml-auto flex-shrink-0" }))] }, `nav-${i}`))), nested && orientation === "vertical" && (_jsx("div", { className: "ml-8 space-y-2", children: Array.from({ length: 2 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [showIcons && _jsx(Skeleton, { className: "h-4 w-4 flex-shrink-0" }), _jsx(Skeleton, { className: "h-4 w-20" })] }, `nested-${i}`))) }))] }));
|
|
23
23
|
}
|
|
@@ -1,26 +1,3 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
label: string;
|
|
4
|
-
href: string;
|
|
5
|
-
}
|
|
6
|
-
export interface NavigationProps extends React.HTMLAttributes<HTMLElement>, React.RefAttributes<HTMLElement> {
|
|
7
|
-
/**
|
|
8
|
-
* Array of navigation links
|
|
9
|
-
*/
|
|
10
|
-
links: NavigationLink[];
|
|
11
|
-
/**
|
|
12
|
-
* Orientation of the navigation
|
|
13
|
-
* @default 'horizontal'
|
|
14
|
-
*/
|
|
15
|
-
orientation?: "horizontal" | "vertical";
|
|
16
|
-
/**
|
|
17
|
-
* Mobile layout style
|
|
18
|
-
* @default 'dropdown'
|
|
19
|
-
*/
|
|
20
|
-
mobileLayout?: "dropdown" | "vertical";
|
|
21
|
-
/**
|
|
22
|
-
* Current pathname for active link detection
|
|
23
|
-
*/
|
|
24
|
-
pathname?: string;
|
|
25
|
-
}
|
|
2
|
+
import type { NavigationProps } from "./navigation.types";
|
|
26
3
|
export declare function Navigation({ ref, className, links, orientation, mobileLayout, pathname, ...props }: NavigationProps): React.JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useClickOutside } from "@kala-ui/react-hooks";
|
|
3
4
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
4
5
|
import { ChevronDown } from "lucide-react";
|
|
5
6
|
import * as React from "react";
|
|
@@ -7,22 +8,14 @@ import { isActivePath } from "../../lib/active-path.js";
|
|
|
7
8
|
export function Navigation({ ref, className, links, orientation = "horizontal", mobileLayout = "dropdown", pathname = "", ...props }) {
|
|
8
9
|
const [isMobileOpen, setIsMobileOpen] = React.useState(false);
|
|
9
10
|
const isActive = (href) => isActivePath(pathname, href);
|
|
10
|
-
// Close mobile menu when clicking outside
|
|
11
|
-
React
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const target = event.target;
|
|
16
|
-
if (!target.closest("[data-mobile-nav]")) {
|
|
17
|
-
setIsMobileOpen(false);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
document.addEventListener("click", handleClickOutside);
|
|
21
|
-
return () => document.removeEventListener("click", handleClickOutside);
|
|
22
|
-
}, [isMobileOpen]);
|
|
11
|
+
// Close mobile menu when clicking outside the wrapper (toggle + dropdown);
|
|
12
|
+
// React bails out on the redundant setState while already closed.
|
|
13
|
+
const mobileNavRef = useClickOutside(() => {
|
|
14
|
+
setIsMobileOpen(false);
|
|
15
|
+
});
|
|
23
16
|
// Vertical desktop navigation (explicit opt-in via orientation)
|
|
24
17
|
if (orientation === "vertical") {
|
|
25
|
-
return (_jsx("nav", { "data-
|
|
18
|
+
return (_jsx("nav", { "data-kala-component": "navigation", className: cn(orientation === "vertical"
|
|
26
19
|
? "hidden md:flex md:flex-col gap-2"
|
|
27
20
|
: "flex md:hidden flex-col gap-2", className), "aria-label": "Navigation", ...props, ref: ref, children: links.map((link) => (_jsx("a", { href: link.href, className: cn("text-sm font-medium px-3 py-2 rounded-md transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring", isActive(link.href)
|
|
28
21
|
? "bg-accent text-accent-foreground"
|
|
@@ -30,15 +23,12 @@ export function Navigation({ ref, className, links, orientation = "horizontal",
|
|
|
30
23
|
}
|
|
31
24
|
// Horizontal on desktop + stacked vertical list on mobile
|
|
32
25
|
if (mobileLayout === "vertical") {
|
|
33
|
-
return (_jsxs(_Fragment, { children: [_jsx("nav", { "data-
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsx("nav", { "data-kala-component": "navigation", className: cn("hidden md:flex flex-row items-center gap-6", className), "aria-label": "Main navigation", ...props, ref: ref, children: links.map((link) => (_jsx("a", { href: link.href, className: cn("text-sm font-medium transition-colors hover:text-primary outline-none focus-visible:underline", isActive(link.href) ? "text-primary" : "text-foreground"), "aria-current": isActive(link.href) ? "page" : undefined, children: link.label }, link.href))) }), _jsx("nav", { "data-kala-component": "navigation", className: cn("flex md:hidden flex-col gap-2", className), "aria-label": "Mobile navigation", children: links.map((link) => (_jsx("a", { href: link.href, className: cn("text-sm font-medium px-3 py-2 rounded-md transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring", isActive(link.href)
|
|
34
27
|
? "bg-accent text-accent-foreground"
|
|
35
28
|
: "text-foreground hover:bg-accent hover:text-accent-foreground"), "aria-current": isActive(link.href) ? "page" : undefined, children: link.label }, link.href))) })] }));
|
|
36
29
|
}
|
|
37
30
|
// Horizontal desktop + dropdown mobile
|
|
38
|
-
return (_jsxs(_Fragment, { children: [_jsx("nav", { "data-
|
|
39
|
-
e.stopPropagation();
|
|
40
|
-
setIsMobileOpen(!isMobileOpen);
|
|
41
|
-
}, className: "flex items-center gap-2 text-sm font-medium text-foreground hover:text-primary transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-md px-2 py-1", "aria-label": "Toggle mobile navigation", "aria-expanded": isMobileOpen, children: ["Menu", _jsx(ChevronDown, { className: cn("size-4 transition-transform", isMobileOpen && "rotate-180") })] }), isMobileOpen && (_jsx("nav", { "data-comp": "navigation", className: "mt-2 flex flex-col gap-2 bg-popover border rounded-md p-3 text-popover-foreground kala-surface-popover", "aria-label": "Mobile navigation", onClick: (e) => e.stopPropagation(), children: links.map((link) => (_jsx("a", { href: link.href, onClick: () => setIsMobileOpen(false), className: cn("text-sm font-medium px-3 py-2 rounded-md transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring", isActive(link.href)
|
|
31
|
+
return (_jsxs(_Fragment, { children: [_jsx("nav", { "data-kala-component": "navigation", className: cn("hidden md:flex flex-row items-center gap-6", className), "aria-label": "Main navigation", ...props, ref: ref, children: links.map((link) => (_jsx("a", { href: link.href, className: cn("text-sm font-medium transition-colors hover:text-primary outline-none focus-visible:underline", isActive(link.href) ? "text-primary" : "text-foreground"), "aria-current": isActive(link.href) ? "page" : undefined, children: link.label }, link.href))) }), _jsxs("div", { "data-kala-component": "navigation", className: "md:hidden", ref: mobileNavRef, children: [_jsxs("button", { type: "button", onClick: () => setIsMobileOpen(!isMobileOpen), className: "flex items-center gap-2 text-sm font-medium text-foreground hover:text-primary transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-md px-2 py-1", "aria-label": "Toggle mobile navigation", "aria-expanded": isMobileOpen, children: ["Menu", _jsx(ChevronDown, { className: cn("size-4 transition-transform", isMobileOpen && "rotate-180") })] }), isMobileOpen && (_jsx("nav", { className: "mt-2 flex flex-col gap-2 bg-popover border rounded-md p-3 text-popover-foreground kala-surface-popover", "aria-label": "Mobile navigation", children: links.map((link) => (_jsx("a", { href: link.href, onClick: () => setIsMobileOpen(false), className: cn("text-sm font-medium px-3 py-2 rounded-md transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring", isActive(link.href)
|
|
42
32
|
? "bg-accent text-accent-foreground"
|
|
43
33
|
: "text-foreground hover:bg-accent hover:text-accent-foreground"), "aria-current": isActive(link.href) ? "page" : undefined, children: link.label }, link.href))) }))] })] }));
|
|
44
34
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export interface NavigationLink {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
}
|
|
6
|
+
export interface NavigationProps extends React.HTMLAttributes<HTMLElement>, React.RefAttributes<HTMLElement> {
|
|
7
|
+
/** Array of navigation links */
|
|
8
|
+
links: NavigationLink[];
|
|
9
|
+
/** Orientation of the navigation. @default 'horizontal' */
|
|
10
|
+
orientation?: "horizontal" | "vertical";
|
|
11
|
+
/** Mobile layout style. @default 'dropdown' */
|
|
12
|
+
mobileLayout?: "dropdown" | "vertical";
|
|
13
|
+
/** Current pathname for active link detection */
|
|
14
|
+
pathname?: string;
|
|
15
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { SessionCard
|
|
1
|
+
export { SessionCard } from "./session-card";
|
|
2
|
+
export type { SessionCardProps, SessionData, } from "./session-card.types";
|
|
2
3
|
export { SessionCardSkeleton } from "./session-card-skeleton";
|