| @@ -30,3 +30,5 @@ data/cache/ | ||
| .exectest.sh | ||
| .vscode/ | ||
| .idea/ | ||
| + | ||
| + | _ops/ |
| @@ -1,45 +0,0 @@ | ||
| - | <!doctype html><html><head><meta charset="utf-8"> | |
| - | <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| - | <link href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;600;800&family=Atkinson+Hyperlegible:wght@400;700&display=swap" rel="stylesheet"> | |
| - | <style> | |
| - | * { box-sizing: border-box; } | |
| - | body { margin:0; background:#ECEAE5; font-family:'Atkinson Hyperlegible',system-ui,sans-serif; color:#1A1A1A; padding:28px; } | |
| - | h1.board { font-family:'Public Sans'; font-weight:800; font-size:26px; margin:0 0 6px; } | |
| - | p.boardsub { font-size:17px; color:#44423D; margin:0 0 24px; } | |
| - | .grid { display:grid; grid-template-columns:repeat(2,1fr); gap:22px; } | |
| - | .card { background:#FFFFFF; border:2px solid #CFCBC2; border-radius:10px; padding:22px; } | |
| - | .name { font-family:'Public Sans'; font-weight:800; font-size:15px; letter-spacing:.04em; text-transform:uppercase; } | |
| - | .hex { font-size:13px; color:#6b6862; margin:2px 0 16px; } | |
| - | .eyebrow { font-family:'Public Sans'; font-weight:600; font-size:12px; letter-spacing:.12em; text-transform:uppercase; } | |
| - | .h { font-family:'Public Sans'; font-weight:800; font-size:28px; line-height:1.05; margin:6px 0 10px; } | |
| - | .lead { font-size:18px; line-height:1.5; margin:0 0 18px; } | |
| - | .btn { display:inline-block; font-family:'Public Sans'; font-weight:700; font-size:18px; padding:14px 22px; border-radius:8px; color:#1A1A1A; } | |
| - | .link { font-size:18px; font-weight:700; text-decoration:underline; text-underline-offset:3px; display:inline-block; margin-top:16px; } | |
| - | .pill { display:inline-block; font-family:'Public Sans'; font-weight:700; font-size:14px; padding:8px 14px; border-radius:999px; margin-top:16px; } | |
| - | .swatch { width:34px; height:34px; border-radius:7px; display:inline-block; vertical-align:middle; margin-right:8px; border:1px solid rgba(0,0,0,.15);} | |
| - | </style></head> | |
| - | <body> | |
| - | <h1 class="board">Docket - accent options (large-print look, white background)</h1> | |
| - | <p class="boardsub">Same neutral layout, five accents. Each shows a heading, a primary button, a link, and an "active page" pill.</p> | |
| - | <div class="grid" id="grid"></div> | |
| - | <script> | |
| - | const opts = [ | |
| - | {name:'1 · Brass / Amber', t:'#8A6A12', s:'#D8A93A', w:'#FBF3DF', hex:'text #8A6A12 · fill #D8A93A'}, | |
| - | {name:'2 · Deep Teal', t:'#0E6E63', s:'#14857A', w:'#E1F2EF', hex:'text #0E6E63 · fill #14857A'}, | |
| - | {name:'3 · Forest Green', t:'#2E6B3E', s:'#3A8A4E', w:'#E6F2E8', hex:'text #2E6B3E · fill #3A8A4E'}, | |
| - | {name:'4 · Bronze / Copper',t:'#94531F', s:'#C0712E', w:'#F8ECE1', hex:'text #94531F · fill #C0712E'}, | |
| - | {name:'5 · Ink only (minimal)',t:'#1A1A1A', s:'#1A1A1A', w:'#EEEDE9', hex:'near-black, accent only on the button'}, | |
| - | ]; | |
| - | document.getElementById('grid').innerHTML = opts.map(o=>` | |
| - | <div class="card"> | |
| - | <div class="name"><span class="swatch" style="background:${o.s}"></span>${o.name}</div> | |
| - | <div class="hex">${o.hex}</div> | |
| - | <div class="eyebrow" style="color:${o.t}">ROLL-CALL REGISTER</div> | |
| - | <div class="h">Votes, on the record</div> | |
| - | <div class="lead">Each row is a recorded vote in the House or Senate - what it was about, the tally, and a link.</div> | |
| - | <a class="btn" style="background:${o.s}">Search the record →</a> | |
| - | <br><a class="link" style="color:${o.t}">H.R. 22 - view this bill</a> | |
| - | <br><span class="pill" style="background:${o.w};color:${o.t}">Bills (active)</span> | |
| - | </div>`).join(''); | |
| - | </script> | |
| - | </body></html> |
| @@ -1,22 +0,0 @@ | ||
| - | import sqlite3, os | |
| - | DB = os.environ.get("DOCKET_DB_PATH", "/opt/docket/data/docket.db") | |
| - | c = sqlite3.connect(DB, timeout=30) | |
| - | print("== DB ==") | |
| - | pc = c.execute("pragma page_count").fetchone()[0] | |
| - | ps = c.execute("pragma page_size").fetchone()[0] | |
| - | print("size_mb:", round(pc * ps / 1e6, 1), "journal_mode:", c.execute("pragma journal_mode").fetchone()[0]) | |
| - | print("wal_autocheckpoint:", c.execute("pragma wal_autocheckpoint").fetchone()[0]) | |
| - | print("== #11 districts ==") | |
| - | print("district top:", c.execute("select district,count(*) cc from members group by district order by cc desc limit 8").fetchall()) | |
| - | cur = c.execute("select name,state,district,chamber from members where chamber='house' limit 5").fetchall() | |
| - | print("house sample:", cur) | |
| - | print("== #2 bill text/summary availability ==") | |
| - | cols = [r[1] for r in c.execute("pragma table_info(bills)")] | |
| - | print("bills has summary col:", "summary" in cols, "| cols:", cols) | |
| - | print("bill_versions count:", c.execute("select count(*) from bill_versions").fetchone()[0]) | |
| - | print("== #5 vote source urls (sample) ==") | |
| - | for (u,) in c.execute("select source_url from votes where source_url is not null order by id desc limit 4"): | |
| - | print("VOTEURL:", u) | |
| - | print("== row counts ==") | |
| - | for t in ("votes","vote_positions","bills","members","awards"): | |
| - | print(t, c.execute("select count(*) from " + t).fetchone()[0]) |
| @@ -1,10 +0,0 @@ | ||
| - | import sqlite3, os | |
| - | DB = os.environ.get("DOCKET_DB_PATH", "/opt/docket/data/docket.db") | |
| - | c = sqlite3.connect(DB, timeout=60) | |
| - | print("checkpoint:", c.execute("pragma wal_checkpoint(TRUNCATE)").fetchone()) | |
| - | c.execute("pragma optimize") | |
| - | c.execute("analyze") | |
| - | print("optimized + analyzed") | |
| - | pc = c.execute("pragma page_count").fetchone()[0] | |
| - | ps = c.execute("pragma page_size").fetchone()[0] | |
| - | print("db size mb:", round(pc * ps / 1e6, 1)) |
| @@ -1,75 +0,0 @@ | ||
| - | // Docket mobile QA crawl - drives chromium at iPhone viewport across every | |
| - | // route, screenshots, and auto-detects horizontal overflow + console errors. | |
| - | const { chromium } = require("playwright"); | |
| - | const fs = require("fs"); | |
| - | ||
| - | const BASE = process.env.QA_BASE || "http://localhost:3000"; | |
| - | const OUT = "/opt/docket/_ops/qa"; | |
| - | fs.mkdirSync(OUT, { recursive: true }); | |
| - | ||
| - | const routes = [ | |
| - | "/", "/register", "/votes", "/members", "/bills", "/laws", "/spending", | |
| - | "/executive", "/states", "/schedule", "/calendar", "/search", | |
| - | "/how-we-pick", "/who-pays", | |
| - | "/votes/6017", "/votes/6002", "/members/A000055", "/bill/2136", | |
| - | "/bill/1", "/states/CA", | |
| - | ]; | |
| - | ||
| - | (async () => { | |
| - | const browser = await chromium.launch(); | |
| - | const ctx = await browser.newContext({ | |
| - | viewport: { width: 390, height: 844 }, | |
| - | deviceScaleFactor: 2, | |
| - | isMobile: true, | |
| - | hasTouch: true, | |
| - | userAgent: | |
| - | "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", | |
| - | }); | |
| - | const report = []; | |
| - | for (const r of routes) { | |
| - | const page = await ctx.newPage(); | |
| - | const errors = []; | |
| - | page.on("console", (m) => { if (m.type() === "error") errors.push(m.text().slice(0, 160)); }); | |
| - | page.on("pageerror", (e) => errors.push("PAGEERR " + e.message.slice(0, 160))); | |
| - | let status = 0; | |
| - | try { | |
| - | const resp = await page.goto(BASE + r, { waitUntil: "networkidle", timeout: 35000 }); | |
| - | status = resp ? resp.status() : 0; | |
| - | } catch (e) { errors.push("GOTO " + e.message.slice(0, 120)); } | |
| - | await page.waitForTimeout(700); | |
| - | const m = await page.evaluate(() => { | |
| - | const vw = window.innerWidth; | |
| - | const sw = document.documentElement.scrollWidth; | |
| - | const over = []; | |
| - | document.querySelectorAll("body *").forEach((el) => { | |
| - | const rb = el.getBoundingClientRect(); | |
| - | if (rb.right > vw + 2 && rb.width > 40 && el.children.length < 8) { | |
| - | const cls = (typeof el.className === "string" ? el.className : "").slice(0, 34); | |
| - | over.push(el.tagName + (cls ? "." + cls : "") + " →" + Math.round(rb.right)); | |
| - | } | |
| - | }); | |
| - | // tap-target check: interactive els < 40px tall | |
| - | const small = []; | |
| - | document.querySelectorAll("a,button,[role=button],summary").forEach((el) => { | |
| - | const rb = el.getBoundingClientRect(); | |
| - | if (rb.height > 0 && rb.height < 40 && rb.width > 0) small.push((el.textContent || "").trim().slice(0, 18)); | |
| - | }); | |
| - | return { vw, sw, overflow: sw > vw + 2, offenders: [...new Set(over)].slice(0, 8), smallTargets: [...new Set(small)].slice(0, 8) }; | |
| - | }); | |
| - | const name = (r === "/" ? "home" : r.replace(/^\//, "").replace(/\//g, "_")); | |
| - | await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: false }); | |
| - | report.push({ route: r, status, ...m, errors: errors.slice(0, 5) }); | |
| - | await page.close(); | |
| - | } | |
| - | await browser.close(); | |
| - | fs.writeFileSync(`${OUT}/report.json`, JSON.stringify(report, null, 2)); | |
| - | console.log("==== DOCKET MOBILE QA (390px) ===="); | |
| - | for (const x of report) { | |
| - | console.log( | |
| - | `[${x.status}] ovf=${x.overflow ? "YES(" + x.sw + ")" : "no"} ${x.route}` + | |
| - | (x.offenders.length ? `\n OVERFLOW: ${x.offenders.join(" | ")}` : "") + | |
| - | (x.smallTargets.length ? `\n SMALL-TAP: ${x.smallTargets.join(", ")}` : "") + | |
| - | (x.errors.length ? `\n ERRORS: ${x.errors.join(" ;; ")}` : "") | |
| - | ); | |
| - | } | |
| - | })(); |
| @@ -1,34 +0,0 @@ | ||
| - | #!/bin/bash | |
| - | # Build + verify helper for Docket frontend work. | |
| - | # Syncs the share source -> /opt build dir, rebuilds, restarts the prod server, | |
| - | # runs the mobile QA crawl, and prints the overflow/hydration summary. | |
| - | # Run ON CT247: bash /shared/projects/docket/_ops/rebuild-qa.sh | |
| - | set -e | |
| - | D=/shared/projects/docket | |
| - | O=/opt/docket | |
| - | # sync frontend source share -> /opt (data, node_modules, .next stay local) | |
| - | tar -C "$D" --exclude=node_modules --exclude=.git --exclude=.next --exclude=data --exclude=_ops/qa --exclude=_ops/logs -cf - \ | |
| - | app components lib public next.config.js tailwind.config.ts tsconfig.json postcss.config.js middleware.ts 2>/dev/null \ | |
| - | | tar -C "$O" -xf - | |
| - | cd "$O" | |
| - | echo "== building ==" | |
| - | npm run build 2>&1 | tail -6 | |
| - | echo "== restart ==" | |
| - | for pid in $(ss -ltnp 2>/dev/null | grep :3000 | grep -oE 'pid=[0-9]+' | cut -d= -f2); do kill -9 "$pid"; done | |
| - | sleep 2 | |
| - | DOCKET_DB_PATH="$O/data/docket.db" nohup ./node_modules/.bin/next start -H 0.0.0.0 -p 3000 > "$O/_ops/logs/server.log" 2>&1 < /dev/null & | |
| - | sleep 8 | |
| - | echo "== crawl ==" | |
| - | node "$O/_ops/qa-crawl.js" > "$O/_ops/qa/crawl.log" 2>&1 || true | |
| - | python3 - <<'PY' | |
| - | import json | |
| - | d = json.load(open("/opt/docket/_ops/qa/report.json")) | |
| - | bad = [x for x in d if x.get("overflow") or any("418" in e for e in x.get("errors", []))] | |
| - | if not bad: | |
| - | print("QA CLEAN - no overflow, no hydration errors across", len(d), "routes") | |
| - | else: | |
| - | for x in bad: | |
| - | print("ISSUE", x["route"], "overflow" if x.get("overflow") else "", | |
| - | "#418" if any("418" in e for e in x.get("errors", [])) else "", | |
| - | "| offenders:", " ".join(x.get("offenders", [])[:4])) | |
| - | PY |
| @@ -1,73 +0,0 @@ | ||
| - | // Docket v2 design screenshots - takes home + bills + bill-detail | |
| - | // at 390px (mobile) and 1024px (desktop), saves to qa-view/ as v2-*.png | |
| - | const { chromium } = require("/opt/docket/node_modules/playwright"); | |
| - | const fs = require("fs"); | |
| - | const path = require("path"); | |
| - | ||
| - | const BASE = process.env.QA_BASE || "http://localhost:3000"; | |
| - | const OUT = "/shared/projects/docket/_ops/qa-view"; | |
| - | fs.mkdirSync(OUT, { recursive: true }); | |
| - | ||
| - | // Find a real bill ID to screenshot - look for any real URL from the /bills page | |
| - | async function findBillId(page) { | |
| - | await page.goto(BASE + "/bills", { waitUntil: "networkidle", timeout: 35000 }); | |
| - | // Try to find the first bill card link | |
| - | const href = await page.$eval( | |
| - | 'a[href^="/bill/"]', | |
| - | (el) => el.getAttribute("href") | |
| - | ).catch(() => null); | |
| - | return href || "/bill/2136"; // fallback to known test ID | |
| - | } | |
| - | ||
| - | (async () => { | |
| - | const browser = await chromium.launch(); | |
| - | const viewports = [ | |
| - | { name: "mobile", width: 390, height: 844, mobile: true, dpr: 2 }, | |
| - | { name: "desktop", width: 1024, height: 768, mobile: false, dpr: 1 }, | |
| - | ]; | |
| - | ||
| - | let billPath = "/bill/2136"; | |
| - | ||
| - | for (const vp of viewports) { | |
| - | const ctx = await browser.newContext({ | |
| - | viewport: { width: vp.width, height: vp.height }, | |
| - | deviceScaleFactor: vp.dpr, | |
| - | isMobile: vp.mobile, | |
| - | hasTouch: vp.mobile, | |
| - | }); | |
| - | const page = await ctx.newPage(); | |
| - | ||
| - | // Find a real bill for the first viewport pass | |
| - | if (vp.name === "mobile") { | |
| - | try { | |
| - | billPath = await findBillId(page); | |
| - | console.log("Bill path:", billPath); | |
| - | } catch (e) { | |
| - | console.warn("Could not auto-detect bill path, using default"); | |
| - | } | |
| - | } | |
| - | ||
| - | const shots = [ | |
| - | { route: "/", name: "v2-home" }, | |
| - | { route: "/bills", name: "v2-bills" }, | |
| - | { route: billPath, name: "v2-bill" }, | |
| - | ]; | |
| - | ||
| - | for (const s of shots) { | |
| - | try { | |
| - | await page.goto(BASE + s.route, { waitUntil: "networkidle", timeout: 35000 }); | |
| - | await page.waitForTimeout(1000); | |
| - | const outPath = path.join(OUT, `${s.name}-${vp.name}.png`); | |
| - | await page.screenshot({ path: outPath, fullPage: false }); | |
| - | console.log(`OK ${outPath}`); | |
| - | } catch (e) { | |
| - | console.error(`FAIL ${s.route}: ${e.message}`); | |
| - | } | |
| - | } | |
| - | ||
| - | await ctx.close(); | |
| - | } | |
| - | ||
| - | await browser.close(); | |
| - | console.log("\nScreenshots saved to", OUT); | |
| - | })(); |
| @@ -1,81 +0,0 @@ | ||
| - | // Docket V3 design screenshots - captures dark-default theme | |
| - | // Home + /bills + bill-detail at 390px (mobile) and 1280px (desktop) | |
| - | // Saves to qa-view/ as v3-*.png | |
| - | const { chromium } = require("/opt/docket/node_modules/playwright"); | |
| - | const fs = require("fs"); | |
| - | const path = require("path"); | |
| - | ||
| - | const BASE = process.env.QA_BASE || "http://localhost:3000"; | |
| - | const OUT = "/shared/projects/docket/_ops/qa-view"; | |
| - | fs.mkdirSync(OUT, { recursive: true }); | |
| - | ||
| - | // Find a real bill ID from the /bills page | |
| - | async function findBillId(page) { | |
| - | await page.goto(BASE + "/bills", { waitUntil: "networkidle", timeout: 35000 }); | |
| - | const href = await page.$eval( | |
| - | 'a[href^="/bill/"]', | |
| - | (el) => el.getAttribute("href") | |
| - | ).catch(() => null); | |
| - | return href || "/bill/2136"; | |
| - | } | |
| - | ||
| - | (async () => { | |
| - | const browser = await chromium.launch(); | |
| - | const viewports = [ | |
| - | { name: "mobile", width: 390, height: 844, mobile: true, dpr: 2 }, | |
| - | { name: "desktop", width: 1280, height: 900, mobile: false, dpr: 1 }, | |
| - | ]; | |
| - | ||
| - | let billPath = "/bill/2136"; | |
| - | ||
| - | for (const vp of viewports) { | |
| - | const ctx = await browser.newContext({ | |
| - | viewport: { width: vp.width, height: vp.height }, | |
| - | deviceScaleFactor: vp.dpr, | |
| - | isMobile: vp.mobile, | |
| - | hasTouch: vp.mobile, | |
| - | // Force dark mode preference so the site stays in dark default | |
| - | colorScheme: "dark", | |
| - | }); | |
| - | const page = await ctx.newPage(); | |
| - | ||
| - | // Clear any persisted display-mode so we get the true default (dark) | |
| - | await page.addInitScript(() => { | |
| - | localStorage.removeItem("docket-display"); | |
| - | }); | |
| - | ||
| - | // Find a real bill on mobile pass | |
| - | if (vp.name === "mobile") { | |
| - | try { | |
| - | billPath = await findBillId(page); | |
| - | console.log("Bill path:", billPath); | |
| - | } catch (e) { | |
| - | console.warn("Could not auto-detect bill path, using default"); | |
| - | } | |
| - | } | |
| - | ||
| - | const shots = [ | |
| - | { route: "/", name: "v3-home" }, | |
| - | { route: "/bills", name: "v3-bills" }, | |
| - | { route: billPath, name: "v3-bill" }, | |
| - | ]; | |
| - | ||
| - | for (const s of shots) { | |
| - | try { | |
| - | await page.goto(BASE + s.route, { waitUntil: "networkidle", timeout: 35000 }); | |
| - | // Wait for fonts + transitions | |
| - | await page.waitForTimeout(1200); | |
| - | const outPath = path.join(OUT, `${s.name}-${vp.name}.png`); | |
| - | await page.screenshot({ path: outPath, fullPage: false }); | |
| - | console.log("OK ", outPath); | |
| - | } catch (e) { | |
| - | console.error("FAIL", s.route + ":", e.message); | |
| - | } | |
| - | } | |
| - | ||
| - | await ctx.close(); | |
| - | } | |
| - | ||
| - | await browser.close(); | |
| - | console.log("\nV3 screenshots saved to", OUT); | |
| - | })(); |
| @@ -1,49 +0,0 @@ | ||
| - | // Docket V3b screenshots - dark + light mode, home page, header switcher visible. | |
| - | // Outputs: v3b-home-dark.png and v3b-home-light.png to qa-view/ | |
| - | const { chromium } = require("/opt/docket/node_modules/playwright"); | |
| - | const fs = require("fs"); | |
| - | const path = require("path"); | |
| - | ||
| - | const BASE = process.env.QA_BASE || "http://localhost:3000"; | |
| - | const OUT = "/shared/projects/docket/_ops/qa-view"; | |
| - | fs.mkdirSync(OUT, { recursive: true }); | |
| - | ||
| - | (async () => { | |
| - | const browser = await chromium.launch(); | |
| - | ||
| - | const modes = [ | |
| - | { key: "dark", storage: "dark", colorScheme: "dark" }, | |
| - | { key: "light", storage: "light", colorScheme: "light" }, | |
| - | ]; | |
| - | ||
| - | for (const m of modes) { | |
| - | const ctx = await browser.newContext({ | |
| - | viewport: { width: 1280, height: 900 }, | |
| - | deviceScaleFactor: 1, | |
| - | isMobile: false, | |
| - | colorScheme: m.colorScheme, | |
| - | }); | |
| - | const page = await ctx.newPage(); | |
| - | ||
| - | // Set the persisted mode so the pre-paint script and React both see the right value | |
| - | await page.addInitScript((mode) => { | |
| - | localStorage.setItem("docket-display", mode); | |
| - | }, m.storage); | |
| - | ||
| - | try { | |
| - | await page.goto(BASE + "/", { waitUntil: "networkidle", timeout: 35000 }); | |
| - | // Wait for fonts, transitions, and the icon switcher to mount | |
| - | await page.waitForTimeout(1500); | |
| - | const outPath = path.join(OUT, `v3b-home-${m.key}.png`); | |
| - | await page.screenshot({ path: outPath, fullPage: false }); | |
| - | console.log("OK ", outPath); | |
| - | } catch (e) { | |
| - | console.error("FAIL", m.key + ":", e.message); | |
| - | } | |
| - | ||
| - | await ctx.close(); | |
| - | } | |
| - | ||
| - | await browser.close(); | |
| - | console.log("\nV3b screenshots saved to", OUT); | |
| - | })(); |
| @@ -1,49 +0,0 @@ | ||
| - | # Docket - Design V3: MATCH ZION'S LIVE SITES (LOCKED 2026-06-27) | |
| - | ||
| - | The light V2 (Atkinson, then Archivo/Fraunces light + serif body) is REJECTED - it reads as a generic AI look. Replicate the theme of Zion's actual live sites. DO NOT invent or use any generated/AI design pack. | |
| - | ||
| - | ## GROUND TRUTH (study these, replicate them) | |
| - | - Reference screenshots (captured from the live sites): `_ops/qa-view/REF-treetrace-desktop.png`, `_ops/qa-view/REF-zionboggan-desktop.png`. | |
| - | - Their live CSS: treetrace.dev `/assets/css/{tokens,shell,glass}.css`; zionboggan.com `/assets/css/{main,flare,sections}.css`. | |
| - | - What both share (the aesthetic to match): | |
| - | - **DARK** near-black backgrounds (ink ~#0B0C0E, raised surface ~#121316/#1A1C1F). | |
| - | - **Archivo 800-900, UPPERCASE, big, tight tracking** for display headlines (e.g. "RECORD THE PATH", "ZION BOGGAN"). | |
| - | - **JetBrains Mono, uppercase, tracked** for kicker labels, nav, tags, data, terminal cards. | |
| - | - A **serif** (Fraunces) for paragraph/body prose (warm, readable) - verify against their CSS and match their body role. | |
| - | - **ONE bold accent per view** (treetrace cobalt #2E5BFF, zionboggan red ~#A40000) - used sparingly (buttons, kickers, highlights), bordered/outline buttons. | |
| - | - Composition: small mono kicker → HUGE Archivo headline → serif paragraph → outline buttons + tag row. Generous whitespace; it BREATHES (do not crowd). | |
| - | ||
| - | ## Docket V3 tokens (his treetrace dark treatment + NEUTRAL accent) | |
| - | ``` | |
| - | --bg: #0B0C0E /* ink */ | |
| - | --surface: #121316 /* cards/chrome */ | |
| - | --raised: #1A1C20 /* hover/raised */ | |
| - | --text: #F2EFE9 /* warm paper */ | |
| - | --text-soft: #9BA0A6 /* ash secondary */ | |
| - | --border: rgba(155,160,166,.18) | |
| - | --accent: #E0A53B /* BRASS - neutral (cobalt/red are partisan-coded for a gov site); rationed, one signal per view */ | |
| - | --accent-dim:#B07F25 | |
| - | --accent-wash:rgba(224,165,59,.10) | |
| - | --yea: #7FA88C /* muted sage, labeled */ --nay: #B98A8A /* muted clay, labeled */ | |
| - | ``` | |
| - | Keep Light + Plain as optional toggles; **DEFAULT = this dark theme** (his look). | |
| - | ||
| - | ## Fonts (his trio - match his ROLES exactly) | |
| - | - Archivo (display, 800-900, uppercase headlines/labels). JetBrains Mono (kickers, nav, tags, data tokens - readable size >=14px, NOT tiny). Fraunces (serif paragraph/body prose, comfortable >=17px). Drop Atkinson/Public-Sans entirely. | |
| - | ||
| - | ## FIX what was wrong (within the dark aesthetic) | |
| - | 1. **Crowding** → generous whitespace + clear vertical rhythm; fewer elements per screen; let headlines breathe like his sites do. | |
| - | 2. **Legibility/size** → bigger than his dense portfolio: body >=17px, kicker/labels >=14px (not 11-12px), big tap targets >=44px. | |
| - | 3. **Navigation** → simple 6-item nav (Home · Bills · Votes · Representatives · Spending · States) + prominent Search; clear active state; clean mobile menu. Breadcrumbs + big Back. | |
| - | 4. **Responsive** → big simple cards (NOT dense tables); zero overflow at 390px (keep the responsive-card work). | |
| - | 5. **Plain language** → plain dates/words, expand acronyms. | |
| - | ||
| - | ## Keep (don't lose) | |
| - | All functional work: perf/ISR (no force-dynamic), search+sort+filters, Cite this, vote→bill display, state data, photos, neutrality gate (no adjectives, source on every fact, no comments, muted/never-saturated party colors). The federal-vs-state bill fix. | |
| - | ||
| - | ## Build order | |
| - | 1. Read his live CSS + the REF screenshots. Set the dark theme tokens + font roles to match. | |
| - | 2. Rebuild HOME + /bills + /bill/[id] as the TEMPLATE in his theme (dark, Archivo-uppercase, mono labels, brass, breathing whitespace, big text, simple nav). | |
| - | 3. STOP - screenshot (dark default) → overseer shows Zion for approval. | |
| - | 4. After approval: propagate to all pages + the federal/state bill distinction. | |
| - | 5. QA crawl CLEAN; purge Cloudflare cache so it goes live. |
| @@ -1,43 +0,0 @@ | ||
| - | # Docket - UX/QoL Redesign Spec (2026-06-27) | |
| - | ||
| - | "Back to the drawing board" for usability. Audience = an **elderly citizen** (low tech-literacy, low vision) AND a **high-school/college student citing Docket as an educational source**. Decisions below are LOCKED by Zion. Keep strict neutrality (no lean, no adjectives) and keep all data/sourcing. | |
| - | ||
| - | ## Decisions (locked) | |
| - | 1. **Default look = legible/light "Reference" theme.** The dark "forensic terminal" becomes an OPTIONAL mode, not the default. | |
| - | 2. **"Cite this" = first-class feature** (APA/MLA/Chicago + permalink + accessed-date) on every record + page. | |
| - | 3. Search/filter on every list page. De-clutter IA. Clear labels. Performance already fixed. | |
| - | ||
| - | ## Theme system (3 modes; default changes) | |
| - | Mode toggle stays, but the DEFAULT becomes **Reference**: | |
| - | - **Reference (NEW DEFAULT - light):** background `#F8FAFC`, surface `#FFFFFF`, text `#0F172A`/`#020617`, muted `#475569`, border `#E2E8F0`. **Accent = brass `#B07F25`** (the brand thread; brass darkened for AA on light - NEVER use blue/red as accent, neutrality). Body font **Atkinson Hyperlegible** (engineered for low vision). Headings **Fraunces** (keep, scholarly/citable). Mono **JetBrains Mono ONLY for true data tokens** (vote tallies, bill numbers, dates) - not for prose. Body ≥18px, line-height 1.6, generous whitespace, single-column, max-w ~720-860px for reading. | |
| - | - **Dark (forensic - now optional):** the existing dark brand (ink/brass/mono). Preserved for power users. | |
| - | - **Plain (high-contrast):** existing max-contrast mode (system font, ≥20px) for max accessibility. | |
| - | All three: WCAG AA min (AAA target), ≥44px tap targets, visible focus rings, underlined links, no hover-only, prefers-reduced-motion respected. | |
| - | ||
| - | ## IA / declutter (each page does ONE job) | |
| - | - **Register (home):** the ranked feed, period. Remove the crowding side cards (composition chart, awards card, topic chips, changes feed) from the main view - move them to a compact, collapsed "More" area or their own pages. Lead with a short plain-language explainer + the feed. One primary action. | |
| - | - **Every list page (Register, Bills, Votes, Members, Spending, Executive, States):** a prominent labeled **search/filter bar at the top** (mobile + desktop) - search box ≥44px tall, plus the relevant dropdown filters. Results as the existing responsive cards. | |
| - | - **Detail pages (bill, vote, member):** clear heading → key facts → source → "Cite this". No crowding. | |
| - | - Consistent header on every page: wordmark, a clear page-nav (hamburger on mobile / simple bar on desktop) with active-page indicator, the mode toggle as a small distinct control, and a global search entry. | |
| - | ||
| - | ## Search/filter (item #1) | |
| - | - **/members:** search by name; filter by state, chamber, party, current-vs-historical. (Already defaults current-first.) | |
| - | - **/bills:** keep existing filters + ensure the search box is prominent. | |
| - | - **/votes:** add search/filter (by chamber, result, date/keyword). | |
| - | - Reuse a shared SearchFilterBar component. Server-side filtering where possible (perf), client-side for already-loaded sets. | |
| - | ||
| - | ## "Cite this" (item #2 - first-class) | |
| - | - A `CiteThis` component on every record (bill, vote, member, award) and every page. | |
| - | - Generates a ready citation in **APA, MLA, and Chicago**, plus a **permalink** and the **accessed date** (today). Example (APA): *Docket. (2026). H.R. 22 - [title] [Data set]. U.S. Congress. Retrieved Month D, YYYY, from https://usdocket.org/bill/...* - but ALWAYS point the underlying source to the official .gov record too (neutrality rule #1). | |
| - | - Copy-to-clipboard buttons. Neutral, factual, no adjectives. | |
| - | - This is the differentiator for the student/educator audience. | |
| - | ||
| - | ## Labels / clarity (the "51" problem) | |
| - | - **States page:** show **"50 states"** as the count. **DC and the 5 territories are their own clearly-labeled rows/section - DC is a federal district, NOT a state; never produce a "51 states" number.** Coverage framed plainly. | |
| - | - Plain dates ("June 26, 2026"), plain status words, expand acronyms on first use, ≤8th-grade reading level for any explanatory copy (docs/ACCESSIBILITY.md). | |
| - | ||
| - | ## Out of scope / keep | |
| - | - Keep all data, sources, neutrality gate, the 3-mode toggle mechanism. Keep the brass brand thread. Don't regress the responsive cards or the perf fixes (PERF_NOTES.md). | |
| - | ||
| - | ## Verify | |
| - | Build + run the mobile QA crawl (`_ops/rebuild-qa.sh`) → must stay QA CLEAN (no overflow/hydration). Then check the new default renders light + legible, search works on list pages, Cite this works, states labeled correctly. Screenshots at 390px + desktop. |