compend 0.0.0 → 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 +34 -0
- package/README.md +377 -2
- package/config.js +147 -0
- package/dashboard/api-handler.js +77 -0
- package/dashboard/public/app.js +338 -0
- package/dashboard/public/index.html +66 -0
- package/dashboard/public/logo.svg +1 -0
- package/dashboard/public/style.css +497 -0
- package/dashboard.js +203 -0
- package/db.js +569 -0
- package/embedding.js +81 -0
- package/index.js +179 -0
- package/logo.svg +1 -1
- package/package.json +20 -4
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
let state = { type: '', status: '', search: '', tags: [], limit: 20, offset: 0, total: 0 };
|
|
2
|
+
let loadVersion = 0;
|
|
3
|
+
let expandedSlug = null;
|
|
4
|
+
|
|
5
|
+
function esc(s) {
|
|
6
|
+
const d = document.createElement('div');
|
|
7
|
+
d.textContent = String(s);
|
|
8
|
+
return d.innerHTML;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function timeAgo(ts) {
|
|
12
|
+
const sec = Math.floor(Date.now() / 1000 - ts);
|
|
13
|
+
if (sec < 60) return 'just now';
|
|
14
|
+
if (sec < 3600) return Math.floor(sec / 60) + 'm ago';
|
|
15
|
+
if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
|
|
16
|
+
return Math.floor(sec / 86400) + 'd ago';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function typeClass(t) {
|
|
20
|
+
t = (t || '').toLowerCase();
|
|
21
|
+
if (t.startsWith('skill')) return 'skill';
|
|
22
|
+
if (t.startsWith('agent')) return 'agent';
|
|
23
|
+
if (t.startsWith('instr')) return 'instruction';
|
|
24
|
+
if (t.startsWith('prompt')) return 'prompt';
|
|
25
|
+
if (t.startsWith('workflow')) return 'workflow';
|
|
26
|
+
if (t.startsWith('ref')) return 'reference';
|
|
27
|
+
return 'default';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ─────────── TOASTS ─────────── */
|
|
31
|
+
|
|
32
|
+
function toast(msg, type, duration) {
|
|
33
|
+
type = type || 'info';
|
|
34
|
+
duration = duration || 4000;
|
|
35
|
+
const container = document.getElementById('toast-container');
|
|
36
|
+
const el = document.createElement('div');
|
|
37
|
+
el.className = 'toast toast-' + type;
|
|
38
|
+
el.textContent = msg;
|
|
39
|
+
container.appendChild(el);
|
|
40
|
+
const timer = setTimeout(() => removeToast(el), duration);
|
|
41
|
+
el._timer = timer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeToast(el) {
|
|
45
|
+
if (el._removing) return;
|
|
46
|
+
el._removing = true;
|
|
47
|
+
clearTimeout(el._timer);
|
|
48
|
+
el.classList.add('removing');
|
|
49
|
+
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 250);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ─────────── SKELETON ─────────── */
|
|
53
|
+
|
|
54
|
+
function showSkeleton() {
|
|
55
|
+
const tbody = document.getElementById('tbody');
|
|
56
|
+
const skeleton = document.getElementById('loading-skeleton');
|
|
57
|
+
if (skeleton) skeleton.style.display = '';
|
|
58
|
+
if (tbody) tbody.innerHTML = '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hideSkeleton() {
|
|
62
|
+
const skeleton = document.getElementById('loading-skeleton');
|
|
63
|
+
if (skeleton) skeleton.style.display = 'none';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function initSkeleton() {
|
|
67
|
+
const tbody = document.getElementById('skeleton-body');
|
|
68
|
+
if (!tbody) return;
|
|
69
|
+
const rows = [];
|
|
70
|
+
for (let i = 0; i < 8; i++) {
|
|
71
|
+
rows.push('<tr class="skeleton-row"><td class="skeleton-cell tiny"></td><td class="skeleton-cell narrow"></td><td class="skeleton-cell wide"></td><td class="skeleton-cell narrow"></td><td class="skeleton-cell tiny"></td></tr>');
|
|
72
|
+
}
|
|
73
|
+
tbody.innerHTML = rows.join('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ─────────── DETAIL TOGGLE ─────────── */
|
|
77
|
+
|
|
78
|
+
function toggleDetail(slug) {
|
|
79
|
+
const detail = document.querySelector('.detail-row[data-slug="' + CSS.escape(slug) + '"]');
|
|
80
|
+
if (!detail) return;
|
|
81
|
+
const open = detail.style.display !== 'none';
|
|
82
|
+
if (open) {
|
|
83
|
+
detail.style.display = 'none';
|
|
84
|
+
detail.querySelector('.detail').classList.remove('open');
|
|
85
|
+
expandedSlug = null;
|
|
86
|
+
} else {
|
|
87
|
+
if (expandedSlug) {
|
|
88
|
+
const prev = document.querySelector('.detail-row[data-slug="' + CSS.escape(expandedSlug) + '"]');
|
|
89
|
+
if (prev) {
|
|
90
|
+
prev.style.display = 'none';
|
|
91
|
+
prev.querySelector('.detail').classList.remove('open');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
detail.style.display = '';
|
|
95
|
+
detail.querySelector('.detail').classList.add('open');
|
|
96
|
+
expandedSlug = slug;
|
|
97
|
+
loadConceptBody(slug, detail.querySelector('.detail'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function loadConceptBody(slug, detailEl) {
|
|
102
|
+
try {
|
|
103
|
+
const r = await fetch('/api/concepts/' + encodeURIComponent(slug));
|
|
104
|
+
if (!r.ok) throw new Error('Failed to load');
|
|
105
|
+
const concept = await r.json();
|
|
106
|
+
const fm = concept.frontmatter ? JSON.stringify(concept.frontmatter, null, 2) : '{}';
|
|
107
|
+
const refs = (concept.references || []).length
|
|
108
|
+
? '\n\nReferences:\n' + concept.references.map(ref => ' • ' + esc(ref.slug) + ' (' + esc(ref.type) + ')').join('\n')
|
|
109
|
+
: '';
|
|
110
|
+
const deps = (concept.dependencies || []).length
|
|
111
|
+
? '\n\nDependencies:\n' + concept.dependencies.map(d => ' • ' + esc(d.slug) + (d.title ? ' — ' + esc(d.title) : '')).join('\n')
|
|
112
|
+
: '';
|
|
113
|
+
detailEl.innerHTML = '<pre>' + esc(concept.body) + '\n\n─── Frontmatter ───\n' + esc(fm) + refs + deps + '</pre>';
|
|
114
|
+
} catch (e) {
|
|
115
|
+
detailEl.innerHTML = '<pre>Error loading concept</pre>';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ─────────── API ─────────── */
|
|
120
|
+
|
|
121
|
+
function showError(msg) {
|
|
122
|
+
const el = document.getElementById('error');
|
|
123
|
+
if (el) {
|
|
124
|
+
el.textContent = msg;
|
|
125
|
+
el.style.display = 'block';
|
|
126
|
+
}
|
|
127
|
+
console.error(msg);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function loadStatsAndTypes() {
|
|
131
|
+
const r = await fetch('/api/stats');
|
|
132
|
+
if (!r.ok) throw new Error('Failed to load stats');
|
|
133
|
+
const data = await r.json();
|
|
134
|
+
const stats = document.getElementById('stats');
|
|
135
|
+
stats.textContent = data.total + ' concept' + (data.total !== 1 ? 's' : '') + ' · '
|
|
136
|
+
+ data.types.map(t => t.type + ': ' + t.count).join(' · ');
|
|
137
|
+
|
|
138
|
+
const sel = document.getElementById('type');
|
|
139
|
+
const current = sel.value;
|
|
140
|
+
sel.innerHTML = '<option value="">all</option>' + data.types.map(t =>
|
|
141
|
+
'<option value="' + esc(t.type) + '">' + esc(t.type) + ' (' + t.count + ')</option>'
|
|
142
|
+
).join('');
|
|
143
|
+
if (data.types.some(t => t.type === current)) sel.value = current;
|
|
144
|
+
else sel.value = '';
|
|
145
|
+
state.type = sel.value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadTags() {
|
|
149
|
+
const r = await fetch('/api/tags' + (state.type ? '?type=' + encodeURIComponent(state.type) : ''));
|
|
150
|
+
if (!r.ok) return;
|
|
151
|
+
const data = await r.json();
|
|
152
|
+
const bar = document.getElementById('tags-filter');
|
|
153
|
+
if (data.tags.length === 0) {
|
|
154
|
+
bar.innerHTML = '';
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
bar.innerHTML = data.tags.map(t =>
|
|
158
|
+
'<label class="tag-check"><input type="checkbox" value="' + esc(t.name) + '"'
|
|
159
|
+
+ (state.tags.includes(t.name) ? ' checked' : '')
|
|
160
|
+
+ '> ' + esc(t.name) + ' <span class="tag-count">' + t.count + '</span></label>'
|
|
161
|
+
).join('');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function loadConcepts() {
|
|
165
|
+
const version = ++loadVersion;
|
|
166
|
+
showSkeleton();
|
|
167
|
+
|
|
168
|
+
const params = new URLSearchParams();
|
|
169
|
+
if (state.type) params.set('type', state.type);
|
|
170
|
+
if (state.status) params.set('status', state.status);
|
|
171
|
+
params.set('limit', state.limit);
|
|
172
|
+
params.set('offset', state.offset);
|
|
173
|
+
if (state.search) params.set('search', state.search);
|
|
174
|
+
if (state.tags.length) params.set('tags', state.tags.join(','));
|
|
175
|
+
|
|
176
|
+
const r = await fetch('/api/concepts?' + params.toString());
|
|
177
|
+
if (!r.ok) throw new Error('Failed to load concepts');
|
|
178
|
+
const data = await r.json();
|
|
179
|
+
if (version !== loadVersion) return;
|
|
180
|
+
|
|
181
|
+
hideSkeleton();
|
|
182
|
+
document.getElementById('error').style.display = 'none';
|
|
183
|
+
|
|
184
|
+
const concepts = data.concepts || [];
|
|
185
|
+
state.total = data.total;
|
|
186
|
+
|
|
187
|
+
const tbody = document.getElementById('tbody');
|
|
188
|
+
const pagination = document.getElementById('pagination');
|
|
189
|
+
|
|
190
|
+
if (concepts.length === 0) {
|
|
191
|
+
const msg = state.search ? 'No results for "' + esc(state.search) + '"' : 'No concepts indexed';
|
|
192
|
+
tbody.innerHTML = '<tr><td colspan="5"><div class="empty">' + msg + '</div></td></tr>';
|
|
193
|
+
pagination.innerHTML = '';
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let savedScroll = 0;
|
|
198
|
+
if (expandedSlug) {
|
|
199
|
+
const el = document.querySelector('.detail-row[data-slug="' + CSS.escape(expandedSlug) + '"] .detail');
|
|
200
|
+
if (el) savedScroll = el.scrollTop;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
tbody.innerHTML = concepts.map(function (c) {
|
|
204
|
+
const tc = typeClass(c.type);
|
|
205
|
+
const tagPills = (c.tags || []).map(function (t) { return '<span class="tag-pill">' + esc(t) + '</span>'; }).join(' ');
|
|
206
|
+
return '<tr class="concept-row" data-slug="' + esc(c.slug) + '" tabindex="0" role="button" aria-label="Concept: ' + esc(c.title || c.slug) + '">'
|
|
207
|
+
+ '<td><span class="type-badge type-' + tc + '">' + esc(c.type) + '</span></td>'
|
|
208
|
+
+ '<td class="title-cell">' + esc(c.title || c.slug) + '</td>'
|
|
209
|
+
+ '<td class="desc-cell">' + esc(c.description || '') + '</td>'
|
|
210
|
+
+ '<td class="tags-cell">' + (tagPills || '<span class="related-none">—</span>') + '</td>'
|
|
211
|
+
+ '<td class="status-cell">' + (c.status ? '<span class="status-badge status-' + c.status + '">' + esc(c.status) + '</span>' : '<span class="related-none">—</span>') + '</td></tr>'
|
|
212
|
+
+ '<tr class="detail-row" data-slug="' + esc(c.slug) + '" style="display:none"><td colspan="5"><div class="detail"><pre>Loading...</pre></div></td></tr>';
|
|
213
|
+
}).join('');
|
|
214
|
+
|
|
215
|
+
if (expandedSlug) {
|
|
216
|
+
const detail = tbody.querySelector('.detail-row[data-slug="' + CSS.escape(expandedSlug) + '"]');
|
|
217
|
+
if (detail) {
|
|
218
|
+
detail.style.display = '';
|
|
219
|
+
detail.querySelector('.detail').classList.add('open');
|
|
220
|
+
if (savedScroll > 0) {
|
|
221
|
+
const el = detail.querySelector('.detail');
|
|
222
|
+
if (el) el.scrollTop = savedScroll;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const tp = Math.ceil(data.total / state.limit);
|
|
228
|
+
const cp = Math.floor(state.offset / state.limit) + 1;
|
|
229
|
+
pagination.innerHTML = '<button id="page-prev"' + (cp <= 1 ? ' disabled' : '') + ' aria-label="Previous page">← Prev</button>'
|
|
230
|
+
+ '<span>Page ' + cp + ' of ' + tp + '</span>'
|
|
231
|
+
+ '<button id="page-next"' + (cp >= tp ? ' disabled' : '') + ' aria-label="Next page">Next →</button>';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ─────────── EVENT WIRING ─────────── */
|
|
235
|
+
|
|
236
|
+
document.getElementById('type').addEventListener('change', function () {
|
|
237
|
+
state.type = this.value;
|
|
238
|
+
state.offset = 0;
|
|
239
|
+
state.tags = [];
|
|
240
|
+
loadTags().catch(showError);
|
|
241
|
+
loadConcepts().catch(showError);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
document.getElementById('status').addEventListener('change', function () {
|
|
245
|
+
state.status = this.value;
|
|
246
|
+
state.offset = 0;
|
|
247
|
+
loadConcepts().catch(showError);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
let searchTimer = null;
|
|
251
|
+
document.getElementById('search').addEventListener('input', function () {
|
|
252
|
+
clearTimeout(searchTimer);
|
|
253
|
+
searchTimer = setTimeout(function () {
|
|
254
|
+
state.search = this.value.trim();
|
|
255
|
+
state.offset = 0;
|
|
256
|
+
loadConcepts().catch(showError);
|
|
257
|
+
}.bind(this), 200);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
document.getElementById('tags-filter').addEventListener('change', function (e) {
|
|
261
|
+
if (e.target.type === 'checkbox') {
|
|
262
|
+
if (e.target.checked) {
|
|
263
|
+
state.tags.push(e.target.value);
|
|
264
|
+
} else {
|
|
265
|
+
state.tags = state.tags.filter(t => t !== e.target.value);
|
|
266
|
+
}
|
|
267
|
+
state.offset = 0;
|
|
268
|
+
loadConcepts().catch(showError);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
document.getElementById('tbody').addEventListener('click', function (e) {
|
|
273
|
+
const row = e.target.closest('.concept-row');
|
|
274
|
+
if (row) {
|
|
275
|
+
toggleDetail(row.dataset.slug);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
document.getElementById('tbody').addEventListener('keydown', function (e) {
|
|
280
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
281
|
+
const row = e.target.closest('.concept-row');
|
|
282
|
+
if (row) {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
toggleDetail(row.dataset.slug);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
document.getElementById('pagination').addEventListener('click', function (e) {
|
|
290
|
+
const btn = e.target.closest('button');
|
|
291
|
+
if (!btn) return;
|
|
292
|
+
if (btn.id === 'page-prev' && state.offset > 0) {
|
|
293
|
+
state.offset = Math.max(0, state.offset - state.limit);
|
|
294
|
+
loadConcepts().catch(showError);
|
|
295
|
+
} else if (btn.id === 'page-next') {
|
|
296
|
+
state.offset += state.limit;
|
|
297
|
+
loadConcepts().catch(showError);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
document.getElementById('theme-btn').addEventListener('click', function () {
|
|
302
|
+
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
303
|
+
document.documentElement.setAttribute('data-theme', isLight ? '' : 'light');
|
|
304
|
+
localStorage.setItem('compend-theme', isLight ? 'dark' : 'light');
|
|
305
|
+
this.setAttribute('aria-label', isLight ? 'Switch to dark theme' : 'Switch to light theme');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/* ===== SSE: Real-Time Updates ===== */
|
|
309
|
+
const es = new EventSource('/api/events');
|
|
310
|
+
|
|
311
|
+
es.onerror = function () {};
|
|
312
|
+
|
|
313
|
+
es.addEventListener('index_complete', function (e) {
|
|
314
|
+
try {
|
|
315
|
+
const data = JSON.parse(e.data);
|
|
316
|
+
toast('Index complete: +' + (data.added || 0) + ' ~' + (data.updated || 0) + ' -' + (data.removed || 0), 'success');
|
|
317
|
+
loadConcepts().catch(showError);
|
|
318
|
+
loadStatsAndTypes().catch(showError);
|
|
319
|
+
} catch (_) {}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
setInterval(function () {
|
|
323
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
324
|
+
loadConcepts().catch(function () {});
|
|
325
|
+
}
|
|
326
|
+
}, 30000);
|
|
327
|
+
|
|
328
|
+
/* Init */
|
|
329
|
+
(function () {
|
|
330
|
+
initSkeleton();
|
|
331
|
+
if (document.documentElement.getAttribute('data-theme') === 'light') {
|
|
332
|
+
document.getElementById('theme-btn').setAttribute('aria-label', 'Switch to dark theme');
|
|
333
|
+
}
|
|
334
|
+
loadStatsAndTypes()
|
|
335
|
+
.then(function () { return loadTags(); })
|
|
336
|
+
.then(function () { return loadConcepts(); })
|
|
337
|
+
.catch(showError);
|
|
338
|
+
})();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Compend</title>
|
|
7
|
+
<meta name="description" content="Compend knowledge reference dashboard">
|
|
8
|
+
<link rel="stylesheet" href="/style.css">
|
|
9
|
+
<link rel="icon" href="/logo.svg" type="image/svg+xml">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app" role="application" aria-label="Compend knowledge reference dashboard">
|
|
13
|
+
<header role="banner">
|
|
14
|
+
<div class="logo-title">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 -960 960 960" width="28" fill="currentColor" aria-hidden="true"><path d="M440-278v-394q-41-24-87-36t-93-12q-36 0-71.5 7T120-692v396q35-12 69.5-18t70.5-6q47 0 91.5 10.5T440-278Zm40 118q-48-38-104-59t-116-21q-42 0-82.5 11T100-198q-21 11-40.5-1T40-234v-482q0-11 5.5-21T62-752q46-24 96-36t102-12q74 0 126 17t112 52q11 6 16.5 14t5.5 21v418q44-21 88.5-31.5T700-320q36 0 70.5 6t69.5 18v-481q15 5 29.5 11t28.5 14q11 5 16.5 15t5.5 21v482q0 23-19.5 35t-40.5 1q-37-20-77.5-31T700-240q-60 0-116 21t-104 59Zm140-240v-440l120-40v440l-120 40Zm-340-99Z"/></svg>
|
|
16
|
+
<h1>Compend</h1>
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<button id="theme-btn" class="theme-btn" aria-label="Toggle theme"><svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="currentColor" aria-hidden="true"><path d="M337.5-463Q311-498 289-537q-5 14-6.5 28.5T281-480q0 83 58 141t141 58q14 0 28.5-2t28.5-6q-39-22-74-48.5T396-396q-32-32-58.5-67ZM567-364.5Q630-328 702-308q-40 51-98 79.5T481-200q-117 0-198.5-81.5T201-480q0-65 28.5-123t79.5-98q20 72 56.5 135T453-452q51 51 114 87.5ZM743-380q-20-5-39.5-11T665-405q8-18 11.5-36.5T680-480q0-83-58.5-141.5T480-680q-20 0-38.5 3.5T405-665q-8-19-13.5-38T381-742q24-9 49-13.5t51-4.5q117 0 198.5 81.5T761-480q0 26-4.5 51T743-380ZM440-840v-120h80v120h-80Zm0 840v-120h80V0h-80Zm323-706-57-57 85-84 57 56-85 85ZM169-113l-57-56 85-85 57 57-85 84Zm671-327v-80h120v80H840ZM0-440v-80h120v80H0Zm791 328-85-85 57-57 84 85-56 57ZM197-706l-84-85 56-57 85 85-57 57Zm199 310Z"></path></svg></button>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<div class="filters" role="search" aria-label="Filter concepts">
|
|
24
|
+
<div class="filter-row">
|
|
25
|
+
<div class="field">
|
|
26
|
+
<label for="type">Type</label>
|
|
27
|
+
<select id="type" aria-label="Filter by type"><option value="">all</option></select>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="field">
|
|
30
|
+
<label for="status">Status</label>
|
|
31
|
+
<select id="status" aria-label="Filter by status"><option value="">any</option><option value="stable">stable</option><option value="draft">draft</option><option value="deprecated">deprecated</option></select>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="field search-field">
|
|
34
|
+
<label for="search">Search</label>
|
|
35
|
+
<input id="search" placeholder="Search concepts..." autocomplete="off" aria-label="Search concepts" role="searchbox">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="tags-filter" class="tags-bar" aria-label="Filter by tags"></div>
|
|
39
|
+
</div>
|
|
40
|
+
<div id="error" role="alert" aria-live="assertive"></div>
|
|
41
|
+
<div id="stats" role="status" aria-live="polite">Loading...</div>
|
|
42
|
+
|
|
43
|
+
<div id="loading-skeleton" aria-hidden="true" style="display:none">
|
|
44
|
+
<table aria-hidden="true">
|
|
45
|
+
<thead><tr><th>Type</th><th>Title</th><th>Description</th><th>Tags</th><th>Status</th></tr></thead>
|
|
46
|
+
<tbody id="skeleton-body"></tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div id="table-wrap">
|
|
51
|
+
<table aria-label="Concepts list">
|
|
52
|
+
<thead>
|
|
53
|
+
<tr><th scope="col">Type</th><th scope="col">Title</th><th scope="col">Description</th><th scope="col">Tags</th><th scope="col">Status</th></tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody id="tbody" aria-live="polite" aria-relevant="additions removals"></tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
<div id="pagination" role="navigation" aria-label="Pagination"></div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div id="toast-container" role="status" aria-live="polite" aria-atomic="true"></div>
|
|
62
|
+
|
|
63
|
+
<script>if(localStorage.getItem('compend-theme')==='light'){document.documentElement.setAttribute('data-theme','light')}</script>
|
|
64
|
+
<script src="/app.js"></script>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="currentColor"><path d="M440-278v-394q-41-24-87-36t-93-12q-36 0-71.5 7T120-692v396q35-12 69.5-18t70.5-6q47 0 91.5 10.5T440-278Zm40 118q-48-38-104-59t-116-21q-42 0-82.5 11T100-198q-21 11-40.5-1T40-234v-482q0-11 5.5-21T62-752q46-24 96-36t102-12q74 0 126 17t112 52q11 6 16.5 14t5.5 21v418q44-21 88.5-31.5T700-320q36 0 70.5 6t69.5 18v-481q15 5 29.5 11t28.5 14q11 5 16.5 15t5.5 21v482q0 23-19.5 35t-40.5 1q-37-20-77.5-31T700-240q-60 0-116 21t-104 59Zm140-240v-440l120-40v440l-120 40Zm-340-99Z"/></svg>
|