spectoflow 0.23.3 → 0.23.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.23.3",
3
+ "version": "0.23.5",
4
4
  "description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
5
5
  "keywords": [
6
6
  "spec-driven-development",
@@ -363,17 +363,26 @@ function renderOverview(){
363
363
  topRow.append(ocard(t('chart.scopeVsDelivered'), area));
364
364
  box.append(topRow);
365
365
 
366
- // Workflow-at-a-glance strip (reuses the wf-arrow flow animation)
366
+ // Workflow-at-a-glance strip (reuses the wf-arrow flow animation) — clicking a step opens the same
367
+ // enable/disable popover as the dedicated Workflow tab (openWfPop/renderWfPop), so a step can be
368
+ // toggled right from the Board without switching tabs.
367
369
  const strip=el('div','wf-strip');
368
370
  const steps=P.workflow||[];
369
371
  steps.forEach((st,i)=>{
370
- const node=el('div','wf-mini'+(st.enabled?'':' off'));
372
+ const node=el('div','wf-mini'+(st.enabled?'':' off')+(st.name===wfPopStep?' is-selected':''));
373
+ node.dataset.name=st.name;
374
+ node.tabIndex=0; node.setAttribute('role','button'); node.setAttribute('aria-expanded',String(st.name===wfPopStep));
375
+ node.title=st.name;
371
376
  node.append(el('span','dot')); node.append(el('span','nm',st.name));
377
+ const sel=(e)=>{ e.stopPropagation(); if(wfPopStep===st.name) closeWfPop(); else openWfPop(st.name); };
378
+ node.addEventListener('click',sel);
379
+ node.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); sel(e); } });
372
380
  strip.append(node);
373
381
  if(i<steps.length-1){ const a=el('div','wf-arrow'+(st.enabled&&steps[i+1].enabled?'':' off')); strip.append(a); }
374
382
  });
375
383
  if(!steps.length) strip.append(el('div','empty',t('board.noWorkflow')));
376
384
  box.append(ocard(t('board.workflowGlance'), strip));
385
+ if(wfPopStep) renderWfPop(); // re-anchor an open popover after a live re-render of this strip too
377
386
 
378
387
  // Per-phase progress bars — only phases that actually hold tasks (headings with no checkbox tasks
379
388
  // are noise, not phases), and cap the list height with an internal scroll so a big project with
@@ -675,16 +684,27 @@ function wfPopFill(pop, s, idx){
675
684
  const actions=el('div','wf-pop-actions'); actions.append(btn); pop.append(actions);
676
685
  }
677
686
  function openWfPop(name){ wfPopStep=name; renderWfPop(); }
678
- function closeWfPop(){ wfPopStep=null; const p=$('#wfPop'); if(p) p.hidden=true; $$('#wfDiagram .wf-step2.is-selected').forEach(x=>x.classList.remove('is-selected')); }
687
+ function closeWfPop(){ wfPopStep=null; const p=$('#wfPop'); if(p) p.hidden=true; $$('#wfDiagram .wf-step2.is-selected,.wf-mini.is-selected').forEach(x=>x.classList.remove('is-selected')); }
688
+ // The workflow popover has two possible triggers now: the dedicated Workflow tab's own pipeline
689
+ // (#wfDiagram .wf-step2), and the Board's "at a glance" strip (.wf-mini). Both stay mounted in the
690
+ // DOM at all times (every .panel renders every tick; only CSS hides the inactive ones), so anchoring
691
+ // always prefers whichever one sits on the currently VISIBLE panel — a hidden panel's element has a
692
+ // zero-size getBoundingClientRect() and would position the popover at the top-left corner.
693
+ function findWfAnchor(name){
694
+ const boardActive=document.querySelector('.panel[data-panel="board"].is-active');
695
+ if(boardActive){ const m=boardActive.querySelector('.wf-mini[data-name="'+CSS.escape(name)+'"]'); if(m) return m; }
696
+ const idx=(P.workflow||[]).findIndex(s=>s.name===name);
697
+ return $('#wfDiagram .wf-step2[data-idx="'+idx+'"]');
698
+ }
679
699
  function renderWfPop(){
680
700
  const p=$('#wfPop'); if(!p) return;
681
701
  const steps=P.workflow||[]; const idx=steps.findIndex(s=>s.name===wfPopStep);
682
702
  if(idx<0){ closeWfPop(); return; }
683
- const anchor=$('#wfDiagram .wf-step2[data-idx="'+idx+'"]');
703
+ const anchor=findWfAnchor(wfPopStep);
684
704
  if(!anchor){ p.hidden=true; return; }
685
- $$('#wfDiagram .wf-step2.is-selected').forEach(x=>x.classList.remove('is-selected')); anchor.classList.add('is-selected');
705
+ $$('#wfDiagram .wf-step2.is-selected,.wf-mini.is-selected').forEach(x=>x.classList.remove('is-selected')); anchor.classList.add('is-selected');
686
706
  wfPopFill(p, steps[idx], idx);
687
- positionWfPop(anchor.querySelector('.wf-circle')||anchor, p);
707
+ positionWfPop(anchor.classList.contains('wf-step2')?(anchor.querySelector('.wf-circle')||anchor):anchor, p);
688
708
  }
689
709
  function positionWfPop(anchorEl, pop){
690
710
  const r=anchorEl.getBoundingClientRect();
@@ -1070,6 +1090,7 @@ function tabFromPath(){
1070
1090
  }
1071
1091
  function taskFromPath(){ const s=pathSegments(); return (ROUTES.includes(s[0])&&s[1])?decodeURIComponent(s[1]):null; }
1072
1092
  function navigateTab(tabId,push){
1093
+ closeWfPop(); // an open popover is anchored to whichever panel is currently active — never carry it across
1073
1094
  activeTab=tabId; try{ localStorage.setItem('spf-tab',tabId); }catch{}
1074
1095
  if(push!==false){
1075
1096
  const isCustom=tabId.indexOf('custom:')===0;
@@ -1711,7 +1732,7 @@ document.addEventListener('click',e=>{ if(!document.body.classList.contains('nav
1711
1732
  document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeNav(); });
1712
1733
  // workflow step popover — close on outside click / Esc / resize. (No scroll-to-close: a capture
1713
1734
  // scroll listener also fires on the popover's own internal scroll, which would slam it shut.)
1714
- document.addEventListener('click',e=>{ if(wfPopStep && !e.target.closest('#wfPop') && !e.target.closest('.wf-step2')) closeWfPop(); });
1735
+ document.addEventListener('click',e=>{ if(wfPopStep && !e.target.closest('#wfPop') && !e.target.closest('.wf-step2') && !e.target.closest('.wf-mini')) closeWfPop(); });
1715
1736
  document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeWfPop(); });
1716
1737
  window.addEventListener('resize',()=>{ if(wfPopStep) closeWfPop(); });
1717
1738
  // keep the URL and the path in sync when the user uses the browser back/forward buttons
@@ -142,7 +142,12 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
142
142
  .legend-count { font-family:var(--mono); color:var(--ink); font-weight:600; }
143
143
 
144
144
  .wf-strip { display:flex; flex-wrap:wrap; align-items:center; gap:0; }
145
- .wf-mini { display:flex; align-items:center; gap:6px; padding:6px 11px; border:1px solid var(--line); border-radius:999px; background:var(--surface-2); font-size:11.5px; font-weight:600; white-space:nowrap; }
145
+ /* Clickable opens the same enable/disable popover as the dedicated Workflow tab (see
146
+ findWfAnchor/openWfPop in app.js), so a step can be toggled right from this Board glance strip. */
147
+ .wf-mini { display:flex; align-items:center; gap:6px; padding:6px 11px; border:1px solid var(--line); border-radius:999px; background:var(--surface-2); font-size:11.5px; font-weight:600; white-space:nowrap; cursor:pointer; transition:border-color .15s,transform .15s,box-shadow .15s; }
148
+ .wf-mini:hover { border-color:var(--signal); transform:translateY(-1px); }
149
+ .wf-mini:focus-visible { outline:2px solid var(--signal); outline-offset:1px; }
150
+ .wf-mini.is-selected { border-color:var(--signal); border-style:solid; box-shadow:0 0 0 3px color-mix(in srgb,var(--signal) 20%,transparent); }
146
151
  .wf-mini .dot { width:6px; height:6px; border-radius:50%; background:var(--s-done); }
147
152
  .wf-mini.off { opacity:.42; border-style:dashed; }
148
153
  .wf-mini.off .dot { background:var(--faint); }
@@ -0,0 +1,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "commonjs"
4
+ }