blodemd 0.0.13 → 0.0.14

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 +1193 -978
  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
@@ -0,0 +1,431 @@
1
+ "use client";
2
+
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
+ query: string;
44
+ status: SearchStatus;
45
+ }
46
+
47
+ type SearchAction =
48
+ | { type: "load-error" }
49
+ | { type: "load-start" }
50
+ | { items: SearchItem[]; type: "load-success" }
51
+ | { type: "reset-query" }
52
+ | { index: number; type: "set-active-index" }
53
+ | { query: string; type: "set-query" };
54
+
55
+ const MAX_RESULTS = 12;
56
+ const INITIAL_SEARCH_STATE: SearchState = {
57
+ activeIndex: 0,
58
+ items: [],
59
+ query: "",
60
+ status: "idle",
61
+ };
62
+
63
+ const searchMatches = (item: SearchItem, query: string) => {
64
+ if (!query) {
65
+ return true;
66
+ }
67
+
68
+ const haystack =
69
+ `${item.title} ${item.path} ${item.href ?? ""}`.toLowerCase();
70
+ return haystack.includes(query);
71
+ };
72
+
73
+ const getWrappedNextIndex = (current: number, length: number) => {
74
+ if (length === 0 || current >= length - 1) {
75
+ return 0;
76
+ }
77
+
78
+ return current + 1;
79
+ };
80
+
81
+ const getWrappedPrevIndex = (current: number, length: number) => {
82
+ if (length === 0 || current <= 0) {
83
+ return length - 1;
84
+ }
85
+
86
+ return current - 1;
87
+ };
88
+
89
+ const searchReducer = (
90
+ state: SearchState,
91
+ action: SearchAction
92
+ ): SearchState => {
93
+ switch (action.type) {
94
+ case "load-error": {
95
+ return {
96
+ ...state,
97
+ status: "error",
98
+ };
99
+ }
100
+ case "load-start": {
101
+ return {
102
+ ...state,
103
+ status: "loading",
104
+ };
105
+ }
106
+ case "load-success": {
107
+ return {
108
+ ...state,
109
+ items: action.items,
110
+ status: "ready",
111
+ };
112
+ }
113
+ case "reset-query": {
114
+ return {
115
+ ...state,
116
+ activeIndex: 0,
117
+ query: "",
118
+ };
119
+ }
120
+ case "set-active-index": {
121
+ return {
122
+ ...state,
123
+ activeIndex: action.index,
124
+ };
125
+ }
126
+ case "set-query": {
127
+ return {
128
+ ...state,
129
+ activeIndex: 0,
130
+ query: action.query,
131
+ };
132
+ }
133
+ default: {
134
+ return state;
135
+ }
136
+ }
137
+ };
138
+
139
+ const SearchResults = ({
140
+ activeIndex,
141
+ basePath,
142
+ filteredItems,
143
+ onResultClick,
144
+ onResultMouseEnter,
145
+ status,
146
+ }: {
147
+ activeIndex: number;
148
+ basePath: string;
149
+ filteredItems: SearchItem[];
150
+ onResultClick: (event: ReactMouseEvent<HTMLButtonElement>) => void;
151
+ onResultMouseEnter: (event: ReactMouseEvent<HTMLButtonElement>) => void;
152
+ status: SearchStatus;
153
+ }) => {
154
+ if (status === "loading") {
155
+ return (
156
+ <div className="px-3 py-10 text-center text-sm text-muted-foreground">
157
+ Loading search index...
158
+ </div>
159
+ );
160
+ }
161
+
162
+ if (status === "error") {
163
+ return (
164
+ <div className="px-3 py-10 text-center text-sm text-muted-foreground">
165
+ Search is temporarily unavailable.
166
+ </div>
167
+ );
168
+ }
169
+
170
+ if (status === "ready" && filteredItems.length === 0) {
171
+ return (
172
+ <div className="px-3 py-10 text-center text-sm text-muted-foreground">
173
+ No results found.
174
+ </div>
175
+ );
176
+ }
177
+
178
+ if (status !== "ready") {
179
+ return null;
180
+ }
181
+
182
+ return (
183
+ <div className="grid gap-1">
184
+ {filteredItems.map((item, index) => {
185
+ const href = item.href
186
+ ? resolveHref(item.href, basePath)
187
+ : toDocHref(item.path, basePath);
188
+ const isActive = index === activeIndex;
189
+
190
+ return (
191
+ <button
192
+ className={
193
+ isActive
194
+ ? "grid gap-1 rounded-xl bg-accent px-3 py-2 text-left text-foreground transition-colors"
195
+ : "grid gap-1 rounded-xl px-3 py-2 text-left text-muted-foreground transition-colors hover:bg-accent/70 hover:text-foreground"
196
+ }
197
+ data-index={index}
198
+ key={`${item.path}-${item.href ?? "internal"}`}
199
+ onClick={onResultClick}
200
+ onMouseEnter={onResultMouseEnter}
201
+ type="button"
202
+ >
203
+ <span className="text-sm font-medium text-foreground">
204
+ {item.title}
205
+ </span>
206
+ <span className="text-xs">{href}</span>
207
+ </button>
208
+ );
209
+ })}
210
+ </div>
211
+ );
212
+ };
213
+
214
+ export const SearchDialog = ({
215
+ basePath,
216
+ open,
217
+ onOpenChange,
218
+ }: {
219
+ basePath: string;
220
+ open: boolean;
221
+ onOpenChange: (open: boolean) => void;
222
+ }) => {
223
+ const router = useRouter();
224
+ const inputRef = useRef<HTMLInputElement>(null);
225
+ const requestRef = useRef<Promise<void> | null>(null);
226
+ const loadedRef = useRef(false);
227
+ const [state, dispatch] = useReducer(searchReducer, INITIAL_SEARCH_STATE);
228
+ const { activeIndex, items, query, status } = state;
229
+
230
+ const deferredQuery = useDeferredValue(query.trim().toLowerCase());
231
+
232
+ const loadSearchItems = useCallback(() => {
233
+ if (loadedRef.current) {
234
+ return Promise.resolve();
235
+ }
236
+
237
+ if (requestRef.current) {
238
+ return requestRef.current;
239
+ }
240
+
241
+ dispatch({ type: "load-start" });
242
+ const request = (async () => {
243
+ try {
244
+ const response = await fetch(toDocHref("search", basePath), {
245
+ headers: {
246
+ accept: "application/json",
247
+ },
248
+ });
249
+
250
+ if (!response.ok) {
251
+ throw new Error(`Failed to load search index: ${response.status}`);
252
+ }
253
+
254
+ const payload = (await response.json()) as SearchResponse;
255
+ const nextItems = Array.isArray(payload.items) ? payload.items : [];
256
+ loadedRef.current = true;
257
+ startTransition(() => {
258
+ dispatch({ items: nextItems, type: "load-success" });
259
+ });
260
+ } catch {
261
+ dispatch({ type: "load-error" });
262
+ } finally {
263
+ requestRef.current = null;
264
+ }
265
+ })();
266
+
267
+ requestRef.current = request;
268
+ return request;
269
+ }, [basePath]);
270
+
271
+ // Load the search index as soon as the dialog mounts.
272
+ useEffect(() => {
273
+ const run = async () => {
274
+ try {
275
+ await loadSearchItems();
276
+ } catch {
277
+ // Errors are surfaced via the reducer's "load-error" action.
278
+ }
279
+ };
280
+
281
+ run();
282
+ }, [loadSearchItems]);
283
+
284
+ const filteredItems = useMemo(
285
+ () =>
286
+ items
287
+ .filter((item) => searchMatches(item, deferredQuery))
288
+ .slice(0, MAX_RESULTS),
289
+ [deferredQuery, items]
290
+ );
291
+
292
+ const closeSearch = useCallback(() => {
293
+ dispatch({ type: "reset-query" });
294
+ onOpenChange(false);
295
+ }, [onOpenChange]);
296
+
297
+ const runSelection = useCallback(
298
+ (item: SearchItem) => {
299
+ closeSearch();
300
+ const href = item.href
301
+ ? resolveHref(item.href, basePath)
302
+ : toDocHref(item.path, basePath);
303
+
304
+ if (item.href && isExternalHref(item.href)) {
305
+ window.open(href, "_blank", "noopener,noreferrer");
306
+ return;
307
+ }
308
+
309
+ router.push(href);
310
+ },
311
+ [basePath, closeSearch, router]
312
+ );
313
+
314
+ const handleOpenChange = useCallback(
315
+ (nextOpen: boolean) => {
316
+ if (!nextOpen) {
317
+ closeSearch();
318
+ }
319
+ },
320
+ [closeSearch]
321
+ );
322
+
323
+ const handleQueryChange = useCallback(
324
+ (event: ChangeEvent<HTMLInputElement>) => {
325
+ dispatch({ query: event.target.value, type: "set-query" });
326
+ },
327
+ []
328
+ );
329
+
330
+ const handleResultClick = useCallback(
331
+ (event: ReactMouseEvent<HTMLButtonElement>) => {
332
+ const index = Number(event.currentTarget.dataset.index);
333
+ const item = filteredItems[index];
334
+ if (!item) {
335
+ return;
336
+ }
337
+
338
+ runSelection(item);
339
+ },
340
+ [filteredItems, runSelection]
341
+ );
342
+
343
+ const handleResultMouseEnter = useCallback(
344
+ (event: ReactMouseEvent<HTMLButtonElement>) => {
345
+ const index = Number(event.currentTarget.dataset.index);
346
+ if (Number.isNaN(index)) {
347
+ return;
348
+ }
349
+
350
+ dispatch({ index, type: "set-active-index" });
351
+ },
352
+ []
353
+ );
354
+
355
+ const handleDialogKeyDown = useCallback(
356
+ (event: ReactKeyboardEvent<HTMLDivElement>) => {
357
+ if (event.key === "ArrowDown") {
358
+ event.preventDefault();
359
+ dispatch({
360
+ index: getWrappedNextIndex(activeIndex, filteredItems.length),
361
+ type: "set-active-index",
362
+ });
363
+ return;
364
+ }
365
+
366
+ if (event.key === "ArrowUp") {
367
+ event.preventDefault();
368
+ dispatch({
369
+ index: getWrappedPrevIndex(activeIndex, filteredItems.length),
370
+ type: "set-active-index",
371
+ });
372
+ return;
373
+ }
374
+
375
+ if (event.key === "Enter") {
376
+ const activeItem = filteredItems[activeIndex];
377
+ if (!activeItem) {
378
+ return;
379
+ }
380
+
381
+ event.preventDefault();
382
+ runSelection(activeItem);
383
+ }
384
+ },
385
+ [activeIndex, filteredItems, runSelection]
386
+ );
387
+
388
+ return (
389
+ <Dialog onOpenChange={handleOpenChange} open={open}>
390
+ <DialogContent
391
+ className="max-w-2xl gap-0 overflow-hidden p-0"
392
+ onKeyDown={handleDialogKeyDown}
393
+ showCloseButton={false}
394
+ >
395
+ <DialogTitle className="sr-only">Search documentation</DialogTitle>
396
+ <DialogDescription className="sr-only">
397
+ Search documentation pages and jump directly to a result.
398
+ </DialogDescription>
399
+ <div className="flex items-center gap-3 border-b border-border px-4 py-3">
400
+ <SearchIcon className="size-4 text-muted-foreground" />
401
+ <input
402
+ aria-label="Search documentation"
403
+ className="w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
404
+ onChange={handleQueryChange}
405
+ placeholder="Search docs..."
406
+ ref={inputRef}
407
+ type="text"
408
+ value={query}
409
+ />
410
+ <button
411
+ className="rounded-md border border-border px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
412
+ onClick={closeSearch}
413
+ type="button"
414
+ >
415
+ Esc
416
+ </button>
417
+ </div>
418
+ <div className="max-h-[min(70vh,32rem)] overflow-y-auto p-2">
419
+ <SearchResults
420
+ activeIndex={activeIndex}
421
+ basePath={basePath}
422
+ filteredItems={filteredItems}
423
+ onResultClick={handleResultClick}
424
+ onResultMouseEnter={handleResultMouseEnter}
425
+ status={status}
426
+ />
427
+ </div>
428
+ </DialogContent>
429
+ </Dialog>
430
+ );
431
+ };