spectoflow 0.23.1 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.23.1",
3
+ "version": "0.23.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",
@@ -135,8 +135,18 @@ function createHandlers(root) {
135
135
  if (p === '/api/workflow/toggle' && req.method === 'POST') {
136
136
  const { name } = await body(req); const wf = path.join(root, '.spectoflow', 'workflow.md');
137
137
  const lines = fs.readFileSync(wf, 'utf8').split('\n');
138
+ // Must strip a trailing {cap:... skill:... policy} annotation (added in D29) BEFORE stripping
139
+ // "(optional)" — same order as store.js's readWorkflow(), which is what the client's own step
140
+ // names (and so `name` here) are actually derived from. Every step in the default workflow.md
141
+ // template carries one of these annotations, so getting this order wrong breaks toggling
142
+ // every single step, not just an edge case.
143
+ const stepName = (rest) => {
144
+ const ann = rest.match(/\{([^}]*)\}\s*$/);
145
+ if (ann) rest = rest.slice(0, ann.index).trim();
146
+ return rest.replace(/\s*\(optional\)\s*$/i, '').trim();
147
+ };
138
148
  for (let i = 0; i < lines.length; i++) { const m = lines[i].match(/^(\s*- \[)( |x|X)(\]\s+)(.*)$/);
139
- if (m && m[4].replace(/\s*\(optional\)\s*$/i, '').trim() === name) lines[i] = m[1] + (m[2].trim() ? ' ' : 'x') + m[3] + m[4]; }
149
+ if (m && stepName(m[4]) === name) lines[i] = m[1] + (m[2].trim() ? ' ' : 'x') + m[3] + m[4]; }
140
150
  fs.writeFileSync(wf, lines.join('\n')); emit({ type: 'change' }); sendJSON(res, 200, { ok: true }); return true;
141
151
  }
142
152
 
@@ -1791,6 +1791,8 @@ const topLangSel=$('#topLang'); if(topLangSel) topLangSel.addEventListener('chan
1791
1791
  const setDesignSel=$('#setDesign'); if(setDesignSel) setDesignSel.addEventListener('change',()=>saveDesign(setDesignSel.value));
1792
1792
  const topAgentSel=$('#topAgent'); if(topAgentSel) topAgentSel.addEventListener('change',(e)=>saveAgent(e.target.value));
1793
1793
  const setAgentSel=$('#setAgent'); if(setAgentSel) setAgentSel.addEventListener('change',(e)=>saveAgent(e.target.value));
1794
+ // hub-mode pill — only ever shown when this tab is actually served via the multi-project hub
1795
+ (function(){ const p=$('#hubPill'); if(p && PROJECT_ID) p.hidden=false; })();
1794
1796
  // theme
1795
1797
  (function(){ const s=localStorage.getItem('spf-theme'); if(s)document.documentElement.setAttribute('data-theme',s);
1796
1798
  $('#themeToggle').addEventListener('click',()=>{ const c=document.documentElement.getAttribute('data-theme'); const n=c==='dark'?'light':'dark'; document.documentElement.setAttribute('data-theme',n); localStorage.setItem('spf-theme',n); }); })();
@@ -14,17 +14,25 @@
14
14
  <img class="brand-logo-img is-dark" src="/logo-white.png" alt="" />
15
15
  <img class="brand-logo-img is-light" src="/logo-dark.png" alt="" />
16
16
  <span class="hub-brand-name">spectoflow</span>
17
+ <span class="hub-brand-tag">Hub</span>
17
18
  </div>
18
19
  <button class="hub-theme-toggle" id="hubThemeToggle" aria-label="Toggle theme" title="Toggle theme">◐</button>
19
20
  </header>
20
21
 
21
22
  <main class="hub-main">
22
23
  <div class="hub-titlebar">
23
- <h1>Your projects</h1>
24
- <button class="hub-add-btn" id="hubAddBtn">+ Add project</button>
24
+ <div class="hub-titlebar-text">
25
+ <h1>Your projects</h1>
26
+ <p class="hub-subtitle" id="hubSubtitle">One dashboard, every project — pick one to open its board.</p>
27
+ </div>
28
+ <button class="hub-add-btn" id="hubAddBtn">
29
+ <svg viewBox="0 0 18 18" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><line x1="9" y1="3.4" x2="9" y2="14.6"/><line x1="3.4" y1="9" x2="14.6" y2="9"/></svg>
30
+ Add project
31
+ </button>
25
32
  </div>
26
33
 
27
34
  <div id="hubEmpty" class="hub-empty" hidden>
35
+ <svg class="hub-empty-icon" viewBox="0 0 18 18" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.2 15.5 6v6L9 15.8 2.5 12V6z"/><path d="M9 6.6v4.8"/><circle cx="9" cy="13.4" r=".9" fill="currentColor" stroke="none"/></svg>
28
36
  <p class="hub-empty-title">No projects yet</p>
29
37
  <p class="hub-empty-sub">Add your first project to get started — point spectoflow at any folder on your computer.</p>
30
38
  <button class="hub-add-btn hub-add-btn-lg" id="hubAddBtnEmpty">+ Add your first project</button>
@@ -28,19 +28,34 @@
28
28
  return Math.floor(h / 24) + 'd ago';
29
29
  }
30
30
 
31
+ const subtitle = document.getElementById('hubSubtitle');
32
+
31
33
  async function loadProjects() {
32
34
  const r = await fetch('/api/hub/projects');
33
35
  const data = await r.json();
34
36
  const rows = data.projects || [];
35
37
  empty.hidden = rows.length > 0;
38
+ if (subtitle) {
39
+ subtitle.textContent = rows.length
40
+ ? `${rows.length} project${rows.length === 1 ? '' : 's'} · click a card to open its board`
41
+ : 'One dashboard, every project — pick one to open its board.';
42
+ }
36
43
  grid.innerHTML = rows.map((p) => {
37
44
  const pct = p.stats && p.stats.total ? Math.round(100 * p.stats.done / p.stats.total) : null;
45
+ const stage = pct === null ? 'new' : pct >= 100 ? 'done' : pct > 0 ? 'active' : 'new';
46
+ const stageLabel = { new: 'New', active: 'In progress', done: 'Done' }[stage];
38
47
  return `<div class="hub-card" data-id="${p.id}">
39
48
  <a class="hub-card-open" href="/p/${p.id}/board">
40
- <div class="hub-card-name">${esc(p.name)}</div>
49
+ <div class="hub-card-top">
50
+ <div class="hub-card-name">${esc(p.name)}</div>
51
+ <span class="hub-card-stage is-${stage}">${stageLabel}</span>
52
+ </div>
41
53
  <div class="hub-card-path">${esc(p.path)}</div>
42
- ${pct !== null ? `<div class="hub-card-progress"><div class="hub-card-progress-fill" style="width:${pct}%"></div></div><div class="hub-card-pct">${pct}% · ${p.stats.done}/${p.stats.total} tasks</div>` : ''}
43
- <div class="hub-card-meta">Opened ${esc(timeAgo(p.lastOpened))}</div>
54
+ <div class="hub-card-progress"><div class="hub-card-progress-fill" style="width:${pct ?? 0}%"></div></div>
55
+ <div class="hub-card-foot">
56
+ <span class="hub-card-pct">${pct !== null ? `${pct}% · ${p.stats.done}/${p.stats.total} tasks` : 'No tasks yet'}</span>
57
+ <span class="hub-card-meta">Opened ${esc(timeAgo(p.lastOpened))}</span>
58
+ </div>
44
59
  </a>
45
60
  <button class="hub-card-remove" data-remove="${p.id}" title="Remove from this list" aria-label="Remove ${esc(p.name)}">&times;</button>
46
61
  </div>`;
@@ -18,7 +18,10 @@
18
18
  <img class="brand-logo-img is-dark" src="/logo-white.png" alt="spectoflow" />
19
19
  <img class="brand-logo-img is-light" src="/logo-dark.png" alt="spectoflow" />
20
20
  </a>
21
- <a class="hub-back-link" href="/" title="Back to your projects" aria-label="Back to your projects">⌂</a>
21
+ <a class="hub-pill" id="hubPill" href="/" title="Back to all your projects" aria-label="Hub mode — back to all your projects" hidden>
22
+ <svg viewBox="0 0 18 18" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.2 15.5 6v6L9 15.8 2.5 12V6z"/></svg>
23
+ <span>Hub</span>
24
+ </a>
22
25
  <div class="brand-text">
23
26
  <div class="brand-line">
24
27
  <span class="brand-name">spectoflow</span>
@@ -969,35 +969,45 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
969
969
  /* Phase progress: cap the list so a big project (many phases) doesn't dominate the overview */
970
970
  .bars-block.scroll-cap { max-height:340px; overflow-y:auto; padding-right:6px; }
971
971
 
972
- /* ---- Hub landing page (multi-project) — reuses the same tokens as the per-project dashboard,
973
- deliberately simpler: no per-design skins, no tabs, just a calm project picker. ---- */
974
- .hub-body { min-height:100%; display:flex; flex-direction:column; }
975
- .hub-header { display:flex; align-items:center; justify-content:space-between; padding:16px 24px; border-bottom:1px solid var(--line); }
976
- .hub-brand { display:flex; align-items:center; gap:9px; font-weight:700; }
977
- .hub-brand .brand-logo-img { width:22px; height:22px; }
978
- .hub-brand-name { font-size:15px; }
972
+ /* ---- Hub landing page (multi-project) — reuses the same tokens as the per-project dashboard, but
973
+ picks its own display/body/mono pairing (self-hosted fonts already shipped for the design skins)
974
+ so it reads as a considered destination in its own right, not an afterthought default page. ---- */
975
+ .hub-body { min-height:100%; display:flex; flex-direction:column; --sans:'IBM Plex Sans', system-ui, sans-serif; --display:'Sora', system-ui, sans-serif; --mono:'JetBrains Mono', ui-monospace, monospace; font-family:var(--sans); }
976
+ .hub-header { display:flex; align-items:center; justify-content:space-between; padding:18px 28px; border-bottom:1px solid var(--line); }
977
+ .hub-brand { display:flex; align-items:center; gap:10px; font-weight:700; }
978
+ .hub-brand .brand-logo-img { width:24px; height:24px; }
979
+ .hub-brand-name { font-family:var(--display); font-size:16px; letter-spacing:-.01em; }
980
+ .hub-brand-tag { font-family:var(--mono); font-size:9.5px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--signal); border:1px solid color-mix(in srgb,var(--signal) 40%,var(--line)); border-radius:999px; padding:2px 7px; }
979
981
  .hub-theme-toggle { width:32px; height:32px; border-radius:8px; border:1px solid var(--line); background:var(--surface); color:var(--ink); cursor:pointer; font-size:14px; }
980
- .hub-main { flex:1; max-width:1080px; width:100%; margin:0 auto; padding:32px 24px 60px; }
981
- .hub-titlebar { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:24px; flex-wrap:wrap; }
982
- .hub-titlebar h1 { font-size:24px; font-weight:700; margin:0; }
983
- .hub-add-btn { font-family:var(--sans); font-size:13.5px; font-weight:600; padding:9px 16px; border-radius:9px; border:1px solid transparent; background:var(--signal); color:var(--on-accent); cursor:pointer; transition:filter .15s; }
984
- .hub-add-btn:hover { filter:brightness(1.08); }
985
- .hub-add-btn-lg { padding:12px 22px; font-size:14.5px; margin-top:14px; }
986
- .hub-empty { text-align:center; padding:60px 20px; border:1px dashed var(--line); border-radius:var(--radius); }
987
- .hub-empty-title { font-size:18px; font-weight:700; margin:0 0 8px; }
982
+ .hub-main { flex:1; max-width:1120px; width:100%; margin:0 auto; padding:40px 28px 64px; }
983
+ .hub-titlebar { display:flex; align-items:flex-end; justify-content:space-between; gap:20px; margin-bottom:30px; flex-wrap:wrap; }
984
+ .hub-titlebar h1 { font-family:var(--display); font-size:28px; font-weight:700; letter-spacing:-.015em; margin:0; }
985
+ .hub-subtitle { color:var(--muted); font-size:13.5px; margin:6px 0 0; }
986
+ .hub-add-btn { display:inline-flex; align-items:center; gap:7px; font-family:var(--sans); font-size:13.5px; font-weight:600; padding:10px 18px; border-radius:var(--radius); border:1px solid transparent; background:var(--signal); color:var(--on-accent); cursor:pointer; transition:filter .15s,transform .15s; }
987
+ .hub-add-btn:hover { filter:brightness(1.08); transform:translateY(-1px); }
988
+ .hub-add-btn-lg { padding:12px 22px; font-size:14.5px; margin-top:16px; }
989
+ .hub-empty { text-align:center; padding:70px 20px; border:1px dashed var(--line); border-radius:var(--radius); color:var(--faint); }
990
+ .hub-empty-icon { margin-bottom:10px; }
991
+ .hub-empty-title { font-family:var(--display); font-size:19px; font-weight:700; color:var(--ink); margin:0 0 8px; }
988
992
  .hub-empty-sub { color:var(--muted); font-size:13.5px; max-width:420px; margin:0 auto; }
989
- .hub-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; }
990
- .hub-card { position:relative; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); transition:border-color .15s,transform .15s; }
991
- .hub-card:hover { border-color:var(--signal); transform:translateY(-1px); }
992
- .hub-card-open { display:block; padding:16px; text-decoration:none; color:inherit; }
993
- .hub-card-name { font-size:15px; font-weight:700; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
994
- .hub-card-path { font-family:var(--mono); font-size:10.5px; color:var(--faint); margin-top:4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
995
- .hub-card-progress { height:5px; border-radius:999px; background:var(--surface-2); margin-top:12px; overflow:hidden; }
996
- .hub-card-progress-fill { height:100%; background:var(--s-done); border-radius:999px; }
997
- .hub-card-pct { font-family:var(--mono); font-size:10.5px; color:var(--muted); margin-top:6px; }
998
- .hub-card-meta { font-size:11px; color:var(--faint); margin-top:10px; }
999
- .hub-card-remove { position:absolute; top:8px; right:8px; width:22px; height:22px; border-radius:999px; border:1px solid var(--line); background:var(--surface-2); color:var(--muted); cursor:pointer; font-size:13px; line-height:1; }
1000
- .hub-card-remove:hover { color:var(--s-blocked); border-color:var(--s-blocked); }
993
+ .hub-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(270px,1fr)); gap:16px; }
994
+ .hub-card { position:relative; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); transition:border-color .15s,transform .15s,box-shadow .15s; }
995
+ .hub-card:hover { border-color:var(--signal); transform:translateY(-2px); box-shadow:var(--shadow); }
996
+ .hub-card-open { display:block; padding:18px; text-decoration:none; color:inherit; }
997
+ .hub-card-top { display:flex; align-items:center; justify-content:space-between; gap:10px; }
998
+ .hub-card-name { font-family:var(--display); font-size:15.5px; font-weight:700; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
999
+ .hub-card-stage { flex-shrink:0; font-family:var(--mono); font-size:9.5px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; padding:2px 7px; border-radius:999px; border:1px solid var(--line); color:var(--muted); }
1000
+ .hub-card-stage.is-active { color:var(--signal); border-color:color-mix(in srgb,var(--signal) 40%,var(--line)); }
1001
+ .hub-card-stage.is-done { color:var(--s-done); border-color:color-mix(in srgb,var(--s-done) 40%,var(--line)); }
1002
+ .hub-card-path { font-family:var(--mono); font-size:10.5px; color:var(--faint); margin-top:5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1003
+ .hub-card-progress { height:5px; border-radius:999px; background:var(--surface-2); margin-top:14px; overflow:hidden; }
1004
+ .hub-card-progress-fill { height:100%; background:linear-gradient(90deg,var(--cool),var(--signal)); border-radius:999px; transition:width .4s ease; }
1005
+ .hub-card-foot { display:flex; align-items:baseline; justify-content:space-between; gap:10px; margin-top:8px; }
1006
+ .hub-card-pct { font-family:var(--mono); font-size:10.5px; color:var(--muted); }
1007
+ .hub-card-meta { font-size:11px; color:var(--faint); flex-shrink:0; }
1008
+ .hub-card-remove { position:absolute; top:8px; right:8px; width:22px; height:22px; border-radius:999px; border:1px solid var(--line); background:var(--surface-2); color:var(--muted); cursor:pointer; font-size:13px; line-height:1; opacity:.6; transition:opacity .15s,color .15s,border-color .15s; }
1009
+ .hub-card:hover .hub-card-remove, .hub-card-remove:focus-visible { opacity:1; }
1010
+ .hub-card-remove:hover { color:var(--s-blocked); border-color:var(--s-blocked); opacity:1; }
1001
1011
 
1002
1012
  .hub-modal { position:fixed; inset:0; background:rgba(0,0,0,.5); display:flex; align-items:center; justify-content:center; z-index:20; padding:20px; }
1003
1013
  .hub-modal[hidden] { display:none; }
@@ -1023,7 +1033,10 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
1023
1033
  .hub-modal-error { color:var(--s-blocked); font-size:12.5px; margin:4px 0 0; }
1024
1034
  .hub-modal-status { color:var(--muted); font-size:12.5px; margin:4px 0 0; }
1025
1035
 
1026
- /* "back to hub" plain link next to the brand logo on the per-project dashboard; a full navigation
1027
- (not SPA), since leaving to the hub means leaving this project's dashboard entirely. */
1028
- .hub-back-link { display:flex; align-items:center; justify-content:center; width:26px; height:26px; border-radius:7px; color:var(--muted); text-decoration:none; font-size:15px; flex-shrink:0; transition:background .15s,color .15s; }
1029
- .hub-back-link:hover { background:var(--surface-2); color:var(--ink); }
1036
+ /* Hub-mode indicator + back-to-hub link, combined into one element: its mere presence tells the
1037
+ viewer this tab is served via the multi-project hub (never shown for the legacy single-project
1038
+ server), and it doubles as an unambiguous, obviously-clickable way back to the project list. */
1039
+ .hub-pill { display:flex; align-items:center; gap:5px; font-family:var(--mono); font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--on-accent); background:var(--signal); border-radius:999px; padding:5px 11px 5px 9px; text-decoration:none; flex-shrink:0; transition:filter .15s,transform .15s,box-shadow .15s; }
1040
+ .hub-pill:hover { filter:brightness(1.1); transform:translateY(-1px); box-shadow:0 2px 10px color-mix(in srgb,var(--signal) 45%,transparent); }
1041
+ .hub-pill[hidden] { display:none; }
1042
+ @media (max-width:640px){ .hub-pill span { display:none; } .hub-pill { padding:6px; } }