@grainulation/orchard 1.0.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/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/orchard.js +209 -0
- package/lib/assignments.js +98 -0
- package/lib/conflicts.js +150 -0
- package/lib/dashboard.js +291 -0
- package/lib/doctor.js +137 -0
- package/lib/export.js +98 -0
- package/lib/farmer.js +107 -0
- package/lib/planner.js +198 -0
- package/lib/server.js +707 -0
- package/lib/sync.js +100 -0
- package/lib/tracker.js +124 -0
- package/package.json +50 -0
- package/public/index.html +922 -0
- package/templates/dashboard.html +981 -0
- package/templates/orchard-dashboard.html +171 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
|
+
<meta name="theme-color" content="#0a0e1a">
|
|
9
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' rx='14' fill='%230a0e1a'/><text x='32' y='34' text-anchor='middle' dominant-baseline='central' fill='%23d4a574' font-family='-apple-system,system-ui,sans-serif' font-weight='800' font-size='32'>O</text></svg>">
|
|
10
|
+
<title>Orchard</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #0a0e1a; --bg-s: #111827; --bg-e: #1a2332; --bg-h: #1e293b;
|
|
14
|
+
--bdr: #1e293b; --bdr-f: #334155;
|
|
15
|
+
--txt: #e2e8f0; --txt-m: #94a3b8; --txt-d: #8494a7;
|
|
16
|
+
--acc: #d4a574; --acc-d: #6a5239;
|
|
17
|
+
--grn: #22c55e; --grn-d: rgba(34,197,94,0.12);
|
|
18
|
+
--ylw: #eab308; --ylw-d: rgba(234,179,8,0.08);
|
|
19
|
+
--org: #f97316; --org-d: rgba(249,115,22,0.1);
|
|
20
|
+
--red: #ef4444; --red-d: rgba(239,68,68,0.1);
|
|
21
|
+
--pur: #a855f7; --pur-d: rgba(168,85,247,0.1);
|
|
22
|
+
--cyn: #06b6d4; --cyn-d: rgba(6,182,212,0.1);
|
|
23
|
+
}
|
|
24
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
25
|
+
body{font-family:'SF Mono','Fira Code','JetBrains Mono',monospace;background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,rgba(59,130,246,0.08) 0%,transparent 60%),radial-gradient(ellipse at 80% 20%,rgba(167,139,250,0.06) 0%,transparent 50%);color:var(--txt);line-height:1.6;overflow-y:auto}
|
|
26
|
+
|
|
27
|
+
.skip-link{position:absolute;top:-100px;left:0;background:var(--acc);color:#fff;padding:8px 16px;z-index:1000;font-size:12px;text-decoration:none;border-radius:0 0 4px 0}
|
|
28
|
+
.skip-link:focus{top:0}
|
|
29
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
30
|
+
:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
|
|
31
|
+
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|
|
32
|
+
|
|
33
|
+
.app{max-width:1200px;margin:0 auto;padding:12px 24px;padding-left:calc(24px + env(safe-area-inset-left));padding-right:calc(24px + env(safe-area-inset-right));padding-bottom:calc(12px + env(safe-area-inset-bottom));overflow-x:hidden}
|
|
34
|
+
.main-grid{display:grid;grid-template-columns:1fr 340px;gap:16px;height:calc(100vh - 48px - 60px - 48px - 16px);height:calc(100dvh - 48px - 60px - 48px - 16px);min-height:400px}
|
|
35
|
+
.main-grid>*{overflow-y:auto;overflow-x:hidden}
|
|
36
|
+
|
|
37
|
+
/* Toolbar */
|
|
38
|
+
.toolbar{position:sticky;top:0;z-index:100;background:rgba(255,255,255,0.08);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--bdr);padding:4px 24px;padding-top:calc(4px + env(safe-area-inset-top));padding-left:calc(24px + env(safe-area-inset-left));padding-right:calc(24px + env(safe-area-inset-right));display:flex;align-items:center;gap:10px}
|
|
39
|
+
.toolbar canvas{flex-shrink:0}
|
|
40
|
+
.tool-status{font-size:10px;color:var(--txt-d);transition:color 0.3s}
|
|
41
|
+
.toolbar-spacer{flex:1}
|
|
42
|
+
.toolbar-right{display:flex;align-items:center;gap:8px}
|
|
43
|
+
.sprint-select{padding:6px 28px 6px 12px;border-radius:6px;background:#111827;border:1px solid #1e293b;color:#e2e8f0;font-size:12px;font-family:inherit;outline:none;cursor:pointer;max-width:320px;text-overflow:ellipsis;-webkit-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center}
|
|
44
|
+
.status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--txt-d);transition:background 0.3s,box-shadow 0.3s}
|
|
45
|
+
.status-dot.ok{background:#34c759;box-shadow:0 0 6px rgba(52,199,89,0.5)}
|
|
46
|
+
header .meta{color:var(--txt-d);font-size:11px;display:flex;gap:16px;flex-wrap:wrap;flex:1;display:none}
|
|
47
|
+
|
|
48
|
+
/* Hero -- compact bar */
|
|
49
|
+
.hero{background:var(--bg-s);border:1px solid var(--bdr);border-radius:4px;padding:10px 16px;margin-bottom:12px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
50
|
+
.hero-score{font-size:28px;font-weight:700;line-height:1;letter-spacing:-1px;flex-shrink:0}
|
|
51
|
+
.hero-detail{flex:1;min-width:0}
|
|
52
|
+
.hero-label{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:0.8px;color:var(--txt-d);display:none}
|
|
53
|
+
.hero-prose{font-size:11px;color:var(--txt-m);line-height:1.5}
|
|
54
|
+
.hero-prose strong{color:var(--txt)}
|
|
55
|
+
.hero-kpis{display:flex;gap:16px;font-size:10px;color:var(--txt-d)}
|
|
56
|
+
.hero-kpis strong{color:var(--txt-m)}
|
|
57
|
+
.status-label{font-size:8px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;margin-left:4px}
|
|
58
|
+
.status-label-good{color:var(--grn)}.status-label-warn{color:var(--ylw)}.status-label-critical{color:var(--red)}
|
|
59
|
+
|
|
60
|
+
/* KPI -- hidden, merged into hero */
|
|
61
|
+
.kpi-row{display:none}
|
|
62
|
+
|
|
63
|
+
/* Stat tiles */
|
|
64
|
+
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:12px}
|
|
65
|
+
.stat-tile{background:var(--bg-s);border:1px solid var(--bdr);border-radius:4px;padding:10px 14px}
|
|
66
|
+
.stat-tile-value{font-size:24px;font-weight:700;line-height:1.1;letter-spacing:-0.5px}
|
|
67
|
+
.stat-tile-label{font-size:9px;color:var(--txt-d);text-transform:uppercase;letter-spacing:0.5px;margin-top:2px}
|
|
68
|
+
.stat-tile-accent .stat-tile-value{color:var(--acc)}
|
|
69
|
+
.stat-tile-green .stat-tile-value{color:var(--grn)}
|
|
70
|
+
.stat-tile-red .stat-tile-value{color:var(--red)}
|
|
71
|
+
.stat-tile-cyan .stat-tile-value{color:var(--cyn)}
|
|
72
|
+
@media(max-width:600px){.stats-grid{grid-template-columns:repeat(3,1fr);gap:6px}.stat-tile{padding:8px 10px}.stat-tile-value{font-size:18px}}
|
|
73
|
+
|
|
74
|
+
/* Section */
|
|
75
|
+
.section{margin-bottom:8px}
|
|
76
|
+
.section-title{font-size:11px;font-weight:600;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--bdr);display:flex;align-items:center;gap:8px}
|
|
77
|
+
.section-title .count{color:var(--txt-d);font-size:10px;font-weight:400}
|
|
78
|
+
|
|
79
|
+
/* Graph */
|
|
80
|
+
.graph-wrap{position:relative;background:var(--bg-s);border:1px solid var(--bdr);border-radius:4px;overflow:hidden;cursor:grab;flex:1}
|
|
81
|
+
.graph-wrap:active{cursor:grabbing}
|
|
82
|
+
.graph-canvas{position:relative}
|
|
83
|
+
.graph-svg{position:absolute;top:0;left:0;pointer-events:none}
|
|
84
|
+
|
|
85
|
+
/* Graph nodes */
|
|
86
|
+
.g-node{position:absolute;background:var(--bg-e);border:1.5px solid var(--bdr-f);border-radius:4px;padding:6px 10px;width:140px;cursor:pointer;z-index:2;transition:border-color 0.15s,box-shadow 0.15s}
|
|
87
|
+
.g-node:hover{border-color:var(--acc);box-shadow:0 0 12px rgba(59,130,246,0.15)}
|
|
88
|
+
.g-node.selected{border-color:var(--acc);box-shadow:0 0 16px rgba(59,130,246,0.25)}
|
|
89
|
+
.g-node.in-cycle{border-color:var(--red);box-shadow:0 0 10px rgba(239,68,68,0.2)}
|
|
90
|
+
.g-node .n-name{font-size:11px;font-weight:700;color:var(--txt);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:4px}
|
|
91
|
+
.g-node .n-meta{display:flex;gap:6px;align-items:center;font-size:9px;color:var(--txt-m)}
|
|
92
|
+
.g-node .n-bar{height:3px;background:var(--bg-h);border-radius:2px;margin-top:6px;overflow:hidden}
|
|
93
|
+
.g-node .n-bar-fill{height:100%;border-radius:2px}
|
|
94
|
+
|
|
95
|
+
/* Phase badges */
|
|
96
|
+
.ph{font-size:8px;padding:1px 5px;border-radius:2px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px}
|
|
97
|
+
.ph-define{background:var(--pur-d);color:var(--pur)}
|
|
98
|
+
.ph-research{background:var(--acc-d);color:var(--acc)}
|
|
99
|
+
.ph-prototype{background:var(--grn-d);color:var(--grn)}
|
|
100
|
+
.ph-evaluate{background:var(--ylw-d);color:var(--ylw)}
|
|
101
|
+
.ph-feedback{background:var(--org-d);color:var(--org)}
|
|
102
|
+
.ph-archived{background:rgba(100,116,139,0.12);color:var(--txt-d)}
|
|
103
|
+
.ph-unknown{background:rgba(100,116,139,0.12);color:var(--txt-d)}
|
|
104
|
+
|
|
105
|
+
/* Detail panel */
|
|
106
|
+
.detail-panel{background:var(--bg-s);border:1px solid var(--bdr);border-radius:4px;padding:20px;margin-bottom:24px;display:none}
|
|
107
|
+
.detail-panel.open{display:block}
|
|
108
|
+
.detail-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
|
109
|
+
.detail-header h2{font-size:14px;font-weight:700;flex:1}
|
|
110
|
+
.detail-close{background:none;border:1px solid var(--bdr);color:var(--txt-d);width:44px;height:44px;border-radius:4px;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center}
|
|
111
|
+
.detail-close:hover{border-color:var(--bdr-f);color:var(--txt)}
|
|
112
|
+
.detail-close:active{background:var(--bg-h);transform:scale(0.95)}
|
|
113
|
+
.f-btn:active{transform:scale(0.96);opacity:0.8}
|
|
114
|
+
.s-card:active{border-color:var(--acc);transform:scale(0.99)}
|
|
115
|
+
.g-node:active{border-color:var(--acc);transform:scale(0.97)}
|
|
116
|
+
.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
117
|
+
.detail-stat{font-size:11px}
|
|
118
|
+
.detail-stat .dl{color:var(--txt-m);font-size:10px;text-transform:uppercase;letter-spacing:0.4px;margin-bottom:2px}
|
|
119
|
+
.detail-stat .dv{font-weight:600;color:var(--txt)}
|
|
120
|
+
.detail-question{font-size:12px;color:var(--txt-m);margin-top:12px;padding:12px;background:var(--bg-e);border-radius:4px;line-height:1.5}
|
|
121
|
+
.detail-edges{margin-top:16px}
|
|
122
|
+
.detail-edge{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:11px;border-bottom:1px solid var(--bdr)}
|
|
123
|
+
.detail-edge:last-child{border-bottom:none}
|
|
124
|
+
|
|
125
|
+
/* Filter */
|
|
126
|
+
.filter-row{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center}
|
|
127
|
+
.filter-row .label{font-size:10px;color:var(--txt-d);text-transform:uppercase;letter-spacing:0.4px}
|
|
128
|
+
.f-btn{background:var(--bg-s);border:1px solid var(--bdr);color:var(--txt-m);padding:8px 12px;border-radius:3px;font-family:inherit;font-size:10px;cursor:pointer;min-height:36px}
|
|
129
|
+
.f-btn:hover{border-color:var(--bdr-f);color:var(--txt)}
|
|
130
|
+
.f-btn.active{border-color:var(--acc);color:var(--acc);background:var(--acc-d)}
|
|
131
|
+
|
|
132
|
+
/* Sprint cards -- right panel, single column */
|
|
133
|
+
.sprint-cards{display:flex;flex-direction:column;gap:8px}
|
|
134
|
+
.s-card{background:var(--bg-s);border:1px solid var(--bdr);border-radius:4px;padding:10px 12px;cursor:pointer;transition:border-color 0.15s}
|
|
135
|
+
.s-card:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
|
|
136
|
+
.s-card:hover{border-color:var(--bdr-f)}
|
|
137
|
+
.s-card.in-cycle{border-left:3px solid var(--red)}
|
|
138
|
+
.s-card-name{font-size:12px;font-weight:700;color:var(--txt);margin-bottom:6px;display:flex;align-items:center;gap:8px;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
139
|
+
.s-card-question{font-size:10px;color:var(--txt-d);line-height:1.4;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
140
|
+
.s-card-stats{display:flex;gap:12px;font-size:10px;color:var(--txt-d);flex-wrap:wrap}
|
|
141
|
+
.s-card-stats strong{color:var(--txt-m)}
|
|
142
|
+
.s-card-bar{height:3px;background:var(--bg-h);border-radius:2px;margin-top:8px;overflow:hidden;display:flex}
|
|
143
|
+
.s-card-bar span{height:100%}
|
|
144
|
+
|
|
145
|
+
/* Edge type badges */
|
|
146
|
+
.e-badge{font-size:8px;padding:1px 5px;border-radius:2px;font-weight:600;text-transform:uppercase}
|
|
147
|
+
.e-resolved_by{background:var(--grn-d);color:var(--grn)}
|
|
148
|
+
.e-conflict{background:var(--red-d);color:var(--red)}
|
|
149
|
+
.e-reference{background:var(--acc-d);color:var(--acc)}
|
|
150
|
+
|
|
151
|
+
/* Collapsible */
|
|
152
|
+
.collapsible{margin-bottom:24px;border:1px solid var(--bdr);border-radius:4px;overflow:hidden}
|
|
153
|
+
.collapsible-header{padding:12px 16px;background:var(--bg-s);cursor:pointer;display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;user-select:none}
|
|
154
|
+
.collapsible-header:hover{background:var(--bg-e)}
|
|
155
|
+
.collapsible-header .arrow{color:var(--txt-d);font-size:10px;transition:transform 0.2s}
|
|
156
|
+
.collapsible.open .arrow{transform:rotate(90deg)}
|
|
157
|
+
.collapsible-body{display:none;padding:16px;border-top:1px solid var(--bdr)}
|
|
158
|
+
.collapsible.open .collapsible-body{display:block}
|
|
159
|
+
|
|
160
|
+
footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--bdr);color:var(--txt-d);font-size:10px;display:flex;justify-content:space-between}
|
|
161
|
+
.grain-idle { display: block; margin: 0 auto 10px; }
|
|
162
|
+
|
|
163
|
+
/* Mobile: stack layout, scrollable graph */
|
|
164
|
+
@media(max-width:768px){
|
|
165
|
+
.toolbar{padding:4px 12px}
|
|
166
|
+
.app{padding:8px 12px}
|
|
167
|
+
.main-grid{grid-template-columns:1fr;height:auto;min-height:0}
|
|
168
|
+
.main-grid>*{overflow-y:visible;overflow-x:hidden}
|
|
169
|
+
#graph-section{order:2}
|
|
170
|
+
#cards-section{order:1}
|
|
171
|
+
.graph-wrap{overflow:auto;-webkit-overflow-scrolling:touch;min-height:300px;max-height:50vh}
|
|
172
|
+
.hero{padding:8px 12px;gap:10px}
|
|
173
|
+
.hero-score{font-size:22px}
|
|
174
|
+
.hero-kpis{flex-wrap:wrap;gap:8px}
|
|
175
|
+
.detail-panel{padding:12px}
|
|
176
|
+
.detail-grid{grid-template-columns:1fr}
|
|
177
|
+
.g-node{width:110px;padding:4px 8px}
|
|
178
|
+
.g-node .n-name{font-size:10px}
|
|
179
|
+
.g-node .n-meta{font-size:8px}
|
|
180
|
+
.s-card{padding:8px 10px}
|
|
181
|
+
.filter-row{gap:4px}
|
|
182
|
+
.f-btn{padding:2px 6px;font-size:9px}
|
|
183
|
+
.section-title{font-size:10px}
|
|
184
|
+
.sprint-select{max-width:180px}
|
|
185
|
+
}
|
|
186
|
+
/* Small phones */
|
|
187
|
+
@media(max-width:480px){
|
|
188
|
+
.toolbar{padding:4px 8px;gap:6px}
|
|
189
|
+
.app{padding:6px 8px}
|
|
190
|
+
.hero-score{font-size:18px}
|
|
191
|
+
.hero-kpis{font-size:9px}
|
|
192
|
+
.g-node{width:90px;padding:3px 6px}
|
|
193
|
+
.g-node .n-name{font-size:9px}
|
|
194
|
+
.sprint-select{max-width:140px}
|
|
195
|
+
}
|
|
196
|
+
</style>
|
|
197
|
+
</head>
|
|
198
|
+
<body>
|
|
199
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
200
|
+
|
|
201
|
+
<header class="toolbar" role="banner">
|
|
202
|
+
<canvas id="grainLogo" width="256" height="256"></canvas>
|
|
203
|
+
<div class="toolbar-spacer"></div>
|
|
204
|
+
<div class="toolbar-right">
|
|
205
|
+
<select id="sprintSelector" class="sprint-select" aria-label="Select sprint to inspect"></select>
|
|
206
|
+
<div class="status-dot" id="statusDot"></div>
|
|
207
|
+
</div>
|
|
208
|
+
</header>
|
|
209
|
+
|
|
210
|
+
<div class="app">
|
|
211
|
+
<main id="main-content">
|
|
212
|
+
|
|
213
|
+
<!-- Hero -- compact summary bar -->
|
|
214
|
+
<div class="hero" id="hero" role="region" aria-label="Ecosystem health overview"></div>
|
|
215
|
+
|
|
216
|
+
<!-- Stats tiles -->
|
|
217
|
+
<div class="stats-grid" id="stats-grid" role="region" aria-label="Ecosystem statistics"></div>
|
|
218
|
+
|
|
219
|
+
<!-- Detail panel (overlay when sprint selected) -->
|
|
220
|
+
<div class="detail-panel" id="detail" role="region" aria-label="Sprint detail"></div>
|
|
221
|
+
|
|
222
|
+
<!-- Two-column: graph left, sprint list right -->
|
|
223
|
+
<div class="main-grid">
|
|
224
|
+
<div id="graph-section" role="region" aria-label="Dependency graph" style="display:flex;flex-direction:column">
|
|
225
|
+
<div class="section-title" style="flex-shrink:0">Dependency Graph <span class="count" id="graph-count"></span></div>
|
|
226
|
+
<div class="graph-wrap" id="graph-wrap">
|
|
227
|
+
<div class="graph-canvas" id="graph-canvas">
|
|
228
|
+
<svg class="graph-svg" id="graph-svg"></svg>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div id="cards-section" style="display:flex;flex-direction:column">
|
|
233
|
+
<div class="section-title" style="flex-shrink:0">Sprints <span class="count" id="cards-count"></span></div>
|
|
234
|
+
<div class="filter-row" id="filter-row" style="flex-shrink:0"></div>
|
|
235
|
+
<div class="sprint-cards" id="sprint-cards" style="flex:1;overflow-y:auto"></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<!-- Hidden KPIs (data still computed) -->
|
|
240
|
+
<div class="kpi-row" id="kpis" role="region" aria-label="Key metrics"></div>
|
|
241
|
+
|
|
242
|
+
</main>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<script>
|
|
246
|
+
var D = __SPRINT_DATA__;
|
|
247
|
+
var sprints = D.sprints || [];
|
|
248
|
+
var edges = D.edges || [];
|
|
249
|
+
var cycles = D.cycles || [];
|
|
250
|
+
var statuses = D.status || [];
|
|
251
|
+
|
|
252
|
+
// Index helpers
|
|
253
|
+
var statusByPath = {};
|
|
254
|
+
statuses.forEach(function(s) { statusByPath[s.path] = s; });
|
|
255
|
+
|
|
256
|
+
var cycleSet = {};
|
|
257
|
+
cycles.forEach(function(c) { c.forEach(function(n) { cycleSet[n] = true; }); });
|
|
258
|
+
|
|
259
|
+
function shortName(p) {
|
|
260
|
+
var parts = (p || '').replace(/\\/g, '/').split('/');
|
|
261
|
+
var name = parts[parts.length - 1] || parts[parts.length - 2] || p;
|
|
262
|
+
return name === '.' ? '(root)' : name;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function phaseClass(p) { return 'ph ph-' + (p || 'unknown').toLowerCase(); }
|
|
266
|
+
function isInCycle(path) {
|
|
267
|
+
var name = shortName(path);
|
|
268
|
+
return cycleSet[name] || cycleSet[path] || false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Compute ecosystem health
|
|
272
|
+
var totalClaims = 0, totalActive = 0, totalResolved = 0, totalSuperseded = 0;
|
|
273
|
+
var activeSprints = 0, phases = {};
|
|
274
|
+
statuses.forEach(function(s) {
|
|
275
|
+
totalClaims += s.total;
|
|
276
|
+
totalActive += s.active;
|
|
277
|
+
totalResolved += s.resolved;
|
|
278
|
+
totalSuperseded += s.superseded;
|
|
279
|
+
var ph = (s.phase || 'unknown').toLowerCase();
|
|
280
|
+
if (ph !== 'archived') activeSprints++;
|
|
281
|
+
phases[ph] = (phases[ph] || 0) + 1;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
var resolutionRate = totalClaims > 0 ? totalResolved / totalClaims : 0;
|
|
285
|
+
var activeRate = totalClaims > 0 ? totalActive / totalClaims : 0;
|
|
286
|
+
var connectivity = edges.length > 0 ? Math.min(edges.length / sprints.length, 3) / 3 : 0;
|
|
287
|
+
var phaseSpread = Math.min(Object.keys(phases).length / 4, 1);
|
|
288
|
+
var cyclesPenalty = cycles.length > 0 ? Math.min(cycles.length * 0.1, 0.3) : 0;
|
|
289
|
+
|
|
290
|
+
var health = Math.round(
|
|
291
|
+
resolutionRate * 25 +
|
|
292
|
+
activeRate * 20 +
|
|
293
|
+
connectivity * 25 +
|
|
294
|
+
phaseSpread * 20 +
|
|
295
|
+
(1 - cyclesPenalty) * 10
|
|
296
|
+
);
|
|
297
|
+
health = Math.max(0, Math.min(100, health));
|
|
298
|
+
|
|
299
|
+
// Size confidence: penalize scores from tiny datasets
|
|
300
|
+
var sizeConfidence = totalClaims < 20 ? 0.6 : totalClaims < 50 ? 0.8 : totalClaims < 100 ? 0.9 : 1.0;
|
|
301
|
+
health = Math.round(health * sizeConfidence);
|
|
302
|
+
var lowConfidence = sizeConfidence < 1.0;
|
|
303
|
+
|
|
304
|
+
function healthColor(h) { return h >= 70 ? 'var(--grn)' : h >= 40 ? 'var(--ylw)' : 'var(--red)'; }
|
|
305
|
+
function healthLabel(h) { return h >= 70 ? 'good' : h >= 40 ? 'warn' : 'critical'; }
|
|
306
|
+
|
|
307
|
+
// Sprint selector dropdown
|
|
308
|
+
var sprintSel = document.getElementById('sprintSelector');
|
|
309
|
+
var selOpt0 = document.createElement('option');
|
|
310
|
+
selOpt0.value = '-1';
|
|
311
|
+
selOpt0.textContent = 'All sprints (' + sprints.length + ')';
|
|
312
|
+
sprintSel.appendChild(selOpt0);
|
|
313
|
+
sprints.forEach(function(s, i) {
|
|
314
|
+
var opt = document.createElement('option');
|
|
315
|
+
opt.value = i;
|
|
316
|
+
var name = shortName(s.path);
|
|
317
|
+
var ph = (s.phase || '').toLowerCase();
|
|
318
|
+
var prefix = ph === 'archived' ? '[archived] ' : '';
|
|
319
|
+
opt.textContent = prefix + name + ' (' + s.claimCount + ')';
|
|
320
|
+
sprintSel.appendChild(opt);
|
|
321
|
+
});
|
|
322
|
+
sprintSel.addEventListener('change', function() {
|
|
323
|
+
var idx = parseInt(this.value);
|
|
324
|
+
if (idx >= 0) selectSprint(idx);
|
|
325
|
+
else closeSprint();
|
|
326
|
+
});
|
|
327
|
+
document.getElementById('statusDot').classList.add('ok');
|
|
328
|
+
|
|
329
|
+
// Hero
|
|
330
|
+
var heroLines = [];
|
|
331
|
+
heroLines.push('Tracking <strong>' + sprints.length + '</strong> sprints with <strong>' + totalClaims + '</strong> total claims across the ecosystem.');
|
|
332
|
+
if (cycles.length > 0) heroLines.push('<span style="color:var(--red)">' + cycles.length + ' dependency cycle(s) detected</span> -- circular references between sprints.');
|
|
333
|
+
heroLines.push('Resolution rate: <strong>' + (resolutionRate * 100).toFixed(1) + '%</strong>. ' +
|
|
334
|
+
'Cross-sprint connectivity: <strong>' + edges.length + ' edges</strong> linking ' + activeSprints + ' active sprints.');
|
|
335
|
+
|
|
336
|
+
document.getElementById('hero').innerHTML =
|
|
337
|
+
'<div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap">' +
|
|
338
|
+
'<span class="hero-score" style="color:' + healthColor(health) + '">' + health + '</span>' +
|
|
339
|
+
'<span class="status-label status-label-' + healthLabel(health) + '">' + healthLabel(health) + '</span>' +
|
|
340
|
+
(lowConfidence ? '<span style="font-size:10px;color:var(--txt-d)">(low confidence)</span>' : '') +
|
|
341
|
+
'<span style="color:var(--txt-d);font-size:10px;margin-left:auto">' +
|
|
342
|
+
activeSprints + ' sprints · ' + totalClaims + ' claims · ' + edges.length + ' edges' +
|
|
343
|
+
(cycles.length > 0 ? ' · <span style="color:var(--red)">' + cycles.length + ' cycle' + (cycles.length > 1 ? 's' : '') + '</span>' : '') +
|
|
344
|
+
' · ' + (resolutionRate * 100).toFixed(0) + '% resolved' +
|
|
345
|
+
'</span>' +
|
|
346
|
+
'</div>';
|
|
347
|
+
|
|
348
|
+
// Stats tiles
|
|
349
|
+
document.getElementById('stats-grid').innerHTML = [
|
|
350
|
+
{ v: sprints.length, l: 'Sprints', cls: 'stat-tile-accent' },
|
|
351
|
+
{ v: activeSprints, l: 'Active', cls: 'stat-tile-green' },
|
|
352
|
+
{ v: totalClaims, l: 'Claims', cls: '' },
|
|
353
|
+
{ v: edges.length, l: 'Edges', cls: 'stat-tile-cyan' },
|
|
354
|
+
{ v: cycles.length, l: 'Cycles', cls: cycles.length > 0 ? 'stat-tile-red' : '' },
|
|
355
|
+
{ v: (resolutionRate * 100).toFixed(0) + '%', l: 'Resolved', cls: resolutionRate > 0.5 ? 'stat-tile-green' : 'stat-tile-red' },
|
|
356
|
+
].map(function(s) {
|
|
357
|
+
return '<div class="stat-tile ' + s.cls + '"><div class="stat-tile-value">' + s.v + '</div><div class="stat-tile-label">' + s.l + '</div></div>';
|
|
358
|
+
}).join('');
|
|
359
|
+
|
|
360
|
+
// KPIs
|
|
361
|
+
var phaseDistHtml = Object.keys(phases).sort().map(function(p) {
|
|
362
|
+
return '<span class="' + phaseClass(p) + '">' + p + '</span> ' + phases[p];
|
|
363
|
+
}).join(' · ');
|
|
364
|
+
|
|
365
|
+
document.getElementById('kpis').innerHTML = [
|
|
366
|
+
{ l: 'Active Sprints', v: activeSprints, s: '/ ' + sprints.length + ' total' },
|
|
367
|
+
{ l: 'Total Claims', v: totalClaims, s: totalActive + ' active, ' + totalResolved + ' resolved' },
|
|
368
|
+
{ l: 'Dependencies', v: edges.length, s: cycles.length > 0 ? cycles.length + ' cycle(s)' : 'no cycles' },
|
|
369
|
+
{ l: 'Phase Distribution', v: '', s: phaseDistHtml },
|
|
370
|
+
].map(function(k) {
|
|
371
|
+
return '<div class="kpi"><div class="kpi-label">' + k.l + '</div>' +
|
|
372
|
+
(k.v !== '' ? '<div class="kpi-value">' + k.v + '</div>' : '') +
|
|
373
|
+
'<div class="kpi-sub">' + k.s + '</div></div>';
|
|
374
|
+
}).join('');
|
|
375
|
+
|
|
376
|
+
// --- GRAPH: container-first layout ---
|
|
377
|
+
var NW = 130, NH = 44;
|
|
378
|
+
|
|
379
|
+
var pathIdx = {};
|
|
380
|
+
sprints.forEach(function(s, i) { pathIdx[s.path] = i; });
|
|
381
|
+
|
|
382
|
+
// Measure container FIRST
|
|
383
|
+
var graphWrap = document.getElementById('graph-wrap');
|
|
384
|
+
var canvas = document.getElementById('graph-canvas');
|
|
385
|
+
var svg = document.getElementById('graph-svg');
|
|
386
|
+
|
|
387
|
+
document.getElementById('graph-count').textContent = sprints.length + ' nodes, ' + edges.length + ' edges';
|
|
388
|
+
|
|
389
|
+
var wrapRect = graphWrap.getBoundingClientRect();
|
|
390
|
+
var GW = Math.max(400, Math.floor(wrapRect.width) - 4);
|
|
391
|
+
var GH = Math.max(300, Math.floor(wrapRect.height) - 4);
|
|
392
|
+
var PAD = 8;
|
|
393
|
+
|
|
394
|
+
// Available space for node centers
|
|
395
|
+
var areaW = GW - NW - PAD * 2;
|
|
396
|
+
var areaH = GH - NH - PAD * 2;
|
|
397
|
+
|
|
398
|
+
// Compute grid: fill available space evenly
|
|
399
|
+
var n = sprints.length;
|
|
400
|
+
var cols = Math.ceil(Math.sqrt(n * (areaW / areaH)));
|
|
401
|
+
var rows = Math.ceil(n / cols);
|
|
402
|
+
// Recalc to avoid wasted rows
|
|
403
|
+
while ((rows - 1) * cols >= n) rows--;
|
|
404
|
+
var cellW = areaW / cols;
|
|
405
|
+
var cellH = areaH / Math.max(rows, 1);
|
|
406
|
+
|
|
407
|
+
// Sort by edge count so hubs land center
|
|
408
|
+
var edgeCount = {};
|
|
409
|
+
edges.forEach(function(e) {
|
|
410
|
+
edgeCount[e.from] = (edgeCount[e.from] || 0) + 1;
|
|
411
|
+
edgeCount[e.to] = (edgeCount[e.to] || 0) + 1;
|
|
412
|
+
});
|
|
413
|
+
var order = sprints.map(function(s, i) { return { i: i, deg: edgeCount[s.path] || 0 }; });
|
|
414
|
+
order.sort(function(a, b) { return b.deg - a.deg; });
|
|
415
|
+
|
|
416
|
+
// Spiral-out placement: hubs get center cells
|
|
417
|
+
var placed = [];
|
|
418
|
+
var cx = Math.floor(cols / 2), cy = Math.floor(rows / 2);
|
|
419
|
+
var grid = {};
|
|
420
|
+
var dirs = [[0,0],[1,0],[0,1],[-1,0],[0,-1]]; // seed + cardinal
|
|
421
|
+
var spiral = [];
|
|
422
|
+
// Generate spiral coordinates from center
|
|
423
|
+
(function() {
|
|
424
|
+
var x = cx, y = cy, dx = 1, dy = 0, steps = 1, stepCount = 0, turnCount = 0;
|
|
425
|
+
for (var k = 0; k < cols * rows + 10; k++) {
|
|
426
|
+
if (x >= 0 && x < cols && y >= 0 && y < rows) spiral.push([x, y]);
|
|
427
|
+
x += dx; y += dy; stepCount++;
|
|
428
|
+
if (stepCount >= steps) {
|
|
429
|
+
stepCount = 0; turnCount++;
|
|
430
|
+
var tmp = dx; dx = -dy; dy = tmp;
|
|
431
|
+
if (turnCount % 2 === 0) steps++;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
})();
|
|
435
|
+
|
|
436
|
+
// Place nodes along spiral
|
|
437
|
+
var nodes = [];
|
|
438
|
+
var si = 0;
|
|
439
|
+
for (var k = 0; k < spiral.length && si < order.length; k++) {
|
|
440
|
+
var gx = spiral[k][0], gy = spiral[k][1];
|
|
441
|
+
var key = gx + ',' + gy;
|
|
442
|
+
if (grid[key]) continue;
|
|
443
|
+
grid[key] = true;
|
|
444
|
+
var oi = order[si].i;
|
|
445
|
+
nodes[oi] = {
|
|
446
|
+
x: PAD + gx * cellW + cellW / 2 - NW / 2,
|
|
447
|
+
y: PAD + gy * cellH + cellH / 2 - NH / 2,
|
|
448
|
+
vx: 0, vy: 0,
|
|
449
|
+
sprint: sprints[oi], idx: oi
|
|
450
|
+
};
|
|
451
|
+
si++;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// No spring nudge — grid stays fixed, edges just connect where nodes are.
|
|
455
|
+
// User can drag to rearrange if needed.
|
|
456
|
+
|
|
457
|
+
canvas.style.width = GW + 'px';
|
|
458
|
+
canvas.style.height = GH + 'px';
|
|
459
|
+
|
|
460
|
+
svg.setAttribute('width', GW);
|
|
461
|
+
svg.setAttribute('height', GH);
|
|
462
|
+
svg.style.width = GW + 'px';
|
|
463
|
+
svg.style.height = GH + 'px';
|
|
464
|
+
|
|
465
|
+
// SVG edges
|
|
466
|
+
var defs = '<defs>' +
|
|
467
|
+
'<marker id="a-ref" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="var(--acc)" opacity="0.6"/></marker>' +
|
|
468
|
+
'<marker id="a-res" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="var(--grn)" opacity="0.6"/></marker>' +
|
|
469
|
+
'<marker id="a-con" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="var(--red)" opacity="0.6"/></marker>' +
|
|
470
|
+
'</defs>';
|
|
471
|
+
|
|
472
|
+
// Edge rendering function (called on drag too)
|
|
473
|
+
function renderEdges() {
|
|
474
|
+
var edgeSvg = '';
|
|
475
|
+
edges.forEach(function(e) {
|
|
476
|
+
var fi = pathIdx[e.from], ti = pathIdx[e.to];
|
|
477
|
+
if (fi === undefined || ti === undefined) return;
|
|
478
|
+
var a = nodes[fi], b = nodes[ti];
|
|
479
|
+
var ax = a.x + NW / 2, ay = a.y + NH / 2;
|
|
480
|
+
var bx = b.x + NW / 2, by = b.y + NH / 2;
|
|
481
|
+
var color = e.type === 'conflict' ? 'var(--red)' : e.type === 'resolved_by' ? 'var(--grn)' : 'var(--acc)';
|
|
482
|
+
var marker = e.type === 'conflict' ? 'a-con' : e.type === 'resolved_by' ? 'a-res' : 'a-ref';
|
|
483
|
+
var opacity = Math.min(0.25 + Math.log2(e.count + 1) * 0.15, 0.85);
|
|
484
|
+
var sw = Math.min(1 + Math.log2(e.count + 1) * 0.5, 3);
|
|
485
|
+
var mx = (ax + bx) / 2 + (ay - by) * 0.15;
|
|
486
|
+
var my = (ay + by) / 2 + (bx - ax) * 0.15;
|
|
487
|
+
edgeSvg += '<path d="M' + ax + ',' + ay + ' Q' + mx + ',' + my + ' ' + bx + ',' + by + '" ' +
|
|
488
|
+
'fill="none" stroke="' + color + '" stroke-width="' + sw + '" stroke-opacity="' + opacity + '" ' +
|
|
489
|
+
'marker-end="url(#' + marker + ')" data-from="' + fi + '" data-to="' + ti + '"/>';
|
|
490
|
+
});
|
|
491
|
+
svg.innerHTML = defs + edgeSvg;
|
|
492
|
+
}
|
|
493
|
+
renderEdges();
|
|
494
|
+
|
|
495
|
+
// DOM nodes
|
|
496
|
+
var maxClaims = Math.max.apply(null, sprints.map(function(s) { return s.claimCount; }).concat([1]));
|
|
497
|
+
var nodeEls = [];
|
|
498
|
+
nodes.forEach(function(n, i) {
|
|
499
|
+
var s = n.sprint;
|
|
500
|
+
var st = statusByPath[s.path] || {};
|
|
501
|
+
var name = shortName(s.path);
|
|
502
|
+
var phase = (s.phase || 'unknown').toLowerCase();
|
|
503
|
+
var cycle = isInCycle(s.path) ? ' in-cycle' : '';
|
|
504
|
+
var pct = (s.claimCount / maxClaims) * 100;
|
|
505
|
+
var barColor = phase === 'archived' ? 'var(--txt-d)' : phase === 'research' ? 'var(--acc)' :
|
|
506
|
+
phase === 'prototype' ? 'var(--grn)' : phase === 'evaluate' ? 'var(--ylw)' : 'var(--pur)';
|
|
507
|
+
var div = document.createElement('div');
|
|
508
|
+
div.className = 'g-node' + cycle;
|
|
509
|
+
div.setAttribute('data-idx', i);
|
|
510
|
+
div.style.left = Math.round(n.x) + 'px';
|
|
511
|
+
div.style.top = Math.round(n.y) + 'px';
|
|
512
|
+
div.title = (s.question || '').replace(/"/g, '"');
|
|
513
|
+
div.innerHTML = '<div class="n-name">' + name + '</div>' +
|
|
514
|
+
'<div class="n-meta"><span class="' + phaseClass(phase) + '">' + s.phase + '</span><span>' + s.claimCount + '</span></div>' +
|
|
515
|
+
'<div class="n-bar"><div class="n-bar-fill" style="width:' + pct + '%;background:' + barColor + '"></div></div>';
|
|
516
|
+
canvas.appendChild(div);
|
|
517
|
+
nodeEls.push(div);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// --- DRAG TO MOVE NODES ---
|
|
521
|
+
var _dragIdx = -1, _dragOX = 0, _dragOY = 0, _didDrag = false;
|
|
522
|
+
|
|
523
|
+
canvas.addEventListener('mousedown', function(ev) {
|
|
524
|
+
var el = ev.target.closest('.g-node');
|
|
525
|
+
if (!el) return;
|
|
526
|
+
_dragIdx = parseInt(el.getAttribute('data-idx'));
|
|
527
|
+
var r = graphWrap.getBoundingClientRect();
|
|
528
|
+
_dragOX = ev.clientX - nodes[_dragIdx].x - r.left;
|
|
529
|
+
_dragOY = ev.clientY - nodes[_dragIdx].y - r.top;
|
|
530
|
+
_didDrag = false;
|
|
531
|
+
el.style.zIndex = 10;
|
|
532
|
+
el.style.cursor = 'grabbing';
|
|
533
|
+
ev.preventDefault();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
document.addEventListener('mousemove', function(ev) {
|
|
537
|
+
if (_dragIdx < 0) return;
|
|
538
|
+
_didDrag = true;
|
|
539
|
+
var r = graphWrap.getBoundingClientRect();
|
|
540
|
+
var nx = ev.clientX - _dragOX - r.left;
|
|
541
|
+
var ny = ev.clientY - _dragOY - r.top;
|
|
542
|
+
nx = Math.max(0, Math.min(GW - NW, nx));
|
|
543
|
+
ny = Math.max(0, Math.min(GH - NH, ny));
|
|
544
|
+
nodes[_dragIdx].x = nx;
|
|
545
|
+
nodes[_dragIdx].y = ny;
|
|
546
|
+
nodeEls[_dragIdx].style.left = Math.round(nx) + 'px';
|
|
547
|
+
nodeEls[_dragIdx].style.top = Math.round(ny) + 'px';
|
|
548
|
+
renderEdges();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
document.addEventListener('mouseup', function(ev) {
|
|
552
|
+
if (_dragIdx < 0) return;
|
|
553
|
+
nodeEls[_dragIdx].style.zIndex = 2;
|
|
554
|
+
nodeEls[_dragIdx].style.cursor = 'pointer';
|
|
555
|
+
if (!_didDrag) selectSprint(_dragIdx);
|
|
556
|
+
_dragIdx = -1;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Touch events for mobile drag
|
|
560
|
+
canvas.addEventListener('touchstart', function(ev) {
|
|
561
|
+
var touch = ev.touches[0];
|
|
562
|
+
var el = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
563
|
+
if (el) el = el.closest('.g-node');
|
|
564
|
+
if (!el) return;
|
|
565
|
+
_dragIdx = parseInt(el.getAttribute('data-idx'));
|
|
566
|
+
var r = graphWrap.getBoundingClientRect();
|
|
567
|
+
_dragOX = touch.clientX - nodes[_dragIdx].x - r.left;
|
|
568
|
+
_dragOY = touch.clientY - nodes[_dragIdx].y - r.top;
|
|
569
|
+
_didDrag = false;
|
|
570
|
+
el.style.zIndex = 10;
|
|
571
|
+
}, { passive: true });
|
|
572
|
+
|
|
573
|
+
canvas.addEventListener('touchmove', function(ev) {
|
|
574
|
+
if (_dragIdx < 0) return;
|
|
575
|
+
ev.preventDefault();
|
|
576
|
+
_didDrag = true;
|
|
577
|
+
var touch = ev.touches[0];
|
|
578
|
+
var r = graphWrap.getBoundingClientRect();
|
|
579
|
+
var nx = touch.clientX - _dragOX - r.left;
|
|
580
|
+
var ny = touch.clientY - _dragOY - r.top;
|
|
581
|
+
nx = Math.max(0, Math.min(GW - NW, nx));
|
|
582
|
+
ny = Math.max(0, Math.min(GH - NH, ny));
|
|
583
|
+
nodes[_dragIdx].x = nx;
|
|
584
|
+
nodes[_dragIdx].y = ny;
|
|
585
|
+
nodeEls[_dragIdx].style.left = Math.round(nx) + 'px';
|
|
586
|
+
nodeEls[_dragIdx].style.top = Math.round(ny) + 'px';
|
|
587
|
+
renderEdges();
|
|
588
|
+
}, { passive: false });
|
|
589
|
+
|
|
590
|
+
canvas.addEventListener('touchend', function(ev) {
|
|
591
|
+
if (_dragIdx < 0) return;
|
|
592
|
+
nodeEls[_dragIdx].style.zIndex = 2;
|
|
593
|
+
if (!_didDrag) selectSprint(_dragIdx);
|
|
594
|
+
_dragIdx = -1;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// --- DETAIL PANEL ---
|
|
598
|
+
var selectedIdx = -1;
|
|
599
|
+
|
|
600
|
+
function selectSprint(idx) {
|
|
601
|
+
var panel = document.getElementById('detail');
|
|
602
|
+
if (selectedIdx === idx) { panel.classList.remove('open'); selectedIdx = -1; clearSelection(); sprintSel.value = '-1'; return; }
|
|
603
|
+
selectedIdx = idx;
|
|
604
|
+
sprintSel.value = idx;
|
|
605
|
+
|
|
606
|
+
// Highlight node
|
|
607
|
+
clearSelection();
|
|
608
|
+
var nodeEl = document.querySelector('.g-node[data-idx="' + idx + '"]');
|
|
609
|
+
if (nodeEl) nodeEl.classList.add('selected');
|
|
610
|
+
|
|
611
|
+
var s = sprints[idx];
|
|
612
|
+
var st = statusByPath[s.path] || {};
|
|
613
|
+
var name = shortName(s.path);
|
|
614
|
+
var inEdges = edges.filter(function(e) { return e.to === s.path; });
|
|
615
|
+
var outEdges = edges.filter(function(e) { return e.from === s.path; });
|
|
616
|
+
|
|
617
|
+
var html = '<div class="detail-header">' +
|
|
618
|
+
'<h2>' + name + '</h2>' +
|
|
619
|
+
'<span class="' + phaseClass(s.phase) + '">' + s.phase + '</span>' +
|
|
620
|
+
'<button class="detail-close" onclick="closeSprint()" aria-label="Close detail">×</button>' +
|
|
621
|
+
'</div>';
|
|
622
|
+
|
|
623
|
+
html += '<div class="detail-grid">' +
|
|
624
|
+
'<div class="detail-stat"><div class="dl">Claims</div><div class="dv">' + s.claimCount + '</div></div>' +
|
|
625
|
+
'<div class="detail-stat"><div class="dl">Active / Resolved / Superseded</div><div class="dv" style="color:var(--grn)">' +
|
|
626
|
+
(st.active || 0) + '<span style="color:var(--txt-d)"> / </span><span style="color:var(--txt-m)">' +
|
|
627
|
+
(st.resolved || 0) + '</span><span style="color:var(--txt-d)"> / </span><span style="color:var(--txt-d)">' +
|
|
628
|
+
(st.superseded || 0) + '</span></div></div>' +
|
|
629
|
+
'<div class="detail-stat"><div class="dl">Initiated</div><div class="dv">' + (s.initiated || '--') + '</div></div>' +
|
|
630
|
+
'<div class="detail-stat"><div class="dl">Days Since Update</div><div class="dv">' +
|
|
631
|
+
(st.daysSinceUpdate != null ? st.daysSinceUpdate + 'd' : '--') + '</div></div>' +
|
|
632
|
+
'<div class="detail-stat"><div class="dl">Topics</div><div class="dv">' + (s.topics || []).length + '</div></div>' +
|
|
633
|
+
'<div class="detail-stat"><div class="dl">Inbound / Outbound Edges</div><div class="dv">' + inEdges.length + ' in / ' + outEdges.length + ' out</div></div>' +
|
|
634
|
+
'</div>';
|
|
635
|
+
|
|
636
|
+
if (s.question) {
|
|
637
|
+
html += '<div class="detail-question">' + s.question + '</div>';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (inEdges.length + outEdges.length > 0) {
|
|
641
|
+
html += '<div class="detail-edges"><div style="font-size:10px;font-weight:600;text-transform:uppercase;color:var(--txt-d);margin-bottom:6px">Connections</div>';
|
|
642
|
+
inEdges.forEach(function(e) {
|
|
643
|
+
html += '<div class="detail-edge"><span class="e-badge e-' + e.type + '">' + e.type + '</span>' +
|
|
644
|
+
'<span style="color:var(--txt-m)">' + shortName(e.from) + '</span>' +
|
|
645
|
+
'<span style="color:var(--txt-d)">--> ' + name + '</span>' +
|
|
646
|
+
'<span style="color:var(--txt-m);font-size:9px">x' + e.count + '</span></div>';
|
|
647
|
+
});
|
|
648
|
+
outEdges.forEach(function(e) {
|
|
649
|
+
html += '<div class="detail-edge"><span class="e-badge e-' + e.type + '">' + e.type + '</span>' +
|
|
650
|
+
'<span style="color:var(--txt)">' + name + '</span>' +
|
|
651
|
+
'<span style="color:var(--txt-m)">--> </span>' +
|
|
652
|
+
'<span style="color:var(--txt-m)">' + shortName(e.to) + '</span>' +
|
|
653
|
+
'<span style="color:var(--txt-m);font-size:9px">x' + e.count + '</span></div>';
|
|
654
|
+
});
|
|
655
|
+
html += '</div>';
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
panel.innerHTML = html;
|
|
659
|
+
panel.classList.add('open');
|
|
660
|
+
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function closeSprint() {
|
|
664
|
+
document.getElementById('detail').classList.remove('open');
|
|
665
|
+
clearSelection();
|
|
666
|
+
selectedIdx = -1;
|
|
667
|
+
sprintSel.value = '-1';
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function clearSelection() {
|
|
671
|
+
document.querySelectorAll('.g-node.selected').forEach(function(el) { el.classList.remove('selected'); });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// --- SPRINT CARDS ---
|
|
675
|
+
var phaseFilter = '__active__';
|
|
676
|
+
var allPhases = [];
|
|
677
|
+
var seenPhases = {};
|
|
678
|
+
statuses.forEach(function(s) {
|
|
679
|
+
var p = (s.phase || 'unknown').toLowerCase();
|
|
680
|
+
if (!seenPhases[p]) { allPhases.push(p); seenPhases[p] = true; }
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
document.getElementById('filter-row').innerHTML =
|
|
684
|
+
'<span class="label">Filter</span>' +
|
|
685
|
+
'<button class="f-btn active" data-filter="__active__" onclick="filterPhase(\'__active__\',this)">Active</button>' +
|
|
686
|
+
'<button class="f-btn" data-filter="__all__" onclick="filterPhase(null,this)">All</button>' +
|
|
687
|
+
allPhases.map(function(p) {
|
|
688
|
+
return '<button class="f-btn" data-filter="' + p + '" onclick="filterPhase(\'' + p + '\',this)">' +
|
|
689
|
+
'<span class="' + phaseClass(p) + '" style="margin-right:4px">' + p + '</span>' +
|
|
690
|
+
phases[p] + '</button>';
|
|
691
|
+
}).join('');
|
|
692
|
+
|
|
693
|
+
function filterPhase(p, btn) {
|
|
694
|
+
phaseFilter = p;
|
|
695
|
+
document.querySelectorAll('.filter-row .f-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
696
|
+
if (btn) btn.classList.add('active');
|
|
697
|
+
renderCards();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function renderCards() {
|
|
701
|
+
var sorted = statuses.slice().sort(function(a, b) {
|
|
702
|
+
var da = a.daysSinceUpdate != null ? a.daysSinceUpdate : -1;
|
|
703
|
+
var db = b.daysSinceUpdate != null ? b.daysSinceUpdate : -1;
|
|
704
|
+
return db - da;
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (phaseFilter === '__active__') {
|
|
708
|
+
sorted = sorted.filter(function(s) { return (s.phase || 'unknown').toLowerCase() !== 'archived'; });
|
|
709
|
+
} else if (phaseFilter) {
|
|
710
|
+
sorted = sorted.filter(function(s) { return (s.phase || 'unknown').toLowerCase() === phaseFilter; });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
document.getElementById('cards-count').textContent = sorted.length + ' sprints';
|
|
714
|
+
|
|
715
|
+
document.getElementById('sprint-cards').innerHTML = sorted.map(function(s) {
|
|
716
|
+
var name = shortName(s.path);
|
|
717
|
+
var phase = (s.phase || 'unknown').toLowerCase();
|
|
718
|
+
var cycle = isInCycle(s.path) ? ' in-cycle' : '';
|
|
719
|
+
var si = pathIdx[s.path];
|
|
720
|
+
var activeW = s.total > 0 ? (s.active / s.total * 100) : 0;
|
|
721
|
+
var resolvedW = s.total > 0 ? (s.resolved / s.total * 100) : 0;
|
|
722
|
+
var supersededW = s.total > 0 ? (s.superseded / s.total * 100) : 0;
|
|
723
|
+
var daysStr = s.daysSinceUpdate != null ? s.daysSinceUpdate + 'd ago' : '';
|
|
724
|
+
var topics = (s.topTopics || []).slice(0, 3).map(function(t) { return t.topic.replace(/-/g, ' '); }).join(', ');
|
|
725
|
+
|
|
726
|
+
return '<div class="s-card' + cycle + '" tabindex="0" role="button" onclick="selectSprint(' + si + ')" onkeydown="if(event.key===\'Enter\')selectSprint(' + si + ')">' +
|
|
727
|
+
'<div class="s-card-name"><span class="' + phaseClass(phase) + '">' + s.phase + '</span>' + name + '</div>' +
|
|
728
|
+
'<div class="s-card-question">' + (s.question || '') + '</div>' +
|
|
729
|
+
'<div class="s-card-stats">' +
|
|
730
|
+
'<span><strong>' + s.total + '</strong> claims</span>' +
|
|
731
|
+
'<span style="color:var(--grn)">' + s.active + ' active</span>' +
|
|
732
|
+
(daysStr ? '<span>' + daysStr + '</span>' : '') +
|
|
733
|
+
'</div>' +
|
|
734
|
+
'<div class="s-card-bar">' +
|
|
735
|
+
'<span style="width:' + activeW + '%;background:var(--grn)"></span>' +
|
|
736
|
+
'<span style="width:' + resolvedW + '%;background:var(--acc)"></span>' +
|
|
737
|
+
'<span style="width:' + supersededW + '%;background:var(--txt-d)"></span>' +
|
|
738
|
+
'</div>' +
|
|
739
|
+
(topics ? '<div style="font-size:9px;color:var(--txt-d);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + topics + '</div>' : '') +
|
|
740
|
+
'</div>';
|
|
741
|
+
}).join('');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
renderCards();
|
|
745
|
+
|
|
746
|
+
// (edges and footer removed -- data visible in graph and hero)
|
|
747
|
+
|
|
748
|
+
// Collapsible
|
|
749
|
+
function toggle(id) {
|
|
750
|
+
var el = document.getElementById(id);
|
|
751
|
+
el.classList.toggle('open');
|
|
752
|
+
el.querySelector('.collapsible-header').setAttribute('aria-expanded', el.classList.contains('open'));
|
|
753
|
+
}
|
|
754
|
+
function toggleKey(e, id) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(id); } }
|
|
755
|
+
|
|
756
|
+
// ── Grain Logo Animation ──
|
|
757
|
+
(function() {
|
|
758
|
+
var LW = 0.025;
|
|
759
|
+
var TOOL = { name: 'Orchard', letter: 'O', color: '#d4a574' };
|
|
760
|
+
var _c, _ctx, _s, _cx, _textStart, _restText, _font;
|
|
761
|
+
var _state = 'drawon', _start = null, _raf;
|
|
762
|
+
var _openPts = null, _closedPts = null;
|
|
763
|
+
|
|
764
|
+
function _lerp(a,b,t){ return {x:a.x+(b.x-a.x)*t, y:a.y+(b.y-a.y)*t}; }
|
|
765
|
+
function _easeInOut(t){ return t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2; }
|
|
766
|
+
|
|
767
|
+
function _bracket(ctx, s, color, alpha) {
|
|
768
|
+
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
769
|
+
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
770
|
+
if(alpha!==undefined) ctx.globalAlpha=alpha;
|
|
771
|
+
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
772
|
+
ctx.beginPath(); ctx.moveTo(cx,topY); ctx.lineTo(cx-fe,topY);
|
|
773
|
+
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
774
|
+
ctx.lineTo(cx,botY); ctx.stroke();
|
|
775
|
+
if(alpha!==undefined) ctx.globalAlpha=1;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function _drawBracket(ctx, s, color, progress) {
|
|
779
|
+
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
780
|
+
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
781
|
+
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
782
|
+
var seg1=0.12, seg2=0.72;
|
|
783
|
+
ctx.beginPath();
|
|
784
|
+
if(progress<=seg1){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe*(progress/seg1),topY);}
|
|
785
|
+
else if(progress<=seg1+seg2){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);ctx.stroke();ctx.beginPath();
|
|
786
|
+
var bt=(progress-seg1)/seg2;
|
|
787
|
+
var p0={x:cx-fe,y:topY},p1={x:cx-gw*0.52,y:cy-gh*0.32},p2={x:cx-gw*0.52,y:cy+gh*0.24},p3={x:cx-fe,y:botY};
|
|
788
|
+
var q1=_lerp(p0,p1,bt),q2=_lerp(p1,p2,bt),q3=_lerp(p2,p3,bt);
|
|
789
|
+
var r1=_lerp(q1,q2,bt),r2=_lerp(q2,q3,bt),s1=_lerp(r1,r2,bt);
|
|
790
|
+
ctx.moveTo(p0.x,p0.y);ctx.bezierCurveTo(q1.x,q1.y,r1.x,r1.y,s1.x,s1.y);}
|
|
791
|
+
else{ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);
|
|
792
|
+
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
793
|
+
ctx.lineTo((cx-fe)+fe*((progress-seg1-seg2)/(1-seg1-seg2)),botY);}
|
|
794
|
+
ctx.stroke();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function _drawName(ctx, s, spellP, alpha) {
|
|
798
|
+
var a = alpha !== undefined ? alpha : 1;
|
|
799
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
800
|
+
var cy = s/2 + s*0.02;
|
|
801
|
+
ctx.globalAlpha = a; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
802
|
+
ctx.fillText(TOOL.letter, _cx, cy);
|
|
803
|
+
if(_restText.length > 0 && spellP > 0) {
|
|
804
|
+
var n = _restText.length, num = Math.min(n, Math.ceil(spellP * n));
|
|
805
|
+
var rawP = spellP * n, charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
806
|
+
var full = charP >= 1 ? num : num - 1;
|
|
807
|
+
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
808
|
+
if(full > 0) { ctx.globalAlpha = a; ctx.fillText(_restText.slice(0, full), _textStart, cy); }
|
|
809
|
+
if(full < num) {
|
|
810
|
+
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
811
|
+
ctx.globalAlpha = a * (0.3 + 0.7 * charP);
|
|
812
|
+
ctx.fillText(_restText[full], _textStart + prevW, cy);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
ctx.globalAlpha = 1;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function _getOpenPts(s) {
|
|
819
|
+
if(_openPts && _openPts._s === s) return _openPts;
|
|
820
|
+
var cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68, topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
821
|
+
var pts=[];
|
|
822
|
+
for(var t=0;t<=1;t+=0.05) pts.push({x:cx-fe*t,y:topY});
|
|
823
|
+
var p0={x:cx-fe,y:topY},p1={x:cx-gw*0.52,y:cy-gh*0.32},p2={x:cx-gw*0.52,y:cy+gh*0.24},p3={x:cx-fe,y:botY};
|
|
824
|
+
for(var t=0;t<=1;t+=0.02){var u=1-t;pts.push({x:u*u*u*p0.x+3*u*u*t*p1.x+3*u*t*t*p2.x+t*t*t*p3.x,y:u*u*u*p0.y+3*u*u*t*p1.y+3*u*t*t*p2.y+t*t*t*p3.y});}
|
|
825
|
+
for(var t=0;t<=1;t+=0.05) pts.push({x:(cx-fe)+fe*t,y:botY});
|
|
826
|
+
pts._s=s; _openPts=pts; return pts;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function _getClosedPts(s) {
|
|
830
|
+
if(_closedPts && _closedPts._s === s) return _closedPts;
|
|
831
|
+
var cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68, topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
832
|
+
var pts=[];
|
|
833
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:cx-fe*t,y:topY});
|
|
834
|
+
var lp0={x:cx-fe,y:topY},lp1={x:cx-gw*0.52,y:cy-gh*0.32},lp2={x:cx-gw*0.52,y:cy+gh*0.24},lp3={x:cx-fe,y:botY};
|
|
835
|
+
for(var t=0;t<=1;t+=0.02){var u=1-t;pts.push({x:u*u*u*lp0.x+3*u*u*t*lp1.x+3*u*t*t*lp2.x+t*t*t*lp3.x,y:u*u*u*lp0.y+3*u*u*t*lp1.y+3*u*t*t*lp2.y+t*t*t*lp3.y});}
|
|
836
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx-fe)+2*fe*t,y:botY});
|
|
837
|
+
var rp0={x:cx+fe,y:botY},rp1={x:cx+gw*0.52,y:cy+gh*0.24},rp2={x:cx+gw*0.52,y:cy-gh*0.32},rp3={x:cx+fe,y:topY};
|
|
838
|
+
for(var t=0;t<=1;t+=0.02){var u=1-t;pts.push({x:u*u*u*rp0.x+3*u*u*t*rp1.x+3*u*t*t*rp2.x+t*t*t*rp3.x,y:u*u*u*rp0.y+3*u*u*t*rp1.y+3*u*t*t*rp2.y+t*t*t*rp3.y});}
|
|
839
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx+fe)-fe*t,y:topY});
|
|
840
|
+
pts._s=s; _closedPts=pts; return pts;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function _frame(ts) {
|
|
844
|
+
if(!_c) return;
|
|
845
|
+
if(!_start) _start = ts;
|
|
846
|
+
var e = ts - _start, ctx = _ctx, s = _s;
|
|
847
|
+
ctx.clearRect(0, 0, _c.width, s);
|
|
848
|
+
switch(_state) {
|
|
849
|
+
case 'drawon':
|
|
850
|
+
var bp = _easeInOut(Math.min(1, e / 1400));
|
|
851
|
+
_drawBracket(ctx, s, TOOL.color, bp);
|
|
852
|
+
var la = Math.max(0, Math.min(1, (e - 900) / 400));
|
|
853
|
+
if(la > 0) {
|
|
854
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
855
|
+
ctx.globalAlpha = la; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
856
|
+
ctx.fillText(TOOL.letter, _cx, s/2 + s*0.02); ctx.globalAlpha = 1;
|
|
857
|
+
}
|
|
858
|
+
if(e > 1300 && _restText.length > 0) {
|
|
859
|
+
var sp = Math.min(1, (e - 1300) / (200 * _restText.length));
|
|
860
|
+
var n = _restText.length, num = Math.min(n, Math.ceil(sp * n));
|
|
861
|
+
if(num > 0) {
|
|
862
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
863
|
+
var cy = s/2 + s*0.02, rawP = sp * n;
|
|
864
|
+
var charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
865
|
+
var full = charP >= 1 ? num : num - 1;
|
|
866
|
+
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
867
|
+
if(full > 0) ctx.fillText(_restText.slice(0, full), _textStart, cy);
|
|
868
|
+
if(full < num) {
|
|
869
|
+
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
870
|
+
ctx.globalAlpha = 0.3 + 0.7 * charP;
|
|
871
|
+
ctx.fillText(_restText[full], _textStart + prevW, cy); ctx.globalAlpha = 1;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if(e > 1300 + 200 * _restText.length + 400) { _state = 'idle'; _start = ts; }
|
|
876
|
+
break;
|
|
877
|
+
case 'idle':
|
|
878
|
+
var breathe = 0.5 + 0.5 * (0.5 + 0.5 * Math.sin(e / 1200));
|
|
879
|
+
_bracket(ctx, s, TOOL.color, breathe);
|
|
880
|
+
var textBreath = 0.88 + 0.12 * (0.5 + 0.5 * Math.sin(e / 1800));
|
|
881
|
+
_drawName(ctx, s, 1, textBreath);
|
|
882
|
+
break;
|
|
883
|
+
case 'shimmer':
|
|
884
|
+
_bracket(ctx, s, TOOL.color, 0.2);
|
|
885
|
+
var spts = _getOpenPts(s), sspeed = 1800;
|
|
886
|
+
var spos = (e % sspeed) / sspeed;
|
|
887
|
+
var sidx = Math.floor(spos * (spts.length - 1));
|
|
888
|
+
var spt = spts[sidx];
|
|
889
|
+
var sgrad = ctx.createRadialGradient(spt.x, spt.y, 0, spt.x, spt.y, s * 0.10);
|
|
890
|
+
sgrad.addColorStop(0, TOOL.color + 'aa'); sgrad.addColorStop(1, 'transparent');
|
|
891
|
+
ctx.fillStyle = sgrad; ctx.fillRect(0, 0, _c.width, s);
|
|
892
|
+
var strailFrac = 0.18;
|
|
893
|
+
var si0 = Math.max(0, Math.floor((spos - strailFrac) * (spts.length - 1)));
|
|
894
|
+
ctx.strokeStyle = TOOL.color; ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
895
|
+
ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.moveTo(spts[si0].x, spts[si0].y);
|
|
896
|
+
for(var si = si0 + 1; si <= sidx; si++) ctx.lineTo(spts[si].x, spts[si].y);
|
|
897
|
+
ctx.stroke(); ctx.globalAlpha = 1;
|
|
898
|
+
_drawName(ctx, s, 1, undefined);
|
|
899
|
+
break;
|
|
900
|
+
case 'orbit':
|
|
901
|
+
_bracket(ctx, s, TOOL.color, 0.15);
|
|
902
|
+
_drawName(ctx, s, 1, 0.4);
|
|
903
|
+
var pts = _getOpenPts(s), speed = 1200, trailFrac = 0.28;
|
|
904
|
+
var halfCycle = (e % speed) / speed;
|
|
905
|
+
var cycle = (e % (speed * 2)) / (speed * 2);
|
|
906
|
+
var pos = cycle < 0.5 ? halfCycle : 1 - halfCycle;
|
|
907
|
+
var headIdx = Math.floor(pos * (pts.length - 1));
|
|
908
|
+
var trailLen = Math.floor(trailFrac * pts.length);
|
|
909
|
+
var dir = cycle < 0.5 ? 1 : -1;
|
|
910
|
+
ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
911
|
+
for(var i = 0; i < trailLen; i++) {
|
|
912
|
+
var idx = headIdx - dir * (trailLen - i);
|
|
913
|
+
if(idx < 0 || idx >= pts.length) continue;
|
|
914
|
+
var nxt = idx + dir;
|
|
915
|
+
if(nxt < 0 || nxt >= pts.length) continue;
|
|
916
|
+
ctx.globalAlpha = (i / trailLen) * 0.7; ctx.strokeStyle = TOOL.color;
|
|
917
|
+
ctx.beginPath(); ctx.moveTo(pts[idx].x, pts[idx].y); ctx.lineTo(pts[nxt].x, pts[nxt].y); ctx.stroke();
|
|
918
|
+
}
|
|
919
|
+
ctx.globalAlpha = 1;
|
|
920
|
+
break;
|
|
921
|
+
case 'dim':
|
|
922
|
+
var dim = 0.1 + 0.08 * Math.sin(e / 2000);
|
|
923
|
+
_bracket(ctx, s, TOOL.color, dim);
|
|
924
|
+
_drawName(ctx, s, 1, 0.2);
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
// Animate idle canvases
|
|
928
|
+
var idles = document.querySelectorAll('.grain-idle');
|
|
929
|
+
for(var i = 0; i < idles.length; i++) {
|
|
930
|
+
var ic = idles[i], ictx = ic.getContext('2d'), is = ic.width;
|
|
931
|
+
ictx.clearRect(0, 0, is, is);
|
|
932
|
+
var off = i * 400, cx = is/2;
|
|
933
|
+
var b = 0.25 + 0.35 * (0.5 + 0.5 * Math.sin((ts + off) / 1500));
|
|
934
|
+
var lw=is*LW, icy=is/2, gw=is*0.72, gh=is*0.68, topY=icy-gh/2, botY=icy+gh/2, fe=gw*0.30;
|
|
935
|
+
ictx.globalAlpha=b; ictx.strokeStyle=TOOL.color; ictx.lineWidth=lw; ictx.lineCap='round'; ictx.lineJoin='round';
|
|
936
|
+
ictx.beginPath(); ictx.moveTo(cx,topY); ictx.lineTo(cx-fe,topY);
|
|
937
|
+
ictx.bezierCurveTo(cx-gw*0.52,icy-gh*0.32, cx-gw*0.52,icy+gh*0.24, cx-fe,botY);
|
|
938
|
+
ictx.lineTo(cx,botY); ictx.stroke();
|
|
939
|
+
ictx.globalAlpha = 0.3 + 0.2 * (0.5 + 0.5 * Math.sin((ts + off) / 1500));
|
|
940
|
+
ictx.fillStyle=TOOL.color; ictx.textAlign='center'; ictx.textBaseline='middle';
|
|
941
|
+
ictx.font='800 '+(is*0.38)+'px -apple-system,"SF Pro Display","Helvetica Neue",Arial,sans-serif';
|
|
942
|
+
ictx.fillText(TOOL.letter, cx, icy+is*0.02); ictx.globalAlpha=1;
|
|
943
|
+
}
|
|
944
|
+
_raf = requestAnimationFrame(_frame);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Init: measure text, size canvas, start animation
|
|
948
|
+
_c = document.getElementById('grainLogo');
|
|
949
|
+
if(_c) {
|
|
950
|
+
_c.style.width = '0px';
|
|
951
|
+
_s = 256;
|
|
952
|
+
var fontRatio = 0.38;
|
|
953
|
+
var dh = 64;
|
|
954
|
+
_c.height = _s; _c.width = 1024;
|
|
955
|
+
_ctx = _c.getContext('2d');
|
|
956
|
+
_cx = _s / 2;
|
|
957
|
+
_restText = TOOL.name.slice(1);
|
|
958
|
+
_font = '800 ' + (_s * fontRatio) + 'px -apple-system,"SF Pro Display","Helvetica Neue",Arial,sans-serif';
|
|
959
|
+
_ctx.font = _font;
|
|
960
|
+
var letterW = _ctx.measureText(TOOL.letter).width;
|
|
961
|
+
var restW = _restText.length > 0 ? _ctx.measureText(_restText).width : 0;
|
|
962
|
+
_textStart = _cx + letterW / 2 + _s * 0.02;
|
|
963
|
+
var totalW = Math.ceil(_textStart + restW + _s * 0.12);
|
|
964
|
+
_c.width = totalW;
|
|
965
|
+
_ctx = _c.getContext('2d');
|
|
966
|
+
_c.style.height = dh + 'px';
|
|
967
|
+
_c.style.width = Math.round(totalW / _s * dh) + 'px';
|
|
968
|
+
_state = 'drawon'; _start = null;
|
|
969
|
+
_raf = requestAnimationFrame(_frame);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
window._grainSetState = function(state) {
|
|
973
|
+
if(_state === state) return;
|
|
974
|
+
if(_state === 'drawon') { _pendingState = state; return; }
|
|
975
|
+
_state = state; _start = null;
|
|
976
|
+
if(!_raf) _raf = requestAnimationFrame(_frame);
|
|
977
|
+
};
|
|
978
|
+
})();
|
|
979
|
+
</script>
|
|
980
|
+
</body>
|
|
981
|
+
</html>
|