kupos-ui-components-lib 9.10.1 → 9.10.3

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.
@@ -0,0 +1,22 @@
1
+ export interface SurveyProps {
2
+ variant?: "mobile" | "desktop";
3
+ isOpen?: boolean;
4
+ selectedScore?: number | null;
5
+ onScoreChange?: (score: number) => void;
6
+ feedback?: string;
7
+ onFeedbackChange?: (text: string) => void;
8
+ onClose?: () => void;
9
+ onSkip?: () => void;
10
+ onSubmit?: (score: number, feedback: string) => void;
11
+ isSubmitted?: boolean;
12
+ isLoading?: boolean;
13
+ colors?: {
14
+ secondaryColor?: string;
15
+ tertiaryColor?: string;
16
+ primaryColor?: string;
17
+ };
18
+ icons?: {
19
+ surveyIcon?: string;
20
+ closeIcon?: string;
21
+ };
22
+ }
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";
@@ -250,7 +250,7 @@ const FeatureServiceUi = ({
250
250
  </div>
251
251
  </div>
252
252
 
253
- <div className="flex flex-col gap-[8px]">
253
+ <div className="flex flex-col gap-[10px]">
254
254
  <div className="text-[12px] bold-text">
255
255
  {travelDate
256
256
  ? new Date(travelDate).toLocaleDateString("es-CL", {
@@ -363,9 +363,8 @@ const FeatureServiceUi = ({
363
363
  {/* MIDDLE: competing operators + viewers */}
364
364
  <div className="min-w-0 px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]">
365
365
  <div className="text-center">
366
- <div className="bold-text text-[14px]">
367
- {operatorsCompetingCount} operadores compitiendo
368
- <br /> por tu compra
366
+ <div className="bold-text text-[13px]">
367
+ {operatorsCompetingCount} operadores compitiendo por tu compra
369
368
  </div>
370
369
  </div>
371
370
 
@@ -377,7 +376,7 @@ const FeatureServiceUi = ({
377
376
  style={{
378
377
  // height: "80px",
379
378
  border: "1px solid #363c48",
380
- backgroundColor: "#1a202e",
379
+ backgroundColor: "#fff",
381
380
  padding: "14px 10px",
382
381
  }}
383
382
  >
@@ -393,13 +392,13 @@ const FeatureServiceUi = ({
393
392
  isSoldOut ? "grayscale" : ""
394
393
  }`}
395
394
  />
396
- <span className="text-[11px] truncate max-w-full text-center">
395
+ <span className="text-[11px] truncate max-w-full text-center text-[#464647]">
397
396
  {op.name}
398
397
  </span>
399
398
  <div className="bg-[#FF8F45] text-white text-[12px] font-bold px-[10px] py-[4px] rounded-[4px] bold-text whitespace-nowrap">
400
399
  <span>{op?.time}</span>
401
400
  </div>
402
- <span className="text-[10px] mt-[6px]">
401
+ <span className="text-[10px] mt-[6px] text-[#464647]">
403
402
  {op?.seatsAvailable}
404
403
  </span>
405
404
  </div>
@@ -407,10 +406,13 @@ const FeatureServiceUi = ({
407
406
  </div>
408
407
 
409
408
  <div
410
- className="flex w-full items-center justify-center gap-[6px] text-[12px]"
409
+ className="flex w-full items-center justify-center gap-[6px] text-[12px] rounded-full"
411
410
  style={{
412
411
  padding: "8px 14px",
413
412
  marginBottom: "6px",
413
+ border: "1px solid #363c48",
414
+ backgroundColor: "#1a202e",
415
+ // padding: "14px 10px",
414
416
  }}
415
417
  >
416
418
  <LottiePlayer
@@ -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;