radiant-docs 0.1.39 → 0.1.40
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/package.json +1 -1
- package/template/astro.config.mjs +38 -7
- package/template/package-lock.json +19 -7
- package/template/package.json +1 -1
- package/template/scripts/generate-robots-txt.mjs +29 -1
- package/template/scripts/stamp-image-versions.mjs +59 -33
- package/template/src/components/Footer.astro +2 -1
- package/template/src/components/Header.astro +8 -6
- package/template/src/components/LogoLink.astro +2 -1
- package/template/src/components/MdxPage.astro +15 -4
- package/template/src/components/PagePagination.astro +61 -0
- package/template/src/components/SidebarDropdown.astro +12 -8
- package/template/src/components/SidebarGroup.astro +1 -1
- package/template/src/components/SidebarMenu.astro +1 -1
- package/template/src/components/SidebarSegmented.astro +6 -5
- package/template/src/components/TableOfContents.astro +4 -13
- package/template/src/components/chat/AskAiWidget.tsx +274 -39
- package/template/src/components/endpoint/PlaygroundForm.astro +2 -1
- package/template/src/components/user/CodeBlock.astro +1 -1
- package/template/src/components/user/CodeGroup.astro +16 -1
- package/template/src/components/user/ComponentPreviewBlock.astro +1 -0
- package/template/src/components/user/Image.astro +43 -53
- package/template/src/layouts/Layout.astro +217 -7
- package/template/src/lib/base-path.ts +98 -0
- package/template/src/lib/component-error.ts +49 -10
- package/template/src/lib/mdx/remark-resolve-internal-links.ts +128 -18
- package/template/src/lib/pagefind.ts +62 -14
- package/template/src/lib/routes.ts +49 -1
- package/template/src/lib/static-asset-url.ts +3 -1
- package/template/src/lib/utils.ts +12 -4
- package/template/src/lib/validation.ts +376 -36
- package/template/src/pages/404.astro +2 -1
- package/template/src/pages/[...slug].astro +68 -6
- package/template/src/styles/global.css +62 -1
|
@@ -1,28 +1,54 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import { validateProps } from "../../lib/component-error";
|
|
2
|
+
import { validateNoUnknownProps, validateProps } from "../../lib/component-error";
|
|
4
3
|
import { renderMarkdown } from "../../lib/utils";
|
|
5
4
|
import { resolveStaticAssetUrl } from "../../lib/static-asset-url";
|
|
6
5
|
|
|
7
|
-
interface Props
|
|
6
|
+
interface Props {
|
|
8
7
|
src: string;
|
|
8
|
+
alt?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
width?: number | string;
|
|
9
11
|
zoom?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const
|
|
14
|
+
const imageProps = Astro.props as Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
validateNoUnknownProps(
|
|
17
|
+
"Image",
|
|
18
|
+
imageProps,
|
|
19
|
+
["src", "alt", "title", "width", "zoom"],
|
|
20
|
+
Astro.url.pathname,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
validateProps(
|
|
24
|
+
"Image",
|
|
25
|
+
imageProps,
|
|
26
|
+
{
|
|
27
|
+
src: { required: true, type: "string" },
|
|
28
|
+
alt: { type: "string" },
|
|
29
|
+
title: { type: "string" },
|
|
30
|
+
width: { type: ["number", "string"] },
|
|
31
|
+
zoom: { type: "boolean" },
|
|
32
|
+
},
|
|
33
|
+
Astro.url.pathname,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const { src, alt, title, width, zoom = true } = imageProps as Props;
|
|
13
37
|
const zoomEnabled = zoom !== false;
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
const resolvedSrc = resolveStaticAssetUrl(src);
|
|
39
|
+
const rawWidth = width;
|
|
40
|
+
const imageAttrs: Record<string, unknown> = { src: resolvedSrc };
|
|
41
|
+
if (typeof alt === "string") {
|
|
42
|
+
imageAttrs.alt = alt;
|
|
43
|
+
}
|
|
44
|
+
if (width !== undefined) {
|
|
45
|
+
imageAttrs.width = width;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const zoomAttrs: Record<string, unknown> = { src: resolvedSrc };
|
|
49
|
+
if (typeof alt === "string") {
|
|
50
|
+
zoomAttrs.alt = alt;
|
|
17
51
|
}
|
|
18
|
-
const rawStyle = attrsRecord.style;
|
|
19
|
-
const rawWidth = attrsRecord.width;
|
|
20
|
-
const zoomAttrs: Record<string, unknown> = { ...attrsRecord };
|
|
21
|
-
delete zoomAttrs.style;
|
|
22
|
-
delete zoomAttrs.width;
|
|
23
|
-
delete zoomAttrs.height;
|
|
24
|
-
delete zoomAttrs.class;
|
|
25
|
-
delete zoomAttrs.className;
|
|
26
52
|
|
|
27
53
|
function isConstrainedWidthValue(value: unknown): boolean {
|
|
28
54
|
if (typeof value === "number") {
|
|
@@ -55,48 +81,12 @@ function isConstrainedWidthValue(value: unknown): boolean {
|
|
|
55
81
|
return true;
|
|
56
82
|
}
|
|
57
83
|
|
|
58
|
-
|
|
59
|
-
if (typeof styleValue === "string") {
|
|
60
|
-
const widthMatch = styleValue.match(/(?:^|;)\s*width\s*:\s*([^;]+)/i);
|
|
61
|
-
const maxWidthMatch = styleValue.match(
|
|
62
|
-
/(?:^|;)\s*max-width\s*:\s*([^;]+)/i,
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
isConstrainedWidthValue(widthMatch?.[1]) ||
|
|
67
|
-
isConstrainedWidthValue(maxWidthMatch?.[1])
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!styleValue || typeof styleValue !== "object") return false;
|
|
72
|
-
const styleObject = styleValue as Record<string, unknown>;
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
isConstrainedWidthValue(styleObject.width) ||
|
|
76
|
-
isConstrainedWidthValue(styleObject.maxWidth) ||
|
|
77
|
-
isConstrainedWidthValue(styleObject["max-width"])
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const hasCustomImageWidth =
|
|
82
|
-
isConstrainedWidthValue(rawWidth) || hasStyleWidthConstraint(rawStyle);
|
|
84
|
+
const hasCustomImageWidth = isConstrainedWidthValue(rawWidth);
|
|
83
85
|
|
|
84
86
|
const captionHtml =
|
|
85
87
|
typeof title === "string" && title.trim().length > 0
|
|
86
88
|
? (await renderMarkdown(title)).replace(/^<p>([\s\S]*)<\/p>\n?$/, "$1")
|
|
87
89
|
: "";
|
|
88
|
-
|
|
89
|
-
validateProps(
|
|
90
|
-
"Image",
|
|
91
|
-
Astro.props as Record<string, any>,
|
|
92
|
-
{
|
|
93
|
-
src: { required: true, type: "string" },
|
|
94
|
-
alt: { type: "string" },
|
|
95
|
-
title: { type: "string" },
|
|
96
|
-
zoom: { type: "boolean" },
|
|
97
|
-
},
|
|
98
|
-
Astro.url.pathname,
|
|
99
|
-
);
|
|
100
90
|
---
|
|
101
91
|
|
|
102
92
|
<figure
|
|
@@ -207,7 +197,7 @@ validateProps(
|
|
|
207
197
|
class="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-(--rd-code-surface)"
|
|
208
198
|
>
|
|
209
199
|
<img
|
|
210
|
-
{...
|
|
200
|
+
{...imageAttrs}
|
|
211
201
|
x-ref="img"
|
|
212
202
|
title={title}
|
|
213
203
|
class:list={[
|
|
@@ -7,11 +7,18 @@ import googleSansLatinExtWoff2 from "../assets/fonts/google-sans-flex/latin-ext.
|
|
|
7
7
|
import geistMonoLatinWoff2 from "../assets/fonts/geist-mono/latin.woff2?url";
|
|
8
8
|
import geistMonoLatinExtWoff2 from "../assets/fonts/geist-mono/latin-ext.woff2?url";
|
|
9
9
|
import Sidebar from "../components/Sidebar.astro";
|
|
10
|
-
import
|
|
10
|
+
import colors from "tailwindcss/colors";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_THEME_COLOR_DARK,
|
|
13
|
+
DEFAULT_THEME_COLOR_LIGHT,
|
|
14
|
+
getConfig,
|
|
15
|
+
type BaseColorOption,
|
|
16
|
+
} from "../lib/validation";
|
|
11
17
|
import Header from "../components/Header.astro";
|
|
12
18
|
import Footer from "../components/Footer.astro";
|
|
13
19
|
import AskAiWidget from "../components/chat/AskAiWidget";
|
|
14
20
|
import { ClientRouter } from "astro:transitions";
|
|
21
|
+
import { prependBasePath, stripBasePath, withBasePath } from "../lib/base-path";
|
|
15
22
|
import { resolveStaticAssetUrl } from "../lib/static-asset-url";
|
|
16
23
|
|
|
17
24
|
interface Props {
|
|
@@ -36,6 +43,117 @@ function routePathToOgImagePath(routePath: string): string {
|
|
|
36
43
|
|
|
37
44
|
const { pageTitle, pageDescription } = Astro.props as Props;
|
|
38
45
|
const config = await getConfig();
|
|
46
|
+
const themeBaseColor = config.theme?.baseColor ?? "neutral";
|
|
47
|
+
const themeThemeColor = config.theme?.themeColor;
|
|
48
|
+
const lightBaseColor: BaseColorOption =
|
|
49
|
+
typeof themeBaseColor === "string" ? themeBaseColor : themeBaseColor.light;
|
|
50
|
+
const darkBaseColor: BaseColorOption =
|
|
51
|
+
typeof themeBaseColor === "string" ? themeBaseColor : themeBaseColor.dark;
|
|
52
|
+
const lightThemeColor =
|
|
53
|
+
typeof themeThemeColor === "string"
|
|
54
|
+
? themeThemeColor
|
|
55
|
+
: (themeThemeColor?.light ?? DEFAULT_THEME_COLOR_LIGHT);
|
|
56
|
+
const darkThemeColor =
|
|
57
|
+
typeof themeThemeColor === "string"
|
|
58
|
+
? themeThemeColor
|
|
59
|
+
: (themeThemeColor?.dark ?? DEFAULT_THEME_COLOR_DARK);
|
|
60
|
+
const neutralColorShades = [
|
|
61
|
+
"50",
|
|
62
|
+
"100",
|
|
63
|
+
"200",
|
|
64
|
+
"300",
|
|
65
|
+
"400",
|
|
66
|
+
"500",
|
|
67
|
+
"600",
|
|
68
|
+
"700",
|
|
69
|
+
"800",
|
|
70
|
+
"900",
|
|
71
|
+
"950",
|
|
72
|
+
] as const;
|
|
73
|
+
const paletteColors = colors as unknown as Record<
|
|
74
|
+
string,
|
|
75
|
+
Record<(typeof neutralColorShades)[number], string>
|
|
76
|
+
>;
|
|
77
|
+
const getNeutralPaletteVariables = (baseColor: BaseColorOption): string[] => {
|
|
78
|
+
const palette = paletteColors[baseColor] ?? paletteColors.neutral;
|
|
79
|
+
return [
|
|
80
|
+
`--color-neutral: ${palette["500"]};`,
|
|
81
|
+
...neutralColorShades.map(
|
|
82
|
+
(shade) => `--color-neutral-${shade}: ${palette[shade]};`,
|
|
83
|
+
),
|
|
84
|
+
];
|
|
85
|
+
};
|
|
86
|
+
function normalizeHexColorToRgb(
|
|
87
|
+
hexColor: string,
|
|
88
|
+
): { r: number; g: number; b: number } | null {
|
|
89
|
+
const normalized = hexColor.replace("#", "").trim();
|
|
90
|
+
if (/^[a-fA-F0-9]{3,4}$/.test(normalized)) {
|
|
91
|
+
const [r, g, b] = normalized.split("");
|
|
92
|
+
if (!r || !g || !b) return null;
|
|
93
|
+
return {
|
|
94
|
+
r: Number.parseInt(`${r}${r}`, 16),
|
|
95
|
+
g: Number.parseInt(`${g}${g}`, 16),
|
|
96
|
+
b: Number.parseInt(`${b}${b}`, 16),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (/^[a-fA-F0-9]{6,8}$/.test(normalized)) {
|
|
101
|
+
return {
|
|
102
|
+
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
103
|
+
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
104
|
+
b: Number.parseInt(normalized.slice(4, 6), 16),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getThemeForegroundColor(hexColor: string): "#ffffff" | "#111827" {
|
|
112
|
+
const rgb = normalizeHexColorToRgb(hexColor);
|
|
113
|
+
if (!rgb) return "#ffffff";
|
|
114
|
+
|
|
115
|
+
const toLinear = (channel: number): number => {
|
|
116
|
+
const normalized = channel / 255;
|
|
117
|
+
return normalized <= 0.03928
|
|
118
|
+
? normalized / 12.92
|
|
119
|
+
: ((normalized + 0.055) / 1.055) ** 2.4;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const luminance =
|
|
123
|
+
0.2126 * toLinear(rgb.r) +
|
|
124
|
+
0.7152 * toLinear(rgb.g) +
|
|
125
|
+
0.0722 * toLinear(rgb.b);
|
|
126
|
+
return luminance > 0.45 ? "#111827" : "#ffffff";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const getThemeColorVariables = (themeColor: string): string[] => {
|
|
130
|
+
const foreground = getThemeForegroundColor(themeColor);
|
|
131
|
+
const iconFilter = foreground === "#ffffff" ? "invert(1)" : "none";
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
`--color-theme: ${themeColor};`,
|
|
135
|
+
`--color-theme-foreground: ${foreground};`,
|
|
136
|
+
`--color-theme-icon-filter: ${iconFilter};`,
|
|
137
|
+
"--color-theme-top: color-mix(in oklab, var(--color-theme) 88%, white);",
|
|
138
|
+
"--color-theme-bottom: color-mix(in oklab, var(--color-theme) 90%, black);",
|
|
139
|
+
];
|
|
140
|
+
};
|
|
141
|
+
const lightNeutralVariables = getNeutralPaletteVariables(lightBaseColor);
|
|
142
|
+
const darkNeutralVariables = getNeutralPaletteVariables(darkBaseColor);
|
|
143
|
+
const lightThemeColorVariables = getThemeColorVariables(lightThemeColor);
|
|
144
|
+
const darkThemeColorVariables = getThemeColorVariables(darkThemeColor);
|
|
145
|
+
const neutralPaletteCss = [
|
|
146
|
+
"html[data-theme='light'], html:not(.dark):not([data-theme='dark']) {",
|
|
147
|
+
...[...lightNeutralVariables, ...lightThemeColorVariables].map(
|
|
148
|
+
(declaration) => ` ${declaration.replace(/;$/, " !important;")}`,
|
|
149
|
+
),
|
|
150
|
+
"}",
|
|
151
|
+
"html.dark, html[data-theme='dark'] {",
|
|
152
|
+
...[...darkNeutralVariables, ...darkThemeColorVariables].map(
|
|
153
|
+
(declaration) => ` ${declaration.replace(/;$/, " !important;")}`,
|
|
154
|
+
),
|
|
155
|
+
"}",
|
|
156
|
+
].join("\n");
|
|
39
157
|
const resolvedPageTitle = pageTitle?.trim();
|
|
40
158
|
const resolvedPageDescription =
|
|
41
159
|
typeof pageDescription === "string" && pageDescription.trim().length > 0
|
|
@@ -48,12 +166,13 @@ const resolvedMetaDescription = resolvedPageDescription ?? fallbackDescription;
|
|
|
48
166
|
const documentTitle = resolvedPageTitle
|
|
49
167
|
? `${resolvedPageTitle} | ${config.title}`
|
|
50
168
|
: `${config.title} Docs`;
|
|
169
|
+
const routePathname = stripBasePath(Astro.url.pathname);
|
|
51
170
|
const canonicalUrl = new URL(
|
|
52
|
-
|
|
171
|
+
prependBasePath(routePathname),
|
|
53
172
|
Astro.site ?? Astro.url,
|
|
54
173
|
).toString();
|
|
55
174
|
const ogImageUrl = new URL(
|
|
56
|
-
resolveStaticAssetUrl(routePathToOgImagePath(
|
|
175
|
+
resolveStaticAssetUrl(routePathToOgImagePath(routePathname)),
|
|
57
176
|
Astro.site ?? Astro.url,
|
|
58
177
|
);
|
|
59
178
|
const ogImageHref = ogImageUrl.toString();
|
|
@@ -64,21 +183,21 @@ const parsedOrgTier = Number.parseInt(
|
|
|
64
183
|
const orgTier =
|
|
65
184
|
Number.isFinite(parsedOrgTier) && parsedOrgTier > 0 ? parsedOrgTier : 1;
|
|
66
185
|
const isDev = import.meta.env.DEV;
|
|
67
|
-
const askAiDevHost = (import.meta.env.ASK_AI_DEV_HOST ?? "").toString().trim();
|
|
68
|
-
const askAiDevProxySecret = (import.meta.env.ASK_AI_DEV_PROXY_SECRET ?? "")
|
|
186
|
+
const askAiDevHost = (import.meta.env.ASK_AI_DEV_HOST ?? "s").toString().trim();
|
|
187
|
+
const askAiDevProxySecret = (import.meta.env.ASK_AI_DEV_PROXY_SECRET ?? "s")
|
|
69
188
|
.toString()
|
|
70
189
|
.trim();
|
|
71
190
|
const hasAskAiDevConfig =
|
|
72
191
|
askAiDevHost.length > 0 && askAiDevProxySecret.length > 0;
|
|
73
192
|
const askAiEnabled = isDev || orgTier >= 3;
|
|
74
193
|
const askAiChatAvailable = isDev ? hasAskAiDevConfig : orgTier >= 3;
|
|
75
|
-
let askAiApiPath = "/_platform/ask-ai";
|
|
194
|
+
let askAiApiPath = withBasePath("/_platform/ask-ai");
|
|
76
195
|
|
|
77
196
|
if (isDev && hasAskAiDevConfig) {
|
|
78
197
|
try {
|
|
79
198
|
askAiApiPath = new URL("/_platform/ask-ai", askAiDevHost).toString();
|
|
80
199
|
} catch {
|
|
81
|
-
askAiApiPath = "/_platform/ask-ai";
|
|
200
|
+
askAiApiPath = withBasePath("/_platform/ask-ai");
|
|
82
201
|
}
|
|
83
202
|
}
|
|
84
203
|
---
|
|
@@ -86,6 +205,7 @@ if (isDev && hasAskAiDevConfig) {
|
|
|
86
205
|
<!doctype html>
|
|
87
206
|
<html lang="en">
|
|
88
207
|
<head>
|
|
208
|
+
<style is:inline is:global set:html={neutralPaletteCss}></style>
|
|
89
209
|
<ClientRouter />
|
|
90
210
|
<script is:inline>
|
|
91
211
|
const applyTheme = () => {
|
|
@@ -115,6 +235,96 @@ if (isDev && hasAskAiDevConfig) {
|
|
|
115
235
|
// Run on initial load
|
|
116
236
|
applyTheme();
|
|
117
237
|
</script>
|
|
238
|
+
<script is:inline>
|
|
239
|
+
(() => {
|
|
240
|
+
const isEmbedded = window.self !== window.top;
|
|
241
|
+
const isEmbedMode =
|
|
242
|
+
new URLSearchParams(window.location.search).get("embed") === "1";
|
|
243
|
+
|
|
244
|
+
if (!isEmbedMode || !isEmbedded) {
|
|
245
|
+
if (isEmbedMode && !isEmbedded) {
|
|
246
|
+
console.info("[iframe-ready] skipping: not embedded iframe");
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.info("[iframe-ready] init", {
|
|
252
|
+
href: window.location.href,
|
|
253
|
+
embedded: isEmbedded,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const readySentSessionKey = "radiant_docs_iframe_ready_sent";
|
|
257
|
+
const hasSentReady = () => {
|
|
258
|
+
try {
|
|
259
|
+
const sent = sessionStorage.getItem(readySentSessionKey) === "1";
|
|
260
|
+
console.info("[iframe-ready] session flag read", {
|
|
261
|
+
key: readySentSessionKey,
|
|
262
|
+
sent,
|
|
263
|
+
});
|
|
264
|
+
return sent;
|
|
265
|
+
} catch {
|
|
266
|
+
console.warn(
|
|
267
|
+
"[iframe-ready] failed to read sessionStorage flag",
|
|
268
|
+
readySentSessionKey,
|
|
269
|
+
);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const markReadySent = () => {
|
|
274
|
+
try {
|
|
275
|
+
sessionStorage.setItem(readySentSessionKey, "1");
|
|
276
|
+
console.info("[iframe-ready] session flag set", {
|
|
277
|
+
key: readySentSessionKey,
|
|
278
|
+
value: "1",
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore storage failures (privacy mode, disabled storage, etc.)
|
|
282
|
+
console.warn(
|
|
283
|
+
"[iframe-ready] failed to set sessionStorage flag",
|
|
284
|
+
readySentSessionKey,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const notifyParentReady = () => {
|
|
290
|
+
const readyUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
291
|
+
console.info("[iframe-ready] posting IFRAME_READY", { readyUrl });
|
|
292
|
+
window.parent.postMessage(
|
|
293
|
+
{
|
|
294
|
+
type: "IFRAME_READY",
|
|
295
|
+
url: readyUrl,
|
|
296
|
+
},
|
|
297
|
+
"*",
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const handleReadyRequest = (event) => {
|
|
302
|
+
const payload = event.data;
|
|
303
|
+
if (!payload || typeof payload !== "object") return;
|
|
304
|
+
|
|
305
|
+
if (payload.type === "IFRAME_READY_REQUEST") {
|
|
306
|
+
console.info("[iframe-ready] received IFRAME_READY_REQUEST");
|
|
307
|
+
notifyParentReady();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
window.addEventListener("message", handleReadyRequest);
|
|
312
|
+
|
|
313
|
+
if (!hasSentReady()) {
|
|
314
|
+
const notifyParentReadyOnce = () => {
|
|
315
|
+
if (hasSentReady()) return;
|
|
316
|
+
markReadySent();
|
|
317
|
+
notifyParentReady();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (document.readyState === "complete") notifyParentReadyOnce();
|
|
321
|
+
else
|
|
322
|
+
window.addEventListener("load", notifyParentReadyOnce, {
|
|
323
|
+
once: true,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
</script>
|
|
118
328
|
<link
|
|
119
329
|
rel="preload"
|
|
120
330
|
href={googleSansLatinWoff2}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
// Set by astro.config.mjs so build-time MDX remark plugins can see the
|
|
5
|
+
// configured base before Astro injects import.meta.env.BASE_URL.
|
|
6
|
+
var __RADIANT_DOCS_BASE_PATH__: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeBasePath(value: string | null | undefined): string {
|
|
10
|
+
const trimmed = value?.trim() ?? "";
|
|
11
|
+
if (!trimmed || trimmed === "/") return "";
|
|
12
|
+
|
|
13
|
+
const pathname = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
14
|
+
const normalized = pathname.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
|
|
15
|
+
return normalized === "/" ? "" : normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getDocsBasePath(): string {
|
|
19
|
+
const astroBasePath = normalizeBasePath(import.meta.env.BASE_URL);
|
|
20
|
+
return (
|
|
21
|
+
astroBasePath ||
|
|
22
|
+
normalizeBasePath(globalThis.__RADIANT_DOCS_BASE_PATH__)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function splitPathSuffix(value: string): { pathname: string; suffix: string } {
|
|
27
|
+
const match = value.match(/^([^?#]*)(.*)$/);
|
|
28
|
+
return {
|
|
29
|
+
pathname: match?.[1] ?? value,
|
|
30
|
+
suffix: match?.[2] ?? "",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isExternalOrDocumentLocalHref(value: string): boolean {
|
|
35
|
+
if (!value) return true;
|
|
36
|
+
if (value.startsWith("#") || value.startsWith("?")) return true;
|
|
37
|
+
if (value.startsWith("//")) return true;
|
|
38
|
+
if (value.startsWith("./") || value.startsWith("../")) return true;
|
|
39
|
+
return EXTERNAL_PROTOCOL_REGEX.test(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyBasePath(
|
|
43
|
+
href: string,
|
|
44
|
+
options: { preserveAlreadyPrefixed: boolean },
|
|
45
|
+
): string {
|
|
46
|
+
const value = href.trim();
|
|
47
|
+
if (!value || isExternalOrDocumentLocalHref(value)) {
|
|
48
|
+
return href;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const basePath = getDocsBasePath();
|
|
52
|
+
if (!basePath) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { pathname, suffix } = splitPathSuffix(value);
|
|
57
|
+
const normalizedPathname = pathname.startsWith("/")
|
|
58
|
+
? pathname
|
|
59
|
+
: `/${pathname}`;
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
options.preserveAlreadyPrefixed &&
|
|
63
|
+
(normalizedPathname === basePath ||
|
|
64
|
+
normalizedPathname.startsWith(`${basePath}/`))
|
|
65
|
+
) {
|
|
66
|
+
return `${normalizedPathname}${suffix}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (normalizedPathname === "/") {
|
|
70
|
+
return `${basePath}${suffix}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `${basePath}${normalizedPathname}${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function withBasePath(href: string): string {
|
|
77
|
+
return applyBasePath(href, { preserveAlreadyPrefixed: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function prependBasePath(href: string): string {
|
|
81
|
+
return applyBasePath(href, { preserveAlreadyPrefixed: false });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function stripBasePath(pathname: string): string {
|
|
85
|
+
const value = pathname || "/";
|
|
86
|
+
const basePath = getDocsBasePath();
|
|
87
|
+
if (!basePath) return value;
|
|
88
|
+
|
|
89
|
+
if (value === basePath) {
|
|
90
|
+
return "/";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value.startsWith(`${basePath}/`)) {
|
|
94
|
+
return value.slice(basePath.length) || "/";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
@@ -85,27 +85,33 @@ export function validateType(
|
|
|
85
85
|
componentName: string,
|
|
86
86
|
propName: string,
|
|
87
87
|
value: unknown,
|
|
88
|
-
expectedType:
|
|
88
|
+
expectedType:
|
|
89
|
+
| "string"
|
|
90
|
+
| "number"
|
|
91
|
+
| "boolean"
|
|
92
|
+
| "object"
|
|
93
|
+
| "array"
|
|
94
|
+
| readonly ("string" | "number" | "boolean" | "object" | "array")[],
|
|
89
95
|
pathname: string
|
|
90
96
|
): void {
|
|
91
97
|
// Skip if undefined (optional props)
|
|
92
98
|
if (value === undefined) return;
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
100
|
+
const expectedTypes = Array.isArray(expectedType)
|
|
101
|
+
? expectedType
|
|
102
|
+
: [expectedType];
|
|
103
|
+
const isValid = expectedTypes.some((type) =>
|
|
104
|
+
type === "array" ? Array.isArray(value) : typeof value === type
|
|
105
|
+
);
|
|
101
106
|
|
|
102
107
|
if (!isValid) {
|
|
103
108
|
const sourceFile = getSourceFile(pathname);
|
|
104
109
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
110
|
+
const expectedTypeLabel = expectedTypes.join(" or ");
|
|
105
111
|
throw new Error(
|
|
106
112
|
formatError(
|
|
107
113
|
componentName,
|
|
108
|
-
`Invalid prop "${propName}": expected ${
|
|
114
|
+
`Invalid prop "${propName}": expected ${expectedTypeLabel}, got ${actualType}`,
|
|
109
115
|
sourceFile
|
|
110
116
|
)
|
|
111
117
|
);
|
|
@@ -123,10 +129,43 @@ export function validateType(
|
|
|
123
129
|
*/
|
|
124
130
|
export type PropSchema = {
|
|
125
131
|
required?: boolean;
|
|
126
|
-
type?:
|
|
132
|
+
type?:
|
|
133
|
+
| "string"
|
|
134
|
+
| "number"
|
|
135
|
+
| "boolean"
|
|
136
|
+
| "object"
|
|
137
|
+
| "array"
|
|
138
|
+
| readonly ("string" | "number" | "boolean" | "object" | "array")[];
|
|
127
139
|
enum?: readonly string[];
|
|
128
140
|
};
|
|
129
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Validates that no unsupported props are passed to a component.
|
|
144
|
+
*/
|
|
145
|
+
export function validateNoUnknownProps(
|
|
146
|
+
componentName: string,
|
|
147
|
+
props: Record<string, unknown>,
|
|
148
|
+
allowedProps: readonly string[],
|
|
149
|
+
pathname: string
|
|
150
|
+
): void {
|
|
151
|
+
const allowed = new Set(allowedProps);
|
|
152
|
+
const unknownProps = Object.keys(props).filter((key) => !allowed.has(key));
|
|
153
|
+
|
|
154
|
+
if (unknownProps.length === 0) return;
|
|
155
|
+
|
|
156
|
+
const sourceFile = getSourceFile(pathname);
|
|
157
|
+
const unknownLabel = unknownProps.map((name) => `"${name}"`).join(", ");
|
|
158
|
+
const allowedLabel = allowedProps.map((name) => `"${name}"`).join(", ");
|
|
159
|
+
const propLabel = unknownProps.length === 1 ? "prop" : "props";
|
|
160
|
+
throw new Error(
|
|
161
|
+
formatError(
|
|
162
|
+
componentName,
|
|
163
|
+
`Unsupported ${propLabel}: ${unknownLabel}. Allowed props: ${allowedLabel}`,
|
|
164
|
+
sourceFile
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
130
169
|
export function validateProps(
|
|
131
170
|
componentName: string,
|
|
132
171
|
props: Record<string, unknown>,
|