composeai 0.1.7 → 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.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,
@@ -4927,6 +4932,107 @@ function useComposerHandle(ref, onSubmit) {
4927
4932
  };
4928
4933
  }, [editor, ref, onSubmit, addFiles]);
4929
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
+ }
4930
5036
 
4931
5037
  // src/internal/shortcut.ts
4932
5038
  var MODIFIERS = /* @__PURE__ */ new Set([
@@ -5004,6 +5110,7 @@ function matchesShortcut(parsed, event) {
5004
5110
  var Composer = forwardRef(function Composer2(props, ref) {
5005
5111
  const {
5006
5112
  placeholder = "Send a message\u2026",
5113
+ animatedPlaceholder,
5007
5114
  onSend,
5008
5115
  onStop,
5009
5116
  isStreaming,
@@ -5033,6 +5140,7 @@ var Composer = forwardRef(function Composer2(props, ref) {
5033
5140
  attachmentOptions,
5034
5141
  dir
5035
5142
  } = props;
5143
+ const hasPlaceholder = props.placeholder != null;
5036
5144
  const tokenStyle = useMemo(() => {
5037
5145
  const derived = color ? deriveColorTokens(color) : null;
5038
5146
  if (!derived && !tokens) return void 0;
@@ -5076,6 +5184,8 @@ var Composer = forwardRef(function Composer2(props, ref) {
5076
5184
  ComposerCard,
5077
5185
  {
5078
5186
  placeholder,
5187
+ animatedPlaceholder,
5188
+ hasPlaceholder,
5079
5189
  initialValue,
5080
5190
  handleRef: ref,
5081
5191
  onSend,
@@ -5113,6 +5223,8 @@ var RICH_NODES = [
5113
5223
  var PLAIN_NODES = [MentionNode];
5114
5224
  function ComposerCard({
5115
5225
  placeholder,
5226
+ animatedPlaceholder,
5227
+ hasPlaceholder,
5116
5228
  initialValue,
5117
5229
  handleRef,
5118
5230
  onSend,
@@ -5174,6 +5286,8 @@ function ComposerCard({
5174
5286
  ComposerInner,
5175
5287
  {
5176
5288
  placeholder,
5289
+ animatedPlaceholder,
5290
+ hasPlaceholder,
5177
5291
  mode,
5178
5292
  variant,
5179
5293
  multiline,
@@ -5194,6 +5308,8 @@ function ComposerCard({
5194
5308
  }
5195
5309
  function ComposerInner({
5196
5310
  placeholder,
5311
+ animatedPlaceholder,
5312
+ hasPlaceholder,
5197
5313
  mode,
5198
5314
  variant,
5199
5315
  multiline,
@@ -5324,6 +5440,13 @@ function ComposerInner({
5324
5440
  });
5325
5441
  }, [editor, registerRunPrompt, submit]);
5326
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;
5327
5450
  const mermaidActive = multiline && mode === "markdown" && !!features.mermaid;
5328
5451
  const toolbarSlot = /* @__PURE__ */ jsx(Toolbar, { extras: toolbarExtras, variant, submit });
5329
5452
  const sendButton = /* @__PURE__ */ jsx(
@@ -5347,7 +5470,8 @@ function ComposerInner({
5347
5470
  /* @__PURE__ */ jsx(
5348
5471
  EditorShell,
5349
5472
  {
5350
- placeholder,
5473
+ placeholder: effectivePlaceholder,
5474
+ animated: animatedFrame?.active ?? false,
5351
5475
  mode,
5352
5476
  variant,
5353
5477
  multiline,