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.d.cts CHANGED
@@ -531,6 +531,20 @@ interface GhostedAutoCompleteConfig {
531
531
  */
532
532
  minLength?: number;
533
533
  }
534
+ /**
535
+ * Object form of {@link ComposerProps.animatedPlaceholder}. Use it when you
536
+ * need to opt into looping; the bare `string[]` form is equivalent to
537
+ * `{ phrases, loop: false }`.
538
+ */
539
+ interface AnimatedPlaceholderConfig {
540
+ /** Phrases the empty editor types out, in order. */
541
+ phrases: string[];
542
+ /**
543
+ * Cycle the list forever. Defaults to `false` — the list plays through once
544
+ * and then settles (see {@link ComposerProps.animatedPlaceholder}).
545
+ */
546
+ loop?: boolean;
547
+ }
534
548
  /**
535
549
  * Editor helpers handed to a {@link CustomAction} when it's clicked, so a
536
550
  * custom toolbar button can mutate the composer the same way a slash command
@@ -748,6 +762,46 @@ interface ComposerProps {
748
762
  */
749
763
  focusShortcut?: string | false | null;
750
764
  placeholder?: string;
765
+ /**
766
+ * Animated placeholder: a list of phrases the empty editor cycles through
767
+ * with a typewriter effect — each phrase is revealed one character at a
768
+ * time, held, erased, and the next begins. The animation takes precedence
769
+ * over the static {@link placeholder} while the editor is empty and pauses
770
+ * the moment the user starts typing.
771
+ *
772
+ * Pass a `string[]` for the default behaviour, or an
773
+ * {@link AnimatedPlaceholderConfig} to enable looping.
774
+ *
775
+ * **Looping** (`loop`, default `false`):
776
+ * - `loop: false` — play through the list once, then settle: on the
777
+ * {@link placeholder} prop if one was given, otherwise on the last phrase
778
+ * (left on screen).
779
+ * - `loop: true` — cycle the list forever.
780
+ *
781
+ * Honours `prefers-reduced-motion` by showing the first phrase statically.
782
+ *
783
+ * @example
784
+ * // Play once, then rest on the last phrase (no `placeholder` given):
785
+ * <Composer
786
+ * animatedPlaceholder={[
787
+ * "Ask me anything…",
788
+ * "Summarize this thread",
789
+ * "Draft a reply to Alex",
790
+ * ]}
791
+ * onSend={...}
792
+ * />
793
+ *
794
+ * @example
795
+ * // Loop forever:
796
+ * <Composer
797
+ * animatedPlaceholder={{
798
+ * phrases: ["Ask me anything…", "Summarize this thread"],
799
+ * loop: true,
800
+ * }}
801
+ * onSend={...}
802
+ * />
803
+ */
804
+ animatedPlaceholder?: string[] | AnimatedPlaceholderConfig;
751
805
  /**
752
806
  * Shorthand for `classNames.root`. Kept for back-compat; if both are set,
753
807
  * the two are merged (`className` first, then `classNames.root`).
@@ -1016,4 +1070,4 @@ interface SuggestionRowProps {
1016
1070
  }
1017
1071
  declare function SuggestionRow({ items, onSelect, className }: SuggestionRowProps): react.JSX.Element;
1018
1072
 
1019
- export { type Attachment, type AttachmentKind, type AttachmentOptions, type AttachmentStatus, type AttachmentTypeOption, type AttachmentsConfig, Composer, type ComposerFeatures, type ComposerHandle, type ComposerIcons, type ComposerPromptBehavior, type ComposerPromptsConfig, type ComposerProps, type ComposerSlot, type ComposerSlotClassNames, type ComposerSlots, type ComposerSubmitPayload, type ComposerSxMap, type ComposerSxValue, type ComposerTokens, type CustomAction, type CustomActionContext, type DiagramRenderer, type GhostedAutoCompleteConfig, type IconComponent, type IconProps, type MarkdownConfig, type MarkdownMode, type MentionConfig, type MentionItem, type MentionRef, type MermaidConfig, type SendButtonRenderProps, type SlashCommand, type SlashCommandContext, type SlashConfig, type StopButtonRenderProps, SuggestionRow, type SuggestionRowProps };
1073
+ export { type AnimatedPlaceholderConfig, type Attachment, type AttachmentKind, type AttachmentOptions, type AttachmentStatus, type AttachmentTypeOption, type AttachmentsConfig, Composer, type ComposerFeatures, type ComposerHandle, type ComposerIcons, type ComposerPromptBehavior, type ComposerPromptsConfig, type ComposerProps, type ComposerSlot, type ComposerSlotClassNames, type ComposerSlots, type ComposerSubmitPayload, type ComposerSxMap, type ComposerSxValue, type ComposerTokens, type CustomAction, type CustomActionContext, type DiagramRenderer, type GhostedAutoCompleteConfig, type IconComponent, type IconProps, type MarkdownConfig, type MarkdownMode, type MentionConfig, type MentionItem, type MentionRef, type MermaidConfig, type SendButtonRenderProps, type SlashCommand, type SlashCommandContext, type SlashConfig, type StopButtonRenderProps, SuggestionRow, type SuggestionRowProps };
package/dist/index.d.ts CHANGED
@@ -531,6 +531,20 @@ interface GhostedAutoCompleteConfig {
531
531
  */
532
532
  minLength?: number;
533
533
  }
534
+ /**
535
+ * Object form of {@link ComposerProps.animatedPlaceholder}. Use it when you
536
+ * need to opt into looping; the bare `string[]` form is equivalent to
537
+ * `{ phrases, loop: false }`.
538
+ */
539
+ interface AnimatedPlaceholderConfig {
540
+ /** Phrases the empty editor types out, in order. */
541
+ phrases: string[];
542
+ /**
543
+ * Cycle the list forever. Defaults to `false` — the list plays through once
544
+ * and then settles (see {@link ComposerProps.animatedPlaceholder}).
545
+ */
546
+ loop?: boolean;
547
+ }
534
548
  /**
535
549
  * Editor helpers handed to a {@link CustomAction} when it's clicked, so a
536
550
  * custom toolbar button can mutate the composer the same way a slash command
@@ -748,6 +762,46 @@ interface ComposerProps {
748
762
  */
749
763
  focusShortcut?: string | false | null;
750
764
  placeholder?: string;
765
+ /**
766
+ * Animated placeholder: a list of phrases the empty editor cycles through
767
+ * with a typewriter effect — each phrase is revealed one character at a
768
+ * time, held, erased, and the next begins. The animation takes precedence
769
+ * over the static {@link placeholder} while the editor is empty and pauses
770
+ * the moment the user starts typing.
771
+ *
772
+ * Pass a `string[]` for the default behaviour, or an
773
+ * {@link AnimatedPlaceholderConfig} to enable looping.
774
+ *
775
+ * **Looping** (`loop`, default `false`):
776
+ * - `loop: false` — play through the list once, then settle: on the
777
+ * {@link placeholder} prop if one was given, otherwise on the last phrase
778
+ * (left on screen).
779
+ * - `loop: true` — cycle the list forever.
780
+ *
781
+ * Honours `prefers-reduced-motion` by showing the first phrase statically.
782
+ *
783
+ * @example
784
+ * // Play once, then rest on the last phrase (no `placeholder` given):
785
+ * <Composer
786
+ * animatedPlaceholder={[
787
+ * "Ask me anything…",
788
+ * "Summarize this thread",
789
+ * "Draft a reply to Alex",
790
+ * ]}
791
+ * onSend={...}
792
+ * />
793
+ *
794
+ * @example
795
+ * // Loop forever:
796
+ * <Composer
797
+ * animatedPlaceholder={{
798
+ * phrases: ["Ask me anything…", "Summarize this thread"],
799
+ * loop: true,
800
+ * }}
801
+ * onSend={...}
802
+ * />
803
+ */
804
+ animatedPlaceholder?: string[] | AnimatedPlaceholderConfig;
751
805
  /**
752
806
  * Shorthand for `classNames.root`. Kept for back-compat; if both are set,
753
807
  * the two are merged (`className` first, then `classNames.root`).
@@ -1016,4 +1070,4 @@ interface SuggestionRowProps {
1016
1070
  }
1017
1071
  declare function SuggestionRow({ items, onSelect, className }: SuggestionRowProps): react.JSX.Element;
1018
1072
 
1019
- export { type Attachment, type AttachmentKind, type AttachmentOptions, type AttachmentStatus, type AttachmentTypeOption, type AttachmentsConfig, Composer, type ComposerFeatures, type ComposerHandle, type ComposerIcons, type ComposerPromptBehavior, type ComposerPromptsConfig, type ComposerProps, type ComposerSlot, type ComposerSlotClassNames, type ComposerSlots, type ComposerSubmitPayload, type ComposerSxMap, type ComposerSxValue, type ComposerTokens, type CustomAction, type CustomActionContext, type DiagramRenderer, type GhostedAutoCompleteConfig, type IconComponent, type IconProps, type MarkdownConfig, type MarkdownMode, type MentionConfig, type MentionItem, type MentionRef, type MermaidConfig, type SendButtonRenderProps, type SlashCommand, type SlashCommandContext, type SlashConfig, type StopButtonRenderProps, SuggestionRow, type SuggestionRowProps };
1073
+ export { type AnimatedPlaceholderConfig, type Attachment, type AttachmentKind, type AttachmentOptions, type AttachmentStatus, type AttachmentTypeOption, type AttachmentsConfig, Composer, type ComposerFeatures, type ComposerHandle, type ComposerIcons, type ComposerPromptBehavior, type ComposerPromptsConfig, type ComposerProps, type ComposerSlot, type ComposerSlotClassNames, type ComposerSlots, type ComposerSubmitPayload, type ComposerSxMap, type ComposerSxValue, type ComposerTokens, type CustomAction, type CustomActionContext, type DiagramRenderer, type GhostedAutoCompleteConfig, type IconComponent, type IconProps, type MarkdownConfig, type MarkdownMode, type MentionConfig, type MentionItem, type MentionRef, type MermaidConfig, type SendButtonRenderProps, type SlashCommand, type SlashCommandContext, type SlashConfig, type StopButtonRenderProps, SuggestionRow, type SuggestionRowProps };
package/dist/index.js CHANGED
@@ -800,6 +800,7 @@ function useComposerContext() {
800
800
  }
801
801
  function EditorShell({
802
802
  placeholder,
803
+ animated,
803
804
  mode,
804
805
  variant,
805
806
  multiline,
@@ -818,7 +819,11 @@ function EditorShell({
818
819
  const editor = slotProps("editor", editorClass, classNames, sx);
819
820
  const editorResolved = resolveSx(sx?.editor);
820
821
  const placeholderBase = mirrorEditorPadding(editorResolved);
821
- const placeholderClass = isCompact ? "composer-placeholder composer-placeholder--compact" : 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
+ );
822
827
  const placeholderProps = slotProps(
823
828
  "placeholder",
824
829
  placeholderClass,
@@ -3386,14 +3391,18 @@ function SlashCommandPlugin({ config, onSubmit }) {
3386
3391
  config.trigger ?? "/",
3387
3392
  { minLength: 0, maxLength: 32, allowWhitespace: false }
3388
3393
  );
3394
+ const itemsRef = useRef(config.items);
3395
+ itemsRef.current = config.items;
3396
+ const isAsync = !isSyncItems(config.items);
3389
3397
  useEffect(() => {
3390
- if (isSyncItems(config.items)) {
3398
+ const items = itemsRef.current;
3399
+ if (isSyncItems(items)) {
3391
3400
  setIsLoading(false);
3392
3401
  return;
3393
3402
  }
3394
3403
  let cancelled = false;
3395
3404
  setIsLoading(true);
3396
- Promise.resolve(config.items(query)).then((res) => {
3405
+ Promise.resolve(items(query)).then((res) => {
3397
3406
  if (cancelled) return;
3398
3407
  setAsyncItems(res);
3399
3408
  setIsLoading(false);
@@ -3401,7 +3410,7 @@ function SlashCommandPlugin({ config, onSubmit }) {
3401
3410
  return () => {
3402
3411
  cancelled = true;
3403
3412
  };
3404
- }, [query, config.items]);
3413
+ }, [query, isAsync]);
3405
3414
  const allItems = useMemo(() => {
3406
3415
  return isSyncItems(config.items) ? config.items : asyncItems ?? [];
3407
3416
  }, [config.items, asyncItems]);
@@ -3592,14 +3601,18 @@ function MentionPlugin({ config }) {
3592
3601
  maxLength: 32,
3593
3602
  allowWhitespace: false
3594
3603
  });
3604
+ const itemsRef = useRef(config.items);
3605
+ itemsRef.current = config.items;
3606
+ const isAsync = !isSyncItems2(config.items);
3595
3607
  useEffect(() => {
3596
- if (isSyncItems2(config.items)) {
3608
+ const items = itemsRef.current;
3609
+ if (isSyncItems2(items)) {
3597
3610
  setIsLoading(false);
3598
3611
  return;
3599
3612
  }
3600
3613
  let cancelled = false;
3601
3614
  setIsLoading(true);
3602
- Promise.resolve(config.items(query)).then((res) => {
3615
+ Promise.resolve(items(query)).then((res) => {
3603
3616
  if (cancelled) return;
3604
3617
  setAsyncItems(res);
3605
3618
  setIsLoading(false);
@@ -3607,7 +3620,7 @@ function MentionPlugin({ config }) {
3607
3620
  return () => {
3608
3621
  cancelled = true;
3609
3622
  };
3610
- }, [query, config.items]);
3623
+ }, [query, isAsync]);
3611
3624
  const allItems = useMemo(() => {
3612
3625
  return isSyncItems2(config.items) ? config.items : asyncItems ?? [];
3613
3626
  }, [config.items, asyncItems]);
@@ -4927,6 +4940,107 @@ function useComposerHandle(ref, onSubmit) {
4927
4940
  };
4928
4941
  }, [editor, ref, onSubmit, addFiles]);
4929
4942
  }
4943
+ var DEFAULT_TIMING = {
4944
+ typeSpeed: 55,
4945
+ deleteSpeed: 28,
4946
+ holdDuration: 1800,
4947
+ pauseDuration: 450
4948
+ };
4949
+ function useAnimatedPlaceholder(phrases, enabled, options) {
4950
+ const [state, setState] = useState({
4951
+ text: "",
4952
+ active: true
4953
+ });
4954
+ const optsRef = useRef({});
4955
+ optsRef.current = options ?? {};
4956
+ const active = enabled && Array.isArray(phrases) && phrases.length > 0;
4957
+ const key = active ? phrases.join("\u241F") : "";
4958
+ const loop = !!options?.loop;
4959
+ const phrasesRef = useRef(phrases);
4960
+ phrasesRef.current = phrases;
4961
+ useEffect(() => {
4962
+ if (!active) {
4963
+ setState({ text: "", active: true });
4964
+ return;
4965
+ }
4966
+ const list = phrasesRef.current;
4967
+ const lastIdx = list.length - 1;
4968
+ const reduceMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
4969
+ if (reduceMotion) {
4970
+ setState({ text: list[0], active: false });
4971
+ return;
4972
+ }
4973
+ let timer;
4974
+ let phraseIdx = 0;
4975
+ let charIdx = 0;
4976
+ let phase = "typing";
4977
+ const tick = () => {
4978
+ const timing = { ...DEFAULT_TIMING, ...optsRef.current };
4979
+ const { typeSpeed, deleteSpeed, holdDuration, pauseDuration } = timing;
4980
+ const settleTo = optsRef.current.settleTo;
4981
+ const current = list[phraseIdx];
4982
+ const isLast = phraseIdx === lastIdx;
4983
+ switch (phase) {
4984
+ case "typing": {
4985
+ charIdx += 1;
4986
+ setState({ text: current.slice(0, charIdx), active: true });
4987
+ if (charIdx >= current.length) {
4988
+ phase = "holding";
4989
+ timer = setTimeout(tick, holdDuration);
4990
+ } else {
4991
+ timer = setTimeout(tick, typeSpeed);
4992
+ }
4993
+ break;
4994
+ }
4995
+ case "holding": {
4996
+ if (!loop && isLast) {
4997
+ if (settleTo !== void 0) {
4998
+ phase = "settling";
4999
+ timer = setTimeout(tick, deleteSpeed);
5000
+ } else {
5001
+ setState({ text: current, active: false });
5002
+ }
5003
+ } else {
5004
+ phase = "deleting";
5005
+ timer = setTimeout(tick, deleteSpeed);
5006
+ }
5007
+ break;
5008
+ }
5009
+ case "deleting": {
5010
+ charIdx -= 1;
5011
+ setState({ text: current.slice(0, Math.max(0, charIdx)), active: true });
5012
+ if (charIdx <= 0) {
5013
+ phase = "pausing";
5014
+ timer = setTimeout(tick, pauseDuration);
5015
+ } else {
5016
+ timer = setTimeout(tick, deleteSpeed);
5017
+ }
5018
+ break;
5019
+ }
5020
+ case "pausing": {
5021
+ phraseIdx = phraseIdx >= lastIdx ? 0 : phraseIdx + 1;
5022
+ charIdx = 0;
5023
+ phase = "typing";
5024
+ timer = setTimeout(tick, typeSpeed);
5025
+ break;
5026
+ }
5027
+ case "settling": {
5028
+ charIdx -= 1;
5029
+ if (charIdx <= 0) {
5030
+ setState({ text: settleTo ?? "", active: false });
5031
+ } else {
5032
+ setState({ text: current.slice(0, charIdx), active: true });
5033
+ timer = setTimeout(tick, deleteSpeed);
5034
+ }
5035
+ break;
5036
+ }
5037
+ }
5038
+ };
5039
+ timer = setTimeout(tick, DEFAULT_TIMING.typeSpeed);
5040
+ return () => clearTimeout(timer);
5041
+ }, [active, key, loop]);
5042
+ return active ? state : null;
5043
+ }
4930
5044
 
4931
5045
  // src/internal/shortcut.ts
4932
5046
  var MODIFIERS = /* @__PURE__ */ new Set([
@@ -5004,6 +5118,7 @@ function matchesShortcut(parsed, event) {
5004
5118
  var Composer = forwardRef(function Composer2(props, ref) {
5005
5119
  const {
5006
5120
  placeholder = "Send a message\u2026",
5121
+ animatedPlaceholder,
5007
5122
  onSend,
5008
5123
  onStop,
5009
5124
  isStreaming,
@@ -5033,6 +5148,7 @@ var Composer = forwardRef(function Composer2(props, ref) {
5033
5148
  attachmentOptions,
5034
5149
  dir
5035
5150
  } = props;
5151
+ const hasPlaceholder = props.placeholder != null;
5036
5152
  const tokenStyle = useMemo(() => {
5037
5153
  const derived = color ? deriveColorTokens(color) : null;
5038
5154
  if (!derived && !tokens) return void 0;
@@ -5076,6 +5192,8 @@ var Composer = forwardRef(function Composer2(props, ref) {
5076
5192
  ComposerCard,
5077
5193
  {
5078
5194
  placeholder,
5195
+ animatedPlaceholder,
5196
+ hasPlaceholder,
5079
5197
  initialValue,
5080
5198
  handleRef: ref,
5081
5199
  onSend,
@@ -5113,6 +5231,8 @@ var RICH_NODES = [
5113
5231
  var PLAIN_NODES = [MentionNode];
5114
5232
  function ComposerCard({
5115
5233
  placeholder,
5234
+ animatedPlaceholder,
5235
+ hasPlaceholder,
5116
5236
  initialValue,
5117
5237
  handleRef,
5118
5238
  onSend,
@@ -5174,6 +5294,8 @@ function ComposerCard({
5174
5294
  ComposerInner,
5175
5295
  {
5176
5296
  placeholder,
5297
+ animatedPlaceholder,
5298
+ hasPlaceholder,
5177
5299
  mode,
5178
5300
  variant,
5179
5301
  multiline,
@@ -5194,6 +5316,8 @@ function ComposerCard({
5194
5316
  }
5195
5317
  function ComposerInner({
5196
5318
  placeholder,
5319
+ animatedPlaceholder,
5320
+ hasPlaceholder,
5197
5321
  mode,
5198
5322
  variant,
5199
5323
  multiline,
@@ -5324,6 +5448,13 @@ function ComposerInner({
5324
5448
  });
5325
5449
  }, [editor, registerRunPrompt, submit]);
5326
5450
  const isCompact = variant === "compact";
5451
+ const animatedPhrases = Array.isArray(animatedPlaceholder) ? animatedPlaceholder : animatedPlaceholder?.phrases;
5452
+ const animatedLoop = Array.isArray(animatedPlaceholder) ? false : !!animatedPlaceholder?.loop;
5453
+ const animatedFrame = useAnimatedPlaceholder(animatedPhrases, !hasText, {
5454
+ loop: animatedLoop,
5455
+ settleTo: hasPlaceholder ? placeholder : void 0
5456
+ });
5457
+ const effectivePlaceholder = animatedFrame?.text ?? placeholder;
5327
5458
  const mermaidActive = multiline && mode === "markdown" && !!features.mermaid;
5328
5459
  const toolbarSlot = /* @__PURE__ */ jsx(Toolbar, { extras: toolbarExtras, variant, submit });
5329
5460
  const sendButton = /* @__PURE__ */ jsx(
@@ -5347,7 +5478,8 @@ function ComposerInner({
5347
5478
  /* @__PURE__ */ jsx(
5348
5479
  EditorShell,
5349
5480
  {
5350
- placeholder,
5481
+ placeholder: effectivePlaceholder,
5482
+ animated: animatedFrame?.active ?? false,
5351
5483
  mode,
5352
5484
  variant,
5353
5485
  multiline,