spectoflow 0.16.4 → 0.17.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/package.json +1 -1
- package/templates/config.json +1 -0
- package/templates/dashboard/public/app.js +1 -1
- package/templates/dashboard/public/designs/console.css +193 -0
- package/templates/dashboard/public/designs/console.js +144 -0
- package/templates/dashboard/public/designs/orbit.css +207 -0
- package/templates/dashboard/public/designs/orbit.js +197 -0
- package/templates/dashboard/public/designs.js +2 -0
- package/templates/dashboard/public/index.html +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
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",
|
package/templates/config.json
CHANGED
|
@@ -672,7 +672,7 @@ function setLangSelect(lang){
|
|
|
672
672
|
sel.value=lang;
|
|
673
673
|
}
|
|
674
674
|
// ---- design skins (data-design) — switchable, persisted per viewer + as the project default ----
|
|
675
|
-
function currentDesign(){ return document.documentElement.getAttribute('data-design')||'
|
|
675
|
+
function currentDesign(){ return document.documentElement.getAttribute('data-design')||'console'; }
|
|
676
676
|
function applyDesign(id){ document.documentElement.setAttribute('data-design',id); try{ localStorage.setItem('spf-design',id); }catch{} }
|
|
677
677
|
async function saveDesign(id){ applyDesign(id); if(P) render(); /* re-read token colours into the SVG charts */ flash(); try{ await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({design:id})}); }catch{} }
|
|
678
678
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/* Spectral Console — dark-first bento control room. Amber accent (--signal), cyan flow (--cool).
|
|
2
|
+
Scoped entirely under html[data-design="console"] so other designs are untouched.
|
|
3
|
+
Fonts (self-hosted, declared once in ../styles.css @font-face rules — reused here by family name):
|
|
4
|
+
Sora (display), IBM Plex Sans (sans), JetBrains Mono (mono). No external font requests. */
|
|
5
|
+
|
|
6
|
+
/* ============ TOKENS ============ */
|
|
7
|
+
html[data-design="console"] {
|
|
8
|
+
--bg:#0b1220; --surface:#111a2b; --surface-2:#172236; --surface-3:#1d2a42;
|
|
9
|
+
--line:#223049; --line-2:#2c3c5a;
|
|
10
|
+
--ink:#e8eef8; --muted:#8a9bb5; --faint:#5f6f8a;
|
|
11
|
+
--signal:#e6a54b; --cool:#38c5d9; --on-accent:#1a1206;
|
|
12
|
+
--s-todo:#6b7a90; --s-in_progress:#4f8cff; --s-to_validate:#ff9b3d; --s-to_analyze:#ff5fd2;
|
|
13
|
+
--s-done:#3fbf7f; --s-blocked:#ff5c5c;
|
|
14
|
+
--radius:14px; --shadow:0 12px 40px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.04) inset;
|
|
15
|
+
--display:'Sora',var(--sans); --sans:'IBM Plex Sans',system-ui,sans-serif; --mono:'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace;
|
|
16
|
+
--cx-glow-signal:0 0 0 1px rgba(230,165,75,.35), 0 0 32px rgba(230,165,75,.18);
|
|
17
|
+
--cx-glow-cool:0 0 0 1px rgba(56,197,217,.35), 0 0 32px rgba(56,197,217,.16);
|
|
18
|
+
--cx-grid:rgba(255,255,255,.035);
|
|
19
|
+
--cx-rail-w:72px;
|
|
20
|
+
}
|
|
21
|
+
html[data-design="console"][data-theme="light"] {
|
|
22
|
+
--bg:#f3f5f9; --surface:#ffffff; --surface-2:#eef2f8; --surface-3:#e4eaf4;
|
|
23
|
+
--line:#dbe2ee; --line-2:#c9d3e3;
|
|
24
|
+
--ink:#0f1728; --muted:#55657f; --faint:#8593ab;
|
|
25
|
+
--signal:#b8781f; --cool:#0e8fa3; --on-accent:#fff8ec;
|
|
26
|
+
--s-todo:#7c8aa3; --s-in_progress:#2f6fe0; --s-to_validate:#d97a17; --s-to_analyze:#c93fae;
|
|
27
|
+
--s-done:#1f9c5c; --s-blocked:#d93a3a;
|
|
28
|
+
--shadow:0 10px 30px rgba(20,30,60,.10), 0 1px 0 rgba(255,255,255,.6) inset;
|
|
29
|
+
--cx-glow-signal:0 0 0 1px rgba(184,120,31,.35), 0 0 28px rgba(184,120,31,.14);
|
|
30
|
+
--cx-glow-cool:0 0 0 1px rgba(14,143,163,.35), 0 0 28px rgba(14,143,163,.14);
|
|
31
|
+
--cx-grid:rgba(15,23,40,.06);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ============ AMBIENT BACKDROP ============ */
|
|
35
|
+
html[data-design="console"] body { background:var(--bg); position:relative; }
|
|
36
|
+
html[data-design="console"] body::before {
|
|
37
|
+
content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
|
|
38
|
+
background:
|
|
39
|
+
radial-gradient(900px 500px at 10% -10%, color-mix(in srgb,var(--signal) 14%,transparent), transparent 60%),
|
|
40
|
+
radial-gradient(800px 500px at 100% 0%, color-mix(in srgb,var(--cool) 14%,transparent), transparent 60%),
|
|
41
|
+
linear-gradient(var(--cx-grid) 1px, transparent 1px), linear-gradient(90deg, var(--cx-grid) 1px, transparent 1px);
|
|
42
|
+
background-size:auto,auto,32px 32px,32px 32px;
|
|
43
|
+
}
|
|
44
|
+
html[data-design="console"] .topbar,
|
|
45
|
+
html[data-design="console"] .stage { position:relative; z-index:1; }
|
|
46
|
+
|
|
47
|
+
/* ============ LEFT ICON RAIL ============ */
|
|
48
|
+
html[data-design="console"] nav.tabs#tabs {
|
|
49
|
+
position:fixed; left:0; top:0; bottom:0; z-index:30; width:var(--cx-rail-w);
|
|
50
|
+
display:flex; flex-direction:column; align-items:center; gap:6px;
|
|
51
|
+
padding:16px 10px; overflow-y:auto; overflow-x:hidden; max-width:none;
|
|
52
|
+
background:color-mix(in srgb,var(--surface) 84%,transparent); backdrop-filter:blur(10px);
|
|
53
|
+
border-right:1px solid var(--line); border-bottom:0;
|
|
54
|
+
}
|
|
55
|
+
html[data-design="console"] .topbar { padding-left:calc(var(--cx-rail-w) + 16px); }
|
|
56
|
+
html[data-design="console"] .stage { margin-left:var(--cx-rail-w); }
|
|
57
|
+
html[data-design="console"] nav.tabs#tabs .tab {
|
|
58
|
+
position:relative; flex-direction:column; justify-content:center; gap:0;
|
|
59
|
+
width:52px; height:48px; padding:0; border-radius:12px; flex-shrink:0;
|
|
60
|
+
transition:background .18s,color .18s,transform .18s;
|
|
61
|
+
}
|
|
62
|
+
html[data-design="console"] nav.tabs#tabs .tab:hover { transform:translateY(-1px); }
|
|
63
|
+
html[data-design="console"] nav.tabs#tabs .tab.is-active {
|
|
64
|
+
color:var(--signal); background:var(--surface-2);
|
|
65
|
+
box-shadow:inset 3px 0 0 var(--signal);
|
|
66
|
+
}
|
|
67
|
+
html[data-design="console"] nav.tabs#tabs .tab-ico { width:19px; height:19px; }
|
|
68
|
+
html[data-design="console"] nav.tabs#tabs .tab-label {
|
|
69
|
+
position:absolute; left:60px; top:50%; transform:translateY(-50%) translateX(-4px);
|
|
70
|
+
background:var(--surface-3); color:var(--ink); padding:5px 9px; border-radius:7px;
|
|
71
|
+
font-size:12px; font-weight:500; white-space:nowrap; border:1px solid var(--line-2);
|
|
72
|
+
opacity:0; pointer-events:none; transition:opacity .15s,transform .15s; z-index:5;
|
|
73
|
+
display:block !important; box-shadow:var(--shadow);
|
|
74
|
+
}
|
|
75
|
+
html[data-design="console"] nav.tabs#tabs .tab:hover .tab-label,
|
|
76
|
+
html[data-design="console"] nav.tabs#tabs .tab:focus-visible .tab-label {
|
|
77
|
+
opacity:1; transform:translateY(-50%) translateX(0);
|
|
78
|
+
}
|
|
79
|
+
html[data-design="console"] nav.tabs#tabs .tab-badge {
|
|
80
|
+
position:absolute; top:2px; right:4px; margin-left:0; min-width:15px; height:15px; font-size:9.5px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ============ REVEAL + HOVER LIFT (bento tiles / cards) ============ */
|
|
84
|
+
html[data-design="console"] .kpi,
|
|
85
|
+
html[data-design="console"] .ocard,
|
|
86
|
+
html[data-design="console"] .acard,
|
|
87
|
+
html[data-design="console"] .card {
|
|
88
|
+
animation:cx-rise .5s cubic-bezier(.2,.7,.2,1) both;
|
|
89
|
+
transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;
|
|
90
|
+
}
|
|
91
|
+
html[data-design="console"] .kpi:hover,
|
|
92
|
+
html[data-design="console"] .ocard:hover,
|
|
93
|
+
html[data-design="console"] .acard:hover,
|
|
94
|
+
html[data-design="console"] .card:hover {
|
|
95
|
+
transform:translateY(-2px); border-color:var(--line-2); box-shadow:var(--shadow);
|
|
96
|
+
}
|
|
97
|
+
html[data-design="console"] .kpi-row .kpi:nth-child(1){animation-delay:.02s}
|
|
98
|
+
html[data-design="console"] .kpi-row .kpi:nth-child(2){animation-delay:.06s}
|
|
99
|
+
html[data-design="console"] .kpi-row .kpi:nth-child(3){animation-delay:.1s}
|
|
100
|
+
html[data-design="console"] .kpi-row .kpi:nth-child(4){animation-delay:.14s}
|
|
101
|
+
html[data-design="console"] .overview-top .ocard:nth-child(1){animation-delay:.08s}
|
|
102
|
+
html[data-design="console"] .overview-top .ocard:nth-child(2){animation-delay:.14s}
|
|
103
|
+
@keyframes cx-rise { from{ opacity:0; transform:translateY(8px);} to{ opacity:1; transform:none;} }
|
|
104
|
+
|
|
105
|
+
/* status color square before KPI eyebrow labels */
|
|
106
|
+
html[data-design="console"] .kpi-label { display:flex; align-items:center; gap:7px; }
|
|
107
|
+
html[data-design="console"] .kpi-label::before { content:""; width:8px; height:8px; border-radius:2px; background:var(--faint); flex-shrink:0; }
|
|
108
|
+
html[data-design="console"] .kpi:nth-child(1) .kpi-label::before { background:var(--s-done); }
|
|
109
|
+
html[data-design="console"] .kpi:nth-child(2) .kpi-label::before { background:var(--s-in_progress); }
|
|
110
|
+
html[data-design="console"] .kpi:nth-child(3) .kpi-label::before { background:var(--s-to_validate); }
|
|
111
|
+
html[data-design="console"] .kpi:nth-child(4) .kpi-label::before { background:var(--s-blocked); }
|
|
112
|
+
html[data-design="console"] .kpi-num { font-family:var(--display); font-weight:800; letter-spacing:-.02em; }
|
|
113
|
+
html[data-design="console"] .panel-title { font-family:var(--display); }
|
|
114
|
+
|
|
115
|
+
/* running agent — pulsing cyan ring (the sidebar "running" list is the only running-state row
|
|
116
|
+
this codebase renders: li.run-live inside .flatlist#running) */
|
|
117
|
+
html[data-design="console"] .flatlist li.run-live {
|
|
118
|
+
position:relative; border-color:var(--cool); box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--cool) 30%,transparent);
|
|
119
|
+
}
|
|
120
|
+
html[data-design="console"] .flatlist li.run-live::before {
|
|
121
|
+
content:""; position:absolute; left:6px; top:50%; width:7px; height:7px; margin-top:-3px;
|
|
122
|
+
border-radius:50%; background:var(--cool); box-shadow:0 0 0 0 color-mix(in srgb,var(--cool) 55%,transparent);
|
|
123
|
+
animation:cx-ripple 1.8s ease-out infinite;
|
|
124
|
+
}
|
|
125
|
+
html[data-design="console"] .flatlist li.run-live { padding-left:18px; }
|
|
126
|
+
@keyframes cx-ripple {
|
|
127
|
+
0% { box-shadow:0 0 0 0 color-mix(in srgb,var(--cool) 55%,transparent); }
|
|
128
|
+
70% { box-shadow:0 0 0 7px transparent; }
|
|
129
|
+
100% { box-shadow:0 0 0 0 transparent; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* sync dot pulse (reinforces the base pulse with a cyan tint on this design) */
|
|
133
|
+
html[data-design="console"] .sync-dot { box-shadow:0 0 0 0 color-mix(in srgb,var(--cool) 45%,transparent); }
|
|
134
|
+
|
|
135
|
+
/* ============ WORKFLOW PIPELINE — flowing particles on enabled connectors ============ */
|
|
136
|
+
html[data-design="console"] .wf-link { position:relative; overflow:visible; }
|
|
137
|
+
html[data-design="console"] .wf-link.on { background:linear-gradient(90deg,color-mix(in srgb,var(--signal) 55%,var(--line)),var(--line)); }
|
|
138
|
+
html[data-design="console"] .wf-link.on::after {
|
|
139
|
+
content:""; position:absolute; top:-2px; left:0; width:6px; height:6px; border-radius:50%;
|
|
140
|
+
background:var(--cool); box-shadow:0 0 8px var(--cool); opacity:0;
|
|
141
|
+
animation:cx-travel 2.2s linear infinite;
|
|
142
|
+
}
|
|
143
|
+
html[data-design="console"] .wf-strip .wf-arrow:not(.off) { position:relative; overflow:visible; }
|
|
144
|
+
html[data-design="console"] .wf-strip .wf-arrow:not(.off)::after {
|
|
145
|
+
content:""; position:absolute; top:50%; left:0; width:5px; height:5px; margin-top:-2.5px; border-radius:50%;
|
|
146
|
+
background:var(--cool); box-shadow:0 0 6px var(--cool); opacity:0; animation:cx-travel 2s linear infinite;
|
|
147
|
+
}
|
|
148
|
+
@keyframes cx-travel { 0%{ left:0; opacity:0; } 10%{ opacity:1; } 90%{ opacity:1; } 100%{ left:100%; opacity:0; } }
|
|
149
|
+
|
|
150
|
+
/* ============ COMMAND PALETTE (injected by console.js) ============ */
|
|
151
|
+
.cx-search-btn {
|
|
152
|
+
display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:8px;
|
|
153
|
+
border:1px solid var(--line-2); background:var(--surface); color:var(--muted);
|
|
154
|
+
font:inherit; font-size:12px; cursor:pointer; transition:border-color .18s,box-shadow .18s;
|
|
155
|
+
}
|
|
156
|
+
.cx-search-btn:hover { border-color:var(--cool); box-shadow:var(--cx-glow-cool); color:var(--ink); }
|
|
157
|
+
.cx-search-btn kbd { font:500 10.5px var(--mono); padding:1px 5px; border-radius:5px; background:var(--surface-2); border:1px solid var(--line-2); color:var(--muted); }
|
|
158
|
+
.cx-cmdk { position:fixed; inset:0; z-index:80; display:grid; place-items:start center; padding-top:12vh;
|
|
159
|
+
background:rgba(3,8,18,.55); backdrop-filter:blur(6px); }
|
|
160
|
+
html[data-design="console"][data-theme="light"] .cx-cmdk { background:rgba(15,23,40,.35); }
|
|
161
|
+
.cx-cmdk .cx-panel { width:min(560px,92vw); background:var(--surface); border:1px solid var(--line-2); border-radius:16px;
|
|
162
|
+
box-shadow:0 30px 80px rgba(0,0,0,.5); overflow:hidden; animation:cx-pop .16s cubic-bezier(.2,.7,.2,1); }
|
|
163
|
+
@keyframes cx-pop { from{ opacity:0; transform:translateY(-6px) scale(.985);} to{ opacity:1; transform:none;} }
|
|
164
|
+
.cx-cmdk input { width:100%; border:0; background:transparent; color:var(--ink); font:500 15px var(--sans);
|
|
165
|
+
padding:15px 18px; outline:none; border-bottom:1px solid var(--line); }
|
|
166
|
+
.cx-cmdk .cx-list { max-height:50vh; overflow:auto; padding:8px; }
|
|
167
|
+
.cx-cmdk .cx-item { display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; font-size:13px; color:var(--ink-2,var(--ink)); }
|
|
168
|
+
.cx-cmdk .cx-item:hover, .cx-cmdk .cx-item.is-sel { background:var(--surface-2); }
|
|
169
|
+
.cx-cmdk .cx-item .cx-tag { margin-left:auto; font:500 10px var(--mono); text-transform:uppercase; letter-spacing:.06em; color:var(--faint); }
|
|
170
|
+
.cx-cmdk .cx-empty { padding:18px; text-align:center; color:var(--faint); font-size:12.5px; }
|
|
171
|
+
.cx-cmdk .cx-foot { display:flex; gap:14px; padding:8px 14px; border-top:1px solid var(--line); font:400 11px var(--mono); color:var(--faint); }
|
|
172
|
+
|
|
173
|
+
/* ============ RESPONSIVE — rail collapses to a bottom bar ============ */
|
|
174
|
+
@media (max-width:820px) {
|
|
175
|
+
html[data-design="console"] nav.tabs#tabs {
|
|
176
|
+
left:0; right:0; top:auto; bottom:0; width:auto; height:auto;
|
|
177
|
+
flex-direction:row; justify-content:space-around; overflow-x:auto; overflow-y:hidden;
|
|
178
|
+
border-right:0; border-top:1px solid var(--line); padding:6px 4px;
|
|
179
|
+
}
|
|
180
|
+
html[data-design="console"] .topbar { padding-left:16px; }
|
|
181
|
+
html[data-design="console"] .stage { margin-left:0; margin-bottom:64px; }
|
|
182
|
+
html[data-design="console"] nav.tabs#tabs .tab-label { display:none !important; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@media (prefers-reduced-motion: reduce) {
|
|
186
|
+
html[data-design="console"] .kpi,
|
|
187
|
+
html[data-design="console"] .ocard,
|
|
188
|
+
html[data-design="console"] .acard,
|
|
189
|
+
html[data-design="console"] .card { animation:none; }
|
|
190
|
+
html[data-design="console"] .wf-link.on::after,
|
|
191
|
+
html[data-design="console"] .wf-strip .wf-arrow::after,
|
|
192
|
+
html[data-design="console"] .flatlist li.run-live::before { animation:none; display:none; }
|
|
193
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* Spectral Console — command palette (Ctrl/Cmd+K). Only active while
|
|
2
|
+
document.documentElement.dataset.design === 'console'; watches for live design switches. */
|
|
3
|
+
(function () {
|
|
4
|
+
'use strict';
|
|
5
|
+
var active = false;
|
|
6
|
+
var searchBtn = null;
|
|
7
|
+
var overlay = null;
|
|
8
|
+
var items = [];
|
|
9
|
+
var filtered = [];
|
|
10
|
+
var selIdx = 0;
|
|
11
|
+
|
|
12
|
+
function buildItems() {
|
|
13
|
+
var out = [];
|
|
14
|
+
document.querySelectorAll('#tabs .tab').forEach(function (tab) {
|
|
15
|
+
var label = tab.querySelector('.tab-label');
|
|
16
|
+
var text = label ? label.textContent.trim() : (tab.dataset.tab || 'tab');
|
|
17
|
+
out.push({ text: 'Go to ' + text, tag: 'nav', run: function () { tab.click(); } });
|
|
18
|
+
});
|
|
19
|
+
var runBtn = document.getElementById('runQuickBtn');
|
|
20
|
+
if (runBtn) out.push({ text: 'Run · open chat', tag: 'action', run: function () { runBtn.click(); } });
|
|
21
|
+
var themeBtn = document.getElementById('themeToggle');
|
|
22
|
+
if (themeBtn) out.push({ text: 'Toggle theme', tag: 'action', run: function () { themeBtn.click(); } });
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderList(q) {
|
|
27
|
+
var list = overlay.querySelector('.cx-list');
|
|
28
|
+
filtered = !q ? items : items.filter(function (it) { return it.text.toLowerCase().indexOf(q.toLowerCase()) !== -1; });
|
|
29
|
+
selIdx = 0;
|
|
30
|
+
list.innerHTML = '';
|
|
31
|
+
if (!filtered.length) {
|
|
32
|
+
var empty = document.createElement('div');
|
|
33
|
+
empty.className = 'cx-empty';
|
|
34
|
+
empty.textContent = 'No matches';
|
|
35
|
+
list.appendChild(empty);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
filtered.forEach(function (it, i) {
|
|
39
|
+
var row = document.createElement('div');
|
|
40
|
+
row.className = 'cx-item' + (i === 0 ? ' is-sel' : '');
|
|
41
|
+
row.innerHTML = '<span>' + it.text.replace(/</g, '<') + '</span><span class="cx-tag">' + it.tag + '</span>';
|
|
42
|
+
row.addEventListener('click', function () { runItem(i); });
|
|
43
|
+
list.appendChild(row);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function highlight() {
|
|
48
|
+
overlay.querySelectorAll('.cx-item').forEach(function (row, i) {
|
|
49
|
+
row.classList.toggle('is-sel', i === selIdx);
|
|
50
|
+
});
|
|
51
|
+
var sel = overlay.querySelector('.cx-item.is-sel');
|
|
52
|
+
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runItem(i) {
|
|
56
|
+
var it = filtered[i];
|
|
57
|
+
if (!it) return;
|
|
58
|
+
close();
|
|
59
|
+
try { it.run(); } catch (e) { /* no-op: target control may not exist in this view */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function open() {
|
|
63
|
+
if (overlay || !active) return;
|
|
64
|
+
items = buildItems();
|
|
65
|
+
overlay = document.createElement('div');
|
|
66
|
+
overlay.className = 'cx-cmdk';
|
|
67
|
+
overlay.innerHTML =
|
|
68
|
+
'<div class="cx-panel">' +
|
|
69
|
+
'<input type="text" placeholder="Go to a tab, run, toggle theme…" autocomplete="off" spellcheck="false" />' +
|
|
70
|
+
'<div class="cx-list"></div>' +
|
|
71
|
+
'<div class="cx-foot"><span>↑↓ navigate</span><span>↵ select</span><span>esc close</span></div>' +
|
|
72
|
+
'</div>';
|
|
73
|
+
document.body.appendChild(overlay);
|
|
74
|
+
renderList('');
|
|
75
|
+
var input = overlay.querySelector('input');
|
|
76
|
+
input.focus();
|
|
77
|
+
input.addEventListener('input', function () { renderList(input.value); });
|
|
78
|
+
overlay.addEventListener('mousedown', function (e) { if (e.target === overlay) close(); });
|
|
79
|
+
overlay.addEventListener('keydown', function (e) {
|
|
80
|
+
if (e.key === 'Escape') { close(); }
|
|
81
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); selIdx = Math.min(selIdx + 1, filtered.length - 1); highlight(); }
|
|
82
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); selIdx = Math.max(selIdx - 1, 0); highlight(); }
|
|
83
|
+
else if (e.key === 'Enter') { e.preventDefault(); runItem(selIdx); }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function close() {
|
|
88
|
+
if (overlay) { overlay.remove(); overlay = null; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function onKeydown(e) {
|
|
92
|
+
if (!active) return;
|
|
93
|
+
var k = e.key === 'k' || e.key === 'K';
|
|
94
|
+
if (k && (e.metaKey || e.ctrlKey)) { e.preventDefault(); overlay ? close() : open(); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function injectButton() {
|
|
98
|
+
if (searchBtn || document.getElementById('cxSearchBtn')) return;
|
|
99
|
+
var host = document.querySelector('.top-right');
|
|
100
|
+
if (!host) return;
|
|
101
|
+
searchBtn = document.createElement('button');
|
|
102
|
+
searchBtn.id = 'cxSearchBtn';
|
|
103
|
+
searchBtn.className = 'cx-search-btn';
|
|
104
|
+
searchBtn.type = 'button';
|
|
105
|
+
searchBtn.title = 'Command palette';
|
|
106
|
+
searchBtn.innerHTML =
|
|
107
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>' +
|
|
108
|
+
'<span>Search…</span><kbd>⌘K</kbd>';
|
|
109
|
+
searchBtn.addEventListener('click', open);
|
|
110
|
+
host.insertBefore(searchBtn, host.firstChild);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removeButton() {
|
|
114
|
+
if (searchBtn) { searchBtn.remove(); searchBtn = null; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function activate() {
|
|
118
|
+
if (active) return;
|
|
119
|
+
active = true;
|
|
120
|
+
injectButton();
|
|
121
|
+
document.addEventListener('keydown', onKeydown);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function deactivate() {
|
|
125
|
+
if (!active) return;
|
|
126
|
+
active = false;
|
|
127
|
+
close();
|
|
128
|
+
removeButton();
|
|
129
|
+
document.removeEventListener('keydown', onKeydown);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sync() {
|
|
133
|
+
if (document.documentElement.dataset.design === 'console') activate();
|
|
134
|
+
else deactivate();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sync();
|
|
138
|
+
var mo = new MutationObserver(sync);
|
|
139
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-design'] });
|
|
140
|
+
// .top-right may not exist yet on first paint in some load orders — retry once shortly after.
|
|
141
|
+
if (document.documentElement.dataset.design === 'console' && !document.getElementById('cxSearchBtn')) {
|
|
142
|
+
setTimeout(injectButton, 300);
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* ============================================================================================
|
|
2
|
+
* Orbit — light-first, airy, circular design skin. Teal signal + amber accent, dotted ground,
|
|
3
|
+
* Space Grotesk display / IBM Plex Sans body / JetBrains Mono numerics (all self-hosted, see the
|
|
4
|
+
* @font-face rules already declared in styles.css). Navigation is NOT the tab bar — the tab bar is
|
|
5
|
+
* hidden and replaced by a round "hub" button in the header that opens a full-screen radial menu
|
|
6
|
+
* (orbit.js builds it from the very same #tabs buttons, so routing logic is fully reused).
|
|
7
|
+
* ============================================================================================ */
|
|
8
|
+
|
|
9
|
+
/* ---- tokens: light (default) ---- */
|
|
10
|
+
html[data-design="orbit"] {
|
|
11
|
+
--bg:#f2f6f7; --surface:#ffffff; --surface-2:#eaf1f2; --line:#d6e1e4;
|
|
12
|
+
--ink:#0e1d21; --muted:#5d7379; --faint:#8ea1a6;
|
|
13
|
+
--signal:#0f8b8d; --cool:#e6a54b; --on-accent:#ffffff;
|
|
14
|
+
--s-todo:#8194a0; --s-in_progress:#2f6fe0; --s-to_validate:#d97a17; --s-to_analyze:#c93fae;
|
|
15
|
+
--s-done:#1f9c5c; --s-blocked:#d93a3a;
|
|
16
|
+
--radius:20px; --shadow:0 14px 40px rgba(14,29,33,.12);
|
|
17
|
+
--sans:'IBM Plex Sans',ui-sans-serif,system-ui,sans-serif;
|
|
18
|
+
--display:'Space Grotesk',ui-sans-serif,system-ui,sans-serif;
|
|
19
|
+
--mono:'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace;
|
|
20
|
+
--ob-dot: rgba(14,29,33,.07);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ---- tokens: dark ---- */
|
|
24
|
+
html[data-design="orbit"][data-theme="dark"] {
|
|
25
|
+
--bg:#0c1516; --surface:#121d1f; --surface-2:#182629; --line:#233639;
|
|
26
|
+
--ink:#e7f0f1; --muted:#8aa0a5; --faint:#5f7479;
|
|
27
|
+
--signal:#2fc3c5; --cool:#e6a54b; --on-accent:#06191a;
|
|
28
|
+
--s-todo:#6f838c; --s-in_progress:#5b93ff; --s-to_validate:#ff9b3d; --s-to_analyze:#ff5fd2;
|
|
29
|
+
--s-done:#3fbf7f; --s-blocked:#ff5c5c;
|
|
30
|
+
--shadow:0 14px 40px rgba(0,0,0,.45);
|
|
31
|
+
--ob-dot: rgba(255,255,255,.06);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ---- base feel ---- */
|
|
35
|
+
html[data-design="orbit"] body {
|
|
36
|
+
font-family:var(--sans);
|
|
37
|
+
background-image: radial-gradient(var(--ob-dot) 1.2px, transparent 1.2px);
|
|
38
|
+
background-size: 22px 22px;
|
|
39
|
+
}
|
|
40
|
+
html[data-design="orbit"] .kpi-num,
|
|
41
|
+
html[data-design="orbit"] .card .ct,
|
|
42
|
+
html[data-design="orbit"] .panel-title,
|
|
43
|
+
html[data-design="orbit"] .brand-name { font-family:var(--display); letter-spacing:-.01em; }
|
|
44
|
+
html[data-design="orbit"] .kpi-num { font-weight:700; }
|
|
45
|
+
|
|
46
|
+
/* navigation is the radial menu — the tab bar is hidden, content runs full width */
|
|
47
|
+
html[data-design="orbit"] nav.tabs { display:none; }
|
|
48
|
+
html[data-design="orbit"] .topbar { grid-template-columns:auto 1fr auto; }
|
|
49
|
+
|
|
50
|
+
/* pills + rounder cards */
|
|
51
|
+
html[data-design="orbit"] .btn,
|
|
52
|
+
html[data-design="orbit"] .run-btn,
|
|
53
|
+
html[data-design="orbit"] .theme-toggle,
|
|
54
|
+
html[data-design="orbit"] .mode-chip,
|
|
55
|
+
html[data-design="orbit"] .meta-chip { border-radius:999px; }
|
|
56
|
+
html[data-design="orbit"] .kpi,
|
|
57
|
+
html[data-design="orbit"] .card,
|
|
58
|
+
html[data-design="orbit"] .flatlist li,
|
|
59
|
+
html[data-design="orbit"] .journal-row { border-radius:var(--radius); }
|
|
60
|
+
html[data-design="orbit"] .card:hover,
|
|
61
|
+
html[data-design="orbit"] .kpi:hover { transform:translateY(-2px); box-shadow:var(--shadow); transition:transform .2s,box-shadow .2s; }
|
|
62
|
+
|
|
63
|
+
/* running-agent avatars ripple (best-effort; harmless if the class isn't present) */
|
|
64
|
+
html[data-design="orbit"] .avatar.running,
|
|
65
|
+
html[data-design="orbit"] .av.running { position:relative; }
|
|
66
|
+
html[data-design="orbit"] .avatar.running::after,
|
|
67
|
+
html[data-design="orbit"] .av.running::after {
|
|
68
|
+
content:""; position:absolute; inset:-5px; border-radius:50%; border:2px solid var(--signal);
|
|
69
|
+
animation: ob-ripple 1.6s ease-out infinite;
|
|
70
|
+
}
|
|
71
|
+
@keyframes ob-ripple { 0%{ transform:scale(.9); opacity:.8; } 100%{ transform:scale(1.3); opacity:0; } }
|
|
72
|
+
|
|
73
|
+
/* flowing teal particles along the workflow pipeline connectors */
|
|
74
|
+
html[data-design="orbit"] .wf-arrow::after { background:linear-gradient(90deg,transparent,var(--signal),transparent); }
|
|
75
|
+
html[data-design="orbit"] .wf-conn { position:relative; overflow:hidden; background:linear-gradient(90deg,var(--cool),var(--signal)); }
|
|
76
|
+
html[data-design="orbit"] .wf-conn::after {
|
|
77
|
+
content:""; position:absolute; top:-2px; left:0; width:6px; height:6px; border-radius:50%;
|
|
78
|
+
background:var(--signal); box-shadow:0 0 8px var(--signal); animation: ob-flow 2.4s linear infinite;
|
|
79
|
+
}
|
|
80
|
+
@keyframes ob-flow { 0%{ left:-6px; opacity:0; } 10%{ opacity:1; } 90%{ opacity:1; } 100%{ left:100%; opacity:0; } }
|
|
81
|
+
|
|
82
|
+
/* card reveal on load (reuses the existing body.booting convention) */
|
|
83
|
+
html[data-design="orbit"] body.booting .kpi,
|
|
84
|
+
html[data-design="orbit"] body.booting .card { animation: ob-rise .45s cubic-bezier(.2,.8,.2,1) both; }
|
|
85
|
+
@keyframes ob-rise { from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
|
|
86
|
+
|
|
87
|
+
/* ============================================================================================
|
|
88
|
+
* THE HUB — round brand button in the header; shows live global progress %, doubles as the
|
|
89
|
+
* radial-menu trigger. Injected by orbit.js as the first child of .brand.
|
|
90
|
+
* ============================================================================================ */
|
|
91
|
+
html[data-design="orbit"] .ob-hub {
|
|
92
|
+
position:relative; width:46px; height:46px; border-radius:50%; flex-shrink:0;
|
|
93
|
+
background:var(--signal); color:var(--on-accent); display:grid; place-items:center;
|
|
94
|
+
box-shadow:0 8px 20px color-mix(in srgb, var(--signal) 40%, transparent);
|
|
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
|
+
}
|
|
103
|
+
@keyframes ob-breathe { 0%,100%{ transform:scale(1); opacity:.35; } 50%{ transform:scale(1.08); opacity:.12; } }
|
|
104
|
+
|
|
105
|
+
/* ---- fixed bottom-left FAB on small screens ---- */
|
|
106
|
+
@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-pct { font-size:14px; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ============================================================================================
|
|
112
|
+
* THE OVERLAY + DIAL
|
|
113
|
+
* ============================================================================================ */
|
|
114
|
+
html[data-design="orbit"] .ob-ov {
|
|
115
|
+
position:fixed; inset:0; z-index:80; display:grid; place-items:center;
|
|
116
|
+
background:rgba(14,29,33,.42); backdrop-filter:blur(6px); animation: ob-fade .18s ease;
|
|
117
|
+
}
|
|
118
|
+
html[data-design="orbit"][data-theme="dark"] .ob-ov { background:rgba(0,0,0,.55); }
|
|
119
|
+
html[data-design="orbit"] .ob-ov.ob-closing { animation: ob-fadeout .15s ease forwards; }
|
|
120
|
+
@keyframes ob-fade { from{ opacity:0; } to{ opacity:1; } }
|
|
121
|
+
@keyframes ob-fadeout { to{ opacity:0; } }
|
|
122
|
+
|
|
123
|
+
html[data-design="orbit"] .ob-dial {
|
|
124
|
+
position:relative; width:284px; height:284px; flex:none; user-select:none;
|
|
125
|
+
animation: ob-dialpop .28s cubic-bezier(.2,.8,.2,1);
|
|
126
|
+
}
|
|
127
|
+
@keyframes ob-dialpop { from{ transform:scale(.7); opacity:0; } to{ transform:none; opacity:1; } }
|
|
128
|
+
@media (max-width:900px) { html[data-design="orbit"] .ob-dial { width:210px; height:210px; } }
|
|
129
|
+
|
|
130
|
+
html[data-design="orbit"] .ob-dial .ob-ring { position:absolute; inset:0; width:100%; height:100%; transform:rotate(-90deg); }
|
|
131
|
+
html[data-design="orbit"] .ob-dial .ob-track { fill:none; stroke:var(--surface-2); stroke-width:16; }
|
|
132
|
+
html[data-design="orbit"] .ob-dial .ob-prog { fill:none; stroke:var(--signal); stroke-width:16; transition:stroke-dasharray 1s cubic-bezier(.2,.7,.2,1); }
|
|
133
|
+
html[data-design="orbit"] .ob-dial .ob-marker { fill:none; stroke:var(--cool); stroke-width:16; transition:transform .5s cubic-bezier(.3,.8,.3,1); transform-origin:142px 142px; }
|
|
134
|
+
@media (max-width:900px) { html[data-design="orbit"] .ob-dial .ob-marker { transform-origin:105px 105px; } }
|
|
135
|
+
|
|
136
|
+
html[data-design="orbit"] .ob-chev {
|
|
137
|
+
position:absolute; width:30px; height:30px; border-radius:50%; display:grid; place-items:center;
|
|
138
|
+
color:var(--on-accent); background:var(--signal); box-shadow:var(--shadow); border:0; cursor:pointer;
|
|
139
|
+
transition:transform .18s,background .18s;
|
|
140
|
+
}
|
|
141
|
+
html[data-design="orbit"] .ob-chev svg { width:14px; height:14px; }
|
|
142
|
+
html[data-design="orbit"] .ob-chev:hover { transform:scale(1.12); filter:brightness(1.08); }
|
|
143
|
+
html[data-design="orbit"] .ob-chev.ob-up { left:50%; top:-4px; transform:translateX(-50%); }
|
|
144
|
+
html[data-design="orbit"] .ob-chev.ob-up:hover { transform:translateX(-50%) scale(1.12); }
|
|
145
|
+
html[data-design="orbit"] .ob-chev.ob-down { left:50%; bottom:-4px; transform:translateX(-50%); }
|
|
146
|
+
html[data-design="orbit"] .ob-chev.ob-down:hover { transform:translateX(-50%) scale(1.12); }
|
|
147
|
+
html[data-design="orbit"] .ob-chev.ob-left { top:50%; left:-4px; transform:translateY(-50%); }
|
|
148
|
+
html[data-design="orbit"] .ob-chev.ob-left:hover { transform:translateY(-50%) scale(1.12); }
|
|
149
|
+
html[data-design="orbit"] .ob-chev.ob-right { top:50%; right:-4px; transform:translateY(-50%); }
|
|
150
|
+
html[data-design="orbit"] .ob-chev.ob-right:hover { transform:translateY(-50%) scale(1.12); }
|
|
151
|
+
|
|
152
|
+
html[data-design="orbit"] .ob-center {
|
|
153
|
+
position:absolute; left:50%; top:50%; width:76px; height:76px; transform:translate(-50%,-50%);
|
|
154
|
+
border-radius:50%; display:grid; place-items:center; background:var(--signal); color:var(--on-accent);
|
|
155
|
+
box-shadow:0 10px 24px color-mix(in srgb, var(--signal) 40%, transparent); text-align:center; line-height:1;
|
|
156
|
+
border:0; cursor:pointer;
|
|
157
|
+
}
|
|
158
|
+
html[data-design="orbit"] .ob-center .ob-pct { font-family:var(--display); font-size:22px; font-weight:700; letter-spacing:-.03em; }
|
|
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; }
|
|
160
|
+
html[data-design="orbit"] .ob-center::after {
|
|
161
|
+
content:""; position:absolute; inset:-6px; border-radius:50%; border:2px solid var(--signal); opacity:.35;
|
|
162
|
+
animation: ob-breathe 2.6s ease-in-out infinite;
|
|
163
|
+
}
|
|
164
|
+
@media (max-width:900px) {
|
|
165
|
+
html[data-design="orbit"] .ob-center { width:60px; height:60px; }
|
|
166
|
+
html[data-design="orbit"] .ob-center .ob-pct { font-size:17px; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
html[data-design="orbit"] .ob-item {
|
|
170
|
+
position:absolute; left:50%; top:50%; width:58px; height:58px; margin:-29px 0 0 -29px;
|
|
171
|
+
border-radius:50%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
|
|
172
|
+
color:var(--ink); background:var(--surface); border:1px solid var(--line); cursor:pointer;
|
|
173
|
+
transform:translate(0,0) scale(.3); opacity:0; animation: ob-orbit .5s cubic-bezier(.2,.8,.2,1) forwards;
|
|
174
|
+
transition:background .18s,color .18s,box-shadow .18s; padding:0;
|
|
175
|
+
}
|
|
176
|
+
html[data-design="orbit"] .ob-item .ob-ico { width:19px; height:19px; display:inline-flex; }
|
|
177
|
+
html[data-design="orbit"] .ob-item .ob-ico svg { width:100%; height:100%; }
|
|
178
|
+
html[data-design="orbit"] .ob-item .ob-lb { font:600 9px var(--sans); letter-spacing:.01em; white-space:nowrap; }
|
|
179
|
+
html[data-design="orbit"] .ob-item:hover { background:var(--surface-2); }
|
|
180
|
+
html[data-design="orbit"] .ob-item.is-active { background:var(--surface); color:var(--signal); box-shadow:var(--shadow), 0 0 0 2px var(--signal); }
|
|
181
|
+
html[data-design="orbit"] .ob-item .ob-bd {
|
|
182
|
+
position:absolute; top:2px; right:2px; min-width:15px; height:15px; padding:0 4px; border-radius:8px;
|
|
183
|
+
background:var(--s-blocked); color:#fff; font:700 9px/15px var(--mono); text-align:center;
|
|
184
|
+
}
|
|
185
|
+
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(98px) rotate(calc(-1*var(--a))) scale(1); } }
|
|
186
|
+
@media (max-width:900px) {
|
|
187
|
+
html[data-design="orbit"] .ob-item { width:46px; height:46px; margin:-23px 0 0 -23px; }
|
|
188
|
+
html[data-design="orbit"] .ob-item .ob-lb { display:none; }
|
|
189
|
+
html[data-design="orbit"] .ob-item .ob-ico { width:16px; height:16px; }
|
|
190
|
+
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(72px) rotate(calc(-1*var(--a))) scale(1); } }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@media (prefers-reduced-motion: reduce) {
|
|
194
|
+
html[data-design="orbit"] .ob-item,
|
|
195
|
+
html[data-design="orbit"] .ob-dial,
|
|
196
|
+
html[data-design="orbit"] .ob-ov,
|
|
197
|
+
html[data-design="orbit"] .ob-hub::after,
|
|
198
|
+
html[data-design="orbit"] .ob-center::after,
|
|
199
|
+
html[data-design="orbit"] .wf-conn::after,
|
|
200
|
+
html[data-design="orbit"] .avatar.running::after,
|
|
201
|
+
html[data-design="orbit"] .av.running::after,
|
|
202
|
+
html[data-design="orbit"] body.booting .kpi,
|
|
203
|
+
html[data-design="orbit"] body.booting .card {
|
|
204
|
+
animation:none !important;
|
|
205
|
+
}
|
|
206
|
+
html[data-design="orbit"] .ob-item { opacity:1; transform:rotate(var(--a)) translate(98px) rotate(calc(-1*var(--a))); }
|
|
207
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Orbit design behavior: replaces the hidden tab bar with a round "hub" button (live global %)
|
|
4
|
+
* that opens a full-screen radial menu built from the very same #tabs buttons — so all routing
|
|
5
|
+
* stays owned by app.js; this file only clones markup and forwards clicks/keys to the real tabs.
|
|
6
|
+
* Active only while document.documentElement.dataset.design === 'orbit'; watches that attribute
|
|
7
|
+
* so switching designs live cleanly builds/tears down without touching any other file.
|
|
8
|
+
*/
|
|
9
|
+
(function () {
|
|
10
|
+
var active = false;
|
|
11
|
+
var hubEl = null, ovEl = null, tabsObserver = null, fillObserver = null, pctInterval = null;
|
|
12
|
+
var state = { open: false };
|
|
13
|
+
|
|
14
|
+
function isOrbit() { return document.documentElement.getAttribute('data-design') === 'orbit'; }
|
|
15
|
+
function tabs() { return Array.prototype.slice.call(document.querySelectorAll('#tabs .tab')); }
|
|
16
|
+
function activeIndex(list) { var i = list.findIndex(function (t) { return t.classList.contains('is-active'); }); return i < 0 ? 0 : i; }
|
|
17
|
+
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
18
|
+
function chevSvg(dir) {
|
|
19
|
+
var d = { up: 'M6 15l6-6 6 6', down: 'M6 9l6 6 6-6', left: 'M15 6l-6 6 6 6', right: 'M9 6l6 6-6 6' }[dir];
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ---- hub button (injected into .brand) ---- */
|
|
24
|
+
function buildHub() {
|
|
25
|
+
if (hubEl) return;
|
|
26
|
+
var brand = document.querySelector('.brand');
|
|
27
|
+
if (!brand) return;
|
|
28
|
+
hubEl = document.createElement('button');
|
|
29
|
+
hubEl.type = 'button';
|
|
30
|
+
hubEl.className = 'ob-hub';
|
|
31
|
+
hubEl.title = 'Menu (M)';
|
|
32
|
+
hubEl.setAttribute('aria-haspopup', 'dialog');
|
|
33
|
+
hubEl.innerHTML = '<span class="ob-pct">0%</span>';
|
|
34
|
+
brand.insertBefore(hubEl, brand.firstChild);
|
|
35
|
+
hubEl.addEventListener('click', toggleOverlay);
|
|
36
|
+
}
|
|
37
|
+
function removeHub() { if (hubEl) { hubEl.remove(); hubEl = null; } }
|
|
38
|
+
|
|
39
|
+
/* ---- global progress % — read #globalMeterFill's width, kept live ---- */
|
|
40
|
+
function updatePct() {
|
|
41
|
+
var fill = document.getElementById('globalMeterFill');
|
|
42
|
+
var pct = 0;
|
|
43
|
+
if (fill && fill.style && fill.style.width) pct = parseFloat(fill.style.width) || 0;
|
|
44
|
+
pct = Math.max(0, Math.min(100, Math.round(pct)));
|
|
45
|
+
if (hubEl) { var hp = hubEl.querySelector('.ob-pct'); if (hp) hp.textContent = pct + '%'; }
|
|
46
|
+
if (ovEl) {
|
|
47
|
+
var cp = ovEl.querySelector('.ob-center .ob-pct'); if (cp) cp.textContent = pct + '%';
|
|
48
|
+
var prog = ovEl.querySelector('.ob-prog');
|
|
49
|
+
if (prog) { var C = 804.2, len = (pct / 100 * C).toFixed(1); prog.setAttribute('stroke-dasharray', len + ' ' + C); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ---- radial overlay ---- */
|
|
54
|
+
function buildOverlay() {
|
|
55
|
+
if (ovEl) return;
|
|
56
|
+
var list = tabs(), n = list.length || 1;
|
|
57
|
+
var idx = activeIndex(list);
|
|
58
|
+
var items = list.map(function (tab, i) {
|
|
59
|
+
var angle = -90 + i * (360 / n);
|
|
60
|
+
var ico = tab.querySelector('.tab-ico');
|
|
61
|
+
var lbl = tab.querySelector('.tab-label');
|
|
62
|
+
var badgeEl = tab.querySelector('.tab-badge');
|
|
63
|
+
var badge = badgeEl && !badgeEl.hidden ? '<span class="ob-bd">' + escapeHtml(badgeEl.textContent) + '</span>' : '';
|
|
64
|
+
var cls = 'ob-item' + (i === idx ? ' is-active' : '');
|
|
65
|
+
return '<button type="button" class="' + cls + '" style="--a:' + angle + 'deg" data-tab-idx="' + i + '">' + badge +
|
|
66
|
+
'<span class="ob-ico">' + (ico ? ico.innerHTML : '') + '</span>' +
|
|
67
|
+
'<span class="ob-lb">' + escapeHtml(lbl ? lbl.textContent : '') + '</span></button>';
|
|
68
|
+
}).join('');
|
|
69
|
+
var markerAngle = idx * (360 / n);
|
|
70
|
+
ovEl = document.createElement('div');
|
|
71
|
+
ovEl.className = 'ob-ov';
|
|
72
|
+
ovEl.innerHTML =
|
|
73
|
+
'<div class="ob-dial" role="dialog" aria-label="spectoflow navigation">' +
|
|
74
|
+
'<svg class="ob-ring" viewBox="0 0 284 284" aria-hidden="true">' +
|
|
75
|
+
'<circle class="ob-track" cx="142" cy="142" r="128" stroke-dasharray="176 25" stroke-dashoffset="-12"></circle>' +
|
|
76
|
+
'<circle class="ob-prog" cx="142" cy="142" r="128" stroke-dasharray="0 804.2"></circle>' +
|
|
77
|
+
'<circle class="ob-marker" cx="142" cy="142" r="128" stroke-dasharray="34 770.2" style="transform:rotate(' + markerAngle + 'deg)"></circle>' +
|
|
78
|
+
'</svg>' +
|
|
79
|
+
'<button type="button" class="ob-chev ob-up" title="Previous (Up)">' + chevSvg('up') + '</button>' +
|
|
80
|
+
'<button type="button" class="ob-chev ob-down" title="Next (Down)">' + chevSvg('down') + '</button>' +
|
|
81
|
+
'<button type="button" class="ob-chev ob-left" title="Search / Board">' + chevSvg('left') + '</button>' +
|
|
82
|
+
'<button type="button" class="ob-chev ob-right" title="Run">' + chevSvg('right') + '</button>' +
|
|
83
|
+
'<button type="button" class="ob-center" title="Close (Esc)"><span class="ob-pct">0%</span><span class="ob-lbl">Delivered</span></button>' +
|
|
84
|
+
items +
|
|
85
|
+
'</div>';
|
|
86
|
+
document.body.appendChild(ovEl);
|
|
87
|
+
updatePct();
|
|
88
|
+
ovEl.addEventListener('click', onOverlayClick);
|
|
89
|
+
var tabsEl = document.getElementById('tabs');
|
|
90
|
+
if (tabsEl) {
|
|
91
|
+
tabsObserver = new MutationObserver(syncActiveVisual);
|
|
92
|
+
tabsObserver.observe(tabsEl, { attributes: true, attributeFilter: ['class'], subtree: true });
|
|
93
|
+
}
|
|
94
|
+
var center = ovEl.querySelector('.ob-center');
|
|
95
|
+
if (center && center.focus) center.focus();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function syncActiveVisual() {
|
|
99
|
+
if (!ovEl) return;
|
|
100
|
+
var list = tabs(), n = list.length || 1, idx = activeIndex(list);
|
|
101
|
+
var items = ovEl.querySelectorAll('.ob-item');
|
|
102
|
+
items.forEach(function (el, i) { el.classList.toggle('is-active', i === idx); });
|
|
103
|
+
var marker = ovEl.querySelector('.ob-marker');
|
|
104
|
+
if (marker) marker.style.transform = 'rotate(' + (idx * (360 / n)) + 'deg)';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cycle(dir) {
|
|
108
|
+
var list = tabs(), n = list.length;
|
|
109
|
+
if (!n) return;
|
|
110
|
+
var idx = (activeIndex(list) + dir + n) % n;
|
|
111
|
+
list[idx].click();
|
|
112
|
+
}
|
|
113
|
+
function goLeft() {
|
|
114
|
+
var search = document.getElementById('search');
|
|
115
|
+
if (search) { closeOverlay(); search.focus(); return; }
|
|
116
|
+
var board = tabs().filter(function (t) { return t.dataset.tab === 'board'; })[0];
|
|
117
|
+
if (board) board.click();
|
|
118
|
+
closeOverlay();
|
|
119
|
+
}
|
|
120
|
+
function goRight() {
|
|
121
|
+
var run = document.getElementById('runQuickBtn');
|
|
122
|
+
if (run) run.click();
|
|
123
|
+
closeOverlay();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onOverlayClick(e) {
|
|
127
|
+
var item = e.target.closest('.ob-item');
|
|
128
|
+
if (item) {
|
|
129
|
+
var idx = +item.dataset.tabIdx, list = tabs();
|
|
130
|
+
if (list[idx]) list[idx].click();
|
|
131
|
+
closeOverlay();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (e.target.closest('.ob-center')) { closeOverlay(); return; }
|
|
135
|
+
var chev = e.target.closest('.ob-chev');
|
|
136
|
+
if (chev) {
|
|
137
|
+
if (chev.classList.contains('ob-up')) cycle(-1);
|
|
138
|
+
else if (chev.classList.contains('ob-down')) cycle(1);
|
|
139
|
+
else if (chev.classList.contains('ob-left')) goLeft();
|
|
140
|
+
else if (chev.classList.contains('ob-right')) goRight();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (e.target === ovEl) closeOverlay();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toggleOverlay() { if (state.open) closeOverlay(); else openOverlay(); }
|
|
147
|
+
function openOverlay() { buildOverlay(); state.open = true; }
|
|
148
|
+
function closeOverlay() {
|
|
149
|
+
if (tabsObserver) { tabsObserver.disconnect(); tabsObserver = null; }
|
|
150
|
+
state.open = false;
|
|
151
|
+
if (!ovEl) return;
|
|
152
|
+
var el = ovEl; ovEl = null;
|
|
153
|
+
el.classList.add('ob-closing');
|
|
154
|
+
setTimeout(function () { el.remove(); }, 160);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ---- keyboard: m toggles, arrows cycle while open, Enter/Esc close ---- */
|
|
158
|
+
function isTyping(el) {
|
|
159
|
+
return el && (['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) !== -1 || el.isContentEditable);
|
|
160
|
+
}
|
|
161
|
+
function onKeydown(e) {
|
|
162
|
+
if (!active) return;
|
|
163
|
+
if ((e.key === 'm' || e.key === 'M') && !isTyping(document.activeElement)) { e.preventDefault(); toggleOverlay(); return; }
|
|
164
|
+
if (!state.open) return;
|
|
165
|
+
if (e.key === 'Escape') closeOverlay();
|
|
166
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); cycle(-1); }
|
|
167
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); cycle(1); }
|
|
168
|
+
else if (e.key === 'Enter') closeOverlay();
|
|
169
|
+
}
|
|
170
|
+
document.addEventListener('keydown', onKeydown);
|
|
171
|
+
|
|
172
|
+
/* ---- enter / leave the design ---- */
|
|
173
|
+
function enter() {
|
|
174
|
+
buildHub();
|
|
175
|
+
updatePct();
|
|
176
|
+
pctInterval = setInterval(updatePct, 2000);
|
|
177
|
+
var fill = document.getElementById('globalMeterFill');
|
|
178
|
+
if (fill) {
|
|
179
|
+
fillObserver = new MutationObserver(updatePct);
|
|
180
|
+
fillObserver.observe(fill, { attributes: true, attributeFilter: ['style'] });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function leave() {
|
|
184
|
+
closeOverlay();
|
|
185
|
+
removeHub();
|
|
186
|
+
if (pctInterval) { clearInterval(pctInterval); pctInterval = null; }
|
|
187
|
+
if (fillObserver) { fillObserver.disconnect(); fillObserver = null; }
|
|
188
|
+
}
|
|
189
|
+
function sync() {
|
|
190
|
+
var on = isOrbit();
|
|
191
|
+
if (on && !active) { active = true; enter(); }
|
|
192
|
+
else if (!on && active) { active = false; leave(); }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
new MutationObserver(sync).observe(document.documentElement, { attributes: true, attributeFilter: ['data-design'] });
|
|
196
|
+
sync();
|
|
197
|
+
})();
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
(function (root) {
|
|
22
22
|
const DESIGNS = [
|
|
23
|
+
{ id: 'console', name: 'Spectral Console', desc: 'Default — dark real-time console: left icon rail, ⌘K palette, bento overview, amber + cyan flow.' },
|
|
24
|
+
{ id: 'orbit', name: 'Orbit', desc: 'Light, airy, circular — a radial menu opens on click of the hub; teal dial + amber brand.' },
|
|
23
25
|
{ id: 'control-room', name: 'Control Room', desc: 'Dark violet engineering control room — the original.' },
|
|
24
26
|
{ id: 'obsidian', name: 'Obsidian Ops', desc: 'Near-black mission-control — electric lime + cyan, mono type, Linear/Vercel precision.' },
|
|
25
27
|
{ id: 'neon-command', name: 'Neon Command', desc: 'Glassmorphism — aurora violet + cyan, Space Grotesk display, control-room ambiance.' },
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" data-theme="dark" data-design="
|
|
2
|
+
<html lang="en" data-theme="dark" data-design="console">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>spectoflow · control</title>
|
|
7
|
-
<link rel="icon" type="image/png" href="logo-
|
|
7
|
+
<link rel="icon" type="image/png" href="logo-dark.png" />
|
|
8
|
+
<link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="logo-white.png" />
|
|
8
9
|
<link rel="stylesheet" href="styles.css" />
|
|
10
|
+
<link rel="stylesheet" href="designs/console.css" />
|
|
11
|
+
<link rel="stylesheet" href="designs/orbit.css" />
|
|
9
12
|
</head>
|
|
10
13
|
<body>
|
|
11
14
|
<header class="topbar">
|
|
@@ -309,5 +312,7 @@
|
|
|
309
312
|
<script src="icons.js"></script>
|
|
310
313
|
<script src="designs.js"></script>
|
|
311
314
|
<script src="app.js"></script>
|
|
315
|
+
<script src="designs/console.js"></script>
|
|
316
|
+
<script src="designs/orbit.js"></script>
|
|
312
317
|
</body>
|
|
313
318
|
</html>
|