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.
- package/README.md +1 -1
- package/dev-server/lib/local-runtime.tsx +12 -15
- package/dev-server/package.json +5 -1
- package/dist/cli.mjs +1211 -983
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/api/api-playground.tsx +2 -5
- package/docs/components/api/api-reference.tsx +29 -3
- package/docs/components/docs/contextual-menu.tsx +37 -33
- package/docs/components/docs/copy-page-menu.tsx +18 -0
- package/docs/components/docs/doc-header.tsx +4 -1
- package/docs/components/docs/doc-shell.tsx +48 -18
- package/docs/components/docs/doc-sidebar.tsx +131 -89
- package/docs/components/docs/doc-toc.tsx +1 -0
- package/docs/components/docs/mobile-nav.tsx +72 -68
- package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
- package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
- package/docs/components/git/repo-picker.tsx +5 -2
- package/docs/components/mdx/index.tsx +31 -21
- package/docs/components/posthog-provider.tsx +42 -0
- package/docs/components/tenant-analytics.tsx +49 -0
- package/docs/components/third-parties.tsx +4 -1
- package/docs/components/ui/copy-button.tsx +16 -2
- package/docs/components/ui/search-dialog.tsx +431 -0
- package/docs/components/ui/search.tsx +44 -427
- package/docs/components/web-mcp.tsx +115 -0
- package/docs/lib/agent-skills.ts +48 -0
- package/docs/lib/config.ts +4 -5
- package/docs/lib/docs-collection.ts +7 -22
- package/docs/lib/docs-runtime.tsx +160 -10
- package/docs/lib/llms-full.ts +82 -0
- package/docs/lib/marketing-markdown.ts +79 -0
- package/docs/lib/marketing-site.ts +31 -0
- package/docs/lib/mdx.ts +17 -6
- package/docs/lib/routes.ts +19 -3
- package/docs/lib/shiki.ts +4 -0
- package/docs/lib/tenancy.ts +15 -2
- package/docs/lib/tenant-headers.ts +29 -0
- package/docs/lib/tenant-static.ts +73 -99
- package/package.json +8 -5
- package/packages/@repo/common/package.json +1 -1
- package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
- package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/analytics.js +18 -0
- package/packages/@repo/contracts/dist/deployment.js +1 -1
- package/packages/@repo/contracts/dist/git.js +2 -2
- package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/ids.js +2 -1
- package/packages/@repo/contracts/dist/index.d.ts +1 -0
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -0
- package/packages/@repo/contracts/dist/project.d.ts +18 -1
- package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/project.js +3 -1
- package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
- package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/tenant.js +2 -0
- package/packages/@repo/contracts/package.json +1 -1
- package/packages/@repo/contracts/src/analytics.ts +33 -0
- package/packages/@repo/contracts/src/deployment.ts +1 -1
- package/packages/@repo/contracts/src/git.ts +2 -2
- package/packages/@repo/contracts/src/ids.ts +2 -1
- package/packages/@repo/contracts/src/index.ts +1 -0
- package/packages/@repo/contracts/src/project.ts +3 -1
- package/packages/@repo/contracts/src/tenant.ts +2 -0
- package/packages/@repo/models/dist/docs-config.d.ts +0 -198
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -2
- package/packages/@repo/models/dist/tenant.d.ts +12 -0
- package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/models/package.json +1 -1
- package/packages/@repo/models/src/docs-config.ts +3 -3
- package/packages/@repo/models/src/tenant.ts +15 -0
- package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
- package/packages/@repo/prebuild/dist/openapi.js +32 -4
- package/packages/@repo/prebuild/package.json +1 -1
- package/packages/@repo/prebuild/src/openapi.ts +42 -4
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +18 -5
- package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
- package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/config-loader.js +231 -0
- package/packages/@repo/previewing/dist/constants.d.ts +15 -0
- package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/constants.js +14 -0
- package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/content-index.js +291 -0
- package/packages/@repo/previewing/dist/index.d.ts +13 -121
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +11 -1003
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/format.js +43 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/links.js +31 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
- package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
- package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/search-index.js +23 -0
- package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
- package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/serialization.js +108 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/toc-index.js +53 -0
- package/packages/@repo/previewing/dist/types.d.ts +92 -0
- package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/types.js +1 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/utility-index.js +201 -0
- package/packages/@repo/previewing/package.json +1 -1
- package/packages/@repo/previewing/src/blob-source.ts +20 -5
- package/packages/@repo/previewing/src/config-loader.ts +316 -0
- package/packages/@repo/previewing/src/constants.ts +16 -0
- package/packages/@repo/previewing/src/content-index.ts +384 -0
- package/packages/@repo/previewing/src/index.ts +69 -1515
- package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
- package/packages/@repo/previewing/src/markdown/format.ts +63 -0
- package/packages/@repo/previewing/src/markdown/links.ts +46 -0
- package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
- package/packages/@repo/previewing/src/search-index.ts +38 -0
- package/packages/@repo/previewing/src/serialization.ts +191 -0
- package/packages/@repo/previewing/src/toc-index.ts +72 -0
- package/packages/@repo/previewing/src/types.ts +105 -0
- package/packages/@repo/previewing/src/utility-index.ts +304 -0
- package/packages/@repo/validation/package.json +1 -1
- package/scripts/repo-packages.mjs +8 -0
- 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
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
232
|
-
const
|
|
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
|
-
|
|
259
|
-
|
|
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
|
|
292
|
-
(
|
|
293
|
-
|
|
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 =
|
|
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
|
-
|
|
56
|
+
setOpen(false);
|
|
375
57
|
return;
|
|
376
58
|
}
|
|
377
59
|
|
|
378
|
-
|
|
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
|
-
}, [
|
|
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={
|
|
426
|
-
onMouseEnter={
|
|
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={
|
|
435
|
-
onMouseEnter={
|
|
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
|
-
|
|
442
|
-
<
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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");
|
package/docs/lib/config.ts
CHANGED
|
@@ -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;
|