mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
fix(comic): DataCloneError beim Anlegen + stärkere Hover-States
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<string[]>([])` 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) <noreply@anthropic.com>
This commit is contained in:
parent
75c366bff4
commit
32147194fd
3 changed files with 93 additions and 14 deletions
|
|
@ -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}
|
||||
>
|
||||
<Plus size={16} />
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { STYLE_ORDER, STYLE_LABELS } from '../constants';
|
||||
|
|
@ -27,20 +32,76 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<div class="picker" role="radiogroup" aria-label="Comic-Stil">
|
||||
{#each STYLE_ORDER as style (style)}
|
||||
<button
|
||||
type="button"
|
||||
class="option"
|
||||
class:active={value === style}
|
||||
role="radio"
|
||||
aria-checked={value === style}
|
||||
{disabled}
|
||||
onclick={() => onChange(style)}
|
||||
class="rounded-lg border px-3 py-2.5 text-left transition-colors
|
||||
{value === style
|
||||
? 'border-primary bg-primary/10 text-foreground'
|
||||
: 'border-border bg-background text-foreground hover:bg-muted'}"
|
||||
aria-pressed={value === style}
|
||||
>
|
||||
<div class="text-sm font-medium">{STYLE_LABELS[style].de}</div>
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">{HINTS[style]}</div>
|
||||
<span class="label">{STYLE_LABELS[style].de}</span>
|
||||
<span class="hint">{HINTS[style]}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-background) / 0.5);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background-color 0.15s,
|
||||
box-shadow 0.15s,
|
||||
transform 0.05s;
|
||||
}
|
||||
.option:hover:not([disabled]):not(.active) {
|
||||
border-color: hsl(var(--color-primary) / 0.5);
|
||||
background: hsl(var(--color-primary) / 0.05);
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 0.04);
|
||||
}
|
||||
.option:active:not([disabled]) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.option.active {
|
||||
border-color: hsl(var(--color-primary));
|
||||
background: hsl(var(--color-primary) / 0.08);
|
||||
box-shadow: 0 1px 3px hsl(var(--color-primary) / 0.15);
|
||||
}
|
||||
.option:focus-visible {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.hint {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.35;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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<string[]>([])` 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<LocalComicStory, 'title' | 'description' | 'storyContext' | 'tags' | 'characterMediaIds'>
|
||||
>
|
||||
): Promise<void> {
|
||||
const wrapped = { ...patch } as Record<string, unknown>;
|
||||
// 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<string, unknown> = { ...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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue