Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions app/(dashboard)/assets/[id]/asset-content-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { useState, useTransition } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { RichTextEditor } from '@/components/rich-text-editor'
import { AlignLeft, NotebookPen, Pencil } from 'lucide-react'
import { toast } from 'sonner'
import { updateAssetContentAction } from '../../actions'
Expand Down Expand Up @@ -64,26 +65,21 @@ export function AssetContentCard({ assetId, productSlug, field, value }: Editabl
<CardContent>
{editing ? (
<div className="space-y-2">
<Textarea
<RichTextEditor
value={draft}
onChange={(e) => setDraft(e.target.value)}
rows={field === 'notes' ? 10 : 4}
placeholder={meta.placeholder}
onChange={setDraft}
autoFocus
size={field === 'notes' ? 'tall' : 'default'}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => setEditing(false)} disabled={isPending}>Cancel</Button>
<Button size="sm" onClick={save} disabled={isPending}>{isPending ? 'Saving…' : 'Save'}</Button>
</div>
</div>
) : value ? (
field === 'notes' ? (
<div className="prose prose-sm prose-invert max-w-none [&_h1]:text-base [&_h2]:text-sm [&_h3]:text-sm [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_code]:text-xs [&_table]:text-xs">
<ReactMarkdown>{value}</ReactMarkdown>
</div>
) : (
<p className="text-sm text-muted-foreground whitespace-pre-line">{value}</p>
)
<div className="prose prose-sm prose-invert max-w-none [&_h1]:text-base [&_h2]:text-sm [&_h3]:text-sm [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_code]:text-xs [&_table]:text-xs [&_ul.contains-task-list]:list-none [&_ul.contains-task-list]:pl-1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{value}</ReactMarkdown>
</div>
) : (
<p className="text-sm text-muted-foreground italic">{meta.empty}</p>
)}
Expand Down
5 changes: 3 additions & 2 deletions app/(dashboard)/plans/[id]/plan-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { RichTextField } from '@/components/rich-text-field'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Sheet,
Expand Down Expand Up @@ -120,8 +121,8 @@ export function PlanEditSheet({ plan, members = [] }: { plan: CodePlanDetail; me
<Input id="edit-title" name="title" defaultValue={plan.title} required />
</div>
<div className="space-y-2">
<Label htmlFor="edit-description">Description</Label>
<Textarea id="edit-description" name="description" defaultValue={plan.description} rows={3} />
<Label>Description</Label>
<RichTextField name="description" defaultValue={plan.description} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
Expand Down
6 changes: 3 additions & 3 deletions app/(dashboard)/plans/plan-create-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState, useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { RichTextField } from '@/components/rich-text-field'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Sheet,
Expand Down Expand Up @@ -82,8 +82,8 @@ export function PlanCreatePanel({
</div>

<div className="space-y-2">
<Label htmlFor="np-description">Description <span className="text-destructive">*</span></Label>
<Textarea id="np-description" name="description" placeholder="What changes does this plan coordinate?" rows={3} required />
<Label>Description <span className="text-destructive">*</span></Label>
<RichTextField name="description" />
</div>

<div className="grid gap-3 sm:grid-cols-2">
Expand Down
23 changes: 12 additions & 11 deletions app/(dashboard)/work-items/work-item-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Sheet,
Expand All @@ -30,6 +29,8 @@ import {
import { Trash2, ArrowUpRight, Link2, Unlink, ExternalLink, FileText } from 'lucide-react'
import { toast } from 'sonner'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { RichTextField } from '@/components/rich-text-field'
import type { WorkItemStatus } from '@/lib/types'
import type { WorkItemWithContext } from '@/lib/db/queries'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -259,7 +260,13 @@ function WorkItemEditor({

<form ref={formRef} onBlur={() => commit()} onSubmit={(e) => e.preventDefault()} className="space-y-4 px-4">
<Input name="title" defaultValue={item.title} disabled={isMirrored} className="font-medium" aria-label="Title" />
<Textarea name="description" defaultValue={item.description} disabled={isMirrored} rows={3} placeholder="Description" aria-label="Description" />
{isMirrored ? (
<div className="prose prose-sm prose-invert max-w-none rounded-md border border-border px-3 py-2 [&_table]:text-xs">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{item.description}</ReactMarkdown>
</div>
) : (
<RichTextField name="description" defaultValue={item.description} />
)}
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label className="text-xs">Asset</Label>
Expand Down Expand Up @@ -405,7 +412,7 @@ function SpecSection({ specUrl }: { specUrl: string }) {
</p>
{markdown ? (
<div className="prose prose-sm prose-invert max-w-none max-h-64 overflow-y-auto rounded-md border border-border p-3 [&_h1]:text-base [&_h2]:text-sm [&_h3]:text-sm [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_code]:text-xs [&_table]:text-xs">
<ReactMarkdown>{markdown}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{markdown}</ReactMarkdown>
</div>
) : (
<p className="text-xs text-muted-foreground">
Expand Down Expand Up @@ -490,14 +497,8 @@ function WorkItemForm({
</div>

<div className="space-y-2">
<Label htmlFor="wi-description">Description</Label>
<Textarea
id="wi-description"
name="description"
defaultValue={item?.description}
placeholder="What is being asked for, or what is wrong?"
rows={3}
/>
<Label>Description</Label>
<RichTextField name="description" defaultValue={item?.description} />
</div>

<div className="grid gap-3 sm:grid-cols-2">
Expand Down
165 changes: 165 additions & 0 deletions components/rich-text-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
'use client'

import { useEditor, EditorContent, type Editor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
import { TableKit } from '@tiptap/extension-table'
import { TaskItem, TaskList } from '@tiptap/extension-list'
import { Button } from '@/components/ui/button'
import {
Bold,
Italic,
Strikethrough,
Code,
Heading2,
Heading3,
List,
ListOrdered,
ListTodo,
Quote,
SquareCode,
Table as TableIcon,
Undo,
Redo,
} from 'lucide-react'
import { cn } from '@/lib/utils'

/**
* Markdown-canonical rich text editor: takes markdown in, emits markdown out
* (the same dialect the MCP server reads/writes). GFM: tables, task lists,
* strikethrough.
*/
export function RichTextEditor({
value,
onChange,
autoFocus = false,
size = 'default',
}: {
/** Markdown source. Read once on mount — the editor owns the content after that. */
value: string
onChange: (markdown: string) => void
autoFocus?: boolean
size?: 'default' | 'tall'
}) {
const editor = useEditor({
extensions: [
StarterKit,
Markdown,
TableKit.configure({ table: { resizable: false } }),
TaskList,
TaskItem.configure({ nested: true }),
],
content: value,
contentType: 'markdown',
autofocus: autoFocus ? 'end' : false,
immediatelyRender: false,
shouldRerenderOnTransaction: true,
onUpdate: ({ editor }) => onChange(editor.getMarkdown()),
editorProps: {
attributes: {
class: cn(
'prose prose-sm prose-invert max-w-none focus:outline-none px-3 py-2',
'[&_h1]:text-base [&_h2]:text-sm [&_h3]:text-sm [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_code]:text-xs [&_table]:text-xs',
'[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1 [&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1',
'[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-1',
'[&_ul[data-type=taskList]_li]:flex [&_ul[data-type=taskList]_li]:gap-2 [&_ul[data-type=taskList]_label]:mt-0.5',
size === 'tall' ? 'min-h-56' : 'min-h-28',
),
},
},
})

return (
<div className="rounded-md border border-input bg-transparent focus-within:ring-1 focus-within:ring-ring">
{editor && <Toolbar editor={editor} />}
<EditorContent editor={editor} />
</div>
)
}

function ToolbarButton({
onClick,
active = false,
disabled = false,
title,
children,
}: {
onClick: () => void
active?: boolean
disabled?: boolean
title: string
children: React.ReactNode
}) {
return (
<Button
type="button"
variant={active ? 'secondary' : 'ghost'}
size="icon"
className="h-7 w-7"
title={title}
disabled={disabled}
onMouseDown={(e) => e.preventDefault()}
onClick={onClick}
>
{children}
</Button>
)
}

function Toolbar({ editor }: { editor: Editor }) {
const c = () => editor.chain().focus()
return (
<div className="flex flex-wrap items-center gap-0.5 border-b border-border px-1.5 py-1">
<ToolbarButton title="Bold" active={editor.isActive('bold')} onClick={() => c().toggleBold().run()}>
<Bold className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Italic" active={editor.isActive('italic')} onClick={() => c().toggleItalic().run()}>
<Italic className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Strikethrough" active={editor.isActive('strike')} onClick={() => c().toggleStrike().run()}>
<Strikethrough className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Inline code" active={editor.isActive('code')} onClick={() => c().toggleCode().run()}>
<Code className="h-3.5 w-3.5" />
</ToolbarButton>
<span className="mx-1 h-4 w-px bg-border" />
<ToolbarButton title="Heading 2" active={editor.isActive('heading', { level: 2 })} onClick={() => c().toggleHeading({ level: 2 }).run()}>
<Heading2 className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Heading 3" active={editor.isActive('heading', { level: 3 })} onClick={() => c().toggleHeading({ level: 3 }).run()}>
<Heading3 className="h-3.5 w-3.5" />
</ToolbarButton>
<span className="mx-1 h-4 w-px bg-border" />
<ToolbarButton title="Bullet list" active={editor.isActive('bulletList')} onClick={() => c().toggleBulletList().run()}>
<List className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Numbered list" active={editor.isActive('orderedList')} onClick={() => c().toggleOrderedList().run()}>
<ListOrdered className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Task list" active={editor.isActive('taskList')} onClick={() => c().toggleList('taskList', 'taskItem').run()}>
<ListTodo className="h-3.5 w-3.5" />
</ToolbarButton>
<span className="mx-1 h-4 w-px bg-border" />
<ToolbarButton title="Blockquote" active={editor.isActive('blockquote')} onClick={() => c().toggleBlockquote().run()}>
<Quote className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Code block" active={editor.isActive('codeBlock')} onClick={() => c().toggleCodeBlock().run()}>
<SquareCode className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton
title="Insert table"
active={editor.isActive('table')}
onClick={() => c().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
>
<TableIcon className="h-3.5 w-3.5" />
</ToolbarButton>
<span className="mx-1 h-4 w-px bg-border" />
<ToolbarButton title="Undo" disabled={!editor.can().undo()} onClick={() => c().undo().run()}>
<Undo className="h-3.5 w-3.5" />
</ToolbarButton>
<ToolbarButton title="Redo" disabled={!editor.can().redo()} onClick={() => c().redo().run()}>
<Redo className="h-3.5 w-3.5" />
</ToolbarButton>
</div>
)
}
26 changes: 26 additions & 0 deletions components/rich-text-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import { useState } from 'react'
import { RichTextEditor } from '@/components/rich-text-editor'

/**
* RichTextEditor adapter for FormData-based forms: mirrors the markdown into
* a hidden input so form submit / onBlur-commit flows pick it up unchanged.
*/
export function RichTextField({
name,
defaultValue = '',
size = 'default',
}: {
name: string
defaultValue?: string
size?: 'default' | 'tall'
}) {
const [markdown, setMarkdown] = useState(defaultValue)
return (
<div>
<input type="hidden" name={name} value={markdown} readOnly />
<RichTextEditor value={defaultValue} onChange={setMarkdown} size={size} />
</div>
)
}
3 changes: 2 additions & 1 deletion components/spec-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { FileText, ExternalLink } from 'lucide-react'

Expand Down Expand Up @@ -27,7 +28,7 @@ export function SpecCard({ specUrl, markdown }: { specUrl: string; markdown: str
<CardContent>
{markdown ? (
<div className="prose prose-sm prose-invert max-w-none max-h-[480px] overflow-y-auto [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:rounded [&_code]:text-xs [&_table]:text-sm">
<ReactMarkdown>{markdown}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{markdown}</ReactMarkdown>
</div>
) : (
<p className="text-sm text-muted-foreground">
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeplans",
"version": "0.3.25",
"version": "0.3.26",
"description": "Manage and track coordinated changes across your software architecture.",
"author": "Sai Prakash <sylonzero@gmail.com>",
"homepage": "https://codeplans.ai",
Expand Down Expand Up @@ -52,6 +52,12 @@
"@radix-ui/react-tooltip": "1.2.8",
"@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.100.1",
"@tiptap/extension-list": "^3.28.0",
"@tiptap/extension-table": "^3.28.0",
"@tiptap/markdown": "^3.28.0",
"@tiptap/pm": "^3.28.0",
"@tiptap/react": "^3.28.0",
"@tiptap/starter-kit": "^3.28.0",
"@vercel/analytics": "1.6.1",
"autoprefixer": "^10.4.20",
"bcryptjs": "^3.0.3",
Expand All @@ -75,6 +81,7 @@
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.0",
"remark-gfm": "^4.0.1",
"resend": "^6.12.4",
"sonner": "^1.7.1",
"tailwind-merge": "^3.3.1",
Expand Down
Loading
Loading