@@ -946,6 +1250,7 @@ function RecommendRuntimePreviewCard({
@@ -966,43 +1271,48 @@ function RecommendRuntimePreviewCard({
function RecommendRuntimeCover({
entry,
className = '',
+ resolvedCoverUrls,
}: {
entry: PlatformPublicGalleryCard;
className?: string;
+ resolvedCoverUrls?: RecommendResolvedCoverUrlMap;
}) {
- const coverImage = resolvePlatformWorldCoverImage(entry);
- const fallbackCoverImage = resolvePlatformWorldFallbackCoverImage(entry);
-
return (
- {coverImage || fallbackCoverImage ? (
-
- ) : (
-
- )}
-
+
);
}
-function RecommendRuntimeMountedProbe({
- onMounted,
+function RecommendRuntimeReadyProbe({
+ rootRef,
+ onReady,
}: {
- onMounted: () => void;
+ rootRef: RefObject
;
+ onReady: () => void;
}) {
useEffect(() => {
- const animationFrameId = window.requestAnimationFrame(onMounted);
- return () => window.cancelAnimationFrame(animationFrameId);
- }, [onMounted]);
+ const abortController = new AbortController();
+
+ void readyRecommendRuntime(
+ rootRef.current,
+ abortController.signal,
+ ).then((ready) => {
+ if (ready) {
+ onReady();
+ }
+ });
+
+ return () => abortController.abort();
+ }, [onReady, rootRef]);
return null;
}
@@ -1012,15 +1322,55 @@ function RecommendRuntimeVisual({
runtimeContent,
isStarting,
isRuntimeReady,
+ resolvedCoverUrls,
}: {
entry: PlatformPublicGalleryCard;
runtimeContent?: ReactNode;
isStarting: boolean;
isRuntimeReady: boolean;
+ resolvedCoverUrls?: RecommendResolvedCoverUrlMap;
}) {
const [isRuntimeMounted, setIsRuntimeMounted] = useState(false);
+ const [isCoverMinVisible, setIsCoverMinVisible] = useState(true);
const activeEntryKey = buildPublicGalleryCardKey(entry);
const previousEntryKeyRef = useRef(activeEntryKey);
+ const runtimeVisibilityRef = useRef({
+ hasRuntimeContent: Boolean(runtimeContent),
+ isRuntimeMounted: false,
+ isRuntimeReady,
+ isStarting,
+ });
+ const runtimeViewportRef = useRef(null);
+
+ useEffect(() => {
+ runtimeVisibilityRef.current = {
+ hasRuntimeContent: Boolean(runtimeContent),
+ isRuntimeMounted,
+ isRuntimeReady,
+ isStarting,
+ };
+ }, [isRuntimeMounted, isRuntimeReady, isStarting, runtimeContent]);
+
+ useEffect(() => {
+ const currentRuntimeVisibility = runtimeVisibilityRef.current;
+ if (
+ previousEntryKeyRef.current !== activeEntryKey &&
+ currentRuntimeVisibility.hasRuntimeContent &&
+ currentRuntimeVisibility.isRuntimeMounted &&
+ currentRuntimeVisibility.isRuntimeReady &&
+ !currentRuntimeVisibility.isStarting
+ ) {
+ setIsCoverMinVisible(false);
+ return undefined;
+ }
+
+ setIsCoverMinVisible(true);
+ const timeoutId = window.setTimeout(() => {
+ setIsCoverMinVisible(false);
+ }, RECOMMEND_RUNTIME_COVER_MIN_VISIBLE_MS);
+
+ return () => window.clearTimeout(timeoutId);
+ }, [activeEntryKey]);
useEffect(() => {
if (previousEntryKeyRef.current === activeEntryKey) {
@@ -1037,33 +1387,40 @@ function RecommendRuntimeVisual({
});
}, [activeEntryKey, isRuntimeReady, isStarting]);
- const handleRuntimeMounted = useCallback(() => {
+ const handleRuntimeReady = useCallback(() => {
if (!isStarting && isRuntimeReady) {
setIsRuntimeMounted(true);
}
}, [isRuntimeReady, isStarting]);
const shouldShowCover =
- !runtimeContent || isStarting || !isRuntimeReady || !isRuntimeMounted;
+ isCoverMinVisible ||
+ !runtimeContent ||
+ isStarting ||
+ !isRuntimeReady ||
+ !isRuntimeMounted;
return (
{runtimeContent ? (
{runtimeContent}
+
-
) : null}
{
const entryMap = new Map();
const sourceEntries =
@@ -5571,6 +5930,8 @@ export function RpgEntryHomeView({
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
useState<1 | -1 | null>(null);
+ const [isRecommendDragResetting, setIsRecommendDragResetting] =
+ useState(false);
const activeRecommendEntryKeyForSelection = activeRecommendEntry
? buildPublicGalleryCardKey(activeRecommendEntry)
: null;
@@ -5587,6 +5948,7 @@ export function RpgEntryHomeView({
}
setRecommendDragCommitDirection(direction);
+ setIsRecommendDragResetting(false);
const panelHeight =
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
const commitDistance = panelHeight > 0 ? panelHeight : window.innerHeight;
@@ -5599,8 +5961,12 @@ export function RpgEntryHomeView({
} else {
onSelectPreviousRecommendEntry?.(activeRecommendEntryKeyForSelection);
}
+ setIsRecommendDragResetting(true);
setRecommendDragOffsetY(0);
setRecommendDragCommitDirection(null);
+ window.requestAnimationFrame(() => {
+ setIsRecommendDragResetting(false);
+ });
}, RECOMMEND_ENTRY_COMMIT_ANIMATION_MS);
},
[
@@ -5689,7 +6055,9 @@ export function RpgEntryHomeView({
} satisfies CSSProperties;
const recommendRailClassName = recommendDragCommitDirection
? 'platform-recommend-swipe-rail--committing'
- : recommendDragOffsetY === 0
+ : isRecommendDragResetting
+ ? 'platform-recommend-swipe-rail--resetting'
+ : recommendDragOffsetY === 0
? 'platform-recommend-swipe-rail--settled'
: 'platform-recommend-swipe-rail--dragging';
const selectNextRecommendEntry = useCallback(() => {
@@ -5826,6 +6194,7 @@ export function RpgEntryHomeView({
}
/>
@@ -5848,6 +6217,7 @@ export function RpgEntryHomeView({
runtimeContent={recommendRuntimeContent}
isStarting={isStartingRecommendEntry}
isRuntimeReady={isRecommendRuntimeReady}
+ resolvedCoverUrls={resolvedRecommendCoverUrls}
/>
}
onDragPointerDown={beginRecommendDrag}
@@ -5875,6 +6245,7 @@ export function RpgEntryHomeView({
}
/>
diff --git a/src/index.css b/src/index.css
index 9017c5b0..ee047ae7 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5303,13 +5303,13 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
background: var(--platform-recommend-runtime-fill);
opacity: 1;
pointer-events: auto;
- transition: opacity 420ms ease;
will-change: opacity;
}
.platform-recommend-runtime-cover--hidden {
opacity: 0;
pointer-events: none;
+ transition: opacity 420ms ease;
}
.platform-recommend-swipe-stage {
@@ -5335,6 +5335,10 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
transition: none;
}
+ .platform-recommend-swipe-rail--resetting {
+ transition: none;
+ }
+
.platform-recommend-swipe-page {
position: absolute;
inset: 0;
diff --git a/src/index.test.ts b/src/index.test.ts
index d01d35b4..b10d925c 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -129,3 +129,18 @@ describe('index stylesheet creation agent hero contrast', () => {
expect(hintBlock).toContain('rgba(255, 255, 255, 0.72) !important');
});
});
+
+describe('index stylesheet recommend runtime cover', () => {
+ it('only fades the card cover out after runtime is ready', () => {
+ const css = readIndexCss();
+
+ const coverBlock = getCssBlock(css, '.platform-recommend-runtime-cover');
+ expect(coverBlock).not.toContain('transition: opacity');
+
+ const hiddenCoverBlock = getCssBlock(
+ css,
+ '.platform-recommend-runtime-cover--hidden',
+ );
+ expect(hiddenCoverBlock).toContain('transition: opacity 420ms ease;');
+ });
+});