refine: redesigned layout, flower cards, edit sync, and bouquet preview framing

* refine: move upload mode toggle to top and compact it

* refine: simplify upload layout and remove editorial numbers

* refine: unify flow continue bar and redesign result page layout

* refine: show bouquet flowers as scrollable cards on result page

* refine: add flip-to-Korean on result flower cards

* refine: improve result rationale and sync recipe on edit

* refine: shorten edit title and align bouquet image framing to 3:4
This commit is contained in:
Chaewon Lee
2026-06-14 17:21:20 +09:00
committed by GitHub
parent 4b27c82036
commit 07e4eeaca3
25 changed files with 1303 additions and 409 deletions

View File

@@ -3,8 +3,12 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import DescriptionCard from '$lib/components/ui/Artwork/DescriptionCard.svelte';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import Header from '$lib/components/ui/Header.svelte';
import { editImages, fetchJob, finalizeJob, toDataUrl } from '$lib/flowerFlow/api.js';
import { buildBriefBouquetTitle } from '$lib/flowerFlow/resolveRecipeFlowers.js';
import { getFlowString, saveFlow } from '$lib/flowerFlow/session.js';
const jobId = getFlowString('jobId');
@@ -23,7 +27,7 @@
let selectionPoints = $state([]);
let initialImage = $state(null);
let generatedImage = $state(null);
let recipe = $state(null);
let moodAnalysis = $state(null);
let editing = $state(false);
let continuing = $state(false);
/** @type {Array<{ id: string, role: 'user' | 'assistant', prompt?: string, mode?: string, status?: 'pending' | 'done' | 'error', afterImage?: { mimeType: string, base64: string } | null, error?: string }>} */
@@ -33,7 +37,7 @@
const imageSrc = $derived(toDataUrl(generatedImage));
const hasAreaSelection = $derived(selectionPoints.length > 2);
const title = $derived(recipe?.concept ?? 'Generated bouquet');
const title = $derived(buildBriefBouquetTitle(moodAnalysis));
const description = $derived.by(() => {
if (hasAreaSelection) {
return 'Your prompt will apply to the marked area only.';
@@ -234,7 +238,7 @@
initialImage = job.images.primary;
generatedImage = job.images.primary;
recipe = job.recipe ?? null;
moodAnalysis = job.moodAnalysis ?? null;
loading = false;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load generated bouquet';
@@ -244,19 +248,19 @@
</script>
{#snippet editableImageFrame(image, editable = false)}
<div class="relative w-[42%] overflow-hidden bg-track ring-1 ring-black/5">
<div class="relative w-full max-w-44 sm:max-w-52 overflow-hidden bg-track ring-1 ring-black/5">
{#if image}
<img
src={toDataUrl(image)}
alt="Generated bouquet"
class={[
'aspect-[4/5] w-full object-contain',
'aspect-[3/4] w-full object-contain object-center',
editable && areaSelectionActive ? 'opacity-90' : ''
]}
draggable="false"
/>
{:else}
<div class="aspect-[4/5] w-full bg-placeholder"></div>
<div class="aspect-[3/4] w-full bg-placeholder"></div>
{/if}
{#if editable && image}
@@ -336,13 +340,17 @@
class="flex min-h-0 w-full shrink-0 flex-col border-b border-line px-6 py-6 lg:w-[44%] lg:border-r lg:border-b-0 lg:px-10 lg:py-8"
>
<div class="mx-auto flex min-h-0 w-full max-w-100 flex-1 flex-col items-center justify-center gap-6">
<div class="overflow-hidden bg-track shadow-sm ring-1 ring-black/5">
<div class="w-full max-w-24 overflow-hidden bg-track shadow-sm ring-1 ring-black/5 sm:max-w-28 lg:max-w-75">
{#if loading}
<div class="aspect-[4/5] w-full animate-pulse bg-placeholder"></div>
<div class="aspect-[3/4] w-full animate-pulse bg-placeholder"></div>
{:else if imageSrc}
<img src={imageSrc} alt="Generated bouquet" class="aspect-[4/5] w-full object-cover" />
<img
src={imageSrc}
alt="Generated bouquet"
class="aspect-[3/4] w-full object-contain object-center"
/>
{:else}
<div class="aspect-[4/5] w-full bg-placeholder"></div>
<div class="aspect-[3/4] w-full bg-placeholder"></div>
{/if}
</div>
@@ -350,9 +358,9 @@
</div>
</section>
<section class="relative flex min-h-0 flex-1 flex-col overflow-hidden">
<section class="relative flex min-h-0 flex-1 flex-col overflow-hidden pb-44 lg:pb-8">
<div
class="mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-4 px-6 py-5 pb-36 lg:py-6 lg:pb-6"
class="mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-4 px-6 py-5 lg:py-6"
>
<div class="shrink-0">
<p class="text-xs tracking-[0.2em] text-muted uppercase">Edit bouquet</p>
@@ -415,16 +423,14 @@
</div>
</div>
<div
class="fixed right-0 bottom-0 left-0 z-20 space-y-2 border-t border-line/60 bg-surface/95 px-4 pt-3 pb-5 backdrop-blur-sm lg:static lg:mx-auto lg:w-full lg:max-w-2xl lg:border-t-0 lg:bg-transparent lg:px-6 lg:pt-0 lg:pb-6 lg:backdrop-blur-none"
>
<FlowContinueBar class="lg:mx-auto lg:w-full lg:max-w-2xl">
{#if error}
<p class="rounded bg-surface/95 px-3 py-2 text-sm text-red-600 ring-1 ring-black/5">
{error}
</p>
{/if}
<div class="flex items-center gap-2 rounded-full border border-pill bg-surface py-1.5 pr-1.5 pl-5">
<div class="flex w-full items-center gap-2 rounded-full border border-pill bg-surface py-1.5 pr-1.5 pl-5">
<textarea
bind:value={prompt}
rows="1"
@@ -468,17 +474,15 @@
</button>
</div>
<div class="flex justify-end">
<button
type="button"
disabled={editing || continuing}
onclick={continueToResult}
class="px-2 py-2 text-sm whitespace-nowrap text-ink underline-offset-4 hover:underline disabled:opacity-50"
>
{continuing ? 'Preparing result...' : 'Continue to result ->'}
</button>
</div>
</div>
<button
type="button"
disabled={editing || continuing}
onclick={continueToResult}
class={FLOW_CONTINUE_BUTTON}
>
{continuing ? 'Preparing result...' : 'Continue to result ->'}
</button>
</FlowContinueBar>
</section>
</main>
</div>