@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.
- package/CHANGELOG.md +33 -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,206 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
import { useSurface } from './surface';
|
|
9
|
+
|
|
10
|
+
interface TabsProps
|
|
11
|
+
extends Omit<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>, 'value' | 'onValueChange'> {
|
|
12
|
+
value?: string;
|
|
13
|
+
onValueChange?: (value: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TabsRoot = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Root>, TabsProps>(
|
|
17
|
+
({ value, onValueChange, ...props }, ref) => (
|
|
18
|
+
<TabsPrimitive.Root ref={ref} value={value} onValueChange={onValueChange} {...props} />
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
TabsRoot.displayName = 'Tabs';
|
|
22
|
+
|
|
23
|
+
const TabsList = React.forwardRef<
|
|
24
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
25
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
26
|
+
>(({ className, children, ...props }, ref) => {
|
|
27
|
+
const listRef = React.useRef<HTMLDivElement>(null);
|
|
28
|
+
const [indicatorStyle, setIndicatorStyle] = React.useState<React.CSSProperties>({
|
|
29
|
+
opacity: 0,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Update indicator position when active tab changes
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const listElement = listRef.current;
|
|
35
|
+
if (!listElement) return;
|
|
36
|
+
|
|
37
|
+
const updateIndicator = () => {
|
|
38
|
+
const activeTab = listElement.querySelector('[data-state="active"]') as HTMLElement;
|
|
39
|
+
if (activeTab) {
|
|
40
|
+
const listRect = listElement.getBoundingClientRect();
|
|
41
|
+
const tabRect = activeTab.getBoundingClientRect();
|
|
42
|
+
|
|
43
|
+
setIndicatorStyle({
|
|
44
|
+
width: tabRect.width,
|
|
45
|
+
height: tabRect.height,
|
|
46
|
+
left: tabRect.left - listRect.left,
|
|
47
|
+
opacity: 1,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Initial position
|
|
53
|
+
updateIndicator();
|
|
54
|
+
|
|
55
|
+
// Use MutationObserver to detect when active tab changes
|
|
56
|
+
const observer = new MutationObserver((mutations) => {
|
|
57
|
+
for (const mutation of mutations) {
|
|
58
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'data-state') {
|
|
59
|
+
updateIndicator();
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Observe all trigger elements for data-state changes
|
|
66
|
+
const triggers = listElement.querySelectorAll('[role="tab"]');
|
|
67
|
+
triggers.forEach((trigger) => {
|
|
68
|
+
observer.observe(trigger, { attributes: true, attributeFilter: ['data-state'] });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Also update on resize
|
|
72
|
+
const resizeObserver = new ResizeObserver(updateIndicator);
|
|
73
|
+
resizeObserver.observe(listElement);
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
observer.disconnect();
|
|
77
|
+
resizeObserver.disconnect();
|
|
78
|
+
};
|
|
79
|
+
}, [children]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<TabsPrimitive.List
|
|
83
|
+
ref={(node) => {
|
|
84
|
+
// Handle both refs
|
|
85
|
+
(listRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
86
|
+
if (typeof ref === 'function') {
|
|
87
|
+
ref(node);
|
|
88
|
+
} else if (ref) {
|
|
89
|
+
ref.current = node;
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
className={cn(
|
|
93
|
+
'relative inline-flex h-10 items-center justify-center rounded-full px-1 text-muted-foreground',
|
|
94
|
+
// Consistent bg-default matches Button secondary variant for toolbar harmony
|
|
95
|
+
'bg-default dark:bg-[var(--color-default-on-surface)]',
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{/* Animated sliding indicator. In dark mode the previous `bg-muted`
|
|
101
|
+
value resolved to the same color as `bg-default` on the container,
|
|
102
|
+
so the active tab was invisible without relying purely on
|
|
103
|
+
shadow-md. `bg-popover` lands ~0.08 lightness above `bg-default`
|
|
104
|
+
in dark mode for a clearly visible elevation. */}
|
|
105
|
+
<span
|
|
106
|
+
className="absolute top-1/2 -translate-y-1/2 rounded-full bg-surface dark:bg-popover shadow-md transition-[left,width,opacity]"
|
|
107
|
+
style={{
|
|
108
|
+
...indicatorStyle,
|
|
109
|
+
transitionDuration: '350ms',
|
|
110
|
+
transitionTimingFunction: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
111
|
+
}}
|
|
112
|
+
aria-hidden="true"
|
|
113
|
+
/>
|
|
114
|
+
{children}
|
|
115
|
+
</TabsPrimitive.List>
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
119
|
+
|
|
120
|
+
interface TabsTriggerProps extends Omit<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>, 'value'> {
|
|
121
|
+
// HeroUI uses 'id' for tab identification, Radix uses 'value'
|
|
122
|
+
// At least one must be provided
|
|
123
|
+
id?: string;
|
|
124
|
+
value?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const TabsTrigger = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Trigger>, TabsTriggerProps>(
|
|
128
|
+
({ className, id, value, ...props }, ref) => {
|
|
129
|
+
const { variant: surfaceVariant } = useSurface();
|
|
130
|
+
const isOnSurface = surfaceVariant === 'default' || surfaceVariant === 'secondary';
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<TabsPrimitive.Trigger
|
|
134
|
+
ref={ref}
|
|
135
|
+
value={id ?? value ?? ''}
|
|
136
|
+
className={cn(
|
|
137
|
+
'relative z-10 inline-flex h-8 items-center justify-center gap-2 whitespace-nowrap rounded-full px-4 text-sm font-label',
|
|
138
|
+
'transition-[color,opacity]',
|
|
139
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2',
|
|
140
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
141
|
+
'data-[state=active]:text-foreground data-[state=active]:opacity-100',
|
|
142
|
+
// Inactive tabs use element opacity (not color opacity) to avoid SVG path overlap issues
|
|
143
|
+
isOnSurface
|
|
144
|
+
? 'data-[state=inactive]:text-foreground data-[state=inactive]:opacity-30 data-[state=inactive]:hover:opacity-50'
|
|
145
|
+
: 'data-[state=inactive]:text-foreground data-[state=inactive]:opacity-50 data-[state=inactive]:hover:opacity-70',
|
|
146
|
+
className
|
|
147
|
+
)}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
154
|
+
|
|
155
|
+
interface TabsContentProps extends Omit<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>, 'value'> {
|
|
156
|
+
// HeroUI uses 'id' for panel identification, Radix uses 'value'
|
|
157
|
+
// At least one must be provided
|
|
158
|
+
id?: string;
|
|
159
|
+
value?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const TabsContent = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Content>, TabsContentProps>(
|
|
163
|
+
({ className, id, value, ...props }, ref) => (
|
|
164
|
+
<TabsPrimitive.Content
|
|
165
|
+
ref={ref}
|
|
166
|
+
value={id ?? value ?? ''}
|
|
167
|
+
className={cn(
|
|
168
|
+
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2',
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
176
|
+
|
|
177
|
+
// HeroUI ListContainer - wrapper for the tabs list (passthrough div)
|
|
178
|
+
const TabsListContainer = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
179
|
+
({ className, ...props }, ref) => <div ref={ref} className={className} {...props} />
|
|
180
|
+
);
|
|
181
|
+
TabsListContainer.displayName = 'TabsListContainer';
|
|
182
|
+
|
|
183
|
+
// HeroUI Tab - alias for TabsTrigger
|
|
184
|
+
const TabsTab = TabsTrigger;
|
|
185
|
+
|
|
186
|
+
// HeroUI Panel - alias for TabsContent
|
|
187
|
+
const TabsPanel = TabsContent;
|
|
188
|
+
|
|
189
|
+
// HeroUI Indicator - now rendered automatically by TabsList, this is a no-op
|
|
190
|
+
const TabsIndicator = () => null;
|
|
191
|
+
|
|
192
|
+
// Create compound component with all aliases
|
|
193
|
+
const TabsBase = TabsRoot;
|
|
194
|
+
|
|
195
|
+
// HeroUI-compatible compound component
|
|
196
|
+
const Tabs = Object.assign(TabsBase, {
|
|
197
|
+
List: TabsList,
|
|
198
|
+
ListContainer: TabsListContainer,
|
|
199
|
+
Tab: TabsTab,
|
|
200
|
+
Trigger: TabsTrigger,
|
|
201
|
+
Panel: TabsPanel,
|
|
202
|
+
Content: TabsContent,
|
|
203
|
+
Indicator: TabsIndicator,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, TabsRoot };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
export interface TextFieldProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/**
|
|
8
|
+
* Accessible label for the input.
|
|
9
|
+
* If not provided via children, use aria-label on the div.
|
|
10
|
+
*/
|
|
11
|
+
'aria-label'?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Error state - applies error styling to children inputs
|
|
14
|
+
*/
|
|
15
|
+
isInvalid?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Disabled state - disables all children inputs
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Required state - marks all children inputs as required
|
|
22
|
+
*/
|
|
23
|
+
required?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* TextField - A wrapper component that groups Label and Input together.
|
|
28
|
+
*
|
|
29
|
+
* Groups Label and Input with a convenient wrapper that can propagate
|
|
30
|
+
* disabled, required, and invalid states to child inputs.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <TextField>
|
|
35
|
+
* <Label>Email</Label>
|
|
36
|
+
* <Input type="email" placeholder="Enter email..." />
|
|
37
|
+
* </TextField>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
|
|
41
|
+
({ className, children, isInvalid, disabled, required, ...props }, ref) => {
|
|
42
|
+
// Clone children to pass through state props to Input/Textarea children
|
|
43
|
+
const enhancedChildren = React.Children.map(children, (child) => {
|
|
44
|
+
if (!React.isValidElement(child)) return child;
|
|
45
|
+
|
|
46
|
+
// Get the display name or type name to check if it's an input component
|
|
47
|
+
const childType = child.type;
|
|
48
|
+
const displayName =
|
|
49
|
+
typeof childType === 'function'
|
|
50
|
+
? (childType as React.ComponentType).displayName || childType.name
|
|
51
|
+
: typeof childType === 'string'
|
|
52
|
+
? childType
|
|
53
|
+
: '';
|
|
54
|
+
|
|
55
|
+
// Check if this is an input-like element that should receive state props
|
|
56
|
+
const isInputElement =
|
|
57
|
+
displayName === 'Input' ||
|
|
58
|
+
displayName === 'Textarea' ||
|
|
59
|
+
displayName === 'TextArea' ||
|
|
60
|
+
displayName === 'input' ||
|
|
61
|
+
displayName === 'textarea';
|
|
62
|
+
|
|
63
|
+
if (isInputElement) {
|
|
64
|
+
const childProps = child.props as Record<string, unknown>;
|
|
65
|
+
return React.cloneElement(child as React.ReactElement<Record<string, unknown>>, {
|
|
66
|
+
isInvalid: (childProps.isInvalid as boolean) ?? isInvalid,
|
|
67
|
+
disabled: (childProps.disabled as boolean) ?? disabled,
|
|
68
|
+
required: (childProps.required as boolean) ?? required,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return child;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div ref={ref} className={cn('flex flex-col gap-1.5', className)} {...props}>
|
|
77
|
+
{enhancedChildren}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
TextField.displayName = 'TextField';
|
|
83
|
+
|
|
84
|
+
export { TextField };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import { useSurface } from './surface';
|
|
6
|
+
|
|
7
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
8
|
+
/** Whether the textarea is in an invalid state */
|
|
9
|
+
isInvalid?: boolean;
|
|
10
|
+
/** Visual variant - auto-detected from Surface context, or can be set explicitly */
|
|
11
|
+
variant?: 'default' | 'filled';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
15
|
+
({ className, isInvalid, disabled, readOnly, required, 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
|
+
<textarea
|
|
22
|
+
className={cn(
|
|
23
|
+
'flex min-h-[80px] w-full rounded-input px-2.5 py-2 text-sm transition-colors',
|
|
24
|
+
'text-foreground border-none',
|
|
25
|
+
'placeholder:text-foreground/50',
|
|
26
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus',
|
|
27
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
28
|
+
// Variant styles - semantic: field on page vs field on surface
|
|
29
|
+
effectiveVariant === 'default' && 'bg-field shadow-soft dark:shadow-none',
|
|
30
|
+
effectiveVariant === 'filled' && 'bg-[var(--color-field-on-surface)]',
|
|
31
|
+
isInvalid && 'ring-2 ring-danger focus-visible:ring-danger',
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
ref={ref}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
readOnly={readOnly}
|
|
37
|
+
required={required}
|
|
38
|
+
aria-invalid={isInvalid}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
Textarea.displayName = 'Textarea';
|
|
45
|
+
|
|
46
|
+
// Alias for HeroUI compatibility (HeroUI uses TextArea, Shadcn uses Textarea)
|
|
47
|
+
const TextArea = Textarea;
|
|
48
|
+
TextArea.displayName = 'TextArea';
|
|
49
|
+
|
|
50
|
+
export { Textarea, TextArea };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
8
|
+
|
|
9
|
+
const TooltipRoot = TooltipPrimitive.Root;
|
|
10
|
+
|
|
11
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
12
|
+
|
|
13
|
+
const TooltipContent = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
16
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
17
|
+
<TooltipPrimitive.Portal>
|
|
18
|
+
<TooltipPrimitive.Content
|
|
19
|
+
ref={ref}
|
|
20
|
+
sideOffset={sideOffset}
|
|
21
|
+
className={cn(
|
|
22
|
+
'z-tooltip overflow-hidden rounded-tooltip bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 font-caption',
|
|
23
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
24
|
+
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
</TooltipPrimitive.Portal>
|
|
30
|
+
));
|
|
31
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
32
|
+
|
|
33
|
+
// HeroUI-compatible compound component
|
|
34
|
+
interface TooltipProps {
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
delayDuration?: number;
|
|
37
|
+
skipDelayDuration?: number;
|
|
38
|
+
disableHoverableContent?: boolean;
|
|
39
|
+
// HeroUI compatibility props
|
|
40
|
+
delay?: number;
|
|
41
|
+
closeDelay?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TooltipBase = ({ children, delayDuration, delay, ...props }: TooltipProps) => (
|
|
45
|
+
<TooltipProvider>
|
|
46
|
+
<TooltipRoot delayDuration={delay ?? delayDuration ?? 200} {...props}>
|
|
47
|
+
{children}
|
|
48
|
+
</TooltipRoot>
|
|
49
|
+
</TooltipProvider>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// HeroUI-compatible compound component with .Trigger and .Content
|
|
53
|
+
const Tooltip = Object.assign(TooltipBase, {
|
|
54
|
+
Trigger: TooltipTrigger,
|
|
55
|
+
Content: TooltipContent,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipRoot };
|