First CH Designs
BLK-11Blocks

会社概要・アクセス

左に商号・代表者・設立・事業内容の定義リスト、右にCSSだけで組んだ地図プレースホルダ+住所+交通、下に曜日×区分の営業時間表を置いた会社案内ブロック。医療系サイト(けやき通り診療所ほか)の診療時間表の行列の組み方をコーポレート用に転用し、カードを使わず hairline 罫線と余白だけでグルーピングしている。画像素材ゼロ。

追加日:
2026-08-05
依存:
なし
tags
#company #about #access #map #table #matrix #definition-list #hairline #section #block #brand #motion #no-image #ref:keyaki-kampo-jp

プレビュー

BLK-11 — Company & Access

会社の輪郭と、たどり着き方

はじめて取引する会社について知りたいことは、だいたい決まっています。 だれが、いつから、何をしているのか。そして、どこへ行けば会えるのか。

会社概要

商号
First CH合同会社(First CH LLC)
代表者
代表社員 千原 隼人
設立
2024年12月
事業年度
10月1日 〜 翌9月30日
事業内容
  • Webサイト・LP制作
  • AI導入コンサルティング
  • 業務システム開発
対応エリア
全国(オンライン対応)

アクセス

〒150-0001東京都渋谷区神宮前0-0-0
First CHビル 5F

電車
地下鉄◯◯線「けやき通り」駅2番出口より徒歩5分
バス
市営バス「けやき通り三丁目」下車・徒歩2分
お車
首都高速◯◯出口より約8分(提携駐車場3台)

営業時間

曜日ごとの営業状況(ご来社・オンライン相談)
区分
ご来社月曜日 受付火曜日 受付水曜日 受付木曜日 受付金曜日 受付土曜日 事前予約制日曜日 休業
オンライン月曜日 受付火曜日 受付水曜日 受付木曜日 受付金曜日 受付土曜日 受付日曜日 事前予約制

営業時間 10:00–19:00(土曜は 10:00–15:00)/祝日・年末年始は休業。土曜のご来社は前日までのご予約制です。

"use client";

import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";

const montserrat = Montserrat({
  weight: ["600", "700"],
  subsets: ["latin"],
  display: "swap",
});

/**
 * 会社概要・アクセス(定義リスト × 行列表 × CSS地図)
 * 参考: 医療系サイト群(けやき通り診療所ほか)の「診療時間表」。曜日×区分のマスに ●/△/− を置く
 * 組み方を、コーポレートの営業時間表へ転用した(見た目のコピーではなく自社ブランドで再構築)。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - 行列表(matrix)は素直に <table> で組む。table-fixed +「先頭列だけ固定幅」にすると、
 *   271px(375px 幅でのプレビュー実寸)でも7曜日が等分されて破綻しない。可変列に幅を書かないのが肝。
 * - 記号セル(●/△/−)は視覚専用。意味は <span class="sr-only">受付</span> で渡し記号側を aria-hidden に。
 *   これをやらないと読み上げが「黒丸 黒丸 黒丸…」になり表の意味が消える。
 * - 地図プレースホルダは画像ゼロで作れる: 温かいグレーの下地 + 白い帯(道路)+ 街区の矩形
 *   + 破線の線路。ピンだけ transform の scale リングで呼吸させる(opacity/transform のみ)。
 *   地図は装飾なので aria-hidden にし、住所・交通は必ずテキストで併記する。
 * - 脱カード: 左右カラムは lg:divide-x の hairline で仕切り、上下は border-y と余白だけで束ねる。
 *   罫線を1本引くたびに箱を1つ減らせる。
 */

type ProfileRow = {
  term: string;
  /** 単一行のとき */
  value?: string;
  /** 箇条書きのとき(事業内容など) */
  items?: string[];
};

const profile: ProfileRow[] = [
  { term: "商号", value: "First CH合同会社(First CH LLC)" },
  { term: "代表者", value: "代表社員 千原 隼人" },
  { term: "設立", value: "2024年12月" },
  { term: "事業年度", value: "10月1日 〜 翌9月30日" },
  {
    term: "事業内容",
    items: [
      "Webサイト・LP制作",
      "AI導入コンサルティング",
      "業務システム開発",
    ],
  },
  { term: "対応エリア", value: "全国(オンライン対応)" },
];

/** 交通は「場所」と「そこからの行き方」に分け、後半を whitespace-nowrap で割らせない */
const routes: { mode: string; place: string; tail: string }[] = [
  { mode: "電車", place: "地下鉄◯◯線「けやき通り」駅", tail: "2番出口より徒歩5分" },
  { mode: "バス", place: "市営バス「けやき通り三丁目」", tail: "下車・徒歩2分" },
  { mode: "お車", place: "首都高速◯◯出口より約8分", tail: "(提携駐車場3台)" },
];

/** 診療時間表の転用: 行=区分 / 列=曜日 / マス=状態 */
const days = ["月", "火", "水", "木", "金", "土", "日"] as const;

type Mark = "open" | "appt" | "closed";

const markMeta: Record<Mark, { glyph: string; label: string; className: string }> = {
  open: { glyph: "●", label: "受付", className: "text-amber-600" },
  appt: { glyph: "△", label: "事前予約制", className: "text-gray-500" },
  closed: { glyph: "−", label: "休業", className: "text-gray-300" },
};

const hours: { scope: string; marks: Mark[] }[] = [
  {
    scope: "ご来社",
    marks: ["open", "open", "open", "open", "open", "appt", "closed"],
  },
  {
    scope: "オンライン",
    marks: ["open", "open", "open", "open", "open", "open", "appt"],
  },
];

/** 街区の矩形(CSS地図。単位は %) */
const cityBlocks = [
  { left: 4, top: 6, w: 26, h: 22 },
  { left: 46, top: 4, w: 20, h: 16 },
  { left: 72, top: 8, w: 22, h: 26 },
  { left: 6, top: 36, w: 22, h: 18 },
  { left: 52, top: 30, w: 16, h: 14 },
  { left: 76, top: 44, w: 18, h: 20 },
  { left: 10, top: 64, w: 18, h: 16 },
  { left: 44, top: 62, w: 24, h: 14 },
];

export default function CompanyAccess() {
  const rootRef = useRef<HTMLElement>(null);
  const [active, setActive] = useState(false);

  // 画面内に入ったらリビール開始(IO 非対応環境は即時表示)
  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    if (typeof IntersectionObserver === "undefined") {
      const id = requestAnimationFrame(() => setActive(true));
      return () => cancelAnimationFrame(id);
    }
    const io = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting)) {
          setActive(true);
          io.disconnect();
        }
      },
      { threshold: 0.15 }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const reveal = `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
    active
      ? "translate-y-0 opacity-100"
      : "translate-y-6 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
  }`;

  return (
    <section
      ref={rootRef}
      aria-labelledby="company-access-heading"
      className="w-full max-w-5xl bg-white"
    >
      {/* ヘッダー */}
      <header className="max-w-2xl">
        <p
          className={`${montserrat.className} text-xs font-bold tracking-[0.25em] text-amber-600 uppercase ${reveal}`}
          style={{ transitionDelay: "0ms" }}
        >
          BLK-11 — Company &amp; Access
        </p>
        <h2
          id="company-access-heading"
          className={`mt-4 text-[clamp(1.7rem,4vw,2.4rem)] leading-[1.3] font-bold tracking-tight text-gray-900 ${reveal}`}
          style={{ transitionDelay: "80ms" }}
        >
          会社の輪郭と、
          <span className="relative whitespace-nowrap">
            たどり着き方
            <span
              aria-hidden="true"
              className="absolute -bottom-1 left-0 h-[3px] w-full bg-amber-500/70"
            />
          </span>
          。
        </h2>
        <p
          className={`mt-5 text-[15px] leading-[1.9] font-medium text-gray-600 ${reveal}`}
          style={{ transitionDelay: "160ms" }}
        >
          はじめて取引する会社について知りたいことは、だいたい決まっています。
          だれが、いつから、何をしているのか。そして、どこへ行けば会えるのか。
        </p>
      </header>

      {/* 会社概要 / アクセス(hairline で2分割・箱で囲まない) */}
      <div
        className={`mt-12 grid grid-cols-1 gap-10 border-t border-gray-200 pt-10 lg:grid-cols-12 lg:gap-0 lg:divide-x lg:divide-gray-200 ${reveal}`}
        style={{ transitionDelay: "240ms" }}
      >
        {/* 左: 会社概要(定義リスト) */}
        <div className="lg:col-span-7 lg:pr-10">
          <h3 className="flex items-center gap-2.5 text-sm font-bold tracking-[0.08em] text-gray-900">
            <span aria-hidden="true" className="h-px w-5 flex-none bg-amber-600" />
            会社概要
          </h3>
          <dl className="mt-5 divide-y divide-gray-100 border-t border-gray-100">
            {profile.map((row) => (
              <div
                key={row.term}
                className="grid grid-cols-[5rem_1fr] gap-x-4 py-3.5 sm:grid-cols-[7rem_1fr]"
              >
                <dt className="pt-[0.3em] text-[11px] font-bold tracking-[0.12em] whitespace-nowrap text-gray-500 sm:text-xs">
                  {row.term}
                </dt>
                <dd className="text-[13px] leading-[1.85] font-medium text-gray-900 sm:text-sm">
                  {row.items ? (
                    <ul className="space-y-1.5">
                      {row.items.map((item) => (
                        <li key={item} className="flex items-start gap-2.5">
                          <span
                            aria-hidden="true"
                            className="mt-[0.85em] h-px w-3 flex-none bg-amber-600"
                          />
                          <span>{item}</span>
                        </li>
                      ))}
                    </ul>
                  ) : (
                    row.value
                  )}
                </dd>
              </div>
            ))}
          </dl>
        </div>

        {/* 右: アクセス(CSS地図 + 住所 + 交通) */}
        <div className="lg:col-span-5 lg:pl-10">
          <h3 className="flex items-center gap-2.5 text-sm font-bold tracking-[0.08em] text-gray-900">
            <span aria-hidden="true" className="h-px w-5 flex-none bg-amber-600" />
            アクセス
          </h3>

          {/* 地図プレースホルダ(画像ゼロ・装飾なので aria-hidden) */}
          <div
            aria-hidden="true"
            className="relative mt-5 aspect-[4/3] w-full overflow-hidden rounded-lg border border-gray-200 bg-[#faf9f7]"
          >
            {cityBlocks.map((b, i) => (
              <span
                key={i}
                className="absolute rounded-[2px] bg-gray-200/80"
                style={{
                  left: `${b.left}%`,
                  top: `${b.top}%`,
                  width: `${b.w}%`,
                  height: `${b.h}%`,
                }}
              />
            ))}
            {/* 道路(白い帯) */}
            <span className="absolute top-[30%] left-0 h-[7%] w-full bg-white" />
            <span className="absolute top-0 left-[34%] h-full w-[6%] bg-white" />
            <span className="absolute top-[80%] -left-[10%] h-[5%] w-[130%] -rotate-[9deg] bg-white" />
            {/* 線路(破線) */}
            <span className="absolute top-[14%] left-0 h-px w-full bg-[repeating-linear-gradient(to_right,#c9c4bd_0_8px,transparent_8px_15px)]" />
            {/* 駅マーク */}
            <span className="absolute top-[14%] left-[34%] size-2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-gray-400 bg-white" />
            {/* 現在地ピン(transform のみで呼吸させる) */}
            <span className="absolute top-[52%] left-[58%] flex size-3 -translate-x-1/2 -translate-y-1/2 items-center justify-center">
              <span className="absolute size-3 rounded-full bg-amber-500/45 animate-[ca-pulse_2.6s_ease-out_infinite] motion-reduce:animate-none" />
              <span className="relative size-3 rounded-full bg-amber-600 ring-2 ring-white" />
            </span>
            <span
              className={`${montserrat.className} absolute top-2.5 left-3 text-[9px] font-semibold tracking-[0.22em] text-gray-400 uppercase`}
            >
              Map
            </span>
          </div>

          {/* 住所 */}
          <p className="mt-5 text-[13px] leading-[1.9] font-medium text-gray-900 sm:text-sm">
            <span className="block whitespace-nowrap text-gray-500">〒150-0001</span>
            東京都渋谷区神宮前0-0-0
            <br />
            First CHビル 5F
          </p>

          {/* 交通(hairline の行) */}
          <dl className="mt-5 divide-y divide-gray-100 border-t border-gray-100">
            {routes.map((r) => (
              <div key={r.mode} className="grid grid-cols-[3.25rem_1fr] gap-x-3 py-3">
                <dt className="pt-[0.25em] text-[11px] font-bold tracking-[0.12em] whitespace-nowrap text-gray-500">
                  {r.mode}
                </dt>
                <dd className="text-[13px] leading-[1.8] text-gray-700">
                  {r.place}
                  <span className="whitespace-nowrap">{r.tail}</span>
                </dd>
              </div>
            ))}
          </dl>

          <div className="mt-6">
            <a
              href="#"
              className="group inline-flex items-center gap-2 text-[13px] font-semibold text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
            >
              <span className="relative">
                Googleマップで見る
                <span className="absolute -bottom-1 left-0 h-px w-full origin-left scale-x-100 bg-gray-900 transition-transform duration-300 ease-out group-hover:scale-x-0 motion-reduce:transition-none" />
                <span className="absolute -bottom-1 left-0 h-px w-full origin-right scale-x-0 bg-amber-600 transition-transform delay-150 duration-300 ease-out group-hover:origin-left group-hover:scale-x-100 motion-reduce:transition-none" />
              </span>
              <span
                aria-hidden="true"
                className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
              >
                →
              </span>
            </a>
          </div>
        </div>
      </div>

      {/* 営業時間(診療時間表の行列を転用) */}
      <div
        className={`mt-10 border-t border-b border-gray-200 py-10 ${reveal}`}
        style={{ transitionDelay: "340ms" }}
      >
        <div className="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-3">
          <h3 className="flex items-center gap-2.5 text-sm font-bold tracking-[0.08em] text-gray-900">
            <span aria-hidden="true" className="h-px w-5 flex-none bg-amber-600" />
            営業時間
          </h3>
          <ul
            aria-hidden="true"
            className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-gray-500"
          >
            {(Object.keys(markMeta) as Mark[]).map((m) => (
              <li key={m} className="flex items-center gap-1.5 whitespace-nowrap">
                <span className={`text-xs ${markMeta[m].className}`}>
                  {markMeta[m].glyph}
                </span>
                {markMeta[m].label}
              </li>
            ))}
          </ul>
        </div>

        <table className="mt-5 w-full table-fixed border-collapse text-center">
          <caption className="sr-only">
            曜日ごとの営業状況(ご来社・オンライン相談)
          </caption>
          <thead>
            <tr className="border-b border-gray-200">
              <th
                scope="col"
                className="w-[4.5rem] pb-2.5 text-left text-[11px] font-bold tracking-[0.12em] whitespace-nowrap text-gray-500 sm:w-32 sm:text-xs"
              >
                区分
              </th>
              {days.map((d) => (
                <th
                  key={d}
                  scope="col"
                  className={`pb-2.5 text-[11px] font-bold sm:text-xs ${
                    d === "日"
                      ? "text-amber-700"
                      : d === "土"
                        ? "text-gray-500"
                        : "text-gray-700"
                  }`}
                >
                  {d}
                </th>
              ))}
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-100">
            {hours.map((row) => (
              <tr key={row.scope}>
                <th
                  scope="row"
                  className="py-3.5 text-left text-[12px] font-semibold whitespace-nowrap text-gray-900 sm:text-sm"
                >
                  {row.scope}
                </th>
                {row.marks.map((m, i) => (
                  <td key={days[i]} className="py-3.5">
                    <span className="sr-only">{`${days[i]}曜日 ${markMeta[m].label}`}</span>
                    <span
                      aria-hidden="true"
                      className={`text-sm leading-none sm:text-base ${markMeta[m].className}`}
                    >
                      {markMeta[m].glyph}
                    </span>
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>

        <p className="mt-4 text-[12px] leading-[1.9] text-gray-500">
          営業時間 10:00–19:00(土曜は 10:00–15:00)/
          <span className="whitespace-nowrap">祝日・年末年始は休業</span>
          。土曜のご来社は前日までのご予約制です。
        </p>
      </div>

      {/* ピンの呼吸(このブロック内で完結) */}
      <style>{`
        @keyframes ca-pulse {
          0%   { transform: scale(1);   opacity: 0.55; }
          70%  { transform: scale(3.4); opacity: 0; }
          100% { transform: scale(3.4); opacity: 0; }
        }
      `}</style>
    </section>
  );
}

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

npx shadcn@latest add https://designs.first-ch.com/r/company-access.json