First CH Designs
BLK-70Blocks

監修者プロフィール(資格から根拠を引き当てる台帳)

記事・サービスの監修者を1人だけ深く見せる信頼ブロック。顔写真は使わず、イニシャルと登録番号を収めた角印のタイポグラフィで人物を示す。左カラムの保有資格は飾りのバッジではなく操作列で、押す・マウスを乗せる・フォーカスすると、右カラムの経歴と執筆・登壇実績のうちその資格を裏づける行だけが残る。非該当行は消さずに opacity で沈めるので、絞り込んでも行の高さとスクロール位置が動かない。該当の合図は幅2pxの縦罫の opacity だけで、レイアウトは一切動かさない。下見(hover/focus)とピン留め(click)を別の state に持ち、ピン留め中に他をなぞって戻れる。絞り込み結果は sr-only の aria-live で件数を読み上げ、視覚側のハイライトは装飾に留める。カードで囲わず gap-px の hairline と余白でグルーピングし、画像素材ゼロ・依存ゼロ。人物・経歴・実績はすべて表示サンプル。

追加日:
2026-09-20
依存:
なし
tags
#block #trust #profile #credential #career #ledger #cross-highlight #aria-live #hairline #container-query #no-image

プレビュー

肩書きではなく、根拠から見せる監修者

左の資格を押すと、その資格を裏づける経歴と実績だけが残ります。資格・経歴・実績がそれぞれ独立した箇条書きにならないよう、対応関係そのものを読めるかたちにしています。

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

shadcn CLI でプロジェクトに追加

npx shadcn@latest add https://designs.first-ch.com/r/supervisor-profile-eeat.json