refactor: replace preset detail modal layoutId morph with bottom sheet slide-up + category crossfade
- Remove layoutId from preset card in game-page-client.tsx and modal in preset-detail-modal.tsx to eliminate text smearing during morph animation. - Replace modal scale/opacity spring animation with bottom sheet y-slide spring (y: 100% → 0). - Wrap settings table content in AnimatePresence mode='wait' with keyed motion.div for crossfade when switching categories.
This commit is contained in:
@@ -751,7 +751,6 @@ export function GamePageClient({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={preset.id}
|
key={preset.id}
|
||||||
layoutId={preset.id}
|
|
||||||
onClick={() => setSelectedPresetId(preset.id)}
|
onClick={() => setSelectedPresetId(preset.id)}
|
||||||
className={`shrink-0 w-72 flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer hover:border-primary/30 ${
|
className={`shrink-0 w-72 flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer hover:border-primary/30 ${
|
||||||
raw
|
raw
|
||||||
|
|||||||
@@ -164,14 +164,13 @@ export function PresetDetailModal({
|
|||||||
>
|
>
|
||||||
{/* Modal card */}
|
{/* Modal card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId={preset.id}
|
|
||||||
className="relative w-full max-w-4xl max-h-[90vh] overflow-hidden rounded-xl border border-border bg-background flex flex-col"
|
className="relative w-full max-w-4xl max-h-[90vh] overflow-hidden rounded-xl border border-border bg-background flex flex-col"
|
||||||
initial={{ scale: 0.95, opacity: 0 }}
|
initial={{ y: "100%", opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
exit={{ scale: 0.95, opacity: 0 }}
|
exit={{ y: "100%", opacity: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: "spring",
|
||||||
damping: 25,
|
damping: 30,
|
||||||
stiffness: 300,
|
stiffness: 300,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -427,62 +426,71 @@ export function PresetDetailModal({
|
|||||||
{/* Right panel */}
|
{/* Right panel */}
|
||||||
<div className="flex-1 overflow-y-auto p-5">
|
<div className="flex-1 overflow-y-auto p-5">
|
||||||
{hasCategories ? (
|
{hasCategories ? (
|
||||||
<div className="flex flex-col gap-4">
|
<AnimatePresence mode="wait">
|
||||||
{/* Category navigation */}
|
<motion.div
|
||||||
<div className="flex items-center justify-between">
|
key={currentCategory?.category ?? "empty"}
|
||||||
<button
|
initial={{ opacity: 0 }}
|
||||||
onClick={goPrevCategory}
|
animate={{ opacity: 1 }}
|
||||||
className="p-1.5 rounded-md hover:bg-text/5 transition-colors cursor-pointer"
|
exit={{ opacity: 0 }}
|
||||||
aria-label="Previous category"
|
transition={{ duration: 0.15 }}
|
||||||
>
|
className="flex flex-col gap-4"
|
||||||
<ChevronLeftIcon className="h-4 w-4 text-text/50" />
|
>
|
||||||
</button>
|
{/* Category navigation */}
|
||||||
<span className="text-sm font-medium text-text/80">
|
<div className="flex items-center justify-between">
|
||||||
{currentCategory?.category}{" "}
|
<button
|
||||||
<span className="text-text/40">
|
onClick={goPrevCategory}
|
||||||
({activeCategoryIndex + 1}/{categories.length})
|
className="p-1.5 rounded-md hover:bg-text/5 transition-colors cursor-pointer"
|
||||||
|
aria-label="Previous category"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="h-4 w-4 text-text/50" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-medium text-text/80">
|
||||||
|
{currentCategory?.category}{" "}
|
||||||
|
<span className="text-text/40">
|
||||||
|
({activeCategoryIndex + 1}/{categories.length})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<button
|
||||||
<button
|
onClick={goNextCategory}
|
||||||
onClick={goNextCategory}
|
className="p-1.5 rounded-md hover:bg-text/5 transition-colors cursor-pointer"
|
||||||
className="p-1.5 rounded-md hover:bg-text/5 transition-colors cursor-pointer"
|
aria-label="Next category"
|
||||||
aria-label="Next category"
|
>
|
||||||
>
|
<ChevronRightIcon className="h-4 w-4 text-text/50" />
|
||||||
<ChevronRightIcon className="h-4 w-4 text-text/50" />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings table */}
|
{/* Settings table */}
|
||||||
<div className="rounded-lg border border-border overflow-hidden">
|
<div className="rounded-lg border border-border overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-text/3">
|
<thead className="bg-text/3">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left px-2 py-1.5 md:px-3 md:py-2 text-xs font-medium uppercase tracking-wider text-text/50">
|
<th className="text-left px-2 py-1.5 md:px-3 md:py-2 text-xs font-medium uppercase tracking-wider text-text/50">
|
||||||
Setting
|
Setting
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right px-2 py-1.5 md:px-3 md:py-2 text-xs font-medium uppercase tracking-wider text-text/50">
|
<th className="text-right px-2 py-1.5 md:px-3 md:py-2 text-xs font-medium uppercase tracking-wider text-text/50">
|
||||||
Value
|
Value
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{currentCategory?.settings.map((setting, sIdx) => (
|
|
||||||
<tr
|
|
||||||
key={sIdx}
|
|
||||||
className="border-t border-border"
|
|
||||||
>
|
|
||||||
<td className="px-2 py-1.5 md:px-3 md:py-2 text-text/70">
|
|
||||||
{setting.title}
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 md:px-3 md:py-2 text-right font-medium text-text">
|
|
||||||
{formatValue(setting.value)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{currentCategory?.settings.map((setting, sIdx) => (
|
||||||
</div>
|
<tr
|
||||||
</div>
|
key={sIdx}
|
||||||
|
className="border-t border-border"
|
||||||
|
>
|
||||||
|
<td className="px-2 py-1.5 md:px-3 md:py-2 text-text/70">
|
||||||
|
{setting.title}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 md:px-3 md:py-2 text-right font-medium text-text">
|
||||||
|
{formatValue(setting.value)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3 rounded-lg border border-border bg-text/2">
|
<div className="flex flex-col items-center justify-center py-16 gap-3 rounded-lg border border-border bg-text/2">
|
||||||
<p className="text-sm text-text/40">
|
<p className="text-sm text-text/40">
|
||||||
|
|||||||
Reference in New Issue
Block a user