feat: add upload page UI (moodboard + SNS feed)

* feat: add upload page initial UI

* feat: add moodboard and SNS feed upload layouts

* fix: improve mobile upload page layout
This commit is contained in:
Chaewon Lee
2026-06-08 15:02:44 +09:00
committed by GitHub
parent 8186066d3a
commit e9dcd22b53
11 changed files with 347 additions and 1 deletions

View File

@@ -0,0 +1,19 @@
<script>
// The exhibited artwork — always shown on the left, acting like a step indicator.
import Vase from './Vase.svelte';
import DescriptionCard from './DescriptionCard.svelte';
let { title = 'Title', description = 'Description Description Description' } = $props();
</script>
<section
class="flex w-full shrink-0 flex-col border-b border-line lg:min-h-0 lg:w-[44%] lg:shrink-0 lg:overflow-y-auto lg:border-r lg:border-b-0"
>
<!-- mobile: compact row · desktop: centered column -->
<div
class="mx-auto flex w-full max-w-100 flex-row items-center gap-12 px-6 py-5 lg:flex-1 lg:flex-col lg:items-center lg:justify-center lg:gap-10 lg:px-6 lg:py-12"
>
<Vase />
<DescriptionCard {title} {description} />
</div>
</section>

View File

@@ -0,0 +1,8 @@
<script>
let { title = 'Title', description = 'Description Description Description' } = $props();
</script>
<div class="min-w-0 flex-1 border border-line-strong px-4 py-3 lg:w-54 lg:flex-none lg:px-6 lg:py-5">
<h3 class="text-sm">{title}</h3>
<p class="mt-2 text-xs leading-snug">{description}</p>
</div>

View File

@@ -0,0 +1,11 @@
<script>
import vaseIllustration from '$lib/assets/vase-illustration.svg';
</script>
<img
src={vaseIllustration}
alt=""
class="mx-auto h-auto w-full max-w-24 shrink-0 sm:max-w-28 lg:max-w-75"
width="320"
height="452"
/>

View File

@@ -0,0 +1,21 @@
<script>
// `step` is 1-based; the matching dot is highlighted as the current step.
let { step = 1, total = 6 } = $props();
const dots = $derived(Array.from({ length: total }, (_, i) => i));
</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">
<span class="size-4 shrink-0 bg-placeholder"></span>
<span class="text-lg tracking-wide">AI Florist</span>
</div>
<div class="flex items-center gap-3 sm:gap-4">
{#each dots as dot (dot)}
<span
class={['rounded-[1px]', dot === step - 1 ? 'size-2.5 bg-subtle' : 'size-2 bg-placeholder']}
></span>
{/each}
</div>
</header>

View File

@@ -0,0 +1,62 @@
<script>
import UploadTile from './UploadTile.svelte';
// One reference image per category, laid out as an interlocking collage
// (offset seams, varied heights) instead of equal quarters.
const tiles = [
{ key: 'color', label: 'Color', aspect: 'aspect-4/5' },
{ key: 'season', label: 'Season', aspect: 'aspect-4/3' },
{ key: 'character', label: 'Character', aspect: 'aspect-4/3' },
{ key: 'location', label: 'Location', aspect: 'aspect-4/5' }
];
</script>
<div class="moodboard w-full min-h-0 flex-1">
{#each tiles as tile (tile.key)}
<UploadTile
label={tile.label}
class="tile tile-{tile.key} h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto {tile.aspect}"
/>
{/each}
</div>
<style>
.moodboard {
display: grid;
gap: 0;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
width: 100%;
flex: 1;
min-height: 0;
}
@media (min-width: 1024px) {
.moodboard {
grid-template-rows: repeat(5, 1fr);
grid-template-areas:
'color season'
'color season'
'color location'
'character location'
'character location';
min-height: 34rem;
}
:global(.tile-color) {
grid-area: color;
}
:global(.tile-season) {
grid-area: season;
}
:global(.tile-character) {
grid-area: character;
}
:global(.tile-location) {
grid-area: location;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<script>
import UploadTile from './UploadTile.svelte';
// Two SNS feed screenshots. On desktop they fill the panel edge-to-edge in
// a staggered composition (one raised on the right, one dropped on the
// left); below that they fall back to a simple side-by-side / stacked grid.
</script>
<div class="feed w-full min-h-0 flex-1">
<UploadTile class="tile-one h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto aspect-4/5" />
<UploadTile class="tile-two h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto aspect-4/5" />
</div>
<style>
.feed {
display: grid;
gap: 0;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
width: 100%;
flex: 1;
min-height: 0;
}
@media (min-width: 1024px) {
.feed {
grid-template-rows: repeat(5, 1fr);
grid-template-areas:
'. two'
'one two'
'one two'
'one two'
'one .';
min-height: 34rem;
}
:global(.tile-one) {
grid-area: one;
}
:global(.tile-two) {
grid-area: two;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<script>
import { onDestroy } from 'svelte';
// A single click-to-upload slot: a light bordered placeholder when empty,
// the chosen image (cover) when filled. Layout (size / grid placement) is
// supplied by the parent via `class` and `style` so the same tile works in
// both the moodboard and the SNS feed.
let { label = null, class: klass = '', style = '' } = $props();
let preview = $state(null);
function pick(event) {
const file = event.currentTarget.files?.[0];
if (!file) return;
if (preview) URL.revokeObjectURL(preview);
preview = URL.createObjectURL(file);
}
onDestroy(() => {
if (preview) URL.revokeObjectURL(preview);
});
</script>
<label
class={[
'group relative flex cursor-pointer items-center justify-center overflow-hidden bg-track transition-colors',
!preview && 'border border-line hover:border-line-strong',
klass
]}
{style}
>
<input
type="file"
accept="image/*"
class="sr-only"
aria-label={label ? `Add a ${label} image` : 'Add an image'}
onchange={pick}
/>
{#if preview}
<img src={preview} alt={label ?? ''} class="h-full w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/45 to-transparent"></div>
{#if label}
<span class="absolute bottom-3 left-4 text-sm tracking-[0.15em] text-surface uppercase"
>{label}</span
>
{/if}
<span
class="absolute top-3 right-3 rounded-full bg-black/40 px-2.5 py-1 text-xs text-surface opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100"
>
Change
</span>
{:else}
<div
class="flex flex-col items-center gap-3 text-subtle transition-transform group-hover:scale-105"
>
<span
class="flex size-10 items-center justify-center rounded-full border border-current text-xl leading-none"
aria-hidden="true">+</span
>
{#if label}
<span class="text-sm tracking-[0.15em] uppercase">{label}</span>
{/if}
</div>
{/if}
</label>