@assistant-ui/react 0.14.24 → 0.14.26

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.
Files changed (50) hide show
  1. package/dist/legacy-runtime/hooks/AttachmentContext.d.ts +12 -12
  2. package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
  3. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  4. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  5. package/dist/primitives/threadList/ThreadListRoot.d.ts.map +1 -1
  6. package/dist/primitives/threadList/ThreadListRoot.js +10 -3
  7. package/dist/primitives/threadList/ThreadListRoot.js.map +1 -1
  8. package/dist/primitives/threadListFocusGroup.d.ts +26 -0
  9. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -0
  10. package/dist/primitives/threadListFocusGroup.js +15 -0
  11. package/dist/primitives/threadListFocusGroup.js.map +1 -0
  12. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
  13. package/dist/primitives/threadListItem/ThreadListItemRoot.js +45 -27
  14. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  15. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
  16. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +26 -2
  17. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  18. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.d.ts.map +1 -1
  19. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +63 -38
  20. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
  21. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.d.ts +20 -3
  22. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.d.ts.map +1 -1
  23. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +95 -14
  24. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
  25. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.d.ts.map +1 -1
  26. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +38 -21
  27. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
  28. package/dist/utils/hooks/useMediaQuery.js +2 -2
  29. package/dist/utils/hooks/useMediaQuery.js.map +1 -1
  30. package/dist/utils/smooth/useSmooth.d.ts +4 -0
  31. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  32. package/dist/utils/smooth/useSmooth.js +7 -1
  33. package/dist/utils/smooth/useSmooth.js.map +1 -1
  34. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  35. package/dist/utils/useToolArgsFieldStatus.d.ts.map +1 -1
  36. package/package.json +8 -6
  37. package/src/primitives/messagePart/MessagePartText.tsx +1 -0
  38. package/src/primitives/threadList/ThreadListRoot.tsx +8 -1
  39. package/src/primitives/threadListFocusGroup.ts +24 -0
  40. package/src/primitives/threadListItem/ThreadListItemRoot.tsx +53 -5
  41. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +45 -0
  42. package/src/primitives/threadListItem/ThreadListKeyboardNav.test.tsx +141 -0
  43. package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +22 -1
  44. package/src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx +64 -3
  45. package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +29 -2
  46. package/src/tests/generative-ui.test.tsx +21 -1
  47. package/src/utils/hooks/useMediaQuery.ts +3 -2
  48. package/src/utils/smooth/useSmooth.test.tsx +26 -0
  49. package/src/utils/smooth/useSmooth.ts +7 -1
  50. package/src/primitives/threadListItem/ThreadListItemTrigger.ts +0 -23
@@ -1,37 +1,118 @@
1
1
  "use client";
2
2
  import { useDropdownMenuScope } from "./scope.js";
3
3
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
4
+ import { createContext, useContext } from "@assistant-ui/tap/react-shim";
4
5
  import { jsx } from "react/jsx-runtime";
5
6
  import { DropdownMenu } from "radix-ui";
7
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
6
8
  //#region src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx
9
+ const ThreadListItemMoreSetOpenContext = createContext(() => {});
10
+ const useThreadListItemMoreSetOpen = () => {
11
+ return useContext(ThreadListItemMoreSetOpenContext);
12
+ };
13
+ const ThreadListItemMoreSharedFocusGroupContext = createContext(false);
14
+ const useThreadListItemMoreSharedFocusGroup = () => {
15
+ return useContext(ThreadListItemMoreSharedFocusGroupContext);
16
+ };
17
+ /**
18
+ * Root container for the overflow menu, built on Radix DropdownMenu.
19
+ *
20
+ * Defaults to a standard, self-contained modal dropdown. Pass
21
+ * {@link ThreadListItemMorePrimitiveRoot.Props.sharedFocusGroup} to fold it into
22
+ * the thread list's keyboard navigation instead.
23
+ */
7
24
  const ThreadListItemMorePrimitiveRoot = (t0) => {
8
- const $ = c(6);
25
+ const $ = c(26);
9
26
  let __scopeThreadListItemMore;
27
+ let defaultOpen;
28
+ let modalProp;
29
+ let onOpenChange;
30
+ let openProp;
10
31
  let rest;
32
+ let t1;
11
33
  if ($[0] !== t0) {
12
- ({__scopeThreadListItemMore, ...rest} = t0);
34
+ ({__scopeThreadListItemMore, open: openProp, defaultOpen, onOpenChange, sharedFocusGroup: t1, modal: modalProp, ...rest} = t0);
13
35
  $[0] = t0;
14
36
  $[1] = __scopeThreadListItemMore;
15
- $[2] = rest;
37
+ $[2] = defaultOpen;
38
+ $[3] = modalProp;
39
+ $[4] = onOpenChange;
40
+ $[5] = openProp;
41
+ $[6] = rest;
42
+ $[7] = t1;
16
43
  } else {
17
44
  __scopeThreadListItemMore = $[1];
18
- rest = $[2];
45
+ defaultOpen = $[2];
46
+ modalProp = $[3];
47
+ onOpenChange = $[4];
48
+ openProp = $[5];
49
+ rest = $[6];
50
+ t1 = $[7];
19
51
  }
52
+ const sharedFocusGroup = t1 === void 0 ? false : t1;
53
+ const modal = sharedFocusGroup ? false : modalProp ?? true;
20
54
  const scope = useDropdownMenuScope(__scopeThreadListItemMore);
21
- let t1;
22
- if ($[3] !== rest || $[4] !== scope) {
23
- t1 = /* @__PURE__ */ jsx(DropdownMenu.Root, {
55
+ const t2 = defaultOpen ?? false;
56
+ let t3;
57
+ if ($[8] !== onOpenChange) {
58
+ t3 = onOpenChange ? { onChange: onOpenChange } : {};
59
+ $[8] = onOpenChange;
60
+ $[9] = t3;
61
+ } else t3 = $[9];
62
+ let t4;
63
+ if ($[10] !== openProp || $[11] !== t2 || $[12] !== t3) {
64
+ t4 = {
65
+ prop: openProp,
66
+ defaultProp: t2,
67
+ caller: "ThreadListItemMorePrimitive.Root",
68
+ ...t3
69
+ };
70
+ $[10] = openProp;
71
+ $[11] = t2;
72
+ $[12] = t3;
73
+ $[13] = t4;
74
+ } else t4 = $[13];
75
+ const [open, setOpen] = useControllableState(t4);
76
+ let t5;
77
+ if ($[14] !== modal || $[15] !== open || $[16] !== rest || $[17] !== scope || $[18] !== setOpen) {
78
+ t5 = /* @__PURE__ */ jsx(DropdownMenu.Root, {
24
79
  ...scope,
25
- ...rest
80
+ ...rest,
81
+ modal,
82
+ open,
83
+ onOpenChange: setOpen
84
+ });
85
+ $[14] = modal;
86
+ $[15] = open;
87
+ $[16] = rest;
88
+ $[17] = scope;
89
+ $[18] = setOpen;
90
+ $[19] = t5;
91
+ } else t5 = $[19];
92
+ let t6;
93
+ if ($[20] !== setOpen || $[21] !== t5) {
94
+ t6 = /* @__PURE__ */ jsx(ThreadListItemMoreSetOpenContext.Provider, {
95
+ value: setOpen,
96
+ children: t5
97
+ });
98
+ $[20] = setOpen;
99
+ $[21] = t5;
100
+ $[22] = t6;
101
+ } else t6 = $[22];
102
+ let t7;
103
+ if ($[23] !== sharedFocusGroup || $[24] !== t6) {
104
+ t7 = /* @__PURE__ */ jsx(ThreadListItemMoreSharedFocusGroupContext.Provider, {
105
+ value: sharedFocusGroup,
106
+ children: t6
26
107
  });
27
- $[3] = rest;
28
- $[4] = scope;
29
- $[5] = t1;
30
- } else t1 = $[5];
31
- return t1;
108
+ $[23] = sharedFocusGroup;
109
+ $[24] = t6;
110
+ $[25] = t7;
111
+ } else t7 = $[25];
112
+ return t7;
32
113
  };
33
114
  ThreadListItemMorePrimitiveRoot.displayName = "ThreadListItemMorePrimitive.Root";
34
115
  //#endregion
35
- export { ThreadListItemMorePrimitiveRoot };
116
+ export { ThreadListItemMorePrimitiveRoot, useThreadListItemMoreSetOpen, useThreadListItemMoreSharedFocusGroup };
36
117
 
37
118
  //# sourceMappingURL=ThreadListItemMoreRoot.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadListItemMoreRoot.js","names":["c","_c","FC","DropdownMenu","DropdownMenuPrimitive","ScopedProps","useDropdownMenuScope","ThreadListItemMorePrimitiveRoot","Props","DropdownMenuProps","t0","$","__scopeThreadListItemMore","rest","scope","t1","displayName"],"sources":["../../../src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx"],"sourcesContent":["\"use client\";\n\nimport type { FC } from \"react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport { type ScopedProps, useDropdownMenuScope } from \"./scope\";\n\nexport namespace ThreadListItemMorePrimitiveRoot {\n export type Props = DropdownMenuPrimitive.DropdownMenuProps;\n}\n\nexport const ThreadListItemMorePrimitiveRoot: FC<\n ThreadListItemMorePrimitiveRoot.Props\n> = ({\n __scopeThreadListItemMore,\n ...rest\n}: ScopedProps<ThreadListItemMorePrimitiveRoot.Props>) => {\n const scope = useDropdownMenuScope(__scopeThreadListItemMore);\n\n return <DropdownMenuPrimitive.Root {...scope} {...rest} />;\n};\n\nThreadListItemMorePrimitiveRoot.displayName =\n \"ThreadListItemMorePrimitive.Root\";\n"],"mappings":";;;;;;AAUA,MAAaO,mCAETG,OAAA;CAAA,MAAAC,IAAAV,EAAA,CAAA;CAAA,IAAAW;CAAA,IAAAC;CAAA,IAAAF,EAAA,OAAAD,IAAA;EAAC,CAAA,CAAAE,8BAAAC,QAAAH;EAGgDC,EAAA,KAAAD;EAAAC,EAAA,KAAAC;EAAAD,EAAA,KAAAE;CAAA,OAAA;EAAAD,4BAAAD,EAAA;EAAAE,OAAAF,EAAA;CAAA;CACnD,MAAAG,QAAcR,qBAAqBM,yBAAyB;CAAE,IAAAG;CAAA,IAAAJ,EAAA,OAAAE,QAAAF,EAAA,OAAAG,OAAA;EAEvDC,KAAA,oBAAA,aAAA,MAAA;GAAA,GAAgCD;GAAK,GAAMD;EAAI,CAAA;EAAIF,EAAA,KAAAE;EAAAF,EAAA,KAAAG;EAAAH,EAAA,KAAAI;CAAA,OAAAA,KAAAJ,EAAA;CAAA,OAAnDI;AAAmD;AAG5DR,gCAAgCS,cAC9B"}
1
+ {"version":3,"file":"ThreadListItemMoreRoot.js","names":["c","_c","createContext","Dispatch","FC","SetStateAction","useContext","DropdownMenu","DropdownMenuPrimitive","useControllableState","ScopedProps","useDropdownMenuScope","ThreadListItemMoreSetOpenContext","useThreadListItemMoreSetOpen","ThreadListItemMoreSharedFocusGroupContext","useThreadListItemMoreSharedFocusGroup","ThreadListItemMorePrimitiveRoot","Props","DropdownMenuProps","sharedFocusGroup","t0","$","__scopeThreadListItemMore","defaultOpen","modalProp","onOpenChange","openProp","rest","t1","open","modal","undefined","scope","t2","t3","onChange","t4","prop","defaultProp","caller","setOpen","t5","t6","t7","displayName"],"sources":["../../../src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n type Dispatch,\n type FC,\n type SetStateAction,\n useContext,\n} from \"react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { type ScopedProps, useDropdownMenuScope } from \"./scope\";\n\nconst ThreadListItemMoreSetOpenContext = createContext<\n Dispatch<SetStateAction<boolean>>\n>(() => {});\n\nexport const useThreadListItemMoreSetOpen = (): Dispatch<\n SetStateAction<boolean>\n> => useContext(ThreadListItemMoreSetOpenContext);\n\nconst ThreadListItemMoreSharedFocusGroupContext = createContext(false);\n\nexport const useThreadListItemMoreSharedFocusGroup = (): boolean =>\n useContext(ThreadListItemMoreSharedFocusGroupContext);\n\nexport namespace ThreadListItemMorePrimitiveRoot {\n export type Props = DropdownMenuPrimitive.DropdownMenuProps & {\n /**\n * Join the menu to the thread list's focus group: Right opens it, Left/Escape\n * close it and return focus to the trigger (mirrored in RTL). Forces the menu\n * non-modal, since a focus trap can't let focus cross the trigger/menu\n * boundary. Defaults to a standalone modal dropdown.\n */\n sharedFocusGroup?: boolean | undefined;\n };\n}\n\n/**\n * Root container for the overflow menu, built on Radix DropdownMenu.\n *\n * Defaults to a standard, self-contained modal dropdown. Pass\n * {@link ThreadListItemMorePrimitiveRoot.Props.sharedFocusGroup} to fold it into\n * the thread list's keyboard navigation instead.\n */\nexport const ThreadListItemMorePrimitiveRoot: FC<\n ThreadListItemMorePrimitiveRoot.Props\n> = ({\n __scopeThreadListItemMore,\n open: openProp,\n defaultOpen,\n onOpenChange,\n sharedFocusGroup = false,\n modal: modalProp,\n ...rest\n}: ScopedProps<ThreadListItemMorePrimitiveRoot.Props>) => {\n const modal = sharedFocusGroup ? false : (modalProp ?? true);\n const scope = useDropdownMenuScope(__scopeThreadListItemMore);\n const [open, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n caller: \"ThreadListItemMorePrimitive.Root\",\n ...(onOpenChange ? { onChange: onOpenChange } : {}),\n });\n\n return (\n <ThreadListItemMoreSharedFocusGroupContext.Provider\n value={sharedFocusGroup}\n >\n <ThreadListItemMoreSetOpenContext.Provider value={setOpen}>\n <DropdownMenuPrimitive.Root\n {...scope}\n {...rest}\n modal={modal}\n open={open}\n onOpenChange={setOpen}\n />\n </ThreadListItemMoreSetOpenContext.Provider>\n </ThreadListItemMoreSharedFocusGroupContext.Provider>\n );\n};\n\nThreadListItemMorePrimitiveRoot.displayName =\n \"ThreadListItemMorePrimitive.Root\";\n"],"mappings":";;;;;;;;AAaA,MAAMY,mCAAmCV,oBAEjC,CAAC,CAAC;AAEV,MAAaW,qCAA+B;CAAA,OAEvCP,WAAWM,gCAAgC;AAAC;AAEjD,MAAME,4CAA4CZ,cAAc,KAAK;AAErE,MAAaa,8CAAwC;CAAA,OACnDT,WAAWQ,yCAAyC;AAAC;;;;;;;;AAqBvD,MAAaE,mCAETI,OAAA;CAAA,MAAAC,IAAApB,EAAA,EAAA;CAAA,IAAAqB;CAAA,IAAAC;CAAA,IAAAC;CAAA,IAAAC;CAAA,IAAAC;CAAA,IAAAC;CAAA,IAAAC;CAAA,IAAAP,EAAA,OAAAD,IAAA;EAAC,CAAA,CAAAE,2BAAAO,MAAAH,UAAAH,aAAAE,cAAAN,kBAAAS,IAAAE,OAAAN,cAAAG,QAAAP;EAQgDC,EAAA,KAAAD;EAAAC,EAAA,KAAAC;EAAAD,EAAA,KAAAE;EAAAF,EAAA,KAAAG;EAAAH,EAAA,KAAAI;EAAAJ,EAAA,KAAAK;EAAAL,EAAA,KAAAM;EAAAN,EAAA,KAAAO;CAAA,OAAA;EAAAN,4BAAAD,EAAA;EAAAE,cAAAF,EAAA;EAAAG,YAAAH,EAAA;EAAAI,eAAAJ,EAAA;EAAAK,WAAAL,EAAA;EAAAM,OAAAN,EAAA;EAAAO,KAAAP,EAAA;CAAA;CAHnD,MAAAF,mBAAAS,OAAAG,KAAAA,IAAA,QAAAH;CAIA,MAAAE,QAAcX,mBAAA,QAA4BK,aAAA;CAC1C,MAAAQ,QAAcrB,qBAAqBW,yBAAyB;CAG7C,MAAAW,KAAAV,eAAA;CAAoB,IAAAW;CAAA,IAAAb,EAAA,OAAAI,cAAA;EAE7BS,KAAAT,eAAA,EAAAU,UAA2BV,aAAkB,IAA7C,CAA6C;EAACJ,EAAA,KAAAI;EAAAJ,EAAA,KAAAa;CAAA,OAAAA,KAAAb,EAAA;CAAA,IAAAe;CAAA,IAAAf,EAAA,QAAAK,YAAAL,EAAA,QAAAY,MAAAZ,EAAA,QAAAa,IAAA;EAJPE,KAAA;GAAAC,MACrCX;GAAQY,aACDL;GAAoBM,QACzB;GAAkC,GACtCL;EACN;EAACb,EAAA,MAAAK;EAAAL,EAAA,MAAAY;EAAAZ,EAAA,MAAAa;EAAAb,EAAA,MAAAe;CAAA,OAAAA,KAAAf,EAAA;CALD,MAAA,CAAAQ,MAAAW,WAAwB/B,qBAAqB2B,EAK5C;CAAE,IAAAK;CAAA,IAAApB,EAAA,QAAAS,SAAAT,EAAA,QAAAQ,QAAAR,EAAA,QAAAM,QAAAN,EAAA,QAAAW,SAAAX,EAAA,QAAAmB,SAAA;EAOGC,KAAA,oBAAA,aAAA,MAAA;GAAA,GACMT;GAAK,GACLL;GACGG;GACDD;GACQW,cAAAA;EAAO,CAAA;EACrBnB,EAAA,MAAAS;EAAAT,EAAA,MAAAQ;EAAAR,EAAA,MAAAM;EAAAN,EAAA,MAAAW;EAAAX,EAAA,MAAAmB;EAAAnB,EAAA,MAAAoB;CAAA,OAAAA,KAAApB,EAAA;CAAA,IAAAqB;CAAA,IAAArB,EAAA,QAAAmB,WAAAnB,EAAA,QAAAoB,IAAA;EAPJC,KAAA,oBAAA,iCAAA,UAAA;GAAkDF,OAAAA;aAChDC;EAOF,CAAA;EAA4CpB,EAAA,MAAAmB;EAAAnB,EAAA,MAAAoB;EAAApB,EAAA,MAAAqB;CAAA,OAAAA,KAAArB,EAAA;CAAA,IAAAsB;CAAA,IAAAtB,EAAA,QAAAF,oBAAAE,EAAA,QAAAqB,IAAA;EAX9CC,KAAA,oBAAA,0CAAA,UAAA;GACSxB,OAAAA;aAEPuB;EASF,CAAA;EAAqDrB,EAAA,MAAAF;EAAAE,EAAA,MAAAqB;EAAArB,EAAA,MAAAsB;CAAA,OAAAA,KAAAtB,EAAA;CAAA,OAZrDsB;AAYqD;AAIzD3B,gCAAgC4B,cAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadListItemMoreTrigger.d.ts","names":[],"sources":["../../../src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx"],"mappings":";;;;;kBAQiB,kCAAA;EAAA,KACH,OAAA,GAAU,YAAA,QAAoB,YAAA,CAAsB,OAAA;EAAA,KACpD,KAAA,GAAQ,mBAAA,QAA2B,YAAA,CAAsB,OAAA;AAAA;AAAA,cAG1D,kCAAA,kBAAkC,yBAAA,CAAA,IAAA,CAAA,YAAA,CAAA,wBAAA,mBAAA,aAAA,CAAA,iBAAA"}
1
+ {"version":3,"file":"ThreadListItemMoreTrigger.d.ts","names":[],"sources":["../../../src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx"],"mappings":";;;;;kBAkBiB,kCAAA;EAAA,KACH,OAAA,GAAU,YAAA,QAAoB,YAAA,CAAsB,OAAA;EAAA,KACpD,KAAA,GAAQ,mBAAA,QAA2B,YAAA,CAAsB,OAAA;AAAA;AAAA,cAG1D,kCAAA,kBAAkC,yBAAA,CAAA,IAAA,CAAA,YAAA,CAAA,wBAAA,mBAAA,aAAA,CAAA,iBAAA"}
@@ -1,37 +1,54 @@
1
1
  "use client";
2
2
  import { DropdownMenuRenderTrigger } from "../dropdownMenuRenderPrimitives.js";
3
+ import { useThreadListItemFocus } from "../threadListFocusGroup.js";
3
4
  import { useDropdownMenuScope } from "./scope.js";
5
+ import { useThreadListItemMoreSetOpen, useThreadListItemMoreSharedFocusGroup } from "./ThreadListItemMoreRoot.js";
4
6
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
5
7
  import { forwardRef } from "@assistant-ui/tap/react-shim";
6
8
  import { jsx } from "react/jsx-runtime";
9
+ import { useComposedRefs } from "@radix-ui/react-compose-refs";
10
+ import { composeEventHandlers } from "@radix-ui/primitive";
11
+ import { Direction } from "radix-ui";
7
12
  //#region src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx
8
13
  const ThreadListItemMorePrimitiveTrigger = forwardRef((t0, ref) => {
9
- const $ = c(7);
10
- let __scopeThreadListItemMore;
11
- let rest;
12
- if ($[0] !== t0) {
13
- ({__scopeThreadListItemMore, ...rest} = t0);
14
- $[0] = t0;
15
- $[1] = __scopeThreadListItemMore;
16
- $[2] = rest;
17
- } else {
18
- __scopeThreadListItemMore = $[1];
19
- rest = $[2];
20
- }
14
+ const $ = c(10);
15
+ const { __scopeThreadListItemMore, ...rest } = t0;
21
16
  const scope = useDropdownMenuScope(__scopeThreadListItemMore);
17
+ const focus = useThreadListItemFocus();
18
+ const setOpen = useThreadListItemMoreSetOpen();
19
+ const sharedFocusGroup = useThreadListItemMoreSharedFocusGroup();
20
+ const composedRef = useComposedRefs(ref, focus?.moreRef);
21
+ const openKey = Direction.useDirection() === "rtl" ? "ArrowLeft" : "ArrowRight";
22
+ const T0 = DropdownMenuRenderTrigger;
22
23
  let t1;
23
- if ($[3] !== ref || $[4] !== rest || $[5] !== scope) {
24
- t1 = /* @__PURE__ */ jsx(DropdownMenuRenderTrigger, {
24
+ if ($[0] !== openKey || $[1] !== setOpen || $[2] !== sharedFocusGroup) {
25
+ t1 = (event) => {
26
+ if (!sharedFocusGroup || event.key !== openKey) return;
27
+ event.preventDefault();
28
+ setOpen(true);
29
+ };
30
+ $[0] = openKey;
31
+ $[1] = setOpen;
32
+ $[2] = sharedFocusGroup;
33
+ $[3] = t1;
34
+ } else t1 = $[3];
35
+ const t2 = composeEventHandlers(rest.onKeyDown, t1);
36
+ let t3;
37
+ if ($[4] !== T0 || $[5] !== composedRef || $[6] !== rest || $[7] !== scope || $[8] !== t2) {
38
+ t3 = /* @__PURE__ */ jsx(T0, {
25
39
  ...scope,
26
40
  ...rest,
27
- ref
41
+ onKeyDown: t2,
42
+ ref: composedRef
28
43
  });
29
- $[3] = ref;
30
- $[4] = rest;
31
- $[5] = scope;
32
- $[6] = t1;
33
- } else t1 = $[6];
34
- return t1;
44
+ $[4] = T0;
45
+ $[5] = composedRef;
46
+ $[6] = rest;
47
+ $[7] = scope;
48
+ $[8] = t2;
49
+ $[9] = t3;
50
+ } else t3 = $[9];
51
+ return t3;
35
52
  });
36
53
  ThreadListItemMorePrimitiveTrigger.displayName = "ThreadListItemMorePrimitive.Trigger";
37
54
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadListItemMoreTrigger.js","names":["c","_c","ComponentRef","forwardRef","DropdownMenu","DropdownMenuPrimitive","WithRenderPropProps","DropdownMenuRenderTrigger","ScopedProps","useDropdownMenuScope","ThreadListItemMorePrimitiveTrigger","Element","Trigger","Props","t0","ref","$","__scopeThreadListItemMore","rest","scope","t1","displayName"],"sources":["../../../src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ComponentRef, forwardRef } from \"react\";\nimport type { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport type { WithRenderPropProps } from \"../../utils/Primitive\";\nimport { DropdownMenuRenderTrigger } from \"../dropdownMenuRenderPrimitives\";\nimport { type ScopedProps, useDropdownMenuScope } from \"./scope\";\n\nexport namespace ThreadListItemMorePrimitiveTrigger {\n export type Element = ComponentRef<typeof DropdownMenuPrimitive.Trigger>;\n export type Props = WithRenderPropProps<typeof DropdownMenuPrimitive.Trigger>;\n}\n\nexport const ThreadListItemMorePrimitiveTrigger = forwardRef<\n ThreadListItemMorePrimitiveTrigger.Element,\n ThreadListItemMorePrimitiveTrigger.Props\n>(\n (\n {\n __scopeThreadListItemMore,\n ...rest\n }: ScopedProps<ThreadListItemMorePrimitiveTrigger.Props>,\n ref,\n ) => {\n const scope = useDropdownMenuScope(__scopeThreadListItemMore);\n\n return <DropdownMenuRenderTrigger {...scope} {...rest} ref={ref} />;\n },\n);\n\nThreadListItemMorePrimitiveTrigger.displayName =\n \"ThreadListItemMorePrimitive.Trigger\";\n"],"mappings":";;;;;;;AAaA,MAAaU,qCAAqCP,YAIhDW,IAAAC,QAAA;CAAA,MAAAC,IAAAf,EAAA,CAAA;CAAA,IAAAgB;CAAA,IAAAC;CAAA,IAAAF,EAAA,OAAAF,IAAA;EACE,CAAA,CAAAG,8BAAAC,QAAAJ;EAGwDE,EAAA,KAAAF;EAAAE,EAAA,KAAAC;EAAAD,EAAA,KAAAE;CAAA,OAAA;EAAAD,4BAAAD,EAAA;EAAAE,OAAAF,EAAA;CAAA;CAGxD,MAAAG,QAAcV,qBAAqBQ,yBAAyB;CAAE,IAAAG;CAAA,IAAAJ,EAAA,OAAAD,OAAAC,EAAA,OAAAE,QAAAF,EAAA,OAAAG,OAAA;EAEvDC,KAAA,oBAAC,2BAAD;GAA0B,GAAKD;GAAK,GAAMD;GAAWH;EAAG,CAAA;EAAIC,EAAA,KAAAD;EAAAC,EAAA,KAAAE;EAAAF,EAAA,KAAAG;EAAAH,EAAA,KAAAI;CAAA,OAAAA,KAAAJ,EAAA;CAAA,OAA5DI;AAA4D,CAEvE;AAEAV,mCAAmCW,cACjC"}
1
+ {"version":3,"file":"ThreadListItemMoreTrigger.js","names":["c","_c","ComponentRef","forwardRef","Direction","DropdownMenu","DropdownMenuPrimitive","useComposedRefs","composeEventHandlers","WithRenderPropProps","DropdownMenuRenderTrigger","useThreadListItemFocus","useThreadListItemMoreSharedFocusGroup","useThreadListItemMoreSetOpen","ScopedProps","useDropdownMenuScope","ThreadListItemMorePrimitiveTrigger","Element","Trigger","Props","t0","ref","$","__scopeThreadListItemMore","rest","scope","focus","setOpen","sharedFocusGroup","composedRef","moreRef","direction","useDirection","openKey","T0","t1","event","key","preventDefault","t2","onKeyDown","t3","displayName"],"sources":["../../../src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ComponentRef, forwardRef } from \"react\";\nimport {\n Direction,\n type DropdownMenu as DropdownMenuPrimitive,\n} from \"radix-ui\";\nimport { useComposedRefs } from \"@radix-ui/react-compose-refs\";\nimport { composeEventHandlers } from \"@radix-ui/primitive\";\nimport type { WithRenderPropProps } from \"../../utils/Primitive\";\nimport { DropdownMenuRenderTrigger } from \"../dropdownMenuRenderPrimitives\";\nimport { useThreadListItemFocus } from \"../threadListFocusGroup\";\nimport {\n useThreadListItemMoreSharedFocusGroup,\n useThreadListItemMoreSetOpen,\n} from \"./ThreadListItemMoreRoot\";\nimport { type ScopedProps, useDropdownMenuScope } from \"./scope\";\n\nexport namespace ThreadListItemMorePrimitiveTrigger {\n export type Element = ComponentRef<typeof DropdownMenuPrimitive.Trigger>;\n export type Props = WithRenderPropProps<typeof DropdownMenuPrimitive.Trigger>;\n}\n\nexport const ThreadListItemMorePrimitiveTrigger = forwardRef<\n ThreadListItemMorePrimitiveTrigger.Element,\n ThreadListItemMorePrimitiveTrigger.Props\n>(\n (\n {\n __scopeThreadListItemMore,\n ...rest\n }: ScopedProps<ThreadListItemMorePrimitiveTrigger.Props>,\n ref,\n ) => {\n const scope = useDropdownMenuScope(__scopeThreadListItemMore);\n const focus = useThreadListItemFocus();\n const setOpen = useThreadListItemMoreSetOpen();\n const sharedFocusGroup = useThreadListItemMoreSharedFocusGroup();\n const composedRef = useComposedRefs(ref, focus?.moreRef);\n const direction = Direction.useDirection();\n const openKey = direction === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\";\n\n return (\n <DropdownMenuRenderTrigger\n {...scope}\n {...rest}\n onKeyDown={composeEventHandlers(rest.onKeyDown, (event) => {\n if (!sharedFocusGroup || event.key !== openKey) return;\n event.preventDefault();\n setOpen(true);\n })}\n ref={composedRef}\n />\n );\n },\n);\n\nThreadListItemMorePrimitiveTrigger.displayName =\n \"ThreadListItemMorePrimitive.Trigger\";\n"],"mappings":";;;;;;;;;;;;AAuBA,MAAagB,qCAAqCb,YAIhDiB,IAAAC,QAAA;CAAA,MAAAC,IAAArB,EAAA,EAAA;CACE,MAAA,EAAAsB,2BAAA,GAAAC,SAAAJ;CAMA,MAAAK,QAAcV,qBAAqBQ,yBAAyB;CAC5D,MAAAG,QAAcf,uBAAuB;CACrC,MAAAgB,UAAgBd,6BAA6B;CAC7C,MAAAe,mBAAyBhB,sCAAsC;CAC/D,MAAAiB,cAAoBtB,gBAAgBc,KAAKK,OAAKI,OAAS;CAEvD,MAAAG,UADkB7B,UAAS4B,aACXD,MAAc,QAAd,cAAA;CAGb,MAAAG,KAAAxB;CAAyB,IAAAyB;CAAA,IAAAb,EAAA,OAAAW,WAAAX,EAAA,OAAAK,WAAAL,EAAA,OAAAM,kBAAA;EAGwBO,MAAAC,UAAA;GAC9C,IAAI,CAACR,oBAAoBQ,MAAKC,QAASJ,SAAO;GAC9CG,MAAKE,eAAgB;GACrBX,QAAQ,IAAI;EAAC;EACdL,EAAA,KAAAW;EAAAX,EAAA,KAAAK;EAAAL,EAAA,KAAAM;EAAAN,EAAA,KAAAa;CAAA,OAAAA,KAAAb,EAAA;CAJU,MAAAiB,KAAA/B,qBAAqBgB,KAAIgB,WAAYL,EAI/C;CAAC,IAAAM;CAAA,IAAAnB,EAAA,OAAAY,MAAAZ,EAAA,OAAAO,eAAAP,EAAA,OAAAE,QAAAF,EAAA,OAAAG,SAAAH,EAAA,OAAAiB,IAAA;EAPJE,KAAA,oBAAC,IAAD;GAA0B,GACpBhB;GAAK,GACLD;GACO,WAAAe;GAKNV,KAAAA;EAAW,CAAA;EAChBP,EAAA,KAAAY;EAAAZ,EAAA,KAAAO;EAAAP,EAAA,KAAAE;EAAAF,EAAA,KAAAG;EAAAH,EAAA,KAAAiB;EAAAjB,EAAA,KAAAmB;CAAA,OAAAA,KAAAnB,EAAA;CAAA,OATFmB;AASE,CAGR;AAEAzB,mCAAmC0B,cACjC"}
@@ -9,7 +9,7 @@ const useMediaQuery = (query) => {
9
9
  let t0;
10
10
  if ($[0] !== query) {
11
11
  t0 = (callback) => {
12
- if (typeof window === "undefined" || query === null) return noopUnsubscribe;
12
+ if (typeof window === "undefined" || query === null || !window.matchMedia) return noopUnsubscribe;
13
13
  const mql = window.matchMedia(query);
14
14
  mql.addEventListener("change", callback);
15
15
  return () => mql.removeEventListener("change", callback);
@@ -21,7 +21,7 @@ const useMediaQuery = (query) => {
21
21
  let t1;
22
22
  if ($[2] !== query) {
23
23
  t1 = () => {
24
- if (typeof window === "undefined" || query === null) return false;
24
+ if (typeof window === "undefined" || query === null || !window.matchMedia) return false;
25
25
  return window.matchMedia(query).matches;
26
26
  };
27
27
  $[2] = query;
@@ -1 +1 @@
1
- {"version":3,"file":"useMediaQuery.js","names":["c","_c","useCallback","useSyncExternalStore","getServerSnapshot","noopUnsubscribe","useMediaQuery","query","$","t0","callback","window","mql","matchMedia","addEventListener","removeEventListener","subscribe","t1","matches","getSnapshot"],"sources":["../../../src/utils/hooks/useMediaQuery.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\nconst getServerSnapshot = () => false;\nconst noopUnsubscribe = () => {};\n\nexport const useMediaQuery = (query: string | null): boolean => {\n const subscribe = useCallback(\n (callback: () => void) => {\n if (typeof window === \"undefined\" || query === null)\n return noopUnsubscribe;\n const mql = window.matchMedia(query);\n mql.addEventListener(\"change\", callback);\n return () => mql.removeEventListener(\"change\", callback);\n },\n [query],\n );\n\n const getSnapshot = useCallback(() => {\n if (typeof window === \"undefined\" || query === null) return false;\n return window.matchMedia(query).matches;\n }, [query]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n};\n"],"mappings":";;;;AAIA,MAAMI,0BAA0B;AAChC,MAAMC,wBAAwB,CAAC;AAE/B,MAAaC,iBAAgBC,UAAA;CAAA,MAAAC,IAAAP,EAAA,CAAA;CAAA,IAAAQ;CAAA,IAAAD,EAAA,OAAAD,OAAA;EAEzBE,MAAAC,aAAA;GACE,IAAI,OAAOC,WAAW,eAAeJ,UAAU,MAAI,OAC1CF;GACT,MAAAO,MAAYD,OAAME,WAAYN,KAAK;GACnCK,IAAGE,iBAAkB,UAAUJ,QAAQ;GAAC,aAC3BE,IAAGG,oBAAqB,UAAUL,QAAQ;EAAC;EACzDF,EAAA,KAAAD;EAAAC,EAAA,KAAAC;CAAA,OAAAA,KAAAD,EAAA;CAPH,MAAAQ,YAAkBP;CAShB,IAAAQ;CAAA,IAAAT,EAAA,OAAAD,OAAA;EAE8BU,WAAA;GAC9B,IAAI,OAAON,WAAW,eAAeJ,UAAU,MAAI,OAAS;GAAM,OAC3DI,OAAME,WAAYN,KAAK,CAAC,CAAAW;EAAQ;EACxCV,EAAA,KAAAD;EAAAC,EAAA,KAAAS;CAAA,OAAAA,KAAAT,EAAA;CAAW,OAELL,qBAAqBa,WAAWG,IAAaf,iBAAiB;AAAC"}
1
+ {"version":3,"file":"useMediaQuery.js","names":["c","_c","useCallback","useSyncExternalStore","getServerSnapshot","noopUnsubscribe","useMediaQuery","query","$","t0","callback","window","matchMedia","mql","addEventListener","removeEventListener","subscribe","t1","matches","getSnapshot"],"sources":["../../../src/utils/hooks/useMediaQuery.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\nconst getServerSnapshot = () => false;\nconst noopUnsubscribe = () => {};\n\nexport const useMediaQuery = (query: string | null): boolean => {\n const subscribe = useCallback(\n (callback: () => void) => {\n if (typeof window === \"undefined\" || query === null || !window.matchMedia)\n return noopUnsubscribe;\n const mql = window.matchMedia(query);\n mql.addEventListener(\"change\", callback);\n return () => mql.removeEventListener(\"change\", callback);\n },\n [query],\n );\n\n const getSnapshot = useCallback(() => {\n if (typeof window === \"undefined\" || query === null || !window.matchMedia)\n return false;\n return window.matchMedia(query).matches;\n }, [query]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n};\n"],"mappings":";;;;AAIA,MAAMI,0BAA0B;AAChC,MAAMC,wBAAwB,CAAC;AAE/B,MAAaC,iBAAgBC,UAAA;CAAA,MAAAC,IAAAP,EAAA,CAAA;CAAA,IAAAQ;CAAA,IAAAD,EAAA,OAAAD,OAAA;EAEzBE,MAAAC,aAAA;GACE,IAAI,OAAOC,WAAW,eAAeJ,UAAU,QAA3C,CAAoDI,OAAMC,YAAW,OAChEP;GACT,MAAAQ,MAAYF,OAAMC,WAAYL,KAAK;GACnCM,IAAGC,iBAAkB,UAAUJ,QAAQ;GAAC,aAC3BG,IAAGE,oBAAqB,UAAUL,QAAQ;EAAC;EACzDF,EAAA,KAAAD;EAAAC,EAAA,KAAAC;CAAA,OAAAA,KAAAD,EAAA;CAPH,MAAAQ,YAAkBP;CAShB,IAAAQ;CAAA,IAAAT,EAAA,OAAAD,OAAA;EAE8BU,WAAA;GAC9B,IAAI,OAAON,WAAW,eAAeJ,UAAU,QAA3C,CAAoDI,OAAMC,YAAW,OAChE;GAAM,OACRD,OAAMC,WAAYL,KAAK,CAAC,CAAAW;EAAQ;EACxCV,EAAA,KAAAD;EAAAC,EAAA,KAAAS;CAAA,OAAAA,KAAAT,EAAA;CAAW,OAELL,qBAAqBa,WAAWG,IAAaf,iBAAiB;AAAC"}
@@ -39,6 +39,10 @@ type SmoothOptions = {
39
39
  * the reveal. Returns the part state with `text` replaced by the revealed
40
40
  * prefix and `status` reporting `running` until the reveal catches up.
41
41
  *
42
+ * The reveal auto-disables under `prefers-reduced-motion: reduce`,
43
+ * committing the full text immediately; this takes precedence over an
44
+ * explicit `smooth={true}`.
45
+ *
42
46
  * @example
43
47
  * ```tsx
44
48
  * const { text, status } = useSmooth(useMessagePartText(), {
@@ -1 +1 @@
1
- {"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;AAiBA;KAAY,aAAA;;;;;;EAMV,OAAA;EAmBW;AAAA;AAsGb;;;EAnHE,iBAAA;EAoH2B;;;;EA/G3B,gBAAA;EAiHuC;;;;;;;EAzGvC,WAAA;AAAA;;;;;AAyG2D;;;;;;;;;;;;cAHhD,SAAA,GACX,KAAA,EAAO,gBAAA,IAAoB,eAAA,GAAkB,oBAAA,GAC7C,MAAA,aAAkB,aAAA,KACjB,gBAAA,IAAoB,eAAA,GAAkB,oBAAA"}
1
+ {"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;AAkBA;KAAY,aAAA;;;;;;EAMV,OAAA;EAmBW;AAAA;AA0Gb;;;EAvHE,iBAAA;EAwH2B;;;;EAnH3B,gBAAA;EAqHuC;;;;;;;EA7GvC,WAAA;AAAA;;;;;AA6G2D;;;;;;;;;;;;;;;;cAHhD,SAAA,GACX,KAAA,EAAO,gBAAA,IAAoB,eAAA,GAAkB,oBAAA,GAC7C,MAAA,aAAkB,aAAA,KACjB,gBAAA,IAAoB,eAAA,GAAkB,oBAAA"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { writableStore } from "../../context/ReadonlyStore.js";
3
+ import { useMediaQuery } from "../hooks/useMediaQuery.js";
3
4
  import { useSmoothStatusStore } from "./SmoothContext.js";
4
5
  import { useAui, useAuiState } from "@assistant-ui/store";
5
6
  import { useEffect, useMemo, useRef, useState } from "@assistant-ui/tap/react-shim";
@@ -66,6 +67,10 @@ const positiveOr = (value, fallback) => value !== void 0 && value > 0 ? value :
66
67
  * the reveal. Returns the part state with `text` replaced by the revealed
67
68
  * prefix and `status` reporting `running` until the reveal catches up.
68
69
  *
70
+ * The reveal auto-disables under `prefers-reduced-motion: reduce`,
71
+ * committing the full text immediately; this takes precedence over an
72
+ * explicit `smooth={true}`.
73
+ *
69
74
  * @example
70
75
  * ```tsx
71
76
  * const { text, status } = useSmooth(useMessagePartText(), {
@@ -76,8 +81,9 @@ const positiveOr = (value, fallback) => value !== void 0 && value > 0 ? value :
76
81
  */
77
82
  const useSmooth = (state, smooth = false) => {
78
83
  const { text } = state;
84
+ const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
79
85
  const options = typeof smooth === "object" && smooth !== null ? smooth : void 0;
80
- const enabled = smooth !== false && smooth !== null;
86
+ const enabled = smooth !== false && smooth !== null && !reduceMotion;
81
87
  const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);
82
88
  const maxCharIntervalMs = positiveOr(options?.maxCharIntervalMs, DEFAULT_MAX_CHAR_INTERVAL_MS);
83
89
  const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);
@@ -1 +1 @@
1
- {"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","constructor","currentText","setText","newText","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n public lastCommitTime: number = 0;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n constructor(\n public currentText: string,\n private setText: (newText: string) => void,\n ) {}\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part());\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","constructor","currentText","setText","newText","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useMediaQuery } from \"../hooks/useMediaQuery\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n public lastCommitTime: number = 0;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n constructor(\n public currentText: string,\n private setText: (newText: string) => void,\n ) {}\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n *\n * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part());\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":";;;;;;;;AA8CA,MAAMmB,mBAAmB;AACzB,MAAMC,+BAA+B;AAErC,IAAMC,qBAAN,MAAyB;CAYdS;CACCC;CAZV,mBAA0C;CAC1C,iBAAiCP,KAAKC,IAAI;CAC1C,iBAAgC;CAEhC,aAA4B;CAC5B,UAAyBN;CACzB,oBAAmCC;CACnC,mBAAkCQ;CAClC,cAA6B;CAE7BC,YACE,aACA,SACA;EAFOC,KAAAA,cAAAA;EACCC,KAAAA,UAAAA;CACP;CAEHE,QAAQ;EACN,IAAI,KAAKX,qBAAqB,MAAM;EACpC,KAAKC,iBAAiBC,KAAKC,IAAI;EAC/B,KAAKS,QAAQ;CACf;CAEAC,OAAO;EACL,IAAI,KAAKb,qBAAqB,MAAM;GAClCc,qBAAqB,KAAKd,gBAAgB;GAC1C,KAAKA,mBAAmB;EAC1B;CACF;CAEA,gBAAwB;EACtB,MAAMe,cAAcb,KAAKC,IAAI;EAE7B,IAAIc,gBADcF,cAAc,KAAKd;EAGrC,MAAMiB,iBAAiB,KAAKb,WAAWc,SAAS,KAAKX,YAAYW;EACjE,MAAMC,kBAAkBC,KAAKC,IAC3B,KAAK5B,mBACL,KAAKD,UAAUyB,cACjB;EAEA,MAAMK,aAAaF,KAAKC,IAAIJ,gBAAgB,KAAKvB,gBAAgB;EACjE,IAAI6B,aAAa;EACjB,OAAOP,iBAAiBG,mBAAmBI,aAAaD,YAAY;GAClEC;GACAP,iBAAiBG;EACnB;EAGA,IAAII,eAAeD,cAAcA,eAAe,KAAK5B,kBACnDsB,gBAAgB;EAGlB,IAAIO,eAAeN,gBACjB,KAAKlB,mBAAmByB,sBAAsB,KAAKb,OAAO;OAE1D,KAAKZ,mBAAmB;EAE1B,IAAIwB,eAAe,GAAG;EAEtB,KAAKhB,cAAc,KAAKH,WAAWqB,MACjC,GACA,KAAKlB,YAAYW,SAASK,UAC5B;EACA,KAAKvB,iBAAiBc,cAAcE;EAGpC,IADmBO,eAAeN,kBAChBH,cAAc,KAAKX,kBAAkB,KAAKR,aAAa;GACvE,KAAKQ,iBAAiBW;GACtB,KAAKN,QAAQ,KAAKD,WAAW;EAC/B;CACF;AACF;AAEA,MAAMoB,gBAAmCC,OAAOC,OAAO,EACrDC,MAAM,UACR,CAAC;AAED,MAAMC,cAAcC,OAA2BC,aAC7CD,UAAUE,KAAAA,KAAaF,QAAQ,IAAIA,QAAQC;;;;;;;;;;;;;;;;;;;;;AAsB7C,MAAaE,aACXC,OACAC,SAAkC,UAC8B;CAChE,MAAM,EAAEC,SAASF;CACjB,MAAMG,eAAenD,cAAc,kCAAkC;CACrE,MAAMoD,UACJ,OAAOH,WAAW,YAAYA,WAAW,OAAOA,SAASH,KAAAA;CAC3D,MAAMO,UAAUJ,WAAW,SAASA,WAAW,QAAQ,CAACE;CACxD,MAAM/C,UAAUuC,WAAWS,SAAShD,SAASI,gBAAgB;CAC7D,MAAMH,oBAAoBsC,WACxBS,SAAS/C,mBACTI,4BACF;CACA,MAAMH,mBAAmBqC,WAAWS,SAAS9C,kBAAkBW,QAAQ;CACvE,MAAMV,cAAcoC,WAAWS,SAAS7C,aAAa,CAAC;CAEtD,MAAM,CAAC+C,eAAeC,oBAAoB/D,SACxCwD,MAAMQ,OAAOd,SAAS,YAAY,KAAKQ,IACzC;CAWA,MAAMO,MAAMhE,OAAO;CACnB,MAAMiE,OAAOhE,kBAAkB+D,IAAIC,KAAK,CAAC;CACzC,MAAM,CAACC,UAAUC,eAAepE,SAASkE,IAAI;CAC7C,IAAIA,SAASC,YAAY,CAACT,KAAKW,WAAWP,aAAa,GAAG;EACxDM,YAAYF,IAAI;EAChBH,iBAAiBP,MAAMQ,OAAOd,SAAS,YAAY,KAAKQ,IAAI;CAC9D;CAEA,MAAMY,oBAAoB7D,qBAAqB,EAAE8D,UAAU,KAAK,CAAC;CACjE,MAAM3C,UAAUrB,gBAAgBmD,WAAiB;EAC/CK,iBAAiBL,MAAI;EACrB,IAAIY,mBAAmB;GACrB,MAAME,SACJV,kBAAkBJ,UAAQF,MAAMQ,OAAOd,SAAS,YAC5CH,gBACAS,MAAMQ;GACZtD,cAAc4D,iBAAiB,CAAC,CAACG,SAASD,QAAQ,IAAI;EACxD;CACF,CAAC;CAGD3E,gBAAgB;EACd,IAAIyE,mBAAmB;GACrB,MAAME,WACJX,YAAYC,kBAAkBJ,QAAQF,MAAMQ,OAAOd,SAAS,aACxDH,gBACAS,MAAMQ;GACZtD,cAAc4D,iBAAiB,CAAC,CAACG,SAASD,UAAQ,IAAI;EACxD;CACF,GAAG;EAACF;EAAmBT;EAASH;EAAMI;EAAeN,MAAMQ;CAAM,CAAC;CAElE,MAAM,CAACU,eAAe1E,SACpB,IAAIkB,mBAAmB4C,eAAelC,OAAO,CAC/C;CAEA/B,gBAAgB;EACd6E,YAAY9D,UAAUA;EACtB8D,YAAY7D,oBAAoBA;EAChC6D,YAAY5D,mBAAmBA;EAC/B4D,YAAY3D,cAAcA;CAC5B,GAAG;EAAC2D;EAAa9D;EAASC;EAAmBC;EAAkBC;CAAW,CAAC;CAE3E,MAAM4D,kBAAkB5E,OAAOmE,IAAI;CACnCrE,gBAAgB;EACd,IAAI,CAACgE,SAAS;GACZa,YAAY1C,KAAK;GACjB;EACF;EAOA,MAAM4C,cAAcD,gBAAgBE,YAAYX;EAChDS,gBAAgBE,UAAUX;EAC1B,IAAIU,eAAe,CAAClB,KAAKW,WAAWK,YAAYlD,UAAU,GAAG;GAC3D,IAAIgC,MAAMQ,OAAOd,SAAS,WAAW;IACnCwB,YAAY/C,cAAc;IAC1B+C,YAAYlD,aAAakC;IACzBgB,YAAYnD,iBAAiB;IAC7BmD,YAAY5C,MAAM;GACpB,OAAO;IACL4C,YAAY/C,cAAc+B;IAC1BgB,YAAYlD,aAAakC;IACzBgB,YAAY1C,KAAK;GACnB;GACA;EACF;EAEA0C,YAAYlD,aAAakC;EACzBgB,YAAY5C,MAAM;CACpB,GAAG;EAAC4C;EAAab;EAASH;EAAMF,MAAMQ,OAAOd;EAAMgB;CAAI,CAAC;CAExDrE,gBAAgB;EACd,aAAa;GACX6E,YAAY1C,KAAK;EACnB;CACF,GAAG,CAAC0C,WAAW,CAAC;CAEhB,OAAO5E,cAEH+D,UACI;EACE,GAAGL;EACHE,MAAMI;EACNE,QAAQN,SAASI,gBAAgBN,MAAMQ,SAASjB;CAClD,IACAS,OACN;EAACK;EAASC;EAAeN;EAAOE;CAAI,CACtC;AACF"}
@@ -1,5 +1,7 @@
1
1
  //#region src/utils/useToolArgsFieldStatus.d.ts
2
2
  declare const useToolArgsFieldStatus: (fieldPath: (string | number)[]) => {
3
+ type: string;
4
+ } | {
3
5
  readonly type: "running";
4
6
  } | {
5
7
  readonly type: "complete";
@@ -7,8 +9,6 @@ declare const useToolArgsFieldStatus: (fieldPath: (string | number)[]) => {
7
9
  readonly type: "incomplete";
8
10
  readonly reason: "cancelled" | "length" | "content-filter" | "other" | "error";
9
11
  readonly error?: unknown;
10
- } | {
11
- type: string;
12
12
  };
13
13
  //#endregion
14
14
  export { useToolArgsFieldStatus };
@@ -1 +1 @@
1
- {"version":3,"file":"useToolArgsFieldStatus.d.ts","names":[],"sources":["../../src/utils/useToolArgsFieldStatus.ts"],"mappings":";cAKa,sBAAA,GAA0B,SAAA;EAAA"}
1
+ {"version":3,"file":"useToolArgsFieldStatus.d.ts","names":[],"sources":["../../src/utils/useToolArgsFieldStatus.ts"],"mappings":";cAKa,sBAAA,GAA0B,SAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.14.24",
3
+ "version": "0.14.26",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,21 +55,23 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.2.19",
58
+ "@assistant-ui/core": "^0.2.20",
59
59
  "@assistant-ui/store": "^0.2.19",
60
60
  "@assistant-ui/tap": "^0.9.3",
61
61
  "@radix-ui/primitive": "^1.1.4",
62
+ "@radix-ui/react-collection": "^1.1.10",
62
63
  "@radix-ui/react-compose-refs": "^1.1.3",
63
64
  "@radix-ui/react-context": "^1.1.4",
64
65
  "@radix-ui/react-primitive": "^2.1.6",
65
66
  "@radix-ui/react-use-callback-ref": "^1.1.2",
67
+ "@radix-ui/react-use-controllable-state": "^1.2.3",
66
68
  "@radix-ui/react-use-escape-keydown": "^1.1.2",
67
69
  "assistant-cloud": "^0.1.34",
68
- "assistant-stream": "^0.3.24",
70
+ "assistant-stream": "^0.3.25",
69
71
  "nanoid": "^5.1.15",
70
72
  "radix-ui": "^1.6.0",
71
73
  "react-textarea-autosize": "^8.5.9",
72
- "safe-content-frame": "^0.0.21",
74
+ "safe-content-frame": "^0.0.22",
73
75
  "zod": "^4.4.3",
74
76
  "zustand": "^5.0.14"
75
77
  },
@@ -97,8 +99,8 @@
97
99
  "react": "^19.2.7",
98
100
  "react-dom": "^19.2.7",
99
101
  "vitest": "^4.1.9",
100
- "@assistant-ui/vite": "0.0.6",
101
- "@assistant-ui/x-buildutils": "0.0.16"
102
+ "@assistant-ui/vite": "0.0.8",
103
+ "@assistant-ui/x-buildutils": "0.0.17"
102
104
  },
103
105
  "publishConfig": {
104
106
  "access": "public",
@@ -20,6 +20,7 @@ export namespace MessagePartPrimitiveText {
20
20
  * Whether to enable smooth text streaming animation.
21
21
  * When enabled, text appears with a typing effect as it streams in.
22
22
  * Pass a `SmoothOptions` object to tune the reveal rate.
23
+ * Auto-disables under `prefers-reduced-motion: reduce`.
23
24
  * @default true
24
25
  */
25
26
  smooth?: boolean | SmoothOptions;
@@ -6,6 +6,7 @@ import {
6
6
  type ComponentRef,
7
7
  forwardRef,
8
8
  } from "react";
9
+ import { ThreadListCollection } from "../threadListFocusGroup";
9
10
 
10
11
  type PrimitiveDivProps = ComponentPropsWithoutRef<typeof Primitive.div>;
11
12
 
@@ -18,7 +19,13 @@ export const ThreadListPrimitiveRoot = forwardRef<
18
19
  ThreadListPrimitiveRoot.Element,
19
20
  ThreadListPrimitiveRoot.Props
20
21
  >((props, ref) => {
21
- return <Primitive.div {...props} ref={ref} />;
22
+ return (
23
+ <ThreadListCollection.Provider scope={undefined}>
24
+ <ThreadListCollection.Slot scope={undefined}>
25
+ <Primitive.div {...props} ref={ref} />
26
+ </ThreadListCollection.Slot>
27
+ </ThreadListCollection.Provider>
28
+ );
22
29
  });
23
30
 
24
31
  ThreadListPrimitiveRoot.displayName = "ThreadListPrimitive.Root";
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import { createCollection } from "@radix-ui/react-collection";
4
+ import { createContext, useContext, type RefObject } from "react";
5
+
6
+ const [collection, useThreadListCollection] =
7
+ createCollection<HTMLButtonElement>("ThreadList");
8
+
9
+ export const ThreadListCollection = collection;
10
+ export { useThreadListCollection };
11
+
12
+ type ThreadListItemFocus = {
13
+ triggerRef: RefObject<HTMLButtonElement | null>;
14
+ moreRef: RefObject<HTMLButtonElement | null>;
15
+ };
16
+
17
+ const ThreadListItemFocusContext = createContext<ThreadListItemFocus | null>(
18
+ null,
19
+ );
20
+
21
+ export const ThreadListItemFocusProvider = ThreadListItemFocusContext.Provider;
22
+
23
+ export const useThreadListItemFocus = (): ThreadListItemFocus | null =>
24
+ useContext(ThreadListItemFocusContext);
@@ -5,8 +5,17 @@ import {
5
5
  type ComponentRef,
6
6
  forwardRef,
7
7
  type ComponentPropsWithoutRef,
8
+ type KeyboardEvent,
9
+ useMemo,
10
+ useRef,
8
11
  } from "react";
9
12
  import { useAuiState } from "@assistant-ui/store";
13
+ import { composeEventHandlers } from "@radix-ui/primitive";
14
+ import { Direction } from "radix-ui";
15
+ import {
16
+ ThreadListItemFocusProvider,
17
+ useThreadListCollection,
18
+ } from "../threadListFocusGroup";
10
19
 
11
20
  type PrimitiveDivProps = ComponentPropsWithoutRef<typeof Primitive.div>;
12
21
 
@@ -23,12 +32,51 @@ export const ThreadListItemPrimitiveRoot = forwardRef<
23
32
  (s) => s.threads.mainThreadId === s.threadListItem.id,
24
33
  );
25
34
 
35
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
36
+ const moreRef = useRef<HTMLButtonElement | null>(null);
37
+ const focus = useMemo(() => ({ triggerRef, moreRef }), []);
38
+
39
+ const getItems = useThreadListCollection(undefined);
40
+ const direction = Direction.useDirection();
41
+
42
+ const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
43
+ const forwardKey = direction === "rtl" ? "ArrowLeft" : "ArrowRight";
44
+ const backKey = direction === "rtl" ? "ArrowRight" : "ArrowLeft";
45
+ const trigger = triggerRef.current;
46
+ const more = moreRef.current;
47
+
48
+ if (trigger && event.target === trigger) {
49
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
50
+ const triggers = getItems()
51
+ .map((item) => item.ref.current)
52
+ .filter((node): node is HTMLButtonElement => node !== null);
53
+ const next =
54
+ triggers[
55
+ triggers.indexOf(trigger) + (event.key === "ArrowDown" ? 1 : -1)
56
+ ];
57
+ if (next) {
58
+ next.focus();
59
+ event.preventDefault();
60
+ }
61
+ } else if (event.key === forwardKey && more) {
62
+ more.focus();
63
+ event.preventDefault();
64
+ }
65
+ } else if (more && event.target === more && event.key === backKey) {
66
+ trigger?.focus();
67
+ event.preventDefault();
68
+ }
69
+ };
70
+
26
71
  return (
27
- <Primitive.div
28
- {...(isMain ? { "data-active": "true", "aria-current": "true" } : null)}
29
- {...props}
30
- ref={ref}
31
- />
72
+ <ThreadListItemFocusProvider value={focus}>
73
+ <Primitive.div
74
+ {...(isMain ? { "data-active": "true", "aria-current": "true" } : null)}
75
+ {...props}
76
+ onKeyDown={composeEventHandlers(props.onKeyDown, onKeyDown)}
77
+ ref={ref}
78
+ />
79
+ </ThreadListItemFocusProvider>
32
80
  );
33
81
  });
34
82