@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.
- package/README.md +9 -10
- package/dist/file-icon/index.cjs +449 -61
- package/dist/file-icon/index.cjs.map +1 -1
- package/dist/file-icon/index.d.cts +56 -18
- package/dist/file-icon/index.d.ts +56 -18
- package/dist/file-icon/index.mjs +448 -62
- package/dist/file-icon/index.mjs.map +1 -1
- package/dist/tree/index.cjs +49 -22
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +9 -3
- package/dist/tree/index.d.ts +9 -3
- package/dist/tree/index.mjs +49 -22
- package/dist/tree/index.mjs.map +1 -1
- package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
- package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
- package/package.json +8 -13
- package/src/components/FloatingToolbar/index.tsx +37 -3
- package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
- package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
- package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
- package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
- package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
- package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
- package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
- package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
- package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
- package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
- package/src/lib/page-snapshot/capture/budget.ts +95 -0
- package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
- package/src/lib/page-snapshot/capture/classify.ts +111 -0
- package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
- package/src/lib/page-snapshot/capture/fold.ts +96 -0
- package/src/lib/page-snapshot/capture/scope.ts +169 -0
- package/src/lib/page-snapshot/capture/walk.ts +250 -0
- package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
- package/src/lib/page-snapshot/cst/directives.ts +47 -0
- package/src/lib/page-snapshot/cst/payload.ts +50 -0
- package/src/lib/page-snapshot/cst/serialize.ts +84 -0
- package/src/lib/page-snapshot/cst/types.ts +115 -0
- package/src/lib/page-snapshot/engine.ts +176 -0
- package/src/lib/page-snapshot/index.ts +93 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
- package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
- package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
- package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
- package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
- package/src/lib/page-snapshot/react/provider.tsx +162 -0
- package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
- package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
- package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
- package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
- package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
- package/src/lib/page-snapshot/redaction/audit.ts +58 -0
- package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
- package/src/lib/page-snapshot/redaction/index.ts +75 -0
- package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
- package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
- package/src/lib/page-snapshot/refs/registry.ts +46 -0
- package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
- package/src/lib/page-snapshot/staleness/hash.ts +20 -0
- package/src/lib/page-snapshot/tokens.ts +15 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
- package/src/tools/Chat/README.md +277 -39
- package/src/tools/Chat/composer/Composer.tsx +471 -0
- package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
- package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
- package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
- package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
- package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
- package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
- package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
- package/src/tools/Chat/composer/index.ts +45 -0
- package/src/tools/Chat/composer/size-context.tsx +26 -0
- package/src/tools/Chat/composer/types.ts +143 -0
- package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
- package/src/tools/Chat/context/ChatProvider.tsx +54 -3
- package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
- package/src/tools/Chat/core/index.ts +23 -1
- package/src/tools/Chat/core/markdown.ts +1 -1
- package/src/tools/Chat/core/metadata.ts +47 -0
- package/src/tools/Chat/core/payload-dispatch.ts +1 -1
- package/src/tools/Chat/core/transport/http.ts +71 -32
- package/src/tools/Chat/core/transport/sse.ts +18 -10
- package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
- package/src/tools/Chat/highlight/README.md +103 -0
- package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
- package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
- package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
- package/src/tools/Chat/highlight/index.ts +21 -0
- package/src/tools/Chat/highlight/resolveRef.ts +42 -0
- package/src/tools/Chat/highlight/types.ts +49 -0
- package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
- package/src/tools/Chat/hooks/index.ts +0 -5
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
- package/src/tools/Chat/hooks/useChat.ts +47 -14
- package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
- package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
- package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
- package/src/tools/Chat/index.ts +25 -219
- package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
- package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
- package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
- package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
- package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
- package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
- package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
- package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
- package/src/tools/Chat/launcher/header/index.ts +26 -0
- package/src/tools/Chat/launcher/index.ts +3 -10
- package/src/tools/Chat/lazy.tsx +38 -284
- package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
- package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
- package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
- package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
- package/src/tools/Chat/messages/blocks/index.ts +12 -0
- package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
- package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
- package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
- package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
- package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
- package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
- package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
- package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
- package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
- package/src/tools/Chat/{components → messages}/index.ts +11 -5
- package/src/tools/Chat/public.ts +212 -0
- package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
- package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
- package/src/tools/Chat/shell/index.ts +15 -0
- package/src/tools/Chat/types/block.ts +120 -0
- package/src/tools/Chat/types/config.ts +0 -5
- package/src/tools/Chat/types/index.ts +17 -0
- package/src/tools/Chat/types/message.ts +3 -0
- package/src/tools/Chat/utils/index.ts +4 -0
- package/src/tools/CodeEditor/README.md +4 -6
- package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
- package/src/tools/CodeEditor/components/Editor.tsx +96 -44
- package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
- package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
- package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
- package/src/tools/CodeEditor/lazy.tsx +6 -0
- package/src/tools/CodeEditor/lib/index.ts +1 -1
- package/src/tools/CodeEditor/lib/themes.ts +3 -39
- package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
- package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
- package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
- package/src/tools/CronScheduler/index.tsx +1 -1
- package/src/tools/CronScheduler/types/index.ts +8 -3
- package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
- package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
- package/src/tools/FileIcon/FileIcon.tsx +24 -39
- package/src/tools/FileIcon/get-file-icon.ts +73 -0
- package/src/tools/FileIcon/icons/icon-data.ts +399 -0
- package/src/tools/FileIcon/index.ts +4 -0
- package/src/tools/FileIcon/loader.ts +17 -35
- package/src/tools/FileIcon/specialFolders.ts +18 -0
- package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
- package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
- package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
- package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
- package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
- package/src/tools/ImageViewer/utils/constants.ts +3 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
- package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
- package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
- package/src/tools/LottiePlayer/index.tsx +14 -4
- package/src/tools/LottiePlayer/lazy.tsx +11 -3
- package/src/tools/LottiePlayer/types.ts +31 -1
- package/src/tools/LottiePlayer/useLottie.ts +32 -9
- package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
- package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
- package/src/tools/Map/components/MapCluster.tsx +28 -21
- package/src/tools/Map/components/MapContainer.tsx +11 -4
- package/src/tools/Map/components/MapLegend.tsx +46 -15
- package/src/tools/Map/components/MapMarker.tsx +31 -2
- package/src/tools/Map/hooks/useMapEvents.ts +64 -105
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
- package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
- package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
- package/src/tools/MarkdownEditor/lazy.tsx +32 -7
- package/src/tools/MarkdownEditor/styles.css +13 -0
- package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
- package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
- package/src/tools/MarkdownMessage/components.tsx +22 -9
- package/src/tools/MarkdownMessage/types.ts +24 -1
- package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
- package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
- package/src/tools/Mermaid/index.tsx +6 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
- package/src/tools/OpenapiViewer/constants.ts +3 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
- package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
- package/src/tools/PrettyCode/lazy.tsx +1 -1
- package/src/tools/SpeechRecognition/README.md +1 -1
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
- package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
- package/src/tools/SpeechRecognition/components/index.ts +2 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
- package/src/tools/SpeechRecognition/core/language.ts +23 -6
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
- package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
- package/src/tools/Tree/README.md +4 -8
- package/src/tools/Tree/TreeRoot.tsx +22 -10
- package/src/tools/Tree/components/TreeContent.tsx +24 -4
- package/src/tools/Tree/components/TreeLabel.tsx +8 -2
- package/src/tools/Tree/components/TreeRow.tsx +16 -6
- package/src/tools/Tree/data/flatten.ts +10 -4
- package/src/tools/Tree/types.ts +4 -0
- package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
- package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
- package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
- package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
- package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
- package/src/tools/Uploader/utils/formatters.ts +8 -3
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
- package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
- package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
- package/src/tools/index.ts +2 -1
- package/src/tools/Chat/components/AudioToggle.tsx +0 -78
- package/src/tools/Chat/components/ChatRoot.tsx +0 -305
- package/src/tools/Chat/components/Composer.tsx +0 -216
- package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
- package/src/tools/Chat/types.ts +0 -9
- package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
- package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
- package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
- package/src/tools/Tour/README.md +0 -373
- package/src/tools/Tour/components/Tour.tsx +0 -12
- package/src/tools/Tour/components/TourContent.tsx +0 -171
- package/src/tools/Tour/components/TourNavigation.tsx +0 -77
- package/src/tools/Tour/components/TourProgress.tsx +0 -88
- package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
- package/src/tools/Tour/components/index.ts +0 -5
- package/src/tools/Tour/context/TourContext.ts +0 -19
- package/src/tools/Tour/context/TourProvider.tsx +0 -292
- package/src/tools/Tour/context/index.ts +0 -2
- package/src/tools/Tour/hooks/index.ts +0 -3
- package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
- package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
- package/src/tools/Tour/hooks/useTour.ts +0 -42
- package/src/tools/Tour/index.ts +0 -38
- package/src/tools/Tour/types/index.ts +0 -224
- package/src/tools/Tour/utils/dom.ts +0 -98
- package/src/tools/Tour/utils/index.ts +0 -3
- package/src/tools/Tour/utils/logger.ts +0 -3
- package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
- /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
- /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
- /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
- /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
94
|
-
map.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
76
|
+
|
|
77
|
+
const handleZoomStart = () => {
|
|
78
|
+
handlersRef.current.onZoomStart?.()
|
|
107
79
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
146
|
-
|
|
104
|
+
if (isLoaded) {
|
|
105
|
+
handlersRef.current.onLoad?.()
|
|
147
106
|
}
|
|
148
|
-
}, [isLoaded
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
329
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
19
|
-
import
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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,
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
}
|