@sanvika/ui 0.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 (85) hide show
  1. package/README.md +36 -0
  2. package/package.json +60 -0
  3. package/src/components/buttons/Button.jsx +96 -0
  4. package/src/components/buttons/FavoriteHeartButton.jsx +41 -0
  5. package/src/components/buttons/ScrollButton.jsx +36 -0
  6. package/src/components/buttons/ThreeDotButton.jsx +36 -0
  7. package/src/components/common/Container.jsx +38 -0
  8. package/src/components/common/Section.jsx +60 -0
  9. package/src/components/icons/BellIcon.jsx +10 -0
  10. package/src/components/icons/ChevronDown.jsx +20 -0
  11. package/src/components/icons/EmailIcon.jsx +9 -0
  12. package/src/components/icons/FaAngleDown.jsx +9 -0
  13. package/src/components/icons/FaAngleRight.jsx +9 -0
  14. package/src/components/icons/FaArrowDown.jsx +9 -0
  15. package/src/components/icons/FaArrowLeft.jsx +9 -0
  16. package/src/components/icons/FaArrowUp.jsx +9 -0
  17. package/src/components/icons/FaBell.jsx +9 -0
  18. package/src/components/icons/FaBullhorn.jsx +10 -0
  19. package/src/components/icons/FaCalendarAlt.jsx +9 -0
  20. package/src/components/icons/FaCheck.jsx +9 -0
  21. package/src/components/icons/FaCheckCircle.jsx +9 -0
  22. package/src/components/icons/FaCheckDouble.jsx +9 -0
  23. package/src/components/icons/FaChevronDown.jsx +9 -0
  24. package/src/components/icons/FaChevronRight.jsx +9 -0
  25. package/src/components/icons/FaCircle.jsx +9 -0
  26. package/src/components/icons/FaComments.jsx +9 -0
  27. package/src/components/icons/FaEye.jsx +9 -0
  28. package/src/components/icons/FaEyeSlash.jsx +9 -0
  29. package/src/components/icons/FaHome.jsx +9 -0
  30. package/src/components/icons/FaKeyboard.jsx +9 -0
  31. package/src/components/icons/FaLocationArrow.jsx +9 -0
  32. package/src/components/icons/FaMapMarkerAlt.jsx +9 -0
  33. package/src/components/icons/FaMoon.jsx +9 -0
  34. package/src/components/icons/FaPaperPlane.jsx +9 -0
  35. package/src/components/icons/FaPaperclip.jsx +9 -0
  36. package/src/components/icons/FaSignInAlt.jsx +9 -0
  37. package/src/components/icons/FaSmile.jsx +9 -0
  38. package/src/components/icons/FaStar.jsx +9 -0
  39. package/src/components/icons/FaTag.jsx +9 -0
  40. package/src/components/icons/FaThumbsDown.jsx +9 -0
  41. package/src/components/icons/FaThumbsUp.jsx +9 -0
  42. package/src/components/icons/FaTrash.jsx +9 -0
  43. package/src/components/icons/FaUser.jsx +9 -0
  44. package/src/components/icons/FaUserCircle.jsx +9 -0
  45. package/src/components/icons/FacebookIcon.jsx +9 -0
  46. package/src/components/icons/HalfMoonIcon.jsx +18 -0
  47. package/src/components/icons/HeartIcon.jsx +9 -0
  48. package/src/components/icons/InstagramIcon.jsx +9 -0
  49. package/src/components/icons/LinkedInIcon.jsx +9 -0
  50. package/src/components/icons/MapMarkerIcon.jsx +9 -0
  51. package/src/components/icons/MdWbSunny.jsx +9 -0
  52. package/src/components/icons/ReFreshIcon.jsx +49 -0
  53. package/src/components/icons/SearchIcon.jsx +20 -0
  54. package/src/components/icons/StarIcon.jsx +9 -0
  55. package/src/components/icons/TelegramIcon.jsx +9 -0
  56. package/src/components/icons/TwitterIcon.jsx +9 -0
  57. package/src/components/icons/UserIcon.jsx +9 -0
  58. package/src/components/icons/WhatsappIcon.jsx +9 -0
  59. package/src/components/icons/YoutubeIcon.jsx +9 -0
  60. package/src/components/icons/index.js +60 -0
  61. package/src/components/layout/Footer.jsx +142 -0
  62. package/src/components/layout/Navbar.jsx +65 -0
  63. package/src/components/modals/Modal.jsx +74 -0
  64. package/src/components/progressBar/ProgressBar.jsx +113 -0
  65. package/src/context/ThemeContext.jsx +91 -0
  66. package/src/index.js +29 -0
  67. package/src/styles/base/elements.css +71 -0
  68. package/src/styles/base/reset.css +52 -0
  69. package/src/styles/base/typography.css +80 -0
  70. package/src/styles/components/buttons/Button.module.css +160 -0
  71. package/src/styles/components/buttons/FavoriteHeartButton.module.css +35 -0
  72. package/src/styles/components/buttons/ThreeDotButton.module.css +41 -0
  73. package/src/styles/components/common/Container.module.css +25 -0
  74. package/src/styles/components/common/Section.module.css +54 -0
  75. package/src/styles/components/layouts/Footer.module.css +100 -0
  76. package/src/styles/components/layouts/Layout.module.css +21 -0
  77. package/src/styles/components/layouts/Navbar.module.css +121 -0
  78. package/src/styles/components/modals/Modal.module.css +39 -0
  79. package/src/styles/components/progressBar/ProgressBar.module.css +28 -0
  80. package/src/styles/fouc-prevention.css +19 -0
  81. package/src/styles/index.css +27 -0
  82. package/src/styles/tokens/dark.css +121 -0
  83. package/src/styles/tokens/light.css +158 -0
  84. package/src/styles/utilities/colors.css +59 -0
  85. package/src/styles/utilities/transitions.css +40 -0
@@ -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,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,91 @@
1
+ "use client";
2
+ // src/context/ThemeContext.jsx
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ useCallback,
9
+ } from "react";
10
+
11
+ const ThemeContext = createContext();
12
+
13
+ export const ThemeProvider = ({ children }) => {
14
+ const [mounted, setMounted] = useState(false);
15
+ const [theme, setTheme] = useState("light"); // SSR fallback
16
+
17
+ // Step 1: On mount, load theme from localStorage. Default = dark.
18
+ useEffect(() => {
19
+ try {
20
+ const saved = localStorage.getItem("theme");
21
+ const resolved = saved ?? "dark"; // First visit = dark
22
+ setTheme(resolved);
23
+ document.documentElement.setAttribute("data-theme", resolved);
24
+
25
+ // Listen for OS-level theme changes only if user has not set a preference
26
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
27
+ const onSystemChange = (e) => {
28
+ if (!localStorage.getItem("theme")) {
29
+ const sys = e.matches ? "dark" : "light";
30
+ setTheme(sys);
31
+ }
32
+ };
33
+ mq.addEventListener("change", onSystemChange);
34
+ setMounted(true);
35
+ return () => mq.removeEventListener("change", onSystemChange);
36
+ } catch {
37
+ setTheme("dark");
38
+ document.documentElement.setAttribute("data-theme", "dark");
39
+ setMounted(true);
40
+ }
41
+ }, []);
42
+
43
+ // Step 2: Apply theme with smooth transition class
44
+ const applyTheme = useCallback((next) => {
45
+ const root = document.documentElement;
46
+ root.classList.add("theme-transition");
47
+ root.setAttribute("data-theme", next);
48
+ localStorage.setItem("theme", next);
49
+ const t = setTimeout(() => root.classList.remove("theme-transition"), 300);
50
+ return () => clearTimeout(t);
51
+ }, []);
52
+
53
+ // Step 3: Apply whenever theme state changes (after mount)
54
+ useEffect(() => {
55
+ if (mounted) applyTheme(theme);
56
+ }, [theme, mounted, applyTheme]);
57
+
58
+ // Step 4: Toggle
59
+ const toggleTheme = useCallback(() => {
60
+ setTheme((prev) => (prev === "light" ? "dark" : "light"));
61
+ }, []);
62
+
63
+ // Step 5: Explicit setter (for admin/testing)
64
+ const setThemeExplicitly = useCallback((next) => {
65
+ if (next !== "light" && next !== "dark") return;
66
+ setTheme(next);
67
+ }, []);
68
+
69
+ // React 19 direct context syntax
70
+ return (
71
+ <ThemeContext
72
+ value={{
73
+ theme,
74
+ toggleTheme,
75
+ setTheme: setThemeExplicitly,
76
+ isDarkMode: theme === "dark",
77
+ isThemeReady: mounted,
78
+ }}
79
+ >
80
+ {children}
81
+ </ThemeContext>
82
+ );
83
+ };
84
+
85
+ export const useTheme = () => {
86
+ const ctx = useContext(ThemeContext);
87
+ if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
88
+ return ctx;
89
+ };
90
+
91
+ export { ThemeContext };
package/src/index.js ADDED
@@ -0,0 +1,29 @@
1
+ // src/index.js
2
+ // @sanvika/ui — main barrel export
3
+ // Import this in your project: import { Button, Modal, Navbar, ... } from "@sanvika/ui"
4
+
5
+ // ─── Layout ──────────────────────────────────────────────────────────────────
6
+ export { default as Navbar } from "./components/layout/Navbar.jsx";
7
+ export { default as Footer } from "./components/layout/Footer.jsx";
8
+
9
+ // ─── Buttons ─────────────────────────────────────────────────────────────────
10
+ export { default as Button } from "./components/buttons/Button.jsx";
11
+ export { default as FavoriteHeartButton } from "./components/buttons/FavoriteHeartButton.jsx";
12
+ export { default as ScrollButton } from "./components/buttons/ScrollButton.jsx";
13
+ export { default as ThreeDotButton } from "./components/buttons/ThreeDotButton.jsx";
14
+
15
+ // ─── Common ──────────────────────────────────────────────────────────────────
16
+ export { default as Container } from "./components/common/Container.jsx";
17
+ export { default as Section } from "./components/common/Section.jsx";
18
+
19
+ // ─── Modals ──────────────────────────────────────────────────────────────────
20
+ export { default as Modal } from "./components/modals/Modal.jsx";
21
+
22
+ // ─── ProgressBar ─────────────────────────────────────────────────────────────
23
+ export { default as ProgressBar } from "./components/progressBar/ProgressBar.jsx";
24
+
25
+ // ─── Icons (re-exported from icons barrel) ───────────────────────────────────
26
+ export * from "./components/icons/index.js";
27
+
28
+ // ─── Theme Context ────────────────────────────────────────────────────────────
29
+ export { ThemeProvider, useTheme } from "./context/ThemeContext.jsx";
@@ -0,0 +1,71 @@
1
+ /* src/styles/base/elements.css */
2
+
3
+ /* ========================================== */
4
+ /* FORM & INTERACTIVE ELEMENTS */
5
+ /* ========================================== */
6
+
7
+ input,
8
+ textarea,
9
+ select {
10
+ background-color: var(--input-bg);
11
+ color: var(--text-color);
12
+ border: 1px solid var(--border-color-light);
13
+ border-radius: var(--radius-md);
14
+ padding: var(--space-2) var(--space-3);
15
+ font-family: var(--font-sans);
16
+ font-size: var(--text-sm);
17
+ width: 100%;
18
+ transition:
19
+ border-color var(--transition-fast),
20
+ box-shadow var(--transition-fast),
21
+ background-color var(--transition-base);
22
+ outline: none;
23
+ }
24
+
25
+ input:focus,
26
+ textarea:focus,
27
+ select:focus {
28
+ border-color: var(--focus-border-color);
29
+ box-shadow: 0 0 0 3px var(--focus-shadow-color);
30
+ }
31
+
32
+ button {
33
+ cursor: pointer;
34
+ font-family: var(--font-sans);
35
+ transition:
36
+ background-color var(--transition-fast),
37
+ color var(--transition-fast),
38
+ border-color var(--transition-fast),
39
+ transform var(--transition-fast),
40
+ box-shadow var(--transition-fast);
41
+ }
42
+
43
+ button:disabled {
44
+ opacity: 0.55;
45
+ cursor: not-allowed;
46
+ pointer-events: none;
47
+ }
48
+
49
+ hr {
50
+ border: none;
51
+ border-top: 1px solid var(--border-color-light);
52
+ margin: var(--space-6) 0;
53
+ }
54
+
55
+ table {
56
+ width: 100%;
57
+ border-collapse: collapse;
58
+ }
59
+
60
+ th,
61
+ td {
62
+ padding: var(--space-3) var(--space-4);
63
+ text-align: left;
64
+ border-bottom: 1px solid var(--border-color-light);
65
+ }
66
+
67
+ th {
68
+ font-weight: 600;
69
+ color: var(--heading-color);
70
+ background-color: var(--section-bg);
71
+ }
@@ -0,0 +1,52 @@
1
+ /* src/styles/base/reset.css */
2
+
3
+ /* ========================================== */
4
+ /* UNIVERSAL RESET & OPTIMIZATION */
5
+ /* ========================================== */
6
+
7
+ *,
8
+ *::before,
9
+ *::after {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ -webkit-font-smoothing: antialiased;
14
+ -moz-osx-font-smoothing: grayscale;
15
+ }
16
+
17
+ html {
18
+ height: 100%;
19
+ scroll-behavior: smooth;
20
+ scrollbar-gutter: stable;
21
+ }
22
+
23
+ body {
24
+ min-height: 100%;
25
+ width: 100%;
26
+ font-family: var(--font-sans);
27
+ font-size: var(--text-base);
28
+ line-height: 1.6;
29
+ background-color: var(--bg-color);
30
+ color: var(--text-color);
31
+ transition:
32
+ background-color var(--transition-base),
33
+ color var(--transition-base);
34
+ overflow-x: hidden;
35
+ }
36
+
37
+ ::placeholder {
38
+ color: var(--placeholder-color);
39
+ opacity: 1;
40
+ }
41
+
42
+ img,
43
+ video {
44
+ max-width: 100%;
45
+ height: auto;
46
+ display: block;
47
+ }
48
+
49
+ ul,
50
+ ol {
51
+ list-style: none;
52
+ }
@@ -0,0 +1,80 @@
1
+ /* src/styles/base/typography.css */
2
+
3
+ /* ========================================== */
4
+ /* TYPOGRAPHY & TEXT ELEMENTS */
5
+ /* ========================================== */
6
+
7
+ a {
8
+ color: var(--link-color);
9
+ text-decoration: none;
10
+ transition: color var(--transition-fast);
11
+ }
12
+
13
+ a:hover {
14
+ color: var(--link-hover-color);
15
+ text-decoration: underline;
16
+ }
17
+
18
+ h1,
19
+ h2,
20
+ h3,
21
+ h4,
22
+ h5,
23
+ h6 {
24
+ color: var(--heading-color);
25
+ font-weight: 700;
26
+ line-height: 1.25;
27
+ letter-spacing: -0.01em;
28
+ }
29
+
30
+ h1 {
31
+ font-size: clamp(var(--text-2xl), 5vw, var(--text-4xl));
32
+ }
33
+ h2 {
34
+ font-size: clamp(var(--text-xl), 4vw, var(--text-3xl));
35
+ }
36
+ h3 {
37
+ font-size: clamp(var(--text-lg), 3vw, var(--text-2xl));
38
+ }
39
+ h4 {
40
+ font-size: var(--text-xl);
41
+ }
42
+ h5 {
43
+ font-size: var(--text-lg);
44
+ }
45
+ h6 {
46
+ font-size: var(--text-base);
47
+ }
48
+
49
+ p {
50
+ color: var(--text-color);
51
+ margin-bottom: var(--space-4);
52
+ }
53
+
54
+ p:last-child {
55
+ margin-bottom: 0;
56
+ }
57
+
58
+ label {
59
+ color: var(--label-color);
60
+ font-weight: 500;
61
+ font-size: var(--text-sm);
62
+ }
63
+
64
+ small {
65
+ font-size: var(--text-xs);
66
+ color: var(--secondary-text-color);
67
+ }
68
+
69
+ strong {
70
+ font-weight: 700;
71
+ }
72
+
73
+ code {
74
+ font-family: var(--font-mono);
75
+ font-size: 0.9em;
76
+ background-color: var(--muted-bg);
77
+ padding: 0.1em 0.35em;
78
+ border-radius: var(--radius-sm);
79
+ border: 1px solid var(--border-color-light);
80
+ }
@@ -0,0 +1,160 @@
1
+ /* src/styles/components/buttons/Button.module.css */
2
+
3
+ /* Universal Button — Mobile-First, SSOT via CSS variables */
4
+
5
+ .button {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ gap: clamp(4px, 0.5vw, 6px);
10
+
11
+ background-color: var(--button-bg, var(--btn-primary-bg));
12
+ color: var(--button-text, var(--btn-primary-text));
13
+ border: 1px solid var(--button-border, transparent);
14
+
15
+ font-family: var(--font-sans);
16
+ font-weight: 500;
17
+ font-size: clamp(0.8rem, 2.5vw, 0.9rem);
18
+ line-height: 1.2;
19
+ white-space: nowrap;
20
+ text-decoration: none;
21
+
22
+ padding: clamp(6px, 1.2vw, 10px) clamp(12px, 2.5vw, 20px);
23
+ border-radius: var(--radius-md);
24
+ min-height: clamp(32px, 8vw, 40px);
25
+
26
+ cursor: pointer;
27
+ user-select: none;
28
+ position: relative;
29
+ overflow: hidden;
30
+ flex-shrink: 0;
31
+
32
+ transition:
33
+ background-color 0.3s ease,
34
+ color 0.3s ease,
35
+ border-color 0.3s ease,
36
+ box-shadow 0.3s ease;
37
+ }
38
+
39
+ .button:hover {
40
+ background-color: var(--button-hover-bg, var(--btn-primary-hover-bg));
41
+ color: var(--button-hover-text, var(--btn-primary-hover-text));
42
+ box-shadow: 0 2px 8px var(--shadow-color);
43
+ }
44
+
45
+ .button:active {
46
+ box-shadow: none;
47
+ }
48
+
49
+ .button:focus-visible {
50
+ outline: 2px solid var(--focus-border-color);
51
+ outline-offset: 2px;
52
+ }
53
+
54
+ .button:disabled {
55
+ opacity: 0.55;
56
+ cursor: not-allowed;
57
+ pointer-events: none;
58
+ box-shadow: none;
59
+ }
60
+
61
+ /* ===== INTENT VARIANTS ===== */
62
+
63
+ .primary {
64
+ --button-bg: var(--btn-primary-bg);
65
+ --button-text: var(--btn-primary-text);
66
+ --button-hover-bg: var(--btn-primary-hover-bg);
67
+ --button-hover-text: var(--btn-primary-hover-text);
68
+ }
69
+
70
+ .secondary {
71
+ --button-bg: var(--btn-secondary-bg);
72
+ --button-text: var(--btn-secondary-text);
73
+ --button-hover-bg: var(--btn-secondary-hover-bg);
74
+ --button-hover-text: var(--btn-secondary-hover-text);
75
+ }
76
+
77
+ .danger {
78
+ --button-bg: var(--btn-danger-bg);
79
+ --button-text: var(--btn-danger-text);
80
+ --button-hover-bg: var(--btn-danger-hover-bg);
81
+ --button-hover-text: var(--btn-danger-hover-text);
82
+ }
83
+
84
+ .success {
85
+ --button-bg: var(--btn-success-bg);
86
+ --button-text: var(--btn-success-text);
87
+ --button-hover-bg: var(--btn-success-hover-bg);
88
+ --button-hover-text: var(--btn-success-hover-text);
89
+ }
90
+
91
+ /* ===== APPEARANCE VARIANTS ===== */
92
+
93
+ .outline {
94
+ --button-bg: transparent;
95
+ --button-hover-bg: var(--hover-color);
96
+ --button-text: var(--text-color);
97
+ --button-hover-text: var(--text-color);
98
+ --button-border: var(--border-color-light);
99
+ }
100
+
101
+ .ghost {
102
+ --button-bg: transparent;
103
+ --button-hover-bg: var(--hover-color);
104
+ --button-text: var(--text-color);
105
+ --button-hover-text: var(--text-color);
106
+ --button-border: transparent;
107
+ box-shadow: none;
108
+ }
109
+
110
+ .ghost:hover {
111
+ box-shadow: none;
112
+ }
113
+
114
+ /* ===== SIZE VARIANTS ===== */
115
+
116
+ .sm {
117
+ font-size: var(--text-xs);
118
+ padding: clamp(3px, 0.8vw, 5px) clamp(8px, 2vw, 12px);
119
+ min-height: clamp(24px, 6vw, 32px);
120
+ border-radius: var(--radius-sm);
121
+ }
122
+
123
+ .lg {
124
+ font-size: var(--text-base);
125
+ padding: clamp(10px, 2vw, 14px) clamp(20px, 5vw, 32px);
126
+ min-height: clamp(40px, 10vw, 56px);
127
+ border-radius: var(--radius-lg);
128
+ }
129
+
130
+ .full {
131
+ width: 100%;
132
+ }
133
+
134
+ /* ===== SLOT ELEMENTS ===== */
135
+
136
+ .icon {
137
+ display: flex;
138
+ align-items: center;
139
+ flex-shrink: 0;
140
+ color: inherit;
141
+ }
142
+
143
+ .text {
144
+ color: inherit;
145
+ }
146
+
147
+ .badge {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ min-width: 18px;
152
+ height: 18px;
153
+ padding: 0 clamp(2px, 0.5vw, 4px);
154
+ border-radius: var(--radius-full);
155
+ font-size: 0.65rem;
156
+ font-weight: 700;
157
+ background-color: var(--error-color);
158
+ color: var(--btn-primary-text);
159
+ margin-left: clamp(2px, 0.5vw, 4px);
160
+ }
@@ -0,0 +1,35 @@
1
+ /* src/styles/components/buttons/FavoriteHeartButton.module.css */
2
+
3
+ .favoriteHeartButton {
4
+ background-color: transparent !important;
5
+ border: none !important;
6
+ padding: clamp(4px, 1.5vw, 8px) !important;
7
+ cursor: pointer;
8
+ min-width: clamp(32px, 8vw, 48px) !important;
9
+ max-width: clamp(32px, 8vw, 48px) !important;
10
+ height: clamp(32px, 8vw, 48px) !important;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ gap: 0 !important;
15
+ transition: opacity 0.2s ease;
16
+ }
17
+
18
+ .favorited {
19
+ color: var(--danger-color, #dc3545) !important;
20
+ }
21
+
22
+ .notFavorited {
23
+ color: var(--secondary-text-color) !important;
24
+ }
25
+
26
+ .favoriteHeartButton:disabled {
27
+ cursor: not-allowed;
28
+ opacity: 0.6;
29
+ }
30
+
31
+ .favoriteHeartButton:hover:not(:disabled) {
32
+ opacity: 0.75;
33
+ background-color: transparent !important;
34
+ border: none !important;
35
+ }