Kosmesis
Components

Prompt Input

A chat composer — auto-resizing textarea + submit button + a toolbar slot. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add prompt-input
pnpm dlx kosmesis add prompt-input
yarn dlx kosmesis add prompt-input
bunx kosmesis add prompt-input

Install the following dependencies:

npm install @morphos/icons
pnpm add @morphos/icons
yarn add @morphos/icons
bun add @morphos/icons

Copy and paste the following code into your project.

prompt-input.tsx
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/icons
pnpm add @praxisjs/css @morphos/icons
yarn add @praxisjs/css @morphos/icons
bun add @praxisjs/css @morphos/icons

Copy and paste the following code into your project.

prompt-input.tsx
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

PropTypeDefault
valuestring— (uncontrolled when omitted)
defaultValuestring""
placeholderstring"Ask anything..."
disabledbooleanfalse
onChange(value: string) => void
onSubmit(value: string) => void
classstring

On this page