add party edit sidebar with raid selection
This commit is contained in:
parent
b5cc2b7b9f
commit
cf29c00c8d
4 changed files with 598 additions and 132 deletions
|
|
@ -54,12 +54,17 @@
|
|||
import Icon from '$lib/components/Icon.svelte'
|
||||
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
||||
import { openDescriptionSidebar } from '$lib/features/description/openDescriptionSidebar.svelte'
|
||||
import {
|
||||
openPartyEditSidebar,
|
||||
type PartyEditValues
|
||||
} from '$lib/features/party/openPartyEditSidebar.svelte'
|
||||
import PartyInfoGrid from '$lib/components/party/info/PartyInfoGrid.svelte'
|
||||
import { DropdownMenu } from 'bits-ui'
|
||||
import DropdownItem from '$lib/components/ui/dropdown/DropdownItem.svelte'
|
||||
import JobSection from '$lib/components/job/JobSection.svelte'
|
||||
import { Gender } from '$lib/utils/jobUtils'
|
||||
import { openJobSelectionSidebar, openJobSkillSelectionSidebar } from '$lib/features/job/openJobSidebar.svelte'
|
||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||
import { partyAdapter, type UpdatePartyParams } from '$lib/api/adapters/party.adapter'
|
||||
import { extractErrorMessage } from '$lib/utils/errors'
|
||||
import { transformSkillsToArray } from '$lib/utils/jobSkills'
|
||||
import { findNextEmptySlot, SLOT_NOT_FOUND } from '$lib/utils/gridHelpers'
|
||||
|
|
@ -269,6 +274,16 @@
|
|||
return result.canEdit
|
||||
})
|
||||
|
||||
// Element mapping for theming (used for party element which is numeric)
|
||||
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
|
||||
1: 'wind',
|
||||
2: 'fire',
|
||||
3: 'water',
|
||||
4: 'earth',
|
||||
5: 'dark',
|
||||
6: 'light'
|
||||
}
|
||||
|
||||
// Derived elements for character image logic
|
||||
const mainWeapon = $derived(
|
||||
(party?.weapons ?? []).find((w) => w?.mainhand || w?.position === -1)
|
||||
|
|
@ -276,6 +291,10 @@
|
|||
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
||||
const partyElement = $derived((party as any)?.element)
|
||||
|
||||
// User's element preference (string) - used for UI theming
|
||||
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||
const userElement = $derived(party.user?.avatar?.element as ElementType | undefined)
|
||||
|
||||
// Check if any items in the party are linked to collection (for sync menu option)
|
||||
const hasCollectionLinks = $derived.by(() => {
|
||||
const hasLinkedWeapons = (party?.weapons ?? []).some((w) => w?.collectionWeaponId)
|
||||
|
|
@ -333,7 +352,7 @@
|
|||
}
|
||||
|
||||
// Party operations
|
||||
async function updatePartyDetails(updates: Partial<Party>) {
|
||||
async function updatePartyDetails(updates: Omit<UpdatePartyParams, 'id' | 'shortcode'>) {
|
||||
if (!canEdit()) return
|
||||
|
||||
loading = true
|
||||
|
|
@ -341,7 +360,8 @@
|
|||
|
||||
try {
|
||||
// Use TanStack Query mutation to update party
|
||||
await updatePartyMutation.mutateAsync({ shortcode: party.shortcode, ...updates })
|
||||
// Note: API expects UUID (id), shortcode is for cache invalidation
|
||||
await updatePartyMutation.mutateAsync({ id: party.id, shortcode: party.shortcode, ...updates })
|
||||
// Party will be updated via cache invalidation
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Failed to update party'
|
||||
|
|
@ -417,6 +437,45 @@
|
|||
})
|
||||
}
|
||||
|
||||
function openSettingsPanel() {
|
||||
if (!canEdit()) return
|
||||
|
||||
const initialValues: PartyEditValues = {
|
||||
name: party.name ?? '',
|
||||
fullAuto: party.fullAuto ?? false,
|
||||
autoGuard: party.autoGuard ?? false,
|
||||
autoSummon: party.autoSummon ?? false,
|
||||
chargeAttack: party.chargeAttack ?? true,
|
||||
clearTime: party.clearTime ?? null,
|
||||
buttonCount: party.buttonCount ?? null,
|
||||
chainCount: party.chainCount ?? null,
|
||||
summonCount: party.summonCount ?? null,
|
||||
videoUrl: party.videoUrl ?? null,
|
||||
raid: party.raid ?? null,
|
||||
raidId: party.raid?.id ?? null
|
||||
}
|
||||
|
||||
openPartyEditSidebar({
|
||||
initialValues,
|
||||
element: userElement,
|
||||
onSave: async (values) => {
|
||||
await updatePartyDetails({
|
||||
name: values.name,
|
||||
fullAuto: values.fullAuto,
|
||||
autoGuard: values.autoGuard,
|
||||
autoSummon: values.autoSummon,
|
||||
chargeAttack: values.chargeAttack,
|
||||
clearTime: values.clearTime,
|
||||
buttonCount: values.buttonCount,
|
||||
chainCount: values.chainCount,
|
||||
summonCount: values.summonCount,
|
||||
videoUrl: values.videoUrl,
|
||||
raidId: values.raidId
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteParty() {
|
||||
// Only allow deletion if user owns the party
|
||||
if (party.user?.id !== authUserId) return
|
||||
|
|
@ -888,7 +947,7 @@
|
|||
<DropdownMenu.Content class="dropdown-content" sideOffset={6} align="end">
|
||||
{#if canEdit()}
|
||||
<DropdownItem>
|
||||
<button onclick={openEditDialog} disabled={loading}>Edit</button>
|
||||
<button onclick={openSettingsPanel} disabled={loading}>Edit</button>
|
||||
</DropdownItem>
|
||||
{#if hasCollectionLinks}
|
||||
<DropdownItem>
|
||||
|
|
@ -926,41 +985,12 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{#if party.description || party.raid}
|
||||
<div class="cards">
|
||||
{#if party.description}
|
||||
<div
|
||||
class="description-card clickable"
|
||||
onclick={openDescriptionPanel}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === 'Enter' && openDescriptionPanel()}
|
||||
aria-label="View full description"
|
||||
>
|
||||
<h2 class="card-label">Description</h2>
|
||||
<div class="card-content">
|
||||
<DescriptionRenderer content={party.description} truncate={true} maxLines={4} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if party.raid}
|
||||
<div class="raid-card">
|
||||
<h2 class="card-label">Raid</h2>
|
||||
<div class="raid-content">
|
||||
<span class="raid-name">
|
||||
{typeof party.raid.name === 'string'
|
||||
? party.raid.name
|
||||
: party.raid.name?.en || party.raid.name?.ja || 'Unknown Raid'}
|
||||
</span>
|
||||
{#if party.raid.group}
|
||||
<span class="raid-difficulty">Difficulty: {party.raid.group.difficulty}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<PartyInfoGrid
|
||||
{party}
|
||||
canEdit={canEdit()}
|
||||
onOpenDescription={openDescriptionPanel}
|
||||
onOpenEdit={openSettingsPanel}
|
||||
/>
|
||||
|
||||
<PartySegmentedControl
|
||||
selectedTab={activeTab}
|
||||
|
|
@ -1230,99 +1260,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Cards container
|
||||
.cards {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
|
||||
// Individual card styles
|
||||
.description-card,
|
||||
.raid-card {
|
||||
flex: 1;
|
||||
min-width: 0; // Allow flexbox to shrink items
|
||||
background: var(--card-bg);
|
||||
border: 0.5px solid var(--button-bg);
|
||||
border-radius: $card-corner;
|
||||
padding: $unit-2x;
|
||||
// box-shadow: $card-elevation;
|
||||
text-align: left;
|
||||
|
||||
.card-label {
|
||||
margin: 0 0 $unit 0;
|
||||
font-size: $font-small;
|
||||
font-weight: $bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
line-height: 1.5;
|
||||
|
||||
// Text truncation after 3 lines
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-hint {
|
||||
display: none;
|
||||
margin-top: $unit;
|
||||
font-size: $font-small;
|
||||
color: var(--accent-blue);
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
@include smooth-transition($duration-quick, box-shadow);
|
||||
|
||||
&:hover {
|
||||
box-shadow: $card-elevation-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Specific styling for raid card
|
||||
.raid-card {
|
||||
flex: 0 0 auto;
|
||||
min-width: 250px;
|
||||
|
||||
.raid-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.raid-name {
|
||||
font-weight: $bold;
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
.raid-difficulty {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-small;
|
||||
}
|
||||
}
|
||||
|
||||
// Description card takes up more space
|
||||
.description-card {
|
||||
flex: 2;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: $unit-three-quarter;
|
||||
background: rgba(209, 58, 58, 0.1); // Using raw value since CSS variables don't work in rgba()
|
||||
|
|
|
|||
153
src/lib/components/sidebar/EditRaidPane.svelte
Normal file
153
src/lib/components/sidebar/EditRaidPane.svelte
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* EditRaidPane - Raid selection pane for the sidebar
|
||||
*
|
||||
* Allows users to browse and select a raid from organized groups.
|
||||
* Features section tabs (Raids/Events/Solo), search, and sort toggle.
|
||||
*/
|
||||
import type { RaidFull } from '$lib/types/api/raid'
|
||||
import type { Raid } from '$lib/types/api/entities'
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import { raidQueries } from '$lib/api/queries/raid.queries'
|
||||
import { RaidSection } from '$lib/utils/raidSection'
|
||||
import RaidSectionTabs from '$lib/components/raid/RaidSectionTabs.svelte'
|
||||
import RaidGroupList from '$lib/components/raid/RaidGroupList.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
import Input from '$lib/components/ui/Input.svelte'
|
||||
|
||||
interface Props {
|
||||
/** Currently selected raid */
|
||||
currentRaid?: Raid | null
|
||||
/** Callback when a raid is selected */
|
||||
onSelect: (raid: RaidFull | null) => void
|
||||
}
|
||||
|
||||
let { currentRaid, onSelect }: Props = $props()
|
||||
|
||||
// State
|
||||
let searchQuery = $state('')
|
||||
let selectedSection = $state(RaidSection.Raid)
|
||||
let sortAscending = $state(false)
|
||||
|
||||
// Fetch all raid groups
|
||||
const groupsQuery = createQuery(() => raidQueries.groups())
|
||||
|
||||
// Filter groups by section and sort
|
||||
const filteredGroups = $derived(() => {
|
||||
if (!groupsQuery.data) return []
|
||||
|
||||
// Filter by section
|
||||
let groups = groupsQuery.data.filter((group) => {
|
||||
const groupSection =
|
||||
typeof group.section === 'string' ? parseInt(group.section, 10) : group.section
|
||||
return groupSection === selectedSection
|
||||
})
|
||||
|
||||
// Sort by difficulty
|
||||
groups = [...groups].sort((a, b) => {
|
||||
const diff = a.difficulty - b.difficulty
|
||||
return sortAscending ? diff : -diff
|
||||
})
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
function handleRaidSelect(raid: RaidFull) {
|
||||
// Toggle: clicking selected raid unselects it
|
||||
if (raid.id === currentRaid?.id) {
|
||||
onSelect(null)
|
||||
} else {
|
||||
onSelect(raid)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSort() {
|
||||
sortAscending = !sortAscending
|
||||
}
|
||||
|
||||
const isLoading = $derived(groupsQuery.isLoading)
|
||||
</script>
|
||||
|
||||
<div class="edit-raid-pane">
|
||||
<!-- Search Input -->
|
||||
<div class="search-section">
|
||||
<Input
|
||||
leftIcon="search"
|
||||
placeholder="Search raids..."
|
||||
bind:value={searchQuery}
|
||||
contained
|
||||
clearable
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Controls: Tabs + Sort -->
|
||||
<div class="controls-section">
|
||||
<RaidSectionTabs bind:value={selectedSection} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
icon={sortAscending ? 'arrow-up' : 'arrow-down'}
|
||||
onclick={toggleSort}
|
||||
title="Toggle sort order"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Raid List -->
|
||||
<div class="raid-list-container">
|
||||
{#if isLoading}
|
||||
<div class="loading-state">
|
||||
<span>Loading raids...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<RaidGroupList
|
||||
groups={filteredGroups()}
|
||||
selectedRaidId={currentRaid?.id}
|
||||
onSelect={handleRaidSelect}
|
||||
{searchQuery}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/layout' as *;
|
||||
@use '$src/themes/effects' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
|
||||
.edit-raid-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: $unit-2x $unit-2x $unit $unit-2x;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
padding: $unit $unit-2x $unit-2x $unit-2x;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.raid-list-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $unit-4x;
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
342
src/lib/components/sidebar/PartyEditSidebar.svelte
Normal file
342
src/lib/components/sidebar/PartyEditSidebar.svelte
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* PartyEditSidebar - Edit party metadata (settings, performance, video)
|
||||
*
|
||||
* This sidebar is opened via openPartyEditSidebar() and provides
|
||||
* editing for battle settings, clear time, metrics, video URL, and raid.
|
||||
*/
|
||||
import DetailsSection from './details/DetailsSection.svelte'
|
||||
import DetailRow from './details/DetailRow.svelte'
|
||||
import Input from '$lib/components/ui/Input.svelte'
|
||||
import BattleSettingsSection from '$lib/components/party/edit/BattleSettingsSection.svelte'
|
||||
import ClearTimeInput from '$lib/components/party/edit/ClearTimeInput.svelte'
|
||||
import YouTubeUrlInput from '$lib/components/party/edit/YouTubeUrlInput.svelte'
|
||||
import MetricField from '$lib/components/party/edit/MetricField.svelte'
|
||||
import EditRaidPane from '$lib/components/sidebar/EditRaidPane.svelte'
|
||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import { usePaneStack } from '$lib/stores/paneStack.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import type { Raid } from '$lib/types/api/entities'
|
||||
import type { RaidFull } from '$lib/types/api/raid'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
|
||||
export interface PartyEditValues {
|
||||
name: string
|
||||
fullAuto: boolean
|
||||
autoGuard: boolean
|
||||
autoSummon: boolean
|
||||
chargeAttack: boolean
|
||||
clearTime: number | null
|
||||
buttonCount: number | null
|
||||
chainCount: number | null
|
||||
summonCount: number | null
|
||||
videoUrl: string | null
|
||||
raid: Raid | null
|
||||
raidId: string | null
|
||||
}
|
||||
|
||||
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||
|
||||
interface Props {
|
||||
/** Current party values */
|
||||
initialValues: PartyEditValues
|
||||
/** Party element for switch theming */
|
||||
element?: ElementType
|
||||
/** Callback when save is requested */
|
||||
onSave?: (values: PartyEditValues) => void
|
||||
}
|
||||
|
||||
let { initialValues, element, onSave }: Props = $props()
|
||||
|
||||
// Get the pane stack for pushing EditRaidPane
|
||||
const paneStack = usePaneStack()
|
||||
|
||||
// Local state - initialized from initialValues
|
||||
let name = $state(initialValues.name)
|
||||
let fullAuto = $state(initialValues.fullAuto)
|
||||
let autoGuard = $state(initialValues.autoGuard)
|
||||
let autoSummon = $state(initialValues.autoSummon)
|
||||
let chargeAttack = $state(initialValues.chargeAttack)
|
||||
let clearTime = $state(initialValues.clearTime)
|
||||
let buttonCount = $state(initialValues.buttonCount)
|
||||
let chainCount = $state(initialValues.chainCount)
|
||||
let summonCount = $state(initialValues.summonCount)
|
||||
let videoUrl = $state(initialValues.videoUrl)
|
||||
let raid = $state<Raid | null>(initialValues.raid)
|
||||
let raidId = $state<string | null>(initialValues.raidId)
|
||||
|
||||
// Check if any values have changed
|
||||
const hasChanges = $derived(
|
||||
name !== initialValues.name ||
|
||||
fullAuto !== initialValues.fullAuto ||
|
||||
autoGuard !== initialValues.autoGuard ||
|
||||
autoSummon !== initialValues.autoSummon ||
|
||||
chargeAttack !== initialValues.chargeAttack ||
|
||||
clearTime !== initialValues.clearTime ||
|
||||
buttonCount !== initialValues.buttonCount ||
|
||||
chainCount !== initialValues.chainCount ||
|
||||
summonCount !== initialValues.summonCount ||
|
||||
videoUrl !== initialValues.videoUrl ||
|
||||
raidId !== initialValues.raidId
|
||||
)
|
||||
|
||||
// Expose save function for sidebar action button
|
||||
export function save() {
|
||||
const values: PartyEditValues = {
|
||||
name,
|
||||
fullAuto,
|
||||
autoGuard,
|
||||
autoSummon,
|
||||
chargeAttack,
|
||||
clearTime,
|
||||
buttonCount,
|
||||
chainCount,
|
||||
summonCount,
|
||||
videoUrl,
|
||||
raid,
|
||||
raidId
|
||||
}
|
||||
onSave?.(values)
|
||||
sidebar.close()
|
||||
}
|
||||
|
||||
// Update sidebar action button state based on changes
|
||||
$effect(() => {
|
||||
// Read hasChanges to track it
|
||||
const changed = hasChanges
|
||||
// Use untrack to prevent tracking sidebar mutations
|
||||
untrack(() => {
|
||||
if (changed) {
|
||||
sidebar.setAction(save, 'Save', element)
|
||||
} else {
|
||||
sidebar.setAction(undefined, 'Save', element)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function handleSettingsChange(
|
||||
field: 'fullAuto' | 'autoGuard' | 'autoSummon' | 'chargeAttack',
|
||||
value: boolean
|
||||
) {
|
||||
if (field === 'fullAuto') fullAuto = value
|
||||
else if (field === 'autoGuard') autoGuard = value
|
||||
else if (field === 'autoSummon') autoSummon = value
|
||||
else chargeAttack = value
|
||||
}
|
||||
|
||||
function getRaidName(r: Raid | null): string {
|
||||
if (!r) return ''
|
||||
if (typeof r.name === 'string') return r.name
|
||||
return r.name?.en || r.name?.ja || 'Unknown Raid'
|
||||
}
|
||||
|
||||
function openRaidPane() {
|
||||
paneStack.push({
|
||||
id: 'edit-raid',
|
||||
title: 'Select Raid',
|
||||
component: EditRaidPane,
|
||||
props: {
|
||||
currentRaid: raid,
|
||||
onSelect: handleRaidSelected
|
||||
},
|
||||
scrollable: true
|
||||
})
|
||||
}
|
||||
|
||||
function getRaidElementClass(r: Raid | null): string {
|
||||
if (!r) return ''
|
||||
const elementMap: Record<number, string> = {
|
||||
1: 'wind',
|
||||
2: 'fire',
|
||||
3: 'water',
|
||||
4: 'earth',
|
||||
5: 'dark',
|
||||
6: 'light'
|
||||
}
|
||||
return elementMap[r.element] ?? ''
|
||||
}
|
||||
|
||||
function handleRaidSelected(selectedRaid: RaidFull | null) {
|
||||
if (selectedRaid) {
|
||||
// Convert RaidFull to Raid (they have compatible structures)
|
||||
raid = {
|
||||
id: selectedRaid.id,
|
||||
slug: selectedRaid.slug,
|
||||
name: selectedRaid.name,
|
||||
level: selectedRaid.level,
|
||||
element: selectedRaid.element,
|
||||
group: selectedRaid.group
|
||||
? {
|
||||
id: selectedRaid.group.id,
|
||||
name: selectedRaid.group.name,
|
||||
section: String(selectedRaid.group.section),
|
||||
order: selectedRaid.group.order,
|
||||
difficulty: selectedRaid.group.difficulty,
|
||||
hl: selectedRaid.group.hl,
|
||||
extra: selectedRaid.group.extra,
|
||||
guidebooks: selectedRaid.group.guidebooks
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
raidId = selectedRaid.id
|
||||
} else {
|
||||
raid = null
|
||||
raidId = null
|
||||
}
|
||||
paneStack.pop()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="party-edit-sidebar">
|
||||
<div class="top-fields">
|
||||
<Input
|
||||
label="Title"
|
||||
bind:value={name}
|
||||
placeholder="Enter party title..."
|
||||
contained
|
||||
fullWidth
|
||||
/>
|
||||
<YouTubeUrlInput label="Video" bind:value={videoUrl} contained />
|
||||
</div>
|
||||
|
||||
<DetailsSection title="Battle">
|
||||
<DetailRow label="Raid" noHover compact>
|
||||
{#snippet children()}
|
||||
<button
|
||||
type="button"
|
||||
class="raid-select-button {getRaidElementClass(raid)}"
|
||||
onclick={openRaidPane}
|
||||
>
|
||||
{#if raid}
|
||||
<span class="raid-name">{getRaidName(raid)}</span>
|
||||
{:else}
|
||||
<span class="placeholder">Select raid...</span>
|
||||
{/if}
|
||||
<Icon name="chevron-right" size={16} class="chevron-icon" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</DetailRow>
|
||||
</DetailsSection>
|
||||
|
||||
<BattleSettingsSection
|
||||
bind:fullAuto
|
||||
bind:autoGuard
|
||||
bind:autoSummon
|
||||
bind:chargeAttack
|
||||
{element}
|
||||
onchange={handleSettingsChange}
|
||||
/>
|
||||
|
||||
<DetailsSection title="Performance">
|
||||
<DetailRow label="Clear Time" noHover compact>
|
||||
{#snippet children()}
|
||||
<ClearTimeInput bind:value={clearTime} contained />
|
||||
{/snippet}
|
||||
</DetailRow>
|
||||
<DetailRow label="Button Count" noHover compact>
|
||||
{#snippet children()}
|
||||
<MetricField bind:value={buttonCount} label="B" contained />
|
||||
{/snippet}
|
||||
</DetailRow>
|
||||
<DetailRow label="Chain Count" noHover compact>
|
||||
{#snippet children()}
|
||||
<MetricField bind:value={chainCount} label="C" contained />
|
||||
{/snippet}
|
||||
</DetailRow>
|
||||
<DetailRow label="Summon Count" noHover compact>
|
||||
{#snippet children()}
|
||||
<MetricField bind:value={summonCount} label="S" contained />
|
||||
{/snippet}
|
||||
</DetailRow>
|
||||
</DetailsSection>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/layout' as *;
|
||||
@use '$src/themes/effects' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
|
||||
.party-edit-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-3x;
|
||||
padding-bottom: $unit-2x;
|
||||
}
|
||||
|
||||
.top-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-2x;
|
||||
}
|
||||
|
||||
.raid-select-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.raid-name {
|
||||
font-size: $font-regular;
|
||||
font-weight: $medium;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: $font-regular;
|
||||
font-weight: $medium;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Element-specific colors when raid is selected
|
||||
.raid-select-button {
|
||||
&.wind {
|
||||
.raid-name,
|
||||
.chevron-icon {
|
||||
color: $wind-text-30;
|
||||
}
|
||||
}
|
||||
&.fire {
|
||||
.raid-name,
|
||||
.chevron-icon {
|
||||
color: $fire-text-30;
|
||||
}
|
||||
}
|
||||
&.water {
|
||||
.raid-name,
|
||||
.chevron-icon {
|
||||
color: $water-text-30;
|
||||
}
|
||||
}
|
||||
&.earth {
|
||||
.raid-name,
|
||||
.chevron-icon {
|
||||
color: $earth-text-30;
|
||||
}
|
||||
}
|
||||
&.dark {
|
||||
.raid-name,
|
||||
.chevron-icon {
|
||||
color: $dark-text-30;
|
||||
}
|
||||
}
|
||||
&.light {
|
||||
.raid-name,
|
||||
.chevron-icon {
|
||||
color: $light-text-30;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/lib/features/party/openPartyEditSidebar.svelte.ts
Normal file
34
src/lib/features/party/openPartyEditSidebar.svelte.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import PartyEditSidebar, {
|
||||
type PartyEditValues
|
||||
} from '$lib/components/sidebar/PartyEditSidebar.svelte'
|
||||
|
||||
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||
|
||||
interface PartyEditSidebarOptions {
|
||||
/** Current party values for editing */
|
||||
initialValues: PartyEditValues
|
||||
/** Party element for switch theming */
|
||||
element?: ElementType
|
||||
/** Callback when user saves changes */
|
||||
onSave: (values: PartyEditValues) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the party edit sidebar for editing battle settings, performance metrics, and video URL.
|
||||
*/
|
||||
export function openPartyEditSidebar(options: PartyEditSidebarOptions) {
|
||||
const { initialValues, element, onSave } = options
|
||||
|
||||
sidebar.openWithComponent('Edit Party Settings', PartyEditSidebar, {
|
||||
initialValues,
|
||||
element,
|
||||
onSave
|
||||
})
|
||||
}
|
||||
|
||||
export function closePartyEditSidebar() {
|
||||
sidebar.close()
|
||||
}
|
||||
|
||||
export type { PartyEditValues }
|
||||
Loading…
Reference in a new issue