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:
@@ -33,7 +33,7 @@
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt="Selected bouquet"
|
||||
class="aspect-[3/4] h-auto w-full object-cover"
|
||||
class="aspect-[3/4] h-auto w-full object-contain object-center"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
</script>
|
||||
|
||||
<div class="w-64 max-w-full flex-none border border-line-strong bg-white px-4 py-3 shadow-sm lg:px-6 lg:py-5">
|
||||
<h3 class="text-sm font-semibold">{title}</h3>
|
||||
<p class="mt-2 text-xs leading-snug">{description}</p>
|
||||
<h3 class="text-sm leading-snug font-semibold">{title}</h3>
|
||||
<p class="mt-2 text-xs leading-relaxed">{description}</p>
|
||||
</div>
|
||||
|
||||
21
src/lib/components/ui/FlowContinueBar.svelte
Normal file
21
src/lib/components/ui/FlowContinueBar.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script module>
|
||||
export const FLOW_CONTINUE_BUTTON =
|
||||
'w-full px-2 py-3 text-sm whitespace-nowrap text-ink underline-offset-4 hover:underline disabled:opacity-50 lg:w-auto';
|
||||
</script>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Fixed bottom on mobile, bottom-right of the right panel on desktop.
|
||||
* Pair with a section using `pb-[3.75rem] lg:pb-8` (taller pb on edit if the bar has extra controls).
|
||||
*/
|
||||
let { class: klass = '', children } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={[
|
||||
'fixed right-0 bottom-0 left-0 z-20 space-y-2 bg-placeholder/30 px-4 pb-5 lg:static lg:flex lg:shrink-0 lg:flex-col lg:items-end lg:bg-transparent lg:px-6 lg:pb-0',
|
||||
klass
|
||||
]}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import logoUrl from '$lib/assets/logo.svg';
|
||||
|
||||
// `step` is 1-based; the matching dot is highlighted as the current step.
|
||||
@@ -8,10 +9,14 @@
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-between border-b border-line px-6 py-5 md:px-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class="flex cursor-pointer items-center gap-3"
|
||||
aria-label="AI Florist home"
|
||||
>
|
||||
<img src={logoUrl} alt="" class="size-7 shrink-0 translate-y-px" aria-hidden="true" />
|
||||
<span class="text-lg leading-none tracking-wide">AI Florist</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
{#each dots as dot (dot)}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
{/if}
|
||||
</h1>
|
||||
<p class="text-sm text-muted">
|
||||
{style ?? '—'} | ₩{budget.toLocaleString('ko-KR')}
|
||||
{style ?? '...'} | ₩{budget.toLocaleString('ko-KR')}
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
131
src/lib/components/ui/result/BouquetFlowerCard.svelte
Normal file
131
src/lib/components/ui/result/BouquetFlowerCard.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script>
|
||||
import flowerIconUrl from '$lib/assets/flower.svg';
|
||||
|
||||
let {
|
||||
name,
|
||||
nameKo,
|
||||
wordOfFlower,
|
||||
wordOfFlowerKo,
|
||||
imageSrc,
|
||||
role = 'main'
|
||||
} = $props();
|
||||
|
||||
let flipped = $state(false);
|
||||
|
||||
const roleLabel = $derived(
|
||||
role === 'main' ? 'Main' : role === 'greenery' ? 'Greenery' : 'Filler'
|
||||
);
|
||||
const roleLabelKo = $derived(
|
||||
role === 'main' ? '메인' : role === 'greenery' ? '그리너리' : '필러'
|
||||
);
|
||||
|
||||
function toggleFlip() {
|
||||
flipped = !flipped;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flip-card h-[16.25rem] w-40 shrink-0 snap-start cursor-pointer border-none bg-transparent p-0 text-left"
|
||||
aria-label={flipped ? `${nameKo} card, show English` : `${name} card, show Korean`}
|
||||
onclick={toggleFlip}
|
||||
>
|
||||
<div class="flip-card-inner h-full" class:is-flipped={flipped}>
|
||||
<article
|
||||
class="flip-card-face flex h-full flex-col overflow-hidden rounded-2xl border border-line-strong bg-white shadow-sm"
|
||||
>
|
||||
<div class="flex h-6 shrink-0 items-center gap-1.5 px-3 pt-3">
|
||||
<img src={flowerIconUrl} alt="" class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span class="text-xs leading-none text-ink">{roleLabel}</span>
|
||||
</div>
|
||||
|
||||
<div class="relative mx-2 mt-2 min-h-0 flex-1">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
class="h-full w-full object-contain object-bottom"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 px-3 pb-4 pt-2">
|
||||
<h3
|
||||
class="flex min-h-8 items-center justify-center text-center text-sm leading-tight tracking-wide text-ink"
|
||||
>
|
||||
<span class="line-clamp-2">{name}</span>
|
||||
</h3>
|
||||
|
||||
<p class="line-clamp-2 text-center text-[0.6875rem] leading-snug text-ink">
|
||||
{wordOfFlower}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article
|
||||
class="flip-card-face flip-card-back flex h-full flex-col overflow-hidden rounded-2xl border border-line-strong bg-white shadow-sm"
|
||||
aria-hidden={!flipped}
|
||||
>
|
||||
<div class="flex h-6 shrink-0 items-center gap-1.5 px-3 pt-3">
|
||||
<img src={flowerIconUrl} alt="" class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span class="text-xs leading-none text-ink">{roleLabelKo}</span>
|
||||
</div>
|
||||
|
||||
<div class="relative mx-2 mt-2 min-h-0 flex-1">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={nameKo}
|
||||
class="h-full w-full object-contain object-bottom"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 px-3 pb-4 pt-2">
|
||||
<h3
|
||||
class="flex min-h-8 items-center justify-center text-center text-sm leading-tight tracking-wide text-ink"
|
||||
>
|
||||
<span class="line-clamp-2">{nameKo}</span>
|
||||
</h3>
|
||||
|
||||
<p class="line-clamp-2 text-center text-[0.6875rem] leading-snug text-ink">
|
||||
{wordOfFlowerKo}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.flip-card {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.flip-card-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.55s cubic-bezier(0.4, 0.2, 0.2, 1);
|
||||
}
|
||||
|
||||
.flip-card-inner.is-flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-card-face {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.flip-card-back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.flip-card-inner {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
src/lib/components/ui/result/BouquetFlowerCarousel.svelte
Normal file
29
src/lib/components/ui/result/BouquetFlowerCarousel.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import BouquetFlowerCard from './BouquetFlowerCard.svelte';
|
||||
|
||||
/** @type {{ id: number, name: string, nameKo: string, wordOfFlower: string, wordOfFlowerKo: string, imageSrc: string, role?: 'main' | 'sub' | 'greenery' }[]} */
|
||||
let { flowers = [] } = $props();
|
||||
</script>
|
||||
|
||||
{#if flowers.length === 0}
|
||||
<p class="text-sm text-muted">Flower details will appear once the bouquet recipe is ready.</p>
|
||||
{:else}
|
||||
<div class="min-h-0 w-full">
|
||||
<p class="mb-4 text-xs tracking-[0.2em] text-muted uppercase">Flowers in your bouquet</p>
|
||||
|
||||
<div
|
||||
class="flex snap-x snap-mandatory gap-4 overflow-x-auto px-0.5 py-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{#each flowers as flower (flower.id)}
|
||||
<BouquetFlowerCard
|
||||
name={flower.name}
|
||||
nameKo={flower.nameKo}
|
||||
wordOfFlower={flower.wordOfFlower}
|
||||
wordOfFlowerKo={flower.wordOfFlowerKo}
|
||||
imageSrc={flower.imageSrc}
|
||||
role={flower.role ?? 'main'}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -4,11 +4,7 @@
|
||||
import { hydrateDevUpload } from '$lib/dev/hydrateUpload.js';
|
||||
import { getFlowObject, isDevSeeded } from '$lib/flowerFlow/session.js';
|
||||
|
||||
let {
|
||||
primaryFile = $bindable(null),
|
||||
uploadedTiles = $bindable(),
|
||||
caption = 'build their moodboard!'
|
||||
} = $props();
|
||||
let { primaryFile = $bindable(null), uploadedTiles = $bindable() } = $props();
|
||||
|
||||
let colorFile = $state(null);
|
||||
let seasonFile = $state(null);
|
||||
@@ -58,170 +54,28 @@
|
||||
</script>
|
||||
|
||||
<div class="moodboard min-h-0 w-full flex-1">
|
||||
<div class="collage">
|
||||
<span class="mood-number number-color">(01)</span>
|
||||
<span class="mood-number number-season">(02)</span>
|
||||
<span class="mood-number number-character">(03)</span>
|
||||
<span class="mood-number number-location">(04)</span>
|
||||
<span class="mood-caption">{caption}</span>
|
||||
|
||||
<UploadTile
|
||||
label="Color"
|
||||
bind:file={colorFile}
|
||||
class="moodboard-tile tile-color"
|
||||
/>
|
||||
<UploadTile
|
||||
label="Season"
|
||||
bind:file={seasonFile}
|
||||
class="moodboard-tile tile-season"
|
||||
/>
|
||||
<UploadTile
|
||||
label="Character"
|
||||
bind:file={characterFile}
|
||||
class="moodboard-tile tile-character"
|
||||
/>
|
||||
<UploadTile
|
||||
label="Location"
|
||||
bind:file={locationFile}
|
||||
class="moodboard-tile tile-location"
|
||||
/>
|
||||
</div>
|
||||
<UploadTile label="Color" bind:file={colorFile} class="tile tile-color" />
|
||||
<UploadTile label="Season" bind:file={seasonFile} class="tile tile-season" />
|
||||
<UploadTile label="Character" bind:file={characterFile} class="tile tile-character" />
|
||||
<UploadTile label="Location" bind:file={locationFile} class="tile tile-location" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.moodboard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.collage {
|
||||
position: relative;
|
||||
width: min(100%, 34rem);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
height: 100%;
|
||||
aspect-ratio: 4 / 5.2;
|
||||
max-height: 44rem;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1.5rem 0;
|
||||
}
|
||||
|
||||
.moodboard :global(.moodboard-tile) {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgb(56 50 47 / 0.08);
|
||||
}
|
||||
|
||||
/* 01 — top-left portrait */
|
||||
:global(.tile-color) {
|
||||
top: 8%;
|
||||
left: 4%;
|
||||
width: 30%;
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
|
||||
/* 02 — right portrait, dips below the top of 01 */
|
||||
:global(.tile-season) {
|
||||
top: 13%;
|
||||
right: 3%;
|
||||
width: 29%;
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
|
||||
/* 03 — landscape, lower-left */
|
||||
:global(.tile-character) {
|
||||
top: 49%;
|
||||
left: 10%;
|
||||
width: 36%;
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
/* 04 — bottom-right portrait */
|
||||
:global(.tile-location) {
|
||||
top: 62%;
|
||||
right: 4%;
|
||||
width: 29%;
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
|
||||
.mood-number,
|
||||
.mood-caption {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.mood-number {
|
||||
font-size: clamp(1rem, 2.2vw, 1.5rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.number-color {
|
||||
top: 13%;
|
||||
left: 35%;
|
||||
}
|
||||
|
||||
.number-season {
|
||||
top: 38%;
|
||||
left: 60%;
|
||||
}
|
||||
|
||||
.number-character {
|
||||
top: 73%;
|
||||
left: 9%;
|
||||
}
|
||||
|
||||
.number-location {
|
||||
top: 58%;
|
||||
right: 11%;
|
||||
}
|
||||
|
||||
.mood-caption {
|
||||
/* horizontally centered over the toggle capsule (the left flex child of
|
||||
the bottom bar), not the section. Its center sits left of the collage
|
||||
midpoint because the "Continue" button occupies the bar's right side. */
|
||||
left: 29%;
|
||||
top: 84%;
|
||||
font-size: clamp(0.85rem, 1.7vw, 1.1rem);
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.moodboard {
|
||||
align-items: flex-start;
|
||||
padding: 1.5rem 1rem 7rem;
|
||||
}
|
||||
|
||||
.collage {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
aspect-ratio: auto;
|
||||
min-height: 0;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.moodboard :global(.moodboard-tile) {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:global(.tile-color),
|
||||
:global(.tile-season),
|
||||
:global(.tile-location) {
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
|
||||
:global(.tile-character) {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.mood-number,
|
||||
.mood-caption {
|
||||
display: none;
|
||||
}
|
||||
.moodboard :global(.tile) {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
import { hydrateDevUpload } from '$lib/dev/hydrateUpload.js';
|
||||
import { getFlowObject, isDevSeeded } from '$lib/flowerFlow/session.js';
|
||||
|
||||
let { primaryFile = $bindable(null), hasImage = $bindable(), caption = 'upload their feed!' } =
|
||||
$props();
|
||||
let { primaryFile = $bindable(null), hasImage = $bindable() } = $props();
|
||||
|
||||
let firstFile = $state(null);
|
||||
|
||||
@@ -36,12 +35,7 @@
|
||||
</script>
|
||||
|
||||
<div class="feed min-h-0 w-full flex-1">
|
||||
<div class="sns-collage">
|
||||
<span class="sns-number">(01)</span>
|
||||
<span class="sns-caption">{caption}</span>
|
||||
|
||||
<UploadTile bind:file={firstFile} class="sns-tile" />
|
||||
</div>
|
||||
<UploadTile bind:file={firstFile} class="sns-tile" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -49,76 +43,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.sns-collage {
|
||||
position: relative;
|
||||
width: min(100%, 42rem);
|
||||
height: 100%;
|
||||
aspect-ratio: 4 / 5;
|
||||
max-height: 34rem;
|
||||
padding: 0.75rem 1.5rem 0;
|
||||
}
|
||||
|
||||
.feed :global(.sns-tile) {
|
||||
position: absolute;
|
||||
top: 12%;
|
||||
left: 50%;
|
||||
width: 58%;
|
||||
height: 46%;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgb(56 50 47 / 0.08);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sns-number,
|
||||
.sns-caption {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.sns-number {
|
||||
top: 6%;
|
||||
left: 23%;
|
||||
font-size: clamp(1rem, 2.2vw, 1.5rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sns-caption {
|
||||
left: 50%;
|
||||
bottom: 13%;
|
||||
font-size: clamp(0.9rem, 1.9vw, 1.25rem);
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.feed {
|
||||
align-items: flex-start;
|
||||
padding: 1.5rem 1rem 7rem;
|
||||
}
|
||||
|
||||
.sns-collage {
|
||||
width: 100%;
|
||||
aspect-ratio: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.feed :global(.sns-tile) {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 4 / 5;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sns-number,
|
||||
.sns-caption {
|
||||
display: none;
|
||||
}
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
max-width: min(20rem, 100%);
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user