Component · Container

PromptInput

Container for AI prompt entry. Owns the state machine (ready → submitted → streaming → error), keyboard shortcuts, attachment validation, and coordinates all subcomponents.

Preview

Anatomy

text
<prompt-input>
├── <prompt-input-attachments>           (optional, see Attachments docs)
│   └── <prompt-input-attachment>
├── <prompt-input-textarea>              (required)
└── <prompt-input-toolbar>               (optional but recommended)
    ├── <prompt-input-tools>
    │   ├── <prompt-input-action-menu>
    │   ├── <ai-button>
    │   └── <prompt-input-model-select>
    └── <prompt-input-submit>            (one per toolbar)

Each subcomponent has its own reference page:

Installation

bash
npm install @shadng/prompt-input

Basic usage

typescript
import { Component, signal } from '@angular/core';
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputSubmitEvent,
  PromptInputTextarea,
  PromptInputToolbar,
} from '@shadng/prompt-input';

@Component({
  selector: 'app-chat',
  imports: [PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputSubmit],
  template: `
    <prompt-input
      [(value)]="message"
      [state]="status()"
      (submitted)="onSubmit($event)"
    >
      <prompt-input-textarea placeholder="Ask anything…" />
      <prompt-input-toolbar>
        <prompt-input-submit />
      </prompt-input-toolbar>
    </prompt-input>
  `,
})
export class ChatComponent {
  message = signal('');
  status = signal<'ready' | 'submitted' | 'streaming' | 'error'>('ready');

  async onSubmit({ value }: PromptInputSubmitEvent) {
    this.status.set('submitted');
    // ... call your LLM, set 'streaming' on first chunk, 'ready' on done, 'error' on failure
  }
}

API — Inputs

NameTypeDefaultDescription
valueSignal<string>signal('')Textarea value. Two-way bindable with [(value)].
attachmentsSignal<readonly File[]>signal([])List of attached files. Two-way bindable.
state'ready' | 'submitted' | 'streaming' | 'error''ready'State machine. Drives submit visual + Esc behavior + aria-live announcements.
size'sm' | 'md' | 'lg''md'Visual size — padding, gap, font.
variant'default' | 'ghost' | 'bordered''default'Border treatment.
disabledbooleanfalseDisables the whole container (children inherit via DI).
submitOnEnterbooleantrueWhen false, only Mod+Enter submits. Useful for multi-line drafting.
ariaLabelstring'AI prompt input'aria-label for the form container.
acceptAttachmentsreadonly string[] | falsefalseMIME patterns accepted (e.g. ['image/*', 'application/pdf']). false disables attachments.
maxAttachmentsnumber10Maximum simultaneous attachments.
maxAttachmentSizenumber10485760Maximum bytes per file (10MB default).

API — Outputs

NameTypeDescription
submittedPromptInputSubmitEventFires on Enter (when submitOnEnter), Mod+Enter, or submit button click in ready state. Payload: { value, attachments, preventDefault }.
canceledvoidFires on submit click during streaming, or Esc during streaming.
retriedvoidFires on submit click in error state.
attachmentErrorPromptInputAttachmentErrorFires when an attachment is rejected. Payload: { file, reason, message }.

API — Imperative methods

Access via @ViewChild(PromptInput):

NameTypeDescription
submit()() => voidProgrammatically trigger submit (respects current state).
clear()() => voidClear textarea value and attachments.
focusTextarea()() => voidFocus the first <textarea> descendant.
addFiles(files)(files: readonly File[]) => voidAdd files programmatically — runs validation and emits errors.
removeAttachment(file)(file: File) => voidRemove a specific attachment.

States

The container holds a state machine driven by the state input. Visual treatment and submit behavior change per state. All children of <prompt-input> read state via dependency injection.

NameTypeDescription
readystateDefault. User can type and submit.
submittedstateMessage sent, waiting for first chunk. Submit shows spinner. Textarea disabled.
streamingstateReceiving response. Submit becomes Stop button (destructive tint). Esc cancels.
errorstateLast submit failed. Container border destructive. Submit becomes Retry.

Variants

size: sm | md (default) | lg — affects padding, gap, font size.

variant: default (border + bg) | ghost (transparent) | bordered (heavier border).

Keyboard shortcuts

NameTypeDescription
Enteron containerSubmit (if submitOnEnter=true). Otherwise newline.
Shift+Enterin textareaInsert newline. Never submits.
Mod+Enteron containerForce submit regardless of submitOnEnter.
Escon containerClear textarea (in ready/error) or cancel streaming.
Mod+Kglobal (document)Focus the first textarea on the page.

Accessibility

  • role: form with configurable aria-label
  • aria-live polite region announces state changes ("Message submitted", "AI is responding", "Submission failed")
  • aria-disabled reflects the disabled input
  • data-state attribute exposes current state for CSS targeting
  • focus-within ring on the container for keyboard navigation
  • Reduced motion: respects prefers-reduced-motion — dropdowns skip the scale animation

Theming

Semantic CSS variables consumed by the container:

NameTypeDescription
--backgroundsemanticContainer background.
--inputsemanticContainer border (default variant).
--ringsemanticFocus-within ring.
--destructivesemanticBorder + ring tint when state="error".
--radius-mdsemanticContainer border-radius.

Design decisions

  • Monolithic components (no Brain/Helm split) — follow shadcn original; spartan-ng/brain supplies headless primitives when needed (ADR-011).
  • No library-namespace prefix on selectors — preserves white-label (ADR-012).
  • Two-tier tokens (primitives → semantics, shadcn-compatible) — rebrand in one edit (ADR-013).
  • State machine over reactive state — explicit transitions, easier to reason about under streaming.

Known issues

  • The global Mod+K shortcut focuses the first <textarea> inside the container. If multiple PromptInputs are mounted, the first one in DOM wins.
  • Drag-and-drop and paste validation only run when acceptAttachments is set to an array. Default false disables attachments entirely.
  • Dropdown components (action-menu, model-select) use a v0.1 native popover. Migration to @spartan-ng/brain menu/select is planned for v0.2 once their API stabilizes.
MIT © Kalvnerv0.1.0 · pre-release