@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
|
@@ -11,8 +11,10 @@ import { Component, type ReactNode } from 'react';
|
|
|
11
11
|
|
|
12
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
13
|
|
|
14
|
+
import { BlockError, safeParseBlock } from '../../../../common/blocks';
|
|
14
15
|
import type { MessageBlock } from '../../types/block';
|
|
15
|
-
import {
|
|
16
|
+
import { BLOCK_SCHEMAS } from '../../types/blockSchemas';
|
|
17
|
+
import { useChatBubbleStyles, BLOCK_SURFACE } from '../../styles';
|
|
16
18
|
import { BUILTIN_BLOCK_REGISTRY, type BlockRegistry } from './registry';
|
|
17
19
|
|
|
18
20
|
export interface MessageBlocksProps {
|
|
@@ -29,9 +31,30 @@ export interface MessageBlocksProps {
|
|
|
29
31
|
strict?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/** Block kinds rendered inside a chat-bubble surface,
|
|
34
|
+
/** Block kinds rendered inside a chat-bubble surface (bubble-tinted, inline). */
|
|
33
35
|
const PROSE_KINDS = new Set<MessageBlock['kind']>(['text', 'markdown']);
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Block kinds wrapped in the shared media-card surface (`BLOCK_SURFACE`) so
|
|
39
|
+
* every media block shares one radius / border / shadow. `prose` kinds get
|
|
40
|
+
* the bubble surface instead (above).
|
|
41
|
+
*
|
|
42
|
+
* Bare kinds skip the light card and own their frame:
|
|
43
|
+
* - `custom` — host-owned chrome.
|
|
44
|
+
* - `link` — the LinkPreviewCard ships its own frame.
|
|
45
|
+
* - `audio` — the player draws its own card AND a scrub-time tooltip that
|
|
46
|
+
* sits ABOVE the bar; a clipping card would crop that tooltip, so the
|
|
47
|
+
* player rounds itself to the shared 2xl radius instead (see AudioBlock).
|
|
48
|
+
* - `diff` — the DiffViewer ships its own bordered panel + a floating
|
|
49
|
+
* copy button that a clip would crop; it re-rounds to 2xl (see DiffBlock).
|
|
50
|
+
*
|
|
51
|
+
* `code` is NOT bare: its dark editor panel can't be reliably re-rounded
|
|
52
|
+
* from outside (PrettyCode rounds its inner layers to `rounded-lg`), so we
|
|
53
|
+
* let the shared `overflow-hidden rounded-2xl` card CLIP it to the uniform
|
|
54
|
+
* corner instead. (Code has no overflowing tooltip, so clipping is safe.)
|
|
55
|
+
*/
|
|
56
|
+
const BARE_KINDS = new Set<MessageBlock['kind']>(['custom', 'link', 'audio', 'diff']);
|
|
57
|
+
|
|
35
58
|
interface BoundaryProps {
|
|
36
59
|
kind: string;
|
|
37
60
|
children: ReactNode;
|
|
@@ -91,8 +114,28 @@ export function MessageBlocks({
|
|
|
91
114
|
| ((b: MessageBlock, c: { appearance: 'compact' | 'full'; isUser: boolean }) => ReactNode)
|
|
92
115
|
| undefined;
|
|
93
116
|
|
|
117
|
+
// Central payload validation — every block flows through here, so a
|
|
118
|
+
// malformed payload (wrong-typed `src`, non-array gallery `items`, bad
|
|
119
|
+
// map coords) renders a safe `<BlockError>` instead of throwing inside
|
|
120
|
+
// its renderer and blanking the transcript. Kinds without a schema
|
|
121
|
+
// (e.g. host-defined `custom`) skip this and render as-is. The renderer
|
|
122
|
+
// can therefore trust its input. The error boundary below stays as a
|
|
123
|
+
// last-resort runtime guard.
|
|
124
|
+
const schema = BLOCK_SCHEMAS[block.kind];
|
|
125
|
+
|
|
94
126
|
let body: ReactNode;
|
|
95
|
-
if (
|
|
127
|
+
if (schema) {
|
|
128
|
+
const parsed = safeParseBlock(schema, block);
|
|
129
|
+
if (!parsed.ok) {
|
|
130
|
+
body = <BlockError label={block.kind} reason={parsed.error} />;
|
|
131
|
+
} else if (renderer) {
|
|
132
|
+
body = renderer(block, { appearance, isUser });
|
|
133
|
+
} else if (strict) {
|
|
134
|
+
body = <UnknownBlock kind={block.kind} />;
|
|
135
|
+
} else {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
} else if (renderer) {
|
|
96
139
|
body = renderer(block, { appearance, isUser });
|
|
97
140
|
} else if (strict) {
|
|
98
141
|
body = <UnknownBlock kind={block.kind} />;
|
|
@@ -101,6 +144,29 @@ export function MessageBlocks({
|
|
|
101
144
|
}
|
|
102
145
|
|
|
103
146
|
const isProse = PROSE_KINDS.has(block.kind);
|
|
147
|
+
const isBare = BARE_KINDS.has(block.kind);
|
|
148
|
+
|
|
149
|
+
// Surface selection — one consistent frame per category:
|
|
150
|
+
// prose → tinted chat bubble (inline)
|
|
151
|
+
// bare → no frame (host-owned `custom` block)
|
|
152
|
+
// media → shared `BLOCK_SURFACE` card (image/video/map/code/…)
|
|
153
|
+
let framed: ReactNode;
|
|
154
|
+
if (isProse) {
|
|
155
|
+
framed = (
|
|
156
|
+
<div
|
|
157
|
+
className={cn(
|
|
158
|
+
'inline-block max-w-full rounded-2xl px-3.5 py-2 text-sm',
|
|
159
|
+
bubbleSurface,
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{body}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
} else if (isBare) {
|
|
166
|
+
framed = body;
|
|
167
|
+
} else {
|
|
168
|
+
framed = <div className={cn('w-full', BLOCK_SURFACE)}>{body}</div>;
|
|
169
|
+
}
|
|
104
170
|
|
|
105
171
|
return (
|
|
106
172
|
<div key={block.id} data-block-kind={block.kind} className="w-full">
|
|
@@ -109,20 +175,7 @@ export function MessageBlocks({
|
|
|
109
175
|
{block.caption}
|
|
110
176
|
</div>
|
|
111
177
|
) : null}
|
|
112
|
-
<BlockErrorBoundary kind={block.kind}>
|
|
113
|
-
{isProse ? (
|
|
114
|
-
<div
|
|
115
|
-
className={cn(
|
|
116
|
-
'inline-block max-w-full rounded-2xl px-3.5 py-2 text-sm',
|
|
117
|
-
bubbleSurface,
|
|
118
|
-
)}
|
|
119
|
-
>
|
|
120
|
-
{body}
|
|
121
|
-
</div>
|
|
122
|
-
) : (
|
|
123
|
-
body
|
|
124
|
-
)}
|
|
125
|
-
</BlockErrorBoundary>
|
|
178
|
+
<BlockErrorBoundary kind={block.kind}>{framed}</BlockErrorBoundary>
|
|
126
179
|
</div>
|
|
127
180
|
);
|
|
128
181
|
})}
|
|
@@ -16,10 +16,11 @@ import { MarkdownMessage } from '../../../dev/code/MarkdownMessage';
|
|
|
16
16
|
import type {
|
|
17
17
|
AudioBlock,
|
|
18
18
|
CodeBlock,
|
|
19
|
+
DiffBlock,
|
|
19
20
|
GalleryBlock,
|
|
20
21
|
ImageBlock,
|
|
21
22
|
JsonBlock,
|
|
22
|
-
|
|
23
|
+
LinkBlock,
|
|
23
24
|
MapBlock,
|
|
24
25
|
MarkdownBlock,
|
|
25
26
|
MermaidBlock,
|
|
@@ -63,7 +64,8 @@ const MapBlockRenderer = lazyBlock<MapBlock>(() => import('./renderers/MapBlock'
|
|
|
63
64
|
const JsonBlockRenderer = lazyBlock<JsonBlock>(() => import('./renderers/JsonBlock'));
|
|
64
65
|
const MermaidBlockRenderer = lazyBlock<MermaidBlock>(() => import('./renderers/MermaidBlock'));
|
|
65
66
|
const CodeBlockRenderer = lazyBlock<CodeBlock>(() => import('./renderers/CodeBlock'));
|
|
66
|
-
const
|
|
67
|
+
const DiffBlockRenderer = lazyBlock<DiffBlock>(() => import('./renderers/DiffBlock'));
|
|
68
|
+
const LinkBlockRenderer = lazyBlock<LinkBlock>(() => import('./renderers/LinkBlock'));
|
|
67
69
|
|
|
68
70
|
/** Render a `text` / `markdown` block as bubble-styled prose. */
|
|
69
71
|
function renderProse(content: string, isUser: boolean) {
|
|
@@ -85,7 +87,8 @@ export const BUILTIN_BLOCK_REGISTRY: BlockRegistry = {
|
|
|
85
87
|
json: (block, ctx) => <JsonBlockRenderer block={block} ctx={ctx} />,
|
|
86
88
|
mermaid: (block, ctx) => <MermaidBlockRenderer block={block} ctx={ctx} />,
|
|
87
89
|
code: (block, ctx) => <CodeBlockRenderer block={block} ctx={ctx} />,
|
|
88
|
-
|
|
90
|
+
diff: (block, ctx) => <DiffBlockRenderer block={block} ctx={ctx} />,
|
|
91
|
+
link: (block, ctx) => <LinkBlockRenderer block={block} ctx={ctx} />,
|
|
89
92
|
// `custom` has no built-in renderer — it falls through to the
|
|
90
93
|
// unknown-kind fallback unless the host registers one.
|
|
91
94
|
};
|
|
@@ -8,6 +8,11 @@ import type { BlockRendererProps } from './types';
|
|
|
8
8
|
|
|
9
9
|
export default function AudioBlockRenderer({ block, ctx }: BlockRendererProps<AudioBlock>) {
|
|
10
10
|
const variant = block.variant ?? (ctx.appearance === 'compact' ? 'compact' : 'default');
|
|
11
|
+
// `audio` is a "bare" block (no clipping card) because the player renders
|
|
12
|
+
// a scrub-time tooltip ABOVE the bar that a clip would crop. So the player
|
|
13
|
+
// owns its frame — aligned to the shared 2xl radius + soft shadow.
|
|
14
|
+
// `!`-utilities win over PlayerShell's template-string `rounded-lg`
|
|
15
|
+
// regardless of stylesheet order.
|
|
11
16
|
return (
|
|
12
17
|
<LazyPlayer
|
|
13
18
|
src={block.src}
|
|
@@ -15,6 +20,7 @@ export default function AudioBlockRenderer({ block, ctx }: BlockRendererProps<Au
|
|
|
15
20
|
artist={block.artist}
|
|
16
21
|
cover={block.cover}
|
|
17
22
|
variant={variant}
|
|
23
|
+
className="!rounded-2xl shadow-sm"
|
|
18
24
|
/>
|
|
19
25
|
);
|
|
20
26
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat image album — a Telegram/WhatsApp-style adaptive collage for 1–N
|
|
5
|
+
* images. NOT a true masonry: messengers use a small set of deterministic
|
|
6
|
+
* album layouts (no JS height measuring, no extra deps), which is the right
|
|
7
|
+
* call for a chat where albums are a small, bounded case.
|
|
8
|
+
*
|
|
9
|
+
* 1 → single image 2 → two equal columns
|
|
10
|
+
* 3 → one tall + two stacked 4 → 2×2 grid
|
|
11
|
+
* 5+ → 2×2 grid, the last visible tile shows a "+N more" overlay
|
|
12
|
+
*
|
|
13
|
+
* Clicking any tile opens the shared zoom/pan `ImageViewer` lightbox in a
|
|
14
|
+
* dialog, seeded at the clicked index, with prev/next across the whole set.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import { Dialog, DialogContent, DialogTitle } from '@djangocfg/ui-core/components';
|
|
20
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
21
|
+
|
|
22
|
+
import { LazyImageViewer } from '../../../../media/ImageViewer/lazy';
|
|
23
|
+
|
|
24
|
+
export interface ChatAlbumImage {
|
|
25
|
+
id: string;
|
|
26
|
+
src: string;
|
|
27
|
+
thumbnail?: string;
|
|
28
|
+
alt?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChatImageAlbumProps {
|
|
32
|
+
images: ChatAlbumImage[];
|
|
33
|
+
/** Max tiles shown before collapsing the rest into a "+N" overlay. */
|
|
34
|
+
maxVisible?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tile — a single clickable image cell. */
|
|
38
|
+
function Tile({
|
|
39
|
+
image,
|
|
40
|
+
onClick,
|
|
41
|
+
className,
|
|
42
|
+
overlayCount,
|
|
43
|
+
}: {
|
|
44
|
+
image: ChatAlbumImage;
|
|
45
|
+
onClick: () => void;
|
|
46
|
+
className?: string;
|
|
47
|
+
overlayCount?: number;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
aria-label={`Open image${image.alt ? `: ${image.alt}` : ''}`}
|
|
54
|
+
className={cn(
|
|
55
|
+
'group relative block overflow-hidden bg-muted',
|
|
56
|
+
'cursor-zoom-in focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
61
|
+
<img
|
|
62
|
+
src={image.thumbnail ?? image.src}
|
|
63
|
+
alt={image.alt ?? ''}
|
|
64
|
+
loading="lazy"
|
|
65
|
+
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.02]"
|
|
66
|
+
/>
|
|
67
|
+
{overlayCount ? (
|
|
68
|
+
<span className="absolute inset-0 flex items-center justify-center bg-black/50 text-xl font-semibold text-white">
|
|
69
|
+
+{overlayCount}
|
|
70
|
+
</span>
|
|
71
|
+
) : null}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function ChatImageAlbum({ images, maxVisible = 4 }: ChatImageAlbumProps) {
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
const [index, setIndex] = useState(0);
|
|
79
|
+
|
|
80
|
+
const openAt = useCallback((i: number) => {
|
|
81
|
+
setIndex(i);
|
|
82
|
+
setOpen(true);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const count = images.length;
|
|
86
|
+
const visible = images.slice(0, Math.min(count, maxVisible));
|
|
87
|
+
const hiddenCount = count - visible.length;
|
|
88
|
+
|
|
89
|
+
// Layout per count. `gap-0.5` mimics the tight messenger seam; the outer
|
|
90
|
+
// BLOCK_SURFACE provides the rounded clip + border.
|
|
91
|
+
let grid: React.ReactNode;
|
|
92
|
+
if (count === 1) {
|
|
93
|
+
const img = images[0];
|
|
94
|
+
grid = (
|
|
95
|
+
<Tile image={img} onClick={() => openAt(0)} className="max-h-96 w-full" />
|
|
96
|
+
);
|
|
97
|
+
} else if (count === 2) {
|
|
98
|
+
grid = (
|
|
99
|
+
<div className="grid aspect-[2/1] grid-cols-2 gap-0.5">
|
|
100
|
+
{visible.map((img, i) => (
|
|
101
|
+
<Tile key={img.id} image={img} onClick={() => openAt(i)} />
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
} else if (count === 3) {
|
|
106
|
+
grid = (
|
|
107
|
+
<div className="grid aspect-[3/2] grid-cols-2 grid-rows-2 gap-0.5">
|
|
108
|
+
<Tile image={visible[0]} onClick={() => openAt(0)} className="row-span-2" />
|
|
109
|
+
<Tile image={visible[1]} onClick={() => openAt(1)} />
|
|
110
|
+
<Tile image={visible[2]} onClick={() => openAt(2)} />
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
// 4+ → 2×2, last visible tile carries the "+N" overlay when collapsed.
|
|
115
|
+
grid = (
|
|
116
|
+
<div className="grid aspect-square grid-cols-2 grid-rows-2 gap-0.5">
|
|
117
|
+
{visible.map((img, i) => {
|
|
118
|
+
const isLast = i === visible.length - 1;
|
|
119
|
+
return (
|
|
120
|
+
<Tile
|
|
121
|
+
key={img.id}
|
|
122
|
+
image={img}
|
|
123
|
+
onClick={() => openAt(i)}
|
|
124
|
+
overlayCount={isLast && hiddenCount > 0 ? hiddenCount : undefined}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
{grid}
|
|
135
|
+
|
|
136
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
137
|
+
<DialogContent className="h-[90vh] w-[90vw] max-w-[1400px] overflow-hidden p-0 sm:rounded-xl">
|
|
138
|
+
<DialogTitle className="sr-only">Image {index + 1} of {count}</DialogTitle>
|
|
139
|
+
{open && (
|
|
140
|
+
<LazyImageViewer
|
|
141
|
+
images={images.map((img) => ({
|
|
142
|
+
file: { name: img.alt ?? 'image', path: img.src },
|
|
143
|
+
src: img.src,
|
|
144
|
+
}))}
|
|
145
|
+
initialIndex={index}
|
|
146
|
+
inDialog
|
|
147
|
+
autoFocus
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
</DialogContent>
|
|
151
|
+
</Dialog>
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -1,19 +1,107 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Code block — read-only syntax highlighting via `PrettyCode`.
|
|
5
|
+
*
|
|
6
|
+
* Inline, the panel is height-capped (`maxLines`) so a long snippet never
|
|
7
|
+
* blows up the transcript — beyond the cap PrettyCode scrolls internally.
|
|
8
|
+
* When capped, a header expand (⤢) button opens the FULL code in a large
|
|
9
|
+
* dialog (same pattern as the map / image blocks), where it reads/scrolls
|
|
10
|
+
* comfortably and copies in one shot.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { Check, Copy, X } from 'lucide-react';
|
|
16
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
|
+
import { Dialog, DialogClose, DialogContent, DialogTitle } from '@djangocfg/ui-core/components';
|
|
4
18
|
|
|
5
19
|
import { LazyPrettyCode } from '../../../../dev/code/PrettyCode/lazy';
|
|
6
20
|
import type { Language } from '../../../../dev/code/PrettyCode/lazy';
|
|
7
21
|
import type { CodeBlock } from '../../../types/block';
|
|
8
22
|
import type { BlockRendererProps } from './types';
|
|
9
23
|
|
|
24
|
+
/** Roomy on-dark button for the expanded dialog header. */
|
|
25
|
+
const DIALOG_BTN =
|
|
26
|
+
'inline-flex h-8 items-center justify-center gap-1.5 rounded-lg px-2.5 text-xs font-medium ' +
|
|
27
|
+
'text-white/70 transition-colors hover:bg-white/10 hover:text-white ' +
|
|
28
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/40';
|
|
29
|
+
|
|
10
30
|
export default function CodeBlockRenderer({ block, ctx }: BlockRendererProps<CodeBlock>) {
|
|
11
31
|
const compact = ctx.appearance === 'compact';
|
|
32
|
+
const [open, setOpen] = useState(false);
|
|
33
|
+
const [copied, setCopied] = useState(false);
|
|
34
|
+
const language = block.language as Language;
|
|
35
|
+
|
|
36
|
+
const copyAll = useCallback(() => {
|
|
37
|
+
void navigator.clipboard?.writeText(block.code).then(() => {
|
|
38
|
+
setCopied(true);
|
|
39
|
+
setTimeout(() => setCopied(false), 1400);
|
|
40
|
+
});
|
|
41
|
+
}, [block.code]);
|
|
42
|
+
|
|
43
|
+
// 16 lines in the narrow chat, 24 in a full-page chat. Beyond this the
|
|
44
|
+
// inline panel scrolls and the header shows the expand button.
|
|
45
|
+
const maxLines = compact ? 16 : 24;
|
|
46
|
+
|
|
12
47
|
return (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
48
|
+
<>
|
|
49
|
+
{/* Inline — capped + internally scrollable; expand opens the dialog.
|
|
50
|
+
The dark panel is clipped to the shared 2xl corner by BLOCK_SURFACE. */}
|
|
51
|
+
<LazyPrettyCode
|
|
52
|
+
data={block.code}
|
|
53
|
+
language={language}
|
|
54
|
+
isCompact={compact}
|
|
55
|
+
maxLines={maxLines}
|
|
56
|
+
onExpand={() => setOpen(true)}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
{/* Full code, uncapped, in a large dialog (map/image-block pattern).
|
|
60
|
+
ONE header (language + copy + close) over a dark panel; the body
|
|
61
|
+
renders PrettyCode in `plain` (chrome-less) so there's no second
|
|
62
|
+
header to collide with — the dialog owns the chrome and scroll. */}
|
|
63
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
64
|
+
<DialogContent
|
|
65
|
+
className="flex h-[85vh] w-[90vw] max-w-[1100px] flex-col overflow-hidden border-0 p-0 sm:rounded-xl"
|
|
66
|
+
// Fixed GitHub-dark surface (matches PrettyCode's own panel) — the
|
|
67
|
+
// code theme is always dark regardless of the app theme, so a
|
|
68
|
+
// theme-driven `bg-code` (light in light mode) would be unreadable.
|
|
69
|
+
style={{ backgroundColor: '#0d1117' }}
|
|
70
|
+
// We render our own close inside the single header, so suppress the
|
|
71
|
+
// default X (it would collide with the header buttons).
|
|
72
|
+
closeButton={false}
|
|
73
|
+
>
|
|
74
|
+
<DialogTitle className="sr-only">{block.caption ?? language ?? 'Code'}</DialogTitle>
|
|
75
|
+
{open && (
|
|
76
|
+
<>
|
|
77
|
+
{/* Roomy dialog header: language left; actions right in the
|
|
78
|
+
conventional order — primary "Copy" then "Close" (X) at the
|
|
79
|
+
far edge. Taller + larger than the inline header to suit the
|
|
80
|
+
big surface. */}
|
|
81
|
+
<div className="flex h-11 shrink-0 items-center justify-between gap-2 border-b border-white/[0.08] px-4">
|
|
82
|
+
<span className="min-w-0 truncate font-mono text-sm lowercase tracking-wide text-white/55">
|
|
83
|
+
{language}
|
|
84
|
+
</span>
|
|
85
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
86
|
+
<button type="button" onClick={copyAll} aria-label="Copy code" className={DIALOG_BTN}>
|
|
87
|
+
{copied ? <Check className="size-3.5" aria-hidden /> : <Copy className="size-3.5" aria-hidden />}
|
|
88
|
+
<span>{copied ? 'Copied' : 'Copy'}</span>
|
|
89
|
+
</button>
|
|
90
|
+
<DialogClose
|
|
91
|
+
aria-label="Close"
|
|
92
|
+
className={cn(DIALOG_BTN, 'w-8 px-0')}
|
|
93
|
+
>
|
|
94
|
+
<X className="size-4" aria-hidden />
|
|
95
|
+
</DialogClose>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
99
|
+
<LazyPrettyCode data={block.code} language={language} variant="plain" />
|
|
100
|
+
</div>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</DialogContent>
|
|
104
|
+
</Dialog>
|
|
105
|
+
</>
|
|
18
106
|
);
|
|
19
107
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/** Diff block — thin chat wrapper around the lazy `DiffViewer`. */
|
|
4
|
+
|
|
5
|
+
import { LazyDiffViewer } from '../../../../dev/code/DiffViewer/lazy';
|
|
6
|
+
import type { DiffBlock } from '../../../types/block';
|
|
7
|
+
import type { BlockRendererProps } from './types';
|
|
8
|
+
|
|
9
|
+
export default function DiffBlockRenderer({ block, ctx }: BlockRendererProps<DiffBlock>) {
|
|
10
|
+
// Compact chat is narrow → default to single-column `unified`; full
|
|
11
|
+
// bubbles have room for `split`. A pinned `layout` always wins.
|
|
12
|
+
const layout = block.layout ?? (ctx.appearance === 'compact' ? 'unified' : 'split');
|
|
13
|
+
const common = {
|
|
14
|
+
layout,
|
|
15
|
+
language: block.language,
|
|
16
|
+
// `diff` is a "bare" block (no clipping card) because the viewer draws
|
|
17
|
+
// its own bordered panel + a floating copy button that a clip would
|
|
18
|
+
// crop. So it owns its frame — re-rounded to the shared 2xl radius.
|
|
19
|
+
// `!`-utilities win over the viewer's template-string `rounded-xl`
|
|
20
|
+
// regardless of stylesheet order.
|
|
21
|
+
className: '!rounded-2xl',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return block.patch !== undefined ? (
|
|
25
|
+
<LazyDiffViewer {...common} patch={block.patch} />
|
|
26
|
+
) : (
|
|
27
|
+
<LazyDiffViewer {...common} oldCode={block.oldCode ?? ''} newCode={block.newCode ?? ''} />
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Gallery block.
|
|
5
|
+
*
|
|
6
|
+
* - Compact chat (the common case) → a lightweight Telegram-style
|
|
7
|
+
* `ChatImageAlbum` collage: deterministic 1–N layouts, lazy thumbnails,
|
|
8
|
+
* tap-to-lightbox. No carousel chrome, no extra deps.
|
|
9
|
+
* - Full appearance → the richer `Gallery` (carousel + thumbnail strip +
|
|
10
|
+
* lightbox) for a roomy, full-page chat.
|
|
11
|
+
*
|
|
12
|
+
* Frame (radius / border / clip) comes from the shared `BLOCK_SURFACE`.
|
|
13
|
+
*/
|
|
4
14
|
|
|
5
15
|
import { Gallery } from '../../../../media/Gallery';
|
|
6
16
|
import type { GalleryMediaItem } from '../../../../media/Gallery';
|
|
7
17
|
import type { GalleryBlock } from '../../../types/block';
|
|
8
18
|
import type { BlockRendererProps } from './types';
|
|
19
|
+
import { ChatImageAlbum } from './ChatImageAlbum';
|
|
9
20
|
|
|
10
21
|
export default function GalleryBlockRenderer({ block, ctx }: BlockRendererProps<GalleryBlock>) {
|
|
22
|
+
if (ctx.appearance === 'compact') {
|
|
23
|
+
return <ChatImageAlbum images={block.items} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
const images: GalleryMediaItem[] = block.items.map((item) => ({
|
|
12
27
|
id: item.id,
|
|
13
28
|
src: item.src,
|
|
14
29
|
thumbnail: item.thumbnail,
|
|
15
30
|
alt: item.alt,
|
|
16
31
|
}));
|
|
17
|
-
return
|
|
18
|
-
<div className="overflow-hidden rounded-xl border border-border">
|
|
19
|
-
<Gallery
|
|
20
|
-
images={images}
|
|
21
|
-
showThumbnails={ctx.appearance !== 'compact'}
|
|
22
|
-
enableLightbox
|
|
23
|
-
/>
|
|
24
|
-
</div>
|
|
25
|
-
);
|
|
32
|
+
return <Gallery images={images} showThumbnails enableLightbox />;
|
|
26
33
|
}
|
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Image block — a thumbnail that opens a fullscreen zoom/pan lightbox on
|
|
5
|
+
* click (Telegram / WhatsApp style). The outer frame (radius / border /
|
|
6
|
+
* shadow) is owned by the shared `BLOCK_SURFACE` in MessageBlocks; here we
|
|
7
|
+
* only render the image and the lightbox dialog.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
|
|
12
|
+
import { Dialog, DialogContent, DialogTitle } from '@djangocfg/ui-core/components';
|
|
4
13
|
|
|
5
14
|
import { LazyImageViewer } from '../../../../media/ImageViewer/lazy';
|
|
6
15
|
import type { ImageBlock } from '../../../types/block';
|
|
7
16
|
import type { BlockRendererProps } from './types';
|
|
8
17
|
|
|
9
18
|
export default function ImageBlockRenderer({ block }: BlockRendererProps<ImageBlock>) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<LazyImageViewer
|
|
14
|
-
images={[{ file: { name: block.alt ?? 'image', path: block.src }, src: block.src }]}
|
|
15
|
-
/>
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
const alt = block.alt ?? 'image';
|
|
21
|
+
|
|
19
22
|
return (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
<>
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={() => setOpen(true)}
|
|
27
|
+
aria-label={`Open image${block.alt ? `: ${block.alt}` : ''}`}
|
|
28
|
+
className="block w-full cursor-zoom-in transition-opacity hover:opacity-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
29
|
+
>
|
|
30
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
31
|
+
<img src={block.src} alt={alt} className="max-h-96 w-full object-cover" />
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
35
|
+
<DialogContent className="h-[90vh] w-[90vw] max-w-[1400px] overflow-hidden p-0 sm:rounded-xl">
|
|
36
|
+
<DialogTitle className="sr-only">{alt}</DialogTitle>
|
|
37
|
+
{open && (
|
|
38
|
+
<LazyImageViewer
|
|
39
|
+
images={[{ file: { name: alt, path: block.src }, src: block.src }]}
|
|
40
|
+
inDialog
|
|
41
|
+
autoFocus
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</DialogContent>
|
|
45
|
+
</Dialog>
|
|
46
|
+
</>
|
|
26
47
|
);
|
|
27
48
|
}
|
|
@@ -12,12 +12,17 @@ export default function JsonBlockRenderer({ block, ctx }: BlockRendererProps<Jso
|
|
|
12
12
|
// hover-revealed toolbar + card.
|
|
13
13
|
const mode = block.mode ?? (ctx.appearance === 'compact' ? 'compact' : 'full');
|
|
14
14
|
const compact = mode !== 'full';
|
|
15
|
+
// The outer card (radius / border / shadow) comes from the shared
|
|
16
|
+
// `BLOCK_SURFACE`; the tree itself is unbordered with a little padding so
|
|
17
|
+
// it doesn't sit flush against the frame.
|
|
15
18
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
<div className="p-2">
|
|
20
|
+
<LazyJsonTree
|
|
21
|
+
data={block.data}
|
|
22
|
+
toolbar={compact ? 'never' : 'auto'}
|
|
23
|
+
bordered={false}
|
|
24
|
+
defaultExpandedDepth={compact ? 1 : 2}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
22
27
|
);
|
|
23
28
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Link block — renders a `LinkPreviewCard`. If the block carries resolved
|
|
5
|
+
* `data` it shows immediately; otherwise it pulls the host resolver from
|
|
6
|
+
* chat context (`config.linkPreview.resolve`) and unfurls lazily.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LinkPreviewCard } from '../../../../../common/link-preview';
|
|
10
|
+
import { useChatContextOptional } from '../../../context';
|
|
11
|
+
import type { LinkBlock } from '../../../types/block';
|
|
12
|
+
import type { BlockRendererProps } from './types';
|
|
13
|
+
|
|
14
|
+
export default function LinkBlockRenderer({ block, ctx }: BlockRendererProps<LinkBlock>) {
|
|
15
|
+
const chat = useChatContextOptional();
|
|
16
|
+
const resolver = chat?.config.linkPreview?.resolve;
|
|
17
|
+
|
|
18
|
+
// No data and no resolver → nothing to render (card returns null too).
|
|
19
|
+
if (!block.data && !resolver) return null;
|
|
20
|
+
|
|
21
|
+
// The link payload (`url` + optional pre-resolved `data`) is validated
|
|
22
|
+
// centrally at the registry dispatch point (MessageBlocks) before this
|
|
23
|
+
// renderer runs, so we can trust it here.
|
|
24
|
+
return (
|
|
25
|
+
<LinkPreviewCard
|
|
26
|
+
url={block.url}
|
|
27
|
+
data={block.data}
|
|
28
|
+
resolver={resolver}
|
|
29
|
+
appearance={ctx.appearance}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|