hensei-web/src/lib/features/database/detail/DetailScaffold.svelte

263 lines
6.1 KiB
Svelte

<svelte:options runes={true} />
<script lang="ts">
import { DropdownMenu } from 'bits-ui'
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
import Button from '$lib/components/ui/Button.svelte'
import type { Snippet } from 'svelte'
export type DetailTab = 'info' | 'images' | 'raw'
interface Props {
type: 'character' | 'summon' | 'weapon' | 'job'
item: any
image: string
showEdit?: boolean
editUrl?: string
editMode?: boolean
isSaving?: boolean
saveSuccess?: boolean
saveError?: string | null
onSave?: () => void
onCancel?: () => void
// Tab navigation
currentTab?: DetailTab
onTabChange?: (tab: DetailTab) => void
showTabs?: boolean
// Image download handlers
onDownloadAllImages?: (force: boolean) => Promise<void>
onDownloadSize?: (size: string) => Promise<void>
availableSizes?: string[]
}
let {
type,
item,
image,
showEdit = false,
editUrl,
editMode = false,
isSaving = false,
saveSuccess = false,
saveError = null,
onSave,
onCancel,
currentTab = 'info',
onTabChange,
showTabs = true,
onDownloadAllImages,
onDownloadSize,
availableSizes = [],
children
}: Props & { children: Snippet } = $props()
let isDownloading = $state(false)
let dropdownOpen = $state(false)
function handleTabChange(value: string) {
onTabChange?.(value as DetailTab)
}
async function handleDownloadAll(force: boolean) {
if (!onDownloadAllImages) return
isDownloading = true
dropdownOpen = false
try {
await onDownloadAllImages(force)
} finally {
isDownloading = false
}
}
async function handleDownloadSize(size: string) {
if (!onDownloadSize) return
isDownloading = true
dropdownOpen = false
try {
await onDownloadSize(size)
} finally {
isDownloading = false
}
}
// Show download button when on images tab and can edit
const showDownloadDropdown = $derived(
showEdit && currentTab === 'images' && onDownloadAllImages && !editMode
)
</script>
<div class="content">
<DetailsHeader
{type}
{item}
{image}
{editMode}
{showEdit}
{editUrl}
onSave={onSave ?? (() => {})}
onCancel={onCancel ?? (() => {})}
{isSaving}
/>
{#if showTabs && !editMode}
<div class="tab-navigation">
<SegmentedControl
value={currentTab}
onValueChange={handleTabChange}
variant="background"
size="small"
>
<Segment value="info">Info</Segment>
<Segment value="images">Images</Segment>
<Segment value="raw">Raw Data</Segment>
</SegmentedControl>
{#if showDownloadDropdown}
<div class="download-dropdown">
<DropdownMenu.Root bind:open={dropdownOpen}>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="secondary" size="small" disabled={isDownloading}>
{isDownloading ? 'Downloading...' : 'Download Images'}
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="dropdown-menu" sideOffset={4}>
<DropdownMenu.Item
class="dropdown-menu-item"
onclick={() => handleDownloadAll(false)}
>
Download All Images
</DropdownMenu.Item>
<DropdownMenu.Item
class="dropdown-menu-item"
onclick={() => handleDownloadAll(true)}
>
Re-download All Images
</DropdownMenu.Item>
{#if availableSizes.length > 0}
<DropdownMenu.Separator class="dropdown-menu-separator" />
{#each availableSizes as size}
<DropdownMenu.Item
class="dropdown-menu-item"
onclick={() => handleDownloadSize(size)}
>
Download All "{size}" Images
</DropdownMenu.Item>
{/each}
{/if}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
{/if}
</div>
{/if}
{#if saveSuccess || saveError}
<div class="edit-controls">
{#if saveSuccess}
<span class="success-message">Changes saved successfully!</span>
{/if}
{#if saveError}
<span class="error-message">{saveError}</span>
{/if}
</div>
{/if}
{@render children?.()}
</div>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/layout' as layout;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
@use '$src/themes/effects' as effects;
.content {
background: white;
border-radius: layout.$card-corner;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: visible;
position: relative;
}
.tab-navigation {
padding: spacing.$unit-2x;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: spacing.$unit;
}
.download-dropdown {
margin-left: auto;
}
.edit-controls {
padding: spacing.$unit-2x;
border-bottom: 1px solid colors.$grey-80;
display: flex;
gap: spacing.$unit;
align-items: center;
.success-message {
color: colors.$grey-30;
font-size: typography.$font-small;
animation: fadeIn effects.$duration-opacity-fade ease-in;
}
.error-message {
color: colors.$error;
font-size: typography.$font-small;
animation: fadeIn effects.$duration-opacity-fade ease-in;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
// Import menu styles
:global(.dropdown-menu) {
background: var(--app-bg, white);
border: 1px solid var(--border-color, #ddd);
border-radius: layout.$card-corner;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: spacing.$unit-half;
min-width: calc(spacing.$unit * 22.5);
z-index: 200;
}
:global(.dropdown-menu-item) {
padding: spacing.$unit spacing.$unit-2x;
border-radius: layout.$item-corner-small;
cursor: pointer;
font-size: typography.$font-regular;
color: var(--text-primary);
display: flex;
align-items: center;
gap: spacing.$unit;
&:hover {
background: var(--button-contained-bg-hover, #f5f5f5);
}
}
:global(.dropdown-menu-separator) {
height: 1px;
background: var(--menu-separator, #e5e5e5);
margin: spacing.$unit-half 0;
}
</style>