✏️

Pencilを使った『デザイン駆動開発』

に公開

はじめに

Pencil はrepo内に .pen ファイルとしてデザインを保存できるツールです。AIエージェントがMCP経由でデザインの構造を読み書きできるので、デザインファイルをSOT(Single Source of Truth)にして、型定義・API・コンポーネントを導出するという開発フローが可能になります。

この記事では、プロフィールカードを題材にこのフローを実際に試してみます。

Pencilとは

PencilはGUIでUIをデザインし、成果物を .pen ファイルとしてrepo内に保存できるツールです。特徴は2つあります。

1. ファイルがrepoに入る。 .pen ファイルはgitで管理でき、コードと同じcommitでデザインの変更を追跡できます。

2. AIエージェントが構造を読み書きできる。 PencilはMCP(Model Context Protocol)サーバーを提供しています。MCPはAIエージェントが外部ツールを関数のように呼び出すためのプロトコルで、Claude CodeやCursorなどが対応しています。PencilのMCPサーバーを通じて、エージェントからデザインの読み取り(batch_get)や編集(batch_design)ができます。

今回作るもの

プロフィールカードを題材に、デザインをSOTにした開発フローを試してみます。

  1. Pencilでプロフィールカードをデザインする ← 人間が判断するのはここだけ
  2. .pen の構造からAPIスキーマ(型定義)を導出する ← AIが構造を読んで自動導出
  3. 型定義を元にHonoで実装する ← 型とスタイルから機械的に実装

フレームワークにはHonoを選びました。JSXが組み込みで、ビルドステップなしにSSRできるので、最小構成でデモが動きます。

記事の最後では、デザイン変更時に下流の型・API・コンポーネントが自動で追従する流れも紹介します。

1. Pencilでプロフィールカードをデザインする

Pencilでデザインを作る方法は2つあります。GUIエディタで直接操作するか、AIエージェントとの対話で進めるか。ここではClaude CodeからMCPツール経由でデザインを組み立てた過程を紹介します。

まずPencilの get_style_guide ツールでスタイルガイド(Swiss Clean)を取得し、その配色・タイポグラフィに沿ってデザインします。次に batch_design ツールでフレーム内に要素を配置していきます。I はInsert操作で、第1引数が親ノード、第2引数がプロパティです:

// "screen" は .pen ファイルのルートキャンバスを指す
card=I("screen", {type: "frame", name: "Profile Card", width: 400,
  layout: "vertical", alignItems: "center", padding: 32, gap: 20,
  fill: "#FFFFFF", cornerRadius: 16, stroke: {fill: "#E4E4E7", thickness: 1.5}})
avatar=I(card, {type: "ellipse", name: "Avatar", width: 80, height: 80})
nameText=I(card, {type: "text", name: "Name", content: "Taro Yamada",
  fontSize: 28, fontWeight: "600", fill: "#18181B"})
titleText=I(card, {type: "text", name: "Title", content: "Full-Stack Developer",
  fontSize: 15, fill: "#71717A"})

実際の作業では「プロフィールカードを作って。名前、肩書き、自己紹介、SNSリンク、スキルタグを含めて」のような自然言語の指示でデザインが生成されます。人間はAIと対話しながら「何をどう見せるか」を決め、AIがそれを .pen に反映していきます。

今回作成したプロフィールカードの構成:

要素 デザイン上のノード
アバター画像 円形(80×80px)
名前 Inter 28px / SemiBold / #18181B
肩書き Inter 15px / Regular / #71717A
自己紹介文 Inter 14px / Regular / #71717A / 中央揃え
SNSリンク アイコン + テキスト(GitHub, X)
スキルタグ ピル型バッジ(TypeScript, React, Hono, Node.js)

カード全体は白背景 (#FFFFFF)、角丸16px、ボーダー #E4E4E7 1.5px というシンプルな構成です。

ポイント:人間が考えるのはこの工程だけです。 デザインが決まれば、以降はすべて .pen の構造から導出できます。

2. デザインからAPIスキーマを導出する

デザインが完成したので、ここから .pen の構造を読み取って型定義を導出していきます。MCPの batch_get ツールを実行すると、こんなJSON構造が返ってきます(見やすさのため一部省略):

{
  "name": "Profile Card",
  "type": "frame",
  "layout": "vertical",
  "width": 400,
  "alignItems": "center",
  "padding": 32,
  "gap": 20,
  "fill": "#FFFFFF",
  "cornerRadius": 16,
  "stroke": { "fill": "#E4E4E7", "thickness": 1.5 },
  "children": [
    { "name": "Avatar", "type": "ellipse", "width": 80, "height": 80,
      "fill": { "type": "image", "url": "..." } },
    { "name": "Name", "type": "text", "content": "Taro Yamada",
      "fontSize": 28, "fontWeight": "600", "fill": "#18181B" },
    { "name": "Title", "type": "text", "content": "Full-Stack Developer",
      "fontSize": 15, "fill": "#71717A" },
    { "name": "Bio", "type": "text", "content": "Web technologies enthusiast...",
      "fontSize": 14, "fill": "#71717A", "textAlign": "center" },
    { "name": "Divider", "type": "rectangle", "height": 1, "fill": "#E4E4E7" },
    { "name": "SNS Links", "type": "frame", "gap": 24, "children": [
      { "name": "GitHub Icon", "type": "icon_font", "iconFontName": "github" },
      { "name": "GitHub", "type": "text", "content": "github.com/example-taro" },
      { "name": "X Icon", "type": "icon_font", "iconFontName": "twitter" },
      { "name": "X", "type": "text", "content": "@example_taro" }
    ]},
    { "name": "Skill Tags", "type": "frame", "gap": 8, "children": [
      { "name": "Tag", "type": "frame", "cornerRadius": 100, "fill": "#EFF6FF",
        "children": [{ "content": "TypeScript" }] },
      { "name": "Tag", "type": "frame", "children": [{ "content": "React" }] },
      { "name": "Tag", "type": "frame", "children": [{ "content": "Hono" }] },
      { "name": "Tag", "type": "frame", "children": [{ "content": "Node.js" }] }
    ]}
  ]
}

ポイントは、このJSONにデータフィールドだけでなく、レイアウト情報(layout, gap, padding, alignItems)やスタイル情報(fontSize, fill, cornerRadius)も含まれていることです。つまり型定義だけでなく、CSSの値もここから直接取れます。スクリーンショットではなく構造データだからこそ、値が正確に伝わります。

この構造から型定義を導出するプロセスは、機械的な変換と意味推論の2段階に分かれます。

機械的な変換

ノードの type と name から直接マッピングできるフィールドです:

  • type: "text", name: "Name" → テキスト内容 → name: string
  • type: "text", name: "Title" → テキスト内容 → title: string
  • type: "text", name: "Bio" → テキスト内容 → bio: string
  • type: "ellipse", fill.type: "image" → 画像URL → avatarUrl: string
  • name: "Skill Tags" + 同構造の Tag の繰り返し → skills: string[]

テキストノードは string、画像フィルを持つ図形は画像URL、同じ構造の繰り返しは配列。ノードの型から機械的に決まります。

意味推論

SNS Linksの構造はもう少し推論が要ります:

{ "name": "GitHub Icon", "type": "icon_font", "iconFontName": "github" },
{ "name": "GitHub", "type": "text", "content": "github.com/example-taro" },
{ "name": "X Icon", "type": "icon_font", "iconFontName": "twitter" },
{ "name": "X", "type": "text", "content": "@example_taro" }

icon_font と text のペアが交互に並ぶパターンから、AIエージェントは「プラットフォームアイコン + リンクテキスト」の繰り返し構造と認識します。iconFontName: "github" からプラットフォーム名を、隣接するテキストの content から表示テキストを読み取り、sns: { platform: string; url: string }[] という型を導出します。デザイン上の github.com/example-taro からは完全なURL https://github.com/example-taro も推測してAPIデータに反映してくれます。

こうしたパターン認識はAIエージェントが得意とするところで、ノード名、アイコン名、繰り返しパターンといった構造的なヒントが十分にあるため推論の精度は高いです。

導出された型定義

type Profile = {
  name: string;
  title: string;
  bio: string;
  avatarUrl: string;
  sns: { platform: string; url: string }[];
  skills: string[];
};

「このテキストは名前だから name: string で…」「このタグが4つ並んでるから配列で…」という翻訳作業を、.pen の構造情報をもとにAIエージェントが自動でやってくれます。

3. Honoで実装する

型定義が決まれば、APIエンドポイントとコンポーネントの実装はほぼ自動的に決まります。

APIエンドポイント

import { Hono } from "hono";
import type { Profile } from "./types/profile.js";

const app = new Hono();

const profile: Profile = {
  name: "Taro Yamada",
  title: "Full-Stack Developer",
  bio: "Web technologies enthusiast...",
  avatarUrl: "https://api.dicebear.com/9.x/shapes/svg?seed=taro",
  sns: [
    { platform: "GitHub", url: "https://github.com/example-taro" },
    { platform: "X", url: "https://x.com/example_taro" },
  ],
  skills: ["TypeScript", "React", "Hono", "Node.js"],
};

app.get("/api/profile", (c) => c.json(profile));

Profile 型がそのままAPIのレスポンス型になります。サンプルデータもデザインのダミーテキストからそのまま持ってこれます。

コンポーネント

Honoの組み込みJSX(hono/jsx)でSSRします。見てほしいのは、CSSの各値がデザインの構造からそのまま来ているところです:

const ProfileCard: FC<{ profile: Profile }> = ({ profile }) => (
  <div style={{
    width: 400,                       // ← .pen: width: 400
    borderRadius: 16,                 // ← .pen: cornerRadius: 16
    border: "1.5px solid #E4E4E7",    // ← .pen: stroke
    padding: 32,                      // ← .pen: padding: 32
    display: "flex",
    flexDirection: "column",          // ← .pen: layout: "vertical"
    alignItems: "center",             // ← .pen: alignItems: "center"
    gap: 20,                          // ← .pen: gap: 20
  }}>
    <img src={profile.avatarUrl} width={80} height={80}
         style={{ borderRadius: "50%" }} />
    <h1 style={{ fontSize: 28, fontWeight: 600, color: "#18181B" }}>
      {profile.name}
    </h1>
    <p style={{ fontSize: 15, color: "#71717A" }}>{profile.title}</p>
    <p style={{ fontSize: 14, color: "#71717A", textAlign: "center" }}>
      {profile.bio}
    </p>
    {/* ... SNS Links, Skill Tags ... */}
  </div>
);

layout: "vertical" → flexDirection: "column"、gap: 20 → gap: 20。デザインのノードツリーがそのままコンポーネントツリーになり、プロパティがそのままCSSの値になります。

SSRルーティング

APIとコンポーネントをつなぐSSRルートもシンプルです:

app.get("/", (c) => {
  return c.html(
    <html lang="ja">
      <head>
        <meta charset="UTF-8" />
        <title>{profile.name} - Profile</title>
      </head>
      <body style={{
        margin: 0, minHeight: "100vh",
        display: "flex", alignItems: "center", justifyContent: "center",
        backgroundColor: "#F8F9FA", fontFamily: "Inter, sans-serif",
      }}>
        <ProfileCard profile={profile} />
      </body>
    </html>
  );
});

HonoのJSXでHTMLをサーバーサイドレンダリングして、ブラウザにはビルド済みHTMLが届きます。/api/profile はデータ用、/ は表示用で、フロントエンドのビルドステップは要りません。

「デザインを見てCSSを書く」という手作業がないので、デザインと実装がずれようがありません。

4. デザイン変更に追従する

.pen をSOTにする利点が一番出るのは変更への追従です。

たとえば「プロフィールカードに所在地を追加したい」という要望が来たとします。

Step 1: デザインを更新する。 PencilのGUIエディタ、またはAIエージェントに指示して Bio の下にアイコンとテキストを追加します:

locIcon=I("profileCard", {type: "icon_font", iconFontName: "map-pin",
  width: 16, height: 16, fill: "#71717A"})
locText=I("profileCard", {type: "text", name: "Location",
  content: "Tokyo, Japan", fontSize: 14, fill: "#71717A"})

Step 2: AIに下流の更新を指示する。 デザインを更新したら、Claude Codeに「.pen の変更に合わせて型・API・コンポーネントを更新して」と伝えるだけです。エージェントが batch_get で .pen を読み直し、新しい Location テキストノードを見つけて型定義を更新してくれます:

  type Profile = {
    name: string;
    title: string;
    bio: string;
+   location: string;
    avatarUrl: string;
    sns: { platform: string; url: string }[];
    skills: string[];
  };

Step 3: API・コンポーネントも自動更新。 型の変更に伴い、APIレスポンスのデータとコンポーネントのJSXもエージェントが更新します。TypeScriptの型チェックが変更漏れを防いでくれます。

このフロー全体で人間がやったのは、「所在地を追加する」という意思決定と、エージェントへの一言の指示だけです。型定義の変更、APIの更新、コンポーネントの修正はすべてAIエージェントが .pen の構造から導出してくれます。

デザイン更新 → エージェントに指示 → 型・API・コンポーネントが自動更新、という3ステップで完結します。デザインとコードの手動同期が不要になるのがポイントです。

まとめ

この記事では、Pencilの .pen ファイルをSOTにして型定義・API・コンポーネントを導出する流れを紹介しました。

.pen がrepoにあることで、AIエージェントがデザインの構造をそのまま読めて、型・CSS・コンポーネントを生成できます。デザインが変わったときも差分から下流を自動更新できます。

今回はプロフィールカードというシンプルなUIでの実験でしたが、コンポーネントの見た目と型を .pen から生成するパターン自体は、より複雑なUIでもコンポーネント単位で適用できます。興味があればぜひ試してみてください。

Discussion