@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,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
|
+
};
|