@datlv-trustshop/shopify-inapp-components 0.2.3 → 0.2.5

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.
@@ -1,15 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useRef, useCallback, useEffect } from "react";
3
- import { Card, Text, BlockStack, Box, InlineStack, Button, Popover, ActionList, } from "@shopify/polaris";
2
+ import { useState, useRef, useCallback, useEffect, useMemo } from "react";
3
+ import { Card, Text, BlockStack, Box, InlineStack, Button, Popover, ActionList, SkeletonBodyText, SkeletonThumbnail, } from "@shopify/polaris";
4
4
  import { ExternalIcon, MenuHorizontalIcon } from "@shopify/polaris-icons";
5
5
  import { useDashboard } from "../hooks/useDashboard";
6
6
  import { useTranslation } from "../hooks/useTranslations";
7
7
  const DEFAULT_DISMISS_KEY = "ts-dashboard-growapps-dismissed";
8
8
  const DEFAULT_DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000;
9
+ // Fixed dimensions to prevent layout shifts
10
+ const CARD_WIDTH = 282;
11
+ const CARD_MIN_HEIGHT = 140; // Reduced from 180 to avoid excessive white space
12
+ const ICON_SIZE = 60;
13
+ const GAP_SIZE = 20;
9
14
  export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = DEFAULT_DISMISS_KEY, dismissDuration = DEFAULT_DISMISS_DURATION, showDismiss = true, showNavigation = true, maxItems = 6, }) => {
10
15
  const slidesContainerRef = useRef(null);
11
- const currentPositionRef = useRef(0);
12
- const { data } = useDashboard();
16
+ const [currentPosition, setCurrentPosition] = useState(0);
17
+ const { data, loading } = useDashboard();
13
18
  const growAppsTranslations = useTranslation("growApps");
14
19
  const allGrowApps = data?.grow_apps || [];
15
20
  const displayLimit = data?.grow_apps_display_limit || maxItems;
@@ -18,9 +23,13 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
18
23
  const [maxIndex, setMaxIndex] = useState(0);
19
24
  const [popoverActive, setPopoverActive] = useState(false);
20
25
  const [isVisible, setIsVisible] = useState(true);
26
+ const [isContentReady, setIsContentReady] = useState(false);
27
+ const [touchDelta, setTouchDelta] = useState(0);
28
+ const [isSwiping, setIsSwiping] = useState(false);
21
29
  const touchStartX = useRef(null);
22
30
  const touchEndX = useRef(null);
23
31
  const swiping = useRef(false);
32
+ const lastDelta = useRef(0);
24
33
  const togglePopoverActive = () => setPopoverActive((active) => !active);
25
34
  const handleDismiss = () => {
26
35
  setIsVisible(false);
@@ -50,6 +59,15 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
50
59
  }
51
60
  }
52
61
  }, [dismissKey, dismissDuration]);
62
+ // Mark content as ready when data loads
63
+ useEffect(() => {
64
+ if (!loading && growApps.length >= 0) {
65
+ // Small delay to ensure DOM is ready
66
+ requestAnimationFrame(() => {
67
+ setIsContentReady(true);
68
+ });
69
+ }
70
+ }, [loading, growApps.length]);
53
71
  const handleGetAppClick = (app) => {
54
72
  const url = app.button_install_link || app.app_url || app.get_app || "#";
55
73
  if (url && url !== "#") {
@@ -66,8 +84,8 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
66
84
  return;
67
85
  const containerWidth = slidesContainerRef.current.offsetWidth;
68
86
  const totalWidth = slideWrap.scrollWidth;
69
- const slideWidth = 282;
70
- const gap = 20;
87
+ const slideWidth = CARD_WIDTH;
88
+ const gap = GAP_SIZE;
71
89
  const slideStep = slideWidth + gap;
72
90
  const maxSlides = Math.max(0, Math.ceil((totalWidth - containerWidth) / slideStep));
73
91
  const newIndex = Math.max(0, Math.min(currentIndex + direction, maxSlides));
@@ -76,21 +94,30 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
76
94
  return;
77
95
  }
78
96
  const newPosition = -newIndex * slideStep;
79
- slideWrap.style.transform = `translateX(${newPosition}px)`;
80
- currentPositionRef.current = newPosition;
97
+ // Remove direct DOM manipulation - let React handle it
98
+ setCurrentPosition(newPosition);
81
99
  setCurrentIndex(newIndex);
82
100
  setMaxIndex(maxSlides);
83
101
  }, [currentIndex]);
84
- const handleTouchStart = (e) => {
102
+ const handleTouchStart = useCallback((e) => {
85
103
  touchStartX.current = e.touches[0].clientX;
104
+ touchEndX.current = e.touches[0].clientX; // Initialize end position
86
105
  swiping.current = true;
87
- };
88
- const handleTouchMove = (e) => {
89
- if (!swiping.current)
106
+ setIsSwiping(true);
107
+ setTouchDelta(0);
108
+ }, []);
109
+ const handleTouchMove = useCallback((e) => {
110
+ if (!swiping.current || touchStartX.current === null)
90
111
  return;
91
112
  touchEndX.current = e.touches[0].clientX;
92
- };
93
- const handleTouchEnd = () => {
113
+ const delta = touchEndX.current - touchStartX.current;
114
+ // Only update if delta changed significantly (reduce jitter)
115
+ if (Math.abs(delta - lastDelta.current) > 1) {
116
+ lastDelta.current = delta;
117
+ setTouchDelta(delta);
118
+ }
119
+ }, []);
120
+ const handleTouchEnd = useCallback(() => {
94
121
  if (!swiping.current)
95
122
  return;
96
123
  const swipeThreshold = 50;
@@ -103,10 +130,17 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
103
130
  handleSlide(-1);
104
131
  }
105
132
  }
133
+ else {
134
+ // Snap back if swipe wasn't far enough
135
+ setTouchDelta(0);
136
+ }
106
137
  swiping.current = false;
107
138
  touchStartX.current = null;
108
139
  touchEndX.current = null;
109
- };
140
+ lastDelta.current = 0;
141
+ setIsSwiping(false);
142
+ setTouchDelta(0);
143
+ }, [handleSlide]);
110
144
  useEffect(() => {
111
145
  const calculateMaxIndex = () => {
112
146
  if (!slidesContainerRef.current)
@@ -116,23 +150,48 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
116
150
  return;
117
151
  const containerWidth = slidesContainerRef.current.offsetWidth;
118
152
  const totalWidth = slideWrap.scrollWidth;
119
- const slideWidth = 282;
120
- const gap = 20;
121
- const slideStep = slideWidth + gap;
153
+ const slideStep = CARD_WIDTH + GAP_SIZE;
122
154
  const maxSlides = Math.max(0, Math.ceil((totalWidth - containerWidth) / slideStep));
123
155
  setMaxIndex(maxSlides);
124
156
  };
125
- setTimeout(calculateMaxIndex, 100);
126
- const handleResize = () => calculateMaxIndex();
157
+ // Use requestAnimationFrame instead of setTimeout for better performance
158
+ const rafId = requestAnimationFrame(calculateMaxIndex);
159
+ const handleResize = () => {
160
+ requestAnimationFrame(calculateMaxIndex);
161
+ };
127
162
  window.addEventListener("resize", handleResize);
128
163
  return () => {
164
+ cancelAnimationFrame(rafId);
129
165
  window.removeEventListener("resize", handleResize);
130
166
  };
131
167
  }, [growApps.length]);
132
- useEffect(() => { }, [growApps]);
168
+ // Memoize navigation button styles to prevent recalculation
169
+ // Must be defined before any returns to follow React hooks rules
170
+ const navButtonStyle = useMemo(() => ({
171
+ display: "flex",
172
+ alignItems: "center",
173
+ justifyContent: "center",
174
+ width: "24px",
175
+ height: "24px",
176
+ padding: 0,
177
+ border: "none",
178
+ borderRadius: "4px",
179
+ backgroundColor: "transparent",
180
+ transition: "opacity 0.2s ease", // Only animate opacity
181
+ }), []);
182
+ // Don't render at all if dismissed to prevent CLS
133
183
  if (!isVisible) {
134
184
  return null;
135
185
  }
186
+ // Show skeleton while loading to reserve space
187
+ if (loading || !isContentReady) {
188
+ return (_jsx("div", { className: className, style: { width: "100%" }, children: _jsx(Card, { children: _jsx(Box, { children: _jsxs(BlockStack, { gap: "200", children: [_jsxs(BlockStack, { gap: "100", children: [_jsx(SkeletonBodyText, { lines: 1 }), _jsx(SkeletonBodyText, { lines: 1 })] }), _jsx("div", { style: { display: "flex", gap: `${GAP_SIZE}px`, overflow: "hidden" }, children: [1, 2, 3].map((idx) => (_jsx("div", { style: {
189
+ flexShrink: 0,
190
+ width: `${CARD_WIDTH}px`,
191
+ minHeight: `${CARD_MIN_HEIGHT}px`,
192
+ }, children: _jsx(Card, { children: _jsx(Box, { children: _jsxs(BlockStack, { gap: "300", children: [_jsx(SkeletonThumbnail, { size: "large" }), _jsxs(BlockStack, { gap: "200", children: [_jsx(SkeletonBodyText, { lines: 1 }), _jsx(SkeletonBodyText, { lines: 2 })] })] }) }) }) }, idx))) })] }) }) }) }));
193
+ }
194
+ // Handle empty state with reserved space
136
195
  if (growApps.length === 0) {
137
196
  return (_jsx("div", { className: className, style: { width: "100%" }, children: _jsx(Card, { children: _jsx(Box, { children: _jsx(Text, { variant: "bodyMd", tone: "subdued", as: "p", children: growAppsTranslations?.noData || "No apps available" }) }) }) }));
138
197
  }
@@ -155,39 +214,29 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
155
214
  borderRadius: "8px",
156
215
  backgroundColor: "#e3e3e3",
157
216
  }, children: [_jsx("button", { onClick: () => handleSlide(-1), disabled: currentIndex === 0, style: {
158
- display: "flex",
159
- alignItems: "center",
160
- justifyContent: "center",
161
- width: "24px",
162
- height: "24px",
163
- padding: 0,
164
- border: "none",
165
- borderRadius: "4px",
166
- backgroundColor: "transparent",
217
+ ...navButtonStyle,
167
218
  cursor: currentIndex === 0 ? "not-allowed" : "pointer",
168
- transition: "background-color 0.2s ease",
169
219
  opacity: currentIndex === 0 ? 0.5 : 1,
170
220
  }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M10.5 13L5.5 8l5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) }), _jsx("button", { onClick: () => handleSlide(1), disabled: currentIndex >= maxIndex, style: {
171
- display: "flex",
172
- alignItems: "center",
173
- justifyContent: "center",
174
- width: "24px",
175
- height: "24px",
176
- padding: 0,
177
- border: "none",
178
- borderRadius: "4px",
179
- backgroundColor: "transparent",
221
+ ...navButtonStyle,
180
222
  cursor: currentIndex >= maxIndex ? "not-allowed" : "pointer",
181
- transition: "background-color 0.2s ease",
182
223
  opacity: currentIndex >= maxIndex ? 0.5 : 1,
183
224
  }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M5.5 13l5-5-5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) })] }))] })] }), _jsx("div", { className: "slides-container", style: {
184
225
  overflow: "hidden",
185
226
  width: "100%",
227
+ touchAction: "pan-y pinch-zoom", // Allow vertical scroll and pinch, handle horizontal
228
+ WebkitOverflowScrolling: "touch", // iOS momentum scrolling
229
+ userSelect: "none", // Prevent text selection during swipe
230
+ WebkitUserSelect: "none"
231
+ // Removed minHeight to avoid excessive white space
186
232
  }, ref: slidesContainerRef, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: _jsx("div", { className: "slides-wrapper", style: {
187
233
  display: "flex",
188
- gap: "20px",
189
- transition: "transform 0.3s ease-in-out",
190
- transform: "translateX(0px)",
234
+ gap: `${GAP_SIZE}px`,
235
+ transition: isSwiping ? "none" : "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // Better easing
236
+ transform: `translate3d(${currentPosition + touchDelta}px, 0, 0)`, // Use translate3d for GPU
237
+ willChange: isSwiping ? "transform" : "auto", // Only during interaction
238
+ backfaceVisibility: "hidden", // Force GPU acceleration
239
+ perspective: 1000, // Create 3D rendering context
191
240
  }, children: growApps.map((app, index) => {
192
241
  const iconUrl = app.icon_url || app.imageUrl || "";
193
242
  const buttonText = app.button_install_text ||
@@ -197,10 +246,17 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
197
246
  "Get app";
198
247
  return (_jsx("div", { "data-index": index, style: {
199
248
  flexShrink: 0,
200
- width: "282px",
201
- transition: "transform 0.2s ease",
202
- }, children: _jsx(Card, { children: _jsx(Box, { minWidth: "250px", minHeight: "180px", children: _jsxs(BlockStack, { gap: "300", children: [_jsx(InlineStack, { gap: "400", blockAlign: "start", children: iconUrl && (_jsx("img", { src: iconUrl, alt: app.title, width: 60, height: 60, style: {
249
+ width: `${CARD_WIDTH}px`,
250
+ // Remove per-card transition that causes CLS
251
+ }, children: _jsx(Card, { children: _jsx(Box, { minWidth: `${CARD_WIDTH - 32}px`, children: _jsxs(BlockStack, { gap: "300", children: [_jsx(InlineStack, { gap: "400", blockAlign: "start", children: iconUrl ? (_jsx("img", { src: iconUrl, alt: app.title, width: ICON_SIZE, height: ICON_SIZE, style: {
252
+ borderRadius: "8px",
253
+ flexShrink: 0,
254
+ objectFit: "cover",
255
+ }, loading: "eager", decoding: "async" })) : (_jsx("div", { style: {
256
+ width: `${ICON_SIZE}px`,
257
+ height: `${ICON_SIZE}px`,
203
258
  borderRadius: "8px",
259
+ backgroundColor: "#f0f0f0",
204
260
  flexShrink: 0,
205
261
  } })) }), _jsxs(BlockStack, { gap: "200", children: [_jsx("div", { style: {
206
262
  display: "-webkit-box",
@@ -211,6 +267,7 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
211
267
  WebkitBoxOrient: "vertical",
212
268
  overflow: "hidden",
213
269
  textOverflow: "ellipsis",
270
+ minHeight: "20px", // Reserve space
214
271
  }, children: app.title }), _jsx("div", { style: {
215
272
  display: "-webkit-box",
216
273
  WebkitLineClamp: 2,
@@ -220,8 +277,10 @@ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = D
220
277
  fontSize: "13px",
221
278
  color: "#616161",
222
279
  lineHeight: 1.4,
280
+ minHeight: "36px", // Reserve space for 2 lines
223
281
  }, children: app.content }), _jsx("div", { style: {
224
282
  alignSelf: "flex-start",
283
+ minHeight: "36px", // Reserve space for button
225
284
  }, children: _jsx(Button, { icon: ExternalIcon, variant: "secondary", size: "medium", onClick: () => handleGetAppClick(app), children: buttonText }) })] })] }) }) }) }, app.id || index));
226
285
  }) }) })] }) }) }) }));
227
286
  };
@@ -1,17 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useState, useRef, useEffect } from "react";
3
- import { Button, Tabs, Text, BlockStack, InlineStack, Box, Card, } from "@shopify/polaris";
2
+ import { useState, useRef, useEffect, useMemo } from "react";
3
+ import { Button, Tabs, Text, BlockStack, InlineStack, Box, Card, SkeletonBodyText, SkeletonThumbnail, } from "@shopify/polaris";
4
4
  import { ExternalIcon } from "@shopify/polaris-icons";
5
5
  import { ArticleList } from "./ArticleList";
6
6
  import ImageLoading from "./ImageLoading";
7
7
  import { useArticles, useWhatsNew } from "../hooks";
8
8
  import { useTranslation } from "../hooks/useTranslations";
9
9
  import { sdkStyles, mergeStyles } from "./styles";
10
+ // Fixed dimensions to prevent CLS - reduced to avoid excessive white space
11
+ const CONTAINER_MIN_HEIGHT = 300; // Reduced from 450
10
12
  export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick, onViewAllUpdates, onViewAllArticles, maxUpdates = 5, showNavigation = true, showViewAllButton = true, navigate, }) => {
11
13
  const [productUpdateIndex, setProductUpdateIndex] = useState(0);
12
14
  const [articleIndex, setArticleIndex] = useState(0);
13
15
  const [selectedTab, setSelectedTab] = useState(0);
14
16
  const [isMobile, setIsMobile] = useState(false);
17
+ const [isContentReady, setIsContentReady] = useState(false);
18
+ const [touchDelta, setTouchDelta] = useState(0);
19
+ const [isSwiping, setIsSwiping] = useState(false);
15
20
  const touchStartX = useRef(null);
16
21
  const touchEndX = useRef(null);
17
22
  const swiping = useRef(false);
@@ -19,7 +24,7 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
19
24
  const articles = useArticles();
20
25
  const whatsNew = useWhatsNew();
21
26
  const t = useTranslation('whatsNew');
22
- const limitedUpdates = whatsNew.updates.slice(0, maxUpdates);
27
+ const limitedUpdates = useMemo(() => whatsNew.updates.slice(0, maxUpdates), [whatsNew.updates, maxUpdates]);
23
28
  useEffect(() => {
24
29
  const handleResize = () => {
25
30
  setIsMobile(window.innerWidth <= 768);
@@ -28,10 +33,14 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
28
33
  window.addEventListener("resize", handleResize);
29
34
  return () => window.removeEventListener("resize", handleResize);
30
35
  }, []);
31
- React.useEffect(() => {
32
- if (limitedUpdates.length > 0) {
36
+ // Mark content as ready when data loads
37
+ useEffect(() => {
38
+ if (limitedUpdates.length > 0 || articlesList.length > 0) {
39
+ requestAnimationFrame(() => {
40
+ setIsContentReady(true);
41
+ });
33
42
  }
34
- }, [limitedUpdates]);
43
+ }, [limitedUpdates.length, articles.articles.length]);
35
44
  const articlesList = articles.articles;
36
45
  const nextSlide = () => {
37
46
  if (selectedTab === 0) {
@@ -72,11 +81,15 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
72
81
  const handleTouchStart = (e) => {
73
82
  touchStartX.current = e.touches[0].clientX;
74
83
  swiping.current = true;
84
+ setIsSwiping(true);
85
+ setTouchDelta(0);
75
86
  };
76
87
  const handleTouchMove = (e) => {
77
- if (!swiping.current)
88
+ if (!swiping.current || touchStartX.current === null)
78
89
  return;
79
90
  touchEndX.current = e.touches[0].clientX;
91
+ const delta = touchEndX.current - touchStartX.current;
92
+ setTouchDelta(delta);
80
93
  };
81
94
  const handleTouchEnd = () => {
82
95
  if (!swiping.current)
@@ -94,6 +107,8 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
94
107
  swiping.current = false;
95
108
  touchStartX.current = null;
96
109
  touchEndX.current = null;
110
+ setIsSwiping(false);
111
+ setTouchDelta(0);
97
112
  };
98
113
  const handleProductUpdateClick = (update) => {
99
114
  const tryUrl = update.button_try_url || update.link;
@@ -171,13 +186,18 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
171
186
  return articleIndex >= maxIndex;
172
187
  }
173
188
  })();
189
+ // Show skeleton while loading
190
+ if (!isContentReady) {
191
+ return (_jsx("div", { style: { ...sdkStyles.dashboardWhatsNew, minHeight: `${CONTAINER_MIN_HEIGHT}px` }, className: className, children: _jsx(Card, { children: _jsxs(BlockStack, { gap: "400", children: [_jsxs(BlockStack, { gap: "100", children: [_jsx(SkeletonBodyText, { lines: 1 }), _jsx(InlineStack, { gap: "300", blockAlign: "center", align: "space-between", children: _jsx("div", { style: { width: "200px" }, children: _jsx(SkeletonBodyText, { lines: 1 }) }) })] }), _jsx(Box, { children: _jsxs(BlockStack, { gap: "400", children: [_jsx(SkeletonThumbnail, { size: "large" }), _jsx(SkeletonBodyText, { lines: 3 })] }) })] }) }) }));
192
+ }
193
+ // Empty state
174
194
  if (limitedUpdates.length === 0 && articlesList.length === 0) {
175
- return (_jsx("div", { style: mergeStyles(sdkStyles.dashboardWhatsNew), className: className, children: _jsx(Card, { children: _jsx(BlockStack, { gap: "400", children: _jsx(Text, { variant: "bodyMd", tone: "subdued", as: "p", children: t.noData || "No updates available" }) }) }) }));
195
+ return (_jsx("div", { style: { ...sdkStyles.dashboardWhatsNew, minHeight: `${CONTAINER_MIN_HEIGHT}px` }, className: className, children: _jsx(Card, { children: _jsx(BlockStack, { gap: "400", children: _jsx(Text, { variant: "bodyMd", tone: "subdued", as: "p", children: t.noData || "No updates available" }) }) }) }));
176
196
  }
177
197
  const mobileStyles = isMobile
178
198
  ? {
179
199
  slidesContainer: {
180
- overflowX: "auto",
200
+ overflow: "auto",
181
201
  scrollSnapType: "x mandatory",
182
202
  WebkitOverflowScrolling: "touch",
183
203
  scrollbarWidth: "none",
@@ -190,6 +210,7 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
190
210
  scrollSnapAlign: "center",
191
211
  flex: "0 0 85%",
192
212
  maxWidth: "85%",
213
+ minHeight: "280px",
193
214
  },
194
215
  slideImage: {
195
216
  display: "none",
@@ -231,16 +252,20 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
231
252
  ? handleViewAllUpdates
232
253
  : handleViewAllArticles, children: selectedTab === 0
233
254
  ? (t.buttonViewAll?.productUpdate || "View all updates")
234
- : (t.buttonViewAll?.article || "View all articles") })), showNavigation && !isMobile && (_jsxs("div", { style: sdkStyles.slideNavigation, children: [_jsx("button", { onClick: prevSlide, disabled: isPrevDisabled, style: mergeStyles(sdkStyles.slideButton, isPrevDisabled ? sdkStyles.slideButtonDisabled : {}, {
255
+ : (t.buttonViewAll?.article || "View all articles") })), showNavigation && (_jsxs("div", { style: sdkStyles.slideNavigation, children: [_jsx("button", { onClick: prevSlide, disabled: isPrevDisabled, style: mergeStyles(sdkStyles.slideButton, isPrevDisabled ? sdkStyles.slideButtonDisabled : {}, {
235
256
  cursor: isPrevDisabled ? "not-allowed" : "pointer",
236
257
  opacity: isPrevDisabled ? 0.5 : 1,
237
258
  }), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M10.5 13L5.5 8l5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) }), _jsx("button", { onClick: nextSlide, disabled: isNextDisabled, style: mergeStyles(sdkStyles.slideButton, isNextDisabled ? sdkStyles.slideButtonDisabled : {}, {
238
259
  cursor: isNextDisabled ? "not-allowed" : "pointer",
239
260
  opacity: isNextDisabled ? 0.5 : 1,
240
- }), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M5.5 13l5-5-5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) })] }))] })] })] }), _jsx(Box, { children: selectedTab === 0 ? (_jsx("div", { ref: slideContainerRef, style: mergeStyles(sdkStyles.slidesContainer, mobileStyles.slidesContainer, { scrollbarWidth: "none" }), className: isMobile ? "whats-new-mobile-scrollbar" : "", onTouchStart: isMobile ? handleTouchStart : undefined, onTouchMove: isMobile ? handleTouchMove : undefined, onTouchEnd: isMobile ? handleTouchEnd : undefined, children: _jsx("div", { style: mergeStyles(sdkStyles.slidesWrapper, mobileStyles.slidesWrapper, {
261
+ }), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M5.5 13l5-5-5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) })] }))] })] })] }), _jsx(Box, { children: selectedTab === 0 ? (_jsx("div", { ref: slideContainerRef, style: mergeStyles(sdkStyles.slidesContainer, mobileStyles.slidesContainer, {
262
+ scrollbarWidth: "none",
263
+ }), className: isMobile ? "whats-new-mobile-scrollbar" : "", onTouchStart: isMobile ? handleTouchStart : undefined, onTouchMove: isMobile ? handleTouchMove : undefined, onTouchEnd: isMobile ? handleTouchEnd : undefined, children: _jsx("div", { style: mergeStyles(sdkStyles.slidesWrapper, mobileStyles.slidesWrapper, {
241
264
  transform: isMobile
242
- ? "none"
243
- : `translateX(-${productUpdateIndex * 90}%)`,
265
+ ? `translateX(calc(-${productUpdateIndex * 85}% + ${touchDelta}px))` // 85% for mobile with swipe delta
266
+ : `translateX(calc(-${productUpdateIndex * 90}% + ${touchDelta}px))`,
267
+ transition: isSwiping ? "none" : "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
268
+ willChange: "transform", // GPU acceleration
244
269
  }), children: limitedUpdates.map((update) => (_jsxs("div", { style: mergeStyles(sdkStyles.slideBox, isMobile ? mobileStyles.slideBox : {}), children: [_jsxs("div", { style: sdkStyles.slideContent, children: [_jsx(Text, { variant: "headingMd", as: "h3", children: update.title }), (update.releaseDate || update.date_time) && (_jsx(Text, { variant: "bodySm", tone: "subdued", as: "p", children: update.releaseDate || update.date_time })), (update.description || update.content?.text) && (_jsx(Text, { as: "p", children: update.description || update.content?.text })), update.features && update.features.length > 0 && (_jsx("ul", { style: { paddingLeft: "20px", margin: 0 }, children: update.features.map((item, idx) => (_jsx("li", { style: {
245
270
  fontSize: "13px",
246
271
  lineHeight: 1.4,
@@ -254,5 +279,7 @@ export const WhatsNew = ({ className = "", onProductUpdateClick, onArticleClick,
254
279
  }
255
280
  }, children: update.button_learn_more || "Learn more" }))] })] }), _jsx("div", { style: mergeStyles(sdkStyles.slideImage, isMobile ? mobileStyles.slideImage : {}), children: _jsx(ImageLoading, { src: update.imageUrl ||
256
281
  update.image_url ||
257
- "https://asset.trustshop.io/dashboard/news-review-summary.png", alt: `what-new-${update.id}`, width: 712, height: 400, borderRadius: 12 }) })] }, update.id))) }) })) : (_jsx("div", { ref: slideContainerRef, style: mergeStyles(sdkStyles.slidesContainer, { overflowX: "hidden", overflowY: "hidden", scrollbarWidth: "none" }, mobileStyles.slidesContainer), className: isMobile ? "whats-new-mobile-scrollbar" : "", onTouchStart: isMobile ? handleTouchStart : undefined, onTouchMove: isMobile ? handleTouchMove : undefined, onTouchEnd: isMobile ? handleTouchEnd : undefined, children: _jsx(ArticleList, { layout: "slide", onArticleClick: handleArticleClick, showThumbnail: true, showAuthor: true, showDate: true, currentIndex: articleIndex, onSlideChange: setArticleIndex, showNavigation: false }) })) })] }) })] }));
282
+ "https://asset.trustshop.io/dashboard/news-review-summary.png", alt: `what-new-${update.id}`, width: 712, height: 400, borderRadius: 12 }) })] }, update.id))) }) })) : (_jsx("div", { ref: slideContainerRef, style: mergeStyles(sdkStyles.slidesContainer, {
283
+ scrollbarWidth: "none",
284
+ }, mobileStyles.slidesContainer), className: isMobile ? "whats-new-mobile-scrollbar" : "", onTouchStart: isMobile ? handleTouchStart : undefined, onTouchMove: isMobile ? handleTouchMove : undefined, onTouchEnd: isMobile ? handleTouchEnd : undefined, children: _jsx(ArticleList, { layout: "slide", onArticleClick: handleArticleClick, showThumbnail: true, showAuthor: true, showDate: true, currentIndex: articleIndex, onSlideChange: setArticleIndex, showNavigation: false }) })) })] }) })] }));
258
285
  };
@@ -7,3 +7,5 @@ export * from "./useGrowApps";
7
7
  export * from "./usePartnerIntegration";
8
8
  export * from "./useFloatingCards";
9
9
  export * from "./useCampaignTracking";
10
+ export * from "./useFloatingCardActions";
11
+ export * from "./useFloatingCardEngine";
@@ -7,3 +7,5 @@ export * from "./useGrowApps";
7
7
  export * from "./usePartnerIntegration";
8
8
  export * from "./useFloatingCards";
9
9
  export * from "./useCampaignTracking";
10
+ export * from "./useFloatingCardActions";
11
+ export * from "./useFloatingCardEngine";
@@ -0,0 +1,17 @@
1
+ import { FloatingCardData } from '../components/FloatingCard';
2
+ export interface FloatingCardActionsConfig {
3
+ navigate: (path: string) => void;
4
+ onOpenPricing?: () => void;
5
+ onDismiss?: (cardId: string | number) => void;
6
+ specialRoutes?: {
7
+ [key: string]: () => void;
8
+ };
9
+ }
10
+ export interface FloatingCardActionsReturn {
11
+ handlePrimaryAction: (card: FloatingCardData) => void;
12
+ handleSecondaryAction: (card: FloatingCardData) => void;
13
+ handleDismiss: (cardId: string | number) => void;
14
+ resolveAction: (action: FloatingCardData['primary_action'] | FloatingCardData['secondary_action']) => void;
15
+ }
16
+ export declare const useFloatingCardActions: (config: FloatingCardActionsConfig) => FloatingCardActionsReturn;
17
+ export default useFloatingCardActions;
@@ -0,0 +1,54 @@
1
+ import { useCallback } from 'react';
2
+ export const useFloatingCardActions = (config) => {
3
+ const { navigate, onOpenPricing, onDismiss, specialRoutes = {} } = config;
4
+ const resolveAction = useCallback((action) => {
5
+ if (!action?.url)
6
+ return;
7
+ const url = action.url;
8
+ // Check for special routes first
9
+ if (url === '/pricing' || url.startsWith('/pricing?')) {
10
+ if (onOpenPricing) {
11
+ onOpenPricing();
12
+ return;
13
+ }
14
+ }
15
+ // Check custom special routes
16
+ for (const [route, handler] of Object.entries(specialRoutes)) {
17
+ if (url === route || url.startsWith(`${route}?`)) {
18
+ handler();
19
+ return;
20
+ }
21
+ }
22
+ // Handle internal routes (starts with /)
23
+ if (url.startsWith('/')) {
24
+ navigate(url);
25
+ return;
26
+ }
27
+ // Handle external links
28
+ if ((url.startsWith('http://') || url.startsWith('https://')) &&
29
+ action.external !== false) {
30
+ window.open(url, '_blank', 'noopener,noreferrer');
31
+ return;
32
+ }
33
+ // Fallback to navigation for relative paths or unknown formats
34
+ navigate(url);
35
+ }, [navigate, onOpenPricing, specialRoutes]);
36
+ const handlePrimaryAction = useCallback((card) => {
37
+ resolveAction(card.primary_action);
38
+ }, [resolveAction]);
39
+ const handleSecondaryAction = useCallback((card) => {
40
+ resolveAction(card.secondary_action);
41
+ }, [resolveAction]);
42
+ const handleDismiss = useCallback((cardId) => {
43
+ if (onDismiss) {
44
+ onDismiss(cardId);
45
+ }
46
+ }, [onDismiss]);
47
+ return {
48
+ handlePrimaryAction,
49
+ handleSecondaryAction,
50
+ handleDismiss,
51
+ resolveAction,
52
+ };
53
+ };
54
+ export default useFloatingCardActions;
@@ -0,0 +1,21 @@
1
+ import { FloatingCardActionsConfig } from './useFloatingCardActions';
2
+ import { FloatingCardData } from '../components/FloatingCard';
3
+ export interface FloatingCardEngineConfig extends Omit<FloatingCardActionsConfig, 'onDismiss'> {
4
+ shopId?: string;
5
+ currentRoute?: string;
6
+ }
7
+ export interface FloatingCardEngineReturn {
8
+ visibleCards: FloatingCardData[];
9
+ loading: boolean;
10
+ error: Error | null;
11
+ handlePrimaryAction: (card: FloatingCardData) => void;
12
+ handleSecondaryAction: (card: FloatingCardData) => void;
13
+ handleDismiss: (cardId: string | number) => void;
14
+ resolveAction: (action: FloatingCardData['primary_action'] | FloatingCardData['secondary_action']) => void;
15
+ }
16
+ /**
17
+ * Comprehensive hook that combines floating card data fetching with action handling.
18
+ * This provides a complete solution for floating card functionality.
19
+ */
20
+ export declare const useFloatingCardEngine: (config: FloatingCardEngineConfig) => FloatingCardEngineReturn;
21
+ export default useFloatingCardEngine;
@@ -0,0 +1,39 @@
1
+ import { useVisibleFloatingCards } from './useFloatingCards';
2
+ import { useFloatingCardActions } from './useFloatingCardActions';
3
+ /**
4
+ * Comprehensive hook that combines floating card data fetching with action handling.
5
+ * This provides a complete solution for floating card functionality.
6
+ */
7
+ export const useFloatingCardEngine = (config) => {
8
+ const { shopId, currentRoute, navigate, onOpenPricing, specialRoutes } = config;
9
+ // Fetch visible floating cards
10
+ const { visibleCards, dismissCard, loading, error } = useVisibleFloatingCards({
11
+ shopId,
12
+ currentRoute,
13
+ });
14
+ // Set up action handlers with dismiss integrated
15
+ const actions = useFloatingCardActions({
16
+ navigate,
17
+ onOpenPricing,
18
+ specialRoutes,
19
+ onDismiss: (cardId) => {
20
+ // Convert to number if needed for compatibility
21
+ const id = typeof cardId === 'string' ? parseInt(cardId, 10) : cardId;
22
+ if (!isNaN(id)) {
23
+ dismissCard(id);
24
+ }
25
+ },
26
+ });
27
+ return {
28
+ // Data
29
+ visibleCards,
30
+ loading,
31
+ error,
32
+ // Actions
33
+ handlePrimaryAction: actions.handlePrimaryAction,
34
+ handleSecondaryAction: actions.handleSecondaryAction,
35
+ handleDismiss: actions.handleDismiss,
36
+ resolveAction: actions.resolveAction,
37
+ };
38
+ };
39
+ export default useFloatingCardEngine;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { DashboardProvider, DashboardContext, useDashboardContext, } from "./pro
7
7
  export type { DashboardProviderProps, DashboardContextValue, } from "./provider/DashboardProvider";
8
8
  export * from "./hooks";
9
9
  export * from "./components";
10
+ export { CLSMonitor, startCLSMonitoring, stopCLSMonitoring, getCLSMetrics } from "./utils/cls-monitor";
11
+ export type { CLSMetrics, CLSMonitorOptions } from "./utils/cls-monitor";
10
12
  export type { SDKTranslations } from './types/translations';
11
13
  export { defaultTranslations } from './translations/default';
12
14
  export { useTranslations, useTranslation } from './hooks/useTranslations';
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ export * from "./types";
6
6
  export { DashboardProvider, DashboardContext, useDashboardContext, } from "./provider/DashboardProvider";
7
7
  export * from "./hooks";
8
8
  export * from "./components";
9
+ // Export CLS monitoring utilities (optional)
10
+ export { CLSMonitor, startCLSMonitoring, stopCLSMonitoring, getCLSMetrics } from "./utils/cls-monitor";
9
11
  export { defaultTranslations } from './translations/default';
10
12
  export { useTranslations, useTranslation } from './hooks/useTranslations';
11
13
  // Campaign Tracking
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { createContext, useEffect, useState, useCallback, useMemo, useRef, } from "react";
3
3
  import { GlobalDashboardManager } from "../core/global-manager";
4
4
  import { defaultTranslations } from "../translations/default";
5
+ import { injectClsStyles } from "../utils/injectStyles";
5
6
  export const DashboardContext = createContext(null);
6
7
  export const useDashboardContext = () => {
7
8
  const context = React.useContext(DashboardContext);
@@ -38,6 +39,10 @@ export const DashboardProvider = ({ children, config, locale, translations, auto
38
39
  ...config,
39
40
  locale: locale || config.locale || "en",
40
41
  }), [config, locale]);
42
+ // Inject CLS fix styles on mount (only once globally)
43
+ useEffect(() => {
44
+ injectClsStyles();
45
+ }, []);
41
46
  // Register this provider
42
47
  useEffect(() => {
43
48
  const providerId = providerIdRef.current;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CLS (Cumulative Layout Shift) Monitoring Utility
3
+ * Tracks and reports layout shifts for performance optimization
4
+ */
5
+ interface LayoutShiftEntry extends PerformanceEntry {
6
+ value: number;
7
+ hadRecentInput: boolean;
8
+ sources?: Array<{
9
+ node?: Node;
10
+ previousRect: DOMRectReadOnly;
11
+ currentRect: DOMRectReadOnly;
12
+ }>;
13
+ }
14
+ export interface CLSMetrics {
15
+ value: number;
16
+ entries: LayoutShiftEntry[];
17
+ timestamp: number;
18
+ }
19
+ export interface CLSMonitorOptions {
20
+ threshold?: number;
21
+ onShift?: (metrics: CLSMetrics) => void;
22
+ onThresholdExceeded?: (metrics: CLSMetrics) => void;
23
+ debug?: boolean;
24
+ reportToConsole?: boolean;
25
+ reportToAnalytics?: (metrics: CLSMetrics) => void;
26
+ }
27
+ export declare class CLSMonitor {
28
+ private observer;
29
+ private clsValue;
30
+ private clsEntries;
31
+ private options;
32
+ private sessionValue;
33
+ private sessionEntries;
34
+ private lastShiftTime;
35
+ private readonly SESSION_GAP;
36
+ constructor(options?: CLSMonitorOptions);
37
+ start(): void;
38
+ private processLayoutShift;
39
+ private logShift;
40
+ stop(): void;
41
+ reset(): void;
42
+ getCurrentCLS(): CLSMetrics;
43
+ private supportsLayoutShift;
44
+ static identifyProblematicElements(entries: LayoutShiftEntry[]): Element[];
45
+ static getAttribution(entries: LayoutShiftEntry[]): Map<string, number>;
46
+ }
47
+ export declare function startCLSMonitoring(options?: CLSMonitorOptions): CLSMonitor;
48
+ export declare function stopCLSMonitoring(): void;
49
+ export declare function getCLSMetrics(): CLSMetrics | null;
50
+ export {};
@@ -0,0 +1,184 @@
1
+ /**
2
+ * CLS (Cumulative Layout Shift) Monitoring Utility
3
+ * Tracks and reports layout shifts for performance optimization
4
+ */
5
+ export class CLSMonitor {
6
+ constructor(options = {}) {
7
+ this.observer = null;
8
+ this.clsValue = 0;
9
+ this.clsEntries = [];
10
+ this.sessionValue = 0;
11
+ this.sessionEntries = [];
12
+ this.lastShiftTime = 0;
13
+ this.SESSION_GAP = 5000; // 5 seconds
14
+ this.options = {
15
+ threshold: options.threshold || 0.1, // Good CLS threshold
16
+ onShift: options.onShift || (() => { }),
17
+ onThresholdExceeded: options.onThresholdExceeded || (() => { }),
18
+ debug: options.debug || false,
19
+ reportToConsole: options.reportToConsole || false,
20
+ reportToAnalytics: options.reportToAnalytics || (() => { }),
21
+ };
22
+ }
23
+ start() {
24
+ if (!this.supportsLayoutShift()) {
25
+ console.warn('PerformanceObserver for layout-shift not supported');
26
+ return;
27
+ }
28
+ try {
29
+ this.observer = new PerformanceObserver((list) => {
30
+ const entries = list.getEntries();
31
+ for (const entry of entries) {
32
+ // Ignore shifts with recent input (user-initiated)
33
+ if (!entry.hadRecentInput) {
34
+ this.processLayoutShift(entry);
35
+ }
36
+ }
37
+ });
38
+ this.observer.observe({
39
+ type: 'layout-shift',
40
+ buffered: true
41
+ });
42
+ if (this.options.debug) {
43
+ console.log('CLS Monitor started');
44
+ }
45
+ }
46
+ catch (error) {
47
+ console.error('Failed to start CLS Monitor:', error);
48
+ }
49
+ }
50
+ processLayoutShift(entry) {
51
+ const now = Date.now();
52
+ // Check if this is part of a new session
53
+ if (now - this.lastShiftTime > this.SESSION_GAP) {
54
+ // Start new session
55
+ this.sessionValue = 0;
56
+ this.sessionEntries = [];
57
+ }
58
+ this.lastShiftTime = now;
59
+ this.sessionValue += entry.value;
60
+ this.sessionEntries.push(entry);
61
+ // Update cumulative value (max session value)
62
+ if (this.sessionValue > this.clsValue) {
63
+ this.clsValue = this.sessionValue;
64
+ this.clsEntries = [...this.sessionEntries];
65
+ }
66
+ const metrics = {
67
+ value: this.clsValue,
68
+ entries: this.clsEntries,
69
+ timestamp: now,
70
+ };
71
+ // Report shift
72
+ this.options.onShift(metrics);
73
+ // Check threshold
74
+ if (this.clsValue > this.options.threshold) {
75
+ this.options.onThresholdExceeded(metrics);
76
+ }
77
+ // Debug logging
78
+ if (this.options.debug || this.options.reportToConsole) {
79
+ this.logShift(entry, metrics);
80
+ }
81
+ // Report to analytics
82
+ this.options.reportToAnalytics(metrics);
83
+ }
84
+ logShift(entry, metrics) {
85
+ const affectedElements = entry.sources?.map(source => {
86
+ if (source.node && source.node.nodeType === 1) {
87
+ const element = source.node;
88
+ return {
89
+ element: element.tagName,
90
+ id: element.id,
91
+ className: element.className,
92
+ previousRect: source.previousRect,
93
+ currentRect: source.currentRect,
94
+ };
95
+ }
96
+ return null;
97
+ }).filter(Boolean) || [];
98
+ console.group(`🎯 Layout Shift Detected`);
99
+ console.log('Shift Value:', entry.value.toFixed(4));
100
+ console.log('Session CLS:', this.sessionValue.toFixed(4));
101
+ console.log('Total CLS:', metrics.value.toFixed(4));
102
+ console.log('Threshold:', this.options.threshold);
103
+ console.log('Status:', metrics.value > this.options.threshold ? '❌ Bad' : '✅ Good');
104
+ if (affectedElements.length > 0) {
105
+ console.log('Affected Elements:', affectedElements);
106
+ }
107
+ console.groupEnd();
108
+ }
109
+ stop() {
110
+ if (this.observer) {
111
+ this.observer.disconnect();
112
+ this.observer = null;
113
+ if (this.options.debug) {
114
+ console.log('CLS Monitor stopped');
115
+ }
116
+ }
117
+ }
118
+ reset() {
119
+ this.clsValue = 0;
120
+ this.clsEntries = [];
121
+ this.sessionValue = 0;
122
+ this.sessionEntries = [];
123
+ this.lastShiftTime = 0;
124
+ }
125
+ getCurrentCLS() {
126
+ return {
127
+ value: this.clsValue,
128
+ entries: this.clsEntries,
129
+ timestamp: Date.now(),
130
+ };
131
+ }
132
+ supportsLayoutShift() {
133
+ return typeof PerformanceObserver !== 'undefined' &&
134
+ PerformanceObserver.supportedEntryTypes?.includes('layout-shift');
135
+ }
136
+ // Helper method to identify problematic elements
137
+ static identifyProblematicElements(entries) {
138
+ const problematicElements = new Set();
139
+ for (const entry of entries) {
140
+ if (entry.sources) {
141
+ for (const source of entry.sources) {
142
+ if (source.node && source.node.nodeType === 1) {
143
+ problematicElements.add(source.node);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return Array.from(problematicElements);
149
+ }
150
+ // Helper to get CLS attribution
151
+ static getAttribution(entries) {
152
+ const attribution = new Map();
153
+ for (const entry of entries) {
154
+ if (entry.sources) {
155
+ for (const source of entry.sources) {
156
+ if (source.node && source.node.nodeType === 1) {
157
+ const element = source.node;
158
+ const key = `${element.tagName}${element.id ? '#' + element.id : ''}${element.className ? '.' + element.className.split(' ').join('.') : ''}`;
159
+ attribution.set(key, (attribution.get(key) || 0) + entry.value);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ return attribution;
165
+ }
166
+ }
167
+ // Singleton instance for easy global usage
168
+ let globalMonitor = null;
169
+ export function startCLSMonitoring(options) {
170
+ if (!globalMonitor) {
171
+ globalMonitor = new CLSMonitor(options);
172
+ globalMonitor.start();
173
+ }
174
+ return globalMonitor;
175
+ }
176
+ export function stopCLSMonitoring() {
177
+ if (globalMonitor) {
178
+ globalMonitor.stop();
179
+ globalMonitor = null;
180
+ }
181
+ }
182
+ export function getCLSMetrics() {
183
+ return globalMonitor ? globalMonitor.getCurrentCLS() : null;
184
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Automatically inject CLS fix styles into the document
3
+ * This ensures the styles are always applied without requiring manual imports
4
+ */
5
+ export declare const polarisClsFixStyles = "\n/* Critical Polaris CLS Fixes */\n.Polaris-ShadowBevel {\n min-height: inherit !important;\n will-change: contents;\n}\n\n.Polaris-Card {\n min-height: 100px;\n contain: layout style;\n}\n\n.Polaris-Tabs__Panel {\n min-height: 350px;\n contain: layout;\n}\n\n.Polaris-Tabs__TabPanel {\n min-height: inherit;\n}\n\n.Polaris-Box {\n /* Prevent height changes from dynamic content */\n contain: style;\n}\n\n/* Skeleton state improvements */\n.Polaris-SkeletonBodyText,\n.Polaris-SkeletonDisplayText,\n.Polaris-SkeletonThumbnail {\n animation: none !important;\n background: linear-gradient(\n 90deg,\n #f0f0f0 25%,\n #f8f8f8 50%,\n #f0f0f0 75%\n );\n background-size: 200% 100%;\n animation: pulse 1.5s ease-in-out infinite !important;\n}\n\n@keyframes pulse {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n\n/* Prevent layout shifts from Polaris transitions */\n.Polaris-Card,\n.Polaris-Banner,\n.Polaris-CalloutCard {\n transform: translateZ(0);\n will-change: transform;\n}\n\n/* Fix for navigation tabs */\n.custom-tabs-wrapper-dashboard {\n min-height: 400px;\n}\n\n/* GPU acceleration for transforms */\n[style*=\"transform\"] {\n will-change: transform;\n}\n";
6
+ /**
7
+ * Inject CLS fix styles into the document head
8
+ * This is called automatically when the SDK initializes
9
+ */
10
+ export declare function injectClsStyles(): void;
11
+ /**
12
+ * Remove injected styles (for cleanup)
13
+ */
14
+ export declare function removeClsStyles(): void;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Automatically inject CLS fix styles into the document
3
+ * This ensures the styles are always applied without requiring manual imports
4
+ */
5
+ const STYLE_ID = 'ts-shopify-polaris-cls-fixes';
6
+ export const polarisClsFixStyles = `
7
+ /* Critical Polaris CLS Fixes */
8
+ .Polaris-ShadowBevel {
9
+ min-height: inherit !important;
10
+ will-change: contents;
11
+ }
12
+
13
+ .Polaris-Card {
14
+ min-height: 100px;
15
+ contain: layout style;
16
+ }
17
+
18
+ .Polaris-Tabs__Panel {
19
+ min-height: 350px;
20
+ contain: layout;
21
+ }
22
+
23
+ .Polaris-Tabs__TabPanel {
24
+ min-height: inherit;
25
+ }
26
+
27
+ .Polaris-Box {
28
+ /* Prevent height changes from dynamic content */
29
+ contain: style;
30
+ }
31
+
32
+ /* Skeleton state improvements */
33
+ .Polaris-SkeletonBodyText,
34
+ .Polaris-SkeletonDisplayText,
35
+ .Polaris-SkeletonThumbnail {
36
+ animation: none !important;
37
+ background: linear-gradient(
38
+ 90deg,
39
+ #f0f0f0 25%,
40
+ #f8f8f8 50%,
41
+ #f0f0f0 75%
42
+ );
43
+ background-size: 200% 100%;
44
+ animation: pulse 1.5s ease-in-out infinite !important;
45
+ }
46
+
47
+ @keyframes pulse {
48
+ 0% {
49
+ background-position: 200% 0;
50
+ }
51
+ 100% {
52
+ background-position: -200% 0;
53
+ }
54
+ }
55
+
56
+ /* Prevent layout shifts from Polaris transitions */
57
+ .Polaris-Card,
58
+ .Polaris-Banner,
59
+ .Polaris-CalloutCard {
60
+ transform: translateZ(0);
61
+ will-change: transform;
62
+ }
63
+
64
+ /* Fix for navigation tabs */
65
+ .custom-tabs-wrapper-dashboard {
66
+ min-height: 400px;
67
+ }
68
+
69
+ /* GPU acceleration for transforms */
70
+ [style*="transform"] {
71
+ will-change: transform;
72
+ }
73
+ `;
74
+ /**
75
+ * Inject CLS fix styles into the document head
76
+ * This is called automatically when the SDK initializes
77
+ */
78
+ export function injectClsStyles() {
79
+ // Check if styles are already injected
80
+ if (typeof document === 'undefined') {
81
+ return; // SSR safety
82
+ }
83
+ if (document.getElementById(STYLE_ID)) {
84
+ return; // Already injected
85
+ }
86
+ // Create and inject style element
87
+ const styleElement = document.createElement('style');
88
+ styleElement.id = STYLE_ID;
89
+ styleElement.textContent = polarisClsFixStyles;
90
+ // Add to head (or body as fallback)
91
+ const target = document.head || document.body;
92
+ target.appendChild(styleElement);
93
+ }
94
+ /**
95
+ * Remove injected styles (for cleanup)
96
+ */
97
+ export function removeClsStyles() {
98
+ if (typeof document === 'undefined') {
99
+ return;
100
+ }
101
+ const styleElement = document.getElementById(STYLE_ID);
102
+ if (styleElement) {
103
+ styleElement.remove();
104
+ }
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datlv-trustshop/shopify-inapp-components",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "description": "React TypeScript components for Shopify in-app dashboard content",
6
6
  "main": "dist/index.js",