@snowcone-app/ui 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "React components for merchandise visualization and customization",
5
5
  "keywords": [
6
6
  "react",
@@ -878,6 +878,10 @@ export const HeroProductImage = memo(function HeroProductImage({
878
878
  // useEffect runs and overlays prevUrl (showing OLD on top), then prevUrl
879
879
  // fades out to reveal NEW again — i.e. the new → old → new flash.
880
880
  const [renderedUrl, setRenderedUrl] = useState<string | null>(displayUrl);
881
+ // `renderedUrl` is set as soon as a URL exists, but the <img> paints nothing
882
+ // until the render is actually fetched (seconds, on a cold render). Keep the
883
+ // pulsing skeleton up until the first image has painted.
884
+ const [firstImageLoaded, setFirstImageLoaded] = useState(false);
881
885
  const prevDisplayUrlRef = useRef<string | null>(displayUrl);
882
886
  // Fire the L3 "you forgot signMockupUrl" dev hint at most once per component.
883
887
  const signHintShownRef = useRef(false);
@@ -972,6 +976,13 @@ export const HeroProductImage = memo(function HeroProductImage({
972
976
  style={style}
973
977
  data-hero-image="true"
974
978
  >
979
+ {/* Loading skeleton — painted UNDER the <img> so the image covers it the
980
+ instant it renders, with no state-flush flash. Removed once the first
981
+ image has loaded; later URL changes crossfade over a visible image. */}
982
+ {!firstImageLoaded && (
983
+ <div className="absolute inset-0 bg-muted-foreground/20 animate-pulse" />
984
+ )}
985
+
975
986
  {/* Current image — shows `renderedUrl`, which the crossfade effect only
976
987
  advances after the next image has finished preloading. This prevents
977
988
  the new → old → new flash that happened when displayUrl was bound
@@ -990,6 +1001,7 @@ export const HeroProductImage = memo(function HeroProductImage({
990
1001
  fetchPriority="high"
991
1002
  onClick={onClick}
992
1003
  onLoad={() => {
1004
+ setFirstImageLoaded(true);
993
1005
  if (!onLoadCalledRef.current && onLoad) {
994
1006
  onLoadCalledRef.current = true;
995
1007
  onLoad();
@@ -997,6 +1009,7 @@ export const HeroProductImage = memo(function HeroProductImage({
997
1009
  onUrlGeneratedRef.current?.(renderedUrl);
998
1010
  }}
999
1011
  onError={() => {
1012
+ setFirstImageLoaded(true);
1000
1013
  onError?.();
1001
1014
  // Dev-only guidance for the most common signed-shop trap: a mockup
1002
1015
  // <img> that loads an UNSIGNED resolver URL gets a 403 "Missing
@@ -1047,11 +1060,6 @@ export const HeroProductImage = memo(function HeroProductImage({
1047
1060
  />
1048
1061
  )}
1049
1062
 
1050
- {/* Simple placeholder - shown until we have a URL to render */}
1051
- {!renderedUrl && (
1052
- <div className="absolute inset-0 bg-zinc-200" />
1053
- )}
1054
-
1055
1063
  {/* SAFARI FIX: Disabled shimmer overlay - it depends on layers state which causes re-renders */}
1056
1064
  {/* {showShimmer && layers.length > 0 && (
1057
1065
  <div