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