📦

🩺看護師が医療現場で使う計算ツールの開発に挑戦してみた #6:①ResultBox共通化とUI統一

に公開

💡 はじめに

現役看護師として働きながら、Next.js × TypeScript × Tailwind CSS を学習し、
そのアウトプットとして「看護師向け計算ツールアプリ」を開発しています。

このツールは、投薬量・点滴速度・体液バランスなど
医療現場でよく使う計算をまとめた Web アプリです。

現場で使える実用性と、開発者としての再利用性・型安全性の両立を目指し、 各機能をステップごとに公開しています。

🩺 今回のテーマ:ResultBox共通化とUI統一

前回までは、ロジックや型安全性の統一 に重点を置いていました。
今回からは UI・UX改善フェーズ に入り、
各計算結果の表示コンポーネントを統一することに挑戦しました。

🚨 課題

初期段階ではページごとにUI構成がバラバラで、次のような問題がありました。

  • 結果の表示位置やスタイルが異なる
  • 注意書きのフォーマットが統一されていない
  • スマホでは結果が画面外になり見づらい

🧭 解決の方向性

  • ResultBox.tsx を共通コンポーネント化
  • 注意・学習文を展開できる Disclosure(@headlessui/react) を採用
  • スムーススクロールを導入し、モバイルでも快適に操作可能に

⚙️ 実装構成

app/_components/
├─ ResultBox.tsx ← 計算結果表示(Disclosure組み込み)
├─ HelpBox.tsx ← 注意・説明文用(必要に応じて独立)
└─ scrollToRef.ts ← スムーススクロール共通関数

🧩 ResultBox.tsx(抜粋)

"use client";

import React, { useRef, useEffect, useState } from "react";
import { Disclosure } from "@headlessui/react";
import Image from "next/image";
import { helpTexts } from "@/config/helpTexts";

export const ResultBox = ({ title, results, color = "blue", typeId }: Props) => {
  const bg = {
    blue: "bg-blue-50 border-blue-200 text-blue-800",
    yellow: "bg-yellow-50 border-yellow-200 text-yellow-800",
  }[color];

  const panelRef = useRef<HTMLDivElement>(null);
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    if (isOpen && panelRef.current) {
      panelRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
    }
  }, [isOpen]);

  return (
    <Disclosure>
      {({ open }) => {
        useEffect(() => setIsOpen(open), [open]);

        return (
          <div className={`border rounded p-4 ${bg}`}>
            <div className="flex items-start justify-between">
              <h3 className="font-semibold">{title}</h3>
              {helpText && (
                <Disclosure.Button aria-label="注意説明">
                  <Image src="/icons/help-icon.svg" alt="注意" width={28} height={28} />
                </Disclosure.Button>
              )}
            </div>

            <ul className="space-y-2 mt-2">
              {results.map((r, i) => (
                <li key={i} className="flex items-baseline gap-2">
                  <span className="text-base">{r.label}:</span>
                  <strong className="text-2xl">{r.value}</strong>
                  <span className="text-sm">{r.unit}</span>
                </li>
              ))}
            </ul>

            {helpText && (
              <Disclosure.Panel ref={panelRef} className={`mt-3 w-full p-3 ${bg} rounded text-sm`}>
              </Disclosure.Panel>
            )}
          </div>
        );
      }}
    </Disclosure>
  );
};

💡 Disclosureとは?

@headlessui/react に含まれるUIコンポーネントで、
アコーディオン構造を安全かつ最小構成で実現できます。

特徴 内容
状態管理(open) 内部で自動化
ARIA属性 自動付与でアクセシビリティ対応
拡張性 任意のHTML要素で利用可能
<Disclosure>
  <Disclosure.Button>開く/閉じる</Disclosure.Button>
  <Disclosure.Panel>展開内容</Disclosure.Panel>
</Disclosure>

✨ スムーススクロールを共通化

scrollToRef.ts に関数を作成して各ページから共通利用。


export const scrollToRef = (ref: React.RefObject<HTMLElement>) => {
  if (ref.current) {
    ref.current.scrollIntoView({ behavior: "smooth", block: "start" });
  }
};

→ scrollToRef(resultRef) のように呼び出すだけで
開閉時や計算実行時に自動スクロールが可能。

🎨 UIデザインの工夫

要素 Tailwind設定 目的
数値 text-2xl 結果の視認性UP
単位 text-sm メインとの差別化
説明文 text-sm bg-〇〇-50 柔らかい背景で読みやすく
アイコン w-[28px] h-[28px] cursor-pointer モバイルでも押しやすい

🧩 Popover vs Disclosure:どちらを使うべき?

以前は注意文を Popover(吹き出し) で表示していましたが、
実際に現場で使うと「長文が見づらい」「誤タップで消える」などの課題がありました。

項目 Popover(吹き出し) Disclosure(展開型)
表示位置 浮く(上に重なる) 下に展開
表示速度 即時 スクロール連動
適した内容 短文・単位補足 長文・学習説明
UX印象 軽快 安心・安定
看護現場例 「mEqとmmolの違い」 「K補正の上限値・投与判断」

→ 学習・教育的な内容には Disclosure、
単位や補足には Popover が最適かなと考えました。

🧠 学びのまとめ

  • Headless UI の Disclosure は状態管理+ARIA対応済みで安全
  • useRef+scrollIntoView でモバイルUXを大幅改善
  • カラーパレット統一で一貫した見た目を維持
  • 「伝えるUI」と「見せるUI」を使い分ける設計が重要

💬 まとめ

ResultBox の共通化により、
全ての計算ページで統一されたUIを再利用可能になりました。

🚀 次回

スムーススクロールで操作感を高める ― React+TailwindでUX改善!
まだまだ改善していきます!!

Discussion