@djangocfg/ui-tools 2.1.404 → 2.1.408

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 (336) hide show
  1. package/README.md +9 -11
  2. package/dist/file-icon/index.cjs +449 -61
  3. package/dist/file-icon/index.cjs.map +1 -1
  4. package/dist/file-icon/index.d.cts +56 -18
  5. package/dist/file-icon/index.d.ts +56 -18
  6. package/dist/file-icon/index.mjs +448 -62
  7. package/dist/file-icon/index.mjs.map +1 -1
  8. package/dist/tree/index.cjs +49 -22
  9. package/dist/tree/index.cjs.map +1 -1
  10. package/dist/tree/index.d.cts +9 -3
  11. package/dist/tree/index.d.ts +9 -3
  12. package/dist/tree/index.mjs +49 -22
  13. package/dist/tree/index.mjs.map +1 -1
  14. package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
  15. package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
  16. package/package.json +13 -16
  17. package/src/components/FloatingToolbar/index.tsx +37 -3
  18. package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
  19. package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
  20. package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
  21. package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
  22. package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
  23. package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
  24. package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
  25. package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
  26. package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
  27. package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
  28. package/src/lib/page-snapshot/capture/budget.ts +95 -0
  29. package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
  30. package/src/lib/page-snapshot/capture/classify.ts +111 -0
  31. package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
  32. package/src/lib/page-snapshot/capture/fold.ts +96 -0
  33. package/src/lib/page-snapshot/capture/scope.ts +169 -0
  34. package/src/lib/page-snapshot/capture/walk.ts +250 -0
  35. package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
  36. package/src/lib/page-snapshot/cst/directives.ts +47 -0
  37. package/src/lib/page-snapshot/cst/payload.ts +50 -0
  38. package/src/lib/page-snapshot/cst/serialize.ts +84 -0
  39. package/src/lib/page-snapshot/cst/types.ts +115 -0
  40. package/src/lib/page-snapshot/engine.ts +176 -0
  41. package/src/lib/page-snapshot/index.ts +93 -0
  42. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
  43. package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
  44. package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
  45. package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
  46. package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
  47. package/src/lib/page-snapshot/react/provider.tsx +162 -0
  48. package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
  49. package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
  50. package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
  51. package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
  52. package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
  53. package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
  54. package/src/lib/page-snapshot/redaction/audit.ts +58 -0
  55. package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
  56. package/src/lib/page-snapshot/redaction/index.ts +75 -0
  57. package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
  58. package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
  59. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
  60. package/src/lib/page-snapshot/refs/registry.ts +46 -0
  61. package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
  62. package/src/lib/page-snapshot/staleness/hash.ts +20 -0
  63. package/src/lib/page-snapshot/tokens.ts +15 -0
  64. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
  65. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
  66. package/src/tools/AudioPlayer/lazy.tsx +13 -27
  67. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
  68. package/src/tools/Chat/README.md +267 -39
  69. package/src/tools/Chat/composer/Composer.tsx +471 -0
  70. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  71. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  72. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  73. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  74. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  75. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  76. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  77. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  78. package/src/tools/Chat/composer/index.ts +45 -0
  79. package/src/tools/Chat/composer/size-context.tsx +26 -0
  80. package/src/tools/Chat/composer/types.ts +143 -0
  81. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  82. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  83. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  84. package/src/tools/Chat/core/index.ts +23 -1
  85. package/src/tools/Chat/core/markdown.ts +1 -1
  86. package/src/tools/Chat/core/metadata.ts +47 -0
  87. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  88. package/src/tools/Chat/core/transport/http.ts +71 -32
  89. package/src/tools/Chat/core/transport/sse.ts +18 -10
  90. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  91. package/src/tools/Chat/highlight/README.md +103 -0
  92. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  93. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  94. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  95. package/src/tools/Chat/highlight/index.ts +21 -0
  96. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  97. package/src/tools/Chat/highlight/types.ts +49 -0
  98. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  99. package/src/tools/Chat/hooks/index.ts +0 -5
  100. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  101. package/src/tools/Chat/hooks/useChat.ts +47 -14
  102. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  103. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  104. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  105. package/src/tools/Chat/index.ts +25 -219
  106. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  107. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  108. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  109. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  110. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  111. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  112. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  113. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  114. package/src/tools/Chat/launcher/header/index.ts +26 -0
  115. package/src/tools/Chat/launcher/index.ts +3 -10
  116. package/src/tools/Chat/lazy.tsx +38 -284
  117. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  118. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  119. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  120. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  121. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  122. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  123. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  124. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  125. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  126. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  127. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  128. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  130. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  131. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  132. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  133. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  134. package/src/tools/Chat/public.ts +212 -0
  135. package/src/tools/Chat/shell/ChatRoot.tsx +326 -0
  136. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  137. package/src/tools/Chat/shell/index.ts +15 -0
  138. package/src/tools/Chat/types/block.ts +120 -0
  139. package/src/tools/Chat/types/config.ts +0 -5
  140. package/src/tools/Chat/types/index.ts +17 -0
  141. package/src/tools/Chat/types/message.ts +3 -0
  142. package/src/tools/Chat/utils/index.ts +4 -0
  143. package/src/tools/CodeEditor/README.md +4 -6
  144. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  145. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  146. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  147. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  148. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  149. package/src/tools/CodeEditor/lazy.tsx +6 -0
  150. package/src/tools/CodeEditor/lib/index.ts +1 -1
  151. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  152. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  153. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  154. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  155. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  156. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  157. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  158. package/src/tools/CronScheduler/index.tsx +1 -1
  159. package/src/tools/CronScheduler/types/index.ts +8 -3
  160. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  161. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  162. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  163. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  164. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  165. package/src/tools/FileIcon/index.ts +4 -0
  166. package/src/tools/FileIcon/loader.ts +17 -35
  167. package/src/tools/FileIcon/specialFolders.ts +18 -0
  168. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  169. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  170. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  171. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  172. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  173. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  174. package/src/tools/ImageViewer/components/ImageViewer.tsx +106 -26
  175. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  176. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  177. package/src/tools/ImageViewer/utils/index.ts +1 -0
  178. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  179. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  180. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  181. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  182. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  183. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  184. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  185. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  186. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  187. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  188. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  189. package/src/tools/JsonForm/widgets/index.ts +1 -0
  190. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  191. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  192. package/src/tools/LottiePlayer/index.tsx +14 -4
  193. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  194. package/src/tools/LottiePlayer/types.ts +31 -1
  195. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  196. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  197. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  198. package/src/tools/Map/components/MapCluster.tsx +28 -21
  199. package/src/tools/Map/components/MapContainer.tsx +11 -4
  200. package/src/tools/Map/components/MapLegend.tsx +46 -15
  201. package/src/tools/Map/components/MapMarker.tsx +31 -2
  202. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  203. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  204. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  205. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  206. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  207. package/src/tools/MarkdownEditor/styles.css +13 -0
  208. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  209. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  210. package/src/tools/MarkdownMessage/components.tsx +22 -9
  211. package/src/tools/MarkdownMessage/types.ts +24 -1
  212. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  213. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  214. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  215. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  216. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  217. package/src/tools/Mermaid/index.tsx +6 -0
  218. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  221. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  222. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  223. package/src/tools/OpenapiViewer/constants.ts +3 -0
  224. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  225. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  226. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  227. package/src/tools/PrettyCode/lazy.tsx +1 -1
  228. package/src/tools/SpeechRecognition/README.md +1 -1
  229. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  230. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  231. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  232. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  233. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  234. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  235. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  236. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  237. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  238. package/src/tools/Tree/README.md +4 -8
  239. package/src/tools/Tree/TreeRoot.tsx +22 -10
  240. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  241. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  242. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  243. package/src/tools/Tree/data/flatten.ts +10 -4
  244. package/src/tools/Tree/types.ts +4 -0
  245. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  246. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  247. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  248. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  249. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  250. package/src/tools/Uploader/utils/formatters.ts +8 -3
  251. package/src/tools/VideoPlayer/README.md +87 -230
  252. package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
  253. package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
  254. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +39 -0
  255. package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
  256. package/src/tools/VideoPlayer/canvas/index.ts +12 -0
  257. package/src/tools/VideoPlayer/canvas/jsx-augmentation.ts +47 -0
  258. package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
  259. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +40 -0
  260. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +78 -0
  261. package/src/tools/VideoPlayer/index.ts +51 -65
  262. package/src/tools/VideoPlayer/lazy.tsx +11 -54
  263. package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
  264. package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
  265. package/src/tools/VideoPlayer/parts/index.ts +15 -0
  266. package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
  267. package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
  268. package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
  269. package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
  270. package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
  271. package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
  272. package/src/tools/VideoPlayer/styles/video-player.css +141 -0
  273. package/src/tools/VideoPlayer/types.ts +82 -0
  274. package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
  275. package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
  276. package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
  277. package/src/tools/index.ts +37 -29
  278. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  279. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  280. package/src/tools/Chat/components/Composer.tsx +0 -216
  281. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  282. package/src/tools/Chat/types.ts +0 -9
  283. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  284. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  285. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  286. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  287. package/src/tools/Tour/README.md +0 -373
  288. package/src/tools/Tour/components/Tour.tsx +0 -12
  289. package/src/tools/Tour/components/TourContent.tsx +0 -171
  290. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  291. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  292. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  293. package/src/tools/Tour/components/index.ts +0 -5
  294. package/src/tools/Tour/context/TourContext.ts +0 -19
  295. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  296. package/src/tools/Tour/context/index.ts +0 -2
  297. package/src/tools/Tour/hooks/index.ts +0 -3
  298. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  299. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  300. package/src/tools/Tour/hooks/useTour.ts +0 -42
  301. package/src/tools/Tour/index.ts +0 -38
  302. package/src/tools/Tour/types/index.ts +0 -224
  303. package/src/tools/Tour/utils/dom.ts +0 -98
  304. package/src/tools/Tour/utils/index.ts +0 -3
  305. package/src/tools/Tour/utils/logger.ts +0 -3
  306. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  307. package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
  308. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
  309. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
  310. package/src/tools/VideoPlayer/components/index.ts +0 -14
  311. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
  312. package/src/tools/VideoPlayer/context/index.ts +0 -8
  313. package/src/tools/VideoPlayer/hooks/index.ts +0 -12
  314. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
  315. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
  316. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
  317. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
  318. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
  319. package/src/tools/VideoPlayer/providers/index.ts +0 -8
  320. package/src/tools/VideoPlayer/types/index.ts +0 -38
  321. package/src/tools/VideoPlayer/types/player.ts +0 -116
  322. package/src/tools/VideoPlayer/types/provider.ts +0 -93
  323. package/src/tools/VideoPlayer/types/sources.ts +0 -97
  324. package/src/tools/VideoPlayer/utils/debug.ts +0 -14
  325. package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
  326. package/src/tools/VideoPlayer/utils/index.ts +0 -12
  327. package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
  328. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  329. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  330. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  331. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  332. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  333. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  334. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  335. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  336. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -0,0 +1,471 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, useEffect, useRef } from 'react';
4
+
5
+ import { Textarea } from '@djangocfg/ui-core/components';
6
+ import { cn } from '@djangocfg/ui-core/lib';
7
+
8
+ import { useChatContextOptional } from '../context';
9
+ import type { UseChatComposerReturn } from '../hooks/useChatComposer';
10
+ import { Attachments } from '../messages/Attachments';
11
+ import { ComposerActionBar } from './ComposerActionBar';
12
+ import { ComposerButton } from './ComposerButton';
13
+ import { ComposerFooter } from './ComposerFooter';
14
+ import { ComposerSizeProvider } from './size-context';
15
+ import { useComposerActions } from './useComposerActions';
16
+ import type {
17
+ ComposerAppearance,
18
+ ComposerFooterProps,
19
+ ComposerLayout,
20
+ ComposerSize,
21
+ ComposerSlotComponents,
22
+ ComposerSlotProps,
23
+ ComposerSlots,
24
+ } from './types';
25
+
26
+ export type { ComposerSize, ComposerAppearance } from './types';
27
+
28
+ export interface ComposerProps {
29
+ composer: UseChatComposerReturn;
30
+ placeholder?: string;
31
+ disabled?: boolean;
32
+ showAttachmentButton?: boolean;
33
+ onPickFiles?: () => void;
34
+ className?: string;
35
+ textareaClassName?: string;
36
+ /** Visual size — controls textarea height + button slot size.
37
+ *
38
+ * - ``sm`` — dense compact composer (admin sidebars, etc); defaults
39
+ * to the `inline` single-row layout.
40
+ * - ``md`` — default. Stacked layout (textarea + action bar).
41
+ * - ``lg`` — generous textarea. Use when the chat is the page's
42
+ * primary surface (onboarding, support).
43
+ */
44
+ size?: ComposerSize;
45
+ /** Spaciousness of the surface — orthogonal to `size`.
46
+ *
47
+ * - ``compact`` — default; embedded composer (FAB panel, dock).
48
+ * - ``full`` — full-page chat; roomier surface, taller textarea,
49
+ * larger padding + radius. The host owns the centered max-width
50
+ * container; the composer just makes itself more spacious.
51
+ */
52
+ appearance?: ComposerAppearance;
53
+ /** Show "Stop" button instead of "Send" while streaming. */
54
+ isStreaming?: boolean;
55
+ onCancel?: () => void;
56
+
57
+ // ── Slot system ──────────────────────────────────────────────────────────
58
+ /** Layout geometry. Default `stacked`; `size="sm"` falls back to `inline`. */
59
+ layout?: ComposerLayout;
60
+ /** Tier A — declarative action descriptors + raw-node escape hatches. */
61
+ composerSlots?: ComposerSlots;
62
+ /** Tier B — replace a primitive entirely. */
63
+ slots?: ComposerSlotComponents;
64
+ /** Per-slot prop overrides for the built-in primitives. */
65
+ slotProps?: ComposerSlotProps;
66
+ /** Telegram-style mic↔send swap. Default `true`. */
67
+ micSendSwap?: boolean;
68
+ /** Footer toolbar config below the input surface. `false` hides it. */
69
+ footer?: ComposerFooterProps | false;
70
+ }
71
+
72
+ const SIZE_CLASSES: Record<ComposerSize, {
73
+ surface: string;
74
+ textarea: string;
75
+ text: string;
76
+ containerPadding: string;
77
+ /** inline-layout slot height enforcement */
78
+ inlineSlot: string;
79
+ }> = {
80
+ sm: {
81
+ surface: 'rounded-xl px-2 py-1',
82
+ textarea: 'min-h-7 max-h-44 px-1.5 py-1',
83
+ text: 'text-sm',
84
+ containerPadding: 'px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]',
85
+ inlineSlot: '[&>:not(textarea)]:h-7',
86
+ },
87
+ md: {
88
+ surface: 'rounded-2xl px-2 py-1.5',
89
+ textarea: 'min-h-9 max-h-60 px-2 py-1.5',
90
+ text: 'text-base sm:text-sm',
91
+ containerPadding: 'px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
92
+ inlineSlot: '[&>:not(textarea)]:h-9',
93
+ },
94
+ lg: {
95
+ surface: 'rounded-2xl px-3 py-2.5',
96
+ textarea: 'min-h-12 max-h-72 px-2.5 py-2',
97
+ text: 'text-base',
98
+ containerPadding: 'px-3.5 pt-3 pb-[max(0.875rem,env(safe-area-inset-bottom))]',
99
+ inlineSlot: '[&>:not(textarea)]:h-12',
100
+ },
101
+ };
102
+
103
+ /** Per-size cluster gap for the stacked grid layout. */
104
+ const GAP: Record<ComposerSize, string> = {
105
+ sm: 'gap-0.5',
106
+ md: 'gap-1',
107
+ lg: 'gap-1.5',
108
+ };
109
+
110
+ /** Extra spaciousness layered on top of `size` when `appearance="full"`.
111
+ * Empty strings for `compact` keep the current geometry untouched. */
112
+ const APPEARANCE_CLASSES: Record<ComposerAppearance, {
113
+ surface: string;
114
+ textarea: string;
115
+ text: string;
116
+ containerPadding: string;
117
+ }> = {
118
+ compact: { surface: '', textarea: '', text: '', containerPadding: '' },
119
+ full: {
120
+ surface: 'rounded-3xl px-3 py-2',
121
+ textarea: 'min-h-10 max-h-80 px-2.5 py-2',
122
+ text: 'text-base',
123
+ containerPadding: 'px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]',
124
+ },
125
+ };
126
+
127
+ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Composer(
128
+ {
129
+ composer,
130
+ placeholder = 'Type a message...',
131
+ disabled,
132
+ showAttachmentButton = false,
133
+ onPickFiles,
134
+ className,
135
+ textareaClassName,
136
+ size = 'md',
137
+ appearance = 'compact',
138
+ isStreaming: isStreamingProp,
139
+ onCancel: onCancelProp,
140
+ layout: layoutProp,
141
+ composerSlots,
142
+ slots,
143
+ slotProps,
144
+ micSendSwap = true,
145
+ footer,
146
+ },
147
+ ref,
148
+ ) {
149
+ const ctx = useChatContextOptional();
150
+ const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
151
+ const onCancel = onCancelProp ?? ctx?.cancelStream;
152
+ const isDisabled = disabled ?? isStreaming;
153
+ const sz = SIZE_CLASSES[size];
154
+ // `appearance` is orthogonal to `size` — `full` layers extra
155
+ // spaciousness (radius, padding, textarea height) over the chosen size.
156
+ const ap = APPEARANCE_CLASSES[appearance];
157
+ // `size="sm"` defaults to the single-row `inline` layout unless the
158
+ // host overrides it explicitly — see §3.1 of the redesign doc.
159
+ const layout: ComposerLayout = layoutProp ?? (size === 'sm' ? 'inline' : 'stacked');
160
+
161
+ // Register the composer's handle with the chat context so other parts
162
+ // of the tree (useAutoFocusOnStreamEnd, VoiceComposerSlot) can drive it
163
+ // imperatively. No-op when used outside a ChatProvider.
164
+ const register = ctx?.registerComposer;
165
+ const composerFocus = composer.focus;
166
+ const composerSetValue = composer.setValue;
167
+ const textareaRef = composer.textareaRef;
168
+ const getValueRef = useRef<() => string>(() => composer.value);
169
+ getValueRef.current = () => composer.value;
170
+ // A custom `Textarea` slot (e.g. the TipTap `<ComposerRichTextarea>`)
171
+ // has no `<textarea>` — the default `composer.focus` would be a no-op.
172
+ // In that case the slot registers its own imperative handle; skip
173
+ // ours so we don't overwrite it (child effects run before parent).
174
+ const hasCustomTextarea = !!slots?.Textarea;
175
+ useEffect(() => {
176
+ if (!register || hasCustomTextarea) return;
177
+ register({
178
+ focus: composerFocus,
179
+ moveCursorToEnd: () => {
180
+ const el = textareaRef.current;
181
+ if (!el) return;
182
+ const end = el.value.length;
183
+ el.setSelectionRange(end, end);
184
+ },
185
+ getValue: () => getValueRef.current(),
186
+ setValue: composerSetValue,
187
+ });
188
+ return () => register(null);
189
+ }, [register, hasCustomTextarea, composerFocus, composerSetValue, textareaRef]);
190
+
191
+ const inlineStart = composerSlots?.inlineStart;
192
+ const inlineEnd = composerSlots?.inlineEnd;
193
+ const blockStart = composerSlots?.blockStart;
194
+
195
+ // Clicking the padding around the input (not a button/link) should focus
196
+ // the editor — same affordance as ChatGPT/Gemini. Works for both the
197
+ // plain `<textarea>` and the TipTap (`contenteditable`) backend by
198
+ // querying the focusable node inside the surface itself, so it does not
199
+ // depend on which textarea variant is mounted.
200
+ const surfaceRef = useRef<HTMLDivElement>(null);
201
+ const handleSurfaceMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
202
+ if (isDisabled) return;
203
+ const target = e.target as HTMLElement;
204
+ // A real interactive element (or the editor itself) handles its own
205
+ // focus — only act on clicks that land on the bare surface padding.
206
+ if (
207
+ target.closest(
208
+ 'textarea, input, button, a, [contenteditable="true"], [role="button"], [role="menu"]',
209
+ )
210
+ ) {
211
+ return;
212
+ }
213
+ const editable = surfaceRef.current?.querySelector<HTMLElement>(
214
+ 'textarea, [contenteditable="true"]',
215
+ );
216
+ if (!editable) return;
217
+ // Prevent the mousedown from blurring/again-selecting and move focus
218
+ // (caret lands at the end via the textarea's own selection on focus).
219
+ e.preventDefault();
220
+ editable.focus();
221
+ };
222
+
223
+ // Merge built-in send/stop/attach descriptors with host arrays.
224
+ const { actionsStart, actionsEnd } = useComposerActions({
225
+ composer,
226
+ isStreaming,
227
+ isDisabled,
228
+ showAttachmentButton,
229
+ onPickFiles,
230
+ actionsStart: composerSlots?.actionsStart,
231
+ actionsEnd: composerSlots?.actionsEnd,
232
+ onSend: () => void composer.submit(),
233
+ onCancel,
234
+ micSendSwap,
235
+ });
236
+
237
+ const SendSlot = slots?.SendButton;
238
+ const AttachSlot = slots?.AttachButton;
239
+ const TextareaSlot = slots?.Textarea;
240
+ const ActionBarSlot = slots?.ActionBar;
241
+
242
+ // ── Textarea ───────────────────────────────────────────────────────────
243
+ const textareaNode = TextareaSlot ? (
244
+ <TextareaSlot
245
+ composer={composer}
246
+ placeholder={placeholder}
247
+ disabled={isDisabled}
248
+ size={size}
249
+ className={slotProps?.textarea?.className ?? textareaClassName}
250
+ />
251
+ ) : (
252
+ <Textarea
253
+ {...composer.textareaProps}
254
+ rows={1}
255
+ placeholder={placeholder}
256
+ aria-label={placeholder}
257
+ aria-multiline="true"
258
+ aria-keyshortcuts="Enter"
259
+ disabled={isDisabled}
260
+ className={cn(
261
+ 'flex-1 resize-none border-0 bg-transparent shadow-none',
262
+ 'focus-visible:ring-0 focus-visible:outline-none',
263
+ sz.textarea,
264
+ sz.text,
265
+ ap.textarea,
266
+ ap.text,
267
+ slotProps?.textarea?.className,
268
+ textareaClassName,
269
+ )}
270
+ />
271
+ );
272
+
273
+ // ── Action bar ─────────────────────────────────────────────────────────
274
+ const actionBarNode = ActionBarSlot ? (
275
+ <ActionBarSlot
276
+ actionsStart={actionsStart}
277
+ actionsEnd={actionsEnd}
278
+ size={size}
279
+ layout={layout}
280
+ inlineStart={inlineStart}
281
+ inlineEnd={inlineEnd}
282
+ />
283
+ ) : (
284
+ <ComposerActionBar
285
+ actionsStart={actionsStart}
286
+ actionsEnd={actionsEnd}
287
+ size={size}
288
+ layout={layout}
289
+ inlineStart={inlineStart}
290
+ inlineEnd={inlineEnd}
291
+ className={slotProps?.actionBar?.className}
292
+ />
293
+ );
294
+
295
+ // Tier B fully-custom Send / Attach replacements override the
296
+ // descriptor-driven action bar. They render alongside it for hosts
297
+ // that need a branded button — the built-in send still lives in
298
+ // `actionsEnd` unless the host also drops it from `composerSlots`.
299
+ const customSend = SendSlot ? (
300
+ <SendSlot
301
+ streaming={isStreaming}
302
+ disabled={!composer.canSubmit}
303
+ size={size}
304
+ onSend={() => void composer.submit()}
305
+ onCancel={onCancel}
306
+ {...slotProps?.send}
307
+ />
308
+ ) : null;
309
+ const customAttach =
310
+ AttachSlot && showAttachmentButton ? (
311
+ <AttachSlot
312
+ disabled={isDisabled}
313
+ size={size}
314
+ onClick={() => onPickFiles?.()}
315
+ {...slotProps?.attach}
316
+ />
317
+ ) : null;
318
+
319
+ // ── Footer ─────────────────────────────────────────────────────────────
320
+ const footerNode =
321
+ footer === false ? null : (
322
+ <ComposerFooter
323
+ {...footer}
324
+ value={composer.value}
325
+ size={footer?.size ?? size}
326
+ />
327
+ );
328
+
329
+ const trayNode =
330
+ blockStart ??
331
+ (composer.attachments.length > 0 ? (
332
+ <Attachments
333
+ attachments={composer.attachments}
334
+ onRemove={(a) => composer.removeAttachment(a.id)}
335
+ />
336
+ ) : null);
337
+
338
+ // ── Inline (compact single-row) layout ─────────────────────────────────
339
+ // Start cluster sits left of the textarea, end cluster right of it —
340
+ // `[📎][actionsStart] [textarea] [actionsEnd][🎙][▶]` (§3.6).
341
+ if (layout === 'inline') {
342
+ return (
343
+ <ComposerSizeProvider value={size}>
344
+ <div
345
+ ref={ref}
346
+ className={cn(
347
+ 'border-t border-border bg-background/95',
348
+ sz.containerPadding,
349
+ ap.containerPadding,
350
+ className,
351
+ )}
352
+ >
353
+ {trayNode ? <div className="mb-1.5">{trayNode}</div> : null}
354
+ <div
355
+ className={cn(
356
+ 'flex items-end gap-1.5 [&>:not(textarea)]:shrink-0',
357
+ sz.inlineSlot,
358
+ )}
359
+ >
360
+ {customAttach}
361
+ {ActionBarSlot ? (
362
+ actionBarNode
363
+ ) : (
364
+ <ComposerActionBar
365
+ actionsStart={actionsStart}
366
+ actionsEnd={[]}
367
+ size={size}
368
+ layout={layout}
369
+ inlineStart={inlineStart}
370
+ cluster="start"
371
+ />
372
+ )}
373
+ {textareaNode}
374
+ {ActionBarSlot ? null : (
375
+ <ComposerActionBar
376
+ actionsStart={[]}
377
+ actionsEnd={actionsEnd}
378
+ size={size}
379
+ layout={layout}
380
+ inlineEnd={inlineEnd}
381
+ cluster="end"
382
+ />
383
+ )}
384
+ {customSend}
385
+ </div>
386
+ {footerNode}
387
+ </div>
388
+ </ComposerSizeProvider>
389
+ );
390
+ }
391
+
392
+ // ── Stacked (default) layout — two-tier surface ────────────────────────
393
+ // The textarea always occupies the full top row; the action bar (attach /
394
+ // tools on the left, mic / send on the right) sits in a fixed row below.
395
+ // The draft never shares a baseline with the buttons — the text stays put
396
+ // whether it is one line or many.
397
+ const surfaceWrapper = (
398
+ <div
399
+ ref={surfaceRef}
400
+ onMouseDown={handleSurfaceMouseDown}
401
+ className={cn(
402
+ // Raised input panel — reads against the chat background via the
403
+ // neutral `muted` surface + a hairline ring (Gemini/ChatGPT
404
+ // style), not a heavy drawn border. Focus firms up the ring
405
+ // without a hard accent frame.
406
+ 'flex flex-col rounded-2xl text-foreground',
407
+ 'bg-muted ring-1 ring-border',
408
+ 'focus-within:ring-ring/50',
409
+ 'transition-colors',
410
+ // The whole surface reads as a text field — the padding around the
411
+ // editor shows a text caret and is click-to-focus.
412
+ 'cursor-text',
413
+ sz.surface,
414
+ ap.surface,
415
+ )}
416
+ >
417
+ {/* Tier B: a fully-custom ActionBar renders below the textarea. */}
418
+ {ActionBarSlot ? (
419
+ <>
420
+ {textareaNode}
421
+ <div className="mt-1 flex items-center gap-1.5">
422
+ {customAttach}
423
+ {actionBarNode}
424
+ {customSend}
425
+ </div>
426
+ </>
427
+ ) : (
428
+ <>
429
+ {/* primary — textarea, full width on top */}
430
+ <div className="flex">{textareaNode}</div>
431
+
432
+ {/* action bar — leading cluster left, trailing cluster right */}
433
+ <div className={cn('mt-1 flex items-center justify-between', GAP[size])}>
434
+ <div className={cn('flex items-center', GAP[size])}>
435
+ {customAttach}
436
+ {inlineStart}
437
+ {actionsStart.map((action) => (
438
+ <ComposerButton key={action.id} action={action} size={size} />
439
+ ))}
440
+ </div>
441
+ <div className={cn('flex items-center', GAP[size])}>
442
+ {inlineEnd}
443
+ {actionsEnd.map((action) => (
444
+ <ComposerButton key={action.id} action={action} size={size} />
445
+ ))}
446
+ {customSend}
447
+ </div>
448
+ </div>
449
+ </>
450
+ )}
451
+ </div>
452
+ );
453
+
454
+ return (
455
+ <ComposerSizeProvider value={size}>
456
+ <div
457
+ ref={ref}
458
+ className={cn(
459
+ 'border-t border-border bg-background/95',
460
+ sz.containerPadding,
461
+ ap.containerPadding,
462
+ className,
463
+ )}
464
+ >
465
+ {trayNode ? <div className="mb-2">{trayNode}</div> : null}
466
+ {surfaceWrapper}
467
+ {footerNode}
468
+ </div>
469
+ </ComposerSizeProvider>
470
+ );
471
+ });
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@djangocfg/ui-core/lib';
4
+
5
+ import { ComposerButton } from './ComposerButton';
6
+ import type { ActionBarProps } from './types';
7
+
8
+ const GAP: Record<ActionBarProps['size'], string> = {
9
+ sm: 'gap-0.5',
10
+ md: 'gap-1',
11
+ lg: 'gap-1.5',
12
+ };
13
+
14
+ /**
15
+ * Renders the `blockEnd` action bar — `actionsStart` left, `actionsEnd`
16
+ * right, `justify-between`. Wrapped in `role="toolbar"` only when it
17
+ * actually carries interactive controls.
18
+ *
19
+ * `cluster` renders just one side — used by the `inline` layout, which
20
+ * places the start cluster before the textarea and the end cluster after.
21
+ */
22
+ export function ComposerActionBar({
23
+ actionsStart,
24
+ actionsEnd,
25
+ size,
26
+ inlineStart,
27
+ inlineEnd,
28
+ className,
29
+ cluster,
30
+ }: ActionBarProps & { className?: string; cluster?: 'start' | 'end' }) {
31
+ const hasInteractive = actionsStart.length > 0 || actionsEnd.length > 0;
32
+ const gap = GAP[size];
33
+
34
+ const startCluster = (
35
+ <div className={cn('flex items-center', gap)}>
36
+ {inlineStart}
37
+ {actionsStart.map((action) => (
38
+ <ComposerButton key={action.id} action={action} size={size} />
39
+ ))}
40
+ </div>
41
+ );
42
+ const endCluster = (
43
+ <div className={cn('flex items-center', gap)}>
44
+ {inlineEnd}
45
+ {actionsEnd.map((action) => (
46
+ <ComposerButton key={action.id} action={action} size={size} />
47
+ ))}
48
+ </div>
49
+ );
50
+
51
+ // Single-cluster mode — no toolbar wrapper, the caller owns layout.
52
+ if (cluster === 'start') return startCluster;
53
+ if (cluster === 'end') return endCluster;
54
+
55
+ return (
56
+ <div
57
+ role={hasInteractive ? 'toolbar' : undefined}
58
+ aria-label={hasInteractive ? 'Composer actions' : undefined}
59
+ className={cn('flex w-full items-center justify-between', className)}
60
+ >
61
+ {startCluster}
62
+ {endCluster}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { X } from 'lucide-react';
5
+
6
+ import { cn } from '@djangocfg/ui-core/lib';
7
+
8
+ /** One action button in the banner. */
9
+ export interface ComposerBannerAction {
10
+ /** Stable key. */
11
+ id: string;
12
+ label: ReactNode;
13
+ onClick: () => void;
14
+ /** `primary` — filled accent; `outline` — bordered neutral. Default `outline`. */
15
+ variant?: 'primary' | 'outline';
16
+ }
17
+
18
+ export interface ComposerBannerProps {
19
+ /** Leading icon (Lucide element). */
20
+ icon?: ReactNode;
21
+ /** Bold headline. */
22
+ title: ReactNode;
23
+ /** Secondary line under the title. */
24
+ description?: ReactNode;
25
+ /** Up to two trailing action buttons. */
26
+ actions?: ComposerBannerAction[];
27
+ /** Accent tint: `info` neutral, `warning` destructive, `upgrade` primary. */
28
+ variant?: 'info' | 'warning' | 'upgrade';
29
+ /** Show a dismiss `×` in the top-right corner. */
30
+ onDismiss?: () => void;
31
+ className?: string;
32
+ }
33
+
34
+ /** Icon-tint per variant — keeps the surface neutral, accents the icon only. */
35
+ const ICON_TINT: Record<NonNullable<ComposerBannerProps['variant']>, string> = {
36
+ info: 'text-muted-foreground',
37
+ warning: 'text-destructive',
38
+ upgrade: 'text-primary',
39
+ };
40
+
41
+ const ACTION_VARIANT: Record<
42
+ NonNullable<ComposerBannerAction['variant']>,
43
+ string
44
+ > = {
45
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
46
+ outline:
47
+ 'border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
48
+ };
49
+
50
+ /**
51
+ * A standalone notice bubble that sits above the composer — its own
52
+ * rounded surface, separated by a gap (drop it into
53
+ * `composerSlots.blockStart`). Mirrors ChatGPT's "free limit reached"
54
+ * banner: leading icon, title + description, trailing action buttons.
55
+ */
56
+ export function ComposerBanner({
57
+ icon,
58
+ title,
59
+ description,
60
+ actions,
61
+ variant = 'info',
62
+ onDismiss,
63
+ className,
64
+ }: ComposerBannerProps) {
65
+ return (
66
+ <div
67
+ role="status"
68
+ className={cn(
69
+ 'flex items-start gap-3 rounded-2xl border border-border bg-muted/60 px-3.5 py-3',
70
+ 'text-sm text-foreground',
71
+ className,
72
+ )}
73
+ >
74
+ {icon ? (
75
+ <span
76
+ className={cn('mt-0.5 flex shrink-0 [&_svg]:size-5', ICON_TINT[variant])}
77
+ aria-hidden
78
+ >
79
+ {icon}
80
+ </span>
81
+ ) : null}
82
+
83
+ <div className="min-w-0 flex-1">
84
+ <p className="font-semibold leading-snug">{title}</p>
85
+ {description ? (
86
+ <p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
87
+ ) : null}
88
+ </div>
89
+
90
+ {actions && actions.length > 0 ? (
91
+ <div className="flex shrink-0 items-center gap-1.5">
92
+ {actions.map((action) => (
93
+ <button
94
+ key={action.id}
95
+ type="button"
96
+ onClick={action.onClick}
97
+ className={cn(
98
+ 'inline-flex h-8 cursor-pointer items-center justify-center rounded-lg px-3',
99
+ 'text-xs font-medium transition-colors',
100
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
101
+ ACTION_VARIANT[action.variant ?? 'outline'],
102
+ )}
103
+ >
104
+ {action.label}
105
+ </button>
106
+ ))}
107
+ </div>
108
+ ) : null}
109
+
110
+ {onDismiss ? (
111
+ <button
112
+ type="button"
113
+ onClick={onDismiss}
114
+ aria-label="Dismiss"
115
+ title="Dismiss"
116
+ className={cn(
117
+ 'inline-flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-lg',
118
+ 'text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground',
119
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
120
+ '[&_svg]:size-4',
121
+ )}
122
+ >
123
+ <X aria-hidden />
124
+ </button>
125
+ ) : null}
126
+ </div>
127
+ );
128
+ }