收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -15,6 +15,8 @@ import type {
|
||||
PuzzleClearRuntimeSnapshotResponse,
|
||||
PuzzleClearWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/puzzleClear';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type PuzzleClearRuntimeShellProps = {
|
||||
@@ -176,7 +178,9 @@ function readCellPositionFromElement(element: Element | null) {
|
||||
return { row, col };
|
||||
}
|
||||
|
||||
function resolvePointerReleaseCell(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
function resolvePointerReleaseCell(
|
||||
event: ReactPointerEvent<HTMLButtonElement>,
|
||||
) {
|
||||
if (typeof document.elementFromPoint !== 'function') {
|
||||
return null;
|
||||
}
|
||||
@@ -218,9 +222,7 @@ function mergeRects(rects: PuzzleClearRect[]) {
|
||||
}
|
||||
const left = Math.min(...visibleRects.map((rect) => rect.left));
|
||||
const top = Math.min(...visibleRects.map((rect) => rect.top));
|
||||
const right = Math.max(
|
||||
...visibleRects.map((rect) => rect.left + rect.width),
|
||||
);
|
||||
const right = Math.max(...visibleRects.map((rect) => rect.left + rect.width));
|
||||
const bottom = Math.max(
|
||||
...visibleRects.map((rect) => rect.top + rect.height),
|
||||
);
|
||||
@@ -249,7 +251,9 @@ function isSameGridPosition(
|
||||
left: PuzzleClearGridPosition | null | undefined,
|
||||
right: PuzzleClearGridPosition | null | undefined,
|
||||
) {
|
||||
return Boolean(left && right && left.row === right.row && left.col === right.col);
|
||||
return Boolean(
|
||||
left && right && left.row === right.row && left.col === right.col,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBoardPreviewState(
|
||||
@@ -357,11 +361,14 @@ function buildPuzzleClearClearTransition(
|
||||
}
|
||||
}
|
||||
const nextCardIds = new Set(
|
||||
nextRun.board.cells.flatMap((cell) => (cell.card ? [cell.card.cardId] : [])),
|
||||
nextRun.board.cells.flatMap((cell) =>
|
||||
cell.card ? [cell.card.cardId] : [],
|
||||
),
|
||||
);
|
||||
const clearedCells = previousRun.board.cells
|
||||
.filter((cell): cell is PuzzleClearBoardCell & { card: PuzzleClearCardAsset } =>
|
||||
Boolean(cell.card),
|
||||
.filter(
|
||||
(cell): cell is PuzzleClearBoardCell & { card: PuzzleClearCardAsset } =>
|
||||
Boolean(cell.card),
|
||||
)
|
||||
.filter((cell) => !nextCardIds.has(cell.card.cardId))
|
||||
.map((cell) => ({
|
||||
@@ -420,7 +427,9 @@ function buildPuzzleClearClearTransition(
|
||||
if (!previousPosition) {
|
||||
return false;
|
||||
}
|
||||
return previousPosition.row !== cell.row || previousPosition.col !== cell.col;
|
||||
return (
|
||||
previousPosition.row !== cell.row || previousPosition.col !== cell.col
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -450,19 +459,19 @@ export function PuzzleClearRuntimeShell({
|
||||
const dragDropTimerRef = useRef<number | null>(null);
|
||||
const swapFeedbackTimerRef = useRef<number | null>(null);
|
||||
const swapFlightTimerRef = useRef<number | null>(null);
|
||||
const previousRunRef = useRef<PuzzleClearRuntimeSnapshotResponse | null>(null);
|
||||
const previousRunRef = useRef<PuzzleClearRuntimeSnapshotResponse | null>(
|
||||
null,
|
||||
);
|
||||
const clearTransitionTimerRef = useRef<number | null>(null);
|
||||
const mergeHighlightTimerRef = useRef<number | null>(null);
|
||||
const cellElementRefMap = useRef(new Map<string, HTMLButtonElement>());
|
||||
const [selectedCell, setSelectedCell] = useState<PuzzleClearGridPosition | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedCell, setSelectedCell] =
|
||||
useState<PuzzleClearGridPosition | null>(null);
|
||||
const [dragState, setDragState] = useState<PuzzleClearDragState | null>(null);
|
||||
const [swapFeedback, setSwapFeedback] =
|
||||
useState<PuzzleClearSwapFeedbackState | null>(null);
|
||||
const [swapFlight, setSwapFlight] = useState<PuzzleClearSwapFlightState | null>(
|
||||
null,
|
||||
);
|
||||
const [swapFlight, setSwapFlight] =
|
||||
useState<PuzzleClearSwapFlightState | null>(null);
|
||||
const [clearTransition, setClearTransition] =
|
||||
useState<PuzzleClearClearTransitionState | null>(null);
|
||||
const [mergeHighlight, setMergeHighlight] =
|
||||
@@ -551,7 +560,10 @@ export function PuzzleClearRuntimeShell({
|
||||
'';
|
||||
const cardBackSrc = profile?.draft.cardBackImageSrc?.trim() || '';
|
||||
const fallbackCardBackSrc = '/creation-type-references/puzzle.webp';
|
||||
const effectiveCardBackSrc = resolveCardBackSrc(cardBackSrc, fallbackCardBackSrc);
|
||||
const effectiveCardBackSrc = resolveCardBackSrc(
|
||||
cardBackSrc,
|
||||
fallbackCardBackSrc,
|
||||
);
|
||||
const isPlaying = activeRun?.status === 'playing';
|
||||
const hasWonLevel = activeRun?.status === 'level_cleared';
|
||||
const hasFinished = activeRun?.status === 'finished';
|
||||
@@ -571,12 +583,14 @@ export function PuzzleClearRuntimeShell({
|
||||
dragState.phase === 'dropping'
|
||||
? `translate(${dragState.targetRect ? dragState.targetRect.left - (dragState.currentX - dragState.pointerOffsetX) : 0}px, ${
|
||||
dragState.targetRect
|
||||
? dragState.targetRect.top - (dragState.currentY - dragState.pointerOffsetY)
|
||||
? dragState.targetRect.top -
|
||||
(dragState.currentY - dragState.pointerOffsetY)
|
||||
: 0
|
||||
}px) scale(0.94)`
|
||||
: dragState.phase === 'returning'
|
||||
? `translate(${dragState.sourceRect.left - (dragState.currentX - dragState.pointerOffsetX)}px, ${
|
||||
dragState.sourceRect.top - (dragState.currentY - dragState.pointerOffsetY)
|
||||
dragState.sourceRect.top -
|
||||
(dragState.currentY - dragState.pointerOffsetY)
|
||||
}px) scale(1)`
|
||||
: 'translate3d(0,0,0) scale(1.04)',
|
||||
opacity: dragState.phase === 'returning' ? 0.86 : 0.98,
|
||||
@@ -842,25 +856,25 @@ export function PuzzleClearRuntimeShell({
|
||||
const sourceGroupId = target.lockedGroupId;
|
||||
const sourceGroup =
|
||||
sourceGroupId !== null
|
||||
? lockedGroups.find((group) => group.groupId === sourceGroupId) ?? null
|
||||
? (lockedGroups.find((group) => group.groupId === sourceGroupId) ??
|
||||
null)
|
||||
: null;
|
||||
const sourceRect =
|
||||
(sourceGroup
|
||||
? mergeRects(
|
||||
sourceGroup.cells
|
||||
.map((entry) =>
|
||||
readElementRect(
|
||||
cellElementRefMap.current.get(getCellKey(entry.row, entry.col)),
|
||||
),
|
||||
)
|
||||
.filter((rect): rect is PuzzleClearRect => rect !== null),
|
||||
)
|
||||
: readElementRect(event.currentTarget)) ?? {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
const sourceRect = (sourceGroup
|
||||
? mergeRects(
|
||||
sourceGroup.cells
|
||||
.map((entry) =>
|
||||
readElementRect(
|
||||
cellElementRefMap.current.get(getCellKey(entry.row, entry.col)),
|
||||
),
|
||||
)
|
||||
.filter((rect): rect is PuzzleClearRect => rect !== null),
|
||||
)
|
||||
: readElementRect(event.currentTarget)) ?? {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
setDragState({
|
||||
card: target.card,
|
||||
from: origin,
|
||||
@@ -876,7 +890,9 @@ export function PuzzleClearRuntimeShell({
|
||||
});
|
||||
};
|
||||
|
||||
const handleCellPointerMove = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
const handleCellPointerMove = (
|
||||
event: ReactPointerEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
if (dragPointerIdRef.current !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
@@ -928,9 +944,7 @@ export function PuzzleClearRuntimeShell({
|
||||
scheduleDragFeedbackReset(140);
|
||||
return;
|
||||
}
|
||||
const target = cells.get(
|
||||
getCellKey(releaseTarget.row, releaseTarget.col),
|
||||
);
|
||||
const target = cells.get(getCellKey(releaseTarget.row, releaseTarget.col));
|
||||
if (!target) {
|
||||
setSelectedCell(null);
|
||||
setDragState((current) =>
|
||||
@@ -953,7 +967,9 @@ export function PuzzleClearRuntimeShell({
|
||||
suppressNextClickRef.current = false;
|
||||
}, 0);
|
||||
const targetRect = readElementRect(
|
||||
cellElementRefMap.current.get(getCellKey(releaseTarget.row, releaseTarget.col)),
|
||||
cellElementRefMap.current.get(
|
||||
getCellKey(releaseTarget.row, releaseTarget.col),
|
||||
),
|
||||
);
|
||||
const sourceRect =
|
||||
dragState?.sourceRect ??
|
||||
@@ -973,7 +989,7 @@ export function PuzzleClearRuntimeShell({
|
||||
currentY: point.y,
|
||||
targetRect,
|
||||
target: releaseTarget,
|
||||
}
|
||||
}
|
||||
: current,
|
||||
);
|
||||
if (targetRect && sourceRect && sourceCard) {
|
||||
@@ -1000,26 +1016,28 @@ export function PuzzleClearRuntimeShell({
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(248,251,255,0.92),rgba(236,253,245,0.9))]" />
|
||||
|
||||
<header className="relative z-20 flex items-center justify-between gap-2 px-3 pb-2 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-4">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onBack}
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full bg-white/82 px-3 py-2 text-sm shadow-sm backdrop-blur"
|
||||
tone="ghost"
|
||||
shape="pill"
|
||||
className="min-h-0 bg-white/82 px-3 py-2 text-sm shadow-sm backdrop-blur"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
<div className="rounded-full border border-white/70 bg-white/86 px-3 py-2 text-sm font-black shadow-sm backdrop-blur">
|
||||
{formatTimer(secondsLeft)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onRetryLevel}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full bg-white/82 px-3 py-2 text-sm shadow-sm backdrop-blur"
|
||||
tone="ghost"
|
||||
shape="pill"
|
||||
className="min-h-0 bg-white/82 px-3 py-2 text-sm shadow-sm backdrop-blur"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重试
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</header>
|
||||
|
||||
<main className="relative z-10 mx-auto flex w-full max-w-[34rem] flex-1 flex-col px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-4">
|
||||
@@ -1089,133 +1107,146 @@ export function PuzzleClearRuntimeShell({
|
||||
className="pointer-events-none absolute inset-0 h-full w-full select-none object-cover"
|
||||
/>
|
||||
) : null}
|
||||
{Array.from({ length: (board?.rows ?? 3) * (board?.cols ?? 3) }, (_, index) => {
|
||||
const cols = board?.cols ?? 3;
|
||||
const row = Math.floor(index / cols);
|
||||
const col = index % cols;
|
||||
const cell = cells.get(getCellKey(row, col));
|
||||
const card = cell?.card ?? null;
|
||||
const isDragOrigin = isSameGridPosition(
|
||||
boardPreview.dragOrigin,
|
||||
{ row, col },
|
||||
);
|
||||
const isDraggedGroupCell = draggedGroupCellKeys.has(
|
||||
getCellKey(row, col),
|
||||
);
|
||||
const selected =
|
||||
selectedCell?.row === row && selectedCell?.col === col;
|
||||
const isSwapFeedbackCell =
|
||||
boardPreview.swapFeedback &&
|
||||
((boardPreview.swapFeedback.from.row === row &&
|
||||
boardPreview.swapFeedback.from.col === col) ||
|
||||
(boardPreview.swapFeedback.to.row === row &&
|
||||
boardPreview.swapFeedback.to.col === col));
|
||||
const style = {
|
||||
...getCardImageStyle(card),
|
||||
'--puzzle-clear-card-index': index,
|
||||
} as CSSProperties;
|
||||
{Array.from(
|
||||
{ length: (board?.rows ?? 3) * (board?.cols ?? 3) },
|
||||
(_, index) => {
|
||||
const cols = board?.cols ?? 3;
|
||||
const row = Math.floor(index / cols);
|
||||
const col = index % cols;
|
||||
const cell = cells.get(getCellKey(row, col));
|
||||
const card = cell?.card ?? null;
|
||||
const isDragOrigin = isSameGridPosition(
|
||||
boardPreview.dragOrigin,
|
||||
{ row, col },
|
||||
);
|
||||
const isDraggedGroupCell = draggedGroupCellKeys.has(
|
||||
getCellKey(row, col),
|
||||
);
|
||||
const selected =
|
||||
selectedCell?.row === row && selectedCell?.col === col;
|
||||
const isSwapFeedbackCell =
|
||||
boardPreview.swapFeedback &&
|
||||
((boardPreview.swapFeedback.from.row === row &&
|
||||
boardPreview.swapFeedback.from.col === col) ||
|
||||
(boardPreview.swapFeedback.to.row === row &&
|
||||
boardPreview.swapFeedback.to.col === col));
|
||||
const style = {
|
||||
...getCardImageStyle(card),
|
||||
'--puzzle-clear-card-index': index,
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={getCellKey(row, col)}
|
||||
ref={(node) => {
|
||||
const key = getCellKey(row, col);
|
||||
if (node) {
|
||||
cellElementRefMap.current.set(key, node);
|
||||
return;
|
||||
return (
|
||||
<button
|
||||
key={getCellKey(row, col)}
|
||||
ref={(node) => {
|
||||
const key = getCellKey(row, col);
|
||||
if (node) {
|
||||
cellElementRefMap.current.set(key, node);
|
||||
return;
|
||||
}
|
||||
cellElementRefMap.current.delete(key);
|
||||
}}
|
||||
type="button"
|
||||
disabled={!isPlaying || isBusy || isTransitioning}
|
||||
onPointerDown={(event) =>
|
||||
handleCellPointerDown(event, row, col)
|
||||
}
|
||||
cellElementRefMap.current.delete(key);
|
||||
}}
|
||||
type="button"
|
||||
disabled={!isPlaying || isBusy || isTransitioning}
|
||||
onPointerDown={(event) => handleCellPointerDown(event, row, col)}
|
||||
onPointerMove={handleCellPointerMove}
|
||||
onPointerCancel={() => {
|
||||
dragOriginRef.current = null;
|
||||
dragPointerIdRef.current = null;
|
||||
setDragState(null);
|
||||
setSelectedCell(null);
|
||||
}}
|
||||
onPointerUp={(event) => handleCellPointerUp(event, row, col)}
|
||||
onLostPointerCapture={(event) => {
|
||||
if (dragPointerIdRef.current !== event.pointerId) {
|
||||
return;
|
||||
onPointerMove={handleCellPointerMove}
|
||||
onPointerCancel={() => {
|
||||
dragOriginRef.current = null;
|
||||
dragPointerIdRef.current = null;
|
||||
setDragState(null);
|
||||
setSelectedCell(null);
|
||||
}}
|
||||
onPointerUp={(event) =>
|
||||
handleCellPointerUp(event, row, col)
|
||||
}
|
||||
if (dragReleasePointerIdRef.current === event.pointerId) {
|
||||
return;
|
||||
}
|
||||
dragOriginRef.current = null;
|
||||
dragPointerIdRef.current = null;
|
||||
setSelectedCell(null);
|
||||
setDragState(null);
|
||||
}}
|
||||
onClick={() => handleCellClick(row, col)}
|
||||
data-puzzle-clear-cell="true"
|
||||
data-puzzle-clear-row={row}
|
||||
data-puzzle-clear-col={col}
|
||||
draggable={false}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
className={`puzzle-clear-card relative z-10 grid min-h-0 touch-none select-none place-items-center overflow-hidden rounded-[0.6rem] border transition ${
|
||||
card ? 'bg-white/78 p-0.5 shadow-sm' : 'bg-transparent p-0 shadow-none'
|
||||
} ${
|
||||
selected
|
||||
? 'border-emerald-500 ring-2 ring-emerald-300'
|
||||
: card
|
||||
? 'border-white/80'
|
||||
: 'border-white/25'
|
||||
} ${
|
||||
cell?.lockedGroupId && !isDraggedGroupCell
|
||||
? 'puzzle-clear-card--locked-group-cell'
|
||||
: ''
|
||||
} ${
|
||||
card && showOpeningPhase ? 'puzzle-clear-card--opening' : ''
|
||||
} ${
|
||||
hasWonLevel || hasFinished
|
||||
? 'puzzle-clear-card--cleared'
|
||||
: ''
|
||||
} ${
|
||||
isDragOrigin
|
||||
? 'puzzle-clear-card--ghost-origin puzzle-clear-card--drag-origin-empty'
|
||||
: ''
|
||||
} ${
|
||||
isDraggedGroupCell && !isDragOrigin
|
||||
? 'puzzle-clear-card--drag-group-empty'
|
||||
: ''
|
||||
} ${
|
||||
isSwapFeedbackCell ? 'puzzle-clear-card--swap-feedback' : ''
|
||||
} ${
|
||||
cell?.lockedGroupId && !isDraggedGroupCell
|
||||
? 'puzzle-clear-card--merge-feedback'
|
||||
: ''
|
||||
} ${
|
||||
clearTransitionCellKeys.dropping.has(getCellKey(row, col))
|
||||
? 'puzzle-clear-card--transition-hidden'
|
||||
: ''
|
||||
}`}
|
||||
style={style}
|
||||
aria-label={`卡片 ${row + 1}-${col + 1}`}
|
||||
>
|
||||
{card && !isDraggedGroupCell ? (
|
||||
<span className="absolute inset-1 rounded-[0.48rem] bg-[var(--puzzle-clear-card-tone)] opacity-45" />
|
||||
) : null}
|
||||
{card?.imageSrc &&
|
||||
!isDragOrigin &&
|
||||
!isDraggedGroupCell &&
|
||||
!cell?.lockedGroupId ? (
|
||||
<ResolvedAssetImage
|
||||
src={card.imageSrc}
|
||||
alt=""
|
||||
draggable={false}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
className="pointer-events-none relative z-10 h-full w-full select-none rounded-[0.48rem] object-cover"
|
||||
/>
|
||||
) : null}
|
||||
{isDragOrigin ? (
|
||||
<span className="relative z-10 h-full w-full rounded-[0.48rem] border border-dashed border-white/28 bg-white/8" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
onLostPointerCapture={(event) => {
|
||||
if (dragPointerIdRef.current !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
if (dragReleasePointerIdRef.current === event.pointerId) {
|
||||
return;
|
||||
}
|
||||
dragOriginRef.current = null;
|
||||
dragPointerIdRef.current = null;
|
||||
setSelectedCell(null);
|
||||
setDragState(null);
|
||||
}}
|
||||
onClick={() => handleCellClick(row, col)}
|
||||
data-puzzle-clear-cell="true"
|
||||
data-puzzle-clear-row={row}
|
||||
data-puzzle-clear-col={col}
|
||||
draggable={false}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
className={`puzzle-clear-card relative z-10 grid min-h-0 touch-none select-none place-items-center overflow-hidden rounded-[0.6rem] border transition ${
|
||||
card
|
||||
? 'bg-white/78 p-0.5 shadow-sm'
|
||||
: 'bg-transparent p-0 shadow-none'
|
||||
} ${
|
||||
selected
|
||||
? 'border-emerald-500 ring-2 ring-emerald-300'
|
||||
: card
|
||||
? 'border-white/80'
|
||||
: 'border-white/25'
|
||||
} ${
|
||||
cell?.lockedGroupId && !isDraggedGroupCell
|
||||
? 'puzzle-clear-card--locked-group-cell'
|
||||
: ''
|
||||
} ${
|
||||
card && showOpeningPhase
|
||||
? 'puzzle-clear-card--opening'
|
||||
: ''
|
||||
} ${
|
||||
hasWonLevel || hasFinished
|
||||
? 'puzzle-clear-card--cleared'
|
||||
: ''
|
||||
} ${
|
||||
isDragOrigin
|
||||
? 'puzzle-clear-card--ghost-origin puzzle-clear-card--drag-origin-empty'
|
||||
: ''
|
||||
} ${
|
||||
isDraggedGroupCell && !isDragOrigin
|
||||
? 'puzzle-clear-card--drag-group-empty'
|
||||
: ''
|
||||
} ${
|
||||
isSwapFeedbackCell
|
||||
? 'puzzle-clear-card--swap-feedback'
|
||||
: ''
|
||||
} ${
|
||||
cell?.lockedGroupId && !isDraggedGroupCell
|
||||
? 'puzzle-clear-card--merge-feedback'
|
||||
: ''
|
||||
} ${
|
||||
clearTransitionCellKeys.dropping.has(getCellKey(row, col))
|
||||
? 'puzzle-clear-card--transition-hidden'
|
||||
: ''
|
||||
}`}
|
||||
style={style}
|
||||
aria-label={`卡片 ${row + 1}-${col + 1}`}
|
||||
>
|
||||
{card && !isDraggedGroupCell ? (
|
||||
<span className="absolute inset-1 rounded-[0.48rem] bg-[var(--puzzle-clear-card-tone)] opacity-45" />
|
||||
) : null}
|
||||
{card?.imageSrc &&
|
||||
!isDragOrigin &&
|
||||
!isDraggedGroupCell &&
|
||||
!cell?.lockedGroupId ? (
|
||||
<ResolvedAssetImage
|
||||
src={card.imageSrc}
|
||||
alt=""
|
||||
draggable={false}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
className="pointer-events-none relative z-10 h-full w-full select-none rounded-[0.48rem] object-cover"
|
||||
/>
|
||||
) : null}
|
||||
{isDragOrigin ? (
|
||||
<span className="relative z-10 h-full w-full rounded-[0.48rem] border border-dashed border-white/28 bg-white/8" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{lockedGroups.length > 0 ? (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
@@ -1304,15 +1335,16 @@ export function PuzzleClearRuntimeShell({
|
||||
<div
|
||||
key={`drop:${clearTransition.transitionKey}:${cell.row}:${cell.col}:${cell.card.cardId}`}
|
||||
className="puzzle-clear-transition-piece puzzle-clear-transition-piece--drop overflow-hidden rounded-[0.6rem] border border-white/72 bg-white/70"
|
||||
style={{
|
||||
gridColumn: cell.col + 1,
|
||||
gridRow: cell.row + 1,
|
||||
'--puzzle-clear-drop-delay': `${cell.delayMs}ms`,
|
||||
'--puzzle-clear-drop-start-y': `calc(-120% - ${Math.max(
|
||||
0,
|
||||
cell.distance - 1,
|
||||
) * 25}%)`,
|
||||
} as CSSProperties}
|
||||
style={
|
||||
{
|
||||
gridColumn: cell.col + 1,
|
||||
gridRow: cell.row + 1,
|
||||
'--puzzle-clear-drop-delay': `${cell.delayMs}ms`,
|
||||
'--puzzle-clear-drop-start-y': `calc(-120% - ${
|
||||
Math.max(0, cell.distance - 1) * 25
|
||||
}%)`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<ResolvedAssetImage
|
||||
src={cell.card.imageSrc}
|
||||
@@ -1425,9 +1457,13 @@ export function PuzzleClearRuntimeShell({
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
className="mt-3 rounded-2xl text-sm leading-6"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
@@ -1446,25 +1482,24 @@ export function PuzzleClearRuntimeShell({
|
||||
{hasFailed ? '本关失败' : hasFinished ? '已完成' : '本关完成'}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onRetryLevel}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--ghost min-h-11 justify-center bg-white px-3 py-2 text-sm"
|
||||
tone="ghost"
|
||||
className="min-h-11 justify-center bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
onClick={hasFinished || hasFailed ? onBack : onNextLevel}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--primary min-h-11 justify-center gap-2 px-3 py-2 text-sm"
|
||||
className="min-h-11 justify-center gap-2 px-3 py-2 text-sm"
|
||||
>
|
||||
{hasFinished || hasFailed ? '返回' : '下一关'}
|
||||
{!hasFinished && !hasFailed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : null}
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user