import React from 'react'; import type { LucideIcon } from 'lucide-react'; /** * The one empty state. An empty screen is an INVITATION TO ACT, not a status report. * * WHY THIS EXISTS (, finding B58) * * There was no shared component, so every surface invented one - or didn't: * - **Patient portal** - a designed state: centred icon, bold heading, an explanatory line, a next step. * - **Receptionist** - a thin-bordered grey box holding one line of muted text. Measured: **it reads * as a disabled text input**, not as an empty list. * The patient version already won that argument; this is it, generalised. **The decision was already * made somewhere in the codebase**, which is what makes this cheap. * * Its type and spacing are the shared ak-empty rules in index.css, so nothing here sets a size or colour. * * **This owns PRESENTATION only.** Whether "no results" is even TRUE - i.e. whether the server was ever * asked - is question, and a prettier false statement is worse than an ugly one. */ export interface EmptyStateProps { /** A lucide icon component. Optional: some contexts are too small to carry one. */ icon?: LucideIcon; /** What is not here. Stated plainly - "No upcoming appointments", not "Nothing to display". */ title: string; /** * THE NEXT STEP, and it must be reachable FROM HERE. "Switch to X" is only ever right when X is * unambiguously visible from this screen - B26 was two nested tabs sharing one name, so * the instruction named a control the reader could not identify. Naming the ACTION survives a rename. */ description?: React.ReactNode; /** An optional control - a button or link. The invitation, when there is a single obvious one. */ action?: React.ReactNode; /** * `compact` is NOT a style preference - it is what stops this component fighting a settled design. * * The receptionist overview carries **seven** empty-able panels on one screen, and its ruling is that * the page must never scroll. Seven full-size states - each an icon, a heading and a line, with 8-unit * padding - would blow that on an empty morning, which is precisely when a receptionist is looking. * Seven icons on one board is also noise rather than information. * * Compact keeps the SEMANTICS (a real empty state, centred, quiet, not a bordered box masquerading * as a disabled input) and drops the vertical cost. Same component, so the two cannot drift. */ size?: 'default' | 'compact'; className?: string; 'data-testid'?: string; } export const EmptyState: React.FC = ({ icon: Icon, title, description, action, size = 'default', className = '', 'data-testid': testId, }) => { const compact = size === 'compact'; return (
{Icon && ( // `aria-hidden` is deliberately NOT restated here - lucide already sets it on any icon // with no accessible name. It WAS restated, and a mutation run exposed the cost: removing // it changed nothing, so the test asserting it was vacuous while looking like coverage. // A redundant line that reads as load-bearing is worse than no line. )} {/* A

, not an

— card 240 (B10): an empty state put "what you do not have" into the document outline while the section it belonged to had no heading at all. */}

{title}

{description && (

{description}

)} {action &&
{action}
}

); }; export default EmptyState;