@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.
- 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 +88 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- 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 +267 -42
- 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 +26 -2
- 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/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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<UrlChip>` — a compact, clickable "URL chip" (Chrome-omnibox style):
|
|
5
|
+
* a favicon + the domain with a middle-ellipsised path
|
|
6
|
+
* (`github.com/wailsapp/…/README.md`), monospace, with the full href on
|
|
7
|
+
* hover. Opens in a new tab.
|
|
8
|
+
*
|
|
9
|
+
* This is the SURFACE-AGNOSTIC, React counterpart of the TipTap composer's
|
|
10
|
+
* `editorChip` (`kind: 'url'`, see `forms/MarkdownEditor/chip/ChipNode.ts`).
|
|
11
|
+
* It matches that chip's look pixel-for-pixel using semantic tokens, so a
|
|
12
|
+
* URL reads identically whether it sits in the composer (being typed) or in
|
|
13
|
+
* a rendered chat bubble (`MarkdownMessage`'s `urlChipRule`).
|
|
14
|
+
*
|
|
15
|
+
* Presentational + self-contained: it imports only the pure URL detector
|
|
16
|
+
* helpers (`url/detect`) — no TipTap / ProseMirror — so it can render a
|
|
17
|
+
* known URL anywhere (chat messages, link lists, etc).
|
|
18
|
+
*
|
|
19
|
+
* Favicon degradation: the favicon `<img>` falls back to a lucide `Globe`
|
|
20
|
+
* glyph on `onError` (offline / blocked / unknown host) so the chip never
|
|
21
|
+
* shows a broken-image marker — mirroring the composer chip's behaviour.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useState } from 'react';
|
|
25
|
+
import { Globe } from 'lucide-react';
|
|
26
|
+
|
|
27
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
28
|
+
import {
|
|
29
|
+
splitUrl,
|
|
30
|
+
truncateUrlLabel,
|
|
31
|
+
faviconUrl,
|
|
32
|
+
} from '../../tools/forms/MarkdownEditor/url/detect';
|
|
33
|
+
|
|
34
|
+
export interface UrlChipProps {
|
|
35
|
+
/** The full, navigable href (`https://…`). */
|
|
36
|
+
href: string;
|
|
37
|
+
/**
|
|
38
|
+
* Approximate max characters for the truncated label. The domain is
|
|
39
|
+
* always preserved; only the path's middle collapses. Default 40.
|
|
40
|
+
*/
|
|
41
|
+
maxChars?: number;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render a URL as an inline, clickable chip. Favicon → globe glyph on
|
|
47
|
+
* favicon load error.
|
|
48
|
+
*/
|
|
49
|
+
export function UrlChip({ href, maxChars = 40, className }: UrlChipProps) {
|
|
50
|
+
const { host } = splitUrl(href);
|
|
51
|
+
const favicon = faviconUrl(host);
|
|
52
|
+
const label = truncateUrlLabel(href, { maxChars });
|
|
53
|
+
|
|
54
|
+
// Favicon load failure → swap to the globe glyph. Also covers the empty-
|
|
55
|
+
// favicon case (unparseable host → faviconUrl returns '').
|
|
56
|
+
const [imgFailed, setImgFailed] = useState(false);
|
|
57
|
+
const showGlobe = imgFailed || !favicon;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<a
|
|
61
|
+
href={href}
|
|
62
|
+
target="_blank"
|
|
63
|
+
rel="noopener noreferrer"
|
|
64
|
+
title={href}
|
|
65
|
+
className={cn(
|
|
66
|
+
// Inline pill matching the composer's `editor-chip--url`: muted
|
|
67
|
+
// surface + border, monospace, semantic tokens only. `max-w` +
|
|
68
|
+
// `truncate` (on the label) is the CSS safety net under the
|
|
69
|
+
// precomputed middle-ellipsis label. No raw blue link styling —
|
|
70
|
+
// the primary accent only lights up the border on hover + the
|
|
71
|
+
// globe glyph.
|
|
72
|
+
'inline-flex max-w-[22rem] items-center gap-1 rounded align-baseline',
|
|
73
|
+
'border border-border bg-muted px-1.5 py-0.5',
|
|
74
|
+
'font-mono text-[0.8125em] leading-tight text-foreground no-underline',
|
|
75
|
+
'cursor-pointer transition-colors hover:border-primary',
|
|
76
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<span
|
|
81
|
+
className={cn(
|
|
82
|
+
'flex size-3.5 shrink-0 items-center justify-center',
|
|
83
|
+
showGlobe && 'text-primary',
|
|
84
|
+
)}
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
>
|
|
87
|
+
{showGlobe ? (
|
|
88
|
+
<Globe className="size-3.5" />
|
|
89
|
+
) : (
|
|
90
|
+
<img
|
|
91
|
+
src={favicon}
|
|
92
|
+
alt=""
|
|
93
|
+
width={14}
|
|
94
|
+
height={14}
|
|
95
|
+
className="size-3.5 rounded-[2px]"
|
|
96
|
+
onError={() => setImgFailed(true)}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
<span className="truncate">{label}</span>
|
|
101
|
+
</a>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default UrlChip;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface-agnostic presentational "chips" — compact inline pills that
|
|
3
|
+
* render a URL or a file path with a favicon/icon + middle-ellipsis label.
|
|
4
|
+
*
|
|
5
|
+
* These match the TipTap composer's `editorChip` look (see
|
|
6
|
+
* `forms/MarkdownEditor/chip/ChipNode.ts`) but are plain React, so the same
|
|
7
|
+
* visual is reusable across every surface (composer, chat bubble, lists).
|
|
8
|
+
*
|
|
9
|
+
* - `UrlChip` — clickable URL chip (favicon → globe fallback).
|
|
10
|
+
* - `FilePathChip` — local file-path chip (re-exported from the editor's
|
|
11
|
+
* standalone path chip; it's already presentational/dependency-light, so
|
|
12
|
+
* we wrap rather than duplicate).
|
|
13
|
+
*/
|
|
14
|
+
export { UrlChip, default as UrlChipDefault } from './UrlChip';
|
|
15
|
+
export type { UrlChipProps } from './UrlChip';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
FilePathChip,
|
|
19
|
+
type FilePathChipProps,
|
|
20
|
+
} from '../../tools/forms/MarkdownEditor/filePath/FilePathChip';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LinkPreviewCard — a presentational URL-unfurl card (favicon + og:image +
|
|
5
|
+
* title / description / site name), Notion / Telegram style.
|
|
6
|
+
*
|
|
7
|
+
* It NEVER fetches the page itself — the browser can't read cross-origin
|
|
8
|
+
* meta tags (CORS). It either renders pre-resolved `data`, or calls the
|
|
9
|
+
* host-provided `resolver(url)` (a Wails/Go method, a Next.js API route)
|
|
10
|
+
* once on mount, showing a skeleton until it resolves. A `null` result
|
|
11
|
+
* (or a thrown resolver) renders nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useRef, useState } from 'react';
|
|
15
|
+
|
|
16
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
|
+
|
|
18
|
+
import type { LinkPreviewData, ResolveLinkPreview } from './types';
|
|
19
|
+
|
|
20
|
+
export interface LinkPreviewCardProps {
|
|
21
|
+
url: string;
|
|
22
|
+
/** Pre-resolved metadata. When present, no resolver call is made. */
|
|
23
|
+
data?: LinkPreviewData;
|
|
24
|
+
/** Host resolver used when `data` is absent. */
|
|
25
|
+
resolver?: ResolveLinkPreview;
|
|
26
|
+
/** Layout — `compact` (default) is a tight horizontal card; `full` is a
|
|
27
|
+
* roomier card with a larger cover image. */
|
|
28
|
+
appearance?: 'compact' | 'full';
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Hostname for the fallback site label / favicon. */
|
|
33
|
+
function hostnameOf(url: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
36
|
+
} catch {
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type LoadState =
|
|
42
|
+
| { status: 'ready'; data: LinkPreviewData }
|
|
43
|
+
| { status: 'loading' }
|
|
44
|
+
| { status: 'empty' };
|
|
45
|
+
|
|
46
|
+
export function LinkPreviewCard({
|
|
47
|
+
url,
|
|
48
|
+
data,
|
|
49
|
+
resolver,
|
|
50
|
+
appearance = 'compact',
|
|
51
|
+
className,
|
|
52
|
+
}: LinkPreviewCardProps) {
|
|
53
|
+
// If data is supplied we're immediately ready; else loading (resolver) or
|
|
54
|
+
// empty (nothing to do).
|
|
55
|
+
const [state, setState] = useState<LoadState>(() =>
|
|
56
|
+
data ? { status: 'ready', data } : resolver ? { status: 'loading' } : { status: 'empty' },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Keep the latest resolver without re-running the effect on identity churn.
|
|
60
|
+
const resolverRef = useRef(resolver);
|
|
61
|
+
resolverRef.current = resolver;
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (data) {
|
|
65
|
+
setState({ status: 'ready', data });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const resolve = resolverRef.current;
|
|
69
|
+
if (!resolve) {
|
|
70
|
+
setState({ status: 'empty' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let alive = true;
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
setState({ status: 'loading' });
|
|
77
|
+
|
|
78
|
+
resolve(url, controller.signal)
|
|
79
|
+
.then((result) => {
|
|
80
|
+
if (!alive) return;
|
|
81
|
+
setState(result ? { status: 'ready', data: result } : { status: 'empty' });
|
|
82
|
+
})
|
|
83
|
+
.catch(() => {
|
|
84
|
+
if (alive) setState({ status: 'empty' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
alive = false;
|
|
89
|
+
controller.abort();
|
|
90
|
+
};
|
|
91
|
+
}, [url, data]);
|
|
92
|
+
|
|
93
|
+
if (state.status === 'empty') return null;
|
|
94
|
+
|
|
95
|
+
const host = hostnameOf(url);
|
|
96
|
+
|
|
97
|
+
// Shared frame — matches the calm media-card surface used elsewhere.
|
|
98
|
+
const frame = cn(
|
|
99
|
+
'block w-full overflow-hidden rounded-2xl border border-border bg-card shadow-sm',
|
|
100
|
+
'transition-colors hover:bg-accent/40',
|
|
101
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
102
|
+
className,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (state.status === 'loading') {
|
|
106
|
+
return (
|
|
107
|
+
<div className={cn(frame, 'pointer-events-none')} aria-busy="true">
|
|
108
|
+
<div className="flex items-center gap-3 p-3">
|
|
109
|
+
<div className="size-9 shrink-0 animate-pulse rounded-md bg-muted" />
|
|
110
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
111
|
+
<div className="h-3 w-2/3 animate-pulse rounded bg-muted" />
|
|
112
|
+
<div className="h-2.5 w-1/3 animate-pulse rounded bg-muted" />
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const meta = state.data;
|
|
120
|
+
const title = meta.title || host;
|
|
121
|
+
const siteName = meta.siteName || host;
|
|
122
|
+
const isFull = appearance === 'full';
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className={frame}>
|
|
126
|
+
{/* Cover image (full appearance, when present) — top banner. */}
|
|
127
|
+
{isFull && meta.image ? (
|
|
128
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
129
|
+
<img
|
|
130
|
+
src={meta.image}
|
|
131
|
+
alt=""
|
|
132
|
+
loading="lazy"
|
|
133
|
+
// Fixed banner height (not just max-h) so `object-cover` actually
|
|
134
|
+
// crops to fill — with only `max-h` the image keeps its aspect and
|
|
135
|
+
// letterboxes. `h-44` is a sensible cover banner.
|
|
136
|
+
className="h-44 w-full border-b border-border object-cover"
|
|
137
|
+
/>
|
|
138
|
+
) : null}
|
|
139
|
+
|
|
140
|
+
<div className="flex items-stretch gap-3 p-3">
|
|
141
|
+
<div className="min-w-0 flex-1">
|
|
142
|
+
{/* Site row — favicon + site name. */}
|
|
143
|
+
<div className="mb-1 flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
144
|
+
{meta.favicon ? (
|
|
145
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
146
|
+
<img
|
|
147
|
+
src={meta.favicon}
|
|
148
|
+
alt=""
|
|
149
|
+
loading="lazy"
|
|
150
|
+
className="size-4 shrink-0 rounded-sm"
|
|
151
|
+
/>
|
|
152
|
+
) : null}
|
|
153
|
+
<span className="truncate">{siteName}</span>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="line-clamp-2 text-sm font-medium text-foreground">{title}</div>
|
|
157
|
+
|
|
158
|
+
{meta.description ? (
|
|
159
|
+
<div className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
|
160
|
+
{meta.description}
|
|
161
|
+
</div>
|
|
162
|
+
) : null}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Compact thumbnail (compact appearance, when present) — side. */}
|
|
166
|
+
{!isFull && meta.image ? (
|
|
167
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
168
|
+
<img
|
|
169
|
+
src={meta.image}
|
|
170
|
+
alt=""
|
|
171
|
+
loading="lazy"
|
|
172
|
+
className="size-16 shrink-0 self-center rounded-lg object-cover"
|
|
173
|
+
/>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
</a>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface-agnostic link-preview (URL unfurl) primitives.
|
|
3
|
+
*
|
|
4
|
+
* `LinkPreviewCard` is a pure presentational card (favicon + og:image +
|
|
5
|
+
* title / description / site name) that NEVER fetches the page — it renders
|
|
6
|
+
* pre-resolved `data` or calls a host-provided `resolver(url)`. It is shared
|
|
7
|
+
* across every surface: the chat `LinkBlock` and the NotionEditor `bookmark`
|
|
8
|
+
* node both import it and supply their own resolver. No chat coupling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { LinkPreviewCard, type LinkPreviewCardProps } from './LinkPreviewCard';
|
|
12
|
+
export type { LinkPreviewData, ResolveLinkPreview } from './types';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link-preview (URL unfurl) types — the data contract between the host's
|
|
3
|
+
* metadata fetcher and the presentational card.
|
|
4
|
+
*
|
|
5
|
+
* The browser CANNOT fetch arbitrary URLs to scrape OpenGraph/meta tags
|
|
6
|
+
* (CORS blocks cross-origin HTML reads), so resolution is ALWAYS a host
|
|
7
|
+
* concern: a server route (Next.js) or a native backend (Wails/Go). The
|
|
8
|
+
* UI never fetches — it receives already-resolved metadata, or a resolver
|
|
9
|
+
* callback the host implements.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolved metadata for one URL — the unfurl result. All fields except
|
|
14
|
+
* `url` are optional: a card renders from whatever subset is available
|
|
15
|
+
* (favicon + title is enough; a bare `url` falls back to the hostname).
|
|
16
|
+
* Plain JSON so it survives history persistence + SSE transport.
|
|
17
|
+
*/
|
|
18
|
+
export interface LinkPreviewData {
|
|
19
|
+
/** The canonical/resolved URL the card links to. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** og:title → twitter:title → <title>. */
|
|
22
|
+
title?: string;
|
|
23
|
+
/** og:description → twitter:description → <meta name=description>. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** og:image → twitter:image (absolute URL; host resolves relative ones). */
|
|
26
|
+
image?: string;
|
|
27
|
+
/** Favicon URL (host resolves <link rel=icon> / /favicon.ico). */
|
|
28
|
+
favicon?: string;
|
|
29
|
+
/** og:site_name → hostname. */
|
|
30
|
+
siteName?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Host-provided resolver. Given a URL, returns its preview metadata, or
|
|
35
|
+
* `null` when nothing useful could be fetched (so the card is skipped).
|
|
36
|
+
* Implementations:
|
|
37
|
+
* - Wails/Go desktop → a bound `FetchLinkMetadata(url)` service method.
|
|
38
|
+
* - Next.js web → a `/api/unfurl?url=` route.
|
|
39
|
+
* The host owns caching, SSRF protection, timeouts and bot User-Agents.
|
|
40
|
+
*/
|
|
41
|
+
export type ResolveLinkPreview = (
|
|
42
|
+
url: string,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
) => Promise<LinkPreviewData | null>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TipTap helpers — reusable across every NotionEditor block NodeView.
|
|
3
|
+
*
|
|
4
|
+
* `useNodeAttrs` is the write-back seam an interactive NodeView uses to push UI
|
|
5
|
+
* changes (drag a pin, switch a basemap) back into the document's node attrs,
|
|
6
|
+
* so the change survives the markdown round-trip.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { useNodeAttrs } from './useNodeAttrs';
|
|
10
|
+
export type { UseNodeAttrsResult } from './useNodeAttrs';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `useNodeAttrs` — the single write-back seam every interactive block NodeView
|
|
5
|
+
* uses to persist UI changes back into the document (→ TipTap node attrs →
|
|
6
|
+
* markdown round-trip).
|
|
7
|
+
*
|
|
8
|
+
* A NodeView is normally READ-ONLY: it renders `node.attrs` but has no path to
|
|
9
|
+
* push a user action (drag a pin, toggle a basemap) back into the node. This
|
|
10
|
+
* hook wraps TipTap's `updateAttributes` so an interactive component can do:
|
|
11
|
+
*
|
|
12
|
+
* const { attrs, patch } = useNodeAttrs<MapBlockPayload>(props);
|
|
13
|
+
* // …on drag-end:
|
|
14
|
+
* patch({ markers: nextMarkers });
|
|
15
|
+
*
|
|
16
|
+
* and the new attrs land in the document — `getMarkdown()` then serialises the
|
|
17
|
+
* updated payload.
|
|
18
|
+
*
|
|
19
|
+
* ## API
|
|
20
|
+
*
|
|
21
|
+
* - `attrs` — `props.node.attrs` typed as `T`. The current committed payload.
|
|
22
|
+
* - `patch(next)` — commit a partial attr update IMMEDIATELY. `updateAttributes`
|
|
23
|
+
* is a single ProseMirror transaction, so this is cheap; use it for DISCRETE
|
|
24
|
+
* edits (drag-END, add/remove pin, basemap pick) — which is the recommended
|
|
25
|
+
* design: the caller fires `patch` only when a value is final, never per
|
|
26
|
+
* drag-frame, so no transaction spam.
|
|
27
|
+
* - `patchDebounced(next, ms?)` — coalesce HIGH-FREQUENCY updates (a live edit
|
|
28
|
+
* firing many times: text input, a continuous slider). Rapid calls collapse
|
|
29
|
+
* into one trailing transaction after `ms` of quiet (default 250ms). Pending
|
|
30
|
+
* patches are MERGED so partial fields accumulate. Auto-flushes on unmount so
|
|
31
|
+
* the last edit is never lost.
|
|
32
|
+
* - `flush()` — commit any pending debounced patch immediately (e.g. on blur).
|
|
33
|
+
*
|
|
34
|
+
* Pure + reusable: no editor-specific coupling, only the `NodeViewProps`
|
|
35
|
+
* contract (`node`, `updateAttributes`). Node attrs stay plain-JSON
|
|
36
|
+
* serializable — pass only JSON-safe values through `patch`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
40
|
+
import type { NodeViewProps } from '@tiptap/react';
|
|
41
|
+
|
|
42
|
+
/** Default debounce window (ms) for `patchDebounced`. */
|
|
43
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
44
|
+
|
|
45
|
+
export interface UseNodeAttrsResult<T extends Record<string, unknown>> {
|
|
46
|
+
/** The node's current committed attrs, typed as `T`. */
|
|
47
|
+
attrs: T;
|
|
48
|
+
/** Commit a partial attr update immediately (one transaction). */
|
|
49
|
+
patch: (next: Partial<T>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Commit a partial attr update on a trailing debounce; rapid calls merge
|
|
52
|
+
* into a single transaction. For live/high-frequency edits.
|
|
53
|
+
*/
|
|
54
|
+
patchDebounced: (next: Partial<T>, ms?: number) => void;
|
|
55
|
+
/** Flush any pending debounced patch now (e.g. on blur). */
|
|
56
|
+
flush: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Two-way binding between a NodeView's UI and its node attrs.
|
|
61
|
+
*
|
|
62
|
+
* @param props The `NodeViewProps` TipTap hands the React NodeView component.
|
|
63
|
+
*/
|
|
64
|
+
export function useNodeAttrs<T extends Record<string, unknown>>(
|
|
65
|
+
props: NodeViewProps,
|
|
66
|
+
): UseNodeAttrsResult<T> {
|
|
67
|
+
const { node, updateAttributes } = props;
|
|
68
|
+
const attrs = node.attrs as T;
|
|
69
|
+
|
|
70
|
+
// Keep `updateAttributes` reachable from the debounce timer without making
|
|
71
|
+
// the debounced callback re-create on every render (it can change identity).
|
|
72
|
+
const updateRef = useRef(updateAttributes);
|
|
73
|
+
updateRef.current = updateAttributes;
|
|
74
|
+
|
|
75
|
+
// Pending merged patch + its timer, held across renders.
|
|
76
|
+
const pendingRef = useRef<Partial<T> | null>(null);
|
|
77
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
78
|
+
|
|
79
|
+
const flush = useCallback(() => {
|
|
80
|
+
if (timerRef.current != null) {
|
|
81
|
+
clearTimeout(timerRef.current);
|
|
82
|
+
timerRef.current = null;
|
|
83
|
+
}
|
|
84
|
+
const pending = pendingRef.current;
|
|
85
|
+
if (pending) {
|
|
86
|
+
pendingRef.current = null;
|
|
87
|
+
updateRef.current(pending);
|
|
88
|
+
}
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const patch = useCallback(
|
|
92
|
+
(next: Partial<T>) => {
|
|
93
|
+
// A discrete edit supersedes any queued debounced patch: merge the
|
|
94
|
+
// pending fields in, then commit the lot as one transaction.
|
|
95
|
+
const pending = pendingRef.current;
|
|
96
|
+
if (timerRef.current != null) {
|
|
97
|
+
clearTimeout(timerRef.current);
|
|
98
|
+
timerRef.current = null;
|
|
99
|
+
}
|
|
100
|
+
pendingRef.current = null;
|
|
101
|
+
updateRef.current(pending ? { ...pending, ...next } : next);
|
|
102
|
+
},
|
|
103
|
+
[],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const patchDebounced = useCallback(
|
|
107
|
+
(next: Partial<T>, ms: number = DEFAULT_DEBOUNCE_MS) => {
|
|
108
|
+
// Merge so partial fields accumulate across rapid calls.
|
|
109
|
+
pendingRef.current = { ...(pendingRef.current ?? {}), ...next };
|
|
110
|
+
if (timerRef.current != null) clearTimeout(timerRef.current);
|
|
111
|
+
timerRef.current = setTimeout(() => {
|
|
112
|
+
timerRef.current = null;
|
|
113
|
+
const pending = pendingRef.current;
|
|
114
|
+
pendingRef.current = null;
|
|
115
|
+
if (pending) updateRef.current(pending);
|
|
116
|
+
}, ms);
|
|
117
|
+
},
|
|
118
|
+
[],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Never drop the last edit: flush on unmount.
|
|
122
|
+
useEffect(() => () => flush(), [flush]);
|
|
123
|
+
|
|
124
|
+
return useMemo(
|
|
125
|
+
() => ({ attrs, patch, patchDebounced, flush }),
|
|
126
|
+
[attrs, patch, patchDebounced, flush],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -35,7 +35,7 @@ export function PageSnapshotChip({ className }: PageSnapshotChipProps) {
|
|
|
35
35
|
className={cn(
|
|
36
36
|
'inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs',
|
|
37
37
|
isStale
|
|
38
|
-
? 'border-
|
|
38
|
+
? 'border-warning-border/40 text-warning'
|
|
39
39
|
: 'border-border text-muted-foreground',
|
|
40
40
|
className,
|
|
41
41
|
)}
|
package/src/tools/chat/README.md
CHANGED
|
@@ -48,7 +48,8 @@ export function MyChat() {
|
|
|
48
48
|
- **Facebook-style unread notifier.** `useChatUnreadNotifier()` prefixes `document.title` with `(N)` and paints a small red dot over the favicon while the tab is in background. Sober by default — the favicon is the attention signal, the title stays readable. Title rotation is available as opt-in (`title: { mode: 'rotate' }`) for hosts without a favicon. Cross-tab coordinated via `useActiveTab` from `@djangocfg/ui-core`: only the elected leader tab mutates title/favicon, the count is broadcast so every tab's FAB badge stays in sync. Pluggable `ChatNotifier` interface lets Wails / Electron hosts route to a native dock badge instead.
|
|
49
49
|
- **Personas.** `config.user` + `config.assistant` for default identity; `message.sender` for per-message overrides.
|
|
50
50
|
- **Audio.** Pass `audio: ChatAudioConfig` (sounds map) to `<ChatLauncher>` — the hook runs internally. Built on `@djangocfg/ui-core/hooks/useNotificationSounds`: Safari unlock, persisted mute, per-event toggles, reduced-motion respect. Slack/Linear-style per-event volume scale (error ≈ 0.25, mention ≈ 1.0). **Built-in sounds bundled as base64 data URLs** in the lazy chat chunk (~136KB) — `audio={{}}` just works. `silenced` + `onSoundEvent` for native hosts (cmdop_go / Tauri).
|
|
51
|
-
- **Rich attachments.** `AttachmentsGrid` for thumbnails, `AttachmentsList` for custom renderers; `onAttachmentOpen` for lightbox.
|
|
51
|
+
- **Rich attachments.** `AttachmentsGrid` for thumbnails, `AttachmentsList` for custom renderers; `onAttachmentOpen` for lightbox. Image tiles open a built-in fullscreen `ImageViewer` lightbox (with prev/next across the message's images) **by default** — no host wiring — and a host `onAttachmentOpen` still overrides.
|
|
52
|
+
- **URL chips in bubbles.** `config.linkChips` (off by default) renders bare `http(s)` URLs in the message markdown bubble as compact favicon chips (`github.com/…/README.md`), matching the composer's URL chip. `[labeled](url)` links stay normal styled links; `www.` hosts are promoted and chipped.
|
|
52
53
|
- **Unified attach pipeline.** Paperclip, `+` menu, drag-drop and ⌘V/Ctrl+V all funnel through one validated path (`useComposerAttach`); set `showAttachmentButton` / `attach` and the composer ships its own picker. Optional `uploadFn` is the only web/Wails seam.
|
|
53
54
|
- **Paste-as-chunk.** A long plain-text paste (over `attach.pasteTextThreshold`, default 2000 chars — ChatGPT/Claude behaviour) becomes a `type:'text'` "Pasted text" attachment chip instead of flooding the textarea. The chip opens a read-only `PastedTextDialog` preview (rendered via `MarkdownMessage`); the exact payload rides in `ChatAttachment.text` to `onSubmit(content, attachments)`. Toggle with `attach.pasteTextAsChunk`.
|
|
54
55
|
- **Tool-payload dispatcher.** `dispatchToolPayload(matchers, fallback)` — pluggable predicates render `<LazyJsonTree>` / `<LazyMap>` / etc.
|
|
@@ -537,12 +538,36 @@ function MyError() {
|
|
|
537
538
|
}
|
|
538
539
|
```
|
|
539
540
|
|
|
541
|
+
## URL chips in bubbles — `config.linkChips`
|
|
542
|
+
|
|
543
|
+
Off by default. When `config.linkChips` is `true`, bare web URLs inside a
|
|
544
|
+
message's **markdown bubble** render as compact `<UrlChip>`s (favicon + domain
|
|
545
|
+
+ middle-ellipsis path, e.g. `github.com/wailsapp/…/README.md`) instead of a
|
|
546
|
+
plain blue link — matching the chip the composer shows while you type a URL.
|
|
547
|
+
|
|
548
|
+
```tsx
|
|
549
|
+
<ChatRoot transport={transport} config={{ linkChips: true }} />
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
It plugs in via `MessageBubble`: when on, it passes `linkRules={[urlChipRule]}`
|
|
553
|
+
to `MarkdownMessage` (`urlChipRule` is the built-in `LinkRule` from
|
|
554
|
+
`dev/code/MarkdownMessage`). The rule is appended **after** any host-supplied
|
|
555
|
+
`linkRules`, so host rules still win. Scope and behaviour:
|
|
556
|
+
|
|
557
|
+
- Only **bare** `http(s)` autolinks are chipped (link text === href).
|
|
558
|
+
- `[labeled](url)` links keep their author-chosen text as a normal styled link.
|
|
559
|
+
- `www.` hosts are promoted to `https://www.…` (so GFM autolinks them) and
|
|
560
|
+
then chipped — matching the composer's autolink whitelist.
|
|
561
|
+
|
|
562
|
+
The chip is the same surface-agnostic `UrlChip` the composer's chip mirrors —
|
|
563
|
+
see [`common/chips`](../../common/chips/README.md).
|
|
564
|
+
|
|
540
565
|
## Message blocks
|
|
541
566
|
|
|
542
567
|
A message can carry typed, serializable rich content beyond its markdown
|
|
543
568
|
`content`: an optional `blocks: MessageBlock[]`. `MessageBlock` is a
|
|
544
569
|
discriminated union on `kind` — `text`, `markdown`, `audio`, `video`,
|
|
545
|
-
`image`, `gallery`, `map`, `json`, `mermaid`, `code`,
|
|
570
|
+
`image`, `gallery`, `map`, `json`, `mermaid`, `code`, plus a
|
|
546
571
|
`custom` escape hatch. Payloads are plain JSON, so blocks survive history
|
|
547
572
|
persistence and SSE transport.
|
|
548
573
|
|
|
@@ -589,6 +614,55 @@ Exports (`@djangocfg/ui-tools/chat`): `MessageBlocks`,
|
|
|
589
614
|
`createBlockRegistry`, `BUILTIN_BLOCK_REGISTRY`, and the `MessageBlock` /
|
|
590
615
|
`BlockRegistry` / `BlockRenderer` / `BlockRenderContext` types.
|
|
591
616
|
|
|
617
|
+
### The `map` block
|
|
618
|
+
|
|
619
|
+
`MapBlock` wraps the lazy MapLibre renderer. Beyond `center` / `zoom` /
|
|
620
|
+
`routes` / `polygons`, all of its tuning is plain JSON so a map survives
|
|
621
|
+
history persistence and SSE transport:
|
|
622
|
+
|
|
623
|
+
- **`basemap?: string`** — a built-in style key (`'light'` / `'dark'` /
|
|
624
|
+
`'streets'` · `'liberty'` / `'bright'` / `'positron'`) or a full
|
|
625
|
+
style-JSON URL. Omit for the default basemap.
|
|
626
|
+
- **`terrain?: boolean`** — 3D terrain + hillshade (free AWS Terrarium DEM,
|
|
627
|
+
no key).
|
|
628
|
+
- **`userLocation?: boolean`** — opt in to the toolbar "locate-me" chip
|
|
629
|
+
(default off). A plain boolean, **not** coordinates — the browser supplies
|
|
630
|
+
the live position only after the user grants permission; nothing is baked
|
|
631
|
+
into the block. Needs a secure context (HTTPS / localhost).
|
|
632
|
+
- **per-marker `icon?` / `color?`** — `icon` is an image URL rendered as a
|
|
633
|
+
rounded thumbnail pin; `color` is a CSS color for the default pin (ignored
|
|
634
|
+
when `icon` is set).
|
|
635
|
+
- **per-marker `card?`** — an info-card popup opened above the pin on click
|
|
636
|
+
(Google-Maps style), rendered by the map's default `MarkerCard`. Pure
|
|
637
|
+
JSON — strings/URLs only:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
card?: {
|
|
641
|
+
title: string;
|
|
642
|
+
description?: string;
|
|
643
|
+
image?: string;
|
|
644
|
+
badge?: string;
|
|
645
|
+
actions?: Array<{ label: string; href?: string }>; // href-only links
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
`actions` are `href`-only links (no `onClick` / `icon` — those can't cross
|
|
650
|
+
the wire). When a marker has no `card`, clicking it does nothing extra.
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
const map: MapBlock = {
|
|
654
|
+
kind: 'map', id: 'b3', center: { lat: 52.52, lng: 13.405 }, zoom: 12,
|
|
655
|
+
basemap: 'dark', terrain: true, userLocation: true,
|
|
656
|
+
markers: [{
|
|
657
|
+
id: 'm1', lat: 52.52, lng: 13.405, color: '#22c55e',
|
|
658
|
+
card: {
|
|
659
|
+
title: 'Berlin office', description: 'Mitte', badge: 'HQ',
|
|
660
|
+
actions: [{ label: 'Directions', href: 'https://maps.example/berlin' }],
|
|
661
|
+
},
|
|
662
|
+
}],
|
|
663
|
+
};
|
|
664
|
+
```
|
|
665
|
+
|
|
592
666
|
## Transport contract
|
|
593
667
|
|
|
594
668
|
A single I/O seam.
|
|
@@ -692,6 +766,18 @@ function — there is no separate `renderHeader` / `renderEmpty` prop and no
|
|
|
692
766
|
`<Composer>`; it receives `{ composer, config }` — the live composer hook
|
|
693
767
|
plus the same config object. See **[Slots inventory](#slots-inventory)** below.
|
|
694
768
|
|
|
769
|
+
### Image attachments — default lightbox
|
|
770
|
+
|
|
771
|
+
When you **don't** wire `messages.onAttachmentOpen` (or a tile's `onClick`),
|
|
772
|
+
image attachments are still clickable: each `AttachmentsGrid` / `AttachmentsList`
|
|
773
|
+
mounts an `ImageLightboxProvider` that renders a single shared `ImageViewer`
|
|
774
|
+
(`LazyImageViewer`) dialog for the group. Clicking an image opens a fullscreen
|
|
775
|
+
gallery with prev/next across **all** the image attachments of that message,
|
|
776
|
+
starting at the clicked index (non-image files are skipped). A host-supplied
|
|
777
|
+
`onAttachmentOpen` / `onClick` always wins (see `useImageTileClick` in
|
|
778
|
+
`messages/Attachments.tsx`), so existing lightbox wiring is unaffected. See the
|
|
779
|
+
[`ImageViewer` docs](../media/ImageViewer/README.md).
|
|
780
|
+
|
|
695
781
|
## Composer slot system
|
|
696
782
|
|
|
697
783
|
`<Composer>` is a single bordered input surface — textarea on top, an
|