@cleocode/cleo 2026.5.83 → 2026.5.86

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.
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>CLEO Docs Viewer</title>
7
+ <link rel="stylesheet" href="/viewer/styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="topbar">
11
+ <h1>CLEO Docs</h1>
12
+ <input id="filter" type="search" placeholder="Filter by slug, title, type..." autocomplete="off" />
13
+ </header>
14
+ <main class="layout">
15
+ <aside id="sidebar">
16
+ <div id="doc-list" aria-live="polite">Loading docs...</div>
17
+ </aside>
18
+ <section id="content">
19
+ <article id="doc-render">
20
+ <p class="hint">Select a document from the sidebar, or open a direct link like <code>/docs/&lt;slug&gt;</code>.</p>
21
+ </article>
22
+ </section>
23
+ </main>
24
+ <script src="/viewer/viewer.js" type="module"></script>
25
+ </body>
26
+ </html>
@@ -0,0 +1,132 @@
1
+ :root {
2
+ --bg: #0f1115;
3
+ --surface: #161a22;
4
+ --surface-2: #1f2632;
5
+ --border: #2a3140;
6
+ --text: #d6dbe3;
7
+ --text-muted: #8a93a3;
8
+ --accent: #6aa7ff;
9
+ --accent-hover: #88baff;
10
+ --code-bg: #0b0d12;
11
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
12
+ }
13
+
14
+ * { box-sizing: border-box; }
15
+
16
+ html, body {
17
+ margin: 0;
18
+ padding: 0;
19
+ background: var(--bg);
20
+ color: var(--text);
21
+ height: 100%;
22
+ }
23
+
24
+ .topbar {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 1rem;
28
+ padding: 0.75rem 1.25rem;
29
+ background: var(--surface);
30
+ border-bottom: 1px solid var(--border);
31
+ position: sticky;
32
+ top: 0;
33
+ z-index: 10;
34
+ }
35
+
36
+ .topbar h1 {
37
+ margin: 0;
38
+ font-size: 1rem;
39
+ letter-spacing: 0.04em;
40
+ color: var(--accent);
41
+ }
42
+
43
+ #filter {
44
+ flex: 1;
45
+ max-width: 480px;
46
+ background: var(--surface-2);
47
+ border: 1px solid var(--border);
48
+ color: var(--text);
49
+ padding: 0.45rem 0.75rem;
50
+ border-radius: 6px;
51
+ font: inherit;
52
+ }
53
+
54
+ #filter:focus { outline: 2px solid var(--accent); outline-offset: 0; }
55
+
56
+ .layout {
57
+ display: grid;
58
+ grid-template-columns: 280px 1fr;
59
+ min-height: calc(100vh - 56px);
60
+ }
61
+
62
+ #sidebar {
63
+ border-right: 1px solid var(--border);
64
+ background: var(--surface);
65
+ overflow-y: auto;
66
+ padding: 0.5rem 0;
67
+ max-height: calc(100vh - 56px);
68
+ position: sticky;
69
+ top: 56px;
70
+ }
71
+
72
+ #doc-list a {
73
+ display: block;
74
+ padding: 0.5rem 1rem;
75
+ color: var(--text);
76
+ text-decoration: none;
77
+ border-left: 3px solid transparent;
78
+ font-size: 0.9rem;
79
+ line-height: 1.3;
80
+ }
81
+
82
+ #doc-list a small {
83
+ display: block;
84
+ color: var(--text-muted);
85
+ font-size: 0.75rem;
86
+ margin-top: 2px;
87
+ }
88
+
89
+ #doc-list a:hover { background: var(--surface-2); }
90
+ #doc-list a.active {
91
+ background: var(--surface-2);
92
+ border-left-color: var(--accent);
93
+ }
94
+
95
+ #content { padding: 2rem 2.5rem; max-width: 60rem; }
96
+
97
+ #doc-render { line-height: 1.55; }
98
+ #doc-render h1 { border-bottom: 1px solid var(--border); padding-bottom: 0.35rem; }
99
+ #doc-render h1, #doc-render h2, #doc-render h3 { color: #fff; }
100
+ #doc-render a { color: var(--accent); }
101
+ #doc-render a:hover { color: var(--accent-hover); }
102
+ #doc-render code {
103
+ background: var(--code-bg);
104
+ padding: 0.1rem 0.35rem;
105
+ border-radius: 4px;
106
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
107
+ font-size: 0.9em;
108
+ }
109
+ #doc-render pre {
110
+ background: var(--code-bg);
111
+ padding: 0.85rem 1rem;
112
+ border-radius: 6px;
113
+ overflow-x: auto;
114
+ border: 1px solid var(--border);
115
+ }
116
+ #doc-render pre code { background: transparent; padding: 0; }
117
+ #doc-render blockquote {
118
+ margin: 0;
119
+ padding: 0.4rem 1rem;
120
+ border-left: 3px solid var(--accent);
121
+ color: var(--text-muted);
122
+ background: var(--surface);
123
+ }
124
+ #doc-render table { border-collapse: collapse; margin: 1rem 0; }
125
+ #doc-render th, #doc-render td {
126
+ border: 1px solid var(--border);
127
+ padding: 0.4rem 0.75rem;
128
+ }
129
+ #doc-render th { background: var(--surface-2); }
130
+
131
+ .hint { color: var(--text-muted); }
132
+ .error { color: #ff7f7f; }
@@ -0,0 +1,300 @@
1
+ // CLEO Docs Viewer SPA — minimal, zero-dep client for /api/docs + /api/docs/:slug.
2
+ // Renders markdown via a tiny inline renderer (sufficient for headings, code,
3
+ // lists, links, blockquotes, inline-code, bold, italic, tables, hr). For
4
+ // anything richer we recommend wiring a real markdown lib later.
5
+
6
+ const $list = document.getElementById('doc-list');
7
+ const $render = document.getElementById('doc-render');
8
+ const $filter = document.getElementById('filter');
9
+
10
+ const state = {
11
+ docs: [],
12
+ activeSlug: null,
13
+ };
14
+
15
+ function escapeHtml(str) {
16
+ return String(str)
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;');
22
+ }
23
+
24
+ // Tiny markdown -> HTML renderer. Not CommonMark-complete; covers the basics.
25
+ function renderMarkdown(md) {
26
+ if (typeof md !== 'string') return '';
27
+ const lines = md.split('\n');
28
+ const out = [];
29
+ let inCode = false;
30
+ let codeBuf = [];
31
+ let codeLang = '';
32
+ let listType = null;
33
+ let listBuf = [];
34
+ let paraBuf = [];
35
+ let inTable = false;
36
+ let tableBuf = [];
37
+
38
+ const flushPara = () => {
39
+ if (paraBuf.length === 0) return;
40
+ out.push('<p>' + inlineMd(paraBuf.join(' ')) + '</p>');
41
+ paraBuf = [];
42
+ };
43
+ const flushList = () => {
44
+ if (listType === null || listBuf.length === 0) return;
45
+ const tag = listType === 'ol' ? 'ol' : 'ul';
46
+ out.push(`<${tag}>` + listBuf.map((i) => `<li>${inlineMd(i)}</li>`).join('') + `</${tag}>`);
47
+ listBuf = [];
48
+ listType = null;
49
+ };
50
+ const flushTable = () => {
51
+ if (!inTable || tableBuf.length === 0) return;
52
+ const rows = tableBuf.map((r) =>
53
+ r
54
+ .replace(/^\||\|$/g, '')
55
+ .split('|')
56
+ .map((c) => c.trim()),
57
+ );
58
+ const head = rows[0];
59
+ const body = rows.slice(2);
60
+ let html = '<table><thead><tr>';
61
+ for (const c of head) html += `<th>${inlineMd(c)}</th>`;
62
+ html += '</tr></thead><tbody>';
63
+ for (const row of body) {
64
+ html += '<tr>';
65
+ for (const c of row) html += `<td>${inlineMd(c)}</td>`;
66
+ html += '</tr>';
67
+ }
68
+ html += '</tbody></table>';
69
+ out.push(html);
70
+ tableBuf = [];
71
+ inTable = false;
72
+ };
73
+
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const line = lines[i];
76
+ const fence = line.match(/^```(\w*)\s*$/);
77
+ if (fence) {
78
+ flushPara();
79
+ flushList();
80
+ flushTable();
81
+ if (inCode) {
82
+ out.push(
83
+ `<pre><code class="lang-${escapeHtml(codeLang)}">` +
84
+ escapeHtml(codeBuf.join('\n')) +
85
+ '</code></pre>',
86
+ );
87
+ codeBuf = [];
88
+ codeLang = '';
89
+ inCode = false;
90
+ } else {
91
+ inCode = true;
92
+ codeLang = fence[1] || '';
93
+ }
94
+ continue;
95
+ }
96
+ if (inCode) {
97
+ codeBuf.push(line);
98
+ continue;
99
+ }
100
+
101
+ // Blank line → flush block buffers
102
+ if (/^\s*$/.test(line)) {
103
+ flushPara();
104
+ flushList();
105
+ flushTable();
106
+ continue;
107
+ }
108
+
109
+ // Tables (very basic — | a | b | with separator row)
110
+ if (/^\s*\|.*\|\s*$/.test(line)) {
111
+ if (!inTable) {
112
+ flushPara();
113
+ flushList();
114
+ inTable = true;
115
+ }
116
+ tableBuf.push(line.trim());
117
+ continue;
118
+ } else if (inTable) {
119
+ flushTable();
120
+ }
121
+
122
+ // Headings
123
+ const h = line.match(/^(#{1,6})\s+(.*)$/);
124
+ if (h) {
125
+ flushPara();
126
+ flushList();
127
+ const level = h[1].length;
128
+ out.push(`<h${level}>${inlineMd(h[2])}</h${level}>`);
129
+ continue;
130
+ }
131
+
132
+ // HR
133
+ if (/^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
134
+ flushPara();
135
+ flushList();
136
+ out.push('<hr/>');
137
+ continue;
138
+ }
139
+
140
+ // Blockquote
141
+ const bq = line.match(/^>\s?(.*)$/);
142
+ if (bq) {
143
+ flushPara();
144
+ flushList();
145
+ out.push(`<blockquote>${inlineMd(bq[1])}</blockquote>`);
146
+ continue;
147
+ }
148
+
149
+ // Ordered / unordered list items
150
+ const ol = line.match(/^\s*(\d+)\.\s+(.*)$/);
151
+ const ul = line.match(/^\s*[-*+]\s+(.*)$/);
152
+ if (ol) {
153
+ flushPara();
154
+ if (listType !== 'ol') flushList();
155
+ listType = 'ol';
156
+ listBuf.push(ol[2]);
157
+ continue;
158
+ }
159
+ if (ul) {
160
+ flushPara();
161
+ if (listType !== 'ul') flushList();
162
+ listType = 'ul';
163
+ listBuf.push(ul[1]);
164
+ continue;
165
+ }
166
+
167
+ // Default — accumulate paragraph
168
+ flushList();
169
+ paraBuf.push(line);
170
+ }
171
+
172
+ if (inCode) {
173
+ out.push('<pre><code>' + escapeHtml(codeBuf.join('\n')) + '</code></pre>');
174
+ }
175
+ flushPara();
176
+ flushList();
177
+ flushTable();
178
+
179
+ return out.join('\n');
180
+ }
181
+
182
+ // Inline markdown — code spans, bold, italic, links.
183
+ function inlineMd(s) {
184
+ let str = escapeHtml(s);
185
+ // Inline code
186
+ str = str.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
187
+ // Bold + italic combined
188
+ str = str.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
189
+ // Bold
190
+ str = str.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
191
+ // Italic
192
+ str = str.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
193
+ // Links [text](href)
194
+ str = str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, href) => {
195
+ const safe = String(href).replace(/"/g, '%22');
196
+ return `<a href="${safe}" target="_blank" rel="noopener">${text}</a>`;
197
+ });
198
+ return str;
199
+ }
200
+
201
+ async function fetchJson(url) {
202
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
203
+ const body = await res.json().catch(() => null);
204
+ if (!res.ok || !body) {
205
+ const code = body?.error?.code ?? 'E_HTTP';
206
+ const msg = body?.error?.message ?? res.statusText;
207
+ throw new Error(`${code}: ${msg}`);
208
+ }
209
+ if (body && body.success === false) {
210
+ throw new Error(`${body.error?.code ?? 'E_UNKNOWN'}: ${body.error?.message ?? 'unknown'}`);
211
+ }
212
+ return body.data;
213
+ }
214
+
215
+ function renderDocList(docs) {
216
+ if (docs.length === 0) {
217
+ $list.innerHTML = '<p class="hint">No published docs in this project. Run <code>cleo docs publish</code> first.</p>';
218
+ return;
219
+ }
220
+ const html = docs
221
+ .map((d) => {
222
+ const slug = escapeHtml(d.slug || d.id);
223
+ const title = escapeHtml(d.title || d.slug || d.id);
224
+ const type = d.type ? `<small>${escapeHtml(d.type)}</small>` : '';
225
+ const isActive = d.slug === state.activeSlug ? ' class="active"' : '';
226
+ return `<a href="/docs/${slug}" data-slug="${slug}"${isActive}>${title}${type}</a>`;
227
+ })
228
+ .join('');
229
+ $list.innerHTML = html;
230
+ $list.querySelectorAll('a').forEach((a) => {
231
+ a.addEventListener('click', (e) => {
232
+ e.preventDefault();
233
+ const slug = a.getAttribute('data-slug');
234
+ navigate(slug);
235
+ });
236
+ });
237
+ }
238
+
239
+ function applyFilter(q) {
240
+ const lower = q.toLowerCase().trim();
241
+ if (!lower) return renderDocList(state.docs);
242
+ const filtered = state.docs.filter((d) => {
243
+ return (
244
+ (d.slug || '').toLowerCase().includes(lower) ||
245
+ (d.title || '').toLowerCase().includes(lower) ||
246
+ (d.type || '').toLowerCase().includes(lower)
247
+ );
248
+ });
249
+ renderDocList(filtered);
250
+ }
251
+
252
+ async function loadDoc(slug) {
253
+ if (!slug) {
254
+ $render.innerHTML = '<p class="hint">Select a document from the sidebar.</p>';
255
+ return;
256
+ }
257
+ state.activeSlug = slug;
258
+ $render.innerHTML = '<p class="hint">Loading...</p>';
259
+ try {
260
+ const data = await fetchJson(`/api/docs/${encodeURIComponent(slug)}`);
261
+ const md = data.content ?? '';
262
+ $render.innerHTML = renderMarkdown(md);
263
+ document.title = `${data.title || slug} — CLEO Docs`;
264
+ // Highlight active in sidebar
265
+ $list.querySelectorAll('a').forEach((a) => {
266
+ a.classList.toggle('active', a.getAttribute('data-slug') === slug);
267
+ });
268
+ } catch (err) {
269
+ $render.innerHTML = `<p class="error">${escapeHtml(String(err.message || err))}</p>`;
270
+ }
271
+ }
272
+
273
+ function navigate(slug) {
274
+ history.pushState({ slug }, '', `/docs/${slug}`);
275
+ loadDoc(slug);
276
+ }
277
+
278
+ window.addEventListener('popstate', (e) => {
279
+ const slug = e.state?.slug || slugFromPath(location.pathname);
280
+ if (slug) loadDoc(slug);
281
+ });
282
+
283
+ function slugFromPath(pathname) {
284
+ const m = pathname.match(/^\/docs\/([^/?#]+)/);
285
+ return m ? decodeURIComponent(m[1]) : null;
286
+ }
287
+
288
+ $filter.addEventListener('input', (e) => applyFilter(e.target.value));
289
+
290
+ (async function init() {
291
+ try {
292
+ const data = await fetchJson('/api/docs');
293
+ state.docs = data.docs || [];
294
+ renderDocList(state.docs);
295
+ const initialSlug = slugFromPath(location.pathname);
296
+ if (initialSlug) loadDoc(initialSlug);
297
+ } catch (err) {
298
+ $list.innerHTML = `<p class="error">${escapeHtml(String(err.message || err))}</p>`;
299
+ }
300
+ })();