import{type PropertyValues,type TemplateResult}from'lit';import{LyraElement}from'../../../internal/lyra-element.js';export type ChatViewportLive='off'|'polite'|'assertive';export interface LyraChatViewportEventMap{'lr-follow-change':CustomEvent<{following:boolean;}>;} /** * `` — the transcript scroll container: owns stick-to-bottom behavior while an * answer streams, the "jump to latest" pill, and the unread divider. * * **Two supported content shapes, auto-detected:** ordinary element children (typically * ``s -- *slotted mode*), or exactly one `` (*virtual mode*, * detected via `instanceof` against the imported class so custom prefixes keep working). In virtual * mode this component defers all scrolling to the slotted list's own `scrollToIndex()`, and sizes * that list to its own height -- without which the list would scroll inside `lr-virtual-list`'s * 24rem `--lr-virtual-list-height` default no matter how tall this viewport is. That sizing is a * percentage, so virtual mode needs a height-bounded parent, the same requirement slotted mode's * own scroll container already has; a consumer's own rule or inline style setting * `--lr-virtual-list-height` on the list still wins. * * **Follow/release state machine.** While `follow` is engaged, content growth re-scrolls to the end. * Release happens only on a *user-intent* gesture (wheel, touchmove, scrollbar-drag, or * PageUp/ArrowUp/Home while the log region has focus) that leaves the view more than * `bottomThreshold` from the end -- a scroll caused by this component's own programmatic scrolling, * or by a layout shift, never releases it. Reaching the bottom again by any means re-engages `follow`. * The internal log defaults to `live="off"`, which avoids announcing every streaming token. Consumers * that append complete messages at an announcement-safe cadence can opt into `polite` or `assertive`; * each newly appended direct child's accessibility-exposed text is then announced through the * document's shared light-DOM sink. Hidden, inert, `aria-hidden`, and CSS-hidden content is omitted. * Existing declarative children stay silent on mount, and appending the same text again creates a * new announcement. The shadow log itself always remains `aria-live="off"`. * * **`scrollToUnread()` in virtual mode.** The target row is scrolled with `align: 'start'` so the * divider boundary lands at the top of the view with the unread content visible below it -- the * only alignment that matches what "scroll to the divider" means here. The underlying virtual list * only issues a corrective re-scroll for an initially-unmeasured target when that target's *own* * height is what was uncertain (`align: 'end'`/a downward `align: 'auto'`); a `'start'`-aligned * target's position is a function of the rows *before* it, so it has no such self-correction. In * practice the rows leading up to an unread boundary are usually ones the reader already scrolled * past (and so already measured), which keeps this accurate in the common case; a still-unmeasured * long-distance jump can land approximately rather than pixel-exact. Fixing that fully would mean * changing how the underlying list resolves offsets, which is out of scope here -- `align: 'end'` * was considered and rejected because it changes the visible outcome (it would put the *bottom* of * the boundary row at the viewport's bottom edge, hiding the unread content the jump is meant to * reveal, not just changing how precisely it lands). * * Renders no messages and computes no unread state itself -- the host supplies `unreadStartIndex`; * no virtualization of its own (``); not a generic overflow surface * (``); no message semantics (``). * * @customElement lr-chat-viewport * @slot - The transcript: ordinary element children, or exactly one ``. * @event lr-follow-change - `detail: { following }` -- fired whenever `follow` flips (user * scroll-up release, or reaching the bottom again). Never fired for the initial mount state. * @csspart base - The positioning root. * @csspart scroll - The scroll container (`role="log"` and, in slotted mode, `tabindex="0"`). In * virtual mode it stops scrolling itself and drops its tab stop because the slotted list owns * both scrolling and keyboard focus. * @csspart content - The slotted-content wrapper the growth observers watch. * @csspart jump-pill - The built-in jump-to-latest button, absent while `follow` is engaged. * @csspart unread-divider - The "New messages" separator (slotted mode only). * @status stable * @since 4.0.0 */ export declare class LyraChatViewport extends LyraElement{static styles:import("lit").CSSResultGroup[];follow:boolean; /** Live-region policy for newly appended direct children. Keep `off` for token-by-token * streaming; use `polite` or `assertive` only when messages are appended at an announcement-safe * cadence. Only accessibility-exposed text is copied. The shadow `role="log"` remains non-live; * announcements use a shared light-DOM sink. */ live:ChatViewportLive; /** Px distance from the end still counted as "at bottom." */ bottomThreshold:number; /** Index of the first unread item -- element-child index in slotted mode, `items` index in virtual * mode. Host-owned unread bookkeeping in, divider/pill count out. `null` disables both. */ unreadStartIndex:number|null; /** Accessible name for the log region. Defaults to the localized `chatViewportLabel`; * a host `aria-label` (see `accessibleLabel`) wins over both. */ label:string; /** Host `aria-label`, forwarded to the internal `role="log"` element -- an `aria-label` left * on the custom-element host itself names nothing, because the log role lives inside the * shadow root. Wins over `label` and the localized default. */ accessibleLabel:string|null;private unreadDividerTop;private scrollEl?;private contentEl?;private contentSlot?;private pendingUserIntent; /** Owner-bound in-flight proactive expiry scheduled by `markUserIntent()` -- see that method and * `cancelPendingUserIntentExpiry()`. */ private pendingUserIntentExpiryFrame;private scrollbarDragActive;private scrollbarDragWindow?;private isMounting;private pendingScrollBehavior?;private contentResizeObserver?;private contentMutationObserver?;private announcementSink?;private scrollResizeObserver?;private growthFrame;private listenedVirtualList?;private observerWindow?; /** Which shape `armObservers()` last actually built watchers for -- `null` after a teardown. * Guards against rebuilding on a redundant `armObservers()` call (see its own comment). */ private armedMode; /** Bumped on every real (non-skipped) `armObservers()` call and on teardown -- invalidates a * still-pending deferred initial-measurement microtask from a now-stale arm. */ private armGeneration;private followFocusRepair;private readonly knownProjectedNodes;private unreadBoundaryEl?;private get messageElements();private get virtualListEl(); /** `bottomThreshold` normalized to a finite, non-negative pixel distance -- a non-finite value * would otherwise make the `distanceFromEnd <= bottomThreshold` comparison in `onScroll()` * always false (a `NaN` comparison never succeeds), permanently preventing `follow` from * re-engaging once released. */ private get effectiveBottomThreshold(); /** `unreadStartIndex` normalized to a finite, non-negative integer -- `null` (the documented * "disabled" sentinel) is passed through as-is, never coerced into a number. */ private get effectiveUnreadStartIndex();connectedCallback():void;disconnectedCallback():void;protected willUpdate(changed:PropertyValues):void;firstUpdated(changed:PropertyValues):void;protected updated(changed:PropertyValues):void;private transcriptFocusOwner; /** Scrolls to the end and re-engages `follow`. Default `smooth`, forced to `auto` under * `prefers-reduced-motion`. */ scrollToBottom(options?:{behavior?:'auto'|'smooth';}):void; /** Scrolls the unread divider to the top of the view; `false` when `unreadStartIndex` is * `null`/out of range. Does not re-engage `follow`. See the class doc for why virtual mode uses * `align: 'start'` and what that trades off. */ scrollToUnread(options?:{behavior?:'auto'|'smooth';}):boolean;private performScrollToEnd;private get totalCount();private get unreadCount();private pillLabel;private updateUnreadDividerPosition;private syncSemanticUnreadBoundary;private markUserIntent;private cancelPendingUserIntentExpiry; /** Clears the pending user-intent flag and cancels its proactive expiry, if one is still * scheduled. */ private clearUserIntent; /** Consumes the pending user-intent flag, returning whatever it held. Always clears it (and its * proactive expiry, if still pending). */ private consumeUserIntent;private onPointerDown;private onPointerUp;private releaseScrollbarDragListeners;private onKeyDown;private onScroll;private onVirtualRangeChanged;private onSlotChange;private scheduleGrowthTick;private releaseAnnouncementSink;private syncAnnouncementSink; /** * A projected node's announcement text. `composedAccessibilityText()` computes an accessible * *name*: a subtree rooted at an element that names itself via `aria-label` (e.g. * ``'s internal `role="article"` bubble, named after the message author) * correctly stops there per ARIA accname semantics and never descends into its own content -- * exactly right for naming that landmark, but wrong for announcing a new chat message, where the * actual composed (slotted) content is what a listener needs to hear alongside the author label. * For a node with its own shadow root, also walk its light-DOM content directly -- bypassing the * shadow tree's name-bearing landmark entirely -- and append whatever text that surfaces which * the name-only pass didn't already include. Plain elements (no shadow root, e.g. the `
` * and `` cases this same sink announces) are unaffected: `composedAccessibilityText(node)` * already walks their own light DOM directly, so there is nothing separate to add. */ private announcementTextFor;private onContentMutations;private announceNewProjectedNodes;private armObservers;private teardownObservers;render():TemplateResult;}declare global{interface HTMLElementTagNameMap{'lr-chat-viewport':LyraChatViewport;}}