@thorprovider/blocks 1.0.0 → 2.0.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 (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -1,260 +0,0 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import { ChevronDown, Search, SlidersHorizontal, X } from 'lucide-react'
5
-
6
- import { Badge } from '../../../ui/badge'
7
- import { Button } from '../../../ui/button'
8
- import {
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuSeparator,
13
- DropdownMenuTrigger,
14
- } from '../../../ui/dropdown-menu'
15
- import { Input } from '../../../ui/input'
16
- import { cn } from '../../../lib/utils'
17
-
18
- export interface CategoryFilter1Category {
19
- id: string
20
- name: string
21
- count: number
22
- }
23
-
24
- export interface CategoryFilter1PriceRange {
25
- id: string
26
- label: string
27
- min: number
28
- max: number | null
29
- }
30
-
31
- export interface CategoryFilter1SortOption {
32
- id: string
33
- label: string
34
- }
35
-
36
- export interface CategoryFilter1Filters {
37
- category: string
38
- priceRange: string
39
- sort: string
40
- search: string
41
- }
42
-
43
- export interface CategoryFilter1Props {
44
- title?: string
45
- description?: string
46
- categories: CategoryFilter1Category[]
47
- priceRanges: CategoryFilter1PriceRange[]
48
- sortOptions: CategoryFilter1SortOption[]
49
- onFilterChange?: (filters: CategoryFilter1Filters) => void
50
- }
51
-
52
- export function CategoryFilter1({
53
- title,
54
- description,
55
- categories,
56
- priceRanges,
57
- sortOptions,
58
- onFilterChange,
59
- }: CategoryFilter1Props) {
60
- const [selectedCategory, setSelectedCategory] = useState('all')
61
- const [selectedPriceRange, setSelectedPriceRange] = useState('all')
62
- const [selectedSort, setSelectedSort] = useState('featured')
63
- const [searchQuery, setSearchQuery] = useState('')
64
-
65
- const activeFilters: { type: string; label: string; value: string }[] = []
66
- if (selectedCategory !== 'all') {
67
- const category = categories.find(c => c.id === selectedCategory)
68
- if (category) activeFilters.push({ type: 'category', label: category.name, value: selectedCategory })
69
- }
70
- if (selectedPriceRange !== 'all') {
71
- const priceRange = priceRanges.find(p => p.id === selectedPriceRange)
72
- if (priceRange) activeFilters.push({ type: 'price', label: priceRange.label, value: selectedPriceRange })
73
- }
74
- if (searchQuery) {
75
- activeFilters.push({ type: 'search', label: `"${searchQuery}"`, value: searchQuery })
76
- }
77
-
78
- const selectedCategoryCount = categories.find(c => c.id === selectedCategory)?.count ?? 0
79
-
80
- const clearFilter = (type: string) => {
81
- let nextCategory = selectedCategory
82
- let nextPrice = selectedPriceRange
83
- let nextSearch = searchQuery
84
- if (type === 'category') nextCategory = 'all'
85
- if (type === 'price') nextPrice = 'all'
86
- if (type === 'search') nextSearch = ''
87
- setSelectedCategory(nextCategory)
88
- setSelectedPriceRange(nextPrice)
89
- setSearchQuery(nextSearch)
90
- onFilterChange?.({ category: nextCategory, priceRange: nextPrice, sort: selectedSort, search: nextSearch })
91
- }
92
-
93
- const clearAllFilters = () => {
94
- setSelectedCategory('all')
95
- setSelectedPriceRange('all')
96
- setSearchQuery('')
97
- onFilterChange?.({ category: 'all', priceRange: 'all', sort: selectedSort, search: '' })
98
- }
99
-
100
- const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
101
- const next = e.target.value
102
- setSearchQuery(next)
103
- onFilterChange?.({ category: selectedCategory, priceRange: selectedPriceRange, sort: selectedSort, search: next })
104
- }
105
-
106
- const handleSortSelect = (id: string) => {
107
- setSelectedSort(id)
108
- onFilterChange?.({ category: selectedCategory, priceRange: selectedPriceRange, sort: id, search: searchQuery })
109
- }
110
-
111
- const handleCategorySelect = (id: string) => {
112
- setSelectedCategory(id)
113
- onFilterChange?.({ category: id, priceRange: selectedPriceRange, sort: selectedSort, search: searchQuery })
114
- }
115
-
116
- const handlePriceRangeSelect = (id: string) => {
117
- setSelectedPriceRange(id)
118
- onFilterChange?.({ category: selectedCategory, priceRange: id, sort: selectedSort, search: searchQuery })
119
- }
120
-
121
- return (
122
- <section className="py-8">
123
- <div className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
124
- <div className="mb-8">
125
- {title ? <h2 className="text-3xl font-bold tracking-tight text-balance">{title}</h2> : null}
126
- {description ? <p className="text-muted-foreground mt-2">{description}</p> : null}
127
- {!description && title ? (
128
- <p className="text-muted-foreground mt-2">
129
- Browse our collection of {selectedCategoryCount} products
130
- </p>
131
- ) : null}
132
- </div>
133
-
134
- <div className="mb-6 flex flex-col gap-4">
135
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
136
- <div className="relative max-w-md flex-1">
137
- <Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
138
- <Input
139
- placeholder="Search products..."
140
- value={searchQuery}
141
- onChange={handleSearchChange}
142
- className="h-9 pl-10"
143
- />
144
- </div>
145
-
146
- <DropdownMenu>
147
- <DropdownMenuTrigger asChild>
148
- <Button variant="outline" className="h-9 w-full cursor-pointer px-4 py-2 sm:w-auto">
149
- <SlidersHorizontal data-icon="inline-start" />
150
- Sort: {sortOptions.find(s => s.id === selectedSort)?.label}
151
- <ChevronDown data-icon="inline-end" />
152
- </Button>
153
- </DropdownMenuTrigger>
154
- <DropdownMenuContent align="end" className="w-56">
155
- {sortOptions.map(option => (
156
- <DropdownMenuItem
157
- key={option.id}
158
- onClick={() => handleSortSelect(option.id)}
159
- className={selectedSort === option.id ? 'bg-accent' : ''}
160
- >
161
- {option.label}
162
- </DropdownMenuItem>
163
- ))}
164
- </DropdownMenuContent>
165
- </DropdownMenu>
166
- </div>
167
-
168
- <div className="flex flex-wrap gap-3">
169
- <DropdownMenu>
170
- <DropdownMenuTrigger asChild>
171
- <Button variant="outline" size="sm" className="h-8 cursor-pointer px-3 text-xs">
172
- Category: {categories.find(c => c.id === selectedCategory)?.name}
173
- <ChevronDown data-icon="inline-end" />
174
- </Button>
175
- </DropdownMenuTrigger>
176
- <DropdownMenuContent className="w-56">
177
- {categories.map(category => (
178
- <DropdownMenuItem
179
- key={category.id}
180
- onClick={() => handleCategorySelect(category.id)}
181
- className={cn(selectedCategory === category.id && 'bg-accent')}
182
- >
183
- <div className="flex w-full items-center justify-between">
184
- <span>{category.name}</span>
185
- <Badge variant="secondary" className="px-2.5 py-0.5 text-xs font-semibold">
186
- {category.count}
187
- </Badge>
188
- </div>
189
- </DropdownMenuItem>
190
- ))}
191
- </DropdownMenuContent>
192
- </DropdownMenu>
193
-
194
- <DropdownMenu>
195
- <DropdownMenuTrigger asChild>
196
- <Button variant="outline" size="sm" className="h-8 cursor-pointer px-3 text-xs">
197
- Price: {priceRanges.find(p => p.id === selectedPriceRange)?.label}
198
- <ChevronDown data-icon="inline-end" />
199
- </Button>
200
- </DropdownMenuTrigger>
201
- <DropdownMenuContent className="w-48">
202
- {priceRanges.map(range => (
203
- <DropdownMenuItem
204
- key={range.id}
205
- onClick={() => handlePriceRangeSelect(range.id)}
206
- className={cn(selectedPriceRange === range.id && 'bg-accent')}
207
- >
208
- {range.label}
209
- </DropdownMenuItem>
210
- ))}
211
- </DropdownMenuContent>
212
- </DropdownMenu>
213
- </div>
214
-
215
- {activeFilters.length > 0 ? (
216
- <div className="flex flex-wrap items-center gap-2">
217
- <span className="text-muted-foreground text-sm font-medium">Active filters:</span>
218
- {activeFilters.map(filter => (
219
- <Badge key={filter.type} variant="secondary" className="px-2.5 py-0.5 font-semibold">
220
- {filter.label}
221
- <Button
222
- variant="ghost"
223
- size="sm"
224
- className="!p-1 h-auto cursor-pointer text-inherit"
225
- onClick={() => clearFilter(filter.type)}
226
- >
227
- <X className="size-3" />
228
- </Button>
229
- </Badge>
230
- ))}
231
- <DropdownMenuSeparator className="mx-2" />
232
- <Button
233
- variant="ghost"
234
- size="sm"
235
- onClick={clearAllFilters}
236
- className="h-auto cursor-pointer p-1.5 text-xs text-muted-foreground"
237
- >
238
- Clear all
239
- </Button>
240
- </div>
241
- ) : null}
242
- </div>
243
-
244
- <div className="bg-muted/50 rounded-lg border p-4">
245
- <div className="flex items-center justify-between">
246
- <div className="flex items-center gap-4">
247
- <span className="text-sm font-medium">Showing {selectedCategoryCount} results</span>
248
- {searchQuery && <span className="text-muted-foreground text-sm">for &quot;{searchQuery}&quot;</span>}
249
- </div>
250
- <div className="text-muted-foreground text-xs">
251
- Sorted by {sortOptions.find(s => s.id === selectedSort)?.label}
252
- </div>
253
- </div>
254
- </div>
255
- </div>
256
- </section>
257
- )
258
- }
259
-
260
- export default CategoryFilter1
@@ -1,222 +0,0 @@
1
- 'use client'
2
-
3
- import { useId } from 'react'
4
-
5
- import { Button } from '../../../ui/button'
6
- import { CardTitle } from '../../../ui/card'
7
- import { Input } from '../../../ui/input'
8
- import { CheckboxCheck } from '../checkout-forms/checkbox-check'
9
-
10
- import type {
11
- CategoryFilter2Category,
12
- CategoryFilter2Collection,
13
- CategoryFilter2Color,
14
- CategoryFilter2PriceRange,
15
- } from './category-filter-2'
16
-
17
- interface CategoryFilter2FilterPanelProps {
18
- selectedText?: string
19
- clearAllLabel?: string
20
- priceRangeTitle?: string
21
- priceRange: number[]
22
- /**
23
- * Real backend-provided range only. When absent the price section is not
24
- * rendered — the block never fabricates a `0..1000` range (E2 design
25
- * Decision 8 / spec "Price controls require real price metadata").
26
- */
27
- priceRangeConfig?: CategoryFilter2PriceRange
28
- minLabel?: string
29
- maxLabel?: string
30
- onPriceChange: (range: number[]) => void
31
- categoriesTitle?: string
32
- categories: CategoryFilter2Category[]
33
- selectedCategories: string[]
34
- onCategoryChange: (category: string, checked: boolean) => void
35
- collectionsTitle?: string
36
- collections: CategoryFilter2Collection[]
37
- selectedCollections: string[]
38
- onCollectionChange: (collection: string, checked: boolean) => void
39
- sizesTitle?: string
40
- sizes: string[]
41
- selectedSize: string | null
42
- onSizeChange: (size: string | null) => void
43
- colorsTitle?: string
44
- colors: CategoryFilter2Color[]
45
- selectedColor: string | null
46
- onColorChange: (color: string | null) => void
47
- onClearAll: () => void
48
- }
49
-
50
- export function CategoryFilter2FilterPanel({
51
- selectedText = 'Filtros',
52
- clearAllLabel = 'Restablecer',
53
- priceRangeTitle = 'Rango de precio',
54
- priceRange,
55
- priceRangeConfig,
56
- minLabel = 'Mínimo',
57
- maxLabel = 'Máximo',
58
- onPriceChange,
59
- categoriesTitle = 'Categoría',
60
- categories,
61
- selectedCategories,
62
- onCategoryChange,
63
- collectionsTitle = 'Colección',
64
- collections,
65
- selectedCollections,
66
- onCollectionChange,
67
- sizesTitle = 'Tamaño',
68
- sizes,
69
- selectedSize,
70
- onSizeChange,
71
- colorsTitle = 'Color',
72
- colors,
73
- selectedColor,
74
- onColorChange,
75
- onClearAll,
76
- }: CategoryFilter2FilterPanelProps) {
77
- const priceInputId = useId()
78
- const categoryInputId = useId()
79
- const collectionInputId = useId()
80
-
81
- return (
82
- <>
83
- <div className="flex flex-col gap-6">
84
- <div className="mb-3 flex items-center justify-between">
85
- <h1 className="font-semibold">{selectedText}</h1>
86
- <Button variant="ghost" size="sm" className="h-8 cursor-pointer px-2 text-xs" onClick={onClearAll}>
87
- {clearAllLabel}
88
- </Button>
89
- </div>
90
- </div>
91
-
92
- {priceRangeConfig ? (
93
- <div className="mt-4 flex flex-col gap-4">
94
- <CardTitle className="font-semibold">{priceRangeTitle}</CardTitle>
95
- <div className="flex items-center gap-3">
96
- <div className="flex flex-1 flex-col gap-1">
97
- <label htmlFor={`${priceInputId}-min`}>{minLabel}</label>
98
- <Input
99
- id={`${priceInputId}-min`}
100
- type="number"
101
- value={priceRange[0] ?? priceRangeConfig.min}
102
- onChange={e => {
103
- const value = parseInt(e.target.value)
104
- if (!isNaN(value) && value >= priceRangeConfig.min) {
105
- onPriceChange([value, priceRange[1] ?? priceRangeConfig.max])
106
- }
107
- }}
108
- min={priceRangeConfig.min}
109
- max={priceRange[1] ?? priceRangeConfig.max}
110
- step={priceRangeConfig.step}
111
- className="h-9"
112
- />
113
- </div>
114
- <div className="mt-5 flex-none">-</div>
115
- <div className="flex flex-1 flex-col gap-1">
116
- <label htmlFor={`${priceInputId}-max`}>{maxLabel}</label>
117
- <Input
118
- id={`${priceInputId}-max`}
119
- type="number"
120
- value={priceRange[1] ?? priceRangeConfig.max}
121
- onChange={e => {
122
- const value = parseInt(e.target.value)
123
- if (!isNaN(value) && value <= priceRangeConfig.max) {
124
- onPriceChange([priceRange[0] ?? priceRangeConfig.min, value])
125
- }
126
- }}
127
- min={priceRange[0] ?? priceRangeConfig.min}
128
- max={priceRangeConfig.max}
129
- step={priceRangeConfig.step}
130
- className="h-9"
131
- />
132
- </div>
133
- </div>
134
- </div>
135
- ) : null}
136
-
137
- {categories.length > 0 ? (
138
- <div className="mt-4 flex flex-col gap-4">
139
- <CardTitle className="font-semibold">{categoriesTitle}</CardTitle>
140
- <div className="flex flex-col gap-2">
141
- {categories.map(category => (
142
- <div key={category.id} className="flex items-center gap-2">
143
- <CheckboxCheck
144
- id={`${categoryInputId}-${category.id}`}
145
- checked={selectedCategories.includes(category.id)}
146
- onCheckedChange={checked => onCategoryChange(category.id, checked as boolean)}
147
- />
148
- <label htmlFor={`${categoryInputId}-${category.id}`} className="text-sm">
149
- {category.label}
150
- </label>
151
- </div>
152
- ))}
153
- </div>
154
- </div>
155
- ) : null}
156
-
157
- {collections.length > 0 ? (
158
- <div className="mt-4 flex flex-col gap-4">
159
- <CardTitle className="font-semibold">{collectionsTitle}</CardTitle>
160
- <div className="flex flex-col gap-2">
161
- {collections.map(collection => (
162
- <div key={collection.id} className="flex items-center gap-2">
163
- <CheckboxCheck
164
- id={`${collectionInputId}-${collection.id}`}
165
- checked={selectedCollections.includes(collection.id)}
166
- onCheckedChange={checked => onCollectionChange(collection.id, checked as boolean)}
167
- />
168
- <label htmlFor={`${collectionInputId}-${collection.id}`} className="text-sm">
169
- {collection.label}
170
- </label>
171
- </div>
172
- ))}
173
- </div>
174
- </div>
175
- ) : null}
176
-
177
- {sizes.length > 0 ? (
178
- <div className="mt-4 flex flex-col gap-4">
179
- <CardTitle className="font-semibold">{sizesTitle}</CardTitle>
180
- <div className="flex flex-wrap gap-2">
181
- {sizes.map(size => (
182
- <Button
183
- key={size}
184
- variant={selectedSize === size ? 'default' : 'outline'}
185
- size="sm"
186
- className="h-8 cursor-pointer px-3 text-xs"
187
- onClick={() => onSizeChange(size === selectedSize ? null : size)}
188
- aria-pressed={selectedSize === size}
189
- >
190
- {size}
191
- </Button>
192
- ))}
193
- </div>
194
- </div>
195
- ) : null}
196
-
197
- {colors.length > 0 ? (
198
- <div className="mt-4 flex flex-col gap-4">
199
- <CardTitle className="font-semibold">{colorsTitle}</CardTitle>
200
- <div className="flex flex-wrap gap-2">
201
- {colors.map(color => (
202
- <Button
203
- key={color.id}
204
- onClick={() => onColorChange(color.id === selectedColor ? null : color.id)}
205
- className={`relative size-8 cursor-pointer rounded-sm transition-transform duration-200 ${
206
- selectedColor === color.id ? 'ring-primary scale-105 ring-2' : 'hover:ring-foreground/20 hover:ring-1'
207
- } ${color.id === 'white' ? 'border-border border' : ''}`}
208
- style={{ backgroundColor: color.color }}
209
- aria-pressed={selectedColor === color.id}
210
- aria-label={color.id}
211
- >
212
- {selectedColor === color.id && <div className="border-background absolute inset-0 rounded-sm border-2" />}
213
- </Button>
214
- ))}
215
- </div>
216
- </div>
217
- ) : null}
218
- </>
219
- )
220
- }
221
-
222
- export default CategoryFilter2FilterPanel