1
0
Fork 0

feat(wiki): add rendered template preview to template dialog

Template dialog gains a rendered preview (markdown pipeline + scoped template CSS + HTML templates) alongside the existing source preview, with Rendered/Markdown toggle; editor threads the template maps through.

Ultraworked with [Sisyphus](https://github.com/code-yeongju/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-06 17:22:54 -04:00
parent 44b78ba7d9
commit 28250550ab
3 changed files with 84 additions and 6 deletions

View file

@ -215,6 +215,8 @@ export function WikiEditor({
onLinkDialogChange={setLinkDialogOpen} onLinkDialogChange={setLinkDialogOpen}
pending={pending} pending={pending}
templates={templates} templates={templates}
templateCssMap={templateCssMap}
templateHtmlMap={templateHtmlMap}
/> />
</div> </div>

View file

@ -63,6 +63,8 @@ interface WikiEditorBodyProps {
readonly onLinkDialogChange: (open: boolean) => void; readonly onLinkDialogChange: (open: boolean) => void;
readonly pending: boolean; readonly pending: boolean;
readonly templates: TemplateMap; readonly templates: TemplateMap;
readonly templateCssMap?: Record<string, string>;
readonly templateHtmlMap?: Record<string, string>;
} }
export function WikiEditorBody({ export function WikiEditorBody({
@ -74,6 +76,8 @@ export function WikiEditorBody({
onLinkDialogChange, onLinkDialogChange,
pending, pending,
templates, templates,
templateCssMap,
templateHtmlMap,
}: WikiEditorBodyProps) { }: WikiEditorBodyProps) {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const mirrorRef = useRef<HTMLDivElement>(null); const mirrorRef = useRef<HTMLDivElement>(null);
@ -429,6 +433,8 @@ export function WikiEditorBody({
onOpenChange={setTemplateDialogOpen} onOpenChange={setTemplateDialogOpen}
disabled={pending} disabled={pending}
onInsert={insertTemplateBody} onInsert={insertTemplateBody}
templateCssMap={templateCssMap}
templateHtmlMap={templateHtmlMap}
/> />
<WikiEditorToolbar groups={groups} disabled={pending || uploading} /> <WikiEditorToolbar groups={groups} disabled={pending || uploading} />
<div className="relative min-h-[23.5rem] w-full rounded-md border border-input bg-transparent shadow-sm transition-[box-shadow] focus-within:border-ring focus-within:ring-1 focus-within:ring-ring lg:h-[calc(100vh-10rem)]"> <div className="relative min-h-[23.5rem] w-full rounded-md border border-input bg-transparent shadow-sm transition-[box-shadow] focus-within:border-ring focus-within:ring-1 focus-within:ring-ring lg:h-[calc(100vh-10rem)]">

View file

@ -14,6 +14,9 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { extractPlaceholders, type TemplateMap } from "@/lib/wiki/templates"; import { extractPlaceholders, type TemplateMap } from "@/lib/wiki/templates";
import { prepareMarkdown } from "@/lib/wiki/prepare";
import { buildTemplateStyles } from "@/lib/wiki/templateCss";
import { WikiPreparedBody } from "@/components/frontend/wiki/WikiContent";
interface WikiTemplateDialogProps { interface WikiTemplateDialogProps {
readonly templates: TemplateMap; readonly templates: TemplateMap;
@ -21,6 +24,8 @@ interface WikiTemplateDialogProps {
readonly onOpenChange: (open: boolean) => void; readonly onOpenChange: (open: boolean) => void;
readonly disabled?: boolean; readonly disabled?: boolean;
readonly onInsert: (body: string) => void; readonly onInsert: (body: string) => void;
readonly templateCssMap?: Record<string, string>;
readonly templateHtmlMap?: Record<string, string>;
} }
function escapeRegExp(text: string): string { function escapeRegExp(text: string): string {
@ -48,9 +53,14 @@ export function WikiTemplateDialog({
onOpenChange, onOpenChange,
disabled, disabled,
onInsert, onInsert,
templateCssMap,
templateHtmlMap,
}: WikiTemplateDialogProps) { }: WikiTemplateDialogProps) {
const [selectedName, setSelectedName] = useState<string | null>(null); const [selectedName, setSelectedName] = useState<string | null>(null);
const [values, setValues] = useState<Record<string, string>>({}); const [values, setValues] = useState<Record<string, string>>({});
const [previewMode, setPreviewMode] = useState<"rendered" | "source">(
"rendered",
);
const names = useMemo(() => Object.keys(templates).sort(), [templates]); const names = useMemo(() => Object.keys(templates).sort(), [templates]);
const selectedBody = selectedName !== null ? (templates[selectedName] ?? "") : ""; const selectedBody = selectedName !== null ? (templates[selectedName] ?? "") : "";
@ -62,11 +72,35 @@ export function WikiTemplateDialog({
() => fillTemplate(selectedBody, values), () => fillTemplate(selectedBody, values),
[selectedBody, values], [selectedBody, values],
); );
const preparedPreview = useMemo(
() =>
selectedName !== null
? prepareMarkdown(filled, templates, templateHtmlMap ?? {})
: "",
[selectedName, filled, templates, templateHtmlMap],
);
const { css: previewCss, scopeClasses: previewScopeClasses } = useMemo(
() =>
buildTemplateStyles(
filled,
selectedName !== null
? [
{
name: selectedName,
body: templates[selectedName] ?? "",
css: templateCssMap?.[selectedName] ?? null,
},
]
: [],
),
[filled, selectedName, templates, templateCssMap],
);
function handleOpenChange(next: boolean) { function handleOpenChange(next: boolean) {
if (!next) { if (!next) {
setSelectedName(null); setSelectedName(null);
setValues({}); setValues({});
setPreviewMode("rendered");
} }
onOpenChange(next); onOpenChange(next);
} }
@ -143,12 +177,48 @@ export function WikiTemplateDialog({
</p> </p>
)} )}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
Preview Preview
</span> </span>
<div className="flex items-center gap-1">
<Button
type="button"
size="sm"
variant={previewMode === "rendered" ? "secondary" : "ghost"}
className="h-6 px-2 text-xs"
onClick={() => setPreviewMode("rendered")}
>
Rendered
</Button>
<Button
type="button"
size="sm"
variant={previewMode === "source" ? "secondary" : "ghost"}
className="h-6 px-2 text-xs"
onClick={() => setPreviewMode("source")}
>
Markdown
</Button>
</div>
</div>
{previewMode === "rendered" ? (
<div className="max-h-72 min-h-24 overflow-auto rounded-md border bg-muted/20 p-3">
{previewCss ? (
<style dangerouslySetInnerHTML={{ __html: previewCss }} />
) : null}
<div className={cn("wiki-markdown", previewScopeClasses)}>
<WikiPreparedBody
prepared={preparedPreview}
htmlTemplates={templateHtmlMap}
/>
</div>
</div>
) : (
<pre className="max-h-40 overflow-auto rounded-md border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap"> <pre className="max-h-40 overflow-auto rounded-md border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap">
{filled} {filled}
</pre> </pre>
)}
</div> </div>
<div className={cn("flex justify-end gap-2")}> <div className={cn("flex justify-end gap-2")}>
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>