@cleocode/cleo 2026.5.86 → 2026.5.88
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/assets/viewer/index.html +2 -0
- package/assets/viewer/styles.css +64 -0
- package/assets/viewer/viewer.js +97 -0
- package/dist/cli/index.js +240 -156
- package/dist/cli/index.js.map +3 -3
- package/package.json +11 -11
package/assets/viewer/index.html
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
<header class="topbar">
|
|
11
11
|
<h1>CLEO Docs</h1>
|
|
12
12
|
<input id="filter" type="search" placeholder="Filter by slug, title, type..." autocomplete="off" />
|
|
13
|
+
<input id="search" type="search" placeholder="Search content (ranked)..." autocomplete="off" aria-label="Search docs by content" />
|
|
13
14
|
</header>
|
|
14
15
|
<main class="layout">
|
|
15
16
|
<aside id="sidebar">
|
|
17
|
+
<div id="search-results" aria-live="polite" hidden></div>
|
|
16
18
|
<div id="doc-list" aria-live="polite">Loading docs...</div>
|
|
17
19
|
</aside>
|
|
18
20
|
<section id="content">
|
package/assets/viewer/styles.css
CHANGED
|
@@ -130,3 +130,67 @@ html, body {
|
|
|
130
130
|
|
|
131
131
|
.hint { color: var(--text-muted); }
|
|
132
132
|
.error { color: #ff7f7f; }
|
|
133
|
+
|
|
134
|
+
/* T9647 — search results UI */
|
|
135
|
+
#search {
|
|
136
|
+
flex: 1;
|
|
137
|
+
max-width: 360px;
|
|
138
|
+
background: var(--surface-2);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
color: var(--text);
|
|
141
|
+
padding: 0.45rem 0.75rem;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
font: inherit;
|
|
144
|
+
}
|
|
145
|
+
#search:focus { outline: 2px solid var(--accent); outline-offset: 0; }
|
|
146
|
+
|
|
147
|
+
#search-results {
|
|
148
|
+
padding: 0.25rem 0;
|
|
149
|
+
border-bottom: 1px solid var(--border);
|
|
150
|
+
}
|
|
151
|
+
#search-results .result {
|
|
152
|
+
display: block;
|
|
153
|
+
padding: 0.55rem 1rem;
|
|
154
|
+
color: var(--text);
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
border-left: 3px solid transparent;
|
|
157
|
+
}
|
|
158
|
+
#search-results .result:hover {
|
|
159
|
+
background: var(--surface-2);
|
|
160
|
+
border-left-color: var(--accent);
|
|
161
|
+
}
|
|
162
|
+
#search-results .result-head {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: baseline;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
font-size: 0.9rem;
|
|
167
|
+
}
|
|
168
|
+
#search-results .result-name {
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
color: var(--accent);
|
|
171
|
+
flex: 1;
|
|
172
|
+
word-break: break-word;
|
|
173
|
+
}
|
|
174
|
+
#search-results .result-type {
|
|
175
|
+
font-size: 0.7rem;
|
|
176
|
+
color: var(--text-muted);
|
|
177
|
+
text-transform: uppercase;
|
|
178
|
+
letter-spacing: 0.03em;
|
|
179
|
+
}
|
|
180
|
+
#search-results .result-score {
|
|
181
|
+
font-size: 0.75rem;
|
|
182
|
+
color: var(--text-muted);
|
|
183
|
+
font-variant-numeric: tabular-nums;
|
|
184
|
+
}
|
|
185
|
+
#search-results .result-snippet {
|
|
186
|
+
color: var(--text-muted);
|
|
187
|
+
font-size: 0.8rem;
|
|
188
|
+
line-height: 1.4;
|
|
189
|
+
margin-top: 0.2rem;
|
|
190
|
+
}
|
|
191
|
+
#search-results mark {
|
|
192
|
+
background: rgba(106, 167, 255, 0.25);
|
|
193
|
+
color: inherit;
|
|
194
|
+
padding: 0 2px;
|
|
195
|
+
border-radius: 2px;
|
|
196
|
+
}
|
package/assets/viewer/viewer.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
const $list = document.getElementById('doc-list');
|
|
7
7
|
const $render = document.getElementById('doc-render');
|
|
8
8
|
const $filter = document.getElementById('filter');
|
|
9
|
+
const $search = document.getElementById('search');
|
|
10
|
+
const $searchResults = document.getElementById('search-results');
|
|
9
11
|
|
|
10
12
|
const state = {
|
|
11
13
|
docs: [],
|
|
12
14
|
activeSlug: null,
|
|
15
|
+
searchSeq: 0,
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
function escapeHtml(str) {
|
|
@@ -287,6 +290,100 @@ function slugFromPath(pathname) {
|
|
|
287
290
|
|
|
288
291
|
$filter.addEventListener('input', (e) => applyFilter(e.target.value));
|
|
289
292
|
|
|
293
|
+
// ── Search (T9647) — debounced /api/search with ranked results ──────────────
|
|
294
|
+
|
|
295
|
+
function debounce(fn, ms) {
|
|
296
|
+
let timer = null;
|
|
297
|
+
return function debounced(...args) {
|
|
298
|
+
if (timer !== null) clearTimeout(timer);
|
|
299
|
+
timer = setTimeout(() => {
|
|
300
|
+
timer = null;
|
|
301
|
+
fn.apply(this, args);
|
|
302
|
+
}, ms);
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function highlightMatches(snippet, query) {
|
|
307
|
+
if (!query) return escapeHtml(snippet);
|
|
308
|
+
const terms = query
|
|
309
|
+
.toLowerCase()
|
|
310
|
+
.split(/\s+/)
|
|
311
|
+
.filter((t) => t.length >= 2);
|
|
312
|
+
if (terms.length === 0) return escapeHtml(snippet);
|
|
313
|
+
// Build a regex that matches any term, case-insensitive. Escape regex metachars.
|
|
314
|
+
const pattern = terms
|
|
315
|
+
.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
316
|
+
.join('|');
|
|
317
|
+
const re = new RegExp(`(${pattern})`, 'gi');
|
|
318
|
+
const escaped = escapeHtml(snippet);
|
|
319
|
+
return escaped.replace(re, '<mark>$1</mark>');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderSearchResults(query, payload) {
|
|
323
|
+
if (!payload || !payload.hits || payload.hits.length === 0) {
|
|
324
|
+
$searchResults.innerHTML =
|
|
325
|
+
`<p class="hint">No results for <code>${escapeHtml(query)}</code>.</p>`;
|
|
326
|
+
$searchResults.hidden = false;
|
|
327
|
+
$list.hidden = true;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const html = payload.hits
|
|
331
|
+
.map((h) => {
|
|
332
|
+
const slug = escapeHtml(h.slug || h.id);
|
|
333
|
+
const name = escapeHtml(h.name || h.slug || h.id);
|
|
334
|
+
const type = h.type ? `<span class="result-type">${escapeHtml(h.type)}</span>` : '';
|
|
335
|
+
const score = typeof h.score === 'number' ? h.score.toFixed(3) : '—';
|
|
336
|
+
const snippet = highlightMatches(h.snippet || '', query);
|
|
337
|
+
return [
|
|
338
|
+
`<a href="/docs/${slug}" data-slug="${slug}" class="result">`,
|
|
339
|
+
`<div class="result-head"><span class="result-name">${name}</span>${type}<span class="result-score">${score}</span></div>`,
|
|
340
|
+
`<div class="result-snippet">${snippet}</div>`,
|
|
341
|
+
`</a>`,
|
|
342
|
+
].join('');
|
|
343
|
+
})
|
|
344
|
+
.join('');
|
|
345
|
+
$searchResults.innerHTML = html;
|
|
346
|
+
$searchResults.hidden = false;
|
|
347
|
+
$list.hidden = true;
|
|
348
|
+
$searchResults.querySelectorAll('a.result').forEach((a) => {
|
|
349
|
+
a.addEventListener('click', (e) => {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
const slug = a.getAttribute('data-slug');
|
|
352
|
+
navigate(slug);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function clearSearchResults() {
|
|
358
|
+
$searchResults.hidden = true;
|
|
359
|
+
$searchResults.innerHTML = '';
|
|
360
|
+
$list.hidden = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runSearch(query) {
|
|
364
|
+
const q = query.trim();
|
|
365
|
+
if (q.length === 0) {
|
|
366
|
+
clearSearchResults();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const seq = ++state.searchSeq;
|
|
370
|
+
try {
|
|
371
|
+
const url = `/api/search?q=${encodeURIComponent(q)}&limit=20`;
|
|
372
|
+
const data = await fetchJson(url);
|
|
373
|
+
// Drop stale responses if a newer query has fired.
|
|
374
|
+
if (seq !== state.searchSeq) return;
|
|
375
|
+
renderSearchResults(q, data);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (seq !== state.searchSeq) return;
|
|
378
|
+
$searchResults.innerHTML = `<p class="error">${escapeHtml(String(err.message || err))}</p>`;
|
|
379
|
+
$searchResults.hidden = false;
|
|
380
|
+
$list.hidden = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const debouncedSearch = debounce(runSearch, 300);
|
|
385
|
+
$search.addEventListener('input', (e) => debouncedSearch(e.target.value));
|
|
386
|
+
|
|
290
387
|
(async function init() {
|
|
291
388
|
try {
|
|
292
389
|
const data = await fetchJson('/api/docs');
|