@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for the chat `MessageBlock` discriminated union (`./block.ts`).
|
|
3
|
+
*
|
|
4
|
+
* These are the runtime VALIDATORS used at the single registry dispatch point
|
|
5
|
+
* (`messages/blocks/MessageBlocks.tsx`): every block flows through
|
|
6
|
+
* `safeParseBlock(BLOCK_SCHEMAS[kind], block)` BEFORE its renderer runs, so a
|
|
7
|
+
* malformed wire / hand-authored payload renders a safe `<BlockError>` card
|
|
8
|
+
* instead of throwing inside a renderer and blanking the transcript.
|
|
9
|
+
*
|
|
10
|
+
* Design rules (mirroring `common/blocks/schemas.ts`):
|
|
11
|
+
* - The TS types in `./block.ts` stay the source of truth; these schemas are
|
|
12
|
+
* kept in sync and `Extends<…>`-checked below so they can't silently drift.
|
|
13
|
+
* - The map / link shapes are COMPOSED from the shared
|
|
14
|
+
* `MapBlockPayloadSchema` / `LinkPreviewDataSchema` in `common/blocks` — we
|
|
15
|
+
* never re-describe a map/link payload here.
|
|
16
|
+
* - LENIENT where the renderer is tolerant: optional fields stay optional,
|
|
17
|
+
* `json.data` is `unknown` (JsonTree is throw-safe), extra keys are stripped.
|
|
18
|
+
* - STRICT on the load-bearing shape: a field whose wrong type would THROW in
|
|
19
|
+
* the renderer is required + typed (gallery `items` must be an array;
|
|
20
|
+
* image/video/audio `src` must be a string; map coords are finite
|
|
21
|
+
* numbers via the shared schema).
|
|
22
|
+
* - `custom` and the prose kinds (`text`/`markdown`) carry no schema in
|
|
23
|
+
* `BLOCK_SCHEMAS` — `custom` is host-defined (don't over-constrain) and the
|
|
24
|
+
* prose kinds can't throw on a wrong-typed field worth guarding. They render
|
|
25
|
+
* as-is. (`text`/`markdown` schemas are still defined for completeness.)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
|
|
30
|
+
import { MapBlockPayloadSchema, LinkPreviewDataSchema } from '../../../common/blocks';
|
|
31
|
+
import type {
|
|
32
|
+
TextBlock,
|
|
33
|
+
MarkdownBlock,
|
|
34
|
+
AudioBlock,
|
|
35
|
+
VideoBlock,
|
|
36
|
+
ImageBlock,
|
|
37
|
+
GalleryBlock,
|
|
38
|
+
MapBlock,
|
|
39
|
+
JsonBlock,
|
|
40
|
+
MermaidBlock,
|
|
41
|
+
CodeBlock,
|
|
42
|
+
DiffBlock,
|
|
43
|
+
LinkBlock,
|
|
44
|
+
MessageBlock,
|
|
45
|
+
} from './block';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Base fields shared by every block (`BlockBase` in `./block.ts`). We validate
|
|
49
|
+
* the WHOLE block object (incl. `kind` / `id` / `caption`), so each schema
|
|
50
|
+
* composes this. `id` is required (it is the React key + memo discriminant).
|
|
51
|
+
*/
|
|
52
|
+
const BlockBaseSchema = z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
caption: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const TextBlockSchema = BlockBaseSchema.extend({
|
|
58
|
+
kind: z.literal('text'),
|
|
59
|
+
text: z.string(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const MarkdownBlockSchema = BlockBaseSchema.extend({
|
|
63
|
+
kind: z.literal('markdown'),
|
|
64
|
+
markdown: z.string(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const AudioBlockSchema = BlockBaseSchema.extend({
|
|
68
|
+
kind: z.literal('audio'),
|
|
69
|
+
src: z.string(),
|
|
70
|
+
title: z.string().optional(),
|
|
71
|
+
artist: z.string().optional(),
|
|
72
|
+
cover: z.string().optional(),
|
|
73
|
+
variant: z.enum(['default', 'compact']).optional(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const VideoBlockSchema = BlockBaseSchema.extend({
|
|
77
|
+
kind: z.literal('video'),
|
|
78
|
+
src: z.string(),
|
|
79
|
+
poster: z.string().optional(),
|
|
80
|
+
title: z.string().optional(),
|
|
81
|
+
aspectRatio: z.union([z.number(), z.literal('auto'), z.literal('fill')]).optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const ImageBlockSchema = BlockBaseSchema.extend({
|
|
85
|
+
kind: z.literal('image'),
|
|
86
|
+
src: z.string(),
|
|
87
|
+
alt: z.string().optional(),
|
|
88
|
+
interactive: z.boolean().optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const GalleryItemSchema = z.object({
|
|
92
|
+
id: z.string(),
|
|
93
|
+
src: z.string(),
|
|
94
|
+
thumbnail: z.string().optional(),
|
|
95
|
+
alt: z.string().optional(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const GalleryBlockSchema = BlockBaseSchema.extend({
|
|
99
|
+
kind: z.literal('gallery'),
|
|
100
|
+
// STRICT: `items` must be an array — the renderer maps over it.
|
|
101
|
+
items: z.array(GalleryItemSchema),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Map block = the shared serializable map payload (`MapBlockPayloadSchema`,
|
|
106
|
+
* which already enforces finite-number `center.lat/lng` etc.) + the base block
|
|
107
|
+
* fields + the chat-only `userLocation` opt-in flag. Composed, never
|
|
108
|
+
* duplicated.
|
|
109
|
+
*/
|
|
110
|
+
const MapBlockSchema = MapBlockPayloadSchema.merge(BlockBaseSchema).extend({
|
|
111
|
+
kind: z.literal('map'),
|
|
112
|
+
userLocation: z.boolean().optional(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const JsonBlockSchema = BlockBaseSchema.extend({
|
|
116
|
+
kind: z.literal('json'),
|
|
117
|
+
// LENIENT: JsonTree renders anything safely — accept any payload.
|
|
118
|
+
data: z.unknown(),
|
|
119
|
+
mode: z.enum(['full', 'compact', 'inline']).optional(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const MermaidBlockSchema = BlockBaseSchema.extend({
|
|
123
|
+
kind: z.literal('mermaid'),
|
|
124
|
+
chart: z.string(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const CodeBlockSchema = BlockBaseSchema.extend({
|
|
128
|
+
kind: z.literal('code'),
|
|
129
|
+
code: z.string(),
|
|
130
|
+
language: z.string(),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const DiffBlockSchema = BlockBaseSchema.extend({
|
|
134
|
+
kind: z.literal('diff'),
|
|
135
|
+
oldCode: z.string().optional(),
|
|
136
|
+
newCode: z.string().optional(),
|
|
137
|
+
patch: z.string().optional(),
|
|
138
|
+
language: z.string().optional(),
|
|
139
|
+
layout: z.enum(['split', 'unified']).optional(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Link block = base fields + `url` + optional pre-resolved `data` (validated
|
|
144
|
+
* against the shared `LinkPreviewDataSchema`). The renderer also tolerates a
|
|
145
|
+
* resolver-only block (no `data`), so `data` stays optional.
|
|
146
|
+
*/
|
|
147
|
+
const LinkBlockSchema = BlockBaseSchema.extend({
|
|
148
|
+
kind: z.literal('link'),
|
|
149
|
+
url: z.string(),
|
|
150
|
+
data: LinkPreviewDataSchema.optional(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── Drift guards ──────────────────────────────────────────────────────────
|
|
154
|
+
// Type-level assertion that each schema's inferred output stays assignable to
|
|
155
|
+
// the canonical block type in `./block.ts`. If a schema and the type diverge,
|
|
156
|
+
// the corresponding assignment fails to compile.
|
|
157
|
+
type Extends<A, B> = A extends B ? true : never;
|
|
158
|
+
const _textOk: Extends<z.infer<typeof TextBlockSchema>, TextBlock> = true;
|
|
159
|
+
const _markdownOk: Extends<z.infer<typeof MarkdownBlockSchema>, MarkdownBlock> = true;
|
|
160
|
+
const _audioOk: Extends<z.infer<typeof AudioBlockSchema>, AudioBlock> = true;
|
|
161
|
+
const _videoOk: Extends<z.infer<typeof VideoBlockSchema>, VideoBlock> = true;
|
|
162
|
+
const _imageOk: Extends<z.infer<typeof ImageBlockSchema>, ImageBlock> = true;
|
|
163
|
+
const _galleryOk: Extends<z.infer<typeof GalleryBlockSchema>, GalleryBlock> = true;
|
|
164
|
+
const _mapOk: Extends<z.infer<typeof MapBlockSchema>, MapBlock> = true;
|
|
165
|
+
const _jsonOk: Extends<z.infer<typeof JsonBlockSchema>, JsonBlock> = true;
|
|
166
|
+
const _mermaidOk: Extends<z.infer<typeof MermaidBlockSchema>, MermaidBlock> = true;
|
|
167
|
+
const _codeOk: Extends<z.infer<typeof CodeBlockSchema>, CodeBlock> = true;
|
|
168
|
+
const _diffOk: Extends<z.infer<typeof DiffBlockSchema>, DiffBlock> = true;
|
|
169
|
+
const _linkOk: Extends<z.infer<typeof LinkBlockSchema>, LinkBlock> = true;
|
|
170
|
+
void [
|
|
171
|
+
_textOk, _markdownOk, _audioOk, _videoOk, _imageOk, _galleryOk, _mapOk,
|
|
172
|
+
_jsonOk, _mermaidOk, _codeOk, _diffOk, _linkOk,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* `kind` → schema. Validated at the registry dispatch point. Kinds WITHOUT an
|
|
177
|
+
* entry are skipped (rendered as-is):
|
|
178
|
+
* - `custom` — host-defined `payload`; over-constraining it would break hosts.
|
|
179
|
+
*
|
|
180
|
+
* Every built-in data block is covered so a malformed payload is caught
|
|
181
|
+
* centrally — and any FUTURE kind is guarded the moment it's added here.
|
|
182
|
+
*/
|
|
183
|
+
export const BLOCK_SCHEMAS: Partial<Record<MessageBlock['kind'], z.ZodType>> = {
|
|
184
|
+
text: TextBlockSchema,
|
|
185
|
+
markdown: MarkdownBlockSchema,
|
|
186
|
+
audio: AudioBlockSchema,
|
|
187
|
+
video: VideoBlockSchema,
|
|
188
|
+
image: ImageBlockSchema,
|
|
189
|
+
gallery: GalleryBlockSchema,
|
|
190
|
+
map: MapBlockSchema,
|
|
191
|
+
json: JsonBlockSchema,
|
|
192
|
+
mermaid: MermaidBlockSchema,
|
|
193
|
+
code: CodeBlockSchema,
|
|
194
|
+
diff: DiffBlockSchema,
|
|
195
|
+
link: LinkBlockSchema,
|
|
196
|
+
// `custom` intentionally omitted — host-defined, rendered as-is.
|
|
197
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ChatAssistantContext, ChatUserContext } from './persona';
|
|
6
6
|
import type { ChatLabels } from './labels';
|
|
7
|
+
import type { ResolveLinkPreview } from '../../../common/link-preview';
|
|
7
8
|
|
|
8
9
|
export type ChatDisplayMode = 'closed' | 'embedded' | 'floating' | 'sidebar' | 'fullscreen';
|
|
9
10
|
|
|
@@ -40,4 +41,36 @@ export interface ChatConfig {
|
|
|
40
41
|
prefs?: ChatPrefs;
|
|
41
42
|
/** Visual labels (i18n is the host's job). */
|
|
42
43
|
labels?: Partial<ChatLabels>;
|
|
44
|
+
/** Link-preview (URL unfurl) wiring — the host owns metadata fetching. */
|
|
45
|
+
linkPreview?: ChatLinkPreviewConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Render bare web URLs inside message bubbles as compact "URL chips"
|
|
48
|
+
* (favicon + domain + middle-ellipsis path, e.g.
|
|
49
|
+
* `github.com/wailsapp/…/README.md`) instead of a plain blue link —
|
|
50
|
+
* matching the composer's URL chip. Only bare `http(s)` autolinks are
|
|
51
|
+
* chipped; a `[labeled](url)` link keeps its author-chosen text as a
|
|
52
|
+
* normal link.
|
|
53
|
+
*
|
|
54
|
+
* Off by default (the host opts in). When on, the chip rule is appended
|
|
55
|
+
* AFTER any host-supplied `linkRules`, so host-specific rules win.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
linkChips?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ChatLinkPreviewConfig {
|
|
62
|
+
/**
|
|
63
|
+
* Host resolver that turns a URL into preview metadata. Required for any
|
|
64
|
+
* link card to appear (the browser cannot fetch meta tags cross-origin).
|
|
65
|
+
* In Wails/Go this wraps a bound `FetchLinkMetadata` method; on the web,
|
|
66
|
+
* a `/api/unfurl` route.
|
|
67
|
+
*/
|
|
68
|
+
resolve?: ResolveLinkPreview;
|
|
69
|
+
/**
|
|
70
|
+
* Auto-render a preview card under a message bubble when its text
|
|
71
|
+
* contains a link (Telegram-style — at most ONE card per message).
|
|
72
|
+
* Off by default; needs `resolve` to be set.
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
75
|
+
autoDetect?: boolean;
|
|
43
76
|
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* into this union by `mapPydanticAIEvent` and friends.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type { ChatMessage } from './message';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Per-run metrics block. Emitted once per assistant turn
|
|
12
|
-
*
|
|
13
|
-
* `message_end`
|
|
11
|
+
* Per-run metrics block. Emitted once per assistant turn, carried into the
|
|
12
|
+
* message either by a non-terminal `message_metrics` event mid-run or via
|
|
13
|
+
* the terminal `message_end` event's generic `patch` slot.
|
|
14
14
|
*
|
|
15
15
|
* All fields optional — the backend omits anything it could not measure.
|
|
16
16
|
*/
|
|
@@ -54,9 +54,16 @@ export type ChatStreamEvent =
|
|
|
54
54
|
}
|
|
55
55
|
| {
|
|
56
56
|
type: 'message_end';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Generic finalize patch merged into the message on done — the ONE
|
|
59
|
+
* slot every finalize feature rides (tokens, sources, blocks, OCR
|
|
60
|
+
* text, attachments-finalized, local-preview metadata). `content`
|
|
61
|
+
* here REPLACES the accumulated bubble body (the append-only gap).
|
|
62
|
+
* The reducer applies it as a `Partial<ChatMessage>` merge — the same
|
|
63
|
+
* mechanism `message_metrics`/`resolved_model` already use. Absent →
|
|
64
|
+
* `message_end` is a pure terminal no-op (just clears `isStreaming`).
|
|
65
|
+
*/
|
|
66
|
+
patch?: Partial<ChatMessage>;
|
|
60
67
|
}
|
|
61
68
|
| {
|
|
62
69
|
/**
|
|
@@ -33,13 +33,15 @@ export type {
|
|
|
33
33
|
JsonBlock,
|
|
34
34
|
MermaidBlock,
|
|
35
35
|
CodeBlock,
|
|
36
|
-
|
|
36
|
+
DiffBlock,
|
|
37
|
+
LinkBlock,
|
|
37
38
|
CustomBlock,
|
|
38
39
|
} from './block';
|
|
40
|
+
export type { LinkPreviewData, ResolveLinkPreview } from '../../../common/link-preview';
|
|
39
41
|
export type { ChatMessage } from './message';
|
|
40
42
|
export { DEFAULT_LABELS } from './labels';
|
|
41
43
|
export type { ChatLabels } from './labels';
|
|
42
|
-
export type { ChatConfig, ChatPrefs, ChatDisplayMode } from './config';
|
|
44
|
+
export type { ChatConfig, ChatPrefs, ChatDisplayMode, ChatLinkPreviewConfig } from './config';
|
|
43
45
|
export type { ChatStreamEvent, ChatMessageMetrics } from './events';
|
|
44
46
|
export type {
|
|
45
47
|
CreateSessionOptions,
|
|
@@ -17,6 +17,13 @@ export interface ChatMessage {
|
|
|
17
17
|
content: string;
|
|
18
18
|
/** epoch ms — serializable. */
|
|
19
19
|
createdAt: number;
|
|
20
|
+
/**
|
|
21
|
+
* Stable per-session transcript position, supplied by hosts whose backend
|
|
22
|
+
* keys messages by an absolute sequence (e.g. a monotonic `chat_history.seq`).
|
|
23
|
+
* The synthetic `id` is a UI key only; `seq` is the durable handle the host
|
|
24
|
+
* passes back to an authoritative edit/truncate API. Optional — engines that
|
|
25
|
+
* don't track a backend seq leave it undefined. */
|
|
26
|
+
seq?: number;
|
|
20
27
|
isStreaming?: boolean;
|
|
21
28
|
isError?: boolean;
|
|
22
29
|
/** Bumps on edit so memo keys invalidate. */
|
|
@@ -104,8 +104,8 @@ const TOKEN_CLASS: Record<string, string> = {
|
|
|
104
104
|
string: 'text-[#a31515] dark:text-emerald-400',
|
|
105
105
|
number: 'text-[#098658] dark:text-sky-400',
|
|
106
106
|
boolean: 'text-[#0550ae] dark:text-amber-400',
|
|
107
|
-
null: 'text-
|
|
108
|
-
punctuation: 'text-
|
|
107
|
+
null: 'text-muted-foreground/70 dark:text-muted-foreground/60',
|
|
108
|
+
punctuation: 'text-muted-foreground dark:text-muted-foreground',
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
function TokenSpan({
|
|
@@ -131,7 +131,7 @@ function HighlightMatch({ text, query }: { text: string; query: string }) {
|
|
|
131
131
|
return (
|
|
132
132
|
<>
|
|
133
133
|
{text.slice(0, idx)}
|
|
134
|
-
<mark className="rounded-sm bg-
|
|
134
|
+
<mark className="rounded-sm bg-warning/30 px-0.5 text-inherit">
|
|
135
135
|
{text.slice(idx, idx + query.length)}
|
|
136
136
|
</mark>
|
|
137
137
|
{text.slice(idx + query.length)}
|
|
@@ -189,7 +189,7 @@ function JsonNode({
|
|
|
189
189
|
|
|
190
190
|
const rowClass = cn(
|
|
191
191
|
'group flex items-center gap-0 py-px hover:bg-muted/40 rounded-sm',
|
|
192
|
-
nodeMatches && 'bg-
|
|
192
|
+
nodeMatches && 'bg-warning/15',
|
|
193
193
|
);
|
|
194
194
|
|
|
195
195
|
const rowStyle: CSSProperties = { paddingLeft: `${depth * 20 + 8}px` };
|
|
@@ -264,7 +264,7 @@ function JsonNode({
|
|
|
264
264
|
className={copyIconClass}
|
|
265
265
|
>
|
|
266
266
|
{pathCopied ? (
|
|
267
|
-
<CopyPlus className="size-3 text-
|
|
267
|
+
<CopyPlus className="size-3 text-success" />
|
|
268
268
|
) : (
|
|
269
269
|
<CopyPlus className="size-3" />
|
|
270
270
|
)}
|
|
@@ -317,7 +317,7 @@ function JsonNode({
|
|
|
317
317
|
className={copyIconClass}
|
|
318
318
|
>
|
|
319
319
|
{pathCopied ? (
|
|
320
|
-
<CopyPlus className="size-3 text-
|
|
320
|
+
<CopyPlus className="size-3 text-success" />
|
|
321
321
|
) : (
|
|
322
322
|
<CopyPlus className="size-3" />
|
|
323
323
|
)}
|
|
@@ -489,7 +489,7 @@ function Toolbar({
|
|
|
489
489
|
'copy',
|
|
490
490
|
copiedAll ? 'Copied!' : 'Copy JSON',
|
|
491
491
|
copiedAll ? (
|
|
492
|
-
<Check className={cn(iconCls, 'text-
|
|
492
|
+
<Check className={cn(iconCls, 'text-success')} />
|
|
493
493
|
) : (
|
|
494
494
|
<Copy className={iconCls} />
|
|
495
495
|
),
|
|
@@ -376,6 +376,7 @@ Defaults are English. Function-shaped entries (`confirmDeleteTitle(count)`, `con
|
|
|
376
376
|
```tsx
|
|
377
377
|
<TreeRoot
|
|
378
378
|
appearance={{
|
|
379
|
+
variant: 'explorer', // 'explorer' (default) | 'list'
|
|
379
380
|
density: 'cozy', // 'compact' | 'cozy' | 'comfortable'
|
|
380
381
|
accent: 'default', // 'subtle' | 'default' | 'strong'
|
|
381
382
|
radius: 'sm', // 'none' | 'sm' | 'md'
|
|
@@ -387,6 +388,35 @@ Defaults are English. Function-shaped entries (`confirmDeleteTitle(count)`, `con
|
|
|
387
388
|
/>
|
|
388
389
|
```
|
|
389
390
|
|
|
391
|
+
### Variant — `explorer` vs `list`
|
|
392
|
+
|
|
393
|
+
`variant` is a high-level look that sets sensible defaults for three
|
|
394
|
+
fields (`hideLeafIcons`, `hideFolderIcons`, `rowSizing`,
|
|
395
|
+
`showActiveIndicator`); any of those passed explicitly still wins.
|
|
396
|
+
|
|
397
|
+
| Variant | Look | Defaults |
|
|
398
|
+
| --- | --- | --- |
|
|
399
|
+
| `'explorer'` (default) | Classic file/folder tree | leaf + folder icons, **fixed** single-line rows, active bar on |
|
|
400
|
+
| `'list'` | macOS-sidebar / **chat-history** list | **no icons** (chevron only on groups), **auto-height** rows (multi-line label friendly), quiet selection (no active bar) |
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
// A grouped chat-history sidebar (date / topic buckets → sessions),
|
|
404
|
+
// multi-line rows (title + dimmed meta), no file glyphs:
|
|
405
|
+
<TreeRoot<Payload>
|
|
406
|
+
data={groupedSessions}
|
|
407
|
+
getItemName={(n) => label(n)}
|
|
408
|
+
appearance={{ variant: 'list', indent: 10 }}
|
|
409
|
+
showIndentGuides={false}
|
|
410
|
+
renderLabel={({ node }) => /* title + meta */ null}
|
|
411
|
+
/>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Fine-grained fields (override the variant):
|
|
415
|
+
|
|
416
|
+
- `hideLeafIcons` / `hideFolderIcons` — drop the per-row icon (chevron stays).
|
|
417
|
+
- `rowSizing: 'fixed' | 'auto'` — `auto` turns `rowHeight` into a *minimum*
|
|
418
|
+
so a multi-line label grows the row instead of overflowing.
|
|
419
|
+
|
|
390
420
|
The resolved appearance is exposed on the root container as CSS variables:
|
|
391
421
|
|
|
392
422
|
```
|
|
@@ -175,7 +175,16 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
|
|
|
175
175
|
tabIndex={-1}
|
|
176
176
|
style={{
|
|
177
177
|
paddingLeft: 6 + level * appearance.indent,
|
|
178
|
-
|
|
178
|
+
// `auto` sizing lets a multi-line label (e.g. title + meta in a
|
|
179
|
+
// chat-history list) grow the row instead of overflowing a fixed
|
|
180
|
+
// height. `fixed` keeps the classic single-line explorer row.
|
|
181
|
+
...(appearance.rowSizing === 'auto'
|
|
182
|
+
? {
|
|
183
|
+
minHeight: 'var(--tree-row-height)',
|
|
184
|
+
paddingTop: 4,
|
|
185
|
+
paddingBottom: 4,
|
|
186
|
+
}
|
|
187
|
+
: { height: 'var(--tree-row-height)' }),
|
|
179
188
|
gap: 'var(--tree-gap)',
|
|
180
189
|
}}
|
|
181
190
|
{...(dnd.active ? draggable.listeners : {})}
|
|
@@ -234,7 +243,7 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
|
|
|
234
243
|
/>
|
|
235
244
|
) : renderIcon ? (
|
|
236
245
|
renderIcon(slot)
|
|
237
|
-
) : (
|
|
246
|
+
) : (isFolder ? appearance.hideFolderIcons : appearance.hideLeafIcons) ? null : (
|
|
238
247
|
<TreeIcon isFolder={isFolder} isExpanded={isExpanded} />
|
|
239
248
|
)}
|
|
240
249
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react';
|
|
3
4
|
import {
|
|
4
5
|
ContextMenu,
|
|
5
6
|
ContextMenuContent,
|
|
@@ -14,6 +15,60 @@ import type {
|
|
|
14
15
|
TreeRowRenderProps,
|
|
15
16
|
} from '../../types';
|
|
16
17
|
|
|
18
|
+
interface RowContextMenuProps<T> {
|
|
19
|
+
rowProps: TreeRowRenderProps<T>;
|
|
20
|
+
items: TreeContextMenuItem<T>[];
|
|
21
|
+
trigger: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-row context menu.
|
|
26
|
+
*
|
|
27
|
+
* PERF: every visible row wraps its trigger in a `<ContextMenu>`, so a
|
|
28
|
+
* large tree mounts hundreds of these. Building the full
|
|
29
|
+
* `<ContextMenuContent>` item subtree for each of them on every render is
|
|
30
|
+
* what made right-click feel laggy (the selection change on right-click
|
|
31
|
+
* re-renders all rows, each rebuilding its menu body). We now render the
|
|
32
|
+
* content body ONLY while this row's menu is actually open — closed rows
|
|
33
|
+
* cost just the lightweight `ContextMenu` + `Trigger` shell.
|
|
34
|
+
*/
|
|
35
|
+
function RowContextMenu<T>({
|
|
36
|
+
rowProps,
|
|
37
|
+
items,
|
|
38
|
+
trigger,
|
|
39
|
+
}: RowContextMenuProps<T>) {
|
|
40
|
+
const [open, setOpen] = useState(false);
|
|
41
|
+
return (
|
|
42
|
+
<ContextMenu onOpenChange={setOpen}>
|
|
43
|
+
<ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
|
|
44
|
+
{open ? (
|
|
45
|
+
<ContextMenuContent>
|
|
46
|
+
{items.map((item, idx) => {
|
|
47
|
+
if (item === 'separator') {
|
|
48
|
+
return <ContextMenuSeparator key={`sep-${idx}`} />;
|
|
49
|
+
}
|
|
50
|
+
const Icon = item.icon;
|
|
51
|
+
return (
|
|
52
|
+
<ContextMenuItem
|
|
53
|
+
key={item.id}
|
|
54
|
+
disabled={item.disabled}
|
|
55
|
+
variant={item.destructive ? 'destructive' : undefined}
|
|
56
|
+
onSelect={() => item.onSelect(rowProps)}
|
|
57
|
+
>
|
|
58
|
+
{Icon ? <Icon /> : null}
|
|
59
|
+
{item.label}
|
|
60
|
+
{item.shortcut ? (
|
|
61
|
+
<ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
|
|
62
|
+
) : null}
|
|
63
|
+
</ContextMenuItem>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</ContextMenuContent>
|
|
67
|
+
) : null}
|
|
68
|
+
</ContextMenu>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
17
72
|
/**
|
|
18
73
|
* Render an array of declarative menu items as a themed `<ContextMenu>`
|
|
19
74
|
* wrapped around the supplied trigger element. Pure presentational layer
|
|
@@ -24,33 +79,7 @@ export function renderItemsAsContextMenu<T>(
|
|
|
24
79
|
items: TreeContextMenuItem<T>[],
|
|
25
80
|
trigger: React.ReactNode,
|
|
26
81
|
): React.ReactNode {
|
|
27
|
-
return
|
|
28
|
-
<ContextMenu>
|
|
29
|
-
<ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
|
|
30
|
-
<ContextMenuContent>
|
|
31
|
-
{items.map((item, idx) => {
|
|
32
|
-
if (item === 'separator') {
|
|
33
|
-
return <ContextMenuSeparator key={`sep-${idx}`} />;
|
|
34
|
-
}
|
|
35
|
-
const Icon = item.icon;
|
|
36
|
-
return (
|
|
37
|
-
<ContextMenuItem
|
|
38
|
-
key={item.id}
|
|
39
|
-
disabled={item.disabled}
|
|
40
|
-
variant={item.destructive ? 'destructive' : undefined}
|
|
41
|
-
onSelect={() => item.onSelect(rowProps)}
|
|
42
|
-
>
|
|
43
|
-
{Icon ? <Icon /> : null}
|
|
44
|
-
{item.label}
|
|
45
|
-
{item.shortcut ? (
|
|
46
|
-
<ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
|
|
47
|
-
) : null}
|
|
48
|
-
</ContextMenuItem>
|
|
49
|
-
);
|
|
50
|
-
})}
|
|
51
|
-
</ContextMenuContent>
|
|
52
|
-
</ContextMenu>
|
|
53
|
-
);
|
|
82
|
+
return <RowContextMenu rowProps={rowProps} items={items} trigger={trigger} />;
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
/**
|
|
@@ -6,6 +6,20 @@ export type TreeDensity = 'compact' | 'cozy' | 'comfortable';
|
|
|
6
6
|
export type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
|
|
7
7
|
export type TreeRadius = 'none' | 'sm' | 'md';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* High-level look. Sets sensible defaults for icons / row sizing / the
|
|
11
|
+
* active indicator; any individual `TreeAppearance` field still overrides.
|
|
12
|
+
*
|
|
13
|
+
* - `'explorer'` (default) — classic file/folder tree: leaf + folder
|
|
14
|
+
* icons, fixed single-line rows, VSCode active-bar.
|
|
15
|
+
* - `'list'` — macOS-sidebar / chat-history look: no icons (chevron
|
|
16
|
+
* only on groups), auto-height rows (multi-line label friendly), quiet
|
|
17
|
+
* selection (no active-bar).
|
|
18
|
+
*/
|
|
19
|
+
export type TreeVariant = 'explorer' | 'list';
|
|
20
|
+
|
|
21
|
+
export type TreeRowSizing = 'fixed' | 'auto';
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Cosmetic configuration. Every field is optional; missing values fall
|
|
11
25
|
* back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
|
|
@@ -38,6 +52,21 @@ export interface TreeAppearance {
|
|
|
38
52
|
* Mimics the VSCode active-tab indicator. Default: `true`.
|
|
39
53
|
*/
|
|
40
54
|
showActiveIndicator?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* High-level look. Default: `'explorer'`. Sets defaults for the three
|
|
57
|
+
* fields below; pass any of them explicitly to override the variant.
|
|
58
|
+
*/
|
|
59
|
+
variant?: TreeVariant;
|
|
60
|
+
/** Hide per-leaf icons (chevron + label only). Variant default. */
|
|
61
|
+
hideLeafIcons?: boolean;
|
|
62
|
+
/** Hide folder icons too (chevron stays). Variant default. */
|
|
63
|
+
hideFolderIcons?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* `'fixed'` — every row is exactly `rowHeight` (single-line explorer).
|
|
66
|
+
* `'auto'` — `rowHeight` is a *minimum*; rows grow to fit a multi-line
|
|
67
|
+
* label (title + meta). Variant default.
|
|
68
|
+
*/
|
|
69
|
+
rowSizing?: TreeRowSizing;
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
export interface ResolvedAppearance {
|
|
@@ -52,6 +81,9 @@ export interface ResolvedAppearance {
|
|
|
52
81
|
radius: TreeRadius;
|
|
53
82
|
indentGuideOpacity: number;
|
|
54
83
|
showActiveIndicator: boolean;
|
|
84
|
+
hideLeafIcons: boolean;
|
|
85
|
+
hideFolderIcons: boolean;
|
|
86
|
+
rowSizing: TreeRowSizing;
|
|
55
87
|
}
|
|
56
88
|
|
|
57
89
|
const DENSITY_PRESETS: Record<
|
|
@@ -63,6 +95,31 @@ const DENSITY_PRESETS: Record<
|
|
|
63
95
|
comfortable: { rowHeight: 32, iconSize: 16, fontSize: 14, gap: 8 },
|
|
64
96
|
};
|
|
65
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Per-variant defaults for the icon / sizing / indicator fields. The
|
|
100
|
+
* `'list'` variant is the macOS-sidebar / chat-history look.
|
|
101
|
+
*/
|
|
102
|
+
const VARIANT_DEFAULTS: Record<
|
|
103
|
+
TreeVariant,
|
|
104
|
+
Pick<
|
|
105
|
+
ResolvedAppearance,
|
|
106
|
+
'hideLeafIcons' | 'hideFolderIcons' | 'rowSizing' | 'showActiveIndicator'
|
|
107
|
+
>
|
|
108
|
+
> = {
|
|
109
|
+
explorer: {
|
|
110
|
+
hideLeafIcons: false,
|
|
111
|
+
hideFolderIcons: false,
|
|
112
|
+
rowSizing: 'fixed',
|
|
113
|
+
showActiveIndicator: true,
|
|
114
|
+
},
|
|
115
|
+
list: {
|
|
116
|
+
hideLeafIcons: true,
|
|
117
|
+
hideFolderIcons: true,
|
|
118
|
+
rowSizing: 'auto',
|
|
119
|
+
showActiveIndicator: false,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
66
123
|
export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
|
|
67
124
|
density: 'cozy',
|
|
68
125
|
...DENSITY_PRESETS.cozy,
|
|
@@ -71,7 +128,7 @@ export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
|
|
|
71
128
|
accent: 'default',
|
|
72
129
|
radius: 'sm',
|
|
73
130
|
indentGuideOpacity: 0.4,
|
|
74
|
-
|
|
131
|
+
...VARIANT_DEFAULTS.explorer,
|
|
75
132
|
};
|
|
76
133
|
|
|
77
134
|
/**
|
|
@@ -88,6 +145,9 @@ export function resolveAppearance(
|
|
|
88
145
|
|
|
89
146
|
const density: TreeDensity = input?.density ?? 'cozy';
|
|
90
147
|
const preset = DENSITY_PRESETS[density];
|
|
148
|
+
// Variant sets the icon/sizing/indicator defaults; explicit fields win.
|
|
149
|
+
const variant: TreeVariant = input?.variant ?? 'explorer';
|
|
150
|
+
const v = VARIANT_DEFAULTS[variant];
|
|
91
151
|
|
|
92
152
|
return {
|
|
93
153
|
density,
|
|
@@ -101,8 +161,10 @@ export function resolveAppearance(
|
|
|
101
161
|
radius: input?.radius ?? DEFAULT_TREE_APPEARANCE.radius,
|
|
102
162
|
indentGuideOpacity:
|
|
103
163
|
input?.indentGuideOpacity ?? DEFAULT_TREE_APPEARANCE.indentGuideOpacity,
|
|
104
|
-
showActiveIndicator:
|
|
105
|
-
|
|
164
|
+
showActiveIndicator: input?.showActiveIndicator ?? v.showActiveIndicator,
|
|
165
|
+
hideLeafIcons: input?.hideLeafIcons ?? v.hideLeafIcons,
|
|
166
|
+
hideFolderIcons: input?.hideFolderIcons ?? v.hideFolderIcons,
|
|
167
|
+
rowSizing: input?.rowSizing ?? v.rowSizing,
|
|
106
168
|
};
|
|
107
169
|
}
|
|
108
170
|
|