composeai 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,896 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, SVGProps, ReactNode, CSSProperties } from 'react';
3
+
4
+ /**
5
+ * Built-in icon set for the composer.
6
+ *
7
+ * Why inline instead of `lucide-react`?
8
+ * - Removes the runtime dependency entirely.
9
+ * - Lets consumers override any icon with their own component via the
10
+ * `icons` prop on `<Composer />` — useful for matching a brand kit
11
+ * (Heroicons, Phosphor, Material, etc).
12
+ *
13
+ * The defaults below are 24x24 stroke SVGs drawn in the same visual idiom as
14
+ * lucide so the out-of-the-box composer looks the same as before. Each icon
15
+ * forwards arbitrary SVGProps so callers can pass `className`, `style`,
16
+ * `aria-*`, etc.
17
+ */
18
+
19
+ type IconProps = SVGProps<SVGSVGElement>;
20
+ type IconComponent = ComponentType<IconProps>;
21
+ /**
22
+ * Public icon slot map. Consumers can override any of these via the
23
+ * `icons` prop on `<Composer />`. Anything left unspecified falls back to
24
+ * the built-in lucide-style default above.
25
+ */
26
+ interface ComposerIcons {
27
+ /** Submit / send the message. */
28
+ send: IconComponent;
29
+ /** Stop an in-flight streaming response. */
30
+ stop: IconComponent;
31
+ /** Toolbar: attach any file. */
32
+ attach: IconComponent;
33
+ /** Toolbar: pick an image. */
34
+ image: IconComponent;
35
+ /** Voice plugin: start recording. */
36
+ voice: IconComponent;
37
+ /** Voice plugin: transcription in progress. */
38
+ voiceRecording: IconComponent;
39
+ /** Toolbar: enable web search. */
40
+ web: IconComponent;
41
+ /** Close / dismiss (e.g. lightbox, chip remove). */
42
+ close: IconComponent;
43
+ /** Zoom into an attachment / diagram. */
44
+ zoom: IconComponent;
45
+ /** Generic file (PDF, doc, etc) attachment. */
46
+ file: IconComponent;
47
+ /** Audio attachment. */
48
+ audio: IconComponent;
49
+ /** Decorative sparkle (suggestions, diagram preview header). */
50
+ sparkle: IconComponent;
51
+ /** Generic loading spinner (used on attachment chips during upload). */
52
+ spinner: IconComponent;
53
+ /** Generic warning / failure badge (used on failed-upload chips). */
54
+ warning: IconComponent;
55
+ }
56
+
57
+ /**
58
+ * Stylable surfaces inside the composer. Every entry corresponds to a real
59
+ * DOM node the package owns. `classNames` and `sx` props are keyed by these
60
+ * slot names so consumers can reskin a single piece without forking the
61
+ * component.
62
+ */
63
+ type ComposerSlot = "root" | "card" | "editor" | "placeholder" | "toolbar" | "toolbarButton" | "sendButton" | "stopButton" | "hint" | "attachmentTray" | "attachmentChip" | "mention" | "mentionMenu" | "mentionItem" | "slashMenu" | "slashItem" | "mermaidPreview";
64
+ /**
65
+ * Per-slot className overrides. Strings are merged after the built-in
66
+ * classes (last-wins via standard CSS cascade), so consumers can layer
67
+ * Tailwind utilities or their own classes on top of the defaults.
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * <Composer
72
+ * classNames={{
73
+ * card: "bg-gradient-to-br from-violet-500/10 to-pink-500/10",
74
+ * sendButton: "bg-violet-600 hover:bg-violet-700",
75
+ * }}
76
+ * />
77
+ * ```
78
+ */
79
+ type ComposerSlotClassNames = Partial<Record<ComposerSlot, string>>;
80
+ /**
81
+ * Lightweight `sx` value: a plain CSS object merged onto a slot's `style`
82
+ * with a few token-aware shortcuts.
83
+ *
84
+ * Token-aware keys (`color`, `bg`, `backgroundColor`, `borderColor`,
85
+ * `outlineColor`, `fill`, `stroke`) accept either a real CSS value
86
+ * (`"#fff"`, `"rgb(...)"`, `"red"`) or one of the composer's design tokens
87
+ * (`"primary"`, `"accent"`, `"border"`, `"card"`, `"muted"`,
88
+ * `"foreground"`, `"background"`, `"destructive"`, `"success"`,
89
+ * `"warning"`), in which case it expands to `hsl(var(--<token>))`.
90
+ *
91
+ * Sizing/spacing keys accept numbers (React adds `px`) or any CSS length
92
+ * string (`"200px"`, `"50%"`, `"clamp(...)"`).
93
+ *
94
+ * No pseudo selectors, no media queries, no responsive arrays — reach for
95
+ * `classNames` when you need those.
96
+ */
97
+ type ComposerSxValue = CSSProperties & {
98
+ /** Shortcut for `backgroundColor`; accepts a token name. */
99
+ bg?: string;
100
+ };
101
+ /**
102
+ * Per-slot sx overrides. Each value follows {@link ComposerSxValue}.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * <Composer
107
+ * sx={{
108
+ * card: { bg: "card", borderColor: "primary", borderRadius: 12 },
109
+ * editor: { minHeight: 80, fontFamily: "JetBrains Mono, monospace" },
110
+ * sendButton: { bg: "primary", color: "primary-foreground" },
111
+ * }}
112
+ * />
113
+ * ```
114
+ */
115
+ type ComposerSxMap = Partial<Record<ComposerSlot, ComposerSxValue>>;
116
+ /**
117
+ * Design tokens applied as inline CSS custom properties on the composer
118
+ * root, so they cascade into every slot — including the package's built-in
119
+ * CSS — without overriding the consumer app's global theme.
120
+ *
121
+ * Color tokens are HSL components (e.g. `"258 90% 62%"`) so they compose
122
+ * cleanly with opacities (`hsl(var(--primary) / 0.1)`), matching the
123
+ * convention used internally.
124
+ *
125
+ * Sizing tokens (`radius`, `fontSize`) accept a number (treated as `px`)
126
+ * or any CSS length string.
127
+ */
128
+ interface ComposerTokens {
129
+ primary?: string;
130
+ primaryForeground?: string;
131
+ accent?: string;
132
+ accentForeground?: string;
133
+ background?: string;
134
+ foreground?: string;
135
+ card?: string;
136
+ cardForeground?: string;
137
+ popover?: string;
138
+ popoverForeground?: string;
139
+ muted?: string;
140
+ mutedForeground?: string;
141
+ border?: string;
142
+ ring?: string;
143
+ input?: string;
144
+ destructive?: string;
145
+ destructiveForeground?: string;
146
+ success?: string;
147
+ successForeground?: string;
148
+ warning?: string;
149
+ warningForeground?: string;
150
+ /** Outer card corner radius. Number → px. Default `28px`. */
151
+ radius?: number | string;
152
+ /** Editor base font size. Number → px. Default `15px`. */
153
+ fontSize?: number | string;
154
+ /** Font family for the editor and chips. Defaults to inherit. */
155
+ fontFamily?: string;
156
+ }
157
+ /**
158
+ * Consumer-supplied renderer for fenced code blocks the composer treats as
159
+ * diagrams (currently `mermaid`). When provided, the composer will NOT try to
160
+ * dynamically import the `mermaid` package — your renderer is fully in charge.
161
+ * When omitted, the composer falls back to a lazy `import("mermaid")`; if
162
+ * `mermaid` isn't installed, the diagram is silently skipped.
163
+ *
164
+ * Future fence languages may be routed here too; inspect `language` to
165
+ * decide what to do.
166
+ */
167
+ type DiagramRenderer = (params: {
168
+ /** Raw code inside the fence, with the surrounding ``` stripped. */
169
+ code: string;
170
+ /** Language tag from the opening fence — e.g. "mermaid". */
171
+ language: string;
172
+ }) => ReactNode;
173
+ /**
174
+ * How a click on a quick-prompt chip is interpreted.
175
+ * - `"sendValue"` (default): drop the prompt into the editor and submit
176
+ * immediately. The user sees their `onSend` payload fire as if they
177
+ * typed it and pressed Enter.
178
+ * - `"initValue"`: drop the prompt into the editor but do NOT submit —
179
+ * the user can edit it further before sending.
180
+ */
181
+ type ComposerPromptBehavior = "sendValue" | "initValue";
182
+ interface ComposerPromptsConfig {
183
+ /**
184
+ * Full list of prompts the consumer wants to expose. Order is preserved
185
+ * unless `randomize` is `true` (the default).
186
+ */
187
+ items: string[];
188
+ /**
189
+ * What clicking a chip should do. Defaults to `"sendValue"` — clicking
190
+ * a prompt fills the editor and submits immediately, which matches the
191
+ * "starter prompts" UX users see in most AI chat surfaces.
192
+ */
193
+ behavior?: ComposerPromptBehavior;
194
+ /**
195
+ * Optional notification fired whenever the user picks a prompt — useful
196
+ * for analytics. Fires regardless of `behavior`.
197
+ */
198
+ onSelect?: (prompt: string) => void;
199
+ /**
200
+ * Maximum number of chips rendered at once. Defaults to `3`, hard-capped
201
+ * at `5` to keep the chip row from dominating the composer.
202
+ */
203
+ maxToShow?: number;
204
+ /**
205
+ * When `items.length > maxToShow`, pick a random subset on each mount
206
+ * so different sessions see different suggestions. Defaults to `true`.
207
+ * Set `false` to always show the first N items in order.
208
+ */
209
+ randomize?: boolean;
210
+ }
211
+ type AttachmentKind = "image" | "audio" | "file";
212
+ /**
213
+ * Upload lifecycle for an attachment. Only meaningful when
214
+ * `attachmentOptions.uploadFirst` is enabled.
215
+ * - `undefined` / `"ready"`: no upload in flight (default; same as today).
216
+ * - `"uploading"`: `onUpload(file)` is running — chip shows a spinner.
217
+ * - `"uploaded"`: `onUpload` resolved truthy — chip looks normal again.
218
+ * - `"failed"`: `onUpload` resolved falsy or threw — chip shows a warning.
219
+ * The user can dismiss the chip and re-attach to retry.
220
+ */
221
+ type AttachmentStatus = "ready" | "uploading" | "uploaded" | "failed";
222
+ interface Attachment {
223
+ id: string;
224
+ kind: AttachmentKind;
225
+ name: string;
226
+ mimeType: string;
227
+ size: number;
228
+ file: File;
229
+ /** Object URL for previews (revoked when removed). */
230
+ previewUrl?: string;
231
+ /** Optional natural width/height for images. */
232
+ width?: number;
233
+ height?: number;
234
+ /** Optional duration (seconds) for audio. */
235
+ duration?: number;
236
+ /**
237
+ * Upload lifecycle. `undefined` is equivalent to `"ready"` and means
238
+ * "no upload pipeline in play" — the historical default. Populated only
239
+ * when `attachmentOptions.uploadFirst` is on.
240
+ */
241
+ status?: AttachmentStatus;
242
+ /** Optional message surfaced when `status === "failed"`. */
243
+ error?: string;
244
+ }
245
+ interface MentionItem {
246
+ id: string;
247
+ label: string;
248
+ description?: string;
249
+ avatarUrl?: string;
250
+ icon?: ReactNode;
251
+ }
252
+ interface MentionRef {
253
+ id: string;
254
+ label: string;
255
+ }
256
+ interface SlashCommand {
257
+ id: string;
258
+ label: string;
259
+ description?: string;
260
+ group?: string;
261
+ icon?: ReactNode;
262
+ shortcut?: string;
263
+ /**
264
+ * Called when the command is chosen. Receive helpers to mutate the editor.
265
+ * If omitted, the slash text is simply removed.
266
+ */
267
+ onSelect?: (ctx: SlashCommandContext) => void;
268
+ }
269
+ interface SlashCommandContext {
270
+ /** Inserts text at the current selection (after removing the slash trigger). */
271
+ insertText: (text: string) => void;
272
+ /** Replaces the slash trigger with raw markdown content. */
273
+ insertMarkdown: (markdown: string) => void;
274
+ /** Closes the menu without changing the editor. */
275
+ cancel: () => void;
276
+ /** Submits the composer immediately. */
277
+ submit: () => void;
278
+ }
279
+ interface ComposerSubmitPayload {
280
+ /** Plain text (chips collapsed to their labels). */
281
+ text: string;
282
+ /** Serialized markdown including chips as `@label`. */
283
+ markdown: string;
284
+ attachments: Attachment[];
285
+ mentions: MentionRef[];
286
+ }
287
+ interface MentionConfig {
288
+ /** Static list, or async resolver that receives the query (without `@`). */
289
+ items: MentionItem[] | ((query: string) => MentionItem[] | Promise<MentionItem[]>);
290
+ /** Visual character used to render the trigger. Defaults to "@". */
291
+ trigger?: string;
292
+ /** Limit suggestion count. Defaults to 8. */
293
+ maxItems?: number;
294
+ }
295
+ interface SlashConfig {
296
+ items: SlashCommand[] | ((query: string) => SlashCommand[] | Promise<SlashCommand[]>);
297
+ trigger?: string;
298
+ maxItems?: number;
299
+ }
300
+ /**
301
+ * One entry in the paperclip's type-picker menu. When `AttachmentsConfig.types`
302
+ * is supplied, clicking the paperclip opens a small popover listing these
303
+ * options; picking one opens the OS file dialog already scoped to that
304
+ * entry's `accept` string.
305
+ */
306
+ interface AttachmentTypeOption {
307
+ /** Stable identifier (used as React key). */
308
+ id: string;
309
+ /** Display label, e.g. "PDF", "Word", "Spreadsheet". */
310
+ label: string;
311
+ /**
312
+ * `accept` attribute passed to the file input when this option is
313
+ * picked. Examples: `".pdf"`, `".docx,.doc"`, `"image/*"`,
314
+ * `".xlsx,.xls,application/vnd.ms-excel"`.
315
+ */
316
+ accept: string;
317
+ /** Optional second line, e.g. `".pdf"` or `"Word document"`. */
318
+ description?: string;
319
+ /** Optional leading icon, rendered to the start of the label. */
320
+ icon?: ReactNode;
321
+ }
322
+ interface AttachmentsConfig {
323
+ /**
324
+ * Show the generic "Attach file" picker button (paperclip icon). Honours
325
+ * `accept` so the OS dialog can include any allowed mime — images, PDFs,
326
+ * docs, audio, video, etc. Defaults to `true`.
327
+ */
328
+ file?: boolean;
329
+ /**
330
+ * Show the dedicated "Add image" picker button (image icon). Forces
331
+ * `accept="image/*"` on the underlying input so iOS / Android open the
332
+ * camera-roll picker directly instead of the generic Files app — a UX win
333
+ * on mobile chat apps. **Defaults to `false`** because the paperclip
334
+ * already accepts images via its `accept` string; opt in when the second
335
+ * tap-target is worth it.
336
+ */
337
+ image?: boolean;
338
+ /**
339
+ * Accept attribute string passed to the generic "Attach file" picker.
340
+ * Has no effect on the image-only picker (which is always `image/*`) or
341
+ * on `types` entries (which carry their own `accept`).
342
+ * Defaults to `"image/*,application/pdf,text/*,audio/*,video/*"`.
343
+ */
344
+ accept?: string;
345
+ /**
346
+ * Pre-defined attachment types. When **omitted or empty** (the default),
347
+ * clicking the paperclip opens the OS file picker directly with
348
+ * `accept`. When **non-empty**, the paperclip becomes a popover trigger
349
+ * — clicking it shows the list, and picking an entry opens the dialog
350
+ * scoped to that entry's `accept`. Great for apps that want to nudge
351
+ * users toward specific formats (e.g. "PDF or Word only").
352
+ *
353
+ * @example
354
+ * ```tsx
355
+ * features={{
356
+ * attachments: {
357
+ * types: [
358
+ * { id: "pdf", label: "PDF", accept: ".pdf", description: ".pdf" },
359
+ * { id: "word", label: "Word", accept: ".docx,.doc", description: ".docx, .doc" },
360
+ * { id: "image", label: "Image", accept: "image/*", description: "PNG, JPG, …" },
361
+ * ],
362
+ * },
363
+ * }}
364
+ * ```
365
+ */
366
+ types?: AttachmentTypeOption[];
367
+ /** Max bytes per file. Default 25 MiB. */
368
+ maxSize?: number;
369
+ /** Maximum total attachments. Default 10. */
370
+ maxCount?: number;
371
+ }
372
+ /**
373
+ * Behavioural switches for attachment lifecycle and submission rules.
374
+ * Lives at the top level (not under `features.attachments`) so the
375
+ * "is the feature on / what file types" config stays cleanly separated
376
+ * from the "what happens when the user attaches / sends" callbacks.
377
+ */
378
+ interface AttachmentOptions {
379
+ /**
380
+ * When `true`, every newly-attached file is immediately handed to
381
+ * `onUpload` in the background. The attachment chip shows a spinner
382
+ * while the upload is in flight and a warning badge if it fails.
383
+ * Sending is blocked until every attachment is either `"uploaded"`
384
+ * or removed. Defaults to `false` — consumers receive raw `File`s in
385
+ * `onSend` and upload themselves.
386
+ *
387
+ * Has no effect unless `onUpload` is also provided.
388
+ */
389
+ uploadFirst?: boolean;
390
+ /**
391
+ * Upload handler invoked once per attached `File` when `uploadFirst`
392
+ * is on. Resolve `true` for success, `false` (or throw) for failure.
393
+ * For richer feedback (URL, server id, …), keep a side map in your app
394
+ * keyed by `file.name` + `file.size` — the composer will hand the
395
+ * same `File` instance back in `onSend`'s payload.
396
+ *
397
+ * @example
398
+ * ```tsx
399
+ * attachmentOptions={{
400
+ * uploadFirst: true,
401
+ * onUpload: async (file) => {
402
+ * const res = await fetch("/api/upload", { method: "POST", body: file });
403
+ * return res.ok;
404
+ * },
405
+ * }}
406
+ * ```
407
+ */
408
+ onUpload?: (file: File) => Promise<boolean> | boolean;
409
+ /**
410
+ * Whether the user can submit a message that has attachments but no
411
+ * text. Defaults to `true` — matches Slack / Discord / WhatsApp where
412
+ * dropping a file is itself a valid message. Set to `false` to force
413
+ * users to write at least one character alongside any attachment.
414
+ */
415
+ canSendOnlyAttachment?: boolean;
416
+ }
417
+ interface MermaidConfig {
418
+ /**
419
+ * Keep the raw ```mermaid source visible in the editor while the preview
420
+ * renders below. Defaults to `true` — the user sees both the code they
421
+ * wrote and the live diagram. Set to `false` to hide the source block
422
+ * once it parses (the diagram preview is still shown), useful when you
423
+ * only want the rendered output in the conversation surface.
424
+ */
425
+ keepSource?: boolean;
426
+ }
427
+ /**
428
+ * Two visual contracts for inline / block markdown styling. Default is
429
+ * `"hybrid"` because it preserves the source byte-for-byte and is what
430
+ * the composer originally shipped with; consumers who want the more
431
+ * conventional "WYSIWYG-ish" feel opt into `"live"`.
432
+ *
433
+ * - `"hybrid"` — markers stay visible (rendered in a muted style) AND the
434
+ * inner text receives the matching format. Typing `**a**`
435
+ * leaves you with `**a**` on screen, the `**` dimmed and
436
+ * the `a` bold. This is what Slack / Discord / iMessage do.
437
+ * The document IS the markdown source.
438
+ *
439
+ * - `"live"` — markers are consumed as soon as the closing one is typed.
440
+ * `**a**` collapses to bold **a** with no asterisks left on
441
+ * screen; `# Title` becomes a styled paragraph with no
442
+ * leading `#`. This is what Notion / Tiptap / Lexical's
443
+ * own `MarkdownShortcutPlugin` do. The document is the
444
+ * *rendered* state; we reconstruct markdown only at submit.
445
+ *
446
+ * Both modes produce the same `markdown` payload on submit — the
447
+ * difference is purely how the editor LOOKS while you're typing.
448
+ */
449
+ type MarkdownMode = "hybrid" | "live";
450
+ /**
451
+ * Fine-grained markdown configuration. Pass `markdown: true` (or omit it)
452
+ * to keep the default behaviour, or supply this object to switch modes /
453
+ * scope the change.
454
+ *
455
+ * @example
456
+ * ```tsx
457
+ * // Notion-like: markers vanish once matched.
458
+ * <Composer features={{ markdown: { mode: "live" } }} />
459
+ * ```
460
+ */
461
+ interface MarkdownConfig {
462
+ /** Visual mode shared by inline and block constructs. Default: `"hybrid"`. */
463
+ mode?: MarkdownMode;
464
+ }
465
+ /**
466
+ * Inline ghost-text autocomplete configuration. When the user's current
467
+ * text is a prefix of one of the suggestions, the remaining characters
468
+ * are rendered after the caret in a muted style. Pressing **Tab** accepts
469
+ * the suggestion (the remainder is inserted into the editor); Escape, a
470
+ * non-matching keystroke, or moving the caret away dismisses it.
471
+ *
472
+ * Operates on the editor's plain-text content (chips collapsed to their
473
+ * labels) and matches against the start of the document — perfect for
474
+ * search-bar–style inputs, command palettes, or templated prompts.
475
+ *
476
+ * Pass an array for the simplest case (`ghostedAutoComplete: [...]`) or
477
+ * a config object for finer control.
478
+ *
479
+ * @example
480
+ * ```tsx
481
+ * <Composer features={{ ghostedAutoComplete: ["My cat is playing", "Hello world"] }} />
482
+ * ```
483
+ *
484
+ * @example
485
+ * ```tsx
486
+ * <Composer
487
+ * features={{
488
+ * ghostedAutoComplete: {
489
+ * suggestions: ["Summarize this thread", "Translate to English"],
490
+ * caseSensitive: false,
491
+ * minLength: 2,
492
+ * },
493
+ * }}
494
+ * />
495
+ * ```
496
+ */
497
+ interface GhostedAutoCompleteConfig {
498
+ /**
499
+ * Static list of completion suggestions. The first entry whose prefix
500
+ * matches the editor's current text wins — so order this list by
501
+ * priority / likelihood.
502
+ */
503
+ suggestions: string[];
504
+ /**
505
+ * When `false` (default), matching is case-insensitive — `"my cat"`
506
+ * matches `"My cat is playing"`. Set to `true` to require an exact
507
+ * case match.
508
+ */
509
+ caseSensitive?: boolean;
510
+ /**
511
+ * Minimum number of characters the user must have typed before a
512
+ * ghost suggestion is shown. Defaults to `1` — the ghost never
513
+ * appears on an empty editor (that's what `placeholder` is for).
514
+ */
515
+ minLength?: number;
516
+ }
517
+ interface ComposerFeatures {
518
+ /** `true` (default) → hybrid mode. Pass a {@link MarkdownConfig} to opt
519
+ * into `"live"` (Notion-style) or otherwise customise behaviour. */
520
+ markdown?: boolean | MarkdownConfig;
521
+ attachments?: boolean | AttachmentsConfig;
522
+ mentions?: false | MentionConfig;
523
+ slashCommands?: false | SlashConfig;
524
+ voice?: boolean;
525
+ mermaid?: boolean | MermaidConfig;
526
+ web?: boolean;
527
+ /**
528
+ * Inline ghost-text autocomplete suggested from a list. Accepts a plain
529
+ * `string[]` shorthand or a {@link GhostedAutoCompleteConfig} for
530
+ * case-sensitivity / minimum-length tuning. Press **Tab** to accept the
531
+ * suggestion. Disabled by default.
532
+ */
533
+ ghostedAutoComplete?: false | string[] | GhostedAutoCompleteConfig;
534
+ }
535
+ interface ComposerHandle {
536
+ focus(): void;
537
+ clear(): void;
538
+ insert(text: string): void;
539
+ submit(): void;
540
+ addAttachments(files: File[]): void;
541
+ }
542
+ /**
543
+ * Render-props received by a custom send-button slot. Returned by the
544
+ * library so consumer components can be completely chrome-free while still
545
+ * inheriting the gating logic (uploads pending, empty editor, streaming).
546
+ *
547
+ * `className` / `style` carry the resolved values from
548
+ * `classNames.sendButton` and `sx.sendButton`, so a consumer who wants
549
+ * "their button but with my colours" can just spread them; a consumer
550
+ * doing a from-scratch design can ignore both.
551
+ */
552
+ interface SendButtonRenderProps {
553
+ /** True when the editor has something sendable (text, or attachments
554
+ * if `canSendOnlyAttachment` is true) AND no uploads are pending or
555
+ * failed. Same gate the default button uses for its disabled state. */
556
+ canSend: boolean;
557
+ /** Invoke this to send. Matches the internal submit pipeline 1:1 —
558
+ * serializes editor state, fires `onSend`, clears, etc. */
559
+ onSend: () => void;
560
+ /** Resolved className from `classNames.sendButton` + sx-derived classes. */
561
+ className?: string;
562
+ /** Resolved inline styles from `sx.sendButton` tokens. */
563
+ style?: CSSProperties;
564
+ }
565
+ /**
566
+ * Render-props received by a custom stop-button slot. Only mounted while
567
+ * `isStreaming` is true on `<Composer />`.
568
+ */
569
+ interface StopButtonRenderProps {
570
+ /** Invoke this to ask the host to stop generation; fires the
571
+ * `onStop` prop on `<Composer />`. */
572
+ onStop: () => void;
573
+ /** Resolved className from `classNames.stopButton` + sx-derived classes. */
574
+ className?: string;
575
+ /** Resolved inline styles from `sx.stopButton` tokens. */
576
+ style?: CSSProperties;
577
+ }
578
+ /**
579
+ * Component-level escape hatches. When a slot is provided the library skips
580
+ * rendering its own element and renders yours instead, passing the same
581
+ * runtime data it would have used internally (callbacks, gating flags,
582
+ * resolved styles). Slots compose with `icons`, `classNames`, and `sx` —
583
+ * use whichever level matches the depth of customization you need:
584
+ *
585
+ * icons — swap the SVG inside the default button
586
+ * classNames — append classes to the default button
587
+ * sx — token-driven inline styles on the default button
588
+ * slots — replace the entire button (or other slot) wholesale
589
+ *
590
+ * @example
591
+ * ```tsx
592
+ * <Composer
593
+ * slots={{
594
+ * sendButton: ({ canSend, onSend, className }) => (
595
+ * <button
596
+ * type="button"
597
+ * disabled={!canSend}
598
+ * onClick={onSend}
599
+ * className={`flex items-center gap-2 rounded-xl bg-emerald-600 px-3 py-1.5 text-white disabled:opacity-40 ${className ?? ""}`}
600
+ * >
601
+ * <span>Send</span>
602
+ * <kbd className="text-xs opacity-75">⏎</kbd>
603
+ * </button>
604
+ * ),
605
+ * }}
606
+ * />
607
+ * ```
608
+ */
609
+ interface ComposerSlots {
610
+ /** Replace the send button. Receives {@link SendButtonRenderProps}. */
611
+ sendButton?: ComponentType<SendButtonRenderProps>;
612
+ /** Replace the stop button (rendered while `isStreaming`).
613
+ * Receives {@link StopButtonRenderProps}. */
614
+ stopButton?: ComponentType<StopButtonRenderProps>;
615
+ }
616
+ interface ComposerProps {
617
+ /** Initial markdown to seed the editor. */
618
+ initialValue?: string;
619
+ /** Called when the user submits. */
620
+ onSend?: (payload: ComposerSubmitPayload) => void;
621
+ /** Called when the stop button is clicked while `isStreaming`. */
622
+ onStop?: () => void;
623
+ isStreaming?: boolean;
624
+ /** Focus the editor on mount. Defaults to `false`. */
625
+ autoFocus?: boolean;
626
+ /**
627
+ * Return focus to the editor after a successful send. Defaults to `true`.
628
+ *
629
+ * Sends triggered by the keyboard (Enter, Cmd/Ctrl+Enter) already keep
630
+ * focus naturally; this prop guarantees the same after Send-button
631
+ * clicks, quick-prompt `sendValue`, or imperative `ref.submit()` so the
632
+ * user can keep typing without a second click. Set to `false` if your
633
+ * UX moves focus elsewhere on send (e.g. a confirmation modal).
634
+ */
635
+ refocusOnSubmit?: boolean;
636
+ /**
637
+ * Global keyboard shortcut that focuses the composer from anywhere on
638
+ * the page. Defaults to `"mod+/"` — `mod` resolves to ⌘ on macOS and
639
+ * Ctrl on Windows/Linux. Pass any combo of `mod` / `cmd` / `meta` /
640
+ * `ctrl` / `alt` / `option` / `shift` plus a single key, separated by
641
+ * `+` (e.g. `"mod+k"`, `"shift+mod+/"`, `"alt+l"`).
642
+ *
643
+ * Set to `false` (or `null`) to disable. The listener is registered on
644
+ * `window` and ignored while a popover / mention / slash menu intercepts
645
+ * the key first.
646
+ */
647
+ focusShortcut?: string | false | null;
648
+ placeholder?: string;
649
+ /**
650
+ * Shorthand for `classNames.root`. Kept for back-compat; if both are set,
651
+ * the two are merged (`className` first, then `classNames.root`).
652
+ */
653
+ className?: string;
654
+ /**
655
+ * Per-slot className overrides. See {@link ComposerSlotClassNames}.
656
+ */
657
+ classNames?: ComposerSlotClassNames;
658
+ /**
659
+ * Per-slot inline-style overrides converted by the lightweight sx engine.
660
+ * See {@link ComposerSxMap}.
661
+ */
662
+ sx?: ComposerSxMap;
663
+ /**
664
+ * Standard React `style` applied to the outer root in addition to (and
665
+ * after) any `sx.root` resolved values. Use for dimensions/positioning of
666
+ * the composer as a whole.
667
+ */
668
+ style?: CSSProperties;
669
+ /**
670
+ * Design tokens applied as inline CSS custom properties on the root, so
671
+ * they cascade into every slot — including the package's built-in CSS —
672
+ * without leaking to the rest of the app. See {@link ComposerTokens}.
673
+ */
674
+ tokens?: ComposerTokens;
675
+ /**
676
+ * Single brand color shorthand. Accepts any of:
677
+ * - HSL components (preferred): `"258 90% 62%"`
678
+ * - hex: `"#7c3aed"` / `"#abc"`
679
+ * - `rgb(...)` / `rgba(...)`
680
+ * - `hsl(...)` / `hsla(...)`
681
+ *
682
+ * Internally derives `--primary`, `--primary-foreground`, `--accent`,
683
+ * `--accent-foreground`, and `--ring` from this one value. That re-tints
684
+ * every "hot" surface — hover backgrounds, selected menu rows, mention
685
+ * chips, the mention-list avatar, the Web pill, focus ring — without
686
+ * touching the neutral chrome (card, border, foreground text).
687
+ *
688
+ * Anything you also pass via `tokens` overrides the derived value, so
689
+ * `color` is just a sensible default you can fine-tune.
690
+ *
691
+ * @example
692
+ * ```tsx
693
+ * <Composer color="#7c3aed" />
694
+ * ```
695
+ */
696
+ color?: string;
697
+ /** Hint text under the composer. `false` hides; `true` shows default. */
698
+ hint?: boolean | ReactNode;
699
+ /**
700
+ * Editor mode.
701
+ * - `"markdown"` (default): rich-text editor that understands markdown.
702
+ * Enables block shortcuts (`# `, `- `, `> `, ```` ``` ````), inline
703
+ * live styling (`**bold**`, `*italic*`, etc.), mermaid previews, and
704
+ * paste-as-rich-text. The serialized payload contains markdown.
705
+ * - `"text"`: plain-text editor. No markdown styling, no block shortcuts,
706
+ * no mermaid. Pasted content is reduced to plain text. The serialized
707
+ * `markdown` field of `onSend` simply equals the text.
708
+ *
709
+ * Mentions, slash commands, attachments and voice are independent of
710
+ * `mode` and work in both.
711
+ */
712
+ mode?: "markdown" | "text";
713
+ /** Toggle / configure built-in plugins. */
714
+ features?: ComposerFeatures;
715
+ /** Extra controls rendered after the built-in toolbar buttons. */
716
+ toolbarExtras?: ReactNode;
717
+ /**
718
+ * Close any open typeahead menu (slash, mentions) when the user clicks
719
+ * or taps outside the composer. Defaults to `true`. Set `false` to keep
720
+ * the menu open until the user dismisses it explicitly (Escape, selecting
721
+ * an item, or moving the caret past the trigger).
722
+ */
723
+ closeMenusOnOutsideClick?: boolean;
724
+ /**
725
+ * Whether the editor is allowed to hold more than one line of content.
726
+ * Defaults to `true`. When `false`, the composer behaves like a
727
+ * single-line input: Enter never inserts a newline (Shift+Enter is also
728
+ * suppressed), and `smartNewline` is implicitly disabled.
729
+ */
730
+ multiline?: boolean;
731
+ /**
732
+ * Whether pressing Enter (no modifiers) submits the message. Defaults to
733
+ * `true`. When `false`, Enter only inserts a newline (assuming
734
+ * `multiline` is `true`); the user can still submit with
735
+ * Cmd/Ctrl+Enter or via the Send button / imperative `ref.submit()`.
736
+ */
737
+ submitOnEnter?: boolean;
738
+ /**
739
+ * Smart "context-aware" Enter behavior. Defaults to `true`. Only takes
740
+ * effect when `multiline` is `true`.
741
+ *
742
+ * - While the editor holds a single line, Enter submits as usual
743
+ * (subject to `submitOnEnter`).
744
+ * - As soon as the editor contains more than one line, Enter inserts
745
+ * a newline instead of submitting, and the user must press
746
+ * Cmd/Ctrl+Enter (or click Send) to submit. This prevents
747
+ * accidental sends while composing longer messages.
748
+ * - In markdown mode, when the cursor sits inside a list paragraph
749
+ * (`- `, `* `, `+ `, or `N. `), Enter continues the list with the
750
+ * next marker (bullet character preserved, numbers
751
+ * auto-incremented). Pressing Enter on an empty list item exits
752
+ * the list — the marker is cleared and the cursor stays on the
753
+ * now-plain line.
754
+ *
755
+ * Set to `false` to keep Enter's behavior fixed regardless of how many
756
+ * lines the user has typed or whether they're inside a list.
757
+ */
758
+ smartNewline?: boolean;
759
+ /**
760
+ * Override any built-in icon with your own React component. The library
761
+ * ships small lucide-style SVGs by default; provide your own to match
762
+ * your design system (Heroicons, Phosphor, Material, etc). Any slot you
763
+ * leave out keeps its default.
764
+ *
765
+ * @example
766
+ * ```tsx
767
+ * <Composer icons={{ send: MyArrowIcon, voice: MyMicIcon }} />
768
+ * ```
769
+ */
770
+ icons?: Partial<ComposerIcons>;
771
+ /**
772
+ * Component-level overrides for chrome pieces (currently `sendButton`
773
+ * and `stopButton`). Each entry, when provided, REPLACES the library's
774
+ * default element while still receiving the runtime data it would have
775
+ * used (callbacks, gating flags, resolved styles). See {@link ComposerSlots}
776
+ * and the per-slot render-prop interfaces ({@link SendButtonRenderProps},
777
+ * {@link StopButtonRenderProps}) for the contract.
778
+ *
779
+ * Composes with `icons` / `classNames` / `sx`: use those for cosmetic
780
+ * tweaks, reach for `slots` only when you need different DOM, behaviour,
781
+ * or accessibility wrapping (e.g. a tooltip-wrapped button, a split
782
+ * "Send / Send & schedule" dropdown, a labelled pill).
783
+ *
784
+ * @example
785
+ * ```tsx
786
+ * <Composer
787
+ * slots={{
788
+ * sendButton: ({ canSend, onSend }) => (
789
+ * <MyBrandButton disabled={!canSend} onClick={onSend}>
790
+ * Send <kbd>⏎</kbd>
791
+ * </MyBrandButton>
792
+ * ),
793
+ * }}
794
+ * />
795
+ * ```
796
+ */
797
+ slots?: ComposerSlots;
798
+ /**
799
+ * Render mermaid (or other future diagram-language) fences yourself instead
800
+ * of relying on the lazy-imported `mermaid` package. Recommended for
801
+ * production apps that already have a diagram pipeline — set this and you
802
+ * can omit `mermaid` from your install entirely.
803
+ *
804
+ * Receives the raw fenced code and the language tag, returns any ReactNode.
805
+ *
806
+ * @example
807
+ * ```tsx
808
+ * <Composer
809
+ * renderDiagram={({ code }) => <MyDiagram source={code} />}
810
+ * />
811
+ * ```
812
+ */
813
+ renderDiagram?: DiagramRenderer;
814
+ /**
815
+ * "Starter" prompts shown as a clickable chip row above the composer.
816
+ * Clicking a chip either fills the editor (`behavior: "initValue"`) or
817
+ * fills + submits (`behavior: "sendValue"`, default). Useful as a
818
+ * zero-typing entry point on an empty chat surface.
819
+ *
820
+ * @example
821
+ * ```tsx
822
+ * <Composer
823
+ * prompts={{
824
+ * items: [
825
+ * "Summarize today's stand-up",
826
+ * "Draft a release email",
827
+ * "Find bugs in this snippet",
828
+ * "Brainstorm names for my project",
829
+ * "Plan next sprint",
830
+ * ],
831
+ * maxToShow: 3, // show 3 chips out of 5
832
+ * randomize: true, // pick a different 3 each mount
833
+ * behavior: "sendValue", // click → fill + submit
834
+ * onSelect: (p) => track("prompt_picked", { p }),
835
+ * }}
836
+ * />
837
+ * ```
838
+ */
839
+ prompts?: ComposerPromptsConfig;
840
+ /**
841
+ * Attachment lifecycle and submission rules — `uploadFirst` for
842
+ * server-side upload pipelines (spinner + warning chip states),
843
+ * `onUpload` for the callback, `canSendOnlyAttachment` to require
844
+ * text alongside attachments. See {@link AttachmentOptions}.
845
+ *
846
+ * @example
847
+ * ```tsx
848
+ * <Composer
849
+ * features={{ attachments: true }}
850
+ * attachmentOptions={{
851
+ * uploadFirst: true,
852
+ * onUpload: async (file) => (await fetch("/api/upload", { method: "POST", body: file })).ok,
853
+ * canSendOnlyAttachment: false,
854
+ * }}
855
+ * />
856
+ * ```
857
+ */
858
+ attachmentOptions?: AttachmentOptions;
859
+ /**
860
+ * Writing direction. The composer's chrome (toolbar, attachment tray,
861
+ * popovers, hint, mention chips, send button) is laid out with CSS
862
+ * logical properties so a single attribute flips left↔right correctly.
863
+ *
864
+ * - `"ltr"` (default behaviour when omitted): left-to-right.
865
+ * - `"rtl"`: right-to-left — Arabic, Hebrew, Persian, Urdu, …
866
+ * - `"auto"`: the browser picks per-paragraph based on the first
867
+ * strong character. Useful for chat surfaces that mix scripts.
868
+ *
869
+ * Applied to the outer composer root *and* the Lexical contenteditable so
870
+ * caret movement, text alignment, and per-line direction (in `"auto"`
871
+ * mode) all behave correctly.
872
+ *
873
+ * @example
874
+ * ```tsx
875
+ * <Composer dir="rtl" placeholder="اكتب رسالة..." onSend={...} />
876
+ * ```
877
+ */
878
+ dir?: "ltr" | "rtl" | "auto";
879
+ }
880
+
881
+ /**
882
+ * ComposeAI — a Lexical-powered, plugin-driven rich input designed for
883
+ * chat / AI assistant interfaces. Internally stateful: parents only listen via
884
+ * `onSend` and (optionally) hold an imperative `ref`.
885
+ */
886
+ declare const Composer: react.ForwardRefExoticComponent<ComposerProps & react.RefAttributes<ComposerHandle>>;
887
+
888
+ interface SuggestionRowProps {
889
+ /** Suggestion labels. Clicking one calls `onSelect` with that string. */
890
+ items: string[];
891
+ onSelect: (value: string) => void;
892
+ className?: string;
893
+ }
894
+ declare function SuggestionRow({ items, onSelect, className }: SuggestionRowProps): react.JSX.Element;
895
+
896
+ export { type Attachment, type AttachmentKind, type AttachmentOptions, type AttachmentStatus, type AttachmentTypeOption, type AttachmentsConfig, Composer, type ComposerFeatures, type ComposerHandle, type ComposerIcons, type ComposerPromptBehavior, type ComposerPromptsConfig, type ComposerProps, type ComposerSlot, type ComposerSlotClassNames, type ComposerSlots, type ComposerSubmitPayload, type ComposerSxMap, type ComposerSxValue, type ComposerTokens, type DiagramRenderer, type GhostedAutoCompleteConfig, type IconComponent, type IconProps, type MarkdownConfig, type MarkdownMode, type MentionConfig, type MentionItem, type MentionRef, type MermaidConfig, type SendButtonRenderProps, type SlashCommand, type SlashCommandContext, type SlashConfig, type StopButtonRenderProps, SuggestionRow, type SuggestionRowProps };