diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..567dbcd --- /dev/null +++ b/README.en.md @@ -0,0 +1,219 @@ +# openlayout + +[한국어](./README.md) | **English** + +A web layout library for choosing, comparing, and previewing website structures and design styles separately — with real webpage-style previews. + +openlayout is a layout/style dictionary that helps you quickly pick page structure and visual language before you start designing. It provides 96 layouts, 88 design styles, 10 style categories, and a webpage-style sample renderer. Each entry comes with recommended use cases, pros and cons, responsive behavior, accessibility checkpoints, a color palette, and Tailwind implementation hints. + +| Item | Value | +| --- | --- | +| Repository | https://github.com/pandaofwild/openlayout | +| Last reviewed | 2026-06-03 | + +## Who it's for + +- Designers who need to quickly compare website structures +- Frontend developers looking for base skeletons for landing pages, dashboards, docs, and commerce screens +- Teams that want to check responsive behavior and accessibility at the same time + +## Key features + +- **Layout explorer**: Filter layouts by search term, category, purpose, and complexity. +- **Full-stage preview**: View layouts large, like a real webpage background, on detail and compare pages. +- **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. +- **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. +- **Implementation hints**: Provides Tailwind code examples and implementation tips per previewType. +- **Project skills**: `skills/layout-recommender/SKILL.md` and `skills/design-style-recommender/SKILL.md` guide purpose-based layout/style recommendations. + +## Why it helps vibe coding + +- State your page purpose up front and you can quickly narrow down layout candidates. e.g. "SaaS dashboard landing", "brand campaign", "docs-style knowledge base". +- Each layout carries its recommended use, situations to avoid, responsive behavior, and accessibility checkpoints, so you can pull design constraints straight into your prompt. +- The large previews and floating description panels on the compare page make it easy to iterate with short feedback like "let's go with this structure" or "this one is weak on mobile". +- `previewType` works like shorthand for implementation direction. e.g. `hero`, `card-grid`, `dashboard`, `docs`, `comparison`. +- `DesignStyle` works like shorthand for visual direction. e.g. `brutalism`, `cyberpunk`, `luxury`, `organic-design`, `saas-style`. +- For a new screen, the most reliable flow is: pick a structure from the layout dictionary first, then pick color/typography/mood from a design style, and finally hand components and copy off to a coding agent. + +## Quick start + +Requirements: + +- Node.js 22 or later +- npm + +Install dependencies: + +```bash +npm install +``` + +Run the dev server: + +```bash +npm run dev +``` + +Open in your browser: + +```text +http://localhost:3000/web-layouts +``` + +The root path (`/`) redirects to `/web-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 | + +## Image generation environment variables + +`/design-styles/generate` and `/api/design-style-images` are local admin features. + +```bash +OPENAI_API_KEY=sk-... +OPENAI_IMAGE_MODEL=gpt-image-1.5 +``` + +- `OPENAI_API_KEY` is required. +- `OPENAI_IMAGE_MODEL` is optional and defaults to `gpt-image-1.5`. +- Generated results are saved to `public/generated/design-styles/{slug}.webp`. +- The image generation route is restricted to local development; on read-only deployment platforms like Vercel, switch to external storage such as Blob/S3. + +## Open-source usage + +This project is distributed under the MIT License. See `LICENSE` for full terms. + +- How to contribute: `CONTRIBUTING.md` +- Security reports: `SECURITY.md` +- Local environment variable example: `.env.example` +- CI: `.github/workflows/ci.yml` + +## Quality checks + +Run these before deploying or uploading changes. + +```bash +npm run lint +npm run build +``` + +## Tech stack + +- Next.js App Router +- React +- TypeScript +- Tailwind CSS + +The layout catalog is static-data driven. No separate database or external API is required. + +## 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/api/design-style-images/route.ts # OpenAI Image API route +src/data/webLayouts.ts # Layout catalog and generated metadata +src/data/designStyles.ts # 88 design styles and generated metadata +src/components/web-layout/ # Explorer, cards, previews, compare UI +src/components/design-style/ # Style cards, filters, samples, generator UI +src/components/style-preset/ # Global selected style provider +src/components/ui/ # Small shared UI primitives +skills/layout-recommender/SKILL.md # Purpose-based layout recommendation skill +skills/design-style-recommender/SKILL.md # Brand-tone-based style recommendation skill +``` + +Key components: + +| File | Role | +| --- | --- | +| `WebLayoutExplorer.tsx` | Search and filter state for the layout list | +| `WebLayoutFilters.tsx` | Filter UI for search term, category, purpose, complexity | +| `WebLayoutCard.tsx` | Layout card and thumbnail | +| `LayoutStagePreview.tsx` | Full-background preview, floating summary, click-to-open description panel | +| `LayoutPreview.tsx` | Browser-style preview utility with viewport switching | +| `LayoutPreviewRenderer.tsx` | Large live preview templates per previewType | +| `WireframeThumbnail.tsx` | Structure thumbnails used on cards and the compare screen | +| `LayoutCodeExample.tsx` | Copyable Tailwind implementation example | +| `WebLayoutCompare.tsx` | Compare page selection, SVG arrow navigation, large preview display | +| `DesignStyleLibrary.tsx` | Search, filter, and applied state for the design style list | +| `DesignStyleCard.tsx` | Design style card, color palette, webpage-style sample, apply button | +| `DesignStyleSampleRenderer.tsx` | 10 webpage-style style samples per sampleType | +| `StylePresetProvider.tsx` | Persists the selected design style and custom palette in localStorage | +| `DesignStyleImageGenerator.tsx` | Style reference image generation admin UI | + +## Project skills + +`skills/layout-recommender/SKILL.md` is an internal skill a coding agent reads to recommend a layout that fits the intended use. + +`skills/design-style-recommender/SKILL.md` is an internal skill read to recommend a design style matching brand tone, industry, emotion, typography, and color direction. + +Example recommendation request: + +```text +This is a B2B SaaS onboarding page. Trust and feature explanation matter, and it has to work on mobile too. Which layout is best? +``` + +The skill is designed to first check the category, `bestFor`, `notGoodFor`, `tags`, and `previewType` in `src/data/webLayouts.ts`, then briefly suggest a top candidate, alternatives, and structures to avoid. + +Example design style recommendation request: + +```text +It's a premium beauty brand landing page, but it shouldn't be too flashy — it needs to look high-end. Which design style is best? +``` + +The design style skill suggests a top style, alternatives, and styles to avoid based on `category`, `tags`, `goodFor`, `useCases`, `palette`, and `sampleType` in `src/data/designStyles.ts`. + +## Adding a layout + +Layout data is managed in `src/data/webLayouts.ts`. + +1. Add a new entry to `layoutSeeds`. +2. Provide `nameKo`, `nameEn`, `category`, `summary`, `previewType`, and `complexity`. +3. Add `bestFor`, `notGoodFor`, and `tags` only when you need to override the defaults. +4. Let the data builder generate `slug`, the long description, pros/cons, responsive notes, accessibility notes, implementation tips, and related layouts. + +When adding a new category, also add a description and defaults to `categoryGuides`. + +## Adding a design style + +Design style data is managed in `src/data/designStyles.ts`. + +1. Add a new entry to `styleSeedTuples`. +2. Provide `slug`, `nameKo`, `nameEn`, `category`, `tone`, `tags`, and `sampleType`. +3. If needed, add a 9-color palette for that slug to `palettes`. +4. If a new category is needed, add visual traits, color notes, typography, and layout tendencies to `categoryProfiles`. +5. Extend `DesignStyleSampleType` and `DesignStyleSampleRenderer.tsx` only when the existing 10 sample renderers can't express it. + +## Adding a preview type + +Add a new previewType when existing templates don't reveal the structure well enough. + +1. Extend the `PreviewType` union in `src/data/webLayouts.ts`. +2. Add a structure description, responsive behavior, and implementation tips to `previewGuides`. +3. Add a live preview renderer to `src/components/web-layout/LayoutPreviewRenderer.tsx`. +4. Add a thumbnail diagram to `src/components/web-layout/WireframeThumbnail.tsx`. +5. If the implementation differs from existing examples, add a Tailwind example to `src/components/web-layout/LayoutCodeExample.tsx`. + +## Writing and design notes + +- Write layout names and summaries practically. A reader should understand when to use a structure without opening the detail page. +- Prefer concrete structure labels like `Header`, `Main`, `Sidebar`, `CTA`, `TOC`, `Product`. +- Detail pages explain limits and trade-offs, not just benefits. +- Long descriptions on the compare page are collapsed by default so the structure diagram can be scanned first. diff --git a/README.md b/README.md index a6d9f10..c9bec05 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # openlayout +**한국어** | [English](./README.en.md) + 웹사이트 레이아웃과 디자인 형식을 분리해서 고르고, 비교하고, 실제 웹페이지형 프리뷰로 확인하는 Web Layout Library입니다. openlayout은 디자인을 시작하기 전에 페이지 구조와 시각 언어를 빠르게 고를 수 있도록 만든 레이아웃/스타일 사전입니다. 96개의 레이아웃, 88개의 디자인 형식, 10개의 스타일 카테고리, 웹페이지형 샘플 렌더러를 제공하며, 각 항목은 추천 용도, 장단점, 반응형 동작, 접근성 체크포인트, 색상표, Tailwind 구현 힌트를 함께 보여줍니다. diff --git a/docs/plans/2026-06-03-design-dictionary-design.md b/docs/plans/2026-06-03-design-dictionary-design.md new file mode 100644 index 0000000..0fb05df --- /dev/null +++ b/docs/plans/2026-06-03-design-dictionary-design.md @@ -0,0 +1,171 @@ +# Design Dictionary — Design Document + +> **Status:** Approved design (2026-06-03). Next step: create an implementation plan with the `writing-plans` skill. +> **Supersedes direction of:** `docs/superpowers/plans/2026-06-02-design-style-library.md` (the design-style library is now one axis of a larger dictionary). + +--- + +## 1. Vision & Concept Model + +**One-line product definition:** Before starting a design, freely combine *Style × Layout* to preview real webpage-like results, later explore down to the component level, and copy code or prompts from any result — a **design dictionary**. + +### The user's full desired direction (recorded verbatim in intent) + +The owner wants to: + +1. Build webs in many styles — pages that genuinely look *different* per style, not the same mock recolored. +2. Build layouts in many styles — explore the structural skeleton together with style. +3. Combine them — cross styles and layouts to view the resulting web instantly. +4. Eventually explore by component too — a design dictionary source viewable per component. +5. Copy from any result — source code or the generation prompt. + +Gallery-first now; code/prompt copy later. + +### Four browsing axes (concept model) + +| Axis | Definition | Example | +| --- | --- | --- | +| **Style** | Visual language — a bundle of color/typography/shape/spacing/decoration tokens | cyberpunk, minimal, brutalism | +| **Layout** | Structural skeleton — the content-placement frame | hero, dashboard, card grid | +| **Web** | Style × Layout = one finished page | "cyberpunk × dashboard" | +| **Component** | Individual styled piece — *later* | button, card, nav | + +### The core shift + +From today's **"swap the palette only"** → a **full design-token system**: style controls color, typography, shape, spacing/density, decoration, and layout variation. + +--- + +## 2. Token System Architecture + +### Problem + +Today only `DesignStyle.palette` (9 colors) actually drives the screen. `typography`, `layoutTraits`, etc. are *descriptive text* and are never applied to rendering. + +### Solution + +Introduce **`StyleTokens`** — the values a style actually uses to control the screen. Every token is emitted as a CSS variable; layout and component renderers read only those variables. + +```ts +type StyleTokens = { + color: { base, surface, text, muted, primary, + accent, accent2, accent3, border } // inherits current palette + typography: { displayFont, bodyFont, weightDisplay, + weightBody, tracking, headingScale } // font / weight / tracking + shape: { radius, borderWidth, borderStyle } // roundness / borders + space: { density: 'airy' | 'normal' | 'tight', + gap, padScale } // whitespace / density + decoration: { shadow, glow, grain, gradient, + effect: 'none' | 'glitch' | 'scanline' | … } // decorative effects + layout: { heroVariant, navStyle, alignment } // layout variation hints +} +``` + +### Application flow + +``` +DesignStyle.tokens + → StyleProvider emits CSS variables (--st-radius, --st-font-display, --st-shadow …) + → LayoutRenderer / ComponentRenderer use only var(--st-*) + → Switching style = swapping a variable bundle = whole screen updates live +``` + +### Scalability (the heart of approach C) + +Adding one style = filling in a `tokens` object only. Renderers are untouched → scaling to 88 styles becomes "data entry." + +### Defaults + overrides + +Per-category default tokens (e.g. the "minimal" family is `airy` + thin fonts); each style overrides only what it needs → minimizes the labor of filling 88 styles. + +### Migration + +Existing `palette` → absorbed into `tokens.color`. Descriptive fields (`typography: string[]`, etc.) stay as human-readable dictionary text; application is handled by the new `tokens`. + +--- + +## 3. Pages / UX Structure + +### Route map (reuse existing assets; the combine view is the key new piece) + +| Route | Role | Status | +| --- | --- | --- | +| `/styles` | Style gallery (88, filter/search) | inherit & rename from `/design-styles` | +| `/styles/[slug]` | Style detail — tokens, color, type + sample | inherit & strengthen | +| `/layouts` | Layout gallery (96) | inherit from `/web-layouts` | +| `/layouts/[slug]` | Layout detail — structure, responsive, a11y | inherit | +| **`/studio`** | **Combine view: Style × Layout → finished web preview** | **new (core)** | +| `/components` | Component dictionary | *later (Phase 6)* | + +Old URLs keep working via redirects (`/web-layouts` → `/layouts`, `/design-styles` → `/styles`). + +### `/studio` — combine view (the heart of the project) + +``` +┌─────────────┬──────────────────────────────┐ +│ left: control│ right: live web preview │ +│ │ │ +│ Style [▼] │ selected Style × Layout │ +│ Layout [▼] │ rendered with full tokens │ +│ viewport[▣▢]│ like a real webpage │ +│ │ │ +│ [copy code] │ (changing style = instant) │ +│ [prompt] │ │ +└─────────────┴──────────────────────────────┘ +``` + +- Pick style/layout from dropdowns (or small thumbnail grids) to cross instantly. +- Desktop/mobile viewport toggle. +- The chosen combo is shareable/bookmarkable via URL query (`?style=cyberpunk&layout=hero`). +- **Copy buttons** activate in Phase 5 (placeholder reserved first). + +### Gallery cards + +Each style/layout card shows a mini thumbnail with full tokens applied, so scanning the list alone makes the "genuinely different" quality visible. + +### Copy feature (Phase 5) + +From `/studio` and detail pages — ① code for the current combo (HTML or React+Tailwind) ② an AI generation prompt. + +--- + +## 4. Phased Roadmap + +Each phase gates on `npm run lint` + `npm run build` passing. + +**Phase 1 — Token system foundation** (core of approach C, top priority) +- New `StyleTokens` type + `StyleProvider` emits all tokens as CSS variables. +- Per-category default tokens + per-style overrides. +- Migrate existing `palette` → `tokens.color`. +- Complete **8–12 representative styles** with full tokens for validation. + +**Phase 2 — Tokenize the layout renderer** +- `LayoutPreviewRenderer` consumes not just color but typography/shape/spacing/decoration tokens. +- Convert layout gallery (`/layouts`) + detail to token-based. +- Validate: the same layout looks *genuinely different* across styles. + +**Phase 3 — Studio combine view** +- New `/studio`: cross style × layout, URL-query sharing, viewport toggle. +- Refresh gallery card thumbnails to full-token rendering. +- Old-URL redirect compatibility. + +**Phase 4 — Fill all 88 styles** +- With the token system validated, expand the rest as "token value entry." +- QA visual difference per style. + +**Phase 5 — Copy feature** +- Copy code (HTML / React+Tailwind). +- Copy AI generation prompt. + +**Phase 6 — Component dictionary** (long term) +- `/components`: explore style application per component (button, card, nav, …). + +--- + +## Decisions Log + +- Deliverable: gallery-first now; code/prompt copy later. +- Concept model confirmed: Style / Layout / Web (= Style × Layout) / Component. +- Style must control everything: color, typography, shape, spacing/density, decoration, and layout variation. +- Approach **C (token system first)** chosen over (A) broad-but-shallow and (B) deep-but-few. +- The full long-term vision must be captured in this document (done above). diff --git a/docs/plans/2026-06-03-design-dictionary.md b/docs/plans/2026-06-03-design-dictionary.md new file mode 100644 index 0000000..bd8163e --- /dev/null +++ b/docs/plans/2026-06-03-design-dictionary.md @@ -0,0 +1,477 @@ +# Design Dictionary Implementation Plan + +> **For Claude:** 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. + +**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. + +**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`. + +**Design doc:** `docs/plans/2026-06-03-design-dictionary-design.md` + +--- + +## Conventions + +- **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. + +--- + +## 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). + +--- + +## Phase 0: Verification Harness + +### Task 0: Add a data-integrity check script + +**Files:** +- Create: `scripts/check-data.mjs` +- Modify: `package.json` (add `"check:data"` script) + +- [ ] **Step 1: Create the check script** + +`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. + +Minimal first version (count + uniqueness against source counts the plan will extend): + +```js +// scripts/check-data.mjs +import { designStyles, designStyleCategories } from "../src/data/designStyles.ts"; + +const errors = []; +function assert(cond, msg) { if (!cond) errors.push(msg); } + +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}`); + +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`); +``` + +> 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. + +- [ ] **Step 2: Add npm script** + +In `package.json` scripts: `"check:data": "node --experimental-strip-types scripts/check-data.mjs"`. + +- [ ] **Step 3: Run it (expect pass on current data)** + +Run: `npm run check:data` +Expected: `data check passed: 88 styles, 10 categories` + +- [ ] **Step 4: Commit** + +```bash +git add scripts/check-data.mjs package.json +git commit -m "Add data-integrity check script for style data" +``` + +--- + +## Phase 1: Token Foundation + +### Task 1: Define the `StyleTokens` type + +**Files:** +- Modify: `src/data/designStyles.ts` (top of file, near `DesignStylePalette`) + +- [ ] **Step 1: Add the token types** + +Add above `DesignStyle`: + +```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"; + }; +}; +``` + +- [ ] **Step 2: Add `tokens` to `DesignStyle`** + +Add `tokens: StyleTokens;` to the `DesignStyle` type. + +- [ ] **Step 3: Verify it fails the build (tokens not yet produced)** + +Run: `npm run build` +Expected: TypeScript error — `buildStyle` return is missing `tokens`. This confirms the type is wired before we implement. + +### Task 2: Add per-category default tokens + +**Files:** +- Modify: `src/data/designStyles.ts` + +- [ ] **Step 1: Add `categoryTokenDefaults`** + +Add a `Record` keyed by the 10 category names, giving each family a sensible baseline. Example entries (fill all 10): + +```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 / 웹" +}; +``` + +- [ ] **Step 2: Commit (no behavior change yet)** + +```bash +git add src/data/designStyles.ts +git commit -m "Add per-category default style tokens" +``` + +### Task 3: Add per-style token overrides + merge in builder + +**Files:** +- Modify: `src/data/designStyles.ts` + +- [ ] **Step 1: Add `styleTokenOverrides`** + +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. + +```ts +type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] }; + +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" } }, + // ... +}; + +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 }, + }; +} +``` + +> Note: object keys with hyphens (e.g. `glitch-art`) must be quoted: `"glitch-art": {...}`. + +- [ ] **Step 2: Produce `tokens` in `buildStyle`** + +In `buildStyle`, before the `return`, compute: + +```ts +const baseTokens = categoryTokenDefaults[seed.category]; +const tokens = mergeTokens(baseTokens, styleTokenOverrides[seed.slug]); +``` + +Add `tokens,` to the returned object. + +- [ ] **Step 3: Build passes now** + +Run: `npm run build` +Expected: exit 0 (the missing-`tokens` error from Task 1 is resolved). + +- [ ] **Step 4: Extend the data check for token completeness** + +In `scripts/check-data.mjs` add: + +```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`); +} +``` + +Run: `npm run check:data` → expect pass for all 88. + +- [ ] **Step 5: Commit** + +```bash +git add src/data/designStyles.ts scripts/check-data.mjs +git commit -m "Generate style tokens via category defaults + per-style overrides" +``` + +### Task 4: Migrate `palette` into `tokens.color` (compat-preserving) + +**Files:** +- Modify: `src/data/designStyles.ts` + +- [ ] **Step 1: Add `color` to `StyleTokens`** + +Add a `color` group to `StyleTokens` mirroring `DesignStylePalette` keys (base, surface, text, muted, primary, accent, accent2, accent3, border). + +- [ ] **Step 2: Derive `tokens.color` from `seed.palette` in `buildStyle`** + +```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]) }; +``` + +Keep the legacy `palette` field on `DesignStyle` for now (no breaking change to existing components). + +- [ ] **Step 3: check:data + build + commit** + +```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)" +``` + +### Task 5: Fully tune 8–12 representative styles + +**Files:** +- Modify: `src/data/designStyles.ts` (`styleTokenOverrides`) + +Representative set (one per category + a few extremes): `minimalism`, `brutalism`, `cyberpunk`, `luxury`, `organic-design`, `kawaii`, `streetwear`, `editorial-design`, `glassmorphism`, `y2k`, `maximalism`, `swiss-design`. + +- [ ] **Step 1: Write override tokens for each so they look visibly distinct** + +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). + +- [ ] **Step 2: check:data + build + 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" +``` + +--- + +## Phase 2: Token CSS Pipeline + +### Task 6: Add a token→CSS-variable emitter and hook + +**Files:** +- Create: `src/components/style-preset/styleTokenVars.ts` +- Modify: `src/components/style-preset/StylePresetProvider.tsx` + +- [ ] **Step 1: Create `styleTokenVars.ts`** + +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). + +- [ ] **Step 2: Apply on `.style-preset-root`** + +In `StylePresetProvider`, spread `styleTokenVars(activePreset)` into the root `style` alongside the legacy vars, and add `data-st-effect={activePreset.tokens.decoration.effect}`. + +- [ ] **Step 3: Expose tokens via context** + +Add `tokens: activePreset.tokens` to the context value and `useStyleTokens()` convenience hook. + +- [ ] **Step 4: build + commit** + +```bash +npm run build +git add src/components/style-preset/ +git commit -m "Emit style tokens as --st-* CSS variables and expose via hook" +``` + +### Task 7: Add token-driven utilities + effect layers to globals.css + +**Files:** +- Modify: `src/app/globals.css` + +- [ ] **Step 1: Add base token utility classes** + +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`). + +- [ ] **Step 2: Add effect layers keyed by `[data-st-effect]`** + +`[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`. + +- [ ] **Step 3: build + commit** + +```bash +npm run build +git add src/app/globals.css +git commit -m "Add token-driven utility classes and effect layers" +``` + +--- + +## Phase 3: Tokenize the Layout Renderer + +### Task 8: Convert `LayoutPreviewRenderer` to `--st-*` tokens + +**Files:** +- Modify: `src/components/web-layout/LayoutPreviewRenderer.tsx` + +- [ ] **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** + +```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" +``` + +### Task 9: Tokenize the design-style sample renderer + +**Files:** +- Modify: `src/components/design-style/DesignStyleSampleRenderer.tsx` + +- [ ] Apply the same `--st-*` token treatment so style cards/samples reflect typography/shape/decoration, not only palette. build + commit. + +--- + +## Phase 4: Studio Combine View + +### Task 10: Create the studio page shell + +**Files:** +- Create: `src/app/studio/page.tsx` +- Create: `src/components/studio/StudioView.tsx` (client) + +- [ ] **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. + +### Task 11: Studio thumbnail pickers (optional polish) + +- [ ] Replace dropdowns with small thumbnail grids for style/layout using existing card thumbnails. build + commit. + +--- + +## Phase 5: Route Restructure + +### Task 12: Add `/styles` and `/layouts` canonical routes with redirects + +**Files:** +- Create: `src/app/styles/...`, `src/app/layouts/...` (or rename existing dirs) +- Modify: `next.config.ts` (redirects) +- Modify: nav/links across `src/` + +- [ ] **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. + +--- + +## Phase 6: Fill All 88 Styles + +### Task 13: Token values for every remaining style + +**Files:** +- Modify: `src/data/designStyles.ts` (`styleTokenOverrides`) + +- [ ] **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. + +--- + +## Phase 7: Copy Feature + +### Task 14: Prompt copy + +**Files:** +- Create: `src/lib/exportPrompt.ts` +- Modify: `src/components/studio/StudioView.tsx`, style/layout detail pages + +- [ ] Build a function that composes a generation prompt from the selected style (`imagePrompt`, tokens) + layout (`previewType`, structure). Wire the "프롬프트 복사" button (clipboard). build + commit. + +### Task 15: Code copy + +**Files:** +- Create: `src/lib/exportCode.ts` + +- [ ] Generate a self-contained HTML+Tailwind (or React+Tailwind) snippet for the current Style × Layout using the token CSS variables inlined. Wire "코드 복사". build + commit. + +--- + +## Phase 8: Component Dictionary (long term) + +### Task 16: `/components` route + +- [ ] 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. + +--- + +## Verification Checklist (whole feature) + +- [ ] `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). + +--- + +## Progress Log + +- [ ] 2026-06-03: Plan created from approved design doc. diff --git a/docs/superpowers/plans/2026-06-02-design-style-library.md b/docs/superpowers/plans/2026-06-02-design-style-library.md index c667304..24ea191 100644 --- a/docs/superpowers/plans/2026-06-02-design-style-library.md +++ b/docs/superpowers/plans/2026-06-02-design-style-library.md @@ -301,7 +301,7 @@ export type DesignStyle = { ## Milestone Checklist -- [ ] **M0: Plan Document** +- [x] **M0: Plan Document** - [x] Create this plan. - [x] Keep this document updated after each completed task. @@ -1080,7 +1080,7 @@ Expected: - Modify: `src/app/globals.css` - Modify: `src/components/web-layout/LayoutPreviewRenderer.tsx` -- [ ] **Step 1: Use selected `DesignStyle` for CSS variables** +- [x] **Step 1: Use selected `DesignStyle` for CSS variables** Provider must write: @@ -1096,7 +1096,7 @@ Provider must write: --style-border ``` -- [ ] **Step 2: Confirm localStorage persistence** +- [x] **Step 2: Confirm localStorage persistence** Manual test: @@ -1110,7 +1110,7 @@ Expected: - Applied style strip shows `사이버펑크`. - Layout preview cards use dark neon colors. -- [ ] **Step 3: Confirm compare page** +- [x] **Step 3: Confirm compare page** Manual test: @@ -1207,7 +1207,7 @@ IMAGE_QUALITY=medium - [x] `/web-layouts/compare` still loads. - [x] Mobile width 390px has no horizontal page overflow. - [x] Design style cards look like real style samples, not plain wireframes. -- [ ] Layout preview cards still look like real webpages. +- [x] Layout preview cards still look like real webpages. - [x] Search filters update instantly. - [x] Category filters update instantly. - [x] Selected style persists after reload. @@ -1234,3 +1234,4 @@ IMAGE_QUALITY=medium - [x] 2026-06-02: M6 image generation complete: added `/api/design-style-images`, `/design-styles/generate`, local save path, and verified missing `OPENAI_API_KEY` returns 503 with a clear error. - [x] M7 complete. - [x] 2026-06-02: M7 release complete: README and project skill updated, `npm run lint` and `npm run build` pass on `0.1.4`, visual checks captured, and release is prepared for GitHub tag `v0.1.4`. +- [x] 2026-06-03: LayoutPreviewRenderer rewritten to use `--style-*` CSS variables directly (removing hardcoded hex colors), fixing pseudo-element color override bugs in globals.css. All Task 5 verification steps confirmed complete. Plan document fully checked off. diff --git a/package.json b/package.json index 165c15d..099b783 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "check:data": "node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/check-data.mjs" }, "engines": { "node": ">=22" diff --git a/scripts/check-data.mjs b/scripts/check-data.mjs new file mode 100644 index 0000000..61206db --- /dev/null +++ b/scripts/check-data.mjs @@ -0,0 +1,34 @@ +// scripts/check-data.mjs +import { designStyles, designStyleCategories } from "../src/data/designStyles.ts"; + +const errors = []; +function assert(cond, msg) { if (!cond) errors.push(msg); } + +// Minimum counts guard against accidental mass-deletion. +// Raise these if new styles / categories are intentionally added. +assert(designStyles.length >= 88, `expected at least 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 at least 10 categories, got ${designStyleCategories.length}`); + +const slugSet = new Set(designStyles.map((s) => s.slug)); +for (const s of designStyles) { + for (const rel of s.related) { + assert(slugSet.has(rel), `style ${s.slug}: related slug "${rel}" does not exist`); + } +} + +const categorySet = new Set(designStyleCategories); +for (const s of designStyles) { + assert(categorySet.has(s.category), `style ${s.slug}: unknown category "${s.category}"`); +} + +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(s.tokens.color.base === s.palette.base, `style ${s.slug} tokens.color.base mismatch`); +} + +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`); diff --git a/src/app/globals.css b/src/app/globals.css index 9c29d2a..3d789b2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -481,6 +481,101 @@ main header { border-color: rgb(var(--style-border-rgb) / 0.22) !important; } +/* ─── Style token utility classes ─────────────────────────────────────────── */ + +.st-surface { + background-color: var(--st-surface, var(--style-surface, #F0EEE8)); + color: var(--st-text, var(--style-text, #1E1E1E)); +} + +.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); + box-shadow: var(--st-shadow, none); +} + +.st-display { + font-family: var(--st-font-display, var(--font-display)); + font-weight: var(--st-weight-display, 700); + letter-spacing: var(--st-tracking, -0.05em); +} + +.st-body { + font-family: var(--st-font-body, var(--font-sans)); + font-weight: var(--st-weight-body, 400); +} + +.st-accent { + color: var(--st-accent, var(--style-accent, #DB4A2B)); +} + +/* ─── Effect layers ────────────────────────────────────────────────────────── */ + +/* Glow: neon/cyber glow on cards */ +[data-st-effect="glow"] .st-card { + box-shadow: var(--st-shadow, 0 0 16px rgb(var(--st-accent-rgb, 0 229 255) / 0.4)); +} + +/* Grain: film-grain noise overlay on preview canvases */ +[data-st-effect="grain"] .raw-preview-canvas::before, +[data-st-effect="grain"] .raw-wireframe::before { + opacity: 0.06; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 200px 200px; + mix-blend-mode: overlay; +} + +/* Scanline: horizontal CRT scan-line overlay */ +[data-st-effect="scanline"] .raw-preview-canvas::after, +[data-st-effect="scanline"] .raw-wireframe::after { + background: repeating-linear-gradient( + to bottom, + transparent 0px, + transparent 2px, + rgb(0 0 0 / 0.07) 2px, + rgb(0 0 0 / 0.07) 4px + ); + mix-blend-mode: overlay; + pointer-events: none; +} + +/* Gradient: animated gradient accent overlay */ +[data-st-effect="gradient"] .raw-preview-canvas, +[data-st-effect="gradient"] .raw-wireframe { + background: linear-gradient( + 135deg, + var(--st-base, #E4E2DD) 0%, + rgb(var(--st-accent-rgb, 219 74 43) / 0.15) 50%, + var(--st-base, #E4E2DD) 100% + ) !important; +} + +/* Glitch: CSS-only glitch shift on headings */ +@keyframes st-glitch { + 0%, 100% { clip-path: none; transform: none; } + 20% { clip-path: inset(20% 0 60% 0); transform: translateX(-4px); } + 40% { clip-path: inset(60% 0 20% 0); transform: translateX(4px); } + 60% { clip-path: none; transform: none; } +} + +[data-st-effect="glitch"] .st-display, +[data-st-effect="glitch"] .raw-preview-canvas h3 { + animation: st-glitch 3s steps(1) infinite; +} + +/* Density modifiers */ +[data-st-density="tight"] .raw-preview-canvas { + --internal-gap-scale: 0.7; +} + +[data-st-density="airy"] .raw-preview-canvas { + --internal-gap-scale: 1.3; +} + +/* ─── Reduced motion overrides ─────────────────────────────────────────────── */ + @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; @@ -494,4 +589,9 @@ main header { scroll-behavior: auto !important; transition-duration: 0.001ms !important; } + + [data-st-effect="glitch"] .st-display, + [data-st-effect="glitch"] .raw-preview-canvas h3 { + animation: none; + } } diff --git a/src/components/style-preset/StylePresetProvider.tsx b/src/components/style-preset/StylePresetProvider.tsx index 819f199..268eb96 100644 --- a/src/components/style-preset/StylePresetProvider.tsx +++ b/src/components/style-preset/StylePresetProvider.tsx @@ -16,8 +16,10 @@ import { getDesignStyleBySlug, type DesignStyle, type DesignStylePalette, + type StyleTokens, } from "@/data/designStyles"; import { createPromptStylePreset } from "@/lib/paletteGenerator"; +import { styleTokenVars } from "./styleTokenVars"; type StylePresetContextValue = { activePreset: DesignStyle; @@ -29,6 +31,7 @@ type StylePresetContextValue = { selectedSlug: string; setPrompt: (prompt: string) => void; setSelectedSlug: (slug: string) => void; + tokens: StyleTokens; }; type StyleVariables = CSSProperties & Record<`--style-${string}`, string>; @@ -170,6 +173,7 @@ export function StylePresetProvider({ children }: { children: ReactNode }) { }, palette: activePreset.palette, prompt, + tokens: activePreset.tokens, resetCustomPreset: () => { userChangedBeforeStorageReady.current = true; setStyleState((current) => ({ @@ -198,8 +202,10 @@ export function StylePresetProvider({ children }: { children: ReactNode }) {
{children}
@@ -217,4 +223,8 @@ export function useStylePreset() { return value; } +export function useStyleTokens() { + return useStylePreset().activePreset.tokens; +} + export { designStyles as stylePresets }; diff --git a/src/components/style-preset/styleTokenVars.ts b/src/components/style-preset/styleTokenVars.ts new file mode 100644 index 0000000..8bfefa5 --- /dev/null +++ b/src/components/style-preset/styleTokenVars.ts @@ -0,0 +1,56 @@ +import type { CSSProperties } from "react"; +import type { DesignStyle } from "@/data/designStyles"; + +function hexToRgb(hex: string): string { + const normalized = hex.replace("#", ""); + const full = normalized.length === 3 + ? normalized.split("").map((c) => c + c).join("") + : normalized; + const value = Number.parseInt(full, 16); + if (Number.isNaN(value)) return "30 30 30"; + return `${(value >> 16) & 255} ${(value >> 8) & 255} ${value & 255}`; +} + +export function styleTokenVars(style: DesignStyle): CSSProperties { + const { color, typography, shape, space, decoration, layout } = style.tokens; + return { + // Color tokens + "--st-base": color.base, + "--st-surface": color.surface, + "--st-text": color.text, + "--st-muted": color.muted, + "--st-primary": color.primary, + "--st-accent": color.accent, + "--st-accent-2": color.accent2, + "--st-accent-3": color.accent3, + "--st-border": color.border, + // Color RGB (for rgb(var(--st-accent-rgb) / 0.5) usage) + "--st-base-rgb": hexToRgb(color.base), + "--st-surface-rgb": hexToRgb(color.surface), + "--st-text-rgb": hexToRgb(color.text), + "--st-primary-rgb": hexToRgb(color.primary), + "--st-accent-rgb": hexToRgb(color.accent), + "--st-accent-2-rgb": hexToRgb(color.accent2), + "--st-accent-3-rgb": hexToRgb(color.accent3), + "--st-border-rgb": hexToRgb(color.border), + // Typography tokens + "--st-font-display": typography.displayFont, + "--st-font-body": typography.bodyFont, + "--st-weight-display": String(typography.weightDisplay), + "--st-weight-body": String(typography.weightBody), + "--st-tracking": typography.tracking, + "--st-heading-scale": String(typography.headingScale), + // Shape tokens + "--st-radius": shape.radius, + "--st-border-width": shape.borderWidth, + // Space tokens + "--st-gap": space.gap, + "--st-pad-scale": String(space.padScale), + // Decoration token + "--st-shadow": decoration.shadow, + // Layout tokens + "--st-hero-variant": layout.heroVariant, + "--st-nav-style": layout.navStyle, + "--st-alignment": layout.alignment, + } as CSSProperties; +} diff --git a/src/components/web-layout/LayoutPreviewRenderer.tsx b/src/components/web-layout/LayoutPreviewRenderer.tsx index 070b8f9..80f900b 100644 --- a/src/components/web-layout/LayoutPreviewRenderer.tsx +++ b/src/components/web-layout/LayoutPreviewRenderer.tsx @@ -35,7 +35,10 @@ function Region({ }) { return ( @@ -46,23 +49,23 @@ function Region({ function SampleHeader({ compact = false }: { compact?: boolean }) { return ( -
+
- + Raw Co.
{compact ? ( - + Menu ) : ( -
@@ -71,7 +74,7 @@ function SampleHeader({ compact = false }: { compact?: boolean }) { function RawLabel({ children, className }: { children: ReactNode; className?: string }) { return ( -

+

{children}

); @@ -89,7 +92,7 @@ function RawHeading({ return (

@@ -119,9 +122,9 @@ function SoftScene({ className, children }: { className?: string; children?: Rea return (
@@ -133,20 +136,20 @@ function SoftScene({ className, children }: { className?: string; children?: Rea function ProductTile({ name, index }: { name: string; index: number }) { return (
-
+
- +

{name}

-

₩{128 + index * 17}

+

₩{128 + index * 17}

); @@ -154,8 +157,8 @@ function ProductTile({ name, index }: { name: string; index: number }) { function MetricCard({ label, value }: { label: string; value: string }) { return ( -
-

{label}

+
+

{label}

{value}

); @@ -176,16 +179,16 @@ function SingleColumnSample({ layout, compact, showLabels }: SampleProps) { > {layout.nameEn} - {compact ? "One clear read" : "One clear read"} + One clear read -

+

{layout.summary}

-
+

{layout.nameKo}

-

+

본문 폭을 제한하고 큰 제목, 짧은 설명, 반복 CTA를 같은 흐름 안에 둔 실제 읽기형 페이지입니다.

@@ -215,16 +218,16 @@ function TwoColumnSample({ layout, compact, showLabels }: SampleProps) {
method -

{layout.summary}

+

{layout.summary}