Chat input
The whole flow (attach, type, record): everything floats on one layer
Adaptive width, folded 360 · unfolded 840 · wide 1040 (scroll →): bubbles clamp to min(82%, bubbleMax), the pill clamps to bubbleMax and centers
Sent 0 messages · pending 0 attachments · recording off. The + attaches (X removes), the mic pops the HUD above the pill (Cancel or the mic ends it), Enter sends, long-press the circle to flip send↔mic. The island is live.
The headline surface of the chat experience: a large, fully-rounded pill that FLOATS
above the message cards, laid out [attach] [text field] [send/mic]. Canonical truth is
the Android app’s ChatInputBar, brought up as the shipped Compose NockerlChatInput;
the web NockerlChatInput mirrors it 1:1.
Three things make it read as the signature it is:
- The §2 floating border. The pill container carries the signature thick cyan edge:
border.width.floating(1.5px) in the full accent, the mark that trains the eye that a thick cyan border = a layer hovering above content (design laws §2). The border rides the container only; the controls inside keep their own treatment. - A field that is part of the pill. The text field is transparent inside the pill
(ink on the chrome on-tokens, accent caret), deliberately not a recessed
TextAreawell (a well-in-a-pill would be a box-in-a-box). It grows to five lines, then scrolls. - The send↔mic morph (the one refinement). The trailing 48px accent circle is a
single toggle: SEND when there’s text, MIC when empty, long-press to flip manually. The
glyph cross-fades + scales (0.6 → 1) on the fast/standard motion tokens. One
legible morph on interpolatable props (§4), never a same-color hard swap. Frozen under
prefers-reduced-motion.
// Web: @dizyx/nockerl-react. Controlled value; the action circle auto-picks// send (text present) or mic (empty); Enter sends, Shift+Enter breaks the line.import { NockerlChatInput } from '@dizyx/nockerl-react';
export function Composer({ onSend, onMic, onAttach }: ComposerProps) { const [draft, setDraft] = useState(''); return ( <NockerlChatInput value={draft} onValueChange={setDraft} onSend={() => { onSend(draft); setDraft(''); }} onMic={onMic} onAttach={onAttach} /> );}// Android: Jetpack Compose (CANONICAL source: chat/ui/ChatInputBar.kt, shipped as// NockerlChatInput). The same grammar: floating pill + attach + field + send/mic// with the cross-fade morph; long-press flips the manual override.NockerlChatInput( value = draft, onValueChange = { draft = it }, onSend = { viewModel.send(draft); draft = "" }, onMic = { viewModel.startRecording() }, onAttach = { attachSheetOpen = true },)Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
value * | string | The message text (controlled). | |
onValueChange * | (next: string) => void | Text edits. | |
onSend * | () => void | Fired when the action button is in SEND mode and activated (tap or Enter). | |
onMic * | () => void | Fired when the action button is in MIC mode and tapped. | |
placeholder | string | 'Message Nockerl…' | The empty-field hint. |
ariaLabel | string | 'Message' | Accessible name for the text field (screen readers). |
disabled | boolean | false | Inert input + actions. Freeze ruling (task 2686/): react uses disabled (the DOM/react family idiom); compose keeps enabled (its idiom): semantics shared, casing platform-idiomatic per law §9. |
onAttach | (() => void) | undefined | Optional leading attach action; omit to hide the attach button. | |
attachIcon | ReactNode | IconPlus | The attach glyph (default a plus; pass a paperclip from your set). |
leadingAccessory | ReactNode | GENERIC leading-accessory SLOT (task 2682) that replaces the built-in attach button entirely when provided (compose any control cluster; you own its a11y). The onAttach/attachIcon pair stays as the zero-config default. | |
attachments | NockerlAttachment[] | PENDING ATTACHMENTS MODEL (task 2682): when non-empty the composite renders the real NockerlAttachmentPopover floating directly ABOVE the pill (the canonical r5 integration), aligned to the pill's column. Empty (default) renders nothing: the resting DOM is byte-identical to the bare pill. | |
onRemoveAttachment | ((index: number) => void) | undefined | Remove handler for the attachments model (fired with the index). | |
contextAccessory | ReactNode | GENERIC context/accessory SLOT (task 2682) that rides directly above the pill (below the attachments row): session chips, a context-gauge strip, the Recording HUD. Deliberately untyped (never session-typed props); empty renders nothing. | |
maxLines | number | 5 | Max visible text lines before the field scrolls (the native maxLines). Default 5. |
className | string | Extra class on the pill. |
The Compose signature mirrors the web props: value, onValueChange, onSend, onMic,
placeholder, disabled, optional onAttach + attachIcon. Long-press routes through
Modifier.longPressPop (pop feedback + haptics); the morph runs on AnimatedContent
with the fast/standard motion tokens.
Attachment popover
Section titled “Attachment popover”The moment something is attached, the pending thumbnails appear directly above the
pill, the shipped NockerlAttachmentPopover (canonical source: the app’s
PendingImageRow, brought up as the Compose NockerlAttachmentPopover). Try it live in
the demo above: the + attaches, the badge removes, a send consumes them.
Each thumbnail is a small tile on the same L3 floating layer as the pill (messages
scroll under both), clipped to the panel radius, traced by the warm agent-family
edge at the shared border.width.floating weight, deliberately not cyan: the warm
edge marks dismissable transient chrome, where the pill’s cyan §2 edge marks the
fixed floating layer. A compact status-error badge overhangs the top-end corner to
remove the attachment; the row scrolls horizontally on overflow.
The component owns only the frame (float + edge + badge). The caller supplies each image src and keeps the pick/decode/upload plumbing outside.
import { NockerlAttachmentPopover } from '@dizyx/nockerl-react';
<NockerlAttachmentPopover attachments={pending} // [{ src, alt }] onRemove={(i) => setPending((p) => p.filter((_, j) => j !== i))}/>// Android: Compose (CANONICAL). One Painter per pending attachment.NockerlAttachmentPopover( attachments = pendingPainters, onRemove = { viewModel.removeAttachment(it) },)| Prop | Type | Default | Description |
|---|---|---|---|
attachments * | NockerlAttachment[] | One entry per pending attachment, in display order. | |
onRemove * | (index: number) => void | Fired with the index whose remove badge was activated. | |
disabled | boolean | false | Inert remove badges (freeze ruling per law §9: react uses disabled; compose keeps enabled). |
className | string | Extra class on the row. |
attachments: List<Painter> · onRemove: (Int) -> Unit · modifier · enabled. The
same frame; images arrive as Painters instead of src URLs.
The integrated flow
Section titled “The integrated flow”The demo above is the whole experience on one stage: tap the mic and the
Recording HUD pops directly above the pill, a sibling on
the same L3 floating layer, one small gap, both carrying the §2 floating edge. The HUD
leads with the brand mark (Nockerl is listening, never an anonymous red strip),
then the pulsing status dot, the m:ss timer, the live 5-bar meter, and a ghost Cancel
(the canonical order ratified with the native NockerlRecordingHud).
Stack order, bottom-up, everything floating over the scrolling feed:
- The pill: persistent, cyan §2 edge.
- The Recording HUD: transient, cyan edge, brand-led (present only while recording).
- The attachment popover: transient, warm dismissable edge (present only with pending attachments).
The voice state machine (recording → transcribing → streaming), the real microphone, and the upload plumbing all live in the app. The design system supplies the three floating surfaces and their shared grammar.
Platform notes
Section titled “Platform notes”- Enter sends on web (Shift+Enter breaks the line). The native pill has no return-to-send; unify brand expression, honor platform behavior.
- Long-press flips the send↔mic override on both platforms (touch/pointer). Keyboard users always have auto mode: type → send, clear → mic.
- The pill never renders a glow or colored shadow: depth is the L3 neutral lift + the top catch-light; the cyan edge is a border, not an emission (§1 + §2).