spectoflow 0.22.0 → 0.22.2
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/public/app.js +28 -2
- package/templates/dashboard/public/designs/orbit.css +5 -3
- package/templates/dashboard/public/designs/orbit.js +21 -1
- package/templates/dashboard/public/i18n.js +6 -6
- package/templates/dashboard/public/index.html +3 -0
- package/templates/dashboard/public/styles.css +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.2",
|
|
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",
|
|
@@ -7,6 +7,7 @@ function updateStatusLabels(){ for(const k of Object.keys(STATUS)) STATUS[k]=t('
|
|
|
7
7
|
let P = null, openTaskId = null;
|
|
8
8
|
let filter = { status: 'all', q: '' }; // board filter state — client-side only, read-only
|
|
9
9
|
let boardView = (()=>{ try{ return localStorage.getItem('spf-board-view')||'list'; }catch{ return 'list'; } })(); // 'list' | 'kanban'
|
|
10
|
+
let sideHidden = (()=>{ try{ return localStorage.getItem('spf-side-hidden')==='1'; }catch{ return false; } })(); // right sidebar (Journal/Specs/Running) — mainly to give Kanban's own-width columns more room
|
|
10
11
|
let backlogFilter = { status: 'open', q: '' }; // backlog defaults to open (not-done) tasks
|
|
11
12
|
let backlogSort = { col: 'id', dir: 'asc' }; // backlog sort state — client-side only
|
|
12
13
|
let backlogPage = 1; const BACKLOG_PAGE = 25; // backlog pagination — client-side only
|
|
@@ -164,7 +165,7 @@ function render(){
|
|
|
164
165
|
if(meter) meter.title=`${t('kpi.globalProgress')}: ${s.pct}% (${s.done}/${s.total} ${t('kpi.tasksLabel')})`;
|
|
165
166
|
renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
|
|
166
167
|
renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
|
|
167
|
-
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles();
|
|
168
|
+
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles(); applySideHidden();
|
|
168
169
|
renderCustomDashboards(); // adds/removes nav tabs + panels before applyActiveTab() below reads them
|
|
169
170
|
applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
|
|
170
171
|
applyI18nStatic(); // re-translate the static markup (nav, headers, placeholders…) for this tick's language
|
|
@@ -1540,7 +1541,22 @@ openTaskId = taskFromPath(); // deep-link straight to a task drawer
|
|
|
1540
1541
|
function applyActiveTab(){
|
|
1541
1542
|
$$('#tabs .tab').forEach(t=> t.classList.toggle('is-active', t.dataset.tab===activeTab));
|
|
1542
1543
|
$$('.panel').forEach(p=> p.classList.toggle('is-active', p.dataset.panel===activeTab));
|
|
1543
|
-
|
|
1544
|
+
fitTabs();
|
|
1545
|
+
}
|
|
1546
|
+
// Horizontal top-nav designs (Console's vertical rail and Orbit's hidden/radial nav manage their own
|
|
1547
|
+
// layout independently) can run out of room as tabs are added over time — a single px breakpoint
|
|
1548
|
+
// tuned for whatever tab count existed back then goes stale the moment a tab is added or removed
|
|
1549
|
+
// (exactly what happened when the Files tab pushed the row from 10 to 11 items: some tabs, including
|
|
1550
|
+
// Personalize, silently overflowed with no visible way to reach them). Measure the row's REAL
|
|
1551
|
+
// overflow instead of guessing from viewport width alone, so it's correct at any tab count.
|
|
1552
|
+
function fitTabs(){
|
|
1553
|
+
const tabsEl=$('#tabs'); if(!tabsEl) return;
|
|
1554
|
+
const design=document.documentElement.getAttribute('data-design');
|
|
1555
|
+
if(design==='console'||design==='orbit'){ tabsEl.classList.remove('tabs-compact'); return; }
|
|
1556
|
+
tabsEl.classList.remove('tabs-compact'); // measure at full (labelled) size first
|
|
1557
|
+
if(tabsEl.scrollWidth>tabsEl.clientWidth+1) tabsEl.classList.add('tabs-compact');
|
|
1558
|
+
}
|
|
1559
|
+
window.addEventListener('resize', (()=>{ let t=null; return ()=>{ clearTimeout(t); t=setTimeout(fitTabs,120); }; })());
|
|
1544
1560
|
$$('#tabs .tab').forEach(tab=> tab.addEventListener('click',()=> navigateTab(tab.dataset.tab)));
|
|
1545
1561
|
// mobile hamburger — toggles the tab dropdown (body.nav-open); closes on tab pick / outside / Esc
|
|
1546
1562
|
const navToggle=$('#navToggle');
|
|
@@ -1568,6 +1584,16 @@ $$('#statusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ filter.s
|
|
|
1568
1584
|
$('#search').addEventListener('input', e=>{ filter.q=e.target.value; renderBoard(); });
|
|
1569
1585
|
// board view switch — List (phase-grouped) vs Kanban (columns by status), persisted per viewer
|
|
1570
1586
|
$$('#boardViewToggle .vt-btn').forEach(b=> b.addEventListener('click', ()=>{ boardView=b.dataset.view; try{ localStorage.setItem('spf-board-view',boardView); }catch{} renderBoard(); }));
|
|
1587
|
+
// right sidebar hide/show — Kanban's own-width columns need the room, and the toggle stays
|
|
1588
|
+
// persisted per viewer like every other layout preference here
|
|
1589
|
+
function applySideHidden(){
|
|
1590
|
+
const panel=$('.panel[data-panel="board"]'); const btn=$('#sideToggle');
|
|
1591
|
+
if(panel) panel.classList.toggle('side-hidden', sideHidden);
|
|
1592
|
+
if(btn){ btn.setAttribute('aria-pressed', String(sideHidden)); btn.title=t(sideHidden?'board.showSidebar':'board.hideSidebar'); }
|
|
1593
|
+
}
|
|
1594
|
+
const sideToggleBtn=$('#sideToggle');
|
|
1595
|
+
if(sideToggleBtn) sideToggleBtn.addEventListener('click', ()=>{ sideHidden=!sideHidden; try{ localStorage.setItem('spf-side-hidden', sideHidden?'1':'0'); }catch{} applySideHidden(); });
|
|
1596
|
+
applySideHidden();
|
|
1571
1597
|
// expand / collapse all phases (List view) — keeps a big board compact by default
|
|
1572
1598
|
const phaseToggleAllBtn=$('#phaseToggleAll');
|
|
1573
1599
|
if(phaseToggleAllBtn) phaseToggleAllBtn.addEventListener('click', ()=>{
|
|
@@ -199,12 +199,14 @@ html[data-design="orbit"] .ob-item .ob-bd {
|
|
|
199
199
|
position:absolute; top:2px; right:2px; min-width:15px; height:15px; padding:0 4px; border-radius:8px;
|
|
200
200
|
background:var(--s-blocked); color:#fff; font:700 9px/15px var(--mono); text-align:center;
|
|
201
201
|
}
|
|
202
|
-
|
|
202
|
+
/* --ob-r is set inline on .ob-dial (computed per open, from the actual tab count) and inherits down
|
|
203
|
+
to every .ob-item — the 98px/72px fallbacks only cover a stylesheet loaded before orbit.js runs. */
|
|
204
|
+
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(var(--ob-r,98px)) rotate(calc(-1*var(--a))) scale(1); } }
|
|
203
205
|
@media (max-width:900px) {
|
|
204
206
|
html[data-design="orbit"] .ob-item { width:46px; height:46px; margin:-23px 0 0 -23px; }
|
|
205
207
|
html[data-design="orbit"] .ob-item .ob-lb { display:none; }
|
|
206
208
|
html[data-design="orbit"] .ob-item .ob-ico { width:16px; height:16px; }
|
|
207
|
-
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(72px) rotate(calc(-1*var(--a))) scale(1); } }
|
|
209
|
+
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(var(--ob-r,72px)) rotate(calc(-1*var(--a))) scale(1); } }
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -220,7 +222,7 @@ html[data-design="orbit"] .ob-item .ob-bd {
|
|
|
220
222
|
html[data-design="orbit"] body.booting .card {
|
|
221
223
|
animation:none !important;
|
|
222
224
|
}
|
|
223
|
-
html[data-design="orbit"] .ob-item { opacity:1; transform:rotate(var(--a)) translate(98px) rotate(calc(-1*var(--a))); }
|
|
225
|
+
html[data-design="orbit"] .ob-item { opacity:1; transform:rotate(var(--a)) translate(var(--ob-r,98px)) rotate(calc(-1*var(--a))); }
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
/* chat FAB — amber brand accent (the teal is the dial's); readable on both sets */
|
|
@@ -71,11 +71,31 @@
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Items sit on a ring of radius R; at n evenly-spaced items the straight-line gap between two
|
|
75
|
+
// adjacent item centers is a chord of length 2R·sin(π/n). Below ~9 items the tuned default radius
|
|
76
|
+
// already clears the item's own diameter with room to spare; past that, more tabs (built-in ones
|
|
77
|
+
// like this session's new Files tab, or future custom dashboards) would pack the same fixed ring
|
|
78
|
+
// tighter and tighter until items visibly overlap. Grow the radius instead — solved for the chord
|
|
79
|
+
// to equal the item diameter plus a minimum gap — so the ring always has room for however many
|
|
80
|
+
// tabs exist, never a fixed count tuned for whatever the tab bar happened to hold at the time.
|
|
81
|
+
function ringRadius(n, itemDiameter, minGap) {
|
|
82
|
+
if (n <= 1) return itemDiameter; // a lone item has no neighbor to clear
|
|
83
|
+
var needed = (itemDiameter + minGap) / (2 * Math.sin(Math.PI / n));
|
|
84
|
+
return Math.max(itemDiameter, needed);
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
/* ---- radial overlay ---- */
|
|
75
88
|
function buildOverlay() {
|
|
76
89
|
if (ovEl) return;
|
|
77
90
|
var list = tabs(), n = list.length || 1;
|
|
78
91
|
var idx = activeIndex(list);
|
|
92
|
+
var isCompact = window.matchMedia('(max-width:900px)').matches;
|
|
93
|
+
var itemDiameter = isCompact ? 46 : 58;
|
|
94
|
+
var baseRadius = isCompact ? 72 : 98; // the tuned default for <=9 items — never shrink below it
|
|
95
|
+
// the gap accounts for the label text under each icon, which can run wider than the icon
|
|
96
|
+
// circle itself ("Agents & Skills" is the long pole) — a gap sized only to the circle left
|
|
97
|
+
// adjacent labels touching even though the circles themselves were visibly clear.
|
|
98
|
+
var radius = Math.max(baseRadius, ringRadius(n, itemDiameter, 22));
|
|
79
99
|
var items = list.map(function (tab, i) {
|
|
80
100
|
var angle = -90 + i * (360 / n);
|
|
81
101
|
var ico = tab.querySelector('.tab-ico');
|
|
@@ -91,7 +111,7 @@
|
|
|
91
111
|
ovEl = document.createElement('div');
|
|
92
112
|
ovEl.className = 'ob-ov';
|
|
93
113
|
ovEl.innerHTML =
|
|
94
|
-
'<div class="ob-dial" role="dialog" aria-label="spectoflow navigation">' +
|
|
114
|
+
'<div class="ob-dial" role="dialog" aria-label="spectoflow navigation" style="--ob-r:' + radius + 'px">' +
|
|
95
115
|
'<svg class="ob-ring" viewBox="0 0 284 284" aria-hidden="true">' +
|
|
96
116
|
'<circle class="ob-track" cx="142" cy="142" r="128" stroke-dasharray="176 25" stroke-dashoffset="-12"></circle>' +
|
|
97
117
|
'<circle class="ob-prog" cx="142" cy="142" r="128" stroke-dasharray="0 804.2"></circle>' +
|
|
@@ -34,7 +34,7 @@ en: {
|
|
|
34
34
|
'topbar.lang.title':'Output language — change from here or in Personalize',
|
|
35
35
|
'topbar.agent.title':'Active agent — change from here or in Personalize',
|
|
36
36
|
'topbar.agent.none':'No agent found',
|
|
37
|
-
'board.filterPlaceholder':'Filter tasks…','board.expandAll':'Expand all','board.collapseAll':'Collapse all',
|
|
37
|
+
'board.filterPlaceholder':'Filter tasks…','board.expandAll':'Expand all','board.collapseAll':'Collapse all','board.hideSidebar':'Hide sidebar','board.showSidebar':'Show sidebar',
|
|
38
38
|
'board.expandAllTitle':'Expand or collapse all phases','board.viewList':'List','board.viewKanban':'Kanban',
|
|
39
39
|
'board.viewList.title':'Grouped by phase','board.viewKanban.title':'Columns by status',
|
|
40
40
|
'board.noTasksMatch':'No tasks match this filter.',
|
|
@@ -146,7 +146,7 @@ fr: {
|
|
|
146
146
|
'topbar.lang.title':'Langue de sortie — modifiable ici ou dans Personnalisation',
|
|
147
147
|
'topbar.agent.title':'Agent actif — modifiable ici ou dans Personnalisation',
|
|
148
148
|
'topbar.agent.none':'Aucun agent trouvé',
|
|
149
|
-
'board.filterPlaceholder':'Filtrer les tâches…','board.expandAll':'Tout déplier','board.collapseAll':'Tout replier',
|
|
149
|
+
'board.filterPlaceholder':'Filtrer les tâches…','board.expandAll':'Tout déplier','board.collapseAll':'Tout replier','board.hideSidebar':'Masquer la barre latérale','board.showSidebar':'Afficher la barre latérale',
|
|
150
150
|
'board.expandAllTitle':'Déplier ou replier toutes les phases','board.viewList':'Liste','board.viewKanban':'Kanban',
|
|
151
151
|
'board.viewList.title':'Regroupé par phase','board.viewKanban.title':'Colonnes par statut',
|
|
152
152
|
'board.noTasksMatch':'Aucune tâche ne correspond à ce filtre.',
|
|
@@ -258,7 +258,7 @@ es: {
|
|
|
258
258
|
'topbar.lang.title':'Idioma de salida — se cambia aquí o en Personalizar',
|
|
259
259
|
'topbar.agent.title':'Agente activo — se cambia aquí o en Personalizar',
|
|
260
260
|
'topbar.agent.none':'No se encontró ningún agente',
|
|
261
|
-
'board.filterPlaceholder':'Filtrar tareas…','board.expandAll':'Expandir todo','board.collapseAll':'Colapsar todo',
|
|
261
|
+
'board.filterPlaceholder':'Filtrar tareas…','board.expandAll':'Expandir todo','board.collapseAll':'Colapsar todo','board.hideSidebar':'Ocultar barra lateral','board.showSidebar':'Mostrar barra lateral',
|
|
262
262
|
'board.expandAllTitle':'Expandir o colapsar todas las fases','board.viewList':'Lista','board.viewKanban':'Kanban',
|
|
263
263
|
'board.viewList.title':'Agrupado por fase','board.viewKanban.title':'Columnas por estado',
|
|
264
264
|
'board.noTasksMatch':'Ninguna tarea coincide con este filtro.',
|
|
@@ -370,7 +370,7 @@ de: {
|
|
|
370
370
|
'topbar.lang.title':'Ausgabesprache — hier oder in Personalisieren änderbar',
|
|
371
371
|
'topbar.agent.title':'Aktiver Agent — hier oder in Personalisieren änderbar',
|
|
372
372
|
'topbar.agent.none':'Kein Agent gefunden',
|
|
373
|
-
'board.filterPlaceholder':'Aufgaben filtern…','board.expandAll':'Alle ausklappen','board.collapseAll':'Alle einklappen',
|
|
373
|
+
'board.filterPlaceholder':'Aufgaben filtern…','board.expandAll':'Alle ausklappen','board.collapseAll':'Alle einklappen','board.hideSidebar':'Seitenleiste ausblenden','board.showSidebar':'Seitenleiste einblenden',
|
|
374
374
|
'board.expandAllTitle':'Alle Phasen ein- oder ausklappen','board.viewList':'Liste','board.viewKanban':'Kanban',
|
|
375
375
|
'board.viewList.title':'Nach Phase gruppiert','board.viewKanban.title':'Spalten nach Status',
|
|
376
376
|
'board.noTasksMatch':'Keine Aufgabe entspricht diesem Filter.',
|
|
@@ -482,7 +482,7 @@ pt: {
|
|
|
482
482
|
'topbar.lang.title':'Idioma de saída — altere aqui ou em Personalizar',
|
|
483
483
|
'topbar.agent.title':'Agente ativo — altere aqui ou em Personalizar',
|
|
484
484
|
'topbar.agent.none':'Nenhum agente encontrado',
|
|
485
|
-
'board.filterPlaceholder':'Filtrar tarefas…','board.expandAll':'Expandir tudo','board.collapseAll':'Recolher tudo',
|
|
485
|
+
'board.filterPlaceholder':'Filtrar tarefas…','board.expandAll':'Expandir tudo','board.collapseAll':'Recolher tudo','board.hideSidebar':'Ocultar barra lateral','board.showSidebar':'Mostrar barra lateral',
|
|
486
486
|
'board.expandAllTitle':'Expandir ou recolher todas as fases','board.viewList':'Lista','board.viewKanban':'Kanban',
|
|
487
487
|
'board.viewList.title':'Agrupado por fase','board.viewKanban.title':'Colunas por estado',
|
|
488
488
|
'board.noTasksMatch':'Nenhuma tarefa corresponde a este filtro.',
|
|
@@ -594,7 +594,7 @@ it: {
|
|
|
594
594
|
'topbar.lang.title':'Lingua di output — modificabile qui o in Personalizza',
|
|
595
595
|
'topbar.agent.title':'Agente attivo — modificabile qui o in Personalizza',
|
|
596
596
|
'topbar.agent.none':'Nessun agente trovato',
|
|
597
|
-
'board.filterPlaceholder':'Filtra attività…','board.expandAll':'Espandi tutto','board.collapseAll':'Comprimi tutto',
|
|
597
|
+
'board.filterPlaceholder':'Filtra attività…','board.expandAll':'Espandi tutto','board.collapseAll':'Comprimi tutto','board.hideSidebar':'Nascondi barra laterale','board.showSidebar':'Mostra barra laterale',
|
|
598
598
|
'board.expandAllTitle':'Espandi o comprimi tutte le fasi','board.viewList':'Elenco','board.viewKanban':'Kanban',
|
|
599
599
|
'board.viewList.title':'Raggruppato per fase','board.viewKanban.title':'Colonne per stato',
|
|
600
600
|
'board.noTasksMatch':'Nessuna attività corrisponde a questo filtro.',
|
|
@@ -89,6 +89,9 @@
|
|
|
89
89
|
</div>
|
|
90
90
|
<button class="mini-btn" id="phaseToggleAll" data-i18n-title="board.expandAllTitle" data-i18n="board.expandAll" title="Expand or collapse all phases">Expand all</button>
|
|
91
91
|
<input type="search" id="search" class="search" data-i18n-ph="board.filterPlaceholder" placeholder="Filter tasks…" autocomplete="off" />
|
|
92
|
+
<button class="mini-btn" id="sideToggle" type="button" data-i18n-title="board.hideSidebar" title="Hide sidebar">
|
|
93
|
+
<svg viewBox="0 0 18 18" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="2" y="3" width="14" height="12" rx="2"/><line x1="11.5" y1="3" x2="11.5" y2="15"/></svg>
|
|
94
|
+
</button>
|
|
92
95
|
</div>
|
|
93
96
|
<div class="board" id="board"></div>
|
|
94
97
|
</div>
|
|
@@ -73,9 +73,14 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
73
73
|
.panel-sub { color:var(--muted); font-size:12.5px; margin:0 0 16px; }
|
|
74
74
|
.count { font-family:var(--mono); font-size:10px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
|
|
75
75
|
|
|
76
|
-
/* main column + right sidebar
|
|
76
|
+
/* main column + right sidebar — hideable (mainly to give Kanban's own-width columns more room
|
|
77
|
+
instead of forcing horizontal scroll on top of the sidebar already eating 300px) */
|
|
77
78
|
.main { min-width:0; display:flex; flex-direction:column; }
|
|
78
79
|
.side { border-left:1px solid var(--line); padding:18px 16px; display:flex; flex-direction:column; gap:22px; }
|
|
80
|
+
.panel[data-panel="board"].side-hidden { grid-template-columns:1fr; }
|
|
81
|
+
.panel[data-panel="board"].side-hidden .side { display:none; }
|
|
82
|
+
#sideToggle { display:inline-flex; align-items:center; justify-content:center; padding:5px 9px; }
|
|
83
|
+
#sideToggle[aria-pressed="true"] { color:var(--signal); border-color:var(--signal); }
|
|
79
84
|
.rail-title { font-size:11px; text-transform:uppercase; letter-spacing:.1em; color:var(--faint); margin:0 0 8px; font-weight:700; display:flex; gap:8px; align-items:center; }
|
|
80
85
|
.flatlist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:5px; font-size:12.5px; }
|
|
81
86
|
.flatlist li { background:var(--surface); border:1px solid var(--line); border-radius:7px; padding:6px 9px; color:var(--muted); }
|
|
@@ -389,9 +394,13 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
389
394
|
.btn.primary { background:var(--signal); color:var(--on-accent); border-color:transparent; font-weight:600; }
|
|
390
395
|
.empty { color:var(--faint); font-style:italic; font-size:12.5px; }
|
|
391
396
|
:focus-visible { outline:2px solid var(--cool); outline-offset:2px; }
|
|
392
|
-
/*
|
|
393
|
-
|
|
394
|
-
|
|
397
|
+
/* A fixed px breakpoint here would go stale the moment a tab is added or removed (exactly what
|
|
398
|
+
happened when the Files tab pushed the row from 10 to 11 items) — fitTabs() in app.js measures
|
|
399
|
+
the tab row's REAL overflow instead and adds this class, so it's correct at any tab count and any
|
|
400
|
+
viewport width, not just the count it happened to be tuned against once. Narrow-viewport padding
|
|
401
|
+
still tightens up below ~1180px regardless — smaller tabs simply need less to overflow. */
|
|
402
|
+
@media (max-width:1180px){ .tabs .tab{ padding:7px 8px; } }
|
|
403
|
+
.tabs-compact .tab-label { display:none; }
|
|
395
404
|
@media (max-width:900px){
|
|
396
405
|
.panel[data-panel="board"].is-active { grid-template-columns:1fr; }
|
|
397
406
|
.side { border-left:0; border-top:1px solid var(--line); flex-direction:row; flex-wrap:wrap; gap:16px 28px; }
|