All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 35s
Drop the visible "העתק" / "ערוך" labels and keep just the icon — matches the editorial/judicial restraint of the surrounding card. Tooltip + aria-label preserve the affordance for hover and assistive tech. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
// Rendering helpers for the formal Israeli citation ("כללי הציטוט האחיד").
|
|
//
|
|
// Backend stores the citation as Markdown: parties' names wrapped in
|
|
// **double asterisks**, everything else regular. These helpers:
|
|
// 1. Render the citation with <strong> for the bold ranges.
|
|
// 2. Copy it to the clipboard as BOTH text/html (so Word/Docs paste
|
|
// with bold preserved) and text/plain (which keeps the markers
|
|
// so the markdown survives a plain-text paste).
|
|
|
|
import { useState } from "react";
|
|
import { Check, Copy } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
function parseSegments(md: string): Array<{ bold: boolean; text: string }> {
|
|
const out: Array<{ bold: boolean; text: string }> = [];
|
|
const re = /\*\*([^*]+)\*\*/g;
|
|
let last = 0;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = re.exec(md)) !== null) {
|
|
if (m.index > last) out.push({ bold: false, text: md.slice(last, m.index) });
|
|
out.push({ bold: true, text: m[1] });
|
|
last = re.lastIndex;
|
|
}
|
|
if (last < md.length) out.push({ bold: false, text: md.slice(last) });
|
|
return out;
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
export function FormattedCitation({
|
|
citation,
|
|
className,
|
|
}: {
|
|
citation: string;
|
|
className?: string;
|
|
}) {
|
|
const segments = parseSegments(citation);
|
|
return (
|
|
<span className={className} dir="rtl">
|
|
{segments.map((s, i) =>
|
|
s.bold ? (
|
|
<strong key={i} className="font-semibold">
|
|
{s.text}
|
|
</strong>
|
|
) : (
|
|
<span key={i}>{s.text}</span>
|
|
),
|
|
)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function CitationCopyButton({
|
|
citation,
|
|
size = "sm",
|
|
}: {
|
|
citation: string;
|
|
size?: "sm" | "xs";
|
|
}) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
async function handleCopy() {
|
|
const segments = parseSegments(citation);
|
|
const html = segments
|
|
.map((s) =>
|
|
s.bold
|
|
? `<strong>${escapeHtml(s.text)}</strong>`
|
|
: escapeHtml(s.text),
|
|
)
|
|
.join("");
|
|
const wrappedHtml = `<span dir="rtl">${html}</span>`;
|
|
try {
|
|
const cb = navigator.clipboard;
|
|
if (typeof ClipboardItem !== "undefined" && cb && "write" in cb) {
|
|
const item = new ClipboardItem({
|
|
"text/html": new Blob([wrappedHtml], { type: "text/html" }),
|
|
"text/plain": new Blob([citation], { type: "text/plain" }),
|
|
});
|
|
await cb.write([item]);
|
|
} else {
|
|
await cb.writeText(citation);
|
|
}
|
|
setCopied(true);
|
|
toast.success("המראה מקום הועתק (עם הדגשה לצדדים)");
|
|
window.setTimeout(() => setCopied(false), 1800);
|
|
} catch (err) {
|
|
console.error("citation copy failed", err);
|
|
toast.error("העתקה נכשלה");
|
|
}
|
|
}
|
|
|
|
const dims = size === "xs" ? "h-7 w-7" : "h-8 w-8";
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
title={copied ? "הועתק" : "העתק לפי כללי הציטוט (עם הדגשה לצדדים)"}
|
|
aria-label={copied ? "הועתק" : "העתק מראה מקום"}
|
|
className={`inline-flex items-center justify-center rounded-md border border-rule bg-surface hover:bg-rule-soft/50 text-ink-muted hover:text-navy ${dims}`}
|
|
>
|
|
{copied ? (
|
|
<Check className="w-3.5 h-3.5 text-emerald-600" />
|
|
) : (
|
|
<Copy className="w-3.5 h-3.5" />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|