diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ff7cdfe --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY= +OPENAI_IMAGE_MODEL=gpt-image-1.5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dd7da2e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + lint-and-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index b4a2eef..9d69e5f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..061f635 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to openlayout + +Thanks for taking the time to improve openlayout. + +## Development setup + +```bash +npm install +npm run dev +``` + +Open `http://localhost:3000/web-layouts`. + +## Before sending changes + +Run both checks: + +```bash +npm run lint +npm run build +``` + +## Adding a web layout + +Layout data lives in `src/data/webLayouts.ts`. + +1. Add a seed to `layoutSeeds`. +2. Reuse an existing `category` and `previewType` when possible. +3. Add a category or preview type only when the current set cannot describe the structure. +4. Update `LayoutPreviewRenderer.tsx` only when a new preview type needs a renderer. + +## Adding a design style + +Design style data lives in `src/data/designStyles.ts`. + +1. Add an entry to `styleSeedTuples`. +2. Add a palette when the generated fallback palette is not specific enough. +3. Reuse an existing `sampleType` when possible. +4. Update `DesignStyleSampleRenderer.tsx` only when a new sample type is needed. + +## Pull request expectations + +- Keep changes focused. +- Include screenshots or short notes for visual changes. +- Avoid adding external services unless the feature cannot work without them. +- Do not commit generated images unless they are intentionally part of the project. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c074ba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 pandaofwild + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 67ca5e0..a6d9f10 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # openlayout -웹사이트 레이아웃을 찾고, 비교하고, 실제 와이어프레임으로 확인하는 Web Layout Library입니다. +웹사이트 레이아웃과 디자인 형식을 분리해서 고르고, 비교하고, 실제 웹페이지형 프리뷰로 확인하는 Web Layout Library입니다. -openlayout은 디자인을 시작하기 전에 페이지 구조를 빠르게 고를 수 있도록 만든 레이아웃 사전입니다. 96개의 레이아웃, 12개의 카테고리, 16개의 프리뷰 템플릿을 제공하며, 각 레이아웃은 추천 용도, 장단점, 반응형 동작, 접근성 체크포인트, Tailwind 구현 힌트를 함께 보여줍니다. +openlayout은 디자인을 시작하기 전에 페이지 구조와 시각 언어를 빠르게 고를 수 있도록 만든 레이아웃/스타일 사전입니다. 96개의 레이아웃, 88개의 디자인 형식, 10개의 스타일 카테고리, 웹페이지형 샘플 렌더러를 제공하며, 각 항목은 추천 용도, 장단점, 반응형 동작, 접근성 체크포인트, 색상표, Tailwind 구현 힌트를 함께 보여줍니다. | 항목 | 값 | | --- | --- | | Repository | https://github.com/pandaofwild/openlayout | -| 마지막 검토일 | 2026-06-01 | +| 마지막 검토일 | 2026-06-03 | ## 대상 사용자 @@ -21,9 +21,13 @@ openlayout은 디자인을 시작하기 전에 페이지 구조를 빠르게 고 - **Full-stage preview**: 상세 페이지와 비교 페이지에서 레이아웃을 실제 웹페이지 배경처럼 크게 확인합니다. - **Floating detail panel**: 기본 화면에는 작은 요약 박스만 두고, 클릭하면 구조 설명과 장단점이 패널로 떠오릅니다. - **Compare view**: 최대 3개의 레이아웃을 선택해 추천 용도, 모바일 대응, 밀도, 난이도를 나란히 비교합니다. +- **Design Style Library**: 88개의 디자인 형식을 카테고리, 태그, 검색어로 탐색하고 상세 페이지에서 색상표와 웹페이지형 샘플을 확인합니다. +- **Style application**: `/design-styles`에서 선택한 디자인 형식이 `/web-layouts`와 `/web-layouts/compare`의 프리뷰 톤에 적용되고 localStorage에 유지됩니다. +- **Prompt palette**: 프롬프트를 섞어 커스텀 색상표를 생성하고 현재 레이아웃 프리뷰에 바로 적용합니다. +- **Image generation admin**: `OPENAI_API_KEY`가 있는 로컬 환경에서 스타일별 참조 이미지를 생성해 `public/generated/design-styles`에 저장할 수 있습니다. - **SVG controls**: 비교 화살표와 설명/닫기/상세 아이콘은 inline SVG로 관리합니다. - **Implementation hints**: previewType별 Tailwind 코드 예시와 구현 팁을 제공합니다. -- **Project skill**: `skills/layout-recommender/SKILL.md`가 목적별 레이아웃 추천 방식을 안내합니다. +- **Project skills**: `skills/layout-recommender/SKILL.md`와 `skills/design-style-recommender/SKILL.md`가 목적별 레이아웃/스타일 추천 방식을 안내합니다. ## 바이브 코딩에 도움되는 점 @@ -31,13 +35,14 @@ openlayout은 디자인을 시작하기 전에 페이지 구조를 빠르게 고 - 각 레이아웃은 추천 용도, 피해야 할 상황, 반응형 동작, 접근성 체크포인트를 함께 갖고 있어 프롬프트에 넣을 설계 조건을 바로 뽑아낼 수 있습니다. - 비교 페이지의 큰 프리뷰와 플로팅 설명 패널을 보면서 "이 구조로 가자", "이건 모바일에서 약하다" 같은 결정을 짧은 피드백으로 반복하기 좋습니다. - `previewType`은 구현 방향의 압축어처럼 쓸 수 있습니다. 예: `hero`, `card-grid`, `dashboard`, `docs`, `comparison`. -- 새 화면을 만들 때는 먼저 이 레이아웃 사전에서 구조를 고르고, 그 다음 색상, 카피, 컴포넌트 디테일을 코딩 에이전트에게 맡기는 흐름이 가장 안정적입니다. +- `DesignStyle`은 시각 방향의 압축어처럼 쓸 수 있습니다. 예: `brutalism`, `cyberpunk`, `luxury`, `organic-design`, `saas-style`. +- 새 화면을 만들 때는 먼저 레이아웃 사전에서 구조를 고르고, 그 다음 디자인 형식에서 색상/타이포/분위기를 고른 뒤, 컴포넌트와 카피를 코딩 에이전트에게 맡기는 흐름이 가장 안정적입니다. ## 빠른 시작 요구 사항: -- Node.js +- Node.js 22 이상 - npm 의존성 설치: @@ -67,6 +72,32 @@ http://localhost:3000/web-layouts | `/web-layouts` | 레이아웃 검색, 필터, 카드 목록 | | `/web-layouts/[slug]` | 구조 설명, 장단점, 반응형 동작, 접근성 노트, 라이브 프리뷰, 코드 예시 | | `/web-layouts/compare` | 최대 3개 레이아웃 비교와 큰 구조 미리보기 | +| `/design-styles` | 디자인 형식 검색, 카테고리/태그 필터, 색상표, 웹페이지형 스타일 샘플 | +| `/design-styles/[slug]` | 디자인 형식 상세 설명, 색상표, 타이포/레이아웃 특징, 관련 스타일 | +| `/design-styles/generate` | OpenAI Image API 기반 로컬 참조 이미지 생성 관리자 | + +## 이미지 생성 환경 변수 + +`/design-styles/generate`와 `/api/design-style-images`는 로컬 관리자 기능입니다. + +```bash +OPENAI_API_KEY=sk-... +OPENAI_IMAGE_MODEL=gpt-image-1.5 +``` + +- `OPENAI_API_KEY`는 필수입니다. +- `OPENAI_IMAGE_MODEL`은 선택값이며 기본값은 `gpt-image-1.5`입니다. +- 생성 결과는 `public/generated/design-styles/{slug}.webp`에 저장됩니다. +- Vercel 같은 읽기 전용 배포 환경에서는 Blob/S3 같은 외부 저장소로 바꾸는 것이 안전합니다. + +## 오픈소스 사용 + +이 프로젝트는 MIT 라이선스로 배포됩니다. 자세한 조건은 `LICENSE`를 확인하세요. + +- 기여 방법: `CONTRIBUTING.md` +- 보안 제보: `SECURITY.md` +- 로컬 환경 변수 예시: `.env.example` +- CI: `.github/workflows/ci.yml` ## 품질 확인 @@ -92,10 +123,18 @@ npm run build src/app/web-layouts/page.tsx # Explorer page src/app/web-layouts/[slug]/page.tsx # Layout detail page src/app/web-layouts/compare/page.tsx # Compare page shell +src/app/design-styles/page.tsx # Design style library page +src/app/design-styles/[slug]/page.tsx # Design style detail page +src/app/design-styles/generate/page.tsx # Local image generation admin +src/app/api/design-style-images/route.ts # OpenAI Image API route src/data/webLayouts.ts # Layout catalog and generated metadata +src/data/designStyles.ts # 88 design styles and generated metadata src/components/web-layout/ # Explorer, cards, previews, compare UI +src/components/design-style/ # Style cards, filters, samples, generator UI +src/components/style-preset/ # Global selected style provider src/components/ui/ # Small shared UI primitives skills/layout-recommender/SKILL.md # Purpose-based layout recommendation skill +skills/design-style-recommender/SKILL.md # Brand-tone-based style recommendation skill ``` 주요 컴포넌트: @@ -111,11 +150,18 @@ skills/layout-recommender/SKILL.md # Purpose-based layout recommendation | `WireframeThumbnail.tsx` | 카드와 비교 화면에 쓰이는 구조 썸네일 | | `LayoutCodeExample.tsx` | 복사 가능한 Tailwind 구현 예시 | | `WebLayoutCompare.tsx` | 비교 페이지 선택, SVG 화살표 탐색, 큰 프리뷰 표시 | +| `DesignStyleLibrary.tsx` | 디자인 형식 목록의 검색, 필터, 적용 상태 | +| `DesignStyleCard.tsx` | 디자인 형식 카드, 색상표, 웹페이지형 샘플, 적용 버튼 | +| `DesignStyleSampleRenderer.tsx` | sampleType별 10개 웹페이지형 스타일 샘플 | +| `StylePresetProvider.tsx` | 선택된 디자인 형식과 커스텀 팔레트 localStorage 유지 | +| `DesignStyleImageGenerator.tsx` | 스타일 참조 이미지 생성 관리자 UI | ## 프로젝트 스킬 `skills/layout-recommender/SKILL.md`는 코딩 에이전트가 사용 목적에 맞는 레이아웃을 추천할 때 읽는 내부 스킬입니다. +`skills/design-style-recommender/SKILL.md`는 브랜드 톤, 업종, 감정, 타이포그래피, 색상 방향에 맞는 디자인 형식을 추천할 때 읽는 내부 스킬입니다. + 추천 요청 예시: ```text @@ -124,6 +170,14 @@ skills/layout-recommender/SKILL.md # Purpose-based layout recommendation 스킬은 `src/data/webLayouts.ts`의 카테고리, `bestFor`, `notGoodFor`, `tags`, `previewType`을 먼저 확인하고 1순위 후보, 대안, 피해야 할 구조를 짧게 제안하도록 설계되어 있습니다. +디자인 스타일 추천 요청 예시: + +```text +프리미엄 뷰티 브랜드 랜딩인데 너무 화려하진 않고 고급스럽게 보여야 해. 어떤 디자인 형식이 좋아? +``` + +디자인 스타일 스킬은 `src/data/designStyles.ts`의 `category`, `tags`, `goodFor`, `useCases`, `palette`, `sampleType`을 기준으로 1순위 스타일, 대안, 피해야 할 스타일을 제안합니다. + ## 레이아웃 추가 레이아웃 데이터는 `src/data/webLayouts.ts`에서 관리합니다. @@ -135,6 +189,16 @@ skills/layout-recommender/SKILL.md # Purpose-based layout recommendation 새 카테고리를 추가할 때는 `categoryGuides`에도 설명과 기본값을 추가해야 합니다. +## 디자인 형식 추가 + +디자인 형식 데이터는 `src/data/designStyles.ts`에서 관리합니다. + +1. `styleSeedTuples`에 새 항목을 추가합니다. +2. `slug`, `nameKo`, `nameEn`, `category`, `tone`, `tags`, `sampleType`을 입력합니다. +3. 필요하면 `palettes`에 해당 slug의 9색 팔레트를 추가합니다. +4. 새 범주가 필요하면 `categoryProfiles`에 시각 특징, 색상 노트, 타이포그래피, 레이아웃 경향을 추가합니다. +5. 기존 10개 샘플 렌더러로 표현이 부족할 때만 `DesignStyleSampleType`과 `DesignStyleSampleRenderer.tsx`를 확장합니다. + ## Preview Type 추가 기존 템플릿으로 구조가 충분히 드러나지 않을 때 새 previewType을 추가합니다. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..54e621f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported versions + +The latest code on the default development branch is supported. + +## Reporting a vulnerability + +Please report security issues through GitHub security advisories if available, or open an issue with a minimal description that does not expose exploit details publicly. + +Include: + +- The affected route, component, or script. +- Steps to reproduce. +- Impact and any known workaround. + +## Secrets and local image generation + +The image generation route uses `OPENAI_API_KEY` only for local administrator workflows. Do not commit real API keys. Use `.env.local` for local secrets and `.env.example` for documented placeholders. diff --git a/docs/superpowers/plans/2026-06-02-design-style-library.md b/docs/superpowers/plans/2026-06-02-design-style-library.md new file mode 100644 index 0000000..c667304 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-design-style-library.md @@ -0,0 +1,1236 @@ +# Design Style Library Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a separate Design Style Library where every style from the pasted prompt has its own searchable card, detail page, palette, usage guidance, image prompt, and actual webpage-like sample. + +**Architecture:** Keep `/web-layouts` focused on layout structure and make `/design-styles` the source of truth for visual formats. Design styles are data-driven TypeScript records, rendered by reusable category, card, detail, and sample components. The selected design style is persisted through the existing client provider and applied to layout previews through CSS variables. + +**Tech Stack:** Next.js App Router, TypeScript, Tailwind CSS v4, local TypeScript data, client-side search/filter state, server-rendered detail pages, optional OpenAI image generation API in a later milestone. + +--- + +## Current Baseline + +- [x] Repository root: `F:\coding\design_pan` +- [x] Existing layout library: `src/app/web-layouts/page.tsx` +- [x] Existing layout detail pages: `src/app/web-layouts/[slug]/page.tsx` +- [x] Existing layout preview renderer: `src/components/web-layout/LayoutPreviewRenderer.tsx` +- [x] Temporary design-style prototype exists in the working tree: + - `src/app/design-styles/page.tsx` + - `src/components/style-preset/StylePresetProvider.tsx` + - `src/components/style-preset/StylePresetPanel.tsx` + - `src/components/style-preset/AppliedStyleStrip.tsx` + - `src/data/stylePresets.ts` + - `src/lib/paletteGenerator.ts` +- [ ] Convert the temporary prototype into a full 88-style design library. +- [ ] Commit only after each milestone passes `npm run lint` and `npm run build`. + +## Product Shape + +### User Flow + +1. User opens `/design-styles`. +2. User searches or filters design styles by category/tag. +3. User opens `/design-styles/[slug]`. +4. User sees a real webpage-like style sample, palette, typography notes, layout traits, use cases, cautions, related styles, and image prompt. +5. User selects a design style. +6. User opens `/web-layouts` or `/web-layouts/compare`. +7. Layout examples are rendered with the selected style. + +### Route Map + +- `/design-styles` + - Full design style library with search, filters, style cards, palette swatches, and sample thumbnails. +- `/design-styles/[slug]` + - Detail page for one style with full explanation and real sample page. +- `/web-layouts` + - Layout structure library. Shows a compact "Applied design format" strip and a link back to `/design-styles`. +- `/web-layouts/compare` + - Existing compare flow, using the selected style variables. +- `/admin/generate-design-images` + - Later milestone. Local development admin page for image generation. +- `/api/generate-design-image` + - Later milestone. Server-only OpenAI image generation route. + +--- + +## Data Model + +### Final Type + +Create `src/data/designStyles.ts` and use this type: + +```ts +export type DesignStylePalette = { + base: string; + surface: string; + text: string; + mutedText: string; + primary: string; + accent: string; + accent2: string; + accent3: string; + border: string; +}; + +export type DesignStyleSampleType = + | "minimal-editorial" + | "brutalist-poster" + | "retro-commerce" + | "cyber-dashboard" + | "luxury-product" + | "organic-brand" + | "kawaii-app" + | "street-campaign" + | "magazine-layout" + | "saas-landing"; + +export type DesignStyle = { + slug: string; + nameKo: string; + nameEn: string; + category: string; + summary: string; + description: string; + visualFeatures: string[]; + colorPalette: string[]; + typography: string[]; + layoutTraits: string[]; + useCases: string[]; + goodFor: string[]; + cautions: string[]; + tags: string[]; + related: string[]; + palette: DesignStylePalette; + imagePrompt: string; + sampleType: DesignStyleSampleType; +}; +``` + +### Required 88 Styles + +#### 모던 / 미니멀 + +- [x] 미니멀리즘 +- [x] 모더니즘 +- [x] 스위스 디자인 +- [x] 인터내셔널 스타일 +- [x] 스칸디나비안 +- [x] 재팬디 +- [x] 웜 미니멀 +- [x] 소프트 미니멀 +- [x] 하이엔드 미니멀 + +#### 강렬 / 실험 + +- [x] 브루탈리즘 +- [x] 뉴 브루탈리즘 +- [x] 안티디자인 +- [x] 맥시멀리즘 +- [x] 글리치 아트 +- [x] 디컨스트럭티비즘 +- [x] 아방가르드 +- [x] 포스트모더니즘 + +#### 레트로 / 빈티지 + +- [x] 레트로 +- [x] 빈티지 +- [x] 70년대 레트로 +- [x] 80년대 레트로 +- [x] 90년대 그래픽 +- [x] Y2K +- [x] 레트로 퓨처리즘 +- [x] 미드센추리 모던 +- [x] 바우하우스 + +#### 미래 / 디지털 + +- [x] 퓨처리즘 +- [x] 사이버펑크 +- [x] 네온 누아르 +- [x] 테크웨어 +- [x] 하이테크 +- [x] AI 에스테틱 +- [x] 홀로그램 스타일 +- [x] 크롬코어 +- [x] 메타버스 스타일 + +#### 럭셔리 / 클래식 + +- [x] 클래식 +- [x] 네오클래식 +- [x] 럭셔리 +- [x] 올드머니 +- [x] 아르데코 +- [x] 아르누보 +- [x] 바로크 +- [x] 로코코 +- [x] 고딕 + +#### 자연 / 수공예 + +- [x] 오가닉 디자인 +- [x] 내추럴 +- [x] 보태니컬 +- [x] 에코 디자인 +- [x] 러스틱 +- [x] 킨포크 +- [x] 핸드메이드 +- [x] 크래프트 +- [x] 와비사비 + +#### 귀여움 / 캐주얼 + +- [x] 키치 +- [x] 카와이 +- [x] 도파민 디자인 +- [x] 팝아트 +- [x] 코믹북 스타일 +- [x] 토이 디자인 +- [x] 플레이풀 디자인 +- [x] 파스텔 스타일 +- [x] 버블 디자인 + +#### 스트리트 / 서브컬처 + +- [x] 스트리트웨어 +- [x] 그래피티 +- [x] 힙합 스타일 +- [x] 스케이트 문화 +- [x] 펑크 +- [x] 그런지 +- [x] 인디 슬리즈 +- [x] 레이브 스타일 +- [x] 로우파이 + +#### 편집 / 타이포그래피 + +- [x] 타이포그래피 중심 +- [x] 에디토리얼 디자인 +- [x] 매거진 스타일 +- [x] 포스터리즘 +- [x] 그리드 시스템 +- [x] 콜라주 +- [x] 포토몽타주 +- [x] 실험 타이포 +- [x] 신문 스타일 + +#### UI / 웹 + +- [x] 플랫 디자인 +- [x] 머티리얼 디자인 +- [x] 뉴모피즘 +- [x] 글래스모피즘 +- [x] 클레이모피즘 +- [x] 다크모드 디자인 +- [x] SaaS 스타일 +- [x] 스타트업 랜딩페이지 + +--- + +## File Structure + +### Data + +- Create: `src/data/designStyles.ts` + - Full `DesignStyle` type. + - Full 88-style `designStyles` array. + - `designStyleCategories`, `designStyleTags`, `getDesignStyleBySlug`, `getRelatedDesignStyles`. +- Retire: `src/data/stylePresets.ts` + - Replace imports with `src/data/designStyles.ts`. + - Delete after all imports are migrated. + +### State And Palette + +- Modify: `src/components/style-preset/StylePresetProvider.tsx` + - Rename context concepts from `StylePreset` to `DesignStyle`. + - Persist selected design style slug and custom prompt palette. + - Continue writing CSS variables on `.style-preset-root`. +- Modify: `src/lib/paletteGenerator.ts` + - Rename imports to use `DesignStyle`. + - Keep deterministic local prompt palette generation. + +### Design Style Pages + +- Modify: `src/app/design-styles/page.tsx` + - Use `DesignStyleLibrary` client component. +- Create: `src/app/design-styles/[slug]/page.tsx` + - Server-render detail page. + - Implement `generateStaticParams`. + - Implement `generateMetadata`. +- Create: `src/components/design-style/DesignStyleLibrary.tsx` + - Client search/filter/list state. +- Create: `src/components/design-style/DesignStyleFilters.tsx` + - Search, category, tag filters. +- Create: `src/components/design-style/DesignStyleCard.tsx` + - Card view for each style. +- Create: `src/components/design-style/DesignStyleSampleRenderer.tsx` + - Render actual webpage-like style sample based on `sampleType`. +- Create: `src/components/design-style/DesignStyleDetailSection.tsx` + - Reusable detail sections. +- Create: `src/components/design-style/ColorPaletteGrid.tsx` + - Palette swatches. +- Create: `src/components/design-style/RelatedDesignStyles.tsx` + - Related style cards. + +### Layout Library Integration + +- Modify: `src/components/style-preset/AppliedStyleStrip.tsx` + - Point to selected `DesignStyle`. + - Link to `/design-styles`. +- Modify: `src/components/web-layout/LayoutPreviewRenderer.tsx` + - Continue using CSS variables. + - No layout data changes required. +- Modify: `src/app/web-layouts/page.tsx` + - Keep layout-first content. + - Keep compact applied design format strip only. + +### Later Image Generation Milestone + +- Create: `src/app/api/generate-design-image/route.ts` +- Create: `src/app/admin/generate-design-images/page.tsx` +- Create: `src/components/design-style/AdminImageGenerator.tsx` +- Create: `src/lib/openai-image.ts` +- Add generated output path: `public/generated/design-styles/{slug}.webp` +- Modify: `README.md` + - Add environment variables and generation workflow. + +--- + +## Milestone Checklist + +- [ ] **M0: Plan Document** + - [x] Create this plan. + - [x] Keep this document updated after each completed task. + +- [x] **M1: Data Foundation** + - [x] Create final `DesignStyle` type. + - [x] Add all 88 styles. + - [x] Add category/tag helpers. + - [x] Add related-style links. + - [x] Replace temporary `stylePresets` imports. + +- [x] **M2: Design Style Listing Page** + - [x] Build search/filter UI. + - [x] Build category navigation. + - [x] Build style cards with palette and sample thumbnail. + - [x] Verify mobile layout. + +- [x] **M3: Design Style Detail Pages** + - [x] Add `/design-styles/[slug]`. + - [x] Add metadata and static params. + - [x] Add detail sections. + - [x] Add related styles. + +- [x] **M4: Webpage-Like Style Samples** + - [x] Implement 10 sample renderers. + - [x] Map all 88 styles to sample renderers. + - [x] Verify at least one style per category visually. + +- [x] **M5: Layout Library Integration** + - [x] Select design style from `/design-styles`. + - [x] Apply selected style to `/web-layouts`. + - [x] Apply selected style to `/web-layouts/compare`. + - [x] Confirm localStorage persistence. + +- [x] **M6: Image Generation** + - [x] Add server API route. + - [x] Add admin page. + - [x] Save generated images under `public/generated/design-styles`. + - [x] Add clear API error handling. + +- [x] **M7: Docs, QA, Release** + - [x] Update README. + - [x] Run `npm run lint`. + - [x] Run `npm run build`. + - [x] Capture visual checks for `/design-styles`, `/design-styles/[slug]`, `/web-layouts`. + - [x] Commit. + - [x] Tag next version. + - [x] Push to GitHub. + +--- + +## Task 1: Data Foundation + +**Files:** +- Create: `src/data/designStyles.ts` +- Modify: `src/components/style-preset/StylePresetProvider.tsx` +- Modify: `src/components/style-preset/StylePresetPanel.tsx` +- Modify: `src/components/style-preset/AppliedStyleStrip.tsx` +- Modify: `src/lib/paletteGenerator.ts` +- Delete after migration: `src/data/stylePresets.ts` + +- [x] **Step 1: Create final data file** + +Create `src/data/designStyles.ts` with the final `DesignStyle` type, helper functions, and the 88-style array. + +Required exports: + +```ts +export type DesignStylePalette = { + base: string; + surface: string; + text: string; + mutedText: string; + primary: string; + accent: string; + accent2: string; + accent3: string; + border: string; +}; + +export type DesignStyleSampleType = + | "minimal-editorial" + | "brutalist-poster" + | "retro-commerce" + | "cyber-dashboard" + | "luxury-product" + | "organic-brand" + | "kawaii-app" + | "street-campaign" + | "magazine-layout" + | "saas-landing"; + +export type DesignStyle = { + slug: string; + nameKo: string; + nameEn: string; + category: string; + summary: string; + description: string; + visualFeatures: string[]; + colorPalette: string[]; + typography: string[]; + layoutTraits: string[]; + useCases: string[]; + goodFor: string[]; + cautions: string[]; + tags: string[]; + related: string[]; + palette: DesignStylePalette; + imagePrompt: string; + sampleType: DesignStyleSampleType; +}; + +export const defaultDesignStyleSlug = "brutalism"; +export const designStyles: DesignStyle[] = []; +export const designStyleCategories = [...new Set(designStyles.map((style) => style.category))]; +export const designStyleTags = [...new Set(designStyles.flatMap((style) => style.tags))].sort(); + +export function getDesignStyleBySlug(slug: string) { + return designStyles.find((style) => style.slug === slug); +} + +export function getRelatedDesignStyles(slugs: string[]) { + return slugs + .map((slug) => getDesignStyleBySlug(slug)) + .filter((style): style is DesignStyle => Boolean(style)); +} +``` + +- [x] **Step 2: Fill all 88 styles** + +Add the 88 styles listed in the "Required 88 Styles" checklist. Each item must have: + +- `summary`: one sentence. +- `description`: 3-5 Korean sentences. +- `visualFeatures`: 4 items. +- `colorPalette`: 4 items. +- `typography`: 3 items. +- `layoutTraits`: 4 items. +- `useCases`: 4 items. +- `goodFor`: 4 items. +- `cautions`: 3 items. +- `tags`: 4-6 items. +- `related`: 3 slugs. +- `palette`: 9 hex colors. +- `imagePrompt`: English prompt ending with `clean composition, high-quality design reference image, no logo, no watermark, suitable for a design encyclopedia card`. +- `sampleType`: one of the 10 renderer types. + +- [x] **Step 3: Verify 88 count** + +Run: + +```powershell +@' +const { designStyles } = require("./.next/server/app/design-styles/page.js"); +console.log(designStyles.length); +'@ | node - +``` + +If this command is awkward because the app build output is not available, use this TypeScript-safe check instead after `npm run build`: + +```powershell +npm run build +``` + +Expected: + +```text +✓ Compiled successfully +``` + +- [x] **Step 4: Replace imports** + +Replace imports from: + +```ts +import { stylePresets } from "@/data/stylePresets"; +``` + +with: + +```ts +import { designStyles } from "@/data/designStyles"; +``` + +Replace `StylePreset` type names with `DesignStyle` type names where the code now handles full design styles. + +- [x] **Step 5: Run lint** + +Run: + +```powershell +npm run lint +``` + +Expected: + +```text +> design_pan@... lint +> eslint +``` + +with exit code `0`. + +--- + +## Task 2: Listing Page + +**Files:** +- Modify: `src/app/design-styles/page.tsx` +- Create: `src/components/design-style/DesignStyleLibrary.tsx` +- Create: `src/components/design-style/DesignStyleFilters.tsx` +- Create: `src/components/design-style/DesignStyleCard.tsx` +- Create: `src/components/design-style/ColorPaletteGrid.tsx` + +- [x] **Step 1: Create `ColorPaletteGrid`** + +`src/components/design-style/ColorPaletteGrid.tsx`: + +```tsx +import type { DesignStylePalette } from "@/data/designStyles"; + +const paletteEntries: Array<[keyof DesignStylePalette, string]> = [ + ["base", "Base"], + ["surface", "Surface"], + ["text", "Text"], + ["primary", "Primary"], + ["accent", "Accent"], + ["accent2", "Accent 2"], + ["accent3", "Accent 3"], + ["border", "Border"], +]; + +export function ColorPaletteGrid({ palette }: { palette: DesignStylePalette }) { + return ( +
+ {paletteEntries.map(([key, label]) => ( + + ))} +
+ ); +} +``` + +- [x] **Step 2: Create card component** + +`src/components/design-style/DesignStyleCard.tsx`: + +```tsx +import Link from "next/link"; +import type { DesignStyle } from "@/data/designStyles"; +import { ColorPaletteGrid } from "@/components/design-style/ColorPaletteGrid"; +import { DesignStyleSampleRenderer } from "@/components/design-style/DesignStyleSampleRenderer"; + +export function DesignStyleCard({ style }: { style: DesignStyle }) { + return ( +
+
+ +
+
+
+

{style.category}

+

+ {style.nameKo} +

+

{style.nameEn}

+

+ {style.summary} +

+
+ +
+ {style.tags.slice(0, 4).map((tag) => ( + + {tag} + + ))} +
+ + 자세히 보기 + +
+
+ ); +} +``` + +- [x] **Step 3: Create filter component** + +`src/components/design-style/DesignStyleFilters.tsx`: + +```tsx +"use client"; + +import { designStyleCategories, designStyleTags } from "@/data/designStyles"; + +type Props = { + category: string; + onCategoryChange: (category: string) => void; + onQueryChange: (query: string) => void; + onReset: () => void; + onTagChange: (tag: string) => void; + query: string; + tag: string; +}; + +export function DesignStyleFilters({ + category, + onCategoryChange, + onQueryChange, + onReset, + onTagChange, + query, + tag, +}: Props) { + return ( +
+
+ + + + +
+
+ ); +} +``` + +- [x] **Step 4: Create library component** + +`src/components/design-style/DesignStyleLibrary.tsx`: + +```tsx +"use client"; + +import { useMemo, useState } from "react"; +import { designStyles } from "@/data/designStyles"; +import { DesignStyleCard } from "@/components/design-style/DesignStyleCard"; +import { DesignStyleFilters } from "@/components/design-style/DesignStyleFilters"; + +export function DesignStyleLibrary() { + const [query, setQuery] = useState(""); + const [category, setCategory] = useState(""); + const [tag, setTag] = useState(""); + + const filteredStyles = useMemo(() => { + const normalizedQuery = query.trim().toLowerCase(); + + return designStyles.filter((style) => { + const matchesQuery = + normalizedQuery.length === 0 || + [ + style.nameKo, + style.nameEn, + style.summary, + style.description, + style.category, + ...style.tags, + ...style.goodFor, + ] + .join(" ") + .toLowerCase() + .includes(normalizedQuery); + const matchesCategory = category === "" || style.category === category; + const matchesTag = tag === "" || style.tags.includes(tag); + + return matchesQuery && matchesCategory && matchesTag; + }); + }, [category, query, tag]); + + function resetFilters() { + setQuery(""); + setCategory(""); + setTag(""); + } + + return ( +
+ +
+

+ {filteredStyles.length}개 디자인 형식 표시 중 +

+
+
+ {filteredStyles.map((style) => ( + + ))} +
+
+ ); +} +``` + +- [x] **Step 5: Wire page** + +Modify `src/app/design-styles/page.tsx` to import and render: + +```tsx +import { DesignStyleLibrary } from "@/components/design-style/DesignStyleLibrary"; + +// inside page body + +``` + +- [x] **Step 6: Verify listing** + +Run: + +```powershell +npm run lint +npm run build +``` + +Expected: both commands exit with code `0`. + +--- + +## Task 3: Detail Pages + +**Files:** +- Create: `src/app/design-styles/[slug]/page.tsx` +- Create: `src/components/design-style/DesignStyleDetailSection.tsx` +- Create: `src/components/design-style/RelatedDesignStyles.tsx` +- Modify: `src/components/design-style/DesignStyleSampleRenderer.tsx` + +- [x] **Step 1: Create detail section component** + +`src/components/design-style/DesignStyleDetailSection.tsx`: + +```tsx +import type { ReactNode } from "react"; + +export function DesignStyleDetailSection({ + children, + title, +}: { + children: ReactNode; + title: string; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} +``` + +- [x] **Step 2: Create related component** + +`src/components/design-style/RelatedDesignStyles.tsx`: + +```tsx +import { getRelatedDesignStyles } from "@/data/designStyles"; +import { DesignStyleCard } from "@/components/design-style/DesignStyleCard"; + +export function RelatedDesignStyles({ slugs }: { slugs: string[] }) { + const styles = getRelatedDesignStyles(slugs); + + if (styles.length === 0) return null; + + return ( +
+

+ Related styles +

+
+ {styles.map((style) => ( + + ))} +
+
+ ); +} +``` + +- [x] **Step 3: Create detail page** + +`src/app/design-styles/[slug]/page.tsx`: + +```tsx +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { designStyles, getDesignStyleBySlug } from "@/data/designStyles"; +import { ColorPaletteGrid } from "@/components/design-style/ColorPaletteGrid"; +import { DesignStyleDetailSection } from "@/components/design-style/DesignStyleDetailSection"; +import { DesignStyleSampleRenderer } from "@/components/design-style/DesignStyleSampleRenderer"; +import { RelatedDesignStyles } from "@/components/design-style/RelatedDesignStyles"; + +export function generateStaticParams() { + return designStyles.map((style) => ({ slug: style.slug })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const style = getDesignStyleBySlug(slug); + + if (!style) { + return { + title: "디자인 형식 없음", + }; + } + + return { + title: style.nameKo, + description: style.summary, + }; +} + +export default async function DesignStyleDetailPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const style = getDesignStyleBySlug(slug); + + if (!style) notFound(); + + return ( +
+
+ + 목록으로 돌아가기 + +
+
+

{style.category}

+

+ {style.nameKo} +

+

{style.nameEn}

+

+ {style.summary} +

+
+ +
+ +
+ +

{style.description}

+
+ + + +
+ +
+ + + + + + +
+ + +
+            {style.imagePrompt}
+          
+
+ +
+ +
+
+
+ ); +} + +function ListBlock({ items, title }: { items: string[]; title: string }) { + return ( + +
    + {items.map((item) => ( +
  • + {item} +
  • + ))} +
+
+ ); +} +``` + +- [x] **Step 4: Verify detail routes** + +Run: + +```powershell +npm run build +``` + +Expected route output includes: + +```text +├ ● /design-styles/[slug] +``` + +--- + +## Task 4: Webpage-Like Sample Renderer + +**Files:** +- Create: `src/components/design-style/DesignStyleSampleRenderer.tsx` + +- [x] **Step 1: Create renderer shell** + +`src/components/design-style/DesignStyleSampleRenderer.tsx`: + +```tsx +import type { DesignStyle } from "@/data/designStyles"; +import { cn } from "@/lib/utils"; + +type Props = { + compact?: boolean; + style: DesignStyle; +}; + +export function DesignStyleSampleRenderer({ compact = false, style }: Props) { + const vars = { + "--sample-base": style.palette.base, + "--sample-surface": style.palette.surface, + "--sample-text": style.palette.text, + "--sample-muted": style.palette.mutedText, + "--sample-primary": style.palette.primary, + "--sample-accent": style.palette.accent, + "--sample-accent-2": style.palette.accent2, + "--sample-accent-3": style.palette.accent3, + "--sample-border": style.palette.border, + } as React.CSSProperties; + + return ( +
+ +
+ ); +} + +function SampleByType({ compact, style }: Props) { + if (style.sampleType === "cyber-dashboard") return ; + if (style.sampleType === "luxury-product") return ; + if (style.sampleType === "organic-brand") return ; + if (style.sampleType === "kawaii-app") return ; + if (style.sampleType === "street-campaign") return ; + if (style.sampleType === "retro-commerce") return ; + if (style.sampleType === "magazine-layout") return ; + if (style.sampleType === "saas-landing") return ; + if (style.sampleType === "minimal-editorial") return ; + return ; +} +``` + +- [x] **Step 2: Add all 10 sample functions** + +Implement these functions in the same file: + +- `MinimalEditorial` +- `BrutalistPoster` +- `RetroCommerce` +- `CyberDashboard` +- `LuxuryProduct` +- `OrganicBrand` +- `KawaiiApp` +- `StreetCampaign` +- `MagazineLayout` +- `SaasLanding` + +Each sample must: + +- Use `style.palette` through CSS variables. +- Show a header. +- Show a large style-specific title. +- Show at least one actual content/product/dashboard/editorial section. +- Avoid generic blank wireframes. +- Fit inside compact card thumbnails. + +- [x] **Step 3: Visual test one style per category** + +Use Chrome screenshot or the in-app browser for these URLs: + +```text +http://127.0.0.1:3001/design-styles/minimalism +http://127.0.0.1:3001/design-styles/brutalism +http://127.0.0.1:3001/design-styles/y2k +http://127.0.0.1:3001/design-styles/cyberpunk +http://127.0.0.1:3001/design-styles/luxury +http://127.0.0.1:3001/design-styles/organic-design +http://127.0.0.1:3001/design-styles/kawaii +http://127.0.0.1:3001/design-styles/streetwear +http://127.0.0.1:3001/design-styles/editorial-design +http://127.0.0.1:3001/design-styles/glassmorphism +``` + +Expected: + +- Each page has a visibly different style. +- No sample is a plain box-only wireframe. +- Text does not overflow at 390px mobile width. + +--- + +## Task 5: Layout Library Integration + +**Files:** +- Modify: `src/components/style-preset/StylePresetProvider.tsx` +- Modify: `src/components/style-preset/AppliedStyleStrip.tsx` +- Modify: `src/app/globals.css` +- Modify: `src/components/web-layout/LayoutPreviewRenderer.tsx` + +- [ ] **Step 1: Use selected `DesignStyle` for CSS variables** + +Provider must write: + +```css +--style-base +--style-surface +--style-text +--style-muted +--style-primary +--style-accent +--style-accent-2 +--style-accent-3 +--style-border +``` + +- [ ] **Step 2: Confirm localStorage persistence** + +Manual test: + +1. Open `/design-styles`. +2. Select `사이버펑크`. +3. Reload the page. +4. Open `/web-layouts`. + +Expected: + +- Applied style strip shows `사이버펑크`. +- Layout preview cards use dark neon colors. + +- [ ] **Step 3: Confirm compare page** + +Manual test: + +1. Open `/web-layouts/compare`. +2. Confirm active preview uses selected style colors. +3. Use left/right arrows. + +Expected: + +- All compared layouts keep the same selected design style. +- Arrows and floating summary still fit on mobile. + +--- + +## Task 6: Image Generation Milestone + +**Files:** +- Create: `src/lib/openai-image.ts` +- Create: `src/app/api/generate-design-image/route.ts` +- Create: `src/app/admin/generate-design-images/page.tsx` +- Create: `src/components/design-style/AdminImageGenerator.tsx` +- Modify: `README.md` + +- [x] **Step 1: Add server-only OpenAI helper** + +`src/lib/openai-image.ts` must: + +- Read `process.env.OPENAI_API_KEY`. +- Read `process.env.OPENAI_IMAGE_MODEL || "gpt-image-2"`. +- Read `process.env.IMAGE_OUTPUT_FORMAT || "webp"`. +- Read `process.env.IMAGE_QUALITY || "medium"`. +- Return a clear error when key is missing. +- Never expose API key to client components. + +- [x] **Step 2: Add API route** + +`src/app/api/generate-design-image/route.ts` must accept: + +```json +{ + "slug": "minimalism", + "force": false +} +``` + +Expected response: + +```json +{ + "ok": true, + "path": "/generated/design-styles/minimalism.webp", + "skipped": false +} +``` + +- [x] **Step 3: Add admin page** + +Admin page buttons: + +- `누락 이미지 생성` +- `선택 이미지 재생성` +- `전체 재생성` + +The page must show: + +- slug +- style name +- current image path +- generation status +- error message + +- [x] **Step 4: README environment section** + +Add: + +```text +OPENAI_API_KEY= +OPENAI_IMAGE_MODEL=gpt-image-2 +IMAGE_OUTPUT_FORMAT=webp +IMAGE_QUALITY=medium +``` + +--- + +## Verification Checklist + +- [x] `npm run lint` passes. +- [x] `npm run build` passes. +- [x] `/design-styles` loads. +- [x] `/design-styles/minimalism` loads. +- [x] `/design-styles/cyberpunk` loads. +- [x] `/design-styles/luxury` loads. +- [x] `/web-layouts` still loads. +- [x] `/web-layouts/compare` still loads. +- [x] Mobile width 390px has no horizontal page overflow. +- [x] Design style cards look like real style samples, not plain wireframes. +- [ ] Layout preview cards still look like real webpages. +- [x] Search filters update instantly. +- [x] Category filters update instantly. +- [x] Selected style persists after reload. +- [x] README explains how to add a new design style. +- [x] README explains image generation environment variables. + +--- + +## Progress Log + +- [x] 2026-06-02: User clarified the goal is all styles from the pasted prompt, each as part of a Design Style Library. +- [x] 2026-06-02: Created this implementation plan. +- [x] M1 complete. +- [x] 2026-06-02: M1 data foundation complete: `designStyles` has 88 unique styles, `npm run lint` passed, and `npm run build` passed. +- [x] M2 complete. +- [x] 2026-06-02: M2 listing page complete: added filters, library cards, palette grid, 10 style sample renderers, `npm run lint` passed, and `npm run build` passed. +- [x] M3 complete. +- [x] 2026-06-02: M3 detail pages complete: `/design-styles/[slug]` statically builds 88 routes, key detail URLs return 200, mobile viewport is `width=device-width`, and 390px `scrollWidth` equals `clientWidth`. +- [x] M4 complete. +- [x] 2026-06-02: M4 sample verification complete: captured a 10-style contact sheet for one representative per category under `.next/codex-screens/style-categories/category-contact-sheet.png`. +- [x] M5 complete. +- [x] 2026-06-02: M5 integration complete: applying `cyberpunk` from `/design-styles` stores `selectedSlug`, `/web-layouts` and `/web-layouts/compare` both hydrate to `data-style-preset="cyberpunk"` with accent `#00E5FF`. +- [x] M6 complete. +- [x] 2026-06-02: M6 image generation complete: added `/api/design-style-images`, `/design-styles/generate`, local save path, and verified missing `OPENAI_API_KEY` returns 503 with a clear error. +- [x] M7 complete. +- [x] 2026-06-02: M7 release complete: README and project skill updated, `npm run lint` and `npm run build` pass on `0.1.4`, visual checks captured, and release is prepared for GitHub tag `v0.1.4`. diff --git a/package-lock.json b/package-lock.json index c3a72e9..251bf54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "name": "design_pan", - "version": "0.1.3", + "name": "openlayout", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "design_pan", - "version": "0.1.3", + "name": "openlayout", + "version": "0.1.4", + "license": "MIT", "dependencies": { - "next": "16.2.6", + "next": "16.2.7", "react": "19.2.4", "react-dom": "19.2.4" }, @@ -1055,9 +1056,9 @@ } }, "node_modules/@next/env": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", - "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.7.tgz", + "integrity": "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1071,9 +1072,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", - "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.7.tgz", + "integrity": "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==", "cpu": [ "arm64" ], @@ -1087,9 +1088,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", - "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.7.tgz", + "integrity": "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==", "cpu": [ "x64" ], @@ -1103,12 +1104,15 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", - "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.7.tgz", + "integrity": "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1119,12 +1123,15 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", - "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.7.tgz", + "integrity": "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1135,12 +1142,15 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", - "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.7.tgz", + "integrity": "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1151,12 +1161,15 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", - "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.7.tgz", + "integrity": "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1167,9 +1180,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", - "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.7.tgz", + "integrity": "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==", "cpu": [ "arm64" ], @@ -1183,9 +1196,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", - "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.7.tgz", + "integrity": "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==", "cpu": [ "x64" ], @@ -5034,12 +5047,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", - "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz", + "integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==", "license": "MIT", "dependencies": { - "@next/env": "16.2.6", + "@next/env": "16.2.7", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -5053,14 +5066,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.6", - "@next/swc-darwin-x64": "16.2.6", - "@next/swc-linux-arm64-gnu": "16.2.6", - "@next/swc-linux-arm64-musl": "16.2.6", - "@next/swc-linux-x64-gnu": "16.2.6", - "@next/swc-linux-x64-musl": "16.2.6", - "@next/swc-win32-arm64-msvc": "16.2.6", - "@next/swc-win32-x64-msvc": "16.2.6", + "@next/swc-darwin-arm64": "16.2.7", + "@next/swc-darwin-x64": "16.2.7", + "@next/swc-linux-arm64-gnu": "16.2.7", + "@next/swc-linux-arm64-musl": "16.2.7", + "@next/swc-linux-x64-gnu": "16.2.7", + "@next/swc-linux-x64-musl": "16.2.7", + "@next/swc-win32-arm64-msvc": "16.2.7", + "@next/swc-win32-x64-msvc": "16.2.7", "sharp": "^0.34.5" }, "peerDependencies": { @@ -5086,34 +5099,6 @@ } } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -5407,7 +5392,6 @@ "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index 7b1b4ec..165c15d 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,34 @@ { - "name": "design_pan", - "version": "0.1.3", - "private": true, + "name": "openlayout", + "version": "0.1.4", + "private": false, + "description": "A web layout and design style library for choosing, comparing, and previewing website structures.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/pandaofwild/openlayout.git" + }, + "bugs": { + "url": "https://github.com/pandaofwild/openlayout/issues" + }, + "homepage": "https://github.com/pandaofwild/openlayout#readme", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint" }, + "engines": { + "node": ">=22" + }, "dependencies": { - "next": "16.2.6", + "next": "16.2.7", "react": "19.2.4", "react-dom": "19.2.4" }, + "overrides": { + "postcss": "8.5.15" + }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", diff --git a/skills/design-style-recommender/SKILL.md b/skills/design-style-recommender/SKILL.md new file mode 100644 index 0000000..e00baae --- /dev/null +++ b/skills/design-style-recommender/SKILL.md @@ -0,0 +1,64 @@ +--- +name: design-style-recommender +description: Use when recommending openlayout design styles by brand tone, audience, industry, visual intensity, typography needs, palette direction, or when adding/refining design style catalog entries in this project. +--- + +# Design Style Recommender + +Use this skill inside openlayout when a user needs a visual style direction before applying it to a layout. + +## Source of Truth + +- Design style data lives in `src/data/designStyles.ts`. +- Prefer existing `designStyles` entries before inventing a new style. +- Match by `category`, `summary`, `description`, `tags`, `goodFor`, `useCases`, `palette`, and `sampleType`. +- When recommending a concrete item, include its route: `/design-styles/{slug}`. + +## Recommendation Workflow + +1. Extract brand tone, audience, industry, content density, desired emotion, typography needs, palette direction, and risk tolerance. +2. Search `src/data/designStyles.ts` for matching Korean/English terms in `nameKo`, `nameEn`, `category`, `tags`, `goodFor`, and `useCases`. +3. Pick one primary style and 2-3 alternatives. +4. Include one style to avoid when contrast, credibility, accessibility, or implementation risk is obvious. +5. Explain each choice in terms of visual impression, palette, typography, layout behavior, and where it fits best. + +Ask a short clarification only when the brand or page goal is unclear. Otherwise infer conservatively and move. + +## Quick Heuristics + +- Quiet premium, portfolio, architecture, editorial brand: start with `minimalism`, `high-end-minimal`, `luxury`, or `old-money`. +- Bold campaign, fashion, event, art direction: start with `brutalism`, `maximalism`, `posterism`, or `streetwear`. +- Tech, AI, gaming, developer tools: start with `cyberpunk`, `high-tech`, `ai-aesthetic`, or `dark-mode-design`. +- Wellness, craft, local, sustainability: start with `organic-design`, `botanical`, `wabi-sabi`, or `kinfolk`. +- Youthful, casual, app, community, goods: start with `kawaii`, `dopamine-design`, `toy-design`, or `playful-design`. +- Content, report, magazine, archive: start with `editorial-design`, `magazine-style`, `grid-system`, or `typography-focused`. +- Product UI, SaaS, startup landing: start with `saas-style`, `flat-design`, `material-design`, or `glassmorphism`. + +## Output Shape + +Use this format unless the user asks otherwise: + +```markdown +추천: [style.nameKo] (`/design-styles/{slug}`) + +왜 맞는지: +- ... + +대안: +- [name] - ... +- [name] - ... + +피하면 좋은 스타일: +- [name or category] - ... + +프롬프트에 넣을 디자인 조건: +- ... +``` + +## When Editing the Catalog + +- Add new entries in `styleSeedTuples`. +- Add a new category only when existing categories cannot explain the visual family. +- Add a palette override in `palettes` when the generated palette bank is not distinctive enough. +- Add a new `sampleType` only when no existing renderer communicates the style as a webpage. +- After code or data edits, run `npm run lint` and `npm run build`. diff --git a/src/app/api/design-style-images/route.ts b/src/app/api/design-style-images/route.ts new file mode 100644 index 0000000..02e639e --- /dev/null +++ b/src/app/api/design-style-images/route.ts @@ -0,0 +1,118 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { NextResponse } from "next/server"; +import { getDesignStyleBySlug } from "@/data/designStyles"; + +export const runtime = "nodejs"; + +type GenerateImageRequest = { + prompt?: string; + quality?: string; + size?: string; + slug?: string; +}; + +type OpenAIImageResponse = { + data?: Array<{ + b64_json?: string; + revised_prompt?: string; + url?: string; + }>; + error?: { + message?: string; + }; +}; + +const allowedQualities = new Set(["auto", "low", "medium", "high"]); +const allowedSizes = new Set(["auto", "1024x1024", "1536x1024", "1024x1536"]); + +function jsonError(message: string, status: number) { + return NextResponse.json({ error: message }, { status }); +} + +function cleanText(input: unknown) { + return typeof input === "string" ? input.trim() : ""; +} + +export async function POST(request: Request) { + if (process.env.NODE_ENV !== "development") { + return jsonError("Image generation is only available in local development.", 404); + } + + const apiKey = process.env.OPENAI_API_KEY; + + if (!apiKey) { + return jsonError("OPENAI_API_KEY is required to generate design style images.", 503); + } + + let body: GenerateImageRequest; + + try { + body = (await request.json()) as GenerateImageRequest; + } catch { + return jsonError("Request body must be valid JSON.", 400); + } + + const slug = cleanText(body.slug); + const style = getDesignStyleBySlug(slug); + + if (!style) { + return jsonError("Unknown design style slug.", 404); + } + + const quality = allowedQualities.has(cleanText(body.quality)) ? cleanText(body.quality) : "medium"; + const size = allowedSizes.has(cleanText(body.size)) ? cleanText(body.size) : "1024x1024"; + const customPrompt = cleanText(body.prompt); + const prompt = [style.imagePrompt, customPrompt].filter(Boolean).join(". ").slice(0, 32000); + const model = process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5"; + const outputFormat = "webp"; + + const openaiResponse = await fetch("https://api.openai.com/v1/images/generations", { + body: JSON.stringify({ + background: "opaque", + model, + output_format: outputFormat, + prompt, + quality, + size, + }), + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + method: "POST", + }); + const result = (await openaiResponse.json()) as OpenAIImageResponse; + + if (!openaiResponse.ok) { + return jsonError(result.error?.message ?? "OpenAI image generation failed.", openaiResponse.status); + } + + const image = result.data?.[0]; + + if (!image?.b64_json && !image?.url) { + return jsonError("OpenAI response did not include image data.", 502); + } + + const bytes = image.b64_json + ? Buffer.from(image.b64_json, "base64") + : Buffer.from(await (await fetch(image.url as string)).arrayBuffer()); + const outputDir = path.join(process.cwd(), "public", "generated", "design-styles"); + const fileName = `${style.slug}.${outputFormat}`; + const filePath = path.join(outputDir, fileName); + + await mkdir(outputDir, { recursive: true }); + await writeFile(filePath, bytes); + + return NextResponse.json({ + model, + path: `/generated/design-styles/${fileName}`, + prompt, + revisedPrompt: image.revised_prompt ?? null, + style: { + nameEn: style.nameEn, + nameKo: style.nameKo, + slug: style.slug, + }, + }); +} diff --git a/src/app/design-styles/[slug]/page.tsx b/src/app/design-styles/[slug]/page.tsx new file mode 100644 index 0000000..42f9b2b --- /dev/null +++ b/src/app/design-styles/[slug]/page.tsx @@ -0,0 +1,123 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { designStyles, getDesignStyleBySlug } from "@/data/designStyles"; +import { ColorPaletteGrid } from "@/components/design-style/ColorPaletteGrid"; +import { DesignStyleDetailSection } from "@/components/design-style/DesignStyleDetailSection"; +import { DesignStyleSampleRenderer } from "@/components/design-style/DesignStyleSampleRenderer"; +import { RelatedDesignStyles } from "@/components/design-style/RelatedDesignStyles"; + +export function generateStaticParams() { + return designStyles.map((style) => ({ slug: style.slug })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const style = getDesignStyleBySlug(slug); + + if (!style) { + return { + title: "디자인 형식 없음", + }; + } + + return { + description: style.summary, + title: `${style.nameKo} | Design Style Library`, + }; +} + +export default async function DesignStyleDetailPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const style = getDesignStyleBySlug(slug); + + if (!style) { + notFound(); + } + + return ( +
+
+ + 목록으로 돌아가기 + + +
+
+

{style.category}

+

+ {style.nameKo} +

+

{style.nameEn}

+

+ {style.summary} +

+
+ {style.tags.slice(0, 6).map((tag) => ( + + {tag} + + ))} +
+
+ +
+ +
+ +

{style.description}

+
+ + + +
+ +
+ + + + + + +
+ +
+ +
+              {style.imagePrompt}
+            
+
+
+ +
+ +
+
+
+ ); +} + +function ListBlock({ items, title }: { items: string[]; title: string }) { + return ( + +
    + {items.map((item) => ( +
  • + {item} +
  • + ))} +
+
+ ); +} diff --git a/src/app/design-styles/generate/page.tsx b/src/app/design-styles/generate/page.tsx new file mode 100644 index 0000000..1cb4273 --- /dev/null +++ b/src/app/design-styles/generate/page.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { DesignStyleImageGenerator } from "@/components/design-style/DesignStyleImageGenerator"; + +export const metadata: Metadata = { + description: "디자인 형식별 참조 이미지를 OpenAI Image API로 생성하는 로컬 관리자 화면", + title: "Design Style Image Generator", +}; + +export default function DesignStyleGeneratePage() { + return ( +
+
+ + 디자인 형식 목록 + +
+

Local admin

+

+ Generate +
+ Style Images +

+

+ `OPENAI_API_KEY`가 설정된 로컬 환경에서 스타일별 참조 이미지를 생성하고 + `public/generated/design-styles`에 저장합니다. +

+
+
+ +
+
+
+ ); +} diff --git a/src/app/design-styles/page.tsx b/src/app/design-styles/page.tsx new file mode 100644 index 0000000..72196f6 --- /dev/null +++ b/src/app/design-styles/page.tsx @@ -0,0 +1,69 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { designStyleCategories, designStyles } from "@/data/designStyles"; +import { DesignStyleLibrary } from "@/components/design-style/DesignStyleLibrary"; + +export const metadata: Metadata = { + title: "Design Style Library", + description: "레이아웃 예시에 적용할 수 있는 디자인 형식과 색상표 프리셋", +}; + +const categories = designStyleCategories; + +export default function DesignStylesPage() { + return ( +
+
+
+
+

+ {designStyles.length} formats / {categories.length} categories +

+

+ Design +
+ Styles +

+
+
+

+ 디자인 형식은 레이아웃 구조와 분리해 고릅니다. 여기서 시각 언어와 + 색상표를 정하면 레이아웃 사전의 모든 예시 화면에 같은 톤이 적용됩니다. +

+
+ + 레이아웃으로 돌아가기 + + + 비교에서 보기 + +
+
+
+
+ +
+
+ {categories.map((category, index) => ( +
+

+ 0{index + 1} +

+

{category}

+
+ ))} +
+
+ +
+ +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 727bbf4..9c29d2a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -393,6 +393,94 @@ main header { background-color: rgb(255 137 169 / 0.72) !important; } +.style-preset-root .raw-preview-canvas, +.style-preset-root .raw-wireframe { + background: + radial-gradient(circle at 18% 18%, rgb(var(--style-accent-rgb) / 0.26), transparent 36%), + radial-gradient(circle at 82% 28%, rgb(var(--style-accent-2-rgb) / 0.22), transparent 38%), + radial-gradient(circle at 48% 92%, rgb(var(--style-accent-3-rgb) / 0.2), transparent 34%), + var(--style-base) !important; + color: var(--style-text) !important; +} + +.style-preset-root .raw-preview-canvas::before, +.style-preset-root .raw-wireframe::before { + background: rgb(var(--style-accent-rgb) / 0.34) !important; +} + +.style-preset-root .raw-preview-canvas::after, +.style-preset-root .raw-wireframe::after { + background: rgb(var(--style-accent-2-rgb) / 0.32) !important; +} + +.style-preset-root .raw-preview-canvas :where(section, header, article, aside, div, button, pre), +.style-preset-root .raw-wireframe :where(section, header, article, aside, div, button, pre) { + border-color: rgb(var(--style-border-rgb) / 0.2) !important; +} + +.style-preset-root .raw-preview-canvas [class*="bg-[#E4E2DD"], +.style-preset-root .raw-preview-canvas [class*="bg-[#F0EEE8"], +.style-preset-root .raw-preview-canvas [class*="bg-[#D9D6D0"], +.style-preset-root .raw-wireframe [class*="bg-[#E4E2DD"], +.style-preset-root .raw-wireframe [class*="bg-[#F0EEE8"], +.style-preset-root .raw-wireframe [class*="bg-[#D9D6D0"] { + background-color: rgb(var(--style-surface-rgb) / 0.82) !important; +} + +.style-preset-root .raw-preview-canvas [class*="bg-[#1E1E1E"], +.style-preset-root .raw-wireframe [class*="bg-[#1E1E1E"] { + background-color: var(--style-primary) !important; +} + +.style-preset-root .raw-preview-canvas [class*="bg-[#DB4A2B"], +.style-preset-root .raw-wireframe [class*="bg-[#DB4A2B"] { + background-color: rgb(var(--style-accent-rgb) / 0.74) !important; +} + +.style-preset-root .raw-preview-canvas [class*="bg-[#F8A348"], +.style-preset-root .raw-wireframe [class*="bg-[#F8A348"] { + background-color: rgb(var(--style-accent-2-rgb) / 0.72) !important; +} + +.style-preset-root .raw-preview-canvas [class*="bg-[#FF89A9"], +.style-preset-root .raw-wireframe [class*="bg-[#FF89A9"] { + background-color: rgb(var(--style-accent-3-rgb) / 0.72) !important; +} + +.style-preset-root .raw-preview-canvas [class*="text-[#1E1E1E"], +.style-preset-root .raw-preview-canvas [class*="text-zinc-9"], +.style-preset-root .raw-preview-canvas [class*="text-zinc-8"], +.style-preset-root .raw-wireframe [class*="text-[#1E1E1E"], +.style-preset-root .raw-wireframe [class*="text-zinc-9"], +.style-preset-root .raw-wireframe [class*="text-zinc-8"] { + color: var(--style-text) !important; +} + +.style-preset-root .raw-preview-canvas [class*="text-[#DB4A2B"], +.style-preset-root .raw-preview-canvas [class*="text-emerald"], +.style-preset-root .raw-preview-canvas [class*="text-rose"], +.style-preset-root .raw-preview-canvas [class*="text-sky"], +.style-preset-root .raw-wireframe [class*="text-[#DB4A2B"], +.style-preset-root .raw-wireframe [class*="text-emerald"], +.style-preset-root .raw-wireframe [class*="text-rose"], +.style-preset-root .raw-wireframe [class*="text-sky"] { + color: var(--style-accent) !important; +} + +.style-preset-root .raw-preview-canvas [class*="text-[#E4E2DD"], +.style-preset-root .raw-preview-canvas [class*="text-white"], +.style-preset-root .raw-wireframe [class*="text-[#E4E2DD"], +.style-preset-root .raw-wireframe [class*="text-white"] { + color: var(--style-surface) !important; +} + +.style-preset-root .raw-preview-canvas [class*="border-[#1E1E1E"], +.style-preset-root .raw-preview-canvas [class*="border-zinc"], +.style-preset-root .raw-wireframe [class*="border-[#1E1E1E"], +.style-preset-root .raw-wireframe [class*="border-zinc"] { + border-color: rgb(var(--style-border-rgb) / 0.22) !important; +} + @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b84220d..01ea5fa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import Link from "next/link"; +import { StylePresetProvider } from "@/components/style-preset/StylePresetProvider"; import "./globals.css"; export const metadata: Metadata = { @@ -10,6 +11,11 @@ export const metadata: Metadata = { description: "웹사이트 설계에 활용할 수 있는 레이아웃 구조 사전", }; +export const viewport: Viewport = { + initialScale: 1, + width: "device-width", +}; + export default function RootLayout({ children, }: Readonly<{ @@ -18,9 +24,11 @@ export default function RootLayout({ return ( - - {children} - + + + {children} + + ); @@ -42,6 +50,7 @@ function RawNavigation() {
{[ ["Library", "/web-layouts"], + ["Styles", "/design-styles"], ["Compare", "/web-layouts/compare"], ["Skill", "/web-layouts#layout-skill"], ].map(([label, href]) => ( @@ -99,6 +108,7 @@ function RawFooter() {
+
+ +
diff --git a/src/components/design-style/ColorPaletteGrid.tsx b/src/components/design-style/ColorPaletteGrid.tsx new file mode 100644 index 0000000..cf68ac3 --- /dev/null +++ b/src/components/design-style/ColorPaletteGrid.tsx @@ -0,0 +1,38 @@ +import type { DesignStylePalette } from "@/data/designStyles"; +import { cn } from "@/lib/utils"; + +const paletteEntries: Array<[keyof DesignStylePalette, string]> = [ + ["base", "Base"], + ["surface", "Surface"], + ["text", "Text"], + ["primary", "Primary"], + ["accent", "Accent"], + ["accent2", "Accent 2"], + ["accent3", "Accent 3"], + ["border", "Border"], +]; + +export function ColorPaletteGrid({ + compact = false, + palette, +}: { + compact?: boolean; + palette: DesignStylePalette; +}) { + return ( +
+ {paletteEntries.map(([key, label]) => ( + + ))} +
+ ); +} diff --git a/src/components/design-style/DesignStyleCard.tsx b/src/components/design-style/DesignStyleCard.tsx new file mode 100644 index 0000000..82b91df --- /dev/null +++ b/src/components/design-style/DesignStyleCard.tsx @@ -0,0 +1,95 @@ +"use client"; + +import Link from "next/link"; +import type { DesignStyle } from "@/data/designStyles"; +import { cn } from "@/lib/utils"; +import { ColorPaletteGrid } from "@/components/design-style/ColorPaletteGrid"; +import { DesignStyleSampleRenderer } from "@/components/design-style/DesignStyleSampleRenderer"; + +type Props = { + isSelected: boolean; + onSelect: (slug: string) => void; + style: DesignStyle; +}; + +export function DesignStyleCard({ isSelected, onSelect, style }: Props) { + return ( +
+
+ +
+
+
+

+ {style.category} +

+

+ {style.nameKo} +

+

+ {style.nameEn} +

+

+ {style.summary} +

+
+ + + +
+ {style.tags.slice(0, 4).map((tag) => ( + + {tag} + + ))} +
+ +
+ + + 자세히 + +
+
+
+ ); +} diff --git a/src/components/design-style/DesignStyleDetailSection.tsx b/src/components/design-style/DesignStyleDetailSection.tsx new file mode 100644 index 0000000..bed1a7f --- /dev/null +++ b/src/components/design-style/DesignStyleDetailSection.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from "react"; + +export function DesignStyleDetailSection({ + children, + title, +}: { + children: ReactNode; + title: string; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} diff --git a/src/components/design-style/DesignStyleFilters.tsx b/src/components/design-style/DesignStyleFilters.tsx new file mode 100644 index 0000000..43170ff --- /dev/null +++ b/src/components/design-style/DesignStyleFilters.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { designStyleCategories, designStyleTags } from "@/data/designStyles"; + +type Props = { + category: string; + onCategoryChange: (category: string) => void; + onQueryChange: (query: string) => void; + onReset: () => void; + onTagChange: (tag: string) => void; + query: string; + tag: string; +}; + +export function DesignStyleFilters({ + category, + onCategoryChange, + onQueryChange, + onReset, + onTagChange, + query, + tag, +}: Props) { + return ( +
+
+ + + + +
+
+ ); +} diff --git a/src/components/design-style/DesignStyleImageGenerator.tsx b/src/components/design-style/DesignStyleImageGenerator.tsx new file mode 100644 index 0000000..0b32716 --- /dev/null +++ b/src/components/design-style/DesignStyleImageGenerator.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useMemo, useState } from "react"; +import Image from "next/image"; +import { designStyles } from "@/data/designStyles"; + +type GenerationResult = { + cacheBust?: number; + error?: string; + model?: string; + path?: string; + prompt?: string; + revisedPrompt?: string | null; + style?: { + nameEn: string; + nameKo: string; + slug: string; + }; +}; + +export function DesignStyleImageGenerator() { + const [slug, setSlug] = useState("minimalism"); + const [prompt, setPrompt] = useState(""); + const [quality, setQuality] = useState("medium"); + const [size, setSize] = useState("1024x1024"); + const [isGenerating, setIsGenerating] = useState(false); + const [result, setResult] = useState(null); + const selectedStyle = useMemo( + () => designStyles.find((style) => style.slug === slug) ?? designStyles[0], + [slug], + ); + + async function generateImage() { + setIsGenerating(true); + setResult(null); + + try { + const response = await fetch("/api/design-style-images", { + body: JSON.stringify({ prompt, quality, size, slug }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + const payload = (await response.json()) as GenerationResult; + + setResult( + response.ok + ? { ...payload, cacheBust: Date.now() } + : { error: payload.error ?? "이미지 생성에 실패했습니다." }, + ); + } catch (error) { + setResult({ error: error instanceof Error ? error.message : "이미지 생성에 실패했습니다." }); + } finally { + setIsGenerating(false); + } + } + + return ( +
+
+
+ + +
+ + +
+ +