@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,161 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { findFilePaths, splitPath, truncatePathLabel } from '../detect';
|
|
3
|
+
|
|
4
|
+
describe('findFilePaths', () => {
|
|
5
|
+
it('detects the repro path embedded in Cyrillic prose', () => {
|
|
6
|
+
const text =
|
|
7
|
+
'прочти манифест /Users/markinmatrix/workspace/@projects/cmdop/@docs/positioning/manifest.md интересно твоё мнение';
|
|
8
|
+
const matches = findFilePaths(text);
|
|
9
|
+
expect(matches).toHaveLength(1);
|
|
10
|
+
expect(matches[0]!.path).toBe(
|
|
11
|
+
'/Users/markinmatrix/workspace/@projects/cmdop/@docs/positioning/manifest.md',
|
|
12
|
+
);
|
|
13
|
+
// The slice at [start,end) must equal the matched path exactly.
|
|
14
|
+
expect(text.slice(matches[0]!.start, matches[0]!.end)).toBe(matches[0]!.path);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('detects a Windows drive path with backslashes', () => {
|
|
18
|
+
const matches = findFilePaths('see C:\\Users\\me\\notes.md please');
|
|
19
|
+
expect(matches).toHaveLength(1);
|
|
20
|
+
expect(matches[0]!.path).toBe('C:\\Users\\me\\notes.md');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('detects a Windows drive path with forward slashes', () => {
|
|
24
|
+
const matches = findFilePaths('open C:/Users/me/notes.md');
|
|
25
|
+
expect(matches[0]!.path).toBe('C:/Users/me/notes.md');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('detects a UNC path', () => {
|
|
29
|
+
const matches = findFilePaths('on \\\\srv\\share\\x.md here');
|
|
30
|
+
expect(matches).toHaveLength(1);
|
|
31
|
+
expect(matches[0]!.path).toBe('\\\\srv\\share\\x.md');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('detects a ~ home path', () => {
|
|
35
|
+
const matches = findFilePaths('edit ~/dev/a.ts now');
|
|
36
|
+
expect(matches).toHaveLength(1);
|
|
37
|
+
expect(matches[0]!.path).toBe('~/dev/a.ts');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('detects a file:// URI', () => {
|
|
41
|
+
const matches = findFilePaths('go to file:///x/y.md ok');
|
|
42
|
+
expect(matches).toHaveLength(1);
|
|
43
|
+
expect(matches[0]!.path).toBe('file:///x/y.md');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('detects a directory path ending in a folder', () => {
|
|
47
|
+
const matches = findFilePaths('cd /Users/me/positioning');
|
|
48
|
+
expect(matches).toHaveLength(1);
|
|
49
|
+
expect(matches[0]!.path).toBe('/Users/me/positioning');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('detects a directory path ending in a trailing separator', () => {
|
|
53
|
+
const matches = findFilePaths('cd /Users/me/positioning/ then');
|
|
54
|
+
expect(matches[0]!.path).toBe('/Users/me/positioning/');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('trims trailing sentence punctuation', () => {
|
|
58
|
+
const matches = findFilePaths('see /a/b/c.md. Done.');
|
|
59
|
+
expect(matches).toHaveLength(1);
|
|
60
|
+
expect(matches[0]!.path).toBe('/a/b/c.md');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('trims a closing paren', () => {
|
|
64
|
+
const matches = findFilePaths('(at /a/b/c.md)');
|
|
65
|
+
expect(matches[0]!.path).toBe('/a/b/c.md');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── False positives ──
|
|
69
|
+
|
|
70
|
+
it('does NOT match a prose slash "a / b"', () => {
|
|
71
|
+
expect(findFilePaths('a / b')).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does NOT match a single-segment root "/foo"', () => {
|
|
75
|
+
expect(findFilePaths('just /foo here')).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does NOT match an https URL as a file path', () => {
|
|
79
|
+
expect(findFilePaths('visit https://x.com/y/z page')).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does NOT match an http URL as a file path', () => {
|
|
83
|
+
expect(findFilePaths('http://example.com/a/b')).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does NOT match bare relative tokens', () => {
|
|
87
|
+
expect(findFilePaths('run ./script and src/index.ts')).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('finds multiple paths in one string', () => {
|
|
91
|
+
const matches = findFilePaths('a /x/y.ts and ~/z/w.md done');
|
|
92
|
+
expect(matches.map((m) => m.path)).toEqual(['/x/y.ts', '~/z/w.md']);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('splitPath', () => {
|
|
97
|
+
it('splits a unix file path', () => {
|
|
98
|
+
expect(splitPath('/Users/me/manifest.md')).toEqual({
|
|
99
|
+
dir: 'Users/me',
|
|
100
|
+
base: 'manifest.md',
|
|
101
|
+
isDir: false,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('treats an extensionless basename as a dir', () => {
|
|
106
|
+
const r = splitPath('/Users/me/positioning');
|
|
107
|
+
expect(r.base).toBe('positioning');
|
|
108
|
+
expect(r.isDir).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('treats a trailing-separator path as a dir', () => {
|
|
112
|
+
const r = splitPath('/Users/me/docs/');
|
|
113
|
+
expect(r.base).toBe('docs');
|
|
114
|
+
expect(r.isDir).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('is separator-agnostic for Windows paths', () => {
|
|
118
|
+
const r = splitPath('C:\\Users\\me\\notes.md');
|
|
119
|
+
expect(r.base).toBe('notes.md');
|
|
120
|
+
expect(r.isDir).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('strips a file:// scheme', () => {
|
|
124
|
+
expect(splitPath('file:///x/y.md').base).toBe('y.md');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('treats a dotfile as a file', () => {
|
|
128
|
+
expect(splitPath('/home/me/.gitignore').isDir).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('truncatePathLabel', () => {
|
|
133
|
+
it('keeps a short path unchanged', () => {
|
|
134
|
+
expect(truncatePathLabel('/a/b.md')).toBe('/a/b.md');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('middle-ellipsizes a long path but keeps the basename', () => {
|
|
138
|
+
const label = truncatePathLabel(
|
|
139
|
+
'/Users/markinmatrix/workspace/@projects/cmdop/@docs/positioning/manifest.md',
|
|
140
|
+
);
|
|
141
|
+
expect(label).toContain('manifest.md');
|
|
142
|
+
expect(label).toContain('…');
|
|
143
|
+
// Basename is never cut.
|
|
144
|
+
expect(label.endsWith('manifest.md')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('respects a custom maxChars', () => {
|
|
148
|
+
const label = truncatePathLabel('/very/long/path/to/some/file.md', {
|
|
149
|
+
maxChars: 16,
|
|
150
|
+
});
|
|
151
|
+
expect(label.endsWith('file.md')).toBe(true);
|
|
152
|
+
expect(label).toContain('…');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('keeps the parent folder hint for a long path', () => {
|
|
156
|
+
const label = truncatePathLabel(
|
|
157
|
+
'/Users/markinmatrix/workspace/@projects/cmdop/@docs/positioning/manifest.md',
|
|
158
|
+
);
|
|
159
|
+
expect(label).toContain('positioning');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-path detection + display helpers for the chat editor's "file chip"
|
|
3
|
+
* decoration. Pure, side-effect-free, and unit-testable — no TipTap /
|
|
4
|
+
* ProseMirror / React imports live here so the same functions back the
|
|
5
|
+
* standalone `<FilePathChip>` and the editor decoration alike.
|
|
6
|
+
*
|
|
7
|
+
* Design goal: **LOW false-positive**. We only treat a token as a local
|
|
8
|
+
* file path when it matches an explicit, unambiguous ABSOLUTE shape —
|
|
9
|
+
* never a bare relative token (`./x`, `src/y`) and never a web URL. See
|
|
10
|
+
* `reports/research.md` for the rationale (there is no single universal
|
|
11
|
+
* path regex; detect by safe shapes only).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Match result ──
|
|
15
|
+
|
|
16
|
+
export interface FilePathMatch {
|
|
17
|
+
/** Inclusive start offset into the scanned string. */
|
|
18
|
+
start: number;
|
|
19
|
+
/** Exclusive end offset into the scanned string. */
|
|
20
|
+
end: number;
|
|
21
|
+
/** The matched path text (already trailing-punctuation-trimmed). */
|
|
22
|
+
path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Detector ──
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Trailing sentence punctuation that, when it hangs off the END of a
|
|
29
|
+
* detected path token, is almost certainly prose and not part of the
|
|
30
|
+
* filename — trim it. Real filenames effectively never end in these.
|
|
31
|
+
* (A leading `.` like a dotfile is fine — we only trim the tail.)
|
|
32
|
+
*/
|
|
33
|
+
const TRAILING_PUNCT = new Set([
|
|
34
|
+
'.', ',', ')', ']', '}', ';', ':', '!', '?',
|
|
35
|
+
'«', '»', '"', "'", '”', '’', '…',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The four absolute path shapes, as a single alternation. Each alternative
|
|
40
|
+
* is whitespace-bounded by construction (`\S` runs, no raw spaces). Order
|
|
41
|
+
* matters: more specific schemes (`file://`, UNC, drive) come before the
|
|
42
|
+
* bare Unix root so the longest correct form wins.
|
|
43
|
+
*
|
|
44
|
+
* - `file://…` — `file:` URI (any non-space tail).
|
|
45
|
+
* - `\\server\share…` — Windows UNC (two leading backslashes).
|
|
46
|
+
* - `C:\…` / `C:/…` — Windows drive (single letter + `:` + slash).
|
|
47
|
+
* - `~/…` — home-relative (Unix/macOS).
|
|
48
|
+
* - `/seg/seg…` — Unix/macOS absolute, REQUIRES ≥2 segments so a
|
|
49
|
+
* lone `/` or prose `a / b` never matches.
|
|
50
|
+
*
|
|
51
|
+
* Unicode is allowed inside segments (`@projects`, Cyrillic dirs) — we
|
|
52
|
+
* match on "not whitespace" rather than an ASCII allow-list, then strip
|
|
53
|
+
* trailing punctuation afterwards.
|
|
54
|
+
*/
|
|
55
|
+
const PATH_RE = new RegExp(
|
|
56
|
+
[
|
|
57
|
+
// file:// URI — scheme then a non-space tail.
|
|
58
|
+
String.raw`file:\/\/\S+`,
|
|
59
|
+
// UNC: \\server\share... (need a host char after the two slashes).
|
|
60
|
+
String.raw`\\\\[^\s\\/][^\s]*`,
|
|
61
|
+
// Windows drive: C:\... or C:/... (single drive letter).
|
|
62
|
+
String.raw`[A-Za-z]:[\\/][^\s]*`,
|
|
63
|
+
// Home: ~/...
|
|
64
|
+
String.raw`~[\\/][^\s]+`,
|
|
65
|
+
// Unix/macOS absolute: /seg/seg... — at least TWO segments. A
|
|
66
|
+
// segment is a run of non-space, non-separator chars; we require
|
|
67
|
+
// a separator + segment to repeat at least once after the root.
|
|
68
|
+
String.raw`\/[^\s\\/]+(?:[\\/][^\s\\/]+)+[\\/]?`,
|
|
69
|
+
].join('|'),
|
|
70
|
+
'gu',
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Trim trailing prose punctuation from a raw token. Repeats so multiple
|
|
75
|
+
* trailing marks (`manifest.md).` → `manifest.md`) all come off, but
|
|
76
|
+
* stops as soon as the tail is a normal path char. We never trim into an
|
|
77
|
+
* empty string.
|
|
78
|
+
*/
|
|
79
|
+
function trimTrailingPunct(raw: string): string {
|
|
80
|
+
let end = raw.length;
|
|
81
|
+
while (end > 1 && TRAILING_PUNCT.has(raw[end - 1]!)) {
|
|
82
|
+
end -= 1;
|
|
83
|
+
}
|
|
84
|
+
return raw.slice(0, end);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find every absolute local file path in `text`.
|
|
89
|
+
*
|
|
90
|
+
* Returns matches in document order with `{ start, end, path }` where
|
|
91
|
+
* `[start, end)` are offsets into the ORIGINAL string (after trailing-
|
|
92
|
+
* punctuation trimming, `end` points just past the kept path — the
|
|
93
|
+
* trimmed punctuation is left out of the range so the chip doesn't cover
|
|
94
|
+
* it). Web URLs (`https://x.com/y`), bare relative tokens, and a lone
|
|
95
|
+
* prose slash are intentionally NOT matched.
|
|
96
|
+
*/
|
|
97
|
+
export function findFilePaths(text: string): FilePathMatch[] {
|
|
98
|
+
if (!text) return [];
|
|
99
|
+
|
|
100
|
+
const out: FilePathMatch[] = [];
|
|
101
|
+
PATH_RE.lastIndex = 0;
|
|
102
|
+
let m: RegExpExecArray | null;
|
|
103
|
+
|
|
104
|
+
while ((m = PATH_RE.exec(text)) !== null) {
|
|
105
|
+
const raw = m[0];
|
|
106
|
+
const start = m.index;
|
|
107
|
+
|
|
108
|
+
// Guard against a zero-width match (shouldn't happen with these
|
|
109
|
+
// alternatives, but keep the loop safe).
|
|
110
|
+
if (raw.length === 0) {
|
|
111
|
+
PATH_RE.lastIndex += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Leading-boundary guard. A real path token starts the string or
|
|
116
|
+
// follows whitespace / an opening delimiter. If it's glued onto a
|
|
117
|
+
// preceding non-space char it's a SUBSTRING of a larger token — most
|
|
118
|
+
// importantly a web URL's path (`https://x.com/y/z` → the `/y/z`
|
|
119
|
+
// would otherwise match the Unix-absolute shape). Reject those.
|
|
120
|
+
if (start > 0) {
|
|
121
|
+
const prev = text[start - 1]!;
|
|
122
|
+
if (!/[\s([{<«"'‘“]/.test(prev)) continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const trimmed = trimTrailingPunct(raw);
|
|
126
|
+
// A bare drive root (`C:\`) or `~/` with nothing meaningful is not a
|
|
127
|
+
// useful chip — but the regexes above already require a tail segment,
|
|
128
|
+
// so `trimmed` here is always a real path. Still, skip if trimming
|
|
129
|
+
// collapsed it to a non-path remnant.
|
|
130
|
+
if (trimmed.length < 2) continue;
|
|
131
|
+
|
|
132
|
+
out.push({
|
|
133
|
+
start,
|
|
134
|
+
end: start + trimmed.length,
|
|
135
|
+
path: trimmed,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Split (separator-agnostic) ──
|
|
143
|
+
|
|
144
|
+
export interface SplitPath {
|
|
145
|
+
/** Everything before the basename (no trailing separator). */
|
|
146
|
+
dir: string;
|
|
147
|
+
/** Last non-empty path segment (the file or folder name). */
|
|
148
|
+
base: string;
|
|
149
|
+
/**
|
|
150
|
+
* Heuristic: the path points at a directory rather than a file —
|
|
151
|
+
* either it ends in a separator, or its basename has no file
|
|
152
|
+
* extension (a dotted `manifest.md` reads as a file; `positioning`
|
|
153
|
+
* reads as a dir).
|
|
154
|
+
*/
|
|
155
|
+
isDir: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Strip a `file://` (or `file:`) scheme prefix for display/splitting.
|
|
160
|
+
* `file:///Users/x` → `/Users/x`; `file://host/share` → `host/share`.
|
|
161
|
+
*/
|
|
162
|
+
function stripFileScheme(path: string): string {
|
|
163
|
+
const lower = path.toLowerCase();
|
|
164
|
+
if (lower.startsWith('file://')) {
|
|
165
|
+
// file:///abs → /abs (three slashes: empty host, keep the root)
|
|
166
|
+
// file://host → host
|
|
167
|
+
return path.slice('file://'.length);
|
|
168
|
+
}
|
|
169
|
+
if (lower.startsWith('file:')) return path.slice('file:'.length);
|
|
170
|
+
return path;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** True when `base` looks like it carries a real file extension. */
|
|
174
|
+
function hasExtension(base: string): boolean {
|
|
175
|
+
const dot = base.lastIndexOf('.');
|
|
176
|
+
// No dot, leading dot only (dotfile like `.gitignore` — treat as file),
|
|
177
|
+
// or a trailing dot → decide:
|
|
178
|
+
// - `.gitignore` (dot at 0) IS a file → has "extension".
|
|
179
|
+
// - `name.` (dot last) → not a real extension.
|
|
180
|
+
if (dot === -1) return false;
|
|
181
|
+
if (dot === base.length - 1) return false; // trailing dot
|
|
182
|
+
if (dot === 0) return true; // dotfile
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Split a path into `{ dir, base, isDir }`, separator-agnostic (handles
|
|
188
|
+
* `/` and `\` mixed). The basename is the last NON-EMPTY segment so a
|
|
189
|
+
* trailing separator (`/Users/me/docs/`) still yields `base = "docs"`
|
|
190
|
+
* and `isDir = true`.
|
|
191
|
+
*/
|
|
192
|
+
export function splitPath(path: string): SplitPath {
|
|
193
|
+
const cleaned = stripFileScheme(path);
|
|
194
|
+
const endsWithSep = /[\\/]$/.test(cleaned);
|
|
195
|
+
|
|
196
|
+
// Split on either separator, drop empty segments (collapses `//`,
|
|
197
|
+
// leading root, trailing sep).
|
|
198
|
+
const segments = cleaned.split(/[\\/]+/).filter((s) => s.length > 0);
|
|
199
|
+
const base = segments.length > 0 ? segments[segments.length - 1]! : cleaned;
|
|
200
|
+
const dirSegments = segments.slice(0, -1);
|
|
201
|
+
const dir = dirSegments.join('/');
|
|
202
|
+
|
|
203
|
+
const isDir = endsWithSep || !hasExtension(base);
|
|
204
|
+
|
|
205
|
+
return { dir, base, isDir };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Truncate (Apple/Finder middle-ellipsis) ──
|
|
209
|
+
|
|
210
|
+
export interface TruncateOptions {
|
|
211
|
+
/**
|
|
212
|
+
* Approximate max characters for the label. The basename is ALWAYS
|
|
213
|
+
* kept in full (never cut); only the middle collapses. Default 40.
|
|
214
|
+
*/
|
|
215
|
+
maxChars?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ELLIPSIS = '…';
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Produce a compact, Finder/Chrome-style label for a path: the basename
|
|
222
|
+
* is sacred (always shown in full), the MIDDLE collapses to `…`, and a
|
|
223
|
+
* leading root hint is kept when there's room.
|
|
224
|
+
*
|
|
225
|
+
* Examples (maxChars ≈ 40):
|
|
226
|
+
* /Users/me/dev/proj/positioning/manifest.md
|
|
227
|
+
* → /Users/…/positioning/manifest.md
|
|
228
|
+
* /a/b.md → /a/b.md (already short — unchanged)
|
|
229
|
+
* C:\Users\me\notes.md → C:\…\notes.md (separator preserved? — we
|
|
230
|
+
* normalise display to `/` for compactness)
|
|
231
|
+
*/
|
|
232
|
+
export function truncatePathLabel(path: string, opts: TruncateOptions = {}): string {
|
|
233
|
+
const maxChars = opts.maxChars ?? 40;
|
|
234
|
+
const display = stripFileScheme(path);
|
|
235
|
+
|
|
236
|
+
// Short enough — show as-is.
|
|
237
|
+
if (display.length <= maxChars) return display;
|
|
238
|
+
|
|
239
|
+
const endsWithSep = /[\\/]$/.test(display);
|
|
240
|
+
const segments = display.split(/[\\/]+/).filter((s) => s.length > 0);
|
|
241
|
+
if (segments.length <= 1) {
|
|
242
|
+
// Single segment longer than maxChars — keep the basename intact
|
|
243
|
+
// (CSS ellipsis is the safety net in the chip).
|
|
244
|
+
return display;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const base = segments[segments.length - 1]!;
|
|
248
|
+
const baseLabel = endsWithSep ? `${base}/` : base;
|
|
249
|
+
|
|
250
|
+
// Detect a leading root hint we can preserve:
|
|
251
|
+
// - Unix/macOS absolute → leading "/" + first segment.
|
|
252
|
+
// - Windows drive → "C:".
|
|
253
|
+
// - `~` home → "~".
|
|
254
|
+
let rootHint = '';
|
|
255
|
+
if (/^[A-Za-z]:[\\/]/.test(display)) {
|
|
256
|
+
rootHint = display.slice(0, 2); // "C:"
|
|
257
|
+
} else if (display.startsWith('~')) {
|
|
258
|
+
rootHint = '~';
|
|
259
|
+
} else if (/^[\\/]/.test(display)) {
|
|
260
|
+
rootHint = `/${segments[0]!}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The penultimate segment (parent folder) is a useful hint to keep
|
|
264
|
+
// when room allows — `.../positioning/manifest.md`.
|
|
265
|
+
const parent = segments.length >= 2 ? segments[segments.length - 2]! : '';
|
|
266
|
+
|
|
267
|
+
// Build candidates from most-informative to least, pick the first that
|
|
268
|
+
// fits within maxChars (the basename is never sacrificed).
|
|
269
|
+
const candidates: string[] = [];
|
|
270
|
+
if (rootHint && parent) {
|
|
271
|
+
candidates.push(`${rootHint}/${ELLIPSIS}/${parent}/${baseLabel}`);
|
|
272
|
+
}
|
|
273
|
+
if (parent) {
|
|
274
|
+
candidates.push(`${ELLIPSIS}/${parent}/${baseLabel}`);
|
|
275
|
+
}
|
|
276
|
+
if (rootHint) {
|
|
277
|
+
candidates.push(`${rootHint}/${ELLIPSIS}/${baseLabel}`);
|
|
278
|
+
}
|
|
279
|
+
candidates.push(`${ELLIPSIS}/${baseLabel}`);
|
|
280
|
+
|
|
281
|
+
for (const c of candidates) {
|
|
282
|
+
if (c.length <= maxChars) return c;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Even `…/base` overflows (a very long basename) — return it anyway;
|
|
286
|
+
// the chip's CSS `text-overflow: ellipsis` clips the tail visually
|
|
287
|
+
// while the full path stays in the tooltip.
|
|
288
|
+
return `${ELLIPSIS}/${baseLabel}`;
|
|
289
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { FilePathChip } from './FilePathChip';
|
|
4
|
+
export type { FilePathChipProps } from './FilePathChip';
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
findFilePaths,
|
|
8
|
+
splitPath,
|
|
9
|
+
truncatePathLabel,
|
|
10
|
+
} from './detect';
|
|
11
|
+
export type {
|
|
12
|
+
FilePathMatch,
|
|
13
|
+
SplitPath,
|
|
14
|
+
TruncateOptions,
|
|
15
|
+
} from './detect';
|
|
@@ -8,3 +8,22 @@ export type {
|
|
|
8
8
|
} from './types';
|
|
9
9
|
export { mentionPresets } from './mentionPresets';
|
|
10
10
|
export type { SlashCommandInfo } from './slash/types';
|
|
11
|
+
|
|
12
|
+
// File-path chip — the standalone presentational chip plus the pure
|
|
13
|
+
// detector helpers (usable outside the editor, e.g. rendering a known
|
|
14
|
+
// path in a chat message).
|
|
15
|
+
export { FilePathChip } from './filePath/FilePathChip';
|
|
16
|
+
export type { FilePathChipProps } from './filePath/FilePathChip';
|
|
17
|
+
export { findFilePaths, splitPath, truncatePathLabel } from './filePath/detect';
|
|
18
|
+
export type { FilePathMatch, SplitPath, TruncateOptions } from './filePath/detect';
|
|
19
|
+
|
|
20
|
+
// URL chip — pure detector + display helpers backing the in-editor URL chip
|
|
21
|
+
// (the `editorChip` node's `kind: 'url'`). Usable standalone too.
|
|
22
|
+
export {
|
|
23
|
+
findUrls,
|
|
24
|
+
splitUrl,
|
|
25
|
+
truncateUrlLabel,
|
|
26
|
+
faviconUrl,
|
|
27
|
+
normalizeHref,
|
|
28
|
+
} from './url/detect';
|
|
29
|
+
export type { UrlMatch, SplitUrl, UrlLabelOptions } from './url/detect';
|
|
@@ -54,6 +54,16 @@ export { LazyMarkdownEditor as MarkdownEditor };
|
|
|
54
54
|
// Light surface — pure helpers + types.
|
|
55
55
|
export { mentionPresets } from './mentionPresets';
|
|
56
56
|
|
|
57
|
+
// File-path chip — the standalone presentational chip + the pure detector
|
|
58
|
+
// helpers. The chip is light enough to export eagerly (no TipTap at module
|
|
59
|
+
// scope — it only pulls `FileIcon` + the pure detector), so consumers can
|
|
60
|
+
// render a known path outside the editor without loading the ~200 KB
|
|
61
|
+
// TipTap chunk.
|
|
62
|
+
export { FilePathChip } from './filePath/FilePathChip';
|
|
63
|
+
export type { FilePathChipProps } from './filePath/FilePathChip';
|
|
64
|
+
export { findFilePaths, splitPath, truncatePathLabel } from './filePath/detect';
|
|
65
|
+
export type { FilePathMatch, SplitPath, TruncateOptions } from './filePath/detect';
|
|
66
|
+
|
|
57
67
|
export type {
|
|
58
68
|
MarkdownEditorProps,
|
|
59
69
|
MarkdownEditorHandle,
|
|
@@ -79,6 +79,17 @@
|
|
|
79
79
|
margin: 0.25em 0;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/* The first / last block must hug the editor box — a leading/trailing
|
|
83
|
+
* 0.25em paragraph margin reads as extra air above the first line and a
|
|
84
|
+
* phantom empty line below the caret in the single-line chat composer.
|
|
85
|
+
* Inter-paragraph spacing (between two <p>) is untouched. */
|
|
86
|
+
.markdown-editor .tiptap > :first-child {
|
|
87
|
+
margin-top: 0;
|
|
88
|
+
}
|
|
89
|
+
.markdown-editor .tiptap > :last-child {
|
|
90
|
+
margin-bottom: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
/* Lists — Notion uses tighter row spacing than browser default. */
|
|
83
94
|
.markdown-editor .tiptap ul,
|
|
84
95
|
.markdown-editor .tiptap ol {
|
|
@@ -246,9 +257,40 @@
|
|
|
246
257
|
background: color-mix(in oklab, var(--color-primary, var(--primary)) 25%, transparent);
|
|
247
258
|
}
|
|
248
259
|
|
|
249
|
-
|
|
260
|
+
/* Empty-editor placeholder.
|
|
261
|
+
*
|
|
262
|
+
* TipTap's stock recipe paints the placeholder as a `float: left;
|
|
263
|
+
* height: 0` `::before`. That float reparents the empty paragraph's
|
|
264
|
+
* inline content box, and Chromium then renders the text caret of the
|
|
265
|
+
* (otherwise empty) `<p>` at a zero-height / clipped position behind the
|
|
266
|
+
* float — so the caret is INVISIBLE until the first character is typed
|
|
267
|
+
* (after a space it suddenly blinks). Standard chat composers
|
|
268
|
+
* (ChatGPT / Claude) show a blinking caret in the empty box.
|
|
269
|
+
*
|
|
270
|
+
* Fix: take the placeholder OUT of inline flow — `position: absolute` +
|
|
271
|
+
* `pointer-events: none`. The empty paragraph keeps its natural,
|
|
272
|
+
* full-line-height caret slot (so the caret blinks immediately), and the
|
|
273
|
+
* placeholder text simply overlays the same spot without ever covering
|
|
274
|
+
* the 1px caret (the caret paints on top of the absolutely-positioned,
|
|
275
|
+
* pointer-events-none pseudo). `caret-color` is forced to the foreground
|
|
276
|
+
* so it can never inherit `transparent`. */
|
|
277
|
+
.markdown-editor .tiptap .ProseMirror,
|
|
278
|
+
.markdown-editor .tiptap,
|
|
279
|
+
.markdown-editor .ProseMirror {
|
|
280
|
+
caret-color: var(--color-foreground, var(--foreground));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.markdown-editor .tiptap p.is-editor-empty:first-child,
|
|
284
|
+
.markdown-editor .ProseMirror p.is-editor-empty:first-child {
|
|
285
|
+
position: relative;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.markdown-editor .tiptap p.is-editor-empty:first-child::before,
|
|
289
|
+
.markdown-editor .ProseMirror p.is-editor-empty:first-child::before {
|
|
250
290
|
content: attr(data-placeholder);
|
|
251
|
-
|
|
291
|
+
position: absolute;
|
|
292
|
+
left: 0;
|
|
293
|
+
top: 0;
|
|
252
294
|
pointer-events: none;
|
|
253
295
|
height: 0;
|
|
254
296
|
opacity: 0.4;
|
|
@@ -287,6 +329,96 @@
|
|
|
287
329
|
display: inline;
|
|
288
330
|
}
|
|
289
331
|
|
|
332
|
+
/* ─────────────────────────────────────────────────────────────────────
|
|
333
|
+
Editor chip — the `editorChip` atom NODE. ONE component renders both a
|
|
334
|
+
file-path chip (`--path`) and a URL chip (`--url`); each shows an icon +
|
|
335
|
+
a VSCode-style middle-ellipsis label and serialises back to the raw
|
|
336
|
+
path / URL. Unlike the old decoration, the node OWNS its rendering, so a
|
|
337
|
+
long path collapses to one line (no wrap). It's an atom, so caret never
|
|
338
|
+
lands inside — `user-select: none`, whole-chip delete.
|
|
339
|
+
───────────────────────────────────────────────────────────────────── */
|
|
340
|
+
.markdown-editor .tiptap .editor-chip,
|
|
341
|
+
.markdown-editor .ProseMirror .editor-chip {
|
|
342
|
+
display: inline-flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 3px;
|
|
345
|
+
max-width: 22rem;
|
|
346
|
+
vertical-align: baseline;
|
|
347
|
+
background: var(--color-muted, var(--muted));
|
|
348
|
+
color: var(--color-foreground, var(--foreground));
|
|
349
|
+
border: 1px solid var(--color-border, var(--border));
|
|
350
|
+
border-radius: 4px;
|
|
351
|
+
padding: 0 5px;
|
|
352
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
353
|
+
font-size: 0.85em;
|
|
354
|
+
line-height: 1.45;
|
|
355
|
+
white-space: nowrap;
|
|
356
|
+
text-decoration: none;
|
|
357
|
+
user-select: none;
|
|
358
|
+
cursor: default;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* URL chip is clickable — pointer cursor, but keep the chip look (no blue
|
|
362
|
+
underline; the link accent lives on hover only). */
|
|
363
|
+
.markdown-editor .tiptap a.editor-chip,
|
|
364
|
+
.markdown-editor .ProseMirror a.editor-chip {
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
color: var(--color-foreground, var(--foreground));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.markdown-editor .tiptap a.editor-chip:hover,
|
|
370
|
+
.markdown-editor .ProseMirror a.editor-chip:hover {
|
|
371
|
+
border-color: var(--color-primary, var(--primary));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* The middle-ellipsis label is precomputed; this is the CSS safety net for
|
|
375
|
+
an unusually long basename / slug. */
|
|
376
|
+
.markdown-editor .tiptap .editor-chip__label,
|
|
377
|
+
.markdown-editor .ProseMirror .editor-chip__label {
|
|
378
|
+
overflow: hidden;
|
|
379
|
+
text-overflow: ellipsis;
|
|
380
|
+
white-space: nowrap;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Leading icon box — fixed pixel size so the inner SVG (vendored FileIcon
|
|
384
|
+
markup, sized 100%) doesn't expand to the line box. */
|
|
385
|
+
.markdown-editor .tiptap .editor-chip__icon,
|
|
386
|
+
.markdown-editor .ProseMirror .editor-chip__icon {
|
|
387
|
+
display: inline-flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
justify-content: center;
|
|
390
|
+
width: 14px;
|
|
391
|
+
height: 14px;
|
|
392
|
+
flex: none;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.markdown-editor .tiptap .editor-chip__icon svg,
|
|
396
|
+
.markdown-editor .ProseMirror .editor-chip__icon svg,
|
|
397
|
+
.markdown-editor .tiptap .editor-chip__favicon,
|
|
398
|
+
.markdown-editor .ProseMirror .editor-chip__favicon {
|
|
399
|
+
width: 14px;
|
|
400
|
+
height: 14px;
|
|
401
|
+
display: block;
|
|
402
|
+
flex: none;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.markdown-editor .tiptap .editor-chip__favicon,
|
|
406
|
+
.markdown-editor .ProseMirror .editor-chip__favicon {
|
|
407
|
+
border-radius: 2px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Folder glyph inherits the warning token tint (matches FileIcon). */
|
|
411
|
+
.markdown-editor .tiptap .editor-chip__icon--folder,
|
|
412
|
+
.markdown-editor .ProseMirror .editor-chip__icon--folder {
|
|
413
|
+
color: var(--color-warning, var(--warning, #d99a00));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* URL icon (globe fallback) carries the link/primary accent. */
|
|
417
|
+
.markdown-editor .tiptap .editor-chip__icon--globe,
|
|
418
|
+
.markdown-editor .ProseMirror .editor-chip__icon--globe {
|
|
419
|
+
color: var(--color-primary, var(--primary));
|
|
420
|
+
}
|
|
421
|
+
|
|
290
422
|
/* Mention inline chip */
|
|
291
423
|
.markdown-mention {
|
|
292
424
|
background: var(--color-primary, var(--primary));
|