@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.
Files changed (82) hide show
  1. package/package.json +15 -29
  2. package/src/components/buttons/Button.jsx +97 -0
  3. package/src/components/buttons/Button.stories.jsx +110 -0
  4. package/src/components/buttons/FavoriteHeartButton.jsx +41 -0
  5. package/src/components/buttons/FavoriteHeartButton.stories.jsx +48 -0
  6. package/src/components/buttons/ScrollButton.jsx +36 -0
  7. package/src/components/buttons/ScrollButton.stories.jsx +21 -0
  8. package/src/components/buttons/ThreeDotButton.jsx +36 -0
  9. package/src/components/buttons/ThreeDotButton.stories.jsx +14 -0
  10. package/src/components/common/Container.jsx +38 -0
  11. package/src/components/common/Section.jsx +60 -0
  12. package/src/components/common/Section.stories.jsx +64 -0
  13. package/src/components/icons/BellIcon.jsx +10 -0
  14. package/src/components/icons/ChevronDown.jsx +20 -0
  15. package/src/components/icons/EmailIcon.jsx +9 -0
  16. package/src/components/icons/FaAngleDown.jsx +9 -0
  17. package/src/components/icons/FaAngleRight.jsx +9 -0
  18. package/src/components/icons/FaArrowDown.jsx +9 -0
  19. package/src/components/icons/FaArrowLeft.jsx +9 -0
  20. package/src/components/icons/FaArrowUp.jsx +9 -0
  21. package/src/components/icons/FaBell.jsx +9 -0
  22. package/src/components/icons/FaBullhorn.jsx +10 -0
  23. package/src/components/icons/FaCalendarAlt.jsx +9 -0
  24. package/src/components/icons/FaCheck.jsx +9 -0
  25. package/src/components/icons/FaCheckCircle.jsx +9 -0
  26. package/src/components/icons/FaCheckDouble.jsx +9 -0
  27. package/src/components/icons/FaChevronDown.jsx +9 -0
  28. package/src/components/icons/FaChevronRight.jsx +9 -0
  29. package/src/components/icons/FaCircle.jsx +9 -0
  30. package/src/components/icons/FaComments.jsx +9 -0
  31. package/src/components/icons/FaEye.jsx +9 -0
  32. package/src/components/icons/FaEyeSlash.jsx +9 -0
  33. package/src/components/icons/FaHome.jsx +9 -0
  34. package/src/components/icons/FaKeyboard.jsx +9 -0
  35. package/src/components/icons/FaLocationArrow.jsx +9 -0
  36. package/src/components/icons/FaMapMarkerAlt.jsx +9 -0
  37. package/src/components/icons/FaMoon.jsx +9 -0
  38. package/src/components/icons/FaPaperPlane.jsx +9 -0
  39. package/src/components/icons/FaPaperclip.jsx +9 -0
  40. package/src/components/icons/FaSignInAlt.jsx +9 -0
  41. package/src/components/icons/FaSmile.jsx +9 -0
  42. package/src/components/icons/FaStar.jsx +9 -0
  43. package/src/components/icons/FaTag.jsx +9 -0
  44. package/src/components/icons/FaThumbsDown.jsx +9 -0
  45. package/src/components/icons/FaThumbsUp.jsx +9 -0
  46. package/src/components/icons/FaTrash.jsx +9 -0
  47. package/src/components/icons/FaUser.jsx +9 -0
  48. package/src/components/icons/FaUserCircle.jsx +9 -0
  49. package/src/components/icons/FacebookIcon.jsx +9 -0
  50. package/src/components/icons/HalfMoonIcon.jsx +18 -0
  51. package/src/components/icons/HeartIcon.jsx +9 -0
  52. package/src/components/icons/InstagramIcon.jsx +9 -0
  53. package/src/components/icons/LinkedInIcon.jsx +9 -0
  54. package/src/components/icons/MapMarkerIcon.jsx +9 -0
  55. package/src/components/icons/MdWbSunny.jsx +9 -0
  56. package/src/components/icons/ReFreshIcon.jsx +49 -0
  57. package/src/components/icons/SearchIcon.jsx +20 -0
  58. package/src/components/icons/StarIcon.jsx +9 -0
  59. package/src/components/icons/TelegramIcon.jsx +9 -0
  60. package/src/components/icons/TwitterIcon.jsx +9 -0
  61. package/src/components/icons/UserIcon.jsx +9 -0
  62. package/src/components/icons/WhatsappIcon.jsx +9 -0
  63. package/src/components/icons/YoutubeIcon.jsx +9 -0
  64. package/src/components/icons/index.js +60 -0
  65. package/src/components/layout/Footer.jsx +53 -0
  66. package/src/components/layout/Footer.stories.jsx +28 -0
  67. package/src/components/layout/Navbar.jsx +50 -0
  68. package/src/components/layout/Navbar.stories.jsx +42 -0
  69. package/src/components/layout/SubNavbar.jsx +36 -0
  70. package/src/components/modals/Modal.jsx +74 -0
  71. package/src/components/modals/Modal.stories.jsx +93 -0
  72. package/src/components/progressBar/ProgressBar.jsx +113 -0
  73. package/src/components/progressBar/ProgressBar.stories.jsx +67 -0
  74. package/src/context/ThemeContext.jsx +91 -0
  75. package/src/index.js +33 -0
  76. package/src/layouts/Layout.jsx +24 -0
  77. package/src/server/index.js +159 -0
  78. package/README.md +0 -36
  79. package/dist/EmailIcon-DumDw7u2.js +0 -781
  80. package/dist/EmailIcon-ssF1iAVu.js +0 -782
  81. package/dist/icons/index.js +0 -4
  82. 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
+ };