3ペインレイアウト(カテゴリ+一覧+詳細)

応用例 中級

この画面パターンについて

左ペインのカテゴリ選択・中央ペインの一覧・右ペインの詳細プレビューが連動する3ペインレイアウトのパターンです。メールクライアント・ナレッジベース・CRMなど、「分類→一覧→詳細」の階層を1画面で扱うUIの定番構成です。3つのペインが単一の state オブジェクトを共有し、どのペインを操作しても render() 1回で全体が更新される仕組みが実装の核心です。

こんな場面で使えます

  • ナレッジベース・FAQ管理 — カテゴリで絞り込みながら記事を素早く探して内容を確認する
  • メール・問い合わせ管理 — フォルダ(受信/送信/下書き)→メール一覧→本文の3段構造
  • CRM・顧客管理 — セグメントで絞り込んだ顧客リストから1件を選んで詳細を確認する

この画面で使っているUIコンポーネント

#パーツこの画面での役割
1カテゴリリスト(左ペイン)クリックで中央の一覧を絞り込み。選択中カテゴリをハイライト
2件数バッジ(左ペイン)各カテゴリの記事数をリスト右端にバッジで表示
3検索ボックス(中央ペイン上部)タイトルをリアルタイム部分一致で絞り込み
4アイテムリスト(中央ペイン)絞り込み後の記事一覧。クリックで右ペインに詳細を表示。選択行をハイライト
5件数表示(中央ペイン)「全N件 / M件を表示」の件数テキスト
6ステータスバッジ公開(緑)・下書き(グレー)を色分けバッジで表示
7詳細プレビューパネル(右ペイン)選択した記事のタイトル・本文・カテゴリ・ステータス・更新日を表示
8空状態表示右ペイン未選択時・検索結果ゼロ時の案内メッセージ

実装のポイント・注意点

3ペインの連動は state = { categoryId, keyword, articleId } の3値で管理します。カテゴリ選択・検索・記事選択はそれぞれ対応する state の値を書き換えて render() を呼ぶだけで、render() 内で絞り込み→一覧描画→詳細描画をまとめて行います。カテゴリを切り替えたときは keywordarticleId を同時にリセットし、右ペインが前の選択を引き継がないようにします。検索ボックスとカテゴリは AND 条件で絞り込みます(選択中カテゴリ内でキーワード検索)。左ペインの件数バッジはカテゴリ選択・検索に関わらず常に全件数を表示し、右ペインは articleIdnull のとき空状態表示に切り替えます。

8個のUIコンポーネントをHTML・CSS・バニラJavaScriptのみで組み合わせており、 フレームワーク不要で画面ごとコピペして使えます。

動作サンプル

3ペインレイアウトのデモ画面 動作サンプルを別ウィンドウで確認 ↗

試してみる:

  • 左ペインのカテゴリをクリックして一覧が絞り込まれることを確認。検索ワードもリセットされる
  • カテゴリを選んだ状態で検索ボックスにキーワードを入れて、AND条件で絞り込まれることを確認
  • 一覧の行をクリックして右ペインに詳細が表示されること、別の行を選ぶと切り替わることを確認

そのほかの操作も自由に試してみてください。

サンプルソース

4つのファイルを同じフォルダに保存し、ローカルサーバー(VS Code Live Server等)経由で index.html を開くと動作確認できます。
ファイル名:index.html / style.css / script.js + data/ フォルダに data.json
fetch を使用しているため file:// での直接表示は動作しません(CORSエラー)。 保存時の文字コードは UTF-8 を指定してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ナレッジベース</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="three-pane-screen">

  <!-- ===== ヘッダー ===== -->
  <div class="screen-header">
    <h1 class="screen-title">ナレッジベース</h1>
  </div>

  <!-- ===== 3ペインコンテナ ===== -->
  <div class="pane-container">

    <!-- 左ペイン:カテゴリリスト -->
    <aside class="pane pane-left">
      <ul class="category-list" id="categoryList" role="listbox" aria-label="カテゴリ">
        <!-- JSで生成 -->
      </ul>
    </aside>

    <!-- 中央ペイン:記事一覧 -->
    <main class="pane pane-center">
      <div class="search-bar">
        <input
          type="text"
          class="search-input"
          id="searchInput"
          placeholder="キーワードで検索..."
          aria-label="記事を検索"
          autocomplete="off"
        >
      </div>
      <p class="result-count" id="resultCount" aria-live="polite"></p>
      <ul class="article-list" id="articleList" role="listbox" aria-label="記事一覧">
        <!-- JSで生成 -->
      </ul>
      <div class="empty-state" id="centerEmpty" hidden>
        <p class="empty-message">該当する記事がありません</p>
      </div>
    </main>

    <!-- 右ペイン:詳細プレビュー -->
    <aside class="pane pane-right">
      <div class="empty-state" id="rightEmpty">
        <p class="empty-message">記事を選択してください</p>
      </div>
      <div class="article-detail" id="articleDetail" hidden>
        <h2 class="detail-title" id="detailTitle"></h2>
        <div class="detail-meta" id="detailMeta"></div>
        <hr class="detail-divider">
        <p class="detail-body" id="detailBody"></p>
      </div>
    </aside>

  </div><!-- /.pane-container -->
</div><!-- /.three-pane-screen -->

<script src="./script.js"></script>
</body>
</html>
/* ===== リセット・ベース ===== */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --accent:        #2B7FE8;
  --accent-light:  #EBF3FD;
  --accent-border: #BFDBFE;
  --border:        #E5E9F0;
  --bg:            #F4F6F9;
  --bg-white:      #FFFFFF;
  --text-primary:  #1A2332;
  --text-secondary:#5A6A7A;
  --text-muted:    #9AA5B4;
  --badge-pub-bg:  #DCFCE7;
  --badge-pub-text:#166534;
  --badge-drft-bg: #F1F5F9;
  --badge-drft-text:#64748B;
  --left-width:    210px;
  --right-width:   340px;
  --header-height: 52px;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Hiragino Sans', 'Yu Gothic UI', sans-serif;
  font-size: 14px;
  color: var(--text-primary);
  background: var(--bg);
  height: 100vh;
  overflow: hidden;
}

/* ===== ヘッダー ===== */
.screen-header {
  height: var(--header-height);
  display: flex;
  align-items: center;
  padding: 0 20px;
  background: var(--bg-white);
  border-bottom: 1px solid var(--border);
}

.screen-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text-primary);
}

/* ===== 3ペインコンテナ ===== */
.pane-container {
  display: flex;
  height: calc(100vh - var(--header-height));
}

/* ===== 共通ペインスタイル ===== */
.pane {
  overflow-y: auto;
  background: var(--bg-white);
}

/* ===== 左ペイン:カテゴリリスト ===== */
.pane-left {
  width: var(--left-width);
  flex-shrink: 0;
  border-right: 1px solid var(--border);
  padding: 12px 0;
}

.category-list { list-style: none; }

.category-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 16px;
  cursor: pointer;
  font-size: 13px;
  transition: background 0.1s;
  user-select: none;
}

.category-item:hover { background: var(--bg); }

.category-item.is-active {
  background: var(--accent-light);
  color: var(--accent);
  font-weight: 600;
}

.count-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 18px;
  padding: 0 5px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--bg);
  border-radius: 9px;
}

.category-item.is-active .count-badge {
  color: var(--accent);
  background: var(--accent-border);
}

/* ===== 中央ペイン:記事一覧 ===== */
.pane-center {
  flex: 1;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.search-bar {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.search-input {
  width: 100%;
  padding: 7px 12px;
  font-size: 13px;
  color: var(--text-primary);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
  transition: border-color 0.15s, background 0.15s;
}

.search-input:focus {
  border-color: var(--accent);
  background: var(--bg-white);
}

.result-count {
  padding: 8px 16px;
  font-size: 12px;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.article-list {
  list-style: none;
  flex: 1;
  overflow-y: auto;
}

.article-item {
  padding: 12px 16px;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent;
  transition: background 0.1s, border-left-color 0.1s;
  user-select: none;
}

.article-item:hover { background: var(--bg); }

.article-item.is-active {
  background: var(--accent-light);
  border-left-color: var(--accent);
}

.article-item-title {
  font-size: 13px;
  font-weight: 500;
  margin-bottom: 4px;
  word-break: break-all;
}

.article-item.is-active .article-item-title { color: var(--accent); }

.article-item-meta {
  display: flex;
  align-items: center;
  gap: 8px;
}

.article-item-date {
  font-size: 11px;
  color: var(--text-muted);
}

.pane-center .empty-state {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pane-center .empty-state[hidden] { display: none; }

/* ===== 右ペイン:詳細プレビュー ===== */
.pane-right {
  width: var(--right-width);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
}

#rightEmpty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

#rightEmpty[hidden] { display: none; }

.article-detail {
  padding: 20px;
  flex: 1;
  overflow-y: auto;
}

.article-detail[hidden] { display: none; }

.detail-title {
  font-size: 16px;
  font-weight: 700;
  line-height: 1.5;
  margin-bottom: 10px;
  word-break: break-all;
}

.detail-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.detail-category { font-size: 12px; color: var(--text-secondary); }
.detail-date     { font-size: 12px; color: var(--text-muted); }

.detail-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin-bottom: 14px;
}

.detail-body {
  font-size: 13px;
  color: var(--text-secondary);
  line-height: 1.75;
}

/* ===== ステータスバッジ ===== */
.badge-status {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  font-size: 11px;
  font-weight: 600;
  border-radius: 10px;
}

.badge-status.published {
  background: var(--badge-pub-bg);
  color: var(--badge-pub-text);
}

.badge-status.draft {
  background: var(--badge-drft-bg);
  color: var(--badge-drft-text);
}

/* ===== 空状態 共通 ===== */
.empty-state { text-align: center; padding: 40px 20px; }
.empty-message { font-size: 13px; color: var(--text-muted); }

/* ===== レスポンシブ ===== */
@media (max-width: 768px) {
  .pane-right { display: none; }
  .pane-center { border-right: none; }
}

@media (max-width: 480px) {
  .pane-left { width: 160px; }
}
/* =====================================================
   3ペインレイアウト

   仕組み:state(categoryId / keyword / articleId)の3値を
   唯一の情報源にする。カテゴリ選択・検索・記事選択はそれぞれ
   state を書き換えて render() を呼ぶだけ。
   render() が左・中央・右の3ペインをまとめて更新する。

   カテゴリ切替時は keyword と articleId をリセットする。
   キーワード検索時は articleId をリセットする。
   行生成は createElement + textContent(XSS対策)。
   ===================================================== */

// ===== 設定値 =====
var ALL_CATEGORY_ID = '__all__'; // 「すべて」カテゴリの仮想ID

// ===== データ =====
var categories = [];
var articles = [];

// ===== 状態 =====
var state = {
  categoryId: ALL_CATEGORY_ID,
  keyword: '',
  articleId: null
};

// ===== DOM参照 =====
var categoryList  = document.getElementById('categoryList');
var searchInput   = document.getElementById('searchInput');
var resultCount   = document.getElementById('resultCount');
var articleList   = document.getElementById('articleList');
var centerEmpty   = document.getElementById('centerEmpty');
var rightEmpty    = document.getElementById('rightEmpty');
var articleDetail = document.getElementById('articleDetail');
var detailTitle   = document.getElementById('detailTitle');
var detailMeta    = document.getElementById('detailMeta');
var detailBody    = document.getElementById('detailBody');

// ===== 初期化 =====
fetch('./data/data.json')
  .then(function(res) { return res.json(); })
  .then(function(data) {
    categories = data.categories;
    articles   = data.articles;
    render();
  })
  .catch(function() {
    centerEmpty.hidden = false;
    centerEmpty.querySelector('.empty-message').textContent =
      'データを読み込めませんでした。サーバー経由でアクセスしてください。';
  });

// ===== フィルタリング =====

// 現在の state に基づいて絞り込んだ記事配列を返す
function getFilteredArticles() {
  return articles.filter(function(a) {
    var inCategory = state.categoryId === ALL_CATEGORY_ID
      || a.categoryId === state.categoryId;
    var inKeyword  = state.keyword === ''
      || a.title.indexOf(state.keyword) !== -1;
    return inCategory && inKeyword;
  });
}

// ===== 描画 =====

function render() {
  renderLeftPane();
  renderCenterPane();
  renderRightPane();
}

// 左ペイン:カテゴリリスト
function renderLeftPane() {
  categoryList.innerHTML = '';

  // 「すべて」行(全件数を表示)
  categoryList.appendChild(
    createCategoryItem(ALL_CATEGORY_ID, 'すべて', articles.length)
  );

  categories.forEach(function(cat) {
    var count = articles.filter(function(a) {
      return a.categoryId === cat.id;
    }).length;
    categoryList.appendChild(createCategoryItem(cat.id, cat.name, count));
  });
}

// カテゴリ行のDOM生成
function createCategoryItem(id, name, count) {
  var li = document.createElement('li');
  li.className = 'category-item' + (state.categoryId === id ? ' is-active' : '');
  li.setAttribute('role', 'option');
  li.setAttribute('data-id', id);
  li.setAttribute('aria-selected', state.categoryId === id ? 'true' : 'false');

  var nameSpan = document.createElement('span');
  nameSpan.className = 'category-name';
  nameSpan.textContent = name;

  var badge = document.createElement('span');
  badge.className = 'count-badge';
  badge.textContent = count;

  li.appendChild(nameSpan);
  li.appendChild(badge);
  return li;
}

// 中央ペイン:記事一覧
function renderCenterPane() {
  var filtered = getFilteredArticles();

  var total = state.categoryId === ALL_CATEGORY_ID
    ? articles.length
    : articles.filter(function(a) { return a.categoryId === state.categoryId; }).length;

  if (state.keyword !== '') {
    resultCount.textContent = '全' + total + '件 / ' + filtered.length + '件を表示';
  } else {
    resultCount.textContent = '全' + filtered.length + '件';
  }

  articleList.innerHTML = '';

  if (filtered.length === 0) {
    centerEmpty.hidden = false;
    articleList.hidden = true;
    return;
  }

  centerEmpty.hidden = true;
  articleList.hidden = false;

  filtered.forEach(function(article) {
    articleList.appendChild(createArticleItem(article));
  });
}

// 記事行のDOM生成
function createArticleItem(article) {
  var li = document.createElement('li');
  li.className = 'article-item' + (state.articleId === article.id ? ' is-active' : '');
  li.setAttribute('role', 'option');
  li.setAttribute('data-id', article.id);
  li.setAttribute('aria-selected', state.articleId === article.id ? 'true' : 'false');

  var titleEl = document.createElement('p');
  titleEl.className = 'article-item-title';
  titleEl.textContent = article.title;

  var metaEl = document.createElement('div');
  metaEl.className = 'article-item-meta';

  var badge = createStatusBadge(article.status);
  var dateEl = document.createElement('span');
  dateEl.className = 'article-item-date';
  dateEl.textContent = article.updatedAt;

  metaEl.appendChild(badge);
  metaEl.appendChild(dateEl);
  li.appendChild(titleEl);
  li.appendChild(metaEl);
  return li;
}

// 右ペイン:詳細プレビュー
function renderRightPane() {
  if (state.articleId === null) {
    rightEmpty.hidden    = false;
    articleDetail.hidden = true;
    return;
  }

  var article = articles.find(function(a) { return a.id === state.articleId; });
  if (!article) {
    rightEmpty.hidden    = false;
    articleDetail.hidden = true;
    return;
  }

  rightEmpty.hidden    = true;
  articleDetail.hidden = false;

  detailTitle.textContent = article.title;
  detailBody.textContent  = article.body;

  detailMeta.innerHTML = '';

  var badge = createStatusBadge(article.status);

  var catName = '';
  var cat = categories.find(function(c) { return c.id === article.categoryId; });
  if (cat) { catName = cat.name; }

  var catEl = document.createElement('span');
  catEl.className = 'detail-category';
  catEl.textContent = 'カテゴリ:' + catName;

  var dateEl = document.createElement('span');
  dateEl.className = 'detail-date';
  dateEl.textContent = '更新日:' + article.updatedAt;

  detailMeta.appendChild(badge);
  detailMeta.appendChild(catEl);
  detailMeta.appendChild(dateEl);
}

// ステータスバッジのDOM生成
function createStatusBadge(status) {
  var badge = document.createElement('span');
  badge.className = 'badge-status ' + status;
  badge.textContent = status === 'published' ? '公開' : '下書き';
  return badge;
}

// ===== イベント =====

// カテゴリリスト:イベント委譲
categoryList.addEventListener('click', function(e) {
  var item = e.target.closest('[data-id]');
  if (!item) return;
  state.categoryId = item.getAttribute('data-id');
  // カテゴリ切替で検索ワードと選択記事をリセット
  state.keyword    = '';
  state.articleId  = null;
  searchInput.value = '';
  render();
});

// 記事リスト:イベント委譲
articleList.addEventListener('click', function(e) {
  var item = e.target.closest('[data-id]');
  if (!item) return;
  state.articleId = Number(item.getAttribute('data-id'));
  render();
});

// 検索ボックス:入力のたびに絞り込み
searchInput.addEventListener('input', function() {
  state.keyword   = searchInput.value;
  // 検索ワード変更で選択記事をリセット
  state.articleId = null;
  render();
});
{
  "categories": [
    { "id": "intro",    "name": "はじめに" },
    { "id": "basics",   "name": "基本操作" },
    { "id": "settings", "name": "設定" },
    { "id": "trouble",  "name": "トラブル" },
    { "id": "pricing",  "name": "料金" },
    { "id": "other",    "name": "その他" }
  ],
  "articles": [
    { "id": 1,  "categoryId": "intro",    "title": "このサービスでできること",       "body": "本サービスはデータの管理・共有・分析を一元化するためのツールです。登録・閲覧・エクスポートに対応しており、チームでの共同作業にも活用できます。まずはアカウントを作成してご利用ください。",                                                                                                                                  "status": "published", "updatedAt": "2026-06-10" },
    { "id": 2,  "categoryId": "intro",    "title": "アカウントを作成するには",         "body": "トップページの「新規登録」ボタンからメールアドレスとパスワードを入力して登録できます。登録後に確認メールが届きますので、メール内のリンクをクリックして認証を完了してください。",                                                                                                                      "status": "published", "updatedAt": "2026-06-09" },
    { "id": 3,  "categoryId": "intro",    "title": "はじめに確認すべきこと",           "body": "初めてご利用の方は、まず利用規約と個人情報の取り扱いをご確認ください。その後、プロフィール設定と通知設定を済ませておくとスムーズにご利用いただけます。",                                                                                                                                                    "status": "published", "updatedAt": "2026-06-08" },
    { "id": 4,  "categoryId": "intro",    "title": "利用規約と個人情報の取り扱い",     "body": "本サービスのご利用にあたっては利用規約への同意が必要です。個人情報は第三者への提供を行わず、セキュリティに配慮した方法で管理いたします。詳細は別途ご案内する規約ページをご確認ください。",                                                                                                          "status": "draft",     "updatedAt": "2026-06-05" },
    { "id": 5,  "categoryId": "basics",   "title": "データの登録方法",                 "body": "一覧画面右上の「+ 新規追加」ボタンをクリックして登録フォームを開きます。必須項目を入力して「保存」を押すと一覧に追加されます。CSVからの一括インポートも利用できます。",                                                                                                                                  "status": "published", "updatedAt": "2026-06-12" },
    { "id": 6,  "categoryId": "basics",   "title": "データの検索方法",                 "body": "一覧画面上部の検索ボックスにキーワードを入力すると、名前・メモ欄を対象にリアルタイムで絞り込みができます。ステータスやカテゴリでの絞り込みはフィルターボタンを使用してください。",                                                                                                                  "status": "published", "updatedAt": "2026-06-11" },
    { "id": 7,  "categoryId": "basics",   "title": "データの編集と削除",               "body": "一覧の行をクリックして詳細パネルを開き、「編集」ボタンから変更できます。削除は確認ダイアログが表示されてから実行されます。削除したデータは復元できませんのでご注意ください。",                                                                                                                          "status": "published", "updatedAt": "2026-06-10" },
    { "id": 8,  "categoryId": "basics",   "title": "ファイルのエクスポート",           "body": "一覧画面上部の「エクスポート」ボタンからCSV形式でダウンロードできます。フィルター・検索後の表示中データのみをエクスポートしたい場合は、絞り込みを行った状態で操作してください。",                                                                                                                  "status": "published", "updatedAt": "2026-06-09" },
    { "id": 9,  "categoryId": "basics",   "title": "複数件の一括操作",                 "body": "一覧のチェックボックスで複数行を選択し、「一括操作」メニューからステータス変更や削除が行えます。選択できる件数の上限は100件です。大量データの操作は時間がかかる場合があります。",                                                                                                                        "status": "draft",     "updatedAt": "2026-06-07" },
    { "id": 10, "categoryId": "settings", "title": "プロフィール情報の変更",           "body": "画面右上のアイコンから「プロフィール設定」を開き、名前・連絡先・アバター画像を変更できます。変更後は「保存」ボタンを押すまで反映されません。アバター画像はJPEG・PNG形式に対応しています。",                                                                                                          "status": "published", "updatedAt": "2026-06-13" },
    { "id": 11, "categoryId": "settings", "title": "通知の設定",                       "body": "設定メニューの「通知」タブから、メール通知とプッシュ通知のオン/オフを個別に切り替えられます。重要なアラートのみ受け取りたい場合は「重要度:高」のみをオンにしてください。",                                                                                                                              "status": "published", "updatedAt": "2026-06-12" },
    { "id": 12, "categoryId": "settings", "title": "表示言語の切り替え",               "body": "設定メニューの「言語」タブから日本語・英語を切り替えられます。変更はページをリロードした後に反映されます。ブラウザの言語設定より本サービスの設定が優先されます。",                                                                                                                                          "status": "published", "updatedAt": "2026-06-10" },
    { "id": 13, "categoryId": "settings", "title": "パスワードの変更",                 "body": "設定メニューの「セキュリティ」タブから現在のパスワードを入力した上で新しいパスワードに変更できます。パスワードは8文字以上・英数字混在が必要です。変更後は再ログインが必要です。",                                                                                                                      "status": "published", "updatedAt": "2026-06-09" },
    { "id": 14, "categoryId": "settings", "title": "二段階認証の設定",                 "body": "設定メニューの「セキュリティ」タブから二段階認証を有効にできます。認証アプリ(Google Authenticator等)でQRコードをスキャンして設定してください。設定後はログイン時にコードの入力が必要になります。",                                                                                                  "status": "draft",     "updatedAt": "2026-06-06" },
    { "id": 15, "categoryId": "settings", "title": "メールアドレスの変更",             "body": "設定メニューの「アカウント」タブから新しいメールアドレスを入力して変更申請できます。変更後は新しいアドレスに確認メールが届きますので、リンクをクリックして変更を確定してください。",                                                                                                                    "status": "published", "updatedAt": "2026-06-08" },
    { "id": 16, "categoryId": "trouble",  "title": "ログインできない場合",             "body": "メールアドレスとパスワードを再確認ください。パスワードを忘れた場合はログイン画面の「パスワードを忘れた方はこちら」から再設定できます。それでも解決しない場合はサポートへお問い合わせください。",                                                                                                          "status": "published", "updatedAt": "2026-06-14" },
    { "id": 17, "categoryId": "trouble",  "title": "データが表示されない",             "body": "フィルターや検索ボックスに条件が残っている可能性があります。ページをリロードするか、検索ボックスをクリアして全件表示に戻してください。それでも表示されない場合はサポートにお問い合わせください。",                                                                                                          "status": "published", "updatedAt": "2026-06-13" },
    { "id": 18, "categoryId": "trouble",  "title": "エラーメッセージが出た",           "body": "エラーコードと操作内容をメモしてサポートにお問い合わせください。一時的な通信エラーの場合はブラウザをリロードすることで解消する場合があります。キャッシュのクリアも効果的です。",                                                                                                                          "status": "published", "updatedAt": "2026-06-11" },
    { "id": 19, "categoryId": "trouble",  "title": "メールが届かない",                 "body": "まず迷惑メールフォルダをご確認ください。登録メールアドレスの入力ミスの可能性もありますので、設定画面でメールアドレスを確認してください。それでも届かない場合はサポートへご連絡ください。",                                                                                                                "status": "draft",     "updatedAt": "2026-06-08" },
    { "id": 20, "categoryId": "pricing",  "title": "料金プランの種類",                 "body": "フリー・スタンダード・プロの3プランをご用意しています。フリープランはデータ件数に上限あり、スタンダード以上で無制限。プロプランはAPI連携と優先サポートが追加されます。",                                                                                                                                "status": "published", "updatedAt": "2026-06-10" },
    { "id": 21, "categoryId": "pricing",  "title": "プランの変更方法",                 "body": "設定メニューの「プラン」タブからアップグレード・ダウングレードができます。アップグレードは即時反映、ダウングレードは翌月更新日に反映されます。差額の日割り計算は自動で行われます。",                                                                                                                    "status": "published", "updatedAt": "2026-06-09" },
    { "id": 22, "categoryId": "pricing",  "title": "請求書の確認方法",                 "body": "設定メニューの「請求」タブから過去の請求履歴を確認・PDFでダウンロードできます。領収書が必要な場合はサポートまでお問い合わせください。請求書は毎月1日に発行されます。",                                                                                                                                  "status": "published", "updatedAt": "2026-06-08" },
    { "id": 23, "categoryId": "pricing",  "title": "解約の手続き",                     "body": "設定メニューの「プラン」タブ下部の「解約する」から手続きができます。解約後も当月末までご利用いただけます。データは解約後30日間保持されますので、必要なデータはエクスポートしてください。",                                                                                                            "status": "draft",     "updatedAt": "2026-06-05" },
    { "id": 24, "categoryId": "other",    "title": "お問い合わせ方法",                 "body": "画面右下のチャットアイコン、またはメールでお問い合わせいただけます。営業時間は平日10:00〜18:00です。お急ぎの場合はチャットをご利用ください。翌営業日中に返信いたします。",                                                                                                                              "status": "published", "updatedAt": "2026-06-12" },
    { "id": 25, "categoryId": "other",    "title": "アップデート情報",                 "body": "新機能の追加や不具合修正などのアップデート情報はこのページに随時掲載します。重要なアップデートはメールでもお知らせします。最新バージョンへの更新は自動で行われます。",                                                                                                                                    "status": "published", "updatedAt": "2026-06-10" },
    { "id": 26, "categoryId": "other",    "title": "よくある質問まとめ",               "body": "よくいただくご質問をまとめたページです。パスワードのリセット・データのエクスポート・プランの変更など、各カテゴリーの記事もあわせてご参照ください。解決しない場合はサポートへご連絡ください。",                                                                                                          "status": "published", "updatedAt": "2026-06-08" }
  ]
}

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定やカテゴリ変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# 3ペインレイアウト 作成依頼

## 概要
左にカテゴリリスト・中央に記事一覧・右に詳細プレビューを並べた
3ペインレイアウト(ナレッジベース画面)を作成してください。
JSONをfetchで取得し、3つのペインが連動して動作します。

## 要件

### 全体レイアウト
- 左ペイン(200px固定):カテゴリリスト
- 中央ペイン(残り幅):検索ボックス+記事一覧
- 右ペイン(340px固定):詳細プレビュー
- 各ペインは独立してスクロールできる

### 左ペイン:カテゴリリスト
- 「すべて(全件数)」を先頭に固定表示し、デフォルト選択にする
- 各カテゴリ行の右端にそのカテゴリの記事件数をバッジで表示する
- 選択中のカテゴリ行をアクセントカラーでハイライトする

### 中央ペイン:記事一覧
- 上部に検索ボックス(キーワードでタイトルをリアルタイム部分一致検索)
- 「全N件 / M件を表示」の件数テキストを表示する
- 記事行にタイトル・ステータスバッジ(公開/下書き)・更新日を表示する
- 選択中の行を左ボーダーライン+薄いアクセントカラー背景でハイライトする
- 絞り込み結果が0件のとき「該当する記事がありません」を表示する

### 右ペイン:詳細プレビュー
- 記事未選択時は「記事を選択してください」の空状態を表示する
- 記事選択時はタイトル・ステータスバッジ・カテゴリ名・更新日・本文を表示する

### 3ペインの連動ルール
- カテゴリ切替時:検索ワードと選択記事をリセットし右ペインを空状態に戻す
- キーワード入力時:選択記事をリセットし右ペインを空状態に戻す
- カテゴリ絞り込みとキーワード検索はAND条件で動作する

## データ仕様
- JSONファイル(data/data.json)をfetchで取得する
- 構造:{ "categories": [...], "articles": [...] }
- カテゴリ:{ "id": "intro", "name": "はじめに" } の形式
- 記事:{ "id": 1, "categoryId": "intro", "title": "...", "body": "...",
  "status": "published" | "draft", "updatedAt": "YYYY-MM-DD" } の形式
- カテゴリ6種・記事25件程度のサンプルデータをJSONとして出力すること

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- state = { categoryId, keyword, articleId } の1オブジェクトで3ペインを管理する
- すべての操作は state を書き換えて render() を呼ぶだけ。render() が3ペインをまとめて更新する
- DOM生成は createElement + textContent を使い、innerHTML に変数を結合しない
- イベントはリストコンテナにイベント委譲(addEventListener)で登録する
- レスポンシブ:768px以下では右ペインを非表示にする

## 出力形式
HTML・CSS・JavaScript・JSONを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。