@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,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import * as SliderPrimitive from '@radix-ui/react-slider';
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
interface SliderProps
|
|
10
|
+
extends Omit<
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,
|
|
12
|
+
'value' | 'onValueChange'
|
|
13
|
+
> {
|
|
14
|
+
// Accepts both array and single number for convenience
|
|
15
|
+
value?: number | number[];
|
|
16
|
+
onValueChange?: (value: number[]) => void;
|
|
17
|
+
onChangeEnd?: (value: number[]) => void;
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Context for passing slider state to compound components
|
|
22
|
+
interface SliderContextValue {
|
|
23
|
+
values: number[];
|
|
24
|
+
min: number;
|
|
25
|
+
max: number;
|
|
26
|
+
}
|
|
27
|
+
const SliderContext = React.createContext<SliderContextValue | null>(null);
|
|
28
|
+
|
|
29
|
+
// HeroUI-compatible compound components
|
|
30
|
+
const SliderTrack = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
31
|
+
({ className, children, ...props }, ref) => (
|
|
32
|
+
<SliderPrimitive.Track
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn('relative h-2 w-full grow overflow-hidden rounded-full bg-muted', className)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</SliderPrimitive.Track>
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
SliderTrack.displayName = 'SliderTrack';
|
|
42
|
+
|
|
43
|
+
const SliderThumb = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
44
|
+
({ className, ...props }, ref) => (
|
|
45
|
+
<SliderPrimitive.Thumb
|
|
46
|
+
ref={ref}
|
|
47
|
+
// Base thumb size: h-6 w-6 (24px), touch devices can override via CSS to 32-36px
|
|
48
|
+
className={cn(
|
|
49
|
+
'relative block h-6 w-6 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
SliderThumb.displayName = 'SliderThumb';
|
|
57
|
+
|
|
58
|
+
// HeroUI Fill component - displays the filled range portion
|
|
59
|
+
const SliderFill = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
60
|
+
({ className, ...props }, ref) => (
|
|
61
|
+
<SliderPrimitive.Range ref={ref} className={cn('absolute h-full bg-primary', className)} {...props} />
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
SliderFill.displayName = 'SliderFill';
|
|
65
|
+
|
|
66
|
+
// HeroUI Output component - renders current value(s)
|
|
67
|
+
interface SliderOutputProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'> {
|
|
68
|
+
children?: React.ReactNode | ((props: { state: { values: number[] } }) => React.ReactNode);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const SliderOutput = React.forwardRef<HTMLSpanElement, SliderOutputProps>(
|
|
72
|
+
({ className, children, ...props }, ref) => {
|
|
73
|
+
const context = React.useContext(SliderContext);
|
|
74
|
+
const values = context?.values ?? [0];
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<span ref={ref} className={cn('text-sm text-foreground/60', className)} {...props}>
|
|
78
|
+
{typeof children === 'function'
|
|
79
|
+
? children({ state: { values } })
|
|
80
|
+
: children ?? values.join(' - ')}
|
|
81
|
+
</span>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
SliderOutput.displayName = 'SliderOutput';
|
|
86
|
+
|
|
87
|
+
const SliderBase = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
|
88
|
+
(
|
|
89
|
+
{
|
|
90
|
+
className,
|
|
91
|
+
value,
|
|
92
|
+
onValueChange,
|
|
93
|
+
onChangeEnd,
|
|
94
|
+
min = 0,
|
|
95
|
+
max = 100,
|
|
96
|
+
step = 1,
|
|
97
|
+
children,
|
|
98
|
+
...props
|
|
99
|
+
},
|
|
100
|
+
ref
|
|
101
|
+
) => {
|
|
102
|
+
// Normalize value to array (accepts both single number and array for convenience)
|
|
103
|
+
const normalizedValue = value !== undefined ? (Array.isArray(value) ? value : [value]) : undefined;
|
|
104
|
+
|
|
105
|
+
// Track internal value for context
|
|
106
|
+
const [internalValue, setInternalValue] = React.useState(normalizedValue ?? [min]);
|
|
107
|
+
|
|
108
|
+
// Track if we're currently dragging (for iOS body scroll prevention)
|
|
109
|
+
const isDraggingRef = React.useRef(false);
|
|
110
|
+
const touchMoveHandlerRef = React.useRef<((e: TouchEvent) => void) | null>(null);
|
|
111
|
+
|
|
112
|
+
// Prevent scroll during drag - added to document on pointer down, removed on pointer up
|
|
113
|
+
const preventScroll = React.useCallback((e: TouchEvent) => {
|
|
114
|
+
if (isDraggingRef.current) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
}
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const handleChange = (newValue: number[]) => {
|
|
120
|
+
// Mark as actively dragging once value changes
|
|
121
|
+
isDraggingRef.current = true;
|
|
122
|
+
setInternalValue(newValue);
|
|
123
|
+
onValueChange?.(newValue);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleChangeEnd = (newValue: number[]) => {
|
|
127
|
+
isDraggingRef.current = false;
|
|
128
|
+
onChangeEnd?.(newValue);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Handle pointer down - add document-level touchmove prevention
|
|
132
|
+
const handlePointerDown = React.useCallback(() => {
|
|
133
|
+
// Add touchmove listener to document to intercept scroll during drag
|
|
134
|
+
if (!touchMoveHandlerRef.current) {
|
|
135
|
+
touchMoveHandlerRef.current = preventScroll;
|
|
136
|
+
document.addEventListener('touchmove', preventScroll, { passive: false });
|
|
137
|
+
}
|
|
138
|
+
}, [preventScroll]);
|
|
139
|
+
|
|
140
|
+
// Handle pointer up/cancel - remove document listener
|
|
141
|
+
const handlePointerUp = React.useCallback(() => {
|
|
142
|
+
isDraggingRef.current = false;
|
|
143
|
+
if (touchMoveHandlerRef.current) {
|
|
144
|
+
document.removeEventListener('touchmove', touchMoveHandlerRef.current);
|
|
145
|
+
touchMoveHandlerRef.current = null;
|
|
146
|
+
}
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
// Cleanup on unmount
|
|
150
|
+
React.useEffect(() => {
|
|
151
|
+
return () => {
|
|
152
|
+
if (touchMoveHandlerRef.current) {
|
|
153
|
+
document.removeEventListener('touchmove', touchMoveHandlerRef.current);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
// Check if children are provided (compound pattern)
|
|
159
|
+
const hasChildren = React.Children.count(children) > 0;
|
|
160
|
+
|
|
161
|
+
// Determine number of thumbs based on value array length or defaultValue
|
|
162
|
+
const thumbCount = normalizedValue?.length ?? props.defaultValue?.length ?? 1;
|
|
163
|
+
|
|
164
|
+
// Context value for compound components
|
|
165
|
+
const contextValue = React.useMemo(
|
|
166
|
+
() => ({
|
|
167
|
+
values: normalizedValue ?? internalValue,
|
|
168
|
+
min,
|
|
169
|
+
max,
|
|
170
|
+
}),
|
|
171
|
+
[normalizedValue, internalValue, min, max]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<SliderContext.Provider value={contextValue}>
|
|
176
|
+
<SliderPrimitive.Root
|
|
177
|
+
ref={ref}
|
|
178
|
+
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
|
179
|
+
value={normalizedValue}
|
|
180
|
+
onValueChange={handleChange}
|
|
181
|
+
onValueCommit={handleChangeEnd}
|
|
182
|
+
onPointerDown={handlePointerDown}
|
|
183
|
+
onPointerUp={handlePointerUp}
|
|
184
|
+
onPointerCancel={handlePointerUp}
|
|
185
|
+
onLostPointerCapture={handlePointerUp}
|
|
186
|
+
min={min}
|
|
187
|
+
max={max}
|
|
188
|
+
step={step}
|
|
189
|
+
{...props}
|
|
190
|
+
>
|
|
191
|
+
{hasChildren ? (
|
|
192
|
+
children
|
|
193
|
+
) : (
|
|
194
|
+
<>
|
|
195
|
+
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-muted">
|
|
196
|
+
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
197
|
+
</SliderPrimitive.Track>
|
|
198
|
+
{Array.from({ length: thumbCount }).map((_, index) => (
|
|
199
|
+
<SliderPrimitive.Thumb
|
|
200
|
+
key={index}
|
|
201
|
+
className="relative block h-6 w-6 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
</SliderPrimitive.Root>
|
|
207
|
+
</SliderContext.Provider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
SliderBase.displayName = SliderPrimitive.Root.displayName;
|
|
212
|
+
|
|
213
|
+
// Compound component with sub-components
|
|
214
|
+
const Slider = Object.assign(SliderBase, {
|
|
215
|
+
Track: SliderTrack,
|
|
216
|
+
Thumb: SliderThumb,
|
|
217
|
+
Fill: SliderFill,
|
|
218
|
+
Output: SliderOutput,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export { Slider, SliderTrack, SliderThumb, SliderFill, SliderOutput };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
const spinnerVariants = cva('pointer-events-none relative origin-center animate-spin size-6 text-primary', {
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: 'size-4',
|
|
11
|
+
md: 'size-6',
|
|
12
|
+
lg: 'size-8',
|
|
13
|
+
xl: 'size-10',
|
|
14
|
+
},
|
|
15
|
+
color: {
|
|
16
|
+
primary: 'text-primary',
|
|
17
|
+
secondary: 'text-foreground/60',
|
|
18
|
+
success: 'text-success',
|
|
19
|
+
warning: 'text-warning',
|
|
20
|
+
danger: 'text-danger',
|
|
21
|
+
current: 'text-current',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export interface SpinnerProps
|
|
27
|
+
extends Omit<React.ComponentProps<'span'>, 'color'>,
|
|
28
|
+
VariantProps<typeof spinnerVariants> {
|
|
29
|
+
/** Label for screen readers */
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Spinner({ className, size, color, label = 'Loading', ...props }: SpinnerProps) {
|
|
34
|
+
const id = React.useId();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<span
|
|
38
|
+
role="status"
|
|
39
|
+
aria-label={label}
|
|
40
|
+
className={cn(spinnerVariants({ size, color }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<svg viewBox="0 0 24 24" className="size-full" aria-hidden>
|
|
44
|
+
<defs>
|
|
45
|
+
<linearGradient id={`spinner-grad-1-${id}`} x1="50%" x2="50%" y1="5.271%" y2="91.793%">
|
|
46
|
+
<stop offset="0%" stopColor="currentColor" />
|
|
47
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity={0.55} />
|
|
48
|
+
</linearGradient>
|
|
49
|
+
<linearGradient id={`spinner-grad-2-${id}`} x1="50%" x2="50%" y1="15.24%" y2="87.15%">
|
|
50
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity={0} />
|
|
51
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity={0.55} />
|
|
52
|
+
</linearGradient>
|
|
53
|
+
</defs>
|
|
54
|
+
<g fill="none">
|
|
55
|
+
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
|
56
|
+
<path
|
|
57
|
+
d="M8.749.021a1.5 1.5 0 0 1 .497 2.958A7.5 7.5 0 0 0 3 10.375a7.5 7.5 0 0 0 7.5 7.5v3c-5.799 0-10.5-4.7-10.5-10.5C0 5.23 3.726.865 8.749.021"
|
|
58
|
+
fill={`url(#spinner-grad-1-${id})`}
|
|
59
|
+
transform="translate(1.5 1.625)"
|
|
60
|
+
/>
|
|
61
|
+
<path
|
|
62
|
+
d="M15.392 2.673a1.5 1.5 0 0 1 2.119-.115A10.48 10.48 0 0 1 21 10.375c0 5.8-4.701 10.5-10.5 10.5v-3a7.5 7.5 0 0 0 5.007-13.084a1.5 1.5 0 0 1-.115-2.118"
|
|
63
|
+
fill={`url(#spinner-grad-2-${id})`}
|
|
64
|
+
transform="translate(1.5 1.625)"
|
|
65
|
+
/>
|
|
66
|
+
</g>
|
|
67
|
+
</svg>
|
|
68
|
+
</span>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { Spinner, spinnerVariants };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '../accordion';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'UI/Accordion',
|
|
6
|
+
component: Accordion,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
} satisfies Meta<typeof Accordion>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof Accordion>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Accordion type="single" collapsible className="w-[400px]">
|
|
19
|
+
<AccordionItem value="item-1">
|
|
20
|
+
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
|
21
|
+
<AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
|
|
22
|
+
</AccordionItem>
|
|
23
|
+
<AccordionItem value="item-2">
|
|
24
|
+
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
|
25
|
+
<AccordionContent>
|
|
26
|
+
Yes. It comes with default styles that match your theme.
|
|
27
|
+
</AccordionContent>
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
<AccordionItem value="item-3">
|
|
30
|
+
<AccordionTrigger>Is it animated?</AccordionTrigger>
|
|
31
|
+
<AccordionContent>
|
|
32
|
+
Yes. It has smooth expand/collapse animations using CSS keyframes.
|
|
33
|
+
</AccordionContent>
|
|
34
|
+
</AccordionItem>
|
|
35
|
+
</Accordion>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Multiple: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<Accordion type="multiple" className="w-[400px]">
|
|
42
|
+
<AccordionItem value="item-1">
|
|
43
|
+
<AccordionTrigger>Can I open multiple items?</AccordionTrigger>
|
|
44
|
+
<AccordionContent>
|
|
45
|
+
Yes! When using type="multiple", you can have multiple items open at once.
|
|
46
|
+
</AccordionContent>
|
|
47
|
+
</AccordionItem>
|
|
48
|
+
<AccordionItem value="item-2">
|
|
49
|
+
<AccordionTrigger>How does it work?</AccordionTrigger>
|
|
50
|
+
<AccordionContent>
|
|
51
|
+
Just set the type prop to "multiple" instead of "single".
|
|
52
|
+
</AccordionContent>
|
|
53
|
+
</AccordionItem>
|
|
54
|
+
<AccordionItem value="item-3">
|
|
55
|
+
<AccordionTrigger>What about collapsible?</AccordionTrigger>
|
|
56
|
+
<AccordionContent>
|
|
57
|
+
The collapsible prop only applies to type="single" accordions.
|
|
58
|
+
</AccordionContent>
|
|
59
|
+
</AccordionItem>
|
|
60
|
+
</Accordion>
|
|
61
|
+
),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const DefaultOpen: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<Accordion type="single" defaultValue="item-2" collapsible className="w-[400px]">
|
|
67
|
+
<AccordionItem value="item-1">
|
|
68
|
+
<AccordionTrigger>First Section</AccordionTrigger>
|
|
69
|
+
<AccordionContent>Content for the first section.</AccordionContent>
|
|
70
|
+
</AccordionItem>
|
|
71
|
+
<AccordionItem value="item-2">
|
|
72
|
+
<AccordionTrigger>Second Section (Default Open)</AccordionTrigger>
|
|
73
|
+
<AccordionContent>
|
|
74
|
+
This section is open by default because of the defaultValue prop.
|
|
75
|
+
</AccordionContent>
|
|
76
|
+
</AccordionItem>
|
|
77
|
+
<AccordionItem value="item-3">
|
|
78
|
+
<AccordionTrigger>Third Section</AccordionTrigger>
|
|
79
|
+
<AccordionContent>Content for the third section.</AccordionContent>
|
|
80
|
+
</AccordionItem>
|
|
81
|
+
</Accordion>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const FAQ: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<div className="w-[500px]">
|
|
88
|
+
<h2 className="text-xl font-heading mb-4">Frequently Asked Questions</h2>
|
|
89
|
+
<Accordion type="single" collapsible>
|
|
90
|
+
<AccordionItem value="q1">
|
|
91
|
+
<AccordionTrigger>What payment methods do you accept?</AccordionTrigger>
|
|
92
|
+
<AccordionContent>
|
|
93
|
+
We accept all major credit cards (Visa, MasterCard, American Express), PayPal,
|
|
94
|
+
and bank transfers for larger orders.
|
|
95
|
+
</AccordionContent>
|
|
96
|
+
</AccordionItem>
|
|
97
|
+
<AccordionItem value="q2">
|
|
98
|
+
<AccordionTrigger>How long does shipping take?</AccordionTrigger>
|
|
99
|
+
<AccordionContent>
|
|
100
|
+
Standard shipping takes 5-7 business days. Express shipping (2-3 days) is
|
|
101
|
+
available for an additional fee. International orders may take 10-14 days.
|
|
102
|
+
</AccordionContent>
|
|
103
|
+
</AccordionItem>
|
|
104
|
+
<AccordionItem value="q3">
|
|
105
|
+
<AccordionTrigger>What is your return policy?</AccordionTrigger>
|
|
106
|
+
<AccordionContent>
|
|
107
|
+
We offer a 30-day return policy for all unused items in their original
|
|
108
|
+
packaging. Custom orders are final sale and cannot be returned.
|
|
109
|
+
</AccordionContent>
|
|
110
|
+
</AccordionItem>
|
|
111
|
+
<AccordionItem value="q4">
|
|
112
|
+
<AccordionTrigger>Do you offer wholesale pricing?</AccordionTrigger>
|
|
113
|
+
<AccordionContent>
|
|
114
|
+
Yes! We offer wholesale pricing for orders of 50+ units. Please contact our
|
|
115
|
+
sales team at wholesale@example.com for more information.
|
|
116
|
+
</AccordionContent>
|
|
117
|
+
</AccordionItem>
|
|
118
|
+
</Accordion>
|
|
119
|
+
</div>
|
|
120
|
+
),
|
|
121
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Icon } from '@iconify/react';
|
|
3
|
+
import { Badge, Chip } from '../badge';
|
|
4
|
+
import { Surface } from '../surface';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Badge> = {
|
|
7
|
+
title: 'UI/Badge',
|
|
8
|
+
component: Badge,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
},
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
argTypes: {
|
|
14
|
+
variant: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['default', 'secondary', 'destructive', 'success', 'warning', 'outline', 'gradient'],
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
control: 'select',
|
|
20
|
+
options: ['default', 'sm', 'lg'],
|
|
21
|
+
},
|
|
22
|
+
color: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['default', 'primary', 'secondary', 'success', 'warning', 'danger'],
|
|
25
|
+
description: 'HeroUI Chip compatibility prop',
|
|
26
|
+
},
|
|
27
|
+
dot: {
|
|
28
|
+
control: 'boolean',
|
|
29
|
+
},
|
|
30
|
+
startContent: { control: false },
|
|
31
|
+
endContent: { control: false },
|
|
32
|
+
onClose: { control: false },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
38
|
+
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
children: 'Badge',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const AllVariants: Story = {
|
|
46
|
+
render: () => (
|
|
47
|
+
<div className="flex flex-wrap gap-2">
|
|
48
|
+
<Badge variant="default">Default</Badge>
|
|
49
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
50
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
51
|
+
<Badge variant="success">Success</Badge>
|
|
52
|
+
<Badge variant="warning">Warning</Badge>
|
|
53
|
+
<Badge variant="outline">Outline</Badge>
|
|
54
|
+
<Badge variant="gradient">Gradient</Badge>
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const AllSizes: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<Badge size="sm">Small</Badge>
|
|
63
|
+
<Badge size="default">Default</Badge>
|
|
64
|
+
<Badge size="lg">Large</Badge>
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const WithDot: Story = {
|
|
70
|
+
render: () => (
|
|
71
|
+
<div className="flex flex-wrap gap-2">
|
|
72
|
+
<Badge variant="default" dot>
|
|
73
|
+
Active
|
|
74
|
+
</Badge>
|
|
75
|
+
<Badge variant="success" dot>
|
|
76
|
+
Online
|
|
77
|
+
</Badge>
|
|
78
|
+
<Badge variant="warning" dot>
|
|
79
|
+
Pending
|
|
80
|
+
</Badge>
|
|
81
|
+
<Badge variant="destructive" dot>
|
|
82
|
+
Offline
|
|
83
|
+
</Badge>
|
|
84
|
+
</div>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const WithIcons: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<div className="flex flex-wrap gap-2">
|
|
91
|
+
<Badge startContent={<Icon icon="gravity-ui:check" className="size-3" />}>Verified</Badge>
|
|
92
|
+
<Badge endContent={<Icon icon="gravity-ui:arrow-right" className="size-3" />}>Next</Badge>
|
|
93
|
+
<Badge
|
|
94
|
+
variant="success"
|
|
95
|
+
startContent={<Icon icon="gravity-ui:circle-check-fill" className="size-3" />}
|
|
96
|
+
>
|
|
97
|
+
Completed
|
|
98
|
+
</Badge>
|
|
99
|
+
</div>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Dismissible: Story = {
|
|
104
|
+
render: () => (
|
|
105
|
+
<div className="flex flex-wrap gap-2">
|
|
106
|
+
<Badge onClose={() => alert('Dismissed!')}>Dismissible</Badge>
|
|
107
|
+
<Badge variant="secondary" onClose={() => alert('Dismissed!')}>
|
|
108
|
+
Click X to close
|
|
109
|
+
</Badge>
|
|
110
|
+
<Badge variant="success" onClose={() => alert('Dismissed!')}>
|
|
111
|
+
Success
|
|
112
|
+
</Badge>
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const HeroUIColorProp: Story = {
|
|
118
|
+
name: 'HeroUI Color Prop (Chip)',
|
|
119
|
+
render: () => (
|
|
120
|
+
<div className="flex flex-wrap gap-2">
|
|
121
|
+
<Chip color="default">Default</Chip>
|
|
122
|
+
<Chip color="primary">Primary</Chip>
|
|
123
|
+
<Chip color="secondary">Secondary</Chip>
|
|
124
|
+
<Chip color="success">Success</Chip>
|
|
125
|
+
<Chip color="warning">Warning</Chip>
|
|
126
|
+
<Chip color="danger">Danger</Chip>
|
|
127
|
+
</div>
|
|
128
|
+
),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const StatusBadges: Story = {
|
|
132
|
+
render: () => (
|
|
133
|
+
<div className="space-y-4">
|
|
134
|
+
<div className="flex items-center gap-2">
|
|
135
|
+
<span className="text-sm text-foreground/70 w-20">Order:</span>
|
|
136
|
+
<Badge variant="success" dot>
|
|
137
|
+
Shipped
|
|
138
|
+
</Badge>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="flex items-center gap-2">
|
|
141
|
+
<span className="text-sm text-foreground/70 w-20">Payment:</span>
|
|
142
|
+
<Badge variant="warning" dot>
|
|
143
|
+
Pending
|
|
144
|
+
</Badge>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<span className="text-sm text-foreground/70 w-20">Status:</span>
|
|
148
|
+
<Badge variant="destructive" dot>
|
|
149
|
+
Cancelled
|
|
150
|
+
</Badge>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const TagList: Story = {
|
|
157
|
+
render: () => (
|
|
158
|
+
<div className="flex flex-wrap gap-2 max-w-sm">
|
|
159
|
+
{['React', 'TypeScript', 'Tailwind', 'Radix UI', 'Storybook'].map((tag) => (
|
|
160
|
+
<Badge key={tag} variant="secondary" onClose={() => console.log(`Remove ${tag}`)}>
|
|
161
|
+
{tag}
|
|
162
|
+
</Badge>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const OnSurfaces: Story = {
|
|
169
|
+
name: 'Surface Awareness',
|
|
170
|
+
parameters: {
|
|
171
|
+
layout: 'padded',
|
|
172
|
+
},
|
|
173
|
+
render: () => (
|
|
174
|
+
<div className="space-y-8">
|
|
175
|
+
{/* On transparent background (page) */}
|
|
176
|
+
<div className="space-y-3">
|
|
177
|
+
<h3 className="text-sm font-medium text-muted-foreground">On Page Background (transparent)</h3>
|
|
178
|
+
<Surface variant="transparent" className="p-4 rounded-lg border border-dashed border-border">
|
|
179
|
+
<div className="flex flex-wrap gap-2">
|
|
180
|
+
<Badge variant="default">Default</Badge>
|
|
181
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
182
|
+
<Badge variant="outline">Outline</Badge>
|
|
183
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
184
|
+
<Badge variant="success">Success</Badge>
|
|
185
|
+
<Badge variant="warning">Warning</Badge>
|
|
186
|
+
</div>
|
|
187
|
+
</Surface>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* On default surface (card) */}
|
|
191
|
+
<div className="space-y-3">
|
|
192
|
+
<h3 className="text-sm font-medium text-muted-foreground">On Surface (card)</h3>
|
|
193
|
+
<Surface variant="default" className="p-4 rounded-lg">
|
|
194
|
+
<div className="flex flex-wrap gap-2">
|
|
195
|
+
<Badge variant="default">Default</Badge>
|
|
196
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
197
|
+
<Badge variant="outline">Outline</Badge>
|
|
198
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
199
|
+
<Badge variant="success">Success</Badge>
|
|
200
|
+
<Badge variant="warning">Warning</Badge>
|
|
201
|
+
</div>
|
|
202
|
+
</Surface>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* On secondary surface */}
|
|
206
|
+
<div className="space-y-3">
|
|
207
|
+
<h3 className="text-sm font-medium text-muted-foreground">On Secondary Surface</h3>
|
|
208
|
+
<Surface variant="secondary" className="p-4 rounded-lg">
|
|
209
|
+
<div className="flex flex-wrap gap-2">
|
|
210
|
+
<Badge variant="default">Default</Badge>
|
|
211
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
212
|
+
<Badge variant="outline">Outline</Badge>
|
|
213
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
214
|
+
<Badge variant="success">Success</Badge>
|
|
215
|
+
<Badge variant="warning">Warning</Badge>
|
|
216
|
+
</div>
|
|
217
|
+
</Surface>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
),
|
|
221
|
+
};
|