索引に戻る

ユーザー調査リクルートボード

グリッド
生成可能
ユーザー調査リクルートボード

作品として見る

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

user-research-recruitment-board-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

1.8 KB
「ユーザー調査リクルートボード」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
利用者セグメントごとの募集枠と、匿名候補の適格性・同意・日程・調査完了を追跡する。

## 渡す必要があるもの
- セグメント条件と目標人数、氏名や連絡先を含まない候補コード、選考状態、同意状態、調査日を用意する。

## 入力の形
- `title` — string — 募集進捗または調査対象の要点
- `subtitle` — string? — 募集期間・調査ラウンド・連絡窓口チーム
- `study` — string — 調査名またはラウンド
- `segments` — array (3–8) — 対象条件と必要人数
- `participants` — array (3–16) — 個人情報を含まない候補コード、セグメント、状態、同意、非識別選考メモ、調査日

入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。

## 向かない場合
- 調査で得た引用・発見・根拠を記録する場合は、研究エビデンステーブルを使う。

## 作り方
入力を JSON にまとめ、次を実行します。

```bash
node scripts/render.mjs user-research-recruitment-board <入力.json> --theme ink -o out.html
```

出力は 1 ファイルで完結し、外部から何も読み込みません。

## 原本
- 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

ファイル

テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書

template.tsx

8.3 KB

/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>;
}

schema.ts

3.0 KB

/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>;

meta.yaml

1.9 KB

/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: []

sample-data.json

1.5 KB

/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" }
  ]
}