blodemd 0.0.13 → 0.0.15

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 (136) hide show
  1. package/README.md +1 -1
  2. package/dev-server/lib/local-runtime.tsx +12 -15
  3. package/dev-server/package.json +5 -1
  4. package/dist/cli.mjs +1211 -983
  5. package/dist/cli.mjs.map +1 -1
  6. package/docs/app/globals.css +1 -1
  7. package/docs/components/api/api-playground.tsx +2 -5
  8. package/docs/components/api/api-reference.tsx +29 -3
  9. package/docs/components/docs/contextual-menu.tsx +37 -33
  10. package/docs/components/docs/copy-page-menu.tsx +18 -0
  11. package/docs/components/docs/doc-header.tsx +4 -1
  12. package/docs/components/docs/doc-shell.tsx +48 -18
  13. package/docs/components/docs/doc-sidebar.tsx +131 -89
  14. package/docs/components/docs/doc-toc.tsx +1 -0
  15. package/docs/components/docs/mobile-nav.tsx +72 -68
  16. package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
  17. package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
  18. package/docs/components/git/repo-picker.tsx +5 -2
  19. package/docs/components/mdx/index.tsx +31 -21
  20. package/docs/components/posthog-provider.tsx +42 -0
  21. package/docs/components/tenant-analytics.tsx +49 -0
  22. package/docs/components/third-parties.tsx +4 -1
  23. package/docs/components/ui/copy-button.tsx +16 -2
  24. package/docs/components/ui/search-dialog.tsx +431 -0
  25. package/docs/components/ui/search.tsx +44 -427
  26. package/docs/components/web-mcp.tsx +115 -0
  27. package/docs/lib/agent-skills.ts +48 -0
  28. package/docs/lib/config.ts +4 -5
  29. package/docs/lib/docs-collection.ts +7 -22
  30. package/docs/lib/docs-runtime.tsx +160 -10
  31. package/docs/lib/llms-full.ts +82 -0
  32. package/docs/lib/marketing-markdown.ts +79 -0
  33. package/docs/lib/marketing-site.ts +31 -0
  34. package/docs/lib/mdx.ts +17 -6
  35. package/docs/lib/routes.ts +19 -3
  36. package/docs/lib/shiki.ts +4 -0
  37. package/docs/lib/tenancy.ts +15 -2
  38. package/docs/lib/tenant-headers.ts +29 -0
  39. package/docs/lib/tenant-static.ts +73 -99
  40. package/package.json +8 -5
  41. package/packages/@repo/common/package.json +1 -1
  42. package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
  43. package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
  44. package/packages/@repo/contracts/dist/analytics.js +18 -0
  45. package/packages/@repo/contracts/dist/deployment.js +1 -1
  46. package/packages/@repo/contracts/dist/git.js +2 -2
  47. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
  48. package/packages/@repo/contracts/dist/ids.js +2 -1
  49. package/packages/@repo/contracts/dist/index.d.ts +1 -0
  50. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  51. package/packages/@repo/contracts/dist/index.js +1 -0
  52. package/packages/@repo/contracts/dist/project.d.ts +18 -1
  53. package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
  54. package/packages/@repo/contracts/dist/project.js +3 -1
  55. package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
  56. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
  57. package/packages/@repo/contracts/dist/tenant.js +2 -0
  58. package/packages/@repo/contracts/package.json +1 -1
  59. package/packages/@repo/contracts/src/analytics.ts +33 -0
  60. package/packages/@repo/contracts/src/deployment.ts +1 -1
  61. package/packages/@repo/contracts/src/git.ts +2 -2
  62. package/packages/@repo/contracts/src/ids.ts +2 -1
  63. package/packages/@repo/contracts/src/index.ts +1 -0
  64. package/packages/@repo/contracts/src/project.ts +3 -1
  65. package/packages/@repo/contracts/src/tenant.ts +2 -0
  66. package/packages/@repo/models/dist/docs-config.d.ts +0 -198
  67. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  68. package/packages/@repo/models/dist/docs-config.js +3 -2
  69. package/packages/@repo/models/dist/tenant.d.ts +12 -0
  70. package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
  71. package/packages/@repo/models/package.json +1 -1
  72. package/packages/@repo/models/src/docs-config.ts +3 -3
  73. package/packages/@repo/models/src/tenant.ts +15 -0
  74. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
  75. package/packages/@repo/prebuild/dist/openapi.js +32 -4
  76. package/packages/@repo/prebuild/package.json +1 -1
  77. package/packages/@repo/prebuild/src/openapi.ts +42 -4
  78. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  79. package/packages/@repo/previewing/dist/blob-source.js +18 -5
  80. package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
  81. package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
  82. package/packages/@repo/previewing/dist/config-loader.js +231 -0
  83. package/packages/@repo/previewing/dist/constants.d.ts +15 -0
  84. package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
  85. package/packages/@repo/previewing/dist/constants.js +14 -0
  86. package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
  87. package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
  88. package/packages/@repo/previewing/dist/content-index.js +291 -0
  89. package/packages/@repo/previewing/dist/index.d.ts +13 -121
  90. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  91. package/packages/@repo/previewing/dist/index.js +11 -1003
  92. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
  93. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
  94. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
  95. package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
  96. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
  97. package/packages/@repo/previewing/dist/markdown/format.js +43 -0
  98. package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
  99. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
  100. package/packages/@repo/previewing/dist/markdown/links.js +31 -0
  101. package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
  102. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
  103. package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
  104. package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
  105. package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
  106. package/packages/@repo/previewing/dist/search-index.js +23 -0
  107. package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
  108. package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
  109. package/packages/@repo/previewing/dist/serialization.js +108 -0
  110. package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
  111. package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
  112. package/packages/@repo/previewing/dist/toc-index.js +53 -0
  113. package/packages/@repo/previewing/dist/types.d.ts +92 -0
  114. package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
  115. package/packages/@repo/previewing/dist/types.js +1 -0
  116. package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
  117. package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
  118. package/packages/@repo/previewing/dist/utility-index.js +201 -0
  119. package/packages/@repo/previewing/package.json +1 -1
  120. package/packages/@repo/previewing/src/blob-source.ts +20 -5
  121. package/packages/@repo/previewing/src/config-loader.ts +316 -0
  122. package/packages/@repo/previewing/src/constants.ts +16 -0
  123. package/packages/@repo/previewing/src/content-index.ts +384 -0
  124. package/packages/@repo/previewing/src/index.ts +69 -1515
  125. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
  126. package/packages/@repo/previewing/src/markdown/format.ts +63 -0
  127. package/packages/@repo/previewing/src/markdown/links.ts +46 -0
  128. package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
  129. package/packages/@repo/previewing/src/search-index.ts +38 -0
  130. package/packages/@repo/previewing/src/serialization.ts +191 -0
  131. package/packages/@repo/previewing/src/toc-index.ts +72 -0
  132. package/packages/@repo/previewing/src/types.ts +105 -0
  133. package/packages/@repo/previewing/src/utility-index.ts +304 -0
  134. package/packages/@repo/validation/package.json +1 -1
  135. package/scripts/repo-packages.mjs +8 -0
  136. package/docs/components/ui/site-footer.tsx +0 -41
@@ -1,66 +1,24 @@
1
1
  "use client";
2
2
 
3
3
  import { SearchIcon } from "blode-icons-react";
4
- import { useRouter } from "next/navigation";
5
- import {
6
- startTransition,
7
- useCallback,
8
- useDeferredValue,
9
- useEffect,
10
- useMemo,
11
- useReducer,
12
- useRef,
13
- } from "react";
14
- import type {
15
- ChangeEvent,
16
- KeyboardEvent as ReactKeyboardEvent,
17
- MouseEvent as ReactMouseEvent,
18
- } from "react";
19
-
20
- import {
21
- Dialog,
22
- DialogContent,
23
- DialogDescription,
24
- DialogTitle,
25
- } from "@/components/ui/dialog";
26
- import { isExternalHref, resolveHref, toDocHref } from "@/lib/routes";
27
-
28
- export interface SearchItem {
29
- href?: string;
30
- title: string;
31
- path: string;
32
- }
33
-
34
- interface SearchResponse {
35
- items: SearchItem[];
36
- }
37
-
38
- type SearchStatus = "idle" | "loading" | "ready" | "error";
39
-
40
- interface SearchState {
41
- activeIndex: number;
42
- items: SearchItem[];
43
- open: boolean;
44
- query: string;
45
- status: SearchStatus;
46
- }
47
-
48
- type SearchAction =
49
- | { type: "close" }
50
- | { type: "load-error" }
51
- | { type: "load-start" }
52
- | { items: SearchItem[]; type: "load-success" }
53
- | { type: "open" }
54
- | { index: number; type: "set-active-index" }
55
- | { query: string; type: "set-query" };
56
-
57
- const MAX_RESULTS = 12;
58
- const INITIAL_SEARCH_STATE: SearchState = {
59
- activeIndex: 0,
60
- items: [],
61
- open: false,
62
- query: "",
63
- status: "idle",
4
+ import dynamic from "next/dynamic";
5
+ import { useCallback, useEffect, useState } from "react";
6
+
7
+ const SearchDialog = dynamic(
8
+ async () => {
9
+ const mod = await import("@/components/ui/search-dialog");
10
+ return mod.SearchDialog;
11
+ },
12
+ { loading: () => null, ssr: false }
13
+ );
14
+
15
+ const preloadDialog = async () => {
16
+ // Eagerly fetch the dialog chunk so it's warm by the time the user opens it.
17
+ try {
18
+ await import("@/components/ui/search-dialog");
19
+ } catch {
20
+ // Ignore preload failures; the dialog will retry on open.
21
+ }
64
22
  };
65
23
 
66
24
  const isEditableTarget = (target: EventTarget | null) =>
@@ -69,298 +27,22 @@ const isEditableTarget = (target: EventTarget | null) =>
69
27
  target instanceof HTMLTextAreaElement ||
70
28
  target instanceof HTMLSelectElement;
71
29
 
72
- const searchMatches = (item: SearchItem, query: string) => {
73
- if (!query) {
74
- return true;
75
- }
76
-
77
- const haystack =
78
- `${item.title} ${item.path} ${item.href ?? ""}`.toLowerCase();
79
- return haystack.includes(query);
80
- };
81
-
82
- const getWrappedNextIndex = (current: number, length: number) => {
83
- if (length === 0 || current >= length - 1) {
84
- return 0;
85
- }
86
-
87
- return current + 1;
88
- };
89
-
90
- const getWrappedPrevIndex = (current: number, length: number) => {
91
- if (length === 0 || current <= 0) {
92
- return length - 1;
93
- }
94
-
95
- return current - 1;
96
- };
97
-
98
- const searchReducer = (
99
- state: SearchState,
100
- action: SearchAction
101
- ): SearchState => {
102
- switch (action.type) {
103
- case "close": {
104
- return {
105
- ...state,
106
- activeIndex: 0,
107
- open: false,
108
- query: "",
109
- };
110
- }
111
- case "load-error": {
112
- return {
113
- ...state,
114
- status: "error",
115
- };
116
- }
117
- case "load-start": {
118
- return {
119
- ...state,
120
- status: "loading",
121
- };
122
- }
123
- case "load-success": {
124
- return {
125
- ...state,
126
- items: action.items,
127
- status: "ready",
128
- };
129
- }
130
- case "open": {
131
- return {
132
- ...state,
133
- open: true,
134
- };
135
- }
136
- case "set-active-index": {
137
- return {
138
- ...state,
139
- activeIndex: action.index,
140
- };
141
- }
142
- case "set-query": {
143
- return {
144
- ...state,
145
- activeIndex: 0,
146
- query: action.query,
147
- };
148
- }
149
- default: {
150
- return state;
151
- }
152
- }
153
- };
154
-
155
- const SearchResults = ({
156
- activeIndex,
157
- basePath,
158
- filteredItems,
159
- onResultClick,
160
- onResultMouseEnter,
161
- status,
162
- }: {
163
- activeIndex: number;
164
- basePath: string;
165
- filteredItems: SearchItem[];
166
- onResultClick: (event: ReactMouseEvent<HTMLButtonElement>) => void;
167
- onResultMouseEnter: (event: ReactMouseEvent<HTMLButtonElement>) => void;
168
- status: SearchStatus;
169
- }) => {
170
- if (status === "loading") {
171
- return (
172
- <div className="px-3 py-10 text-center text-sm text-muted-foreground">
173
- Loading search index...
174
- </div>
175
- );
176
- }
177
-
178
- if (status === "error") {
179
- return (
180
- <div className="px-3 py-10 text-center text-sm text-muted-foreground">
181
- Search is temporarily unavailable.
182
- </div>
183
- );
184
- }
185
-
186
- if (status === "ready" && filteredItems.length === 0) {
187
- return (
188
- <div className="px-3 py-10 text-center text-sm text-muted-foreground">
189
- No results found.
190
- </div>
191
- );
192
- }
193
-
194
- if (status !== "ready") {
195
- return null;
196
- }
197
-
198
- return (
199
- <div className="grid gap-1">
200
- {filteredItems.map((item, index) => {
201
- const href = item.href
202
- ? resolveHref(item.href, basePath)
203
- : toDocHref(item.path, basePath);
204
- const isActive = index === activeIndex;
205
-
206
- return (
207
- <button
208
- className={
209
- isActive
210
- ? "grid gap-1 rounded-xl bg-accent px-3 py-2 text-left text-foreground transition-colors"
211
- : "grid gap-1 rounded-xl px-3 py-2 text-left text-muted-foreground transition-colors hover:bg-accent/70 hover:text-foreground"
212
- }
213
- data-index={index}
214
- key={`${item.path}-${item.href ?? "internal"}`}
215
- onClick={onResultClick}
216
- onMouseEnter={onResultMouseEnter}
217
- type="button"
218
- >
219
- <span className="text-sm font-medium text-foreground">
220
- {item.title}
221
- </span>
222
- <span className="text-xs">{href}</span>
223
- </button>
224
- );
225
- })}
226
- </div>
227
- );
228
- };
229
-
230
30
  export const Search = ({ basePath }: { basePath: string }) => {
231
- const router = useRouter();
232
- const inputRef = useRef<HTMLInputElement>(null);
233
- const requestRef = useRef<Promise<void> | null>(null);
234
- const loadedRef = useRef(false);
235
- const [state, dispatch] = useReducer(searchReducer, INITIAL_SEARCH_STATE);
236
- const { activeIndex, items, open, query, status } = state;
237
-
238
- const deferredQuery = useDeferredValue(query.trim().toLowerCase());
239
-
240
- const loadSearchItems = useCallback(() => {
241
- if (loadedRef.current) {
242
- return Promise.resolve();
243
- }
244
-
245
- if (requestRef.current) {
246
- return requestRef.current;
247
- }
248
-
249
- dispatch({ type: "load-start" });
250
- const request = (async () => {
251
- try {
252
- const response = await fetch(toDocHref("search", basePath), {
253
- headers: {
254
- accept: "application/json",
255
- },
256
- });
31
+ const [open, setOpen] = useState(false);
32
+ const [hasMounted, setHasMounted] = useState(false);
257
33
 
258
- if (!response.ok) {
259
- throw new Error(`Failed to load search index: ${response.status}`);
260
- }
261
-
262
- const payload = (await response.json()) as SearchResponse;
263
- const nextItems = Array.isArray(payload.items) ? payload.items : [];
264
- loadedRef.current = true;
265
- startTransition(() => {
266
- dispatch({ items: nextItems, type: "load-success" });
267
- });
268
- } catch {
269
- dispatch({ type: "load-error" });
270
- } finally {
271
- requestRef.current = null;
272
- }
273
- })();
274
-
275
- requestRef.current = request;
276
- return request;
277
- }, [basePath]);
278
-
279
- const filteredItems = useMemo(
280
- () =>
281
- items
282
- .filter((item) => searchMatches(item, deferredQuery))
283
- .slice(0, MAX_RESULTS),
284
- [deferredQuery, items]
285
- );
286
-
287
- const closeSearch = useCallback(() => {
288
- dispatch({ type: "close" });
34
+ const openSearch = useCallback(() => {
35
+ preloadDialog();
36
+ setHasMounted(true);
37
+ setOpen(true);
289
38
  }, []);
290
39
 
291
- const runSelection = useCallback(
292
- (item: SearchItem) => {
293
- closeSearch();
294
- const href = item.href
295
- ? resolveHref(item.href, basePath)
296
- : toDocHref(item.path, basePath);
297
-
298
- if (item.href && isExternalHref(item.href)) {
299
- window.open(href, "_blank", "noopener,noreferrer");
300
- return;
301
- }
302
-
303
- router.push(href);
304
- },
305
- [basePath, closeSearch, router]
306
- );
307
-
308
- const openSearch = useCallback(async () => {
309
- dispatch({ type: "open" });
310
- await loadSearchItems();
311
- }, [loadSearchItems]);
312
-
313
- const handleOpenChange = useCallback(
314
- (nextOpen: boolean) => {
315
- if (!nextOpen) {
316
- closeSearch();
317
- }
318
- },
319
- [closeSearch]
320
- );
321
-
322
- const warmSearch = useCallback(async () => {
323
- try {
324
- await loadSearchItems();
325
- } catch {
326
- // Ignore warm-up failures and let the explicit open path show the error state.
327
- }
328
- }, [loadSearchItems]);
329
-
330
- const handleQueryChange = useCallback(
331
- (event: ChangeEvent<HTMLInputElement>) => {
332
- dispatch({ query: event.target.value, type: "set-query" });
333
- },
334
- []
335
- );
336
-
337
- const handleResultClick = useCallback(
338
- (event: ReactMouseEvent<HTMLButtonElement>) => {
339
- const index = Number(event.currentTarget.dataset.index);
340
- const item = filteredItems[index];
341
- if (!item) {
342
- return;
343
- }
344
-
345
- runSelection(item);
346
- },
347
- [filteredItems, runSelection]
348
- );
349
-
350
- const handleResultMouseEnter = useCallback(
351
- (event: ReactMouseEvent<HTMLButtonElement>) => {
352
- const index = Number(event.currentTarget.dataset.index);
353
- if (Number.isNaN(index)) {
354
- return;
355
- }
356
-
357
- dispatch({ index, type: "set-active-index" });
358
- },
359
- []
360
- );
40
+ const handleOpenChange = useCallback((next: boolean) => {
41
+ setOpen(next);
42
+ }, []);
361
43
 
362
44
  useEffect(() => {
363
- const handleKeydown = async (event: KeyboardEvent) => {
45
+ const handleKeydown = (event: KeyboardEvent) => {
364
46
  if (
365
47
  (event.key === "k" && (event.metaKey || event.ctrlKey)) ||
366
48
  event.key === "/"
@@ -371,50 +53,19 @@ export const Search = ({ basePath }: { basePath: string }) => {
371
53
 
372
54
  event.preventDefault();
373
55
  if (open) {
374
- closeSearch();
56
+ setOpen(false);
375
57
  return;
376
58
  }
377
59
 
378
- await openSearch();
60
+ preloadDialog();
61
+ setHasMounted(true);
62
+ setOpen(true);
379
63
  }
380
64
  };
381
65
 
382
66
  document.addEventListener("keydown", handleKeydown);
383
67
  return () => document.removeEventListener("keydown", handleKeydown);
384
- }, [closeSearch, open, openSearch]);
385
-
386
- const handleDialogKeyDown = useCallback(
387
- (event: ReactKeyboardEvent<HTMLDivElement>) => {
388
- if (event.key === "ArrowDown") {
389
- event.preventDefault();
390
- dispatch({
391
- index: getWrappedNextIndex(activeIndex, filteredItems.length),
392
- type: "set-active-index",
393
- });
394
- return;
395
- }
396
-
397
- if (event.key === "ArrowUp") {
398
- event.preventDefault();
399
- dispatch({
400
- index: getWrappedPrevIndex(activeIndex, filteredItems.length),
401
- type: "set-active-index",
402
- });
403
- return;
404
- }
405
-
406
- if (event.key === "Enter") {
407
- const activeItem = filteredItems[activeIndex];
408
- if (!activeItem) {
409
- return;
410
- }
411
-
412
- event.preventDefault();
413
- runSelection(activeItem);
414
- }
415
- },
416
- [activeIndex, filteredItems, runSelection]
417
- );
68
+ }, [open]);
418
69
 
419
70
  return (
420
71
  <>
@@ -422,8 +73,8 @@ export const Search = ({ basePath }: { basePath: string }) => {
422
73
  aria-label="Search documentation"
423
74
  className="inline-flex size-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground md:hidden"
424
75
  onClick={openSearch}
425
- onFocus={warmSearch}
426
- onMouseEnter={warmSearch}
76
+ onFocus={preloadDialog}
77
+ onMouseEnter={preloadDialog}
427
78
  type="button"
428
79
  >
429
80
  <SearchIcon className="size-4.5" />
@@ -431,54 +82,20 @@ export const Search = ({ basePath }: { basePath: string }) => {
431
82
  <button
432
83
  className="relative hidden h-8 w-full items-center justify-start rounded-lg border border-border bg-muted/50 pl-3 text-sm font-normal text-foreground shadow-none transition-colors hover:bg-muted/80 md:flex md:w-48 lg:w-56 xl:w-64 dark:bg-card"
433
84
  onClick={openSearch}
434
- onFocus={warmSearch}
435
- onMouseEnter={warmSearch}
85
+ onFocus={preloadDialog}
86
+ onMouseEnter={preloadDialog}
436
87
  type="button"
437
88
  >
438
89
  <span className="hidden lg:inline-flex">Search documentation...</span>
439
90
  <span className="inline-flex lg:hidden">Search...</span>
440
91
  </button>
441
- <Dialog onOpenChange={handleOpenChange} open={open}>
442
- <DialogContent
443
- className="max-w-2xl gap-0 overflow-hidden p-0"
444
- onKeyDown={handleDialogKeyDown}
445
- showCloseButton={false}
446
- >
447
- <DialogTitle className="sr-only">Search documentation</DialogTitle>
448
- <DialogDescription className="sr-only">
449
- Search documentation pages and jump directly to a result.
450
- </DialogDescription>
451
- <div className="flex items-center gap-3 border-b border-border px-4 py-3">
452
- <SearchIcon className="size-4 text-muted-foreground" />
453
- <input
454
- aria-label="Search documentation"
455
- className="w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
456
- onChange={handleQueryChange}
457
- placeholder="Search docs..."
458
- ref={inputRef}
459
- type="text"
460
- value={query}
461
- />
462
- <button
463
- className="rounded-md border border-border px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
464
- onClick={closeSearch}
465
- type="button"
466
- >
467
- Esc
468
- </button>
469
- </div>
470
- <div className="max-h-[min(70vh,32rem)] overflow-y-auto p-2">
471
- <SearchResults
472
- activeIndex={activeIndex}
473
- basePath={basePath}
474
- filteredItems={filteredItems}
475
- onResultClick={handleResultClick}
476
- onResultMouseEnter={handleResultMouseEnter}
477
- status={status}
478
- />
479
- </div>
480
- </DialogContent>
481
- </Dialog>
92
+ {hasMounted ? (
93
+ <SearchDialog
94
+ basePath={basePath}
95
+ onOpenChange={handleOpenChange}
96
+ open={open}
97
+ />
98
+ ) : null}
482
99
  </>
483
100
  );
484
101
  };
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ // oxlint-disable require-await -- execute signatures must return Promise for WebMCP
4
+ // oxlint-disable func-style -- exported component uses function-declaration style
5
+ // oxlint-disable no-void -- provideContext returns a promise we intentionally discard
6
+
7
+ import { useEffect } from "react";
8
+
9
+ interface JSONSchemaObject {
10
+ type: string;
11
+ properties?: Record<string, unknown>;
12
+ required?: string[];
13
+ description?: string;
14
+ additionalProperties?: boolean;
15
+ }
16
+
17
+ interface WebMCPTool {
18
+ name: string;
19
+ description: string;
20
+ inputSchema: JSONSchemaObject;
21
+ execute: (args: Record<string, unknown>) => Promise<unknown>;
22
+ }
23
+
24
+ interface NavigatorModelContext {
25
+ provideContext?: (context: { tools: WebMCPTool[] }) => void | Promise<void>;
26
+ }
27
+
28
+ const buildTools = (): WebMCPTool[] => [
29
+ {
30
+ description:
31
+ "Return the shell command that scaffolds a new Blode.md docs site with the given project slug.",
32
+ execute: async ({ slug, template }) => {
33
+ const projectSlug = typeof slug === "string" && slug ? slug : "my-docs";
34
+ const variant =
35
+ template === "starter" || template === "minimal" ? template : "minimal";
36
+ return {
37
+ command: `npx blodemd new docs --slug ${projectSlug} --template ${variant} -y`,
38
+ };
39
+ },
40
+ inputSchema: {
41
+ additionalProperties: false,
42
+ properties: {
43
+ slug: {
44
+ description: "Project slug (lowercase, hyphen-separated).",
45
+ type: "string",
46
+ },
47
+ template: {
48
+ description: "Template to use: 'minimal' or 'starter'.",
49
+ type: "string",
50
+ },
51
+ },
52
+ required: ["slug"],
53
+ type: "object",
54
+ },
55
+ name: "blodemd_scaffold_command",
56
+ },
57
+ {
58
+ description:
59
+ "Return the shell command that deploys a local Blode.md docs directory to the given project.",
60
+ execute: async ({ directory, project }) => {
61
+ const dir =
62
+ typeof directory === "string" && directory ? directory : "docs";
63
+ const projectSlug =
64
+ typeof project === "string" && project ? project : "my-docs";
65
+ return {
66
+ command: `npx blodemd push ${dir} --project ${projectSlug}`,
67
+ };
68
+ },
69
+ inputSchema: {
70
+ additionalProperties: false,
71
+ properties: {
72
+ directory: {
73
+ description: "Local directory containing the docs.",
74
+ type: "string",
75
+ },
76
+ project: {
77
+ description: "Target Blode.md project slug.",
78
+ type: "string",
79
+ },
80
+ },
81
+ required: ["project"],
82
+ type: "object",
83
+ },
84
+ name: "blodemd_deploy_command",
85
+ },
86
+ {
87
+ description: "Open the Blode.md dashboard in the current tab.",
88
+ execute: async () => {
89
+ if (typeof window !== "undefined") {
90
+ window.location.assign("/app");
91
+ }
92
+ return { ok: true };
93
+ },
94
+ inputSchema: {
95
+ additionalProperties: false,
96
+ properties: {},
97
+ type: "object",
98
+ },
99
+ name: "blodemd_open_dashboard",
100
+ },
101
+ ];
102
+
103
+ export function WebMcpTools() {
104
+ useEffect(() => {
105
+ const nav = navigator as Navigator & {
106
+ modelContext?: NavigatorModelContext;
107
+ };
108
+ if (!nav.modelContext?.provideContext) {
109
+ return;
110
+ }
111
+ void nav.modelContext.provideContext({ tools: buildTools() });
112
+ }, []);
113
+
114
+ return null;
115
+ }
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const BLODEMD_SKILL_CONTENT = `---
4
+ name: blodemd
5
+ description: Scaffold, preview, and deploy beautiful MDX documentation sites with Blode.md. Use when the user wants to create a new docs site, validate their docs config, preview locally, or deploy to Blode.md. Triggers include "create docs", "deploy docs", "push docs", "preview docs", "scaffold a docs site", "set up documentation", "validate docs.json", or any task involving MDX documentation deployment.
6
+ user-invocable: true
7
+ argument-hint: <command> [options]
8
+ allowed-tools: Bash(npx blodemd *), Bash(blodemd *)
9
+ ---
10
+
11
+ # Blode.md
12
+
13
+ Scaffold, preview, and deploy MDX documentation sites from the terminal. Write locally, ship with one command. Sign in once with GitHub in your browser, no API keys ever.
14
+
15
+ ## Commands
16
+
17
+ - \`npx blodemd login\` - Browser sign-in with GitHub
18
+ - \`npx blodemd whoami\` - Show current authentication status
19
+ - \`npx blodemd new [directory] --slug <project-slug> --template <minimal|starter> -y\` - Scaffold a new docs site
20
+ - \`npx blodemd validate [dir]\` - Validate docs.json against the schema
21
+ - \`npx blodemd dev --dir <dir> --port 3030\` - Preview locally with hot reload
22
+ - \`npx blodemd push [dir] --project <slug>\` - Deploy a docs site
23
+
24
+ ## Workflow
25
+
26
+ 1. Verify auth with \`npx blodemd whoami\`
27
+ 2. Scaffold or locate the docs directory
28
+ 3. Validate the configuration
29
+ 4. Preview locally
30
+ 5. Deploy to Blode.md
31
+
32
+ ## docs.json
33
+
34
+ The \`docs.json\` file is the config for a Blode.md site. It must include a \`slug\` field:
35
+
36
+ \`\`\`json
37
+ {
38
+ "slug": "my-project",
39
+ "name": "My Project"
40
+ }
41
+ \`\`\`
42
+ `;
43
+
44
+ export const BLODEMD_SKILL_DESCRIPTION =
45
+ "Scaffold, preview, and deploy beautiful MDX documentation sites with Blode.md from the CLI.";
46
+
47
+ export const sha256Hex = (input: string) =>
48
+ createHash("sha256").update(input, "utf8").digest("hex");
@@ -1,7 +1,6 @@
1
+ // Kept for server-side consumers in apps/docs that need platform-wide metadata
2
+ // (e.g. MCP server card). The richer marketing config lives in apps/web.
3
+
1
4
  export const siteConfig = {
2
- links: {
3
- author: "https://matthewblode.com",
4
- github: "https://github.com/mblode/blodemd",
5
- },
6
5
  version: "0.0.9",
7
- };
6
+ } as const;