From dcf4c9a8e79fa4e7e5ff136d03175251517cb005 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Apr 2026 05:56:48 -0500 Subject: [PATCH] =?UTF-8?q?demo:=20search.html=20=E2=80=94=20Live=20Market?= =?UTF-8?q?=20explainer=20rewrite=20+=20fp-bar=20viewport-paint=20+=20comp?= =?UTF-8?q?act=20contract=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four UI changes landing together since they all polish Section ① and Section ② of the public demo: 1. Section ① (Live Market — Chicago) explainer rewritten data-source- first ("Live from City of Chicago Open Data...") with bolded dial names so a skimmer can map the visual to the prose. Drops the "internal calendar" jargon and the slightly-overclaiming "rest of the page is reacting" framing — downstream sections read the same feed but don't react to the per-shift filter, so the new copy says "this row is its heartbeat" instead. 2. Fill-probability bar gets a left-to-right paint reveal (clip-path inset animation) so the green→gold→orange→red gradient reads as a *timeline growing* instead of a static heatmap with a "danger zone" at the right. Followed by a 30%-wide shimmer sweep on a 3.4s loop for live-signal feel. 3. Paint trigger moved from on-render to IntersectionObserver — by the time the user scrolls to Section ② the on-render animation had already finished. Now each bar paints in over 2.8s when it enters viewport (threshold 0.2, 350ms entry delay). Single shared observer, unobserve()s after firing so the watch list trends to zero. 4. Contract cards now compact-by-default with click-to-expand. New summary strip shows revenue / margin / fill-by-1wk / top candidate so scanners get the punchline without expanding. Click anywhere on the card surface (excluding inner content) to expand the full FP curve, economics grid, candidates list, and Project Index. Project Index auto-opens with the parent card so users actually find the build signals — but only on user-driven expand (avoiding 20× OSHA scrapes on page load). grid-template-rows: 0fr → 1fr animation handles the smooth height transition. All four animations honor prefers-reduced-motion. Co-Authored-By: Claude Opus 4.7 (1M context) --- mcp-server/search.html | 290 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 264 insertions(+), 26 deletions(-) diff --git a/mcp-server/search.html b/mcp-server/search.html index ff9add7..d1f1eb9 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -202,6 +202,147 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun 50% { opacity:1; transform:scale(1.1) } } +/* ─── Fill-probability bar — viewport-triggered paint + shimmer ───── + The bar paints in left-to-right via a clip-path animation so the + green → gold → orange → red gradient reads as a *timeline growing* + instead of a static heatmap with "danger zone" at the right edge. + + Triggering: the .fp-bar starts in a fully-clipped state and only + paints when JS adds .lit via IntersectionObserver — fires when the + bar enters viewport, so a fast scroller sees every bar animate as + it scrolls past, instead of missing the show on first paint. + + Timing: 350ms entry delay (lets the eye lock onto the bar before + motion starts) + 2800ms paint (slow enough to actually watch the + timeline build). Easing matches the card-in stagger (cubic-bezier + .2 .7 .2 1) so the page reads as one consistent motion language. + + Shimmer: 30%-wide highlight sweeps across every ~3.4s on a 3400ms + delay (350 entry + 2800 paint + 250 dwell) so the two motions + never compete — the bar finishes drawing, *then* it pulses. */ +@keyframes fp-paint { + from { clip-path: inset(0 100% 0 0); } + to { clip-path: inset(0 0 0 0); } +} +@keyframes fp-shimmer { + 0% { transform: translateX(-100%); opacity: 0 } + 10% { opacity: 1 } + 60% { transform: translateX(260%); opacity: 1 } + 61%, 100% { transform: translateX(260%); opacity: 0 } +} +.fp-bar { + position: relative; + /* Hidden until .lit is added — IntersectionObserver fires the paint + animation by toggling the class, which is the only thing that + unmasks the clip-path. */ + clip-path: inset(0 100% 0 0); +} +.fp-bar.lit { + animation: fp-paint 2800ms cubic-bezier(.2,.7,.2,1) 350ms both; +} +.fp-bar::after { + content: ""; + position: absolute; + top: 0; left: 0; + width: 30%; + height: 100%; + background: linear-gradient(90deg, + transparent 0%, + rgba(255,255,255,0.18) 50%, + transparent 100%); + pointer-events: none; +} +.fp-bar.lit::after { + animation: fp-shimmer 3.4s ease-in-out 3400ms infinite; +} +@media (prefers-reduced-motion: reduce) { + /* Skip the clip-path hide too — otherwise reduced-motion users + would see permanently invisible bars. The .lit class still gets + added by JS but the animation is `none`, so bar just appears. */ + .fp-bar { clip-path: none } + .fp-bar.lit { animation: none } + .fp-bar.lit::after { animation: none; opacity: 0 } +} + +/* ─── Staffer's Console: compact contract card with click-to-expand ─ + Each card stays compact by default so a coordinator scanning 20+ + contracts sees ~5 cards per viewport instead of ~1.5. The summary + strip below the pills shows revenue / margin / fill-by-1wk / top + candidate so scanners get the punchline without expanding. The + whole card surface is clickable; clicks inside the expanded details + don't bubble to the toggle (so contractor links / SMS copy still + work). When a card expands, its Project Index auto-opens too — + solves the "users aren't finding the build signals" gap without + firing 20× OSHA scrapes on page load. */ +.contract-card { + cursor: pointer; + transition: transform 180ms ease, box-shadow 180ms ease; +} +.contract-card:hover { + transform: translateY(-1px); + box-shadow: 0 4px 14px rgba(0,0,0,0.32), 0 0 0 1px #30363d; +} +.contract-card.expanded { cursor: default } +.contract-card.expanded:hover { transform: none } +.contract-card .card-strip { + background: #0d1117; + border: 1px solid #171d27; + border-radius: 8px; + padding: 8px 12px; + margin-bottom: 10px; + display: flex; + flex-wrap: wrap; + gap: 14px; + font-size: 11px; + color: #8b949e; + align-items: baseline; +} +.contract-card .card-strip strong { + font-weight: 600; + font-variant-numeric: tabular-nums; + margin-right: 3px; +} +/* Modern smooth-expand: grid-template-rows transitions cleanly to + the actual content height. Safari 17.4+ / Chrome 117+ / FF 117+. */ +.contract-card .card-details { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 380ms cubic-bezier(.2,.7,.2,1); +} +.contract-card.expanded .card-details { grid-template-rows: 1fr } +.contract-card .card-details > .card-details-inner { + overflow: hidden; + min-height: 0; +} +.contract-card.expanded .card-details > .card-details-inner { + padding-top: 4px; +} +.contract-card .card-toggle { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 0 2px; + font-size: 10px; + color: #545d68; + letter-spacing: 1.5px; + text-transform: uppercase; + user-select: none; +} +.contract-card .card-toggle .chevron { + display: inline-block; + transition: transform 320ms cubic-bezier(.2,.7,.2,1); + font-size: 12px; + line-height: 1; +} +.contract-card.expanded .card-toggle .chevron { transform: rotate(180deg) } +@media (prefers-reduced-motion: reduce) { + .contract-card { transition: none } + .contract-card:hover { transform: none } + .contract-card .card-details { transition: none } + .contract-card .card-toggle .chevron { transition: none } +} + /* Bottom section-jump nav — mobile only */ /* Desktop: top nav handles navigation; this dock stays hidden. */ /* Mobile: top nav collapses (.bar nav:display:none below); this fixed dock */ @@ -503,17 +644,20 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
① Live Market — Chicago right now - +

- The console on the left is the live punch clock — current time, the active shift, and - four dials for what that shift looks like right now: open permits, workers needed, - bench available, and coverage. The panel beside it adds combined bill demand for those - permits, deadline pressure on the open queue, the top roles in demand, and a shift - comparison strip. Click any shift in the comparison to filter every - dial and the bill rate to that shift's slice of the internal calendar — past, active, - or upcoming. Click the same shift again to return to the all-shifts total. This is the - real world the rest of the page is reacting to. + Live from the City of Chicago Open Data permit feed (Building Permits + ≥ $250K), cross-referenced against our 500K-worker bench. The console on the left is + the punch clock — current time, today's active shift, and four dials watching + open permits, workers needed, + bench depth, and projected coverage. The panel on + the right reads the same feed financially: combined bill demand if every permit fills, + deadline pressure across overdue / urgent / soon / scheduled, the four roles + being asked for most, and a shift-mix bar. Click any shift bar to + re-slice the dials and the dollar counter to that shift's calendar slice; click again + to clear. The same permit feed drives the staffing forecast, the staffer's console, + and worker matches further down — this row is its heartbeat.

@@ -2355,8 +2499,12 @@ function loadLiveContracts(){ var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{}; var urg=tl.urgency||'scheduled'; var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd'; - var card=document.createElement('div');card.className='insight info'; + var card=document.createElement('div');card.className='insight info contract-card'; card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid '+borderColor; + // Foldable details container — fpRow / ecRow / Project Index / + // description / candidate rows all live here. Hidden until the + // card gets .expanded; CSS animates the grid-template-rows. + var detailsInner=document.createElement('div');detailsInner.className='card-details-inner'; // Header — permit var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px'; var left=document.createElement('div'); @@ -2416,6 +2564,48 @@ function loadLiveContracts(){ } if(pillRow.childNodes.length) card.appendChild(pillRow); + // Compact summary strip — at-a-glance numbers so scanners get + // the headline without expanding. Mirrors the colors used in + // the full economics grid below: revenue green, margin tiered. + var strip=document.createElement('div');strip.className='card-strip'; + function stripCell(label,value,color){ + var s=document.createElement('span'); + var b=document.createElement('strong');b.style.color=color||'#e6edf3';b.textContent=value; + s.appendChild(b);s.appendChild(document.createTextNode(label)); + return s; + } + if(c.economics){ + strip.appendChild(stripCell(' rev','$'+Math.round(c.economics.gross_revenue/1000)+'K','#3fb950')); + var stripMarginColor=c.economics.margin_pct>=25?'#3fb950':c.economics.margin_pct>=10?'#d29922':'#f85149'; + strip.appendChild(stripCell(' margin',c.economics.margin_pct+'%',stripMarginColor)); + } + if(c.fill_probability&&c.fill_probability.curve&&c.fill_probability.curve.length){ + var bucket7=c.fill_probability.curve.find(function(pt){return pt.day===7}); + var fillBucket=bucket7||c.fill_probability.curve[0]; + var spanLabel=bucket7?' fill by 1wk':' fill by day '+fillBucket.day; + strip.appendChild(stripCell(spanLabel,fillBucket.cumulative_pct+'%','#58a6ff')); + } + var topCand=(prop.candidates||[])[0]; + if(topCand&&topCand.name){ + // Abbreviate "Maria Sanchez" → "Maria S." so the strip stays one line. + var nameParts=topCand.name.trim().split(/\s+/); + var topShort=nameParts.length>1?nameParts[0]+' '+(nameParts[nameParts.length-1][0]||'')+'.':nameParts[0]; + strip.appendChild(stripCell(' top match',topShort,'#e6edf3')); + } + if(strip.childNodes.length) card.appendChild(strip); + + // Pattern (meta-index) chip — kept visible on the compact card + // because it's a glance-worthy "we have playbooks for this kind + // of contract" signal that doesn't take much vertical space. + if(c.discovered_pattern){ + var pat=document.createElement('div');pat.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5'; + var plabel=document.createElement('span');plabel.style.cssText='color:#3fb950;font-weight:600;margin-right:6px'; + plabel.textContent='MEMORY ('+c.pattern_matched+' playbooks):'; + pat.appendChild(plabel); + pat.appendChild(document.createTextNode(' '+c.discovered_pattern)); + card.appendChild(pat); + } + // Fill-probability curve — shows "likelihood of filling by day N" // as a horizontal bar of cumulative percentages. Drill down that // J asked for: "percentage likelihood of filling them on a certain time." @@ -2429,9 +2619,27 @@ function loadLiveContracts(){ var fpBase=document.createElement('span');fpBase.textContent='base '+fp.base_pct+'% · pool × urgency'; fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase); fpRow.appendChild(fpLabel); - // Horizontal stacked bar — each bucket as a segment + // Horizontal stacked bar — each bucket as a segment. + // .fp-bar starts hidden (clip-path); IntersectionObserver adds + // .lit when the bar enters viewport, which fires the paint + + // shimmer animations defined in the