検索テーブル画面(検索+一覧+ページング)
この画面パターンについて
業務アプリで最も出現頻度が高い「検索条件 → 一覧表示 → ページ送り」の画面パターンです。 フィルター・ソート・ページングを1つの状態オブジェクトで管理して毎回再描画する、単方向フローの状態管理が実装の核心です。 マスタ管理・会員一覧など「データを探して選ぶ」あらゆる画面の土台として流用できます。
こんな場面で使えます
- 会員・顧客管理 — 名前で検索して対象者を探す
- 案件・注文一覧 — ステータスで絞り込んで処理対象を確認する
- 商品マスタ管理 — カテゴリ別に一覧して内容を確認する
この画面で使っているUIコンポーネント
| # | パーツ | この画面での役割 |
|---|---|---|
| 1 | 検索テキストボックス | キーワード部分一致で絞り込み |
| 2 | セレクトボックス ×2 | カテゴリ・ステータスの即時絞り込み |
| 3 | 検索・クリアボタン | 条件の反映と全リセット |
| 4 | ヒット件数表示 | 「全35件中 1〜10件」を常時表示 |
| 5 | ソート可能テーブル | 列ヘッダークリックで昇順/降順 |
| 6 | ステータスバッジ | 有効・停止・保留を色分け表示 |
| 7 | ページネーション | 10件/ページ・条件維持で移動 |
| 8 | 空状態表示 | 0件時にメッセージ+クリア導線 |
実装のポイント・注意点
検索・ソート・ページングはそれぞれ単純でも、組み合わせると状態の整合性が問題になります。
この画面では state = { keyword, category, status, sortKey, sortOrder, page } を唯一の情報源とし、
どの操作も state を書き換えて update() を呼ぶだけに統一します。
update() が毎回「絞り込み → ソート → ページ切り出し → 描画」をやり直すため、操作同士の干渉を個別に考える必要がなくなります。
定番バグは「絞り込み後に存在しないページを指したまま」になるものです。フィルター変更時に page を1へ戻すことで防ぎます。
ソートは slice() でコピーしてから行い、元データの順序を壊さないようにします。
8個のUIコンポーネントをHTML・CSS・バニラJavaScriptのみで組み合わせており、 フレームワーク不要で画面ごとコピペして使えます。
動作サンプル
動作サンプルを別ウィンドウで確認 ↗
試してみる:
- 列ヘッダー(ID・名前・更新日)をクリックしてソートの切り替えを確認
- カテゴリやステータスを変更して、ページが1ページ目に戻ることを確認
- ヒットしないキーワードで検索して、空状態表示と「条件をクリア」を確認
そのほかの操作も自由に試してみてください。
サンプルソース
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="search-screen">
<h1 class="screen-title">アイテム管理</h1>
<!-- ===== 検索条件エリア ===== -->
<form class="search-panel" id="searchForm">
<div class="search-field">
<label for="keyword">キーワード</label>
<input type="text" id="keyword" placeholder="名前で検索">
</div>
<div class="search-field">
<label for="categoryFilter">カテゴリ</label>
<select id="categoryFilter">
<option value="">すべて</option>
<option value="タイプ1">タイプ1</option>
<option value="タイプ2">タイプ2</option>
<option value="タイプ3">タイプ3</option>
</select>
</div>
<div class="search-field">
<label for="statusFilter">ステータス</label>
<select id="statusFilter">
<option value="">すべて</option>
<option value="active">有効</option>
<option value="suspended">停止</option>
<option value="pending">保留</option>
</select>
</div>
<div class="search-actions">
<button type="submit" class="btn-primary">検索</button>
<button type="button" class="btn-secondary" id="clearBtn">クリア</button>
</div>
</form>
<!-- ===== 件数表示 ===== -->
<p class="result-count" id="resultCount"></p>
<!-- ===== テーブル ===== -->
<div class="table-wrapper" id="tableWrapper">
<table class="data-table" id="dataTable">
<thead>
<tr>
<th data-sort-key="id" class="sortable">ID</th>
<th data-sort-key="name" class="sortable">名前</th>
<th>カテゴリ</th>
<th>ステータス</th>
<th data-sort-key="updatedAt" class="sortable">更新日</th>
</tr>
</thead>
<tbody id="tableBody"><!-- JSで生成 --></tbody>
</table>
</div>
<!-- ===== 空状態(0件時に表示) ===== -->
<div class="empty-state" id="emptyState" hidden>
<p class="empty-icon">🔍</p>
<p class="empty-message">条件に一致するデータがありません</p>
<button type="button" class="btn-secondary" id="emptyClearBtn">条件をクリア</button>
</div>
<!-- ===== ページネーション ===== -->
<nav class="pagination" id="pagination" aria-label="ページネーション"><!-- JSで生成 --></nav>
</div>
<script src="./script.js"></script>
</body>
</html>
/* ===== 検索テーブル画面 — style.css ===== */
*, *::before, *::after { box-sizing: border-box; }
:root {
--color-primary: #2B7FE8;
--color-text: #1E293B;
--color-muted: #64748B;
--color-border: #D0D7E0;
--color-bg: #F4F6F9;
--color-card: #FFFFFF;
--color-th-bg: #F1F5F9;
--color-row-hover: #F4F8FE;
}
body {
margin: 0;
font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
background: var(--color-bg);
color: var(--color-text);
}
/* ===== 画面レイアウト ===== */
.search-screen {
max-width: 1160px;
margin: 0 auto;
padding: 24px 20px 48px;
}
.screen-title {
font-size: 22px;
margin: 0 0 16px;
}
/* ===== 検索条件エリア ===== */
.search-panel {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
padding: 16px;
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 8px;
margin-bottom: 16px;
}
.search-field label {
display: block;
font-size: 12px;
color: var(--color-muted);
margin-bottom: 4px;
}
.search-field input,
.search-field select {
height: 38px;
padding: 0 10px;
font-size: 14px;
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 6px;
background: #fff;
}
.search-field input { width: 220px; }
.search-field select { min-width: 130px; }
.search-field input:focus,
.search-field select:focus {
outline: 2px solid var(--color-primary);
outline-offset: -1px;
}
.search-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
/* ===== ボタン ===== */
.btn-primary,
.btn-secondary {
height: 38px;
padding: 0 20px;
font-size: 14px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-primary {
color: #fff;
background: var(--color-primary);
border: 1.5px solid var(--color-primary);
}
.btn-primary:hover { background: #1F6BD0; }
.btn-secondary {
color: #5A6A7A;
background: #fff;
border: 1.5px solid var(--color-border);
}
.btn-secondary:hover {
background: var(--color-bg);
border-color: #9AA5B4;
}
/* ===== 件数表示 ===== */
.result-count {
font-size: 13px;
color: var(--color-muted);
margin: 0 0 8px;
}
/* ===== テーブル ===== */
/* スマホ幅では横スクロールで対応する */
.table-wrapper {
overflow-x: auto;
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 8px;
}
.data-table {
width: 100%;
min-width: 640px;
border-collapse: collapse;
font-size: 14px;
}
.data-table th,
.data-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid #E6EBF1;
}
.data-table thead th {
background: var(--color-th-bg);
font-size: 13px;
color: var(--color-muted);
white-space: nowrap;
}
.data-table tbody tr:hover { background: var(--color-row-hover); }
.data-table tbody tr:last-child td { border-bottom: none; }
/* ===== ソート可能ヘッダー ===== */
/* ニュートラル時は薄い ⇅、ソート中は ▲ / ▼ を表示する */
.sortable {
cursor: pointer;
user-select: none;
}
.sortable:hover { color: var(--color-primary); }
.sortable::after {
content: "⇅";
margin-left: 6px;
color: #B6C0CC;
}
.sortable.sort-asc::after {
content: "▲";
color: var(--color-primary);
}
.sortable.sort-desc::after {
content: "▼";
color: var(--color-primary);
}
/* ===== ステータスバッジ ===== */
.status-badge {
display: inline-block;
padding: 3px 12px;
font-size: 12px;
border-radius: 9999px;
white-space: nowrap;
}
.badge-active { color: #166534; background: #DCFCE7; }
.badge-suspended { color: #475569; background: #E2E8F0; }
.badge-pending { color: #854D0E; background: #FEF9C3; }
/* ===== ページネーション ===== */
.pagination {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 16px;
}
.pagination button {
min-width: 38px;
height: 38px;
padding: 0 12px;
font-size: 14px;
color: var(--color-text);
background: #fff;
border: 1px solid var(--color-border);
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.pagination button:hover:not(:disabled):not(.current) {
background: var(--color-row-hover);
}
.pagination button.current {
color: #fff;
background: var(--color-primary);
border-color: var(--color-primary);
cursor: default;
}
.pagination button:disabled {
color: #B6C0CC;
background: var(--color-bg);
cursor: not-allowed;
}
/* ===== 空状態 ===== */
.empty-state {
padding: 48px 20px;
text-align: center;
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 8px;
}
.empty-icon {
font-size: 32px;
margin: 0 0 8px;
}
.empty-message {
font-size: 15px;
color: var(--color-muted);
margin: 0 0 16px;
}
/* hidden 属性を確実に効かせる(display 指定との競合対策) */
.search-screen [hidden] { display: none !important; }
/* ===== レスポンシブ(768px以下) ===== */
@media (max-width: 768px) {
.search-panel {
flex-direction: column;
align-items: stretch;
}
.search-field input,
.search-field select {
width: 100%;
}
.search-actions {
margin-left: 0;
}
.search-actions button {
flex: 1;
}
}
/* =====================================================
検索テーブル画面のスクリプト
仕組み:すべての操作は state(検索条件・ソート・ページ)を
書き換えて update() を呼ぶだけ。update() が
絞り込み → ソート → ページ分割 → 描画 を毎回やり直すため、
操作同士の干渉を個別に考える必要がない。
行の生成は createElement + textContent(XSS対策)。
===================================================== */
// ===== 設定値 =====
var PAGE_SIZE = 10; // 1ページの表示件数
var JSON_PATH = './data/data.json'; // データの取得元
// ステータス値と表示名・バッジ色クラスの対応表
var STATUS_MAP = {
active: { label: '有効', className: 'badge-active' },
suspended: { label: '停止', className: 'badge-suspended' },
pending: { label: '保留', className: 'badge-pending' }
};
// ===== 状態管理 =====
var allItems = []; // fetchで取得した全データ(並び順は変更しない)
var state = {
keyword: '', // キーワード(名前の部分一致)
category: '', // カテゴリ('' はすべて)
status: '', // ステータス('' はすべて)
sortKey: 'id', // ソート対象の列
sortOrder: 'asc', // 'asc'(昇順)/ 'desc'(降順)
page: 1 // 現在のページ番号
};
// ===== DOM要素 =====
var searchForm = document.getElementById('searchForm');
var keywordInput = document.getElementById('keyword');
var categorySelect = document.getElementById('categoryFilter');
var statusSelect = document.getElementById('statusFilter');
var clearBtn = document.getElementById('clearBtn');
var resultCount = document.getElementById('resultCount');
var tableWrapper = document.getElementById('tableWrapper');
var tableBody = document.getElementById('tableBody');
var emptyState = document.getElementById('emptyState');
var emptyClearBtn = document.getElementById('emptyClearBtn');
var pagination = document.getElementById('pagination');
var sortHeaders = document.querySelectorAll('th.sortable');
// ===== 初期化(データ読み込み) =====
fetch(JSON_PATH)
.then(function (res) {
if (!res.ok) { throw new Error('HTTP ' + res.status); }
return res.json();
})
.then(function (data) {
allItems = data.items;
update();
})
.catch(function (err) {
console.error('読み込みエラー:', err);
resultCount.textContent = 'データを読み込めませんでした。ローカルサーバーで開いているか確認してください。';
});
// ===== 検索・絞り込み =====
// 検索ボタン(またはEnter) → キーワードを反映して1ページ目から表示
searchForm.addEventListener('submit', function (e) {
e.preventDefault();
state.keyword = keywordInput.value.trim();
state.page = 1;
update();
});
// カテゴリ変更 → 即時に絞り込み再描画(検索ボタン不要)
categorySelect.addEventListener('change', function () {
state.category = categorySelect.value;
// フィルターを変えたら必ず1ページ目に戻す
// (絞り込み後に存在しないページを指したままになるのを防ぐ)
state.page = 1;
update();
});
// ステータス変更 → 即時に絞り込み再描画
statusSelect.addEventListener('change', function () {
state.status = statusSelect.value;
state.page = 1;
update();
});
// クリアボタン → 全条件をリセットして初期表示に戻す
clearBtn.addEventListener('click', clearAll);
// 空状態の「条件をクリア」 → 同じく全リセットして一覧に復帰
emptyClearBtn.addEventListener('click', clearAll);
// 入力欄と state を初期状態に戻して再描画する
function clearAll() {
keywordInput.value = '';
categorySelect.value = '';
statusSelect.value = '';
state.keyword = '';
state.category = '';
state.status = '';
state.sortKey = 'id';
state.sortOrder = 'asc';
state.page = 1;
update();
}
// ===== ソート =====
// 列ヘッダークリック → 同じ列なら昇順/降順をトグル、別の列なら昇順に切り替え
sortHeaders.forEach(function (th) {
th.addEventListener('click', function () {
var key = th.dataset.sortKey;
if (state.sortKey === key) {
state.sortOrder = (state.sortOrder === 'asc') ? 'desc' : 'asc';
} else {
state.sortKey = key;
state.sortOrder = 'asc';
}
update();
});
});
// ===== 更新処理(絞り込み → ソート → ページ分割 → 描画) =====
function update() {
var filtered = applyFilter(allItems);
var sorted = applySort(filtered);
var totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE));
// 件数が減って現在ページが範囲外になった場合は最終ページに丸める(保険)
if (state.page > totalPages) { state.page = totalPages; }
var start = (state.page - 1) * PAGE_SIZE;
var pageItems = sorted.slice(start, start + PAGE_SIZE);
renderCount(sorted.length);
renderTable(pageItems);
renderPagination(totalPages, sorted.length);
renderSortIndicator();
toggleEmptyState(sorted.length === 0);
}
// 3条件(キーワード・カテゴリ・ステータス)のAND絞り込み
function applyFilter(items) {
return items.filter(function (item) {
if (state.keyword && item.name.indexOf(state.keyword) === -1) { return false; }
if (state.category && item.category !== state.category) { return false; }
if (state.status && item.status !== state.status) { return false; }
return true;
});
}
// 現在のソートキー・ソート順で並べ替えた新しい配列を返す
function applySort(items) {
// slice() でコピーしてからソートし、元データの並び順を壊さない
var sorted = items.slice();
sorted.sort(function (a, b) {
// updatedAt は YYYY-MM-DD 形式なので文字列比較で正しく並ぶ
// (日付形式を変える場合は Date オブジェクトへの変換が必要)
var result = 0;
if (a[state.sortKey] < b[state.sortKey]) { result = -1; }
if (a[state.sortKey] > b[state.sortKey]) { result = 1; }
return (state.sortOrder === 'asc') ? result : -result;
});
return sorted;
}
// ===== 描画 =====
// 「全35件中 1〜10件を表示」のヒット件数表示
function renderCount(total) {
if (total === 0) {
resultCount.textContent = '';
return;
}
var start = (state.page - 1) * PAGE_SIZE + 1;
var end = Math.min(state.page * PAGE_SIZE, total);
resultCount.textContent = '全' + total + '件中 ' + start + '〜' + end + '件を表示';
}
// 現在ページのデータでテーブル本体を描き直す
function renderTable(items) {
tableBody.textContent = '';
items.forEach(function (item) {
var tr = document.createElement('tr');
tr.appendChild(createCell(String(item.id)));
tr.appendChild(createCell(item.name));
tr.appendChild(createCell(item.category));
tr.appendChild(createStatusCell(item.status));
tr.appendChild(createCell(item.updatedAt));
tableBody.appendChild(tr);
});
}
// テキストセルを生成する
function createCell(text) {
var td = document.createElement('td');
td.textContent = text;
return td;
}
// ステータスバッジ入りのセルを生成する
function createStatusCell(status) {
var td = document.createElement('td');
var badge = document.createElement('span');
var info = STATUS_MAP[status];
badge.className = 'status-badge ' + info.className;
badge.textContent = info.label;
td.appendChild(badge);
return td;
}
// 「前へ」「ページ番号」「次へ」のボタン列を描き直す
function renderPagination(totalPages, totalItems) {
pagination.textContent = '';
if (totalItems === 0) { return; }
pagination.appendChild(createPageButton('前へ', state.page - 1, state.page === 1, false));
for (var i = 1; i <= totalPages; i++) {
pagination.appendChild(createPageButton(String(i), i, false, i === state.page));
}
pagination.appendChild(createPageButton('次へ', state.page + 1, state.page === totalPages, false));
}
// ページ移動ボタンを生成する(クリックでそのページへ。条件は維持される)
function createPageButton(label, page, disabled, isCurrent) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = label;
btn.disabled = disabled;
if (isCurrent) {
btn.className = 'current';
btn.setAttribute('aria-current', 'page');
}
btn.addEventListener('click', function () {
if (disabled || isCurrent) { return; }
state.page = page;
update();
});
return btn;
}
// ソート中の列ヘッダーに ▲ / ▼ を付け、他の列はニュートラルに戻す
function renderSortIndicator() {
sortHeaders.forEach(function (th) {
th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sortKey === state.sortKey) {
th.classList.add(state.sortOrder === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
}
// 0件時はテーブルとページネーションを隠して空状態を表示する
function toggleEmptyState(isEmpty) {
tableWrapper.hidden = isEmpty;
pagination.hidden = isEmpty;
emptyState.hidden = !isEmpty;
}
{
"items": [
{ "id": 1, "name": "サンプルアイテムA", "category": "タイプ1", "status": "active", "updatedAt": "2026-06-01" },
{ "id": 2, "name": "サンプルアイテムB", "category": "タイプ2", "status": "suspended", "updatedAt": "2026-05-28" },
{ "id": 3, "name": "サンプルアイテムC", "category": "タイプ3", "status": "pending", "updatedAt": "2026-05-30" },
{ "id": 4, "name": "サンプルアイテムD", "category": "タイプ1", "status": "suspended", "updatedAt": "2026-04-15" },
{ "id": 5, "name": "サンプルアイテムE", "category": "タイプ2", "status": "pending", "updatedAt": "2026-06-03" },
{ "id": 6, "name": "サンプルアイテムF", "category": "タイプ3", "status": "active", "updatedAt": "2026-03-22" },
{ "id": 7, "name": "サンプルアイテムG", "category": "タイプ1", "status": "pending", "updatedAt": "2026-05-11" },
{ "id": 8, "name": "サンプルアイテムH", "category": "タイプ2", "status": "active", "updatedAt": "2026-04-02" },
{ "id": 9, "name": "サンプルアイテムI", "category": "タイプ3", "status": "suspended", "updatedAt": "2026-06-07" },
{ "id": 10, "name": "サンプルアイテムJ", "category": "タイプ1", "status": "active", "updatedAt": "2026-03-18" },
{ "id": 11, "name": "サンプルアイテムK", "category": "タイプ2", "status": "suspended", "updatedAt": "2026-05-06" },
{ "id": 12, "name": "サンプルアイテムL", "category": "タイプ3", "status": "pending", "updatedAt": "2026-04-25" },
{ "id": 13, "name": "サンプルアイテムM", "category": "タイプ1", "status": "suspended", "updatedAt": "2026-06-09" },
{ "id": 14, "name": "サンプルアイテムN", "category": "タイプ2", "status": "pending", "updatedAt": "2026-03-30" },
{ "id": 15, "name": "サンプルアイテムO", "category": "タイプ3", "status": "active", "updatedAt": "2026-05-19" },
{ "id": 16, "name": "サンプルアイテムP", "category": "タイプ1", "status": "pending", "updatedAt": "2026-04-08" },
{ "id": 17, "name": "サンプルアイテムQ", "category": "タイプ2", "status": "active", "updatedAt": "2026-06-05" },
{ "id": 18, "name": "サンプルアイテムR", "category": "タイプ3", "status": "suspended", "updatedAt": "2026-03-12" },
{ "id": 19, "name": "サンプルアイテムS", "category": "タイプ1", "status": "active", "updatedAt": "2026-05-23" },
{ "id": 20, "name": "サンプルアイテムT", "category": "タイプ2", "status": "suspended", "updatedAt": "2026-04-19" },
{ "id": 21, "name": "サンプルアイテムU", "category": "タイプ3", "status": "pending", "updatedAt": "2026-06-02" },
{ "id": 22, "name": "サンプルアイテムV", "category": "タイプ1", "status": "suspended", "updatedAt": "2026-03-25" },
{ "id": 23, "name": "サンプルアイテムW", "category": "タイプ2", "status": "pending", "updatedAt": "2026-05-15" },
{ "id": 24, "name": "サンプルアイテムX", "category": "タイプ3", "status": "active", "updatedAt": "2026-04-28" },
{ "id": 25, "name": "サンプルアイテムY", "category": "タイプ1", "status": "pending", "updatedAt": "2026-06-08" },
{ "id": 26, "name": "サンプルアイテムZ", "category": "タイプ2", "status": "active", "updatedAt": "2026-03-09" },
{ "id": 27, "name": "サンプルアイテムAA", "category": "タイプ3", "status": "suspended", "updatedAt": "2026-05-02" },
{ "id": 28, "name": "サンプルアイテムAB", "category": "タイプ1", "status": "active", "updatedAt": "2026-04-12" },
{ "id": 29, "name": "サンプルアイテムAC", "category": "タイプ2", "status": "suspended", "updatedAt": "2026-06-04" },
{ "id": 30, "name": "サンプルアイテムAD", "category": "タイプ3", "status": "pending", "updatedAt": "2026-03-16" },
{ "id": 31, "name": "サンプルアイテムAE", "category": "タイプ1", "status": "suspended", "updatedAt": "2026-05-27" },
{ "id": 32, "name": "サンプルアイテムAF", "category": "タイプ2", "status": "pending", "updatedAt": "2026-04-05" },
{ "id": 33, "name": "サンプルアイテムAG", "category": "タイプ3", "status": "active", "updatedAt": "2026-06-10" },
{ "id": 34, "name": "サンプルアイテムAH", "category": "タイプ1", "status": "pending", "updatedAt": "2026-03-20" },
{ "id": 35, "name": "サンプルアイテムAI", "category": "タイプ2", "status": "active", "updatedAt": "2026-05-09" }
]
}
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様の画面パターンを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様の画面をゼロから生成・カスタマイズできます。列の追加や1ページの件数変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# 検索テーブル画面 作成依頼
## 概要
検索フォーム・ソート可能テーブル・ページネーションを組み合わせた一覧画面を作成してください。
## 要件
- キーワード(名前の部分一致)・カテゴリ・ステータスの3条件でAND絞り込みができる
- 列ヘッダー(ID・名前・更新日)クリックで昇順/降順ソートが切り替わり、▲▼で状態を表示する
- 1ページ10件のページネーション(前へ/次へ+ページ番号)を表示する
- 「全35件中 1〜10件を表示」のようなヒット件数を常時表示する
- ステータスは「有効(緑)・停止(グレー)・保留(黄)」のバッジで色分け表示する
- 検索結果が0件のときはテーブルの代わりに空状態メッセージと「条件をクリア」ボタンを表示する
- 絞り込み条件を変更したらページは1ページ目に戻す
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- データは fetch で ./data/data.json から読み込む(items 配列・35件想定)
- 動作確認用に data.json のサンプルデータも出力してください
(items 配列。フィールドは id / name / category / status / updatedAt。
ページ送りを確認できるよう25件以上)
- レスポンシブ対応:必要(768px以下で検索条件を縦積み、テーブルは横スクロール)
## 動作詳細
- 状態(検索条件・ソートキー・ソート順・現在ページ)は単一のオブジェクトで管理し、
操作のたびに「フィルター → ソート → ページ切り出し → 描画」の順で再描画する
- セレクトボックスの変更は即時反映、キーワードは検索ボタンまたはEnterで反映する
- テーブル行の生成は createElement と textContent を使い、innerHTML に変数を結合しない
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。