@djangocfg/ui-tools 2.1.431 → 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.
Files changed (171) hide show
  1. package/package.json +28 -14
  2. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  3. package/src/common/CodePanelHeader/index.ts +1 -0
  4. package/src/common/blocks/BlockError.tsx +46 -0
  5. package/src/common/blocks/index.ts +34 -0
  6. package/src/common/blocks/schemas.ts +143 -0
  7. package/src/common/blocks/types.ts +76 -0
  8. package/src/common/chips/README.md +67 -0
  9. package/src/common/chips/UrlChip.tsx +105 -0
  10. package/src/common/chips/index.ts +20 -0
  11. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  12. package/src/common/link-preview/index.ts +12 -0
  13. package/src/common/link-preview/types.ts +44 -0
  14. package/src/common/tiptap/index.ts +10 -0
  15. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  16. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  17. package/src/tools/chat/README.md +88 -2
  18. package/src/tools/chat/composer/Composer.tsx +45 -12
  19. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  20. package/src/tools/chat/constants.ts +9 -2
  21. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  22. package/src/tools/chat/core/reducer.ts +32 -8
  23. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  24. package/src/tools/chat/core/transport/mock.ts +4 -1
  25. package/src/tools/chat/hooks/useChat.ts +20 -20
  26. package/src/tools/chat/lazy.tsx +10 -0
  27. package/src/tools/chat/messages/Attachments.tsx +267 -42
  28. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  29. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  30. package/src/tools/chat/messages/MessageList.tsx +107 -21
  31. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  32. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  33. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  34. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  35. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  36. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  37. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  38. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  39. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  40. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  41. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  42. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  43. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  44. package/src/tools/chat/messages/index.ts +13 -0
  45. package/src/tools/chat/public.ts +5 -1
  46. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  47. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  48. package/src/tools/chat/styles/index.ts +1 -0
  49. package/src/tools/chat/types/attachment.ts +26 -2
  50. package/src/tools/chat/types/block.ts +56 -9
  51. package/src/tools/chat/types/blockSchemas.ts +197 -0
  52. package/src/tools/chat/types/config.ts +33 -0
  53. package/src/tools/chat/types/events.ts +14 -7
  54. package/src/tools/chat/types/index.ts +4 -2
  55. package/src/tools/chat/types/message.ts +7 -0
  56. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  57. package/src/tools/dev/Map/README.md +30 -345
  58. package/src/tools/dev/Map/cards/index.ts +6 -0
  59. package/src/tools/dev/Map/cards/types.ts +67 -0
  60. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  61. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  62. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  63. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  64. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  65. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  66. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  67. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  68. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  69. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  70. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  71. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  72. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  73. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  74. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  75. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  76. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  77. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  78. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  79. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  80. package/src/tools/dev/Map/components/index.ts +35 -1
  81. package/src/tools/dev/Map/geocode/index.ts +2 -0
  82. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  83. package/src/tools/dev/Map/hooks/index.ts +37 -0
  84. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  85. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  86. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  87. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  88. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  89. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  90. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  91. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  92. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  93. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  94. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  95. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  96. package/src/tools/dev/Map/index.ts +59 -2
  97. package/src/tools/dev/Map/layers/index.ts +1 -0
  98. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  99. package/src/tools/dev/Map/lazy.tsx +120 -0
  100. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  101. package/src/tools/dev/Map/styles/index.ts +62 -0
  102. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  103. package/src/tools/dev/Map/types.ts +31 -3
  104. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  105. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  106. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  107. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  108. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  109. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  110. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  111. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  112. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  113. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  114. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  115. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  116. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  117. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  118. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  119. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  120. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  121. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  122. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  123. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  124. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  125. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  126. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  127. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  128. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  129. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  130. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  131. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  132. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  133. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  134. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  135. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  136. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  137. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  138. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  139. package/src/tools/forms/NotionEditor/README.md +109 -188
  140. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  141. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  142. package/src/tools/forms/NotionEditor/styles.css +25 -0
  143. package/src/tools/forms/NotionEditor/types.ts +9 -0
  144. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  145. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  146. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  147. package/src/tools/media/ImageViewer/README.md +9 -0
  148. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  149. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  150. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  151. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  152. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  153. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  154. package/src/tools/media/ImageViewer/types.ts +10 -0
  155. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  156. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  157. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  158. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  159. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  160. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  161. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  162. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  163. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  164. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  165. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  166. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  167. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  168. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  169. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  170. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  171. 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 { FloatingToolbar } from '../../../../common/FloatingToolbar';
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
- scrollIsolation?: boolean; // Block scroll capture until user clicks (default: true)
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, scrollIsolation, maxLines, variant = 'card' }: PrettyCodeProps) => {
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
- {/* Toolbar: hidden by default, appears on hover. Absolute overlay so it doesn't shift layout.
309
- scrollIsolation is force-disabled when content fits without scrolling
310
- otherwise the "Click to scroll" prompt shows on a block that has
311
- nothing to scroll, which reads as a bug. */}
312
- <div className="absolute inset-x-0 top-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-150 z-30">
313
- <div className="pointer-events-auto">
314
- <FloatingToolbar
315
- containerRef={containerRef}
316
- scrollIsolation={shouldScroll ? scrollIsolation : false}
317
- label={
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={`${prismClassName} rounded-lg`}
342
+ className={prismClassName}
344
343
  style={{
345
344
  ...restStyle,
346
345
  margin: 0,
347
- // Top padding gives the hover toolbar room to sit over
348
- // the first line without covering it. Before: 2.5rem —
349
- // too much empty space on short snippets. Now: 1rem,
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-zinc-900">
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-zinc-900">
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({ heading: { levels: [1, 2, 3] } }),
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
- class: 'markdown-editor-content focus:outline-none text-sm',
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="px-3 py-2">
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: