PromptInput
Container for AI prompt entry. Owns the state machine (ready → submitted → streaming → error), keyboard shortcuts, attachment validation, and coordinates all subcomponents.
Preview
<prompt-input [(value)]="message">
<prompt-input-textarea placeholder="Type a prompt — Mod+K focuses globally" />
<prompt-input-toolbar>
<prompt-input-submit />
</prompt-input-toolbar>
</prompt-input>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:
- PromptInputTextarea — auto-expanding text input
- PromptInputSubmit — 4-state submit button
- PromptInputToolbar — flex wrapper for the bottom row
- PromptInputTools — left-side group of actions
- Button — toolbar action button (from
@shadng/core) - PromptInputAttachments — attachment list container
- PromptInputAttachment — individual attachment card
- PromptInputActionMenu — dropdown for less-frequent actions
- PromptInputModelSelect — model picker
Installation
bash
npm install @shadng/prompt-inputBasic 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
| Name | Type | Default | Description |
|---|---|---|---|
| value | Signal<string> | signal('') | Textarea value. Two-way bindable with [(value)]. |
| attachments | Signal<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. |
| disabled | boolean | false | Disables the whole container (children inherit via DI). |
| submitOnEnter | boolean | true | When false, only Mod+Enter submits. Useful for multi-line drafting. |
| ariaLabel | string | 'AI prompt input' | aria-label for the form container. |
| acceptAttachments | readonly string[] | false | false | MIME patterns accepted (e.g. ['image/*', 'application/pdf']). false disables attachments. |
| maxAttachments | number | 10 | Maximum simultaneous attachments. |
| maxAttachmentSize | number | 10485760 | Maximum bytes per file (10MB default). |
API — Outputs
| Name | Type | Description |
|---|---|---|
| submitted | PromptInputSubmitEvent | Fires on Enter (when submitOnEnter), Mod+Enter, or submit button click in ready state. Payload: { value, attachments, preventDefault }. |
| canceled | void | Fires on submit click during streaming, or Esc during streaming. |
| retried | void | Fires on submit click in error state. |
| attachmentError | PromptInputAttachmentError | Fires when an attachment is rejected. Payload: { file, reason, message }. |
API — Imperative methods
Access via @ViewChild(PromptInput):
| Name | Type | Description |
|---|---|---|
| submit() | () => void | Programmatically trigger submit (respects current state). |
| clear() | () => void | Clear textarea value and attachments. |
| focusTextarea() | () => void | Focus the first <textarea> descendant. |
| addFiles(files) | (files: readonly File[]) => void | Add files programmatically — runs validation and emits errors. |
| removeAttachment(file) | (file: File) => void | Remove 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.
| Name | Type | Description |
|---|---|---|
| ready | state | Default. User can type and submit. |
| submitted | state | Message sent, waiting for first chunk. Submit shows spinner. Textarea disabled. |
| streaming | state | Receiving response. Submit becomes Stop button (destructive tint). Esc cancels. |
| error | state | Last 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
| Name | Type | Description |
|---|---|---|
| Enter | on container | Submit (if submitOnEnter=true). Otherwise newline. |
| Shift+Enter | in textarea | Insert newline. Never submits. |
| Mod+Enter | on container | Force submit regardless of submitOnEnter. |
| Esc | on container | Clear textarea (in ready/error) or cancel streaming. |
| Mod+K | global (document) | Focus the first textarea on the page. |
Accessibility
- role:
formwith configurablearia-label - aria-live polite region announces state changes ("Message submitted", "AI is responding", "Submission failed")
- aria-disabled reflects the
disabledinput - 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:
| Name | Type | Description |
|---|---|---|
| --background | semantic | Container background. |
| --input | semantic | Container border (default variant). |
| --ring | semantic | Focus-within ring. |
| --destructive | semantic | Border + ring tint when state="error". |
| --radius-md | semantic | Container 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+Kshortcut 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
acceptAttachmentsis set to an array. Defaultfalsedisables attachments entirely. - Dropdown components (action-menu, model-select) use a v0.1 native popover. Migration to
@spartan-ng/brainmenu/select is planned for v0.2 once their API stabilizes.