@djangocfg/ui-tools 2.1.431 → 2.1.433
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.433",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -276,6 +276,11 @@
|
|
|
276
276
|
"import": "./src/tools/visual/charts/Sparkline/index.ts",
|
|
277
277
|
"require": "./src/tools/visual/charts/Sparkline/index.ts"
|
|
278
278
|
},
|
|
279
|
+
"./smooth-line": {
|
|
280
|
+
"types": "./src/tools/visual/charts/SmoothLine/index.ts",
|
|
281
|
+
"import": "./src/tools/visual/charts/SmoothLine/index.ts",
|
|
282
|
+
"require": "./src/tools/visual/charts/SmoothLine/index.ts"
|
|
283
|
+
},
|
|
279
284
|
"./rating": {
|
|
280
285
|
"types": "./src/tools/visual/indicators/Rating/index.ts",
|
|
281
286
|
"import": "./src/tools/visual/indicators/Rating/index.ts",
|
|
@@ -329,22 +334,30 @@
|
|
|
329
334
|
"test:watch": "vitest"
|
|
330
335
|
},
|
|
331
336
|
"peerDependencies": {
|
|
332
|
-
"@djangocfg/i18n": "^2.1.
|
|
333
|
-
"@djangocfg/ui-core": "^2.1.
|
|
337
|
+
"@djangocfg/i18n": "^2.1.433",
|
|
338
|
+
"@djangocfg/ui-core": "^2.1.433",
|
|
334
339
|
"consola": "^3.4.2",
|
|
335
340
|
"lodash-es": "^4.18.1",
|
|
336
341
|
"lucide-react": "^0.545.0",
|
|
337
342
|
"react": "^19.2.4",
|
|
338
343
|
"react-dom": "^19.2.4",
|
|
339
344
|
"tailwindcss": "^4.1.18",
|
|
345
|
+
"zod": "^4.3.6",
|
|
340
346
|
"zustand": "^5.0.0"
|
|
341
347
|
},
|
|
348
|
+
"peerDependenciesMeta": {
|
|
349
|
+
"zod": {
|
|
350
|
+
"optional": true
|
|
351
|
+
}
|
|
352
|
+
},
|
|
342
353
|
"dependencies": {
|
|
343
354
|
"@dnd-kit/core": "^6.3.1",
|
|
344
355
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
345
356
|
"@dnd-kit/sortable": "^10.0.0",
|
|
346
357
|
"@dnd-kit/utilities": "^3.2.2",
|
|
347
358
|
"@floating-ui/dom": "^1.7.4",
|
|
359
|
+
"@radix-ui/react-direction": "^1.1.1",
|
|
360
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
348
361
|
"@rjsf/core": "^6.1.2",
|
|
349
362
|
"@rjsf/utils": "^6.1.2",
|
|
350
363
|
"@rjsf/validator-ajv8": "^6.1.2",
|
|
@@ -366,22 +379,23 @@
|
|
|
366
379
|
"@tiptap/react": "^3.23.0",
|
|
367
380
|
"@tiptap/starter-kit": "^3.23.0",
|
|
368
381
|
"@tiptap/suggestion": "^3.23.0",
|
|
369
|
-
"
|
|
370
|
-
"
|
|
382
|
+
"@turf/turf": "^7.3.5",
|
|
383
|
+
"@types/qrcode": "^1.5.5",
|
|
371
384
|
"@wavesurfer/react": "^1.0.12",
|
|
372
|
-
"@radix-ui/react-slot": "^1.2.4",
|
|
373
|
-
"@radix-ui/react-direction": "^1.1.1",
|
|
374
385
|
"class-variance-authority": "^0.7.1",
|
|
375
386
|
"hls-video-element": "^1.5.11",
|
|
387
|
+
"json-edit-react": "^1.30.1",
|
|
388
|
+
"lowlight": "^3.3.0",
|
|
376
389
|
"maplibre-gl": "^4.7.1",
|
|
377
390
|
"media-chrome": "^4.19.0",
|
|
378
391
|
"media-icons": "next",
|
|
379
392
|
"mermaid": "^11.12.0",
|
|
380
393
|
"monaco-editor": "^0.55.1",
|
|
381
394
|
"openapi-sampler": "^1.7.2",
|
|
395
|
+
"pmtiles": "^4.4.1",
|
|
382
396
|
"prism-react-renderer": "^2.4.1",
|
|
383
397
|
"prismjs": "^1.30.0",
|
|
384
|
-
"
|
|
398
|
+
"qrcode": "^1.5.4",
|
|
385
399
|
"react-lottie-player": "^2.1.0",
|
|
386
400
|
"react-map-gl": "^8.1.0",
|
|
387
401
|
"react-markdown": "10.1.0",
|
|
@@ -394,8 +408,7 @@
|
|
|
394
408
|
"remark-emoji": "^5.0.2",
|
|
395
409
|
"remark-gfm": "4.0.1",
|
|
396
410
|
"remark-smartypants": "^3.0.2",
|
|
397
|
-
"
|
|
398
|
-
"@types/qrcode": "^1.5.5",
|
|
411
|
+
"tiptap-extension-global-drag-handle": "^0.1.18",
|
|
399
412
|
"vimeo-video-element": "^1.7.2",
|
|
400
413
|
"wavesurfer.js": "^7.12.1",
|
|
401
414
|
"youtube-video-element": "^1.9.0"
|
|
@@ -405,9 +418,9 @@
|
|
|
405
418
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
406
419
|
},
|
|
407
420
|
"devDependencies": {
|
|
408
|
-
"@djangocfg/i18n": "^2.1.
|
|
409
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
410
|
-
"@djangocfg/ui-core": "^2.1.
|
|
421
|
+
"@djangocfg/i18n": "^2.1.433",
|
|
422
|
+
"@djangocfg/typescript-config": "^2.1.433",
|
|
423
|
+
"@djangocfg/ui-core": "^2.1.433",
|
|
411
424
|
"@types/lodash-es": "^4.17.12",
|
|
412
425
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
413
426
|
"@types/node": "^25.2.3",
|
|
@@ -421,7 +434,8 @@
|
|
|
421
434
|
"tailwindcss": "^4.1.18",
|
|
422
435
|
"tsup": "^8.5.0",
|
|
423
436
|
"typescript": "^5.9.3",
|
|
424
|
-
"vitest": "^3.1.4"
|
|
437
|
+
"vitest": "^3.1.4",
|
|
438
|
+
"zod": "^4.3.6"
|
|
425
439
|
},
|
|
426
440
|
"publishConfig": {
|
|
427
441
|
"access": "public"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodePanelHeader — a slim, persistent header bar for the code panel
|
|
5
|
+
* (PrettyCode), GitHub / ChatGPT style: the detected language on the left,
|
|
6
|
+
* a copy button on the right. In flow (not an absolute hover overlay), so
|
|
7
|
+
* the copy button never floats over the code.
|
|
8
|
+
*
|
|
9
|
+
* Code panels are usually DARK (like the map basemap is always light), so
|
|
10
|
+
* the header defaults to FIXED on-dark colors — theme tokens
|
|
11
|
+
* (`text-foreground`, `border-border`) would render dark-on-dark / an
|
|
12
|
+
* over-bright divider on the dark panel. Pass `dark={false}` for a
|
|
13
|
+
* light-surfaced panel, which switches to muted semantic tokens.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useCallback, useState, type ReactNode } from 'react';
|
|
17
|
+
|
|
18
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
19
|
+
import { Check, Copy, Maximize2 } from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
export interface CodePanelHeaderProps {
|
|
22
|
+
/** Display label on the left (e.g. "typescript", "json", "bash"). */
|
|
23
|
+
language?: ReactNode;
|
|
24
|
+
/** Text copied when the copy button is pressed. Omit to hide copy. */
|
|
25
|
+
copyValue?: string;
|
|
26
|
+
/** a11y label for the copy button. */
|
|
27
|
+
copyLabel?: string;
|
|
28
|
+
/** When set, an expand (⤢) button is shown that calls this — used to open
|
|
29
|
+
* the panel full-screen when it's height-capped. */
|
|
30
|
+
onExpand?: () => void;
|
|
31
|
+
/** a11y / tooltip label for the expand button. */
|
|
32
|
+
expandLabel?: string;
|
|
33
|
+
/** Extra trailing actions, rendered left of the copy button. */
|
|
34
|
+
actions?: ReactNode;
|
|
35
|
+
/** Dark panel (default). `false` → light-surface muted tokens. */
|
|
36
|
+
dark?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CodePanelHeader({
|
|
41
|
+
language,
|
|
42
|
+
copyValue,
|
|
43
|
+
copyLabel = 'Copy',
|
|
44
|
+
onExpand,
|
|
45
|
+
expandLabel = 'Expand',
|
|
46
|
+
actions,
|
|
47
|
+
dark = true,
|
|
48
|
+
className,
|
|
49
|
+
}: CodePanelHeaderProps) {
|
|
50
|
+
const [copied, setCopied] = useState(false);
|
|
51
|
+
|
|
52
|
+
const onCopy = useCallback(() => {
|
|
53
|
+
if (copyValue == null) return;
|
|
54
|
+
void navigator.clipboard?.writeText(copyValue).then(() => {
|
|
55
|
+
setCopied(true);
|
|
56
|
+
setTimeout(() => setCopied(false), 1400);
|
|
57
|
+
});
|
|
58
|
+
}, [copyValue]);
|
|
59
|
+
|
|
60
|
+
// Fixed on-dark vs semantic light. The hairline is faint so it never
|
|
61
|
+
// reads as a heavy rule across the panel.
|
|
62
|
+
const dividerCls = dark ? 'border-white/[0.06]' : 'border-border/60';
|
|
63
|
+
const labelCls = dark ? 'text-white/40' : 'text-muted-foreground';
|
|
64
|
+
const btnCls = dark
|
|
65
|
+
? 'text-white/45 hover:bg-white/10 hover:text-white/80 focus-visible:ring-white/40'
|
|
66
|
+
: 'text-muted-foreground hover:bg-foreground/10 hover:text-foreground focus-visible:ring-ring';
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
'flex items-center justify-between gap-2 border-b px-3 py-1',
|
|
72
|
+
dividerCls,
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<span
|
|
77
|
+
className={cn(
|
|
78
|
+
'min-w-0 truncate font-mono text-[11px] lowercase tracking-wide',
|
|
79
|
+
labelCls,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{language}
|
|
83
|
+
</span>
|
|
84
|
+
|
|
85
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
86
|
+
{actions}
|
|
87
|
+
{onExpand ? (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={onExpand}
|
|
91
|
+
aria-label={expandLabel}
|
|
92
|
+
title={expandLabel}
|
|
93
|
+
className={cn(
|
|
94
|
+
'inline-flex size-5 items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1',
|
|
95
|
+
btnCls,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Maximize2 className="size-3" aria-hidden />
|
|
99
|
+
</button>
|
|
100
|
+
) : null}
|
|
101
|
+
{copyValue != null ? (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={onCopy}
|
|
105
|
+
aria-label={copyLabel}
|
|
106
|
+
className={cn(
|
|
107
|
+
'inline-flex size-5 items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1',
|
|
108
|
+
btnCls,
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{copied ? (
|
|
112
|
+
<Check className="size-3" aria-hidden />
|
|
113
|
+
) : (
|
|
114
|
+
<Copy className="size-3" aria-hidden />
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CodePanelHeader, type CodePanelHeaderProps } from './CodePanelHeader';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<BlockError>` — the safe presentational fallback a data-driven block renders
|
|
5
|
+
* when its payload fails schema validation (a corrupted ```map JSON, a
|
|
6
|
+
* malformed wire block). Informative, not scary: a compact muted card that says
|
|
7
|
+
* the block couldn't render, with an optional short reason. Never throws, so the
|
|
8
|
+
* surrounding editor / message list stays alive.
|
|
9
|
+
*
|
|
10
|
+
* Semantic tokens only (`bg-muted` / `border-border` / `text-muted-foreground`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AlertTriangle } from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
export interface BlockErrorProps {
|
|
16
|
+
/**
|
|
17
|
+
* Short, human-readable reason (e.g. the first Zod issue). Optional — when
|
|
18
|
+
* omitted only the generic headline shows.
|
|
19
|
+
*/
|
|
20
|
+
reason?: string;
|
|
21
|
+
/** What couldn't be rendered (e.g. "map", "bookmark"). Defaults to "block". */
|
|
22
|
+
label?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function BlockError({ reason, label = 'block', className }: BlockErrorProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
role="status"
|
|
30
|
+
className={
|
|
31
|
+
'flex items-start gap-2.5 rounded-lg border border-border bg-muted px-3 py-2.5 text-sm text-muted-foreground' +
|
|
32
|
+
(className ? ` ${className}` : '')
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
|
36
|
+
<div className="min-w-0">
|
|
37
|
+
<div className="font-medium text-foreground">Couldn't render this {label}</div>
|
|
38
|
+
{reason ? (
|
|
39
|
+
<div className="mt-0.5 break-words text-xs text-muted-foreground">{reason}</div>
|
|
40
|
+
) : null}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default BlockError;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared block payload types — the serializable wire shapes that both the
|
|
3
|
+
* chat message blocks and the NotionEditor block NodeViews reference, so the
|
|
4
|
+
* shapes can't drift. The link-preview data shape lives in
|
|
5
|
+
* `common/link-preview`; the map payload lives here. Re-exported below so a
|
|
6
|
+
* single import (`common/blocks`) reaches every shared block payload.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
MapBlockPayload,
|
|
11
|
+
MapMarkerPayload,
|
|
12
|
+
MapRoutePayload,
|
|
13
|
+
MapPolygonPayload,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
// Re-export the link-preview data contract from its home so consumers can
|
|
17
|
+
// reach every shared block payload from one place.
|
|
18
|
+
export type { LinkPreviewData, ResolveLinkPreview } from '../link-preview';
|
|
19
|
+
|
|
20
|
+
// Runtime validators + the safe-render fallback. A block NodeView/renderer
|
|
21
|
+
// parses its payload through the matching schema and renders `<BlockError>`
|
|
22
|
+
// on failure instead of throwing.
|
|
23
|
+
export {
|
|
24
|
+
MapMarkerPayloadSchema,
|
|
25
|
+
MapRoutePayloadSchema,
|
|
26
|
+
MapPolygonPayloadSchema,
|
|
27
|
+
MapBlockPayloadSchema,
|
|
28
|
+
LinkPreviewDataSchema,
|
|
29
|
+
safeParseBlock,
|
|
30
|
+
} from './schemas';
|
|
31
|
+
export type { SafeParseResult } from './schemas';
|
|
32
|
+
|
|
33
|
+
export { BlockError } from './BlockError';
|
|
34
|
+
export type { BlockErrorProps } from './BlockError';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for the shared, serializable block payloads in `./types.ts` and
|
|
3
|
+
* the link-preview data contract. These are the runtime VALIDATORS that let a
|
|
4
|
+
* block NodeView render a safe `<BlockError>` fallback instead of throwing when
|
|
5
|
+
* it's handed a hand-authored / corrupted payload (a bad ```map JSON, a
|
|
6
|
+
* malformed wire block).
|
|
7
|
+
*
|
|
8
|
+
* The TS payload types stay the source of truth in `./types.ts` (and
|
|
9
|
+
* `common/link-preview/types.ts`); these schemas are kept in SYNC with them and
|
|
10
|
+
* `satisfies`-checked below so they can't silently drift. We deliberately do
|
|
11
|
+
* NOT re-export `z.infer` types under the same names — existing imports
|
|
12
|
+
* (`MapBlockPayload`, `LinkPreviewData`) keep coming from their type homes.
|
|
13
|
+
*
|
|
14
|
+
* Schemas are permissive on optional/extra fields (`.passthrough` is implied by
|
|
15
|
+
* Zod object stripping unknowns) but STRICT on the load-bearing shape: numbers
|
|
16
|
+
* are numbers, `center`/`url` are required. Anything that wouldn't render
|
|
17
|
+
* cleanly fails `safeParse`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import type {
|
|
22
|
+
MapMarkerPayload,
|
|
23
|
+
MapRoutePayload,
|
|
24
|
+
MapPolygonPayload,
|
|
25
|
+
MapBlockPayload,
|
|
26
|
+
} from './types';
|
|
27
|
+
import type { LinkPreviewData } from '../link-preview';
|
|
28
|
+
|
|
29
|
+
/** A finite number — rejects NaN/Infinity that would break the map camera. */
|
|
30
|
+
const finiteNumber = z.number().finite();
|
|
31
|
+
|
|
32
|
+
/** `{ lat, lng }` — a geographic point; both finite numbers, required. */
|
|
33
|
+
const LatLngSchema = z.object({
|
|
34
|
+
lat: finiteNumber,
|
|
35
|
+
lng: finiteNumber,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Marker info-card (pure JSON; `href`-only actions). */
|
|
39
|
+
const MarkerCardSchema = z.object({
|
|
40
|
+
title: z.string(),
|
|
41
|
+
description: z.string().optional(),
|
|
42
|
+
image: z.string().optional(),
|
|
43
|
+
badge: z.string().optional(),
|
|
44
|
+
actions: z
|
|
45
|
+
.array(z.object({ label: z.string(), href: z.string().optional() }))
|
|
46
|
+
.optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** A single map marker — pin + optional label / icon / colour / card. */
|
|
50
|
+
export const MapMarkerPayloadSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
lat: finiteNumber,
|
|
53
|
+
lng: finiteNumber,
|
|
54
|
+
label: z.string().optional(),
|
|
55
|
+
icon: z.string().optional(),
|
|
56
|
+
color: z.string().optional(),
|
|
57
|
+
card: MarkerCardSchema.optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** An ordered polyline (route / path). */
|
|
61
|
+
export const MapRoutePayloadSchema = z.object({
|
|
62
|
+
id: z.string(),
|
|
63
|
+
points: z.array(LatLngSchema),
|
|
64
|
+
color: z.string().optional(),
|
|
65
|
+
label: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** A filled polygon (zone), auto-closed. */
|
|
69
|
+
export const MapPolygonPayloadSchema = z.object({
|
|
70
|
+
id: z.string(),
|
|
71
|
+
points: z.array(LatLngSchema),
|
|
72
|
+
fillColor: z.string().optional(),
|
|
73
|
+
strokeColor: z.string().optional(),
|
|
74
|
+
label: z.string().optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** The serializable map payload — center + optional viewport + overlays. */
|
|
78
|
+
export const MapBlockPayloadSchema = z.object({
|
|
79
|
+
center: LatLngSchema,
|
|
80
|
+
zoom: finiteNumber.optional(),
|
|
81
|
+
basemap: z.string().optional(),
|
|
82
|
+
terrain: z.boolean().optional(),
|
|
83
|
+
markers: z.array(MapMarkerPayloadSchema).optional(),
|
|
84
|
+
routes: z.array(MapRoutePayloadSchema).optional(),
|
|
85
|
+
polygons: z.array(MapPolygonPayloadSchema).optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Resolved link-preview metadata — `url` required, rest optional. */
|
|
89
|
+
export const LinkPreviewDataSchema = z.object({
|
|
90
|
+
url: z.string(),
|
|
91
|
+
title: z.string().optional(),
|
|
92
|
+
description: z.string().optional(),
|
|
93
|
+
image: z.string().optional(),
|
|
94
|
+
favicon: z.string().optional(),
|
|
95
|
+
siteName: z.string().optional(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Drift guards ──────────────────────────────────────────────────────────
|
|
99
|
+
// A type-level assertion that each schema's inferred output is assignable to
|
|
100
|
+
// the canonical TS payload type. If `./types.ts` and a schema diverge, the
|
|
101
|
+
// corresponding `Extends<…>` resolves to `never` and the assignment fails to
|
|
102
|
+
// compile — keeping the schemas and types in lockstep.
|
|
103
|
+
type Extends<A, B> = A extends B ? true : never;
|
|
104
|
+
const _markerOk: Extends<z.infer<typeof MapMarkerPayloadSchema>, MapMarkerPayload> = true;
|
|
105
|
+
const _routeOk: Extends<z.infer<typeof MapRoutePayloadSchema>, MapRoutePayload> = true;
|
|
106
|
+
const _polygonOk: Extends<z.infer<typeof MapPolygonPayloadSchema>, MapPolygonPayload> = true;
|
|
107
|
+
const _mapOk: Extends<z.infer<typeof MapBlockPayloadSchema>, MapBlockPayload> = true;
|
|
108
|
+
const _linkOk: Extends<z.infer<typeof LinkPreviewDataSchema>, LinkPreviewData> = true;
|
|
109
|
+
// Reference the guards so `noUnusedLocals` stays happy without exporting them.
|
|
110
|
+
void [_markerOk, _routeOk, _polygonOk, _mapOk, _linkOk];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Discriminated result of a guarded parse — never throws.
|
|
114
|
+
*
|
|
115
|
+
* The mutually-exclusive `?: never` fields make narrowing robust: a plain
|
|
116
|
+
* `if (!r.ok)` flows into the error branch even when `T` is a deep object type
|
|
117
|
+
* (TS's discriminant else-narrowing otherwise mis-fires on very large `data`
|
|
118
|
+
* shapes). Callers can therefore use `!r.ok` / `r.ok` freely.
|
|
119
|
+
*/
|
|
120
|
+
export type SafeParseResult<T> =
|
|
121
|
+
| { ok: true; data: T; error?: never }
|
|
122
|
+
| { ok: false; error: string; data?: never };
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate `data` against `schema`, returning a discriminated result instead of
|
|
126
|
+
* throwing. On failure `error` is a short, human-readable reason (the first
|
|
127
|
+
* Zod issue) suitable for a `<BlockError reason=…>` card.
|
|
128
|
+
*
|
|
129
|
+
* The output type is taken from the schema (`z.infer`) so callers get the
|
|
130
|
+
* precisely-typed `data` on the success branch with no extra type argument.
|
|
131
|
+
*/
|
|
132
|
+
export function safeParseBlock<S extends z.ZodTypeAny>(
|
|
133
|
+
schema: S,
|
|
134
|
+
data: unknown,
|
|
135
|
+
): SafeParseResult<z.infer<S>> {
|
|
136
|
+
const result = schema.safeParse(data);
|
|
137
|
+
if (result.success) return { ok: true, data: result.data };
|
|
138
|
+
const first = result.error.issues[0];
|
|
139
|
+
const reason = first
|
|
140
|
+
? `${first.path.length ? first.path.map(String).join('.') + ': ' : ''}${first.message}`
|
|
141
|
+
: 'Invalid payload';
|
|
142
|
+
return { ok: false, error: reason };
|
|
143
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, serializable block payload TYPES — the wire shapes that more than
|
|
3
|
+
* one surface (chat message blocks + the NotionEditor block NodeViews) must
|
|
4
|
+
* agree on. Defining them ONCE here keeps the shapes from drifting: chat's
|
|
5
|
+
* `tools/chat/types/block.ts` and the Notion nodes both reference these.
|
|
6
|
+
*
|
|
7
|
+
* Plain JSON only (no Date / no functions / no React nodes) so payloads
|
|
8
|
+
* survive history persistence, SSE transport, AND TipTap node attrs +
|
|
9
|
+
* markdown round-trips.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A single map marker — a pin plus optional label / icon / colour / card. */
|
|
13
|
+
export interface MapMarkerPayload {
|
|
14
|
+
id: string;
|
|
15
|
+
lat: number;
|
|
16
|
+
lng: number;
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Image URL rendered as the marker pin (rounded thumbnail). */
|
|
19
|
+
icon?: string;
|
|
20
|
+
/** CSS color for the default pin (ignored when `icon` is set). */
|
|
21
|
+
color?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Info-card opened above the pin on click (Google-Maps style). Pure JSON —
|
|
24
|
+
* strings/URLs only; actions are `href`-only links (no `onClick`/`icon`,
|
|
25
|
+
* which can't cross the wire). Rendered by the map's default `MarkerCard`.
|
|
26
|
+
*/
|
|
27
|
+
card?: {
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
image?: string;
|
|
31
|
+
badge?: string;
|
|
32
|
+
actions?: Array<{ label: string; href?: string }>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An ordered polyline (route / path). */
|
|
37
|
+
export interface MapRoutePayload {
|
|
38
|
+
id: string;
|
|
39
|
+
points: Array<{ lat: number; lng: number }>;
|
|
40
|
+
color?: string;
|
|
41
|
+
label?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A filled polygon (zone), auto-closed. */
|
|
45
|
+
export interface MapPolygonPayload {
|
|
46
|
+
id: string;
|
|
47
|
+
points: Array<{ lat: number; lng: number }>;
|
|
48
|
+
fillColor?: string;
|
|
49
|
+
strokeColor?: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Serializable map payload shared by the chat `MapBlock` and the NotionEditor
|
|
55
|
+
* `mapBlock` node. A center + optional zoom / basemap / terrain / markers /
|
|
56
|
+
* routes / polygons. The chat block extends this with chat-only concerns
|
|
57
|
+
* (a `caption`, `userLocation` opt-in); the Notion node serializes this whole
|
|
58
|
+
* object to/from a fenced ```` ```map ```` JSON block.
|
|
59
|
+
*/
|
|
60
|
+
export interface MapBlockPayload {
|
|
61
|
+
center: { lat: number; lng: number };
|
|
62
|
+
zoom?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Basemap style — a built-in key (`'light'`/`'dark'`/`'streets'` ·
|
|
65
|
+
* `'liberty'`/`'bright'`/`'positron'`) or a full style-JSON URL. When
|
|
66
|
+
* omitted the map uses its default basemap.
|
|
67
|
+
*/
|
|
68
|
+
basemap?: string;
|
|
69
|
+
/** Enable 3D terrain + hillshade (free AWS Terrarium DEM, no key). */
|
|
70
|
+
terrain?: boolean;
|
|
71
|
+
markers?: MapMarkerPayload[];
|
|
72
|
+
/** Polylines (routes / paths). */
|
|
73
|
+
routes?: MapRoutePayload[];
|
|
74
|
+
/** Filled zones (polygons). */
|
|
75
|
+
polygons?: MapPolygonPayload[];
|
|
76
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# chips
|
|
2
|
+
|
|
3
|
+
Surface-agnostic presentational "chips" — compact inline pills that render a
|
|
4
|
+
URL or a file path with a favicon/icon + middle-ellipsis label. They match the
|
|
5
|
+
TipTap composer's `editor-chip` look (see
|
|
6
|
+
[`forms/MarkdownEditor/chip/ChipNode.ts`](../../tools/forms/MarkdownEditor/chip/ChipNode.ts))
|
|
7
|
+
but are plain React, so the same visual is reusable on any surface — composer,
|
|
8
|
+
chat bubble, link lists.
|
|
9
|
+
|
|
10
|
+
Internal module (`src/common/chips/`) — consumed by other tools via relative
|
|
11
|
+
import; not a package subpath export:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { UrlChip, FilePathChip } from '../../common/chips';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## `UrlChip`
|
|
18
|
+
|
|
19
|
+
A compact, clickable URL chip (Chrome-omnibox style): a favicon + the domain
|
|
20
|
+
with a middle-ellipsised path (`github.com/wailsapp/…/README.md`), monospace,
|
|
21
|
+
with the full href on `title` hover. Renders an `<a target="_blank"
|
|
22
|
+
rel="noopener noreferrer">`, so it opens in a new tab.
|
|
23
|
+
|
|
24
|
+
- **Favicon fallback.** The favicon `<img>` swaps to a lucide `Globe` glyph on
|
|
25
|
+
load error (offline / blocked / unknown host) — and also when the host is
|
|
26
|
+
unparseable (empty favicon URL) — so the chip never shows a broken image.
|
|
27
|
+
- **Presentational + self-contained.** It imports only the pure URL detector
|
|
28
|
+
helpers from [`forms/MarkdownEditor/url/detect`](../../tools/forms/MarkdownEditor/url/detect.ts)
|
|
29
|
+
— no TipTap / ProseMirror — so it can render a known URL anywhere:
|
|
30
|
+
`splitUrl` (host/domain/path split), `truncateUrlLabel` (the
|
|
31
|
+
middle-ellipsis label), and `faviconUrl` (Google S2 favicon).
|
|
32
|
+
|
|
33
|
+
### Props (`UrlChipProps`)
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Default | Description |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `href` | `string` | required | The full, navigable href (`https://…`). |
|
|
38
|
+
| `maxChars` | `number` | `40` | Approximate max chars for the truncated label. The domain is always kept in full; only the path's middle collapses. |
|
|
39
|
+
| `className` | `string` | — | Extra classes merged onto the chip anchor. |
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<UrlChip href="https://github.com/wailsapp/wails/blob/main/v3/README.md" />
|
|
43
|
+
// → 🟣 github.com/…/README.md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `FilePathChip`
|
|
47
|
+
|
|
48
|
+
Re-exported (with its `FilePathChipProps` type) from the editor's standalone
|
|
49
|
+
path chip,
|
|
50
|
+
[`forms/MarkdownEditor/filePath/FilePathChip.tsx`](../../tools/forms/MarkdownEditor/filePath/FilePathChip.tsx).
|
|
51
|
+
It's already presentational and dependency-light, so this module re-exports it
|
|
52
|
+
rather than duplicating it — `UrlChip` and `FilePathChip` then live behind one
|
|
53
|
+
import.
|
|
54
|
+
|
|
55
|
+
## Where these are reused
|
|
56
|
+
|
|
57
|
+
- **Chat markdown bubble.** `UrlChip` is what
|
|
58
|
+
[`MarkdownMessage`'s `urlChipRule`](../../tools/dev/code/MarkdownMessage/README.md)
|
|
59
|
+
renders for a bare URL when the host opts in via `ChatConfig.linkChips`.
|
|
60
|
+
- **Composer.** The TipTap composer renders its **own** DOM chip via `ChipNode`
|
|
61
|
+
(a single inline atom node, `kind: 'url'`), not this React component — but the
|
|
62
|
+
two are built to match pixel-for-pixel using the same semantic tokens, so a
|
|
63
|
+
URL reads identically whether it's being typed in the composer or sitting in a
|
|
64
|
+
rendered bubble.
|
|
65
|
+
|
|
66
|
+
This shared look is the point: the same URL is one consistent chip across every
|
|
67
|
+
surface.
|