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),
|
|
135
|
-
(glassmorphism aurora)
|
|
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.
|
|
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
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
if(
|
|
567
|
-
|
|
568
|
-
pop.
|
|
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 /
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|