refactor EditCharacterSidebar to use shared CharacterEditPane

Reduces ~240 lines to ~85 lines by using the shared CharacterEditPane
component for edit controls. Both party grid and collection character
editing now use the same underlying UI component.
This commit is contained in:
Justin Edmund 2025-12-02 11:26:23 -08:00
parent dea784780a
commit 2aa961b5e6

View file

@ -1,18 +1,16 @@
<script lang="ts">
/**
* EditCharacterSidebar - Edit sidebar for party grid characters
*
* Uses the shared CharacterEditPane for edit controls.
* Handles GridCharacter-specific data transformation and mutations.
*/
import type { GridCharacter } from '$lib/types/api/party'
import type { Awakening } from '$lib/types/api/entities'
import DetailsSection from './details/DetailsSection.svelte'
import ItemHeader from './details/ItemHeader.svelte'
import AwakeningSelect from './edit/AwakeningSelect.svelte'
import RingsSelect from './edit/RingsSelect.svelte'
import EarringSelect from './edit/EarringSelect.svelte'
import PerpetuityToggle from './edit/PerpetuityToggle.svelte'
import Button from '$lib/components/ui/Button.svelte'
interface ExtendedMastery {
modifier: number
strength: number
}
import CharacterEditPane, {
type CharacterEditValues,
type CharacterEditUpdates
} from './CharacterEditPane.svelte'
interface Props {
character: GridCharacter
@ -24,106 +22,35 @@
// Character data shortcut
const characterData = $derived(character.character)
const characterElement = $derived(characterData?.element)
// Awakening state - initialize from existing awakening
let selectedAwakening = $state<Awakening | undefined>(character.awakening?.type)
let awakeningLevel = $state(character.awakening?.level ?? 1)
// Rings state - initialize from existing overMastery
let rings = $state<ExtendedMastery[]>(
character.overMastery ?? [
{ modifier: 1, strength: 0 },
{ modifier: 2, strength: 0 },
{ modifier: 0, strength: 0 },
{ modifier: 0, strength: 0 }
]
)
// Earring state - initialize from existing aetherialMastery
let earring = $state<ExtendedMastery | undefined>(character.aetherialMastery)
// Perpetuity state - initialize from existing value
let perpetuity = $state(character.perpetuity ?? false)
// Derived conditions for what can be edited
// Characters always have maxAwakeningLevel of 10 (not returned by API, but hardcoded)
const maxAwakeningLevel = 10
const hasAwakening = $derived((characterData?.awakenings?.length ?? 0) > 0)
const availableAwakenings = $derived(characterData?.awakenings ?? [])
// Perpetuity is only available for non-MC characters (position > 0)
const canHavePerpetuity = $derived(character.position > 0)
// Awakening slug to UUID map (awakenings come from API with id: null, only slugs)
const AWAKENING_MAP: Record<string, string> = {
'character-balanced': 'b1847c82-ece0-4d7a-8af1-c7868d90f34a',
'character-atk': '6e233877-8cda-4c8f-a091-3db6f68749e2',
'character-def': 'c95441de-f949-4a62-b02b-101aa2e0a638',
'character-multi': 'e36b0573-79c3-4dd2-9524-c95def4bbb1a'
}
// Element name for the sidebar theming
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
1: 'wind',
2: 'fire',
3: 'water',
4: 'earth',
5: 'dark',
6: 'light'
}
const elementName = $derived(characterElement ? ELEMENT_MAP[characterElement] : undefined)
function handleSave() {
// Build API-formatted updates (field names match Rails API expectations)
const updates: Record<string, unknown> = {}
// Collect awakening updates - convert slug to UUID using AWAKENING_MAP
// The awakenings list from API has id: null, only slugs are populated
if (hasAwakening) {
if (selectedAwakening && selectedAwakening.slug) {
const awakeningId = AWAKENING_MAP[selectedAwakening.slug]
if (awakeningId) {
updates.awakening = {
id: awakeningId,
level: awakeningLevel
}
} else {
console.warn(`Unknown awakening slug: ${selectedAwakening.slug}`)
// Convert GridCharacter data to CharacterEditPane format
const currentValues = $derived<CharacterEditValues>({
awakening: character.awakening
? {
type: character.awakening.type,
level: character.awakening.level ?? 1
}
} else {
updates.awakening = null
}
}
// Collect rings updates - API expects 'rings' not 'overMastery'
updates.rings = rings
// Collect earring updates - API expects 'earring' not 'aetherialMastery'
if (earring) {
updates.earring = earring
}
// Collect perpetuity updates
if (canHavePerpetuity) {
updates.perpetuity = perpetuity
}
onSave?.(updates as Partial<GridCharacter>)
}
function handleCancel() {
// Reset to original values
selectedAwakening = character.awakening?.type
awakeningLevel = character.awakening?.level ?? 1
rings = character.overMastery ?? [
: null,
rings: character.overMastery ?? [
{ modifier: 1, strength: 0 },
{ modifier: 2, strength: 0 },
{ modifier: 0, strength: 0 },
{ modifier: 0, strength: 0 }
]
earring = character.aetherialMastery
perpetuity = character.perpetuity ?? false
],
earring: character.aetherialMastery ?? undefined,
perpetuity: character.perpetuity ?? false
})
function handleSave(updates: CharacterEditUpdates) {
// Transform CharacterEditUpdates to GridCharacter API format
// The CharacterEditPane already formats awakening with id/level
onSave?.(updates as Partial<GridCharacter>)
}
function handleCancel() {
onCancel?.()
}
</script>
@ -137,70 +64,13 @@
gridTranscendence={character.transcendenceStep}
/>
<div class="edit-sections">
{#if hasAwakening && availableAwakenings.length > 0}
<DetailsSection title="Awakening">
<div class="section-content">
<AwakeningSelect
awakenings={availableAwakenings}
value={selectedAwakening}
level={awakeningLevel}
maxLevel={maxAwakeningLevel}
onAwakeningChange={(awakening) => {
selectedAwakening = awakening
}}
onLevelChange={(level) => {
awakeningLevel = level
}}
/>
</div>
</DetailsSection>
{/if}
<DetailsSection title="Over Mastery Rings">
<div class="section-content">
<RingsSelect
{rings}
onChange={(newRings) => {
rings = newRings
}}
/>
</div>
</DetailsSection>
<DetailsSection title="Aetherial Mastery">
<div class="section-content">
<EarringSelect
value={earring}
element={characterElement}
onChange={(newEarring) => {
earring = newEarring
}}
/>
</div>
</DetailsSection>
{#if canHavePerpetuity}
<DetailsSection title="Perpetuity">
<div class="section-content">
<PerpetuityToggle
value={perpetuity}
element={elementName}
onChange={(value) => {
perpetuity = value
}}
/>
</div>
</DetailsSection>
{/if}
</div>
<div class="edit-footer">
<Button variant="secondary" onclick={handleCancel}>Cancel</Button>
<Button variant="primary" element={elementName} elementStyle={!!elementName} onclick={handleSave}>
Save
</Button>
</div>
<CharacterEditPane
{characterData}
{currentValues}
showPerpetuity={canHavePerpetuity}
onSave={handleSave}
onCancel={handleCancel}
/>
</div>
<style lang="scss">
@ -212,28 +82,4 @@
height: 100%;
gap: spacing.$unit-4x;
}
.edit-sections {
flex: 1;
display: flex;
flex-direction: column;
gap: spacing.$unit-3x;
overflow-y: auto;
}
.section-content {
padding: spacing.$unit;
}
.edit-footer {
display: flex;
gap: spacing.$unit-2x;
padding: spacing.$unit-2x;
border-top: 1px solid var(--border-secondary);
flex-shrink: 0;
:global(button) {
flex: 1;
}
}
</style>