spectoflow 0.13.2 → 0.13.3

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/README.md CHANGED
@@ -131,8 +131,9 @@ full-height group-chat panel), and **Info** (a project-at-a-glance summary). URL
131
131
  client-side.
132
132
 
133
133
  **Designs & theme.** The dashboard ships **switchable designs** (Settings → *Dashboard design*):
134
- **Control Room** (violet), **Obsidian Ops** (near-black lime/cyan, mono), and **Neon Command**
135
- (glassmorphism aurora). Each works in light and dark (the moon toggle). A design is a `data-design`
134
+ **Control Room** (violet), **Obsidian Ops** (near-black lime/cyan, mono), **Neon Command**
135
+ (glassmorphism aurora), and **Mission Control** (indigo control panel). Each works in light and dark
136
+ (the moon toggle). A design is a `data-design`
136
137
  skin — a scoped CSS token block plus a one-line entry in `dashboard/public/designs.js`, so adding one
137
138
  is trivial. Fonts are **self-hosted** (`dashboard/public/fonts/*.woff2`), keeping the dashboard fully
138
139
  offline and dependency-free. Your choice persists per viewer (localStorage) and as the project
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
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",
@@ -559,13 +559,20 @@ function renderWfPop(){
559
559
  }
560
560
  function positionWfPop(anchorEl, pop){
561
561
  const r=anchorEl.getBoundingClientRect();
562
+ const vpH=window.innerHeight, m=12, edge=10;
563
+ const below=vpH - r.bottom - m - edge; // room below the step
564
+ const above=r.top - m - edge; // room above the step
565
+ // Prefer below; flip above only when below is cramped and above has more room. Then cap the
566
+ // popover's height to the space on the chosen side so it always fits the viewport and scrolls
567
+ // internally (the enable/disable button stays reachable via the sticky footer).
568
+ const useAbove = below < 240 && above > below;
562
569
  pop.style.visibility='hidden'; pop.hidden=false; pop.classList.remove('above');
563
- const pw=pop.offsetWidth, ph=pop.offsetHeight;
564
- const left=Math.max(10, Math.min(r.left + r.width/2 - pw/2, window.innerWidth-pw-10));
565
- let top=r.bottom + 12, above=false;
566
- if(top + ph > window.innerHeight-10 && r.top - ph - 12 > 10){ top=r.top - ph - 12; above=true; }
567
- pop.style.left=left+'px'; pop.style.top=top+'px';
568
- pop.classList.toggle('above',above);
570
+ pop.style.maxHeight=Math.max(160, (useAbove?above:below))+'px';
571
+ const pw=pop.offsetWidth;
572
+ const left=Math.max(edge, Math.min(r.left + r.width/2 - pw/2, window.innerWidth-pw-edge));
573
+ if(useAbove){ pop.style.top=(r.top - m - pop.offsetHeight)+'px'; pop.classList.add('above'); }
574
+ else { pop.style.top=(r.bottom + m)+'px'; }
575
+ pop.style.left=left+'px';
569
576
  pop.style.setProperty('--caret-x', ((r.left + r.width/2) - left)+'px');
570
577
  pop.style.visibility='';
571
578
  }
@@ -904,10 +911,10 @@ const navToggle=$('#navToggle');
904
911
  if(navToggle) navToggle.addEventListener('click',e=>{ e.stopPropagation(); const open=!document.body.classList.contains('nav-open'); document.body.classList.toggle('nav-open',open); navToggle.setAttribute('aria-expanded',String(open)); });
905
912
  document.addEventListener('click',e=>{ if(!document.body.classList.contains('nav-open')) return; if(e.target.closest('#tabs')||e.target.closest('#navToggle')) return; closeNav(); });
906
913
  document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeNav(); });
907
- // workflow step popover — close on outside click / scroll / resize / Esc
914
+ // workflow step popover — close on outside click / Esc / resize. (No scroll-to-close: a capture
915
+ // scroll listener also fires on the popover's own internal scroll, which would slam it shut.)
908
916
  document.addEventListener('click',e=>{ if(wfPopStep && !e.target.closest('#wfPop') && !e.target.closest('.wf-step2')) closeWfPop(); });
909
917
  document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeWfPop(); });
910
- window.addEventListener('scroll',()=>{ if(wfPopStep) closeWfPop(); },true);
911
918
  window.addEventListener('resize',()=>{ if(wfPopStep) closeWfPop(); });
912
919
  // keep the URL and the path in sync when the user uses the browser back/forward buttons
913
920
  window.addEventListener('popstate',()=>{
@@ -23,7 +23,7 @@
23
23
  { id: 'control-room', name: 'Control Room', desc: 'Dark violet engineering control room — the original.' },
24
24
  { id: 'obsidian', name: 'Obsidian Ops', desc: 'Near-black mission-control — electric lime + cyan, mono type, Linear/Vercel precision.' },
25
25
  { id: 'neon-command', name: 'Neon Command', desc: 'Glassmorphism — aurora violet + cyan, Space Grotesk display, control-room ambiance.' },
26
- // one more from the design set coming: nord…
26
+ { id: 'mission', name: 'Mission Control', desc: 'Indigo control panel on solid slate — clean, flat, status-coloured.' },
27
27
  ];
28
28
  if (typeof module !== 'undefined' && module.exports) module.exports = DESIGNS;
29
29
  else root.DESIGNS = DESIGNS;
@@ -657,8 +657,10 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
657
657
  .wf-detail-skill b { color:var(--ink); font-weight:600; }
658
658
 
659
659
  /* ---- Workflow: click-popover, connector arrows, mobile reflow (v0.13.2) ---- */
660
- .wf-pop { position:fixed; z-index:40; width:min(360px,92vw); background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px 18px; }
660
+ .wf-pop { position:fixed; z-index:40; width:min(360px,92vw); max-height:min(72vh,540px); overflow-y:auto; overscroll-behavior:contain; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px 18px; }
661
661
  .wf-pop[hidden] { display:none; }
662
+ /* keep the enable/disable button visible even when the details scroll (sticky footer) */
663
+ .wf-pop-actions { position:sticky; bottom:0; margin:15px -18px 0; padding:12px 18px; background:var(--surface); border-top:1px solid var(--line); }
662
664
  .wf-pop::before { content:""; position:absolute; top:-8px; left:var(--caret-x,50%); transform:translateX(-50%); border-left:8px solid transparent; border-right:8px solid transparent; border-bottom:8px solid var(--surface); filter:drop-shadow(0 -1px 0 var(--line)); }
663
665
  .wf-pop.above::before { top:auto; bottom:-8px; border-bottom:0; border-top:8px solid var(--surface); filter:drop-shadow(0 1px 0 var(--line)); }
664
666
  .wf-pop-actions { margin-top:15px; display:flex; }
@@ -773,3 +775,24 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
773
775
  --signal:#6d3bff; --cool:#0891b2; --on-accent:#ffffff;
774
776
  --s-done:#0f9d63; --s-to_validate:#0891b2; --s-in_progress:#c2801e; --s-blocked:#d15168;
775
777
  }
778
+
779
+ /* --- Mission Control: indigo control panel on solid slate (reproduces the init-dashboard skill) --- */
780
+ :root[data-design="mission"] {
781
+ --bg:#1b1e24; --surface:#262a33; --surface-2:#2d323d; --line:#353b46;
782
+ --ink:#e9ebf0; --muted:#9aa2af; --faint:#6b7280;
783
+ --signal:#5b6cff; --cool:#22d3ee; --on-accent:#ffffff;
784
+ --s-todo:#6b7280; --s-in_progress:#f59e0b; --s-to_validate:#ec4899; --s-to_analyze:#a855f7; --s-done:#22c55e; --s-blocked:#ef4444;
785
+ --radius:14px; --shadow:0 10px 30px rgba(0,0,0,.4);
786
+ }
787
+ /* signature touches: a filled active tab pill + solid-indigo progress bars */
788
+ :root[data-design="mission"] .tab.is-active { background:var(--signal); color:var(--on-accent); box-shadow:none; }
789
+ :root[data-design="mission"] .progress-meter-fill,
790
+ :root[data-design="mission"] .phase-bar-fill,
791
+ :root[data-design="mission"] .bar-fill { background:var(--signal); }
792
+ :root[data-design="mission"] .fchip.active[data-status="all"] { background:var(--signal); }
793
+ :root[data-design="mission"][data-theme="light"] {
794
+ --bg:#e4e7ee; --surface:#fbfcfe; --surface-2:#eef1f6; --line:#cfd5e0;
795
+ --ink:#1a1f2b; --muted:#565f70; --faint:#8792a6;
796
+ --signal:#5b6cff; --cool:#0891b2; --on-accent:#ffffff;
797
+ --s-todo:#64748b; --s-in_progress:#d97706; --s-to_validate:#db2777; --s-to_analyze:#9333ea; --s-done:#16a34a; --s-blocked:#dc2626;
798
+ }
@@ -203,17 +203,22 @@ const server = http.createServer(async (req,res)=>{
203
203
  let file=p==='/'?'/index.html':p;
204
204
  const full=path.join(PUBLIC,path.normalize(file).replace(/^(\.\.[/\\])+/,''));
205
205
  if(!full.startsWith(PUBLIC)){ res.writeHead(403); return res.end('Forbidden'); }
206
+ // Local tool: always serve the freshest asset — never let the browser cache a stale app.js/css.
207
+ const noCache = { 'Cache-Control': 'no-store, must-revalidate' };
206
208
  fs.readFile(full,(err,data)=>{
207
209
  if(err){
208
210
  if(req.method==='GET' && !path.extname(p) && !p.startsWith('/api/')){
209
211
  return fs.readFile(path.join(PUBLIC,'index.html'),(e2,d2)=>{
210
212
  if(e2){ res.writeHead(404); return res.end('Not found'); }
211
- res.writeHead(200,{'Content-Type':MIME['.html']}); res.end(d2);
213
+ res.writeHead(200,Object.assign({'Content-Type':MIME['.html']},noCache)); res.end(d2);
212
214
  });
213
215
  }
214
216
  res.writeHead(404); return res.end('Not found');
215
217
  }
216
- res.writeHead(200,{'Content-Type':MIME[path.extname(full)]||'application/octet-stream'}); res.end(data);
218
+ const ext=path.extname(full);
219
+ // fonts are content-hashed by name and safe to cache long-term; everything else is no-store
220
+ const headers = ext==='.woff2'||ext==='.woff' ? { 'Cache-Control':'public, max-age=604800' } : noCache;
221
+ res.writeHead(200,Object.assign({'Content-Type':MIME[ext]||'application/octet-stream'},headers)); res.end(data);
217
222
  });
218
223
  }catch(e){ sendJSON(res,500,{error:String(e&&e.message||e)}); }
219
224
  });