@snowcone-app/ui 0.1.42 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +18 -4
  3. package/package.json +9 -5
  4. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  5. package/src/components/LoadingOverlayPrism.tsx +251 -0
  6. package/src/composed/AddToCart.tsx +229 -0
  7. package/src/composed/ArtAlignment.tsx +703 -0
  8. package/src/composed/ArtSelector.tsx +290 -0
  9. package/src/composed/ArtworkCustomizer.tsx +212 -0
  10. package/src/composed/CanvasEditor.tsx +79 -0
  11. package/src/composed/ColorPicker.tsx +111 -0
  12. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  13. package/src/composed/HeroProductImage.tsx +1071 -0
  14. package/src/composed/Lightbox.index.ts +2 -0
  15. package/src/composed/Lightbox.tsx +230 -0
  16. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  17. package/src/composed/PlacementTabs.tsx +179 -0
  18. package/src/composed/ProductCard.tsx +298 -0
  19. package/src/composed/ProductGallery.tsx +54 -0
  20. package/src/composed/ProductImage.tsx +129 -0
  21. package/src/composed/ProductList.tsx +147 -0
  22. package/src/composed/ProductOptions.tsx +305 -0
  23. package/src/composed/RealtimeMockup.tsx +121 -0
  24. package/src/composed/TileCount.tsx +348 -0
  25. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  26. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  27. package/src/composed/carousels/index.ts +11 -0
  28. package/src/composed/carousels/types.ts +58 -0
  29. package/src/composed/grids/MasonryGrid.tsx +238 -0
  30. package/src/composed/grids/index.ts +9 -0
  31. package/src/composed/search/CurrentRefinements.tsx +80 -0
  32. package/src/composed/search/Filters.tsx +49 -0
  33. package/src/composed/search/FiltersButton.tsx +57 -0
  34. package/src/composed/search/FiltersDrawer.tsx +375 -0
  35. package/src/composed/search/ProductGrid.tsx +118 -0
  36. package/src/composed/search/ProductHit.tsx +56 -0
  37. package/src/composed/search/SearchBox.tsx +109 -0
  38. package/src/composed/search/SearchProvider.tsx +136 -0
  39. package/src/composed/search/facetConfig.ts +16 -0
  40. package/src/composed/search/index.ts +22 -0
  41. package/src/composed/search/meilisearchAdapter.ts +20 -0
  42. package/src/composed/search/types.ts +22 -0
  43. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  44. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  45. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  46. package/src/composed/zoom/index.ts +12 -0
  47. package/src/composed/zoom/types.ts +12 -0
  48. package/src/design-system/ColorPalette.tsx +126 -0
  49. package/src/design-system/ColorSwatch.tsx +49 -0
  50. package/src/design-system/DesignSystemPage.tsx +130 -0
  51. package/src/design-system/ThemeSwitcher.tsx +181 -0
  52. package/src/design-system/TypographyScale.tsx +106 -0
  53. package/src/design-system/index.ts +5 -0
  54. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  55. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  56. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  57. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  58. package/src/hooks/useBrand.ts +41 -0
  59. package/src/hooks/useCanvasContext.ts +127 -0
  60. package/src/hooks/useDeviceDetection.ts +64 -0
  61. package/src/hooks/useFocusTrap.ts +70 -0
  62. package/src/hooks/useImagePreloader.ts +268 -0
  63. package/src/hooks/useImageTransition.ts +608 -0
  64. package/src/hooks/usePlacementsProcessor.ts +74 -0
  65. package/src/hooks/useProductGallery.ts +193 -0
  66. package/src/hooks/useProductPage.ts +467 -0
  67. package/src/hooks/useRenderGuard.ts +96 -0
  68. package/src/hooks/useScrollDirection.ts +196 -0
  69. package/src/hooks/viewport/index.ts +25 -0
  70. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  71. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  72. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  73. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  74. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  75. package/src/hooks/visibility/index.ts +15 -0
  76. package/src/hooks/visibility/observerPool.ts +150 -0
  77. package/src/index.ts +240 -0
  78. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  79. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  80. package/src/layouts/hero-zoom/index.ts +30 -0
  81. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  82. package/src/layouts/hero-zoom/types.ts +113 -0
  83. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  84. package/src/layouts/index.ts +9 -0
  85. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  86. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  87. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  88. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  89. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  90. package/src/layouts/pdp/index.ts +40 -0
  91. package/src/lib/env.ts +15 -0
  92. package/src/lib/locale.ts +167 -0
  93. package/src/lib/router.tsx +46 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/lightbox/README.md +77 -0
  96. package/src/next/index.tsx +26 -0
  97. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  98. package/src/patterns/Product.tsx +850 -0
  99. package/src/patterns/ProductPageProvider.tsx +224 -0
  100. package/src/patterns/RealtimeProvider.tsx +1162 -0
  101. package/src/patterns/ShopProvider.tsx +603 -0
  102. package/src/personalization/PersonalizationBridge.tsx +235 -0
  103. package/src/personalization/PersonalizationContext.ts +29 -0
  104. package/src/personalization/PersonalizationInputs.tsx +110 -0
  105. package/src/personalization/PersonalizationProvider.tsx +407 -0
  106. package/src/personalization/canvas-stub.d.ts +22 -0
  107. package/src/personalization/index.ts +43 -0
  108. package/src/personalization/types.ts +48 -0
  109. package/src/personalization/usePersonalization.ts +32 -0
  110. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  111. package/src/personalization/utils.ts +59 -0
  112. package/src/primitives/BrandLogo.tsx +65 -0
  113. package/src/primitives/BrandName.tsx +51 -0
  114. package/src/primitives/Button.tsx +123 -0
  115. package/src/primitives/ColorSwatch.tsx +221 -0
  116. package/src/primitives/DragHintAnimation.tsx +190 -0
  117. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  118. package/src/primitives/FloatingActionGroup.tsx +176 -0
  119. package/src/primitives/ProductPrice.tsx +171 -0
  120. package/src/primitives/ProgressiveBlur.tsx +295 -0
  121. package/src/primitives/ThemeToggle.tsx +125 -0
  122. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  123. package/src/primitives/accordion.tsx +280 -0
  124. package/src/primitives/badge.tsx +137 -0
  125. package/src/primitives/card.tsx +61 -0
  126. package/src/primitives/checkbox.tsx +56 -0
  127. package/src/primitives/collapsible.tsx +51 -0
  128. package/src/primitives/drawer.tsx +828 -0
  129. package/src/primitives/dropdown-menu.tsx +197 -0
  130. package/src/primitives/fieldset.tsx +73 -0
  131. package/src/primitives/index.ts +138 -0
  132. package/src/primitives/input.tsx +91 -0
  133. package/src/primitives/kbd.tsx +130 -0
  134. package/src/primitives/label.tsx +20 -0
  135. package/src/primitives/link.tsx +182 -0
  136. package/src/primitives/popover.tsx +80 -0
  137. package/src/primitives/radio-group.tsx +79 -0
  138. package/src/primitives/scroll-fade.tsx +159 -0
  139. package/src/primitives/select.tsx +170 -0
  140. package/src/primitives/separator.tsx +25 -0
  141. package/src/primitives/slider.tsx +221 -0
  142. package/src/primitives/spinner.tsx +72 -0
  143. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  144. package/src/primitives/stories/Badge.stories.tsx +221 -0
  145. package/src/primitives/stories/Button.stories.tsx +185 -0
  146. package/src/primitives/stories/Card.stories.tsx +171 -0
  147. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  148. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  149. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  150. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  151. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  152. package/src/primitives/stories/Input.stories.tsx +172 -0
  153. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  154. package/src/primitives/stories/Label.stories.tsx +98 -0
  155. package/src/primitives/stories/Link.stories.tsx +260 -0
  156. package/src/primitives/stories/Popover.stories.tsx +178 -0
  157. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  158. package/src/primitives/stories/Select.stories.tsx +222 -0
  159. package/src/primitives/stories/Separator.stories.tsx +134 -0
  160. package/src/primitives/stories/Slider.stories.tsx +203 -0
  161. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  162. package/src/primitives/stories/Surface.stories.tsx +257 -0
  163. package/src/primitives/stories/Switch.stories.tsx +131 -0
  164. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  165. package/src/primitives/stories/TextField.stories.tsx +139 -0
  166. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  167. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  168. package/src/primitives/surface.tsx +86 -0
  169. package/src/primitives/switch.tsx +35 -0
  170. package/src/primitives/tabs.tsx +206 -0
  171. package/src/primitives/text-field.tsx +84 -0
  172. package/src/primitives/textarea.tsx +50 -0
  173. package/src/primitives/tooltip.tsx +58 -0
  174. package/src/services/CanvasExportService.ts +518 -0
  175. package/src/styles/base.css +380 -0
  176. package/src/styles/defaults.css +280 -0
  177. package/src/styles/globals.css +1242 -0
  178. package/src/styles/index.css +17 -0
  179. package/src/styles/ne-themes.css +4740 -0
  180. package/src/styles/tailwind.css +11 -0
  181. package/src/styles/tokens.css +117 -0
  182. package/src/styles/utilities.css +188 -0
  183. package/src/themes/apply-theme.ts +449 -0
  184. package/src/themes/getThemeStyles.ts +454 -0
  185. package/src/themes/index.ts +48 -0
  186. package/src/themes/oklch-theme.ts +283 -0
  187. package/src/themes/presets.ts +989 -0
  188. package/src/themes/types.ts +386 -0
  189. package/src/themes/useTheme.tsx +450 -0
  190. package/src/utils/dev-warnings.ts +161 -0
  191. package/src/utils/devWarnings.ts +153 -0
  192. package/dist/styles.css +0 -1
@@ -0,0 +1,280 @@
1
+ "use client";
2
+
3
+ 'use client';
4
+
5
+ import * as React from 'react';
6
+ import { Icon } from '@iconify/react';
7
+ import { cn } from '../lib/utils';
8
+
9
+ /**
10
+ * CSS Grid-based Accordion
11
+ *
12
+ * Uses CSS `grid-template-rows` animation instead of JavaScript height measurement.
13
+ * This avoids forced reflows that cause 300-400ms delays on page load.
14
+ *
15
+ * Benefits:
16
+ * - No forced reflow/layout thrashing
17
+ * - Smooth height animation
18
+ * - Fully accessible (keyboard nav, ARIA)
19
+ * - Same API as Radix accordion
20
+ *
21
+ * @see https://developer.chrome.com/docs/performance/insights/forced-reflow
22
+ */
23
+
24
+ type AccordionContextValue = {
25
+ value: string[];
26
+ onValueChange: (value: string[]) => void;
27
+ type: 'single' | 'multiple';
28
+ collapsible: boolean;
29
+ };
30
+
31
+ const AccordionContext = React.createContext<AccordionContextValue | null>(null);
32
+
33
+ function useAccordion() {
34
+ const context = React.useContext(AccordionContext);
35
+ if (!context) {
36
+ throw new Error('Accordion components must be used within an Accordion');
37
+ }
38
+ return context;
39
+ }
40
+
41
+ type AccordionItemContextValue = {
42
+ value: string;
43
+ isOpen: boolean;
44
+ triggerId: string;
45
+ contentId: string;
46
+ };
47
+
48
+ const AccordionItemContext = React.createContext<AccordionItemContextValue | null>(null);
49
+
50
+ function useAccordionItem() {
51
+ const context = React.useContext(AccordionItemContext);
52
+ if (!context) {
53
+ throw new Error('AccordionItem components must be used within an AccordionItem');
54
+ }
55
+ return context;
56
+ }
57
+
58
+ // ============================================================================
59
+ // Accordion Root
60
+ // ============================================================================
61
+
62
+ interface AccordionSingleProps {
63
+ type: 'single';
64
+ value?: string;
65
+ defaultValue?: string;
66
+ onValueChange?: (value: string) => void;
67
+ collapsible?: boolean;
68
+ children: React.ReactNode;
69
+ className?: string;
70
+ }
71
+
72
+ interface AccordionMultipleProps {
73
+ type?: 'multiple';
74
+ value?: string[];
75
+ defaultValue?: string[];
76
+ onValueChange?: (value: string[]) => void;
77
+ children: React.ReactNode;
78
+ className?: string;
79
+ }
80
+
81
+ type AccordionProps = AccordionSingleProps | AccordionMultipleProps;
82
+
83
+ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>((props, ref) => {
84
+ const { type = 'multiple', children, className, ...rest } = props;
85
+
86
+ // Handle single vs multiple value state
87
+ const [internalValue, setInternalValue] = React.useState<string[]>(() => {
88
+ if (type === 'single') {
89
+ const singleProps = props as AccordionSingleProps;
90
+ return singleProps.defaultValue ? [singleProps.defaultValue] : [];
91
+ } else {
92
+ const multiProps = props as AccordionMultipleProps;
93
+ return multiProps.defaultValue || [];
94
+ }
95
+ });
96
+
97
+ const value = React.useMemo(() => {
98
+ if (type === 'single') {
99
+ const singleProps = props as AccordionSingleProps;
100
+ return singleProps.value !== undefined ? [singleProps.value] : internalValue;
101
+ } else {
102
+ const multiProps = props as AccordionMultipleProps;
103
+ return multiProps.value !== undefined ? multiProps.value : internalValue;
104
+ }
105
+ }, [type, props, internalValue]);
106
+
107
+ const onValueChange = React.useCallback(
108
+ (newValue: string[]) => {
109
+ setInternalValue(newValue);
110
+ if (type === 'single') {
111
+ const singleProps = props as AccordionSingleProps;
112
+ singleProps.onValueChange?.(newValue[0] || '');
113
+ } else {
114
+ const multiProps = props as AccordionMultipleProps;
115
+ multiProps.onValueChange?.(newValue);
116
+ }
117
+ },
118
+ [type, props]
119
+ );
120
+
121
+ const collapsible = type === 'single' ? ((props as AccordionSingleProps).collapsible ?? false) : true;
122
+
123
+ const contextValue = React.useMemo<AccordionContextValue>(
124
+ () => ({
125
+ value,
126
+ onValueChange,
127
+ type,
128
+ collapsible,
129
+ }),
130
+ [value, onValueChange, type, collapsible]
131
+ );
132
+
133
+ return (
134
+ <AccordionContext.Provider value={contextValue}>
135
+ <div ref={ref} className={className} data-accordion="">
136
+ {children}
137
+ </div>
138
+ </AccordionContext.Provider>
139
+ );
140
+ });
141
+ Accordion.displayName = 'Accordion';
142
+
143
+ // ============================================================================
144
+ // Accordion Item
145
+ // ============================================================================
146
+
147
+ interface AccordionItemProps {
148
+ value: string;
149
+ children: React.ReactNode;
150
+ className?: string;
151
+ disabled?: boolean;
152
+ }
153
+
154
+ const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
155
+ ({ value, children, className, disabled }, ref) => {
156
+ const accordion = useAccordion();
157
+ const isOpen = accordion.value.includes(value);
158
+
159
+ const triggerId = React.useId();
160
+ const contentId = React.useId();
161
+
162
+ const contextValue = React.useMemo<AccordionItemContextValue>(
163
+ () => ({
164
+ value,
165
+ isOpen,
166
+ triggerId,
167
+ contentId,
168
+ }),
169
+ [value, isOpen, triggerId, contentId]
170
+ );
171
+
172
+ return (
173
+ <AccordionItemContext.Provider value={contextValue}>
174
+ <div
175
+ ref={ref}
176
+ className={cn('border-b border-divider', className)}
177
+ data-state={isOpen ? 'open' : 'closed'}
178
+ data-disabled={disabled || undefined}
179
+ >
180
+ {children}
181
+ </div>
182
+ </AccordionItemContext.Provider>
183
+ );
184
+ }
185
+ );
186
+ AccordionItem.displayName = 'AccordionItem';
187
+
188
+ // ============================================================================
189
+ // Accordion Trigger
190
+ // ============================================================================
191
+
192
+ interface AccordionTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
193
+ children: React.ReactNode;
194
+ }
195
+
196
+ const AccordionTrigger = React.forwardRef<HTMLButtonElement, AccordionTriggerProps>(
197
+ ({ className, children, ...props }, ref) => {
198
+ const accordion = useAccordion();
199
+ const item = useAccordionItem();
200
+
201
+ const handleClick = () => {
202
+ if (item.isOpen) {
203
+ // Closing
204
+ if (accordion.type === 'single' && !accordion.collapsible) {
205
+ // Can't close in single non-collapsible mode
206
+ return;
207
+ }
208
+ accordion.onValueChange(accordion.value.filter((v) => v !== item.value));
209
+ } else {
210
+ // Opening
211
+ if (accordion.type === 'single') {
212
+ accordion.onValueChange([item.value]);
213
+ } else {
214
+ accordion.onValueChange([...accordion.value, item.value]);
215
+ }
216
+ }
217
+ };
218
+
219
+ return (
220
+ <h3 className="flex">
221
+ <button
222
+ ref={ref}
223
+ type="button"
224
+ id={item.triggerId}
225
+ aria-expanded={item.isOpen}
226
+ aria-controls={item.contentId}
227
+ data-state={item.isOpen ? 'open' : 'closed'}
228
+ onClick={handleClick}
229
+ className={cn(
230
+ 'flex flex-1 items-center justify-between py-4 transition-all hover:underline [&[data-state=open]>svg]:rotate-180 font-label',
231
+ className
232
+ )}
233
+ {...props}
234
+ >
235
+ {children}
236
+ <Icon icon="gravity-ui:chevron-down" className="size-4 shrink-0 transition-transform duration-200" />
237
+ </button>
238
+ </h3>
239
+ );
240
+ }
241
+ );
242
+ AccordionTrigger.displayName = 'AccordionTrigger';
243
+
244
+ // ============================================================================
245
+ // Accordion Content
246
+ // ============================================================================
247
+
248
+ interface AccordionContentProps {
249
+ children: React.ReactNode;
250
+ className?: string;
251
+ }
252
+
253
+ const AccordionContent = React.forwardRef<HTMLDivElement, AccordionContentProps>(
254
+ ({ className, children }, ref) => {
255
+ const item = useAccordionItem();
256
+
257
+ return (
258
+ <div
259
+ ref={ref}
260
+ id={item.contentId}
261
+ role="region"
262
+ aria-labelledby={item.triggerId}
263
+ data-state={item.isOpen ? 'open' : 'closed'}
264
+ // CSS Grid animation - no JS height measurement needed!
265
+ className={cn(
266
+ 'grid transition-[grid-template-rows] duration-300 ease-out',
267
+ item.isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
268
+ )}
269
+ >
270
+ {/* Inner wrapper must have overflow-hidden and min-h-0 for the grid animation to work */}
271
+ <div className="overflow-hidden min-h-0">
272
+ <div className={cn('pb-4 pt-0 text-sm font-body', className)}>{children}</div>
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+ );
278
+ AccordionContent.displayName = 'AccordionContent';
279
+
280
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,137 @@
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
+ import { useSurface } from './surface';
7
+
8
+ const badgeVariants = cva(
9
+ 'inline-flex items-center rounded-badge border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 font-caption',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: 'border-transparent bg-primary text-primary-foreground',
14
+ secondary: 'border-transparent bg-default text-default-foreground',
15
+ destructive: 'border-transparent bg-danger text-danger-foreground',
16
+ success: 'border-transparent bg-success text-success-foreground',
17
+ warning: 'border-transparent bg-warning text-warning-foreground',
18
+ outline: 'border-divider text-foreground',
19
+ gradient: 'border-transparent bg-gradient-primary text-gradient-foreground',
20
+ },
21
+ size: {
22
+ default: 'px-2.5 py-0.5 text-xs',
23
+ sm: 'px-2 py-0.5 text-[10px]',
24
+ lg: 'px-3 py-1 text-sm',
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: 'default',
29
+ size: 'default',
30
+ },
31
+ }
32
+ );
33
+
34
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
35
+ // HeroUI Chip compatibility props
36
+ color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
37
+ /** Whether to show a dot indicator */
38
+ dot?: boolean;
39
+ /** Content to show on the left */
40
+ startContent?: React.ReactNode;
41
+ /** Content to show on the right */
42
+ endContent?: React.ReactNode;
43
+ /** Close button click handler (makes badge dismissible) */
44
+ onClose?: () => void;
45
+ }
46
+
47
+ function Badge({
48
+ className,
49
+ variant,
50
+ size,
51
+ color,
52
+ dot,
53
+ startContent,
54
+ endContent,
55
+ onClose,
56
+ children,
57
+ ...props
58
+ }: BadgeProps) {
59
+ const { variant: surfaceVariant } = useSurface();
60
+ const isOnDefaultSurface = surfaceVariant === 'default';
61
+ const isOnSecondarySurface = surfaceVariant === 'secondary';
62
+
63
+ // Map HeroUI color prop to variant
64
+ const effectiveVariant = color
65
+ ? (
66
+ {
67
+ default: 'secondary',
68
+ primary: 'default',
69
+ secondary: 'secondary',
70
+ success: 'success',
71
+ warning: 'warning',
72
+ danger: 'destructive',
73
+ } as const
74
+ )[color]
75
+ : variant;
76
+
77
+ // Surface-aware variant overrides
78
+ // - On default surface (white card): dark mode needs lighter bg, light mode base color is fine
79
+ // - On secondary surface: both modes need the raised color for contrast
80
+ const surfaceOverrides = {
81
+ secondary:
82
+ effectiveVariant === 'secondary' &&
83
+ (isOnSecondarySurface
84
+ ? 'bg-[var(--color-default-on-surface)] hover:bg-[var(--color-default-on-surface)]/80'
85
+ : isOnDefaultSurface
86
+ ? 'dark:bg-[var(--color-default-on-surface)] dark:hover:bg-[var(--color-default-on-surface)]/80'
87
+ : false),
88
+ outline:
89
+ effectiveVariant === 'outline' &&
90
+ (isOnDefaultSurface || isOnSecondarySurface) &&
91
+ 'border-muted-foreground/30',
92
+ };
93
+
94
+ return (
95
+ <div
96
+ className={cn(
97
+ badgeVariants({ variant: effectiveVariant, size }),
98
+ surfaceOverrides.secondary,
99
+ surfaceOverrides.outline,
100
+ className
101
+ )}
102
+ {...props}
103
+ >
104
+ {dot && <span className="mr-1.5 size-1.5 rounded-full bg-current" aria-hidden="true" />}
105
+ {startContent && <span className="mr-1">{startContent}</span>}
106
+ {children}
107
+ {endContent && <span className="ml-1">{endContent}</span>}
108
+ {onClose && (
109
+ <button
110
+ type="button"
111
+ onClick={onClose}
112
+ className="ml-1 inline-flex size-3.5 items-center justify-center rounded-full hover:bg-current/20 focus:outline-none"
113
+ aria-label="Remove"
114
+ >
115
+ <svg
116
+ className="size-3"
117
+ xmlns="http://www.w3.org/2000/svg"
118
+ viewBox="0 0 24 24"
119
+ fill="none"
120
+ stroke="currentColor"
121
+ strokeWidth="2"
122
+ strokeLinecap="round"
123
+ strokeLinejoin="round"
124
+ >
125
+ <line x1="18" y1="6" x2="6" y2="18" />
126
+ <line x1="6" y1="6" x2="18" y2="18" />
127
+ </svg>
128
+ </button>
129
+ )}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ // HeroUI Chip alias
135
+ const Chip = Badge;
136
+
137
+ export { Badge, Chip, badgeVariants };
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../lib/utils';
5
+ import { Surface } from './surface';
6
+
7
+ interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ /** Visual variant - 'outlined' adds border and shadow */
9
+ variant?: 'default' | 'outlined';
10
+ }
11
+
12
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(
13
+ ({ className, variant = 'default', ...props }, ref) => {
14
+ return (
15
+ <Surface
16
+ ref={ref}
17
+ variant="default"
18
+ className={cn(
19
+ 'rounded-card text-foreground',
20
+ variant === 'outlined' && 'border border-divider shadow-soft',
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+ );
28
+ Card.displayName = 'Card';
29
+
30
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
31
+ ({ className, ...props }, ref) => (
32
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
33
+ )
34
+ );
35
+ CardHeader.displayName = 'CardHeader';
36
+
37
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
38
+ ({ className, ...props }, ref) => (
39
+ <h3 ref={ref} className={cn('leading-none tracking-tight font-heading', className)} {...props} />
40
+ )
41
+ );
42
+ CardTitle.displayName = 'CardTitle';
43
+
44
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
45
+ ({ className, ...props }, ref) => <p ref={ref} className={cn('text-muted-foreground font-caption', className)} {...props} />
46
+ );
47
+ CardDescription.displayName = 'CardDescription';
48
+
49
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
50
+ ({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
51
+ );
52
+ CardContent.displayName = 'CardContent';
53
+
54
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
55
+ ({ className, ...props }, ref) => (
56
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
57
+ )
58
+ );
59
+ CardFooter.displayName = 'CardFooter';
60
+
61
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
5
+ import { Icon } from '@iconify/react';
6
+ import { cn } from '../lib/utils';
7
+ import { useSurface } from './surface';
8
+
9
+ interface CheckboxProps extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
10
+ /** Visual variant - auto-detected from Surface context, or can be set explicitly */
11
+ variant?: 'default' | 'filled';
12
+ }
13
+
14
+ const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, CheckboxProps>(
15
+ ({ className, checked, variant, ...props }, ref) => {
16
+ // Auto-detect variant from Surface context
17
+ const surface = useSurface();
18
+ const effectiveVariant = variant ?? (surface.variant === 'default' ? 'filled' : 'default');
19
+
20
+ return (
21
+ <CheckboxPrimitive.Root
22
+ ref={ref}
23
+ className={cn(
24
+ 'peer size-4 shrink-0 rounded transition-colors border-none',
25
+ 'focus-visible:outline-none',
26
+ 'disabled:cursor-not-allowed disabled:opacity-50',
27
+ // Variant styles - semantic: field on page vs field on surface
28
+ effectiveVariant === 'default' && 'bg-field shadow-soft dark:shadow-none',
29
+ effectiveVariant === 'filled' && 'bg-[var(--color-field-on-surface)]',
30
+ 'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:shadow-none',
31
+ 'data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:shadow-none',
32
+ className
33
+ )}
34
+ checked={checked}
35
+ {...props}
36
+ >
37
+ <CheckboxPrimitive.Indicator
38
+ className={cn(
39
+ 'flex items-center justify-center text-current',
40
+ 'data-[state=checked]:animate-in data-[state=checked]:zoom-in-0 data-[state=checked]:duration-300',
41
+ 'data-[state=indeterminate]:animate-in data-[state=indeterminate]:zoom-in-0 data-[state=indeterminate]:duration-150'
42
+ )}
43
+ >
44
+ {checked === 'indeterminate' ? (
45
+ <Icon icon="gravity-ui:minus" className="size-3" />
46
+ ) : (
47
+ <Icon icon="gravity-ui:check" className="size-3" />
48
+ )}
49
+ </CheckboxPrimitive.Indicator>
50
+ </CheckboxPrimitive.Root>
51
+ );
52
+ }
53
+ );
54
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
55
+
56
+ export { Checkbox };
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
5
+ import { cn } from '../lib/utils';
6
+
7
+ const CollapsibleRoot = React.forwardRef<
8
+ React.ElementRef<typeof CollapsiblePrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CollapsiblePrimitive.Root
12
+ ref={ref}
13
+ className={cn(className)}
14
+ {...props}
15
+ />
16
+ ));
17
+ CollapsibleRoot.displayName = 'Collapsible';
18
+
19
+ const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
20
+
21
+ const CollapsibleContent = React.forwardRef<
22
+ React.ElementRef<typeof CollapsiblePrimitive.Content>,
23
+ React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
24
+ >(({ className, ...props }, ref) => (
25
+ <CollapsiblePrimitive.Content
26
+ ref={ref}
27
+ className={cn(
28
+ 'overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down',
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ ));
34
+ CollapsibleContent.displayName = 'CollapsibleContent';
35
+
36
+ const Collapsible = CollapsibleRoot;
37
+
38
+ // Disclosure aliases (alternative naming)
39
+ const Disclosure = CollapsibleRoot;
40
+ const DisclosureTrigger = CollapsibleTrigger;
41
+ const DisclosureContent = CollapsibleContent;
42
+
43
+ export {
44
+ Collapsible,
45
+ CollapsibleTrigger,
46
+ CollapsibleContent,
47
+ CollapsibleRoot,
48
+ Disclosure,
49
+ DisclosureTrigger,
50
+ DisclosureContent,
51
+ };