@nucel/ui 0.10.0 → 0.12.0

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 (30) hide show
  1. package/README.md +21 -1
  2. package/package.json +29 -5
  3. package/src/lib/components/ui/Alert.svelte +47 -0
  4. package/src/lib/components/ui/AppCard.svelte +76 -0
  5. package/src/lib/components/ui/BranchPill.svelte +19 -0
  6. package/src/lib/components/ui/CommentPill.svelte +12 -0
  7. package/src/lib/components/ui/KanbanBoard.svelte +27 -0
  8. package/src/lib/components/ui/KanbanCard.svelte +43 -0
  9. package/src/lib/components/ui/KanbanColumn.svelte +52 -0
  10. package/src/lib/components/ui/ListCard.svelte +9 -0
  11. package/src/lib/components/ui/PageHeader.svelte +25 -0
  12. package/src/lib/components/ui/PermissionChips.svelte +49 -0
  13. package/src/lib/components/ui/Section.svelte +21 -0
  14. package/src/lib/components/ui/SectionTitle.svelte +16 -0
  15. package/src/lib/components/ui/StatusPill.svelte +54 -0
  16. package/src/lib/components/ui/editor/RichEditor.svelte +580 -0
  17. package/src/lib/components/ui/editor/mention-suggestion.ts +144 -0
  18. package/src/lib/components/ui/table/Table.svelte +12 -0
  19. package/src/lib/components/ui/table/TableBody.svelte +10 -0
  20. package/src/lib/components/ui/table/TableCaption.svelte +10 -0
  21. package/src/lib/components/ui/table/TableCell.svelte +10 -0
  22. package/src/lib/components/ui/table/TableHead.svelte +10 -0
  23. package/src/lib/components/ui/table/TableHeader.svelte +10 -0
  24. package/src/lib/components/ui/table/TableRow.svelte +10 -0
  25. package/src/lib/components/ui/table/index.ts +7 -0
  26. package/src/lib/index.ts +50 -0
  27. package/src/lib/components/ColorInput.test.ts +0 -126
  28. package/src/lib/components/CopyButton.test.ts +0 -213
  29. package/src/lib/components/IconButton.test.ts +0 -139
  30. package/src/lib/utils/cn.test.ts +0 -993
@@ -0,0 +1,580 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { Editor } from '@tiptap/core';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import Placeholder from '@tiptap/extension-placeholder';
6
+ import Link from '@tiptap/extension-link';
7
+ import Underline from '@tiptap/extension-underline';
8
+ import TextAlign from '@tiptap/extension-text-align';
9
+ import { TextStyle } from '@tiptap/extension-text-style';
10
+ import { Color } from '@tiptap/extension-color';
11
+ import Highlight from '@tiptap/extension-highlight';
12
+ import Typography from '@tiptap/extension-typography';
13
+ import Subscript from '@tiptap/extension-subscript';
14
+ import Superscript from '@tiptap/extension-superscript';
15
+ import CharacterCount from '@tiptap/extension-character-count';
16
+ import { Table } from '@tiptap/extension-table';
17
+ import { TableRow } from '@tiptap/extension-table-row';
18
+ import { TableCell } from '@tiptap/extension-table-cell';
19
+ import { TableHeader } from '@tiptap/extension-table-header';
20
+ import TaskList from '@tiptap/extension-task-list';
21
+ import TaskItem from '@tiptap/extension-task-item';
22
+ import Mention from '@tiptap/extension-mention';
23
+ import { createMentionSuggestion } from './mention-suggestion.js';
24
+
25
+ /**
26
+ * mode:
27
+ * 'full' — wiki editor: full toolbar + all extensions
28
+ * 'standard' — issues/PRs: toolbar without table controls
29
+ * 'minimal' — comments: no toolbar, just formatting via shortcuts
30
+ */
31
+ type Mode = 'full' | 'standard' | 'minimal';
32
+
33
+ type Props = {
34
+ content?: Record<string, unknown> | null;
35
+ placeholder?: string;
36
+ mode?: Mode;
37
+ bordered?: boolean;
38
+ mentionsUrl?: string | null;
39
+ onUpdate?: (json: Record<string, unknown>, html: string) => void;
40
+ onSubmit?: () => void;
41
+ class?: string;
42
+ autofocus?: boolean;
43
+ };
44
+
45
+ let {
46
+ content = null,
47
+ placeholder = 'Write something…',
48
+ mode = 'standard',
49
+ bordered = true,
50
+ mentionsUrl = null,
51
+ onUpdate,
52
+ onSubmit,
53
+ class: className = '',
54
+ autofocus = false,
55
+ }: Props = $props();
56
+
57
+ let element: HTMLDivElement;
58
+ let editor = $state<Editor | undefined>(undefined);
59
+ let wordCount = $state(0);
60
+
61
+ // Active state for toolbar
62
+ let isBold = $state(false);
63
+ let isItalic = $state(false);
64
+ let isUnderline = $state(false);
65
+ let isStrike = $state(false);
66
+ let isCode = $state(false);
67
+ let isBlockquote = $state(false);
68
+ let isCodeBlock = $state(false);
69
+ let isBulletList = $state(false);
70
+ let isOrderedList = $state(false);
71
+ let isTaskList = $state(false);
72
+ let isHighlight = $state(false);
73
+ let isLink = $state(false);
74
+ let currentHeading = $state<number | null>(null);
75
+ let showHeadingMenu = $state(false);
76
+ let showLinkPopover = $state(false);
77
+ let linkUrl = $state('');
78
+
79
+ function sync() {
80
+ if (!editor) return;
81
+ isBold = editor.isActive('bold');
82
+ isItalic = editor.isActive('italic');
83
+ isUnderline = editor.isActive('underline');
84
+ isStrike = editor.isActive('strike');
85
+ isCode = editor.isActive('code');
86
+ isBlockquote = editor.isActive('blockquote');
87
+ isCodeBlock = editor.isActive('codeBlock');
88
+ isBulletList = editor.isActive('bulletList');
89
+ isOrderedList = editor.isActive('orderedList');
90
+ isTaskList = editor.isActive('taskList');
91
+ isHighlight = editor.isActive('highlight');
92
+ isLink = editor.isActive('link');
93
+ currentHeading = editor.isActive('heading', { level: 1 }) ? 1
94
+ : editor.isActive('heading', { level: 2 }) ? 2
95
+ : editor.isActive('heading', { level: 3 }) ? 3
96
+ : null;
97
+ }
98
+
99
+ onMount(() => {
100
+ const extensions: any[] = [
101
+ StarterKit.configure({ link: false, codeBlock: false }),
102
+ Placeholder.configure({ placeholder }),
103
+ Link.configure({ openOnClick: false }),
104
+ Underline,
105
+ TextStyle,
106
+ Color,
107
+ Highlight.configure({ multicolor: false }),
108
+ Typography,
109
+ Subscript,
110
+ Superscript,
111
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
112
+ TaskList,
113
+ TaskItem.configure({ nested: true }),
114
+ CharacterCount,
115
+ ];
116
+
117
+ if (mode === 'full') {
118
+ extensions.push(
119
+ Table.configure({ resizable: true }),
120
+ TableRow,
121
+ TableHeader,
122
+ TableCell,
123
+ );
124
+ }
125
+
126
+ if (mentionsUrl) {
127
+ extensions.push(
128
+ Mention.configure({
129
+ HTMLAttributes: { class: 'mention' },
130
+ renderHTML: ({ node }: any) => ['span', {
131
+ class: 'mention bg-primary/10 text-primary px-1 py-0.5 rounded text-sm font-medium cursor-pointer',
132
+ 'data-id': node.attrs.id,
133
+ 'data-type': node.attrs.type,
134
+ }, `@${node.attrs.label}`],
135
+ suggestion: createMentionSuggestion(mentionsUrl),
136
+ })
137
+ );
138
+ }
139
+
140
+ editor = new Editor({
141
+ element,
142
+ extensions,
143
+ content: content ?? '',
144
+ autofocus: autofocus ? 'end' : false,
145
+ onUpdate({ editor }) {
146
+ wordCount = editor.storage.characterCount.words();
147
+ sync();
148
+ onUpdate?.(editor.getJSON() as Record<string, unknown>, editor.getHTML());
149
+ },
150
+ onSelectionUpdate() { sync(); },
151
+ onCreate({ editor }) {
152
+ wordCount = editor.storage.characterCount.words();
153
+ sync();
154
+ },
155
+ });
156
+ });
157
+
158
+ onDestroy(() => editor?.destroy());
159
+
160
+ function cmd(fn: () => void) {
161
+ fn();
162
+ editor?.view.focus();
163
+ sync();
164
+ }
165
+
166
+ function setHeading(level: 0 | 1 | 2 | 3) {
167
+ showHeadingMenu = false;
168
+ if (!editor) return;
169
+ if (level === 0) editor.chain().focus().setParagraph().run();
170
+ else editor.chain().focus().toggleHeading({ level }).run();
171
+ sync();
172
+ }
173
+
174
+ function openLink() {
175
+ if (!editor) return;
176
+ linkUrl = editor.getAttributes('link').href ?? '';
177
+ showLinkPopover = true;
178
+ showHeadingMenu = false;
179
+ }
180
+
181
+ function applyLink() {
182
+ if (!editor) return;
183
+ if (linkUrl.trim()) editor.chain().focus().setLink({ href: linkUrl.trim() }).run();
184
+ else editor.chain().focus().unsetLink().run();
185
+ showLinkPopover = false;
186
+ linkUrl = '';
187
+ sync();
188
+ }
189
+
190
+ const headingLabel = $derived(
191
+ currentHeading === 1 ? 'H1' : currentHeading === 2 ? 'H2' : currentHeading === 3 ? 'H3' : 'T'
192
+ );
193
+
194
+ const showToolbar = $derived(mode !== 'minimal');
195
+ </script>
196
+
197
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
198
+ <div
199
+ class="rich-editor {className}"
200
+ data-mode={mode}
201
+ data-bordered={bordered}
202
+ onkeydown={(e) => {
203
+ if (onSubmit && (e.metaKey || e.ctrlKey) && e.key === 'Enter') {
204
+ e.preventDefault();
205
+ onSubmit();
206
+ }
207
+ }}
208
+ >
209
+ {#if showToolbar}
210
+ <div class="re-toolbar">
211
+
212
+ <!-- Undo / Redo -->
213
+ <button type="button" title="Undo" class="re-btn" onclick={() => cmd(() => editor?.chain().focus().undo().run())}>
214
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/></svg>
215
+ </button>
216
+ <button type="button" title="Redo" class="re-btn" onclick={() => cmd(() => editor?.chain().focus().redo().run())}>
217
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 019-9 9 9 0 016 2.3L21 13"/></svg>
218
+ </button>
219
+
220
+ <span class="re-sep"></span>
221
+
222
+ <!-- Heading dropdown -->
223
+ <div class="re-dropdown">
224
+ <button type="button" class="re-btn re-heading-btn" title="Text style"
225
+ onclick={() => { showHeadingMenu = !showHeadingMenu; showLinkPopover = false; }}>
226
+ <span class="re-heading-label">{headingLabel}</span>
227
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 9l6 6 6-6"/></svg>
228
+ </button>
229
+ {#if showHeadingMenu}
230
+ <div class="fixed inset-0 z-10" onclick={() => showHeadingMenu = false}></div>
231
+ <div class="re-dropdown-menu">
232
+ <button type="button" class="re-menu-item {!currentHeading ? 're-menu-active' : ''}" onclick={() => setHeading(0)}>Normal</button>
233
+ <button type="button" class="re-menu-item re-h1 {currentHeading === 1 ? 're-menu-active' : ''}" onclick={() => setHeading(1)}>Heading 1</button>
234
+ <button type="button" class="re-menu-item re-h2 {currentHeading === 2 ? 're-menu-active' : ''}" onclick={() => setHeading(2)}>Heading 2</button>
235
+ <button type="button" class="re-menu-item re-h3 {currentHeading === 3 ? 're-menu-active' : ''}" onclick={() => setHeading(3)}>Heading 3</button>
236
+ </div>
237
+ {/if}
238
+ </div>
239
+
240
+ <span class="re-sep"></span>
241
+
242
+ <!-- Inline marks -->
243
+ <button type="button" title="Bold (⌘B)" class="re-btn re-bold {isBold ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleBold().run())}>B</button>
244
+ <button type="button" title="Italic (⌘I)" class="re-btn re-italic {isItalic ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleItalic().run())}>I</button>
245
+ <button type="button" title="Underline (⌘U)" class="re-btn re-underline {isUnderline ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleUnderline().run())}>U</button>
246
+ <button type="button" title="Strikethrough" class="re-btn re-strike {isStrike ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleStrike().run())}>S</button>
247
+ <button type="button" title="Code" class="re-btn re-code {isCode ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleCode().run())}>`</button>
248
+ <button type="button" title="Highlight" class="re-btn {isHighlight ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleHighlight().run())}>
249
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M15.243 3.757L8.586 10.414 6 13l1.5 1.5-3 3L6 19l1.5-1.5 1.5 1.5 2.586-2.586L13 18l6.243-6.243a2 2 0 000-2.829l-1.171-1.171a2 2 0 00-2.829 0z"/></svg>
250
+ </button>
251
+
252
+ <span class="re-sep"></span>
253
+
254
+ <!-- Lists -->
255
+ <button type="button" title="Bullet list" class="re-btn {isBulletList ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleBulletList().run())}>
256
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>
257
+ </button>
258
+ <button type="button" title="Ordered list" class="re-btn {isOrderedList ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleOrderedList().run())}>
259
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4" stroke-linecap="round"/><path d="M4 10h2" stroke-linecap="round"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" stroke-linecap="round"/></svg>
260
+ </button>
261
+ <button type="button" title="Task list" class="re-btn {isTaskList ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleTaskList().run())}>
262
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="M5 8l1.5 1.5L9 6" stroke-linecap="round"/><line x1="13" y1="8" x2="21" y2="8"/><rect x="3" y="14" width="6" height="6" rx="1"/><line x1="13" y1="17" x2="21" y2="17"/></svg>
263
+ </button>
264
+
265
+ <span class="re-sep"></span>
266
+
267
+ <!-- Block elements -->
268
+ <button type="button" title="Blockquote" class="re-btn {isBlockquote ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleBlockquote().run())}>
269
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>
270
+ </button>
271
+ <button type="button" title="Code block" class="re-btn {isCodeBlock ? 're-active' : ''}" onclick={() => cmd(() => editor?.chain().focus().toggleCodeBlock().run())}>
272
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
273
+ </button>
274
+
275
+ <!-- Full mode extras: table insert -->
276
+ {#if mode === 'full'}
277
+ <button type="button" title="Insert table" class="re-btn" onclick={() => cmd(() => editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run())}>
278
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
279
+ </button>
280
+ {/if}
281
+
282
+ <span class="re-sep"></span>
283
+
284
+ <!-- Link -->
285
+ <div class="re-dropdown">
286
+ <button type="button" title="Link" class="re-btn {isLink ? 're-active' : ''}" onclick={openLink}>
287
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
288
+ </button>
289
+ {#if showLinkPopover}
290
+ <div class="fixed inset-0 z-10" onclick={() => showLinkPopover = false}></div>
291
+ <div class="re-link-popover">
292
+ <input
293
+ type="url"
294
+ bind:value={linkUrl}
295
+ placeholder="https://…"
296
+ class="re-link-input"
297
+ onkeydown={(e) => { if (e.key === 'Enter') applyLink(); if (e.key === 'Escape') showLinkPopover = false; }}
298
+ />
299
+ <div class="re-link-actions">
300
+ <button type="button" class="re-link-apply" onclick={applyLink}>Apply</button>
301
+ {#if isLink}
302
+ <button type="button" class="re-link-remove" onclick={() => { editor?.chain().focus().unsetLink().run(); showLinkPopover = false; }}>Remove</button>
303
+ {/if}
304
+ </div>
305
+ </div>
306
+ {/if}
307
+ </div>
308
+
309
+ <!-- Word count -->
310
+ <span class="re-word-count">{wordCount}w</span>
311
+
312
+ <!-- Hint for onSubmit -->
313
+ {#if onSubmit}
314
+ <span class="re-submit-hint">⌘↵ to submit</span>
315
+ {/if}
316
+ </div>
317
+ {/if}
318
+
319
+ <div bind:this={element} class="re-content"></div>
320
+ </div>
321
+
322
+ <style>
323
+ .rich-editor {
324
+ display: flex;
325
+ flex-direction: column;
326
+ border: 1px solid var(--border);
327
+ border-radius: 0.5rem;
328
+ overflow: hidden;
329
+ background: var(--background);
330
+ }
331
+ .rich-editor[data-mode="minimal"],
332
+ .rich-editor[data-bordered="false"] {
333
+ border: none;
334
+ border-radius: 0;
335
+ background: transparent;
336
+ }
337
+ .rich-editor[data-bordered="false"] .re-toolbar {
338
+ border-bottom: 1px solid var(--border);
339
+ background: transparent;
340
+ }
341
+
342
+ /* ── Toolbar ── */
343
+ .re-toolbar {
344
+ display: flex;
345
+ flex-wrap: wrap;
346
+ align-items: center;
347
+ gap: 1px;
348
+ padding: 4px 8px;
349
+ border-bottom: 1px solid var(--border);
350
+ background: var(--muted);
351
+ }
352
+
353
+ .re-btn {
354
+ display: inline-flex;
355
+ align-items: center;
356
+ justify-content: center;
357
+ width: 28px;
358
+ height: 28px;
359
+ padding: 0;
360
+ border: none;
361
+ border-radius: 4px;
362
+ background: transparent;
363
+ color: var(--muted-foreground);
364
+ cursor: pointer;
365
+ font-size: 13px;
366
+ transition: background 0.1s, color 0.1s;
367
+ flex-shrink: 0;
368
+ }
369
+ .re-btn:hover { background: var(--accent); color: var(--foreground); }
370
+ .re-active { background: var(--primary) !important; color: var(--primary-foreground) !important; }
371
+ .re-active:hover { opacity: 0.9; }
372
+
373
+ .re-bold { font-weight: 700; }
374
+ .re-italic { font-style: italic; }
375
+ .re-underline { text-decoration: underline; }
376
+ .re-strike { text-decoration: line-through; }
377
+ .re-code { font-family: ui-monospace, monospace; font-size: 12px; }
378
+
379
+ .re-sep {
380
+ width: 1px;
381
+ height: 18px;
382
+ background: var(--border);
383
+ margin: 0 3px;
384
+ flex-shrink: 0;
385
+ }
386
+
387
+ .re-heading-btn {
388
+ width: auto;
389
+ padding: 0 6px;
390
+ gap: 4px;
391
+ font-size: 12px;
392
+ font-weight: 600;
393
+ min-width: 40px;
394
+ }
395
+ .re-heading-label { min-width: 16px; text-align: center; }
396
+
397
+ .re-dropdown { position: relative; }
398
+ .re-dropdown-menu {
399
+ position: absolute;
400
+ top: 100%;
401
+ left: 0;
402
+ z-index: 20;
403
+ margin-top: 4px;
404
+ min-width: 140px;
405
+ border: 1px solid var(--border);
406
+ border-radius: 8px;
407
+ background: var(--card);
408
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
409
+ overflow: hidden;
410
+ }
411
+ .re-menu-item {
412
+ display: block;
413
+ width: 100%;
414
+ padding: 7px 12px;
415
+ text-align: left;
416
+ border: none;
417
+ background: transparent;
418
+ color: var(--foreground);
419
+ cursor: pointer;
420
+ font-size: 13px;
421
+ transition: background 0.1s;
422
+ }
423
+ .re-menu-item:hover { background: var(--muted); }
424
+ .re-menu-active { background: color-mix(in oklch, var(--primary) 10%, transparent); }
425
+ .re-h1 { font-size: 1.2rem; font-weight: 700; }
426
+ .re-h2 { font-size: 1.05rem; font-weight: 700; }
427
+ .re-h3 { font-size: 0.95rem; font-weight: 600; }
428
+
429
+ .re-link-popover {
430
+ position: absolute;
431
+ top: 100%;
432
+ left: 0;
433
+ z-index: 20;
434
+ margin-top: 4px;
435
+ width: 260px;
436
+ padding: 10px;
437
+ border: 1px solid var(--border);
438
+ border-radius: 8px;
439
+ background: var(--card);
440
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
441
+ }
442
+ .re-link-input {
443
+ width: 100%;
444
+ padding: 6px 10px;
445
+ font-size: 13px;
446
+ border: 1px solid var(--border);
447
+ border-radius: 5px;
448
+ background: var(--background);
449
+ color: var(--foreground);
450
+ outline: none;
451
+ box-sizing: border-box;
452
+ }
453
+ .re-link-input:focus { border-color: var(--primary); }
454
+ .re-link-actions { display: flex; gap: 6px; margin-top: 7px; }
455
+ .re-link-apply {
456
+ padding: 4px 10px;
457
+ font-size: 12px;
458
+ font-weight: 500;
459
+ border: none;
460
+ border-radius: 4px;
461
+ background: var(--primary);
462
+ color: var(--primary-foreground);
463
+ cursor: pointer;
464
+ }
465
+ .re-link-apply:hover { opacity: 0.9; }
466
+ .re-link-remove {
467
+ padding: 4px 10px;
468
+ font-size: 12px;
469
+ border: 1px solid var(--border);
470
+ border-radius: 4px;
471
+ background: transparent;
472
+ color: var(--muted-foreground);
473
+ cursor: pointer;
474
+ }
475
+ .re-link-remove:hover { background: var(--muted); }
476
+
477
+ .re-word-count {
478
+ margin-left: auto;
479
+ font-size: 11px;
480
+ color: var(--muted-foreground);
481
+ opacity: 0.6;
482
+ user-select: none;
483
+ padding-right: 2px;
484
+ }
485
+ .re-submit-hint {
486
+ font-size: 11px;
487
+ color: var(--muted-foreground);
488
+ opacity: 0.5;
489
+ user-select: none;
490
+ white-space: nowrap;
491
+ }
492
+
493
+ /* ── Editor area ── */
494
+ .re-content {
495
+ flex: 1;
496
+ padding: 12px 16px;
497
+ min-height: 120px;
498
+ overflow-y: auto;
499
+ }
500
+ .rich-editor[data-mode="full"] .re-content { min-height: 300px; padding: 20px 24px; }
501
+ .rich-editor[data-mode="minimal"] .re-content { padding: 8px 12px; min-height: 60px; }
502
+
503
+ :global(.rich-editor .ProseMirror) { outline: none; }
504
+ :global(.rich-editor .ProseMirror p.is-editor-empty:first-child::before) {
505
+ color: var(--muted-foreground); opacity: 0.45;
506
+ content: attr(data-placeholder); float: left; height: 0; pointer-events: none;
507
+ }
508
+
509
+ :global(.rich-editor .ProseMirror h1) { font-size: 1.6rem; font-weight: 700; margin: 0.9rem 0 0.4rem; }
510
+ :global(.rich-editor .ProseMirror h2) { font-size: 1.3rem; font-weight: 700; margin: 0.75rem 0 0.35rem; }
511
+ :global(.rich-editor .ProseMirror h3) { font-size: 1.1rem; font-weight: 600; margin: 0.65rem 0 0.3rem; }
512
+ :global(.rich-editor .ProseMirror p) { margin: 0.3rem 0; line-height: 1.65; }
513
+ :global(.rich-editor .ProseMirror a) { color: var(--primary); text-decoration: underline; }
514
+
515
+ :global(.rich-editor .ProseMirror code) {
516
+ background: var(--muted); padding: 0.1em 0.3em; border-radius: 3px;
517
+ font-size: 0.875em; font-family: ui-monospace, monospace;
518
+ }
519
+ :global(.rich-editor .ProseMirror pre) {
520
+ background: var(--muted); border: 1px solid var(--border);
521
+ border-radius: 6px; padding: 12px 16px; overflow-x: auto; margin: 8px 0;
522
+ }
523
+ :global(.rich-editor .ProseMirror pre code) { background: none; padding: 0; font-size: 0.875rem; }
524
+
525
+ :global(.rich-editor .ProseMirror blockquote) {
526
+ border-left: 3px solid var(--border); padding-left: 1rem;
527
+ margin: 8px 0; color: var(--muted-foreground); font-style: italic;
528
+ }
529
+
530
+ :global(.rich-editor .ProseMirror ul) { padding-left: 1.4rem; margin: 4px 0; }
531
+ :global(.rich-editor .ProseMirror ol) { padding-left: 1.4rem; margin: 4px 0; }
532
+ :global(.rich-editor .ProseMirror li) { margin: 2px 0; }
533
+
534
+ :global(.rich-editor .ProseMirror ul[data-type="taskList"]) { list-style: none; padding-left: 0; }
535
+ :global(.rich-editor .ProseMirror ul[data-type="taskList"] li) { display: flex; align-items: flex-start; gap: 8px; margin: 3px 0; }
536
+ :global(.rich-editor .ProseMirror ul[data-type="taskList"] li label) { margin-top: 2px; cursor: pointer; }
537
+ :global(.rich-editor .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div) { opacity: 0.6; text-decoration: line-through; }
538
+
539
+ :global(.rich-editor .ProseMirror table) { border-collapse: collapse; width: 100%; margin: 8px 0; }
540
+ :global(.rich-editor .ProseMirror td, .rich-editor .ProseMirror th) {
541
+ border: 1px solid var(--border); padding: 6px 10px; min-width: 60px;
542
+ }
543
+ :global(.rich-editor .ProseMirror th) { background: var(--muted); font-weight: 600; text-align: left; }
544
+
545
+ :global(.rich-editor .ProseMirror mark) { background: oklch(0.97 0.12 95); border-radius: 2px; padding: 0 1px; }
546
+
547
+ :global(.rich-editor .mention) {
548
+ display: inline-flex; align-items: center;
549
+ padding: 1px 5px; border-radius: 4px; font-size: 0.875em; font-weight: 500;
550
+ background: color-mix(in oklch, var(--primary) 12%, transparent);
551
+ color: var(--primary); cursor: pointer;
552
+ }
553
+
554
+ /* Mention dropdown (tippy) */
555
+ :global(.mention-list) {
556
+ background: var(--card); border: 1px solid var(--border);
557
+ border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15);
558
+ overflow: hidden; min-width: 220px;
559
+ }
560
+ :global(.mention-list-empty) { padding: 10px 14px; font-size: 13px; color: var(--muted-foreground); }
561
+ :global(.mention-list-item) {
562
+ display: flex; align-items: center; gap: 10px;
563
+ width: 100%; padding: 8px 12px; border: none; background: transparent;
564
+ text-align: left; cursor: pointer; transition: background 0.1s;
565
+ }
566
+ :global(.mention-list-item:hover), :global(.mention-list-item.is-selected) { background: var(--muted); }
567
+ :global(.mention-avatar) { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }
568
+ :global(.mention-avatar-fallback) {
569
+ width: 24px; height: 24px; border-radius: 50%; background: var(--muted);
570
+ display: flex; align-items: center; justify-content: center;
571
+ font-size: 11px; font-weight: 600; color: var(--muted-foreground);
572
+ }
573
+ :global(.mention-info) { display: flex; flex-direction: column; flex: 1; min-width: 0; }
574
+ :global(.mention-label) { font-size: 13px; font-weight: 500; color: var(--foreground); }
575
+ :global(.mention-sublabel) { font-size: 11px; color: var(--muted-foreground); }
576
+ :global(.mention-badge) {
577
+ font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px;
578
+ background: var(--secondary); color: var(--secondary-foreground);
579
+ }
580
+ </style>