composeai 0.1.7 → 0.1.9

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
@@ -802,6 +802,7 @@ function useComposerContext() {
802
802
  }
803
803
  function EditorShell({
804
804
  placeholder,
805
+ animated,
805
806
  mode,
806
807
  variant,
807
808
  multiline,
@@ -820,7 +821,11 @@ function EditorShell({
820
821
  const editor = slotProps("editor", editorClass, classNames, sx);
821
822
  const editorResolved = resolveSx(sx?.editor);
822
823
  const placeholderBase = mirrorEditorPadding(editorResolved);
823
- const placeholderClass = isCompact ? "composer-placeholder composer-placeholder--compact" : 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
+ );
824
829
  const placeholderProps = slotProps(
825
830
  "placeholder",
826
831
  placeholderClass,
@@ -3388,14 +3393,18 @@ function SlashCommandPlugin({ config, onSubmit }) {
3388
3393
  config.trigger ?? "/",
3389
3394
  { minLength: 0, maxLength: 32, allowWhitespace: false }
3390
3395
  );
3396
+ const itemsRef = react.useRef(config.items);
3397
+ itemsRef.current = config.items;
3398
+ const isAsync = !isSyncItems(config.items);
3391
3399
  react.useEffect(() => {
3392
- if (isSyncItems(config.items)) {
3400
+ const items = itemsRef.current;
3401
+ if (isSyncItems(items)) {
3393
3402
  setIsLoading(false);
3394
3403
  return;
3395
3404
  }
3396
3405
  let cancelled = false;
3397
3406
  setIsLoading(true);
3398
- Promise.resolve(config.items(query)).then((res) => {
3407
+ Promise.resolve(items(query)).then((res) => {
3399
3408
  if (cancelled) return;
3400
3409
  setAsyncItems(res);
3401
3410
  setIsLoading(false);
@@ -3403,7 +3412,7 @@ function SlashCommandPlugin({ config, onSubmit }) {
3403
3412
  return () => {
3404
3413
  cancelled = true;
3405
3414
  };
3406
- }, [query, config.items]);
3415
+ }, [query, isAsync]);
3407
3416
  const allItems = react.useMemo(() => {
3408
3417
  return isSyncItems(config.items) ? config.items : asyncItems ?? [];
3409
3418
  }, [config.items, asyncItems]);
@@ -3594,14 +3603,18 @@ function MentionPlugin({ config }) {
3594
3603
  maxLength: 32,
3595
3604
  allowWhitespace: false
3596
3605
  });
3606
+ const itemsRef = react.useRef(config.items);
3607
+ itemsRef.current = config.items;
3608
+ const isAsync = !isSyncItems2(config.items);
3597
3609
  react.useEffect(() => {
3598
- if (isSyncItems2(config.items)) {
3610
+ const items = itemsRef.current;
3611
+ if (isSyncItems2(items)) {
3599
3612
  setIsLoading(false);
3600
3613
  return;
3601
3614
  }
3602
3615
  let cancelled = false;
3603
3616
  setIsLoading(true);
3604
- Promise.resolve(config.items(query)).then((res) => {
3617
+ Promise.resolve(items(query)).then((res) => {
3605
3618
  if (cancelled) return;
3606
3619
  setAsyncItems(res);
3607
3620
  setIsLoading(false);
@@ -3609,7 +3622,7 @@ function MentionPlugin({ config }) {
3609
3622
  return () => {
3610
3623
  cancelled = true;
3611
3624
  };
3612
- }, [query, config.items]);
3625
+ }, [query, isAsync]);
3613
3626
  const allItems = react.useMemo(() => {
3614
3627
  return isSyncItems2(config.items) ? config.items : asyncItems ?? [];
3615
3628
  }, [config.items, asyncItems]);
@@ -4929,6 +4942,107 @@ function useComposerHandle(ref, onSubmit) {
4929
4942
  };
4930
4943
  }, [editor, ref, onSubmit, addFiles]);
4931
4944
  }
4945
+ var DEFAULT_TIMING = {
4946
+ typeSpeed: 55,
4947
+ deleteSpeed: 28,
4948
+ holdDuration: 1800,
4949
+ pauseDuration: 450
4950
+ };
4951
+ function useAnimatedPlaceholder(phrases, enabled, options) {
4952
+ const [state, setState] = react.useState({
4953
+ text: "",
4954
+ active: true
4955
+ });
4956
+ const optsRef = react.useRef({});
4957
+ optsRef.current = options ?? {};
4958
+ const active = enabled && Array.isArray(phrases) && phrases.length > 0;
4959
+ const key = active ? phrases.join("\u241F") : "";
4960
+ const loop = !!options?.loop;
4961
+ const phrasesRef = react.useRef(phrases);
4962
+ phrasesRef.current = phrases;
4963
+ react.useEffect(() => {
4964
+ if (!active) {
4965
+ setState({ text: "", active: true });
4966
+ return;
4967
+ }
4968
+ const list = phrasesRef.current;
4969
+ const lastIdx = list.length - 1;
4970
+ const reduceMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
4971
+ if (reduceMotion) {
4972
+ setState({ text: list[0], active: false });
4973
+ return;
4974
+ }
4975
+ let timer;
4976
+ let phraseIdx = 0;
4977
+ let charIdx = 0;
4978
+ let phase = "typing";
4979
+ const tick = () => {
4980
+ const timing = { ...DEFAULT_TIMING, ...optsRef.current };
4981
+ const { typeSpeed, deleteSpeed, holdDuration, pauseDuration } = timing;
4982
+ const settleTo = optsRef.current.settleTo;
4983
+ const current = list[phraseIdx];
4984
+ const isLast = phraseIdx === lastIdx;
4985
+ switch (phase) {
4986
+ case "typing": {
4987
+ charIdx += 1;
4988
+ setState({ text: current.slice(0, charIdx), active: true });
4989
+ if (charIdx >= current.length) {
4990
+ phase = "holding";
4991
+ timer = setTimeout(tick, holdDuration);
4992
+ } else {
4993
+ timer = setTimeout(tick, typeSpeed);
4994
+ }
4995
+ break;
4996
+ }
4997
+ case "holding": {
4998
+ if (!loop && isLast) {
4999
+ if (settleTo !== void 0) {
5000
+ phase = "settling";
5001
+ timer = setTimeout(tick, deleteSpeed);
5002
+ } else {
5003
+ setState({ text: current, active: false });
5004
+ }
5005
+ } else {
5006
+ phase = "deleting";
5007
+ timer = setTimeout(tick, deleteSpeed);
5008
+ }
5009
+ break;
5010
+ }
5011
+ case "deleting": {
5012
+ charIdx -= 1;
5013
+ setState({ text: current.slice(0, Math.max(0, charIdx)), active: true });
5014
+ if (charIdx <= 0) {
5015
+ phase = "pausing";
5016
+ timer = setTimeout(tick, pauseDuration);
5017
+ } else {
5018
+ timer = setTimeout(tick, deleteSpeed);
5019
+ }
5020
+ break;
5021
+ }
5022
+ case "pausing": {
5023
+ phraseIdx = phraseIdx >= lastIdx ? 0 : phraseIdx + 1;
5024
+ charIdx = 0;
5025
+ phase = "typing";
5026
+ timer = setTimeout(tick, typeSpeed);
5027
+ break;
5028
+ }
5029
+ case "settling": {
5030
+ charIdx -= 1;
5031
+ if (charIdx <= 0) {
5032
+ setState({ text: settleTo ?? "", active: false });
5033
+ } else {
5034
+ setState({ text: current.slice(0, charIdx), active: true });
5035
+ timer = setTimeout(tick, deleteSpeed);
5036
+ }
5037
+ break;
5038
+ }
5039
+ }
5040
+ };
5041
+ timer = setTimeout(tick, DEFAULT_TIMING.typeSpeed);
5042
+ return () => clearTimeout(timer);
5043
+ }, [active, key, loop]);
5044
+ return active ? state : null;
5045
+ }
4932
5046
 
4933
5047
  // src/internal/shortcut.ts
4934
5048
  var MODIFIERS = /* @__PURE__ */ new Set([
@@ -5006,6 +5120,7 @@ function matchesShortcut(parsed, event) {
5006
5120
  var Composer = react.forwardRef(function Composer2(props, ref) {
5007
5121
  const {
5008
5122
  placeholder = "Send a message\u2026",
5123
+ animatedPlaceholder,
5009
5124
  onSend,
5010
5125
  onStop,
5011
5126
  isStreaming,
@@ -5035,6 +5150,7 @@ var Composer = react.forwardRef(function Composer2(props, ref) {
5035
5150
  attachmentOptions,
5036
5151
  dir
5037
5152
  } = props;
5153
+ const hasPlaceholder = props.placeholder != null;
5038
5154
  const tokenStyle = react.useMemo(() => {
5039
5155
  const derived = color ? deriveColorTokens(color) : null;
5040
5156
  if (!derived && !tokens) return void 0;
@@ -5078,6 +5194,8 @@ var Composer = react.forwardRef(function Composer2(props, ref) {
5078
5194
  ComposerCard,
5079
5195
  {
5080
5196
  placeholder,
5197
+ animatedPlaceholder,
5198
+ hasPlaceholder,
5081
5199
  initialValue,
5082
5200
  handleRef: ref,
5083
5201
  onSend,
@@ -5115,6 +5233,8 @@ var RICH_NODES = [
5115
5233
  var PLAIN_NODES = [MentionNode];
5116
5234
  function ComposerCard({
5117
5235
  placeholder,
5236
+ animatedPlaceholder,
5237
+ hasPlaceholder,
5118
5238
  initialValue,
5119
5239
  handleRef,
5120
5240
  onSend,
@@ -5176,6 +5296,8 @@ function ComposerCard({
5176
5296
  ComposerInner,
5177
5297
  {
5178
5298
  placeholder,
5299
+ animatedPlaceholder,
5300
+ hasPlaceholder,
5179
5301
  mode,
5180
5302
  variant,
5181
5303
  multiline,
@@ -5196,6 +5318,8 @@ function ComposerCard({
5196
5318
  }
5197
5319
  function ComposerInner({
5198
5320
  placeholder,
5321
+ animatedPlaceholder,
5322
+ hasPlaceholder,
5199
5323
  mode,
5200
5324
  variant,
5201
5325
  multiline,
@@ -5326,6 +5450,13 @@ function ComposerInner({
5326
5450
  });
5327
5451
  }, [editor, registerRunPrompt, submit]);
5328
5452
  const isCompact = variant === "compact";
5453
+ const animatedPhrases = Array.isArray(animatedPlaceholder) ? animatedPlaceholder : animatedPlaceholder?.phrases;
5454
+ const animatedLoop = Array.isArray(animatedPlaceholder) ? false : !!animatedPlaceholder?.loop;
5455
+ const animatedFrame = useAnimatedPlaceholder(animatedPhrases, !hasText, {
5456
+ loop: animatedLoop,
5457
+ settleTo: hasPlaceholder ? placeholder : void 0
5458
+ });
5459
+ const effectivePlaceholder = animatedFrame?.text ?? placeholder;
5329
5460
  const mermaidActive = multiline && mode === "markdown" && !!features.mermaid;
5330
5461
  const toolbarSlot = /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { extras: toolbarExtras, variant, submit });
5331
5462
  const sendButton = /* @__PURE__ */ jsxRuntime.jsx(
@@ -5349,7 +5480,8 @@ function ComposerInner({
5349
5480
  /* @__PURE__ */ jsxRuntime.jsx(
5350
5481
  EditorShell,
5351
5482
  {
5352
- placeholder,
5483
+ placeholder: effectivePlaceholder,
5484
+ animated: animatedFrame?.active ?? false,
5353
5485
  mode,
5354
5486
  variant,
5355
5487
  multiline,