@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.
Files changed (188) hide show
  1. package/dist/tree/index.cjs +43 -8
  2. package/dist/tree/index.cjs.map +1 -1
  3. package/dist/tree/index.d.cts +31 -1
  4. package/dist/tree/index.d.ts +31 -1
  5. package/dist/tree/index.mjs +43 -8
  6. package/dist/tree/index.mjs.map +1 -1
  7. package/package.json +28 -14
  8. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  9. package/src/common/CodePanelHeader/index.ts +1 -0
  10. package/src/common/blocks/BlockError.tsx +46 -0
  11. package/src/common/blocks/index.ts +34 -0
  12. package/src/common/blocks/schemas.ts +143 -0
  13. package/src/common/blocks/types.ts +76 -0
  14. package/src/common/chips/README.md +67 -0
  15. package/src/common/chips/UrlChip.tsx +105 -0
  16. package/src/common/chips/index.ts +20 -0
  17. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  18. package/src/common/link-preview/index.ts +12 -0
  19. package/src/common/link-preview/types.ts +44 -0
  20. package/src/common/tiptap/index.ts +10 -0
  21. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  22. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  23. package/src/tools/chat/README.md +109 -2
  24. package/src/tools/chat/composer/Composer.tsx +45 -12
  25. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  26. package/src/tools/chat/composer/PastedTextDialog.tsx +88 -0
  27. package/src/tools/chat/composer/index.ts +9 -0
  28. package/src/tools/chat/composer/textToAttachment.ts +69 -0
  29. package/src/tools/chat/composer/types.ts +13 -0
  30. package/src/tools/chat/composer/useComposerAttach.tsx +47 -0
  31. package/src/tools/chat/constants.ts +9 -2
  32. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  33. package/src/tools/chat/core/reducer.ts +32 -8
  34. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  35. package/src/tools/chat/core/transport/mock.ts +4 -1
  36. package/src/tools/chat/hooks/useChat.ts +20 -20
  37. package/src/tools/chat/lazy.tsx +10 -0
  38. package/src/tools/chat/messages/Attachments.tsx +335 -46
  39. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  40. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  41. package/src/tools/chat/messages/MessageList.tsx +107 -21
  42. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  43. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  44. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  45. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  46. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  47. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  48. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  49. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  50. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  51. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  52. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  53. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  54. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  55. package/src/tools/chat/messages/index.ts +13 -0
  56. package/src/tools/chat/public.ts +5 -1
  57. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  58. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  59. package/src/tools/chat/styles/index.ts +1 -0
  60. package/src/tools/chat/types/attachment.ts +38 -3
  61. package/src/tools/chat/types/block.ts +56 -9
  62. package/src/tools/chat/types/blockSchemas.ts +197 -0
  63. package/src/tools/chat/types/config.ts +33 -0
  64. package/src/tools/chat/types/events.ts +14 -7
  65. package/src/tools/chat/types/index.ts +4 -2
  66. package/src/tools/chat/types/message.ts +7 -0
  67. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  68. package/src/tools/data/Tree/README.md +30 -0
  69. package/src/tools/data/Tree/components/TreeRow.tsx +11 -2
  70. package/src/tools/data/Tree/context/menu/render.tsx +56 -27
  71. package/src/tools/data/Tree/data/appearance.ts +65 -3
  72. package/src/tools/data/Tree/data/index.ts +2 -0
  73. package/src/tools/data/Tree/index.tsx +2 -0
  74. package/src/tools/dev/Map/README.md +30 -345
  75. package/src/tools/dev/Map/cards/index.ts +6 -0
  76. package/src/tools/dev/Map/cards/types.ts +67 -0
  77. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  78. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  79. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  80. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  81. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  82. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  83. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  84. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  85. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  86. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  87. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  88. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  89. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  90. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  91. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  92. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  93. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  94. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  95. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  96. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  97. package/src/tools/dev/Map/components/index.ts +35 -1
  98. package/src/tools/dev/Map/geocode/index.ts +2 -0
  99. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  100. package/src/tools/dev/Map/hooks/index.ts +37 -0
  101. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  102. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  103. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  104. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  105. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  106. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  107. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  108. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  109. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  110. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  111. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  112. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  113. package/src/tools/dev/Map/index.ts +59 -2
  114. package/src/tools/dev/Map/layers/index.ts +1 -0
  115. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  116. package/src/tools/dev/Map/lazy.tsx +120 -0
  117. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  118. package/src/tools/dev/Map/styles/index.ts +62 -0
  119. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  120. package/src/tools/dev/Map/types.ts +31 -3
  121. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  122. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  123. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  124. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  125. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  126. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  127. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  128. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  129. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  130. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  131. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  132. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  133. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  134. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  135. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  136. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  137. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  138. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  139. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  140. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  141. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  142. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  143. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  144. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  145. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  146. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  147. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  148. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  149. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  150. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  151. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  152. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  153. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  154. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  155. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  156. package/src/tools/forms/NotionEditor/README.md +109 -188
  157. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  158. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  159. package/src/tools/forms/NotionEditor/styles.css +25 -0
  160. package/src/tools/forms/NotionEditor/types.ts +9 -0
  161. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  162. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  163. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  164. package/src/tools/media/ImageViewer/README.md +9 -0
  165. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  166. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  167. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  168. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  169. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  170. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  171. package/src/tools/media/ImageViewer/types.ts +10 -0
  172. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  173. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  174. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  175. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  176. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  177. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  178. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  179. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  180. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  181. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  182. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  183. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  184. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  185. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  186. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  187. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  188. package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
@@ -1,35 +1,197 @@
1
1
  'use client';
2
2
 
3
- /** Map block — thin chat wrapper around `LazyMapContainer` + `MapMarker`. */
3
+ /**
4
+ * Map block — thin chat wrapper around `LazyMapContainer`. Renders point
5
+ * markers plus optional routes (polylines) and polygons (filled zones) by
6
+ * building GeoJSON from the block's lat/lng payloads and feeding the map's
7
+ * existing `MapSource` + layer factories — no bespoke layer logic here.
8
+ */
4
9
 
5
- import { LazyMapContainer, MapMarker } from '../../../../dev/Map/lazy';
10
+ import { LazyMapContainer, MapMarker, MapPopup, MarkerCard, MapSource, MapLayer, useMarkerCard } from '../../../../dev/Map/lazy';
6
11
  import type { MarkerData } from '../../../../dev/Map/lazy';
12
+ import type { MapContainerProps } from '../../../../dev/Map/components';
13
+ import {
14
+ createRouteLayers,
15
+ createPolygonLayer,
16
+ createPolygonOutlineLayer,
17
+ } from '../../../../dev/Map/layers';
18
+ import {
19
+ calculateBounds,
20
+ createLineString,
21
+ createPolygon,
22
+ createFeatureCollection,
23
+ } from '../../../../dev/Map/utils';
7
24
  import type { MapBlock } from '../../../types/block';
8
25
  import type { BlockRendererProps } from './types';
9
26
 
27
+ /** `[lng, lat]` tuple — GeoJSON / maplibre coordinate order. */
28
+ type LngLat = [number, number];
29
+
30
+ /**
31
+ * Derive an `initialViewport` that frames every coordinate. The map exposes
32
+ * an imperative `fitBounds` only through `useMapControl` (inside its own
33
+ * provider), which this frameless wrapper sits outside of — so instead we
34
+ * seed the initial center/zoom from the bounding box. Mirrors maplibre's own
35
+ * fit math: zoom for whichever axis (lng/lat) needs the most room.
36
+ */
37
+ function viewportForBounds(points: LngLat[]): { longitude: number; latitude: number; zoom: number } {
38
+ const [[minLng, minLat], [maxLng, maxLat]] = calculateBounds(points) as [LngLat, LngLat];
39
+ const longitude = (minLng + maxLng) / 2;
40
+ const latitude = (minLat + maxLat) / 2;
41
+
42
+ const lngSpan = Math.max(maxLng - minLng, 1e-6);
43
+ const latSpan = Math.max(maxLat - minLat, 1e-6);
44
+ // World is 360° at zoom 0; each zoom level halves the visible span. Add a
45
+ // little padding (0.6) so geometry never hugs the edges, and clamp.
46
+ const zoomForSpan = (span: number, fullSpan: number) =>
47
+ Math.log2((fullSpan / span) * 0.6);
48
+ const zoom = Math.min(16, Math.max(1, Math.min(zoomForSpan(lngSpan, 360), zoomForSpan(latSpan, 180))));
49
+
50
+ return { longitude, latitude, zoom };
51
+ }
52
+
53
+ type BlockMarker = NonNullable<MapBlock['markers']>[number];
54
+
55
+ /**
56
+ * Marker layer for the chat map block. Renders each marker (preserving the
57
+ * `icon` thumbnail / `color` pin) and, when a marker carries a serializable
58
+ * `card`, opens a `MarkerCard` popup ABOVE the pin on click. Selection is
59
+ * single-card via `useMarkerCard`. Lives inside `LazyMapContainer` so it has
60
+ * the MapProvider context.
61
+ */
62
+ function MapBlockMarkers({ markers }: { markers: BlockMarker[] }) {
63
+ const { openId, toggle, close } = useMarkerCard();
64
+ const open = openId ? markers.find((m) => m.id === openId) : undefined;
65
+
66
+ return (
67
+ <>
68
+ {markers.map((m) => {
69
+ const marker: MarkerData = {
70
+ id: m.id,
71
+ longitude: m.lng,
72
+ latitude: m.lat,
73
+ data: m.label ? { label: m.label } : undefined,
74
+ };
75
+ return (
76
+ <MapMarker
77
+ key={m.id}
78
+ marker={marker}
79
+ color={m.color}
80
+ ariaLabel={m.card?.title ?? m.label}
81
+ onClick={m.card ? () => toggle(m.id) : undefined}
82
+ >
83
+ {m.icon ? (
84
+ <img
85
+ src={m.icon}
86
+ alt={m.label ?? ''}
87
+ className="h-7 w-7 rounded-full object-cover ring-2 ring-background shadow-md"
88
+ />
89
+ ) : undefined}
90
+ </MapMarker>
91
+ );
92
+ })}
93
+
94
+ {open?.card && (
95
+ <MapPopup
96
+ longitude={open.lng}
97
+ latitude={open.lat}
98
+ anchor="bottom"
99
+ onClose={close}
100
+ maxWidth="none"
101
+ >
102
+ <MarkerCard {...open.card} />
103
+ </MapPopup>
104
+ )}
105
+ </>
106
+ );
107
+ }
108
+
10
109
  export default function MapBlockRenderer({ block, ctx }: BlockRendererProps<MapBlock>) {
11
110
  const height = ctx.appearance === 'compact' ? 240 : 380;
111
+
112
+ // The serializable map shape (center / markers / routes / polygons) is
113
+ // validated centrally at the registry dispatch point (MessageBlocks) before
114
+ // this renderer runs, so we can trust the payload here.
115
+ const routes = block.routes ?? [];
116
+ const polygons = block.polygons ?? [];
117
+ const markers = block.markers ?? [];
118
+
119
+ // Collect every coordinate so the viewport can frame the whole scene when
120
+ // the block doesn't pin an explicit center/zoom.
121
+ const allPoints: LngLat[] = [
122
+ ...markers.map((m): LngLat => [m.lng, m.lat]),
123
+ ...routes.flatMap((r) => r.points.map((p): LngLat => [p.lng, p.lat])),
124
+ ...polygons.flatMap((p) => p.points.map((pt): LngLat => [pt.lng, pt.lat])),
125
+ ];
126
+
127
+ // Respect an explicit zoom (author intent); otherwise auto-fit to geometry.
128
+ // Center always defaults to the block's `center` unless we have geometry to
129
+ // frame and no explicit zoom was given.
130
+ const fitted = block.zoom === undefined && allPoints.length > 1 ? viewportForBounds(allPoints) : null;
131
+ const initialViewport = {
132
+ longitude: fitted?.longitude ?? block.center.lng,
133
+ latitude: fitted?.latitude ?? block.center.lat,
134
+ zoom: block.zoom ?? fitted?.zoom ?? 11,
135
+ };
136
+
137
+ // Build the optional props into ONE typed object before spreading, rather
138
+ // than several inline conditional `{...(cond ? {…} : {})}` spreads. The
139
+ // inline form unions each branch to `{} | {prop}` and widens the call to a
140
+ // shape no single `MapContainer` overload accepts (TS2769) under stricter
141
+ // consumer resolution; a single `Partial<MapContainerProps>` literal is
142
+ // checked against the prop type as a unit.
143
+ const mapOptions: Partial<MapContainerProps> = {
144
+ ...(block.basemap ? { mapStyle: block.basemap } : {}),
145
+ ...(block.terrain ? { terrain: true } : {}),
146
+ ...(block.userLocation ? { geolocate: true } : {}),
147
+ };
148
+
149
+ // Frame (radius / border / shadow / clip) comes from the shared
150
+ // `BLOCK_SURFACE` in MessageBlocks — here we only set the height.
12
151
  return (
13
- <div
14
- className="overflow-hidden rounded-xl border border-border"
15
- style={{ height }}
16
- >
152
+ <div className="w-full" style={{ height }}>
17
153
  <LazyMapContainer
18
- initialViewport={{
19
- longitude: block.center.lng,
20
- latitude: block.center.lat,
21
- zoom: block.zoom ?? 11,
22
- }}
154
+ initialViewport={initialViewport}
155
+ fullscreenTitle={block.caption ?? 'Map'}
156
+ {...mapOptions}
23
157
  >
24
- {block.markers?.map((m) => {
25
- const marker: MarkerData = {
26
- id: m.id,
27
- longitude: m.lng,
28
- latitude: m.lat,
29
- data: m.label ? { label: m.label } : undefined,
30
- };
31
- return <MapMarker key={m.id} marker={marker} ariaLabel={m.label} />;
158
+ {/* Filled zones first so routes/markers stack above them. */}
159
+ {polygons.map((poly) => {
160
+ const sourceId = `poly-${poly.id}`;
161
+ const data = createFeatureCollection([
162
+ createPolygon(poly.points.map((p): LngLat => [p.lng, p.lat]), poly.label ? { label: poly.label } : {}),
163
+ ]);
164
+ const fill = createPolygonLayer(`${sourceId}-fill`, sourceId, {
165
+ fillColor: poly.fillColor,
166
+ strokeColor: poly.strokeColor,
167
+ });
168
+ const outline = createPolygonOutlineLayer(`${sourceId}-outline`, sourceId, {
169
+ color: poly.strokeColor,
170
+ });
171
+ return (
172
+ <MapSource key={sourceId} id={sourceId} data={data}>
173
+ <MapLayer {...fill} />
174
+ <MapLayer {...outline} />
175
+ </MapSource>
176
+ );
32
177
  })}
178
+
179
+ {/* Routes (polylines) — white-outlined line via the route factory. */}
180
+ {routes.map((route) => {
181
+ const sourceId = `route-${route.id}`;
182
+ const data = createFeatureCollection([
183
+ createLineString(route.points.map((p): LngLat => [p.lng, p.lat]), route.label ? { label: route.label } : {}),
184
+ ]);
185
+ const { outline, route: line } = createRouteLayers({ sourceId, color: route.color });
186
+ return (
187
+ <MapSource key={sourceId} id={sourceId} data={data}>
188
+ <MapLayer {...outline} />
189
+ <MapLayer {...line} />
190
+ </MapSource>
191
+ );
192
+ })}
193
+
194
+ <MapBlockMarkers markers={markers} />
33
195
  </LazyMapContainer>
34
196
  </div>
35
197
  );
@@ -14,11 +14,8 @@ export default function VideoBlockRenderer({ block }: BlockRendererProps<VideoBl
14
14
  const source: UrlSource | string = block.poster
15
15
  ? { type: 'url', url: block.src, poster: block.poster, title: block.title }
16
16
  : block.src;
17
+ // Frame (radius / clip) comes from the shared `BLOCK_SURFACE`.
17
18
  return (
18
- <VideoPlayer
19
- source={source}
20
- aspectRatio={block.aspectRatio ?? 16 / 9}
21
- className="overflow-hidden rounded-xl"
22
- />
19
+ <VideoPlayer source={source} aspectRatio={block.aspectRatio ?? 16 / 9} />
23
20
  );
24
21
  }
@@ -8,7 +8,20 @@ export {
8
8
  type MessageListHandle,
9
9
  } from './MessageList';
10
10
  export { MessageBubble, type MessageBubbleProps } from './MessageBubble';
11
+ // Re-exported from the shared `common/link-preview` location (the card was
12
+ // hoisted out of chat so the NotionEditor bookmark node can reuse it). The
13
+ // chat public surface (`@djangocfg/ui-tools/chat`) keeps exporting it from
14
+ // here unchanged.
15
+ export { LinkPreviewCard, type LinkPreviewCardProps } from '../../../common/link-preview';
11
16
  export { MessageActions, type MessageActionsProps } from './MessageActions';
17
+ export {
18
+ BubbleContextMenu,
19
+ buildDefaultBubbleMenuItems,
20
+ composeBubbleMenuItems,
21
+ type BubbleContextMenuProps,
22
+ type BubbleMenuItem,
23
+ type DefaultBubbleMenuHandlers,
24
+ } from './BubbleContextMenu';
12
25
  export { Sources, type SourcesProps } from './Sources';
13
26
  export { ToolCalls, type ToolCallsProps, type ToolPayloadKind } from './ToolCalls';
14
27
  export {
@@ -53,8 +53,12 @@ export type {
53
53
  JsonBlock,
54
54
  MermaidBlock,
55
55
  CodeBlock,
56
- LottieBlock,
56
+ DiffBlock,
57
+ LinkBlock,
57
58
  CustomBlock,
59
+ LinkPreviewData,
60
+ ResolveLinkPreview,
61
+ ChatLinkPreviewConfig,
58
62
  } from './types';
59
63
  export { DEFAULT_LABELS } from './types';
60
64
 
@@ -26,6 +26,7 @@ import { EmptyState } from './EmptyState';
26
26
  import { ErrorBanner } from './ErrorBanner';
27
27
  import { JumpToLatest } from '../messages/JumpToLatest';
28
28
  import { MessageBubble } from '../messages/MessageBubble';
29
+ import type { BubbleMenuItem } from '../messages/BubbleContextMenu';
29
30
  import { MessageList, type MessageListHandle } from '../messages/MessageList';
30
31
  import type { AttachmentRendererMap } from '../messages/Attachments';
31
32
  import type { BlockRegistry } from '../messages/blocks';
@@ -102,6 +103,27 @@ export interface ChatMessagesConfig {
102
103
  * Omit to use `BUILTIN_BLOCK_REGISTRY`.
103
104
  */
104
105
  blockRegistry?: BlockRegistry;
106
+ /**
107
+ * Host-injected EXTRA right-click / long-press menu items per bubble,
108
+ * merged after the built-in Copy / Regenerate / Delete defaults.
109
+ * Forwarded to `<MessageBubble bubbleMenuItems>`.
110
+ */
111
+ bubbleMenuItems?: BubbleMenuItem[];
112
+ /**
113
+ * Full control over each bubble's context menu — receives the message
114
+ * and the built-in defaults, returns the final ordered list. Beats
115
+ * `bubbleMenuItems`. Forwarded to `<MessageBubble getBubbleMenuItems>`.
116
+ */
117
+ getBubbleMenuItems?: (message: ChatMessage, defaults: BubbleMenuItem[]) => BubbleMenuItem[];
118
+ /**
119
+ * Host handler for the bubble "Edit" menu item (user messages only — the
120
+ * builtin builder user-gates it). The host loads the message's content into
121
+ * its composer for editing and re-runs the turn on submit via
122
+ * `chat.editMessage` (engine truncate+resend). When omitted the "Edit" item
123
+ * never appears. This is intentionally a host hook, not an inline editor: the
124
+ * edit happens in the real composer, not the bubble.
125
+ */
126
+ onEdit?: (message: ChatMessage) => void;
105
127
  }
106
128
 
107
129
  export interface ChatRootProps {
@@ -278,8 +300,15 @@ function ChatRootShell({ className, listClassName, parts }: ChatRootShellProps)
278
300
  blockRegistry={messages.blockRegistry}
279
301
  onAttachmentOpen={messages.onAttachmentOpen}
280
302
  renderAfterMessage={messages.renderAfter}
303
+ bubbleMenuItems={messages.bubbleMenuItems}
304
+ getBubbleMenuItems={messages.getBubbleMenuItems}
281
305
  onCopy={() => copy(m.content)}
282
306
  onRegenerate={() => void chat.regenerate(m.id)}
307
+ // Edit (user bubbles): hand the message to the host so it can load the
308
+ // text into the real composer. The host owns the edit-mode UI + the
309
+ // submit branch (chat.editMessage). Omitted handler → no Edit item.
310
+ // No in-bubble editor: editing happens in the composer, not the bubble.
311
+ onEdit={messages.onEdit ? () => messages.onEdit?.(m) : undefined}
283
312
  onDelete={() => chat.deleteMessage(m.id)}
284
313
  />
285
314
  ));
@@ -23,6 +23,24 @@ export const BUBBLE_SURFACE = {
23
23
  error: 'bg-destructive/10 text-destructive rounded-tl-md border border-destructive/30',
24
24
  } as const;
25
25
 
26
+ /**
27
+ * Shared media-block surface — the single frame every non-prose
28
+ * `message.blocks` renderer sits in (image, video, gallery, map, audio,
29
+ * code, json, mermaid). Owning the frame here (instead of each
30
+ * renderer inventing its own) guarantees one consistent corner radius,
31
+ * hairline border and soft shadow across all block kinds — the calm,
32
+ * uniform media-card look of Telegram / WhatsApp.
33
+ *
34
+ * `rounded-2xl` (16px) — large, soft messenger corner.
35
+ * `border-border` — hairline, theme-aware.
36
+ * `shadow-sm` — gentle lift off the transcript background.
37
+ * `overflow-hidden` — clips inner content (maps, images) to the radius.
38
+ * `bg-card` — neutral surface so a renderer with a transparent
39
+ * body (json tree) still reads as a card.
40
+ */
41
+ export const BLOCK_SURFACE =
42
+ 'overflow-hidden rounded-2xl border border-border bg-card shadow-sm' as const;
43
+
26
44
  /**
27
45
  * Anchor (link) classes for markdown content rendered inside a bubble.
28
46
  *
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  BUBBLE_SURFACE,
3
+ BLOCK_SURFACE,
3
4
  ANCHOR,
4
5
  TOGGLE,
5
6
  DESTRUCTIVE_SURFACE,
@@ -2,9 +2,32 @@
2
2
  * Attachments + sources attached to a chat message.
3
3
  */
4
4
 
5
+ /**
6
+ * Rich-preview payload for a localpreview-style attachment card. Serializable
7
+ * JSON only (no React nodes) so it survives history persistence + SSE — the
8
+ * render seam already exists (`AttachmentRendererMap`); this is the DATA that
9
+ * feeds it. One generic bag instead of a typed field per media kind: a host
10
+ * populates `kind` + `fields[]` (duration, dimensions, pages, EXIF…) and the
11
+ * default/per-type renderer reads it. Absent → renderer falls back to the
12
+ * name+thumbnail chip.
13
+ */
14
+ export interface AttachmentPreview {
15
+ /** Drives which rich card the renderer mounts (audio/image/pdf/doc/…). */
16
+ kind: string;
17
+ title?: string;
18
+ /** Cover / first-page thumbnail (audio art, pdf page-1). */
19
+ thumbnail?: string;
20
+ /** Ordered label/value rows the card renders (duration, dims, pages, EXIF…). */
21
+ fields?: { label: string; value: string }[];
22
+ }
23
+
5
24
  export interface ChatAttachment {
6
25
  id: string;
7
- type: 'image' | 'file' | 'audio' | 'video';
26
+ type: 'image' | 'file' | 'audio' | 'video' | 'text';
27
+ /**
28
+ * Object-URL / remote URL for binary attachments. Empty string for
29
+ * `type:'text'` chunks — those carry their payload in `text`, not a URL.
30
+ */
8
31
  url: string;
9
32
  thumbnailUrl?: string;
10
33
  name?: string;
@@ -13,8 +36,20 @@ export interface ChatAttachment {
13
36
  status?: 'uploading' | 'ready' | 'error';
14
37
  /** 0..1 while uploading. */
15
38
  progress?: number;
16
- /** Extracted text from images (OCR). */
17
- ocrText?: string;
39
+ /**
40
+ * Raw text payload for `type:'text'` chunks — the "Pasted text"
41
+ * attachment minted when a long paste is converted into a chip
42
+ * instead of dumped into the textarea (ChatGPT/Claude behaviour).
43
+ * Preserves exact content: newlines, indentation, stack traces.
44
+ */
45
+ text?: string;
46
+ /**
47
+ * Optional rich-preview payload for a localpreview-style card (audio
48
+ * cover+duration, image thumbnail+EXIF, pdf pages, OCR metadata…).
49
+ * Serializable JSON; persists through history/SSE. Absent → the default
50
+ * renderer falls back to the name+thumbnail chip.
51
+ */
52
+ preview?: AttachmentPreview;
18
53
  }
19
54
 
20
55
  export interface ChatSource {
@@ -7,6 +7,9 @@
7
7
  * persistence and SSE transport. See `@docs/message-blocks.md`.
8
8
  */
9
9
 
10
+ import type { LinkPreviewData } from '../../../common/link-preview';
11
+ import type { MapBlockPayload } from '../../../common/blocks';
12
+
10
13
  /** Spaciousness hint forwarded from the bubble to renderers. */
11
14
  export type BlockAppearance = 'compact' | 'full';
12
15
 
@@ -59,11 +62,28 @@ export interface GalleryBlock extends BlockBase {
59
62
  items: Array<{ id: string; src: string; thumbnail?: string; alt?: string }>;
60
63
  }
61
64
 
62
- export interface MapBlock extends BlockBase {
65
+ /**
66
+ * Map block — a chat message map. The serializable map SHAPE (center / zoom /
67
+ * basemap / terrain / markers / routes / polygons) is the shared
68
+ * `MapBlockPayload` (defined once in `common/blocks/types.ts`) so the chat
69
+ * map and the NotionEditor `mapBlock` node can't drift. Chat adds only the
70
+ * chat-specific `userLocation` opt-in on top.
71
+ */
72
+ export interface MapBlock extends BlockBase, MapBlockPayload {
63
73
  kind: 'map';
64
- center: { lat: number; lng: number };
65
- zoom?: number;
66
- markers?: Array<{ id: string; lat: number; lng: number; label?: string }>;
74
+ /**
75
+ * Opt in to the map's "show my location" chip (privacy-sensitive).
76
+ *
77
+ * This is a plain boolean OPT-IN flag, **not** coordinates — the browser
78
+ * supplies the live position; we never bake lat/lng into the serialized
79
+ * block. Default **off**: only set `true` when the host wants this chat
80
+ * map to offer locate-me. When `true` it surfaces the toolbar locate-me
81
+ * chip (forwarded to `MapContainer`'s `geolocate` prop); the user must
82
+ * still grant the OS location permission, and nothing is requested until
83
+ * they press the chip. Requires a secure context (HTTPS / localhost).
84
+ * @default false
85
+ */
86
+ userLocation?: boolean;
67
87
  }
68
88
 
69
89
  export interface JsonBlock extends BlockBase {
@@ -84,10 +104,36 @@ export interface CodeBlock extends BlockBase {
84
104
  language: string;
85
105
  }
86
106
 
87
- export interface LottieBlock extends BlockBase {
88
- kind: 'lottie';
89
- /** Animation JSON URL or inline animationData object. */
90
- src: string;
107
+ /**
108
+ * Code-diff block — wraps the `DiffViewer` tool. Provide EITHER
109
+ * `oldCode` + `newCode` (the viewer runs an internal LCS diff) OR a
110
+ * pre-computed unified `patch` string. `layout` pins side-by-side
111
+ * (`split`) vs single-column (`unified`); when omitted it follows the
112
+ * bubble appearance (compact → unified, full → split).
113
+ */
114
+ export interface DiffBlock extends BlockBase {
115
+ kind: 'diff';
116
+ oldCode?: string;
117
+ newCode?: string;
118
+ patch?: string;
119
+ language?: string;
120
+ layout?: 'split' | 'unified';
121
+ }
122
+
123
+ /**
124
+ * Link-preview (URL unfurl) card — favicon + og:image + title/description/
125
+ * site name, Notion/Telegram style. Either the metadata is already
126
+ * resolved (`data`), or only `url` is given and the host's
127
+ * `resolveLinkPreview` resolver fills it in lazily. See
128
+ * `common/link-preview/types.ts` for the data contract (the browser cannot
129
+ * fetch meta tags itself — resolution is a host concern).
130
+ */
131
+ export interface LinkBlock extends BlockBase {
132
+ kind: 'link';
133
+ /** The URL to preview (and link to). */
134
+ url: string;
135
+ /** Pre-resolved metadata. When absent, the host resolver is used. */
136
+ data?: LinkPreviewData;
91
137
  }
92
138
 
93
139
  /**
@@ -113,7 +159,8 @@ export type MessageBlock =
113
159
  | JsonBlock
114
160
  | MermaidBlock
115
161
  | CodeBlock
116
- | LottieBlock
162
+ | DiffBlock
163
+ | LinkBlock
117
164
  | CustomBlock;
118
165
 
119
166
  /** The discriminant literal — `'audio' | 'video' | …`. */