Nystart basert på arkitektonisk innsikt fra Sidelinja v1. Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid. Inneholder: - Komplett arkitekturdokumentasjon (docs/arkitektur.md) - 6 vedtatte retninger (docs/retninger/) - Alle concepts, features, proposals og erfaringer fra v1 - Server-oppsett og drift (docs/setup/) - LiteLLM-konfigurasjon (API-nøkler via env) - Editor.svelte referanse fra v1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
741 lines
19 KiB
Svelte
741 lines
19 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Universell editor-komponent.
|
|
* Fase 1: Tiptap med plaintext, #-mentions, markdown formatting.
|
|
*
|
|
* Modi:
|
|
* compact — chat-input (Enter = submit, ingen synlig toolbar)
|
|
* extended — notater/lengre tekst (Enter = newline, toolbar synlig)
|
|
*
|
|
* Bruk:
|
|
* <Editor mode="compact" onSubmit={(content) => ...} placeholder="Skriv en melding..." />
|
|
* <Editor mode="extended" bind:content onUpdate={() => ...} placeholder="Skriv her..." />
|
|
*/
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { Editor } from '@tiptap/core';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
import Mention from '@tiptap/extension-mention';
|
|
|
|
interface Entity {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
aliases: string[];
|
|
}
|
|
|
|
let {
|
|
mode = 'compact',
|
|
placeholder = '',
|
|
content = $bindable(''),
|
|
autofocus = false,
|
|
onSubmit = (_html: string, _json: Record<string, unknown>, _mentions: Entity[]) => {},
|
|
onUpdate = () => {},
|
|
onExpand = () => {}
|
|
}: {
|
|
mode?: 'compact' | 'extended';
|
|
placeholder?: string;
|
|
content?: string;
|
|
autofocus?: boolean;
|
|
onSubmit?: (html: string, json: Record<string, unknown>, mentions: Entity[]) => void;
|
|
onUpdate?: () => void;
|
|
onExpand?: () => void;
|
|
} = $props();
|
|
|
|
let editorEl: HTMLDivElement;
|
|
let editor = $state<Editor | null>(null);
|
|
let mentions: Entity[] = [];
|
|
let expanded = $state(false);
|
|
let hasContent = $state(false);
|
|
let rawMode = $state(false);
|
|
let rawContent = $state('');
|
|
|
|
// Mention suggestions state
|
|
let suggestions = $state<Entity[]>([]);
|
|
let selectedIndex = $state(0);
|
|
let suggestionsEl: HTMLDivElement | undefined;
|
|
let mentionPopupPos = $state<{ top: number; left: number } | null>(null);
|
|
let mentionCommandFn: ((item: any) => void) | null = null;
|
|
|
|
const typeColors: Record<string, string> = {
|
|
person: '#3b82f6',
|
|
organisasjon: '#f59e0b',
|
|
sted: '#10b981',
|
|
tema: '#8b5cf6',
|
|
konsept: '#ec4899'
|
|
};
|
|
|
|
function getEditorContent() {
|
|
if (!editor) return { html: '', json: {} };
|
|
return {
|
|
html: editor.getHTML(),
|
|
json: editor.getJSON()
|
|
};
|
|
}
|
|
|
|
function clearEditor() {
|
|
editor?.commands.clearContent();
|
|
mentions = [];
|
|
hasContent = false;
|
|
expanded = false;
|
|
}
|
|
|
|
async function searchEntities(query: string): Promise<Entity[]> {
|
|
try {
|
|
const res = await fetch(`/api/entities?q=${encodeURIComponent(query)}&limit=8`);
|
|
if (res.ok) return await res.json();
|
|
} catch { /* ignore */ }
|
|
return [];
|
|
}
|
|
|
|
function handleSubmit() {
|
|
if (!editor || !hasContent) return;
|
|
const { html, json } = getEditorContent();
|
|
onSubmit(html, json, [...mentions]);
|
|
clearEditor();
|
|
}
|
|
|
|
onMount(() => {
|
|
editor = new Editor({
|
|
element: editorEl,
|
|
extensions: [
|
|
StarterKit,
|
|
Placeholder.configure({
|
|
placeholder
|
|
}),
|
|
Mention.configure({
|
|
HTMLAttributes: {
|
|
class: 'mention'
|
|
},
|
|
suggestion: {
|
|
char: '#',
|
|
items: async ({ query }: { query: string }) => {
|
|
if (query.length < 1) return [];
|
|
return await searchEntities(query);
|
|
},
|
|
render: () => {
|
|
return {
|
|
onStart: (props: any) => {
|
|
mentionCommandFn = props.command;
|
|
suggestions = props.items;
|
|
selectedIndex = 0;
|
|
updatePopupPosition(props.clientRect);
|
|
},
|
|
onUpdate: (props: any) => {
|
|
mentionCommandFn = props.command;
|
|
suggestions = props.items;
|
|
selectedIndex = 0;
|
|
updatePopupPosition(props.clientRect);
|
|
},
|
|
onKeyDown: (props: any) => {
|
|
if (props.event.key === 'ArrowDown') {
|
|
selectedIndex = (selectedIndex + 1) % suggestions.length;
|
|
return true;
|
|
}
|
|
if (props.event.key === 'ArrowUp') {
|
|
selectedIndex = (selectedIndex - 1 + suggestions.length) % suggestions.length;
|
|
return true;
|
|
}
|
|
if (props.event.key === 'Enter' || props.event.key === 'Tab') {
|
|
if (suggestions.length > 0) {
|
|
mentionCommandFn?.(suggestions[selectedIndex]);
|
|
return true;
|
|
}
|
|
}
|
|
if (props.event.key === 'Escape') {
|
|
suggestions = [];
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
onExit: () => {
|
|
mentionCommandFn = null;
|
|
suggestions = [];
|
|
mentionPopupPos = null;
|
|
}
|
|
};
|
|
},
|
|
command: ({ editor: ed, range, props: item }: any) => {
|
|
ed.chain()
|
|
.focus()
|
|
.insertContentAt(range, [
|
|
{ type: 'mention', attrs: { id: item.id, label: item.name } },
|
|
{ type: 'text', text: ' ' }
|
|
])
|
|
.run();
|
|
mentions.push(item);
|
|
}
|
|
}
|
|
})
|
|
],
|
|
content: content || '',
|
|
editorProps: {
|
|
attributes: {
|
|
class: `editor-content ${mode}`,
|
|
'data-mode': mode
|
|
},
|
|
handleKeyDown: (_view, event) => {
|
|
// Compact mode: Enter = submit (unless shift held or suggestions open)
|
|
if (mode === 'compact' && event.key === 'Enter' && !event.shiftKey) {
|
|
if (suggestions.length > 0) return false; // Let mention handler deal with it
|
|
event.preventDefault();
|
|
handleSubmit();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
onUpdate: ({ editor: ed }) => {
|
|
content = ed.getHTML();
|
|
hasContent = !ed.isEmpty;
|
|
onUpdate();
|
|
},
|
|
autofocus: autofocus ? 'end' : false
|
|
});
|
|
});
|
|
|
|
onDestroy(() => {
|
|
editor?.destroy();
|
|
});
|
|
|
|
function updatePopupPosition(clientRect: (() => DOMRect | null) | null) {
|
|
if (!clientRect) return;
|
|
const rect = clientRect();
|
|
if (!rect) return;
|
|
const editorRect = editorEl.getBoundingClientRect();
|
|
mentionPopupPos = {
|
|
left: rect.left - editorRect.left,
|
|
top: rect.top - editorRect.top - 4
|
|
};
|
|
}
|
|
|
|
function toggleExpand() {
|
|
expanded = !expanded;
|
|
if (expanded) onExpand();
|
|
if (!expanded) rawMode = false;
|
|
editor?.commands.focus();
|
|
}
|
|
|
|
function toggleRawMode() {
|
|
if (!editor) return;
|
|
if (rawMode) {
|
|
// Switching from raw → rendered: push textarea content into Tiptap
|
|
editor.commands.setContent(rawContent);
|
|
content = rawContent;
|
|
hasContent = !editor.isEmpty;
|
|
rawMode = false;
|
|
editor.commands.focus();
|
|
} else {
|
|
// Switching from rendered → raw: snapshot HTML into textarea
|
|
rawContent = editor.getHTML();
|
|
rawMode = true;
|
|
}
|
|
}
|
|
|
|
function handleRawInput(e: Event) {
|
|
rawContent = (e.target as HTMLTextAreaElement).value;
|
|
content = rawContent;
|
|
hasContent = rawContent.replace(/<[^>]*>/g, '').trim().length > 0;
|
|
}
|
|
|
|
function handleGlobalKeydown(e: KeyboardEvent) {
|
|
// Ctrl+/ or Cmd+/ toggles raw mode
|
|
if ((e.ctrlKey || e.metaKey) && e.key === '/') {
|
|
e.preventDefault();
|
|
toggleRawMode();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="editor-wrapper" class:compact={mode === 'compact'} class:extended={mode === 'extended'} class:expanded onkeydown={handleGlobalKeydown}>
|
|
{#if mode === 'extended' || expanded}
|
|
<div class="toolbar">
|
|
{#if !rawMode}
|
|
<button type="button" class="tool" class:active={editor?.isActive('bold')}
|
|
onclick={() => editor?.chain().focus().toggleBold().run()}
|
|
title="Bold (Ctrl+B)">B</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('italic')}
|
|
onclick={() => editor?.chain().focus().toggleItalic().run()}
|
|
title="Italic (Ctrl+I)"><em>I</em></button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('strike')}
|
|
onclick={() => editor?.chain().focus().toggleStrike().run()}
|
|
title="Strikethrough">S̶</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('code')}
|
|
onclick={() => editor?.chain().focus().toggleCode().run()}
|
|
title="Inline code">{'<>'}</button>
|
|
<span class="separator"></span>
|
|
<button type="button" class="tool" class:active={editor?.isActive('heading', { level: 1 })}
|
|
onclick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
title="Heading 1">H1</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('heading', { level: 2 })}
|
|
onclick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
title="Heading 2">H2</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('heading', { level: 3 })}
|
|
onclick={() => editor?.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
title="Heading 3">H3</button>
|
|
<span class="separator"></span>
|
|
<button type="button" class="tool" class:active={editor?.isActive('bulletList')}
|
|
onclick={() => editor?.chain().focus().toggleBulletList().run()}
|
|
title="Bullet list">•</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('orderedList')}
|
|
onclick={() => editor?.chain().focus().toggleOrderedList().run()}
|
|
title="Numbered list">1.</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('blockquote')}
|
|
onclick={() => editor?.chain().focus().toggleBlockquote().run()}
|
|
title="Quote">❝</button>
|
|
<button type="button" class="tool" class:active={editor?.isActive('codeBlock')}
|
|
onclick={() => editor?.chain().focus().toggleCodeBlock().run()}
|
|
title="Code block">{'{ }'}</button>
|
|
<span class="separator"></span>
|
|
{/if}
|
|
<button type="button" class="tool raw-toggle" class:active={rawMode}
|
|
onclick={toggleRawMode}
|
|
title="Bytt raw/rendered (Ctrl+/)">{'</>'}</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="editor-container">
|
|
{#if rawMode}
|
|
<textarea
|
|
class="raw-editor"
|
|
value={rawContent}
|
|
oninput={handleRawInput}
|
|
spellcheck={false}
|
|
></textarea>
|
|
{:else}
|
|
<div bind:this={editorEl} class="editor-mount"></div>
|
|
{/if}
|
|
|
|
{#if mode === 'compact' && !expanded}
|
|
<button type="button" class="expand-btn" onclick={toggleExpand} title="Utvid editor">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
<polyline points="9 21 3 21 3 15"></polyline>
|
|
<line x1="21" y1="3" x2="14" y2="10"></line>
|
|
<line x1="3" y1="21" x2="10" y2="14"></line>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="send-btn"
|
|
onclick={handleSubmit}
|
|
disabled={!hasContent}
|
|
aria-label="Send"
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
</svg>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if mode === 'compact' && expanded}
|
|
<div class="editor-bottom-bar">
|
|
<button type="button" class="expand-btn" onclick={toggleExpand} title="Minimer">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="4 14 10 14 10 20"></polyline>
|
|
<polyline points="20 10 14 10 14 4"></polyline>
|
|
<line x1="14" y1="10" x2="21" y2="3"></line>
|
|
<line x1="3" y1="21" x2="10" y2="14"></line>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="send-btn"
|
|
onclick={handleSubmit}
|
|
disabled={!hasContent}
|
|
aria-label="Send"
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Mention suggestions popup -->
|
|
{#if suggestions.length > 0 && mentionPopupPos}
|
|
<div
|
|
class="mention-popup"
|
|
bind:this={suggestionsEl}
|
|
style:left="{mentionPopupPos.left}px"
|
|
style:bottom="calc(100% - {mentionPopupPos.top}px)"
|
|
>
|
|
{#each suggestions as entity, i (entity.id)}
|
|
<button
|
|
type="button"
|
|
class="suggestion"
|
|
class:selected={i === selectedIndex}
|
|
onmousedown={(e) => {
|
|
e.preventDefault();
|
|
mentionCommandFn?.(entity);
|
|
}}
|
|
>
|
|
<span class="dot" style:background={typeColors[entity.type] ?? '#8b92a5'}></span>
|
|
<span class="name">{entity.name}</span>
|
|
{#if entity.aliases?.length > 0}
|
|
<span class="alias">{entity.aliases[0]}</span>
|
|
{/if}
|
|
<span class="type">{entity.type}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.editor-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
.editor-container {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 0.4rem;
|
|
background: #0f1117;
|
|
border: 1px solid #2d3148;
|
|
border-radius: 6px;
|
|
padding: 0.4rem;
|
|
}
|
|
|
|
.compact .editor-container {
|
|
align-items: center;
|
|
}
|
|
|
|
.extended .editor-container,
|
|
.expanded .editor-container {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
border-top-left-radius: 0;
|
|
border-top-right-radius: 0;
|
|
}
|
|
|
|
.editor-mount {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Tiptap editor content styling */
|
|
.editor-wrapper :global(.editor-content) {
|
|
outline: none;
|
|
color: #e1e4e8;
|
|
font-size: 0.85rem;
|
|
font-family: inherit;
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content.compact) {
|
|
max-height: 120px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.expanded :global(.editor-content.compact) {
|
|
min-height: 120px;
|
|
max-height: 40vh;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content.extended) {
|
|
min-height: 200px;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Raw editor (source view) */
|
|
.raw-editor {
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 200px;
|
|
max-height: 60vh;
|
|
background: transparent;
|
|
color: #a0a8b8;
|
|
border: none;
|
|
outline: none;
|
|
resize: none;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
padding: 0;
|
|
tab-size: 2;
|
|
}
|
|
|
|
.compact .raw-editor {
|
|
min-height: 120px;
|
|
max-height: 40vh;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content p) {
|
|
margin: 0;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content p + p) {
|
|
margin-top: 0.4em;
|
|
}
|
|
|
|
/* Heading styles (extended mode) */
|
|
.editor-wrapper :global(.editor-content h1) {
|
|
font-size: 1.4rem;
|
|
font-weight: 700;
|
|
margin: 0.8em 0 0.3em;
|
|
color: #f1f3f5;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content h2) {
|
|
font-size: 1.15rem;
|
|
font-weight: 600;
|
|
margin: 0.7em 0 0.25em;
|
|
color: #f1f3f5;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content h3) {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin: 0.6em 0 0.2em;
|
|
color: #f1f3f5;
|
|
}
|
|
|
|
/* List styles */
|
|
.editor-wrapper :global(.editor-content ul),
|
|
.editor-wrapper :global(.editor-content ol) {
|
|
padding-left: 1.2em;
|
|
margin: 0.3em 0;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content li) {
|
|
margin: 0.1em 0;
|
|
}
|
|
|
|
/* Blockquote */
|
|
.editor-wrapper :global(.editor-content blockquote) {
|
|
border-left: 3px solid #3b82f6;
|
|
padding-left: 0.75em;
|
|
margin: 0.4em 0;
|
|
color: #a0a8b8;
|
|
}
|
|
|
|
/* Code */
|
|
.editor-wrapper :global(.editor-content code) {
|
|
background: #1e2235;
|
|
padding: 0.15em 0.3em;
|
|
border-radius: 3px;
|
|
font-size: 0.8rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content pre) {
|
|
background: #1e2235;
|
|
padding: 0.6em;
|
|
border-radius: 6px;
|
|
margin: 0.4em 0;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content pre code) {
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Inline formatting */
|
|
.editor-wrapper :global(.editor-content strong) {
|
|
font-weight: 700;
|
|
color: #f1f3f5;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content em) {
|
|
font-style: italic;
|
|
}
|
|
|
|
.editor-wrapper :global(.editor-content s) {
|
|
text-decoration: line-through;
|
|
color: #8b92a5;
|
|
}
|
|
|
|
/* Mentions */
|
|
.editor-wrapper :global(.mention) {
|
|
color: #8b5cf6;
|
|
background: rgba(139, 92, 246, 0.12);
|
|
padding: 0.1em 0.3em;
|
|
border-radius: 3px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.editor-wrapper :global(.mention::before) {
|
|
content: '#';
|
|
}
|
|
|
|
/* Placeholder */
|
|
.editor-wrapper :global(.tiptap p.is-editor-empty:first-child::before) {
|
|
content: attr(data-placeholder);
|
|
color: #8b92a5;
|
|
float: left;
|
|
height: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Toolbar */
|
|
.toolbar {
|
|
display: flex;
|
|
gap: 0.15rem;
|
|
padding: 0.3rem 0.4rem;
|
|
background: #0f1117;
|
|
border: 1px solid #2d3148;
|
|
border-bottom: none;
|
|
border-radius: 6px 6px 0 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tool {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 28px;
|
|
height: 26px;
|
|
padding: 0 0.35rem;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: #8b92a5;
|
|
font-family: inherit;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tool:hover {
|
|
background: #1e2235;
|
|
color: #e1e4e8;
|
|
}
|
|
|
|
.tool.active {
|
|
background: #1e2235;
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.raw-toggle {
|
|
margin-left: auto;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.65rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.separator {
|
|
width: 1px;
|
|
height: 18px;
|
|
background: #2d3148;
|
|
align-self: center;
|
|
margin: 0 0.15rem;
|
|
}
|
|
|
|
/* Bottom bar (expanded compact mode) */
|
|
.editor-bottom-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.3rem 0.4rem 0;
|
|
}
|
|
|
|
/* Send button (compact mode) */
|
|
.send-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
background: #3b82f6;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: white;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.send-btn:disabled {
|
|
background: #1e2235;
|
|
color: #8b92a5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.send-btn:not(:disabled):hover {
|
|
background: #2563eb;
|
|
}
|
|
|
|
/* Expand button */
|
|
.expand-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: #8b92a5;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.expand-btn:hover {
|
|
background: #1e2235;
|
|
color: #e1e4e8;
|
|
}
|
|
|
|
/* Mention popup */
|
|
.mention-popup {
|
|
position: absolute;
|
|
background: #161822;
|
|
border: 1px solid #2d3148;
|
|
border-radius: 6px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 50;
|
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
|
|
min-width: 200px;
|
|
}
|
|
|
|
.suggestion {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.4rem 0.5rem;
|
|
width: 100%;
|
|
border: none;
|
|
background: none;
|
|
color: #e1e4e8;
|
|
font-family: inherit;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
|
|
.suggestion:hover, .suggestion.selected {
|
|
background: #1e2235;
|
|
}
|
|
|
|
.dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.name {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.alias {
|
|
color: #8b92a5;
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.type {
|
|
color: #8b92a5;
|
|
font-size: 0.7rem;
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|