feat: redesign upload experience and visual branding

* feat: add animated flower step indicator

* feat: add custom pixel cursor

* feat: update branding, cursor, and artwork cards

* feat: add paper texture background

* style: update description card background

* feat: redesign upload moodboard collage layout

* style: update upload colors and add sliding mode toggle

* fix: add static favicon and apple touch icons
This commit is contained in:
Chaewon Lee
2026-06-13 01:20:54 +09:00
committed by GitHub
parent e7c690ac13
commit dda6ca972d
18 changed files with 549 additions and 109 deletions

106
src/lib/assets/cursor.svg Normal file
View File

@@ -0,0 +1,106 @@
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges">
<!-- grid: 4px per pixel, 16x16 grid -->
<path fill="#FFFFFF" d="
M4 4 h4 v4 h-4 z
M8 4 h4 v4 h-4 z
M4 8 h4 v4 h-4 z
M8 8 h4 v4 h-4 z
M12 8 h4 v4 h-4 z
M4 12 h4 v4 h-4 z
M8 12 h4 v4 h-4 z
M12 12 h4 v4 h-4 z
M16 12 h4 v4 h-4 z
M4 16 h4 v4 h-4 z
M8 16 h4 v4 h-4 z
M12 16 h4 v4 h-4 z
M16 16 h4 v4 h-4 z
M20 16 h4 v4 h-4 z
M4 20 h4 v4 h-4 z
M8 20 h4 v4 h-4 z
M12 20 h4 v4 h-4 z
M16 20 h4 v4 h-4 z
M20 20 h4 v4 h-4 z
M24 20 h4 v4 h-4 z
M4 24 h4 v4 h-4 z
M8 24 h4 v4 h-4 z
M12 24 h4 v4 h-4 z
M16 24 h4 v4 h-4 z
M20 24 h4 v4 h-4 z
M24 24 h4 v4 h-4 z
M28 24 h4 v4 h-4 z
M4 28 h4 v4 h-4 z
M8 28 h4 v4 h-4 z
M12 28 h4 v4 h-4 z
M16 28 h4 v4 h-4 z
M20 28 h4 v4 h-4 z
M24 28 h4 v4 h-4 z
M28 28 h4 v4 h-4 z
M32 28 h4 v4 h-4 z
M4 32 h4 v4 h-4 z
M8 32 h4 v4 h-4 z
M12 32 h4 v4 h-4 z
M16 32 h4 v4 h-4 z
M20 32 h4 v4 h-4 z
M4 36 h4 v4 h-4 z
M8 36 h4 v4 h-4 z
M12 36 h4 v4 h-4 z
M16 36 h4 v4 h-4 z
M20 36 h4 v4 h-4 z
M24 36 h4 v4 h-4 z
M28 36 h4 v4 h-4 z
M4 40 h4 v4 h-4 z
M8 40 h4 v4 h-4 z
M16 40 h4 v4 h-4 z
M20 40 h4 v4 h-4 z
M24 40 h4 v4 h-4 z
M28 40 h4 v4 h-4 z
M16 44 h4 v4 h-4 z
M20 44 h4 v4 h-4 z
M24 44 h4 v4 h-4 z
M28 44 h4 v4 h-4 z
M32 44 h4 v4 h-4 z
M20 48 h4 v4 h-4 z
M24 48 h4 v4 h-4 z
M28 48 h4 v4 h-4 z
M32 48 h4 v4 h-4 z
M20 52 h4 v4 h-4 z
M24 52 h4 v4 h-4 z
M28 52 h4 v4 h-4 z
M32 52 h4 v4 h-4 z
"/>
<path fill="#38322F" d="
M8 8 h4 v4 h-4 z
M8 12 h4 v4 h-4 z
M12 12 h4 v4 h-4 z
M8 16 h4 v4 h-4 z
M12 16 h4 v4 h-4 z
M16 16 h4 v4 h-4 z
M8 20 h4 v4 h-4 z
M12 20 h4 v4 h-4 z
M16 20 h4 v4 h-4 z
M20 20 h4 v4 h-4 z
M8 24 h4 v4 h-4 z
M12 24 h4 v4 h-4 z
M16 24 h4 v4 h-4 z
M20 24 h4 v4 h-4 z
M24 24 h4 v4 h-4 z
M8 28 h4 v4 h-4 z
M12 28 h4 v4 h-4 z
M16 28 h4 v4 h-4 z
M20 28 h4 v4 h-4 z
M24 28 h4 v4 h-4 z
M28 28 h4 v4 h-4 z
M8 32 h4 v4 h-4 z
M12 32 h4 v4 h-4 z
M16 32 h4 v4 h-4 z
M8 36 h4 v4 h-4 z
M20 36 h4 v4 h-4 z
M24 36 h4 v4 h-4 z
M20 40 h4 v4 h-4 z
M24 40 h4 v4 h-4 z
M24 44 h4 v4 h-4 z
M28 44 h4 v4 h-4 z
M24 48 h4 v4 h-4 z
M28 48 h4 v4 h-4 z
"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
<svg width="240" height="240" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges">
<g fill="#38322F">
<!-- r1 --><rect x="96" y="12" width="12" height="12"/><rect x="108" y="12" width="12" height="12"/><rect x="120" y="12" width="12" height="12"/><rect x="132" y="12" width="12" height="12"/>
<!-- r2 --><rect x="84" y="24" width="12" height="12"/><rect x="96" y="24" width="12" height="12"/><rect x="108" y="24" width="12" height="12"/><rect x="120" y="24" width="12" height="12"/><rect x="132" y="24" width="12" height="12"/><rect x="144" y="24" width="12" height="12"/>
<!-- r3 --><rect x="84" y="36" width="12" height="12"/><rect x="96" y="36" width="12" height="12"/><rect x="108" y="36" width="12" height="12"/><rect x="120" y="36" width="12" height="12"/><rect x="132" y="36" width="12" height="12"/><rect x="144" y="36" width="12" height="12"/>
<!-- r4 --><rect x="84" y="48" width="12" height="12"/><rect x="96" y="48" width="12" height="12"/><rect x="108" y="48" width="12" height="12"/><rect x="120" y="48" width="12" height="12"/><rect x="132" y="48" width="12" height="12"/><rect x="144" y="48" width="12" height="12"/>
<!-- r5 --><rect x="48" y="60" width="12" height="12"/><rect x="60" y="60" width="12" height="12"/><rect x="72" y="60" width="12" height="12"/><rect x="84" y="60" width="12" height="12"/><rect x="96" y="60" width="12" height="12"/><rect x="108" y="60" width="12" height="12"/><rect x="120" y="60" width="12" height="12"/><rect x="132" y="60" width="12" height="12"/><rect x="144" y="60" width="12" height="12"/><rect x="156" y="60" width="12" height="12"/><rect x="168" y="60" width="12" height="12"/><rect x="180" y="60" width="12" height="12"/>
<!-- r6 --><rect x="36" y="72" width="12" height="12"/><rect x="48" y="72" width="12" height="12"/><rect x="60" y="72" width="12" height="12"/><rect x="72" y="72" width="12" height="12"/><rect x="84" y="72" width="12" height="12"/><rect x="96" y="72" width="12" height="12"/><rect x="108" y="72" width="12" height="12"/><rect x="120" y="72" width="12" height="12"/><rect x="132" y="72" width="12" height="12"/><rect x="144" y="72" width="12" height="12"/><rect x="156" y="72" width="12" height="12"/><rect x="168" y="72" width="12" height="12"/><rect x="180" y="72" width="12" height="12"/><rect x="192" y="72" width="12" height="12"/>
<!-- r7 (center hole) --><rect x="24" y="84" width="12" height="12"/><rect x="36" y="84" width="12" height="12"/><rect x="48" y="84" width="12" height="12"/><rect x="60" y="84" width="12" height="12"/><rect x="72" y="84" width="12" height="12"/><rect x="84" y="84" width="12" height="12"/><rect x="96" y="84" width="12" height="12"/><rect x="132" y="84" width="12" height="12"/><rect x="144" y="84" width="12" height="12"/><rect x="156" y="84" width="12" height="12"/><rect x="168" y="84" width="12" height="12"/><rect x="180" y="84" width="12" height="12"/><rect x="192" y="84" width="12" height="12"/><rect x="204" y="84" width="12" height="12"/>
<!-- r8 (center hole) --><rect x="24" y="96" width="12" height="12"/><rect x="36" y="96" width="12" height="12"/><rect x="48" y="96" width="12" height="12"/><rect x="60" y="96" width="12" height="12"/><rect x="72" y="96" width="12" height="12"/><rect x="84" y="96" width="12" height="12"/><rect x="96" y="96" width="12" height="12"/><rect x="132" y="96" width="12" height="12"/><rect x="144" y="96" width="12" height="12"/><rect x="156" y="96" width="12" height="12"/><rect x="168" y="96" width="12" height="12"/><rect x="180" y="96" width="12" height="12"/><rect x="192" y="96" width="12" height="12"/><rect x="204" y="96" width="12" height="12"/>
<!-- r9 --><rect x="36" y="108" width="12" height="12"/><rect x="48" y="108" width="12" height="12"/><rect x="60" y="108" width="12" height="12"/><rect x="72" y="108" width="12" height="12"/><rect x="84" y="108" width="12" height="12"/><rect x="96" y="108" width="12" height="12"/><rect x="108" y="108" width="12" height="12"/><rect x="120" y="108" width="12" height="12"/><rect x="132" y="108" width="12" height="12"/><rect x="144" y="108" width="12" height="12"/><rect x="156" y="108" width="12" height="12"/><rect x="168" y="108" width="12" height="12"/><rect x="180" y="108" width="12" height="12"/>
<!-- r10 --><rect x="48" y="120" width="12" height="12"/><rect x="60" y="120" width="12" height="12"/><rect x="72" y="120" width="12" height="12"/><rect x="84" y="120" width="12" height="12"/><rect x="96" y="120" width="12" height="12"/><rect x="108" y="120" width="12" height="12"/><rect x="120" y="120" width="12" height="12"/><rect x="132" y="120" width="12" height="12"/><rect x="144" y="120" width="12" height="12"/><rect x="156" y="120" width="12" height="12"/><rect x="168" y="120" width="12" height="12"/><rect x="180" y="120" width="12" height="12"/>
<!-- r11 --><rect x="84" y="132" width="12" height="12"/><rect x="96" y="132" width="12" height="12"/><rect x="108" y="132" width="12" height="12"/><rect x="120" y="132" width="12" height="12"/><rect x="132" y="132" width="12" height="12"/><rect x="144" y="132" width="12" height="12"/>
<!-- r12 --><rect x="84" y="144" width="12" height="12"/><rect x="96" y="144" width="12" height="12"/><rect x="108" y="144" width="12" height="12"/><rect x="120" y="144" width="12" height="12"/><rect x="132" y="144" width="12" height="12"/><rect x="144" y="144" width="12" height="12"/>
<!-- r13 --><rect x="84" y="156" width="12" height="12"/><rect x="96" y="156" width="12" height="12"/><rect x="108" y="156" width="12" height="12"/><rect x="120" y="156" width="12" height="12"/><rect x="132" y="156" width="12" height="12"/><rect x="144" y="156" width="12" height="12"/>
<!-- r14 --><rect x="96" y="168" width="12" height="12"/><rect x="108" y="168" width="12" height="12"/><rect x="120" y="168" width="12" height="12"/><rect x="132" y="168" width="12" height="12"/>
<!-- stem -->
<!-- r15 --><rect x="108" y="180" width="12" height="12"/><rect x="120" y="180" width="12" height="12"/>
<!-- r16 --><rect x="108" y="192" width="12" height="12"/><rect x="120" y="192" width="12" height="12"/>
<!-- r17 leaf --><rect x="72" y="204" width="12" height="12"/><rect x="84" y="204" width="12" height="12"/><rect x="96" y="204" width="12" height="12"/><rect x="108" y="204" width="12" height="12"/><rect x="120" y="204" width="12" height="12"/>
<!-- r18 --><rect x="108" y="216" width="12" height="12"/><rect x="120" y="216" width="12" height="12"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -1,5 +1,5 @@
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path fill="#000000" d="
<path fill="#7d7d7d" d="
M32 6
C36 6 38.5 10.5 38 15.5
C42.5 12 48 12.5 50.5 16

Before

Width:  |  Height:  |  Size: 540 B

After

Width:  |  Height:  |  Size: 540 B

23
src/lib/assets/logo.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg width="240" height="240" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges">
<g fill="#38322F">
<!-- r1 --><rect x="96" y="12" width="12" height="12"/><rect x="108" y="12" width="12" height="12"/><rect x="120" y="12" width="12" height="12"/><rect x="132" y="12" width="12" height="12"/>
<!-- r2 --><rect x="84" y="24" width="12" height="12"/><rect x="96" y="24" width="12" height="12"/><rect x="108" y="24" width="12" height="12"/><rect x="120" y="24" width="12" height="12"/><rect x="132" y="24" width="12" height="12"/><rect x="144" y="24" width="12" height="12"/>
<!-- r3 --><rect x="84" y="36" width="12" height="12"/><rect x="96" y="36" width="12" height="12"/><rect x="108" y="36" width="12" height="12"/><rect x="120" y="36" width="12" height="12"/><rect x="132" y="36" width="12" height="12"/><rect x="144" y="36" width="12" height="12"/>
<!-- r4 --><rect x="84" y="48" width="12" height="12"/><rect x="96" y="48" width="12" height="12"/><rect x="108" y="48" width="12" height="12"/><rect x="120" y="48" width="12" height="12"/><rect x="132" y="48" width="12" height="12"/><rect x="144" y="48" width="12" height="12"/>
<!-- r5 --><rect x="48" y="60" width="12" height="12"/><rect x="60" y="60" width="12" height="12"/><rect x="72" y="60" width="12" height="12"/><rect x="84" y="60" width="12" height="12"/><rect x="96" y="60" width="12" height="12"/><rect x="108" y="60" width="12" height="12"/><rect x="120" y="60" width="12" height="12"/><rect x="132" y="60" width="12" height="12"/><rect x="144" y="60" width="12" height="12"/><rect x="156" y="60" width="12" height="12"/><rect x="168" y="60" width="12" height="12"/><rect x="180" y="60" width="12" height="12"/>
<!-- r6 --><rect x="36" y="72" width="12" height="12"/><rect x="48" y="72" width="12" height="12"/><rect x="60" y="72" width="12" height="12"/><rect x="72" y="72" width="12" height="12"/><rect x="84" y="72" width="12" height="12"/><rect x="96" y="72" width="12" height="12"/><rect x="108" y="72" width="12" height="12"/><rect x="120" y="72" width="12" height="12"/><rect x="132" y="72" width="12" height="12"/><rect x="144" y="72" width="12" height="12"/><rect x="156" y="72" width="12" height="12"/><rect x="168" y="72" width="12" height="12"/><rect x="180" y="72" width="12" height="12"/><rect x="192" y="72" width="12" height="12"/>
<!-- r7 (center hole) --><rect x="24" y="84" width="12" height="12"/><rect x="36" y="84" width="12" height="12"/><rect x="48" y="84" width="12" height="12"/><rect x="60" y="84" width="12" height="12"/><rect x="72" y="84" width="12" height="12"/><rect x="84" y="84" width="12" height="12"/><rect x="96" y="84" width="12" height="12"/><rect x="132" y="84" width="12" height="12"/><rect x="144" y="84" width="12" height="12"/><rect x="156" y="84" width="12" height="12"/><rect x="168" y="84" width="12" height="12"/><rect x="180" y="84" width="12" height="12"/><rect x="192" y="84" width="12" height="12"/><rect x="204" y="84" width="12" height="12"/>
<!-- r8 (center hole) --><rect x="24" y="96" width="12" height="12"/><rect x="36" y="96" width="12" height="12"/><rect x="48" y="96" width="12" height="12"/><rect x="60" y="96" width="12" height="12"/><rect x="72" y="96" width="12" height="12"/><rect x="84" y="96" width="12" height="12"/><rect x="96" y="96" width="12" height="12"/><rect x="132" y="96" width="12" height="12"/><rect x="144" y="96" width="12" height="12"/><rect x="156" y="96" width="12" height="12"/><rect x="168" y="96" width="12" height="12"/><rect x="180" y="96" width="12" height="12"/><rect x="192" y="96" width="12" height="12"/><rect x="204" y="96" width="12" height="12"/>
<!-- r9 --><rect x="36" y="108" width="12" height="12"/><rect x="48" y="108" width="12" height="12"/><rect x="60" y="108" width="12" height="12"/><rect x="72" y="108" width="12" height="12"/><rect x="84" y="108" width="12" height="12"/><rect x="96" y="108" width="12" height="12"/><rect x="108" y="108" width="12" height="12"/><rect x="120" y="108" width="12" height="12"/><rect x="132" y="108" width="12" height="12"/><rect x="144" y="108" width="12" height="12"/><rect x="156" y="108" width="12" height="12"/><rect x="168" y="108" width="12" height="12"/><rect x="180" y="108" width="12" height="12"/>
<!-- r10 --><rect x="48" y="120" width="12" height="12"/><rect x="60" y="120" width="12" height="12"/><rect x="72" y="120" width="12" height="12"/><rect x="84" y="120" width="12" height="12"/><rect x="96" y="120" width="12" height="12"/><rect x="108" y="120" width="12" height="12"/><rect x="120" y="120" width="12" height="12"/><rect x="132" y="120" width="12" height="12"/><rect x="144" y="120" width="12" height="12"/><rect x="156" y="120" width="12" height="12"/><rect x="168" y="120" width="12" height="12"/><rect x="180" y="120" width="12" height="12"/>
<!-- r11 --><rect x="84" y="132" width="12" height="12"/><rect x="96" y="132" width="12" height="12"/><rect x="108" y="132" width="12" height="12"/><rect x="120" y="132" width="12" height="12"/><rect x="132" y="132" width="12" height="12"/><rect x="144" y="132" width="12" height="12"/>
<!-- r12 --><rect x="84" y="144" width="12" height="12"/><rect x="96" y="144" width="12" height="12"/><rect x="108" y="144" width="12" height="12"/><rect x="120" y="144" width="12" height="12"/><rect x="132" y="144" width="12" height="12"/><rect x="144" y="144" width="12" height="12"/>
<!-- r13 --><rect x="84" y="156" width="12" height="12"/><rect x="96" y="156" width="12" height="12"/><rect x="108" y="156" width="12" height="12"/><rect x="120" y="156" width="12" height="12"/><rect x="132" y="156" width="12" height="12"/><rect x="144" y="156" width="12" height="12"/>
<!-- r14 --><rect x="96" y="168" width="12" height="12"/><rect x="108" y="168" width="12" height="12"/><rect x="120" y="168" width="12" height="12"/><rect x="132" y="168" width="12" height="12"/>
<!-- stem -->
<!-- r15 --><rect x="108" y="180" width="12" height="12"/><rect x="120" y="180" width="12" height="12"/>
<!-- r16 --><rect x="108" y="192" width="12" height="12"/><rect x="120" y="192" width="12" height="12"/>
<!-- r17 leaf --><rect x="72" y="204" width="12" height="12"/><rect x="84" y="204" width="12" height="12"/><rect x="96" y="204" width="12" height="12"/><rect x="108" y="204" width="12" height="12"/><rect x="120" y="204" width="12" height="12"/>
<!-- r18 --><rect x="108" y="216" width="12" height="12"/><rect x="120" y="216" width="12" height="12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -2,7 +2,7 @@
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>
<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>
</div>

View File

@@ -0,0 +1,68 @@
<script>
import { onMount } from 'svelte';
import cursorUrl from '$lib/assets/cursor.svg';
let visible = $state(false);
let x = $state(0);
let y = $state(0);
onMount(() => {
const canUseCustomCursor = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
if (!canUseCustomCursor) return;
document.documentElement.classList.add('flower-cursor');
function handlePointerMove(event) {
x = event.clientX;
y = event.clientY;
visible = true;
}
function handlePointerLeave() {
visible = false;
}
window.addEventListener('pointermove', handlePointerMove);
document.addEventListener('mouseleave', handlePointerLeave);
return () => {
document.documentElement.classList.remove('flower-cursor');
window.removeEventListener('pointermove', handlePointerMove);
document.removeEventListener('mouseleave', handlePointerLeave);
};
});
</script>
{#if visible}
<div class="flower-cursor-layer" style={`transform: translate3d(${x}px, ${y}px, 0)`}>
<img class="flower-cursor-icon" src={cursorUrl} alt="" aria-hidden="true" />
</div>
{/if}
<style>
:global(html.flower-cursor),
:global(html.flower-cursor *) {
cursor: none !important;
}
.flower-cursor-layer {
position: fixed;
top: 0;
left: 0;
z-index: 2147483647;
pointer-events: none;
translate: -0.3rem -0.3rem;
}
.flower-cursor-icon {
display: block;
width: 2.35rem;
height: 2.35rem;
}
@media (prefers-reduced-motion: reduce) {
.flower-cursor-layer {
transition: none;
}
}
</style>

View File

@@ -1,4 +1,6 @@
<script>
import logoUrl from '$lib/assets/logo.svg';
// `step` is 1-based; the matching dot is highlighted as the current step.
let { step = 1, total = 7 } = $props();
@@ -7,15 +9,45 @@
<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>
<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>
<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>
<svg
class={[
'shrink-0',
dot === step - 1
? 'flower-icon--current size-4.5 text-subtle'
: 'size-3.5 text-placeholder'
]}
viewBox="0 0 64 64"
aria-hidden="true"
>
<path
fill="currentColor"
d="M32 6 C36 6 38.5 10.5 38 15.5 C42.5 12 48 12.5 50.5 16 C53 19.5 50.5 24.5 46 26.5 C51 27.5 54.5 31.5 53.5 36 C52.5 40.5 47 42 42.5 40 C45.5 44 45 49.5 41 52 C37 54.5 32.5 51.5 31 47 C29.5 51.5 25 54.5 21 52 C17 49.5 16.5 44 19.5 40 C15 42 9.5 40.5 8.5 36 C7.5 31.5 11 27.5 16 26.5 C11.5 24.5 9 19.5 11.5 16 C14 12.5 19.5 12 24 15.5 C23.5 10.5 28 6 32 6 Z"
/>
</svg>
{/each}
</div>
</header>
<style>
.flower-icon--current {
animation: flower-spin 2.4s linear infinite;
}
@keyframes flower-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.flower-icon--current {
animation: none;
}
}
</style>

View File

@@ -4,7 +4,7 @@
import { hydrateDevUpload } from '$lib/dev/hydrateUpload.js';
import { getFlowObject, isDevSeeded } from '$lib/flowerFlow/session.js';
let { primaryFile = $bindable(null) } = $props();
let { primaryFile = $bindable(null), caption = 'build their moodboard!' } = $props();
let colorFile = $state(null);
let seasonFile = $state(null);
@@ -12,7 +12,8 @@
let locationFile = $state(null);
$effect(() => {
primaryFile = colorFile ?? seasonFile ?? characterFile ?? locationFile ?? null;
const next = colorFile ?? seasonFile ?? characterFile ?? locationFile ?? null;
if (primaryFile !== next) primaryFile = next;
});
onMount(async () => {
@@ -35,65 +36,170 @@
</script>
<div class="moodboard min-h-0 w-full flex-1">
<UploadTile
label="Color"
bind:file={colorFile}
class="tile tile-color aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
label="Season"
bind:file={seasonFile}
class="tile tile-season aspect-4/3 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
label="Character"
bind:file={characterFile}
class="tile tile-character aspect-4/3 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
label="Location"
bind:file={locationFile}
class="tile tile-location aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<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>
</div>
<style>
.moodboard {
display: grid;
gap: 0;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
width: 100%;
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0.5rem 1.5rem 1rem;
}
@media (min-width: 1024px) {
.collage {
position: relative;
width: min(100%, 34rem);
height: 100%;
aspect-ratio: 4 / 5.2;
max-height: 44rem;
}
.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 {
grid-template-rows: repeat(5, 1fr);
grid-template-areas:
'color season'
'color season'
'color location'
'character location'
'character location';
min-height: 34rem;
align-items: flex-start;
padding: 1.5rem 1rem 7rem;
}
:global(.tile-color) {
grid-area: color;
.collage {
display: grid;
width: 100%;
aspect-ratio: auto;
min-height: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem;
}
:global(.tile-season) {
grid-area: season;
.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) {
grid-area: character;
aspect-ratio: 4 / 3;
}
:global(.tile-location) {
grid-area: location;
.mood-number,
.mood-caption {
display: none;
}
}
</style>

View File

@@ -4,13 +4,13 @@
import { hydrateDevUpload } from '$lib/dev/hydrateUpload.js';
import { getFlowObject, isDevSeeded } from '$lib/flowerFlow/session.js';
let { primaryFile = $bindable(null) } = $props();
let { primaryFile = $bindable(null), caption = 'upload their feed!' } = $props();
let firstFile = $state(null);
let secondFile = $state(null);
$effect(() => {
primaryFile = firstFile ?? secondFile ?? null;
const next = firstFile ?? null;
if (primaryFile !== next) primaryFile = next;
});
onMount(async () => {
@@ -23,7 +23,6 @@
try {
const files = await hydrateDevUpload(/** @type {Record<string, string>} */ (tiles));
if (files.first) firstFile = files.first;
if (files.second) secondFile = files.second;
} catch {
// dev seed 실패 시 빈 타일 유지
}
@@ -31,45 +30,89 @@
</script>
<div class="feed min-h-0 w-full flex-1">
<UploadTile
bind:file={firstFile}
class="tile-one aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
bind:file={secondFile}
class="tile-two aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<div class="sns-collage">
<span class="sns-number">(01)</span>
<span class="sns-caption">{caption}</span>
<UploadTile bind:file={firstFile} class="sns-tile" />
</div>
</div>
<style>
.feed {
display: grid;
gap: 0;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
width: 100%;
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0.5rem 1.5rem 1rem;
}
@media (min-width: 1024px) {
.sns-collage {
position: relative;
width: min(100%, 42rem);
height: 100%;
aspect-ratio: 4 / 5;
max-height: 34rem;
}
.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 {
grid-template-rows: repeat(5, 1fr);
grid-template-areas:
'. two'
'one two'
'one two'
'one two'
'one .';
min-height: 34rem;
align-items: flex-start;
padding: 1.5rem 1rem 7rem;
}
:global(.tile-one) {
grid-area: one;
.sns-collage {
width: 100%;
aspect-ratio: auto;
min-height: 0;
}
:global(.tile-two) {
grid-area: two;
.feed :global(.sns-tile) {
position: relative;
inset: auto;
width: 100%;
height: auto;
aspect-ratio: 4 / 5;
transform: none;
}
.sns-number,
.sns-caption {
display: none;
}
}
</style>

View File

@@ -3,7 +3,13 @@
// 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 = '', file = $bindable(null) } = $props();
let {
label = null,
showLabel = true,
class: klass = '',
style = '',
file = $bindable(null)
} = $props();
let preview = $state(null);
@@ -44,14 +50,14 @@
{#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}
<div class="absolute inset-0 bg-linear-to-t from-ink/45 to-transparent"></div>
{#if label && showLabel}
<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"
class="absolute top-3 right-3 rounded-full bg-ink/40 px-2.5 py-1 text-xs text-surface opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100"
>
Change
</span>
@@ -63,7 +69,7 @@
class="flex size-10 items-center justify-center rounded-full border border-current text-xl leading-none"
aria-hidden="true">+</span
>
{#if label}
{#if label && showLabel}
<span class="text-sm tracking-[0.15em] uppercase">{label}</span>
{/if}
</div>

View File

@@ -2,15 +2,19 @@
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
import DevSeedButton from '$lib/components/dev/DevSeedButton.svelte';
import FlowerCursor from '$lib/components/ui/FlowerCursor.svelte';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<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()}
<FlowerCursor />
<DevSeedButton />

View File

@@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import DescriptionCard from '$lib/components/ui/Artwork/DescriptionCard.svelte';
import Header from '$lib/components/ui/Header.svelte';
import { editImages, fetchJob, finalizeJob, toDataUrl } from '$lib/flowerFlow/api.js';
import { getFlowString, saveFlow } from '$lib/flowerFlow/session.js';
@@ -182,7 +183,7 @@
<section
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 justify-center gap-6">
<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">
{#if loading}
<div class="aspect-[4/5] w-full animate-pulse bg-placeholder"></div>
@@ -193,10 +194,7 @@
{/if}
</div>
<div class="border border-line-strong bg-surface px-5 py-4">
<h1 class="text-sm">{title}</h1>
<p class="mt-2 text-xs leading-relaxed text-muted">{description}</p>
</div>
<DescriptionCard {title} {description} />
</div>
</section>

View File

@@ -7,7 +7,7 @@
/* Color tokens */
--color-surface: #eeeeee; /* page background, text on dark */
--color-ink: #38322f; /* primary text */
--color-pill: #1a1a1a; /* active toggle pill */
--color-pill: #38322f; /* active toggle pill — logo color */
--color-track: #f4f4f4; /* toggle track */
--color-muted: #9e9e9e; /* inactive / secondary text */
--color-subtle: #7d7d7d; /* accent gray (active step dot, illustration) */
@@ -15,3 +15,20 @@
--color-line: #e5e7eb; /* dividers / light borders */
--color-line-strong: #d1d5db; /* card border */
}
@layer base {
html,
body {
background-color: var(--color-surface);
}
}
@layer utilities {
.bg-surface {
background-color: var(--color-surface);
background-image: url('/paper_texture.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
}

View File

@@ -27,6 +27,13 @@
let loading = $state(false);
let error = $state('');
const recipientPronoun = $derived.by(() => {
const style = typeof userInput.style === 'string' ? userInput.style.toLowerCase() : '';
if (style === 'masculine') return 'his';
if (style === 'feminine') return 'her';
return 'their';
});
async function continueToMessage() {
error = '';
@@ -77,18 +84,20 @@
>
<Header step={2} total={7} />
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
<main class="flex min-h-0 flex-1 flex-col pt-6 lg:flex-row lg:pt-8">
<Artwork />
<section class="relative flex min-h-0 flex-1 flex-col pb-[4.75rem] lg:overflow-hidden lg:pb-0">
<section
class="relative flex min-h-0 flex-1 flex-col pb-[4.75rem] lg:grid lg:grid-rows-[minmax(0,1fr)_auto] lg:overflow-hidden lg:pb-8"
>
{#if mode === 'moodboard'}
<MoodboardGrid bind:primaryFile />
<MoodboardGrid bind:primaryFile caption={`build ${recipientPronoun} moodboard!`} />
{:else}
<SnsFeedUpload bind:primaryFile />
<SnsFeedUpload bind:primaryFile caption={`upload ${recipientPronoun} feed!`} />
{/if}
<div
class="fixed right-0 bottom-0 left-0 z-20 space-y-2 px-4 pb-5 lg:absolute lg:right-8 lg:bottom-8 lg:left-auto lg:w-72 lg:px-0 lg:pb-0"
class="fixed right-0 bottom-0 left-0 z-20 space-y-2 px-4 pb-5 lg:static lg:mx-auto lg:flex lg:w-full lg:max-w-2xl lg:items-center lg:gap-3 lg:space-y-0 lg:px-6 lg:pb-0"
>
{#if error}
<p class="rounded bg-surface/95 px-3 py-2 text-sm text-red-600 ring-1 ring-black/5">
@@ -100,20 +109,26 @@
type="button"
disabled={loading}
onclick={continueToMessage}
class="w-full bg-pill px-4 py-3 text-sm text-surface disabled:opacity-50"
class="w-full px-2 py-3 text-sm whitespace-nowrap text-ink underline-offset-4 hover:underline disabled:opacity-50 lg:order-2 lg:w-auto"
>
{loading ? 'Analyzing mood...' : 'Continue to message'}
{loading ? 'Analyzing mood...' : 'Continue to message ->'}
</button>
<div
class="flex w-full items-center rounded-full bg-surface/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur"
class="relative grid w-full grid-cols-2 items-center rounded-full bg-white p-1.5 shadow-xl ring-1 ring-black/5 lg:order-1 lg:flex-1"
>
<!-- sliding dark thumb: covers one cell, glides to the active one -->
<span
class="pointer-events-none absolute inset-y-1.5 left-1.5 w-[calc(50%-0.375rem)] rounded-full bg-pill transition-transform duration-300 ease-out motion-reduce:transition-none"
style:transform={mode === 'moodboard' ? 'translateX(100%)' : 'translateX(0)'}
aria-hidden="true"
></span>
<button
type="button"
onclick={() => (mode = 'sns')}
class={[
'flex-1 rounded-full px-4 py-2.5 text-center text-sm whitespace-nowrap transition-colors',
mode === 'sns' ? 'bg-pill text-surface' : 'text-muted hover:text-ink'
'relative z-10 w-full rounded-full px-3 py-2.5 text-center text-sm whitespace-nowrap transition-colors',
mode === 'sns' ? 'text-surface' : 'text-muted hover:text-ink'
]}
>
Upload SNS Feed
@@ -122,8 +137,8 @@
type="button"
onclick={() => (mode = 'moodboard')}
class={[
'flex-1 rounded-full px-4 py-2.5 text-center text-sm whitespace-nowrap transition-colors',
mode === 'moodboard' ? 'bg-pill text-surface' : 'text-muted hover:text-ink'
'relative z-10 w-full rounded-full px-3 py-2.5 text-center text-sm whitespace-nowrap transition-colors',
mode === 'moodboard' ? 'text-surface' : 'text-muted hover:text-ink'
]}
>
Build Moodboard

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

BIN
static/paper_texture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB