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:
parent
44b78ba7d9
commit
28250550ab
3 changed files with 84 additions and 6 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
Preview
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
</span>
|
Preview
|
||||||
<pre className="max-h-40 overflow-auto rounded-md border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap">
|
</span>
|
||||||
{filled}
|
<div className="flex items-center gap-1">
|
||||||
</pre>
|
<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">
|
||||||
|
{filled}
|
||||||
|
</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)}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue