blodemd 0.0.8 → 0.0.10
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 +25 -9
- package/dev-server/app/[[...slug]]/page.tsx +1 -0
- package/dev-server/app/favicon.ico +0 -0
- package/dev-server/next.config.js +11 -13
- package/dev-server/package.json +1 -1
- package/dev-server/tsconfig.json +3 -0
- package/dist/cli.mjs +869 -184
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/animate-ui/primitives/buttons/button.tsx +14 -0
- package/docs/components/api/api-playground.tsx +255 -80
- package/docs/components/api/api-reference.tsx +11 -1
- package/docs/components/docs/contextual-menu.tsx +227 -142
- package/docs/components/docs/copy-page-menu.tsx +148 -85
- package/docs/components/docs/doc-header.tsx +13 -3
- package/docs/components/docs/doc-shell.tsx +25 -14
- package/docs/components/docs/mobile-nav.tsx +0 -6
- package/docs/components/mdx/code-group.tsx +171 -62
- package/docs/components/mdx/steps.tsx +1 -1
- package/docs/components/mdx/tabs.tsx +131 -26
- package/docs/components/ui/copy-button.tsx +122 -0
- package/docs/components/ui/input.tsx +0 -1
- package/docs/components/ui/search.tsx +241 -132
- package/docs/components/ui/site-footer.tsx +39 -0
- package/docs/lib/config.ts +7 -0
- package/docs/lib/content-root.ts +33 -0
- package/docs/lib/content-source.ts +70 -0
- package/docs/lib/contextual-options.ts +20 -0
- package/docs/lib/docs-runtime.tsx +595 -0
- package/docs/lib/edge-config.ts +95 -0
- package/docs/lib/env.ts +22 -0
- package/docs/lib/openapi-proxy.ts +88 -0
- package/docs/lib/platform-config.ts +6 -0
- package/docs/lib/routes.ts +39 -0
- package/docs/lib/supabase.ts +13 -0
- package/docs/lib/tenancy.ts +350 -0
- package/docs/lib/tenant-headers.ts +14 -0
- package/docs/lib/tenant-static.ts +529 -0
- package/docs/lib/tenant-utility-context.ts +62 -0
- package/docs/lib/tenants.ts +68 -0
- package/docs/lib/use-mobile.ts +19 -0
- package/package.json +3 -2
- package/packages/@repo/common/dist/index.d.ts +7 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -1
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/src/index.ts +50 -0
- package/packages/@repo/contracts/dist/project.d.ts +1 -1
- package/packages/@repo/contracts/dist/project.js +1 -1
- package/packages/@repo/contracts/src/project.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +194 -29
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -28
- package/packages/@repo/models/src/docs-config.ts +5 -31
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +7 -2
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/fs-source.js +2 -3
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +20 -50
- package/packages/@repo/previewing/src/blob-source.ts +7 -4
- package/packages/@repo/previewing/src/fs-source.ts +2 -3
- package/packages/@repo/previewing/src/index.ts +29 -64
- package/packages/@repo/validation/dist/index.d.ts +2 -2
- package/packages/@repo/validation/dist/index.d.ts.map +1 -1
- package/packages/@repo/validation/dist/index.js +2 -2
- package/packages/@repo/validation/package.json +1 -0
- package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
- package/packages/@repo/validation/src/index.ts +4 -4
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
useDeferredValue,
|
|
9
9
|
useEffect,
|
|
10
10
|
useMemo,
|
|
11
|
+
useReducer,
|
|
11
12
|
useRef,
|
|
12
|
-
useState,
|
|
13
13
|
} from "react";
|
|
14
14
|
import type {
|
|
15
15
|
ChangeEvent,
|
|
@@ -17,6 +17,12 @@ import type {
|
|
|
17
17
|
MouseEvent as ReactMouseEvent,
|
|
18
18
|
} from "react";
|
|
19
19
|
|
|
20
|
+
import {
|
|
21
|
+
Dialog,
|
|
22
|
+
DialogContent,
|
|
23
|
+
DialogDescription,
|
|
24
|
+
DialogTitle,
|
|
25
|
+
} from "@/components/ui/dialog";
|
|
20
26
|
import { isExternalHref, resolveHref, toDocHref } from "@/lib/routes";
|
|
21
27
|
|
|
22
28
|
export interface SearchItem {
|
|
@@ -29,7 +35,33 @@ interface SearchResponse {
|
|
|
29
35
|
items: SearchItem[];
|
|
30
36
|
}
|
|
31
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
|
+
|
|
32
57
|
const MAX_RESULTS = 12;
|
|
58
|
+
const INITIAL_SEARCH_STATE: SearchState = {
|
|
59
|
+
activeIndex: 0,
|
|
60
|
+
items: [],
|
|
61
|
+
open: false,
|
|
62
|
+
query: "",
|
|
63
|
+
status: "idle",
|
|
64
|
+
};
|
|
33
65
|
|
|
34
66
|
const isEditableTarget = (target: EventTarget | null) =>
|
|
35
67
|
(target instanceof HTMLElement && target.isContentEditable) ||
|
|
@@ -63,18 +95,145 @@ const getWrappedPrevIndex = (current: number, length: number) => {
|
|
|
63
95
|
return current - 1;
|
|
64
96
|
};
|
|
65
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
|
+
|
|
66
230
|
export const Search = ({ basePath }: { basePath: string }) => {
|
|
67
231
|
const router = useRouter();
|
|
68
232
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
69
233
|
const requestRef = useRef<Promise<void> | null>(null);
|
|
70
234
|
const loadedRef = useRef(false);
|
|
71
|
-
const [
|
|
72
|
-
const
|
|
73
|
-
const [open, setOpen] = useState(false);
|
|
74
|
-
const [query, setQuery] = useState("");
|
|
75
|
-
const [status, setStatus] = useState<"idle" | "loading" | "ready" | "error">(
|
|
76
|
-
"idle"
|
|
77
|
-
);
|
|
235
|
+
const [state, dispatch] = useReducer(searchReducer, INITIAL_SEARCH_STATE);
|
|
236
|
+
const { activeIndex, items, open, query, status } = state;
|
|
78
237
|
|
|
79
238
|
const deferredQuery = useDeferredValue(query.trim().toLowerCase());
|
|
80
239
|
|
|
@@ -82,11 +241,12 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
82
241
|
if (loadedRef.current) {
|
|
83
242
|
return Promise.resolve();
|
|
84
243
|
}
|
|
244
|
+
|
|
85
245
|
if (requestRef.current) {
|
|
86
246
|
return requestRef.current;
|
|
87
247
|
}
|
|
88
248
|
|
|
89
|
-
|
|
249
|
+
dispatch({ type: "load-start" });
|
|
90
250
|
const request = (async () => {
|
|
91
251
|
try {
|
|
92
252
|
const response = await fetch(toDocHref("search", basePath), {
|
|
@@ -103,11 +263,10 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
103
263
|
const nextItems = Array.isArray(payload.items) ? payload.items : [];
|
|
104
264
|
loadedRef.current = true;
|
|
105
265
|
startTransition(() => {
|
|
106
|
-
|
|
107
|
-
setStatus("ready");
|
|
266
|
+
dispatch({ items: nextItems, type: "load-success" });
|
|
108
267
|
});
|
|
109
268
|
} catch {
|
|
110
|
-
|
|
269
|
+
dispatch({ type: "load-error" });
|
|
111
270
|
} finally {
|
|
112
271
|
requestRef.current = null;
|
|
113
272
|
}
|
|
@@ -126,9 +285,7 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
126
285
|
);
|
|
127
286
|
|
|
128
287
|
const closeSearch = useCallback(() => {
|
|
129
|
-
|
|
130
|
-
setQuery("");
|
|
131
|
-
setActiveIndex(0);
|
|
288
|
+
dispatch({ type: "close" });
|
|
132
289
|
}, []);
|
|
133
290
|
|
|
134
291
|
const runSelection = useCallback(
|
|
@@ -137,20 +294,36 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
137
294
|
const href = item.href
|
|
138
295
|
? resolveHref(item.href, basePath)
|
|
139
296
|
: toDocHref(item.path, basePath);
|
|
297
|
+
|
|
140
298
|
if (item.href && isExternalHref(item.href)) {
|
|
141
299
|
window.open(href, "_blank", "noopener,noreferrer");
|
|
142
300
|
return;
|
|
143
301
|
}
|
|
302
|
+
|
|
144
303
|
router.push(href);
|
|
145
304
|
},
|
|
146
305
|
[basePath, closeSearch, router]
|
|
147
306
|
);
|
|
148
307
|
|
|
149
308
|
const openSearch = useCallback(async () => {
|
|
150
|
-
|
|
309
|
+
dispatch({ type: "open" });
|
|
151
310
|
await loadSearchItems();
|
|
152
311
|
}, [loadSearchItems]);
|
|
153
312
|
|
|
313
|
+
const handleOpenChange = useCallback(
|
|
314
|
+
(nextOpen: boolean) => {
|
|
315
|
+
if (!nextOpen) {
|
|
316
|
+
closeSearch();
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
[closeSearch]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const handleOpenAutoFocus = useCallback((event: Event) => {
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
inputRef.current?.focus();
|
|
325
|
+
}, []);
|
|
326
|
+
|
|
154
327
|
const warmSearch = useCallback(async () => {
|
|
155
328
|
try {
|
|
156
329
|
await loadSearchItems();
|
|
@@ -161,7 +334,7 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
161
334
|
|
|
162
335
|
const handleQueryChange = useCallback(
|
|
163
336
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
164
|
-
|
|
337
|
+
dispatch({ query: event.target.value, type: "set-query" });
|
|
165
338
|
},
|
|
166
339
|
[]
|
|
167
340
|
);
|
|
@@ -186,29 +359,11 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
186
359
|
return;
|
|
187
360
|
}
|
|
188
361
|
|
|
189
|
-
|
|
362
|
+
dispatch({ index, type: "set-active-index" });
|
|
190
363
|
},
|
|
191
364
|
[]
|
|
192
365
|
);
|
|
193
366
|
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
if (!open) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
inputRef.current?.focus();
|
|
200
|
-
const previousOverflow = document.body.style.overflow;
|
|
201
|
-
document.body.style.overflow = "hidden";
|
|
202
|
-
|
|
203
|
-
return () => {
|
|
204
|
-
document.body.style.overflow = previousOverflow;
|
|
205
|
-
};
|
|
206
|
-
}, [open]);
|
|
207
|
-
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
setActiveIndex(0);
|
|
210
|
-
}, [deferredQuery, open]);
|
|
211
|
-
|
|
212
367
|
useEffect(() => {
|
|
213
368
|
const handleKeydown = async (event: KeyboardEvent) => {
|
|
214
369
|
if (
|
|
@@ -224,16 +379,9 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
224
379
|
closeSearch();
|
|
225
380
|
return;
|
|
226
381
|
}
|
|
227
|
-
await openSearch();
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
382
|
|
|
231
|
-
|
|
232
|
-
return;
|
|
383
|
+
await openSearch();
|
|
233
384
|
}
|
|
234
|
-
|
|
235
|
-
event.preventDefault();
|
|
236
|
-
closeSearch();
|
|
237
385
|
};
|
|
238
386
|
|
|
239
387
|
document.addEventListener("keydown", handleKeydown);
|
|
@@ -244,17 +392,19 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
244
392
|
(event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
245
393
|
if (event.key === "ArrowDown") {
|
|
246
394
|
event.preventDefault();
|
|
247
|
-
|
|
248
|
-
getWrappedNextIndex(
|
|
249
|
-
|
|
395
|
+
dispatch({
|
|
396
|
+
index: getWrappedNextIndex(activeIndex, filteredItems.length),
|
|
397
|
+
type: "set-active-index",
|
|
398
|
+
});
|
|
250
399
|
return;
|
|
251
400
|
}
|
|
252
401
|
|
|
253
402
|
if (event.key === "ArrowUp") {
|
|
254
403
|
event.preventDefault();
|
|
255
|
-
|
|
256
|
-
getWrappedPrevIndex(
|
|
257
|
-
|
|
404
|
+
dispatch({
|
|
405
|
+
index: getWrappedPrevIndex(activeIndex, filteredItems.length),
|
|
406
|
+
type: "set-active-index",
|
|
407
|
+
});
|
|
258
408
|
return;
|
|
259
409
|
}
|
|
260
410
|
|
|
@@ -296,89 +446,48 @@ export const Search = ({ basePath }: { basePath: string }) => {
|
|
|
296
446
|
Cmd K
|
|
297
447
|
</span>
|
|
298
448
|
</button>
|
|
299
|
-
{open
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
{status === "error" ? (
|
|
339
|
-
<div className="px-3 py-10 text-center text-sm text-muted-foreground">
|
|
340
|
-
Search is temporarily unavailable.
|
|
341
|
-
</div>
|
|
342
|
-
) : null}
|
|
343
|
-
{status === "ready" && filteredItems.length === 0 ? (
|
|
344
|
-
<div className="px-3 py-10 text-center text-sm text-muted-foreground">
|
|
345
|
-
No results found.
|
|
346
|
-
</div>
|
|
347
|
-
) : null}
|
|
348
|
-
{status === "ready" && filteredItems.length > 0 ? (
|
|
349
|
-
<div className="grid gap-1">
|
|
350
|
-
{filteredItems.map((item, index) => {
|
|
351
|
-
const isActive = index === activeIndex;
|
|
352
|
-
const href = item.href
|
|
353
|
-
? resolveHref(item.href, basePath)
|
|
354
|
-
: toDocHref(item.path, basePath);
|
|
355
|
-
|
|
356
|
-
return (
|
|
357
|
-
<button
|
|
358
|
-
className={`grid gap-1 rounded-xl px-3 py-2 text-left transition-colors ${
|
|
359
|
-
isActive
|
|
360
|
-
? "bg-accent text-foreground"
|
|
361
|
-
: "text-muted-foreground hover:bg-accent/70 hover:text-foreground"
|
|
362
|
-
}`}
|
|
363
|
-
data-index={index}
|
|
364
|
-
key={`${item.path}-${item.href ?? "internal"}`}
|
|
365
|
-
onClick={handleResultClick}
|
|
366
|
-
onMouseEnter={handleResultMouseEnter}
|
|
367
|
-
type="button"
|
|
368
|
-
>
|
|
369
|
-
<span className="text-sm font-medium text-foreground">
|
|
370
|
-
{item.title}
|
|
371
|
-
</span>
|
|
372
|
-
<span className="text-xs">{href}</span>
|
|
373
|
-
</button>
|
|
374
|
-
);
|
|
375
|
-
})}
|
|
376
|
-
</div>
|
|
377
|
-
) : null}
|
|
378
|
-
</div>
|
|
449
|
+
<Dialog onOpenChange={handleOpenChange} open={open}>
|
|
450
|
+
<DialogContent
|
|
451
|
+
className="max-w-2xl gap-0 overflow-hidden p-0"
|
|
452
|
+
onKeyDown={handleDialogKeyDown}
|
|
453
|
+
onOpenAutoFocus={handleOpenAutoFocus}
|
|
454
|
+
showCloseButton={false}
|
|
455
|
+
>
|
|
456
|
+
<DialogTitle className="sr-only">Search documentation</DialogTitle>
|
|
457
|
+
<DialogDescription className="sr-only">
|
|
458
|
+
Search documentation pages and jump directly to a result.
|
|
459
|
+
</DialogDescription>
|
|
460
|
+
<div className="flex items-center gap-3 border-b border-border px-4 py-3">
|
|
461
|
+
<SearchIcon className="size-4 text-muted-foreground" />
|
|
462
|
+
<input
|
|
463
|
+
aria-label="Search documentation"
|
|
464
|
+
className="w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
|
|
465
|
+
onChange={handleQueryChange}
|
|
466
|
+
placeholder="Search docs..."
|
|
467
|
+
ref={inputRef}
|
|
468
|
+
type="text"
|
|
469
|
+
value={query}
|
|
470
|
+
/>
|
|
471
|
+
<button
|
|
472
|
+
className="rounded-md border border-border px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
473
|
+
onClick={closeSearch}
|
|
474
|
+
type="button"
|
|
475
|
+
>
|
|
476
|
+
Esc
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
<div className="max-h-[min(70vh,32rem)] overflow-y-auto p-2">
|
|
480
|
+
<SearchResults
|
|
481
|
+
activeIndex={activeIndex}
|
|
482
|
+
basePath={basePath}
|
|
483
|
+
filteredItems={filteredItems}
|
|
484
|
+
onResultClick={handleResultClick}
|
|
485
|
+
onResultMouseEnter={handleResultMouseEnter}
|
|
486
|
+
status={status}
|
|
487
|
+
/>
|
|
379
488
|
</div>
|
|
380
|
-
</
|
|
381
|
-
|
|
489
|
+
</DialogContent>
|
|
490
|
+
</Dialog>
|
|
382
491
|
</>
|
|
383
492
|
);
|
|
384
493
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
|
|
3
|
+
import { siteConfig } from "@/lib/config";
|
|
4
|
+
|
|
5
|
+
export const SiteFooter = () => (
|
|
6
|
+
<footer className="flex flex-col items-center justify-center gap-2 pt-16 pb-8 text-muted-foreground text-sm">
|
|
7
|
+
<div className="flex items-center gap-1">
|
|
8
|
+
Crafted by
|
|
9
|
+
<a
|
|
10
|
+
className="flex items-center gap-2 rounded-full py-1.5 pr-2.5 pl-1.5 transition-colors hover:text-foreground"
|
|
11
|
+
href={siteConfig.links.author}
|
|
12
|
+
rel="noopener noreferrer"
|
|
13
|
+
target="_blank"
|
|
14
|
+
>
|
|
15
|
+
<Image
|
|
16
|
+
alt="Avatar of Matthew Blode"
|
|
17
|
+
className="rounded-full"
|
|
18
|
+
height={20}
|
|
19
|
+
src="/matthew-blode-profile.jpg"
|
|
20
|
+
unoptimized
|
|
21
|
+
width={20}
|
|
22
|
+
/>
|
|
23
|
+
Matthew Blode
|
|
24
|
+
</a>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="flex items-center gap-2 text-muted-foreground/30">
|
|
27
|
+
<span className="text-muted-foreground">v{siteConfig.version}</span>{" "}
|
|
28
|
+
•
|
|
29
|
+
<a
|
|
30
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
31
|
+
href={siteConfig.links.github}
|
|
32
|
+
rel="noopener noreferrer"
|
|
33
|
+
target="_blank"
|
|
34
|
+
>
|
|
35
|
+
GitHub
|
|
36
|
+
</a>
|
|
37
|
+
</div>
|
|
38
|
+
</footer>
|
|
39
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DOCS_CONFIG_FILE = "docs.json";
|
|
6
|
+
|
|
7
|
+
const EXTERNAL_DOCS_ROOTS: Record<string, string> = {
|
|
8
|
+
allmd: path.join(os.homedir(), "Code/mblode/allmd/apps/docs"),
|
|
9
|
+
"dnd-grid": path.join(os.homedir(), "Code/mblode/dnd-grid/apps/docs"),
|
|
10
|
+
donebear: path.join(os.homedir(), "Code/donebear/donebear/apps/docs"),
|
|
11
|
+
shareful: path.join(os.homedir(), "Code/shareful-ai/shareful-ai/apps/docs"),
|
|
12
|
+
stratasync: path.join(os.homedir(), "Code/donebear/stratasync/apps/docs"),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getTenantDocsPathCandidates = (slug: string): string[] =>
|
|
16
|
+
[
|
|
17
|
+
EXTERNAL_DOCS_ROOTS[slug],
|
|
18
|
+
path.join(process.cwd(), "content", slug),
|
|
19
|
+
path.join(process.cwd(), "apps/docs/content", slug),
|
|
20
|
+
].filter(Boolean) as string[];
|
|
21
|
+
|
|
22
|
+
export const getTenantDocsPath = (slug: string): string => {
|
|
23
|
+
const candidates = getTenantDocsPathCandidates(slug);
|
|
24
|
+
const defaultLocalPath = path.join(process.cwd(), "apps/docs/content", slug);
|
|
25
|
+
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
if (fs.existsSync(path.join(candidate, DOCS_CONFIG_FILE))) {
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return defaultLocalPath;
|
|
33
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SiteConfig, Tenant } from "@repo/models";
|
|
2
|
+
import type { ContentSource } from "@repo/previewing";
|
|
3
|
+
import { createBlobSource, createFsSource } from "@repo/previewing";
|
|
4
|
+
|
|
5
|
+
import { getTenantDocsPath } from "./content-root";
|
|
6
|
+
import { getProjectTag } from "./tenants";
|
|
7
|
+
|
|
8
|
+
const ABSOLUTE_URL_REGEX = /^https?:\/\//i;
|
|
9
|
+
|
|
10
|
+
const resolveAssetUrl = async (
|
|
11
|
+
source: ContentSource,
|
|
12
|
+
value?: string
|
|
13
|
+
): Promise<string | undefined> => {
|
|
14
|
+
if (!value || value.startsWith("/") || ABSOLUTE_URL_REGEX.test(value)) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const resolved = await source.resolveUrl?.(value);
|
|
19
|
+
return resolved ?? value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getTenantContentSource = (tenant: Tenant): ContentSource => {
|
|
23
|
+
if (tenant.activeDeploymentManifestUrl) {
|
|
24
|
+
return createBlobSource(
|
|
25
|
+
tenant.activeDeploymentManifestUrl,
|
|
26
|
+
getProjectTag(tenant.slug)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return createFsSource(tenant.docsPath ?? getTenantDocsPath(tenant.slug));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const resolveSiteConfigAssets = async (
|
|
34
|
+
config: SiteConfig,
|
|
35
|
+
source: ContentSource
|
|
36
|
+
): Promise<SiteConfig> => {
|
|
37
|
+
const [favicon, fontCssUrl, darkLogo, lightLogo, ogImage] = await Promise.all(
|
|
38
|
+
[
|
|
39
|
+
resolveAssetUrl(source, config.favicon),
|
|
40
|
+
resolveAssetUrl(source, config.fonts?.cssUrl),
|
|
41
|
+
resolveAssetUrl(source, config.logo?.dark),
|
|
42
|
+
resolveAssetUrl(source, config.logo?.light),
|
|
43
|
+
resolveAssetUrl(source, config.metadata?.ogImage),
|
|
44
|
+
]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...config,
|
|
49
|
+
favicon,
|
|
50
|
+
fonts: config.fonts
|
|
51
|
+
? {
|
|
52
|
+
...config.fonts,
|
|
53
|
+
cssUrl: fontCssUrl,
|
|
54
|
+
}
|
|
55
|
+
: config.fonts,
|
|
56
|
+
logo: config.logo
|
|
57
|
+
? {
|
|
58
|
+
...config.logo,
|
|
59
|
+
dark: darkLogo,
|
|
60
|
+
light: lightLogo,
|
|
61
|
+
}
|
|
62
|
+
: config.logo,
|
|
63
|
+
metadata: config.metadata
|
|
64
|
+
? {
|
|
65
|
+
...config.metadata,
|
|
66
|
+
ogImage,
|
|
67
|
+
}
|
|
68
|
+
: config.metadata,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ContextualBuiltinOption,
|
|
3
3
|
ContextualCustomOption,
|
|
4
|
+
ContextualOption,
|
|
4
5
|
} from "@repo/models";
|
|
5
6
|
|
|
6
7
|
interface BuiltinOptionDefinition {
|
|
@@ -113,11 +114,30 @@ interface UrlContext {
|
|
|
113
114
|
mcpServerUrl?: string;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
const PAGE_PLACEHOLDER = "$page";
|
|
118
|
+
const BUILTIN_OPTIONS_REQUIRING_PAGE_CONTENT = new Set<ContextualBuiltinOption>(
|
|
119
|
+
["assistant", "copy"]
|
|
120
|
+
);
|
|
121
|
+
|
|
116
122
|
const askPrompt = (url: string) =>
|
|
117
123
|
`Read from ${url} so I can ask questions about it.`;
|
|
118
124
|
|
|
119
125
|
const encoded = (text: string) => encodeURIComponent(text);
|
|
120
126
|
|
|
127
|
+
const customHrefUsesPageContent = (href: ContextualCustomOption["href"]) =>
|
|
128
|
+
typeof href === "string"
|
|
129
|
+
? href.includes(PAGE_PLACEHOLDER)
|
|
130
|
+
: href.query.some((item) => item.value.includes(PAGE_PLACEHOLDER));
|
|
131
|
+
|
|
132
|
+
export const contextualOptionsRequirePageContent = (
|
|
133
|
+
options: ContextualOption[]
|
|
134
|
+
) =>
|
|
135
|
+
options.some((option) =>
|
|
136
|
+
typeof option === "string"
|
|
137
|
+
? BUILTIN_OPTIONS_REQUIRING_PAGE_CONTENT.has(option)
|
|
138
|
+
: customHrefUsesPageContent(option.href)
|
|
139
|
+
);
|
|
140
|
+
|
|
121
141
|
export const buildBuiltinUrl = (
|
|
122
142
|
id: ContextualBuiltinOption,
|
|
123
143
|
context: UrlContext
|