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:
28
src/lib/assets/vase-illustration.svg
Normal file
28
src/lib/assets/vase-illustration.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg width="320" height="452" viewBox="0 0 320 452" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<foreignObject x="48" y="225" width="228" height="115"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(10px);clip-path:url(#bgblur_0_30_37_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="20" d="M177.866 245L68 290.254L177.866 320L256 283.39L177.866 245Z" fill="#7D7D7D" fill-opacity="0.7"/>
|
||||
<foreignObject x="91" y="393" width="138" height="79"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(10px);clip-path:url(#bgblur_1_30_37_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="20" d="M159.14 452L111 443.25L131.632 424L169.8 413L209 442.5L159.14 452Z" fill="#7D7D7D" fill-opacity="0.7"/>
|
||||
<foreignObject x="48" y="270" width="160" height="193"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(10px);clip-path:url(#bgblur_2_30_37_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="20" d="M188 307.566L68 290L124.932 379.865V391.576L111.295 443L166.523 421.87V391.576L188 307.566Z" fill="#BBBBBB" fill-opacity="0.7"/>
|
||||
<path d="M149 335.5C167.167 318.333 199 271.7 181 222.5C158.5 161 232.5 146.5 247.5 144.5C262.5 142.5 311 132.5 300 82" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M182.608 409.5C164.441 392.333 132.608 345.7 150.608 296.5C173.108 235 99.108 220.5 84.108 218.5C69.108 216.5 20.608 206.5 31.608 156" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<foreignObject x="219" y="36" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_3_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="229" y="46" width="91" height="71" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="181" y="5" width="87" height="51"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_4_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="191" y="15" width="67" height="31" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="248" y="-10" width="46" height="35"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_5_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="258" width="26" height="15" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="128" y="128" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_6_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="138" y="138" width="91" height="71" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="-10" y="98" width="80" height="80"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_7_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" y="108" width="60" height="60" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="47" y="52" width="43" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_8_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="57" y="62" width="23" height="23" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="11" y="75" width="59" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_9_30_37_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="21" y="108" width="23" height="39" transform="rotate(-90 21 108)" fill="#D9D9D9" fill-opacity="0.8"/>
|
||||
<foreignObject x="117" y="274" width="148" height="178"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_10_30_37_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="10" d="M127 326.86L255 284L193.383 379.612V390.263L208.362 442L170.915 431.095L127 326.86Z" fill="#D9D9D9" fill-opacity="0.5"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_30_37_clip_path" transform="translate(-48 -225)"><path d="M177.866 245L68 290.254L177.866 320L256 283.39L177.866 245Z"/>
|
||||
</clipPath><clipPath id="bgblur_1_30_37_clip_path" transform="translate(-91 -393)"><path d="M159.14 452L111 443.25L131.632 424L169.8 413L209 442.5L159.14 452Z"/>
|
||||
</clipPath><clipPath id="bgblur_2_30_37_clip_path" transform="translate(-48 -270)"><path d="M188 307.566L68 290L124.932 379.865V391.576L111.295 443L166.523 421.87V391.576L188 307.566Z"/>
|
||||
</clipPath><clipPath id="bgblur_3_30_37_clip_path" transform="translate(-219 -36)"><rect x="229" y="46" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_4_30_37_clip_path" transform="translate(-181 -5)"><rect x="191" y="15" width="67" height="31"/>
|
||||
</clipPath><clipPath id="bgblur_5_30_37_clip_path" transform="translate(-248 10)"><rect x="258" width="26" height="15"/>
|
||||
</clipPath><clipPath id="bgblur_6_30_37_clip_path" transform="translate(-128 -128)"><rect x="138" y="138" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_7_30_37_clip_path" transform="translate(10 -98)"><rect y="108" width="60" height="60"/>
|
||||
</clipPath><clipPath id="bgblur_8_30_37_clip_path" transform="translate(-47 -52)"><rect x="57" y="62" width="23" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_9_30_37_clip_path" transform="translate(-11 -75)"><rect x="21" y="108" width="23" height="39" transform="rotate(-90 21 108)"/>
|
||||
</clipPath><clipPath id="bgblur_10_30_37_clip_path" transform="translate(-117 -274)"><path d="M127 326.86L255 284L193.383 379.612V390.263L208.362 442L170.915 431.095L127 326.86Z"/>
|
||||
</clipPath></defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
19
src/lib/components/ui/Artwork/Artwork.svelte
Normal file
19
src/lib/components/ui/Artwork/Artwork.svelte
Normal 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>
|
||||
8
src/lib/components/ui/Artwork/DescriptionCard.svelte
Normal file
8
src/lib/components/ui/Artwork/DescriptionCard.svelte
Normal 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>
|
||||
11
src/lib/components/ui/Artwork/Vase.svelte
Normal file
11
src/lib/components/ui/Artwork/Vase.svelte
Normal 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"
|
||||
/>
|
||||
21
src/lib/components/ui/Header.svelte
Normal file
21
src/lib/components/ui/Header.svelte
Normal 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>
|
||||
62
src/lib/components/ui/upload/MoodboardGrid.svelte
Normal file
62
src/lib/components/ui/upload/MoodboardGrid.svelte
Normal 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>
|
||||
45
src/lib/components/ui/upload/SnsFeedUpload.svelte
Normal file
45
src/lib/components/ui/upload/SnsFeedUpload.svelte
Normal 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>
|
||||
66
src/lib/components/ui/upload/UploadTile.svelte
Normal file
66
src/lib/components/ui/upload/UploadTile.svelte
Normal 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>
|
||||
@@ -5,5 +5,10 @@
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbit&display=swap" rel="stylesheet" />
|
||||
</svelte:head>
|
||||
{@render children()}
|
||||
|
||||
@@ -1 +1,17 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
/* Typography — Orbit everywhere (set as the default font) */
|
||||
--font-sans: 'Orbit', monospace;
|
||||
|
||||
/* Color tokens */
|
||||
--color-surface: #eeeeee; /* page background, text on dark */
|
||||
--color-ink: #38322f; /* primary text */
|
||||
--color-pill: #1a1a1a; /* active toggle pill */
|
||||
--color-track: #f4f4f4; /* toggle track */
|
||||
--color-muted: #9e9e9e; /* inactive / secondary text */
|
||||
--color-subtle: #7d7d7d; /* accent gray (active step dot, illustration) */
|
||||
--color-placeholder: #d9d9d9; /* placeholder fills, inactive step dots */
|
||||
--color-line: #e5e7eb; /* dividers / light borders */
|
||||
--color-line-strong: #d1d5db; /* card border */
|
||||
}
|
||||
|
||||
65
src/routes/upload/+page.svelte
Normal file
65
src/routes/upload/+page.svelte
Normal file
@@ -0,0 +1,65 @@
|
||||
<script>
|
||||
import Header from '$lib/components/ui/Header.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
|
||||
import MoodboardGrid from '$lib/components/ui/upload/MoodboardGrid.svelte';
|
||||
import SnsFeedUpload from '$lib/components/ui/upload/SnsFeedUpload.svelte';
|
||||
|
||||
// "Build Moodboard" is selected by default in the design
|
||||
let mode = $state('moodboard');
|
||||
</script>
|
||||
|
||||
<!--
|
||||
On desktop the split layout is locked to the viewport height so the left
|
||||
artwork stays put while switching modes. The right panel is a full-bleed
|
||||
upload canvas with the mode toggle floating over it.
|
||||
-->
|
||||
<div
|
||||
class="flex h-dvh flex-col overflow-x-hidden bg-surface text-ink lg:h-screen lg:overflow-hidden"
|
||||
>
|
||||
<Header step={3} total={6} />
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork />
|
||||
|
||||
<!-- Right panel: full-bleed workspace + floating tab switch -->
|
||||
<section
|
||||
class="relative flex min-h-0 flex-1 flex-col pb-[4.75rem] lg:overflow-hidden lg:pb-0"
|
||||
>
|
||||
{#if mode === 'moodboard'}
|
||||
<MoodboardGrid />
|
||||
{:else}
|
||||
<SnsFeedUpload />
|
||||
{/if}
|
||||
|
||||
<!-- full-width on mobile; centered pill on desktop -->
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-20 px-4 pb-5 lg:absolute lg:right-auto lg:bottom-8 lg:left-1/2 lg:w-auto lg:-translate-x-1/2 lg:px-0 lg:pb-0"
|
||||
>
|
||||
<div
|
||||
class="flex w-full items-center rounded-full bg-surface/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur lg:w-auto"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mode = 'sns')}
|
||||
class={[
|
||||
'flex-1 rounded-full px-4 py-2.5 text-center text-sm whitespace-nowrap transition-colors lg:flex-none lg:px-5',
|
||||
mode === 'sns' ? 'bg-pill text-surface' : 'text-muted hover:text-ink'
|
||||
]}
|
||||
>
|
||||
Upload SNS Feed
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mode = 'moodboard')}
|
||||
class={[
|
||||
'flex-1 rounded-full px-4 py-2.5 text-center text-sm whitespace-nowrap transition-colors lg:flex-none lg:px-5',
|
||||
mode === 'moodboard' ? 'bg-pill text-surface' : 'text-muted hover:text-ink'
|
||||
]}
|
||||
>
|
||||
Build Moodboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
Reference in New Issue
Block a user