@djangocfg/widget-editor 0.1.1
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/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +121 -0
- package/src/blocks/BlockError.tsx +46 -0
- package/src/blocks/index.ts +34 -0
- package/src/blocks/schemas.ts +143 -0
- package/src/blocks/types.ts +76 -0
- package/src/chips/README.md +67 -0
- package/src/chips/UrlChip.tsx +105 -0
- package/src/chips/index.ts +20 -0
- package/src/index.ts +12 -0
- package/src/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/link-preview/index.ts +12 -0
- package/src/link-preview/types.ts +44 -0
- package/src/markdown/MarkdownEditor.tsx +700 -0
- package/src/markdown/MentionList.tsx +106 -0
- package/src/markdown/README.md +269 -0
- package/src/markdown/chip/ChipNode.ts +526 -0
- package/src/markdown/chip/syncChips.ts +80 -0
- package/src/markdown/createMentionSuggestion.ts +131 -0
- package/src/markdown/filePath/FilePathChip.tsx +68 -0
- package/src/markdown/filePath/detect.ts +289 -0
- package/src/markdown/filePath/index.ts +15 -0
- package/src/markdown/index.ts +29 -0
- package/src/markdown/lazy.tsx +88 -0
- package/src/markdown/markdown-editor.stories.tsx +500 -0
- package/src/markdown/mentionPresets.ts +49 -0
- package/src/markdown/slash/SlashCommandNode.ts +162 -0
- package/src/markdown/slash/index.ts +4 -0
- package/src/markdown/slash/syncSlashNode.ts +97 -0
- package/src/markdown/slash/types.ts +14 -0
- package/src/markdown/styles.css +522 -0
- package/src/markdown/submitOnEnter.ts +83 -0
- package/src/markdown/types.ts +50 -0
- package/src/markdown/url/detect.ts +227 -0
- package/src/notion/BookmarkNode.ts +161 -0
- package/src/notion/BookmarkView.tsx +45 -0
- package/src/notion/CustomKeymap.ts +48 -0
- package/src/notion/LinkDialog.tsx +133 -0
- package/src/notion/MapEditLayer.tsx +199 -0
- package/src/notion/MapNode.ts +220 -0
- package/src/notion/MapView.tsx +103 -0
- package/src/notion/NotionEditor.tsx +309 -0
- package/src/notion/README.md +158 -0
- package/src/notion/SlashExtension.ts +32 -0
- package/src/notion/SlashList.tsx +136 -0
- package/src/notion/TaskItemView.tsx +41 -0
- package/src/notion/createSlashSuggestion.ts +121 -0
- package/src/notion/extensions.ts +120 -0
- package/src/notion/index.ts +1 -0
- package/src/notion/lazy.tsx +44 -0
- package/src/notion/notion-editor.stories.tsx +293 -0
- package/src/notion/slashItems.ts +178 -0
- package/src/notion/styles.css +503 -0
- package/src/notion/types.ts +37 -0
- package/src/tiptap/index.ts +10 -0
- package/src/tiptap/useNodeAttrs.ts +128 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `syncSlashNode` — bridge between the slash hook's string buffer and
|
|
3
|
+
* the TipTap document's node form.
|
|
4
|
+
*
|
|
5
|
+
* The slash hook lives entirely on the editor value string — it has no
|
|
6
|
+
* concept of nodes. When the host wires a TipTap-backed
|
|
7
|
+
* `<ComposerRichTextarea>` with `slashCommands`, the document needs to
|
|
8
|
+
* pick up two things on its own:
|
|
9
|
+
*
|
|
10
|
+
* 1. After `setContent(newValue)` runs (e.g. the user picked `/verb`
|
|
11
|
+
* from the menu so `composer.value` became `"/verb "`), the
|
|
12
|
+
* leading `/verb` text must turn into a `SlashCommandNode` atom
|
|
13
|
+
* so the chip shows up.
|
|
14
|
+
* 2. If the user backspaces past the chip or types non-slash text,
|
|
15
|
+
* the buffer is no longer a slash command — there's nothing to
|
|
16
|
+
* do because the atom would have been removed wholesale by the
|
|
17
|
+
* backspace.
|
|
18
|
+
*
|
|
19
|
+
* This module owns the post-`setContent` "absorb the leading slash"
|
|
20
|
+
* pass. It is the only piece of TipTap-aware code the slash node needs
|
|
21
|
+
* beyond the node definition itself.
|
|
22
|
+
*/
|
|
23
|
+
import type { Editor } from '@tiptap/react';
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
SlashCommandInfo,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find the leading `/verb` in the document — if and only if it appears
|
|
31
|
+
* as the very first inline text of the first paragraph. Returns
|
|
32
|
+
* `{ verb, from, to }` (`from` / `to` are absolute document positions
|
|
33
|
+
* usable with `insertContentAt`) or `null` when no leading slash is
|
|
34
|
+
* present.
|
|
35
|
+
*/
|
|
36
|
+
function findLeadingSlashText(
|
|
37
|
+
editor: Editor,
|
|
38
|
+
commands: readonly SlashCommandInfo[],
|
|
39
|
+
): { verb: SlashCommandInfo; from: number; to: number } | null {
|
|
40
|
+
const doc = editor.state.doc;
|
|
41
|
+
if (doc.childCount === 0) return null;
|
|
42
|
+
// Walk the document's first inline block. The buffer must START with
|
|
43
|
+
// the slash — verbs in the middle of the message are not slash
|
|
44
|
+
// commands (matches `parseSlashState` semantics).
|
|
45
|
+
const firstBlock = doc.firstChild;
|
|
46
|
+
if (!firstBlock || firstBlock.childCount === 0) return null;
|
|
47
|
+
const firstInline = firstBlock.firstChild;
|
|
48
|
+
if (!firstInline) return null;
|
|
49
|
+
// If the first inline is already a slash atom we're done.
|
|
50
|
+
if (firstInline.type.name === 'slashCommand') return null;
|
|
51
|
+
if (!firstInline.isText) return null;
|
|
52
|
+
const text = firstInline.text ?? '';
|
|
53
|
+
// Mirrors SLASH_RE from ../../../chat/composer/slash/state.ts. Kept
|
|
54
|
+
// inline to avoid pulling the chat package into the editor module.
|
|
55
|
+
const m = /^\/([a-zA-Z][\w-]*)(?:[ \n]|$)/.exec(text);
|
|
56
|
+
if (!m) return null;
|
|
57
|
+
const verb = m[1]?.toLowerCase() ?? '';
|
|
58
|
+
const cmd = commands.find((c) => c.id === verb);
|
|
59
|
+
if (!cmd) return null;
|
|
60
|
+
const slashLen = 1 + (m[1]?.length ?? 0); // `/verb` length, no trailing
|
|
61
|
+
// First text child of the first block starts at position 1 in the
|
|
62
|
+
// document (the block boundary takes position 0).
|
|
63
|
+
const from = 1;
|
|
64
|
+
const to = from + slashLen;
|
|
65
|
+
return { verb: cmd, from, to };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* If the document starts with a `/verb` text node whose verb is in
|
|
70
|
+
* `commands`, replace that text with a `SlashCommandNode` atom. No-op
|
|
71
|
+
* otherwise. Idempotent — calling it on a document that already starts
|
|
72
|
+
* with the atom does nothing.
|
|
73
|
+
*
|
|
74
|
+
* Returns `true` when a replacement happened (so callers can suppress
|
|
75
|
+
* a feedback `onUpdate` if needed).
|
|
76
|
+
*/
|
|
77
|
+
export function syncLeadingSlashNode(
|
|
78
|
+
editor: Editor,
|
|
79
|
+
commands: readonly SlashCommandInfo[],
|
|
80
|
+
): boolean {
|
|
81
|
+
if (!editor || editor.isDestroyed) return false;
|
|
82
|
+
if (commands.length === 0) return false;
|
|
83
|
+
const found = findLeadingSlashText(editor, commands);
|
|
84
|
+
if (!found) return false;
|
|
85
|
+
const { verb, from, to } = found;
|
|
86
|
+
editor
|
|
87
|
+
.chain()
|
|
88
|
+
.insertContentAt(
|
|
89
|
+
{ from, to },
|
|
90
|
+
{
|
|
91
|
+
type: 'slashCommand',
|
|
92
|
+
attrs: { id: verb.id, token: verb.token },
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
.run();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-side slash command info.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately tiny and owned here: the editor's slash menu needs a verb and a
|
|
5
|
+
* token, nothing more. It was once described as a structural subset of the chat
|
|
6
|
+
* package's `SlashCommand` — that type is gone (the chat dropped slash commands
|
|
7
|
+
* when it was generalised for web), and the shape stands on its own.
|
|
8
|
+
*/
|
|
9
|
+
export interface SlashCommandInfo {
|
|
10
|
+
/** Verb without the leading slash (e.g. `"clear"`). */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Display token with the slash (e.g. `"/clear"`). */
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
.markdown-editor .markdown-editor-content {
|
|
2
|
+
outline: none;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/* ─────────────────────────────────────────────────────────────────────
|
|
6
|
+
* Typography — Notion-inspired: comfy line-height, generous heading
|
|
7
|
+
* scale, restrained spacing. Calibrated so the chat composer (compact,
|
|
8
|
+
* single-paragraph) and the document-preview editor (long-form, with
|
|
9
|
+
* many headings) both feel right without a variant prop.
|
|
10
|
+
*
|
|
11
|
+
* Inherit semantic foreground so the editor renders correctly in both
|
|
12
|
+
* light + dark themes (and under any active preset).
|
|
13
|
+
* ───────────────────────────────────────────────────────────────────── */
|
|
14
|
+
.markdown-editor .tiptap,
|
|
15
|
+
.markdown-editor .ProseMirror {
|
|
16
|
+
color: var(--color-foreground, var(--foreground));
|
|
17
|
+
font-family:
|
|
18
|
+
-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto,
|
|
19
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
20
|
+
font-size: 15px;
|
|
21
|
+
line-height: 1.6;
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
-moz-osx-font-smoothing: grayscale;
|
|
24
|
+
text-rendering: optimizeLegibility;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.markdown-editor .tiptap a,
|
|
28
|
+
.markdown-editor .ProseMirror a {
|
|
29
|
+
color: var(--color-primary, var(--primary));
|
|
30
|
+
text-decoration: underline;
|
|
31
|
+
text-underline-offset: 2px;
|
|
32
|
+
text-decoration-thickness: 1px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.markdown-editor .tiptap a:hover,
|
|
36
|
+
.markdown-editor .ProseMirror a:hover {
|
|
37
|
+
text-decoration-thickness: 2px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Headings — Notion scale. Top-padding > bottom so a heading sits
|
|
41
|
+
* visually grouped with the paragraph below it. */
|
|
42
|
+
.markdown-editor .tiptap h1 {
|
|
43
|
+
font-size: 1.875em;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
line-height: 1.25;
|
|
46
|
+
margin: 1em 0 0.25em;
|
|
47
|
+
letter-spacing: -0.015em;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.markdown-editor .tiptap h2 {
|
|
51
|
+
font-size: 1.5em;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
line-height: 1.3;
|
|
54
|
+
margin: 0.9em 0 0.25em;
|
|
55
|
+
letter-spacing: -0.01em;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.markdown-editor .tiptap h3 {
|
|
59
|
+
font-size: 1.25em;
|
|
60
|
+
font-weight: 600;
|
|
61
|
+
line-height: 1.35;
|
|
62
|
+
margin: 0.8em 0 0.2em;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.markdown-editor .tiptap h4 {
|
|
66
|
+
font-size: 1.1em;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
line-height: 1.4;
|
|
69
|
+
margin: 0.7em 0 0.2em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* First heading at the very top of the document has no leading air —
|
|
73
|
+
* Notion convention. */
|
|
74
|
+
.markdown-editor .tiptap > :is(h1, h2, h3, h4):first-child {
|
|
75
|
+
margin-top: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.markdown-editor .tiptap p {
|
|
79
|
+
margin: 0.25em 0;
|
|
80
|
+
}
|
|
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
|
+
|
|
93
|
+
/* Lists — Notion uses tighter row spacing than browser default. */
|
|
94
|
+
.markdown-editor .tiptap ul,
|
|
95
|
+
.markdown-editor .tiptap ol {
|
|
96
|
+
padding-left: 1.5em;
|
|
97
|
+
margin: 0.35em 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.markdown-editor .tiptap ul {
|
|
101
|
+
list-style: disc;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.markdown-editor .tiptap ol {
|
|
105
|
+
list-style: decimal;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.markdown-editor .tiptap li {
|
|
109
|
+
margin: 0.15em 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.markdown-editor .tiptap li > p {
|
|
113
|
+
margin: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Nested lists get a subtler marker. */
|
|
117
|
+
.markdown-editor .tiptap ul ul {
|
|
118
|
+
list-style: circle;
|
|
119
|
+
}
|
|
120
|
+
.markdown-editor .tiptap ul ul ul {
|
|
121
|
+
list-style: square;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Task list (GFM `- [ ] item`). StarterKit ships TaskList extension. */
|
|
125
|
+
.markdown-editor .tiptap ul[data-type="taskList"] {
|
|
126
|
+
list-style: none;
|
|
127
|
+
padding-left: 0.25em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: flex-start;
|
|
133
|
+
gap: 0.5em;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li > label {
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
margin-top: 0.3em;
|
|
139
|
+
user-select: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li > div {
|
|
143
|
+
flex: 1;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li[data-checked="true"] > div {
|
|
148
|
+
opacity: 0.55;
|
|
149
|
+
text-decoration: line-through;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Blockquote — Notion uses a chunky left bar without italic. */
|
|
153
|
+
.markdown-editor .tiptap blockquote {
|
|
154
|
+
border-left: 3px solid var(--color-border, var(--border));
|
|
155
|
+
padding: 0.2em 0 0.2em 1em;
|
|
156
|
+
margin: 0.6em 0;
|
|
157
|
+
color: color-mix(in oklab, var(--color-foreground, var(--foreground)) 80%, transparent);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.markdown-editor .tiptap blockquote > :first-child {
|
|
161
|
+
margin-top: 0;
|
|
162
|
+
}
|
|
163
|
+
.markdown-editor .tiptap blockquote > :last-child {
|
|
164
|
+
margin-bottom: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Divider — full-width thin rule with breathing room. */
|
|
168
|
+
.markdown-editor .tiptap hr {
|
|
169
|
+
border: none;
|
|
170
|
+
border-top: 1px solid var(--color-border, var(--border));
|
|
171
|
+
margin: 1.5em 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Inline code — monospace pill, restrained tint. */
|
|
175
|
+
.markdown-editor .tiptap code {
|
|
176
|
+
background: color-mix(in oklab, var(--color-muted, var(--muted)) 70%, transparent);
|
|
177
|
+
color: var(--color-foreground, var(--foreground));
|
|
178
|
+
padding: 0.15em 0.35em;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
font-size: 0.875em;
|
|
181
|
+
font-family:
|
|
182
|
+
"SF Mono", ui-monospace, "JetBrains Mono", "Fira Code", Menlo, Consolas,
|
|
183
|
+
monospace;
|
|
184
|
+
border: 1px solid color-mix(in oklab, var(--color-border, var(--border)) 60%, transparent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Code block — darker surface so it stands apart from inline code. */
|
|
188
|
+
.markdown-editor .tiptap pre {
|
|
189
|
+
background: var(--color-muted, var(--muted));
|
|
190
|
+
color: var(--color-foreground, var(--foreground));
|
|
191
|
+
padding: 0.9em 1em;
|
|
192
|
+
border-radius: 6px;
|
|
193
|
+
margin: 0.75em 0;
|
|
194
|
+
font-family:
|
|
195
|
+
"SF Mono", ui-monospace, "JetBrains Mono", "Fira Code", Menlo, Consolas,
|
|
196
|
+
monospace;
|
|
197
|
+
font-size: 0.875em;
|
|
198
|
+
line-height: 1.55;
|
|
199
|
+
overflow-x: auto;
|
|
200
|
+
border: 1px solid color-mix(in oklab, var(--color-border, var(--border)) 50%, transparent);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.markdown-editor .tiptap pre code {
|
|
204
|
+
background: transparent;
|
|
205
|
+
border: none;
|
|
206
|
+
padding: 0;
|
|
207
|
+
border-radius: 0;
|
|
208
|
+
font-size: inherit;
|
|
209
|
+
color: inherit;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Tables — borderless, subtle row separators (Notion-flavour). */
|
|
213
|
+
.markdown-editor .tiptap table {
|
|
214
|
+
border-collapse: collapse;
|
|
215
|
+
width: 100%;
|
|
216
|
+
margin: 0.75em 0;
|
|
217
|
+
font-size: 0.95em;
|
|
218
|
+
table-layout: fixed;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.markdown-editor .tiptap th,
|
|
222
|
+
.markdown-editor .tiptap td {
|
|
223
|
+
border: 1px solid var(--color-border, var(--border));
|
|
224
|
+
padding: 0.45em 0.75em;
|
|
225
|
+
vertical-align: top;
|
|
226
|
+
text-align: left;
|
|
227
|
+
min-width: 4ch;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.markdown-editor .tiptap th {
|
|
231
|
+
background: color-mix(in oklab, var(--color-muted, var(--muted)) 60%, transparent);
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Images — rounded, never overflow. */
|
|
236
|
+
.markdown-editor .tiptap img {
|
|
237
|
+
max-width: 100%;
|
|
238
|
+
height: auto;
|
|
239
|
+
border-radius: 6px;
|
|
240
|
+
margin: 0.5em 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.markdown-editor .tiptap strong {
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.markdown-editor .tiptap em {
|
|
248
|
+
font-style: italic;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.markdown-editor .tiptap s {
|
|
252
|
+
text-decoration: line-through;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Selection highlight — semantic ring colour, not browser-blue. */
|
|
256
|
+
.markdown-editor .tiptap ::selection {
|
|
257
|
+
background: color-mix(in oklab, var(--color-primary, var(--primary)) 25%, transparent);
|
|
258
|
+
}
|
|
259
|
+
|
|
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 {
|
|
290
|
+
content: attr(data-placeholder);
|
|
291
|
+
position: absolute;
|
|
292
|
+
left: 0;
|
|
293
|
+
top: 0;
|
|
294
|
+
pointer-events: none;
|
|
295
|
+
height: 0;
|
|
296
|
+
opacity: 0.4;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Focus ring */
|
|
300
|
+
.markdown-editor:focus-within {
|
|
301
|
+
border-color: var(--color-ring, var(--ring));
|
|
302
|
+
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-ring, var(--ring)) 30%, transparent);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Embedded mode — the host surface (e.g. chat composer) draws the
|
|
306
|
+
frame, so the editor must not paint its own border or focus ring. */
|
|
307
|
+
.markdown-editor--unstyled,
|
|
308
|
+
.markdown-editor--unstyled:focus-within {
|
|
309
|
+
border: none;
|
|
310
|
+
background: transparent;
|
|
311
|
+
box-shadow: none;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Slash-command inline chip — TipTap atom analogue of the plain
|
|
315
|
+
`<SlashHighlightTextarea>` mirror. Mirrors `bg-primary/15 text-primary`
|
|
316
|
+
so the two paths look identical. Atom nodes don't admit a caret, so
|
|
317
|
+
light horizontal padding is safe here (unlike the plain mirror,
|
|
318
|
+
which has to be padding-less to keep caret alignment). */
|
|
319
|
+
.markdown-slash-command {
|
|
320
|
+
background: color-mix(in oklab, var(--color-primary, var(--primary)) 15%, transparent);
|
|
321
|
+
color: var(--color-primary, var(--primary));
|
|
322
|
+
padding: 0 2px;
|
|
323
|
+
border-radius: 3px;
|
|
324
|
+
font-weight: 500;
|
|
325
|
+
/* Atom nodes can't be entered; reflect that visually so the user does
|
|
326
|
+
not try to click into the chip to edit it. */
|
|
327
|
+
cursor: default;
|
|
328
|
+
user-select: none;
|
|
329
|
+
display: inline;
|
|
330
|
+
}
|
|
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
|
+
|
|
422
|
+
/* Mention inline chip */
|
|
423
|
+
.markdown-mention {
|
|
424
|
+
background: var(--color-primary, var(--primary));
|
|
425
|
+
color: var(--color-primary-foreground, #fff);
|
|
426
|
+
padding: 1px 6px;
|
|
427
|
+
border-radius: 4px;
|
|
428
|
+
font-size: 0.9em;
|
|
429
|
+
font-weight: 500;
|
|
430
|
+
cursor: default;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Mention dropdown */
|
|
434
|
+
.markdown-mention-list {
|
|
435
|
+
background: var(--color-popover, var(--popover));
|
|
436
|
+
color: var(--color-popover-foreground, var(--popover-foreground));
|
|
437
|
+
border: 1px solid var(--color-border, var(--border));
|
|
438
|
+
border-radius: 8px;
|
|
439
|
+
padding: 4px;
|
|
440
|
+
min-width: 220px;
|
|
441
|
+
max-height: 260px;
|
|
442
|
+
overflow-y: auto;
|
|
443
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.markdown-mention-item {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
gap: 8px;
|
|
450
|
+
width: 100%;
|
|
451
|
+
padding: 6px 8px;
|
|
452
|
+
border: none;
|
|
453
|
+
background: transparent;
|
|
454
|
+
border-radius: 4px;
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
text-align: left;
|
|
457
|
+
font-size: 13px;
|
|
458
|
+
transition: background 0.1s;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.markdown-mention-item:hover,
|
|
462
|
+
.markdown-mention-item.selected {
|
|
463
|
+
background: var(--color-muted, var(--muted));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.markdown-mention-avatar {
|
|
467
|
+
width: 24px;
|
|
468
|
+
height: 24px;
|
|
469
|
+
border-radius: 50%;
|
|
470
|
+
object-fit: cover;
|
|
471
|
+
flex-shrink: 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.markdown-mention-info {
|
|
475
|
+
display: flex;
|
|
476
|
+
flex-direction: column;
|
|
477
|
+
min-width: 0;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.markdown-mention-name {
|
|
481
|
+
font-weight: 500;
|
|
482
|
+
white-space: nowrap;
|
|
483
|
+
overflow: hidden;
|
|
484
|
+
text-overflow: ellipsis;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.markdown-mention-desc {
|
|
488
|
+
font-size: 11px;
|
|
489
|
+
opacity: 0.6;
|
|
490
|
+
white-space: nowrap;
|
|
491
|
+
overflow: hidden;
|
|
492
|
+
text-overflow: ellipsis;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* Toolbar buttons */
|
|
496
|
+
.markdown-toolbar-btn {
|
|
497
|
+
display: flex;
|
|
498
|
+
align-items: center;
|
|
499
|
+
justify-content: center;
|
|
500
|
+
width: 28px;
|
|
501
|
+
height: 28px;
|
|
502
|
+
border-radius: 4px;
|
|
503
|
+
border: none;
|
|
504
|
+
background: transparent;
|
|
505
|
+
/* Explicit semantic colour — without it the icon inherits the host's
|
|
506
|
+
default text colour (UA black on a dark editor surface) and the
|
|
507
|
+
toolbar is invisible. */
|
|
508
|
+
color: var(--foreground);
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
opacity: 0.5;
|
|
511
|
+
transition: opacity 0.15s, background 0.15s;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.markdown-toolbar-btn:hover {
|
|
515
|
+
opacity: 1;
|
|
516
|
+
background: var(--color-muted, var(--muted));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.markdown-toolbar-btn.active {
|
|
520
|
+
opacity: 1;
|
|
521
|
+
background: var(--color-muted, var(--muted));
|
|
522
|
+
}
|