diff --git a/src/client/components/FilterPanel.tsx b/src/client/components/FilterPanel.tsx index e3877cb..d793299 100644 --- a/src/client/components/FilterPanel.tsx +++ b/src/client/components/FilterPanel.tsx @@ -2,15 +2,17 @@ import React, { useState } from "react"; import type { MessageCategory } from "../lib/types"; import { ALL_CATEGORIES, CATEGORY_LABELS } from "../lib/types"; import { CATEGORY_COLORS } from "../lib/constants"; +import { Tooltip } from "./Tooltip"; interface Props { enabledCategories: Set; onToggle: (cat: MessageCategory) => void; autoRedactEnabled: boolean; onAutoRedactToggle: (enabled: boolean) => void; + sensitiveCount: number; } -export function FilterPanel({ enabledCategories, onToggle, autoRedactEnabled, onAutoRedactToggle }: Props) { +export function FilterPanel({ enabledCategories, onToggle, autoRedactEnabled, onAutoRedactToggle, sensitiveCount }: Props) { const [collapsed, setCollapsed] = useState(false); const enabledCount = enabledCategories.size; @@ -69,18 +71,29 @@ export function FilterPanel({ enabledCategories, onToggle, autoRedactEnabled, on
- + + +
)} diff --git a/src/client/components/Tooltip.tsx b/src/client/components/Tooltip.tsx new file mode 100644 index 0000000..10cc380 --- /dev/null +++ b/src/client/components/Tooltip.tsx @@ -0,0 +1,81 @@ +import React, { useState, useRef, useCallback, useEffect } from "react"; + +interface Props { + content: React.ReactNode; + children: React.ReactElement; + delayMs?: number; + side?: "top" | "bottom"; +} + +export function Tooltip({ content, children, delayMs = 150, side = "top" }: Props) { + const [visible, setVisible] = useState(false); + const [position, setPosition] = useState<{ x: number; y: number } | null>(null); + const triggerRef = useRef(null); + const tooltipRef = useRef(null); + const timerRef = useRef>(); + + const show = useCallback(() => { + timerRef.current = setTimeout(() => setVisible(true), delayMs); + }, [delayMs]); + + const hide = useCallback(() => { + clearTimeout(timerRef.current); + setVisible(false); + }, []); + + // Clean up timer on unmount + useEffect(() => () => clearTimeout(timerRef.current), []); + + // Recompute position when visible + useEffect(() => { + if (!visible || !triggerRef.current) return; + const rect = triggerRef.current.getBoundingClientRect(); + setPosition({ + x: rect.left + rect.width / 2, + y: side === "top" ? rect.top : rect.bottom, + }); + }, [visible, side]); + + // Nudge tooltip horizontally if it overflows the viewport + useEffect(() => { + if (!visible || !tooltipRef.current || !position) return; + const el = tooltipRef.current; + const tooltipRect = el.getBoundingClientRect(); + const pad = 8; + if (tooltipRect.left < pad) { + el.style.transform = `translateX(${pad - tooltipRect.left}px)`; + } else if (tooltipRect.right > window.innerWidth - pad) { + el.style.transform = `translateX(${window.innerWidth - pad - tooltipRect.right}px)`; + } else { + el.style.transform = ""; + } + }, [visible, position]); + + return ( + <> + {React.cloneElement(children, { + ref: triggerRef, + onMouseEnter: show, + onMouseLeave: hide, + onFocus: show, + onBlur: hide, + })} + {visible && position && ( +
+ {content} +
+ )} + + ); +} diff --git a/src/client/styles/main.css b/src/client/styles/main.css index 7a04c9f..3b4cf4c 100644 --- a/src/client/styles/main.css +++ b/src/client/styles/main.css @@ -552,3 +552,43 @@ mark.search-highlight { @apply focus-visible:ring-red-500; } } + +/* ═══════════════════════════════════════════════ + Tooltip + ═══════════════════════════════════════════════ */ + +.tooltip-popup { + pointer-events: none; + max-width: 280px; + padding: 0.375rem 0.625rem; + font-size: 0.75rem; + line-height: 1.45; + color: var(--color-foreground); + background: var(--color-surface-overlay); + border: 1px solid var(--color-border); + border-radius: 0.375rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.1); + animation: tooltip-in 120ms cubic-bezier(0.16, 1, 0.3, 1); + white-space: normal; +} + +.tooltip-popup[data-side="top"] { + transform-origin: bottom center; + translate: -50% -100%; +} + +.tooltip-popup[data-side="bottom"] { + transform-origin: top center; + translate: -50% 0; +} + +@keyframes tooltip-in { + from { + opacity: 0; + scale: 0.96; + } + to { + opacity: 1; + scale: 1; + } +}