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.
Files changed (61) hide show
  1. package/dist/index.js +312 -0
  2. package/package.json +38 -0
  3. package/template/.vscode/extensions.json +4 -0
  4. package/template/.vscode/launch.json +11 -0
  5. package/template/astro.config.mjs +216 -0
  6. package/template/ec.config.mjs +51 -0
  7. package/template/package-lock.json +12546 -0
  8. package/template/package.json +51 -0
  9. package/template/public/favicon.svg +9 -0
  10. package/template/src/assets/icons/check.svg +33 -0
  11. package/template/src/assets/icons/danger.svg +37 -0
  12. package/template/src/assets/icons/info.svg +36 -0
  13. package/template/src/assets/icons/lightbulb.svg +74 -0
  14. package/template/src/assets/icons/warning.svg +37 -0
  15. package/template/src/components/Header.astro +176 -0
  16. package/template/src/components/MdxPage.astro +49 -0
  17. package/template/src/components/OpenApiPage.astro +270 -0
  18. package/template/src/components/Search.astro +362 -0
  19. package/template/src/components/Sidebar.astro +19 -0
  20. package/template/src/components/SidebarDropdown.astro +149 -0
  21. package/template/src/components/SidebarGroup.astro +51 -0
  22. package/template/src/components/SidebarLink.astro +56 -0
  23. package/template/src/components/SidebarMenu.astro +46 -0
  24. package/template/src/components/SidebarSubgroup.astro +136 -0
  25. package/template/src/components/TableOfContents.astro +480 -0
  26. package/template/src/components/ThemeSwitcher.astro +84 -0
  27. package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
  28. package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
  29. package/template/src/components/endpoint/PlaygroundField.astro +54 -0
  30. package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
  31. package/template/src/components/endpoint/RequestSnippets.astro +308 -0
  32. package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
  33. package/template/src/components/endpoint/ResponseFields.astro +224 -0
  34. package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
  35. package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
  36. package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
  37. package/template/src/components/ui/Field.astro +69 -0
  38. package/template/src/components/ui/Tag.astro +5 -0
  39. package/template/src/components/ui/demo/CodeDemo.astro +15 -0
  40. package/template/src/components/ui/demo/Demo.astro +3 -0
  41. package/template/src/components/ui/demo/UiDisplay.astro +13 -0
  42. package/template/src/components/user/Accordian.astro +69 -0
  43. package/template/src/components/user/AccordianGroup.astro +13 -0
  44. package/template/src/components/user/Callout.astro +101 -0
  45. package/template/src/components/user/Step.astro +51 -0
  46. package/template/src/components/user/Steps.astro +9 -0
  47. package/template/src/components/user/Tab.astro +25 -0
  48. package/template/src/components/user/Tabs.astro +122 -0
  49. package/template/src/content.config.ts +11 -0
  50. package/template/src/entrypoint.ts +9 -0
  51. package/template/src/layouts/Layout.astro +92 -0
  52. package/template/src/lib/component-error.ts +163 -0
  53. package/template/src/lib/frontmatter-schema.ts +9 -0
  54. package/template/src/lib/oas.ts +24 -0
  55. package/template/src/lib/pagefind.ts +88 -0
  56. package/template/src/lib/routes.ts +316 -0
  57. package/template/src/lib/utils.ts +59 -0
  58. package/template/src/lib/validation.ts +1097 -0
  59. package/template/src/pages/[...slug].astro +77 -0
  60. package/template/src/styles/global.css +209 -0
  61. 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,9 @@
1
+ import collapse from "@alpinejs/collapse";
2
+ import persist from "@alpinejs/persist";
3
+ import focus from "@alpinejs/focus";
4
+
5
+ export default (Alpine: any) => {
6
+ Alpine.plugin(collapse);
7
+ Alpine.plugin(persist);
8
+ Alpine.plugin(focus);
9
+ };
@@ -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,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const docsSchema = z.object({
4
+ title: z
5
+ .string({
6
+ invalid_type_error: "'title' must be a text string.",
7
+ })
8
+ .optional(),
9
+ });
@@ -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
+ }