← Files LegalQuants TransactionalARCHIVED FILE
skills/diligence/scripts/review_ui.py
6.59 KB · Oct 3, 2026 · 06:34 UTC
#!/usr/bin/env python3
# ruff: noqa: E501 -- shared inline CSS and JavaScript literals stay readable.
"""Shared portable brand primitives for Diligence lawyer-facing HTML.
The generated pages stay self-contained. This module is used only while
rendering and introduces no browser runtime or network dependency.
"""
from __future__ import annotations
import posixpath
import urllib.parse
CONTRACT_ID = "lq-lawyer-review-v1"
BRAND_CSS = r"""
:root {
color-scheme: light dark;
--lq-background: #f4f1ea;
--lq-surface: #ffffff;
--lq-surface-muted: #faf8f3;
--lq-foreground: #24211d;
--lq-muted-foreground: #686159;
--lq-border: #ded8cd;
--lq-primary: #173c34;
--lq-primary-foreground: #ffffff;
--lq-accent: #dcebe5;
--lq-accent-foreground: #173c34;
--lq-success: #33613c;
--lq-success-surface: #e6efe4;
--lq-attention: #684813;
--lq-attention-surface: #fff1d5;
--lq-destructive: #7a3028;
--lq-destructive-surface: #f6e7e4;
--lq-ring: #173c34;
--lq-font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--lq-font-serif: "Century Schoolbook", "Iowan Old Style", Palatino, Georgia, serif;
--lq-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--lq-background: #151718;
--lq-surface: #202325;
--lq-surface-muted: #292d2f;
--lq-foreground: #f4f1ea;
--lq-muted-foreground: #bbb5ac;
--lq-border: #44494c;
--lq-primary: #d2eee5;
--lq-primary-foreground: #17312b;
--lq-accent: #233a33;
--lq-accent-foreground: #d2eee5;
--lq-success: #a3c8a0;
--lq-success-surface: #242e1f;
--lq-attention: #ffe3a3;
--lq-attention-surface: #3b3020;
--lq-destructive: #ffc4bc;
--lq-destructive-surface: #392725;
--lq-ring: #d2eee5;
}
}
html[data-theme="light"] {
--lq-background: #f4f1ea;
--lq-surface: #ffffff;
--lq-surface-muted: #faf8f3;
--lq-foreground: #24211d;
--lq-muted-foreground: #686159;
--lq-border: #ded8cd;
--lq-primary: #173c34;
--lq-primary-foreground: #ffffff;
--lq-accent: #dcebe5;
--lq-accent-foreground: #173c34;
--lq-success: #33613c;
--lq-success-surface: #e6efe4;
--lq-attention: #684813;
--lq-attention-surface: #fff1d5;
--lq-destructive: #7a3028;
--lq-destructive-surface: #f6e7e4;
--lq-ring: #173c34;
}
html[data-theme="dark"] {
--lq-background: #151718;
--lq-surface: #202325;
--lq-surface-muted: #292d2f;
--lq-foreground: #f4f1ea;
--lq-muted-foreground: #bbb5ac;
--lq-border: #44494c;
--lq-primary: #d2eee5;
--lq-primary-foreground: #17312b;
--lq-accent: #233a33;
--lq-accent-foreground: #d2eee5;
--lq-success: #a3c8a0;
--lq-success-surface: #242e1f;
--lq-attention: #ffe3a3;
--lq-attention-surface: #3b3020;
--lq-destructive: #ffc4bc;
--lq-destructive-surface: #392725;
--lq-ring: #d2eee5;
}
strong,b,h1,h2,h3,h4,h5,h6 { font-weight: 500; }
.lq-masthead {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
padding: 12px 24px;
background: var(--lq-surface);
color: var(--lq-foreground);
border-bottom: 1px solid var(--lq-border);
font-family: var(--lq-font-sans);
}
.lq-brand { font-weight: 500; letter-spacing: .01em; }
.lq-context { display: flex; align-items: center; justify-content: flex-end; gap: 12px; color: var(--lq-muted-foreground); }
.lq-theme-toggle {
appearance: none;
border: 1px solid var(--lq-border);
border-radius: 4px;
background: var(--lq-surface-muted);
color: var(--lq-foreground);
padding: 6px 9px;
font: 400 13px var(--lq-font-sans);
cursor: pointer;
}
.lq-theme-toggle:focus-visible { outline: 2px solid var(--lq-ring); outline-offset: 2px; }
@media (max-width: 640px) {
.lq-masthead { align-items: flex-start; flex-direction: column; padding: 10px 14px; }
.lq-context { width: 100%; justify-content: space-between; }
}
@media (prefers-reduced-motion: reduce) {
* { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; }
}
"""
THEME_BUTTON = (
'<button class="lq-theme-toggle" id="theme-toggle" type="button" '
'aria-pressed="false">Use dark theme</button>'
)
THEME_JS = r"""
const themeToggle=document.getElementById('theme-toggle');
function systemTheme(){return window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}
function activeTheme(){return document.documentElement.dataset.theme||systemTheme()}
function syncThemeButton(){const dark=activeTheme()==='dark';themeToggle.textContent=dark?'Use light theme':'Use dark theme';themeToggle.setAttribute('aria-pressed',String(dark))}
themeToggle.addEventListener('click',()=>{document.documentElement.dataset.theme=activeTheme()==='dark'?'light':'dark';syncThemeButton()});
syncThemeButton();
"""
def safe_source_path(value: str) -> str:
"""Return a normalized manifest path or refuse an unsafe source path."""
if not isinstance(value, str) or not value or "\\" in value:
raise ValueError(f"unsafe source path: {value!r}")
normalized = posixpath.normpath(value)
if (
value.startswith("/")
or normalized in {".", ".."}
or normalized.startswith("../")
or ":" in normalized.split("/", 1)[0]
):
raise ValueError(f"unsafe source path: {value!r}")
return normalized
def safe_source_prefix(value: str | None) -> str:
"""Return a normalized relative source prefix or refuse absolute/URL input."""
if value in {None, ""}:
return ""
if not isinstance(value, str) or "\\" in value:
raise ValueError(f"unsafe source prefix: {value!r}")
normalized = posixpath.normpath(value)
if (
value.startswith("/")
or normalized in {".", ".."}
or normalized.startswith("../")
or ":" in normalized.split("/", 1)[0]
):
raise ValueError(f"unsafe source prefix: {value!r}")
return normalized
def source_href(
prefix: str | None, path: str, *, link_without_prefix: bool
) -> str | None:
"""Build a percent-encoded relative href after validating both components."""
safe_path = safe_source_path(path)
safe_prefix = safe_source_prefix(prefix)
if not safe_prefix and not link_without_prefix:
return None
joined = posixpath.join(safe_prefix, safe_path) if safe_prefix else safe_path
return urllib.parse.quote(joined, safe="/")
def masthead(context: str) -> str:
"""Return shared masthead markup; caller must HTML-escape context."""
return (
'<div class="lq-masthead" role="banner">'
'<div class="lq-brand">LQ · Diligence Review</div>'
f'<div class="lq-context"><span>{context}</span>{THEME_BUTTON}</div></div>'
)
SHA-256: 13e7042e3f16a4718d6514cedf92400a79e18b34186024bb6666d0b0204d9973