Components
Prompt Input
A chat composer — auto-resizing textarea + submit button + a toolbar slot. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add prompt-inputpnpm dlx kosmesis add prompt-inputyarn dlx kosmesis add prompt-inputbunx kosmesis add prompt-inputInstall the following dependencies:
npm install @morphos/iconspnpm add @morphos/iconsyarn add @morphos/iconsbun add @morphos/iconsCopy and paste the following code into your project.
import { StatefulComponent } from "@praxisjs/core";import { Component, FunctionProp, Prop, Ref, State, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { cn } from "@/lib/utils";export interface PromptInputProps { value?: string; defaultValue?: string; placeholder?: string; disabled?: boolean; onChange?: (value: string) => void; onSubmit?: (value: string) => void; class?: string; children?: Children;}// `value`: pass a plain string for uncontrolled use, or a getter function to stay controlled.@Component()export class PromptInput extends StatefulComponent { @Prop() value?: string; @Prop() defaultValue = ""; @Prop() placeholder = "Ask anything..."; @Prop() disabled = false; @Prop() class?: string; @Prop() children?: Children; @FunctionProp() onChange?: PromptInputProps["onChange"]; @FunctionProp() onSubmit?: PromptInputProps["onSubmit"]; @Ref<HTMLTextAreaElement>() textareaRef!: RefType<HTMLTextAreaElement>; @State() _value = ""; onBeforeMount() { this._value = this.value ?? this.defaultValue; } get text(): string { return this.value ?? this._value; } private readonly _handleInput = (event: Event) => { const target = event.target as HTMLTextAreaElement; target.style.height = "auto"; target.style.height = `${String(Math.min(target.scrollHeight, 200))}px`; if (this.value === undefined) this._value = target.value; this.onChange?.(target.value); }; submit(): void { const text = this.text.trim(); if (!text || this.disabled) return; this.onSubmit?.(text); if (this.value === undefined) this._value = ""; } private readonly _handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); this.submit(); } }; render() { return ( <div data-slot="prompt-input" class={cn("flex flex-col gap-2 rounded-xl border bg-background p-2 shadow-xs", this.class)}> <textarea ref={this.textareaRef} rows={1} value={() => this.text} placeholder={this.placeholder} disabled={this.disabled} class="max-h-[200px] min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed" onInput={this._handleInput} onKeyDown={this._handleKeyDown} /> <div class="flex items-center justify-between gap-2 px-1"> <div class="flex items-center gap-1">{this.children}</div> <button type="button" aria-label="Send message" disabled={() => this.disabled || this.text.trim().length === 0} class="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground disabled:pointer-events-none disabled:opacity-40" onClick={() => { this.submit(); }} > <Icon name="ArrowUp" size={16} /> </button> </div> </div> ); }}Install the following dependencies:
npm install @praxisjs/css @morphos/iconspnpm add @praxisjs/css @morphos/iconsyarn add @praxisjs/css @morphos/iconsbun add @praxisjs/css @morphos/iconsCopy and paste the following code into your project.
import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, FunctionProp, Prop, Ref, State, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class PromptInputStyles extends Stylesheet { $root = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem", borderRadius: "0.75rem", border: `1px solid ${t.border}`, backgroundColor: t.background, padding: "0.5rem", boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }); $textarea = this.css({ maxHeight: "200px", minHeight: "2.25rem", width: "100%", resize: "none", backgroundColor: "transparent", padding: "0.375rem 0.5rem", fontSize: "0.875rem", outline: "none", color: t.foreground, }) .placeholder({ color: t.mutedForeground }) .disabled({ cursor: "not-allowed" }); $footer = this.css({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", padding: "0 0.25rem" }); $toolbar = this.css({ display: "flex", alignItems: "center", gap: "0.25rem" }); $submit = this.css({ display: "flex", height: "2rem", width: "2rem", flexShrink: 0, alignItems: "center", justifyContent: "center", borderRadius: "9999px", backgroundColor: t.primary, color: t.primaryForeground, }).on("&:disabled", { pointerEvents: "none", opacity: 0.4 });}export interface PromptInputProps { value?: string; defaultValue?: string; placeholder?: string; disabled?: boolean; onChange?: (value: string) => void; onSubmit?: (value: string) => void; class?: string; children?: Children;}// `value`: pass a plain string for uncontrolled use, or a getter function to stay controlled.@Component()export class PromptInput extends StatefulComponent { @Styled(PromptInputStyles) $s!: PromptInputStyles; @Prop() value?: string; @Prop() defaultValue = ""; @Prop() placeholder = "Ask anything..."; @Prop() disabled = false; @Prop() class?: string; @Prop() children?: Children; @FunctionProp() onChange?: PromptInputProps["onChange"]; @FunctionProp() onSubmit?: PromptInputProps["onSubmit"]; @Ref<HTMLTextAreaElement>() textareaRef!: RefType<HTMLTextAreaElement>; @State() _value = ""; onBeforeMount() { this._value = this.value ?? this.defaultValue; } get text(): string { return this.value ?? this._value; } private readonly _handleInput = (event: Event) => { const target = event.target as HTMLTextAreaElement; target.style.height = "auto"; target.style.height = `${String(Math.min(target.scrollHeight, 200))}px`; if (this.value === undefined) this._value = target.value; this.onChange?.(target.value); }; submit(): void { const text = this.text.trim(); if (!text || this.disabled) return; this.onSubmit?.(text); if (this.value === undefined) this._value = ""; } private readonly _handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); this.submit(); } }; render() { return ( <div data-slot="prompt-input" class={cx(this.$s.$root, this.class)}> <textarea ref={this.textareaRef} rows={1} value={() => this.text} placeholder={this.placeholder} disabled={this.disabled} class={this.$s.$textarea} onInput={this._handleInput} onKeyDown={this._handleKeyDown} /> <div class={this.$s.$footer}> <div class={this.$s.$toolbar}>{this.children}</div> <button type="button" aria-label="Send message" disabled={() => this.disabled || this.text.trim().length === 0} class={this.$s.$submit} onClick={() => { this.submit(); }} > <Icon name="ArrowUp" size={16} /> </button> </div> </div> ); }}Examples
About
Purely presentational — no Morphos equivalent. value behaves like CalendarState.selected:
pass a plain string for uncontrolled use, or a getter function to stay controlled. Enter submits,
Shift+Enter inserts a newline; children render as a toolbar row below the textarea (attachments,
model pickers, ...).
Usage
import { Icon } from "@morphos/icons";
import { PromptInput } from "@/components/ui/prompt-input";
<PromptInput placeholder="Ask anything..." onSubmit={(value) => sendMessage(value)}>
<Button variant="ghost" size="sm">
<Icon name="Paperclip" size={16} />
</Button>
</PromptInput>Props
| Prop | Type | Default |
|---|---|---|
value | string | — (uncontrolled when omitted) |
defaultValue | string | "" |
placeholder | string | "Ask anything..." |
disabled | boolean | false |
onChange | (value: string) => void | — |
onSubmit | (value: string) => void | — |
class | string | — |