Supervisor Profile Ledger (Credential-to-Evidence)
A trust block that profiles a single content supervisor in depth. No photography: the person is represented by a typographic square seal holding initials and a registration number. The credential list in the left column is a control surface rather than decoration — hovering, focusing or clicking a credential leaves only the career rows and publications that back it. Non-matching rows are dimmed with opacity instead of removed, so row heights and scroll position never move, and the match cue is the opacity of a 2px rule. Preview (hover/focus) and pinned (click) states are kept separately, so you can browse other credentials while one is pinned and fall back to it. The filtered counts are announced through an sr-only aria-live region while the visual highlight stays decorative. Grouped by gap-px hairlines and whitespace instead of cards; zero images, zero dependencies. All people and records are display samples.
- Added:
- 2026-09-20
- Dependencies:
- None
- tags
- #block #trust #profile #credential #career #ledger #cross-highlight #aria-live #hairline #container-query #no-image
Preview
肩書きではなく、根拠から見せる監修者
左の資格を押すと、その資格を裏づける経歴と実績だけが残ります。資格・経歴・実績がそれぞれ独立した箇条書きにならないよう、対応関係そのものを読めるかたちにしています。
BLK-70
4 credentials / 11 records
監修
桐生 奈央
きりゅう なお
労務コンサルタント
人事・労務領域の記事監修を担当
制度の説明は条文と告示に当たって確認し、実務の判断が分かれる箇所は「分かれる」と書くようにしています。数字と年度は公開時点のものを明記します。
保有資格
押すと根拠を絞り込み
監修範囲:労務・社会保険に関する記載範囲
最終確認日:2026-09-12
経歴
6 / 6
2009–2013
人材サービス会社・法人営業
採用支援の窓口として、中小企業の就業条件や賃金テーブルを日常的に読む立場にいました。
2014–2018
社労士事務所・労務顧問
顧問先40社の就業規則改定と社会保険の手続きを担当。行政調査の立ち会いもここで経験しました。
2016–2019
製造業の安全衛生委員会・外部委員
現場の長時間労働と作業環境の改善を、委員会の議題設計から支援しました。
2018–2022
復職支援プログラムの設計
メンタル不調からの復職について、面談の進め方と情報の扱いを定めた運用手順を整えました。
2021–現在
人事データ取扱い規程の整備支援
評価・健康情報の保管期間と開示請求への対応を、規程と台帳の両面から設計しています。
2023–現在
記事監修(一次確認)
公開前に根拠資料との突き合わせを行い、改正の影響を受ける記述には確認日を残しています。
執筆・登壇
5 / 5
2020
執筆
就業規則を見直す順番と、先に決めておくこと
労務系の業界誌・寄稿連載
2021
登壇
形だけにならない安全衛生委員会の回し方
商工団体の実務セミナー
2022
監修
新入社員向け 労務ハンドブック
社内配布冊子(全72ページ)
2024
寄稿
復職面談で聞くこと、聞かないこと
人事向けウェブメディア
2025
登壇
人事データの保管期間と開示請求への備え
オンライン勉強会(参加約180名)
人物・経歴・実績はすべて表示サンプルです。実運用では監修範囲と最終確認日を必ず併記し、資格は登録番号を管理台帳側で保持したうえで、改正のあった記述は確認日を更新します。
"use client";
import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["600", "700", "800"],
subsets: ["latin"],
display: "swap",
});
/**
* 監修者プロフィール(資格から根拠を引き当てる台帳)
* 既存の信頼系は「複数人を等価に並べる名簿」「バッジを等価に並べる帯」「沿革だけを読ませる縦レール」が既出のため、
* 本ブロックは1人の監修者について 資格 → その資格を裏づける経歴・実績 の対応関係を双方向に示す台帳にした。
* 資格は飾りのバッジではなく経歴・実績の絞り込みスイッチで、名簿のイニシャル円ではなく登録番号入りの角印で人物を示す。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - 相互ハイライト(左の操作列 ↔ 右の本文行): hover と pin(クリック)を別stateに持ち、active = hovered ?? pinned で解決する。
* こうするとピン留め中に別項目へマウスを乗せても下見ができ、離せばピンに戻る(1つのstateで兼ねると戻れない)。
* - 非該当行は消さずに opacity で沈める: 行数と高さが変わらないので、絞り込んでも右カラムのスクロール位置が動かない。
* 該当行の合図は幅2pxの縦罫(absolute・opacityのみ変化)で、レイアウトを一切動かさない。
* - 絞り込みの結果は sr-only の aria-live="polite" で件数だけを読み上げる。視覚のハイライトは aria-hidden の装飾に留め、
* 行そのものには状態属性を足さない(読み上げ順は常に台帳の並び順のまま)。
*/
type Credential = {
id: string;
/** 角印の隣に置く欧文の略号(ロゴ画像の代わり) */
abbr: string;
name: string;
year: number;
issuer: string;
};
type CareerRow = {
period: string;
role: string;
body: string;
/** この経歴が裏づける資格のid */
creds: string[];
};
type OutputRow = {
year: string;
kind: "執筆" | "監修" | "登壇" | "寄稿";
title: string;
outlet: string;
creds: string[];
};
const supervisor = {
initials: "KN",
regNo: "0142",
name: "桐生 奈央",
reading: "きりゅう なお",
title: "労務コンサルタント",
affiliation: "人事・労務領域の記事監修を担当",
years: 17,
policy:
"制度の説明は条文と告示に当たって確認し、実務の判断が分かれる箇所は「分かれる」と書くようにしています。数字と年度は公開時点のものを明記します。",
scope: "労務・社会保険に関する記載範囲",
checkedAt: "2026-09-12",
};
const credentials: Credential[] = [
{ id: "sr", abbr: "SR", name: "社会保険労務士", year: 2014, issuer: "国家資格" },
{ id: "hygiene", abbr: "HY", name: "第一種衛生管理者", year: 2016, issuer: "国家資格" },
{ id: "counselor", abbr: "CC", name: "産業カウンセラー", year: 2018, issuer: "民間資格" },
{ id: "privacy", abbr: "PP", name: "個人情報保護士", year: 2021, issuer: "民間資格" },
];
const career: CareerRow[] = [
{
period: "2009–2013",
role: "人材サービス会社・法人営業",
body: "採用支援の窓口として、中小企業の就業条件や賃金テーブルを日常的に読む立場にいました。",
creds: [],
},
{
period: "2014–2018",
role: "社労士事務所・労務顧問",
body: "顧問先40社の就業規則改定と社会保険の手続きを担当。行政調査の立ち会いもここで経験しました。",
creds: ["sr"],
},
{
period: "2016–2019",
role: "製造業の安全衛生委員会・外部委員",
body: "現場の長時間労働と作業環境の改善を、委員会の議題設計から支援しました。",
creds: ["hygiene", "sr"],
},
{
period: "2018–2022",
role: "復職支援プログラムの設計",
body: "メンタル不調からの復職について、面談の進め方と情報の扱いを定めた運用手順を整えました。",
creds: ["counselor"],
},
{
period: "2021–現在",
role: "人事データ取扱い規程の整備支援",
body: "評価・健康情報の保管期間と開示請求への対応を、規程と台帳の両面から設計しています。",
creds: ["privacy", "sr"],
},
{
period: "2023–現在",
role: "記事監修(一次確認)",
body: "公開前に根拠資料との突き合わせを行い、改正の影響を受ける記述には確認日を残しています。",
creds: ["sr", "privacy"],
},
];
const outputs: OutputRow[] = [
{
year: "2020",
kind: "執筆",
title: "就業規則を見直す順番と、先に決めておくこと",
outlet: "労務系の業界誌・寄稿連載",
creds: ["sr"],
},
{
year: "2021",
kind: "登壇",
title: "形だけにならない安全衛生委員会の回し方",
outlet: "商工団体の実務セミナー",
creds: ["hygiene"],
},
{
year: "2022",
kind: "監修",
title: "新入社員向け 労務ハンドブック",
outlet: "社内配布冊子(全72ページ)",
creds: ["sr", "counselor"],
},
{
year: "2024",
kind: "寄稿",
title: "復職面談で聞くこと、聞かないこと",
outlet: "人事向けウェブメディア",
creds: ["counselor"],
},
{
year: "2025",
kind: "登壇",
title: "人事データの保管期間と開示請求への備え",
outlet: "オンライン勉強会(参加約180名)",
creds: ["privacy"],
},
];
/** 資格1件あたりの根拠件数(経歴+実績) */
function evidenceCount(id: string) {
return (
career.filter((c) => c.creds.includes(id)).length +
outputs.filter((o) => o.creds.includes(id)).length
);
}
export default function SupervisorProfileDossier() {
const rootRef = useRef<HTMLElement>(null);
const [inView, setInView] = useState(false);
/** マウス/フォーカスでの下見と、クリックでのピン留めを別に持つ */
const [hovered, setHovered] = useState<string | null>(null);
const [pinned, setPinned] = useState<string | null>(null);
const active = hovered ?? pinned;
// 見出しの下線だけ、画面に入ったら引く(読み物なので一斉スタッガーは掛けない)
useEffect(() => {
const el = rootRef.current;
if (!el) return;
if (typeof IntersectionObserver === "undefined") {
const id = requestAnimationFrame(() => setInView(true));
return () => cancelAnimationFrame(id);
}
const io = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setInView(true);
io.disconnect();
}
},
{ threshold: 0.12 },
);
io.observe(el);
return () => io.disconnect();
}, []);
const activeCred = credentials.find((c) => c.id === active) ?? null;
const careerHit = active ? career.filter((c) => c.creds.includes(active)).length : career.length;
const outputHit = active ? outputs.filter((o) => o.creds.includes(active)).length : outputs.length;
/** 該当=通常表示 / 非該当=沈める。行の高さは変えない */
const dim = (creds: string[]) =>
active && !creds.includes(active)
? "opacity-35 motion-reduce:opacity-45"
: "opacity-100";
const hit = (creds: string[]) => Boolean(active) && creds.includes(active as string);
return (
<section
ref={rootRef}
aria-labelledby="supervisor-profile-heading"
className="@container w-full max-w-5xl bg-white"
>
{/* ヘッダー(英字キッカーは置かない・見出しは体言止め) */}
<div className="flex flex-col gap-5 @2xl:flex-row @2xl:items-end @2xl:justify-between">
<div className="max-w-2xl">
<h2
id="supervisor-profile-heading"
className="text-[clamp(1.65rem,3.6vw,2.3rem)] leading-[1.4] font-bold tracking-tight text-gray-900"
>
肩書きではなく、
<span className="relative inline-block">
<span
aria-hidden="true"
className={`absolute -bottom-1 h-[3px] w-full origin-left bg-amber-500/70 transition-transform duration-700 ease-out motion-reduce:transition-none ${
inView ? "scale-x-100" : "scale-x-0 motion-reduce:scale-x-100"
}`}
/>
<span className="relative">根拠から見せる監修者</span>
</span>
</h2>
<p className="mt-5 text-[15px] leading-[1.9] font-medium text-gray-600">
左の資格を押すと、その資格を裏づける経歴と実績だけが残ります。資格・経歴・実績がそれぞれ独立した箇条書きにならないよう、対応関係そのものを読めるかたちにしています。
</p>
</div>
<div className={`${montserrat.className} shrink-0 text-[11px] font-semibold text-gray-400`}>
<p className="flex items-center gap-1.5 text-gray-500">
<span aria-hidden="true" className="inline-block size-1 bg-amber-600" />
BLK-70
</p>
<p className="mt-1.5 tabular-nums">
{credentials.length} credentials / {career.length + outputs.length} records
</p>
</div>
</div>
{/* 台帳本体: カードで囲わず gap-px と下地色の hairline で2列を束ねる */}
<div className="mt-8 grid gap-px border-y border-gray-200 bg-gray-200 @3xl:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]">
{/* 左: 人物と資格(操作列) */}
<div className="bg-white px-5 py-6 @md:px-7 @md:py-8">
<div className="flex items-start gap-4">
{/* 角印(名簿のイニシャル円ではなく、登録番号を抱えた四角の印) */}
<div
aria-hidden="true"
className="relative size-[76px] shrink-0 border border-gray-900 bg-white"
>
<span className="absolute inset-[3px] border border-gray-200" />
<span
className={`${montserrat.className} absolute inset-x-0 top-[13px] text-center text-[26px] leading-none font-extrabold tracking-tight text-gray-900`}
>
{supervisor.initials}
</span>
<span className="absolute inset-x-[10px] top-[46px] h-px bg-amber-600" />
<span
className={`${montserrat.className} absolute inset-x-0 bottom-[9px] text-center text-[10px] font-semibold tabular-nums text-gray-400`}
>
No.{supervisor.regNo}
</span>
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold text-amber-700">監修</p>
<p className="mt-1 text-[19px] leading-[1.4] font-bold tracking-tight text-gray-900">
{supervisor.name}
</p>
<p className="mt-0.5 text-[11px] text-gray-400">{supervisor.reading}</p>
<p className="mt-2.5 text-[12.5px] leading-[1.7] font-bold text-gray-800">
{supervisor.title}
</p>
<p className="mt-1 text-[12px] leading-[1.7] text-gray-500">
{supervisor.affiliation}
</p>
</div>
</div>
<p className="mt-6 border-t border-gray-200 pt-5 text-[12.5px] leading-[1.95] text-gray-600">
{supervisor.policy}
</p>
<div className="mt-6 flex items-baseline justify-between border-t border-gray-200 pt-5">
<p className="text-[11px] font-bold text-gray-900">保有資格</p>
<p className="text-[11px] text-gray-400">押すと根拠を絞り込み</p>
</div>
<ul className="mt-3">
{credentials.map((c) => {
const on = active === c.id;
return (
<li key={c.id} className="border-t border-gray-200 first:border-t-0">
<button
type="button"
aria-pressed={on}
onMouseEnter={() => setHovered(c.id)}
onMouseLeave={() => setHovered(null)}
onFocus={() => setHovered(c.id)}
onBlur={() => setHovered(null)}
onClick={() => setPinned((p) => (p === c.id ? null : c.id))}
onKeyDown={(e) => {
if (e.key === "Escape") {
setPinned(null);
setHovered(null);
}
}}
className={`group relative flex w-full items-baseline gap-3 py-3 pr-1 pl-3.5 text-left transition-colors duration-200 hover:bg-amber-50/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none ${
on ? "bg-amber-50/60" : ""
}`}
>
<span
aria-hidden="true"
className={`absolute inset-y-0 left-0 w-[2px] bg-amber-600 transition-opacity duration-200 motion-reduce:transition-none ${
on ? "opacity-100" : "opacity-0"
}`}
/>
<span
aria-hidden="true"
className={`${montserrat.className} w-7 shrink-0 text-[12px] font-bold text-gray-400 group-hover:text-amber-700 ${
on ? "text-amber-700" : ""
}`}
>
{c.abbr}
</span>
<span className="min-w-0 flex-1">
<span className="block text-[13px] leading-[1.6] font-bold text-gray-900">
{c.name}
</span>
<span className="mt-0.5 block text-[11px] text-gray-400">{c.issuer}</span>
</span>
<span
className={`${montserrat.className} shrink-0 text-[12px] font-semibold tabular-nums text-gray-500`}
>
{c.year}
</span>
<span className="sr-only">
取得{c.year}年。根拠となる経歴・実績 {evidenceCount(c.id)} 件を強調表示します。
</span>
</button>
</li>
);
})}
<li className="border-t border-gray-200">
<button
type="button"
aria-pressed={active === null}
onClick={() => {
setPinned(null);
setHovered(null);
}}
className="w-full py-3 pl-3.5 text-left text-[11px] font-semibold text-gray-500 transition-colors duration-200 hover:text-amber-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
絞り込みを解除してすべて表示
</button>
</li>
</ul>
<p className="mt-6 border-t border-gray-200 pt-4 text-[11px] leading-[1.85] text-gray-400">
監修範囲:{supervisor.scope}
<br />
最終確認日:<span className="tabular-nums">{supervisor.checkedAt}</span>
</p>
</div>
{/* 右: 根拠(経歴と実績) */}
<div className="bg-white">
<div aria-live="polite" className="sr-only">
{activeCred
? `${activeCred.name}に関連する経歴${careerHit}件、実績${outputHit}件を強調表示中です。`
: "すべての経歴と実績を表示しています。"}
</div>
<div className="px-5 py-6 @md:px-7 @md:py-8">
<div className="flex items-baseline justify-between gap-4 border-b border-gray-900 pb-2.5">
<h3 className="flex items-center gap-2 text-[12px] font-bold text-gray-900">
<span aria-hidden="true" className="inline-block size-1.5 bg-amber-600" />
経歴
</h3>
<p className={`${montserrat.className} text-[11px] tabular-nums text-gray-400`}>
{careerHit} / {career.length}
</p>
</div>
<ul className="mt-1">
{career.map((row) => (
<li
key={row.period}
className={`relative border-t border-gray-200 py-4 pl-3.5 transition-opacity duration-300 first:border-t-0 motion-reduce:transition-none ${dim(row.creds)}`}
>
<span
aria-hidden="true"
className={`absolute inset-y-3 left-0 w-[2px] bg-amber-600 transition-opacity duration-300 motion-reduce:transition-none ${
hit(row.creds) ? "opacity-100" : "opacity-0"
}`}
/>
<div className="grid gap-x-5 gap-y-1 @md:grid-cols-[7.5rem_minmax(0,1fr)]">
<p
className={`${montserrat.className} text-[12px] font-semibold tabular-nums text-gray-500`}
>
{row.period}
</p>
<div>
<p className="text-[13.5px] leading-[1.65] font-bold text-gray-900">
{row.role}
</p>
<p className="mt-1.5 text-[12.5px] leading-[1.95] text-gray-600">{row.body}</p>
</div>
</div>
</li>
))}
</ul>
<div className="mt-9 flex items-baseline justify-between gap-4 border-b border-gray-900 pb-2.5">
<h3 className="flex items-center gap-2 text-[12px] font-bold text-gray-900">
<span aria-hidden="true" className="inline-block size-1.5 bg-amber-600" />
執筆・登壇
</h3>
<p className={`${montserrat.className} text-[11px] tabular-nums text-gray-400`}>
{outputHit} / {outputs.length}
</p>
</div>
<ul className="mt-1">
{outputs.map((row) => (
<li
key={row.title}
className={`relative border-t border-gray-200 py-3.5 pl-3.5 transition-opacity duration-300 first:border-t-0 motion-reduce:transition-none ${dim(row.creds)}`}
>
<span
aria-hidden="true"
className={`absolute inset-y-2.5 left-0 w-[2px] bg-amber-600 transition-opacity duration-300 motion-reduce:transition-none ${
hit(row.creds) ? "opacity-100" : "opacity-0"
}`}
/>
<div className="grid gap-x-5 gap-y-1 @md:grid-cols-[3.5rem_4rem_minmax(0,1fr)] @md:items-baseline">
<p
className={`${montserrat.className} text-[12px] font-semibold tabular-nums text-gray-500`}
>
{row.year}
</p>
<p className="text-[11px] font-bold text-amber-700 @md:text-gray-500">
{row.kind}
</p>
<div>
<p className="text-[13px] leading-[1.7] font-bold text-gray-900">
{row.title}
</p>
<p className="mt-1 text-[11.5px] leading-[1.7] text-gray-400">{row.outlet}</p>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
</div>
<p className="mt-6 text-[12px] leading-[1.9] text-gray-400">
人物・経歴・実績はすべて表示サンプルです。実運用では監修範囲と最終確認日を必ず併記し、資格は登録番号を管理台帳側で保持したうえで、改正のあった記述は確認日を更新します。
</p>
</section>
);
}
Add to your project via shadcn CLI
npx shadcn@latest add https://designs.first-ch.com/r/supervisor-profile-eeat.json