composeai 0.1.5 → 0.1.8

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { createContext, forwardRef, useMemo, useState, useRef, useCallback, useEffect, useContext, useLayoutEffect, useId, isValidElement, cloneElement } from 'react';
1
+ import { createContext, forwardRef, useMemo, useState, useRef, useCallback, useEffect, useContext, useId, isValidElement, cloneElement, useLayoutEffect } from 'react';
2
2
  import { LexicalComposer } from '@lexical/react/LexicalComposer';
3
3
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
- import { ParagraphNode, $getRoot, $createParagraphNode, ElementNode, $createTextNode, TextNode, addClassNamesToElement, $isElementNode, $isTextNode, $isParagraphNode, KEY_ENTER_COMMAND, $getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, PASTE_COMMAND, KEY_BACKSPACE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, KEY_ESCAPE_COMMAND, $applyNodeReplacement, $isLineBreakNode } from 'lexical';
4
+ import { ParagraphNode, $getRoot, $createParagraphNode, ElementNode, $createTextNode, TextNode, addClassNamesToElement, $isElementNode, $isTextNode, $isParagraphNode, $getSelection, $isRangeSelection, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, PASTE_COMMAND, KEY_BACKSPACE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, KEY_ESCAPE_COMMAND, $applyNodeReplacement, $isLineBreakNode } from 'lexical';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
  import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
7
7
  import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
@@ -393,6 +393,13 @@ var IconAttach = makeIcon(
393
393
  /* @__PURE__ */ jsx("path", { d: "m16 6-8.414 8.586a2 2 0 0 0 0 2.828 2 2 0 0 0 2.828 0l8.414-8.586a4 4 0 0 0 0-5.656 4 4 0 0 0-5.656 0l-8.415 8.585a6 6 0 1 0 8.486 8.486" })
394
394
  ] })
395
395
  );
396
+ var IconPlus = makeIcon(
397
+ "IconPlus",
398
+ /* @__PURE__ */ jsxs(Fragment, { children: [
399
+ /* @__PURE__ */ jsx("path", { d: "M12 5v14" }),
400
+ /* @__PURE__ */ jsx("path", { d: "M5 12h14" })
401
+ ] })
402
+ );
396
403
  var IconImage = makeIcon(
397
404
  "IconImage",
398
405
  /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -481,6 +488,7 @@ var DEFAULT_ICONS = {
481
488
  send: IconSend,
482
489
  stop: IconStop,
483
490
  attach: IconAttach,
491
+ plus: IconPlus,
484
492
  image: IconImage,
485
493
  voice: IconVoice,
486
494
  voiceRecording: IconVoiceRecording,
@@ -506,6 +514,8 @@ var DEFAULT_FEATURES = {
506
514
  voice: true,
507
515
  mermaid: true,
508
516
  web: true,
517
+ // No consumer-defined actions by default.
518
+ custom: [],
509
519
  // Off by default — ghost autocomplete is an opt-in input affordance that
510
520
  // only makes sense when the consumer has a curated list of completions.
511
521
  ghostedAutoComplete: false
@@ -537,6 +547,7 @@ function normalizeFeatures(features) {
537
547
  voice: features.voice ?? DEFAULT_FEATURES.voice,
538
548
  mermaid: features.mermaid ?? DEFAULT_FEATURES.mermaid,
539
549
  web: features.web ?? DEFAULT_FEATURES.web,
550
+ custom: features.custom ?? DEFAULT_FEATURES.custom,
540
551
  ghostedAutoComplete: features.ghostedAutoComplete ?? DEFAULT_FEATURES.ghostedAutoComplete
541
552
  };
542
553
  }
@@ -552,6 +563,7 @@ function ComposerProvider({
552
563
  closeMenusOnOutsideClick = true,
553
564
  attachmentOptions,
554
565
  mode = "markdown",
566
+ variant = "compact",
555
567
  multiline = true,
556
568
  submitOnEnter = true,
557
569
  smartNewline = true,
@@ -730,6 +742,7 @@ function ComposerProvider({
730
742
  attachmentOptions: normalizedAttachmentOptions,
731
743
  markdownMode,
732
744
  mode,
745
+ variant,
733
746
  multiline,
734
747
  submitOnEnter,
735
748
  smartNewline,
@@ -762,6 +775,7 @@ function ComposerProvider({
762
775
  normalizedAttachmentOptions,
763
776
  markdownMode,
764
777
  mode,
778
+ variant,
765
779
  multiline,
766
780
  submitOnEnter,
767
781
  smartNewline,
@@ -786,8 +800,11 @@ function useComposerContext() {
786
800
  }
787
801
  function EditorShell({
788
802
  placeholder,
803
+ animated,
789
804
  mode,
805
+ variant,
790
806
  multiline,
807
+ expanded,
791
808
  header,
792
809
  toolbar,
793
810
  sendButton,
@@ -795,11 +812,18 @@ function EditorShell({
795
812
  }) {
796
813
  const { classNames, sx, dir } = useComposerContext();
797
814
  const isMarkdown = mode === "markdown";
798
- const editorClass = multiline ? "composer-editor composer-editor--multiline" : "composer-editor composer-editor--inline";
815
+ const isCompact = variant === "compact";
816
+ const compactInline = isCompact && !expanded;
817
+ const fillEditor = compactInline || !isCompact && !multiline;
818
+ const editorClass = isCompact ? "composer-editor composer-editor--compact" : multiline ? "composer-editor composer-editor--multiline" : "composer-editor composer-editor--inline";
799
819
  const editor = slotProps("editor", editorClass, classNames, sx);
800
820
  const editorResolved = resolveSx(sx?.editor);
801
821
  const placeholderBase = mirrorEditorPadding(editorResolved);
802
- const placeholderClass = multiline ? "composer-placeholder composer-placeholder--multiline" : "composer-placeholder composer-placeholder--inline";
822
+ const placeholderClass = cn(
823
+ isCompact ? "composer-placeholder composer-placeholder--compact" : multiline ? "composer-placeholder composer-placeholder--multiline" : "composer-placeholder composer-placeholder--inline",
824
+ // Adds the blinking caret after the typewriter text.
825
+ animated && "composer-placeholder--animated"
826
+ );
803
827
  const placeholderProps = slotProps(
804
828
  "placeholder",
805
829
  placeholderClass,
@@ -814,8 +838,9 @@ function EditorShell({
814
838
  {
815
839
  className: cn(
816
840
  "composer-editor-block",
817
- // Inline: the editor block is the flex child that fills the row.
818
- !multiline && "composer-editor-block--inline"
841
+ // Inline + compact: the editor block is the flex child that fills the
842
+ // row between the leading actions and the trailing send cluster.
843
+ fillEditor && "composer-editor-block--inline"
819
844
  ),
820
845
  children: isMarkdown ? /* @__PURE__ */ jsx(
821
846
  RichTextPlugin,
@@ -834,6 +859,32 @@ function EditorShell({
834
859
  )
835
860
  }
836
861
  );
862
+ if (isCompact) {
863
+ const actions = toolbar && /* @__PURE__ */ jsx("div", { className: "composer-compact-actions", children: toolbar });
864
+ const sendCluster = sendButton && /* @__PURE__ */ jsx("div", { className: "composer-compact-send", children: sendButton });
865
+ if (expanded) {
866
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
867
+ header,
868
+ editorBlock,
869
+ (actions || sendCluster) && /* @__PURE__ */ jsxs("div", { className: "composer-compact-footer", children: [
870
+ actions ?? /* @__PURE__ */ jsx("span", {}),
871
+ sendCluster
872
+ ] }),
873
+ /* @__PURE__ */ jsx(HistoryPlugin, {}),
874
+ footer
875
+ ] });
876
+ }
877
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
878
+ header,
879
+ /* @__PURE__ */ jsxs("div", { className: "composer-compact-row", children: [
880
+ actions,
881
+ editorBlock,
882
+ sendCluster
883
+ ] }),
884
+ /* @__PURE__ */ jsx(HistoryPlugin, {}),
885
+ footer
886
+ ] });
887
+ }
837
888
  if (!multiline) {
838
889
  return /* @__PURE__ */ jsxs(Fragment, { children: [
839
890
  header,
@@ -1224,6 +1275,63 @@ function $createLinkTextNode(text = "", url = "") {
1224
1275
  function $isLinkTextNode(node) {
1225
1276
  return node instanceof LinkTextNode;
1226
1277
  }
1278
+ var BASE_CLASS = "composer-code-tok";
1279
+ var CodeTokenNode = class _CodeTokenNode extends TextNode {
1280
+ __codeKind;
1281
+ static getType() {
1282
+ return "composeai-code-token";
1283
+ }
1284
+ static clone(node) {
1285
+ return new _CodeTokenNode(node.__text, node.__codeKind, node.__key);
1286
+ }
1287
+ constructor(text = "", codeKind = "text", key) {
1288
+ super(text, key);
1289
+ this.__codeKind = codeKind;
1290
+ }
1291
+ getCodeKind() {
1292
+ return this.getLatest().__codeKind;
1293
+ }
1294
+ setCodeKind(kind) {
1295
+ const self = this.getWritable();
1296
+ self.__codeKind = kind;
1297
+ return self;
1298
+ }
1299
+ createDOM(config) {
1300
+ const dom = super.createDOM(config);
1301
+ addClassNamesToElement(dom, BASE_CLASS, `${BASE_CLASS}--${this.__codeKind}`);
1302
+ return dom;
1303
+ }
1304
+ updateDOM(prevNode, dom, config) {
1305
+ const updated = super.updateDOM(prevNode, dom, config);
1306
+ if (prevNode.__codeKind !== this.__codeKind) {
1307
+ dom.classList.remove(`${BASE_CLASS}--${prevNode.__codeKind}`);
1308
+ dom.classList.add(`${BASE_CLASS}--${this.__codeKind}`);
1309
+ }
1310
+ return updated;
1311
+ }
1312
+ static importJSON(serializedNode) {
1313
+ return $createCodeTokenNode("", serializedNode.codeKind).updateFromJSON(
1314
+ serializedNode
1315
+ );
1316
+ }
1317
+ updateFromJSON(serializedNode) {
1318
+ return super.updateFromJSON(serializedNode);
1319
+ }
1320
+ exportJSON() {
1321
+ return {
1322
+ ...super.exportJSON(),
1323
+ type: _CodeTokenNode.getType(),
1324
+ version: 1,
1325
+ codeKind: this.__codeKind
1326
+ };
1327
+ }
1328
+ };
1329
+ function $createCodeTokenNode(text = "", codeKind = "text") {
1330
+ return $applyNodeReplacement(new CodeTokenNode(text, codeKind));
1331
+ }
1332
+ function $isCodeTokenNode(node) {
1333
+ return node instanceof CodeTokenNode;
1334
+ }
1227
1335
  var HEADING_RE = /^(#{1,6}) /;
1228
1336
  var QUOTE_RE = /^> /;
1229
1337
  var BULLET_RE = /^[-*+] /;
@@ -1297,12 +1405,20 @@ function $computeBlockMap() {
1297
1405
  const map = /* @__PURE__ */ new Map();
1298
1406
  const root = $getRoot();
1299
1407
  let insideCode = false;
1408
+ let codeLang;
1300
1409
  for (const child of root.getChildren()) {
1301
1410
  if (!$isParagraphNode(child)) continue;
1302
- const info = $resolveBlockFor(child, insideCode);
1411
+ let info = $resolveBlockFor(child, insideCode);
1412
+ if (info.kind === "code-fence-open") {
1413
+ insideCode = true;
1414
+ codeLang = info.lang;
1415
+ } else if (info.kind === "code-fence-close") {
1416
+ insideCode = false;
1417
+ codeLang = void 0;
1418
+ } else if (info.kind === "code-line" && codeLang) {
1419
+ info = { ...info, lang: codeLang };
1420
+ }
1303
1421
  map.set(child.getKey(), info);
1304
- if (info.kind === "code-fence-open") insideCode = true;
1305
- else if (info.kind === "code-fence-close") insideCode = false;
1306
1422
  }
1307
1423
  return map;
1308
1424
  }
@@ -1508,11 +1624,6 @@ function $isInsideCodeFence() {
1508
1624
  const block = $detectBlockFor(top);
1509
1625
  return block.kind === "code-line" || block.kind === "code-fence-open";
1510
1626
  }
1511
- function $hasMultiLineContent() {
1512
- const root = $getRoot();
1513
- if (root.getChildrenSize() > 1) return true;
1514
- return root.getTextContent().includes("\n");
1515
- }
1516
1627
  function $offsetWithinParagraph(paragraph, point) {
1517
1628
  if (point.type === "element") {
1518
1629
  const children = paragraph.getChildren();
@@ -1605,10 +1716,8 @@ function KeyboardPlugin({ onSubmit }) {
1605
1716
  return true;
1606
1717
  }
1607
1718
  let inCodeFence = false;
1608
- let hasMultiLine = false;
1609
1719
  editor.getEditorState().read(() => {
1610
1720
  inCodeFence = $isInsideCodeFence();
1611
- hasMultiLine = $hasMultiLineContent();
1612
1721
  });
1613
1722
  if (inCodeFence) {
1614
1723
  if (!multiline) {
@@ -1630,7 +1739,6 @@ function KeyboardPlugin({ onSubmit }) {
1630
1739
  return true;
1631
1740
  }
1632
1741
  }
1633
- if (smartNewline && hasMultiLine) return false;
1634
1742
  if (!submitOnEnter) return false;
1635
1743
  return trySubmit(event);
1636
1744
  },
@@ -1760,6 +1868,134 @@ function PasteDropPlugin() {
1760
1868
  return null;
1761
1869
  }
1762
1870
 
1871
+ // src/plugins/mermaid-highlight.ts
1872
+ var KEYWORDS = /* @__PURE__ */ new Set([
1873
+ "flowchart",
1874
+ "graph",
1875
+ "sequenceDiagram",
1876
+ "classDiagram",
1877
+ "stateDiagram",
1878
+ "stateDiagram-v2",
1879
+ "erDiagram",
1880
+ "journey",
1881
+ "gantt",
1882
+ "pie",
1883
+ "gitGraph",
1884
+ "mindmap",
1885
+ "timeline",
1886
+ "quadrantChart",
1887
+ "requirementDiagram",
1888
+ "C4Context",
1889
+ "subgraph",
1890
+ "end",
1891
+ "direction",
1892
+ "participant",
1893
+ "actor",
1894
+ "note",
1895
+ "loop",
1896
+ "alt",
1897
+ "opt",
1898
+ "par",
1899
+ "and",
1900
+ "else",
1901
+ "rect",
1902
+ "activate",
1903
+ "deactivate",
1904
+ "class",
1905
+ "state",
1906
+ "click",
1907
+ "call",
1908
+ "href",
1909
+ "style",
1910
+ "linkStyle",
1911
+ "classDef",
1912
+ "section",
1913
+ "title",
1914
+ "accTitle",
1915
+ "accDescr",
1916
+ "over",
1917
+ "as",
1918
+ // flowchart directions
1919
+ "TB",
1920
+ "TD",
1921
+ "BT",
1922
+ "RL",
1923
+ "LR"
1924
+ ]);
1925
+ var ARROW_CHARS = /* @__PURE__ */ new Set(["-", "=", ".", "<", ">"]);
1926
+ var WS_RE = /\s/;
1927
+ var WORD_RE = /[A-Za-z0-9_]/;
1928
+ var DIGIT_RE = /[0-9]/;
1929
+ function tokenizeMermaidLine(line) {
1930
+ const out = [];
1931
+ const push = (text, kind) => {
1932
+ if (text) out.push({ text, kind });
1933
+ };
1934
+ let i = 0;
1935
+ const n = line.length;
1936
+ while (i < n) {
1937
+ const c = line[i];
1938
+ if (WS_RE.test(c)) {
1939
+ let j = i + 1;
1940
+ while (j < n && WS_RE.test(line[j])) j++;
1941
+ push(line.slice(i, j), "text");
1942
+ i = j;
1943
+ continue;
1944
+ }
1945
+ if (c === "%" && line[i + 1] === "%") {
1946
+ push(line.slice(i), "comment");
1947
+ i = n;
1948
+ continue;
1949
+ }
1950
+ if (c === '"') {
1951
+ let j = i + 1;
1952
+ while (j < n && line[j] !== '"') j++;
1953
+ j = Math.min(j + 1, n);
1954
+ push(line.slice(i, j), "string");
1955
+ i = j;
1956
+ continue;
1957
+ }
1958
+ if ("[](){}|".includes(c)) {
1959
+ push(c, "punct");
1960
+ i++;
1961
+ continue;
1962
+ }
1963
+ if (ARROW_CHARS.has(c)) {
1964
+ let j = i + 1;
1965
+ while (j < n && ARROW_CHARS.has(line[j])) j++;
1966
+ if (j < n && (line[j] === "x" || line[j] === "o") && j - i >= 2) j++;
1967
+ const run = line.slice(i, j);
1968
+ const isArrow = run.length >= 2 || run.includes(">") || run.includes("<");
1969
+ push(run, isArrow ? "arrow" : "punct");
1970
+ i = j;
1971
+ continue;
1972
+ }
1973
+ if (":;,&+*".includes(c)) {
1974
+ push(c, "punct");
1975
+ i++;
1976
+ continue;
1977
+ }
1978
+ if (DIGIT_RE.test(c)) {
1979
+ let j = i + 1;
1980
+ while (j < n && /[0-9.]/.test(line[j])) j++;
1981
+ push(line.slice(i, j), "number");
1982
+ i = j;
1983
+ continue;
1984
+ }
1985
+ if (WORD_RE.test(c)) {
1986
+ let j = i + 1;
1987
+ while (j < n && WORD_RE.test(line[j])) j++;
1988
+ const word = line.slice(i, j);
1989
+ push(word, KEYWORDS.has(word) ? "keyword" : "ident");
1990
+ i = j;
1991
+ continue;
1992
+ }
1993
+ push(c, "text");
1994
+ i++;
1995
+ }
1996
+ return out;
1997
+ }
1998
+
1763
1999
  // src/plugins/markdown-tokenizer.ts
1764
2000
  var PAIRED_PATTERNS = [
1765
2001
  { open: "**", close: "**", format: "bold" },
@@ -1871,6 +2107,13 @@ function readCurrentChildren(paragraph) {
1871
2107
  });
1872
2108
  } else if ($isMarkdownTokenNode(child)) {
1873
2109
  out.push({ kind: "token", text: child.getTextContent(), format: 0 });
2110
+ } else if ($isCodeTokenNode(child)) {
2111
+ out.push({
2112
+ kind: "code",
2113
+ text: child.getTextContent(),
2114
+ format: 0,
2115
+ codeKind: child.getCodeKind()
2116
+ });
1874
2117
  } else if ($isTextNode(child)) {
1875
2118
  out.push({
1876
2119
  kind: "text",
@@ -1893,6 +2136,7 @@ function nodesEqual(a, b) {
1893
2136
  if (aKind !== bKind) return false;
1894
2137
  if (ai.text !== bi.text) return false;
1895
2138
  if (ai.format !== bi.format) return false;
2139
+ if (ai.codeKind !== bi.codeKind) return false;
1896
2140
  }
1897
2141
  return true;
1898
2142
  }
@@ -2060,7 +2304,18 @@ function $applyStyling(paragraph, block, mode) {
2060
2304
  trailingDropForOffsetMap = body.length;
2061
2305
  body = "";
2062
2306
  } else if (body.length > 0) {
2063
- desired.push({ kind: "text", text: body, format: 0 });
2307
+ if (block.kind === "code-line" && block.lang === "mermaid") {
2308
+ for (const tok of tokenizeMermaidLine(body)) {
2309
+ desired.push({
2310
+ kind: "code",
2311
+ text: tok.text,
2312
+ format: 0,
2313
+ codeKind: tok.kind
2314
+ });
2315
+ }
2316
+ } else {
2317
+ desired.push({ kind: "text", text: body, format: 0 });
2318
+ }
2064
2319
  }
2065
2320
  } else if (block.kind === "hr") {
2066
2321
  if (body.length > 0) {
@@ -2148,6 +2403,8 @@ function $applyStyling(paragraph, block, mode) {
2148
2403
  for (const node of desired) {
2149
2404
  if (node.kind === "token") {
2150
2405
  paragraph.append($createMarkdownTokenNode(node.text));
2406
+ } else if (node.kind === "code") {
2407
+ paragraph.append($createCodeTokenNode(node.text, node.codeKind));
2151
2408
  } else if (node.kind === "link") {
2152
2409
  const t = $createLinkTextNode(node.text, node.url ?? "");
2153
2410
  if (node.format !== 0) t.setFormat(node.format);
@@ -2409,10 +2666,13 @@ async function loadMermaid() {
2409
2666
  function svgToDataUri(svg) {
2410
2667
  return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
2411
2668
  }
2412
- function MermaidPlugin() {
2669
+ var MermaidContext = createContext(null);
2670
+ function useMermaidContext() {
2671
+ return useContext(MermaidContext);
2672
+ }
2673
+ function MermaidProvider({ children }) {
2413
2674
  const [editor] = useLexicalComposerContext();
2414
- const { features, icons, renderDiagram, classNames, sx } = useComposerContext();
2415
- const { sparkle: SparkleIcon } = icons;
2675
+ const { features, renderDiagram } = useComposerContext();
2416
2676
  const [diagrams, setDiagrams] = useState([]);
2417
2677
  const keepSource = typeof features.mermaid === "object" ? features.mermaid.keepSource !== false : true;
2418
2678
  useEffect(() => {
@@ -2420,10 +2680,10 @@ function MermaidPlugin() {
2420
2680
  editor.getEditorState().read(() => {
2421
2681
  const found = [];
2422
2682
  const root = $getRoot();
2423
- const children = root.getChildren();
2683
+ const children2 = root.getChildren();
2424
2684
  let i = 0;
2425
- while (i < children.length) {
2426
- const opener = children[i];
2685
+ while (i < children2.length) {
2686
+ const opener = children2[i];
2427
2687
  if (!$isParagraphNode(opener) || !FENCE_OPEN_MERMAID.test(opener.getTextContent())) {
2428
2688
  i++;
2429
2689
  continue;
@@ -2431,8 +2691,8 @@ function MermaidPlugin() {
2431
2691
  const paragraphKeys = [opener.getKey()];
2432
2692
  const codeLines = [];
2433
2693
  let j = i + 1;
2434
- while (j < children.length) {
2435
- const next = children[j];
2694
+ while (j < children2.length) {
2695
+ const next = children2[j];
2436
2696
  if (!$isParagraphNode(next)) break;
2437
2697
  const text = next.getTextContent();
2438
2698
  paragraphKeys.push(next.getKey());
@@ -2496,26 +2756,190 @@ function MermaidPlugin() {
2496
2756
  hiddenKeysRef.current.clear();
2497
2757
  };
2498
2758
  }, [editor]);
2499
- if (diagrams.length === 0) return null;
2500
- const preview = slotProps(
2501
- "mermaidPreview",
2502
- "composer-mermaid",
2503
- classNames,
2504
- sx
2759
+ const value = useMemo(
2760
+ () => ({ diagrams, renderDiagram }),
2761
+ [diagrams, renderDiagram]
2505
2762
  );
2763
+ return /* @__PURE__ */ jsxs(MermaidContext.Provider, { value, children: [
2764
+ children,
2765
+ /* @__PURE__ */ jsx(MermaidBackground, {})
2766
+ ] });
2767
+ }
2768
+ function MermaidBackground() {
2769
+ const ctx = useMermaidContext();
2770
+ if (!ctx || ctx.renderDiagram) return null;
2771
+ return /* @__PURE__ */ jsx(Fragment, { children: ctx.diagrams.map((d) => /* @__PURE__ */ jsx(DiagramBackdrop, { diagram: d }, d.id)) });
2772
+ }
2773
+ function DiagramBackdrop({ diagram }) {
2774
+ const [editor] = useLexicalComposerContext();
2775
+ const { svg } = useDiagramSvg(diagram);
2776
+ const [rect, setRect] = useState(null);
2777
+ const dataUri = useMemo(() => svg ? svgToDataUri(svg) : null, [svg]);
2778
+ useEffect(() => {
2779
+ if (!svg) {
2780
+ setRect(null);
2781
+ return;
2782
+ }
2783
+ const root = editor.getRootElement();
2784
+ const block2 = root?.parentElement;
2785
+ if (!root || !block2) return;
2786
+ let raf = 0;
2787
+ const measure = () => {
2788
+ raf = 0;
2789
+ const blockRect = block2.getBoundingClientRect();
2790
+ let top = Infinity;
2791
+ let left = Infinity;
2792
+ let right = -Infinity;
2793
+ let bottom = -Infinity;
2794
+ let any = false;
2795
+ for (const key of diagram.paragraphKeys) {
2796
+ const el = editor.getElementByKey(key);
2797
+ if (!el) continue;
2798
+ any = true;
2799
+ const r = el.getBoundingClientRect();
2800
+ top = Math.min(top, r.top);
2801
+ left = Math.min(left, r.left);
2802
+ right = Math.max(right, r.right);
2803
+ bottom = Math.max(bottom, r.bottom);
2804
+ }
2805
+ if (!any) {
2806
+ setRect(null);
2807
+ return;
2808
+ }
2809
+ const relTop = top - blockRect.top;
2810
+ const visTop = Math.max(0, relTop);
2811
+ const visBottom = Math.min(blockRect.height, relTop + (bottom - top));
2812
+ if (visBottom <= visTop) {
2813
+ setRect(null);
2814
+ return;
2815
+ }
2816
+ setRect({
2817
+ top: visTop,
2818
+ left: left - blockRect.left,
2819
+ width: right - left,
2820
+ height: visBottom - visTop
2821
+ });
2822
+ };
2823
+ const schedule = () => {
2824
+ if (!raf) raf = requestAnimationFrame(measure);
2825
+ };
2826
+ schedule();
2827
+ const unregister = editor.registerUpdateListener(schedule);
2828
+ root.addEventListener("scroll", schedule, { passive: true });
2829
+ const ro = new ResizeObserver(schedule);
2830
+ ro.observe(root);
2831
+ return () => {
2832
+ if (raf) cancelAnimationFrame(raf);
2833
+ unregister();
2834
+ root.removeEventListener("scroll", schedule);
2835
+ ro.disconnect();
2836
+ };
2837
+ }, [editor, svg, diagram.paragraphKeys]);
2838
+ const block = editor.getRootElement()?.parentElement;
2839
+ if (!dataUri || !rect || !block) return null;
2840
+ return createPortal(
2841
+ /* @__PURE__ */ jsx(
2842
+ "div",
2843
+ {
2844
+ "aria-hidden": true,
2845
+ className: "composer-mermaid-backdrop",
2846
+ style: {
2847
+ top: rect.top,
2848
+ left: rect.left,
2849
+ width: rect.width,
2850
+ height: rect.height,
2851
+ backgroundImage: `url("${dataUri}")`
2852
+ }
2853
+ }
2854
+ ),
2855
+ block
2856
+ );
2857
+ }
2858
+ function MermaidPreview() {
2859
+ const ctx = useMermaidContext();
2860
+ const { icons, classNames, sx } = useComposerContext();
2861
+ const { sparkle: SparkleIcon } = icons;
2862
+ if (!ctx || ctx.diagrams.length === 0) return null;
2863
+ const preview = slotProps("mermaidPreview", "composer-mermaid", classNames, sx);
2506
2864
  return /* @__PURE__ */ jsxs("div", { ...preview, children: [
2507
2865
  /* @__PURE__ */ jsxs("div", { className: "composer-mermaid-head", children: [
2508
2866
  /* @__PURE__ */ jsx(SparkleIcon, {}),
2509
2867
  "Diagram preview"
2510
2868
  ] }),
2511
- /* @__PURE__ */ jsx("div", { className: "composer-mermaid-row", children: diagrams.map((d) => /* @__PURE__ */ jsx(
2512
- DiagramTile,
2869
+ /* @__PURE__ */ jsx("div", { className: "composer-mermaid-row", children: ctx.diagrams.map((d) => /* @__PURE__ */ jsx(DiagramTile, { diagram: d, renderDiagram: ctx.renderDiagram }, d.id)) })
2870
+ ] });
2871
+ }
2872
+ function MermaidQuickAction() {
2873
+ const ctx = useMermaidContext();
2874
+ const { icons, closeMenusOnOutsideClick } = useComposerContext();
2875
+ const { sparkle: SparkleIcon } = icons;
2876
+ const [open, setOpen] = useState(false);
2877
+ const triggerRef = useRef(null);
2878
+ const popoverRef = useRef(null);
2879
+ const menuId = useId();
2880
+ useEffect(() => {
2881
+ if (!open) return;
2882
+ const onPointerDown = (event) => {
2883
+ if (!closeMenusOnOutsideClick) return;
2884
+ const target = event.target;
2885
+ if (!target) return;
2886
+ if (popoverRef.current?.contains(target)) return;
2887
+ if (triggerRef.current?.contains(target)) return;
2888
+ setOpen(false);
2889
+ };
2890
+ const onKeyDown = (event) => {
2891
+ if (event.key === "Escape") {
2892
+ event.preventDefault();
2893
+ setOpen(false);
2894
+ triggerRef.current?.focus();
2895
+ }
2896
+ };
2897
+ document.addEventListener("pointerdown", onPointerDown, true);
2898
+ document.addEventListener("keydown", onKeyDown);
2899
+ return () => {
2900
+ document.removeEventListener("pointerdown", onPointerDown, true);
2901
+ document.removeEventListener("keydown", onKeyDown);
2902
+ };
2903
+ }, [open, closeMenusOnOutsideClick]);
2904
+ if (!ctx || ctx.diagrams.length === 0) return null;
2905
+ const count = ctx.diagrams.length;
2906
+ return /* @__PURE__ */ jsxs("div", { className: "composer-quick-actions", children: [
2907
+ /* @__PURE__ */ jsxs(
2908
+ "button",
2513
2909
  {
2514
- diagram: d,
2515
- renderDiagram
2516
- },
2517
- d.id
2518
- )) })
2910
+ ref: triggerRef,
2911
+ type: "button",
2912
+ "aria-label": `Diagram preview (${count})`,
2913
+ "aria-haspopup": "dialog",
2914
+ "aria-expanded": open,
2915
+ "aria-controls": open ? menuId : void 0,
2916
+ "data-active": open ? "" : void 0,
2917
+ onClick: () => setOpen((o) => !o),
2918
+ className: "composer-mermaid-trigger",
2919
+ children: [
2920
+ /* @__PURE__ */ jsx(DiagramThumb, { diagram: ctx.diagrams[0], renderDiagram: ctx.renderDiagram }),
2921
+ count > 1 && /* @__PURE__ */ jsx("span", { className: "composer-mermaid-count", children: count })
2922
+ ]
2923
+ }
2924
+ ),
2925
+ open && /* @__PURE__ */ jsxs(
2926
+ "div",
2927
+ {
2928
+ ref: popoverRef,
2929
+ id: menuId,
2930
+ role: "dialog",
2931
+ "aria-label": "Diagram preview",
2932
+ "data-composer-popover": "open",
2933
+ className: "composer-popover-in composer-mermaid-pop",
2934
+ children: [
2935
+ /* @__PURE__ */ jsxs("div", { className: "composer-mermaid-head", children: [
2936
+ /* @__PURE__ */ jsx(SparkleIcon, {}),
2937
+ "Diagram preview"
2938
+ ] }),
2939
+ /* @__PURE__ */ jsx("div", { className: "composer-mermaid-row", children: ctx.diagrams.map((d) => /* @__PURE__ */ jsx(DiagramTile, { diagram: d, renderDiagram: ctx.renderDiagram }, d.id)) })
2940
+ ]
2941
+ }
2942
+ )
2519
2943
  ] });
2520
2944
  }
2521
2945
  function DiagramTile({ diagram, renderDiagram }) {
@@ -2537,12 +2961,9 @@ function ConsumerTile({
2537
2961
  }
2538
2962
  return /* @__PURE__ */ jsx("div", { className: "composer-mermaid-tile", children: content });
2539
2963
  }
2540
- function MermaidTile({ diagram }) {
2541
- const { icons } = useComposerContext();
2542
- const { zoom: ZoomIcon } = icons;
2964
+ function useDiagramSvg(diagram) {
2543
2965
  const [svg, setSvg] = useState(null);
2544
2966
  const [error, setError] = useState(null);
2545
- const [zoom, setZoom] = useState(false);
2546
2967
  const [mermaidMissing, setMermaidMissing] = useState(false);
2547
2968
  const renderId = useMemo(
2548
2969
  () => `mermaid-${diagram.id}-${Math.random().toString(36).slice(2, 8)}`,
@@ -2575,6 +2996,13 @@ function MermaidTile({ diagram }) {
2575
2996
  cancelled = true;
2576
2997
  };
2577
2998
  }, [diagram.code, renderId]);
2999
+ return { svg, error, mermaidMissing };
3000
+ }
3001
+ function MermaidTile({ diagram }) {
3002
+ const { icons } = useComposerContext();
3003
+ const { zoom: ZoomIcon } = icons;
3004
+ const [zoom, setZoom] = useState(false);
3005
+ const { svg, error, mermaidMissing } = useDiagramSvg(diagram);
2578
3006
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2579
3007
  /* @__PURE__ */ jsx(
2580
3008
  "button",
@@ -2610,6 +3038,35 @@ function MermaidTile({ diagram }) {
2610
3038
  )
2611
3039
  ] });
2612
3040
  }
3041
+ function DiagramThumb({ diagram, renderDiagram }) {
3042
+ if (renderDiagram) {
3043
+ return /* @__PURE__ */ jsx(ConsumerThumb, { diagram, renderDiagram });
3044
+ }
3045
+ return /* @__PURE__ */ jsx(MermaidThumb, { diagram });
3046
+ }
3047
+ function ConsumerThumb({
3048
+ diagram,
3049
+ renderDiagram
3050
+ }) {
3051
+ let content = null;
3052
+ try {
3053
+ content = renderDiagram({ code: diagram.code, language: "mermaid" });
3054
+ } catch {
3055
+ content = null;
3056
+ }
3057
+ return /* @__PURE__ */ jsx("span", { className: "composer-mermaid-thumb", children: content });
3058
+ }
3059
+ function MermaidThumb({ diagram }) {
3060
+ const { svg } = useDiagramSvg(diagram);
3061
+ if (!svg) return /* @__PURE__ */ jsx("span", { className: "composer-mermaid-thumb" });
3062
+ return /* @__PURE__ */ jsx(
3063
+ "span",
3064
+ {
3065
+ className: "composer-mermaid-thumb",
3066
+ dangerouslySetInnerHTML: { __html: svg }
3067
+ }
3068
+ );
3069
+ }
2613
3070
  function SlashMenu({
2614
3071
  options,
2615
3072
  selectedIndex,
@@ -3872,8 +4329,284 @@ function AttachmentTypePicker({
3872
4329
  )
3873
4330
  ] });
3874
4331
  }
4332
+ function useCustomActionContext(submit) {
4333
+ const [editor] = useLexicalComposerContext();
4334
+ return useMemo(
4335
+ () => ({
4336
+ insertText: (text) => {
4337
+ editor.update(() => {
4338
+ const sel = $getSelection();
4339
+ if ($isRangeSelection(sel)) sel.insertText(text);
4340
+ });
4341
+ focusEditor(editor);
4342
+ },
4343
+ insertMarkdown: (md) => {
4344
+ editor.update(() => {
4345
+ $insertTextWithParagraphBreaks(md);
4346
+ });
4347
+ focusEditor(editor);
4348
+ },
4349
+ submit
4350
+ }),
4351
+ [editor, submit]
4352
+ );
4353
+ }
4354
+ function CustomActionButtons({ submit }) {
4355
+ const { features, classNames, sx } = useComposerContext();
4356
+ const ctx = useCustomActionContext(submit);
4357
+ const actions = features.custom;
4358
+ const toolbarBtn = slotProps(
4359
+ "toolbarButton",
4360
+ "composer-toolbar-btn",
4361
+ classNames,
4362
+ sx
4363
+ );
4364
+ if (actions.length === 0) return null;
4365
+ return /* @__PURE__ */ jsx(Fragment, { children: actions.map((action, i) => /* @__PURE__ */ jsx(Tooltip, { content: action.title, side: "top", children: /* @__PURE__ */ jsx(
4366
+ "button",
4367
+ {
4368
+ type: "button",
4369
+ "aria-label": action.title,
4370
+ "aria-pressed": action.active,
4371
+ "data-active": action.active ? "" : void 0,
4372
+ disabled: action.disabled,
4373
+ onClick: () => action.onClick(ctx),
4374
+ ...toolbarBtn,
4375
+ children: action.icon
4376
+ }
4377
+ ) }, action.id ?? i)) });
4378
+ }
4379
+ function QuickActionsMenu({ extras, submit }) {
4380
+ const {
4381
+ features,
4382
+ attachmentsConfig,
4383
+ addFiles,
4384
+ webEnabled,
4385
+ toggleWeb,
4386
+ icons,
4387
+ classNames,
4388
+ sx,
4389
+ closeMenusOnOutsideClick
4390
+ } = useComposerContext();
4391
+ const {
4392
+ plus: PlusIcon,
4393
+ attach: AttachIcon,
4394
+ image: ImageIcon,
4395
+ web: WebIcon
4396
+ } = icons;
4397
+ const [open, setOpen] = useState(false);
4398
+ const triggerRef = useRef(null);
4399
+ const popoverRef = useRef(null);
4400
+ const fileInputRef = useRef(null);
4401
+ const imageInputRef = useRef(null);
4402
+ const menuId = useId();
4403
+ const attachmentsEnabled = !!features.attachments;
4404
+ const showFileBtn = attachmentsEnabled && attachmentsConfig.file !== false;
4405
+ const showImageBtn = attachmentsEnabled && attachmentsConfig.image !== false;
4406
+ const types = showFileBtn && Array.isArray(attachmentsConfig.types) ? attachmentsConfig.types : [];
4407
+ const hasTypePicker = types.length > 0;
4408
+ const customActions = features.custom;
4409
+ const actionCtx = useCustomActionContext(submit ?? (() => {
4410
+ }));
4411
+ const hasAnyAction = showFileBtn || showImageBtn || features.web || customActions.length > 0 || !!extras;
4412
+ const close = useCallback(() => setOpen(false), []);
4413
+ const openFilePicker = useCallback((accept) => {
4414
+ const input = fileInputRef.current;
4415
+ if (!input) return;
4416
+ input.accept = accept ?? attachmentsConfig.accept ?? "";
4417
+ input.click();
4418
+ }, [attachmentsConfig.accept]);
4419
+ useEffect(() => {
4420
+ if (!open) return;
4421
+ const onPointerDown = (event) => {
4422
+ if (!closeMenusOnOutsideClick) return;
4423
+ const target = event.target;
4424
+ if (!target) return;
4425
+ if (popoverRef.current?.contains(target)) return;
4426
+ if (triggerRef.current?.contains(target)) return;
4427
+ close();
4428
+ };
4429
+ const onKeyDown = (event) => {
4430
+ if (event.key === "Escape") {
4431
+ event.preventDefault();
4432
+ close();
4433
+ triggerRef.current?.focus();
4434
+ }
4435
+ };
4436
+ document.addEventListener("pointerdown", onPointerDown, true);
4437
+ document.addEventListener("keydown", onKeyDown);
4438
+ return () => {
4439
+ document.removeEventListener("pointerdown", onPointerDown, true);
4440
+ document.removeEventListener("keydown", onKeyDown);
4441
+ };
4442
+ }, [open, closeMenusOnOutsideClick, close]);
4443
+ if (!hasAnyAction) return null;
4444
+ const triggerBtn = slotProps(
4445
+ "toolbarButton",
4446
+ "composer-toolbar-btn",
4447
+ classNames,
4448
+ sx
4449
+ );
4450
+ return /* @__PURE__ */ jsxs("div", { className: "composer-quick-actions", children: [
4451
+ showFileBtn && /* @__PURE__ */ jsx(
4452
+ "input",
4453
+ {
4454
+ ref: fileInputRef,
4455
+ type: "file",
4456
+ multiple: true,
4457
+ accept: attachmentsConfig.accept,
4458
+ hidden: true,
4459
+ onChange: (e) => {
4460
+ const files = e.target.files;
4461
+ if (files) addFiles(Array.from(files));
4462
+ if (fileInputRef.current) fileInputRef.current.value = "";
4463
+ }
4464
+ }
4465
+ ),
4466
+ showImageBtn && /* @__PURE__ */ jsx(
4467
+ "input",
4468
+ {
4469
+ ref: imageInputRef,
4470
+ type: "file",
4471
+ multiple: true,
4472
+ accept: "image/*",
4473
+ hidden: true,
4474
+ onChange: (e) => {
4475
+ const files = e.target.files;
4476
+ if (files) addFiles(Array.from(files));
4477
+ if (imageInputRef.current) imageInputRef.current.value = "";
4478
+ }
4479
+ }
4480
+ ),
4481
+ /* @__PURE__ */ jsx(
4482
+ "button",
4483
+ {
4484
+ ref: triggerRef,
4485
+ type: "button",
4486
+ "aria-label": "Quick actions",
4487
+ "aria-haspopup": "menu",
4488
+ "aria-expanded": open,
4489
+ "aria-controls": open ? menuId : void 0,
4490
+ "data-active": open ? "" : void 0,
4491
+ onClick: () => setOpen((o) => !o),
4492
+ ...triggerBtn,
4493
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
4494
+ }
4495
+ ),
4496
+ open && /* @__PURE__ */ jsxs(
4497
+ "div",
4498
+ {
4499
+ ref: popoverRef,
4500
+ id: menuId,
4501
+ role: "menu",
4502
+ "aria-label": "Quick actions",
4503
+ "data-composer-popover": "open",
4504
+ className: "composer-popover-in composer-attach-menu composer-quick-menu",
4505
+ children: [
4506
+ showFileBtn && !hasTypePicker && /* @__PURE__ */ jsx(
4507
+ ActionItem,
4508
+ {
4509
+ icon: /* @__PURE__ */ jsx(AttachIcon, {}),
4510
+ label: "Attach file",
4511
+ onClick: () => {
4512
+ close();
4513
+ openFilePicker();
4514
+ }
4515
+ }
4516
+ ),
4517
+ hasTypePicker && types.map((type) => /* @__PURE__ */ jsx(
4518
+ ActionItem,
4519
+ {
4520
+ icon: type.icon ?? /* @__PURE__ */ jsx(AttachIcon, {}),
4521
+ label: type.label,
4522
+ description: type.description,
4523
+ onClick: () => {
4524
+ close();
4525
+ openFilePicker(type.accept);
4526
+ }
4527
+ },
4528
+ type.id
4529
+ )),
4530
+ showImageBtn && /* @__PURE__ */ jsx(
4531
+ ActionItem,
4532
+ {
4533
+ icon: /* @__PURE__ */ jsx(ImageIcon, {}),
4534
+ label: "Add image",
4535
+ onClick: () => {
4536
+ close();
4537
+ imageInputRef.current?.click();
4538
+ }
4539
+ }
4540
+ ),
4541
+ features.web && /* @__PURE__ */ jsx(
4542
+ ActionItem,
4543
+ {
4544
+ icon: /* @__PURE__ */ jsx(WebIcon, {}),
4545
+ label: "Search the web",
4546
+ active: webEnabled,
4547
+ onClick: () => {
4548
+ toggleWeb();
4549
+ close();
4550
+ }
4551
+ }
4552
+ ),
4553
+ customActions.map((action, i) => /* @__PURE__ */ jsx(
4554
+ ActionItem,
4555
+ {
4556
+ icon: action.icon,
4557
+ label: action.title,
4558
+ active: action.active,
4559
+ disabled: action.disabled,
4560
+ onClick: () => {
4561
+ action.onClick(actionCtx);
4562
+ close();
4563
+ }
4564
+ },
4565
+ action.id ?? i
4566
+ )),
4567
+ extras && /* @__PURE__ */ jsx("div", { className: "composer-quick-extras", children: extras })
4568
+ ]
4569
+ }
4570
+ )
4571
+ ] });
4572
+ }
4573
+ function ActionItem({
4574
+ icon,
4575
+ label,
4576
+ description,
4577
+ active,
4578
+ disabled,
4579
+ onClick
4580
+ }) {
4581
+ return /* @__PURE__ */ jsxs(
4582
+ "button",
4583
+ {
4584
+ role: "menuitem",
4585
+ type: "button",
4586
+ "aria-pressed": active,
4587
+ "data-active": active ? "" : void 0,
4588
+ disabled,
4589
+ onClick,
4590
+ className: cn("composer-attach-item"),
4591
+ children: [
4592
+ /* @__PURE__ */ jsx("span", { className: "composer-attach-item-icon", children: icon }),
4593
+ /* @__PURE__ */ jsx("span", { className: "composer-attach-item-label", children: label }),
4594
+ description ? /* @__PURE__ */ jsx("span", { className: "composer-attach-item-desc", children: description }) : null
4595
+ ]
4596
+ }
4597
+ );
4598
+ }
3875
4599
  var TOOLBAR_BTN_BASE = "composer-toolbar-btn";
3876
- function Toolbar({ extras }) {
4600
+ function Toolbar({ extras, variant = "full", submit }) {
4601
+ if (variant === "compact") {
4602
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4603
+ /* @__PURE__ */ jsx(QuickActionsMenu, { extras, submit }),
4604
+ /* @__PURE__ */ jsx(MermaidQuickAction, {})
4605
+ ] });
4606
+ }
4607
+ return /* @__PURE__ */ jsx(FullToolbar, { extras, submit });
4608
+ }
4609
+ function FullToolbar({ extras, submit }) {
3877
4610
  const {
3878
4611
  features,
3879
4612
  attachmentsConfig,
@@ -3972,6 +4705,8 @@ function Toolbar({ extras }) {
3972
4705
  ]
3973
4706
  }
3974
4707
  ),
4708
+ /* @__PURE__ */ jsx(CustomActionButtons, { submit: submit ?? (() => {
4709
+ }) }),
3975
4710
  extras
3976
4711
  ] });
3977
4712
  }
@@ -4065,7 +4800,6 @@ function HintBar({ hint }) {
4065
4800
  const {
4066
4801
  multiline,
4067
4802
  submitOnEnter,
4068
- smartNewline,
4069
4803
  focusShortcut,
4070
4804
  classNames,
4071
4805
  sx
@@ -4079,16 +4813,6 @@ function HintBar({ hint }) {
4079
4813
  " to send."
4080
4814
  ] });
4081
4815
  }
4082
- if (smartNewline && submitOnEnter) {
4083
- return /* @__PURE__ */ jsxs(Fragment, { children: [
4084
- "Press ",
4085
- /* @__PURE__ */ jsx(Key, { children: "Enter" }),
4086
- " to send a single line,",
4087
- " ",
4088
- /* @__PURE__ */ jsx(Key, { children: "\u2318/Ctrl + Enter" }),
4089
- " to send once you've started a new line."
4090
- ] });
4091
- }
4092
4816
  if (!submitOnEnter) {
4093
4817
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4094
4818
  "Press ",
@@ -4105,7 +4829,7 @@ function HintBar({ hint }) {
4105
4829
  /* @__PURE__ */ jsx(Key, { children: "Shift + Enter" }),
4106
4830
  " for newline."
4107
4831
  ] });
4108
- }, [multiline, submitOnEnter, smartNewline]);
4832
+ }, [multiline, submitOnEnter]);
4109
4833
  const focusHint = useMemo(() => {
4110
4834
  if (!focusShortcut) return null;
4111
4835
  const label = formatShortcut(focusShortcut);
@@ -4208,6 +4932,107 @@ function useComposerHandle(ref, onSubmit) {
4208
4932
  };
4209
4933
  }, [editor, ref, onSubmit, addFiles]);
4210
4934
  }
4935
+ var DEFAULT_TIMING = {
4936
+ typeSpeed: 55,
4937
+ deleteSpeed: 28,
4938
+ holdDuration: 1800,
4939
+ pauseDuration: 450
4940
+ };
4941
+ function useAnimatedPlaceholder(phrases, enabled, options) {
4942
+ const [state, setState] = useState({
4943
+ text: "",
4944
+ active: true
4945
+ });
4946
+ const optsRef = useRef({});
4947
+ optsRef.current = options ?? {};
4948
+ const active = enabled && Array.isArray(phrases) && phrases.length > 0;
4949
+ const key = active ? phrases.join("\u241F") : "";
4950
+ const loop = !!options?.loop;
4951
+ const phrasesRef = useRef(phrases);
4952
+ phrasesRef.current = phrases;
4953
+ useEffect(() => {
4954
+ if (!active) {
4955
+ setState({ text: "", active: true });
4956
+ return;
4957
+ }
4958
+ const list = phrasesRef.current;
4959
+ const lastIdx = list.length - 1;
4960
+ const reduceMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
4961
+ if (reduceMotion) {
4962
+ setState({ text: list[0], active: false });
4963
+ return;
4964
+ }
4965
+ let timer;
4966
+ let phraseIdx = 0;
4967
+ let charIdx = 0;
4968
+ let phase = "typing";
4969
+ const tick = () => {
4970
+ const timing = { ...DEFAULT_TIMING, ...optsRef.current };
4971
+ const { typeSpeed, deleteSpeed, holdDuration, pauseDuration } = timing;
4972
+ const settleTo = optsRef.current.settleTo;
4973
+ const current = list[phraseIdx];
4974
+ const isLast = phraseIdx === lastIdx;
4975
+ switch (phase) {
4976
+ case "typing": {
4977
+ charIdx += 1;
4978
+ setState({ text: current.slice(0, charIdx), active: true });
4979
+ if (charIdx >= current.length) {
4980
+ phase = "holding";
4981
+ timer = setTimeout(tick, holdDuration);
4982
+ } else {
4983
+ timer = setTimeout(tick, typeSpeed);
4984
+ }
4985
+ break;
4986
+ }
4987
+ case "holding": {
4988
+ if (!loop && isLast) {
4989
+ if (settleTo !== void 0) {
4990
+ phase = "settling";
4991
+ timer = setTimeout(tick, deleteSpeed);
4992
+ } else {
4993
+ setState({ text: current, active: false });
4994
+ }
4995
+ } else {
4996
+ phase = "deleting";
4997
+ timer = setTimeout(tick, deleteSpeed);
4998
+ }
4999
+ break;
5000
+ }
5001
+ case "deleting": {
5002
+ charIdx -= 1;
5003
+ setState({ text: current.slice(0, Math.max(0, charIdx)), active: true });
5004
+ if (charIdx <= 0) {
5005
+ phase = "pausing";
5006
+ timer = setTimeout(tick, pauseDuration);
5007
+ } else {
5008
+ timer = setTimeout(tick, deleteSpeed);
5009
+ }
5010
+ break;
5011
+ }
5012
+ case "pausing": {
5013
+ phraseIdx = phraseIdx >= lastIdx ? 0 : phraseIdx + 1;
5014
+ charIdx = 0;
5015
+ phase = "typing";
5016
+ timer = setTimeout(tick, typeSpeed);
5017
+ break;
5018
+ }
5019
+ case "settling": {
5020
+ charIdx -= 1;
5021
+ if (charIdx <= 0) {
5022
+ setState({ text: settleTo ?? "", active: false });
5023
+ } else {
5024
+ setState({ text: current.slice(0, charIdx), active: true });
5025
+ timer = setTimeout(tick, deleteSpeed);
5026
+ }
5027
+ break;
5028
+ }
5029
+ }
5030
+ };
5031
+ timer = setTimeout(tick, DEFAULT_TIMING.typeSpeed);
5032
+ return () => clearTimeout(timer);
5033
+ }, [active, key, loop]);
5034
+ return active ? state : null;
5035
+ }
4211
5036
 
4212
5037
  // src/internal/shortcut.ts
4213
5038
  var MODIFIERS = /* @__PURE__ */ new Set([
@@ -4285,6 +5110,7 @@ function matchesShortcut(parsed, event) {
4285
5110
  var Composer = forwardRef(function Composer2(props, ref) {
4286
5111
  const {
4287
5112
  placeholder = "Send a message\u2026",
5113
+ animatedPlaceholder,
4288
5114
  onSend,
4289
5115
  onStop,
4290
5116
  isStreaming,
@@ -4303,6 +5129,7 @@ var Composer = forwardRef(function Composer2(props, ref) {
4303
5129
  toolbarExtras,
4304
5130
  closeMenusOnOutsideClick = true,
4305
5131
  mode = "markdown",
5132
+ variant = "compact",
4306
5133
  multiline = true,
4307
5134
  submitOnEnter = true,
4308
5135
  smartNewline = true,
@@ -4313,6 +5140,7 @@ var Composer = forwardRef(function Composer2(props, ref) {
4313
5140
  attachmentOptions,
4314
5141
  dir
4315
5142
  } = props;
5143
+ const hasPlaceholder = props.placeholder != null;
4316
5144
  const tokenStyle = useMemo(() => {
4317
5145
  const derived = color ? deriveColorTokens(color) : null;
4318
5146
  if (!derived && !tokens) return void 0;
@@ -4331,6 +5159,7 @@ var Composer = forwardRef(function Composer2(props, ref) {
4331
5159
  closeMenusOnOutsideClick,
4332
5160
  attachmentOptions,
4333
5161
  mode,
5162
+ variant,
4334
5163
  multiline,
4335
5164
  submitOnEnter,
4336
5165
  smartNewline,
@@ -4355,6 +5184,8 @@ var Composer = forwardRef(function Composer2(props, ref) {
4355
5184
  ComposerCard,
4356
5185
  {
4357
5186
  placeholder,
5187
+ animatedPlaceholder,
5188
+ hasPlaceholder,
4358
5189
  initialValue,
4359
5190
  handleRef: ref,
4360
5191
  onSend,
@@ -4365,6 +5196,7 @@ var Composer = forwardRef(function Composer2(props, ref) {
4365
5196
  isStreaming: !!isStreaming,
4366
5197
  toolbarExtras,
4367
5198
  mode,
5199
+ variant,
4368
5200
  multiline
4369
5201
  }
4370
5202
  ),
@@ -4385,11 +5217,14 @@ var RICH_NODES = [
4385
5217
  MarkdownTokenNode,
4386
5218
  BlockParagraphNode,
4387
5219
  LinkTextNode,
5220
+ CodeTokenNode,
4388
5221
  BLOCK_PARAGRAPH_REPLACEMENT
4389
5222
  ];
4390
5223
  var PLAIN_NODES = [MentionNode];
4391
5224
  function ComposerCard({
4392
5225
  placeholder,
5226
+ animatedPlaceholder,
5227
+ hasPlaceholder,
4393
5228
  initialValue,
4394
5229
  handleRef,
4395
5230
  onSend,
@@ -4400,6 +5235,7 @@ function ComposerCard({
4400
5235
  isStreaming,
4401
5236
  toolbarExtras,
4402
5237
  mode,
5238
+ variant,
4403
5239
  multiline
4404
5240
  }) {
4405
5241
  const { webEnabled, isDraggingFiles, classNames, sx } = useComposerContext();
@@ -4420,7 +5256,8 @@ function ComposerCard({
4420
5256
  "div",
4421
5257
  {
4422
5258
  "data-composer-root": "",
4423
- "data-composer-inline": multiline ? void 0 : "",
5259
+ "data-composer-variant": variant,
5260
+ "data-composer-inline": variant === "full" && !multiline ? "" : void 0,
4424
5261
  "data-composer-web": webEnabled ? "" : void 0,
4425
5262
  "data-composer-dragging": isDraggingFiles ? "" : void 0,
4426
5263
  ...card,
@@ -4449,7 +5286,10 @@ function ComposerCard({
4449
5286
  ComposerInner,
4450
5287
  {
4451
5288
  placeholder,
5289
+ animatedPlaceholder,
5290
+ hasPlaceholder,
4452
5291
  mode,
5292
+ variant,
4453
5293
  multiline,
4454
5294
  handleRef,
4455
5295
  onSend,
@@ -4468,7 +5308,10 @@ function ComposerCard({
4468
5308
  }
4469
5309
  function ComposerInner({
4470
5310
  placeholder,
5311
+ animatedPlaceholder,
5312
+ hasPlaceholder,
4471
5313
  mode,
5314
+ variant,
4472
5315
  multiline,
4473
5316
  handleRef,
4474
5317
  onSend,
@@ -4496,6 +5339,9 @@ function ComposerInner({
4496
5339
  const [hasText, setHasText] = useState(
4497
5340
  !!initialValue && initialValue.trim().length > 0
4498
5341
  );
5342
+ const [isMultiLine, setIsMultiLine] = useState(
5343
+ !!initialValue && initialValue.includes("\n")
5344
+ );
4499
5345
  const onSendRef = useRef(onSend);
4500
5346
  onSendRef.current = onSend;
4501
5347
  const refocusOnSubmitRef = useRef(refocusOnSubmit);
@@ -4574,8 +5420,10 @@ function ComposerInner({
4574
5420
  useEffect(() => {
4575
5421
  return editor.registerUpdateListener(() => {
4576
5422
  editor.getEditorState().read(() => {
4577
- const text = $getRoot().getTextContent().trim();
4578
- setHasText(text.length > 0);
5423
+ const root = $getRoot();
5424
+ const text = root.getTextContent();
5425
+ setHasText(text.trim().length > 0);
5426
+ setIsMultiLine(root.getChildrenSize() > 1 || text.includes("\n"));
4579
5427
  });
4580
5428
  });
4581
5429
  }, [editor]);
@@ -4591,8 +5439,17 @@ function ComposerInner({
4591
5439
  }
4592
5440
  });
4593
5441
  }, [editor, registerRunPrompt, submit]);
4594
- const toolbarSlot = /* @__PURE__ */ jsx(Toolbar, { extras: toolbarExtras });
4595
- const sendButtonSlot = /* @__PURE__ */ jsx(
5442
+ const isCompact = variant === "compact";
5443
+ const animatedPhrases = Array.isArray(animatedPlaceholder) ? animatedPlaceholder : animatedPlaceholder?.phrases;
5444
+ const animatedLoop = Array.isArray(animatedPlaceholder) ? false : !!animatedPlaceholder?.loop;
5445
+ const animatedFrame = useAnimatedPlaceholder(animatedPhrases, !hasText, {
5446
+ loop: animatedLoop,
5447
+ settleTo: hasPlaceholder ? placeholder : void 0
5448
+ });
5449
+ const effectivePlaceholder = animatedFrame?.text ?? placeholder;
5450
+ const mermaidActive = multiline && mode === "markdown" && !!features.mermaid;
5451
+ const toolbarSlot = /* @__PURE__ */ jsx(Toolbar, { extras: toolbarExtras, variant, submit });
5452
+ const sendButton = /* @__PURE__ */ jsx(
4596
5453
  SendButton,
4597
5454
  {
4598
5455
  canSend: (
@@ -4605,38 +5462,38 @@ function ComposerInner({
4605
5462
  onStop
4606
5463
  }
4607
5464
  );
4608
- return /* @__PURE__ */ jsxs(Fragment, { children: [
5465
+ const sendButtonSlot = isCompact && features.voice ? /* @__PURE__ */ jsxs(Fragment, { children: [
5466
+ /* @__PURE__ */ jsx(VoiceButton, {}),
5467
+ sendButton
5468
+ ] }) : sendButton;
5469
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
4609
5470
  /* @__PURE__ */ jsx(
4610
5471
  EditorShell,
4611
5472
  {
4612
- placeholder,
5473
+ placeholder: effectivePlaceholder,
5474
+ animated: animatedFrame?.active ?? false,
4613
5475
  mode,
5476
+ variant,
4614
5477
  multiline,
5478
+ expanded: isCompact && isMultiLine,
4615
5479
  header: /* @__PURE__ */ jsx(AttachmentTray, {}),
4616
5480
  toolbar: toolbarSlot,
4617
5481
  sendButton: sendButtonSlot,
4618
- footer: multiline ? /* @__PURE__ */ jsx(MermaidSlot, {}) : null
5482
+ footer: mermaidActive && !isCompact ? /* @__PURE__ */ jsx(MermaidPreview, {}) : null
4619
5483
  }
4620
5484
  ),
4621
5485
  /* @__PURE__ */ jsx(KeyboardPlugin, { onSubmit: submit }),
4622
5486
  /* @__PURE__ */ jsx(AutoFocusPlugin, { enabled: !!autoFocus }),
4623
5487
  /* @__PURE__ */ jsx(PasteDropPlugin, {}),
4624
5488
  markdownEnabled && /* @__PURE__ */ jsx(MarkdownPlugin, {}),
4625
- features.slashCommands && /* @__PURE__ */ jsx(
4626
- SlashCommandPlugin,
4627
- {
4628
- config: features.slashCommands,
4629
- onSubmit: submit
4630
- }
4631
- ),
5489
+ features.slashCommands && // One typeahead per config — a single config or an array of them, so
5490
+ // consumers can register several trigger symbols (each with its own
5491
+ // action menu) at once. Keyed by trigger; give each a distinct symbol.
5492
+ (Array.isArray(features.slashCommands) ? features.slashCommands : [features.slashCommands]).map((cfg, i) => /* @__PURE__ */ jsx(SlashCommandPlugin, { config: cfg, onSubmit: submit }, cfg.trigger ?? `slash-${i}`)),
4632
5493
  features.mentions && /* @__PURE__ */ jsx(MentionPlugin, { config: features.mentions }),
4633
5494
  features.ghostedAutoComplete && /* @__PURE__ */ jsx(GhostedAutoCompletePlugin, { config: features.ghostedAutoComplete })
4634
5495
  ] });
4635
- }
4636
- function MermaidSlot() {
4637
- const { features, mode } = useComposerContext();
4638
- if (mode !== "markdown" || !features.mermaid) return null;
4639
- return /* @__PURE__ */ jsx(MermaidPlugin, {});
5496
+ return mermaidActive ? /* @__PURE__ */ jsx(MermaidProvider, { children: content }) : content;
4640
5497
  }
4641
5498
  function SuggestionRow({ items, onSelect, className }) {
4642
5499
  const { icons } = useComposerContext();