@sanvika/ui 0.1.5 → 0.2.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.
- package/package.json +15 -29
- package/src/components/buttons/Button.jsx +97 -0
- package/src/components/buttons/Button.stories.jsx +110 -0
- package/src/components/buttons/FavoriteHeartButton.jsx +41 -0
- package/src/components/buttons/FavoriteHeartButton.stories.jsx +48 -0
- package/src/components/buttons/ScrollButton.jsx +36 -0
- package/src/components/buttons/ScrollButton.stories.jsx +21 -0
- package/src/components/buttons/ThreeDotButton.jsx +36 -0
- package/src/components/buttons/ThreeDotButton.stories.jsx +14 -0
- package/src/components/common/Container.jsx +38 -0
- package/src/components/common/Section.jsx +60 -0
- package/src/components/common/Section.stories.jsx +64 -0
- package/src/components/icons/BellIcon.jsx +10 -0
- package/src/components/icons/ChevronDown.jsx +20 -0
- package/src/components/icons/EmailIcon.jsx +9 -0
- package/src/components/icons/FaAngleDown.jsx +9 -0
- package/src/components/icons/FaAngleRight.jsx +9 -0
- package/src/components/icons/FaArrowDown.jsx +9 -0
- package/src/components/icons/FaArrowLeft.jsx +9 -0
- package/src/components/icons/FaArrowUp.jsx +9 -0
- package/src/components/icons/FaBell.jsx +9 -0
- package/src/components/icons/FaBullhorn.jsx +10 -0
- package/src/components/icons/FaCalendarAlt.jsx +9 -0
- package/src/components/icons/FaCheck.jsx +9 -0
- package/src/components/icons/FaCheckCircle.jsx +9 -0
- package/src/components/icons/FaCheckDouble.jsx +9 -0
- package/src/components/icons/FaChevronDown.jsx +9 -0
- package/src/components/icons/FaChevronRight.jsx +9 -0
- package/src/components/icons/FaCircle.jsx +9 -0
- package/src/components/icons/FaComments.jsx +9 -0
- package/src/components/icons/FaEye.jsx +9 -0
- package/src/components/icons/FaEyeSlash.jsx +9 -0
- package/src/components/icons/FaHome.jsx +9 -0
- package/src/components/icons/FaKeyboard.jsx +9 -0
- package/src/components/icons/FaLocationArrow.jsx +9 -0
- package/src/components/icons/FaMapMarkerAlt.jsx +9 -0
- package/src/components/icons/FaMoon.jsx +9 -0
- package/src/components/icons/FaPaperPlane.jsx +9 -0
- package/src/components/icons/FaPaperclip.jsx +9 -0
- package/src/components/icons/FaSignInAlt.jsx +9 -0
- package/src/components/icons/FaSmile.jsx +9 -0
- package/src/components/icons/FaStar.jsx +9 -0
- package/src/components/icons/FaTag.jsx +9 -0
- package/src/components/icons/FaThumbsDown.jsx +9 -0
- package/src/components/icons/FaThumbsUp.jsx +9 -0
- package/src/components/icons/FaTrash.jsx +9 -0
- package/src/components/icons/FaUser.jsx +9 -0
- package/src/components/icons/FaUserCircle.jsx +9 -0
- package/src/components/icons/FacebookIcon.jsx +9 -0
- package/src/components/icons/HalfMoonIcon.jsx +18 -0
- package/src/components/icons/HeartIcon.jsx +9 -0
- package/src/components/icons/InstagramIcon.jsx +9 -0
- package/src/components/icons/LinkedInIcon.jsx +9 -0
- package/src/components/icons/MapMarkerIcon.jsx +9 -0
- package/src/components/icons/MdWbSunny.jsx +9 -0
- package/src/components/icons/ReFreshIcon.jsx +49 -0
- package/src/components/icons/SearchIcon.jsx +20 -0
- package/src/components/icons/StarIcon.jsx +9 -0
- package/src/components/icons/TelegramIcon.jsx +9 -0
- package/src/components/icons/TwitterIcon.jsx +9 -0
- package/src/components/icons/UserIcon.jsx +9 -0
- package/src/components/icons/WhatsappIcon.jsx +9 -0
- package/src/components/icons/YoutubeIcon.jsx +9 -0
- package/src/components/icons/index.js +60 -0
- package/src/components/layout/Footer.jsx +53 -0
- package/src/components/layout/Footer.stories.jsx +28 -0
- package/src/components/layout/Navbar.jsx +50 -0
- package/src/components/layout/Navbar.stories.jsx +42 -0
- package/src/components/layout/SubNavbar.jsx +36 -0
- package/src/components/modals/Modal.jsx +74 -0
- package/src/components/modals/Modal.stories.jsx +93 -0
- package/src/components/progressBar/ProgressBar.jsx +113 -0
- package/src/components/progressBar/ProgressBar.stories.jsx +67 -0
- package/src/context/ThemeContext.jsx +91 -0
- package/src/index.js +33 -0
- package/src/layouts/Layout.jsx +24 -0
- package/src/server/index.js +159 -0
- package/README.md +0 -36
- package/dist/EmailIcon-DumDw7u2.js +0 -781
- package/dist/EmailIcon-ssF1iAVu.js +0 -782
- package/dist/icons/index.js +0 -4
- package/dist/index.js +0 -555
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/components/icons/index.js
|
|
2
|
+
// SSOT: All icons exported from custom SVG files only.
|
|
3
|
+
// NO react-icons direct re-exports here — use custom .jsx files for all icons.
|
|
4
|
+
|
|
5
|
+
export { default as FaCheckCircle } from "./FaCheckCircle.jsx";
|
|
6
|
+
export { default as FaStar } from "./FaStar.jsx";
|
|
7
|
+
export { default as FaEye } from "./FaEye.jsx";
|
|
8
|
+
export { default as FaEyeSlash } from "./FaEyeSlash.jsx";
|
|
9
|
+
export { default as FaTrash } from "./FaTrash.jsx";
|
|
10
|
+
export { default as FaUser } from "./FaUser.jsx";
|
|
11
|
+
export { default as FaCalendarAlt } from "./FaCalendarAlt.jsx";
|
|
12
|
+
export { default as FaThumbsUp } from "./FaThumbsUp.jsx";
|
|
13
|
+
export { default as FaThumbsDown } from "./FaThumbsDown.jsx";
|
|
14
|
+
export { default as FaAngleRight } from "./FaAngleRight.jsx";
|
|
15
|
+
export { default as FaMapMarkerAlt } from "./FaMapMarkerAlt.jsx";
|
|
16
|
+
export { default as FaAngleDown } from "./FaAngleDown.jsx";
|
|
17
|
+
export { default as FaLocationArrow } from "./FaLocationArrow.jsx";
|
|
18
|
+
export { default as FaCheck } from "./FaCheck.jsx";
|
|
19
|
+
export { default as FaCheckDouble } from "./FaCheckDouble.jsx";
|
|
20
|
+
export { default as FaPaperPlane } from "./FaPaperPlane.jsx";
|
|
21
|
+
export { default as FaSmile } from "./FaSmile.jsx";
|
|
22
|
+
export { default as FaPaperclip } from "./FaPaperclip.jsx";
|
|
23
|
+
export { default as FaCircle } from "./FaCircle.jsx";
|
|
24
|
+
export { default as FaArrowUp } from "./FaArrowUp.jsx";
|
|
25
|
+
export { default as FaArrowDown } from "./FaArrowDown.jsx";
|
|
26
|
+
export { default as FaUserCircle } from "./FaUserCircle.jsx";
|
|
27
|
+
export { default as FaChevronRight } from "./FaChevronRight.jsx";
|
|
28
|
+
export { default as FaChevronDown } from "./FaChevronDown.jsx";
|
|
29
|
+
export { default as FaComments } from "./FaComments.jsx";
|
|
30
|
+
export { default as FaStarButton } from "./FaStar.jsx";
|
|
31
|
+
export { default as FaArrowLeft } from "./FaArrowLeft.jsx";
|
|
32
|
+
export { default as FaBell } from "./FaBell.jsx";
|
|
33
|
+
export { default as FaTag } from "./FaTag.jsx";
|
|
34
|
+
export { default as FaMapMarker } from "./FaMapMarkerAlt.jsx";
|
|
35
|
+
export { default as FaHome } from "./FaHome.jsx";
|
|
36
|
+
export { default as FaUserIcon } from "./FaUser.jsx";
|
|
37
|
+
export { default as FaMoon } from "./FaMoon.jsx";
|
|
38
|
+
export { default as MdWbSunny } from "./MdWbSunny.jsx";
|
|
39
|
+
export { default as FaSignInAlt } from "./FaSignInAlt.jsx";
|
|
40
|
+
export { default as FaBullhorn } from "./FaBullhorn.jsx";
|
|
41
|
+
export { default as SearchIcon } from "./SearchIcon.jsx";
|
|
42
|
+
export { default as BellIcon } from "./BellIcon.jsx";
|
|
43
|
+
export { default as MapMarkerIcon } from "./MapMarkerIcon.jsx";
|
|
44
|
+
export { default as UserIcon } from "./UserIcon.jsx";
|
|
45
|
+
export { default as HeartIcon } from "./HeartIcon.jsx";
|
|
46
|
+
export { default as StarIcon } from "./StarIcon.jsx";
|
|
47
|
+
export { default as FacebookIcon } from "./FacebookIcon.jsx";
|
|
48
|
+
export { default as InstagramIcon } from "./InstagramIcon.jsx";
|
|
49
|
+
export { default as YoutubeIcon } from "./YoutubeIcon.jsx";
|
|
50
|
+
export { default as RefreshIcon } from "./ReFreshIcon.jsx";
|
|
51
|
+
export { default as SyncIcon } from "./ReFreshIcon.jsx";
|
|
52
|
+
export { default as ChevronDown } from "./ChevronDown.jsx";
|
|
53
|
+
export { default as TwitterIcon } from "./TwitterIcon.jsx";
|
|
54
|
+
export { default as WhatsappIcon } from "./WhatsappIcon.jsx";
|
|
55
|
+
export { default as FaKeyboard } from "./FaKeyboard.jsx";
|
|
56
|
+
export { default as LinkedInIcon } from "./LinkedInIcon.jsx";
|
|
57
|
+
export { default as TelegramIcon } from "./TelegramIcon.jsx";
|
|
58
|
+
export { default as HalfMoonIcon } from "./HalfMoonIcon.jsx";
|
|
59
|
+
export { default as EmailIcon } from "./EmailIcon.jsx";
|
|
60
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/components/layout/Footer.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import styles from "../../styles/components/layouts/Footer.module.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Footer — props-driven layout shell for all 50+ Sanvika projects.
|
|
7
|
+
* 3-row layout: socialSlot | legalSlot | trustSlot.
|
|
8
|
+
* Each project passes its own social icons, legal links, trust badge via slots.
|
|
9
|
+
*
|
|
10
|
+
* @param {React.ReactNode} socialSlot - Social icons row (+ optional extra actions like RefreshButton)
|
|
11
|
+
* @param {React.ReactNode} legalSlot - Legal links row (About, Contact, Privacy, Terms, etc.)
|
|
12
|
+
* @param {React.ReactNode} trustSlot - Trust badge row (DUNS badge, copyright, etc.)
|
|
13
|
+
* @param {string} [className] - Additional CSS class
|
|
14
|
+
*/
|
|
15
|
+
const Footer = ({
|
|
16
|
+
socialSlot,
|
|
17
|
+
legalSlot,
|
|
18
|
+
trustSlot,
|
|
19
|
+
className = "",
|
|
20
|
+
}) => {
|
|
21
|
+
return (
|
|
22
|
+
<footer className={`${styles.footer} ${className}`.trim()}>
|
|
23
|
+
<div className={styles.footerContent}>
|
|
24
|
+
{/* Row 1: Social icons + extra actions */}
|
|
25
|
+
{socialSlot && (
|
|
26
|
+
<div className={styles.topLine}>
|
|
27
|
+
<div className={styles.socialIconsContainer}>
|
|
28
|
+
<div className={styles.socialIcons}>
|
|
29
|
+
{socialSlot}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
{/* Row 2: Legal links */}
|
|
36
|
+
{legalSlot && (
|
|
37
|
+
<div className={styles.legalLinks}>
|
|
38
|
+
{legalSlot}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{/* Row 3: Trust badge */}
|
|
43
|
+
{trustSlot && (
|
|
44
|
+
<div className={styles.dunsContainer}>
|
|
45
|
+
{trustSlot}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</footer>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default Footer;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/components/layout/Footer.stories.jsx
|
|
2
|
+
import Footer from "./Footer";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Layout/Footer",
|
|
6
|
+
component: Footer,
|
|
7
|
+
parameters: { layout: "fullscreen" },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
export const Default = {};
|
|
13
|
+
|
|
14
|
+
export const CustomLinks = {
|
|
15
|
+
args: {
|
|
16
|
+
legalLinks: [
|
|
17
|
+
{ href: "/about", label: "About" },
|
|
18
|
+
{ href: "/privacy", label: "Privacy" },
|
|
19
|
+
{ href: "/terms", label: "Terms" },
|
|
20
|
+
],
|
|
21
|
+
trustText: "D-U-N-S® Verified · My Company",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const NoSocialLinks = {
|
|
26
|
+
args: { socialLinks: [] },
|
|
27
|
+
};
|
|
28
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/components/layout/Navbar.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import styles from "../../styles/components/layouts/Navbar.module.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Navbar — props-driven layout shell for all 50+ Sanvika projects.
|
|
7
|
+
* 3-column grid: leftSlot | centerSlot | rightSlot.
|
|
8
|
+
* Each project passes its own logo, buttons, menus via slot props.
|
|
9
|
+
*
|
|
10
|
+
* @param {React.ReactNode} leftSlot - Logo area (left column)
|
|
11
|
+
* @param {React.ReactNode} centerSlot - Center menu items wrapped in <ul>
|
|
12
|
+
* @param {React.ReactNode} rightSlot - Right actions (profile, settings)
|
|
13
|
+
* @param {string} [className] - Additional CSS class
|
|
14
|
+
* @param {string} [ariaLabel] - Accessible label for <nav>
|
|
15
|
+
*/
|
|
16
|
+
const Navbar = ({
|
|
17
|
+
leftSlot,
|
|
18
|
+
centerSlot,
|
|
19
|
+
rightSlot,
|
|
20
|
+
className = "",
|
|
21
|
+
ariaLabel = "Main navigation",
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<nav
|
|
25
|
+
className={`${styles.navbar} ${className}`.trim()}
|
|
26
|
+
aria-label={ariaLabel}
|
|
27
|
+
>
|
|
28
|
+
{/* Left: Logo area */}
|
|
29
|
+
<div className={styles.navSectionLeft}>
|
|
30
|
+
{leftSlot}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Center: Menu items */}
|
|
34
|
+
<div className={styles.navSectionCenter}>
|
|
35
|
+
{centerSlot && (
|
|
36
|
+
<ul className={styles.menu}>
|
|
37
|
+
{centerSlot}
|
|
38
|
+
</ul>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Right: Action buttons */}
|
|
43
|
+
<div className={styles.navSectionRight}>
|
|
44
|
+
{rightSlot}
|
|
45
|
+
</div>
|
|
46
|
+
</nav>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default Navbar;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/components/layout/Navbar.stories.jsx
|
|
2
|
+
import Navbar from "./Navbar";
|
|
3
|
+
|
|
4
|
+
const NAV_LINKS = [
|
|
5
|
+
{ href: "/", label: "Home" },
|
|
6
|
+
{ href: "/about", label: "About" },
|
|
7
|
+
{ href: "/listings", label: "Listings" },
|
|
8
|
+
{ href: "/contact", label: "Contact" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Layout/Navbar",
|
|
13
|
+
component: Navbar,
|
|
14
|
+
parameters: { layout: "fullscreen" },
|
|
15
|
+
argTypes: {
|
|
16
|
+
logoText: { control: "text" },
|
|
17
|
+
activePath: { control: "text" },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
export const Default = {
|
|
24
|
+
args: { navLinks: NAV_LINKS, activePath: "/", logoText: "Sanvika" },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ActiveAbout = {
|
|
28
|
+
args: { navLinks: NAV_LINKS, activePath: "/about", logoText: "Sanvika" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const CustomLogo = {
|
|
32
|
+
args: { navLinks: NAV_LINKS, activePath: "/", logoText: "MyApp" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const FewLinks = {
|
|
36
|
+
args: {
|
|
37
|
+
navLinks: [{ href: "/", label: "Home" }, { href: "/contact", label: "Contact" }],
|
|
38
|
+
activePath: "/",
|
|
39
|
+
logoText: "Sanvika",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/components/layout/SubNavbar.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import styles from "../../styles/components/layouts/SubNavbar.module.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SubNavbar — fixed below Navbar, props-driven layout shell.
|
|
7
|
+
* Accepts items as children (React nodes) and optional afterContent
|
|
8
|
+
* for global modals/overlays that should render outside the nav.
|
|
9
|
+
*
|
|
10
|
+
* @param {React.ReactNode} children - Menu items (each wrapped in <li>)
|
|
11
|
+
* @param {React.ReactNode} [afterContent] - Content rendered after nav (modals, overlays)
|
|
12
|
+
* @param {string} [className] - Additional CSS class
|
|
13
|
+
* @param {string} [ariaLabel] - Accessible label for <nav>
|
|
14
|
+
*/
|
|
15
|
+
const SubNavbar = ({
|
|
16
|
+
children,
|
|
17
|
+
afterContent,
|
|
18
|
+
className = "",
|
|
19
|
+
ariaLabel = "Secondary navigation",
|
|
20
|
+
}) => {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<nav
|
|
24
|
+
className={`${styles.subNavbar} ${className}`.trim()}
|
|
25
|
+
aria-label={ariaLabel}
|
|
26
|
+
>
|
|
27
|
+
<ul className={styles.menu}>
|
|
28
|
+
{children}
|
|
29
|
+
</ul>
|
|
30
|
+
</nav>
|
|
31
|
+
{afterContent}
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default SubNavbar;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// src/components/modals/Modal.jsx
|
|
3
|
+
import React, { useEffect, useSyncExternalStore } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import styles from "../../styles/components/modals/Modal.module.css";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modal — pure props-driven portal modal.
|
|
9
|
+
* Close button is NOT included — pass it in via children.
|
|
10
|
+
*
|
|
11
|
+
* @param {boolean} isOpen - controls visibility
|
|
12
|
+
* @param {function} onClose - called on backdrop click or ESC key
|
|
13
|
+
* @param {React.ReactNode} children - modal content (include your own close button here)
|
|
14
|
+
* @param {string} [className]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// SSR-safe mount detection using useSyncExternalStore (React 18+ recommended)
|
|
18
|
+
const emptySubscribe = () => () => {};
|
|
19
|
+
const useIsMounted = () =>
|
|
20
|
+
useSyncExternalStore(emptySubscribe, () => true, () => false);
|
|
21
|
+
|
|
22
|
+
const Modal = ({ isOpen, onClose, children, className }) => {
|
|
23
|
+
const mounted = useIsMounted();
|
|
24
|
+
|
|
25
|
+
// Lock body scroll when modal is open
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (isOpen) {
|
|
28
|
+
document.body.style.overflow = "hidden";
|
|
29
|
+
document.body.style.position = "fixed";
|
|
30
|
+
document.body.style.width = "100%";
|
|
31
|
+
} else {
|
|
32
|
+
document.body.style.overflow = "";
|
|
33
|
+
document.body.style.position = "";
|
|
34
|
+
document.body.style.width = "";
|
|
35
|
+
}
|
|
36
|
+
return () => {
|
|
37
|
+
document.body.style.overflow = "";
|
|
38
|
+
document.body.style.position = "";
|
|
39
|
+
document.body.style.width = "";
|
|
40
|
+
};
|
|
41
|
+
}, [isOpen]);
|
|
42
|
+
|
|
43
|
+
// ESC key closes modal
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handleEsc = (e) => {
|
|
46
|
+
if (e.key === "Escape" && isOpen) onClose();
|
|
47
|
+
};
|
|
48
|
+
if (isOpen) document.addEventListener("keydown", handleEsc);
|
|
49
|
+
return () => document.removeEventListener("keydown", handleEsc);
|
|
50
|
+
}, [isOpen, onClose]);
|
|
51
|
+
|
|
52
|
+
if (!isOpen || !mounted) return null;
|
|
53
|
+
|
|
54
|
+
const handleBackdropClick = (e) => {
|
|
55
|
+
if (e.target === e.currentTarget) onClose();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return createPortal(
|
|
59
|
+
<div
|
|
60
|
+
className={styles.modalOverlay}
|
|
61
|
+
onClick={handleBackdropClick}
|
|
62
|
+
role="dialog"
|
|
63
|
+
aria-modal="true"
|
|
64
|
+
>
|
|
65
|
+
<div className={`${styles.modalContent} ${className ?? ""}`}>
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
</div>,
|
|
69
|
+
document.body
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default Modal;
|
|
74
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/components/modals/Modal.stories.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Modal from "./Modal";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Modal",
|
|
8
|
+
component: Modal,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
// Interactive wrapper since Modal needs open/close state
|
|
15
|
+
const ModalDemo = ({ buttonLabel = "Open Modal", children }) => {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => setOpen(true)}
|
|
21
|
+
style={{
|
|
22
|
+
padding: "8px 20px",
|
|
23
|
+
background: "var(--primary-color, #0984e3)",
|
|
24
|
+
color: "#fff",
|
|
25
|
+
border: "none",
|
|
26
|
+
borderRadius: "6px",
|
|
27
|
+
cursor: "pointer",
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{buttonLabel}
|
|
31
|
+
</button>
|
|
32
|
+
<Modal isOpen={open} onClose={() => setOpen(false)}>
|
|
33
|
+
{children}
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => setOpen(false)}
|
|
36
|
+
style={{
|
|
37
|
+
position: "absolute",
|
|
38
|
+
top: 12,
|
|
39
|
+
right: 12,
|
|
40
|
+
background: "transparent",
|
|
41
|
+
border: "none",
|
|
42
|
+
fontSize: 20,
|
|
43
|
+
cursor: "pointer",
|
|
44
|
+
color: "var(--secondary-text-color)",
|
|
45
|
+
}}
|
|
46
|
+
aria-label="Close modal"
|
|
47
|
+
>
|
|
48
|
+
✖
|
|
49
|
+
</button>
|
|
50
|
+
</Modal>
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Default = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<ModalDemo buttonLabel="Open Modal">
|
|
58
|
+
<h2 style={{ margin: "0 0 12px", color: "var(--heading-color)" }}>Modal Title</h2>
|
|
59
|
+
<p style={{ color: "var(--text-color)" }}>
|
|
60
|
+
This is modal content. Click the ✖ button or press ESC to close. Clicking outside also closes.
|
|
61
|
+
</p>
|
|
62
|
+
</ModalDemo>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithForm = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<ModalDemo buttonLabel="Open Form Modal">
|
|
69
|
+
<h2 style={{ margin: "0 0 16px", color: "var(--heading-color)" }}>Confirm Action</h2>
|
|
70
|
+
<p style={{ marginBottom: 20, color: "var(--secondary-text-color)" }}>
|
|
71
|
+
Are you sure you want to delete this item?
|
|
72
|
+
</p>
|
|
73
|
+
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
74
|
+
<button
|
|
75
|
+
style={{ padding: "8px 16px", background: "var(--secondary-color)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
76
|
+
>
|
|
77
|
+
Cancel
|
|
78
|
+
</button>
|
|
79
|
+
<button
|
|
80
|
+
style={{ padding: "8px 16px", background: "var(--error-color, #dc3545)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
81
|
+
>
|
|
82
|
+
Delete
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
<button
|
|
86
|
+
style={{ position: "absolute", top: 12, right: 12, background: "transparent", border: "none", fontSize: 20, cursor: "pointer", color: "var(--secondary-text-color)" }}
|
|
87
|
+
aria-label="Close"
|
|
88
|
+
>
|
|
89
|
+
✖
|
|
90
|
+
</button>
|
|
91
|
+
</ModalDemo>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// src/components/progressBar/ProgressBar.jsx
|
|
3
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
+
import styles from "../../styles/components/progressBar/ProgressBar.module.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ProgressBar — animated, pure props-driven progress indicator.
|
|
8
|
+
*
|
|
9
|
+
* @param {number} [progress=0] - 0 to 100
|
|
10
|
+
* @param {number} [height=6] - bar height in px
|
|
11
|
+
* @param {string} [color] - CSS color or var() for the filled bar
|
|
12
|
+
* @param {string} [backgroundColor] - CSS color or var() for the track
|
|
13
|
+
* @param {boolean} [animate=true] - smooth animation
|
|
14
|
+
* @param {number} [speed=300] - animation base duration in ms
|
|
15
|
+
* @param {boolean} [showLabel=false] - show percentage label
|
|
16
|
+
* @param {"top"|"bottom"} [labelPosition="top"]
|
|
17
|
+
* @param {function} [onComplete] - called when progress reaches 100
|
|
18
|
+
* @param {string} [className]
|
|
19
|
+
* @param {object} [style]
|
|
20
|
+
*/
|
|
21
|
+
const ProgressBar = ({
|
|
22
|
+
progress = 0,
|
|
23
|
+
height = 6,
|
|
24
|
+
color = "var(--primary-color, #007bff)",
|
|
25
|
+
backgroundColor = "var(--progress-bg, rgba(0, 0, 0, 0.1))",
|
|
26
|
+
animate = true,
|
|
27
|
+
speed = 300,
|
|
28
|
+
showLabel = false,
|
|
29
|
+
labelPosition = "top",
|
|
30
|
+
onComplete = null,
|
|
31
|
+
className = "",
|
|
32
|
+
style = {},
|
|
33
|
+
}) => {
|
|
34
|
+
const [animatedProgress, setAnimatedProgress] = useState(0);
|
|
35
|
+
const rafIdRef = useRef(null);
|
|
36
|
+
const startRef = useRef(0);
|
|
37
|
+
const endRef = useRef(progress);
|
|
38
|
+
const startTimeRef = useRef(null);
|
|
39
|
+
const completedRef = useRef(false);
|
|
40
|
+
|
|
41
|
+
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!animate) return;
|
|
45
|
+
startRef.current = animatedProgress;
|
|
46
|
+
}, [animatedProgress, animate]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!animate) return; // non-animated: use progress directly via clampedProgress below
|
|
50
|
+
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
|
|
51
|
+
endRef.current = progress;
|
|
52
|
+
startTimeRef.current = null;
|
|
53
|
+
if (startRef.current === endRef.current) return;
|
|
54
|
+
|
|
55
|
+
const delta = Math.abs(endRef.current - startRef.current);
|
|
56
|
+
const duration = Math.max(speed, Math.min(1500, 20 * delta));
|
|
57
|
+
|
|
58
|
+
const step = (ts) => {
|
|
59
|
+
if (startTimeRef.current === null) startTimeRef.current = ts;
|
|
60
|
+
const elapsed = ts - startTimeRef.current;
|
|
61
|
+
const t = Math.max(0, Math.min(1, elapsed / duration));
|
|
62
|
+
const eased = easeOutCubic(t);
|
|
63
|
+
const next = startRef.current + (endRef.current - startRef.current) * eased;
|
|
64
|
+
setAnimatedProgress(next);
|
|
65
|
+
if (t < 1) {
|
|
66
|
+
rafIdRef.current = requestAnimationFrame(step);
|
|
67
|
+
} else {
|
|
68
|
+
setAnimatedProgress(endRef.current);
|
|
69
|
+
if (endRef.current >= 100 && onComplete && !completedRef.current) {
|
|
70
|
+
completedRef.current = true;
|
|
71
|
+
onComplete();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
rafIdRef.current = requestAnimationFrame(step);
|
|
76
|
+
return () => {
|
|
77
|
+
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
|
|
78
|
+
};
|
|
79
|
+
}, [progress, animate, speed, onComplete]);
|
|
80
|
+
|
|
81
|
+
// When not animated, use progress directly; when animated, use animatedProgress
|
|
82
|
+
const clampedProgress = Math.max(0, Math.min(100, animate ? animatedProgress : progress));
|
|
83
|
+
const label = `${Math.round(clampedProgress)}%`;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className={`${styles.wrapper} ${className}`}
|
|
88
|
+
style={style}
|
|
89
|
+
role="progressbar"
|
|
90
|
+
aria-valuenow={Math.round(clampedProgress)}
|
|
91
|
+
aria-valuemin={0}
|
|
92
|
+
aria-valuemax={100}
|
|
93
|
+
>
|
|
94
|
+
{showLabel && labelPosition === "top" && (
|
|
95
|
+
<span className={styles.label}>{label}</span>
|
|
96
|
+
)}
|
|
97
|
+
<div
|
|
98
|
+
className={styles.track}
|
|
99
|
+
style={{ height, backgroundColor }}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
className={styles.bar}
|
|
103
|
+
style={{ width: `${clampedProgress}%`, background: color }}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
{showLabel && labelPosition === "bottom" && (
|
|
107
|
+
<span className={styles.label}>{label}</span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/components/progressBar/ProgressBar.stories.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import ProgressBar from "./ProgressBar";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/ProgressBar",
|
|
8
|
+
component: ProgressBar,
|
|
9
|
+
parameters: { layout: "padded" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
progress: { control: { type: "range", min: 0, max: 100, step: 1 } },
|
|
12
|
+
height: { control: { type: "range", min: 2, max: 24, step: 1 } },
|
|
13
|
+
animate: { control: "boolean" },
|
|
14
|
+
showLabel: { control: "boolean" },
|
|
15
|
+
labelPosition: { control: "select", options: ["top", "bottom"] },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
export const Default = {
|
|
22
|
+
args: { progress: 60, height: 6, animate: true },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const WithLabel = {
|
|
26
|
+
args: { progress: 75, height: 8, showLabel: true, labelPosition: "top" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Thick = {
|
|
30
|
+
args: { progress: 45, height: 16, showLabel: true },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Complete = {
|
|
34
|
+
args: { progress: 100, height: 6, showLabel: true },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const NoAnimation = {
|
|
38
|
+
args: { progress: 50, animate: false, height: 6 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Interactive story with increment button
|
|
42
|
+
const ProgressBarInteractive = () => {
|
|
43
|
+
const [val, setVal] = useState(0);
|
|
44
|
+
return (
|
|
45
|
+
<div style={{ width: 360, display: "flex", flexDirection: "column", gap: 16 }}>
|
|
46
|
+
<ProgressBar progress={val} height={8} showLabel animate />
|
|
47
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => setVal((v) => Math.min(100, v + 10))}
|
|
50
|
+
style={{ padding: "6px 16px", background: "var(--primary-color, #007bff)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
51
|
+
>
|
|
52
|
+
+10
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
onClick={() => setVal(0)}
|
|
56
|
+
style={{ padding: "6px 16px", background: "var(--secondary-color, #6c757d)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
57
|
+
>
|
|
58
|
+
Reset
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Interactive = {
|
|
66
|
+
render: () => <ProgressBarInteractive />,
|
|
67
|
+
};
|