@fabio.caffarello/react-design-system 1.4.1 → 1.5.0

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 (35) hide show
  1. package/dist/index.cjs +4 -4
  2. package/dist/index.js +621 -431
  3. package/dist/ui/atoms/NavLink/NavLink.d.ts +5 -5
  4. package/dist/ui/atoms/NavLink/NavLink.stories.d.ts +1 -0
  5. package/dist/ui/atoms/SidebarItem/SidebarItem.d.ts +21 -0
  6. package/dist/ui/atoms/SidebarItem/SidebarItem.stories.d.ts +7 -0
  7. package/dist/ui/atoms/SidebarItem/SidebarItem.test.d.ts +1 -0
  8. package/dist/ui/atoms/index.d.ts +2 -0
  9. package/dist/ui/molecules/NavbarGroup/NavbarGroup.d.ts +23 -0
  10. package/dist/ui/molecules/NavbarGroup/NavbarGroup.stories.d.ts +8 -0
  11. package/dist/ui/molecules/NavbarGroup/NavbarGroup.test.d.ts +1 -0
  12. package/dist/ui/molecules/SidebarGroup/SidebarGroup.d.ts +20 -0
  13. package/dist/ui/molecules/SidebarHeader/SidebarHeader.d.ts +19 -0
  14. package/dist/ui/molecules/index.d.ts +6 -0
  15. package/dist/ui/organisms/Sidebar/Sidebar.d.ts +35 -0
  16. package/dist/ui/organisms/Sidebar/Sidebar.stories.d.ts +8 -0
  17. package/dist/ui/organisms/Sidebar/Sidebar.test.d.ts +1 -0
  18. package/dist/ui/organisms/index.d.ts +2 -0
  19. package/package.json +1 -1
  20. package/src/ui/atoms/NavLink/NavLink.stories.tsx +12 -0
  21. package/src/ui/atoms/NavLink/NavLink.tsx +9 -5
  22. package/src/ui/atoms/SidebarItem/SidebarItem.stories.tsx +55 -0
  23. package/src/ui/atoms/SidebarItem/SidebarItem.test.tsx +25 -0
  24. package/src/ui/atoms/SidebarItem/SidebarItem.tsx +61 -0
  25. package/src/ui/atoms/index.ts +3 -0
  26. package/src/ui/molecules/NavbarGroup/NavbarGroup.stories.tsx +62 -0
  27. package/src/ui/molecules/NavbarGroup/NavbarGroup.test.tsx +32 -0
  28. package/src/ui/molecules/NavbarGroup/NavbarGroup.tsx +71 -0
  29. package/src/ui/molecules/SidebarGroup/SidebarGroup.tsx +52 -0
  30. package/src/ui/molecules/SidebarHeader/SidebarHeader.tsx +80 -0
  31. package/src/ui/molecules/index.ts +9 -0
  32. package/src/ui/organisms/Sidebar/Sidebar.stories.tsx +117 -0
  33. package/src/ui/organisms/Sidebar/Sidebar.test.tsx +40 -0
  34. package/src/ui/organisms/Sidebar/Sidebar.tsx +83 -0
  35. package/src/ui/organisms/index.ts +3 -0
@@ -1,7 +1,8 @@
1
- import type { AnchorHTMLAttributes } from "react";
2
- interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
1
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
2
+ export interface NavLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
3
3
  variant?: "default" | "active" | "disabled";
4
- children: React.ReactNode;
4
+ icon?: ReactNode;
5
+ children: ReactNode;
5
6
  }
6
7
  /**
7
8
  * NavLink Component
@@ -16,5 +17,4 @@ interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
16
17
  * </NavLink>
17
18
  * ```
18
19
  */
19
- export default function NavLink({ variant, className, children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
20
- export {};
20
+ export default function NavLink({ variant, icon, className, children, ...props }: NavLinkProps): import("react/jsx-runtime").JSX.Element;
@@ -4,5 +4,6 @@ declare const meta: Meta<typeof NavLink>;
4
4
  export declare const Default: StoryObj<typeof NavLink>;
5
5
  export declare const Active: StoryObj<typeof NavLink>;
6
6
  export declare const Disabled: StoryObj<typeof NavLink>;
7
+ export declare const WithIcon: StoryObj<typeof NavLink>;
7
8
  export declare const NavigationBar: StoryObj<typeof NavLink>;
8
9
  export default meta;
@@ -0,0 +1,21 @@
1
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
2
+ export interface SidebarItemProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
3
+ href: string;
4
+ isActive?: boolean;
5
+ icon?: ReactNode;
6
+ children: ReactNode;
7
+ }
8
+ /**
9
+ * SidebarItem Component
10
+ *
11
+ * An individual navigation item within a sidebar.
12
+ * Follows Atomic Design principles as an Atom component.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <SidebarItem href="/epics" isActive={true} icon={<EpicIcon />}>
17
+ * Epics
18
+ * </SidebarItem>
19
+ * ```
20
+ */
21
+ export default function SidebarItem({ href, isActive, icon, children, className, ...props }: SidebarItemProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import SidebarItem from "./SidebarItem";
3
+ declare const meta: Meta<typeof SidebarItem>;
4
+ export declare const Default: StoryObj<typeof SidebarItem>;
5
+ export declare const Active: StoryObj<typeof SidebarItem>;
6
+ export declare const WithIcon: StoryObj<typeof SidebarItem>;
7
+ export default meta;
@@ -0,0 +1 @@
1
+ export {};
@@ -13,3 +13,5 @@ export { default as Tooltip } from "./Tooltip/Tooltip";
13
13
  export type { TooltipProps } from "./Tooltip/Tooltip";
14
14
  export { default as Skeleton } from "./Skeleton/Skeleton";
15
15
  export type { SkeletonProps } from "./Skeleton/Skeleton";
16
+ export { default as SidebarItem } from "./SidebarItem/SidebarItem";
17
+ export type { SidebarItemProps } from "./SidebarItem/SidebarItem";
@@ -0,0 +1,23 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+ export interface NavbarGroupProps extends ButtonHTMLAttributes<HTMLButtonElement> {
3
+ label: string;
4
+ isActive?: boolean;
5
+ icon?: ReactNode;
6
+ children?: ReactNode;
7
+ }
8
+ /**
9
+ * NavbarGroup Component
10
+ *
11
+ * A clickable group in the navbar that can expand a sidebar.
12
+ * Follows Atomic Design principles as a Molecule component.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <NavbarGroup
17
+ * label="Agile"
18
+ * isActive={activeGroup === 'agile'}
19
+ * onClick={() => setActiveGroup('agile')}
20
+ * />
21
+ * ```
22
+ */
23
+ export default function NavbarGroup({ label, isActive, icon, onClick, className, children, ...props }: NavbarGroupProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import NavbarGroup from "./NavbarGroup";
3
+ declare const meta: Meta<typeof NavbarGroup>;
4
+ export declare const Default: StoryObj<typeof NavbarGroup>;
5
+ export declare const Active: StoryObj<typeof NavbarGroup>;
6
+ export declare const WithIcon: StoryObj<typeof NavbarGroup>;
7
+ export declare const NavigationBar: StoryObj<typeof NavbarGroup>;
8
+ export default meta;
@@ -0,0 +1,20 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export interface SidebarGroupProps extends HTMLAttributes<HTMLDivElement> {
3
+ title?: string;
4
+ children: ReactNode;
5
+ }
6
+ /**
7
+ * SidebarGroup Component
8
+ *
9
+ * A group container for sidebar items with optional title.
10
+ * Follows Atomic Design principles as a Molecule component.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <SidebarGroup title="Agile">
15
+ * <SidebarItem href="/epics">Epics</SidebarItem>
16
+ * <SidebarItem href="/stories">Stories</SidebarItem>
17
+ * </SidebarGroup>
18
+ * ```
19
+ */
20
+ export default function SidebarGroup({ title, children, className, ...props }: SidebarGroupProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export interface SidebarHeaderProps extends HTMLAttributes<HTMLDivElement> {
3
+ title: string;
4
+ onClose?: () => void;
5
+ showCloseButton?: boolean;
6
+ children?: ReactNode;
7
+ }
8
+ /**
9
+ * SidebarHeader Component
10
+ *
11
+ * Header section of a sidebar with title and optional close button.
12
+ * Follows Atomic Design principles as a Molecule component.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <SidebarHeader title="Navigation" onClose={handleClose} />
17
+ * ```
18
+ */
19
+ export default function SidebarHeader({ title, onClose, showCloseButton, children, className, ...props }: SidebarHeaderProps): import("react/jsx-runtime").JSX.Element;
@@ -8,3 +8,9 @@ export { default as EmptyState } from "./EmptyState/EmptyState";
8
8
  export type { EmptyStateProps } from "./EmptyState/EmptyState";
9
9
  export { default as Dropdown } from "./Dropdown/Dropdown";
10
10
  export type { DropdownProps, DropdownItem } from "./Dropdown/Dropdown";
11
+ export { default as SidebarGroup } from "./SidebarGroup/SidebarGroup";
12
+ export type { SidebarGroupProps } from "./SidebarGroup/SidebarGroup";
13
+ export { default as SidebarHeader } from "./SidebarHeader/SidebarHeader";
14
+ export type { SidebarHeaderProps } from "./SidebarHeader/SidebarHeader";
15
+ export { default as NavbarGroup } from "./NavbarGroup/NavbarGroup";
16
+ export type { NavbarGroupProps } from "./NavbarGroup/NavbarGroup";
@@ -0,0 +1,35 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import SidebarHeader from "../../molecules/SidebarHeader/SidebarHeader";
3
+ import SidebarGroup from "../../molecules/SidebarGroup/SidebarGroup";
4
+ import SidebarItem from "../../atoms/SidebarItem/SidebarItem";
5
+ export interface SidebarProps extends HTMLAttributes<HTMLDivElement> {
6
+ variant?: "default" | "collapsed";
7
+ title?: string;
8
+ showHeader?: boolean;
9
+ onClose?: () => void;
10
+ children: ReactNode;
11
+ }
12
+ /**
13
+ * Sidebar Component
14
+ *
15
+ * A sidebar navigation component with header, groups, and items.
16
+ * Follows Atomic Design principles as an Organism component.
17
+ * Uses Compound Components pattern.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <Sidebar title="Navigation" variant="default">
22
+ * <Sidebar.Group title="Agile">
23
+ * <Sidebar.Item href="/epics" isActive>Epics</Sidebar.Item>
24
+ * <Sidebar.Item href="/stories">Stories</Sidebar.Item>
25
+ * </Sidebar.Group>
26
+ * </Sidebar>
27
+ * ```
28
+ */
29
+ declare function SidebarComponent({ variant, title, showHeader, onClose, children, className, ...props }: SidebarProps): import("react/jsx-runtime").JSX.Element;
30
+ declare namespace SidebarComponent {
31
+ var Group: typeof SidebarGroup;
32
+ var Item: typeof SidebarItem;
33
+ var Header: typeof SidebarHeader;
34
+ }
35
+ export default SidebarComponent;
@@ -0,0 +1,8 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import Sidebar from "./Sidebar";
3
+ declare const meta: Meta<typeof Sidebar>;
4
+ export declare const Default: StoryObj<typeof Sidebar>;
5
+ export declare const WithIcons: StoryObj<typeof Sidebar>;
6
+ export declare const Collapsed: StoryObj<typeof Sidebar>;
7
+ export declare const WithoutHeader: StoryObj<typeof Sidebar>;
8
+ export default meta;
@@ -0,0 +1 @@
1
+ export {};
@@ -2,3 +2,5 @@ export { default as LoginBox } from "./LoginBox/LoginBox";
2
2
  export { default as Modal } from "./Modal/Modal";
3
3
  export { default as Table } from "./Table/Table";
4
4
  export type { TableColumn } from "./Table/Table";
5
+ export { default as Sidebar } from "./Sidebar/Sidebar";
6
+ export type { SidebarProps } from "./Sidebar/Sidebar";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fabio.caffarello/react-design-system",
3
3
  "private": false,
4
- "version": "1.4.1",
4
+ "version": "1.5.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -46,6 +46,18 @@ export const Disabled: StoryObj<typeof NavLink> = {
46
46
  },
47
47
  };
48
48
 
49
+ export const WithIcon: StoryObj<typeof NavLink> = {
50
+ args: {
51
+ children: "Dashboard",
52
+ href: "/dashboard",
53
+ icon: (
54
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
55
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
56
+ </svg>
57
+ ),
58
+ },
59
+ };
60
+
49
61
  export const NavigationBar: StoryObj<typeof NavLink> = {
50
62
  render: () => (
51
63
  <nav className="flex space-x-8">
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import type { AnchorHTMLAttributes } from "react";
3
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
4
4
 
5
- interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
5
+ export interface NavLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
6
6
  variant?: "default" | "active" | "disabled";
7
- children: React.ReactNode;
7
+ icon?: ReactNode;
8
+ children: ReactNode;
8
9
  }
9
10
 
10
11
  /**
@@ -22,10 +23,11 @@ interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
22
23
  */
23
24
  export default function NavLink({
24
25
  variant = "default",
26
+ icon,
25
27
  className = "",
26
28
  children,
27
29
  ...props
28
- }: Props) {
30
+ }: NavLinkProps) {
29
31
  const baseClasses = [
30
32
  "inline-flex",
31
33
  "items-center",
@@ -37,7 +39,7 @@ export default function NavLink({
37
39
  "transition-colors",
38
40
  ];
39
41
 
40
- const variantClasses: Record<NonNullable<Props["variant"]>, string> = {
42
+ const variantClasses: Record<NonNullable<NavLinkProps["variant"]>, string> = {
41
43
  default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
42
44
  active: "border-indigo-500 text-gray-900",
43
45
  disabled: "border-transparent text-gray-300 cursor-not-allowed pointer-events-none",
@@ -52,6 +54,7 @@ export default function NavLink({
52
54
  if (variant === "disabled") {
53
55
  return (
54
56
  <span className={classes} aria-disabled="true">
57
+ {icon && <span className="flex-shrink-0">{icon}</span>}
55
58
  {children}
56
59
  </span>
57
60
  );
@@ -59,6 +62,7 @@ export default function NavLink({
59
62
 
60
63
  return (
61
64
  <a className={classes} {...props}>
65
+ {icon && <span className="flex-shrink-0">{icon}</span>}
62
66
  {children}
63
67
  </a>
64
68
  );
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import SidebarItem from "./SidebarItem";
3
+
4
+ const meta: Meta<typeof SidebarItem> = {
5
+ title: "UI/Atoms/SidebarItem",
6
+ component: SidebarItem,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: "An individual navigation item within a sidebar.",
11
+ },
12
+ },
13
+ },
14
+ argTypes: {
15
+ isActive: {
16
+ control: "boolean",
17
+ description: "Whether the item is currently active",
18
+ },
19
+ href: {
20
+ control: "text",
21
+ description: "URL for the navigation item",
22
+ },
23
+ },
24
+ };
25
+
26
+ export const Default: StoryObj<typeof SidebarItem> = {
27
+ args: {
28
+ href: "/epics",
29
+ children: "Epics",
30
+ isActive: false,
31
+ },
32
+ };
33
+
34
+ export const Active: StoryObj<typeof SidebarItem> = {
35
+ args: {
36
+ href: "/epics",
37
+ children: "Epics",
38
+ isActive: true,
39
+ },
40
+ };
41
+
42
+ export const WithIcon: StoryObj<typeof SidebarItem> = {
43
+ args: {
44
+ href: "/epics",
45
+ children: "Epics",
46
+ isActive: false,
47
+ icon: (
48
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
50
+ </svg>
51
+ ),
52
+ },
53
+ };
54
+
55
+ export default meta;
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import SidebarItem from "./SidebarItem";
4
+
5
+ describe("SidebarItem", () => {
6
+ it("renders with href and children", () => {
7
+ render(<SidebarItem href="/epics">Epics</SidebarItem>);
8
+ const link = screen.getByRole("link", { name: /epics/i });
9
+ expect(link).toBeInTheDocument();
10
+ expect(link).toHaveAttribute("href", "/epics");
11
+ });
12
+
13
+ it("applies active styles when isActive is true", () => {
14
+ render(<SidebarItem href="/epics" isActive>Epics</SidebarItem>);
15
+ const link = screen.getByRole("link");
16
+ expect(link.className).toContain("bg-indigo-50");
17
+ expect(link.className).toContain("text-indigo-700");
18
+ });
19
+
20
+ it("renders icon when provided", () => {
21
+ const icon = <span data-testid="icon">Icon</span>;
22
+ render(<SidebarItem href="/epics" icon={icon}>Epics</SidebarItem>);
23
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
24
+ });
25
+ });
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
4
+
5
+ export interface SidebarItemProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
6
+ href: string;
7
+ isActive?: boolean;
8
+ icon?: ReactNode;
9
+ children: ReactNode;
10
+ }
11
+
12
+ /**
13
+ * SidebarItem Component
14
+ *
15
+ * An individual navigation item within a sidebar.
16
+ * Follows Atomic Design principles as an Atom component.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <SidebarItem href="/epics" isActive={true} icon={<EpicIcon />}>
21
+ * Epics
22
+ * </SidebarItem>
23
+ * ```
24
+ */
25
+ export default function SidebarItem({
26
+ href,
27
+ isActive = false,
28
+ icon,
29
+ children,
30
+ className = "",
31
+ ...props
32
+ }: SidebarItemProps) {
33
+ const baseClasses = [
34
+ "flex",
35
+ "items-center",
36
+ "px-4",
37
+ "py-2",
38
+ "text-sm",
39
+ "font-medium",
40
+ "rounded-md",
41
+ "transition-colors",
42
+ "hover:bg-gray-100",
43
+ ];
44
+
45
+ const activeClasses = isActive
46
+ ? "bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600"
47
+ : "text-gray-700 hover:text-gray-900";
48
+
49
+ const classes = [
50
+ ...baseClasses,
51
+ activeClasses,
52
+ className,
53
+ ].filter(Boolean).join(" ");
54
+
55
+ return (
56
+ <a href={href} className={classes} {...props}>
57
+ {icon && <span className="mr-3 flex-shrink-0">{icon}</span>}
58
+ <span>{children}</span>
59
+ </a>
60
+ );
61
+ }
@@ -25,3 +25,6 @@ export type { TooltipProps } from "./Tooltip/Tooltip";
25
25
 
26
26
  export { default as Skeleton } from "./Skeleton/Skeleton";
27
27
  export type { SkeletonProps } from "./Skeleton/Skeleton";
28
+
29
+ export { default as SidebarItem } from "./SidebarItem/SidebarItem";
30
+ export type { SidebarItemProps } from "./SidebarItem/SidebarItem";
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import NavbarGroup from "./NavbarGroup";
3
+
4
+ const meta: Meta<typeof NavbarGroup> = {
5
+ title: "UI/Molecules/NavbarGroup",
6
+ component: NavbarGroup,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: "A clickable group in the navbar that can expand a sidebar.",
11
+ },
12
+ },
13
+ },
14
+ argTypes: {
15
+ label: {
16
+ control: "text",
17
+ description: "Label text for the group",
18
+ },
19
+ isActive: {
20
+ control: "boolean",
21
+ description: "Whether the group is currently active",
22
+ },
23
+ },
24
+ };
25
+
26
+ export const Default: StoryObj<typeof NavbarGroup> = {
27
+ args: {
28
+ label: "Agile",
29
+ isActive: false,
30
+ },
31
+ };
32
+
33
+ export const Active: StoryObj<typeof NavbarGroup> = {
34
+ args: {
35
+ label: "Agile",
36
+ isActive: true,
37
+ },
38
+ };
39
+
40
+ export const WithIcon: StoryObj<typeof NavbarGroup> = {
41
+ args: {
42
+ label: "Agile",
43
+ isActive: false,
44
+ icon: (
45
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
47
+ </svg>
48
+ ),
49
+ },
50
+ };
51
+
52
+ export const NavigationBar: StoryObj<typeof NavbarGroup> = {
53
+ render: () => (
54
+ <nav className="flex space-x-4 bg-white p-4 border-b">
55
+ <NavbarGroup label="Dashboard" isActive={false} />
56
+ <NavbarGroup label="Agile" isActive={true} />
57
+ <NavbarGroup label="Documentação" isActive={false} />
58
+ </nav>
59
+ ),
60
+ };
61
+
62
+ export default meta;
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import NavbarGroup from "./NavbarGroup";
5
+
6
+ describe("NavbarGroup", () => {
7
+ it("renders with label", () => {
8
+ render(<NavbarGroup label="Agile" />);
9
+ expect(screen.getByRole("button", { name: /agile/i })).toBeInTheDocument();
10
+ });
11
+
12
+ it("applies active styles when isActive is true", () => {
13
+ render(<NavbarGroup label="Agile" isActive />);
14
+ const button = screen.getByRole("button");
15
+ expect(button.className).toContain("bg-indigo-100");
16
+ expect(button.className).toContain("text-indigo-700");
17
+ });
18
+
19
+ it("calls onClick when clicked", async () => {
20
+ const handleClick = vi.fn();
21
+ render(<NavbarGroup label="Agile" onClick={handleClick} />);
22
+ const button = screen.getByRole("button");
23
+ await userEvent.click(button);
24
+ expect(handleClick).toHaveBeenCalledTimes(1);
25
+ });
26
+
27
+ it("renders icon when provided", () => {
28
+ const icon = <span data-testid="icon">Icon</span>;
29
+ render(<NavbarGroup label="Agile" icon={icon} />);
30
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
31
+ });
32
+ });
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
4
+
5
+ export interface NavbarGroupProps extends ButtonHTMLAttributes<HTMLButtonElement> {
6
+ label: string;
7
+ isActive?: boolean;
8
+ icon?: ReactNode;
9
+ children?: ReactNode;
10
+ }
11
+
12
+ /**
13
+ * NavbarGroup Component
14
+ *
15
+ * A clickable group in the navbar that can expand a sidebar.
16
+ * Follows Atomic Design principles as a Molecule component.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <NavbarGroup
21
+ * label="Agile"
22
+ * isActive={activeGroup === 'agile'}
23
+ * onClick={() => setActiveGroup('agile')}
24
+ * />
25
+ * ```
26
+ */
27
+ export default function NavbarGroup({
28
+ label,
29
+ isActive = false,
30
+ icon,
31
+ onClick,
32
+ className = "",
33
+ children,
34
+ ...props
35
+ }: NavbarGroupProps) {
36
+ const baseClasses = [
37
+ "inline-flex",
38
+ "items-center",
39
+ "px-3",
40
+ "py-2",
41
+ "text-sm",
42
+ "font-medium",
43
+ "rounded-md",
44
+ "transition-colors",
45
+ ];
46
+
47
+ const variantClasses = isActive
48
+ ? "bg-indigo-100 text-indigo-700 border-b-2 border-indigo-600"
49
+ : "text-gray-600 hover:bg-gray-100 hover:text-gray-900";
50
+
51
+ const classes = [
52
+ ...baseClasses,
53
+ variantClasses,
54
+ className,
55
+ ].filter(Boolean).join(" ");
56
+
57
+ return (
58
+ <button
59
+ type="button"
60
+ className={classes}
61
+ onClick={onClick}
62
+ aria-expanded={isActive}
63
+ aria-haspopup="true"
64
+ {...props}
65
+ >
66
+ {icon && <span className="mr-2">{icon}</span>}
67
+ <span>{label}</span>
68
+ {children}
69
+ </button>
70
+ );
71
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import type { HTMLAttributes, ReactNode } from "react";
4
+ import { Text } from "../../atoms";
5
+
6
+ export interface SidebarGroupProps extends HTMLAttributes<HTMLDivElement> {
7
+ title?: string;
8
+ children: ReactNode;
9
+ }
10
+
11
+ /**
12
+ * SidebarGroup Component
13
+ *
14
+ * A group container for sidebar items with optional title.
15
+ * Follows Atomic Design principles as a Molecule component.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <SidebarGroup title="Agile">
20
+ * <SidebarItem href="/epics">Epics</SidebarItem>
21
+ * <SidebarItem href="/stories">Stories</SidebarItem>
22
+ * </SidebarGroup>
23
+ * ```
24
+ */
25
+ export default function SidebarGroup({
26
+ title,
27
+ children,
28
+ className = "",
29
+ ...props
30
+ }: SidebarGroupProps) {
31
+ const baseClasses = [
32
+ "space-y-1",
33
+ ];
34
+
35
+ const classes = [
36
+ ...baseClasses,
37
+ className,
38
+ ].filter(Boolean).join(" ");
39
+
40
+ return (
41
+ <div className={classes} {...props}>
42
+ {title && (
43
+ <Text as="h3" className="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
44
+ {title}
45
+ </Text>
46
+ )}
47
+ <div className="space-y-1">
48
+ {children}
49
+ </div>
50
+ </div>
51
+ );
52
+ }