@grainulation/silo 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/silo.js +327 -0
- package/lib/analytics.js +76 -0
- package/lib/import-export.js +174 -0
- package/lib/index.js +28 -0
- package/lib/packs.js +184 -0
- package/lib/search.js +128 -0
- package/lib/serve-mcp.js +337 -0
- package/lib/server.js +425 -0
- package/lib/store.js +145 -0
- package/lib/templates.js +139 -0
- package/package.json +48 -0
- package/packs/api-design.json +189 -0
- package/packs/architecture.json +175 -0
- package/packs/ci-cd.json +175 -0
- package/packs/compliance.json +203 -0
- package/packs/data-engineering.json +175 -0
- package/packs/frontend.json +175 -0
- package/packs/migration.json +147 -0
- package/packs/observability.json +175 -0
- package/packs/security.json +175 -0
- package/packs/team-process.json +175 -0
- package/packs/testing.json +147 -0
- package/public/grainulation-tokens.css +321 -0
- package/public/index.html +803 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="auto" data-tool="silo">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Silo</title>
|
|
7
|
+
<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='%236ee7b7' font-family='-apple-system,system-ui,sans-serif' font-size='34' font-weight='800'>S</text></svg>">
|
|
8
|
+
<!-- STYLE 1: shared tokens -->
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0e1a; --bg2: #111827; --bg3: #1e293b; --bg4: #334155;
|
|
12
|
+
--fg: #e2e8f0; --fg2: #94a3b8; --fg3: #64748b;
|
|
13
|
+
--border: #1e293b; --border-subtle: rgba(255,255,255,0.08);
|
|
14
|
+
--green: #34d399; --red: #f87171; --blue: #60a5fa; --purple: #a78bfa; --orange: #fb923c; --cyan: #22d3ee;
|
|
15
|
+
--space-xs: 4px; --space-sm: 8px; --space-md: 12px; --space-lg: 16px; --space-xl: 24px; --space-2xl: 32px;
|
|
16
|
+
--radius: 8px; --radius-sm: 4px; --radius-lg: 12px;
|
|
17
|
+
--font-sans: -apple-system,BlinkMacSystemFont,'Segoe UI','Inter',sans-serif;
|
|
18
|
+
--font-mono: 'SF Mono','Cascadia Code','JetBrains Mono','Fira Code',monospace;
|
|
19
|
+
--text-xs: 9px; --text-sm: 10px; --text-base: 12px; --text-md: 13px; --text-lg: 15px; --text-xl: 18px;
|
|
20
|
+
--line-height: 1.5;
|
|
21
|
+
--transition-fast: 0.1s ease; --transition-base: 0.15s ease;
|
|
22
|
+
/* silo accent */
|
|
23
|
+
--accent: #6ee7b7; --accent-light: #a7f3d0; --accent-dim: rgba(110,231,183,0.10); --accent-border: rgba(110,231,183,0.25);
|
|
24
|
+
}
|
|
25
|
+
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
26
|
+
html, body { height: 100%; overflow: hidden; }
|
|
27
|
+
body {
|
|
28
|
+
font-family: var(--font-sans); background: var(--bg); color: var(--fg);
|
|
29
|
+
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%);
|
|
30
|
+
font-size: var(--text-md); line-height: var(--line-height);
|
|
31
|
+
-webkit-font-smoothing: antialiased;
|
|
32
|
+
}
|
|
33
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
34
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
35
|
+
::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
|
36
|
+
*:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
37
|
+
</style>
|
|
38
|
+
<!-- STYLE 2: shared layout -->
|
|
39
|
+
<style>
|
|
40
|
+
.app { display:grid; grid-template-columns:320px 1fr; grid-template-rows:auto 1fr auto; height:100vh; overflow:hidden }
|
|
41
|
+
.toolbar { grid-column:1/-1; display:flex; align-items:center; padding:4px var(--space-xl); border-bottom:1px solid var(--border); background:rgba(255,255,255,0.08); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px); gap:10px }
|
|
42
|
+
.toolbar canvas { flex-shrink:0 }
|
|
43
|
+
.toolbar-spacer { flex:1 }
|
|
44
|
+
.toolbar-right { display:flex; align-items:center; gap:var(--space-sm) }
|
|
45
|
+
.status-dot { width:6px; height:6px; border-radius:50%; background:var(--fg3); flex-shrink:0; transition:background 0.3s,box-shadow 0.3s }
|
|
46
|
+
.status-dot.ok { background:var(--green); box-shadow:0 0 6px rgba(52,199,89,0.5) }
|
|
47
|
+
.reconnect-banner { position:fixed;top:0;left:0;right:0;z-index:9999;padding:8px 16px;background:#92400e;color:#fbbf24;font-size:12px;text-align:center;transform:translateY(-100%);transition:transform .3s;font-family:system-ui,sans-serif }
|
|
48
|
+
.reconnect-banner.visible { transform:translateY(0) }
|
|
49
|
+
.reconnect-banner button { background:none;border:1px solid #fbbf24;color:#fbbf24;padding:2px 10px;border-radius:4px;cursor:pointer;font-size:11px;margin-inline-start:8px }
|
|
50
|
+
.sidebar { background:var(--bg2); border-inline-end:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden }
|
|
51
|
+
.content { display:flex; flex-direction:column; overflow:hidden; min-height:0 }
|
|
52
|
+
.footer { grid-column:1/-1; display:flex; align-items:center; justify-content:space-between; padding:var(--space-xs) var(--space-xl); border-top:1px solid var(--border); background:var(--bg2); font-size:var(--text-xs); color:var(--fg3) }
|
|
53
|
+
.footer-links { display:flex; gap:var(--space-lg) }
|
|
54
|
+
.footer a { color:var(--fg3); text-decoration:none; transition:color var(--transition-fast) }
|
|
55
|
+
.footer a:hover { color:var(--accent) }
|
|
56
|
+
.mobile-nav { display:none; grid-column:1/-1; background:var(--bg2); border-bottom:1px solid var(--border) }
|
|
57
|
+
.mobile-nav-bar { display:flex }
|
|
58
|
+
.mobile-tab { flex:1; padding:12px 0; text-align:center; font-size:12px; font-weight:600; color:var(--fg3); background:none; border:none; border-bottom:2px solid transparent; cursor:pointer; font-family:var(--font-sans) }
|
|
59
|
+
.mobile-tab:hover { color:var(--fg2) }
|
|
60
|
+
.mobile-tab.active { color:var(--accent); border-bottom-color:var(--accent) }
|
|
61
|
+
.welcome { padding:var(--space-xl); max-width:600px; margin:0 auto }
|
|
62
|
+
.welcome h2 { font-size:18px; font-weight:700; color:var(--fg); margin-bottom:var(--space-sm) }
|
|
63
|
+
.welcome .subtitle { font-size:13px; color:var(--fg2); line-height:1.6; margin-bottom:var(--space-xl) }
|
|
64
|
+
.welcome-section { margin-bottom:var(--space-xl) }
|
|
65
|
+
.welcome-section h3 { font-size:11px; text-transform:uppercase; letter-spacing:0.8px; color:var(--fg3); margin-bottom:var(--space-md) }
|
|
66
|
+
.welcome-step { display:flex; gap:var(--space-md); align-items:flex-start; margin-bottom:var(--space-md) }
|
|
67
|
+
.welcome-step-num { width:22px; height:22px; border-radius:50%; background:var(--accent-dim); border:1px solid var(--accent-border); color:var(--accent); font-size:11px; font-weight:700; display:flex; align-items:center; justify-content:center; flex-shrink:0 }
|
|
68
|
+
.welcome-step-text { font-size:12px; color:var(--fg2); line-height:1.5 }
|
|
69
|
+
.welcome-step-text strong { color:var(--fg); font-weight:600 }
|
|
70
|
+
.welcome-kbd { display:inline-block; padding:1px 5px; background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius-sm); font-family:var(--font-mono); font-size:10px; color:var(--fg2) }
|
|
71
|
+
.spinner { width:14px; height:14px; border:2px solid var(--bg4); border-top-color:var(--accent); border-radius:50%; animation:spin 0.6s linear infinite; display:inline-block }
|
|
72
|
+
@keyframes spin { to { transform:rotate(360deg) } }
|
|
73
|
+
.toast-container { position:fixed; bottom:var(--space-xl); right:var(--space-xl); z-index:100; display:flex; flex-direction:column; gap:var(--space-sm) }
|
|
74
|
+
.toast { padding:var(--space-sm) var(--space-lg); background:var(--bg3); border:1px solid var(--accent-border); border-radius:var(--radius); font-size:var(--text-sm); color:var(--fg); animation:slideIn 0.2s ease }
|
|
75
|
+
@keyframes slideIn { from { opacity:0; transform:translateY(8px) } to { opacity:1; transform:translateY(0) } }
|
|
76
|
+
.skip-link { position:absolute; top:-40px; inset-inline-start:0; background:var(--accent); color:#000; padding:8px 16px; z-index:10000; font-size:14px; font-weight:600; transition:top .2s }
|
|
77
|
+
.skip-link:focus { top:0 }
|
|
78
|
+
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0 }
|
|
79
|
+
@media (max-width:768px) {
|
|
80
|
+
.app { grid-template-columns:1fr; grid-template-rows:auto auto auto 1fr auto }
|
|
81
|
+
.toolbar { grid-column:1 }
|
|
82
|
+
.mobile-nav { display:block }
|
|
83
|
+
.sidebar { border-inline-end:none; border-bottom:1px solid var(--border); display:none }
|
|
84
|
+
.sidebar.mobile-visible { display:flex; max-height:none }
|
|
85
|
+
.content.mobile-hidden { display:none }
|
|
86
|
+
.footer { grid-column:1 }
|
|
87
|
+
}
|
|
88
|
+
@media (prefers-reduced-motion:reduce) {
|
|
89
|
+
*,*::before,*::after { animation-duration:0.01ms !important; transition-duration:0.01ms !important; scroll-behavior:auto !important }
|
|
90
|
+
}
|
|
91
|
+
/* -- sidebar items -- */
|
|
92
|
+
.search-box { padding:var(--space-md); border-bottom:1px solid var(--border) }
|
|
93
|
+
.search-box input { width:100%; padding:var(--space-sm) var(--space-md); border-radius:var(--radius); background:var(--bg3); border:1px solid var(--border); color:var(--fg); font-size:16px; font-family:var(--font-sans); outline:none; transition:border-color var(--transition-fast) }
|
|
94
|
+
.search-box input::placeholder { color:var(--fg3) }
|
|
95
|
+
.search-box input:focus { border-color:var(--accent-border) }
|
|
96
|
+
.search-box .search-label { display:block; font-size:var(--text-xs); font-weight:600; text-transform:uppercase; letter-spacing:0.1em; color:var(--fg3); padding:0 0 var(--space-xs) }
|
|
97
|
+
.search-box .search-count { font-size:var(--text-xs); color:var(--fg3); padding-top:var(--space-xs) }
|
|
98
|
+
.item-list { flex:1; overflow-y:auto; padding:var(--space-sm) }
|
|
99
|
+
.item-card { padding:var(--space-md); margin-bottom:var(--space-sm); border-radius:var(--radius); background:var(--bg); border:1px solid var(--border); cursor:pointer; transition:border-color var(--transition-fast),background var(--transition-fast) }
|
|
100
|
+
.item-card:hover { border-color:var(--accent-border); background:var(--accent-dim) }
|
|
101
|
+
.item-card.active { border-color:var(--accent); background:var(--accent-dim) }
|
|
102
|
+
.item-name { font-size:var(--text-md); font-weight:600; color:var(--fg) }
|
|
103
|
+
.item-desc { font-size:11px; color:var(--fg2); line-height:1.4; margin-top:2px }
|
|
104
|
+
.item-meta { display:flex; gap:var(--space-sm); flex-wrap:wrap; margin-top:var(--space-xs) }
|
|
105
|
+
.item-badge { font-size:var(--text-xs); padding:1px 6px; border-radius:var(--radius-sm); background:var(--bg3); color:var(--fg3); border:1px solid var(--border) }
|
|
106
|
+
.item-badge.feature { color:var(--blue); background:rgba(59,130,246,0.08); border-color:rgba(59,130,246,0.15); cursor:pointer }
|
|
107
|
+
.item-badge.feature:hover { background:rgba(59,130,246,0.18); border-color:rgba(59,130,246,0.3) }
|
|
108
|
+
.item-badge.feature.active-filter { background:rgba(59,130,246,0.25); border-color:var(--blue) }
|
|
109
|
+
.item-no-results { padding:var(--space-xl); text-align:center; color:var(--fg3); font-size:var(--text-sm) }
|
|
110
|
+
</style>
|
|
111
|
+
<!-- STYLE 3: silo-specific -->
|
|
112
|
+
<style>
|
|
113
|
+
.content { overflow-y:auto; padding:var(--space-xl) }
|
|
114
|
+
|
|
115
|
+
/* main panels */
|
|
116
|
+
.panel { margin-bottom: var(--space-xl); }
|
|
117
|
+
.panel-title {
|
|
118
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
119
|
+
color: var(--fg3); margin-bottom: var(--space-md);
|
|
120
|
+
display: flex; align-items: center; gap: var(--space-sm);
|
|
121
|
+
}
|
|
122
|
+
.panel-title::after {
|
|
123
|
+
content: ''; flex: 1; height: 1px; background: var(--border);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* pack detail */
|
|
127
|
+
.pack-detail-header {
|
|
128
|
+
padding: var(--space-lg); background: var(--bg2); border-radius: var(--radius-lg);
|
|
129
|
+
border: 1px solid var(--border); margin-bottom: var(--space-xl);
|
|
130
|
+
}
|
|
131
|
+
.pack-detail-header h2 { font-size: 18px; font-weight: 600; }
|
|
132
|
+
.pack-detail-header .desc { color: var(--fg2); margin-top: var(--space-sm); font-size: 12px; }
|
|
133
|
+
.pack-detail-header .stats {
|
|
134
|
+
display: flex; gap: var(--space-xl); margin-top: var(--space-lg);
|
|
135
|
+
}
|
|
136
|
+
.stat { text-align: center; }
|
|
137
|
+
.stat .stat-val { font-size: 22px; font-weight: 700; color: var(--accent); }
|
|
138
|
+
.stat .stat-label { font-size: 10px; color: var(--fg3); text-transform: uppercase; letter-spacing: 0.3px; }
|
|
139
|
+
|
|
140
|
+
/* claim table */
|
|
141
|
+
.claim-table { width: 100%; border-collapse: collapse; }
|
|
142
|
+
.claim-table th {
|
|
143
|
+
text-align: start; font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px;
|
|
144
|
+
color: var(--fg3); padding: var(--space-sm) var(--space-md);
|
|
145
|
+
border-bottom: 1px solid var(--border);
|
|
146
|
+
}
|
|
147
|
+
.claim-table td {
|
|
148
|
+
padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--border-subtle);
|
|
149
|
+
font-size: 12px; vertical-align: top;
|
|
150
|
+
}
|
|
151
|
+
.claim-table tr:hover td { background: var(--accent-dim); }
|
|
152
|
+
.type-badge {
|
|
153
|
+
display: inline-block; padding: 1px 6px; border-radius: var(--radius-sm);
|
|
154
|
+
font-size: 10px; font-weight: 500;
|
|
155
|
+
}
|
|
156
|
+
.type-badge.constraint { background: rgba(248,113,113,0.15); color: #f87171; }
|
|
157
|
+
.type-badge.risk { background: rgba(251,146,60,0.15); color: #fb923c; }
|
|
158
|
+
.type-badge.recommendation { background: rgba(96,165,250,0.15); color: #60a5fa; }
|
|
159
|
+
.type-badge.factual { background: rgba(52,211,153,0.15); color: #34d399; }
|
|
160
|
+
.type-badge.estimate { background: rgba(167,139,250,0.15); color: #a78bfa; }
|
|
161
|
+
.type-badge.feedback { background: rgba(251,191,36,0.15); color: #fbbf24; }
|
|
162
|
+
.evidence-badge {
|
|
163
|
+
display: inline-block; padding: 1px 6px; border-radius: var(--radius-sm);
|
|
164
|
+
font-size: 10px; background: var(--bg3); color: var(--fg2);
|
|
165
|
+
}
|
|
166
|
+
.tag { display: inline-block; padding: 1px 5px; font-size: 9px; border-radius: var(--radius-sm); background: var(--bg4); color: var(--fg3); margin-inline-end: 3px; }
|
|
167
|
+
|
|
168
|
+
/* import wizard */
|
|
169
|
+
.import-wizard {
|
|
170
|
+
padding: var(--space-lg); background: var(--bg2); border-radius: var(--radius-lg);
|
|
171
|
+
border: 1px solid var(--accent-border); margin-bottom: var(--space-xl);
|
|
172
|
+
}
|
|
173
|
+
.import-wizard h3 { font-size: 14px; margin-bottom: var(--space-md); }
|
|
174
|
+
.import-wizard input {
|
|
175
|
+
width: 100%; padding: 8px 12px; border-radius: var(--radius);
|
|
176
|
+
background: var(--bg3); border: 1px solid var(--border);
|
|
177
|
+
color: var(--fg); font-size: 12px; font-family: var(--font-mono);
|
|
178
|
+
outline: none; margin-bottom: var(--space-md);
|
|
179
|
+
}
|
|
180
|
+
.import-wizard input:focus { border-color: var(--accent-border); }
|
|
181
|
+
.import-btn {
|
|
182
|
+
padding: 8px 16px; border-radius: var(--radius);
|
|
183
|
+
background: var(--accent-dim); color: var(--accent);
|
|
184
|
+
border: 1px solid var(--accent-border); cursor: pointer;
|
|
185
|
+
font-size: 12px; font-weight: 500; transition: background var(--transition-fast);
|
|
186
|
+
}
|
|
187
|
+
.import-btn:hover { background: var(--accent-border); }
|
|
188
|
+
.import-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
189
|
+
.import-result { margin-top: var(--space-md); font-size: 12px; color: var(--fg2); }
|
|
190
|
+
.import-result.success { color: var(--green); }
|
|
191
|
+
.import-result.error { color: var(--red); }
|
|
192
|
+
|
|
193
|
+
/* welcome grid */
|
|
194
|
+
.welcome-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-sm); }
|
|
195
|
+
.welcome-pack { padding: var(--space-md); background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; transition: border-color var(--transition-fast), background var(--transition-fast); }
|
|
196
|
+
.welcome-pack:hover { border-color: var(--accent-border); background: var(--accent-dim); }
|
|
197
|
+
.welcome-pack .wp-name { font-size: 12px; font-weight: 600; color: var(--fg); }
|
|
198
|
+
.welcome-pack .wp-meta { font-size: 10px; color: var(--fg3); margin-top: 2px; }
|
|
199
|
+
</style>
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
203
|
+
<div id="live-status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
|
|
204
|
+
<div class="reconnect-banner" id="reconnectBanner" role="status" aria-live="polite"></div>
|
|
205
|
+
<div class="app">
|
|
206
|
+
<header class="toolbar" role="banner">
|
|
207
|
+
<canvas id="grainLogo" width="256" height="256"></canvas>
|
|
208
|
+
<div class="toolbar-spacer"></div>
|
|
209
|
+
<div class="toolbar-right">
|
|
210
|
+
<div class="status-dot" id="sse-dot" aria-label="Connection status" role="status"></div>
|
|
211
|
+
</div>
|
|
212
|
+
</header>
|
|
213
|
+
<nav class="mobile-nav" aria-label="Mobile navigation">
|
|
214
|
+
<div class="mobile-nav-bar">
|
|
215
|
+
<button class="mobile-tab active" data-panel="content">Content</button>
|
|
216
|
+
<button class="mobile-tab" data-panel="sidebar">Packs</button>
|
|
217
|
+
</div>
|
|
218
|
+
</nav>
|
|
219
|
+
<aside class="sidebar" aria-label="Pack list">
|
|
220
|
+
<div class="search-box">
|
|
221
|
+
<input type="search" id="searchInput" placeholder="Filter packs..." aria-label="Filter packs" />
|
|
222
|
+
</div>
|
|
223
|
+
<div class="item-list" id="packList" role="listbox" aria-label="Knowledge packs"></div>
|
|
224
|
+
</aside>
|
|
225
|
+
<main class="content" id="main-content" aria-label="Silo workspace">
|
|
226
|
+
<div class="welcome"><h2>Silo</h2><div class="subtitle">Loading packs...</div></div>
|
|
227
|
+
</main>
|
|
228
|
+
<footer class="footer">
|
|
229
|
+
<span>silo v1.0.0 -- @grainulation/silo</span>
|
|
230
|
+
<div class="footer-links">
|
|
231
|
+
<a href="http://localhost:9091">wheat</a>
|
|
232
|
+
<a href="http://localhost:9093">barn</a>
|
|
233
|
+
<a href="http://localhost:9094">mill</a>
|
|
234
|
+
<a href="http://localhost:9090">farmer</a>
|
|
235
|
+
<a href="http://localhost:9096">harvest</a>
|
|
236
|
+
<span style="color:var(--border)">·</span>
|
|
237
|
+
<a href="https://github.com/grainulation/silo" target="_blank" rel="noopener noreferrer">github</a>
|
|
238
|
+
</div>
|
|
239
|
+
</footer>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="toast-container" id="toast-container" aria-live="polite" role="status"></div>
|
|
242
|
+
|
|
243
|
+
<script>
|
|
244
|
+
// -- Shared: helpers --
|
|
245
|
+
var $ = function(id) { return document.getElementById(id); };
|
|
246
|
+
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
247
|
+
|
|
248
|
+
// -- Shared: SSE with exponential backoff --
|
|
249
|
+
var sseRetryCount = 0;
|
|
250
|
+
var sseSource = null;
|
|
251
|
+
function showBanner(count) {
|
|
252
|
+
var b = $('reconnectBanner');
|
|
253
|
+
if (count > 5) {
|
|
254
|
+
b.textContent = '';
|
|
255
|
+
b.appendChild(document.createTextNode('Connection lost. '));
|
|
256
|
+
var btn = document.createElement('button');
|
|
257
|
+
btn.textContent = 'Retry now';
|
|
258
|
+
btn.addEventListener('click', function() { sseRetryCount = 0; connectSSE(); });
|
|
259
|
+
b.appendChild(btn);
|
|
260
|
+
} else if (count > 1) {
|
|
261
|
+
b.textContent = 'Reconnecting (attempt ' + count + ')...';
|
|
262
|
+
} else {
|
|
263
|
+
b.textContent = 'Reconnecting...';
|
|
264
|
+
}
|
|
265
|
+
b.classList.add('visible');
|
|
266
|
+
}
|
|
267
|
+
function hideBanner() { $('reconnectBanner').classList.remove('visible'); }
|
|
268
|
+
|
|
269
|
+
function connectSSE() {
|
|
270
|
+
sseSource = new EventSource('/events');
|
|
271
|
+
sseSource.onopen = function() {
|
|
272
|
+
sseRetryCount = 0;
|
|
273
|
+
$('sse-dot').className = 'status-dot ok';
|
|
274
|
+
if (window._grainSetState) window._grainSetState('idle');
|
|
275
|
+
};
|
|
276
|
+
sseSource.onerror = function() {
|
|
277
|
+
sseSource.close();
|
|
278
|
+
$('sse-dot').className = 'status-dot';
|
|
279
|
+
if (window._grainSetState) window._grainSetState('orbit');
|
|
280
|
+
var delay = Math.min(30000, 1000 * Math.pow(2, sseRetryCount)) + Math.random() * 1000;
|
|
281
|
+
sseRetryCount++;
|
|
282
|
+
setTimeout(connectSSE, delay);
|
|
283
|
+
};
|
|
284
|
+
sseSource.onmessage = function(e) {
|
|
285
|
+
try {
|
|
286
|
+
var msg = JSON.parse(e.data);
|
|
287
|
+
onSSEMessage(msg);
|
|
288
|
+
} catch(ex) {}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// -- Shared: mobile panel switching --
|
|
293
|
+
function switchMobilePanel(panel) {
|
|
294
|
+
var sidebar = document.querySelector('.sidebar');
|
|
295
|
+
var content = document.querySelector('.content');
|
|
296
|
+
document.querySelectorAll('.mobile-tab').forEach(function(tb) { tb.classList.toggle('active', tb.dataset.panel === panel); });
|
|
297
|
+
sidebar.classList.remove('mobile-visible');
|
|
298
|
+
content.classList.remove('mobile-hidden');
|
|
299
|
+
if (panel === 'sidebar') { sidebar.classList.add('mobile-visible'); content.classList.add('mobile-hidden'); }
|
|
300
|
+
}
|
|
301
|
+
document.querySelector('.mobile-nav-bar').addEventListener('click', function(e) {
|
|
302
|
+
var tab = e.target.closest('[data-panel]');
|
|
303
|
+
if (tab) switchMobilePanel(tab.dataset.panel);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// -- Silo: tool-specific logic --
|
|
307
|
+
(function() {
|
|
308
|
+
'use strict';
|
|
309
|
+
|
|
310
|
+
var allPacks = [];
|
|
311
|
+
var activePack = null;
|
|
312
|
+
var searchTimeout = null;
|
|
313
|
+
|
|
314
|
+
// -- SSE message handler --
|
|
315
|
+
window.onSSEMessage = function(msg) {
|
|
316
|
+
if (msg.type === 'state') {
|
|
317
|
+
loadPacks();
|
|
318
|
+
var ls = $('live-status');
|
|
319
|
+
if (ls) ls.textContent = 'Updated: packs refreshed';
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// -- Load packs --
|
|
324
|
+
function loadPacks() {
|
|
325
|
+
fetch('/api/packs').then(function(res) {
|
|
326
|
+
return res.json();
|
|
327
|
+
}).then(function(data) {
|
|
328
|
+
allPacks = data.packs || [];
|
|
329
|
+
renderPackList(allPacks);
|
|
330
|
+
if (!activePack) renderWelcome();
|
|
331
|
+
}).catch(function(err) {
|
|
332
|
+
console.error('Failed to load packs:', err);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// -- Render pack list --
|
|
337
|
+
function renderPackList(packs) {
|
|
338
|
+
var list = $('packList');
|
|
339
|
+
if (packs.length === 0) {
|
|
340
|
+
list.innerHTML = '<div class="item-no-results">No packs found.</div>';
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
list.innerHTML = packs.map(function(p) {
|
|
344
|
+
return '<div class="item-card' + (activePack === p.id ? ' active' : '') + '" data-pack="' + esc(p.id) + '" role="button" tabindex="0" aria-pressed="' + (activePack === p.id) + '">' +
|
|
345
|
+
'<div class="item-name">' + esc(p.name || p.id) + '</div>' +
|
|
346
|
+
(p.description ? '<div class="item-desc">' + esc(truncate(p.description, 120)) + '</div>' : '') +
|
|
347
|
+
'<div class="item-meta">' +
|
|
348
|
+
'<span class="item-badge">' + p.claimCount + ' claims</span>' +
|
|
349
|
+
'<span class="item-badge">v' + esc(p.version || '1.0.0') + '</span>' +
|
|
350
|
+
'</div>' +
|
|
351
|
+
'</div>';
|
|
352
|
+
}).join('');
|
|
353
|
+
|
|
354
|
+
list.querySelectorAll('.item-card').forEach(function(card) {
|
|
355
|
+
card.addEventListener('click', function() { selectPack(card.dataset.pack); });
|
|
356
|
+
card.addEventListener('keydown', function(e) {
|
|
357
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectPack(card.dataset.pack); }
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// -- Select a pack --
|
|
363
|
+
function selectPack(id) {
|
|
364
|
+
activePack = id;
|
|
365
|
+
renderPackList(allPacks);
|
|
366
|
+
|
|
367
|
+
var main = $('main-content');
|
|
368
|
+
main.innerHTML = '<div style="padding:40px;color:var(--fg3);">Loading...</div>';
|
|
369
|
+
|
|
370
|
+
fetch('/api/packs/' + encodeURIComponent(id)).then(function(res) {
|
|
371
|
+
return res.json();
|
|
372
|
+
}).then(function(pack) {
|
|
373
|
+
renderPackDetail(pack);
|
|
374
|
+
document.title = 'Silo - ' + (pack.name || id);
|
|
375
|
+
toast('Loaded ' + (pack.name || id));
|
|
376
|
+
}).catch(function(err) {
|
|
377
|
+
main.innerHTML = '<div style="padding:40px;color:var(--red);">Error loading pack: ' + esc(err.message) + '</div>';
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// -- Render pack detail --
|
|
382
|
+
function renderPackDetail(pack) {
|
|
383
|
+
var main = $('main-content');
|
|
384
|
+
var claims = pack.claims || [];
|
|
385
|
+
|
|
386
|
+
var types = countBy(claims, 'type');
|
|
387
|
+
var evidence = countBy(claims, 'evidence');
|
|
388
|
+
|
|
389
|
+
main.innerHTML =
|
|
390
|
+
'<div class="pack-detail-header">' +
|
|
391
|
+
'<h2>' + esc(pack.name || pack.id) + '</h2>' +
|
|
392
|
+
(pack.description ? '<div class="desc">' + esc(pack.description) + '</div>' : '') +
|
|
393
|
+
'<div class="stats">' +
|
|
394
|
+
'<div class="stat"><div class="stat-val">' + claims.length + '</div><div class="stat-label">Claims</div></div>' +
|
|
395
|
+
'<div class="stat"><div class="stat-val">' + Object.keys(types).length + '</div><div class="stat-label">Types</div></div>' +
|
|
396
|
+
'<div class="stat"><div class="stat-val">' + Object.keys(evidence).length + '</div><div class="stat-label">Evidence Tiers</div></div>' +
|
|
397
|
+
'<div class="stat"><div class="stat-val">v' + esc(pack.version || '1.0.0') + '</div><div class="stat-label">Version</div></div>' +
|
|
398
|
+
'</div>' +
|
|
399
|
+
'</div>' +
|
|
400
|
+
|
|
401
|
+
'<div class="panel">' +
|
|
402
|
+
'<div class="panel-title">Import into Sprint</div>' +
|
|
403
|
+
'<div class="import-wizard">' +
|
|
404
|
+
'<h3>Import "' + esc(pack.name || pack.id) + '" into a sprint</h3>' +
|
|
405
|
+
'<input type="text" id="importTarget" placeholder="/path/to/sprint-directory" />' +
|
|
406
|
+
'<button class="import-btn" id="importBtn">Import ' + claims.length + ' claims</button>' +
|
|
407
|
+
'<div class="import-result" id="importResult"></div>' +
|
|
408
|
+
'</div>' +
|
|
409
|
+
'</div>' +
|
|
410
|
+
|
|
411
|
+
'<div class="panel">' +
|
|
412
|
+
'<div class="panel-title">Claims (' + claims.length + ')</div>' +
|
|
413
|
+
'<table class="claim-table">' +
|
|
414
|
+
'<thead><tr>' +
|
|
415
|
+
'<th>ID</th><th>Type</th><th>Topic</th><th>Content</th><th>Evidence</th><th>Tags</th>' +
|
|
416
|
+
'</tr></thead>' +
|
|
417
|
+
'<tbody>' +
|
|
418
|
+
claims.map(function(c) {
|
|
419
|
+
return '<tr>' +
|
|
420
|
+
'<td style="font-family:var(--font-mono);font-size:10px;white-space:nowrap;">' + esc(c.id || '-') + '</td>' +
|
|
421
|
+
'<td><span class="type-badge ' + esc(c.type || '') + '">' + esc(c.type || '-') + '</span></td>' +
|
|
422
|
+
'<td style="color:var(--fg2);font-size:11px;">' + esc(c.topic || '-') + '</td>' +
|
|
423
|
+
'<td>' + esc(truncate(c.content || c.text || '', 200)) + '</td>' +
|
|
424
|
+
'<td><span class="evidence-badge">' + esc(c.evidence || c.tier || '-') + '</span></td>' +
|
|
425
|
+
'<td>' + (c.tags || []).map(function(tg) { return '<span class="tag">' + esc(tg) + '</span>'; }).join('') + '</td>' +
|
|
426
|
+
'</tr>';
|
|
427
|
+
}).join('') +
|
|
428
|
+
'</tbody>' +
|
|
429
|
+
'</table>' +
|
|
430
|
+
'</div>';
|
|
431
|
+
|
|
432
|
+
// Import handler
|
|
433
|
+
$('importBtn').addEventListener('click', function() {
|
|
434
|
+
var target = $('importTarget').value.trim();
|
|
435
|
+
var resultEl = $('importResult');
|
|
436
|
+
if (!target) {
|
|
437
|
+
resultEl.className = 'import-result error';
|
|
438
|
+
resultEl.textContent = 'Please enter a target sprint directory.';
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
var btn = $('importBtn');
|
|
442
|
+
btn.disabled = true;
|
|
443
|
+
btn.textContent = 'Importing...';
|
|
444
|
+
fetch('/api/import', {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: { 'Content-Type': 'application/json' },
|
|
447
|
+
body: JSON.stringify({ pack: pack.id, targetDir: target }),
|
|
448
|
+
}).then(function(res) {
|
|
449
|
+
return res.json();
|
|
450
|
+
}).then(function(data) {
|
|
451
|
+
if (data.success) {
|
|
452
|
+
resultEl.className = 'import-result success';
|
|
453
|
+
resultEl.textContent = 'Imported ' + data.imported + ' claims (' + data.skippedDuplicates + ' duplicates skipped, ' + data.totalClaims + ' total).';
|
|
454
|
+
toast('Import complete');
|
|
455
|
+
} else {
|
|
456
|
+
resultEl.className = 'import-result error';
|
|
457
|
+
resultEl.textContent = data.error || 'Import failed.';
|
|
458
|
+
}
|
|
459
|
+
}).catch(function(err) {
|
|
460
|
+
resultEl.className = 'import-result error';
|
|
461
|
+
resultEl.textContent = err.message;
|
|
462
|
+
}).finally(function() {
|
|
463
|
+
btn.disabled = false;
|
|
464
|
+
btn.textContent = 'Import ' + claims.length + ' claims';
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// -- Search: filters sidebar pack list by name/description --
|
|
470
|
+
var searchInput = $('searchInput');
|
|
471
|
+
searchInput.addEventListener('input', function() {
|
|
472
|
+
clearTimeout(searchTimeout);
|
|
473
|
+
searchTimeout = setTimeout(function() {
|
|
474
|
+
var q = searchInput.value.trim().toLowerCase();
|
|
475
|
+
if (!q) {
|
|
476
|
+
renderPackList(allPacks);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
var filtered = allPacks.filter(function(p) {
|
|
480
|
+
var name = (p.name || p.id || '').toLowerCase();
|
|
481
|
+
var desc = (p.description || '').toLowerCase();
|
|
482
|
+
return name.indexOf(q) !== -1 || desc.indexOf(q) !== -1;
|
|
483
|
+
});
|
|
484
|
+
renderPackList(filtered);
|
|
485
|
+
}, 150);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
function showEmpty() {
|
|
489
|
+
activePack = null;
|
|
490
|
+
document.title = 'Silo';
|
|
491
|
+
renderPackList(allPacks);
|
|
492
|
+
renderWelcome();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function renderWelcome() {
|
|
496
|
+
var main = $('main-content');
|
|
497
|
+
var totalClaims = allPacks.reduce(function(n, p) { return n + (p.claimCount || 0); }, 0);
|
|
498
|
+
|
|
499
|
+
var packCards = '';
|
|
500
|
+
for (var i = 0; i < allPacks.length; i++) {
|
|
501
|
+
var p = allPacks[i];
|
|
502
|
+
packCards += '<div class="welcome-pack" data-pack="' + esc(p.id) + '" tabindex="0" role="button">' +
|
|
503
|
+
'<div class="wp-name">' + esc(p.name || p.id) + '</div>' +
|
|
504
|
+
'<div class="wp-meta">' + (p.claimCount || 0) + ' claims</div>' +
|
|
505
|
+
'</div>';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
main.innerHTML =
|
|
509
|
+
'<div class="welcome">' +
|
|
510
|
+
'<h2>Silo</h2>' +
|
|
511
|
+
'<div class="subtitle">' +
|
|
512
|
+
'Pre-built knowledge packs for engineering research sprints. ' +
|
|
513
|
+
'Each pack contains typed, evidence-graded claims you can import into any wheat sprint -- ' +
|
|
514
|
+
'constraints, risks, recommendations, and factual baselines so you start with real knowledge instead of a blank page.' +
|
|
515
|
+
'</div>' +
|
|
516
|
+
|
|
517
|
+
'<div class="welcome-section">' +
|
|
518
|
+
'<h3>How it works</h3>' +
|
|
519
|
+
'<div class="welcome-step">' +
|
|
520
|
+
'<span class="welcome-step-num">1</span>' +
|
|
521
|
+
'<div class="welcome-step-text"><strong>Browse packs</strong> -- pick a topic from the sidebar or the grid below. Each pack covers a common engineering decision area with pre-researched claims.</div>' +
|
|
522
|
+
'</div>' +
|
|
523
|
+
'<div class="welcome-step">' +
|
|
524
|
+
'<span class="welcome-step-num">2</span>' +
|
|
525
|
+
'<div class="welcome-step-text"><strong>Search across packs</strong> -- type in the search bar to find specific claims across all packs. Results rank by relevance.</div>' +
|
|
526
|
+
'</div>' +
|
|
527
|
+
'<div class="welcome-step">' +
|
|
528
|
+
'<span class="welcome-step-num">3</span>' +
|
|
529
|
+
'<div class="welcome-step-text"><strong>Import into a sprint</strong> -- from any pack detail page, enter a sprint directory path and import the claims. Duplicates are automatically skipped.</div>' +
|
|
530
|
+
'</div>' +
|
|
531
|
+
'</div>' +
|
|
532
|
+
|
|
533
|
+
'<div class="welcome-section">' +
|
|
534
|
+
'<h3>Available packs (' + allPacks.length + ' packs, ' + totalClaims + ' claims)</h3>' +
|
|
535
|
+
'<div class="welcome-grid">' + packCards + '</div>' +
|
|
536
|
+
'</div>' +
|
|
537
|
+
|
|
538
|
+
'<div class="welcome-section">' +
|
|
539
|
+
'<h3>Keyboard shortcuts</h3>' +
|
|
540
|
+
'<div class="welcome-step">' +
|
|
541
|
+
'<div class="welcome-step-text">' +
|
|
542
|
+
'<span class="welcome-kbd">[</span> <span class="welcome-kbd">]</span> Switch between sidebar and content ' +
|
|
543
|
+
' <span class="welcome-kbd">/</span> Focus search ' +
|
|
544
|
+
' <span class="welcome-kbd">Esc</span> Return to this page' +
|
|
545
|
+
'</div>' +
|
|
546
|
+
'</div>' +
|
|
547
|
+
'</div>' +
|
|
548
|
+
|
|
549
|
+
'</div>';
|
|
550
|
+
|
|
551
|
+
// Make pack cards clickable and keyboard-accessible
|
|
552
|
+
main.querySelectorAll('.welcome-pack').forEach(function(card) {
|
|
553
|
+
card.addEventListener('click', function() { selectPack(card.dataset.pack); });
|
|
554
|
+
card.addEventListener('keydown', function(e) {
|
|
555
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectPack(card.dataset.pack); }
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// -- Helpers --
|
|
561
|
+
function truncate(s, n) { return s && s.length > n ? s.slice(0, n - 3) + '...' : s || ''; }
|
|
562
|
+
function countBy(arr, key) { var c = {}; for (var i = 0; i < arr.length; i++) { var v = arr[i][key] || 'unknown'; c[v] = (c[v] || 0) + 1; } return c; }
|
|
563
|
+
function toast(message) {
|
|
564
|
+
var container = $('toast-container');
|
|
565
|
+
var el = document.createElement('div');
|
|
566
|
+
el.className = 'toast'; el.textContent = message;
|
|
567
|
+
container.appendChild(el);
|
|
568
|
+
setTimeout(function() { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s ease'; setTimeout(function() { el.remove(); }, 300); }, 3000);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// -- Keyboard navigation --
|
|
572
|
+
var siloPanels = ['.sidebar', '.content'];
|
|
573
|
+
var siloActivePanel = 1;
|
|
574
|
+
function switchSiloPanel(dir) {
|
|
575
|
+
siloActivePanel = (siloActivePanel + dir + siloPanels.length) % siloPanels.length;
|
|
576
|
+
var el = document.querySelector(siloPanels[siloActivePanel]);
|
|
577
|
+
if (el) { el.setAttribute('tabindex', '-1'); el.focus(); }
|
|
578
|
+
}
|
|
579
|
+
document.addEventListener('keydown', function(e) {
|
|
580
|
+
if (e.key === '[' && !e.target.matches('input,textarea')) { e.preventDefault(); switchSiloPanel(-1); }
|
|
581
|
+
if (e.key === ']' && !e.target.matches('input,textarea')) { e.preventDefault(); switchSiloPanel(1); }
|
|
582
|
+
if (e.key === '/' && !e.target.matches('input,textarea,select')) { e.preventDefault(); $('searchInput').focus(); }
|
|
583
|
+
if (e.key === 'Escape') { showEmpty(); var ls = $('live-status'); if (ls) ls.textContent = 'Selection cleared'; }
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// -- Init --
|
|
587
|
+
connectSSE();
|
|
588
|
+
loadPacks();
|
|
589
|
+
})();
|
|
590
|
+
</script>
|
|
591
|
+
<script>
|
|
592
|
+
// -- Grain Logo Animation --
|
|
593
|
+
(function() {
|
|
594
|
+
var LW = 0.025;
|
|
595
|
+
var TOOL = { name: 'Silo', letter: 'S', color: '#6ee7b7' };
|
|
596
|
+
var _c, _ctx, _s, _cx, _textStart, _restText, _font;
|
|
597
|
+
var _state = 'drawon', _start = null, _raf;
|
|
598
|
+
var _openPts = null, _closedPts = null;
|
|
599
|
+
|
|
600
|
+
function _lerp(a,b,t){ return {x:a.x+(b.x-a.x)*t, y:a.y+(b.y-a.y)*t}; }
|
|
601
|
+
function _easeInOut(t){ return t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2; }
|
|
602
|
+
|
|
603
|
+
function _bracket(ctx, s, color, alpha) {
|
|
604
|
+
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
605
|
+
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
606
|
+
if(alpha!==undefined) ctx.globalAlpha=alpha;
|
|
607
|
+
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
608
|
+
ctx.beginPath(); ctx.moveTo(cx,topY); ctx.lineTo(cx-fe,topY);
|
|
609
|
+
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
610
|
+
ctx.lineTo(cx,botY); ctx.stroke();
|
|
611
|
+
if(alpha!==undefined) ctx.globalAlpha=1;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function _drawBracket(ctx, s, color, progress) {
|
|
615
|
+
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
616
|
+
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
617
|
+
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
618
|
+
var seg1=0.12, seg2=0.72;
|
|
619
|
+
ctx.beginPath();
|
|
620
|
+
if(progress<=seg1){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe*(progress/seg1),topY);}
|
|
621
|
+
else if(progress<=seg1+seg2){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);ctx.stroke();ctx.beginPath();
|
|
622
|
+
var bt=(progress-seg1)/seg2;
|
|
623
|
+
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};
|
|
624
|
+
var q1=_lerp(p0,p1,bt),q2=_lerp(p1,p2,bt),q3=_lerp(p2,p3,bt);
|
|
625
|
+
var r1=_lerp(q1,q2,bt),r2=_lerp(q2,q3,bt),s1=_lerp(r1,r2,bt);
|
|
626
|
+
ctx.moveTo(p0.x,p0.y);ctx.bezierCurveTo(q1.x,q1.y,r1.x,r1.y,s1.x,s1.y);}
|
|
627
|
+
else{ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);
|
|
628
|
+
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
629
|
+
ctx.lineTo((cx-fe)+fe*((progress-seg1-seg2)/(1-seg1-seg2)),botY);}
|
|
630
|
+
ctx.stroke();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function _drawName(ctx, s, spellP, alpha) {
|
|
634
|
+
var a = alpha !== undefined ? alpha : 1;
|
|
635
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
636
|
+
var cy = s/2 + s*0.02;
|
|
637
|
+
ctx.globalAlpha = a; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
638
|
+
ctx.fillText(TOOL.letter, _cx, cy);
|
|
639
|
+
if(_restText.length > 0 && spellP > 0) {
|
|
640
|
+
var n = _restText.length, num = Math.min(n, Math.ceil(spellP * n));
|
|
641
|
+
var rawP = spellP * n, charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
642
|
+
var full = charP >= 1 ? num : num - 1;
|
|
643
|
+
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
644
|
+
if(full > 0) { ctx.globalAlpha = a; ctx.fillText(_restText.slice(0, full), _textStart, cy); }
|
|
645
|
+
if(full < num) {
|
|
646
|
+
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
647
|
+
ctx.globalAlpha = a * (0.3 + 0.7 * charP);
|
|
648
|
+
ctx.fillText(_restText[full], _textStart + prevW, cy);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
ctx.globalAlpha = 1;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function _getOpenPts(s) {
|
|
655
|
+
if(_openPts && _openPts._s === s) return _openPts;
|
|
656
|
+
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;
|
|
657
|
+
var pts=[];
|
|
658
|
+
for(var t=0;t<=1;t+=0.05) pts.push({x:cx-fe*t,y:topY});
|
|
659
|
+
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};
|
|
660
|
+
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});}
|
|
661
|
+
for(var t=0;t<=1;t+=0.05) pts.push({x:(cx-fe)+fe*t,y:botY});
|
|
662
|
+
pts._s=s; _openPts=pts; return pts;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function _getClosedPts(s) {
|
|
666
|
+
if(_closedPts && _closedPts._s === s) return _closedPts;
|
|
667
|
+
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;
|
|
668
|
+
var pts=[];
|
|
669
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:cx-fe*t,y:topY});
|
|
670
|
+
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};
|
|
671
|
+
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});}
|
|
672
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx-fe)+2*fe*t,y:botY});
|
|
673
|
+
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};
|
|
674
|
+
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});}
|
|
675
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx+fe)-fe*t,y:topY});
|
|
676
|
+
pts._s=s; _closedPts=pts; return pts;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function _frame(ts) {
|
|
680
|
+
if(!_c) return;
|
|
681
|
+
if(!_start) _start = ts;
|
|
682
|
+
var e = ts - _start, ctx = _ctx, s = _s;
|
|
683
|
+
ctx.clearRect(0, 0, _c.width, s);
|
|
684
|
+
switch(_state) {
|
|
685
|
+
case 'drawon':
|
|
686
|
+
var bp = _easeInOut(Math.min(1, e / 1400));
|
|
687
|
+
_drawBracket(ctx, s, TOOL.color, bp);
|
|
688
|
+
var la = Math.max(0, Math.min(1, (e - 900) / 400));
|
|
689
|
+
if(la > 0) {
|
|
690
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
691
|
+
ctx.globalAlpha = la; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
692
|
+
ctx.fillText(TOOL.letter, _cx, s/2 + s*0.02); ctx.globalAlpha = 1;
|
|
693
|
+
}
|
|
694
|
+
if(e > 1100 && _restText.length > 0) {
|
|
695
|
+
var sp = Math.min(1, (e - 1100) / (120 * _restText.length));
|
|
696
|
+
var n = _restText.length, num = Math.min(n, Math.ceil(sp * n));
|
|
697
|
+
if(num > 0) {
|
|
698
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
699
|
+
var cy = s/2 + s*0.02, rawP = sp * n;
|
|
700
|
+
var charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
701
|
+
var full = charP >= 1 ? num : num - 1;
|
|
702
|
+
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
703
|
+
if(full > 0) ctx.fillText(_restText.slice(0, full), _textStart, cy);
|
|
704
|
+
if(full < num) {
|
|
705
|
+
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
706
|
+
ctx.globalAlpha = 0.3 + 0.7 * charP;
|
|
707
|
+
ctx.fillText(_restText[full], _textStart + prevW, cy); ctx.globalAlpha = 1;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if(e > 1100 + 120 * _restText.length + 300) { _state = _pendingState || 'idle'; _pendingState = null; _start = ts; }
|
|
712
|
+
break;
|
|
713
|
+
case 'idle':
|
|
714
|
+
var breathe = 0.5 + 0.5 * (0.5 + 0.5 * Math.sin(e / 1200));
|
|
715
|
+
_bracket(ctx, s, TOOL.color, breathe);
|
|
716
|
+
var textBreath = 0.88 + 0.12 * (0.5 + 0.5 * Math.sin(e / 1800));
|
|
717
|
+
_drawName(ctx, s, 1, textBreath);
|
|
718
|
+
break;
|
|
719
|
+
case 'shimmer':
|
|
720
|
+
_bracket(ctx, s, TOOL.color, 0.2);
|
|
721
|
+
var spts = _getOpenPts(s), sspeed = 1800;
|
|
722
|
+
var spos = (e % sspeed) / sspeed;
|
|
723
|
+
var sidx = Math.floor(spos * (spts.length - 1));
|
|
724
|
+
var spt = spts[sidx];
|
|
725
|
+
var sgrad = ctx.createRadialGradient(spt.x, spt.y, 0, spt.x, spt.y, s * 0.10);
|
|
726
|
+
sgrad.addColorStop(0, TOOL.color + 'aa'); sgrad.addColorStop(1, 'transparent');
|
|
727
|
+
ctx.fillStyle = sgrad; ctx.fillRect(0, 0, _c.width, s);
|
|
728
|
+
var strailFrac = 0.18;
|
|
729
|
+
var si0 = Math.max(0, Math.floor((spos - strailFrac) * (spts.length - 1)));
|
|
730
|
+
ctx.strokeStyle = TOOL.color; ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
731
|
+
ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.moveTo(spts[si0].x, spts[si0].y);
|
|
732
|
+
for(var si = si0 + 1; si <= sidx; si++) ctx.lineTo(spts[si].x, spts[si].y);
|
|
733
|
+
ctx.stroke(); ctx.globalAlpha = 1;
|
|
734
|
+
_drawName(ctx, s, 1, undefined);
|
|
735
|
+
break;
|
|
736
|
+
case 'orbit':
|
|
737
|
+
_bracket(ctx, s, TOOL.color, 0.15);
|
|
738
|
+
_drawName(ctx, s, 1, 0.4);
|
|
739
|
+
var pts = _getOpenPts(s), speed = 1200, trailFrac = 0.28;
|
|
740
|
+
var halfCycle = (e % speed) / speed;
|
|
741
|
+
var cycle = (e % (speed * 2)) / (speed * 2);
|
|
742
|
+
var pos = cycle < 0.5 ? halfCycle : 1 - halfCycle;
|
|
743
|
+
var headIdx = Math.floor(pos * (pts.length - 1));
|
|
744
|
+
var trailLen = Math.floor(trailFrac * pts.length);
|
|
745
|
+
var dir = cycle < 0.5 ? 1 : -1;
|
|
746
|
+
ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
747
|
+
for(var i = 0; i < trailLen; i++) {
|
|
748
|
+
var idx = headIdx - dir * (trailLen - i);
|
|
749
|
+
if(idx < 0 || idx >= pts.length) continue;
|
|
750
|
+
var nxt = idx + dir;
|
|
751
|
+
if(nxt < 0 || nxt >= pts.length) continue;
|
|
752
|
+
ctx.globalAlpha = (i / trailLen) * 0.7; ctx.strokeStyle = TOOL.color;
|
|
753
|
+
ctx.beginPath(); ctx.moveTo(pts[idx].x, pts[idx].y); ctx.lineTo(pts[nxt].x, pts[nxt].y); ctx.stroke();
|
|
754
|
+
}
|
|
755
|
+
ctx.globalAlpha = 1;
|
|
756
|
+
break;
|
|
757
|
+
case 'dim':
|
|
758
|
+
var dim = 0.1 + 0.08 * Math.sin(e / 2000);
|
|
759
|
+
_bracket(ctx, s, TOOL.color, dim);
|
|
760
|
+
_drawName(ctx, s, 1, 0.2);
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
_raf = requestAnimationFrame(_frame);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Init: measure text, size canvas, start animation
|
|
767
|
+
_c = document.getElementById('grainLogo');
|
|
768
|
+
if(_c) {
|
|
769
|
+
_c.style.width = '0px';
|
|
770
|
+
_s = 256;
|
|
771
|
+
var targetFontPx = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
772
|
+
var fontRatio = 0.38;
|
|
773
|
+
var dh = 64;
|
|
774
|
+
_c.height = _s; _c.width = 1024;
|
|
775
|
+
_ctx = _c.getContext('2d');
|
|
776
|
+
_cx = _s / 2;
|
|
777
|
+
_restText = TOOL.name.slice(1);
|
|
778
|
+
_font = '800 ' + (_s * fontRatio) + 'px -apple-system,"SF Pro Display","Helvetica Neue",Arial,sans-serif';
|
|
779
|
+
_ctx.font = _font;
|
|
780
|
+
var letterW = _ctx.measureText(TOOL.letter).width;
|
|
781
|
+
var restW = _restText.length > 0 ? _ctx.measureText(_restText).width : 0;
|
|
782
|
+
_textStart = _cx + letterW / 2 + _s * 0.02;
|
|
783
|
+
var totalW = Math.ceil(_textStart + restW + _s * 0.12);
|
|
784
|
+
_c.width = totalW;
|
|
785
|
+
_ctx = _c.getContext('2d');
|
|
786
|
+
_c.style.height = dh + 'px';
|
|
787
|
+
_c.style.width = Math.round(totalW / _s * dh) + 'px';
|
|
788
|
+
_state = 'drawon'; _start = null;
|
|
789
|
+
_raf = requestAnimationFrame(_frame);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Wire to SSE connection state (defer during drawon)
|
|
793
|
+
window._grainSetState = function(state) {
|
|
794
|
+
if(_state === state) return;
|
|
795
|
+
if(_state === 'drawon') { _pendingState = state; return; }
|
|
796
|
+
_state = state; _start = null;
|
|
797
|
+
if(!_raf) _raf = requestAnimationFrame(_frame);
|
|
798
|
+
};
|
|
799
|
+
var _pendingState = null;
|
|
800
|
+
})();
|
|
801
|
+
</script>
|
|
802
|
+
</body>
|
|
803
|
+
</html>
|