chore: apply prettier formatting and fix lint errors

This commit is contained in:
codenamewont
2026-06-15 09:25:01 +09:00
parent 063a193396
commit 84c8a0aac9
29 changed files with 1421 additions and 471 deletions

View File

@@ -132,7 +132,9 @@ async function main() {
const size = process.env.OPENAI_IMAGE_CATALOG_SIZE || '1024x1536';
const quality = process.env.OPENAI_IMAGE_CATALOG_QUALITY || 'low';
console.log(`대상: ${targets.length}종 · ${size} · quality=${quality}${dryRun ? ' (dry-run)' : ''}`);
console.log(
`대상: ${targets.length}종 · ${size} · quality=${quality}${dryRun ? ' (dry-run)' : ''}`
);
for (const flower of targets) {
const outPath = join(OUT_DIR, `${flower.id}.png`);

View File

@@ -19,13 +19,13 @@
</script>
<section
class="relative flex w-full shrink-0 flex-col border-b border-line lg:min-h-0 lg:h-full lg:w-[44%] lg:shrink-0 lg:overflow-y-auto lg:border-r lg:border-b-0"
class="relative flex w-full shrink-0 flex-col border-b border-line lg:h-full lg:min-h-0 lg:w-[44%] lg:shrink-0 lg:overflow-y-auto lg:border-r lg:border-b-0"
>
<!--
mobile: row · desktop: 꽃 슬롯 높이 고정 → 설명 카드 길이와 무관하게 Y·크기 유지
-->
<div
class="mx-auto flex min-h-0 w-full max-w-100 flex-1 flex-row items-start gap-8 px-6 pt-6 pb-8 lg:flex-col lg:items-center lg:justify-start lg:gap-4 lg:px-6 lg:pb-12 lg:pt-[calc(50%-5rem)]"
class="mx-auto flex min-h-0 w-full max-w-100 flex-1 flex-row items-start gap-8 px-6 pt-6 pb-8 lg:flex-col lg:items-center lg:justify-start lg:gap-4 lg:px-6 lg:pt-[calc(50%-5rem)] lg:pb-12"
>
<div
class="flex h-[11rem] shrink-0 items-end justify-center sm:h-[13rem] lg:h-[min(24rem,36vh)] lg:w-full"
@@ -43,7 +43,7 @@
{/if}
</div>
<div class="min-w-0 shrink-0 lg:w-full lg:flex lg:justify-center">
<div class="min-w-0 shrink-0 lg:flex lg:w-full lg:justify-center">
<DescriptionCard {title} {description} mode={cardMode} />
</div>
</div>

View File

@@ -7,9 +7,14 @@
} = $props();
</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">
<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 leading-snug font-semibold', mode === 'instruction' ? 'text-muted' : 'text-ink']}
class={[
'text-sm leading-snug font-semibold',
mode === 'instruction' ? 'text-muted' : 'text-ink'
]}
>
{title}
</h3>

View File

@@ -1,9 +1,7 @@
<script>
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import FlowContinueBar, { FLOW_CONTINUE_BUTTON } from '$lib/components/ui/FlowContinueBar.svelte';
import GrowthMetaphorIllustration from '$lib/components/ui/landing/GrowthMetaphorIllustration.svelte';
function handleStart() {
@@ -15,7 +13,7 @@
class="relative flex min-h-dvh flex-col bg-surface px-6 py-8 pb-[3.75rem] font-sans text-ink sm:px-10 sm:py-10 lg:px-14 lg:pb-8"
aria-label="Every bouquet starts with a muse — seed to bouquet growth metaphor"
>
<div class="mx-auto flex w-full max-w-6xl min-h-0 flex-1 flex-col justify-center">
<div class="mx-auto flex min-h-0 w-full max-w-6xl flex-1 flex-col justify-center">
<GrowthMetaphorIllustration />
<p class="mt-3 text-left text-sm tracking-[0.18em] text-muted sm:mt-4 sm:text-base">

View File

@@ -1,8 +1,5 @@
<script>
let {
enPlainText = '',
koPlainText = ''
} = $props();
let { enPlainText = '', koPlainText = '' } = $props();
/** @type {'ko' | 'en'} */
let activeLang = $state('ko');
@@ -61,10 +58,14 @@
rows={4}
value={activeText}
oninput={handleInput}
aria-label={activeLang === 'ko' ? '꽃집 주문 멘트 (한국어)' : 'Florist order message (English)'}
aria-label={activeLang === 'ko'
? '꽃집 주문 멘트 (한국어)'
: 'Florist order message (English)'}
></textarea>
{:else}
<p class="min-w-0 flex-1 text-sm text-muted">Complete the flow to generate your order message.</p>
<p class="min-w-0 flex-1 text-sm text-muted">
Complete the flow to generate your order message.
</p>
{/if}
<div class="flex shrink-0 flex-col items-stretch gap-2">

View File

@@ -1,5 +1,6 @@
<script>
import { onMount } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import { env } from '$env/dynamic/public';
let {
@@ -23,8 +24,8 @@
let mapInstance = $state(null);
/** @type {ReturnType<typeof window.kakao.maps.InfoWindow> | null} */
let infoWindow = null;
/** @type {Map<string, { marker: ReturnType<typeof window.kakao.maps.Marker>; shop: (typeof shops)[number] }>} */
let shopMarkerMap = new Map();
/** @type {SvelteMap<string, { marker: ReturnType<typeof window.kakao.maps.Marker>; shop: (typeof shops)[number] }>} */
let shopMarkerMap = new SvelteMap();
function relayoutMap() {
mapInstance?.relayout?.();
@@ -226,9 +227,7 @@
{mapError}
</div>
{:else if !mapReady}
<div
class="absolute inset-0 flex items-center justify-center bg-track text-sm text-muted"
>
<div class="absolute inset-0 flex items-center justify-center bg-track text-sm text-muted">
Loading map...
</div>
{/if}

View File

@@ -71,10 +71,12 @@
{/if}
<div class="flex min-h-0 flex-1 flex-col gap-6 px-6 pb-8 md:px-10 lg:flex-row lg:px-12 lg:pb-10">
<div class="relative flex min-h-64 flex-1 flex-col overflow-hidden border border-line lg:min-h-0">
<div
class="relative flex min-h-64 flex-1 flex-col overflow-hidden border border-line lg:min-h-0"
>
<KakaoMap
initialLat={initialLat}
initialLng={initialLng}
{initialLat}
{initialLng}
{shops}
selectedId={selectedShopId}
{fitBounds}
@@ -96,7 +98,7 @@
{#if loading && shops.length === 0}
<p class="text-sm text-muted">Searching for flower shops...</p>
{:else}
<ShopList shops={shops} bind:selectedId={selectedShopId} onselect={handleShopSelect} />
<ShopList {shops} bind:selectedId={selectedShopId} onselect={handleShopSelect} />
{/if}
</div>
</div>

View File

@@ -8,7 +8,7 @@
'Thank you for always being there',
'I love you',
"I'm proud of you",
'Congratulations!',
'Congratulations!'
];
const selectedPreset = $derived(presets.find((preset) => preset === message) ?? null);

View File

@@ -1,14 +1,7 @@
<script>
import flowerIconUrl from '$lib/assets/flower.svg';
let {
name,
nameKo,
wordOfFlower,
wordOfFlowerKo,
imageSrc,
role = 'main'
} = $props();
let { name, nameKo, wordOfFlower, wordOfFlowerKo, imageSrc, role = 'main' } = $props();
let flipped = $state(false);
@@ -26,7 +19,7 @@
<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"
class="flip-card h-[16.25rem] w-40 shrink-0 cursor-pointer snap-start border-none bg-transparent p-0 text-left"
aria-label={flipped ? `${nameKo} card, show English` : `${name} card, show Korean`}
onclick={toggleFlip}
>
@@ -48,7 +41,7 @@
/>
</div>
<div class="shrink-0 px-3 pb-4 pt-2">
<div class="shrink-0 px-3 pt-2 pb-4">
<h3
class="flex min-h-8 items-center justify-center text-center text-sm leading-tight tracking-wide text-ink"
>
@@ -79,7 +72,7 @@
/>
</div>
<div class="shrink-0 px-3 pb-4 pt-2">
<div class="shrink-0 px-3 pt-2 pb-4">
<h3
class="flex min-h-8 items-center justify-center text-center text-sm leading-tight tracking-wide text-ink"
>

View File

@@ -12,7 +12,7 @@
<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"
class="flex snap-x snap-mandatory [scrollbar-width:none] gap-4 overflow-x-auto px-0.5 py-1 [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
>
{#each flowers as flower (flower.id)}
<BouquetFlowerCard

View File

@@ -16,8 +16,7 @@ export const ARTWORK_CARD_DEFAULTS = {
},
generating: {
title: 'Crafting your bouquet',
description:
'We are turning their mood, photos, and message into a one-of-a-kind arrangement.'
description: 'We are turning their mood, photos, and message into a one-of-a-kind arrangement.'
},
map: {
title: 'Choose a florist',

View File

@@ -20,7 +20,9 @@ export function formatStrictBouquetImagePrompt(recipe) {
'Generate a realistic Korean florist bouquet product photo.',
'',
'STRICT RECIPE — the bouquet must contain ONLY these flowers and NO other flower species:',
allFlowers.length > 0 ? allFlowers.map((flower) => `- ${flower}`).join('\n') : '- (none listed)',
allFlowers.length > 0
? allFlowers.map((flower) => `- ${flower}`).join('\n')
: '- (none listed)',
'',
`Main focal blooms (each must be clearly visible): ${mains.join(', ') || 'none'}`,
`Supporting filler/line flowers (each must appear): ${subs.join(', ') || 'none'}`,

View File

@@ -60,7 +60,10 @@ export function buildFloristOrderMessage(input) {
`Would a reservation be possible?`;
const segments = [
{ text: "Hello, I'd like to inquire about a flower order. It's a bouquet for ", highlight: false },
{
text: "Hello, I'd like to inquire about a flower order. It's a bouquet for ",
highlight: false
},
{ text: relationship, highlight: true },
{ text: ' for ', highlight: false },
{ text: occasion, highlight: true },

View File

@@ -3,468 +3,468 @@
/** @type {{ id: number, name: string, wordOfFlower: string }[]} */
export const flowerCatalogLite = [
{
"id": 1,
"name": "Daffodil",
"wordOfFlower": "self-love"
id: 1,
name: 'Daffodil',
wordOfFlower: 'self-love'
},
{
"id": 2,
"name": "Stock",
"wordOfFlower": "lasting beauty"
id: 2,
name: 'Stock',
wordOfFlower: 'lasting beauty'
},
{
"id": 3,
"name": "Amaryllis",
"wordOfFlower": "dazzling beauty"
id: 3,
name: 'Amaryllis',
wordOfFlower: 'dazzling beauty'
},
{
"id": 4,
"name": "Sweet Pea",
"wordOfFlower": "delicate pleasures"
id: 4,
name: 'Sweet Pea',
wordOfFlower: 'delicate pleasures'
},
{
"id": 5,
"name": "Anthurium",
"wordOfFlower": "hospitality"
id: 5,
name: 'Anthurium',
wordOfFlower: 'hospitality'
},
{
"id": 6,
"name": "Freesia",
"wordOfFlower": "purity"
id: 6,
name: 'Freesia',
wordOfFlower: 'purity'
},
{
"id": 7,
"name": "Tulip",
"wordOfFlower": "benevolence"
id: 7,
name: 'Tulip',
wordOfFlower: 'benevolence'
},
{
"id": 8,
"name": "Hyacinth",
"wordOfFlower": "joy of the heart"
id: 8,
name: 'Hyacinth',
wordOfFlower: 'joy of the heart'
},
{
"id": 9,
"name": "Ranunculus",
"wordOfFlower": "radiant charm"
id: 9,
name: 'Ranunculus',
wordOfFlower: 'radiant charm'
},
{
"id": 10,
"name": "Lilac",
"wordOfFlower": "memories of youth"
id: 10,
name: 'Lilac',
wordOfFlower: 'memories of youth'
},
{
"id": 11,
"name": "Iris",
"wordOfFlower": "good news"
id: 11,
name: 'Iris',
wordOfFlower: 'good news'
},
{
"id": 12,
"name": "Tree Peony",
"wordOfFlower": "wealth and honor"
id: 12,
name: 'Tree Peony',
wordOfFlower: 'wealth and honor'
},
{
"id": 13,
"name": "Peony",
"wordOfFlower": "shyness"
id: 13,
name: 'Peony',
wordOfFlower: 'shyness'
},
{
"id": 14,
"name": "Rose",
"wordOfFlower": "passionate love"
id: 14,
name: 'Rose',
wordOfFlower: 'passionate love'
},
{
"id": 15,
"name": "Snowball Viburnum",
"wordOfFlower": "grace"
id: 15,
name: 'Snowball Viburnum',
wordOfFlower: 'grace'
},
{
"id": 16,
"name": "Carnation",
"wordOfFlower": "a woman's affection"
id: 16,
name: 'Carnation',
wordOfFlower: "a woman's affection"
},
{
"id": 17,
"name": "Clematis",
"wordOfFlower": "nobility"
id: 17,
name: 'Clematis',
wordOfFlower: 'nobility'
},
{
"id": 18,
"name": "Lily",
"wordOfFlower": "purity"
id: 18,
name: 'Lily',
wordOfFlower: 'purity'
},
{
"id": 19,
"name": "Hydrangea",
"wordOfFlower": "heartlessness"
id: 19,
name: 'Hydrangea',
wordOfFlower: 'heartlessness'
},
{
"id": 20,
"name": "Agapanthus",
"wordOfFlower": "love letter"
id: 20,
name: 'Agapanthus',
wordOfFlower: 'love letter'
},
{
"id": 21,
"name": "Allium",
"wordOfFlower": "endless sorrow"
id: 21,
name: 'Allium',
wordOfFlower: 'endless sorrow'
},
{
"id": 22,
"name": "Bellflower (Campanula)",
"wordOfFlower": "a coquettish look"
id: 22,
name: 'Bellflower (Campanula)',
wordOfFlower: 'a coquettish look'
},
{
"id": 23,
"name": "China Aster (Callistephus)",
"wordOfFlower": "trusting love"
id: 23,
name: 'China Aster (Callistephus)',
wordOfFlower: 'trusting love'
},
{
"id": 24,
"name": "Poppy (Papaver)",
"wordOfFlower": "consolation"
id: 24,
name: 'Poppy (Papaver)',
wordOfFlower: 'consolation'
},
{
"id": 25,
"name": "Dahlia",
"wordOfFlower": "gratitude"
id: 25,
name: 'Dahlia',
wordOfFlower: 'gratitude'
},
{
"id": 26,
"name": "Lotus",
"wordOfFlower": "purity"
id: 26,
name: 'Lotus',
wordOfFlower: 'purity'
},
{
"id": 27,
"name": "Gentian",
"wordOfFlower": "I love you when you're sad"
id: 27,
name: 'Gentian',
wordOfFlower: "I love you when you're sad"
},
{
"id": 28,
"name": "Sunflower",
"wordOfFlower": "adoration"
id: 28,
name: 'Sunflower',
wordOfFlower: 'adoration'
},
{
"id": 29,
"name": "Chrysanthemum",
"wordOfFlower": "purity"
id: 29,
name: 'Chrysanthemum',
wordOfFlower: 'purity'
},
{
"id": 30,
"name": "Cockscomb (Celosia)",
"wordOfFlower": "ardent love"
id: 30,
name: 'Cockscomb (Celosia)',
wordOfFlower: 'ardent love'
},
{
"id": 31,
"name": "Anemone",
"wordOfFlower": "sincerity"
id: 31,
name: 'Anemone',
wordOfFlower: 'sincerity'
},
{
"id": 32,
"name": "Cosmos",
"wordOfFlower": "a girl's pure heart"
id: 32,
name: 'Cosmos',
wordOfFlower: "a girl's pure heart"
},
{
"id": 33,
"name": "Red Spider Lily (Lycoris radiata)",
"wordOfFlower": "true love"
id: 33,
name: 'Red Spider Lily (Lycoris radiata)',
wordOfFlower: 'true love'
},
{
"id": 34,
"name": "Gerbera",
"wordOfFlower": "mystery"
id: 34,
name: 'Gerbera',
wordOfFlower: 'mystery'
},
{
"id": 35,
"name": "Calla Lily",
"wordOfFlower": "joy"
id: 35,
name: 'Calla Lily',
wordOfFlower: 'joy'
},
{
"id": 36,
"name": "Bird of Paradise (Strelitzia)",
"wordOfFlower": "mystery"
id: 36,
name: 'Bird of Paradise (Strelitzia)',
wordOfFlower: 'mystery'
},
{
"id": 37,
"name": "Hellebore",
"wordOfFlower": "reason for being"
id: 37,
name: 'Hellebore',
wordOfFlower: 'reason for being'
},
{
"id": 38,
"name": "Lisianthus (Eustoma)",
"wordOfFlower": "appreciation"
id: 38,
name: 'Lisianthus (Eustoma)',
wordOfFlower: 'appreciation'
},
{
"id": 39,
"name": "Scabiosa",
"wordOfFlower": "I have lost all"
id: 39,
name: 'Scabiosa',
wordOfFlower: 'I have lost all'
},
{
"id": 40,
"name": "Wax Flower (Chamelaucium)",
"wordOfFlower": "lasting love"
id: 40,
name: 'Wax Flower (Chamelaucium)',
wordOfFlower: 'lasting love'
},
{
"id": 41,
"name": "Caspia (Limonium)",
"wordOfFlower": "remembrance"
id: 41,
name: 'Caspia (Limonium)',
wordOfFlower: 'remembrance'
},
{
"id": 42,
"name": "Ruscus",
"wordOfFlower": "endurance"
id: 42,
name: 'Ruscus',
wordOfFlower: 'endurance'
},
{
"id": 43,
"name": "Veronica (Speedwell)",
"wordOfFlower": "fidelity"
id: 43,
name: 'Veronica (Speedwell)',
wordOfFlower: 'fidelity'
},
{
"id": 44,
"name": "Solidago (Goldenrod)",
"wordOfFlower": "encouragement"
id: 44,
name: 'Solidago (Goldenrod)',
wordOfFlower: 'encouragement'
},
{
"id": 45,
"name": "Bouvardia",
"wordOfFlower": "enthusiasm"
id: 45,
name: 'Bouvardia',
wordOfFlower: 'enthusiasm'
},
{
"id": 46,
"name": "Tweedia (Oxypetalum)",
"wordOfFlower": "believe in me"
id: 46,
name: 'Tweedia (Oxypetalum)',
wordOfFlower: 'believe in me'
},
{
"id": 47,
"name": "Craspedia (Billy Balls)",
"wordOfFlower": "good health"
id: 47,
name: 'Craspedia (Billy Balls)',
wordOfFlower: 'good health'
},
{
"id": 48,
"name": "Amaranthus",
"wordOfFlower": "immortality"
id: 48,
name: 'Amaranthus',
wordOfFlower: 'immortality'
},
{
"id": 49,
"name": "Queen Anne's Lace (Ammi)",
"wordOfFlower": "sanctuary"
id: 49,
name: "Queen Anne's Lace (Ammi)",
wordOfFlower: 'sanctuary'
},
{
"id": 50,
"name": "Nigella (Love-in-a-mist)",
"wordOfFlower": "perplexity"
id: 50,
name: 'Nigella (Love-in-a-mist)',
wordOfFlower: 'perplexity'
},
{
"id": 51,
"name": "Brunia",
"wordOfFlower": "unity"
id: 51,
name: 'Brunia',
wordOfFlower: 'unity'
},
{
"id": 52,
"name": "Snapdragon",
"wordOfFlower": "desire"
id: 52,
name: 'Snapdragon',
wordOfFlower: 'desire'
},
{
"id": 53,
"name": "Lupine",
"wordOfFlower": "lust for life"
id: 53,
name: 'Lupine',
wordOfFlower: 'lust for life'
},
{
"id": 54,
"name": "Gladiolus",
"wordOfFlower": "secret meeting"
id: 54,
name: 'Gladiolus',
wordOfFlower: 'secret meeting'
},
{
"id": 55,
"name": "Foxglove (Digitalis)",
"wordOfFlower": "ardent love"
id: 55,
name: 'Foxglove (Digitalis)',
wordOfFlower: 'ardent love'
},
{
"id": 56,
"name": "Delphinium",
"wordOfFlower": "understand my heart"
id: 56,
name: 'Delphinium',
wordOfFlower: 'understand my heart'
},
{
"id": 57,
"name": "Salvia (Scarlet Sage)",
"wordOfFlower": "burning heart"
id: 57,
name: 'Salvia (Scarlet Sage)',
wordOfFlower: 'burning heart'
},
{
"id": 58,
"name": "Grape Hyacinth (Muscari)",
"wordOfFlower": "disappointment"
id: 58,
name: 'Grape Hyacinth (Muscari)',
wordOfFlower: 'disappointment'
},
{
"id": 59,
"name": "Forget-me-not",
"wordOfFlower": "true love"
id: 59,
name: 'Forget-me-not',
wordOfFlower: 'true love'
},
{
"id": 60,
"name": "Statice (Limonium)",
"wordOfFlower": "eternal love"
id: 60,
name: 'Statice (Limonium)',
wordOfFlower: 'eternal love'
},
{
"id": 61,
"name": "Astilbe",
"wordOfFlower": "bashfulness"
id: 61,
name: 'Astilbe',
wordOfFlower: 'bashfulness'
},
{
"id": 62,
"name": "Strawflower (Helichrysum)",
"wordOfFlower": "always remember"
id: 62,
name: 'Strawflower (Helichrysum)',
wordOfFlower: 'always remember'
},
{
"id": 63,
"name": "Cornflower (Centaurea)",
"wordOfFlower": "happiness"
id: 63,
name: 'Cornflower (Centaurea)',
wordOfFlower: 'happiness'
},
{
"id": 64,
"name": "Chinese Lantern (Physalis)",
"wordOfFlower": "falsehood"
id: 64,
name: 'Chinese Lantern (Physalis)',
wordOfFlower: 'falsehood'
},
{
"id": 65,
"name": "Globe Amaranth (Gomphrena)",
"wordOfFlower": "immortality"
id: 65,
name: 'Globe Amaranth (Gomphrena)',
wordOfFlower: 'immortality'
},
{
"id": 66,
"name": "Showy Stonecrop (Sedum)",
"wordOfFlower": "hope"
id: 66,
name: 'Showy Stonecrop (Sedum)',
wordOfFlower: 'hope'
},
{
"id": 67,
"name": "Baby's Breath (Gypsophila)",
"wordOfFlower": "earnest joy"
id: 67,
name: "Baby's Breath (Gypsophila)",
wordOfFlower: 'earnest joy'
},
{
"id": 68,
"name": "Cattleya",
"wordOfFlower": "you are a beauty"
id: 68,
name: 'Cattleya',
wordOfFlower: 'you are a beauty'
},
{
"id": 69,
"name": "Oncidium",
"wordOfFlower": "innocent heart"
id: 69,
name: 'Oncidium',
wordOfFlower: 'innocent heart'
},
{
"id": 70,
"name": "Dendrobium",
"wordOfFlower": "a beauty"
id: 70,
name: 'Dendrobium',
wordOfFlower: 'a beauty'
},
{
"id": 71,
"name": "Vanda Orchid",
"wordOfFlower": "a token of affection"
id: 71,
name: 'Vanda Orchid',
wordOfFlower: 'a token of affection'
},
{
"id": 72,
"name": "Holly",
"wordOfFlower": "protection"
id: 72,
name: 'Holly',
wordOfFlower: 'protection'
},
{
"id": 73,
"name": "Nandina (Heavenly Bamboo)",
"wordOfFlower": "enduring love"
id: 73,
name: 'Nandina (Heavenly Bamboo)',
wordOfFlower: 'enduring love'
},
{
"id": 74,
"name": "Pussy Willow (Salix gracilistyla)",
"wordOfFlower": "freedom"
id: 74,
name: 'Pussy Willow (Salix gracilistyla)',
wordOfFlower: 'freedom'
},
{
"id": 75,
"name": "Maidenhair Fern (Adiantum)",
"wordOfFlower": "charm"
id: 75,
name: 'Maidenhair Fern (Adiantum)',
wordOfFlower: 'charm'
},
{
"id": 76,
"name": "Cotton",
"wordOfFlower": "mother's love"
id: 76,
name: 'Cotton',
wordOfFlower: "mother's love"
},
{
"id": 77,
"name": "Hosta (Plantain Lily)",
"wordOfFlower": "composure"
id: 77,
name: 'Hosta (Plantain Lily)',
wordOfFlower: 'composure'
},
{
"id": 78,
"name": "Beautyberry (Callicarpa)",
"wordOfFlower": "intelligence"
id: 78,
name: 'Beautyberry (Callicarpa)',
wordOfFlower: 'intelligence'
},
{
"id": 79,
"name": "Reed",
"wordOfFlower": "faith"
id: 79,
name: 'Reed',
wordOfFlower: 'faith'
},
{
"id": 80,
"name": "Foxtail Millet",
"wordOfFlower": "equality"
id: 80,
name: 'Foxtail Millet',
wordOfFlower: 'equality'
},
{
"id": 81,
"name": "Mint",
"wordOfFlower": "virtue"
id: 81,
name: 'Mint',
wordOfFlower: 'virtue'
},
{
"id": 82,
"name": "Asparagus Fern",
"wordOfFlower": "constancy"
id: 82,
name: 'Asparagus Fern',
wordOfFlower: 'constancy'
},
{
"id": 83,
"name": "Silver Grass (Miscanthus)",
"wordOfFlower": "retirement"
id: 83,
name: 'Silver Grass (Miscanthus)',
wordOfFlower: 'retirement'
},
{
"id": 84,
"name": "Eucalyptus",
"wordOfFlower": "memories"
id: 84,
name: 'Eucalyptus',
wordOfFlower: 'memories'
},
{
"id": 85,
"name": "Ivy (Hedera)",
"wordOfFlower": "a steadfast heart"
id: 85,
name: 'Ivy (Hedera)',
wordOfFlower: 'a steadfast heart'
},
{
"id": 86,
"name": "Olive",
"wordOfFlower": "peace"
id: 86,
name: 'Olive',
wordOfFlower: 'peace'
},
{
"id": 87,
"name": "Mock Orange (Pittosporum tobira)",
"wordOfFlower": "embrace"
id: 87,
name: 'Mock Orange (Pittosporum tobira)',
wordOfFlower: 'embrace'
},
{
"id": 88,
"name": "Cherry Blossom",
"wordOfFlower": "spiritual beauty"
id: 88,
name: 'Cherry Blossom',
wordOfFlower: 'spiritual beauty'
},
{
"id": 89,
"name": "Forsythia",
"wordOfFlower": "hope"
id: 89,
name: 'Forsythia',
wordOfFlower: 'hope'
},
{
"id": 90,
"name": "Plum Blossom (Prunus mume)",
"wordOfFlower": "pure heart"
id: 90,
name: 'Plum Blossom (Prunus mume)',
wordOfFlower: 'pure heart'
},
{
"id": 91,
"name": "Magnolia",
"wordOfFlower": "love of nature"
id: 91,
name: 'Magnolia',
wordOfFlower: 'love of nature'
},
{
"id": 92,
"name": "Redbud (Cercis chinensis)",
"wordOfFlower": "friendship"
id: 92,
name: 'Redbud (Cercis chinensis)',
wordOfFlower: 'friendship'
},
{
"id": 93,
"name": "Flowering Quince (Chaenomeles)",
"wordOfFlower": "trust"
id: 93,
name: 'Flowering Quince (Chaenomeles)',
wordOfFlower: 'trust'
}
];

View File

@@ -236,8 +236,7 @@ export function buildBouquetRationale(moodAnalysis, userInput, recipe) {
}
if (cardMessage && mainFlower) {
const messageRef =
cardMessage.length <= 40 ? `your message, "${cardMessage}"` : 'your message';
const messageRef = cardMessage.length <= 40 ? `your message, "${cardMessage}"` : 'your message';
parts.push(
`For ${messageRef}, ${mainFlower.name} (${mainFlower.wordOfFlower}) felt like the right fit.`
);

View File

@@ -56,7 +56,8 @@ export function getFlowUserInput() {
const input = getFlowObject('userInput');
if (!input) return {};
const { notes: _notes, ...createOnly } = input;
const createOnly = { ...input };
delete createOnly.notes;
return createOnly;
}

View File

@@ -35,5 +35,4 @@ export const LANDING_CYCLE_HOLD_MS = 3000;
const lastStage = LANDING_GROWTH_STAGES[LANDING_GROWTH_STAGES.length - 1];
/** 4단계 reveal 완료 + hold 후 다음 사이클까지 */
export const LANDING_CYCLE_MS =
lastStage.delayMs + LANDING_STAGE_REVEAL_MS + LANDING_CYCLE_HOLD_MS;
export const LANDING_CYCLE_MS = lastStage.delayMs + LANDING_STAGE_REVEAL_MS + LANDING_CYCLE_HOLD_MS;

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,8 @@ function pointInPolygon(x, y, polygon) {
const yi = polygon[i].y;
const xj = polygon[j].x;
const yj = polygon[j].y;
const intersects = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi + Number.EPSILON) + xi;
const intersects =
yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi + Number.EPSILON) + xi;
if (intersects) inside = !inside;
}
return inside;
@@ -68,11 +69,18 @@ function readJpegDimensions(buffer) {
* @param {string} mimeType
*/
export function readImageDimensions(buffer, mimeType) {
if (mimeType.includes('png') || (buffer[0] === 0x89 && buffer.toString('ascii', 1, 4) === 'PNG')) {
if (
mimeType.includes('png') ||
(buffer[0] === 0x89 && buffer.toString('ascii', 1, 4) === 'PNG')
) {
return readPngDimensions(buffer);
}
if (mimeType.includes('jpeg') || mimeType.includes('jpg') || (buffer[0] === 0xff && buffer[1] === 0xd8)) {
if (
mimeType.includes('jpeg') ||
mimeType.includes('jpg') ||
(buffer[0] === 0xff && buffer[1] === 0xd8)
) {
return readJpegDimensions(buffer);
}

View File

@@ -99,12 +99,15 @@ export async function editBouquetImage(sourceImage, editPrompt, options = {}) {
const model = getImageModel();
/** @type {import('@google/generative-ai').Part[]} */
const parts = [{ text: editPrompt }, {
inlineData: {
data: sourceImage.base64,
mimeType: sourceImage.mimeType
const parts = [
{ text: editPrompt },
{
inlineData: {
data: sourceImage.base64,
mimeType: sourceImage.mimeType
}
}
}];
];
if (mask) {
parts.push(

View File

@@ -107,8 +107,7 @@ export function mockApplyRecipeEdit(recipe, editPrompt) {
if (!label.toLowerCase().includes(fromToken)) return label;
const colorPrefix = label.match(/^(\w+)\s+/i)?.[1];
const capitalizedTo =
toToken.charAt(0).toUpperCase() + toToken.slice(1).toLowerCase();
const capitalizedTo = toToken.charAt(0).toUpperCase() + toToken.slice(1).toLowerCase();
if (colorPrefix && !fromToken.includes(' ')) {
return `${colorPrefix} ${capitalizedTo}`;

View File

@@ -68,7 +68,9 @@ Rules:
- Budget should be ${budget}.`;
const result = await model.generateContent(prompt);
return normalizeRecipeLists(/** @type {BouquetRecipe} */ (parseJsonFromText(result.response.text())));
return normalizeRecipeLists(
/** @type {BouquetRecipe} */ (parseJsonFromText(result.response.text()))
);
}
/**
@@ -144,5 +146,7 @@ Rules:
- The updated recipe is the sole source of truth for the next bouquet image — every listed flower must be included in the image prompt.`;
const result = await model.generateContent(prompt);
return normalizeRecipeLists(/** @type {BouquetRecipe} */ (parseJsonFromText(result.response.text())));
return normalizeRecipeLists(
/** @type {BouquetRecipe} */ (parseJsonFromText(result.response.text()))
);
}

View File

@@ -56,7 +56,10 @@ export async function POST({ request }) {
}
if (!job.images?.primary) {
return json({ error: 'bouquet image is missing. Generate images first.', code: 'bad_request' }, 400);
return json(
{ error: 'bouquet image is missing. Generate images first.', code: 'bad_request' },
400
);
}
const priorRecipe = normalizeRecipeLists(job.recipe);
@@ -75,11 +78,7 @@ export async function POST({ request }) {
const provider = getImageProvider();
const mask =
mode === 'area' && selection.length >= 3
? buildAreaEditMask(
sourceImage,
selection,
provider === 'gemini' ? 'gemini' : 'openai'
)
? buildAreaEditMask(sourceImage, selection, provider === 'gemini' ? 'gemini' : 'openai')
: null;
console.log(

View File

@@ -5,9 +5,7 @@
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';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import FlowContinueBar, { FLOW_CONTINUE_BUTTON } from '$lib/components/ui/FlowContinueBar.svelte';
import {
consumeDevCreateSnapshot,
deleteFlowKey,

View File

@@ -3,9 +3,7 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import DescriptionCard from '$lib/components/ui/Artwork/DescriptionCard.svelte';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import FlowContinueBar, { FLOW_CONTINUE_BUTTON } from '$lib/components/ui/FlowContinueBar.svelte';
import Header from '$lib/components/ui/Header.svelte';
import { editImages, fetchJob, finalizeJob, toDataUrl } from '$lib/flowerFlow/api.js';
import { buildBriefBouquetTitle } from '$lib/flowerFlow/resolveRecipeFlowers.js';
@@ -185,9 +183,7 @@
const afterImage = result.images?.primary ?? null;
generatedImage = afterImage;
chatMessages = chatMessages.map((message) =>
message.id === assistantMessageId
? { ...message, status: 'done', afterImage }
: message
message.id === assistantMessageId ? { ...message, status: 'done', afterImage } : message
);
saveFlow({
editInstruction: instruction,
@@ -248,7 +244,7 @@
</script>
{#snippet editableImageFrame(image, editable = false)}
<div class="relative w-full max-w-44 sm:max-w-52 overflow-hidden bg-track ring-1 ring-black/5">
<div class="relative w-full max-w-44 overflow-hidden bg-track ring-1 ring-black/5 sm:max-w-52">
{#if image}
<img
src={toDataUrl(image)}
@@ -339,8 +335,12 @@
<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 lg:pb-12"
>
<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="w-full max-w-24 overflow-hidden bg-track shadow-sm ring-1 ring-black/5 sm:max-w-28 lg:max-w-75">
<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="w-full max-w-24 overflow-hidden bg-track shadow-sm ring-1 ring-black/5 sm:max-w-28 lg:max-w-75"
>
{#if loading}
<div class="aspect-[3/4] w-full animate-pulse bg-placeholder"></div>
{:else if imageSrc}
@@ -359,9 +359,7 @@
</section>
<section class="relative flex min-h-0 flex-1 flex-col overflow-hidden pb-44 lg:pb-8">
<div
class="mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-4 px-6 py-5 lg:py-6"
>
<div class="mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-4 px-6 py-5 lg:py-6">
<div class="shrink-0">
<p class="text-xs tracking-[0.2em] text-muted uppercase">Edit bouquet</p>
<h2 class="mt-1 text-lg">Tell us how you want to refine it.</h2>
@@ -430,7 +428,9 @@
</p>
{/if}
<div class="flex w-full items-center gap-2 rounded-full border border-pill bg-surface py-1.5 pr-1.5 pl-5">
<div
class="flex w-full items-center gap-2 rounded-full border border-pill bg-surface py-1.5 pr-1.5 pl-5"
>
<textarea
bind:value={prompt}
rows="1"

View File

@@ -6,7 +6,11 @@
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
import GenerationActivityFeed from '$lib/components/ui/generating/GenerationActivityFeed.svelte';
import { buildRecipe, generateImages } from '$lib/flowerFlow/api.js';
import { createGenerationProgress, DEFAULT_ESTIMATED_MS, MOCK_ESTIMATED_MS } from '$lib/flowerFlow/generationProgress.js';
import {
createGenerationProgress,
DEFAULT_ESTIMATED_MS,
MOCK_ESTIMATED_MS
} from '$lib/flowerFlow/generationProgress.js';
import { createGeneratingArtworkCycle } from '$lib/flowerFlow/generatingArtworkCycle.js';
import {
clearFlow,

View File

@@ -6,9 +6,7 @@
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';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import FlowContinueBar, { FLOW_CONTINUE_BUTTON } from '$lib/components/ui/FlowContinueBar.svelte';
import { skipDevImages } from '$lib/flowerFlow/devSeed.js';
import {
consumeDevMessageSnapshot,
@@ -32,9 +30,7 @@
const artworkVariant = $derived(hasMessage ? 'message1' : 'upload2');
const artworkTitle = $derived(
hasMessage ? 'Your message' : ARTWORK_CARD_DEFAULTS.message.title
);
const artworkTitle = $derived(hasMessage ? 'Your message' : ARTWORK_CARD_DEFAULTS.message.title);
const artworkDescription = $derived(
hasMessage ? message.trim() : ARTWORK_CARD_DEFAULTS.message.description

View File

@@ -5,12 +5,14 @@
import Header from '$lib/components/ui/Header.svelte';
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
import BouquetFlowerCarousel from '$lib/components/ui/result/BouquetFlowerCarousel.svelte';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import FlowContinueBar, { FLOW_CONTINUE_BUTTON } from '$lib/components/ui/FlowContinueBar.svelte';
import { fetchJob, toDataUrl } from '$lib/flowerFlow/api.js';
import { getFlowerImageSrc } from '$lib/flowerFlow/flowerImagePaths.js';
import { resolveRecipeFlowers, buildBouquetRationale, buildBriefBouquetTitle } from '$lib/flowerFlow/resolveRecipeFlowers.js';
import {
resolveRecipeFlowers,
buildBouquetRationale,
buildBriefBouquetTitle
} from '$lib/flowerFlow/resolveRecipeFlowers.js';
import { getFlowString } from '$lib/flowerFlow/session.js';
let loading = $state(true);
@@ -63,7 +65,9 @@
/>
<section class="relative flex min-h-0 flex-1 flex-col pb-[3.75rem] lg:overflow-hidden lg:pb-8">
<div class="flex min-h-0 flex-1 flex-col justify-center overflow-hidden px-6 py-6 lg:px-8 lg:py-8">
<div
class="flex min-h-0 flex-1 flex-col justify-center overflow-hidden px-6 py-6 lg:px-8 lg:py-8"
>
{#if loading}
<p class="text-sm text-muted">Loading result...</p>
{:else if error}

View File

@@ -5,9 +5,7 @@
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';
import FlowContinueBar, {
FLOW_CONTINUE_BUTTON
} from '$lib/components/ui/FlowContinueBar.svelte';
import FlowContinueBar, { FLOW_CONTINUE_BUTTON } from '$lib/components/ui/FlowContinueBar.svelte';
import { analyzeMood } from '$lib/flowerFlow/api.js';
import {
deleteFlowKey,
@@ -61,12 +59,12 @@
},
character: {
title: 'Their character',
description: 'A face, a gesture, a presence. Something in them is starting to take floral form.'
description:
'A face, a gesture, a presence. Something in them is starting to take floral form.'
},
location: {
title: 'A sense of place',
description:
'City grit or quiet coast. Where they belong roots the arrangement in memory.'
description: 'City grit or quiet coast. Where they belong roots the arrangement in memory.'
}
};