demo: search.html — Live Market explainer rewrite + fp-bar viewport-paint + compact contract cards
Some checks failed
lakehouse/auditor 18 blocking issues: cloud: claim not backed — "Verified end-to-end via playwright on devop.live/lakehouse:"
Some checks failed
lakehouse/auditor 18 blocking issues: cloud: claim not backed — "Verified end-to-end via playwright on devop.live/lakehouse:"
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) <noreply@anthropic.com>
This commit is contained in:
parent
f892230699
commit
f4dc1b29e3
@ -202,6 +202,147 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
|
|||||||
50% { opacity:1; transform:scale(1.1) }
|
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 */
|
/* Bottom section-jump nav — mobile only */
|
||||||
/* Desktop: top nav handles navigation; this dock stays hidden. */
|
/* Desktop: top nav handles navigation; this dock stays hidden. */
|
||||||
/* Mobile: top nav collapses (.bar nav:display:none below); this fixed dock */
|
/* 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
|
|||||||
<div class="section" id="live-market-section">
|
<div class="section" id="live-market-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-title" style="color:#e6edf3;font-size:13px">① Live Market — Chicago right now</span>
|
<span class="section-title" style="color:#e6edf3;font-size:13px">① Live Market — Chicago right now</span>
|
||||||
<span class="section-meta" id="live-market-meta">Public permit data · per-shift schedule on the internal calendar · click any shift to filter</span>
|
<span class="section-meta" id="live-market-meta">City of Chicago Open Data · cross-referenced against the 500K-worker bench · click any shift to slice the dials</span>
|
||||||
</div>
|
</div>
|
||||||
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px">
|
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px">
|
||||||
The console on the left is the live punch clock — current time, the active shift, and
|
Live from the <strong>City of Chicago Open Data</strong> permit feed (Building Permits
|
||||||
four dials for what that shift looks like right now: open permits, workers needed,
|
≥ $250K), cross-referenced against our 500K-worker bench. The console on the left is
|
||||||
bench available, and coverage. The panel beside it adds combined bill demand for those
|
the punch clock — current time, today's active shift, and four dials watching
|
||||||
permits, deadline pressure on the open queue, the top roles in demand, and a shift
|
<strong>open permits</strong>, <strong>workers needed</strong>,
|
||||||
comparison strip. <strong>Click any shift</strong> in the comparison to filter every
|
<strong>bench depth</strong>, and <strong>projected coverage</strong>. The panel on
|
||||||
dial and the bill rate to that shift's slice of the internal calendar — past, active,
|
the right reads the same feed financially: combined bill demand if every permit fills,
|
||||||
or upcoming. Click the same shift again to return to the all-shifts total. This is the
|
deadline pressure across <em>overdue / urgent / soon / scheduled</em>, the four roles
|
||||||
<em>real world</em> the rest of the page is reacting to.
|
being asked for most, and a shift-mix bar. <strong>Click any shift bar</strong> 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.
|
||||||
</p>
|
</p>
|
||||||
<div id="live-market-hero" style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start">
|
<div id="live-market-hero" style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start">
|
||||||
<div style="grid-column:1/-1">
|
<div style="grid-column:1/-1">
|
||||||
@ -2355,8 +2499,12 @@ function loadLiveContracts(){
|
|||||||
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
|
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
|
||||||
var urg=tl.urgency||'scheduled';
|
var urg=tl.urgency||'scheduled';
|
||||||
var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd';
|
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;
|
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
|
// Header — permit
|
||||||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
|
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
|
||||||
var left=document.createElement('div');
|
var left=document.createElement('div');
|
||||||
@ -2416,6 +2564,48 @@ function loadLiveContracts(){
|
|||||||
}
|
}
|
||||||
if(pillRow.childNodes.length) card.appendChild(pillRow);
|
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"
|
// Fill-probability curve — shows "likelihood of filling by day N"
|
||||||
// as a horizontal bar of cumulative percentages. Drill down that
|
// as a horizontal bar of cumulative percentages. Drill down that
|
||||||
// J asked for: "percentage likelihood of filling them on a certain time."
|
// 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';
|
var fpBase=document.createElement('span');fpBase.textContent='base '+fp.base_pct+'% · pool × urgency';
|
||||||
fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase);
|
fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase);
|
||||||
fpRow.appendChild(fpLabel);
|
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 <style> block above.
|
||||||
var fpBar=document.createElement('div');
|
var fpBar=document.createElement('div');
|
||||||
|
fpBar.className='fp-bar';
|
||||||
fpBar.style.cssText='display:flex;height:8px;border-radius:4px;overflow:hidden;background:#161b22;margin-bottom:6px';
|
fpBar.style.cssText='display:flex;height:8px;border-radius:4px;overflow:hidden;background:#161b22;margin-bottom:6px';
|
||||||
|
// Lazy-init a single shared observer — the same instance can
|
||||||
|
// watch every .fp-bar on the page. unobserve() after firing so
|
||||||
|
// the bar doesn't re-paint if the user scrolls back past it.
|
||||||
|
if(!window._lhFpObserver){
|
||||||
|
window._lhFpObserver=new IntersectionObserver(function(entries){
|
||||||
|
entries.forEach(function(e){
|
||||||
|
if(e.isIntersecting){
|
||||||
|
e.target.classList.add('lit');
|
||||||
|
window._lhFpObserver.unobserve(e.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},{ threshold: 0.2 });
|
||||||
|
}
|
||||||
|
window._lhFpObserver.observe(fpBar);
|
||||||
fp.curve.forEach(function(pt,idx){
|
fp.curve.forEach(function(pt,idx){
|
||||||
var prev=idx===0?0:fp.curve[idx-1].cumulative_pct;
|
var prev=idx===0?0:fp.curve[idx-1].cumulative_pct;
|
||||||
var delta=pt.cumulative_pct-prev;
|
var delta=pt.cumulative_pct-prev;
|
||||||
@ -2471,7 +2679,7 @@ function loadLiveContracts(){
|
|||||||
fpNote.style.cssText='font-size:9px;color:#545d68;margin-top:6px;line-height:1.4';
|
fpNote.style.cssText='font-size:9px;color:#545d68;margin-top:6px;line-height:1.4';
|
||||||
fpNote.textContent='Cumulative chance the role gets fully staffed by that point, given pool depth, urgency, and past fill patterns.';
|
fpNote.textContent='Cumulative chance the role gets fully staffed by that point, given pool depth, urgency, and past fill patterns.';
|
||||||
fpRow.appendChild(fpNote);
|
fpRow.appendChild(fpNote);
|
||||||
card.appendChild(fpRow);
|
detailsInner.appendChild(fpRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Economics panel — "as though the contracts were accepted and filled"
|
// Economics panel — "as though the contracts were accepted and filled"
|
||||||
@ -2497,7 +2705,7 @@ function loadLiveContracts(){
|
|||||||
var overColor=ec.over_bill_count>0?'#f85149':'#8b949e';
|
var overColor=ec.over_bill_count>0?'#f85149':'#8b949e';
|
||||||
ecRow.appendChild(ecCell('Over-Bill Pool',ec.over_bill_count+'/'+(prop.candidates||[]).length,
|
ecRow.appendChild(ecCell('Over-Bill Pool',ec.over_bill_count+'/'+(prop.candidates||[]).length,
|
||||||
ec.over_bill_count>0?'$'+ec.over_bill_pool_margin_at_risk.toLocaleString()+' at risk':'none flagged',overColor));
|
ec.over_bill_count>0?'$'+ec.over_bill_pool_margin_at_risk.toLocaleString()+' at risk':'none flagged',overColor));
|
||||||
card.appendChild(ecRow);
|
detailsInner.appendChild(ecRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project Index — portfolio of public-data signals for this permit's
|
// Project Index — portfolio of public-data signals for this permit's
|
||||||
@ -2572,22 +2780,13 @@ function loadLiveContracts(){
|
|||||||
ebBody.appendChild(errDiv);
|
ebBody.appendChild(errDiv);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
card.appendChild(eb);
|
detailsInner.appendChild(eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
if(p.description){
|
if(p.description){
|
||||||
var desc=document.createElement('div');desc.style.cssText='color:#94a3b8;font-size:11px;margin-bottom:10px;line-height:1.5';
|
var desc=document.createElement('div');desc.style.cssText='color:#94a3b8;font-size:11px;margin-bottom:10px;line-height:1.5';
|
||||||
desc.textContent=p.description;card.appendChild(desc);
|
desc.textContent=p.description;detailsInner.appendChild(desc);
|
||||||
}
|
|
||||||
// Pattern (meta-index) chip
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
// Candidates
|
// Candidates
|
||||||
var cands=prop.candidates||[];
|
var cands=prop.candidates||[];
|
||||||
@ -2615,13 +2814,52 @@ function loadLiveContracts(){
|
|||||||
sub2.textContent=subText;
|
sub2.textContent=subText;
|
||||||
info.appendChild(nm);info.appendChild(sub2);
|
info.appendChild(nm);info.appendChild(sub2);
|
||||||
row.appendChild(av);row.appendChild(info);
|
row.appendChild(av);row.appendChild(info);
|
||||||
card.appendChild(row);
|
detailsInner.appendChild(row);
|
||||||
});
|
});
|
||||||
if(cands.length>3){
|
if(cands.length>3){
|
||||||
var more=document.createElement('div');more.style.cssText='font-size:10px;color:#58a6ff;padding:4px 10px;margin-top:2px';
|
var more=document.createElement('div');more.style.cssText='font-size:10px;color:#58a6ff;padding:4px 10px;margin-top:2px';
|
||||||
more.textContent='+ '+(cands.length-3)+' more candidates available';
|
more.textContent='+ '+(cands.length-3)+' more candidates available';
|
||||||
card.appendChild(more);
|
detailsInner.appendChild(more);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap the foldable section + add it to the card. The CSS
|
||||||
|
// .card-details rule animates grid-template-rows: 0fr → 1fr
|
||||||
|
// when the parent gets .expanded, so this whole block grows
|
||||||
|
// smoothly to its natural height when the user clicks.
|
||||||
|
var details=document.createElement('div');details.className='card-details';
|
||||||
|
details.appendChild(detailsInner);
|
||||||
|
card.appendChild(details);
|
||||||
|
|
||||||
|
// Toggle affordance — small caret + label at the bottom. CSS
|
||||||
|
// rotates the chevron 180° when expanded; the label flips text
|
||||||
|
// via the click handler below.
|
||||||
|
var toggleRow=document.createElement('div');toggleRow.className='card-toggle';
|
||||||
|
var chevron=document.createElement('span');chevron.className='chevron';chevron.textContent='▾';
|
||||||
|
var toggleLabel=document.createElement('span');
|
||||||
|
toggleLabel.textContent='click for full curve, economics, candidates, project index';
|
||||||
|
toggleRow.appendChild(chevron);toggleRow.appendChild(toggleLabel);
|
||||||
|
card.appendChild(toggleRow);
|
||||||
|
|
||||||
|
// Click handler — expand/collapse + auto-open Project Index when
|
||||||
|
// expanding. Skips the toggle when the click landed inside the
|
||||||
|
// already-expanded details (so contractor links / SMS copy /
|
||||||
|
// <details> toggles inside still work) and when the user is
|
||||||
|
// selecting text (so a triple-click drag doesn't snap it shut).
|
||||||
|
card.addEventListener('click',function(e){
|
||||||
|
if(e.target.closest('.card-details')) return;
|
||||||
|
if(window.getSelection&&window.getSelection().toString().length>0) return;
|
||||||
|
var nowExpanded=!card.classList.contains('expanded');
|
||||||
|
card.classList.toggle('expanded');
|
||||||
|
toggleLabel.textContent=nowExpanded
|
||||||
|
? 'click to collapse'
|
||||||
|
: 'click for full curve, economics, candidates, project index';
|
||||||
|
// Auto-open the Project Index ONLY on user-driven expand. This
|
||||||
|
// avoids firing 20+ OSHA scrapes on page load — each open
|
||||||
|
// triggers /intelligence/permit_entities, which live-scrapes
|
||||||
|
// OSHA at ~1-2s per contractor.
|
||||||
|
if(nowExpanded&&typeof eb!=='undefined'&&eb&&!eb.open) eb.open=true;
|
||||||
|
});
|
||||||
|
|
||||||
el.appendChild(card);
|
el.appendChild(card);
|
||||||
});
|
});
|
||||||
}).catch(function(e){
|
}).catch(function(e){
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user