@copilotkit/vue 2.0.0-rc.0 → 2.0.0-rc.2

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 (239) hide show
  1. package/PARITY.md +13 -12
  2. package/README.md +1 -2
  3. package/dist/components/copilot-provider/CopilotKit.vue.d.ts +3 -1
  4. package/dist/components/copilot-provider/CopilotKit.vue.d.ts.map +1 -1
  5. package/dist/index.cjs +43 -46
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.mjs +10170 -7506
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/v2/components/A2UIMessageRenderer.d.ts +7 -0
  10. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  11. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  12. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +11 -2
  13. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +136 -0
  15. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -0
  16. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChat.vue.d.ts +1 -0
  18. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  19. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +3 -1
  20. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  21. package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts +16 -0
  22. package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts.map +1 -0
  23. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  24. package/dist/v2/components/chat/CopilotChatAttachmentRenderer.vue.d.ts.map +1 -1
  25. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
  26. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts +22 -0
  27. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts.map +1 -0
  28. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +69 -16
  29. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  30. package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts +12 -0
  31. package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts.map +1 -0
  32. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts +22 -0
  33. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -0
  34. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts +20 -0
  35. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  36. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  37. package/dist/v2/components/chat/CopilotChatSuggestionPill.vue.d.ts.map +1 -1
  38. package/dist/v2/components/chat/CopilotChatSuggestionView.vue.d.ts.map +1 -1
  39. package/dist/v2/components/chat/CopilotChatToggleButton.vue.d.ts.map +1 -1
  40. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts +32 -0
  41. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -0
  42. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  43. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts +11 -1
  44. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  45. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts.map +1 -1
  46. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -0
  47. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  48. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts +5 -0
  49. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts.map +1 -0
  50. package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
  51. package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts.map +1 -1
  52. package/dist/v2/components/chat/CopilotModalHeaderTitle.d.ts.map +1 -1
  53. package/dist/v2/components/chat/CopilotPopup.vue.d.ts +1 -0
  54. package/dist/v2/components/chat/CopilotPopup.vue.d.ts.map +1 -1
  55. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts +1 -0
  56. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts.map +1 -1
  57. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts +1 -0
  58. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  59. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts.map +1 -1
  60. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts +2 -0
  61. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts.map +1 -1
  62. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +2 -0
  63. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts.map +1 -1
  64. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +2 -0
  65. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  66. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts.map +1 -1
  67. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
  68. package/dist/v2/components/chat/index.d.ts +17 -0
  69. package/dist/v2/components/chat/index.d.ts.map +1 -1
  70. package/dist/v2/components/chat/inspector-shortcuts.d.ts +12 -0
  71. package/dist/v2/components/chat/inspector-shortcuts.d.ts.map +1 -0
  72. package/dist/v2/components/chat/message-feedback.d.ts +89 -0
  73. package/dist/v2/components/chat/message-feedback.d.ts.map +1 -0
  74. package/dist/v2/components/chat/tool-call-capture.d.ts +39 -0
  75. package/dist/v2/components/chat/tool-call-capture.d.ts.map +1 -0
  76. package/dist/v2/components/chat/tool-call-display-context.d.ts +39 -0
  77. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -0
  78. package/dist/v2/components/chat/tool-call-group-parts.d.ts +144 -0
  79. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -0
  80. package/dist/v2/components/chat/tool-call-layout.d.ts +115 -0
  81. package/dist/v2/components/chat/tool-call-layout.d.ts.map +1 -0
  82. package/dist/v2/components/chat/turn-work.d.ts +35 -0
  83. package/dist/v2/components/chat/turn-work.d.ts.map +1 -0
  84. package/dist/v2/components/chat/types.d.ts +104 -4
  85. package/dist/v2/components/chat/types.d.ts.map +1 -1
  86. package/dist/v2/components/chat/use-lightbox.d.ts +18 -0
  87. package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -0
  88. package/dist/v2/components/icons/index.d.ts +1 -1
  89. package/dist/v2/components/icons/index.d.ts.map +1 -1
  90. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +55 -0
  91. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -0
  92. package/dist/v2/components/ui/CopilotPopover.vue.d.ts +40 -0
  93. package/dist/v2/components/ui/CopilotPopover.vue.d.ts.map +1 -0
  94. package/dist/v2/components/ui/CopilotTooltip.vue.d.ts +37 -0
  95. package/dist/v2/components/ui/CopilotTooltip.vue.d.ts.map +1 -0
  96. package/dist/v2/components/ui/dropdown-menu.d.ts +14 -0
  97. package/dist/v2/components/ui/dropdown-menu.d.ts.map +1 -0
  98. package/dist/v2/components/ui/floating.d.ts +84 -0
  99. package/dist/v2/components/ui/floating.d.ts.map +1 -0
  100. package/dist/v2/components/ui/presence.d.ts +9 -0
  101. package/dist/v2/components/ui/presence.d.ts.map +1 -0
  102. package/dist/v2/components/ui/use-id.d.ts +9 -0
  103. package/dist/v2/components/ui/use-id.d.ts.map +1 -0
  104. package/dist/v2/hooks/use-default-render-tool.d.ts +6 -0
  105. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  106. package/dist/v2/hooks/use-render-tool.d.ts +4 -0
  107. package/dist/v2/hooks/use-render-tool.d.ts.map +1 -1
  108. package/dist/v2/index.cjs +1 -1
  109. package/dist/v2/index.d.cts +3 -0
  110. package/dist/v2/index.d.mts +3 -0
  111. package/dist/v2/index.d.ts +3 -0
  112. package/dist/v2/index.d.ts.map +1 -1
  113. package/dist/v2/index.mjs +165 -162
  114. package/dist/v2/lib/message-feedback.d.ts +33 -0
  115. package/dist/v2/lib/message-feedback.d.ts.map +1 -0
  116. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  117. package/dist/v2/providers/CopilotKitProvider.types.d.ts +56 -1
  118. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  119. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +5 -1
  120. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  121. package/dist/v2/providers/index.d.ts +1 -1
  122. package/dist/v2/providers/index.d.ts.map +1 -1
  123. package/dist/v2/providers/keys.d.ts +8 -0
  124. package/dist/v2/providers/keys.d.ts.map +1 -1
  125. package/dist/v2/providers/types.d.ts +9 -0
  126. package/dist/v2/providers/types.d.ts.map +1 -1
  127. package/dist/v2/styles/CopilotKitStyles.d.ts +18 -0
  128. package/dist/v2/styles/CopilotKitStyles.d.ts.map +1 -0
  129. package/dist/v2/types/defineToolCallRenderer.d.ts +2 -0
  130. package/dist/v2/types/defineToolCallRenderer.d.ts.map +1 -1
  131. package/dist/v2/types/vue-tool-call-renderer.d.ts +7 -0
  132. package/dist/v2/types/vue-tool-call-renderer.d.ts.map +1 -1
  133. package/package.json +11 -23
  134. package/src/components/copilot-provider/CopilotKit.vue +4 -1
  135. package/src/v2/__tests__/exports.test.ts +4 -2
  136. package/src/v2/components/A2UIMessageRenderer.ts +55 -67
  137. package/src/v2/components/A2UISurfaceActivityRenderer.vue +11 -23
  138. package/src/v2/components/__tests__/A2UIBasicCatalogColors.test.ts +8 -9
  139. package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +63 -36
  140. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +14 -257
  141. package/src/v2/components/a2ui/A2UIRecoveryStates.ts +444 -0
  142. package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +175 -0
  143. package/src/v2/components/chat/BasicMarkdown.vue +8 -6
  144. package/src/v2/components/chat/CopilotChat.vue +7 -0
  145. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +267 -67
  146. package/src/v2/components/chat/CopilotChatAssistantToolbarButton.vue +46 -0
  147. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +141 -168
  148. package/src/v2/components/chat/CopilotChatAttachmentRenderer.vue +67 -18
  149. package/src/v2/components/chat/CopilotChatAudioRecorder.vue +2 -2
  150. package/src/v2/components/chat/CopilotChatFeedbackCommentPopover.vue +240 -0
  151. package/src/v2/components/chat/CopilotChatInput.vue +210 -161
  152. package/src/v2/components/chat/CopilotChatInspectorButton.vue +331 -0
  153. package/src/v2/components/chat/CopilotChatLightbox.vue +105 -0
  154. package/src/v2/components/chat/CopilotChatMessageView.vue +261 -18
  155. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +21 -22
  156. package/src/v2/components/chat/CopilotChatSuggestionPill.vue +5 -14
  157. package/src/v2/components/chat/CopilotChatSuggestionView.vue +1 -4
  158. package/src/v2/components/chat/CopilotChatToggleButton.vue +3 -9
  159. package/src/v2/components/chat/CopilotChatToggleButtonCloseIcon.ts +1 -1
  160. package/src/v2/components/chat/CopilotChatToggleButtonOpenIcon.ts +1 -1
  161. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +330 -0
  162. package/src/v2/components/chat/CopilotChatToolCallItem.vue +21 -11
  163. package/src/v2/components/chat/CopilotChatToolCallsView.vue +192 -21
  164. package/src/v2/components/chat/CopilotChatUserMessage.vue +105 -42
  165. package/src/v2/components/chat/CopilotChatView.vue +33 -38
  166. package/src/v2/components/chat/CopilotChatWorkSummary.vue +85 -0
  167. package/src/v2/components/chat/CopilotModalHeader.vue +10 -8
  168. package/src/v2/components/chat/CopilotModalHeaderCloseButton.ts +7 -6
  169. package/src/v2/components/chat/CopilotModalHeaderTitle.ts +1 -4
  170. package/src/v2/components/chat/CopilotPopup.vue +3 -0
  171. package/src/v2/components/chat/CopilotPopupView.vue +1 -0
  172. package/src/v2/components/chat/CopilotPopupViewInternal.vue +9 -19
  173. package/src/v2/components/chat/CopilotPopupWelcomeScreen.vue +5 -8
  174. package/src/v2/components/chat/CopilotSidebar.vue +5 -0
  175. package/src/v2/components/chat/CopilotSidebarView.vue +2 -0
  176. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -17
  177. package/src/v2/components/chat/CopilotSidebarWelcomeScreen.vue +7 -10
  178. package/src/v2/components/chat/StreamingMarkdownDefault.vue +3 -5
  179. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.inspector.test.ts +269 -0
  180. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.learningFeedback.test.ts +760 -0
  181. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.ts +3 -5
  182. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.ts +2 -2
  183. package/src/v2/components/chat/__tests__/CopilotChatInput.addMenu.test.ts +328 -0
  184. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.ts +6 -3
  185. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +22 -13
  186. package/src/v2/components/chat/__tests__/CopilotChatLightbox.test.ts +160 -0
  187. package/src/v2/components/chat/__tests__/CopilotChatMessageView.toolCallDisplay.test.ts +735 -0
  188. package/src/v2/components/chat/__tests__/CopilotChatSuggestionPill.test.ts +1 -1
  189. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.ts +3 -1
  190. package/src/v2/components/chat/__tests__/CopilotChatToolCallsView.learning.test.ts +75 -0
  191. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.attachments.test.ts +143 -0
  192. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.ts +5 -4
  193. package/src/v2/components/chat/__tests__/CopilotChatView.rootWidth.test.ts +7 -33
  194. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.ts +1 -1
  195. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +3 -3
  196. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +126 -0
  197. package/src/v2/components/chat/__tests__/messageFeedbackDefaults.test.ts +33 -0
  198. package/src/v2/components/chat/__tests__/modalChatDisplayProps.test.ts +44 -0
  199. package/src/v2/components/chat/index.ts +2 -0
  200. package/src/v2/components/chat/inspector-shortcuts.ts +34 -0
  201. package/src/v2/components/chat/message-feedback.ts +291 -0
  202. package/src/v2/components/chat/tool-call-capture.ts +55 -0
  203. package/src/v2/components/chat/tool-call-display-context.ts +49 -0
  204. package/src/v2/components/chat/tool-call-group-parts.ts +292 -0
  205. package/src/v2/components/chat/tool-call-layout.ts +304 -0
  206. package/src/v2/components/chat/turn-work.ts +61 -0
  207. package/src/v2/components/chat/types.ts +113 -3
  208. package/src/v2/components/chat/use-lightbox.ts +64 -0
  209. package/src/v2/components/icons/index.ts +4 -0
  210. package/src/v2/components/ui/CopilotDropdownMenuContent.vue +465 -0
  211. package/src/v2/components/ui/CopilotPopover.vue +138 -0
  212. package/src/v2/components/ui/CopilotTooltip.vue +237 -0
  213. package/src/v2/components/ui/__tests__/CopilotTooltip.test.ts +168 -0
  214. package/src/v2/components/ui/dropdown-menu.ts +15 -0
  215. package/src/v2/components/ui/floating.ts +321 -0
  216. package/src/v2/components/ui/presence.ts +54 -0
  217. package/src/v2/components/ui/use-id.ts +16 -0
  218. package/src/v2/hooks/use-default-render-tool.ts +123 -62
  219. package/src/v2/hooks/use-render-tool.ts +8 -0
  220. package/src/v2/index.ts +3 -0
  221. package/src/v2/lib/__tests__/message-feedback.test.ts +152 -0
  222. package/src/v2/lib/message-feedback.ts +120 -0
  223. package/src/v2/providers/CopilotChatConfigurationProvider.vue +29 -2
  224. package/src/v2/providers/CopilotKitProvider.types.ts +57 -0
  225. package/src/v2/providers/CopilotKitProvider.vue +125 -3
  226. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.openThread.test.ts +69 -0
  227. package/src/v2/providers/__tests__/CopilotKitProvider.learning.test.ts +534 -0
  228. package/src/v2/providers/index.ts +4 -1
  229. package/src/v2/providers/keys.ts +12 -0
  230. package/src/v2/providers/types.ts +11 -0
  231. package/src/v2/styles/CopilotKitStyles.ts +95 -0
  232. package/src/v2/styles/__tests__/CopilotKitStyles.client.test.ts +149 -0
  233. package/src/v2/styles/__tests__/CopilotKitStyles.ssr.test.ts +71 -0
  234. package/src/v2/types/defineToolCallRenderer.ts +4 -0
  235. package/src/v2/types/vue-tool-call-renderer.ts +7 -0
  236. package/vite.config.ts +2 -1
  237. package/dist/styles.css +0 -2
  238. package/scripts/scope-preflight.mjs +0 -100
  239. package/src/styles/globals.css +0 -233
@@ -0,0 +1,465 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * React's `<DropdownMenuContent>` and `<DropdownMenuSubContent>` (Radix) for
4
+ * Vue, drawn from a list of entries: items, separators, and submenus. It is
5
+ * teleported to `document.body` and positioned next to `anchor`.
6
+ *
7
+ * Keyboard: ArrowUp/ArrowDown move between items, Home/End jump, typing
8
+ * jumps to the next item that starts with the typed text, Enter/Space
9
+ * activate, ArrowRight/Enter/Space open a submenu and ArrowLeft closes it,
10
+ * Escape and Tab close the whole menu. Hovering an item focuses it; hovering
11
+ * a submenu trigger opens its submenu.
12
+ *
13
+ * @internal
14
+ */
15
+ import { computed, nextTick, onBeforeUnmount, ref, toRef, watch } from "vue";
16
+ import type { ComponentPublicInstance } from "vue";
17
+ import { IconChevronRight } from "../icons";
18
+ import type { FloatingAlign, FloatingSide } from "./floating";
19
+ import { useFloating } from "./floating";
20
+ import { usePresence } from "./presence";
21
+ import { useCopilotId } from "./use-id";
22
+ import type {
23
+ CopilotDropdownMenuDismissReason,
24
+ CopilotDropdownMenuEntry,
25
+ CopilotDropdownMenuItem,
26
+ } from "./dropdown-menu";
27
+
28
+ defineOptions({ name: "CopilotDropdownMenuContent", inheritAttrs: false });
29
+
30
+ const props = withDefaults(
31
+ defineProps<{
32
+ open: boolean;
33
+ anchor: HTMLElement | null;
34
+ items: CopilotDropdownMenuEntry[];
35
+ /** Shared by a menu and its submenus, to tell inside from outside. */
36
+ menuId: string;
37
+ id?: string;
38
+ labelledBy?: string;
39
+ side?: FloatingSide;
40
+ align?: FloatingAlign;
41
+ sideOffset?: number;
42
+ /** Where focus goes when it opens. */
43
+ initialFocus?: "first" | "last" | "content" | "none";
44
+ /** A submenu, drawn with `.cpk-dropdown-menu-sub-content`. */
45
+ sub?: boolean;
46
+ }>(),
47
+ {
48
+ id: undefined,
49
+ labelledBy: undefined,
50
+ side: "bottom",
51
+ align: "start",
52
+ sideOffset: 4,
53
+ initialFocus: "content",
54
+ sub: false,
55
+ },
56
+ );
57
+
58
+ const emit = defineEmits<{
59
+ /** An item was chosen; the menu should close and run it. */
60
+ select: [item: CopilotDropdownMenuItem];
61
+ /** The whole menu should close. */
62
+ dismiss: [reason: CopilotDropdownMenuDismissReason];
63
+ /** ArrowLeft in a submenu: close it and focus its trigger. */
64
+ "close-sub": [];
65
+ /** The pointer reached this content. */
66
+ "pointer-enter": [];
67
+ /** The pointer left this content. */
68
+ "pointer-leave": [];
69
+ }>();
70
+
71
+ defineSlots<{
72
+ /** Content before the items, such as a label. */
73
+ header?: () => unknown;
74
+ /** The content of an item; defaults to its label. */
75
+ item?: (props: { item: CopilotDropdownMenuItem }) => unknown;
76
+ }>();
77
+
78
+ // Radix waits this long on a submenu trigger before opening it on hover.
79
+ const SUB_OPEN_DELAY_MS = 100;
80
+ // Time the pointer has to cross from a submenu trigger onto its submenu.
81
+ const SUB_GRACE_MS = 300;
82
+ // Type-ahead text resets after this long without typing.
83
+ const TYPEAHEAD_RESET_MS = 1000;
84
+
85
+ const generatedId = useCopilotId("cpk-menu");
86
+ const contentId = computed(() => props.id ?? generatedId);
87
+ const wrapper = ref<HTMLElement | null>(null);
88
+ const content = ref<HTMLElement | null>(null);
89
+ const itemElements = ref<(HTMLElement | null)[]>([]);
90
+ const open = toRef(props, "open");
91
+ const anchor = toRef(props, "anchor");
92
+ const present = usePresence(open, content);
93
+ const openSub = ref<number | null>(null);
94
+ const subInitialFocus = ref<"first" | "none">("none");
95
+ const highlighted = ref<number | null>(null);
96
+
97
+ const { placement, wrapperStyle } = useFloating(anchor, wrapper, open, () => ({
98
+ side: props.side,
99
+ align: props.align,
100
+ sideOffset: props.sideOffset,
101
+ }));
102
+
103
+ const isGroup = (
104
+ entry: CopilotDropdownMenuEntry,
105
+ ): entry is CopilotDropdownMenuItem & {
106
+ items: CopilotDropdownMenuEntry[];
107
+ } => entry !== "-" && Array.isArray(entry.items) && entry.items.length > 0;
108
+
109
+ const setItemElement = (
110
+ index: number,
111
+ element: Element | ComponentPublicInstance | null,
112
+ ) => {
113
+ itemElements.value[index] = element instanceof HTMLElement ? element : null;
114
+ };
115
+
116
+ const focusableIndexes = () =>
117
+ props.items.flatMap((entry, index) =>
118
+ entry !== "-" && itemElements.value[index] ? [index] : [],
119
+ );
120
+
121
+ const focusItem = (index: number | undefined) => {
122
+ if (index === undefined) return;
123
+ itemElements.value[index]?.focus();
124
+ };
125
+
126
+ const focusedIndex = () => {
127
+ const active = document.activeElement;
128
+ const index = itemElements.value.findIndex((element) => element === active);
129
+ return index === -1 ? null : index;
130
+ };
131
+
132
+ watch(
133
+ [open, content],
134
+ async ([isOpen, element]) => {
135
+ if (!isOpen || !element) {
136
+ if (!isOpen) openSub.value = null;
137
+ return;
138
+ }
139
+ await nextTick();
140
+ const indexes = focusableIndexes();
141
+ if (props.initialFocus === "first") focusItem(indexes[0]);
142
+ else if (props.initialFocus === "last") focusItem(indexes.at(-1));
143
+ else if (props.initialFocus === "content") element.focus();
144
+ },
145
+ { flush: "post" },
146
+ );
147
+
148
+ let subOpenTimer: ReturnType<typeof setTimeout> | undefined;
149
+ let graceUntil = 0;
150
+
151
+ const clearSubTimer = () => {
152
+ clearTimeout(subOpenTimer);
153
+ subOpenTimer = undefined;
154
+ };
155
+
156
+ const openSubmenu = (index: number, focus: "first" | "none") => {
157
+ clearSubTimer();
158
+ subInitialFocus.value = focus;
159
+ openSub.value = index;
160
+ };
161
+
162
+ const closeSubmenu = () => {
163
+ clearSubTimer();
164
+ openSub.value = null;
165
+ };
166
+
167
+ const onSubClosedByKey = (index: number) => {
168
+ closeSubmenu();
169
+ focusItem(index);
170
+ };
171
+
172
+ let typeahead = "";
173
+ let typeaheadTimer: ReturnType<typeof setTimeout> | undefined;
174
+
175
+ const labelOf = (index: number) => {
176
+ const entry = props.items[index];
177
+ return entry && entry !== "-" ? entry.label.trim().toLowerCase() : "";
178
+ };
179
+
180
+ const runTypeahead = (key: string) => {
181
+ clearTimeout(typeaheadTimer);
182
+ typeahead += key.toLowerCase();
183
+ typeaheadTimer = setTimeout(() => {
184
+ typeahead = "";
185
+ }, TYPEAHEAD_RESET_MS);
186
+ const indexes = focusableIndexes();
187
+ const current = focusedIndex();
188
+ const position = current === null ? -1 : indexes.indexOf(current);
189
+ // Repeating one letter cycles through the items that start with it; a
190
+ // longer search may still match the focused item.
191
+ const repeated = typeahead.split("").every((c) => c === typeahead[0]);
192
+ const search = repeated ? typeahead[0]! : typeahead;
193
+ const start = Math.max(0, position + (repeated ? 1 : 0));
194
+ const ordered = [...indexes.slice(start), ...indexes.slice(0, start)];
195
+ focusItem(ordered.find((index) => labelOf(index).startsWith(search)));
196
+ };
197
+
198
+ const activate = (index: number, focus: "first" | "none") => {
199
+ const entry = props.items[index];
200
+ if (!entry || entry === "-") return;
201
+ if (isGroup(entry)) {
202
+ openSubmenu(index, focus);
203
+ return;
204
+ }
205
+ emit("select", entry);
206
+ };
207
+
208
+ const onKeyDown = (event: KeyboardEvent) => {
209
+ const indexes = focusableIndexes();
210
+ const current = focusedIndex();
211
+ const position = current === null ? -1 : indexes.indexOf(current);
212
+ const currentEntry = current === null ? undefined : props.items[current];
213
+
214
+ switch (event.key) {
215
+ case "ArrowDown":
216
+ event.preventDefault();
217
+ focusItem(indexes[Math.min(position + 1, indexes.length - 1)]);
218
+ return;
219
+ case "ArrowUp":
220
+ event.preventDefault();
221
+ focusItem(
222
+ position === -1 ? indexes.at(-1) : indexes[Math.max(position - 1, 0)],
223
+ );
224
+ return;
225
+ case "Home":
226
+ case "PageUp":
227
+ event.preventDefault();
228
+ focusItem(indexes[0]);
229
+ return;
230
+ case "End":
231
+ case "PageDown":
232
+ event.preventDefault();
233
+ focusItem(indexes.at(-1));
234
+ return;
235
+ case "Enter":
236
+ case " ":
237
+ if (current === null) return;
238
+ if (event.key === " " && typeahead) {
239
+ event.preventDefault();
240
+ runTypeahead(" ");
241
+ return;
242
+ }
243
+ event.preventDefault();
244
+ activate(current, "first");
245
+ return;
246
+ case "ArrowRight":
247
+ if (current !== null && currentEntry && isGroup(currentEntry)) {
248
+ event.preventDefault();
249
+ openSubmenu(current, "first");
250
+ }
251
+ return;
252
+ case "ArrowLeft":
253
+ if (props.sub) {
254
+ event.preventDefault();
255
+ emit("close-sub");
256
+ }
257
+ return;
258
+ case "Escape":
259
+ event.preventDefault();
260
+ // Escape closes only the menu, not a chat popup around it.
261
+ event.stopPropagation();
262
+ emit("dismiss", "escape");
263
+ return;
264
+ case "Tab":
265
+ event.preventDefault();
266
+ emit("dismiss", "tab");
267
+ return;
268
+ }
269
+
270
+ if (
271
+ event.key.length === 1 &&
272
+ !event.ctrlKey &&
273
+ !event.metaKey &&
274
+ !event.altKey
275
+ ) {
276
+ runTypeahead(event.key);
277
+ }
278
+ };
279
+
280
+ const onItemPointerMove = (index: number, event: PointerEvent) => {
281
+ if (event.pointerType !== "mouse" && event.pointerType !== "pen") return;
282
+ if (Date.now() < graceUntil && openSub.value !== index) return;
283
+ graceUntil = 0;
284
+ const element = itemElements.value[index];
285
+ if (element && document.activeElement !== element) element.focus();
286
+ const entry = props.items[index];
287
+ if (entry && isGroup(entry)) {
288
+ if (openSub.value !== index && subOpenTimer === undefined) {
289
+ subOpenTimer = setTimeout(
290
+ () => openSubmenu(index, "none"),
291
+ SUB_OPEN_DELAY_MS,
292
+ );
293
+ }
294
+ } else if (openSub.value !== null) {
295
+ closeSubmenu();
296
+ }
297
+ };
298
+
299
+ const onItemPointerLeave = (index: number, event: PointerEvent) => {
300
+ if (event.pointerType !== "mouse" && event.pointerType !== "pen") return;
301
+ const entry = props.items[index];
302
+ if (entry && isGroup(entry)) {
303
+ clearSubTimer();
304
+ if (openSub.value === index) {
305
+ graceUntil = Date.now() + SUB_GRACE_MS;
306
+ return;
307
+ }
308
+ }
309
+ if (Date.now() < graceUntil) return;
310
+ // As Radix does, focus leaves the item for the menu itself.
311
+ if (document.activeElement === itemElements.value[index]) {
312
+ content.value?.focus();
313
+ }
314
+ };
315
+
316
+ const onItemClick = (index: number) => {
317
+ activate(index, "none");
318
+ };
319
+
320
+ const onSubPointerEnter = () => {
321
+ graceUntil = 0;
322
+ };
323
+
324
+ // The root closes on a pointer down or focus outside the menu, its
325
+ // submenus, and its trigger, which toggles the menu itself.
326
+ const isInsideMenu = (target: EventTarget | null) =>
327
+ target instanceof Element &&
328
+ (target.closest(`[data-cpk-menu="${props.menuId}"]`) !== null ||
329
+ (props.anchor?.contains(target) ?? false));
330
+
331
+ const onOutside = (event: Event) => {
332
+ if (!isInsideMenu(event.target)) emit("dismiss", "outside");
333
+ };
334
+
335
+ const listen = (on: boolean) => {
336
+ if (props.sub || typeof document === "undefined") return;
337
+ const method = on ? "addEventListener" : "removeEventListener";
338
+ document[method]("pointerdown", onOutside, { capture: true });
339
+ document[method]("focusin", onOutside, { capture: true });
340
+ };
341
+
342
+ watch(open, (isOpen) => listen(isOpen), { immediate: true });
343
+
344
+ onBeforeUnmount(() => {
345
+ listen(false);
346
+ clearSubTimer();
347
+ clearTimeout(typeaheadTimer);
348
+ });
349
+
350
+ const contentStyle = computed(() => ({
351
+ outline: "none",
352
+ "--radix-dropdown-menu-content-transform-origin":
353
+ placement.value?.transformOrigin ?? "center",
354
+ "--radix-dropdown-menu-content-available-height": placement.value
355
+ ? `${placement.value.availableHeight}px`
356
+ : undefined,
357
+ }));
358
+
359
+ const subId = (index: number) => `${contentId.value}-sub-${index}`;
360
+ </script>
361
+
362
+ <template>
363
+ <Teleport to="body">
364
+ <div
365
+ v-if="present"
366
+ ref="wrapper"
367
+ data-radix-popper-content-wrapper
368
+ :style="wrapperStyle()"
369
+ >
370
+ <div
371
+ :id="contentId"
372
+ ref="content"
373
+ data-copilotkit
374
+ :data-cpk-menu="menuId"
375
+ :data-slot="sub ? 'dropdown-menu-sub-content' : 'dropdown-menu-content'"
376
+ :class="
377
+ sub ? 'cpk-dropdown-menu-sub-content' : 'cpk-dropdown-menu-content'
378
+ "
379
+ role="menu"
380
+ aria-orientation="vertical"
381
+ :aria-labelledby="labelledBy"
382
+ tabindex="-1"
383
+ :data-state="open ? 'open' : 'closed'"
384
+ :data-side="placement?.side ?? side"
385
+ :data-align="placement?.align ?? align"
386
+ :style="contentStyle"
387
+ v-bind="$attrs"
388
+ @keydown="onKeyDown"
389
+ @pointerenter="emit('pointer-enter')"
390
+ @pointerleave="emit('pointer-leave')"
391
+ >
392
+ <slot name="header" />
393
+ <template v-for="(entry, index) in items" :key="index">
394
+ <div
395
+ v-if="entry === '-'"
396
+ role="separator"
397
+ aria-orientation="horizontal"
398
+ data-slot="dropdown-menu-separator"
399
+ class="cpk-dropdown-menu-separator"
400
+ />
401
+ <template v-else-if="isGroup(entry)">
402
+ <div
403
+ :id="`${subId(index)}-trigger`"
404
+ :ref="(element) => setItemElement(index, element)"
405
+ role="menuitem"
406
+ tabindex="-1"
407
+ aria-haspopup="menu"
408
+ :aria-expanded="openSub === index"
409
+ :aria-controls="openSub === index ? subId(index) : undefined"
410
+ data-slot="dropdown-menu-sub-trigger"
411
+ class="cpk-dropdown-menu-sub-trigger"
412
+ :data-state="openSub === index ? 'open' : 'closed'"
413
+ :data-highlighted="highlighted === index ? '' : undefined"
414
+ @focus="highlighted = index"
415
+ @blur="highlighted = null"
416
+ @pointermove="onItemPointerMove(index, $event)"
417
+ @pointerleave="onItemPointerLeave(index, $event)"
418
+ @click="onItemClick(index)"
419
+ >
420
+ {{ entry.label }}
421
+ <IconChevronRight
422
+ class="cpk-dropdown-menu-sub-trigger-icon"
423
+ aria-hidden="true"
424
+ />
425
+ </div>
426
+ <CopilotDropdownMenuContent
427
+ :id="subId(index)"
428
+ :open="open && openSub === index"
429
+ :anchor="itemElements[index] ?? null"
430
+ :items="entry.items"
431
+ :menu-id="menuId"
432
+ :labelled-by="`${subId(index)}-trigger`"
433
+ side="right"
434
+ align="start"
435
+ :side-offset="0"
436
+ :initial-focus="subInitialFocus"
437
+ sub
438
+ @select="emit('select', $event)"
439
+ @dismiss="emit('dismiss', $event)"
440
+ @close-sub="onSubClosedByKey(index)"
441
+ @pointer-enter="onSubPointerEnter"
442
+ />
443
+ </template>
444
+ <div
445
+ v-else
446
+ :ref="(element) => setItemElement(index, element)"
447
+ role="menuitem"
448
+ tabindex="-1"
449
+ data-slot="dropdown-menu-item"
450
+ data-variant="default"
451
+ :class="['cpk-dropdown-menu-item', entry.class]"
452
+ :data-highlighted="highlighted === index ? '' : undefined"
453
+ @focus="highlighted = index"
454
+ @blur="highlighted = null"
455
+ @pointermove="onItemPointerMove(index, $event)"
456
+ @pointerleave="onItemPointerLeave(index, $event)"
457
+ @click="onItemClick(index)"
458
+ >
459
+ <slot name="item" :item="entry">{{ entry.label }}</slot>
460
+ </div>
461
+ </template>
462
+ </div>
463
+ </div>
464
+ </Teleport>
465
+ </template>
@@ -0,0 +1,138 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * React's `<PopoverContent>` (Radix, non-modal) for Vue: content in a portal
4
+ * next to an anchor element, so it floats over the page instead of taking
5
+ * room in the layout. Escape, a pointer down outside, and focus moving
6
+ * outside dismiss it, unless the handler of `escape-key-down` or
7
+ * `interact-outside` calls `preventDefault()`. It does not move focus by
8
+ * itself; the parent decides where focus goes.
9
+ *
10
+ * @internal
11
+ */
12
+ import { computed, onBeforeUnmount, ref, toRef, watch } from "vue";
13
+ import type { FloatingAlign, FloatingSide } from "./floating";
14
+ import { useFloating } from "./floating";
15
+ import { usePresence } from "./presence";
16
+ import { useCopilotId } from "./use-id";
17
+
18
+ defineOptions({ inheritAttrs: false });
19
+
20
+ const props = withDefaults(
21
+ defineProps<{
22
+ open: boolean;
23
+ /** The element the content points at. */
24
+ anchor: HTMLElement | null;
25
+ side?: FloatingSide;
26
+ align?: FloatingAlign;
27
+ sideOffset?: number;
28
+ collisionPadding?: number;
29
+ /** Hides the content while its anchor is scrolled out of view. */
30
+ hideWhenDetached?: boolean;
31
+ }>(),
32
+ {
33
+ side: "bottom",
34
+ align: "start",
35
+ sideOffset: 6,
36
+ collisionPadding: 0,
37
+ hideWhenDetached: false,
38
+ },
39
+ );
40
+
41
+ const emit = defineEmits<{
42
+ /** Escape was pressed while it was open. */
43
+ "escape-key-down": [event: KeyboardEvent];
44
+ /** A pointer down or focus outside it. */
45
+ "interact-outside": [event: Event];
46
+ /** It should close: the events above were not prevented. */
47
+ dismiss: [];
48
+ }>();
49
+
50
+ defineSlots<{ default(): unknown }>();
51
+
52
+ const id = useCopilotId("cpk-popover");
53
+ const wrapper = ref<HTMLElement | null>(null);
54
+ const content = ref<HTMLElement | null>(null);
55
+ const open = toRef(props, "open");
56
+ const anchor = toRef(props, "anchor");
57
+ const present = usePresence(open, content);
58
+
59
+ const { placement, wrapperStyle } = useFloating(anchor, wrapper, open, () => ({
60
+ side: props.side,
61
+ align: props.align,
62
+ sideOffset: props.sideOffset,
63
+ collisionPadding: props.collisionPadding,
64
+ hideWhenDetached: props.hideWhenDetached,
65
+ }));
66
+
67
+ const isInside = (target: EventTarget | null) =>
68
+ target instanceof Node && !!content.value?.contains(target);
69
+
70
+ const onKeyDown = (event: KeyboardEvent) => {
71
+ if (event.key !== "Escape") return;
72
+ emit("escape-key-down", event);
73
+ if (!event.defaultPrevented) emit("dismiss");
74
+ };
75
+
76
+ const onOutside = (event: Event) => {
77
+ if (isInside(event.target)) return;
78
+ emit("interact-outside", event);
79
+ if (!event.defaultPrevented) emit("dismiss");
80
+ };
81
+
82
+ const listen = (on: boolean) => {
83
+ if (typeof document === "undefined") return;
84
+ // Capture, as Radix does, so a handler can stop Escape from also reaching
85
+ // a chat popup's own Escape listener.
86
+ if (on) {
87
+ document.addEventListener("keydown", onKeyDown, { capture: true });
88
+ document.addEventListener("pointerdown", onOutside, { capture: true });
89
+ document.addEventListener("focusin", onOutside, { capture: true });
90
+ } else {
91
+ document.removeEventListener("keydown", onKeyDown, { capture: true });
92
+ document.removeEventListener("pointerdown", onOutside, { capture: true });
93
+ document.removeEventListener("focusin", onOutside, { capture: true });
94
+ }
95
+ };
96
+
97
+ watch(open, (isOpen) => listen(isOpen), { immediate: true });
98
+ onBeforeUnmount(() => listen(false));
99
+
100
+ const contentStyle = computed(() => ({
101
+ "--radix-popover-content-transform-origin":
102
+ placement.value?.transformOrigin ?? "center",
103
+ "--radix-popover-content-available-height": placement.value
104
+ ? `${placement.value.availableHeight}px`
105
+ : undefined,
106
+ }));
107
+
108
+ defineExpose({ content });
109
+ </script>
110
+
111
+ <template>
112
+ <Teleport to="body">
113
+ <div
114
+ v-if="present"
115
+ ref="wrapper"
116
+ data-radix-popper-content-wrapper
117
+ :style="wrapperStyle()"
118
+ >
119
+ <div
120
+ :id="id"
121
+ ref="content"
122
+ role="dialog"
123
+ tabindex="-1"
124
+ data-copilotkit
125
+ data-copilotkit-popover
126
+ data-slot="popover-content"
127
+ class="cpk-popover"
128
+ :data-state="open ? 'open' : 'closed'"
129
+ :data-side="placement?.side ?? side"
130
+ :data-align="placement?.align ?? align"
131
+ :style="contentStyle"
132
+ v-bind="$attrs"
133
+ >
134
+ <slot />
135
+ </div>
136
+ </div>
137
+ </Teleport>
138
+ </template>