@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +26 -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,182 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Slot } from '@radix-ui/react-slot';
5
+ import { cva, type VariantProps } from 'class-variance-authority';
6
+ import { cn } from '../lib/utils';
7
+
8
+ const linkVariants = cva(
9
+ [
10
+ 'inline-flex items-center gap-1 font-medium transition-colors',
11
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2',
12
+ 'disabled:pointer-events-none disabled:opacity-50',
13
+ 'aria-disabled:pointer-events-none aria-disabled:opacity-50',
14
+ ],
15
+ {
16
+ variants: {
17
+ color: {
18
+ default: 'text-foreground hover:text-foreground/80',
19
+ primary: 'text-primary hover:text-primary/80',
20
+ secondary: 'text-secondary hover:text-secondary/80',
21
+ success: 'text-success hover:text-success/80',
22
+ warning: 'text-warning hover:text-warning/80',
23
+ danger: 'text-danger hover:text-danger/80',
24
+ },
25
+ size: {
26
+ sm: 'text-sm',
27
+ md: 'text-base',
28
+ lg: 'text-lg',
29
+ },
30
+ underline: {
31
+ none: '',
32
+ hover: [
33
+ 'relative',
34
+ 'after:absolute after:bottom-0 after:left-0 after:h-[1px] after:w-full',
35
+ 'after:origin-bottom-right after:scale-x-0',
36
+ 'after:bg-current after:transition-transform after:duration-200',
37
+ 'hover:after:origin-bottom-left hover:after:scale-x-100',
38
+ ],
39
+ always: [
40
+ 'relative',
41
+ 'after:absolute after:bottom-0 after:left-0 after:h-[1px] after:w-full',
42
+ 'after:bg-current after:opacity-50',
43
+ 'hover:after:opacity-100',
44
+ ],
45
+ },
46
+ underlineOffset: {
47
+ 1: 'after:bottom-0',
48
+ 2: 'after:-bottom-0.5',
49
+ 3: 'after:-bottom-1',
50
+ },
51
+ },
52
+ defaultVariants: {
53
+ color: 'primary',
54
+ size: 'md',
55
+ underline: 'hover',
56
+ underlineOffset: 1,
57
+ },
58
+ }
59
+ );
60
+
61
+ export interface LinkProps
62
+ extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'color'>,
63
+ VariantProps<typeof linkVariants> {
64
+ /**
65
+ * If true, the link will be rendered as its child element (using Radix Slot).
66
+ * Useful for integrating with routing libraries like Next.js Link.
67
+ */
68
+ asChild?: boolean;
69
+ /**
70
+ * If true, adds an external link icon.
71
+ */
72
+ isExternal?: boolean;
73
+ /**
74
+ * If true, shows a block-style link (full width with padding).
75
+ */
76
+ isBlock?: boolean;
77
+ }
78
+
79
+ /**
80
+ * Link - A styled anchor component for navigation.
81
+ *
82
+ * Supports animated underlines, external link indicators, and integration
83
+ * with routing libraries via the asChild prop.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * <Link href="/about">About</Link>
88
+ * <Link href="https://example.com" isExternal>External Link</Link>
89
+ * <Link asChild underline="hover">
90
+ * <NextLink href="/dashboard">Dashboard</NextLink>
91
+ * </Link>
92
+ * ```
93
+ */
94
+ const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
95
+ (
96
+ {
97
+ className,
98
+ color,
99
+ size,
100
+ underline,
101
+ underlineOffset,
102
+ asChild = false,
103
+ isExternal = false,
104
+ isBlock = false,
105
+ children,
106
+ target,
107
+ rel,
108
+ ...props
109
+ },
110
+ ref
111
+ ) => {
112
+ const Comp = asChild ? Slot : 'a';
113
+
114
+ // Handle external links
115
+ const externalProps = isExternal
116
+ ? {
117
+ target: target ?? '_blank',
118
+ rel: rel ?? 'noopener noreferrer',
119
+ }
120
+ : { target, rel };
121
+
122
+ return (
123
+ <Comp
124
+ ref={ref}
125
+ className={cn(
126
+ linkVariants({ color, size, underline, underlineOffset }),
127
+ isBlock && 'w-full justify-start rounded-md px-3 py-2 hover:bg-muted',
128
+ className
129
+ )}
130
+ {...externalProps}
131
+ {...props}
132
+ >
133
+ {children}
134
+ {isExternal && <LinkIcon />}
135
+ </Comp>
136
+ );
137
+ }
138
+ );
139
+ Link.displayName = 'Link';
140
+
141
+ /**
142
+ * LinkIcon - External link indicator icon.
143
+ *
144
+ * Used automatically when isExternal is true, or can be used manually.
145
+ */
146
+ interface LinkIconProps extends React.SVGAttributes<SVGElement> {
147
+ /**
148
+ * If true, renders as child element using Slot.
149
+ */
150
+ asChild?: boolean;
151
+ }
152
+
153
+ const LinkIcon = React.forwardRef<SVGSVGElement, LinkIconProps>(
154
+ ({ className, asChild = false, children, ...props }, ref) => {
155
+ if (asChild && children) {
156
+ return <Slot className={cn('size-3.5 shrink-0', className)}>{children}</Slot>;
157
+ }
158
+
159
+ return (
160
+ <svg
161
+ ref={ref}
162
+ className={cn('size-3.5 shrink-0', className)}
163
+ xmlns="http://www.w3.org/2000/svg"
164
+ viewBox="0 0 24 24"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ strokeWidth="2"
168
+ strokeLinecap="round"
169
+ strokeLinejoin="round"
170
+ aria-hidden="true"
171
+ {...props}
172
+ >
173
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
174
+ <polyline points="15 3 21 3 21 9" />
175
+ <line x1="10" y1="14" x2="21" y2="3" />
176
+ </svg>
177
+ );
178
+ }
179
+ );
180
+ LinkIcon.displayName = 'LinkIcon';
181
+
182
+ export { Link, LinkIcon, linkVariants };
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
5
+ import { cn } from '../lib/utils';
6
+ import { SurfaceContext } from './surface';
7
+
8
+ const PopoverRoot = PopoverPrimitive.Root;
9
+
10
+ const PopoverTrigger = PopoverPrimitive.Trigger;
11
+
12
+ const PopoverAnchor = PopoverPrimitive.Anchor;
13
+
14
+ interface PopoverContentProps extends React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> {
15
+ /** Preserve text selection when popover opens (useful for canvas/editor contexts) */
16
+ preserveSelection?: boolean;
17
+ /** Use maximum z-index to appear above fullscreen modals (for canvas/editor contexts) */
18
+ maxZIndex?: boolean;
19
+ }
20
+
21
+ const PopoverContent = React.forwardRef<
22
+ React.ElementRef<typeof PopoverPrimitive.Content>,
23
+ PopoverContentProps
24
+ >(({ className, align = 'center', sideOffset = 4, preserveSelection, maxZIndex, children, style, ...props }, ref) => (
25
+ <PopoverPrimitive.Portal>
26
+ <SurfaceContext.Provider value={{ variant: 'default' }}>
27
+ <PopoverPrimitive.Content
28
+ ref={ref}
29
+ align={align}
30
+ sideOffset={sideOffset}
31
+ data-preserve-selection={preserveSelection ? '' : undefined}
32
+ className={cn(
33
+ 'z-dropdown w-72 overflow-hidden rounded-tooltip border-none bg-popover p-4 text-popover-foreground shadow-soft outline-none',
34
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
35
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
36
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
37
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
38
+ 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
39
+ className
40
+ )}
41
+ style={maxZIndex ? { ...style, zIndex: 2147483647 } : style}
42
+ {...props}
43
+ >
44
+ {children}
45
+ </PopoverPrimitive.Content>
46
+ </SurfaceContext.Provider>
47
+ </PopoverPrimitive.Portal>
48
+ ));
49
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
50
+
51
+ interface PopoverProps {
52
+ children: React.ReactNode;
53
+ open?: boolean;
54
+ onOpenChange?: (open: boolean) => void;
55
+ defaultOpen?: boolean;
56
+ modal?: boolean;
57
+ }
58
+
59
+ const PopoverBase = ({ children, open, onOpenChange, ...props }: PopoverProps) => (
60
+ <PopoverRoot open={open} onOpenChange={onOpenChange} {...props}>
61
+ {children}
62
+ </PopoverRoot>
63
+ );
64
+
65
+ // Compound component with subcomponents
66
+ const Popover = Object.assign(PopoverBase, {
67
+ Trigger: PopoverTrigger,
68
+ Content: PopoverContent,
69
+ Anchor: PopoverAnchor,
70
+ // HeroUI uses Dialog for the content wrapper - map to Content
71
+ Dialog: React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
72
+ ({ children, className, ...props }, ref) => (
73
+ <div ref={ref} className={className} {...props}>
74
+ {children}
75
+ </div>
76
+ )
77
+ ),
78
+ });
79
+
80
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverRoot };
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
5
+ import { cn } from '../lib/utils';
6
+ import { useSurface } from './surface';
7
+
8
+ const RadioGroup = React.forwardRef<
9
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />
13
+ ));
14
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
15
+
16
+ interface RadioGroupItemProps extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
17
+ /** Visual variant - auto-detected from Surface context, or can be set explicitly */
18
+ variant?: 'default' | 'filled';
19
+ }
20
+
21
+ const RadioGroupItem = React.forwardRef<
22
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
23
+ RadioGroupItemProps
24
+ >(({ className, variant, ...props }, ref) => {
25
+ // Auto-detect variant from Surface context
26
+ const surface = useSurface();
27
+ const effectiveVariant = variant ?? (surface.variant === 'default' ? 'filled' : 'default');
28
+
29
+ return (
30
+ <RadioGroupPrimitive.Item
31
+ ref={ref}
32
+ className={cn(
33
+ 'aspect-square size-4 rounded-full border-none transition-colors',
34
+ 'focus-visible:outline-none',
35
+ 'disabled:cursor-not-allowed disabled:opacity-50',
36
+ // Variant styles - semantic: field on page vs field on surface
37
+ effectiveVariant === 'default' && 'bg-field shadow-soft dark:shadow-none',
38
+ effectiveVariant === 'filled' && 'bg-[var(--color-field-on-surface)]',
39
+ 'data-[state=checked]:bg-primary data-[state=checked]:shadow-none',
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
45
+ <div className="size-1.5 rounded-full bg-primary-foreground" />
46
+ </RadioGroupPrimitive.Indicator>
47
+ </RadioGroupPrimitive.Item>
48
+ );
49
+ });
50
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
51
+
52
+ // Radio component - wraps RadioGroupItem with label pattern
53
+ interface RadioProps extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
54
+ children?: React.ReactNode;
55
+ /** Visual variant - auto-detected from Surface context, or can be set explicitly */
56
+ variant?: 'default' | 'filled';
57
+ }
58
+
59
+ const Radio = React.forwardRef<React.ElementRef<typeof RadioGroupPrimitive.Item>, RadioProps>(
60
+ ({ children, className, variant, ...props }, ref) => {
61
+ if (children) {
62
+ return (
63
+ <div className="flex items-center space-x-2">
64
+ <RadioGroupItem ref={ref} variant={variant} {...props} />
65
+ <label
66
+ htmlFor={props.id}
67
+ className="text-sm leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70 font-label"
68
+ >
69
+ {children}
70
+ </label>
71
+ </div>
72
+ );
73
+ }
74
+ return <RadioGroupItem ref={ref} className={className} variant={variant} {...props} />;
75
+ }
76
+ );
77
+ Radio.displayName = 'Radio';
78
+
79
+ export { RadioGroup, RadioGroupItem, Radio };
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ /**
7
+ * ScrollFade Component
8
+ *
9
+ * A container that shows a gradient fade at the bottom when content is scrollable,
10
+ * indicating there's more content below. The gradient fades out when the user
11
+ * scrolls to the bottom.
12
+ *
13
+ * @example Basic usage
14
+ * <ScrollFade className="h-[400px]">
15
+ * <div className="space-y-4">
16
+ * {items.map(item => <Card key={item.id}>{item.content}</Card>)}
17
+ * </div>
18
+ * </ScrollFade>
19
+ *
20
+ * @example With custom fade color
21
+ * <ScrollFade fadeColor="rgb(0, 0, 0)" className="h-full bg-black">
22
+ * <DarkModeContent />
23
+ * </ScrollFade>
24
+ *
25
+ * @example Disabled fade
26
+ * <ScrollFade showFade={false}>
27
+ * <Content />
28
+ * </ScrollFade>
29
+ */
30
+
31
+ interface ScrollFadeProps extends React.HTMLAttributes<HTMLDivElement> {
32
+ /** Whether to show the gradient fade. Default: true */
33
+ showFade?: boolean;
34
+ /** Height of the gradient fade in pixels or CSS value. Default: 80 */
35
+ fadeHeight?: number | string;
36
+ /** Color for the gradient fade. Should match the container background. Default: uses CSS variable */
37
+ fadeColor?: string;
38
+ /** Additional class for the scroll container */
39
+ scrollClassName?: string;
40
+ /** Threshold in pixels from bottom to consider "at bottom". Default: 20 */
41
+ bottomThreshold?: number;
42
+ /** Callback when scroll position changes */
43
+ onScrollChange?: (info: { isAtBottom: boolean; isAtTop: boolean; scrollTop: number }) => void;
44
+ }
45
+
46
+ const ScrollFade = React.forwardRef<HTMLDivElement, ScrollFadeProps>(
47
+ (
48
+ {
49
+ className,
50
+ children,
51
+ showFade = true,
52
+ fadeHeight = 80,
53
+ fadeColor,
54
+ scrollClassName,
55
+ bottomThreshold = 20,
56
+ onScrollChange,
57
+ ...props
58
+ },
59
+ ref
60
+ ) => {
61
+ const [isAtBottom, setIsAtBottom] = React.useState(false);
62
+ const [isAtTop, setIsAtTop] = React.useState(true);
63
+ // Default to true - will be corrected after layout check
64
+ const [hasOverflow, setHasOverflow] = React.useState(true);
65
+ const scrollRef = React.useRef<HTMLDivElement>(null);
66
+
67
+ // Check if content overflows on mount and resize
68
+ React.useEffect(() => {
69
+ const checkOverflow = () => {
70
+ if (scrollRef.current) {
71
+ const hasScroll = scrollRef.current.scrollHeight > scrollRef.current.clientHeight;
72
+ setHasOverflow(hasScroll);
73
+ // If no overflow, we're at bottom
74
+ if (!hasScroll) {
75
+ setIsAtBottom(true);
76
+ } else {
77
+ // Check initial scroll position
78
+ const atBottom = scrollRef.current.scrollHeight - scrollRef.current.scrollTop - scrollRef.current.clientHeight < bottomThreshold;
79
+ setIsAtBottom(atBottom);
80
+ }
81
+ }
82
+ };
83
+
84
+ // Initial check after a short delay to ensure layout is complete
85
+ checkOverflow();
86
+ const timeoutId = setTimeout(checkOverflow, 100);
87
+
88
+ // Re-check on resize
89
+ const resizeObserver = new ResizeObserver(checkOverflow);
90
+ if (scrollRef.current) {
91
+ resizeObserver.observe(scrollRef.current);
92
+ }
93
+
94
+ return () => {
95
+ clearTimeout(timeoutId);
96
+ resizeObserver.disconnect();
97
+ };
98
+ }, [children, bottomThreshold]);
99
+
100
+ const handleScroll = React.useCallback(
101
+ (e: React.UIEvent<HTMLDivElement>) => {
102
+ const target = e.target as HTMLDivElement;
103
+ const scrollTop = target.scrollTop;
104
+ const atBottom =
105
+ target.scrollHeight - scrollTop - target.clientHeight < bottomThreshold;
106
+ const atTop = scrollTop < bottomThreshold;
107
+
108
+ setIsAtBottom(atBottom);
109
+ setIsAtTop(atTop);
110
+ onScrollChange?.({ isAtBottom: atBottom, isAtTop: atTop, scrollTop });
111
+ },
112
+ [bottomThreshold, onScrollChange]
113
+ );
114
+
115
+ const fadeHeightValue = typeof fadeHeight === "number" ? `${fadeHeight}px` : fadeHeight;
116
+
117
+ return (
118
+ <div
119
+ ref={ref}
120
+ className={cn("relative overflow-hidden min-h-0", className)}
121
+ {...props}
122
+ >
123
+ <div
124
+ ref={scrollRef}
125
+ className={cn("absolute inset-0 overflow-y-auto", scrollClassName)}
126
+ style={{
127
+ WebkitOverflowScrolling: "touch",
128
+ overscrollBehavior: "contain",
129
+ }}
130
+ onScroll={handleScroll}
131
+ >
132
+ {children}
133
+ </div>
134
+
135
+ {/* Gradient fade - hides when scrolled to bottom or no overflow */}
136
+ {showFade && (
137
+ <div
138
+ className={cn(
139
+ "absolute bottom-0 left-0 right-0 pointer-events-none transition-opacity duration-300 z-10",
140
+ isAtBottom || !hasOverflow ? "opacity-0" : "opacity-100",
141
+ !fadeColor && "bg-gradient-to-t from-background to-transparent"
142
+ )}
143
+ style={{
144
+ height: fadeHeightValue,
145
+ ...(fadeColor && {
146
+ background: `linear-gradient(to top, ${fadeColor}, transparent)`,
147
+ }),
148
+ }}
149
+ aria-hidden="true"
150
+ />
151
+ )}
152
+ </div>
153
+ );
154
+ }
155
+ );
156
+ ScrollFade.displayName = "ScrollFade";
157
+
158
+ export { ScrollFade };
159
+ export type { ScrollFadeProps };
@@ -0,0 +1,170 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SelectPrimitive from '@radix-ui/react-select';
5
+ import { Icon } from '@iconify/react';
6
+ import { cn } from '../lib/utils';
7
+ import { useSurface } from './surface';
8
+
9
+ const Select = SelectPrimitive.Root;
10
+
11
+ const SelectGroup = SelectPrimitive.Group;
12
+
13
+ const SelectValue = SelectPrimitive.Value;
14
+
15
+ interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
16
+ /** Visual variant - auto-detected from Surface context, or can be set explicitly */
17
+ variant?: 'default' | 'filled';
18
+ }
19
+
20
+ const SelectTrigger = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Trigger>, SelectTriggerProps>(
21
+ ({ className, children, variant, ...props }, ref) => {
22
+ // Auto-detect variant from Surface context
23
+ const surface = useSurface();
24
+ const effectiveVariant = variant ?? (surface.variant === 'default' ? 'filled' : 'default');
25
+
26
+ return (
27
+ <SelectPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ 'flex h-9 w-full items-center justify-between gap-2 rounded-input px-2.5 py-1.5 text-sm transition-colors',
31
+ 'text-foreground border-none',
32
+ 'focus:ring-focus focus:ring-2 focus:outline-none',
33
+ 'disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
34
+ // Placeholder styling (Radix adds data-placeholder attribute to trigger)
35
+ 'data-[placeholder]:text-foreground/50',
36
+ // Variant styles - semantic: field on page vs field on surface
37
+ effectiveVariant === 'default' && 'bg-field shadow-soft dark:shadow-none',
38
+ effectiveVariant === 'filled' && 'bg-[var(--color-field-on-surface)]',
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <SelectPrimitive.Icon asChild>
45
+ <Icon
46
+ icon="gravity-ui:chevron-down"
47
+ className="text-foreground/40 size-4 transition-transform duration-200 ease-out [[data-state=open]>&]:rotate-180"
48
+ />
49
+ </SelectPrimitive.Icon>
50
+ </SelectPrimitive.Trigger>
51
+ );
52
+ }
53
+ );
54
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
55
+
56
+ const SelectScrollUpButton = React.forwardRef<
57
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
58
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
59
+ >(({ className, ...props }, ref) => (
60
+ <SelectPrimitive.ScrollUpButton
61
+ ref={ref}
62
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
63
+ {...props}
64
+ >
65
+ <Icon icon="gravity-ui:chevron-up" className="size-4" />
66
+ </SelectPrimitive.ScrollUpButton>
67
+ ));
68
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
69
+
70
+ const SelectScrollDownButton = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
73
+ >(({ className, ...props }, ref) => (
74
+ <SelectPrimitive.ScrollDownButton
75
+ ref={ref}
76
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
77
+ {...props}
78
+ >
79
+ <Icon icon="gravity-ui:chevron-down" className="size-4" />
80
+ </SelectPrimitive.ScrollDownButton>
81
+ ));
82
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
83
+
84
+ const SelectContent = React.forwardRef<
85
+ React.ElementRef<typeof SelectPrimitive.Content>,
86
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
87
+ >(({ className, children, position = 'popper', ...props }, ref) => (
88
+ <SelectPrimitive.Portal>
89
+ <SelectPrimitive.Content
90
+ ref={ref}
91
+ className={cn(
92
+ 'z-dropdown bg-popover text-popover-foreground shadow-soft relative min-w-[8rem] overflow-hidden rounded-tooltip border-none p-1',
93
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
94
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
95
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
96
+ 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
97
+ position === 'popper' &&
98
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
99
+ className
100
+ )}
101
+ position={position}
102
+ {...props}
103
+ >
104
+ <SelectScrollUpButton />
105
+ <SelectPrimitive.Viewport
106
+ className={cn(position === 'popper' && 'w-full min-w-[var(--radix-select-trigger-width)]')}
107
+ >
108
+ {children}
109
+ </SelectPrimitive.Viewport>
110
+ <SelectScrollDownButton />
111
+ </SelectPrimitive.Content>
112
+ </SelectPrimitive.Portal>
113
+ ));
114
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
115
+
116
+ const SelectLabel = React.forwardRef<
117
+ React.ElementRef<typeof SelectPrimitive.Label>,
118
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
119
+ >(({ className, ...props }, ref) => (
120
+ <SelectPrimitive.Label
121
+ ref={ref}
122
+ className={cn('text-muted-foreground px-2 py-1.5 text-xs font-label', className)}
123
+ {...props}
124
+ />
125
+ ));
126
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
127
+
128
+ const SelectItem = React.forwardRef<
129
+ React.ElementRef<typeof SelectPrimitive.Item>,
130
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
131
+ >(({ className, children, ...props }, ref) => (
132
+ <SelectPrimitive.Item
133
+ ref={ref}
134
+ className={cn(
135
+ 'relative flex w-full cursor-default items-center rounded-full py-1 pr-8 pl-2 text-sm text-foreground outline-none select-none font-body',
136
+ 'hover:bg-default/50 focus:bg-default/50 dark:hover:bg-white/5 dark:focus:bg-white/5 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
137
+ className
138
+ )}
139
+ {...props}
140
+ >
141
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
142
+ <SelectPrimitive.ItemIndicator>
143
+ <Icon icon="gravity-ui:check" className="size-4" />
144
+ </SelectPrimitive.ItemIndicator>
145
+ </span>
146
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
147
+ </SelectPrimitive.Item>
148
+ ));
149
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
150
+
151
+ const SelectSeparator = React.forwardRef<
152
+ React.ElementRef<typeof SelectPrimitive.Separator>,
153
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
154
+ >(({ className, ...props }, ref) => (
155
+ <SelectPrimitive.Separator ref={ref} className={cn('bg-divider mx-2 my-1 h-px', className)} {...props} />
156
+ ));
157
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
158
+
159
+ export {
160
+ Select,
161
+ SelectGroup,
162
+ SelectValue,
163
+ SelectTrigger,
164
+ SelectContent,
165
+ SelectLabel,
166
+ SelectItem,
167
+ SelectSeparator,
168
+ SelectScrollUpButton,
169
+ SelectScrollDownButton,
170
+ };
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SeparatorPrimitive from '@radix-ui/react-separator';
5
+ import { cn } from '../lib/utils';
6
+
7
+ const Separator = React.forwardRef<
8
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
10
+ >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
11
+ <SeparatorPrimitive.Root
12
+ ref={ref}
13
+ decorative={decorative}
14
+ orientation={orientation}
15
+ className={cn(
16
+ 'shrink-0 bg-divider',
17
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ));
23
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
24
+
25
+ export { Separator };