Added rich body editing
X-Lovable-Edit-ID: edt-0c30a0d8-26db-41ed-8eb9-7a63c74c0c8a Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -46,6 +46,10 @@
|
||||
"@tanstack/react-router": "^1.168.0",
|
||||
"@tanstack/react-start": "^1.167.14",
|
||||
"@tanstack/router-plugin": "^1.167.10",
|
||||
"@tiptap/extension-text-align": "^3.22.3",
|
||||
"@tiptap/extension-underline": "^3.22.3",
|
||||
"@tiptap/react": "^3.22.3",
|
||||
"@tiptap/starter-kit": "^3.22.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Bold, Italic, Underline as UnderlineIcon, List, ListOrdered,
|
||||
AlignLeft, AlignCenter, AlignRight, AlignJustify, Quote, Undo, Redo,
|
||||
Heading2, Superscript as SupIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RichTextEditorProps {
|
||||
value: string; // HTML
|
||||
onChange: (html: string) => void;
|
||||
onInsertFootnote?: () => void;
|
||||
className?: string;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
onClick, active, disabled, children, title,
|
||||
}: {
|
||||
onClick: () => void; active?: boolean; disabled?: boolean; children: React.ReactNode; title: string;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={active ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function RichTextEditor({
|
||||
value, onChange, onInsertFootnote, className, minHeight = 400,
|
||||
}: RichTextEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({ heading: { levels: [2, 3] } }),
|
||||
Underline,
|
||||
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
||||
],
|
||||
content: value || "<p></p>",
|
||||
onUpdate: ({ editor }) => onChange(editor.getHTML()),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "prose prose-sm max-w-none focus:outline-none px-10 py-8 bg-white text-black",
|
||||
style: 'font-family: "Bookman Old Style", "URW Bookman", Georgia, serif; font-size: 12pt; line-height: 1.5;',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Keep editor in sync when value is reset externally
|
||||
useEffect(() => {
|
||||
if (editor && value !== editor.getHTML()) {
|
||||
editor.commands.setContent(value || "<p></p>", { emitUpdate: false });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value === ""]);
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("border rounded-md overflow-hidden bg-card", className)}>
|
||||
<Toolbar editor={editor} onInsertFootnote={onInsertFootnote} />
|
||||
<div className="bg-muted/20 overflow-auto" style={{ minHeight }}>
|
||||
<div className="max-w-[8.5in] mx-auto my-4 shadow-sm">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Toolbar({ editor, onInsertFootnote }: { editor: Editor; onInsertFootnote?: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b bg-muted/30">
|
||||
<ToolbarButton title="Undo" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()}>
|
||||
<Undo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Redo" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()}>
|
||||
<Redo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
<ToolbarButton title="Bold" active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()}>
|
||||
<Bold className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Italic" active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()}>
|
||||
<Italic className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Underline" active={editor.isActive("underline")} onClick={() => editor.chain().focus().toggleUnderline().run()}>
|
||||
<UnderlineIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
<ToolbarButton title="Heading" active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Bullet list" active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()}>
|
||||
<List className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Numbered list" active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()}>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Block quote" active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()}>
|
||||
<Quote className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
<ToolbarButton title="Align left" active={editor.isActive({ textAlign: "left" })} onClick={() => editor.chain().focus().setTextAlign("left").run()}>
|
||||
<AlignLeft className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Center" active={editor.isActive({ textAlign: "center" })} onClick={() => editor.chain().focus().setTextAlign("center").run()}>
|
||||
<AlignCenter className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Align right" active={editor.isActive({ textAlign: "right" })} onClick={() => editor.chain().focus().setTextAlign("right").run()}>
|
||||
<AlignRight className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Justify" active={editor.isActive({ textAlign: "justify" })} onClick={() => editor.chain().focus().setTextAlign("justify").run()}>
|
||||
<AlignJustify className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
{onInsertFootnote && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
<Button type="button" variant="ghost" size="sm" className="h-8 px-2 text-xs" onClick={onInsertFootnote} title="Insert footnote at cursor">
|
||||
<SupIcon className="h-3.5 w-3.5 mr-1" /> Footnote
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper exposed for the parent to insert a numbered footnote marker
|
||||
export function insertFootnoteMarker(editor: Editor | null, num: number) {
|
||||
if (!editor) return;
|
||||
editor.chain().focus().insertContent(`<sup>[${num}]</sup>`).run();
|
||||
}
|
||||
+173
-4
@@ -11,9 +11,16 @@ import {
|
||||
TextRun,
|
||||
WidthType,
|
||||
BorderStyle,
|
||||
Footer,
|
||||
LevelFormat,
|
||||
} from "docx";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
export interface PleadingFootnote {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface PleadingInput {
|
||||
courtType: "CIRCUIT" | "COUNTY";
|
||||
circuit: string; // e.g. "ELEVENTH"
|
||||
@@ -22,7 +29,9 @@ export interface PleadingInput {
|
||||
defendants: string; // multi-line
|
||||
caseNumber: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
bodyHtml?: string; // rich HTML body
|
||||
body?: string; // legacy plain text fallback
|
||||
footnotes?: PleadingFootnote[];
|
||||
}
|
||||
|
||||
const FONT = "Bookman Old Style";
|
||||
@@ -44,8 +53,16 @@ const verticalLine = {
|
||||
right: { style: BorderStyle.SINGLE, size: 8, color: "000000" },
|
||||
};
|
||||
|
||||
function run(text: string, opts: { bold?: boolean } = {}) {
|
||||
return new TextRun({ text, bold: opts.bold, font: FONT, size: SIZE });
|
||||
function run(text: string, opts: { bold?: boolean; italic?: boolean; underline?: boolean; superscript?: boolean; size?: number } = {}) {
|
||||
return new TextRun({
|
||||
text,
|
||||
bold: opts.bold,
|
||||
italics: opts.italic,
|
||||
underline: opts.underline ? {} : undefined,
|
||||
superScript: opts.superscript,
|
||||
font: FONT,
|
||||
size: opts.size ?? SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
function p(text: string, opts: { bold?: boolean; align?: (typeof AlignmentType)[keyof typeof AlignmentType] } = {}) {
|
||||
@@ -59,6 +76,108 @@ function emptyP() {
|
||||
return new Paragraph({ children: [run("")] });
|
||||
}
|
||||
|
||||
// ---------- HTML → docx Paragraphs ----------
|
||||
|
||||
type RunStyle = { bold?: boolean; italic?: boolean; underline?: boolean; superscript?: boolean };
|
||||
|
||||
function alignFromStyle(node: Element): (typeof AlignmentType)[keyof typeof AlignmentType] | undefined {
|
||||
const ta = (node.getAttribute("style") || "").match(/text-align:\s*(left|center|right|justify)/i)?.[1]?.toLowerCase();
|
||||
switch (ta) {
|
||||
case "center": return AlignmentType.CENTER;
|
||||
case "right": return AlignmentType.RIGHT;
|
||||
case "justify": return AlignmentType.JUSTIFIED;
|
||||
case "left": return AlignmentType.LEFT;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function collectRuns(node: Node, style: RunStyle, out: TextRun[]) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent || "";
|
||||
if (text) out.push(run(text, style));
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const el = node as Element;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const next: RunStyle = { ...style };
|
||||
if (tag === "strong" || tag === "b") next.bold = true;
|
||||
if (tag === "em" || tag === "i") next.italic = true;
|
||||
if (tag === "u") next.underline = true;
|
||||
if (tag === "sup") next.superscript = true;
|
||||
if (tag === "br") {
|
||||
out.push(new TextRun({ text: "", break: 1, font: FONT, size: SIZE }));
|
||||
return;
|
||||
}
|
||||
el.childNodes.forEach((child) => collectRuns(child, next, out));
|
||||
}
|
||||
|
||||
function blockToParagraphs(el: Element, listCtx?: { ref: string; level: number }): Paragraph[] {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const align = alignFromStyle(el);
|
||||
|
||||
if (tag === "ul" || tag === "ol") {
|
||||
const ref = tag === "ul" ? "pl-bullets" : "pl-numbers";
|
||||
const level = (listCtx?.level ?? -1) + 1;
|
||||
const out: Paragraph[] = [];
|
||||
el.querySelectorAll(":scope > li").forEach((li) => {
|
||||
const runs: TextRun[] = [];
|
||||
li.childNodes.forEach((child) => {
|
||||
if (child.nodeType === Node.ELEMENT_NODE && /^(ul|ol)$/i.test((child as Element).tagName)) return;
|
||||
collectRuns(child, {}, runs);
|
||||
});
|
||||
out.push(new Paragraph({
|
||||
numbering: { reference: ref, level },
|
||||
children: runs.length ? runs : [run("")],
|
||||
}));
|
||||
li.querySelectorAll(":scope > ul, :scope > ol").forEach((nested) => {
|
||||
out.push(...blockToParagraphs(nested as Element, { ref, level }));
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
if (tag === "blockquote") {
|
||||
const out: Paragraph[] = [];
|
||||
el.childNodes.forEach((child) => {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
out.push(...blockToParagraphs(child as Element).map((par) => par));
|
||||
} else if (child.nodeType === Node.TEXT_NODE && (child.textContent || "").trim()) {
|
||||
out.push(new Paragraph({
|
||||
alignment: align,
|
||||
indent: { left: 720 },
|
||||
children: [run(child.textContent || "")],
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (out.length === 0) out.push(new Paragraph({ indent: { left: 720 }, children: [run("")] }));
|
||||
return out;
|
||||
}
|
||||
|
||||
if (tag === "h1" || tag === "h2" || tag === "h3") {
|
||||
const runs: TextRun[] = [];
|
||||
el.childNodes.forEach((c) => collectRuns(c, { bold: true }, runs));
|
||||
return [new Paragraph({ alignment: align, children: runs.length ? runs : [run("")] })];
|
||||
}
|
||||
|
||||
// Default: paragraph
|
||||
const runs: TextRun[] = [];
|
||||
el.childNodes.forEach((c) => collectRuns(c, {}, runs));
|
||||
return [new Paragraph({ alignment: align, children: runs.length ? runs : [run("")] })];
|
||||
}
|
||||
|
||||
function htmlToParagraphs(html: string): Paragraph[] {
|
||||
if (typeof window === "undefined" || !html) return [];
|
||||
const doc = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
|
||||
const root = doc.body.firstElementChild;
|
||||
if (!root) return [];
|
||||
const out: Paragraph[] = [];
|
||||
Array.from(root.children).forEach((child) => {
|
||||
out.push(...blockToParagraphs(child));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildPleadingDoc(input: PleadingInput): Document {
|
||||
const headerLine1 = `IN THE ${input.courtType} COURT OF THE ${input.circuit} JUDICIAL CIRCUIT,`;
|
||||
const headerLine2 = `IN AND FOR ${input.county.toUpperCase()} COUNTY, FLORIDA`;
|
||||
@@ -114,14 +233,59 @@ export function buildPleadingDoc(input: PleadingInput): Document {
|
||||
bodyParagraphs.push(p(input.title.toUpperCase(), { bold: true, align: AlignmentType.CENTER }));
|
||||
bodyParagraphs.push(emptyP());
|
||||
}
|
||||
if (input.body) {
|
||||
if (input.bodyHtml && input.bodyHtml.trim()) {
|
||||
bodyParagraphs.push(...htmlToParagraphs(input.bodyHtml));
|
||||
} else if (input.body) {
|
||||
input.body.split("\n").forEach((line) => bodyParagraphs.push(p(line)));
|
||||
}
|
||||
|
||||
// Footnotes rendered as endnote-style block at bottom of document
|
||||
const footnotes = (input.footnotes || []).filter((f) => f.text.trim().length > 0);
|
||||
if (footnotes.length > 0) {
|
||||
bodyParagraphs.push(emptyP());
|
||||
bodyParagraphs.push(new Paragraph({
|
||||
border: { top: { style: BorderStyle.SINGLE, size: 6, color: "000000", space: 4 } },
|
||||
children: [run("")],
|
||||
}));
|
||||
footnotes.forEach((f) => {
|
||||
bodyParagraphs.push(new Paragraph({
|
||||
children: [
|
||||
run(`${f.id}`, { superscript: true, size: 20 }),
|
||||
run(" "),
|
||||
run(f.text, { size: 20 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: FONT, size: SIZE } } },
|
||||
},
|
||||
numbering: {
|
||||
config: [
|
||||
{
|
||||
reference: "pl-bullets",
|
||||
levels: [0, 1, 2].map((lvl) => ({
|
||||
level: lvl,
|
||||
format: LevelFormat.BULLET,
|
||||
text: "\u2022",
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720 * (lvl + 1), hanging: 360 } } },
|
||||
})),
|
||||
},
|
||||
{
|
||||
reference: "pl-numbers",
|
||||
levels: [0, 1, 2].map((lvl) => ({
|
||||
level: lvl,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: `%${lvl + 1}.`,
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720 * (lvl + 1), hanging: 360 } } },
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
@@ -130,6 +294,11 @@ export function buildPleadingDoc(input: PleadingInput): Document {
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
|
||||
},
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [run("")] })],
|
||||
}),
|
||||
},
|
||||
children: [
|
||||
p(headerLine1, { bold: true, align: AlignmentType.CENTER }),
|
||||
p(headerLine2, { bold: true, align: AlignmentType.CENTER }),
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { FL_CIRCUITS } from "@/lib/florida";
|
||||
import { Packer } from "docx";
|
||||
import { buildPleadingDoc, downloadPleading } from "@/lib/docx-pleading";
|
||||
import { buildPleadingDoc, downloadPleading, type PleadingFootnote } from "@/lib/docx-pleading";
|
||||
import { RichTextEditor } from "@/components/documents/rich-text-editor";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { toast } from "sonner";
|
||||
import { Download, Save, Loader2 } from "lucide-react";
|
||||
import { Download, Save, Loader2, Plus, Trash2 } from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/documents/pleading/new")({
|
||||
component: PleadingNewPage,
|
||||
@@ -32,7 +33,8 @@ function PleadingNewPage() {
|
||||
const [defendants, setDefendants] = useState("");
|
||||
const [caseNumber, setCaseNumber] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const [bodyHtml, setBodyHtml] = useState("<p></p>");
|
||||
const [footnotes, setFootnotes] = useState<PleadingFootnote[]>([]);
|
||||
const [docName, setDocName] = useState("Pleading");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -41,7 +43,6 @@ function PleadingNewPage() {
|
||||
return c?.counties ?? [];
|
||||
}, [circuit]);
|
||||
|
||||
// Reset county when circuit changes if current county isn't valid
|
||||
const handleCircuitChange = (v: string) => {
|
||||
setCircuit(v);
|
||||
const c = FL_CIRCUITS.find((x) => x.value === v);
|
||||
@@ -51,7 +52,7 @@ function PleadingNewPage() {
|
||||
const input = {
|
||||
courtType, circuit, county,
|
||||
plaintiffs, defendants, caseNumber,
|
||||
title, body,
|
||||
title, bodyHtml, footnotes,
|
||||
};
|
||||
|
||||
const headerLine1 = `IN THE ${courtType} COURT OF THE ${circuit} JUDICIAL CIRCUIT,`;
|
||||
@@ -75,7 +76,7 @@ function PleadingNewPage() {
|
||||
const { error: insErr } = await supabase.from("generated_documents").insert({
|
||||
name: docName || "Pleading",
|
||||
kind: "pleading",
|
||||
payload: input,
|
||||
payload: input as any,
|
||||
storage_path: path,
|
||||
created_by: user?.id,
|
||||
});
|
||||
@@ -89,6 +90,17 @@ function PleadingNewPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const addFootnote = () => {
|
||||
const nextId = (footnotes[footnotes.length - 1]?.id ?? 0) + 1;
|
||||
setFootnotes([...footnotes, { id: nextId, text: "" }]);
|
||||
};
|
||||
const updateFootnote = (id: number, text: string) =>
|
||||
setFootnotes(footnotes.map((f) => (f.id === id ? { ...f, text } : f)));
|
||||
const removeFootnote = (id: number) => {
|
||||
const remaining = footnotes.filter((f) => f.id !== id).map((f, i) => ({ ...f, id: i + 1 }));
|
||||
setFootnotes(remaining);
|
||||
};
|
||||
|
||||
return (
|
||||
<ProtectedLayout>
|
||||
<PageContainer>
|
||||
@@ -106,8 +118,8 @@ function PleadingNewPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Top row: settings + caption preview */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Form */}
|
||||
<Card>
|
||||
<CardContent className="p-5 space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
@@ -161,29 +173,25 @@ function PleadingNewPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Case No.</Label>
|
||||
<Input value={caseNumber} onChange={(e) => setCaseNumber(e.target.value)} placeholder="e.g. 2025-CA-001234" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Pleading title (optional)</Label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="e.g. COMPLAINT FOR DAMAGES" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Body (optional)</Label>
|
||||
<Textarea rows={10} value={body} onChange={(e) => setBody(e.target.value)} placeholder="Type the body of the pleading..." />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Case No.</Label>
|
||||
<Input value={caseNumber} onChange={(e) => setCaseNumber(e.target.value)} placeholder="e.g. 2025-CA-001234" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Pleading title</Label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="e.g. COMPLAINT FOR DAMAGES" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preview */}
|
||||
{/* Caption preview */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="bg-muted/30 px-4 py-2 border-b text-xs uppercase tracking-wider text-muted-foreground">Preview</div>
|
||||
<div className="bg-muted/30 px-4 py-2 border-b text-xs uppercase tracking-wider text-muted-foreground">Caption preview</div>
|
||||
<div
|
||||
className="p-10 bg-white text-black min-h-[600px]"
|
||||
className="p-8 bg-white text-black"
|
||||
style={{ fontFamily: '"Bookman Old Style", "URW Bookman", Georgia, serif', fontSize: 12 }}
|
||||
>
|
||||
<div className="text-center font-bold leading-snug">
|
||||
@@ -207,13 +215,70 @@ function PleadingNewPage() {
|
||||
{title && (
|
||||
<div className="text-center font-bold mt-8">{title.toUpperCase()}</div>
|
||||
)}
|
||||
{body && (
|
||||
<div className="mt-4 whitespace-pre-line">{body}</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Full-width body editor */}
|
||||
<Card className="mt-6">
|
||||
<CardContent className="p-5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-base">Body</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Bookman Old Style, 12pt · formatting and lists carry into the .docx
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RichTextEditor
|
||||
value={bodyHtml}
|
||||
onChange={setBodyHtml}
|
||||
minHeight={500}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Footnotes */}
|
||||
<Card className="mt-6">
|
||||
<CardContent className="p-5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-base">Footnotes</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Reference them in the body with <code className="px-1 rounded bg-muted">[1]</code>, <code className="px-1 rounded bg-muted">[2]</code>… (use the Footnote toolbar button to insert a superscript marker)
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={addFootnote}>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add footnote
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{footnotes.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground italic py-4 text-center border border-dashed rounded">
|
||||
No footnotes yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{footnotes.map((f) => (
|
||||
<div key={f.id} className="flex gap-2 items-start">
|
||||
<div className="font-bold text-sm pt-2 w-8 text-right">{f.id}.</div>
|
||||
<Textarea
|
||||
rows={2}
|
||||
value={f.text}
|
||||
onChange={(e) => updateFootnote(f.id, e.target.value)}
|
||||
placeholder="Footnote text…"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={() => removeFootnote(f.id)} title="Remove">
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
</ProtectedLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user