← back to Agent Cabinet
enlarge gap/suggestion handles to 30px with hover/focus labels, make ghost officer node read as actionable, focus primary drawer action on open
6fb76954efa0b67ce9b51b4e3e1e5b0d8ad8bad6 · 2026-06-18 18:59:49 -0700 · SteveStudio2
Files touched
Diff
commit 6fb76954efa0b67ce9b51b4e3e1e5b0d8ad8bad6
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu Jun 18 18:59:49 2026 -0700
enlarge gap/suggestion handles to 30px with hover/focus labels, make ghost officer node read as actionable, focus primary drawer action on open
---
pyramid.html | 33 ++++++++++++++++++++++++---------
1 file changed, 24 insertions(+), 9 deletions(-)
diff --git a/pyramid.html b/pyramid.html
index 86aa66a..18712e1 100644
--- a/pyramid.html
+++ b/pyramid.html
@@ -125,11 +125,19 @@
.mini:hover{background:#1b2435}
.sug-hit{cursor:pointer}
#sug-handles{position:absolute;inset:0;pointer-events:none;z-index:6}
- .sug-h{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;cursor:pointer;width:22px;height:22px;border-radius:50%;
- border:2px solid #0b0e14;background:#fbbf24;color:#3a2a05;font-size:12px;font-weight:900;line-height:1;display:flex;align-items:center;justify-content:center;
+ .sug-h{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;cursor:pointer;width:30px;height:30px;border-radius:50%;
+ border:2px solid #0b0e14;background:#fbbf24;color:#3a2a05;font-size:15px;font-weight:900;line-height:1;display:flex;align-items:center;justify-content:center;
box-shadow:0 0 0 2px rgba(251,191,36,.35),0 3px 9px rgba(0,0,0,.55);transition:transform .12s}
- .sug-h:hover{transform:translate(-50%,-50%) scale(1.4);z-index:7}
+ .sug-h:hover,.sug-h:focus-visible{transform:translate(-50%,-50%) scale(1.3);z-index:8}
.sug-h.approved{background:#34d399;color:#062a1c;box-shadow:0 0 0 2px rgba(52,211,153,.4),0 3px 9px rgba(0,0,0,.55)}
+ /* hover/focus label on suggestion + gap handles */
+ .sug-h::after,.gap-h::after{content:attr(data-act);position:absolute;left:50%;bottom:calc(100% + 7px);transform:translateX(-50%);
+ white-space:nowrap;font-size:11px;font-weight:700;letter-spacing:.02em;padding:3px 8px;border-radius:6px;border:1px solid;
+ opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 4px 14px rgba(0,0,0,.5);z-index:9}
+ .sug-h::after{background:#33280a;color:#fde68a;border-color:#fbbf24}
+ .gap-h::after{background:#2a1414;color:#fca5a5;border-color:#b91c1c}
+ .sug-h.approved::after,.gap-h.approved::after{background:#0f2a1c;color:#86efac;border-color:#16a34a}
+ .sug-h:hover::after,.sug-h:focus-visible::after,.gap-h:hover::after,.gap-h:focus-visible::after{opacity:1}
.rel-flow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;margin-bottom:4px}
.rel-end{background:#161d2c;border:1px solid #2b3650;border-radius:6px;padding:3px 9px;font-weight:700}
.rel-to{border-color:#fbbf24;color:#fde68a}
@@ -211,11 +219,15 @@
.orphan-chip:hover{filter:brightness(1.25);border-color:#f87171}
.orphan-chip.gap-approved{background:#0f2a1c;border-color:#34d399;color:#86efac}
.gap-hit{cursor:pointer}
- .gap-h{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;cursor:pointer;width:22px;height:22px;border-radius:50%;
- border:2px solid #0b0e14;background:#f87171;color:#3a0a0a;font-size:13px;font-weight:900;line-height:1;display:flex;align-items:center;justify-content:center;
+ .gap-h{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;cursor:pointer;width:30px;height:30px;border-radius:50%;
+ border:2px solid #0b0e14;background:#f87171;color:#3a0a0a;font-size:17px;font-weight:900;line-height:1;display:flex;align-items:center;justify-content:center;
box-shadow:0 0 0 2px rgba(248,113,113,.35),0 3px 9px rgba(0,0,0,.55);transition:transform .12s}
- .gap-h:hover{transform:translate(-50%,-50%) scale(1.4);z-index:7}
+ .gap-h:hover,.gap-h:focus-visible{transform:translate(-50%,-50%) scale(1.3);z-index:8}
.gap-h.approved{background:#34d399;color:#062a1c;box-shadow:0 0 0 2px rgba(52,211,153,.4),0 3px 9px rgba(0,0,0,.55)}
+ /* the ghost (proposed) officer node itself reads as actionable */
+ .officer-node.gap-node{cursor:pointer;transition:filter .12s,box-shadow .12s}
+ .officer-node.gap-node:hover,.officer-node.gap-node:focus-visible{filter:brightness(1.22);box-shadow:0 0 0 2px rgba(248,113,113,.4),0 6px 22px rgba(0,0,0,.5)}
+ .orphan-chip:focus-visible{outline:2px solid #f87171;outline-offset:2px}
</style>
</head>
<body>
@@ -619,7 +631,7 @@ function drawCross(){
sg+=`<path d="${d}" fill="none" stroke="${ap?'#34d399':'#fbbf24'}" stroke-width="${ap?1.9:1.3}" stroke-opacity="${ap?.95:.82}" ${ap?'':'stroke-dasharray="1.5 4.5"'} marker-end="url(#${ap?'arrG':'arr'})"/>`;
sg+=`<path class="sug-hit" data-key="${key}" d="${d}" fill="none" stroke="transparent" stroke-width="16" pointer-events="stroke" style="cursor:pointer"/>`;
const hx=(a.x+bx)/2, hy=(a.y+by)/2;
- hh+=`<button class="sug-h${ap?' approved':''}" data-key="${key}" style="left:${hx}px;top:${hy}px" title="${offName(g.from)} → ${offName(g.to)} · ${g.kind==='skill'?'/':'@'}${g.id} (click to approve / remove)">${ap?'✓':'◆'}</button>`;
+ hh+=`<button class="sug-h${ap?' approved':''}" data-key="${key}" data-act="${ap?'Approved ✓':'Review move'}" style="left:${hx}px;top:${hy}px" title="${offName(g.from)} → ${offName(g.to)} · ${g.kind==='skill'?'/':'@'}${g.id} (click to review / approve)">${ap?'✓':'◆'}</button>`;
});
svg.innerHTML=`<defs>`+
`<marker id="arr" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#fbbf24"/></marker>`+
@@ -654,7 +666,8 @@ function drawGaps(){
gp+=`<path class="gap-hit" data-key="${key}" d="M${a.x},${a.y} C${a.x},${my} ${b.x},${my} ${b.x},${b.y}" fill="none" stroke="transparent" stroke-width="16" pointer-events="stroke"/>`;
const hx=(a.x+b.x)/2, hy=(a.y+b.y)/2;
const ttl=g.type==='new-vp'?('Stand up '+g.label):('Adopt @'+g.id+' → '+g._target.nm);
- hh+=`<button class="gap-h${ap?' approved':''}" data-key="${key}" style="left:${hx}px;top:${hy}px" title="${ttl} (click to approve / dismiss)">${ap?'✓':'+'}</button>`;
+ const act=ap?'Approved ✓':'Approve gap';
+ hh+=`<button class="gap-h${ap?' approved':''}" data-key="${key}" data-act="${act}" style="left:${hx}px;top:${hy}px" title="${ttl} (click to approve / dismiss)">${ap?'✓':'+'}</button>`;
});
svg.innerHTML=`<defs>`+
`<marker id="gapArr" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#f87171"/></marker>`+
@@ -923,7 +936,9 @@ document.addEventListener('keydown', e=>{
const open=dr.classList.contains('open');
if(open && !_drawerWasOpen){
_drawerReturn=document.activeElement;
- const btn=[...dr.querySelectorAll('.dr-actions .dr-btn, .dr-x')].find(b=>b.offsetParent!==null);
+ // Prefer the primary action (Approve / Run); fall back to close (✕).
+ const btn=[...dr.querySelectorAll('.dr-actions .dr-btn')].find(b=>b.offsetParent!==null)
+ || dr.querySelector('.dr-x');
if(btn) requestAnimationFrame(()=>btn.focus());
} else if(!open && _drawerWasOpen){
if(_drawerReturn && typeof _drawerReturn.focus==='function') _drawerReturn.focus();
← 183cbbf calmer overlay defaults (Shared+Suggested off) with localSto
·
back to Agent Cabinet
·
cabinet: register dw-promo-banner director under vp-dw-marke 75715aa →