@djangocfg/ui-tools 2.1.407 → 2.1.409

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 (297) hide show
  1. package/README.md +9 -10
  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 +8 -13
  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/parts/Meta/TimeDisplay.tsx +2 -5
  67. package/src/tools/Chat/README.md +277 -39
  68. package/src/tools/Chat/composer/Composer.tsx +471 -0
  69. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  70. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  71. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  72. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  73. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  74. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  75. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  76. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  77. package/src/tools/Chat/composer/index.ts +45 -0
  78. package/src/tools/Chat/composer/size-context.tsx +26 -0
  79. package/src/tools/Chat/composer/types.ts +143 -0
  80. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  81. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  82. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  83. package/src/tools/Chat/core/index.ts +23 -1
  84. package/src/tools/Chat/core/markdown.ts +1 -1
  85. package/src/tools/Chat/core/metadata.ts +47 -0
  86. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  87. package/src/tools/Chat/core/transport/http.ts +71 -32
  88. package/src/tools/Chat/core/transport/sse.ts +18 -10
  89. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  90. package/src/tools/Chat/highlight/README.md +103 -0
  91. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  92. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  93. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  94. package/src/tools/Chat/highlight/index.ts +21 -0
  95. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  96. package/src/tools/Chat/highlight/types.ts +49 -0
  97. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  98. package/src/tools/Chat/hooks/index.ts +0 -5
  99. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  100. package/src/tools/Chat/hooks/useChat.ts +47 -14
  101. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  102. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  103. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  104. package/src/tools/Chat/index.ts +25 -219
  105. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  106. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  107. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  108. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  109. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  110. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  111. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  112. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  113. package/src/tools/Chat/launcher/header/index.ts +26 -0
  114. package/src/tools/Chat/launcher/index.ts +3 -10
  115. package/src/tools/Chat/lazy.tsx +38 -284
  116. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  117. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  118. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  119. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  120. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  121. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  122. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  123. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  124. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  125. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  126. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  127. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  128. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  130. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  131. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  132. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  133. package/src/tools/Chat/public.ts +212 -0
  134. package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
  135. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  136. package/src/tools/Chat/shell/index.ts +15 -0
  137. package/src/tools/Chat/types/block.ts +120 -0
  138. package/src/tools/Chat/types/config.ts +0 -5
  139. package/src/tools/Chat/types/index.ts +17 -0
  140. package/src/tools/Chat/types/message.ts +3 -0
  141. package/src/tools/Chat/utils/index.ts +4 -0
  142. package/src/tools/CodeEditor/README.md +4 -6
  143. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  144. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  145. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  146. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  147. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  148. package/src/tools/CodeEditor/lazy.tsx +6 -0
  149. package/src/tools/CodeEditor/lib/index.ts +1 -1
  150. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  151. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  152. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  153. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  154. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  155. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  156. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  157. package/src/tools/CronScheduler/index.tsx +1 -1
  158. package/src/tools/CronScheduler/types/index.ts +8 -3
  159. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  160. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  161. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  162. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  163. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  164. package/src/tools/FileIcon/index.ts +4 -0
  165. package/src/tools/FileIcon/loader.ts +17 -35
  166. package/src/tools/FileIcon/specialFolders.ts +18 -0
  167. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  168. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  169. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  170. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  171. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  172. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  173. package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
  174. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  175. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  176. package/src/tools/ImageViewer/utils/index.ts +1 -0
  177. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  178. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  179. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  180. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  181. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  182. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  183. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  184. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  185. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  186. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  187. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  188. package/src/tools/JsonForm/widgets/index.ts +1 -0
  189. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  190. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  191. package/src/tools/LottiePlayer/index.tsx +14 -4
  192. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  193. package/src/tools/LottiePlayer/types.ts +31 -1
  194. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  195. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  196. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  197. package/src/tools/Map/components/MapCluster.tsx +28 -21
  198. package/src/tools/Map/components/MapContainer.tsx +11 -4
  199. package/src/tools/Map/components/MapLegend.tsx +46 -15
  200. package/src/tools/Map/components/MapMarker.tsx +31 -2
  201. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  202. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  203. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  204. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  205. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  206. package/src/tools/MarkdownEditor/styles.css +13 -0
  207. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  208. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  209. package/src/tools/MarkdownMessage/components.tsx +22 -9
  210. package/src/tools/MarkdownMessage/types.ts +24 -1
  211. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  212. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  213. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  214. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  215. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  216. package/src/tools/Mermaid/index.tsx +6 -0
  217. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  218. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  221. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  222. package/src/tools/OpenapiViewer/constants.ts +3 -0
  223. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  224. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  225. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  226. package/src/tools/PrettyCode/lazy.tsx +1 -1
  227. package/src/tools/SpeechRecognition/README.md +1 -1
  228. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  229. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  230. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  231. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  232. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  233. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  234. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  235. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  236. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  237. package/src/tools/Tree/README.md +4 -8
  238. package/src/tools/Tree/TreeRoot.tsx +22 -10
  239. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  240. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  241. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  242. package/src/tools/Tree/data/flatten.ts +10 -4
  243. package/src/tools/Tree/types.ts +4 -0
  244. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  245. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  246. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  247. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  248. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  249. package/src/tools/Uploader/utils/formatters.ts +8 -3
  250. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
  251. package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
  252. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
  253. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
  254. package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
  255. package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
  256. package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
  257. package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
  258. package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
  259. package/src/tools/index.ts +2 -1
  260. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  261. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  262. package/src/tools/Chat/components/Composer.tsx +0 -216
  263. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  264. package/src/tools/Chat/types.ts +0 -9
  265. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  266. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  267. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  268. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  269. package/src/tools/Tour/README.md +0 -373
  270. package/src/tools/Tour/components/Tour.tsx +0 -12
  271. package/src/tools/Tour/components/TourContent.tsx +0 -171
  272. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  273. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  274. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  275. package/src/tools/Tour/components/index.ts +0 -5
  276. package/src/tools/Tour/context/TourContext.ts +0 -19
  277. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  278. package/src/tools/Tour/context/index.ts +0 -2
  279. package/src/tools/Tour/hooks/index.ts +0 -3
  280. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  281. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  282. package/src/tools/Tour/hooks/useTour.ts +0 -42
  283. package/src/tools/Tour/index.ts +0 -38
  284. package/src/tools/Tour/types/index.ts +0 -224
  285. package/src/tools/Tour/utils/dom.ts +0 -98
  286. package/src/tools/Tour/utils/index.ts +0 -3
  287. package/src/tools/Tour/utils/logger.ts +0 -3
  288. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  289. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  290. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  291. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  292. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  293. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  294. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  295. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  296. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  297. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect } from 'react';
3
+ import { useEffect, useRef } from 'react';
4
4
 
5
5
  import { useMapContext } from '../context';
6
6
 
@@ -9,141 +9,100 @@ import type { MapEventHandlers, MapMouseEvent, MapViewport } from '../types'
9
9
  /**
10
10
  * Hook for subscribing to map events
11
11
  * Automatically cleans up event listeners on unmount
12
+ *
13
+ * Handlers are stored in a ref so callers don't need to memoize the
14
+ * `handlers` object — the underlying map listeners are attached once
15
+ * (per `isLoaded`) and always call the latest handler implementations.
12
16
  */
13
17
  export function useMapEvents(handlers: MapEventHandlers) {
14
18
  const { mapRef, setViewport, setHoveredFeature, isLoaded } = useMapContext()
15
19
 
16
- const handleClick = useCallback(
17
- (event: maplibregl.MapMouseEvent & { features?: GeoJSON.Feature[] }) => {
18
- if (!handlers.onClick) return
20
+ // Keep latest handlers without forcing re-subscription.
21
+ const handlersRef = useRef(handlers)
22
+ useEffect(() => {
23
+ handlersRef.current = handlers
24
+ }, [handlers])
19
25
 
26
+ useEffect(() => {
27
+ const map = mapRef.current?.getMap()
28
+ if (!map || !isLoaded) return
29
+
30
+ const handleClick = (
31
+ event: maplibregl.MapMouseEvent & { features?: GeoJSON.Feature[] }
32
+ ) => {
33
+ const onClick = handlersRef.current.onClick
34
+ if (!onClick) return
20
35
  const mapEvent: MapMouseEvent = {
21
36
  lngLat: event.lngLat,
22
37
  point: event.point,
23
38
  features: event.features,
24
39
  originalEvent: event.originalEvent,
25
40
  }
26
- handlers.onClick(mapEvent)
27
- },
28
- [handlers]
29
- )
30
-
31
- const handleMouseMove = useCallback(
32
- (event: maplibregl.MapMouseEvent & { features?: GeoJSON.Feature[] }) => {
33
- if (!handlers.onHover) return
41
+ onClick(mapEvent)
42
+ }
34
43
 
44
+ const handleMouseMove = (
45
+ event: maplibregl.MapMouseEvent & { features?: GeoJSON.Feature[] }
46
+ ) => {
47
+ const onHover = handlersRef.current.onHover
48
+ if (!onHover) return
35
49
  const feature = event.features?.[0] ?? null
36
50
  setHoveredFeature(feature)
37
-
38
51
  const mapEvent: MapMouseEvent = {
39
52
  lngLat: event.lngLat,
40
53
  point: event.point,
41
54
  features: event.features,
42
55
  originalEvent: event.originalEvent,
43
56
  }
44
- handlers.onHover(mapEvent)
45
- },
46
- [handlers, setHoveredFeature]
47
- )
48
-
49
- const handleMoveStart = useCallback(() => {
50
- handlers.onMoveStart?.()
51
- }, [handlers])
52
-
53
- const handleMoveEnd = useCallback(() => {
54
- const map = mapRef.current
55
- if (!map) return
56
-
57
- const center = map.getCenter()
58
- const zoom = map.getZoom()
59
- const bearing = map.getBearing()
60
- const pitch = map.getPitch()
61
-
62
- const newViewport: MapViewport = {
63
- longitude: center.lng,
64
- latitude: center.lat,
65
- zoom,
66
- bearing,
67
- pitch,
57
+ onHover(mapEvent)
68
58
  }
69
59
 
70
- setViewport(newViewport)
71
- handlers.onMoveEnd?.(newViewport)
72
- }, [mapRef, setViewport, handlers])
73
-
74
- const handleZoomStart = useCallback(() => {
75
- handlers.onZoomStart?.()
76
- }, [handlers])
77
-
78
- const handleZoomEnd = useCallback(() => {
79
- const zoom = mapRef.current?.getZoom()
80
- if (zoom !== undefined) {
81
- handlers.onZoomEnd?.(zoom)
60
+ const handleMoveStart = () => {
61
+ handlersRef.current.onMoveStart?.()
82
62
  }
83
- }, [mapRef, handlers])
84
-
85
- const handleLoad = useCallback(() => {
86
- handlers.onLoad?.()
87
- }, [handlers])
88
-
89
- useEffect(() => {
90
- const map = mapRef.current?.getMap()
91
- if (!map || !isLoaded) return
92
63
 
93
- if (handlers.onClick) {
94
- map.on('click', handleClick)
95
- }
96
- if (handlers.onHover) {
97
- map.on('mousemove', handleMouseMove)
98
- }
99
- if (handlers.onMoveStart) {
100
- map.on('movestart', handleMoveStart)
101
- }
102
- if (handlers.onMoveEnd) {
103
- map.on('moveend', handleMoveEnd)
64
+ const handleMoveEnd = () => {
65
+ const center = map.getCenter()
66
+ const newViewport: MapViewport = {
67
+ longitude: center.lng,
68
+ latitude: center.lat,
69
+ zoom: map.getZoom(),
70
+ bearing: map.getBearing(),
71
+ pitch: map.getPitch(),
72
+ }
73
+ setViewport(newViewport)
74
+ handlersRef.current.onMoveEnd?.(newViewport)
104
75
  }
105
- if (handlers.onZoomStart) {
106
- map.on('zoomstart', handleZoomStart)
76
+
77
+ const handleZoomStart = () => {
78
+ handlersRef.current.onZoomStart?.()
107
79
  }
108
- if (handlers.onZoomEnd) {
109
- map.on('zoomend', handleZoomEnd)
80
+
81
+ const handleZoomEnd = () => {
82
+ handlersRef.current.onZoomEnd?.(map.getZoom())
110
83
  }
111
84
 
85
+ map.on('click', handleClick)
86
+ map.on('mousemove', handleMouseMove)
87
+ map.on('movestart', handleMoveStart)
88
+ map.on('moveend', handleMoveEnd)
89
+ map.on('zoomstart', handleZoomStart)
90
+ map.on('zoomend', handleZoomEnd)
91
+
112
92
  return () => {
113
- if (handlers.onClick) {
114
- map.off('click', handleClick)
115
- }
116
- if (handlers.onHover) {
117
- map.off('mousemove', handleMouseMove)
118
- }
119
- if (handlers.onMoveStart) {
120
- map.off('movestart', handleMoveStart)
121
- }
122
- if (handlers.onMoveEnd) {
123
- map.off('moveend', handleMoveEnd)
124
- }
125
- if (handlers.onZoomStart) {
126
- map.off('zoomstart', handleZoomStart)
127
- }
128
- if (handlers.onZoomEnd) {
129
- map.off('zoomend', handleZoomEnd)
130
- }
93
+ map.off('click', handleClick)
94
+ map.off('mousemove', handleMouseMove)
95
+ map.off('movestart', handleMoveStart)
96
+ map.off('moveend', handleMoveEnd)
97
+ map.off('zoomstart', handleZoomStart)
98
+ map.off('zoomend', handleZoomEnd)
131
99
  }
132
- }, [
133
- mapRef,
134
- isLoaded,
135
- handlers,
136
- handleClick,
137
- handleMouseMove,
138
- handleMoveStart,
139
- handleMoveEnd,
140
- handleZoomStart,
141
- handleZoomEnd,
142
- ])
100
+ }, [mapRef, isLoaded, setViewport, setHoveredFeature])
143
101
 
102
+ // Fire onLoad once the map reports loaded.
144
103
  useEffect(() => {
145
- if (isLoaded && handlers.onLoad) {
146
- handleLoad()
104
+ if (isLoaded) {
105
+ handlersRef.current.onLoad?.()
147
106
  }
148
- }, [isLoaded, handlers.onLoad, handleLoad])
107
+ }, [isLoaded])
149
108
  }
@@ -23,7 +23,17 @@ interface MarkdownManager {
23
23
  serialize: (json: Record<string, unknown>) => string;
24
24
  }
25
25
 
26
+ /**
27
+ * Serialize the editor document to a markdown string.
28
+ *
29
+ * `@tiptap/markdown` v3 augments the `Editor` with a `getMarkdown()`
30
+ * method; we use it when present and fall back to the storage manager
31
+ * (and ultimately `getText()`) so the editor still produces *something*
32
+ * if the extension shape ever drifts.
33
+ */
26
34
  function getMarkdown(editor: Editor): string {
35
+ const withMd = editor as Editor & { getMarkdown?: () => string };
36
+ if (typeof withMd.getMarkdown === 'function') return withMd.getMarkdown();
27
37
  const storage = editor.storage.markdown as { manager?: MarkdownManager } | undefined;
28
38
  if (!storage?.manager) return editor.getText();
29
39
  return storage.manager.serialize(editor.getJSON());
@@ -49,6 +59,13 @@ export interface MarkdownEditorProps {
49
59
  className?: string;
50
60
  disabled?: boolean;
51
61
  showToolbar?: boolean;
62
+ /**
63
+ * Drop the editor's own border / background / focus ring. Use when
64
+ * the editor is embedded inside a host surface that already draws
65
+ * the frame (e.g. the chat composer's textarea slot) — avoids a
66
+ * double border.
67
+ */
68
+ unstyled?: boolean;
52
69
  /**
53
70
  * `@`-mention autocomplete config.
54
71
  *
@@ -117,6 +134,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
117
134
  className = '',
118
135
  disabled = false,
119
136
  showToolbar = true,
137
+ unstyled = false,
120
138
  mentions,
121
139
  onMentionIdsChange,
122
140
  onSubmit,
@@ -237,6 +255,9 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
237
255
  editable: !disabled,
238
256
  extensions,
239
257
  content: value,
258
+ // `value` is a markdown string — without this Tiptap treats the
259
+ // content as JSON and renders `# Hello` literally instead of an <h1>.
260
+ contentType: 'markdown',
240
261
  onUpdate: ({ editor }) => {
241
262
  if (isExternalUpdate.current) return;
242
263
  onChange(getMarkdown(editor));
@@ -253,16 +274,27 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
253
274
  },
254
275
  });
255
276
 
277
+ // Sync external `value` → editor. `setContent` is given `contentType:
278
+ // 'markdown'` so the string is parsed (not treated as JSON), and
279
+ // `emitUpdate: false` so it does NOT re-fire `onUpdate` (which would
280
+ // loop back into `onChange`). `isExternalUpdate` stays as a guard.
256
281
  useEffect(() => {
257
282
  if (!editor) return;
258
283
  const current = getMarkdown(editor);
259
284
  if (current !== value) {
260
285
  isExternalUpdate.current = true;
261
- editor.commands.setContent(value);
286
+ editor.commands.setContent(value, { contentType: 'markdown', emitUpdate: false });
262
287
  isExternalUpdate.current = false;
263
288
  }
264
289
  }, [value, editor]);
265
290
 
291
+ // Keep editability in sync with the `disabled` prop. `useEditor` only
292
+ // reads `editable` on init, so toggling `disabled` later is otherwise
293
+ // a no-op.
294
+ useEffect(() => {
295
+ editor?.setEditable(!disabled);
296
+ }, [editor, disabled]);
297
+
266
298
  // Imperative API for hosts that drive the editor without owning a
267
299
  // TipTap ref directly — chat composer registration, voice slot,
268
300
  // focus-on-stream-end.
@@ -280,7 +312,12 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
280
312
  [editor],
281
313
  );
282
314
 
283
- const wrapperClass = `markdown-editor rounded-md border border-input bg-background ${disabled ? 'opacity-60' : ''} ${className}`.trim();
315
+ // `unstyled` drops the editor's own frame (border / bg / focus ring)
316
+ // so it can sit inside a host surface that already draws one.
317
+ const chromeClass = unstyled
318
+ ? 'markdown-editor--unstyled'
319
+ : 'rounded-md border border-input bg-background';
320
+ const wrapperClass = `markdown-editor ${chromeClass} ${disabled ? 'opacity-60' : ''} ${className}`.trim();
284
321
 
285
322
  return (
286
323
  <div className={wrapperClass}>
@@ -319,14 +356,32 @@ function MarkdownToolbar({ editor }: { editor: Editor }) {
319
356
  ], [editor]);
320
357
 
321
358
  return (
322
- <div className="flex items-center gap-0.5 px-2 py-1.5 border-b border-border">
359
+ <div
360
+ className="flex items-center gap-0.5 px-2 py-1.5 border-b border-border"
361
+ role="toolbar"
362
+ aria-label="Text formatting"
363
+ >
323
364
  {items.map((item, i) => {
324
- if (!item) return <div key={i} className="w-px h-4 bg-border mx-1" />;
365
+ if (!item) {
366
+ return <div key={i} className="w-px h-4 bg-border mx-1" aria-hidden="true" />;
367
+ }
325
368
  const Icon = item.icon;
326
369
  const btnClass = `markdown-toolbar-btn ${item.active ? 'active' : ''}`;
327
370
  return (
328
- <button key={i} type="button" onClick={item.action} title={item.title} className={btnClass}>
329
- <Icon style={{ width: 14, height: 14 }} />
371
+ <button
372
+ key={i}
373
+ type="button"
374
+ // Prevent the button from stealing focus from the editor on
375
+ // mousedown — keeps the selection intact so the formatting
376
+ // command applies to the user's current selection.
377
+ onMouseDown={(e) => e.preventDefault()}
378
+ onClick={item.action}
379
+ title={item.title}
380
+ aria-label={item.title}
381
+ aria-pressed={item.active}
382
+ className={btnClass}
383
+ >
384
+ <Icon style={{ width: 14, height: 14 }} aria-hidden="true" />
330
385
  </button>
331
386
  );
332
387
  })}
@@ -1,6 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { forwardRef, useImperativeHandle, useState, useCallback, useEffect, type KeyboardEvent } from 'react';
3
+ import {
4
+ forwardRef,
5
+ useImperativeHandle,
6
+ useState,
7
+ useCallback,
8
+ useEffect,
9
+ useRef,
10
+ type KeyboardEvent,
11
+ } from 'react';
4
12
  import type { MentionItem } from './types';
5
13
 
6
14
  export interface MentionListRef {
@@ -15,9 +23,18 @@ interface MentionListProps {
15
23
  export const MentionList = forwardRef<MentionListRef, MentionListProps>(
16
24
  ({ items, command }, ref) => {
17
25
  const [selectedIndex, setSelectedIndex] = useState(0);
26
+ // One ref per rendered option so keyboard nav can scroll the active
27
+ // item into view inside the (scrollable, max-height) dropdown.
28
+ const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
18
29
 
19
30
  useEffect(() => setSelectedIndex(0), [items]);
20
31
 
32
+ // Keep the highlighted item visible when ArrowUp/Down moves the
33
+ // selection past the visible window of the dropdown.
34
+ useEffect(() => {
35
+ itemRefs.current[selectedIndex]?.scrollIntoView({ block: 'nearest' });
36
+ }, [selectedIndex]);
37
+
21
38
  const select = useCallback(
22
39
  (index: number) => {
23
40
  const item = items[index];
@@ -28,6 +45,7 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
28
45
 
29
46
  useImperativeHandle(ref, () => ({
30
47
  onKeyDown: (event: KeyboardEvent) => {
48
+ if (items.length === 0) return false;
31
49
  if (event.key === 'ArrowUp') {
32
50
  setSelectedIndex((i) => (i + items.length - 1) % items.length);
33
51
  return true;
@@ -36,7 +54,9 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
36
54
  setSelectedIndex((i) => (i + 1) % items.length);
37
55
  return true;
38
56
  }
39
- if (event.key === 'Enter') {
57
+ // Tab commits the highlighted item too — matches GitHub / Slack /
58
+ // ChatGPT mention pickers. Returning true keeps focus in the editor.
59
+ if (event.key === 'Enter' || event.key === 'Tab') {
40
60
  select(selectedIndex);
41
61
  return true;
42
62
  }
@@ -47,12 +67,25 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
47
67
  if (items.length === 0) return null;
48
68
 
49
69
  return (
50
- <div className="markdown-mention-list">
70
+ <div className="markdown-mention-list" role="listbox" aria-label="Mention suggestions">
51
71
  {items.map((item, i) => {
52
72
  const isSelected = i === selectedIndex;
53
73
  const cls = `markdown-mention-item ${isSelected ? 'selected' : ''}`;
54
74
  return (
55
- <button key={item.id} type="button" className={cls} onClick={() => select(i)}>
75
+ <button
76
+ key={item.id}
77
+ type="button"
78
+ role="option"
79
+ aria-selected={isSelected}
80
+ ref={(el) => {
81
+ itemRefs.current[i] = el;
82
+ }}
83
+ className={cls}
84
+ // Pointer-driven selection mirrors the keyboard highlight so
85
+ // hovering then clicking never picks a different item.
86
+ onMouseEnter={() => setSelectedIndex(i)}
87
+ onClick={() => select(i)}
88
+ >
56
89
  {item.thumbnail && (
57
90
  <img src={item.thumbnail} alt="" className="markdown-mention-avatar" />
58
91
  )}
@@ -36,6 +36,14 @@ export function createMentionSuggestion(
36
36
  },
37
37
  });
38
38
 
39
+ // The popup wrapper is mounted in onStart and removed in onExit.
40
+ // While mounted it must stay hidden whenever MentionList renders
41
+ // nothing (empty query / no matches), otherwise an empty positioned
42
+ // div lingers next to the caret as a ghost block.
43
+ const setPopupVisible = (visible: boolean) => {
44
+ if (popup) popup.style.display = visible ? '' : 'none';
45
+ };
46
+
39
47
  const updatePosition = () => {
40
48
  if (!popup) return;
41
49
  const virtualEl = buildVirtualElement();
@@ -81,6 +89,7 @@ export function createMentionSuggestion(
81
89
  popup.style.cssText = 'position: absolute; top: 0; left: 0; z-index: 99999;';
82
90
  popup.appendChild(component.element);
83
91
  document.body.appendChild(popup);
92
+ setPopupVisible(props.items.length > 0);
84
93
 
85
94
  getReferenceRect = () => props.clientRect?.() ?? null;
86
95
 
@@ -98,6 +107,8 @@ export function createMentionSuggestion(
98
107
  },
99
108
  });
100
109
 
110
+ setPopupVisible(props.items.length > 0);
111
+
101
112
  // Refresh reference accessor so autoUpdate sees the new caret rect.
102
113
  getReferenceRect = () => props.clientRect?.() ?? null;
103
114
  updatePosition();
@@ -9,23 +9,48 @@
9
9
  * dropdown). Together that's ~200 KB minified — wrap it in React.lazy so
10
10
  * pages that don't render an editor don't pay.
11
11
  *
12
+ * Why a hand-rolled lazy wrapper instead of `createLazyComponent`:
13
+ * `createLazyComponent` returns a plain function component that does not
14
+ * forward `ref`, so the imperative `MarkdownEditorHandle` (focus /
15
+ * moveCursorToEnd — used by chat composer registration + voice slot)
16
+ * would be silently dropped. The wrapper below is a `forwardRef` so the
17
+ * ref reaches the underlying TipTap editor through `React.lazy`.
18
+ *
12
19
  * Light surface kept here:
13
20
  * - All public types (erased at compile time).
14
21
  * - `mentionPresets` — pure data describing how to render mentions to
15
22
  * markdown. No TipTap imports at module scope.
16
23
  */
17
24
 
18
- import { createLazyComponent, LoadingFallback } from '../../components';
19
- import type { MarkdownEditorProps } from './MarkdownEditor';
25
+ import { Suspense, lazy, forwardRef } from 'react';
26
+ import { LoadingFallback } from '../../components';
27
+ import type { MarkdownEditorProps, MarkdownEditorHandle } from './MarkdownEditor';
28
+
29
+ const MarkdownEditorImpl = lazy(() =>
30
+ import('./MarkdownEditor').then((m) => ({ default: m.MarkdownEditor })),
31
+ );
20
32
 
21
- export const LazyMarkdownEditor = createLazyComponent<MarkdownEditorProps>(
22
- () => import('./MarkdownEditor').then((m) => ({ default: m.MarkdownEditor })),
23
- {
24
- displayName: 'LazyMarkdownEditor',
25
- fallback: <LoadingFallback minHeight={140} text="Loading editor…" />,
33
+ /**
34
+ * Lazy-loaded `MarkdownEditor`. Forwards `ref` (`MarkdownEditorHandle`)
35
+ * to the underlying TipTap editor once the chunk resolves.
36
+ */
37
+ export const LazyMarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
38
+ function LazyMarkdownEditor(props, ref) {
39
+ return (
40
+ <Suspense fallback={<LoadingFallback minHeight={140} text="Loading editor…" />}>
41
+ <MarkdownEditorImpl {...props} ref={ref} />
42
+ </Suspense>
43
+ );
26
44
  },
27
45
  );
28
46
 
47
+ // `MarkdownEditor` is the public eager-looking name — it resolves to the
48
+ // same lazy component so importing it from this subpath does NOT pull
49
+ // ~200 KB of TipTap into the caller's initial bundle. Keeping the name
50
+ // here means `@djangocfg/ui-tools/markdown-editor` exposes
51
+ // `MarkdownEditor`, `LazyMarkdownEditor`, and `mentionPresets`.
52
+ export { LazyMarkdownEditor as MarkdownEditor };
53
+
29
54
  // Light surface — pure helpers + types.
30
55
  export { mentionPresets } from './mentionPresets';
31
56
 
@@ -107,6 +107,15 @@
107
107
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-ring, var(--ring)) 30%, transparent);
108
108
  }
109
109
 
110
+ /* Embedded mode — the host surface (e.g. chat composer) draws the
111
+ frame, so the editor must not paint its own border or focus ring. */
112
+ .markdown-editor--unstyled,
113
+ .markdown-editor--unstyled:focus-within {
114
+ border: none;
115
+ background: transparent;
116
+ box-shadow: none;
117
+ }
118
+
110
119
  /* Mention inline chip */
111
120
  .markdown-mention {
112
121
  background: var(--color-primary, var(--primary));
@@ -190,6 +199,10 @@
190
199
  border-radius: 4px;
191
200
  border: none;
192
201
  background: transparent;
202
+ /* Explicit semantic colour — without it the icon inherits the host's
203
+ default text colour (UA black on a dark editor surface) and the
204
+ toolbar is invisible. */
205
+ color: var(--foreground);
193
206
  cursor: pointer;
194
207
  opacity: 0.5;
195
208
  transition: opacity 0.15s, background 0.15s;
@@ -1,6 +1,5 @@
1
1
  import React, { memo } from 'react';
2
2
  import { CopyButton } from '@djangocfg/ui-core/components';
3
- import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
4
3
  import PrettyCode from '../PrettyCode';
5
4
 
6
5
  interface CodeBlockProps {
@@ -8,6 +7,13 @@ interface CodeBlockProps {
8
7
  language: string;
9
8
  isUser: boolean;
10
9
  isCompact?: boolean;
10
+ /**
11
+ * Code surface palette. Default `'dark'` — syntax highlighting
12
+ * ships its own contrast model and reads washed out on a light
13
+ * surface (GitHub / ChatGPT / VSCode convention). Pass `'light'`
14
+ * to opt a host into a light code surface anyway.
15
+ */
16
+ codeTheme?: 'dark' | 'light';
11
17
  }
12
18
 
13
19
  /**
@@ -23,22 +29,25 @@ interface CodeBlockProps {
23
29
  * `isCompact` change. `code` is the main trigger — long code blocks
24
30
  * should not re-render when parent chat scrolls.
25
31
  */
26
- function CodeBlockRaw({ code, language }: CodeBlockProps) {
27
- const theme = useResolvedTheme();
28
-
32
+ function CodeBlockRaw({ code, language, codeTheme = 'dark' }: CodeBlockProps) {
29
33
  // Chat fences are always rendered in PrettyCode's compact mode:
30
34
  // 12px font, tighter padding, line-height 1.4. A fenced block in
31
35
  // a chat bubble shouldn't outweigh two paragraphs of body text —
32
36
  // the standalone PrettyCode story keeps the larger default for
33
37
  // docs/diff viewers.
38
+ //
39
+ // The surface defaults to dark: syntax highlighting ships its own
40
+ // contrast model, so a light code surface in a light-theme bubble
41
+ // renders washed out (GitHub / ChatGPT / VSCode convention). Hosts
42
+ // can opt into a light surface via `codeTheme="light"`.
34
43
  return (
35
44
  <div className="my-2">
36
45
  <PrettyCode
37
46
  data={code}
38
47
  language={language}
39
48
  className="text-xs"
40
- customBg="bg-code"
41
- mode={theme}
49
+ mode={codeTheme}
50
+ customBg={codeTheme === 'light' ? 'bg-code' : undefined}
42
51
  isCompact
43
52
  // Disable click-to-scroll-isolation in chat markdown: code
44
53
  // fences here are part of an assistant reply, not a docs
@@ -60,15 +69,15 @@ export const CodeBlock = memo(CodeBlockRaw);
60
69
  * Memoised: re-renders only when `code`, `language`, `isUser` or
61
70
  * `isCompact` change.
62
71
  */
63
- function CodeBlockFallbackRaw({ code, isUser }: CodeBlockProps) {
64
- // See CodeBlock above for the spirit of this layout — palette
65
- // pre-computed before render.
66
- const copyHoverClass = isUser
67
- ? 'hover:bg-white/20 text-white'
68
- : 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground';
72
+ function CodeBlockFallbackRaw({ code, codeTheme = 'dark' }: CodeBlockProps) {
73
+ const isDark = codeTheme === 'dark';
74
+ // On the dark surface the copy button is always light; on a light
75
+ // surface it follows the semantic muted token.
69
76
  const copyButtonClass =
70
- `absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity ` +
71
- `h-8 w-8 ${copyHoverClass}`;
77
+ `absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8 ` +
78
+ (isDark
79
+ ? 'text-white/70 hover:text-white hover:bg-white/15'
80
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted');
72
81
 
73
82
  return (
74
83
  <div className="relative group my-3">
@@ -78,9 +87,23 @@ function CodeBlockFallbackRaw({ code, isUser }: CodeBlockProps) {
78
87
  className={copyButtonClass}
79
88
  title="Copy code"
80
89
  />
81
- <pre className="p-3 rounded text-xs font-mono overflow-x-auto bg-code text-code-foreground border border-code-border">
82
- <code>{code}</code>
83
- </pre>
90
+ {isDark ? (
91
+ // Fixed dark surface to match PrettyCode's default.
92
+ <pre
93
+ className="p-3 rounded text-xs font-mono overflow-x-auto border"
94
+ style={{
95
+ backgroundColor: '#0d1117',
96
+ color: '#e5e7eb',
97
+ borderColor: '#1f2937',
98
+ }}
99
+ >
100
+ <code>{code}</code>
101
+ </pre>
102
+ ) : (
103
+ <pre className="p-3 rounded text-xs font-mono overflow-x-auto bg-code text-code-foreground border border-code-border">
104
+ <code>{code}</code>
105
+ </pre>
106
+ )}
84
107
  </div>
85
108
  );
86
109
  }