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 +1 -1
- package/templates/dashboard/handlers.js +11 -1
- package/templates/dashboard/public/app.js +2 -0
- package/templates/dashboard/public/hub.html +10 -2
- package/templates/dashboard/public/hub.js +18 -3
- package/templates/dashboard/public/index.html +4 -1
- package/templates/dashboard/public/styles.css +44 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.23.
|
|
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]
|
|
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
|
-
<
|
|
24
|
-
|
|
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-
|
|
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
|
-
|
|
43
|
-
<div class="hub-card-
|
|
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)}">×</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-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
.hub-
|
|
976
|
-
.hub-
|
|
977
|
-
.hub-brand
|
|
978
|
-
.hub-brand-
|
|
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:
|
|
981
|
-
.hub-titlebar { display:flex; align-items:
|
|
982
|
-
.hub-titlebar h1 { font-size:
|
|
983
|
-
.hub-
|
|
984
|
-
.hub-add-btn
|
|
985
|
-
.hub-add-btn
|
|
986
|
-
.hub-
|
|
987
|
-
.hub-empty
|
|
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(
|
|
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(-
|
|
992
|
-
.hub-card-open { display:block; padding:
|
|
993
|
-
.hub-card-
|
|
994
|
-
.hub-card-
|
|
995
|
-
.hub-card-
|
|
996
|
-
.hub-card-
|
|
997
|
-
.hub-card-
|
|
998
|
-
.hub-card-
|
|
999
|
-
.hub-card-
|
|
1000
|
-
.hub-card-
|
|
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
|
-
/*
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
.hub-
|
|
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; } }
|