spectoflow 0.13.3 → 0.13.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.13.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const STATUS = { todo:'To do', in_progress:'In progress', to_validate:'To validate', to_analyze:'To analyze', done:'Done', blocked:'Blocked' };
|
|
3
3
|
let P = null, openTaskId = null;
|
|
4
4
|
let filter = { status: 'all', q: '' }; // board filter state — client-side only, read-only
|
|
5
|
+
let boardView = (()=>{ try{ return localStorage.getItem('spf-board-view')||'list'; }catch{ return 'list'; } })(); // 'list' | 'kanban'
|
|
5
6
|
let backlogFilter = { status: 'open', q: '' }; // backlog defaults to open (not-done) tasks
|
|
6
7
|
let backlogSort = { col: 'id', dir: 'asc' }; // backlog sort state — client-side only
|
|
7
8
|
let backlogPage = 1; const BACKLOG_PAGE = 25; // backlog pagination — client-side only
|
|
@@ -315,6 +316,10 @@ function renderBoard(){
|
|
|
315
316
|
running.forEach(a=>{ const e=li('run-live',''); e.innerHTML=`<b>${a.tool}</b> · ${a.task||'—'}`; rl.append(e); });
|
|
316
317
|
|
|
317
318
|
const board=$('#board'); board.innerHTML='';
|
|
319
|
+
updateBoardViewToggle();
|
|
320
|
+
board.classList.toggle('is-kanban', boardView==='kanban');
|
|
321
|
+
if(!tasks.length){ board.append(emptyState()); return; }
|
|
322
|
+
if(boardView==='kanban'){ renderKanban(board, tasks); return; } // columns by status
|
|
318
323
|
let shown=0;
|
|
319
324
|
(P.plans||[]).forEach(pl=> pl.phases.forEach(ph=>{
|
|
320
325
|
const filtered=ph.tasks.filter(taskMatches);
|
|
@@ -322,9 +327,29 @@ function renderBoard(){
|
|
|
322
327
|
if(!filtered.length) return; // hide phases with zero matching tasks
|
|
323
328
|
board.append(renderPhase(ph,pl.file,filtered));
|
|
324
329
|
}));
|
|
325
|
-
if(!
|
|
326
|
-
|
|
330
|
+
if(!shown) board.append(noMatchState());
|
|
331
|
+
updatePhaseToggleAll();
|
|
327
332
|
}
|
|
333
|
+
// Kanban view — one column per status, filtered by the text search (columns already are the statuses).
|
|
334
|
+
function renderKanban(board, tasks){
|
|
335
|
+
const q=filter.q.trim().toLowerCase();
|
|
336
|
+
const match=(t)=> !q || (t.title+' '+t.id).toLowerCase().includes(q);
|
|
337
|
+
const cols=el('div','kanban');
|
|
338
|
+
Object.keys(STATUS).forEach(st=>{
|
|
339
|
+
const colTasks=tasks.filter(t=> t.status===st && match(t));
|
|
340
|
+
const col=el('div','kanban-col');
|
|
341
|
+
const head=el('div','kanban-col-head');
|
|
342
|
+
const dot=el('span','kanban-dot'); dot.style.background='var(--s-'+st+')';
|
|
343
|
+
head.append(dot, el('span','kanban-col-title',STATUS[st]||st), el('span','kanban-col-count',String(colTasks.length)));
|
|
344
|
+
col.append(head);
|
|
345
|
+
const body=el('div','kanban-col-body');
|
|
346
|
+
if(!colTasks.length) body.append(el('div','kanban-empty','—'));
|
|
347
|
+
colTasks.forEach(t=> body.append(renderTask(t)));
|
|
348
|
+
col.append(body); cols.append(col);
|
|
349
|
+
});
|
|
350
|
+
board.append(cols);
|
|
351
|
+
}
|
|
352
|
+
function updateBoardViewToggle(){ $$('#boardViewToggle .vt-btn').forEach(b=> b.classList.toggle('active', b.dataset.view===boardView)); }
|
|
328
353
|
function li(cls,txt){ const e=el('li',cls); e.textContent=txt; return e; }
|
|
329
354
|
function emptyState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent='No plans yet. Ask your agent to build something — it will run Intake and write plans/*.md.'; return d; }
|
|
330
355
|
function noMatchState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent='No tasks match this filter.'; return d; }
|
|
@@ -411,16 +436,26 @@ function backlogRow(r){
|
|
|
411
436
|
return tr;
|
|
412
437
|
}
|
|
413
438
|
|
|
414
|
-
// ----
|
|
415
|
-
|
|
416
|
-
|
|
439
|
+
// ---- phase expand state: we track which phases are EXPANDED (default = none), so on a big
|
|
440
|
+
// project the board opens compact — just phase headers with progress — and the user opens what
|
|
441
|
+
// they need. Persisted per phase title (guarded for private mode). ----
|
|
442
|
+
function loadExpanded(){
|
|
443
|
+
try{ const raw=localStorage.getItem('spf-expanded'); const arr=raw?JSON.parse(raw):[]; return new Set(Array.isArray(arr)?arr:[]); }
|
|
417
444
|
catch{ return new Set(); }
|
|
418
445
|
}
|
|
419
|
-
function
|
|
420
|
-
let
|
|
446
|
+
function saveExpanded(set){ try{ localStorage.setItem('spf-expanded', JSON.stringify([...set])); }catch{} }
|
|
447
|
+
let expandedPhases=loadExpanded();
|
|
448
|
+
function allPhaseTitles(){ const t=new Set(); (P.plans||[]).forEach(pl=> pl.phases.forEach(ph=> t.add(ph.title))); return [...t]; }
|
|
449
|
+
function updatePhaseToggleAll(){
|
|
450
|
+
const btn=$('#phaseToggleAll'); if(!btn) return;
|
|
451
|
+
const titles=allPhaseTitles();
|
|
452
|
+
const allOpen = titles.length>0 && titles.every(t=> expandedPhases.has(t));
|
|
453
|
+
btn.textContent = allOpen ? 'Collapse all' : 'Expand all';
|
|
454
|
+
btn.dataset.state = allOpen ? 'open' : 'closed';
|
|
455
|
+
}
|
|
421
456
|
|
|
422
457
|
function renderPhase(ph,file,filteredTasks){
|
|
423
|
-
const isCollapsed
|
|
458
|
+
const isCollapsed=!expandedPhases.has(ph.title);
|
|
424
459
|
const sec=el('section','phase'+(isCollapsed?' is-collapsed':''));
|
|
425
460
|
const head=el('div','phase-head'); head.tabIndex=0; head.setAttribute('role','button'); head.setAttribute('aria-expanded',String(!isCollapsed));
|
|
426
461
|
head.append(el('span','chevron'));
|
|
@@ -434,8 +469,8 @@ function renderPhase(ph,file,filteredTasks){
|
|
|
434
469
|
const toggle=()=>{
|
|
435
470
|
const now=sec.classList.toggle('is-collapsed');
|
|
436
471
|
head.setAttribute('aria-expanded',String(!now));
|
|
437
|
-
if(now)
|
|
438
|
-
|
|
472
|
+
if(now) expandedPhases.delete(ph.title); else expandedPhases.add(ph.title);
|
|
473
|
+
saveExpanded(expandedPhases); updatePhaseToggleAll();
|
|
439
474
|
};
|
|
440
475
|
head.addEventListener('click',toggle);
|
|
441
476
|
head.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggle(); } });
|
|
@@ -927,6 +962,16 @@ applyActiveTab(); // sync to the resolved tab before the first render
|
|
|
927
962
|
// filters (status chips + search) — client-side only, does not write anything
|
|
928
963
|
$$('#statusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ filter.status=b.dataset.status; renderBoard(); }));
|
|
929
964
|
$('#search').addEventListener('input', e=>{ filter.q=e.target.value; renderBoard(); });
|
|
965
|
+
// board view switch — List (phase-grouped) vs Kanban (columns by status), persisted per viewer
|
|
966
|
+
$$('#boardViewToggle .vt-btn').forEach(b=> b.addEventListener('click', ()=>{ boardView=b.dataset.view; try{ localStorage.setItem('spf-board-view',boardView); }catch{} renderBoard(); }));
|
|
967
|
+
// expand / collapse all phases (List view) — keeps a big board compact by default
|
|
968
|
+
const phaseToggleAllBtn=$('#phaseToggleAll');
|
|
969
|
+
if(phaseToggleAllBtn) phaseToggleAllBtn.addEventListener('click', ()=>{
|
|
970
|
+
const titles=allPhaseTitles();
|
|
971
|
+
const allOpen = titles.length>0 && titles.every(t=> expandedPhases.has(t));
|
|
972
|
+
if(allOpen) expandedPhases.clear(); else titles.forEach(t=> expandedPhases.add(t));
|
|
973
|
+
saveExpanded(expandedPhases); renderBoard();
|
|
974
|
+
});
|
|
930
975
|
// backlog: independent filters + sortable column headers — client-side only (reset to page 1 on change)
|
|
931
976
|
$$('#backlogStatusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ backlogFilter.status=b.dataset.status; backlogPage=1; renderBacklog(); }));
|
|
932
977
|
$('#backlogSearch').addEventListener('input', e=>{ backlogFilter.q=e.target.value; backlogPage=1; renderBacklog(); });
|
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
<button class="fchip" data-status="done">Done</button>
|
|
62
62
|
<button class="fchip" data-status="blocked">Blocked</button>
|
|
63
63
|
</div>
|
|
64
|
+
<div class="view-toggle" id="boardViewToggle" role="group" aria-label="Board view">
|
|
65
|
+
<button class="vt-btn active" data-view="list" title="Grouped by phase"><span class="tab-ico" data-icon="backlog"></span><span>List</span></button>
|
|
66
|
+
<button class="vt-btn" data-view="kanban" title="Columns by status"><span class="tab-ico" data-icon="board"></span><span>Kanban</span></button>
|
|
67
|
+
</div>
|
|
68
|
+
<button class="mini-btn" id="phaseToggleAll" title="Expand or collapse all phases">Expand all</button>
|
|
64
69
|
<input type="search" id="search" class="search" placeholder="Filter tasks…" autocomplete="off" />
|
|
65
70
|
</div>
|
|
66
71
|
<div class="board" id="board"></div>
|
|
@@ -796,3 +796,28 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
|
|
|
796
796
|
--signal:#5b6cff; --cool:#0891b2; --on-accent:#ffffff;
|
|
797
797
|
--s-todo:#64748b; --s-in_progress:#d97706; --s-to_validate:#db2777; --s-to_analyze:#9333ea; --s-done:#16a34a; --s-blocked:#dc2626;
|
|
798
798
|
}
|
|
799
|
+
|
|
800
|
+
/* ---- Board view toggle (List / Kanban) + Kanban view (v0.13.4) ---- */
|
|
801
|
+
.view-toggle { display:inline-flex; gap:2px; padding:2px; border:1px solid var(--line); border-radius:999px; background:var(--surface); flex-shrink:0; }
|
|
802
|
+
.vt-btn { display:inline-flex; align-items:center; gap:6px; font:inherit; font-size:12px; padding:5px 12px; border:0; border-radius:999px; background:transparent; color:var(--muted); cursor:pointer; }
|
|
803
|
+
.vt-btn .tab-ico { width:14px; height:14px; }
|
|
804
|
+
.vt-btn:hover { color:var(--ink); }
|
|
805
|
+
.vt-btn.active { background:var(--signal); color:var(--on-accent); font-weight:600; }
|
|
806
|
+
/* the status chips are redundant in Kanban (columns already are the statuses) */
|
|
807
|
+
.main:has(.board.is-kanban) #statusChips { display:none; }
|
|
808
|
+
|
|
809
|
+
.kanban { display:grid; grid-auto-flow:column; grid-auto-columns:minmax(228px,1fr); gap:12px; overflow-x:auto; padding:4px 2px 14px; align-items:start; }
|
|
810
|
+
.kanban-col { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); display:flex; flex-direction:column; min-height:84px; }
|
|
811
|
+
.kanban-col-head { display:flex; align-items:center; gap:8px; padding:11px 13px; border-bottom:1px solid var(--line); border-radius:var(--radius) var(--radius) 0 0; }
|
|
812
|
+
.kanban-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
813
|
+
.kanban-col-title { font-size:11.5px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--ink); }
|
|
814
|
+
.kanban-col-count { margin-left:auto; font-family:var(--mono); font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:0 7px; }
|
|
815
|
+
.kanban-col-body { display:flex; flex-direction:column; gap:8px; padding:10px; }
|
|
816
|
+
.kanban-empty { color:var(--faint); font-size:12px; text-align:center; padding:10px 0; }
|
|
817
|
+
.kanban .task { width:auto; }
|
|
818
|
+
|
|
819
|
+
/* ---- board compactness: expand/collapse-all button + kanban column scroll (v0.13.5) ---- */
|
|
820
|
+
.mini-btn { font:inherit; font-size:11.5px; padding:5px 12px; border:1px solid var(--line); border-radius:999px; background:var(--surface); color:var(--muted); cursor:pointer; flex-shrink:0; }
|
|
821
|
+
.mini-btn:hover { color:var(--ink); border-color:var(--cool); }
|
|
822
|
+
.main:has(.board.is-kanban) #phaseToggleAll { display:none; } /* phases are a List concept */
|
|
823
|
+
.kanban-col-body { max-height:64vh; overflow-y:auto; } /* keep columns compact on a big project */
|