mdorigin 0.1.1 → 0.1.3

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.
Files changed (40) hide show
  1. package/README.md +97 -9
  2. package/dist/adapters/cloudflare.d.ts +7 -1
  3. package/dist/adapters/cloudflare.js +11 -1
  4. package/dist/adapters/node.d.ts +4 -0
  5. package/dist/adapters/node.js +51 -11
  6. package/dist/cli/build-cloudflare.js +11 -4
  7. package/dist/cli/build-index.js +17 -3
  8. package/dist/cli/build-search.d.ts +1 -0
  9. package/dist/cli/build-search.js +45 -0
  10. package/dist/cli/dev.js +14 -4
  11. package/dist/cli/main.js +15 -3
  12. package/dist/cli/search.d.ts +1 -0
  13. package/dist/cli/search.js +36 -0
  14. package/dist/cloudflare.d.ts +3 -0
  15. package/dist/cloudflare.js +67 -6
  16. package/dist/core/api.d.ts +13 -0
  17. package/dist/core/api.js +160 -0
  18. package/dist/core/content-store.js +5 -0
  19. package/dist/core/content-type.d.ts +1 -0
  20. package/dist/core/content-type.js +3 -0
  21. package/dist/core/directory-index.d.ts +1 -1
  22. package/dist/core/directory-index.js +5 -1
  23. package/dist/core/extensions.d.ts +66 -0
  24. package/dist/core/extensions.js +86 -0
  25. package/dist/core/markdown.d.ts +4 -0
  26. package/dist/core/markdown.js +53 -0
  27. package/dist/core/request-handler.d.ts +6 -0
  28. package/dist/core/request-handler.js +211 -68
  29. package/dist/core/site-config.d.ts +18 -0
  30. package/dist/core/site-config.js +88 -16
  31. package/dist/html/template.d.ts +8 -0
  32. package/dist/html/template.js +228 -12
  33. package/dist/html/theme.js +254 -9
  34. package/dist/index-builder.d.ts +3 -1
  35. package/dist/index-builder.js +82 -45
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +1 -0
  38. package/dist/search.d.ts +59 -0
  39. package/dist/search.js +370 -0
  40. package/package.json +20 -5
@@ -14,6 +14,14 @@ export function renderDocument(options) {
14
14
  const faviconMeta = options.favicon
15
15
  ? `<link rel="icon" href="${escapeHtml(options.favicon)}">`
16
16
  : '';
17
+ const absoluteSocialImageUrl = getAbsoluteSiteAssetUrl(options.siteUrl, options.socialImage);
18
+ const socialImageMeta = absoluteSocialImageUrl
19
+ ? [
20
+ `<meta property="og:image" content="${escapeHtml(absoluteSocialImageUrl)}">`,
21
+ '<meta name="twitter:card" content="summary_large_image">',
22
+ `<meta name="twitter:image" content="${escapeHtml(absoluteSocialImageUrl)}">`,
23
+ ].join('')
24
+ : '';
17
25
  const alternateMarkdownMeta = options.alternateMarkdownPath
18
26
  ? `<link rel="alternate" type="text/markdown" href="${escapeHtml(options.alternateMarkdownPath)}">`
19
27
  : '';
@@ -23,6 +31,24 @@ export function renderDocument(options) {
23
31
  .map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
24
32
  .join('')}</ul></nav>`
25
33
  : '';
34
+ const searchToggleBlock = options.searchEnabled
35
+ ? [
36
+ '<div class="site-search" data-site-search>',
37
+ '<button type="button" class="site-search__toggle" aria-expanded="false" aria-controls="site-search-panel">Search</button>',
38
+ '<div id="site-search-panel" class="site-search__panel" hidden>',
39
+ '<form class="site-search__form" role="search" action="/api/search" method="get">',
40
+ '<label class="site-search__label" for="site-search-input">Search site</label>',
41
+ '<div class="site-search__controls">',
42
+ '<input id="site-search-input" class="site-search__input" type="search" name="q" placeholder="Search docs and skills" autocomplete="off">',
43
+ '<button type="submit" class="site-search__submit">Go</button>',
44
+ '</div>',
45
+ '<p class="site-search__hint">Search is powered by <code>/api/search</code>.</p>',
46
+ '</form>',
47
+ '<div class="site-search__results" data-site-search-results></div>',
48
+ '</div>',
49
+ '</div>',
50
+ ].join('')
51
+ : '';
26
52
  const footerNavBlock = options.footerNav && options.footerNav.length > 0
27
53
  ? `<nav class="site-footer__nav"><ul>${options.footerNav
28
54
  .map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
@@ -46,12 +72,23 @@ export function renderDocument(options) {
46
72
  const footerTextBlock = options.footerText
47
73
  ? `<p class="site-footer__text">${escapeHtml(options.footerText)}</p>`
48
74
  : '';
49
- const footerBlock = footerNavBlock || socialLinksBlock || footerTextBlock || editLinkBlock
50
- ? `<footer class="site-footer"><div class="site-footer__inner">${footerNavBlock}${socialLinksBlock}${footerTextBlock}${editLinkBlock}</div></footer>`
75
+ const footerMetaBlock = socialLinksBlock || editLinkBlock
76
+ ? `<div class="site-footer__meta">${socialLinksBlock}${editLinkBlock}</div>`
77
+ : '';
78
+ const footerBlock = footerNavBlock || footerTextBlock || footerMetaBlock
79
+ ? `<footer class="site-footer"><div class="site-footer__inner">${footerNavBlock}${footerTextBlock}${footerMetaBlock}</div></footer>`
51
80
  : '';
52
81
  const articleBody = options.template === 'catalog'
53
- ? renderCatalogArticle(options.body, options.catalogEntries ?? [])
82
+ ? renderCatalogArticle(options.body, options.catalogEntries ?? [], {
83
+ requestPath: options.catalogRequestPath ?? '/',
84
+ initialPostCount: options.catalogInitialPostCount ?? 10,
85
+ loadMoreStep: options.catalogLoadMoreStep ?? 10,
86
+ })
54
87
  : options.body;
88
+ const searchScript = options.searchEnabled ? renderSearchScript() : '';
89
+ const headerBlock = options.headerHtml ??
90
+ `<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="${brandHref}">${logoBlock}<span>${siteTitle}</span></a></p>${siteDescriptionBlock}</div><div class="site-header__actions">${navBlock}${searchToggleBlock}</div></div></header>`;
91
+ const renderedFooterBlock = options.footerHtml ?? footerBlock;
55
92
  return [
56
93
  '<!doctype html>',
57
94
  '<html lang="en">',
@@ -62,19 +99,33 @@ export function renderDocument(options) {
62
99
  summaryMeta,
63
100
  canonicalMeta,
64
101
  faviconMeta,
102
+ socialImageMeta,
65
103
  alternateMarkdownMeta,
66
104
  stylesheetBlock,
67
105
  '</head>',
68
106
  `<body data-theme="${options.theme}" data-template="${options.template}">`,
69
- `<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="${brandHref}">${logoBlock}<span>${siteTitle}</span></a></p>${siteDescriptionBlock}</div>${navBlock}</div></header>`,
107
+ headerBlock,
70
108
  '<main>',
71
109
  `<article>${articleBody}</article>`,
72
110
  '</main>',
73
- footerBlock,
111
+ renderedFooterBlock,
112
+ searchScript,
74
113
  '</body>',
75
114
  '</html>',
76
115
  ].join('');
77
116
  }
117
+ function getAbsoluteSiteAssetUrl(siteUrl, href) {
118
+ if (!href) {
119
+ return undefined;
120
+ }
121
+ if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) || href.startsWith('//')) {
122
+ return href;
123
+ }
124
+ if (!siteUrl) {
125
+ return undefined;
126
+ }
127
+ return new URL(href.replace(/^\//, ''), `${siteUrl}/`).toString();
128
+ }
78
129
  export function escapeHtml(value) {
79
130
  return value
80
131
  .replaceAll('&', '&amp;')
@@ -102,18 +153,29 @@ function renderSocialIcon(icon) {
102
153
  function iconSvg(pathData) {
103
154
  return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${pathData}"></path></svg>`;
104
155
  }
105
- function renderCatalogArticle(body, entries) {
156
+ function renderCatalogArticle(body, entries, options) {
106
157
  if (entries.length === 0) {
107
158
  return body;
108
159
  }
109
160
  const directories = entries.filter((entry) => entry.kind === 'directory');
110
161
  const articles = entries.filter((entry) => entry.kind === 'article');
162
+ const initialPostCount = Math.max(1, options.initialPostCount);
163
+ const visibleArticles = articles.slice(0, initialPostCount);
164
+ const shouldLoadMore = articles.length > visibleArticles.length;
111
165
  return [
112
166
  `<div class="catalog-page__body">${body}</div>`,
113
167
  '<section class="catalog-page" aria-label="Catalog">',
114
168
  directories.length > 0 ? renderCatalogDirectories(directories) : '',
115
- articles.length > 0 ? renderCatalogArticles(articles) : '',
169
+ articles.length > 0
170
+ ? renderCatalogArticles(visibleArticles, {
171
+ requestPath: options.requestPath,
172
+ nextOffset: visibleArticles.length,
173
+ loadMoreStep: options.loadMoreStep,
174
+ hasMore: shouldLoadMore,
175
+ })
176
+ : '',
116
177
  '</section>',
178
+ shouldLoadMore ? renderCatalogLoadMoreScript() : '',
117
179
  ].join('');
118
180
  }
119
181
  function renderCatalogDirectories(entries) {
@@ -125,12 +187,166 @@ function renderCatalogDirectories(entries) {
125
187
  '</div>',
126
188
  ].join('');
127
189
  }
128
- function renderCatalogArticles(entries) {
190
+ export function renderCatalogArticleItems(entries) {
191
+ return entries
192
+ .map((entry) => `<a class="catalog-item" href="${escapeHtml(entry.href)}"><strong class="catalog-item__title">${escapeHtml(entry.title)}</strong>${entry.detail
193
+ ? `<span class="catalog-item__detail">${escapeHtml(entry.detail)}</span>`
194
+ : ''}</a>`)
195
+ .join('');
196
+ }
197
+ function renderCatalogArticles(entries, options) {
129
198
  return [
130
- '<div class="catalog-list">',
131
- ...entries.map((entry) => `<a class="catalog-item" href="${escapeHtml(entry.href)}"><strong class="catalog-item__title">${escapeHtml(entry.title)}</strong>${entry.detail
132
- ? `<span class="catalog-item__detail">${escapeHtml(entry.detail)}</span>`
133
- : ''}</a>`),
199
+ '<div class="catalog-list" data-catalog-articles>',
200
+ renderCatalogArticleItems(entries),
134
201
  '</div>',
202
+ options.hasMore
203
+ ? `<div class="catalog-load-more"><button type="button" class="catalog-load-more__button" data-catalog-load-more data-request-path="${escapeHtml(options.requestPath)}" data-next-offset="${escapeHtml(String(options.nextOffset))}" data-load-more-step="${escapeHtml(String(options.loadMoreStep))}">Load more</button></div>`
204
+ : '',
205
+ ].join('');
206
+ }
207
+ function renderCatalogLoadMoreScript() {
208
+ return `<script>
209
+ (() => {
210
+ const button = document.querySelector('[data-catalog-load-more]');
211
+ const list = document.querySelector('[data-catalog-articles]');
212
+ if (!(button instanceof HTMLButtonElement) || !(list instanceof HTMLElement)) {
213
+ return;
214
+ }
215
+
216
+ const loadMore = async () => {
217
+ const requestPath = button.dataset.requestPath;
218
+ const nextOffset = button.dataset.nextOffset;
219
+ const loadMoreStep = button.dataset.loadMoreStep;
220
+ if (!requestPath || !nextOffset || !loadMoreStep) {
221
+ return;
222
+ }
223
+
224
+ button.disabled = true;
225
+ const previousLabel = button.textContent;
226
+ button.textContent = 'Loading...';
227
+
228
+ try {
229
+ const url = new URL(requestPath, window.location.origin);
230
+ url.searchParams.set('catalog-format', 'posts');
231
+ url.searchParams.set('catalog-offset', nextOffset);
232
+ url.searchParams.set('catalog-limit', loadMoreStep);
233
+
234
+ const response = await fetch(url.toString(), {
235
+ headers: { Accept: 'application/json' },
236
+ });
237
+ if (!response.ok) {
238
+ throw new Error('Failed to load more posts');
239
+ }
240
+
241
+ const payload = await response.json();
242
+ if (typeof payload.itemsHtml === 'string' && payload.itemsHtml !== '') {
243
+ list.insertAdjacentHTML('beforeend', payload.itemsHtml);
244
+ }
245
+
246
+ if (payload.hasMore === true && typeof payload.nextOffset === 'number') {
247
+ button.dataset.nextOffset = String(payload.nextOffset);
248
+ button.disabled = false;
249
+ button.textContent = previousLabel ?? 'Load more';
250
+ return;
251
+ }
252
+
253
+ button.remove();
254
+ } catch {
255
+ button.disabled = false;
256
+ button.textContent = previousLabel ?? 'Load more';
257
+ }
258
+ };
259
+
260
+ button.addEventListener('click', () => {
261
+ void loadMore();
262
+ });
263
+ })();
264
+ </script>`;
265
+ }
266
+ function renderSearchScript() {
267
+ return [
268
+ '<script>',
269
+ '(function () {',
270
+ ' const root = document.querySelector("[data-site-search]");',
271
+ ' if (!root) return;',
272
+ ' const toggle = root.querySelector(".site-search__toggle");',
273
+ ' const panel = root.querySelector(".site-search__panel");',
274
+ ' const form = root.querySelector(".site-search__form");',
275
+ ' const input = root.querySelector(".site-search__input");',
276
+ ' const results = root.querySelector("[data-site-search-results]");',
277
+ ' if (!toggle || !panel || !form || !input || !results) return;',
278
+ ' let controller = null;',
279
+ ' function renderMessage(message) {',
280
+ ' results.innerHTML = `<p class="site-search__message">${escapeHtmlForScript(message)}</p>`;',
281
+ ' }',
282
+ ' function renderHits(hits) {',
283
+ ' if (!Array.isArray(hits) || hits.length === 0) {',
284
+ ' renderMessage("No results.");',
285
+ ' return;',
286
+ ' }',
287
+ ' results.innerHTML = hits.map((hit) => {',
288
+ ' const href = escapeHtmlForScript(hit.canonicalUrl || hit.docId || "#");',
289
+ ' const title = escapeHtmlForScript(hit.title || hit.relativePath || "Untitled");',
290
+ ' const summary = typeof hit.summary === "string" ? `<span class="site-search__item-summary">${escapeHtmlForScript(hit.summary)}</span>` : "";',
291
+ ' const excerpt = hit.bestMatch && typeof hit.bestMatch.excerpt === "string" ? `<span class="site-search__item-excerpt">${escapeHtmlForScript(hit.bestMatch.excerpt)}</span>` : "";',
292
+ ' return `<a class="site-search__item" href="${href}"><strong class="site-search__item-title">${title}</strong>${summary}${excerpt}</a>`;',
293
+ ' }).join("");',
294
+ ' }',
295
+ ' async function runSearch(query) {',
296
+ ' if (controller) controller.abort();',
297
+ ' controller = new AbortController();',
298
+ ' renderMessage("Searching...");',
299
+ ' try {',
300
+ ' const url = new URL("/api/search", window.location.origin);',
301
+ ' url.searchParams.set("q", query);',
302
+ ' url.searchParams.set("topK", "8");',
303
+ ' const response = await fetch(url, { signal: controller.signal });',
304
+ ' if (!response.ok) {',
305
+ ' renderMessage(`Search failed (${response.status}).`);',
306
+ ' return;',
307
+ ' }',
308
+ ' const payload = await response.json();',
309
+ ' renderHits(payload.hits);',
310
+ ' } catch (error) {',
311
+ ' if (error && typeof error === "object" && "name" in error && error.name === "AbortError") return;',
312
+ ' renderMessage("Search failed.");',
313
+ ' }',
314
+ ' }',
315
+ ' function setOpen(open) {',
316
+ ' toggle.setAttribute("aria-expanded", open ? "true" : "false");',
317
+ ' panel.hidden = !open;',
318
+ ' if (open) {',
319
+ ' input.focus();',
320
+ ' if (!results.innerHTML) renderMessage("Search docs, guides, and skills.");',
321
+ ' }',
322
+ ' }',
323
+ ' toggle.addEventListener("click", () => setOpen(panel.hidden));',
324
+ ' form.addEventListener("submit", (event) => {',
325
+ ' event.preventDefault();',
326
+ ' const query = input.value.trim();',
327
+ ' if (!query) {',
328
+ ' renderMessage("Enter a search query.");',
329
+ ' return;',
330
+ ' }',
331
+ ' void runSearch(query);',
332
+ ' });',
333
+ ' document.addEventListener("keydown", (event) => {',
334
+ ' if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {',
335
+ ' event.preventDefault();',
336
+ ' setOpen(true);',
337
+ ' }',
338
+ ' if (event.key === "Escape" && !panel.hidden) setOpen(false);',
339
+ ' });',
340
+ '})();',
341
+ '',
342
+ 'function escapeHtmlForScript(value) {',
343
+ ' return String(value)',
344
+ ' .replaceAll("&", "&amp;")',
345
+ ' .replaceAll("<", "&lt;")',
346
+ ' .replaceAll(">", "&gt;")',
347
+ ' .replaceAll(\'"\', "&quot;")',
348
+ ' .replaceAll("\\\'", "&#39;");',
349
+ '}',
350
+ '</script>',
135
351
  ].join('');
136
352
  }
@@ -1,13 +1,135 @@
1
1
  export function getBuiltInThemeStyles(theme) {
2
- switch (theme) {
3
- case 'atlas':
4
- return buildAtlasThemeStyles();
5
- case 'gazette':
6
- return buildGazetteThemeStyles();
7
- case 'paper':
8
- default:
9
- return buildPaperThemeStyles();
10
- }
2
+ const themeStyles = (() => {
3
+ switch (theme) {
4
+ case 'atlas':
5
+ return buildAtlasThemeStyles();
6
+ case 'gazette':
7
+ return buildGazetteThemeStyles();
8
+ case 'paper':
9
+ default:
10
+ return buildPaperThemeStyles();
11
+ }
12
+ })();
13
+ return `${themeStyles}\n${buildSharedSearchStyles()}`;
14
+ }
15
+ function buildSharedSearchStyles() {
16
+ return `
17
+ .site-header__actions {
18
+ min-width: 0;
19
+ display: flex;
20
+ align-items: flex-end;
21
+ justify-content: flex-end;
22
+ gap: 1rem;
23
+ }
24
+ .site-search {
25
+ position: relative;
26
+ }
27
+ .site-search__toggle {
28
+ appearance: none;
29
+ border: 1px solid var(--border);
30
+ background: color-mix(in srgb, var(--surface) 92%, transparent);
31
+ color: var(--muted);
32
+ border-radius: 999px;
33
+ padding: 0.45rem 0.82rem;
34
+ font: inherit;
35
+ font-size: 0.88rem;
36
+ cursor: pointer;
37
+ }
38
+ .site-search__toggle:hover {
39
+ color: var(--text);
40
+ }
41
+ .site-search__panel {
42
+ position: absolute;
43
+ top: calc(100% + 0.7rem);
44
+ right: 0;
45
+ width: min(32rem, calc(100vw - 2rem));
46
+ padding: 1rem;
47
+ border: 1px solid var(--border);
48
+ border-radius: 18px;
49
+ background: color-mix(in srgb, var(--surface) 95%, white 5%);
50
+ box-shadow: 0 18px 40px rgba(24, 18, 11, 0.14);
51
+ z-index: 2;
52
+ }
53
+ .site-search__label {
54
+ display: block;
55
+ font-size: 0.8rem;
56
+ text-transform: uppercase;
57
+ letter-spacing: 0.08em;
58
+ color: var(--muted);
59
+ margin-bottom: 0.55rem;
60
+ }
61
+ .site-search__controls {
62
+ display: grid;
63
+ grid-template-columns: minmax(0, 1fr) auto;
64
+ gap: 0.6rem;
65
+ }
66
+ .site-search__input,
67
+ .site-search__submit {
68
+ font: inherit;
69
+ }
70
+ .site-search__input {
71
+ width: 100%;
72
+ border: 1px solid var(--border);
73
+ border-radius: 12px;
74
+ background: var(--surface);
75
+ color: var(--text);
76
+ padding: 0.7rem 0.85rem;
77
+ }
78
+ .site-search__submit {
79
+ appearance: none;
80
+ border: 1px solid var(--text);
81
+ border-radius: 12px;
82
+ background: var(--text);
83
+ color: var(--surface);
84
+ padding: 0.7rem 0.95rem;
85
+ cursor: pointer;
86
+ }
87
+ .site-search__hint,
88
+ .site-search__message {
89
+ color: var(--muted);
90
+ font-size: 0.84rem;
91
+ }
92
+ .site-search__results {
93
+ margin-top: 0.9rem;
94
+ display: grid;
95
+ gap: 0.7rem;
96
+ }
97
+ .site-search__item {
98
+ display: block;
99
+ text-decoration: none;
100
+ border-top: 1px solid var(--border);
101
+ padding-top: 0.7rem;
102
+ }
103
+ .site-search__item:first-child {
104
+ border-top: 0;
105
+ padding-top: 0;
106
+ }
107
+ .site-search__item-title {
108
+ display: block;
109
+ color: var(--text);
110
+ }
111
+ .site-search__item-summary,
112
+ .site-search__item-excerpt {
113
+ display: block;
114
+ margin-top: 0.25rem;
115
+ color: var(--muted);
116
+ font-size: 0.88rem;
117
+ }
118
+ .site-search__item-excerpt {
119
+ font-size: 0.82rem;
120
+ }
121
+ @media (max-width: 720px) {
122
+ .site-header__actions {
123
+ flex-direction: column;
124
+ align-items: stretch;
125
+ }
126
+ .site-search__panel {
127
+ left: 0;
128
+ right: auto;
129
+ width: min(100%, 34rem);
130
+ }
131
+ }
132
+ `.trim();
11
133
  }
12
134
  function buildPaperThemeStyles() {
13
135
  return `
@@ -125,6 +247,13 @@ main {
125
247
  padding-top: 1rem;
126
248
  color: var(--muted);
127
249
  }
250
+ .site-footer__meta {
251
+ margin-top: 0.9rem;
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: space-between;
255
+ gap: 1rem;
256
+ }
128
257
  .site-footer__nav ul,
129
258
  .site-footer__social {
130
259
  list-style: none;
@@ -163,6 +292,19 @@ main {
163
292
  display: block;
164
293
  margin-top: 0.9rem;
165
294
  }
295
+ .site-footer__edit-link {
296
+ display: inline-block;
297
+ margin-top: 0;
298
+ font-size: 0.78rem;
299
+ color: var(--muted);
300
+ opacity: 0.72;
301
+ }
302
+ @media (max-width: 720px) {
303
+ .site-footer__meta {
304
+ flex-direction: column;
305
+ align-items: flex-start;
306
+ }
307
+ }
166
308
  article {
167
309
  background: color-mix(in srgb, var(--surface) 92%, white 8%);
168
310
  border: 1px solid var(--border);
@@ -253,6 +395,27 @@ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
253
395
  .catalog-item:hover {
254
396
  text-decoration: none;
255
397
  }
398
+ .catalog-load-more {
399
+ margin-top: 1.2rem;
400
+ }
401
+ .catalog-load-more__button {
402
+ appearance: none;
403
+ border: 1px solid var(--border);
404
+ background: var(--surface);
405
+ color: var(--text);
406
+ border-radius: 999px;
407
+ padding: 0.65rem 1rem;
408
+ font: inherit;
409
+ font-size: 0.95rem;
410
+ cursor: pointer;
411
+ }
412
+ .catalog-load-more__button:hover {
413
+ background: color-mix(in srgb, var(--surface) 86%, var(--code-bg) 14%);
414
+ }
415
+ .catalog-load-more__button:disabled {
416
+ cursor: wait;
417
+ opacity: 0.7;
418
+ }
256
419
  @media (max-width: 720px) {
257
420
  html { font-size: 17px; }
258
421
  .site-header, main, .site-footer { padding-left: 1rem; padding-right: 1rem; }
@@ -386,6 +549,13 @@ main {
386
549
  padding-top: 1rem;
387
550
  color: var(--muted);
388
551
  }
552
+ .site-footer__meta {
553
+ margin-top: 1rem;
554
+ display: flex;
555
+ align-items: center;
556
+ justify-content: space-between;
557
+ gap: 1rem;
558
+ }
389
559
  .site-footer__nav ul,
390
560
  .site-footer__social {
391
561
  list-style: none;
@@ -424,6 +594,19 @@ main {
424
594
  display: block;
425
595
  margin-top: 0.95rem;
426
596
  }
597
+ .site-footer__edit-link {
598
+ display: inline-block;
599
+ margin-top: 0;
600
+ font-size: 0.78rem;
601
+ color: var(--muted);
602
+ opacity: 0.72;
603
+ }
604
+ @media (max-width: 720px) {
605
+ .site-footer__meta {
606
+ flex-direction: column;
607
+ align-items: flex-start;
608
+ }
609
+ }
427
610
  article {
428
611
  background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(247,250,252,0.98));
429
612
  border: 1px solid var(--border);
@@ -518,6 +701,27 @@ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
518
701
  .catalog-item:hover {
519
702
  text-decoration: none;
520
703
  }
704
+ .catalog-load-more {
705
+ margin-top: 1.2rem;
706
+ }
707
+ .catalog-load-more__button {
708
+ appearance: none;
709
+ border: 1px solid var(--border);
710
+ background: var(--surface);
711
+ color: var(--text);
712
+ border-radius: 999px;
713
+ padding: 0.65rem 1rem;
714
+ font: inherit;
715
+ font-size: 0.95rem;
716
+ cursor: pointer;
717
+ }
718
+ .catalog-load-more__button:hover {
719
+ background: color-mix(in srgb, var(--surface) 75%, var(--accent) 25%);
720
+ }
721
+ .catalog-load-more__button:disabled {
722
+ cursor: wait;
723
+ opacity: 0.7;
724
+ }
521
725
  @media (max-width: 720px) {
522
726
  .site-header__inner, main, .site-footer { padding-left: 1rem; padding-right: 1rem; }
523
727
  .site-header__inner {
@@ -648,6 +852,13 @@ main {
648
852
  padding-top: 1rem;
649
853
  color: var(--muted);
650
854
  }
855
+ .site-footer__meta {
856
+ margin-top: 0.95rem;
857
+ display: flex;
858
+ align-items: center;
859
+ justify-content: space-between;
860
+ gap: 1rem;
861
+ }
651
862
  .site-footer__nav ul,
652
863
  .site-footer__social {
653
864
  list-style: none;
@@ -686,6 +897,19 @@ main {
686
897
  display: block;
687
898
  margin-top: 0.9rem;
688
899
  }
900
+ .site-footer__edit-link {
901
+ display: inline-block;
902
+ margin-top: 0;
903
+ font-size: 0.78rem;
904
+ color: var(--muted);
905
+ opacity: 0.72;
906
+ }
907
+ @media (max-width: 720px) {
908
+ .site-footer__meta {
909
+ flex-direction: column;
910
+ align-items: flex-start;
911
+ }
912
+ }
689
913
  article {
690
914
  background:
691
915
  linear-gradient(180deg, rgba(255,255,255,0.94), rgba(255,253,248,0.98)),
@@ -785,6 +1009,27 @@ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
785
1009
  .catalog-item:hover {
786
1010
  text-decoration: none;
787
1011
  }
1012
+ .catalog-load-more {
1013
+ margin-top: 1.2rem;
1014
+ }
1015
+ .catalog-load-more__button {
1016
+ appearance: none;
1017
+ border: 1px solid var(--border);
1018
+ background: var(--surface);
1019
+ color: var(--text);
1020
+ border-radius: 999px;
1021
+ padding: 0.65rem 1rem;
1022
+ font: inherit;
1023
+ font-size: 0.95rem;
1024
+ cursor: pointer;
1025
+ }
1026
+ .catalog-load-more__button:hover {
1027
+ background: color-mix(in srgb, var(--surface) 84%, #fff 16%);
1028
+ }
1029
+ .catalog-load-more__button:disabled {
1030
+ cursor: wait;
1031
+ opacity: 0.7;
1032
+ }
788
1033
  @media (max-width: 720px) {
789
1034
  html { font-size: 17px; }
790
1035
  .site-header, main, .site-footer { padding-left: 1rem; padding-right: 1rem; }
@@ -1,13 +1,15 @@
1
+ import { type MdoPlugin } from './core/extensions.js';
1
2
  export interface BuildIndexOptions {
2
3
  rootDir?: string;
3
4
  dir?: string;
5
+ plugins?: MdoPlugin[];
4
6
  }
5
7
  export interface BuildIndexResult {
6
8
  updatedFiles: string[];
7
9
  skippedDirectories: string[];
8
10
  }
9
11
  export declare function buildDirectoryIndexes(options: BuildIndexOptions): Promise<BuildIndexResult>;
10
- export declare function buildManagedIndexBlock(directoryPath: string): Promise<string>;
12
+ export declare function buildManagedIndexBlock(directoryPath: string, plugins?: MdoPlugin[]): Promise<string>;
11
13
  export declare function upsertManagedIndexBlock(source: string, block: string, options?: {
12
14
  directoryPath?: string;
13
15
  }): string;