mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
tiptap improvements + removing ask AI button
This commit is contained in:
parent
73ba7fee99
commit
9c1ddf66ed
10 changed files with 316 additions and 117 deletions
|
|
@ -27,6 +27,8 @@
|
|||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tiptap/extension-link": "^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/react": "^3.15.3",
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
|
|
|
|||
|
|
@ -251,28 +251,6 @@ const collectDirPaths = (nodes: TreeNode[]): string[] =>
|
|||
const collectFilePaths = (nodes: TreeNode[]): string[] =>
|
||||
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() {
|
||||
// File browser state (for Knowledge section)
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||
|
|
@ -630,9 +608,9 @@ function App() {
|
|||
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) => {
|
||||
if (section === 'ask-ai' || section === 'agents') {
|
||||
if (section === 'agents') {
|
||||
setSelectedPath(null)
|
||||
setIsGraphOpen(false)
|
||||
}
|
||||
|
|
@ -971,7 +949,7 @@ function App() {
|
|||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<SidebarSectionProvider defaultSection="ask-ai" onSectionChange={handleSectionChange}>
|
||||
<SidebarSectionProvider defaultSection="knowledge" onSectionChange={handleSectionChange}>
|
||||
<div className="flex h-svh w-full">
|
||||
{/* Icon sidebar - always visible, fixed position */}
|
||||
<SidebarIcon />
|
||||
|
|
@ -992,7 +970,6 @@ function App() {
|
|||
expandedPaths={expandedPaths}
|
||||
onSelectFile={toggleExpand}
|
||||
knowledgeActions={knowledgeActions}
|
||||
chats={chatHistory}
|
||||
/>
|
||||
<SidebarInset className="!overflow-hidden min-h-0">
|
||||
{/* Header with sidebar trigger */}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { useState, useCallback } from 'react'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
BoldIcon,
|
||||
ItalicIcon,
|
||||
|
|
@ -10,42 +17,104 @@ import {
|
|||
Heading3Icon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
ListTodoIcon,
|
||||
QuoteIcon,
|
||||
MinusIcon,
|
||||
LinkIcon,
|
||||
CodeSquareIcon,
|
||||
Undo2Icon,
|
||||
Redo2Icon,
|
||||
ExternalLinkIcon,
|
||||
Trash2Icon,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface EditorToolbarProps {
|
||||
editor: Editor | null
|
||||
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
|
||||
}
|
||||
|
||||
export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||
if (!editor) return null
|
||||
export function EditorToolbar({ editor, onSelectionHighlight }: EditorToolbarProps) {
|
||||
const [linkUrl, setLinkUrl] = useState('')
|
||||
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
||||
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
const openLinkPopover = useCallback(() => {
|
||||
if (!editor) return
|
||||
const previousUrl = editor.getAttributes('link').href || ''
|
||||
setLinkUrl(previousUrl)
|
||||
|
||||
if (url === null) return
|
||||
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
return
|
||||
// Highlight the current selection while popover is open
|
||||
const { from, to } = editor.state.selection
|
||||
if (from !== to && onSelectionHighlight) {
|
||||
onSelectionHighlight({ from, to })
|
||||
}
|
||||
|
||||
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 (
|
||||
<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
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
data-active={editor.isActive('bold') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Bold"
|
||||
title="Bold (Ctrl+B)"
|
||||
>
|
||||
<BoldIcon className="size-4" />
|
||||
</Button>
|
||||
|
|
@ -55,7 +124,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
|||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
data-active={editor.isActive('italic') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Italic"
|
||||
title="Italic (Ctrl+I)"
|
||||
>
|
||||
<ItalicIcon className="size-4" />
|
||||
</Button>
|
||||
|
|
@ -82,6 +151,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
|||
|
||||
<div className="separator" />
|
||||
|
||||
{/* Headings */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
|
|
@ -115,6 +185,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
|||
|
||||
<div className="separator" />
|
||||
|
||||
{/* Lists */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
|
|
@ -135,9 +206,20 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
|||
>
|
||||
<ListOrderedIcon className="size-4" />
|
||||
</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" />
|
||||
|
||||
{/* Blocks */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
|
|
@ -166,16 +248,78 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
|||
>
|
||||
<MinusIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={setLink}
|
||||
data-active={editor.isActive('link') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Link"
|
||||
|
||||
{/* Link with popover */}
|
||||
<Popover
|
||||
open={isLinkPopoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
closeLinkPopover()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkIcon className="size-4" />
|
||||
</Button>
|
||||
<PopoverTrigger asChild>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 Link from '@tiptap/extension-link'
|
||||
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 { useEffect, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { EditorToolbar } from './editor-toolbar'
|
||||
|
|
@ -30,6 +34,41 @@ type WikiLinkMatch = {
|
|||
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({
|
||||
content,
|
||||
onChange,
|
||||
|
|
@ -40,6 +79,17 @@ export function MarkdownEditor({
|
|||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | 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({
|
||||
extensions: [
|
||||
|
|
@ -62,6 +112,10 @@ export function MarkdownEditor({
|
|||
}
|
||||
: undefined,
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
}),
|
||||
|
|
@ -71,6 +125,7 @@ export function MarkdownEditor({
|
|||
transformCopiedText: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
selectionHighlightExtension,
|
||||
],
|
||||
content: '',
|
||||
onUpdate: ({ editor }) => {
|
||||
|
|
@ -184,6 +239,14 @@ export function MarkdownEditor({
|
|||
}
|
||||
}, [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 filteredFiles = useMemo(() => {
|
||||
if (!activeWikiLink) return []
|
||||
|
|
@ -237,7 +300,7 @@ export function MarkdownEditor({
|
|||
|
||||
return (
|
||||
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
|
||||
<EditorToolbar editor={editor} />
|
||||
<EditorToolbar editor={editor} onSelectionHighlight={setSelectionHighlight} />
|
||||
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
|
||||
<EditorContent editor={editor} />
|
||||
{wikiLinks ? (
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import {
|
|||
FolderPlus,
|
||||
Loader2,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
MessageSquarePlus,
|
||||
Microscope,
|
||||
Network,
|
||||
Pencil,
|
||||
|
|
@ -88,11 +86,9 @@ type SidebarContentPanelProps = {
|
|||
expandedPaths: Set<string>
|
||||
onSelectFile: (path: string, kind: "file" | "dir") => void
|
||||
knowledgeActions: KnowledgeActions
|
||||
chats: { id: string; title: string; preview: string; time: string }[]
|
||||
} & React.ComponentProps<typeof Sidebar>
|
||||
|
||||
const sectionTitles = {
|
||||
"ask-ai": "Ask AI",
|
||||
knowledge: "Knowledge",
|
||||
agents: "Agents",
|
||||
}
|
||||
|
|
@ -166,7 +162,6 @@ export function SidebarContentPanel({
|
|||
expandedPaths,
|
||||
onSelectFile,
|
||||
knowledgeActions,
|
||||
chats,
|
||||
...props
|
||||
}: SidebarContentPanelProps) {
|
||||
const { activeSection } = useSidebarSection()
|
||||
|
|
@ -179,9 +174,6 @@ export function SidebarContentPanel({
|
|||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{activeSection === "ask-ai" && (
|
||||
<ChatSection chats={chats} />
|
||||
)}
|
||||
{activeSection === "knowledge" && (
|
||||
<KnowledgeSection
|
||||
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
|
||||
function KnowledgeSection({
|
||||
tree,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
HelpCircle,
|
||||
Settings,
|
||||
Ship,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
} from "lucide-react"
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ type SecondaryItem = {
|
|||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ id: "ask-ai", title: "Ask AI", icon: Sparkles },
|
||||
{ id: "knowledge", title: "Knowledge", icon: Brain },
|
||||
{ id: "agents", title: "Agents", icon: Bot },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ const SIDEBAR_WIDTH_MIN = 200
|
|||
const SIDEBAR_WIDTH_MAX = 480
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
const SIDEBAR_OFFSET = "0px" // Default offset for nested sidebars
|
||||
|
||||
type SidebarContextProps = {
|
||||
|
|
@ -100,22 +99,6 @@ function SidebarProvider({
|
|||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [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".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from "react"
|
||||
|
||||
export type ActiveSection = "ask-ai" | "knowledge" | "agents"
|
||||
export type ActiveSection = "knowledge" | "agents"
|
||||
|
||||
type SidebarSectionContextProps = {
|
||||
activeSection: ActiveSection
|
||||
|
|
@ -20,7 +20,7 @@ export function useSidebarSection() {
|
|||
}
|
||||
|
||||
export function SidebarSectionProvider({
|
||||
defaultSection = "ask-ai",
|
||||
defaultSection = "knowledge",
|
||||
onSectionChange,
|
||||
children,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -175,17 +175,41 @@
|
|||
.tiptap-editor .ProseMirror ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5em;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label {
|
||||
flex-shrink: 0;
|
||||
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 */
|
||||
|
|
@ -228,3 +252,32 @@
|
|||
width: 0;
|
||||
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
24
apps/x/pnpm-lock.yaml
generated
|
|
@ -130,6 +130,12 @@ importers:
|
|||
'@tiptap/extension-placeholder':
|
||||
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))
|
||||
'@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':
|
||||
specifier: ^3.15.3
|
||||
version: 3.15.3
|
||||
|
|
@ -1775,6 +1781,16 @@ packages:
|
|||
peerDependencies:
|
||||
'@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':
|
||||
resolution: {integrity: sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==}
|
||||
peerDependencies:
|
||||
|
|
@ -5929,6 +5945,14 @@ snapshots:
|
|||
dependencies:
|
||||
'@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))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue