hensei-web/src/lib/components/artifact/ArtifactEditPane.svelte
Justin Edmund 3a41adc4f2 add artifact UI components for phase 4
- DisclosureRow: iOS-style disclosure row for navigation
- ArtifactSkillRow: shows skill with modifiers and level/value controls
- ArtifactModifierList: selectable list of skills by polarity
- ArtifactGradeDisplay: shows letter grade, breakdown, recommendation
- ArtifactEditPane: main edit pane combining base props, skills, grade
2025-12-03 16:08:17 -08:00

305 lines
8.2 KiB
Svelte

<svelte:options runes={true} />
<script lang="ts">
import type {
ArtifactInstance,
ArtifactSkillInstance,
ArtifactSkill,
ArtifactGrade
} from '$lib/types/api/artifact'
import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact'
import { createQuery } from '@tanstack/svelte-query'
import { artifactQueries } from '$lib/api/queries/artifact.queries'
import { usePaneStack, type PaneConfig } from '$lib/stores/paneStack.svelte'
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
import Select from '$lib/components/ui/Select.svelte'
import ArtifactSkillRow from './ArtifactSkillRow.svelte'
import ArtifactModifierList from './ArtifactModifierList.svelte'
import ArtifactGradeDisplay from './ArtifactGradeDisplay.svelte'
import { getElementIcon } from '$lib/utils/images'
interface Props {
/** The artifact instance being edited */
artifact: ArtifactInstance
/** Handler when artifact is updated */
onUpdate?: (updates: Partial<ArtifactInstance>) => void
/** Whether editing is disabled (view-only mode) */
disabled?: boolean
}
const { artifact, onUpdate, disabled = false }: Props = $props()
// Get the pane stack from context for pushing modifier selection panes
const paneStack = usePaneStack()
// Local state for edits
let element = $state(artifact.element)
let level = $state(artifact.level)
let proficiency = $state(artifact.proficiency)
let skills = $state<(ArtifactSkillInstance | null)[]>([...artifact.skills])
// Derived values
const artifactData = $derived(artifact.artifact)
const isQuirk = $derived(isQuirkArtifact(artifactData))
const canChangeElement = $derived(true) // Artifacts can always change element
const canChangeProficiency = $derived(isQuirk) // Only quirk artifacts have variable proficiency
// Query all skills for skill rows
const skillsQuery = createQuery(() => artifactQueries.skills())
// Get skills available for each slot
function getSkillsForSlot(slot: number): ArtifactSkill[] {
if (!skillsQuery.data) return []
const group = getSkillGroupForSlot(slot)
return skillsQuery.data.filter((s) => s.skillGroup === group)
}
// Element options
const elementOptions = [
{ value: 1, label: 'Wind', image: getElementIcon(1) },
{ value: 2, label: 'Fire', image: getElementIcon(2) },
{ value: 3, label: 'Water', image: getElementIcon(3) },
{ value: 4, label: 'Earth', image: getElementIcon(4) },
{ value: 5, label: 'Dark', image: getElementIcon(5) },
{ value: 6, label: 'Light', image: getElementIcon(6) }
]
// Level options (1-5 for standard, fixed at 1 for quirk)
const levelOptions = $derived(
isQuirk
? [{ value: 1, label: '1' }]
: [
{ value: 1, label: '1' },
{ value: 2, label: '2' },
{ value: 3, label: '3' },
{ value: 4, label: '4' },
{ value: 5, label: '5' }
]
)
// Proficiency options (1-10 for quirk artifacts)
const proficiencyOptions = [
{ value: 1, label: 'Sabre' },
{ value: 2, label: 'Dagger' },
{ value: 3, label: 'Spear' },
{ value: 4, label: 'Axe' },
{ value: 5, label: 'Staff' },
{ value: 6, label: 'Gun' },
{ value: 7, label: 'Melee' },
{ value: 8, label: 'Bow' },
{ value: 9, label: 'Harp' },
{ value: 10, label: 'Katana' }
]
// Get proficiency display name
function getProficiencyName(profValue: number | null | undefined): string {
if (!profValue) return '—'
const option = proficiencyOptions.find((o) => o.value === profValue)
return option?.label ?? '—'
}
// Push modifier selection pane for a specific slot
function handleSelectModifier(slot: number) {
const config: PaneConfig = {
id: `modifier-select-${slot}`,
title: `Select Skill ${slot}`,
component: ArtifactModifierList,
props: {
slot,
selectedModifier: skills[slot - 1]?.modifier,
onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill)
}
}
paneStack.push(config)
}
// Handle when a modifier is selected from the list
function handleModifierSelected(slot: number, skill: ArtifactSkill) {
const index = slot - 1
const newSkills = [...skills]
// Create new skill instance with first available strength and level 1
const firstStrength = skill.baseValues.find((v) => v !== null && v !== 0) ?? 0
newSkills[index] = {
modifier: skill.modifier,
strength: firstStrength,
level: 1
}
skills = newSkills
notifyUpdate()
// Pop back to the edit pane
paneStack.pop()
}
// Handle skill updates from skill row
function handleUpdateSkill(slot: number, update: Partial<ArtifactSkillInstance>) {
const index = slot - 1
const currentSkill = skills[index]
if (!currentSkill) return
const newSkills = [...skills]
newSkills[index] = { ...currentSkill, ...update }
skills = newSkills
notifyUpdate()
}
// Handle element change
function handleElementChange(newElement: number | undefined) {
if (newElement === undefined) return
element = newElement
notifyUpdate()
}
// Handle level change
function handleLevelChange(newLevel: number | undefined) {
if (newLevel === undefined) return
level = newLevel
notifyUpdate()
}
// Handle proficiency change
function handleProficiencyChange(newProficiency: number | undefined) {
if (newProficiency === undefined) return
proficiency = newProficiency
notifyUpdate()
}
// Notify parent of updates
function notifyUpdate() {
if (!onUpdate) return
const updates: Partial<ArtifactInstance> = {
element,
level,
skills: [...skills]
}
if (isQuirk && proficiency !== undefined) {
updates.proficiency = proficiency
}
onUpdate(updates)
}
// Current grade (from artifact or could be recalculated)
const currentGrade: ArtifactGrade = $derived(artifact.grade)
</script>
<div class="artifact-edit-pane">
<DetailsSection title="Base Properties">
<DetailRow label="Element" noHover>
{#if disabled}
<span class="element-display">
<img src={getElementIcon(element)} alt="" class="element-icon" />
{elementOptions.find((o) => o.value === element)?.label ?? '—'}
</span>
{:else}
<Select
options={elementOptions}
value={element}
onValueChange={handleElementChange}
size="small"
contained
{disabled}
/>
{/if}
</DetailRow>
{#if canChangeProficiency}
<DetailRow label="Proficiency" noHover>
{#if disabled}
<span>{getProficiencyName(proficiency)}</span>
{:else}
<Select
options={proficiencyOptions}
value={proficiency}
onValueChange={handleProficiencyChange}
size="small"
contained
placeholder="Select proficiency"
{disabled}
/>
{/if}
</DetailRow>
{:else}
<DetailRow label="Proficiency" value={getProficiencyName(artifactData.proficiency)} />
{/if}
<DetailRow label="Level" noHover>
{#if disabled || isQuirk}
<span>{level}</span>
{:else}
<Select
options={levelOptions}
value={level}
onValueChange={handleLevelChange}
size="small"
contained
{disabled}
/>
{/if}
</DetailRow>
</DetailsSection>
{#if !isQuirk}
<DetailsSection title="Skills">
<div class="skills-list">
{#each [1, 2, 3, 4] as slot}
<ArtifactSkillRow
{slot}
skill={skills[slot - 1] ?? null}
availableSkills={getSkillsForSlot(slot)}
onSelectModifier={() => handleSelectModifier(slot)}
onUpdateSkill={(update) => handleUpdateSkill(slot, update)}
{disabled}
/>
{/each}
</div>
</DetailsSection>
{/if}
<DetailsSection title="Grade">
<div class="grade-section">
<ArtifactGradeDisplay grade={currentGrade} />
</div>
</DetailsSection>
</div>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
@use '$src/themes/layout' as layout;
.artifact-edit-pane {
display: flex;
flex-direction: column;
gap: spacing.$unit-3x;
padding-bottom: spacing.$unit-4x;
}
.element-display {
display: flex;
align-items: center;
gap: spacing.$unit-half;
}
.element-icon {
width: 20px;
height: 20px;
}
.skills-list {
display: flex;
flex-direction: column;
gap: spacing.$unit;
padding: 0 spacing.$unit;
}
.grade-section {
padding: spacing.$unit;
}
</style>