From cdb458fe2e1ab19f12bc81893cae6d63200ad301 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Wed, 3 Jun 2026 23:40:58 +0900 Subject: [PATCH 01/18] Tokenize LayoutPreviewRenderer: repoint to --st-*, apply type/shape tokens - All color refs repointed from --style-* to --st-* namespace - RawHeading uses --st-font-display, --st-weight-display, --st-tracking - Cards/containers use --st-radius, --st-shadow inline styles - RawButton uses --st-radius, --st-border-width - Body font uses --st-font-body Co-Authored-By: Claude Sonnet 4.6 --- .../web-layout/LayoutPreviewRenderer.tsx | 347 +++++++++++++----- 1 file changed, 250 insertions(+), 97 deletions(-) diff --git a/src/components/web-layout/LayoutPreviewRenderer.tsx b/src/components/web-layout/LayoutPreviewRenderer.tsx index 80f900b..a189cd1 100644 --- a/src/components/web-layout/LayoutPreviewRenderer.tsx +++ b/src/components/web-layout/LayoutPreviewRenderer.tsx @@ -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,16 @@ function RawHeading({ return (

{children}

@@ -108,9 +118,10 @@ function RawButton({ children, tone = "dark" }: { children: ReactNode; tone?: "d className={cn( "h-9 border px-4 text-[11px] font-bold uppercase tracking-[0.12em]", tone === "dark" - ? "border-[var(--style-primary)] bg-[var(--style-primary)] text-[var(--style-surface)]" - : "border-[rgb(var(--style-border-rgb)_/_0.28)] bg-[rgb(var(--style-surface-rgb)_/_0.80)] text-[var(--style-text)]", + ? "border-[var(--st-primary)] bg-[var(--st-primary)] text-[var(--st-surface)]" + : "border-[rgb(var(--st-border-rgb)_/_0.28)] bg-[rgb(var(--st-surface-rgb)_/_0.80)] text-[var(--st-text)]", )} + style={{ borderRadius: "var(--st-radius)", borderWidth: "var(--st-border-width)" }} type="button" > {children} @@ -122,11 +133,12 @@ function SoftScene({ className, children }: { className?: string; children?: Rea return (
{children}
@@ -135,21 +147,24 @@ function SoftScene({ className, children }: { className?: string; children?: Rea function ProductTile({ name, index }: { name: string; index: number }) { return ( -
-
+
+
- +

{name}

-

₩{128 + index * 17}

+

₩{128 + index * 17}

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

{label}

-

{value}

+
+

{label}

+

+ {value} +

); } @@ -181,14 +204,23 @@ function SingleColumnSample({ layout, compact, showLabels }: SampleProps) { One clear read -

+

{layout.summary}

-
-

+

+

{layout.nameKo}

-

+

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

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

{layout.summary}

+

+ {layout.summary} +

@@ -689,17 +817,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 +844,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} +

      ))}
      From d7d54dc4c06887e8111043efb9bac933cbdd9d5c Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Wed, 3 Jun 2026 23:45:22 +0900 Subject: [PATCH 02/18] Tokenize DesignStyleSampleRenderer: apply --st-* typography/shape tokens Sample renderers now spread styleTokenVars so --st-* variables are available on the sample root. Display/heading text uses --st-font-display, --st-weight-display, --st-tracking. Body text uses --st-font-body. Card containers use --st-radius and --st-shadow. Co-Authored-By: Claude Sonnet 4.6 --- .../DesignStyleSampleRenderer.tsx | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/src/components/design-style/DesignStyleSampleRenderer.tsx b/src/components/design-style/DesignStyleSampleRenderer.tsx index 6c3fa72..d540a4d 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), }; } @@ -84,7 +86,10 @@ function MinimalEditorial({ compact = false, style }: Props) {

      {style.category}

      -

      +

      {style.nameEn}

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

      +

      {style.summary}

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

      Raw format

      -

      +

      {style.nameEn}

      -

      +

      {style.summary}

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

      +
      +

      {style.nameEn}

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

      Atelier edition

      -

      +

      {style.nameEn}

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

      Natural system

      -

      +

      {style.nameEn}

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

      +

      {style.nameEn}

      daily app

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

      +
      +

      {style.nameEn}

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

      Issue 06

      -

      +

      {style.nameEn}

      @@ -321,14 +350,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}
      From e798f22e98928fa4fc54c35594184e41f7e19080 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Wed, 3 Jun 2026 23:48:23 +0900 Subject: [PATCH 03/18] =?UTF-8?q?Add=20/studio=20combine=20view:=20Style?= =?UTF-8?q?=20=C3=97=20Layout=20live=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StudioView client component with style/layout dropdowns, viewport toggle, token preview swatch, and URL-query sharing. Preview area applies chosen style tokens independently via its own style-preset-root. Copy buttons reserved as disabled placeholders for Phase 7. Also adds Studio link to primary navigation. Co-Authored-By: Claude Sonnet 4.6 --- src/app/layout.tsx | 1 + src/app/studio/page.tsx | 11 ++ src/components/studio/StudioView.tsx | 191 +++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/app/studio/page.tsx create mode 100644 src/components/studio/StudioView.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 01ea5fa..ede1d8a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -51,6 +51,7 @@ function RawNavigation() { {[ ["Library", "/web-layouts"], ["Styles", "/design-styles"], + ["Studio", "/studio"], ["Compare", "/web-layouts/compare"], ["Skill", "/web-layouts#layout-skill"], ].map(([label, href]) => ( 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/components/studio/StudioView.tsx b/src/components/studio/StudioView.tsx new file mode 100644 index 0000000..5941ee0 --- /dev/null +++ b/src/components/studio/StudioView.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useMemo } 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 type { PreviewViewport } from "@/components/web-layout/ViewportSwitcher"; + +const DEFAULT_STYLE = "brutalism"; +const DEFAULT_LAYOUT = "hero-layout"; + +function StudioViewInner() { + const router = useRouter(); + const params = useSearchParams(); + + const selectedStyleSlug = params.get("style") ?? DEFAULT_STYLE; + const selectedLayoutSlug = params.get("layout") ?? DEFAULT_LAYOUT; + const viewport = (params.get("vp") ?? "desktop") as PreviewViewport; + + const selectedStyle = useMemo( + () => designStyles.find((s) => s.slug === selectedStyleSlug) ?? designStyles[0], + [selectedStyleSlug], + ); + + const selectedLayout = useMemo( + () => webLayouts.find((l) => l.slug === selectedLayoutSlug) ?? webLayouts[0], + [selectedLayoutSlug], + ); + + 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]); + + return ( +
      +
      + + {/* Header */} +
      +

      Studio

      +

      + Style × Layout +

      +

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

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

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

      +
      + +
      +
      +
      + ); +} + +export function StudioView() { + return ( + + + + ); +} From 3a01ee15efab969f62c4a462a3417392c26a43db Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Wed, 3 Jun 2026 23:56:17 +0900 Subject: [PATCH 04/18] Restructure routes: /web-layouts->/layouts, /design-styles->/styles Rename route directories with git mv, add permanent redirects from old URLs in next.config.ts, update all internal links and docs. API route /api/design-style-images unchanged. Co-Authored-By: Claude Sonnet 4.6 --- CONTRIBUTING.md | 2 +- README.en.md | 32 +++++++++---------- README.md | 32 +++++++++---------- next.config.ts | 9 +++++- skills/design-style-recommender/SKILL.md | 4 +-- skills/layout-recommender/SKILL.md | 4 +-- src/app/layout.tsx | 26 +++++++-------- .../{web-layouts => layouts}/[slug]/page.tsx | 2 +- .../{web-layouts => layouts}/compare/page.tsx | 2 +- src/app/{web-layouts => layouts}/page.tsx | 4 +-- src/app/page.tsx | 2 +- .../{design-styles => styles}/[slug]/page.tsx | 2 +- .../generate/page.tsx | 2 +- src/app/{design-styles => styles}/page.tsx | 4 +-- .../design-style/DesignStyleCard.tsx | 2 +- .../style-preset/AppliedStyleStrip.tsx | 2 +- src/components/web-layout/WebLayoutCard.tsx | 2 +- .../web-layout/WebLayoutCompare.tsx | 2 +- .../web-layout/WebLayoutExplorer.tsx | 2 +- 19 files changed, 72 insertions(+), 65 deletions(-) rename src/app/{web-layouts => layouts}/[slug]/page.tsx (99%) rename src/app/{web-layouts => layouts}/compare/page.tsx (97%) rename src/app/{web-layouts => layouts}/page.tsx (98%) rename src/app/{design-styles => styles}/[slug]/page.tsx (99%) rename src/app/{design-styles => styles}/generate/page.tsx (96%) rename src/app/{design-styles => styles}/page.tsx (97%) 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..e933c62 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ 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에 유지됩니다. - **Prompt palette**: 프롬프트를 섞어 커스텀 색상표를 생성하고 현재 레이아웃 프리뷰에 바로 적용합니다. - **Image generation admin**: `OPENAI_API_KEY`가 있는 로컬 환경에서 스타일별 참조 이미지를 생성해 `public/generated/design-styles`에 저장할 수 있습니다. - **SVG controls**: 비교 화살표와 설명/닫기/상세 아이콘은 inline SVG로 관리합니다. @@ -62,25 +62,25 @@ 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개 레이아웃 비교와 큰 구조 미리보기 | +| `/styles` | 디자인 형식 검색, 카테고리/태그 필터, 색상표, 웹페이지형 스타일 샘플 | +| `/styles/[slug]` | 디자인 형식 상세 설명, 색상표, 타이포/레이아웃 특징, 관련 스타일 | +| `/styles/generate` | OpenAI Image API 기반 로컬 참조 이미지 생성 관리자 | ## 이미지 생성 환경 변수 -`/design-styles/generate`와 `/api/design-style-images`는 로컬 관리자 기능입니다. +`/styles/generate`와 `/api/design-style-images`는 로컬 관리자 기능입니다. ```bash OPENAI_API_KEY=sk-... @@ -122,12 +122,12 @@ 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/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/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/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/layout.tsx b/src/app/layout.tsx index ede1d8a..a1f934e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -43,17 +43,17 @@ function RawNavigation() {
      Openlayout
      {[ - ["Library", "/web-layouts"], - ["Styles", "/design-styles"], + ["Library", "/layouts"], + ["Styles", "/styles"], ["Studio", "/studio"], - ["Compare", "/web-layouts/compare"], - ["Skill", "/web-layouts#layout-skill"], + ["Compare", "/layouts/compare"], + ["Skill", "/layouts#layout-skill"], ].map(([label, href]) => ( @@ -108,18 +108,18 @@ function RawFooter() {
      diff --git a/src/app/web-layouts/[slug]/page.tsx b/src/app/layouts/[slug]/page.tsx similarity index 99% rename from src/app/web-layouts/[slug]/page.tsx rename to src/app/layouts/[slug]/page.tsx index 044adc3..c60194c 100644 --- a/src/app/web-layouts/[slug]/page.tsx +++ b/src/app/layouts/[slug]/page.tsx @@ -47,7 +47,7 @@ export default async function LayoutDetailPage({ params }: LayoutDetailPageProps
      목록으로 돌아가기 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/design-styles/[slug]/page.tsx b/src/app/styles/[slug]/page.tsx similarity index 99% rename from src/app/design-styles/[slug]/page.tsx rename to src/app/styles/[slug]/page.tsx index 42f9b2b..6536e54 100644 --- a/src/app/design-styles/[slug]/page.tsx +++ b/src/app/styles/[slug]/page.tsx @@ -46,7 +46,7 @@ export default async function DesignStyleDetailPage({ return (
      - + 목록으로 돌아가기 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/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/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/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() {

      레이아웃 비교하기 From bece074f8c92f8609250024574ccd3010537bd79 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 00:24:47 +0900 Subject: [PATCH 05/18] Split radius token into card vs pill to fix oversized rounding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single --st-radius applied uniformly turned large kawaii cards into giant stadium shapes. Add a separate radiusPill token: - --st-radius: cards/containers (kawaii now 20px, not 9999px) - --st-radius-pill: buttons/small elements (kawaii 9999px) Category defaults set radiusPill = radius except 귀여움/캐주얼. RawButton uses --st-radius-pill. check-data asserts both fields. Co-Authored-By: Claude Opus 4.8 --- scripts/check-data.mjs | 2 ++ src/components/style-preset/styleTokenVars.ts | 1 + .../web-layout/LayoutPreviewRenderer.tsx | 2 +- src/data/designStyles.ts | 23 ++++++++++--------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/scripts/check-data.mjs b/scripts/check-data.mjs index 61206db..2c59b2c 100644 --- a/scripts/check-data.mjs +++ b/scripts/check-data.mjs @@ -27,6 +27,8 @@ 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.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(s.tokens.color.base === s.palette.base, `style ${s.slug} tokens.color.base mismatch`); } diff --git a/src/components/style-preset/styleTokenVars.ts b/src/components/style-preset/styleTokenVars.ts index 8bfefa5..a390974 100644 --- a/src/components/style-preset/styleTokenVars.ts +++ b/src/components/style-preset/styleTokenVars.ts @@ -42,6 +42,7 @@ 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, // Space tokens "--st-gap": space.gap, diff --git a/src/components/web-layout/LayoutPreviewRenderer.tsx b/src/components/web-layout/LayoutPreviewRenderer.tsx index a189cd1..8558483 100644 --- a/src/components/web-layout/LayoutPreviewRenderer.tsx +++ b/src/components/web-layout/LayoutPreviewRenderer.tsx @@ -121,7 +121,7 @@ function RawButton({ children, tone = "dark" }: { children: ReactNode; tone?: "d ? "border-[var(--st-primary)] bg-[var(--st-primary)] text-[var(--st-surface)]" : "border-[rgb(var(--st-border-rgb)_/_0.28)] bg-[rgb(var(--st-surface-rgb)_/_0.80)] text-[var(--st-text)]", )} - style={{ borderRadius: "var(--st-radius)", borderWidth: "var(--st-border-width)" }} + style={{ borderRadius: "var(--st-radius-pill)", borderWidth: "var(--st-border-width)" }} type="button" > {children} diff --git a/src/data/designStyles.ts b/src/data/designStyles.ts index e1816b2..726b284 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" }, @@ -583,7 +584,7 @@ const styleTokenOverrides: Record decoration: { shadow: "0 0 24px rgb(var(--st-accent-rgb) / 0.6)", effect: "glow" }, }, "kawaii": { - shape: { radius: "9999px", borderWidth: "2px" }, + shape: { radius: "20px", radiusPill: "9999px", borderWidth: "2px" }, decoration: { shadow: "4px 4px 0 var(--st-accent)", effect: "none" }, }, "luxury": { From e85b8a5c176e0c32f047cdfede75ca3643f6f39f Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 00:55:27 +0900 Subject: [PATCH 06/18] Add reference screenshot tooling for style token research - scripts/style-references.json: curated reference URLs for 12 styles - scripts/capture-references.mjs: Playwright auto-capture script - npm run capture:refs (or with slug args: capture:refs brutalism kawaii) - public/references/ added to .gitignore (copyright, local-only) - playwright added as devDependency Setup: npm install && npx playwright install chromium Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 ++ package.json | 4 +- scripts/capture-references.mjs | 107 +++++++++++++++++++++++++++++++++ scripts/style-references.json | 51 ++++++++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 scripts/capture-references.mjs create mode 100644 scripts/style-references.json 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/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..a7174b1 --- /dev/null +++ b/scripts/capture-references.mjs @@ -0,0 +1,107 @@ +/** + * capture-references.mjs + * + * Takes viewport screenshots of reference websites for each design style. + * Screenshots are saved to public/references/[slug]/ and GITIGNORED. + * + * Usage: + * node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/capture-references.mjs + * + * Requirements: + * npm install --save-dev playwright + * npx playwright install chromium + * + * Optional — capture only specific styles: + * node ... scripts/capture-references.mjs brutalism kawaii luxury + */ + +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 — if slugs given, only capture those +const targetSlugs = process.argv.slice(2); +const slugsToCapture = targetSlugs.length > 0 + ? targetSlugs + : Object.keys(references).filter((k) => !k.startsWith("_")); + +const VIEWPORT = { width: 1440, height: 900 }; +const TIMEOUT = 20_000; + +async function capture() { + console.log(`\n📸 Capturing reference screenshots for: ${slugsToCapture.join(", ")}\n`); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: VIEWPORT }); + + let total = 0; + let failed = 0; + + for (const slug of slugsToCapture) { + const sites = references[slug]; + if (!sites) { + console.warn(`⚠️ No references found for: ${slug}`); + continue; + } + + const outDir = join(OUT_DIR, slug); + await mkdir(outDir, { recursive: true }); + + for (const { url, title } of sites) { + const filename = url + .replace(/^https?:\/\//, "") + .replace(/[^a-z0-9]/gi, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 60) + ".jpg"; + + const outPath = join(outDir, filename); + + if (existsSync(outPath)) { + console.log(` ⏭ ${slug}/${filename} — already exists, skipping`); + continue; + } + + process.stdout.write(` 📷 ${slug} — ${title} ... `); + const page = await context.newPage(); + + try { + await page.goto(url, { waitUntil: "domcontentloaded", timeout: TIMEOUT }); + // Wait a moment for above-the-fold visuals to settle + await page.waitForTimeout(2000); + + const buf = await page.screenshot({ + type: "jpeg", + quality: 88, + clip: { x: 0, y: 0, width: VIEWPORT.width, height: VIEWPORT.height }, + }); + + await writeFile(outPath, buf); + total++; + console.log(`✓ saved ${filename}`); + } catch (err) { + failed++; + console.log(`✗ failed — ${err.message.split("\n")[0]}`); + } finally { + await page.close(); + } + } + } + + await context.close(); + await browser.close(); + + console.log(`\n✅ Done — ${total} captured, ${failed} failed`); + console.log(`📁 Saved to: public/references/\n`); +} + +capture().catch((err) => { + console.error("\n❌ Fatal error:", err.message); + process.exit(1); +}); diff --git a/scripts/style-references.json b/scripts/style-references.json new file mode 100644 index 0000000..c237a5d --- /dev/null +++ b/scripts/style-references.json @@ -0,0 +1,51 @@ +{ + "_comment": "Reference websites for each design style. Used by capture-references.mjs to take local screenshots. DO NOT commit public/references/ — screenshots are local-only (copyright).", + "minimalism": [ + { "url": "https://linear.app", "title": "Linear — product UI minimalism" }, + { "url": "https://notion.so", "title": "Notion — content-first minimalism" } + ], + "brutalism": [ + { "url": "https://www.balenciaga.com", "title": "Balenciaga — fashion brutalism" }, + { "url": "https://www.bloomberg.com", "title": "Bloomberg — editorial brutalism" } + ], + "cyberpunk": [ + { "url": "https://www.cyberpunk.net", "title": "Cyberpunk 2077 — neon/dark digital" }, + { "url": "https://www.razer.com", "title": "Razer — green neon tech aesthetic" } + ], + "luxury": [ + { "url": "https://www.loewe.com", "title": "Loewe — quiet luxury, serif, airy" }, + { "url": "https://www.bottegaveneta.com", "title": "Bottega Veneta — understated luxury" } + ], + "organic-design": [ + { "url": "https://www.aesop.com", "title": "Aesop — warm organic brand" }, + { "url": "https://www.goop.com", "title": "Goop — wellness, earthy tones" } + ], + "kawaii": [ + { "url": "https://www.sanrio.com", "title": "Sanrio — kawaii brand identity" }, + { "url": "https://line.me", "title": "LINE — playful app aesthetic" } + ], + "streetwear": [ + { "url": "https://www.supremenewyork.com", "title": "Supreme — raw street simplicity" }, + { "url": "https://www.palace.ltd", "title": "Palace — skate brand graphic energy" } + ], + "editorial-design": [ + { "url": "https://www.nytimes.com", "title": "New York Times — editorial hierarchy" }, + { "url": "https://monocle.com", "title": "Monocle — magazine grid system" } + ], + "glassmorphism": [ + { "url": "https://www.apple.com/ios", "title": "Apple iOS — frosted glass UI" }, + { "url": "https://www.microsoft.com/en-us/windows", "title": "Windows 11 — Fluent Design glass" } + ], + "y2k": [ + { "url": "https://web.archive.org/web/20020101000000*/myspace.com", "title": "MySpace archive — Y2K web nostalgia" }, + { "url": "https://www.blingee.com", "title": "Blingee — chrome/glitter Y2K energy" } + ], + "maximalism": [ + { "url": "https://www.gucci.com", "title": "Gucci — maximalist luxury brand" }, + { "url": "https://www.versace.com", "title": "Versace — bold maximalist patterns" } + ], + "swiss-design": [ + { "url": "https://www.swissinfo.ch", "title": "Swissinfo — grid-based editorial" }, + { "url": "https://www.sbb.ch", "title": "SBB Swiss Railways — international style" } + ] +} From 26cc6a740249af0fa8553cf6bffe93b436417a80 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 01:09:07 +0900 Subject: [PATCH 07/18] Improve style references: better URLs, add galleries, sites/galleries split - Replaced broken/flaky links (MySpace archive, Blingee) with reliable ones - Added gallery sources: Dribbble, Awwwards, Pinterest, Behance per style - Split into 'sites' (real brand pages) and 'galleries' (inspiration) - Added note field explaining token research value of each reference - capture-references.mjs now supports --sites-only flag and writes .txt sidecar Co-Authored-By: Claude Sonnet 4.6 --- scripts/capture-references.mjs | 141 +++++++++++++++--------- scripts/style-references.json | 196 ++++++++++++++++++++++++--------- 2 files changed, 237 insertions(+), 100 deletions(-) diff --git a/scripts/capture-references.mjs b/scripts/capture-references.mjs index a7174b1..3e5726c 100644 --- a/scripts/capture-references.mjs +++ b/scripts/capture-references.mjs @@ -3,16 +3,16 @@ * * 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: - * node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/capture-references.mjs + * 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 * - * Requirements: - * npm install --save-dev playwright + * First-time setup: + * npm install * npx playwright install chromium - * - * Optional — capture only specific styles: - * node ... scripts/capture-references.mjs brutalism kawaii luxury */ import { chromium } from "playwright"; @@ -25,8 +25,11 @@ 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 — if slugs given, only capture those -const targetSlugs = process.argv.slice(2); +// 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("_")); @@ -34,18 +37,70 @@ const slugsToCapture = targetSlugs.length > 0 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 for: ${slugsToCapture.join(", ")}\n`); + 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 sites = references[slug]; - if (!sites) { + const entry = references[slug]; + if (!entry) { console.warn(`⚠️ No references found for: ${slug}`); continue; } @@ -53,52 +108,36 @@ async function capture() { const outDir = join(OUT_DIR, slug); await mkdir(outDir, { recursive: true }); - for (const { url, title } of sites) { - const filename = url - .replace(/^https?:\/\//, "") - .replace(/[^a-z0-9]/gi, "-") - .replace(/-+/g, "-") - .replace(/^-|-$/g, "") - .slice(0, 60) + ".jpg"; - - const outPath = join(outDir, filename); - - if (existsSync(outPath)) { - console.log(` ⏭ ${slug}/${filename} — already exists, skipping`); - continue; - } - - process.stdout.write(` 📷 ${slug} — ${title} ... `); - const page = await context.newPage(); - - try { - await page.goto(url, { waitUntil: "domcontentloaded", timeout: TIMEOUT }); - // Wait a moment for above-the-fold visuals to settle - await page.waitForTimeout(2000); - - const buf = await page.screenshot({ - type: "jpeg", - quality: 88, - clip: { x: 0, y: 0, width: VIEWPORT.width, height: VIEWPORT.height }, - }); - - await writeFile(outPath, buf); - total++; - console.log(`✓ saved ${filename}`); - } catch (err) { - failed++; - console.log(`✗ failed — ${err.message.split("\n")[0]}`); - } finally { - await page.close(); - } + 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 — ${total} captured, ${failed} failed`); - console.log(`📁 Saved to: public/references/\n`); + console.log(`\n✅ Done`); + console.log(` Captured: ${total} Failed: ${failed} Skipped (exists): ${skipped}`); + console.log(` 📁 public/references/\n`); } capture().catch((err) => { diff --git a/scripts/style-references.json b/scripts/style-references.json index c237a5d..4132203 100644 --- a/scripts/style-references.json +++ b/scripts/style-references.json @@ -1,51 +1,149 @@ { - "_comment": "Reference websites for each design style. Used by capture-references.mjs to take local screenshots. DO NOT commit public/references/ — screenshots are local-only (copyright).", - "minimalism": [ - { "url": "https://linear.app", "title": "Linear — product UI minimalism" }, - { "url": "https://notion.so", "title": "Notion — content-first minimalism" } - ], - "brutalism": [ - { "url": "https://www.balenciaga.com", "title": "Balenciaga — fashion brutalism" }, - { "url": "https://www.bloomberg.com", "title": "Bloomberg — editorial brutalism" } - ], - "cyberpunk": [ - { "url": "https://www.cyberpunk.net", "title": "Cyberpunk 2077 — neon/dark digital" }, - { "url": "https://www.razer.com", "title": "Razer — green neon tech aesthetic" } - ], - "luxury": [ - { "url": "https://www.loewe.com", "title": "Loewe — quiet luxury, serif, airy" }, - { "url": "https://www.bottegaveneta.com", "title": "Bottega Veneta — understated luxury" } - ], - "organic-design": [ - { "url": "https://www.aesop.com", "title": "Aesop — warm organic brand" }, - { "url": "https://www.goop.com", "title": "Goop — wellness, earthy tones" } - ], - "kawaii": [ - { "url": "https://www.sanrio.com", "title": "Sanrio — kawaii brand identity" }, - { "url": "https://line.me", "title": "LINE — playful app aesthetic" } - ], - "streetwear": [ - { "url": "https://www.supremenewyork.com", "title": "Supreme — raw street simplicity" }, - { "url": "https://www.palace.ltd", "title": "Palace — skate brand graphic energy" } - ], - "editorial-design": [ - { "url": "https://www.nytimes.com", "title": "New York Times — editorial hierarchy" }, - { "url": "https://monocle.com", "title": "Monocle — magazine grid system" } - ], - "glassmorphism": [ - { "url": "https://www.apple.com/ios", "title": "Apple iOS — frosted glass UI" }, - { "url": "https://www.microsoft.com/en-us/windows", "title": "Windows 11 — Fluent Design glass" } - ], - "y2k": [ - { "url": "https://web.archive.org/web/20020101000000*/myspace.com", "title": "MySpace archive — Y2K web nostalgia" }, - { "url": "https://www.blingee.com", "title": "Blingee — chrome/glitter Y2K energy" } - ], - "maximalism": [ - { "url": "https://www.gucci.com", "title": "Gucci — maximalist luxury brand" }, - { "url": "https://www.versace.com", "title": "Versace — bold maximalist patterns" } - ], - "swiss-design": [ - { "url": "https://www.swissinfo.ch", "title": "Swissinfo — grid-based editorial" }, - { "url": "https://www.sbb.ch", "title": "SBB Swiss Railways — international style" } - ] + "_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" } + ] + } } From 6a849c71db02b24519d55048e5c89568ef5d7400 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:09:57 +0900 Subject: [PATCH 08/18] Fix studio query defaults and selection normalization --- src/components/studio/StudioView.tsx | 49 +++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/components/studio/StudioView.tsx b/src/components/studio/StudioView.tsx index 5941ee0..3ac6fa3 100644 --- a/src/components/studio/StudioView.tsx +++ b/src/components/studio/StudioView.tsx @@ -1,7 +1,7 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { Suspense, useMemo } from "react"; +import { Suspense, useEffect, useMemo } from "react"; import { designStyles } from "@/data/designStyles"; import { webLayouts } from "@/data/webLayouts"; import { styleTokenVars } from "@/components/style-preset/styleTokenVars"; @@ -9,26 +9,57 @@ import { LayoutPreviewRenderer } from "@/components/web-layout/LayoutPreviewRend import type { PreviewViewport } from "@/components/web-layout/ViewportSwitcher"; const DEFAULT_STYLE = "brutalism"; -const DEFAULT_LAYOUT = "hero-layout"; +const DEFAULT_LAYOUT = "hero-focused-layout"; function StudioViewInner() { const router = useRouter(); const params = useSearchParams(); - const selectedStyleSlug = params.get("style") ?? DEFAULT_STYLE; - const selectedLayoutSlug = params.get("layout") ?? DEFAULT_LAYOUT; - const viewport = (params.get("vp") ?? "desktop") as PreviewViewport; + 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 === selectedStyleSlug) ?? designStyles[0], - [selectedStyleSlug], + () => + designStyles.find((s) => s.slug === requestedStyleSlug) ?? + designStyles.find((s) => s.slug === DEFAULT_STYLE) ?? + designStyles[0], + [requestedStyleSlug], ); const selectedLayout = useMemo( - () => webLayouts.find((l) => l.slug === selectedLayoutSlug) ?? webLayouts[0], - [selectedLayoutSlug], + () => + 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); From 9b4e09e90475cc81caa57cc9437267afa30b96be Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:11:57 +0900 Subject: [PATCH 09/18] Complete style token variable and utility coverage --- scripts/check-data.mjs | 4 +++ src/app/globals.css | 28 +++++++++++++++++-- src/components/style-preset/styleTokenVars.ts | 1 + 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/scripts/check-data.mjs b/scripts/check-data.mjs index 2c59b2c..5ef3642 100644 --- a/scripts/check-data.mjs +++ b/scripts/check-data.mjs @@ -27,8 +27,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/src/app/globals.css b/src/app/globals.css index 3d789b2..2c872b8 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(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/components/style-preset/styleTokenVars.ts b/src/components/style-preset/styleTokenVars.ts index a390974..2903322 100644 --- a/src/components/style-preset/styleTokenVars.ts +++ b/src/components/style-preset/styleTokenVars.ts @@ -44,6 +44,7 @@ export function styleTokenVars(style: DesignStyle): CSSProperties { "--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), From 88fd19d94aecc07a60dd26f03954c04cf39ddaf7 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:17:06 +0900 Subject: [PATCH 10/18] Restore Playwright lock entries --- package-lock.json | 63 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 12 deletions(-) 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", From abc575a75a9fb0c6662e3d11c7f4c8f3b81a846a Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:17:16 +0900 Subject: [PATCH 11/18] Apply spacing and typography tokens across previews --- src/app/globals.css | 2 +- .../DesignStyleSampleRenderer.tsx | 11 ++++--- .../web-layout/LayoutPreviewRenderer.tsx | 32 +++++++++---------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 2c872b8..eb40a03 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -497,7 +497,7 @@ main header { .st-display { font-family: var(--st-font-display, var(--font-display)); - font-size: calc(1em * var(--st-heading-scale, 1)); + 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); } diff --git a/src/components/design-style/DesignStyleSampleRenderer.tsx b/src/components/design-style/DesignStyleSampleRenderer.tsx index d540a4d..675cbeb 100644 --- a/src/components/design-style/DesignStyleSampleRenderer.tsx +++ b/src/components/design-style/DesignStyleSampleRenderer.tsx @@ -42,8 +42,8 @@ function SampleFrame({ return (
      - +
      + Studio
      diff --git a/src/components/web-layout/LayoutPreviewRenderer.tsx b/src/components/web-layout/LayoutPreviewRenderer.tsx index 8558483..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,7 +36,7 @@ function Region({ return (
      - + Raw Co.
      {compact ? ( - + Menu ) : ( @@ -96,16 +96,14 @@ function RawHeading({ return (

      {children}

      @@ -116,12 +114,12 @@ function RawButton({ children, tone = "dark" }: { children: ReactNode; tone?: "d return (
      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"); +} From 7eccf5f5a0a8e602c2436b3d043c93f6b6f1cd7a Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:23:47 +0900 Subject: [PATCH 14/18] Add code copy export for studio combinations --- src/components/studio/StudioView.tsx | 14 ++++-- src/lib/exportCode.ts | 72 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/lib/exportCode.ts diff --git a/src/components/studio/StudioView.tsx b/src/components/studio/StudioView.tsx index 24bddb7..c12ec91 100644 --- a/src/components/studio/StudioView.tsx +++ b/src/components/studio/StudioView.tsx @@ -6,6 +6,7 @@ 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"; @@ -76,6 +77,12 @@ function StudioViewInner() { window.setTimeout(() => setCopied(null), 1400); } + async function copyCode() { + await navigator.clipboard.writeText(exportDesignCode(selectedStyle, selectedLayout)); + setCopied("code"); + window.setTimeout(() => setCopied(null), 1400); + } + return (
      @@ -175,12 +182,11 @@ function StudioViewInner() { {/* Copy buttons (Phase 7 — disabled placeholder) */}
      +
      + + +`; +} From bacdb474a730e9ac9c5cb9f7e1fc9ed53c450b88 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:25:48 +0900 Subject: [PATCH 15/18] Add copy actions to detail pages --- README.md | 4 ++++ src/app/layouts/[slug]/page.tsx | 9 ++++++++ src/app/styles/[slug]/page.tsx | 8 +++++++ src/components/export/CopyTextButton.tsx | 29 ++++++++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 src/components/export/CopyTextButton.tsx diff --git a/README.md b/README.md index e933c62..71d8762 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ openlayout은 디자인을 시작하기 전에 페이지 구조와 시각 언어 - **Compare view**: 최대 3개의 레이아웃을 선택해 추천 용도, 모바일 대응, 밀도, 난이도를 나란히 비교합니다. - **Design Style Library**: 88개의 디자인 형식을 카테고리, 태그, 검색어로 탐색하고 상세 페이지에서 색상표와 웹페이지형 샘플을 확인합니다. - **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로 관리합니다. @@ -74,9 +76,11 @@ http://localhost:3000/layouts | `/layouts` | 레이아웃 검색, 필터, 카드 목록 | | `/layouts/[slug]` | 구조 설명, 장단점, 반응형 동작, 접근성 노트, 라이브 프리뷰, 코드 예시 | | `/layouts/compare` | 최대 3개 레이아웃 비교와 큰 구조 미리보기 | +| `/studio` | 디자인 스타일과 레이아웃을 조합해 실제 웹 프리뷰를 보고 코드/프롬프트를 복사 | | `/styles` | 디자인 형식 검색, 카테고리/태그 필터, 색상표, 웹페이지형 스타일 샘플 | | `/styles/[slug]` | 디자인 형식 상세 설명, 색상표, 타이포/레이아웃 특징, 관련 스타일 | | `/styles/generate` | OpenAI Image API 기반 로컬 참조 이미지 생성 관리자 | +| `/components` | 디자인 스타일 토큰을 버튼, 카드, 내비게이션, 입력 필드, 배지에 적용해 비교 | ## 이미지 생성 환경 변수 diff --git a/src/app/layouts/[slug]/page.tsx b/src/app/layouts/[slug]/page.tsx index c60194c..9360075 100644 --- a/src/app/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 = { @@ -81,6 +83,13 @@ export default async function LayoutDetailPage({ params }: LayoutDetailPageProps

      {layout.description}

      +
      + +
      diff --git a/src/app/styles/[slug]/page.tsx b/src/app/styles/[slug]/page.tsx index 6536e54..f38b85f 100644 --- a/src/app/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"; @@ -94,6 +95,13 @@ export default async function DesignStyleDetailPage({
      +
      + +
                     {style.imagePrompt}
                   
      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 ( + + ); +} From 40a69b7bf4b7dd9b16f46dc44ae0a7e4c8f12ac5 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:28:39 +0900 Subject: [PATCH 16/18] Add tokenized component dictionary MVP --- README.md | 4 + src/app/components/page.tsx | 23 ++++++ src/app/layout.tsx | 3 + .../ComponentDictionaryView.tsx | 74 +++++++++++++++++++ .../ComponentPreviewRenderer.tsx | 70 ++++++++++++++++++ src/data/componentSpecs.ts | 47 ++++++++++++ 6 files changed, 221 insertions(+) create mode 100644 src/app/components/page.tsx create mode 100644 src/components/component-dictionary/ComponentDictionaryView.tsx create mode 100644 src/components/component-dictionary/ComponentPreviewRenderer.tsx create mode 100644 src/data/componentSpecs.ts diff --git a/README.md b/README.md index 71d8762..8eac7a0 100644 --- a/README.md +++ b/README.md @@ -129,14 +129,18 @@ npm run build 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/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/layout.tsx b/src/app/layout.tsx index a1f934e..8a99402 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -52,6 +52,7 @@ function RawNavigation() { ["Library", "/layouts"], ["Styles", "/styles"], ["Studio", "/studio"], + ["Components", "/components"], ["Compare", "/layouts/compare"], ["Skill", "/layouts#layout-skill"], ].map(([label, href]) => ( @@ -110,6 +111,8 @@ function RawFooter() { items={[ ["Library", "/layouts"], ["Styles", "/styles"], + ["Studio", "/studio"], + ["Components", "/components"], ["Compare", "/layouts/compare"], ["GitHub", "https://github.com/pandaofwild/openlayout"], ]} 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/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: "상태, 카테고리, 짧은 메타 정보를 표시합니다.", + }, +]; From b8a706722c0abab271e6f2821baaca12995b49d5 Mon Sep 17 00:00:00 2001 From: pandaofwild Date: Thu, 4 Jun 2026 14:40:28 +0900 Subject: [PATCH 17/18] Fix studio mobile preview overflow --- src/components/studio/StudioView.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/studio/StudioView.tsx b/src/components/studio/StudioView.tsx index c12ec91..6cf119c 100644 --- a/src/components/studio/StudioView.tsx +++ b/src/components/studio/StudioView.tsx @@ -98,10 +98,10 @@ function StudioViewInner() {

      -
      +
      {/* Left: Controls */} -