[object Object]

← 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 →