diff --git a/src/lib/api/adapters/__tests__/party.adapter.test.ts b/src/lib/api/adapters/__tests__/party.adapter.test.ts index d792ab93..e56b33ce 100644 --- a/src/lib/api/adapters/__tests__/party.adapter.test.ts +++ b/src/lib/api/adapters/__tests__/party.adapter.test.ts @@ -117,13 +117,14 @@ describe('PartyAdapter', () => { }) const result = await adapter.update({ + id: 'uuid-123', shortcode: 'ABC123', name: 'Updated Party' }) expect(result).toEqual(updatedParty) expect(global.fetch).toHaveBeenCalledWith( - 'https://api.example.com/parties/ABC123', + 'https://api.example.com/parties/uuid-123', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ diff --git a/src/lib/api/adapters/party.adapter.ts b/src/lib/api/adapters/party.adapter.ts index 7aa40c5b..cc2f786a 100644 --- a/src/lib/api/adapters/party.adapter.ts +++ b/src/lib/api/adapters/party.adapter.ts @@ -21,7 +21,7 @@ export interface CreatePartyParams { description?: string | undefined visibility?: 'public' | 'private' | 'unlisted' | undefined jobId?: string | undefined - raidId?: string | undefined + raidId?: string | null | undefined guidebookId?: string | undefined extras?: Record | undefined } @@ -30,7 +30,24 @@ export interface CreatePartyParams { * Parameters for updating a party */ export interface UpdatePartyParams extends CreatePartyParams { + /** Party UUID (required for API update) */ + id: string + /** Party shortcode (for cache invalidation) */ shortcode: string + // Battle settings + fullAuto?: boolean + autoGuard?: boolean + autoSummon?: boolean + chargeAttack?: boolean + // Performance metrics (null to clear) + clearTime?: number | null + buttonCount?: number | null + chainCount?: number | null + summonCount?: number | null + // Video (null to clear) + videoUrl?: string | null + // Raid (null to clear) + raidId?: string | null } /** @@ -119,10 +136,11 @@ export class PartyAdapter extends BaseAdapter { /** * Updates a party + * Note: API expects UUID for update, not shortcode */ async update(params: UpdatePartyParams): Promise { - const { shortcode, ...updateParams } = params - const response = await this.request<{ party: Party }>(`/parties/${shortcode}`, { + const { id, shortcode, ...updateParams } = params + const response = await this.request<{ party: Party }>(`/parties/${id}`, { method: 'PATCH', body: { party: updateParams diff --git a/src/lib/api/queries/raid.queries.ts b/src/lib/api/queries/raid.queries.ts new file mode 100644 index 00000000..18ef497c --- /dev/null +++ b/src/lib/api/queries/raid.queries.ts @@ -0,0 +1,68 @@ +/** + * Raid Query Options Factory + * + * Provides type-safe, reusable query configurations for raid operations + * using TanStack Query v6 patterns. + * + * @module api/queries/raid + */ + +import { queryOptions } from '@tanstack/svelte-query' +import { raidAdapter } from '$lib/api/adapters/raid.adapter' +import type { RaidFull, RaidGroupFull } from '$lib/types/api/raid' + +/** + * Raid query options factory + * + * Provides query configurations for all raid-related operations. + * + * @example + * ```typescript + * import { createQuery } from '@tanstack/svelte-query' + * import { raidQueries } from '$lib/api/queries/raid.queries' + * + * // All raid groups with their raids + * const groups = createQuery(() => raidQueries.groups()) + * + * // Single raid by slug + * const raid = createQuery(() => raidQueries.bySlug('proto-bahamut')) + * ``` + */ +export const raidQueries = { + /** + * All raid groups with their raids + * + * @returns Query options for fetching all raid groups + */ + groups: () => + queryOptions({ + queryKey: ['raids', 'groups'] as const, + queryFn: () => raidAdapter.getGroups(), + staleTime: 1000 * 60 * 60, // 1 hour - raid data rarely changes + gcTime: 1000 * 60 * 60 * 24 // 24 hours + }), + + /** + * Single raid by slug + * + * @param slug - Raid slug + * @returns Query options for fetching a single raid + */ + bySlug: (slug: string) => + queryOptions({ + queryKey: ['raids', slug] as const, + queryFn: () => raidAdapter.getBySlug(slug), + enabled: !!slug, + staleTime: 1000 * 60 * 60, // 1 hour + gcTime: 1000 * 60 * 60 * 24 // 24 hours + }) +} + +/** + * Query key helpers for cache invalidation + */ +export const raidKeys = { + all: ['raids'] as const, + groups: () => [...raidKeys.all, 'groups'] as const, + detail: (slug: string) => [...raidKeys.all, slug] as const +} diff --git a/src/lib/api/schemas/party.ts b/src/lib/api/schemas/party.ts index 60d8507f..0f23d274 100644 --- a/src/lib/api/schemas/party.ts +++ b/src/lib/api/schemas/party.ts @@ -120,6 +120,8 @@ const MinimalScalarsSchema = z buttonCount: z.number().nullish().optional(), chainCount: z.number().nullish().optional(), turnCount: z.number().nullish().optional(), + summonCount: z.number().nullish().optional(), + videoUrl: z.string().nullish().optional(), visibility: z.enum(['public', 'private', 'unlisted']).nullish().optional() }) .partial() @@ -373,6 +375,10 @@ export const PartySchemaRaw = z.object({ button_count: z.number().nullish(), turn_count: z.number().nullish(), chain_count: z.number().nullish(), + summon_count: z.number().nullish(), + + // Video URL + video_url: z.string().nullish(), // Relations raid_id: z.string().nullish(), diff --git a/src/lib/components/party/Party.svelte b/src/lib/components/party/Party.svelte index f5b37067..034b6414 100644 --- a/src/lib/components/party/Party.svelte +++ b/src/lib/components/party/Party.svelte @@ -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 = { + 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) { + async function updatePartyDetails(updates: Omit) { 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 @@ {#if canEdit()} - + {#if hasCollectionLinks} @@ -926,41 +985,12 @@ - {#if party.description || party.raid} -
- {#if party.description} -
e.key === 'Enter' && openDescriptionPanel()} - aria-label="View full description" - > -

Description

-
- -
-
- {/if} - - {#if party.raid} -
-

Raid

-
- - {typeof party.raid.name === 'string' - ? party.raid.name - : party.raid.name?.en || party.raid.name?.ja || 'Unknown Raid'} - - {#if party.raid.group} - Difficulty: {party.raid.group.difficulty} - {/if} -
-
- {/if} -
- {/if} + + /** + * BattleSettingsSection - Switch toggles for battle settings + * + * Displays toggles for Full Auto, Auto Guard, Auto Summon, and Charge Attack. + */ + import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte' + import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte' + import Switch from '$lib/components/ui/switch/Switch.svelte' + + type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' + + interface Props { + fullAuto?: boolean + autoGuard?: boolean + autoSummon?: boolean + chargeAttack?: boolean + /** Element for switch color theming */ + element?: ElementType + onchange?: ( + field: 'fullAuto' | 'autoGuard' | 'autoSummon' | 'chargeAttack', + value: boolean + ) => void + disabled?: boolean + } + + let { + fullAuto = $bindable(false), + autoGuard = $bindable(false), + autoSummon = $bindable(false), + chargeAttack = $bindable(true), + element, + onchange, + disabled = false + }: Props = $props() + + function handleChange( + 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 + + onchange?.(field, value) + } + + + + + {#snippet children()} + handleChange('chargeAttack', v)} + /> + {/snippet} + + + + {#snippet children()} + handleChange('fullAuto', v)} + /> + {/snippet} + + + + {#snippet children()} + handleChange('autoSummon', v)} + /> + {/snippet} + + + + {#snippet children()} + handleChange('autoGuard', v)} + /> + {/snippet} + + diff --git a/src/lib/components/party/edit/ClearTimeInput.svelte b/src/lib/components/party/edit/ClearTimeInput.svelte new file mode 100644 index 00000000..d939c3ab --- /dev/null +++ b/src/lib/components/party/edit/ClearTimeInput.svelte @@ -0,0 +1,204 @@ + + +
+
+ + min +
+ : +
+ + sec +
+
+ + diff --git a/src/lib/components/party/edit/MetricField.svelte b/src/lib/components/party/edit/MetricField.svelte new file mode 100644 index 00000000..5a2675a7 --- /dev/null +++ b/src/lib/components/party/edit/MetricField.svelte @@ -0,0 +1,129 @@ + + +
+ + {label} +
+ + diff --git a/src/lib/components/party/edit/YouTubeUrlInput.svelte b/src/lib/components/party/edit/YouTubeUrlInput.svelte new file mode 100644 index 00000000..e62b561e --- /dev/null +++ b/src/lib/components/party/edit/YouTubeUrlInput.svelte @@ -0,0 +1,347 @@ + + +
+ {#if label} + + {/if} +
+ + {#if inputValue && !disabled} + + {/if} +
+ + {#if showError} +

Please enter a valid YouTube URL

+ {/if} + + {#if showPreview && thumbnailUrl} +
+ + Video thumbnail +
+ + + +
+
+
+ {#if isLoadingMetadata} + Loading... + {:else if videoTitle} + {videoTitle} + {/if} +
+
+ {/if} +
+ + diff --git a/src/lib/components/party/info/DescriptionTile.svelte b/src/lib/components/party/info/DescriptionTile.svelte new file mode 100644 index 00000000..ee23b559 --- /dev/null +++ b/src/lib/components/party/info/DescriptionTile.svelte @@ -0,0 +1,29 @@ + + + + {#if description} + + {:else} + No description + {/if} + + + diff --git a/src/lib/components/party/info/InfoTile.svelte b/src/lib/components/party/info/InfoTile.svelte new file mode 100644 index 00000000..2923e5e6 --- /dev/null +++ b/src/lib/components/party/info/InfoTile.svelte @@ -0,0 +1,73 @@ + + +
e.key === 'Enter' && onclick?.() : undefined} +> + {#if label} +

{label}

+ {/if} +
+ {@render children()} +
+
+ + diff --git a/src/lib/components/party/info/PartyInfoGrid.svelte b/src/lib/components/party/info/PartyInfoGrid.svelte new file mode 100644 index 00000000..09701e9e --- /dev/null +++ b/src/lib/components/party/info/PartyInfoGrid.svelte @@ -0,0 +1,140 @@ + + +
+ +
+ {#if showDescription} + + {/if} + + {#if showRaid} + + {/if} +
+ + +
+ {#if showPerformance} + + {/if} + + + + {#if showVideo} + + {/if} +
+
+ + diff --git a/src/lib/components/party/info/PerformanceTile.svelte b/src/lib/components/party/info/PerformanceTile.svelte new file mode 100644 index 00000000..54426ae2 --- /dev/null +++ b/src/lib/components/party/info/PerformanceTile.svelte @@ -0,0 +1,89 @@ + + + +
+
+ + + + + {formatClearTime(clearTime)} +
+
+ {bcsDisplay()} +
+
+
+ + diff --git a/src/lib/components/party/info/RaidTile.svelte b/src/lib/components/party/info/RaidTile.svelte new file mode 100644 index 00000000..18640cb8 --- /dev/null +++ b/src/lib/components/party/info/RaidTile.svelte @@ -0,0 +1,57 @@ + + + + {#if raid} +
+ {raidName()} + {#if raid.group?.difficulty} + Lv. {raid.group.difficulty} + {/if} +
+ {:else} + No raid selected + {/if} +
+ + diff --git a/src/lib/components/party/info/SettingsTile.svelte b/src/lib/components/party/info/SettingsTile.svelte new file mode 100644 index 00000000..17cd4e81 --- /dev/null +++ b/src/lib/components/party/info/SettingsTile.svelte @@ -0,0 +1,83 @@ + + + +
+ {#each settings as setting (setting.key)} + + {setting.label} + + {/each} +
+
+ + diff --git a/src/lib/components/party/info/VideoTile.svelte b/src/lib/components/party/info/VideoTile.svelte new file mode 100644 index 00000000..73ff2c7e --- /dev/null +++ b/src/lib/components/party/info/VideoTile.svelte @@ -0,0 +1,198 @@ + + + + {#if videoUrl && videoId} +
+ {#if isPlaying && embedUrl} +
+ +
+ {:else if thumbnailUrl} + + {/if} + {#if videoTitle} +

{videoTitle}

+ {/if} +
+ {:else} + No video + {/if} +
+ + diff --git a/src/lib/components/raid/RaidGroupItem.svelte b/src/lib/components/raid/RaidGroupItem.svelte new file mode 100644 index 00000000..5a935c31 --- /dev/null +++ b/src/lib/components/raid/RaidGroupItem.svelte @@ -0,0 +1,161 @@ + + +
+
+ {getGroupName(group)} + {#if group.extra} + EX + {/if} +
+ +
+ {#each group.raids as raid (raid.id)} + {@const isSelected = raid.id === selectedRaidId} + + {/each} +
+
+ + diff --git a/src/lib/components/raid/RaidGroupList.svelte b/src/lib/components/raid/RaidGroupList.svelte new file mode 100644 index 00000000..b8a95ea4 --- /dev/null +++ b/src/lib/components/raid/RaidGroupList.svelte @@ -0,0 +1,95 @@ + + +
+ {#if hasResults} + {#each filteredGroups() as group (group.id)} + + {/each} + {:else} +
+ No raids found +
+ {/if} +
+ + diff --git a/src/lib/components/raid/RaidSectionTabs.svelte b/src/lib/components/raid/RaidSectionTabs.svelte new file mode 100644 index 00000000..2b107568 --- /dev/null +++ b/src/lib/components/raid/RaidSectionTabs.svelte @@ -0,0 +1,43 @@ + + + + {#each sections as section (section.value)} + {section.label} + {/each} + diff --git a/src/lib/components/sidebar/EditRaidPane.svelte b/src/lib/components/sidebar/EditRaidPane.svelte new file mode 100644 index 00000000..31227f17 --- /dev/null +++ b/src/lib/components/sidebar/EditRaidPane.svelte @@ -0,0 +1,153 @@ + + + + +
+ +
+ +
+ + +
+ +
+ + +
+ {#if isLoading} +
+ Loading raids... +
+ {:else} + + {/if} +
+
+ + diff --git a/src/lib/components/sidebar/PartyEditSidebar.svelte b/src/lib/components/sidebar/PartyEditSidebar.svelte new file mode 100644 index 00000000..367f52e6 --- /dev/null +++ b/src/lib/components/sidebar/PartyEditSidebar.svelte @@ -0,0 +1,342 @@ + + +
+
+ + +
+ + + + {#snippet children()} + + {/snippet} + + + + + + + + {#snippet children()} + + {/snippet} + + + {#snippet children()} + + {/snippet} + + + {#snippet children()} + + {/snippet} + + + {#snippet children()} + + {/snippet} + + +
+ + diff --git a/src/lib/components/sidebar/details/DetailRow.svelte b/src/lib/components/sidebar/details/DetailRow.svelte index 1c76a663..0e4d3409 100644 --- a/src/lib/components/sidebar/details/DetailRow.svelte +++ b/src/lib/components/sidebar/details/DetailRow.svelte @@ -9,12 +9,14 @@ noHover?: boolean /** Remove padding for inline edit contexts */ noPadding?: boolean + /** Remove min-width from value (for compact controls like switches) */ + compact?: boolean } - let { label, value, children, noHover = false, noPadding = false }: Props = $props() + let { label, value, children, noHover = false, noPadding = false, compact = false }: Props = $props() -
+
{label} {#if children} @@ -62,5 +64,9 @@ text-align: right; min-width: 180px; } + + &.compact .value { + min-width: unset; + } } diff --git a/src/lib/components/ui/Input.svelte b/src/lib/components/ui/Input.svelte index 1bccea5a..4258d5de 100644 --- a/src/lib/components/ui/Input.svelte +++ b/src/lib/components/ui/Input.svelte @@ -10,6 +10,8 @@ label?: string leftIcon?: string rightIcon?: string + clearable?: boolean + onClear?: () => void counter?: number maxLength?: number hidden?: boolean @@ -31,6 +33,8 @@ label, leftIcon, rightIcon, + clearable = false, + onClear, counter, maxLength, hidden = false, @@ -69,7 +73,12 @@ const showCounter = $derived( counter !== undefined || (charsRemaining !== undefined && charsRemaining <= 5) ) - const hasWrapper = $derived(accessory || leftIcon || rightIcon || maxLength !== undefined || validationIcon) + const hasWrapper = $derived(accessory || leftIcon || rightIcon || clearable || maxLength !== undefined || validationIcon) + + function handleClear() { + value = '' + onClear?.() + } const fieldsetClasses = $derived( ['fieldset', hidden && 'hidden', fullWidth && 'full', className].filter(Boolean).join(' ') @@ -92,11 +101,6 @@ .join(' ') ) - // Debug: log what's in restProps - $effect(() => { - console.log('[Input] restProps keys:', Object.keys(restProps)) - console.log('[Input] hasWrapper:', hasWrapper, 'validationIcon:', validationIcon) - })
@@ -144,6 +148,12 @@ {/if} + {#if clearable && value} + + {/if} + {#if showCounter} {charsRemaining !== undefined ? charsRemaining : currentCount} @@ -345,6 +355,32 @@ } } + .clearButton { + position: absolute; + right: $unit-2x; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + border: none; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + border-radius: $unit-half; + @include smooth-transition($duration-quick, background-color, color); + + &:hover { + background: var(--surface-tertiary); + color: var(--text-primary); + } + + :global(svg) { + fill: currentColor; + } + } + &:has(.iconLeft) input { padding-left: $unit-5x; } @@ -357,6 +393,10 @@ padding-right: $unit-5x; } + &:has(.clearButton) input { + padding-right: $unit-5x; + } + &:has(.counter) input { padding-right: $unit-8x; } diff --git a/src/lib/components/ui/switch/Switch.svelte b/src/lib/components/ui/switch/Switch.svelte index 0eaf1fe7..5e450f28 100644 --- a/src/lib/components/ui/switch/Switch.svelte +++ b/src/lib/components/ui/switch/Switch.svelte @@ -69,16 +69,18 @@ --sw-checked-bg-hover: var(--null-button-bg-hover); background: $grey-70; - border: 2px solid transparent; + border: none; box-sizing: border-box; position: relative; cursor: pointer; + display: flex; + align-items: center; @include smooth-transition($duration-instant, background-color); } :global([data-switch-root].switch:focus), :global([data-switch-root].switch:focus-visible) { - @include focus-ring($blue); + outline: none; } :global([data-switch-root].switch:hover:not(:disabled)) { @@ -139,76 +141,93 @@ cursor: not-allowed; } - // Size: Small + // Size: Small (22px track height, 16px thumb) :global([data-switch-root].switch.small) { - $height: $unit-3x; // 24px - border-radius: calc($height / 2); - padding-left: $unit-fourth; - padding-right: $unit-fourth; - width: calc($unit-5x + $unit-half); // 44px - height: $height; + $track-height: 22px; + $thumb-size: 16px; + $track-padding: 3px; + $track-width: 40px; + + border-radius: calc($track-height / 2); + padding: 0 $track-padding; + width: $track-width; + height: $track-height; } :global([data-switch-root].switch.small .thumb) { - height: calc($unit-2x + $unit-half); // 20px - width: calc($unit-2x + $unit-half); // 20px - border-radius: calc(($unit-2x + $unit-half) / 2); + $thumb-size: 16px; + height: $thumb-size; + width: $thumb-size; + border-radius: 50%; } :global([data-switch-root].switch.small .thumb[data-state='checked']) { - transform: translateX(calc($unit-2x + $unit-half)); // 20px + // Move distance: track-width - thumb-size - (2 * padding) + transform: translateX(18px); } - // Size: Medium (default) + // Size: Medium (default) (26px track height, 20px thumb) :global([data-switch-root].switch.medium) { - $height: calc($unit-4x + $unit-fourth); // 34px - border-radius: calc($height / 2); - padding-left: $unit-half; - padding-right: $unit-half; - width: $unit-7x + $unit-fourth; // 58px - height: $height; + $track-height: 26px; + $thumb-size: 20px; + $track-padding: 3px; + $track-width: 48px; + + border-radius: calc($track-height / 2); + padding: 0 $track-padding; + width: $track-width; + height: $track-height; } :global([data-switch-root].switch.medium .thumb) { - height: $unit-3x + $unit-fourth; // 26px - width: $unit-3x + $unit-fourth; // 26px - border-radius: calc(($unit-3x + $unit-fourth) / 2); + $thumb-size: 20px; + height: $thumb-size; + width: $thumb-size; + border-radius: 50%; } :global([data-switch-root].switch.medium .thumb[data-state='checked']) { - transform: translateX(21px); + // Move distance: track-width - thumb-size - (2 * padding) + transform: translateX(22px); } - // Size: Large + // Size: Large (30px track height, 24px thumb) :global([data-switch-root].switch.large) { - $height: $unit-5x; // 40px - border-radius: calc($height / 2); - padding-left: $unit-half; - padding-right: $unit-half; - width: calc($unit-8x + $unit); // 72px - height: $height; + $track-height: 30px; + $thumb-size: 24px; + $track-padding: 3px; + $track-width: 56px; + + border-radius: calc($track-height / 2); + padding: 0 $track-padding; + width: $track-width; + height: $track-height; } :global([data-switch-root].switch.large .thumb) { - height: calc($unit-4x); // 32px - width: calc($unit-4x); // 32px - border-radius: $unit-2x; + $thumb-size: 24px; + height: $thumb-size; + width: $thumb-size; + border-radius: 50%; } :global([data-switch-root].switch.large .thumb[data-state='checked']) { - transform: translateX(calc($unit-4x)); // 32px + // Move distance: track-width - thumb-size - (2 * padding) + transform: translateX(26px); } // Thumb base styles :global([data-switch-root] .thumb) { - background: $grey-100; + background: white; display: block; + flex-shrink: 0; @include smooth-transition($duration-instant, transform); - transform: translateX(-1px); + transform: translateX(0); cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } :global([data-switch-root] .thumb[data-state='checked']) { - background: $grey-100; + background: white; } diff --git a/src/lib/features/party/openPartyEditSidebar.svelte.ts b/src/lib/features/party/openPartyEditSidebar.svelte.ts new file mode 100644 index 00000000..b0859f31 --- /dev/null +++ b/src/lib/features/party/openPartyEditSidebar.svelte.ts @@ -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 } diff --git a/src/lib/types/api/party.ts b/src/lib/types/api/party.ts index 2f5e79e7..b66d34ae 100644 --- a/src/lib/types/api/party.ts +++ b/src/lib/types/api/party.ts @@ -107,6 +107,8 @@ export interface Party { buttonCount?: number turnCount?: number chainCount?: number + summonCount?: number + videoUrl?: string visibility?: import('$lib/types/visibility').PartyVisibility element?: number favorited?: boolean