Law Firm — Document Vault
A secure client document vault for a law firm showing matters, contracts and filings in a sortable table with type icons, live search, filter by matter, status badges for shared signed and pending files, view and download actions plus an upload dropzone.
MCP
Code
:root {
--navy: #1b2a4a;
--navy-d: #101b33;
--gold: #b08d57;
--gold-soft: #efe3cf;
--ink: #1a1f2b;
--muted: #5b6577;
--bg: #f5f3ee;
--white: #ffffff;
--line: #e3ddd0;
--green: #2f7a55;
--green-bg: #e3f0e8;
--amber: #9a6a16;
--amber-bg: #f6ecd6;
--blue: #2b4d7a;
--blue-bg: #e2eaf5;
--serif: "Georgia", "Times New Roman", serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--shadow: 0 1px 2px rgba(16, 27, 51, .06), 0 8px 24px rgba(16, 27, 51, .06);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--ink);
line-height: 1.5;
padding: clamp(16px, 4vw, 48px);
-webkit-font-smoothing: antialiased;
}
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.vault {
max-width: 920px;
margin: 0 auto;
background: var(--white);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: var(--shadow);
padding: clamp(20px, 4vw, 40px);
}
/* masthead */
.masthead {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 16px; }
.crest {
width: 52px; height: 52px;
display: grid; place-items: center;
background: var(--navy);
color: var(--gold-soft);
font-family: var(--serif);
font-size: 22px; font-weight: 700;
border-radius: 8px;
position: relative;
letter-spacing: -1px;
}
.crest span { color: var(--gold); }
.firm {
font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
color: var(--gold); font-weight: 600;
}
h1 {
font-family: var(--serif);
font-size: clamp(24px, 4vw, 32px);
font-weight: 700;
color: var(--navy);
letter-spacing: -.5px;
}
.client { text-align: right; display: grid; gap: 2px; }
.client-label {
font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
}
.client-name { font-family: var(--serif); font-size: 16px; color: var(--ink); font-weight: 700; }
.client-id { font-size: 12px; color: var(--muted); }
.gold-rule {
height: 2px;
background: linear-gradient(90deg, var(--gold), var(--gold-soft) 60%, transparent);
margin: 22px 0 26px;
}
/* stats */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 26px;
}
.stat {
background: var(--bg);
border: 1px solid var(--line);
border-radius: 10px;
padding: 14px 16px;
}
.stat-num {
display: block;
font-family: var(--serif);
font-size: 26px; font-weight: 700;
color: var(--navy);
line-height: 1;
}
.stat-cap {
font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
}
/* controls */
.controls { display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.field { position: relative; }
.field.search { flex: 1 1 240px; }
.field .ico {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
width: 18px; height: 18px; fill: none; stroke: var(--muted); stroke-width: 2;
stroke-linecap: round; pointer-events: none;
}
input[type="search"], select {
width: 100%;
font-family: inherit; font-size: 14px; color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
border-radius: 9px;
padding: 11px 14px;
}
input[type="search"] { padding-left: 38px; }
select { min-width: 180px; cursor: pointer; }
input:focus-visible, select:focus-visible,
.sort:focus-visible, .act:focus-visible, .browse:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
/* table */
.table-wrap {
border: 1px solid var(--line);
border-radius: 12px;
overflow: auto;
}
.table-wrap:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
table.docs { width: 100%; border-collapse: collapse; font-size: 14px; }
thead th {
text-align: left;
background: var(--bg);
color: var(--muted);
font-size: 11px; letter-spacing: .08em; text-transform: uppercase; font-weight: 600;
padding: 12px 16px;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.sort {
background: none; border: 0; cursor: pointer;
font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
display: inline-flex; align-items: center; gap: 5px; padding: 0;
}
.sort:hover { color: var(--navy); }
.arr { font-size: 9px; color: var(--gold); width: 8px; }
tbody tr { border-bottom: 1px solid var(--line); transition: background .15s; }
tbody tr:last-child { border-bottom: 0; }
tbody tr:hover { background: #fbfaf6; }
tbody td { padding: 13px 16px; vertical-align: middle; }
.doc-cell { display: flex; align-items: center; gap: 11px; }
.ftype {
width: 32px; height: 32px; flex: none;
display: grid; place-items: center;
border-radius: 7px;
font-size: 10px; font-weight: 700; letter-spacing: .03em;
color: var(--white);
}
.ftype.pdf { background: #9a3434; }
.ftype.doc { background: #2b4d7a; }
.ftype.xls { background: #2f7a55; }
.ftype.sig { background: var(--gold); }
.doc-name { font-weight: 600; color: var(--ink); }
.doc-sub { font-size: 12px; color: var(--muted); }
.col-type, .col-matter { color: var(--muted); }
.col-act { width: 1%; }
.badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 600;
padding: 4px 10px; border-radius: 999px;
white-space: nowrap;
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge.signed { color: var(--green); background: var(--green-bg); }
.badge.pending { color: var(--amber); background: var(--amber-bg); }
.badge.shared { color: var(--blue); background: var(--blue-bg); }
.actions { display: flex; gap: 6px; justify-content: flex-end; }
.act {
display: inline-grid; place-items: center;
width: 32px; height: 32px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 8px;
cursor: pointer;
color: var(--muted);
transition: all .15s;
}
.act svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.act:hover { color: var(--navy); border-color: var(--gold); background: var(--gold-soft); }
.act:active { transform: scale(.94); }
.empty { padding: 40px 16px; text-align: center; color: var(--muted); font-style: italic; }
/* dropzone */
.dropzone {
margin-top: 22px;
border: 2px dashed var(--line);
border-radius: 12px;
padding: 30px 20px;
text-align: center;
background: var(--bg);
transition: all .18s;
}
.dropzone.drag { border-color: var(--gold); background: var(--gold-soft); }
.up {
width: 30px; height: 30px; margin: 0 auto 8px;
display: block; fill: none; stroke: var(--gold); stroke-width: 2;
stroke-linecap: round; stroke-linejoin: round;
}
.drop-lead { font-family: var(--serif); font-size: 16px; color: var(--navy); font-weight: 700; }
.drop-sub { font-size: 13px; color: var(--muted); margin-top: 4px; }
.browse { color: var(--gold); font-weight: 600; cursor: pointer; text-decoration: underline; }
.foot { margin-top: 18px; font-size: 12px; color: var(--muted); text-align: center; }
/* responsive */
@media (max-width: 520px) {
body { padding: 12px; }
.vault { padding: 18px; }
.masthead { flex-direction: column; gap: 12px; }
.client { text-align: left; }
.stats { grid-template-columns: repeat(2, 1fr); }
.controls { flex-direction: column; }
select { min-width: 0; }
.col-type, .col-matter, thead .col-type, thead .col-matter { display: none; }
thead { display: none; }
table.docs, tbody, tbody tr, tbody td { display: block; width: 100%; }
tbody tr { padding: 12px 0; }
tbody td { padding: 4px 14px; }
.actions { justify-content: flex-start; padding-top: 8px; }
}(function () {
"use strict";
var docs = [
{ name: "Stock Purchase Agreement", sub: "v4 — executed", type: "Agreement", ext: "pdf", matter: "Series B Financing", date: "2026-06-18", status: "Signed" },
{ name: "Investor Rights Agreement", sub: "Counsel review", type: "Agreement", ext: "doc", matter: "Series B Financing", date: "2026-06-15", status: "Pending" },
{ name: "Cap Table — Post Series B", sub: "Schedule A", type: "Schedule", ext: "xls", matter: "Series B Financing", date: "2026-06-12", status: "Shared" },
{ name: "Patent Assignment — Filing 8842", sub: "USPTO confirmation", type: "Filing", ext: "pdf", matter: "IP Portfolio", date: "2026-06-09", status: "Signed" },
{ name: "Trademark Application — MERIDIAN", sub: "Class 9, 42", type: "Filing", ext: "doc", matter: "IP Portfolio", date: "2026-06-04", status: "Pending" },
{ name: "Executive Offer Letter — D. Okafor", sub: "VP Engineering", type: "Letter", ext: "sig", matter: "Employment", date: "2026-05-29", status: "Signed" },
{ name: "Employee Handbook 2026", sub: "Final draft", type: "Policy", ext: "pdf", matter: "Employment", date: "2026-05-22", status: "Shared" },
{ name: "Master Services Agreement — Lumio", sub: "Redline 3", type: "Agreement", ext: "doc", matter: "Vendor Contracts", date: "2026-05-19", status: "Pending" },
{ name: "NDA — Greystone Partners", sub: "Mutual", type: "Agreement", ext: "sig", matter: "Vendor Contracts", date: "2026-05-11", status: "Signed" },
{ name: "Board Consent — May 2026", sub: "Unanimous written", type: "Resolution", ext: "pdf", matter: "Corporate Governance", date: "2026-05-06", status: "Signed" },
{ name: "Bylaws — Amended & Restated", sub: "Exhibit C", type: "Policy", ext: "doc", matter: "Corporate Governance", date: "2026-04-28", status: "Shared" }
];
var rows = document.getElementById("rows");
var empty = document.getElementById("empty");
var search = document.getElementById("search");
var matter = document.getElementById("matter");
var sortBtns = document.querySelectorAll(".sort");
var drop = document.getElementById("drop");
var fileInput = document.getElementById("file");
var sortKey = "date";
var sortDir = -1; // newest first
var statusOrder = { Pending: 0, Shared: 1, Signed: 2 };
function fmtDate(iso) {
var d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
var viewIco = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';
var dlIco = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4"/><path d="M5 21h14"/></svg>';
function render() {
var q = search.value.trim().toLowerCase();
var m = matter.value;
var list = docs.filter(function (d) {
var matchQ = !q || (d.name + " " + d.matter + " " + d.type).toLowerCase().indexOf(q) !== -1;
var matchM = !m || d.matter === m;
return matchQ && matchM;
});
list.sort(function (a, b) {
var av, bv;
if (sortKey === "name") { av = a.name.toLowerCase(); bv = b.name.toLowerCase(); }
else if (sortKey === "status") { av = statusOrder[a.status]; bv = statusOrder[b.status]; }
else { av = a.date; bv = b.date; }
if (av < bv) return -1 * sortDir;
if (av > bv) return 1 * sortDir;
return 0;
});
rows.innerHTML = "";
list.forEach(function (d) {
var tr = document.createElement("tr");
var st = d.status.toLowerCase();
tr.innerHTML =
'<td><div class="doc-cell">' +
'<span class="ftype ' + d.ext + '">' + d.ext.toUpperCase() + '</span>' +
'<span><span class="doc-name">' + d.name + '</span><br>' +
'<span class="doc-sub">' + d.sub + '</span></span>' +
'</div></td>' +
'<td class="col-type">' + d.type + '</td>' +
'<td class="col-matter">' + d.matter + '</td>' +
'<td>' + fmtDate(d.date) + '</td>' +
'<td><span class="badge ' + st + '">' + d.status + '</span></td>' +
'<td class="col-act"><div class="actions">' +
'<button class="act" type="button" aria-label="View ' + d.name + '">' + viewIco + '</button>' +
'<button class="act" type="button" aria-label="Download ' + d.name + '">' + dlIco + '</button>' +
'</div></td>';
rows.appendChild(tr);
});
empty.hidden = list.length !== 0;
updateStats();
}
function updateStats() {
document.getElementById("stat-total").textContent = docs.length;
document.getElementById("stat-signed").textContent = docs.filter(function (d) { return d.status === "Signed"; }).length;
document.getElementById("stat-pending").textContent = docs.filter(function (d) { return d.status === "Pending"; }).length;
}
function setArrows() {
sortBtns.forEach(function (b) {
var arr = b.querySelector(".arr");
if (b.dataset.key === sortKey) arr.textContent = sortDir === 1 ? "▲" : "▼";
else arr.textContent = "";
});
}
// events
search.addEventListener("input", render);
matter.addEventListener("change", render);
sortBtns.forEach(function (b) {
b.addEventListener("click", function () {
var key = b.dataset.key;
if (sortKey === key) sortDir *= -1;
else { sortKey = key; sortDir = key === "date" ? -1 : 1; }
setArrows();
render();
});
});
// view / download feedback (event delegation)
rows.addEventListener("click", function (e) {
var btn = e.target.closest(".act");
if (!btn) return;
var orig = btn.getAttribute("aria-label");
btn.style.borderColor = "var(--gold)";
btn.style.background = "var(--gold-soft)";
setTimeout(function () { btn.style.borderColor = ""; btn.style.background = ""; }, 320);
void orig;
});
// upload dropzone (UI only)
function addUpload(name) {
var ext = /\.pdf$/i.test(name) ? "pdf" : /\.xls/i.test(name) ? "xls" : "doc";
docs.unshift({
name: name.replace(/\.[^.]+$/, ""),
sub: "Uploaded just now",
type: "Upload",
ext: ext,
matter: matter.value || "Series B Financing",
date: new Date().toISOString().slice(0, 10),
status: "Pending"
});
render();
}
["dragenter", "dragover"].forEach(function (ev) {
drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add("drag"); });
});
["dragleave", "drop"].forEach(function (ev) {
drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove("drag"); });
});
drop.addEventListener("drop", function (e) {
var files = e.dataTransfer && e.dataTransfer.files;
if (files && files.length) {
Array.prototype.forEach.call(files, function (f) { addUpload(f.name); });
} else {
addUpload("New Document.pdf");
}
});
fileInput.addEventListener("change", function () {
Array.prototype.forEach.call(fileInput.files, function (f) { addUpload(f.name); });
fileInput.value = "";
});
setArrows();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hartwell & Vance LLP — Document Vault</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="vault" aria-labelledby="vault-title">
<header class="masthead">
<div class="brand">
<span class="crest" aria-hidden="true">H<span>V</span></span>
<div>
<p class="firm">Hartwell & Vance LLP</p>
<h1 id="vault-title">Document Vault</h1>
</div>
</div>
<div class="client">
<span class="client-label">Client</span>
<span class="client-name">Meridian Robotics, Inc.</span>
<span class="client-id">Acct #MR-2041</span>
</div>
</header>
<div class="gold-rule" aria-hidden="true"></div>
<section class="stats" aria-label="Vault summary">
<div class="stat">
<span class="stat-num" id="stat-total">0</span>
<span class="stat-cap">Documents</span>
</div>
<div class="stat">
<span class="stat-num" id="stat-signed">0</span>
<span class="stat-cap">Signed</span>
</div>
<div class="stat">
<span class="stat-num" id="stat-pending">0</span>
<span class="stat-cap">Pending</span>
</div>
<div class="stat">
<span class="stat-num">5</span>
<span class="stat-cap">Matters</span>
</div>
</section>
<section class="controls" aria-label="Filter documents">
<div class="field search">
<label for="search" class="sr-only">Search documents</label>
<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="search" id="search" placeholder="Search by file or matter…" autocomplete="off" />
</div>
<div class="field">
<label for="matter" class="sr-only">Filter by matter</label>
<select id="matter">
<option value="">All matters</option>
<option value="Series B Financing">Series B Financing</option>
<option value="IP Portfolio">IP Portfolio</option>
<option value="Employment">Employment</option>
<option value="Vendor Contracts">Vendor Contracts</option>
<option value="Corporate Governance">Corporate Governance</option>
</select>
</div>
</section>
<div class="table-wrap" role="region" aria-label="Documents" tabindex="0">
<table class="docs">
<thead>
<tr>
<th><button class="sort" data-key="name" aria-label="Sort by name">Document <span class="arr"></span></button></th>
<th class="col-type">Type</th>
<th class="col-matter">Matter</th>
<th><button class="sort" data-key="date" aria-label="Sort by date">Date <span class="arr"></span></button></th>
<th><button class="sort" data-key="status" aria-label="Sort by status">Status <span class="arr"></span></button></th>
<th class="col-act"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="rows"><!-- rendered by script.js --></tbody>
</table>
<p class="empty" id="empty" hidden>No documents match your filters.</p>
</div>
<section class="dropzone" id="drop" aria-label="Upload documents">
<input type="file" id="file" class="sr-only" multiple />
<svg class="up" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 16V4m0 0L7 9m5-5 5 5"/><path d="M4 17v2a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-2"/></svg>
<p class="drop-lead">Drag & drop files to upload</p>
<p class="drop-sub">or <label for="file" class="browse">browse your device</label> — PDF, DOCX up to 25 MB</p>
</section>
<p class="foot">All files are encrypted at rest. Illustrative UI only — not legal advice.</p>
</main>
<script src="script.js"></script>
</body>
</html>Document Vault
An authoritative, trust-first client portal for a law firm. The page leads with a serif masthead, a thin gold rule and a stat strip, then presents every matter document in a clean table — file name with a type icon, document type, the matter it belongs to, the date, and a status badge for Shared, Signed or Pending files.
A live search box filters by file or matter as you type, while a matter dropdown narrows the table to a single engagement. Each row exposes View and Download actions, and column headers sort the table by name, date or status. An upload dropzone at the foot accepts drag-and-drop (UI only), appending new rows as pending uploads with realistic timestamps.
Everything is vanilla JS with no libraries — sorting, filtering, the empty state and the dropzone are all hand-rolled, with focus styles, labelled controls and aria attributes throughout.
Illustrative UI only — not legal advice and not for real client matter storage.