tiptap improvements + removing ask AI button

This commit is contained in:
tusharmagar 2026-01-14 11:49:29 +05:30 committed by Ramnique Singh
parent 73ba7fee99
commit 9c1ddf66ed
10 changed files with 316 additions and 117 deletions

View file

@ -27,6 +27,8 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tiptap/extension-link": "^3.15.3", "@tiptap/extension-link": "^3.15.3",
"@tiptap/extension-placeholder": "^3.15.3", "@tiptap/extension-placeholder": "^3.15.3",
"@tiptap/extension-task-item": "^3.15.3",
"@tiptap/extension-task-list": "^3.15.3",
"@tiptap/pm": "^3.15.3", "@tiptap/pm": "^3.15.3",
"@tiptap/react": "^3.15.3", "@tiptap/react": "^3.15.3",
"@tiptap/starter-kit": "^3.15.3", "@tiptap/starter-kit": "^3.15.3",

View file

@ -251,28 +251,6 @@ const collectDirPaths = (nodes: TreeNode[]): string[] =>
const collectFilePaths = (nodes: TreeNode[]): string[] => const collectFilePaths = (nodes: TreeNode[]): string[] =>
nodes.flatMap(n => n.kind === 'file' ? [n.path] : (n.children ? collectFilePaths(n.children) : [])) nodes.flatMap(n => n.kind === 'file' ? [n.path] : (n.children ? collectFilePaths(n.children) : []))
// Sample chat history (will be replaced with real data later)
const chatHistory = [
{
id: 'project-kickoff',
title: 'Project kickoff',
preview: 'Scope, roles, and milestones.',
time: 'Today',
},
{
id: 'design-review',
title: 'Design review',
preview: 'UI polish and sidebar UX.',
time: 'Yesterday',
},
{
id: 'tools-audit',
title: 'Tools audit',
preview: 'MCP inventory and tool gaps.',
time: 'Mon',
},
]
function App() { function App() {
// File browser state (for Knowledge section) // File browser state (for Knowledge section)
const [selectedPath, setSelectedPath] = useState<string | null>(null) const [selectedPath, setSelectedPath] = useState<string | null>(null)
@ -630,9 +608,9 @@ function App() {
setExpandedPaths(newExpanded) setExpandedPaths(newExpanded)
} }
// Handle sidebar section changes - switch to chat view for ask-ai and agents // Handle sidebar section changes - switch to chat view for agents
const handleSectionChange = useCallback((section: ActiveSection) => { const handleSectionChange = useCallback((section: ActiveSection) => {
if (section === 'ask-ai' || section === 'agents') { if (section === 'agents') {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
} }
@ -971,7 +949,7 @@ function App() {
return ( return (
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<SidebarSectionProvider defaultSection="ask-ai" onSectionChange={handleSectionChange}> <SidebarSectionProvider defaultSection="knowledge" onSectionChange={handleSectionChange}>
<div className="flex h-svh w-full"> <div className="flex h-svh w-full">
{/* Icon sidebar - always visible, fixed position */} {/* Icon sidebar - always visible, fixed position */}
<SidebarIcon /> <SidebarIcon />
@ -992,7 +970,6 @@ function App() {
expandedPaths={expandedPaths} expandedPaths={expandedPaths}
onSelectFile={toggleExpand} onSelectFile={toggleExpand}
knowledgeActions={knowledgeActions} knowledgeActions={knowledgeActions}
chats={chatHistory}
/> />
<SidebarInset className="!overflow-hidden min-h-0"> <SidebarInset className="!overflow-hidden min-h-0">
{/* Header with sidebar trigger */} {/* Header with sidebar trigger */}

View file

@ -1,5 +1,12 @@
import { useState, useCallback } from 'react'
import type { Editor } from '@tiptap/react' import type { Editor } from '@tiptap/react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { import {
BoldIcon, BoldIcon,
ItalicIcon, ItalicIcon,
@ -10,42 +17,104 @@ import {
Heading3Icon, Heading3Icon,
ListIcon, ListIcon,
ListOrderedIcon, ListOrderedIcon,
ListTodoIcon,
QuoteIcon, QuoteIcon,
MinusIcon, MinusIcon,
LinkIcon, LinkIcon,
CodeSquareIcon, CodeSquareIcon,
Undo2Icon,
Redo2Icon,
ExternalLinkIcon,
Trash2Icon,
} from 'lucide-react' } from 'lucide-react'
interface EditorToolbarProps { interface EditorToolbarProps {
editor: Editor | null editor: Editor | null
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
} }
export function EditorToolbar({ editor }: EditorToolbarProps) { export function EditorToolbar({ editor, onSelectionHighlight }: EditorToolbarProps) {
if (!editor) return null const [linkUrl, setLinkUrl] = useState('')
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
const setLink = () => { const openLinkPopover = useCallback(() => {
const previousUrl = editor.getAttributes('link').href if (!editor) return
const url = window.prompt('URL', previousUrl) const previousUrl = editor.getAttributes('link').href || ''
setLinkUrl(previousUrl)
if (url === null) return // Highlight the current selection while popover is open
const { from, to } = editor.state.selection
if (url === '') { if (from !== to && onSelectionHighlight) {
editor.chain().focus().extendMarkRange('link').unsetLink().run() onSelectionHighlight({ from, to })
return
} }
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run() setIsLinkPopoverOpen(true)
} }, [editor, onSelectionHighlight])
const closeLinkPopover = useCallback(() => {
setIsLinkPopoverOpen(false)
setLinkUrl('')
onSelectionHighlight?.(null)
}, [onSelectionHighlight])
const applyLink = useCallback(() => {
if (!editor) return
if (linkUrl === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run()
} else {
// Ensure URL has protocol
let url = linkUrl.trim()
if (url && !url.match(/^https?:\/\//i) && !url.startsWith('mailto:')) {
url = 'https://' + url
}
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}
closeLinkPopover()
}, [editor, linkUrl, closeLinkPopover])
const removeLink = useCallback(() => {
if (!editor) return
editor.chain().focus().extendMarkRange('link').unsetLink().run()
closeLinkPopover()
}, [editor, closeLinkPopover])
if (!editor) return null
const isLinkActive = editor.isActive('link')
return ( return (
<div className="editor-toolbar"> <div className="editor-toolbar">
{/* Undo / Redo */}
<Button
variant="ghost"
size="icon-sm"
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
title="Undo (Ctrl+Z)"
>
<Undo2Icon className="size-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
title="Redo (Ctrl+Shift+Z)"
>
<Redo2Icon className="size-4" />
</Button>
<div className="separator" />
{/* Text formatting */}
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={() => editor.chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
data-active={editor.isActive('bold') || undefined} data-active={editor.isActive('bold') || undefined}
className="data-[active]:bg-accent" className="data-[active]:bg-accent"
title="Bold" title="Bold (Ctrl+B)"
> >
<BoldIcon className="size-4" /> <BoldIcon className="size-4" />
</Button> </Button>
@ -55,7 +124,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
data-active={editor.isActive('italic') || undefined} data-active={editor.isActive('italic') || undefined}
className="data-[active]:bg-accent" className="data-[active]:bg-accent"
title="Italic" title="Italic (Ctrl+I)"
> >
<ItalicIcon className="size-4" /> <ItalicIcon className="size-4" />
</Button> </Button>
@ -82,6 +151,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
<div className="separator" /> <div className="separator" />
{/* Headings */}
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
@ -115,6 +185,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
<div className="separator" /> <div className="separator" />
{/* Lists */}
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
@ -135,9 +206,20 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
> >
<ListOrderedIcon className="size-4" /> <ListOrderedIcon className="size-4" />
</Button> </Button>
<Button
variant="ghost"
size="icon-sm"
onClick={() => editor.chain().focus().toggleTaskList().run()}
data-active={editor.isActive('taskList') || undefined}
className="data-[active]:bg-accent"
title="Task List"
>
<ListTodoIcon className="size-4" />
</Button>
<div className="separator" /> <div className="separator" />
{/* Blocks */}
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
@ -166,16 +248,78 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
> >
<MinusIcon className="size-4" /> <MinusIcon className="size-4" />
</Button> </Button>
<Button
variant="ghost" {/* Link with popover */}
size="icon-sm" <Popover
onClick={setLink} open={isLinkPopoverOpen}
data-active={editor.isActive('link') || undefined} onOpenChange={(open) => {
className="data-[active]:bg-accent" if (!open) {
title="Link" closeLinkPopover()
}
}}
> >
<LinkIcon className="size-4" /> <PopoverTrigger asChild>
</Button> <Button
variant="ghost"
size="icon-sm"
onClick={openLinkPopover}
data-active={isLinkActive || undefined}
className="data-[active]:bg-accent"
title="Link"
>
<LinkIcon className="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-3" align="start">
<div className="flex flex-col gap-3">
<div className="text-sm font-medium">
{isLinkActive ? 'Edit Link' : 'Add Link'}
</div>
<Input
placeholder="https://example.com"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
applyLink()
}
if (e.key === 'Escape') {
setIsLinkPopoverOpen(false)
}
}}
autoFocus
/>
<div className="flex items-center gap-2">
<Button size="sm" onClick={applyLink} className="flex-1">
{isLinkActive ? 'Update' : 'Apply'}
</Button>
{isLinkActive && (
<>
<Button
size="sm"
variant="outline"
onClick={() => {
window.open(linkUrl, '_blank')
}}
title="Open link"
>
<ExternalLinkIcon className="size-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={removeLink}
title="Remove link"
>
<Trash2Icon className="size-4" />
</Button>
</>
)}
</div>
</div>
</PopoverContent>
</Popover>
</div> </div>
) )
} }

View file

@ -1,7 +1,11 @@
import { useEditor, EditorContent } from '@tiptap/react' import { useEditor, EditorContent, Extension } from '@tiptap/react'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link' import Link from '@tiptap/extension-link'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import { Markdown } from 'tiptap-markdown' import { Markdown } from 'tiptap-markdown'
import { useEffect, useCallback, useMemo, useRef, useState } from 'react' import { useEffect, useCallback, useMemo, useRef, useState } from 'react'
import { EditorToolbar } from './editor-toolbar' import { EditorToolbar } from './editor-toolbar'
@ -30,6 +34,41 @@ type WikiLinkMatch = {
query: string query: string
} }
type SelectionHighlightRange = { from: number; to: number } | null
// Plugin key for the selection highlight
const selectionHighlightKey = new PluginKey('selectionHighlight')
// Create the selection highlight extension
const createSelectionHighlightExtension = (getRange: () => SelectionHighlightRange) => {
return Extension.create({
name: 'selectionHighlight',
addProseMirrorPlugins() {
return [
new Plugin({
key: selectionHighlightKey,
props: {
decorations(state) {
const range = getRange()
if (!range) return DecorationSet.empty
const { from, to } = range
if (from >= to || from < 0 || to > state.doc.content.size) {
return DecorationSet.empty
}
const decoration = Decoration.inline(from, to, {
class: 'selection-highlight',
})
return DecorationSet.create(state.doc, [decoration])
},
},
}),
]
},
})
}
export function MarkdownEditor({ export function MarkdownEditor({
content, content,
onChange, onChange,
@ -40,6 +79,17 @@ export function MarkdownEditor({
const wrapperRef = useRef<HTMLDivElement>(null) const wrapperRef = useRef<HTMLDivElement>(null)
const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | null>(null) const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | null>(null)
const [anchorPosition, setAnchorPosition] = useState<{ left: number; top: number } | null>(null) const [anchorPosition, setAnchorPosition] = useState<{ left: number; top: number } | null>(null)
const [selectionHighlight, setSelectionHighlight] = useState<SelectionHighlightRange>(null)
const selectionHighlightRef = useRef<SelectionHighlightRange>(null)
// Keep ref in sync with state for the plugin to access
selectionHighlightRef.current = selectionHighlight
// Memoize the selection highlight extension
const selectionHighlightExtension = useMemo(
() => createSelectionHighlightExtension(() => selectionHighlightRef.current),
[]
)
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
@ -62,6 +112,10 @@ export function MarkdownEditor({
} }
: undefined, : undefined,
}), }),
TaskList,
TaskItem.configure({
nested: true,
}),
Placeholder.configure({ Placeholder.configure({
placeholder, placeholder,
}), }),
@ -71,6 +125,7 @@ export function MarkdownEditor({
transformCopiedText: true, transformCopiedText: true,
transformPastedText: true, transformPastedText: true,
}), }),
selectionHighlightExtension,
], ],
content: '', content: '',
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
@ -184,6 +239,14 @@ export function MarkdownEditor({
} }
}, [editor, content]) }, [editor, content])
// Force re-render decorations when selection highlight changes
useEffect(() => {
if (editor) {
// Trigger a transaction to force decoration re-render
editor.view.dispatch(editor.state.tr)
}
}, [editor, selectionHighlight])
const normalizedQuery = normalizeWikiPath(activeWikiLink?.query ?? '').toLowerCase() const normalizedQuery = normalizeWikiPath(activeWikiLink?.query ?? '').toLowerCase()
const filteredFiles = useMemo(() => { const filteredFiles = useMemo(() => {
if (!activeWikiLink) return [] if (!activeWikiLink) return []
@ -237,7 +300,7 @@ export function MarkdownEditor({
return ( return (
<div className="tiptap-editor" onKeyDown={handleKeyDown}> <div className="tiptap-editor" onKeyDown={handleKeyDown}>
<EditorToolbar editor={editor} /> <EditorToolbar editor={editor} onSelectionHighlight={setSelectionHighlight} />
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}> <div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{wikiLinks ? ( {wikiLinks ? (

View file

@ -15,8 +15,6 @@ import {
FolderPlus, FolderPlus,
Loader2, Loader2,
Mail, Mail,
MessageSquare,
MessageSquarePlus,
Microscope, Microscope,
Network, Network,
Pencil, Pencil,
@ -88,11 +86,9 @@ type SidebarContentPanelProps = {
expandedPaths: Set<string> expandedPaths: Set<string>
onSelectFile: (path: string, kind: "file" | "dir") => void onSelectFile: (path: string, kind: "file" | "dir") => void
knowledgeActions: KnowledgeActions knowledgeActions: KnowledgeActions
chats: { id: string; title: string; preview: string; time: string }[]
} & React.ComponentProps<typeof Sidebar> } & React.ComponentProps<typeof Sidebar>
const sectionTitles = { const sectionTitles = {
"ask-ai": "Ask AI",
knowledge: "Knowledge", knowledge: "Knowledge",
agents: "Agents", agents: "Agents",
} }
@ -166,7 +162,6 @@ export function SidebarContentPanel({
expandedPaths, expandedPaths,
onSelectFile, onSelectFile,
knowledgeActions, knowledgeActions,
chats,
...props ...props
}: SidebarContentPanelProps) { }: SidebarContentPanelProps) {
const { activeSection } = useSidebarSection() const { activeSection } = useSidebarSection()
@ -179,9 +174,6 @@ export function SidebarContentPanel({
</div> </div>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
{activeSection === "ask-ai" && (
<ChatSection chats={chats} />
)}
{activeSection === "knowledge" && ( {activeSection === "knowledge" && (
<KnowledgeSection <KnowledgeSection
tree={tree} tree={tree}
@ -200,43 +192,6 @@ export function SidebarContentPanel({
) )
} }
// Chat Section
function ChatSection({ chats }: { chats: { id: string; title: string; preview: string; time: string }[] }) {
return (
<SidebarGroup>
<SidebarGroupLabel className="flex items-center justify-between">
<span>Recent Chats</span>
<Tooltip>
<TooltipTrigger asChild>
<button className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1 transition-colors">
<MessageSquarePlus className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">New Chat</TooltipContent>
</Tooltip>
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{chats.map((chat) => (
<SidebarMenuItem key={chat.id}>
<SidebarMenuButton className="h-auto items-start gap-2 py-2">
<MessageSquare className="mt-0.5 size-4" />
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-sm font-medium">{chat.title}</span>
<span className="text-xs text-muted-foreground">{chat.time}</span>
</div>
<span className="truncate text-xs text-muted-foreground">{chat.preview}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}
// Knowledge Section // Knowledge Section
function KnowledgeSection({ function KnowledgeSection({
tree, tree,

View file

@ -7,7 +7,6 @@ import {
HelpCircle, HelpCircle,
Settings, Settings,
Ship, Ship,
Sparkles,
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
@ -33,7 +32,6 @@ type SecondaryItem = {
} }
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{ id: "ask-ai", title: "Ask AI", icon: Sparkles },
{ id: "knowledge", title: "Knowledge", icon: Brain }, { id: "knowledge", title: "Knowledge", icon: Brain },
{ id: "agents", title: "Agents", icon: Bot }, { id: "agents", title: "Agents", icon: Bot },
] ]

View file

@ -30,7 +30,6 @@ const SIDEBAR_WIDTH_MIN = 200
const SIDEBAR_WIDTH_MAX = 480 const SIDEBAR_WIDTH_MAX = 480
const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
const SIDEBAR_OFFSET = "0px" // Default offset for nested sidebars const SIDEBAR_OFFSET = "0px" // Default offset for nested sidebars
type SidebarContextProps = { type SidebarContextProps = {
@ -100,22 +99,6 @@ function SidebarProvider({
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile]) }, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed". // We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed" const state = open ? "expanded" : "collapsed"

View file

@ -2,7 +2,7 @@
import * as React from "react" import * as React from "react"
export type ActiveSection = "ask-ai" | "knowledge" | "agents" export type ActiveSection = "knowledge" | "agents"
type SidebarSectionContextProps = { type SidebarSectionContextProps = {
activeSection: ActiveSection activeSection: ActiveSection
@ -20,7 +20,7 @@ export function useSidebarSection() {
} }
export function SidebarSectionProvider({ export function SidebarSectionProvider({
defaultSection = "ask-ai", defaultSection = "knowledge",
onSectionChange, onSectionChange,
children, children,
}: { }: {

View file

@ -175,17 +175,41 @@
.tiptap-editor .ProseMirror ul[data-type="taskList"] { .tiptap-editor .ProseMirror ul[data-type="taskList"] {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
margin: 0.5em 0;
} }
.tiptap-editor .ProseMirror ul[data-type="taskList"] li { .tiptap-editor .ProseMirror ul[data-type="taskList"] li {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 0.5em; gap: 0.5em;
margin: 0.25em 0;
} }
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label { .tiptap-editor .ProseMirror ul[data-type="taskList"] li > label {
flex-shrink: 0; flex-shrink: 0;
margin-top: 0.25em; margin-top: 0.25em;
cursor: pointer;
}
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"] {
width: 1em;
height: 1em;
cursor: pointer;
accent-color: var(--primary);
}
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > div {
flex: 1;
}
.tiptap-editor .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div {
text-decoration: line-through;
opacity: 0.6;
}
/* Nested task lists */
.tiptap-editor .ProseMirror ul[data-type="taskList"] ul[data-type="taskList"] {
margin-left: 1.5em;
} }
/* Selection */ /* Selection */
@ -228,3 +252,32 @@
width: 0; width: 0;
pointer-events: none; pointer-events: none;
} }
/* Wiki Links */
.tiptap-editor .ProseMirror .wiki-link,
.tiptap-editor .ProseMirror a[data-type="wiki-link"] {
color: var(--primary);
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
padding: 0.1em 0.3em;
border-radius: 0.25em;
text-decoration: none;
cursor: pointer;
transition: background-color 0.15s ease;
}
.tiptap-editor .ProseMirror .wiki-link:hover,
.tiptap-editor .ProseMirror a[data-type="wiki-link"]:hover {
background-color: color-mix(in srgb, var(--primary) 20%, transparent);
}
/* Disabled button state */
.editor-toolbar button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Selection highlight for when editor loses focus (e.g., link popover open) */
.tiptap-editor .ProseMirror .selection-highlight {
background-color: color-mix(in srgb, var(--primary) 25%, transparent);
border-radius: 2px;
}

24
apps/x/pnpm-lock.yaml generated
View file

@ -130,6 +130,12 @@ importers:
'@tiptap/extension-placeholder': '@tiptap/extension-placeholder':
specifier: ^3.15.3 specifier: ^3.15.3
version: 3.15.3(@tiptap/extensions@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)) version: 3.15.3(@tiptap/extensions@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
'@tiptap/extension-task-item':
specifier: ^3.15.3
version: 3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
'@tiptap/extension-task-list':
specifier: ^3.15.3
version: 3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
'@tiptap/pm': '@tiptap/pm':
specifier: ^3.15.3 specifier: ^3.15.3
version: 3.15.3 version: 3.15.3
@ -1775,6 +1781,16 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/core': ^3.15.3 '@tiptap/core': ^3.15.3
'@tiptap/extension-task-item@3.15.3':
resolution: {integrity: sha512-bkrmouc1rE5n9ONw2G7+zCGfBRoF2HJWq8REThPMzg/6+L5GJJ5YTN4UmncaP48U9jHX8xeihjgg9Ypenjl4lw==}
peerDependencies:
'@tiptap/extension-list': ^3.15.3
'@tiptap/extension-task-list@3.15.3':
resolution: {integrity: sha512-nh8iBk1LHVIoqxphLoqZlLAN9fF2i9ZeK+2TjGSS35lfh7sYzRoSjNW0E81Uy48YuCzM1NQYghYR5Qfc7vm4jA==}
peerDependencies:
'@tiptap/extension-list': ^3.15.3
'@tiptap/extension-text@3.15.3': '@tiptap/extension-text@3.15.3':
resolution: {integrity: sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==} resolution: {integrity: sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==}
peerDependencies: peerDependencies:
@ -5929,6 +5945,14 @@ snapshots:
dependencies: dependencies:
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3) '@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
'@tiptap/extension-task-item@3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))':
dependencies:
'@tiptap/extension-list': 3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)
'@tiptap/extension-task-list@3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))':
dependencies:
'@tiptap/extension-list': 3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)
'@tiptap/extension-text@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))': '@tiptap/extension-text@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))':
dependencies: dependencies:
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3) '@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)