diff --git a/.gitignore b/.gitignore index 9d69e5f..0d41d46 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,10 @@ AGENTS.md CLAUDE.md +# local reference screenshots (copyright — keep local only) +/public/references/ +/.playwright-mcp/ + # debug npm-debug.log* yarn-debug.log* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 061f635..65068be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ npm install npm run dev ``` -Open `http://localhost:3000/web-layouts`. +Open `http://localhost:3000/layouts`. ## Before sending changes diff --git a/README.en.md b/README.en.md index 567dbcd..006c62b 100644 --- a/README.en.md +++ b/README.en.md @@ -24,7 +24,7 @@ openlayout is a layout/style dictionary that helps you quickly pick page structu - **Floating detail panel**: The base screen shows only a small summary box; clicking floats up a panel with structure description and pros/cons. - **Compare view**: Select up to 3 layouts to compare recommended use, mobile support, density, and difficulty side by side. - **Design Style Library**: Explore 88 design styles by category, tag, and search term, and check color palettes and webpage-style samples on detail pages. -- **Style application**: A design style chosen in `/design-styles` is applied to the preview tone of `/web-layouts` and `/web-layouts/compare`, and persisted in localStorage. +- **Style application**: A design style chosen in `/styles` is applied to the preview tone of `/layouts` and `/layouts/compare`, and persisted in localStorage. - **Prompt palette**: Mix prompts to generate a custom color palette and apply it directly to the current layout preview. - **Image generation admin**: In a local environment with `OPENAI_API_KEY`, generate per-style reference images and save them to `public/generated/design-styles`. - **SVG controls**: Comparison arrows and the info/close/detail icons are managed as inline SVG. @@ -62,25 +62,25 @@ npm run dev Open in your browser: ```text -http://localhost:3000/web-layouts +http://localhost:3000/layouts ``` -The root path (`/`) redirects to `/web-layouts`. +The root path (`/`) redirects to `/layouts`. ## Main routes | Route | Contents | | --- | --- | -| `/web-layouts` | Layout search, filters, and card list | -| `/web-layouts/[slug]` | Structure description, pros/cons, responsive behavior, accessibility notes, live preview, code example | -| `/web-layouts/compare` | Compare up to 3 layouts with large structure previews | -| `/design-styles` | Design style search, category/tag filters, color palettes, webpage-style samples | -| `/design-styles/[slug]` | Design style detail, color palette, typography/layout traits, related styles | -| `/design-styles/generate` | Local reference image generation admin powered by the OpenAI Image API | +| `/layouts` | Layout search, filters, and card list | +| `/layouts/[slug]` | Structure description, pros/cons, responsive behavior, accessibility notes, live preview, code example | +| `/layouts/compare` | Compare up to 3 layouts with large structure previews | +| `/styles` | Design style search, category/tag filters, color palettes, webpage-style samples | +| `/styles/[slug]` | Design style detail, color palette, typography/layout traits, related styles | +| `/styles/generate` | Local reference image generation admin powered by the OpenAI Image API | ## Image generation environment variables -`/design-styles/generate` and `/api/design-style-images` are local admin features. +`/styles/generate` and `/api/design-style-images` are local admin features. ```bash OPENAI_API_KEY=sk-... @@ -122,12 +122,12 @@ The layout catalog is static-data driven. No separate database or external API i ## Project structure ```text -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/layouts/page.tsx # Explorer page +src/app/layouts/[slug]/page.tsx # Layout detail page +src/app/layouts/compare/page.tsx # Compare page shell +src/app/styles/page.tsx # Design style library page +src/app/styles/[slug]/page.tsx # Design style detail page +src/app/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 diff --git a/README.md b/README.md index c9bec05..8eac7a0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ openlayout은 디자인을 시작하기 전에 페이지 구조와 시각 언어 - **Floating detail panel**: 기본 화면에는 작은 요약 박스만 두고, 클릭하면 구조 설명과 장단점이 패널로 떠오릅니다. - **Compare view**: 최대 3개의 레이아웃을 선택해 추천 용도, 모바일 대응, 밀도, 난이도를 나란히 비교합니다. - **Design Style Library**: 88개의 디자인 형식을 카테고리, 태그, 검색어로 탐색하고 상세 페이지에서 색상표와 웹페이지형 샘플을 확인합니다. -- **Style application**: `/design-styles`에서 선택한 디자인 형식이 `/web-layouts`와 `/web-layouts/compare`의 프리뷰 톤에 적용되고 localStorage에 유지됩니다. +- **Style application**: `/styles`에서 선택한 디자인 형식이 `/layouts`와 `/layouts/compare`의 프리뷰 톤에 적용되고 localStorage에 유지됩니다. +- **Studio copy**: `/studio`에서 선택한 Style x Layout 조합의 프롬프트와 self-contained HTML/CSS 코드를 복사할 수 있습니다. +- **Component dictionary**: `/components`에서 같은 스타일 토큰이 주요 UI 컴포넌트에 어떻게 적용되는지 확인할 수 있습니다. - **Prompt palette**: 프롬프트를 섞어 커스텀 색상표를 생성하고 현재 레이아웃 프리뷰에 바로 적용합니다. - **Image generation admin**: `OPENAI_API_KEY`가 있는 로컬 환경에서 스타일별 참조 이미지를 생성해 `public/generated/design-styles`에 저장할 수 있습니다. - **SVG controls**: 비교 화살표와 설명/닫기/상세 아이콘은 inline SVG로 관리합니다. @@ -62,25 +64,27 @@ npm run dev 브라우저에서 열기: ```text -http://localhost:3000/web-layouts +http://localhost:3000/layouts ``` -루트 경로(`/`)는 `/web-layouts`로 리다이렉트됩니다. +루트 경로(`/`)는 `/layouts`로 리다이렉트됩니다. ## 주요 라우트 | Route | 내용 | | --- | --- | -| `/web-layouts` | 레이아웃 검색, 필터, 카드 목록 | -| `/web-layouts/[slug]` | 구조 설명, 장단점, 반응형 동작, 접근성 노트, 라이브 프리뷰, 코드 예시 | -| `/web-layouts/compare` | 최대 3개 레이아웃 비교와 큰 구조 미리보기 | -| `/design-styles` | 디자인 형식 검색, 카테고리/태그 필터, 색상표, 웹페이지형 스타일 샘플 | -| `/design-styles/[slug]` | 디자인 형식 상세 설명, 색상표, 타이포/레이아웃 특징, 관련 스타일 | -| `/design-styles/generate` | OpenAI Image API 기반 로컬 참조 이미지 생성 관리자 | +| `/layouts` | 레이아웃 검색, 필터, 카드 목록 | +| `/layouts/[slug]` | 구조 설명, 장단점, 반응형 동작, 접근성 노트, 라이브 프리뷰, 코드 예시 | +| `/layouts/compare` | 최대 3개 레이아웃 비교와 큰 구조 미리보기 | +| `/studio` | 디자인 스타일과 레이아웃을 조합해 실제 웹 프리뷰를 보고 코드/프롬프트를 복사 | +| `/styles` | 디자인 형식 검색, 카테고리/태그 필터, 색상표, 웹페이지형 스타일 샘플 | +| `/styles/[slug]` | 디자인 형식 상세 설명, 색상표, 타이포/레이아웃 특징, 관련 스타일 | +| `/styles/generate` | OpenAI Image API 기반 로컬 참조 이미지 생성 관리자 | +| `/components` | 디자인 스타일 토큰을 버튼, 카드, 내비게이션, 입력 필드, 배지에 적용해 비교 | ## 이미지 생성 환경 변수 -`/design-styles/generate`와 `/api/design-style-images`는 로컬 관리자 기능입니다. +`/styles/generate`와 `/api/design-style-images`는 로컬 관리자 기능입니다. ```bash OPENAI_API_KEY=sk-... @@ -122,17 +126,21 @@ npm run build ## 프로젝트 구조 ```text -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/layouts/page.tsx # Explorer page +src/app/layouts/[slug]/page.tsx # Layout detail page +src/app/layouts/compare/page.tsx # Compare page shell +src/app/studio/page.tsx # Style x Layout studio +src/app/components/page.tsx # Component dictionary +src/app/styles/page.tsx # Design style library page +src/app/styles/[slug]/page.tsx # Design style detail page +src/app/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/data/componentSpecs.ts # Tokenized component dictionary specs src/components/web-layout/ # Explorer, cards, previews, compare UI src/components/design-style/ # Style cards, filters, samples, generator UI +src/components/component-dictionary/ # Component token previews and picker 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 diff --git a/docs/plans/2026-06-03-design-dictionary.md b/docs/plans/2026-06-03-design-dictionary.md index bd8163e..d55da3f 100644 --- a/docs/plans/2026-06-03-design-dictionary.md +++ b/docs/plans/2026-06-03-design-dictionary.md @@ -1,477 +1,1089 @@ -# Design Dictionary Implementation Plan +# Design Dictionary Completion Plan -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Turn openlayout from a "palette-swap preview gallery" into a design dictionary where a full **style-token system** drives Style × Layout combinations into genuinely distinct previewable webs, with later component-level browsing and code/prompt copy. +**Goal:** Finish the design dictionary by tightening the already-built token/studio foundation, completing remaining style-token coverage, and adding copy/component dictionary features. -**Architecture:** Styles gain a machine-applied `StyleTokens` object (color, typography, shape, spacing, decoration, layout hints). A provider emits every token as a `--st-*` CSS variable; layout and component renderers read only those variables. Per-category default tokens + per-style overrides keep filling 88 styles cheap. A new `/studio` route crosses styles and layouts. +**Architecture:** `DesignStyle.tokens` is the machine-applied source of truth. `StylePresetProvider` and `styleTokenVars()` emit `--st-*` CSS variables; renderers consume those variables. `/studio` combines a style and layout through URL query state, while `/styles`, `/layouts`, and future `/components` expose the dictionary axes. -**Tech Stack:** Next.js App Router (see `node_modules/next/dist/docs/` before writing framework code — this is a modified Next.js), React 19, TypeScript, Tailwind CSS v4, static TypeScript data. No test framework is installed; verification uses a Node data-integrity script (`scripts/check-data.mjs`) plus `npm run lint` and `npm run build`. +**Tech Stack:** Modified Next.js 16 App Router, React 19, TypeScript, Tailwind CSS v4, static TypeScript data, Node 22. Before editing framework routes or config, read the relevant guide in `node_modules/next/dist/docs/` because this project uses a modified Next.js. **Design doc:** `docs/plans/2026-06-03-design-dictionary-design.md` --- -## Conventions +## Rebaseline Status -- **Verification gate** for every task: the relevant `node scripts/check-data.mjs` assertions pass AND `npm run lint` AND `npm run build` exit 0. -- **Commit** after each task with a focused message ending in the Co-Authored-By trailer. -- Work happens on a feature branch (current: `codex/design-style-library`, or a fresh `design-dictionary` branch if preferred). -- CSS variable namespace: `--st-*` (new token system), separate from the legacy `--style-*` used today, so the two can coexist during migration. -- DRY / YAGNI: do not build copy or component features before Phase 5/6. +This plan was rebaselined on 2026-06-04 against the actual working tree on `codex/design-style-library`. ---- +- [x] `npm run check:data` passes: 88 styles, 10 categories. +- [x] `npm run lint` passes. +- [x] `npm run build` passes and includes `/studio`, `/styles`, `/layouts`. +- [x] Working tree is clean. +- [x] Route restructure is already implemented: `/styles`, `/layouts`, redirects from `/design-styles` and `/web-layouts`. ## Milestone Checklist -- [ ] **M1: Token foundation** — `StyleTokens` type, category defaults, overrides, builder merge, data-integrity script, 8–12 representative styles fully tuned. -- [ ] **M2: Token CSS pipeline** — provider emits `--st-*` variables; a `useStyleTokens` hook; globals.css token utilities. -- [ ] **M3: Layout renderer tokenized** — `LayoutPreviewRenderer` consumes type/shape/space/decoration tokens, not just color. -- [ ] **M4: Studio combine view** — `/studio` crossing style × layout, URL query, viewport toggle. -- [ ] **M5: Route restructure** — `/styles` + `/layouts` canonical, redirects from old URLs. -- [ ] **M6: Fill all 88 styles** — token values for every style; visual QA. -- [ ] **M7: Copy feature** — code + prompt copy from studio and detail pages. -- [ ] **M8: Component dictionary** — `/components` (long term). +- [x] **M1: Token foundation** — `StyleTokens`, category defaults, builder merge, `tokens.color`, data check, and 12 representative overrides exist. +- [x] **M2: Token CSS pipeline** — provider/hook/emitter/effects exist; spacing, padding, heading scale, gap, density, and border-style utilities are covered. +- [x] **M3: Layout renderer tokenized** — colors/type/shape/shadow plus spacing, padding, heading, density, and border-style tokens are consumed in previews. +- [x] **M4: Studio combine view** — `/studio` exists with style/layout selection, URL query sync, viewport toggle, and disabled copy buttons. +- [x] **M5: Route restructure** — `/styles` + `/layouts` canonical routes and redirects exist. +- [x] **M6: Fill all 88 styles** — all 88 styles have valid tokens and 24+ high-impact overrides are tracked with visual QA evidence. +- [x] **M7: Copy feature** — prompt and code copy from studio/detail pages. +- [x] **M8: Component dictionary** — `/components` route and tokenized component specs. + +## Known Gaps To Fix First + +- [x] `/studio` default layout slug is invalid: `DEFAULT_LAYOUT = "hero-layout"` but the real first hero slug is `hero-focused-layout`; dashboard query should use `dashboard-layout`, not `dashboard`. +- [x] `styleTokenVars()` does not emit `--st-border-style`, even though `StyleTokens.shape.borderStyle` exists. +- [x] `.st-pad` does not exist in `globals.css`. +- [x] `--st-gap`, `--st-pad-scale`, and `--st-heading-scale` are emitted but barely consumed. +- [x] `styleTokenOverrides` covers 12 representative styles only; the rest mostly inherit category defaults. +- [x] `docs/plans/2026-06-03-design-dictionary.md` used to show everything unchecked; this rebaseline is now the source of truth. --- -## Phase 0: Verification Harness +## File Structure For Remaining Work + +### Existing Files To Modify + +- `src/components/studio/StudioView.tsx` + - Normalize invalid query slugs, fix defaults, and wire copy buttons. +- `src/components/style-preset/styleTokenVars.ts` + - Emit any missing `--st-*` variables. +- `src/app/globals.css` + - Add missing token utilities and make spacing/density utilities usable. +- `src/components/web-layout/LayoutPreviewRenderer.tsx` + - Consume spacing/padding/heading/border-style tokens more consistently. +- `src/components/design-style/DesignStyleSampleRenderer.tsx` + - Keep sample cards aligned with token behavior after utility changes. +- `src/data/designStyles.ts` + - Add remaining per-style token overrides in category batches. +- `scripts/check-data.mjs` + - Add stricter assertions for token completeness and export/copy assumptions. +- `README.md` + - Update the route table and feature bullets in the documentation task after `/studio` copy and `/components` are implemented. + +### New Files To Create + +- `src/lib/exportPrompt.ts` + - Compose a prompt from selected style + layout + tokens. +- `src/lib/exportCode.ts` + - Generate a self-contained HTML snippet using inline `--st-*` variables. +- `src/data/componentSpecs.ts` + - Define component dictionary data. +- `src/components/component-dictionary/ComponentDictionaryView.tsx` + - Client picker for style + component. +- `src/components/component-dictionary/ComponentPreviewRenderer.tsx` + - Tokenized previews for button/card/nav/input/badge. +- `src/app/components/page.tsx` + - Component dictionary route. + +--- -### Task 0: Add a data-integrity check script +## Task 1: Studio Query And Default Cleanup **Files:** -- Create: `scripts/check-data.mjs` -- Modify: `package.json` (add `"check:data"` script) +- Modify: `src/components/studio/StudioView.tsx` +- Verify: `src/data/webLayouts.ts` -- [ ] **Step 1: Create the check script** +- [x] **Step 1: Replace invalid default slug** -`scripts/check-data.mjs` imports the built data and asserts invariants. Because the data is TS, run it after `npm run build` against the compiled output, OR use a tiny inline TS loader. Simplest reliable approach: assert against the source via a regex-free dynamic import using `tsx` is NOT available (no new deps), so instead validate the JSON-able invariants by importing from a small generated manifest. +Set the studio default layout to a real slug: -Minimal first version (count + uniqueness against source counts the plan will extend): +```ts +const DEFAULT_STYLE = "brutalism"; +const DEFAULT_LAYOUT = "hero-focused-layout"; +``` -```js -// scripts/check-data.mjs -import { designStyles, designStyleCategories } from "../src/data/designStyles.ts"; +- [x] **Step 2: Normalize query-derived selections** -const errors = []; -function assert(cond, msg) { if (!cond) errors.push(msg); } +Replace the current selection block with a normalized slug flow: -assert(designStyles.length === 88, `expected 88 styles, got ${designStyles.length}`); -const slugs = new Set(designStyles.map((s) => s.slug)); -assert(slugs.size === designStyles.length, "duplicate style slugs found"); -assert(designStyleCategories.length === 10, `expected 10 categories, got ${designStyleCategories.length}`); +```ts +const requestedStyleSlug = params.get("style") ?? DEFAULT_STYLE; +const requestedLayoutSlug = params.get("layout") ?? DEFAULT_LAYOUT; +const viewport = (params.get("vp") === "mobile" ? "mobile" : "desktop") as PreviewViewport; + +const selectedStyle = useMemo( + () => designStyles.find((s) => s.slug === requestedStyleSlug) ?? designStyles.find((s) => s.slug === DEFAULT_STYLE) ?? designStyles[0], + [requestedStyleSlug], +); + +const selectedLayout = useMemo( + () => webLayouts.find((l) => l.slug === requestedLayoutSlug) ?? webLayouts.find((l) => l.slug === DEFAULT_LAYOUT) ?? webLayouts[0], + [requestedLayoutSlug], +); + +const selectedStyleSlug = selectedStyle.slug; +const selectedLayoutSlug = selectedLayout.slug; +``` + +- [x] **Step 3: Fix URL self-healing** + +Add an effect after selections are computed: -if (errors.length) { console.error("DATA CHECK FAILED:\n" + errors.join("\n")); process.exit(1); } -console.log(`data check passed: ${designStyles.length} styles, ${designStyleCategories.length} categories`); +```ts +useEffect(() => { + const next = new URLSearchParams(params.toString()); + let changed = false; + + if (next.get("style") !== selectedStyleSlug) { + next.set("style", selectedStyleSlug); + changed = true; + } + + if (next.get("layout") !== selectedLayoutSlug) { + next.set("layout", selectedLayoutSlug); + changed = true; + } + + if (next.get("vp") !== viewport) { + next.set("vp", viewport); + changed = true; + } + + if (changed) router.replace(`/studio?${next.toString()}`); +}, [params, router, selectedLayoutSlug, selectedStyleSlug, viewport]); ``` -> Note: Node cannot import `.ts` directly without a loader. Use `node --experimental-strip-types scripts/check-data.mjs` (Node 22+ supports type stripping). The repo pins Node 22 via `.nvmrc`/engines, so this works. +Also update the React import: -- [ ] **Step 2: Add npm script** +```ts +import { Suspense, useEffect, useMemo } from "react"; +``` -In `package.json` scripts: `"check:data": "node --experimental-strip-types scripts/check-data.mjs"`. +- [x] **Step 4: Verify** -- [ ] **Step 3: Run it (expect pass on current data)** +Run: -Run: `npm run check:data` -Expected: `data check passed: 88 styles, 10 categories` +```powershell +npm run lint +npm run build +``` -- [ ] **Step 4: Commit** +Expected: -```bash -git add scripts/check-data.mjs package.json -git commit -m "Add data-integrity check script for style data" +```text +Route (app) +├ ○ /studio ``` ---- +- [x] **Step 5: Commit** + +```powershell +git add src/components/studio/StudioView.tsx +git commit -m "Fix studio query defaults and selection normalization" +``` -## Phase 1: Token Foundation +--- -### Task 1: Define the `StyleTokens` type +## Task 2: Complete Token CSS Variable Coverage **Files:** -- Modify: `src/data/designStyles.ts` (top of file, near `DesignStylePalette`) +- Modify: `src/components/style-preset/styleTokenVars.ts` +- Modify: `src/app/globals.css` +- Modify: `scripts/check-data.mjs` -- [ ] **Step 1: Add the token types** +- [x] **Step 1: Emit border style** -Add above `DesignStyle`: +Add this variable in `styleTokenVars()` under shape tokens: ```ts -export type StyleDensity = "airy" | "normal" | "tight"; -export type StyleEffect = "none" | "glitch" | "scanline" | "grain" | "glow" | "gradient"; - -export type StyleTokens = { - typography: { - displayFont: string; // CSS font-family stack - bodyFont: string; - weightDisplay: number; // 400..900 - weightBody: number; - tracking: string; // e.g. "-0.05em" - headingScale: number; // multiplier on base heading sizes, e.g. 1.0 - }; - shape: { - radius: string; // e.g. "0px" | "12px" | "9999px" - borderWidth: string; // e.g. "1px" | "3px" - borderStyle: "solid" | "dashed" | "double"; - }; - space: { - density: StyleDensity; - gap: string; // base gap, e.g. "0.75rem" - padScale: number; // multiplier on base padding - }; - decoration: { - shadow: string; // CSS box-shadow or "none" - effect: StyleEffect; - }; - layout: { - heroVariant: "left" | "center" | "split"; - navStyle: "minimal" | "boxed" | "underline"; - alignment: "left" | "center"; - }; -}; +"--st-border-style": shape.borderStyle, +``` + +- [x] **Step 2: Add missing utility classes** + +Add these utilities to `src/app/globals.css` near the existing style token utility section: + +```css +.st-pad { + padding: calc(1rem * var(--st-pad-scale, 1)); +} + +.st-gap { + gap: var(--st-gap, 0.75rem); +} + +.st-border { + border-width: var(--st-border-width, 1px); + border-style: var(--st-border-style, solid); + border-color: rgb(var(--st-border-rgb, 30 30 30) / 0.22); +} + +.st-display { + font-size: calc(1em * var(--st-heading-scale, 1)); +} +``` + +If `.st-display` already exists, merge the `font-size` line into the existing rule instead of creating a duplicate selector. + +- [x] **Step 3: Keep density modifiers but bind them to real tokens** + +Replace the current density block with: + +```css +[data-st-density="tight"] .raw-preview-canvas, +[data-st-density="tight"] .raw-wireframe { + --internal-gap-scale: 0.7; +} + +[data-st-density="airy"] .raw-preview-canvas, +[data-st-density="airy"] .raw-wireframe { + --internal-gap-scale: 1.3; +} + +.raw-preview-canvas .st-density-gap, +.raw-wireframe .st-density-gap { + gap: calc(var(--internal-gap-scale, 1) * 1rem); +} ``` -- [ ] **Step 2: Add `tokens` to `DesignStyle`** +- [x] **Step 4: Strengthen data check** -Add `tokens: StyleTokens;` to the `DesignStyle` type. +In `scripts/check-data.mjs`, add assertions inside the existing `for (const s of designStyles)` loop: -- [ ] **Step 3: Verify it fails the build (tokens not yet produced)** +```js +assert(["solid","dashed","double"].includes(s.tokens.shape.borderStyle), `style ${s.slug} bad borderStyle`); +assert(typeof s.tokens.space.gap === "string" && s.tokens.space.gap.length > 0, `style ${s.slug} missing space.gap`); +assert(typeof s.tokens.space.padScale === "number", `style ${s.slug} bad padScale`); +assert(typeof s.tokens.typography.headingScale === "number", `style ${s.slug} bad headingScale`); +``` -Run: `npm run build` -Expected: TypeScript error — `buildStyle` return is missing `tokens`. This confirms the type is wired before we implement. +- [x] **Step 5: Verify** -### Task 2: Add per-category default tokens +Run: + +```powershell +npm run check:data +npm run lint +npm run build +``` + +Expected: + +```text +data check passed: 88 styles, 10 categories +``` + +- [x] **Step 6: Commit** + +```powershell +git add src/components/style-preset/styleTokenVars.ts src/app/globals.css scripts/check-data.mjs +git commit -m "Complete style token variable and utility coverage" +``` + +--- + +## Task 3: Apply Spacing, Padding, Heading, And Border-Style Tokens In Renderers **Files:** -- Modify: `src/data/designStyles.ts` +- Modify: `src/components/web-layout/LayoutPreviewRenderer.tsx` +- Modify: `src/components/design-style/DesignStyleSampleRenderer.tsx` -- [ ] **Step 1: Add `categoryTokenDefaults`** +- [x] **Step 1: Add token helper classes to renderer primitives** -Add a `Record` keyed by the 10 category names, giving each family a sensible baseline. Example entries (fill all 10): +In `LayoutPreviewRenderer.tsx`, update shared primitives so repeated regions inherit spacing and border style: -```ts -const categoryTokenDefaults: Record = { - "모던 / 미니멀": { - typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 600, weightBody: 400, tracking: "-0.02em", headingScale: 1.0 }, - shape: { radius: "2px", borderWidth: "1px", borderStyle: "solid" }, - space: { density: "airy", gap: "1rem", padScale: 1.2 }, - decoration: { shadow: "none", effect: "none" }, - layout: { heroVariant: "left", navStyle: "minimal", alignment: "left" }, - }, - "강렬 / 실험": { - typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 800, weightBody: 500, tracking: "-0.05em", headingScale: 1.25 }, - shape: { radius: "0px", borderWidth: "3px", borderStyle: "solid" }, - space: { density: "tight", gap: "0.5rem", padScale: 0.9 }, - decoration: { shadow: "6px 6px 0 var(--st-primary)", effect: "none" }, - layout: { heroVariant: "split", navStyle: "boxed", alignment: "left" }, - }, - "미래 / 디지털": { - typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"SFMono-Regular", monospace', weightDisplay: 700, weightBody: 400, tracking: "0em", headingScale: 1.1 }, - shape: { radius: "4px", borderWidth: "1px", borderStyle: "solid" }, - space: { density: "normal", gap: "0.75rem", padScale: 1.0 }, - decoration: { shadow: "0 0 18px rgb(var(--st-accent-rgb) / 0.5)", effect: "glow" }, - layout: { heroVariant: "center", navStyle: "underline", alignment: "left" }, - }, - // ... fill remaining 7 categories: - // "레트로 / 빈티지", "럭셔리 / 클래식", "자연 / 수공예", - // "귀여움 / 캐주얼", "스트리트 / 서브컬처", "편집 / 타이포그래피", "UI / 웹" -}; +```tsx +className={cn( + "min-w-0 max-w-full st-border st-density-gap bg-[rgb(var(--st-surface-rgb)_/_0.72)]", + className, +)} +``` + +Use this on the `Region`/`AnnotatedRegion` wrapper where the file currently repeats `border border-[rgb(var(--st-border-rgb)_/_0.18)]`. + +- [x] **Step 2: Apply `st-pad` to repeated surface containers** + +For repeated preview panels/cards, replace hardcoded `p-4` or `p-3` where safe with: + +```tsx +className="st-pad ..." +``` + +Keep compact-only padding classes when needed for thumbnail fit, but make the large preview path read `--st-pad-scale`. + +- [x] **Step 3: Apply `st-display` to headings** + +For heading helper output, include: + +```tsx +className={cn("st-display break-words uppercase leading-none", className)} ``` -- [ ] **Step 2: Commit (no behavior change yet)** +Remove duplicate inline `fontFamily`, `fontWeight`, and `letterSpacing` only where `.st-display` fully covers the same behavior. + +- [x] **Step 4: Keep design-style samples compatible** -```bash -git add src/data/designStyles.ts -git commit -m "Add per-category default style tokens" +In `DesignStyleSampleRenderer.tsx`, preserve the existing `--sample-*` variables for older sample-specific colors, but add `st-display`, `st-border`, or `st-pad` to reusable sample cards where those classes do not break compact thumbnails. + +- [x] **Step 5: Verify visual token difference** + +Run: + +```powershell +npm run lint +npm run build ``` -### Task 3: Add per-style token overrides + merge in builder +Then visually check these URLs: + +```text +http://localhost:3000/studio?style=brutalism&layout=dashboard-layout +http://localhost:3000/studio?style=luxury&layout=dashboard-layout +http://localhost:3000/studio?style=cyberpunk&layout=dashboard-layout +``` + +Expected differences: + +- `brutalism`: hard borders, heavy title weight, tight density, hard shadow. +- `luxury`: serif display, airy spacing, thin borders, restrained/no shadow. +- `cyberpunk`: mono body, glow shadow, neon contrast. + +- [x] **Step 6: Commit** + +```powershell +git add src/components/web-layout/LayoutPreviewRenderer.tsx src/components/design-style/DesignStyleSampleRenderer.tsx +git commit -m "Apply spacing and typography tokens across previews" +``` + +--- + +## Task 4: Refine Remaining Style Tokens In Category Batches **Files:** - Modify: `src/data/designStyles.ts` +- Modify: `scripts/check-data.mjs` -- [ ] **Step 1: Add `styleTokenOverrides`** +- [x] **Step 1: Add an override coverage list to data check** -A `Record>` for styles that need to differ from their category baseline. Add a small deep-merge helper. Start with overrides only for the representative styles (Task 5); others inherit category defaults. +Because `styleTokenOverrides` is local to `designStyles.ts`, add an exported coverage marker near the overrides: ```ts -type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] }; +export const tunedStyleTokenSlugs = Object.keys(styleTokenOverrides).sort(); +``` -const styleTokenOverrides: Record> = { - kawaii: { shape: { radius: "9999px", borderWidth: "2px" }, decoration: { shadow: "4px 4px 0 var(--st-accent)", effect: "none" } }, - glitch-art: { decoration: { effect: "glitch" }, shape: { radius: "0px" } }, - // ... -}; +Then import it in `scripts/check-data.mjs`: + +```js +import { designStyles, designStyleCategories, tunedStyleTokenSlugs } from "../src/data/designStyles.ts"; +``` -function mergeTokens(base: StyleTokens, over?: DeepPartial): StyleTokens { - if (!over) return base; - return { - typography: { ...base.typography, ...over.typography }, - shape: { ...base.shape, ...over.shape }, - space: { ...base.space, ...over.space }, - decoration: { ...base.decoration, ...over.decoration }, - layout: { ...base.layout, ...over.layout }, - }; +Add: + +```js +assert(tunedStyleTokenSlugs.length >= 24, `expected at least 24 tuned style token overrides, got ${tunedStyleTokenSlugs.length}`); +for (const slug of tunedStyleTokenSlugs) { + assert(slugSet.has(slug), `tuned style slug "${slug}" does not exist`); } ``` -> Note: object keys with hyphens (e.g. `glitch-art`) must be quoted: `"glitch-art": {...}`. +This creates a concrete first target without pretending all 88 are fully hand-tuned in one pass. + +- [x] **Step 2: Tune 12 additional high-impact styles** + +Add overrides for these slugs in `styleTokenOverrides`: + +```text +modernism +warm-minimal +new-brutalism +glitch-art +retro-futurism +neon-noir +old-money +wabi-sabi +dopamine-design +graffiti +posterism +neumorphism +``` + +Each override must change at least two non-color groups among `typography`, `shape`, `space`, `decoration`, `layout`. + +- [x] **Step 3: Verify the new coverage target** + +Run: + +```powershell +npm run check:data +npm run lint +npm run build +``` + +Expected: + +```text +data check passed: 88 styles, 10 categories +``` + +- [x] **Step 4: Visual QA** + +Open `/styles` and scan one category at a time. Record notes in the progress log for: + +```text +모던 / 미니멀 +강렬 / 실험 +레트로 / 빈티지 +미래 / 디지털 +럭셔리 / 클래식 +자연 / 수공예 +귀여움 / 캐주얼 +스트리트 / 서브컬처 +편집 / 타이포그래피 +UI / 웹 +``` + +Expected: -- [ ] **Step 2: Produce `tokens` in `buildStyle`** +- Category-level visual language is distinct. +- The 24 tuned styles have visible differences beyond color. +- No horizontal overflow at 390px. -In `buildStyle`, before the `return`, compute: +- [x] **Step 5: Commit** + +```powershell +git add src/data/designStyles.ts scripts/check-data.mjs +git commit -m "Tune additional style tokens and track override coverage" +``` + +--- + +## Task 5: Prompt Copy Feature + +**Files:** +- Create: `src/lib/exportPrompt.ts` +- Modify: `src/components/studio/StudioView.tsx` + +- [x] **Step 1: Create prompt exporter** + +Create `src/lib/exportPrompt.ts`: ```ts -const baseTokens = categoryTokenDefaults[seed.category]; -const tokens = mergeTokens(baseTokens, styleTokenOverrides[seed.slug]); +import type { DesignStyle } from "@/data/designStyles"; +import type { WebLayout } from "@/data/webLayouts"; + +export function exportDesignPrompt(style: DesignStyle, layout: WebLayout) { + const tokens = style.tokens; + + return [ + `Create a high-quality webpage design reference for ${style.nameEn} (${style.nameKo}) using a ${layout.nameEn}.`, + `Visual style: ${style.summary}`, + `Layout structure: ${layout.summary}`, + `Preview type: ${layout.previewType}.`, + `Color system: base ${tokens.color.base}, surface ${tokens.color.surface}, text ${tokens.color.text}, primary ${tokens.color.primary}, accent ${tokens.color.accent}.`, + `Typography: display font ${tokens.typography.displayFont}, body font ${tokens.typography.bodyFont}, display weight ${tokens.typography.weightDisplay}, tracking ${tokens.typography.tracking}.`, + `Shape and spacing: radius ${tokens.shape.radius}, border ${tokens.shape.borderWidth} ${tokens.shape.borderStyle}, density ${tokens.space.density}, gap ${tokens.space.gap}.`, + `Decoration: shadow ${tokens.decoration.shadow}, effect ${tokens.decoration.effect}.`, + `Use cases: ${style.useCases.join(", ")}.`, + `Avoid: ${style.cautions.join(", ")}.`, + "Return a polished webpage composition, no logo, no watermark, production-ready visual hierarchy.", + ].join("\n"); +} + +export function exportLayoutPrompt(layout: WebLayout) { + return [ + `Create a production-ready webpage using a ${layout.nameEn} (${layout.nameKo}).`, + `Layout summary: ${layout.summary}`, + `Preview type: ${layout.previewType}.`, + `Structure: ${layout.structure.join(", ")}.`, + `Responsive behavior: ${layout.responsiveBehavior.join(", ")}.`, + `Best for: ${layout.bestFor.join(", ")}.`, + `Avoid: ${layout.notGoodFor.join(", ")}.`, + "Keep the hierarchy clear, responsive, accessible, and suitable for a design dictionary reference.", + ].join("\n"); +} ``` -Add `tokens,` to the returned object. +- [x] **Step 2: Add clipboard state in Studio** -- [ ] **Step 3: Build passes now** +In `StudioView.tsx`, import `useState` and the exporter: -Run: `npm run build` -Expected: exit 0 (the missing-`tokens` error from Task 1 is resolved). +```ts +import { Suspense, useEffect, useMemo, useState } from "react"; +import { exportDesignPrompt } from "@/lib/exportPrompt"; +``` -- [ ] **Step 4: Extend the data check for token completeness** +Add state and handler inside `StudioViewInner()`: -In `scripts/check-data.mjs` add: +```ts +const [copied, setCopied] = useState<"code" | "prompt" | null>(null); -```js -for (const s of designStyles) { - assert(s.tokens, `style ${s.slug} missing tokens`); - assert(["airy","normal","tight"].includes(s.tokens.space.density), `style ${s.slug} bad density`); - assert(typeof s.tokens.typography.weightDisplay === "number", `style ${s.slug} bad weightDisplay`); +async function copyPrompt() { + await navigator.clipboard.writeText(exportDesignPrompt(selectedStyle, selectedLayout)); + setCopied("prompt"); + window.setTimeout(() => setCopied(null), 1400); } ``` -Run: `npm run check:data` → expect pass for all 88. +- [x] **Step 3: Enable the prompt button** -- [ ] **Step 5: Commit** +Replace the disabled prompt button with: -```bash -git add src/data/designStyles.ts scripts/check-data.mjs -git commit -m "Generate style tokens via category defaults + per-style overrides" +```tsx + +``` + +- [x] **Step 4: Detail pages** + +For this task, add prompt copy only to `/studio`. Detail pages are completed in Task 7 with a reusable copy button. + +- [x] **Step 5: Verify** + +Run: + +```powershell +npm run lint +npm run build +``` + +Manual check: + +```text +/studio?style=cyberpunk&layout=dashboard-layout ``` -### Task 4: Migrate `palette` into `tokens.color` (compat-preserving) +Click `프롬프트 복사`; expected button text changes to `프롬프트 복사됨`. + +- [x] **Step 6: Commit** + +```powershell +git add src/lib/exportPrompt.ts src/components/studio/StudioView.tsx +git commit -m "Add prompt copy export for studio combinations" +``` + +--- + +## Task 6: Code Copy Feature **Files:** -- Modify: `src/data/designStyles.ts` +- Create: `src/lib/exportCode.ts` +- Modify: `src/components/studio/StudioView.tsx` + +- [x] **Step 1: Create code exporter** -- [ ] **Step 1: Add `color` to `StyleTokens`** +Create `src/lib/exportCode.ts`: + +```ts +import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; +import type { DesignStyle } from "@/data/designStyles"; +import type { WebLayout } from "@/data/webLayouts"; + +function cssVars(style: DesignStyle) { + return Object.entries(styleTokenVars(style)) + .map(([key, value]) => ` ${key}: ${String(value)};`) + .join("\n"); +} + +export function exportDesignCode(style: DesignStyle, layout: WebLayout) { + return ` +
+
+ ${style.nameEn} + +
+
+

${layout.previewType}

+

${layout.nameKo}

+

${layout.summary}

+ +
+
+ +`; +} +``` -Add a `color` group to `StyleTokens` mirroring `DesignStylePalette` keys (base, surface, text, muted, primary, accent, accent2, accent3, border). +- [x] **Step 2: Wire code copy in Studio** -- [ ] **Step 2: Derive `tokens.color` from `seed.palette` in `buildStyle`** +Import: ```ts -const color = { - base: seed.palette.base, surface: seed.palette.surface, text: seed.palette.text, - muted: seed.palette.mutedText, primary: seed.palette.primary, accent: seed.palette.accent, - accent2: seed.palette.accent2, accent3: seed.palette.accent3, border: seed.palette.border, -}; -const tokens = { color, ...mergeTokens(baseTokens, styleTokenOverrides[seed.slug]) }; +import { exportDesignCode } from "@/lib/exportCode"; ``` -Keep the legacy `palette` field on `DesignStyle` for now (no breaking change to existing components). +Add handler: + +```ts +async function copyCode() { + await navigator.clipboard.writeText(exportDesignCode(selectedStyle, selectedLayout)); + setCopied("code"); + window.setTimeout(() => setCopied(null), 1400); +} +``` -- [ ] **Step 3: check:data + build + commit** +Replace the disabled code button: -```bash -npm run check:data && npm run build -git add src/data/designStyles.ts -git commit -m "Add tokens.color derived from palette (keep palette for compat)" +```tsx + ``` -### Task 5: Fully tune 8–12 representative styles +- [x] **Step 3: Verify** -**Files:** -- Modify: `src/data/designStyles.ts` (`styleTokenOverrides`) +Run: + +```powershell +npm run lint +npm run build +``` -Representative set (one per category + a few extremes): `minimalism`, `brutalism`, `cyberpunk`, `luxury`, `organic-design`, `kawaii`, `streetwear`, `editorial-design`, `glassmorphism`, `y2k`, `maximalism`, `swiss-design`. +Manual check: -- [ ] **Step 1: Write override tokens for each so they look visibly distinct** +```text +/studio?style=luxury&layout=product-demo-layout +``` -For each, set typography/shape/space/decoration that match the style's character (e.g. luxury = serif display, airy, thin gold borders; brutalism = heavy weight, 0 radius, thick black borders, hard offset shadow; glassmorphism = blur shadow, 16px radius). +Click `코드 복사`; expected button text changes to `코드 복사됨`. -- [ ] **Step 2: check:data + build + commit** +- [x] **Step 4: Commit** -```bash -npm run check:data && npm run build -git add src/data/designStyles.ts -git commit -m "Tune full tokens for 12 representative styles" +```powershell +git add src/lib/exportCode.ts src/components/studio/StudioView.tsx +git commit -m "Add code copy export for studio combinations" ``` --- -## Phase 2: Token CSS Pipeline - -### Task 6: Add a token→CSS-variable emitter and hook +## Task 7: Detail Page Copy Buttons And README Update **Files:** -- Create: `src/components/style-preset/styleTokenVars.ts` -- Modify: `src/components/style-preset/StylePresetProvider.tsx` +- Create: `src/components/export/CopyTextButton.tsx` +- Modify: `src/app/styles/[slug]/page.tsx` +- Modify: `src/app/layouts/[slug]/page.tsx` +- Modify: `README.md` -- [ ] **Step 1: Create `styleTokenVars.ts`** +- [x] **Step 1: Create reusable copy button** -A pure function `styleTokenVars(style: DesignStyle): CSSProperties` returning all `--st-*` variables (color + color `-rgb` + `--st-radius`, `--st-border-width`, `--st-font-display`, `--st-font-body`, `--st-weight-display`, `--st-tracking`, `--st-gap`, `--st-pad-scale`, `--st-shadow`, `--st-heading-scale`, plus `--st-effect` as a data attribute value). Reuse the existing `hexToRgb` (export it from the provider or duplicate locally). +Create `src/components/export/CopyTextButton.tsx`: -- [ ] **Step 2: Apply on `.style-preset-root`** +```tsx +"use client"; -In `StylePresetProvider`, spread `styleTokenVars(activePreset)` into the root `style` alongside the legacy vars, and add `data-st-effect={activePreset.tokens.decoration.effect}`. +import { useState } from "react"; -- [ ] **Step 3: Expose tokens via context** +type CopyTextButtonProps = { + copiedLabel: string; + idleLabel: string; + text: string; +}; -Add `tokens: activePreset.tokens` to the context value and `useStyleTokens()` convenience hook. +export function CopyTextButton({ copiedLabel, idleLabel, text }: CopyTextButtonProps) { + const [copied, setCopied] = useState(false); + + async function copy() { + await navigator.clipboard.writeText(text); + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + } + + return ( + + ); +} +``` -- [ ] **Step 4: build + commit** +- [x] **Step 2: Add style prompt copy to style detail page** -```bash -npm run build -git add src/components/style-preset/ -git commit -m "Emit style tokens as --st-* CSS variables and expose via hook" +In `src/app/styles/[slug]/page.tsx`, import: + +```ts +import { CopyTextButton } from "@/components/export/CopyTextButton"; ``` -### Task 7: Add token-driven utilities + effect layers to globals.css +Inside the `이미지 생성 프롬프트` section, place the button above the `pre`: -**Files:** -- Modify: `src/app/globals.css` +```tsx +
+ +
+``` + +- [x] **Step 3: Add layout prompt copy to layout detail page** + +In `src/app/layouts/[slug]/page.tsx`, import: + +```ts +import { CopyTextButton } from "@/components/export/CopyTextButton"; +import { exportLayoutPrompt } from "@/lib/exportPrompt"; +``` + +In the sidebar info block after `layout.description`, add: + +```tsx +
+ +
+``` -- [ ] **Step 1: Add base token utility classes** +Keep the existing `LayoutCodeExample` component in place; it already provides code copy on layout detail pages. -Add classes that read `--st-*`: `.st-surface`, `.st-card` (radius+border+shadow), `.st-display` (font+weight+tracking+scale), `.st-body`, `.st-pad` (uses `--st-pad-scale`). +- [x] **Step 4: Update README route table** -- [ ] **Step 2: Add effect layers keyed by `[data-st-effect]`** +In `README.md`, update the route table so it includes: -`[data-st-effect="glow"] .st-card { box-shadow: var(--st-shadow); }`, `[data-st-effect="grain"]` overlay, `[data-st-effect="scanline"]` overlay, `[data-st-effect="glitch"]` animation. Respect `prefers-reduced-motion`. +```markdown +| `/studio` | 디자인 스타일과 레이아웃을 조합해 실제 웹 프리뷰를 보고 코드/프롬프트를 복사 | +| `/components` | 디자인 스타일 토큰을 버튼, 카드, 내비게이션, 입력 필드, 배지에 적용해 비교 | +``` + +Also update the feature list near the top to mention: + +```markdown +- **Studio copy**: `/studio`에서 선택한 Style x Layout 조합의 프롬프트와 self-contained HTML/CSS 코드를 복사할 수 있습니다. +- **Component dictionary**: `/components`에서 같은 스타일 토큰이 주요 UI 컴포넌트에 어떻게 적용되는지 확인할 수 있습니다. +``` -- [ ] **Step 3: build + commit** +- [x] **Step 5: Verify** -```bash +Run: + +```powershell +npm run lint npm run build -git add src/app/globals.css -git commit -m "Add token-driven utility classes and effect layers" ``` ---- +Manual checks: -## Phase 3: Tokenize the Layout Renderer +```text +/styles/cyberpunk +/layouts/dashboard-layout +``` -### Task 8: Convert `LayoutPreviewRenderer` to `--st-*` tokens +Expected: -**Files:** -- Modify: `src/components/web-layout/LayoutPreviewRenderer.tsx` +- Style detail page copies `style.imagePrompt`. +- Layout detail page copies `exportLayoutPrompt(layout)`. +- Existing layout code copy still works. -- [ ] **Step 1: Replace `--style-*` color refs with `--st-*`** across the renderer (the renderer already uses CSS-variable color form from the prior change — repoint the namespace). -- [ ] **Step 2: Apply non-color tokens**: card containers use `rounded-[var(--st-radius)] border-[length:var(--st-border-width)] shadow-[var(--st-shadow)]`; headings use `.st-display`; padding scales with `--st-pad-scale`; gaps use `--st-gap`. -- [ ] **Step 3: Verify same layout differs across styles**: build, then visual-check `/web-layouts/[slug]` after selecting `brutalism` vs `luxury` vs `cyberpunk` — radius, weight, borders, shadow visibly change. -- [ ] **Step 4: build + lint + commit** +- [x] **Step 6: Commit** -```bash -npm run lint && npm run build -git add src/components/web-layout/LayoutPreviewRenderer.tsx -git commit -m "Drive layout preview renderer from full style tokens" +```powershell +git add src/components/export/CopyTextButton.tsx src/app/styles/[slug]/page.tsx src/app/layouts/[slug]/page.tsx README.md +git commit -m "Add copy actions to detail pages" ``` -### Task 9: Tokenize the design-style sample renderer +--- + +## Task 8: Component Dictionary MVP **Files:** -- Modify: `src/components/design-style/DesignStyleSampleRenderer.tsx` +- Create: `src/data/componentSpecs.ts` +- Create: `src/components/component-dictionary/ComponentPreviewRenderer.tsx` +- Create: `src/components/component-dictionary/ComponentDictionaryView.tsx` +- Create: `src/app/components/page.tsx` +- Modify: `src/app/layout.tsx` +- Modify: `README.md` -- [ ] Apply the same `--st-*` token treatment so style cards/samples reflect typography/shape/decoration, not only palette. build + commit. +- [x] **Step 1: Create component specs** ---- +Create `src/data/componentSpecs.ts`: -## Phase 4: Studio Combine View +```ts +export type ComponentSpecType = "button" | "card" | "nav" | "input" | "badge"; + +export type ComponentSpec = { + slug: string; + nameKo: string; + nameEn: string; + type: ComponentSpecType; + summary: string; +}; -### Task 10: Create the studio page shell +export const componentSpecs: ComponentSpec[] = [ + { slug: "button", nameKo: "버튼", nameEn: "Button", type: "button", summary: "주요 행동을 유도하는 클릭 요소입니다." }, + { slug: "card", nameKo: "카드", nameEn: "Card", type: "card", summary: "반복 콘텐츠를 묶어 비교 가능하게 보여줍니다." }, + { slug: "nav", nameKo: "내비게이션", nameEn: "Navigation", type: "nav", summary: "페이지 이동과 현재 구조를 보여줍니다." }, + { slug: "input", nameKo: "입력 필드", nameEn: "Input", type: "input", summary: "검색과 폼 입력을 받는 기본 컨트롤입니다." }, + { slug: "badge", nameKo: "배지", nameEn: "Badge", type: "badge", summary: "상태, 카테고리, 짧은 메타 정보를 표시합니다." }, +]; +``` -**Files:** -- Create: `src/app/studio/page.tsx` -- Create: `src/components/studio/StudioView.tsx` (client) +- [x] **Step 2: Create tokenized renderer** -- [ ] **Step 1**: `StudioView` holds `style` and `layout` selection state, initialized from URL query (`useSearchParams`) with sensible defaults. -- [ ] **Step 2**: Layout: left control column (style select, layout select, viewport toggle), right live preview rendering `` for the chosen layout inside a `.style-preset-root`-scoped wrapper carrying the chosen style's `styleTokenVars`. -- [ ] **Step 3**: Sync selection → URL (`router.replace` with new query) so combos are shareable. -- [ ] **Step 4**: Reserve disabled "코드 복사" / "프롬프트 복사" buttons (Phase 7). -- [ ] **Step 5**: lint + build, visual-check `/studio?style=cyberpunk&layout=dashboard`, commit. +Create `src/components/component-dictionary/ComponentPreviewRenderer.tsx`: -### Task 11: Studio thumbnail pickers (optional polish) +```tsx +import type { ComponentSpec } from "@/data/componentSpecs"; -- [ ] Replace dropdowns with small thumbnail grids for style/layout using existing card thumbnails. build + commit. +export function ComponentPreviewRenderer({ component }: { component: ComponentSpec }) { + if (component.type === "button") { + return ; + } ---- + if (component.type === "card") { + return

Card title

A reusable surface that follows the selected style tokens.

; + } -## Phase 5: Route Restructure + if (component.type === "nav") { + return ; + } -### Task 12: Add `/styles` and `/layouts` canonical routes with redirects + if (component.type === "input") { + return ; + } -**Files:** -- Create: `src/app/styles/...`, `src/app/layouts/...` (or rename existing dirs) -- Modify: `next.config.ts` (redirects) -- Modify: nav/links across `src/` + return Status badge; +} +``` -- [ ] **Step 1**: Move/rename `design-styles` → `styles`, `web-layouts` → `layouts` (keep `[slug]`, `compare`, `generate`). -- [ ] **Step 2**: Add permanent redirects in `next.config.ts`: `/web-layouts/:path*` → `/layouts/:path*`, `/design-styles/:path*` → `/styles/:path*`. -- [ ] **Step 3**: Update all internal `href`s and the `skills/*` docs and README route tables. -- [ ] **Step 4**: lint + build, click-through check, commit. +- [x] **Step 3: Create client view** + +Create `src/components/component-dictionary/ComponentDictionaryView.tsx`: + +```tsx +"use client"; + +import { useMemo, useState } from "react"; +import { ComponentPreviewRenderer } from "@/components/component-dictionary/ComponentPreviewRenderer"; +import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; +import { componentSpecs } from "@/data/componentSpecs"; +import { designStyles } from "@/data/designStyles"; + +export function ComponentDictionaryView() { + const [styleSlug, setStyleSlug] = useState("brutalism"); + const [componentSlug, setComponentSlug] = useState("button"); + + const style = useMemo(() => designStyles.find((item) => item.slug === styleSlug) ?? designStyles[0], [styleSlug]); + const component = useMemo(() => componentSpecs.find((item) => item.slug === componentSlug) ?? componentSpecs[0], [componentSlug]); + + return ( +
+ +
+

{style.nameKo} x {component.nameKo}

+
+ +
+

{component.summary}

+
+
+ ); +} +``` ---- +- [x] **Step 4: Create route** -## Phase 6: Fill All 88 Styles +Create `src/app/components/page.tsx`: -### Task 13: Token values for every remaining style +```tsx +import type { Metadata } from "next"; +import { ComponentDictionaryView } from "@/components/component-dictionary/ComponentDictionaryView"; -**Files:** -- Modify: `src/data/designStyles.ts` (`styleTokenOverrides`) +export const metadata: Metadata = { + title: "Components | openlayout", + description: "Preview UI components with design style tokens.", +}; -- [ ] **Step 1**: For each of the ~76 not-yet-tuned styles, add overrides where the category default is not specific enough (many will be fine inheriting defaults). -- [ ] **Step 2**: Extend `scripts/check-data.mjs` to warn on styles that are visually identical to their category default if you want stricter coverage (optional). -- [ ] **Step 3**: Visual QA pass — scan `/styles` gallery; ensure no two categories look interchangeable. build + commit in batches by category. +export default function ComponentsPage() { + return ( +
+
+

Component Dictionary

+

+ Components +

+
+ +
+
+
+ ); +} +``` ---- +- [x] **Step 5: Add nav link** -## Phase 7: Copy Feature +In `src/app/layout.tsx`, add `["Components", "/components"]` to the desktop and mobile nav lists near `Studio`. -### Task 14: Prompt copy +- [x] **Step 6: Verify** -**Files:** -- Create: `src/lib/exportPrompt.ts` -- Modify: `src/components/studio/StudioView.tsx`, style/layout detail pages +Run: -- [ ] Build a function that composes a generation prompt from the selected style (`imagePrompt`, tokens) + layout (`previewType`, structure). Wire the "프롬프트 복사" button (clipboard). build + commit. +```powershell +npm run lint +npm run build +``` -### Task 15: Code copy +Expected build output includes: -**Files:** -- Create: `src/lib/exportCode.ts` +```text +├ ○ /components +``` -- [ ] Generate a self-contained HTML+Tailwind (or React+Tailwind) snippet for the current Style × Layout using the token CSS variables inlined. Wire "코드 복사". build + commit. +Manual visual check: ---- +```text +/components +``` + +Expected: -## Phase 8: Component Dictionary (long term) +- Changing style updates component color/type/shape/shadow. +- Button/card/nav/input/badge all render without overflow at 390px. -### Task 16: `/components` route +- [x] **Step 7: Commit** -- [ ] Define a `ComponentSpec` data model (button, card, nav, input, badge, …) rendered with `--st-*` tokens; gallery + detail; reuse studio's style picker. Detailed sub-plan to be written when Phases 1–7 land. +```powershell +git add src/data/componentSpecs.ts src/components/component-dictionary src/app/components/page.tsx src/app/layout.tsx README.md +git commit -m "Add tokenized component dictionary MVP" +``` --- -## Verification Checklist (whole feature) +## Task 9: Final Verification And Plan Closeout + +**Files:** +- Modify: `docs/plans/2026-06-03-design-dictionary.md` + +- [x] **Step 1: Run full gate** + +```powershell +npm run check:data +npm run lint +npm run build +``` + +Expected: -- [ ] `npm run check:data` passes (88 styles, all have valid tokens). -- [ ] `npm run lint` passes. -- [ ] `npm run build` passes. -- [ ] Same layout looks visibly different across brutalism / luxury / cyberpunk (radius, weight, border, shadow, density). -- [ ] `/studio?style=...&layout=...` renders the combo and is shareable via URL. -- [ ] Old URLs (`/web-layouts`, `/design-styles`) redirect. -- [ ] No horizontal overflow at 390px width. -- [ ] Selected style persists across reload (existing localStorage behavior intact). +```text +data check passed: 88 styles, 10 categories +``` + +and build route output includes: + +```text +├ ○ /components +├ ○ /studio +├ ○ /styles +├ ○ /layouts +``` + +- [x] **Step 2: Manual route checks** + +Check: + +```text +/studio?style=cyberpunk&layout=dashboard-layout +/studio?style=luxury&layout=product-demo-layout +/styles +/layouts +/components +/web-layouts +/design-styles +``` + +Expected: + +- Studio renders selected combo and normalizes invalid slugs. +- Copy buttons work. +- Old URLs redirect to canonical routes. +- No horizontal overflow at 390px on `/studio` and `/components`. + +- [x] **Step 3: Update progress log** + +Add a dated progress log entry at the bottom of this file summarizing completed tasks and verification commands. + +- [x] **Step 4: Commit** + +```powershell +git add docs/plans/2026-06-03-design-dictionary.md +git commit -m "Close out design dictionary completion plan" +``` --- ## Progress Log -- [ ] 2026-06-03: Plan created from approved design doc. +- [x] 2026-06-03: Original plan created from approved design doc. +- [x] 2026-06-04: Rebaselined plan against actual code. Confirmed `npm run check:data`, `npm run lint`, and `npm run build` pass. Current implementation is effectively through M5, with M2/M3/M6 refinements and M7/M8 remaining. +- [x] 2026-06-04: Task 4 visual QA checked `/styles`: 88 cards rendered on desktop, 390px mobile had no horizontal overflow, screenshots saved under `.next/codex-screens/styles-token-qa`. +- [x] 2026-06-04: Tasks 5-8 completed: `/studio` prompt/code copy, detail-page copy buttons, README route updates, and `/components` tokenized component dictionary. +- [x] 2026-06-04: Task 9 final gate passed: `npm run check:data`, `npm run lint`, and `npm run build` all exited 0; build generated 195 static pages and included `/components`, `/studio`, `/styles`, and `/layouts`. +- [x] 2026-06-04: Task 9 route QA passed for `/studio?style=cyberpunk&layout=dashboard-layout`, `/studio?style=luxury&layout=product-demo-layout`, `/styles`, `/layouts`, `/components`, `/web-layouts`, and `/design-styles`. Studio copy buttons worked, invalid query slugs normalized to `/studio?style=brutalism&layout=hero-focused-layout&vp=desktop`, legacy redirects landed on `/layouts` and `/styles`, and 390px overflow checks passed for `/studio` and `/components`. Screenshots saved under `.next/codex-screens/final-route-qa`. diff --git a/next.config.ts b/next.config.ts index e9ffa30..4473cb8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + async redirects() { + return [ + { source: "/web-layouts", destination: "/layouts", permanent: true }, + { source: "/web-layouts/:path*", destination: "/layouts/:path*", permanent: true }, + { source: "/design-styles", destination: "/styles", permanent: true }, + { source: "/design-styles/:path*", destination: "/styles/:path*", permanent: true }, + ]; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 251bf54..43b3d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,12 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.6", + "playwright": "^1.49.0", "tailwindcss": "^4", "typescript": "^5" + }, + "engines": { + "node": ">=22" } }, "node_modules/@alloc/quick-lru": { @@ -1110,9 +1114,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1129,9 +1130,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1148,9 +1146,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1167,9 +1162,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3673,6 +3665,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5378,6 +5385,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 099b783..b6a638e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "build": "next build", "start": "next start", "lint": "eslint", - "check:data": "node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/check-data.mjs" + "check:data": "node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/check-data.mjs", + "capture:refs": "node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/capture-references.mjs" }, "engines": { "node": ">=22" @@ -32,6 +33,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "playwright": "^1.49.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/scripts/capture-references.mjs b/scripts/capture-references.mjs new file mode 100644 index 0000000..3e5726c --- /dev/null +++ b/scripts/capture-references.mjs @@ -0,0 +1,146 @@ +/** + * capture-references.mjs + * + * Takes viewport screenshots of reference websites for each design style. + * Screenshots are saved to public/references/[slug]/ and GITIGNORED. + * Captures both "sites" (real brand pages) and "galleries" (inspiration platforms). + * + * Usage: + * npm run capture:refs # all 12 styles + * npm run capture:refs brutalism kawaii luxury # specific styles only + * npm run capture:refs -- --sites-only # skip galleries + * + * First-time setup: + * npm install + * npx playwright install chromium + */ + +import { chromium } from "playwright"; +import { mkdir, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import references from "./style-references.json" with { type: "json" }; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OUT_DIR = join(__dirname, "..", "public", "references"); + +// Parse CLI args +const rawArgs = process.argv.slice(2); +const sitesOnly = rawArgs.includes("--sites-only"); +const targetSlugs = rawArgs.filter((a) => !a.startsWith("--")); + +const slugsToCapture = targetSlugs.length > 0 + ? targetSlugs + : Object.keys(references).filter((k) => !k.startsWith("_")); + +const VIEWPORT = { width: 1440, height: 900 }; +const TIMEOUT = 20_000; + +function makeFilename(url) { + return url + .replace(/^https?:\/\//, "") + .replace(/[^a-z0-9]/gi, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 60) + ".jpg"; +} + +async function capturePage(page, { url, title, note }, outDir, label) { + const filename = makeFilename(url); + const outPath = join(outDir, filename); + + if (existsSync(outPath)) { + console.log(` ⏭ ${label} — already exists, skipping`); + return { ok: true, skipped: true }; + } + + process.stdout.write(` 📷 ${label} (${title}) ... `); + + try { + await page.goto(url, { waitUntil: "domcontentloaded", timeout: TIMEOUT }); + await page.waitForTimeout(2500); + + const buf = await page.screenshot({ + type: "jpeg", + quality: 88, + clip: { x: 0, y: 0, width: VIEWPORT.width, height: VIEWPORT.height }, + }); + + await writeFile(outPath, buf); + console.log(`✓`); + + // Write a sidecar .txt with metadata for token research reference + const meta = [ + `url: ${url}`, + `title: ${title}`, + note ? `note: ${note}` : null, + `captured: ${new Date().toISOString()}`, + ].filter(Boolean).join("\n"); + await writeFile(outPath.replace(".jpg", ".txt"), meta); + + return { ok: true, skipped: false }; + } catch (err) { + console.log(`✗ ${err.message.split("\n")[0]}`); + return { ok: false, skipped: false }; + } +} + +async function capture() { + console.log(`\n📸 Capturing reference screenshots`); + console.log(` Styles: ${slugsToCapture.join(", ")}`); + console.log(` Mode: ${sitesOnly ? "sites only" : "sites + galleries"}\n`); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: VIEWPORT }); + + let total = 0; + let failed = 0; + let skipped = 0; + + for (const slug of slugsToCapture) { + const entry = references[slug]; + if (!entry) { + console.warn(`⚠️ No references found for: ${slug}`); + continue; + } + + const outDir = join(OUT_DIR, slug); + await mkdir(outDir, { recursive: true }); + + console.log(`\n▸ ${slug}`); + + const sites = entry.sites ?? []; + const galleries = sitesOnly ? [] : (entry.galleries ?? []); + + const page = await context.newPage(); + + for (const item of sites) { + const result = await capturePage(page, item, outDir, `site`); + if (result.ok && !result.skipped) total++; + else if (!result.ok) failed++; + else skipped++; + } + + for (const item of galleries) { + const result = await capturePage(page, item, outDir, `gallery`); + if (result.ok && !result.skipped) total++; + else if (!result.ok) failed++; + else skipped++; + } + + await page.close(); + } + + await context.close(); + await browser.close(); + + console.log(`\n✅ Done`); + console.log(` Captured: ${total} Failed: ${failed} Skipped (exists): ${skipped}`); + console.log(` 📁 public/references/\n`); +} + +capture().catch((err) => { + console.error("\n❌ Fatal error:", err.message); + process.exit(1); +}); diff --git a/scripts/check-data.mjs b/scripts/check-data.mjs index 61206db..e525245 100644 --- a/scripts/check-data.mjs +++ b/scripts/check-data.mjs @@ -1,5 +1,5 @@ // scripts/check-data.mjs -import { designStyles, designStyleCategories } from "../src/data/designStyles.ts"; +import { designStyles, designStyleCategories, tunedStyleTokenSlugs } from "../src/data/designStyles.ts"; const errors = []; function assert(cond, msg) { if (!cond) errors.push(msg); } @@ -12,6 +12,11 @@ assert(slugs.size === designStyles.length, "duplicate style slugs found"); assert(designStyleCategories.length >= 10, `expected at least 10 categories, got ${designStyleCategories.length}`); const slugSet = new Set(designStyles.map((s) => s.slug)); +assert(tunedStyleTokenSlugs.length >= 24, `expected at least 24 tuned style token overrides, got ${tunedStyleTokenSlugs.length}`); +for (const slug of tunedStyleTokenSlugs) { + assert(slugSet.has(slug), `tuned style slug "${slug}" does not exist`); +} + for (const s of designStyles) { for (const rel of s.related) { assert(slugSet.has(rel), `style ${s.slug}: related slug "${rel}" does not exist`); @@ -27,6 +32,12 @@ for (const s of designStyles) { assert(s.tokens !== undefined, `style ${s.slug} missing tokens`); assert(["airy","normal","tight"].includes(s.tokens.space.density), `style ${s.slug} bad density: ${s.tokens.space.density}`); assert(typeof s.tokens.typography.weightDisplay === "number", `style ${s.slug} bad weightDisplay`); + assert(typeof s.tokens.typography.headingScale === "number", `style ${s.slug} bad headingScale`); + assert(typeof s.tokens.shape.radius === "string" && s.tokens.shape.radius.length > 0, `style ${s.slug} missing shape.radius`); + assert(typeof s.tokens.shape.radiusPill === "string" && s.tokens.shape.radiusPill.length > 0, `style ${s.slug} missing shape.radiusPill`); + assert(["solid","dashed","double"].includes(s.tokens.shape.borderStyle), `style ${s.slug} bad borderStyle`); + assert(typeof s.tokens.space.gap === "string" && s.tokens.space.gap.length > 0, `style ${s.slug} missing space.gap`); + assert(typeof s.tokens.space.padScale === "number", `style ${s.slug} bad padScale`); assert(s.tokens.color.base === s.palette.base, `style ${s.slug} tokens.color.base mismatch`); } diff --git a/scripts/style-references.json b/scripts/style-references.json new file mode 100644 index 0000000..4132203 --- /dev/null +++ b/scripts/style-references.json @@ -0,0 +1,149 @@ +{ + "_comment": "Reference websites per design style. Entries are split into 'sites' (real brand/product pages) and 'galleries' (curation/inspiration platforms). Used by capture-references.mjs — screenshots are local-only. DO NOT commit public/references/.", + + "minimalism": { + "sites": [ + { "url": "https://linear.app", "title": "Linear", "note": "타이포, 넓은 여백, 모노크롬 — 현대 SaaS 미니멀의 교과서" }, + { "url": "https://stripe.com", "title": "Stripe", "note": "여백·그리드·타이포 비율이 매우 정교한 미니멀 B2B" }, + { "url": "https://www.muji.com", "title": "Muji", "note": "일본식 절제 미니멀, 오프화이트 배경 + 로우 채도" } + ], + "galleries": [ + { "url": "https://www.awwwards.com/websites/minimal/", "title": "Awwwards — Minimal", "note": "수상작 모음 — 타이포/여백 토큰 참고용" }, + { "url": "https://dribbble.com/tags/minimalist-web-design", "title": "Dribbble — Minimalist Web Design" } + ] + }, + + "brutalism": { + "sites": [ + { "url": "https://www.balenciaga.com", "title": "Balenciaga", "note": "패션 브루탈리즘 — 검정 배경, 굵은 산세리프, 각진 레이아웃" }, + { "url": "https://brutalistwebsites.com", "title": "Brutalist Websites", "note": "브루탈리즘 전문 갤러리 사이트 — 실제 예시 풍부" }, + { "url": "https://www.bloomberg.com", "title": "Bloomberg", "note": "에디토리얼 브루탈리즘 — 높은 정보 밀도, 굵은 타이포" } + ], + "galleries": [ + { "url": "https://www.awwwards.com/awwwards/collections/brutalism/", "title": "Awwwards — Brutalism Collection" }, + { "url": "https://dribbble.com/tags/neo-brutalism", "title": "Dribbble — Neo Brutalism" } + ] + }, + + "cyberpunk": { + "sites": [ + { "url": "https://www.cyberpunk.net", "title": "Cyberpunk 2077 Official", "note": "네온, 글리치, 다크 배경 — 장르 정의 사이트" }, + { "url": "https://www.razer.com", "title": "Razer", "note": "그린 네온 + 블랙 — 게이밍 사이버펑크 대표" } + ], + "galleries": [ + { "url": "https://dribbble.com/search/cyberpunk-website", "title": "Dribbble — Cyberpunk Website", "note": "UI 패턴, 네온 컬러, 글리치 효과 참고" }, + { "url": "https://www.behance.net/search/projects/cyberpunk%20website%20design", "title": "Behance — Cyberpunk Website Design" }, + { "url": "https://webflow.com/made-in-webflow/cyberpunk", "title": "Webflow Made-in-Webflow — Cyberpunk" } + ] + }, + + "luxury": { + "sites": [ + { "url": "https://www.loewe.com", "title": "Loewe", "note": "Quiet luxury 교과서 — 세리프, 아이보리, 극도의 여백" }, + { "url": "https://www.bottegaveneta.com", "title": "Bottega Veneta", "note": "이미지 중심, 카피 최소화, 크래프트 고급감" }, + { "url": "https://www.celine.com", "title": "Celine", "note": "감성적 미니멀 럭셔리 — 클로즈업 + 세리프 타이포" } + ], + "galleries": [ + { "url": "https://www.awwwards.com/websites/luxury/", "title": "Awwwards — Luxury", "note": "수상 럭셔리 사이트 — 폰트·여백·색 토큰 참고" }, + { "url": "https://mediaboom.com/news/luxury-fashion-website-design/", "title": "Mediaboom — 50 Luxury Fashion Websites" } + ] + }, + + "organic-design": { + "sites": [ + { "url": "https://www.aesop.com", "title": "Aesop", "note": "Awwwards 수상 — 흙빛 팔레트, 자연 질감, 넓은 여백" }, + { "url": "https://www.kinfolk.com", "title": "Kinfolk", "note": "라이프스타일 매거진 — 따뜻한 사진 + 슬로우 리듬" } + ], + "galleries": [ + { "url": "https://99designs.com/inspiration/websites/organic", "title": "99designs — Organic Websites", "note": "38개 이상 유기적 디자인 사례 — 색·여백 참고" }, + { "url": "https://dribbble.com/tags/organic-design", "title": "Dribbble — Organic Design" }, + { "url": "https://www.siteinspire.com/website/7284-aesop", "title": "Site Inspire — Aesop" } + ] + }, + + "kawaii": { + "sites": [ + { "url": "https://www.sanrio.com", "title": "Sanrio", "note": "카와이 원조 — 핑크, 둥근 모서리, 캐릭터 아이콘" }, + { "url": "https://www.supercutekawaii.com", "title": "Super Cute Kawaii", "note": "카와이 콘텐츠 사이트 — 파스텔, 버블 폰트, 스티커 UI" } + ], + "galleries": [ + { "url": "https://dribbble.com/tags/kawaii-website", "title": "Dribbble — Kawaii Website", "note": "카와이 UI 패턴, 버튼 모양, 일러스트 스타일 참고" }, + { "url": "https://dribbble.com/tags/kawaii-ui", "title": "Dribbble — Kawaii UI" }, + { "url": "https://www.pinterest.com/wixcom/kawaii-japanese-websites/", "title": "Pinterest — Kawaii Japanese Websites (140개 핀)" } + ] + }, + + "streetwear": { + "sites": [ + { "url": "https://www.supremenewyork.com", "title": "Supreme", "note": "스트리트웨어 원형 — 빨간 박스, 산세리프, 직접적 레이아웃" }, + { "url": "https://www.palace.ltd", "title": "Palace Skateboards", "note": "UK 스케이트 에너지 — 그래픽, 타이포, 거친 밀도" }, + { "url": "https://www.stussy.com", "title": "Stüssy", "note": "서프+스케이트 헤리티지 — 그래픽 중심, 레이어드 텍스처" } + ], + "galleries": [ + { "url": "https://dribbble.com/tags/streetwear", "title": "Dribbble — Streetwear" }, + { "url": "https://www.pinterest.com/ideas/supreme-clothing-streetwear/930480009757/", "title": "Pinterest — Supreme / Streetwear" } + ] + }, + + "editorial-design": { + "sites": [ + { "url": "https://www.nytimes.com", "title": "New York Times", "note": "에디토리얼 위계의 정석 — 타이포 스케일, 그리드, 밀도" }, + { "url": "https://monocle.com", "title": "Monocle", "note": "매거진 그리드 — 사진+캡션+세리프 조합 참고" }, + { "url": "https://www.theguardian.com", "title": "The Guardian", "note": "대형 에디토리얼 그리드, 컬러 섹션 구분" } + ], + "galleries": [ + { "url": "https://www.awwwards.com/inspiration/editorial-layout", "title": "Awwwards — Editorial Layout" }, + { "url": "https://www.subframe.com/tips/editorial-website-design-examples", "title": "Subframe — 25 Editorial Website Examples" }, + { "url": "https://www.pinterest.com/alisaaronson/typographic-spreads-publication-design/", "title": "Pinterest — Typographic Spreads" } + ] + }, + + "glassmorphism": { + "sites": [ + { "url": "https://www.apple.com/ios", "title": "Apple iOS", "note": "iOS 26 glassmorphism — 현재 가장 강하게 적용된 실제 제품 UI" }, + { "url": "https://reflect.app", "title": "Reflect", "note": "노트 앱 — 홈페이지 글래스모피즘 구현 잘 됨" } + ], + "galleries": [ + { "url": "https://onepagelove.com/style/glassmorphism", "title": "One Page Love — Glassmorphism", "note": "큐레이션 갤러리 — blur/opacity/shadow 토큰 참고" }, + { "url": "https://webflow.com/made-in-webflow/glassmorphism", "title": "Webflow — Glassmorphism" }, + { "url": "https://dribbble.com/tags/glassmorphism", "title": "Dribbble — Glassmorphism" } + ] + }, + + "y2k": { + "sites": [ + { "url": "https://www.webdesignmuseum.org/exhibitions/y2k-aesthetic-in-web-design", "title": "Web Design Museum — Y2K", "note": "실제 2000년대 초 웹디자인 아카이브 — 색·폰트·레이아웃 원형 확인" }, + { "url": "https://www.blingee.com", "title": "Blingee", "note": "크롬·글리터·버블 Y2K 에너지 살아있는 실제 사이트" } + ], + "galleries": [ + { "url": "https://www.pinterest.com/ideas/y2k-website-design-inspiration/893524089634/", "title": "Pinterest — Y2K Website Design Inspiration", "note": "Y2K 웹디자인 무드보드 — 크롬, 그라디언트, 버블폰트 참고" }, + { "url": "https://www.behance.net/search/projects/y2k%20website", "title": "Behance — Y2K Website Projects" }, + { "url": "https://webflow.com/blog/y2k-aesthetic", "title": "Webflow Blog — Y2K Aesthetic Guide" } + ] + }, + + "maximalism": { + "sites": [ + { "url": "https://www.gucci.com", "title": "Gucci", "note": "패션 맥시멀리즘 — 패턴 레이어, 에클렉틱 타이포, 강한 색" }, + { "url": "https://www.versace.com", "title": "Versace", "note": "그리스 모티프 + 골드 + 볼드 패턴 — 과감한 장식성" }, + { "url": "https://www.libertylondon.com", "title": "Liberty London", "note": "꽃무늬·패턴 맥시멀리즘 — 텍스타일 기반 밀도감" } + ], + "galleries": [ + { "url": "https://onepagelove.com/tag/maximalist", "title": "One Page Love — Maximalist" }, + { "url": "https://dribbble.com/tags/maximalism", "title": "Dribbble — Maximalism" } + ] + }, + + "swiss-design": { + "sites": [ + { "url": "https://www.swissinfo.ch", "title": "Swissinfo", "note": "실제 스위스 정보 포털 — 그리드, 인터내셔널 타이포 시스템" }, + { "url": "https://www.sbb.ch", "title": "SBB 스위스 철도", "note": "기능 중심 그리드 — 스위스 공공 디자인 언어" }, + { "url": "https://www.muji.com", "title": "Muji", "note": "스위스 스타일 응용 — 그리드 질서, 산세리프, 오프화이트" } + ], + "galleries": [ + { "url": "https://dribbble.com/tags/swiss-grid", "title": "Dribbble — Swiss Grid", "note": "그리드 시스템 + 타이포 구성 참고" }, + { "url": "https://www.pinterest.com/ideas/swiss-design/", "title": "Pinterest — Swiss Design" }, + { "url": "https://docs.mew.design/blog/swiss-design-style/", "title": "Mew Design — Swiss Style Guide" } + ] + } +} diff --git a/skills/design-style-recommender/SKILL.md b/skills/design-style-recommender/SKILL.md index e00baae..e9a0fa9 100644 --- a/skills/design-style-recommender/SKILL.md +++ b/skills/design-style-recommender/SKILL.md @@ -12,7 +12,7 @@ Use this skill inside openlayout when a user needs a visual style direction befo - 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}`. +- When recommending a concrete item, include its route: `/styles/{slug}`. ## Recommendation Workflow @@ -39,7 +39,7 @@ Ask a short clarification only when the brand or page goal is unclear. Otherwise Use this format unless the user asks otherwise: ```markdown -추천: [style.nameKo] (`/design-styles/{slug}`) +추천: [style.nameKo] (`/styles/{slug}`) 왜 맞는지: - ... diff --git a/skills/layout-recommender/SKILL.md b/skills/layout-recommender/SKILL.md index 802ab8a..be20dd6 100644 --- a/skills/layout-recommender/SKILL.md +++ b/skills/layout-recommender/SKILL.md @@ -12,7 +12,7 @@ Use this skill inside the openlayout project to recommend layouts that match a p - Layout data lives in `src/data/webLayouts.ts`. - Prefer existing `webLayouts` entries before inventing a new layout. - Match by `category`, `bestFor`, `notGoodFor`, `tags`, `previewType`, and `complexity`. -- When recommending a concrete item, include its route: `/web-layouts/{slug}`. +- When recommending a concrete item, include its route: `/layouts/{slug}`. ## Recommendation Workflow @@ -39,7 +39,7 @@ Ask a short clarification only when the page goal is unclear. Otherwise infer co Use this format unless the user asks otherwise: ```markdown -추천: [layout.nameKo] (`/web-layouts/{slug}`) +추천: [layout.nameKo] (`/layouts/{slug}`) 왜 맞는지: - ... diff --git a/src/app/components/page.tsx b/src/app/components/page.tsx new file mode 100644 index 0000000..07d57be --- /dev/null +++ b/src/app/components/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import { ComponentDictionaryView } from "@/components/component-dictionary/ComponentDictionaryView"; + +export const metadata: Metadata = { + title: "Components | openlayout", + description: "Preview UI components with design style tokens.", +}; + +export default function ComponentsPage() { + return ( +
+
+

Component Dictionary

+

+ Components +

+
+ +
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 3d789b2..eb40a03 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -491,12 +491,13 @@ main header { .st-card { background-color: var(--st-surface, var(--style-surface, #F0EEE8)); border-radius: var(--st-radius, 2px); - border: var(--st-border-width, 1px) solid rgb(var(--st-border-rgb, 30 30 30) / 0.2); + border: var(--st-border-width, 1px) var(--st-border-style, solid) rgb(var(--st-border-rgb, 30 30 30) / 0.2); box-shadow: var(--st-shadow, none); } .st-display { font-family: var(--st-font-display, var(--font-display)); + font-size: calc(var(--st-display-size, 1em) * var(--st-heading-scale, 1)); font-weight: var(--st-weight-display, 700); letter-spacing: var(--st-tracking, -0.05em); } @@ -510,6 +511,20 @@ main header { color: var(--st-accent, var(--style-accent, #DB4A2B)); } +.st-pad { + padding: calc(1rem * var(--st-pad-scale, 1)); +} + +.st-gap { + gap: var(--st-gap, 0.75rem); +} + +.st-border { + border-width: var(--st-border-width, 1px); + border-style: var(--st-border-style, solid); + border-color: rgb(var(--st-border-rgb, 30 30 30) / 0.22); +} + /* ─── Effect layers ────────────────────────────────────────────────────────── */ /* Glow: neon/cyber glow on cards */ @@ -566,14 +581,21 @@ main header { } /* Density modifiers */ -[data-st-density="tight"] .raw-preview-canvas { +[data-st-density="tight"] .raw-preview-canvas, +[data-st-density="tight"] .raw-wireframe { --internal-gap-scale: 0.7; } -[data-st-density="airy"] .raw-preview-canvas { +[data-st-density="airy"] .raw-preview-canvas, +[data-st-density="airy"] .raw-wireframe { --internal-gap-scale: 1.3; } +.raw-preview-canvas .st-density-gap, +.raw-wireframe .st-density-gap { + gap: calc(var(--internal-gap-scale, 1) * 1rem); +} + /* ─── Reduced motion overrides ─────────────────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 01ea5fa..8a99402 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -43,16 +43,18 @@ function RawNavigation() {
Openlayout
{[ - ["Library", "/web-layouts"], - ["Styles", "/design-styles"], - ["Compare", "/web-layouts/compare"], - ["Skill", "/web-layouts#layout-skill"], + ["Library", "/layouts"], + ["Styles", "/styles"], + ["Studio", "/studio"], + ["Components", "/components"], + ["Compare", "/layouts/compare"], + ["Skill", "/layouts#layout-skill"], ].map(([label, href]) => ( @@ -107,18 +109,20 @@ function RawFooter() {
diff --git a/src/app/web-layouts/[slug]/page.tsx b/src/app/layouts/[slug]/page.tsx similarity index 93% rename from src/app/web-layouts/[slug]/page.tsx rename to src/app/layouts/[slug]/page.tsx index 044adc3..9360075 100644 --- a/src/app/web-layouts/[slug]/page.tsx +++ b/src/app/layouts/[slug]/page.tsx @@ -1,11 +1,13 @@ import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; +import { CopyTextButton } from "@/components/export/CopyTextButton"; import { Badge } from "@/components/ui/badge"; import { LayoutCodeExample } from "@/components/web-layout/LayoutCodeExample"; import { LayoutStagePreview } from "@/components/web-layout/LayoutStagePreview"; import { RelatedLayouts } from "@/components/web-layout/RelatedLayouts"; import { webLayouts, getLayoutBySlug, type WebLayout } from "@/data/webLayouts"; +import { exportLayoutPrompt } from "@/lib/exportPrompt"; import { complexityTone, formatComplexity } from "@/lib/utils"; type LayoutDetailPageProps = { @@ -47,7 +49,7 @@ export default async function LayoutDetailPage({ params }: LayoutDetailPageProps
목록으로 돌아가기 @@ -81,6 +83,13 @@ export default async function LayoutDetailPage({ params }: LayoutDetailPageProps

{layout.description}

+
+ +
diff --git a/src/app/web-layouts/compare/page.tsx b/src/app/layouts/compare/page.tsx similarity index 97% rename from src/app/web-layouts/compare/page.tsx rename to src/app/layouts/compare/page.tsx index 0abb6e1..63c0517 100644 --- a/src/app/web-layouts/compare/page.tsx +++ b/src/app/layouts/compare/page.tsx @@ -13,7 +13,7 @@ export default function ComparePage() {
목록으로 돌아가기 diff --git a/src/app/web-layouts/page.tsx b/src/app/layouts/page.tsx similarity index 98% rename from src/app/web-layouts/page.tsx rename to src/app/layouts/page.tsx index 12a682b..5c156c4 100644 --- a/src/app/web-layouts/page.tsx +++ b/src/app/layouts/page.tsx @@ -43,7 +43,7 @@ export default function WebLayoutsPage() { Compare now @@ -97,7 +97,7 @@ export default function WebLayoutsPage() { ].map(([index, label]) => ( {index} diff --git a/src/app/page.tsx b/src/app/page.tsx index b45bf65..cba7f5d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function Home() { - redirect("/web-layouts"); + redirect("/layouts"); } diff --git a/src/app/studio/page.tsx b/src/app/studio/page.tsx new file mode 100644 index 0000000..4b63c3b --- /dev/null +++ b/src/app/studio/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import { StudioView } from "@/components/studio/StudioView"; + +export const metadata: Metadata = { + title: "Studio | openlayout", + description: "Combine a design style with a layout and preview the result.", +}; + +export default function StudioPage() { + return ; +} diff --git a/src/app/design-styles/[slug]/page.tsx b/src/app/styles/[slug]/page.tsx similarity index 92% rename from src/app/design-styles/[slug]/page.tsx rename to src/app/styles/[slug]/page.tsx index 42f9b2b..f38b85f 100644 --- a/src/app/design-styles/[slug]/page.tsx +++ b/src/app/styles/[slug]/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import Link from "next/link"; import { designStyles, getDesignStyleBySlug } from "@/data/designStyles"; +import { CopyTextButton } from "@/components/export/CopyTextButton"; import { ColorPaletteGrid } from "@/components/design-style/ColorPaletteGrid"; import { DesignStyleDetailSection } from "@/components/design-style/DesignStyleDetailSection"; import { DesignStyleSampleRenderer } from "@/components/design-style/DesignStyleSampleRenderer"; @@ -46,7 +47,7 @@ export default async function DesignStyleDetailPage({ return (
- + 목록으로 돌아가기 @@ -94,6 +95,13 @@ export default async function DesignStyleDetailPage({
+
+ +
               {style.imagePrompt}
             
diff --git a/src/app/design-styles/generate/page.tsx b/src/app/styles/generate/page.tsx similarity index 96% rename from src/app/design-styles/generate/page.tsx rename to src/app/styles/generate/page.tsx index 1cb4273..d01eb26 100644 --- a/src/app/design-styles/generate/page.tsx +++ b/src/app/styles/generate/page.tsx @@ -11,7 +11,7 @@ export default function DesignStyleGeneratePage() { return (
- + 디자인 형식 목록
diff --git a/src/app/design-styles/page.tsx b/src/app/styles/page.tsx similarity index 97% rename from src/app/design-styles/page.tsx rename to src/app/styles/page.tsx index 72196f6..bcb09a3 100644 --- a/src/app/design-styles/page.tsx +++ b/src/app/styles/page.tsx @@ -33,13 +33,13 @@ export default function DesignStylesPage() {
레이아웃으로 돌아가기 비교에서 보기 diff --git a/src/components/component-dictionary/ComponentDictionaryView.tsx b/src/components/component-dictionary/ComponentDictionaryView.tsx new file mode 100644 index 0000000..263f945 --- /dev/null +++ b/src/components/component-dictionary/ComponentDictionaryView.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { ComponentPreviewRenderer } from "@/components/component-dictionary/ComponentPreviewRenderer"; +import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; +import { componentSpecs } from "@/data/componentSpecs"; +import { designStyles } from "@/data/designStyles"; + +export function ComponentDictionaryView() { + const [styleSlug, setStyleSlug] = useState("brutalism"); + const [componentSlug, setComponentSlug] = useState("button"); + + const style = useMemo( + () => designStyles.find((item) => item.slug === styleSlug) ?? designStyles[0], + [styleSlug], + ); + const component = useMemo( + () => componentSpecs.find((item) => item.slug === componentSlug) ?? componentSpecs[0], + [componentSlug], + ); + + return ( +
+ + +
+

+ {style.nameKo} x {component.nameKo} +

+
+ +
+

+ {component.summary} +

+
+
+ ); +} diff --git a/src/components/component-dictionary/ComponentPreviewRenderer.tsx b/src/components/component-dictionary/ComponentPreviewRenderer.tsx new file mode 100644 index 0000000..05f02e6 --- /dev/null +++ b/src/components/component-dictionary/ComponentPreviewRenderer.tsx @@ -0,0 +1,70 @@ +import type { CSSProperties } from "react"; +import type { ComponentSpec } from "@/data/componentSpecs"; + +export function ComponentPreviewRenderer({ component }: { component: ComponentSpec }) { + if (component.type === "button") { + return ( + + ); + } + + if (component.type === "card") { + return ( +
+

+ Card title +

+

+ A reusable surface that follows the selected style tokens. +

+
+ ); + } + + if (component.type === "nav") { + return ( + + ); + } + + if (component.type === "input") { + return ( + + ); + } + + return ( + + Status badge + + ); +} diff --git a/src/components/design-style/DesignStyleCard.tsx b/src/components/design-style/DesignStyleCard.tsx index 82b91df..79aa30f 100644 --- a/src/components/design-style/DesignStyleCard.tsx +++ b/src/components/design-style/DesignStyleCard.tsx @@ -84,7 +84,7 @@ export function DesignStyleCard({ isSelected, onSelect, style }: Props) { ? "border-[#E4E2DD]/30 text-[#E4E2DD] hover:bg-[#E4E2DD] hover:text-[#1E1E1E]" : "border-[#1E1E1E]/25 text-[#1E1E1E] hover:border-[#1E1E1E]", )} - href={`/design-styles/${style.slug}`} + href={`/styles/${style.slug}`} > 자세히 diff --git a/src/components/design-style/DesignStyleSampleRenderer.tsx b/src/components/design-style/DesignStyleSampleRenderer.tsx index 6c3fa72..675cbeb 100644 --- a/src/components/design-style/DesignStyleSampleRenderer.tsx +++ b/src/components/design-style/DesignStyleSampleRenderer.tsx @@ -1,5 +1,6 @@ import type { CSSProperties, ReactNode } from "react"; import type { DesignStyle } from "@/data/designStyles"; +import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; import { cn } from "@/lib/utils"; type Props = { @@ -8,7 +9,7 @@ type Props = { style: DesignStyle; }; -type SampleVariables = CSSProperties & Record<`--sample-${string}`, string>; +type SampleVariables = CSSProperties & Record<`--sample-${string}`, string> & Record<`--st-${string}`, string>; function withAlpha(color: string, alpha: string) { return /^#[\da-f]{6}$/i.test(color) ? `${color}${alpha}` : color; @@ -28,6 +29,7 @@ function sampleVariables(style: DesignStyle): SampleVariables { "--sample-primary": palette.primary, "--sample-surface": palette.surface, "--sample-text": palette.text, + ...styleTokenVars(style), }; } @@ -40,8 +42,8 @@ function SampleFrame({ return (
- +
+ Studio
@@ -84,7 +89,10 @@ function MinimalEditorial({ compact = false, style }: Props) {

{style.category}

-

+

{style.nameEn}

@@ -94,10 +102,10 @@ function MinimalEditorial({ compact = false, style }: Props) {
-
+
-

+

{style.summary}

@@ -117,12 +125,15 @@ function BrutalistPoster({ compact = false, style }: Props) {

Raw format

-

+

{style.nameEn}

-

+

{style.summary}

@@ -140,12 +151,15 @@ function RetroCommerce({ compact = false, style }: Props) {
-
+
New Drop
{[style.palette.accent, style.palette.accent2, style.palette.accent3].map((color, index) => ( -
+
@@ -175,8 +189,11 @@ function CyberDashboard({ compact = false, style }: Props) {
))}
-
-

+
+

{style.nameEn}

@@ -199,14 +216,17 @@ function LuxuryProduct({ compact = false, style }: Props) {

Atelier edition

-

+

{style.nameEn}

-
+
@@ -223,7 +243,10 @@ function OrganicBrand({ compact = false, style }: Props) {

Natural system

-

+

{style.nameEn}

@@ -245,11 +268,14 @@ function KawaiiApp({ compact = false, style }: Props) {
-
+
-

+

{style.nameEn}

daily app

@@ -273,8 +299,11 @@ function StreetCampaign({ compact = false, style }: Props) {
-
-

+
+

{style.nameEn}

@@ -293,7 +322,10 @@ function MagazineLayout({ compact = false, style }: Props) {

Issue 06

-

+

{style.nameEn}

@@ -321,14 +353,17 @@ function SaasLanding({ compact = false, style }: Props) {

Product system

-

+

{style.nameEn}

-
+
{[style.palette.accent, style.palette.accent2, style.palette.accent3].map((color, index) => ( -
+
0{index + 1}
diff --git a/src/components/export/CopyTextButton.tsx b/src/components/export/CopyTextButton.tsx new file mode 100644 index 0000000..c831c0a --- /dev/null +++ b/src/components/export/CopyTextButton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; + +type CopyTextButtonProps = { + copiedLabel: string; + idleLabel: string; + text: string; +}; + +export function CopyTextButton({ copiedLabel, idleLabel, text }: CopyTextButtonProps) { + const [copied, setCopied] = useState(false); + + async function copy() { + await navigator.clipboard.writeText(text); + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + } + + return ( + + ); +} diff --git a/src/components/studio/StudioView.tsx b/src/components/studio/StudioView.tsx new file mode 100644 index 0000000..6cf119c --- /dev/null +++ b/src/components/studio/StudioView.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useMemo, useState } from "react"; +import { designStyles } from "@/data/designStyles"; +import { webLayouts } from "@/data/webLayouts"; +import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; +import { LayoutPreviewRenderer } from "@/components/web-layout/LayoutPreviewRenderer"; +import { exportDesignCode } from "@/lib/exportCode"; +import { exportDesignPrompt } from "@/lib/exportPrompt"; +import type { PreviewViewport } from "@/components/web-layout/ViewportSwitcher"; + +const DEFAULT_STYLE = "brutalism"; +const DEFAULT_LAYOUT = "hero-focused-layout"; + +function StudioViewInner() { + const router = useRouter(); + const params = useSearchParams(); + const [copied, setCopied] = useState<"code" | "prompt" | null>(null); + + const requestedStyleSlug = params.get("style") ?? DEFAULT_STYLE; + const requestedLayoutSlug = params.get("layout") ?? DEFAULT_LAYOUT; + const viewport = (params.get("vp") === "mobile" ? "mobile" : "desktop") as PreviewViewport; + + const selectedStyle = useMemo( + () => + designStyles.find((s) => s.slug === requestedStyleSlug) ?? + designStyles.find((s) => s.slug === DEFAULT_STYLE) ?? + designStyles[0], + [requestedStyleSlug], + ); + + const selectedLayout = useMemo( + () => + webLayouts.find((l) => l.slug === requestedLayoutSlug) ?? + webLayouts.find((l) => l.slug === DEFAULT_LAYOUT) ?? + webLayouts[0], + [requestedLayoutSlug], + ); + + const selectedStyleSlug = selectedStyle.slug; + const selectedLayoutSlug = selectedLayout.slug; + + useEffect(() => { + const next = new URLSearchParams(params.toString()); + let changed = false; + + if (next.get("style") !== selectedStyleSlug) { + next.set("style", selectedStyleSlug); + changed = true; + } + + if (next.get("layout") !== selectedLayoutSlug) { + next.set("layout", selectedLayoutSlug); + changed = true; + } + + if (next.get("vp") !== viewport) { + next.set("vp", viewport); + changed = true; + } + + if (changed) router.replace(`/studio?${next.toString()}`); + }, [params, router, selectedLayoutSlug, selectedStyleSlug, viewport]); + + function update(key: string, value: string) { + const next = new URLSearchParams(params.toString()); + next.set(key, value); + router.replace(`/studio?${next.toString()}`); + } + + const tokenVars = useMemo(() => styleTokenVars(selectedStyle), [selectedStyle]); + + async function copyPrompt() { + await navigator.clipboard.writeText(exportDesignPrompt(selectedStyle, selectedLayout)); + setCopied("prompt"); + window.setTimeout(() => setCopied(null), 1400); + } + + async function copyCode() { + await navigator.clipboard.writeText(exportDesignCode(selectedStyle, selectedLayout)); + setCopied("code"); + window.setTimeout(() => setCopied(null), 1400); + } + + return ( +
+
+ + {/* Header */} +
+

Studio

+

+ Style × Layout +

+

+ 스타일과 레이아웃을 골라 조합된 웹을 미리 봅니다. +

+
+ +
+ + {/* Left: Controls */} + + + {/* Right: Live preview */} +
+
+ +
+

+ {selectedStyle.nameKo} × {selectedLayout.nameKo} · URL로 공유 가능 +

+
+ +
+
+
+ ); +} + +export function StudioView() { + return ( + + + + ); +} diff --git a/src/components/style-preset/AppliedStyleStrip.tsx b/src/components/style-preset/AppliedStyleStrip.tsx index aa1e02f..3ef417b 100644 --- a/src/components/style-preset/AppliedStyleStrip.tsx +++ b/src/components/style-preset/AppliedStyleStrip.tsx @@ -35,7 +35,7 @@ export function AppliedStyleStrip() {
형식 바꾸기 diff --git a/src/components/style-preset/styleTokenVars.ts b/src/components/style-preset/styleTokenVars.ts index 8bfefa5..2903322 100644 --- a/src/components/style-preset/styleTokenVars.ts +++ b/src/components/style-preset/styleTokenVars.ts @@ -42,7 +42,9 @@ export function styleTokenVars(style: DesignStyle): CSSProperties { "--st-heading-scale": String(typography.headingScale), // Shape tokens "--st-radius": shape.radius, + "--st-radius-pill": shape.radiusPill, "--st-border-width": shape.borderWidth, + "--st-border-style": shape.borderStyle, // Space tokens "--st-gap": space.gap, "--st-pad-scale": String(space.padScale), diff --git a/src/components/web-layout/LayoutPreviewRenderer.tsx b/src/components/web-layout/LayoutPreviewRenderer.tsx index 80f900b..cd3a4ce 100644 --- a/src/components/web-layout/LayoutPreviewRenderer.tsx +++ b/src/components/web-layout/LayoutPreviewRenderer.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; import type { WebLayout } from "@/data/webLayouts"; import { AnnotatedRegion } from "@/components/web-layout/LayoutAnnotations"; import type { PreviewViewport } from "@/components/web-layout/ViewportSwitcher"; @@ -36,11 +36,12 @@ function Region({ return ( {children} @@ -49,23 +50,26 @@ function Region({ function SampleHeader({ compact = false }: { compact?: boolean }) { return ( -
+
- - + + Raw Co.
{compact ? ( - + Menu ) : ( -
@@ -74,7 +78,7 @@ function SampleHeader({ compact = false }: { compact?: boolean }) { function RawLabel({ children, className }: { children: ReactNode; className?: string }) { return ( -

+

{children}

); @@ -92,10 +96,14 @@ function RawHeading({ return (

{children}

@@ -106,11 +114,12 @@ function RawButton({ children, tone = "dark" }: { children: ReactNode; tone?: "d return ( ))}
- @@ -562,21 +663,29 @@ function DocsSample({ layout, compact, showLabels }: SampleProps) { Docs
{["Start", "Structure", "Patterns", "Code"].map((item) => ( -

{item}

+

{item}

))}
{layout.nameEn} Build notes -

{layout.summary}

-
+        

+ {layout.summary} +

+
           {`
\n ${layout.previewType}\n
`}
Index -
+
{layout.structure.slice(0, 4).map((item) =>

{item}

)}
@@ -594,15 +703,24 @@ function FeedSample({ layout, compact, showLabels, denseContent }: SampleProps)
{Array.from({ length: denseContent ? 6 : 4 }).map((_, index) => ( -
+
- +

Layout note {index + 1}

-

{layout.nameEn}

+

{layout.nameEn}

-

{layout.summary}

+

+ {layout.summary} +

))}
@@ -615,21 +733,21 @@ function MapListSample({ layout, compact, showLabels }: SampleProps) { return (
{[20, 44, 66, 78].map((left, index) => ( @@ -639,9 +757,13 @@ function MapListSample({ layout, compact, showLabels }: SampleProps) { {layout.nameEn}
{layout.bestFor.slice(0, 4).map((item, index) => ( -
+

{item}

-

Zone 0{index + 1}

+

Zone 0{index + 1}

))}
@@ -664,16 +786,20 @@ function ComparisonSample({ layout, compact, showLabels }: SampleProps) { className={cn( "border p-4", index === 1 - ? "border-[var(--style-primary)] bg-[var(--style-primary)] text-[var(--style-surface)]" - : "border-[rgb(var(--style-border-rgb)_/_0.16)] bg-[rgb(var(--style-surface-rgb)_/_0.78)] text-[var(--style-text)]", + ? "border-[var(--st-primary)] bg-[var(--st-primary)] text-[var(--st-surface)]" + : "border-[rgb(var(--st-border-rgb)_/_0.16)] bg-[rgb(var(--st-surface-rgb)_/_0.78)] text-[var(--st-text)]", )} key={item} + style={{ borderRadius: "var(--st-radius)", borderWidth: "var(--st-border-width)", boxShadow: "var(--st-shadow)" }} >

{item}

-

+

{index === 1 ? "Fit" : "Raw"}

-
+
{layout.bestFor.slice(0, 3).map((purpose) =>

{purpose}

)}
@@ -689,17 +815,20 @@ function TimelineSample({ layout, compact, showLabels }: SampleProps) { {layout.nameEn} Process line -
    +
      {stepNames.map((item, index) => (
    1. {item}

      -

      +

      {index === 0 ? layout.summary : layout.responsiveBehavior[index - 1]}

    2. @@ -713,25 +842,47 @@ function ScrollStorySample({ layout, compact, showLabels }: SampleProps) { return (
      - Scene 01 -

      + Scene 01 +

      Scroll
      story

      -

      {layout.summary}

      -
      +

      + {layout.summary} +

      +
      {layout.structure.slice(0, 4).map((item, index) => ( -
      -

      0{index + 1}

      -

      {item}

      +
      +

      0{index + 1}

      +

      + {item} +

      ))}
      diff --git a/src/components/web-layout/WebLayoutCard.tsx b/src/components/web-layout/WebLayoutCard.tsx index b7b514a..ae92269 100644 --- a/src/components/web-layout/WebLayoutCard.tsx +++ b/src/components/web-layout/WebLayoutCard.tsx @@ -57,7 +57,7 @@ export function WebLayoutCard({ layout, compact = false }: WebLayoutCardProps) { ))}
      상세 보기 diff --git a/src/components/web-layout/WebLayoutCompare.tsx b/src/components/web-layout/WebLayoutCompare.tsx index 3f71787..bf5310a 100644 --- a/src/components/web-layout/WebLayoutCompare.tsx +++ b/src/components/web-layout/WebLayoutCompare.tsx @@ -77,7 +77,7 @@ export function WebLayoutCompare() { data-testid="layout-stage" > diff --git a/src/components/web-layout/WebLayoutExplorer.tsx b/src/components/web-layout/WebLayoutExplorer.tsx index c1161ad..836724d 100644 --- a/src/components/web-layout/WebLayoutExplorer.tsx +++ b/src/components/web-layout/WebLayoutExplorer.tsx @@ -67,7 +67,7 @@ export function WebLayoutExplorer() {

      레이아웃 비교하기 diff --git a/src/data/componentSpecs.ts b/src/data/componentSpecs.ts new file mode 100644 index 0000000..20d9438 --- /dev/null +++ b/src/data/componentSpecs.ts @@ -0,0 +1,47 @@ +export type ComponentSpecType = "button" | "card" | "nav" | "input" | "badge"; + +export type ComponentSpec = { + slug: string; + nameKo: string; + nameEn: string; + type: ComponentSpecType; + summary: string; +}; + +export const componentSpecs: ComponentSpec[] = [ + { + slug: "button", + nameKo: "버튼", + nameEn: "Button", + type: "button", + summary: "주요 행동을 유도하는 클릭 요소입니다.", + }, + { + slug: "card", + nameKo: "카드", + nameEn: "Card", + type: "card", + summary: "반복 콘텐츠를 묶어 비교 가능하게 보여줍니다.", + }, + { + slug: "nav", + nameKo: "내비게이션", + nameEn: "Navigation", + type: "nav", + summary: "페이지 이동과 현재 구조를 보여줍니다.", + }, + { + slug: "input", + nameKo: "입력 필드", + nameEn: "Input", + type: "input", + summary: "검색과 폼 입력을 받는 기본 컨트롤입니다.", + }, + { + slug: "badge", + nameKo: "배지", + nameEn: "Badge", + type: "badge", + summary: "상태, 카테고리, 짧은 메타 정보를 표시합니다.", + }, +]; diff --git a/src/data/designStyles.ts b/src/data/designStyles.ts index e1816b2..a785e09 100644 --- a/src/data/designStyles.ts +++ b/src/data/designStyles.ts @@ -40,6 +40,7 @@ export type StyleTokens = { }; shape: { radius: string; + radiusPill: string; borderWidth: string; borderStyle: "solid" | "dashed" | "double"; }; @@ -223,7 +224,7 @@ const categoryTokenDefaults: Record = { "모던 / 미니멀": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 600, weightBody: 400, tracking: "-0.02em", headingScale: 1.0 }, - shape: { radius: "2px", borderWidth: "1px", borderStyle: "solid" }, + shape: { radius: "2px", radiusPill: "2px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "airy", gap: "1rem", padScale: 1.2 }, decoration: { shadow: "none", effect: "none" }, layout: { heroVariant: "left", navStyle: "minimal", alignment: "left" }, @@ -231,7 +232,7 @@ const categoryTokenDefaults: Record = { "강렬 / 실험": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 800, weightBody: 500, tracking: "-0.05em", headingScale: 1.25 }, - shape: { radius: "0px", borderWidth: "3px", borderStyle: "solid" }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "3px", borderStyle: "solid" }, space: { density: "tight", gap: "0.5rem", padScale: 0.9 }, decoration: { shadow: "6px 6px 0 var(--st-primary)", effect: "none" }, layout: { heroVariant: "split", navStyle: "boxed", alignment: "left" }, @@ -239,7 +240,7 @@ const categoryTokenDefaults: Record = { "레트로 / 빈티지": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 700, weightBody: 400, tracking: "-0.01em", headingScale: 1.1 }, - shape: { radius: "4px", borderWidth: "2px", borderStyle: "solid" }, + shape: { radius: "4px", radiusPill: "4px", borderWidth: "2px", borderStyle: "solid" }, space: { density: "normal", gap: "0.75rem", padScale: 1.0 }, decoration: { shadow: "3px 3px 0 var(--st-primary)", effect: "grain" }, layout: { heroVariant: "center", navStyle: "boxed", alignment: "center" }, @@ -247,7 +248,7 @@ const categoryTokenDefaults: Record = { "미래 / 디지털": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"SFMono-Regular", monospace', weightDisplay: 700, weightBody: 400, tracking: "0em", headingScale: 1.1 }, - shape: { radius: "4px", borderWidth: "1px", borderStyle: "solid" }, + shape: { radius: "4px", radiusPill: "4px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "normal", gap: "0.75rem", padScale: 1.0 }, decoration: { shadow: "0 0 18px rgb(var(--st-accent-rgb) / 0.5)", effect: "glow" }, layout: { heroVariant: "center", navStyle: "underline", alignment: "left" }, @@ -255,7 +256,7 @@ const categoryTokenDefaults: Record = { "럭셔리 / 클래식": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Georgia", "Times New Roman", serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 400, weightBody: 300, tracking: "0.08em", headingScale: 0.9 }, - shape: { radius: "0px", borderWidth: "1px", borderStyle: "solid" }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "airy", gap: "1.5rem", padScale: 1.5 }, decoration: { shadow: "none", effect: "none" }, layout: { heroVariant: "center", navStyle: "minimal", alignment: "center" }, @@ -263,7 +264,7 @@ const categoryTokenDefaults: Record = { "자연 / 수공예": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Satoshi", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 600, weightBody: 400, tracking: "0.01em", headingScale: 1.0 }, - shape: { radius: "8px", borderWidth: "1px", borderStyle: "solid" }, + shape: { radius: "8px", radiusPill: "8px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "airy", gap: "1.25rem", padScale: 1.3 }, decoration: { shadow: "none", effect: "grain" }, layout: { heroVariant: "left", navStyle: "minimal", alignment: "left" }, @@ -271,7 +272,7 @@ const categoryTokenDefaults: Record = { "귀여움 / 캐주얼": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 700, weightBody: 400, tracking: "-0.01em", headingScale: 1.05 }, - shape: { radius: "9999px", borderWidth: "2px", borderStyle: "solid" }, + shape: { radius: "20px", radiusPill: "9999px", borderWidth: "2px", borderStyle: "solid" }, space: { density: "normal", gap: "0.75rem", padScale: 1.0 }, decoration: { shadow: "4px 4px 0 var(--st-accent)", effect: "none" }, layout: { heroVariant: "center", navStyle: "boxed", alignment: "center" }, @@ -279,7 +280,7 @@ const categoryTokenDefaults: Record = { "스트리트 / 서브컬처": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 900, weightBody: 500, tracking: "-0.04em", headingScale: 1.3 }, - shape: { radius: "0px", borderWidth: "3px", borderStyle: "solid" }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "3px", borderStyle: "solid" }, space: { density: "tight", gap: "0.5rem", padScale: 0.85 }, decoration: { shadow: "4px 4px 0 var(--st-accent)", effect: "none" }, layout: { heroVariant: "split", navStyle: "boxed", alignment: "left" }, @@ -287,7 +288,7 @@ const categoryTokenDefaults: Record = { "편집 / 타이포그래피": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Clash Display", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 700, weightBody: 400, tracking: "-0.03em", headingScale: 1.15 }, - shape: { radius: "0px", borderWidth: "2px", borderStyle: "solid" }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "2px", borderStyle: "solid" }, space: { density: "normal", gap: "0.75rem", padScale: 1.0 }, decoration: { shadow: "none", effect: "none" }, layout: { heroVariant: "left", navStyle: "underline", alignment: "left" }, @@ -295,7 +296,7 @@ const categoryTokenDefaults: Record = { "UI / 웹": { color: { base: "", surface: "", text: "", muted: "", primary: "", accent: "", accent2: "", accent3: "", border: "" }, typography: { displayFont: '"Satoshi", sans-serif', bodyFont: '"Satoshi", sans-serif', weightDisplay: 700, weightBody: 400, tracking: "-0.01em", headingScale: 1.0 }, - shape: { radius: "8px", borderWidth: "1px", borderStyle: "solid" }, + shape: { radius: "8px", radiusPill: "8px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "normal", gap: "0.75rem", padScale: 1.0 }, decoration: { shadow: "0 2px 8px rgba(0,0,0,0.12)", effect: "none" }, layout: { heroVariant: "center", navStyle: "minimal", alignment: "left" }, @@ -572,18 +573,54 @@ const styleTokenOverrides: Record shape: { radius: "0px", borderWidth: "1px" }, decoration: { shadow: "none", effect: "none" }, }, + "modernism": { + typography: { weightDisplay: 700, tracking: "-0.03em", headingScale: 1.08 }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "1px" }, + space: { density: "airy", gap: "1.1rem", padScale: 1.25 }, + layout: { heroVariant: "left", navStyle: "underline", alignment: "left" }, + }, + "warm-minimal": { + typography: { weightDisplay: 500, weightBody: 400, tracking: "-0.01em", headingScale: 0.96 }, + shape: { radius: "10px", radiusPill: "9999px", borderWidth: "1px" }, + space: { density: "airy", gap: "1.4rem", padScale: 1.45 }, + decoration: { shadow: "0 10px 30px rgb(var(--st-text-rgb) / 0.08)", effect: "none" }, + }, "brutalism": { typography: { weightDisplay: 900, tracking: "-0.05em" }, shape: { radius: "0px", borderWidth: "4px" }, decoration: { shadow: "6px 6px 0 var(--st-primary)", effect: "none" }, }, + "new-brutalism": { + typography: { weightDisplay: 900, weightBody: 600, tracking: "-0.06em", headingScale: 1.32 }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "5px" }, + space: { density: "tight", gap: "0.35rem", padScale: 0.78 }, + decoration: { shadow: "8px 8px 0 var(--st-accent)", effect: "none" }, + }, + "glitch-art": { + typography: { bodyFont: '"SFMono-Regular", monospace', weightDisplay: 800, tracking: "0.02em", headingScale: 1.22 }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "2px", borderStyle: "dashed" }, + space: { density: "tight", gap: "0.45rem", padScale: 0.82 }, + decoration: { shadow: "3px 0 0 var(--st-accent), -3px 0 0 var(--st-accent-2)", effect: "glitch" }, + }, "cyberpunk": { typography: { bodyFont: '"SFMono-Regular", monospace', tracking: "0.02em" }, shape: { radius: "2px", borderWidth: "1px" }, decoration: { shadow: "0 0 24px rgb(var(--st-accent-rgb) / 0.6)", effect: "glow" }, }, + "retro-futurism": { + typography: { bodyFont: '"SFMono-Regular", monospace', weightDisplay: 800, tracking: "0.03em", headingScale: 1.12 }, + shape: { radius: "14px", radiusPill: "9999px", borderWidth: "2px" }, + space: { density: "normal", gap: "0.9rem", padScale: 1.08 }, + decoration: { shadow: "0 0 22px rgb(var(--st-accent-rgb) / 0.45)", effect: "gradient" }, + }, + "neon-noir": { + typography: { bodyFont: '"SFMono-Regular", monospace', weightDisplay: 700, tracking: "0.04em", headingScale: 1.08 }, + shape: { radius: "2px", radiusPill: "2px", borderWidth: "1px" }, + space: { density: "tight", gap: "0.55rem", padScale: 0.9 }, + decoration: { shadow: "0 0 28px rgb(var(--st-accent-rgb) / 0.7)", effect: "scanline" }, + }, "kawaii": { - shape: { radius: "9999px", borderWidth: "2px" }, + shape: { radius: "20px", radiusPill: "9999px", borderWidth: "2px" }, decoration: { shadow: "4px 4px 0 var(--st-accent)", effect: "none" }, }, "luxury": { @@ -592,29 +629,65 @@ const styleTokenOverrides: Record space: { density: "airy", gap: "2rem", padScale: 1.8 }, decoration: { shadow: "none", effect: "none" }, }, + "old-money": { + typography: { displayFont: '"Georgia", "Times New Roman", serif', weightDisplay: 400, weightBody: 300, tracking: "0.06em", headingScale: 0.92 }, + shape: { radius: "2px", radiusPill: "2px", borderWidth: "1px" }, + space: { density: "airy", gap: "1.8rem", padScale: 1.65 }, + decoration: { shadow: "0 14px 36px rgb(var(--st-text-rgb) / 0.10)", effect: "none" }, + }, "organic-design": { shape: { radius: "12px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "airy", gap: "1.25rem", padScale: 1.3 }, decoration: { shadow: "none", effect: "grain" }, }, + "wabi-sabi": { + typography: { displayFont: '"Georgia", "Times New Roman", serif', weightDisplay: 400, tracking: "0.02em", headingScale: 0.9 }, + shape: { radius: "6px", radiusPill: "12px", borderWidth: "1px", borderStyle: "dashed" }, + space: { density: "airy", gap: "1.6rem", padScale: 1.55 }, + decoration: { shadow: "none", effect: "grain" }, + }, "streetwear": { typography: { weightDisplay: 900, tracking: "-0.04em", headingScale: 1.2 }, shape: { radius: "0px", borderWidth: "4px" }, space: { density: "tight", gap: "0.4rem", padScale: 0.8 }, decoration: { shadow: "5px 5px 0 var(--st-accent)", effect: "none" }, }, + "dopamine-design": { + typography: { weightDisplay: 800, weightBody: 500, tracking: "-0.03em", headingScale: 1.18 }, + shape: { radius: "24px", radiusPill: "9999px", borderWidth: "2px" }, + space: { density: "normal", gap: "0.8rem", padScale: 1.05 }, + decoration: { shadow: "5px 5px 0 var(--st-accent-2)", effect: "gradient" }, + }, + "graffiti": { + typography: { weightDisplay: 900, weightBody: 600, tracking: "-0.05em", headingScale: 1.28 }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "3px", borderStyle: "dashed" }, + space: { density: "tight", gap: "0.45rem", padScale: 0.85 }, + decoration: { shadow: "6px 4px 0 var(--st-accent)", effect: "grain" }, + }, "editorial-design": { typography: { weightDisplay: 800, tracking: "-0.04em", headingScale: 1.2 }, shape: { radius: "0px", borderWidth: "2px" }, space: { density: "normal", gap: "1rem", padScale: 1.0 }, decoration: { shadow: "none", effect: "none" }, }, + "posterism": { + typography: { weightDisplay: 900, weightBody: 500, tracking: "-0.06em", headingScale: 1.35 }, + shape: { radius: "0px", radiusPill: "0px", borderWidth: "3px" }, + space: { density: "tight", gap: "0.5rem", padScale: 0.82 }, + layout: { heroVariant: "center", navStyle: "boxed", alignment: "center" }, + }, "glassmorphism": { typography: { weightDisplay: 500, tracking: "-0.01em" }, shape: { radius: "16px", borderWidth: "1px", borderStyle: "solid" }, space: { density: "normal", gap: "1rem", padScale: 1.1 }, decoration: { shadow: "0 8px 32px rgba(0,0,0,0.18)", effect: "none" }, }, + "neumorphism": { + typography: { weightDisplay: 600, weightBody: 400, tracking: "-0.02em", headingScale: 0.98 }, + shape: { radius: "24px", radiusPill: "9999px", borderWidth: "1px" }, + space: { density: "normal", gap: "1rem", padScale: 1.2 }, + decoration: { shadow: "12px 12px 24px rgb(var(--st-text-rgb) / 0.12), -12px -12px 24px rgb(var(--st-surface-rgb) / 0.9)", effect: "none" }, + }, "y2k": { typography: { weightDisplay: 800, tracking: "-0.02em", headingScale: 1.15 }, shape: { radius: "12px", borderWidth: "2px" }, @@ -625,7 +698,7 @@ const styleTokenOverrides: Record typography: { weightDisplay: 800, tracking: "-0.03em", headingScale: 1.2 }, shape: { radius: "4px", borderWidth: "2px" }, space: { density: "tight", gap: "0.5rem", padScale: 0.9 }, - decoration: { shadow: "4px 4px 0 var(--st-accent), -4px -4px 0 var(--st-accent2)", effect: "grain" }, + decoration: { shadow: "4px 4px 0 var(--st-accent), -4px -4px 0 var(--st-accent-2)", effect: "grain" }, }, "swiss-design": { typography: { weightDisplay: 700, tracking: "-0.02em", headingScale: 1.05 }, @@ -636,6 +709,8 @@ const styleTokenOverrides: Record }, }; +export const tunedStyleTokenSlugs = Object.keys(styleTokenOverrides).sort(); + function mergeTokens( base: StyleTokens, over?: DeepPartial>, diff --git a/src/lib/exportCode.ts b/src/lib/exportCode.ts new file mode 100644 index 0000000..6b5eb5c --- /dev/null +++ b/src/lib/exportCode.ts @@ -0,0 +1,72 @@ +import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; +import type { DesignStyle } from "@/data/designStyles"; +import type { WebLayout } from "@/data/webLayouts"; + +function cssVars(style: DesignStyle) { + return Object.entries(styleTokenVars(style)) + .map(([key, value]) => ` ${key}: ${String(value)};`) + .join("\n"); +} + +export function exportDesignCode(style: DesignStyle, layout: WebLayout) { + return ` +
      +
      + ${style.nameEn} + +
      +
      +

      ${layout.previewType}

      +

      ${layout.nameKo}

      +

      ${layout.summary}

      + +
      +
      + +`; +} diff --git a/src/lib/exportPrompt.ts b/src/lib/exportPrompt.ts new file mode 100644 index 0000000..9518c87 --- /dev/null +++ b/src/lib/exportPrompt.ts @@ -0,0 +1,33 @@ +import type { DesignStyle } from "@/data/designStyles"; +import type { WebLayout } from "@/data/webLayouts"; + +export function exportDesignPrompt(style: DesignStyle, layout: WebLayout) { + const tokens = style.tokens; + + return [ + `Create a high-quality webpage design reference for ${style.nameEn} (${style.nameKo}) using a ${layout.nameEn}.`, + `Visual style: ${style.summary}`, + `Layout structure: ${layout.summary}`, + `Preview type: ${layout.previewType}.`, + `Color system: base ${tokens.color.base}, surface ${tokens.color.surface}, text ${tokens.color.text}, primary ${tokens.color.primary}, accent ${tokens.color.accent}.`, + `Typography: display font ${tokens.typography.displayFont}, body font ${tokens.typography.bodyFont}, display weight ${tokens.typography.weightDisplay}, tracking ${tokens.typography.tracking}.`, + `Shape and spacing: radius ${tokens.shape.radius}, border ${tokens.shape.borderWidth} ${tokens.shape.borderStyle}, density ${tokens.space.density}, gap ${tokens.space.gap}.`, + `Decoration: shadow ${tokens.decoration.shadow}, effect ${tokens.decoration.effect}.`, + `Use cases: ${style.useCases.join(", ")}.`, + `Avoid: ${style.cautions.join(", ")}.`, + "Return a polished webpage composition, no logo, no watermark, production-ready visual hierarchy.", + ].join("\n"); +} + +export function exportLayoutPrompt(layout: WebLayout) { + return [ + `Create a production-ready webpage using a ${layout.nameEn} (${layout.nameKo}).`, + `Layout summary: ${layout.summary}`, + `Preview type: ${layout.previewType}.`, + `Structure: ${layout.structure.join(", ")}.`, + `Responsive behavior: ${layout.responsiveBehavior.join(", ")}.`, + `Best for: ${layout.bestFor.join(", ")}.`, + `Avoid: ${layout.notGoodFor.join(", ")}.`, + "Keep the hierarchy clear, responsive, accessible, and suitable for a design dictionary reference.", + ].join("\n"); +}