|
<script lang="ts"> |
|
import { onMount } from 'svelte'; |
|
import type { PicletInstance } from '$lib/db/schema'; |
|
import { deletePicletInstance } from '$lib/db/piclets'; |
|
import { uiStore } from '$lib/stores/ui'; |
|
|
|
interface Props { |
|
instance: PicletInstance; |
|
onClose: () => void; |
|
onDeleted?: () => void; |
|
} |
|
|
|
let { instance, onClose, onDeleted }: Props = $props(); |
|
let showDeleteConfirm = $state(false); |
|
let selectedTab = $state<'about' | 'stats' | 'actions'>('about'); |
|
let expandedMoves = $state(new Set<number>()); |
|
|
|
onMount(() => { |
|
uiStore.openDetailPage(); |
|
return () => { |
|
uiStore.closeDetailPage(); |
|
}; |
|
}); |
|
|
|
async function handleDelete() { |
|
if (!instance.id) return; |
|
|
|
try { |
|
await deletePicletInstance(instance.id); |
|
onDeleted?.(); |
|
onClose(); |
|
} catch (err) { |
|
console.error('Failed to delete piclet:', err); |
|
} |
|
} |
|
|
|
function getStatPercentage(value: number, max: number = 255): number { |
|
return Math.round((value / max) * 100); |
|
} |
|
|
|
function getHpColor(current: number, max: number): string { |
|
const ratio = current / max; |
|
if (ratio < 0.2) return '#ff3b30'; |
|
if (ratio < 0.5) return '#ff9500'; |
|
return '#34c759'; |
|
} |
|
|
|
function toggleMoveExpanded(index: number) { |
|
if (expandedMoves.has(index)) { |
|
expandedMoves.delete(index); |
|
} else { |
|
expandedMoves.add(index); |
|
} |
|
expandedMoves = new Set(expandedMoves); |
|
} |
|
</script> |
|
|
|
<div class="detail-page"> |
|
<div class="navigation-bar"> |
|
<button class="back-btn" onclick={onClose} aria-label="Go back"> |
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
<path d="M19 12H5m0 0l7 7m-7-7l7-7"></path> |
|
</svg> |
|
</button> |
|
<h2>{instance.nickname || instance.typeId}</h2> |
|
<div class="nav-spacer"></div> |
|
</div> |
|
|
|
<div class="content-scroll"> |
|
|
|
<div class="header-card"> |
|
<div class="header-layout"> |
|
<div class="image-column"> |
|
<div class="image-container"> |
|
<img |
|
src={instance.imageData || instance.imageUrl} |
|
alt={instance.nickname || instance.typeId} |
|
class="piclet-image" |
|
/> |
|
</div> |
|
<div class="id-badge"> |
|
#{instance.id?.toString().padStart(3, '0') || '???'} |
|
</div> |
|
</div> |
|
|
|
<div class="info-column"> |
|
<h1 class="piclet-name">{instance.nickname || instance.typeId}</h1> |
|
|
|
|
|
<div class="compact-info"> |
|
<div class="level-badge">Lv. {instance.level}</div> |
|
|
|
<div class="compact-stat-bar"> |
|
<div class="stat-bar-label"> |
|
<span>HP</span> |
|
<span>{instance.currentHp}/{instance.maxHp}</span> |
|
</div> |
|
<div class="hp-bar-track"> |
|
<div |
|
class="hp-bar-fill" |
|
style="width: {getStatPercentage(instance.currentHp, instance.maxHp)}%; background-color: {getHpColor(instance.currentHp, instance.maxHp)};" |
|
></div> |
|
</div> |
|
</div> |
|
|
|
<div class="compact-stat-bar"> |
|
<div class="stat-bar-label"> |
|
<span>XP</span> |
|
<span>{instance.xp} pts</span> |
|
</div> |
|
<div class="xp-bar-track"> |
|
<div class="xp-bar-fill" style="width: 45%;"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-bar"> |
|
<button |
|
class="tab-button" |
|
class:active={selectedTab === 'about'} |
|
onclick={() => selectedTab = 'about'} |
|
> |
|
About |
|
</button> |
|
<button |
|
class="tab-button" |
|
class:active={selectedTab === 'stats'} |
|
onclick={() => selectedTab = 'stats'} |
|
> |
|
Stats |
|
</button> |
|
<button |
|
class="tab-button" |
|
class:active={selectedTab === 'actions'} |
|
onclick={() => selectedTab = 'actions'} |
|
> |
|
Actions |
|
</button> |
|
</div> |
|
|
|
<!-- Tab Content --> |
|
<div class="tab-content"> |
|
{#if selectedTab === 'about'} |
|
<div class="content-card"> |
|
<p class="description">{instance.concept}</p> |
|
</div> |
|
{:else if selectedTab === 'stats'} |
|
<div class="content-card"> |
|
<h3 class="section-title">Battle Stats</h3> |
|
<div class="stats-list"> |
|
<div class="stat-row"> |
|
<span>Attack</span> |
|
<span class="stat-value">{instance.attack}</span> |
|
</div> |
|
<div class="stat-row"> |
|
<span>Defense</span> |
|
<span class="stat-value">{instance.defense}</span> |
|
</div> |
|
<div class="stat-row"> |
|
<span>Field Attack</span> |
|
<span class="stat-value">{instance.fieldAttack}</span> |
|
</div> |
|
<div class="stat-row"> |
|
<span>Field Defense</span> |
|
<span class="stat-value">{instance.fieldDefense}</span> |
|
</div> |
|
<div class="stat-row"> |
|
<span>Speed</span> |
|
<span class="stat-value">{instance.speed}</span> |
|
</div> |
|
</div> |
|
|
|
<div class="divider"></div> |
|
|
|
<div class="stat-summary"> |
|
<div class="summary-item"> |
|
<span class="summary-label">BST</span> |
|
<span class="summary-value">{instance.bst}</span> |
|
</div> |
|
<div class="summary-item"> |
|
<span class="summary-label">Tier</span> |
|
<span class="summary-value">{instance.tier.toUpperCase()}</span> |
|
</div> |
|
</div> |
|
</div> |
|
{:else if selectedTab === 'actions'} |
|
<div class="content-card"> |
|
{#each instance.moves as move, index} |
|
<button |
|
class="move-card" |
|
onclick={() => toggleMoveExpanded(index)} |
|
> |
|
<div class="move-header"> |
|
<span class="move-name">{move.name}</span> |
|
<div class="move-badges"> |
|
{#if move.power > 0} |
|
<span class="power-badge">PWR {move.power}</span> |
|
{/if} |
|
</div> |
|
</div> |
|
|
|
<p class="move-description">{move.description}</p> |
|
|
|
<div class="move-stats"> |
|
<span>Accuracy: {move.accuracy}%</span> |
|
<span>PP: {move.currentPp}/{move.pp}</span> |
|
</div> |
|
|
|
{#if expandedMoves.has(index)} |
|
<div class="move-details"> |
|
<p>Type: {move.type}</p> |
|
</div> |
|
{/if} |
|
</button> |
|
{/each} |
|
</div> |
|
{/if} |
|
</div> |
|
|
|
<!-- Actions --> |
|
<div class="bottom-actions"> |
|
{#if showDeleteConfirm} |
|
<p class="delete-confirm">Are you sure you want to release this piclet?</p> |
|
<button class="btn btn-danger" onclick={handleDelete}>Yes, Release</button> |
|
<button class="btn btn-secondary" onclick={() => showDeleteConfirm = false}>Cancel</button> |
|
{:else} |
|
<button class="btn btn-danger" onclick={() => showDeleteConfirm = true}>Release Piclet</button> |
|
{/if} |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<style> |
|
.detail-page { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: #f2f2f7; |
|
z-index: 1000; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
|
|
.navigation-bar { |
|
background: white; |
|
padding: 16px; |
|
padding-top: calc(16px + env(safe-area-inset-top, 0)); |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
border-bottom: 0.5px solid #c6c6c8; |
|
} |
|
|
|
.navigation-bar h2 { |
|
margin: 0; |
|
font-size: 17px; |
|
font-weight: 600; |
|
flex: 1; |
|
text-align: center; |
|
} |
|
|
|
.back-btn { |
|
background: none; |
|
border: none; |
|
color: #007bff; |
|
cursor: pointer; |
|
padding: 0; |
|
display: flex; |
|
align-items: center; |
|
width: 24px; |
|
height: 24px; |
|
} |
|
|
|
.nav-spacer { |
|
width: 24px; |
|
} |
|
|
|
|
|
.content-scroll { |
|
flex: 1; |
|
overflow-y: auto; |
|
-webkit-overflow-scrolling: touch; |
|
} |
|
|
|
|
|
.header-card { |
|
margin: 16px; |
|
padding: 20px; |
|
border-radius: 16px; |
|
background: linear-gradient(135deg, #636366, #48484a); |
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.header-layout { |
|
display: flex; |
|
gap: 16px; |
|
} |
|
|
|
.image-column { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
gap: 8px; |
|
} |
|
|
|
.image-container { |
|
background: #f0f0f0; |
|
padding: 8px; |
|
border-radius: 12px; |
|
} |
|
|
|
.piclet-image { |
|
width: 120px; |
|
height: 120px; |
|
object-fit: contain; |
|
} |
|
|
|
.id-badge { |
|
background: rgba(255, 255, 255, 0.2); |
|
padding: 4px 12px; |
|
border-radius: 12px; |
|
font-size: 14px; |
|
font-weight: bold; |
|
color: white; |
|
} |
|
|
|
.info-column { |
|
flex: 1; |
|
display: flex; |
|
flex-direction: column; |
|
color: white; |
|
} |
|
|
|
.piclet-name { |
|
margin: 0 0 12px; |
|
font-size: 24px; |
|
font-weight: bold; |
|
} |
|
|
|
.compact-info { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 8px; |
|
} |
|
|
|
.level-badge { |
|
background: rgba(142, 142, 147, 0.8); |
|
padding: 3px 10px; |
|
border-radius: 12px; |
|
font-size: 13px; |
|
font-weight: bold; |
|
width: fit-content; |
|
} |
|
|
|
.compact-stat-bar { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 4px; |
|
} |
|
|
|
.stat-bar-label { |
|
display: flex; |
|
justify-content: space-between; |
|
font-size: 12px; |
|
font-weight: 500; |
|
} |
|
|
|
.hp-bar-track, |
|
.xp-bar-track { |
|
height: 6px; |
|
background: rgba(255, 255, 255, 0.3); |
|
border-radius: 3px; |
|
overflow: hidden; |
|
} |
|
|
|
.hp-bar-fill, |
|
.xp-bar-fill { |
|
height: 100%; |
|
transition: width 0.3s; |
|
} |
|
|
|
.xp-bar-fill { |
|
background: #af52de; |
|
} |
|
|
|
|
|
.tab-bar { |
|
margin: 0 16px 16px; |
|
height: 32px; |
|
background: #e5e5ea; |
|
border-radius: 8px; |
|
display: flex; |
|
padding: 2px; |
|
} |
|
|
|
.tab-button { |
|
flex: 1; |
|
background: none; |
|
border: none; |
|
border-radius: 6px; |
|
font-size: 13px; |
|
font-weight: 500; |
|
color: #8e8e93; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
} |
|
|
|
.tab-button.active { |
|
background: #636366; |
|
color: white; |
|
} |
|
|
|
|
|
.tab-content { |
|
margin: 0 16px 16px; |
|
} |
|
|
|
.content-card { |
|
background: white; |
|
border-radius: 12px; |
|
padding: 16px; |
|
border: 0.5px solid #c6c6c8; |
|
} |
|
|
|
.description { |
|
margin: 0 0 16px; |
|
font-size: 16px; |
|
line-height: 1.4; |
|
color: #000; |
|
} |
|
|
|
|
|
|
|
.section-title { |
|
margin: 0 0 16px; |
|
font-size: 18px; |
|
font-weight: bold; |
|
color: #000; |
|
} |
|
|
|
.stats-list { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 12px; |
|
} |
|
|
|
.stat-row { |
|
display: flex; |
|
justify-content: space-between; |
|
font-size: 15px; |
|
} |
|
|
|
.stat-value { |
|
font-weight: 600; |
|
} |
|
|
|
.divider { |
|
height: 1px; |
|
background: #e5e5ea; |
|
margin: 16px 0; |
|
} |
|
|
|
.stat-summary { |
|
display: flex; |
|
justify-content: space-around; |
|
} |
|
|
|
.summary-item { |
|
text-align: center; |
|
} |
|
|
|
.summary-label { |
|
display: block; |
|
font-size: 14px; |
|
color: #8e8e93; |
|
margin-bottom: 4px; |
|
} |
|
|
|
.summary-value { |
|
font-size: 16px; |
|
font-weight: bold; |
|
color: #000; |
|
} |
|
|
|
|
|
.move-card { |
|
width: 100%; |
|
background: #f2f2f7; |
|
border: 1px solid rgba(0, 123, 255, 0.3); |
|
border-radius: 8px; |
|
padding: 12px; |
|
margin-bottom: 12px; |
|
cursor: pointer; |
|
text-align: left; |
|
transition: all 0.2s; |
|
} |
|
|
|
.move-card:active { |
|
transform: scale(0.98); |
|
} |
|
|
|
.move-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 8px; |
|
} |
|
|
|
.move-name { |
|
font-size: 16px; |
|
font-weight: bold; |
|
color: #000; |
|
} |
|
|
|
.move-badges { |
|
display: flex; |
|
gap: 6px; |
|
} |
|
|
|
.power-badge { |
|
background: rgba(255, 59, 48, 0.2); |
|
color: #ff3b30; |
|
padding: 4px 8px; |
|
border-radius: 12px; |
|
font-size: 12px; |
|
font-weight: 500; |
|
} |
|
|
|
.move-description { |
|
margin: 0 0 12px; |
|
font-size: 14px; |
|
color: #666; |
|
line-height: 1.3; |
|
} |
|
|
|
.move-stats { |
|
display: flex; |
|
gap: 16px; |
|
font-size: 13px; |
|
color: #8e8e93; |
|
} |
|
|
|
.move-details { |
|
margin-top: 12px; |
|
padding-top: 12px; |
|
border-top: 1px solid #e5e5ea; |
|
font-size: 14px; |
|
color: #666; |
|
} |
|
|
|
.move-details p { |
|
margin: 0; |
|
} |
|
|
|
|
|
.bottom-actions { |
|
padding: 16px; |
|
background: white; |
|
border-top: 0.5px solid #c6c6c8; |
|
text-align: center; |
|
} |
|
|
|
.delete-confirm { |
|
margin: 0 0 1rem; |
|
color: #ff3b30; |
|
font-size: 16px; |
|
} |
|
|
|
.btn { |
|
padding: 0.75rem 1.5rem; |
|
border: none; |
|
border-radius: 8px; |
|
font-size: 16px; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: transform 0.2s; |
|
} |
|
|
|
.btn:active { |
|
transform: scale(0.95); |
|
} |
|
|
|
.btn-danger { |
|
background: #ff3b30; |
|
color: white; |
|
width: 100%; |
|
} |
|
|
|
.btn-secondary { |
|
background: #e5e5ea; |
|
color: #333; |
|
margin-top: 8px; |
|
width: 100%; |
|
} |
|
|
|
@media (min-width: 768px) { |
|
.detail-page { |
|
position: relative; |
|
max-width: 600px; |
|
margin: 0 auto; |
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
|
} |
|
} |
|
</style> |