fix: Use fixed-position slide-over and render markdown descriptions with tiptap
- Switch TaskSlideOver from absolute to fixed positioning so it overlays the viewport instead of fighting with phase panel scroll - Render task descriptions as rich HTML via markdownToSafeHtml (markdown → tiptap JSON → HTML roundtrip) with prose typography classes - Move TaskSlideOver render to ExecutionProvider level in ExecutionTab - Remove unnecessary relative overflow-hidden wrapper from PhaseDetailPanel - Export markdownToSafeHtml from markdown-to-tiptap utility
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
PhaseDetailPanel,
|
||||
PhaseDetailEmpty,
|
||||
} from "@/components/execution/PhaseDetailPanel";
|
||||
import { TaskSlideOver } from "@/components/execution/TaskSlideOver";
|
||||
import { Skeleton } from "@/components/Skeleton";
|
||||
|
||||
interface ExecutionTabProps {
|
||||
@@ -179,6 +180,7 @@ export function ExecutionTab({
|
||||
phases={sortedPhases}
|
||||
onAddPhase={handleStartAdd}
|
||||
/>
|
||||
<TaskSlideOver />
|
||||
</ExecutionProvider>
|
||||
);
|
||||
}
|
||||
@@ -252,7 +254,7 @@ export function ExecutionTab({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TaskSlideOver />
|
||||
</ExecutionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { mapEntityStatus } from "@/components/StatusDot";
|
||||
import { PhaseNumberBadge } from "@/components/DependencyChip";
|
||||
import { type SerializedTask } from "@/components/TaskRow";
|
||||
import { TaskGraph } from "./TaskGraph";
|
||||
import { TaskSlideOver } from "./TaskSlideOver";
|
||||
import { PhaseContentEditor } from "@/components/editor/PhaseContentEditor";
|
||||
import { ChangeSetBanner } from "@/components/ChangeSetBanner";
|
||||
import { Skeleton } from "@/components/Skeleton";
|
||||
@@ -210,7 +209,6 @@ export function PhaseDetailPanel({
|
||||
detailAgent?.status === "idle" && !!latestChangeSet;
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -424,8 +422,6 @@ export function PhaseDetailPanel({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TaskSlideOver />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { X, Trash2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { StatusDot } from "@/components/StatusDot";
|
||||
import { getCategoryConfig } from "@/lib/category";
|
||||
import { markdownToSafeHtml } from "@/lib/markdown-to-tiptap";
|
||||
import { useExecutionContext } from "./ExecutionContext";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -35,13 +36,22 @@ export function TaskSlideOver() {
|
||||
dependencies.every((d) => d.status === "completed");
|
||||
const canQueue = task !== null && task.status === "pending" && allDepsComplete;
|
||||
|
||||
const descriptionHtml = useMemo(() => {
|
||||
if (!task?.description) return null;
|
||||
try {
|
||||
return markdownToSafeHtml(task.description);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [task?.description]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{selectedEntry && task && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
className="absolute inset-0 z-10 bg-background/60 backdrop-blur-[2px]"
|
||||
className="fixed inset-0 z-40 bg-background/60 backdrop-blur-[2px]"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
@@ -51,7 +61,7 @@ export function TaskSlideOver() {
|
||||
|
||||
{/* Panel */}
|
||||
<motion.div
|
||||
className="absolute inset-y-0 right-0 z-20 flex w-full max-w-md flex-col border-l border-border bg-background shadow-xl"
|
||||
className="fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col border-l border-border bg-background shadow-xl"
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
@@ -100,9 +110,16 @@ export function TaskSlideOver() {
|
||||
|
||||
{/* Description */}
|
||||
<Section title="Description">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{task.description ?? "No description"}
|
||||
</p>
|
||||
{descriptionHtml ? (
|
||||
<div
|
||||
className="prose prose-sm prose-p:my-1 prose-headings:mb-1 prose-headings:mt-3 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 prose-blockquote:my-1 prose-pre:my-1 prose-hr:my-2 dark:prose-invert max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No description
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* Dependencies */}
|
||||
|
||||
@@ -27,14 +27,12 @@ interface PipelineTabProps {
|
||||
export function PipelineTab({ initiativeId, phases, phasesLoading }: PipelineTabProps) {
|
||||
return (
|
||||
<ExecutionProvider>
|
||||
<div className="relative overflow-hidden">
|
||||
<PipelineTabInner
|
||||
initiativeId={initiativeId}
|
||||
phases={phases}
|
||||
phasesLoading={phasesLoading}
|
||||
/>
|
||||
<TaskSlideOver />
|
||||
</div>
|
||||
<PipelineTabInner
|
||||
initiativeId={initiativeId}
|
||||
phases={phases}
|
||||
phasesLoading={phasesLoading}
|
||||
/>
|
||||
<TaskSlideOver />
|
||||
</ExecutionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,16 +5,27 @@
|
||||
* Uses @tiptap/html's generateJSON to parse HTML into Tiptap nodes.
|
||||
*/
|
||||
|
||||
import { generateJSON } from '@tiptap/html';
|
||||
import { generateJSON, generateHTML } from '@tiptap/html';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table';
|
||||
|
||||
const extensions = [StarterKit, Table, TableRow, TableCell, TableHeader];
|
||||
|
||||
/**
|
||||
* Convert markdown string to Tiptap JSON document.
|
||||
*/
|
||||
export function markdownToTiptapJson(markdown: string): object {
|
||||
const html = markdownToHtml(markdown);
|
||||
return generateJSON(html, [StarterKit, Table, TableRow, TableCell, TableHeader]);
|
||||
return generateJSON(html, extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown string to sanitized HTML via tiptap roundtrip.
|
||||
* Markdown → HTML → tiptap JSON → HTML ensures only known nodes survive.
|
||||
*/
|
||||
export function markdownToSafeHtml(markdown: string): string {
|
||||
const json = markdownToTiptapJson(markdown);
|
||||
return generateHTML(json as Parameters<typeof generateHTML>[0], extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user