radiant-docs 0.1.0
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/dist/index.js +312 -0
- package/package.json +38 -0
- package/template/.vscode/extensions.json +4 -0
- package/template/.vscode/launch.json +11 -0
- package/template/astro.config.mjs +216 -0
- package/template/ec.config.mjs +51 -0
- package/template/package-lock.json +12546 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +9 -0
- package/template/src/assets/icons/check.svg +33 -0
- package/template/src/assets/icons/danger.svg +37 -0
- package/template/src/assets/icons/info.svg +36 -0
- package/template/src/assets/icons/lightbulb.svg +74 -0
- package/template/src/assets/icons/warning.svg +37 -0
- package/template/src/components/Header.astro +176 -0
- package/template/src/components/MdxPage.astro +49 -0
- package/template/src/components/OpenApiPage.astro +270 -0
- package/template/src/components/Search.astro +362 -0
- package/template/src/components/Sidebar.astro +19 -0
- package/template/src/components/SidebarDropdown.astro +149 -0
- package/template/src/components/SidebarGroup.astro +51 -0
- package/template/src/components/SidebarLink.astro +56 -0
- package/template/src/components/SidebarMenu.astro +46 -0
- package/template/src/components/SidebarSubgroup.astro +136 -0
- package/template/src/components/TableOfContents.astro +480 -0
- package/template/src/components/ThemeSwitcher.astro +84 -0
- package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
- package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
- package/template/src/components/endpoint/PlaygroundField.astro +54 -0
- package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
- package/template/src/components/endpoint/RequestSnippets.astro +308 -0
- package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
- package/template/src/components/endpoint/ResponseFields.astro +224 -0
- package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
- package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
- package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
- package/template/src/components/ui/Field.astro +69 -0
- package/template/src/components/ui/Tag.astro +5 -0
- package/template/src/components/ui/demo/CodeDemo.astro +15 -0
- package/template/src/components/ui/demo/Demo.astro +3 -0
- package/template/src/components/ui/demo/UiDisplay.astro +13 -0
- package/template/src/components/user/Accordian.astro +69 -0
- package/template/src/components/user/AccordianGroup.astro +13 -0
- package/template/src/components/user/Callout.astro +101 -0
- package/template/src/components/user/Step.astro +51 -0
- package/template/src/components/user/Steps.astro +9 -0
- package/template/src/components/user/Tab.astro +25 -0
- package/template/src/components/user/Tabs.astro +122 -0
- package/template/src/content.config.ts +11 -0
- package/template/src/entrypoint.ts +9 -0
- package/template/src/layouts/Layout.astro +92 -0
- package/template/src/lib/component-error.ts +163 -0
- package/template/src/lib/frontmatter-schema.ts +9 -0
- package/template/src/lib/oas.ts +24 -0
- package/template/src/lib/pagefind.ts +88 -0
- package/template/src/lib/routes.ts +316 -0
- package/template/src/lib/utils.ts +59 -0
- package/template/src/lib/validation.ts +1097 -0
- package/template/src/pages/[...slug].astro +77 -0
- package/template/src/styles/global.css +209 -0
- package/template/tsconfig.json +5 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
|
|
4
|
+
const html = await Astro.slots.render("default");
|
|
5
|
+
|
|
6
|
+
const labelRegex = /label="([^"]+)"/g;
|
|
7
|
+
const iconRegex = /icon="([^"]*)"/g;
|
|
8
|
+
let labels = [];
|
|
9
|
+
let icons = [];
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = labelRegex.exec(html)) !== null) {
|
|
12
|
+
labels.push(match[1]);
|
|
13
|
+
}
|
|
14
|
+
while ((match = iconRegex.exec(html)) !== null) {
|
|
15
|
+
icons.push(match[1]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (labels.length === 0) {
|
|
19
|
+
const pagePath = Astro.url.pathname.replace(/^\/documentation\//, "").replace(/\/$/, "");
|
|
20
|
+
throw new Error(
|
|
21
|
+
`[USER_ERROR]: <Tabs>: Must contain at least two <Tab> children (in ${pagePath}.mdx)`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tabRegex = /<section[^>]*data-label="[^"]*"[^>]*>(.*?)<\/section>/gs;
|
|
26
|
+
let tabContents = [];
|
|
27
|
+
let contentMatch;
|
|
28
|
+
while ((contentMatch = tabRegex.exec(html)) !== null) {
|
|
29
|
+
tabContents.push(contentMatch[1]);
|
|
30
|
+
}
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<div x-data="{
|
|
34
|
+
activeTab: 0,
|
|
35
|
+
containerHeight: 'auto',
|
|
36
|
+
markerStyle: { left: null, width: null },
|
|
37
|
+
init() {
|
|
38
|
+
this.$nextTick(() => {
|
|
39
|
+
this.updateMarker(this.activeTab);
|
|
40
|
+
this.updateHeight();
|
|
41
|
+
});
|
|
42
|
+
this.$watch('activeTab', (value) => {
|
|
43
|
+
this.updateMarker(value);
|
|
44
|
+
this.updateHeight();
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
updateMarker(index) {
|
|
48
|
+
const el = this.$refs['tab-' + index];
|
|
49
|
+
if (el) {
|
|
50
|
+
this.markerStyle = {
|
|
51
|
+
left: el.offsetLeft + 'px',
|
|
52
|
+
width: el.offsetWidth + 'px',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
updateHeight() {
|
|
57
|
+
this.$nextTick(() => {
|
|
58
|
+
// We look for the internal wrapper or the content div specifically
|
|
59
|
+
const activeSlide = this.$refs['content-' + this.activeTab];
|
|
60
|
+
if (activeSlide) {
|
|
61
|
+
// scrollHeight is often more reliable than offsetHeight for hidden overflow
|
|
62
|
+
this.containerHeight = activeSlide.scrollHeight + 'px';
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}"
|
|
67
|
+
class="my-5">
|
|
68
|
+
<ul
|
|
69
|
+
class="relative isolate not-prose flex border border-neutral-200 w-fit bg-neutral-100/80 rounded-lg p-[3px] inset-shadow-sm"
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
class="absolute top-[3px] bottom-[3px] bg-white rounded-md shadow-sm transition-all duration-300 ease-out -z-10 flex items-center justify-center"
|
|
73
|
+
style="left: 3px;"
|
|
74
|
+
:style="markerStyle.width ? `left: ${markerStyle.left}; width: ${markerStyle.width}` : ''"
|
|
75
|
+
>
|
|
76
|
+
<span class="px-3 font-medium text-sm opacity-0 select-none" x-show="!markerStyle.width">
|
|
77
|
+
{labels[0]}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{ labels.map((label, index) => (
|
|
82
|
+
<li>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
x-ref={`tab-${index}`}
|
|
86
|
+
@click={`activeTab = ${index}`}
|
|
87
|
+
class="relative px-3 h-[32px] font-medium text-sm transition-colors duration-200 cursor-pointer text-nowrap flex items-center gap-2"
|
|
88
|
+
style={index === 0 ? "" : ""}
|
|
89
|
+
class:list={[index === 0 ? "text-neutral-900" : "text-neutral-500"]}
|
|
90
|
+
:class={`{
|
|
91
|
+
'text-neutral-900': activeTab === ${index},
|
|
92
|
+
'text-neutral-500 hover:text-neutral-600': activeTab !== ${index}
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
{icons[index] && <Icon name={`lucide:${icons[index]}`} class="size-4 shrink-0" />}
|
|
96
|
+
{label}
|
|
97
|
+
</button>
|
|
98
|
+
</li>
|
|
99
|
+
)) }
|
|
100
|
+
</ul>
|
|
101
|
+
|
|
102
|
+
<div
|
|
103
|
+
class="mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
|
|
104
|
+
:style="'height: ' + containerHeight"
|
|
105
|
+
>
|
|
106
|
+
<div
|
|
107
|
+
class="flex items-start transition-transform duration-300 ease-in-out"
|
|
108
|
+
:style="'transform: translateX(-' + (activeTab * 100) + '%)'"
|
|
109
|
+
>
|
|
110
|
+
{ tabContents.map((content, index) => (
|
|
111
|
+
// We add a ref here so we can measure the height
|
|
112
|
+
<div
|
|
113
|
+
x-ref={`content-${index}`}
|
|
114
|
+
class="w-full shrink-0 transition-opacity duration-300 ease-in-out"
|
|
115
|
+
:style={`activeTab === ${index} ? 'opacity: 1' : 'opacity: 0 pointer-events-none'`}
|
|
116
|
+
style={index === 0 ? 'opacity: 1' : 'opacity: 0'}
|
|
117
|
+
set:html={content}
|
|
118
|
+
/>
|
|
119
|
+
)) }
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineCollection } from "astro:content";
|
|
2
|
+
import { glob } from "astro/loaders";
|
|
3
|
+
import { docsSchema } from "./lib/frontmatter-schema";
|
|
4
|
+
|
|
5
|
+
const docs = defineCollection({
|
|
6
|
+
// Load Markdown and MDX files from src/content/docs
|
|
7
|
+
// This pattern excludes non-content files like docs.json and images
|
|
8
|
+
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/docs" }),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const collections = { docs };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/global.css";
|
|
3
|
+
import Sidebar from "../components/Sidebar.astro";
|
|
4
|
+
import { getConfig } from "../lib/validation";
|
|
5
|
+
import Header from "../components/Header.astro";
|
|
6
|
+
|
|
7
|
+
const config = await getConfig();
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<!doctype html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<script is:inline>
|
|
14
|
+
const applyTheme = () => {
|
|
15
|
+
const localStorageTheme = localStorage.getItem("theme");
|
|
16
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
|
17
|
+
.matches
|
|
18
|
+
? "dark"
|
|
19
|
+
: "light";
|
|
20
|
+
const resolvedTheme =
|
|
21
|
+
localStorageTheme === "system" || !localStorageTheme
|
|
22
|
+
? systemTheme
|
|
23
|
+
: localStorageTheme;
|
|
24
|
+
|
|
25
|
+
document.documentElement.classList.toggle(
|
|
26
|
+
"dark",
|
|
27
|
+
resolvedTheme === "dark"
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Run on initial load
|
|
32
|
+
applyTheme();
|
|
33
|
+
</script>
|
|
34
|
+
<meta charset="UTF-8" />
|
|
35
|
+
<meta name="viewport" content="width=device-width" />
|
|
36
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
37
|
+
<meta name="generator" content={Astro.generator} />
|
|
38
|
+
<title>{config.title} Docs</title>
|
|
39
|
+
</head>
|
|
40
|
+
<body
|
|
41
|
+
class="bg-background text-neutral-900 dark:text-white"
|
|
42
|
+
x-data="{ open: false }"
|
|
43
|
+
x-bind:class="open ? 'overflow-hidden touch-none' : ''"
|
|
44
|
+
>
|
|
45
|
+
<!-- Edges -->
|
|
46
|
+
<div class="fixed top-0 inset-x-0 h-16 bg-background-dark -z-10"></div>
|
|
47
|
+
<div
|
|
48
|
+
class="fixed top-1 inset-x-1 h-16 bg-background transition-color duration-700 -z-10 rounded-t-xl"
|
|
49
|
+
>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="fixed top-0 inset-x-0 h-1 bg-background-dark z-50"></div>
|
|
52
|
+
<div
|
|
53
|
+
class="fixed top-[63px] -z-10 w-[5px] right-0 bottom-0 bg-background-dark border-l border-l-border"
|
|
54
|
+
>
|
|
55
|
+
</div>
|
|
56
|
+
<div
|
|
57
|
+
class="fixed top-[63px] -z-10 w-[5px] left-0 bottom-0 bg-background-dark border-r border-r-border"
|
|
58
|
+
>
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
class="fixed -z-10 top-1 inset-x-1 bottom-0 rounded-xl shadow-[0_1px_1px_#00000005,0_4px_8px_-4px_#0000000a,0_16px_24px_-8px_#0000000f]"
|
|
62
|
+
>
|
|
63
|
+
</div>
|
|
64
|
+
<!-- Header -->
|
|
65
|
+
<Header />
|
|
66
|
+
|
|
67
|
+
<!-- Desktop Sidebar -->
|
|
68
|
+
<div
|
|
69
|
+
class="bg-background w-[283px] ml-[5px] fixed inset-y-0 mt-17 hidden lg:block border-r border-r-border-light"
|
|
70
|
+
data-pagefind-ignore
|
|
71
|
+
>
|
|
72
|
+
<Sidebar />
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Mobile Menu -->
|
|
76
|
+
<div
|
|
77
|
+
x-show="open"
|
|
78
|
+
x-cloak
|
|
79
|
+
class="bg-background mx-[5px] min-h-[calc(100vh-68px)] mt-17 fixed inset-0 lg:hidden z-40 overflow-y-auto"
|
|
80
|
+
x-transition.opacity
|
|
81
|
+
>
|
|
82
|
+
<Sidebar />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Main Content -->
|
|
86
|
+
<div class="px-5 sm:px-7 lg:pl-[calc(288px+24px)] pt-16 lg:pr-7">
|
|
87
|
+
<main class="max-w-2xl xl:max-w-5xl mx-auto pt-16 pb-16">
|
|
88
|
+
<slot />
|
|
89
|
+
</main>
|
|
90
|
+
</div>
|
|
91
|
+
</body>
|
|
92
|
+
</html>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component validation utilities for user-facing MDX components.
|
|
3
|
+
* All errors are tagged with [USER_ERROR] for proper error handling in runner.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Derives the source MDX file path from an Astro URL pathname
|
|
8
|
+
*/
|
|
9
|
+
function getSourceFile(pathname: string): string {
|
|
10
|
+
const pagePath = pathname
|
|
11
|
+
.replace(/^\/documentation\//, "") // Remove base path
|
|
12
|
+
.replace(/\/$/, ""); // Remove trailing slash
|
|
13
|
+
return `${pagePath}.mdx`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Formats a user-friendly error message with file location
|
|
18
|
+
*/
|
|
19
|
+
function formatError(
|
|
20
|
+
componentName: string,
|
|
21
|
+
message: string,
|
|
22
|
+
sourceFile: string
|
|
23
|
+
): string {
|
|
24
|
+
return `[USER_ERROR]: <${componentName}>: ${message} (in ${sourceFile})`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates that a prop value is one of the allowed values
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* validateEnum("Callout", "type", type, ["warning", "info", "tip"], Astro.url.pathname);
|
|
32
|
+
*/
|
|
33
|
+
export function validateEnum<T extends string>(
|
|
34
|
+
componentName: string,
|
|
35
|
+
propName: string,
|
|
36
|
+
value: T,
|
|
37
|
+
validValues: readonly T[],
|
|
38
|
+
pathname: string
|
|
39
|
+
): void {
|
|
40
|
+
if (!validValues.includes(value)) {
|
|
41
|
+
const sourceFile = getSourceFile(pathname);
|
|
42
|
+
throw new Error(
|
|
43
|
+
formatError(
|
|
44
|
+
componentName,
|
|
45
|
+
`Invalid prop ${propName}="${value}". Expected one of: ${validValues.join(
|
|
46
|
+
", "
|
|
47
|
+
)}`,
|
|
48
|
+
sourceFile
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates that a required prop is provided and not empty
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* validateRequired("Step", "title", title, Astro.url.pathname);
|
|
59
|
+
*/
|
|
60
|
+
export function validateRequired(
|
|
61
|
+
componentName: string,
|
|
62
|
+
propName: string,
|
|
63
|
+
value: unknown,
|
|
64
|
+
pathname: string
|
|
65
|
+
): void {
|
|
66
|
+
if (value === undefined || value === null || value === "") {
|
|
67
|
+
const sourceFile = getSourceFile(pathname);
|
|
68
|
+
throw new Error(
|
|
69
|
+
formatError(
|
|
70
|
+
componentName,
|
|
71
|
+
`Missing required prop "${propName}"`,
|
|
72
|
+
sourceFile
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validates that a prop is of the expected type
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* validateType("Accordion", "defaultOpen", defaultOpen, "boolean", Astro.url.pathname);
|
|
83
|
+
*/
|
|
84
|
+
export function validateType(
|
|
85
|
+
componentName: string,
|
|
86
|
+
propName: string,
|
|
87
|
+
value: unknown,
|
|
88
|
+
expectedType: "string" | "number" | "boolean" | "object" | "array",
|
|
89
|
+
pathname: string
|
|
90
|
+
): void {
|
|
91
|
+
// Skip if undefined (optional props)
|
|
92
|
+
if (value === undefined) return;
|
|
93
|
+
|
|
94
|
+
let isValid = false;
|
|
95
|
+
|
|
96
|
+
if (expectedType === "array") {
|
|
97
|
+
isValid = Array.isArray(value);
|
|
98
|
+
} else {
|
|
99
|
+
isValid = typeof value === expectedType;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isValid) {
|
|
103
|
+
const sourceFile = getSourceFile(pathname);
|
|
104
|
+
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
105
|
+
throw new Error(
|
|
106
|
+
formatError(
|
|
107
|
+
componentName,
|
|
108
|
+
`Invalid prop "${propName}": expected ${expectedType}, got ${actualType}`,
|
|
109
|
+
sourceFile
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validates multiple props at once using a schema object
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* validateProps("Callout", Astro.props, {
|
|
120
|
+
* type: { enum: ["warning", "info", "tip", "danger", "check"] },
|
|
121
|
+
* title: { type: "string" },
|
|
122
|
+
* }, Astro.url.pathname);
|
|
123
|
+
*/
|
|
124
|
+
export type PropSchema = {
|
|
125
|
+
required?: boolean;
|
|
126
|
+
type?: "string" | "number" | "boolean" | "object" | "array";
|
|
127
|
+
enum?: readonly string[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export function validateProps(
|
|
131
|
+
componentName: string,
|
|
132
|
+
props: Record<string, unknown>,
|
|
133
|
+
schema: Record<string, PropSchema>,
|
|
134
|
+
pathname: string
|
|
135
|
+
): void {
|
|
136
|
+
for (const [propName, rules] of Object.entries(schema)) {
|
|
137
|
+
const value = props[propName];
|
|
138
|
+
|
|
139
|
+
// Check required
|
|
140
|
+
if (rules.required) {
|
|
141
|
+
validateRequired(componentName, propName, value, pathname);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Skip further checks if value is undefined (optional)
|
|
145
|
+
if (value === undefined) continue;
|
|
146
|
+
|
|
147
|
+
// Check type
|
|
148
|
+
if (rules.type) {
|
|
149
|
+
validateType(componentName, propName, value, rules.type, pathname);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check enum
|
|
153
|
+
if (rules.enum) {
|
|
154
|
+
validateEnum(
|
|
155
|
+
componentName,
|
|
156
|
+
propName,
|
|
157
|
+
value as string,
|
|
158
|
+
rules.enum,
|
|
159
|
+
pathname
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Oas from "oas";
|
|
2
|
+
import { loadOpenApiSpec } from "./validation";
|
|
3
|
+
|
|
4
|
+
// Cache for dereferenced Oas instances (key: filePathOrUrl, value: Oas instance)
|
|
5
|
+
const oasInstanceCache = new Map<string, Oas>();
|
|
6
|
+
|
|
7
|
+
export async function getOasInstance(filePathOrUrl: string): Promise<Oas> {
|
|
8
|
+
// Check cache first
|
|
9
|
+
if (oasInstanceCache.has(filePathOrUrl)) {
|
|
10
|
+
return oasInstanceCache.get(filePathOrUrl)!;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Load the spec (uses existing cache)
|
|
14
|
+
const openApiDoc = await loadOpenApiSpec(filePathOrUrl);
|
|
15
|
+
|
|
16
|
+
// Create and dereference Oas instance
|
|
17
|
+
const api = new Oas(openApiDoc);
|
|
18
|
+
await api.dereference();
|
|
19
|
+
|
|
20
|
+
// Cache the dereferenced instance
|
|
21
|
+
oasInstanceCache.set(filePathOrUrl, api);
|
|
22
|
+
|
|
23
|
+
return api;
|
|
24
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Pagefind TypeScript types and wrapper
|
|
2
|
+
|
|
3
|
+
export interface PagefindSearchResult {
|
|
4
|
+
id: string;
|
|
5
|
+
data: () => Promise<PagefindResultData>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PagefindResultData {
|
|
9
|
+
url: string;
|
|
10
|
+
content: string;
|
|
11
|
+
word_count: number;
|
|
12
|
+
excerpt: string;
|
|
13
|
+
meta: {
|
|
14
|
+
title?: string;
|
|
15
|
+
image?: string;
|
|
16
|
+
[key: string]: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
sub_results?: PagefindSubResult[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PagefindSubResult {
|
|
22
|
+
title: string;
|
|
23
|
+
url: string;
|
|
24
|
+
excerpt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PagefindSearchResponse {
|
|
28
|
+
results: PagefindSearchResult[];
|
|
29
|
+
unfilteredResultCount: number;
|
|
30
|
+
filters: Record<string, Record<string, number>>;
|
|
31
|
+
totalFilters: Record<string, Record<string, number>>;
|
|
32
|
+
timings: {
|
|
33
|
+
preload: number;
|
|
34
|
+
search: number;
|
|
35
|
+
total: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PagefindInstance {
|
|
40
|
+
init: () => Promise<void>;
|
|
41
|
+
search: (
|
|
42
|
+
query: string,
|
|
43
|
+
options?: { filters?: Record<string, string> }
|
|
44
|
+
) => Promise<PagefindSearchResponse>;
|
|
45
|
+
filters: () => Promise<Record<string, Record<string, number>>>;
|
|
46
|
+
preload: (query: string) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let pagefindInstance: PagefindInstance | null = null;
|
|
50
|
+
|
|
51
|
+
export async function getPagefind(): Promise<PagefindInstance | null> {
|
|
52
|
+
if (pagefindInstance) return pagefindInstance;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Completely bypass Vite's module resolution by constructing the import dynamically
|
|
56
|
+
// This uses Function constructor to create a truly runtime import
|
|
57
|
+
const importPagefind = new Function(
|
|
58
|
+
'return import("/pagefind/pagefind.js")'
|
|
59
|
+
) as () => Promise<PagefindInstance>;
|
|
60
|
+
|
|
61
|
+
const pagefind = await importPagefind();
|
|
62
|
+
await pagefind.init();
|
|
63
|
+
pagefindInstance = pagefind;
|
|
64
|
+
return pagefindInstance;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.warn("Pagefind not available:", e);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function search(
|
|
72
|
+
query: string,
|
|
73
|
+
limit: number = 8
|
|
74
|
+
): Promise<PagefindResultData[]> {
|
|
75
|
+
const pagefind = await getPagefind();
|
|
76
|
+
if (!pagefind || !query.trim()) return [];
|
|
77
|
+
|
|
78
|
+
const response = await pagefind.search(query);
|
|
79
|
+
|
|
80
|
+
// Load full data for top results
|
|
81
|
+
const results = await Promise.all(
|
|
82
|
+
response.results.slice(0, limit).map((result) => result.data())
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
console.log("results", results);
|
|
86
|
+
|
|
87
|
+
return results;
|
|
88
|
+
}
|