@snowcone-app/ui 0.1.43 → 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.
- package/CHANGELOG.md +26 -0
- package/README.md +18 -4
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1071 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- package/dist/styles.css +0 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgressiveBlur - Layered backdrop-filter blur effect
|
|
5
|
+
*
|
|
6
|
+
* Creates a smooth progressive blur using multiple stacked layers with
|
|
7
|
+
* increasing blur values. Perfect for sticky headers, footers, or any
|
|
8
|
+
* UI element where content should gracefully blur as it scrolls underneath.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // In a sticky header - blur content scrolling up from below
|
|
12
|
+
* <header className="sticky top-0">
|
|
13
|
+
* <nav>...</nav>
|
|
14
|
+
* <ProgressiveBlur position="bottom" height="40px" />
|
|
15
|
+
* </header>
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // At bottom of viewport - blur content scrolling down
|
|
19
|
+
* <ProgressiveBlur position="top" height="60px" />
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import React from "react";
|
|
23
|
+
|
|
24
|
+
function cn(...classes: (string | undefined | null | false)[]) {
|
|
25
|
+
return classes.filter(Boolean).join(" ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProgressiveBlurProps {
|
|
29
|
+
/** Additional CSS classes */
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Height of the blur zone */
|
|
32
|
+
height?: string;
|
|
33
|
+
/** Position of the blur effect */
|
|
34
|
+
position?: "top" | "bottom" | "both";
|
|
35
|
+
/** Custom blur levels (default: exponential scale from 0.5px to 64px) */
|
|
36
|
+
blurLevels?: number[];
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ProgressiveBlur creates a layered blur effect using multiple stacked divs
|
|
42
|
+
* with increasing backdrop-filter blur values and gradient masks.
|
|
43
|
+
*
|
|
44
|
+
* The default blur levels [0.5, 1, 2, 4, 8, 16, 32, 64] create an exponential
|
|
45
|
+
* progression that looks smooth and natural.
|
|
46
|
+
*/
|
|
47
|
+
export function ProgressiveBlur({
|
|
48
|
+
className,
|
|
49
|
+
height = "30%",
|
|
50
|
+
position = "bottom",
|
|
51
|
+
blurLevels = [0.5, 1, 2, 4, 8, 16, 32, 64],
|
|
52
|
+
}: ProgressiveBlurProps) {
|
|
53
|
+
// Create array with length equal to blurLevels.length - 2 (for before/after pseudo elements)
|
|
54
|
+
const divElements = Array(blurLevels.length - 2).fill(null);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"gradient-blur pointer-events-none absolute inset-x-0 z-10",
|
|
60
|
+
className,
|
|
61
|
+
position === "top"
|
|
62
|
+
? "top-0"
|
|
63
|
+
: position === "bottom"
|
|
64
|
+
? "bottom-0"
|
|
65
|
+
: "inset-y-0"
|
|
66
|
+
)}
|
|
67
|
+
style={{
|
|
68
|
+
height: position === "both" ? "100%" : height,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{/* First blur layer (pseudo element) */}
|
|
72
|
+
<div
|
|
73
|
+
className="absolute inset-0"
|
|
74
|
+
style={{
|
|
75
|
+
zIndex: 1,
|
|
76
|
+
backdropFilter: `blur(${blurLevels[0]}px)`,
|
|
77
|
+
WebkitBackdropFilter: `blur(${blurLevels[0]}px)`,
|
|
78
|
+
maskImage:
|
|
79
|
+
position === "bottom"
|
|
80
|
+
? `linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)`
|
|
81
|
+
: position === "top"
|
|
82
|
+
? `linear-gradient(to top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)`
|
|
83
|
+
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`,
|
|
84
|
+
WebkitMaskImage:
|
|
85
|
+
position === "bottom"
|
|
86
|
+
? `linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)`
|
|
87
|
+
: position === "top"
|
|
88
|
+
? `linear-gradient(to top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)`
|
|
89
|
+
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`,
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* Middle blur layers */}
|
|
94
|
+
{divElements.map((_, index) => {
|
|
95
|
+
const blurIndex = index + 1;
|
|
96
|
+
const startPercent = blurIndex * 12.5;
|
|
97
|
+
const midPercent = (blurIndex + 1) * 12.5;
|
|
98
|
+
const endPercent = (blurIndex + 2) * 12.5;
|
|
99
|
+
|
|
100
|
+
const maskGradient =
|
|
101
|
+
position === "bottom"
|
|
102
|
+
? `linear-gradient(to bottom, rgba(0,0,0,0) ${startPercent}%, rgba(0,0,0,1) ${midPercent}%, rgba(0,0,0,1) ${endPercent}%, rgba(0,0,0,0) ${endPercent + 12.5}%)`
|
|
103
|
+
: position === "top"
|
|
104
|
+
? `linear-gradient(to top, rgba(0,0,0,0) ${startPercent}%, rgba(0,0,0,1) ${midPercent}%, rgba(0,0,0,1) ${endPercent}%, rgba(0,0,0,0) ${endPercent + 12.5}%)`
|
|
105
|
+
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
key={`blur-${index}`}
|
|
110
|
+
className="absolute inset-0"
|
|
111
|
+
style={{
|
|
112
|
+
zIndex: index + 2,
|
|
113
|
+
backdropFilter: `blur(${blurLevels[blurIndex]}px)`,
|
|
114
|
+
WebkitBackdropFilter: `blur(${blurLevels[blurIndex]}px)`,
|
|
115
|
+
maskImage: maskGradient,
|
|
116
|
+
WebkitMaskImage: maskGradient,
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
|
|
122
|
+
{/* Last blur layer (pseudo element) */}
|
|
123
|
+
<div
|
|
124
|
+
className="absolute inset-0"
|
|
125
|
+
style={{
|
|
126
|
+
zIndex: blurLevels.length,
|
|
127
|
+
backdropFilter: `blur(${blurLevels[blurLevels.length - 1]}px)`,
|
|
128
|
+
WebkitBackdropFilter: `blur(${blurLevels[blurLevels.length - 1]}px)`,
|
|
129
|
+
maskImage:
|
|
130
|
+
position === "bottom"
|
|
131
|
+
? `linear-gradient(to bottom, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
|
|
132
|
+
: position === "top"
|
|
133
|
+
? `linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
|
|
134
|
+
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`,
|
|
135
|
+
WebkitMaskImage:
|
|
136
|
+
position === "bottom"
|
|
137
|
+
? `linear-gradient(to bottom, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
|
|
138
|
+
: position === "top"
|
|
139
|
+
? `linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
|
|
140
|
+
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Horizontal Blur Variants
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
export interface HorizontalProgressiveBlurProps {
|
|
152
|
+
/** Width of the blur zone in pixels */
|
|
153
|
+
width: number;
|
|
154
|
+
/** Additional CSS classes */
|
|
155
|
+
className?: string;
|
|
156
|
+
/** Custom blur levels (default: smooth transition for bidirectional effect) */
|
|
157
|
+
blurLevels?: number[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* HorizontalProgressiveBlur - Creates a bidirectional horizontal progressive blur effect.
|
|
162
|
+
*
|
|
163
|
+
* Blurs from both edges toward the center, creating maximum blur at the center (the seam)
|
|
164
|
+
* and fading to clear on both the left and right sides. Perfect for seamless image transitions.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```tsx
|
|
168
|
+
* // Center seam blur between two images
|
|
169
|
+
* <div className="relative">
|
|
170
|
+
* <img src="/left.jpg" />
|
|
171
|
+
* <HorizontalProgressiveBlur width={100} className="left-1/2 -translate-x-1/2" />
|
|
172
|
+
* <img src="/right.jpg" />
|
|
173
|
+
* </div>
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function HorizontalProgressiveBlur({
|
|
177
|
+
width,
|
|
178
|
+
className = "",
|
|
179
|
+
blurLevels = [0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 9, 13, 18, 24],
|
|
180
|
+
}: HorizontalProgressiveBlurProps) {
|
|
181
|
+
const numLayers = blurLevels.length;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
"absolute top-0 bottom-0 pointer-events-none",
|
|
187
|
+
className
|
|
188
|
+
)}
|
|
189
|
+
style={{ width }}
|
|
190
|
+
>
|
|
191
|
+
{blurLevels.map((blur, index) => {
|
|
192
|
+
// Bidirectional: blur increases from edges (0%, 100%) toward center (50%)
|
|
193
|
+
// Each layer covers a band that gets narrower as blur increases
|
|
194
|
+
const layerRatio = index / (numLayers - 1); // 0 to 1
|
|
195
|
+
const bandStart = layerRatio * 47; // 0% to 47%
|
|
196
|
+
const bandEnd = 100 - bandStart; // 100% to 53%
|
|
197
|
+
|
|
198
|
+
// Smoother gradient transitions
|
|
199
|
+
const fadeWidth = 3;
|
|
200
|
+
const maskGradient = `linear-gradient(to right,
|
|
201
|
+
rgba(0,0,0,0) 0%,
|
|
202
|
+
rgba(0,0,0,0) ${Math.max(0, bandStart - fadeWidth)}%,
|
|
203
|
+
rgba(0,0,0,1) ${bandStart + fadeWidth}%,
|
|
204
|
+
rgba(0,0,0,1) ${bandEnd - fadeWidth}%,
|
|
205
|
+
rgba(0,0,0,0) ${Math.min(100, bandEnd + fadeWidth)}%,
|
|
206
|
+
rgba(0,0,0,0) 100%
|
|
207
|
+
)`;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
key={`blur-layer-${index}`}
|
|
212
|
+
className="absolute inset-0"
|
|
213
|
+
style={{
|
|
214
|
+
zIndex: index + 1,
|
|
215
|
+
backdropFilter: `blur(${blur}px)`,
|
|
216
|
+
WebkitBackdropFilter: `blur(${blur}px)`,
|
|
217
|
+
maskImage: maskGradient,
|
|
218
|
+
WebkitMaskImage: maskGradient,
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface RightToLeftProgressiveBlurProps {
|
|
228
|
+
/** Width of the blur zone in pixels */
|
|
229
|
+
width: number;
|
|
230
|
+
/** Additional CSS classes */
|
|
231
|
+
className?: string;
|
|
232
|
+
/** Custom blur levels (default: exponential increase for unidirectional effect) */
|
|
233
|
+
blurLevels?: number[];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* RightToLeftProgressiveBlur - Creates a unidirectional progressive blur effect.
|
|
238
|
+
*
|
|
239
|
+
* Blur increases from right (clear) to left (max blur). Anchored to the right edge,
|
|
240
|
+
* blur builds up toward the left. Useful for fading content into a blurred edge.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```tsx
|
|
244
|
+
* // Blur left edge of content
|
|
245
|
+
* <div className="relative">
|
|
246
|
+
* <RightToLeftProgressiveBlur width={80} className="left-0" />
|
|
247
|
+
* <Content />
|
|
248
|
+
* </div>
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export function RightToLeftProgressiveBlur({
|
|
252
|
+
width,
|
|
253
|
+
className = "",
|
|
254
|
+
blurLevels = [1, 2, 4, 8, 16, 24, 32, 40],
|
|
255
|
+
}: RightToLeftProgressiveBlurProps) {
|
|
256
|
+
const numLayers = blurLevels.length;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
className={cn(
|
|
261
|
+
"absolute top-0 bottom-0 pointer-events-none",
|
|
262
|
+
className
|
|
263
|
+
)}
|
|
264
|
+
style={{ width }}
|
|
265
|
+
>
|
|
266
|
+
{blurLevels.map((blur, index) => {
|
|
267
|
+
// Each layer starts from the right and extends further left
|
|
268
|
+
// Layer 0 (lowest blur): covers 0% - 12.5%
|
|
269
|
+
// Layer N (highest blur): covers 0% - 100%
|
|
270
|
+
const endPercent = ((index + 1) / numLayers) * 100;
|
|
271
|
+
|
|
272
|
+
const maskGradient = `linear-gradient(to right,
|
|
273
|
+
rgba(0,0,0,1) 0%,
|
|
274
|
+
rgba(0,0,0,1) ${endPercent}%,
|
|
275
|
+
rgba(0,0,0,0) ${Math.min(100, endPercent + 5)}%,
|
|
276
|
+
rgba(0,0,0,0) 100%
|
|
277
|
+
)`;
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<div
|
|
281
|
+
key={`blur-layer-${index}`}
|
|
282
|
+
className="absolute inset-0"
|
|
283
|
+
style={{
|
|
284
|
+
zIndex: index + 1,
|
|
285
|
+
backdropFilter: `blur(${blur}px)`,
|
|
286
|
+
WebkitBackdropFilter: `blur(${blur}px)`,
|
|
287
|
+
maskImage: maskGradient,
|
|
288
|
+
WebkitMaskImage: maskGradient,
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
})}
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
export interface ThemeToggleProps {
|
|
6
|
+
isDark?: boolean;
|
|
7
|
+
onToggle?: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ThemeToggle - A primitive floating button for toggling between light and dark themes
|
|
15
|
+
*
|
|
16
|
+
* A fixed-position button that displays a sun/moon icon based on the current theme.
|
|
17
|
+
* Commonly used for implementing dark mode toggles in applications.
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Fixed positioning with configurable corners
|
|
21
|
+
* - Animated icon transitions
|
|
22
|
+
* - Accessible with ARIA labels
|
|
23
|
+
* - Responsive hover and active states
|
|
24
|
+
* - Built-in disabled state
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const [isDark, setIsDark] = useState(false);
|
|
29
|
+
*
|
|
30
|
+
* <ThemeToggle
|
|
31
|
+
* isDark={isDark}
|
|
32
|
+
* onToggle={() => setIsDark(!isDark)}
|
|
33
|
+
* position="bottom-right"
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // With Next.js theme provider
|
|
40
|
+
* import { useTheme } from 'next-themes';
|
|
41
|
+
*
|
|
42
|
+
* function MyApp() {
|
|
43
|
+
* const { theme, setTheme } = useTheme();
|
|
44
|
+
* return (
|
|
45
|
+
* <ThemeToggle
|
|
46
|
+
* isDark={theme === 'dark'}
|
|
47
|
+
* onToggle={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
48
|
+
* />
|
|
49
|
+
* );
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param isDark - Whether dark mode is currently active (default: false)
|
|
54
|
+
* @param onToggle - Callback function when toggle is clicked
|
|
55
|
+
* @param className - Additional CSS classes for customization
|
|
56
|
+
* @param position - Corner position (default: "bottom-right")
|
|
57
|
+
* @param disabled - Disable the toggle button (default: false)
|
|
58
|
+
*/
|
|
59
|
+
export function ThemeToggle({
|
|
60
|
+
isDark = false,
|
|
61
|
+
onToggle,
|
|
62
|
+
className = "",
|
|
63
|
+
position = "bottom-right",
|
|
64
|
+
disabled = false,
|
|
65
|
+
}: ThemeToggleProps) {
|
|
66
|
+
const positionClasses = {
|
|
67
|
+
"bottom-right": "bottom-6 right-6",
|
|
68
|
+
"bottom-left": "bottom-6 left-6",
|
|
69
|
+
"top-right": "top-6 right-6",
|
|
70
|
+
"top-left": "top-6 left-6",
|
|
71
|
+
}[position];
|
|
72
|
+
|
|
73
|
+
const handleClick = () => {
|
|
74
|
+
if (!disabled && onToggle) {
|
|
75
|
+
onToggle();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={handleClick}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
className={`
|
|
85
|
+
fixed z-50 w-12 h-12 rounded-full shadow-lg transition-all duration-200
|
|
86
|
+
flex items-center justify-center
|
|
87
|
+
bg-background text-foreground border border-border hover:bg-muted
|
|
88
|
+
${positionClasses}
|
|
89
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:scale-105 active:scale-95 cursor-pointer'}
|
|
90
|
+
${className}
|
|
91
|
+
`}
|
|
92
|
+
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
|
|
93
|
+
title={`Switch to ${isDark ? 'light' : 'dark'} mode`}
|
|
94
|
+
>
|
|
95
|
+
{isDark ? (
|
|
96
|
+
// Sun icon for light mode
|
|
97
|
+
<svg
|
|
98
|
+
className="w-5 h-5"
|
|
99
|
+
fill="none"
|
|
100
|
+
strokeLinecap="round"
|
|
101
|
+
strokeLinejoin="round"
|
|
102
|
+
strokeWidth="2"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
>
|
|
106
|
+
<circle cx="12" cy="12" r="5" />
|
|
107
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
|
108
|
+
</svg>
|
|
109
|
+
) : (
|
|
110
|
+
// Moon icon for dark mode
|
|
111
|
+
<svg
|
|
112
|
+
className="w-5 h-5"
|
|
113
|
+
fill="none"
|
|
114
|
+
strokeLinecap="round"
|
|
115
|
+
strokeLinejoin="round"
|
|
116
|
+
strokeWidth="2"
|
|
117
|
+
viewBox="0 0 24 24"
|
|
118
|
+
stroke="currentColor"
|
|
119
|
+
>
|
|
120
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
121
|
+
</svg>
|
|
122
|
+
)}
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0064 §3 Layer 3 — story-coverage gate.
|
|
3
|
+
*
|
|
4
|
+
* Every file in `src/primitives/*.tsx` must have a matching
|
|
5
|
+
* `src/primitives/stories/*.stories.tsx`. Fails CI on net-new
|
|
6
|
+
* primitives shipped without stories.
|
|
7
|
+
*
|
|
8
|
+
* Naming heuristic:
|
|
9
|
+
* - `Button.tsx` → `Button.stories.tsx` (PascalCase)
|
|
10
|
+
* - `accordion.tsx` → `Accordion.stories.tsx` (lowercase → PascalCase)
|
|
11
|
+
* - `dropdown-menu.tsx` → `DropdownMenu.stories.tsx` (kebab → PascalCase)
|
|
12
|
+
*
|
|
13
|
+
* Allowlist (`PRIMITIVES_WITHOUT_STORIES`) is for files that are NOT
|
|
14
|
+
* actual primitives (e.g., utility components, animation helpers,
|
|
15
|
+
* brand-name components). Each entry needs a one-line comment explaining
|
|
16
|
+
* why it doesn't need a story.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from "vitest";
|
|
20
|
+
import { promises as fs } from "fs";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
|
|
23
|
+
const PRIMITIVES_DIR = join(__dirname, "..");
|
|
24
|
+
const STORIES_DIR = join(__dirname, "..", "stories");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Files in primitives/ that don't need a story. Each entry MUST cite
|
|
28
|
+
* a reason; reviewers reject without one.
|
|
29
|
+
*/
|
|
30
|
+
const PRIMITIVES_WITHOUT_STORIES = new Map<string, string>([
|
|
31
|
+
["index", "barrel export, not a primitive"],
|
|
32
|
+
["BrandLogo", "wraps next/image with brand-config props; no behavior to story"],
|
|
33
|
+
["BrandName", "thin <span> reading from brand-config; covered by AppShell story when one exists"],
|
|
34
|
+
["DragHintAnimation", "decorative animation; no a11y-relevant state surface"],
|
|
35
|
+
["EdgeSwipeGuards", "iOS gesture-suppression overlay; no visible UI"],
|
|
36
|
+
["FloatingActionGroup", "layout container; tested via composed-pattern stories when those land"],
|
|
37
|
+
["ProductPrice", "formatter component; covered by formatter unit tests"],
|
|
38
|
+
["ProgressiveBlur", "decorative blur overlay; no a11y-relevant state surface"],
|
|
39
|
+
["scroll-fade", "decorative scroll mask; no a11y-relevant state surface"],
|
|
40
|
+
["ColorSwatch", "TODO — story coverage gap; ADR-0064 §2 cleanup PR queues this"],
|
|
41
|
+
["ThemeToggle", "TODO — story coverage gap; ADR-0064 §2 cleanup PR queues this"],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function toPascal(slug: string): string {
|
|
45
|
+
return slug
|
|
46
|
+
.split(/[-_]/g)
|
|
47
|
+
.filter((s) => s.length > 0)
|
|
48
|
+
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
49
|
+
.join("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("ADR-0064 §3 Layer 3 — primitive story coverage", () => {
|
|
53
|
+
it("every primitive has a matching .stories.tsx (or is allowlisted)", async () => {
|
|
54
|
+
const primitives = (await fs.readdir(PRIMITIVES_DIR))
|
|
55
|
+
.filter((f) => f.endsWith(".tsx"))
|
|
56
|
+
.map((f) => f.replace(/\.tsx$/, ""));
|
|
57
|
+
|
|
58
|
+
const stories = new Set(
|
|
59
|
+
(await fs.readdir(STORIES_DIR))
|
|
60
|
+
.filter((f) => f.endsWith(".stories.tsx"))
|
|
61
|
+
.map((f) => f.replace(/\.stories\.tsx$/, "")),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const violations: string[] = [];
|
|
65
|
+
for (const p of primitives) {
|
|
66
|
+
if (PRIMITIVES_WITHOUT_STORIES.has(p)) continue;
|
|
67
|
+
const expected = toPascal(p);
|
|
68
|
+
if (!stories.has(expected)) {
|
|
69
|
+
violations.push(`${p}.tsx → expected stories/${expected}.stories.tsx`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (violations.length > 0) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Primitives without stories (ADR-0064 §3 Layer 3):\n ${violations.join("\n ")}\n` +
|
|
75
|
+
`Either add the story or allowlist with reason in PRIMITIVES_WITHOUT_STORIES.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("every allowlist entry corresponds to a real primitive file", async () => {
|
|
81
|
+
const primitives = new Set(
|
|
82
|
+
(await fs.readdir(PRIMITIVES_DIR))
|
|
83
|
+
.filter((f) => f.endsWith(".tsx") || f === "index")
|
|
84
|
+
.map((f) => f.replace(/\.tsx$/, "")),
|
|
85
|
+
);
|
|
86
|
+
primitives.add("index");
|
|
87
|
+
const stale: string[] = [];
|
|
88
|
+
for (const allowed of PRIMITIVES_WITHOUT_STORIES.keys()) {
|
|
89
|
+
if (!primitives.has(allowed)) {
|
|
90
|
+
stale.push(allowed);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
expect(
|
|
94
|
+
stale,
|
|
95
|
+
`Stale allowlist entries (primitive removed but exception remains): ${stale.join(", ")}`,
|
|
96
|
+
).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
});
|