hensei-web/src/lib/components/sidebar/edit/AwakeningSelect.svelte

179 lines
4.3 KiB
Svelte

<svelte:options runes={true} />
<script lang="ts">
import type { Awakening } from '$lib/types/api/entities'
import { NO_AWAKENING } from '$lib/types/api/entities'
import Select from '$lib/components/ui/Select.svelte'
import Input from '$lib/components/ui/Input.svelte'
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
import { getAwakeningImage } from '$lib/utils/modifiers'
interface Props {
/** Available awakenings for the weapon */
awakenings: Awakening[]
/** Currently selected awakening */
value?: Awakening
/** Current awakening level */
level?: number
/** Maximum awakening level for the weapon */
maxLevel: number
/** Called when awakening type changes */
onAwakeningChange?: (awakening: Awakening | undefined) => void
/** Called when awakening level changes */
onLevelChange?: (level: number) => void
}
let {
awakenings,
value = undefined,
level = 1,
maxLevel,
onAwakeningChange,
onLevelChange
}: Props = $props()
// Local state for the selected awakening ID (use id or slug as key)
let selectedId = $state(value ? (value.id || value.slug || NO_AWAKENING.id) : NO_AWAKENING.id)
let localLevel = $state(level)
// Error state for level input
let levelError = $state('')
// Helper to get a unique identifier for an awakening (use id if available, fallback to slug)
function getAwakeningKey(awk: Awakening): string {
return awk.id || awk.slug || 'unknown'
}
// Build options list with NO_AWAKENING first
const options = $derived.by(() => {
const sorted = [...awakenings].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const items: Array<{ value: string; label: string; image?: string }> = sorted.map((awk) => {
const img = getAwakeningImage({ type: awk, level: 1 })
return {
value: getAwakeningKey(awk),
label: awk.name?.en || awk.name?.ja || 'Unknown',
image: img ?? undefined
}
})
// Add NO_AWAKENING at the beginning if not already present
if (!awakenings.find((a) => getAwakeningKey(a) === NO_AWAKENING.id)) {
items.unshift({
value: NO_AWAKENING.id,
label: NO_AWAKENING.name.en
})
}
return items
})
// Is the current selection the "No awakening" option?
const isNoAwakening = $derived(selectedId === NO_AWAKENING.id)
// Handle awakening type change
function handleAwakeningChange(newId: string | undefined) {
if (!newId) {
selectedId = NO_AWAKENING.id
onAwakeningChange?.(undefined)
return
}
selectedId = newId
if (selectedId === NO_AWAKENING.id) {
onAwakeningChange?.(undefined)
} else {
// Find by id first, then by slug (for awakenings with null id)
const selected = awakenings.find((a) => getAwakeningKey(a) === selectedId)
onAwakeningChange?.(selected)
}
}
// Handle level change with validation
function handleLevelChange(event: Event) {
const input = event.target as HTMLInputElement
const newLevel = parseInt(input.value, 10)
// Validate the level
if (isNaN(newLevel)) {
levelError = 'Please enter a valid number'
return
}
if (newLevel < 1) {
levelError = 'Level must be at least 1'
return
}
if (newLevel > maxLevel) {
levelError = `Level cannot exceed ${maxLevel}`
return
}
if (!Number.isInteger(newLevel)) {
levelError = 'Level must be a whole number'
return
}
levelError = ''
localLevel = newLevel
onLevelChange?.(newLevel)
}
</script>
<div class="awakening-select">
<div class="awakening-type">
<Select
options={options}
value={selectedId}
onValueChange={handleAwakeningChange}
placeholder="Select awakening"
size="medium"
fullWidth
contained
/>
</div>
{#if !isNoAwakening}
<DetailRow label="Level" noHover noPadding>
<Input
type="number"
min={1}
max={maxLevel}
step={1}
value={localLevel}
oninput={handleLevelChange}
error={levelError || undefined}
contained
variant="number"
placeholder="1~{maxLevel}"
/>
</DetailRow>
{/if}
{#if levelError}
<p class="level-error">{levelError}</p>
{/if}
</div>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.awakening-select {
display: flex;
flex-direction: column;
gap: spacing.$unit-2x;
}
.awakening-type {
flex: 1;
}
.level-error {
margin: 0;
font-size: typography.$font-small;
color: colors.$error;
}
</style>