Files
chitai/frontend/src/lib/components/forms/edit-book/edit-book.svelte
T
patrick 5f2d68694d feat: rebuild the edit dialog around a cover and files rail
Drops the three tabs for a wide dialog: cover and files in a fixed rail,
all fourteen metadata fields grouped beside them, Save in the footer.

Adds file management, which the Files tab never had — it could only upload,
never list or remove. Files now show size and format with download and
delete, the latter asking whether to remove it from disk too.
2026-08-12 11:40:49 -04:00

69 lines
2.7 KiB
Svelte

<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { type Book } from '$lib/schema';
import EditCover from './edit-cover.svelte';
import EditFiles from './edit-files.svelte';
import EditMetadata from './edit-metadata.svelte';
let { book, open = $bindable() }: { book?: Book; open: boolean } = $props();
/**
* The metadata form lives in a child, but Save belongs in the dialog footer —
* a submit button reaches it by id rather than the footer having to duplicate
* the submit logic.
*/
const METADATA_FORM_ID = 'edit-book-metadata';
</script>
<!--
This dialog is mounted once, in (root)/(library)/+layout.svelte, and `book`
changes underneath it as different books are edited. bookToEdit is never
cleared, so {#if book} stays true and the forms never unmount — they seed
local state from `book` on mount, so without this key you would open Edit on a
second book and see the first book's authors, tags and cover, then save them
onto the wrong record. Keying on the id remounts the forms per book.
-->
<Dialog.Root bind:open>
{#if book}
{#key book.id}
<Dialog.Content
class="flex h-[85vh] max-w-[min(72rem,95vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-[min(72rem,95vw)]"
>
<Dialog.Header class="shrink-0 space-y-0 border-b px-5 py-3 text-left">
<Dialog.Title class="truncate font-serif text-base font-normal">{book.title}</Dialog.Title
>
<Dialog.Description class="truncate text-xs">
{book.authors.map((author) => author.name).join(', ') || 'Unknown author'}
</Dialog.Description>
</Dialog.Header>
<!-- Rail and form scroll independently so the footer never moves -->
<div class="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[16rem_1fr]">
<aside
class="flex min-w-0 flex-col gap-5 overflow-y-auto border-b bg-sidebar p-4 md:border-r md:border-b-0"
>
<EditCover {book} />
<EditFiles {book} />
</aside>
<div class="min-h-0 overflow-y-auto p-5">
<EditMetadata {book} bind:open formId={METADATA_FORM_ID} />
</div>
</div>
<Dialog.Footer class="shrink-0 items-center gap-2 border-t px-5 py-3 sm:justify-between">
<!-- Cover and file changes hit the server as they happen, while the
fields wait for Save. Saying so is the cheapest way to stop
Cancel reading as "undo everything". -->
<p class="text-xs text-muted-foreground">Cover and file changes apply immediately</p>
<div class="flex gap-2">
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
<Button type="submit" form={METADATA_FORM_ID}>Save changes</Button>
</div>
</Dialog.Footer>
</Dialog.Content>
{/key}
{/if}
</Dialog.Root>