spectoflow 0.13.3 → 0.13.4

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",
3
+ "version": "0.13.4",
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,28 @@ 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(!tasks.length) board.append(emptyState());
326
- else if(!shown) board.append(noMatchState());
330
+ if(!shown) board.append(noMatchState());
327
331
  }
332
+ // Kanban view — one column per status, filtered by the text search (columns already are the statuses).
333
+ function renderKanban(board, tasks){
334
+ const q=filter.q.trim().toLowerCase();
335
+ const match=(t)=> !q || (t.title+' '+t.id).toLowerCase().includes(q);
336
+ const cols=el('div','kanban');
337
+ Object.keys(STATUS).forEach(st=>{
338
+ const colTasks=tasks.filter(t=> t.status===st && match(t));
339
+ const col=el('div','kanban-col');
340
+ const head=el('div','kanban-col-head');
341
+ const dot=el('span','kanban-dot'); dot.style.background='var(--s-'+st+')';
342
+ head.append(dot, el('span','kanban-col-title',STATUS[st]||st), el('span','kanban-col-count',String(colTasks.length)));
343
+ col.append(head);
344
+ const body=el('div','kanban-col-body');
345
+ if(!colTasks.length) body.append(el('div','kanban-empty','—'));
346
+ colTasks.forEach(t=> body.append(renderTask(t)));
347
+ col.append(body); cols.append(col);
348
+ });
349
+ board.append(cols);
350
+ }
351
+ function updateBoardViewToggle(){ $$('#boardViewToggle .vt-btn').forEach(b=> b.classList.toggle('active', b.dataset.view===boardView)); }
328
352
  function li(cls,txt){ const e=el('li',cls); e.textContent=txt; return e; }
329
353
  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
354
  function noMatchState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent='No tasks match this filter.'; return d; }
@@ -927,6 +951,8 @@ applyActiveTab(); // sync to the resolved tab before the first render
927
951
  // filters (status chips + search) — client-side only, does not write anything
928
952
  $$('#statusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ filter.status=b.dataset.status; renderBoard(); }));
929
953
  $('#search').addEventListener('input', e=>{ filter.q=e.target.value; renderBoard(); });
954
+ // board view switch — List (phase-grouped) vs Kanban (columns by status), persisted per viewer
955
+ $$('#boardViewToggle .vt-btn').forEach(b=> b.addEventListener('click', ()=>{ boardView=b.dataset.view; try{ localStorage.setItem('spf-board-view',boardView); }catch{} renderBoard(); }));
930
956
  // backlog: independent filters + sortable column headers — client-side only (reset to page 1 on change)
931
957
  $$('#backlogStatusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ backlogFilter.status=b.dataset.status; backlogPage=1; renderBacklog(); }));
932
958
  $('#backlogSearch').addEventListener('input', e=>{ backlogFilter.q=e.target.value; backlogPage=1; renderBacklog(); });
@@ -61,6 +61,10 @@
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>
64
68
  <input type="search" id="search" class="search" placeholder="Filter tasks…" autocomplete="off" />
65
69
  </div>
66
70
  <div class="board" id="board"></div>
@@ -796,3 +796,22 @@ 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; }