spectoflow 0.17.1 → 0.18.0
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/README.md +23 -6
- package/package.json +1 -1
- package/templates/AGENTS.md +29 -3
- package/templates/README.md +9 -1
- package/templates/agents/framework-curator.md +94 -0
- package/templates/agents/qa-engineer.md +8 -5
- package/templates/capabilities.md +10 -1
- package/templates/dashboard/custom/.gitkeep +3 -0
- package/templates/dashboard/public/app.js +343 -128
- package/templates/dashboard/public/designs/console.css +10 -0
- package/templates/dashboard/public/designs/orbit.css +35 -18
- package/templates/dashboard/public/designs/orbit.js +30 -5
- package/templates/dashboard/public/i18n.js +607 -0
- package/templates/dashboard/public/index.html +127 -102
- package/templates/dashboard/public/styles.css +42 -1
- package/templates/dashboard/server.js +8 -1
- package/templates/lib/custom-dashboard.js +76 -0
- package/templates/lib/store.js +29 -5
- package/templates/skills/generate-agent/SKILL.md +135 -0
- package/templates/skills/generate-dashboard/SKILL.md +152 -0
- package/templates/skills/generate-skill/SKILL.md +153 -0
- package/templates/skills/propose-customizations/SKILL.md +72 -0
- package/templates/skills/write-e2e-tests/SKILL.md +72 -17
|
@@ -54,6 +54,15 @@ html[data-design="console"] nav.tabs#tabs {
|
|
|
54
54
|
}
|
|
55
55
|
html[data-design="console"] .topbar { padding-left:calc(var(--cx-rail-w) + 16px); }
|
|
56
56
|
html[data-design="console"] .stage { margin-left:var(--cx-rail-w); }
|
|
57
|
+
/* .app-footer is a sibling of .stage, not inside it — the rail is fixed to the viewport (not just
|
|
58
|
+
.stage), so without this it clips the footer's left edge (the brand name loses its first letters)
|
|
59
|
+
at every scroll position, not just at the top. */
|
|
60
|
+
html[data-design="console"] .app-footer { margin-left:var(--cx-rail-w); }
|
|
61
|
+
/* The header brand mark reads too small at the base 26px against this design's darker, denser
|
|
62
|
+
topbar — give it more presence. The footer's own (deliberately smaller) logo keeps its existing,
|
|
63
|
+
more specific rule and is unaffected. */
|
|
64
|
+
html[data-design="console"] .topbar .brand-logo { height:34px; }
|
|
65
|
+
html[data-design="console"] .topbar .brand-logo-img { height:34px; }
|
|
57
66
|
html[data-design="console"] nav.tabs#tabs .tab {
|
|
58
67
|
position:relative; flex-direction:column; justify-content:center; gap:0;
|
|
59
68
|
width:52px; height:48px; padding:0; border-radius:12px; flex-shrink:0;
|
|
@@ -179,6 +188,7 @@ html[data-design="console"][data-theme="light"] .cx-cmdk { background:rgba(15,23
|
|
|
179
188
|
}
|
|
180
189
|
html[data-design="console"] .topbar { padding-left:16px; }
|
|
181
190
|
html[data-design="console"] .stage { margin-left:0; margin-bottom:64px; }
|
|
191
|
+
html[data-design="console"] .app-footer { margin-left:0; }
|
|
182
192
|
html[data-design="console"] nav.tabs#tabs .tab-label { display:none !important; }
|
|
183
193
|
}
|
|
184
194
|
|
|
@@ -45,6 +45,12 @@ html[data-design="orbit"] .kpi-num { font-weight:700; }
|
|
|
45
45
|
|
|
46
46
|
/* navigation is the radial menu — the tab bar is hidden, content runs full width */
|
|
47
47
|
html[data-design="orbit"] nav.tabs { display:none; }
|
|
48
|
+
|
|
49
|
+
/* The hub (orbit.js) takes over the brand mark's visual role, so the original logo link is hidden;
|
|
50
|
+
the "spectoflow" text becomes the dashboard link in its place (see orbit.js's brand-name handler). */
|
|
51
|
+
html[data-design="orbit"] .brand-logo { display:none; }
|
|
52
|
+
html[data-design="orbit"] .brand-name { cursor:pointer; transition:opacity .15s; }
|
|
53
|
+
html[data-design="orbit"] .brand-name:hover { opacity:.72; text-decoration:underline; text-underline-offset:3px; }
|
|
48
54
|
html[data-design="orbit"] .topbar { grid-template-columns:auto 1fr auto; }
|
|
49
55
|
|
|
50
56
|
/* pills + rounder cards */
|
|
@@ -88,24 +94,30 @@ html[data-design="orbit"] body.booting .card { animation: ob-rise .45s cubic-bez
|
|
|
88
94
|
* THE HUB — round brand button in the header; shows live global progress %, doubles as the
|
|
89
95
|
* radial-menu trigger. Injected by orbit.js as the first child of .brand.
|
|
90
96
|
* ============================================================================================ */
|
|
97
|
+
/* The hub IS the brand mark: the original .brand-logo link is hidden (see below) and this button
|
|
98
|
+
takes over both roles it used to split — visual identity AND, now, the menu trigger. The ring is a
|
|
99
|
+
conic-gradient driven by --ob-pct (set in orbit.js on every progress update): a live "how much of
|
|
100
|
+
the project shipped" reading at a glance, without needing a text percentage on the closed hub. */
|
|
91
101
|
html[data-design="orbit"] .ob-hub {
|
|
92
|
-
position:relative; width:46px; height:46px; border-radius:50%; flex-shrink:0;
|
|
93
|
-
background:var(--signal)
|
|
94
|
-
box-shadow:0
|
|
95
|
-
transition:transform .18s; cursor:pointer; border:0; font:inherit;
|
|
96
|
-
}
|
|
97
|
-
html[data-design="orbit"] .ob-hub:hover { transform:scale(1.06); }
|
|
98
|
-
html[data-design="orbit"] .ob-hub .ob-pct { font-family:var(--display); font-size:12px; font-weight:700; letter-spacing:-.02em; }
|
|
99
|
-
html[data-design="orbit"] .ob-hub::after {
|
|
100
|
-
content:""; position:absolute; inset:-5px; border-radius:50%; border:2px solid var(--signal); opacity:.35;
|
|
101
|
-
animation: ob-breathe 2.6s ease-in-out infinite;
|
|
102
|
+
position:relative; width:46px; height:46px; border-radius:50%; flex-shrink:0; padding:3px;
|
|
103
|
+
background:conic-gradient(var(--signal) calc(var(--ob-pct,0) * 1%), var(--surface-3) 0);
|
|
104
|
+
display:grid; place-items:center; box-shadow:0 6px 16px color-mix(in srgb, var(--signal) 28%, transparent);
|
|
105
|
+
transition:transform .18s,box-shadow .18s; cursor:pointer; border:0; font:inherit;
|
|
102
106
|
}
|
|
103
|
-
|
|
107
|
+
html[data-design="orbit"] .ob-hub::before {
|
|
108
|
+
content:""; position:absolute; inset:3px; border-radius:50%; background:var(--surface); z-index:0;
|
|
109
|
+
}
|
|
110
|
+
/* No `display` here: the theme-toggle rule (.brand-logo-img.is-dark/.is-light in styles.css) must stay
|
|
111
|
+
in control of which of the two cloned <img>s is shown — an explicit display:block on this class
|
|
112
|
+
used to outrank it (higher specificity) and forced BOTH clones visible at once, a ghosted double
|
|
113
|
+
logo most visible in dark mode (the two assets differ more there than in light mode). */
|
|
114
|
+
html[data-design="orbit"] .ob-hub .ob-hub-logo { position:relative; z-index:1; height:21px; width:auto; }
|
|
115
|
+
html[data-design="orbit"] .ob-hub:hover { transform:scale(1.07); box-shadow:0 8px 20px color-mix(in srgb, var(--signal) 38%, transparent); }
|
|
104
116
|
|
|
105
117
|
/* ---- fixed bottom-left FAB on small screens ---- */
|
|
106
118
|
@media (max-width:900px) {
|
|
107
|
-
html[data-design="orbit"] .ob-hub { position:fixed; left:16px; bottom:18px; z-index:42; width:56px; height:56px; }
|
|
108
|
-
html[data-design="orbit"] .ob-hub .ob-
|
|
119
|
+
html[data-design="orbit"] .ob-hub { position:fixed; left:16px; bottom:18px; z-index:42; width:56px; height:56px; padding:4px; }
|
|
120
|
+
html[data-design="orbit"] .ob-hub .ob-hub-logo { height:25px; }
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
/* ============================================================================================
|
|
@@ -149,21 +161,26 @@ html[data-design="orbit"] .ob-chev.ob-left:hover { transform:translateY(-50%) sc
|
|
|
149
161
|
html[data-design="orbit"] .ob-chev.ob-right { top:50%; right:-4px; transform:translateY(-50%); }
|
|
150
162
|
html[data-design="orbit"] .ob-chev.ob-right:hover { transform:translateY(-50%) scale(1.12); }
|
|
151
163
|
|
|
164
|
+
/* The dial's center repeats the brand mark alone — the same logo as the hub, at a size that makes it
|
|
165
|
+
the clear focal point of the whole dial, so identity carries through from closed button to open
|
|
166
|
+
menu. The % is already encoded by the ring around it (.ob-prog); repeating it as text here would
|
|
167
|
+
only compete with the logo instead of framing it. */
|
|
152
168
|
html[data-design="orbit"] .ob-center {
|
|
153
169
|
position:absolute; left:50%; top:50%; width:76px; height:76px; transform:translate(-50%,-50%);
|
|
154
|
-
border-radius:50%; display:grid; place-items:center;
|
|
155
|
-
|
|
170
|
+
border-radius:50%; display:grid; place-items:center;
|
|
171
|
+
background:var(--signal); color:var(--on-accent);
|
|
172
|
+
box-shadow:0 10px 24px color-mix(in srgb, var(--signal) 40%, transparent);
|
|
156
173
|
border:0; cursor:pointer;
|
|
157
174
|
}
|
|
158
|
-
html[data-design="orbit"] .ob-center .ob-
|
|
159
|
-
html[data-design="orbit"] .ob-center .ob-lbl { font:500 9.5px var(--mono); opacity:.85; margin-top:2px; letter-spacing:.06em; text-transform:uppercase; }
|
|
175
|
+
html[data-design="orbit"] .ob-center .ob-center-logo { height:34px; width:auto; }
|
|
160
176
|
html[data-design="orbit"] .ob-center::after {
|
|
161
177
|
content:""; position:absolute; inset:-6px; border-radius:50%; border:2px solid var(--signal); opacity:.35;
|
|
162
178
|
animation: ob-breathe 2.6s ease-in-out infinite;
|
|
163
179
|
}
|
|
180
|
+
@keyframes ob-breathe { 0%,100%{ transform:scale(1); opacity:.35; } 50%{ transform:scale(1.08); opacity:.12; } }
|
|
164
181
|
@media (max-width:900px) {
|
|
165
182
|
html[data-design="orbit"] .ob-center { width:60px; height:60px; }
|
|
166
|
-
html[data-design="orbit"] .ob-center .ob-
|
|
183
|
+
html[data-design="orbit"] .ob-center .ob-center-logo { height:27px; }
|
|
167
184
|
}
|
|
168
185
|
|
|
169
186
|
html[data-design="orbit"] .ob-item {
|
|
@@ -20,7 +20,21 @@
|
|
|
20
20
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="' + d + '"/></svg>';
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/* ----
|
|
23
|
+
/* ---- the brand mark, reused wherever Orbit needs a logo ----
|
|
24
|
+
Clones the header's own theme-aware <img> pair (.brand-logo-img.is-dark/.is-light) rather than
|
|
25
|
+
hardcoding a filename, so a custom-uploaded logo or a future asset change is picked up for free.
|
|
26
|
+
The light/dark swap rule is written purely against the shared class names (no ancestor scoping),
|
|
27
|
+
so it keeps working no matter where these clones end up (hub button, dial center). */
|
|
28
|
+
function logoImgsHtml(extraClass) {
|
|
29
|
+
var imgs = document.querySelectorAll('.brand > .brand-logo .brand-logo-img');
|
|
30
|
+
if (!imgs.length) return '';
|
|
31
|
+
return Array.prototype.map.call(imgs, function (img) {
|
|
32
|
+
var variant = img.className.replace('brand-logo-img', '').trim();
|
|
33
|
+
return '<img class="brand-logo-img' + (variant ? ' ' + variant : '') + (extraClass ? ' ' + extraClass : '') + '" src="' + img.getAttribute('src') + '" alt="" />';
|
|
34
|
+
}).join('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ---- hub button (injected into .brand) — the logo itself, ringed with live progress ---- */
|
|
24
38
|
function buildHub() {
|
|
25
39
|
if (hubEl) return;
|
|
26
40
|
var brand = document.querySelector('.brand');
|
|
@@ -30,21 +44,28 @@
|
|
|
30
44
|
hubEl.className = 'ob-hub';
|
|
31
45
|
hubEl.title = 'Menu (M)';
|
|
32
46
|
hubEl.setAttribute('aria-haspopup', 'dialog');
|
|
33
|
-
hubEl.
|
|
47
|
+
hubEl.setAttribute('aria-label', 'Open menu');
|
|
48
|
+
hubEl.innerHTML = logoImgsHtml('ob-hub-logo');
|
|
34
49
|
brand.insertBefore(hubEl, brand.firstChild);
|
|
35
50
|
hubEl.addEventListener('click', toggleOverlay);
|
|
36
51
|
}
|
|
37
52
|
function removeHub() { if (hubEl) { hubEl.remove(); hubEl = null; } }
|
|
38
53
|
|
|
54
|
+
/* ---- "spectoflow" text next to the hub opens the dashboard (the hub itself opens the menu) ---- */
|
|
55
|
+
function onBrandNameClick() {
|
|
56
|
+
var board = tabs().filter(function (t) { return t.dataset.tab === 'board'; })[0];
|
|
57
|
+
if (board) board.click();
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
/* ---- global progress % — read #globalMeterFill's width, kept live ---- */
|
|
40
61
|
function updatePct() {
|
|
41
62
|
var fill = document.getElementById('globalMeterFill');
|
|
42
63
|
var pct = 0;
|
|
43
64
|
if (fill && fill.style && fill.style.width) pct = parseFloat(fill.style.width) || 0;
|
|
44
65
|
pct = Math.max(0, Math.min(100, Math.round(pct)));
|
|
45
|
-
if (hubEl)
|
|
66
|
+
if (hubEl) hubEl.style.setProperty('--ob-pct', pct); // drives the hub's conic-gradient ring
|
|
46
67
|
if (ovEl) {
|
|
47
|
-
|
|
68
|
+
// the center is the logo alone; progress is read from the ring around it, not repeated as text
|
|
48
69
|
var prog = ovEl.querySelector('.ob-prog');
|
|
49
70
|
if (prog) { var C = 804.2, len = (pct / 100 * C).toFixed(1); prog.setAttribute('stroke-dasharray', len + ' ' + C); }
|
|
50
71
|
}
|
|
@@ -80,7 +101,7 @@
|
|
|
80
101
|
'<button type="button" class="ob-chev ob-down" title="Next (Down)">' + chevSvg('down') + '</button>' +
|
|
81
102
|
'<button type="button" class="ob-chev ob-left" title="Search / Board">' + chevSvg('left') + '</button>' +
|
|
82
103
|
'<button type="button" class="ob-chev ob-right" title="Run">' + chevSvg('right') + '</button>' +
|
|
83
|
-
'<button type="button" class="ob-center" title="Close (Esc)"
|
|
104
|
+
'<button type="button" class="ob-center" title="Close (Esc)" aria-label="Close menu">' + logoImgsHtml('ob-center-logo') + '</button>' +
|
|
84
105
|
items +
|
|
85
106
|
'</div>';
|
|
86
107
|
document.body.appendChild(ovEl);
|
|
@@ -179,12 +200,16 @@
|
|
|
179
200
|
fillObserver = new MutationObserver(updatePct);
|
|
180
201
|
fillObserver.observe(fill, { attributes: true, attributeFilter: ['style'] });
|
|
181
202
|
}
|
|
203
|
+
var bn = document.querySelector('.brand-name');
|
|
204
|
+
if (bn) bn.addEventListener('click', onBrandNameClick);
|
|
182
205
|
}
|
|
183
206
|
function leave() {
|
|
184
207
|
closeOverlay();
|
|
185
208
|
removeHub();
|
|
186
209
|
if (pctInterval) { clearInterval(pctInterval); pctInterval = null; }
|
|
187
210
|
if (fillObserver) { fillObserver.disconnect(); fillObserver = null; }
|
|
211
|
+
var bn = document.querySelector('.brand-name');
|
|
212
|
+
if (bn) bn.removeEventListener('click', onBrandNameClick);
|
|
188
213
|
}
|
|
189
214
|
function sync() {
|
|
190
215
|
var on = isOrbit();
|