split artifact pane into detail and edit views

- CollectionArtifactDetailPane: read-only view with Edit button in header
- CollectionArtifactEditPane: editable form pushed onto pane stack
- ArtifactSkillDisplay: new read-only skill display component
This commit is contained in:
Justin Edmund 2025-12-03 17:54:18 -08:00
parent 190e2140b1
commit 34821aa487
5 changed files with 401 additions and 159 deletions

View file

@ -0,0 +1,98 @@
<svelte:options runes={true} />
<script lang="ts">
/**
* ArtifactSkillDisplay - Read-only display of an artifact skill
*/
import type { ArtifactSkillInstance } from '$lib/types/api/artifact'
import { createQuery } from '@tanstack/svelte-query'
import { artifactQueries } from '$lib/api/queries/artifact.queries'
interface Props {
slot: number
skill: ArtifactSkillInstance
}
const { slot, skill }: Props = $props()
// Query skills to get the full skill definition
const skillsQuery = createQuery(() => artifactQueries.skills())
// Find the skill definition
const skillDef = $derived(
skillsQuery.data?.find((s) => s.modifier === skill.modifier)
)
const modifierName = $derived(skillDef?.name?.en ?? `Skill ${slot}`)
const suffix = $derived(skillDef?.suffix?.en ?? '')
const isNegative = $derived(skillDef?.polarity === 'negative')
</script>
<div class="skill-display">
<div class="skill-header">
<span class="slot">Skill {slot}</span>
<span class="level">Lv.{skill.level}</span>
</div>
<div class="skill-info">
<span class="modifier-name">{modifierName}</span>
<span class="strength" class:negative={isNegative}>
{isNegative ? '' : '+'}{skill.strength}{suffix}
</span>
</div>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/typography' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/colors' as *;
.skill-display {
display: flex;
flex-direction: column;
gap: $unit-fourth;
padding: $unit;
background: var(--input-bg);
border-radius: $item-corner;
}
.skill-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.slot {
font-size: $font-tiny;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.level {
font-size: $font-tiny;
color: var(--text-secondary);
}
.skill-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.modifier-name {
font-size: $font-small;
font-weight: $medium;
color: var(--text-primary);
}
.strength {
font-size: $font-small;
font-weight: $bold;
color: $wind-text-20;
&.negative {
color: $error;
}
}
</style>

View file

@ -0,0 +1,199 @@
<svelte:options runes={true} />
<script lang="ts">
/**
* CollectionArtifactDetailPane - View-only pane for artifact details
*
* Shows artifact info in read-only mode. If user is owner,
* provides Edit button in header to push edit pane onto stack.
*/
import type { CollectionArtifact } from '$lib/types/api/artifact'
import { isQuirkArtifact } from '$lib/types/api/artifact'
import { usePaneStack, type PaneConfig } from '$lib/stores/paneStack.svelte'
import { sidebar } from '$lib/stores/sidebar.svelte'
import { getArtifactImage } from '$lib/utils/images'
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
import ArtifactGradeDisplay from '$lib/components/artifact/ArtifactGradeDisplay.svelte'
import ArtifactSkillDisplay from '$lib/components/artifact/ArtifactSkillDisplay.svelte'
import CollectionArtifactEditPane from './CollectionArtifactEditPane.svelte'
interface Props {
artifact: CollectionArtifact
isOwner?: boolean
onClose?: () => void
}
let { artifact, isOwner = false, onClose }: Props = $props()
const paneStack = usePaneStack()
// Image and name
const imageUrl = $derived(getArtifactImage(artifact.artifact?.granblueId))
const displayName = $derived.by(() => {
const name = artifact.artifact?.name
if (!name) return '—'
if (typeof name === 'string') return name
return name.en || name.ja || '—'
})
// Artifact properties
const isQuirk = $derived(isQuirkArtifact(artifact.artifact))
const proficiency = $derived(
(isQuirk ? artifact.proficiency : artifact.artifact?.proficiency) ?? undefined
)
// Skills (only for standard artifacts)
const skills = $derived(artifact.skills ?? [])
const hasSkills = $derived(!isQuirk && skills.some((s) => s !== null))
// Push edit pane
function handleEdit() {
const config: PaneConfig = {
id: `artifact-edit-${artifact.id}`,
title: 'Edit Artifact',
component: CollectionArtifactEditPane,
props: {
artifact,
onClose
}
}
paneStack.push(config)
}
// Set up the Edit action button in the pane header for owners
$effect(() => {
if (isOwner) {
sidebar.setAction(handleEdit, 'Edit')
}
return () => {
// Clean up action when component unmounts
sidebar.clearAction()
}
})
</script>
<div class="artifact-detail-pane">
<!-- Header with image -->
<div class="pane-header">
<div class="artifact-image">
<img src={imageUrl} alt={displayName} />
</div>
<h2 class="artifact-name">{displayName}</h2>
{#if artifact.nickname}
<p class="artifact-nickname">"{artifact.nickname}"</p>
{/if}
</div>
<!-- Details content -->
<div class="pane-content">
<DetailsSection title="Properties">
<DetailRow label="Element">
<ElementLabel element={artifact.element} size="medium" />
</DetailRow>
<DetailRow label="Proficiency">
<ProficiencyLabel {proficiency} size="medium" />
</DetailRow>
<DetailRow label="Level" value="Lv.{artifact.level}" />
{#if isQuirk}
<DetailRow label="Type" value="Quirk" />
{/if}
</DetailsSection>
{#if hasSkills}
<DetailsSection title="Skills">
<div class="skills-list">
{#each skills as skill, index}
{#if skill}
<ArtifactSkillDisplay slot={index + 1} {skill} />
{/if}
{/each}
</div>
</DetailsSection>
{/if}
<DetailsSection title="Grade">
<div class="grade-section">
<ArtifactGradeDisplay grade={artifact.grade} />
</div>
</DetailsSection>
</div>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/typography' as *;
@use '$src/themes/layout' as *;
.artifact-detail-pane {
display: flex;
flex-direction: column;
height: 100%;
}
.pane-header {
display: flex;
flex-direction: column;
align-items: center;
padding: $unit-2x;
border-bottom: 1px solid var(--border-secondary);
}
.artifact-image {
width: 80px;
height: 80px;
border-radius: $item-corner;
overflow: hidden;
background: var(--card-bg, #f5f5f5);
margin-bottom: $unit;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.artifact-name {
margin: 0;
font-size: $font-large;
font-weight: $bold;
color: var(--text-primary);
text-align: center;
}
.artifact-nickname {
margin: $unit-half 0 0;
font-size: $font-small;
color: var(--text-secondary);
font-style: italic;
text-align: center;
}
.pane-content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: $unit-2x;
padding-bottom: $unit-2x;
}
.skills-list {
display: flex;
flex-direction: column;
gap: $unit;
padding: 0 $unit;
}
.grade-section {
padding: $unit;
}
</style>

View file

@ -0,0 +1,102 @@
<svelte:options runes={true} />
<script lang="ts">
/**
* CollectionArtifactEditPane - Edit pane for modifying artifact properties
*
* Pushed onto pane stack from CollectionArtifactDetailPane.
* Handles saving and deleting artifacts.
*/
import type { CollectionArtifact } from '$lib/types/api/artifact'
import { useUpdateCollectionArtifact, useDeleteCollectionArtifact } from '$lib/api/mutations/artifact.mutations'
import { usePaneStack } from '$lib/stores/paneStack.svelte'
import ArtifactEditPane from '$lib/components/artifact/ArtifactEditPane.svelte'
import Button from '$lib/components/ui/Button.svelte'
interface Props {
artifact: CollectionArtifact
onClose?: () => void
}
let { artifact, onClose }: Props = $props()
const paneStack = usePaneStack()
// Mutations
const updateMutation = useUpdateCollectionArtifact()
const deleteMutation = useDeleteCollectionArtifact()
// Handle updates from ArtifactEditPane
function handleUpdate(updates: Partial<CollectionArtifact>) {
updateMutation.mutate({
id: artifact.id,
input: {
element: updates.element,
level: updates.level,
proficiency: updates.proficiency,
skill1: updates.skills?.[0] ?? undefined,
skill2: updates.skills?.[1] ?? undefined,
skill3: updates.skills?.[2] ?? undefined,
skill4: updates.skills?.[3] ?? undefined
}
})
}
// Handle delete
function handleDelete() {
if (confirm('Are you sure you want to delete this artifact from your collection?')) {
deleteMutation.mutate(artifact.id, {
onSuccess: () => {
onClose?.()
}
})
}
}
</script>
<div class="artifact-edit-pane">
<!-- Edit pane content -->
<div class="pane-content">
<ArtifactEditPane
{artifact}
onUpdate={handleUpdate}
/>
</div>
<!-- Actions footer -->
<div class="pane-footer">
<Button
variant="destructive"
size="small"
onclick={handleDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>
</div>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/typography' as *;
@use '$src/themes/layout' as *;
.artifact-edit-pane {
display: flex;
flex-direction: column;
height: 100%;
}
.pane-content {
flex: 1;
overflow-y: auto;
}
.pane-footer {
display: flex;
justify-content: center;
padding: $unit-2x;
border-top: 1px solid var(--border-secondary);
flex-shrink: 0;
}
</style>

View file

@ -1,157 +0,0 @@
<svelte:options runes={true} />
<script lang="ts">
import type { CollectionArtifact } from '$lib/types/api/artifact'
import { useUpdateCollectionArtifact, useDeleteCollectionArtifact } from '$lib/api/mutations/artifact.mutations'
import { getArtifactImage } from '$lib/utils/images'
import ArtifactEditPane from '$lib/components/artifact/ArtifactEditPane.svelte'
import Button from '$lib/components/ui/Button.svelte'
interface Props {
artifact: CollectionArtifact
isOwner?: boolean
onClose?: () => void
}
let { artifact, isOwner = false, onClose }: Props = $props()
// Mutations
const updateMutation = useUpdateCollectionArtifact()
const deleteMutation = useDeleteCollectionArtifact()
// Image and name for header
const imageUrl = $derived(getArtifactImage(artifact.artifact?.granblueId))
const displayName = $derived.by(() => {
const name = artifact.artifact?.name
if (!name) return '—'
if (typeof name === 'string') return name
return name.en || name.ja || '—'
})
// Handle updates from ArtifactEditPane
function handleUpdate(updates: Partial<CollectionArtifact>) {
updateMutation.mutate({
id: artifact.id,
input: {
element: updates.element,
level: updates.level,
proficiency: updates.proficiency,
skill1: updates.skills?.[0] ?? undefined,
skill2: updates.skills?.[1] ?? undefined,
skill3: updates.skills?.[2] ?? undefined,
skill4: updates.skills?.[3] ?? undefined
}
})
}
// Handle delete
function handleDelete() {
if (confirm('Are you sure you want to delete this artifact from your collection?')) {
deleteMutation.mutate(artifact.id, {
onSuccess: () => {
onClose?.()
}
})
}
}
</script>
<div class="artifact-pane">
<!-- Header with image -->
<div class="pane-header">
<div class="artifact-image">
<img src={imageUrl} alt={displayName} />
</div>
<h2 class="artifact-name">{displayName}</h2>
{#if artifact.nickname}
<p class="artifact-nickname">"{artifact.nickname}"</p>
{/if}
</div>
<!-- Edit pane content -->
<div class="pane-content">
<ArtifactEditPane
{artifact}
onUpdate={isOwner ? handleUpdate : undefined}
disabled={!isOwner}
/>
</div>
<!-- Actions footer (owner only) -->
{#if isOwner}
<div class="pane-footer">
<Button
variant="destructive"
size="small"
onclick={handleDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>
</div>
{/if}
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/typography' as *;
@use '$src/themes/layout' as *;
.artifact-pane {
display: flex;
flex-direction: column;
height: 100%;
}
.pane-header {
display: flex;
flex-direction: column;
align-items: center;
padding: $unit-2x;
border-bottom: 1px solid var(--border-secondary);
}
.artifact-image {
width: 80px;
height: 80px;
border-radius: $item-corner;
overflow: hidden;
background: var(--card-bg, #f5f5f5);
margin-bottom: $unit;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.artifact-name {
margin: 0;
font-size: $font-large;
font-weight: $bold;
color: var(--text-primary);
text-align: center;
}
.artifact-nickname {
margin: $unit-half 0 0;
font-size: $font-small;
color: var(--text-secondary);
font-style: italic;
text-align: center;
}
.pane-content {
flex: 1;
overflow-y: auto;
}
.pane-footer {
display: flex;
justify-content: center;
padding: $unit-2x;
border-top: 1px solid var(--border-secondary);
flex-shrink: 0;
}
</style>

View file

@ -3,7 +3,7 @@
import type { CollectionArtifact } from '$lib/types/api/artifact'
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { artifactQueries } from '$lib/api/queries/artifact.queries'
import CollectionArtifactPane from '$lib/components/collection/CollectionArtifactPane.svelte'
import CollectionArtifactDetailPane from '$lib/components/collection/CollectionArtifactDetailPane.svelte'
import CollectionArtifactCard from '$lib/components/collection/CollectionArtifactCard.svelte'
import CollectionArtifactRow from '$lib/components/collection/CollectionArtifactRow.svelte'
import Icon from '$lib/components/Icon.svelte'
@ -76,7 +76,7 @@
? artifact.artifact.name
: artifact.artifact?.name?.en || 'Artifact'
sidebar.openWithComponent(artifactName, CollectionArtifactPane, {
sidebar.openWithComponent(artifactName, CollectionArtifactDetailPane, {
artifact,
isOwner: data.isOwner,
onClose: () => sidebar.close()