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