@snowcone-app/ui 0.1.42 → 0.2.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 (192) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +18 -4
  3. package/package.json +9 -5
  4. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  5. package/src/components/LoadingOverlayPrism.tsx +251 -0
  6. package/src/composed/AddToCart.tsx +229 -0
  7. package/src/composed/ArtAlignment.tsx +703 -0
  8. package/src/composed/ArtSelector.tsx +290 -0
  9. package/src/composed/ArtworkCustomizer.tsx +212 -0
  10. package/src/composed/CanvasEditor.tsx +79 -0
  11. package/src/composed/ColorPicker.tsx +111 -0
  12. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  13. package/src/composed/HeroProductImage.tsx +1071 -0
  14. package/src/composed/Lightbox.index.ts +2 -0
  15. package/src/composed/Lightbox.tsx +230 -0
  16. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  17. package/src/composed/PlacementTabs.tsx +179 -0
  18. package/src/composed/ProductCard.tsx +298 -0
  19. package/src/composed/ProductGallery.tsx +54 -0
  20. package/src/composed/ProductImage.tsx +129 -0
  21. package/src/composed/ProductList.tsx +147 -0
  22. package/src/composed/ProductOptions.tsx +305 -0
  23. package/src/composed/RealtimeMockup.tsx +121 -0
  24. package/src/composed/TileCount.tsx +348 -0
  25. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  26. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  27. package/src/composed/carousels/index.ts +11 -0
  28. package/src/composed/carousels/types.ts +58 -0
  29. package/src/composed/grids/MasonryGrid.tsx +238 -0
  30. package/src/composed/grids/index.ts +9 -0
  31. package/src/composed/search/CurrentRefinements.tsx +80 -0
  32. package/src/composed/search/Filters.tsx +49 -0
  33. package/src/composed/search/FiltersButton.tsx +57 -0
  34. package/src/composed/search/FiltersDrawer.tsx +375 -0
  35. package/src/composed/search/ProductGrid.tsx +118 -0
  36. package/src/composed/search/ProductHit.tsx +56 -0
  37. package/src/composed/search/SearchBox.tsx +109 -0
  38. package/src/composed/search/SearchProvider.tsx +136 -0
  39. package/src/composed/search/facetConfig.ts +16 -0
  40. package/src/composed/search/index.ts +22 -0
  41. package/src/composed/search/meilisearchAdapter.ts +20 -0
  42. package/src/composed/search/types.ts +22 -0
  43. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  44. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  45. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  46. package/src/composed/zoom/index.ts +12 -0
  47. package/src/composed/zoom/types.ts +12 -0
  48. package/src/design-system/ColorPalette.tsx +126 -0
  49. package/src/design-system/ColorSwatch.tsx +49 -0
  50. package/src/design-system/DesignSystemPage.tsx +130 -0
  51. package/src/design-system/ThemeSwitcher.tsx +181 -0
  52. package/src/design-system/TypographyScale.tsx +106 -0
  53. package/src/design-system/index.ts +5 -0
  54. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  55. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  56. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  57. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  58. package/src/hooks/useBrand.ts +41 -0
  59. package/src/hooks/useCanvasContext.ts +127 -0
  60. package/src/hooks/useDeviceDetection.ts +64 -0
  61. package/src/hooks/useFocusTrap.ts +70 -0
  62. package/src/hooks/useImagePreloader.ts +268 -0
  63. package/src/hooks/useImageTransition.ts +608 -0
  64. package/src/hooks/usePlacementsProcessor.ts +74 -0
  65. package/src/hooks/useProductGallery.ts +193 -0
  66. package/src/hooks/useProductPage.ts +467 -0
  67. package/src/hooks/useRenderGuard.ts +96 -0
  68. package/src/hooks/useScrollDirection.ts +196 -0
  69. package/src/hooks/viewport/index.ts +25 -0
  70. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  71. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  72. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  73. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  74. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  75. package/src/hooks/visibility/index.ts +15 -0
  76. package/src/hooks/visibility/observerPool.ts +150 -0
  77. package/src/index.ts +240 -0
  78. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  79. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  80. package/src/layouts/hero-zoom/index.ts +30 -0
  81. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  82. package/src/layouts/hero-zoom/types.ts +113 -0
  83. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  84. package/src/layouts/index.ts +9 -0
  85. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  86. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  87. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  88. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  89. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  90. package/src/layouts/pdp/index.ts +40 -0
  91. package/src/lib/env.ts +15 -0
  92. package/src/lib/locale.ts +167 -0
  93. package/src/lib/router.tsx +46 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/lightbox/README.md +77 -0
  96. package/src/next/index.tsx +26 -0
  97. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  98. package/src/patterns/Product.tsx +850 -0
  99. package/src/patterns/ProductPageProvider.tsx +224 -0
  100. package/src/patterns/RealtimeProvider.tsx +1162 -0
  101. package/src/patterns/ShopProvider.tsx +603 -0
  102. package/src/personalization/PersonalizationBridge.tsx +235 -0
  103. package/src/personalization/PersonalizationContext.ts +29 -0
  104. package/src/personalization/PersonalizationInputs.tsx +110 -0
  105. package/src/personalization/PersonalizationProvider.tsx +407 -0
  106. package/src/personalization/canvas-stub.d.ts +22 -0
  107. package/src/personalization/index.ts +43 -0
  108. package/src/personalization/types.ts +48 -0
  109. package/src/personalization/usePersonalization.ts +32 -0
  110. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  111. package/src/personalization/utils.ts +59 -0
  112. package/src/primitives/BrandLogo.tsx +65 -0
  113. package/src/primitives/BrandName.tsx +51 -0
  114. package/src/primitives/Button.tsx +123 -0
  115. package/src/primitives/ColorSwatch.tsx +221 -0
  116. package/src/primitives/DragHintAnimation.tsx +190 -0
  117. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  118. package/src/primitives/FloatingActionGroup.tsx +176 -0
  119. package/src/primitives/ProductPrice.tsx +171 -0
  120. package/src/primitives/ProgressiveBlur.tsx +295 -0
  121. package/src/primitives/ThemeToggle.tsx +125 -0
  122. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  123. package/src/primitives/accordion.tsx +280 -0
  124. package/src/primitives/badge.tsx +137 -0
  125. package/src/primitives/card.tsx +61 -0
  126. package/src/primitives/checkbox.tsx +56 -0
  127. package/src/primitives/collapsible.tsx +51 -0
  128. package/src/primitives/drawer.tsx +828 -0
  129. package/src/primitives/dropdown-menu.tsx +197 -0
  130. package/src/primitives/fieldset.tsx +73 -0
  131. package/src/primitives/index.ts +138 -0
  132. package/src/primitives/input.tsx +91 -0
  133. package/src/primitives/kbd.tsx +130 -0
  134. package/src/primitives/label.tsx +20 -0
  135. package/src/primitives/link.tsx +182 -0
  136. package/src/primitives/popover.tsx +80 -0
  137. package/src/primitives/radio-group.tsx +79 -0
  138. package/src/primitives/scroll-fade.tsx +159 -0
  139. package/src/primitives/select.tsx +170 -0
  140. package/src/primitives/separator.tsx +25 -0
  141. package/src/primitives/slider.tsx +221 -0
  142. package/src/primitives/spinner.tsx +72 -0
  143. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  144. package/src/primitives/stories/Badge.stories.tsx +221 -0
  145. package/src/primitives/stories/Button.stories.tsx +185 -0
  146. package/src/primitives/stories/Card.stories.tsx +171 -0
  147. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  148. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  149. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  150. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  151. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  152. package/src/primitives/stories/Input.stories.tsx +172 -0
  153. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  154. package/src/primitives/stories/Label.stories.tsx +98 -0
  155. package/src/primitives/stories/Link.stories.tsx +260 -0
  156. package/src/primitives/stories/Popover.stories.tsx +178 -0
  157. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  158. package/src/primitives/stories/Select.stories.tsx +222 -0
  159. package/src/primitives/stories/Separator.stories.tsx +134 -0
  160. package/src/primitives/stories/Slider.stories.tsx +203 -0
  161. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  162. package/src/primitives/stories/Surface.stories.tsx +257 -0
  163. package/src/primitives/stories/Switch.stories.tsx +131 -0
  164. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  165. package/src/primitives/stories/TextField.stories.tsx +139 -0
  166. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  167. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  168. package/src/primitives/surface.tsx +86 -0
  169. package/src/primitives/switch.tsx +35 -0
  170. package/src/primitives/tabs.tsx +206 -0
  171. package/src/primitives/text-field.tsx +84 -0
  172. package/src/primitives/textarea.tsx +50 -0
  173. package/src/primitives/tooltip.tsx +58 -0
  174. package/src/services/CanvasExportService.ts +518 -0
  175. package/src/styles/base.css +380 -0
  176. package/src/styles/defaults.css +280 -0
  177. package/src/styles/globals.css +1242 -0
  178. package/src/styles/index.css +17 -0
  179. package/src/styles/ne-themes.css +4740 -0
  180. package/src/styles/tailwind.css +11 -0
  181. package/src/styles/tokens.css +117 -0
  182. package/src/styles/utilities.css +188 -0
  183. package/src/themes/apply-theme.ts +449 -0
  184. package/src/themes/getThemeStyles.ts +454 -0
  185. package/src/themes/index.ts +48 -0
  186. package/src/themes/oklch-theme.ts +283 -0
  187. package/src/themes/presets.ts +989 -0
  188. package/src/themes/types.ts +386 -0
  189. package/src/themes/useTheme.tsx +450 -0
  190. package/src/utils/dev-warnings.ts +161 -0
  191. package/src/utils/devWarnings.ts +153 -0
  192. package/dist/styles.css +0 -1
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Carousel Components
3
+ *
4
+ * Production-grade carousels for product image display.
5
+ *
6
+ * @module carousels
7
+ */
8
+
9
+ export * from "./types";
10
+ export * from "./MobileProductCarousel";
11
+ export * from "./HeroCarousel";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Carousel Types
3
+ *
4
+ * Shared interfaces for carousel components.
5
+ */
6
+
7
+ /**
8
+ * Represents a single image in a carousel.
9
+ *
10
+ * This type supports both static images and dynamic mockups.
11
+ * For mockups, the `src` may be updated in real-time via the RealtimeProvider.
12
+ */
13
+ export interface CarouselImage {
14
+ /** The image source URL */
15
+ src: string;
16
+
17
+ /** Natural width of the image in pixels */
18
+ naturalWidth: number;
19
+
20
+ /** Natural height of the image in pixels */
21
+ naturalHeight: number;
22
+
23
+ /**
24
+ * Anchor point for image positioning (0-1 range, vertical position).
25
+ * Used to determine which part of the image to show when cropped.
26
+ */
27
+ imageAnchor: number;
28
+
29
+ /** Accessible label for the image */
30
+ label: string;
31
+
32
+ /**
33
+ * Whether this is a real-time mockup that can be updated.
34
+ * When true, the carousel will integrate with RealtimeProvider
35
+ * to receive URL updates.
36
+ */
37
+ isRealMockup?: boolean;
38
+
39
+ /**
40
+ * Whether this is a placeholder waiting for content.
41
+ * Displays a loading skeleton when true.
42
+ */
43
+ isPlaceholder?: boolean;
44
+
45
+ /** Placement identifier for mockup generation */
46
+ placement?: string;
47
+
48
+ /** Mockup identifier for real-time updates */
49
+ mockupId?: string;
50
+
51
+ /** Aspect ratio of the mockup (e.g., 1.78 for 16:9) */
52
+ aspectRatio?: number;
53
+ }
54
+
55
+ /**
56
+ * Alias for CarouselImage - used in hero contexts for semantic clarity.
57
+ */
58
+ export type HeroImage = CarouselImage;
@@ -0,0 +1,238 @@
1
+ "use client";
2
+
3
+ /**
4
+ * MasonryGrid - Responsive masonry layout primitive
5
+ *
6
+ * A pure layout component for creating masonry-style grids.
7
+ * No business logic - just handles responsive column calculation
8
+ * and distributing items across columns.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * // Basic usage with fixed columns
13
+ * <MasonryGrid
14
+ * items={artworks}
15
+ * columns={3}
16
+ * gap={8}
17
+ * renderItem={(artwork, index) => (
18
+ * <ArtworkCard key={artwork.id} artwork={artwork} />
19
+ * )}
20
+ * />
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * // Responsive columns
26
+ * <MasonryGrid
27
+ * items={products}
28
+ * columns={{ sm: 2, md: 3, lg: 4, xl: 5 }}
29
+ * gap={16}
30
+ * renderItem={(product, index) => (
31
+ * <ProductCard key={product.id} product={product} />
32
+ * )}
33
+ * />
34
+ * ```
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * // With custom breakpoints
39
+ * <MasonryGrid
40
+ * items={images}
41
+ * columns={{ base: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7 }}
42
+ * renderItem={(image, index) => (
43
+ * <img key={index} src={image.src} alt={image.alt} />
44
+ * )}
45
+ * />
46
+ * ```
47
+ */
48
+
49
+ import React, { useState, useEffect, useMemo } from "react";
50
+
51
+ /**
52
+ * Responsive column configuration.
53
+ * Keys are Tailwind-style breakpoint names, values are column counts.
54
+ */
55
+ export interface ResponsiveColumns {
56
+ /** Default/base columns (< 640px) */
57
+ base?: number;
58
+ /** sm: >= 640px */
59
+ sm?: number;
60
+ /** md: >= 768px */
61
+ md?: number;
62
+ /** lg: >= 1024px */
63
+ lg?: number;
64
+ /** xl: >= 1280px */
65
+ xl?: number;
66
+ /** 2xl: >= 1536px */
67
+ "2xl"?: number;
68
+ }
69
+
70
+ export interface MasonryGridProps<T> {
71
+ /** Array of items to display */
72
+ items: T[];
73
+
74
+ /**
75
+ * Render function for each item.
76
+ * @param item The item to render
77
+ * @param index The index of the item in the original array
78
+ */
79
+ renderItem: (item: T, index: number) => React.ReactNode;
80
+
81
+ /**
82
+ * Number of columns or responsive column configuration.
83
+ * @default { base: 2, sm: 3, md: 4, lg: 5, xl: 6 }
84
+ */
85
+ columns?: number | ResponsiveColumns;
86
+
87
+ /**
88
+ * Gap between items in pixels.
89
+ * @default 8
90
+ */
91
+ gap?: number;
92
+
93
+ /** Additional CSS classes for the container */
94
+ className?: string;
95
+
96
+ /** Empty state content */
97
+ emptyContent?: React.ReactNode;
98
+ }
99
+
100
+ // Tailwind-style breakpoints in pixels
101
+ const BREAKPOINTS = {
102
+ sm: 640,
103
+ md: 768,
104
+ lg: 1024,
105
+ xl: 1280,
106
+ "2xl": 1536,
107
+ };
108
+
109
+ /**
110
+ * Default responsive column configuration
111
+ */
112
+ const DEFAULT_COLUMNS: ResponsiveColumns = {
113
+ base: 2,
114
+ sm: 3,
115
+ md: 4,
116
+ lg: 5,
117
+ xl: 6,
118
+ };
119
+
120
+ /**
121
+ * Get the column count for a given viewport width
122
+ */
123
+ function getColumnCount(
124
+ width: number,
125
+ columns: number | ResponsiveColumns
126
+ ): number {
127
+ if (typeof columns === "number") {
128
+ return columns;
129
+ }
130
+
131
+ // Find the appropriate column count based on viewport width
132
+ // Check from largest to smallest breakpoint
133
+ if (width >= BREAKPOINTS["2xl"] && columns["2xl"] !== undefined) {
134
+ return columns["2xl"];
135
+ }
136
+ if (width >= BREAKPOINTS.xl && columns.xl !== undefined) {
137
+ return columns.xl;
138
+ }
139
+ if (width >= BREAKPOINTS.lg && columns.lg !== undefined) {
140
+ return columns.lg;
141
+ }
142
+ if (width >= BREAKPOINTS.md && columns.md !== undefined) {
143
+ return columns.md;
144
+ }
145
+ if (width >= BREAKPOINTS.sm && columns.sm !== undefined) {
146
+ return columns.sm;
147
+ }
148
+
149
+ return columns.base ?? 2;
150
+ }
151
+
152
+ /**
153
+ * Distribute items into columns for masonry layout.
154
+ * Items are distributed round-robin style to maintain visual order.
155
+ */
156
+ function distributeItems<T>(items: T[], columnCount: number): T[][] {
157
+ const columns: T[][] = Array.from({ length: columnCount }, () => []);
158
+
159
+ items.forEach((item, index) => {
160
+ const columnIndex = index % columnCount;
161
+ columns[columnIndex].push(item);
162
+ });
163
+
164
+ return columns;
165
+ }
166
+
167
+ export function MasonryGrid<T>({
168
+ items,
169
+ renderItem,
170
+ columns = DEFAULT_COLUMNS,
171
+ gap = 8,
172
+ className = "",
173
+ emptyContent,
174
+ }: MasonryGridProps<T>) {
175
+ const [columnCount, setColumnCount] = useState(() =>
176
+ typeof columns === "number"
177
+ ? columns
178
+ : getColumnCount(
179
+ typeof window !== "undefined" ? window.innerWidth : 1024,
180
+ columns
181
+ )
182
+ );
183
+
184
+ // Update column count on resize
185
+ useEffect(() => {
186
+ const updateColumnCount = () => {
187
+ setColumnCount(getColumnCount(window.innerWidth, columns));
188
+ };
189
+
190
+ // Initial update
191
+ updateColumnCount();
192
+
193
+ window.addEventListener("resize", updateColumnCount);
194
+ return () => window.removeEventListener("resize", updateColumnCount);
195
+ }, [columns]);
196
+
197
+ // Distribute items into columns
198
+ const distributedColumns = useMemo(
199
+ () => distributeItems(items, columnCount),
200
+ [items, columnCount]
201
+ );
202
+
203
+ // Handle empty state
204
+ if (items.length === 0) {
205
+ if (emptyContent) {
206
+ return <>{emptyContent}</>;
207
+ }
208
+ return null;
209
+ }
210
+
211
+ return (
212
+ <div
213
+ className={`flex ${className}`}
214
+ style={{ gap: `${gap}px` }}
215
+ role="list"
216
+ >
217
+ {distributedColumns.map((columnItems, columnIndex) => (
218
+ <div
219
+ key={columnIndex}
220
+ className="flex-1 flex flex-col"
221
+ style={{ gap: `${gap}px` }}
222
+ >
223
+ {columnItems.map((item, itemIndex) => {
224
+ // Calculate original index for the render function
225
+ const originalIndex = itemIndex * columnCount + columnIndex;
226
+ return (
227
+ <div key={originalIndex} role="listitem">
228
+ {renderItem(item, originalIndex)}
229
+ </div>
230
+ );
231
+ })}
232
+ </div>
233
+ ))}
234
+ </div>
235
+ );
236
+ }
237
+
238
+ export default MasonryGrid;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Grid Components
3
+ *
4
+ * Layout primitives for grid-based displays.
5
+ *
6
+ * @module grids
7
+ */
8
+
9
+ export * from "./MasonryGrid";
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useCurrentRefinements } from "react-instantsearch";
4
+ import { X as LucideX } from "lucide-react";
5
+ import type { ComponentType } from "react";
6
+
7
+ // Cast to fix React 19 type compatibility with lucide-react
8
+ type IconProps = { className?: string; size?: number };
9
+ const XIcon = LucideX as ComponentType<IconProps>;
10
+
11
+ export function CurrentRefinements() {
12
+ const { items, refine } = useCurrentRefinements();
13
+
14
+ const formatPriceValue = (priceInCents: number): string => {
15
+ const priceInDollars = priceInCents / 100;
16
+ if (priceInDollars >= 100) {
17
+ return `$${(priceInDollars / 1000).toFixed(1)}k`;
18
+ } else if (priceInDollars >= 10) {
19
+ return `$${priceInDollars.toFixed(0)}`;
20
+ } else {
21
+ return `$${priceInDollars.toFixed(2)}`;
22
+ }
23
+ };
24
+
25
+ const formatRefinementLabel = (attribute: string, label: string) => {
26
+ if (attribute === "price") {
27
+ // Handle price range format like "100 <= price <= 5000"
28
+ const rangeMatch = label.match(/(\d+)\s*<=\s*price\s*<=\s*(\d+)/);
29
+ if (rangeMatch) {
30
+ const min = parseInt(rangeMatch[1]);
31
+ const max = parseInt(rangeMatch[2]);
32
+ return `${formatPriceValue(min)} - ${formatPriceValue(max)}`;
33
+ }
34
+
35
+ // Handle >= format like "price >= 3434" or "≥ 3434"
36
+ const minMatch = label.match(/[≥>=]+\s*(\d+)/);
37
+ if (minMatch) {
38
+ const min = parseInt(minMatch[1]);
39
+ return `Min ${formatPriceValue(min)}`;
40
+ }
41
+
42
+ // Handle <= format like "price <= 1000" or "≤ 1000"
43
+ const maxMatch = label.match(/[≤<=]+\s*(\d+)/);
44
+ if (maxMatch) {
45
+ const max = parseInt(maxMatch[1]);
46
+ return `Max ${formatPriceValue(max)}`;
47
+ }
48
+ }
49
+ return label;
50
+ };
51
+
52
+ const allRefinements = items.flatMap((item) =>
53
+ item.refinements.map((refinement) => ({
54
+ ...refinement,
55
+ refine: () => refine(refinement),
56
+ attribute: item.attribute,
57
+ displayLabel: formatRefinementLabel(item.attribute, refinement.label),
58
+ }))
59
+ );
60
+
61
+ if (allRefinements.length === 0) {
62
+ return null;
63
+ }
64
+
65
+ return (
66
+ <div className="flex flex-wrap items-center gap-2 mb-4 -mt-1" role="group" aria-label="Active filters">
67
+ {allRefinements.map((refinement, index) => (
68
+ <button
69
+ key={`${refinement.attribute}-${refinement.value}-${index}`}
70
+ className="bg-primary text-on-primary hover:bg-primary/90 inline-flex h-7 items-center gap-1.5 rounded-full px-3 text-sm transition-colors"
71
+ onClick={() => refinement.refine()}
72
+ aria-label={`Remove filter: ${refinement.displayLabel}`}
73
+ >
74
+ <span className="truncate">{refinement.displayLabel}</span>
75
+ <XIcon className="w-3 h-3" />
76
+ </button>
77
+ ))}
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { FiltersButton } from "./FiltersButton";
5
+ import { FiltersDrawer } from "./FiltersDrawer";
6
+
7
+ export interface FiltersProps {
8
+ /**
9
+ * Optional className for the trigger button
10
+ */
11
+ buttonClassName?: string;
12
+
13
+ /**
14
+ * Optional button text
15
+ */
16
+ buttonText?: React.ReactNode;
17
+ }
18
+
19
+ /**
20
+ * Filters - Self-contained filters with button and drawer
21
+ *
22
+ * Manages its own open/close state internally so you don't need to set up useState.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <Filters />
27
+ * ```
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <Filters buttonText="Show Filters" buttonClassName="custom-class" />
32
+ * ```
33
+ */
34
+ export function Filters({ buttonClassName, buttonText }: FiltersProps) {
35
+ const [isOpen, setIsOpen] = useState(false);
36
+
37
+ return (
38
+ <>
39
+ <FiltersButton
40
+ onClick={() => setIsOpen(true)}
41
+ className={buttonClassName}
42
+ >
43
+ {buttonText}
44
+ </FiltersButton>
45
+
46
+ <FiltersDrawer isOpen={isOpen} onClose={() => setIsOpen(false)} />
47
+ </>
48
+ );
49
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { SlidersHorizontal as LucideSlidersHorizontal } from "lucide-react";
4
+ import type { ComponentType } from "react";
5
+ import { Button } from "../../primitives/Button";
6
+
7
+ // Cast to fix React 19 type compatibility with lucide-react
8
+ type IconProps = { className?: string; size?: number };
9
+ const SlidersHorizontalIcon = LucideSlidersHorizontal as ComponentType<IconProps>;
10
+
11
+ export interface FiltersButtonProps {
12
+ /**
13
+ * Click handler to open filters drawer
14
+ */
15
+ onClick: () => void;
16
+
17
+ /**
18
+ * Additional CSS classes
19
+ */
20
+ className?: string;
21
+
22
+ /**
23
+ * Button text
24
+ */
25
+ children?: React.ReactNode;
26
+ }
27
+
28
+ /**
29
+ * FiltersButton - Button to open the filters drawer
30
+ *
31
+ * Uses Button field variant to match Input styling.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const [isOpen, setIsOpen] = useState(false);
36
+ *
37
+ * <FiltersButton onClick={() => setIsOpen(true)} />
38
+ * <FiltersDrawer isOpen={isOpen} onClose={() => setIsOpen(false)} />
39
+ * ```
40
+ */
41
+ export function FiltersButton({
42
+ onClick,
43
+ className = "",
44
+ children = "Filters",
45
+ }: FiltersButtonProps) {
46
+ return (
47
+ <Button
48
+ variant="field"
49
+ onClick={onClick}
50
+ className={className}
51
+ aria-label="Open filters"
52
+ >
53
+ <SlidersHorizontalIcon className="w-4 h-4" />
54
+ <span>{children}</span>
55
+ </Button>
56
+ );
57
+ }