From b223427bd37867112a8f4fb5c1e7833c25d5050a Mon Sep 17 00:00:00 2001 From: Lukas May Date: Wed, 4 Mar 2026 08:08:53 +0100 Subject: [PATCH] fix: Use fixed-position slide-over and render markdown descriptions with tiptap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/src/components/ExecutionTab.tsx | 4 ++- .../components/execution/PhaseDetailPanel.tsx | 4 --- .../components/execution/TaskSlideOver.tsx | 29 +++++++++++++++---- .../src/components/pipeline/PipelineTab.tsx | 14 ++++----- apps/web/src/lib/markdown-to-tiptap.ts | 15 ++++++++-- 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/ExecutionTab.tsx b/apps/web/src/components/ExecutionTab.tsx index 8927735..7edec19 100644 --- a/apps/web/src/components/ExecutionTab.tsx +++ b/apps/web/src/components/ExecutionTab.tsx @@ -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} /> + ); } @@ -252,7 +254,7 @@ export function ExecutionTab({ )} - + ); } diff --git a/apps/web/src/components/execution/PhaseDetailPanel.tsx b/apps/web/src/components/execution/PhaseDetailPanel.tsx index 0c9a26b..64c0e3a 100644 --- a/apps/web/src/components/execution/PhaseDetailPanel.tsx +++ b/apps/web/src/components/execution/PhaseDetailPanel.tsx @@ -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 ( -
{/* Header */}
@@ -424,8 +422,6 @@ export function PhaseDetailPanel({ )}
- -
); } diff --git a/apps/web/src/components/execution/TaskSlideOver.tsx b/apps/web/src/components/execution/TaskSlideOver.tsx index 4b5a6ce..fb5930e 100644 --- a/apps/web/src/components/execution/TaskSlideOver.tsx +++ b/apps/web/src/components/execution/TaskSlideOver.tsx @@ -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 ( {selectedEntry && task && ( <> {/* Backdrop */} -

- {task.description ?? "No description"} -

+ {descriptionHtml ? ( +
+ ) : ( +

+ No description +

+ )} {/* Dependencies */} diff --git a/apps/web/src/components/pipeline/PipelineTab.tsx b/apps/web/src/components/pipeline/PipelineTab.tsx index 45b8ab9..ea6fe12 100644 --- a/apps/web/src/components/pipeline/PipelineTab.tsx +++ b/apps/web/src/components/pipeline/PipelineTab.tsx @@ -27,14 +27,12 @@ interface PipelineTabProps { export function PipelineTab({ initiativeId, phases, phasesLoading }: PipelineTabProps) { return ( -
- - -
+ +
); } diff --git a/apps/web/src/lib/markdown-to-tiptap.ts b/apps/web/src/lib/markdown-to-tiptap.ts index 4c28903..d03aa60 100644 --- a/apps/web/src/lib/markdown-to-tiptap.ts +++ b/apps/web/src/lib/markdown-to-tiptap.ts @@ -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[0], extensions); } /**