// PapersVal — primitives: icons, pills, helpers
// Exported via window for app.jsx
// ---------- Icons (inline SVG, currentColor) ----------
const Icon = {
Search: () => (
),
Plus: () => (
),
Close: () => (
),
Copy: () => (
),
Check: () => (
),
ArrowLeft: () => (
),
ArrowRight: () => (
),
ChevronDown: () => (
),
Share: () => (
),
Download: () => (
),
More: () => (
),
Link: () => (
),
Edit: () => (
),
Sparkle: () => (
),
Folder: () => (
),
Menu: () => (
),
Refresh: () => (
),
Accept: () => (
),
Replace: () => (
),
Note: () => (
)
};
// ---------- Status / Severity language ----------
const STATUS_LABEL = {
match: "Matches",
partial: "Partial match",
does_not_match: "Mismatch",
not_found: "Not found",
needs_review: "Needs review",
extra: "Extra clause",
n_a: "Not applicable"
};
const SEV_LABEL = { high: "High", medium: "Medium", low: "Low", info: "Extra" };
function StatusPill({ status, compact }) {
const cls = status === "does_not_match" ? "dnm" : status;
return (
{STATUS_LABEL[status]}
);
}
function SevPill({ severity }) {
const cls = severity === "medium" ? "med" : severity === "info" ? "info" : severity;
return {SEV_LABEL[severity]};
}
// ---------- Render contract paragraph with marks ----------
// Given paragraph text and an array of marks for this paragraph, return JSX
// with wrappers around the right offsets.
// findingsState: { [id]: { resolution: 'replaced' | 'accepted' | 'noted', ... } }
// viewMode: 'review' (default — annotations + original) | 'final' (suggestions applied inline)
// filterMatch: function(finding) -> bool. If supplied, non-matching marks render as dimmed.
function renderParagraph(text, marks, selectedFindingId, onMarkClick, findingsById, findingsState, viewMode = "review", filterMatch = null) {
if (!marks || marks.length === 0) return text;
const sorted = [...marks].sort((a, b) => a.start - b.start);
const out = [];
let cursor = 0;
sorted.forEach((m, i) => {
if (m.start > cursor) out.push(text.slice(cursor, m.start));
const finding = findingsById[m.findingId];
const isSelected = selectedFindingId === m.findingId;
const isConflict = finding && finding.severity === "high";
const isExtra = finding && finding.status === "extra";
const state = findingsState && findingsState[m.findingId];
const replaced = state?.resolution === "replaced";
const accepted = state?.resolution === "accepted";
const noted = state?.resolution === "noted";
const originalSlice = text.slice(m.start, m.end);
const matchesFilter = !filterMatch || (finding && filterMatch(finding));
if (viewMode === "final") {
if (replaced && finding?.suggested) {
out.push({finding.suggested});
} else {
out.push(originalSlice);
}
} else {
const cls = ["mark"];
if (isConflict) cls.push("conflict");
if (isExtra) cls.push("extra");
if (isSelected) cls.push("selected");
if (replaced) cls.push("applied");
if (accepted) cls.push("accepted");
if (noted) cls.push("noted");
if (!matchesFilter) cls.push("dimmed");
out.push(
{ e.stopPropagation(); onMarkClick && onMarkClick(m.findingId); }}>
{originalSlice}
);
}
cursor = m.end;
});
if (cursor < text.length) out.push(text.slice(cursor));
return out;
}
// ---------- Mini stat (for sidebar history rows) ----------
function MiniStat({ kind, value, glyph, children }) {
return (
{children}
);
}
Object.assign(window, {
Icon,
StatusPill,
SevPill,
renderParagraph,
MiniStat,
STATUS_LABEL,
SEV_LABEL
});