feat: info icon on all PageHeader pages + Call Desk header restyled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 21:49:00 +05:30
parent dfcaa175ab
commit 313842a922
8 changed files with 64 additions and 12 deletions

View File

@@ -1,29 +1,71 @@
// PageHeader — consistent header layout for all list pages.
//
// Row 1: Title (+ optional badge + subtitle) on the left,
// Row 1: Title (+ optional badge + info icon) on the left,
// controls (search, columns, export, etc.) on the right.
// Row 2: Optional tabs (underline style) — no extra borders.
//
// The `infoText` prop renders as a hoverable info icon (ⓘ) next to
// the title. Long descriptive text goes here instead of inline
// subtitle — keeps the header compact.
//
// Usage:
// <PageHeader
// title="Appointments"
// badge={37}
// subtitle="Manage appointments"
// title="Contacts"
// badge={16}
// infoText="People who reached out directly — phone, walk-in, referral."
// controls={<><Input .../> <Button .../></>}
// tabs={<Tabs ...><TabList ...>{...}</TabList></Tabs>}
// />
import type { ReactNode } from 'react';
import { useState, useRef, useEffect, type ReactNode } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
interface PageHeaderProps {
title: string;
badge?: number | string;
/** Short inline text next to badge — use sparingly (e.g. "17 total") */
subtitle?: string;
/** Longer descriptive text shown on info icon hover/click */
infoText?: string;
controls?: ReactNode;
tabs?: ReactNode;
}
export const PageHeader = ({ title, badge, subtitle, controls, tabs }: PageHeaderProps) => (
const InfoTooltip = ({ text }: { text: string }) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen(!open)}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
className="flex size-5 items-center justify-center rounded-full text-fg-quaternary hover:text-fg-secondary transition duration-100 ease-linear"
title={text}
>
<FontAwesomeIcon icon={faCircleInfo} className="size-4" />
</button>
{open && (
<div className="absolute left-0 top-full mt-1 z-50 w-72 rounded-lg bg-primary px-3 py-2 text-xs text-tertiary shadow-lg ring-1 ring-secondary">
{text}
</div>
)}
</div>
);
};
export const PageHeader = ({ title, badge, subtitle, infoText, controls, tabs }: PageHeaderProps) => (
<div className="shrink-0">
{/* Row 1: title + controls */}
<div className="flex items-center justify-between px-6 py-3">
@@ -37,6 +79,7 @@ export const PageHeader = ({ title, badge, subtitle, controls, tabs }: PageHeade
{subtitle && (
<span className="text-sm text-tertiary ml-1">{subtitle}</span>
)}
{infoText && <InfoTooltip text={infoText} />}
</div>
{controls && (
<div className="flex items-center gap-2">