@js-empire/emperor-ui 1.0.1 → 1.1.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 (166) hide show
  1. package/.husky/pre-commit +4 -0
  2. package/.storybook/main.ts +2 -11
  3. package/.storybook/preview.ts +1 -1
  4. package/.vscode/extensions.json +3 -0
  5. package/README.md +68 -1
  6. package/package.json +16 -4
  7. package/public/icons/emperor-ui-logo.ico +0 -0
  8. package/public/images/avatar-female.jpg +0 -0
  9. package/public/images/avatar-male.jpg +0 -0
  10. package/public/images/emperor-ui-logo.png +0 -0
  11. package/src/components/atoms/brand/brand.stories.tsx +27 -0
  12. package/src/components/atoms/brand/brand.tsx +56 -0
  13. package/src/components/atoms/brand/index.ts +1 -0
  14. package/src/components/atoms/brand/styles/classes.ts +9 -0
  15. package/src/components/atoms/brand/styles/index.ts +2 -0
  16. package/src/components/atoms/brand/styles/styles.ts +0 -0
  17. package/src/components/atoms/column/column.stories.tsx +36 -0
  18. package/src/components/atoms/column/column.tsx +21 -0
  19. package/src/components/atoms/column/index.ts +1 -0
  20. package/src/components/atoms/container/column.stories.tsx +36 -0
  21. package/src/components/atoms/container/container.tsx +28 -0
  22. package/src/components/atoms/container/index.ts +1 -0
  23. package/src/components/atoms/index.ts +6 -0
  24. package/src/components/atoms/portal/index.ts +1 -0
  25. package/src/components/atoms/portal/portal.stories.tsx +43 -0
  26. package/src/components/atoms/portal/portal.tsx +23 -0
  27. package/src/components/atoms/row/index.ts +1 -0
  28. package/src/components/atoms/row/row.stories.tsx +36 -0
  29. package/src/components/atoms/row/row.tsx +26 -0
  30. package/src/components/atoms/uploader/avatar-label.tsx +83 -0
  31. package/src/components/atoms/uploader/index.ts +7 -0
  32. package/src/components/atoms/uploader/stories/uploader.stories.tsx +41 -0
  33. package/src/components/atoms/uploader/upload-file-error-box.tsx +29 -0
  34. package/src/components/atoms/uploader/upload-file-input.tsx +36 -0
  35. package/src/components/atoms/uploader/upload-file-label.tsx +74 -0
  36. package/src/components/atoms/uploader/upload-file-listing.tsx +53 -0
  37. package/src/components/atoms/uploader/uploader.tsx +55 -0
  38. package/src/components/atoms/uploader/view-image-modal.tsx +39 -0
  39. package/src/components/index.ts +4 -8
  40. package/src/components/molecules/index.ts +5 -0
  41. package/src/components/molecules/item-card/item-card.tsx +6 -0
  42. package/src/components/molecules/nav-bar/index.ts +3 -0
  43. package/src/components/molecules/nav-bar/nav-bar-item.tsx +70 -0
  44. package/src/components/molecules/nav-bar/nav-bar.tsx +65 -0
  45. package/src/components/molecules/nav-bar/stories/hover-effect/nav-bar-hover-effect.stories.tsx +52 -0
  46. package/src/components/molecules/nav-bar/stories/nav-bar.stories.tsx +50 -0
  47. package/src/components/molecules/nav-bar/styles/classes.ts +68 -0
  48. package/src/components/molecules/nav-bar/styles/index.ts +2 -0
  49. package/src/components/molecules/nav-bar/styles/styles.ts +84 -0
  50. package/src/components/molecules/nav-bar/sub-items-box.tsx +57 -0
  51. package/src/components/molecules/scaffold/scaffold.stories.tsx +21 -0
  52. package/src/components/molecules/scaffold/scaffold.tsx +36 -0
  53. package/src/components/molecules/side-bar/compact-side-bar.tsx +73 -0
  54. package/src/components/molecules/side-bar/index.ts +1 -0
  55. package/src/components/molecules/side-bar/side-bar-drawer.tsx +124 -0
  56. package/src/components/molecules/side-bar/side-bar.stories.tsx +110 -0
  57. package/src/components/molecules/side-bar/side-bar.tsx +31 -0
  58. package/src/components/molecules/side-bar/styles/classes.ts +28 -0
  59. package/src/components/molecules/side-bar/styles/index.ts +2 -0
  60. package/src/components/molecules/side-bar/styles/styles.ts +13 -0
  61. package/src/components/organisms/footer/footer.tsx +20 -0
  62. package/src/components/organisms/footer/styles/classes.ts +15 -0
  63. package/src/components/organisms/footer/styles/index.ts +2 -0
  64. package/src/components/organisms/footer/styles/styles.ts +9 -0
  65. package/src/components/organisms/header/header.tsx +94 -0
  66. package/src/components/organisms/header/segmented-header-content.tsx +37 -0
  67. package/src/components/organisms/header/stories/header.stories.tsx +143 -0
  68. package/src/components/organisms/header/styles/classes.ts +22 -0
  69. package/src/components/organisms/header/styles/index.ts +2 -0
  70. package/src/components/organisms/header/styles/styles.ts +39 -0
  71. package/src/components/organisms/index.ts +4 -0
  72. package/src/components/templates/index.ts +1 -0
  73. package/src/components/templates/landing-page/index.ts +1 -0
  74. package/src/components/templates/landing-page/landing-page.stories.tsx +21 -0
  75. package/src/components/templates/landing-page/landing-page.tsx +57 -0
  76. package/src/components/templates/landing-page/styles/classes.ts +11 -0
  77. package/src/components/templates/landing-page/styles/index.ts +1 -0
  78. package/src/constants/defaults.ts +42 -7
  79. package/src/constants/fake.ts +2 -0
  80. package/src/constants/index.ts +2 -0
  81. package/src/constants/uploader.ts +27 -0
  82. package/src/context/emperor-ui-context.ts +4 -4
  83. package/src/context/index.ts +2 -0
  84. package/src/context/navigation-context.ts +6 -0
  85. package/src/context/uploader-context.ts +6 -0
  86. package/src/enums/index.ts +2 -0
  87. package/src/enums/placeholders.ts +4 -0
  88. package/src/enums/preserved-keys.ts +3 -0
  89. package/src/hooks/index.ts +3 -0
  90. package/src/hooks/use-navigation.ts +12 -0
  91. package/src/hooks/use-uploader-context.ts +14 -0
  92. package/src/hooks/use-uploader.tsx +215 -0
  93. package/src/index.ts +9 -5
  94. package/src/main.tsx +3 -0
  95. package/src/mocks/header.tsx +118 -0
  96. package/src/mocks/index.ts +1 -0
  97. package/src/providers/config-provider.tsx +54 -0
  98. package/src/providers/emperor-ui-provider.tsx +17 -24
  99. package/src/providers/index.ts +3 -0
  100. package/src/providers/navigation-provider.tsx +42 -0
  101. package/src/providers/uploader-provider.tsx +53 -0
  102. package/src/styles/globals.css +13 -0
  103. package/src/styles/hero.ts +2 -0
  104. package/src/types/components/atoms/brand.ts +13 -0
  105. package/src/types/components/atoms/column.ts +3 -0
  106. package/src/types/components/atoms/container.ts +3 -0
  107. package/src/types/components/atoms/index.ts +6 -0
  108. package/src/types/components/atoms/portal.ts +6 -0
  109. package/src/types/components/atoms/row.ts +3 -0
  110. package/src/types/components/atoms/uploader.ts +104 -0
  111. package/src/types/components/index.ts +3 -8
  112. package/src/types/components/molecules/header/header.ts +51 -0
  113. package/src/types/components/molecules/index.ts +9 -0
  114. package/src/types/components/molecules/nav-bar/nav-bar.ts +65 -0
  115. package/src/types/components/molecules/side-bar/index.ts +1 -0
  116. package/src/types/components/molecules/side-bar/side-bar.ts +40 -0
  117. package/src/types/components/templates/index.ts +1 -0
  118. package/src/types/components/templates/landing-page.ts +10 -0
  119. package/src/types/context/config.ts +54 -0
  120. package/src/types/context/index.ts +2 -1
  121. package/src/types/context/navigation.ts +17 -0
  122. package/src/types/shared/components.ts +4 -0
  123. package/src/utils/compress-images.ts +36 -0
  124. package/src/utils/index.ts +2 -0
  125. package/src/utils/storybook.tsx +15 -0
  126. package/tsconfig.app.json +3 -2
  127. package/tsconfig.node.json +0 -1
  128. package/vite.config.ts +4 -0
  129. package/dist/emperor-ui.js +0 -3171
  130. package/dist/emperor-ui.umd.cjs +0 -6
  131. package/dist/index.d.ts +0 -200
  132. package/src/components/footer/footer.tsx +0 -6
  133. package/src/components/header/header.tsx +0 -49
  134. package/src/components/item-card/item-card.tsx +0 -6
  135. package/src/components/nav-bar/index.ts +0 -1
  136. package/src/components/nav-bar/nav-bar.tsx +0 -6
  137. package/src/components/scaffold/scaffold.tsx +0 -15
  138. package/src/index.css +0 -1
  139. package/src/types/components/header/header.ts +0 -21
  140. package/src/types/components/nav-bar/nav-bar.ts +0 -9
  141. package/src/types/context/emperor-ui.ts +0 -37
  142. package/tailwind.config.js +0 -6
  143. /package/src/components/{filter → molecules/filter}/filter.tsx +0 -0
  144. /package/src/components/{filter → molecules/filter}/index.ts +0 -0
  145. /package/src/components/{item-card → molecules/item-card}/index.ts +0 -0
  146. /package/src/components/{scaffold → molecules/scaffold}/index.ts +0 -0
  147. /package/src/components/{footer → organisms/footer}/index.ts +0 -0
  148. /package/src/components/{header → organisms/header}/index.ts +0 -0
  149. /package/src/components/{item-details → organisms/item-details}/index.ts +0 -0
  150. /package/src/components/{item-details → organisms/item-details}/item-details.tsx +0 -0
  151. /package/src/components/{listings → organisms/listings}/index.ts +0 -0
  152. /package/src/components/{listings → organisms/listings}/listings.tsx +0 -0
  153. /package/src/types/components/{filter → molecules/filter}/filter.ts +0 -0
  154. /package/src/types/components/{filter → molecules/filter}/index.ts +0 -0
  155. /package/src/types/components/{footer → molecules/footer}/footer.ts +0 -0
  156. /package/src/types/components/{footer → molecules/footer}/index.ts +0 -0
  157. /package/src/types/components/{header → molecules/header}/index.ts +0 -0
  158. /package/src/types/components/{item-card → molecules/item-card}/index.ts +0 -0
  159. /package/src/types/components/{item-card → molecules/item-card}/item-card.ts +0 -0
  160. /package/src/types/components/{item-details → molecules/item-details}/index.ts +0 -0
  161. /package/src/types/components/{item-details → molecules/item-details}/item-details.ts +0 -0
  162. /package/src/types/components/{listings → molecules/listings}/index.ts +0 -0
  163. /package/src/types/components/{listings → molecules/listings}/listings.ts +0 -0
  164. /package/src/types/components/{nav-bar → molecules/nav-bar}/index.ts +0 -0
  165. /package/src/types/components/{scaffold → molecules/scaffold}/index.ts +0 -0
  166. /package/src/types/components/{scaffold → molecules/scaffold}/scaffold.ts +0 -0
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Uploader } from "@components";
3
+ import { getStorybookDecorators } from "@utils";
4
+ import { UploaderProps } from "@types";
5
+ import { useUploader } from "@hooks";
6
+
7
+ const meta: Meta<typeof Uploader> = {
8
+ title: "Atoms/Uploader",
9
+ component: Uploader,
10
+ parameters: {
11
+ layout: "centered",
12
+ },
13
+ tags: ["autodocs"],
14
+ decorators: getStorybookDecorators({
15
+ config: {
16
+ layout: {
17
+ withScaffold: false,
18
+ },
19
+ },
20
+ }),
21
+ };
22
+
23
+ export default meta;
24
+
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ export const Default: Story = {
28
+ args: {},
29
+ render: (args: UploaderProps) => {
30
+ const uploadProps = useUploader({
31
+ fileTypes: ["image"],
32
+ labelId: "image",
33
+ maxCount: 1,
34
+ isRequired: false,
35
+ compressFiles: true,
36
+ isMulti: true,
37
+ });
38
+
39
+ return <Uploader {...uploadProps} {...args} />;
40
+ },
41
+ };
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { cn } from "@heroui/react";
4
+ import { useEmperorUI, useUploaderContext } from "@hooks";
5
+ import { useMemo } from "react";
6
+
7
+ export function UploadFileErrorBox() {
8
+ const { config } = useEmperorUI();
9
+ const { files, isRequired, classNames } = useUploaderContext();
10
+
11
+ const locales = config?.interLocalization?.locales;
12
+ const lang = config?.interLocalization?.lang;
13
+
14
+ const locale = locales?.[lang || "en"];
15
+
16
+ const isError = useMemo(
17
+ () => files?.length === 0 && isRequired,
18
+ [files, isRequired],
19
+ );
20
+
21
+ if (isError)
22
+ return (
23
+ <p className={cn("text-[14px] text-danger", classNames?.error)}>
24
+ {locale?.errorUploadingFile}
25
+ </p>
26
+ );
27
+
28
+ return null;
29
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { cn } from "@heroui/react";
4
+ import { useUploaderContext } from "@hooks";
5
+ import { fileTypesMapping } from "@constants";
6
+
7
+ export function UploadFileInput() {
8
+ const {
9
+ fileTypes = [],
10
+ isMulti,
11
+ labelId,
12
+ onInputChange,
13
+ classNames,
14
+ } = useUploaderContext();
15
+
16
+ const getFileAccepts = (): string => {
17
+ const acceptedTypes: string[] = [];
18
+
19
+ fileTypes?.forEach((fileType) => {
20
+ acceptedTypes.push(...(fileTypesMapping?.[fileType] || []));
21
+ });
22
+
23
+ return acceptedTypes.join(", ");
24
+ };
25
+
26
+ return (
27
+ <input
28
+ id={labelId}
29
+ type="file"
30
+ accept={getFileAccepts()}
31
+ className={cn("hidden", classNames?.input)}
32
+ onChange={onInputChange}
33
+ multiple={isMulti}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { Spinner, cn } from "@heroui/react";
4
+ import { useEmperorUI, useUploaderContext } from "@hooks";
5
+ import { UploadCloud } from "lucide-react";
6
+ import { useState } from "react";
7
+
8
+ export function UploadFileLabel() {
9
+ const { config } = useEmperorUI();
10
+ const [draggableMessage, setDraggableMessage] = useState("");
11
+ const {
12
+ labelId,
13
+ classNames,
14
+ labelContent,
15
+ isDraggable,
16
+ onInputChange,
17
+ isLoading,
18
+ } = useUploaderContext();
19
+
20
+ const locales = config?.interLocalization?.locales;
21
+ const lang = config?.interLocalization?.lang;
22
+
23
+ const locale = locales?.[lang || "en"];
24
+
25
+ const handleDrop = (
26
+ event: React.ChangeEvent<HTMLInputElement> &
27
+ React.DragEvent<HTMLLabelElement>,
28
+ ) => {
29
+ event.preventDefault();
30
+ if (onInputChange) onInputChange(event);
31
+ };
32
+
33
+ const handleDragOver = (
34
+ event: React.ChangeEvent<HTMLInputElement> &
35
+ React.DragEvent<HTMLLabelElement>,
36
+ ) => {
37
+ event.preventDefault();
38
+ setDraggableMessage(locale?.dropHere || "");
39
+ };
40
+
41
+ const handleDragLeave = () => {
42
+ setDraggableMessage("");
43
+ };
44
+
45
+ if (isLoading)
46
+ return (
47
+ <div className={cn("w-full flex mx-auto", classNames?.label)}>
48
+ <Spinner className="mx-auto" size="lg" />;
49
+ </div>
50
+ );
51
+
52
+ return (
53
+ <label
54
+ className={cn("w-full cursor-pointer mx-auto", classNames?.label)}
55
+ htmlFor={labelId}
56
+ onDrop={isDraggable ? handleDrop : () => {}}
57
+ onDragOver={isDraggable ? handleDragOver : () => {}}
58
+ onDragLeave={isDraggable ? handleDragLeave : () => {}}
59
+ >
60
+ {labelContent || (
61
+ <div className="pointer-events-none flex size-full flex-col items-center justify-center gap-2 rounded-md border border-dashed bg-primary/10 px-2 py-8 text-xs">
62
+ <UploadCloud className="size-10 text-primary" />
63
+
64
+ <p className="font-bold">{locale?.selectFile || ""}</p>
65
+ <p className="opacity-70">{locale?.selectionTypes || ""}</p>
66
+
67
+ {draggableMessage && (
68
+ <p className="text-sm font-bold">{draggableMessage}</p>
69
+ )}
70
+ </div>
71
+ )}
72
+ </label>
73
+ );
74
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { Button, cn } from "@heroui/react";
4
+ import { useUploaderContext } from "@hooks";
5
+ import { Eye, Trash2 } from "lucide-react";
6
+
7
+ export function UploadFileListing() {
8
+ const { files, setSelectedFile, handleClearFile, classNames, modal } =
9
+ useUploaderContext();
10
+
11
+ return files?.map((file) => {
12
+ const isFileViewable =
13
+ modal?.onOpen && file?.view && file?.file?.name && file?.type === "image";
14
+
15
+ if (file)
16
+ return (
17
+ <div
18
+ key={file?.file?.name}
19
+ className={cn(
20
+ "flex justify-between items-center p-2 gap-2 w-full border border-black/30 rounded-lg",
21
+ classNames?.listing,
22
+ )}
23
+ >
24
+ <p className="w-full text-ellipsis text-xs">{file?.file?.name}</p>
25
+
26
+ <Button
27
+ isIconOnly
28
+ variant="flat"
29
+ className="size-8 min-w-8 rounded-full"
30
+ color="danger"
31
+ onPress={() => handleClearFile(file?.file?.name)}
32
+ startContent={<Trash2 className="rounded-lg" />}
33
+ />
34
+
35
+ {isFileViewable && (
36
+ <Button
37
+ isIconOnly
38
+ variant="flat"
39
+ className="size-8 min-w-8 rounded-full"
40
+ color="primary"
41
+ onPress={() => {
42
+ setSelectedFile?.(file);
43
+ modal?.onOpen?.();
44
+ }}
45
+ startContent={<Eye className="rounded-lg" />}
46
+ />
47
+ )}
48
+ </div>
49
+ );
50
+
51
+ return null;
52
+ });
53
+ }
@@ -0,0 +1,55 @@
1
+ import type { UploaderProps } from "@types";
2
+ import { cn } from "@utils";
3
+ import { cva, VariantProps } from "class-variance-authority";
4
+ import { forwardRef, ComponentProps } from "react";
5
+ import { UploaderProvider } from "@providers";
6
+ import {
7
+ AvatarLabel,
8
+ UploadFileLabel,
9
+ ViewImageModal,
10
+ UploadFileListing,
11
+ UploadFileErrorBox,
12
+ UploadFileInput,
13
+ } from "@components";
14
+
15
+ const uploaderStyles = cva(["w-full flex flex-col gap-2"], {
16
+ variants: {},
17
+ defaultVariants: {},
18
+ compoundVariants: [],
19
+ });
20
+
21
+ /**
22
+ * @usage
23
+ * ```
24
+ * const uploadProps = useUpload({
25
+ labelId: "uploaded-file",
26
+ fileTypes: ["image", "pdf"],
27
+ isRequired: true,
28
+ isMulti: true,
29
+ });
30
+
31
+ <Uploader {...uploadProps} />
32
+ * ```
33
+ */
34
+ export const Uploader = forwardRef<
35
+ HTMLDivElement,
36
+ ComponentProps<"div"> & VariantProps<typeof uploaderStyles> & UploaderProps
37
+ >(({ className, ...props }, ref) => {
38
+ const { isAvatar, hideListing, isFileViewable, hideErrorMessage } = props;
39
+
40
+ return (
41
+ <UploaderProvider {...props}>
42
+ <div ref={ref} className={cn(uploaderStyles({ className }))} {...props}>
43
+ {isAvatar ? <AvatarLabel /> : <UploadFileLabel />}
44
+
45
+ {!hideListing && <UploadFileListing />}
46
+
47
+ {!hideErrorMessage && <UploadFileErrorBox />}
48
+
49
+ <UploadFileInput />
50
+
51
+ {isFileViewable && <ViewImageModal />}
52
+ </div>
53
+ </UploaderProvider>
54
+ );
55
+ });
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { Image, Modal, ModalBody, ModalContent } from "@heroui/react";
4
+ import { useEmperorUI, useUploaderContext } from "@hooks";
5
+
6
+ export function ViewImageModal() {
7
+ const { config } = useEmperorUI();
8
+ const { isOpen, onClose, onOpenChange, selectedFile } = useUploaderContext();
9
+
10
+ const lang = config?.interLocalization?.lang || "en";
11
+
12
+ const src = selectedFile?.view;
13
+ const alt = selectedFile?.file?.name;
14
+
15
+ if (!src || !alt) return null;
16
+
17
+ return (
18
+ <Modal
19
+ placement="center"
20
+ isOpen={isOpen}
21
+ dir={lang === "ar" ? "rtl" : "ltr"}
22
+ onClose={onClose}
23
+ onOpenChange={onOpenChange}
24
+ size="xl"
25
+ >
26
+ <ModalContent className="px-5">
27
+ <ModalBody className="h-[60vh]">
28
+ <Image
29
+ className="size-full rounded-md object-cover"
30
+ src={src}
31
+ alt={alt}
32
+ width={500}
33
+ height={500}
34
+ />
35
+ </ModalBody>
36
+ </ModalContent>
37
+ </Modal>
38
+ );
39
+ }
@@ -1,8 +1,4 @@
1
- export * from "./filter";
2
- export * from "./footer";
3
- export * from "./header";
4
- export * from "./item-card";
5
- export * from "./item-details";
6
- export * from "./listings";
7
- export * from "./nav-bar";
8
- export * from "./scaffold";
1
+ export * from "./templates";
2
+ export * from "./molecules";
3
+ export * from "./atoms";
4
+ export * from "./organisms";
@@ -0,0 +1,5 @@
1
+ export * from "./filter";
2
+ export * from "./item-card";
3
+ export * from "./nav-bar";
4
+ export * from "./side-bar";
5
+ export * from "./scaffold";
@@ -0,0 +1,6 @@
1
+ import type { ItemCardProps } from "@types";
2
+ import { cn } from "@utils";
3
+
4
+ export function ItemCard({ className }: ItemCardProps) {
5
+ return <div className={cn("", className)}>Item Card Component</div>;
6
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./nav-bar";
2
+ export * from "./sub-items-box";
3
+ export * from "./nav-bar-item";
@@ -0,0 +1,70 @@
1
+ import { cn } from "@utils";
2
+ import { VariantProps } from "class-variance-authority";
3
+ import { forwardRef, ComponentProps } from "react";
4
+ import type { NavBarItemProps } from "@types";
5
+ import { navBarItemClasses, navBarItemStyles } from "./styles";
6
+ import { useEmperorUI, useNavigation } from "@hooks";
7
+
8
+ export const NavBarItem = forwardRef<
9
+ HTMLLIElement,
10
+ ComponentProps<"li"> &
11
+ VariantProps<typeof navBarItemClasses> &
12
+ NavBarItemProps
13
+ >(({ className, item, variant, hoverEffect, ...props }, ref) => {
14
+ const { config } = useEmperorUI();
15
+ const {
16
+ hoveredItemId,
17
+ subItemsBoxIsHovered,
18
+ setSubItems,
19
+ setHoveredItemId,
20
+ setIsSubItemsBoxOpen,
21
+ } = useNavigation();
22
+
23
+ const primaryColor = config?.theme?.colors?.primary;
24
+ const foregroundColor = config?.theme?.colors?.foreground;
25
+
26
+ const { id, label, Icon, subItems } = item;
27
+
28
+ const isHovered = hoveredItemId === id;
29
+
30
+ const handleHover = (id: string | null) => {
31
+ if (id) {
32
+ setHoveredItemId(id);
33
+
34
+ if (subItems && subItems.length > 0) {
35
+ setSubItems(subItems);
36
+ setIsSubItemsBoxOpen(true);
37
+ } else {
38
+ setIsSubItemsBoxOpen(false);
39
+ }
40
+ return;
41
+ }
42
+
43
+ setHoveredItemId(null);
44
+
45
+ if (!subItemsBoxIsHovered) {
46
+ setIsSubItemsBoxOpen(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <li
52
+ ref={ref}
53
+ data-slot="emperor-nav-bar-item"
54
+ style={navBarItemStyles({
55
+ foregroundColor,
56
+ primaryColor,
57
+ hoverEffect,
58
+ isHovered,
59
+ variant,
60
+ })}
61
+ className={cn(navBarItemClasses({ hoverEffect, variant }))}
62
+ onMouseEnter={() => handleHover(id)}
63
+ onMouseLeave={() => handleHover(null)}
64
+ {...props}
65
+ >
66
+ {Icon && <Icon className="size-4" />}
67
+ {label && <p>{label}</p>}
68
+ </li>
69
+ );
70
+ });
@@ -0,0 +1,65 @@
1
+ import { cn } from "@utils";
2
+ import { VariantProps } from "class-variance-authority";
3
+ import { forwardRef, ComponentProps } from "react";
4
+ import type { NavBarProps } from "@types";
5
+ import { useEmperorUI } from "@hooks";
6
+ import {
7
+ navBarClasses,
8
+ navBarMenuClasses,
9
+ navBarMenuStyles,
10
+ navBarStyles,
11
+ } from "./styles";
12
+ import { NavBarItem, SubItemsBox } from "@components";
13
+
14
+ export const NavBar = forwardRef<
15
+ HTMLDivElement,
16
+ ComponentProps<"nav"> & VariantProps<typeof navBarClasses> & NavBarProps
17
+ >(
18
+ (
19
+ {
20
+ className,
21
+ hoverEffect = "default",
22
+ variant = "default",
23
+ items = [],
24
+ subItemsColumns = 3,
25
+ ...props
26
+ },
27
+ ref,
28
+ ) => {
29
+ const { config } = useEmperorUI();
30
+
31
+ const primaryColor = config?.theme?.colors?.primary;
32
+ const foregroundColor = config?.theme?.colors?.foreground;
33
+
34
+ return (
35
+ <nav
36
+ ref={ref}
37
+ data-slot="emperor-nav-bar"
38
+ className={cn(navBarClasses({ hoverEffect, variant, className }))}
39
+ style={navBarStyles({
40
+ foregroundColor,
41
+ primaryColor,
42
+ variant,
43
+ })}
44
+ {...props}
45
+ >
46
+ <ul
47
+ className={cn(navBarMenuClasses({ className }))}
48
+ style={navBarMenuStyles({ hoverEffect, variant })}
49
+ data-slot="emperor-nav-bar-menu"
50
+ >
51
+ {items?.map((item) => (
52
+ <NavBarItem
53
+ key={item.id}
54
+ item={item}
55
+ variant={variant}
56
+ hoverEffect={hoverEffect}
57
+ />
58
+ ))}
59
+ </ul>
60
+
61
+ <SubItemsBox subItemsColumns={subItemsColumns} />
62
+ </nav>
63
+ );
64
+ },
65
+ );
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { NavBar } from "@components";
3
+ import { getStorybookDecorators } from "@utils";
4
+ import { MOCK_HEADER_ITEMS } from "@mocks";
5
+
6
+ const meta: Meta<typeof NavBar> = {
7
+ title: "Molecules/NavBar/HoverEffect",
8
+ component: NavBar,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ decorators: getStorybookDecorators({
14
+ config: {
15
+ layout: {
16
+ withScaffold: false,
17
+ },
18
+ },
19
+ }),
20
+ };
21
+
22
+ export default meta;
23
+
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const SolidHover: Story = {
27
+ args: {
28
+ items: MOCK_HEADER_ITEMS,
29
+ hoverEffect: "solid",
30
+ },
31
+ };
32
+
33
+ export const GhostHover: Story = {
34
+ args: {
35
+ items: MOCK_HEADER_ITEMS,
36
+ hoverEffect: "ghost",
37
+ },
38
+ };
39
+
40
+ export const BorderedHover: Story = {
41
+ args: {
42
+ items: MOCK_HEADER_ITEMS,
43
+ hoverEffect: "bordered",
44
+ },
45
+ };
46
+
47
+ export const UnderlinedHover: Story = {
48
+ args: {
49
+ items: MOCK_HEADER_ITEMS,
50
+ hoverEffect: "underline",
51
+ },
52
+ };
@@ -0,0 +1,50 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { NavBar } from "@components";
3
+ import { getStorybookDecorators } from "@utils";
4
+ import { MOCK_HEADER_ITEMS, MOCK_HEADER_ITEMS_WITH_SUB_ITEMS } from "@mocks";
5
+
6
+ const meta: Meta<typeof NavBar> = {
7
+ title: "Molecules/NavBar",
8
+ component: NavBar,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ decorators: getStorybookDecorators({
14
+ config: {
15
+ layout: {
16
+ withScaffold: false,
17
+ },
18
+ },
19
+ }),
20
+ };
21
+
22
+ export default meta;
23
+
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ args: {
28
+ items: MOCK_HEADER_ITEMS,
29
+ },
30
+ };
31
+
32
+ export const Solid: Story = {
33
+ args: {
34
+ items: MOCK_HEADER_ITEMS,
35
+ variant: "solid",
36
+ },
37
+ };
38
+
39
+ export const Bordered: Story = {
40
+ args: {
41
+ items: MOCK_HEADER_ITEMS,
42
+ variant: "bordered",
43
+ },
44
+ };
45
+
46
+ export const WithSubItems: Story = {
47
+ args: {
48
+ items: MOCK_HEADER_ITEMS_WITH_SUB_ITEMS,
49
+ },
50
+ };
@@ -0,0 +1,68 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const navBarClasses = cva(["relative flex items-center gap-3"], {
4
+ variants: {
5
+ hoverEffect: {
6
+ default: [],
7
+ solid: [],
8
+ underline: [],
9
+ ghost: [],
10
+ bordered: [],
11
+ none: [],
12
+ },
13
+ variant: {
14
+ default: [],
15
+ solid: [],
16
+ bordered: [],
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ hoverEffect: "default",
21
+ variant: "default",
22
+ },
23
+ });
24
+
25
+ export const navBarMenuClasses = cva(["size-full flex items-center"], {
26
+ variants: {
27
+ variant: {
28
+ default: [],
29
+ solid: [],
30
+ bordered: [],
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "default",
35
+ },
36
+ });
37
+
38
+ export const navBarItemClasses = cva(
39
+ [
40
+ "relative cursor-pointer px-4 py-2 transition-all font-semibold flex items-center gap-2",
41
+ ],
42
+ {
43
+ variants: {
44
+ hoverEffect: {
45
+ default: [],
46
+ solid: ["hover:opacity-80"],
47
+ underline: [
48
+ "relative font-bold",
49
+ "before:absolute before:bottom-0 before:left-0",
50
+ "before:h-0.5 before:w-full before:bg-current before:rounded-lg",
51
+ "before:scale-x-0 before:transition-transform hover:before:scale-x-100",
52
+ ],
53
+ ghost: [],
54
+ bordered: ["last:border-r-2! first:border-l-2!"],
55
+ none: [],
56
+ },
57
+ variant: {
58
+ default: [],
59
+ solid: [],
60
+ bordered: ["last:border-r-2! first:border-l-2!"],
61
+ },
62
+ },
63
+ defaultVariants: {
64
+ hoverEffect: "default",
65
+ variant: "default",
66
+ },
67
+ },
68
+ );
@@ -0,0 +1,2 @@
1
+ export * from "./classes";
2
+ export * from "./styles";