This commit is contained in:
2026-05-08 21:46:11 +08:00
parent 94975e4735
commit e410f7974e
13 changed files with 757 additions and 426 deletions
@@ -4699,6 +4699,31 @@ export function PlatformEntryFlowShellImpl({
],
);
const saveAndExitRecommendPuzzleRuntime = useCallback(async () => {
if (activeRecommendRuntimeKind !== 'puzzle') {
return;
}
const currentRun = puzzleRunRef.current;
if (!currentRun) {
setActiveRecommendRuntimeKind(null);
return;
}
// 中文注释:推荐页嵌入拼图的“保存并退出”沿用现有运行态语义;
// 正式 run 的每次交换/拖动/通关已写回后端,退出时只收口暂停和本地快照。
const closedRun = currentRun.currentLevel
? setLocalPuzzlePaused(currentRun, false)
: currentRun;
puzzleRunRef.current = null;
setPuzzleRun(null);
setActiveRecommendRuntimeKind(null);
if (closedRun.currentLevel) {
setPuzzleError(null);
}
}, [activeRecommendRuntimeKind, setPuzzleError]);
const leaveAgentWorkspace = useCallback(() => {
enterCreateTab();
sessionController.resetSessionViewState();
@@ -5952,6 +5977,9 @@ export function PlatformEntryFlowShellImpl({
async (entry: PlatformPublicGalleryCard) => {
const entryKey = getPlatformPublicGalleryEntryKey(entry);
const runtimeKind = getPlatformRecommendRuntimeKind(entry);
if (entryKey !== activeRecommendEntryKey) {
await saveAndExitRecommendPuzzleRuntime();
}
setActiveRecommendEntryKey(entryKey);
setActiveRecommendRuntimeKind(runtimeKind);
setActiveRecommendRuntimeError(null);
@@ -6025,6 +6053,8 @@ export function PlatformEntryFlowShellImpl({
}
},
[
activeRecommendEntryKey,
saveAndExitRecommendPuzzleRuntime,
selectedPuzzleDetail,
setBigFishError,
setMatch3DError,
@@ -6037,6 +6067,38 @@ export function PlatformEntryFlowShellImpl({
startVisualNovelRunFromProfile,
],
);
const selectAdjacentRecommendRuntimeEntry = useCallback(
(direction: 1 | -1) => {
if (recommendRuntimeEntries.length === 0) {
return;
}
const activeIndex = recommendRuntimeEntries.findIndex(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
);
const baseIndex = activeIndex >= 0 ? activeIndex : 0;
const nextIndex =
(baseIndex + direction + recommendRuntimeEntries.length) %
recommendRuntimeEntries.length;
const nextEntry = recommendRuntimeEntries[nextIndex];
if (!nextEntry) {
return;
}
if (
getPlatformPublicGalleryEntryKey(nextEntry) === activeRecommendEntryKey
) {
return;
}
void selectRecommendRuntimeEntry(nextEntry);
},
[
activeRecommendEntryKey,
recommendRuntimeEntries,
selectRecommendRuntimeEntry,
],
);
const recommendRuntimeContent = useMemo(() => {
if (
@@ -6153,6 +6215,8 @@ export function PlatformEntryFlowShellImpl({
}
error={puzzleError}
embedded
hideBackButton
hideExitControls
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
@@ -6270,7 +6334,7 @@ export function PlatformEntryFlowShellImpl({
}
return (
<div className="flex h-full min-h-0 items-center justify-center bg-black px-5 text-center text-sm font-semibold leading-6 text-white">
<div className={`platform-theme ${platformThemeClass} flex h-full min-h-0 items-center justify-center bg-[var(--platform-recommend-runtime-state-fill)] px-5 text-center text-sm font-semibold leading-6 text-[var(--platform-recommend-runtime-state-text)]`}>
</div>
);
@@ -6291,6 +6355,7 @@ export function PlatformEntryFlowShellImpl({
match3dError,
match3dFlow,
match3dRun,
platformThemeClass,
puzzleError,
puzzleRun,
recommendRuntimeEntries,
@@ -7357,9 +7422,12 @@ export function PlatformEntryFlowShellImpl({
isVisualNovelBusy
}
recommendRuntimeError={activeRecommendRuntimeError}
onSelectRecommendEntry={(entry) => {
void selectRecommendRuntimeEntry(entry);
}}
onSelectNextRecommendEntry={() =>
selectAdjacentRecommendRuntimeEntry(1)
}
onSelectPreviousRecommendEntry={() =>
selectAdjacentRecommendRuntimeEntry(-1)
}
onOpenLibraryDetail={(entry) => {
runProtectedAction(() => {
void detailNavigation.openLibraryDetail(entry);
@@ -240,7 +240,7 @@ test('首次退出引导的作品改造按钮进入改造流程', () => {
expect(screen.queryByRole('dialog')).toBeNull();
});
test('顶部作者显示头像昵称,底部功能居中放大且不显示等待候选', () => {
test('顶部不显示作者,关卡标题和倒计时更紧凑', () => {
const runWithoutNext: PuzzleRunSnapshot = {
...clearedRun,
recommendedNextProfileId: null,
@@ -256,15 +256,17 @@ test('顶部作者显示头像昵称,底部功能居中放大且不显示等
/>,
);
const avatar = screen.getByText('测');
const timer = screen.getByText('4:48');
const hintButton = screen.getByRole('button', { name: '提示' });
const referenceButton = screen.getByRole('button', { name: '原图' });
const freezeButton = screen.getByRole('button', { name: '冻结' });
expect(avatar.className).toContain('rounded-full');
expect(screen.getByText('测试作者')).toBeTruthy();
expect(timer.className).toContain('text-2xl');
expect(screen.queryByText('测试作者')).toBeNull();
expect(screen.getByText('第 1 关')).toBeTruthy();
expect(screen.getByText('潮雾拼图')).toBeTruthy();
expect(timer.className).toContain('puzzle-runtime-timer');
expect(timer.className).toContain('text-lg');
expect(timer.className).not.toContain('text-2xl');
expect(hintButton.className).toContain('h-16');
expect(referenceButton.className).toContain('h-16');
expect(freezeButton.className).toContain('h-16');
@@ -467,14 +469,32 @@ test('拼图棋盘使用贴近移动端边缘的正方形舞台承载切块', ()
);
const board = screen.getByTestId('puzzle-board');
expect(board.className).toContain('puzzle-runtime-board');
expect(board.className).toContain('aspect-square');
expect(board.className).toContain('max-w-[min(99vw,calc(100vh_-_16.5rem))]');
expect(board.className).toContain('max-w-[min(99vw,calc(100vh_-_14rem))]');
expect(board.className).not.toContain('aspect-video');
expect(board.className).not.toContain('aspect-[9/16]');
expect(board.getAttribute('style')).toContain('grid-template-rows');
expect(container.querySelector('.min-h-\\[4\\.5rem\\]')).toBeNull();
});
test('拼图运行态主体使用主题语义类承接明暗主题', () => {
const { container } = renderPuzzleRuntime(
<PuzzleRuntimeShell
run={null}
onBack={vi.fn()}
onSwapPieces={vi.fn()}
onDragPiece={vi.fn()}
onAdvanceNextLevel={vi.fn()}
/>,
);
expect(container.firstElementChild?.className).toContain(
'puzzle-runtime-shell',
);
expect(container.querySelector('.puzzle-runtime-pill')).toBeTruthy();
});
test('合并块按实际拼块外轮廓描边', () => {
const mergedRun: PuzzleRunSnapshot = {
...clearedRun,
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,13 @@
/* @vitest-environment jsdom */
import { act, render, screen, waitFor, within } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { afterEach, expect, test, vi } from 'vitest';
@@ -302,6 +309,17 @@ vi.mock('../ResolvedAssetImage', () => ({
const originalMatchMedia = window.matchMedia;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const originalCancelAnimationFrame = window.cancelAnimationFrame;
function dispatchClientYPointerEvent(
target: HTMLElement,
type: string,
clientY: number,
) {
const event = new Event(type, { bubbles: true, cancelable: true });
Object.assign(event, { clientY });
target.dispatchEvent(event);
}
const puzzlePublicEntry = {
sourceType: 'puzzle',
workId: 'puzzle-work-public-1',
@@ -512,7 +530,8 @@ function renderLoggedOutHomeView(
| 'activeRecommendEntryKey'
| 'isStartingRecommendEntry'
| 'recommendRuntimeError'
| 'onSelectRecommendEntry'
| 'onSelectNextRecommendEntry'
| 'onSelectPreviousRecommendEntry'
>
> = {},
) {
@@ -566,7 +585,8 @@ function renderLoggedOutHomeView(
activeRecommendEntryKey={overrides.activeRecommendEntryKey}
isStartingRecommendEntry={overrides.isStartingRecommendEntry}
recommendRuntimeError={overrides.recommendRuntimeError}
onSelectRecommendEntry={overrides.onSelectRecommendEntry}
onSelectNextRecommendEntry={overrides.onSelectNextRecommendEntry}
onSelectPreviousRecommendEntry={overrides.onSelectPreviousRecommendEntry}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
/>
@@ -960,7 +980,7 @@ test('shows a reachable login entry in logged out mobile shell', async () => {
expect(openLoginModal).toHaveBeenCalledTimes(1);
});
test('logged out bottom nav keeps creation centered with recommend icon', () => {
test('logged out bottom nav turns active recommend tab into next action', () => {
const { container } = renderLoggedOutHomeView(vi.fn());
const nav = container.querySelector('.platform-bottom-nav');
@@ -968,11 +988,11 @@ test('logged out bottom nav keeps creation centered with recommend icon', () =>
const buttons = within(nav as HTMLElement).getAllByRole('button');
expect(buttons.map((button) => button.textContent)).toEqual([
'推荐',
'下一个',
'创作',
'发现',
]);
expect(buttons[0]?.querySelector('.lucide-gamepad-2')).toBeTruthy();
expect(buttons[0]?.querySelector('.lucide-chevron-down')).toBeTruthy();
expect(buttons[1]?.querySelector('.lucide-sparkles')).toBeTruthy();
expect(buttons[2]?.querySelector('.lucide-compass')).toBeTruthy();
});
@@ -1091,16 +1111,18 @@ test('public gallery cards hide work code until detail is opened', async () => {
expect(onOpenGalleryDetail).toHaveBeenCalledWith(puzzlePublicEntry);
});
test('mobile recommend page renders runtime viewport and bottom switcher', () => {
const onSelectRecommendEntry = vi.fn();
test('mobile recommend page renders runtime viewport without bottom work cards', () => {
const onOpenGalleryDetail = vi.fn();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onSelectRecommendEntry,
onOpenGalleryDetail,
});
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
const runtimePanel = document.querySelector('.platform-recommend-runtime-panel');
expect(runtimePanel).toBeTruthy();
expect(runtimePanel?.className).not.toContain('bg-black');
expect(screen.queryByText('一张用于公开分享的拼图作品。')).toBeNull();
expect(
document.querySelector('.platform-public-work-card__cover'),
@@ -1109,33 +1131,66 @@ test('mobile recommend page renders runtime viewport and bottom switcher', () =>
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
expect(screen.getAllByText('20').length).toBeGreaterThan(0);
expect(screen.getAllByText('12').length).toBeGreaterThan(0);
const switchButton = screen.getByRole('button', {
name: '切换到 奇幻拼图',
});
expect(switchButton.getAttribute('aria-pressed')).toBe('true');
expect(screen.queryByRole('button', { name: '切换到 奇幻拼图' })).toBeNull();
expect(
screen.queryByRole('button', { name: '查看 奇幻拼图 详情' }),
).toBeNull();
expect(
screen.queryByRole('button', { name: '打开 奇幻拼图 详情' }),
).toBeNull();
expect(document.querySelector('.platform-recommend-switcher')).toBeNull();
fireEvent.click(screen.getByLabelText('奇幻拼图 作品信息'));
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('mobile recommend switcher selects a different public work', async () => {
const user = userEvent.setup();
const onSelectRecommendEntry = vi.fn();
const secondEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-second',
profileId: 'puzzle-profile-second',
publicWorkCode: 'PZ-SECOND',
worldName: '第二拼图',
} satisfies PlatformPublicGalleryCard;
test('mobile recommend loading state is themed instead of hardcoded black', () => {
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry, secondEntry],
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onSelectRecommendEntry,
isStartingRecommendEntry: true,
recommendRuntimeContent: null,
});
await user.click(screen.getByRole('button', { name: '切换到 第二拼图' }));
const loadingState = screen.getByText('加载中...');
expect(loadingState.className).toContain('platform-recommend-runtime-state');
expect(loadingState.className).not.toContain('bg-black');
});
expect(onSelectRecommendEntry).toHaveBeenCalledWith(secondEntry);
test('mobile recommend meta swipes between public works', () => {
const onSelectNextRecommendEntry = vi.fn();
const onSelectPreviousRecommendEntry = vi.fn();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onSelectNextRecommendEntry,
onSelectPreviousRecommendEntry,
});
const meta = screen.getByLabelText('奇幻拼图 作品信息');
dispatchClientYPointerEvent(meta, 'pointerdown', 240);
dispatchClientYPointerEvent(meta, 'pointerup', 180);
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
expect(onSelectPreviousRecommendEntry).not.toHaveBeenCalled();
dispatchClientYPointerEvent(meta, 'pointerdown', 180);
dispatchClientYPointerEvent(meta, 'pointerup', 240);
expect(onSelectPreviousRecommendEntry).toHaveBeenCalledTimes(1);
});
test('active recommend bottom tab selects next work instead of navigating', async () => {
const user = userEvent.setup();
const onSelectNextRecommendEntry = vi.fn();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onSelectNextRecommendEntry,
});
await user.click(screen.getByRole('button', { name: '下一个' }));
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
});
test('mobile recommend meta loads real author avatar from public user summary', async () => {
+67 -98
View File
@@ -122,7 +122,8 @@ export interface RpgEntryHomeViewProps {
activeRecommendEntryKey?: string | null;
isStartingRecommendEntry?: boolean;
recommendRuntimeError?: string | null;
onSelectRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
onSelectNextRecommendEntry?: () => void;
onSelectPreviousRecommendEntry?: () => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
@@ -149,6 +150,8 @@ const HERO_SURFACE_CLASS =
'platform-surface platform-surface--hero platform-interactive-card min-w-0';
const MOBILE_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface min-w-0 space-y-4 overflow-hidden pb-2';
const MOBILE_RECOMMEND_PAGE_STAGE_CLASS =
'platform-page-stage min-w-0 space-y-4 overflow-hidden pb-2';
const DESKTOP_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface min-w-0 space-y-5 pb-4';
const DESKTOP_LAYOUT_QUERY = '(min-width: 1024px)';
@@ -165,6 +168,7 @@ const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
@@ -664,12 +668,15 @@ function CreationLibraryCard({
function RecommendRuntimeMeta({
entry,
authorAvatarUrl,
onOpenDetail,
onSelectNext,
onSelectPrevious,
}: {
entry: PlatformPublicGalleryCard;
authorAvatarUrl?: string | null;
onOpenDetail: () => void;
onSelectNext?: () => void;
onSelectPrevious?: () => void;
}) {
const swipeStartYRef = useRef<number | null>(null);
const playCount = getPlatformWorldPlayCount(entry);
const remixCount = getPlatformWorldRemixCount(entry);
const likeCount = getPlatformWorldLikeCount(entry);
@@ -682,11 +689,37 @@ function RecommendRuntimeMeta({
{ label: '点赞', value: likeCount, icon: Heart },
{ label: '改造', value: remixCount, icon: MessageCircle },
];
const handlePointerEnd = (clientY: number) => {
const startY = swipeStartYRef.current;
swipeStartYRef.current = null;
if (startY === null) {
return;
}
const deltaY = clientY - startY;
if (Math.abs(deltaY) < RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX) {
return;
}
if (deltaY < 0) {
onSelectNext?.();
return;
}
onSelectPrevious?.();
};
return (
<section
className="platform-recommend-work-meta"
aria-label={`${entry.worldName} 作品信息`}
onPointerDown={(event) => {
swipeStartYRef.current = event.clientY;
}}
onPointerUp={(event) => handlePointerEnd(event.clientY)}
onPointerCancel={() => {
swipeStartYRef.current = null;
}}
>
<div className="platform-recommend-work-meta__stats">
{statItems.map(({ label, value, icon: Icon }) => (
@@ -702,11 +735,8 @@ function RecommendRuntimeMeta({
</div>
<div className="platform-recommend-work-meta__row">
<button
type="button"
onClick={onOpenDetail}
<div
className="platform-recommend-work-meta__identity"
aria-label={`打开 ${entry.worldName} 详情`}
>
<span
className="platform-recommend-work-meta__avatar"
@@ -730,62 +760,12 @@ function RecommendRuntimeMeta({
{displayName}
</span>
</span>
</button>
<button
type="button"
onClick={onOpenDetail}
className="platform-recommend-work-meta__detail-button"
aria-label={`查看 ${entry.worldName} 详情`}
title="详情"
>
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
</section>
);
}
function RecommendWorkSwitchItem({
entry,
active,
onSelect,
}: {
entry: PlatformPublicGalleryCard;
active: boolean;
onSelect: () => void;
}) {
const displayName = formatPlatformWorkDisplayName(entry.worldName);
const typeLabel = describePublicGalleryCardKind(entry);
const playCount = getPlatformWorldPlayCount(entry);
const likeCount = getPlatformWorldLikeCount(entry);
return (
<button
type="button"
onClick={onSelect}
aria-label={`切换到 ${entry.worldName}`}
aria-pressed={active}
className={`platform-recommend-switch-card ${active ? 'platform-recommend-switch-card--active' : ''}`}
>
<span className="platform-recommend-switch-card__kind">{typeLabel}</span>
<span className="platform-recommend-switch-card__title">
{displayName}
</span>
<span className="platform-recommend-switch-card__stats">
<span>
<Gamepad2 className="h-3 w-3" aria-hidden="true" />
{formatCompactCount(playCount)}
</span>
<span>
<Heart className="h-3 w-3" aria-hidden="true" />
{formatCompactCount(likeCount)}
</span>
</span>
</button>
);
}
function SaveArchiveCard({
entry,
onClick,
@@ -2861,7 +2841,8 @@ export function RpgEntryHomeView({
activeRecommendEntryKey = null,
isStartingRecommendEntry = false,
recommendRuntimeError = null,
onSelectRecommendEntry,
onSelectNextRecommendEntry,
onSelectPreviousRecommendEntry,
onOpenLibraryDetail,
onDeleteLibraryEntry,
deletingLibraryEntryId = null,
@@ -3722,6 +3703,12 @@ export function RpgEntryHomeView({
) ??
recommendedFeedEntries[0] ??
null;
const selectNextRecommendEntry = useCallback(() => {
onSelectNextRecommendEntry?.();
}, [onSelectNextRecommendEntry]);
const selectPreviousRecommendEntry = useCallback(() => {
onSelectPreviousRecommendEntry?.();
}, [onSelectPreviousRecommendEntry]);
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
@@ -3785,7 +3772,7 @@ export function RpgEntryHomeView({
const mobileRecommendContent: ReactNode = (
<div
className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage platform-mobile-recommend-stage`}
className={`${MOBILE_RECOMMEND_PAGE_STAGE_CLASS} platform-mobile-home-stage platform-mobile-recommend-stage`}
>
{platformError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
@@ -3823,42 +3810,9 @@ export function RpgEntryHomeView({
<RecommendRuntimeMeta
entry={activeRecommendEntry}
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
onOpenDetail={() => onOpenGalleryDetail(activeRecommendEntry)}
onSelectNext={selectNextRecommendEntry}
onSelectPrevious={selectPreviousRecommendEntry}
/>
) : null}
{recommendedFeedEntries.length > 0 ? (
<section
className="platform-recommend-switcher"
aria-label="推荐作品"
>
{recommendedFeedEntries.map((entry) => {
const cardKey = buildPublicGalleryCardKey(entry);
const active =
activeRecommendEntryKey === cardKey ||
Boolean(
!activeRecommendEntryKey &&
activeRecommendEntry &&
buildPublicGalleryCardKey(activeRecommendEntry) === cardKey,
);
return (
<RecommendWorkSwitchItem
key={`${cardKey}:recommend-switch`}
entry={entry}
active={active}
onSelect={() => {
if (onSelectRecommendEntry) {
onSelectRecommendEntry(entry);
return;
}
onOpenGalleryDetail(entry);
}}
/>
);
})}
</section>
) : !isLoadingPlatform ? (
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
) : null}
@@ -4706,10 +4660,25 @@ export function RpgEntryHomeView({
<PlatformTabButton
key={tab}
active={activeTab === tab}
label={tabLabels[tab]}
icon={tabIcons[tab]}
label={
activeTab === 'home' && tab === 'home'
? '下一个'
: tabLabels[tab]
}
icon={
activeTab === 'home' && tab === 'home'
? ChevronDown
: tabIcons[tab]
}
emphasized={tab === 'create'}
onClick={() => onTabChange(tab)}
onClick={() => {
if (activeTab === 'home' && tab === 'home') {
selectNextRecommendEntry();
return;
}
onTabChange(tab);
}}
/>
))}
</div>
+310 -114
View File
@@ -505,6 +505,59 @@ body {
),
radial-gradient(circle at right, rgba(255, 205, 178, 0.14), transparent 28%),
linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 241, 246, 0.9));
--platform-recommend-runtime-fill: var(--platform-panel-fill);
--platform-recommend-runtime-border: rgba(232, 191, 205, 0.42);
--platform-recommend-runtime-shadow: 0 18px 44px rgba(215, 87, 134, 0.13),
inset 0 0 0 1px rgba(255, 255, 255, 0.58);
--platform-recommend-runtime-state-fill: radial-gradient(
circle at 50% 18%,
rgba(255, 91, 132, 0.12),
transparent 34%
),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 246, 249, 0.94));
--platform-recommend-runtime-state-text: var(--platform-text-strong);
--puzzle-runtime-shell-fill: var(--platform-body-fill);
--puzzle-runtime-stage-fill: radial-gradient(
circle at 50% 18%,
rgba(255, 91, 132, 0.13),
transparent 30%
),
radial-gradient(circle at 18% 82%, rgba(255, 138, 115, 0.13), transparent 28%),
linear-gradient(180deg, #fffefe 0%, #fff7fa 58%, #fff1f5 100%);
--puzzle-runtime-grid-line: rgba(130, 75, 95, 0.06);
--puzzle-runtime-text-strong: var(--platform-text-strong);
--puzzle-runtime-text-base: var(--platform-text-base);
--puzzle-runtime-text-soft: var(--platform-text-soft);
--puzzle-runtime-surface-fill: rgba(255, 255, 255, 0.76);
--puzzle-runtime-surface-fill-strong: rgba(255, 255, 255, 0.9);
--puzzle-runtime-surface-border: rgba(232, 191, 205, 0.48);
--puzzle-runtime-board-fill: rgba(255, 255, 255, 0.68);
--puzzle-runtime-board-border: rgba(255, 126, 154, 0.28);
--puzzle-runtime-board-shadow: 0 30px 80px rgba(215, 87, 134, 0.14);
--puzzle-runtime-piece-fill: rgba(255, 255, 255, 0.74);
--puzzle-runtime-piece-border: rgba(232, 191, 205, 0.54);
--puzzle-runtime-piece-empty-fill: rgba(255, 228, 236, 0.34);
--puzzle-runtime-piece-empty-text: rgba(92, 70, 80, 0.38);
--puzzle-runtime-piece-selected-fill: linear-gradient(135deg, #ff4f8b, #ff8a73);
--puzzle-runtime-piece-selected-text: #fff7fb;
--puzzle-runtime-piece-selected-border: rgba(255, 79, 139, 0.68);
--puzzle-runtime-piece-overlay: rgba(61, 24, 38, 0.06);
--puzzle-runtime-control-fill: rgba(255, 255, 255, 0.72);
--puzzle-runtime-control-hover-fill: rgba(255, 91, 132, 0.1);
--puzzle-runtime-primary-fill: var(--platform-button-primary-fill);
--puzzle-runtime-primary-text: var(--platform-button-primary-text);
--puzzle-runtime-primary-shadow: var(--platform-profile-action-shadow);
--puzzle-runtime-accent-text: var(--platform-cool-text);
--puzzle-runtime-cool-text: #0f8fa9;
--puzzle-runtime-danger-fill: rgba(255, 228, 233, 0.9);
--puzzle-runtime-danger-text: #c2415d;
--puzzle-runtime-backdrop-fill: rgba(43, 20, 32, 0.34);
--puzzle-runtime-dialog-fill: var(--platform-modal-fill);
--puzzle-runtime-dialog-border: var(--platform-modal-border);
--puzzle-runtime-table-fill: rgba(255, 255, 255, 0.62);
--puzzle-runtime-table-row-fill: rgba(255, 91, 132, 0.12);
--puzzle-runtime-next-card-fill: rgba(255, 255, 255, 0.66);
--puzzle-runtime-next-card-hover-fill: rgba(255, 91, 132, 0.1);
}
.platform-theme--dark {
@@ -684,6 +737,54 @@ body {
),
radial-gradient(circle at right, rgba(255, 205, 178, 0.14), transparent 28%),
linear-gradient(180deg, rgba(8, 10, 14, 0.22), rgba(8, 10, 14, 0.9));
--platform-recommend-runtime-fill: #030303;
--platform-recommend-runtime-border: rgba(255, 255, 255, 0.08);
--platform-recommend-runtime-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.025),
0 18px 44px rgba(0, 0, 0, 0.18);
--platform-recommend-runtime-state-fill: #030303;
--platform-recommend-runtime-state-text: rgba(255, 255, 255, 0.92);
--puzzle-runtime-shell-fill: #020617;
--puzzle-runtime-stage-fill: radial-gradient(
circle at 50% 20%,
rgba(251, 191, 36, 0.18),
transparent 28%
),
radial-gradient(circle at 20% 80%, rgba(249, 115, 22, 0.16), transparent 26%),
linear-gradient(180deg, #2d160e, #020617);
--puzzle-runtime-grid-line: rgba(255, 255, 255, 0.04);
--puzzle-runtime-text-strong: #ffffff;
--puzzle-runtime-text-base: rgba(255, 255, 255, 0.82);
--puzzle-runtime-text-soft: rgba(255, 255, 255, 0.58);
--puzzle-runtime-surface-fill: rgba(0, 0, 0, 0.34);
--puzzle-runtime-surface-fill-strong: rgba(0, 0, 0, 0.5);
--puzzle-runtime-surface-border: rgba(255, 255, 255, 0.12);
--puzzle-runtime-board-fill: rgba(255, 255, 255, 0.08);
--puzzle-runtime-board-border: rgba(255, 255, 255, 0.16);
--puzzle-runtime-board-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
--puzzle-runtime-piece-fill: rgba(255, 255, 255, 0.12);
--puzzle-runtime-piece-border: rgba(255, 255, 255, 0.22);
--puzzle-runtime-piece-empty-fill: rgba(0, 0, 0, 0.18);
--puzzle-runtime-piece-empty-text: rgba(255, 255, 255, 0.2);
--puzzle-runtime-piece-selected-fill: rgba(251, 191, 36, 0.84);
--puzzle-runtime-piece-selected-text: #020617;
--puzzle-runtime-piece-selected-border: rgba(253, 230, 138, 1);
--puzzle-runtime-piece-overlay: rgba(0, 0, 0, 0.1);
--puzzle-runtime-control-fill: rgba(0, 0, 0, 0.36);
--puzzle-runtime-control-hover-fill: rgba(255, 255, 255, 0.1);
--puzzle-runtime-primary-fill: #fde68a;
--puzzle-runtime-primary-text: #020617;
--puzzle-runtime-primary-shadow: 0 14px 36px rgba(251, 191, 36, 0.26);
--puzzle-runtime-accent-text: #fde68a;
--puzzle-runtime-cool-text: #cffafe;
--puzzle-runtime-danger-fill: rgba(239, 68, 68, 0.2);
--puzzle-runtime-danger-text: #fee2e2;
--puzzle-runtime-backdrop-fill: rgba(2, 6, 23, 0.68);
--puzzle-runtime-dialog-fill: rgba(2, 6, 23, 0.94);
--puzzle-runtime-dialog-border: rgba(255, 255, 255, 0.14);
--puzzle-runtime-table-fill: rgba(255, 255, 255, 0.06);
--puzzle-runtime-table-row-fill: rgba(251, 191, 36, 0.14);
--puzzle-runtime-next-card-fill: rgba(255, 255, 255, 0.06);
--puzzle-runtime-next-card-hover-fill: rgba(251, 191, 36, 0.1);
}
.platform-brand-logo {
@@ -1907,6 +2008,205 @@ body {
inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.puzzle-runtime-shell {
background: var(--puzzle-runtime-shell-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-stage {
background: var(--puzzle-runtime-stage-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-stage__grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(var(--puzzle-runtime-grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--puzzle-runtime-grid-line) 1px, transparent 1px);
background-size: 34px 34px;
opacity: 0.8;
}
.puzzle-runtime-pill,
.puzzle-runtime-icon-button,
.puzzle-runtime-header-card,
.puzzle-runtime-toolbar {
border: 1px solid var(--puzzle-runtime-surface-border);
background: var(--puzzle-runtime-surface-fill);
color: var(--puzzle-runtime-text-strong);
backdrop-filter: blur(14px);
}
.puzzle-runtime-icon-button {
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-header-card {
background: var(--puzzle-runtime-surface-fill-strong);
}
.puzzle-runtime-level-badge {
background: var(--puzzle-runtime-control-fill);
color: var(--puzzle-runtime-accent-text);
}
.puzzle-runtime-timer {
background: var(--puzzle-runtime-control-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-timer--urgent {
background: var(--puzzle-runtime-danger-fill);
color: var(--puzzle-runtime-danger-text);
}
.puzzle-runtime-board {
border-color: var(--puzzle-runtime-board-border);
background: var(--puzzle-runtime-board-fill);
box-shadow: var(--puzzle-runtime-board-shadow);
}
.puzzle-runtime-piece {
border-color: var(--puzzle-runtime-piece-border);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-piece--selected {
border-color: var(--puzzle-runtime-piece-selected-border);
background: var(--puzzle-runtime-piece-selected-fill);
color: var(--puzzle-runtime-piece-selected-text);
box-shadow: var(--puzzle-runtime-primary-shadow);
}
.puzzle-runtime-piece--merged {
border-color: transparent;
background: transparent;
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-piece--filled {
background: var(--puzzle-runtime-piece-fill);
}
.puzzle-runtime-piece--empty {
border-color: var(--puzzle-runtime-surface-border);
background: var(--puzzle-runtime-piece-empty-fill);
color: var(--puzzle-runtime-piece-empty-text);
}
.puzzle-runtime-piece-overlay {
background: var(--puzzle-runtime-piece-overlay);
}
.puzzle-runtime-primary-button {
border: 1px solid var(--platform-button-primary-border);
background: var(--puzzle-runtime-primary-fill);
color: var(--puzzle-runtime-primary-text);
box-shadow: var(--puzzle-runtime-primary-shadow);
}
.puzzle-runtime-tool-button {
color: var(--puzzle-runtime-text-base);
}
.puzzle-runtime-tool-button:hover {
background: var(--puzzle-runtime-control-hover-fill);
}
.puzzle-runtime-tool-button--active {
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.puzzle-runtime-tool-button__warm {
color: var(--puzzle-runtime-accent-text);
}
.puzzle-runtime-tool-button__cool {
color: var(--puzzle-runtime-cool-text);
}
.puzzle-runtime-status-chip {
background: var(--puzzle-runtime-control-fill);
color: var(--puzzle-runtime-text-soft);
backdrop-filter: blur(12px);
}
.puzzle-runtime-error-chip {
background: var(--puzzle-runtime-danger-fill);
color: var(--puzzle-runtime-danger-text);
}
.puzzle-runtime-modal-overlay {
background: var(--puzzle-runtime-backdrop-fill);
}
.puzzle-runtime-dialog {
border: 1px solid var(--puzzle-runtime-dialog-border);
background: var(--puzzle-runtime-dialog-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-dialog__line {
border-color: var(--puzzle-runtime-surface-border);
}
.puzzle-runtime-dialog__soft {
color: var(--puzzle-runtime-text-soft);
}
.puzzle-runtime-dialog__body {
color: var(--puzzle-runtime-text-base);
}
.puzzle-runtime-secondary-button {
border: 1px solid var(--puzzle-runtime-surface-border);
background: var(--puzzle-runtime-control-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-stat-card,
.puzzle-runtime-settings-card {
border: 1px solid var(--puzzle-runtime-surface-border);
background: var(--puzzle-runtime-table-fill);
}
.puzzle-runtime-leaderboard-head {
background: var(--puzzle-runtime-table-fill);
color: var(--puzzle-runtime-text-soft);
}
.puzzle-runtime-leaderboard-row {
border-color: var(--puzzle-runtime-surface-border);
color: var(--puzzle-runtime-text-base);
}
.puzzle-runtime-leaderboard-row--active {
background: var(--puzzle-runtime-table-row-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-next-card {
border: 1px solid var(--puzzle-runtime-surface-border);
background: var(--puzzle-runtime-next-card-fill);
color: var(--puzzle-runtime-text-strong);
}
.puzzle-runtime-next-card:hover {
border-color: var(--platform-button-primary-border);
background: var(--puzzle-runtime-next-card-hover-fill);
}
.puzzle-runtime-next-card__media {
background: var(--puzzle-runtime-control-fill);
}
.puzzle-runtime-next-card__tag {
background: var(--puzzle-runtime-control-fill);
color: var(--puzzle-runtime-text-soft);
}
@media (max-width: 639px) {
:root {
--platform-bottom-nav-height: 3.85rem;
@@ -2095,7 +2395,7 @@ body {
height: 100%;
min-height: 0;
flex-direction: column;
gap: 0.55rem;
gap: 0.28rem;
border: 0;
border-radius: 0;
background: transparent;
@@ -2109,12 +2409,10 @@ body {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--platform-recommend-runtime-border);
border-radius: 1.65rem;
background: #030303;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.025),
0 18px 44px rgba(0, 0, 0, 0.18);
background: var(--platform-recommend-runtime-fill);
box-shadow: var(--platform-recommend-runtime-shadow);
}
.platform-recommend-runtime-viewport {
@@ -2122,7 +2420,7 @@ body {
inset: 0;
min-width: 0;
overflow: hidden;
background: #030303;
background: var(--platform-recommend-runtime-fill);
}
.platform-recommend-runtime-state {
@@ -2132,8 +2430,8 @@ body {
width: 100%;
border: 0;
place-items: center;
background: #030303;
color: rgba(255, 255, 255, 0.92);
background: var(--platform-recommend-runtime-state-fill);
color: var(--platform-recommend-runtime-state-text);
font-size: clamp(1.8rem, 10vw, 2.45rem);
font-weight: 950;
letter-spacing: 0;
@@ -2148,6 +2446,8 @@ body {
flex: 0 0 auto;
min-width: 0;
color: var(--platform-text-strong);
touch-action: pan-y;
user-select: none;
}
.platform-recommend-work-meta__stats {
@@ -2170,7 +2470,7 @@ body {
}
.platform-recommend-work-meta__row {
margin-top: 0.55rem;
margin-top: 0.36rem;
display: flex;
min-width: 0;
align-items: center;
@@ -2184,9 +2484,6 @@ body {
flex: 1 1 auto;
align-items: center;
gap: 0.55rem;
border: 0;
background: transparent;
padding: 0;
color: inherit;
text-align: left;
}
@@ -2231,107 +2528,6 @@ body {
color: var(--platform-text-soft);
}
.platform-recommend-work-meta__detail-button {
display: inline-flex;
width: 2.35rem;
height: 2.35rem;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border: 1px solid var(--platform-surface-border);
border-radius: 9999px;
background: var(--platform-button-secondary-fill);
color: var(--platform-text-strong);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
.platform-recommend-switcher {
display: flex;
flex: 0 0 auto;
min-width: 0;
gap: 0.55rem;
margin-right: -0.25rem;
overflow-x: auto;
overscroll-behavior-x: contain;
padding: 0.05rem 0.25rem 0.2rem 0;
scroll-snap-type: x mandatory;
scrollbar-width: none;
touch-action: pan-x;
-webkit-overflow-scrolling: touch;
}
.platform-recommend-switcher::-webkit-scrollbar {
display: none;
}
.platform-recommend-switch-card {
display: grid;
width: min(9.1rem, 42vw);
min-height: 4.25rem;
flex: 0 0 auto;
scroll-snap-align: start;
gap: 0.18rem;
border: 1px solid color-mix(in srgb, var(--platform-surface-border) 72%, transparent);
border-radius: 1.05rem;
background: color-mix(in srgb, var(--platform-panel-fill) 90%, #050505);
padding: 0.58rem 0.65rem;
color: var(--platform-text-strong);
text-align: left;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12);
}
.platform-recommend-switch-card--active {
border-color: color-mix(
in srgb,
var(--platform-warm-text) 72%,
var(--platform-surface-border)
);
background: color-mix(
in srgb,
var(--platform-button-secondary-fill) 84%,
#151515
);
box-shadow:
0 14px 34px rgba(0, 0, 0, 0.16),
inset 0 0 0 1px color-mix(in srgb, var(--platform-warm-text) 42%, transparent);
}
.platform-recommend-switch-card__kind {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.64rem;
font-weight: 850;
color: var(--platform-text-soft);
}
.platform-recommend-switch-card__title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.88rem;
font-weight: 950;
line-height: 1.15;
}
.platform-recommend-switch-card__stats {
display: flex;
min-width: 0;
gap: 0.55rem;
font-size: 0.68rem;
font-weight: 850;
color: var(--platform-text-base);
}
.platform-recommend-switch-card__stats span {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 0.18rem;
}
.platform-mobile-home-stage .platform-desktop-search {
border-radius: 9999px;
padding: 0.64rem 0.9rem;