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,207 @@
1
+ ---
2
+ import type { NavOpenApi } from "../../lib/validation";
3
+ import { loadOpenApiSpec } from "../../lib/validation";
4
+ import SidebarEndpointLink from "./SidebarEndpointLink.astro";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import yaml from "yaml";
8
+
9
+ interface Props {
10
+ openapi: string | NavOpenApi;
11
+ parentSlug?: string;
12
+ }
13
+
14
+ const { openapi: openApiPathOrConfig, parentSlug = "" } = Astro.props;
15
+
16
+ // Extract file path and filter options
17
+ let openApiPath: string;
18
+ let include: string[] | undefined;
19
+ let exclude: string[] | undefined;
20
+
21
+ if (typeof openApiPathOrConfig === "string") {
22
+ openApiPath = openApiPathOrConfig;
23
+ } else {
24
+ openApiPath = openApiPathOrConfig.source;
25
+ include = openApiPathOrConfig.include;
26
+ exclude = openApiPathOrConfig.exclude;
27
+ }
28
+
29
+ // Load the OpenAPI spec
30
+ const openApiDoc = await loadOpenApiSpec(openApiPath);
31
+
32
+ // Helper function to parse endpoint string
33
+ function parseEndpointString(
34
+ endpointStr: string
35
+ ): { method: string; path: string } | null {
36
+ const trimmed = endpointStr.trim();
37
+ const parts = trimmed.split(/\s+/);
38
+
39
+ if (parts.length !== 2) {
40
+ return null;
41
+ }
42
+
43
+ const method = parts[0].toUpperCase();
44
+ let path = parts[1];
45
+
46
+ if (!path.startsWith("/")) {
47
+ path = "/" + path;
48
+ }
49
+
50
+ const normalizedPath = path.toLowerCase();
51
+
52
+ return { method, path: normalizedPath };
53
+ }
54
+
55
+ // Helper function to check if an endpoint should be included
56
+ function shouldIncludeEndpoint(
57
+ method: string,
58
+ pathStr: string,
59
+ include?: string[],
60
+ exclude?: string[]
61
+ ): boolean {
62
+ const normalizedMethod = method.toUpperCase();
63
+ const normalizedPath = pathStr.toLowerCase();
64
+ const endpointKey = `${normalizedMethod} ${normalizedPath}`;
65
+
66
+ if (include) {
67
+ return include.some((entry) => {
68
+ const parsed = parseEndpointString(entry);
69
+ if (!parsed) return false;
70
+ return `${parsed.method} ${parsed.path}` === endpointKey;
71
+ });
72
+ }
73
+
74
+ if (exclude) {
75
+ return !exclude.some((entry) => {
76
+ const parsed = parseEndpointString(entry);
77
+ if (!parsed) return false;
78
+ return `${parsed.method} ${parsed.path}` === endpointKey;
79
+ });
80
+ }
81
+
82
+ return true;
83
+ }
84
+
85
+ // Extract endpoints from OpenAPI spec
86
+ const paths = openApiDoc.paths || {};
87
+ const httpMethods = [
88
+ "get",
89
+ "post",
90
+ "put",
91
+ "delete",
92
+ "patch",
93
+ "head",
94
+ "options",
95
+ "trace",
96
+ ];
97
+
98
+ interface Endpoint {
99
+ method: string;
100
+ path: string;
101
+ summary?: string;
102
+ tags?: string[];
103
+ }
104
+
105
+ const endpoints: Endpoint[] = [];
106
+
107
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
108
+ if (!pathItem || typeof pathItem !== "object") continue;
109
+
110
+ for (const method of httpMethods) {
111
+ const operation = (pathItem as any)[method];
112
+ if (!operation) continue;
113
+
114
+ if (!shouldIncludeEndpoint(method, pathStr, include, exclude)) {
115
+ continue;
116
+ }
117
+
118
+ endpoints.push({
119
+ method,
120
+ path: pathStr,
121
+ summary: operation.summary,
122
+ tags: operation.tags,
123
+ });
124
+ }
125
+ }
126
+
127
+ // Group endpoints by first tag
128
+ interface TagGroup {
129
+ tag: string;
130
+ endpoints: Endpoint[];
131
+ }
132
+
133
+ const tagGroups = new Map<string, Endpoint[]>();
134
+ const untaggedEndpoints: Endpoint[] = [];
135
+
136
+ for (const endpoint of endpoints) {
137
+ if (endpoint.tags && endpoint.tags.length > 0) {
138
+ const firstTag = endpoint.tags[0];
139
+ if (!tagGroups.has(firstTag)) {
140
+ tagGroups.set(firstTag, []);
141
+ }
142
+ tagGroups.get(firstTag)!.push(endpoint);
143
+ } else {
144
+ untaggedEndpoints.push(endpoint);
145
+ }
146
+ }
147
+
148
+ // Only add "Other" group if there are other tagged groups
149
+ // If no tags exist at all, we'll render untagged endpoints without a group header
150
+ const hasTaggedGroups = tagGroups.size > 0;
151
+ if (untaggedEndpoints.length > 0 && hasTaggedGroups) {
152
+ tagGroups.set("Other", untaggedEndpoints);
153
+ }
154
+
155
+ // Convert to array and sort by tag name
156
+ const sortedTagGroups: TagGroup[] = Array.from(tagGroups.entries())
157
+ .map(([tag, endpoints]) => ({ tag, endpoints }))
158
+ .sort((a, b) => {
159
+ // Put "Other" at the end
160
+ if (a.tag === "Other") return 1;
161
+ if (b.tag === "Other") return -1;
162
+ return a.tag.localeCompare(b.tag);
163
+ });
164
+ ---
165
+
166
+ <ul class="px-2 pt-4 pb-3">
167
+ {
168
+ // If there are tagged groups, render them (including "Other" if it exists)
169
+ hasTaggedGroups
170
+ ? sortedTagGroups.map((group, index) => (
171
+ <li
172
+ class={
173
+ index > 0
174
+ ? "mt-5 pt-[25px] relative before:absolute before:top-0 before:inset-x-0 before:h-px before:bg-linear-[90deg,transparent,var(--color-neutral-200)_20%,var(--color-neutral-200)_80%,transparent] dark:before:bg-linear-[90deg,transparent,var(--color-neutral-700)_20%,var(--color-neutral-700)_80%,transparent]"
175
+ : ""
176
+ }
177
+ >
178
+ <div class="text-sm font-semibold mb-2 flex items-center gap-2 px-2">
179
+ {group.tag}
180
+ </div>
181
+ <ul>
182
+ {group.endpoints.map((endpoint) => (
183
+ <li>
184
+ <SidebarEndpointLink
185
+ method={endpoint.method}
186
+ path={endpoint.path}
187
+ summary={endpoint.summary}
188
+ parentSlug={parentSlug}
189
+ />
190
+ </li>
191
+ ))}
192
+ </ul>
193
+ </li>
194
+ ))
195
+ : // If no tags at all, render untagged endpoints directly without group headers
196
+ untaggedEndpoints.map((endpoint, index) => (
197
+ <li>
198
+ <SidebarEndpointLink
199
+ method={endpoint.method}
200
+ path={endpoint.path}
201
+ summary={endpoint.summary}
202
+ parentSlug={parentSlug}
203
+ />
204
+ </li>
205
+ ))
206
+ }
207
+ </ul>
@@ -0,0 +1,69 @@
1
+ ---
2
+ import { renderMarkdown } from "../../lib/utils";
3
+
4
+ interface Props {
5
+ name: string;
6
+ type: string;
7
+ description?: string;
8
+ required?: boolean;
9
+ optional?: boolean;
10
+ enum?: (string | number)[];
11
+ }
12
+
13
+ const {
14
+ name,
15
+ type,
16
+ description,
17
+ required,
18
+ optional,
19
+ enum: enumValues,
20
+ } = Astro.props;
21
+
22
+ const formattedDescription = description
23
+ ? await renderMarkdown(description)
24
+ : null;
25
+ ---
26
+
27
+ <div>
28
+ <div class="flex items-center gap-2">
29
+ <h5 class="font-normal font-mono text-[15px] text-neutral-400">
30
+ {name.split(".").map((part, i, a) => (
31
+ <>{i > 0 && <span class="inline">.</span>}<span class:list={["inline", a.length - 1 === i && "text-neutral-900 font-semibold"]}>{part}</span></>
32
+ ))}
33
+ <!-- {name.split(".").map((part, i, a) => (
34
+ <>{i > 0 && <span class="inline">.</span>}<span class:list={["inline", a.length - 1 === i && "text-neutral-900 font-semibold"]}>{part}</span></>
35
+ ))} -->
36
+ </h5>
37
+ <code
38
+ class="text-[11px] font-medium text-neutral-600 border border-neutral-200 bg-neutral-50 px-1 rounded-md"
39
+ >{type}</code
40
+ >
41
+ {
42
+ required && (
43
+ <div class="text-red-700/70 bg-red-50 border border-red-700/10 rounded-full px-2 text-[11px] font-mono leading-none py-0.5 pb-0.5 font-[450] h-fit">
44
+ required
45
+ </div>
46
+ )
47
+ }
48
+ {
49
+ optional && (
50
+ <div class="text-blue-700/70 bg-blue-50 border border-blue-700/10 rounded-full px-2 text-[13px] leading-none py-px pb-0.5 font-[450] h-fit">
51
+ optional
52
+ </div>
53
+ )
54
+ }
55
+ </div>
56
+ {
57
+ enumValues && (
58
+ <div class="text-sm mt-1.5">
59
+ <span class="font-medium.">Options:</span>
60
+ {enumValues.map((v, i, a) => (
61
+ <>
62
+ <code class="text-[11px] font-medium text-neutral-600 border border-neutral-200 bg-neutral-50 px-1 py-px rounded-md">{v}</code>{a.length - 2 === i ? " or " : a.length - 1 !== i && ", "}
63
+ </>
64
+ ))}
65
+ </div>
66
+ )
67
+ }
68
+ <div class="mt-1.5 prose-rules prose-sm!" set:html={formattedDescription} />
69
+ </div>
@@ -0,0 +1,5 @@
1
+ <span
2
+ class="text-[9px] bg-blue-100 text-blue-800 border border-blue-800/20 px-1.5 py-px rounded-full tracking-wide font-bold"
3
+ >
4
+ <slot />
5
+ </span>
@@ -0,0 +1,15 @@
1
+ <span class="demo-component">
2
+ <slot />
3
+ </span>
4
+
5
+ <style>
6
+ @reference "../../../styles/global.css";
7
+
8
+ :global(.demo-component .frame) {
9
+ @apply shadow-none! my-0!;
10
+ }
11
+
12
+ :global(.demo-component .frame pre) {
13
+ @apply border-none! rounded-none rounded-b-xl bg-neutral-50! inset-shadow-xs;
14
+ }
15
+ </style>
@@ -0,0 +1,3 @@
1
+ <div class="border border-neutral-200 rounded-xl shadow-xs my-5!">
2
+ <slot />
3
+ </div>
@@ -0,0 +1,13 @@
1
+ ---
2
+ interface Props {
3
+ centerUi?: boolean;
4
+ }
5
+
6
+ const { centerUi } = Astro.props;
7
+ ---
8
+
9
+ <div
10
+ class:list={["px-8 flex justify-center", centerUi && "items-center min-h-96"]}
11
+ >
12
+ <slot />
13
+ </div>
@@ -0,0 +1,69 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import { validateProps } from "../../lib/component-error";
4
+
5
+ interface Props {
6
+ title: string;
7
+ icon?: string;
8
+ defaultOpen?: boolean;
9
+ titleSize?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
10
+ }
11
+
12
+ const { title, icon, defaultOpen = false, titleSize } = Astro.props;
13
+
14
+ validateProps(
15
+ "Accordion",
16
+ { title, icon, defaultOpen, titleSize },
17
+ {
18
+ title: { required: true, type: "string" },
19
+ icon: { type: "string" },
20
+ defaultOpen: { type: "boolean" },
21
+ titleSize: { enum: ["xs", "sm", "md", "lg", "xl", "2xl"] },
22
+ },
23
+ Astro.url.pathname
24
+ );
25
+ ---
26
+
27
+ <div
28
+ x-data=`{
29
+ id: null,
30
+ localExpanded: ${defaultOpen},
31
+ get expanded() {
32
+ return (typeof active !== 'undefined') ? (active === this.id) : this.localExpanded;
33
+ },
34
+ set expanded(value) {
35
+ if (typeof active !== 'undefined') {
36
+ active = value ? this.id : null;
37
+ } else {
38
+ this.localExpanded = value;
39
+ }
40
+ }
41
+ }`
42
+ x-init="id = (typeof register === 'function') ? register() : Math.random()"
43
+ role="region"
44
+ class="block border-b border-neutral-800/10 pb-4 pt-4 first:pt-0 last:border-b-0 last:pb-0"
45
+ >
46
+ <h4 class="not-prose">
47
+ <button
48
+ type="button"
49
+ x-on:click="expanded = !expanded"
50
+ :aria-expanded="expanded"
51
+ class="group flex w-full items-center justify-between gap-2.5 text-left font-medium text-neutral-800"
52
+ >
53
+ {icon && <Icon name={`lucide:${icon}`} />}
54
+ <span class:list={["flex-1", titleSize && `text-${titleSize}`]}
55
+ >{title}</span
56
+ >
57
+ <Icon
58
+ name="lucide:chevron-down"
59
+ class="size-5 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200"
60
+ x-bind:class="expanded && 'rotate-180'"
61
+ />
62
+ </button>
63
+ </h4>
64
+ <div x-show="expanded" x-collapse>
65
+ <div class="pt-2 *:first:mt-0 *:last:mb-0">
66
+ <slot />
67
+ </div>
68
+ </div>
69
+ </div>
@@ -0,0 +1,13 @@
1
+ <div
2
+ x-data="{
3
+ active: null,
4
+ count: 0,
5
+ register() {
6
+ this.count++;
7
+ return this.count;
8
+ }
9
+ }"
10
+ class="my-5 w-full"
11
+ >
12
+ <slot />
13
+ </div>
@@ -0,0 +1,101 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import warningIcon from "../../assets/icons/warning.svg?url";
4
+ import infoIcon from "../../assets/icons/info.svg?url";
5
+ import lightbulbIcon from "../../assets/icons/lightbulb.svg?url";
6
+ import dangerIcon from "../../assets/icons/danger.svg?url";
7
+ import checkIcon from "../../assets/icons/check.svg?url";
8
+ import { validateProps } from "../../lib/component-error";
9
+
10
+ type CalloutType = "warning" | "info" | "tip" | "danger" | "check";
11
+
12
+ interface Props {
13
+ type?: CalloutType;
14
+ title?: string;
15
+ icon?: string;
16
+ color?: string;
17
+ }
18
+
19
+ const typeDefaults: Record<
20
+ CalloutType,
21
+ { icon: string; color: string; title: string }
22
+ > = {
23
+ warning: {
24
+ icon: warningIcon,
25
+ color: "bg-amber-500",
26
+ title: "Warning",
27
+ },
28
+ info: {
29
+ icon: infoIcon,
30
+ color: "bg-sky-600/80",
31
+ title: "Info",
32
+ },
33
+ tip: {
34
+ icon: lightbulbIcon,
35
+ color: "bg-yellow-500",
36
+ title: "Tip",
37
+ },
38
+ danger: {
39
+ icon: dangerIcon,
40
+ color: "bg-red-600",
41
+ title: "Danger",
42
+ },
43
+ check: {
44
+ icon: checkIcon,
45
+ color: "bg-green-600",
46
+ title: "Success",
47
+ },
48
+ };
49
+
50
+ const { type = "info", title, icon, color } = Astro.props;
51
+
52
+ validateProps(
53
+ "Callout",
54
+ { type, title, icon, color },
55
+ {
56
+ type: { enum: ["warning", "info", "tip", "danger", "check"] },
57
+ title: { type: "string" },
58
+ icon: { type: "string" },
59
+ color: { type: "string" },
60
+ },
61
+ Astro.url.pathname
62
+ );
63
+
64
+ const defaults = typeDefaults[type];
65
+ const resolvedTitle = title ?? defaults.title;
66
+ ---
67
+
68
+ <aside
69
+ class:list={[
70
+ "my-5 space-y-1 rounded-lg border border-neutral-200/80 shadow-xs px-4 py-3.5 pl-6 relative",
71
+ ]}
72
+ role="note"
73
+ >
74
+ <div
75
+ class:list={[
76
+ "absolute left-2 inset-y-2 w-[2.5px] rounded-full",
77
+ defaults.color,
78
+ ]}
79
+ style={color && { backgroundColor: color }}
80
+ >
81
+ </div>
82
+ <div class="flex items-center gap-2 not-prose">
83
+ {
84
+ icon ? (
85
+ <Icon
86
+ name={`lucide:${icon}`}
87
+ class=""
88
+ style={color && { color: color }}
89
+ />
90
+ ) : (
91
+ <img src={defaults.icon} alt="" width="16" height="16" class="" />
92
+ )
93
+ }
94
+ <h6 class:list={["font-semibold text-sm"]}>
95
+ {resolvedTitle}
96
+ </h6>
97
+ </div>
98
+ <div class="[&>p]:m-0 [&>p:not(:last-child)]:mb-2">
99
+ <slot />
100
+ </div>
101
+ </aside>
@@ -0,0 +1,51 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import { validateProps } from "../../lib/component-error";
4
+
5
+ interface Props {
6
+ title: string;
7
+ }
8
+
9
+ const { title } = Astro.props;
10
+
11
+ validateProps(
12
+ "Step",
13
+ { title },
14
+ {
15
+ title: { required: true, type: "string" },
16
+ },
17
+ Astro.url.pathname
18
+ );
19
+ ---
20
+
21
+ <div
22
+ class:list={[
23
+ "relative pl-10 step-item pb-4 last:pb-0 space-y-4",
24
+ "before:absolute before:left-[10.5px] before:top-8 before:bottom-0 before:w-px before:bg-linear-[transparent,var(--color-neutral-200)_10%,var(--color-neutral-200)_90%,transparent]",
25
+ ]}
26
+ data-step-panel
27
+ >
28
+ <div
29
+ class:list={[
30
+ "flex items-center gap-1.5 not-prose",
31
+ "step-number before:bg-neutral-900 before:size-[22px] before:rounded-full before:text-white before:flex before:items-center before:justify-center before:text-xs before:font-bold before:absolute before:left-px before:top-[3px] before:shadow-[inset_0_1px_0_rgb(255,255,255,0.2),0_0_0_1px_var(--color-neutral-800),var(--shadow-md)]",
32
+ ]}
33
+ >
34
+ <h3 class="text-lg font-semibold text-neutral-900">
35
+ {title}
36
+ </h3>
37
+ </div>
38
+ <div class="[&>p]:m-0 [&>p:not(:last-child)]:mb-2">
39
+ <slot />
40
+ </div>
41
+ </div>
42
+
43
+ <style>
44
+ .step-item {
45
+ counter-increment: step-counter;
46
+ }
47
+
48
+ .step-number::before {
49
+ content: counter(step-counter);
50
+ }
51
+ </style>
@@ -0,0 +1,9 @@
1
+ ---
2
+ interface Props {}
3
+
4
+ const stepsId = `steps-${Math.random().toString(36).substring(2, 9)}`;
5
+ ---
6
+
7
+ <div class="my-6 space-y-1.5" id={stepsId}>
8
+ <slot />
9
+ </div>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import { validateProps, validateRequired } from "../../lib/component-error";
3
+
4
+ interface Props {
5
+ label: string;
6
+ icon?: string;
7
+ }
8
+ const { label, icon } = Astro.props;
9
+
10
+ validateProps(
11
+ "Tab",
12
+ { label, icon },
13
+ {
14
+ label: { required: true, type: "string" },
15
+ icon: { type: "string" },
16
+ },
17
+ Astro.url.pathname
18
+ );
19
+ ---
20
+
21
+ <section data-label={label} data-icon={icon || ""}>
22
+ <div class="*:m-0! *:mb-2">
23
+ <slot />
24
+ </div>
25
+ </section>