import { type HTMLAttributes, type ReactNode } from 'react';
import type { ClinicalNoteCardProps } from '../clinical-note-card';
import type { RichTextEditorProps, RichTextEditorToolbar, RedactorFactory, AiConsentGate } from '../rich-text-editor';
/**
* One note in the panel — the data subset of `ClinicalNoteCard`.
*
* `aria-label` names the card for assistive tech. Set it whenever a row is not
* a clinical note: the card's fallback name is "Clinical note", so an entry
* that is something else (a dated event, an import) is announced as one.
*/
export type NotesPanelNote = Pick & {
/** What the host lets the reader do with this note. */
actions?: NotesPanelNoteActions;
/**
* The host keeps this note when it is deleted and lists it again marked
* `deleted` (a clinical record, for example). Its delete does not fade
* the row: the row stays in place, its controls paused, until `notes`
* marks it deleted or drops it. Host controls from `noteActions` hear
* `deleting: true` meanwhile, so they can pause too. Leave it unset for
* a note the host removes, or the row fades out and then comes back.
*/
keptOnDelete?: boolean;
};
/** Per-note write permissions; an absent key means allowed. The panel hides
* the pencil on `edit: false`, the pin toggle on `pin: false`, the built-in
* delete on `delete: false`, and the pin, flag, pencil and delete on a
* deleted note. */
export type NotesPanelNoteActions = {
edit?: boolean;
delete?: boolean;
pin?: boolean;
};
/**
* Curated agent handle for NotesPanel. Reads the note timeline's STRUCTURE
* (ids + pinned / flagged / clinical / deleted flags, never the note body
* or author, both PHI) and drives the composer + per-note toggles. See
* `notes-panel.agent.ts`.
*/
export interface NotesPanelHandle {
/** Timeline snapshot — ids + flags only, never note content (PHI). A note
* whose delete is in flight is left out; a pin or flag in flight reports
* the value it moved to. `deleted` marks a note kept for the record: it
* takes no pin or flag. */
getNotes: () => Array<{
id?: string;
pinned: boolean;
flagged: boolean;
clinical: boolean;
deleted: boolean;
}>;
/** Whether the composer is mounted (onAddNote is wired). */
canAddNote: () => boolean;
/** Whether clinical notes may be written (the permission gate). */
canAddClinical: () => boolean;
/** Id of the note the host has addressed, or null when none is. */
getCurrentNoteId: () => string | null;
/** Commit a new note with explicit content (mirrors the composer's submit). */
addNote: (submission: NotesPanelSubmission) => void | Promise;
/**
* Toggle a note's pinned state: the same path as the pin toggle, with its
* gate. Returns the host's promise, so a refusal rejects (a synchronous
* throw comes back as a handled rejection). Also rejects, without calling
* the host, when the panel refuses: no `onTogglePin`, no note with this
* id, a deleted note, `actions.pin: false`, or a pin or delete for the
* note still pending. `undefined` only when the host returned nothing.
*/
togglePin: (id: string) => void | Promise;
/** As `togglePin`, for the flag. Refused for a deleted note. */
toggleFlag: (id: string) => void | Promise;
}
/** A write the host refused, as `onWriteError` hears it. */
export interface NotesPanelWriteFailure {
/** What did not happen. `unpin` and `unflag` are the toggles turned off;
* `add` and `edit` are the composer's saves. */
kind: 'add' | 'edit' | 'delete' | 'pin' | 'unpin' | 'flag' | 'unflag';
/** The note written to. Absent for `add`: the note never existed. */
noteId?: string;
/** What the handler rejected or threw with. */
error: unknown;
/**
* Whether the panel told the reader. `false` when the answer came after
* the panel unmounted or while it was hidden, inert or aria-hidden (a
* rail panel on its way out, say), after the note left `notes` (for an
* edit, also once it was marked deleted), while the note was being
* deleted, after a newer write on the same note started, or when a new
* write began before the refusal could show (a new write clears the
* message). A refused save the panel did not show has lost its draft
* too. It is decided after the render that follows the refusal, so a
* host that changes `notes` or hides the panel in the same tick as it
* rejects is judged on that change.
*/
shown: boolean;
}
/** Payload handed to `onAddNote` when the composer commits. */
export interface NotesPanelSubmission {
/** The note body — rich HTML (`composer='rich'`) or plain text. */
content: string;
/** Whether the "clinical note" toggle was set. */
clinical: boolean;
}
export interface NotesPanelProps extends Omit, 'title' | 'children'> {
/**
* Stable instance id. Surfaced on the root as `data-component-id` so an
* agent / MCP UI bridge can address this specific notes panel.
*/
id?: string;
/** Notes to render, newest-first by convention. Pinned notes lead. */
notes: NotesPanelNote[];
/**
* Commit a new note. Omit to hide the composer (read-only timeline). Return
* a promise to keep the draft until the host has saved it: the draft clears
* when it resolves, and a rejection keeps it and shows the error's message.
*/
onAddNote?: (submission: NotesPanelSubmission) => void | Promise;
/** The host's notes read. `'loading'` and `'error'` never show the empty
* text; notes already held still render. Default `'ready'`. */
status?: 'ready' | 'loading' | 'error';
/** Retry a failed read. Renders a retry button in the error state. */
onRetry?: () => void;
/**
* The note the reader ASKED for, addressed by id from somewhere else in the
* record — a marker in a chart, a row in a grid, an agent. The panel marks
* it, scrolls it to the middle of its own scroll region and moves focus to
* it, and renders it unclamped so the thing that was asked for is readable
* in full without a second click.
*
* Opening a panel is not the same as answering a question: a rail's
* `setActiveTool` moves no focus and announces nothing, so a control whose
* name promises "in the notes panel" produces no perceivable change for a
* keyboard or screen-reader reader unless the panel lands them on the note.
* That is what this prop is for; without it, a host has to reach into the
* panel's DOM to do the same job.
*
* Marked on three channels, never colour alone (WCAG 1.4.1): an accent
* wash that fades in and out over the card (steady where motion is off),
* the focus outline on the row, and `aria-current` plus a visually-hidden
* sentence naming the mark. Nothing is left on the row once the wash ends.
*
* Ids that match no note are ignored. To address the SAME note twice — the
* reader pressing the same marker again after scrolling away — bump
* `currentNoteKey` alongside it; this prop alone is a value, not a request.
*/
currentNoteId?: string;
/**
* Bump on every address to re-scroll and re-focus the note in `currentNoteId`
* and replay its wash, even when the id has not changed.
*
* Addressing is a request, not a value: a reader who presses the same marker
* a second time (having scrolled away, or having moved focus off the note) is
* asking to be taken back, and a control whose name promises the notes panel
* must do something perceivable every time it is pressed. Without this the
* second press is inert — the panel is already showing that note, so nothing
* re-runs. Hosts increment a counter in the same handler that opens the tool.
*/
currentNoteKey?: string | number;
/**
* `'rich'` (default) mounts the kit `RichTextEditor` (Redactor; degrades to a
* textarea where Redactor is absent). `'plain'` keeps a simple `TextArea` for
* lightweight surfaces / deterministic tests.
*/
composer?: 'rich' | 'plain';
/** Toolbar preset for the rich composer. Defaults to `'standard'`. */
editorToolbar?: RichTextEditorToolbar;
/** Inject the Redactor factory for the rich composer (else `window.$R`). */
redactor?: RedactorFactory;
/** Consumer image-upload endpoint for the rich composer. */
imageUploadUrl?: string;
/** Show the "clinical note" toggle in the composer. Default `true`. */
showClinicalToggle?: boolean;
/**
* Permission gate — when `false` the clinical toggle is disabled and shows
* `clinicalGatedReason` on hover/focus. Mirrors platform's
* `practice_add_clinical_note` permission. Default `true`.
*/
canAddClinicalNote?: boolean;
/** Tooltip shown when the clinical toggle is gated off. */
clinicalGatedReason?: string;
/** Initial state of the clinical toggle. Default `false`. */
defaultClinical?: boolean;
onRequestAiDraft?: RichTextEditorProps['onRequestAiDraft'];
aiDraftFormat?: RichTextEditorProps['aiDraftFormat'];
aiPrivacyNotice?: RichTextEditorProps['aiPrivacyNotice'];
onDictate?: RichTextEditorProps['onDictate'];
onProofread?: RichTextEditorProps['onProofread'];
onAutoFormat?: RichTextEditorProps['onAutoFormat'];
/**
* Patient AI-consent gate for Help-me-write + both mics, forwarded
* straight to the rich composer. See `AiConsentGate`. Omit for today's
* ungated behaviour. Ignored by the `'plain'` composer (it offers
* neither affordance).
*/
aiConsent?: AiConsentGate;
/**
* Template variables for the rich composer's "Insert variable" menu and
* `[[TOKEN]]` chip rendering. Forwarded straight to `RichTextEditor`.
* Ignored by the `'plain'` composer.
*/
variables?: RichTextEditorProps['variables'];
/** Decide whether a `[[TOKEN]]` is valid. See `RichTextEditor`. */
validateVariable?: RichTextEditorProps['validateVariable'];
/** The invalid `[[TOKEN]]`s in the draft, reported by the rich composer. */
onVariableValidityChange?: RichTextEditorProps['onVariableValidityChange'];
/**
* Bring your own Help-me-write surface (e.g. practice templates) into the
* composer, forwarded straight to `RichTextEditor`. See
* `RichTextEditorProps['renderHelpMeWrite']`.
*/
renderHelpMeWrite?: RichTextEditorProps['renderHelpMeWrite'];
/** Practice templates in the composer's AI pill. See `RichTextEditor`. */
aiTemplates?: RichTextEditorProps['aiTemplates'];
/** After a pill action rewrote the draft (offer Undo). See `RichTextEditor`. */
onAiApplied?: RichTextEditorProps['onAiApplied'];
/** Upload an image yourself and resolve its URL. See `RichTextEditor`. */
onUploadImage?: RichTextEditorProps['onUploadImage'];
/** An AI call ran out of credits. See `RichTextEditor`. */
onAiCreditsExhausted?: RichTextEditorProps['onAiCreditsExhausted'];
/** Show the editor extensions affordance. See `RichTextEditor`. */
showExtensions?: RichTextEditorProps['showExtensions'];
extensions?: RichTextEditorProps['extensions'];
extensionContext?: RichTextEditorProps['extensionContext'];
onExtensionRun?: RichTextEditorProps['onExtensionRun'];
/**
* Shows the AI-credits/bundles hover card on every AI action trigger in
* the composer. See `RichTextEditor`.
*/
aiCreditsNotice?: RichTextEditorProps['aiCreditsNotice'];
/**
* Rendered just above the composer — e.g. the Help-me-write templates
* pattern's `undoNotice`. Renders in both add and edit mode, whenever the
* composer itself is mounted.
*/
composerBefore?: ReactNode;
/**
* Rendered just after the composer — e.g. the Help-me-write templates
* pattern's `manager` dialog. Renders in both add and edit mode, whenever
* the composer itself is mounted.
*/
composerAfter?: ReactNode;
/**
* Pin or unpin a note; renders a pin toggle per note. Called on the click
* with the value the note moves to. Return a promise and the panel shows
* that value at once (a pinned note moves to the top), ignores further
* clicks on that toggle until the promise settles and `notes` agrees, and
* on a rejection puts the note back and shows the error's message above
* the list. Return nothing and the change is left to your next render, as
* before.
*/
onTogglePin?: (id: string, next: boolean) => void | Promise;
/** As `onTogglePin`, for the flagged state (flagging does not re-order).
* Hidden on a deleted note. */
onToggleFlag?: (id: string, next: boolean) => void | Promise;
/**
* Host-owned actions for each note (share, print…), rendered by the note
* card beside its pin and flag, aligned to the inline end. The host
* supplies labelled controls and owns what they do. Deleting belongs to
* `onDeleteNote` and editing to `onEditNote`, which keep the panel's
* confirm, rollback and focus. `state.deleting` is true while a delete
* for the note is in flight: pause your controls then, as the panel
* pauses its own.
*/
noteActions?: (note: NotesPanelNote, state: {
deleting: boolean;
}) => ReactNode;
/**
* Save an edit to an existing note. When provided, each note the reader may
* edit gets a pencil; it loads the note into the SAME composer that writes
* new notes, in place, so a rich note is edited as rich text and keeps its
* formatting. Return a promise to hold the pending state; a rejection keeps
* the draft. Hidden on `actions.edit: false` and on a deleted note.
*/
onEditNote?: (id: string, submission: NotesPanelSubmission) => void | Promise;
/**
* Let an edit change a note's clinical flag. Default `false`: the "Clinical
* note" toggle is hidden while editing and the submission carries the
* note's existing flag. Most hosts save a note's content only, and a live
* toggle that changes nothing is a lie. Set `true` only when the host
* persists `submission.clinical` on edit.
*/
editClinical?: boolean;
/**
* Delete a note; renders a delete button per note. Called on the click
* that confirms it (on the delete click itself with
* `confirmDelete={false}`), before the row fades, so the write reaches you
* even if the panel unmounts during the fade. The row fades and stays out
* of the list until `notes` drops it or returns it marked `deleted`. A
* note marked `keptOnDelete` does not fade: its row stays in place, its
* controls paused, until then. A reload that finishes (`status` from
* `'loading'` to `'ready'`) and still lists it brings it back. Resolve
* once the delete has landed; a rejection brings the row back and shows
* the error's message. A synchronous return counts as landed, and the
* row stays out: return the request's promise, and don't open your own
* confirm in here (a cancel would still hide the note). Dropping
* the note from `notes` in the same tick skips the fade (the rows below
* still slide up). Hidden on `actions.delete: false` and on a deleted
* note.
*/
onDeleteNote?: (id: string) => void | Promise;
/**
* Hears every refused write: a save from the composer, a delete, a pin or
* a flag, with `shown` saying whether the panel told the reader. Report
* the ones it did not (`shown: false`, for example when the panel
* unmounted first) yourself, in a toast. Leave the rest to the panel, or
* the reader hears it twice.
*/
onWriteError?: (failure: NotesPanelWriteFailure) => void;
/**
* Confirm in an alert dialog before calling `onDeleteNote`. Default
* `true`. `false` deletes on the click.
*/
confirmDelete?: boolean;
/** Panel heading. Defaults to the localised "Notes". */
title?: string;
/** Composer placeholder. */
composerPlaceholder?: string;
/** Save-button label. */
addNoteLabel?: string;
/** Rendered when there are no notes. */
emptyState?: ReactNode;
/** Accessible name for the panel region (defaults to `title`). */
'aria-label'?: string;
}
export declare const NotesPanel: import("react").ForwardRefExoticComponent>;
//# sourceMappingURL=notes-panel.d.ts.map