@djangocfg/ui-tools 2.1.430 → 2.1.432
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/dist/tree/index.cjs +43 -8
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +31 -1
- package/dist/tree/index.d.ts +31 -1
- package/dist/tree/index.mjs +43 -8
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +28 -14
- package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
- package/src/common/CodePanelHeader/index.ts +1 -0
- package/src/common/blocks/BlockError.tsx +46 -0
- package/src/common/blocks/index.ts +34 -0
- package/src/common/blocks/schemas.ts +143 -0
- package/src/common/blocks/types.ts +76 -0
- package/src/common/chips/README.md +67 -0
- package/src/common/chips/UrlChip.tsx +105 -0
- package/src/common/chips/index.ts +20 -0
- package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/common/link-preview/index.ts +12 -0
- package/src/common/link-preview/types.ts +44 -0
- package/src/common/tiptap/index.ts +10 -0
- package/src/common/tiptap/useNodeAttrs.ts +128 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
- package/src/tools/chat/README.md +109 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- package/src/tools/chat/composer/PastedTextDialog.tsx +88 -0
- package/src/tools/chat/composer/index.ts +9 -0
- package/src/tools/chat/composer/textToAttachment.ts +69 -0
- package/src/tools/chat/composer/types.ts +13 -0
- package/src/tools/chat/composer/useComposerAttach.tsx +47 -0
- package/src/tools/chat/constants.ts +9 -2
- package/src/tools/chat/core/extractFirstUrl.ts +45 -0
- package/src/tools/chat/core/reducer.ts +32 -8
- package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
- package/src/tools/chat/core/transport/mock.ts +4 -1
- package/src/tools/chat/hooks/useChat.ts +20 -20
- package/src/tools/chat/lazy.tsx +10 -0
- package/src/tools/chat/messages/Attachments.tsx +335 -46
- package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
- package/src/tools/chat/messages/MessageBubble.tsx +175 -31
- package/src/tools/chat/messages/MessageList.tsx +107 -21
- package/src/tools/chat/messages/ToolCalls.tsx +28 -13
- package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
- package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
- package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
- package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
- package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
- package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
- package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
- package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
- package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
- package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
- package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
- package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
- package/src/tools/chat/messages/index.ts +13 -0
- package/src/tools/chat/public.ts +5 -1
- package/src/tools/chat/shell/ChatRoot.tsx +29 -0
- package/src/tools/chat/styles/bubbleTokens.ts +18 -0
- package/src/tools/chat/styles/index.ts +1 -0
- package/src/tools/chat/types/attachment.ts +38 -3
- package/src/tools/chat/types/block.ts +56 -9
- package/src/tools/chat/types/blockSchemas.ts +197 -0
- package/src/tools/chat/types/config.ts +33 -0
- package/src/tools/chat/types/events.ts +14 -7
- package/src/tools/chat/types/index.ts +4 -2
- package/src/tools/chat/types/message.ts +7 -0
- package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
- package/src/tools/data/Tree/README.md +30 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +11 -2
- package/src/tools/data/Tree/context/menu/render.tsx +56 -27
- package/src/tools/data/Tree/data/appearance.ts +65 -3
- package/src/tools/data/Tree/data/index.ts +2 -0
- package/src/tools/data/Tree/index.tsx +2 -0
- package/src/tools/dev/Map/README.md +30 -345
- package/src/tools/dev/Map/cards/index.ts +6 -0
- package/src/tools/dev/Map/cards/types.ts +67 -0
- package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
- package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
- package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
- package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
- package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
- package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
- package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
- package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
- package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
- package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
- package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
- package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
- package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
- package/src/tools/dev/Map/components/MapControls.tsx +27 -0
- package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
- package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
- package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
- package/src/tools/dev/Map/components/index.ts +35 -1
- package/src/tools/dev/Map/geocode/index.ts +2 -0
- package/src/tools/dev/Map/geocode/photon.ts +265 -0
- package/src/tools/dev/Map/hooks/index.ts +37 -0
- package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
- package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
- package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
- package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
- package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
- package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
- package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
- package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
- package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
- package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
- package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
- package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
- package/src/tools/dev/Map/index.ts +59 -2
- package/src/tools/dev/Map/layers/index.ts +1 -0
- package/src/tools/dev/Map/layers/symbol.ts +93 -0
- package/src/tools/dev/Map/lazy.tsx +120 -0
- package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
- package/src/tools/dev/Map/styles/index.ts +62 -0
- package/src/tools/dev/Map/styles/terrain.ts +29 -0
- package/src/tools/dev/Map/types.ts +31 -3
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
- package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
- package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
- package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
- package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
- package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
- package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
- package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
- package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
- package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
- package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
- package/src/tools/forms/MarkdownEditor/README.md +84 -0
- package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
- package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
- package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
- package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
- package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
- package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
- package/src/tools/forms/MarkdownEditor/index.ts +19 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
- package/src/tools/forms/MarkdownEditor/styles.css +134 -2
- package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
- package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
- package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
- package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
- package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
- package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
- package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
- package/src/tools/forms/NotionEditor/README.md +109 -188
- package/src/tools/forms/NotionEditor/extensions.ts +15 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
- package/src/tools/forms/NotionEditor/styles.css +25 -0
- package/src/tools/forms/NotionEditor/types.ts +9 -0
- package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
- package/src/tools/input/SpeechRecognition/types.ts +10 -0
- package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
- package/src/tools/media/ImageViewer/README.md +9 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
- package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
- package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
- package/src/tools/media/ImageViewer/components/index.ts +2 -0
- package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
- package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
- package/src/tools/media/ImageViewer/types.ts +10 -0
- package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
- package/src/tools/media/ImageViewer/utils/index.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
- package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
- package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
- package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
- package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
- package/src/tools/visual/charts/SmoothLine/README.md +97 -0
- package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
- package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
- package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
- package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
- package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
- package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
- package/src/tools/visual/indicators/Fps/types.ts +1 -1
- package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
- package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
|
@@ -4,8 +4,7 @@ import { Highlight, Language, themes } from 'prism-react-renderer';
|
|
|
4
4
|
import React, { useMemo, useRef } from 'react';
|
|
5
5
|
|
|
6
6
|
import { useAppT } from '@djangocfg/i18n';
|
|
7
|
-
import {
|
|
8
|
-
import { CopyAction } from '../../../../common/FloatingToolbar/actions';
|
|
7
|
+
import { CodePanelHeader } from '../../../../common/CodePanelHeader';
|
|
9
8
|
|
|
10
9
|
// Surface palette — fixed dark regardless of host theme. Code blocks
|
|
11
10
|
// follow the IDE / GitHub / ChatGPT convention: syntax highlighting
|
|
@@ -39,13 +38,22 @@ interface PrettyCodeProps {
|
|
|
39
38
|
inline?: boolean;
|
|
40
39
|
customBg?: string; // Custom background class
|
|
41
40
|
isCompact?: boolean; // Compact mode for smaller font sizes
|
|
42
|
-
|
|
41
|
+
/** @deprecated No-op since the toolbar moved into a persistent
|
|
42
|
+
* `CodePanelHeader` (no scroll-capturing overlay). Kept for API
|
|
43
|
+
* back-compat; will be removed in a future major. */
|
|
44
|
+
scrollIsolation?: boolean;
|
|
43
45
|
/**
|
|
44
46
|
* Line count at which the viewer starts to scroll instead of growing.
|
|
45
47
|
* ``undefined`` (default) = always grows to fit content, no scroll.
|
|
46
48
|
* Set e.g. ``50`` to cap short snippets inline and scroll long ones.
|
|
47
49
|
*/
|
|
48
50
|
maxLines?: number;
|
|
51
|
+
/**
|
|
52
|
+
* When set, an expand (⤢) button appears in the header — but ONLY while
|
|
53
|
+
* the content is actually height-capped (i.e. exceeds `maxLines`). Lets a
|
|
54
|
+
* host open the full code full-screen. No-op when the code fits.
|
|
55
|
+
*/
|
|
56
|
+
onExpand?: () => void;
|
|
49
57
|
/**
|
|
50
58
|
* Visual variant. ``"card"`` (default) ships full chrome (border,
|
|
51
59
|
* background, hover toolbar, optional internal scroll). ``"plain"``
|
|
@@ -54,7 +62,7 @@ interface PrettyCodeProps {
|
|
|
54
62
|
variant?: 'card' | 'plain';
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
const PrettyCode = ({ data, language, className, mode = 'dark', inline = false, customBg, isCompact = false,
|
|
65
|
+
const PrettyCode = ({ data, language, className, mode = 'dark', inline = false, customBg, isCompact = false, maxLines, onExpand, variant = 'card' }: PrettyCodeProps) => {
|
|
58
66
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
59
67
|
const t = useAppT();
|
|
60
68
|
|
|
@@ -302,28 +310,19 @@ const PrettyCode = ({ data, language, className, mode = 'dark', inline = false,
|
|
|
302
310
|
return (
|
|
303
311
|
<div
|
|
304
312
|
ref={containerRef}
|
|
305
|
-
className={`group relative ${bgClass} rounded-lg border ${isDarkMode ? '' : 'border-border'} ${className || ''}`}
|
|
313
|
+
className={`group relative overflow-hidden ${bgClass} rounded-lg border ${isDarkMode ? '' : 'border-border'} ${className || ''}`}
|
|
306
314
|
style={customBg ? undefined : surfaceStyle}
|
|
307
315
|
>
|
|
308
|
-
{/*
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-muted/80 text-muted-foreground border border-border/50 backdrop-blur-sm">
|
|
319
|
-
{displayLanguage}
|
|
320
|
-
</span>
|
|
321
|
-
}
|
|
322
|
-
>
|
|
323
|
-
<CopyAction value={contentJson} title={labels.copyCode} />
|
|
324
|
-
</FloatingToolbar>
|
|
325
|
-
</div>
|
|
326
|
-
</div>
|
|
316
|
+
{/* Persistent header bar (GitHub / ChatGPT style): language label
|
|
317
|
+
left, copy button right, in flow — never floats over the code. */}
|
|
318
|
+
<CodePanelHeader
|
|
319
|
+
language={displayLanguage}
|
|
320
|
+
copyValue={contentJson}
|
|
321
|
+
copyLabel={labels.copyCode}
|
|
322
|
+
dark={isDarkMode}
|
|
323
|
+
// Expand only matters when the code is actually height-capped.
|
|
324
|
+
onExpand={onExpand && shouldScroll ? onExpand : undefined}
|
|
325
|
+
/>
|
|
327
326
|
|
|
328
327
|
<div
|
|
329
328
|
className={shouldScroll ? 'h-full overflow-auto' : ''}
|
|
@@ -340,15 +339,13 @@ const PrettyCode = ({ data, language, className, mode = 'dark', inline = false,
|
|
|
340
339
|
const { backgroundColor: _bg, ...restStyle } = style;
|
|
341
340
|
return (
|
|
342
341
|
<pre
|
|
343
|
-
className={
|
|
342
|
+
className={prismClassName}
|
|
344
343
|
style={{
|
|
345
344
|
...restStyle,
|
|
346
345
|
margin: 0,
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
// toolbar overlays with translucent bg on hover only.
|
|
351
|
-
padding: '1rem 1rem 0.75rem 1rem',
|
|
346
|
+
// The header now occupies the top; the code body just needs
|
|
347
|
+
// even padding (no extra top room for an overlay anymore).
|
|
348
|
+
padding: '0.875rem 1rem',
|
|
352
349
|
fontSize,
|
|
353
350
|
lineHeight: lineHeightRatio,
|
|
354
351
|
fontFamily: 'monospace',
|
|
@@ -15,7 +15,7 @@ const PrettyCodeClient = lazy(() => import('./PrettyCode.client'));
|
|
|
15
15
|
|
|
16
16
|
// Loading fallback component
|
|
17
17
|
const LoadingFallback = () => (
|
|
18
|
-
<div className="relative rounded-sm border border-border overflow-hidden bg-muted dark:bg-
|
|
18
|
+
<div className="relative rounded-sm border border-border overflow-hidden bg-muted dark:bg-code">
|
|
19
19
|
<div className="p-4">
|
|
20
20
|
<div className="flex items-center gap-2">
|
|
21
21
|
<div className="animate-pulse h-4 w-4 rounded-full bg-muted-foreground/20"></div>
|
|
@@ -41,6 +41,9 @@ export interface PrettyCodeProps {
|
|
|
41
41
|
* to inline short snippets and cap long ones.
|
|
42
42
|
*/
|
|
43
43
|
maxLines?: number;
|
|
44
|
+
/** When set, a header expand (⤢) button appears while the code is
|
|
45
|
+
* height-capped (exceeds `maxLines`). Lets a host open it full-screen. */
|
|
46
|
+
onExpand?: () => void;
|
|
44
47
|
/**
|
|
45
48
|
* Visual variant:
|
|
46
49
|
* - ``"card"`` (default) — full chrome: border, background, hover
|
|
@@ -27,6 +27,8 @@ export interface PrettyCodeProps {
|
|
|
27
27
|
isCompact?: boolean;
|
|
28
28
|
scrollIsolation?: boolean;
|
|
29
29
|
maxLines?: number;
|
|
30
|
+
/** Header expand (⤢) callback, shown while the code is height-capped. */
|
|
31
|
+
onExpand?: () => void;
|
|
30
32
|
/** ``'card'`` (default) ships the full chrome. ``'plain'`` is
|
|
31
33
|
* chrome-less — for embedding inside another scroll container. */
|
|
32
34
|
variant?: 'card' | 'plain';
|
|
@@ -40,7 +42,7 @@ export type { Language };
|
|
|
40
42
|
|
|
41
43
|
function CodeLoadingFallback() {
|
|
42
44
|
return (
|
|
43
|
-
<div className="relative rounded-sm border border-border overflow-hidden bg-muted dark:bg-
|
|
45
|
+
<div className="relative rounded-sm border border-border overflow-hidden bg-muted dark:bg-code">
|
|
44
46
|
<div className="p-4">
|
|
45
47
|
<div className="flex items-center gap-2">
|
|
46
48
|
<div className="animate-pulse h-4 w-4 rounded-full bg-muted-foreground/20" />
|
|
@@ -18,11 +18,46 @@ import { SlashCommandNode } from './slash/SlashCommandNode';
|
|
|
18
18
|
import { syncLeadingSlashNode } from './slash/syncSlashNode';
|
|
19
19
|
import type { SlashCommandInfo } from './slash/types';
|
|
20
20
|
import { SubmitOnEnter } from './submitOnEnter';
|
|
21
|
+
import { ChipNode } from './chip/ChipNode';
|
|
22
|
+
import { syncChips } from './chip/syncChips';
|
|
21
23
|
import type { MentionAttrs, MentionConfig } from './types';
|
|
22
24
|
import './styles.css';
|
|
23
25
|
|
|
24
26
|
// ── Helpers ──
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Whitelist of URL schemes the autolinker is allowed to turn into a
|
|
30
|
+
* link. We deliberately keep this to real, clickable transports.
|
|
31
|
+
*/
|
|
32
|
+
const AUTO_LINK_PROTOCOL = /^(?:https?|ftp|mailto|file):/i;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether an autolink candidate should actually become a link.
|
|
36
|
+
*
|
|
37
|
+
* StarterKit's bundled `Link` extension ships with a permissive default
|
|
38
|
+
* `shouldAutoLink` that links any token whose "hostname" contains a dot
|
|
39
|
+
* with a plausible TLD. That over-eagerly turns *bare file paths and
|
|
40
|
+
* dotted filenames* into `http://…` links — e.g. typing a local path
|
|
41
|
+
* like `…/positioning/manifest.md` linkifies `manifest.md` as the
|
|
42
|
+
* Moldova (`.md`) domain, and pasted prose around a dotted token can be
|
|
43
|
+
* swallowed into one anchor. None of that is wanted in a chat composer.
|
|
44
|
+
*
|
|
45
|
+
* We instead require an explicit, known protocol (`http`, `https`,
|
|
46
|
+
* `ftp`, `mailto`, `file`) or a `www.`-prefixed host. linkifyjs has
|
|
47
|
+
* already split the candidate on whitespace, so the match is inherently
|
|
48
|
+
* bounded to a single token — a path like `/a/b/file.md` is never a
|
|
49
|
+
* `*://` URL and never a `www.` host, so it stays plain text, and any
|
|
50
|
+
* Unicode/Cyrillic words after a real URL remain plain text too.
|
|
51
|
+
*/
|
|
52
|
+
function shouldAutoLinkUrl(url: string): boolean {
|
|
53
|
+
if (AUTO_LINK_PROTOCOL.test(url)) return true;
|
|
54
|
+
// Bare `www.host…` (no scheme) — linkify prepends the default
|
|
55
|
+
// protocol; allow it since it's an unambiguous web address, not a
|
|
56
|
+
// local path.
|
|
57
|
+
if (/^www\.[^\s/$.?#].[^\s]*$/i.test(url)) return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
interface MarkdownManager {
|
|
27
62
|
serialize: (json: Record<string, unknown>) => string;
|
|
28
63
|
}
|
|
@@ -105,6 +140,38 @@ export interface MarkdownEditorProps {
|
|
|
105
140
|
* chat package's `SlashCommand[]` — pass either directly.
|
|
106
141
|
*/
|
|
107
142
|
slashCommands?: readonly SlashCommandInfo[];
|
|
143
|
+
/**
|
|
144
|
+
* Render absolute LOCAL file paths typed/pasted into the editor as
|
|
145
|
+
* compact, atomic file chips (icon + VSCode-style middle-ellipsis label,
|
|
146
|
+
* full path on hover) — Cursor/Finder style. Backed by an inline atom
|
|
147
|
+
* NODE (`editorChip`, `kind: 'path'`), NOT a decoration: the chip owns
|
|
148
|
+
* its rendering so a long path collapses to ONE line (`…/dev/Map/index.ts`)
|
|
149
|
+
* instead of wrapping. The node serialises back to the RAW path, so caret
|
|
150
|
+
* (whole-chip delete), copy, and the submitted markdown all keep the
|
|
151
|
+
* literal `…/manifest.md`. Detection is conservative (absolute Unix /
|
|
152
|
+
* macOS / `~` / Windows-drive / UNC / `file://` only — never bare
|
|
153
|
+
* relative tokens or web URLs).
|
|
154
|
+
*
|
|
155
|
+
* Defaults to ON (the chat composer wants it). Pass `false` for editors
|
|
156
|
+
* that should leave paths as plain text. Like `mentions`/`slashCommands`
|
|
157
|
+
* this is captured by `useEditor` on first render — flipping it after
|
|
158
|
+
* mount won't add/remove the extension on the live instance.
|
|
159
|
+
*/
|
|
160
|
+
filePathChips?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Render web URLs typed/pasted into the editor as compact, atomic URL
|
|
163
|
+
* chips (favicon — degrading to a globe glyph — + domain and a
|
|
164
|
+
* middle-ellipsis of the path, e.g. `github.com/…/README.md`, full URL on
|
|
165
|
+
* hover). Clickable (opens in a new tab, `rel=noopener`). Same atom NODE
|
|
166
|
+
* as the file-path chip (`editorChip`, `kind: 'url'`); it REPLACES the
|
|
167
|
+
* plain blue underlined link look and serialises back to the RAW URL so
|
|
168
|
+
* copy / submit / markdown keep the literal address. Uses the same
|
|
169
|
+
* autolink whitelist as the editor (`http`/`https`/`ftp`/`mailto`/`file`
|
|
170
|
+
* + bare `www.`).
|
|
171
|
+
*
|
|
172
|
+
* Defaults to ON. Captured on first render like `filePathChips`.
|
|
173
|
+
*/
|
|
174
|
+
urlChips?: boolean;
|
|
108
175
|
/** Called when mentioned IDs change */
|
|
109
176
|
onMentionIdsChange?: (ids: string[]) => void;
|
|
110
177
|
/**
|
|
@@ -169,6 +236,8 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
169
236
|
unstyled = false,
|
|
170
237
|
mentions,
|
|
171
238
|
slashCommands,
|
|
239
|
+
filePathChips = true,
|
|
240
|
+
urlChips = true,
|
|
172
241
|
onMentionIdsChange,
|
|
173
242
|
onSubmit,
|
|
174
243
|
autoFocus = false,
|
|
@@ -195,6 +264,11 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
195
264
|
);
|
|
196
265
|
slashCommandsRef.current = slashCommands ?? [];
|
|
197
266
|
const slashEnabledOnMountRef = useRef<boolean>(slashCommands !== undefined);
|
|
267
|
+
// File-path / URL chips: captured once on mount (the ChipNode extension is
|
|
268
|
+
// installed on first render, like mentions/slash). Both default ON. Kept
|
|
269
|
+
// in refs so the post-`setContent` `syncChips` pass reads the right flags.
|
|
270
|
+
const filePathChipsOnMountRef = useRef<boolean>(filePathChips);
|
|
271
|
+
const urlChipsOnMountRef = useRef<boolean>(urlChips);
|
|
198
272
|
const slashWarnedRef = useRef(false);
|
|
199
273
|
if (
|
|
200
274
|
process.env.NODE_ENV !== 'production' &&
|
|
@@ -241,7 +315,25 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
241
315
|
|
|
242
316
|
const extensions = useMemo(() => {
|
|
243
317
|
const exts: AnyExtension[] = [
|
|
244
|
-
StarterKit.configure({
|
|
318
|
+
StarterKit.configure({
|
|
319
|
+
heading: { levels: [1, 2, 3] },
|
|
320
|
+
// Constrain the bundled Link extension's autolinker. The default
|
|
321
|
+
// links bare dotted tokens (e.g. a file path's `manifest.md`)
|
|
322
|
+
// and can over-capture pasted prose; we only auto-link real
|
|
323
|
+
// URLs (explicit scheme or `www.`). `defaultProtocol: 'https'`
|
|
324
|
+
// is the modern default for the rare schemeless `www.` case.
|
|
325
|
+
//
|
|
326
|
+
// When `urlChips` is ON the ChipNode OWNS URL rendering (a chip,
|
|
327
|
+
// not a blue underline), so we turn OFF the link autolinker to
|
|
328
|
+
// avoid the two fighting over the same token. Markdown `[text](url)`
|
|
329
|
+
// links typed deliberately still render via the link mark — only
|
|
330
|
+
// the implicit autolink of a bare URL is suppressed.
|
|
331
|
+
link: {
|
|
332
|
+
defaultProtocol: 'https',
|
|
333
|
+
autolink: !urlChipsOnMountRef.current,
|
|
334
|
+
shouldAutoLink: shouldAutoLinkUrl,
|
|
335
|
+
},
|
|
336
|
+
}),
|
|
245
337
|
Placeholder.configure({ placeholder }),
|
|
246
338
|
Markdown,
|
|
247
339
|
// SubmitOnEnter — when the consumer wired an onSubmit, intercept
|
|
@@ -268,6 +360,22 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
268
360
|
exts.push(SlashCommandNode);
|
|
269
361
|
}
|
|
270
362
|
|
|
363
|
+
// ChipNode — a single inline ATOM node that renders BOTH file-path
|
|
364
|
+
// chips (`kind: 'path'`) and URL chips (`kind: 'url'`) with a
|
|
365
|
+
// VSCode-style middle-ellipsis label, and serialises back to the raw
|
|
366
|
+
// path / URL. Registered whenever either chip feature is on (the node's
|
|
367
|
+
// input/paste rules are gated per-kind via `enablePaths`/`enableUrls`).
|
|
368
|
+
// Replaces the old FilePathChipExtension decoration — a node owns its
|
|
369
|
+
// rendering, so a long path collapses to ONE line instead of wrapping.
|
|
370
|
+
if (filePathChipsOnMountRef.current || urlChipsOnMountRef.current) {
|
|
371
|
+
exts.push(
|
|
372
|
+
ChipNode.configure({
|
|
373
|
+
enablePaths: filePathChipsOnMountRef.current,
|
|
374
|
+
enableUrls: urlChipsOnMountRef.current,
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
271
379
|
if (mentions) {
|
|
272
380
|
// ── Why .extend() with renderMarkdown ──
|
|
273
381
|
//
|
|
@@ -356,9 +464,41 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
356
464
|
},
|
|
357
465
|
editorProps: {
|
|
358
466
|
attributes: {
|
|
359
|
-
|
|
467
|
+
// 15px matches the `.ProseMirror` typographic baseline in
|
|
468
|
+
// styles.css — a comfortable chat-composer body size. Do NOT
|
|
469
|
+
// shrink to `text-sm` here: that 14px override made the composer
|
|
470
|
+
// input read too small against the rest of the chat surface.
|
|
471
|
+
class: 'markdown-editor-content focus:outline-none text-[15px]',
|
|
360
472
|
style: `min-height: ${minHeight}px`,
|
|
361
473
|
},
|
|
474
|
+
handleDOMEvents: {
|
|
475
|
+
// FILE drops must NOT be consumed by ProseMirror. PM's built-in
|
|
476
|
+
// `editHandlers.drop` calls `event.preventDefault()` on the editor
|
|
477
|
+
// surface (prosemirror-view: handleDrop), which swallows a file
|
|
478
|
+
// drop before it can bubble to the host wrapper's `onDrop` (the
|
|
479
|
+
// chat composer's attach pipeline).
|
|
480
|
+
//
|
|
481
|
+
// PM's dispatch (prosemirror-view `dispatchEvent`) runs the
|
|
482
|
+
// built-in handler ONLY when the custom handler returns a falsy
|
|
483
|
+
// value AND did not `preventDefault`. So to let a file drop reach
|
|
484
|
+
// the composer we must:
|
|
485
|
+
// • return `true` → tells PM "handled", it SKIPS its built-in
|
|
486
|
+
// drop, so it never preventDefaults; the
|
|
487
|
+
// native event keeps bubbling to React onDrop.
|
|
488
|
+
// • NOT preventDefault here (that would also block the bubble
|
|
489
|
+
// target from acting, and PM treats defaultPrevented as
|
|
490
|
+
// "handled" too — same skip, but we keep it explicit).
|
|
491
|
+
// For text / node drops (no `dataTransfer.files`) return `false`
|
|
492
|
+
// so PM's normal in-editor drop handling runs unchanged.
|
|
493
|
+
drop: (_view, event) => {
|
|
494
|
+
const dt = (event as DragEvent).dataTransfer;
|
|
495
|
+
if (dt && dt.files && dt.files.length > 0) {
|
|
496
|
+
// Hands-off: skip PM's drop, let it bubble to composer.onDrop.
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
},
|
|
501
|
+
},
|
|
362
502
|
},
|
|
363
503
|
});
|
|
364
504
|
|
|
@@ -383,6 +523,17 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
383
523
|
if (slashEnabledOnMountRef.current) {
|
|
384
524
|
syncLeadingSlashNode(editor, slashCommandsRef.current);
|
|
385
525
|
}
|
|
526
|
+
// Convert any bare path / URL text in the freshly-parsed markdown into
|
|
527
|
+
// chip atoms. Input/paste rules handle live typing; this covers
|
|
528
|
+
// content that arrived via the controlled `value` (parsed straight to
|
|
529
|
+
// plain text). The chips serialise back to the raw string, so the next
|
|
530
|
+
// `getMarkdown(editor)` still equals `value` — no feedback loop.
|
|
531
|
+
if (filePathChipsOnMountRef.current || urlChipsOnMountRef.current) {
|
|
532
|
+
syncChips(editor, {
|
|
533
|
+
paths: filePathChipsOnMountRef.current,
|
|
534
|
+
urls: urlChipsOnMountRef.current,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
386
537
|
isExternalUpdate.current = false;
|
|
387
538
|
}
|
|
388
539
|
}, [value, editor]);
|
|
@@ -444,10 +595,24 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
444
595
|
: 'rounded-md border border-input bg-background';
|
|
445
596
|
const wrapperClass = `markdown-editor ${chromeClass} ${disabled ? 'opacity-60' : ''} ${className}`.trim();
|
|
446
597
|
|
|
598
|
+
// Inner content padding.
|
|
599
|
+
//
|
|
600
|
+
// Standalone editor: comfy `px-3 py-2` frame.
|
|
601
|
+
//
|
|
602
|
+
// Unstyled (embedded in the chat composer surface): the host owns the
|
|
603
|
+
// OUTER frame, but the editor text still needs a small, EVEN inset so
|
|
604
|
+
// it doesn't sit flush in the corner. We give it a balanced horizontal +
|
|
605
|
+
// top inset and a DELIBERATELY tighter bottom (`pb-px`): a symmetric
|
|
606
|
+
// `py-2` would stack a second 8px gap under the single caret line and
|
|
607
|
+
// read as a phantom empty line (the bug this replaces). `pt-1.5 pb-px`
|
|
608
|
+
// breathes at the top without re-introducing the bottom gap; `px-2`
|
|
609
|
+
// keeps left == right so the caret isn't jammed against the edge.
|
|
610
|
+
const contentPad = unstyled ? 'px-2 pt-1.5 pb-px' : 'px-3 py-2';
|
|
611
|
+
|
|
447
612
|
return (
|
|
448
613
|
<div className={wrapperClass}>
|
|
449
614
|
{showToolbar && editor && <MarkdownToolbar editor={editor} />}
|
|
450
|
-
<div className=
|
|
615
|
+
<div className={contentPad}>
|
|
451
616
|
<EditorContent editor={editor} />
|
|
452
617
|
</div>
|
|
453
618
|
</div>
|
|
@@ -12,6 +12,8 @@ WYSIWYG markdown editor based on Tiptap. Renders markdown visually (headings, li
|
|
|
12
12
|
- Toolbar with icon buttons
|
|
13
13
|
- `@`-mentions with auto-flipping popup (`@floating-ui/dom`)
|
|
14
14
|
- Customizable markdown serialization for mentions (six built-in presets)
|
|
15
|
+
- File-path chips and URL chips — atomic inline nodes with VSCode-style
|
|
16
|
+
middle-ellipsis truncation (see [File-path & URL chips](#file-path--url-chips))
|
|
15
17
|
- Markdown input/output (stored as plain markdown string)
|
|
16
18
|
- SSR-safe (`immediatelyRender: false`)
|
|
17
19
|
|
|
@@ -57,6 +59,8 @@ import '@djangocfg/ui-tools/dist.css';
|
|
|
57
59
|
| `disabled` | `boolean` | `false` | Read-only mode |
|
|
58
60
|
| `showToolbar` | `boolean` | `true` | Show formatting toolbar |
|
|
59
61
|
| `mentions` | `MentionConfig` | — | `@`-mention autocomplete config |
|
|
62
|
+
| `filePathChips` | `boolean` | `true` | Render absolute local file paths as atomic chips — see [File-path & URL chips](#file-path--url-chips) |
|
|
63
|
+
| `urlChips` | `boolean` | `true` | Render web URLs as atomic, clickable chips — see [File-path & URL chips](#file-path--url-chips) |
|
|
60
64
|
| `onMentionIdsChange` | `(ids: string[]) => void` | — | Called when mentioned IDs change |
|
|
61
65
|
| `onSubmit` | `() => boolean \| void` | — | Enter handler — when set, Enter submits and Shift+Enter inserts a newline (ChatGPT / Telegram chat behaviour). Return `false` to fall back to default HardBreak. See [Submit on Enter](#submit-on-enter) below. |
|
|
62
66
|
|
|
@@ -143,6 +147,86 @@ Either `id` or `label` may be empty strings if upstream config didn't populate t
|
|
|
143
147
|
|
|
144
148
|
> Mentions are write-only: the markdown isn't parsed back into mention nodes on `setContent`. After submit/reset, the editor receives a plain string — fine for chat composers.
|
|
145
149
|
|
|
150
|
+
## File-path & URL chips
|
|
151
|
+
|
|
152
|
+
The editor turns absolute **local file paths** and **web URLs** into compact,
|
|
153
|
+
atomic inline chips — like VSCode / Cursor. Both default ON; gate them with
|
|
154
|
+
the `filePathChips` / `urlChips` props.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<MarkdownEditor
|
|
158
|
+
value={text}
|
|
159
|
+
onChange={setText}
|
|
160
|
+
filePathChips // default true — paths → chips
|
|
161
|
+
urlChips // default true — URLs → chips
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### What you get
|
|
166
|
+
|
|
167
|
+
| | File-path chip (`kind: 'path'`) | URL chip (`kind: 'url'`) |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| Icon | VSCode file icon by extension (folder glyph for dirs) | favicon (Google S2) → globe glyph on error |
|
|
170
|
+
| Label | middle-ellipsis, basename kept — `…/dev/Map/index.ts` | domain + middle-ellipsis path — `github.com/…/README.md` |
|
|
171
|
+
| Tooltip | full path (`title`) | full URL (`title`) |
|
|
172
|
+
| Clickable | no | yes — opens in a new tab (`rel=noopener noreferrer`) |
|
|
173
|
+
| Detection | absolute Unix / macOS / `~` / Windows-drive / UNC / `file://` | `http`/`https`/`ftp`/`mailto`/`file` + bare `www.` |
|
|
174
|
+
|
|
175
|
+
### Why a node, not a decoration (the long-path fix)
|
|
176
|
+
|
|
177
|
+
The chip is an **atomic inline NODE** (`editorChip`), the same shape as the
|
|
178
|
+
`@`-mention and `/`-command chips — NOT a decoration. A decoration is an
|
|
179
|
+
overlay: it can style the raw text but can't replace it, so a long path
|
|
180
|
+
rendered in full and **wrapped to several lines**. A node owns its
|
|
181
|
+
rendering, so it shows the truncated label on **one line** while the full
|
|
182
|
+
string lives in the node's attribute.
|
|
183
|
+
|
|
184
|
+
A single node renders both kinds (a `kind` attribute switches path vs URL) —
|
|
185
|
+
one component, one set of styles, DRY.
|
|
186
|
+
|
|
187
|
+
### Text / markdown fidelity (the whole point)
|
|
188
|
+
|
|
189
|
+
Each chip **serialises back to the RAW string**. `renderText` and
|
|
190
|
+
`renderMarkdown` both emit the literal path / URL, so:
|
|
191
|
+
|
|
192
|
+
- copying a chip yields the raw path / URL,
|
|
193
|
+
- the submitted message and `getMarkdown()` contain the real string,
|
|
194
|
+
- never any chip markup.
|
|
195
|
+
|
|
196
|
+
A bare URL serialises as the bare URL; a file path as the literal path.
|
|
197
|
+
|
|
198
|
+
### Auto-conversion
|
|
199
|
+
|
|
200
|
+
- **Typing**: type a path / URL followed by a **space** and it becomes a
|
|
201
|
+
chip (an input rule on the word boundary — punctuation inside the token
|
|
202
|
+
like `.ts` is preserved because only whitespace triggers conversion).
|
|
203
|
+
- **Pasting**: a paste rule converts every detected path / URL in the
|
|
204
|
+
pasted text, keeping the surrounding prose.
|
|
205
|
+
- **Initial `value`**: paths / URLs in the controlled markdown are
|
|
206
|
+
converted after `setContent` (the same way leading `/`-commands are).
|
|
207
|
+
|
|
208
|
+
### Caret / editing
|
|
209
|
+
|
|
210
|
+
Atomic nodes behave like a single character: arrow keys step over the whole
|
|
211
|
+
chip, and Backspace removes / reverts the whole chip at once (the raw text
|
|
212
|
+
is never partially lost) — VSCode-like.
|
|
213
|
+
|
|
214
|
+
### Standalone `<FilePathChip>`
|
|
215
|
+
|
|
216
|
+
The presentational React chip still exists for rendering a known path
|
|
217
|
+
**outside** the editor (a chat message, a file list). It's exported from
|
|
218
|
+
`@djangocfg/ui-tools/markdown-editor`:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { FilePathChip } from '@djangocfg/ui-tools/markdown-editor';
|
|
222
|
+
|
|
223
|
+
<FilePathChip path="/Users/me/dev/Map/index.ts" />
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The pure detector / display helpers are exported too:
|
|
227
|
+
`findFilePaths`, `splitPath`, `truncatePathLabel` (paths) and
|
|
228
|
+
`findUrls`, `splitUrl`, `truncateUrlLabel`, `faviconUrl` (URLs).
|
|
229
|
+
|
|
146
230
|
## Submit on Enter
|
|
147
231
|
|
|
148
232
|
For chat composers you usually want **Enter = send**, **Shift+Enter = newline** — ChatGPT / Telegram / Slack behaviour. Pass an `onSubmit` and that's what you get:
|