@grainulation/barn 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/public/index.html +0 -905
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grainulation/barn",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Template browser and sprint toolkit for the grainulation ecosystem",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "grainulation contributors",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"bin/",
|
|
39
39
|
"lib/",
|
|
40
|
-
"public/",
|
|
40
|
+
"public/grainulation-tokens.css",
|
|
41
|
+
"public/status-icons.svg",
|
|
42
|
+
"public/grainulation-icons.svg",
|
|
41
43
|
"tools/",
|
|
42
44
|
"templates/",
|
|
43
45
|
"CHANGELOG.md",
|
package/public/index.html
DELETED
|
@@ -1,905 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" dir="auto" data-tool="barn">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Barn</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='%23f43f5e' font-family='-apple-system,system-ui,sans-serif' font-size='34' font-weight='800'>B</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
|
-
/* barn accent */
|
|
23
|
-
--accent: #f43f5e; --accent-light: #fb7185; --accent-dim: rgba(244,63,94,0.10); --accent-border: rgba(244,63,94,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; padding-top:var(--space-xl) }
|
|
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: barn-specific -->
|
|
112
|
-
<style>
|
|
113
|
-
.preview-header { padding:var(--space-lg) var(--space-xl); border-bottom:1px solid var(--border); background:var(--bg2); display:flex; align-items:center; gap:var(--space-lg) }
|
|
114
|
-
.preview-header .title { font-size:var(--text-lg); font-weight:700 }
|
|
115
|
-
.preview-header .badge { font-size:var(--text-xs); padding:2px var(--space-sm); border-radius:var(--radius-sm); background:var(--accent-dim); color:var(--accent-light); font-weight:600; text-transform:uppercase; letter-spacing:0.04em }
|
|
116
|
-
.preview-tabs { display:flex; border-bottom:1px solid var(--border); background:var(--bg2); padding:0 var(--space-xl) }
|
|
117
|
-
.preview-tab { padding:var(--space-sm) var(--space-lg); font-size:11px; color:var(--fg3); cursor:pointer; border-bottom:2px solid transparent; font-family:inherit; background:none; border-top:none; border-inline-start:none; border-inline-end:none }
|
|
118
|
-
.preview-tab:hover { color:var(--fg2) }
|
|
119
|
-
.preview-tab.active { color:var(--accent-light); border-bottom-color:var(--accent) }
|
|
120
|
-
.preview-content { padding:0; flex:1; overflow:hidden }
|
|
121
|
-
.source-view { padding:20px var(--space-xl); font-family:var(--font-mono); font-size:11px; line-height:1.6; color:var(--fg2); white-space:pre-wrap; overflow-y:auto; height:100%; tab-size:2 }
|
|
122
|
-
.source-view .line-num { display:inline-block; width:36px; text-align:end; color:var(--fg3); margin-inline-end:var(--space-lg); user-select:none; opacity:0.5 }
|
|
123
|
-
.source-view .placeholder { color:var(--orange); font-weight:600 }
|
|
124
|
-
.source-view .comment { color:var(--fg3); font-style:italic }
|
|
125
|
-
.source-view .tag { color:var(--accent-light) }
|
|
126
|
-
.source-view .attr { color:var(--purple) }
|
|
127
|
-
.source-view .str { color:var(--green) }
|
|
128
|
-
.render-frame { width:100%; height:100%; border:none; background:#fff }
|
|
129
|
-
.info-panel { padding:var(--space-xl); overflow-y:auto; height:100% }
|
|
130
|
-
.info-panel h3 { font-size:var(--text-base); font-weight:600; color:var(--accent-light); text-transform:uppercase; letter-spacing:0.08em; margin-bottom:var(--space-md) }
|
|
131
|
-
.info-row { display:flex; justify-content:space-between; padding:var(--space-sm) 0; border-bottom:1px solid var(--border); font-size:var(--text-base) }
|
|
132
|
-
.info-row .label { color:var(--fg3) }
|
|
133
|
-
.info-row .value { color:var(--fg); font-weight:500 }
|
|
134
|
-
.placeholder-list { display:flex; flex-wrap:wrap; gap:6px; margin-top:var(--space-sm) }
|
|
135
|
-
.placeholder-tag { font-family:var(--font-mono); font-size:var(--text-sm); padding:3px var(--space-sm); background:rgba(251,146,60,0.1); color:var(--orange); border-radius:var(--radius-sm); border:1px solid rgba(251,146,60,0.2) }
|
|
136
|
-
.preview-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--fg3); gap:var(--space-sm) }
|
|
137
|
-
.preview-empty .text { font-size:var(--text-md) }
|
|
138
|
-
.welcome-feat { display:inline-block; padding:2px var(--space-sm); background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius-sm); font-size:var(--text-sm); color:var(--fg2); margin:2px }
|
|
139
|
-
</style>
|
|
140
|
-
</head>
|
|
141
|
-
<body>
|
|
142
|
-
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
143
|
-
<div id="live-status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
|
|
144
|
-
<div class="reconnect-banner" id="reconnectBanner" role="status" aria-live="polite"></div>
|
|
145
|
-
<div class="app">
|
|
146
|
-
<header class="toolbar" role="banner">
|
|
147
|
-
<canvas id="grainLogo" width="256" height="256"></canvas>
|
|
148
|
-
<div class="toolbar-spacer"></div>
|
|
149
|
-
<div class="toolbar-right">
|
|
150
|
-
<div class="status-dot" id="sse-dot" aria-label="Connection status" role="status"></div>
|
|
151
|
-
</div>
|
|
152
|
-
</header>
|
|
153
|
-
<nav class="mobile-nav" aria-label="Mobile navigation">
|
|
154
|
-
<div class="mobile-nav-bar">
|
|
155
|
-
<button class="mobile-tab active" data-panel="content">Preview</button>
|
|
156
|
-
<button class="mobile-tab" data-panel="sidebar">Templates</button>
|
|
157
|
-
</div>
|
|
158
|
-
</nav>
|
|
159
|
-
<aside class="sidebar" id="sidebar" aria-label="Template list">
|
|
160
|
-
<!-- sidebar rendered by JS -->
|
|
161
|
-
</aside>
|
|
162
|
-
<main class="content" id="main-content" aria-label="Barn workspace">
|
|
163
|
-
<div class="welcome" id="barn-welcome"><h2>Barn</h2><div class="subtitle">Loading templates...</div></div>
|
|
164
|
-
</main>
|
|
165
|
-
<footer class="footer">
|
|
166
|
-
<span>barn v1.0.1 -- @grainulation/barn</span>
|
|
167
|
-
<div class="footer-links">
|
|
168
|
-
<a href="http://localhost:9091">wheat</a>
|
|
169
|
-
<a href="http://localhost:9094">mill</a>
|
|
170
|
-
<a href="http://localhost:9095">silo</a>
|
|
171
|
-
<a href="http://localhost:9090">farmer</a>
|
|
172
|
-
<a href="http://localhost:9096">harvest</a>
|
|
173
|
-
</div>
|
|
174
|
-
</footer>
|
|
175
|
-
</div>
|
|
176
|
-
<div class="toast-container" id="toast-container" aria-live="polite" role="status"></div>
|
|
177
|
-
|
|
178
|
-
<script>
|
|
179
|
-
// -- Shared: helpers --
|
|
180
|
-
var $ = function(id) { return document.getElementById(id); };
|
|
181
|
-
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
182
|
-
|
|
183
|
-
// -- Shared: SSE with exponential backoff --
|
|
184
|
-
var sseRetryCount = 0;
|
|
185
|
-
var sseSource = null;
|
|
186
|
-
function showBanner(count) {
|
|
187
|
-
var b = $('reconnectBanner');
|
|
188
|
-
if (count > 5) {
|
|
189
|
-
b.textContent = '';
|
|
190
|
-
b.appendChild(document.createTextNode('Connection lost. '));
|
|
191
|
-
var btn = document.createElement('button');
|
|
192
|
-
btn.textContent = 'Retry now';
|
|
193
|
-
btn.addEventListener('click', function() { sseRetryCount = 0; connectSSE(); });
|
|
194
|
-
b.appendChild(btn);
|
|
195
|
-
} else if (count > 1) {
|
|
196
|
-
b.textContent = 'Reconnecting (attempt ' + count + ')...';
|
|
197
|
-
} else {
|
|
198
|
-
b.textContent = 'Reconnecting...';
|
|
199
|
-
}
|
|
200
|
-
b.classList.add('visible');
|
|
201
|
-
}
|
|
202
|
-
function hideBanner() { $('reconnectBanner').classList.remove('visible'); }
|
|
203
|
-
|
|
204
|
-
function connectSSE() {
|
|
205
|
-
sseSource = new EventSource('/events');
|
|
206
|
-
sseSource.onopen = function() {
|
|
207
|
-
sseRetryCount = 0;
|
|
208
|
-
$('sse-dot').className = 'status-dot ok';
|
|
209
|
-
if (window._grainSetState) window._grainSetState('idle');
|
|
210
|
-
};
|
|
211
|
-
sseSource.onerror = function() {
|
|
212
|
-
sseSource.close();
|
|
213
|
-
$('sse-dot').className = 'status-dot';
|
|
214
|
-
if (window._grainSetState) window._grainSetState('orbit');
|
|
215
|
-
var delay = Math.min(30000, 1000 * Math.pow(2, sseRetryCount)) + Math.random() * 1000;
|
|
216
|
-
sseRetryCount++;
|
|
217
|
-
setTimeout(connectSSE, delay);
|
|
218
|
-
};
|
|
219
|
-
sseSource.onmessage = function(e) {
|
|
220
|
-
try {
|
|
221
|
-
var msg = JSON.parse(e.data);
|
|
222
|
-
onSSEMessage(msg); // tool-specific handler
|
|
223
|
-
} catch(ex) {}
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// -- Shared: mobile panel switching --
|
|
228
|
-
function switchMobilePanel(panel) {
|
|
229
|
-
var sidebar = document.querySelector('.sidebar');
|
|
230
|
-
var content = document.querySelector('.content');
|
|
231
|
-
document.querySelectorAll('.mobile-tab').forEach(function(tb) { tb.classList.toggle('active', tb.dataset.panel === panel); });
|
|
232
|
-
sidebar.classList.remove('mobile-visible');
|
|
233
|
-
content.classList.remove('mobile-hidden');
|
|
234
|
-
if (panel === 'sidebar') { sidebar.classList.add('mobile-visible'); content.classList.add('mobile-hidden'); }
|
|
235
|
-
}
|
|
236
|
-
document.querySelector('.mobile-nav-bar').addEventListener('click', function(e) {
|
|
237
|
-
var tab = e.target.closest('[data-panel]');
|
|
238
|
-
if (tab) switchMobilePanel(tab.dataset.panel);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// -- Barn: toast notifications --
|
|
242
|
-
function toast(message) {
|
|
243
|
-
var container = $('toast-container');
|
|
244
|
-
var el = document.createElement('div');
|
|
245
|
-
el.className = 'toast'; el.textContent = message;
|
|
246
|
-
container.appendChild(el);
|
|
247
|
-
setTimeout(function() { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s ease'; setTimeout(function() { el.remove(); }, 300); }, 3000);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// -- Barn: i18n passthrough --
|
|
251
|
-
var t = function(s, v) { return v ? s.replace(/\{(\w+)\}/g, function(_, k) { return v[k] != null ? v[k] : k; }) : s; };
|
|
252
|
-
|
|
253
|
-
// -- Barn: state --
|
|
254
|
-
var state = { templates: [], sprints: [], activeSprint: null, manifest: null };
|
|
255
|
-
var selected = null;
|
|
256
|
-
var activeTab = 'source';
|
|
257
|
-
var searchQuery = '';
|
|
258
|
-
var activeTagFilter = null;
|
|
259
|
-
|
|
260
|
-
// -- Barn: SSE message handler --
|
|
261
|
-
function onSSEMessage(msg) {
|
|
262
|
-
if (msg.type === 'state') {
|
|
263
|
-
state = msg.data;
|
|
264
|
-
render();
|
|
265
|
-
var ls = $('live-status');
|
|
266
|
-
if (ls) ls.textContent = t('Updated: {count} templates loaded', { count: (state.templates || []).length });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// -- Barn: refresh --
|
|
271
|
-
function refresh() {
|
|
272
|
-
fetch('/api/refresh', { method: 'POST' }).then(function(resp) {
|
|
273
|
-
return resp.json();
|
|
274
|
-
}).then(function(data) {
|
|
275
|
-
state = data;
|
|
276
|
-
render();
|
|
277
|
-
}).catch(function() {});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// -- Barn: rendering --
|
|
281
|
-
function formatBytes(b) {
|
|
282
|
-
if (b < 1024) return b + ' B';
|
|
283
|
-
return (b / 1024).toFixed(1) + ' KB';
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function render() {
|
|
287
|
-
renderSidebar();
|
|
288
|
-
renderMain();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function getFilteredTemplates() {
|
|
292
|
-
var q = searchQuery.toLowerCase();
|
|
293
|
-
return state.templates.filter(function(tpl) {
|
|
294
|
-
// Tag filter
|
|
295
|
-
if (activeTagFilter) {
|
|
296
|
-
var allTags = (tpl.features || []).concat(tpl.tags || []);
|
|
297
|
-
if (!allTags.some(function(tag) { return tag.toLowerCase() === activeTagFilter.toLowerCase(); })) return false;
|
|
298
|
-
}
|
|
299
|
-
// Text search
|
|
300
|
-
if (!q) return true;
|
|
301
|
-
var haystack = [
|
|
302
|
-
tpl.name,
|
|
303
|
-
tpl.title || '',
|
|
304
|
-
tpl.description || '',
|
|
305
|
-
].concat(tpl.placeholders || []).concat(tpl.features || []).concat(tpl.tags || []).join(' ').toLowerCase();
|
|
306
|
-
return haystack.indexOf(q) !== -1;
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function renderSidebar() {
|
|
311
|
-
var sidebar = $('sidebar');
|
|
312
|
-
var filtered = getFilteredTemplates();
|
|
313
|
-
var total = state.templates.length;
|
|
314
|
-
var shown = filtered.length;
|
|
315
|
-
var isFiltered = searchQuery || activeTagFilter;
|
|
316
|
-
|
|
317
|
-
var html = '<div class="search-box">'
|
|
318
|
-
+ '<input type="search" id="searchInput" placeholder="' + t('Filter templates...') + '" value="' + esc(searchQuery) + '">';
|
|
319
|
-
if (activeTagFilter) {
|
|
320
|
-
html += '<div class="search-count" role="status"><span style="color:var(--blue)">[' + esc(activeTagFilter) + ']</span>'
|
|
321
|
-
+ ' <button data-action="clear-tag" style="background:none;border:none;color:var(--accent-light);cursor:pointer;font-size:10px;font-family:inherit;padding:0;margin-inline-start:4px">clear</button></div>';
|
|
322
|
-
}
|
|
323
|
-
html += '</div>';
|
|
324
|
-
|
|
325
|
-
html += '<div class="item-list" role="listbox" aria-label="Templates">';
|
|
326
|
-
|
|
327
|
-
if (filtered.length === 0) {
|
|
328
|
-
var msg = total === 0
|
|
329
|
-
? t('No templates found. Add .html files to the templates/ directory.')
|
|
330
|
-
: t('No templates match your filter.');
|
|
331
|
-
html += '<div class="item-no-results">' + msg + '</div>';
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
for (var i = 0; i < filtered.length; i++) {
|
|
335
|
-
var tpl = filtered[i];
|
|
336
|
-
var isActive = selected === tpl.name;
|
|
337
|
-
var allTags = (tpl.features || []).concat(tpl.tags || []);
|
|
338
|
-
html += '<div class="item-card ' + (isActive ? 'active' : '') + '" data-template="' + esc(tpl.name) + '" tabindex="0" role="button" aria-pressed="' + isActive + '">'
|
|
339
|
-
+ '<div class="item-name">' + esc(tpl.title || tpl.name) + '</div>'
|
|
340
|
-
+ (tpl.description ? '<div class="item-desc">' + esc(tpl.description) + '</div>' : '')
|
|
341
|
-
+ '<div class="item-meta">'
|
|
342
|
-
+ '<span class="item-badge">' + esc(tpl.lines) + ' lines</span>'
|
|
343
|
-
+ '<span class="item-badge">' + formatBytes(tpl.size) + '</span>';
|
|
344
|
-
for (var j = 0; j < allTags.length; j++) {
|
|
345
|
-
var f = allTags[j];
|
|
346
|
-
html += '<span class="item-badge feature' + (activeTagFilter && f.toLowerCase() === activeTagFilter.toLowerCase() ? ' active-filter' : '') + '" data-filter-tag="' + esc(f) + '">' + esc(f) + '</span>';
|
|
347
|
-
}
|
|
348
|
-
html += '</div></div>';
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (state.sprints.length > 0) {
|
|
352
|
-
html += '<div style="padding:var(--space-sm) 0"><span class="search-label" style="padding:var(--space-sm) var(--space-sm) 6px;display:block">' + t('Sprints') + '</span>';
|
|
353
|
-
for (var k = 0; k < state.sprints.length; k++) {
|
|
354
|
-
var s = state.sprints[k];
|
|
355
|
-
var sprintActive = s.status === 'active';
|
|
356
|
-
html += '<div class="item-card' + (sprintActive ? ' active' : '') + '">'
|
|
357
|
-
+ '<div class="item-name">' + esc(s.name || 'unnamed') + '</div>'
|
|
358
|
-
+ '<div class="item-meta">'
|
|
359
|
-
+ '<span class="item-badge" style="color:var(--blue)">' + esc(s.phase) + '</span>'
|
|
360
|
-
+ '<span class="item-badge">' + esc(s.claims_count || 0) + ' claims</span>'
|
|
361
|
-
+ '</div></div>';
|
|
362
|
-
}
|
|
363
|
-
html += '</div>';
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
html += '</div>';
|
|
367
|
-
sidebar.innerHTML = html;
|
|
368
|
-
|
|
369
|
-
if (isFiltered) {
|
|
370
|
-
var ls = $('live-status');
|
|
371
|
-
if (ls) ls.textContent = shown + ' of ' + total + ' templates shown';
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
var input = document.getElementById('searchInput');
|
|
375
|
-
if (input) {
|
|
376
|
-
input.addEventListener('input', onSearchInput);
|
|
377
|
-
if (document.activeElement && document.activeElement.id === 'searchInput') {
|
|
378
|
-
input.focus();
|
|
379
|
-
input.selectionStart = input.selectionEnd = input.value.length;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
var searchDebounce = null;
|
|
385
|
-
function onSearchInput(e) {
|
|
386
|
-
searchQuery = e.target.value;
|
|
387
|
-
clearTimeout(searchDebounce);
|
|
388
|
-
searchDebounce = setTimeout(function() {
|
|
389
|
-
renderSidebar();
|
|
390
|
-
var input = document.getElementById('searchInput');
|
|
391
|
-
if (input) { input.focus(); input.selectionStart = input.selectionEnd = input.value.length; }
|
|
392
|
-
}, 150);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function filterByTag(tag) {
|
|
396
|
-
if (activeTagFilter && activeTagFilter.toLowerCase() === tag.toLowerCase()) {
|
|
397
|
-
activeTagFilter = null;
|
|
398
|
-
} else {
|
|
399
|
-
activeTagFilter = tag;
|
|
400
|
-
}
|
|
401
|
-
renderSidebar();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function clearTagFilter() {
|
|
405
|
-
activeTagFilter = null;
|
|
406
|
-
renderSidebar();
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function renderBarnWelcome() {
|
|
410
|
-
var main = $('main-content');
|
|
411
|
-
var templates = state && state.templates ? state.templates : [];
|
|
412
|
-
var sprints = state && state.sprints ? state.sprints : [];
|
|
413
|
-
|
|
414
|
-
// Collect all unique features/tags
|
|
415
|
-
var allFeats = [];
|
|
416
|
-
var featSet = {};
|
|
417
|
-
for (var i = 0; i < templates.length; i++) {
|
|
418
|
-
var feats = templates[i].features || [];
|
|
419
|
-
var tags = templates[i].tags || [];
|
|
420
|
-
for (var j = 0; j < feats.length; j++) { if (!featSet[feats[j]]) { featSet[feats[j]] = true; allFeats.push(feats[j]); } }
|
|
421
|
-
for (var k = 0; k < tags.length; k++) { if (!featSet[tags[k]]) { featSet[tags[k]] = true; allFeats.push(tags[k]); } }
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
var tplList = '';
|
|
425
|
-
for (var i = 0; i < templates.length; i++) {
|
|
426
|
-
var tpl = templates[i];
|
|
427
|
-
tplList += '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04)">'
|
|
428
|
-
+ '<span style="font-size:12px;font-weight:600;color:var(--fg)">' + esc(tpl.name) + '</span>'
|
|
429
|
-
+ '<span style="font-size:10px;color:var(--fg3)">' + (tpl.placeholders || []).length + ' placeholders, ' + (tpl.lines || '?') + ' lines</span>'
|
|
430
|
-
+ '</div>';
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
var featsHtml = '';
|
|
434
|
-
for (var i = 0; i < allFeats.length; i++) {
|
|
435
|
-
featsHtml += '<span class="welcome-feat">' + esc(allFeats[i]) + '</span>';
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
main.innerHTML =
|
|
439
|
-
'<div class="welcome">'
|
|
440
|
-
+ '<h2>Barn</h2>'
|
|
441
|
-
+ '<div class="subtitle">'
|
|
442
|
-
+ 'HTML templates for wheat sprint output. '
|
|
443
|
-
+ 'Barn stores self-contained, dark-theme templates that wheat uses to generate reports, presentations, dashboards, and other deliverables. '
|
|
444
|
-
+ 'Each template declares its placeholders, so wheat knows exactly what data to inject.'
|
|
445
|
-
+ '</div>'
|
|
446
|
-
|
|
447
|
-
+ '<div class="welcome-section">'
|
|
448
|
-
+ '<h3>How it works</h3>'
|
|
449
|
-
+ '<div class="welcome-step">'
|
|
450
|
-
+ '<span class="welcome-step-num">1</span>'
|
|
451
|
-
+ '<div class="welcome-step-text"><strong>Browse templates</strong> -- the sidebar lists all available templates. Use the search bar or click a tag to filter.</div>'
|
|
452
|
-
+ '</div>'
|
|
453
|
-
+ '<div class="welcome-step">'
|
|
454
|
-
+ '<span class="welcome-step-num">2</span>'
|
|
455
|
-
+ '<div class="welcome-step-text"><strong>Preview and inspect</strong> -- click a template to see its rendered preview, raw HTML source, and declared placeholders. Toggle between tabs.</div>'
|
|
456
|
-
+ '</div>'
|
|
457
|
-
+ '<div class="welcome-step">'
|
|
458
|
-
+ '<span class="welcome-step-num">3</span>'
|
|
459
|
-
+ '<div class="welcome-step-text"><strong>Used by wheat automatically</strong> -- when you run <strong>/brief</strong>, <strong>/present</strong>, or <strong>/status</strong>, wheat pulls the right template from barn and fills in your sprint data.</div>'
|
|
460
|
-
+ '</div>'
|
|
461
|
-
+ '</div>'
|
|
462
|
-
|
|
463
|
-
+ (templates.length > 0 ? '<div class="welcome-section">'
|
|
464
|
-
+ '<h3>Templates (' + templates.length + ')</h3>'
|
|
465
|
-
+ tplList
|
|
466
|
-
+ '</div>' : '')
|
|
467
|
-
|
|
468
|
-
+ (allFeats.length > 0 ? '<div class="welcome-section">'
|
|
469
|
-
+ '<h3>Features and tags</h3>'
|
|
470
|
-
+ '<div>' + featsHtml + '</div>'
|
|
471
|
-
+ '</div>' : '')
|
|
472
|
-
|
|
473
|
-
+ (sprints.length > 0 ? '<div class="welcome-section">'
|
|
474
|
-
+ '<h3>Active sprints detected (' + sprints.length + ')</h3>'
|
|
475
|
-
+ '<div style="font-size:12px;color:var(--fg2);line-height:1.6">'
|
|
476
|
-
+ sprints.map(function(s) { return esc(s.name || s.dir); }).join(', ')
|
|
477
|
-
+ '</div>'
|
|
478
|
-
+ '</div>' : '')
|
|
479
|
-
|
|
480
|
-
+ '<div class="welcome-section">'
|
|
481
|
-
+ '<h3>Keyboard shortcuts</h3>'
|
|
482
|
-
+ '<div class="welcome-step">'
|
|
483
|
-
+ '<div class="welcome-step-text">'
|
|
484
|
-
+ '<span class="welcome-kbd">[</span> <span class="welcome-kbd">]</span> Switch panels '
|
|
485
|
-
+ ' <span class="welcome-kbd">Esc</span> Return to this page'
|
|
486
|
-
+ ' <span class="welcome-kbd">/</span> Search'
|
|
487
|
-
+ '</div>'
|
|
488
|
-
+ '</div>'
|
|
489
|
-
+ '</div>'
|
|
490
|
-
+ '</div>';
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function renderMain() {
|
|
494
|
-
var main = $('main-content');
|
|
495
|
-
if (!selected) {
|
|
496
|
-
renderBarnWelcome();
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
var tpl = null;
|
|
501
|
-
for (var i = 0; i < state.templates.length; i++) {
|
|
502
|
-
if (state.templates[i].name === selected) { tpl = state.templates[i]; break; }
|
|
503
|
-
}
|
|
504
|
-
if (!tpl) return;
|
|
505
|
-
|
|
506
|
-
main.innerHTML =
|
|
507
|
-
'<div class="preview-header">'
|
|
508
|
-
+ '<span class="title">' + esc(tpl.name) + '.html</span>'
|
|
509
|
-
+ '<span class="badge">' + esc(tpl.features[0] || t('template')) + '</span>'
|
|
510
|
-
+ '<span style="margin-inline-start:auto;font-size:10px;color:var(--fg3)">' + esc(tpl.placeholders.length) + ' placeholders · ' + esc(tpl.lines) + ' lines</span>'
|
|
511
|
-
+ '</div>'
|
|
512
|
-
+ '<div class="preview-tabs" role="tablist" aria-label="Template view tabs">'
|
|
513
|
-
+ '<button class="preview-tab ' + (activeTab === 'source' ? 'active' : '') + '" data-tab="source" role="tab" id="tab-source" aria-selected="' + (activeTab === 'source') + '" aria-controls="previewContent">Source</button>'
|
|
514
|
-
+ '<button class="preview-tab ' + (activeTab === 'preview' ? 'active' : '') + '" data-tab="preview" role="tab" id="tab-preview" aria-selected="' + (activeTab === 'preview') + '" aria-controls="previewContent">Preview</button>'
|
|
515
|
-
+ '<button class="preview-tab ' + (activeTab === 'info' ? 'active' : '') + '" data-tab="info" role="tab" id="tab-info" aria-selected="' + (activeTab === 'info') + '" aria-controls="previewContent">Info</button>'
|
|
516
|
-
+ '</div>'
|
|
517
|
-
+ '<div class="preview-content" id="previewContent" role="tabpanel" aria-labelledby="tab-' + activeTab + '"></div>';
|
|
518
|
-
|
|
519
|
-
loadTabContent();
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function loadTabContent() {
|
|
523
|
-
var container = $('previewContent');
|
|
524
|
-
if (!selected || !container) return;
|
|
525
|
-
|
|
526
|
-
if (activeTab === 'source') {
|
|
527
|
-
fetch('/api/template?name=' + encodeURIComponent(selected)).then(function(resp) {
|
|
528
|
-
return resp.text();
|
|
529
|
-
}).then(function(text) {
|
|
530
|
-
container.innerHTML = '<div class="source-view">' + highlightHTML(text) + '</div>';
|
|
531
|
-
}).catch(function() {
|
|
532
|
-
container.innerHTML = '<div class="source-view">' + t('failed to load') + '</div>';
|
|
533
|
-
});
|
|
534
|
-
} else if (activeTab === 'preview') {
|
|
535
|
-
fetch('/api/template?name=' + encodeURIComponent(selected)).then(function(resp) {
|
|
536
|
-
return resp.text();
|
|
537
|
-
}).then(function(text) {
|
|
538
|
-
// Replace placeholders with sample content
|
|
539
|
-
text = text.replace(/\{\{TITLE\}\}/g, 'Sample Title');
|
|
540
|
-
text = text.replace(/\{\{SUBTITLE\}\}/g, 'A sample subtitle for preview');
|
|
541
|
-
text = text.replace(/\{\{[A-Z_]+\}\}/g, '...');
|
|
542
|
-
var blob = new Blob([text], { type: 'text/html' });
|
|
543
|
-
var url = URL.createObjectURL(blob);
|
|
544
|
-
container.innerHTML = '<iframe class="render-frame" sandbox="allow-same-origin" title="Template preview" src="' + url + '"></iframe>';
|
|
545
|
-
}).catch(function() {
|
|
546
|
-
container.innerHTML = '<div class="preview-empty"><div class="text">' + t('preview failed') + '</div></div>';
|
|
547
|
-
});
|
|
548
|
-
} else if (activeTab === 'info') {
|
|
549
|
-
var tpl = null;
|
|
550
|
-
for (var i = 0; i < state.templates.length; i++) {
|
|
551
|
-
if (state.templates[i].name === selected) { tpl = state.templates[i]; break; }
|
|
552
|
-
}
|
|
553
|
-
if (!tpl) return;
|
|
554
|
-
var placeholderHtml = '';
|
|
555
|
-
for (var j = 0; j < tpl.placeholders.length; j++) {
|
|
556
|
-
placeholderHtml += '<span class="placeholder-tag">' + esc(tpl.placeholders[j]) + '</span>';
|
|
557
|
-
}
|
|
558
|
-
container.innerHTML = '<div class="info-panel">'
|
|
559
|
-
+ '<h3>' + t('Template Details') + '</h3>'
|
|
560
|
-
+ '<div class="info-row"><span class="label">' + t('File') + '</span><span class="value">' + esc(tpl.file) + '</span></div>'
|
|
561
|
-
+ '<div class="info-row"><span class="label">' + t('Lines') + '</span><span class="value">' + esc(tpl.lines) + '</span></div>'
|
|
562
|
-
+ '<div class="info-row"><span class="label">' + t('Size') + '</span><span class="value">' + formatBytes(tpl.size) + '</span></div>'
|
|
563
|
-
+ '<div class="info-row"><span class="label">' + t('Features') + '</span><span class="value">' + (tpl.features.map(function(f) { return esc(f); }).join(', ') || t('none')) + '</span></div>'
|
|
564
|
-
+ '<h3 style="margin-top:24px">' + t('Placeholders') + '</h3>'
|
|
565
|
-
+ '<div class="placeholder-list">' + placeholderHtml + '</div>'
|
|
566
|
-
+ (tpl.description ? '<h3 style="margin-top:24px">' + t('Description') + '</h3><p style="font-size:12px;color:var(--fg2);line-height:1.6;">' + esc(tpl.description) + '</p>' : '')
|
|
567
|
-
+ '</div>';
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Focus management after tab switch
|
|
571
|
-
container.setAttribute('tabindex', '-1');
|
|
572
|
-
container.focus();
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function highlightHTML(text) {
|
|
576
|
-
var lines = text.split('\n');
|
|
577
|
-
var result = [];
|
|
578
|
-
for (var i = 0; i < lines.length; i++) {
|
|
579
|
-
var highlighted = esc(lines[i]);
|
|
580
|
-
// Highlight placeholders
|
|
581
|
-
highlighted = highlighted.replace(/(\{\{[A-Z_]+\}\})/g, '<span class="placeholder">$1</span>');
|
|
582
|
-
// Highlight HTML comments
|
|
583
|
-
highlighted = highlighted.replace(/(<!--.*?-->)/g, '<span class="comment">$1</span>');
|
|
584
|
-
result.push('<span class="line-num">' + (i + 1) + '</span>' + highlighted);
|
|
585
|
-
}
|
|
586
|
-
return result.join('\n');
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function selectTemplate(name) {
|
|
590
|
-
selected = name;
|
|
591
|
-
activeTab = 'source';
|
|
592
|
-
document.title = 'Barn - ' + (name);
|
|
593
|
-
render();
|
|
594
|
-
// On mobile, switch to content panel so the user sees the selected template
|
|
595
|
-
var mobileNav = document.querySelector('.mobile-nav');
|
|
596
|
-
if (mobileNav && mobileNav.offsetHeight > 0) {
|
|
597
|
-
switchMobilePanel('content');
|
|
598
|
-
}
|
|
599
|
-
// Focus management after render -- focus the first tab button
|
|
600
|
-
var firstTab = $('tab-source');
|
|
601
|
-
if (firstTab) firstTab.focus();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function switchTab(tab) {
|
|
605
|
-
activeTab = tab;
|
|
606
|
-
var tabs = document.querySelectorAll('.preview-tab');
|
|
607
|
-
tabs.forEach(function(tb) {
|
|
608
|
-
var isActive = (tb.dataset.tab || tb.textContent.toLowerCase()) === tab;
|
|
609
|
-
tb.classList.toggle('active', isActive);
|
|
610
|
-
tb.setAttribute('aria-selected', String(isActive));
|
|
611
|
-
});
|
|
612
|
-
// Update tabpanel aria-labelledby
|
|
613
|
-
var panel = $('previewContent');
|
|
614
|
-
if (panel) panel.setAttribute('aria-labelledby', 'tab-' + tab);
|
|
615
|
-
loadTabContent();
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// -- Barn: keyboard navigation --
|
|
619
|
-
var barnPanels = ['.sidebar', '.content'];
|
|
620
|
-
var barnActivePanel = 1;
|
|
621
|
-
function switchBarnPanel(dir) {
|
|
622
|
-
barnActivePanel = (barnActivePanel + dir + barnPanels.length) % barnPanels.length;
|
|
623
|
-
var el = document.querySelector(barnPanels[barnActivePanel]);
|
|
624
|
-
if (el) { el.setAttribute('tabindex', '-1'); el.focus(); }
|
|
625
|
-
}
|
|
626
|
-
document.addEventListener('keydown', function(e) {
|
|
627
|
-
if (e.key === '[' && !e.target.matches('input,textarea')) { e.preventDefault(); switchBarnPanel(-1); }
|
|
628
|
-
if (e.key === ']' && !e.target.matches('input,textarea')) { e.preventDefault(); switchBarnPanel(1); }
|
|
629
|
-
if (e.key === '/' && !e.target.matches('input,textarea,select')) { e.preventDefault(); var si = document.getElementById('searchInput'); if (si) si.focus(); }
|
|
630
|
-
if (e.key === 'Escape') {
|
|
631
|
-
if (searchQuery || activeTagFilter) {
|
|
632
|
-
searchQuery = '';
|
|
633
|
-
activeTagFilter = null;
|
|
634
|
-
render();
|
|
635
|
-
} else {
|
|
636
|
-
selected = null;
|
|
637
|
-
activeTab = 'source';
|
|
638
|
-
document.title = 'Barn';
|
|
639
|
-
render();
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Arrow key navigation for sidebar template cards
|
|
644
|
-
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !e.target.matches('input,textarea')) {
|
|
645
|
-
var cards = Array.from(document.querySelectorAll('#sidebar [data-template]'));
|
|
646
|
-
if (cards.length === 0) return;
|
|
647
|
-
var idx = cards.indexOf(document.activeElement);
|
|
648
|
-
if (idx === -1 && e.key === 'ArrowDown') {
|
|
649
|
-
cards[0].focus();
|
|
650
|
-
e.preventDefault();
|
|
651
|
-
} else if (idx >= 0) {
|
|
652
|
-
e.preventDefault();
|
|
653
|
-
var next = e.key === 'ArrowDown' ? Math.min(idx + 1, cards.length - 1) : Math.max(idx - 1, 0);
|
|
654
|
-
cards[next].focus();
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// -- Barn: event delegation --
|
|
660
|
-
$('sidebar').addEventListener('click', function(e) {
|
|
661
|
-
// Clear tag filter
|
|
662
|
-
if (e.target.closest('[data-action="clear-tag"]')) {
|
|
663
|
-
e.stopPropagation();
|
|
664
|
-
clearTagFilter();
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
// Tag filter click (handle before template selection)
|
|
668
|
-
var tagEl = e.target.closest('[data-filter-tag]');
|
|
669
|
-
if (tagEl) {
|
|
670
|
-
e.stopPropagation();
|
|
671
|
-
filterByTag(tagEl.dataset.filterTag);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
var card = e.target.closest('[data-template]');
|
|
675
|
-
if (card) selectTemplate(card.dataset.template);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// Keyboard access for template cards (Enter/Space)
|
|
679
|
-
$('sidebar').addEventListener('keydown', function(e) {
|
|
680
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
681
|
-
var card = e.target.closest('[data-template]');
|
|
682
|
-
if (card) {
|
|
683
|
-
e.preventDefault();
|
|
684
|
-
selectTemplate(card.dataset.template);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
$('main-content').addEventListener('click', function(e) {
|
|
690
|
-
var tab = e.target.closest('[data-tab]');
|
|
691
|
-
if (tab) switchTab(tab.dataset.tab);
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// -- Init --
|
|
695
|
-
connectSSE();
|
|
696
|
-
</script>
|
|
697
|
-
<script>
|
|
698
|
-
(function() {
|
|
699
|
-
var LW = 0.025;
|
|
700
|
-
var TOOL = { name: 'Barn', letter: 'B', color: '#f43f5e' };
|
|
701
|
-
var _c, _ctx, _s, _cx, _textStart, _restText, _font;
|
|
702
|
-
var _state = 'drawon', _start = null, _raf, _pendingState = null;
|
|
703
|
-
var _openPts = null, _closedPts = null;
|
|
704
|
-
|
|
705
|
-
function _lerp(a,b,t){ return {x:a.x+(b.x-a.x)*t, y:a.y+(b.y-a.y)*t}; }
|
|
706
|
-
function _easeInOut(t){ return t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2; }
|
|
707
|
-
|
|
708
|
-
function _bracket(ctx, s, color, alpha) {
|
|
709
|
-
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
710
|
-
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
711
|
-
if(alpha!==undefined) ctx.globalAlpha=alpha;
|
|
712
|
-
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
713
|
-
ctx.beginPath(); ctx.moveTo(cx,topY); ctx.lineTo(cx-fe,topY);
|
|
714
|
-
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
715
|
-
ctx.lineTo(cx,botY); ctx.stroke();
|
|
716
|
-
if(alpha!==undefined) ctx.globalAlpha=1;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function _drawBracket(ctx, s, color, progress) {
|
|
720
|
-
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
721
|
-
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
722
|
-
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
723
|
-
var seg1=0.12, seg2=0.72;
|
|
724
|
-
ctx.beginPath();
|
|
725
|
-
if(progress<=seg1){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe*(progress/seg1),topY);}
|
|
726
|
-
else if(progress<=seg1+seg2){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);ctx.stroke();ctx.beginPath();
|
|
727
|
-
var bt=(progress-seg1)/seg2;
|
|
728
|
-
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};
|
|
729
|
-
var q1=_lerp(p0,p1,bt),q2=_lerp(p1,p2,bt),q3=_lerp(p2,p3,bt);
|
|
730
|
-
var r1=_lerp(q1,q2,bt),r2=_lerp(q2,q3,bt),s1=_lerp(r1,r2,bt);
|
|
731
|
-
ctx.moveTo(p0.x,p0.y);ctx.bezierCurveTo(q1.x,q1.y,r1.x,r1.y,s1.x,s1.y);}
|
|
732
|
-
else{ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);
|
|
733
|
-
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
734
|
-
ctx.lineTo((cx-fe)+fe*((progress-seg1-seg2)/(1-seg1-seg2)),botY);}
|
|
735
|
-
ctx.stroke();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function _drawName(ctx, s, spellP, alpha) {
|
|
739
|
-
var a = alpha !== undefined ? alpha : 1;
|
|
740
|
-
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
741
|
-
var cy = s/2 + s*0.02;
|
|
742
|
-
ctx.globalAlpha = a; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
743
|
-
ctx.fillText(TOOL.letter, _cx, cy);
|
|
744
|
-
if(_restText.length > 0 && spellP > 0) {
|
|
745
|
-
var n = _restText.length, num = Math.min(n, Math.ceil(spellP * n));
|
|
746
|
-
var rawP = spellP * n, charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
747
|
-
var full = charP >= 1 ? num : num - 1;
|
|
748
|
-
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
749
|
-
if(full > 0) { ctx.globalAlpha = a; ctx.fillText(_restText.slice(0, full), _textStart, cy); }
|
|
750
|
-
if(full < num) {
|
|
751
|
-
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
752
|
-
ctx.globalAlpha = a * (0.3 + 0.7 * charP);
|
|
753
|
-
ctx.fillText(_restText[full], _textStart + prevW, cy);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
ctx.globalAlpha = 1;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function _getOpenPts(s) {
|
|
760
|
-
if(_openPts && _openPts._s === s) return _openPts;
|
|
761
|
-
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;
|
|
762
|
-
var pts=[];
|
|
763
|
-
for(var t=0;t<=1;t+=0.05) pts.push({x:cx-fe*t,y:topY});
|
|
764
|
-
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};
|
|
765
|
-
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});}
|
|
766
|
-
for(var t=0;t<=1;t+=0.05) pts.push({x:(cx-fe)+fe*t,y:botY});
|
|
767
|
-
pts._s=s; _openPts=pts; return pts;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function _getClosedPts(s) {
|
|
771
|
-
if(_closedPts && _closedPts._s === s) return _closedPts;
|
|
772
|
-
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;
|
|
773
|
-
var pts=[];
|
|
774
|
-
for(var t=0;t<=1;t+=0.03) pts.push({x:cx-fe*t,y:topY});
|
|
775
|
-
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};
|
|
776
|
-
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});}
|
|
777
|
-
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx-fe)+2*fe*t,y:botY});
|
|
778
|
-
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};
|
|
779
|
-
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});}
|
|
780
|
-
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx+fe)-fe*t,y:topY});
|
|
781
|
-
pts._s=s; _closedPts=pts; return pts;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function _frame(ts) {
|
|
785
|
-
if(!_c) return;
|
|
786
|
-
if(!_start) _start = ts;
|
|
787
|
-
var e = ts - _start, ctx = _ctx, s = _s;
|
|
788
|
-
ctx.clearRect(0, 0, _c.width, s);
|
|
789
|
-
switch(_state) {
|
|
790
|
-
case 'drawon':
|
|
791
|
-
var bp = _easeInOut(Math.min(1, e / 1400));
|
|
792
|
-
_drawBracket(ctx, s, TOOL.color, bp);
|
|
793
|
-
var la = Math.max(0, Math.min(1, (e - 900) / 400));
|
|
794
|
-
if(la > 0) {
|
|
795
|
-
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
796
|
-
ctx.globalAlpha = la; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
797
|
-
ctx.fillText(TOOL.letter, _cx, s/2 + s*0.02); ctx.globalAlpha = 1;
|
|
798
|
-
}
|
|
799
|
-
if(e > 1100 && _restText.length > 0) {
|
|
800
|
-
var sp = Math.min(1, (e - 1100) / (120 * _restText.length));
|
|
801
|
-
var n = _restText.length, num = Math.min(n, Math.ceil(sp * n));
|
|
802
|
-
if(num > 0) {
|
|
803
|
-
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
804
|
-
var cy = s/2 + s*0.02, rawP = sp * n;
|
|
805
|
-
var 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.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 = 0.3 + 0.7 * charP;
|
|
812
|
-
ctx.fillText(_restText[full], _textStart + prevW, cy); ctx.globalAlpha = 1;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
if(e > 1100 + 120 * _restText.length + 300) { _state = _pendingState || 'idle'; _pendingState = null; _start = ts; }
|
|
817
|
-
break;
|
|
818
|
-
case 'idle':
|
|
819
|
-
var breathe = 0.5 + 0.5 * (0.5 + 0.5 * Math.sin(e / 1200));
|
|
820
|
-
_bracket(ctx, s, TOOL.color, breathe);
|
|
821
|
-
var textBreath = 0.88 + 0.12 * (0.5 + 0.5 * Math.sin(e / 1800));
|
|
822
|
-
_drawName(ctx, s, 1, textBreath);
|
|
823
|
-
break;
|
|
824
|
-
case 'shimmer':
|
|
825
|
-
_bracket(ctx, s, TOOL.color, 0.2);
|
|
826
|
-
var spts = _getOpenPts(s), sspeed = 1800;
|
|
827
|
-
var spos = (e % sspeed) / sspeed;
|
|
828
|
-
var sidx = Math.floor(spos * (spts.length - 1));
|
|
829
|
-
var spt = spts[sidx];
|
|
830
|
-
var sgrad = ctx.createRadialGradient(spt.x, spt.y, 0, spt.x, spt.y, s * 0.10);
|
|
831
|
-
sgrad.addColorStop(0, TOOL.color + 'aa'); sgrad.addColorStop(1, 'transparent');
|
|
832
|
-
ctx.fillStyle = sgrad; ctx.fillRect(0, 0, _c.width, s);
|
|
833
|
-
var strailFrac = 0.18;
|
|
834
|
-
var si0 = Math.max(0, Math.floor((spos - strailFrac) * (spts.length - 1)));
|
|
835
|
-
ctx.strokeStyle = TOOL.color; ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
836
|
-
ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.moveTo(spts[si0].x, spts[si0].y);
|
|
837
|
-
for(var si = si0 + 1; si <= sidx; si++) ctx.lineTo(spts[si].x, spts[si].y);
|
|
838
|
-
ctx.stroke(); ctx.globalAlpha = 1;
|
|
839
|
-
_drawName(ctx, s, 1, undefined);
|
|
840
|
-
break;
|
|
841
|
-
case 'orbit':
|
|
842
|
-
_bracket(ctx, s, TOOL.color, 0.15);
|
|
843
|
-
_drawName(ctx, s, 1, 0.4);
|
|
844
|
-
var pts = _getOpenPts(s), speed = 1200, trailFrac = 0.28;
|
|
845
|
-
var halfCycle = (e % speed) / speed;
|
|
846
|
-
var cycle = (e % (speed * 2)) / (speed * 2);
|
|
847
|
-
var pos = cycle < 0.5 ? halfCycle : 1 - halfCycle;
|
|
848
|
-
var headIdx = Math.floor(pos * (pts.length - 1));
|
|
849
|
-
var trailLen = Math.floor(trailFrac * pts.length);
|
|
850
|
-
var dir = cycle < 0.5 ? 1 : -1;
|
|
851
|
-
ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
852
|
-
for(var i = 0; i < trailLen; i++) {
|
|
853
|
-
var idx = headIdx - dir * (trailLen - i);
|
|
854
|
-
if(idx < 0 || idx >= pts.length) continue;
|
|
855
|
-
var nxt = idx + dir;
|
|
856
|
-
if(nxt < 0 || nxt >= pts.length) continue;
|
|
857
|
-
ctx.globalAlpha = (i / trailLen) * 0.7; ctx.strokeStyle = TOOL.color;
|
|
858
|
-
ctx.beginPath(); ctx.moveTo(pts[idx].x, pts[idx].y); ctx.lineTo(pts[nxt].x, pts[nxt].y); ctx.stroke();
|
|
859
|
-
}
|
|
860
|
-
ctx.globalAlpha = 1;
|
|
861
|
-
break;
|
|
862
|
-
case 'dim':
|
|
863
|
-
var dim = 0.1 + 0.08 * Math.sin(e / 2000);
|
|
864
|
-
_bracket(ctx, s, TOOL.color, dim);
|
|
865
|
-
_drawName(ctx, s, 1, 0.2);
|
|
866
|
-
break;
|
|
867
|
-
}
|
|
868
|
-
_raf = requestAnimationFrame(_frame);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
_c = document.getElementById('grainLogo');
|
|
872
|
-
if(_c) {
|
|
873
|
-
_c.style.width = '0px';
|
|
874
|
-
_s = 256;
|
|
875
|
-
var targetFontPx = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
876
|
-
var fontRatio = 0.38;
|
|
877
|
-
var dh = 64;
|
|
878
|
-
_c.height = _s; _c.width = 1024;
|
|
879
|
-
_ctx = _c.getContext('2d');
|
|
880
|
-
_cx = _s / 2;
|
|
881
|
-
_restText = TOOL.name.slice(1);
|
|
882
|
-
_font = '800 ' + (_s * fontRatio) + 'px -apple-system,"SF Pro Display","Helvetica Neue",Arial,sans-serif';
|
|
883
|
-
_ctx.font = _font;
|
|
884
|
-
var letterW = _ctx.measureText(TOOL.letter).width;
|
|
885
|
-
var restW = _restText.length > 0 ? _ctx.measureText(_restText).width : 0;
|
|
886
|
-
_textStart = _cx + letterW / 2 + _s * 0.02;
|
|
887
|
-
var totalW = Math.ceil(_textStart + restW + _s * 0.12);
|
|
888
|
-
_c.width = totalW;
|
|
889
|
-
_ctx = _c.getContext('2d');
|
|
890
|
-
_c.style.height = dh + 'px';
|
|
891
|
-
_c.style.width = Math.round(totalW / _s * dh) + 'px';
|
|
892
|
-
_state = 'drawon'; _start = null;
|
|
893
|
-
_raf = requestAnimationFrame(_frame);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
window._grainSetState = function(state) {
|
|
897
|
-
if(_state === state) return;
|
|
898
|
-
if(_state === 'drawon') { _pendingState = state; return; }
|
|
899
|
-
_state = state; _start = null;
|
|
900
|
-
if(!_raf) _raf = requestAnimationFrame(_frame);
|
|
901
|
-
};
|
|
902
|
-
})();
|
|
903
|
-
</script>
|
|
904
|
-
</body>
|
|
905
|
-
</html>
|