Back to index

User Research Recruitment Board

Grid
Generates documents
User Research Recruitment Board

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

user-research-recruitment-board-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.7 KB
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

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

8.3 KB

/designs/user-research-recruitment-board/source/template.tsx

Show contents
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>;
}

schema.ts

3.0 KB

/designs/user-research-recruitment-board/source/schema.ts

Show contents
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>;

meta.yaml

1.9 KB

/designs/user-research-recruitment-board/source/meta.yaml

Show contents
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: []

sample-data.json

1.5 KB

/designs/user-research-recruitment-board/source/sample-data.json

Show contents
{
  "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" }
  ]
}