kupos-ui-components-lib 10.4.13 → 10.4.15

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 (56) hide show
  1. package/dist/assets/images/anims/service_list/60_percent_anim.json +1 -0
  2. package/dist/assets/images/filter/arrow-down.png +0 -0
  3. package/dist/assets/images/filter/rapido.svg +14 -0
  4. package/dist/components/FilterSortBar/FilterChip.d.ts +6 -2
  5. package/dist/components/FilterSortBar/FilterChip.js +10 -32
  6. package/dist/components/FilterSortBar/FilterSortBarMobile.d.ts +1 -1
  7. package/dist/components/FilterSortBar/FilterSortBarMobile.js +86 -38
  8. package/dist/components/FilterSortBar/FiltersMasterSheet.d.ts +3 -1
  9. package/dist/components/FilterSortBar/FiltersMasterSheet.js +28 -97
  10. package/dist/components/FilterSortBar/OptionCheckboxList.js +5 -16
  11. package/dist/components/FilterSortBar/PriceRangeSlider.js +17 -55
  12. package/dist/components/FilterSortBar/SheetFooter.js +3 -30
  13. package/dist/components/FilterSortBar/defaultIcons.js +9 -0
  14. package/dist/components/FilterSortBar/icons.d.ts +7 -0
  15. package/dist/components/FilterSortBar/icons.js +8 -1
  16. package/dist/components/FilterSortBar/index.d.ts +1 -1
  17. package/dist/components/FilterSortBar/types.d.ts +34 -0
  18. package/dist/components/PaymentOptions/PaymentOptionsDesktop.d.ts +4 -0
  19. package/dist/components/PaymentOptions/PaymentOptionsDesktop.js +5 -0
  20. package/dist/components/PaymentOptions/PaymentOptionsMobile.d.ts +4 -0
  21. package/dist/components/PaymentOptions/PaymentOptionsMobile.js +5 -0
  22. package/dist/components/PaymentOptions/ResponsivePaymentOptions.d.ts +3 -0
  23. package/dist/components/PaymentOptions/ResponsivePaymentOptions.js +10 -0
  24. package/dist/components/PaymentOptions/index.d.ts +5 -0
  25. package/dist/components/PaymentOptions/index.js +4 -0
  26. package/dist/components/PaymentOptions/mobileTypes.d.ts +0 -0
  27. package/dist/components/PaymentOptions/mobileTypes.js +0 -0
  28. package/dist/components/PaymentOptions/type.d.ts +4 -0
  29. package/dist/components/PaymentOptions/type.js +1 -0
  30. package/dist/components/PaymentSideBar/PaymentSideBarMobile.js +1 -1
  31. package/dist/components/ServiceItem/ExpandedDropdown.d.ts +19 -0
  32. package/dist/components/ServiceItem/ExpandedDropdown.js +28 -0
  33. package/dist/components/ServiceItem/ServiceItemDesktopPeru.d.ts +6 -0
  34. package/dist/components/ServiceItem/ServiceItemDesktopPeru.js +612 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/styles.css +233 -0
  37. package/dist/ui/PaymentCardMobile/PaymentCardMobile.js +40 -51
  38. package/dist/ui/TopAmenities/PromoCountdown.d.ts +18 -0
  39. package/dist/ui/TopAmenities/PromoCountdown.js +55 -0
  40. package/package.json +2 -2
  41. package/src/assets/images/filter/arrow-down.png +0 -0
  42. package/src/assets/images/filter/rapido.svg +14 -0
  43. package/src/components/FilterSortBar/FilterChip.tsx +31 -35
  44. package/src/components/FilterSortBar/FilterSortBarMobile.tsx +122 -66
  45. package/src/components/FilterSortBar/FiltersMasterSheet.tsx +34 -118
  46. package/src/components/FilterSortBar/OptionCheckboxList.tsx +5 -16
  47. package/src/components/FilterSortBar/PriceRangeSlider.tsx +20 -61
  48. package/src/components/FilterSortBar/SheetFooter.tsx +4 -32
  49. package/src/components/FilterSortBar/defaultIcons.ts +11 -0
  50. package/src/components/FilterSortBar/icons.tsx +38 -2
  51. package/src/components/FilterSortBar/index.ts +2 -0
  52. package/src/components/FilterSortBar/types.ts +36 -0
  53. package/src/components/PaymentSideBar/PaymentSideBarMobile.tsx +4 -1
  54. package/src/index.ts +2 -0
  55. package/src/ui/PaymentCardMobile/PaymentCardMobile.tsx +136 -98
  56. package/src/components/FilterSortBar/SingleFilterSheet.tsx +0 -82
@@ -1,9 +1,22 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
  import FilterChip from "./FilterChip";
3
- import SingleFilterSheet from "./SingleFilterSheet";
4
3
  import FiltersMasterSheet from "./FiltersMasterSheet";
5
4
  import { DEFAULT_ICONS } from "./defaultIcons";
6
- import { FilterCategory, FilterCategoryKey, FilterSortBarMobileProps } from "./types";
5
+ import { FilterCategory, FilterCategoryKey, FilterSortBarMobileProps, SortOption } from "./types";
6
+
7
+ // Sort chips reuse an existing category or time-of-day icon where the
8
+ // concept already has one (Horario→clock, Empresas→bus, Precio→piggy bank,
9
+ // Después de las 6 PM→moon, Antes de las 6 AM→sunrise, Salón Cama→seat);
10
+ // only keys with no such match (e.g. "duration" for Rápido) fall back to
11
+ // icons.sortIcons.
12
+ const SORT_ICON_SOURCE: Partial<Record<string, { category?: FilterCategoryKey; timeOfDay?: "sunrise" | "morning" | "sunset" | "night" }>> = {
13
+ time: { category: "time" },
14
+ company: { category: "company" },
15
+ price: { category: "price" },
16
+ after_6pm: { timeOfDay: "night" },
17
+ before_6am: { timeOfDay: "sunrise" },
18
+ cama: { category: "seatType" },
19
+ };
7
20
 
8
21
  const DEFAULT_COLORS = {
9
22
  primaryColor: "#FF5C60",
@@ -12,8 +25,6 @@ const DEFAULT_COLORS = {
12
25
  chipTextColor: "#464647",
13
26
  badgeBackgroundColor: "#FFCBCC",
14
27
  badgeTextColor: "#FF5C60",
15
- // Falls back to primaryColor/primaryTextColor below when the consumer
16
- // doesn't pass a dedicated CTA button color.
17
28
  buttonColor: "#FF5C60",
18
29
  buttonTextColor: "#FFFFFF",
19
30
  };
@@ -25,7 +36,10 @@ const getCategoryActiveCount = (category: FilterCategory) => {
25
36
  ? 1
26
37
  : 0;
27
38
  }
28
- return category.options?.filter((o) => o.selected).length || 0;
39
+ // checkedViaSort options are a display mirror of an active sort chip, not
40
+ // a real selection — the sort chip's own active count already covers it,
41
+ // so counting it here too would double it.
42
+ return category.options?.filter((o) => o.selected && !o.checkedViaSort).length || 0;
29
43
  };
30
44
 
31
45
  const isCategoryActive = (category: FilterCategory) => getCategoryActiveCount(category) > 0;
@@ -40,6 +54,8 @@ const FilterSortBarMobile = ({
40
54
  categories,
41
55
  onApply,
42
56
  onClearFilter,
57
+ sortOptions,
58
+ onSortChange,
43
59
  currencySign = "$",
44
60
  colors,
45
61
  icons,
@@ -55,6 +71,14 @@ const FilterSortBarMobile = ({
55
71
  categoryIcons: { ...DEFAULT_ICONS.categoryIcons, ...icons?.categoryIcons },
56
72
  categoryIconsActive: { ...DEFAULT_ICONS.categoryIconsActive, ...icons?.categoryIconsActive },
57
73
  timeOfDayIcons: { ...DEFAULT_ICONS.timeOfDayIcons, ...icons?.timeOfDayIcons },
74
+ sortIcons: { ...DEFAULT_ICONS.sortIcons, ...icons?.sortIcons },
75
+ };
76
+
77
+ const resolveSortIcon = (key: string): string | undefined => {
78
+ const source = SORT_ICON_SOURCE[key];
79
+ if (source?.category) return resolvedIcons.categoryIcons?.[source.category];
80
+ if (source?.timeOfDay) return resolvedIcons.timeOfDayIcons?.[source.timeOfDay];
81
+ return resolvedIcons.sortIcons?.[key];
58
82
  };
59
83
 
60
84
  // Re-sync the draft whenever the committed categories change from the
@@ -63,9 +87,14 @@ const FilterSortBarMobile = ({
63
87
  setDraftCategories(categories);
64
88
  }, [categories]);
65
89
 
66
- const openCategory = draftCategories.find((c) => c.key === openSheet) || null;
67
- const isMasterOpen = openSheet === "all";
68
- const totalActiveCategories = categories.filter(isCategoryActive).length;
90
+ // Every filter chip opens the same master sheet, pre-selecting whichever
91
+ // category was tapped ("all" — the Filtros button — leaves it unset so
92
+ // FiltersMasterSheet falls back to its first category).
93
+ const isMasterOpen = openSheet !== null;
94
+ const initialCategoryKey = openSheet && openSheet !== "all" ? openSheet : undefined;
95
+ const totalActiveCategories =
96
+ categories.filter(isCategoryActive).length +
97
+ (sortOptions || []).filter((s) => !s.direction && s.active).length;
69
98
 
70
99
  const handleScroll = () => {
71
100
  setIsScrolled((scrollRef.current?.scrollLeft ?? 0) > 4);
@@ -103,15 +132,6 @@ const FilterSortBarMobile = ({
103
132
  );
104
133
  };
105
134
 
106
- const handleApplySingle = () => {
107
- if (openSheet && openSheet !== "all") onApply(openSheet, draftCategories);
108
- closeSheet();
109
- };
110
-
111
- const handleClearSingle = () => {
112
- if (openSheet && openSheet !== "all") onClearFilter(openSheet);
113
- };
114
-
115
135
  const handleApplyAll = () => {
116
136
  onApply("all", draftCategories);
117
137
  closeSheet();
@@ -137,9 +157,45 @@ const FilterSortBarMobile = ({
137
157
  onApply(categoryKey, updated);
138
158
  };
139
159
 
160
+ // Directional sorts (Horario, Precio, Rápido) are mutually exclusive —
161
+ // picking one deactivates the others, and re-tapping the active one flips
162
+ // its direction. Cohort sorts (no `direction`, e.g. Después de las 6 PM,
163
+ // Salón Cama) are independent toggles that layer on top of whichever
164
+ // directional sort is active.
165
+ const handleSortChipClick = (key: string) => {
166
+ if (!sortOptions || !onSortChange) return;
167
+ const target = sortOptions.find((s) => s.key === key);
168
+ if (!target) return;
169
+
170
+ const updated: SortOption[] = sortOptions.map((s) => {
171
+ if (s.direction) {
172
+ if (s.key !== key) return s.active ? { ...s, active: false } : s;
173
+ return s.active ? { ...s, direction: s.direction === "asc" ? "desc" : "asc" } : { ...s, active: true };
174
+ }
175
+ return s.key === key ? { ...s, active: !s.active } : s;
176
+ });
177
+ onSortChange(updated);
178
+
179
+ // A cohort sort chip's matching checkbox (checkedViaSort: true, option
180
+ // id === sort key) mirrors this chip inside the Filtros sheet. Keep that
181
+ // mirror in sync when the chip is toggled from the bar — otherwise the
182
+ // chip turns off but the underlying filter option stays selected.
183
+ if (!target.direction) {
184
+ const nextSelected = !target.active;
185
+ const hasMirror = categories.some((cat) => cat.options?.some((o) => o.checkedViaSort && o.id === key));
186
+ if (hasMirror) {
187
+ const updatedCategories = categories.map((cat) => ({
188
+ ...cat,
189
+ options: cat.options?.map((o) => (o.checkedViaSort && o.id === key ? { ...o, selected: nextSelected } : o)),
190
+ }));
191
+ onApply("all", updatedCategories);
192
+ }
193
+ }
194
+ };
195
+
140
196
  return (
141
197
  <div>
142
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
198
+ <div className="flex items-center gap-[8px]">
143
199
  <FilterChip
144
200
  label="Filtros"
145
201
  active={totalActiveCategories > 0}
@@ -152,73 +208,73 @@ const FilterSortBarMobile = ({
152
208
  icons={resolvedIcons}
153
209
  />
154
210
  <div
155
- style={{
156
- width: 1,
157
- alignSelf: "stretch",
158
- backgroundColor: resolvedColors.chipBorderColor,
159
- opacity: 0.5,
160
- flexShrink: 0,
161
- }}
211
+ className="w-px self-stretch flex-shrink-0 opacity-50"
212
+ style={{ backgroundColor: resolvedColors.chipBorderColor }}
162
213
  />
163
214
  <div
164
215
  ref={scrollRef}
165
216
  onScroll={handleScroll}
166
- style={{
167
- display: "flex",
168
- alignItems: "center",
169
- gap: 8,
170
- overflowX: "auto",
171
- scrollbarWidth: "none",
172
- paddingBottom: 2,
173
- }}
217
+ className="flex items-center gap-[8px] overflow-x-auto pb-[2px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
174
218
  >
175
- {categories.map((category) => (
219
+ {sortOptions?.map((sort) => (
176
220
  <FilterChip
177
- key={category.key}
178
- label={category.label}
179
- active={isCategoryActive(category)}
180
- isOpen={openSheet === category.key}
181
- onClick={() => openSheetFor(category.key)}
182
- iconSrc={resolvedIcons.categoryIcons?.[category.key]}
183
- activeIconSrc={resolvedIcons.categoryIconsActive?.[category.key]}
184
- badgeCount={getCategoryActiveCount(category)}
221
+ key={`sort:${sort.key}`}
222
+ label={sort.label}
223
+ active={sort.active}
224
+ isOpen={false}
225
+ onClick={() => handleSortChipClick(sort.key)}
226
+ iconSrc={resolveSortIcon(sort.key)}
227
+ sortDirection={sort.direction && sort.active ? sort.direction : undefined}
228
+ hideTrailingIcon={!(sort.direction && sort.active)}
229
+ badgeCount={sort.active ? sort.count : undefined}
185
230
  colors={resolvedColors}
186
231
  icons={resolvedIcons}
187
232
  />
188
233
  ))}
234
+ {categories
235
+ // hideChip categories never get a standalone top-bar chip, even
236
+ // once something is selected inside the Filtros sheet — surfacing
237
+ // it there duplicated the dedicated sort chip for the same
238
+ // concept (e.g. "Horarios" filter next to the "Horario" sort chip).
239
+ .filter((category) => !category.hideChip)
240
+ .map((category) => (
241
+ <FilterChip
242
+ key={category.key}
243
+ label={category.label}
244
+ active={isCategoryActive(category)}
245
+ isOpen={openSheet === category.key}
246
+ onClick={() => openSheetFor(category.key)}
247
+ iconSrc={resolvedIcons.categoryIcons?.[category.key]}
248
+ activeIconSrc={resolvedIcons.categoryIconsActive?.[category.key]}
249
+ badgeCount={getCategoryActiveCount(category)}
250
+ colors={resolvedColors}
251
+ icons={resolvedIcons}
252
+ />
253
+ ))}
189
254
  {categories
190
255
  .filter((category) => category.inlineOptions && category.options?.length)
191
256
  .flatMap((category) =>
192
- (category.options || []).map((option) => (
193
- <FilterChip
194
- key={`${category.key}:${option.id}`}
195
- label={option.label}
196
- active={option.selected}
197
- isOpen={false}
198
- onClick={() => handleInlineOptionToggle(category.key, option.id)}
199
- iconSrc={option.icon ? resolvedIcons.timeOfDayIcons?.[option.icon] : undefined}
200
- colors={resolvedColors}
201
- icons={resolvedIcons}
202
- />
203
- )),
257
+ (category.options || [])
258
+ .filter((option) => !option.excludeFromInline)
259
+ .map((option) => (
260
+ <FilterChip
261
+ key={`${category.key}:${option.id}`}
262
+ label={option.label}
263
+ active={option.selected}
264
+ isOpen={false}
265
+ onClick={() => handleInlineOptionToggle(category.key, option.id)}
266
+ iconSrc={option.icon ? resolvedIcons.timeOfDayIcons?.[option.icon] : undefined}
267
+ colors={resolvedColors}
268
+ icons={resolvedIcons}
269
+ />
270
+ )),
204
271
  )}
205
272
  </div>
206
273
  </div>
207
274
 
208
- <SingleFilterSheet
209
- category={openCategory}
210
- onClose={closeSheet}
211
- onToggleOption={(optionId) => openSheet && openSheet !== "all" && handleDraftToggleOption(openSheet, optionId)}
212
- onPriceRangeChange={(min, max) => openSheet && openSheet !== "all" && handleDraftPriceRangeChange(openSheet, min, max)}
213
- onClear={handleClearSingle}
214
- onApply={handleApplySingle}
215
- currencySign={currencySign}
216
- colors={resolvedColors}
217
- icons={resolvedIcons}
218
- />
219
-
220
275
  <FiltersMasterSheet
221
276
  isOpen={isMasterOpen}
277
+ initialCategoryKey={initialCategoryKey}
222
278
  categories={draftCategories}
223
279
  onClose={closeSheet}
224
280
  onToggleOption={handleDraftToggleOption}
@@ -1,6 +1,6 @@
1
- import React, { useMemo, useState } from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
  import { BottomSheet } from "../../ui/BottomSheet";
3
- import { CloseIcon, SearchIcon } from "./icons";
3
+ import { CloseIcon } from "./icons";
4
4
  import OptionCheckboxList from "./OptionCheckboxList";
5
5
  import PriceRangeSlider from "./PriceRangeSlider";
6
6
  import SheetFooter from "./SheetFooter";
@@ -8,6 +8,8 @@ import { FilterCategory, FilterCategoryKey, FilterSortBarColors, FilterSortBarIc
8
8
 
9
9
  interface FiltersMasterSheetProps {
10
10
  isOpen: boolean;
11
+ /** Category to pre-select when the sheet opens — set when a specific filter chip (rather than the Filtros button) was tapped. */
12
+ initialCategoryKey?: FilterCategoryKey;
11
13
  categories: FilterCategory[];
12
14
  onClose: () => void;
13
15
  onToggleOption: (categoryKey: FilterCategoryKey, optionId: string) => void;
@@ -21,6 +23,7 @@ interface FiltersMasterSheetProps {
21
23
 
22
24
  const FiltersMasterSheet = ({
23
25
  isOpen,
26
+ initialCategoryKey,
24
27
  categories,
25
28
  onClose,
26
29
  onToggleOption,
@@ -31,124 +34,49 @@ const FiltersMasterSheet = ({
31
34
  colors,
32
35
  icons,
33
36
  }: FiltersMasterSheetProps) => {
34
- const [activeKey, setActiveKey] = useState<FilterCategoryKey>(categories[0]?.key ?? "time");
35
- const [searchQuery, setSearchQuery] = useState("");
37
+ const [activeKey, setActiveKey] = useState<FilterCategoryKey>(initialCategoryKey ?? categories[0]?.key ?? "time");
36
38
 
37
- const activeCategory = categories.find((c) => c.key === activeKey) || categories[0];
39
+ // Reset the pre-selected tab every time the sheet is (re)opened, whether
40
+ // via a specific filter chip or the general Filtros button.
41
+ useEffect(() => {
42
+ if (isOpen) {
43
+ setActiveKey(initialCategoryKey ?? categories[0]?.key ?? "time");
44
+ }
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, [isOpen]);
38
47
 
39
- const filteredOptions = useMemo(() => {
40
- if (!activeCategory?.options) return [];
41
- if (!searchQuery.trim()) return activeCategory.options;
42
- const q = searchQuery.trim().toLowerCase();
43
- return activeCategory.options.filter((o) => o.label.toLowerCase().includes(q));
44
- }, [activeCategory, searchQuery]);
48
+ const activeCategory = categories.find((c) => c.key === activeKey) || categories[0];
45
49
 
46
50
  if (!isOpen) return null;
47
51
 
48
52
  return (
49
- <BottomSheet isOpen={isOpen} onClose={onClose} showBackdrop height="75vh" padding="20px 0 0px">
50
- <div style={{ display: "flex", flexDirection: "column", height: "calc(75vh - 40px)" }}>
51
- <div
52
- style={{
53
- display: "flex",
54
- alignItems: "center",
55
- justifyContent: "space-between",
56
- padding: "0 20px 14px",
57
- }}
58
- >
59
- <span style={{ fontSize: 16, fontWeight: 700, color: "#464647" }}>Filtros</span>
60
- <div
61
- onClick={onClose}
62
- style={{ cursor: "pointer", padding: 4, WebkitTapHighlightColor: "transparent" }}
63
- >
64
- <CloseIcon size={14} src={icons?.closeIcon} />
65
- </div>
66
- </div>
67
-
68
- <div style={{ padding: "0 20px 14px" }}>
69
- <div
70
- style={{
71
- display: "flex",
72
- alignItems: "center",
73
- gap: 8,
74
- border: "1px solid #E5E5E5",
75
- borderRadius: 10,
76
- padding: "9px 12px",
77
- }}
78
- >
79
- <SearchIcon size={15} src={icons?.searchIcon} />
80
- <input
81
- value={searchQuery}
82
- onChange={(e) => setSearchQuery(e.target.value)}
83
- placeholder="Buscar filtro"
84
- style={{
85
- border: "none",
86
- outline: "none",
87
- fontSize: 13,
88
- flex: 1,
89
- color: "#464647",
90
- background: "transparent",
91
- }}
92
- />
53
+ <BottomSheet isOpen={isOpen} onClose={onClose} showBackdrop height="65vh" padding="20px 0 0px">
54
+ <div className="flex flex-col h-[calc(65vh-40px)]">
55
+ <div className="flex items-center justify-between px-[20px] pb-[14px]">
56
+ <span className="text-[16px] font-bold text-[#464647] font-['DMSans-Bold',sans-serif]">Filtros</span>
57
+ <div onClick={onClose} className="cursor-pointer p-[4px] [-webkit-tap-highlight-color:transparent]">
58
+ <CloseIcon size={22} src={icons?.closeIcon} />
93
59
  </div>
94
60
  </div>
95
61
 
96
- <div style={{ display: "flex", flex: 1, overflow: "hidden", position: "relative", borderTop: "1px solid #E5E5E5", borderBottom: "1px solid #E5E5E5" }}>
97
- <div
98
- style={{
99
- position: "absolute",
100
- top: 0,
101
- bottom: 0,
102
- left: 0,
103
- width: 130,
104
- backgroundColor: "#F3F4F6",
105
- zIndex: 0,
106
- }}
107
- />
108
- <div
109
- style={{
110
- width: 130,
111
- flexShrink: 0,
112
- position: "relative",
113
- zIndex: 1,
114
- }}
115
- >
62
+ <div className="flex flex-1 overflow-hidden relative border-t border-b border-[#E5E5E5]">
63
+ <div className="absolute top-0 bottom-0 left-0 w-[120px] bg-[#F9FAFB] z-0" />
64
+ <div className="w-[120px] flex-shrink-0 relative z-[1] bg-[#F9FAFB]">
116
65
  {categories.map((cat) => {
117
66
  const isActive = cat.key === activeKey;
118
67
  return (
119
68
  <div
120
69
  key={cat.key}
121
70
  onClick={() => setActiveKey(cat.key)}
122
- style={{
123
- position: "relative",
124
- display: "flex",
125
- alignItems: "center",
126
- minHeight: 44,
127
- boxSizing: "border-box",
128
- padding: "8px 8px 8px 20px",
129
- fontSize: 13,
130
- lineHeight: 1.25,
131
- fontWeight: isActive ? 700 : 400,
132
- color: isActive ? colors.primaryColor : "#464647",
133
- backgroundColor: isActive ? "#fff" : "transparent",
134
- cursor: "pointer",
135
- WebkitTapHighlightColor: "transparent",
136
- userSelect: "none",
137
- WebkitUserSelect: "none",
138
- }}
71
+ className={`relative flex items-center min-h-[44px] box-border py-[8px] pr-[8px] pl-[20px] text-[13px] leading-[1.25] cursor-pointer select-none [-webkit-tap-highlight-color:transparent] ${
72
+ isActive ? "font-bold bg-[#FFFFFF]" : "font-normal bg-transparent"
73
+ }`}
74
+ style={{ color: isActive ? colors.primaryColor : "#464647" }}
139
75
  >
140
76
  {isActive && (
141
77
  <div
142
- style={{
143
- position: "absolute",
144
- left: 0,
145
- top: 8,
146
- bottom: 8,
147
- width: 4,
148
- borderTopRightRadius: 4,
149
- borderBottomRightRadius: 4,
150
- backgroundColor: colors.primaryColor,
151
- }}
78
+ className="absolute left-[0px] top-[8px] bottom-[8px] w-[4px] rounded-tr-[4px] rounded-br-[4px]"
79
+ style={{ backgroundColor: colors.primaryColor }}
152
80
  />
153
81
  )}
154
82
  {cat.label}
@@ -157,21 +85,9 @@ const FiltersMasterSheet = ({
157
85
  })}
158
86
  </div>
159
87
 
160
- <div
161
- style={{ flex: 1, overflowY: "auto", paddingLeft: 16, paddingRight: 20, position: "relative", zIndex: 1, backgroundColor: "#fff" }}
162
- >
88
+ <div className="flex-1 overflow-y-auto pl-[16px] pr-[20px] relative z-[1] bg-white">
163
89
  {activeCategory && (
164
- <div
165
- style={{
166
- fontSize: 11,
167
- fontWeight: 700,
168
- color: "#9B9B9B",
169
- textTransform: "uppercase",
170
- letterSpacing: 0.4,
171
- marginBottom: 5,
172
- marginTop: 15,
173
- }}
174
- >
90
+ <div className="text-[11px] font-bold text-[#9B9B9B] uppercase tracking-[0.4px] mb-[5px] mt-[15px]">
175
91
  {activeCategory.sectionLabel || activeCategory.label}
176
92
  </div>
177
93
  )}
@@ -186,7 +102,7 @@ const FiltersMasterSheet = ({
186
102
  />
187
103
  ) : (
188
104
  <OptionCheckboxList
189
- options={filteredOptions}
105
+ options={activeCategory?.options || []}
190
106
  kind={activeCategory?.kind || "checkbox"}
191
107
  onToggle={(optionId) => onToggleOption(activeCategory.key, optionId)}
192
108
  primaryColor={colors.primaryColor}
@@ -197,7 +113,7 @@ const FiltersMasterSheet = ({
197
113
  </div>
198
114
  </div>
199
115
 
200
- <div style={{ padding: "0px 20px 0" }}>
116
+ <div className="px-[20px]">
201
117
  <SheetFooter onClear={onClearAll} onApply={onApplyAll} colors={colors} />
202
118
  </div>
203
119
  </div>
@@ -23,7 +23,7 @@ const RatingLabel = ({
23
23
  starColor: string;
24
24
  icons?: FilterSortBarIcons;
25
25
  }) => (
26
- <div style={{ display: "flex", alignItems: "center", gap: 1, whiteSpace: "nowrap" }}>
26
+ <div className="flex items-center gap-[1px] whitespace-nowrap">
27
27
  {Array.from({ length: 5 }, (_, i) => (
28
28
  <StarIcon
29
29
  key={i}
@@ -34,14 +34,14 @@ const RatingLabel = ({
34
34
  emptySrc={icons?.starEmptyIcon}
35
35
  />
36
36
  ))}
37
- <span style={{ marginLeft: 6 }}>{label}</span>
37
+ <span className="ml-[6px]">{label}</span>
38
38
  </div>
39
39
  );
40
40
 
41
41
  const OptionCheckboxList = ({ options, kind, onToggle, primaryColor, starColor, icons }: OptionCheckboxListProps) => {
42
42
  if (!options?.length) {
43
43
  return (
44
- <div style={{ padding: "24px 0", textAlign: "center", color: "#9B9B9B", fontSize: 13 }}>
44
+ <div className="py-[24px] text-center text-[#9B9B9B] text-[13px]">
45
45
  No hay opciones disponibles
46
46
  </div>
47
47
  );
@@ -53,23 +53,12 @@ const OptionCheckboxList = ({ options, kind, onToggle, primaryColor, starColor,
53
53
  <div
54
54
  key={option.id}
55
55
  onClick={() => onToggle(option.id)}
56
- style={{
57
- display: "flex",
58
- alignItems: "center",
59
- justifyContent: "space-between",
60
- padding: "12px 2px",
61
- cursor: "pointer",
62
- fontSize: 13,
63
- color: "#464647",
64
- WebkitTapHighlightColor: "transparent",
65
- userSelect: "none",
66
- WebkitUserSelect: "none",
67
- }}
56
+ className="flex items-center justify-between py-[12px] px-[2px] cursor-pointer text-[13px] text-[#464647] select-none [-webkit-tap-highlight-color:transparent]"
68
57
  >
69
58
  {kind === "rating" ? (
70
59
  <RatingLabel stars={option.stars} label={option.label} starColor={starColor} icons={icons} />
71
60
  ) : option.icon ? (
72
- <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
61
+ <div className="flex items-center gap-[10px]">
73
62
  <TimeOfDayIcon variant={option.icon} size={16} src={icons?.timeOfDayIcons?.[option.icon]} />
74
63
  <span>{option.label}</span>
75
64
  </div>
@@ -72,66 +72,37 @@ const PriceRangeSlider = ({
72
72
  };
73
73
 
74
74
  return (
75
- <div style={{ padding: "8px 4px 4px" }}>
76
- <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 28 }}>
77
- <div
78
- style={{
79
- flex: 1,
80
- border: "1px solid #E5E5E5",
81
- borderRadius: 8,
82
- padding: "6px 10px 8px",
83
- }}
84
- >
85
- <div style={{ fontSize: 11, color: "#9B9B9B", marginBottom: 2 }}>{minLabel}</div>
86
- <div style={{ fontSize: 13, fontWeight: 700, color: "#464647" }}>
75
+ <div className="p-[8px_4px_4px]">
76
+ <div className="flex items-center gap-[8px] mb-[28px]">
77
+ <div className="flex-1 border border-[#E5E5E5] rounded-[8px] p-[6px_10px_8px]">
78
+ <div className="text-[11px] text-[#464647] mb-[2px]">{minLabel}</div>
79
+ <div className="text-[13px] font-bold text-[#464647]">
87
80
  {formatCurrency(currentMin, currencySign)}
88
81
  </div>
89
82
  </div>
90
- <span style={{ color: "#9B9B9B", fontSize: 13 }}>-</span>
91
- <div
92
- style={{
93
- flex: 1,
94
- border: "1px solid #E5E5E5",
95
- borderRadius: 8,
96
- padding: "6px 10px 8px",
97
- }}
98
- >
99
- <div style={{ fontSize: 11, color: "#9B9B9B", marginBottom: 2 }}>{maxLabel}</div>
100
- <div style={{ fontSize: 13, fontWeight: 700, color: "#464647" }}>
83
+ <span className="text-[#9B9B9B] text-[13px]">-</span>
84
+ <div className="flex-1 border border-[#E5E5E5] rounded-[8px] p-[6px_10px_8px]">
85
+ <div className="text-[11px] text-[#464647] mb-[2px]">{maxLabel}</div>
86
+ <div className="text-[13px] font-bold text-[#464647]">
101
87
  {formatCurrency(currentMax, currencySign)}
102
88
  </div>
103
89
  </div>
104
90
  </div>
105
91
 
106
- <div style={{ padding: `0 ${THUMB_RADIUS}px`, overflow: "visible" }}>
92
+ <div className="overflow-visible" style={{ padding: `0 ${THUMB_RADIUS}px` }}>
107
93
  <div
108
94
  ref={trackRef}
109
- style={{
110
- position: "relative",
111
- height: THUMB_SIZE,
112
- display: "flex",
113
- alignItems: "center",
114
- touchAction: "none",
115
- overflow: "visible",
116
- }}
95
+ className="relative flex items-center overflow-visible [touch-action:none]"
96
+ style={{ height: THUMB_SIZE }}
117
97
  >
98
+ <div className="absolute left-0 right-0 h-[3px] rounded-[2px] bg-[#E5E5E5]" />
99
+ {/* Stops right at each thumb's border instead of reaching its center
100
+ (which would run the line underneath half of the circle). */}
118
101
  <div
102
+ className="absolute h-[3px] rounded-[2px]"
119
103
  style={{
120
- position: "absolute",
121
- left: 0,
122
- right: 0,
123
- height: 3,
124
- borderRadius: 2,
125
- backgroundColor: "#E5E5E5",
126
- }}
127
- />
128
- <div
129
- style={{
130
- position: "absolute",
131
- left: `${minPct}%`,
132
- right: `${100 - maxPct}%`,
133
- height: 3,
134
- borderRadius: 2,
104
+ left: `calc(${minPct}% + ${THUMB_RADIUS}px)`,
105
+ right: `calc(${100 - maxPct}% + ${THUMB_RADIUS}px)`,
135
106
  backgroundColor: primaryColor,
136
107
  }}
137
108
  />
@@ -148,32 +119,20 @@ const PriceRangeSlider = ({
148
119
  aria-valuemin={min}
149
120
  aria-valuemax={max}
150
121
  aria-valuenow={thumb === "min" ? currentMin : currentMax}
122
+ className="absolute rounded-full bg-white cursor-grab shadow-[0_1px_4px_rgba(0,0,0,0.25)]"
151
123
  style={{
152
- position: "absolute",
153
124
  left: `${pct}%`,
154
125
  transform: "translateX(-50%)",
155
126
  width: THUMB_SIZE,
156
127
  height: THUMB_SIZE,
157
- borderRadius: "50%",
158
- backgroundColor: "#fff",
159
128
  border: `2px solid ${primaryColor}`,
160
- boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
161
- cursor: "grab",
162
129
  zIndex: thumb === "min" ? 2 : 3,
163
130
  }}
164
131
  />
165
132
  ))}
166
133
  </div>
167
134
  </div>
168
- <div
169
- style={{
170
- display: "flex",
171
- justifyContent: "space-between",
172
- fontSize: 11,
173
- color: "#9B9B9B",
174
- marginTop: 8,
175
- }}
176
- >
135
+ <div className="flex justify-between text-[11px] text-[#464647] mt-[8px]">
177
136
  <span>{formatCurrency(min, currencySign)}</span>
178
137
  <span>{formatCurrency(max, currencySign)}</span>
179
138
  </div>