From 32147194fdff92615e6ba43ef002d33d4f8113b9 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sat, 25 Apr 2026 13:39:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(comic):=20DataCloneError=20beim=20Anlegen?= =?UTF-8?q?=20+=20st=C3=A4rkere=20Hover-States?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Bugs beim ersten Anlegen einer Comic-Story: 1. **DataCloneError "[object Array] could not be cloned"** beim `comicStoriesTable.add(newLocal)`. Ursache: StoryForm deklariert `characterMediaIds`/`tags` als `$state([])` und reicht die Proxies direkt an comicStoriesStore.createStory() durch. Dexie/IndexedDB's structured-clone refuseiert Svelte-5-State- Proxies — `tags`+`panelMeta` werden vorher von encryptRecord zu Ciphertext-Strings gewandelt, aber `characterMediaIds` (und `panelImageIds`/`tags` falls plaintext) bleiben Arrays und die schmieren als Proxy in den IDB-Write rein. Fix: Arrays beim Store-Eintritt mit `[...arr]` snapshotten. Greift jetzt auch in updateStory() für die gleichen Felder (zukünftiges StoryForm-Edit-Mode wäre sonst denselben Bug wert). Wardrobe hat das Problem latent auch, fix dort folgt wenn jemand auf gleiche Stelle stößt — die Comics-Lösung isoliert. 2. **Kein Button-Feel auf der Create-Seite.** StylePicker und CharacterPicker hatten zu schwache Tailwind-Hover-Klassen (`hover:bg-muted` only) — User las die Tiles nicht klar als klickbar. StylePicker komplett auf scoped CSS umgezogen (Pattern wie PanelModelPicker / wardrobe TryOnModelPicker): hover ändert border-color (border-primary/50) UND background (primary/5) UND fügt einen schwachen Schatten hinzu, plus active:translate-y-px für Touch-Feedback. Active-Tile bekommt klaren primary-Border + primary/8-bg + Schatten. Focus-visible mit Outline für Keyboard-Nav. role="radiogroup" + aria-checked statt aria-pressed für korrekte Semantik. CharacterPicker: Body-Ref-Toggle bekommt jetzt hover:border-primary/50 + hover:shadow-sm und beim aktiven State einen leichten primary- Schatten. Add-Garment-Button kriegt hover:border-primary/50 + hover:bg-primary/5 + hover:shadow-sm. 5 Encryption-Tests weiter grün. check für comic-Files clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../comic/components/CharacterPicker.svelte | 11 ++- .../comic/components/StylePicker.svelte | 77 +++++++++++++++++-- .../modules/comic/stores/stories.svelte.ts | 19 ++++- 3 files changed, 93 insertions(+), 14 deletions(-) diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/CharacterPicker.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/CharacterPicker.svelte index 8277104b4..2ae71ef6f 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/CharacterPicker.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/CharacterPicker.svelte @@ -142,8 +142,10 @@ type="button" {disabled} onclick={toggleBody} - class="relative h-20 w-20 overflow-hidden rounded-md border transition-colors - {bodyInValue ? 'border-primary/50' : 'border-border opacity-50 hover:opacity-100'}" + class="relative h-20 w-20 overflow-hidden rounded-md border transition-all active:translate-y-px + {bodyInValue + ? 'border-primary shadow-sm shadow-primary/20' + : 'border-border opacity-60 hover:border-primary/50 hover:opacity-100 hover:shadow-sm'}" aria-pressed={bodyInValue} title={bodyInValue ? 'Body-Ref entfernen' : 'Body-Ref hinzufügen'} > @@ -208,7 +210,10 @@ type="button" {disabled} onclick={() => (showGarmentPicker = !showGarmentPicker)} - class="flex h-20 w-20 flex-col items-center justify-center gap-1 rounded-md border border-dashed border-border bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + class="flex h-20 w-20 flex-col items-center justify-center gap-1 rounded-md border border-dashed border-border bg-background text-muted-foreground transition-all hover:border-primary/50 hover:bg-primary/5 hover:text-foreground hover:shadow-sm active:translate-y-px" + class:!border-primary={showGarmentPicker} + class:!bg-primary={showGarmentPicker} + class:bg-opacity-10={showGarmentPicker} aria-expanded={showGarmentPicker} > diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/StylePicker.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/StylePicker.svelte index 5db300a9b..cdf703843 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/StylePicker.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/StylePicker.svelte @@ -3,6 +3,11 @@ story-create time, fixed afterward (restyling = new story). Each tile carries a short "what this looks like" hint so the user can pick without having to memorise the preset mapping in styles.ts. + + Markup pattern matches `PanelModelPicker` / wardrobe's + `TryOnModelPicker` so the create-flow has a coherent "click these + cards" affordance — both border AND background shift on hover so + the tiles read clearly as buttons. --> -
+
{#each STYLE_ORDER as style (style)} {/each}
+ + diff --git a/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts b/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts index 27b964901..5402e58e6 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts @@ -35,16 +35,21 @@ export const comicStoriesStore = { if (input.characterMediaIds.length === 0) { throw new Error('Story needs at least one character reference image'); } + // Spread incoming arrays to break Svelte 5 $state proxies — the + // caller (StoryForm) declares `characterMediaIds`/`tags` as + // `$state([])` and passes them directly. IndexedDB's + // structured-clone refuses to clone proxies, so without this + // `comicStoriesTable.add(...)` throws DataCloneError. const newLocal: LocalComicStory = { id: crypto.randomUUID(), title: input.title, description: input.description ?? null, style: input.style, - characterMediaIds: input.characterMediaIds, + characterMediaIds: [...input.characterMediaIds], storyContext: input.storyContext ?? null, panelImageIds: [], panelMeta: {}, - tags: input.tags ?? [], + tags: input.tags ? [...input.tags] : [], isFavorite: input.isFavorite ?? false, visibility: defaultVisibilityFor(getActiveSpace()?.type), }; @@ -64,7 +69,15 @@ export const comicStoriesStore = { Pick > ): Promise { - const wrapped = { ...patch } as Record; + // Same proxy-breaking copy as createStory: any array on the patch + // might be a $state proxy if the caller is a Svelte 5 component. + const wrapped: Record = { ...patch }; + if (Array.isArray(wrapped.characterMediaIds)) { + wrapped.characterMediaIds = [...(wrapped.characterMediaIds as string[])]; + } + if (Array.isArray(wrapped.tags)) { + wrapped.tags = [...(wrapped.tags as string[])]; + } await encryptRecord('comicStories', wrapped); await comicStoriesTable.update(id, { ...wrapped,