kupos-ui-components-lib 9.10.1 → 9.10.2

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/src/index.ts CHANGED
@@ -25,6 +25,16 @@ import {
25
25
  ResponsiveFilterBar,
26
26
  } from "./components/FilterBar";
27
27
 
28
+ // Import Survey components
29
+ import {
30
+ SurveyDesktop,
31
+ SurveyMobile,
32
+ ResponsiveSurvey,
33
+ } from "./components/Survey";
34
+
35
+ // Import Modal components
36
+ import { Modal, ModalHeader } from "./ui/Modal";
37
+
28
38
  export {
29
39
  ServiceItemDesktop,
30
40
  ServiceItemMobile,
@@ -46,6 +56,16 @@ export {
46
56
  FilterBarDesktop,
47
57
  FilterBarMobile,
48
58
  ResponsiveFilterBar,
59
+
60
+
61
+ //Survey components
62
+ SurveyDesktop,
63
+ SurveyMobile,
64
+ ResponsiveSurvey,
65
+
66
+ // Modal components
67
+ Modal,
68
+ ModalHeader,
49
69
  };
50
70
 
51
71
  // Also export types
@@ -54,3 +74,6 @@ export type { MobileServiceItemProps } from "./components/ServiceItem/mobileType
54
74
  export type { PaymentSideBarProps } from "./components/PaymentSideBar/types";
55
75
  export type { ServiceListProps } from "./components/ServiceList/types";
56
76
  export type { FilterBarProps } from "./components/FilterBar/tyoes";
77
+ export type { SurveyProps } from "./components/Survey/types";
78
+ export type { ModalProps, ModalVariant, ModalSize, ModalHeaderProps } from "./ui/Modal";
79
+
@@ -0,0 +1,131 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom";
3
+
4
+ export interface BottomSheetProps {
5
+ isOpen: boolean;
6
+ onClose?: () => void;
7
+ children: React.ReactNode;
8
+ showHandle?: boolean;
9
+ showBackdrop?: boolean;
10
+ backdropColor?: string;
11
+ blurBackground?: boolean;
12
+ blurAmount?: string;
13
+ closeOnBackdrop?: boolean;
14
+ padding?: string | number;
15
+ maxHeight?: string;
16
+ borderRadius?: string;
17
+ }
18
+
19
+ const STYLE_ID = "__kupos_bottom_sheet_style__";
20
+
21
+ const injectStyle = () => {
22
+ if (typeof document === "undefined") return;
23
+ if (document.getElementById(STYLE_ID)) return;
24
+ const style = document.createElement("style");
25
+ style.id = STYLE_ID;
26
+ style.textContent = `
27
+ @keyframes __ks_slideUp {
28
+ from { transform: translateY(100%); }
29
+ to { transform: translateY(0); }
30
+ }
31
+ @keyframes __ks_fadeIn {
32
+ from { opacity: 0; }
33
+ to { opacity: 1; }
34
+ }
35
+ .__ks_sheet {
36
+ animation: __ks_slideUp 0.38s cubic-bezier(0.32, 0.72, 0, 1) both;
37
+ }
38
+ .__ks_backdrop {
39
+ animation: __ks_fadeIn 0.3s ease both;
40
+ }
41
+ `;
42
+ document.head.appendChild(style);
43
+ };
44
+
45
+ const BottomSheet = ({
46
+ isOpen,
47
+ onClose,
48
+ children,
49
+ showHandle = true,
50
+ showBackdrop = false,
51
+ backdropColor = "rgba(0,0,0,0.45)",
52
+ blurBackground = false,
53
+ blurAmount = "6px",
54
+ closeOnBackdrop = true,
55
+ padding = "20px 20px 32px",
56
+ maxHeight = "92vh",
57
+ borderRadius = "24px 24px 0 0",
58
+ }: BottomSheetProps) => {
59
+ if (!isOpen) return null;
60
+
61
+ injectStyle();
62
+
63
+ const sheet = (
64
+ <>
65
+ {(showBackdrop || blurBackground) && (
66
+ <div
67
+ className="__ks_backdrop"
68
+ onClick={closeOnBackdrop ? onClose : undefined}
69
+ style={{
70
+ position: "fixed",
71
+ top: 0,
72
+ left: 0,
73
+ right: 0,
74
+ bottom: 0,
75
+ zIndex: 9998,
76
+ backgroundColor: showBackdrop ? backdropColor : "rgba(0,0,0,0.4)",
77
+ backdropFilter: blurBackground ? `blur(${blurAmount})` : undefined,
78
+ WebkitBackdropFilter: blurBackground
79
+ ? `blur(${blurAmount})`
80
+ : undefined,
81
+ }}
82
+ />
83
+ )}
84
+
85
+ <div
86
+ className="__ks_sheet"
87
+ style={{
88
+ position: "fixed",
89
+ left: 0,
90
+ right: 0,
91
+ bottom: 0,
92
+ zIndex: 9999,
93
+ background: "#FFFFFF",
94
+ maxHeight,
95
+ borderRadius,
96
+ overflowY: "auto",
97
+ boxSizing: "border-box" as const,
98
+ boxShadow: "0 -8px 40px rgba(0,0,0,0.15)",
99
+ }}
100
+ >
101
+ {showHandle && (
102
+ <div
103
+ style={{
104
+ display: "flex",
105
+ justifyContent: "center",
106
+ padding: "12px 0 4px",
107
+ }}
108
+ >
109
+ <div
110
+ style={{
111
+ width: 40,
112
+ height: 4,
113
+ borderRadius: 999,
114
+ backgroundColor: "#E5E7EB",
115
+ }}
116
+ />
117
+ </div>
118
+ )}
119
+
120
+ <div style={{ padding }}>{children}</div>
121
+ </div>
122
+ </>
123
+ );
124
+
125
+ if (typeof document !== "undefined") {
126
+ return ReactDOM.createPortal(sheet, document.body) as React.ReactElement;
127
+ }
128
+ return sheet;
129
+ };
130
+
131
+ export default BottomSheet;
@@ -0,0 +1,2 @@
1
+ export { default as BottomSheet } from "./BottomSheet";
2
+ export type { BottomSheetProps } from "./BottomSheet";
@@ -0,0 +1,92 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom";
3
+
4
+ export type ModalVariant = "center" | "bottom-sheet";
5
+ export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
6
+
7
+ export interface ModalProps {
8
+ isOpen: boolean;
9
+ onClose?: () => void;
10
+ children: React.ReactNode;
11
+ variant?: ModalVariant;
12
+ size?: ModalSize;
13
+ closeOnBackdrop?: boolean;
14
+ padding?: string | number;
15
+ borderRadius?: string | number;
16
+ backdropColor?: string;
17
+ maxWidth?: number | string;
18
+ }
19
+
20
+ const SIZE_MAP: Record<ModalSize, number | string> = {
21
+ sm: 400,
22
+ md: 520,
23
+ lg: 640,
24
+ xl: 800,
25
+ full: "100%",
26
+ };
27
+
28
+ const Modal = ({
29
+ isOpen,
30
+ onClose,
31
+ children,
32
+ variant = "center",
33
+ size = "md",
34
+ closeOnBackdrop = true,
35
+ padding = "28px 32px 24px",
36
+ borderRadius,
37
+ backdropColor = "rgba(0,0,0,0.45)",
38
+ maxWidth,
39
+ }: ModalProps) => {
40
+ if (!isOpen) return null;
41
+
42
+ const resolvedMaxWidth = maxWidth ?? SIZE_MAP[size];
43
+ const resolvedRadius =
44
+ borderRadius ?? (variant === "bottom-sheet" ? "24px 24px 0 0" : 24);
45
+
46
+ const overlayStyle: React.CSSProperties = {
47
+ position: "fixed",
48
+ top: 0,
49
+ left: 0,
50
+ right: 0,
51
+ bottom: 0,
52
+ zIndex: 9999,
53
+ display: "flex",
54
+ alignItems: variant === "bottom-sheet" ? "flex-end" : "center",
55
+ justifyContent: "center",
56
+ padding: variant === "bottom-sheet" ? 0 : 16,
57
+ backgroundColor: backdropColor,
58
+ };
59
+
60
+ const cardStyle: React.CSSProperties = {
61
+ background: "#FFFFFF",
62
+ width: "100%",
63
+ maxWidth: resolvedMaxWidth,
64
+ maxHeight: variant === "bottom-sheet" ? "92vh" : undefined,
65
+ borderRadius: resolvedRadius,
66
+ padding,
67
+ position: "relative",
68
+ boxSizing: "border-box",
69
+ overflowY: variant === "bottom-sheet" ? "auto" : undefined,
70
+ };
71
+
72
+ const modal = (
73
+ <div
74
+ style={overlayStyle}
75
+ onClick={closeOnBackdrop ? onClose : undefined}
76
+ >
77
+ <div
78
+ style={cardStyle}
79
+ onClick={(e) => e.stopPropagation()}
80
+ >
81
+ {children}
82
+ </div>
83
+ </div>
84
+ );
85
+
86
+ if (typeof document !== "undefined") {
87
+ return ReactDOM.createPortal(modal, document.body) as React.ReactElement;
88
+ }
89
+ return modal;
90
+ };
91
+
92
+ export default Modal;
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+
3
+ export interface ModalHeaderProps {
4
+ title?: React.ReactNode;
5
+ onClose?: () => void;
6
+ children?: React.ReactNode;
7
+ }
8
+
9
+ const ModalHeader = ({ title, onClose, children }: ModalHeaderProps) => (
10
+ <div
11
+ style={{
12
+ display: "flex",
13
+ alignItems: "center",
14
+ justifyContent: "space-between",
15
+ marginBottom: 20,
16
+ }}
17
+ >
18
+ <div style={{ flex: 1 }}>
19
+ {title && (
20
+ <span
21
+ style={{
22
+ fontSize: "1rem",
23
+ fontWeight: 700,
24
+ color: "#111827",
25
+ }}
26
+ >
27
+ {title}
28
+ </span>
29
+ )}
30
+ {children}
31
+ </div>
32
+
33
+ {onClose && (
34
+ <button
35
+ onClick={onClose}
36
+ style={{
37
+ width: 36,
38
+ height: 36,
39
+ borderRadius: "50%",
40
+ background: "#F3F4F6",
41
+ border: "none",
42
+ cursor: "pointer",
43
+ fontSize: 14,
44
+ color: "#6B7280",
45
+ display: "flex",
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ flexShrink: 0,
49
+ marginLeft: 12,
50
+ }}
51
+ >
52
+
53
+ </button>
54
+ )}
55
+ </div>
56
+ );
57
+
58
+ export default ModalHeader;
@@ -0,0 +1,4 @@
1
+ export { default as Modal } from "./Modal";
2
+ export { default as ModalHeader } from "./ModalHeader";
3
+ export type { ModalProps, ModalVariant, ModalSize } from "./Modal";
4
+ export type { ModalHeaderProps } from "./ModalHeader";
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { FeedbackConfig } from "./constants";
3
+
4
+ interface FeedbackBannerProps {
5
+ config: FeedbackConfig | null;
6
+ }
7
+
8
+ const FeedbackBanner = ({ config }: FeedbackBannerProps) => {
9
+ if (!config) return null;
10
+ return (
11
+ <div
12
+ style={{
13
+ display: "flex",
14
+ alignItems: "center",
15
+ gap: 12,
16
+ borderRadius: 20,
17
+ padding: "12px 16px",
18
+ marginTop: 16,
19
+ backgroundColor: config.bannerBg,
20
+ }}
21
+ >
22
+ <span style={{ fontSize: 22 }}>{config.emoji}</span>
23
+ <span
24
+ style={{
25
+ fontWeight: 600,
26
+ fontSize: "13.33px",
27
+ color: config.bannerText,
28
+ }}
29
+ >
30
+ {config.message}
31
+ </span>
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export default FeedbackBanner;
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import { FeedbackConfig, MAX_CHARS } from "./constants";
3
+
4
+ interface FeedbackTextareaProps {
5
+ config: FeedbackConfig | null;
6
+ feedback: string;
7
+ onFeedbackChange?: (text: string) => void;
8
+ colors?: {
9
+ primaryColor?: string;
10
+ };
11
+ }
12
+
13
+ const FeedbackTextarea = ({
14
+ config,
15
+ feedback,
16
+ onFeedbackChange,
17
+ colors,
18
+ }: FeedbackTextareaProps) => {
19
+ if (!config) return null;
20
+ return (
21
+ <div style={{ position: "relative", marginTop: 42 }}>
22
+ <span
23
+ style={{
24
+ position: "absolute",
25
+ left: 16,
26
+ top: -10,
27
+ background: "#FFFFFF",
28
+ padding: "0 8px",
29
+ fontSize: "13.33px",
30
+ fontWeight: 600,
31
+ color: colors?.primaryColor || "#374151",
32
+ zIndex: 1,
33
+ }}
34
+ >
35
+ {config.question}
36
+ </span>
37
+
38
+ <div
39
+ style={{
40
+ border: "1.5px solid #E5E7EB",
41
+ borderRadius: 16,
42
+ overflow: "hidden",
43
+ position: "relative",
44
+ background: "transparent",
45
+ }}
46
+ >
47
+ <textarea
48
+ value={feedback}
49
+ onChange={(e) => {
50
+ if (e.target.value.length <= MAX_CHARS)
51
+ onFeedbackChange?.(e.target.value);
52
+ }}
53
+ placeholder="Déjanos tus comentarios (opcional)"
54
+ rows={4}
55
+ style={{
56
+ width: "100%",
57
+ padding: "16px 16px 28px",
58
+ background: "transparent",
59
+ color: "#374151",
60
+ fontSize: "13.33px",
61
+ resize: "none",
62
+ outline: "none",
63
+ border: "none",
64
+ boxSizing: "border-box" as const,
65
+ fontFamily: "inherit",
66
+ }}
67
+ />
68
+ <div
69
+ style={{
70
+ position: "absolute",
71
+ bottom: 8,
72
+ right: 16,
73
+ fontSize: 12,
74
+ color: "#9CA3AF",
75
+ }}
76
+ >
77
+ {feedback.length}/{MAX_CHARS}
78
+ </div>
79
+ </div>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ export default FeedbackTextarea;
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+
3
+ const HeartIcon = () => (
4
+ <svg
5
+ width="28"
6
+ height="28"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="#22c55e"
10
+ strokeWidth="1.8"
11
+ strokeLinecap="round"
12
+ strokeLinejoin="round"
13
+ >
14
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
15
+ </svg>
16
+ );
17
+
18
+ export default HeartIcon;
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import { getZoneColor, getZoneShadow } from "./constants";
3
+
4
+ interface ScoreButtonsProps {
5
+ selectedScore?: number | null;
6
+ onScoreChange?: (score: number) => void;
7
+ buttonHeight?: number;
8
+ fontSize?: number;
9
+ gap?: number;
10
+ colors?: {
11
+ secondaryColor?: string;
12
+ primaryColor?: string;
13
+ };
14
+ }
15
+
16
+ const ScoreButtons = ({
17
+ selectedScore,
18
+ onScoreChange,
19
+ buttonHeight = 54,
20
+ fontSize = 15,
21
+ gap = 8,
22
+ colors,
23
+ }: ScoreButtonsProps) => (
24
+ <div style={{ marginBottom: 4 }}>
25
+ <div
26
+ style={{
27
+ display: "grid",
28
+ gridTemplateColumns: "repeat(10, 1fr)",
29
+ gap,
30
+ }}
31
+ >
32
+ {Array.from({ length: 10 }, (_, i) => i + 1).map((num) => {
33
+ const isSelected = selectedScore === num;
34
+ const zoneColor = getZoneColor(num);
35
+ const activeColor = colors?.secondaryColor || zoneColor;
36
+ return (
37
+ <button
38
+ key={num}
39
+ onClick={() => onScoreChange?.(num)}
40
+ onMouseEnter={(e) => {
41
+ if (!isSelected) {
42
+ e.currentTarget.style.borderColor = activeColor;
43
+ e.currentTarget.style.color = activeColor;
44
+ }
45
+ }}
46
+ onMouseLeave={(e) => {
47
+ if (!isSelected) {
48
+ e.currentTarget.style.borderColor = "#E5E7EB";
49
+ e.currentTarget.style.color = "#111827";
50
+ }
51
+ }}
52
+ style={{
53
+ height: buttonHeight,
54
+ borderRadius: 10,
55
+ fontSize,
56
+ border: isSelected ? "none" : "1px solid #E5E7EB",
57
+ background: isSelected ? activeColor : "#FFFFFF",
58
+ color: isSelected ? "#FFFFFF" : "#111827",
59
+ cursor: "pointer",
60
+ transition: "border-color 0.15s, color 0.15s, background 0.15s",
61
+ boxShadow: isSelected
62
+ ? colors?.secondaryColor
63
+ ? `0 2px 8px ${colors.secondaryColor}58`
64
+ : getZoneShadow(num)
65
+ : "none",
66
+ display: "flex",
67
+ alignItems: "center",
68
+ justifyContent: "center",
69
+ boxSizing: "border-box" as const,
70
+ }}
71
+ >
72
+ {num}
73
+ </button>
74
+ );
75
+ })}
76
+ </div>
77
+
78
+ <div
79
+ style={{
80
+ display: "flex",
81
+ justifyContent: "space-between",
82
+ marginTop: 8,
83
+ }}
84
+ >
85
+ <span style={{ fontSize: 12, color: "#9CA3AF" }}>Poco probable</span>
86
+ <span style={{ fontSize: 12, color: "#9CA3AF" }}>Muy probable</span>
87
+ </div>
88
+ </div>
89
+ );
90
+
91
+ export default ScoreButtons;
@@ -0,0 +1,145 @@
1
+ import React from "react";
2
+ import { BRAND } from "./constants";
3
+
4
+ interface SurveyFooterProps {
5
+ selectedScore?: number | null;
6
+ onSkip?: () => void;
7
+ onSubmit: () => void;
8
+ layout?: "inline" | "stacked";
9
+ colors?: {
10
+ secondaryColor?: string;
11
+ tertiaryColor?: string;
12
+ primaryColor?: string;
13
+ };
14
+ }
15
+
16
+ const SurveyFooter = ({
17
+ selectedScore,
18
+ onSkip,
19
+ onSubmit,
20
+ layout = "inline",
21
+ colors,
22
+ }: SurveyFooterProps) => {
23
+ if (layout === "stacked") {
24
+ return (
25
+ <div style={{ marginTop: 24 }}>
26
+ <button
27
+ onClick={onSkip}
28
+ style={{
29
+ display: "block",
30
+ width: "100%",
31
+ textAlign: "center",
32
+ fontSize: "13.33px",
33
+ color: "#6B7280",
34
+ background: "none",
35
+ border: "none",
36
+ cursor: "pointer",
37
+ padding: "0 0 14px",
38
+ }}
39
+ >
40
+ Saltar por ahora
41
+ </button>
42
+
43
+ <button
44
+ onClick={onSubmit}
45
+ disabled={selectedScore == null}
46
+ style={{
47
+ display: "flex",
48
+ alignItems: "center",
49
+ justifyContent: "center",
50
+ gap: 8,
51
+ width: "100%",
52
+ padding: "16px",
53
+ borderRadius: 999,
54
+ fontSize: "13.33px",
55
+ fontWeight: 700,
56
+ letterSpacing: "0.08em",
57
+ textTransform: "uppercase" as const,
58
+ border: "none",
59
+ cursor: selectedScore != null ? "pointer" : "not-allowed",
60
+ background:
61
+ selectedScore != null
62
+ ? colors?.secondaryColor
63
+ : "rgba(232,76,13,0.28)",
64
+ color: "#FFFFFF",
65
+ transition: "background 0.15s",
66
+ }}
67
+ >
68
+ Enviar comentarios <span>→</span>
69
+ </button>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <>
76
+ <div
77
+ style={{
78
+ display: "flex",
79
+ alignItems: "center",
80
+ justifyContent: "space-between",
81
+ marginTop: 24,
82
+ }}
83
+ >
84
+ <button
85
+ onClick={onSkip}
86
+ style={{
87
+ fontSize: "13.33px",
88
+ color: "#6B7280",
89
+ background: "none",
90
+ border: "none",
91
+ cursor: "pointer",
92
+ padding: 0,
93
+ }}
94
+ >
95
+ Saltar por ahora
96
+ </button>
97
+
98
+ <button
99
+ onClick={onSubmit}
100
+ disabled={selectedScore == null}
101
+ style={{
102
+ display: "flex",
103
+ alignItems: "center",
104
+ gap: 8,
105
+ padding: "14px 28px",
106
+ borderRadius: 999,
107
+ fontSize: "13.33px",
108
+ fontWeight: 700,
109
+ letterSpacing: "0.08em",
110
+ textTransform: "uppercase" as const,
111
+ border: "none",
112
+ cursor: selectedScore != null ? "pointer" : "not-allowed",
113
+ background:
114
+ selectedScore != null
115
+ ? colors?.secondaryColor
116
+ : "rgba(232,76,13,0.28)",
117
+ color: "#FFFFFF",
118
+ transition: "background 0.15s",
119
+ minWidth: 160,
120
+ justifyContent: "center",
121
+ }}
122
+ >
123
+ Enviar comentarios <span>→</span>
124
+ </button>
125
+ </div>
126
+
127
+ <div
128
+ style={{
129
+ display: "flex",
130
+ alignItems: "center",
131
+ justifyContent: "center",
132
+ gap: 6,
133
+ marginTop: 14,
134
+ fontSize: 12,
135
+ color: "#9CA3AF",
136
+ }}
137
+ >
138
+ <span>🔒</span>
139
+ <span>Anónimo · solo se usa para mejorar el servicio</span>
140
+ </div>
141
+ </>
142
+ );
143
+ };
144
+
145
+ export default SurveyFooter;