PatientCard — Full Example
apps/docs/src/content/docs/extending/patient-card Click to copy Copied!
apps/docs/src/content/docs/extending/patient-card PatientCard — Full Example
Section titled “PatientCard — Full Example”The complete source for the PatientCard component from the Extending HELiX Components tutorial.
Copy this into your project as patient-card.ts and adjust the import path for @helixui/library to match your setup.
/** * patient-card.ts — Example: Extending HelixCard for a clinical context * * Key patterns: * - static styles = [...ParentClass.styles, css`...`] * - override render() calling super.render() * - @property reactive properties with reflect: true * - Custom event dispatch with typed detail interface * - HTMLElementTagNameMap TypeScript augmentation * - JSDoc CEM annotations (@element, @slot, @event, @cssprop) */
import { html, css, type TemplateResult, type PropertyValues } from 'lit';import { customElement, property } from 'lit/decorators.js';import { HelixCard } from '@helixui/library';
// ─── Event Detail Interface ───────────────────────────────────────────────────
export interface PatientCardStatusChangeDetail { previousStatus: PatientCard['status']; status: PatientCard['status']; mrn: string | undefined;}
// ─── Component ───────────────────────────────────────────────────────────────
/** * A patient-context card that extends HelixCard with clinical status * indicators, severity banners, and a domain-specific status-change event. * * @element org-patient-card * * @summary Displays patient information with a live status badge and optional * severity banner. Inherits all slots, CSS parts, and design token support * from HelixCard. * * @slot image - Patient photo or avatar media. * @slot heading - Patient name. Use a semantic heading element (h2, h3, etc.). * @slot - Default slot for patient details and clinical notes. * @slot footer - Timestamps, last-updated metadata, status badge area. * @slot actions - Action buttons (schedule, message, view chart). * * @fires {CustomEvent<PatientCardStatusChangeDetail>} org-status-change - * Fired when the status property changes. bubbles: true, composed: true. * * @csspart card - Inherited: the outer card container element. * @csspart image - Inherited: the image slot container. * @csspart heading - Inherited: the heading slot container. * @csspart body - Inherited: the body slot container. * @csspart footer - Inherited: the footer slot container. * @csspart actions - Inherited: the actions slot container. * * @cssprop [--org-patient-card-status-color=var(--hx-color-neutral-500)] * Color of the status badge text and background tint. * * @cssprop [--org-patient-card-severity-bg=var(--hx-color-warning-100,#fef3c7)] * Background color of the high-severity banner. */@customElement('org-patient-card')export class PatientCard extends HelixCard { // ─── Styles ────────────────────────────────────────────────────────────────
static override styles = [ ...HelixCard.styles, css` :host([status='critical']) { --org-patient-card-status-color: var(--hx-color-error-600, #dc2626); } :host([status='stable']) { --org-patient-card-status-color: var(--hx-color-success-600, #16a34a); } :host([status='monitoring']) { --org-patient-card-status-color: var(--hx-color-warning-600, #d97706); } :host([status='discharged']) { --org-patient-card-status-color: var(--hx-color-neutral-400, #9ca3af); }
.patient-status-badge { display: inline-flex; align-items: center; gap: var(--hx-space-1, 0.25rem); padding: var(--hx-space-1, 0.25rem) var(--hx-space-2, 0.5rem); border-radius: var(--hx-border-radius-full, 9999px); font-size: var(--hx-font-size-xs, 0.75rem); font-weight: var(--hx-font-weight-semibold, 600); letter-spacing: var(--hx-letter-spacing-wide, 0.025em); text-transform: uppercase; color: var(--org-patient-card-status-color, var(--hx-color-neutral-500, #6b7280)); background-color: color-mix( in srgb, var(--org-patient-card-status-color, var(--hx-color-neutral-500, #6b7280)) 12%, transparent ); }
.patient-severity-banner { padding: var(--hx-space-2, 0.5rem) var(--hx-card-padding, var(--hx-space-6, 1.5rem)); background-color: var(--org-patient-card-severity-bg, var(--hx-color-warning-100, #fef3c7)); color: var(--hx-color-warning-800, #92400e); font-size: var(--hx-font-size-sm, 0.875rem); font-weight: var(--hx-font-weight-medium, 500); border-bottom: var(--hx-border-width-thin, 1px) solid var(--hx-card-border-color, var(--hx-color-neutral-200, #dee2e6)); } `, ];
// ─── Properties ────────────────────────────────────────────────────────────
/** * Clinical status of the patient. * @attr status */ @property({ type: String, reflect: true }) status: 'stable' | 'monitoring' | 'critical' | 'discharged' = 'stable';
/** * Severity level. When 'high', renders a severity banner above the card body. * @attr severity */ @property({ type: String, reflect: true }) severity: 'low' | 'medium' | 'high' = 'low';
/** * Medical Record Number — included in org-status-change event detail. * @attr mrn */ @property({ type: String }) mrn: string | undefined;
// ─── Lifecycle ─────────────────────────────────────────────────────────────
override updated(changedProperties: PropertyValues<this>): void { super.updated(changedProperties);
if (changedProperties.has('status')) { const previousStatus = changedProperties.get('status') as PatientCard['status'];
this.dispatchEvent( new CustomEvent<PatientCardStatusChangeDetail>('org-status-change', { bubbles: true, composed: true, detail: { previousStatus, status: this.status, mrn: this.mrn, }, }), ); } }
// ─── Render ────────────────────────────────────────────────────────────────
override render(): TemplateResult { return html` ${this.severity === 'high' ? html` <div class="patient-severity-banner" role="status" aria-live="polite"> High severity — clinical review required </div> ` : ''} ${super.render()}
<span class="patient-status-badge" aria-label="Status: ${this.status}"> ${this.status} </span> `; }}
// ─── TypeScript Global Type Augmentation ───────────────────────────────────────
declare global { interface HTMLElementTagNameMap { 'org-patient-card': PatientCard; }}Interactive card pattern (whole card navigates — hx-href + hx-label, no actions slot):
<org-patient-card variant="featured" elevation="raised" status="critical" severity="high" mrn="MRN-00123456" hx-label="Navigate to patient James Martin's chart" hx-href="/patients/MRN-00123456"> <img slot="image" src="/photos/patient-thumb.jpg" alt="" /> <h2 slot="heading">James Martin</h2> <p>DOB: 1958-03-11 • Room 412 • Attending: Dr. Okafor</p> <time slot="footer" datetime="2026-03-24T14:22:00Z">Updated 14:22</time></org-patient-card>Or — non-interactive card with action buttons (the destination lives on the button instead):
<org-patient-card variant="featured" elevation="raised" status="critical" severity="high" mrn="MRN-00123456"> <img slot="image" src="/photos/patient-thumb.jpg" alt="" /> <h2 slot="heading">James Martin</h2> <p>DOB: 1958-03-11 • Room 412 • Attending: Dr. Okafor</p> <time slot="footer" datetime="2026-03-24T14:22:00Z">Updated 14:22</time> <hx-button slot="actions" variant="primary" hx-size="sm" href="/patients/MRN-00123456"> View Chart </hx-button></org-patient-card>HelixCard flags interactive-card-plus-actions-slot as an ARIA anti-pattern; pick one pattern per card instance.
document.querySelector('org-patient-card').addEventListener('org-status-change', (e) => { const { status, previousStatus, mrn } = e.detail; auditLog.record({ mrn, from: previousStatus, to: status, at: Date.now() });});Return to the tutorial for a walkthrough of each pattern.