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.
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "composeai",
3
+ "version": "0.1.1",
4
+ "description": "The modern React composer for AI applications — a drop-in Lexical-powered chat input with markdown, mentions, slash commands, attachments, voice, streaming stop, and opt-in plugins for copilots, chatbots, and agent UIs.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./composer.css": "./src/composer.css"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src/composer.css",
20
+ "README.md"
21
+ ],
22
+ "sideEffects": [
23
+ "./src/composer.css"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "typecheck": "tsc --noEmit",
29
+ "clean": "rm -rf dist"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^18.0.0 || ^19.0.0",
33
+ "react-dom": "^18.0.0 || ^19.0.0",
34
+ "mermaid": "^10 || ^11"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "mermaid": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "@lexical/react": "^0.45.0",
43
+ "lexical": "^0.45.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/react": "^18.3.12",
47
+ "@types/react-dom": "^18.3.1",
48
+ "react": "^18.3.1",
49
+ "react-dom": "^18.3.1",
50
+ "tsup": "^8.3.0",
51
+ "typescript": "^5.6.3"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "keywords": [
57
+ "react",
58
+ "composeai",
59
+ "composer",
60
+ "ai-composer",
61
+ "ai-chat",
62
+ "chat-input",
63
+ "chatbox",
64
+ "copilot",
65
+ "llm",
66
+ "agent-ui",
67
+ "lexical",
68
+ "markdown",
69
+ "mentions",
70
+ "slash-commands",
71
+ "attachments"
72
+ ],
73
+ "license": "MIT",
74
+ "homepage": "https://igbaryya.github.io/react-comporsor/",
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+https://github.com/igbaryya/react-comporsor.git"
78
+ },
79
+ "bugs": {
80
+ "url": "https://github.com/igbaryya/react-comporsor/issues"
81
+ }
82
+ }
@@ -0,0 +1,481 @@
1
+ /* ComposeAI — component styles.
2
+ *
3
+ * Plain CSS (no Tailwind required) keyed off the CSS variables every consumer
4
+ * defines for their theme:
5
+ * --background, --foreground, --primary, --primary-foreground,
6
+ * --muted, --muted-foreground, --border, --card, --card-foreground,
7
+ * --popover, --popover-foreground, --accent, --accent-foreground,
8
+ * --destructive, --success, --warning
9
+ * Values are HSL components (e.g. `258 90% 62%`) so utilities can compose
10
+ * with opacities via `hsl(var(--primary) / 0.1)`.
11
+ */
12
+
13
+ /* ----------------------------------------------------------------------------
14
+ * Composer-wide custom properties.
15
+ *
16
+ * These are READ by both the package CSS below and the Tailwind utility
17
+ * classes the components use (`bg-card`, `rounded-[28px]`, etc.). They are
18
+ * SET inline on the composer root by `<Composer />` itself when the consumer
19
+ * passes `tokens={...}`, so they only affect that one composer instance —
20
+ * never the consumer app's global theme.
21
+ *
22
+ * --composer-radius outer card corner radius (default 28px)
23
+ * --composer-font-size editor base font size (default 15px)
24
+ * --composer-font-family editor font family (default inherit)
25
+ * ------------------------------------------------------------------------- */
26
+
27
+ [data-composer-root] {
28
+ border-radius: var(--composer-radius, 28px);
29
+ }
30
+
31
+ /* The gradient overlay and drop overlay sit absolutely inside the card and
32
+ * must mirror the outer corner radius. They no longer need a hardcoded
33
+ * value of their own. */
34
+ [data-composer-root] > [data-composer-overlay] {
35
+ border-radius: inherit;
36
+ }
37
+
38
+ [data-composer-root] .composer-editor {
39
+ font-size: var(--composer-font-size, 15px);
40
+ font-family: var(--composer-font-family, inherit);
41
+ }
42
+
43
+ .composer-editor {
44
+ position: relative;
45
+ display: block;
46
+ width: 100%;
47
+ font-size: 15px;
48
+ line-height: 1.6;
49
+ color: hsl(var(--foreground));
50
+ outline: none;
51
+ overflow-wrap: anywhere;
52
+ }
53
+
54
+ .composer-paragraph {
55
+ margin: 0;
56
+ }
57
+ .composer-paragraph + .composer-paragraph {
58
+ margin-top: 0.5rem;
59
+ }
60
+
61
+ .composer-quote {
62
+ margin: 0.5rem 0;
63
+ padding-inline-start: 0.75rem;
64
+ border-inline-start: 2px solid hsl(var(--border));
65
+ color: hsl(var(--muted-foreground));
66
+ }
67
+
68
+ .composer-h1 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; margin: 0.75rem 0 0.5rem; }
69
+ .composer-h2 { font-size: 1.125rem; font-weight: 600; letter-spacing: -0.01em; margin: 0.75rem 0 0.5rem; }
70
+ .composer-h3 { font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; margin: 0.625rem 0 0.375rem; }
71
+ .composer-h4 { font-size: 0.9375rem; font-weight: 600; margin: 0.5rem 0 0.375rem; }
72
+ .composer-h5 { font-size: 0.875rem; font-weight: 600; margin: 0.5rem 0 0.25rem; }
73
+ .composer-h6 { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin: 0.5rem 0 0.25rem; }
74
+
75
+ .composer-ul {
76
+ margin: 0.25rem 0;
77
+ padding-inline-start: 1.5rem;
78
+ list-style: disc;
79
+ }
80
+ .composer-ol {
81
+ margin: 0.25rem 0;
82
+ padding-inline-start: 1.5rem;
83
+ list-style: decimal;
84
+ }
85
+ .composer-ul > li + li,
86
+ .composer-ol > li + li {
87
+ margin-top: 0.125rem;
88
+ }
89
+ .composer-ul::marker,
90
+ .composer-ol::marker,
91
+ .composer-li::marker {
92
+ color: hsl(var(--muted-foreground));
93
+ }
94
+ .composer-li {
95
+ padding-inline-start: 0.25rem;
96
+ }
97
+ .composer-li-nested {
98
+ list-style: circle;
99
+ }
100
+
101
+ .composer-bold { font-weight: 600; }
102
+ .composer-italic { font-style: italic; }
103
+ .composer-underline { text-decoration: underline; text-underline-offset: 2px; }
104
+ .composer-strike { text-decoration: line-through; }
105
+
106
+ .composer-code {
107
+ padding: 0.0625rem 0.25rem;
108
+ border-radius: 0.25rem;
109
+ border: 1px solid hsl(var(--border));
110
+ background: hsl(var(--muted));
111
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
112
+ font-size: 13px;
113
+ }
114
+ .composer-code-block {
115
+ display: block;
116
+ margin: 0.5rem 0;
117
+ padding: 0.5rem 0.75rem;
118
+ border-radius: 0.5rem;
119
+ border: 1px solid hsl(var(--border));
120
+ background: hsl(var(--muted));
121
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
122
+ font-size: 13px;
123
+ line-height: 1.6;
124
+ overflow-x: auto;
125
+ }
126
+
127
+ .composer-link {
128
+ color: hsl(var(--primary));
129
+ text-decoration: underline;
130
+ text-underline-offset: 2px;
131
+ }
132
+
133
+ .composer-md-token {
134
+ color: hsl(var(--muted-foreground) / 0.5);
135
+ }
136
+
137
+ /* ----------------------------------------------------------------------------
138
+ * Slack-style block markdown.
139
+ * Paragraphs get a `data-md-block` attribute set by MarkdownPlugin based on
140
+ * the leading marker on their line. We style the entire paragraph for blocks
141
+ * while leaving the markers visible (rendered as muted tokens by the plugin).
142
+ * ------------------------------------------------------------------------- */
143
+
144
+ /* Headings: font-scale/weight applied via `::first-line` so the heading
145
+ * styling stays confined to the marker's line even when the user inserts
146
+ * soft line breaks (Shift+Enter) inside the same paragraph. The margins
147
+ * stay on the paragraph itself because `::first-line` cannot affect box
148
+ * properties. */
149
+ .composer-paragraph[data-md-block="heading-1"] {
150
+ margin-top: 0.5rem;
151
+ margin-bottom: 0.125rem;
152
+ }
153
+ .composer-paragraph[data-md-block="heading-1"]::first-line {
154
+ font-size: 1.625rem;
155
+ line-height: 1.2;
156
+ font-weight: 700;
157
+ letter-spacing: -0.015em;
158
+ }
159
+
160
+ .composer-paragraph[data-md-block="heading-2"] {
161
+ margin-top: 0.5rem;
162
+ margin-bottom: 0.125rem;
163
+ }
164
+ .composer-paragraph[data-md-block="heading-2"]::first-line {
165
+ font-size: 1.375rem;
166
+ line-height: 1.25;
167
+ font-weight: 700;
168
+ letter-spacing: -0.012em;
169
+ }
170
+
171
+ .composer-paragraph[data-md-block="heading-3"] {
172
+ margin-top: 0.375rem;
173
+ }
174
+ .composer-paragraph[data-md-block="heading-3"]::first-line {
175
+ font-size: 1.1875rem;
176
+ line-height: 1.3;
177
+ font-weight: 700;
178
+ }
179
+
180
+ .composer-paragraph[data-md-block="heading-4"] {
181
+ margin-top: 0.25rem;
182
+ }
183
+ .composer-paragraph[data-md-block="heading-4"]::first-line {
184
+ font-size: 1.0625rem;
185
+ line-height: 1.35;
186
+ font-weight: 700;
187
+ }
188
+
189
+ .composer-paragraph[data-md-block="heading-5"]::first-line {
190
+ font-size: 0.9375rem;
191
+ line-height: 1.4;
192
+ font-weight: 700;
193
+ }
194
+
195
+ .composer-paragraph[data-md-block="heading-6"]::first-line {
196
+ font-size: 0.8125rem;
197
+ line-height: 1.4;
198
+ font-weight: 700;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.06em;
201
+ color: hsl(var(--muted-foreground));
202
+ }
203
+
204
+ .composer-paragraph[data-md-block="quote"] {
205
+ position: relative;
206
+ padding-inline-start: 0.875rem;
207
+ margin-inline-start: 0.125rem;
208
+ color: hsl(var(--muted-foreground));
209
+ font-style: italic;
210
+ }
211
+ .composer-paragraph[data-md-block="quote"]::before {
212
+ content: "";
213
+ position: absolute;
214
+ inset-inline-start: 0;
215
+ top: 0.2rem;
216
+ bottom: 0.2rem;
217
+ width: 3px;
218
+ border-radius: 9999px;
219
+ background: hsl(var(--primary) / 0.55);
220
+ }
221
+
222
+ /* Lists: visible indentation rail, marker recolored so it reads as an active
223
+ * bullet rather than a stray dash. Marker stays in flow so the caret can
224
+ * traverse it naturally. `text-indent` is direction-aware in modern engines
225
+ * (it indents from the line's start edge) so the negative value pulls the
226
+ * marker into the gutter on both sides. */
227
+ .composer-paragraph[data-md-block="list-bullet"],
228
+ .composer-paragraph[data-md-block="list-numbered"] {
229
+ padding-inline-start: 1.25rem;
230
+ text-indent: -0.875rem;
231
+ }
232
+ .composer-paragraph[data-md-block="list-bullet"] > .composer-md-token:first-child,
233
+ .composer-paragraph[data-md-block="list-numbered"] > .composer-md-token:first-child {
234
+ color: hsl(var(--primary) / 0.85);
235
+ font-weight: 600;
236
+ }
237
+ .composer-paragraph[data-md-block="list-bullet"] + .composer-paragraph[data-md-block="list-bullet"],
238
+ .composer-paragraph[data-md-block="list-numbered"] + .composer-paragraph[data-md-block="list-numbered"] {
239
+ margin-top: 0.125rem;
240
+ }
241
+
242
+ .composer-paragraph[data-md-block="code-fence-open"],
243
+ .composer-paragraph[data-md-block="code-fence-close"],
244
+ .composer-paragraph[data-md-block="code-line"] {
245
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
246
+ font-size: 13px;
247
+ line-height: 1.55;
248
+ background: hsl(var(--muted));
249
+ padding: 0.125rem 0.625rem;
250
+ border-inline-start: 3px solid hsl(var(--primary) / 0.4);
251
+ }
252
+ .composer-paragraph[data-md-block^="code-"] + .composer-paragraph[data-md-block^="code-"] {
253
+ margin-top: 0;
254
+ }
255
+ .composer-paragraph[data-md-block="code-fence-open"] {
256
+ padding-top: 0.375rem;
257
+ border-start-start-radius: 0.375rem;
258
+ border-start-end-radius: 0.375rem;
259
+ color: hsl(var(--muted-foreground));
260
+ }
261
+ .composer-paragraph[data-md-block="code-fence-close"] {
262
+ padding-bottom: 0.375rem;
263
+ border-end-start-radius: 0.375rem;
264
+ border-end-end-radius: 0.375rem;
265
+ color: hsl(var(--muted-foreground));
266
+ }
267
+ /* Live-mode fence lines: the visible body is empty (the `` ``` `` chars are
268
+ * stashed on the BlockParagraphNode). Optional language tag from
269
+ * `data-md-lang` is surfaced as a small label so the user can still see
270
+ * what language they typed. The default `<br>` Lexical inserts in empty
271
+ * paragraphs gives the line just enough height to be selectable / clickable. */
272
+ .composer-paragraph[data-md-block="code-fence-open"][data-md-lang]::before {
273
+ content: attr(data-md-lang);
274
+ display: inline-block;
275
+ font-size: 11px;
276
+ color: hsl(var(--muted-foreground) / 0.7);
277
+ margin-inline-end: 0.5rem;
278
+ }
279
+
280
+ .composer-paragraph[data-md-block="hr"] {
281
+ position: relative;
282
+ color: transparent;
283
+ user-select: none;
284
+ margin: 0.5rem 0;
285
+ height: 1px;
286
+ }
287
+ .composer-paragraph[data-md-block="hr"]::after {
288
+ content: "";
289
+ position: absolute;
290
+ inset-inline: 0;
291
+ top: 50%;
292
+ border-top: 1px solid hsl(var(--border));
293
+ }
294
+ .composer-paragraph[data-md-block="hr"] .composer-md-token {
295
+ visibility: hidden;
296
+ }
297
+
298
+ /* Headings: muted leading `#` tokens get a bit more presence so they still
299
+ * read as scaffolding rather than disappearing into the body type. */
300
+ .composer-paragraph[data-md-block^="heading-"] > .composer-md-token:first-child {
301
+ color: hsl(var(--muted-foreground) / 0.55);
302
+ font-weight: 600;
303
+ }
304
+
305
+ .composer-mention {
306
+ display: inline;
307
+ padding: 0.0625rem 0.375rem;
308
+ border-radius: 0.375rem;
309
+ background: hsl(var(--primary) / 0.1);
310
+ color: hsl(var(--primary));
311
+ font-weight: 500;
312
+ /* The chip wraps an editable TextNode — make sure its child looks
313
+ and acts like part of the chip surface. */
314
+ white-space: pre;
315
+ }
316
+
317
+ /* Trigger character ("@" / "#" / …) is rendered as a pseudo-element so
318
+ it is visible but not part of the editable text content. Backspacing
319
+ can only ever shrink the label inside the chip — the trigger glyph
320
+ stays put until the entire chip is removed (which happens
321
+ automatically when the label becomes empty, via
322
+ MentionNode#canBeEmpty = false). */
323
+ .composer-mention::before {
324
+ content: attr(data-mention-prefix);
325
+ user-select: none;
326
+ }
327
+
328
+ .composer-token-attr { color: hsl(var(--primary)); }
329
+ .composer-token-comment { color: hsl(var(--muted-foreground)); font-style: italic; }
330
+ .composer-token-function { color: hsl(var(--primary)); }
331
+ .composer-token-operator { color: hsl(var(--muted-foreground)); }
332
+ .composer-token-property { color: hsl(var(--foreground)); }
333
+ .composer-token-punctuation { color: hsl(var(--muted-foreground)); }
334
+ .composer-token-selector { color: hsl(var(--success)); }
335
+ .composer-token-variable { color: hsl(var(--warning)); }
336
+
337
+ /* Scoped thin scrollbar for the editor's max-height area. */
338
+ .composer-editor {
339
+ scrollbar-width: thin;
340
+ scrollbar-color: hsl(var(--border)) transparent;
341
+ }
342
+ .composer-editor::-webkit-scrollbar {
343
+ width: 6px;
344
+ height: 6px;
345
+ }
346
+ .composer-editor::-webkit-scrollbar-track {
347
+ background: transparent;
348
+ }
349
+ .composer-editor::-webkit-scrollbar-thumb {
350
+ background: hsl(var(--border));
351
+ border-radius: 9999px;
352
+ }
353
+
354
+ /* Soft mount animation for small popovers (attachment type picker, …).
355
+ Subtle fade + 4px slide-down so the dropdown feels tied to its trigger
356
+ instead of popping abruptly into existence. */
357
+ @keyframes composer-popover-in {
358
+ from {
359
+ opacity: 0;
360
+ transform: translateY(-4px);
361
+ }
362
+ to {
363
+ opacity: 1;
364
+ transform: translateY(0);
365
+ }
366
+ }
367
+ .composer-popover-in {
368
+ animation: composer-popover-in 160ms ease-out;
369
+ }
370
+
371
+ /* ----------------------------------------------------------------------------
372
+ * Ghosted autocomplete overlay.
373
+ *
374
+ * GhostedAutoCompletePlugin portals a non-interactive div into the
375
+ * `.composer-editor-block` container. It contains:
376
+ * [.composer-ghost-overlay-typed] — invisible spacer matching what the
377
+ * user typed so the suggestion lines
378
+ * up exactly with the caret.
379
+ * [.composer-ghost-suggestion] — the muted "ghost" remainder.
380
+ *
381
+ * The overlay sits BEHIND the contenteditable (`z-index: 0` vs the
382
+ * editor's stacking-context-implicit `auto`) and is `pointer-events:
383
+ * none` so it never intercepts clicks/selection. The padding utility
384
+ * classes set on the overlay element MUST mirror the editor's padding —
385
+ * `EditorShell.tsx`'s `editorClass` is the source of truth.
386
+ * ------------------------------------------------------------------------- */
387
+
388
+ .composer-ghost-overlay {
389
+ font-size: var(--composer-font-size, 15px);
390
+ font-family: var(--composer-font-family, inherit);
391
+ line-height: 1.6;
392
+ /* `pre-wrap`: preserves spaces and lets a long suggestion wrap onto
393
+ * the next line. `break-word` is the modern alias of `word-wrap` —
394
+ * needed so an unbroken URL-style suggestion can still wrap inside
395
+ * the editor's bounds.
396
+ * Color is transparent on the container; only the suggestion span
397
+ * re-colors itself. The typed span stays transparent so it occupies
398
+ * caret-aligned space without bleeding any text into the overlay. */
399
+ white-space: pre-wrap;
400
+ word-wrap: break-word;
401
+ color: transparent;
402
+ /* Sit behind the contenteditable so the caret blink and selection
403
+ * highlight (which Lexical paints on the editor itself) stay on top. */
404
+ z-index: 0;
405
+ }
406
+
407
+ .composer-ghost-overlay-typed {
408
+ /* `visibility: hidden` keeps the text in the box-model — its width
409
+ * pushes the suggestion span to the right place — but never paints. */
410
+ visibility: hidden;
411
+ }
412
+
413
+ .composer-ghost-suggestion {
414
+ color: hsl(var(--muted-foreground) / 0.6);
415
+ /* Just enough emphasis to look like a suggestion (slightly italic
416
+ * reads as "this isn't your text yet"). Skip if your design system
417
+ * prefers a flat muted look. */
418
+ font-style: italic;
419
+ }
420
+
421
+ /* Inline-layout overrides: mirror the editor's 36px row line-height so
422
+ * the suggestion sits on the same baseline as the typed text. */
423
+ .composer-ghost-overlay.composer-ghost-overlay--inline {
424
+ line-height: 2.25rem;
425
+ white-space: nowrap;
426
+ }
427
+
428
+ /* ----------------------------------------------------------------------------
429
+ * Inline / single-line layout.
430
+ *
431
+ * Activated when `<Composer multiline={false} />`. The card collapses from a
432
+ * stacked column (attachments / editor / toolbar+send / mermaid) to a single
433
+ * horizontal bar (toolbar | editor | send), with the attachment tray and the
434
+ * drag overlay still rendering above where applicable.
435
+ *
436
+ * Wins over `--composer-radius` on the card itself because this rule
437
+ * targets the same element with an extra attribute, raising specificity.
438
+ * ------------------------------------------------------------------------- */
439
+
440
+ [data-composer-root][data-composer-inline] {
441
+ border-radius: 9999px;
442
+ }
443
+
444
+ /* The contenteditable in inline mode needs its own line-height because the
445
+ * base `.composer-editor` rule (specificity 0,1,0) defaults to `1.6`, which
446
+ * shrinks the line box to ~24px and leaves typed text top-aligned inside
447
+ * the 36px row while the absolutely-positioned placeholder (its own
448
+ * line-height) appears centered — a visible mismatch the moment the user
449
+ * starts typing. The compound-class selector below raises specificity to
450
+ * 0,2,0 so the inline override always wins regardless of stylesheet load
451
+ * order, with the line-height matching the 36px row height so a single
452
+ * line of text is perfectly centered. */
453
+ .composer-editor.composer-editor--inline {
454
+ line-height: 2.25rem; /* matches h-9 / leading-9 (36px) */
455
+ scrollbar-width: none;
456
+ }
457
+ .composer-editor.composer-editor--inline::-webkit-scrollbar {
458
+ display: none;
459
+ }
460
+
461
+ /* Lexical wraps every line in a block-level <p>. In inline mode we collapse
462
+ * those to inline so the editor scrolls horizontally instead of stacking
463
+ * vertically, and the inline content baseline aligns to the parent line
464
+ * box (centered, thanks to the 36px line-height above). */
465
+ .composer-editor--inline .composer-paragraph,
466
+ .composer-editor--inline p {
467
+ display: inline;
468
+ margin: 0;
469
+ white-space: nowrap;
470
+ line-height: inherit;
471
+ }
472
+ .composer-editor--inline .composer-paragraph + .composer-paragraph {
473
+ margin-top: 0;
474
+ }
475
+
476
+ /* The MermaidSlot is already gated out by the React tree when multiline is
477
+ * false, but this defensive rule keeps any stray `[data-mermaid-tile]` from
478
+ * showing if a consumer renders one manually inside an inline composer. */
479
+ [data-composer-root][data-composer-inline] [data-mermaid-tile] {
480
+ display: none;
481
+ }