@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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/package.json +121 -0
  4. package/src/blocks/BlockError.tsx +46 -0
  5. package/src/blocks/index.ts +34 -0
  6. package/src/blocks/schemas.ts +143 -0
  7. package/src/blocks/types.ts +76 -0
  8. package/src/chips/README.md +67 -0
  9. package/src/chips/UrlChip.tsx +105 -0
  10. package/src/chips/index.ts +20 -0
  11. package/src/index.ts +12 -0
  12. package/src/link-preview/LinkPreviewCard.tsx +178 -0
  13. package/src/link-preview/index.ts +12 -0
  14. package/src/link-preview/types.ts +44 -0
  15. package/src/markdown/MarkdownEditor.tsx +700 -0
  16. package/src/markdown/MentionList.tsx +106 -0
  17. package/src/markdown/README.md +269 -0
  18. package/src/markdown/chip/ChipNode.ts +526 -0
  19. package/src/markdown/chip/syncChips.ts +80 -0
  20. package/src/markdown/createMentionSuggestion.ts +131 -0
  21. package/src/markdown/filePath/FilePathChip.tsx +68 -0
  22. package/src/markdown/filePath/detect.ts +289 -0
  23. package/src/markdown/filePath/index.ts +15 -0
  24. package/src/markdown/index.ts +29 -0
  25. package/src/markdown/lazy.tsx +88 -0
  26. package/src/markdown/markdown-editor.stories.tsx +500 -0
  27. package/src/markdown/mentionPresets.ts +49 -0
  28. package/src/markdown/slash/SlashCommandNode.ts +162 -0
  29. package/src/markdown/slash/index.ts +4 -0
  30. package/src/markdown/slash/syncSlashNode.ts +97 -0
  31. package/src/markdown/slash/types.ts +14 -0
  32. package/src/markdown/styles.css +522 -0
  33. package/src/markdown/submitOnEnter.ts +83 -0
  34. package/src/markdown/types.ts +50 -0
  35. package/src/markdown/url/detect.ts +227 -0
  36. package/src/notion/BookmarkNode.ts +161 -0
  37. package/src/notion/BookmarkView.tsx +45 -0
  38. package/src/notion/CustomKeymap.ts +48 -0
  39. package/src/notion/LinkDialog.tsx +133 -0
  40. package/src/notion/MapEditLayer.tsx +199 -0
  41. package/src/notion/MapNode.ts +220 -0
  42. package/src/notion/MapView.tsx +103 -0
  43. package/src/notion/NotionEditor.tsx +309 -0
  44. package/src/notion/README.md +158 -0
  45. package/src/notion/SlashExtension.ts +32 -0
  46. package/src/notion/SlashList.tsx +136 -0
  47. package/src/notion/TaskItemView.tsx +41 -0
  48. package/src/notion/createSlashSuggestion.ts +121 -0
  49. package/src/notion/extensions.ts +120 -0
  50. package/src/notion/index.ts +1 -0
  51. package/src/notion/lazy.tsx +44 -0
  52. package/src/notion/notion-editor.stories.tsx +293 -0
  53. package/src/notion/slashItems.ts +178 -0
  54. package/src/notion/styles.css +503 -0
  55. package/src/notion/types.ts +37 -0
  56. package/src/tiptap/index.ts +10 -0
  57. package/src/tiptap/useNodeAttrs.ts +128 -0
@@ -0,0 +1,4 @@
1
+ export { SlashCommandNode } from './SlashCommandNode';
2
+ export type { SlashCommandNodeOptions } from './SlashCommandNode';
3
+ export { syncLeadingSlashNode } from './syncSlashNode';
4
+ export type { SlashCommandInfo } from './types';
@@ -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
+ }