@seekora-ai/ui-sdk-react 0.2.10 → 0.2.12
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/dist/components/primitives/ActionButtons.d.ts +27 -0
- package/dist/components/primitives/ActionButtons.d.ts.map +1 -0
- package/dist/components/primitives/ActionButtons.js +78 -0
- package/dist/components/primitives/AnalyticsProvider.d.ts +22 -0
- package/dist/components/primitives/AnalyticsProvider.d.ts.map +1 -0
- package/dist/components/primitives/AnalyticsProvider.js +87 -0
- package/dist/components/primitives/BadgeList.d.ts +14 -0
- package/dist/components/primitives/BadgeList.d.ts.map +1 -0
- package/dist/components/primitives/BadgeList.js +45 -0
- package/dist/components/primitives/ImageDisplay.d.ts +10 -1
- package/dist/components/primitives/ImageDisplay.d.ts.map +1 -1
- package/dist/components/primitives/ImageDisplay.js +49 -9
- package/dist/components/primitives/ImageZoom.d.ts +33 -0
- package/dist/components/primitives/ImageZoom.d.ts.map +1 -0
- package/dist/components/primitives/ImageZoom.js +357 -0
- package/dist/components/primitives/PriceDisplay.d.ts +21 -0
- package/dist/components/primitives/PriceDisplay.d.ts.map +1 -0
- package/dist/components/primitives/PriceDisplay.js +44 -0
- package/dist/components/primitives/RatingDisplay.d.ts +43 -0
- package/dist/components/primitives/RatingDisplay.d.ts.map +1 -0
- package/dist/components/primitives/RatingDisplay.js +114 -0
- package/dist/components/primitives/VariantSelector.d.ts +30 -0
- package/dist/components/primitives/VariantSelector.d.ts.map +1 -0
- package/dist/components/primitives/VariantSelector.js +162 -0
- package/dist/components/primitives/VariantSwatches.d.ts +28 -0
- package/dist/components/primitives/VariantSwatches.d.ts.map +1 -0
- package/dist/components/primitives/VariantSwatches.js +173 -0
- package/dist/components/primitives/index.d.ts +9 -0
- package/dist/components/primitives/index.d.ts.map +1 -1
- package/dist/components/primitives/index.js +9 -0
- package/dist/components/primitives/withAnalytics.d.ts +24 -0
- package/dist/components/primitives/withAnalytics.d.ts.map +1 -0
- package/dist/components/primitives/withAnalytics.js +73 -0
- package/dist/components/product-page/ProductInfo.d.ts +25 -2
- package/dist/components/product-page/ProductInfo.d.ts.map +1 -1
- package/dist/components/product-page/ProductInfo.js +20 -5
- package/dist/components/suggestions/types.d.ts +24 -0
- package/dist/components/suggestions/types.d.ts.map +1 -1
- package/dist/components/suggestions/utils.d.ts +37 -0
- package/dist/components/suggestions/utils.d.ts.map +1 -1
- package/dist/components/suggestions/utils.js +118 -0
- package/dist/components/suggestions-primitives/ItemCard.d.ts +10 -1
- package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemCard.js +20 -6
- package/dist/components/suggestions-primitives/ProductCard.d.ts +27 -3
- package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCard.js +124 -17
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts +44 -0
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/ProductCardLayouts.js +105 -0
- package/dist/components/suggestions-primitives/ProductGrid.d.ts +6 -1
- package/dist/components/suggestions-primitives/ProductGrid.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductGrid.js +2 -2
- package/dist/hooks/useProductAnalytics.d.ts +49 -0
- package/dist/hooks/useProductAnalytics.d.ts.map +1 -0
- package/dist/hooks/useProductAnalytics.js +116 -0
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +6 -0
- package/dist/hooks/useVariantSelection.d.ts +28 -0
- package/dist/hooks/useVariantSelection.d.ts.map +1 -0
- package/dist/hooks/useVariantSelection.js +44 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +1107 -679
- package/dist/src/index.esm.js +2267 -600
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +2283 -599
- package/dist/src/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionButtons – card action buttons (add to cart, wishlist, buy now, quick view)
|
|
3
|
+
*
|
|
4
|
+
* Renders a set of action buttons for product cards. Can be positioned absolutely
|
|
5
|
+
* over the image (on hover) or inline below the card content.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
export type ActionButtonType = 'addToCart' | 'wishlist' | 'buyNow' | 'quickView' | 'compare';
|
|
9
|
+
export interface ActionButton {
|
|
10
|
+
type: ActionButtonType;
|
|
11
|
+
label?: string;
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ActionButtonsProps {
|
|
18
|
+
buttons: ActionButton[];
|
|
19
|
+
layout?: 'horizontal' | 'vertical' | 'overlay';
|
|
20
|
+
position?: 'top-right' | 'bottom-center' | 'inline';
|
|
21
|
+
showLabels?: boolean;
|
|
22
|
+
size?: 'small' | 'medium' | 'large';
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: React.CSSProperties;
|
|
25
|
+
}
|
|
26
|
+
export declare function ActionButtons({ buttons, layout, position, showLabels, size, className, style, }: ActionButtonsProps): React.JSX.Element;
|
|
27
|
+
//# sourceMappingURL=ActionButtons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActionButtons.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ActionButtons.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7F,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,YAAY,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAwBD,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,MAAqB,EACrB,QAAmB,EACnB,UAAkB,EAClB,IAAe,EACf,SAAS,EACT,KAAK,GACN,EAAE,kBAAkB,qBAqFpB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionButtons – card action buttons (add to cart, wishlist, buy now, quick view)
|
|
3
|
+
*
|
|
4
|
+
* Renders a set of action buttons for product cards. Can be positioned absolutely
|
|
5
|
+
* over the image (on hover) or inline below the card content.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { clsx } from 'clsx';
|
|
9
|
+
const DEFAULT_ICONS = {
|
|
10
|
+
addToCart: '🛒',
|
|
11
|
+
wishlist: '♡',
|
|
12
|
+
buyNow: '⚡',
|
|
13
|
+
quickView: '👁',
|
|
14
|
+
compare: '⚖',
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_LABELS = {
|
|
17
|
+
addToCart: 'Add to Cart',
|
|
18
|
+
wishlist: 'Wishlist',
|
|
19
|
+
buyNow: 'Buy Now',
|
|
20
|
+
quickView: 'Quick View',
|
|
21
|
+
compare: 'Compare',
|
|
22
|
+
};
|
|
23
|
+
const BUTTON_SIZES = {
|
|
24
|
+
small: { width: 28, height: 28, fontSize: '0.75rem', iconSize: '1rem' },
|
|
25
|
+
medium: { width: 36, height: 36, fontSize: '0.875rem', iconSize: '1.25rem' },
|
|
26
|
+
large: { width: 44, height: 44, fontSize: '1rem', iconSize: '1.5rem' },
|
|
27
|
+
};
|
|
28
|
+
export function ActionButtons({ buttons, layout = 'horizontal', position = 'inline', showLabels = false, size = 'medium', className, style, }) {
|
|
29
|
+
const sizeConfig = BUTTON_SIZES[size];
|
|
30
|
+
const isOverlay = position !== 'inline';
|
|
31
|
+
const containerStyle = {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
34
|
+
gap: 6,
|
|
35
|
+
...(isOverlay ? {
|
|
36
|
+
position: 'absolute',
|
|
37
|
+
...(position === 'top-right' ? { top: 8, right: 8 } : {}),
|
|
38
|
+
...(position === 'bottom-center' ? { bottom: 8, left: '50%', transform: 'translateX(-50%)' } : {}),
|
|
39
|
+
} : {}),
|
|
40
|
+
...style,
|
|
41
|
+
};
|
|
42
|
+
const buttonBaseStyle = {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
justifyContent: 'center',
|
|
46
|
+
gap: 4,
|
|
47
|
+
padding: showLabels ? '0 12px' : 0,
|
|
48
|
+
width: showLabels ? 'auto' : sizeConfig.width,
|
|
49
|
+
height: sizeConfig.height,
|
|
50
|
+
fontSize: sizeConfig.fontSize,
|
|
51
|
+
fontWeight: 500,
|
|
52
|
+
border: 'none',
|
|
53
|
+
borderRadius: 6,
|
|
54
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
55
|
+
color: 'var(--seekora-text, #111827)',
|
|
56
|
+
cursor: 'pointer',
|
|
57
|
+
transition: 'all 150ms ease',
|
|
58
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
59
|
+
};
|
|
60
|
+
const handleClick = (btn, e) => {
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
if (btn.onClick && !btn.disabled && !btn.loading) {
|
|
64
|
+
btn.onClick(e);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return (React.createElement("div", { className: clsx('seekora-action-buttons', `seekora-action-buttons--${layout}`, className), style: containerStyle }, buttons.map((btn, i) => {
|
|
68
|
+
const icon = btn.icon ?? DEFAULT_ICONS[btn.type];
|
|
69
|
+
const label = btn.label ?? DEFAULT_LABELS[btn.type];
|
|
70
|
+
return (React.createElement("button", { key: i, type: "button", className: clsx('seekora-action-button', `seekora-action-button--${btn.type}`, btn.disabled && 'seekora-action-button--disabled', btn.loading && 'seekora-action-button--loading'), style: {
|
|
71
|
+
...buttonBaseStyle,
|
|
72
|
+
opacity: btn.disabled ? 0.5 : 1,
|
|
73
|
+
cursor: btn.disabled ? 'not-allowed' : 'pointer',
|
|
74
|
+
}, onClick: (e) => handleClick(btn, e), disabled: btn.disabled, "aria-label": label, title: label }, btn.loading ? (React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, "\u23F3")) : (React.createElement(React.Fragment, null,
|
|
75
|
+
React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, icon),
|
|
76
|
+
showLabels && React.createElement("span", null, label)))));
|
|
77
|
+
})));
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsProvider – context + delegated event listener for data-seekora-* attribute-based analytics
|
|
3
|
+
*
|
|
4
|
+
* Installs a delegated click listener on a container. Any descendant with
|
|
5
|
+
* data-seekora-track="click"|"add-to-cart" and data-seekora-product-id="..."
|
|
6
|
+
* automatically fires analytics events without React wiring.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import type { SeekoraClient, SearchContext } from '@seekora-ai/search-sdk';
|
|
10
|
+
export interface AnalyticsProviderProps {
|
|
11
|
+
client: SeekoraClient;
|
|
12
|
+
context?: SearchContext | Partial<SearchContext>;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
interface AnalyticsContextValue {
|
|
16
|
+
client: SeekoraClient;
|
|
17
|
+
context?: SearchContext | Partial<SearchContext>;
|
|
18
|
+
}
|
|
19
|
+
export declare const useAnalyticsProvider: () => AnalyticsContextValue | null;
|
|
20
|
+
export declare function AnalyticsProvider({ client, context, children }: AnalyticsProviderProps): React.JSX.Element;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=AnalyticsProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnalyticsProvider.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/AnalyticsProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAoE,MAAM,OAAO,CAAC;AACzF,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG3E,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACjD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,UAAU,qBAAqB;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAClD;AAID,eAAO,MAAM,oBAAoB,oCAAqC,CAAC;AAEvE,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,sBAAsB,qBAsFtF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsProvider – context + delegated event listener for data-seekora-* attribute-based analytics
|
|
3
|
+
*
|
|
4
|
+
* Installs a delegated click listener on a container. Any descendant with
|
|
5
|
+
* data-seekora-track="click"|"add-to-cart" and data-seekora-product-id="..."
|
|
6
|
+
* automatically fires analytics events without React wiring.
|
|
7
|
+
*/
|
|
8
|
+
import React, { useRef, useEffect, useCallback, createContext, useContext } from 'react';
|
|
9
|
+
import { log } from '@seekora-ai/ui-sdk-core';
|
|
10
|
+
const AnalyticsContext = createContext(null);
|
|
11
|
+
export const useAnalyticsProvider = () => useContext(AnalyticsContext);
|
|
12
|
+
export function AnalyticsProvider({ client, context, children }) {
|
|
13
|
+
const containerRef = useRef(null);
|
|
14
|
+
const sendEvent = useCallback(async (eventName, metadata) => {
|
|
15
|
+
try {
|
|
16
|
+
await client.trackEvent?.({
|
|
17
|
+
event_name: eventName,
|
|
18
|
+
metadata: {
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
source: 'analytics_provider_delegation',
|
|
21
|
+
...metadata,
|
|
22
|
+
},
|
|
23
|
+
}, context);
|
|
24
|
+
log.verbose(`AnalyticsProvider: ${eventName}`, metadata);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
log.warn(`AnalyticsProvider: Failed to track ${eventName}`, { error });
|
|
28
|
+
}
|
|
29
|
+
}, [client, context]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const container = containerRef.current;
|
|
32
|
+
if (!container)
|
|
33
|
+
return;
|
|
34
|
+
const handleClick = (e) => {
|
|
35
|
+
const target = e.target;
|
|
36
|
+
// Walk up from target to find element with data-seekora-track
|
|
37
|
+
const tracked = target.closest('[data-seekora-track]');
|
|
38
|
+
if (!tracked)
|
|
39
|
+
return;
|
|
40
|
+
const trackType = tracked.getAttribute('data-seekora-track');
|
|
41
|
+
const productId = tracked.getAttribute('data-seekora-product-id');
|
|
42
|
+
const position = tracked.getAttribute('data-seekora-position');
|
|
43
|
+
const section = tracked.getAttribute('data-seekora-section');
|
|
44
|
+
const variantSku = tracked.getAttribute('data-seekora-variant-sku');
|
|
45
|
+
const variantOption = tracked.getAttribute('data-seekora-variant-option');
|
|
46
|
+
const variantValue = tracked.getAttribute('data-seekora-variant-value');
|
|
47
|
+
const metadata = {};
|
|
48
|
+
if (productId)
|
|
49
|
+
metadata.product_id = productId;
|
|
50
|
+
if (position)
|
|
51
|
+
metadata.position = parseInt(position, 10);
|
|
52
|
+
if (section)
|
|
53
|
+
metadata.section = section;
|
|
54
|
+
switch (trackType) {
|
|
55
|
+
case 'click':
|
|
56
|
+
sendEvent('product.click', metadata);
|
|
57
|
+
if (client?.trackClick && productId) {
|
|
58
|
+
const pos = position ? parseInt(position, 10) + 1 : 1;
|
|
59
|
+
Promise.resolve(client.trackClick(productId, pos, context)).catch(() => { });
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case 'add-to-cart':
|
|
63
|
+
if (variantSku)
|
|
64
|
+
metadata.variant_sku = variantSku;
|
|
65
|
+
sendEvent('product.add_to_cart', metadata);
|
|
66
|
+
break;
|
|
67
|
+
case 'variant-select':
|
|
68
|
+
if (variantOption)
|
|
69
|
+
metadata.option_name = variantOption;
|
|
70
|
+
if (variantValue)
|
|
71
|
+
metadata.option_value = variantValue;
|
|
72
|
+
sendEvent('product.variant_select', metadata);
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
// Custom track type — use it as event name
|
|
76
|
+
if (trackType) {
|
|
77
|
+
sendEvent(trackType, metadata);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
container.addEventListener('click', handleClick);
|
|
83
|
+
return () => container.removeEventListener('click', handleClick);
|
|
84
|
+
}, [client, context, sendEvent]);
|
|
85
|
+
return (React.createElement(AnalyticsContext.Provider, { value: { client, context } },
|
|
86
|
+
React.createElement("div", { ref: containerRef, style: { display: 'contents' } }, children)));
|
|
87
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BadgeList – renders product badges (sale, new, sold out, custom)
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import type { ProductBadge } from '@seekora-ai/ui-sdk-types';
|
|
6
|
+
export interface BadgeListProps {
|
|
7
|
+
badges: ProductBadge[];
|
|
8
|
+
maxBadges?: number;
|
|
9
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'inline';
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function BadgeList({ badges, maxBadges, position, className, style, }: BadgeListProps): React.JSX.Element | null;
|
|
14
|
+
//# sourceMappingURL=BadgeList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BadgeList.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/BadgeList.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,GAAG,QAAQ,CAAC;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAkBD,wBAAgB,SAAS,CAAC,EACxB,MAAM,EACN,SAAS,EACT,QAAqB,EACrB,SAAS,EACT,KAAK,GACN,EAAE,cAAc,4BA4ChB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BadgeList – renders product badges (sale, new, sold out, custom)
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { clsx } from 'clsx';
|
|
6
|
+
const positionStyles = {
|
|
7
|
+
'top-left': { position: 'absolute', top: 6, left: 6 },
|
|
8
|
+
'top-right': { position: 'absolute', top: 6, right: 6 },
|
|
9
|
+
'bottom-left': { position: 'absolute', bottom: 6, left: 6 },
|
|
10
|
+
'bottom-right': { position: 'absolute', bottom: 6, right: 6 },
|
|
11
|
+
inline: {},
|
|
12
|
+
};
|
|
13
|
+
const typeColors = {
|
|
14
|
+
sale: { bg: '#ef4444', text: '#fff' },
|
|
15
|
+
new: { bg: '#3b82f6', text: '#fff' },
|
|
16
|
+
soldOut: { bg: '#6b7280', text: '#fff' },
|
|
17
|
+
limited: { bg: '#f59e0b', text: '#fff' },
|
|
18
|
+
custom: { bg: '#111827', text: '#fff' },
|
|
19
|
+
};
|
|
20
|
+
export function BadgeList({ badges, maxBadges, position = 'top-left', className, style, }) {
|
|
21
|
+
if (!badges || badges.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
const visible = maxBadges ? badges.slice(0, maxBadges) : badges;
|
|
24
|
+
return (React.createElement("div", { className: clsx('seekora-badge-list', className), style: {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
flexWrap: 'wrap',
|
|
27
|
+
gap: 4,
|
|
28
|
+
zIndex: 1,
|
|
29
|
+
...positionStyles[position],
|
|
30
|
+
...style,
|
|
31
|
+
} }, visible.map((badge, i) => {
|
|
32
|
+
const colors = typeColors[badge.type ?? 'custom'];
|
|
33
|
+
return (React.createElement("span", { key: `${badge.text}-${i}`, className: clsx('seekora-badge', badge.type && `seekora-badge--${badge.type === 'soldOut' ? 'sold-out' : badge.type}`), style: {
|
|
34
|
+
display: 'inline-block',
|
|
35
|
+
padding: '2px 8px',
|
|
36
|
+
borderRadius: 4,
|
|
37
|
+
fontSize: '0.6875rem',
|
|
38
|
+
fontWeight: 600,
|
|
39
|
+
lineHeight: 1.4,
|
|
40
|
+
backgroundColor: badge.color ?? colors.bg,
|
|
41
|
+
color: badge.textColor ?? colors.text,
|
|
42
|
+
whiteSpace: 'nowrap',
|
|
43
|
+
} }, badge.text));
|
|
44
|
+
})));
|
|
45
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* ProductCard, ProductGallery. Overridable via className/style.
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
|
+
import type { ImageZoomMode } from './ImageZoom';
|
|
8
9
|
export type ImageDisplayVariant = 'single' | 'carousel' | 'hover' | 'thumbStrip' | 'thumbList';
|
|
9
10
|
export interface ImageDisplayProps {
|
|
10
11
|
images: string[];
|
|
@@ -14,6 +15,14 @@ export interface ImageDisplayProps {
|
|
|
14
15
|
style?: React.CSSProperties;
|
|
15
16
|
carouselAutoplay?: boolean;
|
|
16
17
|
carouselIntervalMs?: number;
|
|
18
|
+
/** Enable zoom functionality */
|
|
19
|
+
enableZoom?: boolean;
|
|
20
|
+
/** Zoom mode: 'hover' (Amazon-style), 'lens', 'click' (lightbox), 'both' (hover + click) */
|
|
21
|
+
zoomMode?: ImageZoomMode;
|
|
22
|
+
/** Zoom magnification level (2 = 200%, 3 = 300%) */
|
|
23
|
+
zoomLevel?: number;
|
|
24
|
+
/** Show navigation dots for carousel */
|
|
25
|
+
showDots?: boolean;
|
|
17
26
|
}
|
|
18
|
-
export declare function ImageDisplay({ images, variant, alt, className, style, carouselAutoplay, carouselIntervalMs, }: ImageDisplayProps): React.JSX.Element;
|
|
27
|
+
export declare function ImageDisplay({ images, variant, alt, className, style, carouselAutoplay, carouselIntervalMs, enableZoom, zoomMode, zoomLevel, showDots, }: ImageDisplayProps): React.JSX.Element;
|
|
19
28
|
//# sourceMappingURL=ImageDisplay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageDisplay.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ImageDisplay.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAgC,MAAM,OAAO,CAAC;AAGrD,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,WAAW,CAAC;AAE/F,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ImageDisplay.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ImageDisplay.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAgC,MAAM,OAAO,CAAC;AAGrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,WAAW,CAAC;AAE/F,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gCAAgC;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAUD,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,OAAkB,EAClB,GAAQ,EACR,SAAS,EACT,KAAK,EACL,gBAAwB,EACxB,kBAAyB,EACzB,UAAkB,EAClB,QAAiB,EACjB,SAAe,EACf,QAAe,GAChB,EAAE,iBAAiB,qBAsNnB"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
8
|
import { clsx } from 'clsx';
|
|
9
|
+
import { ImageZoom } from './ImageZoom';
|
|
9
10
|
const imgBaseStyle = {
|
|
10
11
|
width: '100%',
|
|
11
12
|
aspectRatio: '1',
|
|
@@ -13,7 +14,7 @@ const imgBaseStyle = {
|
|
|
13
14
|
borderRadius: 4,
|
|
14
15
|
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
15
16
|
};
|
|
16
|
-
export function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, }) {
|
|
17
|
+
export function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, enableZoom = false, zoomMode = 'both', zoomLevel = 2.5, showDots = true, }) {
|
|
17
18
|
const [index, setIndex] = useState(0);
|
|
18
19
|
const [hovering, setHovering] = useState(false);
|
|
19
20
|
const safeImages = Array.isArray(images) ? images.filter(Boolean) : [];
|
|
@@ -22,13 +23,21 @@ export function ImageDisplay({ images, variant = 'single', alt = '', className,
|
|
|
22
23
|
return React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-placeholder', className), style: { ...imgBaseStyle, ...style }, "aria-hidden": true });
|
|
23
24
|
}
|
|
24
25
|
if (variant === 'single') {
|
|
26
|
+
if (enableZoom) {
|
|
27
|
+
return (React.createElement(ImageZoom, { src: safeImages[0], alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: 0, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style } }));
|
|
28
|
+
}
|
|
25
29
|
return (React.createElement("img", { src: safeImages[0], alt: alt, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" }));
|
|
26
30
|
}
|
|
27
31
|
if (variant === 'hover') {
|
|
28
32
|
const showSecond = safeImages.length > 1 && hovering;
|
|
29
33
|
const src = showSecond ? safeImages[1] : safeImages[0];
|
|
34
|
+
const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
35
|
+
if (enableZoom) {
|
|
36
|
+
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
37
|
+
React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
|
|
38
|
+
}
|
|
30
39
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
31
|
-
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style:
|
|
40
|
+
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: hoverImgStyle, loading: "lazy" })));
|
|
32
41
|
}
|
|
33
42
|
if (variant === 'carousel') {
|
|
34
43
|
const go = (delta) => {
|
|
@@ -41,16 +50,41 @@ export function ImageDisplay({ images, variant = 'single', alt = '', className,
|
|
|
41
50
|
return next;
|
|
42
51
|
});
|
|
43
52
|
};
|
|
53
|
+
const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
54
|
+
const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-carousel-main", style: carouselImgStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: carouselImgStyle, loading: "lazy" }));
|
|
44
55
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
|
|
45
|
-
|
|
56
|
+
mainImage,
|
|
46
57
|
safeImages.length > 1 && (React.createElement(React.Fragment, null,
|
|
47
|
-
React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: () => go(-1) }),
|
|
48
|
-
React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: () => go(1) })))));
|
|
58
|
+
React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(-1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u2039"),
|
|
59
|
+
React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u203A"),
|
|
60
|
+
showDots && (React.createElement("div", { className: "seekora-img-carousel-dots", style: {
|
|
61
|
+
position: 'absolute',
|
|
62
|
+
bottom: 8,
|
|
63
|
+
left: '50%',
|
|
64
|
+
transform: 'translateX(-50%)',
|
|
65
|
+
display: 'flex',
|
|
66
|
+
gap: 6,
|
|
67
|
+
padding: '6px 12px',
|
|
68
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
69
|
+
borderRadius: 12,
|
|
70
|
+
zIndex: 10,
|
|
71
|
+
} }, safeImages.map((_, i) => (React.createElement("button", { key: i, type: "button", "aria-label": `Go to image ${i + 1}`, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); }, onClick: (e) => { e.stopPropagation(); setIndex(i); }, style: {
|
|
72
|
+
width: 8,
|
|
73
|
+
height: 8,
|
|
74
|
+
borderRadius: '50%',
|
|
75
|
+
border: 'none',
|
|
76
|
+
padding: 0,
|
|
77
|
+
backgroundColor: i === index ? '#fff' : 'rgba(255,255,255,0.5)',
|
|
78
|
+
cursor: 'pointer',
|
|
79
|
+
transition: 'all 150ms ease',
|
|
80
|
+
} })))))))));
|
|
49
81
|
}
|
|
50
82
|
if (variant === 'thumbStrip' || variant === 'thumbList') {
|
|
83
|
+
const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
84
|
+
const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-thumb-main", style: thumbMainStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: thumbMainStyle, loading: "lazy" }));
|
|
51
85
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: 8, ...style } },
|
|
52
|
-
|
|
53
|
-
React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: () => setIndex(i) },
|
|
86
|
+
mainImage,
|
|
87
|
+
React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); setIndex(i); }, onClick: (e) => e.stopPropagation() },
|
|
54
88
|
React.createElement("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })))))));
|
|
55
89
|
}
|
|
56
90
|
return React.createElement("img", { src: current, alt: alt, className: clsx('seekora-img-display', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" });
|
|
@@ -64,11 +98,17 @@ function arrowStyle(left) {
|
|
|
64
98
|
width: 32,
|
|
65
99
|
height: 32,
|
|
66
100
|
borderRadius: '50%',
|
|
67
|
-
border: '
|
|
68
|
-
backgroundColor: '
|
|
101
|
+
border: 'none',
|
|
102
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
103
|
+
color: '#111',
|
|
104
|
+
fontSize: '1.25rem',
|
|
105
|
+
fontWeight: 'bold',
|
|
69
106
|
cursor: 'pointer',
|
|
70
107
|
display: 'flex',
|
|
71
108
|
alignItems: 'center',
|
|
72
109
|
justifyContent: 'center',
|
|
110
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
111
|
+
zIndex: 10,
|
|
112
|
+
transition: 'all 150ms ease',
|
|
73
113
|
};
|
|
74
114
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageZoom – zoom on hover and click (Amazon-style magnifier + lightbox)
|
|
3
|
+
*
|
|
4
|
+
* Supports three zoom modes:
|
|
5
|
+
* - hover: Magnified view in a separate panel on the right (Amazon style)
|
|
6
|
+
* - lens: Magnifying glass that follows cursor
|
|
7
|
+
* - click: Full-screen lightbox modal
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
export type ImageZoomMode = 'hover' | 'lens' | 'click' | 'both';
|
|
11
|
+
export interface ImageZoomProps {
|
|
12
|
+
src: string;
|
|
13
|
+
alt?: string;
|
|
14
|
+
mode?: ImageZoomMode;
|
|
15
|
+
zoomLevel?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
style?: React.CSSProperties;
|
|
18
|
+
/** Show zoom indicator icon */
|
|
19
|
+
showZoomIndicator?: boolean;
|
|
20
|
+
/** Lens size in pixels (for lens mode) */
|
|
21
|
+
lensSize?: number;
|
|
22
|
+
/** Zoom panel size (for hover mode) */
|
|
23
|
+
zoomPanelSize?: {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
};
|
|
27
|
+
/** Multiple images for lightbox carousel */
|
|
28
|
+
images?: string[];
|
|
29
|
+
/** Current image index (for multi-image support) */
|
|
30
|
+
currentIndex?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function ImageZoom({ src, alt, mode, zoomLevel, className, style, showZoomIndicator, lensSize, zoomPanelSize, images, currentIndex, }: ImageZoomProps): React.JSX.Element;
|
|
33
|
+
//# sourceMappingURL=ImageZoom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageZoom.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ImageZoom.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAGxE,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhE,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,+BAA+B;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,SAAS,CAAC,EACxB,GAAG,EACH,GAAQ,EACR,IAAa,EACb,SAAe,EACf,SAAS,EACT,KAAK,EACL,iBAAwB,EACxB,QAAc,EACd,aAA2C,EAC3C,MAAM,EACN,YAAgB,GACjB,EAAE,cAAc,qBAgehB"}
|