feat: add create and message page UI
Co-authored-by: 이지은 <ijieun@ijieun-ui-MacBookPro.local>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Local notes (not pushed)
|
||||
docs/
|
||||
|
||||
71
src/lib/components/ui/create/BudgetSlider.svelte
Normal file
71
src/lib/components/ui/create/BudgetSlider.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script>
|
||||
let { budget = $bindable(50_000) } = $props();
|
||||
|
||||
const min = 10_000;
|
||||
const max = 150_000;
|
||||
const step = 5_000;
|
||||
|
||||
const fillPercent = $derived(((budget - min) / (max - min)) * 100);
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-xs tracking-[0.2em] text-muted uppercase">Budget</p>
|
||||
<p class="mt-2 text-3xl font-semibold tracking-tight">
|
||||
₩{budget.toLocaleString('ko-KR')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="relative h-6">
|
||||
<div class="absolute top-1/2 right-0 left-0 h-px -translate-y-1/2 bg-placeholder"></div>
|
||||
<div
|
||||
class="absolute top-1/2 left-0 h-px -translate-y-1/2 bg-subtle"
|
||||
style="width: {fillPercent}%"
|
||||
></div>
|
||||
<input
|
||||
type="range"
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
bind:value={budget}
|
||||
aria-label="Budget"
|
||||
class="budget-slider absolute inset-0 w-full cursor-pointer appearance-none bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-muted">
|
||||
<span>₩10,000</span>
|
||||
<span>₩150,000+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.budget-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-pill);
|
||||
border: none;
|
||||
margin-top: -0.4375rem;
|
||||
}
|
||||
|
||||
.budget-slider::-moz-range-thumb {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-pill);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.budget-slider::-webkit-slider-runnable-track {
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.budget-slider::-moz-range-track {
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
66
src/lib/components/ui/create/ContextForm.svelte
Normal file
66
src/lib/components/ui/create/ContextForm.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import OptionGroup from './OptionGroup.svelte';
|
||||
import BudgetSlider from './BudgetSlider.svelte';
|
||||
|
||||
let {
|
||||
who = $bindable(null),
|
||||
whatFor = $bindable(null),
|
||||
style = $bindable(null),
|
||||
budget = $bindable(50_000)
|
||||
} = $props();
|
||||
|
||||
const hasAnySelection = $derived(who !== null || whatFor !== null || style !== null);
|
||||
|
||||
const whoOptions = ['Friend', 'Family', 'Partner', 'Teacher', 'Others'];
|
||||
const whatForOptions = ['Birthday', 'Anniversary', 'Thanks', 'Daily'];
|
||||
const styleOptions = ['Feminine', 'Masculine', 'Neutral'];
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 flex-col justify-center px-6 py-10 md:px-12 lg:px-16 lg:py-16">
|
||||
<header class="mb-10 space-y-3 lg:mb-14">
|
||||
{#if !hasAnySelection}
|
||||
<h1 class="text-3xl leading-relaxed font-light text-muted md:text-4xl lg:text-[2.75rem]">
|
||||
Who are we making flowers for?
|
||||
</h1>
|
||||
<p class="text-sm text-muted">Pick a few details below</p>
|
||||
{:else}
|
||||
<h1 class="text-3xl leading-tight font-light text-muted md:text-4xl lg:text-[2.75rem]">
|
||||
{#if whatFor}
|
||||
A <span class="font-semibold text-ink underline decoration-ink/30 underline-offset-4"
|
||||
>{whatFor}</span
|
||||
>
|
||||
bouquet for
|
||||
{:else}
|
||||
A bouquet for
|
||||
{/if}
|
||||
{#if who}
|
||||
<span class="font-semibold text-ink underline decoration-ink/30 underline-offset-4"
|
||||
>{who}</span
|
||||
>
|
||||
{:else}
|
||||
<span class="text-muted">...</span>
|
||||
{/if}
|
||||
</h1>
|
||||
<p class="text-sm text-muted">
|
||||
{style ?? '—'} | ₩{budget.toLocaleString('ko-KR')}
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="space-y-8 lg:space-y-10">
|
||||
<OptionGroup label="Who" options={whoOptions} selected={who} onchange={(v) => (who = v)} />
|
||||
<OptionGroup
|
||||
label="What for"
|
||||
options={whatForOptions}
|
||||
selected={whatFor}
|
||||
onchange={(v) => (whatFor = v)}
|
||||
/>
|
||||
<OptionGroup
|
||||
label="Style"
|
||||
options={styleOptions}
|
||||
selected={style}
|
||||
onchange={(v) => (style = v)}
|
||||
/>
|
||||
<BudgetSlider bind:budget />
|
||||
</div>
|
||||
</div>
|
||||
21
src/lib/components/ui/create/OptionGroup.svelte
Normal file
21
src/lib/components/ui/create/OptionGroup.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
let { label, options, selected, onchange } = $props();
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm tracking-[0.2em] text-muted uppercase">{label}</p>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2">
|
||||
{#each options as option (option)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onchange(option)}
|
||||
class={[
|
||||
'text-xl tracking-wide transition-colors',
|
||||
selected === option ? 'text-ink' : 'text-muted hover:text-ink'
|
||||
]}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
34
src/lib/components/ui/message/MessageForm.svelte
Normal file
34
src/lib/components/ui/message/MessageForm.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import MessagePresetList from './MessagePresetList.svelte';
|
||||
|
||||
let { message = $bindable('') } = $props();
|
||||
|
||||
const presets = [
|
||||
'Happy Birthday!',
|
||||
'Thank you for always being there',
|
||||
'I love you',
|
||||
"I'm proud of you",
|
||||
'Congratulations!',
|
||||
];
|
||||
|
||||
const selectedPreset = $derived(presets.find((preset) => preset === message) ?? null);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 flex-col justify-center px-6 py-10 md:px-12 lg:px-16 lg:py-16">
|
||||
<header class="mb-8 lg:mb-10">
|
||||
<textarea
|
||||
bind:value={message}
|
||||
placeholder="Write something from your heart"
|
||||
rows="1"
|
||||
aria-label="Write your message"
|
||||
class="w-full resize-none border-none bg-transparent text-3xl leading-relaxed font-light text-ink placeholder:text-muted focus:outline-none md:text-4xl lg:text-[2.75rem]"
|
||||
></textarea>
|
||||
<div class="mt-6 border-b border-pill lg:mt-3"></div>
|
||||
</header>
|
||||
|
||||
<MessagePresetList
|
||||
options={presets}
|
||||
selected={selectedPreset}
|
||||
onchange={(preset) => (message = preset)}
|
||||
/>
|
||||
</div>
|
||||
19
src/lib/components/ui/message/MessagePresetList.svelte
Normal file
19
src/lib/components/ui/message/MessagePresetList.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
// OptionGroup과 같은 선택 버튼 스타일 — message는 세로 목록
|
||||
let { options, selected, onchange } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex max-w-md flex-col gap-y-8">
|
||||
{#each options as option (option)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onchange(option)}
|
||||
class={[
|
||||
'text-left text-xl tracking-wide transition-colors',
|
||||
selected === option ? 'text-ink' : 'text-muted hover:text-ink'
|
||||
]}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1 +1,42 @@
|
||||
<h1>/create page</h1>
|
||||
<script>
|
||||
import Header from '$lib/components/ui/Header.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
|
||||
import ContextForm from '$lib/components/ui/create/ContextForm.svelte';
|
||||
|
||||
let who = $state(null);
|
||||
let whatFor = $state(null);
|
||||
let style = $state(null);
|
||||
let budget = $state(50_000);
|
||||
|
||||
const hasAnySelection = $derived(who !== null || whatFor !== null || style !== null);
|
||||
|
||||
const artworkTitle = $derived.by(() => {
|
||||
if (!hasAnySelection) return 'Title';
|
||||
const occasion = whatFor ? `A ${whatFor} bouquet for` : 'A bouquet for';
|
||||
return `${occasion} ${who ?? '...'}`;
|
||||
});
|
||||
|
||||
const artworkDescription = $derived(
|
||||
hasAnySelection
|
||||
? `${style ?? '—'} style · ₩${budget.toLocaleString('ko-KR')} budget`
|
||||
: 'Description Description Description'
|
||||
);
|
||||
</script>
|
||||
|
||||
<!--
|
||||
upload와 같은 2열 레이아웃: 좌측 Artwork 고정, 우측 ContextForm.
|
||||
선택값이 바뀌면 create1 → create2 헤드라인·요약이 반응형으로 전환됩니다.
|
||||
-->
|
||||
<div
|
||||
class="flex h-dvh flex-col overflow-x-hidden bg-surface text-ink lg:h-screen lg:overflow-hidden"
|
||||
>
|
||||
<Header step={1} total={6} />
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork title={artworkTitle} description={artworkDescription} />
|
||||
|
||||
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||
<ContextForm bind:who bind:whatFor bind:style bind:budget />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
29
src/routes/message/+page.svelte
Normal file
29
src/routes/message/+page.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import Header from '$lib/components/ui/Header.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
|
||||
import MessageForm from '$lib/components/ui/message/MessageForm.svelte';
|
||||
|
||||
let message = $state('');
|
||||
|
||||
const artworkTitle = $derived(message ? 'Your message' : 'Title');
|
||||
|
||||
const artworkDescription = $derived(message || 'Description Description Description');
|
||||
</script>
|
||||
|
||||
<!--
|
||||
create / upload와 같은 2열 레이아웃.
|
||||
우측 MessageForm에서 프리셋 메시지를 pill 버튼으로 선택합니다.
|
||||
-->
|
||||
<div
|
||||
class="flex h-dvh flex-col overflow-x-hidden bg-surface text-ink lg:h-screen lg:overflow-hidden"
|
||||
>
|
||||
<Header step={4} total={6} />
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork title={artworkTitle} description={artworkDescription} />
|
||||
|
||||
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||
<MessageForm bind:message />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
Reference in New Issue
Block a user