Seen as a design The generated document itself — its toggles and disclosures work here
Seen as a design The generated document itself — its toggles and disclosures work here
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
Build an HTML document with "User Research Recruitment Board". This layout runs, so there is no composition to rebuild — what it needs is content. ## What this layout does Track recruitment quotas and each pseudonymous candidate's eligibility, consent, schedule, and study completion by segment. ## What you have to supply - Provide segment criteria and targets, non-identifying candidate codes, screening and consent states, and session dates. ## The shape of the input - `title` — string — 募集進捗または調査対象の要点 - `subtitle` — string? — 募集期間・調査ラウンド・連絡窓口チーム - `study` — string — 調査名またはラウンド - `segments` — array (3–8) — 対象条件と必要人数 - `participants` — array (3–16) — 個人情報を含まない候補コード、セグメント、状態、同意、非識別選考メモ、調査日 schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing. ## When not to use it - Use a research evidence table to record findings, quotations, and supporting evidence. ## How to build it Put the input in a JSON file and run: ```bash node scripts/render.mjs user-research-recruitment-board <input.json> --theme ink -o out.html ``` The output is one self-contained file and fetches nothing at render time. ## The source - https://design-vault.pages.dev/designs/user-research-recruitment-board/source/template.tsx - https://design-vault.pages.dev/designs/user-research-recruitment-board/source/schema.ts - https://design-vault.pages.dev/designs/user-research-recruitment-board/source/sample-data.json - https://design-vault.pages.dev/designs/user-research-recruitment-board/sample.html
Template, input schema, sample data and metadata. This document was generated from those four. The generated document
/designs/user-research-recruitment-board/source/template.tsx
import type { Props } from "./schema";
const CSS = String.raw`
.asset-user-research-recruitment-board{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2.5rem;line-height:1.55;font-size:var(--text-body-sm)}
.asset-user-research-recruitment-board *{box-sizing:border-box}.asset-user-research-recruitment-board header{margin-bottom:1.2rem}.asset-user-research-recruitment-board .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary);margin:0 0 .6rem}.asset-user-research-recruitment-board h1{font-size:var(--text-h1);line-height:1.25;max-width:28em;margin:0}.asset-user-research-recruitment-board .subtitle{color:var(--color-secondary);margin:.65rem 0 0}.asset-user-research-recruitment-board .summary{display:flex;gap:1.4rem;flex-wrap:wrap;padding:.7rem 0;border-block:1px solid var(--color-outline);margin-bottom:1rem}.asset-user-research-recruitment-board .summary p{margin:0;color:var(--color-secondary);font-size:var(--text-label)}.asset-user-research-recruitment-board .summary strong{font-family:var(--font-mono);font-size:var(--text-h3);color:var(--color-primary);margin-right:.3rem}.asset-user-research-recruitment-board .segments{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.asset-user-research-recruitment-board .segment{border-top:2px solid var(--color-primary);background:var(--color-surface);min-width:0}.asset-user-research-recruitment-board .segment-head{padding:.7rem .7rem .6rem;background:var(--color-surface-subtle)}.asset-user-research-recruitment-board .segment-head code,.asset-user-research-recruitment-board .participant-head code{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary);overflow-wrap:anywhere}.asset-user-research-recruitment-board .segment-head h2{font-size:var(--text-body-md);margin:.15rem 0}.asset-user-research-recruitment-board .screener{font-size:var(--text-label);color:var(--color-secondary);margin:.2rem 0 0;overflow-wrap:anywhere}.asset-user-research-recruitment-board .quota{display:flex;justify-content:space-between;gap:.5rem;margin-top:.45rem;font-size:var(--text-label);color:var(--color-secondary)}.asset-user-research-recruitment-board .bar{height:3px;background:var(--color-outline);margin-top:.35rem}.asset-user-research-recruitment-board .bar span{display:block;height:100%;background:var(--color-tertiary)}.asset-user-research-recruitment-board .participant{padding:.6rem .7rem;border-bottom:1px solid var(--color-outline);min-width:0}.asset-user-research-recruitment-board .participant:last-child{border-bottom:0}.asset-user-research-recruitment-board .participant-head{display:flex;gap:.45rem;align-items:center;flex-wrap:wrap}.asset-user-research-recruitment-board .badge{font-size:var(--text-label);border:1px solid var(--color-outline);padding:.03rem .32rem;color:var(--color-secondary)}.asset-user-research-recruitment-board .participant p{font-size:var(--text-label);margin:.35rem 0 0;overflow-wrap:anywhere}.asset-user-research-recruitment-board .participant-meta{display:flex;gap:.65rem;flex-wrap:wrap;margin-top:.28rem;color:var(--color-secondary);font-size:var(--text-label)}.asset-user-research-recruitment-board .still-svg{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media(max-width:700px){.asset-user-research-recruitment-board{padding:1.5rem 1rem}.asset-user-research-recruitment-board h1{font-size:var(--text-h2)}.asset-user-research-recruitment-board .segments{grid-template-columns:minmax(0,1fr)}.asset-user-research-recruitment-board .summary{gap:.75rem}}
@media print{.asset-user-research-recruitment-board{max-width:none;padding:1rem}.asset-user-research-recruitment-board .segment{break-inside:avoid}}
`;
const statusLabel = { invited: "招待", screening: "選考中", eligible: "適格", scheduled: "日程確定", completed: "完了", declined: "辞退", "screened-out": "対象外" };
const consentLabel = { pending: "同意確認中", granted: "同意済み", declined: "同意辞退" };
function Still({ data }: { data: Props }) {
return <svg className="still-svg" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" aria-hidden="true">
<rect width="1280" height="720" fill="var(--color-surface)" />
<text x="48" y="44" fontSize="14" fontFamily="sans-serif" fill="var(--color-secondary)">{data.study}</text>
<text x="48" y="85" fontSize="26" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{data.title}</text>
<text x="48" y="120" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">募集対象 · 必要人数 · 匿名候補 · 同意 · 日程</text>
{data.segments.slice(0, 8).map((segment, index) => {
const y = 170 + index * 62;
const completed = data.participants.filter((item) => item.segmentId === segment.id && item.status === "completed").length;
const participants = data.participants.filter((item) => item.segmentId === segment.id);
return <g key={segment.id}>
<line x1="48" x2="1232" y1={y - 19} y2={y - 19} stroke="var(--color-outline)" />
<text x="52" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-tertiary)">{segment.id}</text>
<text x="155" y={y + 3} fontSize="12" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{segment.label}</text>
<text x="480" y={y + 3} fontSize="11" fontFamily="sans-serif" fill="var(--color-secondary)">{completed} / {segment.target} 完了</text>
<text x="640" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-on-surface)">{participants.map((item) => item.id).join(" · ")}</text>
</g>;
})}
<text x="48" y="704" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">{data.segments.length}セグメント · {data.participants.length}匿名候補 · 個人連絡先は含めない</text>
</svg>;
}
export default function Template(data: Props) {
const completed = data.participants.filter((participant) => participant.status === "completed").length;
const scheduled = data.participants.filter((participant) => participant.status === "scheduled").length;
const pendingConsent = data.participants.filter((participant) => participant.consent === "pending").length;
return <section className="asset asset-user-research-recruitment-board">
<style>{CSS}</style>
<header>
<p className="eyebrow">{data.study} · 調査参加者募集</p>
<h1>{data.title}</h1>
{data.subtitle && <p className="subtitle">{data.subtitle}</p>}
</header>
<div className="summary" aria-label="募集進捗"><p><strong>{data.segments.length}</strong>対象セグメント</p><p><strong>{data.participants.length}</strong>候補</p><p><strong>{scheduled}</strong>日程確定</p><p><strong>{completed}</strong>調査完了</p><p><strong>{pendingConsent}</strong>同意確認中</p></div>
<div className="segments">
{data.segments.map((segment) => {
const candidates = data.participants.filter((participant) => participant.segmentId === segment.id);
const done = candidates.filter((participant) => participant.status === "completed").length;
const width = Math.min(100, Math.round(done / segment.target * 100));
return <section className="segment" key={segment.id}>
<div className="segment-head"><code>{segment.id}</code><h2>{segment.label}</h2><p className="screener">{segment.screener}</p><div className="quota"><span>完了枠 {done} / {segment.target}</span><span>候補 {candidates.length}</span></div><div className="bar" aria-label={`${done} of ${segment.target} interviews complete`}><span style={{ width: `${width}%` }} /></div></div>
{candidates.map((participant) => <article className="participant" key={participant.id}>
<div className="participant-head"><code>{participant.id}</code><span className="badge" data-status={participant.status}>{statusLabel[participant.status]}</span><span className="badge" data-consent={participant.consent}>{consentLabel[participant.consent]}</span></div>
<p>{participant.screenerNote}</p>
<div className="participant-meta"><span>区分: {participant.segmentId}</span>{participant.sessionDate && <span>調査日: {participant.sessionDate}</span>}</div>
</article>)}
</section>;
})}
</div>
<Still data={data} />
</section>;
}
/designs/user-research-recruitment-board/source/schema.ts
import { z } from "zod";
const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "日付はYYYY-MM-DD形式にすること");
const segmentSchema = z.object({
id: short.describe("募集対象セグメントの一意な識別子"),
label: short.describe("募集対象の短い説明"),
screener: text.describe("対象条件・除外条件"),
target: z.number().int().min(1).max(20).describe("調査完了に必要な参加人数"),
}).strict();
const participantSchema = z.object({
id: short.describe("個人名を含めない候補者コード"),
segmentId: short.describe("募集セグメントへの参照"),
status: z.enum(["invited", "screening", "eligible", "scheduled", "completed", "declined", "screened-out"]).describe("募集・選考・調査の状態"),
consent: z.enum(["pending", "granted", "declined"]).describe("調査参加同意の状態"),
screenerNote: text.describe("適格性に関する非識別の選考メモ"),
sessionDate: date.optional().describe("予定または実施した調査日"),
}).strict();
export const schema = z.object({
title: text.describe("募集進捗または調査対象の要点"),
subtitle: text.optional().describe("募集期間・調査ラウンド・連絡窓口チーム"),
study: short.describe("調査名またはラウンド"),
segments: z.array(segmentSchema).min(3).max(8).describe("対象条件と必要人数を定義したセグメント"),
participants: z.array(participantSchema).min(3).max(16).describe("匿名コードで追跡する候補者と同意・日程"),
}).strict().superRefine((data, ctx) => {
const segmentIds = data.segments.map(({ id }) => id);
const participantIds = data.participants.map(({ id }) => id);
if (new Set(segmentIds).size !== segmentIds.length) {
ctx.addIssue({ code: "custom", path: ["segments"], message: "セグメントIDは一意にすること" });
}
if (new Set(participantIds).size !== participantIds.length) {
ctx.addIssue({ code: "custom", path: ["participants"], message: "候補者コードは一意にすること" });
}
const knownSegments = new Set(segmentIds);
data.participants.forEach((participant, index) => {
if (!knownSegments.has(participant.segmentId)) {
ctx.addIssue({ code: "custom", path: ["participants", index, "segmentId"], message: `未登録のセグメント: ${participant.segmentId}` });
}
if (["scheduled", "completed"].includes(participant.status) && (participant.consent !== "granted" || !participant.sessionDate)) {
ctx.addIssue({ code: "custom", path: ["participants", index], message: "日程確定・調査完了には同意済みかつ調査日が必要" });
}
if (participant.consent === "declined" && participant.status !== "declined") {
ctx.addIssue({ code: "custom", path: ["participants", index, "status"], message: "同意を辞退した候補者は辞退状態にすること" });
}
});
});
export type Props = z.infer<typeof schema>;
/designs/user-research-recruitment-board/source/meta.yaml
id: user-research-recruitment-board
name:
ja: ユーザー調査リクルートボード
en: User Research Recruitment Board
purpose:
ja: 利用者セグメントごとの募集枠と、匿名候補の適格性・同意・日程・調査完了を追跡する。
en: Track recruitment quotas and each pseudonymous candidate's eligibility, consent, schedule, and study completion by segment.
status: published
still: ./still.png
category: experience
catalogOrder: 855
jobs:
- plan-sequence
- assign-governance
useCases:
- status
layoutKinds:
- grid
- stack
tags:
- user-research
- participant-recruitment
- consent
- screener
- pseudonymous
whenToUse:
- ja: 調査対象の枠数と募集候補、参加同意、セッション日程をセグメント別に調整するとき。
en: Use to coordinate sample quotas, candidate screening, consent, and session schedules by segment.
avoidWhen:
- ja: 調査で得た引用・発見・根拠を記録する場合は、研究エビデンステーブルを使う。
en: Use a research evidence table to record findings, quotations, and supporting evidence.
requiredInputs:
- ja: セグメント条件と目標人数、氏名や連絡先を含まない候補コード、選考状態、同意状態、調査日を用意する。
en: Provide segment criteria and targets, non-identifying candidate codes, screening and consent states, and session dates.
relatedLayouts:
- research-evidence-table
- customer-journey-map
dataSchema:
title: string — 募集進捗または調査対象の要点
subtitle: string? — 募集期間・調査ラウンド・連絡窓口チーム
study: string — 調査名またはラウンド
segments: array (3–8) — 対象条件と必要人数
participants: array (3–16) — 個人情報を含まない候補コード、セグメント、状態、同意、非識別選考メモ、調査日
sources: []
/designs/user-research-recruitment-board/source/sample-data.json
{
"title": "利用頻度の異なる参加者を確保し、完了枠の不足を早めに補う",
"subtitle": "Checkout discovery · 2026-09-21–10-02募集 · Research Operations",
"study": "Checkout Study R4",
"segments": [
{ "id": "weekly-buyers", "label": "週1回以上の購入者", "screener": "直近30日で購入2回以上。従業員アカウントは除外。", "target": 3 },
{ "id": "first-order", "label": "初回購入者", "screener": "過去に注文がなく、今月初回購入を完了した利用者。", "target": 2 },
{ "id": "assistive-tech", "label": "支援技術利用者", "screener": "日常的にキーボードまたはスクリーンリーダーで購入する。", "target": 2 }
],
"participants": [
{ "id": "P-R4-021", "segmentId": "weekly-buyers", "status": "scheduled", "consent": "granted", "screenerNote": "条件一致。モバイル購入を希望。", "sessionDate": "2026-09-25" },
{ "id": "P-R4-022", "segmentId": "weekly-buyers", "status": "screening", "consent": "granted", "screenerNote": "直近注文数と配送先変更経験を確認中。" },
{ "id": "P-R4-023", "segmentId": "first-order", "status": "invited", "consent": "pending", "screenerNote": "初回購入の対象条件を注文イベントで確認済み。" },
{ "id": "P-R4-024", "segmentId": "assistive-tech", "status": "completed", "consent": "granted", "screenerNote": "キーボード操作と音声読み上げを併用。", "sessionDate": "2026-09-23" }
]
}