// 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 });