Chat Composer
Chat composer: auto-growing textarea in a rounded graphite panel, attach button and a round light send button that turns into a stop square while the assistant answers; Enter sends, Shift+Enter adds a line; glow above and disclaimer below.
nocturne/ui/chat-composerSource
"use client";
import { useId, useState, type FormEvent, type KeyboardEvent } from "react";
import { ArrowUp, Plus, Square } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ChatComposerProps {
placeholder?: string;
/** Small print under the composer. Pass "" to hide it. */
disclaimer?: string;
/** While true, the send button becomes a stop button. */
generating?: boolean;
onSend?: (message: string) => void;
onStop?: () => void;
onAttach?: () => void;
/** Paint the Nocturne glow above the composer. */
glow?: boolean;
label?: string;
className?: string;
}
/**
* Chat composer: auto-growing textarea in a rounded graphite panel, attach button, and a round light send
* button that turns into a stop square while the assistant answers. Enter sends, Shift+Enter adds a line.
*/
export function ChatComposer({
placeholder = "Write to Nova…",
disclaimer = "Nova can make mistakes. Check important information.",
generating = false,
onSend,
onStop,
onAttach,
glow = true,
label = "Message",
className,
}: ChatComposerProps) {
const [value, setValue] = useState("");
const id = useId();
const canSend = value.trim().length > 0 && !generating;
const send = () => {
if (!canSend) return;
onSend?.(value.trim());
setValue("");
};
const onSubmit = (e: FormEvent) => {
e.preventDefault();
if (generating) onStop?.();
else send();
};
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault();
send();
}
};
return (
<div className={cn("relative isolate w-full", className)}>
{glow && <div aria-hidden className="pointer-events-none absolute inset-x-[10%] -top-24 -z-10 h-40 bg-(image:--nocturne-glow)" />}
<form onSubmit={onSubmit} className="da-stroke da-transition rounded-da-lg bg-da-surface p-3 shadow-da-md focus-within:border-da-border-strong">
<label htmlFor={id} className="sr-only">
{label}
</label>
<textarea
id={id}
rows={2}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={onKeyDown}
placeholder={placeholder}
className="block max-h-48 min-h-12 w-full resize-none bg-transparent px-2 pt-1 text-[15px] leading-relaxed text-da-fg outline-none [field-sizing:content] placeholder:text-da-muted-fg"
/>
<div className="mt-1 flex items-center justify-end gap-1.5">
<button
type="button"
onClick={onAttach}
aria-label="Add files or context"
className="da-focus da-transition grid size-9 place-items-center rounded-full text-da-muted-fg hover:bg-da-muted hover:text-da-fg"
>
<Plus aria-hidden className="size-5" />
</button>
<button
type="submit"
disabled={!generating && !canSend}
aria-label={generating ? "Stop generating" : "Send message"}
className="da-focus da-transition grid size-9 place-items-center rounded-full bg-da-fg text-da-bg hover:bg-da-fg/90 disabled:bg-da-muted disabled:text-da-muted-fg"
>
{generating ? <Square aria-hidden className="size-3.5 fill-current" /> : <ArrowUp aria-hidden className="size-4" />}
</button>
</div>
</form>
{disclaimer && <p className="mt-2 text-center text-xs text-da-muted-fg">{disclaimer}</p>}
</div>
);
}
export default ChatComposer;
modules/nocturne/ui/chat-composer/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder / label / disclaimer | string | — | Copy; disclaimer "" hides it. |
| generating | boolean | false | Send becomes stop. |
| onSend | (message: string) => void | — | Called with trimmed text. |
| onStop / onAttach | () => void | — | Callbacks. |
| glow | boolean | true | Paint the glow above. |
Input in other art directions
Input
Labelled text field with hint, error state, leading icon, suffix slot and two sizes. The field presses into its shadow on focus; errors switch to a red stroke with an explicit message.
Number Stepper
Numeric input flanked by chunky yellow − / + buttons (disabled at bounds), big display-font value, optional unit suffix; clamps typed values to min/max.
OTP Input
One-time-code input: ruled digit squares (split 3+3 for six digits) that turn yellow and press in on focus; auto-advance, backspace and arrow navigation, paste of the full code, completion callback and error state.
Textarea
Labelled multi-line field with a live character counter chip that turns orange near the limit, hint and error states; presses into its shadow on focus.
Combobox
Autocomplete select: a glass pill input that filters a frosted listbox as you type, with arrow-key highlight, Enter to choose, Escape to close, hints and a check on the selection.
Input
Frosted text field with label, hint, error state, leading icon, trailing slot (button, kbd, unit), rounded or pill shape and two sizes. Focus brightens the glass and adds a soft violet halo.
Range Slider
Glass slider on Radix Slider: frosted track, primary→accent range, glowing gradient thumbs with value bubbles on hover/focus and tick labels; single value or range.
Tag Input
Glass chip input: type and press Enter or comma to add gradient chips, Backspace removes the last, × removes one; quick-add suggestion chips and a max counter.
Input
Hairline text field with label (auto "Optional" marker), hint, error with icon, leading icon, text prefix, trailing slot (unit, kbd) and indigo focus ring. Two sizes.
Search Input
Search field with leading icon, “/” keycap hint that focuses the field, clear button (Escape clears too) and a loading spinner state. Two sizes, controlled or uncontrolled.
Select Menu
Labelled select built on Radix Select: bordered trigger with chevrons, portalled popover with optional icons, descriptions, groups, separators, disabled items and a check on the selected one.
Switch Field
Settings row with label and description on the left and a Radix Switch on the right; plain (for divided lists) or bordered card that highlights when on. Disabled state.
Checkbox Group
Settings checkboxes as ruled rows: square ink check boxes (native inputs), label and muted description; the whole row is clickable.
Input
Underline field: mono uppercase label, a borderless input on a single bottom hairline that turns ink (2px) on focus, optional prefix, hint or red error.
Search Input
Search field with live results: hairline-bordered input with icon and ⌘K hint; matches appear in a ruled list below with the query highlighted and a mono kind label; empty state included.
Select Field
Native select styled as an underline field with a mono label and a custom chevron — reliable on every device and screen reader.
Currency Input
Amount field with currency symbol prefix and an attached currency select; formats with thousands separators on blur, right-aligned mono digits.
File Upload
Receipt/document dropzone: dashed bordered area with upload icon, click-or-drag, file list with size, per-file size validation errors and remove buttons.
Input
Form field: label (with optional marker), bordered 40px input with leading/trailing adornments, blue focus ring, hint text or red error with aria-invalid.
Select Field
Labelled Radix Select styled as a bordered field: grouped options (e.g. GL accounts) with group headings, check on the selected item, portalled popover with data-da.
Chip Select
Multi-select as soft pill checkboxes: unselected sand pills, selected sage pills with a check that pops in; native checkboxes in a fieldset.
Input
Soft filled text field: rounded sand-filled input that turns cream with a sage ring on focus, optional leading icon and trailing unit, hint or gentle clay error message.
Target Slider
Reduction target picker: a thick rounded sage range (0–60%) with tick labels and a “science-aligned” marker, showing the resulting tonnes and a gentle verdict.
Textarea
Soft note field: rounded sand textarea that auto-grows (field-sizing), with a live character counter that turns clay near the limit.
Checkbox List
Round-checkbox to-do list built on native checkboxes: bouncy mint ticks, strike-through when done, meta line per item and an “x of y” counter with a progress bar.
Day Picker
Week-strip date picker: prev/next week arrows and seven rounded day pills (weekday + number) with task-load dots (red when full); the selected day fills periwinkle.
Input
Soft text field: rounded tinted well that turns white with a thick periwinkle ring on focus, label, optional leading icon and suffix, hint or red error message.
Radio Cards
Native radio group rendered as big pastel cards (emoji, title, text); the chosen card lifts with a periwinkle ring and a filled radio dot.